Skip to content

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.

PropTypeDescription
labelstring, requiredShort caption above the value.
valuestring, requiredThe figure to show, usually an {expression}.
tonedefault / muted / accent / good / warn / badColor role: default, muted, accent, good, warn or bad.
alignleft / centerleft or center.
changestringHow 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.
trendup / down / flatup, down or flat: which way the change went, for its arrow and color. Leave it out to take it from change's sign.
goodup / downWhich way is good news: up (default, colored good) or down, for figures like costs or churn where falling is good.
sparkexpressionA 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')}.
sayshape
state price=24 qty=2
stat label="Total" value="${price*qty}"
Rendered by the SayShape engineOpen in playground

Try stat interactively →

rating ​

A star rating. Shows value= as stars, or with bind= lets the user pick a rating.

PropTypeDescription
valuenumberThe rating to show, e.g. 4.5. Ignored when bind= is set.
maxnumberHow many stars (default 5).
bindstateA number state the user sets by pressing a star.
labelstringWhat's being rated, read by screen readers, e.g. label="Your rating". Default: the text just above it.
onactionActions to run after the user picks a rating, e.g. emit:changed. Separate several with ;
sayshape
state stars=4
rating value=4.5
rating bind=stars
Rendered by the SayShape engineOpen in playground

Try rating interactively →

chart ​

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).

PropTypeDescription
"text"textA title above the chart.
kindbar / hbar / line / area / stacked / pie / donut / funnel / waterfall / bullet / heatmap / scatter / treemap / sankeybar 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.
itemsexpression, requiredThe list of records to chart, e.g. items=sales or items={filter(sales, 'open')}.
xname, requiredThe 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.
ynames, requiredThe 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.
labelsstringBar, 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".
labelnameScatter only: the field naming each dot, shown in its tip and the data table, e.g. label=name.
sizenameScatter only: the field whose number sizes each dot (its area), for bubbles, e.g. size=assets.
groupnameScatter only: the field whose category colors each dot, with a legend, at most 6, e.g. group=type.
fromnameSankey only: the field naming where each flow starts, e.g. from=source.
tonameSankey only: the field naming where each flow goes, e.g. to=target.
rowsnameHeatmap only: the field holding each item's row, e.g. rows=day; x= holds its column and y= its number.
targetnameBullet only: the field holding each item's goal, drawn as a dark tick across its bar, e.g. target=goal.
goodup / downBullet only: up when reaching or passing the target is good (sales, signups), down when staying under it is (costs, churn, response time).
totalstringWaterfall only: the name of an extra last bar showing the total the changes add up to, e.g. total="Net income".
formatnumber / money / percentHow 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.

sayshape
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
Rendered by the SayShape engineOpen in playground

Try chart interactively →

chart-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".

PropTypeDescription
"text"textIts name in the legend. Defaults to the field name.
yname, requiredThe field holding each item's number, e.g. y=forecast.

Only inside: chart.

sayshape
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_year
Rendered by the SayShape engineOpen in playground

Try 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.

PropTypeDescription
"text"textWhat the level is, shown at its end, e.g. "Target".
yexpression, requiredThe level, a number or a formula, e.g. y=50000 or y={avg(pluck(sales, 'revenue'))}.

Only inside: chart.

sayshape
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=16000
Rendered by the SayShape engineOpen in playground

Try 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.

PropTypeDescription
"text"text, requiredThe words, kept short, e.g. "Launch".
atstring, requiredThe item it marks: its x value as shown under the chart, e.g. at="Jun".

Only inside: chart.

sayshape
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"
Rendered by the SayShape engineOpen in playground

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.

PropTypeDescription
"text"textA short label under the meter, e.g. "Goal reached".
valueexpression, requiredThe number to show, e.g. value=score or value={round(spent / budget * 100)}.
kindring / gaugering 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.
minnumberThe low end of the range (default 0), a number or an {expression}.
maxnumberThe high end of the range (default 100), e.g. max=500 or max={budget}.
bandsstringOne 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.
goodup / downWhich end of bands is good: up (default), or down for costs, risk or load, where low is good.
formatnumber / money / percentHow the number reads: number (1,200), money ($1,200) or percent, the value's share of the range (82%).
sayshape
state score=72
meter "Health score" kind=gauge value=score max=100 bands="60,85"
Rendered by the SayShape engineOpen in playground

Try meter interactively →

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.

