*! version 1.3.1 15jul2026 Eric Booth / Texas 2036 Data & Research *! dashboardbuilder — build a self-contained, interactive HTML dashboard from Stata *! ---------------------------------------------------------------------------- *! A putdocx-style BUILDER: you call it several times, feeding it one analytic *! subset at a time (each -panel- call snapshots the data currently in memory), *! then -build- writes ONE self-contained HTML file: no CDN, no internet, no *! external files. Charts are readable hand-rolled SVG/JS meant as a STARTER *! WIREFRAME a human can open and tweak. *! *! dashboardbuilder init , title(...) [subtitle() selector() refvalue() tx2036] *! dashboardbuilder tab , name(...) label(...) *! dashboardbuilder panel , [options] *! dashboardbuilder build using file.html [, nocsv nopng notooltip pdf truepdf /// *! callout() sourcenote() replace noopen] *! dashboardbuilder describe | clear | openlast | openfolder *! *! ENGINE: Stata orchestrates; Stata's built-in Python integration (PyStata, *! Stata 16+) does the JSON serialization and HTML templating using ONLY the *! Python standard library (json, os). Nothing to pip install, ever. *! See -help dashboardbuilder- for the full story and worked examples. *! ---------------------------------------------------------------------------- program define dashboardbuilder, rclass version 16 gettoken sub 0 : 0, parse(" ,") local sub = lower(`"`sub'"') if `"`sub'"' == "init" _dbb_init `0' else if `"`sub'"' == "tab" _dbb_tab `0' else if `"`sub'"' == "panel" _dbb_panel `0' else if `"`sub'"' == "build" { _dbb_build `0' return local file `"${DBB_R_FILE}"' return local ntabs "${DBB_NTABS}" return local npanels "${DBB_NPANELS}" return local bytes "${DBB_R_BYTES}" } else if `"`sub'"' == "describe" _dbb_describe else if `"`sub'"' == "clear" _dbb_clear else if `"`sub'"' == "openlast" _dbb_openpath `"${DBB_R_FILE}"' else if `"`sub'"' == "openfolder" _dbb_openpath `"${DBB_R_FILE}"' , folder else { di as err `"dashboardbuilder: unknown subcommand "`sub'""' di as err " valid: init | tab | panel | build | describe | clear" di as err " see {help dashboardbuilder}" exit 198 } end * ═══════════════════════════════════════════════════════════════════════════ * openpath — auto-open a file in the default app, or its folder in the OS file * manager. Backs the auto-open (default; -noopen- suppresses) and the -openlast- / * -openfolder- subcommands. The receipt's clickable links are native {browse} * directives, so they need no subprogram. * ═══════════════════════════════════════════════════════════════════════════ program define _dbb_openpath gettoken path 0 : 0, parse(",") syntax [, folder] local path = trim(`"`path'"') if `"`path'"' == "" { di as err "nothing to open yet — run {bf:dashboardbuilder build} first." exit 198 } local path = subinstr(`"`path'"', char(92), "/", .) // forward slashes if "`folder'" != "" local path = substr(`"`path'"', 1, strrpos(`"`path'"', "/") - 1) * Open in the OS default app: the browser for an .html file, Finder/Explorer * for a folder. Cross-platform branch mirrors sparkta2_open: macOS -open-, * Windows -start-, Linux -xdg-open- (Linux has no -open-). Plain subprocess * calls with no URL parsing, so they are crash-safe. * IMPORTANT — never hand -view browse- (or a {browse} link) a file:// URL. On * macOS (observed on Stata 19.5 / macOS 26) Stata parses it through * NSURLComponents, which THROWS on a path holding '@' (e.g. a Google Drive * path) and ABORTS Stata. The receipt's clickable links use {browse} on the * RAW path (no file:// scheme, so no authority/userinfo parse), which is safe. local _os = lower("`c(os)'") if strpos("`_os'", "win") capture winexec cmd /c start "" `"`path'"' else if strpos("`_os'", "mac") capture shell open `"`path'"' else capture shell xdg-open `"`path'"' & if _rc di as txt `"(could not auto-open; open this path yourself: `path')"' end * ═══════════════════════════════════════════════════════════════════════════ * pycheck — verify Stata's Python integration works BEFORE we depend on it, * and print exact fix-it commands when it does not (per-machine setup). * Only the Python STANDARD LIBRARY is used, so no pip installs are needed. * ═══════════════════════════════════════════════════════════════════════════ program define _dbb_pycheck capture python query if _rc { di as err "dashboardbuilder needs Stata's built-in Python integration (Stata 16+)." di as err "Stata could not find a usable Python. To fix:" di as err `" 1. see what Stata can find: {stata python search}"' di as err `" 2. point Stata at a Python 3, for example:"' di as err `" . python set exec /usr/local/bin/python3, permanently (Mac/Linux)"' di as err `" . python set exec C:\Python312\python.exe, permanently (Windows)"' di as err `" 3. verify: {stata python query}"' di as err "No extra Python packages are required (standard library only)." exit 198 } capture python: import sys; assert sys.version_info[0] >= 3 if _rc { di as err "dashboardbuilder needs Python 3; Stata is currently linked to Python 2." di as err `"Point Stata at a Python 3 install:"' di as err `" . python search"' di as err `" . python set exec , permanently"' exit 198 } capture python: import json, os if _rc { di as err "Python was found, but importing the standard library (json, os) failed," di as err "which suggests a broken Python install. Try pointing Stata at another one:" di as err `" . python search"' di as err `" . python set exec , permanently"' exit 198 } end program define _dbb_require_active if "${DBB_ACTIVE}" != "1" { di as err "no dashboard in progress — run {bf:dashboardbuilder init} first." di as err "see {help dashboardbuilder##examples:examples}" exit 198 } end * ═══════════════════════════════════════════════════════════════════════════ * init — start a dashboard; registers title/theme/selector, makes a temp dir * ═══════════════════════════════════════════════════════════════════════════ program define _dbb_init syntax , TItle(string) [SUBtitle(string) SELector(name) SELLABel(string) /// REFVALue(string) TX2036 THEME(string)] _dbb_pycheck _dbb_pysetup if "${DBB_ACTIVE}" == "1" { di as txt "(discarding the previous unfinished dashboard; starting fresh)" _dbb_clear } * theme: tx2036 flag wins; else theme(); else simple if "`tx2036'" != "" local theme "tx2036" if "`theme'" == "" local theme "simple" local theme = lower("`theme'") if !inlist("`theme'", "simple", "tx2036") { di as err `"theme(`theme') not recognized — use theme(simple) or theme(tx2036)"' exit 198 } if `"`refvalue'"' != "" & "`selector'" == "" { di as err "refvalue() requires selector() — the reference is a value OF the selector" exit 198 } * working dir for panel snapshots (unique per init; lives in c(tmpdir)). * tempfile numbers can be REUSED once a program returns, so a directory from * an earlier init may still exist — loop with a suffix until mkdir succeeds. tempfile tf local base = subinstr(`"`tf'"', char(92), "/", .) + "_dbb" local dir `"`base'"' capture mkdir `"`dir'"' local k 0 while _rc & `k' < 100 { local ++k local dir `"`base'`k'"' capture mkdir `"`dir'"' } if _rc { di as err `"could not create a working directory under `base'"' exit 693 } global DBB_ACTIVE "1" global DBB_DIR `"`dir'"' global DBB_TITLE `"`title'"' global DBB_SUBTITLE `"`subtitle'"' global DBB_THEME "`theme'" global DBB_SEL "`selector'" global DBB_SELLAB `"`sellabel'"' global DBB_REFVAL `"`refvalue'"' global DBB_NTABS "0" global DBB_NPANELS "0" global DBB_LASTTAB "" di as txt "dashboardbuilder: started " as res `"`title'"' /// as txt " (theme: " as res "`theme'" as txt ")" if "`selector'" != "" { di as txt " selector: " as res "`selector'" /// as txt cond(`"`refvalue'"' != "", `" (reference value: "`refvalue'")"', "") di as txt " panels whose data contain a variable named " /// as res "`selector'" as txt " will be filtered by the dropdown." } di as txt " next: {bf:dashboardbuilder tab}, then {bf:dashboardbuilder panel}, then {bf:dashboardbuilder build}" end * ═══════════════════════════════════════════════════════════════════════════ * tab — register a tab (optional; panels without tabs share one implicit page) * ═══════════════════════════════════════════════════════════════════════════ program define _dbb_tab _dbb_require_active syntax , NAme(name) [LABel(string)] if `"`label'"' == "" local label "`name'" forvalues i = 1/${DBB_NTABS} { if "${DBB_TAB_NAME_`i'}" == "`name'" { di as err `"tab name "`name'" already declared"' exit 198 } } local k = ${DBB_NTABS} + 1 global DBB_NTABS "`k'" global DBB_TAB_NAME_`k' "`name'" global DBB_TAB_LAB_`k' `"`label'"' global DBB_LASTTAB "`name'" di as txt `" tab `k' registered: "' as res `"`label'"' as txt `" (name: `name')"' end * ═══════════════════════════════════════════════════════════════════════════ * panel — snapshot the CURRENT data in memory as one dashboard panel * ═══════════════════════════════════════════════════════════════════════════ program define _dbb_panel _dbb_require_active _dbb_pysetup gettoken ptype 0 : 0, parse(" ,") local ptype = lower(`"`ptype'"') if !inlist("`ptype'", "kpi", "line", "bar", "hbar", "compare", "table", "html") { di as err `"panel type "`ptype'" not recognized"' di as err " valid: kpi | line | bar | hbar | compare | table | html" exit 198 } * html panels embed an external file (no data), so they skip the data checks if _N == 0 & "`ptype'" != "html" { di as err "no observations in memory — load/derive the panel's data first" exit 2000 } * ---- per-type option parsing (spec vars differ by type) ---- local x "" local y "" local ref "" local vals "" local vars "" if "`ptype'" == "kpi" { syntax , VALues(varlist numeric) [TAB(name) TItle(string) NOTE(string) INTERP(string)] local vals "`values'" } else if "`ptype'" == "line" { syntax , X(varname) Y(varlist numeric) [TAB(name) TItle(string) NOTE(string) /// INTERP(string) YTItle(string)] } else if inlist("`ptype'", "bar", "hbar") { syntax , X(varname) Y(varname numeric) [TAB(name) TItle(string) NOTE(string) /// INTERP(string) YTItle(string)] } else if "`ptype'" == "compare" { syntax , X(varname) Y(varname numeric) [REF(varname numeric) TAB(name) /// TItle(string) NOTE(string) INTERP(string) YTItle(string)] } else if "`ptype'" == "table" { syntax [, VARs(varlist) TAB(name) TItle(string) NOTE(string) INTERP(string)] if "`vars'" == "" unab vars : _all } else { // html — embed an external file syntax , FIle(string) [TAB(name) TItle(string) NOTE(string) INTERP(string) HEIGHT(integer 520)] } * ---- resolve the tab this panel belongs to (auto-create "main" if none) ---- if "`tab'" == "" local tab "${DBB_LASTTAB}" if "`tab'" == "" { _dbb_tab , name(main) label("Dashboard") local tab "main" } local found 0 forvalues i = 1/${DBB_NTABS} { if "${DBB_TAB_NAME_`i'}" == "`tab'" local found 1 } if !`found' { di as err `"tab(`tab') was never declared — run: dashboardbuilder tab, name(`tab') label("...")"' exit 198 } * ---- html panel: register an external file to inline; no data capture ------ if "`ptype'" == "html" { * absolutize the path (Python reads the file at build time; its cwd differs) local hf = subinstr(`"`file'"', char(92), "/", .) if substr(`"`hf'"', 1, 1) != "/" & strpos(`"`hf'"', ":") == 0 { local hf `"`c(pwd)'/`hf'"' local hf = subinstr(`"`hf'"', char(92), "/", .) } capture confirm file `"`hf'"' if _rc { di as err `"panel html: file not found — `hf'"' di as err " build the HTML first (e.g. a sparkta2 map with export()/offline), then pass its path in file()." exit 601 } local k = ${DBB_NPANELS} + 1 global DBB_NPANELS "`k'" global DBB_P_`k'_TYPE "html" global DBB_P_`k'_TAB "`tab'" global DBB_P_`k'_TITLE `"`title'"' global DBB_P_`k'_NOTE `"`note'"' global DBB_P_`k'_INTERP `"`interp'"' global DBB_P_`k'_YTITLE "" global DBB_P_`k'_X "" global DBB_P_`k'_Y "" global DBB_P_`k'_REF "" global DBB_P_`k'_SELCOL "" global DBB_P_`k'_NROWS "0" global DBB_P_`k'_XDATE "0" global DBB_P_`k'_CAPVARS "" global DBB_P_`k'_HTMLFILE `"`hf'"' global DBB_P_`k'_HEIGHT "`height'" di as txt `" panel `k' captured: "' as res "html" /// cond(`"`title'"' != "", `" "`title'""', "") /// as txt `" — inlines `hf' (iframe, `height'px), tab(`tab')"' exit } * ---- assemble capture varlists ---- * capvars = every column this panel embeds, in display order * rawvars = value columns kept NUMERIC (never value-label decoded) local rawvars "`y' `ref' `vals'" if "`ptype'" == "table" local capvars "`vars'" else local capvars "`x' `y' `ref' `vals'" * selector column rides along when present in this dataset (enables filtering) * (exact: never let "state" abbreviation-match a variable like state2) local selcol "" if "${DBB_SEL}" != "" { capture confirm variable ${DBB_SEL}, exact if !_rc { local selcol "${DBB_SEL}" local capvars "`selcol' `capvars'" } } local capvars : list uniq capvars * x that looks like a Stata date gets a receipt warning later local xdate 0 if "`x'" != "" { local xfmt : format `x' if strpos("`xfmt'", "%t") == 1 local xdate 1 } * ---- register spec globals ---- local k = ${DBB_NPANELS} + 1 global DBB_NPANELS "`k'" global DBB_P_`k'_TYPE "`ptype'" global DBB_P_`k'_TAB "`tab'" global DBB_P_`k'_TITLE `"`title'"' global DBB_P_`k'_NOTE `"`note'"' global DBB_P_`k'_INTERP `"`interp'"' global DBB_P_`k'_YTITLE `"`ytitle'"' global DBB_P_`k'_X "`x'" global DBB_P_`k'_Y "`y' `vals'" global DBB_P_`k'_REF "`ref'" global DBB_P_`k'_SELCOL "`selcol'" global DBB_P_`k'_NROWS "`=_N'" global DBB_P_`k'_XDATE "`xdate'" global DBB_P_`k'_CAPVARS "`capvars'" * ---- snapshot the data to JSON via PyStata (stdlib only) ---- global DBB_K "`k'" global DBB_CAP "`capvars'" global DBB_CAPRAW "`rawvars'" global DBB_PYERR "" * NOTE: ado-file python one-liners run in a per-ado namespace, so reach the * script-defined engine through __main__ explicitly. capture python: import __main__; __main__._dbb_capture() if _rc { di as err "the Python helper functions are missing (was -python clear- run?)." di as err "recover with: {stata discard} then start again from {bf:dashboardbuilder init}" exit 498 } if `"${DBB_PYERR}"' != "" { di as err `"panel snapshot failed in Python: ${DBB_PYERR}"' exit 498 } local nc : word count `capvars' di as txt `" panel `k' captured: "' as res "`ptype'" as txt /// cond(`"`title'"' != "", `" "`title'""', "") /// as txt " — `=_N' rows x `nc' cols, tab(`tab')" /// cond("`selcol'" != "", " [filterable by `selcol']", "") end * ═══════════════════════════════════════════════════════════════════════════ * describe — show what has been registered so far (pre-build) * ═══════════════════════════════════════════════════════════════════════════ program define _dbb_describe _dbb_require_active di as txt "{hline 64}" di as txt "dashboardbuilder — in progress: " as res `"${DBB_TITLE}"' di as txt " theme: " as res "${DBB_THEME}" /// as txt cond("${DBB_SEL}" != "", " selector: {res}${DBB_SEL}{txt}", "") di as txt " tabs: ${DBB_NTABS} panels: ${DBB_NPANELS}" forvalues i = 1/${DBB_NPANELS} { di as txt `" `i'. [${DBB_P_`i'_TAB}] ${DBB_P_`i'_TYPE} "' /// as res `"${DBB_P_`i'_TITLE}"' /// as txt " (${DBB_P_`i'_NROWS} rows)" /// cond("${DBB_P_`i'_SELCOL}" != "", " filterable", "") } di as txt " finish with: {bf:dashboardbuilder build using yourfile.html}" di as txt "{hline 64}" end * ═══════════════════════════════════════════════════════════════════════════ * clear — abandon the current build and drop all state * ═══════════════════════════════════════════════════════════════════════════ program define _dbb_clear capture macro drop DBB_* di as txt "dashboardbuilder: state cleared." end * ═══════════════════════════════════════════════════════════════════════════ * build — assemble the single self-contained HTML file + print the receipt * ═══════════════════════════════════════════════════════════════════════════ program define _dbb_build _dbb_require_active _dbb_pysetup syntax using/ [, replace noCSV noPNG noTOOLtip PDF TRUEpdf /// CALLout(string) SOURCEnote(string) NOOPEN CORNer] if ${DBB_NPANELS} < 1 { di as err "no panels captured — add at least one {bf:dashboardbuilder panel} first" exit 198 } local using = subinstr(`"`using'"', char(92), "/", .) if strpos(`"`using'"', ".html") == 0 & strpos(`"`using'"', ".htm") == 0 { local using `"`using'.html"' } * absolutize BEFORE handing to Python (its cwd can differ from Stata's) if substr(`"`using'"', 1, 1) != "/" & strpos(`"`using'"', ":") == 0 { local using `"`c(pwd)'/`using'"' local using = subinstr(`"`using'"', char(92), "/", .) } capture confirm file `"`using'"' if !_rc & "`replace'" == "" { di as err `"file `using' already exists — add the {bf:replace} option"' exit 602 } global DBB_OUT `"`using'"' * per-panel CSV + PNG buttons and hover tooltips are ON by default * (all self-contained); noCSV / noPNG / noTOOLtip turn them off. global DBB_CSVDL = cond("`csv'" == "nocsv", "0", "1") global DBB_PNGDL = cond("`png'" == "nopng", "0", "1") global DBB_TOOLTIP = cond("`tooltip'" == "notooltip", "0", "1") global DBB_PDFDL = cond("`pdf'" != "", "1", "0") // self-contained print-to-PDF button global DBB_TRUEPDF = cond("`truepdf'" != "", "1", "0") // one-click PDF via CDN library global DBB_CORNER = cond("`corner'" != "", "1", "0") // float the PDF button(s) bottom-right global DBB_CALLOUT `"`callout'"' global DBB_SOURCES `"`sourcenote'"' global DBB_PYERR "" capture python: import __main__; __main__._dbb_assemble() if _rc { di as err "the Python helper functions are missing (was -python clear- run?)." di as err "recover with: {stata discard} then start again from {bf:dashboardbuilder init}" exit 498 } if `"${DBB_PYERR}"' != "" { di as err `"build failed in Python: ${DBB_PYERR}"' exit 498 } * ---- the receipt ---- di as txt "{hline 68}" di as res "dashboardbuilder: build receipt" di as txt "{hline 68}" di as txt " file : " as res `"${DBB_R_FILE}"' di as txt " size : " as res "${DBB_R_KB} KB" as txt " (self-contained; no internet needed to view)" di as txt " theme : " as res "${DBB_THEME}" if "${DBB_SEL}" != "" { di as txt " selector : " as res "${DBB_SEL}" as txt " — ${DBB_R_NSELOPT} options in the dropdown" /// cond(`"${DBB_REFVAL}"' != "", `"; reference = "${DBB_REFVAL}""', "") } di as txt " tabs : " as res "${DBB_NTABS}" as txt " panels : " as res "${DBB_NPANELS}" forvalues i = 1/${DBB_NPANELS} { di as txt `" `i'. [${DBB_P_`i'_TAB}] ${DBB_P_`i'_TYPE}"' /// cond(`"${DBB_P_`i'_TITLE}"' != "", `" "${DBB_P_`i'_TITLE}""', " (untitled)") /// as txt " ${DBB_P_`i'_NROWS} rows" /// cond("${DBB_P_`i'_SELCOL}" != "", " [filters with dropdown]", " [static]") } di as txt " per-panel : CSV " as res cond("${DBB_CSVDL}"=="1","on","off") /// as txt " | PNG " as res cond("${DBB_PNGDL}"=="1","on","off") /// as txt " | hover tooltips " as res cond("${DBB_TOOLTIP}"=="1","on","off") as txt " (all self-contained)" di as txt " download : save-as-PDF (print) " as res cond("${DBB_PDFDL}"=="1","on","off") /// as txt " | one-click PDF " as res cond("${DBB_TRUEPDF}"=="1","on","off") if "${DBB_TRUEPDF}" == "1" { di as txt " {err:note}: one-click PDF loads a JS library from a CDN, so that button" di as txt " needs internet — it will NOT work on an air-gapped machine (the rest of" di as txt " the dashboard still works offline). save-as-PDF (print) is fully offline." } di as txt " notes : callout " as res cond(`"${DBB_CALLOUT}"' != "","yes","no") /// as txt " | sources " as res cond(`"${DBB_SOURCES}"' != "","yes","no") di as txt "{hline 68}" di as res " what likely needs further building (this file is a STARTER):" forvalues t = 1/${DBB_R_NTODO} { di as txt `" - ${DBB_R_TODO_`t'}"' } di as txt "{hline 68}" * Open + navigate: native SMCL {browse} links. A click opens the file (or the * folder) in the default app; the path is the link text, so it also shows in * the clear to copy. {browse} takes the RAW path, never a file:// URL — a * file:// URL aborts Stata on macOS via NSURLComponents when the path holds * '@' (e.g. a Google Drive path); see _dbb_openpath. local _fw = subinstr(`"${DBB_R_FILE}"', char(92), "/", .) local _dir = substr(`"`_fw'"', 1, strrpos(`"`_fw'"', "/") - 1) di as txt " open : " as smcl `"{browse `"`_fw'"'}"' di as txt " folder : " as smcl `"{browse `"`_dir'/"'}"' di as txt `" builder state kept — rerun {bf:build} with other options, or {bf:dashboardbuilder clear}."' * auto-open is ON by default (matches sparkta2); -noopen- suppresses it. if "`noopen'" == "" _dbb_openpath `"${DBB_R_FILE}"' end * ═══════════════════════════════════════════════════════════════════════════ * pysetup — load the Python engine from dashboardbuilder.py (ships with the * package; found on the adopath or in the current folder). Loading a real .py * via -python script- avoids embedding python blocks in the ado (the program * reader cannot store them) and keeps the engine readable/maintainable. * Idempotent: re-running just redefines the same functions, which also * self-heals after a -python clear-. * ═══════════════════════════════════════════════════════════════════════════ program define _dbb_pysetup capture findfile dashboardbuilder.py if _rc { di as err "cannot find dashboardbuilder.py — it installs alongside dashboardbuilder.ado." di as err "reinstall the package, or copy dashboardbuilder.py next to the ado / into" di as err "your current folder. (searched the adopath and the working directory)" exit 601 } local eng `"`r(fn)'"' capture python script `"`eng'"', global if _rc { di as err `"failed to load the Python engine from `eng'"' di as err "check {stata python query} and see {help dashboardbuilder##python}" exit 498 } end