Skip to content

Instantly share code, notes, and snippets.

@pykerl

pykerl/README.md

Last active Jun 7, 2016
Embed
What would you like to do?
Creating static data visualizations with D3 & Node

###Creating static data visualizations with D3.js and Node.js###

See the accompanying blog post for more details on how to create static data viz using D3.js and Node.js

Re-creates Mike Bostock's area choropleth example for Node.js. Allows for output/creation of a non-interactive SVG via the command line.

Requires Node.js and npm. You will also need D3.js for Node.js, installed via,

npm install d3

Command line usage (outputs area_choropleth.svg):

node area_choropleth.js > area_choropleth.svg

Example output (png converted from svg)

alt text

This example recreates a visualization using the following data and JS packages:

  • us.json - US county data (created by Mike Bostock)
  • topojson client API - converts Topojson to Geojson
  • D3.js - JavaScript library for manipulating documents based on data
//require modules
var d3 = require("d3");
var fs = require("fs");
var vm = require('vm');
//import topojson.js client side API
var includeInThisContext = function(path) {
var code = fs.readFileSync(path);
vm.runInThisContext(code, path);
}.bind(this);
includeInThisContext("topojson.js");
//SVG dimensions
var width = 960,
height = 500;
//scale for county-population-based fill
var fill = d3.scale.log()
.domain([10, 500])
.range(["brown", "steelblue"]);
var path = d3.geo.path();
var svg = d3.select("body").append("svg")
.attr('xmlns', 'http://www.w3.org/2000/svg')
.attr("width", width)
.attr("height", height);
//parse the US county population data JSON file
var us = JSON.parse(fs.readFileSync("us.json", 'utf8'));
//enter the counties
svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("d", path)
.style("fill", function(d) { return fill(path.area(d)); });
//outline the states
svg.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a.id !== b.id; }))
.attr("class", "states")
.attr("d", path);
//add css stylesheet
var svg_style = svg.append("defs")
.append('style')
.attr('type','text/css');
//text of the CSS stylesheet below -- note the multi-line JS requires
//escape characters "\" at the end of each line
var css_text = "<![CDATA[ \
.states { \
fill: none; \
stroke: #fff; \
stroke-linejoin: round; \
} \
]]> ";
svg_style.text(css_text);
//print to stdout
console.log(d3.select('body').html());
!function() {
var topojson = {
version: "1.6.7",
mesh: function(topology) { return object(topology, meshArcs.apply(this, arguments)); },
meshArcs: meshArcs,
merge: function(topology) { return object(topology, mergeArcs.apply(this, arguments)); },
mergeArcs: mergeArcs,
feature: featureOrCollection,
neighbors: neighbors,
presimplify: presimplify
};
function stitchArcs(topology, arcs) {
var stitchedArcs = {},
fragmentByStart = {},
fragmentByEnd = {},
fragments = [],
emptyIndex = -1;
// Stitch empty arcs first, since they may be subsumed by other arcs.
arcs.forEach(function(i, j) {
var arc = topology.arcs[i < 0 ? ~i : i], t;
if (arc.length < 3 && !arc[1][0] && !arc[1][1]) {
t = arcs[++emptyIndex], arcs[emptyIndex] = i, arcs[j] = t;
}
});
arcs.forEach(function(i) {
var e = ends(i),
start = e[0],
end = e[1],
f, g;
if (f = fragmentByEnd[start]) {
delete fragmentByEnd[f.end];
f.push(i);
f.end = end;
if (g = fragmentByStart[end]) {
delete fragmentByStart[g.start];
var fg = g === f ? f : f.concat(g);
fragmentByStart[fg.start = f.start] = fragmentByEnd[fg.end = g.end] = fg;
} else {
fragmentByStart[f.start] = fragmentByEnd[f.end] = f;
}
} else if (f = fragmentByStart[end]) {
delete fragmentByStart[f.start];
f.unshift(i);
f.start = start;
if (g = fragmentByEnd[start]) {
delete fragmentByEnd[g.end];
var gf = g === f ? f : g.concat(f);
fragmentByStart[gf.start = g.start] = fragmentByEnd[gf.end = f.end] = gf;
} else {
fragmentByStart[f.start] = fragmentByEnd[f.end] = f;
}
} else {
f = [i];
fragmentByStart[f.start = start] = fragmentByEnd[f.end = end] = f;
}
});
function ends(i) {
var arc = topology.arcs[i < 0 ? ~i : i], p0 = arc[0], p1;
if (topology.transform) p1 = [0, 0], arc.forEach(function(dp) { p1[0] += dp[0], p1[1] += dp[1]; });
else p1 = arc[arc.length - 1];
return i < 0 ? [p1, p0] : [p0, p1];
}
function flush(fragmentByEnd, fragmentByStart) {
for (var k in fragmentByEnd) {
var f = fragmentByEnd[k];
delete fragmentByStart[f.start];
delete f.start;
delete f.end;
f.forEach(function(i) { stitchedArcs[i < 0 ? ~i : i] = 1; });
fragments.push(f);
}
}
flush(fragmentByEnd, fragmentByStart);
flush(fragmentByStart, fragmentByEnd);
arcs.forEach(function(i) { if (!stitchedArcs[i < 0 ? ~i : i]) fragments.push([i]); });
return fragments;
}
function meshArcs(topology, o, filter) {
var arcs = [];
if (arguments.length > 1) {
var geomsByArc = [],
geom;
function arc(i) {
var j = i < 0 ? ~i : i;
(geomsByArc[j] || (geomsByArc[j] = [])).push({i: i, g: geom});
}
function line(arcs) {
arcs.forEach(arc);
}
function polygon(arcs) {
arcs.forEach(line);
}
function geometry(o) {
if (o.type === "GeometryCollection") o.geometries.forEach(geometry);
else if (o.type in geometryType) geom = o, geometryType[o.type](o.arcs);
}
var geometryType = {
LineString: line,
MultiLineString: polygon,
Polygon: polygon,
MultiPolygon: function(arcs) { arcs.forEach(polygon); }
};
geometry(o);
geomsByArc.forEach(arguments.length < 3
? function(geoms) { arcs.push(geoms[0].i); }
: function(geoms) { if (filter(geoms[0].g, geoms[geoms.length - 1].g)) arcs.push(geoms[0].i); });
} else {
for (var i = 0, n = topology.arcs.length; i < n; ++i) arcs.push(i);
}
return {type: "MultiLineString", arcs: stitchArcs(topology, arcs)};
}
function mergeArcs(topology, objects) {
var polygonsByArc = {},
polygons = [],
components = [];
objects.forEach(function(o) {
if (o.type === "Polygon") register(o.arcs);
else if (o.type === "MultiPolygon") o.arcs.forEach(register);
});
function register(polygon) {
polygon.forEach(function(ring) {
ring.forEach(function(arc) {
(polygonsByArc[arc = arc < 0 ? ~arc : arc] || (polygonsByArc[arc] = [])).push(polygon);
});
});
polygons.push(polygon);
}
function exterior(ring) {
return cartesianRingArea(object(topology, {type: "Polygon", arcs: [ring]}).coordinates[0]) > 0; // TODO allow spherical?
}
polygons.forEach(function(polygon) {
if (!polygon._) {
var component = [],
neighbors = [polygon];
polygon._ = 1;
components.push(component);
while (polygon = neighbors.pop()) {
component.push(polygon);
polygon.forEach(function(ring) {
ring.forEach(function(arc) {
polygonsByArc[arc < 0 ? ~arc : arc].forEach(function(polygon) {
if (!polygon._) {
polygon._ = 1;
neighbors.push(polygon);
}
});
});
});
}
}
});
polygons.forEach(function(polygon) {
delete polygon._;
});
return {
type: "MultiPolygon",
arcs: components.map(function(polygons) {
var arcs = [];
// Extract the exterior (unique) arcs.
polygons.forEach(function(polygon) {
polygon.forEach(function(ring) {
ring.forEach(function(arc) {
if (polygonsByArc[arc < 0 ? ~arc : arc].length < 2) {
arcs.push(arc);
}
});
});
});
// Stitch the arcs into one or more rings.
arcs = stitchArcs(topology, arcs);
// If more than one ring is returned,
// at most one of these rings can be the exterior;
// this exterior ring has the same winding order
// as any exterior ring in the original polygons.
if ((n = arcs.length) > 1) {
var sgn = exterior(polygons[0][0]);
for (var i = 0, t; i < n; ++i) {
if (sgn === exterior(arcs[i])) {
t = arcs[0], arcs[0] = arcs[i], arcs[i] = t;
break;
}
}
}
return arcs;
})
};
}
function featureOrCollection(topology, o) {
return o.type === "GeometryCollection" ? {
type: "FeatureCollection",
features: o.geometries.map(function(o) { return feature(topology, o); })
} : feature(topology, o);
}
function feature(topology, o) {
var f = {
type: "Feature",
id: o.id,
properties: o.properties || {},
geometry: object(topology, o)
};
if (o.id == null) delete f.id;
return f;
}
function object(topology, o) {
var absolute = transformAbsolute(topology.transform),
arcs = topology.arcs;
function arc(i, points) {
if (points.length) points.pop();
for (var a = arcs[i < 0 ? ~i : i], k = 0, n = a.length, p; k < n; ++k) {
points.push(p = a[k].slice());
absolute(p, k);
}
if (i < 0) reverse(points, n);
}
function point(p) {
p = p.slice();
absolute(p, 0);
return p;
}
function line(arcs) {
var points = [];
for (var i = 0, n = arcs.length; i < n; ++i) arc(arcs[i], points);
if (points.length < 2) points.push(points[0].slice());
return points;
}
function ring(arcs) {
var points = line(arcs);
while (points.length < 4) points.push(points[0].slice());
return points;
}
function polygon(arcs) {
return arcs.map(ring);
}
function geometry(o) {
var t = o.type;
return t === "GeometryCollection" ? {type: t, geometries: o.geometries.map(geometry)}
: t in geometryType ? {type: t, coordinates: geometryType[t](o)}
: null;
}
var geometryType = {
Point: function(o) { return point(o.coordinates); },
MultiPoint: function(o) { return o.coordinates.map(point); },
LineString: function(o) { return line(o.arcs); },
MultiLineString: function(o) { return o.arcs.map(line); },
Polygon: function(o) { return polygon(o.arcs); },
MultiPolygon: function(o) { return o.arcs.map(polygon); }
};
return geometry(o);
}
function reverse(array, n) {
var t, j = array.length, i = j - n; while (i < --j) t = array[i], array[i++] = array[j], array[j] = t;
}
function bisect(a, x) {
var lo = 0, hi = a.length;
while (lo < hi) {
var mid = lo + hi >>> 1;
if (a[mid] < x) lo = mid + 1;
else hi = mid;
}
return lo;
}
function neighbors(objects) {
var indexesByArc = {}, // arc index -> array of object indexes
neighbors = objects.map(function() { return []; });
function line(arcs, i) {
arcs.forEach(function(a) {
if (a < 0) a = ~a;
var o = indexesByArc[a];
if (o) o.push(i);
else indexesByArc[a] = [i];
});
}
function polygon(arcs, i) {
arcs.forEach(function(arc) { line(arc, i); });
}
function geometry(o, i) {
if (o.type === "GeometryCollection") o.geometries.forEach(function(o) { geometry(o, i); });
else if (o.type in geometryType) geometryType[o.type](o.arcs, i);
}
var geometryType = {
LineString: line,
MultiLineString: polygon,
Polygon: polygon,
MultiPolygon: function(arcs, i) { arcs.forEach(function(arc) { polygon(arc, i); }); }
};
objects.forEach(geometry);
for (var i in indexesByArc) {
for (var indexes = indexesByArc[i], m = indexes.length, j = 0; j < m; ++j) {
for (var k = j + 1; k < m; ++k) {
var ij = indexes[j], ik = indexes[k], n;
if ((n = neighbors[ij])[i = bisect(n, ik)] !== ik) n.splice(i, 0, ik);
if ((n = neighbors[ik])[i = bisect(n, ij)] !== ij) n.splice(i, 0, ij);
}
}
}
return neighbors;
}
function presimplify(topology, triangleArea) {
var absolute = transformAbsolute(topology.transform),
relative = transformRelative(topology.transform),
heap = minHeap(compareArea),
maxArea = 0,
triangle;
if (!triangleArea) triangleArea = cartesianTriangleArea;
topology.arcs.forEach(function(arc) {
var triangles = [];
arc.forEach(absolute);
for (var i = 1, n = arc.length - 1; i < n; ++i) {
triangle = arc.slice(i - 1, i + 2);
triangle[1][2] = triangleArea(triangle);
triangles.push(triangle);
heap.push(triangle);
}
// Always keep the arc endpoints!
arc[0][2] = arc[n][2] = Infinity;
for (var i = 0, n = triangles.length; i < n; ++i) {
triangle = triangles[i];
triangle.previous = triangles[i - 1];
triangle.next = triangles[i + 1];
}
});
while (triangle = heap.pop()) {
var previous = triangle.previous,
next = triangle.next;
// If the area of the current point is less than that of the previous point
// to be eliminated, use the latter's area instead. This ensures that the
// current point cannot be eliminated without eliminating previously-
// eliminated points.
if (triangle[1][2] < maxArea) triangle[1][2] = maxArea;
else maxArea = triangle[1][2];
if (previous) {
previous.next = next;
previous[2] = triangle[2];
update(previous);
}
if (next) {
next.previous = previous;
next[0] = triangle[0];
update(next);
}
}
topology.arcs.forEach(function(arc) {
arc.forEach(relative);
});
function update(triangle) {
heap.remove(triangle);
triangle[1][2] = triangleArea(triangle);
heap.push(triangle);
}
return topology;
};
function cartesianRingArea(ring) {
var i = -1,
n = ring.length,
a,
b = ring[n - 1],
area = 0;
while (++i < n) {
a = b;
b = ring[i];
area += a[0] * b[1] - a[1] * b[0];
}
return area * .5;
}
function cartesianTriangleArea(triangle) {
var a = triangle[0], b = triangle[1], c = triangle[2];
return Math.abs((a[0] - c[0]) * (b[1] - a[1]) - (a[0] - b[0]) * (c[1] - a[1]));
}
function compareArea(a, b) {
return a[1][2] - b[1][2];
}
function minHeap(compare) {
var heap = {},
array = [];
heap.push = function() {
for (var i = 0, n = arguments.length; i < n; ++i) {
var object = arguments[i];
up(object.index = array.push(object) - 1);
}
return array.length;
};
heap.pop = function() {
var removed = array[0],
object = array.pop();
if (array.length) {
array[object.index = 0] = object;
down(0);
}
return removed;
};
heap.remove = function(removed) {
var i = removed.index,
object = array.pop();
if (i !== array.length) {
array[object.index = i] = object;
(compare(object, removed) < 0 ? up : down)(i);
}
return i;
};
function up(i) {
var object = array[i];
while (i > 0) {
var up = ((i + 1) >> 1) - 1,
parent = array[up];
if (compare(object, parent) >= 0) break;
array[parent.index = i] = parent;
array[object.index = i = up] = object;
}
}
function down(i) {
var object = array[i];
while (true) {
var right = (i + 1) << 1,
left = right - 1,
down = i,
child = array[down];
if (left < array.length && compare(array[left], child) < 0) child = array[down = left];
if (right < array.length && compare(array[right], child) < 0) child = array[down = right];
if (down === i) break;
array[child.index = i] = child;
array[object.index = i = down] = object;
}
}
return heap;
}
function transformAbsolute(transform) {
if (!transform) return noop;
var x0,
y0,
kx = transform.scale[0],
ky = transform.scale[1],
dx = transform.translate[0],
dy = transform.translate[1];
return function(point, i) {
if (!i) x0 = y0 = 0;
point[0] = (x0 += point[0]) * kx + dx;
point[1] = (y0 += point[1]) * ky + dy;
};
}
function transformRelative(transform) {
if (!transform) return noop;
var x0,
y0,
kx = transform.scale[0],
ky = transform.scale[1],
dx = transform.translate[0],
dy = transform.translate[1];
return function(point, i) {
if (!i) x0 = y0 = 0;
var x1 = (point[0] - dx) / kx | 0,
y1 = (point[1] - dy) / ky | 0;
point[0] = x1 - x0;
point[1] = y1 - y0;
x0 = x1;
y0 = y1;
};
}
function noop() {}
if (typeof define === "function" && define.amd) define(topojson);
else if (typeof module === "object" && module.exports) module.exports = topojson;
else this.topojson = topojson;
}();
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@phongle1205

This comment has been minimized.

Copy link

@phongle1205 phongle1205 commented Jun 9, 2015

Hi,

I always got error as bellow:
-> % node area_choropleth.js > area_choropleth.svg
/Users/ple/an/genSVG/node_modules/d3/d3.js:562
return n.querySelector(s);
^
TypeError: Cannot read property 'querySelector' of undefined
at d3_select (/Users/ple/an/genSVG/node_modules/d3/d3.js:562:13)
at Object.d3.select (/Users/ple/an/genSVG/node_modules/d3/d3.js:1043:17)
at Object. (/Users/ple/an/genSVG/area_choropleth.js:24:14)
at Module._compile (module.js:426:26)
at Object.Module._extensions..js (module.js:444:10)
at Module.load (module.js:351:32)
at Function.Module._load (module.js:306:12)
at Function.Module.runMain (module.js:467:10)
at startup (node.js:117:18)
at node.js:946:3

I think we need something as jsdom http://stackoverflow.com/questions/19080929/d3js-how-to-generate-standalone-svg-files-nodejs.

Let me know if I did something wrong.

Thanks,

@zhiboz

This comment has been minimized.

Copy link

@zhiboz zhiboz commented Sep 21, 2015

I encountered the same issue as xcirclex.

@Fil

This comment has been minimized.

Copy link

@Fil Fil commented Jan 21, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment