/******************************************************************************* 
APCPLOT: A tool for visualizing APC effects to facilitate Fosse-Winship bounding 
approach to APC analysis.
********************************************************************************
Version: 1.1 (08.05.2025)
Author: Gordey Yastrebov, University of Cologne
License: GPL-3.0
*******************************************************************************/
	
	version 18
	
	pr de apcplot
		syntax [anything(name=graphs)], /// APC effect selection option
		///
			[Bounded] /// plot bounded solution
 			[Keepshape] /// keep shapes
			[ci(str)] /// CI option
			[a(numlist min=1 max=1)] /// exact linear components
			[p(numlist min=1 max=1)] ///
			[c(numlist min=1 max=1)] /// 
			[PEAbounds(numlist min=1 max=2 sort)] /// custom p.-e. bounds
			[PEPbounds(numlist min=1 max=2 sort)] ///
			[PECbounds(numlist min=1 max=2 sort)] ///
			[CIAbounds(numlist min=1 max=2 sort)] /// custom c.-i. bounds
			[CIPbounds(numlist min=1 max=2 sort)] ///
			[CICbounds(numlist min=1 max=2 sort)] ///
		///	
			[Grid(str)] /// diagnostic grid
			[GRIDLABels(str)] /// grid labels selection (off/right/left)
			[ANChorgrid] /// grid anchoring (A=-P=C)
			[GRIDFading(numlist max=1 >=0 <=1)] /// grid fading factor
			[gridlabops(str)] /// grid labels customization
			[gridline(str)] /// grid line decoration
			[GRIDPALette(str)] /// grid color palette
		///
			[NOGRadient] /// supress gradient
			[GRADes(int 100)] /// gradient grades
			[AREACONtour(str)] /// gradient area contour
			[AREAPALette(name)] /// gradient area color palette
		///
			[SHAPEPLops(str asis)] /// shape line options
			[PLotops(str)] /// common plot options
			[APLotops(str asis)] /// specific plot options
			[PPlotops(str asis)] ///
			[CPlotops(str asis)] ///
			[CIPLotops(str)] /// custom CI plot options
			[RECASTci(str)] /// custom CI rendering
			[COMBined] /// combine graphs
			[COMBPLotops(str asis)] // combined plot options

*** Variable symbols for printing
	loc a_letter α
	loc p_letter π
	loc c_letter γ
	loc nu_letter ν
	
*** Restore estimates from APCESTIMATE
	cap est res __apcestimate
	if _rc {
		di as err "The return from {it:apcestimate} not found!"
		exit
	}
	
*** Parse which graphs requested
	loc error = 0
	loc graphs = trim(strlower("`graphs'"))
	loc selection
	if inlist(`:word count `graphs'', 1, 2, 3) {
		forv i=1/`:word count `graphs'' {
			if !inlist("`: word `i' of `graphs''", "a", "p", "c") loc error = 1
			else loc selection `selection' `:word `i' of `graphs''
		}
	} 
	else if "`graphs'" == "" loc selection a p c
	else loc error = 1
	if `error' {
		di as err "Graph selection option incorrectly specified " ///
			"({help apcbound:help apcbound})"
		exit
	}
	
*** Parse confidence interval option
	if "`ci'" == "" loc no_ci = 1
	else {
		cap conf n `ci'
		if _rc == 0 & (`ci' > 0 & `ci' < 100) {
			loc no_ci = 0
			loc ci_lvl = `ci'
			if "`e(apcboundCI)'" != "" {
				if `ci_lvl' != `e(apcboundCI)' & "`bounded'" != "" {
					di as err "Specified confidence level does not match " ///
						"the level from {it:apcbound}." _n "Confidence-interval " ///
						"bounded solution will be inaccurate."
				}	
			}
		}
		else {
			di as err "Confidence interval option incorrectly " ///
				"specified ({help apcbound:help apcbound})"
			exit
		}
	}
	
