Command Palette

Search for a command to run...

Hectal
PHASE 18Advanced ~30 min· topic 4 of 5

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" or undefined * 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 width and height attributes.
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. 1

    Fix both NaN causes

    In the first example, change the data line to const data = rows.map((r) => ({ day: r.day, rides: +(r.rides ?? r.ride) })).filter((d) => Number.isFinite(d.rides));. The Console prints rects with NaN: 0 of 4 and max of rides: 3992: the misspelled column is read, and the row with n/a is left out. A chart that drops a row should also say so on the page (Topic 4.4).

  2. 2

    Fix the blank SVG without guessing

    In the second example, give #a width="600" height="300" in the HTML. diagnose now prints svg 600x300, marks 1, visible 1: the mark was inside the box all along and the missing size was the only problem.

  3. 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 is undefined: the Console prints Error: <path> attribute d: Expected number, "M0,32L100,NaNL200,0". The text of the error is the path itself, so the NaN tells 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 as viewBox="0 0 200 -80", prints Error: <svg> attribute viewBox: A negative value is not valid. ("0 0 200 -80").

  4. 4

    Check a winter day in a summer zone

    In the third example, change the trip to 2025-06-15T00:30:00Z. Both days print 2025-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 NaN hunt: which datum, from which raw row? New tab

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

Press Run to see the page.

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: 3992
A blank SVG and a blurry canvas, diagnosed with numbers New tab

None 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

Press Run to see the page.

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 box
Wrong but looks fine: time zones, a cut baseline, unsorted data New tab

The 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).

terminal
$ Console
── what you'll see ──
scale true: alpha at device pixel (300, 90) = 255 | at (100, 30) = 255
scale false: alpha at device pixel (300, 90) = 0 | at (100, 30) = 255

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 with Intl.DateTimeFormat only when drawing labels. Test with TZ set to a different zone than your own.

  • ▸

    Add a development-only guard that logs when any attribute holds NaN or undefined after 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. 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. 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. 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 contain NaN, 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 text n/a and a misspelled column name. Both become NaN, and JSON.stringify hides it by printing null.

  4. 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 diagnose function that prints the SVG's size, the mark count and how many marks are visible answers all three in one look.

  5. 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 (width and height, 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 call ctx.scale(ratio, ratio) once (Topic 11.4). If you forget the scale, the drawing fills only a quarter of the box.

  6. 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

01

A bar chart is missing two of five bars. How do you find out why?

02

An SVG chart is blank and the console is empty. What do you check, in what order?

03

Text on a canvas chart is blurry on a phone but sharp on your laptop. Why?

04

How can a chart be wrong without any error?

Practice

01

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.

02

A colleague says "my line chart shows nothing". Write the five questions you would ask or checks you would run, in order.

03

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.