Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/snap125-nice-ticks.changed.md
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions skills/documentation/policyengine-design-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";

<BarChart data={data}>
<XAxis dataKey="name" style={{ fontFamily: "var(--font-sans)" }} />
<YAxis style={{ fontFamily: "var(--font-sans)" }} />
<XAxis dataKey="name" niceTicks="snap125" domain={["auto", "auto"]} style={{ fontFamily: "var(--font-sans)" }} />
<YAxis niceTicks="snap125" domain={["auto", "auto"]} style={{ fontFamily: "var(--font-sans)" }} />
<Tooltip separator=": " />
<Bar dataKey="value" fill="var(--chart-1)" />
</BarChart>
Expand Down
76 changes: 28 additions & 48 deletions skills/technical-patterns/policyengine-recharts-skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<XAxis>` and `<YAxis>`:**

```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);

<XAxis
dataKey="x"
type="number"
domain={[0, xTicks[xTicks.length - 1]]}
ticks={xTicks}
niceTicks="snap125"
domain={["auto", "auto"]}
tickFormatter={tickFormatter}
/>
<YAxis
domain={[0, yTicks[yTicks.length - 1]]}
ticks={yTicks}
niceTicks="snap125"
domain={["auto", "auto"]}
tickFormatter={tickFormatter}
/>
```

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.
Expand Down Expand Up @@ -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,
Expand All @@ -134,14 +114,14 @@ export default function MyChart({ data, highlightX }: {
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3" />
<XAxis
dataKey="x" type="number"
domain={[0, xTicks[xTicks.length - 1]]} ticks={xTicks}
niceTicks="snap125" domain={["auto", "auto"]}
tickFormatter={fmt}
tick={{ fontFamily: "var(--font-sans)", fontSize: 12 }}
>
<Label value="X axis" position="bottom" offset={0} />
</XAxis>
<YAxis
domain={[0, yTicks[yTicks.length - 1]]} ticks={yTicks}
niceTicks="snap125" domain={["auto", "auto"]}
tickFormatter={fmt}
tick={{ fontFamily: "var(--font-sans)", fontSize: 12 }}
>
Expand Down Expand Up @@ -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 `<XAxis>` and `<YAxis>` β€” 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -626,15 +626,15 @@ Recharts accepts CSS variables directly via `fill` and `stroke` props:
```jsx
<BarChart data={data}>
<CartesianGrid stroke="var(--border)" />
<XAxis niceTicks domain={["auto", "auto"]} tick={{ fontSize: 12, fontFamily: "var(--font-sans)" }} />
<YAxis niceTicks domain={["auto", "auto"]} tick={{ fontSize: 12, fontFamily: "var(--font-sans)" }} />
<XAxis niceTicks="snap125" domain={["auto", "auto"]} tick={{ fontSize: 12, fontFamily: "var(--font-sans)" }} />
<YAxis niceTicks="snap125" domain={["auto", "auto"]} tick={{ fontSize: 12, fontFamily: "var(--font-sans)" }} />
<Bar dataKey="value" fill="var(--chart-1)" />
</BarChart>
```

**Always use `niceTicks`** on `<XAxis>` and `<YAxis>` β€” 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 `<XAxis>` and `<YAxis>`. 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
Expand Down Expand Up @@ -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
Expand Down