Skip to content

Commit 91ebfe5

Browse files
authored
skill(data-viz): add lazy init, data-code separation, color contrast, icon semantics, field validation, pre-delivery checklist (#434)
Six improvements derived from session learnings — all general, none task-specific: - component-guide: lazy chart initialization pattern for multi-tab dashboards (Chart.js/Recharts/Nivo all render blank in display:none containers) - component-guide: data-code separation for programmatic HTML generation (f-string + JS curly braces cause silent parse failures) - SKILL.md Design Principles: dynamic color safety rule for external/brand colors - SKILL.md Design Principles: icon semantics check - SKILL.md Anti-Patterns: warn against filtering on unvalidated data fields - SKILL.md: pre-delivery checklist (tabs, fields, contrast, icons, tooltips, mobile)
1 parent 167d4a7 commit 91ebfe5

File tree

2 files changed

+82
-0
lines changed

2 files changed

+82
-0
lines changed

.opencode/skills/data-viz/SKILL.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ A single insight might just be one chart with a headline and annotation. Scale c
100100
- **Responsive**: `min-h-[VALUE]` on all charts. Grid stacks on mobile
101101
- **Animation**: Entry transitions only, `duration-300` to `duration-500`. Never continuous
102102
- **Accessibility**: `aria-label` on charts, WCAG AA contrast, don't rely on color alone
103+
- **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%))`
104+
- **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
103105

104106
### Step 5: Interactivity & Annotations
105107

@@ -133,3 +135,16 @@ A single insight might just be one chart with a headline and annotation. Scale c
133135
- Pie charts > 5 slices — use horizontal bar
134136
- Unlabeled dual y-axes — use two separate charts
135137
- Truncated bar axes — always start at zero
138+
- 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
139+
140+
## Pre-Delivery Checklist
141+
142+
Before marking a dashboard complete:
143+
144+
- [ ] Every tab / view activated — all charts render (no blank canvases, no unexpected 0–1 axes)
145+
- [ ] Every field referenced in chart/filter code confirmed present in the data export
146+
- [ ] All text readable on its background — check explicitly when colors come from external data
147+
- [ ] All icons match their label's meaning
148+
- [ ] Tooltips appear on hover for every chart
149+
- [ ] No chart silently receives an empty dataset — add a visible empty state or console warning
150+
- [ ] Mobile: grid stacks correctly, no body-level horizontal overflow

.opencode/skills/data-viz/references/component-guide.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,3 +392,70 @@ const CalloutLabel = ({ viewBox, label, color = "#1e293b" }: { viewBox?: { x: nu
392392
```
393393

394394
**Rules:** Never overlap data. Use `position: "insideTopRight"/"insideTopLeft"` on labels. Pair annotations with tooltips — annotation names the event, tooltip shows the value.
395+
396+
---
397+
398+
## Multi-Tab Dashboard — Lazy Chart Initialization
399+
400+
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`.
401+
402+
**Rule: never initialize a chart until its container is visible.**
403+
404+
```js
405+
// Vanilla JS pattern
406+
var _inited = {};
407+
408+
function activateTab(name) {
409+
// 1. make the tab visible first
410+
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
411+
document.getElementById('tab-' + name).classList.add('active');
412+
// 2. then initialize charts — only on first visit
413+
if (!_inited[name]) {
414+
_inited[name] = true;
415+
initChartsFor(name);
416+
}
417+
}
418+
419+
activateTab('overview'); // init the default visible tab on page load
420+
```
421+
422+
Library-specific notes:
423+
- **Chart.js**: canvas reads as `0×0` inside `display:none` — bars/lines never appear
424+
- **Recharts `ResponsiveContainer`**: reads `clientWidth = 0` — chart collapses to nothing
425+
- **Nivo `Responsive*`**: uses `ResizeObserver` — fires once at `0×0`, never re-fires on show
426+
- **React conditional rendering**: prefer `visibility:hidden` + `position:absolute` over toggling `display:none` if you want charts to stay mounted and pre-rendered
427+
428+
---
429+
430+
## Programmatic Dashboard Generation — Data-Code Separation
431+
432+
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.
433+
434+
**Wrong** — data and JS logic share one f-string, every `{` in JS must be escaped as `{{`:
435+
436+
```python
437+
html = f"""
438+
<script>
439+
const data = {json.dumps(data)}; // fine
440+
const fn = () => {{ return x; }} // must escape — easy to miss
441+
const obj = {{ key: getValue() }}; // one missed escape = blank page
442+
</script>
443+
"""
444+
```
445+
446+
**Right** — separate data from logic entirely:
447+
448+
```python
449+
# Step 1: write data to its own file — no template string needed
450+
with open('data.js', 'w') as f:
451+
f.write('const DATA = ' + json.dumps(data) + ';')
452+
453+
# Step 2: HTML loads both files; app.js is static and never needs escaping
454+
```
455+
456+
```html
457+
<script src="data.js"></script> <!-- generated, data only -->
458+
<script src="app.js"></script> <!-- static, logic only -->
459+
```
460+
461+
Benefits: `app.js` is static and independently testable; `data.js` is regenerated without touching logic; no escaping required in either file.

0 commit comments

Comments
 (0)