# 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 `kind` for 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 `title` as the takeaway ("Spending fell 12% in October") and use `subtitle` for 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](/docs/api#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:

```json
{
  "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

```json
{
  "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`](/docs#quickstart-eve), [`@pagesystems/ai-sdk`](/docs#quickstart-ai-sdk) or the [MCP server](/docs#quickstart-mcp), the model calls `get_page_type_schema` with `"chart"` (the same JSON as [`GET /v1/page-types/chart`](/docs/api#endpoints)), then `create_page` with this input (no `version` or `branding`; branding comes from your dashboard):

```json
{
  "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

```bash
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`:**

```text
✖ Invalid input: expected array, received undefined
  → at page.charts
```

**Misspelling a field name (unknown keys are rejected):**

```text
✖ Unrecognized key: "colour"
  → at page
```
