← Files DataARCHIVED FILE
skills/visualize-data/scripts/render-page-chart.mjs
3.98 KB · Nov 12, 2026 · 18:11 UTC
import { readFile, writeFile } from "node:fs/promises";
import { pathToFileURL } from "node:url";
import { parseArgs } from "node:util";
import { assertJson, inlineJson } from "./inline-chart-input.mjs";
// These are the Pages upload boundary and production sandbox's renderer path.
// Visualize owns chart validation, registration, styling, and interaction.
const maxFragmentBytes = 256 * 1024;
const rendererRoot = "https://persistent.oaistatic.com/plugins/visualize/charts/";
export function renderPageChart(input, { release, baseUrl, entry, element }) {
if (!/^[a-f0-9]{64}$/u.test(release) || baseUrl !== rendererRoot + release + "/" ||
entry !== "runtime.js" || element !== "element.js") {
throw new Error("Use the installed Visualize cdn.json with an immutable portable element release.");
}
const { id, title, description, summary, height = 280, spec, columns, rows } = input;
if (!spec?.type || !title || !id) throw new Error("Supply the reviewed Visualize chart id, title and spec.");
if (!Number.isInteger(height) || height < 120 || height > 800) throw new Error("Chart height must be 120–800.");
if (!Array.isArray(columns) || !columns.length || new Set(columns).size !== columns.length ||
columns.some((c) => typeof c !== "string" || !c.trim() || ["__proto__", "prototype", "constructor"].includes(c))) {
throw new Error("List unique, reviewed plotted columns, not the complete source schema.");
}
// The same input can build the report and source Page fragments. Send only
// explicitly reviewed columns; do not coerce, round, aggregate, or drop rows.
const tuples = rows.map((row, i) => {
const cells = Array.isArray(row) ? row : columns.map((c) => Object.hasOwn(row, c) ? row[c] : undefined);
if (cells.length !== columns.length || cells.some((v) => v !== null && typeof v !== "string" &&
typeof v !== "boolean" && !Number.isFinite(v))) {
throw new Error("Missing or non-scalar chart value in row " + (i + 1) + "; use explicit null for a gap.");
}
return cells;
});
const config = { id, title, ...(description && { description }), ...(summary && { summary }),
height, spec, columns, rows: tuples };
assertJson(config);
const tag = "viz-chart-" + release;
const url = baseUrl + element;
// Match Visualize's portable lifecycle, including its entry-load failure
// state. inlineJson also handles older hosts using string replacement.
const fragment = [
'<link rel="modulepreload" crossorigin="anonymous" href="' + baseUrl + entry + '">',
'<' + tag + ' id="page-chart" style="display:block;min-height:' + (height + 64) + 'px">',
'<div role="status" aria-label="Loading chart"></div></' + tag + '>',
'<script type="module">',
'const chart = document.getElementById("page-chart");',
'chart.config = ' + inlineJson(config) + ';',
'chart.setAttribute("aria-label", chart.config.title);',
'import("' + url + '").then(() => { chart.style.minHeight = ""; }).catch(() => {',
' chart.style.minHeight = ""; chart.setAttribute("role", "alert");',
' chart.textContent = "Chart could not be loaded. Reopen this Page or check access to the chart renderer.";',
'});',
'</script>',
].join("\n");
if (Buffer.byteLength(fragment, "utf8") > maxFragmentBytes) {
throw new Error("Chart exceeds the Pages 256 KiB UTF-8 limit; request a reviewed reduction, never truncate.");
}
return fragment;
}
if (process.argv[1] && pathToFileURL(process.argv[1]).href === import.meta.url) {
const { values: { input, manifest, output } } = parseArgs({ options: {
input: { type: "string" }, manifest: { type: "string" }, output: { type: "string" },
} });
if (!input || !manifest) throw new Error("Supply --input reviewed-chart.json --manifest Visualize/assets/charts/cdn.json");
const readJson = async (path) => JSON.parse(await readFile(path, "utf8"));
const fragment = renderPageChart(await readJson(input), await readJson(manifest));
if (output) await writeFile(output, fragment); else process.stdout.write(fragment);
}
SHA-256: 43ae118c0b84fa520f4d3205d99ca3f371afceddfd82fe43cb453de1e3f65fe4