A hosted TableauOps viz extension is a single self-contained React component. You write the component to a strict contract, and the host compiles it, writes the .trex, and serves it. The contract is what lets the same source run in the builder preview, in the hosted preview harness, and inside Tableau's embedded browser without a build step. This is the shape the AI builder and the MCP server both require.
#The shape
The source is exactly one function literally named Component, a normal function declaration (not arrow-assigned), preceded by exactly two metadata comment lines and nothing else. No prose, no imports, no exports, no other top-level statements.
// viz: {"type":"bar","variant":"horizontal"}
// fields: [{"id":"item","name":"Item","accepts":"dimension","max":1,"required":true,"icon":"level-of-detail","hint":"One mark per value, e.g. Sub-Category."},{"id":"value","name":"Value","accepts":"measure","max":1,"required":true,"icon":"metric","hint":"Bar length, e.g. SUM(Sales)."}]
function Component({ data, schema, theme, fields, config }) {
const rows = Array.isArray(data) ? data : [];
if (rows.length === 0) return <div style={{ padding: 20 }}>No data yet</div>;
const labelField = fields?.item || schema?.find(f => f.type === 'string')?.name;
const valueField = fields?.value || schema?.find(f => f.type === 'number')?.name;
// ... build scales, return an <svg> that fills the container ...
return <svg viewBox="0 0 800 500" style={{ width: '100%', height: '100%' }}>{/* marks */}</svg>;
}
#The two metadata lines
They are load-bearing and they are the only comments allowed in the block.
Line 1, // viz:, classifies the chart: {"type":"bar","variant":"horizontal"}, where type is one of the known chart types (bar, line, area, scatter, donut, heatmap, table, kpi, map, other) and variant is an optional one-word refinement. This is what lets the host classify the generation by chart type with no extra calls.
Line 2, // fields:, declares the Marks-card tiles. The host reads it and writes one <encoding> per entry into the .trex. Never omit it: a viz extension with no encodings loads with nowhere to drop fields and silently renders empty. Each entry carries id, name, accepts (dimension, measure, date, any), max, required, icon, and hint. See viz extension encodings for how each maps to the manifest and the two-to-four-tile cap.
#The props
The component receives a single object:
| Prop | What it is |
|---|---|
data |
Array of plain row objects, may be empty. |
schema |
[{ name, type }] describing the fields, may be undefined. |
theme |
Design tokens (palette, typography, density, radius), may be undefined. |
fields |
Your tile ids mapped to the field name the user dropped there. A max above 1 gives an array; an empty tile is undefined. |
config |
Configure-dialog values, when the extension exposes them. |
Always pick fields in this order: the tile the user filled, then the schema, then the first row's keys. Keep the schema fallback so the component still draws in a preview where there is no Marks card and fields is undefined.
#The sandbox: two globals, nothing else
The sandbox provides React (React 18) and d3 (D3 v7) as globals. Do not import them, and do not import anything else: no npm packages, no framer-motion, no recharts. JSX is fine, it is compiled before your code runs. The sandbox also has no network (fetch, XMLHttpRequest, WebSocket are all unavailable), no parent window, no localStorage or cookies, and no direct DOM access (document.querySelector, d3.select on a real node). D3 is for the math (scales, layouts, path strings from d3.arc / d3.line); React owns the render. The root element must fill its container with a viewBox and width/height of 100%, because the viz renders into whatever pane Tableau gives it.
#FAQ
#What must the function be called?
Component, exactly, and it must be a normal function declaration, not an arrow function assigned to a variable. The host looks for that name when it compiles.
#Can I import React, d3, or an npm package?
No. React (18) and d3 (v7) are provided as globals; use them directly. No imports or exports are allowed at all, and no other library is available in the sandbox.
#What happens if I omit the // fields: line?
The extension ships with no encodings, so Tableau gives the user nowhere to drop a field and the viz renders empty. It is the single most common way a hosted viz extension fails. Always include it, with two to four tiles.
#Why does my code fail when it calls fetch or localStorage?
The sandbox blocks them by design: no network, no parent window, no storage, no direct DOM. A hosted viz gets all of its data through the data and fields props, and persists nothing itself.