*! spider v1.31 (11 May 2024)
*! Asjad Naqvi (asjadnaqvi@gmail.com)

* v1.31	(11 May 2024): changed raformat() to format(). Format default improved. by() and over() error checks added. passthru changed to *.
* v1.3	(16 Feb 2024): rewrite to suport long form, add rotate label, better legend controls.
* v1.23 (12 Nov 2023): Added slabcolor(), saving()
* v1.22 (03 Jul 2023): Fixed bug with numerical variables not passing correctly.
* v1.21 (10 Jun 2023): Options added: ralabcolor() ralabangle().
* v1.2  (20 May 2023): Major fixes to the legend.
* v1.1  (22 Dec 2022): Minor fixes.
* v1.0  (13 Oct 2022): Beta release.

**********************************
* Step-by-step guide on Medium   *
**********************************

* Based on this guide:
* Spider plots (26 Jan, 2021) https://medium.com/the-stata-guide/stata-graphs-spider-plots-613808b51f73

cap program drop spider

program spider, sortpreserve

version 15

	syntax varlist(numeric max=1) [if] [in], by(varname) ///
		[ over(varname) alpha(real 10) ROtate(real 30) DISPLACELab(real 15) DISPLACESpike(real 2)  palette(string) 				] ///
		[ RAnge(numlist min=2 max=2) cuts(real 6) smooth(numlist max=1 >=0 <=1) format(string)  RALABSize(string) ] ///
		[ LWidth(string) MSYMbol(string) MSize(string) MLWIDth(string)  											] /// // spider properties
		[ CColor(string) CWidth(string)	SColor(string) SWidth(string) SLABSize(string)								] /// // circle = C, spikes = S
		[ NOLEGend LEGPOSition(real 6) LEGCOLumns(real 3) LEGSize(real 2.2)  ] ///  // v1.2 updates.
		[ RALABColor(string) RALABAngle(string) SLABColor(string) ROTATELABel ] ///  // v1.2X options
		[ xsize(real 1) ysize(real 1) * ]


	// check dependencies
	cap findfile colorpalette.ado
	if _rc != 0 {
		display as error "The palettes package is missing. Install the {stata ssc install palettes, replace:palettes} and {stata ssc install colrspace, replace:colrspace} packages."
		exit
	}


	cap findfile labmask.ado
	if _rc != 0 {
		qui ssc install labutil, replace
	}

	marksample touse, strok


