Foundation

Charts & data visualization

How to pick the right chart, color palettes, axis and legend rules, accessibility, and the criteria that make a chart feel intuitive.

Status: alpha

This component is currently in alpha status.

Charts & data visualization

A chart is an answer to a question. The job of every chart in Marmo is to make a single question easier to answer than reading a row of numbers — and to do it in a way the eye and brain understand without instructions.

This page covers what to draw, how to draw it, how it should behave, and — at the end — the criteria a chart must clear before it ships, drawn from cognitive psychology and interaction-design fundamentals.

Status: alpha. Library choice is Recharts (see § Library).


Principles

Six rules. They override anything else on this page.

  1. One question per chart. Every chart answers a single, named question. Two questions = two charts.
  2. No distortion. Bar baselines start at zero. No 3D. No truncated axes that exaggerate change. No inverted scales without an explicit label.
  3. Color is meaning. Reusing the same color for the same series across the product is a contract with the reader. Don't break it.
  4. Color is never the only channel. Every color signal also exists as text, icon, position, or pattern.
  5. A chart without a table is incomplete. Every meaningful chart ships with a tabular alternative — for screen readers, for export, for copy-paste.
  6. One clear chart beats six dense tiles. A scorecard plus one well-chosen chart almost always beats a 6-up grid of half-thoughts.
One question · one highlighted series · neutral comparison · sparse gridlines · tabular figures.

Pick the right chart

Start from the question, not the data shape.

QuestionReach forAvoid
How does X compare across categories?Horizontal bar (≥ 5 categories) · Vertical column (≤ 5)Pie chart
How has X changed over time?Line (continuous) · Column (discrete buckets)Line with too few points (< 4)
What is X made of right now?Stacked bar · Treemap · Donut (≤ 4 slices)Pie (> 4 slices), nested donuts
How has X's composition shifted over time?Stacked area (relative) · Stacked column (absolute)Two pie charts side by side
How are values distributed?Histogram · Box plot · DensityBar chart of raw values
Is X correlated with Y?Scatter · Bubble (third dimension)Line chart
Is X above / below a target?Bullet · Gauge (sparingly) · Scorecard with deltaSpeedometer gauges
Where is X happening?Choropleth · Symbol mapPie on a map
What's the single number right now?Scorecard / KPI · Sparkline beside itGauge taking 1/4 of the page
Where does the funnel leak?Funnel · SankeyStacked bar
What's the hierarchy?Treemap · Sunburst (sparingly)Nested pies

Rules of thumb

  • Pie allowed only when there are ≤ 4 slices summing to a meaningful 100%. Otherwise use a bar chart.
  • Two y-axes mislead more than they explain. Split into two charts.
  • 3D: never.
Compare across categories?
Bar (horizontal ≥ 5; vertical ≤ 5)
How has X changed over time?
Line (continuous) · Column (discrete)
What is X made of?
Donut (≤ 4 slices) · Stacked bar · Treemap
How has composition shifted?
Stacked area / column
Is X correlated with Y?
Scatter · Bubble (third dim)
Above / below a target?
Bullet · Scorecard with delta
Start from the question — the answer column is the recommended chart.

Chart catalog

Each chart has a single best-fit and one or two anti-patterns. Treat this as the source of truth when proposing dashboard tiles.

ChartBest forDon't use for
Bar (horizontal)Ranking many categoriesTime series
Column (vertical bar)Comparing few categories or discrete time bucketsLong category names
LineContinuous trends, ≥ 4 time points, 1–5 seriesCategorical comparison
AreaOne trend + magnitude emphasisMany overlapping series
Stacked areaComposition shifting over timeMore than 5 series
Stacked bar / columnParts-of-whole across categoriesWhen totals aren't meaningful
100% stackedComposition only (totals normalized)When absolute values matter
HistogramDistribution of one variableCategorical data
Box plotSpread + outliers across groupsCasual audiences
ScatterRelationship between two variablesSingle-variable summaries
BubbleThree-variable relationship (x, y, size)Qualitative third variable
DonutSingle proportion (≤ 4 slices), KPI centerComparing precise values
TreemapHierarchical composition, many categoriesContinuous trends
FunnelConversion or drop-off across stagesNon-sequential categories
SankeyFlows between nodesSimple part-to-whole
ChoroplethRegional value comparisonCities / point data
Symbol mapPoint events with magnitudeRegion-level summary
SparklineTrend hint inside a row, KPI, or table cellAnything requiring axes
Scorecard / KPISingle number + delta vs. targetMore than one metric
Bullet chartSingle value vs. target + rangesMany series
Bar (horizontal) — ranking categories.
Line — trend over time.
Stacked area — composition over time.
Scatter — relationship between two variables.
  • Free56%
  • Pro28%
  • Team13%
  • Enterprise3%
Donut — single proportion (≤ 4 slices).
12,480
+312 last 7 days
Sparkline — inline trend hint beside a KPI.

Color — palette types

Four palette archetypes carry every chart. Pick the palette by the kind of data, not by the brand.

PaletteUse when…Visual rule
Categorical (qualitative)Series have no order (regions, products, channels, referral sources).One hue per category. Up to 10 distinct hues in the Marmo palette; beyond that, group the long tail into "Other".
Sequential (quantitative, one direction)Single quantitative scale (heatmap intensity, density).One hue, varying lightness — light → dark = low → high.
Diverging (quantitative, two directions around a midpoint)Values diverge from a meaningful center (above/below target, year-over-year change).Two hues meeting at a near-neutral midpoint.
MonochromaticSingle trend or relationship chart where shape > category.One hue, one or two values.
teal
violet
blue
orange
pink
amber
green
indigo
mint
red
Categorical — up to 10 hues. Add an 'Other' bucket beyond ten.
Low (200)High (700)
Sequential — one hue, varying lightness (low → high).
Below targetNeutralAbove target
Diverging — two hues meeting at a neutral midpoint.
Primary bandComparison / secondary
Monochromatic — one hue + `-bright` sibling for same-variable bands.

Categorical tokens — how to use them

Marmo ships ten categorical hues. Each hue has a -bright sibling for emphasis bands inside the same category. Use the hue name, not a chart index — designers reference categories by name in Figma, and tokens line up 1:1.

SwatchTokenHueUse for
--color-chart-cat-tealBrand tealThe most important / brand series, or the leader in a ranked list.
--color-chart-cat-violetVioletSecondary category.
--color-chart-cat-blueBlueThird category.
--color-chart-cat-orangeOrangeFourth category.
--color-chart-cat-pinkPinkFifth category.
--color-chart-cat-amberAmberSixth category. Not for warnings — that's a semantic role.
--color-chart-cat-greenGreenSeventh category. Not for "success" — semantic role.
--color-chart-cat-indigoIndigoEighth category.
--color-chart-cat-mintMintNinth category.
--color-chart-cat-redRedTenth category. Not for "error" — semantic role.

The -bright siblings (--color-chart-cat-teal-bright, --color-chart-cat-violet-bright, …) are paler steps of the same hue:

teal + teal-bright
violet + violet-bright
blue + blue-bright
orange + orange-bright

The two rules that decide which token to use

  1. Different categories → different hues. A new category gets a new hue from the categorical palette, in the order above. Never recolor a category between pages — the hue is the category's identity.
  2. Same variable, different bands → same hue, -bright sibling. When one variable is split into emphasis bands (new vs. returning, active vs. inactive, current vs. comparison period, converted vs. drop-off), use the same hue's base + -bright rather than a second hue. The eye reads "same family" instead of "two unrelated categories".

Example: A "Users by source" chart uses 10 categorical hues — one per source. The "New vs returning users" chart uses one categorical hue at base + -bright — same category, two bands.

Rules

  • 10-hue ceiling for categorical. Beyond ten, perception breaks down — the eye stops distinguishing.
  • Reserve semantic colors. Red = error, green = success, amber = warning, in semantic UI roles. The chart-categorical tokens of the same hue (cat-red, cat-green, cat-amber) are categorical-only — never use them to signal status.
  • One brand accent on the most-important series; neutrals for the rest when the chart has a single hero series. Almost always clearer than rainbows.
  • Color-blind safe. Test categorical palettes through Deuteranopia and Protanopia simulators. When two adjacent hues are close, add patterns or icons.
  • Same hue + -bright always before a second hue when the variable is one thing in two bands.

Composition — axes, labels, gridlines

ElementRule
Bar baselineAlways zero. Truncating exaggerates change and is a distortion.
Linear y-axisDefault. Log only when the range spans 2+ orders of magnitude and the audience expects it. Always label log scales.
Tick density4–7 horizontal gridlines is the sweet spot. More is noise.
Axis labelsOutside the data area. Short. Include units ($, %, s).
Number formattingTabular figures. Locale-aware. Compact for axes (1.2k, 3.4M); full precision in tooltips.
Date formattingMatch the app locale. Abbreviations on axes (Jan, Feb); full dates in tooltips.
GridlinesLight, behind the data. Never crossing labels.
Origin markerShow "0" explicitly when the y-axis crosses it.
Auto-rotated labelsAvoid. If labels are long, switch to horizontal bars.
How are orders trending this year?
Title · the question
Y label · outside data area
Gridlines · light, behind data
Ticks · 4–7 across
Tooltip · hover + focus
Footnote · source / range
Anatomy — title is the question, axis labels outside the data, gridlines light and behind, tooltip on hover and focus.

Context — pair the chart with a summary

A chart shows the shape of the data. A scorecard answers "should I care right now?". On a dashboard, almost every standalone chart should travel with one — above or beside it — so the reader gets both signals in a single glance.

RuleNotes
One scorecard per chartThe KPI that matches the chart's question. Multiple KPIs belong in their own row, not stacked on the chart.
Always show a comparisonDon't display a number without context. Comparison priority: vs. previous period → vs. target → vs. cohort average.
Delta is colored, not just numberedUse semantic ink for up/down, never the categorical palette. Add an arrow or icon so the signal isn't color-only.
Sparkline beside the KPIOptional, but a 30–60 day sparkline makes the delta legible without scrolling.
Time range is part of the title or hintThe same KPI means nothing without "last 30 days" or "Q2". Put it in the chart hint, not the body.
Revenue$585K +8.4% vs last quarter
KPI scorecard + delta + sparkline on the left answers ‘should I care right now?’; the trend chart on the right shows the shape.

Sort, order, and direction

Ordering is meaning. Get it wrong and a ranked chart reads like noise.

RuleNotes
Bars sorted by value, descending, by defaultSwitch to time-ordered only when the x-axis is time.
Time goes left → right, oldest → newestAlways. Never flip for "recent first".
Stacked series ordered largest → smallest, bottom-upThe largest band reads first against the baseline.
Legend order mirrors visual orderTop-down for stacks, left-to-right for line endpoints, value-ranked for bars.
"Other" / long-tail bucket always lastNever in the middle of the ranking.
Categorical series keep the same color across pagesReordering rows is fine; recoloring the same metric isn't.

Annotations & reference lines

Annotations turn a chart into a story. Use them sparingly so they don't compete with the data.

  • Reference lines for targets, thresholds, or averages. Label at the right edge in a neutral tone — never the categorical palette.
  • Vertical event guides for launches, outages, policy changes, or pricing shifts. One-line label, same weight as axis labels, smaller size.
  • Cap at two annotations per dashboard-sized chart. More belongs in a dedicated story or report view.
  • Don't annotate every peak. If the eye already notices it, the chart is doing its job.
  • Annotations are part of the alt text. A screen reader user should learn about the target line and the event from the chart's aria-label or description.
One horizontal target line (‘Goal’) plus one vertical event guide (‘Launch’). Cap at two annotations per dashboard-sized chart.

Small multiples

When the same metric needs to be compared across many categories, don't pile series onto one chart — break it into a grid of tiny identical charts (small multiples).

RuleNotes
Shared axes across every cellSame scale, same range, same formatting. Different scales destroy the comparison and mislead the reader.
Cap the grid at 3×3 (9 cells)Beyond that, sort by value and truncate the tail into "Other" or a "View all" link.
Each cell has its own title (the category)Axis labels appear only on the outer edges of the grid.
Same color across cells when comparing one metricVary only when the cells represent genuinely different series.
Reading order is left-to-right, top-to-bottom, ranked by valueThe top-left cell is the most important one.
US
EU
UK
CA
AU
BR
Six small multiples — one metric, same scale across all cells, same accent color. Each cell gets its own category title at the top.

Legends

