├── .gitignore ├── LICENSE ├── README.md ├── d3.timeline.js ├── examples ├── art_movements.json ├── child_example.html ├── date_example.html ├── int_bands.csv ├── int_example.html ├── procedural.html └── wars.csv ├── index.js ├── package.json ├── src └── timeline.js └── timeline.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.dat 3 | *.iml 4 | *.log 5 | *.out 6 | *.pid 7 | *.seed 8 | *.sublime-* 9 | *.swo 10 | *.swp 11 | *.tgz 12 | *.xml 13 | .DS_Store 14 | .idea 15 | .project 16 | .strong-pm 17 | .gradle 18 | .nyc_output/ 19 | coverage 20 | node_modules 21 | bower_components 22 | npm-debug.log 23 | doc/ 24 | client/dist/* 25 | *-compiled* 26 | build 27 | /logs/*.log 28 | /public/img/ 29 | /public/fonts/ 30 | /public/css/ 31 | /public/js/ 32 | /public/json/assets-map.json 33 | /abacuseditor-cache/ 34 | *.mergeme 35 | /src/ 36 | npm-debug.log* 37 | /stats.json 38 | /node_modules 39 | /._npm 40 | .npm 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # d3.layout.timeline 2 | 3 | Timelines come in all shapes and sizes. One of the most common and popular is the kind that represents the duration of events as bands and tries to efficiently pack them into discrete lanes (sometimes called a swimlane chart). This layout takes an array of data with start and end points and creates the data necessary to draw the data as bands on a timeline. 4 | 5 | ![d3.layout.timeline](timeline.png "d3.layout.timeline") 6 | 7 | `npm install d3-layout-timeline` 8 | 9 | [Simple example using integers for start and end](http://bl.ocks.org/emeeks/d24171dac80dd535521b) 10 | 11 | [Example showing padding, maximum band height and fixed extent](http://bl.ocks.org/emeeks/221c931a1cd8c040d8e7) 12 | 13 | [Example with dates](https://gist.github.com/emeeks/280cb0607c68faf30bb5) 14 | 15 | [Example with dates, categorized to create parallel semantically meaningful timelines](http://bl.ocks.org/emeeks/3184af35f4937d878ac0) 16 | 17 | [Example with hierarchical data](http://bl.ocks.org/emeeks/a666ed4846a7f8bb334d) 18 | 19 | **#timeline(data)** Returns an array of objects based on `data` with drawing instructions. 20 | 21 | **#timeline.size** Sets or gets an array of [width, height] that is used to calculate the location of the timeline bands along with their start position (`d.start`), y-position (`d.y`) height (`d.dy`) and end (`d.end`). Width can be calculated by subtracting start from end for `svg:rect` elements. 22 | 23 | **#timeline.dateFormat** Sets or gets the function that returns the values for the start and end of the bands. Defaults to `function (d) {return new Date(d)}`. The timeline layout can plot floats and ints for relative time (see the simple example that uses integer positions for start and end points). 24 | 25 | **#timeline.bandStart** Sets or gets the function that returns the start of the band. Remember that it will also be passed through `#timeline.dateFormat`. Defaults to `function (d) {return d.start}`. 26 | 27 | **#timeline.bandEnd** Sets or gets the function that returns the start of the band. Remember that it will also be passed through `#timeline.dateFormat`. Defaults to `function (d) {return d.end}`. If you want to use duration-based notation, you might try something like `function (d) {return d.start + d.duration}`. 28 | 29 | **#timeline.extent** Sets or gets extent of the timeline. By default, the extent is set to the minimum start and maximum end, but if you have a range you'd rather set the timeline to, you can do so. This is also passed through `#timeline.dateFormat`. 30 | 31 | **#timeline.padding** Sets or gets the distance in pixels between lanes. Defaults to `0`. 32 | 33 | **#timeline.maxBandHeight** Sets or gets the maximum band height. Defaults to `Infinity` (bands will fill the given height in the `#timeline.size` array minus any necessary padding). 34 | 35 | **#timeline.children** Sets or gets the children accessor, for use with hierarchical timeline data. Typically children are stored in an array in `.children` or `.values`. Set to return null or false to disable hierarchical support. Hierarchical data can be a hierarchical JSON object (like the ubiquitous flare.json dataset) or an array of objects with each having child elements. 36 | 37 | If you're looking for a more sophisticated method of visualizing temporal data, you can check out [Topotime](http://dh.stanford.edu/topotime/). -------------------------------------------------------------------------------- /d3.timeline.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | d3.timeline = function() { 3 | var timelines = []; 4 | var dateAccessor = function(d) { 5 | return new Date(d); 6 | }; 7 | var processedTimelines = []; 8 | var startAccessor = function(d) { 9 | return d.start; 10 | }; 11 | var endAccessor = function(d) { 12 | return d.end; 13 | }; 14 | var size = [500, 100]; 15 | var timelineExtent = [-Infinity, Infinity]; 16 | var setExtent = []; 17 | var displayScale = d3.scaleLinear(); 18 | var swimlanes = { root: [] }; 19 | var swimlaneNumber = 1; 20 | var padding = 0; 21 | var fixedExtent = false; 22 | var maximumHeight = Infinity; 23 | var childAccessor = function(d) { 24 | return null; 25 | }; 26 | var bandID = 1; 27 | var projectedHierarchy = { id: "root", values: [] }; 28 | 29 | function processTimelines(timelines, parentBand) { 30 | if ( 31 | !Array.isArray(timelines) && 32 | Array.isArray(childAccessor(timelines)) 33 | ) { 34 | var rootnode = { id: 0, level: 0 }; 35 | for (var x in timelines) { 36 | if (timelines.hasOwnProperty(x)) { 37 | rootnode[x] = timelines[x]; 38 | } 39 | } 40 | rootnode.id = 0; 41 | rootnode.level = 0; 42 | rootnode.values = []; 43 | projectedHierarchy = rootnode; 44 | processTimelines(childAccessor(timelines), rootnode); 45 | rootnode.start = d3.min(rootnode.values, function(d) { 46 | return d.start; 47 | }); 48 | rootnode.end = d3.max(rootnode.values, function(d) { 49 | return d.end; 50 | }); 51 | processedTimelines.push(rootnode); 52 | 53 | return; 54 | } 55 | 56 | timelines.forEach(function(band) { 57 | var projectedBand = { level: 0, id: bandID }; 58 | 59 | if (parentBand !== undefined) { 60 | projectedBand.parent = parentBand; 61 | } 62 | bandID++; 63 | 64 | for (var x in band) { 65 | if (band.hasOwnProperty(x)) { 66 | projectedBand[x] = band[x]; 67 | } 68 | } 69 | 70 | if (Array.isArray(childAccessor(band))) { 71 | processTimelines(childAccessor(band), projectedBand); 72 | projectedBand.start = d3.min(projectedBand.values, function(d) { 73 | return d.start; 74 | }); 75 | projectedBand.end = d3.max(projectedBand.values, function(d) { 76 | return d.end; 77 | }); 78 | } else { 79 | projectedBand.start = dateAccessor(startAccessor(band)); 80 | projectedBand.end = dateAccessor(endAccessor(band)); 81 | } 82 | 83 | projectedBand.lane = 0; 84 | processedTimelines.push(projectedBand); 85 | if (parentBand) { 86 | if (!parentBand.values) { 87 | parentBand.values = []; 88 | } 89 | parentBand.values.push(projectedBand); 90 | } 91 | if (parentBand === undefined) { 92 | projectedHierarchy.values.push(projectedBand); 93 | } 94 | }); 95 | } 96 | 97 | function projectTimelines() { 98 | if (fixedExtent === false) { 99 | var minStart = d3.min(processedTimelines, function(d) { 100 | return d.start; 101 | }); 102 | var maxEnd = d3.max(processedTimelines, function(d) { 103 | return d.end; 104 | }); 105 | timelineExtent = [minStart, maxEnd]; 106 | } else { 107 | timelineExtent = [ 108 | dateAccessor(setExtent[0]), 109 | dateAccessor(setExtent[1]) 110 | ]; 111 | } 112 | 113 | displayScale.domain(timelineExtent).range([0, size[0]]); 114 | 115 | processedTimelines.forEach(function(band) { 116 | band.originalStart = band.start; 117 | band.originalEnd = band.end; 118 | band.start = displayScale(band.start); 119 | band.end = displayScale(band.end); 120 | }); 121 | } 122 | 123 | function fitsIn(lane, band) { 124 | if (lane.end < band.start || lane.start > band.end) { 125 | return true; 126 | } 127 | var filteredLane = lane.filter(function(d) { 128 | return d.start <= band.end && d.end >= band.start; 129 | }); 130 | if (filteredLane.length === 0) { 131 | return true; 132 | } 133 | return false; 134 | } 135 | 136 | function findlane(band) { 137 | //make the first array 138 | var swimlane = swimlanes["root"]; 139 | if (band.parent) { 140 | swimlane = swimlanes[band.parent.id]; 141 | } 142 | 143 | if (swimlane === undefined) { 144 | swimlanes[band.parent.id] = [[band]]; 145 | swimlane = swimlanes[band.parent.id]; 146 | swimlaneNumber++; 147 | return; 148 | } 149 | var l = swimlane.length - 1; 150 | var x = 0; 151 | 152 | while (x <= l) { 153 | if (fitsIn(swimlane[x], band)) { 154 | swimlane[x].push(band); 155 | return; 156 | } 157 | x++; 158 | } 159 | swimlane[x] = [band]; 160 | return; 161 | } 162 | 163 | function timeline(data) { 164 | if (!arguments.length) return timeline; 165 | 166 | projectedHierarchy = { id: "root", values: [] }; 167 | 168 | processedTimelines = []; 169 | swimlanes = { root: [] }; 170 | 171 | processTimelines(data); 172 | projectTimelines(data); 173 | 174 | processedTimelines.forEach(function(band) { 175 | findlane(band); 176 | }); 177 | 178 | for (var x in swimlanes) { 179 | swimlanes[x].forEach(function(lane, i) { 180 | var height = size[1] / swimlanes[x].length; 181 | height = Math.min(height, maximumHeight); 182 | lane.forEach(function(band) { 183 | band.y = i * height + padding / 2; 184 | band.dy = height - padding; 185 | band.lane = i; 186 | band.dyp = 1 / swimlanes[x].length; 187 | }); 188 | }); 189 | } 190 | 191 | projectedHierarchy.values.forEach(relativePosition); 192 | 193 | processedTimelines.sort(function(a, b) { 194 | if (a.level > b.level) { 195 | return 1; 196 | } 197 | if (a.level < b.level) { 198 | return -1; 199 | } 200 | return 1; 201 | }); 202 | 203 | return processedTimelines; 204 | } 205 | 206 | function relativePosition(band, i) { 207 | if (!band.parent) { 208 | band.level = 0; 209 | } else { 210 | band.level = band.parent.level + 1; 211 | var height = band.dyp * band.parent.dy; 212 | band.y = band.parent.y + band.lane * height + padding / 2; 213 | band.dy = Math.max(1, height - padding); 214 | } 215 | if (band.values) { 216 | band.values.forEach(relativePosition); 217 | } 218 | } 219 | 220 | timeline.childAccessor = function(_x) { 221 | if (!arguments.length) return childAccessor; 222 | childAccessor = _x; 223 | return timeline; 224 | }; 225 | 226 | timeline.dateFormat = function(_x) { 227 | if (!arguments.length) return dateAccessor; 228 | dateAccessor = _x; 229 | return timeline; 230 | }; 231 | 232 | timeline.bandStart = function(_x) { 233 | if (!arguments.length) return startAccessor; 234 | startAccessor = _x; 235 | return timeline; 236 | }; 237 | 238 | timeline.bandEnd = function(_x) { 239 | if (!arguments.length) return endAccessor; 240 | endAccessor = _x; 241 | return timeline; 242 | }; 243 | 244 | timeline.size = function(_x) { 245 | if (!arguments.length) return size; 246 | size = _x; 247 | return timeline; 248 | }; 249 | 250 | timeline.padding = function(_x) { 251 | if (!arguments.length) return padding; 252 | padding = _x; 253 | return timeline; 254 | }; 255 | 256 | timeline.extent = function(_x) { 257 | if (!arguments.length) return timelineExtent; 258 | fixedExtent = true; 259 | setExtent = _x; 260 | if (_x.length === 0) { 261 | fixedExtent = false; 262 | } 263 | return timeline; 264 | }; 265 | 266 | timeline.maxBandHeight = function(_x) { 267 | if (!arguments.length) return maximumHeight; 268 | maximumHeight = _x; 269 | return timeline; 270 | }; 271 | 272 | return timeline; 273 | }; 274 | })(); 275 | -------------------------------------------------------------------------------- /examples/art_movements.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Art Movements", 3 | "children": [ 4 | {"label": "Renaissance", 5 | "children": [ 6 | {"label": "Italian Renaissance", 7 | "start": 1275, 8 | "end": 1602}, 9 | {"label": "Renaissance Classicism", 10 | "start": 1475, 11 | "end": 1602}, 12 | {"label": "Early Netherlandish Painting", 13 | "start": 1400, 14 | "end": 1500} 15 | ] 16 | }, 17 | {"label": "Renaissance to Neoclassicism", 18 | "children": [ 19 | {"label": "Mannerism and Late Renaissance", 20 | "start": 1520, 21 | "end": 1600}, 22 | {"label": "Boroque", 23 | "start": 1600, 24 | "end": 1730}, 25 | {"label": "Dutch Golden Age Painting", 26 | "start": 1702, 27 | "end": 1730}, 28 | {"label": "Flemish Boroque Painting", 29 | "start": 1585, 30 | "end": 1700}, 31 | {"label": "Rococo", 32 | "start": 1720, 33 | "end": 1780}, 34 | {"label": "Neoclassicism", 35 | "start": 1750, 36 | "end": 1830} 37 | ] 38 | }, 39 | {"label": "Romanticism", 40 | "children": [ 41 | {"label": "Nazarene Movement", 42 | "start": 1820, 43 | "end": 1847}, 44 | {"label": "The Ancients", 45 | "start": 1825, 46 | "end": 1845}, 47 | {"label": "Purismo", 48 | "start": 1825, 49 | "end": 1845}, 50 | {"label": "Düsseldorf school", 51 | "start": 1825, 52 | "end": 1865}, 53 | {"label": "Hudson River school", 54 | "start": 1855, 55 | "end": 1880}, 56 | {"label": "Luminism", 57 | "start": 1855, 58 | "end": 1875} 59 | ] 60 | }, 61 | {"label": "Romanticism to Modern Art", 62 | "children": [ 63 | {"label": "Norwich school", 64 | "start": 1803, 65 | "end": 1833}, 66 | {"label": "Biedermeier", 67 | "start": 1815, 68 | "end": 1848}, 69 | {"label": "Photography", 70 | "start": 1830, 71 | "end": 2015}, 72 | {"label": "Realism", 73 | "start": 1830, 74 | "end": 1870}, 75 | {"label": "Barbizon school ", 76 | "start": 1830, 77 | "end": 1870}, 78 | {"label": "Peredvizhniki", 79 | "start": 1870, 80 | "end": 1923}, 81 | {"label": "Hague School", 82 | "start": 1870, 83 | "end": 1900}, 84 | {"label": "American Barbizon school", 85 | "start": 1853, 86 | "end": 1895}, 87 | {"label": "Spanish Eclecticism", 88 | "start": 1845, 89 | "end": 1890}, 90 | {"label": "Macchiaioli", 91 | "start": 1850, 92 | "end": 1859}, 93 | {"label": "Pre-Raphaelite Brotherhood", 94 | "start": 1848, 95 | "end": 1854} 96 | ] 97 | }, 98 | {"label": "Modern Art", 99 | "children": [ 100 | {"label": "Impressionism", 101 | "start": 1860, 102 | "end": 1890, 103 | "children": [ 104 | {"label": "American Impressionism", 105 | "start": 1880, 106 | "end": 1890}, 107 | {"label": "Cos Cob Art Colony", 108 | "start": 1890, 109 | "end": 1899}, 110 | {"label": "Heidelberg School", 111 | "start": 1887, 112 | "end": 1890} 113 | ]}, 114 | {"label": "Arts and Crafts", 115 | "start": 1880, 116 | "end": 1910}, 117 | {"label": "Tonalism", 118 | "start": 1880, 119 | "end": 1920}, 120 | {"label": "Symbolism", 121 | "start": 1880, 122 | "end": 1910, 123 | "children": [ 124 | {"label": "Russian Symbolism", 125 | "start": 1884, 126 | "end": 1910}, 127 | {"label": "Aesthetic Movement", 128 | "start": 1868, 129 | "end": 1901} 130 | ]}, 131 | {"label": "Post-Impressionism", 132 | "start": 1886, 133 | "end": 1905, 134 | "children": [ 135 | {"label": "Les Nabis", 136 | "start": 1888, 137 | "end": 1900}, 138 | {"label": "Cloisonnism", 139 | "start": 1884, 140 | "end": 1886}, 141 | {"label": "Synthetism", 142 | "start": 1887, 143 | "end": 1893} 144 | ]}, 145 | {"label": "Neo-Impressionism", 146 | "start": 1886, 147 | "end": 1906, 148 | "children": [ 149 | {"label": "Pointillism", 150 | "start": 1879, 151 | "end": 1906}, 152 | {"label": "Divisionism", 153 | "start": 1880, 154 | "end": 1880} 155 | ]}, 156 | {"label": "Art Nouveau", 157 | "start": 1890, 158 | "end": 1914, 159 | "children": [ 160 | {"label": "Vienna Secession", 161 | "start": 1897, 162 | "end": 1914}, 163 | {"label": "Jugendstil", 164 | "start": 1890, 165 | "end": 1914}, 166 | {"label": "Modernisme", 167 | "start": 1890, 168 | "end": 1910} 169 | ]}, 170 | {"label": "Russian avant-garde", 171 | "start": 1890, 172 | "end": 1930}, 173 | {"label": "Art à la Rue", 174 | "start": 1890, 175 | "end": 1905}, 176 | {"label": "Young Poland", 177 | "start": 1890, 178 | "end": 1918}, 179 | {"label": "Mir iskusstva", 180 | "start": 1889, 181 | "end": 1900}, 182 | {"label": "Hagenbund", 183 | "start": 1900, 184 | "end": 1930}, 185 | {"label": "Fauvism", 186 | "start": 1904, 187 | "end": 1909}, 188 | {"label": "Expressionism", 189 | "start": 1905, 190 | "end": 1930}, 191 | {"label": "Die Brücke", 192 | "start": 1905, 193 | "end": 1913}, 194 | {"label": "Der Blaue Reiter", 195 | "start": 1911, 196 | "end": 1912}, 197 | {"label": "Bloomsbury Group", 198 | "start": 1905, 199 | "end": 1945} 200 | ] 201 | }, 202 | {"label": "Contemporary Art", 203 | "children": [ 204 | {"label": "Vienna School of Fantastic Realism", 205 | "start": 1946, 206 | "end": 1947}, 207 | {"label": "Neo-Dada", 208 | "start": 1950, 209 | "end": 1959}, 210 | {"label": "International Typographic Style", 211 | "start": 1950, 212 | "end": 1959}, 213 | {"label": "Soviet Nonconformist Art", 214 | "start": 1953, 215 | "end": 1986}, 216 | {"label": "Painters Eleven", 217 | "start": 1954, 218 | "end": 1960}, 219 | {"label": "Pop Art", 220 | "start": 1953, 221 | "end": 1957}, 222 | {"label": "Woodlands School", 223 | "start": 1958, 224 | "end": 1962}, 225 | {"label": "Situationism", 226 | "start": 1957, 227 | "end": 1973}, 228 | {"label": "New realism", 229 | "start": 1960, 230 | "end": 2015}, 231 | {"label": "Magic realism", 232 | "start": 1960, 233 | "end": 1969}, 234 | {"label": "Minimalism", 235 | "start": 1960, 236 | "end": 2015}, 237 | {"label": "Hard-edge painting", 238 | "start": 1960, 239 | "end": 1963}, 240 | {"label": "Fluxus", 241 | "start": 1960, 242 | "end": 1977}, 243 | {"label": "Happening", 244 | "start": 1960, 245 | "end": 2015} 246 | ]} 247 | ] 248 | 249 | } -------------------------------------------------------------------------------- /examples/child_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Timeline with Dates 5 | 6 | 17 | 18 | 19 | 20 | 21 | 22 | 70 | 71 | 72 |
73 | 74 | 75 |
76 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /examples/date_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Timeline with Dates 5 | 6 | 17 | 18 | 19 | 20 | 21 | 22 | 47 | 48 | 49 |
50 | 51 | 52 |
53 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /examples/int_bands.csv: -------------------------------------------------------------------------------- 1 | s,e 2 | 1,7 3 | 1,6 4 | 10,15 5 | 7,8 6 | 4,5 7 | 5,6 8 | 8,9 9 | 3,10 10 | 10,15 11 | 4,8 12 | 10,12 13 | 3,8 14 | 5,12 15 | 4,12 -------------------------------------------------------------------------------- /examples/int_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Timeline with Integers 5 | 6 | 17 | 18 | 19 | 20 | 21 | 22 | 45 | 46 | 47 |
48 | 49 | 50 |
51 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/procedural.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Timeline with Procedural Data 5 | 6 | 17 | 18 | 19 | 20 | 21 | 22 | 70 | 71 | 72 |
73 | 74 | 75 |
76 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /examples/wars.csv: -------------------------------------------------------------------------------- 1 | name,start,end,sphere 2 | "American Revolutionary War","4-19-1775","9-3-1783","European" 3 | "Cherokee–American wars","01-01-1776","12-31-1795","Native" 4 | "Northwest Indian War","01-01-1785","12-31-1795","Native" 5 | "Whiskey Rebellion","01-01-1795","12-31-1795","Internal" 6 | "Quasi-War","01-01-1798","12-31-1800","European" 7 | "First Barbary War","5-10-1801","6-10-1805","Colonial" 8 | "Tecumseh's War","01-01-1811","12-31-1811","Native" 9 | "War of 1812","6-18-1812","2-18-1815","European" 10 | "Red Stick War","01-01-1813","8-31-1814","Native" 11 | "Second Barbary War","06-17-1815","06-19-1815","Colonial" 12 | "First Seminole War","01-01-1817","12-31-1818","Native" 13 | "Texas-Indian Wars","01-01-1820","12-31-1875","Native" 14 | "Arikara War","01-01-1823","12-31-1823","Native" 15 | "Aegean Sea Anti-Piracy Operations","01-01-1825","12-31-1828","Colonial" 16 | "Winnebago War","01-01-1827","12-31-1827","Native" 17 | "First Sumatran expedition","01-01-1832","12-31-1832","Colonial" 18 | "Black Hawk War","01-01-1832","12-31-1832","Native" 19 | "Second Seminole War","01-01-1835","12-31-1842","Native" 20 | "Patriot War","01-01-1838","12-31-1838","European" 21 | "United States Exploring Expedition","01-01-1838","12-31-1842","Colonial" 22 | "Second Sumatran expedition","01-01-1838","12-31-1838","Colonial" 23 | "Mexican–American War","01-01-1846","12-31-1848","Latin America" 24 | "Cayuse War","01-01-1847","12-31-1855","Native" 25 | "Taiping Rebellion","01-01-1850","12-31-1864","Colonial" 26 | "Apache Wars","01-01-1851","12-31-1900","Native" 27 | "Bombardment of Greytown","01-01-1854","12-31-1854","European" 28 | "Puget Sound War","01-01-1855","12-31-1856","Native" 29 | "First Fiji Expedition","01-01-1855","12-31-1855","Colonial" 30 | "Rogue River Wars","01-01-1855","12-31-1856","Native" 31 | "Third Seminole War","01-01-1855","12-31-1858","Native" 32 | "Yakima War","01-01-1855","12-31-1858","Native" 33 | "Filibuster War","01-01-1856","12-31-1857","Latin America" 34 | "Second Opium War","01-01-1856","12-31-1859","Colonial" 35 | "Utah War","01-01-1857","12-31-1858","Internal" 36 | "Navajo Wars","01-01-1858","12-31-1866","Native" 37 | "Second Fiji Expedition","01-01-1858","12-31-1858","Colonial" 38 | "First and Second Cortina War","01-01-1859","12-31-1861","Latin America" 39 | "Paiute War","01-01-1860","12-31-1860","Native" 40 | "Reform War","01-01-1860","12-31-1860","Latin America" 41 | "American Civil War","01-01-1861","12-31-1865","Internal" 42 | "Bombardment of Qui Nhơn","01-01-1861","12-31-1861","Colonial" 43 | "Yavapai Wars","01-01-1861","12-31-1875","Native" 44 | "Dakota War of 1862","01-01-1862","12-31-1862" 45 | "Colorado War","01-01-1863","12-31-1865","Native" 46 | "Shimonoseki War","01-01-1863","12-31-1864","Colonial" 47 | "Snake War","01-01-1864","12-31-1868","Native" 48 | "Powder River War","01-01-1865","12-31-1865","Native" 49 | "Red Cloud's War","01-01-1866","12-31-1868","Native" 50 | "Siege of Mexico City","01-01-1867","12-31-1867","Latin America" 51 | "Formosa Expedition","01-01-1867","12-31-1867","Colonial" 52 | "Comanche Campaign","01-01-1867","12-31-1875","Native" 53 | "United States expedition to Korea","01-01-1871","12-31-1871","Colonial" 54 | "Modoc War","01-01-1872","12-31-1873","Native" 55 | "Red River War","01-01-1874","12-31-1875","Native" 56 | "Las Cuevas War","01-01-1875","12-31-1875","Latin America" 57 | "Great Sioux War of 1876","01-01-1876","12-31-1877","Native" 58 | "Buffalo Hunters' War","01-01-1876","12-31-1877","Native" -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | timeline = require("./src/timeline"); 2 | 3 | module.exports = timeline; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-layout-timeline", 3 | "version": "2.0.0", 4 | "description": "A layout for producing band-style timelines", 5 | "main": "index.js", 6 | "keywords": ["d3", "layout", "hierarchical", "temporal"], 7 | "directories": { 8 | "examples": "examples" 9 | }, 10 | "dependencies": { 11 | "d3-array": "1.2.0", 12 | "d3-scale": "1.0.6" 13 | }, 14 | "devDependencies": {}, 15 | "scripts": {}, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/emeeks/d3.layout.timeline" 19 | }, 20 | "author": { 21 | "name": "Elijah Meeks", 22 | "url": "http://www.elijahmeeks.com" 23 | }, 24 | "license": "Unlicense", 25 | "bugs": { 26 | "url": "https://github.com/emeeks/d3.layout.timeline/issues" 27 | }, 28 | "homepage": "https://github.com/emeeks/d3.layout.timeline" 29 | } 30 | -------------------------------------------------------------------------------- /src/timeline.js: -------------------------------------------------------------------------------- 1 | import { min, max } from "d3-array"; 2 | import { scaleLinear } from "d3-scale"; 3 | 4 | module.exports = function() { 5 | let timelines = []; 6 | let dateAccessor = function(d) { 7 | return new Date(d); 8 | }; 9 | let processedTimelines = []; 10 | let startAccessor = function(d) { 11 | return d.start; 12 | }; 13 | let endAccessor = function(d) { 14 | return d.end; 15 | }; 16 | let size = [500, 100]; 17 | let timelineExtent = [-Infinity, Infinity]; 18 | let setExtent = []; 19 | let displayScale = scaleLinear(); 20 | let swimlanes = { root: [] }; 21 | let swimlaneNumber = 1; 22 | let padding = 0; 23 | let fixedExtent = false; 24 | let maximumHeight = Infinity; 25 | let childAccessor = function(d) { 26 | return null; 27 | }; 28 | let bandID = 1; 29 | let projectedHierarchy = { id: "root", values: [] }; 30 | 31 | function processTimelines(timelines, parentBand) { 32 | if (!Array.isArray(timelines) && Array.isArray(childAccessor(timelines))) { 33 | let rootnode = { id: 0, level: 0 }; 34 | for (let x in timelines) { 35 | if (timelines.hasOwnProperty(x)) { 36 | rootnode[x] = timelines[x]; 37 | } 38 | } 39 | rootnode.id = 0; 40 | rootnode.level = 0; 41 | rootnode.values = []; 42 | projectedHierarchy = rootnode; 43 | processTimelines(childAccessor(timelines), rootnode); 44 | rootnode.start = min(rootnode.values, function(d) { 45 | return d.start; 46 | }); 47 | rootnode.end = max(rootnode.values, function(d) { 48 | return d.end; 49 | }); 50 | processedTimelines.push(rootnode); 51 | 52 | return; 53 | } 54 | 55 | timelines.forEach(function(band) { 56 | let projectedBand = { level: 0, id: bandID }; 57 | 58 | if (parentBand !== undefined) { 59 | projectedBand.parent = parentBand; 60 | } 61 | bandID++; 62 | 63 | for (let x in band) { 64 | if (band.hasOwnProperty(x)) { 65 | projectedBand[x] = band[x]; 66 | } 67 | } 68 | 69 | if (Array.isArray(childAccessor(band))) { 70 | processTimelines(childAccessor(band), projectedBand); 71 | projectedBand.start = min(projectedBand.values, function(d) { 72 | return d.start; 73 | }); 74 | projectedBand.end = max(projectedBand.values, function(d) { 75 | return d.end; 76 | }); 77 | } else { 78 | projectedBand.start = dateAccessor(startAccessor(band)); 79 | projectedBand.end = dateAccessor(endAccessor(band)); 80 | } 81 | 82 | projectedBand.lane = 0; 83 | processedTimelines.push(projectedBand); 84 | if (parentBand) { 85 | if (!parentBand.values) { 86 | parentBand.values = []; 87 | } 88 | parentBand.values.push(projectedBand); 89 | } 90 | if (parentBand === undefined) { 91 | projectedHierarchy.values.push(projectedBand); 92 | } 93 | }); 94 | } 95 | 96 | function projectTimelines() { 97 | if (fixedExtent === false) { 98 | let minStart = min(processedTimelines, function(d) { 99 | return d.start; 100 | }); 101 | let maxEnd = max(processedTimelines, function(d) { 102 | return d.end; 103 | }); 104 | timelineExtent = [minStart, maxEnd]; 105 | } else { 106 | timelineExtent = [dateAccessor(setExtent[0]), dateAccessor(setExtent[1])]; 107 | } 108 | 109 | displayScale.domain(timelineExtent).range([0, size[0]]); 110 | 111 | processedTimelines.forEach(function(band) { 112 | band.originalStart = band.start; 113 | band.originalEnd = band.end; 114 | band.start = displayScale(band.start); 115 | band.end = displayScale(band.end); 116 | }); 117 | } 118 | 119 | function fitsIn(lane, band) { 120 | if (lane.end < band.start || lane.start > band.end) { 121 | return true; 122 | } 123 | let filteredLane = lane.filter(function(d) { 124 | return d.start <= band.end && d.end >= band.start; 125 | }); 126 | if (filteredLane.length === 0) { 127 | return true; 128 | } 129 | return false; 130 | } 131 | 132 | function findlane(band) { 133 | //make the first array 134 | let swimlane = swimlanes["root"]; 135 | if (band.parent) { 136 | swimlane = swimlanes[band.parent.id]; 137 | } 138 | 139 | if (swimlane === undefined) { 140 | swimlanes[band.parent.id] = [[band]]; 141 | swimlane = swimlanes[band.parent.id]; 142 | swimlaneNumber++; 143 | return; 144 | } 145 | let l = swimlane.length - 1; 146 | let x = 0; 147 | 148 | while (x <= l) { 149 | if (fitsIn(swimlane[x], band)) { 150 | swimlane[x].push(band); 151 | return; 152 | } 153 | x++; 154 | } 155 | swimlane[x] = [band]; 156 | return; 157 | } 158 | 159 | function timeline(data) { 160 | if (!arguments.length) return timeline; 161 | 162 | projectedHierarchy = { id: "root", values: [] }; 163 | 164 | processedTimelines = []; 165 | swimlanes = { root: [] }; 166 | 167 | processTimelines(data); 168 | projectTimelines(data); 169 | 170 | processedTimelines.forEach(function(band) { 171 | findlane(band); 172 | }); 173 | 174 | for (let x in swimlanes) { 175 | swimlanes[x].forEach(function(lane, i) { 176 | let height = size[1] / swimlanes[x].length; 177 | height = Math.min(height, maximumHeight); 178 | lane.forEach(function(band) { 179 | band.y = i * height + padding / 2; 180 | band.dy = height - padding; 181 | band.lane = i; 182 | band.dyp = 1 / swimlanes[x].length; 183 | }); 184 | }); 185 | } 186 | 187 | projectedHierarchy.values.forEach(relativePosition); 188 | 189 | processedTimelines.sort(function(a, b) { 190 | if (a.level > b.level) { 191 | return 1; 192 | } 193 | if (a.level < b.level) { 194 | return -1; 195 | } 196 | return 1; 197 | }); 198 | 199 | return processedTimelines; 200 | } 201 | 202 | function relativePosition(band, i) { 203 | if (!band.parent) { 204 | band.level = 0; 205 | } else { 206 | band.level = band.parent.level + 1; 207 | let height = band.dyp * band.parent.dy; 208 | band.y = band.parent.y + band.lane * height + padding / 2; 209 | band.dy = Math.max(1, height - padding); 210 | } 211 | if (band.values) { 212 | band.values.forEach(relativePosition); 213 | } 214 | } 215 | 216 | timeline.childAccessor = function(_x) { 217 | if (!arguments.length) return childAccessor; 218 | childAccessor = _x; 219 | return timeline; 220 | }; 221 | 222 | timeline.dateFormat = function(_x) { 223 | if (!arguments.length) return dateAccessor; 224 | dateAccessor = _x; 225 | return timeline; 226 | }; 227 | 228 | timeline.bandStart = function(_x) { 229 | if (!arguments.length) return startAccessor; 230 | startAccessor = _x; 231 | return timeline; 232 | }; 233 | 234 | timeline.bandEnd = function(_x) { 235 | if (!arguments.length) return endAccessor; 236 | endAccessor = _x; 237 | return timeline; 238 | }; 239 | 240 | timeline.size = function(_x) { 241 | if (!arguments.length) return size; 242 | size = _x; 243 | return timeline; 244 | }; 245 | 246 | timeline.padding = function(_x) { 247 | if (!arguments.length) return padding; 248 | padding = _x; 249 | return timeline; 250 | }; 251 | 252 | timeline.extent = function(_x) { 253 | if (!arguments.length) return timelineExtent; 254 | fixedExtent = true; 255 | setExtent = _x; 256 | if (_x.length === 0) { 257 | fixedExtent = false; 258 | } 259 | return timeline; 260 | }; 261 | 262 | timeline.maxBandHeight = function(_x) { 263 | if (!arguments.length) return maximumHeight; 264 | maximumHeight = _x; 265 | return timeline; 266 | }; 267 | 268 | return timeline; 269 | }; 270 | -------------------------------------------------------------------------------- /timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emeeks/d3.layout.timeline/175e31413aa1f6970d33aa646debc9522711c630/timeline.png --------------------------------------------------------------------------------