What makes a chart honest

A chart can be built from correct numbers and still mislead. The reader does not read the numbers; they read lengths, heights, areas and slopes, and compare them in a second or two. Edward Tufte, whose book The Visual Display of Quantitative Information (1983, second edition 2001) shaped much of modern chart design, put the rule simply: the physical size of what is drawn should be directly proportional to the quantity it represents.

He also gave a way to measure how far a chart departs from that rule, the lie factor: the size of the effect shown in the graphic divided by the size of the effect in the data. A chart with a lie factor of 1 is faithful; one with a lie factor of 5 shows a change five times bigger than it really is. Few people compute it, but it is a useful question to ask of any chart: if this bar looks twice as long, is the number really twice as large?

This page continues Choosing the Right Chart with the Kestrel Energy sample data. Margriet de Wit, the chief financial officer (CFO), gets the charts in her monthly pack, and Ravi Kumar's portal team shows customers a chart of their own energy use. Both audiences deserve charts that do not exaggerate and that they can read, whatever their eyesight.

Axes and baselines

The amounts billed per month, from the sample data (DuckDB 1.5, as of October 2026):

SELECT strftime(period, '%Y-%m') AS month,
       count(*) AS invoices,
       sum(amount_eur) AS billed_eur
FROM invoices
GROUP BY month
ORDER BY month;
┌─────────┬──────────┬───────────────┐
│  month  │ invoices │  billed_eur   │
│ varchar │  int64   │ decimal(38,2) │
├─────────┼──────────┼───────────────┤
│ 2026-06 │        1 │         92.40 │
│ 2026-07 │       12 │       1751.10 │
│ 2026-08 │       12 │       1750.70 │
│ 2026-09 │       12 │       1964.35 │
└─────────┴──────────┴───────────────┘

June holds a single invoice in the sample, so it is left out of the comparison below. Putting an incomplete period next to complete ones is itself a classic way to mislead: the last bar of a "this month so far" chart always looks like a collapse.

The zero baseline

A zero baseline is a value axis that starts at zero, so that the length or height of each bar or area is proportional to its value. For bar, column and area charts it is not a style preference but part of how the chart works: the reader compares lengths, and a length only means something if it is measured from zero. The charting service Datawrapper, which refuses to let users start a bar axis anywhere else, puts it bluntly: "Truncated y-axes in column charts and bar charts are considered deceptive and misleading."

Truncated axes

A truncated axis is a value axis that starts above zero (or ends below the largest value), so that only part of the scale is shown. Here is Margriet's July to September billing, drawn both ways:

The same three monthly billed amounts drawn twice as columns: July 1,751.10 euros, August 1,750.70 and September 1,964.35. On the left the vertical axis starts at 1,700 euros, and the September column looks about five times as tall as August. On the right the axis starts at zero, and September is visibly but modestly taller, by 12%. 1,700 1,800 1,900 2,000 Jul Aug Sep axis from €1,700 0 500 1,000 1,500 2,000 Jul Aug Sep axis from €0
The same three numbers. Left: a truncated axis from €1,700. Right: a zero baseline.

How much does the left chart exaggerate? The script below computes Tufte's lie factor for the change from August to September with both axes:

billed = {"2026-07": 1751.10, "2026-08": 1750.70, "2026-09": 1964.35}
aug, sep = billed["2026-08"], billed["2026-09"]
real_change = (sep - aug) / aug
print(f"real change Aug to Sep: {real_change:+.1%}")

for axis_min in (0, 1700):
    shown_aug = aug - axis_min      # bar heights measured from the axis
    shown_sep = sep - axis_min
    shown_change = (shown_sep - shown_aug) / shown_aug
    lie_factor = shown_change / real_change
    print(f"axis from {axis_min:>4}: shown change {shown_change:+.1%}, lie factor {lie_factor:.1f}")
real change Aug to Sep: +12.2%
axis from    0: shown change +12.2%, lie factor 1.0
axis from 1700: shown change +421.4%, lie factor 34.5

A real rise of 12.2% is drawn as a rise of more than 400%. Nobody lied about a number; the axis did it. Spreadsheets and dashboard tools often choose such an axis automatically, scaling it to the data; the operational version of the same problem is shown on Dashboards.

Line charts are different. The reader looks at the slope and the shape, not at a length from the bottom, so a line chart may zoom in. Datawrapper again: "Line charts have the big advantage that they don't need to start from zero. If your data comes close to zero, however, consider adding the zero baseline." Zooming in is honest when the axis labels are clear and the range is chosen to show real variation, not to turn noise into drama.

