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