├── .gitignore ├── README.md ├── bower.json ├── examples ├── example.html ├── jackie.png ├── timeline1.png ├── timeline2.png ├── timeline3.png ├── timeline4.png ├── timeline5.png ├── timeline6.png ├── timeline7.png ├── timeline8.png ├── timeline9.png ├── troll.png └── wat.png ├── package.json └── src └── d3-timeline.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # d3-timeline 2 | A simple d3 timeline plugin. 3 | 4 | Get something that looks like 5 | 6 | ![Rectangular Timeline](examples/timeline1.png) 7 | 8 | for a dataset that looks like 9 | 10 | ```js 11 | var testData = [ 12 | {label: "person a", times: [ 13 | {"starting_time": 1355752800000, "ending_time": 1355759900000}, 14 | {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, 15 | {label: "person b", times: [ 16 | {"starting_time": 1355759910000, "ending_time": 1355761900000}]}, 17 | {label: "person c", times: [ 18 | {"starting_time": 1355761910000, "ending_time": 1355763910000}]} 19 | ]; 20 | ``` 21 | 22 | with a call that looks like 23 | 24 | ```js 25 | var chart = d3.timeline(); 26 | 27 | var svg = d3.select("#timeline1").append("svg").attr("width", 500) 28 | .datum(testData).call(chart); 29 | ``` 30 | 31 | Works with circles. In case the rectangular edges are too pointy. 32 | 33 | ![Circular Timeline](examples/timeline2.png) 34 | 35 | Combine rectangles and circles to your liking 36 | 37 | ![Rectangular and Circular Timeline](examples/timeline3.png) 38 | 39 | by adding a ``display`` key to the data: 40 | 41 | ```js 42 | var rectAndCircleTestData = [ 43 | {times: [{"starting_time": 1355752800000, "display": "circle"}, 44 | {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, 45 | {times: [{"starting_time": 1355759910000, "display":"circle"}, ]}, 46 | {times: [{"starting_time": 1355761910000, "ending_time": 1355763910000}]} 47 | ]; 48 | ``` 49 | 50 | Make a pseudo-gantt chart thingy 51 | 52 | ![Gantt chart](examples/timeline4.png) 53 | 54 | with icons 55 | 56 | ![Icon chart](examples/timeline5.png) 57 | 58 | For your *really* long charts, it supports scrolling. It can even do things on hover, click, and scroll for when someone accidentally interacts with your chart. 59 | 60 | You can also specify an optional `class` key in the data dictionary. This will label each timeline rectangle item within the visualization with the following id property: "timelineItem_"+class. For example, this data 61 | 62 | ```js 63 | var testData = [ 64 | {class: "pA", label: "person a", times: [ 65 | {"starting_time": 1355752800000, "ending_time": 1355759900000}, 66 | {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, 67 | {class: "pB", label: "person b", times: [ 68 | {"starting_time": 1355759910000, "ending_time": 1355761900000}]}, 69 | {class: "pC", label: "person c", times: [ 70 | {"starting_time": 1355761910000, "ending_time": 1355763910000}]} 71 | ]; 72 | ``` 73 | would generate `` with the following classes: `timelineItem_pA`,`timelineItem_pB`,`timelineItem_pC`. This means that you can dynamically change the visual properties of each timeline item using JQuery like so: `$(".timelineSeries_pA").css("fill","blue");`. 74 | If no custom class is provided, the class attribute will be generated sequentially in the order they have been provided in. e.g.: `timelineSeries_0`. 75 | 76 | Also optional is an `id` field per data element. 77 | 78 | ```js 79 | var testData = [ 80 | {label: "person a", times: [ 81 | {"starting_time": 1355752800000, "ending_time": 1355759900000, "id": "A1"}, 82 | {"starting_time": 1355767900000, "ending_time": 1355774400000, "id": "A2"}]} 83 | ]; 84 | ``` 85 | 86 | This generates ``s with `A1` and `A2` as ids. If no id is provided, the id attribute will be generated sequentially in the order they have been provided in. e.g.: `timelineItem_0_0`. 87 | 88 | Look at the [examples](examples/example.html) for more details. 89 | 90 | ## Data formats 91 | 92 | The simplest data format only requires `starting_time` and `ending_time` for each series of data. 93 | ```js 94 | [ 95 | {times: [ 96 | {"starting_time": 1355752800000, "ending_time": 1355759900000}, 97 | {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, 98 | {times: [ 99 | {"starting_time": 1355759910000, "ending_time": 1355761900000}]} 100 | ]; 101 | ``` 102 | 103 | `label` can be added if you want names by each series of data. In order for this to properly show up, the timeline needs to be called with .stack() 104 | ```js 105 | [ 106 | {label: "person a", times: [ 107 | {"starting_time": 1355752800000, "ending_time": 1355759900000}, 108 | {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, 109 | {label: "person b", times: [ 110 | {"starting_time": 1355759910000, "ending_time": 1355761900000}]} 111 | ]; 112 | ``` 113 | 114 | `icon` can be added if you want icons by each series of data. In order for this to properly show up, the timeline needs to be called with .stack(). Icons and labels can also be mixed in together. 115 | ```js 116 | [ 117 | {icon: "path/to/img.png", times: [ 118 | {"starting_time": 1355752800000, "ending_time": 1355759900000}, 119 | {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, 120 | {label: "person b", times: [ 121 | {"starting_time": 1355759910000, "ending_time": 1355761900000}]} 122 | ]; 123 | ``` 124 | 125 | ### 'times' elements array 126 | Each item in the times array must have `starting_time` and `ending_time`. You could also specify optional `color` or `label` elements within a times item, as well as a [property mapped to a color](#colorpropertypropertyname). 127 | ```js 128 | [ 129 | {label: "person a", times: [{"color":"green", "label":"Weeee", "starting_time": 1355752800000, "ending_time": 1355759900000}, {"color":"blue", "label":"Weeee", "starting_time": 1355767900000, "ending_time": 1355774400000}]}, 130 | {label: "person b", times: [{"color":"pink", "label":"Weeee", "starting_time": 1355759910000, "ending_time": 1355761900000}, ]}, 131 | {label: "person c", times: [{"color":"yellow", "label":"Weeee", "starting_time": 1355761910000, "ending_time": 1355763910000}]} 132 | ]; 133 | ``` 134 | 135 | 136 | ## Method Calls 137 | All methods that take in arguments return the current settings if no argument is passed. 138 | 139 | ### .width(width) 140 | sets the width of the timeline. If the width of the timeline is longer than the width of the svg object, the timeline will automatically scroll. The width of the timeline will default to the width of the svg if width is not set. 141 | 142 | ### .height(height) 143 | sets the height of the timeline. The height of the timeline will be automatically calculated from the height of each item if height is not set on the timeline or the svg. 144 | 145 | ### .itemHeight(height) 146 | sets the height of the data series in the timeline. Defaults to 20px. 147 | 148 | ### .itemMargin(height) 149 | sets the margin between the data series in the timeline. Defaults to 5px. 150 | 151 | ### .margin({left: , right: , top: , bottom: }) 152 | sets the margin of the entire timeline inside of the svg. Defaults to 30px all around. 153 | 154 | ### .display("circle" | "rect") 155 | Displays the data series as either circles or rectangles. Defaults to "rect". 156 | 157 | ### .labelFormat(callback) 158 | registers a function to be called when the text for the label needs to 159 | be generated. Useful if your label looks like this: 160 | ``` 161 | { 162 | en: "my label", 163 | fr: "mon étiquette" 164 | } 165 | ``` 166 | The callback function is passed the whatever the datum.label returns, so 167 | in this case it would be the object above. So the labelFormat might 168 | look something like this: 169 | ```js 170 | .labelFormat(function(label){ return label[currentLocale];}) 171 | ``` 172 | 173 | ### .tickFormat({format: , tickTime: , tickInterval: , tickSize: , numTicks: , tickValues}) 174 | sets the formatting of the ticks in the timeline. Defaults to 175 | ```js 176 | { 177 | format: d3.time.format("%I %p"), 178 | tickTime: d3.time.hours, 179 | tickInterval: 1, 180 | tickSize: 6 181 | } 182 | ``` 183 | Tick interval/values can be set with: 184 | 185 | - ``tickTime`` and ``tickInterval`` 186 | - ``numTicks`` and ``tickInterval`` 187 | - ``tickValues`` 188 | 189 | ### .rotateTicks(degrees) 190 | sets the degree of rotation of the tickmarks. Defaults to no rotation (0 degrees). 191 | 192 | ### .orient("bottom" | "top") 193 | sets the placement of the axis. Defaults to bottom. 194 | 195 | ### .colors(callback) 196 | sets the d3 color scale the data series in the timeline. Defaults to `d3.scale.category20()`. 197 | 198 | ### .colorProperty(propertyName) 199 | sets the data item property name that maps your data items to your color scale. For example if you set your chart's `colors()` and `colorsProperty()` as follows: 200 | 201 | ```js 202 | var colorScale = d3.scale.ordinal().range(['#6b0000','#ef9b0f','#ffee00']) 203 | .domain(['apple','orange','lemon']); 204 | 205 | var chart = d3.timeline() 206 | .colors( colorScale ) 207 | .colorProperty('fruit'); 208 | ``` 209 | 210 | And pass this dataset: 211 | 212 | ```js 213 | var testData = [ 214 | {label: "fruit 1", fruit: "orange", times: [ 215 | {"starting_time": 1355759910000, "ending_time": 1355761900000}]}, 216 | {label: "fruit 2", fruit: "apple", times: [ 217 | {"starting_time": 1355752800000, "ending_time": 1355759900000}, 218 | {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, 219 | {label: "fruit3", fruit: "lemon", times: [ 220 | {"starting_time": 1355761910000, "ending_time": 1355763910000}]} 221 | ]; 222 | ``` 223 | Your chart's bar colors will be determined based on the value of the fruit property: 224 | 225 | ![Color Timeline](examples/timeline7.png) 226 | 227 | You can also set the color property for a specific time object: 228 | 229 | ```js 230 | var testData = [ 231 | {label: "fruit 2", fruit: "apple", times: [ 232 | {fruit: "orange", "starting_time": 1355752800000, "ending_time": 1355759900000}, 233 | {"starting_time": 1355767900000, "ending_time": 1355774400000}, 234 | {fruit: "lemon", "starting_time": 1355774400000, "ending_time": 1355775500000}]} 235 | ]; 236 | ``` 237 | 238 | Properties set in the time object will override the property set for the series: 239 | 240 | ![Timeline With Per-Time Colors](examples/timeline8.png) 241 | 242 | ### .beginning(date) 243 | sets the time that the timeline should start. If `beginning` and `ending` are not set, the timeline will calculate it based off of the smallest and largest times. 244 | 245 | ### .ending(date) 246 | sets the time that the timeline should end. If `beginning` and `ending` are not set, the timeline will calculate it based off of the smallest and largest times. 247 | 248 | ### .stack() 249 | Takes in no arguments. Toggles the stacking/unstacking of data series in the timeline. Needs to be true in order for icons and labels to show up properly. 250 | 251 | ### .relativeTime() 252 | Takes in no arguments. Toggles the calculation and use of relative timestamps. The origin of the timeline will be set to 0 and the starting_time of the first data dictionary in the data array will be subtracted from every subsequent timestamp. 253 | 254 | ### .showToday() 255 | Takes in no arguments. Toggles a vertical line showing the current Date.now() time. Uses showTodayFormat for the line formatting. 256 | 257 | ### .showTodayFormat({marginTop: , marginBottom: , width: , color: }) 258 | Sets the formatting of the showToday line. Color cycle can also be of the format `rgb(x, y, z)`. 259 | 260 | ### .showBorderLine() 261 | Takes in no arguments. Toggles a vertical line showing the borders of one specific timeline. Uses showBorderFormat for the line formatting. 262 | 263 | ### .showBorderFormat({marginTop: , marginBottom:, width: , color: }) 264 | Sets the formatting of the showBorder line. Color cycle can also be of the format `rgb(x, y, z)`. 265 | 266 | ### .showTimeAxis() 267 | Takes in no arguments. Toggles the visibility of the time axis. 268 | 269 | ### .showTimeAxisTick() 270 | Takes in no arguments. Shows tick marks along the X axis according to the arguments for `showTimeAxisTickFormat`. Useful for datasets with a lot of stacked elements. 271 | 272 | ![Timeline With tick marks](examples/timeline9.png) 273 | 274 | ### .showTimeAxisTickFormat(format) 275 | Format for `showTimeAxisTick`. Defaults to ```{stroke: "stroke-dasharray", spacing: "4 10"}```. 276 | 277 | Defaults to 278 | ```js 279 | { 280 | marginTop: 25, 281 | marginBottom: 0, 282 | width: 1, 283 | color: colorCycle 284 | } 285 | ``` 286 | ### .rowSeparators(color) 287 | Sets the display of horizontal lines betweens rows. 288 | 289 | ### .background(color) 290 | Sets the background of the rows. Useful for creating a continuous effect when there are gaps in your data. 291 | 292 | ### .hover(callback) 293 | takes in a callback called on mousemove of the timeline data. Example 294 | 295 | ```js 296 | d3.timeline() 297 | .hover(function (d, i, datum) { 298 | // d is the current rendering object 299 | // i is the index during d3 rendering 300 | // datum is the data object 301 | }); 302 | ``` 303 | 304 | ### .mouseover(callback) 305 | takes in a callback called on mouseover of the timeline data. Example 306 | 307 | ```js 308 | d3.timeline() 309 | .mouseover(function (d, i, datum) { 310 | // d is the current rendering object 311 | // i is the index during d3 rendering 312 | // datum is the data object 313 | }); 314 | ``` 315 | 316 | ### .mouseout(callback) 317 | takes in a callback called on mouseout of the timeline data. Example 318 | 319 | ```js 320 | d3.timeline() 321 | .mouseout(function (d, i, datum) { 322 | // d is the current rendering object 323 | // i is the index during d3 rendering 324 | // datum is the data object 325 | }); 326 | ``` 327 | 328 | ### .click(callback) 329 | takes in a callback called on click of the timeline data. Example 330 | 331 | ```js 332 | d3.timeline() 333 | .click(function (d, i, datum) { 334 | // d is the current rendering object 335 | // i is the index during d3 rendering 336 | // datum is the data object 337 | }); 338 | ``` 339 | 340 | ### .scroll(callback) 341 | takes in a callback called on scroll of the timeline data. Example 342 | 343 | ```js 344 | d3.timeline() 345 | .scroll(function (x, scale) { 346 | // x is the current position of the scroll 347 | // scale is the scale of the axis used 348 | }); 349 | ``` 350 | 351 | ## License 352 | MIT 353 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-timeline", 3 | "version": "0.0.5", 4 | "main": "src/d3-timeline.js", 5 | "license": "MIT", 6 | "ignore": [ 7 | "examples", 8 | "lib", 9 | ".bowerrc", 10 | ".gitignore", 11 | ".git", 12 | ".jshintrc", 13 | "gruntfile.js", 14 | "package.json", 15 | "README.md" 16 | ], 17 | "dependencies": { 18 | "d3": ">=3.4.3" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 38 | 291 | 292 | 293 |
294 |

