* Program to generate forestplots -- used by ipdmetan etc. but can also be run by itself * April 2013 * Forked from main ipdmetan code * September 2013 * Following UK Stata Users Meeting, reworked the plotid() option as recommended by Vince Wiggins * version 1.0 David Fisher 31jan2014 * version 1.01 David Fisher 07feb2014 * Reason: fixed bug - random-effects note being overlaid on x-axis labels * version 1.02 David Fisher 20feb2014 * Reason: allow user to affect null line options * version 1.03 David Fisher 23jul2014 * Reason: implented a couple of suggestions from Phil Jones * Weighting is now consistent across plotid groups * Tidying up some code that unnecessarily restricted where user-defined lcols/rcols could be plotted * Minor bug fixes and code simplification * New (improved?) textsize and aspect ratio algorithm * version 1.04 David Fisher 29jun2015 // Reason: Major update to coincide with publication of Stata Journal article * Aug 2014: fixed issue with _labels * updated SpreadTitle to accept null strings * added 'noBOX' option * Oct 2014: added "newwt" option to "dataid" to reset weights * Jan 2015: re-written leftWD/rightWD sections to use variable formats and manually-calculated indents * rather than using char(160), since this isn't necessarily mapped to "non-breaking space" on all machines * May 2015: Fixed issue with "clipping" long column headings * May 2015: Option to save parameters (aspect ratio, text size, positioning of text columns relative to x-axis tickmarks) * in a matrix, to be used by a subsequent -forestplot- call to maintain consistency * October 2015: Minor fixes to agree with new ipdmetan/admetan versions * July 2016: added rfdist * 30th Sep 2016: added "range(min max)" option so that range = min(_LCI) to max(_UCI) * Coding of _USE: * _USE == 0 subgroup labels (headings) * _USE == 1 successfully estimated trial-level effects * _USE == 2 unsuccessfully estimated trial-level effects ("Insufficient data") * _USE == 3 subgroup effects * _USE == 4 between-subgroup heterogeneity info and/or `hetinfo' placed on new line * _USE == 5 overall effect * _USE == 6 blank lines (text/data in such rows will be ignored in the plot) * _USE == 7 prediction interval data * _USE == 9 titles (internal only) * version 2.0 David Fisher 11may2017 // Not updated nearly as much as -admetan-, -ipdmetan- and -ipdover- // but up-versioned to match * version 2.1 David Fisher 14sep2017 // various bug fixes // improvements to range() and cirange() // improvements to rfopts // - N.B. cannot override "interaction" option with pointopts(msymbol(square)) -- is this a bug or a feature? // for next version: include addplot() option ? * version 3.0 David Fisher 08nov2018 * version 3.1 David Fisher 03dec2018 // only implement lalign() if c(stata_version)>=15 // corrected order of `graphopts' and `fpuseopts' so that -useopts- works as intended // -forestplot- now consistently honours blank varlabels in lcols/rcols (whether string or numeric) * version 3.2 David Fisher 28jan2019 // corrected error which caused first help-file example to fail // some text in help file is updated // improved counting of rows in titles containing compound quotes * version 4.00 David Fisher 25nov2020 // changes to `xlabopts' // changes to `influence' plot (including "hide" option) // changes to fp() option // -double- option added back in; "height" etc. now calculated using values of `id' rather than by counting observations // minor change to ProcessColumns to ensure "95% CI" (in _EFFECT varlabel) is not broken across lines // upversioned to match with -metan- * version 4.01 David Fisher 12feb2021 // minor change to behaviour of -extraline()- and -lcolscheck- // fixed bug in -favours()- which sometimes caused quotes to appear in plot // improvements to code so that earlier versions of Stata do not truncate plot macros // (thanks to Daniel Klein for assistance with testing of earlier versions) * version 4.02 David Fisher 23feb2021 // No changes to -forestplot- code; upversioned to match with -metan- * version 4.03 David Fisher 28apr2021 // fixed automated choice of x-axis with proportions with denominator(#) // now catches extreme cases where `DXmin' or `DXmax' are missing // only implement lalign() if 15.1+ , following user reports that fails with 15.0 // corrected bug which failed to show diamonds correctly if off-scale * version 4.04 David Fisher 16aug2021 // added prefix() option * version 4.05 David Fisher 29nov2021 // fixed bug preventing plotid() and dataid() being specified together // added ability to modify "border line" between the data and the column headings // new option "nooverlay" in two new places: // - for drawing weighted boxes on top of conf. ints. (instead of default = overlaying CIs on top of boxes) // - for drawing data (boxes, CIs etc.) on top of null and/or overall line(s) * version 4.06 David Fisher 12oct2022 // new option "sepline" for drawing prediction interval lines separately (below) confidence interval lines instead of straddling them * version 4.07 David Fisher 15sep2023 // no changes; upversioned to v4.07 alongside metan.ado *! version 4.08 David Fisher 17jun2024 // Q heterogeneity p-value now shows in forestplot as "< 0.001" rather than "= 0.000" // right-alignment of columns in -forestplot- now done via mlabpos() rather than explicit indentation // works with metan v4.07+ : new value _USE==7 defining prediction intervals // fixed bug whereby matname `usedims' might be misinterpreted by Stata as a varname, leading to error // fixed bug whereby untransformed proportions led to forestplot expecting _Prop_ES etc. rather than _ES, leading to error // improvements to ocilineopts() and rfcilineopts() // text printed to screen no longer includes "use() labels() wgt() by()" if these options are empty // `colsonly' option moved out of beta and fully documented program define forestplot, sortpreserve rclass version 11.0 // needs v11 for SMCL in graphs // June 2018 [updated Oct 2018]: check for "useopts", which recreates previous -metan- (or ipdmetan/ipdover) call syntax [varlist(numeric max=5 default=none)] [if] [in] [, USEOPTs * ] local graphopts `"`options'"' local usevlist `varlist' local useifin `if' `in' if `"`useopts'"'!=`""' { local orig_gropts : copy local graphopts local fpusevlist : char _dta[FPUseVarlist] local fpuseifin : char _dta[FPUseIfIn] local fpuseopts : char _dta[FPUseOpts] if `"`fpusevlist'`fpuseifin'`fpuseopts'"'==`""' { nois disp as err `"No stored {bf:forestplot} options found"' exit 198 } // varlist and if/in: if supplied directly, overwrite characteristics if `"`usevlist'"'==`""' local usevlist `fpusevlist' if `"`useifin'"'==`""' local useifin `fpuseifin' local fpcmdline = trim(itrim(`"forestplot `usevlist' `if' `in', `fpuseopts' `graphopts'"')) nois disp as text `"Full command line as defined by {bf:useopts} is as follows:"' nois disp as res `" `fpcmdline'"' nois disp as text `"(Note: any or all of this information may be over-ridden by other options;"' nois disp as text `" in general only the rightmost of any repeated options will be honoured, but see {help repeated_options})"' } // Nov 2018: note that -syntax- is *leftmost*, not rightmost; so `graphopts' must come first to overrule `fpuseopts' local 0 `"`usevlist' `useifin', `graphopts' `fpuseopts'"' // June 2018: main parse syntax [varlist(numeric max=5 default=none)] [if] [in] [, WGT(varname numeric) USE(varname numeric) PREfix(name local) /// /// /// /* General user-specified -forestplot- options */ BY(varname) EFORM EFFect(string asis) LABels(varname string) DP(integer 2) KEEPAll USESTRICT /*(undocumented)*/ /// INTERaction LCols(namelist) RCols(namelist) LEFTJustify COLSONLY RFDIST(varlist numeric min=2 max=2) RFLevel(passthru) /// NULLOFF noNAmes noNULL NULL2(string) noKEEPVars noOVerall noSUbgroup noSTATs noWT noHET LEVEL(passthru) ILevel(passthru) OLevel(passthru) /// XTItle(passthru) /*FAVours(passthru)*/ /// /* N.B. -xtitle- is parsed here so that a blank title can be inserted if necessary */ CUmulative INFluence /*PRoportion*/ DENOMinator(passthru) /// /* undocumented; passed through from -metan-; needed in order to implement "hide" option... */ /// /* ...and to control default null line/x-axis (e.g. for proportion/influence) /// /* Sub-plot identifier for applying different appearance options, and dataset identifier to separate plots */ PLOTID(string) DATAID(string) /// /// TEXTSize(passthru) /// /* legacy -metan9- option, implemented here as a post-hoc option; use at own risk */ /// /* "fine-tuning" options */ SAVEDIms(name) USEDIms(name) ASText(real -9) noADJust /// noPREFIXWARN /// /*(undocumented; suppress "using default varlist" message if passing directly from -metan- using prefix()*/ KEEPXLabs * ] /*(undocumented; colsonly option)*/ local graphopts `"`options'"' // "graph region" options (also includes plotopts for now) marksample touse, novarlist // do this immediately, so that -syntax- can be used again ** N.B. Parts of this early setup may repeat work already done by calling program (e.g. -metan- ) // but hopefully the extra overhead is negligible // Set up variable names if `"`varlist'"'!=`""' { tokenize `varlist' if `"`4'"'!=`""' { nois disp as err `"Syntax has changed as of ipdmetan v2.0 09may2017"' nois disp as err `"{bf:_WT} and {bf:_USE} should now be specified using options {bf:wgt()} and {bf:use()}"' exit 198 } if `"`2'"'==`""' | `"`3'"'==`""' { nois disp as err `"{it:varlist} detected but with too few members; syntax is {it:es lci uci}"' exit 198 } } else { // if not specified, assume "standard" varnames if `"`denominator'"'==`""' { local varlist `prefix'_ES `prefix'_LCI `prefix'_UCI if `"`prefixwarn'"'==`""' nois disp as text `"Note: no {it:varlist} specified; using default {it:varlist}"' as res `" {bf:`varlist'}"' } else { local varlist `prefix'_Prop_ES `prefix'_Prop_LCI `prefix'_Prop_UCI // August 2023 if `"`prefixwarn'"'==`""' { nois disp as text `"Note: no {it:varlist} specified; using default {it:varlist}"' as res `" {bf:`varlist'}"' nois disp as text `" due to option {bf:denominator(}{it:#}{bf:)} being specified"' } foreach x in Prop_ES Prop_LCI Prop_UCI { cap confirm var `prefix'_`x' if _rc { if `"`prefixwarn'"'==`""' { nois disp as text `"Note: expected to find variable "' as res `"{bf:`prefix'_`x'}"' as text `", but failed;"' nois disp as text `"assuming proportions pooled on untransformed scale using variables "' as res `"{bf:`prefix'_ES}, {bf:`prefix'_LCI}, {bf:`prefix'_UCI}"' } local varlist `prefix'_ES `prefix'_LCI `prefix'_UCI // March 2024 continue, break } } } tokenize `varlist' } args _ES _LCI _UCI foreach x in _ES _LCI _UCI { confirm numeric var ``x'' } // Set up data sample to use local _USE `use' if `"`use'"'==`""' { cap confirm numeric var `prefix'_USE if !_rc { if `"`prefixwarn'"'==`""' { nois disp as text `"Note: option {bf:use(}{it:varname}{bf:)} not specified; using default {it:varname}"' as res `" {bf:`prefix'_USE}"' } local _USE `prefix'_USE } else { if _rc!=7 { // if _USE does not exist tempvar _USE qui gen byte `_USE' = cond(missing(`_ES', `_LCI', `_UCI'), 2, 1) nois disp as text `"Note: default variable"' as res `" {bf:`prefix'_USE} "' as text `"not found; all included observations will be assumed to contain study estimates"' } else { nois disp as err `"Default variable {bf:`prefix'_USE} exists but is not numeric"' exit 198 } } } confirm numeric variable `_USE' markout `touse' `_USE' // observations for which _USE is missing qui replace `touse' = 0 if inlist(`_USE', 3, 4) & `"`subgroup'"'!=`""' qui replace `touse' = 0 if `_USE' == 4 & `"`het'"'!=`""' qui replace `touse' = 0 if `_USE' == 5 & `"`overall'"'!=`""' // qui replace `touse' = 0 if inlist(`_USE', 3, 5) & missing(`_ES') & `"`stats'"'!=`""' & `"`rfdist'"'!=`""' qui replace `touse' = 0 if `_USE' == 7 & `"`stats'"'!=`""' & `"`rfdist'"'!=`""' if `"`keepall'"'==`""' qui replace `touse' = 0 if `_USE'==2 // "keepall" option (see -metan-) qui count if `touse' if !r(N) { nois disp as err "no observations" exit 2000 } // return scalar obs = r(N) // Jan 2020: do this later, after BuildPlotCmds, in case of "hidden" pooled observations // Check that UCI is greater than LCI cap assert `_UCI' > `_LCI' if `touse' & !missing(`_UCI') & !(float(`_LCI')==float(`_ES') & float(`_ES')==float(`_UCI')) if _rc { nois disp as err "Error in confidence interval data; please check the following observations:" nois list `_USE' `_LCI' `_UCI' if `touse' & !missing(`_UCI') & !(`_UCI' > `_LCI') & !(float(`_LCI')==float(`_ES') & float(`_ES')==float(`_UCI')) exit 198 } // Weighting variable local _WT `wgt' if `"`wgt'"'==`""' { cap confirm numeric var `prefix'_WT if !_rc { if `"`prefixwarn'"'==`""' { nois disp as text `"Note: option {bf:wgt(}{it:varname}{bf:)} not specified; using default {it:varname}"' as res `" {bf:`prefix'_WT}"' } local _WT `prefix'_WT } else { if _rc!=7 { // if _WT does not exist tempvar _WT qui gen byte `_WT' = 1 if `touse' & inlist(`_USE', 1, 3, 5) // generate as constant if doesn't exist nois disp as text `"Note: default variable"' as res `" {bf:`prefix'_WT} "' as text `"not found; all observations will have equal weights"' local wt nowt // don't display as text column } else { nois disp as err `"Default variable {bf:`prefix'_WT} exists but is not numeric"' exit 198 } } } confirm numeric variable `_WT' // Check existence of `labels' (string) and `by' (should really be numeric but doesn't actually matter) foreach x in labels by { local X = upper("`x'") if `"``x''"'!=`""' local _`X' ``x'' else { cap confirm var `prefix'_`X' if !_rc { local _`X' `prefix'_`X' // use default varnames if they exist and option not explicitly given if "`x'"=="labels" { // don't print message r.e. `by' as it is only used in a minor way by -forestplot- if `"`prefixwarn'"'==`""' { nois disp as text `"Note: option {bf:labels(}{it:varname}{bf:)} not specified; using default {it:varname}"' as res `" {bf:`prefix'_LABELS}"' } } } // Jan 2019 else if "`x'"=="labels" { nois disp as text `"Note: option {bf:labels(}{it:varname}{bf:)} not specified and default {it:varname} {bf:`prefix'_LABELS} not found; observations will be unlabelled"' local names nonames } } } if `"`_LABELS'"'!=`""' { confirm string var `_LABELS' } if `"`_BY'"'!=`""' { confirm var `_BY' } // Check validity of `_USE' (already sorted out existence) // if `usestrict'; otherwise responsibility is with user if `"`usestrict'"'!=`""' { tempvar flag qui gen byte `flag' = `touse' & `_USE'==1 & missing(`_ES', `_LCI', `_UCI', `_WT') qui replace `flag' = 1 if `touse' & `_USE'==2 & !missing(`_ES', `_LCI', `_UCI') if `"`names'"'==`""' { // Jan 2019 qui replace `flag' = 1 if `touse' & `_USE'==6 & !missing(`_LABELS') } qui replace `flag' = 1 if `touse' & inlist(`_USE', 2, 6) & !missing(`_WT') & `"`wt'"'==`""' qui replace `flag' = 1 if `touse' & `_USE' > 6 & !missing(`_USE') qui count if `flag' if r(N) { nois disp as err `"The following observations are inconsistent with {bf:_USE}:"' nois list `_USE' `_LABELS' `_ES' `_LCI' `_UCI' `_WT' if `flag' exit 198 } qui drop `flag' } // Sort out `dataid' and `plotid' tempvar obs touse2 qui gen long `obs' = _n // local nd=1 local 0 `dataid' syntax [varname(default=none)] [, NEWwt] if `"`varlist'"'!=`""' { cap tab `varlist' if `touse', m if _rc { nois disp as err `"error in option {bf:dataid()}"' qui tab `varlist' if `touse', m } if `"`newwt'"'==`""' local dataid `varlist' else { qui gen byte `touse2' = `touse' * inlist(`_USE', 1, 2, 3, 5, 7) local dataid tempvar dtobs dataid // create ordinal version of dataid qui bysort `touse2' `varlist' (`obs') : gen long `dtobs' = `obs'[1] if `touse2' qui bysort `touse2' `dtobs' : gen long `dataid' = (_n==1) if `touse2' qui replace `dataid' = sum(`dataid') if `touse2' label variable `dataid' "dataid" } } if `"`plotid'"'==`""' { tempvar plotid qui gen byte `plotid' = 1 if `touse' // create plotid as constant if not specified; this makes BuildPlotCmds much easier } else { disp _n _c // spacing, in case following on from ipdmetan (etc.) cap confirm var `prefix'_OVER local _OVER = cond(_rc, `""', `"`prefix'_OVER"') local 0 `plotid' syntax name(name=plname id="plotid") [, List noGRaph] local plotid // clear macro; will want to define a tempvar named plotid if "`plname'"!="_n" { confirm var `plname' cap tab `plname' if `touse', m if _rc { nois disp as err `"error in option {bf:plotid()}"' qui tab `plname' if `touse', m } if `"`_OVER'"'==`""' { qui count if `touse' & inlist(`_USE', 1, 2) & missing(`plname') if r(N) { nois disp as err `"Warning: variable {bf:`plname'} (in option {bf:plotid()}) contains missing values"' nois disp as err `"{bf:plotid()} groups and/or allocated numeric codes may not be as expected"' if "`list'"=="" nois disp as err `"This may be checked using the {bf:list} suboption to {bf:plotid()}"' } } } * Create ordinal version of plotid... // tempvar touse2 cap confirm variable `touse2' if _rc { qui gen byte `touse2' = `touse' * inlist(`_USE', 1, 2, 3, 5, 7) } // local plvar `plname' // ...extra tweaking if passed through from (ad)metan/ipdmetan/ipdover (i.e. _STUDY, and possibly _OVER, exists) if inlist("`plname'", "_STUDY", "_n", "_LEVEL", "_OVER") { cap confirm var `prefix'_STUDY local _STUDY = cond(_rc, `"`prefix'_LEVEL"', `"`prefix'_STUDY"') tempvar smiss qui gen byte `smiss' = missing(`_STUDY') if inlist("`plname'", "_STUDY", "_n") { tempvar plvar qui bysort `touse2' `smiss' (`_OVER' `_STUDY') : gen long `plvar' = _n if `touse2' & !`smiss' } else if "`plname'"=="_LEVEL" { tempvar plvar qui bysort `touse2' `smiss' `_BY' (`_OVER' `_STUDY') : gen long `plvar' = _n if `touse2' & !`smiss' } else local plvar `prefix'_OVER } else local plvar `plname' tempvar plobs plotid qui bysort `touse2' `smiss' `plvar' (`obs') : gen long `plobs' = `obs'[1] if `touse2' qui bysort `touse2' `smiss' `plobs' : gen long `plotid' = (_n==1) if `touse2' qui replace `plotid' = sum(`plotid') if `touse2' local np = `plotid'[_N] // number of `plotid' levels (N.B. `plotid' is guaranteed to be ordinal) label variable `plotid' "plotid" * Optionally list observations contained within each plotid group if "`list'" != "" { sort `obs' nois disp as text _n "plotid: observations marked by " as res "`plname'" as text ":" forvalues p=1/`np' { nois disp as text _n "-> plotid = " as res `p' as text ":" nois list `dataid' `_USE' `_BY' `_OVER' `_LABELS' if `touse2' & `plotid'==`p', table noobs sep(0) } if `"`graph'"'!=`""' exit } qui drop `touse2' `plobs' `smiss' } // qui drop `obs' // don't drop yet; use again later // Parse eform option and finalise "effect" text cap nois CheckOpts, soptions opts(`eform' `graphopts') if _rc { if _rc==1 nois disp as err "User break" else nois disp as err `"Error in {bf:forestplot.CheckOpts}"' c_local err noerr // tell calling subroutine not to also report an error exit _rc } local eform `"`s(eform)'"' // either "eform" or nothing local graphopts `"`s(options)'"' if `"`effect'"'==`""' { // amended Feb 2018 due to local x = "" issue with version <13 // local effect = cond(`"`r(effect)'"'=="", "Effect", `"`r(effect)'"') local effect `"`s(effect)'"' if `"`effect'"'==`""' local effect "Effect" if `"`interaction'"'!=`""' local effect `"Interact. `effect'"' } // May 2020: significance levels // If a -metan- results set is used, specifying level() is unnecessary // as the relevant values will be taken from variable characteristics // But levels can be specified manually if necessary. if `"`level'"'!=`""' { if `"`ilevel'"'!=`""' { nois disp as err "Cannot specify both {bf:level()} and {bf:ilevel()}" exit 184 } if `"`olevel'"'!=`""' { nois disp as err "Cannot specify both {bf:level()} and {bf:olevel()}" exit 184 } local ilevel : copy local level local olevel : copy local level local level } local 0 `", `olevel'"' syntax [, OLevel(cilevel) ] * Default placing of labels, effect sizes and weights: // unless noSTATS and/or noWT, effect sizes and weights are first two elements of `rcols' if `"`eform'"'!=`""' local xexp exp if `"`stats'"'==`""' { // determine format summ `_UCI' if `touse', meanonly local fmtx = max(1, ceil(log10(abs(`xexp'(r(max)))))) + 1 + `dp' if `"`keepvars'"'!=`""' tempvar _EFFECT else { cap drop `prefix'_EFFECT local _EFFECT `prefix'_EFFECT } qui gen str `_EFFECT' = string(`xexp'(`_ES'), `"%`fmtx'.`dp'f"') if !missing(`_ES') qui replace `_EFFECT' = `_EFFECT' + " " if !missing(`_EFFECT') qui replace `_EFFECT' = `_EFFECT' + "(" + string(`xexp'(`_LCI'), `"%`fmtx'.`dp'f"') + ", " + string(`xexp'(`_UCI'), `"%`fmtx'.`dp'f"') + ")" qui replace `_EFFECT' = `""' if !(`touse' & inlist(`_USE', 1, 3, 5, 7)) qui replace `_EFFECT' = "(Insufficient data)" if `touse' & `_USE'==2 // March 2020: extra lines to handle "empty" subgroups, and single-study subgroups with `influence' qui replace `_EFFECT' = "(Insufficient data)" if `touse' & inlist(`_USE', 3, 5) & missing(`_LCI') qui replace `_EFFECT' = "(Insufficient data)" if `touse' & `_USE'==1 & missing(`_LCI') & `"`influence'"'!=`""' local f = abs(fmtwidth("`: format `_EFFECT''")) format `_EFFECT' %-`f's // left-justify // variable label if `"`effect'"' == `""' { local effect = cond("`interaction'"!="", "Interaction effect", "Effect") } if `"`ilevel'"'==`""' { local lciLevel : char `_LCI'[Level] local uciLevel : char `_UCI'[Level] if `"`lciLevel'"'!=`""' & `"`uciLevel'"'!=`""' & `"`lciLevel'"'!=`"`uciLevel'"' { nois disp as err "Conflicting confidence limit coverages" exit 198 } local ilevel `lciLevel' if `"`ilevel'"'==`""' local ilevel `uciLevel' } if `"`ilevel'"'==`""' { // if `ilevel' manually specified, use it in preference to the value stored in the variable characteristics local 0 `", `ilevel'"' syntax [, ILevel(cilevel) ] } label variable `_EFFECT' `"`effect' (`ilevel'% CI)"' } if `"`names'"'==`""' local lcols `_LABELS' `lcols' // unless noNAMES specified, add `_LABELS' to `lcols' if "`wt'" == "" local rcols `_WT' `rcols' // unless noWT specified, add `_WT' to `rcols' local rcols `_EFFECT' `rcols' // unless noSTATS specified, add `_EFFECT' to `rcols' // finalise lcols and rcols foreach x of local lcols { cap confirm var `x' if _rc { nois disp as err `"variable {bf:`x'} not found in option {bf:lcols()}"' exit _rc } } foreach x of local rcols { cap confirm var `x' if _rc { nois disp as err `"variable {bf:`x'} not found in option {bf:rcols()}"' exit _rc } } local lcolsN : word count `lcols' local rcolsN : word count `rcols' // [Revised May 2024 for v4.08] // `colsonly' option expects exactly one of `lcolsN' or `rcolsN' to be nonzero if `"`colsonly'"'!=`""' { if !`lcolsN' & !`rcolsN' { disp as err `"Option {bf:colsonly} supplied with no columns of data; nothing to plot"' exit 2000 } else if `lcolsN' & `rcolsN' { disp as err `"Option {bf:colsonly} requires either left-side or right-side data columns, but not both"' exit 198 } } // Check that `keepxlabs' implies `colsonly'; so that later we may use `keepxlabs' in place of `colsonly' where appropriate else if `"`keepxlabs'"'!=`""' { disp as err `"Option {bf:keepxlabs} cannot be specified without {bf:colsonly}"' exit 198 } ** GET MIN AND MAX DISPLAY // [comments from _dispgby subroutine of metan.ado follow] // SORT OUT TICKS- CODE PINCHED FROM MIKE AND FIDDLED. TURNS OUT I'VE BEEN USING SIMILAR NAMES... // AS SUGGESTED BY JS JUST ACCEPT ANYTHING AS TICKS AND RESPONSIBILITY IS TO USER! // N.B. `DXmin', `DXmax' are the left and right co-ords of the graph part // These are NOT NECESSARILY the same as the limits of xlabels, xticks etc. // In particular, if range() is specified then DXmin, DXmax == range; regardless of xlabels, xticks etc. // First, sort out null-line local h0 = 0 // default // if `"`null2'"'!=`""' local nullopt `"null(`null2')"' // Amended Jan 2020 if inlist(trim("`null2'"), "none", "off") local nullopt `"null(`null2')"' opts_exclusive `"`nulloff' `null' `nullopt'"' if `"`nulloff'"'!=`""' local null nonull // "nulloff" and "nonull" are permitted alternatives to null(none|off), // for compatibility with previous versions of -metan- else if `"`null2'"'!=`""' { if inlist("`null2'", "none", "off") local null nonull else { cap nois numlist "`null2'", min(1) max(1) if _rc { disp as err "error in {bf:null()} option" exit _rc } // local h0 = `null2' // May 2020: null2() should be given on same scale as xlabels, to match with fp() local h0 = cond(`"`eform'"'!=`""', ln(`null2'), `null2') if "`null'"!="nonull" local null } } if `"`influence'`denominator'"'!=`""' & inlist(trim("`null2'"), "", "none", "off") local null nonull // N.B. `null' now either contains nothing, or "nonull" // and `h0' contains a number (defaulting to 0), denoting where the null-line will be placed if "`null'"=="" // If `influence' or proportion (i.e. `denominator'), "nonull" is the default unless null2(#) is supplied. // Now find DXmin, DXmax; xticklist, xlablist, xlablim1 summ `_LCI' if `touse', meanonly local DXmin = r(min) // minimum confidence limit summ `_UCI' if `touse', meanonly local DXmax = r(max) // maximum confidence limit if `"`rfdist'"'!=`""' { tokenize `rfdist' args _rfLCI _rfUCI if `"`rflevel'"'==`""' { local lciRFLevel : char `_rfLCI'[RFLevel] local uciRFLevel : char `_rfUCI'[RFLevel] if `"`lciRFLevel'"'!=`""' & `"`uciRFLevel'"'!=`""' & `"`lciRFLevel'"'!=`"`uciRFLevel'"' { nois disp as err "Conflicting confidence limit coverages for predictive interval" exit 198 } local rflevel `lciRFLevel' if `"`rflevel'"'==`""' local rflevel `uciRFLevel' } if `"`rflevel'"'==`""' { // if `rflevel' manually specified, use it in preference to the value stored in the variable characteristics local 0 `", `rflevel'"' syntax [, RFLevel(cilevel) ] } // Note, May 2020: if `rflevel' < `olevel' and low heterogeneity, then (rfLCI, rfUCI) might be tighter than (LCI, UCI) cap { assert missing(`_rfLCI', `_rfUCI') if `touse' & !inlist(`_USE', 3, 5, 4, 7) assert float(`_rfLCI') <= float(`_LCI') if `touse' & !missing(`_rfLCI', `_LCI') & float(`rflevel')>=float(`olevel') assert float(`_rfUCI') >= float(`_UCI') if `touse' & !missing(`_rfUCI', `_UCI') & float(`rflevel')>=float(`olevel') } if _rc { nois disp as err "Error in predictive interval data" exit 198 } summ `_rfLCI' if `touse', meanonly // N.B. unnecessary if passed thru from -metan-, since included in `_LCI'/`_UCI' local DXmin = min(`DXmin', r(min)) // but need to do it anyway summ `_rfUCI' if `touse', meanonly local DXmax = max(`DXmax', r(max)) if `"`stats'"'==`""' { // Generate `rfdindent' to send to -ProcessColumns- // strwid is width of "_ES[_n-1]" as formatted by "%`fmtx'.`dp'f" so it lines up tempvar rfindent qui gen `rfindent' = string(`xexp'(`_ES'[_n-1]), `"%`fmtx'.`dp'f"') if `touse' & `_USE'==7 /* qui gen `rfindent' = cond(`touse' * missing(`_ES') * !missing(`_rfLCI', `_rfUCI'), /// string(`xexp'(`_ES'[_n-1]), `"%`fmtx'.`dp'f"'), `""') */ // Find which column effect sizes (including predictive distribution limits) should appear in, to apply rfindent local rfcol=1 while `"`: word `rfcol' of `rcols''"'!=`"_EFFECT"' & `rfcol' <= `rcolsN' { local ++rfcol } local rfcolopts `"rfindent(`rfindent') rfcol(`rfcol')"' } else { disp as err "Note: options {bf:rfdist} and {bf:nostats} specified together;" disp as err " predictive intervals will be presented graphically but will not appear in text columns" } } // March 2021: handle extreme case if missing(`DXmin') | missing(`DXmax') { summ `_ES' if `touse', meanonly if r(N) { if missing(`DXmin') local DXmin = r(min) if missing(`DXmax') local DXmax = r(max) } else { if missing(`DXmin') local DXmin = `h0' if missing(`DXmax') local DXmax = `h0' } } cap nois ProcessXAxis `DXmin' `DXmax', `eform' h0(`h0') `null' `denominator' `colsonly' `graphopts' if _rc { if _rc==1 nois disp as err `"User break in {bf:forestplot.ProcessXAxis}"' nois disp as err `"Error in {bf:forestplot.ProcessXAxis}"' c_local err noerr // tell calling program (e.g. -metan- ) not to also report an error exit _rc } if "`twowaynote'"!="" c_local twowaynote notwowaynote // so that -metan- does not print an additional message regarding "xlabel" or "force" local CXmin = r(CXmin) // limits of data plotting (i.e. off-scale arrows)... = DX by default local CXmax = r(CXmax) local DXmin = r(DXmin) // limits of data plot region local DXmax = r(DXmax) return local range `"`DXmin' `DXmax'"' local xtitleval = r(xtitleval) // position of xtitle [May 2024: not currently implemented] // May 2024 local graphopts `"`r(xlabopt2)' `r(xlabopt)' `r(xmlabopt2)' `r(xmlabopt)' `r(favopt)' `r(xtickopt2)' `r(xtickopt)' `r(xmtickopt2)' `r(xmtickopt)' `r(options)'"' // Nov 2017 local null `"`r(null)'"' local rowsxlab = r(rowsxlab) local rowsxmlab = r(rowsxmlab) local rowsfav = r(rowsfav) ** Need to make changes to pre-existing data now // e.g. adding new obs to the dataset to contain multi-line column headings // so use -preserve- preserve // [added Nov 2018] // Make data obey the conventions of _USE qui replace `_USE' = 6 if !inrange(`_USE', 0, 7) & `touse' qui replace `_ES' = . if `touse' & `_USE'==2 qui replace `_LCI' = . if `touse' & `_USE'==2 qui replace `_UCI' = . if `touse' & `_USE'==2 if `"`names'"'==`""' { qui replace `_LABELS' = "" if `touse' & `_USE'==6 } qui replace `_WT' = . if `touse' & inlist(`_USE', 2, 6) & `"`wt'"'==`""' * Find `lcimin' = left-most confidence limit among the "diamonds" (including predictive intervals) * (Note: this is *only* used within the `adjust' subroutine within ProcessColumns) tempvar lci2 qui gen `lci2' = cond(`"`null'"'==`""', cond(`_LCI'>`h0', `h0', /// cond(`_LCI'>`CXmin', `_LCI', `CXmin')), cond(`_LCI'>`CXmin', `_LCI', `CXmin')) if `"`rfdist'"'!=`""' { qui replace `lci2' = cond(`"`null'"'==`""', cond(`_rfLCI'>`h0', `h0', /// cond(`_rfLCI'>`CXmin', `_rfLCI', `CXmin')), cond(`_rfLCI'>`CXmin', `_rfLCI', `CXmin')) } summ `lci2' if `touse' & inlist(`_USE', 3, 5, 7), meanonly local lcimin = cond(r(N), r(min), cond(`"`null'"'==`""', `h0', `CXmin')) drop `lci2' * Unpack `usedims' local DXwidthChars = -9 // initialize if `"`usedims'"'!=`""' { cap confirm matrix `usedims' if _rc { nois disp as err "Error in option {bf:usedims()}: " _c confirm matrix `usedims' } local DXwidthChars = `usedims'[1, `=colnumb(matrix(`usedims'), "cdw")'] confirm number `DXwidthChars' assert `DXwidthChars' >= 0 local oldLCImin = `usedims'[1, `=colnumb(matrix(`usedims'), "lcimin")'] confirm number `oldLCImin' // can be <0 local lcimin = min(`lcimin', `oldLCImin') } // Pass exactly one of `DXwidthChars' or `astext' to ProcessColumns // (if specified, `astext' trumps `DXwidthChars') if `"`usedims'"'!=`""' & `astext'==-9 { local astextopt `"dxwidthchars(`DXwidthChars')"' } else { local astext = cond(`astext'==-9, 50, `astext') cap assert `astext' > 0 if _rc { nois disp as err "error in option {bf:astext(}{it:#}{bf:)}: {it:#} must be in the range (0, 100]" exit 125 } local astextopt `"astext(`astext')"' } ** Generate ordering variable (reverse sequential, since y axis runs bottom to top) // Need to do this *before* extra obs are added by ProcessColumns to hold title text // Apr 2020: furthermore, sort such that `touse' obs come *first* ... so need to sort on "negated" `touse' tempvar touse_neg id qui gen byte `touse_neg' = 1 - `touse' qui bysort `touse_neg' (`obs') : gen long `id' = _N - _n + 1 if `touse' drop `touse_neg' ************************ * LEFT & RIGHT COLUMNS * ************************ // Setup: generate tempvars to send to ProcessColumns foreach xx in left right { local x = substr("`xx'", 1, 1) // extract "l" from "left" and "r" from "right" forvalues i=1/``x'colsN' { // N.B. if `lcolsN' or `rcolsN'==0, this loop will be skipped tempvar `xx'`i' local `x'vallist ``x'vallist' ``xx'`i'' // store x-axis positions of columns local `x'coli : word `i' of ``x'cols' local f : format ``x'coli' tokenize `"`f'"', parse("%~s.,") if "`2'"=="~" { // Modified Aug2023 to handle centered format confirm number `3' if `"`leftjustify'"'!=`""' local flen = -abs(`3') else local flen `2'`3' } else { confirm number `2' local flen = `2' if `"`leftjustify'"'!=`""' local flen = -abs(`2') } cap confirm string var ``x'coli' if !_rc local `xx'LB`i' : copy local `x'coli // if string else { // if numeric tempvar `xx'LB`i' if `"`: value label ``x'coli''"'!=`""' { // if labelled (10th July 2017) qui decode ``x'coli', gen(``xx'LB`i'') } else qui gen str ``xx'LB`i'' = string(``x'coli', "`f'") qui replace ``xx'LB`i'' = "" if ``xx'LB`i'' == "." local colName : variable label ``x'coli' // Removed v3.0.1 for consistency with string variables // Now -forestplot- consistently honours *blank* varlabels // if `"`colName'"' == "" & `"``x'coli'"' !=`"`labels'"' local colName = `"``x'coli'"' label variable ``xx'LB`i'' `"`colName'"' } local `x'lablist ``x'lablist' ``xx'LB`i'' // store contents (text/numbers) of columns local `x'fmtlist ``x'fmtlist' `flen' // desired max no. of characters based on format } if !`lcolsN' { tempvar left1 local lvallist `left1' } local `x'optlist `x'vallist(``x'vallist') `x'lablist(``x'lablist') `x'fmtlist(``x'fmtlist') } // niche case: possible that user-specified `_USE' already contains values of 9 for some reason // if so, change them to 99 (doesn't matter what value they are as long as not 0 to 6, or 9) // (and we are under -preserve- ) qui replace `_USE' = 99 if `touse' & `_USE'==9 local oldN = _N cap nois ProcessColumns `_USE' `_EFFECT' if `touse', id(`id') `wt' /// lrcolsn(`lcolsN' `rcolsN') lcimin(`lcimin') dx(`DXmin' `DXmax') /// `loptlist' `roptlist' `rfcolopts' `astextopt' `adjust' `colsonly' `graphopts' if _rc { if _rc==1 nois disp as err `"User break in {bf:forestplot.ProcessColumns}"' else nois disp as err `"Error in {bf:forestplot.ProcessColumns}"' c_local err noerr // tell calling program (e.g. -metan- ) not to also report an error exit _rc } local leftWDtot = r(leftWDtot) local rightWDtot = r(rightWDtot) local astext = r(astext) local AXmin = r(AXmin) local AXmax = r(AXmax) if `"`colsonly'"'!=`""' { if `lcolsN' & !`rcolsN' local AXmax = `DXmin' else if !`lcolsN' & `rcolsN' local AXmin = `DXmax' local AXval = (`AXmax' + `AXmin') / 2 } // June 2023 local lposlist `r(lposlist)' local rposlist `r(rposlist)' local graphopts `"`r(graphopts)'"' // Amended Apr 2020 // New observations, added by ProcessColumns qui replace `touse' = 1 if missing(`touse') & !missing(`id') *** FIND OPTIMAL TEXT SIZE AND ASPECT RATIOS (given user input) // We already have an estimate of the height taken up by x-axis labelling (this is `rowsxlab' from ProcessXAxis) // Next, find basic height to send to GetAspectRatio // Apr 2020: Note that this was previously derived in terms of number of observations // but now we use `id' instead due to `double' option // qui count if `touse' // local height = r(N) summ `id' if `touse', meanonly if r(N) local height = r(max) else local height = 0 // Jan 2020 // need to account for observations which will ultimately *not* be displayed // i.e. either removed using "nooverall"/"nosubgroup"; or "hidden" using OCILineOpts local reduceHeight = 0 if `"`overall'`subgroup'"'!=`""' { if `"`subgroup'"'!=`""' { qui count if `touse' & `_USE'==3 local reduceHeight = r(N) } if `"`overall'"'!=`""' { qui count if `touse' & inlist(`_USE', 4, 5, 7) local reduceHeight = `reduceHeight' + r(N) } } else if `"`cumulative'`influence'"'!=`""' { // `cumulative' or `influence' implies "hide" qui count if `touse' & inlist(`_USE', 3, 5, 7) local reduceHeight = r(N) } else { // user-specified "hide" UserSpecHide `_USE' if `touse', plotid(`plotid') `graphopts' local reduceHeight = r(N) } local height = `height' - `reduceHeight' qui count if `touse' & `_USE'==9 if r(N) local ++height // add 1 to overall height if titles present, to account for the "gap" in `id' (see later) local colWDtot = `leftWDtot' + `rightWDtot' if `"`usedims'"'==`""' { local DXwidthChars = `colWDtot'*((100/`astext') - 1) } // height of "xmlabel" text is assumed to be ~60% of "xlabel" text ... unless favours which uses xmlabel differently! local rowsxlabval = cond(`rowsfav', `rowsxlab', max(`rowsxlab', .6*`rowsxmlab')) GetAspectRatio, astext(`astext') colwdtot(`colWDtot') height(`height') rowsxlab(`rowsxlabval') rowsfav(`rowsfav') /// usedims(`usedims') `xtitle' `textsize' `colsonly' `graphopts' local graphopts `"`r(graphopts)'"' local xsize = r(xsize) local ysize = r(ysize) local fxsize = r(fxsize) local fysize = r(fysize) local yheight = r(yheight) local spacing = r(spacing) local textSize = r(textsize) // textsize as calculated by GetAspectRatio local textSize2 = r(textsize2) // textsize as modified post-hoc by textscale() option [May 2020] local approxChars = r(approxchars) local graphAspect = r(graphaspect) local plotAspect = r(plotaspect) * If specified, store in a matrix the quantities needed to recreate proportions in subsequent forestplot(s) // [`lcimin' added Sep 2017; `height' added Nov 2017] if `"`savedims'"'!=`""' { mat `savedims' = `DXwidthChars', `spacing', `plotAspect', `ysize', `xsize', `textSize', `height', `yheight', `lcimin' mat colnames `savedims' = cdw spacing aspect ysize xsize textsize height yheight lcimin } * Insert labsize(`textSize2') into existing x[m]labopt(s) local 0 `", `graphopts'"' syntax [, XLAbel(string asis) XMLabel(string asis) * ] local graphopts `"`options'"' while trim(`"`xlabel'`xmlabel'"')!=`""' { foreach xop in xlabel xmlabel { if `"``xop''"'!=`""' { local 0 `"``xop''"' syntax [anything(name=xcmd)] , [LABSize(string) LABGAP(string) FAVOURS * ] // colsonly: now add in "dummy" value to x[m]label command if `"`colsonly'"'!=`""' { gettoken tok rest : xcmd if `"`tok'"'==`"__DUMMY__"' local xcmd `AXval' `rest' } if "`xop'"=="xlabel" { local labsizeopt labsize(`textSize2') local labgapopt } else { local labsize = cond(`"`favours'"'!=`""', `textSize2', .6*`textSize2') local labsizeopt labsize(`labsize') if `"`favours'"'!=`""' local labgapopt labgap(5) } local newopts `"`newopts' `xop'(`xcmd', `labsizeopt' `labgapopt' `options')"' } } // Test for repeated options and loop if necessary // Parse for "add" and discard repeated options if appropriate // so that later parsing and updating/replacing of "labsize()" is accurate local 0 `", `graphopts'"' syntax [, XLAbel(string asis) XMLabel(string asis) * ] local graphopts `"`options'"' } // end while loop // local graphopts `"xsize(`xsize') ysize(`ysize') fxsize(`fxsize') fysize(`fysize') aspect(`plotAspect') `graphopts'"' // Modified Jan 2018: f{x|y}size only if usedims/savedims local graphopts `"xsize(`xsize') ysize(`ysize') aspect(`plotAspect') `newopts' `macval(graphopts)'"' if trim(`"`savedims'`usedims'"')!=`""' local graphopts `"fxsize(`fxsize') fysize(`fysize') `macval(graphopts)'"' // Return useful quantities return scalar aspect = `plotAspect' return scalar astext = `astext' return scalar ldw = `leftWDtot' // display width of left-hand side return scalar rdw = `rightWDtot' // display width of right-hand side // local DXwidthChars = cond(`"`usedims'"'!=`""', `DXwidthChars', `colWDtot'*((100/`astext') - 1)) return scalar cdw = `DXwidthChars' // display width of centre (i.e. the "data" part of the plot) return scalar height = `height' return scalar spacing = `spacing' return scalar ysize = `ysize' return scalar xsize = `xsize' return scalar textsize = `textSize2' // May 2020: if -metan9- option textsize() was applied, returns "post-hoc modified" textsize.... if trim(`"`savedims'`usedims'"')!=`""' { // ... which may differ from the `textsize' value stored in matrix `savedims' return scalar fysize = `fysize' return scalar fxsize = `fxsize' } ************************************ * Build plot commands from options * ************************************ // Commands for plotting columns of text (lcols/rcols) forvalues i = 1/`lcolsN' { gettoken lpos`i' lposlist : lposlist local lcolCommands `"`macval(lcolCommands)' scatter `id' `left`i'' if `touse', msymbol(none) mlabel(`leftLB`i'') mlabcolor(black) mlabpos(`lpos`i'') mlabgap(0) mlabsize(`textSize2') ||"' } forvalues i = 1/`rcolsN' { gettoken rpos`i' rposlist : rposlist local rcolCommands `"`macval(rcolCommands)' scatter `id' `right`i'' if `touse', msymbol(none) mlabel(`rightLB`i'') mlabcolor(black) mlabpos(`rpos`i'') mlabgap(0) mlabsize(`textSize2') ||"' } ** Prepare tempvars... // ...for diamonds tempvar DiamX DiamY1 DiamY2 local diamlist `DiamX' `DiamY1' `DiamY2' // ...for "overall effect" lines // Jan 2020: now including overall confidence limit lines tempvar ovLine ovMin ovMax ovLineLCI ovLineUCI ovLineX local ovlist `ovLine' `ovMin' `ovMax' `ovLineLCI' `ovLineUCI' `ovLineX' // ...for off-scale arrows tempvar offscaleL offscaleR local offsclist `offscaleL' `offscaleR' // ...for predictive intervals // Jan 2020: now including confidence limit lines if `"`rfdist'"'!=`""' { tempvar rfLoffscaleL rfLoffscaleR rfRoffscaleL rfRoffscaleR rfLineLCI rfLineUCI rfLineX local rflist `rfLoffscaleL' `rfLoffscaleR' `rfRoffscaleL' `rfRoffscaleR' `rfLineLCI' `rfLineUCI' `rfLineX' } // ...for multiple plotids and/or for area plots tempvar tousePlotID touseDiam touseOCI touseRFCI local tvopts `"diamlist(`diamlist') ovlist(`ovlist') offsclist(`offsclist') rflist(`rflist') touseextra(`tousePlotID' `touseDiam' `touseOCI' `touseRFCI')"' // August 2018: N.B. unusually, have to pass `touse' as an option here (rather than using marksample) // since we need to have the same tempname appearing in the created plot commands cap nois BuildPlotCmds `_USE' `_ES' `_LCI' `_UCI', touse(`touse') id(`id') /// plotid(`plotid') dataid(`dataid') `newwt' h0(`h0') `null' `colsonly' /// `cumulative' `influence' `interaction' `overall' `subgroup' `graphopts' /// wgt(`_WT') rfdist(`_rfLCI' `_rfUCI') cxlist(`CXmin' `CXmax') `tvopts' if _rc { if _rc==1 disp as err "User break" else disp as err `"Error in {bf:forestplot.BuildPlotCmds}"' c_local err noerr // tell calling subroutine not to also report an error exit _rc } local RFPlot `"`s(rfplot)'"' local PCIPlot `"`s(pciplot)'"' local diamPlot `"`s(diamplot)'"' local pointPlot `"`s(pointplot)'"' local ppointPlot `"`s(ppointplot)'"' local olineAreaPlot `"`s(olineareaplot)'"' local borderCommand `"`s(bordercommand)'"' local graphopts `"`s(options)'"' // Nov 2021: see notes within BuildPlotCmds if `"`s(g_overlay_ci)'"'!=`""' { local firstPlot `"`s(ciplot)'"' local secondPlot `"`s(scplot)'"' } else { // current default local firstPlot `"`s(scplot)'"' local secondPlot `"`s(ciplot)'"' } if `"`s(g_olinefirst)'"'!=`""' { local olinePlotFirst `"`s(olineplot)'"' } else local olinePlot `"`s(olineplot)'"' if `"`s(g_nlinefirst)'"'!=`""' { local nullCommandFirst `"`s(nullcommand)'"' } else local nullCommand `"`s(nullcommand)'"' qui count if `touse' if !r(N) { nois disp as err "no observations" exit 2000 } return scalar obs = r(N) *************************** *** DRAW GRAPH *** *************************** // First, if `useopts' and `graphopts' both supplied, check for repeated (non-Stata graph) options in `graphopts' which would cause -twoway- to fail // (otherwise, onus is on user as usual) if `"`useopts'"'!=`""' & `"`orig_gropts'"'!=`""' { local 0 `", `graphopts'"' syntax [, BY(varname) EFORM EFFect(string asis) LABels(varname string) DP(integer 2) KEEPALL /// INTERaction LCols(namelist) RCols(namelist) LEFTJustify COLSONLY RFDIST(varlist numeric min=2 max=2) /// NULLOFF noNAmes noNULL NULL2(string) noKEEPVars noOVerall noSTATs noSUbgroup noWT LEVEL(cilevel) /// XTItle(passthru) FAVours(passthru) /// /* N.B. -xtitle- is parsed here so that a blank title can be inserted if necessary */ CUmulative INFluence /// /* only needed in order to switch _USE==3 back to _USE==1 /// /* Sub-plot identifier for applying different appearance options, and dataset identifier to separate plots */ PLOTID(string) DATAID(string) /// /// /* "fine-tuning" options */ SAVEDIms(name) USEDIms(name) ASText(real -9) noADJust /// FP(string) /// /*(deprecated; now a favours() suboption)*/ KEEPXLabs /// /*(undocumented; colsonly option)*/ RAnge(string) CIRAnge(string) /// /* from ProcessXAxis*/ DXWIDTHChars(real -9) LBUFfer(real 0) RBUFfer(real 1) /// /* from ProcessColumns */ noADJust noLCOLSCHeck TArget(integer 0) MAXWidth(integer 0) MAXLines(integer 0) noTRUNCate /// ADDHeight(real 0) /// /* from GetAspectRatio */ CLASSIC noDIAmonds WGT(varname numeric) NEWwt BOXscale(real 100.0) noBOX /// /* from BuildPlotCmds */ /// /* standard options */ BOXOPts(string asis) DIAMOPts(string asis) POINTOPts(string asis) CIOPts(string asis) OLINEOPts(string asis) /// NLINEOPts(string asis) HLINEOPts(string asis) /// /// /* non-diamond and predictive interval options */ PPOINTOPts(string asis) PCIOPts(string asis) RFOPts(string asis) * ] local graphopts `"`macval(options)'"' } // August 2023: quickly parse for legend() option; if not present, set legend(off) // Legends cannot (currently) be automated; responsibility is to the user to sort them out local 0 `", `graphopts'"' syntax [, LEGend(string asis) * ] if `"`legend'"'==`""' local legend_opt legend(off) local xtitleopt = cond(`"`xtitle'"'==`""', `"xtitle("")"', `"`xtitle'"') // to prevent tempvar name being printed as xtitle summ `id', meanonly // local DYmin = r(min) - 1 // amended Apr 2020 local DYmin = 0 local DYmax = r(max) + 1 // Re-ordered 28th June 2017 so that all twoway options are given together at the end #delimit ; twoway /* Nov 2017: order was: columns, overall, weighted, diamonds */ /* Nov 2021: if requested, place overall and null lines underneath everything else */ `olinePlotFirst' `nullCommandFirst' /* Jan 2020: if applicable, OVERALL CI AREA PLOT first, so that data points remain visible */ `olineAreaPlot' /* WEIGHTED SCATTERPLOT BOXES (plus plot-specific options) */ /* and CONFIDENCE INTERVALS (incl. "offscale" if necessary) */ `firstPlot' `secondPlot' /* OVERALL AND NULL LINES (plus plot-specific options) (Nov 2021: unless placed underneath; see above) */ `olinePlot' `nullCommand' /* DIAMONDS (or markers+CIs if appropriate) FOR SUMMARY ESTIMATES */ /* (and Prediction Intervals if appropriate; plus plot-specific options) */ /* then last of all PLOT EFFECT MARKERS to clarify */ `RFPlot' `PCIPlot' `diamPlot' `pointPlot' `ppointPlot' /* COLUMN VARIBLES (including effect sizes and weights on RHS by default) */ `lcolCommands' `rcolCommands' /* FAVOURS OR XTITLE */ /* do these first, so that their options may be overwritten by the user */ , `favopt' `xtitleopt' /* Y-AXIS OPTIONS */ // Note that, as yscale is merged-implicit, range(`AXmin' `AXmax') noline will take precedence over any user-specified range() sub-option to yscale, // ...but other yscale() options will be honored. Users cannot over-ride the y-range; nor can they supply ylabels or ytitle. yscale(range(`DYmin' `DYmax') noline) ylabel(none) ytitle(`""') `borderCommand' /* X-AXIS OPTIONS */ // Note that, as xscale is merged-implicit, range(`AXmin' `AXmax') will take precedence over any user-specified range() sub-option to xscale, // ...but other xscale() options will be honored. Users should specify the x-range via the separate, command-specific range() option. xscale(range(`AXmin' `AXmax')) `xlabopt' `xmlabopt' `xtickopt' `xmtickopt' `legend_opt' /* OTHER TWOWAY OPTIONS (`graphopts' = user-specified) */ `graphopts' plotregion(margin(zero)) ; #delimit cr end program define getWidth, sortpreserve version 9.0 // ROSS HARRIS, 13TH JULY 2006 // TEXT SIZES VARY DEPENDING ON CHARACTER // THIS PROGRAM GENERATES APPROXIMATE DISPLAY WIDTH OF A STRING // (in terms of the current graphics font) // FIRST ARG IS STRING TO MEASURE, SECOND THE NEW VARIABLE // PREVIOUS CODE DROPPED COMPLETELY AND REPLACED WITH SUGGESTION // FROM Jeff Pitblado // Updated August 2016 by David Fisher (added "touse" and "replace" functionality) syntax anything [if] [in] [, REPLACE] assert `: word count `anything''==2 tokenize `anything' marksample touse if `"`replace'"'==`""' { // assume `2' is newvar confirm new variable `2' qui gen `2' = 0 if `touse' } else { confirm numeric variable `2' qui replace `2' = 0 if `touse' } qui { count if `touse' local N = r(N) tempvar obs bys `touse' : gen int `obs' = _n if `touse' sort `obs' forvalues i = 1/`N'{ local this = `1'[`i'] local width: _length `"`this'"' replace `2' = `width' /*+1*/ in `i' // "+1" blanked out by DF; add back on at point of use if necessary } } // end qui end * exit // METAN UPDATE // ROSS HARRIS, DEC 2006 // MAIN UPDATE IS GRAPHICS IN THE _dispgby PROGRAM // ADDITIONAL OPTIONS ARE lcols AND rcols // THESE AFFECT DISPLAY ONLY AND ALLOW USER TO SPECIFY // VARIABLES AS A FORM OF TABLE. THIS EXTENDS THE label(namevar yearvar) // SYNTAX, ALLOWING AS MANY LEFT COLUMNS AS REQUIRED (WELL, LIMIT IS 10) // IF rcols IS OMMITTED DEFAULT IS THE STUDY EFFECT (95% CI) AND WEIGHT // AS BEFORE- THESE ARE ALWAYS IN UNLESS OMITTED USING OPTIONS // ANYTHING ADDED TO rcols COMES AFTER THIS. ******************** ** May 2007 fixes ** ******************** // "nostandard" had disappeared from help file- back in // I sq. in return list // sorted out the extra top line that appears in column labels // fixed when using aspect ratio using xsize and ysize so inner bit matches graph area- i.e., get rid of spaces for long/wide graphs // variable display format preserved for lcols and rcols // abbreviated varlist now allowed // between groups het. only available with fixed // warnings if any heterogeneity with fixed (for between group het if any sub group has het, overall est if any het) // nulloff option to get rid of line ****************** * DF subroutines * ****************** * CheckOpts // Based on the built-in _check_eformopt.ado, // but expanded from -eform- to general effect specifications. // This program is used by -ipdmetan-, -(ad)metan- and -forestplot- // Not all aspects are relevant to all programs, // but easier to maintain just a single subroutine! program define CheckOpts, sclass syntax [name(name=cmdname)] [, soptions OPts(string asis) ESTVAR(name) ] if "`cmdname'"!="" { _check_eformopt `cmdname', `soptions' eformopts(`opts') } else _get_eformopts, `soptions' eformopts(`opts') allowed(__all__) local summstat = cond(`"`s(opt)'"'==`"eform"', `""', `"`s(opt)'"') if "`summstat'"=="rrr" { local effect `"Risk ratio"' // Stata by default refers to this as a "Relative Risk Ratio" or "RRR" local summstat rr // ... but in MA context most users will expect "Risk Ratio" } else if "`summstat'"=="nohr" { // nohr and noshr are accepted by _get_eformopts local effect `"Haz. ratio"' // but are not assigned names; do this manually local summstat hr local logopt nohr } else if "`summstat'"=="noshr" { local effect `"SHR"' local summstat shr local logopt noshr } else local effect `"`s(str)'"' if "`estvar'"=="_cons" { // if constant model, make use of eform_cons_ti if available local effect = cond(`"`s(eform_cons_ti)'"'!=`""', `"`s(eform_cons_ti)'"', `"`effect'"') } local 0 `", `s(eform)'"' syntax [, EFORM(string asis) * ] local eform = cond(`"`eform'"'!=`""', "eform", "") // Next, parse `s(options)' to extract anything that wouldn't usually be interpreted by _check_eformopt // that is: mean differences (`smd', `wmd' with synonym `md'); `rd' (unless -binreg-); // `coef'/`log' and `nohr'/`noshr' (which all imply `log') // (N.B. do this even if a valid option was found by _check_eformopt, since we still need to check for multiple options) local 0 `", `s(options)'"' syntax [, COEF LOG NOHR NOSHR RD SMD WMD MD * ] // identify multiple options; exit with error if found opts_exclusive "`coef' `log' `nohr' `noshr'" if `"`summstat'"'!=`""' { if trim(`"`md'`smd'`wmd'`rr'`rd'`nohr'`noshr'"')!=`""' { opts_exclusive "`summstat' `md' `smd' `wmd' `rr' `rd' `nohr' `noshr'" } } // if "nonstandard" effect option used else { if trim(`"`md'`wmd'"')!=`""' { // MD and WMD are synonyms local effect WMD local summstat wmd } else { local effect = cond("`smd'"!="", `"SMD"', /// cond("`rd'"!="", `"Risk Diff."', `"`effect'"')) local summstat = cond(`"`summstat'"'==`""', trim(`"`smd'`rd'"'), `"`summstat'"') } else if "`nohr'"!="" { local effect `"Haz. ratio"' local summstat hr local logopt nohr } else if "`noshr'"!="" { local effect `"SHR"' local summstat shr local logopt noshr } // now check against program properties and issue warning if "`cmdname'"!="" { local props : properties `cmdname' if "`cmdname'"=="binreg" local props `props' rd if !`:list summstat in props' { cap _get_eformopts, eformopts(`summstat') if _rc { disp as err `"Note: option {bf:`summstat'} does not appear in properties of command {bf:`cmdname'}"' } } } } // log always takes priority over eform // ==> cancel eform if appropriate local log = cond(trim(`"`coef'`logopt'"')!=`""', "log", "`log'") // `coef' is a synonym for `log'; `logopt' was defined earlier if `"`log'"'!=`""' { if inlist("`summstat'", "rd", "smd", "wmd") { nois disp as err "Log option only appropriate with ratio statistics" exit 198 } local eform } sreturn clear sreturn local logopt = trim(`"`coef'`logopt'"') // "original" log option sreturn local log `"`log'"' // either "log" or nothing sreturn local eform `"`eform'"' // either "eform" or nothing sreturn local summstat `"`summstat'"' // if `eform', original eform option sreturn local effect `"`effect'"' sreturn local options `"`macval(options)'"' end ********************************************************************************* * Subroutine to sort out labels and ticks for x-axis, and find DXmin/DXmax (and CXmin/CXmax if different) * Created August 2016 * Modified & renamed May 2024 program define ProcessXAxis, rclass syntax anything [, RAnge(string) CIRAnge(string) EFORM H0(real 0) noNULL DENOMinator(passthru) COLSONLY FAVours(string asis) /// FP(string) FORCE /* deprecated -metan9- options; now handled differently */ * ] local graphopts `"`options'"' tokenize `anything' args DXmin DXmax * Initial parse of xlabel and xtick // [added May 2020] In case old (v3.x and earlier) syntax is used, with comma-separated values and no sub-options // Otherwise, ignore and send through as-is to -twoway- to pick up any other issues // Note: doesn't apply to xmlabel, xmtick as these were not allowed by -metan9- // Note: MAY 2024: Look for repeated options -- these would not have been *expected* by -metan9- // ... but -twoway- would still have accepted them if supplied local 0 `", `graphopts'"' syntax [, XLAbel(string asis) XTick(string) * ] local graphopts `"`options'"' while trim(`"`xlabel'`xtick'"')!=`""' { foreach xop in xlabel xtick { ParseOldXLabel `"``xop''"', xop(`xop') h0(`h0') `eform' `force' `twowaynote' if `"`r(xlablist)'"'!=`""' local `xop'new `"``xop'new' `xop'(`r(xlablist)')"' } local 0 `", `graphopts'"' syntax [, XLAbel(string asis) XTick(string) * ] local graphopts `"`options'"' } if "`twowaynote'"!="" c_local twowaynote notwowaynote // so that -metan- does not print an additional message regarding "xlabel" or "force" * If `eform', need to extract correct format to use when assembling labels in exponentiated scale if `"`eform'"'!=`""' { local use_format local formatopts `"`graphopts' `xlabelnew'"' local 0 `", `formatopts'"' syntax [, XLAbel(string asis) XMLabel(string asis) * ] local formatopts `"`options'"' while trim(`"`xlabel'`xmlabel'"')!=`""' { foreach xop in xlabel xmlabel { local 0 `"``xop''"' // xlabel, xmlabel syntax [anything] , [FORMAT(string) * ] if `"`format'"'!=`""' local use_format : copy local format // format() is right-most } local 0 `", `formatopts'"' syntax [, XLAbel(string asis) XMLabel(string asis) * ] local formatopts `"`options'"' } } * Full parse x[m]label and x[m]tick, if supplied by user * (Note: we now assume all options follow standard Stata -twoway- syntax) * - Extract numlists for labels & ticks, from which to calculate size of plot (CXmin/max, DXmin/max etc.) * - If `eform', interpret user-supplied label values as on the exponentiated scale; apply labels on the interval scale accordingly * - Calculate no. of rows in case of `colsonly' * But keep repeated options etc. as-is to send to -twoway- , so that subtleties are honoured local 0 `", `graphopts' `xlabelnew' `xticknew'"' syntax [, XLAbel(string asis) XMLabel(string asis) XTick(string) XMTick(string) * ] local graphopts `"`options'"' local rowsxmlab = 0 local rowsxlab = 0 local firstadd = 1 while trim(`"`xlabel'`xmlabel'`xtick'`xmtick'"')!=`""' { foreach xop in xlabel xmlabel xtick xmtick { local xab : copy local xop if "`xop'"=="xlabel" local xab xlab else if "`xop'"=="xmlabel" local xab xmlab local 0 `"``xop''"' syntax [anything(name=`xab'cmd)] , [FORCE ADD ADDNEW /*colsonly-specific option*/ ALTernate /*rightmost, unless "add|addnew" */ * ] // Parse x[m]lablist and obtain numlist (Nov 2017) ProcessXLabels ``xab'cmd', dx(`DXmin' `DXmax') xop(`xop') format(`use_format') `eform' `colsonly' // Add results to previous (if repeated options with `add') if trim(`"`add'`addnew'"')==`""' { local `xab'list `"`r(xlablist)'"' if trim(`"`r(xlabcmd)'`options'"')!=`""' { local `xab'opt `"`xop'(`r(xlabcmd)', `options')"' } } else { if `"`colsonly'"'!=`""' & `"`addnew'"'!=`""' { if `firstadd' local firstadd = 0 else local addopt add local `xab'list2 `"``xab'list2' `r(xlablist)'"' // append values if trim(`"`r(xlabcmd)'`options'"')!=`""' { local `xab'opt2 `"``xab'opt2' `xop'(`r(xlabcmd)', `options' `addopt')"' // repeated options + sub-options } if trim(`"``xab'opt2'"')!=`""' local addcustom add custom // for later } local `xab'list `"``xab'list' `r(xlablist)'"' // append values if trim(`"`r(xlabcmd)'`options'"')!=`""' { local `xab'opt `"``xab'opt' `xop'(`r(xlabcmd)', `options' add)"' // repeated options + sub-options } } if !inlist("`xop'", "xtick", "xmtick") { local rows`xab' = max(`rows`xab'', `=`r(rows)' + (`"`alternate'"'!=`""')') } // `force' option if `"`force'"'!=`""' { if `"`xop'"'!=`"xlabel"' { // "force" option only applies to xlab, not xmlab or ticks nois disp as err "option {bf:force} is not allowed with {bf:`xop'()}" exit 198 } if "`cirange'"!="" { disp as err `"Note: both {bf:cirange()} and {bf:xlabel(, force)} were specifed; {bf:cirange()} takes precedence"' } else if `"`xlablist'"'!=`""' { local xlablist2 : copy local xlablist local min : word 1 of `xlablist2' if inlist(`"`min'"', `"none"', `"."') { gettoken none xlablist2 : xlablist } numlist `"`xlablist2'"', sort local n : word count `r(numlist)' local min : word 1 of `r(numlist)' local max : word `n' of `r(numlist)' if `"`CRXmin'"'==`""' local CRXmin = `min' else { local CRXmin = max(`CRXmin', `min') // if multiple "force" options, use most restrictive } if `"`CRXmax'"'==`""' local CRXmax = `max' else { local CRXmax = min(`CRXmax', `max') // if multiple "force" options, use most restrictive } local forceopt force // indicator that "force" has been specified } } } // end foreach // Test for repeated options and loop if necessary // Parse for "add" and discard repeated options if appropriate // so that later parsing and updating/replacing of "labsize()" is accurate local 0 `", `graphopts'"' syntax [, XLAbel(string asis) XMLabel(string asis) XTick(string) XMTick(string) * ] local graphopts `"`options'"' } // end while loop * If `colsonly', blank out the labels, and make ticks and gridlines invisible if `"`colsonly'"'!=`""' { foreach xop in xlabel xmlabel xtick xmtick { local xab : copy local xop if "`xop'"=="xlabel" local xab xlab else if "`xop'"=="xmlabel" local xab xmlab if inlist("`xop'", "xlabel", "xmlabel") { if `"``xab'opt'"'!=`""' { forvalues i=1/`rows`xab'' { local `xab'txt `"``xab'txt' `" "'"' } if `rows`xab'' > 1 local `xab'txt `"`"``xab'txt'"'"' local `xab'opt `"`xop'( __DUMMY__ ``xab'txt', tlc(none) glc(none) `addcustom')"' } } else { // Process ticklists: these are easier as no labels if `"``xab'opt'"'!=`""' { local `xab'opt `"`xop'( __DUMMY__ , tlc(none) glc(none) `addcustom')"' } } } } * Parse `range' and `cirange' // in both cases, "min" and "max" refer to range of data in terms of LCI, UCI // (that is, initial values of `DXmin', `DXmax') foreach op in range cirange { if `"``op''"'==`""' continue local opmin = cond("`op'"=="range", "RXmin", "CXmin") local opmax = cond("`op'"=="range", "RXmax", "CXmax") tokenize `"``op''"' cap { assert `"`2'"'!=`""' assert `"`3'"'==`""' } if _rc { disp as err `"option {bf:`op'()} must contain exactly two elements"' exit 198 } // if "min", "max" used if inlist(`"`1'"', "min", "max") | inlist(`"`2'"', "min", "max") { if `"`eform'"'!=`""' { forvalues i=1/2 { cap confirm number ``i'' if !_rc local `i' = ln(``i'') } } local `op' `"`1' `2'"' local `op' = subinstr(`"``op''"', `"min"', `"`DXmin'"', .) local `op' = subinstr(`"``op''"', `"max"', `"`DXmax'"', .) numlist `"``op''"', min(2) max(2) sort local `op' = r(numlist) tokenize `"``op''"' args `opmin' `opmax' } else { if `"`eform'"'!=`""' { numlist `"``op''"', min(2) max(2) range(>0) sort local `op' `"`=ln(`1')' `=ln(`2')'"' } else { numlist `"``op''"', min(2) max(2) sort local `op' = r(numlist) } tokenize `"``op''"' args `opmin' `opmax' } } // "force" option if trim(`"`CRXmin'`CRXmax'"')!=`""' { if `"`range'"'==`""' { // if `range' not specified, default to "forced" xlab limits local RXmin = `CRXmin' local RXmax = `CRXmax' local range = trim(`"`CRXmin' `CRXmax'"') } else { // otherwise, set `cirange' instead (see message above) local CXmin = `CRXmin' local CXmax = `CRXmax' local cirange = trim(`"`CRXmin' `CRXmax'"') } } * Check validity of user-defined values if `"`range'"'!=`""' & `"`cirange'"'!=`""' { cap { assert `RXmin' <= `CXmin' assert `RXmax' >= `CXmax' } if _rc { disp as err "interval defined by {opt cirange()} (or {bf:xlabel(, force)}) must lie within that defined by {opt range()}" exit 198 } } // changed Sep 2017 for v2.1 else if `"`cirange'"'==`""' & `"`range'"'!=`""' { local CXmin = max(`RXmin', `DXmin') local CXmax = min(`RXmax', `DXmax') } // Jan 2018: Now re-set DXmin/DXmax if RXmin/RXmax are defined // CHECK CONSEQUENCES OF THIS CAREFULLY if trim(`"`RXmin'`RXmax'"')!=`""' { local DXmin = `RXmin' local DXmax = `RXmax' } // remove null line if lies outside range of x values to be plotted if `"`null'"'==`""' & trim(`"`cirange'`range'`forceopt'"')!=`""' { local removeNull = 0 if `"`cirange'"'!=`""' local removeNull = (`h0' < `CXmin' | `h0' > `CXmax') else local removeNull = (`h0' < `RXmin' | `h0' > `RXmax') if `removeNull' { nois disp as err "null line lies outside of user-specified x-axis range and will be suppressed" local null nonull } } return local null `null' * Apply automated values if -xlabel- not supplied by user // MAY 2024: Note: if xmlabel() is used, AutoXLabel is still potentially run; this matches with standard -twoway- behaviour local xlablim1 = 0 // init local xlablist_all `"`xlablist' `xlablist2'"' local xlablist_all : list uniq xlablist_all if inlist(`"`xlablist_all'"', `"none"', `"."', `"none ."', `". none"') local xlablist_all none if `"`xlablist_all'"'==`""' { AutoXLabel `DXmin' `DXmax', range(`range') `eform' format(`use_format') h0(`h0') `null' `denominator' local DXmin = `r(DXmin)' local DXmax = `r(DXmax)' local xlablim1 = `r(xlablim1)' local xlablist `"`r(xlablist)'"' local xlabopt `"`r(xlabopt)' `xlabopt'"' // Added Feb 2018: If automatic labelling, set rows to 1 (rowsxmlab remains at 0) local rowsxlab = 1 if `"`colsonly'"'!=`""' local xlabopt `"xlabel(__DUMMY__ `" "', tlc(none) glc(none))"' } // end if "`xlablist'" == "" // Final parsing of x-axis labelling quantities (excluding "favours" and [xab]list2/newadd ), to form `XLmin' `XLmax' local xlablist_all `"`xlablist' `xticklist' `xmticklist'"' local xlablist_all : list uniq xlablist_all if inlist(`"`xlablist_all'"', `"none"', `"."', `"none ."', `". none"') local xlablist_all none cap assert `"`xlablist_all'"'!=`""' if _rc { disp as err "Something has gone wrong with x-axis value labelling" exit 198 } if `"`xlablist_all'"'==`"none"' { local XLmin = `h0' local XLmax = `h0' } else { numlist `"`xlablist' `xticklist' `xmticklist'"', sort local n : word count `r(numlist)' local XLmin : word 1 of `r(numlist)' local XLmax : word `n' of `r(numlist)' } * Use symmetrical plot area (around `h0'), unless data "too far" from null if trim(`"`range'`cirange'`forceopt'"')==`""' { // if "too far", adjust `CXmin' and/or `CXmax' to reflect this // where "too far" ==> max(abs(`CXmin'-`h0'), abs(`CXmax'-`h0')) > `CXmax' - `CXmin' local TooFar = 0 if "`null'"=="" | `h0' != 0 { if `h0' - `DXmax' > `DXmax' - `DXmin' { // data "too far" to the left local DXmax = max(`h0' + .5*(`DXmax'-`DXmin'), `XLmax') // clip the right-hand side local TooFar = 1 } if `DXmin' - `h0' > `DXmax' - `DXmin' { // data "too far" to the right local DXmin = min(`h0' - .5*(`DXmax'-`DXmin'), `XLmin') // clip the left-hand side local TooFar = 1 } } if `TooFar' { local DXmin = -max(abs(`DXmin'), abs(`DXmax')) local DXmax = max(abs(`DXmin'), abs(`DXmax')) } } * Final calculation of DXmin, DXmax if trim(`"`RXmin'`RXmax'"')!=`""' { numlist `"`RXmin' `RXmax'"', sort } else { numlist `"`DXmin' `DXmax' `XLmin' `XLmax'"', sort } local n : word count `r(numlist)' local DXmin : word 1 of `r(numlist)' local DXmax : word `n' of `r(numlist)' if trim(`"`CXmin'`CXmax'"')==`""' { local CXmin = `DXmin' local CXmax = `DXmax' } * Now parse `favours' option * - Use similar approach to x[m]label, and ultimately translate into an additional xmlabel option * - Calculate no. of rows in case of `colsonly' local rowsfav = 0 if `"`favours'"' != `""' { local oldfp : copy local fp local 0 `"`favours'"' syntax [anything(everything)] [, FP(string) noSYMmetric /// /* these two are needed right now, others parsed simply to isolate inappropriate options */ FORMAT(string) ANGLE(string) LABGAP(string) LABSTYLE(string) LABSize(string) LABColor(string) noSYMmetric * ] if `"`oldfp'"'!=`""' local fp : copy local oldfp if `"`options'"' != `""' { nois disp as err `"inappropriate suboptions found in {bf:favours()}"' exit 198 } * Parse text, and count how many rows of text there are (i.e. separated with pairs of quotes) local rowsleftfav = 0 local rowsrightfav = 0 gettoken leftfav rest : anything, parse("#") quotes if `"`leftfav'"'!=`"#"' { while `"`rest'"'!=`""' { local ++rowsleftfav gettoken next rest : rest, parse("#") quotes if `"`next'"'==`"#"' continue, break local leftfav `"`leftfav' `next'"' } } else local leftfav `""' local rightfav = trim(`"`rest'"') if `"`rightfav'"'!=`""' { while `"`rest'"'!=`""' { local ++rowsrightfav gettoken next rest : rest, quotes } } local rowsfav = max(1, `rowsleftfav', `rowsrightfav') // Feb 2021: Remove quotes if only a single line if `rowsleftfav'==1 { gettoken new : leftfav, qed(qed) if `qed' local leftfav : copy local new } if `rowsrightfav'==1 { gettoken new : rightfav, qed(qed) if `qed' local rightfav : copy local new } // modified Jan 30th 2018, and again May 21st 2018 if `"`fp'"'==`""' { // August 2018: default is... // May 2018: use smaller of distances from h0 to min(DXmin, XLmin) or max(DXmax, XLmax) local fpmin = min(`DXmin', `XLmin') local fpmax = max(`DXmax', `XLmax') if `"`symmetric'"'==`""' { local fp = min(cond(`fpmin' <= `h0' & `"`leftfav'"'!=`""', (`h0' - `fpmin')/2, .), /// cond(`fpmax' >= `h0' & `"`rightfav'"'!=`""', (`fpmax' - `h0')/2, .)) local leftfp = cond(`fpmin' <= `h0' & `"`leftfav'"'!=`""', `"`=`h0' - `fp'' `"`leftfav'"'"', `""') local rightfp = cond(`fpmax' >= `h0' & `"`rightfav'"'!=`""', `"`=`h0' + `fp'' `"`rightfav'"'"', `""') } // ...but may be overruled with option `nosymmetric', e.g. if distances are extremely unbalanced else { local leftfp = cond(`fpmin' <= `h0' & `"`leftfav'"'!=`""', `"`=(`h0' + `fpmin')/2' `"`leftfav'"'"', `""') local rightfp = cond(`fpmax' >= `h0' & `"`rightfav'"'!=`""', `"`=(`h0' + `fpmax')/2' `"`rightfav'"'"', `""') } } // modified Jan 2020 // User-specified fp() else { numlist `"`fp'"', miss max(2) tokenize `fp' args fpleft fpright if `"`eform'"'!=`""' local fpleft = ln(`fpleft') // fp() should be given on same scale as xlabels if `"`fpright'"'==`""' { // only one value given local fpleft = cond(`fpleft' <= `h0', `fpleft', 2*`h0' - `fpleft') local fpright = 2*`h0' - `fpleft' } else { // two values given: should be one either side of null line) cap assert `fpleft' <= `h0' if _rc { if `h0' != 0 local extra `" (`h0')"' nois disp as err `"Error in {bf:fp()}: left-hand value should lie to the left of the null value`extra'"' exit 198 } cap assert `fpright' >= `h0' if _rc { if `h0' != 0 local extra `" (`h0')"' nois disp as err `"Error in {bf:fp()}: right-hand value should lie to the right of the null value`extra'"' exit 198 } } local leftfp `fpleft' `"`leftfav'"' local rightfp `fpright' `"`rightfav'"' } // Nov 2017 [modified Feb 2018] assert (`rowsfav'>0) == (trim(`"`leftfp'`rightfp'"')!=`""') if `rowsfav' { if `"`colsonly'"'!=`""' { forvalues i=1/`rowsfav' { local favtxt `"`favtxt' `" "'"' } if `rowsfav' > 1 local favtxt `"`"`favtxt'"'"' local dummy __DUMMY__ } else local favtxt `leftfp' `rightfp' local favopt `"xmlabel(`dummy' `favtxt', tlc(none) glc(none) favours `addcustom' `favopt')"' } } // Position of xtitle [May 2024: not currently implemented] local xtitleval = cond("`xlablist'"=="", `xlablim1', .5*(`CXmin' + `CXmax')) return scalar xtitleval = `xtitleval' // Return scalars return scalar CXmin = `CXmin' return scalar CXmax = `CXmax' return scalar DXmin = `DXmin' return scalar DXmax = `DXmax' // moved Feb 2018; modified Oct 2018; modified May 2024 return scalar rowsxlab = `rowsxlab' return scalar rowsxmlab = `rowsxmlab' return scalar rowsfav = `rowsfav' return local xlabopt `"`xlabopt'"' return local xmlabopt `"`xmlabopt'"' return local favopt `"`favopt'"' return local xtickopt `"`xtickopt'"' return local xmtickopt `"`xmtickopt'"' // New May 2024: for use with `colsonly' return local xlabopt2 `"`xlabopt2'"' return local xmlabopt2 `"`xmlabopt2'"' return local xtickopt2 `"`xtickopt2'"' return local xmtickopt2 `"`xmtickopt2'"' return local options `"`graphopts'"' end * ParseOldXLabel: Initial parse of xlabel and xtick, in case old (v3.x and earlier) syntax is used // (with comma-separated values and no sub-options) // Originally written May 2020; moved into separate subroutine May 2024 // subroutine of ProcessXAxis program define ParseOldXLabel, rclass syntax [anything(name=xlabcmd)], XOP(string) /*for error messages*/ [ H0(real 0) EFORM FORCE noTWOWAYNOTE ] local done = 0 local comma = 0 local csv = 1 local lblcmd tokenize `xlabcmd', parse(",") if inlist(`"`1'"', `"none"', `"minmax"') | substr(`"`1'"', 1, 1)==`"#"' { gettoken xlablist : xlabcmd // return original xlabel option as-is; presumably using modern Stata -twoway- syntax } else { while `"`1'"' != `""' { cap confirm number `1' if !_rc { if `"`2'"'==`""' & `"`lblcmd'"'==`""' local csv = 0 local lblcmd `"`lblcmd' `1'"' } else { cap assert `"`1'"'==`","' if !_rc local comma = 1 else local csv = 0 } mac shift } if `csv' { // originally a comma-separated list of numbers; but now commas replaced by spaces if `"`lblcmd'"'!=`""' { capture numlist `"`lblcmd'"' if _rc { nois disp as err `"error in option {bf:`xlname'()}: invalid numlist"' exit _rc } local xlablist = r(numlist) if `comma' { // [Oct 2020:] If `h0' is absent, add it back in. // Previous versions of -metan- added h0 by default (unless "nonull"), so that e.g. "xlabel(.1, 10) eform" would result in .1, 1 and 10 being marked. // With the "new" syntax based on standard -twoway- options, `h0' needs to be included in xlabel() in order for it to appear. if `"`null'"'==`""' { local newh0 = cond(`"`eform'"'!=`""', exp(`h0'), `h0') if !`: list newh0 in xlablist' local xlablist `xlablist' `newh0' } numlist `"`xlablist'"', sort local xlablist = r(numlist) if !`done' & `"`twowaynote'"'==`""' { nois disp as err _n `"Note: with {bf:metan} version 4 and above, the preferred syntax is for {bf:`xop'()}"' nois disp as err `" to contain a standard Stata numlist, so e.g. {bf:`xop'(`xlablist')}; see {help numlist:help numlist}"' } local done = 1 c_local twowaynote notwowaynote // so that -metan- does not print an additional message regarding "force" } } } else gettoken xlablist : xlabcmd // return original xlabel option as-is; presumably using modern Stata -twoway- syntax // convert legacy -force- option into modern twoway xlabel option if `"`xop'"'==`"xlabel"' & `"`force'"'!=`""' & `csv' { if `done' local xlablist `"`xlablist', force"' else { if `"`xlablist'"'==`""' { nois disp as err `"main option {bf:force} not allowed without {bf:xlabel()}"' } else nois disp as err `"option {bf:force} only allowed as a suboption to {bf:xlabel()}"' exit 198 } c_local twowaynote notwowaynote // so that -metan- does not print an additional message regarding "force" } } return local xlablist `"`xlablist'"' end * ProcessXLabels: Parse user-specified x[m]label() and x[m]tick() options // Moved into separate subroutine May 2024; now also handles repeated options // subroutine of ProcessXAxis program define ProcessXLabels, rclass syntax [anything(name=xlabcmd)], XOP(string) /*for error messages*/ DX(numlist min=2 max=2) /*in case of "minmax" rule*/ /// [ EFORM COLSONLY FORMAT(string) /*KEEPXLabs*/ ] local rows = 0 // init // First, look at ticks; these are easier (no labels!) if inlist("`xop'", "xtick", "xmtick") { if `"`xlabcmd'"'!=`""' { cap numlist `"`xlabcmd'"' if _rc { disp as err `"invalid label specifier, : `xlabcmd'"' exit 198 } if `"`eform'"'!=`""' { // assume given on exponentiated scale if "eform" specified, so need to take logs cap numlist "`xlabcmd'", range(>0) // ...in which case, all values must be greater than zero if _rc { disp as err `"option {bf:eform} specified, but {bf:`xop'()} contains non-positive values"' exit 198 } local exlabcmd `"`xlabcmd'"' local xlabcmd foreach xi of numlist `exlabcmd' { local xlabcmd `"`xlabcmd' `=ln(`xi')'"' } } local newxlabcmd: copy local xlabcmd // to match with code further down local xlablist: copy local xlabcmd // to match with code further down } } // end of ticks else { local rest : copy local xlabcmd while `"`rest'"'!=`""' | `"`lbl'"'!=`""' { // Feb 2018: added the second part of this stmt if `"`lbl'"'!=`""' local lbl2 `"`lbl'"' // Nov 2017: user-specified labels need to go round the loop once, before being applied local lbl gettoken tok rest : rest, qed(qed) if `"`tok'"'!=`""' { // if text label found, check for embedded quotes (i.e. multiple lines) if `qed' { local rest2 `"`"`tok'"'"' gettoken el : rest2, qed(qed2) if !`qed2' { disp as err `"invalid label specifier, : ``xl'list':"' exit 198 } local newxlabcmd `"`newxlabcmd' `rest2'"' local newlist local rest2 : copy local el while `"`rest2'"'!=`""' { gettoken el rest3 : rest2, quotes if `"`el'"'==`"`""'"' { // local newlist : list rest2 - el // modified Feb 2018; check continue, break } local newlist `"`newlist' `el'"' local rest2 `"`rest3'"' } local rows = max(`rows', `: word count `newlist'') local lbl2 } // end if `qed' // else, check if valid numlist else { if substr(`"`tok'"', 1, 1)==`"#"' { local hash `"#"' if substr(`"`tok'"', 2, 1)==`"#"' local hash `"##"' disp as err `"Cannot use the {bf:`hash'}# syntax in the {bf:`xop'()} option of {bf:forestplot}; please use a {it:numlist} instead"' exit 198 } if inlist(`"`tok'"', `"none"', `"."') local rule_none : copy local tok else { if `"`tok'"'==`"minmax"' local tok `DXmin' `DXmax' numlist `"`tok'"' local rows = max(`rows', 1) if `"`eform'"'!=`""' { cap numlist `"`tok'"', range(>0) if _rc { disp as err `"option {bf:eform} specified, but {bf:`xop'()} contains non-positive values"' exit 198 } // if eform, need to expand numlist and take logs local nl = r(numlist) local N : word count `nl' forvalues i=1/`N' { local el : word `i' of `nl' local xlablist `"`xlablist' `=ln(`el')'"' local newxli `"`=ln(`el')'"' local lbl = cond("`format'"=="", string(`el'), string(`el', "`format'")) if `i'==1 & `"`lbl2'"'!=`""' local newxli `"`"`lbl2'"' `newxli'"' if `i'<`N' local newxli `"`newxli' `"`lbl'"'"' local lbl2 // don't add the last label yet, in case user has specified their own label local newxlabcmd `"`newxlabcmd' `newxli'"' } } // else, can simply add unexpanded numlist else { local xlablist `"`xlablist' `tok'"' local newxlabcmd `"`newxlabcmd' `tok'"' local lbl2 } } } // end else } // end if `"`tok'"'!=`""' // if lbl, add it now if `"`lbl2'"'!=`""' { local newxlabcmd `"`newxlabcmd' `"`lbl2'"'"' local lbl local lbl2 } } // end while loop } // end else (if not ticks) local xlablist = trim(`"`rule_none' `xlablist'"') local xlabcmd = trim(`"`rule_none' `newxlabcmd'"') cap assert `"`xlabcmd'"'==`""' if `"`xlablist'"'==`""' if _rc { disp as err "Error in {bf:`xop'()}" exit 198 } return local xlablist `"`xlablist'"' return local xlabcmd `"`xlabcmd'"' // Note: xlabcmd and xlablist should be identical except that xlabcmd may also have labels return scalar rows = `rows' end * AutoXLabel: If xlabel not supplied by user, choose sensible values. // Default is for symmetrical limits, with 3 labelled values including null // N.B. First modified from original -metan- code by DF, March 2013 // with further improvements by DF, January 2015 // Modifed April 2017 to avoid interminable looping if [base]^`mag' = missing // Modified & renamed May 2024 // subroutine of ProcessXAxis program define AutoXLabel, rclass syntax anything [, RANGE(numlist min=2 max=2) EFORM FORMAT(string) H0(real 0) noNULL FORCE DENOMinator(string) ] tokenize `anything' args DXmin DXmax tokenize `range' args RXmin RXmax local xlablim1 = 0 // init // [Mar 2020] If `proportion', simply choose 0, .5 and 1 ... [Mar 2021] multiplied by `denominator' // [Apr 2021] ... but only if `range' not specified and smaller than DXmin/DXmax if "`denominator'"!="" { cap confirm number `denominator' if _rc { nois disp as err `"`denominator' found where number expected in option {bf:denominator(#)}"' exit 198 } local xlablist if `"`range'"'!=`""' { local ii = max(`RXmin', 0) local xlablist `"`xlablist' `ii'"' local ii = min(`RXmax', `denominator') local xlablist `"`xlablist' `ii'"' } else { foreach i of numlist 0 .5 1 { local ii = `denominator' * `i' local xlablist `"`xlablist' `ii'"' } } local xlabopt `"`xlablist'"' } else { // If null line, choose values based around `h0' // (i.e. `xlabinit1' = `h0'... but `h0' is automatically selected anyway so no need to explicitly define `xlabinit1') if "`null'" == "" | `h0' != 0 { // [N.B. "h0 != 0" added Jan 2020] local xlabinit2 = max(abs(`DXmin' - `h0'), abs(`DXmax' - `h0')) local xlabinit "`xlabinit2'" } // if `nulloff', choose values in two stages: firstly based on the midpoint between CXmin and CXmax (`xlab[init|lim]1') // and then based on the difference between CXmin/CXmax and the midpoint (`xlab[init|lim]2') else { local xlabinit1 = (`DXmax' + `DXmin')/2 local xlabinit2 = abs(`DXmax' - `xlabinit1') // N.B. same as abs(`CXmin' - `xlabinit1') if float(`xlabinit1') != 0 { local xlabinit "`=abs(`xlabinit1')' `xlabinit2'" } else local xlabinit `xlabinit2' } assert "`xlabinit'"!="" assert "`xlabinit2'"!="" assert `: word count `xlabinit'' == ("`null'"!="" & `h0'==0)*(float(`DXmax')!=-float(`DXmin')) + 1 // should be >= 1 local counter = 1 foreach xval of numlist `xlabinit' { if `"`eform'"'==`""' { // linear scale local mag = floor(log10(`xval')) local xdiff = abs(`xval'-`mag') foreach i of numlist 1 2 5 10 { local ii = `i' * 10^`mag' if missing(`mag') local ii = 0 // March 2021: catch extreme case if missing(`ii') { local ii = `=`i'-1' * 10^`mag' local xdiff = abs(float(`xval' - `ii')) local xlablim = `ii' continue, break } else if abs(float(`xval' - `ii')) <= float(`xdiff') { local xdiff = abs(float(`xval' - `ii')) local xlablim = `ii' } } } else { // log scale local mag = round(`xval'/ln(2)) local xdiff = abs(`xval' - ln(2)) forvalues i=1/`mag' { local ii = ln(2^`i') if missing(`ii') { local ii = ln(2^`=`i'-1') local xdiff = abs(float(`xval' - `ii')) local xlablim = `ii' continue, break } else if abs(float(`xval' - `ii')) <= float(`xdiff') { local xdiff = abs(float(`xval' - `ii')) local xlablim = `ii' } } // if effect is small, use 1.5, 1.33, 1.25 or 1.11 instead, as appropriate foreach i of numlist 1.5 `=1/0.75' 1.25 `=1/0.9' { local ii = ln(`i') if abs(float(`xval' - `ii')) <= float(`xdiff') { local xdiff = abs(float(`xval' - `ii')) local xlablim = `ii' } } } // if nonull, center limits around `xlablim1', which should have been optimized by the above code if "`null'" != "" & `h0'==0 { // nonull if `counter'==1 { local xlablim1 = `xlablim'*sign(`xlabinit1') } if `counter'>1 | `: word count `xlabinit''==1 { local xlablim2 = `xlablim' local xlablims `"`=`xlablim1'+`xlablim2'' `=`xlablim1'-`xlablim2''"' } } else local xlablims `"`xlablims' `xlablim'"' local ++counter } // end foreach xval of numlist `xlabinit' // if nulloff, don't recalculate CXmin/CXmax if "`null'" != "" & `h0'==0 numlist `"`xlablim1' `xlablims'"' else { numlist `"`=`h0' - `xlablims'' `h0' `=`h0' + `xlablims''"', sort // default: limits symmetrical about `h0' tokenize `"`r(numlist)'"' // if data are "too far" from null (`h0'), take one limit (but not the other) plus null // where "too far" ==> abs(`CXmin' - `h0') > `CXmax' - `CXmin' // (this works whether data are "too far" to the left OR right, since our limits are symmetrical about `h0') if abs(`DXmin' - `h0') > `DXmax' - `DXmin' { if `3' > `DXmax' numlist `"`1' `h0'"' else if `1' < `DXmin' numlist `"`h0' `3'"' } else if trim("`range'`cirange'`forceopt'")=="" { // "standard" situation numlist `"`1' `h0' `3'"' local DXmin = `h0' - `xlabinit2' local DXmax = `h0' + `xlabinit2' } } local xlablist=r(numlist) } // if log scale, label with exponentiated values local xlabopt `"`xlablist'"' if `"`eform'"'!=`""' { local xlabopt foreach xi of numlist `xlablist' { local lbl = cond("`format'"=="", string(exp(`xi')), string(exp(`xi'), "`format'")) local xlabopt `"`xlabopt' `xi' `"`lbl'"'"' } } return scalar DXmin = `DXmin' return scalar DXmax = `DXmax' return scalar xlablim1 = `xlablim1' return local xlablist `"`xlablist'"' return local xlabopt `"xlabel(`xlabopt')"' end // End of subroutines of ProcessXAXis ********************************************************************************* * Process left and right columns -- obtain co-ordinates etc. program define ProcessColumns, rclass syntax varlist(min=1 max=2) [if] [in], ID(varname numeric) LRCOLSN(numlist integer >=0) LCIMIN(real) DX(numlist min=2 max=2) /// [LVALlist(namelist) LLABlist(varlist) LFMTLIST(string) /// RVALlist(namelist) RLABlist(varlist) RFMTLIST(string) RFINDENT(varname) RFCOL(integer 1) /// DXWIDTHChars(real -9) ASText(integer -9) LBUFfer(real 0) RBUFfer(real 1) /// noADJust noLCOLSCHeck TArget(integer 0) MAXWidth(integer 0) MAXLines(integer 0) noTRUNCate noWT DOUBLE COLSONLY * ] local graphopts `"`options' `double'"' marksample touse, novarlist // rename and unpack local DXwidthChars : copy local dxwidthchars tokenize `varlist' args _USE _EFFECT // Oct 2020: _EFFECT is only used if `double', to prevent doubling of _EFFECT variable tokenize `lrcolsn' args lcolsN rcolsN tokenize `dx' args DXmin DXmax tempvar strlen strwid local digitwid : _length 0 // width of a digit (e.g. "0") in current graphics font = roughly average non-space character width local spacewid : _length " " // width of a space in current graphics font summ `id' if `touse', meanonly assert r(min)==1 local maxid = r(max) local multip = 1 local add = 0 quietly { // Apr 2020 // DOUBLE LINE OPTION if `"`double'"'!=`""' & (`lcolsN' + `rcolsN' - ("`_EFFECT'"!="") - ("`wt'"=="")) { tempvar expand expand 2 if `touse' & inlist(`_USE', 1, 2), gen(`expand') replace `_USE' = 6 if `touse' & `expand' // TITLES CLOSER TOGETHER, GAP BENEATH local multip = 0.45 local add = 0.5 } local maxN = _N ** Left columns local leftWDtot = 0 local nlines = 0 forvalues i = 1 / `lcolsN' { local leftLB`i' : word `i' of `llablist' local fmtlen : word `i' of `lfmtlist' // Aug 2023: remove "~" symbol (centered format) if necessary tokenize `fmtlen', parse("~") if "`1'"=="~" local fmtlen `2' confirm integer number `fmtlen' gen long `strlen' = length(`leftLB`i'') summ `strlen' if `touse', meanonly local maxlen = r(max) // max length of existing text // Apr 2020 ** DOUBLE LINE OPTION if `"`double'"'!=`""' { forvalues j = 1 / `maxid' { summ `_USE' in `j', meanonly if inlist(`r(min)', 1, 2) { summ `id' in `j', meanonly local idj = r(min) local leftLBj = `leftLB`i''[`j'] SpreadTitle `"`leftLBj'"', target(`=round(`maxlen'/2)') maxlines(2) notruncate replace `leftLB`i'' = `"`r(title1)'"' if `touse' & `id'==`idj' & !`expand' replace `leftLB`i'' = `"`r(title2)'"' if `touse' & `id'==`idj' & `expand' } } getWidth `leftLB`i'' `strwid' summ `strwid' if `touse', meanonly local leftWD`i' = r(max) // exact width of `maxlen' string } else { getWidth `leftLB`i'' `strwid' summ `strwid' if `touse', meanonly local maxwid = r(max) // max width of existing text local leftWD`i' = cond(abs(`fmtlen') <= `maxlen', `maxwid', /// // exact width of `maxlen' string abs(`fmtlen')*`digitwid') // approx. max width (based on `digitwid') } ** Check whether title string is longer than the data itself // If so, potentially allow spread over a suitable number of lines // [DF JAN 2015: Future work might be to re-write (incl. SpreadTitle) to use width rather than length??] // If more than one lcol, restrict to width of "data only" (i.e. _USE==1, 2). // Otherwise, title may be as long as the max string length in the column. // [Note that, as the title isn't stored as data (yet), the max string length does NOT account for the title string itself.] if `lcolsN' > 1 local anduse `"& inlist(`_USE', 1, 2)"' summ `strlen' if `touse' `anduse', meanonly local maxlen = r(max) local colName : variable label `leftLB`i'' if `"`colName'"'!=`""' { if `target' <= 0 | missing(`target') { if `maxwidth' local target_opt = `maxwidth' else if "`double'"=="" { local target_opt = max(abs(`fmtlen'), `maxlen') } else local target_opt = `maxlen' } local maxwidth_opt = cond(`maxwidth', `maxwidth', `=2*`target_opt'') SpreadTitle `"`colName'"', target(`target_opt') maxwidth(`maxwidth_opt') maxlines(`maxlines') `truncate' if `r(nlines)' > `nlines' { local oldN = _N set obs `=`oldN' + `r(nlines)' - `nlines'' local nlines = r(nlines) replace `_USE' = 9 if _n > `oldN' replace `touse' = 1 if _n > `oldN' replace `id' = `maxid' + (_n - `maxN')*`multip' + `add' + 1 if _n > `oldN' // "+1" leaves a one-line gap between titles & main data } local l = `nlines' - `r(nlines)' forvalues j = `r(nlines)'(-1)1 { local k = _N - (`j' + `l') + 1 replace `leftLB`i'' = `"`r(title`j')'"' in `k' } getWidth `leftLB`i'' `strwid', replace // re-calculate `strwid' to include titles summ `strwid' if `touse', meanonly local maxwid = r(max) local leftWD`i' = max(`leftWD`i'', `maxwid') // in case title is necessarily longer than the variable, even after SpreadTitle } local leftWD`i' = `leftWD`i'' + (2 - (`i'==`lcolsN'))*`digitwid' // having calculated the indent, add a buffer (2x except for last col) local leftWDtot = `leftWDtot' + `leftWD`i'' // running calculation of total width (including titles) drop `strlen' `strwid' } // end of forvalues i=1/`lcolsN' ** Right columns local rightWDtot = 0 forvalues i=1/`rcolsN' { // if `rcolsN'==0, loop will be skipped local rightLB`i' : word `i' of `rlablist' local fmtlen : word `i' of `rfmtlist' // Aug 2023: remove "~" symbol (centered format) if necessary tokenize `fmtlen', parse("~") if "`1'"=="~" local fmtlen `2' confirm integer number `fmtlen' gen long `strlen' = length(`rightLB`i'') summ `strlen' if `touse', meanonly local maxlen = r(max) // max length of existing text // Apr 2020 ** DOUBLE LINE OPTION if `"`double'"'!=`""' { forvalues j = 1 / `maxid' { summ `_USE' in `j', meanonly if inlist(`r(min)', 1, 2) { summ `id' in `j', meanonly local idj = r(min) local rightLBj = `rightLB`i''[`j'] if `"`rightLB`i''"'!=`"`_EFFECT'"' { SpreadTitle `"`rightLBj'"', target(`=round(`maxlen'/2)') maxlines(2) notruncate replace `rightLB`i'' = `"`r(title1)'"' if `id'==`idj' & !`expand' replace `rightLB`i'' = `"`r(title2)'"' if `id'==`idj' & `expand' } else { // Oct 2020: if _EFFECT, simply blank out the duplicated second line replace `rightLB`i'' = `""' if `id'==`idj' & `expand' } } } getWidth `rightLB`i'' `strwid' summ `strwid' if `touse', meanonly local rightWD`i' = r(max) // exact width of `maxlen' string } else { getWidth `rightLB`i'' `strwid' summ `strwid' if `touse', meanonly local maxwid = r(max) // max width of existing text local rightWD`i' = cond(abs(`fmtlen') <= `maxlen', `maxwid', /// // exact width of `maxlen' string abs(`fmtlen')*`digitwid') // approx. max width (based on `digitwid') } ** Check whether title string is longer than the data itself // If so, spread it over a suitable number of lines // [DF JAN 2015: Future work might be to re-write (incl. SpreadTitle) to use width rather than length??] local colName : variable label `rightLB`i'' if `"`colName'"'!=`""' { // June 2020 // If _EFFECT column, make sure a line break is not placed in the middle of "(95% CI)" local ci_break = 0 local strpos1 = strpos(`"`colName'"', `"("') local strpos2 = strpos(`"`colName'"', `"% CI)"') if `strpos2' & (`strpos2' - `strpos1' <= 3) { local colName = subinstr(`"`colName'"', `"% CI)"', `"%_CI)"', 1) local ci_break = 1 } if `target' <= 0 | missing(`target') { if `maxwidth' local target_opt = `maxwidth' else if "`double'"=="" { local target_opt = max(abs(`fmtlen'), `maxlen') } else local target_opt = `maxlen' } local maxwidth_opt = cond(`maxwidth', `maxwidth', `=2*`target_opt'') SpreadTitle `"`colName'"', target(`target_opt') maxwidth(`maxwidth_opt') maxlines(`maxlines') `truncate' if `r(nlines)' > `nlines' { local oldN = _N set obs `=`oldN' + `r(nlines)' - `nlines'' local nlines = r(nlines) replace `_USE' = 9 if _n > `oldN' replace `touse' = 1 if _n > `oldN' replace `id' = `maxid' + (_n - `maxN')*`multip' + `add' + 1 if _n > `oldN' // "+1" leaves a one-line gap between titles & main data } local l = `nlines' - `r(nlines)' forvalues j = `r(nlines)'(-1)1 { local k = _N - (`j' + `l') + 1 // June 2020 // Reset the "(95% CI)" string, if appropriate local rtitlej `"`r(title`j')'"' if `ci_break' { local strpos1 = strpos(`"`rtitlej'"', `"("') local strpos2 = strpos(`"`rtitlej'"', `"%_CI)"') if `strpos2' & (`strpos2' - `strpos1' <= 3) { local rtitlej = subinstr(`"`rtitlej'"', `"%_CI)"', `"% CI)"', 1) local ci_break = 1 } } replace `rightLB`i'' = `"`rtitlej'"' in `k' } getWidth `rightLB`i'' `strwid', replace // re-calculate `strwid' to include titles summ `strwid' if `touse', meanonly local maxwid = r(max) local rightWD`i' = max(`rightWD`i'', `maxwid') // in case title is necessarily longer than the variable, even after SpreadTitle } local rightWD`i' = `rightWD`i'' + (2 - (`i'==`rcolsN'))*`digitwid' // having calculated the indent, add a buffer (2x except for last col) local rightWDtot = `rightWDtot' + `rightWD`i'' // running calculation of total width (incl. buffer) drop `strlen' `strwid' } // end of forvalues i=1/`rcols' if !`rcolsN' & `"`colsonly'"'!=`""' local rightWDtot = 0 // if `colsonly', set to zero... [ADDED MAY 2024] else { local rightWDtot = `rightWDtot' + `rbuffer'*`digitwid' // ...otherwise use a minimal buffer (default 1x but can be overwritten) local leftWDtot = max(`leftWDtot', `digitwid') // ...before first RHS column and after last } ** "Adjust" routine * Notes: // Unless we're dealing with a very non-standard user-specific case, // effect sizes corresponding to pooled diamonds (_USE==3, 5) will usually be much tighter around the null value than individual effects (_USE==1, 2). // The longest strings of text are also likely to be found in _USE==0, 3, 4, 5, since these contain subgroup headings and heterogeneity info. // Therefore, we may be able to improve the aesthetics of the plot by: // (1) allowing text in LH columns for _USE==3, 5 to overlap text in LH columns for _USE==1, 2; // (2) allowing text in LH columns for _USE==3, 5 to extend into the central plot area, beyond the default limit of `DXmin' but without overwriting plot elements. * However, there are some considerations: // - LH text columns for _USE==1, 2 must *never* extend beyond `DXmin' (o/w long study labels and long CI limits might be overwritten) // - Column titles (i.e. variable labels; _USE==9) may only be extended for the last (i.e. right-most) left-hand column // - LH text columns for _USE==0, 3, 4, 5 may only be extended if there is no data in the remaining LH columns (if any) to their right // - In particular, if data exists in LH columns to the right, default behaviour is for "heterogeneity info" to be placed on a new line (_USE==4) // rather than at the end of the "pooled overall/subgroup" text (_USE==3, 5). // This may be overruled using `noextraline' with -(ad)metan- which implies `nolcolcheck' with -forestplot- // - _USE==6 represents a blank line, so these rows are irrelevant to the calculations. The user may place text in such rows at their own discretion; it may get overwritten. * Hence, the strategy is: // - Recalculate column widths (`leftWD`i'') restricting to _USE==1, 2, 9 (except last column, for which exclude _USE==9) // - BUT if a subsequent LH column has data in _USE==0, 3, 5 then previous adjustments are cancelled (unless `nolcolcheck') // - In this way, build up a recalculated total width (`leftWDtotNoTi'). If this is less than the original total (`leftWDtot'), then there is scope for "adjustment" (see below). if "`adjust'" == "" { // initialise locals local leftWDtotTi = 0 local leftWDtotNoTi = 0 // local adjustTot = 0 // local adjustNew = 0 // May 2018 // If `nocolscheck', any lengthy text will be in _USE==4 rather than 3 or 5; and such text should only exist in the first column so may be ignored // June 2018: this section re-written // Re-calculate widths of `lcols' for study estimates only (i.e. _USE==1, 2; this is `leftWD`i'NoTi') local lastcol = 1 forvalues i=1/`lcolsN' { local fmtlen : word `i' of `lfmtlist' // desired max no. of characters based on format -- also shows whether left- or right-justified // Aug 2023: remove "~" symbol (centered format) if necessary tokenize `fmtlen', parse("~") if "`1'"=="~" local fmtlen `2' // Check for data in observations *other* than study estimates (i.e. _USE==0, 3, 5, 7) // if there is, this becomes `lastcol' gen long `strlen' = length(`leftLB`i'') summ `strlen' if `touse' & inlist(`_USE', 0, 3, 5, 7), meanonly if r(N) & r(max) local lastcol = `i' // Now compare "total width" with "width for study estimates only" for current column only // (including titles, UNLESS last column of all (`lcolsN'); so that, if multiple columns, adjusted width of first column includes title // and hence, second column doesn't obscure it) summ `strlen' if `touse' & (inlist(`_USE', 1, 2) | (`i'<`lcolsN' & `_USE'==9)), meanonly if !r(N) local leftWD`i'NoTi = 0 // if summary diamonds only (added Sep 2017 for v2.1) else { local maxlen = r(max) // max length of text for study estimates only getWidth `leftLB`i'' `strwid' summ `strwid' if `touse' & (inlist(`_USE', 1, 2) | (`i'<`lcolsN' & `_USE'==9)), meanonly local maxwid = r(max) // max width of text for study estimates only if "`double'"!="" local leftWD`i'NoTi = `maxwid' else { local leftWD`i'NoTi = cond(abs(`fmtlen') <= `maxlen', `maxwid', /// // exact width of `maxlen' string abs(`fmtlen')*`digitwid') // approx. max width (based on `digitwid') } // replace `lindent`i'NoTi' = cond(`fmtlen'>0, `leftWD`i'NoTi' - `strwid', 0) // indent if right-justified drop `strwid' } local leftWD`i'NoTi = `leftWD`i'NoTi' + (2 - (`i'==`lcolsN'))*`digitwid' // having calculated the indent, add a buffer (2x except for last col) drop `strlen' } // Finally, iterate `leftWDtotTi' ("unadjusted" widths up to and including `lastcol') // and `leftWDtotNoTi' ("unadjusted" widths up to `lastcol', then "adjusted" widths) // Plus, if appropriate, cancel previous *single* adjustments // (the above code only handles the running totals) if `"`lcolscheck'"'!=`""' local lastcol = 1 forvalues i=1/`lcolsN' { if `i' < `lastcol' { local leftWD`i'NoTi = `leftWD`i'' // replace `lindent`i'NoTi' = `lindent`i'' local leftWDtotTi = `leftWDtotTi' + `leftWD`i'' } else if `i' == `lastcol' { local leftWDtotTi = `leftWDtotTi' + `leftWD`i'' } local leftWDtotNoTi = `leftWDtotNoTi' + `leftWD`i'NoTi' } // If appropriate, allow _USE=0,3,4,5 to extend into main plot by a factor of (lcimin-DXmin)/DXwidth // where `lcimin' is the left-most confidence limit among the "diamonds" (including prediction intervals) // i.e. 1 + ((`lcimin'-`DXmin')/`DXwidth') * ((100-`astext')/`astext')) is the percentage increase // to apply to `leftWDtot'+`rightWDtot' in order to obtain `newleftWDtot'+`rightWDtot'. // Then rearrange to find `newleftWDtot'. if `leftWDtotNoTi' < `leftWDtot' { // June 2018: // Firstly, reset `leftWDtot' local leftWDtot = max(`leftWDtotTi', `leftWDtotNoTi') // sort out astext... need to do this now, but will be recalculated later (line 890) if `DXwidthChars'!=-9 & `astext'==-9 { local astext2 = (`leftWDtot' + `rightWDtot')/`DXwidthChars' local astext = 100 * `astext2'/(1 + `astext2') } else { local astext = cond(`astext'==-9, 50, `astext') assert `astext' >= 0 local astext2 = `astext'/(100 - `astext') } // define some additional locals to make final formula clearer local totWD = `leftWDtot' + `rightWDtot' local lciWD = (`lcimin' - `DXmin')/(`DXmax' - `DXmin') local newleftWDtot = cond(`DXwidthChars'==-9, /// (`totWD' / ((`lciWD'/`astext2') + 1)) - `rightWDtot', /// `leftWDtot' - `lciWD'*`DXwidthChars') // Finally, reset `leftWDtot' once more // BUT don't make it any less than `leftWDtotNoTi', *unless* there are no obs with inlist(`_USE', 1, 2) // o/w longest study labels might overwrite longest CIs. count if `touse' & inlist(`_USE', 1, 2) local leftWDtot = cond(r(N), max(`leftWDtotNoTi', `newleftWDtot'), `newleftWDtot') // ...and similarly replace individual column widths forvalues i=1/`lcolsN' { local leftWD`i' = `leftWD`i'NoTi' // replace `lindent`i'' = `lindent`i'NoTi' } } } // end if "`adjust'" == "" if !`lcolsN' & `"`colsonly'"'!=`""' local leftWDtot = 0 // if `colsonly', set to zero... [ADDED MAY 2024] else { local leftWDtot = `leftWDtot' + `lbuffer'*`digitwid' // LHS buffer; default is zero local leftWDtot = max(`leftWDtot', `digitwid') // ...otherwise use a minimal buffer for LHS of plotted data } // Calculate `textWD', using `astext' (% of graph width taken by text) // to relate the width of plot area in "plot units" to the width of the columns in "text units" if `DXwidthChars'!=-9 & (`astext'==-9 | `"`newleftWDtot'"'!=`""') { local astext2 = (`leftWDtot' + `rightWDtot')/`DXwidthChars' local astext = 100 * `astext2'/(1 + `astext2') // local astext = cond(`DXwidthChars'>0, 100 * `astext2'/(1 + `astext2'), 100) // added Feb 2018 } else { local astext = cond(`astext'==-9, 50, `astext') assert `astext' >= 0 local astext2 = `astext'/(100 - `astext') } local textWD = `astext2' * (`DXmax' - `DXmin')/(`leftWDtot' + `rightWDtot') // Generate positions of columns, in terms of "plot co-ordinates" // (N.B. although the "starting positions", `leftWD`i'' and `rightWD`i'', are constants, there will be indents if right-justified // and anyway, all will need to be stored in variables for use with -twoway-) local leftWDruntot = 0 forvalues i = 1/`lcolsN' { local left`i' : word `i' of `lvallist' // extract next tempvar name from predefined list // June 2023: deprecate indentation in favour of use of mlabpos() local nextval = `DXmin' - (`leftWDtot' - `leftWDruntot')*`textWD' local leftWDruntot = `leftWDruntot' + `leftWD`i'' // iterate local nextpos 3 gen double `left`i'' = `nextval' // default, if left-justified // Aug 2023: allow centered formatting local fmtlen : word `i' of `lfmtlist' tokenize `fmtlen', parse("~") if "`1'"=="~" { // if centered, place halfway between this position and the next and use mlabpos(0) local nextnextval = `DXmin' - (`leftWDtot' - `leftWDruntot' + (2 - (`i'==`lcolsN'))*`digitwid')*`textWD' replace `left`i'' = (`left`i'' + `nextnextval')/2 local nextpos 0 } else if `fmtlen'>=0 { local nextnextval = `DXmin' - (`leftWDtot' - `leftWDruntot' + (2 - (`i'==`lcolsN'))*`digitwid')*`textWD' replace `left`i'' = `nextnextval' // if right-justified; remove buffer from position value local nextpos 9 } local lposlist `lposlist' `nextpos' } if !`lcolsN' { // Added July 2015 local left1 : word 1 of `lvallist' gen `left1' = `DXmin' - 2*`digitwid'*`textWD' } if `"`rfindent'"'!=`""' { tempvar rindent gen `rindent' = . } local rightWDruntot = `digitwid' // initial 1x buffer forvalues i = 1/`rcolsN' { // if `rcolsN'=0 then loop will be skipped local right`i' : word `i' of `rvallist' // extract next tempvar name from predefined list // June 2023: deprecate indentation in favour of use of mlabpos() local nextval = `DXmax' + `rightWDruntot'*`textWD' local rightWDruntot = `rightWDruntot' + `rightWD`i'' // iterate local nextpos 3 gen double `right`i'' = `nextval' // default, if left-justified local fmtlen : word `i' of `rfmtlist' tokenize `fmtlen', parse("~") if "`1'"=="~" { // if centered, place halfway between this position and the next and use mlabpos(0) local nextnextval = `DXmax' + (`rightWDruntot' - (2 - (`i'==`rcolsN'))*`digitwid')*`textWD' replace `right`i'' = (`right`i'' + `nextnextval')/2 local nextpos 0 } // special case: if rfdist and left-justified, impose a small indent so that the CIs line up else if `"`rfindent'"'!=`""' & `i'==`rfcol' & `fmtlen'<0 { getWidth `rfindent' `rindent' if `touse' & !missing(`rfindent'), replace replace `right`i'' = `right`i'' + (`rindent' + `spacewid')*`textWD' if `touse' & !missing(`rfindent') } // if right-justified, obtain position of *next* column and use mlabpos(); but remove buffer from position value else if `fmtlen'>=0 { local nextnextval = `DXmax' + (`rightWDruntot' - (2 - (`i'==`rcolsN'))*`digitwid')*`textWD' replace `right`i'' = `nextnextval' local nextpos 9 } local rposlist `rposlist' `nextpos' } // Finish off `double' if `"`double'"'!=`""' { recast float `id' replace `id' = `id' - 0.45 if `expand'==1 } } // end quietly // AXmin AXmax ARE THE OVERALL LEFT AND RIGHT COORDS summ `left1' if `touse', meanonly local AXmin = r(min) local AXmax = `DXmax' + `rightWDtot'*`textWD' return scalar leftWDtot = `leftWDtot' return scalar rightWDtot = `rightWDtot' return scalar AXmin = `AXmin' return scalar AXmax = `AXmax' return scalar astext = `astext' // June 2023 return local lposlist `lposlist' return local rposlist `rposlist' return local graphopts `"`graphopts'"' end * Subroutine to "spread" titles out over multiple lines if appropriate // Updated July 2014 // August 2016: identical program now used here, in admetan.ado, and in ipdover.ado // May 2017: updated to accept substrings delineated by quotes (c.f. multi-line axis titles) // August 2017: updated for better handling of maxlines() // March 2018: updated to receive text in quotes, hence both avoiding parsing problems with commas, and maintaining spacing // May 2018 and Nov 2018: updated truncation procedure // subroutine of ProcessColumns program define SpreadTitle, rclass syntax [anything(name=title id="title string")] [, TArget(integer 0) MAXWidth(integer 0) MAXLines(integer 0) noTRUNCate noUPDATE ] * Target = aim for this width, but allow expansion if alternative is wrapping "too early" (i.e before line is adequately filled) // (may be replaced by `titlelen'/`maxlines' if `maxlines' and `notruncate' are also specified) * Maxwidth = absolute maximum width ... but will be increased if a "long" string is encountered before the last line * Maxlines = maximum no. lines (default 3) * noTruncate = don't truncate final line if "too long" (even if greater than `maxwidth') * noUpdate = don't update `target' if `maxwidth' is increased (see above) tokenize `title' if `"`1'"'==`""' { return scalar nlines = 0 return scalar maxwidth = 0 exit } if `maxwidth' & !`maxlines' { cap assert `maxwidth'>=`target' if _rc { nois disp as err `"{bf:maxwidth()} must be greater than or equal to {bf:target()}"' exit 198 } } ** Find length of title string, or maximum length of multi-line title string // First run: strip off outer quotes if necessary, but watch out for initial/final spaces! gettoken tok : title, qed(qed) cap assert `"`tok'"'==`"`1'"' if _rc { gettoken tok rest : tok, qed(qed) assert `"`tok'"'==`"`1'"' local title1 title1 // specifies that title is not multi-line } local currentlen = length(`"`1'"') local titlelen = length(`"`1'"') // Subsequent runs: successive calls to -gettoken-, monitoring quotes with the qed() option macro shift while `"`1'"'!=`""' { local oldqed = `qed' gettoken tok rest : rest, qed(qed) assert `"`tok'"'==`"`1'"' if !`oldqed' & !`qed' local currentlen = `currentlen' + 1 + length(`"`1'"') else { local titlelen = max(`titlelen', `currentlen') local currentlen = length(`"`1'"') } macro shift } local titlelen = max(`titlelen', `currentlen') // Save user-specified parameter values separately local target_orig = `target' local maxwidth_orig = `maxwidth' local maxlines_orig = `maxlines' // Now finalise `target' and calculate `spread' local maxlines = cond(`maxlines_orig', `maxlines_orig', 3) // use a default value for `maxlines' of 3 in these calculations local target = cond(`target_orig', `target_orig', /// cond(`maxwidth_orig', min(`maxwidth_orig', `titlelen'/`maxlines'), `titlelen'/`maxlines')) local spread = min(int(`titlelen'/`target') + 1, `maxlines') local crit = cond(`maxwidth_orig', min(`maxwidth_orig', `titlelen'/`spread'), `titlelen'/`spread') ** If substrings are present, delineated by quotes, treat this as a line-break // Hence, need to first process each substring separately and obtain parameters, // then select the most appropriate overall parameters given the user-specified options, // and finally create the final line-by-line output strings. tokenize `title' local line = 1 local title`line' : copy local 1 // i.e. `"`title`line''"'==`"`1'"' local newwidth = length(`"`title`line''"') // if first "word" is by itself longer than `maxwidth' ... if `maxwidth' & !(`maxlines' & (`line'==`maxlines')) { // ... reset parameters and start over while length(`"`1'"') > `maxwidth' { local maxwidth = length(`"`1'"') local target = cond(`target_orig', cond(`"`update'"'!=`""', `target_orig', `target_orig' + `maxwidth' - `maxwidth_orig'), /// cond(`maxwidth', min(`maxwidth', `titlelen'/`maxlines'), `titlelen'/`maxlines')) local spread = min(int(`titlelen'/`target') + 1, `maxlines') local crit = cond(`maxwidth', min(`maxwidth', `titlelen'/`spread'), `titlelen'/`spread') } } macro shift local next : copy local 1 // i.e. `"`next'"'==`"`1'"' (was `"`2'"' before macro shift!) while `"`1'"' != `""' { // local check = `"`title`line''"' + `" "' + `"`next'"' // (potential) next iteration of `title`line'' local check `"`title`line'' `next'"' // (amended Apr 2018 due to local x = "" issue with version <13) if length(`"`check'"') > `crit' { // if longer than ideal... // ...and further from target than before, or greater than maxwidth if abs(length(`"`check'"') - `crit') > abs(length(`"`title`line''"') - `crit') /// | (`maxwidth' & (length(`"`check'"') > `maxwidth')) { if `maxlines' & (`line'==`maxlines') { // if reached max no. of lines local title`line' : copy local check // - use next iteration anyway (to be truncated) macro shift local next : copy local 1 local newwidth = max(`newwidth', length(`"`title`line''"')) // update `newwidth' continue, break } else { // otherwise: local ++line // - new line // if first "word" of new line (i.e. `next') is by itself longer than `maxwidth' ... if `maxwidth' & (length(`"`next'"') > `maxwidth') { // ... if we're on the last line or last token, continue as normal ... if !((`maxlines' & (`line'==`maxlines')) | `"`2'"'==`""') { // ... but otherwise, reset parameters and start over local maxwidth = length(`"`next'"') local target = cond(`target_orig', cond(`"`update'"'!=`""', `target_orig', `target_orig' + `maxwidth' - `maxwidth_orig'), /// cond(`maxwidth', min(`maxwidth', `titlelen'/`maxlines'), `titlelen'/`maxlines')) local spread = min(int(`titlelen'/`target') + 1, `maxlines') local crit = cond(`maxwidth', min(`maxwidth', `titlelen'/`spread'), `titlelen'/`spread') // restart loop tokenize `title' local tok = 1 local line = 1 local title`line' : copy local 1 // i.e. `"`title`line''"'==`"`1'"' local newwidth = length(`"`title`line''"') macro shift local next : copy local 1 // i.e. `"`next'"'==`"`1'"' (was `"`2'"' before macro shift!) continue } } local title`line' : copy local next // - begin new line with next word } } else local title`line' : copy local check // else use next iteration } else local title`line' : copy local check // else use next iteration macro shift local next : copy local 1 local newwidth = max(`newwidth', length(`"`title`line''"')) // update `newwidth' } // (N.B. won't be done if reached max no. of lines, as loop broken) * Return strings forvalues i=1/`line' { // truncate if appropriate (last line only) if `i'==`line' & "`truncate'"=="" & `maxwidth' { local title`i' = substr(`"`title`i''"', 1, `maxwidth') } return local title`i' `"`title`i''"' } * Return values return scalar nlines = `line' return scalar maxwidth = min(`newwidth', `maxwidth') return scalar target = `target' end ********************************************************************************* program define UserSpecHide, rclass syntax varname [if] [in], PLOTID(varname numeric) [ OCILINEOPts(string asis) RFCILINEOPts(string asis) * ] local _USE : copy local varlist marksample touse local reduceHeight = 0 local 0 `", `ocilineopts'"' syntax [, HIDE * ] if `"`hide'"'!=`""' { qui count if `touse' & inlist(`_USE', 3, 5, 7) local reduceHeight = r(N) } else { local 0 `", `rfcilineopts'"' syntax [, HIDE * ] if `"`hide'"'!=`""' { qui count if `touse' & inlist(`_USE', 3, 5, 7) local reduceHeight = r(N) } else { summ `plotid' if `touse', meanonly forvalues p = 1/`r(max)' { local hide local 0 `", `graphopts'"' syntax [, OCILINE`p'opts(string asis) RFCILINE`p'opts(string asis) * ] local 0 `", `ociline`p'opts'"' syntax [, HIDE * ] if `"`hide'"'!=`""' { qui count if `touse' & `plotid'==`p' & inlist(`_USE', 3, 5, 7) local reduceHeight = `reduceHeight' + r(N) } else { local 0 `", `rfciline`p'opts'"' syntax [, HIDE * ] if `"`hide'"'!=`""' { qui count if `touse' & `plotid'==`p' & inlist(`_USE', 3, 5, 7) local reduceHeight = `reduceHeight' + r(N) } } } } } return scalar N = `reduceHeight' end ********************************************************************************* *** FIND OPTIMAL TEXT SIZE AND ASPECT RATIOS (given user input) // Notes: (David Fisher, July 2014) // Let X, Y be dimensions of graphregion (outer; controlled by xsize(), ysize()); x, y be dimensions of plotregion (inner; controlled by aspect()). // `approxChars' is the approximate width of the plot, in "character units" (i.e. width of [LHS text + RHS text] divided by `astext') // Note that a "character unit" is the width of a character relative to its height; // hence `height' is the approximate height of the plot, in terms of both rows of text (with zero gap between rows) AND "character units". // If Y/X = `graphAspect'<1, `textSize' is the height of a row of text relative to Y; otherwise it is height relative to X. // (Note that the default `graphAspect' = 4/5.5 = 0.73 < 1) // We then let `approxChars' = x, and manipulate to find the optimum text size for the plot layout. // FEB 2015: `textsize' is deprecated, since it causes problems with spilling on the RHS. // Instead, using `spacing' to fine-tune the aspect ratio (and hence the text size) // or use `aspect' to completely user-define the aspect ratio. // MAY 2020: Following discussion with Jonathan Sterne, option textsize() has been reinstated // But it works *at the end*, after the aspect ratio has already been calculated -- so it's "use at your own risk" // Internally, it is renamed to `textscale', since it is actually a scale, and it avoids clashing with already-coded `textsize' // - Note that this code has been changed considerably from the original -metan9- code. // Moved into separate subroutine Nov 2017 (for v2.2 beta) program define GetAspectRatio, rclass syntax [, ASTEXT(real 50) COLWDTOT(real 0) HEIGHT(real 0) USEDIMS(name) /// ASPECT(real -9) SPacing(real -9) XSIZe(real -9) YSIZe(real -9) FXSIZe(real -9) FYSIZe(real -9) /// TItle(string asis) SUBtitle(string asis) CAPTION(string asis) NOTE2(string asis) noNOTE noWARNing /// XTItle(string asis) ROWSFAV(real 0) ADDHeight(real 0) /*(undocumented)*/ /// TEXTSize(real 100.0) /// /* legacy -metan9- option, implemented here as a post-hoc option; use at own risk */ ROWSXLAB(real 0) /*DXWIDTHChars(real -9)*/ DOUBLE COLSONLY * ] local graphopts `"`options'"' // Error message copied directly from -metan9- if `textsize' < 20 | `textsize' > 500 { di as error "Text scale (TEXTSize) must be within 20-500" di as error "Value is character size relative to graph" di as error "Outside range will either be unreadable or too large" exit 198 } local textscale : copy local textsize // rename to `textscale' for internal use... local textsize // ... and reset `textsize' macro [May 2020] * Unpack `usedims' local DXwidthChars : copy local dxwidthchars // added Feb 2018: clarity local DXwidthChars = -9 // initialize local oldTextSize = -9 // initialize if `"`usedims'"'!=`""' { local DXwidthChars = `usedims'[1, `=colnumb(matrix(`usedims'), "cdw")'] local spacing = cond(`spacing'==-9, `usedims'[1, `=colnumb(matrix(`usedims'), "spacing")'], `spacing') local oldPlotAspect = `usedims'[1, `=colnumb(matrix(`usedims'), "aspect")'] // modified Nov 2017 local oldXSize = `usedims'[1, `=colnumb(matrix(`usedims'), "xsize")'] local oldYSize = `usedims'[1, `=colnumb(matrix(`usedims'), "ysize")'] local oldTextSize = `usedims'[1, `=colnumb(matrix(`usedims'), "textsize")'] local oldHeight = `usedims'[1, `=colnumb(matrix(`usedims'), "height")'] // added Sep 2017 local oldYheight = `usedims'[1, `=colnumb(matrix(`usedims'), "yheight")'] // added Sep 2017 numlist "`DXwidthChars' `spacing' `oldPlotAspect' `oldXSize' `oldYSize' `oldTextSize' `oldHeight' `oldYheight'", min(8) max(8) range(>=0) } * Obtain number of rows within each title element // (see help title_options) // [modified Nov 2017] // (N.B. favours will be done separately) // [modified Feb 2018] // [Jan 2019: converted to subroutine for better parsing of compound quotes] foreach opt in title subtitle caption note xtitle { GetRows ``opt'' local rows`opt' = r(rows) } local condtitle = 2*`rowstitle' + 1.5*`rowssubtitle' + 1.25*`rowscaption' + 1.25*`rowsnote' // approximate multipliers for different text sizes + gaps local condtitle = `condtitle' + (`"`title'"'!=`""' & `"`subtitle'"'!=`""') // additional gap between title and subtitle, if *both* specified local condtitle = `condtitle' + 2 + `addheight' // add 2 for graphregion(margin()) // Now derive small amounts `xdelta', `ydelta', to take account of the space taken up by titles etc. // Assume that, if plot is "full-width", then X = x * xdelta // and that, if plot is "full-height", then Y = y * ydelta // local ydelta = (`height' + `condtitle' + (`"`xlablist'"'!=`""') + `rowsfav' + `rowsxtitle')/`height' local ydelta = (`height' + `condtitle' + `rowsxlab' + `rowsfav' + `rowsxtitle')/`height' // Nov 2017 local xdelta = (`height' + `condtitle')/`height' // Oct 2016: check logic of this, why difference in what is added?? // Notes Feb 2015: // - could maybe be improved, but for now `addheight' option (undocumented) allows user to tweak // - also think about line widths (thicknesses), can we keep them constant-ish?? // May 2016: yes, should be quite easy -- choose a reasonable value based on the height, then amend it in the same way as textsize * Derive `approxChars', `spacing' and `plotAspect' // (possibly using saved "dimensions") // (for future: investigate using margins to "centre on DXwidth" within graphregion??) if `"`usedims'"'==`""' { local approxChars = 100*`colwdtot'/`astext' if `aspect' != -9 { // user-specified aspect of plotregion if `spacing' == -9 local spacing = `aspect' * `approxChars' / `height' // [modified Nov 2017] local plotAspect = `aspect' } else { // if not user-specified // if "natural aspect" (`height'/`approxChars') is 2x1 or wider, use double spacing; else use 1.5-spacing // Apr 2020: if -double- option, increase the spacing again... 4/3 seems to work (N.B. this is 2/1.5, i.e. ratio of usual options) // (unless user-specified, in which case use that) if `spacing' == -9 local spacing = cond(`height'/`approxChars' <= .5, 2, 1.5) if `"`double'"'!=`""' local spacing = (4/3) * `spacing' local plotAspect = `spacing' * `height' / `approxChars' } } else { // if `usedims' supplied local approxChars = `colwdtot' + cond(`"`colsonly'"'!=`""', 0, `DXwidthChars') // modified Feb 2018 local plotAspect = cond(`aspect'==-9, `spacing'*`height'/`approxChars', `aspect') // `spacing' here is from `usedims' unless over-ridden by user } numlist "`plotAspect' `spacing'", range(>=0) * Derive graphAspect = Y/X (defaults to 4/5.5 = 0.727 unless specified) // [modified Nov 2017] if `"`usedims'"'==`""' { local oldYSize = 4 local oldXSize = 5.5 } local graphAspect = cond(`ysize'==-9, `oldYSize', `ysize') /// / cond(`xsize'==-9, `oldXSize', `xsize') // July 2015 * Standard approach is now to use `graphAspect' and `plotAspect' to determine `textSize'. if `"`usedims'"'==`""' { // (1) If y/x < Y/X < 1 (i.e. plot takes up full width of "wide" graph) then X = x * xdelta // ==> `textSize' = 100/Y = 100/(X * `graphAspect') = 100/(`xdelta' * `approxChars' * `graphAspect') if `graphAspect' <= 1 & `plotAspect' <= `graphAspect' { local textSize = 100 / (`xdelta' * `approxChars' * `graphAspect') } // (2) If Y/X < 1 and y/x > Y/X (i.e. plot is less wide than "wide" graph) then Y = y * ydelta // ==> `textSize' = 100/Y = 100/(ydelta * x * `plotAspect') = 100 / (`ydelta' * `approxChars' * `plotAspect') else if `graphAspect' <= 1 & `plotAspect' > `graphAspect' { local textSize = 100 / (`ydelta' * `approxChars' * `plotAspect') } // (3) If y/x > Y/X > 1 (i.e. plot takes up full height of "tall" graph) then Y = y * ydelta // ==> `textSize' = 100/X = 100 * `graphAspect'/(y * ydelta) = 100 * `graphAspect' / (`ydelta' * `approxChars' * `plotAspect') else if `graphAspect' > 1 & `plotAspect' > `graphAspect' { local textSize = (100 * `graphAspect') / (`ydelta' * `approxChars' * `plotAspect') } // (4) If Y/X > 1 and y/x < Y/X (i.e. plot is less tall than "tall" graph) then X = x * xdelta // ==> `textSize' = 100/X = 100 / (`xdelta' * `approxChars') else if `graphAspect' > 1 & `plotAspect' <= `graphAspect' { local textSize = 100 / (`xdelta' * `approxChars') } // [added Nov 2017] // If Y/X = `graphAspect' <= 1 ("wide"), set fysize to 100; else ("tall") set fxsize to 100 // in other words, min dimension is always 100; the other is >100 local fxsize = cond(`fxsize' == -9, cond(`graphAspect' <= 1, 100/`graphAspect', 100), `fxsize') local fysize = cond(`fysize' == -9, cond(`graphAspect' <= 1, 100, 100*`graphAspect'), `fysize') } * Else if `usedims' supplied: * oldGraphAspect and oldPlotAspect would have been derived using the rules above * we immediately know the new plotAspect = `spacing'*`height'/`approxChars' (using new `approxChars') * (assuming the height is the same -- come back to this point maybe) * So: // (1) old y/x < Y/X < 1 ==> plot takes up full width // (a) if newplotAspect is wider still (new y/x < old y/x) then it will have to "shrink" (i.e. lose height) // ==> widen newgraphAspect by the same amount?? (minus delta, because that will be constant) // But, since in all cases Y is less than X, `textSize' is based on Y, so should still be correct. // (b) if newplotAspect is less wide (new y/x > old y/x) it will fit fine, so again `textSize' will be fine. // (2) old Y/X < 1, old y/x > Y/X (i.e. old plot is less wide than "wide" graph) // (a) if newplotAspect is wider, then everything is fine UNLESS new y/x ends up Y/X > 1 (i.e. plot takes up full height of "tall" graph) // (a) if newplotAspect is wider, then everything is fine UNLESS new y/x ends up need to widen newgraphAspect (minus delta, because that will be constant) // Then if newgraphAspect is still > 1, we're in case (1)(a) again // BUT if newgraphAspect is now < 1, then we'll need to amend `textSize'. // (b) if newplotAspect is less wide, it will fit fine, so again `textSize' will be fine. // (4) If Y/X > 1 and y/x < Y/X (i.e. plot is less tall than "tall" graph) // (a) if newplotAspect is wider, newgraphAspect will ALWAYS need to be widened to avoid "shrinkage" // Then if newgraphAspect is still > 1, we're in case (1)(a) again // BUT if newgraphAspect is now < 1, then we'll need to amend `textSize'. // (b) if newplotAspect is less wide, it will have to "expand" (i.e. gain height) // ==> *reduce* width of newgraphAspect by the same amount // But, since in all cases X is less than Y, `textSize' is based on X, so should still be correct. * So, scenarios in which to take action are: // (1)(a): increase width of newgraphAspect; // no change to `textSize' // (2)(a): check new y/x: if y/x < Y/X then increase width of newgraphAspect; // no change to `textSize' // (3)(a): check new y/x: if y/x < Y/X then increase width of newgraphAspect; // then check new Y/X: if <1 then need to amend `textSize' // (4)(a): increase width of newgraphAspect; // check new Y/X: if <1 then need to amend `textSize' // (4)(b): reduce width of newgraphAspect; // no change to `textSize' else { local textSize = `oldTextSize' // tidy this up // 1a & 2a if `graphAspect' <= 1 & `plotAspect' <= `graphAspect' { if `xsize'==-9 | `ysize'==-9 { local graphAspect = `graphAspect' * `plotAspect' / `oldPlotAspect' // Modified Nov 2017 if `xsize'==-9 & `ysize'==-9 local xsize = `oldYSize' / `graphAspect' else { if `xsize'==-9 local xsize = `ysize' / `graphAspect' else local ysize = `xsize' * `graphAspect' } } } // 3a, 4a, 4b else if `graphAspect' > 1 & /// ((`oldPlotAspect' > `graphAspect' & `plotAspect' <= `graphAspect') /// | (`oldPlotAspect' <= `graphAspect')) { if `xsize'==-9 | `ysize'==-9 { local oldGraphAspect = `graphAspect' local graphAspect = `oldGraphAspect' * `plotAspect' / `oldPlotAspect' // Modified Nov 2017 if `xsize'==-9 & `ysize'==-9 local xsize = `oldYSize' / `graphAspect' else { if `xsize'==-9 local xsize = `ysize' / `graphAspect' else local ysize = `xsize' * `graphAspect' } // 3a, 4a if `graphAspect' <= 1 { local textSize = `textSize' / `oldGraphAspect' } } } // Added Nov 2017, revised Feb 2018 local fxsize = cond(`fxsize'==-9, 100*(`oldPlotAspect'/`plotAspect')*(`height'/`oldHeight')*(`oldXSize'/`oldYSize'), `fxsize') local fysize = cond(`fysize'==-9, 100*`ydelta'*`height'/`oldYheight', `fysize') } * Notes: for random-effects analyses, sample-size weights, or user-defined (will overwrite the first two) if `"`note2'"'!=`""' { local 0 `"`note2'"' syntax [anything(name=notetxt everything)] [, SIze(string) * ] if "`size'"=="" local size = `textSize' * .75 * `textscale'/100 // use 75% of text size used for rest of plot if "`colsonly'"!="" local notetxt `"" ""' // added Feb 2018 // May 2018: Having parsed the note, now suppress it if noWARNing or noNOTE if `"`warning'`note'"'==`""' local noteopt `"note(`notetxt', size(`size') `options')"' } // collect options relevant to GetAspectRatio which also need ultimately to be passed to -twoway- // N.B. *not* favours; instead returned as `leftfav' and 'rightfav' // [Feb 2018] Also *not* xtitle, as already parsed at beginning of code foreach opt in /*xtitle*/ title subtitle caption { if trim(`"``opt''"')!=`""' { local graphopts `"`graphopts' `opt'(``opt'')"' } } return local graphopts `"`graphopts' `noteopt'"' * Return scalars return scalar xsize = cond(`xsize'==-9, 5.5, `xsize') // added Nov 2017 return scalar ysize = cond(`ysize'==-9, 4, `ysize') // added Nov 2017 return scalar fxsize = `fxsize' return scalar fysize = `fysize' return scalar yheight = `ydelta'*`height' return scalar textsize = `textSize' // textsize as calculated by this routine return scalar textsize2 = `textSize' * `textscale'/100 // textsize as modified post-hoc by textscale() option [May 2020] return scalar spacing = `spacing' return scalar approxchars = `approxChars' return scalar graphaspect = `graphAspect' return scalar plotaspect = `plotAspect' end * GetRows: subroutine of GetAspectRatio // added Jan 2019 program define GetRows, rclass syntax [anything(id="text string")] [, *] local rows = 0 if `"`anything'"'!=`""' { // March 2018 // word count has trouble with apostrophes (but not double-quotes) // so replace them with "a" for the purposes of word-counting /* local rest : subinstr local anything `"'"' `"a"', all gettoken foo bar : rest, qed(q) quotes local rows = cond(`q', `: word count `rest'', 1) */ // Jan 2019: if title() etc. finds "" or `""' at the start, the title is set to nothing if substr(trim(`"`anything'"'), 1, 2)==`""""' | substr(trim(`"`anything'"'), 1, 4)==`"`""'"' { return scalar rows = 0 exit } // Jan 2019: else, remove quotes using gettoken gettoken foo bar : anything, qed(q) quotes local rows = cond(`q', `: word count `anything'', 1) } return scalar rows = `rows' end ********************************************************************************* ** Program to build plot commands for the different elements // from plotopts and plot`p'opts // August 2018: removed "sortpreserve" (since we are adding new obs). // Instead, repect sort order (of `touse' `id') "manually". // (N.B. no further sorting takes place in main routine hereafter.) // August 2018: N.B. unusually, have to pass `touse' as an option here (rather than using marksample) // since we need to have the same tempname appearing in the created plot commands program define BuildPlotCmds, sclass syntax varlist(numeric min=4 max=4 default=none), TOUSE(varname numeric) ID(varname numeric) /// CXLIST(numlist min=2 max=2) /// [PLOTID(varname numeric) DATAID(varname numeric) NEWwt H0(real 0) noNULL /// CLASSIC noDIAmonds INTERaction COLSONLY CUmulative INFluence noOVerall noSUbgroup /// WGT(varname numeric) RFDIST(varlist numeric) BOXscale(real 100.0) noBOX /// DIAMLIST(namelist) OVLIST(namelist) OFFSCLIST(namelist) RFLIST(namelist) TOUSEEXTRA(namelist) * ] // JAN 2020: This subroutine has been rearranged. // GENERAL IDEA: we don't *need* the actual variables in order to build the plot commands; we just to know the variable *names*. // Therefore, we can build the plot commands *first*, and then mess about with the variables themselves afterwards. // This means e.g. we can first identify whether, and where, -expand- is needed for area plots (e.g. diamonds or CI area plots) // and then do this work *later*, whilst accounting for "hidden" pooled observations (needed e.g. for `cumulative' or `influence'). tokenize `varlist' args _USE _ES _LCI _UCI tokenize `cxlist' args CXmin CXmax local _WT `wgt' local awweight `"[aw= `_WT']"' // moved here 30th Jan 2018 if "`box'"!="" local oldbox nobox // allow "global" option `nobox' for compatibility with -metan- // N.B. can't be used with plotid; instead box`p'opts(msymbol(none)) can be used ** Some initial setup summ `plotid' if `touse', meanonly local np = r(max) cap confirm var `dataid' if _rc local nd = 1 else { qui tab `dataid' if `touse' // Nov 2021: changed from "summ `dataid'" as may not be ordinal local nd = r(r) local dataidopt `"& `dataid'==`dataid'[_n-1]"' } ** SETUP OFF-SCALE ARROWS -- fairly straightforward // (include use==3, 5, 7 in case of pciopts/rfopts) tokenize `offsclist' args offscaleL offscaleR qui gen byte `offscaleL' = `touse' * inlist(`_USE', 1, 3, 5, 7) * (float(`_LCI') < float(`CXmin')) qui gen byte `offscaleR' = `touse' * inlist(`_USE', 1, 3, 5, 7) * (float(`_UCI') > float(`CXmax') & !missing(`_UCI')) // rfdist: only applies to use==3, 5, 7 // BUT may need up to four tempvars in niche cases (e.g. only part of the rfCI is visible) // ==> to save on tempvars, only use them if more than one; o/w use local macros if `"`rfdist'"'!=`""' { tokenize `rfdist' args _rfLCI _rfUCI tokenize `rflist' args rfLoffscaleL rfRoffscaleR rfRoffscaleL rfLoffscaleR rfLineLCI rfLineUCI rfLineX local touse3 `"`touse' & inlist(`_USE', 3, 5, 7)"' qui count if `touse3' if r(N) { gen byte `rfLoffscaleL' = `touse3' * (float(`_rfLCI') < float(`CXmin')) gen byte `rfRoffscaleR' = `touse3' * (float(`_rfUCI') > float(`CXmax') & !missing(`_rfUCI')) qui count if `touse3' & float(`_UCI') < float(`CXmin') if r(N) { gen byte `rfRoffscaleL' = `touse3' * (float(`_UCI') < float(`CXmin')) } else { gen byte `rfRoffscaleL' = `touse3' * (float(`_LCI') > float(`CXmax') & !missing(`_LCI')) } qui count if `touse3' & float(`_LCI') > float(`CXmax') & !missing(`_LCI') if r(N) { gen byte `rfLoffscaleR' = `touse3' * (float(`_LCI') > float(`CXmax') & !missing(`_LCI')) } else { gen byte `rfLoffscaleR' = `touse3' * (float(`_UCI') < float(`CXmin')) } } else { local rfLoffscaleL = 0 local rfRoffscaleR = 0 } } ** "OVERALL EFFECT" LINES tokenize `ovlist' args ovLine ovMin ovMax ovLineLCI ovLineUCI ovLineX // `ovLineLCI' `ovLineUCI' `ovLineX' added Jan 2020 qui gen float `ovLine' = . // Construct groups of observations containing a single obs where _USE==3 or 5 // Within each `dataid', such groups ("olinegroup") are identified by _USE==5 if present ("overall"), or _USE==3 otherwise ("subgroup"). qui count if `touse' & inlist(`_USE', 3, 5) if r(N) { tempvar useno qui gen byte `useno' = `_USE' * inlist(`_USE', 3, 5) if `touse' sort `touse' `dataid' `id' qui by `touse' : replace `useno' = `useno'[_n-1] if _n>1 & `useno'<=`useno'[_n-1] `dataidopt' // find the largest value (from 3 & 5) "so far" tempvar olinegroup check qui gen int `olinegroup' = (`_USE'==`useno') * (`useno'>0) qui by `touse' `dataid' : replace `olinegroup' = sum(`olinegroup') if inlist(`_USE', 1, 2, 3, 5) // study obs & pooled results // "check": only draw oline if there are study obs in the same olinegroup qui gen byte `check' = inlist(`_USE', 1, 2) if `touse' qui bysort `touse' `dataid' `olinegroup' (`check') : replace `check' = `check'[_N] sort `touse' `dataid' `olinegroup' `id' // Store values for later plotting // [modified Jan 2020] qui by `touse' `dataid' `olinegroup' : replace `ovLine' = `_ES'[1] if `touse' & `check' & !( `_ES'[1] > `CXmax' | `_ES'[1] < `CXmin') } // "flags" to identify dummy variables for multiple plotids, // and/or where area plots are needed, for later -expand- [added Jan 2020] tokenize `touseextra' args tv0 touseDiam touseOCI touseRFCI qui gen byte `touseDiam' = 2 * `touse' * (`"`cumulative'`influence'"'==`""') // 0 = hide (+ ociline); 1 = pci/ppoint (line); 2 = diamonds (area + no lines; default) qui gen byte `touseOCI' = 0 // 0 = no lines or area (default); 1 = lines; 2 = area (+ lines) if `"`rfdist'"'!=`""' qui gen byte `touseRFCI' = 0 // same ** IF MULTIPLE PLOTIDs, or if dataid(varname, newwt) specified, // create dummy obs with global min & max weights, to maintain correct weighting throughout if (`np' > 1 | `"`newwt'"'!=`""') { // Amended June 2015 // create new `touse', including new dummy obs qui gen byte `tv0' = `touse' local tousePlotID `tv0' // find global min & max weights, to maintain consistency across subgroups if `"`newwt'"'==`""' { // weight consistent across dataid, so just do this once summ `_WT' if `touse' & inlist(`_USE', 1, 2), meanonly local minwt = r(min) local maxwt = r(max) } local oldN = _N local newN = `oldN' + 2*`nd'*`np' // N.B. `nd' indexes `dataid'; `np' indexes `plotid' qui set obs `newN' forvalues i=1/`nd' { forvalues j=1/`np' { // dataid-specific min/max weights required if `"`newwt'"'!=`""' { // weight consistent across dataid, so just use locals summ `_WT' if `touse' & inlist(`_USE', 1, 2) & `dataid'==`i', meanonly local minwt = r(min) local maxwt = r(max) } local k = `oldN' + (`i'-1)*2*`np' + 2*`j' if `"`dataidopt'"'!=`""' { qui replace `dataid' = `i' in `=`k'-1' / `k' } qui replace `plotid' = `j' in `=`k'-1' / `k' qui replace `_WT' = `minwt' in `=`k'-1' qui replace `_WT' = `maxwt' in `k' } } qui replace `_USE' = 1 in `=`oldN' + 1' / `newN' qui replace `touse' = 0 in `=`oldN' + 1' / `newN' qui replace `tousePlotID' = 1 in `=`oldN' + 1' / `newN' } // these dummy obs are identifiable by "`tousePlotID' & !`touse'" else local tousePlotID `touse' // else, no need for separate variable `tousePlotID' ** DEFAULTS * Default options for simple graph elements cap assert `boxscale' >=0 if _rc == 9 { disp as err `"value of {bf:boxscale()} must be >= 0"' exit 125 } else if _rc { disp as err `"error in {bf:boxscale()} option"' exit _rc } local boxSize = `boxscale'/150 local defShape = cond("`interaction'"!="", "circle", "square") local defColor = cond("`classic'"!="", "black", "180 180 180") local defBoxOpts `"mcolor("`defColor'") msymbol(`defShape') msize(`boxSize')"' if `"`oldbox'"'!=`""' local defBoxOpts `"msymbol(none)"' // -metan- "nobox" option local defCIOpts `"lcolor(black) mcolor(black)"' // includes "mcolor" for arrows (doesn't affect rspike/rcap) local defPointOpts `"msymbol(diamond) mcolor(black) msize(vsmall)"' local defOlineOpts `"lwidth(thin) lcolor(maroon) lpattern(shortdash)"' local defOCIlineOpts `"`defOlineOpts'"' // CI of overall effect local defRFCIlineOpts `"`defOlineOpts'"' // CI of predictive interval // ...and for "pooled" estimates local defShape = cond("`interaction'"!="", "circle", "diamond") local defColor "0 0 100" // local defDiamOpts `"lcolor("`defColor'") lalign(center) fcolor("none")"' local defDiamOpts `"lcolor("`defColor'") fcolor("none")"' if "`c(stata_version)'"!="" { if c(stata_version)>=15.1 local defDiamOpts `"`defDiamOpts' lalign(center)"' // v3.0.1: lalign() only valid for Stata 15+ } local defPPointOpts `"msymbol("`defShape'") mlcolor("`defColor'") mfcolor("none")"' // "pooled" point options (alternative to diamond) local defPCIOpts `"lcolor("`defColor'") mcolor("`defColor'")"' // "pooled" CI options (alternative to diamond) local defRFOpts `"`defPCIOpts'"' // prediction interval options (includes "mcolor" for arrows) local defHlineOpts `"lwidth(thin) lcolor(gs12)"' // horizontal upper border line local defNlineOpts `"lwidth(thin) lcolor(black)"' // null line ** Default options for graph elements that may be plotted in more than one way // (plus, may as well parse some other options too, including disallowed ones) local 0 `", `options'"' syntax [, /// /// /* standard options */ BOXOPts(string asis) DIAMOPts(string asis) POINTOPts(string asis) CIOPts(string asis) /// OLINEOPts(string asis) OCILINEOPts(string asis) RFCILINEOPts(string asis) /// HLINEOPts(string asis) NLINEOPts(string asis) /// /// /* non-diamond and prediction interval options */ PPOINTOPts(string asis) PCIOPts(string asis) RFOPts(string asis) * ] local rest `"`options'"' * Overall and Null lines // NOTE NOV 2021: parse to find "global" noOVerlay options // everything else will be parsed later, with plot#opts foreach plot in oline nline { local 0 `", ``plot'opts'"' syntax [, noOVerlay * ] local g_`plot'first : copy local overlay local `plot'opts `"`macval(options)'"' } * Confidence intervals // since capped lines require a different -twoway- command (-rcap- vs -rspike-) if `"`rfdist'"'==`""' & `"`rfopts'"'!=`""' { nois disp as err `"predictive interval not specified; relevant options will be ignored"' local rfopts } // Same routine applies to study CIs, "pooled" CIs (alternative to diamond), and to prediction intervals: foreach plot in ci pci rf { // NOTE NOV 2021: // Currently we have an option "overlay" here for use with rfplotopts only // the default is "nooverlay" meaning that the pred. int. lines extend outwards from extremities of diamond // the option "overlay" instead places a single line passing straight through and over the top of the diamond. // It has been brought to my attention that it may be desirable for confidence interval lines to be *obscured* by weighted boxes // rather than to be seen overlaid on weighted boxes (current default -- so you can see e.g. very short CIs over large boxes) // however, the default behaviour should not be changed due to backwards-compatibility // solution: use *two* options: "OVerlay" for rf; and "noOVerlay" for ci/pci. // However, the new option noOVerlay behaves differently from old option OVerlay2: // - not allowed within the plotid-specific loops later on (because it's currently implemented as a "global" ordering of plot elements with the -twoway- command) // - similarly: it's not really *used* here; it's simply returned as-is (as an extra soption) to be picked up by the main -twoway- command. // options specific to rfplot if `"`plot'"'==`"rf"' local overlay_opt OVerlay SEPLine else local overlay_opt noOVerlay local 0 `", ``plot'opts'"' syntax [, LColor(passthru) MColor(passthru) LWidth(passthru) MLWidth(passthru) /// RCAP `overlay_opt' HORizontal VERTical * ] // disallowed options if `"`horizontal'"'!=`""' | `"`vertical'"'!=`""' { nois disp as err `"suboptions {bf:horizontal} and {bf:vertical} not allowed in option {bf:`plot'opts()}"' exit 198 } /* if `"`overlay'"'!=`""' & "`plot'"!="rf" { nois disp as err `"suboption {bf:overlay} not allowed in option {bf:`plot'opts()}"' exit 198 } */ // rebuild the option list if `"`lcolor'"'!=`""' & `"`mcolor'"'==`""' local mcolor = subinstr(`"`lcolor'"', "l", "m", 1) // for pc(b)arrow if `"`lwidth'"'!=`""' & `"`mlwidth'"'==`""' local mlwidth m`lwidth' // for pc(b)arrow local `plot'opts `"`mcolor' `lcolor' `mlwidth' `lwidth' `options'"' // "overlay" options (see "Note" above) local g_overlay_`plot' : copy local overlay if `"`plot'"'==`"rf"' { local g_sepline_`plot' : copy local sepline if `"`sepline'"'!=`""' local g_overlay_`plot' overlay // -sepline- implies -overlay- } local uplot = upper("`plot'") local `uplot'PlotType = cond("`rcap'"=="", "rspike", "rcap") } * Diamonds // since if truncated (offscale), line options are removed from -rarea- and drawn separately local 0 `", `diamopts'"' syntax [, Color(passthru) LColor(passthru) /// HORizontal VERTical CMISsing(passthru) SORT * ] // disallowed options if `"`horizontal'"'!=`""' | `"`vertical'"'!=`""' { nois disp as err `"suboptions {bf:horizontal} and {bf:vertical} not allowed in option {bf:diamopts()}"' exit 198 } if `"`cmissing'"'!=`""' { nois disp as err `"suboption {bf:cmissing()} not allowed in option {bf:diamopts()}"' exit 198 } if `"`sort'"'!=`""' { nois disp as err `"suboption {bf:sort} not allowed in option {bf:diamopts()}"' exit 198 } // rebuild the option list if `"`color'"'!=`""' & `"`lcolor'"'==`""' local lcolor l`color' // convert `color' -rarea- option to `lcolor' -line- option local diamopts `"`color' `lcolor' `options'"' tokenize `diamlist' args DiamX DiamY1 DiamY2 ** PARSE PLOT#OPTS // Loop over possible values of `plotid' and test for plot#opts relating specifically to each value numlist "1/`np'" local plvals=r(numlist) // need both of these as explicit numlists, local pplvals `plvals' // for later macro manipulations to remove specific values if necessary forvalues p = 1/`np' { local hide local 0 `", `rest'"' syntax [, /// /// /* standard options */ BOX`p'opts(string asis) DIAM`p'opts(string asis) POINT`p'opts(string asis) CI`p'opts(string asis) /// OLINE`p'opts(string asis) OCILINE`p'opts(string asis) RFCILINE`p'opts(string asis) /// /// /* non-diamond and prediction interval options */ PPOINT`p'opts(string asis) PCI`p'opts(string asis) RF`p'opts(string asis) * ] local rest `"`options'"' * Check if any options were found specifically for this value of `p' local checkopt = 0 local optslist box diam point ci oline ociline rfciline ppoint pci rf foreach op of local optslist { if trim(`"``op'`p'opts'"') != `""' local checkopt = 1 } if `checkopt' { local pplvals : list pplvals - p // remove from list of "default" plotids * INDIVIDUAL STUDY MARKERS local touse2 `"`touse' & `_USE'==1 & `plotid'==`p'"' // use local, not tempvar, so conditions are copied into plot commands qui count if `touse2' if r(N) { * WEIGHTED SCATTER PLOT local 0 `", `box`p'opts'"' syntax [, MLABEL(passthru) MSIZe(passthru) * ] // check for disallowed options if `"`mlabel'"' != `""' { nois disp as err `"suboption {bf:mlabel()} not allowed in option {bf:box`p'opts()}"' exit 198 } if `"`msize'"' != `""' { nois disp as err `"suboption {bf:msize()} not allowed in option {bf:box`p'opts()}"' exit 198 } local scPlotOpts `"`defBoxOpts' `boxopts' `box`p'opts'"' summ `_WT' if `touse2', meanonly if !r(N) nois disp as err `"No weights found for {bf:plotid}==`p'"' else if `nd'==1 local scPlot`"`macval(scPlot)' scatter `id' `_ES' `awweight' if `tousePlotID' & `_USE'==1 & `plotid'==`p', `macval(scPlotOpts)' ||"' else { forvalues d=1/`nd' { local scPlot `"`macval(scPlot)' scatter `id' `_ES' `awweight' if `tousePlotID' & `_USE'==1 & `plotid'==`p' & `dataid'==`d', `macval(scPlotOpts)' ||"' } } // N.B. scatter if `tousePlotID' <-- "dummy obs" for consistent weighting * CONFIDENCE INTERVAL PLOT local 0 `", `ci`p'opts'"' syntax [, LColor(passthru) MColor(passthru) LWidth(passthru) MLWidth(passthru) /// RCAP HORizontal VERTical /*Connect(string)*/ * ] // check for disallowed options + rcap // disallowed options if `"`horizontal'"'!=`""' | `"`vertical'"'!=`""' { nois disp as err `"suboptions {bf:horizontal} and {bf:vertical} not allowed in option {bf:ci`p'opts()}"' exit 198 } /* if `"`connect'"'!=`""' { nois disp as err `"suboption {bf:connect()} not allowed in option {bf:ci`p'opts()}"' exit 198 } */ // rebuild option list if `"`lcolor'"'!=`""' & `"`mcolor'"'==`""' local mcolor = subinstr(`"`lcolor'"', "l", "m", 1) // for pc(b)arrow if `"`lwidth'"'!=`""' & `"`mlwidth'"'==`""' local mlwidth m`lwidth' // for pc(b)arrow local CIPlot`p'Opts `"`defCIOpts' `ciopts' `mcolor' `lcolor' `mlwidth' `lwidth' `options'"' // main options first, then options specific to plot `p' local CIPlot`p'Type = cond("`rcap'"=="", "`CIPlotType'", "rcap") // default: both ends within scale (i.e. no arrows) local CIPlot`"`macval(CIPlot)' `CIPlot`p'Type' `_LCI' `_UCI' `id' if `touse2' & !`offscaleL' & !`offscaleR', hor `macval(CIPlot`p'Opts)' ||"' // if arrows required qui count if `touse2' & `offscaleL' & `offscaleR' if r(N) { // both ends off scale local CIPlot `"`macval(CIPlot)' pcbarrow `id' `_LCI' `id' `_UCI' if `touse2' & `offscaleL' & `offscaleR', `macval(CIPlot`p'Opts)' ||"' } qui count if `touse2' & `offscaleL' & !`offscaleR' if r(N) { // only left off scale local CIPlot `"`macval(CIPlot)' pcarrow `id' `_UCI' `id' `_LCI' if `touse2' & `offscaleL' & !`offscaleR', `macval(CIPlot`p'Opts)' ||"' if "`CIPlot`p'Type'" == "rcap" { // add cap to other end if appropriate local CIPlot `"`macval(CIPlot)' rcap `_UCI' `_UCI' `id' if `touse2' & `offscaleL' & !`offscaleR', hor `macval(CIPlot`p'Opts)' ||"' } } qui count if `touse2' & !`offscaleL' & `offscaleR' if r(N) { // only right off scale local CIPlot `"`macval(CIPlot)' pcarrow `id' `_LCI' `id' `_UCI' if `touse2' & !`offscaleL' & `offscaleR', `macval(CIPlot`p'Opts)' ||"' if "`CIPlot`p'Type'" == "rcap" { // add cap to other end if appropriate local CIPlot `"`macval(CIPlot)' rcap `_LCI' `_LCI' `id' if `touse2' & !`offscaleL' & `offscaleR', hor `macval(CIPlot`p'Opts)' ||"' } } * POINT PLOT (point estimates -- except if "classic") if "`classic'" == "" { local pointPlot`"`macval(pointPlot)' scatter `id' `_ES' if `touse2', `defPointOpts' `pointopts' `point`p'opts' ||"' } } // end if r(N) [i.e. if any obs with _USE==1 & plotid==`p'] * OVERALL LINE(S) (if appropriate) summ `ovLine' if `plotid'==`p', meanonly if r(N) { local olinePlot `"`macval(olinePlot)' rspike `ovMin' `ovMax' `ovLine' if `touse' & `plotid'==`p', `defOlineOpts' `olineopts' `oline`p'opts' ||"' * PREDICTIVE INTERVAL CI LINES (and/or areas) // Do these before standard overall lines, in case of area plots // want pred. int. area plot to be underneath the "standard" CI area plot // Added Jan 2020 if trim(`"`rfcilineopts'`rfciline`p'opts'"') != `""' { if `"`rfdist'"'==`""' { nois disp as err `"predictive interval not specified; relevant suboptions for {bf:plotid==`p'} will be ignored"' } else { local 0 `", `rfciline`p'opts'"' syntax [, LINE AREA HIDE HORizontal VERTical Color(passthru) FColor(passthru) FIntensity(passthru) * ] // disallowed options if `"`horizontal'"'!=`""' | `"`vertical'"'!=`""' { nois disp as err `"suboptions {bf:horizontal} and {bf:vertical} not allowed in option {bf:rfciline`p'opts()}"' exit 198 } if `"`hide'"'!=`""' qui replace `touseDiam' = 0 if `plotid'==`p' // August 2023: don't display vertical lines if: (1) area plot requested; and (2) no explicit line options if !(`"`area'`color'`fcolor'`fintensity'"'!=`""' & `"`line'`options'"'==`""') { local olinePlot `"`macval(olinePlot)' rspike `ovMin' `ovMax' `rfLineLCI' if `touse' & `plotid'==`p', `defRFCIlineOpts' `rfcilineopts' `options' ||"' local olinePlot `"`macval(olinePlot)' rspike `ovMin' `ovMax' `rfLineUCI' if `touse' & `plotid'==`p', `defRFCIlineOpts' `rfcilineopts' `options' ||"' } // area plot -- use `touseRFCI' if `"`area'`color'`fcolor'`fintensity'"'!=`""' { local rfciline`p'opts `"`macval(rfciline`p'Opts)' `color' `fcolor' `fintensity' `options'"' local olineAreaPlot `"`macval(olineAreaPlot)' rarea `ovMin' `ovMax' `rfLineX' if `touseRFCI' & `plotid'==`p', `macval(rfciline`p'opts)' lwidth(none) cmissing(n) ||"' qui replace `touseRFCI' = 2 if `plotid'==`p' } else qui replace `touseRFCI' = 1 if `plotid'==`p' } } * OVERALL LINE(S) (and/or areas) // Added Jan 2020 if `"`ocilineopts'`ociline`p'opts'`influence'"'!=`""' { local 0 `", `ociline`p'opts'"' syntax [, LINE AREA HIDE HORizontal VERTical Color(passthru) FColor(passthru) FIntensity(passthru) * ] // disallowed options if `"`horizontal'"'!=`""' | `"`vertical'"'!=`""' { nois disp as err `"suboptions {bf:horizontal} and {bf:vertical} not allowed in option {bf:ociline`p'opts()}"' exit 198 } if `"`hide'"'!=`""' qui replace `touseDiam' = 0 if `plotid'==`p' // August 2023: don't display vertical lines if: (1) area plot requested; and (2) no explicit line options if !(`"`area'`color'`fcolor'`fintensity'"'!=`""' & `"`line'`options'"'==`""') { local olinePlot `"`macval(olinePlot)' rspike `ovMin' `ovMax' `ovLineLCI' if `touse' & `plotid'==`p', `defOCIlineOpts' `ocilineopts' `options' ||"' local olinePlot `"`macval(olinePlot)' rspike `ovMin' `ovMax' `ovLineUCI' if `touse' & `plotid'==`p', `defOCIlineOpts' `ocilineopts' `options' ||"' } // area plot -- use `touseOCI' if "`area'`color'`fcolor'`fintensity'"!=`""' { local ociline`p'opts `"`ocilineopts' `color' `fcolor' `fintensity' `options'"' local olineAreaPlot `"`macval(olineAreaPlot)' rarea `ovMin' `ovMax' `ovLineX' if `touseOCI' & `plotid'==`p', `macval(ociline`p'opts)' lwidth(none) cmissing(n) ||"' qui replace `touseOCI' = 2 if `plotid'==`p' } else qui replace `touseOCI' = 1 if `plotid'==`p' } } // end if r(N) [i.e. if any obs with `ovline' & plotid==`p'] * POOLED EFFECT MARKERS local touse2 `"`touseDiam' & inlist(`_USE', 3, 5) & `plotid'==`p'"' // use local, not tempvar, so conditions are copied into plot commands // N.B. `touseDiam' implies `touse' // local touse3 : copy local touse2 // June 2023: "don't plot data..." -- this is now assured, since that data is _USE==7, not included in touse3 /* if `"`rfdist'"'!=`""' { // Oct 2022: don't plot data from "second" _USE==3|5 row containing pred.int. text // local touse3 `"`touse3' & float(`_rfLCI')!=float(`_LCI') & float(`_rfUCI')!=float(`_UCI')"' local touse3 `"`touse3' & float(`_rfLCI')<=float(`_LCI') & float(`_rfUCI')>=float(`_UCI')"' } */ qui count if `touse2' if r(N) { * DIAMONDS: DRAW POLYGONS WITH -twoway rarea- * Assume diamond if no "pooled point/CI" options and no "interaction" option if trim(`"`ppointopts'`ppoint`p'opts'`pciopts'`pci`p'opts'`interaction'`diamonds'"') == `""' { local 0 `", `diam`p'opts'"' syntax [, Color(passthru) LColor(passthru) /// HORizontal VERTical CMISsing(passthru) SORT * ] // disallowed options if `"`horizontal'"'!=`""' | `"`vertical'"'!=`""' { nois disp as err `"suboptions {bf:horizontal} and {bf:vertical} not allowed in option {bf:diamopts()}"' exit 198 } if `"`cmissing'"'!=`""' { nois disp as err `"suboption {bf:cmissing()} not allowed in option {bf:diamopts()}"' exit 198 } if `"`sort'"'!=`""' { nois disp as err `"suboption {bf:sort} not allowed in option {bf:diamopts()}"' exit 198 } // rebuild option list if `"`color'"'!=`""' & `"`lcolor'"'==`""' local lcolor l`color' // convert `color' -rarea- option to `lcolor' -line- option local diamPlot`p'Opts `"`defDiamOpts' `diamopts' `color' `lcolor' `options'"' // main options first, then options specific to plot `p' // Now check whether any diamonds are offscale (niche case -- see also comments on ppoint below) // If so, will need to draw round the edges of the polygon, excepting the "offscale edges" // and switch off the line options to -twoway rarea- // (draw these lines *after* drawing the area, though, so that the lines appear on top) qui count if `touse2' & (`offscaleL' | `offscaleR') if r(N) { local diam`p'Line `"line `DiamY1' `DiamX' if `touse2', `macval(diamPlot`p'Opts)' cmissing(n) ||"' local diam`p'Line `"`macval(diam`p'Line)' line `DiamY2' `DiamX' if `touse2', `macval(diamPlot`p'Opts)' cmissing(n) ||"' local diam`p'LWidth `"lwidth(none)"' } local diamPlot `"`macval(diamPlot)' rarea `DiamY1' `DiamY2' `DiamX' if `touse2', `macval(diamPlot`p'Opts)' `diam`p'LWidth' cmissing(n) || `diam`p'Line' "' } * POOLED EFFECTS - PPOINT/PCI else { if trim(`"`diam`p'opts'"') != `""' { nois disp as err `"Note: suboptions for both diamond and pooled point/CI specified for {bf:plotid}==`p';"' nois disp as err `" diamond suboptions will be ignored"' } // shouldn't need to bother with arrows etc. here, as pooled effect should always be narrower than individual estimates // but do it anyway, just in case of non-obvious use case local 0 `", `pci`p'opts'"' syntax [, LColor(passthru) MColor(passthru) LWidth(passthru) MLWidth(passthru) /// RCAP HORizontal VERTical /*Connect(string)*/ * ] // check for disallowed options + rcap // disallowed options if `"`horizontal'"'!=`""' | `"`vertical'"'!=`""' { nois disp as err `"suboptions {bf:horizontal} and {bf:vertical} not allowed in option{bf:pci`p'opts()}"' exit 198 } /* if `"`connect'"' != `""' { nois disp as err `"suboption {bf:connect()} not allowed in option {bf:pci`p'opts}"' exit 198 } */ // rebuild option list if `"`lcolor'"'!=`""' & `"`mcolor'"'==`""' local mcolor = subinstr(`"`lcolor'"', "l", "m", 1) // for pc(b)arrow if `"`lwidth'"'!=`""' & `"`mlwidth'"'==`""' local mlwidth m`lwidth' // for pc(b)arrow local PCIPlot`p'Opts `"`defPCIOpts' `pciopts' `mcolor' `lcolor' `mlwidth' `lwidth' `options'"' // main options first, then options specific to plot `p' local PCIPlot`p'Type = cond("`rcap'"=="", "`PCIPlotType'", "rcap") // default: both ends within scale (i.e. no arrows) local PCIPlot `"`macval(PCIPlot)' `PCIPlot`p'Type' `_LCI' `_UCI' `id' if `touse2' & !`offscaleL' & !`offscaleR', hor `macval(PCIPlot`p'Opts)' ||"' // if arrows are required qui count if `touse2' & `offscaleL' & `offscaleR' if r(N) { // both ends off scale local PCIPlot `"`macval(PCIPlot)' pcbarrow `id' `_LCI' `id' `_UCI' if `touse2' & `offscaleL' & `offscaleR', `macval(PCIPlot`p'Opts)' ||"' } qui count if `touse2' & `offscaleL' & !`offscaleR' if r(N) { // only left off scale local PCIPlot `"`macval(PCIPlot)' pcarrow `id' `_UCI' `id' `_LCI' if `touse2' & `offscaleL' & !`offscaleR', `macval(PCIPlot`p'Opts)' ||"' if "`PCIPlot`p'Type'" == "rcap" { // add cap to other end if appropriate local PCIPlot `"`macval(PCIPlot)' rcap `_UCI' `_UCI' `id' if `touse2' & `offscaleL' & !`offscaleR', hor `macval(PCIPlot`p'Opts)' ||"' } } qui count if `touse2' & !`offscaleL' & `offscaleR' if r(N) { // only right off scale local PCIPlot `"`macval(PCIPlot)' pcarrow `id' `_LCI' `id' `_UCI' if `touse2' & !`offscaleL' & `offscaleR', `macval(PCIPlot`p'Opts)' ||"' if "`PCIPlot`p'Type'" == "rcap" { // add cap to other end if appropriate local PCIPlot `"`macval(PCIPlot)' rcap `_LCI' `_LCI' `id' if `touse2' & !`offscaleL' & `offscaleR', hor `macval(PCIPlot`p'Opts)' ||"' } } local ppointPlot `"`macval(ppointPlot)' scatter `id' `_ES' if `touse2', `defPPointOpts' `ppointopts' `ppoint`p'opts' ||"' qui replace `touseDiam' = 1 if `plotid'==`p' // line, not area } * PREDICTION INTERVAL // if trim(`"`ppointopts'`ppoint`p'opts'`pciopts'`pci`p'opts'`interaction'`diamonds'"') == `""' { if trim(`"`rfopts'`rf`p'opts'"') != `""' { if `"`rfdist'"'==`""' { nois disp as err `"predictive interval not specified; relevant suboptions for {bf:plotid==`p'} will be ignored"' } else { local 0 `", `rf`p'opts'"' syntax [, LColor(passthru) MColor(passthru) LWidth(passthru) MLWidth(passthru) /// RCAP HORizontal VERTical /*Connect(string)*/ OVerlay SEPLine * ] // check for disallowed options + rcap, // plus additional options -overlay- and -sepline- if `"`sepline'"'!=`""' local overlay overlay // -sepline- implies -overlay- // disallowed options if `"`horizontal'"'!=`""' | `"`vertical'"'!=`""' { nois disp as err `"suboptions {bf:horizontal} and {bf:vertical} not allowed in option {bf:rf`p'opts}"' exit 198 } /* if `"`connect'"' != `""' { nois disp as err `"suboption {bf:connect()} not allowed in option {bf:rf`p'opts()}"' exit 198 } */ // rebuild option list if `"`lcolor'"'!=`""' & `"`mcolor'"'==`""' local mcolor = subinstr(`"`lcolor'"', "l", "m", 1) // for pc(b)arrow if `"`lwidth'"'!=`""' & `"`mlwidth'"'==`""' local mlwidth m`lwidth' // for pc(b)arrow local RFPlot`p'Opts `"`defRFOpts' `rfopts' `mcolor' `lcolor' `mlwidth' `lwidth' `options'"' // main options first, then options specific to plot `p' local RFPlot`p'Type = cond("`rcap'"=="", "`RFPlotType'", "rcap") // if overlay, use same approach as for CI/PCI if trim(`"`overlay'`g_overlay_rf'"') != `""' { local touse_add `"`touseDiam' & `plotid'==`p' & float(`_rfUCI')>=float(`CXmin') & float(`_rfLCI')<=float(`CXmax')"' // ^^ Note: `touseDiam' & `plotid'==`p' is the previous definition of `touse2' // Oct 2022: option to plot prediction interval as separate line from confidence interval if trim(`"`sepline'`g_sepline_rf'"') != `""' local touse_add `"`touse_add' & `_USE'==7"' else local touse_add `"`touse_add' & inlist(`_USE', 3, 5)"' /* if trim(`"`sepline'`g_sepline_rf'"') != `""' { local touse_add `"`touse_add' & float(`_rfLCI')==float(`_LCI') & float(`_rfUCI')==float(`_UCI')"' } else local touse_add `"`touse_add' & float(`_rfLCI')!=float(`_LCI') & float(`_rfUCI')!=float(`_UCI')"' */ // default: both ends within scale (i.e. no arrows) // local touse3 `"`touse2' & !`rfLoffscaleL' & !`rfRoffscaleR' & `touse_add'"' local touse3 `"`touse_add' & !`rfLoffscaleL' & !`rfRoffscaleR'"' local RFPlot `"`macval(RFPlot)' `RFPlot`p'Type' `_rfLCI' `_rfUCI' `id' if `touse3', hor `macval(RFPlot`p'Opts)' ||"' // if arrows required // local touse3 `"`touse2' & `rfLoffscaleL' & `rfRoffscaleR' & `touse_add'"' local touse3 `"`touse_add' & `rfLoffscaleL' & `rfRoffscaleR'"' qui count if `touse3' if r(N) { // both ends off scale local RFPlot `"`macval(RFPlot)' pcbarrow `id' `_rfLCI' `id' `_rfUCI' if `touse3', `macval(RFPlot`p'Opts)' ||"' } // local touse3 `"`touse2' & `rfLoffscaleL' & !`rfRoffscaleR' & `touse_add'"' local touse3 `"`touse_add' & `rfLoffscaleL' & !`rfRoffscaleR'"' qui count if `touse3' if r(N) { // only left off scale local RFPlot `"`macval(RFPlot)' pcarrow `id' `_rfUCI' `id' `_rfLCI' if `touse3', `macval(RFPlot`p'Opts)' ||"' if "`RFPlotType'" == "rcap" { // add cap to other end if appropriate local RFPlot `"`macval(RFPlot)' rcap `_rfUCI' `_rfUCI' `id' if `touse3', hor `macval(RFPlot`p'Opts)' ||"' } } // local touse3 `"`touse2' & !`rfLoffscaleL' & `rfRoffscaleR' & `touse_add'"' local touse3 `"`touse_add' & !`rfLoffscaleL' & `rfRoffscaleR'"' qui count if `touse3' if r(N) { // only right off scale local RFPlot `"`macval(RFPlot)' pcarrow `id' `_rfLCI' `id' `_rfUCI' if `touse3', `macval(RFPlot`p'Opts)' ||"' if "`RFPlotType'" == "rcap" { // add cap to other end if appropriate local RFPlot `"`macval(RFPlot)' rcap `_rfLCI' `_rfLCI' `id' if `touse3', hor `macval(RFPlot`p'Opts)' ||"' } } } // otherwise, need to do it slightly differently, as we are dealing with two separate (left/right) lines // plus, note that `sepline' is assumed *not* to be relevant in this case; this simplies matters slightly // (because, if we are "not overlaying" around a diamond, then the line must be on the same row as the diamond) else { // June 2023: we can just use `touse2' as is; that is (reminder): // local touse2 `"`touseDiam' & inlist(`_USE', 3, 5) & `plotid'==`p'"' // identify special cases where only one line required, with two arrows local touse3 `"`touse2' & (`rfLoffscaleL' & `rfLoffscaleR') | (`rfRoffscaleL' & `rfRoffscaleR')"' qui count if `touse3' if r(N) { local RFPlot `"`macval(RFPlot)' pcbarrow `id' `_rfLCI' `id' `_rfUCI' if `touse3', `macval(RFPlot`p'Opts)' ||"' } // left-hand line local touse_add `"float(`_rfLCI')<=float(`CXmax') & float(`_rfLCI')!=float(`_LCI')"' local touse3 `"`touse2' & !`rfLoffscaleL' & !`rfLoffscaleR' & !`offscaleL' & `touse_add'"' local RFPlot `"`macval(RFPlot)' `RFPlot`p'Type' `_LCI' `_rfLCI' `id' if `touse3', hor `macval(RFPlot`p'Opts)' ||"' local touse3 `"`touse2' & `rfLoffscaleL' & !`rfLoffscaleR' & !`offscaleL' & `touse_add'"' qui count if `touse3' if r(N) { // left-hand end off scale local RFPlot `"`macval(RFPlot)' pcarrow `id' `_LCI' `id' `_rfLCI' if `touse3', `macval(RFPlot`p'Opts)' ||"' } local touse3 `"`touse2' & !`rfLoffscaleL' & `rfLoffscaleR' & !`offscaleL' & `touse_add'"' qui count if `touse3' if r(N) { // right-hand end off scale local RFPlot `"`macval(RFPlot)' pcarrow `id' `_rfLCI' `id' `_LCI' if `touse3', `macval(RFPlot`p'Opts)' ||"' } // right-hand line local touse_add `"float(`_rfUCI')>=float(`CXmin') & float(`_rfUCI')!=float(`_UCI')"' local touse3 `"`touse2' & !`rfRoffscaleL' & !`rfRoffscaleR' & !`offscaleR' & `touse_add'"' local RFPlot `"`macval(RFPlot)' `RFPlot`p'Type' `_UCI' `_rfUCI' `id' if `touse3', hor `macval(RFPlot`p'Opts)' ||"' local touse3 `"`touse2' & `rfRoffscaleL' & !`rfRoffscaleR' & !`offscaleR' & `touse_add'"' qui count if `touse3' if r(N) { // left-hand end off scale local RFPlot `"`macval(RFPlot)' pcarrow `id' `_rfUCI' `id' `_UCI' if `touse3', `macval(RFPlot`p'Opts)' ||"' } local touse3 `"`touse2' & !`rfRoffscaleL' & `rfRoffscaleR' & !`offscaleR' & `touse_add'"' qui count if `touse3' if r(N) { // right-hand end off scale local RFPlot `"`macval(RFPlot)' pcarrow `id' `_UCI' `id' `_rfUCI' if `touse3', `macval(RFPlot`p'Opts)' ||"' } } } // end else [i.e. if rfdist] } // if trim(`"`rf`p'opts'"')!=`""' } // end if r(N) [i.e. if any obs with _USE==3,5 & plotid==`p'] } // end if trim(`"`box`p'opts'`diam`p'opts'`point`p'opts'`ci`p'opts'`oline`p'opts'`ppoint`p'opts'`pci`p'opts'"') != `""' } // end forvalues p = 1/`np' * Find invalid/repeated options // any such options would generate a suitable error message at the plotting stage // so just exit here with error, to save the user's time if regexm(`"`rest'"', "(box|diam|point|ci|oline|ociline|ppoint|pci|rf|rfciline)([0-9]+)opt") { local badopt = regexs(1) local badp = regexs(2) if `: list badp in plvals' nois disp as err `"option {bf:`badopt'`badp'opts} supplied multiple times; should only be supplied once"' else nois disp as err `"`badp' is not a valid {bf:plotid} value"' exit 198 } local opts_rest : copy local rest // sreturn local options `"`rest'"' // This is now *just* the standard "twoway" options // i.e. the specialist "forestplot" options have been filtered out * FORM "DEFAULT" TWOWAY PLOT COMMAND (if appropriate) // Changed so that FOR WEIGHTED SCATTER each pplval is plotted separately (otherwise weights get messed up) // Other (nonweighted) plots can continue to be plotted as before if `"`pplvals'"'!=`""' { local pplvals2 : copy local pplvals // copy; only needed for weighted scatter plots local pplvals : subinstr local pplvals " " ",", all // so that "inlist" may be used local hide * INDIVIDUAL STUDY MARKERS local touse2 `"`touse' & `_USE'==1 & inlist(`plotid', `pplvals')"' // use local, not tempvar, so conditions are copied into plot commands qui count if `touse2' if r(N) { * WEIGHTED SCATTER PLOT local 0 `", `boxopts'"' syntax [, MLABEL(passthru) MSIZe(passthru) * ] // check for disallowed options if `"`mlabel'"' != `""' { disp as err "boxopts: option mlabel() not allowed" exit 198 } if `"`msize'"' != `""' { disp as err "boxopts: option msize() not allowed" exit 198 } local scPlotOpts `"`defBoxOpts' `boxopts'"' if `"`pplvals'"'==`"`plvals'"' { // if no plot#opts specified, can plot all plotid groups at once summ `_WT' if `touse2', meanonly if r(N) { if `nd'==1 local scPlot `"`macval(scPlot)' scatter `id' `_ES' `awweight' if `tousePlotID' & `_USE'==1 & inlist(`plotid', `pplvals'), `macval(scPlotOpts)' ||"' else { forvalues d=1/`nd' { local scPlot `"`macval(scPlot)' scatter `id' `_ES' `awweight' if `tousePlotID' & `_USE'==1 & inlist(`plotid', `pplvals') & `dataid'==`d', `macval(scPlotOpts)' ||"' } } } } else { // else, need to plot each group separately to maintain correct weighting (July 2014) foreach p of local pplvals2 { summ `_WT' if `touse' & `_USE'==1 & `plotid'==`p', meanonly if r(N) { if `nd'==1 local scPlot `"`macval(scPlot)' scatter `id' `_ES' `awweight' if `tousePlotID' & `_USE'==1 & `plotid'==`p', `macval(scPlotOpts)' ||"' else { forvalues d=1/`nd' { local scPlot `"`macval(scPlot)' scatter `id' `_ES' `awweight' if `tousePlotID' & `_USE'==1 & `plotid'==`p' & `dataid'==`d', `macval(scPlotOpts)' ||"' } } } } } // N.B. scatter if `tousePlotID' <-- "dummy obs" for consistent weighting * CONFIDENCE INTERVAL PLOT // N.B. options already processed local CIPlotOpts `"`defCIOpts' `ciopts'"' // default: both ends within scale (i.e. no arrows) local CIPlot `"`macval(CIPlot)' `CIPlotType' `_LCI' `_UCI' `id' if `touse2' & !`offscaleL' & !`offscaleR', hor `macval(CIPlotOpts)' ||"' // if arrows required qui count if `touse2' & `offscaleL' & `offscaleR' if r(N) { // both ends off scale local CIPlot `"`macval(CIPlot)' pcbarrow `id' `_LCI' `id' `_UCI' if `touse2' & `offscaleL' & `offscaleR', `macval(CIPlotOpts)' ||"' } qui count if `touse2' & `offscaleL' & !`offscaleR' if r(N) { // only left off scale local CIPlot `"`macval(CIPlot)' pcarrow `id' `_UCI' `id' `_LCI' if `touse2' & `offscaleL' & !`offscaleR', `macval(CIPlotOpts)' ||"' if "`CIPlotType'" == "rcap" { // add cap to other end if appropriate local CIPlot `"`macval(CIPlot)' rcap `_UCI' `_UCI' `id' if `touse2' & `offscaleL' & !`offscaleR', hor `macval(CIPlotOpts)' ||"' } } qui count if `touse2' & !`offscaleL' & `offscaleR' if r(N) { // only right off scale local CIPlot `"`macval(CIPlot)' pcarrow `id' `_LCI' `id' `_UCI' if `touse2' & !`offscaleL' & `offscaleR', `macval(CIPlotOpts)' ||"' if "`CIPlotType'" == "rcap" { // add cap to other end if appropriate local CIPlot `"`macval(CIPlot)' rcap `_LCI' `_LCI' `id' if `touse2' & !`offscaleL' & `offscaleR', hor `macval(CIPlotOpts)' ||"' } } * POINT PLOT (point estimates -- except if "classic") if "`classic'" == "" { local pointPlot `"`macval(pointPlot)' scatter `id' `_ES' if `touse2', `defPointOpts' `pointopts' ||"' } } // end if r(N) [i.e. if any obs with _USE==1 & plotid==`ppvals'] * OVERALL LINE(S) (if appropriate) summ `ovLine' if inlist(`plotid', `pplvals'), meanonly if r(N) { local olinePlot `"`macval(olinePlot)' rspike `ovMin' `ovMax' `ovLine' if `touse' & inlist(`plotid', `pplvals'), `defOlineOpts' `olineopts' ||"' * PREDICTIVE INTERVAL CI LINES (and/or areas) // Do these before standard overall lines, in case of area plots // want pred. int. area plot to be underneath the "standard" CI area plot // Added Jan 2020 if trim(`"`rfcilineopts'"') != `""' { if `"`rfdist'"'==`""' { nois disp as err `"predictive interval not specified; relevant suboptions will be ignored"' } else { local 0 `", `rfcilineopts'"' syntax [, LINE AREA HIDE HORizontal VERTical Color(passthru) FColor(passthru) FIntensity(passthru) * ] // disallowed options if `"`horizontal'"'!=`""' | `"`vertical'"'!=`""' { nois disp as err `"suboptions {bf:horizontal} and {bf:vertical} not allowed in option {bf:rfcilineopts()}"' exit 198 } if `"`hide'"'!=`""' qui replace `touseDiam' = 0 if inlist(`plotid', `pplvals') // August 2023: don't display vertical lines if: (1) area plot requested; and (2) no explicit line options if !(`"`area'`color'`fcolor'`fintensity'"'!=`""' & `"`line'`options'"'==`""') { local olinePlot `"`macval(olinePlot)' rspike `ovMin' `ovMax' `rfLineLCI' if `touse' & inlist(`plotid', `pplvals'), `defRFCIlineOpts' `options' ||"' local olinePlot `"`macval(olinePlot)' rspike `ovMin' `ovMax' `rfLineUCI' if `touse' & inlist(`plotid', `pplvals'), `defRFCIlineOpts' `options' ||"' } // area plot -- use `touseRFCI' if `"`area'`color'`fcolor'`fintensity'"'!=`""' { local rfcilineopts `"`color' `fcolor' `fintensity' `options'"' local olineAreaPlot `"`macval(olineAreaPlot)' rarea `ovMin' `ovMax' `rfLineX' if `touseRFCI' & inlist(`plotid', `pplvals'), `rfcilineopts' lwidth(none) cmissing(n) ||"' qui replace `touseRFCI' = 2 if inlist(`plotid', `pplvals') } else qui replace `touseRFCI' = 1 if inlist(`plotid', `pplvals') } } * OVERALL CI LINES (and/or areas) // Added Jan 2020 if `"`ocilineopts'`influence'"'!=`""' { local 0 `", `ocilineopts'"' syntax [, LINE AREA HIDE HORizontal VERTical Color(passthru) FColor(passthru) FIntensity(passthru) * ] // disallowed options if `"`horizontal'"'!=`""' | `"`vertical'"'!=`""' { nois disp as err `"suboptions {bf:horizontal} and {bf:vertical} not allowed in option {bf:ociline`p'opts()}"' exit 198 } if `"`hide'"'!=`""' qui replace `touseDiam' = 0 if inlist(`plotid', `pplvals') // August 2023: don't display vertical lines if: (1) area plot requested; and (2) no explicit line options if !(`"`area'`color'`fcolor'`fintensity'"'!=`""' & `"`line'`options'"'==`""') { local olinePlot `"`macval(olinePlot)' rspike `ovMin' `ovMax' `ovLineLCI' if `touse' & inlist(`plotid', `pplvals'), `defOCIlineOpts' `options' ||"' local olinePlot `"`macval(olinePlot)' rspike `ovMin' `ovMax' `ovLineUCI' if `touse' & inlist(`plotid', `pplvals'), `defOCIlineOpts' `options' ||"' } // area plot -- use `touseOCI' if `"`area'`color'`fcolor'`fintensity'"'!=`""' { local ocilineopts `"`color' `fcolor' `fintensity' `options'"' local olineAreaPlot `"`macval(olineAreaPlot)' rarea `ovMin' `ovMax' `ovLineX' if `touseOCI' & inlist(`plotid', `pplvals'), `macval(ocilineopts)' lwidth(none) cmissing(n) ||"' qui replace `touseOCI' = 2 if inlist(`plotid', `pplvals') } else qui replace `touseOCI' = 1 if inlist(`plotid', `pplvals') } } // end if r(N) [i.e. if any obs with `ovline' & inlist(plotid, `pplvals')] * POOLED EFFECT MARKERS local touse2 `"`touseDiam' & inlist(`_USE', 3, 5) & inlist(`plotid', `pplvals')"' // use local, not tempvar, so conditions are copied into plot commands // local touse3 : copy local touse2 // nois list `_USE' `_ES' `_LCI' `_UCI' `_rfLCI' `_rfUCI' _EFFECT if `touse3' // June 2023: "don't plot data..." -- this is now assured, since that data is _USE==7, not included in touse3 /* if `"`rfdist'"'!=`""' { // Oct 2022: don't plot data from "second" _USE==3|5 row containing pred.int. text // local touse3 `"`touse3' & float(`_rfLCI')!=float(`_LCI') & float(`_rfUCI')!=float(`_UCI')"' local touse3 `"`touse3' & float(`_rfLCI')<=float(`_LCI') & float(`_rfUCI')>=float(`_UCI')"' } */ qui count if `touse2' if r(N) { * DIAMONDS - DRAW POLYGONS WITH -twoway rarea- * Assume diamond if no "pooled point/CI" options and no "interaction" option if trim(`"`ppointopts'`pciopts'`interaction'`diamonds'"') == `""' { local diamPlotOpts `"`defDiamOpts' `diamopts'"' // Now check whether any diamonds are offscale (niche case!) // If so, will need to draw round the edges of the polygon, excepting the "offscale edges" // and switch off the line options to -twoway rarea- // (draw these lines *after* drawing the area, though, so that the lines appear on top) qui count if `touse2' & (`offscaleL' | `offscaleR') if r(N) { local diamLine `"line `DiamY1' `DiamX' if `touse2', `macval(diamPlotOpts)' cmissing(n) ||"' local diamLine `"`macval(diamLine)' line `DiamY2' `DiamX' if `touse2', `macval(diamPlotOpts)' cmissing(n) ||"' local diamLWidth `"lwidth(none)"' } local diamPlot `"`macval(diamPlot)' rarea `DiamY1' `DiamY2' `DiamX' if `touse2', `macval(diamPlotOpts)' `diamLWidth' cmissing(n) || `diamLine' "' } * POOLED EFFECT - PPOINT/PCI else { if trim(`"`diamopts'"') != `""' { nois disp as err `"Note: suboptions for both diamond and pooled point/CI specified;"' nois disp as err `" diamond suboptions will be ignored"' } // N.B. options already processed local PCIPlotOpts `"`defPCIOpts' `pciopts'"' // default: both ends within scale (i.e. no arrows) local PCIPlot `"`macval(PCIPlot)' `PCIPlotType' `_LCI' `_UCI' `id' if `touse2' & !`offscaleL' & !`offscaleR', hor `macval(PCIPlotOpts)' ||"' // if arrows are required qui count if `touse2' & `offscaleL' & `offscaleR' if r(N) { // both ends off scale local PCIPlot `"`macval(PCIPlot)' pcbarrow `id' `_LCI' `id' `_UCI' if `touse2' & `offscaleL' & `offscaleR', `macval(PCIPlotOpts)' ||"' } qui count if `touse2' & `offscaleL' & !`offscaleR' if r(N) { // only left off scale local PCIPlot `"`macval(PCIPlot)' pcarrow `id' `_UCI' `id' `_LCI' if `touse2' & `offscaleL' & !`offscaleR', `macval(PCIPlotOpts)' ||"' if "`PCIPlotType'" == "rcap" { // add cap to other end if appropriate local PCIPlot `"`macval(PCIPlot)' rcap `_UCI' `_UCI' `id' if `touse2' & `offscaleL' & !`offscaleR', hor `macval(PCIPlotOpts)' ||"' } } qui count if `touse2' & !`offscaleL' & `offscaleR' if r(N) { // only right off scale local PCIPlot `"`macval(PCIPlot)' pcarrow `id' `_LCI' `id' `_UCI' if `touse2' & !`offscaleL' & `offscaleR', `macval(PCIPlotOpts)' ||"' if "`PCIPlotType'" == "rcap" { // add cap to other end if appropriate local PCIPlot `"`macval(PCIPlot)' rcap `_LCI' `_LCI' `id' if `touse2' & !`offscaleL' & `offscaleR', hor `macval(PCIPlotOpts)' ||"' } } local ppointPlot `"`macval(ppointPlot)' scatter `id' `_ES' if `touse2', `defPPointOpts' `ppointopts' ||"' qui replace `touseDiam' = 1 if inlist(`plotid', `pplvals') // line, not area } * PREDICTION INTERVAL if `"`rfdist'"'==`""' { if trim(`"`rfopts'"') != `""' { nois disp as err `"predictive interval not specified; relevant suboptions will be ignored"' } } else { // N.B. options already processed local RFPlotOpts `"`defRFOpts' `rfopts'"' // if overlay, use same approach as for CI/PCI if `"`g_overlay_rf'"'!=`""' { // local touse_add `"float(`_rfUCI')>=float(`CXmin') & float(`_rfLCI')<=float(`CXmax')"' local touse_add `"`touseDiam' & inlist(`plotid', `pplvals') & float(`_rfUCI')>=float(`CXmin') & float(`_rfLCI')<=float(`CXmax')"' // ^^ Note: `touseDiam' & inlist(`plotid', `pplvals') is the previous definition of `touse2' // Oct 2022: option to plot prediction interval as separate line from confidence interval // if `"`g_sepline_rf'"'!=`""' local touse_add `"`touse_add' & float(`_rfLCI')==float(`_LCI') & float(`_rfUCI')==float(`_UCI')"' // else local touse_add `"`touse_add' & float(`_rfLCI')!=float(`_LCI') & float(`_rfUCI')!=float(`_UCI')"' if `"`g_sepline_rf'"'!=`""' local touse_add `"`touse_add' & `_USE'==7"' else local touse_add `"`touse_add' & inlist(`_USE', 3, 5)"' // default: both ends within scale (i.e. no arrows) // local touse3 `"`touse2' & !`rfLoffscaleL' & !`rfRoffscaleR' & `touse_add'"' local touse3 `"`touse_add' & !`rfLoffscaleL' & !`rfRoffscaleR'"' local RFPlot `"`macval(RFPlot)' `RFPlotType' `_rfLCI' `_rfUCI' `id' if `touse3', hor `macval(RFPlotOpts)' ||"' // if arrows required // local touse3 `"`touse2' & `rfLoffscaleL' & `rfRoffscaleR' & `touse_add'"' local touse3 `"`touse_add' & `rfLoffscaleL' & `rfRoffscaleR'"' qui count if `touse3' if r(N) { // both ends off scale local RFPlot `"`macval(RFPlot)' pcbarrow `id' `_rfLCI' `id' `_rfUCI' if `touse3', `macval(RFPlotOpts)' ||"' } // local touse3 `"`touse2' & `rfLoffscaleL' & !`rfRoffscaleR' & `touse_add'"' local touse3 `"`touse_add' & `rfLoffscaleL' & !`rfRoffscaleR'"' qui count if `touse3' if r(N) { // only left off scale local RFPlot `"`macval(RFPlot)' pcarrow `id' `_rfUCI' `id' `_rfLCI' if `touse3', `macval(RFPlotOpts)' ||"' if "`RFPlotType'" == "rcap" { // add cap to other end if appropriate local RFPlot `"`macval(RFPlot)' rcap `_rfUCI' `_rfUCI' `id' if `touse3', hor `macval(RFPlotOpts)' ||"' } } // local touse3 `"`touse2' & !`rfLoffscaleL' & `rfRoffscaleR' & `touse_add'"' local touse3 `"`touse_add' & !`rfLoffscaleL' & `rfRoffscaleR'"' qui count if `touse3' if r(N) { // only right off scale local RFPlot `"`macval(RFPlot)' pcarrow `id' `_rfLCI' `id' `_rfUCI' if `touse3', `macval(RFPlotOpts)' ||"' if "`RFPlotType'" == "rcap" { // add cap to other end if appropriate local RFPlot `"`macval(RFPlot)' rcap `_rfLCI' `_rfLCI' `id' if `touse3', hor `macval(RFPlotOpts)' ||"' } } } // otherwise, need to do it slightly differently, as we are dealing with two separate (left/right) lines // plus, note that `sepline' is assumed *not* to be relevant in this case; this simplies matters slightly // (because, if we are "not overlaying" around a diamond, then the line must be on the same row as the diamond) else { // June 2023: we can just use `touse2' as is; that is (reminder): // local touse2 `"`touseDiam' & inlist(`_USE', 3, 5) & inlist(`plotid', `pplvals')"' // identify special cases where only one line required, with two arrows local touse3 `"`touse2' & (`rfLoffscaleL' & `rfLoffscaleR') | (`rfRoffscaleL' & `rfRoffscaleR')"' qui count if `touse3' if r(N) { local RFPlot `"`macval(RFPlot)' pcbarrow `id' `_rfLCI' `id' `_rfUCI' if `touse3', `macval(RFPlotOpts)' ||"' } // left-hand line local touse_add `"float(`_rfLCI')<=float(`CXmax') & float(`_rfLCI')!=float(`_LCI')"' local touse3 `"`touse2' & !`rfLoffscaleL' & !`rfLoffscaleR' & !`offscaleL' & `touse_add'"' local RFPlot `"`macval(RFPlot)' `RFPlotType' `_LCI' `_rfLCI' `id' if `touse3', hor `macval(RFPlotOpts)' ||"' local touse3 `"`touse2' & `rfLoffscaleL' & !`rfLoffscaleR' & !`offscaleL' & `touse_add'"' qui count if `touse3' if r(N) { // left-hand end off scale local RFPlot `"`macval(RFPlot)' pcarrow `id' `_LCI' `id' `_rfLCI' if `touse3', `macval(RFPlotOpts)' ||"' } local touse3 `"`touse2' & !`rfLoffscaleL' & `rfLoffscaleR' & !`offscaleL' & `touse_add'"' qui count if `touse3' if r(N) { // right-hand end off scale local RFPlot `"`macval(RFPlot)' pcarrow `id' `_rfLCI' `id' `_LCI' if `touse3', `macval(RFPlotOpts)' ||"' } // right-hand line local touse_add `"float(`_rfUCI')>=float(`CXmin') & float(`_rfUCI')!=float(`_UCI')"' local touse3 `"`touse2' & !`rfRoffscaleL' & !`rfRoffscaleR' & !`offscaleR' & `touse_add'"' local RFPlot `"`macval(RFPlot)' `RFPlotType' `_UCI' `_rfUCI' `id' if `touse3', hor `macval(RFPlotOpts)' ||"' local touse3 `"`touse2' & `rfRoffscaleL' & !`rfRoffscaleR' & !`offscaleR' & `touse_add'"' qui count if `touse3' if r(N) { // left-hand end off scale local RFPlot `"`macval(RFPlot)' pcarrow `id' `_rfUCI' `id' `_UCI' if `touse3', `macval(RFPlotOpts)' ||"' } local touse3 `"`touse2' & !`rfRoffscaleL' & `rfRoffscaleR' & !`offscaleR' & `touse_add'"' qui count if `touse3' if r(N) { // right-hand end off scale local RFPlot `"`macval(RFPlot)' pcarrow `id' `_UCI' `id' `_rfUCI' if `touse3', `macval(RFPlotOpts)' ||"' } } } // end if `"`rfdist'"'!=`""' } // end if r(N) [i.e. if any obs with _USE==3,5 & plotid==`ppvals'] } // end if `"`pplvals'"'!=`""' // END GRAPH OPTS * If necessary, finish off storing values for later plotting // added Jan 2020 qui count if `touse' & `touseOCI' if r(N) { sort `touse' `dataid' `olinegroup' `id' qui by `touse' `dataid' `olinegroup' : gen `ovLineLCI' = `_LCI'[1] if `touse' & `check' & !(`_LCI'[1] > `CXmax' | `_LCI'[1] < `CXmin') qui by `touse' `dataid' `olinegroup' : gen `ovLineUCI' = `_UCI'[1] if `touse' & `check' & !(`_UCI'[1] > `CXmax' | `_UCI'[1] < `CXmin') } if `"`rfdist'"'!=`""' { qui count if `touse' & `touseRFCI' if r(N) { sort `touse' `dataid' `olinegroup' `id' qui by `touse' `dataid' `olinegroup' : gen `rfLineLCI' = `_rfLCI'[1] if `touse' & `check' & !(`_rfLCI'[1] > `CXmax' | `_rfLCI'[1] < `CXmin') qui by `touse' `dataid' `olinegroup' : gen `rfLineUCI' = `_rfUCI'[1] if `touse' & `check' & !(`_rfUCI'[1] > `CXmax' | `_rfUCI'[1] < `CXmin') } } // Now, having completed parsing graph opts, reset `touse' and `id' in case of any changes (e.g. "hidden" pooled obs) tempvar touse_id35 qui gen byte `touse_id35' = `touse' * inlist(`_USE', 3, 5, 7) * !`touseDiam' // `hide' // `id' : identify obs with `_USE'==3 or 5, and subtract 1 from obs above qui count if `touse_id35' if r(N) { tempvar id35 qui bysort `touse' (`id') : gen long `id35' = sum(`touse_id35') qui replace `id' = `id' - `id35' if `touse' qui replace `touse' = 0 if `touse_id35' } // Having done this, limit the ovline variables to just the first observation within each olinegroup // (to prevent multiple overlapping lines from being drawn) sort `touse' `dataid' `olinegroup' `id' qui by `touse' `dataid' `olinegroup' : replace `ovLine' = . if _n > 1 qui by `touse' `dataid' `olinegroup' : gen `ovMin' = `id'[1] - 0.5 if `touse' & _n==1 & !missing(`ovLine') qui by `touse' `dataid' `olinegroup' : gen `ovMax' = `id'[_N] + 0.5 if `touse' & _n==1 & !missing(`ovLine') qui count if `touse' & `touseOCI' if r(N) { qui by `touse' `dataid' `olinegroup' : replace `ovLineLCI' = . if _n > 1 qui by `touse' `dataid' `olinegroup' : replace `ovLineUCI' = . if _n > 1 } if `"`rfdist'"'!=`""' { qui count if `touse' & `touseRFCI' if r(N) { qui by `touse' `dataid' `olinegroup' : replace `rfLineLCI' = . if _n > 1 qui by `touse' `dataid' `olinegroup' : replace `rfLineLCI' = . if _n > 1 } } *** AREA PLOTS // Jan 2020: consider all these together, so that their sort orders don't cause conflicts // August 2018 // DRAW DIAMONDS AS POLYGONS USING -twoway rarea- // SO THAT THEY MAY BE FILLED IN (also requires fewer variables) qui count if `touse' & inlist(`_USE', 3, 5) & `touseDiam'==2 // 2 = area (default) if !r(N) qui replace `touseDiam' = 1 if `touseDiam'>1 // to avoid error if no obs with _USE=3 or 5 else { qui expand 4 if `touseDiam'==2 & inlist(`_USE', 3, 5) qui bysort `touse' `id' : replace `touseDiam' = (`touseDiam'>0) * _n qui replace `touseDiam' = 1 if `touseDiam'>1 & !inlist(`_USE', 3, 5) // x-coords qui gen float `DiamX' = cond(`offscaleL', `CXmin', `_LCI') if `touseDiam'==1 & float(`_ES') >= float(`CXmin') qui replace `DiamX' = `_ES' if `touseDiam'==2 qui replace `DiamX' = `CXmin' if `touseDiam'==2 & float(`_ES') < `CXmin' qui replace `DiamX' = `CXmax' if `touseDiam'==2 & float(`_ES') > `CXmax' qui replace `DiamX' = . if `touseDiam'==2 & (float(`_UCI') < `CXmin' | float(`_LCI') > `CXmax') qui replace `DiamX' = cond(`offscaleR', `CXmax', `_UCI') if `touseDiam'==3 & float(`_ES') <= float(`CXmax') qui replace `DiamX' = . if `touseDiam'==4 // upper y-coords qui gen float `DiamY1' = cond(`offscaleL', `id' + 0.4*( abs((`CXmin'-`_LCI')/(`_ES'-`_LCI')) ), `id') if `touseDiam'==1 & float(`_ES') >= float(`CXmin') qui replace `DiamY1' = `id' + 0.4 if `touseDiam'==2 qui replace `DiamY1' = `id' + 0.4*( abs((`_UCI'-`CXmin')/(`_UCI'-`_ES')) ) if `touseDiam'==2 & float(`_ES') < float(`CXmin') qui replace `DiamY1' = `id' + 0.4*( abs((`CXmax'-`_LCI')/(`_ES'-`_LCI')) ) if `touseDiam'==2 & float(`_ES') > float(`CXmax') qui replace `DiamY1' = cond(`offscaleR', `id' + 0.4*( abs((`_UCI'-`CXmax')/(`_UCI'-`_ES')) ), `id') if `touseDiam'==3 & float(`_ES') <= float(`CXmax') qui replace `DiamY1' = . if `touseDiam'==4 // lower y-coords qui gen float `DiamY2' = cond(`offscaleL', `id' - 0.4*( abs((`CXmin'-`_LCI')/(`_ES'-`_LCI')) ), `id') if `touseDiam'==1 & float(`_ES') >= float(`CXmin') qui replace `DiamY2' = `id' - 0.4 if `touseDiam'==2 qui replace `DiamY2' = `id' - 0.4*( abs((`_UCI'-`CXmin')/(`_UCI'-`_ES')) ) if `touseDiam'==2 & float(`_ES') < float(`CXmin') qui replace `DiamY2' = `id' - 0.4*( abs((`CXmax'-`_LCI')/(`_ES'-`_LCI')) ) if `touseDiam'==2 & float(`_ES') > float(`CXmax') qui replace `DiamY2' = cond(`offscaleR', `id' - 0.4*( abs((`_UCI'-`CXmax')/(`_UCI'-`_ES')) ), `id') if `touseDiam'==3 & float(`_ES') <= float(`CXmax') qui replace `DiamY2' = . if `touseDiam'==4 } // OCILine area plots qui count if `touse' & `touseOCI'==2 // 2 = area if r(N) { qui count if `touse' & `touseOCI'==2 & !missing(`ovLineLCI', `ovLineUCI') if r(N) { qui expand 3 if `touse' & `touseOCI'==2 & !missing(`ovLineLCI', `ovLineUCI') qui bysort `touse' `id' (`touseDiam') : replace `touseOCI' = (`touseOCI'>0) * _n qui gen float `ovLineX' = cond(`offscaleL', `CXmin', `ovLineLCI') if `touseOCI'==1 & float(`_ES') >= float(`CXmin') qui replace `ovLineX' = cond(`offscaleR', `CXmax', `ovLineUCI') if `touseOCI'==2 & float(`_ES') <= float(`CXmax') qui replace `ovLineX' = . if `touseOCI'==3 } } // RFCILine area plots if `"`rfdist'"'!=`""' { qui count if `touse' & `touseRFCI'==2 // 2 = area if r(N) { qui count if `touse' & `touseRFCI'==2 & !missing(`rfLineLCI', `rfLineUCI') if r(N) { qui expand 3 if `touse' & `touseRFCI'==2 & !missing(`rfLineLCI', `rfLineUCI') qui bysort `touse' `id' (`touseDiam' `touseOCI') : replace `touseRFCI' = (`touseRFCI'>0) * _n qui gen float `rfLineX' = cond(`rfLoffscaleL', `CXmin', `rfLineLCI') if `touseRFCI'==1 & float(`_ES') >= float(`CXmin') qui replace `rfLineX' = cond(`rfRoffscaleR', `CXmax', `rfLineUCI') if `touseRFCI'==2 & float(`_ES') <= float(`CXmax') qui replace `rfLineX' = . if `touseRFCI'==3 } } } qui replace `touse' = 0 if `touseDiam' > 1 | `touseOCI' > 1 if `"`rfdist'"'!=`""' { qui replace `touse' = 0 if `touseRFCI' > 1 } * Now truncate CIs at CXmin/CXmax qui { local touse2 `"`touse' * inlist(`_USE', 1, 3, 5, 7)"' replace `_LCI' = `CXmin' if `offscaleL' replace `_UCI' = `CXmax' if `offscaleR' replace `_LCI' = . if `touse2' & float(`_UCI') < float(`CXmin') replace `_UCI' = . if `touse2' & float(`_LCI') > float(`CXmax') replace `_ES' = . if `touse2' & float(`_ES') < float(`CXmin') replace `_ES' = . if `touse2' & float(`_ES') > float(`CXmax') if `"`rfdist'"'!=`""' { // Standard case: tempvar rflci2 clonevar `rflci2' = `_rfLCI' replace `_rfLCI' = . if `touse2' & (`offscaleL' | float(`_rfLCI') < float(`CXmin')) replace `_rfUCI' = . if `touse2' & (`offscaleR' | (float(`rflci2') > float(`CXmax') & !missing(`rflci2'))) drop `rflci2' replace `_rfLCI' = `CXmin' if `rfLoffscaleL' replace `_rfUCI' = `CXmax' if `rfRoffscaleR' // Niche case: // If one end of both CI and rfCI are offscale in same direction, // and the other end of the CI is *also* outside the CXmin/CXmax limits (albeit not marked as offscale) // (i.e. the only visible piece will be *part of one end* of the rfCI) // then that piece of the rfCI needs an arrow pointing *towards* _ES. // (This will need checking for again when it comes to constructing the rfplot) cap confirm numeric var `rfRoffscaleL' if !_rc { replace `_rfLCI' = `CXmin' if `touse2' & `rfRoffscaleL' replace `_UCI' = `CXmin' if `touse2' & `rfRoffscaleL' } else local rfRoffscaleL = 0 cap confirm numeric var `rfLoffscaleR' if !_rc { replace `_rfUCI' = `CXmax' if `touse2' & `rfLoffscaleR' replace `_LCI' = `CXmax' if `touse2' & `rfLoffscaleR' } else local rfLoffscaleR = 0 } } // null line, and upper horizontal border line // amended Nov 2021 summ `id' if `touse' & `_USE'==9, meanonly if r(N) { local borderline = r(min) - 1 - 0.25 } else { summ `id' if `touse' & `_USE'!=9, meanonly local borderline = r(max) + 1 - 0.25 } // horizontal "border" line between data and headings local 0 `", `hlineopts'"' syntax [, HORizontal VERTical /*Connect(string)*/ * ] if `"`horizontal'"'!=`""' | `"`vertical'"'!=`""' { nois disp as err `"suboptions {bf:horizontal} and {bf:vertical} not allowed in option {bf:hlineopts()}"' exit 198 } local borderCommand `"yline(`borderline', `defHlineOpts' `options')"' // null line (unless switched off) if "`null'" == "" { local 0 `", `nlineopts'"' syntax [, HORizontal VERTical /*Connect(string)*/ * ] if `"`horizontal'"'!=`""' | `"`vertical'"'!=`""' { nois disp as err `"suboptions {bf:horizontal} and {bf:vertical} not allowed in option {bf:nlineopts()}"' exit 198 } /* if `"`connect'"' != `""' { nois disp as err `"suboption {bf:connect()} not allowed in option {bf:nlineopts()}"' exit 198 } */ // DF: modified to use added line approach instead of pcspike (less complex & poss. more efficient as fewer vars) local nullCommand `" function y=`h0', horiz range(0 `borderline') n(2) `defNlineOpts' `options' ||"' } sreturn clear sreturn local options `"`macval(opts_rest)'"' // This is now *just* the standard "twoway" options // i.e. the specialist "forestplot" options have been filtered out // Return plot commands... sreturn local bordercommand `"`borderCommand'"' // ... unless `colsonly' if `"`colsonly'"'==`""' { sreturn local scplot `"`scPlot'"' sreturn local ciplot `"`CIPlot'"' sreturn local rfplot `"`RFPlot'"' sreturn local pciplot `"`PCIPlot'"' sreturn local diamplot `"`diamPlot'"' sreturn local pointplot `"`pointPlot'"' sreturn local ppointplot `"`ppointPlot'"' sreturn local olineplot `"`olinePlot'"' sreturn local olineareaplot `"`olineAreaPlot'"' sreturn local nullcommand `"`nullCommand'"' // Nov 2021: see notes above sreturn local g_olinefirst `"`g_olinefirst'"' sreturn local g_nlinefirst `"`g_nlinefirst'"' sreturn local g_overlay_ci `"`g_overlay_ci'"' } end