(null)
useEffect(() => {
if (!url) {
setRaw(initialData)
return
}
setIsLoading(true)
setError(null)
fetch(url)
.then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status} — ${r.statusText}`)
return r.text()
})
.then((text) => setRaw(parseCsv(text).rows))
.catch((err: Error) => setError(err.message))
.finally(() => setIsLoading(false))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url, JSON.stringify(initialData)])
// Coerce y values to numbers; leave x untouched (categorical or numeric)
const rows = useMemo(
() =>
raw.map((row) => {
const out: Row = { [x]: row[x] }
for (const k of yKeys) {
const n = Number(row[k])
out[k] = Number.isFinite(n) ? n : (NaN as unknown as number)
}
return out
}),
[raw, x, yKeys]
)
if (isLoading) return Loading chart…
if (error) return Failed to load chart data: {error}
if (rows.length === 0) return No data to chart.
const grid =
const axes = (
<>
>
)
return (
{type === 'bar' ? (
{grid}{axes}
{yKeys.map((k, i) => )}
) : type === 'area' ? (
{grid}{axes}
{yKeys.map((k, i) => )}
) : type === 'pie' ? (
{rows.map((_, i) => | )}
) : type === 'scatter' ? (
{grid}
) : (
{grid}{axes}
{yKeys.map((k, i) => )}
)}
)
}
```
Note: `Chart` reuses `components/ui/parseCsv.ts`, which ships with the template
(the same parser `Table` uses). If that file is absent the portal predates the
current template — copy `parseCsv.ts` from `examples/portaljs-catalog/components/ui/`.
### 6. Render the chart into the showcase's Views section
The showcase `pages/[owner]/[slug].tsx` renders **every** dataset, so a chart must be
applied **only for the chosen dataset** — gate it on the dataset's `(namespace, slug)` so
other datasets' showcases are unaffected. The route already has a **Views placeholder**:
```tsx
{/* Views placeholder — charts and maps are added here by the
/portaljs-add-chart and /portaljs-add-map skills. */}
Views
No views yet. Charts and maps for this dataset are added here.
```
Edit `PORTAL_DIR/pages/[owner]/[slug].tsx`:
1. Add the import near the top (after the `Table` import):
```tsx
import { Chart } from '../../components/Chart'
```
2. Replace the Views placeholder `` so it conditionally renders the chart for the
target dataset and keeps the "no views yet" message for every other dataset. Build
`Y_PROP` as a single string `y="col"` for one column, or `y={['a','b']}` for several.
The data URL is the dataset's file served statically (`/data/`):
```tsx
Views
{dataset.namespace === 'NAMESPACE' && dataset.slug === 'SLUG' ? (
TITLE
) : (
No views yet. Charts and maps for this dataset are added here.
)}
```
If a previous `/portaljs-add-chart` or `/portaljs-add-map` run already replaced this section with a
view-dispatch block, **extend** that block with another `dataset.namespace === … &&
dataset.slug === …` branch rather than overwriting it, so multiple datasets can each have
their own views.
### 7. Verify the build
```bash
cd PORTAL_DIR && npx tsc --noEmit
```
If type-checking fails, tell the user the first `tsc` error and fix it before reporting
success.
### 8. Report success
```
✓ Chart added to DATASET
- Component: components/Chart.tsx (recharts)
- Showcase: pages/[owner]/[slug].tsx Views section —
- Renders at: /@NAMESPACE/SLUG
- Dependency: recharts@^2.15.0 added to package.json
Next: run `npm run dev` and visit http://localhost:3000/@NAMESPACE/SLUG to verify the chart renders.
```
## Notes
- **Why recharts, not `@portaljs/components`:** the bundled package ships leaflet, vega,
ag-grid, and pdf.js in one non-tree-shakeable 1.9 MB blob. `recharts` is ~100 KB
gzipped and tree-shakes. Per `CLAUDE.md`, add a chart library directly.
- **Numeric coercion:** CSV cells are strings; the component runs `Number()` on every
`y` value. Non-numeric cells become `NaN` and render as gaps (line/area) or skipped
bars — clean the data if you see holes.
- **Pie/scatter use the first `y` only.** Pie maps `x` → slice name, `y[0]` → slice
value. Scatter plots `x` (numeric) against `y[0]` (numeric). Pass extra `y` columns
only for line/bar/area (multi-series).
- **One showcase route, many datasets:** `pages/[owner]/[slug].tsx` renders every dataset,
so always gate a view on the dataset's `(namespace, slug)` — otherwise the chart would
appear on every dataset's showcase.
- **Client-side rendering:** the chart fetches in the browser like `Table`, so it works
with static export and needs no server code.
- **Large datasets:** recharts renders all points to SVG; over ~2,000 points gets
sluggish. Pre-aggregate (e.g. yearly buckets) for big series.
```