├── index.js ├── test ├── templates │ ├── input │ │ ├── browser │ │ │ ├── 1.svg │ │ │ ├── 2.svg │ │ │ ├── 3.svg │ │ │ ├── 4.svg │ │ │ └── 5.svg │ │ └── preprocess.html │ └── output │ │ ├── preprocess │ │ ├── 1.html │ │ ├── 2.html │ │ ├── 3.html │ │ ├── 4.html │ │ └── 5.html │ │ └── browser │ │ └── 4.svg ├── test.js └── browser.js ├── examples ├── app.css ├── choropleth.js ├── axis.js ├── v4.js └── stream.js ├── .gitignore ├── src ├── index.js ├── utils.js ├── preprocess.js ├── browser.js └── d3 │ └── axis.js ├── package.json ├── LICENSE └── README.md /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = require('./src'); 3 | -------------------------------------------------------------------------------- /test/templates/input/browser/1.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/templates/input/preprocess.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /test/templates/output/preprocess/1.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/app.css: -------------------------------------------------------------------------------- 1 | .axis path { 2 | display: none; 3 | } 4 | 5 | .axis line { 6 | stroke-opacity: 0.3; 7 | shape-rendering: crispEdges; 8 | } 9 | 10 | .zoom { 11 | fill: none; 12 | pointer-events: all; 13 | } 14 | 15 | .view { 16 | fill: none; 17 | stroke: #000; 18 | } 19 | -------------------------------------------------------------------------------- /test/templates/input/browser/2.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/templates/output/preprocess/2.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | var browser = require('./browser'); 3 | var preprocess = require('./preprocess'); 4 | 5 | module.exports = function (_d3, options) { 6 | options = options || {}; 7 | 8 | var current = browser(); 9 | 10 | var isPreprocessing = false; 11 | 12 | if (options.mode) { 13 | if (options.mode === 'preprocess') { 14 | current = preprocess(); 15 | isPreprocessing = true; 16 | } 17 | } else if (navigator.userAgent && navigator.userAgent.indexOf('Electron') > -1) { 18 | // Electron 19 | current = preprocess(); 20 | isPreprocessing = true; 21 | } 22 | 23 | current.setD3(_d3); 24 | 25 | return { 26 | start: current.start, 27 | stop: current.stop, 28 | isPreprocessing: isPreprocessing 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 2 | var isV4OrBetter = function (d3) { 3 | return (d3.version && (+d3.version.split('.')[0] >= 4)); 4 | }; 5 | 6 | var d3_selection_creator = function (d3, name) { 7 | return typeof name === 'function' ? name 8 | : (name = d3.ns.qualify(name)).local ? function () { return this.ownerDocument.createElementNS(name.space, name.local); } 9 | : function () { return this.ownerDocument.createElementNS(this.namespaceURI, name); }; 10 | }; 11 | 12 | var mapAppendName = function (d3, name) { 13 | if (isV4OrBetter(d3)) { 14 | return typeof name === 'function' ? name : d3.creator(name); 15 | } 16 | return d3_selection_creator(d3, name); 17 | } 18 | 19 | module.exports = { 20 | isV4OrBetter: isV4OrBetter, 21 | mapAppendName: mapAppendName 22 | }; 23 | -------------------------------------------------------------------------------- /test/templates/output/preprocess/3.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-pre", 3 | "version": "1.3.0", 4 | "description": "Prerender SVGs with D3", 5 | "repository": "fivethirtyeight/d3-pre", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "semistandard src/**/* && mocha test/test.js && mochify --transform brfs test/browser.js", 9 | "example-axis": "budo examples/v4.js -- -t [ browserify-css --autoInject=true ]" 10 | }, 11 | "author": "FiveThirtyEight", 12 | "license": "MIT", 13 | "devDependencies": { 14 | "brfs": "^1.4.3", 15 | "browserify-css": "^0.9.1", 16 | "budo": "^9.0.0", 17 | "d3": "^4.2.2", 18 | "expect.js": "^0.3.1", 19 | "jsdom": "^3.1.2", 20 | "lodash": "^4.2.1", 21 | "mocha": "^2.4.5", 22 | "mochify": "^2.16.0", 23 | "semistandard": "^7.0.4" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 ESPN Internet Ventures 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /examples/choropleth.js: -------------------------------------------------------------------------------- 1 | var d3 = require('d3'); 2 | var queue = require('d3-queue').queue; 3 | var topojson = require('topojson'); 4 | 5 | var d3 = require('d3'); 6 | // Require the library and give it a reference to d3 7 | var Prerender = require('d3-pre'); 8 | var prerender = Prerender(d3); 9 | 10 | prerender.start(); 11 | 12 | 13 | var width = 960, 14 | height = 600; 15 | 16 | var rateById = d3.map(); 17 | 18 | var quantize = d3.scale.quantize() 19 | .domain([0, .15]) 20 | .range(d3.range(9).map(function(i) { return "q" + i + "-9"; })); 21 | 22 | var projection = d3.geo.albersUsa() 23 | .scale(1280) 24 | .translate([width / 2, height / 2]); 25 | 26 | var path = d3.geo.path() 27 | .projection(projection); 28 | 29 | var svg = d3.select("#interactive").append("svg") 30 | .attr("viewBox", '0 0 ' + width + ' ' + height); 31 | // .attr("width", width) 32 | // .attr("height", height); 33 | 34 | queue() 35 | .defer(d3.json, "./js/us.json") 36 | .defer(d3.tsv, "./js/unemployment.tsv", function(d) { rateById.set(d.id, +d.rate); }) 37 | .await(ready); 38 | 39 | function ready(error, us) { 40 | if (error) throw error; 41 | 42 | svg.append("g") 43 | .attr("class", "counties") 44 | .selectAll("path") 45 | .data(topojson.feature(us, us.objects.counties).features) 46 | .enter().append("path") 47 | .attr("class", function(d) { return quantize(rateById.get(d.id)); }) 48 | .attr("d", path); 49 | 50 | svg.append("path") 51 | .datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; })) 52 | .attr("class", "states") 53 | .attr("d", path); 54 | } 55 | 56 | d3.select(self.frameElement).style("height", height + "px"); 57 | -------------------------------------------------------------------------------- /examples/axis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var d3 = require('d3'); 4 | // Require the library and give it a reference to d3 5 | var Prerender = require('..'); 6 | var prerender = Prerender(d3); 7 | 8 | 9 | // Then, when you start drawing svg call `prerender.start()` 10 | // this modifies some d3 functions to allow it to be 11 | // aware of SVGs that already exist on the page. 12 | prerender.start(); 13 | 14 | var margin = {top: 20, right: 20, bottom: 30, left: 40}, 15 | width = 960 - margin.left - margin.right, 16 | height = 500 - margin.top - margin.bottom; 17 | 18 | var x = d3.scaleLinear() 19 | .domain([-width / 2, width / 2]) 20 | .range([0, width]); 21 | 22 | var y = d3.scaleLinear() 23 | .domain([-height / 2, height / 2]) 24 | .range([height, 0]); 25 | 26 | var xAxis = d3.axisBottom() 27 | .scale(x) 28 | .tickSize(-height); 29 | 30 | var yAxis = d3.axisLeft() 31 | .scale(y) 32 | .ticks(5) 33 | .tickSize(-width); 34 | 35 | var zoom = d3.zoom() 36 | // .x(x) 37 | // .y(y) 38 | .scaleExtent([1, 32]) 39 | .on("zoom", zoomed); 40 | 41 | var svg = d3.select("body").append("svg") 42 | .attr("viewBox", ' 0 0 ' + (width + margin.left + margin.right) + ' ' + (height + margin.top + margin.bottom)) 43 | .append("g") 44 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 45 | 46 | svg.append("rect") 47 | .attr("width", width) 48 | .attr("height", height); 49 | 50 | var xg = svg.append("g") 51 | .attr("class", "x axis") 52 | .attr("transform", "translate(0," + height + ")") 53 | .call(xAxis); 54 | 55 | var yg = svg.append("g") 56 | .attr("class", "y axis") 57 | .call(yAxis); 58 | 59 | prerender.stop(); 60 | svg.call(zoom); 61 | 62 | function zoomed() { 63 | svg.select(".x.axis").call(xAxis); 64 | svg.select(".y.axis").call(yAxis); 65 | } 66 | -------------------------------------------------------------------------------- /examples/v4.js: -------------------------------------------------------------------------------- 1 | var d3 = require('d3'); 2 | 3 | require('./app.css'); 4 | // Require the library and give it a reference to d3 5 | var Prerender = require('..'); 6 | var prerender = Prerender(d3); 7 | 8 | prerender.start(); 9 | 10 | var svg = d3.select('body').append("svg").attr('width', 600).attr('height', 600), 11 | width = +svg.attr("width"), 12 | height = +svg.attr("height"); 13 | 14 | var x = d3.scaleLinear() 15 | .domain([-1, width + 1]) 16 | .range([-1, width + 1]); 17 | 18 | var y = d3.scaleLinear() 19 | .domain([-1, height + 1]) 20 | .range([-1, height + 1]); 21 | 22 | var xAxis = d3.axisBottom(x) 23 | .ticks((width + 2) / (height + 2) * 10) 24 | .tickSize(height) 25 | .tickPadding(8 - height); 26 | 27 | var yAxis = d3.axisRight(y) 28 | .ticks(10) 29 | .tickSize(width) 30 | .tickPadding(8 - width); 31 | 32 | var view = svg.append("rect") 33 | .attr("class", "view") 34 | .attr("x", 0.5) 35 | .attr("y", 0.5) 36 | .attr("width", width - 1) 37 | .attr("height", height - 1); 38 | 39 | var gX = svg.append("g") 40 | .attr("class", "axis axis--x") 41 | .call(xAxis); 42 | 43 | var gY = svg.append("g") 44 | .attr("class", "axis axis--y") 45 | .call(yAxis); 46 | 47 | svg.append("rect") 48 | .attr("class", "zoom") 49 | .attr("width", width) 50 | .attr("height", height) 51 | 52 | 53 | prerender.stop(); 54 | svg.call(d3.zoom() 55 | .scaleExtent([1, 40]) 56 | .translateExtent([[-100, -100], [width + 90, height + 100]]) 57 | .on("zoom", zoomed)); 58 | 59 | function zoomed() { 60 | svg.select(".x.axis").call(xAxis); 61 | svg.select(".y.axis").call(yAxis); 62 | } 63 | 64 | function zoomed() { 65 | view.attr("transform", d3.event.transform); 66 | gX.call(xAxis.scale(d3.event.transform.rescaleX(x))); 67 | gY.call(yAxis.scale(d3.event.transform.rescaleY(y))); 68 | } 69 | -------------------------------------------------------------------------------- /examples/stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var d3 = require('d3'); 4 | // Require the library and give it a reference to d3 5 | var Prerender = require('d3-pre'); 6 | var prerender = Prerender(d3); 7 | 8 | 9 | // Then, when you start drawing svg call `prerender.start()` 10 | // this modifies some d3 functions to allow it to be 11 | // aware of SVGs that already exist on the page. 12 | prerender.start(); 13 | 14 | var n = 20, // number of layers 15 | m = 200, // number of samples per layer 16 | stack = d3.layout.stack().offset("wiggle"), 17 | layers0 = require('./stream-data-0.json'), // use pre-computed data generated via 18 | layers1 = require('./stream-data-1.json'); // the command: stack(d3.range(n).map(function() { return bumpLayer(m); })); 19 | 20 | var width = 960, 21 | height = 500; 22 | 23 | var x = d3.scale.linear() 24 | .domain([0, m - 1]) 25 | .range([0, width]); 26 | 27 | var y = d3.scale.linear() 28 | .domain([0, d3.max(layers0.concat(layers1), function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); })]) 29 | .range([height, 0]); 30 | 31 | var color = d3.scale.linear() 32 | .range(["#aad", "#556"]); 33 | 34 | var area = d3.svg.area() 35 | .x(function(d) { return x(d.x); }) 36 | .y0(function(d) { return y(d.y0); }) 37 | .y1(function(d) { return y(d.y0 + d.y); }); 38 | 39 | d3.select('#interactive').append('button').text('Update').on('click', transition); 40 | 41 | var svg = d3.select("#interactive").append("svg") 42 | .attr('viewBox', '0 0 ' + width + ' ' + height); 43 | 44 | var layers = svg.selectAll("path") 45 | .data(layers0) 46 | .enter().append("path") 47 | .attr("d", area); 48 | 49 | 50 | // Only choose initial random colors once, during pre-render step 51 | if (prerender.isPreprocessing) { 52 | layers.style("fill", function() { return color(Math.random()); }); 53 | } 54 | 55 | function transition() { 56 | d3.selectAll("path") 57 | .data(function() { 58 | var d = layers1; 59 | layers1 = layers0; 60 | return layers0 = d; 61 | }) 62 | .transition() 63 | .duration(2500) 64 | .attr("d", area); 65 | } 66 | 67 | // Inspired by Lee Byron's test data generator. 68 | function bumpLayer(n) { 69 | 70 | function bump(a) { 71 | var x = 1 / (.1 + Math.random()), 72 | y = 2 * Math.random() - .5, 73 | z = 10 / (.1 + Math.random()); 74 | for (var i = 0; i < n; i++) { 75 | var w = (i / n - y) * z; 76 | a[i] += x * Math.exp(-w * w); 77 | } 78 | } 79 | 80 | var a = [], i; 81 | for (i = 0; i < n; ++i) a[i] = 0; 82 | for (i = 0; i < 5; ++i) bump(a); 83 | return a.map(function(d, i) { return {x: i, y: Math.max(0, d)}; }); 84 | } 85 | -------------------------------------------------------------------------------- /src/preprocess.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils'); 2 | 3 | module.exports = function (count) { 4 | var appendCount = count || -1; 5 | var d3; 6 | var dataString = 'data-pid'; 7 | 8 | var retThis = function () { 9 | return this; 10 | }; 11 | 12 | var setD3 = function (_d3) { 13 | d3 = _d3; 14 | d3.selection.prototype._append = d3.selection.prototype.append; 15 | d3.selection.prototype._html = d3.selection.prototype.html; 16 | d3.selection.prototype._attr = d3.selection.prototype.attr; 17 | d3.selection.prototype._each = d3.selection.prototype.each; 18 | 19 | d3.selection.prototype.append = retThis; 20 | d3.selection.prototype.html = retThis; 21 | d3.selection.prototype.attr = retThis; 22 | d3.selection.prototype.each = retThis; 23 | 24 | if (!utils.isV4OrBetter(d3)) { 25 | d3.selection.enter.prototype._append = d3.selection.enter.prototype.append; 26 | d3.selection.enter.prototype.append = retThis; 27 | } 28 | }; 29 | 30 | var start = function () { 31 | 32 | var newAppend = function (name) { 33 | var ogName = name; 34 | name = utils.mapAppendName(d3, name); 35 | 36 | var isEmpty = -1; 37 | 38 | var topNode = null; 39 | var allNodes = null; 40 | 41 | return this.select(function () { 42 | appendCount++; 43 | 44 | if (isEmpty === -1) { 45 | topNode = d3.select(this); 46 | var selection = topNode.select(ogName + '[' + dataString + '="' + appendCount + '"]'); 47 | 48 | if (selection.empty()) { 49 | isEmpty = 1; 50 | } else { 51 | isEmpty = 0; 52 | allNodes = topNode.select('*'); 53 | } 54 | } 55 | 56 | if (isEmpty === 1) { 57 | var c = this.appendChild(name.apply(this, arguments)); 58 | return d3.select(c).attr(dataString, appendCount).node(); 59 | } else if (isEmpty === 0) { 60 | return allNodes[arguments[2]]; 61 | } 62 | 63 | return selection; 64 | }); 65 | }; 66 | 67 | d3.selection.prototype.append = newAppend; 68 | d3.selection.prototype.html = d3.selection.prototype._html; 69 | d3.selection.prototype.attr = d3.selection.prototype._attr; 70 | d3.selection.prototype.each = d3.selection.prototype._each; 71 | if (!utils.isV4OrBetter(d3)) { 72 | d3.selection.enter.prototype.append = newAppend; 73 | } 74 | 75 | }; 76 | 77 | var stop = function () { 78 | if (!utils.isV4OrBetter(d3)) { 79 | d3.selection.enter.prototype.append = retThis; 80 | } 81 | 82 | d3.selection.prototype.append = retThis; 83 | d3.selection.prototype.html = retThis; 84 | d3.selection.prototype.attr = retThis; 85 | d3.selection.prototype.each = retThis; 86 | }; 87 | 88 | return { 89 | start: start, 90 | stop: stop, 91 | setD3: setD3 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | 2 | var dataString = 'data-pid'; 3 | // var modifiedAxis = require('./d3/axis'); 4 | var utils = require('./utils'); 5 | 6 | module.exports = function (count) { 7 | var d3; 8 | var appendCount = count || -1; 9 | var setD3 = function (_d3) { 10 | d3 = _d3; 11 | }; 12 | var start = function () { 13 | 14 | var newEnterAppend = function (name) { 15 | var ogName = name; 16 | name = utils.mapAppendName(d3, name); 17 | var isEmpty = -1; 18 | 19 | return this.select(function () { 20 | appendCount++; 21 | if (isEmpty === -1) { 22 | var selection = d3.select(ogName + '[' + dataString + '="' + appendCount + '"]'); 23 | if (selection.empty()) { 24 | isEmpty = 1; 25 | } else { 26 | isEmpty = 0; 27 | } 28 | } 29 | 30 | if (isEmpty === 1) { 31 | var c = this.appendChild(name.apply(this, arguments)); 32 | return d3.select(c).attr(dataString, appendCount).node(); 33 | } 34 | 35 | return d3.select('[' + dataString + '="' + appendCount + '"]').node(); 36 | }); 37 | }; 38 | 39 | if (!utils.isV4OrBetter(d3)) { 40 | d3.svg._axis = d3.svg.axis; 41 | d3.svg.axis = modifiedAxis(d3); 42 | d3.selection.enter.prototype._append = d3.selection.enter.prototype.append; 43 | d3.selection.enter.prototype.append = newEnterAppend; 44 | } 45 | 46 | d3.selection.prototype._append = d3.selection.prototype.append; 47 | d3.selection.prototype.append = newEnterAppend; 48 | 49 | 50 | d3.selection.prototype._data = d3.selection.prototype.data; 51 | 52 | d3.selection.prototype.data = function () { 53 | if (!arguments.length) { 54 | return this._data.apply(this, arguments); 55 | } 56 | var output = this._data.apply(this, arguments); 57 | var enter = output.enter(); 58 | 59 | if (enter.empty()) { 60 | // var data = output.data(); 61 | // appendCount += data.length; 62 | 63 | var retThis = function () { 64 | return this; 65 | }; 66 | 67 | // ~should~ be safe to ignore because 68 | // these got pushed to the DOM already 69 | this.attr = retThis; 70 | this.style = retThis; 71 | 72 | var self = this; 73 | output.enter = function () { 74 | return self; 75 | }; 76 | } 77 | 78 | return output; 79 | }; 80 | }; 81 | 82 | var stop = function () { 83 | 84 | d3.selection.prototype.append = d3.selection.prototype._append; 85 | d3.selection.prototype.data = d3.selection.prototype._data; 86 | if (!utils.isV4OrBetter(d3)) { 87 | d3.selection.enter.prototype.append = d3.selection.enter.prototype._append; 88 | d3.svg.axis = d3.svg._axis; 89 | } 90 | }; 91 | 92 | return { 93 | start: start, 94 | stop: stop, 95 | setD3: setD3 96 | }; 97 | }; 98 | -------------------------------------------------------------------------------- /test/templates/input/browser/3.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/templates/output/preprocess/4.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # d3-pre 2 | A JavaScript library that pre-renders d3 visualizations into inline SVG elements, to reduce perceived page-load time and cut down on unwanted paint flashes. 3 | 4 | The pre-rendering tool uses a headless browser to turn d3 code into its resulting SVG, and inserts the markup into your HTML. Then, the `d3-pre` JavaScript library overrides `d3.append` to check if a pre-rendered DOM node already exists before creating a new one. This approach allows you to use pre-rendered SVG without changing your visualization code. 5 | 6 | See an example of the speed benefits of using inline SVG over SVG generated in the client by refreshing this page [without pre-rendering](http://fivethirtyeight.github.io/d3-pre/examples/standard/) 7 | and [with pre-rendering](http://fivethirtyeight.github.io/d3-pre/examples/prerendered/). 8 | 9 | ## Examples 10 | 11 | * [Axis Pan+Zoom](http://fivethirtyeight.github.io/d3-pre/examples/axes/) ([code](./examples/axis.js)) 12 | * [Streamgraph](http://fivethirtyeight.github.io/d3-pre/examples/streamgraph/) ([code](./examples/stream.js)) 13 | * [Choropleth](http://fivethirtyeight.github.io/d3-pre/examples/choropleth/) ([code](./examples/choropleth.js)) 14 | 15 | ##### In the wild 16 | 17 | * http://projects.fivethirtyeight.com/facebook-primary/ 18 | * http://projects.fivethirtyeight.com/2016-election-forecast/ 19 | 20 | ## Installation 21 | 22 | ``` 23 | npm install --save d3-pre 24 | ``` 25 | 26 | ## Usage 27 | 28 | There are two things that you need to do to use this library: 29 | 30 | ### 1. Include d3-pre in your javascript 31 | 32 | ```js 33 | var d3 = require('d3'); 34 | 35 | // Require the library and give it a reference to d3 36 | var Prerender = require('d3-pre'); 37 | var prerender = Prerender(d3); 38 | 39 | 40 | // Then, when you start drawing SVG, call `prerender.start()`. 41 | // This modifies some d3 functions to make it aware 42 | // of the pre-rendered SVGs. 43 | prerender.start(); 44 | 45 | /* 46 | * Existing d3 code goes here 47 | * d3.select('body') 48 | * .append('svg') 49 | * .data(data) 50 | * .enter() 51 | * .append('rect') 52 | * .on('click', clickhandler) 53 | * etc. etc. 54 | */ 55 | 56 | // If you ever want to go back to the unmodified d3, 57 | // just call `prerender.stop()`. 58 | // This is optional and usually not necessary. 59 | prerender.stop(); 60 | 61 | ``` 62 | 63 | 64 | ### 2. Pass your HTML through the pre-rendering tool 65 | 66 | This can be done via a build task (like gulp), or on the command line. To provide control over which DOM modifications are saved back to the HTML file, you can add the following data-attributes in the HTML: 67 | * `data-prerender-ignore`: Any modifications that happen inside a node with this attribute will be ignored. 68 | * `data-prerender-only`: Only modifications inside of this node are saved. 69 | * `data-prerender-minify`: Any SVG with this attribute will automatically be passed through an SVG minification tool. 70 | 71 | #### Command line example 72 | 73 | Install the [command line tool](https://github.com/fivethirtyeight/d3-pre-cli): 74 | 75 | ``` 76 | $ npm install -g d3-pre-cli 77 | ``` 78 | 79 | Run the `d3-pre` command on an HTML file: 80 | 81 | ``` 82 | $ d3-pre ./path/to/index.html 83 | ``` 84 | 85 | This command will open the index.html file in a headless browser, running any JavaScript included on the page. Any modifications that the JavaScript makes to the DOM are saved back to the HTML file. 86 | 87 | #### Gulp example 88 | 89 | Install the [gulp plugin](https://github.com/fivethirtyeight/gulp-d3-pre): 90 | ``` 91 | $ npm install gulp-d3-pre 92 | ``` 93 | 94 | Create a gulp task: 95 | 96 | ```js 97 | var gulp = require('gulp'); 98 | var d3Pre = require('gulp-d3-pre'); 99 | 100 | 101 | gulp.task('prerender-svgs', function() { 102 | gulp.src('./public/index.html') 103 | .pipe(d3Pre(options)) 104 | .pipe(gulp.dest('./public/')); 105 | }) 106 | ``` 107 | 108 | This task will open the index.html file in a headless browser, running any JavaScript included on the page. Any modifications that the JavaScript makes to the DOM are saved back to the HTML file. 109 | The following options may be passed to the gulp plugin: 110 | * `preprocessHTML` - A function to run before running the HTML through the pre-renderer. Takes a string as input and expects a string as output. 111 | * `postprocessHTML` - A function to run after running the HTML through the pre-renderer. Takes a string as input and expects a string as output. 112 | 113 | #### Advanced usage 114 | 115 | Both of the above modules are thin wrappers around [d3-pre-renderer](https://github.com/fivethirtyeight/d3-pre-renderer). If you require more fine-grained control of when and where the pre-rendering step takes place, use d3-pre-renderer directly. 116 | 117 | ## Release Notes 118 | 119 | #### `v1.3.0` 120 | 121 | * Adds support for d3 `v4`. 122 | 123 | ## Contributors 124 | 125 | * [Matthew Conlen](https://github.com/mathisonian) 126 | 127 | ## License 128 | 129 | MIT 130 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* global it, describe */ 2 | 'use strict'; 3 | 4 | var expect = require('expect.js'); 5 | var fs = require('fs'); 6 | var d3 = require('d3'); 7 | var jsdom = require('jsdom'); 8 | var Prerender = require('..'); 9 | var _ = require('lodash'); 10 | 11 | var cleanString = function (str) { 12 | return str.replace(/\s+/g, ' ').trim(); 13 | }; 14 | 15 | var runHeadless = function (inputHtml, fn, cb) { 16 | jsdom.env({ 17 | html: inputHtml, 18 | features: { 19 | QuerySelector: true 20 | }, 21 | done: function (errors, window) { 22 | window.d3 = d3.select(window.document); 23 | fn(window); 24 | window.close(); 25 | cb(); 26 | } 27 | }); 28 | }; 29 | 30 | describe('Setup', function () { 31 | it('should run d3 in jsdom', function (done) { 32 | var inputHtml = cleanString(fs.readFileSync(__dirname + '/templates/input/preprocess.html').toString()); 33 | var outputHtml = cleanString(fs.readFileSync(__dirname + '/templates/output/preprocess/1.html').toString()); 34 | 35 | runHeadless(inputHtml, function (window) { 36 | window.d3.select('body').append('svg'); 37 | var actualOutput = cleanString(window.document.documentElement.outerHTML); 38 | expect(actualOutput).to.eql(outputHtml); 39 | }, done); 40 | }); 41 | }); 42 | 43 | describe('Preprocessor', function () { 44 | it('should append svg elements', function (done) { 45 | var inputHtml = cleanString(fs.readFileSync(__dirname + '/templates/input/preprocess.html').toString()); 46 | var outputHtml = cleanString(fs.readFileSync(__dirname + '/templates/output/preprocess/2.html').toString()); 47 | 48 | var data = _.range(10); 49 | runHeadless(inputHtml, function (window) { 50 | var prerender = new Prerender(d3, { mode: 'preprocess' }); 51 | 52 | prerender.start(); 53 | var svg = window.d3.select('body').append('svg'); 54 | 55 | svg.selectAll('circle') 56 | .data(data) 57 | .enter() 58 | .append('circle') 59 | .attr('cx', function (d) { return d; }) 60 | .attr('cy', function (d) { return d; }); 61 | 62 | var actualOutput = cleanString(window.document.documentElement.outerHTML); 63 | expect(actualOutput).to.eql(outputHtml); 64 | }, done); 65 | }); 66 | 67 | it('should handle selections', function (done) { 68 | var inputHtml = cleanString(fs.readFileSync(__dirname + '/templates/input/preprocess.html').toString()); 69 | var outputHtml = cleanString(fs.readFileSync(__dirname + '/templates/output/preprocess/3.html').toString()); 70 | 71 | var data = _.range(10); 72 | runHeadless(inputHtml, function (window) { 73 | var prerender = new Prerender(d3, { mode: 'preprocess' }); 74 | 75 | prerender.start(); 76 | var svg = window.d3.select('body').append('svg'); 77 | 78 | var groups = svg.selectAll('g') 79 | .data(data) 80 | .enter() 81 | .append('g') 82 | .attr('i', function (d) { return d; }); 83 | 84 | groups 85 | .append('circle') 86 | .attr('dx', function (d) { 87 | return d; 88 | }) 89 | .attr('dy', function (d) { 90 | return d; 91 | }); 92 | 93 | var actualOutput = cleanString(window.document.documentElement.outerHTML); 94 | expect(actualOutput).to.eql(outputHtml); 95 | }, done); 96 | }); 97 | 98 | it('should handle weird nesting', function (done) { 99 | var inputHtml = cleanString(fs.readFileSync(__dirname + '/templates/input/preprocess.html').toString()); 100 | var outputHtml = cleanString(fs.readFileSync(__dirname + '/templates/output/preprocess/4.html').toString()); 101 | 102 | var outerData = _.range(10); 103 | var innerData = _.range(5); 104 | 105 | runHeadless(inputHtml, function (window) { 106 | var prerender = new Prerender(d3, { mode: 'preprocess' }); 107 | 108 | prerender.start(); 109 | var svg = window.d3.select('body').append('svg'); 110 | 111 | var groups = svg.selectAll('g.outer') 112 | .data(outerData) 113 | .enter() 114 | .append('g') 115 | .attr('class', 'outer') 116 | .attr('i', function (d) { return d; }); 117 | 118 | groups.append('rect'); 119 | 120 | groups 121 | .append('g') 122 | .selectAll('rect') 123 | .data(innerData) 124 | .enter() 125 | .append('rect') 126 | .attr('x', function (d) { 127 | return d; 128 | }); 129 | 130 | groups 131 | .append('circle') 132 | .attr('dx', function (d) { 133 | return d; 134 | }) 135 | .attr('dy', function (d) { 136 | return d; 137 | }); 138 | 139 | var actualOutput = cleanString(window.document.documentElement.outerHTML); 140 | 141 | expect(actualOutput).to.eql(outputHtml); 142 | }, done); 143 | }); 144 | 145 | it('should handle multiple levels of nesting', function (done) { 146 | var inputHtml = cleanString(fs.readFileSync(__dirname + '/templates/input/preprocess.html').toString()); 147 | var outputHtml = cleanString(fs.readFileSync(__dirname + '/templates/output/preprocess/5.html').toString()); 148 | 149 | var outerData = _.range(10); 150 | var innerData = _.range(5); 151 | var innerInnerData = _.range(15); 152 | 153 | runHeadless(inputHtml, function (window) { 154 | var prerender = new Prerender(d3, { mode: 'preprocess' }); 155 | 156 | prerender.start(); 157 | var svg = window.d3.select('body').append('svg'); 158 | 159 | var groups = svg.selectAll('g.outer') 160 | .data(outerData) 161 | .enter() 162 | .append('g') 163 | .attr('class', 'outer') 164 | .attr('i', function (d) { return d; }); 165 | 166 | groups 167 | .selectAll('g.inner') 168 | .data(innerData) 169 | .enter() 170 | .append('g') 171 | .attr('class', 'inner') 172 | .selectAll('rect') 173 | .data(innerInnerData) 174 | .enter() 175 | .append('rect'); 176 | 177 | var actualOutput = cleanString(window.document.documentElement.outerHTML); 178 | expect(actualOutput).to.eql(outputHtml); 179 | }, done); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /src/d3/axis.js: -------------------------------------------------------------------------------- 1 | var d3_svg_axisOrients = { 2 | top: 1, 3 | right: 1, 4 | bottom: 1, 5 | left: 1 6 | }; 7 | function d3_identity (d) { 8 | return d; 9 | } 10 | var ε = 1e-6; 11 | function d3_scaleExtent (domain) { 12 | var start = domain[0]; 13 | var stop = domain[domain.length - 1]; 14 | return start < stop ? [ start, stop ] : [ stop, start ]; 15 | } 16 | function d3_scaleRange (scale) { 17 | return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range()); 18 | } 19 | function d3_svg_axisX (selection, x0, x1) { 20 | selection.attr('transform', function (d) { 21 | if (d === undefined) { 22 | return 'translate(0,0)'; 23 | } 24 | var v0 = x0(d); 25 | return 'translate(' + (isFinite(v0) ? v0 : x1(d)) + ',0)'; 26 | }); 27 | } 28 | function d3_svg_axisY (selection, y0, y1) { 29 | selection.attr('transform', function (d) { 30 | if (d === undefined) { 31 | return 'translate(0,0)'; 32 | } 33 | var v0 = y0(d); 34 | return 'translate(0,' + (isFinite(v0) ? v0 : y1(d)) + ')'; 35 | }); 36 | } 37 | 38 | module.exports = function (d3) { 39 | return function () { 40 | var scale = d3.scale.linear(); 41 | var orient = 'bottom'; 42 | var innerTickSize = 6; 43 | var outerTickSize = 6; 44 | var tickPadding = 3; 45 | var tickArguments_ = [10]; 46 | var tickValues = null; 47 | var tickFormat_; 48 | 49 | function axis (g) { 50 | g.each(function () { 51 | var g = d3.select(this); 52 | 53 | // Stash a snapshot of the new scale, and retrieve the old snapshot. 54 | var scale0 = this.__chart__ || scale; 55 | var scale1 = this.__chart__ = scale.copy(); 56 | 57 | // Ticks, or domain values for ordinal scales. 58 | var ticks = tickValues == null ? (scale1.ticks ? scale1.ticks.apply(scale1, tickArguments_) : scale1.domain()) : tickValues; 59 | var tickFormat = tickFormat_ == null ? (scale1.tickFormat ? scale1.tickFormat.apply(scale1, tickArguments_) : d3_identity) : tickFormat_; 60 | var tick = g.selectAll('.tick').data(ticks, scale1); 61 | var tickEnter = tick.enter().insert('g', '.domain').attr('class', 'tick').style('opacity', ε); 62 | var tickExit = d3.transition(tick.exit()).style('opacity', ε).remove(); 63 | var tickUpdate = d3.transition(tick.order()).style('opacity', 1); 64 | var tickSpacing = Math.max(innerTickSize, 0) + tickPadding; 65 | var tickTransform; 66 | 67 | // Domain. 68 | var range = d3_scaleRange(scale1); 69 | var path = g.selectAll('.domain').data([0]); 70 | var pathUpdate = (path.enter().append('path').attr('class', 'domain'), d3.transition(path)); 71 | 72 | tickEnter.append('line'); 73 | tickEnter.append('text'); 74 | 75 | var lineEnter = tickEnter.select('line'); 76 | var lineUpdate = tickUpdate.select('line'); 77 | var text = tick.select('text').text(tickFormat); 78 | var textEnter = tickEnter.select('text'); 79 | var textUpdate = tickUpdate.select('text'); 80 | var sign = orient === 'top' || orient === 'left' ? -1 : 1; 81 | var x1, x2, y1, y2; 82 | 83 | if (orient === 'bottom' || orient === 'top') { 84 | tickTransform = d3_svg_axisX; 85 | x1 = 'x'; 86 | y1 = 'y'; 87 | x2 = 'x2'; 88 | y2 = 'y2'; 89 | text.attr('dy', sign < 0 ? '0em' : '.71em').style('text-anchor', 'middle'); 90 | pathUpdate.attr('d', 'M' + range[0] + ',' + sign * outerTickSize + 'V0H' + range[1] + 'V' + sign * outerTickSize); 91 | } else { 92 | tickTransform = d3_svg_axisY; 93 | x1 = 'y'; 94 | y1 = 'x'; 95 | x2 = 'y2'; 96 | y2 = 'x2'; 97 | text.attr('dy', '.32em').style('text-anchor', sign < 0 ? 'end' : 'start'); 98 | pathUpdate.attr('d', 'M' + sign * outerTickSize + ',' + range[0] + 'H0V' + range[1] + 'H' + sign * outerTickSize); 99 | } 100 | 101 | lineEnter.attr(y2, sign * innerTickSize); 102 | textEnter.attr(y1, sign * tickSpacing); 103 | lineUpdate.attr(x2, 0).attr(y2, sign * innerTickSize); 104 | textUpdate.attr(x1, 0).attr(y1, sign * tickSpacing); 105 | 106 | // If either the new or old scale is ordinal, 107 | // entering ticks are undefined in the old scale, 108 | // and so can fade-in in the new scale’s position. 109 | // Exiting ticks are likewise undefined in the new scale, 110 | // and so can fade-out in the old scale’s position. 111 | if (scale1.rangeBand) { 112 | var x = scale1; 113 | var dx = x.rangeBand() / 2; 114 | scale0 = scale1 = function (d) { return x(d) + dx; }; 115 | } else if (scale0.rangeBand) { 116 | scale0 = scale1; 117 | } else { 118 | tickExit.call(tickTransform, scale1, scale0); 119 | } 120 | 121 | tickEnter.call(tickTransform, scale0, scale1); 122 | tickUpdate.call(tickTransform, scale1, scale1); 123 | }); 124 | } 125 | 126 | axis.scale = function (x) { 127 | if (!arguments.length) return scale; 128 | scale = x; 129 | return axis; 130 | }; 131 | 132 | axis.orient = function (x) { 133 | if (!arguments.length) return orient; 134 | orient = x in d3_svg_axisOrients ? x + '' : 'bottom'; 135 | return axis; 136 | }; 137 | 138 | axis.ticks = function () { 139 | if (!arguments.length) return tickArguments_; 140 | tickArguments_ = arguments; 141 | return axis; 142 | }; 143 | 144 | axis.tickValues = function (x) { 145 | if (!arguments.length) return tickValues; 146 | tickValues = x; 147 | return axis; 148 | }; 149 | 150 | axis.tickFormat = function (x) { 151 | if (!arguments.length) return tickFormat_; 152 | tickFormat_ = x; 153 | return axis; 154 | }; 155 | 156 | axis.tickSize = function (x) { 157 | var n = arguments.length; 158 | if (!n) return innerTickSize; 159 | innerTickSize = +x; 160 | outerTickSize = +arguments[n - 1]; 161 | return axis; 162 | }; 163 | 164 | axis.innerTickSize = function (x) { 165 | if (!arguments.length) return innerTickSize; 166 | innerTickSize = +x; 167 | return axis; 168 | }; 169 | 170 | axis.outerTickSize = function (x) { 171 | if (!arguments.length) return outerTickSize; 172 | outerTickSize = +x; 173 | return axis; 174 | }; 175 | 176 | axis.tickPadding = function (x) { 177 | if (!arguments.length) return tickPadding; 178 | tickPadding = +x; 179 | return axis; 180 | }; 181 | 182 | axis.tickSubdivide = function () { 183 | return arguments.length && axis; 184 | }; 185 | 186 | return axis; 187 | }; 188 | }; 189 | -------------------------------------------------------------------------------- /test/browser.js: -------------------------------------------------------------------------------- 1 | /* global it, describe, beforeEach, afterEach */ 2 | 'use strict'; 3 | var expect = require('expect.js'); 4 | var fs = require('fs'); 5 | var d3 = require('d3'); 6 | var Prerender = require('..'); 7 | var _ = require('lodash'); 8 | 9 | var cleanString = function (str) { 10 | return str.replace(/\s+/g, ' ').trim(); 11 | }; 12 | 13 | var prerender; 14 | describe('In browser', function () { 15 | beforeEach(function () { 16 | d3.select('body').append('div').attr('id', 'test-container'); 17 | prerender = new Prerender(d3, { mode: 'browser' }); 18 | prerender.start(); 19 | }); 20 | 21 | afterEach(function () { 22 | prerender.stop(); 23 | d3.select('#test-container').remove(); 24 | }); 25 | 26 | it('should handle existing svg elements', function (done) { 27 | var inner = cleanString(fs.readFileSync(__dirname + '/templates/input/browser/1.svg').toString()); 28 | d3.select('#test-container').html(inner); 29 | 30 | d3.select('#test-container').append('svg'); 31 | 32 | var results = d3.select('#test-container').html(); 33 | expect(results).to.eql(inner); 34 | done(); 35 | }); 36 | 37 | it('should handle data', function (done) { 38 | var inner = cleanString(fs.readFileSync(__dirname + '/templates/input/browser/2.svg').toString()); 39 | d3.select('#test-container').html(inner); 40 | 41 | var data = _.range(10); 42 | 43 | var svg = d3.select('#test-container').append('svg'); 44 | svg.selectAll('circle') 45 | .data(data) 46 | .enter() 47 | .append('circle') 48 | .attr('cx', function (d) { return d; }) 49 | .attr('cy', function (d) { return d; }); 50 | 51 | var results = d3.select('#test-container').html(); 52 | expect(results).to.be(inner); 53 | done(); 54 | }); 55 | 56 | it('should handle weird nesting', function (done) { 57 | var inner = cleanString(fs.readFileSync(__dirname + '/templates/input/browser/3.svg').toString()); 58 | d3.select('#test-container').html(inner); 59 | var outerData = _.range(10); 60 | var innerData = _.range(5); 61 | 62 | var svg = d3.select('body').append('svg'); 63 | 64 | var groups = svg.selectAll('g.outer') 65 | .data(outerData) 66 | .enter() 67 | .append('g') 68 | .attr('class', 'outer') 69 | .attr('i', function (d) { return d; }); 70 | 71 | groups.append('rect'); 72 | 73 | groups 74 | .append('g') 75 | .selectAll('rect') 76 | .data(innerData) 77 | .enter() 78 | .append('rect') 79 | .attr('x', function (d) { 80 | return d; 81 | }); 82 | 83 | groups 84 | .append('circle') 85 | .attr('dx', function (d) { 86 | return d; 87 | }) 88 | .attr('dy', function (d) { 89 | return d; 90 | }); 91 | 92 | var results = d3.select('#test-container').html(); 93 | expect(results).to.be(inner); 94 | done(); 95 | }); 96 | 97 | it('should handle dataflow correctly', function (done) { 98 | var inner = cleanString(fs.readFileSync(__dirname + '/templates/input/browser/3.svg').toString()); 99 | d3.select('#test-container').html(inner); 100 | var outerData = _.range(10); 101 | var innerData = _.range(5); 102 | 103 | var svg = d3.select('body').append('svg'); 104 | 105 | var groups = svg.selectAll('g.outer') 106 | .data(outerData) 107 | .enter() 108 | .append('g') 109 | .attr('class', 'outer') 110 | .attr('i', function (d) { return d; }); 111 | 112 | groups.append('rect'); 113 | 114 | groups 115 | .append('g') 116 | .selectAll('rect') 117 | .data(innerData) 118 | .enter() 119 | .append('rect') 120 | .attr('x', function (d) { 121 | return d; 122 | }) 123 | .each(function (d, i) { 124 | expect(d).to.be(innerData[i]); 125 | }); 126 | 127 | groups 128 | .append('circle') 129 | .attr('dx', function (d) { 130 | return d; 131 | }) 132 | .attr('dy', function (d) { 133 | return d; 134 | }) 135 | .each(function (d, i) { 136 | expect(d).to.be(outerData[i]); 137 | }); 138 | 139 | var results = d3.select('#test-container').html(); 140 | expect(results).to.be(inner); 141 | done(); 142 | }); 143 | 144 | it('should add new data correctly', function (done) { 145 | var inner = cleanString(fs.readFileSync(__dirname + '/templates/input/browser/4.svg').toString()); 146 | var expected = cleanString(fs.readFileSync(__dirname + '/templates/output/browser/4.svg').toString()); 147 | d3.select('#test-container').html(inner); 148 | var outerData = _.range(10); 149 | var innerData = _.range(5); 150 | var newData = _.range(25); 151 | 152 | var svg = d3.select('body').append('svg'); 153 | 154 | var groups = svg.selectAll('g.outer') 155 | .data(outerData) 156 | .enter() 157 | .append('g') 158 | .attr('class', 'outer') 159 | .attr('i', function (d) { return d; }); 160 | 161 | groups.append('rect'); 162 | 163 | groups 164 | .append('g') 165 | .selectAll('rect') 166 | .data(innerData) 167 | .enter() 168 | .append('rect') 169 | .attr('x', function (d) { 170 | return d; 171 | }) 172 | .each(function (d, i) { 173 | expect(d).to.be(innerData[i]); 174 | }); 175 | 176 | groups 177 | .append('circle') 178 | .attr('dx', function (d) { 179 | return d; 180 | }) 181 | .attr('dy', function (d) { 182 | return d; 183 | }) 184 | .each(function (d, i) { 185 | expect(d).to.be(outerData[i]); 186 | }); 187 | 188 | groups.append('g') 189 | .selectAll('line') 190 | .data(newData) 191 | .enter() 192 | .append('line') 193 | .attr('x1', function (d) { return d; }) 194 | .attr('y1', function (d) { return d; }); 195 | 196 | var results = d3.select('#test-container').html(); 197 | expect(results).to.be(expected); 198 | done(); 199 | }); 200 | 201 | it('should work with nested data', function (done) { 202 | var inner = cleanString(fs.readFileSync(__dirname + '/templates/input/browser/5.svg').toString()); 203 | 204 | d3.select('#test-container').html(inner); 205 | var outerData = _.range(10); 206 | var innerData = _.range(5); 207 | var innerInnerData = _.range(15); 208 | 209 | var svg = d3.select('body').append('svg'); 210 | 211 | var groups = svg.selectAll('g.outer') 212 | .data(outerData) 213 | .enter() 214 | .append('g') 215 | .attr('class', 'outer') 216 | .attr('i', function (d) { return d; }) 217 | .each(function (d, i) { 218 | expect(d).to.be(outerData[i]); 219 | }); 220 | 221 | groups 222 | .selectAll('g.inner') 223 | .data(innerData) 224 | .enter() 225 | .append('g') 226 | .attr('class', 'inner') 227 | .each(function (d, i) { 228 | expect(d).to.be(innerData[i]); 229 | }) 230 | .selectAll('rect') 231 | .data(innerInnerData) 232 | .enter() 233 | .append('rect') 234 | .each(function (d, i) { 235 | expect(d).to.be(innerInnerData[i]); 236 | }); 237 | 238 | var results = d3.select('#test-container').html(); 239 | expect(results).to.be(inner); 240 | done(); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /test/templates/input/browser/4.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/templates/output/browser/4.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/templates/input/browser/5.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/templates/output/preprocess/5.html: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------