Data before design
Three things decide whether a view of data is any good, and all three are settled before you write markup: what decision the view serves, what is actually in the field, and which of several layouts the person wants. Skipping any of them produces a view that looks finished and is wrong.
When to use
- Any card, panel, table, dashboard or detail view over records you did not author
- A stakeholder says "show me X" and X is a column name
- You are about to summarise, truncate, badge, sort, colour or count a field
- You are choosing between two ways to lay out the same information
Skip when the data is your own fixture, or the view is one value you already printed.
A chart or table that will be posted, shared or put in a report still starts here when it shows
records you did not author. Once the data is settled, practical-charts covers how it reads:
the headline title, honest axes, one highlighted series, the source inside the image.
Step 0: name the decision
Write one sentence before touching the data: "Someone looking at this is deciding ___." If you cannot finish it, you are building a data dump and the layout question has no answer.
- Every field earns its place by changing that decision. Interesting but decision-neutral belongs in the detail view, not the card.
- Prominence tracks decision weight, not query convenience. The field that was easiest to fetch is not the field that deserves the largest type.
- A stakeholder naming a column has told you the data they want, not the decision. Ask what they would do differently depending on its value.
Step 1: profile the field, then design
Query before you design. For every field the view will show:
| Check | Why it changes the design |
|---|---|
| Non-null count | A field NULL on every row needs no card — it needs a bug report |
| Distinct values | Four values is a chip set; four thousand is a search box |
| Length: min / mean / p95 / max | The mean hides the overflow — size the container to p95, and decide deliberately what the tail does |
| Coverage across records | A card that fires for 1.6% of records is an empty state with a rare exception |
| Cardinality per parent | One note per person is a paragraph; twenty is a list with its own affordances |
| Date presence | You cannot show staleness for a field that carries no date |
| Vocabulary | Free text with eight house phrasings needs normalising, not rendering |
| A comparator exists | A bare number is unreadable — see below |
State what you found before proposing the design, in numbers.
Every number on screen needs something to compare it against
52 notes tells the reader nothing: they cannot tell whether it is a lot. 52 notes (median 4) is a finding. Before displaying any count, total or score, find its comparator — a median,
a target, the previous period, or the rest of the set — and display them together. If no
comparator exists, that is evidence the number should not be on screen.
The degenerate case is a number that never varies: a count true of every record reads as a finding and is noise.
Step 2: mock the options, then build
For anything card-shaped, produce several static mocks from real records and let the person choose. Build only after that.
- Say in one line what each option optimises for and what it costs
- Mocks are throwaway HTML, not a component — do not wire them up
Mock six states, not one
Design every state the view can be in, each from a real record:
| State | Draw it from |
|---|---|
| Ideal | The typical record — the one at the median, not the one you picked |
| Empty | A record with nothing in the field, and a brand-new account with no records at all |
| Partial | Some fields filled, some not — in real data this is the common case, not the exception |
| Loading | What occupies the space before the data arrives, at the size the data will be |
| Error | The fetch failed, or the value is malformed and cannot be rendered |
| Done | What confirms an action the view offers actually happened |
Then the stress case
Volume extremes (busiest, typical, empty) are not the only extremes. A stress case is data that is present, well-formed, and painful: the name the field mangles, the record whose status makes a cheerful badge wrong, the person whose deleted account the summary still counts. Pick the one record that would most embarrass this view and render it.
Several cards on one page: one key for all of them
A reader learns a colour's meaning on the first card and applies it to every card after it. So before drawing any card on a page with several, write down one category table for the whole page: the canonical name, a fixed order and one colour for each category. Every card, legend and tooltip uses it.
- One category, one colour; one colour, one category. A colour belongs to its category even on cards where that category is absent. Using it again for something else, even on a card that never shows the original, tells the reader the two are the same thing.
- One name per category. "Global Health & Development" on one card and "Global Dev & Health" on the next makes the reader wonder whether they are two categories.
- One taxonomy where the cards cut the same records. Before a card adds its own bucket, check whether it merges into a category the page already has. If a card really needs a finer split, give the sub-categories shades of the parent's colour, not new colours.
- Categories that really are different get unused colours, plus a subtitle saying the breakdown is different. Never reuse a colour the page has already given to another category.
- Same order in every legend, with "Other" always last and always the same neutral grey.
Done when listing every (label, colour) pair across the page gives a one-to-one mapping. Grep the markup or stylesheet for the colour values, not the rendered screenshot.
Rendering rules that fall out of the data
- The empty state does work. "No notes yet" is a dead end. Say why it is empty and what fills it. An empty state a user meets on day one is onboarding, and is worth designing.
- Numbers in a table: right-align, tabular figures (
font-variant-numeric: tabular-nums), one precision for the whole column, units in the header rather than repeated in each cell.practical-chartsrule 13 has the rest: no grid, a tinted key row, inline bars. - The default sort is a design decision. Records arriving in primary-key order is the absence of one.
Common mistakes
| Mistake | What happens |
|---|---|
| Designing from the schema | The column exists; the data does not |
| Building with no decision named | Everything gets equal weight, because nothing has a reason to be bigger |
| Giving the easiest field the most prominence | The layout describes the schema, not the job |
| Showing a number with nothing to compare it to | The reader cannot tell whether it is good, and stops looking |
| Counting something that is always true | A count that never varies reads as a finding and is noise |
| Trusting a plan written before the data was seen | You build the thing the plan named, not the thing the data supports |
| Mocking only the ideal state | The design collapses on the record that has nothing, or half of something |
| Testing only volume extremes | The view survives the biggest record and humiliates someone on an ordinary one |
| Building one option because it was described first | The person cannot choose what they were never shown |
| Picking colours and labels card by card | The same blue means two things on one page, and the reader draws a link that isn't there |
Where the non-obvious parts come from
- Decision-first, and prominence by decision weight — Stephen Few, Information Dashboard Design
- A number needs a comparator — Few again, on context as the thing that makes a number mean anything
- The six states — Scott Hurff's UI Stack
- Stress cases — Eric Meyer & Sara Wachter-Boettcher, Design for Real Life