Since I started writing D3.js sometime last year, I’ve developed a strong and spotless affinity for the tool and data visualization. On this serendipitous odyssey, I believe I’ve found something unconventionally interesting and underappreciated (perhaps overlooked) about D3 that I’d love to share.
What is D3?
D3 (or D3.js, Data-Driven Documents) is “the JavaScript library for bespoke data visualization.”
You see, the primitives are all that matter when it comes to data visualization with D3. There is no concept of “bar charts” or “histograms” or insert chart type, instead, of primitive APIs that serve as building blocks—like arrays, time, scales, axis, etc. This low-level primitives are what makes it customizable and powerful. It’s a case of to whom much is given.
Understanding these APIs is crucial to the end results since they are basically a means to an end. However, these APIs are merely JavaScript and can be used purposefully for JavaScript operations.
D3 is distributed as a distinct set of unique APIs.
For example, d3-array
contains several methods for working with iterables (such as an array, set, or generator) in JavaScript. And even though these methods were designed for “analyzing or visualizing data,” I simply repurpose them for my needs. Even though I could substitute lodash or underscore.js or custom methods for some of these D3 methods, I find that using them works best [for me] because:
- I write D3 for data visualization, so leveraging its methods in my projects helps me get familiar and better with them.
- Even though they’re geared towards data visualization, they can be better designed (because they serve a singular, non-generic purpose) and more performant for everyday usage.
- I can learn JavaScript indirectly because D3 is open source, and by peeking at the source code, I can see how some of these methods are implemented.
Some Examples
d3.permute
To permute or rearrange an array (source) based on a key of indices (keys), I’d use d3.permute
. And this is how the API is implemented:
function permuteFn(source, keys) {
return Array.from(keys, (key) => source[key]);
}
And can be used as such:
const source = { age: 27, club: "FC Barcelona", name: "Pedri", number: 8 };
const keys = ["name", "age", "club", "number"];
const result = permuteFn(source, keys);
With the result being: [ 'Pedri', 27, 'FC Barcelona', 8 ]
.
Map
and InternMap
Take D3’s InternMap
for example. It is used to store key-value pairs and extends the native JavaScript’s Map
 object by allowing Dates and other non-primitive keys. It does this by bypassing the SameValueZero algorithm when determining key equality.
For example in jsMap
below, result
is undefined
because _key
is a different object from the sole key in jsMap
, that is, new Date("2023-01-01")
.
const jsMap = new Map([[new Date("2023-01-01"), 45]]);
const _key = new Date("2023-01-01");
const result = jsMap.get(_key); // undefined
However with d3.InternMap
Dates used as keys preserve their reference.
const d3InternMap = new d3.InternMap([[new Date("2023-01-01"), 45]]);
const _key = new Date("2023-01-01");
const result = d3InternMap.get(_key); // 45
Sorting
Sorting is another. Take the example below:
const numbers = [1, 2, 10];
const sortedNumbers = numbers.sort(); // [1, 10, 2]
sortedNumbers
is [1, 10, 2]
because the numbers were first coerced to strings and then sorted in lexicographic (case-sensitive alphabetical) order!.
With D3:
const numbers = [1, 2, 10];
const sortedNumbers = d3.sort(numbers); // [1, 2, 10]
I could also have written:
const numbers = [1, 2, 10];
const sortedNumbers = numbers.sort((a, b) => (a > b ? 1 : -1)); // [1, 2, 10]
And everything would be fine.