Skip to content

Instantly share code, notes, and snippets.

@tommct
Last active January 1, 2016 19:38
Show Gist options
  • Save tommct/8191276 to your computer and use it in GitHub Desktop.
Save tommct/8191276 to your computer and use it in GitHub Desktop.
D3 Hierarchical Ordinal Ticks

This D3 example demonstrates constrained zooming, much like http://bl.ocks.org/tommct/5671250, but also illustrates the use of hierarchical ordinal tick marks. It does this by using the normalized values that one gets when using a hierarchical partition layout.

# -*- coding: utf-8 -*-
# <nbformat>3.0</nbformat>
# <codecell>
import json
import random
H = {'name': 'root', 'sub':[]}
for chapter in range(1,10):
C = {'name':str(chapter), 'sub':[]}
for section in range(1, random.randint(2,8)):
S = {'name':str(section), 'sub':[]}
for subsection in range(1, random.randint(2,8)):
SS = {'name':str(subsection)}
S['sub'].append(SS)
C['sub'].append(S)
H['sub'].append(C)
with open('hierarchy.json', 'w') as f:
json.dump(H, f)
# <codecell>
{"name": "root", "sub": [{"name": "1", "sub": [{"name": "1", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}]}, {"name": "2", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}, {"name": "7"}]}, {"name": "3", "sub": [{"name": "1"}, {"name": "2"}]}, {"name": "4", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}, {"name": "7"}]}, {"name": "5", "sub": [{"name": "1"}]}, {"name": "6", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}]}, {"name": "7", "sub": [{"name": "1"}, {"name": "2"}]}]}, {"name": "2", "sub": [{"name": "1", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}]}, {"name": "2", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}]}, {"name": "3", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}, {"name": "7"}]}, {"name": "4", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}]}, {"name": "5", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}]}, {"name": "6", "sub": [{"name": "1"}]}, {"name": "7", "sub": [{"name": "1"}]}]}, {"name": "3", "sub": [{"name": "1", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}]}, {"name": "2", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}, {"name": "7"}]}, {"name": "3", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}]}, {"name": "4", "sub": [{"name": "1"}, {"name": "2"}]}, {"name": "5", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}]}, {"name": "6", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}]}, {"name": "7", "sub": [{"name": "1"}, {"name": "2"}]}]}, {"name": "4", "sub": [{"name": "1", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}, {"name": "7"}]}, {"name": "2", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}]}, {"name": "3", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}]}, {"name": "4", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}]}, {"name": "5", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}]}]}, {"name": "5", "sub": [{"name": "1", "sub": [{"name": "1"}]}, {"name": "2", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}]}]}, {"name": "6", "sub": [{"name": "1", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}, {"name": "7"}]}, {"name": "2", "sub": [{"name": "1"}, {"name": "2"}]}, {"name": "3", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}]}, {"name": "4", "sub": [{"name": "1"}]}, {"name": "5", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}]}, {"name": "6", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}]}]}, {"name": "7", "sub": [{"name": "1", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}]}, {"name": "2", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}]}, {"name": "3", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}]}, {"name": "4", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}]}, {"name": "5", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}]}, {"name": "6", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}]}]}, {"name": "8", "sub": [{"name": "1", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}, {"name": "4"}, {"name": "5"}, {"name": "6"}, {"name": "7"}]}]}, {"name": "9", "sub": [{"name": "1", "sub": [{"name": "1"}, {"name": "2"}, {"name": "3"}]}]}]}
<!DOCTYPE html>
<meta charset="utf-8">
<title>Constrained Zoom by Rectangle</title>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style>
body {
font-family: sans-serif;
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
svg {
font: 9pt sans-serif;
shape-rendering: crispEdges;
}
rect {
fill: #ddd;
}
rect.zoom {
stroke: steelblue;
fill: #bbb;
fill-opacity: 0.5;
}
.axis path, .axis line {
fill: none;
stroke: #fff;
}
</style>
<p><label for="zoom-rect"><input type="checkbox" id="zoom-rect"> zoom by rectangle</label>
<script>
var margin = {top: 20, right: 12, bottom: 20, left: 100},
width = 960 - margin.left - margin.right,
height = 430 - margin.top - margin.bottom;
var xmin = 0,
xmax = 500,
ymin = 0,
ymax = 1;
var ynorm = d3.scale.linear().domain([ymin, ymax]);
var x = d3.scale.linear()
.domain([xmin, xmax])
.range([0, width]);
var y = d3.scale.linear()
.domain([ymin, ymax])
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
.tickSize(-height); // Draw gridlines
// Partition layout is used for getting the normalized tick locations at the
// various depths of the hierarchy
var partition = d3.layout.partition()
.children(function(d) { return (d.sub) ? d.sub : null;})
.value(function(d) { return 1;}) // Size by number of entries
.sort(function(a,b){return a.name.localeCompare(b.name);});
var nodes;
d3.json("hierarchy.json", function(json) {
nodes = partition.nodes(json);
ymax = nodes.filter(function(d){return d.depth==3;}).length;
y.domain([ymin, ymax]);
var zoom = d3.behavior.zoom().x(x).y(y).scaleExtent([.001, Infinity]).on("zoom", refresh);
var chapter_x = nodes.filter(function(d){return d.depth==1;})
.map(function(d){return d.x*ymax;});
var chapter_names = nodes.filter(function(d){return d.depth==1;})
.map(function(d){return d.name;});
var chapter_scale = d3.scale.ordinal()
.domain(chapter_x)
.range(chapter_names);
var section_x = nodes.filter(function(d){return d.depth==2;})
.map(function(d){return d.x*ymax;});
var section_names = nodes.filter(function(d){return d.depth==2;})
.map(function(d){return d.parent.name + "." + d.name;});
var section_scale = d3.scale.ordinal()
.domain(section_x)
.range(section_names);
var subsection_x = nodes.filter(function(d){return d.depth==3;})
.map(function(d){return d.x*ymax;});
var subsection_names = nodes.filter(function(d){return d.depth==3;})
.map(function(d){
return d.parent.parent.name + "." + d.parent.name + "." + d.name;
});
var subsection_scale = d3.scale.ordinal()
.domain(subsection_x)
.range(subsection_names);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.tickValues(function() {
var ydom = y.domain();
if ((ydom[1]-ydom[0]) > 0.25*ymax) {
return chapter_x.filter(function(d){
return ((d+.001>=ydom[0])&(d<=ydom[1]));
});
} else if ((y.domain()[1]-y.domain()[0]) > 0.1*ymax) {
return section_x.filter(function(d){
return ((d+.001>=ydom[0])&(d<=ydom[1]));
});
} else {
return subsection_x.filter(function(d){
return ((d+.001>=ydom[0])&(d<=ydom[1]));
});
}
})
.tickFormat(function(d) {
var ydom = y.domain();
if ((ydom[1]-ydom[0]) > 0.25*ymax) {
return chapter_scale(d);
} else if ((ydom[1]-ydom[0]) > 0.1*ymax) {
return section_scale(d);
} else {
return subsection_scale(d);
}
})
.tickSize(-width); // tickLine == gridline
var zoomRect = false;
d3.select("#zoom-rect").on("change", function() {
zoomRect = this.checked;
});
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom)
.append("g")
.on("mousedown", function() {
if (!zoomRect) return;
var e = this,
origin = d3.mouse(e),
rect = svg.append("rect").attr("class", "zoom");
d3.select("body").classed("noselect", true);
origin[0] = Math.max(0, Math.min(width, origin[0]));
origin[1] = Math.max(0, Math.min(height, origin[1]));
d3.select(window)
.on("mousemove.zoomRect", function() {
var m = d3.mouse(e);
m[0] = Math.max(0, Math.min(width, m[0]));
m[1] = Math.max(0, Math.min(height, m[1]));
rect.attr("x", Math.min(origin[0], m[0]))
.attr("y", Math.min(origin[1], m[1]))
.attr("width", Math.abs(m[0] - origin[0]))
.attr("height", Math.abs(m[1] - origin[1]));
})
.on("mouseup.zoomRect", function() {
d3.select(window).on("mousemove.zoomRect", null).on("mouseup.zoomRect", null);
d3.select("body").classed("noselect", false);
var m = d3.mouse(e);
m[0] = Math.max(0, Math.min(width, m[0]));
m[1] = Math.max(0, Math.min(height, m[1]));
if (m[0] !== origin[0] && m[1] !== origin[1]) {
zoom.x(x.domain([origin[0], m[0]].map(x.invert).sort()))
.y(y.domain([origin[1], m[1]].map(y.invert).sort()));
}
rect.remove();
refresh();
}, true);
d3.event.stopPropagation();
});
svg.append("rect")
.attr("width", width)
.attr("height", height);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
function refresh() {
var t = zoom.translate();
var s = zoom.scale();
var tx = t[0],
ty = t[1];
var xdom = x.domain();
var reset_s = 0;
if ((xdom[1] - xdom[0]) >= (xmax - xmin)) {
zoom.x(x.domain([xmin, xmax]));
xdom = x.domain();
reset_s = 1;
}
var ydom = y.domain();
if ((ydom[1] - ydom[0]) >= (ymax - ymin)) {
zoom.y(y.domain([ymin, ymax]));
ydom = y.domain();
reset_s += 1;
}
if (reset_s == 2) { // Both axes are full resolution. Reset.
zoom.scale(1);
tx = 0;
ty = 0;
} else {
if (xdom[0] < xmin) {
tx = 0;
x.domain([xmin, xdom[1] - xdom[0] + xmin]);
xdom = x.domain();
}
if (xdom[1] > xmax) {
xdom[0] -= xdom[1] - xmax;
tx = -xdom[0]*width/(xmax-xmin)*s;
x.domain([xdom[0], xmax]);
}
if (ydom[0] < ymin) {
y.domain([ymin, ydom[1] - ydom[0] + ymin]);
ydom = y.domain();
ty = -(ymax-ydom[1])*height/(ymax-ymin)*s;
}
if (ydom[1] > ymax) {
ydom[0] -= ydom[1] - ymax;
ty = 0;
y.domain([ydom[0], ymax]);
}
}
// Reset (possibly) if hit an edge so that next focus event starts correctly.
zoom.translate([tx, ty]);
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
}
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment