Your chart type, on the Marks card
Know what changed in Tableau 2024.2: a viz extension replaces the marks themselves, and your page IS the viz — then stand one up.
Why it matters
Dashboard extensions live beside the vizzes; viz extensions ARE the viz. Since Tableau 2024.2, the Marks card's type dropdown — Bar, Line, Map — has an Add Extension entry, and choosing yours hands your web page the entire worksheet canvas: Tableau still does the data, the filters, the tooltips' worth of state, and you do the drawing. This is the answer to every 'Tableau can't draw X' conversation — Sankeys, network graphs, sparkline grids — without background-image hacks or floating web objects. The API is deliberately familiar: same library, same handshake, and worksheetContent.worksheet where dashboardContent.dashboard used to be.
Build me a minimal Tableau viz extension (requires Tableau 2024.2+): a viz.html that loads the Extensions API library from https://tableau.github.io/extensions-api/lib/tableau.extensions.1.latest.js, calls tableau.extensions.initializeAsync(), reads tableau.extensions.worksheetContent.worksheet, and prints the worksheet's name and its summary-data row count into the page. Generate the matching .trex using the worksheet-extension element (not dashboard-extension) with id com.tableauops.hello-viz, min-api-version 1.12, and source-location http://localhost:58072/viz.html. Then tell me the exact click-path to run it: a worksheet with a dimension and a measure in play, Marks card type dropdown, Add Extension, choose a local extension.
Step 1 — One element renames, everything transfers
Put the manifest below next to the dashboard one from base camp and the diff is two things: the root child is worksheet-extension instead of dashboard-extension, and the version floor rises to 1.12 (Tableau 2024.2, where viz extensions shipped). Same namespace, same source-location, same icon rules, same permissions story — everything base camp taught about manifests transfers untouched.
That one element changes where Tableau offers your extension: a worksheet-extension appears in the Marks card’s type dropdown on a worksheet, not in the dashboard Objects palette.
<?xml version="1.0" encoding="utf-8"?>
<manifest manifest-version="0.1"
xmlns="http://www.tableau.com/xml/extension_manifest">
<!-- worksheet-extension, not dashboard-extension: this is the
whole difference between living beside the viz and BEING it. -->
<worksheet-extension id="com.tableauops.hello-viz"
extension-version="0.1.0">
<default-locale>en_US</default-locale>
<name resource-id="name"/>
<description>A custom mark type, drawn by my page</description>
<author name="Your Name" email="you@example.com"
organization="TableauOps" website="https://tableauops.com"/>
<!-- Viz extensions shipped in 1.12 / Tableau 2024.2. -->
<min-api-version>1.12</min-api-version>
<source-location>
<url>http://localhost:58072/viz.html</url>
</source-location>
<icon>iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==</icon>
</worksheet-extension>
<resources>
<resource id="name">
<text locale="en_US">Hello, Viz</text>
</resource>
</resources>
</manifest>Step 2 — The handshake, one property over
Build a worksheet with at least a dimension and a measure in play, open the Marks card type dropdown — the one that says Automatic — and choose Add Extension, then your local .trex. Tableau swaps its marks for your page.
In code, the move is worksheetContent.worksheet where the dashboard lessons used dashboardContent.dashboard — and it is a Worksheet like any other: the same getSummaryDataReaderAsync, the same events. Which is the point: everything the reading-the-dashboard route taught is about to become your renderer’s data feed.
(async () => {
await tableau.extensions.initializeAsync();
// The one-property move: worksheetContent, not dashboardContent.
const worksheet = tableau.extensions.worksheetContent.worksheet;
const reader = await worksheet.getSummaryDataReaderAsync();
try {
document.body.textContent =
'Drawing "' + worksheet.name + '" — ' +
reader.totalRowCount + ' rows to render';
} finally {
await reader.releaseAsync();
}
})();Got what you came for? Mark the stop and the line fills in beneath you.