├── LICENSE.md ├── README.md ├── index.html ├── minard.html ├── pathdemo.html └── pathlayout.js /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014, 2015 Benjamin Schmidt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | D3 Trail layout 2 | ================ 3 | 4 | This is a layout function for creating paths in D3 where (unlike the native d3.svg.line() element) you need to apply specific aesthetics to each element of the line. 5 | 6 | Demos 7 | ====== 8 | 9 | The original use case was trails with decaying opacity to represent movement: here's a sample image:![](http://benschmidt.org/maury2.png) (interactive versions forthcoming). 10 | 11 | For a classical example, see how variable-width, staggered entry lines [make it possible to reproduce Minard's famous Napoleon map in motion](http://benschmidt.org/D3-trail/minard.html). Here's a screenshot, although the whole point is the animation. ![](http://benschmidt.org/minard.png) 12 | 13 | 14 | For a more straightforward demo of staggered entry ([see this demo of a random walk](http://benschmidt.org/D3-trail/pathdemo.html)). 15 | 16 | 17 | Features include: 18 | 19 | 1. Controls to define multiple groups of simultaneous lines by setting `grouping`. 20 | 2. Options to return data either with `x1`,`x2`,`y1`, and `y2` elements to use with svg lines *or* with a `coordinates` object to use with geoprojections by setting `coordType`. 21 | 3. Inbuilt methods to easily define `opacity` in the data as an evaporating trail in time like the frames in [my maps of shipping routes](http://sappingattention.blogspot.com/2012/04/visualizing-ocean-shipping.html). 22 | 4. Control over sorting. 23 | 5. Access to both points in the bound data, so that appearance of a segment can be defined by data about the destination point, the origin, or some interaction of them. 24 | 25 | 26 | Usage 27 | ===== 28 | 29 | You instantiate it by calling the function: once parameters are set, run the `layout()` method to get values back. 30 | 31 | 32 | A bare bones example might be: 33 | 34 | ``` {js} 35 | trail = d3.layout.trail().coordType('xy') 36 | 37 | new = trail.data(whatever).layout() 38 | 39 | paths = svg.selectAll("line").data(new) 40 | 41 | paths.enter() 42 | .append('line') 43 | .attr("x1",function(d) {return d.x1}) 44 | .attr("y1",function(d) {return d.y1}) 45 | .attr("y2",function(d) {return d.y2}) 46 | .attr("x2",function(d) {return d.x2}) 47 | 48 | ``` 49 | 50 | API 51 | === 52 | 53 | Creation 54 | -------- 55 | 56 | Create a trail object 57 | ``` 58 | trail = d3.layout.trail() 59 | ``` 60 | 61 | trail.data() 62 | ----------- 63 | 64 | Set (or with no arguments, return) the data to be plotted. Each object in the return values will correspond to two consecutive points in this array. 65 | 66 | 67 | trail.positioner() 68 | ------------------- 69 | 70 | Set (or with no arguments, return) a function used to specify the x and y values for each **point**. 71 | 72 | By default, it returns simply [x,y] for the data: 73 | 74 | ``` 75 | positioner = function(datum) { return [datum.x,datum.y] } 76 | ``` 77 | 78 | You can also drop in any map projection as the positioner. 79 | 80 | 81 | trail.coordType() 82 | ----------------- 83 | 84 | Set (or with no arguments, return) the format of positions for the return values. Can be either 85 | 86 | 1. `"xy"`, in which case you'll get `x1`,`x2`,`y1`, and `y2` elements suitable for use with svg lines, or 87 | 2. `"coordinates"`, in which case the objection will get a `coordinates` element consisting of an array of points so you can simply create a d3.geo.path() element. (Useful if you want to preserve great circle arcs for your paths, for instance; in this case the positioner should return lat and lon, not the projected values, so you don't project them twice.) 88 | 89 | 90 | trail.grouping() 91 | ---------------- 92 | 93 | Set (or with no arguments, return) the grouping 94 | 95 | 96 | 97 | trail.sort() 98 | ------------- 99 | 100 | Set (or with no arguments, return) the function used to order the points. 101 | 102 | If not set, points will be ordered in the direction they went in. 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | And then set the parameters 111 | 112 | 113 | 114 | 115 | Time-specific elements 116 | ----------------------- 117 | 118 | ### trail.time() 119 | 120 | Set (or with no arguments, return) a function that returns how to access the time data. (Must be numeric: dates not yet supported). 121 | 122 | ### trail.currentTime() 123 | 124 | Set (or with no arguments, return) the current time (ie, that for which opacity should be 1); all times after this will be dropped from the returned set. 125 | 126 | ### trail.decayRange() 127 | 128 | Set (or with no arguments, return) the number of time units to be displayed: The opacity of segments ending at `trail.currentTime() - trail.decayRange()` will be zero, and all earlier segments will not be displayed 129 | 130 | 131 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /minard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Minard map in D3 4 | 5 | 6 | 54 | 55 | 56 | 57 | 154 | 164 | 165 | 166 |

Minard's Map of the Russian Campaign

167 |

A trail layout demonstration

168 |
This is a demo of how my D3 trail layout for D3 can reproduce 169 | Minard's map of the retreat of Napoleon's army from Russia. 170 |

171 | The "grouping" elements are being used here to draw several 172 | different paths (by color) simultaneously, and set their width by 173 | the size of Napoleon's army. (From a maximum of 340,000 to an end 174 | value of 4,000: no scale displayed for concision.) Data is taken 175 | from here. 176 | 177 |

This is a reimplementation, not a reimagination, 178 | of Minard's diagram. The only significant addition is the temporal 179 | placement of elements. This helps clarify the most confusing elements of 180 | the original (such as the direction of the temperature graph). All 181 | animation timing is done by purely by D3 transitions. This frees 182 | up color from representing the army's direction: 183 | here the different branches of the French force each have their own 184 | color. Even these minor improvements can only mean, though, that 185 | this has now displaced Minard's original as greatest statistical graphic ever 186 | created. (Unless you really miss the original rivers, I guess.) 187 | 188 |

189 | 190 | 191 | 365 | 366 |
367 |
368 | Last modified: Thu Jun 19 10:52:24 EDT 2014 369 | 370 | -------------------------------------------------------------------------------- /pathdemo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Path demonstration 4 | 5 | 6 | 7 | 8 | 9 | 10 |

D3 path demonstrations

11 | 12 | 13 |
14 | 15 | <> 16 | 17 | 18 | 76 | 77 |
78 | Last modified: Wed Jun 18 15:15:40 EDT 2014 79 | 80 | -------------------------------------------------------------------------------- /pathlayout.js: -------------------------------------------------------------------------------- 1 | d3.layout.trail = function() { 2 | 3 | var that = {}; //output object 4 | 5 | var time = function() {}, //how to access the time data (must be numeric--should but doesn't handle dates); 6 | currentTime, //points of this time will be display with full opacity; 7 | //later points are dropped; 8 | decayRange, //points of this age will have opacity 0. If either currentTime or decayRange is not defined, opacity will be added as some undefined values. 9 | data, // the data being arranged 10 | positioner, // a function that returns the [x,y] for the point. 11 | sort, // a function specifying the sort order 12 | coordType = 'coordinates', //either "coordinates" or "xy"; if the first, returns a "coordinates" array; if the latter, returns x1,y1,x2,y2 13 | grouping; // a function to split the data up into multiple segments; 14 | 15 | grouping = function(d) { 16 | return 1 17 | } 18 | 19 | positioner = function(datum) { 20 | //given a datum, returns an [x,y] array. 21 | //Might be a projection, for example, or a scale output. 22 | return [datum.x,datum.y] 23 | } 24 | 25 | 26 | lineToSegments = function(values) { 27 | //the returned array will be filtered to only include segments that fit the defined values. 28 | 29 | if (currentTime != undefined & decayRange != undefined) { 30 | values = values.filter(function(d) { 31 | return (time(d) <= currentTime && time(d) >= (currentTime-decayRange)) 32 | }) 33 | } 34 | 35 | 36 | 37 | values = d3 38 | .nest() 39 | .key(function(d) {return grouping(d)}) 40 | .entries(values); 41 | 42 | tmp = values; 43 | 44 | output = []; 45 | 46 | 47 | var i = 0 48 | values.forEach(function(element) { 49 | i++; 50 | if (sort!=undefined) { 51 | element.values.sort(sort) 52 | } 53 | if (i==1) { 54 | //console.log(element) 55 | } 56 | var values = element.values; 57 | 58 | for (var i = 0; i < (values.length); i++) { 59 | var current = values[i]; 60 | if (values[i+1] != undefined) { 61 | current.next = values[i+1] 62 | } else { 63 | current.next = {} 64 | } 65 | if (values[i-1] != undefined) { 66 | current.previous = values[i-1] 67 | if (coordType=="coordinates") { 68 | current.coordinates = [ 69 | positioner(values[i-1]), 70 | positioner(values[i]) 71 | ] 72 | } else if (coordType=="xy") { 73 | var a = positioner(values[i-1]), 74 | b = positioner(values[i]); 75 | current.x1=a[0] 76 | current.y1=a[1] 77 | current.x2=b[0] 78 | current.y2=b[1] 79 | } 80 | current.type = "LineString"; 81 | //opacity will probably be this: the percentage of the decay range ago that it was. 82 | //Early tests should guarantee a result between 0 and 1. 83 | } 84 | current.opacity = 1-(currentTime-time(current))/decayRange 85 | } 86 | output = output.concat(values); 87 | }) 88 | return output; 89 | } 90 | 91 | that.layout = function() { 92 | output = lineToSegments(data); 93 | return output; 94 | } 95 | 96 | that.coordType = function(x) { 97 | if (!arguments.length) return coordType; 98 | coordType= x 99 | return that 100 | } 101 | 102 | that.grouping = function(x) { 103 | if (!arguments.length) return grouping; 104 | grouping= x 105 | return that 106 | } 107 | 108 | that.time = function(x) { 109 | if (!arguments.length) return time; 110 | time = x 111 | return that 112 | } 113 | 114 | that.currentTime = function(x) { 115 | if (!arguments.length) return currentTime; 116 | currentTime= x 117 | return that 118 | } 119 | 120 | that.decayRange = function(x) { 121 | if (!arguments.length) return decayRange; 122 | decayRange= x 123 | return that 124 | } 125 | 126 | that.data = function(x,append) { 127 | if (!arguments.length) return data; 128 | 129 | if (append) { 130 | data = data || []; 131 | data = data.concat(x) 132 | } else { 133 | data = x 134 | } 135 | 136 | return that 137 | } 138 | 139 | that.positioner = function(x) { 140 | if (!arguments.length) return positioner; 141 | positioner = x 142 | return that 143 | } 144 | that.sort = function(x) { 145 | if (!arguments.length) return sort; 146 | sort= x 147 | return that 148 | } 149 | 150 | 151 | 152 | return that; 153 | 154 | } 155 | --------------------------------------------------------------------------------