PropTypeDescription
"text"textA title above the table.
itemsexpression, requiredThe list of records, e.g. items=equipment. At most 200 rows; show a page of a longer list with slice() and pagination.
keynameThe field that identifies each row, e.g. key=id. Needed for bind=.
bindstateState that holds the selected row's key. Pressing a row sets it.
sortstateState that holds the field to sort by. Pressing a header sets it; missing values sort last.
directionstateState that holds asc or desc. Pressing the sorted header again flips it.
onactionActions to run after a row is selected, with bind= already set, e.g. emit:changed. Separate several with ;

Children: table-column.

sayshape
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=money
Rendered by the SayShape engineOpen in playground

Try table interactively →

table-column ​

One column of a table: its header and the field it shows.

PropTypeDescription
"text"textThe header. Defaults to the field name.
fieldname, requiredThe field of each record to show, e.g. field=price.
formattext / number / moneytext, number (1,200.5) or money ($1,200.00, US dollars). number and money show only numbers; other values show —.
alignleft / rightleft or right. Default: right for number and money, left for text.

Only inside: table.

sayshape
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=money
Rendered by the SayShape engineOpen in playground

Try 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.

PropTypeDescription
"text"textA title above the comparison.
itemsexpression, requiredThe options, a list of records, e.g. items=plans. Two to four.
namenameThe field holding each option's name, shown at the top of its column (default name).
pickstringThe name of the option to recommend: its column is highlighted and marked Recommended.

Children: compare-row.

sayshape
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=support
Rendered by the SayShape engineOpen in playground

Try compare interactively →

compare-row ​

One row of a comparison: an attribute and the field each option's value comes from.

PropTypeDescription
"text"textThe attribute's name. Defaults to the field name.
fieldname, requiredThe field of each option to show, e.g. field=price. true and false show as a check and a dash.
formattext / number / moneytext, number (1,200.5) or money ($1,200.00).
bestlow / highlow or high: mark the lowest or highest number in the row as the best. Leave out when neither is better.

Only inside: compare.

sayshape
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
Rendered by the SayShape engineOpen in playground

Try 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.

PropTypeDescription
"text"textA title above the rows, usually the record's name.
subtitlestringA muted line under the title, e.g. the company and city.

Children: detail.

sayshape
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=bad
Rendered by the SayShape engineOpen in playground

Try details interactively →

detail ​

One row of details: a field's name and its value.

PropTypeDescription
"text"text, requiredThe field's name, e.g. "Plan".
valuestring, requiredThe value to show, usually an {expression}, e.g. value={order.total} or value="{money(total)}". Empty shows —.
tonedefault / muted / accent / good / warn / badShow the value as a colored badge, for a status: good, warn, bad, accent or muted. Leave out for plain text.

Only inside: details.

sayshape
state renewal="Jan 14, 2027"
details "Ada Lovelace" subtitle="Analytical Engines Ltd · London"
  detail "Status" value="At risk" tone=bad
Rendered by the SayShape engineOpen in playground

Try detail interactively →

schedule ​

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.

PropTypeDescription
"text"textThe title above it, e.g. Support rota.
itemsexpression, requiredThe list of records, e.g. items=shifts. At most 200.
viewday / week / timelineday: 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.
datenameField with each item's day as YYYY-MM-DD. In a timeline, used for an item with no start date (a one-day bar).
startnameField with the start: a time as HH:MM (09:30) in day and week, a date as YYYY-MM-DD in a timeline.
endnameField with the end, written the same way. Missing: an hour after the start, or the same day in a timeline.
labelnameField with what each block says, e.g. label=name.
rowsnameField 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.
tonenameField 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.
keynameThe field that identifies each item, e.g. key=id. Needed for bind=.
bindstateState that holds the selected item's key. Pressing a block sets it.
onactionActions to run after a block is pressed, with bind= already set, e.g. emit:changed. Separate several with ;
sayshape
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=picked
Rendered by the SayShape engineOpen in playground

Try schedule interactively →

board ​

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.

PropTypeDescription
"text"textThe title above the board, e.g. Sales pipeline.
itemsexpression, requiredThe list of records, e.g. items=deals. At most 500.
stagename, requiredField with each record's stage, e.g. stage=status.
stagesstringThe 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.
titlenameField with each card's title, e.g. title=name.
detailnameField with a quieter line under the title, e.g. detail=company.
valuenameField with a short value on the card's right, e.g. value=amount.
formattext / number / moneyHow value= and sum= read: text as is, number (1,200) or money ($1,200, US dollars).
sumnameField to total in each column's header, e.g. sum=amount, written with format=.
tonenameField that colors each card's edge: a value good, warn, bad, accent or muted uses that tone; other values each get their own color.
keynameThe field that identifies each record, e.g. key=id. Needed for bind=.
bindstateState that holds the selected card's key. Pressing a card sets it.
movebooleantrue 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).
onactionActions to run after a card is pressed or moved, with bind= already set, e.g. emit:changed. Separate several with ;
sayshape
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=picked
Rendered by the SayShape engineOpen in playground

