Start with the question

Every month Margriet de Wit, Kestrel Energy's chief financial officer (CFO), receives a key performance indicator pack: a workbook full of numbers about contracts, energy delivered and invoices. For October she has asked for a few charts instead of more tables. The worst way to make them is to open a chart menu and try the options until something looks good. The best way is to write down, in one sentence, what the reader should notice, and let that sentence choose the chart.

Almost every business chart answers one of five kinds of question:

Question Example at Kestrel Usually best Sometimes
Comparison: which is bigger? kWh delivered per tariff in September Bar chart, sorted Dot plot, table
Trend: how does it change over time? Total kWh per day; billed amount per month Line chart Columns, for a handful of periods
Distribution: how are the values spread? Daily kWh of household meters Histogram, box plot Dot strip for small data
Composition: what is it made of? Invoice amounts by status, per month Stacked bar chart Pie, for two or three parts of one whole
Relationship: do two things move together? Weekday use against weekend use, per meter Scatter plot Heatmap, for very many points

Why bars and lines so often win has been measured. In a much-cited 1984 study, William Cleveland and Robert McGill tested how accurately people read values from different visual encodings. Their ranking, from most to least accurate, is usually summarized as: position along a common scale; position along scales that are not aligned; length, direction and angle; area; volume and curvature; and shading or color saturation. A bar chart and a line chart put values on a common scale. A pie chart asks for angles and areas, a bubble chart for areas, a colored map for shades: all lower on the list.

The charts on this page are drawn from the shared Kestrel sample data (kestrel.sql), with the numbers computed by the DuckDB queries next to them. The examples run on DuckDB 1.5 (as of October 2026; version 2.0 is scheduled for 21 October 2026). Reading a chart's axes is covered in Functions and Graphs; making charts honest and readable for everyone is the subject of Honest and Accessible Visualization.

Comparing categories: bar charts

A bar chart shows one value per category as the length of a bar measured from a common zero line. Because the reader compares lengths along one scale, it is the most accurate way to answer "which is bigger, and by how much?". Bars can stand up (often called a column chart) or lie down; horizontal bars leave room for long category names.

Margriet's first question: how much electricity did the customers on each tariff use in September?

SELECT c.tariff,
       count(DISTINCT m.meter_id) AS meters,
       round(sum(r.kwh_import), 1) AS kwh_import
FROM daily_readings r
JOIN meters m USING (meter_id)
JOIN contracts c USING (contract_id)
GROUP BY c.tariff
ORDER BY kwh_import DESC;
┌───────────┬────────┬───────────────┐
│  tariff   │ meters │  kwh_import   │
│  varchar  │ int64  │ decimal(38,1) │
├───────────┼────────┼───────────────┤
│ dynamic   │      3 │        2057.3 │
│ day_night │      3 │        1828.5 │
│ fixed     │      3 │        1074.2 │
│ variable  │      3 │         597.6 │
└───────────┴────────┴───────────────┘
Horizontal bar chart of September 2026 electricity imported per tariff, sorted from largest to smallest: dynamic 2,057.3 kWh, day_night 1,828.5, fixed 1,074.2 and variable 597.6 kWh. Three meters per tariff. The axis starts at zero. dynamic 2,057.3 day_night 1,828.5 fixed 1,074.2 variable 597.6 0 500 1,000 1,500 2,000 kWh imported, September 2026
September kWh per tariff as horizontal bars, sorted by value, starting at zero.

Three habits make bar charts work:

  • Sort by value, unless the categories have a natural order (months, age groups, satisfaction scores 1–5). Alphabetical order makes the reader do the sorting.
  • Start at zero. The length of a bar is the value; cutting off the bottom makes small differences look huge. Why, and what to do instead, is on Honest and Accessible Visualization.
  • Label the values when there are only a few bars. Margriet will quote the numbers in a meeting; she should not have to estimate them from gridlines.

Two categories per group, such as kWh imported and exported per tariff, become grouped bars: two bars side by side per tariff. Beyond three bars per group the chart gets crowded; split it into separate charts instead.

A line chart plots values in order along the horizontal axis, usually time, and connects consecutive points with a line, so the eye follows the rise and fall. The line itself carries a message: "these points belong together, in this order". That makes it the natural chart for a time series and the wrong one for categories that have no order, such as tariffs or cities.

You do not need a charting tool to see a trend. DuckDB's bar() function draws a bar of block characters for a value between a minimum and a maximum, which is enough for a quick look in the terminal:

SELECT strftime(reading_date, '%a %d') AS day,
       round(sum(kwh_import), 1) AS kwh,
       bar(sum(kwh_import), 0, 300, 24) AS chart
FROM daily_readings
GROUP BY reading_date
ORDER BY reading_date;
┌─────────┬───────────────┬──────────────────────────┐
│   day   │      kwh      │          chart           │
│ varchar │ decimal(38,1) │         varchar          │
├─────────┼───────────────┼──────────────────────────┤
│ Tue 01  │         192.3 │ ███████████████▍         │
│ Wed 02  │         199.4 │ ███████████████▉         │
│ Thu 03  │         207.7 │ ████████████████▌        │
│ Fri 04  │         194.6 │ ███████████████▌         │
│ Sat 05  │         128.3 │ ██████████▎              │
│ Sun 06  │         126.6 │ ██████████▏              │
│ Mon 07  │         198.4 │ ███████████████▊         │
│ Tue 08  │         288.1 │ ███████████████████████  │
│ Wed 09  │         199.9 │ ███████████████▉         │
│ Thu 10  │         202.8 │ ████████████████▏        │
│ Fri 11  │         194.7 │ ███████████████▌         │
│ Sat 12  │         136.4 │ ██████████▉              │
│ Sun 13  │         131.3 │ ██████████▌              │
│ Mon 14  │         197.0 │ ███████████████▊         │
│ Tue 15  │         207.2 │ ████████████████▌        │
│ Wed 16  │         196.8 │ ███████████████▋         │
│ Thu 17  │         190.2 │ ███████████████▏         │
│ Fri 18  │         208.3 │ ████████████████▋        │
│ Sat 19  │         131.8 │ ██████████▌              │
│ Sun 20  │         128.8 │ ██████████▎              │
│ Mon 21  │         187.1 │ ██████████████▉          │
│ Tue 22  │         204.2 │ ████████████████▎        │
│ Wed 23  │         212.5 │ █████████████████        │
│ Thu 24  │         199.4 │ ███████████████▉         │
│ Fri 25  │         209.6 │ ████████████████▊        │
│ Sat 26  │         131.4 │ ██████████▌              │
│ Sun 27  │         129.7 │ ██████████▍              │
│ Mon 28  │         210.7 │ ████████████████▊        │
│ Tue 29  │         204.7 │ ████████████████▍        │
│ Wed 30  │         207.6 │ ████████████████▌        │
└─────────┴───────────────┴──────────────────────────┘
  30 rows                                  3 columns

The same 30 numbers as a line chart:

Line chart of the total electricity imported by all twelve meters per day in September 2026. Weekdays sit around 190 to 213 kWh, every weekend drops to about 127 to 136 kWh, and Tuesday 8 September sticks out at 288.1 kWh. The y-axis runs from 0 to 300 kWh. 0 100 200 300 1 Sep 8 Sep 15 Sep 22 Sep 29 Sep 288.1 weekend kWh
Total kWh imported per day in September 2026, all twelve meters with readings.

Two patterns jump out that no monthly total would show: every weekend use drops by about a third (the three business customers are mostly closed), and Tuesday 8 September is far above every other day. The weekend dip is real behavior. The spike is not: it is one meter that reported 91.3 kWh instead of 9.13, one of the problems planted in the sample data and found on Data Types and Data Quality. Charts are one of the fastest ways to find bad data.

A few rules for lines:

  • A line axis does not have to start at zero, because the reader looks at the shape, not at the length from the bottom. Include zero when the values come close to it, and never zoom in so far that noise looks like a trend.
  • Do not draw across missing data. A day without readings should be a gap, not a straight line between its neighbors that pretends to know the value.
  • Few periods, use columns. Margriet's billed amount per month has only four values (June to September); four columns are easier to read and to label than a line with three segments.
  • Mark events on the time axis (a price change, a deploy, an outage) so readers do not have to guess why the line moves. This is an annotation, described on Honest and Accessible Visualization.

Distributions: histograms and box plots

An average hides more than it shows. The questions "what is a normal day for a household?", "how much does it vary?" and "are there impossible values?" are questions about a distribution, and they need a chart that shows all the values at once.

Histograms

A histogram divides the range of a numeric variable into equal intervals, called bins, and draws one bar per bin whose height is the number of values that fall in it. It looks like a bar chart, but the horizontal axis is a continuous number scale, not a list of categories, so the bars touch and their order is fixed. Not to be confused with a Prometheus histogram, which is a type of metric that counts observations in buckets; you would draw that one as a histogram chart or a heatmap.