Scales and areas

The same proportionality rule applies to anything with a size. When a chart uses circles (bubble charts, symbols on a map), the area must be proportional to the value. Because the area of a circle grows with the square of its radius, a tool or designer that scales the radius exaggerates every difference. Compare the open and overdue amounts of all invoices (€1,132.85 and €184.80):

import math

open_eur, overdue_eur = 1132.85, 184.80
ratio = open_eur / overdue_eur
r = 12                                   # radius of the overdue circle, in pixels
area_scaled = r * math.sqrt(ratio)       # area proportional to the amount
radius_scaled = r * ratio                # radius proportional to the amount (wrong)

print(f"open is {ratio:.2f} times overdue")
print(f"area-scaled:   radius {area_scaled:5.1f} px, looks {(area_scaled / r) ** 2:5.2f} times as big")
print(f"radius-scaled: radius {radius_scaled:5.1f} px, looks {(radius_scaled / r) ** 2:5.2f} times as big")
open is 6.13 times overdue
area-scaled:   radius  29.7 px, looks  6.13 times as big
radius-scaled: radius  73.6 px, looks 37.58 times as big
Two ways to draw the open amount (1,132.85 euros) next to the overdue amount (184.80 euros), which is 6.13 times smaller. Left: the circles' areas are proportional to the amounts, so the radius grows 2.48 times. Right: the radius is proportional to the amounts, so the open circle has 37.6 times the area of the overdue circle and looks far too big. overdue open area ∝ amount (correct) overdue open radius ∝ amount (wrong)
Left: circle areas proportional to the amounts. Right: radii proportional to the amounts, an exaggeration by a factor of about six.

Even drawn correctly, areas are hard to compare precisely; bars do the same job better. Other scale decisions that change the message:

  • Logarithmic scales make equal ratios look like equal steps (10 to 100 is as far as 100 to 1,000). They are right for data that spans several orders of magnitude, such as response times, but most readers assume a linear scale, so say "log scale" on the axis.
  • The aspect ratio of a line chart sets how steep a change looks. A tall, narrow chart turns a gentle rise into a cliff; a wide, flat one hides a real rise.
  • Per what? Totals favor big groups. kWh per tariff makes the tariff with a bakery on it look heavy; kWh per meter per day shows something else. Choose the unit before you choose the chart.

Data-ink and chartjunk

Tufte also proposed a way to look at everything that is not data. The data-ink ratio is the share of a chart's ink (or pixels) that shows the data itself, as opposed to frames, backgrounds, heavy gridlines, shadows and decoration; his advice was to make it as high as reasonably possible. Chartjunk, a word he coined in the same book, is visual decoration in a chart that does not show data and distracts from it: 3D effects, textures, pictures behind the bars, ornamental icons, gradients.

Practically, before you publish a chart:

  • Remove 3D, shadows, gradients, background images and borders around the plot.
  • Make gridlines thin and light, or remove them when the values are labeled.
  • Remove a legend that repeats what the axis or a direct label already says.
  • Remove decimals nobody needs: 2,057 kWh, not 2,057.276.
  • Keep what helps reading: axis titles with units, the source, the date of the data, and the zero line.

Do not take minimalism too far. A chart stripped of every axis and label is clean and useless. The aim is that every mark on the chart either shows data or helps the reader read it; this site's own diagrams follow the same black-and-white, thin-line approach.

Color with a purpose

Color in a chart should encode something. The first question is what kind of data the color represents, because that decides the kind of scheme. ColorBrewer, the color advice tool for maps by Cynthia Brewer (with design by Mark Harrower), sorts schemes into three kinds, and its definitions are a good starting point.

Sequential schemes

A sequential color scheme runs from light to dark (or the other way) in one hue or a few related hues, so that more data means a darker or stronger color. In ColorBrewer's words, "Sequential schemes are suited to ordered data that progress from low to high." At Kestrel: average daily kWh per postcode area, or the number of meters per city.

The rainbow color map (called jet in older tools) is a bad sequential scheme: its bright yellow and cyan bands create edges in the data that do not exist, and it reads badly in grayscale and for many people with a color vision deficiency. The plotting library matplotlib replaced jet with viridis as its default in version 2.0 (January 2017); viridis is perceptually uniform, which means equal steps in the data look like equal steps in color.

Diverging schemes

