Skip to content

Instantly share code, notes, and snippets.

@jgbos
Last active August 29, 2015 13:58
Show Gist options
  • Save jgbos/10439682 to your computer and use it in GitHub Desktop.
Save jgbos/10439682 to your computer and use it in GitHub Desktop.
D3 Reusable Charts with Prototypes

Uses prototypes instead of closures to execute the D3 enter/update/exit pattern. This example shows the axes, grids, and plot elements being updated on each call so that the enter, update, and exit functions are executed. The code is based off of the javascript code for mpld3. You can find the code base for this visualization here.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MPLD3</title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script type="text/javascript" src="mpld3_defaults.js"></script>
<script src="viz.js"></script>
</head>
<body>
<div id="test" ></div>
<script type="text/javascript">
function get_data(N){
return viz.sim.linear([{
mu: [1, 1],
std: [0.5, 0.5]
}, {
mu: [2, 2],
std: [0.5, 0.5]
}], N);
}
var data = get_data(100);
draw_test(data);
var inputScale = d3.scale.quantize()
.domain([0, 1])
.range([5, 20, 60, 200, 100, 65, 30, 2]);
!function loop(){
d3.transition()
.ease("linear")
.duration(10000)
.tween("transform", function (){
return function (t){
draw_test(get_data(inputScale(t)));
}
})
}();
function draw_test(data) {
var chart = viz.mpld3.scatter();
chart.x(function(d) {
return d.features[0]
});
chart.y(function(d) {
return d.features[1]
});
chart.c(function(d) {
return d.label;
});
var fig = new viz.plot.figure(chart(data));
fig.draw("#test");
}
</script>
</body>
</html>
var default_figure = {
width: 400,
height: 400,
data: {},
axes: [],
plugins: [{
type: "reset"
}, {
type: "zoom"
}, {
type: "boxzoom"
}]
};
var default_axes = {
xlim: [0, 1],
ylim: [0, 1],
bbox: [0.1, 0.1, 0.8, 0.8],
axesbg: "#EAEAF2",
axesbgalpha: 1.0,
gridOn: true,
xdomain: null,
ydomain: null,
xscale: "linear",
yscale: "linear",
zoomable: true,
axes: [{
position: "left"
}, {
position: "bottom"
}],
grids: [],
xgridprops: {},
ygridprops: {},
lines: [],
paths: [],
markers: [],
texts: [],
collections: [],
sharex: [],
sharey: [],
images: []
};
var default_grid = {
xy: "x",
color: "#FFFFFF",
dasharray: "0, 0",
alpha: "1.0",
nticks: 10,
gridOn: true,
tickvalues: null,
zorder: 0
};
var default_line = {
data: null,
xindex: 0,
yindex: 1,
coordinates: "data",
color: "salmon",
linewidth: 2,
dasharray: "10,0",
alpha: 1.0,
zorder: 2
};
var default_path = {
data: null,
xindex: 0,
yindex: 1,
coordinates: "data",
facecolor: "green",
edgecolor: "black",
edgewidth: 1,
dasharray: "10,0",
pathcodes: null,
offset: null,
offsetcoordinates: "data",
alpha: 1.0,
zorder: 1
}
var default_collection = {
data: null,
paths: null,
offsets: null,
xindex: 0,
yindex: 1,
pathtransforms: [],
pathcoordinates: "display",
offsetcoordinates: "data",
offsetorder: "before",
edgecolors: ["#000000"],
edgewidths: [1.0],
facecolors: ["#0000FF"],
alphas: [1.0],
zorder: 2
}
var default_markers = {
data: null,
xindex: 0,
yindex: 1,
coordinates: "data",
facecolor: "salmon",
edgecolor: null,
edgewidth: 1,
alpha: 1.0,
markersize: 2,
markername: "circle",
markerpath: null,
zorder: 3
};
!function() {
var viz = {};
viz.sim = new viz_Sim();
function viz_Sim() {}
viz_Sim.prototype.linear = function(x, N) {
var randn = d3.random.normal();
var X = [];
x.forEach(function(d, i) {
for (var j = 0; j < N; j++) {
X.push({
features: [ d.mu[0] + randn() * d.std[0], d.mu[1] + randn() * d.std[1] ],
label: i
});
}
});
return X;
};
viz_Sim.prototype.nonlinear = function(x, N) {
return null;
};
viz.mpld3 = {};
viz.mpld3.props = {};
viz.mpld3.scatter = viz_mpld3_scatter;
function viz_mpld3_scatter() {
var xlim, ylim;
var color = d3.scale.category10();
var xValue = function(d) {
return d.x;
};
var yValue = function(d) {
return d.y;
};
var cValue = function(d) {
return d.id;
};
function chart(data) {
data = data.map(function(d, i) {
return [ xValue.call(data, d, i), yValue.call(data, d, i), cValue.call(data, d, i) ];
});
if (!xlim) {
xlim = d3.extent(data, function(d) {
return d[0];
});
}
if (!ylim) {
ylim = d3.extent(data, function(d) {
return d[1];
});
}
var props = viz_copy(default_figure);
var groupByLabel = d3.nest().key(function(d) {
return d[2];
}).entries(data);
props.data = {};
groupByLabel.forEach(function(d, i) {
props.data["data" + i] = d.values.map(function(d) {
return [ d[0], d[1] ];
});
});
var props_axes = viz_copy(default_axes);
props_axes.xlim = xlim;
props_axes.ylim = ylim;
groupByLabel.forEach(function(d, i) {
var props_data = viz_copy(default_markers);
props_data.data = "data" + i;
props_data.facecolor = color(i);
props_axes.markers.push(props_data);
});
var xprops_grid = viz_copy(default_grid);
xprops_grid.xy = "x";
props_axes.axes[1].grid = xprops_grid;
var yprops_grid = viz_copy(default_grid);
yprops_grid.xy = "y";
props_axes.axes[0].grid = yprops_grid;
props.axes.push(props_axes);
return props;
}
function X(d) {
return xscale(d[0]);
}
function Y(d) {
return yscale(d[1]);
}
function C(d) {
return color(d[2]);
}
chart.x = function(_) {
if (!arguments.length) return xValue;
xValue = _;
return chart;
};
chart.y = function(_) {
if (!arguments.length) return yValue;
yValue = _;
return chart;
};
chart.c = function(_) {
if (!arguments.length) return cValue;
cValue = _;
return chart;
};
chart.xlim = function(_) {
if (!arguments.length) return xlim;
xlim = _;
return chart;
};
chart.ylim = function(_) {
if (!arguments.length) return ylim;
ylim = _;
return chart;
};
return chart;
}
function viz_copy(obj) {
if (null == obj || "object" != typeof obj) return obj;
if (obj instanceof Date) {
var copy = new Date();
copy.setTime(obj.getTime());
return copy;
}
if (obj instanceof Array) {
var copy = [];
for (var i = 0, len = obj.length; i < len; i++) {
copy[i] = viz_copy(obj[i]);
}
return copy;
}
if (obj instanceof Object) {
var copy = {};
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) copy[attr] = viz_copy(obj[attr]);
}
return copy;
}
throw new Error("Unable to copy obj! Its type isn't supported.");
}
viz.plot = {};
function insert_css(selector, attributes) {
var head = document.head || document.getElementsByTagName("head")[0];
var style = document.createElement("style");
var css = selector + " {";
for (var prop in attributes) {
css += prop + ":" + attributes[prop] + "; ";
}
css += "}";
style.type = "text/css";
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
head.appendChild(style);
}
viz.plot.figure = viz_Figure;
function viz_Figure(props) {
this.width = props.width;
this.height = props.height;
this.canvas = [];
this.props = props;
this.axes = [];
for (var i = 0; i < props.axes.length; i++) this.axes.push(new viz_Axes(this, props.axes[i]));
}
viz_Figure.prototype.draw = function(figid) {
this.canvas = d3.select(figid).selectAll("svg").data([ this.props ]);
for (var i = 0; i < this.axes.length; i++) this.axes[i].draw(this.canvas);
};
viz.plot.axes = viz_Axes;
function viz_Axes(parent, props) {
this.parent = parent;
this.canvas = parent.canvas;
this.props = props;
var bbox = props.bbox;
this.elements = [];
this.position = [ bbox[0] * parent.width, (1 - bbox[1] - bbox[3]) * parent.height ];
this.width = bbox[2] * parent.width;
this.height = bbox[3] * parent.height;
this.transform = "translate(" + this.position + ")";
this.cssclass = "viz-baseaxes";
this.xscale = d3.scale.linear();
this.yscale = d3.scale.linear();
this.xscale.range([ 0, this.width ]);
this.yscale.range([ this.height, 0 ]);
this.xscale.domain(this.props.xlim);
this.yscale.domain(this.props.ylim);
var axes = this.props.axes;
for (var i = 0; i < axes.length; i++) {
var axis = new viz_Axis(this, props.axes[i]);
this.elements.push(axis);
if (this.props.gridOn || axis.props.grid.gridOn) {
this.elements.push(axis.getGrid());
}
}
var markers = this.props.markers;
for (var i = 0; i < markers.length; i++) {
this.elements.push(new viz_Markers(this, markers[i]));
}
}
viz_Axes.prototype.draw = function(canvas) {
var gEnter = canvas.enter().append("svg").append("g").attr("transform", this.transform).attr("class", this.cssclass);
canvas.attr("width", this.parent.width).attr("height", this.parent.height);
gEnter.append("g").attr("class", "viz-background");
gEnter.append("g").attr("class", "viz-x-axis");
gEnter.append("g").attr("class", "viz-y-axis");
gEnter.append("g").attr("class", "viz-border");
gEnter.append("g").attr("class", "viz-zoom");
gEnter.append("g").attr("class", "viz-figure");
gEnter.append("defs").append("clipPath").attr("id", "clip").append("rect").attr("width", this.width).attr("height", this.height);
gEnter.select("g.viz-figure").attr("clip-path", "url(#clip)");
gEnter.select("g.viz-background").append("rect").attr("width", this.width).attr("height", this.height).attr("class", "mpld3-axesbg").style("fill", this.props.axesbg).style("fill-opacity", this.props.axesbgalpha);
gEnter.select("g.viz-border").append("rect").attr("width", this.width).attr("height", this.height).attr("class", "mpld3-axesborder").style("fill", "#fff").style("fill-opacity", 0).style("stroke", "#000").style("stroke-width", "2pt");
for (var i = 0; i < this.elements.length; i++) this.elements[i].draw(canvas);
};
viz.plot.axis = viz_Axis;
function viz_Axis(parent, props) {
this.parent = parent;
this.props = props;
var trans = {
bottom: [ 0, parent.height ],
top: [ 0, 0 ],
left: [ 0, 0 ],
right: [ parent.width, 0 ]
};
var xy = {
bottom: "x",
top: "x",
left: "y",
right: "y"
};
this.transform = "translate(" + trans[this.props.position] + ")";
this.props.xy = xy[this.props.position];
this.cssclass = "viz-" + this.props.xy + "-axis";
this.scale = parent[this.props.xy + "scale"];
insert_css("." + this.cssclass + " line, " + " ." + this.cssclass + " path", {
"shape-rendering": "crispEdges",
stroke: this.props.axiscolor,
fill: "none"
});
insert_css("." + this.cssclass + " text", {
"font-family": "sans-serif",
"font-size": this.props.fontsize,
fill: this.props.fontcolor,
stroke: "none"
});
}
viz_Axis.prototype.draw = function(canvas) {
this.axis = d3.svg.axis().scale(this.scale).orient(this.props.position).ticks(this.props.nticks).tickValues(this.props.tickvalues).tickFormat(this.props.tickformat);
this.elem = canvas.select("g." + this.cssclass).attr("transform", this.transform).call(this.axis);
};
viz_Axis.prototype.getGrid = function() {
var gridprop = {
nticks: this.props.nticks,
zorder: this.props.zorder,
tickvalues: this.props.tickvalues,
xy: this.props.xy
};
if (this.props.grid) {
for (var key in this.props.grid) {
gridprop[key] = this.props.grid[key];
}
}
return new viz_Grid(this.parent, gridprop);
};
viz.plot.grid = viz_Grid;
function viz_Grid(parent, props) {
this.parent = parent;
this.props = props;
this.cssclass = "viz-" + this.props.xy + "-grid";
if (this.props.xy == "x") {
this.transform = "translate(0,0)";
this.position = "bottom";
this.scale = parent.xscale;
this.tickSize = -parent.height;
} else if (this.props.xy == "y") {
this.transform = "translate(0,0)";
this.position = "left";
this.scale = parent.yscale;
this.tickSize = -parent.width;
} else {
throw "unrecognized grid xy specifier: should be 'x' or 'y'";
}
insert_css("." + this.cssclass + " .tick", {
stroke: this.props.color,
"stroke-dasharray": this.props.dasharray,
"stroke-opacity": this.props.alpha
});
insert_css("." + this.cssclass + " path", {
"stroke-width": 0
});
}
viz_Grid.prototype.draw = function(canvas) {
this.grid = d3.svg.axis().scale(this.scale).orient(this.position).ticks(this.props.nticks).tickValues(this.props.tickvalues).tickSize(this.tickSize, 0, 0).tickFormat("");
this.elem = canvas.select("g.viz-" + this.props.xy + "-axis").append("g").attr("class", this.cssclass).attr("transform", this.transform).call(this.grid);
};
viz.plot.markers = viz_Markers;
function viz_Markers(parent, props) {
this.props = props;
this.parent = parent;
}
viz_Markers.prototype.draw = function(canvas) {
var gs = canvas.select("g.viz-figure");
var data = this.props.data;
var parent = this.parent;
var circle = gs.selectAll("circle." + data).data(function(d) {
return d.data[data];
});
circle.enter().append("svg:circle").attr("class", data).style("fill", this.props.facecolor).attr("cx", function(d) {
return parent.xscale(d[0]);
}).attr("cy", function(d) {
return parent.yscale(d[1]);
}).attr("r", this.props.markersize);
circle.transition().style("fill", this.props.facecolor).attr("cx", function(d) {
return parent.xscale(d[0]);
}).attr("cy", function(d) {
return parent.yscale(d[1]);
}).attr("r", this.props.markersize);
circle.exit().remove();
};
if (typeof module === "object" && module.exports) {
module.exports = viz;
} else {
this.viz = viz;
}
this.viz = viz;
}();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment