diff --git a/changelog.d/snap125-nice-ticks.changed.md b/changelog.d/snap125-nice-ticks.changed.md new file mode 100644 index 0000000..299d522 --- /dev/null +++ b/changelog.d/snap125-nice-ticks.changed.md @@ -0,0 +1 @@ +Update recharts, design, and interactive-tools skills to require niceTicks="snap125" (Recharts v3.8.0+) instead of custom helper functions or stale enum values diff --git a/skills/documentation/policyengine-design-skill/SKILL.md b/skills/documentation/policyengine-design-skill/SKILL.md index 26d558f..13af36f 100644 --- a/skills/documentation/policyengine-design-skill/SKILL.md +++ b/skills/documentation/policyengine-design-skill/SKILL.md @@ -163,8 +163,8 @@ Standard Tailwind spacing classes (`p-4`, `gap-2`, `m-6`) use the default Tailwi import { BarChart, Bar, XAxis, YAxis, Tooltip } from "recharts"; - - + + diff --git a/skills/technical-patterns/policyengine-recharts-skill/SKILL.md b/skills/technical-patterns/policyengine-recharts-skill/SKILL.md index 62ec443..915fb9c 100644 --- a/skills/technical-patterns/policyengine-recharts-skill/SKILL.md +++ b/skills/technical-patterns/policyengine-recharts-skill/SKILL.md @@ -34,58 +34,43 @@ import { ## Nice axis ticks (CRITICAL) -Recharts' default tick generation produces ugly non-round numbers (e.g., $6,000, $28,000, $51,000). This is a known long-standing issue (recharts/recharts#2140, #777, #1164). +Recharts' default tick generation produces ugly non-round numbers. Since **v3.8.0**, Recharts has a built-in `niceTicks` prop that solves this natively. -**Always use explicit ticks with a `niceTicks()` helper:** - -```typescript -/** - * Compute nice round tick values for a chart axis starting at 0. - */ -function niceTicks(dataMax: number, targetCount: number = 5): number[] { - if (dataMax <= 0) return [0]; - const rawStep = dataMax / targetCount; - const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep))); - const normalized = rawStep / magnitude; - - let niceStep: number; - if (normalized <= 1) niceStep = 1 * magnitude; - else if (normalized <= 2) niceStep = 2 * magnitude; - else if (normalized <= 2.5) niceStep = 2.5 * magnitude; - else if (normalized <= 5) niceStep = 5 * magnitude; - else niceStep = 10 * magnitude; - - const niceMax = Math.ceil(dataMax / niceStep) * niceStep; - const ticks: number[] = []; - for (let v = 0; v <= niceMax; v += niceStep) { - ticks.push(Math.round(v * 1e10) / 1e10); - } - return ticks; -} -``` - -Apply to axes: +**Always set `niceTicks="snap125"` on every `` and ``:** ```tsx -const xMax = Math.max(...data.map(d => d.x)); -const yMax = Math.max(...data.map(d => d.y)); -const xTicks = niceTicks(xMax); -const yTicks = niceTicks(yMax); - ``` +The `snap125` algorithm snaps tick step sizes to **{1, 2, 2.5, 5} × 10^n**, producing human-friendly round labels like `0, 5, 10, 15, 20` instead of `0, 4, 8, 12, 16`. It may leave some blank space at chart edges — this is the correct trade-off for readability. + +**Do NOT:** +- Use a custom `niceTicks()` helper function — the built-in prop replaces it +- Use `niceTicks` as a bare boolean or `niceTicks="auto"` — always specify `"snap125"` explicitly +- Manually compute ticks arrays — let Recharts handle it + +**`niceTicks` enum values** (always use `"snap125"`): + +| Value | Behavior | Use? | +|-------|----------|------| +| `"snap125"` | Snaps to {1,2,2.5,5} multiples — roundest labels | **Always use this** | +| `"adaptive"` | Space-efficient, less round labels | No | +| `"auto"` | Context-dependent, mirrors v2 behavior | No | +| `"none"` | No rounding, raw d3 ticks | No | + +**Always pair with `domain={["auto", "auto"]}`** — the default domain `[0, 'auto']` clamps the minimum to 0, which breaks tick calculation for data that doesn't start at 0 (e.g., all-negative values). + ## Tooltip separator Recharts default tooltip separator is ` : ` (with leading space). Always set `separator=": "` on the Tooltip component. @@ -117,11 +102,6 @@ export default function MyChart({ data, highlightX }: { const fmt = (v: number) => v.toLocaleString("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0, }); - const xMax = Math.max(...data.map(d => d.x)); - const yMax = Math.max(...data.map(d => d.y)); - const xTicks = niceTicks(xMax); - const yTicks = niceTicks(yMax); - const highlightPoint = highlightX != null ? data.reduce((best, d) => Math.abs(d.x - highlightX) < Math.abs(best.x - highlightX) ? d : best, @@ -134,14 +114,14 @@ export default function MyChart({ data, highlightX }: { @@ -209,8 +189,8 @@ See `policyengine-design-skill` for the full token reference. ## Key rules -1. **Always use `niceTicks()`** - never rely on Recharts auto-tick generation -2. **Always set `domain={[0, max]}`** - axes must start at 0 +1. **Always set `niceTicks="snap125"`** on every `` and `` — never omit it, never use the bare boolean or `"auto"` +2. **Always set `domain={["auto", "auto"]}`** — required for `niceTicks` to compute correct domains 3. **Always set `type="number"` on XAxis** when using numeric data keys 4. **Always set `separator=": "`** on Tooltip 5. **Always wrap in `ResponsiveContainer`** with explicit height diff --git a/skills/tools-and-apis/policyengine-interactive-tools-skill/SKILL.md b/skills/tools-and-apis/policyengine-interactive-tools-skill/SKILL.md index fe69a01..32f15cb 100644 --- a/skills/tools-and-apis/policyengine-interactive-tools-skill/SKILL.md +++ b/skills/tools-and-apis/policyengine-interactive-tools-skill/SKILL.md @@ -626,15 +626,15 @@ Recharts accepts CSS variables directly via `fill` and `stroke` props: ```jsx - - + + ``` -**Always use `niceTicks`** on `` and `` — this snaps tick values to human-friendly round numbers (e.g., `[0, 5, 10, 15]` instead of `[0, 3.5, 7, 10.5]`). Accepts `true` (boolean) or enum values `'auto'`, `'nice'`, `'equidistant'`, `'none'`. Default to `niceTicks` (boolean) for simplicity. +**Always set `niceTicks="snap125"`** on every `` and ``. This snaps tick step sizes to {1, 2, 2.5, 5} × 10^n, producing human-friendly round labels like `0, 5, 10, 15, 20`. Do NOT use `niceTicks` as a bare boolean or `niceTicks="auto"` — always specify `"snap125"` explicitly. The `snap125` algorithm may leave some blank space at chart edges; this is the correct trade-off for readability. -**Always set `domain={["auto", "auto"]}`** on axes using `niceTicks` — the default recharts domain `[0, 'auto']` clamps the minimum to 0, which breaks tick calculation for data that doesn't start at 0 (e.g., all-negative values). Setting both ends to `"auto"` lets recharts compute the domain from the data. +**Always pair with `domain={["auto", "auto"]}`** — the default recharts domain `[0, 'auto']` clamps the minimum to 0, which breaks tick calculation for data that doesn't start at 0 (e.g., all-negative values). Setting both ends to `"auto"` lets recharts compute the domain from the data. **Format negative dollar values as `-$100`** not `$-100` — use a custom `tickFormatter` like: ```jsx @@ -697,7 +697,7 @@ Test API responses against Python fixtures for numerical accuracy. See `PolicyEn - [ ] **Use Tailwind classes from ui-kit theme** — no hardcoded hex colors - [ ] **Zero hardcoded font names** — all fonts via `var(--font-sans)` - [ ] Recharts charts use `fill="var(--chart-1)"` pattern for SVG props (font, colors) -- [ ] Recharts axes use `niceTicks` with `domain={["auto", "auto"]}` for human-friendly tick values +- [ ] Recharts axes use `niceTicks="snap125"` with `domain={["auto", "auto"]}` for human-friendly tick values - [ ] Negative dollar values formatted as `-$100` not `$-100` - [ ] PE logo is an actual image, not styled text - [ ] Sentence case on all UI text