> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lithoblocks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Visualization blocks

> Draw bar, line, area and pie charts in Slack messages from a plain array of numbers.

The `data_visualization` block puts a chart in a Slack message. Slack draws the chart. LithoBlocks builds the structure Slack draws from, either from a chart you write out yourself or from an array in your data, and keeps it inside Slack's limits so the message sends.

## Prerequisites

* Understanding of [Blocks](/guides/templates-blocks)
* Familiarity with [Placeholders](/guides/templates-placeholders)
* [Sample data](/guides/templates-sample-data) with an array of objects, for dynamic charts

## What Slack draws

Four chart types: `bar`, `line`, `area` and `pie`. Bar, line and area charts have one or more series plotted over a shared set of categories on the x-axis. A pie chart has segments. Colours, the legend, number formatting and axis ranges are Slack's; there is nothing to configure for them.

Charts appear in messages only. Slack does not draw them in modals, and the modal builder does not offer the block.

## Two modes

**Static.** You write the chart in Slack's own shape under `chart`. Strings may carry placeholders. This suits a fixed comparison, or a chart imported from existing Slack JSON.

```json theme={null}
{
  "type": "data_visualization",
  "title": "Revenue by region {{quarter}}",
  "chart": {
    "type": "bar",
    "series": [
      {
        "name": "Revenue",
        "data": [
          { "label": "West", "value": "{{west}}" },
          { "label": "East", "value": 98000 }
        ]
      }
    ],
    "axis_config": { "categories": ["West", "East"], "y_label": "USD" }
  }
}
```

**Dynamic.** You point `arrayPath` at an array in your data and name the fields to read. The engine builds `series`, `data` and `categories` (or `segments` for a pie) when the message compiles. This is the mode to use when a system sends you rows.

```json theme={null}
{
  "type": "data_visualization",
  "title": "Messages sent this week",
  "chart_type": "bar",
  "arrayPath": "days",
  "label_key": "day",
  "series": [{ "name": "Messages", "value_key": "messages" }],
  "axis": { "x_label": "Day", "y_label": "Messages" }
}
```

With this data:

```json theme={null}
{
  "days": [
    { "day": "Mon", "messages": 2300 },
    { "day": "Tue", "messages": "2,150" },
    { "day": "Wed", "messages": 2620 },
    { "day": "Thu", "messages": 2480 },
    { "day": "Fri", "messages": 1980 }
  ]
}
```

it compiles to what Slack receives:

```json theme={null}
{
  "type": "data_visualization",
  "title": "Messages sent this week",
  "chart": {
    "type": "bar",
    "series": [
      {
        "name": "Messages",
        "data": [
          { "label": "Mon", "value": 2300 },
          { "label": "Tue", "value": 2150 },
          { "label": "Wed", "value": 2620 },
          { "label": "Thu", "value": 2480 },
          { "label": "Fri", "value": 1980 }
        ]
      }
    ],
    "axis_config": {
      "categories": ["Mon", "Tue", "Wed", "Thu", "Fri"],
      "x_label": "Day",
      "y_label": "Messages"
    }
  }
}
```

`chart_type` defaults to `bar`. The builder keys (`arrayPath`, `label_key`, `series[].value_key`, `axis`, `overflow` and the rest) never reach Slack; the export replaces them with `chart`.

## The three array shapes

All three read one `arrayPath`. Which fields you name tells the engine which shape the array has.

### One series, one row per category

`label_key` names the field that becomes the x-axis label; each `series[]` entry names a field to read the value from. The example above is this shape: `{ day, messages }` becomes one series called Messages.

### Several series, one row per category

Give `series` one entry per numeric field. Each row supplies one point to every series.

```json theme={null}
{
  "type": "data_visualization",
  "title": "Revenue vs cost",
  "chart_type": "bar",
  "arrayPath": "regions",
  "label_key": "region",
  "series": [
    { "name": "Revenue", "value_key": "revenue" },
    { "name": "Cost", "value_key": "cost" }
  ],
  "axis": { "x_label": "Region", "y_label": "USD" }
}
```

```json theme={null}
{
  "regions": [
    { "region": "West", "revenue": 125000, "cost": 80000 },
    { "region": "East", "revenue": 98000, "cost": 71000 },
    { "region": "North", "revenue": 64000, "cost": 52000 }
  ]
}
```

