Skip to content

Instantly share code, notes, and snippets.

@aviddiviner
Last active January 5, 2018 20:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aviddiviner/84d905e60c6788f77ee21d35f873b236 to your computer and use it in GitHub Desktop.
Save aviddiviner/84d905e60c6788f77ee21d35f873b236 to your computer and use it in GitHub Desktop.
Understanding d3 scale quantize / quantile
license: gpl-3.0

This interactive chart is designed to help visualize how d3.scale.quantize and d3.scale.quantile do their rounding to different values. These are quantitative scales which map a continuous domain to a discrete range.

Drag the blue marker on the top row (input domain) and see where each yellow marker (output range) snaps to its next value. There are a bunch of example scales with different domains and ranges.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
circle, path {
fill: none;
stroke: steelblue;
stroke-width: 2;
}
.dotted {
stroke: lightgrey;
stroke-width: 1;
stroke-dasharray: 10, 2;
}
.handle {
fill: lightblue;
cursor: col-resize;
}
.target {
fill: yellow;
}
.label {
font-family: monospace;
font-size: 11px;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var width = 960,
height = 500,
radius = 6;
var markers = [300, 350, 450, 600, 800];
var narrow = [400, 500, 700];
// Scales
// Here we have a bunch of scales, with the domain and range being similar, to
// see how the scales do their rounding and magic.
// The quantize scale's input domain is a two-element array of numbers. If the
// array contains more than two numbers, only the first and last number are used.
var qz = d3.scale.quantize()
.domain(d3.extent(markers))
.range(markers);
// The input domain of a quantile scale is a set of discrete numeric values.
// The array must not be empty, and must contain at least one numeric value.
var ql1 = d3.scale.quantile()
.domain(d3.extent(markers))
.range(markers);
var ql2 = d3.scale.quantile()
.domain(markers)
.range(markers);
// If the number of values in this scale's range is N + 1, the number of values
// in the domain must be N. If there are fewer than N domain elements, the
// additional values in the range are ignored.
var th = d3.scale.threshold()
.domain(markers.slice(0, -1))
.range(markers);
// Here are the same scales again, but with a narrower range, to illustrate.
var nqz = d3.scale.quantize()
.domain(d3.extent(markers))
.range(narrow);
var nql = d3.scale.quantile()
.domain(d3.extent(markers))
.range(narrow);
var nth = d3.scale.threshold()
.domain(d3.extent(markers))
.range(narrow);
// Boundaries
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
var gs = svg.selectAll(".markers")
.data(markers)
.enter().append("g")
.attr("class", "markers");
gs.append("path")
.attr("class", "dotted")
.attr("d", function(d) { return "M" + d + ",25 L" + d + ",300"; });
gs.append("text")
.attr("class", "label")
.style("text-anchor", "middle")
.attr("dx", function(d) { return d; })
.attr("dy", 15)
.text(function(d) { return d; });
var gs = svg.selectAll(".narrow")
.data(narrow)
.enter().append("g")
.attr("class", "narrow");
gs.append("path")
.attr("class", "dotted")
.attr("d", function(d) { return "M" + d + ",325 L" + d + ",490"; });
gs.append("text")
.attr("class", "label")
.style("text-anchor", "middle")
.attr("dx", function(d) { return d; })
.attr("dy", 315)
.text(function(d) { return d; });
// Drag group
var drag = d3.behavior.drag()
.origin(function(d) { return {x: d.x}; })
.on("drag", dragMove);
var g = svg.append("g")
.datum(function(d) {
return {x: 250, qz: 250, ql1: 250, ql2: 250, th: 250, nqz: 250, nql: 250, nth: 250};
})
.call(drag);
g.append("path")
.attr("class", "joiner")
.attr("d", joinPath);
g.append("text")
.attr("class", "label position")
.style("text-anchor", "middle")
.style("font-weight", "bold")
.attr("dx", function(d) { return d.x; })
.attr("dy", 45)
.text(function(d) { return d.x; });
function addCircle(y, cls, fnx) {
return g.append("circle")
.attr("class", cls)
.attr("cx", fnx)
.attr("cy", y)
.attr("r", radius);
}
addCircle(60, "handle", function(d) { return d.x; });
addCircle(110, "target qz", function(d) { return d.qz; });
addCircle(170, "target ql1", function(d) { return d.ql1; });
addCircle(215, "target ql2", function(d) { return d.ql2; });
addCircle(275, "target th", function(d) { return d.th; });
addCircle(350, "target nqz", function(d) { return d.nqz; });
addCircle(410, "target nql", function(d) { return d.nql; });
addCircle(470, "target nth", function(d) { return d.nth; });
function joinPath(d) {
return "M" + d.x + ",60"
+ "L" + d.qz + ",110"
+ "L" + d.ql1 + ",170"
+ "L" + d.ql2 + ",215"
+ "L" + d.th + ",275"
+ "L" + d.nqz + ",350"
+ "L" + d.nql + ",410"
+ "L" + d.nth + ",470";
}
function dragMove(d) {
var g = d3.select(this);
d.x = Math.max(radius, Math.min(width - radius, d3.event.x));
g.select(".target.qz").attr("cx", d.qz = qz(d.x));
g.select(".target.ql1").attr("cx", d.ql1 = ql1(d.x));
g.select(".target.ql2").attr("cx", d.ql2 = ql2(d.x));
g.select(".target.th").attr("cx", d.th = th(d.x));
g.select(".target.nqz").attr("cx", d.nqz = nqz(d.x));
g.select(".target.nql").attr("cx", d.nql = nql(d.x));
g.select(".target.nth").attr("cx", d.nth = nth(d.x));
g.select(".joiner").attr("d", joinPath(d));
g.select(".position").attr("dx", d.x).text(d.x);
g.select(".handle").attr("cx", d.x);
}
// Labels and decoration
function addLabel(y, text) {
return svg.append("g")
.attr("transform", "translate(10," + (y - 15) + ")")
.selectAll("text")
.data(text.split("\n"))
.enter().append("text")
.attr("class", "label")
.attr("dy", function(d, i) { return (i + 0.35) + "em"; })
.text(function(d) { return d; });
}
addLabel(60, "Drag this:").style("font-weight", "bold");
addLabel(110, "d3.scale.quantize()\n.domain(" + qz.domain() + ")\n.range(" + qz.range() + ")");
addLabel(170, "d3.scale.quantile()\n.domain(" + ql1.domain() + ")\n.range(" + ql1.range() + ")");
addLabel(215, "d3.scale.quantile()\n.domain(" + ql2.domain() + ")\n.range(" + ql2.range() + ")");
addLabel(275, "d3.scale.threshold()\n.domain(" + th.domain() + ")\n.range(" + th.range() + ")");
addLabel(350, "d3.scale.quantize()\n.domain(" + nqz.domain() + ")\n.range(" + nqz.range() + ")");
addLabel(410, "d3.scale.quantile()\n.domain(" + nql.domain() + ")\n.range(" + nql.range() + ")");
addLabel(470, "d3.scale.threshold()\n.domain(" + nth.domain() + ")\n.range(" + nth.range() + ")");
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment