Spaces:
Running
## Embed Chart Authoring Guidelines
Browse files### Quickstart (TL;DR)
- Create a single self-contained HTML fragment: root div + scoped style + IIFE script.
- Draw marks/axes in SVG; render UI (legend and controls) in HTML.
- Place legend and controls BELOW the chart (header appended after the chart). Include a legend title "Legend" and a select labeled "Metric" when relevant.
- Load data from public `/data` first, then fall back to `assets/data`.
- Use `window.ColorPalettes` for colors; stick to CSS variables for theming.
Minimal header markup:
```html
<div class="legend">
<div class="legend-title">Legend</div>
<div class="items"></div>
<!-- items populated by JS: <span class="item"><span class="swatch"></span><span>Name</span></span> -->
</div>
<div class="controls">
<div class="control-group">
<label for="metric-select-<id>">Metric</label>
<select id="metric-select-<id>"></select>
</div>
<!-- optional: other controls -->
</div>
```
See also: `d3-line-simple.html`, `d3-line-quad.html`, `d3-benchmark.html`.
Authoring rules for creating a new interactive chart as a single self-contained `.html` file under `src/content/embeds/`. These conventions are derived from `d3-bar.html`, `d3-comparison.html`, `d3-neural.html`, `d3-line.html`, and `d3-pie.html`.
### A) Colors & palettes (MANDATORY)
- Always obtain color arrays from `window.ColorPalettes`; do not hardcode palettes.
- Use the categorical/sequential/diverging helpers and the current primary color.
- If you change `--primary-color` dynamically, call `window.ColorPalettes.refresh()` so listeners update.
Usage:
```js
// Usage (with explicit counts)
const cat = window.ColorPalettes.getColors('categorical', 8);
const seq = window.ColorPalettes.getColors('sequential', 8);
const div = window.ColorPalettes.getColors('diverging', 7);
// For current primary color string
const primaryHex = window.ColorPalettes.getPrimary();
// If you change --primary-color dynamically, call refresh to notify listeners
document.documentElement.style.setProperty('--primary-color', '#6D4AFF');
window.ColorPalettes.refresh();
```
Notes:
- Keep chart accents (lines, markers, selection) aligned with `--primary-color`.
- Prefer CSS variables for fills/strokes when possible; derive series colors via `ColorPalettes`.
- Provide a graceful fallback to CSS variables if `window.ColorPalettes` is unavailable.
### B) Layout & form elements (HTML-only)
- All UI controls (labels, selects, sliders, buttons, toggles) must be plain HTML inside the root container.
- Do not draw controls with SVG; style them consistently (rounded 8px, custom caret, focus ring).
- Use `<label>` wrapping inputs for accessibility and concise text (e.g., "Metric", "Model Size").
- Manage layout with CSS inside the scoped `<style>` for the root class; avoid global rules.
### C) SVG scope: charts only; UI in HTML
- SVG is for chart primitives (marks, axes, gridlines) only.
- Put legends and controls in HTML (adjacent DOM is preferred; `foreignObject` only if necessary).
- Tooltips are HTML positioned absolutely inside the root container.
- Details: see sections 4 (controls), 5 (tooltips), 8 (legends).
### 1) File, naming, and structure
- Name files with a clear prefix and purpose: `d3-<type>.html` (e.g., `d3-scatter.html`).
- Wrap everything in a single `<div class="<root-class>">`, a `<style>` block scoped to that root class, and a `<script>` IIFE.
- Do not leak globals; do not attach anything to `window`.
- Use a unique, descriptive root class (e.g., `.d3-scatter`).
Minimal skeleton:
```html
<div class="d3-yourchart"></div>
<style>
.d3-yourchart {/* all styles scoped to the root */}
</style>
<script>
(() => {
// Optional dependency loader (e.g., D3)
const ensureD3 = (cb) => {
if (window.d3 && typeof window.d3.select === 'function') return cb();
let s = document.getElementById('d3-cdn-script');
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
s.addEventListener('load', onReady, { once: true });
if (window.d3) onReady();
};
const bootstrap = () => {
const scriptEl = document.currentScript;
// Prefer the closest previous sibling with the root class
let container = scriptEl ? scriptEl.previousElementSibling : null;
if (!(container && container.classList && container.classList.contains('d3-yourchart'))) {
// Fallback: pick the last unmounted instance in the page
const candidates = Array.from(document.querySelectorAll('.d3-yourchart'))
.filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
container = candidates[candidates.length - 1] || null;
}
if (!container) return;
if (container.dataset) {
if (container.dataset.mounted === 'true') return;
container.dataset.mounted = 'true';
}
// Tooltip (optional)
container.style.position = container.style.position || 'relative';
let tip = container.querySelector('.d3-tooltip'); let tipInner;
if (!tip) {
tip = document.createElement('div'); tip.className = 'd3-tooltip';
Object.assign(tip.style, { position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none', padding:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease' });
tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }
// SVG scaffolding (if using D3)
const svg = d3.select(container).append('svg').attr('width','100%').style('display','block');
const gRoot = svg.append('g');
// State & layout
let width = 800, height = 360; const margin = { top: 16, right: 28, bottom: 56, left: 64 };
function updateSize(){
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
width = container.clientWidth || 800;
height = Math.max(260, Math.round(width / 3));
svg.attr('width', width).attr('height', height);
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
return { innerWidth: width - margin.left - margin.right, innerHeight: height - margin.top - margin.bottom, isDark };
}
function render(){
const { innerWidth, innerHeight } = updateSize();
// ... draw/update your chart here using data joins
}
// Initial render + resize handling
render();
const rerender = () => render();
if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(container); }
else { window.addEventListener('resize', rerender); }
};
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); }
else { ensureD3(bootstrap); }
})();
</script>
```
### 2) Mounting and re-entrancy
- Select the closest previous sibling with the root class; fallback to the last unmounted matching element in the document.
- Gate with `data-mounted` to avoid double-initialization when the fragment re-runs.
- Assume the chart can appear multiple times on the same page.
### 3) Styling and theming
- Scope all rules under the root class; do not style `body`, `svg` globally.
- Use CSS variables for theme alignment: `--primary-color`, `--text-color`, `--muted-color`, `--surface-bg`, `--border-color`.
- Derive palette colors from `window.ColorPalettes` (categorical, sequential, diverging); do not hardcode arrays.
- For dark mode–aware strokes/ticks, either:
- Read `document.documentElement.getAttribute('data-theme') === 'dark'`, or
- Prefer CSS-only where possible.
- Keep backgrounds light and borders subtle; the outer card frame is handled by `HtmlEmbed.astro`.
Standard axis/tick/grid colors (global variables from `_variables.css`):
```css
/* Provided globally */
:root {
--axis-color: var(--text-color);
--tick-color: var(--muted-color);
--grid-color: rgba(0,0,0,.08);
}
[data-theme="dark"] {
--axis-color: var(--text-color);
--tick-color: var(--muted-color);
--grid-color: rgba(255,255,255,.10);
}
/* Apply inside charts */
.your-root-class .axes path,
.your-root-class .axes line { stroke: var(--axis-color); }
.your-root-class .axes text { fill: var(--tick-color); }
.your-root-class .grid line { stroke: var(--grid-color); }
```
#### 3.1) Text on fixed-colored backgrounds
- When rendering text over cells/areas with fixed background colors (that do not change with theme), compute a readable text style once from the actual background color.
- Use `window.ColorPalettes.getTextStyleForBackground(bgCss, { blend: 0.6 })` when available; avoid tying text color to dark mode toggles since the background is constant.
- Do not re-evaluate on theme toggle unless the background color itself changes.
Example:
```js
const bg = getComputedStyle(cellRect).fill; // e.g., 'rgb(12, 34, 56)'
const style = window.ColorPalettes?.getTextStyleForBackground
? window.ColorPalettes.getTextStyleForBackground(bg, { blend: 0.6 })
: { fill: 'var(--text-color)' };
textSel.style('fill', style.fill);
```
### 4) Controls (labels, selects, sliders)
- Compose controls as plain HTML elements appended inside the root container (no SVG UI).
- Style selects like in `d3-line.html`/`d3-bar.html` for consistency (rounded 8px, custom caret via data-URI, focus ring).
- Use `<label>` wrapping the input for accessibility; set concise text (e.g., "Metric", "Model Size").
#### 4.1) Required select label: "Metric"
- When a select is used to switch metrics, include a visible label above the select with the exact text "Metric".
- Preferred markup (grouped for easy vertical stacking):
```html
<div class="controls">
<div
- directive.md +10 -0
- style.css +116 -10
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div class="legend">
|
| 2 |
+
<div class="legend-title">Legend</div>
|
| 3 |
+
<div class="items"></div>
|
| 4 |
+
</div>
|
| 5 |
+
<div class="controls">
|
| 6 |
+
<div class="control-group">
|
| 7 |
+
<label for="metric-select-<id>">Metric</label>
|
| 8 |
+
<select id="metric-select-<id>"></select>
|
| 9 |
+
</div>
|
| 10 |
+
</div>
|
|
@@ -1,12 +1,118 @@
|
|
| 1 |
-
|
| 2 |
-
/*
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
-
/*
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================================ */
|
| 2 |
+
/* Design Tokens */
|
| 3 |
+
/* ============================================================================ */
|
| 4 |
+
:root {
|
| 5 |
+
/* Neutrals */
|
| 6 |
+
--neutral-600: rgb(107, 114, 128);
|
| 7 |
+
--neutral-400: rgb(185, 185, 185);
|
| 8 |
+
--neutral-300: rgb(228, 228, 228);
|
| 9 |
+
--neutral-200: rgb(245, 245, 245);
|
| 10 |
+
|
| 11 |
+
--default-font-family: Source Sans Pro, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
| 12 |
+
|
| 13 |
+
/* Brand (OKLCH base + derived states) */
|
| 14 |
+
--primary-base: oklch(0.75 0.12 47);
|
| 15 |
+
--primary-color: var(--primary-base);
|
| 16 |
+
--primary-color-hover: oklch(from var(--primary-color) calc(l - 0.05) c h);
|
| 17 |
+
--primary-color-active: oklch(from var(--primary-color) calc(l - 0.10) c h);
|
| 18 |
+
--on-primary: #ffffff;
|
| 19 |
+
|
| 20 |
+
/* Text & Surfaces */
|
| 21 |
+
--page-bg: #ffffff;
|
| 22 |
+
--text-color: rgba(0, 0, 0, .85);
|
| 23 |
+
--transparent-page-contrast: rgba(255, 255, 255, .85);
|
| 24 |
+
--muted-color: rgba(0, 0, 0, .6);
|
| 25 |
+
--border-color: rgba(0, 0, 0, .1);
|
| 26 |
+
--surface-bg: #fafafa;
|
| 27 |
+
--code-bg: #f6f8fa;
|
| 28 |
+
|
| 29 |
+
/* Links */
|
| 30 |
+
--link-underline: var(--primary-color);
|
| 31 |
+
--link-underline-hover: var(--primary-color-hover);
|
| 32 |
+
|
| 33 |
+
/* Spacing scale */
|
| 34 |
+
--spacing-1: 8px;
|
| 35 |
+
--spacing-2: 12px;
|
| 36 |
+
--spacing-3: 16px;
|
| 37 |
+
--spacing-4: 24px;
|
| 38 |
+
--spacing-5: 32px;
|
| 39 |
+
--spacing-6: 40px;
|
| 40 |
+
--spacing-7: 48px;
|
| 41 |
+
--spacing-8: 56px;
|
| 42 |
+
--spacing-9: 64px;
|
| 43 |
+
--spacing-10: 72px;
|
| 44 |
+
|
| 45 |
+
/* Custom Media aliases compiled by PostCSS */
|
| 46 |
+
@custom-media --bp-xxs (max-width: 320px);
|
| 47 |
+
@custom-media --bp-xs (max-width: 480px);
|
| 48 |
+
@custom-media --bp-sm (max-width: 640px);
|
| 49 |
+
@custom-media --bp-md (max-width: 768px);
|
| 50 |
+
@custom-media --bp-lg (max-width: 1024px);
|
| 51 |
+
@custom-media --bp-xl (max-width: 1280px);
|
| 52 |
+
@custom-media --bp-content-collapse (max-width: 1100px);
|
| 53 |
+
|
| 54 |
+
/* Layout */
|
| 55 |
+
--content-padding-x: 16px;
|
| 56 |
+
/* default page gutter */
|
| 57 |
+
--block-spacing-y: var(--spacing-4);
|
| 58 |
+
/* default vertical spacing between block components */
|
| 59 |
+
|
| 60 |
+
/* Config */
|
| 61 |
+
--palette-count: 8;
|
| 62 |
+
|
| 63 |
+
/* Button tokens */
|
| 64 |
+
--button-radius: 6px;
|
| 65 |
+
--button-padding-x: 12px;
|
| 66 |
+
--button-padding-y: 8px;
|
| 67 |
+
--button-font-size: 14px;
|
| 68 |
+
--button-icon-padding: 8px;
|
| 69 |
+
/* Big button */
|
| 70 |
+
--button-big-padding-x: 16px;
|
| 71 |
+
--button-big-padding-y: 12px;
|
| 72 |
+
--button-big-font-size: 16px;
|
| 73 |
+
--button-big-icon-padding: 12px;
|
| 74 |
+
|
| 75 |
+
/* Table tokens */
|
| 76 |
+
--table-border-radius: 8px;
|
| 77 |
+
--table-header-bg: oklch(from var(--surface-bg) calc(l - 0.02) c h);
|
| 78 |
+
--table-row-odd-bg: oklch(from var(--surface-bg) calc(l - 0.01) c h);
|
| 79 |
+
|
| 80 |
+
/* Z-index */
|
| 81 |
+
--z-base: 0;
|
| 82 |
+
--z-content: 1;
|
| 83 |
+
--z-elevated: 10;
|
| 84 |
+
--z-overlay: 1000;
|
| 85 |
+
--z-modal: 1100;
|
| 86 |
+
--z-tooltip: 1200;
|
| 87 |
+
|
| 88 |
+
/* Charts (global) */
|
| 89 |
+
--axis-color: var(--muted-color);
|
| 90 |
+
--tick-color: var(--text-color);
|
| 91 |
+
--grid-color: rgba(0, 0, 0, .08);
|
| 92 |
}
|
| 93 |
|
| 94 |
+
/* ============================================================================ */
|
| 95 |
+
/* Dark Theme Overrides */
|
| 96 |
+
/* ============================================================================ */
|
| 97 |
+
[data-theme="dark"] {
|
| 98 |
+
--page-bg: #0f1115;
|
| 99 |
+
--text-color: rgba(255, 255, 255, .9);
|
| 100 |
+
--muted-color: rgba(255, 255, 255, .7);
|
| 101 |
+
--border-color: rgba(255, 255, 255, .15);
|
| 102 |
+
--surface-bg: #12151b;
|
| 103 |
+
--code-bg: #12151b;
|
| 104 |
+
--transparent-page-contrast: rgba(0, 0, 0, .85);
|
| 105 |
+
|
| 106 |
+
/* Charts (global) */
|
| 107 |
+
--axis-color: var(--muted-color);
|
| 108 |
+
--tick-color: var(--muted-color);
|
| 109 |
+
--grid-color: rgba(255, 255, 255, .10);
|
| 110 |
+
|
| 111 |
+
/* Primary (lower L in dark) */
|
| 112 |
+
--primary-color-hover: oklch(from var(--primary-color) calc(l - 0.05) c h);
|
| 113 |
+
--primary-color-active: oklch(from var(--primary-color) calc(l - 0.10) c h);
|
| 114 |
+
--on-primary: #0f1115;
|
| 115 |
+
|
| 116 |
+
color-scheme: dark;
|
| 117 |
+
background: #0f1115;
|
| 118 |
+
}
|