levineuwirth.org/static/js/viz.js

159 lines
5.2 KiB
JavaScript

/* viz.js — Vega-Lite renderer with monochrome theming and dark-mode support.
*
* Finds every <script type="application/json" class="vega-spec"> embedded by
* Filters.Viz, renders it into the parent .vega-container via Vega-Embed,
* and re-renders whenever the site's light/dark theme changes.
*
* The site-level monochrome config is always applied; scripts should not
* set colour-related Vega-Lite config keys — use encoding shape, strokeDash,
* and opacity to distinguish series instead of hue.
*/
(function () {
'use strict';
// -------------------------------------------------------------------------
// Monochrome Vega-Lite configs (matched to base.css custom properties)
// -------------------------------------------------------------------------
var LIGHT = {
background: null,
font: 'Spectral, Georgia, "Times New Roman", serif',
mark: { color: '#1a1a1a' },
axis: {
gridColor: '#cccccc',
gridOpacity: 0.6,
domainColor: '#1a1a1a',
tickColor: '#1a1a1a',
labelColor: '#1a1a1a',
titleColor: '#555555',
labelFont: 'Spectral, Georgia, serif',
titleFont: '"Fira Sans", "Helvetica Neue", Arial, sans-serif',
titleFontWeight: 'normal',
},
legend: {
labelColor: '#1a1a1a',
titleColor: '#555555',
labelFont: 'Spectral, Georgia, serif',
titleFont: '"Fira Sans", "Helvetica Neue", Arial, sans-serif',
titleFontWeight: 'normal',
},
title: {
color: '#1a1a1a',
subtitleColor: '#555555',
font: '"Fira Sans", "Helvetica Neue", Arial, sans-serif',
fontWeight: 'normal',
},
range: {
category: ['#1a1a1a', '#555555', '#888888', '#aaaaaa', '#cccccc'],
ordinal: { scheme: 'greys' },
ramp: { scheme: 'greys' },
},
view: { stroke: null },
};
var DARK = {
background: null,
font: 'Spectral, Georgia, "Times New Roman", serif',
mark: { color: '#d4d0c8' },
axis: {
gridColor: '#333333',
gridOpacity: 0.8,
domainColor: '#d4d0c8',
tickColor: '#d4d0c8',
labelColor: '#d4d0c8',
titleColor: '#8c8881',
labelFont: 'Spectral, Georgia, serif',
titleFont: '"Fira Sans", "Helvetica Neue", Arial, sans-serif',
titleFontWeight: 'normal',
},
legend: {
labelColor: '#d4d0c8',
titleColor: '#8c8881',
labelFont: 'Spectral, Georgia, serif',
titleFont: '"Fira Sans", "Helvetica Neue", Arial, sans-serif',
titleFontWeight: 'normal',
},
title: {
color: '#d4d0c8',
subtitleColor: '#8c8881',
font: '"Fira Sans", "Helvetica Neue", Arial, sans-serif',
fontWeight: 'normal',
},
range: {
category: ['#d4d0c8', '#8c8881', '#6a6660', '#444444', '#333333'],
ordinal: { scheme: 'greys' },
ramp: { scheme: 'greys' },
},
view: { stroke: null },
};
// -------------------------------------------------------------------------
// Theme detection (matches theme.js logic)
// -------------------------------------------------------------------------
function isDark() {
var t = document.documentElement.dataset.theme;
if (t === 'dark') return true;
if (t === 'light') return false;
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
function themeConfig() {
return isDark() ? DARK : LIGHT;
}
// -------------------------------------------------------------------------
// Rendering
// -------------------------------------------------------------------------
function renderOne(container) {
var spec = container._vegaSpec;
if (!spec) return;
// Always apply site theme; ignore any config baked into the spec.
var mergedSpec = Object.assign({}, spec, { config: themeConfig() });
vegaEmbed(container, mergedSpec, { actions: false, renderer: 'svg' })
.catch(function (err) { console.error('[viz]', err); });
}
function renderAll() {
var scripts = document.querySelectorAll('script.vega-spec');
for (var i = 0; i < scripts.length; i++) {
var scriptEl = scripts[i];
var container = scriptEl.closest('.vega-container');
if (!container) continue;
try {
// Store the parsed spec on the container before vegaEmbed replaces
// the container's innerHTML (which removes the <script> element).
container._vegaSpec = JSON.parse(scriptEl.textContent);
} catch (e) {
console.error('[viz] Failed to parse Vega-Lite spec:', e);
continue;
}
renderOne(container);
}
}
function reRenderAll() {
var containers = document.querySelectorAll('.vega-container');
for (var i = 0; i < containers.length; i++) {
renderOne(containers[i]);
}
}
// -------------------------------------------------------------------------
// Initialisation and theme-change listener
// -------------------------------------------------------------------------
document.addEventListener('DOMContentLoaded', renderAll);
new MutationObserver(function (mutations) {
for (var i = 0; i < mutations.length; i++) {
if (mutations[i].attributeName === 'data-theme') {
reRenderAll();
return;
}
}
}).observe(document.documentElement, { attributes: true });
}());