Set up a chart on your dashboard
Every chart works the same way: it finds a grid on the same page by its column headers, reads the rows and draws itself. Getting one working is mostly about the grid – the right columns, the right names, on the right page.
Build the grid
Add a grid with the chart's columns to the same page as the chart, behind it and tall.
Map your names
Tell the chart what your columns are called – the builder below writes this for you.
Paste and check
Paste the snippet into an HTML container and compare the row count under the chart with the grid.
Snippet builder
Pick a chart, type your grid's header names next to the ones it expects, and copy the snippet. Anything you leave blank uses the default name.
| Chart expects | Your header | Show as (optional) |
|---|
Copy header text from the grid itself. Spaces and capital letters don't matter.
Platforms
The charts are tested on MicroStrategy and plain web pages. Tableau, Looker, Power BI and Qlik follow each platform's own extension route; that code is a starting point and hasn't been tested here.
Works as-is · tested
The charts were built here first. They read a grid on the same dossier or document page.
- Add a grid with the chart's columns to the same page. Keep only the columns the chart needs, make it wide enough that there's no sideways scrollbar, and make it as tall as you can – the taller it is, the faster every row is read.
- Put the grid behind the chart rather than hiding it – MicroStrategy doesn't draw hidden grids, so there'd be nothing to read.
- Add an HTML container, choose HTML Text, and paste the snippet from the builder below.
- Copy header names from the grid itself, not the dataset panel – derived metrics often show a different name in the grid.
- Keep
data-grid-classanddata-scroll-classin the snippet. MicroStrategy only draws the rows on screen; these tell the chart which box to scroll so it reads them all. - Point filters and selectors at the grid – the HTML container can't be targeted, but the chart redraws whenever the grid changes.
- Check it in presentation mode. Editor mode adds panels that can get in the way.
<div data-chart-embed="time-series-trio"
data-columns='{"date":"Day","v1":"Revenue","v2":"Orders","v3":"Conversion Rate"}'
data-labels='{"v3":"Conversion"}'
data-grid-class="mstrmojo-Xtab" data-scroll-class="mstrmojo-scrollNode"
style="height:100%"></div>
<script>
(function () {
var s = document.createElement("script");
s.src = "https://viz.kaijxnes.com/charts/time-series-trio-1.1.0.js";
s.integrity = "sha384-F2pDVH7opAMJfPa3jfIzMuP8jLXHCiAQONDIo4mvAXK6GnmXvJypmeGdOX0Hhofb";
s.crossOrigin = "anonymous";
document.head.appendChild(s);
})();
</script>Works as-is · tested
Any page where you can add HTML and JavaScript next to a table – an intranet page, a static report, a CMS block. It's how the demos on this site run.
- Make sure the data is an HTML table (or grid of divs) with a header row.
- Add the placeholder and loader. Hide the table with CSS if you only want the chart – but keep it in the page.
<table>
<thead><tr><th>Date</th><th>Revenue</th><th>Orders</th></tr></thead>
<tbody>
<tr><td>2026-10-22</td><td>$12,400</td><td>380</td></tr>
<tr><td>2026-10-21</td><td>$11,950</td><td>366</td></tr>
</tbody>
</table>
<div data-chart-embed="time-series-trio"
data-columns='{"v1":"Revenue","v2":"Orders"}'
style="height:600px"></div>
<script>
(function () {
var s = document.createElement("script");
s.src = "https://viz.kaijxnes.com/charts/time-series-trio-1.1.0.js";
s.integrity = "sha384-F2pDVH7opAMJfPa3jfIzMuP8jLXHCiAQONDIo4mvAXK6GnmXvJypmeGdOX0Hhofb";
s.crossOrigin = "anonymous";
document.head.appendChild(s);
})();
</script>With an extension · not tested
Tableau dashboards don't let a script read other sheets, but a dashboard extension can ask for a worksheet's data. Write it into a table on the extension's page and the chart reads it as normal.
- Add a worksheet with the chart's fields to the dashboard (it can be small or hidden).
- Host the extension page somewhere your Tableau Server or Cloud site allows, with a
.trexmanifest pointing at it. - Use field names that match the chart's headers, or map them with
data-columns.
// Writes rows into a plain table on the page so a chart can read them.
function writeTable(headers, rows) {
var t = document.getElementById("grid");
if (!t) { t = document.createElement("table"); t.id = "grid"; document.body.appendChild(t); }
t.innerHTML = "<thead><tr>" + headers.map(function (h) { return "<th>" + h + "</th>"; }).join("") + "</tr></thead><tbody>" +
rows.map(function (r) { return "<tr>" + r.map(function (v) { return "<td>" + v + "</td>"; }).join("") + "</tr>"; }).join("") + "</tbody>";
}<!-- index.html of a Tableau dashboard extension (a .trex manifest points here) -->
<div data-chart-embed="time-series-trio" style="height:100vh"></div>
<script src="tableau.extensions.1.latest.min.js"></script> <!-- from Tableau's Extensions API download -->
<script>
tableau.extensions.initializeAsync().then(async function () {
var ws = tableau.extensions.dashboardContent.dashboard.worksheets.find(function (w) { return w.name === "Chart data"; });
var reader = await ws.getSummaryDataReaderAsync();
var table = await reader.getAllPagesAsync();
await reader.releaseAsync();
writeTable(table.columns.map(function (c) { return c.fieldName; }),
table.data.map(function (row) { return row.map(function (v) { return v.formattedValue; }); }));
loadChart(); // the script loader from the chart's snippet, wrapped in a function
});
</script>Custom visualization · not tested
A Looker custom visualization receives the query results. Write them into a table and load the chart.
- Host the file and register it under Admin → Visualizations, or in a LookML project manifest.
- Pick dimensions and measures whose labels match the chart's headers, or map them with
data-columns.
// Writes rows into a plain table on the page so a chart can read them.
function writeTable(headers, rows) {
var t = document.getElementById("grid");
if (!t) { t = document.createElement("table"); t.id = "grid"; document.body.appendChild(t); }
t.innerHTML = "<thead><tr>" + headers.map(function (h) { return "<th>" + h + "</th>"; }).join("") + "</tr></thead><tbody>" +
rows.map(function (r) { return "<tr>" + r.map(function (v) { return "<td>" + v + "</td>"; }).join("") + "</tr>"; }).join("") + "</tbody>";
}// Looker custom visualization (registered under Admin > Visualizations or in a LookML manifest)
looker.plugins.visualizations.add({
create: function (element) {
element.innerHTML = '<div data-chart-embed="time-series-trio" style="height:100%"></div>';
loadChart();
},
updateAsync: function (data, element, config, queryResponse, details, done) {
var f = queryResponse.fields.dimension_like.concat(queryResponse.fields.measure_like);
writeTable(f.map(function (x) { return x.label_short || x.label; }),
data.map(function (row) { return f.map(function (x) { return row[x.name].rendered || row[x.name].value; }); }));
done();
}
});Only as a custom visual · not tested
Power BI doesn't run JavaScript from HTML in a report, and visuals can't read each other – so a snippet can't simply be pasted in. The route is a custom visual built with the pbiviz tools, with the chart bundled inside it.
- Create a visual with
pbiviz new, add a table data role, and copy the chart script and Chart.js into the project. - In
update(), write the data view into a table, then start the chart. - Expect your organisation to need to approve custom visuals before they can be used.
// Power BI custom visual (pbiviz) – src/visual.ts
// Bundle the chart script and Chart.js into the visual: Power BI visuals run in a
// sandbox and shouldn't load scripts from the internet.
public update(options: VisualUpdateOptions) {
const t = options.dataViews[0].table;
writeTable(t.columns.map(c => c.displayName),
t.rows.map(r => r.map((v, i) => formatValue(v, t.columns[i].format))));
}With an extension · not tested
A Qlik Sense extension gets the hypercube in paint(). Same pattern: write a table, load the chart.
// Qlik Sense extension – paint() receives the hypercube
paint: function ($element, layout) {
var hc = layout.qHyperCube;
writeTable(hc.qDimensionInfo.concat(hc.qMeasureInfo).map(function (f) { return f.qFallbackTitle; }),
hc.qDataPages[0].qMatrix.map(function (row) { return row.map(function (c) { return c.qText; }); }));
if (!$element.find("[data-chart-embed]").length) { $element.append('<div data-chart-embed="time-series-trio" style="height:100%"></div>'); loadChart(); }
}Everything you can set on the placeholder
| Attribute | What it does | Example |
|---|---|---|
data-chart-embed | Which chart to draw – the chart's id. | data-chart-embed="funnel" |
data-columns | Your grid's header names, by the chart's column keys. Only list the ones that differ from the defaults. Matching ignores spaces and capitals. | data-columns='{"v1":"Revenue","units":"Customers"}' |
data-labels | Shorter names to show on the chart. Without it the chart shows your header names. | data-labels='{"v1":"Revenue (£)"}' |
data-title | Replaces the chart's heading. | data-title="Weekly trading" |
data-lower-better | Metrics where a fall is good news, such as costs – their changes show green when they go down. Gauges also accept KPI names. | data-lower-better='["v2"]' |
data-min-volume | Monthly Bars only: flags months with fewer Units than this as a less reliable reading. | data-min-volume="600" |
data-grid-class / data-scroll-class | For grids that only draw the rows on screen: the class names of the grid's container and its scroll box. On MicroStrategy use mstrmojo-Xtab and mstrmojo-scrollNode. | data-grid-class="mstrmojo-Xtab" |
style="height: …" | The chart fills its placeholder. 100% fills an HTML container; on a web page give it a height. | style="height:600px" |
Getting the grid right
- Same page. Dashboards only build the page you're looking at, so a grid on another page can't be read.
- Behind, not hidden. Hidden grids aren't drawn. Put the grid under the chart or make it small.
- Tall and wide enough. No sideways scrollbar; the taller it is, the fewer scroll steps it takes to read.
- Only the columns you need. Extra columns are ignored, but fewer columns read faster.
- Merged labels are fine. Labels shown once for a group are carried down to each row; total and subtotal rows are skipped.
- Numbers can be formatted.
$1,234,£3.4M,(23)for negatives,12.5%and1.2Kare all read. % metrics are averaged; $ and counts are added up. - Dates. Month keys like
202609,Sep 2026, or daily dates like2026-09-30,30/09/2026and30 Sep 2026. - Filters. Point filters and selectors at the grid; the chart redraws whenever the grid changes.
If it doesn't work
- "Couldn't find the grid" – one of the required headers doesn't match. The message lists the names it looked for; map yours with
data-columns. - Fewer rows than the grid – open Read details under the chart. On MicroStrategy, check the two class attributes are there and make the grid taller.
- A blank box – the platform may block scripts in HTML containers, or block
viz.kaijxnes.com(andkaijxnes.github.io, which the charts use to fetch Chart.js). Ask your admin. - "Chart.js couldn't be loaded" – both copies of Chart.js were blocked or didn't match their fingerprint, so nothing ran.
- Numbers look off – check the subtitle's row count against the grid, and whether a % metric should be averaged rather than added.
For your admin
- The chart reads only what's drawn on the page for the viewer, so their normal permissions and security filters still apply.
- Everything is calculated in the viewer's browser. Nothing is uploaded, and there are no logins, keys, cookies or API calls.
- The only downloads are the chart script and Chart.js, each pinned to an exact version and checked with an SRI hash – a changed file is refused before it runs.
- Published files never change. A new version gets a new file name, so a dashboard only changes when someone pastes a new snippet.