A diverging color scheme uses two contrasting hues that meet at a light, neutral middle, for data with a meaningful midpoint such as zero, a target or an average. ColorBrewer: "Diverging schemes put equal emphasis on mid-range critical values and extremes at both ends of the data range." At Kestrel: the change in use compared with last September, above or below zero, or invoices paid before or after the due date.

Qualitative schemes

A qualitative color scheme uses clearly different hues of similar strength for categories that have no order. ColorBrewer: "Qualitative schemes do not imply magnitude differences between legend classes, and hues are used to create the primary visual differences between classes." At Kestrel: the four tariffs. Keep the number of categories small: beyond six to eight colors, readers can no longer match a color to a legend reliably. Group the rest as "other", or use small multiples.

Color for every reader

Color vision deficiency (CVD, often called color blindness) is a reduced ability to tell certain colors apart, most often red and green, because one type of cone cell in the eye is missing or works differently. It is common: about 1 in 12 men (8%) and 1 in 200 women. In a company of Kestrel's size, dozens of employees, and often someone in the room when Margriet presents, may see her red and green tiles as the same brownish color.

The Web Content Accessibility Guidelines (WCAG) 2.2 (a W3C Recommendation since 5 October 2023, updated 12 December 2024) contain the two rules that matter most for charts. Their basics are on Accessibility Basics:

  • 1.4.1 Use of Color (level A): "Color is not used as the only visual means of conveying information, indicating an action, prompting a response, or distinguishing a visual element." A chart with two lines told apart only by color fails it.
  • 1.4.11 Non-text Contrast (level AA): the parts of graphics required to understand the content need a contrast ratio of at least 3:1 against adjacent colors. W3C's explanation of this criterion notes that chart lines need 3:1 against the background but "do not need to contrast with each other", and that pie slices need not either, if their values are also given in a conforming way, such as labels or a table.

A popular palette designed for color vision deficiency is the one by Masataka Okabe and Kei Ito (2002, updated 2008). Its eight colors stay distinguishable for the common forms of CVD. But "colorblind-safe" is not the same as "readable": WCAG's contrast formula shows that three of the eight colors are too light for lines on a white background. The script computes the contrast of each color against white and against a typical dark-mode background, #121212:

def channel(c):
    c = c / 255
    return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4

def luminance(hex_color):
    r, g, b = (int(hex_color[i:i + 2], 16) for i in (1, 3, 5))
    return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)

def contrast(a, b):
    light, dark = sorted((luminance(a), luminance(b)), reverse=True)
    return (light + 0.05) / (dark + 0.05)

okabe_ito = {
    "black": "#000000", "orange": "#E69F00", "sky blue": "#56B4E9",
    "bluish green": "#009E73", "yellow": "#F0E442", "blue": "#0072B2",
    "vermilion": "#D55E00", "reddish purple": "#CC79A7",
}
print(f"{'color':15} {'hex':8} {'on white':>9} {'on #121212':>11}")
for name, hex_color in okabe_ito.items():
    on_white, on_dark = contrast(hex_color, "#FFFFFF"), contrast(hex_color, "#121212")
    verdict = "ok" if on_white >= 3 else "too light for lines on white"
    print(f"{name:15} {hex_color:8} {on_white:8.2f}  {on_dark:9.2f}   {verdict}")
color           hex       on white  on #121212
black           #000000     21.00       1.12   ok
orange          #E69F00      2.25       8.32   too light for lines on white
sky blue        #56B4E9      2.31       8.12   too light for lines on white
bluish green    #009E73      3.42       5.48   ok
yellow          #F0E442      1.32      14.17   too light for lines on white
blue            #0072B2      5.19       3.61   ok
vermilion       #D55E00      3.87       4.84   ok
reddish purple  #CC79A7      3.06       6.12   ok

Orange, sky blue and yellow fall below 3:1 on white; on the dark background only black fails. A palette therefore has to be checked against the background it will actually be shown on, in both light and dark mode. The formula is WCAG's own (its 0.04045 threshold replaced an older 0.03928 in 2021, with no practical effect).

The reliable fix is not a better palette but a second signal:

  • different line patterns (solid, dashed, dotted) or marker shapes (dot, square, triangle);
  • direct labels on the lines or segments, described in the next section;
  • text that states the values, and the data available as a table;
  • for "good" and "bad", a word or icon as well as green and red: "overdue", not only a red cell.

Labels instead of legends

Direct labeling means writing the name of each series, or the value of each data point, right next to it in the chart, instead of in a separate legend that the reader has to match by color. It removes the back-and-forth between legend and lines, works without color, and survives printing in black and white.

