/portaljs-add-chart
Add a chart (line, bar, area, pie, or scatter) to a dataset's showcase in a PortalJS portal. Installs recharts, writes a reusable Chart component, and renders it in the showcase Views section.
> /plugin marketplace add datopian/portaljs> /plugin install portaljs@datopian-portaljs
Command definition
portaljs-add-chart.mddescription: Add a chart (line, bar, area, pie, or scatter) to a dataset's showcase in a PortalJS portal. Installs recharts, writes a reusable Chart component, and renders it in the showcase Views section.
allowed-tools: Read, Write, Edit, Bash
/portaljs-add-chart
Add a visualization to a dataset's **showcase** in a `portaljs-catalog` portal. Installs `recharts` (added directly — **not** `@portaljs/components`), writes a reusable client-side `Chart` component into the portal's `components/`, and renders a `<Chart />` into the **Views** section of the showcase route `pages/[owner]/[slug].tsx` for the chosen dataset.
Use this after the dataset is registered in `datasets.json` (e.g. via `/portaljs-add-dataset`). The chart reads the same `/public/data/<file>` the showcase's `<Table />` already uses — no data is duplicated.
Required input — ask, don't error
- **Dataset** — which dataset to chart, by **slug** (e.g. `co2-emissions`) or `slug`
within a namespace. It must already be an entry in `datasets.json`.
- **X axis column** — the column name for the category/X axis (e.g. `year`).
- **Y axis column(s)** — one or more numeric column names to plot (e.g. `population`
or `imports,exports`).
- **Portal directory** — path to the portal project (defaults to current directory).
- **Chart type** — `line` (default), `bar`, `area`, `pie`, or `scatter`.
**If the target dataset isn't specified, ask which one (by name/slug) — never dead-end with a missing-input error.**
Steps
1. Gather input from `$ARGUMENTS` (interview if thin)
Extract:
- `DATASET` — dataset slug (required)
- `X` — x-axis column name (required)
- `Y` — comma-separated y-axis column name(s) (required)
- `TYPE` — chart type, one of `line|bar|area|pie|scatter` (default: `line`)
- `PORTAL_DIR` — portal directory (default: `.`)
- `TITLE` — chart heading (default: derived from Y columns, e.g. "Population over Year")
If the dataset (or X/Y) is missing, **ask** and wait. When the user doesn't know the slug, read `PORTAL_DIR/datasets.json` and list the available datasets (`name` → `slug`) so they can pick one:
To add a chart I need:
1. Which dataset? (slug — your catalog has: <name (slug)>, …)
2. X axis column (e.g. year)
3. Y axis column(s), comma-separated (e.g. population or imports,exports)
4. Chart type [line] (line|bar|area|pie|scatter)
5. Portal directory (Enter for current directory)
Validate `TYPE` is one of the five supported values. If not, tell the user and ask them to pick line, bar, area, pie, or scatter.
2. Resolve the dataset from the manifest and its data source
- Read `PORTAL_DIR/datasets.json` and find the entry whose `slug` matches `DATASET`
(if multiple namespaces share the slug, ask which `namespace`). Capture its `namespace`, `file`, and `format`.
- If no entry matches, tell the user and list the available slugs (don't error out) — they
may have meant a different one or need to run `/portaljs-add-dataset` first.
- The data source is the bare file served statically: `DATA_URL = /data/<file>`. The
showcase route is `pages/[owner]/[slug].tsx`; the page rendered for this dataset is `/@<namespace>/<slug>`.
3. Validate the requested columns exist
- For CSV/TSV: read `PORTAL_DIR/public/data/<file>` first line for headers.
- For JSON: read the first object's keys from `PORTAL_DIR/public/data/<file>`.
- Confirm `X` and every `Y` column is present. If any is missing, tell the user which
column wasn't found and list the available headers so they can correct it.
- Warn (do not fail) if a `Y` column's first non-empty value is non-numeric:
Note: column "COL" looks non-numeric — chart values are coerced with Number(); non-numeric cells render as gaps.
4. Install recharts
cd PORTAL_DIR && npm install recharts@^2.15.0
Do **not** install `@portaljs/components`. If the install fails, tell the user (check network and `package.json`) and retry.
5. Write the reusable Chart component
Write `PORTAL_DIR/components/Chart.tsx` **only if it does not already exist** (idempotent — do not overwrite a customized component):
import React, { useEffect, useMemo, useState } from 'react'
import {
ResponsiveContainer,
LineChart, Line,
BarChart, Bar,
AreaChart, Area,
PieChart, Pie, Cell,
ScatterChart, Scatter,
XAxis, YAxis, ZAxis,
CartesianGrid, Tooltip, Legend,
} from 'recharts'
import { t } from '../lib/i18n'
import { parseCsv } from './ui/parseCsv'
type Row = Record<string, string | number>
export interface ChartProps {
/** CSV file URL under /public, e.g. "/data/file.csv" */
url?: string
/** Pre-parsed rows (e.g. from an imported JSON array) */
data?: Row[]
type?: 'line' | 'bar' | 'area' | 'pie' | 'scatter'
/** X-axis / category column key */
x: string
/** One or more numeric column keys to plot */
y: string | string[]
height?: number
}
const PALETTE = ['#2563eb', '#16a34a', '#dc2626', '#d97706', '#7c3aed', '#0891b2', '#db2777', '#65a30d']
export function Chart({ url = '', data: initialData = [], type = 'line', x, y, height = 320 }: ChartProps) {
const yKeys = useMemo(() => (Array.isArray(y) ? y : [y]), [y])
const [raw, setRaw] = useState<Row[]>(initialData)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(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) => {Read more
description: Add a chart (line, bar, area, pie, or scatter) to a dataset's showcase in a PortalJS portal. Installs recharts, writes a reusable Chart component, and renders it in the showcase Views section. allowed-tools: Read, Write, Edit, Bash
/portaljs-add-chart
Add a visualization to a dataset's **showcase** in a `portaljs-catalog` portal. Installs `recharts` (added directly — **not** `@portaljs/components`), writes a reusable client-side `Chart` component into the portal's `components/`, and renders a `<Chart />` into the **Views** section of the showcase route `pages/[owner]/[slug].tsx` for the chosen dataset.
Use this after the dataset is registered in `datasets.json` (e.g. via `/portaljs-add-dataset`). The chart reads the same `/public/data/<file>` the showcase's `<Table />` already uses — no data is duplicated.
Required input — ask, don't error
- **Dataset** — which dataset to chart, by **slug** (e.g. `co2-emissions`) or `slug`
within a namespace. It must already be an entry in `datasets.json`.
- **X axis column** — the column name for the category/X axis (e.g. `year`).
- **Y axis column(s)** — one or more numeric column names to plot (e.g. `population`
or `imports,exports`).
- **Portal directory** — path to the portal project (defaults to current directory).
- **Chart type** — `line` (default), `bar`, `area`, `pie`, or `scatter`.
**If the target dataset isn't specified, ask which one (by name/slug) — never dead-end with a missing-input error.**
Steps
1. Gather input from `$ARGUMENTS` (interview if thin)
Extract:
- `DATASET` — dataset slug (required)
- `X` — x-axis column name (required)
- `Y` — comma-separated y-axis column name(s) (required)
- `TYPE` — chart type, one of `line|bar|area|pie|scatter` (default: `line`)
- `PORTAL_DIR` — portal directory (default: `.`)
- `TITLE` — chart heading (default: derived from Y columns, e.g. "Population over Year")
If the dataset (or X/Y) is missing, **ask** and wait. When the user doesn't know the slug, read `PORTAL_DIR/datasets.json` and list the available datasets (`name` → `slug`) so they can pick one:
To add a chart I need: 1. Which dataset? (slug — your catalog has: <name (slug)>, …) 2. X axis column (e.g. year) 3. Y axis column(s), comma-separated (e.g. population or imports,exports) 4. Chart type [line] (line|bar|area|pie|scatter) 5. Portal directory (Enter for current directory)
Validate `TYPE` is one of the five supported values. If not, tell the user and ask them to pick line, bar, area, pie, or scatter.
2. Resolve the dataset from the manifest and its data source
- Read `PORTAL_DIR/datasets.json` and find the entry whose `slug` matches `DATASET`
(if multiple namespaces share the slug, ask which `namespace`). Capture its `namespace`, `file`, and `format`.
- If no entry matches, tell the user and list the available slugs (don't error out) — they
may have meant a different one or need to run `/portaljs-add-dataset` first.
- The data source is the bare file served statically: `DATA_URL = /data/<file>`. The
showcase route is `pages/[owner]/[slug].tsx`; the page rendered for this dataset is `/@<namespace>/<slug>`.
3. Validate the requested columns exist
- For CSV/TSV: read `PORTAL_DIR/public/data/<file>` first line for headers.
- For JSON: read the first object's keys from `PORTAL_DIR/public/data/<file>`.
- Confirm `X` and every `Y` column is present. If any is missing, tell the user which
column wasn't found and list the available headers so they can correct it.
- Warn (do not fail) if a `Y` column's first non-empty value is non-numeric:
Note: column "COL" looks non-numeric — chart values are coerced with Number(); non-numeric cells render as gaps.
4. Install recharts
cd PORTAL_DIR && npm install recharts@^2.15.0
Do **not** install `@portaljs/components`. If the install fails, tell the user (check network and `package.json`) and retry.
5. Write the reusable Chart component
Write `PORTAL_DIR/components/Chart.tsx` **only if it does not already exist** (idempotent — do not overwrite a customized component):
import React, { useEffect, useMemo, useState } from 'react'
import {
ResponsiveContainer,
LineChart, Line,
BarChart, Bar,
AreaChart, Area,
PieChart, Pie, Cell,
ScatterChart, Scatter,
XAxis, YAxis, ZAxis,
CartesianGrid, Tooltip, Legend,
} from 'recharts'
import { t } from '../lib/i18n'
import { parseCsv } from './ui/parseCsv'
type Row = Record<string, string | number>
export interface ChartProps {
/** CSV file URL under /public, e.g. "/data/file.csv" */
url?: string
/** Pre-parsed rows (e.g. from an imported JSON array) */
data?: Row[]
type?: 'line' | 'bar' | 'area' | 'pie' | 'scatter'
/** X-axis / category column key */
x: string
/** One or more numeric column keys to plot */
y: string | string[]
height?: number
}
const PALETTE = ['#2563eb', '#16a34a', '#dc2626', '#d97706', '#7c3aed', '#0891b2', '#db2777', '#65a30d']
export function Chart({ url = '', data: initialData = [], type = 'line', x, y, height = 320 }: ChartProps) {
const yKeys = useMemo(() => (Array.isArray(y) ? y : [y]), [y])
const [raw, setRaw] = useState<Row[]>(initialData)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(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) => {🌀 AI-native framework for building data portals. Scaffold a full portal from a brief and load datasets in minutes with agentic skills — any backend (CKAN, GitHub, Frictionless).
Other commands on portaljs.
More commands from other plugins.
build
CommandImplement tasks incrementally — build, test, verify, commit. Add "auto" to run the whole plan in one approved pass.