RuleWhy
Position above or to the right of the plot, not floating inside it.Predictable scan path.
Match legend order to the visual order in the chart (top-down stack, left-right line endpoints).Reduces lookup time.
Make legends interactive when there are ≥ 3 series — clicking a legend item filters that series.Standard expectation.
Skip the legend when there is only one series and the title makes it clear.Less chrome.
Don't repeat the title in the legend.Redundant.

Direct labels vs. legends

A legend forces the reader to glance back and forth between the data and the key. Direct labels remove that cost.

  • Prefer direct labels at the end of lines, beside bars, or on slices when there are ≤ 3 series or the labels fit without overlap.
  • Use a legend only when direct labeling would crowd the plot.
  • When direct labels are used, omit the legend entirely — never both.
  • Direct labels inherit series color; pair them with the same icon/marker used in the chart so the connection survives without color.

Legend overflow — +N More

When the series list outruns the legend strip, don't wrap to a third line and don't shrink the type. Show the first few chips inline (in value order, matching the visual order in the chart) and collapse the tail into a +N More chip whose hover/focus popover lists the rest with the same color swatches.

  • Visible chips: 3–5 is the sweet spot at dashboard-tile width.
  • The overflow chip is interactive — hover and focus reveal the popover.
  • The chips inside the popover use the same swatch + label format as the inline chips. No alternative shapes.
  • The overflow popover is supplemental, not the canonical legend — its contents must also be reachable via the chart's "View as table" fallback.

BarListLegend ships this overflow pattern out of the box; see Chart → BarListLegend.


Tooltips

RuleNotes
Trigger: hover and focusKeyboard users get the same info.
Content: category/x-value, all series at that x, formatted numbers, unitsAlways show units.
Crosshair on time-series charts.Helps align across multiple metrics.
One tooltip at a time.Don't scatter tooltips across the chart.
Don't truncate.If the value needs wrapping, allow wrapping.
Stable ordering.Sort tooltip rows the same way each time.
Tooltip with crosshair on a time-series chart. Triggered by hover and focus; tabular figures inside.

States

Charts have the same lifecycle as any other surface. Define each one explicitly.

StatePattern
LoadingSkeleton shaped like the chart. Don't show 0-value bars during load — they read as real data.
EmptyEmptyState inside the chart frame: heading + body + first action.
ErrorAlert (variant: error) above the chart frame, with a Retry. Keep the frame visible so layout doesn't jump.
Partial / no data for rangeInline text inside the frame: "No events in this range. Try a wider window." Keep axes visible if the question stays the same.
Locked / no permissionEmptyState with permission copy (see Voice & Tone — Permission denied).
Loading — `ChartSkeleton` shaped like the chart.

No events yet

Once contacts start interacting, activity will show here.

Empty — `EmptyState` with icon, heading, body, and primary action.

Couldn't load chart

Analytics service didn't respond.

Error — destructive icon, short message, Retry.
No events in this range. Try a wider window.
No data for range — axes visible, inline message inside the frame.

Interaction & keyboard

  • Hover / focus: show tooltip + crosshair.
  • Click: open detail (drill-down) or filter, but never both. Pick one per chart and document it.
  • Keyboard: Tab to enter the chart, Arrow keys to move between data points, Enter to select, Escape to clear selection.
  • Zoom & pan: opt-in; don't ship them on dashboard tiles by default.
  • Brush selection (for time series): only when the user can actually do something with the selection (filter, export, drill).

Accessibility — non-negotiable checklist

  • Data table alternative linked from every chart (<details> or "View as table" toggle).
  • Color is never the sole signal — pair with text, icon, position, or pattern (WCAG 1.4.1).
  • role="img" and aria-label on the SVG root, with a one-sentence summary of the chart's takeaway.
  • aria-describedby linking to a longer description for complex charts.
  • Keyboard navigable through data points with visible focus.
  • Live region (aria-live="polite") for dynamic updates on real-time charts.
  • Don't rely on hover-only tooltips — same info on focus.
  • Respect prefers-reduced-motion for entrance animations and live tickers.
  • Color-blind tested. Run categorical palettes through Deuteranopia + Protanopia simulators.

Library — Recharts

Marmo charts are built on Recharts. It's the only chart library we use, the only one to install, and the only one to reach for when proposing a new chart. Wrap every chart in a Marmo Card or PageSection shell so spacing, headings, and toolbar patterns stay consistent.

Recharts gives us:

  • Bar, column, line, area, stacked area, scatter, pie/donut, composed, and funnel out of the box.
  • React-native composition (<BarChart>, <XAxis>, <Tooltip>, …) — no imperative API.
  • A ResponsiveContainer that handles parent-driven sizing, which keeps charts honest inside dashboard tiles.

Wrapper checklist

Every chart wrapper we ship on top of Recharts must set:

  • Default colors from the token palette (categorical / sequential / diverging / monochromatic).
  • Tabular figures + locale-aware number/date formatting.
  • <Tooltip> with a crosshair on time-series charts; hover and focus.
  • Empty / loading / error states baked in (see States).
  • role="img", aria-label, "View as table" toggle.
  • prefers-reduced-motion respected — isAnimationActive={false} when the user opts out.

@marmoui/ui ships the shared primitives: ChartContainer + ChartTooltip / ChartLegend (token-aware Recharts wrapper), BarList + BarListLegend (DOM-based ranking), ChartFunnel (generic conversion funnel), LeadLifecycleFunnel (trapezoid funnel with built-in quality-flag Alert), ChartSkeleton, ChartEmpty, ChartTable. See Chart for the API. Higher-level scaffolding (ChartTile, KPIScorecard) used in these docs previews is not yet shipped — tracked on the roadmap.


Do / don't

Which channels drove the most signups in March?
Source
Direct
997
Referral
467
Organic search
268
Paid ads
58
Newsletter
12
Title is the question, baseline at zero, one categorical hue per source, sorted descending.
Revenue
3D pie, eight slices, rainbow palette — every distortion at once.
DoDon't
Title each chart with the question it answers.Title with the metric name only ("Revenue").
Anchor bar baselines at zero.Truncate the y-axis to make small differences look big.
Use a single brand accent + neutrals for one-series charts.Rainbow a one-series chart.
Cap categorical palettes at 10 hues; group the tail.Generate a unique color per category indefinitely.
Use a hue's -bright sibling for emphasis bands of the same category.Pick a second hue when the data is one variable in two bands.
Show units in axis labels and tooltips.Show raw numbers without %, $, time units.
Provide a "View as table" toggle.Ship a chart with no tabular alternative.
Reuse the same color for the same series across pages.Recolor the same metric per page.
Match legend order to visual order.Sort the legend alphabetically while bars are ranked by value.
Use Skeleton during load.Show zeroed bars or spinners that hide structure.

Numbers, dates, and locales

  • Use the same date and number format as the rest of the app (locale-aware when shipped).
  • Tabular figures across all chart text — required for axes and tooltips, where digits must align.
  • Compact notation for axis labels (1.2k, 3.4M); full precision in tooltips.
  • Currency: ISO format with locale-aware grouping.

Outliers and data quality

Real data is messy. The chart is responsible for telling the truth about that, not papering over it.

SituationPattern
One value dwarfs the othersSplit into two charts — the top mover on its own, the rest on a shared scale. Never use a broken-axis indicator on a bar chart.
Zero-value categoriesGroup into "Other (N)" in the footer, or hide behind an explicit toggle. Don't show a row of empty bars.
Sparse data in a wide rangeShow the underlying points (scatter or strip) instead of a misleading smooth line through gaps.
Missing / unknown valuesRender the gap explicitly. Never interpolate silently — it invents data the user didn't see.
Cumulative / unbounded seriesCap the axis at a sensible round number and label any clipped points. The reader should know the chart was clipped.
Negative values on a default zero-baseline chartMake the baseline explicit and use the diverging palette. Don't flip the axis to hide the sign.

How this connects to the rest of the system

Every rule on this page is an application of the broader Marmo design language — the five criteria every design must pass: is the question obvious, is the answer effortless, does it match expectations, is exploration safe, is it inclusive? A chart is just a screen with axes.

This page sticks to the chart-specific application: title is the question, ≤ 10 categorical hues (with -bright siblings for same-variable bands), ≤ 4 series per single chart, baselines at zero, hover and focus, Skeleton during load, table alternative.


Sources (chart-specific)

  • Andrew Abela — Chart Chooser methodology (decision-by-question lineage).
  • WCAG 2.1 — 1.4.1 (Use of Color).
  • Norman, Cooper, Weinschenk, Yablonski, Cleveland & McGill — the cognition and perception research behind these criteria.