*** Parse whether/which bounds requested
	loc no_shapes = 0
	if "`bounded'" == "" {
		di as txt "A bounded solution not requested, only shapes will be processed."
	}
	else {
		di as txt "A bounded solution requested."
		loc no_shapes = 1
		if "`keepshape'" != "" loc no_shapes = 0
		if `no_ci' loc types pe
		if !`no_ci' loc types pe ci
		loc pe "point-estimate"
		loc ci "`ci'% confidence-interval"
		foreach type in `types' {
			foreach apcvar in `selection' {
				if "``type'`apcvar'bounds'" == "" {
					if "`e(`type'_bounded_solution)'" != "1" {
						di as err "Neither custom nor evaluated ``type'' " ///
							"bounded solution for ``apcvar'_letter' " ///
							"({it:`e(`apcvar'var)'}) from {it:apcbound} " ///
							"could be found."
						exit
					}
					else {
						loc `type'_`apcvar'_bounds ///
							`=e(`type'`=strupper("`apcvar'")'min)' ///
							`=e(`type'`=strupper("`apcvar'")'max)'
						di as txt "   A ``type'' solution for ``apcvar'_letter' " ///
							"({it:`e(`apcvar'var)'}) assumed from {it:apcbound}."
					}
				}
				else {
					loc `type'_`apcvar'_bounds ``type'`apcvar'bounds'
					di as txt "   A custom ``type'' solution for ``apcvar'_letter' " ///
						" ({it:`e(`apcvar'var)'}) specified."
				}
			}
		}
	}
	
*** Parse exact linear component parameters 
	loc parameters `a' `p' `c'
	if `:word count `parameters'' > 1 {
		di as err "Only a single linear parameter (α, π or γ) can be specified."
		exit
	}
	else if `:word count `parameters'' == 0 {
		loc a_value = 0
		loc p_value = 0
		loc c_value = 0
	}
	else {
		if "`a'" != "" {
			loc letter a
			loc a_value = `a'
			loc p_value = `e(theta1)' - `a'
			loc c_value = `e(theta2)' - `e(theta1)' + `a'
			loc remaining p c
		}
		if "`p'" != "" {
			loc letter p
			loc a_value = `e(theta1)' - `p'
			loc p_value = `p'
			loc c_value = `e(theta2)' - `p'
			loc remaining a c
		}
		if "`c'" != "" {
			loc letter c
			loc a_value = `e(theta1)' - `c'
			loc p_value = `e(theta1)' - `e(theta2)' + `c'
			loc c_value = `c'
			loc remaining a p
		}
		forv i=1/2 {
			loc imply`i' = "``: word `i' of `remaining''_letter' = " + string(``: word `i' of `remaining''_value', "%9.3g")
		}
		di as txt "Parameter {bf:``letter'_letter'} set to {bf:``letter'_value'} " ///
			"(implies {bf:`imply1'} and {bf:`imply2'})".
	}
	
*** Parse grid parameters
	loc error = 0
	if "`grid'" == "" loc no_grid = 1
	* parsing:
	else if `:word count `grid'' == 1 {
		loc no_grid = 0
		loc grid_step = "`grid'"
		loc grid_sign
		loc grid_steps = 1
	}
	else if `:word count `grid'' == 2 {
		loc no_grid = 0
		gettoken grid_step chunk : grid
		if !(`grid_step' > 0 & !mi(`grid_step')) loc error = 1
		if inlist(substr(trim("`chunk'"), 1, 1), "+", "-") {
			loc grid_sign = substr(trim("`chunk'"), 1, 1)
			loc grid_steps = real(substr(trim("`chunk'"), 2, .))
		}
		else loc grid_steps = real("`chunk'")
	}
	else loc error = 1
	* integrity checks:
	if !`no_grid' {
		loc grid_step = real("`grid_step'")
		if !(`grid_step' > 0 & !mi(`grid_step')) loc error = 1
		if mi(`grid_steps') | !(`grid_steps'==int(`grid_steps')) loc error = 1
		if `error' {
			di as err "Inappropriate input in {it:grid()} option!"
			exit
		}
		if "`grid_sign'" == "" loc grid_sign - +
	}

