smapoles's picture
Stand up chart factory static site
1f92077 verified
Raw History Blame Contribute Delete
7.06 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>chart · single view</title>
<!-- Same-origin sibling assets (this file lives at repo root). -->
<link rel="stylesheet" href="./styles/VAR_config.css">
<link rel="stylesheet" href="./styles/base.css">
<link rel="stylesheet" href="./renderers/_shared/cell.css">
<link rel="stylesheet" href="./renderers/_shared/tooltip.css">
<!-- the ONE shared import map: vendored three.js, no CDN. Root-relative. -->
<script type="importmap">
{ "imports": { "three": "./styles/vendor/three.module.js" } }
</script>
<style>
body { padding: var(--space-five); max-width: 1100px; margin: 0 auto; }
.view-head { font-size: var(--size-footnote); color: var(--text-muted);
text-transform: var(--lowercase); letter-spacing: 0.04em;
margin-bottom: var(--space-four); }
.view-head b { color: var(--text-body); }
.view-head a { color: inherit; }
.view-stamps { margin-top: var(--space-one); }
#host { container-type: inline-size; }
#err { color: var(--chart_color_biased); font-size: var(--size-body); }
/* Curated Method & Results — mirrors the .cf-method / .cf-results grammar. */
#prose { margin-top: var(--space-five); }
#prose:empty { margin-top: 0; }
.prose-note { color: var(--text-muted); font-size: var(--size-footnote);
text-transform: var(--lowercase); }
#prose h1, #prose h2, #prose h3, #prose h4 {
color: var(--text-body); margin: var(--space-four) 0 var(--space-two); }
#prose p, #prose li { color: var(--text-secondary); font-size: var(--size-body);
line-height: 1.6; }
#prose ul, #prose ol { padding-left: var(--space-four); }
#prose code { font-size: var(--size-footnote); }
/* Provenance footer. */
.provenance { margin-top: var(--space-five); padding-top: var(--space-four);
border-top: 1px solid var(--ink-15);
font-size: var(--size-footnote); color: var(--text-secondary); }
.provenance h4 { color: var(--text-body); text-transform: var(--lowercase);
letter-spacing: 0.04em; margin: 0 0 var(--space-two); }
.provenance dl { display: grid; grid-template-columns: max-content 1fr;
gap: var(--space-one) var(--space-three); margin: 0 0 var(--space-three); }
.provenance dt { color: var(--text-muted); text-transform: var(--lowercase); }
.provenance dd { margin: 0; word-break: break-word; }
.provenance dd code { font-size: 0.9em; }
.provenance ul { margin: 0; padding-left: var(--space-four); }
.provenance li { margin-bottom: var(--space-one); }
</style>
</head>
<body>
<div class="view-head">
single-chart view · <b id="chart-id">—</b> ·
<a href="gallery/index.html">← all charts</a> ·
switch with <code>?chart=&lt;id&gt;</code>
<div class="view-stamps" id="stamps"></div>
</div>
<div id="host"></div>
<div id="err"></div>
<section id="prose" aria-label="Curated method and results"></section>
<footer class="provenance" id="provenance" hidden></footer>
<script type="module">
const DEFAULT = 'dfa_loadings_panel';
const id = new URLSearchParams(location.search).get('chart') || DEFAULT;
document.getElementById('chart-id').textContent = id;
document.title = `${id} · chart`;
const host = document.getElementById('host');
const proseEl = document.getElementById('prose');
const stampsEl = document.getElementById('stamps');
const provEl = document.getElementById('provenance');
const fail = (m) => { document.getElementById('err').textContent = m; };
const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
// 3. Curated Method & Results — bare HTML fragment produced by the deploy
// script at ./charts/<id>/prose.html. Missing during dev → graceful note.
async function loadProse() {
try {
const res = await fetch(new URL(`./charts/${id}/prose.html`, import.meta.url).href);
if (!res.ok) throw new Error(String(res.status));
proseEl.innerHTML = await res.text();
} catch {
proseEl.innerHTML =
'<p class="prose-note">curated method &amp; results not yet built for this chart.</p>';
}
}
function renderStamps(meta) {
const bits = [];
if (meta?.generated_at) bits.push(`generated ${esc(meta.generated_at)}`);
if (meta?.synced_at) bits.push(`synced ${esc(meta.synced_at)}`);
stampsEl.innerHTML = bits.join(' · ');
}
// 4. Provenance footer — input_hash, params, source.table, APA citations.
function renderProvenance(meta, citations) {
const rows = [];
if (meta?.input_hash)
rows.push(`<dt>input hash</dt><dd><code>${esc(meta.input_hash)}</code></dd>`);
if (meta?.source?.table)
rows.push(`<dt>source table</dt><dd><code>${esc(meta.source.table)}</code></dd>`);
if (meta?.params)
rows.push(`<dt>params</dt><dd><code>${esc(JSON.stringify(meta.params))}</code></dd>`);
let cites = '';
if (citations && citations.length) {
const items = citations
.map((c) => `<li>${esc(c.apa ?? c.key ?? '')}</li>`)
.join('');
cites = `<h4>citations</h4><ul>${items}</ul>`;
}
if (!rows.length && !cites) return;
provEl.innerHTML =
`<h4>provenance</h4><dl>${rows.join('')}</dl>${cites}`;
provEl.hidden = false;
}
try {
const dmod = await import(new URL(`./charts/${id}/data.js`, import.meta.url).href);
const payload = dmod.payload ?? dmod.default;
if (!payload) throw new Error('data.js exports no `payload`');
// Read provenance BEFORE we strip payload.method (citations live there).
const meta = payload.meta ?? {};
const citations = payload.method?.citations ?? [];
renderStamps(meta);
renderProvenance(meta, citations);
const rid = meta.renderer;
if (!rid) throw new Error('no renderer declared in payload.meta');
const rmod = await import(new URL(`./renderers/${rid}/index.js`, import.meta.url).href);
// Suppress the TERSE engine prose: the shared renderMethodology /
// renderResults guard on presence, so removing these keeps mount() from
// drawing them. The curated fragment below is the authoritative write-up.
delete payload.method;
delete payload.results;
rmod.mount(host, payload); // mounts chart + stat cells + table only
// Belt-and-suspenders for any renderer that doesn't guard on presence:
// drop any terse Method/Results nodes it may still have produced.
host.querySelectorAll('[data-cf-method], [data-cf-results]')
.forEach((n) => n.remove());
await loadProse();
} catch (e) {
fail(`could not mount "${id}": ${e.message}. ` +
`Check the id (a folder under charts/) — e.g. ?chart=scalar_vs_topography`);
console.error(e);
}
</script>
</body>
</html>