Charts (chart)
Show data as charts: trends, comparisons, shares and headline numbers. Every chart gets a generated text summary and a data table for screen readers. Display only: the page asks for no answer.
When to use it: You show numbers as charts: a trend over time, a comparison, a split of a whole, or headline figures with their change. Up to four charts per page. It only displays information; the person does not answer.
When not to use it: Do not use it for exact values the person needs to read row by row (use table), or for a single number you can say in chat.
Gather first:
- The data series, already computed, and what the person should take away from them.
Writing good content:
- Pick the
kindfor the question: line or area for change over time, bar for comparing categories, donut for parts of one whole, stat for one headline number. - Write
titleas the takeaway ("Spending fell 12% in October") and usesubtitlefor the period and source. - Set the unit so numbers read correctly, and keep a donut to 8 segments or fewer.
- A chart only displays information; it returns no answer.
Display-only: the page shows information and has no Submit button. It accepts no answer, so no result ever comes back.
Fields
Every page also has version (always 1), title, and optionally description, branding, cover (a header photo) and expiresAt; see Page fields. These are the fields of page (the page type and its data):
| Field | Description |
|---|---|
charts | object[], required. 1 to 4 charts, shown stacked in this order, e.g. a stat row, then a line, then a bar. Limits: 1-4 items. |
charts[].kind | "line" | "area" | "bar" | "bar_horizontal" | "stacked_bar" | "donut" | "stat", required. line or area: change over time. bar: compare categories or periods (vertical columns). bar_horizontal: compare categories with long names or ranked lists. stacked_bar: totals split into parts per category. donut: parts of one whole (2-8 segments). stat: one headline number per series with its change and a sparkline. |
charts[].series | object[], required. 1 to 6 series. donut takes exactly one. For stat, each series is one tile. Limits: 1-6 items. |
charts[].series[].id | string. Optional stable key. Series with the same id keep the same color across the charts on a page (e.g. "revenue" in a stat row and a line). Without ids, colors go in order within each chart. Limits: matches ^[A-Za-z0-9_-]{1,64}$. |
charts[].series[].label | string, required. Name of what is measured, shown in the legend, tooltip and summary, e.g. "Revenue" or "Steps". Limits: 1-60 characters. |
charts[].series[].points | object[], required. Data points, up to 500, in x order for dates and numbers. An empty list shows "Nothing to show". For donut: one point per segment, x the segment label and y its positive value (up to 8). For stat: the last point is the big number; earlier points draw the sparkline and the change vs the previous point. Limits: at most 500 items. |
charts[].series[].points[].x | number | string, required. Position on the x axis: a date ("2026-03-14", or "2026-03" for a month), a number, or a category label ("Groceries"). All x values in one chart use the same type. |
charts[].series[].points[].y | number, required. The value, in the unit of yFormat (e.g. 1250.5 dollars, 12.5 for 12.5%). Limits: -1000000000000-1000000000000. |
charts[].title | string. Optional heading that states the takeaway, e.g. "Spending fell 12% in June". Limits: 1-80 characters. |
charts[].subtitle | string. Optional line under the title: period, source or unit, e.g. "Last 6 months, USD". Limits: 1-140 characters. |
charts[].yFormat | "number" | "money" | "percent". Optional. How values are written: number (default), money (needs currency), or percent (y is already in percent: 12.5 means 12.5%). |
charts[].currency | string. ISO 4217 code for yFormat "money", e.g. "USD". Required with money, not allowed otherwise. Limits: matches ^[A-Z]{3}$. |
charts[].unit | string. Optional unit written after numbers, e.g. "kg" or "steps". Not used with money or percent. Limits: 1-16 characters. |
charts[].xFormat | "day" | "month" | "year". Optional, for date x values only: write dates as days ("Mar 14"), months ("Mar") or years ("2026"). Chosen from the data when omitted. |
charts[].xLabel | string. Optional x axis title, e.g. "Month". Leave out when the values speak for themselves. Limits: 1-40 characters. |
charts[].yLabel | string. Optional y axis title, e.g. "Weight (kg)". Limits: 1-40 characters. |
charts[].annotations | object[]. Optional notes pinned to x values (up to 4), drawn as a thin vertical line with a label. Not for donut or stat. Limits: at most 4 items. |
charts[].annotations[].x | number | string, required. Position on the x axis: a date ("2026-03-14", or "2026-03" for a month), a number, or a category label ("Groceries"). All x values in one chart use the same type. |
charts[].annotations[].label | string, required. Short note, e.g. "Price change". Limits: 1-32 characters. |
charts[].goal | object. Optional target drawn as a horizontal line, e.g. { "value": 10000 } steps a day. Not for donut or stat. |
charts[].goal.value | number, required. Target value in the chart unit. Limits: -1000000000000-1000000000000. |
charts[].goal.label | string. Optional label, defaults to "Goal". Limits: 1-32 characters. |
charts[].higherIsBetter | boolean. Optional, default true. Set false when a rise is bad (spending, weight), so changes and goals are colored and worded correctly. |
Why these are required: A chart needs a kind and labelled series of { x, y } points (donut: one point per segment, stat: the last point is the number). An empty points list shows "Nothing to show". Display-only: no answer.
When optional fields are left out: The heading is the series names and there is no subtitle; values are plain numbers; dates are written as days, months or years depending on the data; no axis titles, notes or goal line; a rise counts as good; series colors go in order within each chart (series with the same id share a color across charts). Up to 4 charts per page and 6 series per chart; donut takes one series of up to 8 positive segments; stacked bars take values of zero or more. Dates and numbers must be in increasing x order. Every chart gets a generated summary sentence (also the link-preview row) and a hidden data table.
Minimal example
Only the required fields:
{
"version": 1,
"title": "Visitors this week",
"page": {
"type": "chart",
"charts": [
{
"kind": "line",
"series": [
{
"label": "Visitors",
"points": [
{
"x": "2026-10-05",
"y": 120
},
{
"x": "2026-10-06",
"y": 148
},
{
"x": "2026-10-07",
"y": 139
}
]
}
]
}
]
}
}Full example
{
"version": 1,
"title": "Lumen Ceramics, October",
"description": "Your shop at a glance for the month.",
"branding": {
"name": "Lumen Ceramics",
"accentColor": "#7c3aed"
},
"page": {
"type": "chart",
"charts": [
{
"kind": "stat",
"yFormat": "money",
"currency": "USD",
"series": [
{
"id": "revenue",
"label": "Revenue",
"points": [
{
"x": "2025-11",
"y": 12400
},
{
"x": "2025-12",
"y": 15800
},
{
"x": "2026-01",
"y": 11200
},
{
"x": "2026-02",
"y": 11900
},
{
"x": "2026-03",
"y": 13100
},
{
"x": "2026-04",
"y": 13600
},
{
"x": "2026-05",
"y": 14800
},
{
"x": "2026-06",
"y": 14200
},
{
"x": "2026-07",
"y": 15300
},
{
"x": "2026-08",
"y": 16100
},
{
"x": "2026-09",
"y": 16700
},
{
"x": "2026-10",
"y": 18250
}
]
},
{
"id": "order",
"label": "Average order",
"points": [
{
"x": "2026-05",
"y": 69.8
},
{
"x": "2026-06",
"y": 69.3
},
{
"x": "2026-07",
"y": 66.2
},
{
"x": "2026-08",
"y": 66
},
{
"x": "2026-09",
"y": 66.5
},
{
"x": "2026-10",
"y": 76.7
}
]
}
]
},
{
"kind": "line",
"title": "Revenue is pulling away from costs",
"subtitle": "Monthly, USD",
"yFormat": "money",
"currency": "USD",
"annotations": [
{
"x": "2026-08",
"label": "Holiday line"
}
],
"series": [
{
"id": "revenue",
"label": "Revenue",
"points": [
{
"x": "2026-05",
"y": 14800
},
{
"x": "2026-06",
"y": 14200
},
{
"x": "2026-07",
"y": 15300
},
{
"x": "2026-08",
"y": 16100
},
{
"x": "2026-09",
"y": 16700
},
{
"x": "2026-10",
"y": 18250
}
]
},
{
"id": "costs",
"label": "Costs",
"points": [
{
"x": "2026-05",
"y": 9100
},
{
"x": "2026-06",
"y": 9400
},
{
"x": "2026-07",
"y": 9300
},
{
"x": "2026-08",
"y": 9800
},
{
"x": "2026-09",
"y": 9900
},
{
"x": "2026-10",
"y": 10100
}
]
}
]
},
{
"kind": "bar_horizontal",
"title": "Mugs sell best",
"subtitle": "Units sold in October",
"unit": "units",
"series": [
{
"label": "Units sold",
"points": [
{
"x": "Mugs",
"y": 96
},
{
"x": "Bowls",
"y": 71
},
{
"x": "Plates",
"y": 44
},
{
"x": "Vases",
"y": 18
},
{
"x": "Planters",
"y": 9
}
]
}
]
}
]
}
}Send it
eve, AI SDK and MCP
With @pagesystems/eve, @pagesystems/ai-sdk or the MCP server, the model calls get_page_type_schema with "chart" (the same JSON as GET /v1/page-types/chart), then create_page with this input (no version or branding; branding comes from your dashboard):
{
"title": "Lumen Ceramics, October",
"description": "Your shop at a glance for the month.",
"page": {
"type": "chart",
"charts": [
{
"kind": "stat",
"yFormat": "money",
"currency": "USD",
"series": [
{
"id": "revenue",
"label": "Revenue",
"points": [
{
"x": "2025-11",
"y": 12400
},
{
"x": "2025-12",
"y": 15800
},
{
"x": "2026-01",
"y": 11200
},
{
"x": "2026-02",
"y": 11900
},
{
"x": "2026-03",
"y": 13100
},
{
"x": "2026-04",
"y": 13600
},
{
"x": "2026-05",
"y": 14800
},
{
"x": "2026-06",
"y": 14200
},
{
"x": "2026-07",
"y": 15300
},
{
"x": "2026-08",
"y": 16100
},
{
"x": "2026-09",
"y": 16700
},
{
"x": "2026-10",
"y": 18250
}
]
},
{
"id": "order",
"label": "Average order",
"points": [
{
"x": "2026-05",
"y": 69.8
},
{
"x": "2026-06",
"y": 69.3
},
{
"x": "2026-07",
"y": 66.2
},
{
"x": "2026-08",
"y": 66
},
{
"x": "2026-09",
"y": 66.5
},
{
"x": "2026-10",
"y": 76.7
}
]
}
]
},
{
"kind": "line",
"title": "Revenue is pulling away from costs",
"subtitle": "Monthly, USD",
"yFormat": "money",
"currency": "USD",
"annotations": [
{
"x": "2026-08",
"label": "Holiday line"
}
],
"series": [
{
"id": "revenue",
"label": "Revenue",
"points": [
{
"x": "2026-05",
"y": 14800
},
{
"x": "2026-06",
"y": 14200
},
{
"x": "2026-07",
"y": 15300
},
{
"x": "2026-08",
"y": 16100
},
{
"x": "2026-09",
"y": 16700
},
{
"x": "2026-10",
"y": 18250
}
]
},
{
"id": "costs",
"label": "Costs",
"points": [
{
"x": "2026-05",
"y": 9100
},
{
"x": "2026-06",
"y": 9400
},
{
"x": "2026-07",
"y": 9300
},
{
"x": "2026-08",
"y": 9800
},
{
"x": "2026-09",
"y": 9900
},
{
"x": "2026-10",
"y": 10100
}
]
}
]
},
{
"kind": "bar_horizontal",
"title": "Mugs sell best",
"subtitle": "Units sold in October",
"unit": "units",
"series": [
{
"label": "Units sold",
"points": [
{
"x": "Mugs",
"y": 96
},
{
"x": "Bowls",
"y": 71
},
{
"x": "Plates",
"y": 44
},
{
"x": "Vases",
"y": 18
},
{
"x": "Planters",
"y": 9
}
]
}
]
}
]
}
}REST
curl https://pagesystems.ai/v1/pages \
-H "Authorization: Bearer $PAGE_SYSTEMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"spec":{"version":1,"title":"Lumen Ceramics, October","description":"Your shop at a glance for the month.","branding":{"name":"Lumen Ceramics","accentColor":"#7c3aed"},"page":{"type":"chart","charts":[{"kind":"stat","yFormat":"money","currency":"USD","series":[{"id":"revenue","label":"Revenue","points":[{"x":"2025-11","y":12400},{"x":"2025-12","y":15800},{"x":"2026-01","y":11200},{"x":"2026-02","y":11900},{"x":"2026-03","y":13100},{"x":"2026-04","y":13600},{"x":"2026-05","y":14800},{"x":"2026-06","y":14200},{"x":"2026-07","y":15300},{"x":"2026-08","y":16100},{"x":"2026-09","y":16700},{"x":"2026-10","y":18250}]},{"id":"order","label":"Average order","points":[{"x":"2026-05","y":69.8},{"x":"2026-06","y":69.3},{"x":"2026-07","y":66.2},{"x":"2026-08","y":66},{"x":"2026-09","y":66.5},{"x":"2026-10","y":76.7}]}]},{"kind":"line","title":"Revenue is pulling away from costs","subtitle":"Monthly, USD","yFormat":"money","currency":"USD","annotations":[{"x":"2026-08","label":"Holiday line"}],"series":[{"id":"revenue","label":"Revenue","points":[{"x":"2026-05","y":14800},{"x":"2026-06","y":14200},{"x":"2026-07","y":15300},{"x":"2026-08","y":16100},{"x":"2026-09","y":16700},{"x":"2026-10","y":18250}]},{"id":"costs","label":"Costs","points":[{"x":"2026-05","y":9100},{"x":"2026-06","y":9400},{"x":"2026-07","y":9300},{"x":"2026-08","y":9800},{"x":"2026-09","y":9900},{"x":"2026-10","y":10100}]}]},{"kind":"bar_horizontal","title":"Mugs sell best","subtitle":"Units sold in October","unit":"units","series":[{"label":"Units sold","points":[{"x":"Mugs","y":96},{"x":"Bowls","y":71},{"x":"Plates","y":44},{"x":"Vases","y":18},{"x":"Planters","y":9}]}]}]}}}'Validation errors
A spec that does not validate is refused with 400 invalid_spec. The message names each problem and its path, so a model can fix the spec and try again. For example:
Leaving out the required field charts:
✖ Invalid input: expected array, received undefined
→ at page.chartsMisspelling a field name (unknown keys are rejected):
✖ Unrecognized key: "colour"
→ at page