← Files DataARCHIVED FILE

skills/visualize-data/scripts/render-page-chart.mjs

3.98 KB · Nov 12, 2026 · 18:11 UTC

↓ Download file

See the change to this file →

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