If a series has no `name`, its `value_key` is used as the name.

### Long format, one row per series and category

Some systems send one row per combination: a platform and a day, a region and a month. Name the field that identifies the series with `series_key` and the numeric field with `value_key`. The engine groups the rows into one series per distinct value of `series_key`, in the order it first sees them.

```json theme={null}
{
  "type": "data_visualization",
  "title": "Concurrent users by platform",
  "chart_type": "area",
  "arrayPath": "rows",
  "label_key": "day",
  "series_key": "platform",
  "value_key": "users",
  "axis": { "x_label": "Day", "y_label": "Users" }
}
```

```json theme={null}
{
  "rows": [
    { "platform": "Desktop", "day": "Mon", "users": 2800 },
    { "platform": "Mobile", "day": "Mon", "users": 1400 },
    { "platform": "Desktop", "day": "Tue", "users": 3050 },
    { "platform": "Mobile", "day": "Tue", "users": 1520 },
    { "platform": "Desktop", "day": "Wed", "users": 3400 },
    { "platform": "Mobile", "day": "Wed", "users": 1700 }
  ]
}
```

This produces two series, Desktop and Mobile, each with Mon, Tue and Wed.

## Pie charts

A pie is the first shape with `chart_type: "pie"`. `label_key` names the segment and `value_key` its size. Slack requires every segment to be positive, so a segment whose value is zero or negative is dropped and reported as a warning. The `Trial` tier below does not appear in the chart.

```json theme={null}
{
  "type": "data_visualization",
  "title": "Plan distribution by tier",
  "chart_type": "pie",
  "arrayPath": "tiers",
  "label_key": "tier",
  "value_key": "count"
}
```

```json theme={null}
{
  "tiers": [
    { "tier": "Free", "count": 540 },
    { "tier": "Pro", "count": 310 },
    { "tier": "Business+", "count": 120 },
    { "tier": "Enterprise", "count": 45 },
    { "tier": "Trial", "count": 0 }
  ]
}
```

A pie has no axis, so `axis` is ignored. A pie with no positive segment at all is a compile error rather than an empty chart.

## Templates instead of keys

`label_key`, `value_key` and `series[].value_key` read a field as it is. When the label or value has to be assembled, use `label_template`, `value_template` or `series[].value_template` with `{{this.field}}`, the same escape hatch `valueTemplate` gives table columns. A template wins over a key when both are present.

```json theme={null}
{
  "type": "data_visualization",
  "title": "Orders by store",
  "chart_type": "bar",
  "arrayPath": "stores",
  "label_template": "{{this.city}} ({{this.code}})",
  "series": [{ "name": "Orders", "value_template": "{{this.orders.count}}" }]
}
```

Whatever the template produces is then read as a number for values, so a template that yields text other than a number becomes 0 with a warning.

## Axis and category order

`axis` takes three optional keys for bar, line and area charts:

* `x_label` and `y_label` name the axes (up to 50 characters each).
* `categories` fixes the x-axis order. Without it, categories are the distinct labels in the order they first appear in the array.

Listing categories also fills gaps. Slack requires every series to carry every category, so a category that no row supplies gets a point with value 0, and the compile reports it. Here the data has no Wednesday:

```json theme={null}
{
  "type": "data_visualization",
  "title": "Weekly active users",
  "chart_type": "line",
  "arrayPath": "points",
  "label_key": "day",
  "series": [{ "name": "Active users", "value_key": "active" }],
  "axis": { "categories": ["Mon", "Tue", "Wed", "Thu", "Fri"], "x_label": "Day", "y_label": "Users" }
}
```

```json theme={null}
{
  "points": [
    { "day": "Tue", "active": 1450 },
    { "day": "Mon", "active": 1200 },
    { "day": "Thu", "active": 1590 },
    { "day": "Fri", "active": 1720 }
  ]
}
```

The chart comes out ordered Mon to Fri, Wed is 0, and the compile carries one warning:

```
Chart "Weekly active users": 1 missing point filled with 0 (Slack requires every category in every series)
```

A row whose label is not in the list is dropped, with a warning, when `categories` is given.

## What the engine normalises, and what it tells you

