* 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/anything else * _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 // 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 // improvements to ocilineopts() and rfcilineopts() 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' nois disp as text `"Full command line as defined by {bf:useopts} is as follows:"' nois disp as res `" forestplot `usevlist' `if' `in', `fpuseopts' `graphopts'"' 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 /// FP(string) /// /*(deprecated; now a favours() suboption)*/ 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 } } else { // if not specified, assume "standard" varnames if `"`denominator'"'!=`""' local varlist `prefix'_Prop_ES `prefix'_Prop_LCI `prefix'_Prop_UCI // August 2023 else 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'}"' if `"`denominator'"'!=`""' nois disp as text `" due to option {bf:denominator(}{it:#}{bf:)} being specified"' } 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'"'==`""' { capture 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'"'==`""' { capture 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' // if no columns AND colsonly supplied, exit with error if !`lcolsN' & !`rcolsN' & `"`colsonly'"'!=`""' { disp as err `"Option {bf:colsonly} supplied with no columns of data; nothing to plot"' exit 2000 } ** 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. // e.g. if range() was specified with values outside the limits of xlabels, xticks etc., then DXmin, DXmax == range. // 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 ProcessXLabs `DXmin' `DXmax', `eform' h0(`h0') `null' `graphopts' `denominator' if _rc { if _rc==1 nois disp as err `"User break in {bf:forestplot.ProcessXLabs}"' nois disp as err `"Error in {bf:forestplot.ProcessXLabs}"' 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) local XLmin = r(XLmin) // limits of x-axis labelled values local XLmax = r(XLmax) return local range `"`DXmin' `DXmax'"' // local xtitleval = r(xtitleval) // position of xtitle local xlablist `"`r(xlablist)'"' local xlabcmd `"`r(xlabcmd)'"' local xlabopts `"`r(xlabopts)'"' local xmlablist `"`r(xmlablist)'"' local xmlabcmd `"`r(xmlabcmd)'"' local xmlabopts `"`r(xmlabopts)'"' local xticklist `"`r(xticklist)'"' local xtickopts `"`r(xtickopts)'"' local xmticklist `"`r(xmticklist)'"' local xmtickopts `"`r(xmtickopts)'"' // Nov 2017 local null `"`r(null)'"' local xlabfmt `"`r(xlabfmt)'"' local xmlabfmt `"`r(xmlabfmt)'"' local graphopts `"`r(options)'"' local rowsxlab = r(rowsxlab) local rowsxmlab = r(rowsxmlab) local adjust = cond(`"`colsonly'"'==`""', `"`adjust'"', `"noadjust"') // END OF TICKS AND LABELS ** 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) tempvar lci2 qui gen `lci2' = cond(`"`null'"'==`""', cond(`_LCI'>`h0', `h0', /// cond(`_LCI'>`CXmin', `_LCI', `CXmin')), cond(`_LCI'>`CXmin', `_LCI', `CXmin')) if `"`rfdist'"'!=`""' { // unecessary if passed thru from -metan-, but do it anyway 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')) // modified 28th June 2017 drop `lci2' * Unpack `usedims' local DXwidthChars = -9 // initialize if `"`usedims'"'!=`""' { local DXwidthChars = `usedims'[1, `=colnumb(`usedims', "cdw")'] confirm number `DXwidthChars' assert `DXwidthChars' >= 0 local dxwidcopt `"dxwidthchars(`DXwidthChars')"' local oldLCImin = `usedims'[1, `=colnumb(`usedims', "lcimin")'] confirm number `oldLCImin' // can be <0 if `"`usedims'"'!=`""' { local lcimin = min(`lcimin', `oldLCImin') } } // astext or dxwidth 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') } capture 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' } } // 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') /// lvallist(`lvallist') llablist(`llablist') lfmtlist(`lfmtlist') /// rvallist(`rvallist') rlablist(`rlablist') rfmtlist(`rfmtlist') `rfcolopts' /// `astextopt' `adjust' `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 AXmin = r(AXmin) local AXmax = r(AXmax) local astext = r(astext) // 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 ProcessXLabs) // 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" local hide local 0 `", `graphopts'"' syntax [, OCILINEOPts(string asis) RFCILINEOPts(string asis) * ] 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) } } } } } } 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 usedimsopt = cond(`"`usedims'"'==`""', `""', `"usedims(`usedims')"') local colWDtot = `leftWDtot' + `rightWDtot' // height of "xmlabel" text is assumed to be ~60% of "xlabel" text ... unless favours which uses xmlabel differently! local rowsxlabval = cond(`"`favours'"'!=`""', `rowsxlab', max(`rowsxlab', .6*`rowsxmlab')) GetAspectRatio, astext(`astext') colwdtot(`colWDtot') height(`height') rowsxlab(`rowsxlabval') /// `xtitle' `favours' `graphopts' `usedimsopt' `dxwidcopt' `textsize' `colsonly' local graphopts `"`r(graphopts)'"' local leftfav `"`r(leftfav)'"' local rightfav `"`r(rightfav)'"' local favopt `"`r(favopt)'"' local rowsfav = r(rowsfav) 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) local DXwidthChars = cond(`"`usedims'"'!=`""', `DXwidthChars', `colWDtot'*((100/`astext') - 1)) * If specified, store in a matrix the quantities needed to recreate proportions in subsequent forestplot(s) // [`lcimin' added 18th Sep 2017, and `height' added 2nd 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 } * Extra work on x-labels and aspect ratio, only needed if `colsonly' [** BETA **] if `"`colsonly'"'!=`""' { if `lcolsN' & !`rcolsN' { // local plotAspect = `plotAspect' * `approxChars'/`leftWDtot' // local xsize = `xsize' * `leftWDtot'/`approxChars' // Nov 2017: do this or not? local AXmax = `DXmin' } else if !`lcolsN' & `rcolsN' { // local plotAspect = `plotAspect' * `approxChars'/`rightWDtot' // local xsize = `xsize' * `rightWDtot'/`approxChars' // Nov 2017: do this or not? local AXmin = `DXmax' } ExtraColsOnly, xlablist(`xlablist') xmlablist(`xmlablist') /// xlabopt(`xlabcmd', `xlabopts') xmlabopt(`xmlabcmd', `xmlabopts') /// xtickopt(`xticklist', `xtickopt') xmtickopt(`xmticklist', `xmtickopt') /// ax(`AXmin' `AXmax') rowsxlab(`rowsxlab' `rowsxmlab' `rowsfav') `keepxlabs' // Feb 2018: removed `graphopts' // xlabel: insert `textSize2' local xlabopt `"xlabel(`s(xlabcmd)', labsize(`textSize2') `s(xlabopts)')"' // xmlabel: insert `textSize' if `favours', otherwise default to 0.6*`textSize' local labsizeopt = cond(`"`favours'"'!=`""', `"labsize(`textSize2')"', `"labsize(`=.6*`textSize2'')"') local xmlabopt = cond(trim(`"`s(xmlabcmd)'`s(xmlabopts)'"')==`""', `""', /// `"xmlabel(`s(xmlabcmd)', `labsizeopt' `s(xmlabopts)')"') // xtick and xmtick: simply use returned options from ExtraColsOnly local xtickopt `"`s(xtickopt)'"' local xmtickopt `"`s(xmtickopt)'"' } // Else, just need to insert labsize(`textSize') into existing `xlabopt' [** DEFAULT **] else { // Amended Feb 2019 for v3.3 local 0 `", `xlabopts'"' syntax [, LABSize(string) * ] local xlabopts `"`macval(options)'"' if `"`labsize'"'==`""' local labsize `textSize2' local xlabopt `"xlabel(`xlabcmd', labsize(`labsize') `xlabopts')"' // If `favours', text size of xmlabel defaults to `textSize'; otherwise to 0.6*`textSize'; similarly for lapgap local 0 `", `xmlabopts'"' syntax [, LABSize(string) LABGAP(string) * ] local xmlabopts `"`macval(options)'"' if `"`labsize'"'==`""' { local labsize = cond(`"`favours'"'!=`""', `textSize2', .6*`textSize2') } if `"`labgap'"'==`""' & `"`favours'"'!=`""' local labgap = 5 if `"`labgap'"'!=`""' local labgapopt `"labgap(`labgap')"' local xmlabopt = cond(trim(`"`xmlabcmd'`xmlabopts'"')==`""', `""', /// `"xmlabel(`xmlabcmd', labsize(`labsize') `labgapopt' `xmlabopts')"') local xtickopt = cond(trim(`"`xticklist'`xtickopts'"')==`""', `""', /// `"xtick(`xticklist', `xtickopts')"') local xmtickopt = cond(trim(`"`xmticklist'`xmtickopts'"')==`""', `""', /// `"xmtick(`xmticklist', `xmtickopts')"') } // 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') `graphopts'"' if trim(`"`savedims'`usedims'"')!=`""' local graphopts `"fxsize(`fxsize') fysize(`fysize') `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' } ** FAVOURS (part 2) // now check for inappropriate options if `"`favours'"' != `""' { // continue to allow fp as a main option, but deprecate it in documentation // documented way is to specify fp() as a suboption to favours() local oldfp `fp' local 0 `", `favopt'"' syntax [, FP(string) FORMAT(string) ANGLE(string) LABGAP(string) LABSTYLE(string) LABSize(string) LABColor(string) noSYMmetric * ] if `"`options'"' != `""' { nois disp as err `"inappropriate suboptions found in {bf:favours()}"' exit 198 } local labsizeopt = cond(`"`labsize'"'!=`""', `"labsize(`labsize')"', `"labsize(`textSize2')"') local labgapopt = cond(`"`labgap'"'!=`""', `"labgap(`labgap')"', `"labgap(5)"') local favopt `"`labsizeopt' `labgapopt'"' foreach opt in format angle labstyle labcolor { if `"``opt''"'!=`""' local favopt `"`favopt' `opt'(``opt'')"' } // modified Jan 30th 2018, and again May 21st 2018 local fp = cond(`"`fp'"'==`""' & `"`oldfp'"'!=`""', `"`oldfp'"', `"`fp'"') 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] // local favopt = cond(trim(`"`leftfp'`rightfp'"')=="", "", `"xmlabel(`leftfp' `rightfp', noticks labels norescale `favopts')"') if trim(`"`leftfp'`rightfp'"')==`""' local favopt else { local addopt = cond(`"`xmlabopt'"'==`""', "", "add") // if xmlabel is also used elsewhere local favopt `"xmlabel(`leftfp' `rightfp', noticks norescale `favopt' `addopt')"' } } // end if trim(`"`leftfav'`rightfav'"') != `""' ************************************ * 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 scPlot `"`s(scplot)'"' // local CIPlot `"`s(ciplot)'"' local RFPlot `"`s(rfplot)'"' local PCIPlot `"`s(pciplot)'"' local diamPlot `"`s(diamplot)'"' local pointPlot `"`s(pointplot)'"' local ppointPlot `"`s(ppointplot)'"' // local olinePlot `"`s(olineplot)'"' local olineAreaPlot `"`s(olineareaplot)'"' // local nullCommand `"`s(nullcommand)'"' 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 ProcessXLabs*/ 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) */ /*`scPlot' `CIPlot'*/ `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 */ yscale(range(`DYmin' `DYmax') noline) ylabel(none) ytitle("") `borderCommand' /* X-AXIS OPTIONS */ 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 * Last modified Jan 2018 for v2.2 program define ProcessXLabs, rclass syntax anything [, XLAbel(string asis) XMLabel(string asis) XTick(string) XMTick(string) FORCE /// RAnge(string) CIRAnge(string) EFORM H0(real 0) noNULL /*PRoportion*/ DENOMinator(string) * ] local graphopts `"`options'"' tokenize `anything' args DXmin DXmax * 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') if "`range'" != `""' { tokenize `range' cap { assert `"`2'"'!=`""' assert `"`3'"'==`""' } if _rc { disp as err `"option {bf:range()} must contain exactly two elements"' exit 198 } 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 range `"`1' `2'"' local range = subinstr(`"`range'"', `"min"', `"`DXmin'"', .) local range = subinstr(`"`range'"', `"max"', `"`DXmax'"', .) numlist "`range'", min(2) max(2) sort local range = r(numlist) tokenize "`range'" args RXmin RXmax } else { if `"`eform'"'!=`""' { numlist "`range'", min(2) max(2) range(>0) sort local range `"`=ln(`1')' `=ln(`2')'"' } else { numlist "`range'", min(2) max(2) sort local range = r(numlist) } tokenize "`range'" args RXmin RXmax } } if "`cirange'" != `""' { tokenize `cirange' cap { assert `"`2'"'!=`""' assert `"`3'"'==`""' } if _rc { disp as err "option {bf:cirange()} 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 cirange `"`1' `2'"' local cirange = subinstr(`"`cirange'"', `"min"', `"`DXmin'"', .) local cirange = subinstr(`"`cirange'"', `"max"', `"`DXmax'"', .) numlist "`cirange'", min(2) max(2) sort local cirange = r(numlist) tokenize "`cirange'" args CXmin CXmax } else { if `"`eform'"'!=`""' { numlist "`cirange'", min(2) max(2) range(>0) sort local cirange `"`=ln(`1')' `=ln(`2')'"' } else { numlist "`cirange'", min(2) max(2) sort local cirange = r(numlist) } tokenize "`cirange'" args CXmin CXmax } } * Initial parse of xlabel and xtick // In case old (v3.x and earlier) syntax is used, with comma-separated values and no sub-options // [added May 2020] local done = 0 foreach xl in xlabel xtick { local comma = 0 local csv = 1 local lblcmd tokenize `"``xl''"', parse(",") while `"`1'"' != `""' { cap confirm number `1' if !_rc local lblcmd `"`lblcmd' `1'"' else cap assert `"`1'"'==`","' if !_rc local comma = 1 else local csv = 0 mac shift } if `csv' { if `"`lblcmd'"'!=`""' { capture numlist `"`lblcmd'"' if _rc { nois disp as err `"error in option {bf:`xl'()}: invalid numlist"' exit _rc } local `xl' = 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 `xl'' local `xl' ``xl'' `newh0' } numlist `"``xl''"', sort local `xl' = r(numlist) if !`done' { nois disp as err _n `"Note: with {bf:metan} version 4 and above, the preferred syntax is for {bf:`xl'()}"' nois disp as err `" to contain a standard Stata numlist, so e.g. {bf:`xl'(``xl'')}; see {help numlist:help numlist}"' } local done = 1 c_local twowaynote notwowaynote // so that -metan- does not print an additional message regarding "force" } } } } // legacy -force- option if "`force'"!="" & `csv' { if `done' local xlabel `"`xlabel', force"' else { nois disp as err "option {bf:force} not allowed" exit 198 } c_local twowaynote notwowaynote // so that -metan- does not print an additional message regarding "force" } * Parse x[m]label if supplied by user local rowsxmlab = 0 local rowsxlab = 0 foreach xl in xlab xmlab { local 0 `"``xl'el'"' // xlabel, xmlabel syntax [anything(name=`xl'cmd)] , [FORCE FORMAT(string) * ] local forceopt = cond(`"`xl'"'==`"xlab"', `"`force'"', `"`forceopt'"') // "force" option only applies to xlab, not xmlab local `xl'opts `"`options'"' // Parse x[m]lablist and obtain numlist (Nov 2017) if `"``xl'cmd'"'!=`""' { local rows`xl' = 1 local lbl local lbl2 local qed local rest : copy local `xl'cmd 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, quotes qed(qed2) if !`qed2' { disp as err `"invalid label specifier, : ``xl'list':"' exit 198 } local new`xl'cmd `"`new`xl'cmd' `rest2'"' 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`xl' = max(`rows`xl'', `: word count `newlist'') local lbl2 } // end if `qed' // else, check if valid numlist else { cap numlist `"`tok'"' if _rc { if substr(`"`tok'"', 1, 1)==`"#"' { disp as err `"Cannot use the {bf:#} syntax in the {bf:`xl'el()} option of {bf:forestplot}; please use a {it:numlist} instead"' } else numlist `"`tok'"' } if `"`eform'"'!=`""' { cap numlist `"`tok'"', range(>0) if _rc { disp as err `"option {bf:eform} specified, but {bf:`xl'el()} 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 `xl'list `"``xl'list' `=ln(`el')'"' local new`xl'i `"`=ln(`el')'"' local lbl = cond("`format'"=="", string(`el'), string(`el', "`format'")) if `i'==1 & `"`lbl2'"'!=`""' local new`xl'i `"`"`lbl2'"' `new`xl'i'"' if `i'<`N' local new`xl'i `"`new`xl'i' `"`lbl'"'"' local lbl2 // don't add the last label yet, in case user has specified their own label local new`xl'cmd `"`new`xl'cmd' `new`xl'i'"' } } // else, can simply add unexpanded numlist else { local `xl'list `"``xl'list' `tok'"' local new`xl'cmd `"`new`xl'cmd' `tok'"' local lbl2 } } // end else } // end if `"`tok'"'!=`""' // if lbl, add it now if `"`lbl2'"'!=`""' { local new`xl'cmd `"`new`xl'cmd' `"`lbl2'"'"' local lbl local lbl2 } } // while loop local `xl'list = trim(`"``xl'list'"') local `xl'cmd = trim(`"`new`xl'cmd'"') } cap assert `"``xl'cmd'"'==`""' if `"``xl'list'"'==`""' if _rc { disp as err "Error in {bf:`xl'el()}" exit 198 } local `xl'fmt : copy local format // added 1st May 2018 } if `"`xlablist'"' != `""' { if "`forceopt'"!=`""' { if "`cirange'"!="" { disp as err `"Note: both {bf:cirange()} and {bf:xlabel(, force)} were specifed; {bf:cirange()} takes precedence"' } else { numlist "`xlablist'", sort local n : word count `r(numlist)' // added Sep 2017 for v2.1 if `"`range'"'==`""' { local RXmin : word 1 of `r(numlist)' // if `range' not specified, default to "forced" xlab limits local RXmax : word `n' of `r(numlist)' local range `"`RXmin' `RXmax'"' } else { local CXmin : word 1 of `r(numlist)' // otherwise, set `cirange' instead local CXmax : word `n' of `r(numlist)' local cirange `"`CXmin' `CXmax'"' } } } } * Parse ticks // JUN 2015 -- for future: is there any call for allowing FORCE, or similar, for ticks?? foreach tick in xtick xmtick { if `"``tick''"' != "" { local 0 `"``tick''"' syntax [anything(name=`tick'list)] , [ FORCE * ] local `tick'opts `"`options'"' if `"`force'"'!=`""' { nois disp as err "option {bf:force} is not allowed with {bf:xtick()}" exit 198 } if `"``tick'list'"' != `""' { cap numlist `"``tick'list'"' if _rc { disp as err `"invalid label specifier, : ``tick'list'"' exit 198 } if `"`eform'"'!=`""' { // assume given on exponentiated scale if "eform" specified, so need to take logs cap numlist "``tick'list'", range(>0) // ...in which case, all values must be greater than zero if _rc { disp as err `"with {bf:eform} option, {bf:`tick'()} values are expected to be on the exponentiated scale"' disp as err `"and therefore strictly greater than zero"' exit 198 } local e`tick'list ``tick'list' local `tick'list foreach xi of numlist `e`tick'list' { local `tick'list `"``tick'list' `=ln(`xi')'"' } } } } } * 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 `"`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' ********************************** * If xlabel not supplied by user * ********************************** // Need to 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 // Last modifed by DF April 2017 to avoid interminable looping if [base]^`mag' = missing local xlablim1=0 // init if `"`xlablist'"' == `""' { // Mar 2020: // If `proportion', simply choose 0, .5 and 1 /* if `"`proportion'"'!=`""' { local xlablist `"0 .5 1"' local xlabcmd `"`xlablist'"' } */ // 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 xlabcmd `"`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 if `"`eform'"'!=`""' { local xlabcmd foreach xi of numlist `xlablist' { local lbl = cond("`xlabfmt'"=="", string(exp(`xi')), string(exp(`xi'), "`xlabfmt'")) local xlabcmd `"`xlabcmd' `xi' `"`lbl'"'"' } // return local xlabfmt `"`xlabfmt'"' // Nov 2017: in case of colsonly + eform } else { local xlabcmd `"`xlablist'"' // If formatting not used here (for string labelling), return it alongside other `xlabopts' to pass to -twoway- // if `"`format'"'!=`""' local xlabopts `"`xlabopts' format(`format')"' } // Added Feb 2018: If automatic labelling, set rows to 1 (rowsxmlab remains at 0) local rowsxlab = 1 } // end if "`xlablist'" == "" if `"`xlabfmt'"'!=`""' local xlabopts `"`xlabopts' format(`xlabfmt')"' if `"`xmlabfmt'"'!=`""' local xmlabopts `"`xmlabopts' format(`xmlabfmt')"' 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' } // Position of xtitle 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' return scalar XLmin = `XLmin' return scalar XLmax = `XLmax' // moved Feb 2018; modified Oct 2018 return scalar rowsxlab = `rowsxlab' return scalar rowsxmlab = `rowsxmlab' return local xlablist `"`xlablist'"' return local xlabcmd `"`xlabcmd'"' return local xlabopts `"`xlabopts'"' return local xmlablist `"`xmlablist'"' return local xmlabcmd `"`xmlabcmd'"' return local xmlabopts `"`xmlabopts'"' return local xticklist `"`xticklist'"' return local xtickopts `"`xtickopts'"' return local xmticklist `"`xmticklist'"' return local xmtickopts `"`xmtickopts'"' return local options `"`graphopts'"' end ********************************************************************************* * Subroutine to do extra work sorting out labels/ticks ONLY IF COLSONLY [** BETA **] // Created Nov 2017 program define ExtraColsOnly, sclass syntax [, XLABLIST(numlist) XMLABLIST(numlist) /// XLABOPT(string asis) XMLABOPT(string asis) XTICKOPT(string asis) XMTICKOPT(string asis) /// AX(numlist) ROWSXLAB(numlist >=0) KEEPXLabs ] // Feb 2018: blanked out * and added xmlablist/opt tokenize `ax' args AXmin AXmax tokenize `rowsxlab' args rowsxlab rowsxmlab rowsfav local rowsxmlab = max(`rowsfav', `rowsxmlab') // adjust xticklist and xlablist if needed // NOV 2017: NEEDS RE-DOING: (Oct 2018: include in next version??) // INCLUDE XMTICK // BUT ALSO, NEED TO TAKE ACCOUNT OF TEXT LABELS IN XLABLIST (BELOW) -- see revised code of ProcessXLabs local lt = cond(`"`keepxlabs'"'==`""', `"<="', `"<"') local gt = cond(`"`keepxlabs'"'==`""', `">="', `">"') // Ticklists: these are easy since no labels, so can use subinstr() foreach tick in xtick xmtick { local 0 `"``tick'opt'"' syntax [anything(name=`tick'list)] [, *] local `tick'opts `"`options'"' local old`tick' = 0 if `"``tick'list'"'!=`""' { local old`tick' = 1 numlist `"``tick'list'"' local `tick'list = r(numlist) foreach xi of numlist ``tick'list' { if `xi' `lt' `AXmin' | `xi' `gt' `AXmax' { local `tick'list = subinstr(`" ``tick'list' "', `" `xi' "', `" "', .) } } local `tick'list = trim(itrim(`"``tick'list'"')) } } // Process xlabcmd and xmlabcmd foreach xl in xlab xmlab { local 0 `"``xl'opt'"' syntax [anything(name=`xl'cmd)] [, FORMAT(string) *] local `xl'fmt `"`format'"' local `xl'opts `"`options'"' // technique for xlablist: first, find which values need to be removed // then (assuming there are any) find them within `xlabcmd' // and remove them AND any associated label. if `"``xl'list'"'!=`""' { numlist `"``xl'list'"' local `xl'list = r(numlist) foreach xi of numlist ``xl'list' { if `xi' `lt' `AXmin' | `xi' `gt' `AXmax' { local remove `"`remove' `xi'"' } } if `: word count `remove'' { local `xl'list : list `xl'list - remove local rest : copy local `xl'cmd local `xl'cmd local flag = 0 while `"`rest'"'!=`""' { gettoken el rest : rest, quotes qed(qed) if !`qed' local flag=0 if `: list el in remove' local flag=1 if `flag'!=1 { local `xl'cmd `"``xl'cmd' `el'"' if `qed' local flag=0 } } } } if `"``xl'list'"'!=`""' { if `"`eform'"'!=`""' { local `xl'cmd foreach xi of numlist ``xl'list' { local lbl = cond("``xl'fmt'"=="", string(exp(`xi')), string(exp(`xi'), "``xl'fmt'")) local `xl'cmd `"``xl'cmd' `xi' `"`lbl'"'"' } } else local `xl'cmd `"``xl'list'"' local `xl'cmd = trim(`"``xl'cmd'"') } // If xlablist has been entirely removed, use blank lines to use up the same space as labels would else if `"`xl'"'==`"xlab"' & `rowsxlab' { forvalues i=1/`rowsxlab' { local xlabtxt `"`xlabtxt' `" "'"' } if `rowsxlab' > 1 local xlabtxt `"`"`xlabtxt'"'"' local xlabcmd `"`AXmin' `xlabtxt'"' local xlabopts `"`xlabopts' tlc(none)"' } // same for xmlablist/favours else if `"`xl'"'==`"xmlab"' & `rowsxmlab' { forvalues i=1/`rowsxmlab' { local xmlabtxt `"`xmlabtxt' `" "'"' } if `rowsxmlab' > 1 local xmlabtxt `"`"`xmlabtxt'"'"' local xmlabcmd `"`AXmin' `xmlabtxt'"' local xmlabopts `"`xmlabopts' tlc(none)"' } } // Process ticklists: these are easier as no labels (see above) foreach tick in xtick xmtick { if `old`tick'' { local `tick'list `AXmin' local `tick'opts `"``tick'opts' tlc(none)"' } } // Oct 2018: CONSIDERATIONS FOR NEXT VERSION // local xlabopt = cond(`"`xlabcmd'"'==`""', `"xlabel(none)"', `"xlabel(`xlabcmd', labsize(`textSize') `xlabopts')"') // local xtickopt = cond(`"`xticklist'"'==`""', `"xtick(none)"', `"xtick(`xticklist', `xtickopts')"') // Nov 2017: what about mtick?? also NEED TO USE BLANK LINES TO USE UP SAME SPACE AS LABELS WOULD. also at the moment user-defined labvalues are not appearing // amend ProcessXLabs so that xlabel "labels" are extracted and tested for number of lines // revisit use of xmlabel in favour of xtitle?? any reason for this now? YES because plot might not be centered sreturn local xlablist `"`xlablist'"' sreturn local xlabcmd `"`xlabcmd'"' sreturn local xlabopts `"`xlabopts'"' sreturn local xmlablist `"`xmlablist'"' sreturn local xmlabcmd `"`xmlabcmd'"' sreturn local xmlabopts `"`xmlabopts'"' if trim(`"`xticklist'`xtickopts'"')!=`""' { sreturn local xtickopt `"`xticklist', `xtickopts'"' } if trim(`"`xmticklist'`xmtickopts'"')!=`""' { sreturn local xmtickopt `"`xmticklist', `xmtickopts'"' } sreturn local options `"`graphopts'"' end ********************************************************************************* * 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) /// [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 * ] 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 local rcolsN = cond(`"`rcolsN'"'==`""', 0, `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' local rightWDtot = `rightWDtot' + (1 + `rbuffer')*`digitwid' // default: 1x buffer before first RHS column and after last, but can be overwritten local rightWDtot = max(`rightWDtot', `digitwid') // in case of no `rcols' ** "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'" == "" local leftWDtot = `leftWDtot' + `lbuffer' // LHS buffer; default is zero // 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" // [modified sep 2017... for latest beta??] 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 ********************************************************************************* *** FIND OPTIMAL TEXT SIZE AND ASPECT RATIOS (given user input) // Notes: (David Fisher, July 2014) // Let X, Y be dimensions of graphregion (controlled by xsize(), ysize()); x, y be dimensions of plotregion (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 // GetAspectRatio, astext(`astext') colwdtot(`colWDtot') height(`height') rowsxlab(`rowsxlab') `graphopts' `usedimsopt' // Jan 2018: double check that NOT returning graphopts doesn't cause any problems 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) FAVours(string asis) 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(`usedims', "cdw")'] local spacing = cond(`spacing'==-9, `usedims'[1, `=colnumb(`usedims', "spacing")'], `spacing') // local oldPlotAspect = cond(`aspect'==-9, `usedims'[1, `=colnumb(`usedims', "aspect")'], `aspect') // local xsize = cond(`xsize'==-9, `usedims'[1, `=colnumb(`usedims', "xsize")'], `xsize') // local ysize = cond(`ysize'==-9, `usedims'[1, `=colnumb(`usedims', "ysize")'], `ysize') local oldPlotAspect = `usedims'[1, `=colnumb(`usedims', "aspect")'] // modified 2nd Nov 2017 for v2.2 beta local oldXSize = `usedims'[1, `=colnumb(`usedims', "xsize")'] local oldYSize = `usedims'[1, `=colnumb(`usedims', "ysize")'] local oldTextSize = `usedims'[1, `=colnumb(`usedims', "textsize")'] local oldHeight = `usedims'[1, `=colnumb(`usedims', "height")'] // added 18th Sep 2017 for v2.2 beta local oldYheight = `usedims'[1, `=colnumb(`usedims', "yheight")'] // added 18th Sep 2017 for v2.2 beta 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 for v2.2 beta] // (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) } * Do the same for `favours' // (N.B. syntax is more complicated so need to do separately) local rowsfav = 0 if `"`favours'"' != `""' { local 0 `"`favours'"' syntax [anything(everything)] [, * ] * 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 } return local leftfav `"`leftfav'"' return local rightfav `"`rightfav'"' return local favopt `"`options'"' } return scalar rowsfav = `rowsfav' 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 2nd Nov 2017 for v2.2] 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'"'!=`""' /*& (`lcolsN'*`rcolsN'==0)*/, 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 2nd Nov 2017 for v2.2 beta] 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 1st Nov 2017 for v2.2 beta] // 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') // local fxsize = cond(`fxsize' == -9, 100, `fxsize') // local fysize = cond(`fysize' == -9, 100, `fysize') // local fsizeopt `"fxsize(`fxsize') fysize(`fysize')"' // return scalar fxsize = `fxsize' // return scalar fysize = `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 2nd Nov 2017 for v2.2 beta] if `xsize'==-9 & `ysize'==-9 local xsize = `oldYSize' / `graphAspect' else { if `xsize'==-9 local xsize = `ysize' / `graphAspect' else local ysize = `xsize' * `graphAspect' } // local xsize = `ysize' / `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 2nd Nov 2017 for v2.2 beta] if `xsize'==-9 & `ysize'==-9 local xsize = `oldYSize' / `graphAspect' else { if `xsize'==-9 local xsize = `ysize' / `graphAspect' else local ysize = `xsize' * `graphAspect' } // local xsize = `ysize' / `graphAspect' // 3a, 4a if `graphAspect' <= 1 { local textSize = `textSize' / `oldGraphAspect' } } } // [added 1st Nov 2017 for v2.2 beta] // local fxsize = cond(`fxsize'==-9, 100*(`oldPlotAspect'/`plotAspect')*(`height'/`oldHeight'), `fxsize') // local fysize = cond(`fysize'==-9, 100*`ydelta'*`height'/`oldYheight', `fysize') // 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') // local fsizeopt `"fxsize(`fxsize') fysize(`fysize')"' // return scalar fxsize = `fxsize' // return scalar fysize = `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 2nd Nov for v2.2 beta] return scalar ysize = cond(`ysize'==-9, 4, `ysize') // [added 2nd Nov for v2.2 beta] 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 [unless `colsonly' **BETA**] 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'"' sreturn local bordercommand `"`borderCommand'"' // 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