Last updated

Tableau Integration

MAIDR ships two Tableau binders. The main one is for the Tableau Embedding API v3: one call — bindTableau(viz) — reads the summary data out of an embedded <tableau-viz> worksheet or dashboard and mounts MAIDR's accessible layer beside it, adding audio sonification, text descriptions, braille output, keyboard navigation, and a description modal to a visualization that a screen reader otherwise reaches only as a static image with a tooltip. The second, bindTableauExtension(), runs the same thing inside a dashboard as a dashboard extension, for dashboards whose host page you cannot change; it is experimental until its screen-reader reach is established.

What this adapter is not. It does not draw a highlight box. A <tableau-viz> is a cross-origin <iframe>: the host page cannot read its SVG, cannot inject ARIA into it, and cannot style anything inside it. Everything the adapter knows comes from the asynchronous data API, and the only visual feedback it can produce is Tableau's own mark selection, driven from the keyboard as the reader navigates. See Limitations before you plan around it.

Quick Start

Load the Embedding API v3 library, MAIDR core, and the MAIDR Tableau adapter; place a <tableau-viz> pointing at a view; then bind once the viz reports that it is interactive.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My Tableau View</title>
    <!-- 1. The Tableau Embedding API v3 — must be type="module". -->
    <script
      type="module"
      src="https://public.tableau.com/javascripts/api/tableau.embedding.3.latest.min.js"
    ></script>
    <!-- 2. MAIDR core, then the MAIDR Tableau adapter (UMD; exposes maidrTableau). -->
    <script src="https://cdn.jsdelivr.net/npm/maidr/dist/maidr.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/maidr/dist/tableau.js"></script>
  </head>
  <body>
    <tableau-viz
      id="tableauViz"
      src="https://public.tableau.com/views/Superstore_embedded_800x800/Overview"
      toolbar="bottom"
      hide-tabs
    ></tableau-viz>

    <script type="module">
      // 3. Wait for FirstInteractive — nothing in the workbook is readable
      //    before it, and the data API rejects if you ask early.
      const viz = document.getElementById('tableauViz');
      viz.addEventListener('firstinteractive', () => {
        maidrTableau.bindTableau(viz, { title: 'Superstore Overview' });
      });
    </script>
  </body>
</html>

