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.
Last active
January 1, 2016 19:38
-
-
Save tommct/8191276 to your computer and use it in GitHub Desktop.
D3 Hierarchical Ordinal Ticks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- 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> | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{"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"}]}]}]} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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