Try board interactively →

tree ​

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.

PropTypeDescription
"text"textThe title above the tree, e.g. Org chart.
itemsexpression, requiredThe list of records, e.g. items=people. At most 300.
idnameThe field that identifies each record, e.g. id=id.
parentnameThe field with the parent's id, e.g. parent=manager. Empty for the top.
labelnameThe field with what each item says, e.g. label=name.
detailnameThe field with a quieter line, e.g. detail=role.
valuenameThe field with a number on the right, e.g. value=headcount.
formattext / number / moneyHow value= reads: text as is, number (1,200) or money ($1,200, US dollars).
kindlist / orglist: indented rows that open and close. org: a top-down chart with boxes and lines, which scrolls sideways when wide.
opennumberHow many levels of a list start open; 0 shows only the top.
bindstateState that holds the selected item's id. Pressing an item sets it.
onactionActions to run after an item is selected, with bind= already set, e.g. emit:changed. Separate several with ;
sayshape
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=picked
Rendered by the SayShape engineOpen in playground

Try tree interactively →

flow ​

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.

PropTypeDescription
"text"textThe title above the flow, e.g. Claim approval.
itemsexpression, requiredThe list of steps, e.g. items=steps. At most 40.
idnameThe field that identifies each step.
labelnameThe field with what each box says.
nextnameThe field with where a step leads: one id, a list of ids, or ids separated by commas. Leave it empty on the last step.
kindnameThe field with each step's shape: step (a box), decision (a diamond), start or end (a pill).
edgesexpressionLabels for arrows, as a list of {from, to, label}, e.g. the Yes and No out of a decision.
directiondown / rightWhich way it runs: down or right. When right doesn't fit its space, it runs down instead.
bindstateState that holds the selected step's id. Pressing a step sets it.
onactionActions to run after a step is selected, with bind= already set, e.g. emit:changed. Separate several with ;
sayshape
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=picked
Rendered by the SayShape engineOpen in playground

Try flow interactively →

map ​

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.

PropTypeDescription
kindmarkers / choropleth / bubblemarkers uses pin children; choropleth colors countries from dataset rows; bubble plots sized locations from dataset rows.
itemsexpressionData rows for kind=choropleth or kind=bubble, e.g. items=sales. Do not embed individual rows in map specs.
regionnameCountry-name column for choropleth, e.g. region=country.
valuenameNumeric column used for country shading or proportional bubble area.
latnameLatitude column for bubble maps.
lonnameLongitude column for bubble maps.
labelnameLocation-name column for bubble maps.
legendbooleanShow a Canvas legend; defaults on for choropleth and bubble, off for markers.
legendTitlestringLegend title, e.g. Revenue ($) or Project value.
highlightstringComma-separated country names to tint, e.g. Japan,Brazil.
bindstateState name that receives the clicked country or marker label.
fitworld / pinsInitial framing: whole world, or zoom to the current pin locations. Re-fits when the pins change.
clusterbooleanGroup nearby pins into clickable counts. Zooming in separates them.
interactivebooleanEnable mouse-wheel zoom and drag pan.
controlsbooleanShow Canvas zoom in, zoom out and reset controls. Opt in to preserve the original minimal map appearance.
routebooleanJoin the pins with a line in the order they are written, for a trip or a delivery run.

Children: pin.

sayshape
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=8
Rendered by the SayShape engineOpen in playground

Try map interactively →

pin ​

A labelled point on a Canvas map. The optional detail is shown in a multiline hover tooltip.

PropTypeDescription
"text"text, requiredThe place name shown next to the pin.
latnumber, requiredLatitude in degrees (-90 to 90).
lonnumber, requiredLongitude in degrees (-180 to 180).
detailstringTooltip details; separate lines with |. Supports interpolated dataset expressions.
toneink / accent / good / warn / bad / mutedTheme color: ink, accent, good, warn, bad or muted.
sizenumberMarker radius in CSS pixels, clamped to 2..18. Supports expressions.

Only inside: map.

sayshape
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"
Rendered by the SayShape engineOpen in playground

Try pin interactively →

SayShape v0.1 is an early prototype under the MIT License. Status · GitHub