diff --git a/.opencode/skills/data-viz/SKILL.md b/.opencode/skills/data-viz/SKILL.md index ad6a02169..44f85fedc 100644 --- a/.opencode/skills/data-viz/SKILL.md +++ b/.opencode/skills/data-viz/SKILL.md @@ -100,6 +100,8 @@ A single insight might just be one chart with a headline and annotation. Scale c - **Responsive**: `min-h-[VALUE]` on all charts. Grid stacks on mobile - **Animation**: Entry transitions only, `duration-300` to `duration-500`. Never continuous - **Accessibility**: `aria-label` on charts, WCAG AA contrast, don't rely on color alone +- **Dynamic color safety**: When colors come from external sources (brand palettes, category maps, API data, user config), never apply them directly as text color without a contrast check. Dark colors are invisible on dark card backgrounds. Safe pattern: use the external color only for non-text elements (left border, dot, underline); always use the standard text color (white / `var(--text)`) for the label itself. If color-coded text is required, apply a minimum lightness floor: `color: hsl(from brandColor h s max(l, 60%))` +- **Icon semantics**: Verify every icon matches its label's actual meaning, not just its visual shape. Common traps: using a rising-trend icon (📈) for metrics where lower is better (latency, error rate, cost); using achievement icons (🏆) for plain counts. When in doubt, use a neutral descriptive icon over a thematic one that could mislead ### Step 5: Interactivity & Annotations @@ -133,3 +135,16 @@ A single insight might just be one chart with a headline and annotation. Scale c - Pie charts > 5 slices — use horizontal bar - Unlabeled dual y-axes — use two separate charts - Truncated bar axes — always start at zero +- Filtering or mapping over a field not confirmed to exist in the data export — an undefined field in `.filter()` or `.map()` produces empty arrays or NaN silently, not an error; always validate the exported schema matches what the chart code consumes + +## Pre-Delivery Checklist + +Before marking a dashboard complete: + +- [ ] Every tab / view activated — all charts render (no blank canvases, no unexpected 0–1 axes) +- [ ] Every field referenced in chart/filter code confirmed present in the data export +- [ ] All text readable on its background — check explicitly when colors come from external data +- [ ] All icons match their label's meaning +- [ ] Tooltips appear on hover for every chart +- [ ] No chart silently receives an empty dataset — add a visible empty state or console warning +- [ ] Mobile: grid stacks correctly, no body-level horizontal overflow diff --git a/.opencode/skills/data-viz/references/component-guide.md b/.opencode/skills/data-viz/references/component-guide.md index 8f792da07..28a6ce46d 100644 --- a/.opencode/skills/data-viz/references/component-guide.md +++ b/.opencode/skills/data-viz/references/component-guide.md @@ -392,3 +392,70 @@ const CalloutLabel = ({ viewBox, label, color = "#1e293b" }: { viewBox?: { x: nu ``` **Rules:** Never overlap data. Use `position: "insideTopRight"/"insideTopLeft"` on labels. Pair annotations with tooltips — annotation names the event, tooltip shows the value. + +--- + +## Multi-Tab Dashboard — Lazy Chart Initialization + +Charts initialized inside a hidden container (`display:none`) render blank. Chart.js, Recharts, and Nivo all read container dimensions at mount time — a hidden container measures as `0×0`. + +**Rule: never initialize a chart until its container is visible.** + +```js +// Vanilla JS pattern +var _inited = {}; + +function activateTab(name) { + // 1. make the tab visible first + document.querySelectorAll('.tab').forEach(el => el.classList.remove('active')); + document.getElementById('tab-' + name).classList.add('active'); + // 2. then initialize charts — only on first visit + if (!_inited[name]) { + _inited[name] = true; + initChartsFor(name); + } +} + +activateTab('overview'); // init the default visible tab on page load +``` + +Library-specific notes: +- **Chart.js**: canvas reads as `0×0` inside `display:none` — bars/lines never appear +- **Recharts `ResponsiveContainer`**: reads `clientWidth = 0` — chart collapses to nothing +- **Nivo `Responsive*`**: uses `ResizeObserver` — fires once at `0×0`, never re-fires on show +- **React conditional rendering**: prefer `visibility:hidden` + `position:absolute` over toggling `display:none` if you want charts to stay mounted and pre-rendered + +--- + +## Programmatic Dashboard Generation — Data-Code Separation + +When generating a standalone HTML dashboard from a script (Python, shell, etc.), never embed JSON data inside a template string that also contains JavaScript. Curly-brace collisions in f-strings / template literals cause silent JS parse failures that are hard to debug. + +**Wrong** — data and JS logic share one f-string, every `{` in JS must be escaped as `{{`: + +```python +html = f""" + +""" +``` + +**Right** — separate data from logic entirely: + +```python +# Step 1: write data to its own file — no template string needed +with open('data.js', 'w') as f: + f.write('const DATA = ' + json.dumps(data) + ';') + +# Step 2: HTML loads both files; app.js is static and never needs escaping +``` + +```html + + +``` + +Benefits: `app.js` is static and independently testable; `data.js` is regenerated without touching logic; no escaping required in either file.