Data

Connect data

Charts take an array of objects. useChartData loads one from a URL or an async function, with loading, error and empty states.

Field accessors

Props such as label, value, x, y, series and size take a field name or a function. Strings like "1,234", "12%" and "₩5,000" are read as numbers.

accessors.tsx
// A field name (string) or a function both work
<PieChart data={rows} label="device" value="sales" />
<PieChart data={rows} label={(d) => d.device.toUpperCase()} value={(d) => d.sales / 100} />

// Dot paths reach nested fields
<BarChart data={rows} x="meta.region" y="stats.revenue" />

// For a wide table (one series per column), pass y an array
<LineChart data={rows} x="month" y={["Search", "Social", "Email"]} />

Loading, error and empty

ChartState shows each state at the chart's height. An error comes with a Try again button.

Loading states

Pick a state with the buttons
LoadingBasic
states.tsx
const st = useChartData(url, { refreshMs: 60_000 });

<ChartState loading={st.loading} error={st.error} empty={st.empty} onRetry={st.reload}>
  <BarChart data={st.data} x="q" y="v" />
</ChartState>

REST JSON

If the response is wrapped, point select at the inner array. Send headers such as auth through init, and refresh on a timer with refreshMs.

Orders by weekday

REST JSON, refreshed every minute
Friday is the busiest dayBasic
examples/data/rest-json.tsx
import { BarChart, ChartCard, ChartState, useChartData } from "ztd-charts";

/**
 * Load from a REST JSON API.
 * If the response is wrapped like { data: { items: [...] } }, select pulls out the inner array.
 */
export function RestJsonChart({ url = "/data/orders.json" }: { url?: string }) {
  const orders = useChartData<{ day: string; orders: number }>(url, {
    select: "data.items",
    refreshMs: 60_000,
    init: { headers: { Accept: "application/json" } },
  });
  return (
    <ChartCard title="Orders by weekday" subtitle="REST JSON, refreshed every minute" takeaway="Friday is the busiest day" level="basic" replay>
      <ChartState loading={orders.loading && !orders.data.length} error={orders.error} empty={orders.empty} onRetry={orders.reload}>
        <BarChart data={orders.data} x="day" y="orders" unit=" orders" />
      </ChartState>
    </ChartCard>
  );
}

CSV

Pass a CSV URL as is. Quotes, line breaks inside cells, BOMs and thousands separators are handled.

Monthly revenue by channel

CSV (USD K)
Social grows fastBasic
examples/data/csv.tsx
import { ChartCard, ChartState, LineChart, parseCSV, useChartData } from "ztd-charts";

/**
 * Pass a CSV file URL as is. It is read with parseCSV, based on the content-type or the .csv extension.
 * Cells such as "1,234", "12%" and "₩5,000" become numbers.
 */
export function CsvChart({ url = "/data/sales.csv" }: { url?: string }) {
  const sales = useChartData(url);
  return (
    <ChartCard title="Monthly revenue by channel" subtitle="CSV (USD K)" takeaway="Social grows fast" level="basic" replay>
      <ChartState loading={sales.loading && !sales.data.length} error={sales.error} empty={sales.empty} onRetry={sales.reload}>
        <LineChart data={sales.data} x="month" y="revenue" series="channel" highlight="Social" unit="K" />
      </ChartState>
    </ChartCard>
  );
}

// If you already have the text, parse it right away
export const rows = parseCSV(`month,channel,revenue
2025-01,Search,120
2025-01,Social,64`);

Supabase and Postgres rows

Supabase (PostgREST) and node-postgres return arrays of rows. numeric columns that arrive as strings are read as numbers. Without keys, the demo shows sample rows.

Daily revenue by plan

Supabase sample rows
Pro leads every dayBasic
examples/data/supabase.tsx
import { AreaChart, ChartCard, ChartState, useChartData } from "ztd-charts";

const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL as string | undefined;
const SUPABASE_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY as string | undefined;

/**
 * Supabase (PostgREST) returns an array of rows, so you can pass it straight to data.
 * numeric columns arrive as strings such as "1234.50", and the chart reads them as numbers.
 *
 * With supabase-js:
 *   const { data, error } = await supabase.from("daily_revenue").select("day, plan, revenue").order("day");
 *   if (error) throw error; return data;
 */
export function SupabaseChart() {
  const configured = !!(SUPABASE_URL && SUPABASE_KEY);
  const revenue = useChartData(
    configured
      ? `${SUPABASE_URL}/rest/v1/daily_revenue?select=day,plan,revenue&order=day`
      : // Without keys, show sample rows (Postgres numeric arrives as strings)
        async () => [
          { day: "2025-06-01", plan: "Basic", revenue: "1820.50" },
          { day: "2025-06-01", plan: "Pro", revenue: "2410.00" },
          { day: "2025-06-02", plan: "Basic", revenue: "1902.25" },
          { day: "2025-06-02", plan: "Pro", revenue: "2675.75" },
          { day: "2025-06-03", plan: "Basic", revenue: "1876.00" },
          { day: "2025-06-03", plan: "Pro", revenue: "2890.10" },
        ],
    configured ? { init: { headers: { apikey: SUPABASE_KEY!, Authorization: `Bearer ${SUPABASE_KEY}` } } } : {},
  );
  return (
    <ChartCard title="Daily revenue by plan" subtitle={configured ? "Supabase daily_revenue" : "Supabase sample rows"} takeaway="Pro leads every day" level="basic" replay>
      <ChartState loading={revenue.loading && !revenue.data.length} error={revenue.error} empty={revenue.empty} onRetry={revenue.reload}>
        <AreaChart data={revenue.data} x="day" y="revenue" series="plan" highlight="Pro" unit="K" />
      </ChartState>
    </ChartCard>
  );
}
examples/data/postgres-api.ts
/**
 * An API that sends Postgres (node-postgres) results to a chart as JSON (in Node: Express or a Next.js Route Handler).
 * pg returns numeric and bigint as strings, and you can send them as they are. The chart reads them as numbers.
 *
 *   // app/api/signups/route.ts (Next.js)
 *   import { Pool } from "pg";
 *   const pool = new Pool({ connectionString: process.env.DATABASE_URL });
 *   export async function GET() {
 *     const { rows } = await pool.query(
 *       "select date_trunc('day', created_at)::date as day, count(*) as signups from users group by 1 order by 1",
 *     );
 *     return Response.json(rows); // [{ day: "2025-06-01", signups: "128" }, ...]
 *   }
 *
 * On the client:
 *   const s = useChartData("/api/signups");
 *   <CalendarHeatmap data={s.data} date="day" value="signups" />
 *
 * Results that bring column names and rows separately (rows + fields, Google Sheets values) go through fromRows.
 */
import { fromRows } from "ztd-charts";

export const fromPgArrayMode = fromRows(
  [
    ["2025-06-01", "128"],
    ["2025-06-02", "141"],
  ],
  ["day", "signups"],
);

Google Sheets CSV

Paste the CSV address of a sheet published to the web. Addresses with format=csv or out:csv are read as CSV.

Leave it empty to use the sample CSV. The columns must be channel and budget; for other columns, change label and value in the code.

Budget by channel

Google Sheets CSV
Search ads take halfBasic
examples/data/google-sheets.tsx
import { ChartCard, ChartState, DonutChart, useChartData } from "ztd-charts";

/**
 * Google Sheets: choose File > Share > Publish to web > CSV, or use one of these address formats.
 *   https://docs.google.com/spreadsheets/d/<sheet ID>/export?format=csv&gid=<tab ID>
 *   https://docs.google.com/spreadsheets/d/<sheet ID>/gviz/tq?tqx=out:csv&sheet=<tab name>
 * Addresses with format=csv or out:csv are read as CSV.
 */
export function SheetChart({ url }: { url: string }) {
  const sheet = useChartData(url);
  return (
    <ChartCard title="Budget by channel" subtitle="Google Sheets CSV" takeaway="Search ads take half" level="basic" replay>
      <ChartState loading={sheet.loading && !sheet.data.length} error={sheet.error} empty={sheet.empty} onRetry={sheet.reload}>
        <DonutChart data={sheet.data} label="channel" value="budget" centerLabel="Budget ($K)" />
      </ChartState>
    </ChartCard>
  );
}

URL in a JSON spec

Put a url in the spec's data and <Chart spec> fetches it. CLI render fetches it first too.

Orders by weekday

data.url in the spec
Friday is the peakBasic
orders.spec.json
{
  "type": "bar",
  "data": {
    "url": "/data/orders.json",
    "select": "data.items"
  },
  "encoding": {
    "x": "day",
    "y": "orders"
  },
  "options": {
    "unit": " orders"
  },
  "card": {
    "title": "Orders by weekday",
    "subtitle": "data.url in the spec",
    "takeaway": "Friday is the peak",
    "level": "basic"
  }
}

Reshape helpers

2D arrays, key-value maps and column sets become arrays of objects too.

helpers.ts
import { parseCSV, fromRows, fromObjects, normalizeData } from "ztd-charts";

parseCSV('month,revenue\nJan,"1,200"\nFeb,1350');
// [{ month: "Jan", revenue: 1200 }, { month: "Feb", revenue: 1350 }]

fromRows([["month", "revenue"], ["Jan", 1200], ["Feb", 1350]]);  // Google Sheets API values
fromRows([["Jan", "1200"]], ["month", "revenue"]);               // SQL rows + column names

fromObjects({ Mobile: 72, Desktop: 23, Tablet: 5 });
// [{ label: "Mobile", value: 72 }, …]
fromObjects({ month: ["Jan", "Feb"], revenue: [1200, 1350] });   // column sets

normalizeData(anything); // CSV text, JSON text, { data }, { rows, columns }, { values }
Actual result
parseCSV(...) → [{"month":"Jan","revenue":1200},{"month":"Feb","revenue":1350}]
Actual result
fromObjects(...) → [{"label":"Mobile","value":72},{"label":"Desktop","value":23},{"label":"Tablet","value":5}]