Capabilities / HTML reports

Connect HTML visuals to live data

Register visual-query datasets and visuals, use the renderer API, build paged tables, and bind live interactions.

X4 Tech SolutionsUpdated November 2, 20267 min read

Build HTML visuals from registered datasets and a small renderer API. A dataset is a visual query; Unity builds and runs it, applies filters, and supplies results. Your visual's HTML, CSS, and JavaScript control how those results appear.

This guide is for precise chat instructions and source-level customization. The agent tool reference contains complete tool schemas.

Register datasets and visuals

The assistant normally uses SaveHtmlVisuals to save datasets, visual definitions, and optional page layout together. First create an HTML page with CreatePage, then inspect its current revision.

ObjectRequired fields
Datasetid, title, query, paging
Visualid, title, dataIds, html, css, script

IDs are local to the page. A dataset's query is the same visual query a native visual stores: groups and values that name model fields by fieldUid, with optional sort and top. A value is a measure, or a column with an aggregation. Tables combine only through model relationships, and output names are the result's column names. A visual's dataIds lists the datasets it reads.

A dataset saved before visual queries keeps its old SQL, read-only, and shows a "rebuild it" error until the assistant rebuilds it as a query.

This example shows the shape of a complete tool argument for a simple live KPI. Replace the version, page ID, and field IDs with inspected values.

JSON
{
  "baseVersion": 3,
  "pageId": "YOUR_PAGE_UUID",
  "data": [{
    "id": "revenue",
    "title": "Revenue total",
    "query": {"groups": [], "values": [{"name": "revenue", "field": "AMOUNT_FIELD_UID", "aggregation": "sum"}]},
    "paging": false
  }],
  "visuals": [{
    "id": "revenue-kpi",
    "title": "Revenue",
    "dataIds": ["revenue"],
    "html": "<div class='kpi-value'></div>",
    "css": "[data-unity-visual='revenue-kpi'] .kpi-value { font-size: 2rem; }",
    "script": "element.querySelector('.kpi-value').textContent = unity.format(unity.value('revenue', 'revenue'), {style: 'currency', currency: 'USD'});"
  }],
  "html": "<main><h1>Revenue</h1><section data-unity-visual='revenue-kpi'></section></main>",
  "label": "Added revenue KPI"
}

A query with no groups has nothing to filter by: its marks can be inspected, not clicked to filter. The example is a tool argument, not a standalone HTTP request.

Preserve existing work

SaveHtmlVisuals saves the supplied changes atomically. It checks each dataset's query against the report model and validates references before accepting the proposed mutation. The result shows each dataset's outputs and generated SQL.

Top-level data: [] and visuals: [] preserve existing registrations; html: null preserves the page layout. Each supplied visual, however, replaces its complete source and dataIds. Retain every dataset that visual still uses; dataIds: [] means no dataset access.

Registering a dataset elsewhere on the page or naming it in JavaScript does not grant the visual access to it.

Use PatchHtmlSource for targeted source replacements. It checks exact matches and the expected revision. Inspect again after a conflict rather than overwriting someone else's changes.

Render actual data

A visual's script is the body of a synchronous function receiving element, data, and unity. It is not a function declaration or a script tag.