A simple timeline

295 |
296 |
297 |
298 |

A simple timeline without Axis

299 |
300 |
301 |
302 |

It works with circles too

303 |
304 |
305 |
306 |

Combine circles and rectangles

307 |
308 |
309 |
310 |

A stacked timeline with hover, click, and scroll events

311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |

We can also use icons

320 |
321 |
322 | 323 |
324 |

We can change colors and put labels

325 |
326 |
327 | 328 |
329 |

We can also rotate tick mark labels

330 |
331 |
332 | 333 |
334 |

A timeline with colors mapped from the data

335 |
336 |
337 | 338 |
339 |

A timeline with colors mapped from the data for individual time objects

340 |
341 |
342 | 343 |
344 |

A timeline with relative timepoints

345 |
346 |
347 | 348 |
349 |

A stacked timeline with axis on top

350 |
351 |
352 | 353 |
354 |

A stacked timeline with backgrounds

355 |
356 |
357 | 358 |
359 |

A stacked timeline with backgrounds and ticks

360 |
361 |
362 | 363 |
364 |

A complex timeline

365 |
366 |
367 | 368 | 369 | 370 | -------------------------------------------------------------------------------- /examples/jackie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuang/d3-timeline/67537bfeb14479f1bdd73d2a612a615d328dee30/examples/jackie.png -------------------------------------------------------------------------------- /examples/timeline1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuang/d3-timeline/67537bfeb14479f1bdd73d2a612a615d328dee30/examples/timeline1.png -------------------------------------------------------------------------------- /examples/timeline2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuang/d3-timeline/67537bfeb14479f1bdd73d2a612a615d328dee30/examples/timeline2.png -------------------------------------------------------------------------------- /examples/timeline3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuang/d3-timeline/67537bfeb14479f1bdd73d2a612a615d328dee30/examples/timeline3.png -------------------------------------------------------------------------------- /examples/timeline4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuang/d3-timeline/67537bfeb14479f1bdd73d2a612a615d328dee30/examples/timeline4.png -------------------------------------------------------------------------------- /examples/timeline5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuang/d3-timeline/67537bfeb14479f1bdd73d2a612a615d328dee30/examples/timeline5.png -------------------------------------------------------------------------------- /examples/timeline6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuang/d3-timeline/67537bfeb14479f1bdd73d2a612a615d328dee30/examples/timeline6.png -------------------------------------------------------------------------------- /examples/timeline7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuang/d3-timeline/67537bfeb14479f1bdd73d2a612a615d328dee30/examples/timeline7.png -------------------------------------------------------------------------------- /examples/timeline8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuang/d3-timeline/67537bfeb14479f1bdd73d2a612a615d328dee30/examples/timeline8.png -------------------------------------------------------------------------------- /examples/timeline9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuang/d3-timeline/67537bfeb14479f1bdd73d2a612a615d328dee30/examples/timeline9.png -------------------------------------------------------------------------------- /examples/troll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuang/d3-timeline/67537bfeb14479f1bdd73d2a612a615d328dee30/examples/troll.png -------------------------------------------------------------------------------- /examples/wat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jiahuang/d3-timeline/67537bfeb14479f1bdd73d2a612a615d328dee30/examples/wat.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-timeline", 3 | "version": "0.0.5", 4 | "main": "src/d3-timeline.js", 5 | "repository": "https://github.com/jiahuang/d3-timeline.git", 6 | "license" : "MIT", 7 | "dependencies": { 8 | "d3": ">=3.4.3" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/d3-timeline.js: -------------------------------------------------------------------------------- 1 | // vim: ts=2 sw=2 2 | (function () { 3 | d3.timeline = function() { 4 | var DISPLAY_TYPES = ["circle", "rect"]; 5 | 6 | var hover = function () {}, 7 | mouseover = function () {}, 8 | mouseout = function () {}, 9 | click = function () {}, 10 | scroll = function () {}, 11 | labelFunction = function(label) { return label; }, 12 | navigateLeft = function () {}, 13 | navigateRight = function () {}, 14 | orient = "bottom", 15 | width = null, 16 | height = null, 17 | rowSeparatorsColor = null, 18 | backgroundColor = null, 19 | tickFormat = { format: d3.time.format("%I %p"), 20 | tickTime: d3.time.hours, 21 | tickInterval: 1, 22 | tickSize: 6, 23 | tickValues: null 24 | }, 25 | colorCycle = d3.scale.category20(), 26 | colorPropertyName = null, 27 | display = "rect", 28 | beginning = 0, 29 | labelMargin = 0, 30 | ending = 0, 31 | margin = {left: 30, right:30, top: 30, bottom:30}, 32 | stacked = false, 33 | rotateTicks = false, 34 | timeIsRelative = false, 35 | fullLengthBackgrounds = false, 36 | itemHeight = 20, 37 | itemMargin = 5, 38 | navMargin = 60, 39 | showTimeAxis = true, 40 | showAxisTop = false, 41 | showTodayLine = false, 42 | timeAxisTick = false, 43 | timeAxisTickFormat = {stroke: "stroke-dasharray", spacing: "4 10"}, 44 | showTodayFormat = {marginTop: 25, marginBottom: 0, width: 1, color: colorCycle}, 45 | showBorderLine = false, 46 | showBorderFormat = {marginTop: 25, marginBottom: 0, width: 1, color: colorCycle}, 47 | showAxisHeaderBackground = false, 48 | showAxisNav = false, 49 | showAxisCalendarYear = false, 50 | axisBgColor = "white", 51 | chartData = {} 52 | ; 53 | 54 | var appendTimeAxis = function(g, xAxis, yPosition) { 55 | 56 | if(showAxisHeaderBackground){ appendAxisHeaderBackground(g, 0, 0); } 57 | 58 | if(showAxisNav){ appendTimeAxisNav(g) }; 59 | 60 | var axis = g.append("g") 61 | .attr("class", "axis") 62 | .attr("transform", "translate(" + 0 + "," + yPosition + ")") 63 | .call(xAxis); 64 | }; 65 | 66 | var appendTimeAxisCalendarYear = function (nav) { 67 | var calendarLabel = beginning.getFullYear(); 68 | 69 | if (beginning.getFullYear() != ending.getFullYear()) { 70 | calendarLabel = beginning.getFullYear() + "-" + ending.getFullYear() 71 | } 72 | 73 | nav.append("text") 74 | .attr("transform", "translate(" + 20 + ", 0)") 75 | .attr("x", 0) 76 | .attr("y", 14) 77 | .attr("class", "calendarYear") 78 | .text(calendarLabel) 79 | ; 80 | }; 81 | var appendTimeAxisNav = function (g) { 82 | var timelineBlocks = 6; 83 | var leftNavMargin = (margin.left - navMargin); 84 | var incrementValue = (width - margin.left)/timelineBlocks; 85 | var rightNavMargin = (width - margin.right - incrementValue + navMargin); 86 | 87 | var nav = g.append('g') 88 | .attr("class", "axis") 89 | .attr("transform", "translate(0, 20)") 90 | ; 91 | 92 | if(showAxisCalendarYear) { appendTimeAxisCalendarYear(nav) }; 93 | 94 | nav.append("text") 95 | .attr("transform", "translate(" + leftNavMargin + ", 0)") 96 | .attr("x", 0) 97 | .attr("y", 14) 98 | .attr("class", "chevron") 99 | .text("<") 100 | .on("click", function () { 101 | return navigateLeft(beginning, chartData); 102 | }) 103 | ; 104 | 105 | nav.append("text") 106 | .attr("transform", "translate(" + rightNavMargin + ", 0)") 107 | .attr("x", 0) 108 | .attr("y", 14) 109 | .attr("class", "chevron") 110 | .text(">") 111 | .on("click", function () { 112 | return navigateRight(ending, chartData); 113 | }) 114 | ; 115 | }; 116 | 117 | var appendAxisHeaderBackground = function (g, xAxis, yAxis) { 118 | g.insert("rect") 119 | .attr("class", "row-green-bar") 120 | .attr("x", xAxis) 121 | .attr("width", width) 122 | .attr("y", yAxis) 123 | .attr("height", itemHeight) 124 | .attr("fill", axisBgColor); 125 | }; 126 | 127 | var appendTimeAxisTick = function(g, xAxis, maxStack) { 128 | g.append("g") 129 | .attr("class", "axis") 130 | .attr("transform", "translate(" + 0 + "," + (margin.top + (itemHeight + itemMargin) * maxStack) + ")") 131 | .attr(timeAxisTickFormat.stroke, timeAxisTickFormat.spacing) 132 | .call(xAxis.tickFormat("").tickSize(-(margin.top + (itemHeight + itemMargin) * (maxStack - 1) + 3), 0, 0)); 133 | }; 134 | 135 | var appendBackgroundBar = function (yAxisMapping, index, g, data, datum) { 136 | var greenbarYAxis = ((itemHeight + itemMargin) * yAxisMapping[index]) + margin.top; 137 | g.selectAll("svg").data(data).enter() 138 | .insert("rect") 139 | .attr("class", "row-green-bar") 140 | .attr("x", fullLengthBackgrounds ? 0 : margin.left) 141 | .attr("width", fullLengthBackgrounds ? width : (width - margin.right - margin.left)) 142 | .attr("y", greenbarYAxis) 143 | .attr("height", itemHeight) 144 | .attr("fill", backgroundColor instanceof Function ? backgroundColor(datum, index) : backgroundColor) 145 | ; 146 | }; 147 | 148 | var appendLabel = function (gParent, yAxisMapping, index, hasLabel, datum) { 149 | var fullItemHeight = itemHeight + itemMargin; 150 | var rowsDown = margin.top + (fullItemHeight/2) + fullItemHeight * (yAxisMapping[index] || 1); 151 | 152 | gParent.append("text") 153 | .attr("class", "timeline-label") 154 | .attr("transform", "translate(" + labelMargin + "," + rowsDown + ")") 155 | .text(hasLabel ? labelFunction(datum.label) : datum.id) 156 | .on("click", function (d, i) { click(d, index, datum); }); 157 | }; 158 | 159 | function timeline (gParent) { 160 | var g = gParent.append("g"); 161 | var gParentSize = gParent[0][0].getBoundingClientRect(); 162 | 163 | var gParentItem = d3.select(gParent[0][0]); 164 | 165 | var yAxisMapping = {}, 166 | maxStack = 1, 167 | minTime = 0, 168 | maxTime = 0; 169 | 170 | setWidth(); 171 | 172 | // check if the user wants relative time 173 | // if so, substract the first timestamp from each subsequent timestamps 174 | if(timeIsRelative){ 175 | g.each(function (d, i) { 176 | d.forEach(function (datum, index) { 177 | datum.times.forEach(function (time, j) { 178 | if(index === 0 && j === 0){ 179 | originTime = time.starting_time; //Store the timestamp that will serve as origin 180 | time.starting_time = 0; //Set the origin 181 | time.ending_time = time.ending_time - originTime; //Store the relative time (millis) 182 | }else{ 183 | time.starting_time = time.starting_time - originTime; 184 | time.ending_time = time.ending_time - originTime; 185 | } 186 | }); 187 | }); 188 | }); 189 | } 190 | 191 | // check how many stacks we're gonna need 192 | // do this here so that we can draw the axis before the graph 193 | if (stacked || ending === 0 || beginning === 0) { 194 | g.each(function (d, i) { 195 | d.forEach(function (datum, index) { 196 | 197 | // create y mapping for stacked graph 198 | if (stacked && Object.keys(yAxisMapping).indexOf(index) == -1) { 199 | yAxisMapping[index] = maxStack; 200 | maxStack++; 201 | } 202 | 203 | // figure out beginning and ending times if they are unspecified 204 | datum.times.forEach(function (time, i) { 205 | if(beginning === 0) 206 | if (time.starting_time < minTime || (minTime === 0 && timeIsRelative === false)) 207 | minTime = time.starting_time; 208 | if(ending === 0) 209 | if (time.ending_time > maxTime) 210 | maxTime = time.ending_time; 211 | }); 212 | }); 213 | }); 214 | 215 | if (ending === 0) { 216 | ending = maxTime; 217 | } 218 | if (beginning === 0) { 219 | beginning = minTime; 220 | } 221 | } 222 | 223 | var scaleFactor = (1/(ending - beginning)) * (width - margin.left - margin.right); 224 | 225 | // draw the axis 226 | var xScale = d3.time.scale() 227 | .domain([beginning, ending]) 228 | .range([margin.left, width - margin.right]); 229 | 230 | var xAxis = d3.svg.axis() 231 | .scale(xScale) 232 | .orient(orient) 233 | .tickFormat(tickFormat.format) 234 | .tickSize(tickFormat.tickSize); 235 | 236 | if (tickFormat.tickValues != null) { 237 | xAxis.tickValues(tickFormat.tickValues); 238 | } else { 239 | xAxis.ticks(tickFormat.numTicks || tickFormat.tickTime, tickFormat.tickInterval); 240 | } 241 | 242 | // draw the chart 243 | g.each(function(d, i) { 244 | chartData = d; 245 | d.forEach( function(datum, index){ 246 | var data = datum.times; 247 | var hasLabel = (typeof(datum.label) != "undefined"); 248 | 249 | // issue warning about using id per data set. Ids should be individual to data elements 250 | if (typeof(datum.id) != "undefined") { 251 | console.warn("d3Timeline Warning: Ids per dataset is deprecated in favor of a 'class' key. Ids are now per data element."); 252 | } 253 | 254 | if (backgroundColor) { appendBackgroundBar(yAxisMapping, index, g, data, datum); } 255 | 256 | g.selectAll("svg").data(data).enter() 257 | .append(function(d, i) { 258 | return document.createElementNS(d3.ns.prefix.svg, "display" in d? d.display:display); 259 | }) 260 | .attr("x", getXPos) 261 | .attr("y", getStackPosition) 262 | .attr("width", function (d, i) { 263 | return (d.ending_time - d.starting_time) * scaleFactor; 264 | }) 265 | .attr("cy", function(d, i) { 266 | return getStackPosition(d, i) + itemHeight/2; 267 | }) 268 | .attr("cx", getXPos) 269 | .attr("r", itemHeight / 2) 270 | .attr("height", itemHeight) 271 | .style("fill", function(d, i){ 272 | var dColorPropName; 273 | if (d.color) return d.color; 274 | if( colorPropertyName ){ 275 | dColorPropName = d[colorPropertyName]; 276 | if ( dColorPropName ) { 277 | return colorCycle( dColorPropName ); 278 | } else { 279 | return colorCycle( datum[colorPropertyName] ); 280 | } 281 | } 282 | return colorCycle(index); 283 | }) 284 | .on("mousemove", function (d, i) { 285 | hover(d, index, datum); 286 | }) 287 | .on("mouseover", function (d, i) { 288 | mouseover(d, i, datum); 289 | }) 290 | .on("mouseout", function (d, i) { 291 | mouseout(d, i, datum); 292 | }) 293 | .on("click", function (d, i) { 294 | click(d, index, datum); 295 | }) 296 | .attr("class", function (d, i) { 297 | return datum.class ? "timelineSeries_"+datum.class : "timelineSeries_"+index; 298 | }) 299 | .attr("id", function(d, i) { 300 | // use deprecated id field 301 | if (datum.id && !d.id) { 302 | return 'timelineItem_'+datum.id; 303 | } 304 | 305 | return d.id ? d.id : "timelineItem_"+index+"_"+i; 306 | }) 307 | ; 308 | 309 | g.selectAll("svg").data(data).enter() 310 | .append("text") 311 | .attr("x", getXTextPos) 312 | .attr("y", getStackTextPosition) 313 | .text(function(d) { 314 | return d.label; 315 | }) 316 | ; 317 | 318 | if (rowSeparatorsColor) { 319 | var lineYAxis = ( itemHeight + itemMargin / 2 + margin.top + (itemHeight + itemMargin) * yAxisMapping[index]); 320 | gParent.append("svg:line") 321 | .attr("class", "row-separator") 322 | .attr("x1", 0 + margin.left) 323 | .attr("x2", width - margin.right) 324 | .attr("y1", lineYAxis) 325 | .attr("y2", lineYAxis) 326 | .attr("stroke-width", 1) 327 | .attr("stroke", rowSeparatorsColor); 328 | } 329 | 330 | // add the label 331 | if (hasLabel) { appendLabel(gParent, yAxisMapping, index, hasLabel, datum); } 332 | 333 | if (typeof(datum.icon) !== "undefined") { 334 | gParent.append("image") 335 | .attr("class", "timeline-label") 336 | .attr("transform", "translate("+ 0 +","+ (margin.top + (itemHeight + itemMargin) * yAxisMapping[index])+")") 337 | .attr("xlink:href", datum.icon) 338 | .attr("width", margin.left) 339 | .attr("height", itemHeight); 340 | } 341 | 342 | function getStackPosition(d, i) { 343 | if (stacked) { 344 | return margin.top + (itemHeight + itemMargin) * yAxisMapping[index]; 345 | } 346 | return margin.top; 347 | } 348 | function getStackTextPosition(d, i) { 349 | if (stacked) { 350 | return margin.top + (itemHeight + itemMargin) * yAxisMapping[index] + itemHeight * 0.75; 351 | } 352 | return margin.top + itemHeight * 0.75; 353 | } 354 | }); 355 | }); 356 | 357 | var belowLastItem = (margin.top + (itemHeight + itemMargin) * maxStack); 358 | var aboveFirstItem = margin.top; 359 | var timeAxisYPosition = showAxisTop ? aboveFirstItem : belowLastItem; 360 | if (showTimeAxis) { appendTimeAxis(g, xAxis, timeAxisYPosition); } 361 | if (timeAxisTick) { appendTimeAxisTick(g, xAxis, maxStack); } 362 | 363 | if (width > gParentSize.width) { 364 | var move = function() { 365 | var x = Math.min(0, Math.max(gParentSize.width - width, d3.event.translate[0])); 366 | zoom.translate([x, 0]); 367 | g.attr("transform", "translate(" + x + ",0)"); 368 | scroll(x*scaleFactor, xScale); 369 | }; 370 | 371 | var zoom = d3.behavior.zoom().x(xScale).on("zoom", move); 372 | 373 | gParent 374 | .attr("class", "scrollable") 375 | .call(zoom); 376 | } 377 | 378 | if (rotateTicks) { 379 | g.selectAll(".tick text") 380 | .attr("transform", function(d) { 381 | return "rotate(" + rotateTicks + ")translate(" 382 | + (this.getBBox().width / 2 + 10) + "," // TODO: change this 10 383 | + this.getBBox().height / 2 + ")"; 384 | }); 385 | } 386 | 387 | var gSize = g[0][0].getBoundingClientRect(); 388 | setHeight(); 389 | 390 | if (showBorderLine) { 391 | g.each(function (d, i) { 392 | d.forEach(function (datum) { 393 | var times = datum.times; 394 | times.forEach(function (time) { 395 | appendLine(xScale(time.starting_time), showBorderFormat); 396 | appendLine(xScale(time.ending_time), showBorderFormat); 397 | }); 398 | }); 399 | }); 400 | } 401 | 402 | if (showTodayLine) { 403 | var todayLine = xScale(new Date()); 404 | appendLine(todayLine, showTodayFormat); 405 | } 406 | 407 | function getXPos(d, i) { 408 | return margin.left + (d.starting_time - beginning) * scaleFactor; 409 | } 410 | 411 | function getXTextPos(d, i) { 412 | return margin.left + (d.starting_time - beginning) * scaleFactor + 5; 413 | } 414 | 415 | function setHeight() { 416 | if (!height && !gParentItem.attr("height")) { 417 | if (itemHeight) { 418 | // set height based off of item height 419 | height = gSize.height + gSize.top - gParentSize.top; 420 | // set bounding rectangle height 421 | d3.select(gParent[0][0]).attr("height", height); 422 | } else { 423 | throw "height of the timeline is not set"; 424 | } 425 | } else { 426 | if (!height) { 427 | height = gParentItem.attr("height"); 428 | } else { 429 | gParentItem.attr("height", height); 430 | } 431 | } 432 | } 433 | 434 | function setWidth() { 435 | if (!width && !gParentSize.width) { 436 | try { 437 | width = gParentItem.attr("width"); 438 | if (!width) { 439 | throw "width of the timeline is not set. As of Firefox 27, timeline().with(x) needs to be explicitly set in order to render"; 440 | } 441 | } catch (err) { 442 | console.log( err ); 443 | } 444 | } else if (!(width && gParentSize.width)) { 445 | try { 446 | width = gParentItem.attr("width"); 447 | } catch (err) { 448 | console.log( err ); 449 | } 450 | } 451 | // if both are set, do nothing 452 | } 453 | 454 | function appendLine(lineScale, lineFormat) { 455 | gParent.append("svg:line") 456 | .attr("x1", lineScale) 457 | .attr("y1", lineFormat.marginTop) 458 | .attr("x2", lineScale) 459 | .attr("y2", height - lineFormat.marginBottom) 460 | .style("stroke", lineFormat.color)//"rgb(6,120,155)") 461 | .style("stroke-width", lineFormat.width); 462 | } 463 | 464 | } 465 | 466 | // SETTINGS 467 | 468 | timeline.margin = function (p) { 469 | if (!arguments.length) return margin; 470 | margin = p; 471 | return timeline; 472 | }; 473 | 474 | timeline.orient = function (orientation) { 475 | if (!arguments.length) return orient; 476 | orient = orientation; 477 | return timeline; 478 | }; 479 | 480 | timeline.itemHeight = function (h) { 481 | if (!arguments.length) return itemHeight; 482 | itemHeight = h; 483 | return timeline; 484 | }; 485 | 486 | timeline.itemMargin = function (h) { 487 | if (!arguments.length) return itemMargin; 488 | itemMargin = h; 489 | return timeline; 490 | }; 491 | 492 | timeline.navMargin = function (h) { 493 | if (!arguments.length) return navMargin; 494 | navMargin = h; 495 | return timeline; 496 | }; 497 | 498 | timeline.height = function (h) { 499 | if (!arguments.length) return height; 500 | height = h; 501 | return timeline; 502 | }; 503 | 504 | timeline.width = function (w) { 505 | if (!arguments.length) return width; 506 | width = w; 507 | return timeline; 508 | }; 509 | 510 | timeline.display = function (displayType) { 511 | if (!arguments.length || (DISPLAY_TYPES.indexOf(displayType) == -1)) return display; 512 | display = displayType; 513 | return timeline; 514 | }; 515 | 516 | timeline.labelFormat = function(f) { 517 | if (!arguments.length) return labelFunction; 518 | labelFunction = f; 519 | return timeline; 520 | }; 521 | 522 | timeline.tickFormat = function (format) { 523 | if (!arguments.length) return tickFormat; 524 | tickFormat = format; 525 | return timeline; 526 | }; 527 | 528 | timeline.hover = function (hoverFunc) { 529 | if (!arguments.length) return hover; 530 | hover = hoverFunc; 531 | return timeline; 532 | }; 533 | 534 | timeline.mouseover = function (mouseoverFunc) { 535 | if (!arguments.length) return mouseover; 536 | mouseover = mouseoverFunc; 537 | return timeline; 538 | }; 539 | 540 | timeline.mouseout = function (mouseoutFunc) { 541 | if (!arguments.length) return mouseout; 542 | mouseout = mouseoutFunc; 543 | return timeline; 544 | }; 545 | 546 | timeline.click = function (clickFunc) { 547 | if (!arguments.length) return click; 548 | click = clickFunc; 549 | return timeline; 550 | }; 551 | 552 | timeline.scroll = function (scrollFunc) { 553 | if (!arguments.length) return scroll; 554 | scroll = scrollFunc; 555 | return timeline; 556 | }; 557 | 558 | timeline.colors = function (colorFormat) { 559 | if (!arguments.length) return colorCycle; 560 | colorCycle = colorFormat; 561 | return timeline; 562 | }; 563 | 564 | timeline.beginning = function (b) { 565 | if (!arguments.length) return beginning; 566 | beginning = b; 567 | return timeline; 568 | }; 569 | 570 | timeline.ending = function (e) { 571 | if (!arguments.length) return ending; 572 | ending = e; 573 | return timeline; 574 | }; 575 | 576 | timeline.labelMargin = function (m) { 577 | if (!arguments.length) return labelMargin; 578 | labelMargin = m; 579 | return timeline; 580 | }; 581 | 582 | timeline.rotateTicks = function (degrees) { 583 | if (!arguments.length) return rotateTicks; 584 | rotateTicks = degrees; 585 | return timeline; 586 | }; 587 | 588 | timeline.stack = function () { 589 | stacked = !stacked; 590 | return timeline; 591 | }; 592 | 593 | timeline.relativeTime = function() { 594 | timeIsRelative = !timeIsRelative; 595 | return timeline; 596 | }; 597 | 598 | timeline.showBorderLine = function () { 599 | showBorderLine = !showBorderLine; 600 | return timeline; 601 | }; 602 | 603 | timeline.showBorderFormat = function(borderFormat) { 604 | if (!arguments.length) return showBorderFormat; 605 | showBorderFormat = borderFormat; 606 | return timeline; 607 | }; 608 | 609 | timeline.showToday = function () { 610 | showTodayLine = !showTodayLine; 611 | return timeline; 612 | }; 613 | 614 | timeline.showTodayFormat = function(todayFormat) { 615 | if (!arguments.length) return showTodayFormat; 616 | showTodayFormat = todayFormat; 617 | return timeline; 618 | }; 619 | 620 | timeline.colorProperty = function(colorProp) { 621 | if (!arguments.length) return colorPropertyName; 622 | colorPropertyName = colorProp; 623 | return timeline; 624 | }; 625 | 626 | timeline.rowSeparators = function (color) { 627 | if (!arguments.length) return rowSeparatorsColor; 628 | rowSeparatorsColor = color; 629 | return timeline; 630 | 631 | }; 632 | 633 | timeline.background = function (color) { 634 | if (!arguments.length) return backgroundColor; 635 | backgroundColor = color; 636 | return timeline; 637 | }; 638 | 639 | timeline.showTimeAxis = function () { 640 | showTimeAxis = !showTimeAxis; 641 | return timeline; 642 | }; 643 | 644 | timeline.showAxisTop = function () { 645 | showAxisTop = !showAxisTop; 646 | return timeline; 647 | }; 648 | 649 | timeline.showAxisCalendarYear = function () { 650 | showAxisCalendarYear = !showAxisCalendarYear; 651 | return timeline; 652 | }; 653 | 654 | timeline.showTimeAxisTick = function () { 655 | timeAxisTick = !timeAxisTick; 656 | return timeline; 657 | }; 658 | 659 | timeline.fullLengthBackgrounds = function () { 660 | fullLengthBackgrounds = !fullLengthBackgrounds; 661 | return timeline; 662 | }; 663 | 664 | timeline.showTimeAxisTickFormat = function(format) { 665 | if (!arguments.length) return timeAxisTickFormat; 666 | timeAxisTickFormat = format; 667 | return timeline; 668 | }; 669 | 670 | timeline.showAxisHeaderBackground = function(bgColor) { 671 | showAxisHeaderBackground = !showAxisHeaderBackground; 672 | if(bgColor) { (axisBgColor = bgColor) }; 673 | return timeline; 674 | }; 675 | 676 | timeline.navigate = function (navigateBackwards, navigateForwards) { 677 | if (!arguments.length) return [navigateLeft, navigateRight]; 678 | navigateLeft = navigateBackwards; 679 | navigateRight = navigateForwards; 680 | showAxisNav = !showAxisNav; 681 | return timeline; 682 | }; 683 | 684 | return timeline; 685 | }; 686 | })(); 687 | --------------------------------------------------------------------------------