plotly_hierarchy and TabML: Tableau habits outside Tableau
Drill-down in a single HTML file, and a scoring service built so Tableau authors cannot run arbitrary code.
Good BI tools teach two habits. Click to go deeper. See the model's number beside the actual.
These two small projects carry those habits further. One takes drill-down to plain Python charts. The other is built to bring model scores into Tableau without letting authors run code. It has not yet run against Tableau itself.
plotly_hierarchy: drill-down in one HTML file
Plotly is a common charting library in Python teams. plotly_hierarchy, version 0.1.0, gives it the drill path that Tableau and Power BI users expect1.
- Flat rows in, a tree out.
HierarchyTree.from_recordstakes rows and an ordered list of levels, such as region, country, state and city. It sums each value up the tree. - One file out.
HierarchyChart.to_htmlwrites a single HTML page that loads Plotly from its CDN. There is no server to run. - Three views. The page switches between bars, a treemap and a sunburst.
- Familiar moves. Click a bar or slice to drill in. A breadcrumb trail jumps back, and a linked table lists the children.
The library is small: a 130-line tree module, a 98-line chart module and a 670-line page template1. Its smoke test sums seven synthetic records to 284,000, then checks the North America and USA subtotals2.
A correction owed
The library's first public demo drew a Kansas City 311 drill tree. Its volumes were invented.
My audit of the old site later checked those figures against the city's data. The demo showed 4 departments; the city's PeopleSoft-era records, pulled 2026-09-24, hold 27. It showed Public Works with 584,200 requests; those records hold 353,7873.
The library computes what it is given. That demo was given fiction. This post replaces it.
TabML: scores in Tableau through a narrow door
Tableau can call Python through an analytics extension. A calculated field using SCRIPT_REAL sends a Python source string to that service4.
Stock TabPy executes the string it receives5. Any author who can write a calculated field can then run code inside the container. TabPy can switch that endpoint off, but then every SCRIPT_ call stops working67. TabML keeps one narrow call alive.
TabML parses instead of executing. It accepts one call shape, with one or more _arg values5:
return tabpy.query('<endpoint>', _arg1, _arg2)['response']
Anything else gets an HTTP 400 that names the supported form. The cost is real: free-form Python scripts stop working5. For a security review, that trade is the point.
Design choices that protect the number
- Argument order is the contract. Tableau cannot name a
SCRIPT_REALargument, so a swapped pair returns a plausible wrong number8. The server checks the count and publishes the order. - JSON models, never pickle. A pickle file is executable code. Loading one would reopen the hole the parser closed5.
- A fixed baseline. The anomaly score's mean comes from training data, not the current view8. A filtered dashboard cannot quietly move the baseline.
- Errors that blame the right thing. A refused script gets a 400, not a 500. The calculated field is usually what is wrong, and the message says so8.
Tested over real HTTP, not mocks
The suite holds 59 tests. HTTP tests bind a real server on a spare port, with no mocks5.
The copy-and-paste snippets in the documentation run through the production parser. A broken example fails the test suite before anyone pastes it5.
Status, plainly
The three demo models are synthetic with a fixed seed: churn, order anomaly and revenue trend. Their coefficients mean nothing5.
The Dockerfile has never been built. The sample workbook has never been opened in Tableau. There is no authentication or TLS, so it belongs on the same host or a private network5.
What this means for your BI team
- Treat your analysts' habits as assets. Keep drill paths and score-beside-actual views when you move tools, wherever they end up.
- Ask who can run code through your BI stack. With stock TabPy, the answer may be every author of a calculated field.
- Test numbers, not just visuals. A drill tree can look right and still carry invented totals.
- Prefer narrow interfaces for models. One allowed call shape is easier to review than a scripting door.
Notes
tableau/plotly_hierarchy/source, v0.1.0:__init__.py,core.py,drilldown.pyandtemplates.py, whole-file line counts. The package has no README. ↩︎tableau/generate_demo_hierarchy.py:run_tests()asserts the 284,000 total, two regions, North America at 179,000 and USA at 127,000. ↩︎tableau/real-data-audit.md: drill-tree rows graded fabricated, with the department count (Q5) and the Public Works volume (Q9). Both queries read the city's PeopleSoft-era dataset, 7at3-sxhp.tableau/tableau-sources/manifest.jsondates them 2026-09-25 02:44-02:45 UTC, the evening of 2026-09-24 in Kansas City. ↩︎- Tableau, Analytics Extensions API reference: analytics extension functions call
/evaluate, sending ascriptstring and_argdata. tableau.github.io/analytics-extensions-api/docs/ae_api_ref.html, accessed 2026-10-17. ↩︎ tableau/tabml/README.md: the parse-not-exec design, the/evaluateroute, models, 59 tests, and the not-verified list. The parser intabml/protocol.pytakes the_argNvalues in order, and each model then requires one per feature. ↩︎- TabPy server configuration,
TABPY_EVALUATE_ENABLE(default true; off returns 404), tableau.github.io/TabPy/docs/server-config.html, accessed 2026-10-17. ↩︎ - TabPy source, github.com/tableau/TabPy:
tabpy/tabpy_server/app/app.pyroutes/evaluatetoEvaluationPlaneDisabledHandlerwhen that setting is off.handlers/evaluation_plane_handler.pyanswers every such request with a 404, "Ad-hoc scripts have been disabled". Accessed 2026-10-17. ↩︎ ../knowledge/modules/tabml-tableau-scoring.md: argument order, 400 versus 500, and the training-time baseline. ↩︎
Sources
Weighing a move between Tableau and Power BI?
Email Daniel