The binder inserts a focusable block immediately before the viz element. Tab to it (it is ahead of the iframe in DOM order, so a keyboard user reaches it before Tableau's own controls swallow focus), press Enter, and MAIDR activates with:

A dashboard of several worksheets opens in MAIDR's subplot lobby rather than inside a chart: on activation MAIDR announces how many subplots the figure has and tells you to use the arrow keys and Enter. See Keyboard Controls for the exact keys.

Two hosting notes, both from Tableau's own documentation:

How It Works

The adapter never imports a Tableau package. It duck-types the live <tableau-viz> element the page already created, so it stays independent of the Embedding library's version.

  1. Find the worksheets. viz.workbook.activeSheet is read at bind time, and again on every refresh — a tabswitched event changes which sheet the worksheets come from. A worksheet sheet is a single worksheet; a dashboard contributes dashboard.worksheets, in the order the author added them — which is also the order Tableau's own documentation says a screen reader narrates a dashboard. A story sheet is skipped with a warning (see Limitations).
  2. Read the summary data. For each worksheet the adapter calls getSummaryColumnsInfoAsync() for the columns in view order, then opens a getSummaryDataReaderAsync() and pages through it. The reader hands its columns back alphabetically, so the adapter builds a view-order-to-alphabetical index map by fieldId and remaps every row; every index downstream is a view index. The reader is always released in a finally block, and every read goes through a per-viz promise chain, because Tableau supports only one active summary-data reader at a time and a leaked one blocks the next read.
  3. Classify the columns. Each column becomes a measure or a dimension — see Supported Chart Types.
  4. Decide the trace type and build the layer. One worksheet produces exactly one MAIDR layer inside its own subplot. How those subplots are arranged depends on what the dashboard reports about itself — see Dashboard Layout.
  5. Mount. A wrapper <div> is inserted before the viz element and a React root renders MAIDR into it. The viz element itself is never moved — <tableau-viz> is a custom element and re-parenting re-runs its connectedCallback, with undocumented consequences for the iframe.

Dashboard Layout

A dashboard's worksheets become a two-dimensional subplot grid that follows the dashboard's own layout when the layout is readable and unambiguous, and a one-worksheet-per-row column whenever it is not. Both paths are always available and nothing on the page has to opt in.

The geometry comes from dashboard.objects: each object reports a position, a size, and — for a worksheet object — the worksheet inside it. The binder reads it once per re-read and hands four numbers and two flags per worksheet to the extractor, which stays pure.

How the grid is worked out. Worksheets are swept top to bottom into bands. A worksheet joins the band being built when its vertical extent overlaps the band's by at least half of the shorter of the two, and starts a new band otherwise; within a band the order is left to right. The threshold is a ratio rather than a pixel tolerance on purpose — ten pixels is generous on a 600px dashboard and meaningless on a 4000px one, and it moves again under browser zoom. Real tiled dashboards miss perfect alignment by the few pixels of an object's padding and border, which is an overlap of about 98%, so the threshold only bites on layouts that are genuinely ambiguous.

Rows are ragged by design: a dashboard with two worksheets over one wide one is a two-then-one grid, not a padded 2×2.

The bottom row of the dashboard is row 0 of the figure, so ↑ moves up the dashboard and ↓ moves down it. This is a consequence of there being no DOM to measure: MAIDR resolves a figure's visual ordering from the rendered SVG, the adapter emits no selectors because the marks are inside a cross-origin iframe, and the layout it falls back to numbers rows from the bottom. One honest cost comes with it: the ordinal in the announcement is counted in data order, so the bottom-left subplot is announced as "Subplot 1 of 6". The subplot's own title — the worksheet name — is what identifies it, and the arrows are the part that had to be spatially true.

When the column is used instead. Any one of these is enough, and each one applies to the whole figure — a partly-guessed grid is worse than an honest column:

Every case except the last two logs nothing when there was no geometry to judge; a dashboard that did report geometry and still got a column says why in one console warning.

The layout is computed when the worksheets are read. A window resize does not recompute it — the adapter re-reads on filter, parameter, data and tab changes, not on resize — for the same reason refreshes are deferred by default: rebuilding the figure under someone who is mid-navigation is worse than a layout that reflects the dashboard as it was read.

Highlighting

There is no overlay, and that is a consequence of the embedding surface rather than a gap. The marks live inside a cross-origin iframe, so their geometry is unreachable; a box drawn on the host page would be drawn from guessed coordinates.

Instead, as the reader moves, the adapter calls selectMarksByValueAsync() on the owning worksheet with the field values of the row under the cursor, using Tableau's select-replace update type. Tableau then highlights that mark with its own selection styling, exactly as a mouse click would.

The selection is cleared again in three places, so a highlight never outlives the cursor that put it there:

Three honest consequences:

Following a click

Selection runs the other way too. When someone clicks a mark in the viz — a sighted colleague pointing at a bar — Tableau fires markselectionchanged, the adapter reads the marks the selection now covers with getMarksAsync(), and matches their dimension values against the values every navigable position was read from. When exactly one position matches, MAIDR's cursor is sent there through window.maidrLive.navigateTo (see Live & Streaming Data):

Two things the API does not give the adapter, and what it does instead:

The click also changes what leaving the figure clears. Focus leaving MAIDR's block still clears the selection MAIDR made — but only the worksheets MAIDR still holds a selection in, and when focus went into the viz the clear waits one second first. The click that took the focus there may have selected a mark, and Tableau reports that a moment later; clearing on the focus change alone would remove the very mark the reader is about to be taken to. A click on a filter control, which changes no marks, leaves MAIDR's highlight for that second and no longer.

Installation

CDN (script tags)

<script
  type="module"
  src="https://public.tableau.com/javascripts/api/tableau.embedding.3.latest.min.js"
></script>
<script src="https://cdn.jsdelivr.net/npm/maidr/dist/maidr.js"></script>
<script src="https://cdn.jsdelivr.net/npm/maidr/dist/tableau.js"></script>
<script type="module">
  // Global: maidrTableau
  document.getElementById('tableauViz').addEventListener('firstinteractive', (event) => {
    maidrTableau.bindTableau(event.target);
  });
</script>

tableau.embedding.3.latest.min.js is served from your own Tableau host — public.tableau.com for Tableau Public, https://your-server/javascripts/api/… for Tableau Server, https://<pod>.online.tableau.com/javascripts/api/… for Tableau Cloud. The version-pinned Tableau CDN build at https://embedding.tableauusercontent.com/tableau.embedding.3.N.N.min.js works too, and is the only one where the version is under your control.

Unlike the other MAIDR adapters, this one cannot be demonstrated from a file:// URL, because the Embedding API refuses to load there.

ESM (modern build tooling / bundlers)

<script type="module">
  import { TableauEventType } from 'https://public.tableau.com/javascripts/api/tableau.embedding.3.latest.min.js';
  import { bindTableau } from 'https://cdn.jsdelivr.net/npm/maidr/dist/tableau.mjs';

  const viz = document.getElementById('tableauViz');
  viz.addEventListener(TableauEventType.FirstInteractive, () => bindTableau(viz));
</script>

npm

npm install maidr
import type { TableauAdapterOptions } from 'maidr/tableau';
import { bindTableau } from 'maidr/tableau';

const options: TableauAdapterOptions = { title: 'Regional sales' };

// `bindTableau` is async — it can only tell you whether any worksheet produced
// a navigable layer after the first read has finished. Top-level `await` is
// valid in an ES module, so no wrapping IIFE is needed.
const binding = await bindTableau(viz, options);
// later: binding?.dispose();

No @tableau/* package is required or installed. The adapter's Tableau types are its own structural interfaces, which is what keeps it version-independent — and what lets the same extraction code serve the Dashboard Extensions binder unchanged. They describe only the members it reads, with one exception: the visual specification is mirrored whole, member for member, against the declarations shipped in @tableau/embedding-api@3.12.1, and each of those types names the file it was read from so it can be re-verified against the package rather than against prose.

Supported Chart Types

getVisualSpecificationAsync() — the call that reports the mark type, the shelves and the encodings — is declared on both public Worksheet interfaces, Embedding and Extensions, and is implemented by the Embedding API's own Worksheet class. When the page has loaded a library build that has it, the adapter uses it and the mark type settles the chart type.

The adapter still feature-detects the call rather than assuming it, because the declaration describes the contract and not the host: the page loads whichever build of the Embedding library it chooses, and an older one predates the method. (A current library talking to an older Tableau Server can also have the method and be refused at runtime; that is caught and treated the same way.) When there is no specification, the chart type is read from the shape of the summary data instead.

Every column is first classified as a measure or a dimension:

Column Read as
isReferenced: false (a tooltip-only passenger) dropped
dataType of spatial or unknown dropped — nothing sonifiable
SUM(Sales), AVG(Profit), CNTD(Order ID), … on a numeric type measure, captioned Sales, Profit, Order ID
YEAR(Order Date), MONTH(Ship Date), WEEKDAY(…), … dimension, temporal
any other numeric column measure
anything else dimension, temporal when its type is a date

The aggregation wrapper is matched by name, and Tableau documents fieldName as not stable across languages — a French workbook yields SOMME(Ventes), which no wrapper list will match. That is why the numeric dataType is the backstop rather than the regex being the only test: such a column is still read as a measure, and the only thing that degrades is the caption, which keeps its wrapper. Nothing is ever assigned the wrong role because of a localized name.

With D the dimensions and M the measures, both in view order, the rungs are tried in this order — and the order is load-bearing:

Condition MAIDR trace Data shape
No measure at all worksheet skipped, with a warning —
Two or more measures, and every dimension is a detail dimension (as many distinct values as there are rows) Scatter (point) flat points, x = first measure, y = second, z = third when present
No dimension at all worksheet skipped, with a warning — a single aggregate has nothing to navigate —
The first dimension is temporal Line (line) nested series, one per group; a missing sample is a null gap, never a zero
One dimension Bar (bar) flat points, one per row in view order
Two or more dimensions Dodged bar (dodged_bar) nested segments, grouped by the second dimension

The scatter rung is tested before the "no dimension" skip on purpose. A worksheet with a continuous field on an axis — an unaggregated Discount, or a Sales (bin) field — has that field classified as a second measure, because every numeric column goes to the measure rung above. Such a worksheet reaches the ladder with no dimensions at all, where "every dimension is a detail dimension" is vacuously true, and numeric-x-against-numeric-y is exactly what it is. Tested the other way round, a perfectly readable view would vanish from the figure.

For the same reason the line rung asks only whether the first dimension is temporal: a continuous numeric field never arrives as a dimension, so a date-part wrapper such as YEAR(Order Date) is the only dimension that can be numeric, and it is already temporal.

A third and further dimension is ignored, with one warning naming them. Grouping always uses the second dimension.

When a visual specification is available, the mark type outranks the ladder above: bar reads as a bar or dodged bar, line as a line, area as an area, pie as a pie, square/heatmap as a heatmap when the grid is complete, and circle/shape as a scatter when there are two measures to put on the axes. Those are eight of the thirteen mark types Tableau declares; the other five are refused — see Box plots, histograms, gantt charts, treemaps and choropleths. A mark type from a later Tableau than this list knows about falls through to the ladder rather than being guessed at.

A dual-axis worksheet reports one marks card per measure. Only the active card is read, and a warning names how many there were: nothing in the API says which axis a card belongs to, or which summary-data column came from it, so merging them into one figure would be invention.

What the adapter refuses to guess

Three readings are reachable only by declaring them (see When The Heuristics Are Wrong), and each refusal has a reason worth knowing:

Box plots, histograms, gantt charts, treemaps and choropleths

None of these are ever inferred as themselves, and the reasons are real: Tableau's summary data for a box plot is the disaggregated marks, not the quartiles, and a quartile MAIDR computed itself is not the quartile Tableau drew; a gantt needs a start and an end, which the summary reports as a duration measure; a choropleth needs centroid latitude/longitude and a neighbour list the API does not expose. Nothing in a table of numbers says which of these the author drew.

What happens instead splits two ways, and the split is by mark type rather than by API surface:

When the host's library build has no getVisualSpecificationAsync at all, the first group loses its refusal too and every one of these views falls to the ladder unwarned.

The remedy is to name the worksheet. Set overrides['<worksheet>'].skip = true to leave a distribution or a geographic view out of the figure, or overrides['<worksheet>'].traceType to declare what it really is — see When The Heuristics Are Wrong.

A worksheet that yields no layer contributes no subplot, so a skipped worksheet never shifts the numbering of the ones that survive. If every worksheet is skipped, bindTableau warns once, mounts nothing, and resolves to null, leaving the page exactly as it was.

When The Heuristics Are Wrong

Everything above can be overridden per worksheet, by name, through the options object. There is one configuration channel and it is plain JavaScript — the Embedding API exposes no settings store, and a second channel before a second host exists would be an abstraction with one caller.

maidrTableau.bindTableau(viz, {
  title: 'Regional performance',
  worksheets: ['Sales by Region', 'Trend'],   // include-list, honoured in this order
  overrides: {
    'Sales by Region': {
      traceType: 'stacked_bar',               // the data cannot reveal this — say so
      x: 'Region',                            // Column.fieldName, or fieldId
      y: 'SUM(Sales)',
      z: 'Segment',
      orientation: 'horz',                    // 'horz' or 'vert' — the summary
                                              // data cannot reveal which
      // A horizontal bar puts the magnitude on x and the category on y, and an
      // explicit caption names the axis as the layer emits it.
      axes: { x: 'Sales (USD)', y: 'Region', z: 'Customer segment' },
    },
    'Trend': { title: 'Sales over time' },
    'Scratch sheet': { skip: true },
  },
});

Three rules govern how an override is honoured:

Two things the summary data never reveals, and which therefore have no default:

Refresh and Filters

The binder listens on the <tableau-viz> element, which is an ordinary DOM EventTarget (the Tableau payload arrives in event.detail):

Event String Why it matters
FirstInteractive firstinteractive the gate — nothing is readable before it
FilterChanged filterchanged quick filters and dashboard actions change the rows
ParameterChanged parameterchanged a parameter control can reshape the whole view
SummaryDataChanged summarydatachanged a data source refresh or extract update
TabSwitched tabswitched the active sheet changed, so which worksheets exist changed too
MarkSelectionChanged markselectionchanged a user clicked a mark — MAIDR's cursor follows it (see Following a click); the adapter's own selections arrive as the same event and are ignored

The four change events funnel into a single trailing-debounced re-read, 250 ms after the last one, because one dashboard filter fires several events across several worksheets and only one re-read is wanted. Each re-read re-discovers the worksheets from the active sheet (tabswitched arrives on the same path and can change which sheet they come from), clears the mark selection first so MAIDR's own selection cannot bias the data that comes back, then re-reads every bound worksheet through the same one-reader-at-a-time chain and rebuilds the figure. A refresh that throws is logged and leaves the previous figure mounted — a stale but correct figure beats a dead one.

By default the refresh is not applied while the reader is inside the chart. It is stored and picked up on the next focus-in, which is far less disruptive than rebuilding the figure under someone who is mid-navigation, and it means an idle dashboard on an auto-refreshing extract never interrupts anyone. Pass live: true to opt into in-place updating with cursor preservation instead:

maidrTableau.bindTableau(viz, { live: true });

The figure's id is captured once at bind time and reused across every refresh, so the same MAIDR instance is updated rather than replaced.

Dashboard Extension

bindTableauExtension() runs MAIDR as a zone of a Tableau dashboard rather than beside an embedded one. Use it when you author the dashboard but do not control the page it is shown on — Tableau Desktop, Tableau Server and Tableau Cloud dashboards viewed in Tableau itself. Everything after discovery is the same code bindTableau runs: the same reads, the same chart-type decisions, the same overrides, the same mark-selection bridge.

Experimental. A browser keeps the extension reachable and its keys to itself, and that is tested. Whether a Tableau dashboard and a screen reader do is not — see below before relying on it for readers who depend on it. The Embedding binder has no such open question.

Adding it to a dashboard

  1. Download maidr.trex.
  2. In a dashboard, drag an Extension object from the Objects pane, choose Access Local Extensions, and pick the file.
  3. Allow it full data when Tableau asks. The extension reads each worksheet's summary data, which the Extensions API returns only to an extension that declares it.
  4. Optionally, open the zone's Configure menu to set options.

The manifest points at https://maidr.ai/examples/tableau-extension/index.html, which is rebuilt with every release, so the extension follows the latest MAIDR. It needs Extensions API 1.10 or later — the release that added the paged summary-data reader — and uses a worksheet's visual specification when the Tableau version has it (Extensions API 1.11 and Tableau 2024.1), falling back to the summary data's shape otherwise, exactly as Supported Chart Types describes.

Tableau Server and Tableau Cloud run a network-enabled extension only after a site administrator adds its URL to the site's safe list, with full data access allowed. That is a per-site decision this adapter cannot make for you.

Hosting it yourself. Copy examples/tableau-extension/ to your own origin, serve dist/maidr.js and dist/tableau.js beside it (the pages load them from ../../dist/), and change the manifest's <url>. Tableau requires https, except for http://localhost while you develop. The pages load the Extensions API library, tableau.extensions.1.latest.min.js, pinned to 1.17.0 from Tableau's extensions-api repository through jsDelivr; vendor it instead if the site's policy wants every script on one origin.

Configuring it

The options are the same TableauAdapterOptions bindTableau takes, stored as JSON in the extension's workbook settings under the key maidr — so an author configures them once and every viewer of the workbook gets them. Settings belong to the extension instance, so two MAIDR zones on one dashboard are configured independently.

The Configure menu opens a dialog with one labelled editor for that JSON. It checks what was typed with the same parser the extension uses, parseTableauSettings, before anything is saved: invalid JSON, an unknown option, a misspelt override field, a trace type MAIDR does not have and an orientation other than 'horz' or 'vert' are all refused with a message naming the option, shown beside the editor and announced. A setting that is somehow saved invalid anyway — written by another tool, say — is not guessed at either: the extension warns in the console and uses its defaults.

{
  "title": "Regional performance",
  "worksheets": ["Sales by Region", "Trend"],
  "overrides": {
    "Sales by Region": { "traceType": "stacked_bar" },
    "Scratch sheet": { "skip": true }
  }
}

Saving rebuilds the figure from the new options at once.

What differs from the Embedding binder

What is and is not known about reaching it

A dashboard holds every extension in an <iframe>, and a reader has to be able to get into it, use it, and get out again with the keyboard alone. e2e_tests/specs/tableauExtension.spec.ts drives the built extension inside an iframe placed between two focusable controls, and establishes in a real browser that:

What it cannot establish is Tableau's half, because the dashboard there is a stand-in. These are open, and tracked in #934:

on Tableau Desktop, Server web, Cloud and Public alike. The Extensions API offers no way to request focus, so none of these can be worked around from inside the extension. Until they are answered, treat the extension as experimental and prefer the Embedding binder where you control the page.

API Reference

bindTableau(viz, options?)

Mounts MAIDR beside a <tableau-viz> element. Call it after the viz has fired firstinteractive.

Parameter Type Description
viz TableauViz The live <tableau-viz> element (document.getElementById(...), or event.target in a firstinteractive handler).
options TableauAdapterOptions? Everything below.

Returns Promise<TableauBinding | null> — await it. The call is asynchronous by necessity: whether any worksheet yields a navigable layer is only knowable once the first read has finished. It resolves to null when no worksheet produced a layer, in which case the page is left exactly as it was found and there is nothing to tab to. Null-check the awaited value, never the call itself: a promise is always truthy, so bindTableau(viz)?.dispose() neither short-circuits nor works — it throws TypeError: binding.dispose is not a function.

The resolved handle carries three members:

Member Type Description
maidr Maidr Getter for the MAIDR data currently mounted, including the onNavigate callback. A getter rather than a snapshot, because every successful refresh replaces the object wholesale.
refresh () => Promise<void> Re-read every bound worksheet and re-render, on demand. Never rejects: a read failure is logged and the previously mounted figure is left in place. Calls are serialized behind any refresh already running.
dispose () => void Unregisters every listener, cancels the pending debounce, clears each bound worksheet's mark selection, unmounts the React root and removes the wrapper element. The <tableau-viz> element is left exactly as it was found.

TableauAdapterOptions

Option Type Description
id string? Stable figure id, kept across refreshes. Defaults to maidr-tableau-<n>.
title string? Figure title announced for the whole view.
live boolean? Apply refreshes in place while the reader is inside the chart. Default false — see Refresh and Filters.
worksheets string[]? Worksheet names to include, honoured in the order written. Default: every worksheet of the active sheet.
layout 'grid' | 'column'? How a dashboard's worksheets are arranged into subplots. Default 'grid': follow the dashboard's own geometry when it is readable and unambiguous, and fall back to one worksheet per row otherwise. 'column' always uses the column. See Dashboard Layout.
overrides Record<string, TableauWorksheetOverride>? Per-worksheet configuration, keyed by worksheet name.
anchorLabel string? Text on the keyboard entry point rendered beside the viz. Defaults to Accessible chart view — press Enter, then use arrow keys. Style it with the [data-maidr-tableau-anchor] attribute selector.

Every field is JSON-serializable by design, so the same object is what the dashboard extension saves with the workbook and parses back.

TableauWorksheetOverride

Option Type Description
skip boolean? Leave this worksheet out of the figure entirely.
traceType TraceType? Force the reading. Outranks both the visual specification and the ladder; falls back with a warning if it cannot be honoured.
title string? Layer title. Defaults to the worksheet name.
x string? Column.fieldName (or fieldId) to use as the category / x axis.
y string? The measure to use as the value.
z string? The dimension to group series by.
orientation Orientation? — 'horz' or 'vert' Emitted only when set — the summary data does not reveal it. Read only by the bar family. Any other string is treated as horizontal by the model, not ignored.
stepDirection StepDirection? — 'hv', 'vh' or 'mid' Emitted only when set, for a step reading.
axes { x?: string; y?: string; z?: string }? Axis labels. Default to the resolved columns' captions — swapped along with the payload on a horizontal bar layer. An explicit caption always wins, and names the axis as the layer emits it.

bindTableauExtension(options?)

Runs MAIDR as a dashboard extension. Call it from the extension's page, after the Extensions library has loaded; it calls initializeAsync itself.

Option Type Description
container HTMLElement? Where the figure is appended. Defaults to document.body.
configureUrl string? The configuration dialog's URL, relative to the page. When set, the zone's Configure menu opens it.
settingsKey string? The settings key the options are saved under. Defaults to 'maidr'.
defaults TableauAdapterOptions? Options that apply when the saved settings leave them out. The saved settings win key by key.
extensions TableauExtensions? The Extensions API. Defaults to window.tableau.extensions.

Returns Promise<TableauExtensionBinding | null>. null when there is no Extensions API on the page, initialization failed, or the extension is not in a dashboard. Otherwise a handle with:

Member Type Description
binding TableauBinding | null The figure currently mounted — the same handle bindTableau returns — or null when the current options leave nothing to mount. Replaced whenever the saved settings change.
options TableauAdapterOptions The options the current figure was built from: defaults, then the saved settings.
configure () => Promise<void> Open the configuration dialog.
dispose () => void Remove every listener and the figure.

configureTableauExtension(options?)

Renders the configuration dialog. Call it from the page at configureUrl. Takes container, settingsKey and extensions, with the same defaults as above; the settingsKey must match the extension's. Resolves to false when there is no Extensions API or the dialog would not initialize.

parseTableauSettings(raw)

Reads a saved setting: { options } for a valid one — a missing or blank setting is { options: {} } — or { error } with a message naming what is wrong. Exported so a page or a build step can check a setting the way the extension and its dialog do.

extractTableau(snapshots, options?)

The pure half of the adapter: it takes the worksheet snapshots the reader produced and returns a TableauExtraction — { maidr, selection }, the MAIDR schema plus a SelectionIndex mapping every navigable position back to the Tableau selection criteria that address it (cells for grid positions, points for point clouds, and worksheets for which worksheet each layer id came from). Synchronous: no DOM, no React, no await. The returned maidr carries no onNavigate — the binder attaches that. Exported for tooling and tests; a page that just wants an accessible chart wants bindTableau.

Type exports

import type {
  SelectionIndex,
  TableauAdapterOptions,
  TableauBinding,
  TableauColumn,
  TableauDataType,
  TableauExtensionBinding,
  TableauExtensionDialogOptions,
  TableauExtensionOptions,
  TableauExtensions,
  TableauExtraction,
  TableauSelectionCriteria,
  TableauSettingsResult,
  TableauViz,
  TableauWorksheet,
  TableauWorksheetOverride,
  WorksheetSnapshot,
} from 'maidr/tableau';

These are minimal structural types describing only the subset of the Tableau API the adapter actually reads. Nothing here depends on a @tableau/* package.

Keyboard Controls

Once the figure is focused, the standard MAIDR shortcuts apply:

Function Key (Windows) Key (Mac)
Move between data points Arrow keys Arrow keys
Go to extremes Ctrl + Arrow Cmd + Arrow
Leave a worksheet for the dashboard's worksheets Escape (or Backspace) Escape (or Delete)
Move between those worksheets Arrow keys: Up / Down between rows, Left / Right along a row (column layout: Up / Down only) Arrow keys: Up / Down between rows, Left / Right along a row (column layout: Up / Down only)
Open the selected worksheet Enter Enter
Toggle Sonification S S
Toggle Braille Mode B B
Toggle Text Mode T T
Toggle Review Mode R R
Auto-play Ctrl + Shift + Arrow Cmd + Shift + Arrow
Stop Auto-play Ctrl Cmd

Two notes specific to this adapter:

For the full list, see the Keyboard Controls reference.

Limitations

Stated plainly, because every one of these is a place where a plausible-looking feature would have had to be guessed:

A runnable page is at tableau-bar.html; remember that it must be served over http(s) and needs a live connection to Tableau Public.

API Documentation

For the complete TypeScript API reference, see the API Documentation.