*! polarspike v1.0 (05 May 2024)
*! Asjad Naqvi (asjadnaqvi@gmail.com)

* v1.0 (05 May 2024): First release

**********************************


cap program drop polarspike


program polarspike, sortpreserve

version 15

	syntax varlist(min=1 max=1 numeric) [if] [in], by(varname) 						  ///
		[ over(varname) radmin(real 5) radmax(real 10) gap(real 2) ROtate(real 0)  	] ///
		[ LWidth(string) palette(string) offset(real 5) format(string)	 			] ///
		[ LABGap(real 5) LABSize(string) LABColor(string) labcond(real -1e15) 		] ///
		[ colorby(varname) LEGPOSition(real 6) LEGCOLumns(real 3) LEGSize(string) NOLEGend	] ///
		[ OGap(real 0.9) OSize(string) OColor(string) 					 			] ///
		[ aspect(real 1) xsize(real 1) ysize(real 1) *	]


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

	if "`over'"!="" & "`colorby'"!="" {
		display as error "Only one of {bf:over()} or {bf:colorby()} can be specified."
		exit
	}

	if `radmin' >= `radmax' {
		di as error  "{bf:radmin()} >= {bf:radmax()}. "
		exit
	}

	if "`radmin'" == "0" local radmin 0.001 // to avoid irregular outputs.


	marksample touse, strok

