*! spider v1.23 (12 Nov 2023)
*! Asjad Naqvi (asjadnaqvi@gmail.com)

* 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   *
**********************************

* if you want to go for even more customization, you can read 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) [if] [in], over(varname) ///
		[ alpha(real 5) 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) raformat(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
		[ title(passthru) subtitle(passthru) note(passthru) scheme(passthru) name(passthru) saving(passthru)		] ///
		[ NOLEGend LEGPOSition(real 6) LEGCOLumns(real 5) LEGSize(real 2.2) xsize(real 1) ysize(real 1) ] ///  // v1.2 updates.
		[ RALABColor(string) RALABAngle(string) SLABColor(string) ]  // v1.2X options

		// TODO:
		// ROTATELABel: allow label rotations
		// POLYgon    : replace circles with polygons
		// legend(passthru) not working. If labels have breaks, it breaks down. Check and fix.
		// add a by() option for long data

	// 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' `over'

	local length : word count `varlist'

	cap confirm numeric var `over'

	if _rc!=0 {  // if string
		tempvar over2
		encode `over', gen(over2)
		local over over2
	}
	else {
		egen   over2 = group(`over')

		if "`: value label `over''" != "" {

			tempvar tempov
			decode `over', gen(`tempov')
			labmask over2, val(`tempov')

		}
		local over over2
	}




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

	tempvar varmin varmax
	egen double `varmin' = rowmin(`varlist')
	egen double `varmax' = rowmax(`varlist')

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

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

	}
	else {
		summ `varmin', meanonly
			local gmin = r(min)
		summ `varmax', meanonly
			local gmax = r(max)

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

		local norm1 = `gmin' - `disp'
		local norm2 = `gmax' + `disp'
	}


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


	levelsof `over'
	local items = r(r)


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


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


		foreach x of varlist `varlist' {
			gen double x_`x' = `x' * cos(angle)
			gen double y_`x' = `x' * sin(angle)
		}


		if "`smooth'" != "" {
			foreach x of varlist `varlist' {
				cap drop _m
				smoother y_`x' x_`x', rho(`smooth')
				ren Cx x_`x'_pts
				ren Cy y_`x'_pts
				sort `over' _id
			}
		}



	/////////////////
	//   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 "`raformat'" == "" local raformat %5.0f

	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'), "`raformat'")  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   = (100 + `displacelab') * cos(angle)
	gen double markery   = (100 + `displacelab') * sin(angle)

	local labs

		forval i = 1/`items' {

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

			local labs `labs' (scatter markery markerx in `i', mc(none) mlab(`over') 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'
	}

	forval i = 1/`length' {

	tokenize `varlist'

		local varn = "``i''"

		colorpalette `palette', nograph `poptions'

		if "`smooth'" == "" {
			local spider  `spider'  (area y_`varn'     x_`varn'    , nodropbase fi(100) fcolor("`r(p`i')'%`alpha'") lc("`r(p`i')'") lw(`lwidth')) ||

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

		}


		local spider2 `spider2' (scatter y_`varn' x_`varn', msize(`msize') mc("`r(p`i')'") msymbol("`msymbol'") mlwidth(`mlwidth')) ||


	}

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


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

	}
	else {

		forval i = 1/`length' {

			local j = `i' + (`cuts' * 2) + `items'

			local varn = "``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' ///
							`title' `subtitle' `note' `scheme' `name' `saving'



	*/

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) ]

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


preserve

	cap drop _id
	keep `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 Cx`x' = (((`t2' - t`x') / (`t2' - `t1')) * `B1x') + (((t`x' - `t1') / (`t2' - `t1')) * `B2x')
		gen 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

	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 *********
*********************************