Foundation
How to pick the right chart, color palettes, axis and legend rules, accessibility, and the criteria that make a chart feel intuitive.
Status: alpha
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).
Six rules. They override anything else on this page.
Start from the question, not the data shape.
| Question | Reach for | Avoid |
|---|---|---|
| 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 · Density | Bar 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 delta | Speedometer gauges |
| Where is X happening? | Choropleth · Symbol map | Pie on a map |
| What's the single number right now? | Scorecard / KPI · Sparkline beside it | Gauge taking 1/4 of the page |
| Where does the funnel leak? | Funnel · Sankey | Stacked bar |
| What's the hierarchy? | Treemap · Sunburst (sparingly) | Nested pies |
Rules of thumb
Each chart has a single best-fit and one or two anti-patterns. Treat this as the source of truth when proposing dashboard tiles.
| Chart | Best for | Don't use for |
|---|---|---|
| Bar (horizontal) | Ranking many categories | Time series |
| Column (vertical bar) | Comparing few categories or discrete time buckets | Long category names |
| Line | Continuous trends, ≥ 4 time points, 1–5 series | Categorical comparison |
| Area | One trend + magnitude emphasis | Many overlapping series |
| Stacked area | Composition shifting over time | More than 5 series |
| Stacked bar / column | Parts-of-whole across categories | When totals aren't meaningful |
| 100% stacked | Composition only (totals normalized) | When absolute values matter |
| Histogram | Distribution of one variable | Categorical data |
| Box plot | Spread + outliers across groups | Casual audiences |
| Scatter | Relationship between two variables | Single-variable summaries |
| Bubble | Three-variable relationship (x, y, size) | Qualitative third variable |
| Donut | Single proportion (≤ 4 slices), KPI center | Comparing precise values |
| Treemap | Hierarchical composition, many categories | Continuous trends |
| Funnel | Conversion or drop-off across stages | Non-sequential categories |
| Sankey | Flows between nodes | Simple part-to-whole |
| Choropleth | Regional value comparison | Cities / point data |
| Symbol map | Point events with magnitude | Region-level summary |
| Sparkline | Trend hint inside a row, KPI, or table cell | Anything requiring axes |
| Scorecard / KPI | Single number + delta vs. target | More than one metric |
| Bullet chart | Single value vs. target + ranges | Many series |
Four palette archetypes carry every chart. Pick the palette by the kind of data, not by the brand.
| Palette | Use 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. |
| Monochromatic | Single trend or relationship chart where shape > category. | One hue, one or two values. |
tealvioletblueorangepinkambergreenindigomintredMarmo 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.
| Swatch | Token | Hue | Use for |
|---|---|---|---|
--color-chart-cat-teal | Brand teal | The most important / brand series, or the leader in a ranked list. | |
--color-chart-cat-violet | Violet | Secondary category. | |
--color-chart-cat-blue | Blue | Third category. | |
--color-chart-cat-orange | Orange | Fourth category. | |
--color-chart-cat-pink | Pink | Fifth category. | |
--color-chart-cat-amber | Amber | Sixth category. Not for warnings — that's a semantic role. | |
--color-chart-cat-green | Green | Seventh category. Not for "success" — semantic role. | |
--color-chart-cat-indigo | Indigo | Eighth category. | |
--color-chart-cat-mint | Mint | Ninth category. | |
--color-chart-cat-red | Red | Tenth 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-brightviolet + violet-brightblue + blue-brightorange + orange-bright-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
cat-red, cat-green, cat-amber) are categorical-only — never use them to signal status.-bright always before a second hue when the variable is one thing in two bands.| Element | Rule |
|---|---|
| Bar baseline | Always zero. Truncating exaggerates change and is a distortion. |
| Linear y-axis | Default. Log only when the range spans 2+ orders of magnitude and the audience expects it. Always label log scales. |
| Tick density | 4–7 horizontal gridlines is the sweet spot. More is noise. |
| Axis labels | Outside the data area. Short. Include units ($, %, s). |
| Number formatting | Tabular figures. Locale-aware. Compact for axes (1.2k, 3.4M); full precision in tooltips. |
| Date formatting | Match the app locale. Abbreviations on axes (Jan, Feb); full dates in tooltips. |
| Gridlines | Light, behind the data. Never crossing labels. |
| Origin marker | Show "0" explicitly when the y-axis crosses it. |
| Auto-rotated labels | Avoid. If labels are long, switch to horizontal bars. |
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.
| Rule | Notes |
|---|---|
| One scorecard per chart | The KPI that matches the chart's question. Multiple KPIs belong in their own row, not stacked on the chart. |
| Always show a comparison | Don't display a number without context. Comparison priority: vs. previous period → vs. target → vs. cohort average. |
| Delta is colored, not just numbered | Use semantic ink for up/down, never the categorical palette. Add an arrow or icon so the signal isn't color-only. |
| Sparkline beside the KPI | Optional, but a 30–60 day sparkline makes the delta legible without scrolling. |
| Time range is part of the title or hint | The same KPI means nothing without "last 30 days" or "Q2". Put it in the chart hint, not the body. |
Ordering is meaning. Get it wrong and a ranked chart reads like noise.
| Rule | Notes |
|---|---|
| Bars sorted by value, descending, by default | Switch to time-ordered only when the x-axis is time. |
| Time goes left → right, oldest → newest | Always. Never flip for "recent first". |
| Stacked series ordered largest → smallest, bottom-up | The largest band reads first against the baseline. |
| Legend order mirrors visual order | Top-down for stacks, left-to-right for line endpoints, value-ranked for bars. |
| "Other" / long-tail bucket always last | Never in the middle of the ranking. |
| Categorical series keep the same color across pages | Reordering rows is fine; recoloring the same metric isn't. |
Annotations turn a chart into a story. Use them sparingly so they don't compete with the data.
aria-label or description.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).
| Rule | Notes |
|---|---|
| Shared axes across every cell | Same 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 metric | Vary only when the cells represent genuinely different series. |
| Reading order is left-to-right, top-to-bottom, ranked by value | The top-left cell is the most important one. |
| Rule | Why |
|---|---|
| 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. |
A legend forces the reader to glance back and forth between the data and the key. Direct labels remove that cost.
+N MoreWhen 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.
BarListLegend ships this overflow pattern out of the box; see Chart → BarListLegend.
| Rule | Notes |
|---|---|
| Trigger: hover and focus | Keyboard users get the same info. |
| Content: category/x-value, all series at that x, formatted numbers, units | Always 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. |
Charts have the same lifecycle as any other surface. Define each one explicitly.
| State | Pattern |
|---|---|
| Loading | Skeleton shaped like the chart. Don't show 0-value bars during load — they read as real data. |
| Empty | EmptyState inside the chart frame: heading + body + first action. |
| Error | Alert (variant: error) above the chart frame, with a Retry. Keep the frame visible so layout doesn't jump. |
| Partial / no data for range | Inline text inside the frame: "No events in this range. Try a wider window." Keep axes visible if the question stays the same. |
| Locked / no permission | EmptyState with permission copy (see Voice & Tone — Permission denied). |
Once contacts start interacting, activity will show here.
Analytics service didn't respond.
<details> or "View as table" toggle).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.aria-live="polite") for dynamic updates on real-time charts.prefers-reduced-motion for entrance animations and live tickers.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:
<BarChart>, <XAxis>, <Tooltip>, …) — no imperative API.ResponsiveContainer that handles parent-driven sizing, which keeps charts honest inside dashboard tiles.Every chart wrapper we ship on top of Recharts must set:
<Tooltip> with a crosshair on time-series charts; hover and focus.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 |
|---|---|
| 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. |
1.2k, 3.4M); full precision in tooltips.Real data is messy. The chart is responsible for telling the truth about that, not papering over it.
| Situation | Pattern |
|---|---|
| One value dwarfs the others | Split 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 categories | Group 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 range | Show the underlying points (scatter or strip) instead of a misleading smooth line through gaps. |
| Missing / unknown values | Render the gap explicitly. Never interpolate silently — it invents data the user didn't see. |
| Cumulative / unbounded series | Cap 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 chart | Make the baseline explicit and use the diverging palette. Don't flip the axis to hide the sign. |
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.