What Makes Software Good?

Good design is innovative.
Good design makes a product useful.
Good design is aesthetic.
Good design makes a product understandable.
Good design is unobtrusive.
Good design is honest.
Good design is long-lasting.
Good design is thorough down to the last detail.
Good design is environmentally-friendly.
Good design is as little design as possible.

I’ve tried in the past to talk about big picture stuff. Things like finding the smallest interesting problem, identifying and minimizing harmful biases in tools, or leveraging related technologies and standards.

Abstraction gradient
Closeness of mapping
Consistency
Diffuseness
Error-proneness
Hard mental operations
Hidden dependencies
Premature commitment
Progressive evaluation
Role-expressiveness
Secondary notation
Viscosity
Visibility

It’s not perfect; no framework is. It was conceived to study visual programming environments, and sometimes feels specific to that application. (Consider visibility, which refers to seeing all the code simultaneously. Is any software today small enough to be visible in its entirety on a single screen? Perhaps modularity would be better?) I find it difficult to assign some usability problems to one dimension or another. (Both hidden dependencies and role-expressiveness suggest I thought the code would do something other than what it did.) Still, it’s a good starting point for thinking about the “cognitive consequences” of software design.

“Indifference towards people and the reality in which they live is actually the one and only cardinal sin in design.”

This implies, for one, that good documentation does not excuse bad design. You can ask people to RTFM, but it is folly to assume they have read everything and memorized every detail. The clarity of examples, and the software’s decipherability and debuggability in the real world, are likely far more important. Form must communicate function.

Case 1. Removing the magic of enter.append.

“D3” stands for Data-Driven Documents. The data refers to the thing you want to visualize, and the document refers to its visual representation. It’s called a “document” because D3 is based on the standard model for web pages: the Document Object Model.

<!DOCTYPE html>
<svg width="960" height="540">
<g transform="translate(32,270)">
<text x="0">b</text>
<text x="32">c</text>
<text x="64">d</text>
<text x="96">k</text>
<text x="128">n</text>
<text x="160">r</text>
<text x="192">t</text>
</g>
</svg>
var data = [
"b",
"c",
"d",
"k",
"n",
"r",
"t"
];
  • The enter selection represents “missing” elements (incoming data) that you may need to create and add to the document.
  • The update selection represents existing elements (persisting data) that you may need to modify (for example, repositioning).
  • The exit selection represents “leftover” elements (outgoing data) that you may need to remove from the document.
bl.ocks.org/a8a5baa4c4a470cda598
var text = g
.selectAll("text")
.data(data, key); // JOIN
text.exit() // EXIT
.remove();
text // UPDATE
.attr("x", function(d, i) { return i * 32; });
text.enter() // ENTER
.append("text")
.attr("x", function(d, i) { return i * 32; }) // 🌶
.text(function(d) { return d; });
var text = g
.selectAll("text")
.data(data, key); // JOIN
text.exit() // EXIT
.remove();
text.enter() // ENTER
.append("text") // 🌶
.text(function(d) { return d; });
text // ENTER + UPDATE
.attr("x", function(d, i) { return i * 32; });
var text = g
.selectAll("text")
.data(data, key); // JOIN
text.exit() // EXIT
.remove();
text.enter() // ENTER
.append("text")
.text(function(d) { return d; })
.merge(text) // ENTER + UPDATE
.attr("x", function(d, i) { return i * 32; });

Maxim 1. Avoid overloading meaning.

What can we learn from this failure? D3 3.x violated a Rams principle: good design makes a product understandable. In cognitive dimension terms, it had poor consistency because selection.append behaved differently on enter selections, and thus the user can’t extend understanding of normal selections to enter. It had poor role-expressiveness because the latter behavior wasn’t obvious. And there’s a hidden dependency: operations on the text selection must be run after appending to enter, though nothing in the code makes this requirement apparent.

Case 2. Removing the magic of transition.each.

A transition is a selection-like interface for animating changes to the document. Instead of changing the document instantaneously, transitions smoothly interpolate the document from its current state to the desired target state over a given duration.

bl.ocks.org/1166403
d3.selectAll("line").transition()
.duration(750)
.attr("x1", x)
.attr("x2", x);
d3.selectAll("text").transition() // 🌶
.duration(750) // 🌶
.attr("x", x);
var t = d3.transition()
.duration(750);
t.each(function() {
d3.selectAll("line").transition() // 🌶
.attr("x1", x)
.attr("x2", x);
d3.selectAll("text").transition() // 🌶
.attr("x", x);
});
var t = d3.transition()
.duration(750);
t.selectAll("line")
.attr("x1", x)
.attr("x2", x);
t.selectAll("text")
.attr("x", x);
var t = d3.transition()
.duration(750);
d3.selectAll("line").transition(t)
.attr("x1", x)
.attr("x2", x);
d3.selectAll("text").transition(t)
.attr("x", x);
var t = d3.transition()
.duration(750);
line.transition(t)
.attr("x1", x)
.attr("x2", x);
text.transition(t)
.attr("x", x);

Maxim 2. Avoid modal behavior.

This is an extension of the previous maxim, avoid overloading meaning, for a more egregious violation. Here, D3 2.8 introduced inconsistency with selection.transition, but the behavioral trigger was not a different class; it was simply being inside a call to transition.each. A remarkable consequence of this design is that you can change the behavior of code you didn’t write by wrapping it with transition.each!

Case 3. Removing the magic of d3.transition(selection).

A powerful concept in most modern programming languages is the ability to define reusable units of code as functions. By wrapping code in a function, you can call it wherever you want, without resorting to copy-and-paste. While some software libraries define their own abstractions for reusing code (say, extending a chart type), D3 is agnostic about how you encapsulate code, and I recommend just using a function.

function makeitred(context) {
context.style("color", "red");
}
d3.select("body").call(makeitred);
d3.select("body").transition().call(makeitred);
function makeitred(context) {
context.each(function() { // 🌶
var s = d3.select(this),
t = d3.transition(s); // 🌶
t.style("color", "red");
});
}
function makeitred(context) {
var s = context.selection ? context.selection() : context,
t = context;
t.style("color", "red");
}
function makeitred(context) {
context.style("color", "red");
}

Maxim 3. Favor parsimony.

The d3.transition method was trying to be clever and combine two operations. The first is checking whether you’re inside the magic transition.each callback. If you are, the second is deriving a new transition from a selection. Yet the latter is already possible using selection.transition, so d3.transition was trying to do too much and hiding too much as a result.

Case 4. Repeating transitions with d3.active.

D3 transitions are finite sequences. Most often, a transition is just a single stage, transitioning from the current state of the document to the desired target state. However, sometimes you want more elaborate sequences that go through several stages:

bl.ocks.org/4341417
bl.ocks.org/346f4d967650b27c0511
svg.selectAll("circle")
.transition()
.duration(2500)
.delay(function(d) { return d * 40; })
.each(slide); // 🌶
function slide() {
var circle = d3.select(this);
(function repeat() {
circle = circle.transition() // 🌶
.attr("cx", width)
.transition()
.attr("cx", 0)
.each("end", repeat);
})(); // 🌶
}
svg.selectAll("circle")
.transition()
.duration(2500)
.delay(function(d) { return d * 40; })
.on("start", slide);
function slide() {
d3.active(this)
.attr("cx", width)
.transition()
.attr("cx", 0)
.transition()
.on("start", slide);
}

Maxim 4. Obscure solutions are not solutions.

This is a case where there’s a valid way to solve a problem, but it’s so intricate and brittle that you’re unlikely to discover it and you’re never going to remember it. I wrote the library and I still had to Google it.

Case 5. Freezing time in the background.

An infinitely-repeating transition in D3 3.x exhibits interesting behavior if you leave it open in a background tab for a long time. Well, by “interesting” I mean it looks like this:

Maxim 5. Question your assumptions.

Sometimes a design flaw may not be fixable by adding or changing a single method. Instead, there may be an underlying assumption that needs reexamination — like that time is absolute.

Case 6. Cancelling transitions with selection.interrupt.

Transitions are often initiated by events, such as the arrival of new data over the wire or user interaction. Since transitions are not instantaneous — they have a duration — that could mean multiple transitions competing to control the fate of elements. To avoid this, transitions should be exclusive, allowing a newer transition to pre-empt (to interrupt) an older one.

bl.ocks.org/3943967
selection
.interrupt() // interrupt the active transition
.transition(); // pre-empt any scheduled transitions
selection
.transition()
.each("start", alert); // 🌶
selection
.interrupt()
.transition();
selection.interrupt();

Maxim 6. Consider all possible usage patterns.

Asynchronous programming is notoriously difficult because the order of operations is highly unpredictable. While it is hard to implement robust and deterministic asynchronous APIs, surely it is harder to use brittle ones. The designer is responsible for being “thorough down to the last detail.”

Case 7. Naming parameters.

I’ll end with an easy one. D3 4.0 includes a few syntax improvements intended to make code more readable and self-describing. Consider this code using D3 3.x:

selection.transition()
.duration(750)
.ease("elastic-out", 1, 0.3);
  • What does the value 1 mean?
  • What does the value 0.3 mean?
  • What easing types besides “elastic-out” are supported?
  • Can I implement a custom easing function?
selection.transition()
.duration(750)
.ease(d3.easeElasticOut
.amplitude(1)
.period(0.3));

Maxim 7. Give hints.

Functions that take many arguments are obviously bad design. Humans shouldn’t be expected to memorize such elaborate definitions. (I can’t tell you how many times I’ve had to look up the arguments to context.arc when drawing to a 2D canvas.)

What Is The Purpose of Good Software?

It’s not just about computing a result quickly and correctly. It’s not even just about concise or elegant notation.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Mike Bostock

Mike Bostock

24K Followers

Building a better computational medium. Founder @observablehq. Creator #d3js. Former @nytgraphics. Pronounced BOSS-tock.