Chart view
Show a view as bars, a line, areas, bars and a line, a donut, a funnel or a single number, grouped on your server.
The Chart view works like Notion charts: a view of the table shows its records as vertical or horizontal bars, a line, areas, bars and a line, a donut, a funnel or a single number instead of rows. Charts follow the view's search and filters, are saved with the view, and ask your server for grouped values when it can answer them. React and Vue behave the same.
Chart is an optional registry item, so the table keeps no chart dependency until you install it. React draws with shadcn/ui charts (Recharts), Vue with Unovis (@unovis/vue and @unovis/ts, the engine behind shadcn-vue charts).
Install and enable
npx shadcn@latest add https://table.yayaw.app/r/yayaw-table-chart.jsonThis installs recharts and the shadcn chart component, and adds the files under components/ui/yayaw-table-chart/. Pass the renderer and list "chart" in displayModes:
import { chartRenderer } from "@/components/ui/yayaw-table-chart/chart-renderer";
<DataTable
displayModeRenderers={{ chart: chartRenderer }}
// ...
/>;npx shadcn-vue@latest add https://table.yayaw.app/r/yayaw-table-vue-chart.jsonThis installs @unovis/vue and @unovis/ts and adds the files under components/ui/yayaw-table-vue/chart/. Pass the renderer and list "chart" in displayModes:
<script setup lang="ts">
import { chartRenderer } from "@/components/ui/yayaw-table-vue/chart/chart-renderer";
</script>
<template>
<YayawDataTable :display-mode-renderers="{ chart: chartRenderer }" />
</template>export const salesConfig = defineTableConfig({
...productConfig,
table: {
...productConfig.table,
displayModes: ["table", "chart"],
chart: { xColumn: "category", metric: "sum", metricColumn: "price" },
},
});Without a passed renderer, "chart" is not offered even if listed in displayModes, and a link asking for it falls back to the default mode. table.chart is optional: an object of default settings (the shape below) that every Chart view starts from, or false to turn the mode off.
The chart library loads lazily: React with lazy and Suspense, Vue with defineAsyncComponent. Recharts or Unovis is fetched when the first chart is shown, not with the table, and the core table items only gain the shared chart model.
Try the charts
Both previews pass chartRenderer in displayModeRenderers. Their host has no aggregate action, so the table groups the records returned by list in the browser; see server-side grouping for large tables. Select a bar or a point to see its records, or choose Show as table for the values. The preview uses the store-opening projects shared by every view guide; see the example setup for project-config.ts and the in-memory host.
Budget by category
A bar chart of the budget summed per category, stacked by status.
Sample data. Changes stay in this preview.
import { defineTableConfig } from "@/components/ui/yayaw-table";
import { projectConfig } from "./project-config";
// Pass `displayModeRenderers={{ chart: chartRenderer }}` to the table.
export const exampleConfig = defineTableConfig({
...projectConfig,
table: {
...projectConfig.table,
displayModes: ["chart", "table"],
defaultDisplayMode: "chart",
chart: {
type: "bar",
xColumn: "category",
metric: "sum",
metricColumn: "budget",
seriesColumn: "status",
stacked: true,
showDataLabels: false,
},
},
});Sample data. Changes stay in this preview.
import { defineTableConfig } from "@/components/ui/yayaw-table-vue";
import { projectConfig } from "./project-config";
// Pass `:display-mode-renderers="{ chart: chartRenderer }"` to the table.
export const exampleConfig = defineTableConfig({
...projectConfig,
table: {
...projectConfig.table,
displayModes: ["chart", "table"],
defaultDisplayMode: "chart",
chart: {
type: "bar",
xColumn: "category",
metric: "sum",
metricColumn: "budget",
seriesColumn: "status",
stacked: true,
showDataLabels: false,
},
},
});Projects over time
A line of the projects counted by week of their start date, as running totals, with the values written on the points.
Sample data. Changes stay in this preview.
import { defineTableConfig } from "@/components/ui/yayaw-table";
import { projectConfig } from "./project-config";
// Pass `displayModeRenderers={{ chart: chartRenderer }}` to the table.
export const exampleConfig = defineTableConfig({
...projectConfig,
table: {
...projectConfig.table,
displayModes: ["chart", "table"],
defaultDisplayMode: "chart",
chart: {
type: "line",
xColumn: "start",
bucket: "week",
metric: "count",
cumulative: true,
showDataLabels: true,
},
},
});Sample data. Changes stay in this preview.
import { defineTableConfig } from "@/components/ui/yayaw-table-vue";
import { projectConfig } from "./project-config";
// Pass `:display-mode-renderers="{ chart: chartRenderer }"` to the table.
export const exampleConfig = defineTableConfig({
...projectConfig,
table: {
...projectConfig.table,
displayModes: ["chart", "table"],
defaultDisplayMode: "chart",
chart: {
type: "line",
xColumn: "start",
bucket: "week",
metric: "count",
cumulative: true,
showDataLabels: true,
},
},
});Chart types
| Type | Value | Shows |
|---|---|---|
| Vertical bars | bar | One bar per group; with a series column, stacked or side-by-side bars. |
| Horizontal bars | horizontalBar | The same bars laid out horizontally, handy for long labels. |
| Line | line | One point per group; with a series column, one line per value. |
| Area | area | A filled line; with a series column, one area per value, stacked, stacked to 100 % or overlapping (stacking). |
| Bars and line | combo | Bars for metric and a line for lineMetric over the same x axis, with a legend for both. When the two metrics have different units, the line gets a right axis. |
| Donut | donut | One slice per group, with the values in the legend. |
| Funnel | funnel | One stage per value of the x column, in option order for a select column (stageOrder overrides it). Each stage shows its value, its share of the first stage and its conversion from the previous one. Stages sit side by side on wide charts and top to bottom on narrow ones. |
| Number | number | One total for every record matching the view. |
Chart settings
Choose Chart under View settings › Display mode, then open Card settings in the same menu. Settings are saved with the view (config.chart) and in the <tableId>-chart URL key, for example ?projects-display=chart&projects-chart={"type":"line"}. Reset returns to table.chart, then to the defaults below.
| Setting | Default | Description |
|---|---|---|
type | "bar" | "bar", "horizontalBar", "line", "area", "combo", "donut", "funnel" or "number". |
xColumn | First select or multi-select column, else the first date column, else the first groupable column | The x axis, or the slices of a donut. Select, multi-select, date, text, number, boolean, email and URL columns can group. |
bucket | "month" | For a date x axis: "day", "week", "month", "quarter" or "year". |
weekStartsOn | 1 (Monday) | First day of week buckets, 0 (Sunday) to 6 (Saturday). The panel offers Monday, Sunday and Saturday. |
metric | "count" | "count", "sum", "avg", "min", "max" or "countDistinct" (unique values). |
metricColumn | None | The column the metric reads: a number column, or any groupable column for countDistinct. Without a fitting column the chart counts records; choosing a metric in the panel picks the first fitting column. |
seriesColumn | None | A second grouping: stacked or grouped bars, one line or one area per value. Any groupable column other than the x column; a date column is bucketed like the x axis (bucket). Only for bar, horizontal bar, line and area charts. At most ten series are drawn: the rest are folded into "Other" for counts and sums, and left out for other metrics. |
stacked | true | Stack the series of a bar chart, or show them side by side. |
stacking | "stacked" | Area series: "stacked", "percent" (shares of 100 %; tables and tooltips show the value and its share) or "none" (overlapping). |
curve | "smooth" | "smooth" or "linear" (straight segments), for lines, areas and the line of a combo chart. |
lineMetric | "count" | The combo chart's line metric, with the same values as metric. The bars use metric. |
lineMetricColumn | None | The column the line metric reads, like metricColumn. |
stageOrder | Option order | Funnel stages in order, as option values. Options left out follow in the column's option order. In the panel, drag a stage or use its arrows. |
fill | false | Fill the nearest size container (CSS container-type: size, for example a dashboard widget) instead of a 320px chart: no title, table toggle or hint; the legend moves beside or under the chart, or hides, and data labels and the value axis stay only when they fit. Dashboards set it on their chart widgets. |
sort | "auto" | "auto" (option order for selects, label for dates, numbers and booleans, value otherwise), "keyAsc", "keyDesc", "valueAsc", "valueDesc" or "manual" (option order). |
cumulative | false | Running totals along the x axis, for bars, lines and areas. |
hideEmpty | false | Hide groups without a value and groups whose value is zero. Otherwise missing date buckets and options are filled in. |
topN | All | Keep the first N groups after sorting (1 to 50; the panel offers 3, 5, 10 and 20). For counts and sums the rest become an "Other" group; for other metrics they are left out. Funnels keep every stage. |
showDataLabels | false | Write the values on the bars, slices or points. |
showLegend | true | Show the legend for series and donuts. |
colors | "options" | "options" colors groups with their option color (else the tag hue when tags are colored); "palette" uses the shadcn --chart-1 to --chart-5 tokens. |
The panel only shows what applies: the date bucket for a date x axis, the week start for weekly buckets, the metric column for metrics other than a count, and only the type and metric for a number chart. Values are formatted with the metric column's numberFormat, and value ticks are round numbers (whole numbers for counts).
Date buckets use the x column's timeZone, or the browser's zone. Date-only values such as 2026-09-24 stay calendar days, so daylight saving time never moves them to another bucket. A multi-select value counts in each of its groups.
Metric display conversions
Set chart.valueFormat to display an aggregate in another unit. A combo chart's
second metric has its own chart.lineValueFormat; converting the bars never
changes the line's units. Both React and Vue accept the same settings:
{
"scale": 0.000001,
"unit": "Mbit/s",
"decimals": 2
}The display formula is raw aggregate * scale + offset. For example,
6,250,000 bit/s becomes 6.25 Mbit/s. A scale of 0.001 converts milliseconds
to seconds; 0.00000095367431640625 converts bytes to MiB. A scale of 1.8
and offset of 32 converts Celsius to Fahrenheit.
scale: finite and strictly positive, default1. Use a reciprocal for division.offset: finite, default0, added once after aggregating and scaling.unit: optional literal suffix, trimmed, 1–32 characters, without control characters.decimals: optional fixed number of fractional digits, an integer from 0 to 12.
An explicit unit uses decimal formatting instead of the source column's currency,
percent or unit style. Without a unit, the column's number format is inherited;
decimals overrides its precision. The reader's locale controls the default
separators. With no format, existing charts render unchanged.
The same formatting applies to metric ticks, tooltips, data labels and the chart's values table. Stored records, queries, filters, sorting, geometry, shares and comparison percentages stay in raw units. Offsets are not record-level formulas: do not use them to calculate a sum of converted records or change share denominators. Invalid formats are rejected as a whole; expressions and unknown keys are not accepted. Overflow displays an em dash instead of an infinite measurement.
Saved views and dashboard inline views retain these settings. The editors preserve
them when changing other options, but do not yet provide conversion controls.
Number widgets use the same contract in settings.valueFormat.
Server-side grouping
A chart asks actions.aggregate for its groups, with the view's query (search, filters, advanced filters and their join) and four more parameters. calculations is then empty:
{
search: "",
filters: {},
advancedFilters: [],
advancedFilterJoin: "and",
calculations: {},
locale: "en",
groupBy: [{ columnId: "dueDate", bucket: "month" }, { columnId: "status" }],
metrics: [{ columnId: "price", fn: "sum" }],
timeZone: "Europe/Paris",
weekStartsOn: 1,
}groupByhas at most two levels: the x axis, then the series. Date columns carry abucket. It is empty for a number chart, which expects one group withkeys: [].metricslists the values to compute per group:{ fn }for a count,{ columnId, fn }otherwise. A bars-and-line chart asks for two metrics in one request, the bars' then the line's; answer both values in that order. A host that answers fewer values than metrics gets the browser fallback below.timeZoneis the x column'stimeZone; when it is left out, use the browser's zone if you know it, or the zone your data is displayed in.weekStartsOnis the first day of week buckets,0(Sunday) to6(Saturday).
Answer one entry per group:
{
groups: [
{ keys: ["2026-09", "Active"], values: [463] },
{ keys: ["2026-09", null], values: [120] },
],
truncated: false,
}keys follow groupBy and values follow metrics. Empty values are null. Date bucket keys are:
| Bucket | Key |
|---|---|
| Day | YYYY-MM-DD |
| Week | The first day of the week, YYYY-MM-DD, following weekStartsOn |
| Month | YYYY-MM |
| Quarter | YYYY-Qn, for example 2026-Q3 |
| Year | YYYY |
Other keys are the raw values: option values, numbers, true or false, text. A record whose multi-select holds several values counts in each of their groups. Return truncated: true when you computed the groups over part of the records only; the chart then shows a notice.
Existing hosts keep working. The TypeScript response type now makes results optional and adds groups and truncated. When there is no aggregate, when it fails, or when it answers without groups (column calculations only), the chart groups the rows returned by list in the browser instead: it pages through the view's matching records up to 2,000 rows, and shows "Only part of the records are counted" when there are more. Without a list action, it groups the table's local rows.
Server example
A Postgres handler that groups orders by month and status, with the dates bucketed in the requested time zone. Column ids map to known SQL expressions, so no request value is ever written into the SQL text:
import type { TableAggregateParams } from "@/components/ui/yayaw-table/providers/table-provider";
import { sql } from "@/server/db"; // your SQL client, with $1 parameters
const COLUMNS: Record<string, { expr: string; date?: boolean }> = {
createdAt: { expr: "created_at", date: true },
status: { expr: "status" },
price: { expr: "price" },
};
const BUCKET_FORMATS = {
day: "YYYY-MM-DD",
month: "YYYY-MM",
quarter: 'YYYY-"Q"Q',
year: "YYYY",
};
function groupExpression(
group: { columnId: string; bucket?: string },
zoneParam: string,
weekStartParam: string
) {
const column = COLUMNS[group.columnId];
if (!column) {
throw new Error(`Unknown column ${group.columnId}`);
}
if (!(column.date && group.bucket)) {
return column.expr;
}
// Local day in the table's time zone (skip AT TIME ZONE for date columns).
const day = `(${column.expr} AT TIME ZONE ${zoneParam})::date`;
if (group.bucket === "week") {
// isodow % 7: Sunday = 0 ... Saturday = 6, like weekStartsOn.
const offset = `((extract(isodow from ${day})::int % 7 - ${weekStartParam} + 7) % 7)`;
return `to_char(${day} - ${offset}, 'YYYY-MM-DD')`;
}
const format = BUCKET_FORMATS[group.bucket as keyof typeof BUCKET_FORMATS];
return `to_char(${day}, '${format}')`;
}
const METRICS = {
count: () => "count(*)",
sum: (expr: string) => `coalesce(sum(${expr}), 0)`,
avg: (expr: string) => `avg(${expr})`,
min: (expr: string) => `min(${expr})`,
max: (expr: string) => `max(${expr})`,
countDistinct: (expr: string) => `count(distinct ${expr})`,
};
export async function aggregateOrders(params: TableAggregateParams) {
if (!params.groupBy) {
return { results: await columnCalculations(params) }; // footer totals
}
const values: unknown[] = [params.timeZone ?? "UTC", params.weekStartsOn ?? 1];
const keys = params.groupBy.map(
(group, index) => `${groupExpression(group, "$1", "$2")} as k${index}`
);
const metrics = (params.metrics ?? []).map((metric) => {
const column = metric.columnId ? COLUMNS[metric.columnId] : undefined;
if (metric.columnId && !column) {
throw new Error(`Unknown column ${metric.columnId}`);
}
const aggregateOf = METRICS[metric.fn];
return aggregateOf(column?.expr ?? "");
});
const selected = [...keys, ...metrics.map((expr, index) => `${expr} as v${index}`)];
const where = buildWhere(params, values); // the same filters as your list action
const rows = await sql(
`select ${selected.join(", ")}
from orders
where ${where}
${keys.length ? `group by ${keys.map((_, index) => index + 1).join(", ")}` : ""}`,
values
);
return {
groups: rows.map((row) => ({
keys: keys.map((_, index) => row[`k${index}`] ?? null),
values: metrics.map((_, index) => Number(row[`v${index}`] ?? 0)),
})),
};
}buildWhere and columnCalculations are your own list filtering and footer totals. Grouping on a multi-select stored as an array needs unnest so that each value gets its own group. The shared model also exports aggregateChartRows(rows, request), the browser fallback itself, which you can use as an in-memory implementation for small datasets or tests.
Click to see the records
Click a bar, a slice or a point to see its records: the group's rules are added to the view's advanced filters, joined with and, and the table opens (else the list, else another offered mode). The filter stays visible and editable in the filter menus. The rules are the ones the filter menus write for the column: select groups become isAnyOf, multi-select contains, date buckets between their first and last day, yes/no groups isAnyOf rules on true or false, empty groups isEmpty and other values equals. In area and bars-and-line charts, clicking anywhere in a category's band selects it. A funnel stage is a button that shows its records.
"Other" groups are not clickable. When the view's filters match any rule (or with two rules or more), a group cannot be added to them and the chart says so under the chart.
Show as table
Show as table replaces the chart with a table of its numbers; Show as chart switches back. Each group name in that table is a button ("Show the records of …"), the keyboard path to the same filtering as a click. Number charts have no table. Recharts adds its own keyboard layer in React (arrow keys move the tooltip); Unovis marks are hidden from assistive technology in Vue and rely on the table.
Translations
The chart has built-in English and French labels; French is used when the locale starts with fr. Override any of them with flat chart.<key> keys in the table's translations, in React and Vue, for example "chart.showTable": "Table view".
Settings:
chartType,typeBar,typeHorizontalBar,typeLine,typeArea,typeCombo,typeDonut,typeFunnel,typeNumber,xColumn,bucket,day,week,month,quarter,year,metric,metricCount,metricSum,metricAvg,metricMin,metricMax,metricCountDistinct,metricColumn,seriesColumn,none,stacked,sort,sortAuto,sortKeyAsc,sortKeyDesc,sortValueAsc,sortValueDesc,sortManual,cumulative,hideEmpty,topN,topAll,top(with{count}),showDataLabels,showLegend,colors,colorsOptions,colorsPalette,weekStartsOn,monday,sunday,saturday,on,off,reset,stacking,stackingStacked,stackingPercent,stackingNone,curve,curveSmooth,curveLinear,barMetric,barMetricColumn,lineMetric,lineMetricColumn,stageOrder,stageOrderHint,stageOrderReset,moveStageUp,moveStageDown(with{stage}).Chart:
showTable,showChart,other,noValue,checked,unchecked,weekOf(with{date}),quarterLabel(with{quarter}and{year}),count,sumOf,avgOf,minOf,maxOf,countDistinctOf(with{column}),total,group,chartOf(the title, with{metric}and{column}),showRecords(with{group}),comboOf(with{bars},{line}and{column}),stage,shareOfFirst,conversion,ofFirstandfromPrevious(with{percent}).Messages:
noColumn,empty,loading,truncated,filterHint,filterUnavailable,funnelHint.
The mode's name in the display mode picker is views.display.chart in React and display.chart in Vue.
Theme
Charts use the shadcn chart tokens --chart-1 to --chart-5 in light and dark mode. The Vue item reads them through --yayaw-chart-1 to --yayaw-chart-5, which fall back to shadcn's neutral theme when your app defines no chart tokens.
Engine differences
Both editions share the model, settings panel, legend markup, title, hint, table fallback, ticks and colors. The drawing engines impose a few differences:
Data labels use Recharts
LabelListin React and UnovisXYLabelsin Vue. Grouped (unstacked) bars have no per-bar labels in Vue; the table lists the values.Donut values are shown in the legend in both editions.
Tooltips follow each engine's positioning.
Compatibility notes
TableDisplayMode now includes "chart". If your code keeps an exhaustive Record<TableDisplayMode, …> map, such as icons or labels per mode, add a chart entry so it keeps compiling:
const modeLabels: Record<TableDisplayMode, string> = {
// ...
chart: "Chart",
};Renderer contexts gain aggregate, advancedFilters and showRecords(rules), which custom display mode renderers can use the same way.
The same release fixes two React issues:
A table whose rows finished loading right after a quick filter change no longer keeps showing skeleton rows.
Date-only advanced filter values read from the URL are local days instead of UTC midnight, so they no longer land a day early west of Greenwich.