*** Extract values and CIs into plot matrices
	foreach apcvar in `selection' {
		loc variable `e(`apcvar'var)'
		loc specification `e(`apcvar'spec)'
		loc ncols = 14
	* polynomial specification:
		if strpos("`specification'", "#") > 0 {
			default_range_of_values `variable'
			loc xvalues = r(xvalues)
			mat ests = J(`:word count `xvalues'', `ncols', .)
			forv i=1/`=rowsof(ests)' {
				loc x = `:word `i' of `xvalues''
				mat ests[`i', 1] = `x' // x original/value scale
				mat ests[`i', 2] = `x' - `e(`apcvar'center)' // xL
				loc formula 0
				forv j=1/`:word count `specification'' {
					loc xpowered = ests[`i', 2]^(`=`j'+1')
					loc formula `formula'+_b[`: word `j' of `specification'']*`xpowered'
				}
				qui lincom `formula', l(`ci_lvl')
				mat ests[`i', 3] = r(estimate) // NL
				mat ests[`i', 4] = r(lb) // NLlbCI
				mat ests[`i', 5] = r(ub) // NLubCI
			}
		}
	* categorical specification:
		else if substr("`specification'", 1, 1) == "i" & strpos("`specification'", ".") > 0 {
			mat b = e(b)
			loc coeflist : colnames b
			loc coefficients
			loc xvalues
			foreach coef of loc coeflist {
				if strpos("`coef'", ".`variable'") {
					loc value = substr("`coef'", 1, strpos("`coef'", ".") - 1)
					loc value : subinstr loc value "b" "", all
					loc value : subinstr loc value "o" "", all
					loc xvalues `xvalues' `value'
					loc coefficients `coefficients' `coef'
				}
			}
			mat ests = J(`: word count `xvalues'', `ncols', .)
			forv i=1/`=rowsof(ests)' {
				loc x = `:word `i' of `xvalues''
				mat ests[`i', 1] = `x' // x original value/scale
				mat ests[`i', 2] = `x' - `e(`apcvar'ref)' // xL
				qui lincom _b[`:word `i' of `coefficients''], l(`ci_lvl')
				mat ests[`i', 3] = r(estimate) // NL
				mat ests[`i', 4] = cond(mi(r(lb)), 0, r(lb)) // NLlbCI
				mat ests[`i', 5] = cond(mi(r(ub)), 0, r(ub)) // NLubCI
			}
		}
	* linear specification
		else if "`specification'" == "" {
			default_range_of_values `variable'
			loc xvalues = r(xvalues)
			mat ests = J(`:word count `xvalues'', `ncols', .)
			forv i=1/`=rowsof(ests)' {
				loc x = `:word `i' of `xvalues''
				mat ests[`i', 1] = `x' // x original value/scale
				mat ests[`i', 2] = `x' - `e(`apcvar'center)'
				mat ests[`i', 3] = 0
				mat ests[`i', 4] = 0
				mat ests[`i', 5] = 0
			}
		}
	* inappropriate input
		else {
			di as error "Variable specifications unclear! Please check."
			exit
		}
		loc baseline = ``apcvar'_value'
		forv i=1/`=rowsof(ests)' {
			loc letter = strupper("`apcvar'")
			loc x = ests[`i', 2]
			loc L_coef_min = `=e(pe`letter'min)'
			loc L_coef_max = `=e(pe`letter'max)'
			if "`apcvar'" == "p" { // a master flipper for period effects
				loc L_coef_min = `=e(pe`letter'max)'
				loc L_coef_max = `=e(pe`letter'min)'
			}
			mat ests[`i', 6] = ests[`i', 3] + `baseline' * `x' // NLshift (PE)
			mat ests[`i', 7] = ests[`i', 4] + `baseline' * `x' // NLlbCIshift (CI)
			mat ests[`i', 8] = ests[`i', 5] + `baseline' * `x' // NLubCIshift (CI)
			mat ests[`i', 9]  = `L_coef_min' * `x' // lbL REDUNDANT?
			mat ests[`i', 10] = `L_coef_max' * `x' // ubL REDUNDANT?
			mat ests[`i', 11] = `L_coef_min' * `x' + ests[`i', 3] // lb = lbL + NL (PE)
			mat ests[`i', 12] = `L_coef_max' * `x' + ests[`i', 3] // ub = ubL + NL (PE)
			mat ests[`i', 13] = ///
				min(`=e(ci`letter'min)' * `x', `=e(ci`letter'max)' * `x') + ///
				ests[`i', 4] // lbCI = NLlbCI (CI)
			mat ests[`i', 14] = ///
				max(`=e(ci`letter'min)' * `x', `=e(ci`letter'max)' * `x') + ///
				ests[`i', 5] // ubCI = NLubCI (CI)
		}
		mat coln ests = x xL NL NLlbCI NLubCI /// cols 1-5
			NLshift NLlbCIshift NLubCIshift /// cols 6-8
			lbL ubL lb ub lbCI ubCI // cols 9-14
		mat `apcvar'_estimates = ests
		mat drop ests
	}
	
***	Grid palette, matrix and label rendering (if requested)
	if !`no_grid' {
	* palettes:
		if "`gridpalette'" == "" loc gridpalette = "tableau"
		if strpos("`gridpalette'", ",") {
			loc comma = strpos("`gridpalette'", ",")
			loc pal1 = trim(substr("`gridpalette'", `comma' + 1, .))
			loc pal2 = trim(substr("`gridpalette'", 1, `comma' - 1))
		}
		else {
			loc pal1 "`gridpalette'"
			loc pal2 "`gridpalette'"				
		}
		if "`gridfading'" == "" loc gridfading = 0
		forv i=1/`grid_steps' {
			loc fading`i'=int(cond(`gridfading'==0,100,100*(1-(`i'-1)/(`grid_steps)'-1))^`gridfading'))
		}
		forv i=1/2 {
			colorpalette `pal`i'', nogr n(`grid_steps')
			forv j = 1/`grid_steps' {
				loc pal`i'color`j' = r(p`j')
			}
		}
	* matrices and labels:
		foreach apcvar in `selection' {
			mat estimates = `apcvar'_estimates
			loc signs `grid_sign'
			if "`anchorgrid'" != "" & "`apcvar'" == "p" {
				if "`grid_sign'" == "-" loc signs +
				if "`grid_sign'" == "+" loc signs -
				if "`grid_sign'" == "- +" loc signs + -
			}
			loc colnames
			forv i=1/`:word count `signs'' {
				loc sign `:word `i' of `signs''
				produce_gradient estimates NLshift `=`sign'`grid_step'' ///
					`grid_steps' grid 0
				mat grid_matrix = r(gradient_matrix)
				loc style place(c)
				if "`gridlabops'" != "" loc style `gridlabops'
				loc xmin = estimates[1, "x"]
				loc xmax = estimates[`=rowsof(estimates)', "x"]		
				loc grid_labels
				forv j=1/`grid_steps' {
					if "`gridlabels'" != "off" {
						loc y_xmin = grid_matrix[1, `j']
						loc y_xmax = grid_matrix[`=rowsof(grid_matrix)', `j']
						loc left_label = "`sign'" + string(`grid_step' * `j', "%9.3g")
						loc right_label `left_label'
						if "`gridlabels'" == "left" loc right_label 
						if "`gridlabels'" == "right" loc left_label
						loc color `pal`i'color`j''
						loc	grid_labels `grid_labels' ///
							text(`y_xmin' `xmin' "`left_label'", c("`color'"%`fading`j'') `style') ///
							text(`y_xmax' `xmax' "`right_label'", c("`color'"%`fading`j'') `style')
					}
					loc colnames `colnames' grid`=`j'+(`i'-1)*`grid_steps''
				}
				loc `apcvar'_grid_labels ``apcvar'_grid_labels' `grid_labels'
				mat grid_matrix`i' = grid_matrix
				loc nruns = `i'
			}
			if `nruns' == 1 mat `apcvar'_grid_matrix = grid_matrix1
			else {
				mat `apcvar'_grid_matrix = (grid_matrix1, grid_matrix2)
				mat coln `apcvar'_grid_matrix = `colnames'
			}
			foreach m in estimates grid_matrix grid_matrix1 grid_matrix2 {
				cap mat drop `m'
			}
		}
	}
	
*** Render a bounded solution matrix (if requested)
	if "`bounded'" != "" {
		if "`nogradient'" == "" {
			if "`areapalette'" == "" loc areapalette "viridis"
			colorpalette `areapalette', nogr n(`grades')
			forv i = 1 / `grades' {
				loc gradcolor`i' = r(p`i')
				//loc pgradcolor`i' = r(p`=`grades'-`i'+1')
			}
			foreach apcvar in `selection' {
				mat estimates = `apcvar'_estimates
				loc step = (`e(pe`=strupper("`apcvar'")'max)' - ///
					`e(pe`=strupper("`apcvar'")'min)') / (`grades' - 1)
				if "`apcvar'" == "p" loc step = -`step'
				produce_gradient estimates lb `step' `grades' grad 1
				mat `apcvar'_gradient_matrix = r(gradient_matrix)
			}
		}
	}
	
*** Combine (and drop redundant) matrices
	foreach apcvar in `selection' {
		mat `apcvar'_plot_matrix = `apcvar'_estimates
		foreach m in grid gradient {
			cap mat `apcvar'_plot_matrix = ///
				(`apcvar'_plot_matrix, `apcvar'_`m'_matrix)
			cap mat drop `apcvar'_`m'_matrix
		}
	}
	
*** Combine matrices,render graphs, and assemble the plot
	loc recast rarea
	loc recastci rarea
	loc atitle Age
	loc ptitle Period
	loc ctitle Cohort
	loc ytitle
	foreach apcvar in `selection' {
		loc xlabels : val lab `e(`apcvar'var)'
		preserve
		clear
		qui svmat `apcvar'_plot_matrix, n(col)
		la val x `xlabels'
		if !`no_shapes' loc pe_plot (line NLshift x, sort `shapeplops')
		if !`no_ci' & !`no_shapes' loc cipe_plot ///
			(`recastci' NLlbCIshift NLubCIshift x, sort `ciplotops')
		if "`areacontour'" != "" {
			qui de grad*, varl
			loc gradlast : word `: word count `r(varlist)'' of `r(varlist)'
			loc contour_plot (rline grad1 `gradlast' x, sort ///
				lp(solid) `areacontour') 
		}
		if !`no_grid' {
			loc grid_plot
			forv i=1/`:word count `grid_sign'' {
				forv j=1/`grid_steps' {
					loc color `pal`i'color`j''%`fading`j''
					loc ivar = `j' + (`i' - 1) * `grid_steps'
					loc grid_plot `grid_plot' ///
						(line grid`ivar' x, sort lc("`color'") lp(dash) `gridline')
				}
			}

		}
		if "`bounded'" != "" {
			loc i = 1
			if "`nogradient'" == "" {
				foreach v of var grad* {
					loc color `gradcolor`i''
					loc gradient_plot `gradient_plot' ///
						(line grad`i' x, sort lc("`color'") lp(solid))
					loc ++i
				}
				loc bounded_plot `gradient_plot'
			} 
			else {
				loc bounded_plot (rarea grad1 grad2 x, sort ///
					lp(solid) fc(`areapalette'))
			}
			if !`no_ci' loc cibounded_plot (`recastci' lbCI ubCI x, sort `ciplotops')
		}
	* MASTER PLOT:
		loc plot `cibounded_plot' `bounded_plot' `contour_plot' `cipe_plot' `grid_plot' `pe_plot'
		tw `plot', ``apcvar'_grid_labels' ///
			yti("`ytitle'") xti(``apcvar'title', height(7)) ///
			leg(off) name(``apcvar'title', replace) ///
			`plotops' ``apcvar'plotops'
		restore
		loc plots_to_combine `plots_to_combine' ``apcvar'title'
		mat drop `apcvar'_plot_matrix
	}
	if "`combined'" != "" & `:word count `graphs'' != 1 {
		gr combine `plots_to_combine', r(1) ycom name(Combined, replace) ///
			xsiz(`: word count `plots_to_combine'') ysiz(1) `combplotops'
	}

	end	

/////// Routines: //////////////////////////////////////////////////////////////
	pr de default_range_of_values, rclass
		syntax varlist(min=1 max=1)
		qui sum `varlist'
		loc step = (r(max) - r(min)) / 20
		numlist "`r(min)'(`step')`r(max)'", sort
		loc values = r(numlist)
		loc xvalues
		foreach value in `values' {
			loc xvalues `xvalues' `=round(`value', 1)'
		}
		ret loc xvalues `xvalues'
	end
	pr de produce_gradient, rclass
		args input_matrix reference_col step grades col_prefix offset
		mat gradient = J(`=rowsof(`input_matrix')', `grades', .)
		loc col_names
		forv j=1/`grades' {
			forv i=1/`=rowsof(gradient)' {
				loc x = `input_matrix'[`i', "xL"]
				loc reference = `input_matrix'[`i', "`reference_col'"]
				mat gradient[`i', `j'] = `reference' + `x' * `step' * (`j' - `offset')
			}
			loc col_names `col_names' `col_prefix'`j'
		}
		mat coln gradient = `col_names'
		ret mat gradient_matrix = gradient
	end
	pr de refine_values, rclass
		syntax, values(str) n(int)
		loc output 
		token `values'
		loc nvalues : word count `values'
		forv i = 1/`=`nvalues'-1' {
			loc a = ``i''
			loc b = ``=`i'+1''
			loc step = (`b' - `a') / (`n' + 1)
			loc output `output' `a'
			forval j = 1/`n' {
				loc newval = `a' + `j' * `step'
				loc output `output' `newval'
			}
		}
		loc output `output' ``nvalues''
		ret loc extended `output'
	end