Every charting tutorial contains this line, or something like it:
var y = d3.scale.linear().domain([0, max]).range([height, 0]);
That reversed range, [height, 0] instead of [0, height], is the first thing you learn about drawing charts in SVG. Screen coordinates grow downward; humans expect values to grow upward; so you flip the range and never think about it again.
In 2015 I was building the display for a well-logging rig. A probe goes down a borehole on a cable, streaming measurements to the surface while the winch pays out. On the very first day, that reflexive flip was wrong.
A well log's vertical axis is depth. Depth grows downward, because down is where the hole goes. Zero is at the top, at the wellhead. Every geologist who would ever look at my screen had spent a career reading paper logs printed that way.
So the line became:
var y = d3.scale.linear().domain([0, 70]).range([0, height]);
No flip. SVG's own coordinate system, the "wrong" one, was the correct one here. I remember how uncomfortable that felt, deleting the reversal I'd typed on autopilot. It's a small thing, but it set the tone for the whole project: nearly every charting convention I knew was a convention about time series, and I wasn't drawing a time series.
Then: how many X axes does a chart have?
The answer for a well log is "as many as you have curves", and they are all different.
In one track, one vertical strip of the display, you overlay several measurements recorded against the same depth. One might be an angle, running 0 to 360 degrees. Another is a count running to several thousand. A third is something else entirely. They share the depth axis and nothing else. Averaging them onto one X scale would be meaningless; there is no common unit.
So each curve gets its own scale, its own axis, and its own header. The headers stack above the plot, one per curve, in the curve's own color:
xX['x' + prop] = d3.scale.linear().range([0, width]);
xX['x' + prop].domain([0, d3.max(linesObj[property], d => Math.max(d.DATA))]);
xX['x' + prop + 'Axis'] = d3.svg.axis()
.scale(xX['x' + prop])
.orient("top") // headers go on top, like a paper log
.ticks(5);
svg.append("g")
.attr("class", "x axis " + prop)
.attr("stroke", colors[lineCounter])
.attr("transform", "translate(0, " + (-20 * lineCounter) + ")") // stack upward
.call(xX['x' + prop + 'Axis']);
Every axis is .orient("top"), offset another 20 pixels up for each curve. That's not a styling choice. It's how a log header has looked since these things were printed on rolls of paper, and the people reading it would not have accepted anything else.
Zoom follows the same logic. The operator wants to stretch or compress depth while the curves keep their own horizontal domains. Depth zoom and curve scaling are separate concerns:
var zoom = d3.behavior.zoom()
.y(y) // depth only
.on("zoom", zoomed);
And then the lines escaped
Here's where it got embarrassing, and where I want to be honest, because this is the part people leave out of write-ups.
Zooming worked, and the curves drew straight past the plot area, over the axis labels, over the headers, out into the margins. A line is just a path; nothing about a scale's range confines the geometry it produces. Values outside the visible depth window still get coordinates, and SVG happily paints them wherever they land.
Well log with three curves and overflowing lines
The screenshot from that question. Three curves in one track, each with its own scale stacked across the top (0–25, 0–8, 0–10) in its own colour; depth running downward on the left, 760 m to 840 m. The yellow marks are the bug: curve geometry painted above the headers and below the plot, in space that belongs to the axes and the margin.
I couldn't see it at first. I asked about it on Stack Overflow in January 2015, D3.js line chart overflows margins, with a screenshot and my zoom handler.
Nobody answered. I found the fix on Stack Overflow anyway, in an unrelated question about reusing an existing shape as a clipping path, and left a comment under mine saying what it was.
The fix is a clipping path: define a rectangle the size of the plot area, and attach it to every drawn path.
var clip = svg.append("defs").append("clipPath").attr("id", "clipBox");
svg.append('rect').attr({ x: 0, y: 0, width: width, height: height, id: "xSliceBox" });
clip.append("use").attr("xlink:href", "#xSliceBox");
// ...and on every single curve:
svg.append("path")
.attr("class", "line x1LineClass")
.attr("d", x1Line(scope.currentData))
.attr("clip-path", "url(#clipBox)"); // CORRECTION
If you open that file today you'll find //CORRECTION sitting next to five separate clip-path attributes, one per curve, added in one pass after I understood the problem.
The part that made it hard: it was all live
None of this was drawn from a file sitting on disk. Depth came from a quadrature encoder counting cable off the winch, with a magnetic sensor reading marks on the cable and a load sensor watching weight. An microcontroller board read the panel, an industrial DAQ streamed the analog channels, Python did calibration fits and interpolation, and the browser subscribed over WebSockets. Points arrived continuously and the log format used by the industry (LAS, the Log ASCII Standard, ancient and immovable) was written in parallel.
Which meant the chart had to redraw while someone was zooming it, without losing their place. Depth advanced; the operator's window into it had to stay where they'd put it. That interaction, where new data must not steal the viewport, is the requirement I would now recognize instantly in any live dashboard, and it is the one I hadn't anticipated at all back then.
What I took from it, and still use
- Charting conventions are domain conventions. "Values grow upward" is not a law of visualization; it's a habit borrowed from time-series. Ask the people who read the output which way their world points.
- A scale bounds the mapping, not the drawing. If output can exceed the visible window, whether by zoom, pan or live data, clip explicitly. Nothing else will.
- When one axis can't carry the units, don't force it. Several axes, several colors, honest headers. A merged scale that means nothing is worse than a busier chart that means something.
- Live data changes the requirements, not just the plumbing. "Redraw" and "redraw without moving what the user is looking at" are different features.
-
Leave the marks. Those
//CORRECTIONcomments are ten years old, and I still know exactly which afternoon they came from.