quietly {
preserve


	keep if `touse'

	local ovtrack = 1

	if "`over'"=="" {
		gen _over = 1
		local over _over
		local ovtrack = 0
	}



	keep `varlist' `by' `over' `colorby'

	cap confirm numeric var `over'
		if !_rc {        // if numeric
			if "`: value label `by''" != "" {   // with val labels

				decode `over', gen(_ov2)
				drop `over'
				local over _ov2
			}
			else {				// without val labels
				gen _ov2 = string(`over')
				drop `over'
				local over _ov2
			}
		}



	if "`colorby'"=="" {
		gen _colorby = 1
		local colorby _colorby
		local clrbytrack = 0
	}
	else {
		local clrbytrack = 1
		cap confirm numeric var `over'

		if !_rc {        // if numeric
			gen _clrby = `colorby'
			local colorby _clrby
			}
		else {				// without val labels
			encode `colorby', gen(_clrby)
			drop `colorby'
			local colorby _clrby
		}

	}



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

	sort `over' `varlist' `by'


	gen double _angle = _n  / _N

	egen _grps = group(`over')
	replace _angle = _angle + (_grps) * `gap' / 100

	summ _angle, meanonly
	replace _angle = (_angle / `r(max)') * (2 * _pi) + (`rotate' * _pi / 180)


	**** add over names

	egen tag = tag(`over')
	recode tag (0=.)

	gen double _xcont     = .
	gen double _ycont     = .
	gen double _anglecont = .

	levelsof `over' if tag==1, local(lvls)

	foreach x of local lvls {

		summ _angle if `over' == "`x'", meanonly

		replace _anglecont = (`r(max)' + `r(min)') / 2 				if tag==1 & `over'== "`x'"
		replace _xcont 	   = (`radmin' * `ogap' * cos(_anglecont)) 	if tag==1 & `over'== "`x'"
		replace _ycont 	   = (`radmin' * `ogap' * sin(_anglecont)) 	if tag==1 & `over'== "`x'"

	}




	// mod the angles
	replace _angle 	   = mod(    _angle, 2 * _pi)
	replace _anglecont = mod(_anglecont, 2 * _pi)


	// generate the spikes
	summ `varlist', meanonly
	local height = `r(max)'

	gen double _radius = `varlist' / `height'


	*** rescale radius to (a,b) = ((b - a)(x - xmin)/(xmax - xmin)) + a
	replace _radius = ((`radmax' - `radmin') * (_radius - 0) / (1 - 0)) + `radmin'


	gen double _xcir = `radmin' * cos(_angle)
	gen double _ycir = `radmin' * sin(_angle)

	gen double _xval =  _radius  * cos(_angle)
	gen double _yval =  _radius  * sin(_angle)


	// add the labels
	if "`format'"  == "" local format %15.0fc

	gen _mylab = `by' + " (" + string(`varlist', "`format'") + ")"
	gen double _length = length(_mylab)
	summ _length, meanonly
	replace _length = _length / r(max)

	gen double _xlab = ((_radius + _length) * (1 + `labgap'/100)) * cos(_angle)
	gen double _ylab = ((_radius + _length) * (1 + `labgap'/100)) * sin(_angle)

	replace _xlab = ((`radmin' + _length) * (1 + `labgap'/100)) * cos(_angle) if `varlist' < 0
	replace _ylab = ((`radmin' + _length) * (1 + `labgap'/100)) * sin(_angle) if `varlist' < 0


	// rotate angles by quadrants
	gen double 	_angle2 = (_angle  * (180 / _pi)) - 180
	replace 	_angle2 =  _angle2 + 180 				if _xlab > 0



	**************************
	** 		Final graph 	**
	**************************

	if "`lwidth'" =="" local lwidth 0.6
	if "`palette'" == "" {
		local palette tableau
	}
	else {
		tokenize "`palette'", p(",")
		local palette  `1'
		local poptions `3'
	}

	if "`labcolor'" == "" local labcolor 	black
	if "`labsize'" 	== "" local labsize 	1.1
	if "`ocolor'" 	== "" local ocolor 		black
	if "`osize'" 	== "" local osize 		1.1
	if "`legsize'" 	== "" local legsize 	1.5

	// spikes

	local cont


	if "`clrbytrack'" == "0" {
		levelsof `over', local(lvls)
		local items = `r(r)'
	}
	else {
		levelsof `colorby', local(lvls)
		local items = `r(r)'
	}


	local i = 1

	foreach x of local lvls {

		colorpalette `palette', n("`items'") nograph `poptions'

		if "`clrbytrack'" == "0" {
			local cont `cont' (pcspike _yval _xval _ycir _xcir if `over' =="`x'", lc("`r(p`i')'") lw(`lwidth'))
		}
		else {
			local cont `cont' (pcspike _yval _xval _ycir _xcir if `colorby' ==`x', lc("`r(p`i')'") lw(`lwidth'))
		}

		local ++i
	}




	// by labels

	forval i = 1/`=_N' {
		summ _angle2 in `i' , meanonly
		local labs2 `labs2' (scatter _ylab _xlab if _n==`i' & _xlab >= 0 & `varlist' > `labcond', mc(none) mlabel(_mylab) mlabangle(`r(mean)') mlabpos(0) mlabgap(zero) mlabcolor("`labcolor'") mlabsize(`labsize'))
		local labs2 `labs2' (scatter _ylab _xlab if _n==`i' & _xlab <  0 & `varlist' > `labcond', mc(none) mlabel(_mylab) mlabangle(`r(mean)') mlabpos(0) mlabgap(zero) mlabcolor("`labcolor'") mlabsize(`labsize'))
	}

	// over labels

	if `ovtrack' != 0 {
		levelsof `over' if tag==1, local(lvls)

		foreach x of local lvls {
			summ _anglecont if `over'== "`x'" & tag==1, meanonly // fix these angles
			local myangle = (`r(mean)'  * (180 / _pi)) - 90

			local labcont `labcont' (scatter _ycont _xcont if `over'== "`x'" & tag==1, mc(none) mlabel(`over') mlabangle(`myangle') mlabpos(0)  mlabcolor("`ocolor'") mlabsize(`osize'))

		}
	}


	// fix the legend

	if "`clrbytrack'" == "0" | "`nolegend'"!="" {
		local mylegend legend(off)
	}
	else {
		gen _id = _n
		levelsof `colorby', local(lvls)

		foreach x of local lvls {
			local varn : label `colorby' `x'
			local entries `" `entries' `x'  "`varn'"  "'
		}
		local mylegend legend(order("`entries'") pos(`legposition') size(`legsize') col(`legcolumns'))
	}



	// extend the axes

	summ _xval, meanonly
	local xlimits = max(r(max), abs(r(min)))

	summ _yval, meanonly
	local ylimits = max(r(max), abs(r(min)))


	local edge = max(`xlimits', `ylimits') * (1 + (`offset' / 100))



	// final

	twoway ///
		`cont' ///
		`labs2' ///
		`labcont' ///
			, ///
			yscale(noline range(-`edge' `edge')) ///
			xscale(noline range(-`edge' `edge')) ///
			xlabel(-`edge' `edge', nogrid) ///
			ylabel(-`edge' `edge', nogrid) ///
			yscale(off) xscale(off)	///
			xsize(`xsize') ysize(`ysize') aspect(`aspect')	///
			`mylegend' `options'


*/
restore
}

end



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