The query below makes the bins for 240 household meter-days, two kWh wide, and also computes the five numbers a box plot needs. floor(kwh / 2) * 2 rounds each value down to the start of its bin:

CREATE TEMP VIEW household_days AS
SELECT r.meter_id, r.reading_date, r.kwh_import
FROM daily_readings r
JOIN meters m USING (meter_id)
JOIN contracts c USING (contract_id)
JOIN customers cu USING (customer_id)
WHERE cu.segment = 'household';

SELECT floor(kwh_import / 2) * 2 AS bin_from, count(*) AS days
FROM household_days
GROUP BY bin_from
ORDER BY bin_from;

SELECT min(kwh_import) AS minimum,
       quantile_cont(kwh_import, 0.25) AS q1,
       median(kwh_import) AS median,
       quantile_cont(kwh_import, 0.75) AS q3,
       max(kwh_import) AS maximum
FROM household_days;
┌──────────┬───────┐
│ bin_from │ days  │
│  double  │ int64 │
├──────────┼───────┤
│     -6.0 │     1 │
│      4.0 │    26 │
│      6.0 │    68 │
│      8.0 │    92 │
│     10.0 │    42 │
│     12.0 │    10 │
│     90.0 │     1 │
└──────────┴───────┘
┌──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐
│   minimum    │      q1      │    median    │      q3      │   maximum    │
│ decimal(8,3) │ decimal(8,3) │ decimal(8,3) │ decimal(8,3) │ decimal(8,3) │
├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤
│       -4.210 │        7.066 │        8.677 │        9.860 │       91.300 │
└──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘
Histogram of 240 daily readings from household meters in September 2026, in bins of 2 kWh, with a box plot underneath on the same scale. Most days fall between 4 and 14 kWh, peaking at 92 days in the 8 to 10 kWh bin. One day is negative, minus 4.21 kWh, and one day is 91.3 kWh, drawn after a break in the axis. The box plot shows a median of 8.68, quartiles of 7.07 and 9.86, whiskers from 4.77 to 12.78 and the two outliers as separate dots. 0 50 100 1 26 68 92 42 10 1 -6 -2 2 6 10 14 90 92 days -4.21 91.3 median 8.68 kWh imported per household meter per day
Histogram (top) and box plot (bottom) of the same 240 values, on one shared kWh scale. The axis is broken between 14 and 90 kWh.

The bulk of household days sits between 4 and 14 kWh, a little skewed to the right. Two values do not belong: one negative reading (a meter cannot import less than nothing) and one of 91.3 kWh. On a normal axis from −6 to 92 the 238 ordinary days would be squeezed into a narrow strip; the figure therefore breaks the axis and marks the break, which is acceptable only when the break is impossible to miss.

Box plots

A box plot (box-and-whisker plot) draws a distribution as a box from the first to the third quartile with a line at the median, plus whiskers that reach toward the extremes and separate marks for values far outside. It is built from the five-number summary explained on Exploratory Data Analysis. The most common rule, from John Tukey, ends the whiskers at the most extreme values within 1.5 times the box length (the interquartile range) beyond the box, and draws anything further out as a point. Here: Q1 7.066, Q3 9.860, so the whiskers may reach from 2.875 to 14.051 kWh; they stop at the actual values 4.770 and 12.779, and −4.210 and 91.300 are drawn as outliers.

A box plot is less detailed than a histogram (it cannot show two peaks, for example), but it is compact: twelve box plots side by side, one per meter, compare twelve distributions in the space of one histogram. Tools differ in how they compute quartiles and where they end the whiskers, so a box plot from a spreadsheet and one from Python can differ slightly; see methods that interpolate.

Parts of a whole: stacked bars and pies

Composition questions ask what share each part has of a total: how much of what was billed has been paid? Two kinds of chart answer it.

SELECT status,
       sum(amount_eur) AS amount_eur,
       round(100 * sum(amount_eur) / sum(sum(amount_eur)) OVER (), 1) AS pct
FROM invoices
GROUP BY status
ORDER BY amount_eur DESC;

PIVOT invoices ON status USING sum(amount_eur) GROUP BY period ORDER BY period;
┌─────────┬───────────────┬────────┐
│ status  │  amount_eur   │  pct   │
│ varchar │ decimal(38,2) │ double │
├─────────┼───────────────┼────────┤
│ paid    │       4240.90 │   76.3 │
│ open    │       1132.85 │   20.4 │
│ overdue │        184.80 │    3.3 │
└─────────┴───────────────┴────────┘
┌────────────┬───────────────┬───────────────┬───────────────┐
│   period   │     open      │    overdue    │     paid      │
│    date    │ decimal(38,2) │ decimal(38,2) │ decimal(38,2) │
├────────────┼───────────────┼───────────────┼───────────────┤
│ 2026-06-01 │          NULL │          NULL │         92.40 │
│ 2026-07-01 │          NULL │         66.70 │       1684.40 │
│ 2026-08-01 │          NULL │        118.10 │       1632.60 │
│ 2026-09-01 │       1132.85 │          NULL │        831.50 │
└────────────┴───────────────┴───────────────┴───────────────┘

Pie charts

A pie chart divides a circle into slices whose angles (and areas) are proportional to each part's share of one total. Its strength is the message "these parts make up one whole"; its weakness is that people judge angles and areas poorly, so slices of 20% and 25% look the same, and comparing two pies is nearly impossible. Edward Tufte's remark that the only worse design than a pie chart is several of them is quoted for a reason.

A pie is acceptable when there are two or three parts of a single total and the point is the rough proportion: paid 76.3%, open 20.4%, overdue 3.3% of everything billed since June. Put the percentages on the slices. With more than three or four parts, with parts of similar size, or to compare several totals, use bars.

Stacked bar charts

A stacked bar chart divides each bar into segments, one per part, so the full bar shows the total and the segments show its composition. Margriet's invoice table above becomes four bars, one per month, each stacked from overdue (at the bottom), open and paid. A 100% stacked bar scales every bar to the same height to compare shares instead of totals.

The catch: only the bottom segment starts on the common baseline. The others float, so the reader can compare totals and the bottom part accurately, and everything in between only roughly. Put the most important part (here: overdue) at the bottom, keep the number of parts small, and keep the same order in every bar.

Relationships: scatter plots

A scatter plot places one dot per item at the position given by two numeric variables, one on each axis, so the cloud of dots shows whether, how strongly and in which direction they are related, and which items do not follow the pattern. It is the picture behind a correlation coefficient, and it shows things the coefficient cannot: groups, curves and single odd points.

Do some customers use less at weekends than on weekdays? One row per meter, with its segment:

SELECT r.meter_id,
       coalesce(cu.segment, 'unknown') AS segment,
       round(avg(r.kwh_import) FILTER (WHERE isodow(r.reading_date) <= 5), 1) AS weekday_avg,
       round(avg(r.kwh_import) FILTER (WHERE isodow(r.reading_date) >= 6), 1) AS weekend_avg
FROM daily_readings r
JOIN meters m USING (meter_id)
JOIN contracts c USING (contract_id)
LEFT JOIN customers cu USING (customer_id)
GROUP BY r.meter_id, segment
ORDER BY weekday_avg DESC;
┌──────────┬───────────┬─────────────┬─────────────┐
│ meter_id │  segment  │ weekday_avg │ weekend_avg │
│ varchar  │  varchar  │   double    │   double    │
├──────────┼───────────┼─────────────┼─────────────┤
│ M-30006  │ business  │        62.9 │        21.5 │
│ M-30011  │ business  │        48.0 │        17.4 │
│ M-30008  │ business  │        18.5 │         7.1 │
│ M-30001  │ household │        11.8 │         9.4 │
│ M-30007  │ household │        11.0 │        11.8 │
│ M-30002  │ household │         9.4 │        11.3 │
│ M-30010  │ household │         9.3 │        10.1 │
│ M-30004  │ household │         8.8 │         9.8 │
│ M-30012  │ household │         7.3 │         8.8 │
│ M-30014  │ unknown   │         7.1 │         8.0 │
│ M-30005  │ household │         6.3 │         7.7 │
│ M-30009  │ household │         5.2 │         6.7 │
└──────────┴───────────┴─────────────┴─────────────┘
  12 rows                                4 columns
Scatter plot with one dot per meter: average weekday use on the horizontal axis (0 to 70 kWh) and average weekend use on the vertical axis (0 to 25 kWh). A dashed line marks equal use on both. The three business meters, M-30006 at 62.9 and 21.5, M-30011 at 48.0 and 17.4 and M-30008 at 18.5 and 7.1, lie far below the line: they use much less at weekends. The households cluster between 5 and 12 kWh, just above the line, except M-30001, which lies just below it. 0 10 20 30 40 50 60 70 0 5 10 15 20 25 same on weekdays and weekends M-30006 M-30011 M-30008 M-30001 households average kWh per weekday average kWh per weekend day
One point per meter: weekday average against weekend average. Squares are business meters, dots the rest.

