*! cellgraph 0.5.0 25aug2026

// Author: Johannes F. Schmieder
// Department of Economics, Boston University
// cellgraph.ado

// First Version: April 2008
// Comments and suggestions welcome: johannes{at}bu.edu

// Notes:
// Routine to generate variably by variable graph (similar to tabstat but as graph)

// Usage:
// cellgraph graphvars, by(byvar1 byvar2) [options]

// Data is collapsed to cell level, where cells are defined by one or two categorical variables (byvar1 and byvar2)
// and cell means (or other statistics) of a third variable (graphvar) are graphed. If more than one graphvar is specified,
// then each graphvar is plotted.

// Example:
/*
	   sysuse nlsw88
	   cellgraph wage, by(grade)
	   cellgraph wage, by(grade union)
	   cellgraph wage, by(grade union)  stat(max)
	   cellgraph wage if industry>2 & industry<10, by(grade industry) nonotes noci legend(col(2))
	   cellgraph wage, by(grade) stat(p25 p50 p90)
	   cellgraph wage, by(grade) stat(sd iqr)
	   cellgraph wage married, by(grade)
	   cellgraph wage married, by(grade) stat(sd iqr)

*/

version 18.0
capture program drop cellgraph
program define cellgraph
	syntax varlist [if] [in] [aweight fweight] , by(str) ///
		[ ///
		/// === Main ===
		Name(str)               /// provide a graph name (just like the name option in other graph commands).
		Stat(str)               /// the cell statistic to be used. If not specified "mean" is assumed. Other possibilities: min, max, sum, sd, var, p10, p25, p50, p75, p90, etc.
		list                    /// list collapsed data at the end of the command.
		SAVing(str)             /// save collapsed data to a file.
		BASEline(str)           /// normalize series to this baseline observation (subtraction).
		Title(str)              /// Title
		SUBTitle(passthru)      /// Subtitle
		YTITle(passthru)        /// Y-axis title
		NOTITLE                 /// don't display title
		/// === Graph options ===
		lpattern                /// specify line pattern.
		lpatterns(str)          /// specify multiple line patterns.
		scatter                 /// create a scatter plot.
		line                    /// create a line plot.
		GRADient                /// apply a color gradient as the gradient for the second by variable.
		Colors(str)             /// provide a list of colors to replace standard palette. Separate colors with semicolons.
		lwidth(passthru)        /// specify line width.
		*                       /// provide any twoway options to pass through to the call of the twoway command. Can also be used to overwrite options that are given as standard, for example title(My Title) would overwrite the standard title with "My Title".
		/// === Marker options ===
		msymbols(str)            /// Change marker symbol where symbol1 etc is of symbolstyle.
		NOMSYMbol                /// do not use marker symbols.
		MSIZE(passthru)          /// specify marker size.
		mcounts                  /// display observation counts next to markers.
		/// === Binning options ===
		binscatter(integer 0)    /// create a binned scatter plot with the specified number of bins.
		bin(real 0)              /// bin the data by the specified real number as bin width.
		lfit                    /// add a linear fit line to the plot.
		coef                    /// display regression coefficients.
		45deg                   /// add a 45-degree reference line.
		/// === Confidence intervals ===
		NOCI                    /// don't display confidence intervals.
		cipattern(str)          /// specify confidence interval pattern, either 'shaded' or 'lines'.
		ciopacity(integer 20)   /// specify the shading coefficient for confidence intervals.
		/// === Controlling for covariates ===
		controls(str)          /// specify controls to partialling out.
		/// === Legend ===
		addnotes                /// Add notes with sample sizes to the legend.
		samplenotes(str)        /// add sample notes to the plot.
		NONOTES                 /// don't display any notes in legend.
		NODATE                  /// don't display date in notes.
		/// === Computational Tools ===
		gtools                  /// use gtools for data processing.
		ftools                  /// use ftools for data processing.
		/// === Category ordering ===
		xorder(str asis)        /// order categories by mean of specified variable. Suboptions: descending, stat(statname).
		/// === Axis label options ===
		XLABel(str asis)        /// additional xlabel suboptions (e.g., ang(45), labsize(small))
		YLABel(str asis)        /// additional ylabel suboptions (e.g., ang(0), labsize(small))
		]

	// Strip leading comma from xlabel if present (allow both "ang(45)" and ", ang(45)")
	local xlabel = strtrim(regexr(`"`xlabel'"', "^,", ""))

	// Set default labsize only if user didn't specify one
	local xlabel_labsize "labsize(medsmall)"
	if strpos(`"`xlabel'"', "labsize") > 0 {
		local xlabel_labsize ""
	}
	// Store user's xlabel content for later use (may contain numlist and/or suboptions)
	local xlabel_user `"`xlabel'"'

	// Strip leading comma from ylabel if present (allow both "ang(0)" and ", ang(0)")
	local ylabel = strtrim(regexr(`"`ylabel'"', "^,", ""))

	// Set default labsize only if user didn't specify one
	local ylabel_labsize "labsize(medsmall)"
	if strpos(`"`ylabel'"', "labsize") > 0 {
		local ylabel_labsize ""
	}

	if "`colors'" == "" {
		local colors  ///
			dknavy; cranberry; dkgreen; edkblue; ///
			dkorange; maroon; olive; eltblue; ///
			eltgreen; emidblue; erose; blue; ///
			purple; brown; cyan; ebblue; ///
			emerald; orange; forest_green; gold; ///
			green; khaki; lavender; lime; ///
			ltblue; ltbluishgray; ltkhaki; ///
			midblue; midgreen; mint; navy; olive_teal; magenta; ///
			orange; orange_red; pink; red; sand; sandb; sienna; stone; teal; yellow
	}
	else {
		// Check if colors are properly semicolon-separated
		local wordcount: word count `colors'
		if `wordcount' > 1 {
			// Check if colors is in RGB format
			local is_rgb = regexm("`colors'", "([0-9]{1,3}) ([0-9]{1,3}) ([0-9]{1,3})")
			if !`is_rgb' {
				// If multiple words but no semicolons, add them
				if !strpos("`colors'", ";") {
					display as text "Warning: Colors should be separated by semicolons. For example: colors(cranberry; dkgreen; dknavy)"
					display as text "Attempting to format colors automatically..."

					local formatted_colors ""
					foreach color of local colors {
						if "`formatted_colors'" == "" {
							local formatted_colors "`color'"
						}
						else {
							local formatted_colors "`formatted_colors'; `color'"
						}
					}
					local colors `formatted_colors'
				}
			}
		}
	}
	__locallist `colors', name(colors)


	if "`lpattern'"=="lpattern" {
		local lpattern_dum = 1
	}
	else local lpattern_dum = 0

	local lpatterns `lpatterns' ///
		solid dash longdash shortdash dash_dot  shortdash_dot  longdash_dot dot

	if "`nomsymbol'"=="" {
		local msymbol_dum = 1
	}
	else local msymbol_dum = 0

	local msymbols `msymbols' ///
		square circle diamond triangle circle_hollow diamond_hollow triangle_hollow square_hollow

	if "`cipattern'" != "" & !inlist("`cipattern'", "shaded", "lines") {
		disp in red "Error: CIPattern must be either 'shaded', 'lines', or missing."
		error 198
	}
	if "`cipattern'" == "" local cipattern "shaded"

	// Parse saving option
	local savefile ""
	local savereplace ""
	if `"`saving'"' != "" {
		__parse_saving `saving'
		local savefile `"`s(savefile)'"'
		local savereplace "`s(savereplace)'"
	}

	// Parse xorder option
	local xorder_var ""
	local xorder_desc = 0
	local xorder_stat "mean"
	if `"`xorder'"' != "" {
		// Parse the xorder option: varname [, descending stat(statname)]
		gettoken xorder_var xorder_opts : xorder, parse(",")
		local xorder_var = strtrim("`xorder_var'")

		// Check for suboptions
		if `"`xorder_opts'"' != "" {
			local xorder_opts = subinstr(`"`xorder_opts'"', ",", "", 1)
			__parse_xorder_opts , `xorder_opts'
			local xorder_desc = `s(descending)'
			if "`s(stat)'" != "" local xorder_stat "`s(stat)'"
		}

		// Confirm the xorder variable exists
		confirm variable `xorder_var'
	}

	marksample touse, novarlist

	// Count the number of by variables and check if it is 1 or 2
	local number_of_by_vars : word count `by'
	if `number_of_by_vars'>2 {
		disp "Specify maximum 2 by variables"
		error 198
	}
	else if `number_of_by_vars'==2 {
		tokenize `by'
		confirm variable `1'
		confirm variable `2'
		// Detect if first by-variable is string (needed for binning validation and baseline comparison)
		local by1_is_string = 0
		capture confirm string variable `1'
		if !_rc local by1_is_string = 1
		qui tab `2' if `touse' & !missing(`2')
		local N_unique = r(r)
		local steps = `N_unique'
		cap assert r(r) <= 60
		if _rc {
			disp in red "Second By-Variable may only take 60 or less distinct values"
			error 198
		}
		replace `touse' = 0 if missing(`1')
		replace `touse' = 0 if missing(`2')
	}
	else {
		confirm variable `by'
		// Detect if by-variable is string (needed for binning validation and baseline comparison)
		local by1_is_string = 0
		capture confirm string variable `by'
		if !_rc local by1_is_string = 1
		replace `touse' = 0 if missing(`by')
	}

	// Set first_by_var for use in controls and other sections
	if `number_of_by_vars'==1 {
		local first_by_var `by'
	}
	else {
		local first_by_var : word 1 of `by'
	}

	// Calculate number of stats
	if "`stat'"=="" {
		local stat mean
	}
	else {
		local sc : word count `stat'
		local steps = `sc'
	}
	// Create a color gradient if the gradient option is specified
	if "`gradient'"!="" {
		local color1 26 31 191
		local r1 : word 1 of `color1'
		local g1 : word 2 of `color1'
		local b1 : word 3 of `color1'

		local color2 234 36 35
		local r2 : word 1 of `color2'
		local g2 : word 2 of `color2'
		local b2 : word 3 of `color2'

		if `"`steps'"'=="" local steps 26

		local colors
		forval i = 1/`steps' {
			local r = round(`r1' + (`i'/`steps') * (`r2'-`r1'))
			local g = round(`g1' + (`i'/`steps') * (`g2'-`g1'))
			local b = round(`b1' + (`i'/`steps') * (`b2'-`b1'))
			if `i'==1 local colors `r' `g' `b'
			else      local colors `colors'; `r' `g' `b'
		}

		__locallist `colors', name(colors)

	}


	// Set the default statistic to mean if not specified


	// Set the noci option if statistic is not mean
	if "`stat'"!="mean" local noci noci

	// Count the number of statistics and set the noci option if there are more than one
	local sc : word count `stat'
	if `sc'>1 local noci noci

	// Check if more than one by variable and more than one statistic are specified
	if `sc'>1 & `number_of_by_vars'>1 {
		di in red "You can either specify more than 1 'by' variable or more than one statistic, but not both"
		error 198
	}

	local figtitle `"`title'"'

	local vc : word count `varlist'
	local i 1
	foreach v in `varlist' {
		local varlabel : variable label `v'
		if `"`varlabel'"'==`""' local varlabel `"`v'"'
		local title`i++' `"`varlabel'"'
	}
	if "`title'"=="" local title `varlist'
	if "`name'"!="" local nameopt name(`name')

	// Preserve the data
	preserve
	qui keep if `touse'

	// Capture original by-variable name/label before potential encoding
	if `number_of_by_vars'==1 {
		local by1_original_name `by'
	}
	else {
		local by1_original_name `1'
	}
	local by1_original_label : variable label `by1_original_name'
	if "`by1_original_label'"=="" local by1_original_label `by1_original_name'

	// If first by-variable is string, encode it to numeric for plotting
	if `by1_is_string' {
		tempvar by1_encoded
		if `number_of_by_vars'==1 {
			encode `by', gen(`by1_encoded')
			local by `by1_encoded'
		}
		else {
			encode `1', gen(`by1_encoded')
			local by `by1_encoded' `2'
			local 1 `by1_encoded'
		}

		// Build xlabel option with original string labels
		local lblname : value label `by1_encoded'
		qui levelsof `by1_encoded', local(by_values)
		local xla ""
		foreach val of local by_values {
			local lbl : label `lblname' `val'
			local xla `xla' `val' `"`lbl'"'
		}
	}
	else {
		// Check if numeric by-variable has value labels (e.g., already encoded)
		if `number_of_by_vars'==1 {
			local lblname : value label `by'
		}
		else {
			local lblname : value label `1'
		}
		if "`lblname'" != "" {
			// Build xlabel option with value labels
			if `number_of_by_vars'==1 {
				qui levelsof `by', local(by_values)
			}
			else {
				qui levelsof `1', local(by_values)
			}
			local xla ""
			foreach val of local by_values {
				local lbl : label `lblname' `val'
				local xla `xla' `val' `"`lbl'"'
			}
		}
	}

	// Validate xorder option requires categorical x-variable
	if "`xorder_var'" != "" & `"`xla'"' == "" {
		di as error "Option xorder() requires a categorical first by-variable (string or with value labels)"
		error 198
	}

	// Calculate x-axis padding if value labels are present to prevent label cutoff
	local xscale_opt ""
	if `"`xla'"' != "" {
		if `number_of_by_vars'==1 {
			qui sum `by'
		}
		else {
			qui sum `1'
		}
		local xmin = r(min)
		local xmax = r(max)
		local xpad = (`xmax' - `xmin') * 0.05
		local xscale_opt xscale(range(`=`xmin'-`xpad'' `=`xmax'+`xpad''))
	}

	if `number_of_by_vars'==1 & `vc'==1 {
		qui count if !missing(`varlist') & !missing(`by') & `touse'
		local N = r(N)
		local cattit `by1_original_label'
	}
	if `number_of_by_vars'==1 & `vc'>1 {
		local cattit `by1_original_label'
	}

	local clist
	foreach v in `varlist' {
		local clist `clist' (count) obs`v'=`v' (sd) sd`v'=`v'
		foreach s in `stat' {
			if "`s'"!="var"	local clist `clist' (`s') `v'_`s'=`v'
			else {
				local clist `clist' (sd) `v'_`s'=`v'
			}
		}
	}

	// Add xorder variable to collapse if specified and not already in varlist
	if "`xorder_var'" != "" {
		local xorder_in_varlist = 0
		foreach v in `varlist' {
			if "`v'" == "`xorder_var'" local xorder_in_varlist = 1
		}
		if !`xorder_in_varlist' {
			local clist `clist' (`xorder_stat') __xorder_sortvar=`xorder_var'
		}
		else {
			// xorder variable is in varlist, use its computed statistic
			local __xorder_use_existing = 1
		}
	}

	// Binning options
	if `bin'!=0 & `binscatter'!=0 {
		di in red "Options 'bin' and 'binscatter'"
		error 184 // cannot be combined
	}
	// Binning requires numeric first by-variable
	if (`bin'!=0 | `binscatter'!=0) & `by1_is_string' {
		di as error "Options 'bin' and 'binscatter' require a numeric first by-variable"
		error 198
	}
	if `number_of_by_vars'==1  & `bin'!=0 {
		replace `by' = `by'-mod(`by',`bin')+`bin'*0.5
	}
	if `number_of_by_vars'==2  & `bin'!=0 {
		replace `first_by_var' = `first_by_var'-mod(`first_by_var',`bin')+`bin'*0.5
	}
	if `number_of_by_vars'==1 & `binscatter'!=0 {
		tempvar miss dum binned
		g `miss' = missing(`first_by_var')
		bys `miss' (`first_by_var'): gen `dum' = int(`binscatter'*(_n-1)/_N)+1
		egen `binned' = mean(`first_by_var'), by(`dum')
		qui replace `first_by_var' = `binned'
	}
	if `number_of_by_vars'==2 & `binscatter'!=0 {
		local first_by_var : word 1 of `by'
		tempvar miss dum binned
		g `miss' = missing(`first_by_var')
		bys `miss' `2' (`first_by_var'): gen `dum' = int(`binscatter'*(_n-1)/_N)+1
		egen `binned' = mean(`first_by_var'), by(`dum' `2')
		qui replace `first_by_var' = `binned'
	}

	// Controlling for covariates
	if "`controls'"!="" {
		local i 1
		foreach v in `varlist' {
			egen ctrl_byvar = group(`first_by_var')
			reghdfe `v' `controls', absorb(ctrl_byvar, savefe ) resid
			predict `v'_resid, residuals
			predict `v'_fe, d
			predict `v'_xb, xb
			sum `v'_xb
			replace `v'_xb = `v'_xb - r(mean)
			replace `v' = `v' - `v'_xb
			// label var `v'_resid "Variance of `title`i''"
			local i = `i' + 1
		}
	}

	// Collapse the data
	if "`gtools'"=="gtools"      gcollapse  `clist' if `touse' [`weight' `exp'], by(`by') fast
	else if "`ftools'"=="ftools" fcollapse  `clist' if `touse' [`weight' `exp'], by(`by') fast
	else qui                      collapse  `clist' if `touse' [`weight' `exp'], by(`by') fast

	// Create labels for the statistics
	foreach s in `stat' {
		local j 1
		foreach v in `varlist' {
			if "`s'"=="var"	{
				replace `v'_`s' = `v'_`s'^2
				label var `v'_`s' "Variance of `title`j++''"
			}
			if "`s'"=="mean" {
				g `v'hi = `v'_mean + 1.96*sd`v'/sqrt(obs`v')
				g `v'lo = `v'_mean - 1.96*sd`v'/sqrt(obs`v')
				label var `v'_`s' "`title`j++''"
			}
			else {
				__statlabel `s'
				label var `v'_`s' "`__statlabel' of `title`j++''"
				// label var `v'_`s' "`title`j++''"
			}
		}
	}

	// Renormalize Variables to Baseline by subtracting the mean of the baseline category (mostly makes sense for log variables)
	if `"`baseline'"'!=`""' & `number_of_by_vars'==1 {
		// For encoded string variables, find the numeric code for the baseline string
		if `by1_is_string' {
			local baseline_code .
			local lblname : value label `by'
			qui levelsof `by', local(by_values)
			foreach val of local by_values {
				local lbl : label `lblname' `val'
				if `"`lbl'"' == `"`baseline'"' {
					local baseline_code `val'
				}
			}
			if `baseline_code' == . {
				di as error "Baseline value '`baseline'' not found in by-variable"
				error 198
			}
		}
		else {
			local baseline_code `baseline'
		}
		foreach s in `stat' {
			foreach v in `varlist' {
				qui sum `v'_`s' if `by'==`baseline_code'
				replace `v'_`s' = `v'_`s' - r(mean)
				if "`s'"=="mean" {
					replace `v'hi = `v'hi - r(mean)
					replace `v'lo = `v'lo - r(mean)
				}
			}
		}
	}
	if `"`baseline'"'!=`""' & `number_of_by_vars'==2 {
		// For encoded string variables, find the numeric code for the baseline string
		if `by1_is_string' {
			local baseline_code .
			local lblname : value label `1'
			qui levelsof `1', local(by_values)
			foreach val of local by_values {
				local lbl : label `lblname' `val'
				if `"`lbl'"' == `"`baseline'"' {
					local baseline_code `val'
				}
			}
			if `baseline_code' == . {
				di as error "Baseline value '`baseline'' not found in first by-variable"
				error 198
			}
		}
		else {
			local baseline_code `baseline'
		}
		foreach s in `stat' {
			foreach v in `varlist' {
				qui tab `2', gen(__dby2_)
				forvalues i =1/`N_unique' {
					qui sum `v'_`s' if __dby2_`i'==1 & `1'==`baseline_code'
					replace `v'_`s' = `v'_`s' - r(mean) if __dby2_`i'==1
					if "`s'"=="mean" {
						replace `v'hi = `v'hi - r(mean) if __dby2_`i'==1
						replace `v'lo = `v'lo - r(mean) if __dby2_`i'==1
					}
				}
				drop __dby2_*
			}
		}
	}

	// Apply xorder sorting and create rank variable for categorical x-axis
	if "`xorder_var'" != "" {
		// Determine which variable to sort by
		if "`__xorder_use_existing'" == "1" {
			// Use the first stat of the xorder variable from varlist
			local xorder_sortby `xorder_var'_`=word("`stat'", 1)'
		}
		else {
			local xorder_sortby __xorder_sortvar
		}

		// Sort the collapsed data
		if `xorder_desc' {
			gsort -`xorder_sortby'
		}
		else {
			sort `xorder_sortby'
		}

		// Generate rank variable
		gen __xrank = _n

		// Rebuild xla mapping ranks to original category labels
		local xla ""
		if `number_of_by_vars'==1 {
			local lblname : value label `by'
			forval i = 1/`=_N' {
				if "`lblname'" != "" {
					local orig_val = `by'[`i']
					local lbl : label `lblname' `orig_val'
				}
				else {
					local lbl = `by'[`i']
				}
				local xla `xla' `i' `"`lbl'"'
			}
			// Replace by variable with rank for plotting
			drop `by'
			rename __xrank `by'
		}
		else {
			local lblname : value label `1'
			forval i = 1/`=_N' {
				if "`lblname'" != "" {
					local orig_val = `1'[`i']
					local lbl : label `lblname' `orig_val'
				}
				else {
					local lbl = `1'[`i']
				}
				local xla `xla' `i' `"`lbl'"'
			}
			// Replace first by variable with rank for plotting
			drop `1'
			rename __xrank `1'
		}

		// Recalculate x-axis padding for new rank-based positions
		local xmin = 1
		local xmax = _N
		local xpad = (`xmax' - `xmin') * 0.05
		if `xpad' < 0.5 local xpad = 0.5
		local xscale_opt xscale(range(`=`xmin'-`xpad'' `=`xmax'+`xpad''))
	}

	// Create labels for observation counts for each outcome variable and count the number of outcome variables
	local varcount 0
	foreach v in `varlist' {
		label var obs`v' "No. Observations"
		local varcount = `varcount'+1
	}

	// If there is only one outcome variable, use its label as y-axis title
	if `varcount'==1 {
		if `"`ytitle'"'=="" local ytitle ytitle(`"`title1'"')
	}

	// Set the graph command to be used
	local graphcmd connected
	if "`scatter'"!="" local graphcmd scatter
	if "`line'"!="" local graphcmd line

	// Build graph command if there is only one by variable
	local colors_original  "`colors'"
	if `number_of_by_vars'==1 {
		local notes ""Number of observations: `N'" "
		if  "`stat'"=="mean" & "`noci'"=="" {
			local i 1
			foreach v in `varlist' {
				gettoken col colors:colors

				if `msymbol_dum' {
					gettoken msym msymbols:msymbols
					local msymbol msymbol(`msym')
				}
				else local msymbol msymbol(none)


				if `lpattern_dum' {
					gettoken lpat lpatterns:lpatterns
					// 					local lpattern lpattern(`"`lpat'"')
					local lpattern lpattern(`"`lpat'"')
				}
				else {
					local lpattern lpattern("#")
				}

				if "`mcounts'"!=""{
					local mlabel mlabel(obs`v') mlabcolor(black) mlabsize(vsmall) mlabposition(1)
				}
				// Confidence intervals depending on formatting
				if "`cipattern'"=="lines" {
					local graphs 	`graphs'	///
						(`graphcmd' `v'hi `by', lpattern("#") color("`col' *.5") msymbol(none) )  ///
						(`graphcmd' `v'lo `by' , lpattern("#") color("`col' *.5") msymbol(none) )
					local legend_order_jump 3
				}
				else if "`cipattern'"=="shaded" {
					local graphs 	`graphs'	///
						(rarea `v'hi `v'lo `by', color("`col' % `ciopacity'") )
					local legend_order_jump 2
				}

				if "`lfit'"=="lfit" {
					if `varcount'==1 & `sc'==1 local lfit_col maroon
					else local lfit_col `col'
					local graphs 	`graphs'	(lfit `v'_mean `by' , lpattern("shortdash") color(`lfit_col') )
					local legend_order_jump = `legend_order_jump'+1
				}
				local graphs `graphs' (`graphcmd' `v'_mean `by' , `lpattern' `msymbol' `msize' `mlabel' `lwidth' color("`col'") )

				if "`lfit'"=="lfit" local order `order' `=`i'*`legend_order_jump''
				else local order `order' `=`i++'*`legend_order_jump''
			}
			if "`45deg'"=="45deg" {
				local graphs `graphs' (line `by' `by' , lpattern("-") color(gray) )
			}
		}
		if  "`stat'"!="mean" | "`noci'"!="" {
			local i 1
			foreach s in `stat' {
				foreach v in `varlist' {
					gettoken col colors:colors
					if `msymbol_dum' {
						gettoken msym msymbols:msymbols
						local msymbol msymbol(`msym')
					}
					else local msymbol msymbol(none)

					if `lpattern_dum'  {
						gettoken lpat lpatterns:lpatterns
						// 					local lpattern lpattern(`"`lpat'"')
						local lpattern lpattern(`"`lpat'"')
					}
					else {
						local lpattern lpattern("#")
					}

					if "`lfit'"=="lfit" {
						if `varcount'==1 & `sc'==1 local lfit_col maroon
						else local lfit_col `col'
						local graphs 	`graphs'	(lfit `v'_`s' `by' , lpattern("shortdash") color(`lfit_col') )
					}
					if "`mcounts'"!=""{
						local mlabel mlabel(obs`v') mlabcolor(black) mlabsize(vsmall) mlabposition(1)
					}

					local graphs `graphs' (`graphcmd' `v'_`s' `by' , `lpattern' `msymbol' `msize' `mlabel' `lwidth' color("`col'") )

					// local statlabel : variable label `v'_`s'
					// local legendlabel `legendlabel' label(`i' "`s'")
					if "`lfit'"=="lfit" local order `order' `=`i'*2'
					else local order `order' `=`i++'*1'
				}
			}
			if "`45deg'"=="45deg" {
				local graphs `graphs' (line `by' `by' , lpattern("-") color(gray) )
			}
		}
		// Add Regression Coefficient of Linear Fit to Graph / only for mean
		if "`coef'"=="coef" & strpos("`stat'", "mean") > 0 {
			local colors "`colors_original'"

			// Compute min and max across all variables in varlist
			local ymin = .
			local ymax = .
			foreach v in `varlist' {
				qui sum `v'_mean
				if r(min) < `ymin' | `ymin' == . {
					local ymin = r(min)
				}
				if r(max) > `ymax' | `ymax' == . {
					local ymax = r(max)
				}
			}
			sum `by'
			local xmin = r(min)
			local xmax = r(max)
			local i 1
			foreach v in `varlist' {
				gettoken col colors:colors
				reg `v'_mean `by'
				__localfmt coef_b = _b[`by'], digits(a2)
				__localfmt coef_se = _se[`by'], digits(a2)

				local xpos = `xmin' + 0.85 * (`xmax' - `xmin')
				if `coef_b' >0  local ypos =  `ymin' + (0.075 * `i') * (`ymax' - `ymin')
				if `coef_b' <0  local ypos =  `ymin' + (0.95 - (0.075 * `i')) * (`ymax' - `ymin')
				local txt `txt' text(`ypos' `xpos' "Slope: `coef_b' [`coef_se']", color("`col'"))
				local i = `i' + 1
			}
		}
	}

	// Build graph command if there are two by variables
	if `number_of_by_vars'==2 { // go over categories of second by variable
		qui tab `2', gen(__dby2_)
		foreach v in `varlist' {
			local cattit `by1_original_label'
			// Count observations in by groups:
			tempvar N
			g `N' = .
			bys `2' `1': replace `N' = sum(obs`v')
			local coef_offset 0
			forvalues i =1/`N_unique' {

				local catlabel : variable label __dby2_`i'
				local catlabel = subinstr("`catlabel'","`2'==","",.)
				qui sum `N' if __dby2_`i'==1
				local max = r(max)
				local notes "`notes' "Number of obs, `catlabel':    `max'""

				gettoken col colors:colors
				if `msymbol_dum' {
					gettoken msym msymbols:msymbols
					local msymbol msymbol(`msym')
				}
				else local msymbol msymbol(none)
				if "`mcounts'"!=""{
					local mlabel mlabel(obs`v') mlabcolor(black) mlabsize(vsmall) mlabposition(1)
				}
				if `lpattern_dum'  {
					gettoken lpat lpatterns:lpatterns
					// 					local lpattern lpattern(`"`lpat'"')
					local lpattern lpattern(`"`lpat'"')
				}
				else {
					local lpattern lpattern("#")
				}


				di `"`noci'"'

				if "`noci'"=="noci" {
					if "`lfit'"=="lfit" {
						local graphs 	`graphs'	(lfit `v'_`stat'   `1' if __dby2_`i'==1, lpattern("shortdash") color("`col'") )
					}

					local graphs `graphs' ///
						(`graphcmd' `v'_`stat'   `1' if __dby2_`i'==1 ,  `lpattern' `msymbol' `msize' `mlabel' `lwidth' color("`col'") )

					if "`lfit'"=="lfit" {
						local legendlabel `legendlabel' label(`=`i'*2' "`catlabel'")
						local order `order' `=`i'*2'
					}
					else {
						local order `order' `i'
						local legendlabel `legendlabel' label(`i' "`catlabel'")
					}
				}
				else {

					// Confidence intervals depending on formatting
					if "`cipattern'"=="lines"  {
						local graphs `graphs' ///
							(`graphcmd' `v'hi `1' if __dby2_`i'==1  , lpattern("#") color("`col' *.5") msymbol(none) )  ///
							(`graphcmd' `v'lo `1' if __dby2_`i'==1  , lpattern("#") color("`col' *.5") msymbol(none) )
							local legendlabel `legendlabel' label(`=`i'*3' "`catlabel'")
							local order `order' `=`i'*3'
					}
					else if "`cipattern'"=="shaded" {
						local graphs `graphs' (rarea `v'hi `v'lo `1' if __dby2_`i'==1 , color("`col' % `ciopacity'") )
						local legendlabel `legendlabel' label(`=`i'*2' "`catlabel'")
						local order `order' `=`i'*2'
					}
					local graphs `graphs' (`graphcmd' `v'_`stat'  `1' if __dby2_`i'==1 , `lpattern' `msymbol' `msize' `mlabel' `lwidth' color("`col'") )


				}
				if "`coef'"=="coef" {
					reg `v'_`stat' `1' if __dby2_`i'==1
					__localfmt coef_b = _b[`1'], digits(a2)
					__localfmt coef_se = _se[`1'], digits(a2)
					sum `v'_`stat' // if __dby2_`i'==1
					local ymin = r(min)
					local ymax = r(max)
					sum `1'
					local xmin = r(min)
					local xmax = r(max)
					local xpos = `xmin' + 0.85 * (`xmax' - `xmin')
					if `coef_b' >0  local ypos =  `ymin' + (0.075 * `i') * (`ymax' - `ymin')
					if `coef_b' <0  local ypos =  `ymin' + (0.95 - (0.075 * `i')) * (`ymax' - `ymin')
					local txt `txt' text(`ypos' `xpos' "Slope: `coef_b' [`coef_se']", color("`col'"))
				}
			}
		}
		if "`45deg'"=="45deg" {
			local graphs `graphs' (line `1' `1' , lpattern("-") color(gray) )
		}
	}

	// local N_legend_items: word count `order'
	// if `N_legend_items' > 1 local legcol legend(col(2))

	if "`addnotes'"=="addnotes" {
		if "`noci'"=="" local notes "`notes' "Bands are 95% confidence intervals""
		if "`nodate'"=="" local notes "`notes' "`c(current_date)' at `c(current_time)'" "
	}
	else {
		local notes
	}
	if "`samplenotes'"!=""  local notes "`notes' "`samplenotes'" "
	/* if "`nonotes'"=="nonotes" local notes */
	// 	if "`legendlabel'"!="" local legendlabel legend(`legendlabel')

	sort `by'
	if `"`figtitle'"'==`""' {
		if `sc'==1 {
			local figtitle `"`=proper("`stat'")' `title1' by `cattit'"'
		}
		else {
			local figtitle `"`title1' by `cattit'"'
		}
	}


	if `"`notitle'"'=="notitle" local figtitle ""


	local legend_columns = 2

	// if `"`subtitle'"' != "" {
	// 	local subtitle subtitle(`"`subtitle'"', margin(small) size(small) )
	// }
	// Build xlabel option: if xla has value labels use those, otherwise use user's numlist
	if `"`xla'"' != "" {
		// xla contains value labels, add user's suboptions after
		local xlabel_opt xlabel(`xla', `xlabel_labsize' `xlabel_user')
	}
	else if `"`xlabel_user'"' != "" {
		// No value labels, user provided xlabel content (numlist and/or suboptions)
		local xlabel_opt xlabel(`xlabel_user', `xlabel_labsize')
	}
	else {
		// Neither value labels nor user xlabel, just use default labsize
		local xlabel_opt xlabel(, `xlabel_labsize')
	}

	twoway  ///
		`graphs' ///
		, `scheme' ///
		title(`"`figtitle'"', margin(small) size(small) ) /// box bexpand
		`subtitle' ///
		legend(order(`order') pos(6) ring(1) region(color(none) margin(zero)) cols(`legend_columns') ///
		size(small) symysize(*.5) symxsize(*1.2) ///
		`legendlabel') ///
		legend(note(`notes' , ///
		size(vsmall) pos(4) ring(1) justification(right) xoffset(0))) ///
		xtitle(`"`cattit'"') `xlabel_opt' `xscale_opt' ylabel(, `ylabel_labsize' `ylabel') ///
		ysize(7.5) xsize(10) graphr(color(white)) `nameopt' `options' ///
		`ytitle' `txt'

	if "`list'"!="" {
		// Build pattern to match all outcome variables (not just last one)
		local list_vars ""
		foreach v in `varlist' {
			local list_vars `list_vars' *`v'*
		}
		list `by' `list_vars', clean noo div
	}

	// Save collapsed data if requested
	if `"`savefile'"' != "" {
		qui save `"`savefile'"', `savereplace'
	}

	restore

end


/*-------------------------------------------------------*/
/* Helper to parse saving() option in isolated namespace  */
/*-------------------------------------------------------*/
cap program drop __parse_saving
program define __parse_saving, sclass
	syntax anything(name=savefile) [, replace]
	sreturn local savefile `"`savefile'"'
	sreturn local savereplace "`replace'"
end

/*-------------------------------------------------------*/
/* Helper to parse xorder() suboptions in isolated namespace */
/*-------------------------------------------------------*/
cap program drop __parse_xorder_opts
program define __parse_xorder_opts, sclass
	syntax , [Descending Stat(str)]
	sreturn local descending = ("`descending'" == "descending")
	sreturn local stat "`stat'"
end


cap program drop __statlabel
program define __statlabel
	local stat `0'
	if "`stat'"=="sd"  local __statlabel "SD"
	if "`stat'"=="iqr" local __statlabel "Interquartile Range"
	if "`stat'"=="median" local __statlabel "Median"
	if "`stat'"=="count" local __statlabel "Count"

	forval i = 1/99 {
		if "`stat'"=="p`i'" {
			if mod(`i',10)==1 local pfx st
			else if mod(`i',10)==2 local pfx nd
			else if mod(`i',10)==3 local pfx rd
			else local pfx th
			if inlist(`i',11,12,13) local pfx th
			local __statlabel "`i'`pfx' pct"
		}
	}

	if "`__statlabel'"=="" local __statlabel "`stat'"
	c_local __statlabel `"`__statlabel'"'
end // __statlabel

/*-------------------------------------------------------*/
/* Tool to calculate expression, format and save in local  */
/*-------------------------------------------------------*/
cap program drop __localfmt
program define __localfmt
	syntax name =exp, digits(str)  // fmt = a2

	__SignificantDigits `digits' ``exp''
	local formatted_local : disp `fmt' ``exp''
	c_local `namelist' `formatted_local'

end // __localfmt

/*-------------------------------------------------------*/
/* Tool to calculate expression, format and save in local  */
/*-------------------------------------------------------*/
cap program drop __SignificantDigits
program define __SignificantDigits // idea stolen from outreg2.ado
	args fmt value
	local d = substr("`fmt'", 2, .)
	capt confirm integer number `d'
	if _rc {
		di as err `"`fmt' not allowed"'
		exit 198
	}
	// missing: format does not matter
	if `value'>=. local fmt "%9.0g"
	// integer: print no decimal places
	else if (`value'-int(`value'))==0 {
		local fmt "%12.0f"
	}
	// value in (-1,1): display up to 9 decimal places with d significant
	// digits, then switch to e-format with d-1 decimal places
	else if abs(`value')<1 {
		local right = -int(log10(abs(`value'-int(`value')))) // zeros after dp
		local dec = max(1,`d' + `right')
		if `dec'<=9 {
			local fmt "%12.`dec'f"
		}
		else {
			local fmt "%12.`=min(9,`d'-1)'e"
		}
	}
	// |values|>=1: display d+1 significant digits or more with at least one
	// decimal place and up to nine digits before the decimal point, then
	// switch to e-format
	else {
		local left = int(log10(abs(`value'))+1) // digits before dp
		if `left'<=9 {
			local fmt "%12.`=max(1,`d' - `left' + 1)'f"
		}
		else {
			local fmt "%12.0e" // alternatively: "%12.`=min(9,`d'-1)'e"
		}
	}
	c_local fmt "`fmt'"
end

/*---------------------------------------------------------*/
/* Program to create local macros containing several string
   elements.
	e.g. locallist First Element; Second Element; Third, name(ellist)
	produces a local macro ellist containing:
	`"First Element"' `"Second Element"' `"Third"'
	Notice the correct placement of the `" and "'
*/
/*---------------------------------------------------------*/

capture program drop __locallist
program define __locallist
	syntax anything, Name(str) [Parse(str)]
	if `"`parse'"'=="" local parse `";"'
	tokenize `"`anything'"', parse(`"`parse'"')
	local i 0
	while `"`1'"'!="" {
		if `"`1'"'!=`"`parse'"' {
			if !`i++' local `name' `"`"`1'"'"'
			else local `name' `"``name'' `"`1'"'"'
		}
		mac shift
	}
	c_local `name' `"``name''"'
end

// sysuse auto , clear
// cellgraph price, by(gear_ratio foreign) msymbols(triangle diamond)