Read the data behind any viz

Pull the summary data out of a worksheet — every row, at any size — into plain JavaScript objects you can chart, export, or send anywhere.

intermediate8 min2 steps

Why it matters

This is the reason extensions exist. The data in a dashboard is already filtered, already aggregated, already exactly what the viewer chose to look at — and getSummaryDataReaderAsync hands it to you as structured rows. Export buttons, custom charts, write-back apps: every one of them starts with this call. The reader pattern matters because dashboards get big: data arrives in 10,000-row pages, so your memory stays flat whether the viz holds 200 rows or 2 million. The old getSummaryDataAsync is deprecated for exactly that reason — it loaded everything, every time.

Prompt Recipe

Write the JavaScript for a Tableau dashboard extension that reads the summary data from a worksheet chosen by name. Use worksheet.getSummaryDataReaderAsync(), then reader.getAllPagesAsync() when the reader's totalRowCount is under 100,000 rows, and a page-by-page loop over reader.getPageAsync(i) otherwise, accumulating only a per-column min, max, and row count instead of retaining rows. Always call reader.releaseAsync() in a finally block. Convert rows to plain objects keyed by column fieldName using each cell's value and formattedValue, and log the column names, the first five rows, and the total row count.

Step 1The reader, whole

The shape of every read: open a reader, take the pages, release it. getAllPagesAsync() concatenates every page into one DataTable — the convenient form, right whenever the viz is dashboard-scale rather than warehouse-scale. Columns describe themselves via fieldName and dataType; each cell carries value (typed, for math) and formattedValue (what the viewer sees, for display). Zipping those into plain objects, as below, is the first thing nearly every extension does.

The finally around releaseAsync() is not decoration — readers hold resources on the Tableau side, and a thrown error between open and release is how extensions spring leaks.

javascript
(async () => {
  await tableau.extensions.initializeAsync();
  const dashboard = tableau.extensions.dashboardContent.dashboard;
  const ws = dashboard.worksheets[0];   // or .find(w => w.name === ...)

  const reader = await ws.getSummaryDataReaderAsync();
  try {
    const table = await reader.getAllPagesAsync();

    const columns = table.columns.map(c => c.fieldName);
    console.log('columns:', columns.join(' | '));

    // Rows as plain objects — the shape every chart lib and export wants.
    const rows = table.data.map(row =>
      Object.fromEntries(row.map((cell, i) =>
        [columns[i], cell.formattedValue])));

    console.table(rows.slice(0, 5));
    console.log(table.data.length + ' rows from "' + ws.name + '"');
  } finally {
    await reader.releaseAsync();   // always — readers hold resources
  }
})();

Step 2When the viz is huge: page by page

The reader tells you what you are facing before you commit: totalRowCount and pageCount are known the moment it opens. Past a few hundred thousand rows, holding every row is the wrong plan — walk getPageAsync(i) and reduce as you go, keeping the aggregate and discarding the page. Memory stays flat at one page no matter how large the viz.

This is the same lesson TSC’s pagination teaches on the server side: the API hands you a page at a time so that scale is a loop, not a crash.

javascript
(async () => {
  await tableau.extensions.initializeAsync();
  const ws = tableau.extensions.dashboardContent.dashboard.worksheets[0];

  // 10,000 rows per page is the default and the maximum.
  const reader = await ws.getSummaryDataReaderAsync();
  try {
    console.log(reader.totalRowCount + ' rows in ' +
                reader.pageCount + ' pages');

    let sum = 0, count = 0;
    for (let i = 0; i < reader.pageCount; i++) {
      const page = await reader.getPageAsync(i);
      const salesCol = page.columns.findIndex(
        c => c.fieldName.includes('Sales'));
      for (const row of page.data) {
        sum += Number(row[salesCol].value) || 0;
        count++;
      }
      // page goes out of scope here — memory stays one page deep
    }
    console.log('sum over ' + count + ' rows: ' + sum);
  } finally {
    await reader.releaseAsync();
  }
})();

Got what you came for? Mark the stop and the line fills in beneath you.