qui {
preserve
	keep if `touse'
	keep `varlist' `by' `over'

	levelsof `by'
	if r(r) < 3 {
		display as error "by() variable should contain at least three categories."
		exit
	}


	cap confirm numeric var `by'

	if _rc!=0 {  // if string
		encode `by', gen(_by)
		local by _by
	}
	else {
		egen _by = group(`by')
		if "`: value label `by''" != "" {
			tempvar tempov
			decode  `by', gen(`tempov')
			labmask _by , val(`tempov')
		}
		local by _by
	}


	if "`over'" == "" {
		gen _over = 1
		local over _over
		local overswitch = 1
	}
	else {

		levelsof `over'

		if r(r) < 2 {
			display as error "over() variable should contain at least two categories."
			exit
		}

		local overswitch = 0
		cap confirm numeric var `over'

		if _rc!=0 {  // if string
			encode `over', gen(_over)
			local over _over
		}
		else {
			egen _over = group(`over')
			if "`: value label `over''" != "" {
				tempvar tempov
				decode `over', gen(`tempov')
				labmask _over, val(`tempov')
			}
			local over _over
		}
	}

	collapse (mean) `varlist', by(`by' `over')

	sum `varlist', meanonly
	local varmin = r(min)
	local varmax = r(max)


	/////////////////////
	// set the scales  //
	/////////////////////

	if "`range'" != "" {
		tokenize `range'
		local norm1 `1'
		local norm2 `2'

	}
	else {

		local disp = (`varmax' - `varmin') * 0.1  // displace minmax by 10%

		local norm1 = `varmin' - `disp'
		local norm2 = `varmax' + `disp'
	}


	// rescale variables between [0,100] from their global min/max. This ensures that negative numbers are there as well
		replace `varlist' = ((`varlist' - `norm1') / (`norm2' - `norm1')) * 100


	sort `over' `by'
	bysort `over': gen _seq = _n

	levelsof `by'
	local items = r(r)

	local ro = `rotate' * _pi / 180
	gen double angle = (_seq * 2 * _pi / `items') + `ro'


	//////////////////////////
	// generate the points  //
	//////////////////////////

		gen double x = `varlist' * cos(angle)
		gen double y = `varlist' * sin(angle)

		sort _over _by

		if "`smooth'" != "" {

			*if _N < 50 set obs 50 // generate the points

			levelsof `over', local(lvls)

			foreach x of local lvls {

				cap drop _m

				smoother y x if `over'==`x' , rho(`smooth')
				ren Cx x`x'_pts   //
				ren Cy y`x'_pts   //

			}
		}



	/////////////////
	//   circles   //
	/////////////////


	if "`ccolor'" == "" local ccolor gs12
	if "`cwidth'" == "" local cwidth 0.1

	// local sides = cond("`polygon'" == "",  300, `items')   // deal with later

	local gap = floor((100 - 0) / (`cuts' - 1))


	if `gap' > _N {
		local newobs = `gap' + 1
		set obs `newobs'
	}


	forval x = 0(`gap')100 {
		local circle `circle' (function sqrt(`x'^2 - x^2), lc(`ccolor') lw(`cwidth') lp(solid) range(-`x' `x') n(`sides')) || (function -sqrt(`x'^2 - x^2), lc(`ccolor') lw(`cwidth') lp(solid) range(-`x' `x')  n(`sides')) ||

		}

	///////////////////////
	//   circle labels   //
	///////////////////////


	if "`format'" == "" local format %12.1f

	local gap2 = (`norm2' - `norm1') / (`cuts' - 1)

	gen xvar = .
	gen yvar = .
	gen xlab = ""

	local i = 1

	forval x = 0(`gap')100 {
		replace xlab = string(`norm1' + ((`i' - 1) * `gap2'), "`format'")  in `i'
		replace xvar =  `x'  in `i'
		replace yvar =  0  in `i'

		local i = `i' + 1
	}


	/////////////////
	//   spikes	   //
	/////////////////

	if "`scolor'" == "" local scolor gs12
	if "`swidth'" == "" local swidth 0.1


	gen double spikes = _n in 1/`items'

	forval x = 1/`items' {
		local theta = `x' * 2 * _pi / `items'
		local liner = (100 + `displacespike') * cos(`theta' + `ro')

		local spike `spike' (function (tan(`theta' + `ro'))*x, n(2) range(0 `liner') lw(`swidth') lc(`scolor') lp(solid)) ||

		}


	/////////////////
	//   labels	   //
	/////////////////

	if "`slabsize'"  == "" local slabsize 2.2
	if "`slabcolor'" == "" local slabcolor black

	gen double markerx = .
	gen double markery = .
	gen 	 markerlab = ""

	levelsof `by'
	local byitems = r(r)

	forval i = 1/`byitems' {

		replace markerx   = (100 + `displacelab') * cos(angle) in `i'
		replace markery   = (100 + `displacelab') * sin(angle) in `i'

		local varn : label `by' `i'
		replace markerlab = "`varn'" in `i'

			if "`rotatelabel'" != "" {
				sum   angle in `i', meanonly
				local angle = (r(mean)  * (180 / _pi)) - 90
			}


		local labs `labs' (scatter markery markerx in `i', mc(none) mlab("markerlab") mlabpos(0) mlabcolor(`slabcolor') mlabangle(`angle')  mlabsize(`slabsize'))  ///  //



	}

	/////////////////
	//   spiders   //
	/////////////////


	if "`lwidth'"  == "" local lwidth  = 0.3
	if "`msymbol'" == "" local msymbol circle
	if "`msize'"   == "" local msize   = 0.3
	if "`mlwidth'" == "" local mlwidth = 0.3
	if "`palette'" == "" {
		local palette tableau
	}
	else {
		tokenize "`palette'", p(",")
		local palette  `1'
		local poptions `3'
	}

	qui levelsof `over'
	local items = r(r)

	forval i = 1/`items' {


		colorpalette `palette', nograph `poptions'

		if "`smooth'" == "" {
			local spider  `spider'  (area y x if `over'==`i'    , nodropbase fi(100) fcolor("`r(p`i')'%`alpha'") lc("`r(p`i')'") lw(`lwidth'))

		}
		else {
			local spider  `spider'  (area y`i'_pts x`i'_pts, nodropbase fi(100) fcolor("`r(p`i')'%`alpha'") lc("`r(p`i')'") lw(`lwidth'))

		}


		local spider2 `spider2' (scatter y x if `over'==`i' , msize(`msize') mc("`r(p`i')'") msymbol("`msymbol'") mlwidth(`mlwidth'))


	}


	/////////////////
	//   legend	   //
	/////////////////


	if "`nolegend'" != "" | `overswitch' == 1  {
		local mylegend legend(off)

	}
	else {

		forval i = 1/`items' {

			local j = `i' + (`cuts' * 2) + `byitems'
			local varn : label `over' `i'

			local entries `" `entries' `j'  "`varn'"  "'
		}


		local mylegend legend(order("`entries'") pos(`legposition') size(`legsize') col(`legcolumns'))

	}


	//////////////////////
	//   final graph	//
	//////////////////////

	local axisr = 100 * (1.3)
	if "`ralabsize'"  == "" local ralabsize = 1.8
	if "`ralabcolor'" == "" local ralabcolor black
	if "`ralabangle'" == "" local ralabangle 0

    twoway	///
			`circle' 	///
			`spike'	 	///
			`spider'	///
			`spider2'	///
			`labs' 		///
			(scatter yvar xvar, mc(none) mlab(xlab) mlabpos(0) mlabsize(`ralabsize') mlabcolor(`ralabcolor') mlabangle(`ralabangle') )  ///
						,    ///
						aspect(1) xsize(`xsize') ysize(`ysize') ///
						xlabel(-`axisr' `axisr') ///
						ylabel(-`axisr' `axisr') ///
							xscale(off) yscale(off)	///
							xlabel(, nogrid) ylabel(, nogrid) ///
							`mylegend' ///
							`options'



	*/

restore
}


end


*********************************
******* subroutines here ********
*********************************

cap program drop smoother

program smoother, sortpreserve

version 15

	syntax varlist(numeric min=2 max=2) [if] [in], [ rho(real 0.5) obs(real 50) ]

	marksample touse, strok

	tokenize `varlist'
	local vary `1'
	local varx `2'


preserve

	keep if `touse'

	cap drop _id
	keep `varlist'

	noi list `varlist'

	drop if `varx'==.
	gen pts = _n
	order pts

	set obs `obs'

	levelsof pts, local(points)
	local last = r(r)

	foreach x of local points {

	// define the four points based on positioning

			local pt0 = `x'
			local pt1 = `x' + 1
			local pt2 = `x' + 2
			local pt3 = `x' + 3

			if `pt1' > `last' {
				local pt1 = `pt1' - `last'
			}

			if `pt2' > `last' {
				local pt2 = `pt2' - `last'
			}

			if `pt3' > `last' {
				local pt3 = `pt3' - `last'
			}

		// control points indexed 0, 1, 2, 3

		forval i = 0/3 {
			qui summ `varx' if pts==`pt`i'', meanonly
			local x`i' = r(mean)

			qui summ `vary' if pts==`pt`i'', meanonly
			local y`i' = r(mean)
		}


		// generate the ts
		tempvar t0 t1 t2 t3

		gen `t0' = 0
		gen `t1' = (((`x1' - `x0')^2 + (`y1' - `y0')^2)^`rho') + `t0'
		gen `t2' = (((`x2' - `x1')^2 + (`y2' - `y1')^2)^`rho') + `t1'
		gen `t3' = (((`x3' - `x2')^2 + (`y3' - `y2')^2)^`rho') + `t2'

		gen t`x' = runiform(`t1',`t2')

			**** calculate the As

		forval i = 1/3 {
			local j = `i' - 1

			tempvar A`i'x A`i'y

			gen `A`i'x' = (((`t`i'' - t`x') / (`t`i'' - `t`j'')) * `x`j'') + (((t`x' - `t`j'') / (`t`i'' - `t`j'')) * `x`i'')
			gen `A`i'y' = (((`t`i'' - t`x') / (`t`i'' - `t`j'')) * `y`j'') + (((t`x' - `t`j'') / (`t`i'' - `t`j'')) * `y`i'')

		}

		**** calculate the Bs

		forval i = 1/2 {
			local j = `i' - 1
			local k = `i' + 1

			tempvar B`i'x B`i'y

			gen `B`i'x' = (((`t`k'' - t`x') / (`t`k'' - `t`j'')) * `A`i'x') + (((t`x' - `t`j'') / (`t`k'' - `t`j'')) * `A`k'x')
			gen `B`i'y' = (((`t`k'' - t`x') / (`t`k'' - `t`j'')) * `A`i'y') + (((t`x' - `t`j'') / (`t`k'' - `t`j'')) * `A`k'y')

		}

		**** calculate the Cs

		gen double Cx`x' = (((`t2' - t`x') / (`t2' - `t1')) * `B1x') + (((t`x' - `t1') / (`t2' - `t1')) * `B2x')
		gen double Cy`x' = (((`t2' - t`x') / (`t2' - `t1')) * `B1y') + (((t`x' - `t1') / (`t2' - `t1')) * `B2y')

	}


	cap drop `varlist' pts

	gen id = _n
	reshape long Cx Cy t, i(id) j(spline)
	sort spline t

	*list Cy Cy spline

	drop id spline t
	gen _id = _n
	order _id



	tempfile mysplines
	save `mysplines', replace

restore

	cap confirm var _id
	if _rc!=0 gen _id = .
	cap drop _merge
	merge m:1 _id using `mysplines'
	cap drop _merge


end

*********************************
******** END OF PROGRAM *********
*********************************