Skip to content
Library · Data & Charts

Data & Charts

The flagship patterns. ReachOut is data-heavy: tables must be dense but effortless to scan, and charts must be beautiful, theme-aware and honest about what their colors mean. Everything on this page is live.

Charts

Renderers live in src/lib/charts/ and read every color from tokens — no hex anywhere. They re-render automatically on theme change and animate once on mount (disabled under reduced-motion).

Line / area chart

renderLineChart(host, { labels, series, area: true }) — trends over time. Gradient area under the first series, hover crosshair, shared tooltip.

Emails sent

Last 7 days · all campaigns

Emails sent 10,580
src: events_hourly
html
import { renderLineChart } from '../../lib/charts/line-chart';

renderLineChart(document.getElementById('chart'), {
  labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
  series: [{ name: 'Emails sent', values: [1420, 1680, 1510, 1890, 2240, 980, 860] }],
  area: true,
});

Grouped bar chart

renderBarChart(host, { labels, series }) — period-over-period or category comparison. Bars grow from the baseline with a light stagger.

Engagement

Weekly opens vs clicks · May 2026

OpensClicks
html
renderBarChart(host, {
  labels: ['W1', 'W2', 'W3', 'W4'],
  series: [
    { name: 'Opens',  values: [4120, 4480, 4310, 4890] },
    { name: 'Clicks', values: [620, 740, 690, 810] },
  ],
});

Donut chart

renderDonutChart(host, { segments, centerLabel }) — share of total. Arcs sweep in on mount; hover thickens the segment.

Traffic by channel

Event volume share

EmailMCPAPI
html
renderDonutChart(host, {
  segments: [
    { label: 'Email', value: 62 },
    { label: 'MCP',   value: 24 },
    { label: 'API',   value: 14 },
  ],
  centerLabel: 'events',
});

Heatmap

renderHeatmap(host, { columns, rows, values }) — intensity matrix using a contiguous slice of the accent ramp (unsigned-range rule).

Send volume by slot

Weekday × time block

Darker = more sendsaccent ramp 100→900
html
renderHeatmap(host, {
  columns: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
  rows: ['09:00', '12:00', '15:00', '18:00'],
  values: [[120, 140, …], …],
});

Sparklines

renderSparkline(host, { values, type }) — tiny inline trends for stat cards and table cells. Line or bar, no axes.

Deliverability98.2%
Open rate41.7%
Agent runs1,204
html
<span class="stat-spark" data-spark='[4,7,5,9,8,12,10,14]'></span>
renderSparkline(el, { values, type: 'line' });

Data tables

Built on .table-wrap + table.data-table. Numeric columns are right-aligned mono with tabular figures; text columns are Inter; status is a badge; change is a delta. One hover rule, one density system.

Full-featured table

Toolbar (title, count, search, actions), sortable headers, primary cell with sub-line, status badges, deltas, progress cell, sparkline cell, row actions, pagination.

Campaigns

6 rows
StatusΔ weekTrendDelivery
Spring launch — EUcmp_01J9…A4 · segment: eu-customers
Sending12,40841.7%↗ +18.2%
64%
Onboarding dripcmp_01J8…F1 · always-on
Live8,92052.3%↗ +4.1%
100%
Re-engagement Q3cmp_01J7…C9 · segment: dormant-90d
Paused4,10418.9%↘ −6.4%
38%
Product update — Julycmp_01J6…B2 · all contacts
Sent21,73038.4%→ +0.2%
100%
Webinar invite — NAcmp_01J5…Z7 · segment: na-leads
Failed2,311↘ −100%
12%
Weekly newsletter #42cmp_01J4…Y3 · subscribers
Scheduled
0%
1–6 of 128 campaigns
12322
html
<div class="table-toolbar">
  <h2 class="table-toolbar-title">Campaigns</h2>
  <span class="table-toolbar-count">6 rows</span>
  <div class="table-toolbar-spacer"></div>
  <input class="table-toolbar-search" type="search" placeholder="Filter…" />
  <button class="btn btn-primary">New campaign</button>
</div>
<div class="table-wrap">
  <table class="data-table">
    <thead>
      <tr>
        <th class="select-col"><input type="checkbox" /></th>
        <th class="sortable" aria-sort="ascending"><button type="button" class="sort-button">Campaign <span class="sort-arrow"></span></button></th>
        <th>Status</th>
        <th class="num">Sent</th>
        <th class="num">Open rate</th>
        <th>Trend</th>
        <th>Progress</th>
        <th class="actions-col"></th>
      </tr>
    </thead>
    <tbody>…</tbody>
  </table>
  <div class="table-pagination">…</div>
</div>

Dense table

.data-table.dense — for admin views, logs and wide schemas. xs cells, tighter padding.

TimestampToolDurationResult
09:32:14.201contacts.upsert84msok
09:32:14.488segments.estimate212msok
09:32:15.102campaigns.send1.4sok
09:32:16.871templates.render46mstimeout
09:32:17.044templates.render38msok
html
<table class="data-table dense">…</table>

Sticky header

.table-wrap.sticky with a max-height — the header stays put while rows scroll.

html
<div class="table-wrap sticky" style="max-height: 240px;">
  <table class="data-table">…</table>
</div>

Table empty state

When a filter or dataset returns nothing, the table body is replaced by the empty state inside the same frame.

No contacts match “churned”

Try a different filter, or ask your agent to broaden the segment.

html
<div class="table-wrap">
  <div class="empty-state">
    <div class="empty-state-icon">⌕</div>
    <h3>No contacts match “churned”</h3>
    <p>Try a different filter, or ask your agent to broaden the segment.</p>
    <button class="btn">Clear filters</button>
  </div>
</div>

Color semantics cheat-sheet

Data meaningColor sourceExample
Signed values (deltas, gains/losses)--positive / --negative / --warning↗ +18.2% ↘ −3.1%
Unsigned ranges (intensity, heatmaps)--accent-100…900 sliceheatmap above
Multi-series categories--chart-1…8 in orderbar & donut above
Brand action / single trend--accentline chart above
Status (live/done/err/warn)badge variantsLive Failed