← Files ClaraARCHIVED FILE
modules/scatter-bubble-analysis/assets/scatter-bubble-review-widget.html
26.6 KB · Oct 5, 2026 · 00:02 UTC
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scatter & Bubble Analysis</title>
<style>
:root {
color-scheme: light;
--paper: #f6f7f8;
--surface: #ffffff;
--surface-soft: #eef3f1;
--ink: #25221e;
--text: #4d5560;
--muted: #69727d;
--border: #d6dce2;
--border-strong: #9aa5b1;
--green: #6e8f72;
--rust: #9b5d46;
--blue: #4d6f91;
--gold: #b08a3c;
--slate: #747982;
--danger: #8f4438;
--radius: 8px;
--content: 1260px;
}
* { box-sizing: border-box; }
html, body { background: var(--paper); }
body {
margin: 0;
color: var(--text);
font-family: Aptos, "Segoe UI", -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 15px;
line-height: 1.45;
letter-spacing: 0;
text-rendering: optimizeLegibility;
}
.app {
min-height: 100vh;
padding: clamp(18px, 3vw, 42px);
}
.shell {
max-width: var(--content);
margin: 0 auto;
}
.topbar {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 24px;
align-items: start;
padding-bottom: 24px;
border-bottom: 1px solid var(--border-strong);
}
.eyebrow {
margin: 0 0 8px;
color: var(--muted);
font-size: 11px;
font-weight: 650;
letter-spacing: .08em;
text-transform: uppercase;
}
h1 {
margin: 0;
max-width: 920px;
color: var(--ink);
font-size: clamp(32px, 5vw, 62px);
font-weight: 650;
line-height: 1;
letter-spacing: 0;
}
.subtitle {
max-width: 820px;
margin: 14px 0 0;
color: var(--muted);
font-size: 16px;
}
.status {
display: inline-flex;
align-items: center;
min-height: 32px;
border: 1px solid var(--border);
border-radius: 999px;
padding: 5px 12px;
background: var(--surface);
color: var(--ink);
font-size: 12px;
font-weight: 650;
white-space: nowrap;
}
.metrics {
display: grid;
grid-template-columns: repeat(6, minmax(120px, 1fr));
border-bottom: 1px solid var(--border);
margin: 0 0 36px;
}
.metric {
min-width: 0;
padding: 18px 18px 20px 0;
}
.metric + .metric {
border-left: 1px solid var(--border);
padding-left: 18px;
}
.metric span {
display: block;
color: var(--muted);
font-size: 12px;
line-height: 1.2;
}
.metric strong {
display: block;
margin-top: 7px;
color: var(--ink);
font-size: clamp(20px, 2.1vw, 29px);
font-weight: 650;
line-height: 1.08;
overflow-wrap: normal;
word-break: normal;
hyphens: none;
font-variant-numeric: tabular-nums;
}
section {
padding: 34px 0 42px;
border-top: 1px solid var(--border);
}
.section-head {
display: grid;
grid-template-columns: minmax(220px, 340px) minmax(0, 1fr);
gap: clamp(22px, 4vw, 56px);
align-items: start;
margin-bottom: 20px;
}
h2 {
margin: 0;
color: var(--ink);
font-size: clamp(24px, 3vw, 36px);
font-weight: 640;
line-height: 1.08;
letter-spacing: 0;
}
.section-head p {
margin: 0;
max-width: 760px;
color: var(--muted);
font-size: 15px;
}
.analysis-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(260px, 330px);
gap: 24px;
align-items: stretch;
}
.plot-panel,
.side-panel {
min-width: 0;
border-top: 1px solid var(--border-strong);
padding-top: 14px;
}
.legacy-toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
color: var(--muted);
font-size: 13px;
}
.button-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
border: 1px solid var(--border-strong);
border-radius: var(--radius);
padding: 6px 11px;
background: var(--surface);
color: var(--ink);
font-weight: 650;
text-decoration: none;
white-space: nowrap;
}
.legacy-frame-shell {
overflow: hidden;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
}
.legacy-frame {
display: block;
width: 100%;
height: min(68vh, 720px);
min-height: 520px;
border: 0;
background: var(--surface);
}
.rank-list {
display: grid;
gap: 10px;
margin: 0;
padding: 0;
list-style: none;
}
.rank-item {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: 10px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
}
.rank {
width: 28px;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.rank-title {
display: block;
color: var(--ink);
font-weight: 650;
overflow-wrap: anywhere;
}
.rank-meta {
display: block;
margin-top: 2px;
color: var(--muted);
font-size: 12px;
}
.tables {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(280px, .72fr);
gap: 24px;
align-items: start;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th {
padding: 10px 8px;
border-bottom: 1px solid var(--border-strong);
color: var(--muted);
text-align: left;
font-weight: 650;
}
td {
padding: 10px 8px;
border-bottom: 1px solid var(--border);
vertical-align: top;
color: var(--text);
}
th:nth-child(n+2),
td:nth-child(n+2) {
text-align: right;
font-variant-numeric: tabular-nums;
}
td:first-child {
color: var(--ink);
font-weight: 620;
text-align: left;
}
.artifact-list {
display: grid;
gap: 8px;
}
.artifact {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: start;
padding: 10px 0;
border-bottom: 1px solid var(--border);
color: var(--text);
text-decoration: none;
}
.artifact:hover .artifact-title {
text-decoration: underline;
text-underline-offset: 3px;
}
.artifact-title {
color: var(--ink);
font-weight: 640;
overflow-wrap: anywhere;
}
.artifact-path {
display: block;
margin-top: 2px;
color: var(--muted);
font-size: 12px;
overflow-wrap: anywhere;
}
.tag {
color: var(--muted);
font-size: 12px;
white-space: nowrap;
}
.notice {
border-left: 3px solid var(--danger);
padding-left: 14px;
color: var(--muted);
max-width: 860px;
}
.empty {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 18px;
background: var(--surface);
color: var(--muted);
}
@media (max-width: 980px) {
.topbar,
.section-head,
.analysis-grid,
.tables {
grid-template-columns: 1fr;
}
.metrics {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.metric:nth-child(odd) {
border-left: 0;
padding-left: 0;
}
}
@media (max-width: 640px) {
.app { padding: 18px; }
.metrics { grid-template-columns: 1fr; }
.metric {
border-left: 0 !important;
padding-left: 0 !important;
}
}
</style>
</head>
<body>
<main class="app">
<div class="shell">
<header class="topbar">
<div>
<p class="eyebrow" id="eyebrow">Scatter & Bubble</p>
<h1 id="page-title">Relationship analysis</h1>
<p class="subtitle" id="subtitle">Loading run payload...</p>
</div>
<span class="status" id="status-pill">loading</span>
</header>
<div class="metrics" id="metrics"></div>
<section id="relationship">
<div class="section-head">
<h2 id="plot-title">Legacy Bubble Chart</h2>
<p id="plot-copy">The primary chart is the legacy Plotly HTML artifact generated by the vendored scatter and bubble charting code.</p>
</div>
<div class="analysis-grid">
<div class="plot-panel">
<div class="legacy-toolbar" id="legacy-chart-toolbar"></div>
<div id="plot"></div>
</div>
<aside class="side-panel">
<h2 id="rank-title" style="font-size: 20px; margin-bottom: 14px;">Largest dots</h2>
<ol class="rank-list" id="rank-list"></ol>
</aside>
</div>
</section>
<section id="table-section">
<div class="section-head">
<h2 id="drivers-title">Drivers and Outputs</h2>
<p id="drivers-copy">The table is the generated relationship summary. Output links point to chart, context, and report artifacts when this page is opened from a plugin run folder.</p>
</div>
<div class="tables">
<div id="driver-table"></div>
<div>
<h2 id="artifacts-title" style="font-size: 20px; margin-bottom: 14px;">Artifacts</h2>
<div class="artifact-list" id="artifact-list"></div>
</div>
</div>
</section>
<section id="caveats">
<div class="section-head">
<h2 id="run-status-title">Run Status</h2>
<p id="run-status"></p>
</div>
<p class="notice" id="caveat"></p>
</section>
</div>
</main>
<script>
const UI_COPY = {
en: {
eyebrow: "Scatter & Bubble",
pageTitle: "Relationship analysis",
plotTitle: "Legacy Bubble Chart",
rankTitle: "Largest dots",
driversTitle: "Drivers and Outputs",
driversCopy: "The table is the generated relationship summary. Output links point to chart, context, and report artifacts when this page is opened from a plugin run folder.",
artifactsTitle: "Artifacts",
runStatusTitle: "Run Status",
run: "Run",
previewFixture: "preview fixture",
ready: "ready",
metrics: ["Top dot", "Top size", "X metric", "Y metric", "Dots", "Charts written"],
plotCopy: "Legacy chart for {y} versus {x} across {count} relationship dots. Total size metric: {total}.",
noRowsPayload: "No relationship rows were included in the payload.",
noRows: "No relationship rows.",
dot: "Dot",
noArtifacts: "No output artifacts in payload.",
artifact: "Artifact",
chartKind: "chart",
contextKind: "context",
reportKind: "report",
chartsStatus: "Charts written: {written} of {attempts}. Source artifacts: {sources}.",
previewNotice: "This is the widget preview. Open the plugin-generated scatter_bubble_review.html from a run folder, or render through MCP, to see real data.",
sourceNotice: "Use the chart and context artifacts for full source context.",
plotAria: "Scatter bubble plot",
statuses: { ready_for_review: "Ready for review", preview: "Preview", empty: "Empty" },
},
es: {
eyebrow: "Dispersión y burbujas",
pageTitle: "Análisis de relaciones",
plotTitle: "Gráfico de burbujas",
rankTitle: "Puntos principales",
driversTitle: "Factores y salidas",
driversCopy: "La tabla contiene el resumen de relaciones generado. Los enlaces llevan a los gráficos, el contexto y los informes cuando esta página se abre desde la carpeta de una ejecución.",
artifactsTitle: "Artefactos",
runStatusTitle: "Estado de la ejecución",
run: "Ejecución",
previewFixture: "vista previa",
ready: "listo",
metrics: ["Punto principal", "Tamaño principal", "Métrica X", "Métrica Y", "Puntos", "Gráficos generados"],
plotCopy: "Gráfico de {y} frente a {x} para {count} puntos de relación. Métrica de tamaño total: {total}.",
noRowsPayload: "Los datos no incluyen filas de relaciones.",
noRows: "No hay filas de relaciones.",
dot: "Punto",
noArtifacts: "Los datos no incluyen artefactos de salida.",
artifact: "Artefacto",
chartKind: "gráfico",
contextKind: "contexto",
reportKind: "informe",
chartsStatus: "Gráficos generados: {written} de {attempts}. Artefactos fuente: {sources}.",
previewNotice: "Esta es la vista previa del widget. Abra scatter_bubble_review.html desde la carpeta de una ejecución o muéstrelo mediante MCP para consultar los datos reales.",
sourceNotice: "Utilice los artefactos de gráficos y contexto para consultar todas las fuentes.",
plotAria: "Gráfico de dispersión y burbujas",
statuses: { ready_for_review: "Listo para revisión", preview: "Vista previa", empty: "Vacío" },
},
};
const PREVIEW_PAYLOAD = {
widget_type: "scatter_bubble_review",
preview: true,
review_payload: {
run_id: "preview-no-run-payload",
status: "preview",
summary: {
dot_dimension: "Model",
x_metric: "Cogs",
y_metric: "Salesamount",
size_metric: "Units",
top_dot: "Mountain-200",
top_size_value: 3552,
chart_count: 5,
legacy_chart_written_count: 4,
source_artifacts_status: "preview",
},
items: [
driver("Mountain-200", 4646789, 8543618, 3552),
driver("Road-250", 3128556, 5036093, 1903),
driver("Road-150", 4023886, 6631322, 1552),
driver("Touring-1000", 2118415, 3408004, 1255),
driver("Hydration Pack", 12051, 32223, 706),
artifact("bubble.html", "chart_artifact", "Legacy bubble"),
artifact("scatter.html", "chart_artifact", "Legacy scatter"),
artifact("scatter_bubble_results.xlsx", "report_artifact", "Results workbook"),
],
item_count: 7,
},
final_artifacts: { caveats: ["Preview fixture. A plugin run page embeds the real review_payload.json."] },
};
function driver(title, x, y, size) {
return {
id: `driver-${title}`,
item_type: "relationship_driver",
title,
output_path: "scatter_bubble_summary.csv",
source_records: [{
kind: "relationship_metrics",
dot_dimension: "Model",
x_metric: "Cogs",
x_value: x,
y_metric: "Salesamount",
y_value: y,
size_metric: "Units",
size_value: size,
}],
data: { Model: title, Cogs: x, Salesamount: y, Units: size },
};
}
function artifact(path, type, title) {
return {
id: `artifact-${path}`,
item_type: type,
title,
output_path: path,
data: { path, kind: type.replace("_artifact", ""), artifact_id: title },
};
}
function payloadSource() {
return window.openai?.toolOutput
|| window.openai?.structuredContent
|| window.__SCATTER_BUBBLE_PAYLOAD__
|| PREVIEW_PAYLOAD;
}
const payload = payloadSource();
const review = payload.review_payload || PREVIEW_PAYLOAD.review_payload;
const summary = review.summary || {};
const allItems = Array.isArray(review.items) ? review.items : [];
const driverItems = allItems.filter((item) => item.item_type === "relationship_driver");
const artifactItems = allItems.filter((item) => item.item_type && item.item_type !== "relationship_driver");
const colors = ["#6e8f72", "#9b5d46", "#4d6f91", "#b08a3c", "#747982", "#7d6f92"];
function language() {
const value = review.language || payload.run_intake?.language || "en";
const code = String(value).trim().toLowerCase().replace("_", "-").split("-", 1)[0];
return code === "es" ? "es" : "en";
}
function copy() {
return UI_COPY[language()];
}
function interpolate(template, values) {
return Object.entries(values).reduce(
(text, [key, value]) => text.replaceAll(`{${key}}`, String(value)),
template,
);
}
function renderStaticCopy() {
const text = copy();
document.documentElement.lang = language();
document.title = text.pageTitle;
document.getElementById("eyebrow").textContent = text.eyebrow;
document.getElementById("page-title").textContent = text.pageTitle;
document.getElementById("plot-title").textContent = text.plotTitle;
document.getElementById("rank-title").textContent = text.rankTitle;
document.getElementById("drivers-title").textContent = text.driversTitle;
document.getElementById("drivers-copy").textContent = text.driversCopy;
document.getElementById("artifacts-title").textContent = text.artifactsTitle;
document.getElementById("run-status-title").textContent = text.runStatusTitle;
}
function esc(value) {
return String(value ?? "").replace(/[&<>"']/g, (char) => ({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
})[char]);
}
function num(value) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : 0;
}
function format(value, digits = 0) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return value == null || value === "" ? "n/a" : String(value);
return new Intl.NumberFormat(language() === "es" ? "es-ES" : undefined, { maximumFractionDigits: digits }).format(parsed);
}
function compact(value) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) return value == null ? "n/a" : String(value);
if (Math.abs(parsed) >= 1000000) return `${format(parsed / 1000000, 1)}m`;
if (Math.abs(parsed) >= 1000) return `${format(parsed / 1000, 0)}k`;
return format(parsed, 0);
}
function metric(label, value) {
return `<article class="metric"><span>${esc(label)}</span><strong>${esc(value)}</strong></article>`;
}
function relationshipRows() {
const xMetric = summary.x_metric || "x";
const yMetric = summary.y_metric || "y";
const sizeMetric = summary.size_metric || "size";
return driverItems.map((item, index) => {
const sourceRecord = (item.source_records || []).find((entry) => entry.kind === "relationship_metrics") || {};
return {
title: item.title || `${copy().dot} ${index + 1}`,
xMetric: sourceRecord.x_metric || xMetric,
yMetric: sourceRecord.y_metric || yMetric,
sizeMetric: sourceRecord.size_metric || sizeMetric,
x: num(sourceRecord.x_value ?? item.data?.[xMetric]),
y: num(sourceRecord.y_value ?? item.data?.[yMetric]),
size: num(sourceRecord.size_value ?? item.data?.[sizeMetric]),
output: item.output_path || "",
};
}).filter((row) => row.x || row.y || row.size);
}
function renderHeader(rows) {
const text = copy();
const mode = payload.preview
? text.previewFixture
: (text.statuses[review.status] || review.status || text.ready);
document.getElementById("status-pill").textContent = mode;
document.getElementById("subtitle").textContent = `${text.run} ${review.run_id || "n/a"}`;
const totals = rows.reduce((acc, row) => {
acc.x += row.x;
acc.y += row.y;
acc.size += row.size;
return acc;
}, { x: 0, y: 0, size: 0 });
document.getElementById("metrics").innerHTML = [
metric(text.metrics[0], summary.top_dot || rows[0]?.title || "n/a"),
metric(text.metrics[1], format(summary.top_size_value ?? rows[0]?.size)),
metric(text.metrics[2], summary.x_metric || rows[0]?.xMetric || "n/a"),
metric(text.metrics[3], summary.y_metric || rows[0]?.yMetric || "n/a"),
metric(text.metrics[4], rows.length),
metric(text.metrics[5], summary.legacy_chart_written_count ?? "n/a"),
].join("");
document.getElementById("plot-copy").textContent = interpolate(text.plotCopy, {
y: summary.y_metric || rows[0]?.yMetric || "Y",
x: summary.x_metric || rows[0]?.xMetric || "X",
count: rows.length,
total: format(totals.size),
});
}
function scale(domainMin, domainMax, rangeMin, rangeMax, value) {
if (domainMax <= domainMin) return (rangeMin + rangeMax) / 2;
return rangeMin + ((value - domainMin) / (domainMax - domainMin)) * (rangeMax - rangeMin);
}
function renderPlot(rows) {
if (!rows.length) {
document.getElementById("plot").innerHTML = `<div class="empty">${esc(copy().noRowsPayload)}</div>`;
return;
}
const width = 920;
const height = 560;
const margin = { left: 76, right: 34, top: 30, bottom: 70 };
const plotW = width - margin.left - margin.right;
const plotH = height - margin.top - margin.bottom;
const minX = Math.min(0, ...rows.map((row) => row.x));
const maxX = Math.max(...rows.map((row) => row.x)) * 1.08;
const minY = Math.min(0, ...rows.map((row) => row.y));
const maxY = Math.max(...rows.map((row) => row.y)) * 1.08;
const maxSize = Math.max(...rows.map((row) => row.size), 1);
const ticks = [0, 0.25, 0.5, 0.75, 1];
const grid = ticks.map((tick) => {
const x = margin.left + tick * plotW;
const y = margin.top + (1 - tick) * plotH;
return `
<line class="grid" x1="${x}" y1="${margin.top}" x2="${x}" y2="${margin.top + plotH}"></line>
<line class="grid" x1="${margin.left}" y1="${y}" x2="${margin.left + plotW}" y2="${y}"></line>
<text class="tick" x="${x}" y="${height - 34}" text-anchor="middle">${esc(compact(minX + tick * (maxX - minX)))}</text>
<text class="tick" x="${margin.left - 12}" y="${y + 4}" text-anchor="end">${esc(compact(minY + tick * (maxY - minY)))}</text>
`;
}).join("");
const dots = rows.map((row, index) => {
const x = scale(minX, maxX, margin.left, margin.left + plotW, row.x);
const y = scale(minY, maxY, margin.top + plotH, margin.top, row.y);
const radius = 7 + Math.sqrt(row.size / maxSize) * 25;
const color = colors[index % colors.length];
const labelY = Math.max(14, y - radius - 8);
return `
<circle class="dot" cx="${x}" cy="${y}" r="${radius}" fill="${color}">
<title>${esc(row.title)} | ${esc(row.xMetric)} ${format(row.x)} | ${esc(row.yMetric)} ${format(row.y)} | ${esc(row.sizeMetric)} ${format(row.size)}</title>
</circle>
<text class="dot-label" x="${x}" y="${labelY}" text-anchor="middle">${esc(row.title)}</text>
`;
}).join("");
document.getElementById("plot").innerHTML = `
<svg class="plot-svg" viewBox="0 0 ${width} ${height}" role="img" aria-label="${esc(copy().plotAria)}">
${grid}
<line class="axis" x1="${margin.left}" y1="${margin.top + plotH}" x2="${margin.left + plotW}" y2="${margin.top + plotH}"></line>
<line class="axis" x1="${margin.left}" y1="${margin.top}" x2="${margin.left}" y2="${margin.top + plotH}"></line>
${dots}
<text class="axis-label" x="${margin.left + plotW / 2}" y="${height - 12}" text-anchor="middle">${esc(rows[0].xMetric)}</text>
<text class="axis-label" transform="translate(18 ${margin.top + plotH / 2}) rotate(-90)" text-anchor="middle">${esc(rows[0].yMetric)}</text>
</svg>
`;
}
function renderRanks(rows) {
const ranked = [...rows].sort((a, b) => b.size - a.size).slice(0, 8);
document.getElementById("rank-list").innerHTML = ranked.map((row, index) => `
<li class="rank-item">
<span class="rank">${index + 1}</span>
<span>
<span class="rank-title">${esc(row.title)}</span>
<span class="rank-meta">${esc(row.sizeMetric)} ${format(row.size)} | ${esc(row.yMetric)} ${compact(row.y)}</span>
</span>
</li>
`).join("");
}
function renderTable(rows) {
if (!rows.length) {
document.getElementById("driver-table").innerHTML = `<div class="empty">${esc(copy().noRows)}</div>`;
return;
}
document.getElementById("driver-table").innerHTML = `
<table>
<thead>
<tr>
<th>${esc(copy().dot)}</th>
<th>${esc(rows[0].xMetric)}</th>
<th>${esc(rows[0].yMetric)}</th>
<th>${esc(rows[0].sizeMetric)}</th>
</tr>
</thead>
<tbody>
${rows.map((row) => `
<tr>
<td>${esc(row.title)}</td>
<td>${format(row.x)}</td>
<td>${format(row.y)}</td>
<td>${format(row.size)}</td>
</tr>
`).join("")}
</tbody>
</table>
`;
}
function artifactKind(item) {
const kind = String(item.item_type || "artifact").replace("_artifact", "").replace("_", " ");
return { chart: copy().chartKind, context: copy().contextKind, report: copy().reportKind }[kind] || kind;
}
function renderArtifacts() {
const artifacts = artifactItems.slice(0, 16);
if (!artifacts.length) {
document.getElementById("artifact-list").innerHTML = `<div class="empty">${esc(copy().noArtifacts)}</div>`;
return;
}
document.getElementById("artifact-list").innerHTML = artifacts.map((item) => {
const path = item.output_path || item.data?.path || "";
const href = path ? ` href="${esc(path)}"` : "";
return `
<a class="artifact"${href}>
<span>
<span class="artifact-title">${esc(item.title || path || copy().artifact)}</span>
<span class="artifact-path">${esc(path)}</span>
</span>
<span class="tag">${esc(artifactKind(item))}</span>
</a>
`;
}).join("");
}
function renderStatus() {
const caveats = payload.final_artifacts?.caveats || [];
const text = copy();
const charts = summary.legacy_chart_written_count ?? "n/a";
const attempts = summary.legacy_chart_attempt_count ?? "n/a";
document.getElementById("run-status").textContent = interpolate(text.chartsStatus, {
written: charts,
attempts,
sources: summary.source_artifacts_status || "n/a",
});
document.getElementById("caveat").textContent = caveats[0] || (
payload.preview
? text.previewNotice
: text.sourceNotice
);
}
const rows = relationshipRows();
renderStaticCopy();
renderHeader(rows);
renderPlot(rows);
renderRanks(rows);
renderTable(rows);
renderArtifacts();
renderStatus();
</script>
</body>
</html>
SHA-256: 3b60aacf1075f72db82121c69a67101cd65514f6045e73750dc64d929721c252