The dashed line marks "same use on both". Business meters fall far below it: they use about a third as much at weekends. Households sit just above it, with one exception, M-30001. Its weekday average includes the 91.3 kWh reading of Tuesday 8 September; without that one wrong value it would sit above the line with the others. One bad reading moved a point to the wrong side of the line, which a table of averages would never have shown.

The meter whose segment is unknown belongs to contract CT-20014, whose customer does not exist in the customers table, another planted problem. The axes do not use the same scale (0–70 against 0–25), which is fine as long as both are labeled; the diagonal then just is not at 45 degrees.

Many series: small multiples

Margriet's next request: "one chart with the daily use of every meter". Twelve lines on one set of axes produce what analysts call a spaghetti chart: lines cross, the business meters flatten every household line to the bottom, and nobody can follow one meter from left to right.

Small multiples are a grid of small charts of the same type, one per category, drawn with the same layout so the eye can move from one to the next and compare. The name was popularized by Edward Tufte. The figure below draws the 30 daily values of each meter straight from the daily_readings table, one panel per meter:

Small multiples: twelve small line charts, one per meter, each showing daily kWh imported from 1 to 30 September 2026 on its own vertical scale from zero to that meter's maximum. The three business meters show a deep dip every weekend. The households wobble from day to day and rise slightly at weekends. M-30001 has one huge spike on 8 September, M-30009 dips below zero on 21 September, and M-30010 has a gap on 17 September. M-30001 max 91.3 M-30002 max 12.5 M-30004 max 10.6 M-30005 max 8.3 M-30006 max 71.8 M-30007 max 12.8 M-30008 max 21.8 M-30009 max 7.7 M-30010 max 12.3 M-30011 max 55.8 M-30012 max 9.8 M-30014 max 8.6
Small multiples: daily kWh per meter, 1–30 September 2026. Each panel has its own vertical scale from zero (or the lowest value) to the maximum shown above it. Business meters are drawn in the accent color.

In one glance: the three business meters (M-30006, M-30008, M-30011) drop sharply every weekend; M-30001 has one enormous spike; M-30009 dips below zero on the 21st; M-30010 has a gap on the 17th. Three of the six data-quality problems in the sample data are visible without a single query.

Whether the panels share one vertical scale is a deliberate choice. A shared scale lets the reader compare levels between panels (the bakery uses about six times as much as a household), but flattens the small users. Free scales, as here, show each meter's pattern clearly, but then the panels must not be compared by height, and every panel needs its scale printed. Say which one you chose in the caption.

When a number or a table is better

A chart is worth drawing when the shape of the data matters. When the reader needs one exact value, or needs to look up values, a chart only adds work:

  • One number with context. "Overdue: €184.80 across 3 invoices (3.3% of billed)" says more than a gauge. Most BI tools have a tile for exactly this; see Business Intelligence Tools.
  • A short table. Twelve meters with their September totals are easier to look up in a sorted table than in a chart, and a table can hold exact figures, units and IDs.
  • A sentence. "Business customers use about a third as much at weekends" is the finding; the scatter plot is the evidence. Put the finding in the title of the chart, and the evidence underneath.

Operational dashboards follow the same logic with different chart types (time series, stat panels, gauges, state timelines); those are described on Dashboards.

Charts to avoid

Some charts are popular because they look impressive, not because they communicate. Avoid, or use with great care:

  • 3D charts. Perspective makes nearer bars and slices look larger and hides values behind each other. There is no third variable, so there is no third dimension to show.
  • Pies with many slices, exploded pies and donut charts used to compare totals. Use sorted bars.
  • Dual-axis charts. Two series with two different vertical scales on one chart. Where the lines cross, and how parallel they look, depends entirely on how the two scales were chosen, so the chart suggests a relationship the author can create at will. Draw two charts above each other with a shared time axis, or use a scatter plot.
  • Lines between unordered categories. A line from "fixed" to "variable" suggests a trend between tariffs that does not exist.
  • Bubble charts and area-based maps when exact comparisons matter: people underestimate area differences, and tools sometimes scale the radius instead of the area.
  • Spaghetti charts: more than four or five lines on one chart. Highlight the one that matters and gray the rest, or use small multiples.