Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 42 additions & 14 deletions caravel/assets/visualizations/treemap.css
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
.node {
border: solid 1px white;
font: 10px sans-serif;
line-height: 12px;
overflow: hidden;
position: absolute;
text-indent: 2px;
padding: 0px; /* form div giving top 1px */
box-sizing: content-box; /* otherwise inheriting border-box */
}

.treemap-container {
position: relative;
margin: auto;
text {
pointer-events: none;
}

.grandparent text {
font-weight: bold;
}

rect {
fill: none;
stroke: #fff;
}

rect.parent,
.grandparent rect {
stroke-width: 2px;
}

rect.parent {
pointer-events: none;
}

.grandparent rect {
fill: #eee;
}

.grandparent:hover rect {
fill: #aaa;
}

.children rect.parent,
.grandparent rect {
cursor: pointer;
}

.children rect.parent {
fill: #bbb;
fill-opacity: .5;
}

.children:hover rect.child {
fill: #bbb;
}
259 changes: 206 additions & 53 deletions caravel/assets/visualizations/treemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,68 +5,222 @@ var px = window.px || require('../javascripts/modules/caravel.js');
// CSS
require('./treemap.css');

/* Modified from https://bl.ocks.org/mbostock/4063582 */
/* Modified from http://bl.ocks.org/ganeshv/6a8e9ada3ab7f2d88022 */
function treemap(slice) {

var div = d3.select(slice.selector);

var _draw = function (data, eltWidth, eltHeight, includeTitle) {
var _draw = function (data, eltWidth, eltHeight, formData) {

var margin = { top: 0, right: 0, bottom: 0, left: 0 },
headerHeight = includeTitle ? 30 : 0,
navBarHeight = 36,
navBarTitleSize = navBarHeight / 3,
navBarBuffer = 10,
width = eltWidth - margin.left - margin.right,
height = eltHeight - headerHeight - margin.top - margin.bottom;
height = eltHeight - navBarHeight - navBarBuffer -
margin.top - margin.bottom,
transitioning,
formatNumber = d3.format(formData.number_format);

var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);

var y = d3.scale.linear()
.domain([0, height])
.range([0, height]);

var treemap = d3.layout.treemap()
.size([width, height])
.value(function (d) { return d.value; });

var root = div.append("div")
.classed("treemap-container", true);

var header = root.append("div")
.style("width", (width + margin.left + margin.right) + "px")
.style("height", headerHeight + "px");

var container = root.append("div")
.style("position", "relative")
.style("width", (width + margin.left + margin.right) + "px")
.style("height", (height + margin.top + margin.bottom) + "px")
.style("left", margin.left + "px")
.style("top", margin.top + "px");

var position = function (selection) {
selection.style("left", function (d) { return d.x + "px"; })
.style("top", function (d) { return d.y + "px"; })
.style("width", function (d) { return Math.max(0, d.dx - 1) + "px"; })
.style("height", function (d) { return Math.max(0, d.dy - 1) + "px"; });
.children(function (d, depth) { return depth ? null : d._children; })
.sort(function (a, b) { return a.value - b.value; })
.ratio(formData.treemap_ratio)
.mode("squarify")
.round(false);

var svg = div.append("svg")
.attr("width", eltWidth)
.attr("height", eltHeight);

var chartContainer = svg.append("g")
.attr("transform", "translate(" + margin.left + "," +
(margin.top + navBarHeight + navBarBuffer) + ")")
.style("shape-rendering", "crispEdges");

var grandparent = svg.append("g")
.attr("class", "grandparent")
.attr("transform", "translate(0," + (margin.top + navBarBuffer / 2) + ")");

grandparent.append("rect")
.attr("width", width)
.attr("height", navBarHeight);

grandparent.append("text")
.attr("x", width / 2)
.attr("y", navBarHeight / 2 + navBarTitleSize / 2)
.style("font-size", navBarTitleSize + "px")
.style("text-anchor", "middle");

var initialize = function (root) {
root.x = root.y = 0;
root.dx = width;
root.dy = height;
root.depth = 0;
};

// Aggregate the values for internal nodes. This is normally done by the
// treemap layout, but not here because of our custom implementation.
// We also take a snapshot of the original children (_children) to avoid
// the children being overwritten when when layout is computed.
var accumulate = function (d) {
return (d._children = d.children)
? d.value = d.children.reduce(function (p, v) { return p + accumulate(v); }, 0)
: d.value;
};

// Compute the treemap layout recursively such that each group of siblings
// uses the same size (1x1) rather than the dimensions of the parent cell.
// This optimizes the layout for the current zoom state. Note that a wrapper
// object is created for the parent node for each group of siblings so that
// the parents dimensions are not discarded as we recurse. Since each group
// of sibling was laid out in 1x1, we must rescale to fit using absolute
// coordinates. This lets us use a viewport to zoom.
var layout = function (d) {
if (d._children) {
treemap.nodes({ _children: d._children });
d._children.forEach(function (c) {
c.x = d.x + c.x * d.dx;
c.y = d.y + c.y * d.dy;
c.dx *= d.dx;
c.dy *= d.dy;
c.parent = d;
layout(c);
});
}
};

var display = function (d) {

var transition = function (d) {
if (transitioning || !d) { return; }
transitioning = true;

var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);

// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);

// Enable anti-aliasing during the transition.
chartContainer.style("shape-rendering", null);

// Draw child nodes on top of parent nodes.
chartContainer.selectAll(".depth").sort(function (a, b) { return a.depth - b.depth; });

// Fade-in entering text.
g2.selectAll("text").style("fill-opacity", 0);

// Transition to the new view.
t1.selectAll(".ptext").call(text).style("fill-opacity", 0);
t1.selectAll(".ctext").call(text2).style("fill-opacity", 0);
t2.selectAll(".ptext").call(text).style("fill-opacity", 1);
t2.selectAll(".ctext").call(text2).style("fill-opacity", 1);
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);

// Remove the old node when the transition is finished.
t1.remove().each("end", function () {
chartContainer.style("shape-rendering", "crispEdges");
transitioning = false;
});
};

grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(name(d));

var g1 = chartContainer.append("g")
.datum(d)
.attr("class", "depth");

var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");

g.filter(function (d) { return d._children; })
.classed("children", true)
.on("click", transition);

var children = g.selectAll(".child")
.data(function (d) { return d._children || [d]; })
.enter().append("g");

children.append("rect")
.attr("class", "child")
.call(rect)
.append("title")
.text(function (d) { return d.name + " (" + formatNumber(d.value) + ")"; });

children.append("text")
.attr("class", "ctext")
.text(function (d) { return d.name; })
.call(text2);

g.append("rect")
.attr("class", "parent")
.call(rect);

var t = g.append("text")
.attr("class", "ptext")
.attr("dy", ".75em");

t.append("tspan")
.text(function (d) { return d.name; });
t.append("tspan")
.attr("dy", "1.0em")
.text(function (d) { return formatNumber(d.value); });
t.call(text);

g.selectAll("rect")
.style("fill", function (d) { return px.color.category21(d.name); });

return g;
};

var text = function (selection) {
selection.selectAll("tspan")
.attr("x", function (d) { return x(d.x) + 6; });
selection.attr("x", function (d) { return x(d.x) + 6; })
.attr("y", function (d) { return y(d.y) + 6; })
.style("opacity", function (d) { return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0; });
};

var text2 = function (selection) {
selection.attr("x", function (d) { return x(d.x + d.dx) - this.getComputedTextLength() - 6; })
.attr("y", function (d) { return y(d.y + d.dy) - 6; })
.style("opacity", function (d) { return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0; });
};

var rect = function (selection) {
selection.attr("x", function (d) { return x(d.x); })
.attr("y", function (d) { return y(d.y); })
.attr("width", function (d) { return x(d.x + d.dx) - x(d.x); })
.attr("height", function (d) { return y(d.y + d.dy) - y(d.y); });
};

var name = function (d) {
return d.parent
? name(d.parent) + " / " + d.name + " (" + formatNumber(d.value) + ")"
: d.name + " (" + formatNumber(d.value) + ")";
};

container.datum(data).selectAll(".node")
.data(treemap.nodes)
.enter().append("div")
.attr("class", "node")
.call(position)
.style("background", function (d) {
return d.children ? px.color.category21(d.name) : null;
})
.style("color", function (d) {
// detect if our background is dark and we need a
// light text color or vice-versa
var bg = d.parent ? px.color.category21(d.parent.name) : null;
if (bg) {
return d3.hsl(bg).l < 0.35 ? '#d3d3d3' : '#111111';
}
})
.text(function (d) { return d.children ? null : d.name; });

if (includeTitle) {
// title to help with multiple metrics (if any)
header.append("span")
.style("font-size", "18px")
.style("font-weight", "bold")
.text(data.name);
}
initialize(data);
accumulate(data);
layout(data);
display(data);

};

Expand All @@ -83,9 +237,8 @@ function treemap(slice) {
var width = slice.width();
// facet muliple metrics (no sense in combining)
var height = slice.height() / json.data.length;
var includeTitles = json.data.length > 1;
for (var i = 0, l = json.data.length; i < l; i ++) {
_draw(json.data[i], width, height, includeTitles);
_draw(json.data[i], width, height, json.form_data);
}

slice.done(json);
Expand Down
23 changes: 22 additions & 1 deletion caravel/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

from collections import OrderedDict
from copy import copy
import math

from wtforms import (
Form, SelectMultipleField, SelectField, TextField, TextAreaField,
BooleanField, IntegerField, HiddenField)
BooleanField, IntegerField, HiddenField, DecimalField)
from wtforms import validators, widgets

from caravel import app
Expand Down Expand Up @@ -336,6 +337,26 @@ def __init__(self, viz):
'9/91 percentiles',
])
),
'treemap_ratio': DecimalField(
'Ratio',
default=0.5 * (1 + math.sqrt(5)), # d3 default, golden ratio
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to see the golden ratio here :)

description='Target aspect ratio for treemap tiles.',
),
'number_format': FreeFormSelectField(
'Number format',
default='.3s',
choices=[
('.3s', '".3s" | 12.3k'),
('.3%', '".3%" | 1234543.210%'),
('.4r', '".4r" | 12350'),
('.3f', '".3f" | 12345.432'),
('+,', '"+," | +12,345.4321'),
('$,.2f', '"$,.2f" | $12,345.43'),
],
description="D3 format syntax for numbers "
"https://github.com/mbostock/\n"
"d3/wiki/Formatting"),

'row_limit':
FreeFormSelectField(
'Row limit',
Expand Down
Loading