Ravi Kumar's portal team shows customers with solar panels their own import and export. The chart below draws the 30 daily rows of meter M-30004 for September; the query summarizes them:

SELECT count(*) AS days,
       sum(kwh_import) AS imported,
       sum(kwh_export) AS exported,
       min(kwh_export) AS lowest_export,
       max(kwh_export) AS highest_export
FROM daily_readings
WHERE meter_id = 'M-30004';
┌───────┬───────────────┬───────────────┬───────────────┬────────────────┐
│ days  │   imported    │   exported    │ lowest_export │ highest_export │
│ int64 │ decimal(38,3) │ decimal(38,3) │ decimal(8,3)  │  decimal(8,3)  │
├───────┼───────────────┼───────────────┼───────────────┼────────────────┤
│    30 │       271.212 │       163.440 │         2.400 │          9.200 │
└───────┴───────────────┴───────────────┴───────────────┴────────────────┘
Line chart for one household with solar panels, meter M-30004, September 2026: electricity imported from the grid per day (solid line, between 7.2 and 10.6 kWh) and electricity exported to the grid (dashed line with open circles, between 2.4 and 9.2 kWh). Each line carries its name at its right-hand end instead of a legend. 0 4 8 12 kWh imported exported 1 Sep 8 Sep 15 Sep 22 Sep 29 Sep
Daily import and export for meter M-30004, September 2026. The lines differ in pattern (solid and dashed), in markers and in color, and each carries its name.

The two lines are told apart three ways: solid against dashed, no markers against open circles, and ink against the accent color. Any one of these would be enough for most readers; together they work for everyone, including someone who sees no difference between the two colors at all. Below the chart, the portal offers the same 30 days as a table.

Annotations and context

An annotation is text or a mark placed on the chart to explain a particular point: an event, an error, a target. Monitoring tools have the same idea for deploys and incidents (see annotations in Grafana). Here is the daily total chart from Choosing the Right Chart again, with context added:

The daily total kWh line chart for September 2026 again, now annotated. The title states the finding: weekdays are steady at about 200 kWh and weekends are about a third lower. The four weekends are shaded and labeled. The spike on 8 September carries a note: meter M-30001 reported 91.3 kWh instead of 9.13; a dashed point shows the corrected total of 205.9 kWh. Weekdays steady at about 200 kWh; weekends a third lower Total kWh imported per day, September 2026, all meters weekend 0 100 200 300 8 Sep: M-30001 reported 91.3 kWh instead of 9.13 corrected total 205.9 (dashed circle) 1 Sep 8 Sep 15 Sep 22 Sep 29 Sep Source: Kestrel sample data, table daily_readings (12 meters)
The same data as the plain line chart, with a title that states the finding, shaded weekends, a note on the faulty reading and the source.

What changed:

  • The title states the finding, not just the subject. "Weekdays steady at about 200 kWh; weekends a third lower" tells Margriet what to see; a subtitle says what is measured.
  • The weekends are marked, so the dips explain themselves.
  • The faulty reading is explained and the corrected value shown, instead of being silently removed or left to start a discussion about a "spike". Whether to correct it in the data is a data-quality decision (see Data Types and Data Quality), not a chart decision.
  • The source is named and the subtitle gives the period, so a reader knows where to check and which data is shown.

Annotate sparingly: one to three notes per chart. When everything is highlighted, nothing is.

Before you publish

Text alternatives

A chart is an image. People who use a screen reader, and anyone whose image does not load, need the same information as text. Give every chart:

  • a short text alternative that states what the chart shows and its main finding (in HTML, alt on an image, or role="img" with a <title> on an inline SVG, as every chart on this page does; see accessible names);
  • the data as a table, nearby or one click away, for anyone who needs the exact numbers;
  • no information that is available only on hover: tooltips cannot be reached with a keyboard in many tools and do not exist on paper.

A checklist

Check Question to ask
Baseline Do bars and areas start at zero? Is a zoomed line axis clearly labeled?
Proportion Does anything look twice as big that is not twice as big (areas, 3D, aspect ratio)?
Complete periods Is the last or first period complete, or marked as partial?
Units and source Do the axes say what and in which unit? Is the source and its date given?
Color Does the scheme fit the data type? Is color ever the only signal? Do lines reach 3:1 contrast in light and dark mode?
Labels Can series be labeled directly instead of in a legend?
Message Does the title state the finding? Are unusual points explained?
Text alternative Is there a text description and the data as a table?