Slack's rules leave no room for partial data, so the engine repairs the chart and reports what it did. Both modes go through the same steps. The report comes back as `warnings` on compile, send and validate responses over the API and MCP, and the builder's preview shows it beside the chart.

| Situation                                  | What happens                                     | Warning                                          |
| ------------------------------------------ | ------------------------------------------------ | ------------------------------------------------ |
| A series has no point for a category       | The point is added with value 0                  | `N missing points filled with 0`                 |
| A value is a string like `"1,250"`         | It is read as the number 1250, without a warning |                                                  |
| A value is not a number at all             | It becomes 0                                     | `N non-numeric values read as 0`                 |
| A pie segment is 0 or negative             | The segment is dropped                           | `N segments dropped`                             |
| More than 20 categories                    | The chart is truncated to 20                     | `truncated to 20 categories, N dropped`          |
| More than 12 series                        | The chart is truncated to 12                     | `truncated to 12 series, N dropped`              |
| More than 12 pie segments                  | The chart is truncated to 12                     | `N segments dropped`                             |
| A label is outside the listed `categories` | The point is dropped                             | `N points outside the listed categories dropped` |

Truncation keeps the chart sending rather than refusing it, because a digest over a long array must still go out. `overflow` decides which end survives: `"last"` (the default) keeps the most recent entries, `"first"` keeps the earliest.

```json theme={null}
{
  "type": "data_visualization",
  "title": "Net revenue delta (vs. forecast)",
  "chart_type": "line",
  "arrayPath": "daily",
  "label_key": "date",
  "series": [{ "name": "Delta", "value_key": "delta" }],
  "axis": { "x_label": "Date", "y_label": "Delta ($)" },
  "overflow": "last"
}
```

With 25 days in `daily`, the chart shows the last 20 and the compile reports `truncated to 20 categories, 5 dropped`.

Text limits are applied the way they are for every block: a title over 50 characters, or a label or series name over 20, is truncated and counted.

## Limits

|                                | Limit                   |
| ------------------------------ | ----------------------- |
| `title`                        | required, 50 characters |
| Labels and series names        | 20 characters           |
| Axis labels                    | 50 characters           |
| Series                         | 12                      |
| Points per series (categories) | 20                      |
| Pie segments                   | 12, all positive        |
| Charts per message             | 2                       |

A third `data_visualization` block in one compiled message is a compile error, not a truncation. Dropping a chart silently would misrepresent the message, so the compile refuses instead and reports how many charts it found. The count is taken after directives run, so blocks produced by an `#each` count too.

Some inputs are errors rather than warnings: a missing `title`, an unknown chart type, an `arrayPath` that does not point at an array, a dynamic chart without `label_key` or `label_template`, a wide chart without `series`, a long-format chart without `value_key`, and a chart that ends up with no points.

## In the builder

Add the block from the picker, choose the chart type and title, then either switch to dynamic and pick the array from your sample data, or edit the series and points in place. Picking an array suggests a mapping from its keys; the numeric ones are marked. The preview is the engine's own compile of the block over the sample data, with the same warnings a send would carry.

The preview is an approximation. Slack draws the final chart with its own colours, spacing and number formatting, and that is what the recipient sees.

## Common issues

**Every point is 0.** The `value_key` does not match a field in the items, or the field holds text that is not a number. Check the item shape in your sample data and look at the warnings.

**Categories come out in the wrong order.** Without `axis.categories` the order is first-seen order in the array. Either sort the array before sending it or list the categories.

**A series is missing.** In long format, `series_key` must name a field that is present on every row. In wide format, each series needs its own `series[]` entry.

**The message is refused for a third chart.** Two per message is Slack's limit. Split the message or drop a chart.

**Nothing draws in the modal.** Slack does not support the block in modals. Charts go in messages.

## Next steps

<CardGroup cols={2}>
  <Card title="Table blocks" icon="table" href="/guides/templates-table-blocks">
    Show the same rows as a table next to the chart.
  </Card>

  <Card title="Sample data" icon="database" href="/guides/templates-sample-data">
    Structure the arrays a chart reads from.
  </Card>

  <Card title="Directives" icon="list" href="/guides/templates-directives">
    Include a chart only when there is data for it.
  </Card>

  <Card title="Blocks" icon="square" href="/guides/templates-blocks">
    Other block types and layout options.
  </Card>
</CardGroup>
