04 · Custom Visualizations & Extensions Overview¶
Tableau's built-in chart types cover most needs, but some requirements
(a custom control, a third-party JS chart library, writing data back to a
source system) need the Extensions API. This module is conceptual —
verification is by hand-tracing the data that would flow to/from an
extension, against the familiar Orders table.
1. What a dashboard extension is¶
- A dashboard extension is a sandboxed web application (HTML/JS)
loaded into a zone on a Tableau dashboard, communicating with the
workbook through the Extensions API (
tableau.extensions.*JavaScript library) — it can read summary/underlying data from worksheets, read/set parameters and filters, and in newer API versions write data back to a source. - Extensions run in an iframe. Tableau Desktop/Server/Cloud must explicitly enable extensions (they're off by default in locked-down deployments) and, for network-hosted extensions, an admin allow-lists the domain — a governance control covered further in Module 8.
2. Reading data into an extension: summary vs. underlying¶
worksheet.getSummaryDataAsync()returns the aggregated data currently rendered in a worksheet — e.g. a bar chart of Region vs. SUM(Sales) returns exactly 3 rows: East 2210, West 3750, Central 920.worksheet.getUnderlyingDataAsync()returns the row-level data feeding the view, before the aggregation shown on screen — for the same worksheet this returns up to 8 rows (one perOrdersrecord), because the aggregation is a rendering step, not a data reduction.- Hand-check: an extension summing the
Salesfield fromgetSummaryDataAsync()should get 2210+3750+920 = 6880. An extension summingSalesfromgetUnderlyingDataAsync()should also get 6880 (1200+450+60+800+2200+950+120+1100) — same total, different row count (3 vs. 8), which is the distinction to test for if a custom chart looks right but a downstream calculation is off by a factor tied to duplicate-counting granularity.
3. Setting a filter from an extension¶
worksheet.applyFilterAsync('Region', ['East'], FilterUpdateType.Replace)sets the Region filter to East only, from the extension's UI rather than a native Tableau filter card.- Verify the effect the same way as any Region filter: the dashboard's other worksheets (if using the same filtered field) should now total 2210, matching Section 1's East-only figure from prior modules.
4. A worked mini-extension: "reset all filters" button¶
- Requirement: a button in the extension zone that clears every filter on the dashboard back to "show all."
- Implementation sketch: iterate
tableau.extensions.dashboardContent .dashboard.worksheets, and for each worksheet callworksheet.clearFilterAsync(fieldName)for each active filter field. - Test by hand: before reset, with Region=East applied, total Sales showing is 2210. After the reset click fires, every worksheet should re-render at the full 6880 — if it shows something else (e.g. still 2210, or an intermediate 3160 = 2210+950), the reset loop missed a worksheet or a filter field.
5. When to reach for an extension vs. built-in features¶
| Need | Built-in Tableau | Extension |
|---|---|---|
| Standard chart types, filters, actions | Yes | Not needed |
| Third-party chart library (e.g. a Sankey, custom map) | No | Yes |
| Writing values back to an external system | No (until write-back APIs) | Yes |
| Custom UI controls beyond parameters/filters | Limited | Yes |
| Must work with zero admin config | Yes | No — requires enabling extensions |
How It Actually Works¶
The summary-vs-underlying distinction in Section 2 is really exposing the Stage-1/Stage-2 query model from Level 1-2 directly through the Extensions API — an extension can choose which stage of VizQL's own pipeline to read from:
getSummaryDataAsync()returns exactly the rows Stage 1'sGROUP BYquery produced for that worksheet — for the Region bar chart, this is literally the same 3-row result set (SELECT Region, SUM(Sales) GROUP BY Region) that the rendered bars are drawn from, which is why it's guaranteed to match the visible chart pixel-for-pixel in aggregate terms.getUnderlyingDataAsync()bypasses that Stage-1 aggregation and returns the pre-GROUP BYrow set VizQL would have aggregated — up to all 8Ordersrows for a sheet with no filters — which is why an extension naively summing this without re-aggregating by Region would produce a correct grand total (6880) but be unable to reproduce the 3-bar breakdown without re-implementing theGROUP BYlogic itself. This distinction is the exact reason the module flags it as a common duplicate-counting bug source: an extension author who expects "underlying data" to already be per-Region will silently get row-level granularity instead.applyFilterAsync()andclearFilterAsync()don't manipulate the rendered view directly — they inject/remove the same kind ofWHEREclause a native filter shelf action would (Level 1 Module 7), which VizQL Server then uses to regenerate and re-run that worksheet's Stage-1 query, exactly like clicking a filter card would; the extension is a programmatic trigger for the identical pipeline, not a separate data path. This is why the reset-button test (Section 4.3) is a genuine pipeline correctness check: if any worksheet'sclearFilterAsynccall is missed, that one sheet keeps querying with its oldWHEREclause while the others regenerate without it, producing exactly the kind of inconsistent intermediate total (3160) the module calls out as a bug signature.
Cheat sheet¶
| API call | Returns |
|---|---|
getSummaryDataAsync() |
Aggregated rows as rendered (e.g. 3 Region rows) |
getUnderlyingDataAsync() |
Row-level data (up to 8 Orders rows) |
applyFilterAsync(field, values, type) |
Sets a filter programmatically |
clearFilterAsync(field) |
Removes a filter |
Exercise¶
An extension calls getSummaryDataAsync() on a worksheet showing
Category vs. SUM(Profit). Using Level 1's Orders table, hand-compute the
3 rows it should return: Furniture = 180+96-40+150 = 386, Electronics =
90+330 = 420, Office Supplies = 18+42 = 60. Confirm these sum to the
workbook's total profit, 386+420+60 = 866.