Topic 18.4
Debugging Charts
In one line
Charts fail in four ways: they show NaN, they show nothing, they look blurry, or they look fine and are wrong. You learn one method (reproduce, shrink, observe, guess, change one thing) and apply it to each failure with real error messages.
Think of it like this
A doctor with a patient who says "I feel ill". The doctor does not guess. They ask where, take a temperature, check one thing at a time and write down what they find. Debugging a chart is the same: "it looks wrong" becomes a symptom, a measurement and a cause.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- symptom
- What you see go wrong: a console error, a blank box, a blurry line, a number that differs from the data.
- reproduce
- Make the problem happen on demand, with the smallest input that shows it.
- hypothesis
- A guess written as one sentence you can test: "the height is NaN because the value is the text n/a".
- NaN
- "Not a Number": what arithmetic gives for something like
+"n/a"orundefined * 2. It spreads through every calculation it touches. - empty selection
- A D3 selection that matched no element. Calls on it do nothing and raise no error.
- backing store
- The grid of pixels a canvas really holds, set by its
widthandheightattributes. - device pixel ratio
- How many physical screen pixels sit behind one CSS pixel (1, 2 or 3 on most screens).
Step by step
01Read the symptom exactly
Copy the console message word for word. Expected length, "NaN" means a value was not a number. A negative value is not valid means a width or height went below zero (Topic 1.2). A thrown TypeError: Cannot read properties of undefined means a lookup returned nothing. Each message already narrows the search.
02Hunt the NaN backwards
The first example draws five days. The Console shows two NaN errors, and then the hunt prints, for each bad rectangle, its datum and the raw row it came from: Wed was "n/a" and Thu has a column named ride, not rides. Two different causes, found in one pass, because the code compares the datum with its source.
03Ask the three blank-SVG questions in order
The second example diagnoses three SVGs. The first has no size: 300 by 150, one mark, zero visible. The second has an empty selection: typo.empty() is true. The third has marks off the box: two marks, none visible. The same diagnose function tells them apart by numbers.
04Size the canvas for the screen
The second example then sets the backing store for ratios 1, 2 and 3: 200 by 60, 400 by 120 and 600 by 180 for the same 200 by 60 box. This page's screen may have a ratio of 1, so the code takes the ratio as a parameter, to show what a high-density phone would do. In your page, read window.devicePixelRatio and redraw if it changes.
05Distrust a chart that has no errors
For a wrong-but-fine chart, add checks that compare the picture with the data: a ratio of drawn lengths, a sorted-order test, a day computed in a fixed zone. The third example prints each one. A check that fails loudly is better than a chart that lies quietly.
Try it yourself
- 1
Fix both NaN causes
In the first example, change the
dataline toconst data = rows.map((r) => ({ day: r.day, rides: +(r.rides ?? r.ride) })).filter((d) => Number.isFinite(d.rides));. The Console printsrects with NaN: 0 of 4andmax of rides: 3992: the misspelled column is read, and the row withn/ais left out. A chart that drops a row should also say so on the page (Topic 4.4). - 2
Fix the blank SVG without guessing
In the second example, give
#awidth="600" height="300"in the HTML.diagnosenow printssvg 600x300, marks 1, visible 1: the mark was inside the box all along and the missing size was the only problem. - 3
Read a path error and a viewBox error
The same hunt works for lines. Draw
d3.line()through three points where the middle value isundefined: the Console printsError: <path> attribute d: Expected number, "M0,32L100,NaNL200,0".The text of the error is the path itself, so theNaNtells you which point broke. Add.defined((p) => Number.isFinite(p.v))to the line and the error goes (the line then splits into pieces: Topic 7.1). A negative height in a viewBox, such asviewBox="0 0 200 -80", printsError: <svg> attribute viewBox: A negative value is not valid. ("0 0 200 -80"). - 4
Check a winter day in a summer zone
In the third example, change the trip to
2025-06-15T00:30:00Z. Both days print2025-06-15, because Azores summer time is UTC+0. A time-zone bug can hide for half a year, so test one winter and one summer date.
Code & diagrams
The two bars are simply missing: the picture shows three bars and no warning on the page. Only the console and the hunt tell you. `d3.max` skips NaN, which is why the scale still looks sane.
Preview
Console
Nothing logged yet.Expected console output
Error: <rect> attribute height: Expected length, "NaN".
Error: <rect> attribute height: Expected length, "NaN".
rects with NaN: 2 of 5
bad datum: {"day":"Wed","rides":"NaN"} | raw row: {"day":"Wed","rides":"n/a"}
bad datum: {"day":"Thu","rides":"NaN"} | raw row: {"day":"Thu","ride":"3742"}
max of rides: 3992None of the three blank SVGs raised an error. The numbers (size, marks, visible) tell them apart in one look. The canvas ratio is a parameter here so that you see the same sizes whatever your own screen's ratio is (1, 2 or 3).
Preview
Console
Nothing logged yet.Expected console output
no size : svg 300x150, marks 1, visible 0
typo selection empty: true | #b marks: 0
off-screen: svg 200x60, marks 2, visible 0
ratio 1: backing store 200x60 for a 200x60 box
ratio 2: backing store 400x120 for a 200x60 box
ratio 3: backing store 600x180 for a 200x60 boxThe time-zone line is the same on every computer because both zones are named. A `new Date(...)` formatted with the computer's own zone would differ between a laptop in Ardenvale and one in another country (Topic 4.3).
Console
(nothing was printed)Expected console output
UTC day: 2025-01-15 | Ardenvale (Atlantic/Azores) day: 2025-01-14
d3.utcFormat day: 2025-01-15 (always the UTC day, on every computer)
baseline 0: Tue looks 1.3x Mon, data says 1.3x
baseline 2900: Tue looks 13.8x Mon, data says 1.3x
sorted by date: false | nearest to 1 June finds index 0 (should be 1)Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Forget to scale the canvas context
Set up a canvas with ratio 2 (canvas.width = 400, canvas.height = 120, CSS size 200 by 60). Run this twice, once with ctx.scale(2, 2) and once without it: ctx.fillRect(0, 0, 200, 60); const a = (x, y) => ctx.getImageData(x, y, 1, 1).data[3]; and log a(300, 90) and a(100, 30).
Myth vs fact
Myth
If there is no error in the console, the chart is correct.
Fact
Most chart bugs raise no error: an empty selection, an unsorted array, a time zone, a cut baseline. Check the picture against the data with a number.
Myth
Blurry canvas text is a font problem.
Fact
It is almost always the backing store being smaller than the screen needs. Fix the size and the ratio, not the font.
Myth
Wrapping the code in try/catch finds the bug.
Fact
An invalid SVG attribute is reported by the browser to the console, not thrown as a JavaScript exception. A try/catch sees nothing. Inspect the elements and the data instead.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Chromium names the element and the attribute but not the datum, so make the datum findable: filter elements by attribute, then read
d3.select(node).datum(). In the browser's Elements panel,$0.__data__gives the datum of the selected element. - ▸
Dates and zones are the commonest silent bug in dashboards. Fix the zone at the edge: parse to UTC (
d3.utcParse), keep UTC in the data, and format in a named zone withIntl.DateTimeFormatonly when drawing labels. Test withTZset to a different zone than your own. - ▸
Add a development-only guard that logs when any attribute holds NaN or
undefinedafter each draw (the checks behind this course's examples do exactly this). It turns the silent class of bug into a loud one in the first run.
Remember this
- 1
The problem this solves: a chart gives little feedback. A wrong number draws a wrong bar and says nothing. A typo in a selector draws nothing and says nothing. Without a method people change five things at once, and the bug disappears for reasons they do not understand. A method makes the search short and the lesson stick.
- 2
The method has five steps. Reproduce: make it fail every time, with the smallest data that shows it. Shrink: remove everything not needed until the bug is tiny. Observe: look at facts, not feelings (the console, the element in the inspector, a logged value). Guess: write one sentence, "I think X because Y". Change one thing and run again. If the guess was wrong, you still learned something. Never change two things between runs.
- 3
The NaN hunt. A bad value makes an SVG attribute invalid, and Chromium prints a real message:
Error: <rect> attribute height: Expected length, "NaN".The message names the element and the attribute but not the datum. Work backwards: select the elements whose attributes containNaN, read the datum bound to each (d3.select(node).datum()), and compare it with the raw input. The first example finds two causes in one run: the textn/aand a misspelled column name. Both becomeNaN, andJSON.stringifyhides it by printingnull. - 4
The blank SVG has three usual causes, checked in this order. One: no size, so the SVG is 300 by 150 pixels and the marks sit outside. Two: the selection is empty (a typo in the id, or the script ran before the element existed), so every later call quietly does nothing. Three: the marks exist but sit outside the visible box, usually an unflipped y scale or a wrong margin. A small
diagnosefunction that prints the SVG's size, the mark count and how many marks are visible answers all three in one look. - 5
The blurry canvas is a size mismatch. A canvas has two sizes: the CSS size (how much room it takes) and the backing store (
widthandheight, how many pixels it holds). On a screen with a device pixel ratio of 2 the box covers 2 by 2 device pixels per CSS pixel, so a 200 by 60 backing store is stretched to 400 by 120 and looks soft. Set the backing store to the CSS size times the ratio and callctx.scale(ratio, ratio)once (Topic 11.4). If you forget thescale, the drawing fills only a quarter of the box. - 6
The chart that is wrong but looks fine is the dangerous one. No error, a plausible picture. The third example lists the usual suspects: a time-zone day bucket (a trip at 00:30 UTC on 15 January is still 14 January in the Azores), a cut baseline (13.8 times instead of 1.3 times), and unsorted data given to a bisector (it returns index 0 where the answer is 1). Defence: assert what you can (
sorted,Number.isFinite), compute a ratio and compare it with the data, and test with a fixed time zone.
Explain it without notes
A bar chart is missing two of five bars. How do you find out why?
An SVG chart is blank and the console is empty. What do you check, in what order?
Text on a canvas chart is blurry on a phone but sharp on your laptop. Why?
How can a chart be wrong without any error?
Practice
Write a function findBadMarks(svg) that returns the elements whose attributes contain NaN, undefined or Infinity, and log each element's datum. Test it on the first example.
A colleague says "my line chart shows nothing". Write the five questions you would ask or checks you would run, in order.
Write setupCanvas(canvas, w, h) that uses window.devicePixelRatio and returns a context where you draw in CSS pixels.
Trade-offs
- ↔
Guards that log NaN on every draw cost a little time, so run them in development and in tests, not in a hot production loop.
- ↔
Dropping bad rows keeps the chart clean but hides data problems; showing them as flagged marks or a count keeps trust. Say what you chose.
Done when you can
I can describe the five-step method and change one thing at a time.
I can trace a NaN attribute back to the datum and the raw input.
I can diagnose a blank SVG with the size, selection and position questions.
I can size a canvas for the device pixel ratio and say what breaks without the scale.
I can name three ways a chart is wrong with no error, and a check for each.