Data components
Figures, ratings, charts, tables and the world map. map and pin ship in the separate sayshape/map entry point because the country outlines add about 76 KB.
Generated from the component definitions in src/components, the same source the parser validates against and the model guide is built from. Every element also accepts if=, tip=. Each example below is rendered live by the SayShape engine.
stat
A labelled number, for key figures like totals or counts. Add change= to show how it moved, and spark= for a small line of its recent values.
| Prop | Type | Description |
|---|---|---|
label | string, required | Short caption above the value. |
value | string, required | The figure to show, usually an {expression}. |
tone | default / muted / accent / good / warn / bad | Color role: default, muted, accent, good, warn or bad. |
align | left / center | left or center. |
change | string | How the figure moved, shown small beside or under it with ▲ or ▼, e.g. change="+12%" or change="{round(growth, 1)}%". Write the sign; don't add an arrow. |
trend | up / down / flat | up, down or flat: which way the change went, for its arrow and color. Leave it out to take it from change's sign. |
good | up / down | Which way is good news: up (default, colored good) or down, for figures like costs or churn where falling is good. |
spark | expression | A list of numbers, oldest first, drawn as a small line under the value, e.g. spark={[12, 15, 14, 19]}, spark=weekly, or from a list of records spark={pluck(sales, 'revenue')}. |
state price=24 qty=2
stat label="Total" value="${price*qty}"rating
A star rating. Shows value= as stars, or with bind= lets the user pick a rating.
| Prop | Type | Description |
|---|---|---|
value | number | The rating to show, e.g. 4.5. Ignored when bind= is set. |
max | number | How many stars (default 5). |
bind | state | A number state the user sets by pressing a star. |
label | string | What's being rated, read by screen readers, e.g. label="Your rating". Default: the text just above it. |
on | action | Actions to run after the user picks a rating, e.g. emit:changed. Separate several with ; |
state stars=4
rating value=4.5
rating bind=starschart
A chart of a list of records: bar or line for one or more numbers per item, such as revenue and last year's revenue by month, stacked for each item's total split into parts, such as revenue by product per month, area for an amount over time, hbar for a ranking with long names, such as top customers, pie or donut for how one number splits across items, such as sales by region, funnel for how many get through each stage, such as leads to won deals, waterfall for how changes add up to a total, such as revenue minus costs, bullet for each KPI against its target, such as quarterly goals, heatmap for one number across two dimensions, such as retention by cohort, scatter for how two numbers relate, such as price against rating, treemap for how a total splits across many parts, such as costs by line item, or sankey for how amounts flow from sources to where they end up, such as budget to departments to projects. It redraws when the list changes, and shows at most 200 items, 6 series and 6 slices (smaller slices are summed into Other).
| Prop | Type | Description |
|---|---|---|
"text" | text | A title above the chart. |
kind | bar / hbar / line / area / stacked / pie / donut / funnel / waterfall / bullet / heatmap / scatter / treemap / sankey | bar compares categories and always starts at 0; hbar draws the bars across, one row per item with its name on the left and its value at the end, for rankings and long names (top 10 artists or customers): sort the list and take() the top items, since rows are drawn in list order; line shows a trend in list order, scaled to the values; area is a line with the space under it filled down to 0, for amounts over time such as visitors a week; stacked piles each item's y fields into one bar, bottom up in the order written, with its total on top, for parts of a whole such as revenue by product per month; pie and donut show each item's share of the total, and donut puts the total in the middle; funnel shows stages top to bottom in list order, each with its share of the stage before; waterfall shows each item as a change from the running total, rises good and falls bad, with total= adding the final total as a last bar; bullet gives each item a row with its bar against the goal in target=, colored good when on target, warn when close and bad when far off, each row on its own scale so mixed units fit, at most 20 rows; heatmap draws a grid with a row per value of rows= and a column per value of x=, each cell shaded by its y number (darker is more), for patterns such as orders by weekday and hour; scatter puts a dot per item at its x and y numbers, both on number scales, with label= naming each dot, size= sizing it by a number and group= coloring it by category (at most 6), and labels= naming the axes, e.g. labels="Risk,Return"; treemap splits a rectangle into one per item, its area its share of the total, biggest top left in the strongest color, with names where they fit, for when there are too many parts for a pie (up to 60; the smallest are drawn together as one). sankey draws bands between columns of places, each as thick as its amount, from one flow per item: from= and to= name the places and y= the amount; columns are worked out from the flows, and flows that go round in a circle show a message (at most 40 places and 120 flows). pie, donut, funnel, waterfall, bullet, heatmap, scatter, treemap and sankey take one y field; pie, donut, funnel, bullet, treemap and sankey take no negative values. |
items | expression, required | The list of records to chart, e.g. items=sales or items={filter(sales, 'open')}. |
x | name, required | The field holding each item's label (each slice's name in a pie), e.g. x=month. For scatter, the field with each item's x number. Not used by sankey. |
y | names, required | The field holding each item's number, e.g. y=revenue. Bar, hbar and line can compare up to 6 fields on one scale, separated by commas with no spaces, e.g. y=revenue,last_year; each gets its own color and a legend. stacked and area take up to 6 fields too. Items without a number are shown as gaps, not zero. |
labels | string | Bar, hbar, line, area and stacked with several y fields: the legend's names, in the same order, separated by commas, e.g. labels="This year,Last year". Without it the legend shows the field names. Scatter: the two axes' names, x then y, e.g. labels="Risk,Return". |
label | name | Scatter only: the field naming each dot, shown in its tip and the data table, e.g. label=name. |
size | name | Scatter only: the field whose number sizes each dot (its area), for bubbles, e.g. size=assets. |
group | name | Scatter only: the field whose category colors each dot, with a legend, at most 6, e.g. group=type. |
from | name | Sankey only: the field naming where each flow starts, e.g. from=source. |
to | name | Sankey only: the field naming where each flow goes, e.g. to=target. |
rows | name | Heatmap only: the field holding each item's row, e.g. rows=day; x= holds its column and y= its number. |
target | name | Bullet only: the field holding each item's goal, drawn as a dark tick across its bar, e.g. target=goal. |
good | up / down | Bullet only: up when reaching or passing the target is good (sales, signups), down when staying under it is (costs, churn, response time). |
total | string | Waterfall only: the name of an extra last bar showing the total the changes add up to, e.g. total="Net income". |
format | number / money / percent | How values read on the axis, at the ends of hbar bars, in a pie's legend and in a donut's middle: number (37K), money ($37K) or percent (42%), for values that are already percentages, like a rate of 42 or a retention of 61.5. On a scatter it formats y only; x reads as a plain number. |
Children: chart-line, chart-rule, chart-note.
state sales=[{month: "Jan", revenue: 12000, last_year: 10000}, {month: "Feb", revenue: 18000, last_year: 14000}, {month: "Mar", revenue: 15000, last_year: 16000}]
chart "Revenue vs last year" kind=line items=sales x=month y=revenue,last_year labels="This year,Last year" format=moneychart-line
Under a bar or line chart: another field of the same items drawn as a line on top, on the same scale, such as a forecast or last year over this year's bars. Name the chart's own bars in the legend with its labels=, like labels="This year".
| Prop | Type | Description |
|---|---|---|
"text" | text | Its name in the legend. Defaults to the field name. |
y | name, required | The field holding each item's number, e.g. y=forecast. |
Only inside: chart.
state sales=[{month: "Jan", revenue: 12000, last_year: 10000}, {month: "Feb", revenue: 18000, last_year: 14000}, {month: "Mar", revenue: 15000, last_year: 16000}]
chart "Revenue vs last year" kind=line items=sales x=month y=revenue,last_year labels="This year,Last year" format=money
chart-line "Last year" y=last_yearTry chart-line interactively →
chart-rule
Under a bar or line chart: a level drawn across the plot, such as a target, a budget or an average.
| Prop | Type | Description |
|---|---|---|
"text" | text | What the level is, shown at its end, e.g. "Target". |
y | expression, required | The level, a number or a formula, e.g. y=50000 or y={avg(pluck(sales, 'revenue'))}. |
Only inside: chart.
state sales=[{month: "Jan", revenue: 12000, last_year: 10000}, {month: "Feb", revenue: 18000, last_year: 14000}, {month: "Mar", revenue: 15000, last_year: 16000}]
chart "Revenue vs last year" kind=line items=sales x=month y=revenue,last_year labels="This year,Last year" format=money
chart-rule "Target" y=16000Try chart-rule interactively →
chart-note
Under a bar or line chart: a marker at one item with a few words, such as a launch or an outage.
| Prop | Type | Description |
|---|---|---|
"text" | text, required | The words, kept short, e.g. "Launch". |
at | string, required | The item it marks: its x value as shown under the chart, e.g. at="Jun". |
Only inside: chart.
state sales=[{month: "Jan", revenue: 12000, last_year: 10000}, {month: "Feb", revenue: 18000, last_year: 14000}, {month: "Mar", revenue: 15000, last_year: 16000}]
chart "Revenue vs last year" kind=line items=sales x=month y=revenue,last_year labels="This year,Last year" format=money
chart-note "Launch" at="Feb"Try chart-note interactively →
meter
One number against its range, as a ring or a gauge: progress toward a goal, a score, utilization or a budget used. Add bands= to color it bad, warn or good by where the value falls.
| Prop | Type | Description |
|---|---|---|
"text" | text | A short label under the meter, e.g. "Goal reached". |
value | expression, required | The number to show, e.g. value=score or value={round(spent / budget * 100)}. |
kind | ring / gauge | ring is a full circle, for progress toward a goal; gauge is a half circle, for a score or a level, and shows min and max at its ends and its bands. |
min | number | The low end of the range (default 0), a number or an {expression}. |
max | number | The high end of the range (default 100), e.g. max=500 or max={budget}. |
bands | string | One or two values that split the range into bad, warn and good, low to high, e.g. bands="60,85": below 60 is bad, 60 to 85 warn, 85 and above good. One value splits it into bad and good. |
good | up / down | Which end of bands is good: up (default), or down for costs, risk or load, where low is good. |
format | number / money / percent | How the number reads: number (1,200), money ($1,200) or percent, the value's share of the range (82%). |
state score=72
meter "Health score" kind=gauge value=score max=100 bands="60,85"table
Rows of a list of records, one table-column per field, for things like inventory, orders or expenses. With sort= and direction=, pressing a header sorts by it; with bind= and key=, pressing a row selects it; columns too wide for the space scroll sideways, the first one staying in view.
| Prop | Type | Description |
|---|---|---|
"text" | text | A title above the table. |
items | expression, required | The list of records, e.g. items=equipment. At most 200 rows; show a page of a longer list with slice() and pagination. |
key | name | The field that identifies each row, e.g. key=id. Needed for bind=. |
bind | state | State that holds the selected row's key. Pressing a row sets it. |
sort | state | State that holds the field to sort by. Pressing a header sets it; missing values sort last. |
direction | state | State that holds asc or desc. Pressing the sorted header again flips it. |
on | action | Actions to run after a row is selected, with bind= already set, e.g. emit:changed. Separate several with ; |
Children: table-column.
state gear=[{id: "tent", name: "Tent", price: 180}, {id: "stove", name: "Stove", price: 45}] picked="" by="name" dir="asc"
table items=gear key=id bind=picked sort=by direction=dir
table-column "Item" field=name
table-column "Price" field=price format=moneytable-column
One column of a table: its header and the field it shows.
| Prop | Type | Description |
|---|---|---|
"text" | text | The header. Defaults to the field name. |
field | name, required | The field of each record to show, e.g. field=price. |
format | text / number / money | text, number (1,200.5) or money ($1,200.00, US dollars). number and money show only numbers; other values show —. |
align | left / right | left or right. Default: right for number and money, left for text. |
Only inside: table.
state gear=[{id: "tent", name: "Tent", price: 180}, {id: "stove", name: "Stove", price: 45}] picked="" by="name" dir="asc"
table items=gear key=id bind=picked sort=by direction=dir
table-column "Price" field=price format=moneyTry table-column interactively →
compare
Two to four options side by side with one row per attribute, marking the best value in each row: plans, products or choices to weigh up. Use compare-row children.
| Prop | Type | Description |
|---|---|---|
"text" | text | A title above the comparison. |
items | expression, required | The options, a list of records, e.g. items=plans. Two to four. |
name | name | The field holding each option's name, shown at the top of its column (default name). |
pick | string | The name of the option to recommend: its column is highlighted and marked Recommended. |
Children: compare-row.
state plans=[{name: "Basic", price: 9, storage: 50, support: "Email"}, {name: "Pro", price: 19, storage: 500, support: "Chat"}, {name: "Team", price: 49, storage: 2000, support: "Phone"}]
compare items=plans pick="Pro"
compare-row "Price a month" field=price format=money best=low
compare-row "Storage (GB)" field=storage format=number best=high
compare-row "Support" field=supportcompare-row
One row of a comparison: an attribute and the field each option's value comes from.
| Prop | Type | Description |
|---|---|---|
"text" | text | The attribute's name. Defaults to the field name. |
field | name, required | The field of each option to show, e.g. field=price. true and false show as a check and a dash. |
format | text / number / money | text, number (1,200.5) or money ($1,200.00). |
best | low / high | low or high: mark the lowest or highest number in the row as the best. Leave out when neither is better. |
Only inside: compare.
state plans=[{name: "Basic", price: 9, storage: 50, support: "Email"}, {name: "Pro", price: 19, storage: 500, support: "Chat"}, {name: "Team", price: 49, storage: 2000, support: "Phone"}]
compare items=plans pick="Pro"
compare-row "Price a month" field=price format=money best=lowTry compare-row interactively →
details
One record as label and value rows, for a single customer, order, policy or ticket: its fields one per line, with an optional status badge. Use detail children; for many records use a table.
| Prop | Type | Description |
|---|---|---|
"text" | text | A title above the rows, usually the record's name. |
subtitle | string | A muted line under the title, e.g. the company and city. |
Children: detail.
state renewal="Jan 14, 2027"
details "Ada Lovelace" subtitle="Analytical Engines Ltd · London"
detail "Plan" value="Enterprise · 240 seats"
detail "Renewal" value={renewal}
detail "Status" value="At risk" tone=baddetail
One row of details: a field's name and its value.
| Prop | Type | Description |
|---|---|---|
"text" | text, required | The field's name, e.g. "Plan". |
value | string, required | The value to show, usually an {expression}, e.g. value={order.total} or value="{money(total)}". Empty shows —. |
tone | default / muted / accent / good / warn / bad | Show the value as a colored badge, for a status: good, warn, bad, accent or muted. Leave out for plain text. |
Only inside: details.
state renewal="Jan 14, 2027"
details "Ada Lovelace" subtitle="Analytical Engines Ltd · London"
detail "Status" value="At risk" tone=badschedule
Blocks laid out on time from a list: a week of shifts, the day's room bookings, an itinerary by hour (times as HH:MM), or a project plan across dates (view=timeline, dates as YYYY-MM-DD). Overlapping blocks sit side by side; it scrolls sideways when wide.
| Prop | Type | Description |
|---|---|---|
"text" | text | The title above it, e.g. Support rota. |
items | expression, required | The list of records, e.g. items=shifts. At most 200. |
view | day / week / timeline | day: one day down a time axis, a column per rows= value. week: a column per day, at most 7. timeline: dates across, a bar per item, with a Today line. |
date | name | Field with each item's day as YYYY-MM-DD. In a timeline, used for an item with no start date (a one-day bar). |
start | name | Field with the start: a time as HH:MM (09:30) in day and week, a date as YYYY-MM-DD in a timeline. |
end | name | Field with the end, written the same way. Missing: an hour after the start, or the same day in a timeline. |
label | name | Field with what each block says, e.g. label=name. |
rows | name | Field that splits blocks into lanes, e.g. rows=room: a column each in day view, a row each in a timeline; shown on the block in week view. |
tone | name | Field that colors each block: a value good, warn, bad, accent or muted uses that tone; other values (a team, a type) each get their own color, with a key. |
key | name | The field that identifies each item, e.g. key=id. Needed for bind=. |
bind | state | State that holds the selected item's key. Pressing a block sets it. |
on | action | Actions to run after a block is pressed, with bind= already set, e.g. emit:changed. Separate several with ; |
state picked=0 shifts=[{id: 1, day: "2026-11-09", from: "08:00", to: "16:00", name: "Ana Ruiz", tier: "Tier 1"}, {id: 2, day: "2026-11-09", from: "12:00", to: "20:00", name: "Ben Okafor", tier: "Tier 2"}, {id: 3, day: "2026-11-10", from: "08:00", to: "16:00", name: "Chloe Park", tier: "Tier 1"}]
schedule "Support rota" items=shifts date=day start=from end=to label=name tone=tier view=week bind=pickedboard
Columns of cards by stage, from a list: a sales pipeline, a task board, hiring stages, support tickets. Each column shows its count and an optional total; move=true lets people drag a card to another stage.
| Prop | Type | Description |
|---|---|---|
"text" | text | The title above the board, e.g. Sales pipeline. |
items | expression, required | The list of records, e.g. items=deals. At most 500. |
stage | name, required | Field with each record's stage, e.g. stage=status. |
stages | string | The columns in order, separated by commas, e.g. stages="Lead,Qualified,Proposal,Won". At most 12. Default: the stages in the records, in the order they first appear. |
title | name | Field with each card's title, e.g. title=name. |
detail | name | Field with a quieter line under the title, e.g. detail=company. |
value | name | Field with a short value on the card's right, e.g. value=amount. |
format | text / number / money | How value= and sum= read: text as is, number (1,200) or money ($1,200, US dollars). |
sum | name | Field to total in each column's header, e.g. sum=amount, written with format=. |
tone | name | Field that colors each card's edge: a value good, warn, bad, accent or muted uses that tone; other values each get their own color. |
key | name | The field that identifies each record, e.g. key=id. Needed for bind=. |
bind | state | State that holds the selected card's key. Pressing a card sets it. |
move | boolean | true lets people move a card to another stage: drag it to another column, or focus it and press Left or Right. It changes the record's stage field, so items= must be a list in state (or worked out from one). |
on | action | Actions to run after a card is pressed or moved, with bind= already set, e.g. emit:changed. Separate several with ; |
state picked=0 deals=[{id: 1, name: "Fleet renewal", company: "Harbor Freight Co", status: "Lead", amount: 48000}, {id: 2, name: "Claims portal", company: "Northwind Mutual", status: "Proposal", amount: 125000}, {id: 3, name: "Warehouse sensors", company: "Larkspur Logistics", status: "Qualified", amount: 36500}]
board "Pipeline" items=deals stage=status stages="Lead,Qualified,Proposal,Won" title=name detail=company value=amount format=money sum=amount bind=pickedtree
A hierarchy from a flat list where each record names its parent: an org chart, folders, a budget by department. kind=list (default) is indented rows that open and close; kind=org is a top-down chart of boxes.
| Prop | Type | Description |
|---|---|---|
"text" | text | The title above the tree, e.g. Org chart. |
items | expression, required | The list of records, e.g. items=people. At most 300. |
id | name | The field that identifies each record, e.g. id=id. |
parent | name | The field with the parent's id, e.g. parent=manager. Empty for the top. |
label | name | The field with what each item says, e.g. label=name. |
detail | name | The field with a quieter line, e.g. detail=role. |
value | name | The field with a number on the right, e.g. value=headcount. |
format | text / number / money | How value= reads: text as is, number (1,200) or money ($1,200, US dollars). |
kind | list / org | list: indented rows that open and close. org: a top-down chart with boxes and lines, which scrolls sideways when wide. |
open | number | How many levels of a list start open; 0 shows only the top. |
bind | state | State that holds the selected item's id. Pressing an item sets it. |
on | action | Actions to run after an item is selected, with bind= already set, e.g. emit:changed. Separate several with ; |
state picked="" people=[{id: "e1", name: "Ana Ruiz", role: "COO"}, {id: "e2", name: "Ben Okafor", role: "Head of Claims", manager: "e1"}, {id: "e3", name: "Chloe Park", role: "Head of Operations", manager: "e1"}, {id: "e4", name: "Dev Malhotra", role: "Claims lead", manager: "e2"}]
tree "Northwind Mutual" items=people parent=manager label=name detail=role bind=pickedflow
Boxes joined by arrows, laid out automatically from a list of steps and where each leads: an approval process, a customer journey, a decision tree. Never give positions.
| Prop | Type | Description |
|---|---|---|
"text" | text | The title above the flow, e.g. Claim approval. |
items | expression, required | The list of steps, e.g. items=steps. At most 40. |
id | name | The field that identifies each step. |
label | name | The field with what each box says. |
next | name | The field with where a step leads: one id, a list of ids, or ids separated by commas. Leave it empty on the last step. |
kind | name | The field with each step's shape: step (a box), decision (a diamond), start or end (a pill). |
edges | expression | Labels for arrows, as a list of {from, to, label}, e.g. the Yes and No out of a decision. |
direction | down / right | Which way it runs: down or right. When right doesn't fit its space, it runs down instead. |
bind | state | State that holds the selected step's id. Pressing a step sets it. |
on | action | Actions to run after a step is selected, with bind= already set, e.g. emit:changed. Separate several with ; |
state picked="" steps=[{id: "in", label: "Claim filed", kind: "start", next: "check"}, {id: "check", label: "Under $5,000?", kind: "decision", next: "auto,review"}, {id: "auto", label: "Approve automatically", next: "pay"}, {id: "review", label: "Adjuster review", next: "pay"}, {id: "pay", label: "Pay out", kind: "end"}]
state edges=[{from: "check", to: "auto", label: "Yes"}, {from: "check", to: "review", label: "No"}]
flow "Claim approval" items=steps edges=edges bind=pickedmap
Canvas map with pin markers, data-driven country choropleths and proportional bubble maps. For data maps, give items plus geographic and numeric field names; Canvas handles colors, sizing and legends.
| Prop | Type | Description |
|---|---|---|
kind | markers / choropleth / bubble | markers uses pin children; choropleth colors countries from dataset rows; bubble plots sized locations from dataset rows. |
items | expression | Data rows for kind=choropleth or kind=bubble, e.g. items=sales. Do not embed individual rows in map specs. |
region | name | Country-name column for choropleth, e.g. region=country. |
value | name | Numeric column used for country shading or proportional bubble area. |
lat | name | Latitude column for bubble maps. |
lon | name | Longitude column for bubble maps. |
label | name | Location-name column for bubble maps. |
legend | boolean | Show a Canvas legend; defaults on for choropleth and bubble, off for markers. |
legendTitle | string | Legend title, e.g. Revenue ($) or Project value. |
highlight | string | Comma-separated country names to tint, e.g. Japan,Brazil. |
bind | state | State name that receives the clicked country or marker label. |
fit | world / pins | Initial framing: whole world, or zoom to the current pin locations. Re-fits when the pins change. |
cluster | boolean | Group nearby pins into clickable counts. Zooming in separates them. |
interactive | boolean | Enable mouse-wheel zoom and drag pan. |
controls | boolean | Show Canvas zoom in, zoom out and reset controls. Opt in to preserve the original minimal map appearance. |
route | boolean | Join the pins with a line in the order they are written, for a trip or a delivery run. |
Children: pin.
state selected=""
map bind=selected fit=pins cluster=true controls=true
pin "Phoenix" lat=33.4484 lon=-112.074 detail="Status: delayed | Contract: 120000" tone=bad size=8pin
A labelled point on a Canvas map. The optional detail is shown in a multiline hover tooltip.
| Prop | Type | Description |
|---|---|---|
"text" | text, required | The place name shown next to the pin. |
lat | number, required | Latitude in degrees (-90 to 90). |
lon | number, required | Longitude in degrees (-180 to 180). |
detail | string | Tooltip details; separate lines with |. Supports interpolated dataset expressions. |
tone | ink / accent / good / warn / bad / muted | Theme color: ink, accent, good, warn, bad or muted. |
size | number | Marker radius in CSS pixels, clamped to 2..18. Supports expressions. |
Only inside: map.
state selected=""
map bind=selected fit=pins cluster=true controls=true
pin "Phoenix" lat=33.4484 lon=-112.074 tone=bad size=9 detail="Status: delayed | Value: 120000"