data[dataId] includes columns, rows, records, outputs (each output's name, whether it is a group or a value, its type and format), optional paging metadata, and errors. records contains actual values keyed by output name and can be empty.

For a dataset named monthly with outputs Month and Revenue:

JavaScript
element.replaceChildren();
const result = data.monthly;

if (result.error) throw new Error(String(result.error));

if (!result.records.length) {
  element.textContent = "No results for these filters.";
} else {
  for (const row of result.records) {
    const mark = document.createElement("button");
    mark.textContent =
      unity.formatDate(row.Month, {month: "short", year: "numeric"}) +
      ": " + unity.format(row.Revenue, {maximumFractionDigits: 0});
    element.appendChild(mark);
    unity.bind(mark, "monthly", row, {columns: ["Month"]});
  }
}

The dataset's query groups by Month and has the value Revenue. A click on a mark filters by its group outputs (here Month); columns can narrow them, but never to a value. Append each mark inside element before binding it.

Use textContent for data values. Decimals can arrive as exact strings; formatting should change the display, not the original values passed to interaction helpers.

Return a cleanup function for listeners, timers, or observers that your code creates. Do not return a Promise from the renderer. Handle errors explicitly when starting asynchronous work.

Renderer helpers

HelperPurpose
unity.data(key)Retrieve a bound dataset result.
unity.value(key, column, row = 0)Read a single returned value.
unity.format(value, options)Format numbers and numeric strings with number-format options.
unity.formatDate(value, options)Format dates; UTC is the default.
unity.bind(element, dataId, row, options)Associate a DOM mark with an actual returned row for inspection and interaction.
unity.activate(dataId, row, options)Activate a datum from a custom handler such as canvas hit testing.
unity.inspect(dataId, row, pointElement)Open explicit inspection for a datum.
unity.table(container, options)Render Unity's searchable, sortable, paged detail table.
unity.query(dataId, options)Request a result window for a custom data view.
unity.onData(callback)React to data updates in page scripts.
unity.navigate(pageId)Request navigation to a report page.
unity.theme.get/set/onChangeSupport an authored page-local theme control.

Generic number formatting preserves nonnumeric text and represents null as an em dash. Use formatDate explicitly for human-readable dates.

A page-local theme choice can be maintained through data refreshes, but should not be treated as an account-wide preference that persists across every reopen.

Build a detail table

Register a detail query with paging: true. Group by the fields each row shows, including a unique one such as the order ID, so each row is one record:

JSON
{"groups": [{"name": "order_id", "field": "ORDER_ID_UID"}, {"name": "ordered_at", "field": "ORDERED_AT_UID"},
            {"name": "region", "field": "REGION_UID"}],
 "values": [{"name": "amount", "field": "AMOUNT_UID", "aggregation": "sum"}],
 "sort": [{"name": "ordered_at", "direction": "desc"}]}

Use your actual model fields. In the visual's HTML, create a bounded container:

HTML
<div class="details" style="height: 420px"></div>

Then use this renderer body:

JavaScript
unity.table(element.querySelector(".details"), {
  dataId: "details",
  columns: ["ordered_at", "region", "amount"]
});

A group can stay in the query while being omitted from visible columns. Clicking a row filters by all of its group outputs.

The helper provides server-side search and sorting, additional result windows, and a windowed DOM. Style its unity-table, unity-table-toolbar, unity-table-scroll, and unity-table-status classes with scoped CSS.

Query a custom result window

Custom controls can request another window against the registered query:

JavaScript
unity.query("details", {
  pageIndex: 0,
  pageSize: 200,
  sorting: [{id: "amount", desc: true}],
  search: "North"
}).then(result => {
  // Render this window; inspect result.window.hasMore for continuation.
  // This result is not a complete-dataset total.
}).catch(error => {
  if (error.name !== "AbortError") {
    element.textContent = "The requested rows could not be loaded.";
  }
});

Reset pagination after search or sorting changes. Requests from a replaced visual/data context can be cancelled with AbortError. Loading, sorting, and searching rows do not save report revisions.

Use a separate dataset for totals. Snapshot results are bounded at 10,000 rows; paged windows are also partial. A query's Top N is a real restriction, not a pagination instruction.

An aggregate can process millions of source rows while returning a few values. Pagination can reuse a temporary ordered engine result for unchanged query/filter context; it does not guarantee a new source snapshot for every window. Long reads expose loading progress and cancellation. See Query processing.

Connect interactions and filters

Bind original returned rows, not formatted labels or invented predicates. Unity builds a click's filters from the dataset's stored query and applies report, page, dataset, and runtime filter context.

Clicking a bound mark can select it and cross-filter related content. Clicking the same selection again or clearing its filter chip returns to the authored baseline. The runtime selection does not save a document edit.

A dataset's filters affect every visual that reads it; HTML visuals have no filters of their own. Report and page filters apply to every dataset. Relationships across separate source connections are not inferred.

See Filters and interactions.

Compose and validate source

Keep each live component separately registered. Put its JavaScript in the script field, scope its CSS, and reference it once where intended through data-unity-visual. Do not place a visual's own placeholder inside itself.

HTML runs with inline browser APIs in a sandbox. Use report assets for images and registered datasets for data; external fetches, remote imports, CDN dependencies, and browser storage are unavailable.

After saving, inspect execution feedback and a real page preview. Query validation cannot determine whether your labels, layout, or business interpretation are correct.

Questions about your environment? Contact us.