Skip to content

Instantly share code, notes, and snippets.

@bollwyvl
Last active December 15, 2015 00:59
Show Gist options
  • Save bollwyvl/5176743 to your computer and use it in GitHub Desktop.
Save bollwyvl/5176743 to your computer and use it in GitHub Desktop.
Collapsible Area Plot Matrix

Shows use of .transition() with .transform()

<html>
<head>
<style>
html, body{
overflow: hidden;
padding: 0;
margin: 0;
}
path.line{
stroke-width: 2;
fill: transparent;
}
path.area{
fill-opacity: 0.2;
}
#controls{
position:fixed;
}
</style>
</head>
<body>
<div id="controls">
<button id="collapse_cols">Collapse Columns</button>
<button id="collapse_rows">Collapse Rows</button>
<button id="makedata">New Data</button>
</div>
<!-- Firefox doesn't assume the SVG is as big as its contents -->
<svg width="100%" height="100%"></svg>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" charset="utf-8" src="script.js"></script>
</body>
</html>
// not necessary for a gist, but usually good practice
;(function(d3){
"use strict";
// some housecleaning to make sure we're not willing things into existence
var window = this,
document = window.document,
documentElement = document.documentElement;
// the root SVG object
var svg = d3.select("svg"),
// the width/height of the document
width, height,
// a global for whether rows and/or columns are collapsed
collapsed = { cols: 0, rows: 0 },
// x/y scales: (r)ow (c)ell. pretty dynamic, see `update_scales`...
y_r = d3.scale.linear(),
x_c = d3.scale.linear(),
// x/y for path... abusing that domain/range default is [0,1]
y_p = d3.scale.linear(),
x_p = d3.scale.linear(),
// ...and some helpers for path things
x_p_idx = function(datum, idx) { return x_p(idx); },
y_p_datum = function(datum) { return y_p(datum); },
// a scale of colors...
color = d3.scale.category10(),
// ...and a helper to make using it easier
color_by_id = function(datum, idx){ return color(idx); },
// a generator for the SVG path `d` attribute that squares off the bottom
area = d3.svg.area()
.interpolate("basis")
.x(x_p_idx)
.y0(1)
.y1(y_p_datum),
// a generator for the SVG path `d` attribute
line = d3.svg.line()
.interpolate("basis")
.x(x_p_idx)
.y(y_p_datum),
// oh right, we need some data...
data = make_data();
// main render function (handles init and update).
function render(){
// data/screen size may have changed
update_scales();
// an outer SVG `g`... not strictly necessary
var outer = svg.selectAll("g.outer")
.data([1]),
// all these `_init` things are only called when new, un-DOM'd data is
// found
outer_init = outer.enter()
.append("g")
.attr("class", "outer"),
// each of the rows of plots
rows = outer.selectAll("g.row")
.data(data),
row_init = rows.enter()
.append("g").attr("class", "row")
// use of d3 call, as we reuse this same stuff in the transition
// below...
.call(tx_row),
// each of the cells (columns within rows)
cells = rows.selectAll("g.cell")
// a little weird: likely, the sub data would be in an attribute:
// e.g. row.vals
.data(function(row){ return row; }),
cell_init = cells.enter()
.append("g").attr("class", "cell")
.call(tx_cell),
// note we don't `selectAll`, as we just want one path per cell... now
areas = cells.select("path.area"),
area_init = cell_init.append("path")
.attr("class", "area")
.style("fill", color_by_id)
.attr("d", area),
lines = cells.select("path.line"),
line_init = cell_init.append("path")
.attr("class", "line")
.style("stroke", color_by_id)
.attr("d", line)
// thanks ahaarnos: the old effect was kind of artistic, like
// calligraphy, but weird
.attr("vector-effect", "non-scaling-stroke");
// it's possible that the "shape" of the data changed: this removes hanging
// DOM elements
rows.exit().remove();
cells.exit().remove();
// all the animation stuff
areas
.transition()
.ease("back")
// not strictly necessary unless data actually changes
.attr("d", area);
lines
.transition()
.ease("back")
// not strictly necessary unless data actually changes
.attr("d", line);
rows
.transition()
.call(tx_row);
cells
.transition()
.call(tx_cell);
}
// make some fake data. not great.
function make_data(){
var rows = Math.ceil(Math.random() * 10),
cols = Math.ceil(Math.random() * 10),
points = Math.ceil(Math.random() * 100);
// probably a prettier way to do this... gets the point across
return d3.range(rows).map(function(){
return d3.range(cols).map(function(){
return d3.range(points).map(function(){
return Math.random();
});
});
});
}
// change the parts of the scales that are dynamic
function update_scales(){
width = documentElement.clientWidth;
height = documentElement.clientHeight;
y_r.domain([0, data.length])
.range([0, height]);
x_c.domain([0, data[0].length])
.range([0, width]);
x_p.domain([0, data[0][0].length - 1]);
}
// vanity replacement for tons of string parse to create SVG transform attrs
function tx(mode){
return function(x, y){
return mode + "(" + x + "," + y + ") ";
};
}
tx.t = tx("translate");
tx.s = tx("scale");
// transform rows
function tx_row(rows){
rows.attr("transform", function(datum, idx){
if(collapsed.rows){ return tx.t(0, 0) + tx.s(1, height); }
return tx.t(0, y_r(idx)) + tx.s(1, y_r(1));
});
}
// transform the columns (cells) within rows
function tx_cell(cells){
cells.attr("transform", function(datum, idx){
if(collapsed.cols){ return tx.t(0, 0) + tx.s(width, 1); }
return tx.t(x_c(idx), 0) + tx.s(x_c(1), 1);
});
}
// event handlers.
d3.selectAll("button").on("click", function(){
var id = d3.select(this).attr("id").split("_");
switch(id[0]){
case "collapse": collapsed[id[1]] = !collapsed[id[1]]; break;
case "makedata": data = make_data(); break;
}
render();
});
d3.select(window).on("resize", render);
// actually render everything.
render();
}).call(this, d3);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment