Skip to content

Instantly share code, notes, and snippets.

@WeikerWT
Created June 25, 2018 21:14
Show Gist options
  • Save WeikerWT/cceb0f9083b472a456be997ed28020ba to your computer and use it in GitHub Desktop.
Save WeikerWT/cceb0f9083b472a456be997ed28020ba to your computer and use it in GitHub Desktop.
Q4 2017
license: CC0-1.0

a treemap prototype where the

  • area of the cell
  • the maxArea of all the cells that share the same depth and same parent with that cell

is used to scale the color on an orange to lightgray colorscale.

the size variable is double-encoded as cell area and cell color.

I extend d3.layout.treemap to calculate the maxArea across a set of sibling nodes and call the extended layout d3.layout.treemap2

the sort property of the treemap2 layout positions the largest area cell in the top-right of the group.

.sort(function(a, b){ return a.value - b.value })

an iteration on http://mbostock.github.io/d3/talk/20111018/treemap.html

and this StackOverflow Answer

forked from micahstubbs's block: Q4 2017

(function(){
d3.layout.treemap2 = function() {
d3.layout.hierarchy = function() {
var sort = d3_layout_hierarchySort, children = d3_layout_hierarchyChildren, value = d3_layout_hierarchyValue;
function hierarchy(root) {
var stack = [ root ], nodes = [], node;
root.depth = 0;
while ((node = stack.pop()) != null) {
nodes.push(node);
if ((childs = children.call(hierarchy, node, node.depth)) && (n = childs.length)) {
var n, childs, child;
while (--n >= 0) {
stack.push(child = childs[n]);
child.parent = node;
child.depth = node.depth + 1;
}
if (value) node.value = 0;
node.children = childs;
} else {
if (value) node.value = +value.call(hierarchy, node, node.depth) || 0;
delete node.children;
}
}
d3_layout_hierarchyVisitAfter(root, function(node) {
var childs, parent;
if (sort && (childs = node.children)) childs.sort(sort);
if (value && (parent = node.parent)) parent.value += node.value;
});
return nodes;
}
hierarchy.sort = function(x) {
if (!arguments.length) return sort;
sort = x;
return hierarchy;
};
hierarchy.children = function(x) {
if (!arguments.length) return children;
children = x;
return hierarchy;
};
hierarchy.value = function(x) {
if (!arguments.length) return value;
value = x;
return hierarchy;
};
hierarchy.revalue = function(root) {
if (value) {
d3_layout_hierarchyVisitBefore(root, function(node) {
if (node.children) node.value = 0;
});
d3_layout_hierarchyVisitAfter(root, function(node) {
var parent;
if (!node.children) node.value = +value.call(hierarchy, node, node.depth) || 0;
if (parent = node.parent) parent.value += node.value;
});
}
return root;
};
return hierarchy;
};
function d3_layout_hierarchyRebind(object, hierarchy) {
d3.rebind(object, hierarchy, "sort", "children", "value");
object.nodes = object;
object.links = d3_layout_hierarchyLinks;
return object;
}
function d3_layout_hierarchyVisitBefore(node, callback) {
var nodes = [ node ];
while ((node = nodes.pop()) != null) {
callback(node);
if ((children = node.children) && (n = children.length)) {
var n, children;
while (--n >= 0) nodes.push(children[n]);
}
}
}
function d3_layout_hierarchyVisitAfter(node, callback) {
var nodes = [ node ], nodes2 = [];
while ((node = nodes.pop()) != null) {
nodes2.push(node);
if ((children = node.children) && (n = children.length)) {
var i = -1, n, children;
while (++i < n) nodes.push(children[i]);
}
}
while ((node = nodes2.pop()) != null) {
callback(node);
}
}
function d3_layout_hierarchyChildren(d) {
return d.children;
}
function d3_layout_hierarchyValue(d) {
return d.value;
}
function d3_layout_hierarchySort(a, b) {
return b.value - a.value;
}
function d3_layout_hierarchyLinks(nodes) {
return d3.merge(nodes.map(function(parent) {
return (parent.children || []).map(function(child) {
return {
source: parent,
target: child
};
});
}));
}
d3.layout.partition = function() {
var hierarchy = d3.layout.hierarchy(), size = [ 1, 1 ];
function position(node, x, dx, dy) {
var children = node.children;
node.x = x;
node.y = node.depth * dy;
node.dx = dx;
node.dy = dy;
if (children && (n = children.length)) {
var i = -1, n, c, d;
dx = node.value ? dx / node.value : 0;
while (++i < n) {
position(c = children[i], x, d = c.value * dx, dy);
x += d;
}
}
}
function depth(node) {
var children = node.children, d = 0;
if (children && (n = children.length)) {
var i = -1, n;
while (++i < n) d = Math.max(d, depth(children[i]));
}
return 1 + d;
}
function partition(d, i) {
var nodes = hierarchy.call(this, d, i);
position(nodes[0], 0, size[0], size[1] / depth(nodes[0]));
return nodes;
}
partition.size = function(x) {
if (!arguments.length) return size;
size = x;
return partition;
};
return d3_layout_hierarchyRebind(partition, hierarchy);
};
var hierarchy = d3.layout.hierarchy(), round = Math.round, size = [ 1, 1 ], padding = null, pad = d3_layout_treemapPadNull, sticky = false, stickies, mode = "squarify", ratio = .5 * (1 + Math.sqrt(5));
function scale(children, k) {
var i = -1, n = children.length, child, area;
while (++i < n) {
area = (child = children[i]).value * (k < 0 ? 0 : k);
child.area = isNaN(area) || area <= 0 ? 0 : area;
}
}
function squarify(node) {
var children = node.children;
if (children && children.length) {
var rect = pad(node), row = [], remaining = children.slice(), child, best = Infinity, score, u = mode === "slice" ? rect.dx : mode === "dice" ? rect.dy : mode === "slice-dice" ? node.depth & 1 ? rect.dy : rect.dx : Math.min(rect.dx, rect.dy), n;
scale(remaining, rect.dx * rect.dy / node.value);
row.area = 0;
while ((n = remaining.length) > 0) {
row.push(child = remaining[n - 1]);
row.area += child.area;
if (mode !== "squarify" || (score = worst(row, u)) <= best) {
remaining.pop();
best = score;
} else {
row.area -= row.pop().area;
position(row, u, rect, false);
u = Math.min(rect.dx, rect.dy);
row.length = row.area = 0;
best = Infinity;
}
}
if (row.length) {
position(row, u, rect, true);
row.length = row.area = 0;
}
children.forEach(squarify);
}
}
function stickify(node) {
var children = node.children;
if (children && children.length) {
var rect = pad(node), remaining = children.slice(), child, row = [];
scale(remaining, rect.dx * rect.dy / node.value);
row.area = 0;
while (child = remaining.pop()) {
row.push(child);
row.area += child.area;
if (child.z != null) {
position(row, child.z ? rect.dx : rect.dy, rect, !remaining.length);
row.length = row.area = 0;
}
}
children.forEach(stickify);
}
}
function worst(row, u) {
var s = row.area, r, rmax = 0, rmin = Infinity, i = -1, n = row.length;
while (++i < n) {
if (!(r = row[i].area)) continue;
if (r < rmin) rmin = r;
if (r > rmax) rmax = r;
}
s *= s;
u *= u;
return s ? Math.max(u * rmax * ratio / s, s / (u * rmin * ratio)) : Infinity;
}
function position(row, u, rect, flush) {
var i = -1, n = row.length, x = rect.x, y = rect.y, v = u ? round(row.area / u) : 0, o;
if (u == rect.dx) {
if (flush || v > rect.dy) v = rect.dy;
while (++i < n) {
o = row[i];
o.x = x;
o.y = y;
o.dy = v;
x += o.dx = Math.min(rect.x + rect.dx - x, v ? round(o.area / v) : 0);
}
o.z = true;
o.dx += rect.x + rect.dx - x;
rect.y += v;
rect.dy -= v;
} else {
if (flush || v > rect.dx) v = rect.dx;
while (++i < n) {
o = row[i];
o.x = x;
o.y = y;
o.dx = v;
y += o.dy = Math.min(rect.y + rect.dy - y, v ? round(o.area / v) : 0);
}
o.z = false;
o.dy += rect.y + rect.dy - y;
rect.x += v;
rect.dx -= v;
}
}
function treemap(d) {
var nodes = stickies || hierarchy(d), root = nodes[0];
root.x = 0;
root.y = 0;
root.dx = size[0];
root.dy = size[1];
if (stickies) hierarchy.revalue(root);
scale([ root ], root.dx * root.dy / root.value);
(stickies ? stickify : squarify)(root);
if (sticky) stickies = nodes;
// ************************************************
var maxDepth = d3.max(nodes.map(function(d){ return d.depth }));
// loop through all the depths, or hierarchy levels from the deepest level
// to level 1
for(i = maxDepth; i > 0; i--){
var parents = d3.set([]);
// create an array of child nodes for the current depth
var children = nodes.filter(function(d){ return d.depth == i })
// get a set of unique parent names for the level
children.forEach(function(child){
parents.add(child.parent.name)
})
var currentParents = parents.values();
// calculate the maxArea across all children of each parent
// then, store that maxArea as a property on each child object
currentParents.forEach(function(currentParent) {
var currentChildren = children.filter(function(d) { return d.parent.name == currentParent });
var currentMaxArea = d3.max( currentChildren.map(function(d){ return d.area }) )
currentChildren.forEach(function(d) { d["maxArea"] = currentMaxArea });
})
}
console.log("nodes from inside of the treemap2 layout", nodes);
// ************************************************
return nodes;
}
treemap.size = function(x) {
if (!arguments.length) return size;
size = x;
return treemap;
};
treemap.padding = function(x) {
if (!arguments.length) return padding;
function padFunction(node) {
var p = x.call(treemap, node, node.depth);
return p == null ? d3_layout_treemapPadNull(node) : d3_layout_treemapPad(node, typeof p === "number" ? [ p, p, p, p ] : p);
}
function padConstant(node) {
return d3_layout_treemapPad(node, x);
}
var type;
pad = (padding = x) == null ? d3_layout_treemapPadNull : (type = typeof x) === "function" ? padFunction : type === "number" ? (x = [ x, x, x, x ],
padConstant) : padConstant;
return treemap;
};
treemap.round = function(x) {
if (!arguments.length) return round != Number;
round = x ? Math.round : Number;
return treemap;
};
treemap.sticky = function(x) {
if (!arguments.length) return sticky;
sticky = x;
stickies = null;
return treemap;
};
treemap.ratio = function(x) {
if (!arguments.length) return ratio;
ratio = x;
return treemap;
};
treemap.mode = function(x) {
if (!arguments.length) return mode;
mode = x + "";
return treemap;
};
return d3_layout_hierarchyRebind(treemap, hierarchy);
};
function d3_layout_treemapPadNull(node) {
return {
x: node.x,
y: node.y,
dx: node.dx,
dy: node.dy
};
}
function d3_layout_treemapPad(node, padding) {
var x = node.x + padding[3], y = node.y + padding[0], dx = node.dx - padding[1] - padding[3], dy = node.dy - padding[0] - padding[2];
if (dx < 0) {
x += dx / 2;
dx = 0;
}
if (dy < 0) {
y += dy / 2;
dy = 0;
}
return {
x: x,
y: y,
dx: dx,
dy: dy
};
}
})();
{
"name": "Momentum",
"children": [
{
"name": "2017Q4",
"children": [
{
"name": "Collaboration Risk Assessment",
"children": [
{
"name": "Non-Sales",
"children": [
{"name": "Inspire Q4", "size": 25}
]
}
]
},
{
"name": "Pipeline Growth",
"children": [
{"name": "New Opportunities Q4", "size": 12},
{
"name": "Marketing",
"children": [
{"name": "Twitter Impressions Q4", "size": 550},
{"name": "Form Generation Q4", "size": 75},
{"name": "Website Visits Q4", "size": 78}
]
},
{"name": "Cold Calling Q4", "size": 250},
{"name": "Freemium Q4", "size": 231}
]
},
{
"name": "Non-Sales",
"children": [
{"name": "Inspire Q4", "size": 78},
{"name": "XYZZ Q4", "size": 27}
]
}
]
}
]
}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<link type="text/css" rel="stylesheet" href="style.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script src="d3.layout.treemap2.js" type="text/javascript"></script>
<style type="text/css">
.chart {
display: block;
margin: auto;
margin-top: 40px;
}
text {
font-size: 11px;
}
rect {
fill: none;
}
</style>
</head>
<body>
<div id="body">
<div id="footer">
d3.layout.treemap2
<div class="hint">click or option-click to descend or ascend</div>
<div><select>
<option value="size">Size</option>
<option value="count">Count</option>
</select></div>
</div>
</div>
<script type="text/javascript">
// an iteration on
// http://mbostock.github.io/d3/talk/20111018/treemap.html
var w = 1280 - 80,
h = 800 - 180,
x = d3.scale.linear().range([0, w]),
y = d3.scale.linear().range([0, h]),
root,
node,
maxArea;
// ************************************************
var materialOrange = "#FC582F"
// http://stackoverflow.com/questions/21734017/d3-js-how-to-decide-both-the-area-and-the-color-of-each-square-by-the-size-of-e
var colorLinearScale = d3.scale.linear()
.range([0,1]);
// wait to set the domain until we know
// the maxArea for the current child node
// and it's siblings
//colors can be specified as any CSS color string
var colorInterpolator = d3.interpolateHsl("lightgray", materialOrange);
// ************************************************
var treemap = d3.layout.treemap2()
.round(false)
.size([w, h])
.sticky(true)
.sort(function(a, b){ return a.value - b.value })
.value(function(d) { return d.size; });
var svg = d3.select("#body").append("div")
.attr("class", "chart")
.style("width", w + "px")
.style("height", h + "px")
.append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(.5,.5)");
d3.json("flare.json", function(data) {
node = root = data;
var nodes = treemap.nodes(root)
.filter(function(d) { return !d.children; });
var cell = svg.selectAll("g")
.data(nodes)
.enter().append("svg:g")
.attr("class", "cell")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.on("click", function(d) { return zoom(node == d.parent ? root : d.parent); });
cell.append("svg:rect")
.attr("width", function(d) { return d.dx - 1; })
.attr("height", function(d) { return d.dy - 1; })
.style("fill", function(d) {
colorLinearScale.domain([0, d.maxArea]);
return colorInterpolator(colorLinearScale(d.area));
});
cell.append("svg:text")
.attr("x", function(d) { return d.dx / 2; })
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; })
.style("opacity", function(d) { d.w = this.getComputedTextLength(); return d.dx > d.w ? 1 : 0; });
d3.select(window).on("click", function() { zoom(root); });
d3.select("select").on("change", function() {
treemap.value(this.value == "size" ? size : count).nodes(root);
zoom(node);
});
});
function size(d) {
return d.size;
}
function count(d) {
return 1;
}
function zoom(d) {
var kx = w / d.dx, ky = h / d.dy;
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
var t = svg.selectAll("g.cell").transition()
.duration(d3.event.altKey ? 7500 : 750)
.attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; });
t.select("rect")
.attr("width", function(d) { return kx * d.dx - 1; })
.attr("height", function(d) { return ky * d.dy - 1; })
t.select("text")
.attr("x", function(d) { return kx * d.dx / 2; })
.attr("y", function(d) { return ky * d.dy / 2; })
.style("opacity", function(d) { return kx * d.dx > d.w ? 1 : 0; });
node = d;
d3.event.stopPropagation();
}
</script>
</body>
</html>
body {
background: url(texture-noise.png);
overflow: hidden;
margin: 0;
font-size: 14px;
font-family: "Helvetica Neue", Helvetica;
}
#chart, #header, #footer {
position: absolute;
top: 0;
}
#header, #footer {
z-index: 1;
display: block;
font-size: 36px;
font-weight: 300;
text-shadow: 0 1px 0 #fff;
}
#header.inverted, #footer.inverted {
color: #fff;
text-shadow: 0 1px 4px #000;
}
#header {
top: 80px;
left: 140px;
width: 1000px;
}
#footer {
top: 680px;
right: 140px;
text-align: right;
}
rect {
fill: none;
pointer-events: all;
}
pre {
font-size: 18px;
}
line {
stroke: #000;
stroke-width: 1.5px;
}
.string, .regexp {
color: #f39;
}
.keyword {
color: #00c;
}
.comment {
color: #777;
font-style: oblique;
}
.number {
color: #369;
}
.class, .special {
color: #1181B8;
}
a:link, a:visited {
color: #000;
text-decoration: none;
}
a:hover {
color: #666;
}
.hint {
position: absolute;
right: 0;
width: 1280px;
font-size: 12px;
color: #999;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment