BonnardBonnard

Connecting a Database

Feed charts and views from live warehouse data with typed ChartData, covering why numbers must be numbers and how to catch a bad encoding before a host renders it.

The visualize tool takes SQL from the agent. But when you author dashboards and views over live, unseen data, the encoding is only as good as the types the data arrives with. This page shows how to feed a chart from a real database so it renders correctly the first time.

chart(rows) vs chart(ChartData)

chart (and chartCell) take either raw rows or a typed ChartData:

import { chart, buildChartData } from "@bonnard/mcp-charts";

// Raw rows: types are sniffed from a sample of the values.
const spec = chart(rows, { chartType: "line" });

// Typed ChartData: column types come from your driver, not a sniff.
const data = buildChartData({ rows, columns, mapKind }); // or an adapter's runSql(...)
const specTyped = chart(data, { chartType: "line" });

Both call the same function. Pass raw Record<string, unknown>[] to let inference sniff, or pass a { rows, fields?, encode?, notes? } ChartData to trust declared types. When you build views from live data, prefer the typed source: an adapter (or buildChartData) hands the resolver column types from your driver, so the encoding is decided from types, not guessed from a sample.

// In a view's render, ride the adapter's types:
render: async () => chart(await runSql("select month, revenue from ..."), { chartType: "line" }),

Numbers must be numbers

This is the single most common way a live chart goes wrong. Some drivers stringify decimals and bigints. Postgres NUMERIC / BIGINT, for example, arrive as revenue: "1234", not 1234.

When inference sees a column that's entirely numeric strings, it recovers it to a measure and adds an advisory note. But the robust fix is to not rely on recovery:

  • Use an adapter (Warehouse Adapters). It maps the driver's column types for you, so a NUMERIC column is a measure regardless of how the driver serialises it.
  • Declare the column's kind via fields (or buildChartData).
  • Cast in SQL: revenue::float8, count(*)::int.

fields and encode are trusted verbatim. A wrong declaration is honoured, not corrected: mark a measure as a string dimension and you get a blank series (with a note). Declare types that match the data.

Typed fields

const spec = chart(rows, {
  chartType: "bar",
  fields: [
    { name: "region", kind: "string", role: "dimension" },
    { name: "revenue", kind: "number", role: "measure", format: "currency", currency: "USD" },
  ],
});

A FieldMeta carries name, and optionally role (measure / dimension / time), kind (number / string / time / boolean), format (number / currency / percent), granularity (dayyear), currency, and label.

Catch a bad encoding before it renders: explain()

Most data mistakes are invisible to tsc. Rows are Record<string, unknown>[], so the type system can't see that sales is a numeric string or that a column is all-null. explain() diagnoses how rows (or a typed ChartData) would be charted, without building the render payload: the inferred field typing, the resolved chartType / x / series, and any notes.

Assert the encoding in a unit test or CI, so a broken chart fails your build instead of a host:

import { explain } from "@bonnard/mcp-charts";

const e = explain(sampleRows, { chartType: "bar" });
expect(e.series.length).toBeGreaterThan(0); // caught a blank chart

strict mode

Pass strict: true to promote encoding-failure advisories (zero series, an ignored encode column) from notes to thrown errors, for loud failure in authoring and CI:

// Throws on a zero-series result or an ignored encode column, instead of returning a blank chart.
explain(sampleRows, { chartType: "bar", strict: true });

Leave strict off in production (the default): there, a degenerate result renders with an advisory note rather than crashing the tool call. strict is available on chart, chartCell, and explain via ResolveOptions.

Zero-series notes

When a plotting chart (bar / line / area / pie / scatter) resolves to no measure series, the spec carries a prominent note rather than rendering a silently blank chart:

No measure column to plot — the chart has no data series. Check that value columns contain numbers (not strings), or declare types via fields.

That single guard covers the whole "blank chart" family: numeric-string measures that weren't recovered, an all-null column, a wrong fields declaration, or an encode.y naming a column that isn't in the result.

Troubleshooting

SymptomLikely causeFix
Blank chart, "No measure column to plot" notevalue column arrived as strings, or a fields/encode declaration left no measurereturn numbers (not numeric strings), or declare the column kind: "number"
Bar/line over "dates" isn't sorteddates are strings in a loose format (01/15/2026, Jan 2025)return ISO dates (YYYY-MM-DD) so the axis sorts chronologically
Multi-slice pie / odd funnel, with a precondition noteforced a chart type whose shape needs a category + a measuresupply one dimension + one measure, or drop the forced chartType
"Ignored unknown encode column" noteencode.x / encode.y names a column not in the resultfix the column name (the note lists the available columns)
A render_view param is rejectedthe caller passed a param the view didn't declare, or the wrong typedeclare it in the view's params, or fix the value. See Named views
unknown item_id from render_viewthe item_id names no chart cell in the viewpass an id set via chartCell(rows, { id }); the error lists the selectable ids

Next

On this page