Repository: denisemauldin/d3-timeline Branch: master Commit: 2d821ec5b12c Files: 17 Total size: 107.2 KB Directory structure: gitextract_dkg6lrp9/ ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── dist/ │ └── d3-timelines.js ├── examples/ │ ├── days.html │ ├── days_scrollable.html │ ├── example.html │ ├── scrollable.html │ ├── sp500.csv │ ├── timeline.html │ └── timelineStacked.html ├── index.js ├── package.json ├── rollup.config.js ├── src/ │ └── timelines.js └── test/ └── timeline-test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store build/ node_modules npm-debug.log *.swp ================================================ FILE: .npmignore ================================================ build/*.zip test/ ================================================ FILE: LICENSE ================================================ Copyright 2017, Denise Mauldin All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # d3-timeline A d3 v4 version of d3-timeline Get something that looks like ![Rectangular Timeline](examples/timeline1.png) for a dataset that looks like ```js var testData = [ {label: "person a", times: [ {"starting_time": 1355752800000, "ending_time": 1355759900000}, {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, {label: "person b", times: [ {"starting_time": 1355759910000, "ending_time": 1355761900000}]}, {label: "person c", times: [ {"starting_time": 1355761910000, "ending_time": 1355763910000}]} ]; ``` with a call that looks like ```js var chart = d3.timelines(); var svg = d3.select("#timeline1").append("svg").attr("width", 500) .datum(testData).call(chart); ``` Works with circles. In case the rectangular edges are too pointy. ![Circular Timeline](examples/timeline2.png) Combine rectangles and circles to your liking ![Rectangular and Circular Timeline](examples/timeline3.png) by adding a ``display`` key to the data: ```js var rectAndCircleTestData = [ {times: [{"starting_time": 1355752800000, "display": "circle"}, {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, {times: [{"starting_time": 1355759910000, "display":"circle"}, ]}, {times: [{"starting_time": 1355761910000, "ending_time": 1355763910000}]} ]; ``` Make a pseudo-gantt chart thingy ![Gantt chart](examples/timeline4.png) with icons ![Icon chart](examples/timeline5.png) 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. 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 ```js var testData = [ {class: "pA", label: "person a", times: [ {"starting_time": 1355752800000, "ending_time": 1355759900000}, {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, {class: "pB", label: "person b", times: [ {"starting_time": 1355759910000, "ending_time": 1355761900000}]}, {class: "pC", label: "person c", times: [ {"starting_time": 1355761910000, "ending_time": 1355763910000}]} ]; ``` 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");`. 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`. Also optional is an `id` field per data element. ```js var testData = [ {label: "person a", times: [ {"starting_time": 1355752800000, "ending_time": 1355759900000, "id": "A1"}, {"starting_time": 1355767900000, "ending_time": 1355774400000, "id": "A2"}]} ]; ``` 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`. Look at the [examples](examples/example.html) for more details. ## Data formats The simplest data format only requires `starting_time` and `ending_time` for each series of data. ```js [ {times: [ {"starting_time": 1355752800000, "ending_time": 1355759900000}, {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, {times: [ {"starting_time": 1355759910000, "ending_time": 1355761900000}]} ]; ``` `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() ```js [ {label: "person a", times: [ {"starting_time": 1355752800000, "ending_time": 1355759900000}, {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, {label: "person b", times: [ {"starting_time": 1355759910000, "ending_time": 1355761900000}]} ]; ``` `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. ```js [ {icon: "path/to/img.png", times: [ {"starting_time": 1355752800000, "ending_time": 1355759900000}, {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, {label: "person b", times: [ {"starting_time": 1355759910000, "ending_time": 1355761900000}]} ]; ``` ### 'times' elements array 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). ```js [ {label: "person a", times: [{"color":"green", "label":"Weeee", "starting_time": 1355752800000, "ending_time": 1355759900000}, {"color":"blue", "label":"Weeee", "starting_time": 1355767900000, "ending_time": 1355774400000}]}, {label: "person b", times: [{"color":"pink", "label":"Weeee", "starting_time": 1355759910000, "ending_time": 1355761900000}, ]}, {label: "person c", times: [{"color":"yellow", "label":"Weeee", "starting_time": 1355761910000, "ending_time": 1355763910000}]} ]; ``` ## Method Calls All methods that take in arguments return the current settings if no argument is passed. ### .width(width) 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. ### .height(height) 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. ### .itemHeight(height) sets the height of the data series in the timeline. Defaults to 20px. ### .itemMargin(height) sets the margin between the data series in the timeline. Defaults to 5px. ### .margin({left: , right: , top: , bottom: }) sets the margin of the entire timeline inside of the svg. Defaults to 30px all around. ### .display("circle" | "rect") Displays the data series as either circles or rectangles. Defaults to "rect". ### .labelFormat(callback) registers a function to be called when the text for the label needs to be generated. Useful if your label looks like this: ``` { en: "my label", fr: "mon étiquette" } ``` The callback function is passed the whatever the datum.label returns, so in this case it would be the object above. So the labelFormat might look something like this: ```js .labelFormat(function(label){ return label[currentLocale];}) ``` ### .tickFormat({format: , tickTime: , tickInterval: , tickSize: , numTicks: , tickValues}) sets the formatting of the ticks in the timeline. Defaults to ```js { format: d3.time.format("%I %p"), tickTime: d3.time.hours, tickInterval: 1, tickSize: 6 } ``` Tick interval/values can be set with: - ``tickTime`` and ``tickInterval`` - ``numTicks`` and ``tickInterval`` - ``tickValues`` ### .rotateTicks(degrees) sets the degree of rotation of the tickmarks. Defaults to no rotation (0 degrees). ### .orient("bottom" | "top") sets the placement of the axis. Defaults to bottom. ### .colors(callback) sets the d3 color scale the data series in the timeline. Defaults to `d3.scale.category20()`. ### .colorProperty(propertyName) 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: ```js var colorScale = d3.scale.ordinal().range(['#6b0000','#ef9b0f','#ffee00']) .domain(['apple','orange','lemon']); var chart = d3.timelines() .colors( colorScale ) .colorProperty('fruit'); ``` And pass this dataset: ```js var testData = [ {label: "fruit 1", fruit: "orange", times: [ {"starting_time": 1355759910000, "ending_time": 1355761900000}]}, {label: "fruit 2", fruit: "apple", times: [ {"starting_time": 1355752800000, "ending_time": 1355759900000}, {"starting_time": 1355767900000, "ending_time": 1355774400000}]}, {label: "fruit3", fruit: "lemon", times: [ {"starting_time": 1355761910000, "ending_time": 1355763910000}]} ]; ``` Your chart's bar colors will be determined based on the value of the fruit property: ![Color Timeline](examples/timeline7.png) You can also set the color property for a specific time object: ```js var testData = [ {label: "fruit 2", fruit: "apple", times: [ {fruit: "orange", "starting_time": 1355752800000, "ending_time": 1355759900000}, {"starting_time": 1355767900000, "ending_time": 1355774400000}, {fruit: "lemon", "starting_time": 1355774400000, "ending_time": 1355775500000}]} ]; ``` Properties set in the time object will override the property set for the series: ![Timeline With Per-Time Colors](examples/timeline8.png) ### .beginning(date) 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. ### .ending(date) 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. ### .stack() 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. ### .relativeTime() 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. ### .showToday() Takes in no arguments. Toggles a vertical line showing the current Date.now() time. Uses showTodayFormat for the line formatting. ### .showTodayFormat({marginTop: , marginBottom: , width: , color: }) Sets the formatting of the showToday line. Color cycle can also be of the format `rgb(x, y, z)`. ### .showBorderLine() Takes in no arguments. Toggles a vertical line showing the borders of one specific timeline. Uses showBorderFormat for the line formatting. ### .showBorderFormat({marginTop: , marginBottom:, width: , color: }) Sets the formatting of the showBorder line. Color cycle can also be of the format `rgb(x, y, z)`. ### .showTimeAxis() Takes in no arguments. Toggles the visibility of the time axis. ### .showTimeAxisTick() 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. ![Timeline With tick marks](examples/timeline9.png) ### .showTimeAxisTickFormat(format) Format for `showTimeAxisTick`. Defaults to ```{stroke: "stroke-dasharray", spacing: "4 10"}```. Defaults to ```js { marginTop: 25, marginBottom: 0, width: 1, color: colorCycle } ``` ### .rowSeparators(color) Sets the display of horizontal lines betweens rows. ### .background(color) Sets the background of the rows. Useful for creating a continuous effect when there are gaps in your data. ### .hover(callback) takes in a callback called on mousemove of the timeline data. Example ```js d3.timelines() .hover(function (d, i, datum) { // d is the current rendering object // i is the index during d3 rendering // datum is the data object }); ``` ### .xAxisClass Default is timeline-xAxis, but can be used to set any class. ### .allowZoom Default is true. Takes a boolean. If set to false, does not allow zooming. ### .mouseover(callback) takes in a callback called on mouseover of the timeline data. Example ```js d3.timelines() .mouseover(function (d, i, datum) { // d is the current rendering object // i is the index during d3 rendering // datum is the data object }); ``` ### .mouseout(callback) takes in a callback called on mouseout of the timeline data. Example ```js d3.timelines() .mouseout(function (d, i, datum) { // d is the current rendering object // i is the index during d3 rendering // datum is the data object }); ``` ### .click(callback) takes in a callback called on click of the timeline data. Example ```js d3.timelines() .click(function (d, i, datum) { // d is the current rendering object // i is the index during d3 rendering // datum is the data object }); ``` ### .scroll(callback) takes in a callback called on scroll of the timeline data. Example ```js d3.timelines() .scroll(function (x, scale) { // x is the current position of the scroll // scale is the scale of the axis used }); ``` ## Build To build: - `npm install` - `npm test` - copy the build/d3-timelines.js to dist/d3-timelines.js ## Previous work Inspired by jiahuang - https://github.com/jiahuang/d3-timeline ## License MIT ================================================ FILE: dist/d3-timelines.js ================================================ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-axis'), require('d3-array'), require('d3-time-format'), require('d3-time'), require('d3-scale'), require('d3-selection'), require('d3-zoom')) : typeof define === 'function' && define.amd ? define(['exports', 'd3-axis', 'd3-array', 'd3-time-format', 'd3-time', 'd3-scale', 'd3-selection', 'd3-zoom'], factory) : (factory((global.d3 = global.d3 || {}),global.d3,global.d3,global.d3,global.d3,global.d3,global.d3,global.d3)); }(this, function (exports,d3Axis,d3Array,d3TimeFormat,d3Time,d3Scale,d3Selection,d3Zoom) { 'use strict'; var timelines = function() { var DISPLAY_TYPES = ["circle", "rect"]; var hover = function () {}, mouseover = function () {}, mouseout = function () {}, click = function () {}, scroll = function () {}, labelFunction = function(label) { return label; }, labelFloat = 0, // floats up this many pixels navigateLeft = function () {}, navigateRight = function () {}, orient = "bottom", width = null, height = null, rowSeparatorsColor = null, backgroundColor = null, tickFormat = { format: d3TimeFormat.timeFormat("%I %p"), tickTime: d3Time.timeHour, tickInterval: 1, tickSize: 6, tickValues: null }, allowZoom = true, axisBgColor = "white", chartData = {}, colorCycle = d3Scale.scaleOrdinal(d3Scale.schemeCategory20), colorPropertyName = null, display = "rect", beginning = 0, labelMargin = 0, ending = 0, margin = {left: 30, right:30, top: 30, bottom:30}, maxZoom = 5, stacked = false, rotateTicks = false, timeIsRelative = false, timeIsLinear = false, fullLengthBackgrounds = false, itemHeight = 20, itemMargin = 5, navMargin = 60, showTimeAxis = true, showAxisTop = false, showTodayLine = false, timeAxisTick = false, timeAxisTickFormat = {stroke: "stroke-dasharray", spacing: "4 10"}, showTodayFormat = {marginTop: 25, marginBottom: 0, width: 1, color: colorCycle}, showBorderLine = false, showBorderFormat = {marginTop: 25, marginBottom: 0, width: 1, color: colorCycle}, showBorderLineClass = "timeline-border-line", showAxisHeaderBackground = false, showAxisNav = false, showAxisCalendarYear = false, xAxisClass = 'timeline-xAxis' ; var appendTimeAxis = function(g, xAxis, yPosition) { if(showAxisHeaderBackground){ appendAxisHeaderBackground(g, 0, 0); } if(showAxisNav){ appendTimeAxisNav(g); } var axis = g.append("g") .attr("class", xAxisClass) .attr("transform", "translate(" + 0 + "," + yPosition + ")") .call(xAxis); return axis; }; var appendTimeAxisCalendarYear = function (nav) { var calendarLabel = beginning.getFullYear(); if (beginning.getFullYear() != ending.getFullYear()) { calendarLabel = beginning.getFullYear() + "-" + ending.getFullYear(); } nav.append("text") .attr("transform", "translate(" + 20 + ", 0)") .attr("x", 0) .attr("y", 14) .attr("class", "calendarYear") .text(calendarLabel) ; }; var appendTimeAxisNav = function (g) { var timelineBlocks = 6; var leftNavMargin = (margin.left - navMargin); var incrementValue = (width - margin.left)/timelineBlocks; var rightNavMargin = (width - margin.right - incrementValue + navMargin); var nav = g.append('g') .attr("class", "axis") .attr("transform", "translate(0, 20)") ; if(showAxisCalendarYear) { appendTimeAxisCalendarYear(nav); } nav.append("text") .attr("transform", "translate(" + leftNavMargin + ", 0)") .attr("x", 0) .attr("y", 14) .attr("class", "chevron") .text("<") .on("click", function () { return navigateLeft(beginning, chartData); }) ; nav.append("text") .attr("transform", "translate(" + rightNavMargin + ", 0)") .attr("x", 0) .attr("y", 14) .attr("class", "chevron") .text(">") .on("click", function () { return navigateRight(ending, chartData); }) ; }; var appendAxisHeaderBackground = function (g, xAxis, yAxis) { g.insert("rect") .attr("class", "row-green-bar") .attr("x", xAxis) .attr("width", width) .attr("y", yAxis) .attr("height", itemHeight) .attr("fill", axisBgColor); }; var appendTimeAxisTick = function(g, xAxis, maxStack) { g.append("g") .attr("class", "axis") .attr("transform", "translate(" + 0 + "," + (margin.top + (itemHeight + itemMargin) * maxStack) + ")") .attr(timeAxisTickFormat.stroke, timeAxisTickFormat.spacing) .call(xAxis.tickFormat("").tickSize(-(margin.top + (itemHeight + itemMargin) * (maxStack - 1) + 3), 0, 0)); }; var appendBackgroundBar = function (yAxisMapping, index, g, data, datum) { var greenbarYAxis = ((itemHeight + itemMargin) * yAxisMapping[index]) + margin.top; g.selectAll("svg") .data(data).enter() .insert("rect", ":first-child") .attr("class", "row-green-bar") .attr("x", fullLengthBackgrounds ? 0 : margin.left) .attr("width", fullLengthBackgrounds ? width : (width - margin.right - margin.left)) .attr("y", greenbarYAxis) .attr("height", itemHeight) .attr("fill", backgroundColor instanceof Function ? backgroundColor(datum, index) : backgroundColor) ; }; var appendLabel = function (gParent, yAxisMapping, index, hasLabel, datum) { var fullItemHeight = itemHeight + itemMargin; var rowsDown = margin.top + (fullItemHeight/2) + fullItemHeight * (yAxisMapping[index] || 1); gParent.append("text") .attr("class", "timeline-label") .attr("transform", "translate(" + labelMargin + "," + rowsDown + ")") .text(hasLabel ? labelFunction(datum.label) : datum.id) .on("click", function (d, i) { console.log("label click!"); var point = d3Selection.mouse(this); gParent.append("rect") .attr("id", "clickpoint") .attr("x", point[0]) .attr("width", 10) .attr("height", itemHeight); click(d, index, datum, point, xScale.invert(point[0])); }); }; /*########################### #### START timelines ### #############################*/ function timelines (gParent) { var gParentSize = gParent.node().getBoundingClientRect(); // the svg size var gParentItem = d3Selection.select(gParent.node()); // the svg var g = gParent.append("g").attr("class", "container"); var yAxisMapping = {}, maxStack = 1, minTime = 0, maxTime = 0; setWidth(); // check if the user wants relative time // if so, substract the first timestamp from each subsequent timestamps if(timeIsRelative){ g.each(function (d, i) { var originTime = 0; d.forEach(function (datum, index) { datum.times.forEach(function (time, j) { if(index === 0 && j === 0){ originTime = time.starting_time; //Store the timestamp that will serve as origin time.starting_time = 0; //Set tahe origin time.ending_time = time.ending_time - originTime; //Store the relative time (millis) }else{ time.starting_time = time.starting_time - originTime; time.ending_time = time.ending_time - originTime; } }); }); }); } // check how many stacks we're gonna need // do this here so that we can draw the axis before the graph if (stacked || ending === 0 || beginning === 0) { g.each(function (d, i) { d.forEach(function (datum, index) { // create y mapping for stacked graph if (stacked && Object.keys(yAxisMapping).indexOf(index) == -1) { yAxisMapping[index] = maxStack; maxStack++; } // figure out beginning and ending times if they are unspecified datum.times.forEach(function (time, i) { if(beginning === 0) if (time.starting_time < minTime || (minTime === 0 && timeIsRelative === false)) minTime = time.starting_time; if(ending === 0) if (time.ending_time > maxTime) maxTime = time.ending_time; }); }); }); if (ending === 0) { ending = maxTime; } if (beginning === 0) { beginning = minTime; } } var scaleFactor = (1/(ending - beginning)) * (width - margin.left - margin.right); function formatDays(d) { var days = Math.floor(d / 86400), hours = Math.floor((d - (days * 86400)) / 3600), minutes = Math.floor((d - (days * 86400) - (hours * 3600)) / 60), seconds = d - (days * 86400) - (hours * 3600) - (minutes * 60); var output = ''; if (seconds) { output = seconds + 's'; } if (minutes) { output = minutes + 'm ' + output; } if (hours) { output = hours + 'h ' + output; } if (days) { output = days + 'd ' + output; } return output; }; var xScale; var xAxis; if (orient == "bottom") { xAxis = d3Axis.axisBottom(); } else if (orient == "top") { xAxis = d3Axis.axisTop(); } if (timeIsLinear) { xScale = d3Scale.scaleLinear() .domain([beginning, ending]) .range([margin.left, width - margin.right]); xAxis.scale(xScale) .tickFormat(formatDays) .tickValues(d3Array.range(0, ending, 86400)); } else { xScale = d3Scale.scaleTime() .domain([beginning, ending]) .range([margin.left, width - margin.right]); xAxis.scale(xScale) .tickFormat(tickFormat.format) .tickSize(tickFormat.tickSize); } if (tickFormat.tickValues !== null) { xAxis.tickValues(tickFormat.tickValues); } else { xAxis.tickArguments(tickFormat.numTicks || [tickFormat.tickTime, tickFormat.tickInterval]); } // append a view for zoom/pan support var view = g.append("g") .attr("class", "view"); // draw the chart g.each(function(d, i) { chartData = d; d.forEach( function(datum, index){ var data = datum.times; data.forEach(function(d) { d.name = datum.name }); var hasLabel = (typeof(datum.label) != "undefined"); // issue warning about using id per data set. Ids should be individual to data elements if (typeof(datum.id) != "undefined") { console.warn("d3Timeline Warning: Ids per dataset is deprecated in favor of a 'class' key. Ids are now per data element."); } if (backgroundColor) { appendBackgroundBar(yAxisMapping, index, g, data, datum); } view.selectAll("svg") .data(data).enter() .append(function(d, i) { return document.createElementNS(d3Selection.namespaces.svg, "display" in d? d.display:display); }) .attr("x", getXPos) .attr("y", getStackPosition) .attr("width", function (d, i) { return (d.ending_time - d.starting_time) * scaleFactor; }) .attr("cy", function(d, i) { return getStackPosition(d, i) + itemHeight/2; }) .attr("cx", getXPos) .attr("r", itemHeight / 2) .attr("height", itemHeight) .style("fill", function(d, i){ var dColorPropName; if (d.color) return d.color; if( colorPropertyName ){ dColorPropName = d[colorPropertyName]; if ( dColorPropName ) { return colorCycle( dColorPropName ); } else { return colorCycle( datum[colorPropertyName] ); } } return colorCycle(index); }) .on("mousemove", function (d, i) { hover(d, index, datum, i); }) .on("mouseover", function (d, i) { mouseover(d, i, datum, i); }) .on("mouseout", function (d, i) { mouseout(d, i, datum, i); }) .on("click", function (d, i) { var point = d3Selection.mouse(this); var selectedRect = d3Selection.select(this).node(); var selectorLabel = "text#" + selectedRect.id + '.textnumbers'; var selectedLabel = d3Selection.select(selectorLabel).node(); click(d, index, datum, selectedLabel, selectedRect, xScale.invert(point[0])); }) .attr("class", function (d, i) { return datum.class ? "timelineSeries_"+datum.class : "timelineSeries_"+index; }) .attr("id", function(d, i) { // use deprecated id field if (datum.id && !d.id) { return 'timelineItem_'+datum.id; } return d.id ? d.id : "timelineItem_"+index+"_"+i; }) ; // appends the labels to the boxes - DAY/HOUR LABEL view.selectAll("svg") .data(data).enter() .append("text") .attr("class", "textlabels") .attr("id", function(d) { return d.id }) .attr("x", function(d, i) { return getXTextPos(d, i, d.label, '.textlabels')}) .attr("y", (getStackTextPosition() - labelFloat)) .text(function(d) { return d.label; }) .on("click", function(d, i){ // when clicking on the label, call the click for the rectangle with the same id var point = d3Selection.mouse(this); var id = this.id; var labelSelector = "text#" + id + ".textnumbers"; var selectedLabel = d3Selection.select(labelSelector).node(); var selector = "rect#" + id; var selectedRect = d3Selection.select(selector).node(); click(d, index, datum, selectedLabel, selectedRect, xScale.invert(point[0])); }) ; // appends the NUMBER LABEL view.selectAll("svg").data(data).enter() .filter(function(d) { return d.labelNumber !== undefined; }) .append("text") .attr("class", "textnumbers") .attr("id", function(d) { return d.id }) .attr("x", function(d, i) { return getXTextPos(d, i, d.labelNumber, '.textnumbers')}) .attr("y", getStackTextPosition) .text(function(d) { return d.labelNumber; }) .on("click", function(d, i){ // when clicking on the label, call the click for the rectangle with the same id var point = d3Selection.mouse(this); var id = this.id; var selectedLabel = d3Selection.select(this).node(); var selector = "rect#" + id; var selectedRect = d3Selection.select(selector).node(); click(d, index, datum, selectedLabel, selectedRect, xScale.invert(point[0])); }) ; if (rowSeparatorsColor) { var lineYAxis = ( itemHeight + itemMargin / 2 + margin.top + (itemHeight + itemMargin) * yAxisMapping[index]); gParent.append("svg:line") .attr("class", "row-separator") .attr("x1", 0 + margin.left) .attr("x2", width - margin.right) .attr("y1", lineYAxis) .attr("y2", lineYAxis) .attr("stroke-width", 1) .attr("stroke", rowSeparatorsColor); } // add the label if (hasLabel) { appendLabel(gParent, yAxisMapping, index, hasLabel, datum); } if (typeof(datum.icon) !== "undefined") { gParent.append("image") .attr("class", "timeline-label") .attr("transform", "translate("+ 0 +","+ (margin.top + (itemHeight + itemMargin) * yAxisMapping[index])+")") .attr("xlink:href", datum.icon) .attr("width", margin.left) .attr("height", itemHeight); } function getStackPosition(d, i) { if (stacked) { return margin.top + (itemHeight + itemMargin) * yAxisMapping[index]; } return margin.top; } function getStackTextPosition(d, i) { if (stacked) { return margin.top + (itemHeight + itemMargin) * yAxisMapping[index] + itemHeight * 0.75; } return margin.top + itemHeight * 0.75; } }); }); var belowLastItem = (margin.top + (itemHeight + itemMargin) * maxStack); var aboveFirstItem = margin.top; var timeAxisYPosition = showAxisTop ? aboveFirstItem : belowLastItem; var gX; if (showTimeAxis) { gX = appendTimeAxis(g, xAxis, timeAxisYPosition); } if (timeAxisTick) { appendTimeAxisTick(g, xAxis, maxStack); } if (width > gParentSize.width) { // only if the scrolling should be allowed var move = function() { g.select(".view") .attr("transform", "translate(" + d3Selection.event.transform.x + ",0)" + "scale(" + d3Selection.event.transform.k + " 1)"); g.selectAll(".timeline-xAxis") .attr("transform", function(d) { return "translate(" + d3Selection.event.transform.x + ", " + timeAxisYPosition + ")" + "scale(" + d3Selection.event.transform.k + " 1)"; }); var new_xScale = d3Selection.event.transform.rescaleX(xScale); g.selectAll('.timeline-xAxis').call(function(d) { xAxis.scale(new_xScale); }); var xpos = -d3Selection.event.transform.x; scroll(xpos, xScale); }; }; var zoom = d3Zoom.zoom() .scaleExtent([0, maxZoom]) // max zoom defaults to 5 .translateExtent([[0, 0], [width, 0]]) // [x0, y0], [x1, y1] don't allow translating y-axis .on("zoom", move); gParent .classed("scrollable", true) .call(zoom); if (! allowZoom) { g.on("wheel", function() { d3Selection.event.preventDefault(); d3Selection.event.stopImmediatePropagation(); }); g.on("dblclick.zoom", function() { d3Selection.event.preventDefault(); d3Selection.event.stopImmediatePropagation(); }); } if (rotateTicks) { g.selectAll(".tick text") .attr("transform", function(d) { return "rotate(" + rotateTicks + ")translate(" + (this.getBBox().width / 2 + 10) + "," // TODO: change this 10 + this.getBBox().height / 2 + ")"; }); } // use the size of the elements added to the timeline to set the height //var gSize = g._groups[0][0].getBoundingClientRect(); var gSize = g.node().getBoundingClientRect(); setHeight(); if (showBorderLine) { g.each(function (d, i) { d.forEach(function (datum) { var times = datum.times; times.forEach(function (time) { appendLine(xScale(time.starting_time), showBorderFormat, showBorderLineClass); appendLine(xScale(time.ending_time), showBorderFormat, showBorderLineClass); }); }); }); } if (showTodayLine) { var todayLine = xScale(new Date()); appendLine(todayLine, showTodayFormat); } function getXPos(d, i) { return margin.left + (d.starting_time - beginning) * scaleFactor; } function getTextWidth(text, font) { // re-use canvas object for better performance var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas")); var context = canvas.getContext("2d"); context.font = font; var metrics = context.measureText(text); return metrics.width; } function getXTextPos(d, i, text, style) { var width = 0; if (d.ending_time) { width = (((d.ending_time - d.starting_time) / 2) * scaleFactor); } if (text && style) { // get the style data for the class selector pass in var textl = getComputedStyle(document.querySelector(style)); // create a fontsize fontfamily string - 12pt Graphik var fontInfo = textl.fontSize + ' ' + textl.fontFamily; // calculate the width of the text in that fontsize var tl = getTextWidth(text, fontInfo); // subtract half of the text length from the xPosition to keep the text centered var textLength = tl / 2; var xPosition = margin.left + ((d.starting_time - beginning) * scaleFactor) + width - textLength; return xPosition; } else { return margin.left + (d.starting_time - beginning) * scaleFactor + 5; } } function setHeight() { if (!height && !gParentSize.height) { if (itemHeight) { // set height based off of item height height = gSize.height + gSize.top - gParentSize.top; // set bounding rectangle height d3Selection.select(gParent).node().attr("height", height); //select(view).node().attr("height", height); } else { throw "height of the timeline is not set"; } } else { if (!height) { height = gParentSize.height; } else { gParentItem.node().attr("height", height); //view.node().attr("height", height); } } } function setWidth() { if (!width && !gParentSize.width) { try { width = gParentItem.node().attr("width"); if (!width) { throw "width of the timeline is not set. As of Firefox 27, timeline().with(x) needs to be explicitly set in order to render"; } } catch (err) { console.log( err ); } } else if (!width && gParentSize.width) { try { width = gParentSize.width; } catch (err) { console.log( err ); } } // if both are set, do nothing } function appendLine(lineScale, lineFormat, lineClass) { lineClass = lineClass || "timeline-line"; view.append("svg:line") .attr("x1", lineScale) .attr("y1", lineFormat.marginTop) .attr("x2", lineScale) .attr("y2", height - lineFormat.marginBottom) .attr("class", lineClass) .style("stroke", lineFormat.color)//"rgb(6,120,155)" .style("stroke-width", lineFormat.width); } } // SETTINGS timelines.margin = function (p) { if (!arguments.length) return margin; margin = p; return timelines; }; timelines.orient = function (orientation) { if (!arguments.length) return orient; orient = orientation; return timelines; }; timelines.itemHeight = function (h) { if (!arguments.length) return itemHeight; itemHeight = h; return timelines; }; timelines.itemMargin = function (h) { if (!arguments.length) return itemMargin; itemMargin = h; return timelines; }; timelines.navMargin = function (h) { if (!arguments.length) return navMargin; navMargin = h; return timelines; }; timelines.height = function (h) { if (!arguments.length) return height; height = h; return timelines; }; timelines.width = function (w) { if (!arguments.length) return width; width = w; return timelines; }; timelines.display = function (displayType) { if (!arguments.length || (DISPLAY_TYPES.indexOf(displayType) == -1)) return display; display = displayType; return timelines; }; timelines.labelFormat = function(f) { if (!arguments.length) return labelFunction; labelFunction = f; return timelines; }; timelines.tickFormat = function (format) { if (!arguments.length) return tickFormat; tickFormat = format; return timelines; }; timelines.allowZoom = function (zoomSetting) { if (!arguments.length) return allowZoom; allowZoom = zoomSetting; return timelines; }; timelines.maxZoom = function (max) { if (!arguments.length) return maxZoom; maxZoom = max; return timelines; }; timelines.hover = function (hoverFunc) { if (!arguments.length) return hover; hover = hoverFunc; return timelines; }; timelines.mouseover = function (mouseoverFunc) { if (!arguments.length) return mouseover; mouseover = mouseoverFunc; return timelines; }; timelines.mouseout = function (mouseoutFunc) { if (!arguments.length) return mouseout; mouseout = mouseoutFunc; return timelines; }; timelines.click = function (clickFunc) { if (!arguments.length) return click; click = clickFunc; return timelines; }; timelines.scroll = function (scrollFunc) { if (!arguments.length) return scroll; scroll = scrollFunc; return timelines; }; timelines.colors = function (colorFormat) { if (!arguments.length) return colorCycle; colorCycle = colorFormat; return timelines; }; timelines.beginning = function (b) { if (!arguments.length) return beginning; beginning = b; return timelines; }; timelines.ending = function (e) { if (!arguments.length) return ending; ending = e; return timelines; }; timelines.labelMargin = function (m) { if (!arguments.length) return labelMargin; labelMargin = m; return timelines; }; timelines.labelFloat = function (f) { if (!arguments.length) return labelFloat; labelFloat = f; return timelines; }; timelines.rotateTicks = function (degrees) { if (!arguments.length) return rotateTicks; rotateTicks = degrees; return timelines; }; timelines.stack = function () { stacked = !stacked; return timelines; }; timelines.relativeTime = function() { timeIsRelative = !timeIsRelative; return timelines; }; timelines.linearTime = function() { timeIsLinear = !timeIsLinear; return timelines; }; timelines.showBorderLine = function () { showBorderLine = !showBorderLine; return timelines; }; timelines.showBorderFormat = function(borderFormat) { if (!arguments.length) return showBorderFormat; showBorderFormat = borderFormat; return timelines; }; // CSS class for the lines added by showBorder timelines.showBorderLineClass = function(borderClass) { if (!arguments.length) return showBorderLineClass; showBorderLineClass = borderClass; return timelines; }; timelines.showToday = function () { showTodayLine = !showTodayLine; return timelines; }; timelines.showTodayFormat = function(todayFormat) { if (!arguments.length) return showTodayFormat; showTodayFormat = todayFormat; return timelines; }; timelines.colorProperty = function(colorProp) { if (!arguments.length) return colorPropertyName; colorPropertyName = colorProp; return timelines; }; timelines.rowSeparators = function (color) { if (!arguments.length) return rowSeparatorsColor; rowSeparatorsColor = color; return timelines; }; timelines.background = function (color) { if (!arguments.length) return backgroundColor; backgroundColor = color; return timelines; }; timelines.showTimeAxis = function () { showTimeAxis = !showTimeAxis; return timelines; }; timelines.showAxisTop = function () { showAxisTop = !showAxisTop; return timelines; }; timelines.showAxisCalendarYear = function () { showAxisCalendarYear = !showAxisCalendarYear; return timelines; }; timelines.showTimeAxisTick = function () { timeAxisTick = !timeAxisTick; return timelines; }; timelines.fullLengthBackgrounds = function () { fullLengthBackgrounds = !fullLengthBackgrounds; return timelines; }; timelines.showTimeAxisTickFormat = function(format) { if (!arguments.length) return timeAxisTickFormat; timeAxisTickFormat = format; return timelines; }; timelines.showAxisHeaderBackground = function(bgColor) { showAxisHeaderBackground = !showAxisHeaderBackground; if(bgColor) { (axisBgColor = bgColor); } return timelines; }; // CSS class for the x-axis timelines.xAxisClass = function (axisClass) { if (!arguments.length) return xAxisClass; xAxisClass = axisClass; return timelines; }; timelines.navigate = function (navigateBackwards, navigateForwards) { if (!arguments.length) return [navigateLeft, navigateRight]; navigateLeft = navigateBackwards; navigateRight = navigateForwards; showAxisNav = !showAxisNav; return timelines; }; timelines.version = function() { return "1.0.0"; }; return timelines; }; exports.timelines = timelines; Object.defineProperty(exports, '__esModule', { value: true }); })); ================================================ FILE: examples/days.html ================================================

Differentiation

Erythroid - J.Namath Nature 2010
11 Days
 

Assays

================================================ FILE: examples/days_scrollable.html ================================================

Differentiation

Erythroid - J.Namath Nature 2010
11 Days
 

Assays

Assay Type Timepoint Replication
================================================ FILE: examples/example.html ================================================

A simple timeline

A simple timeline without Axis

It works with circles too

Combine circles and rectangles

A stacked timeline with hover, click, and scroll events

We can also use icons

We can change colors and put labels

We can also rotate tick mark labels

A timeline with colors mapped from the data

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

A timeline with relative timepoints

A stacked timeline with axis on top

A stacked timeline with backgrounds

A stacked timeline with backgrounds and ticks

A complex timeline

================================================ FILE: examples/scrollable.html ================================================

A stacked timeline with hover, click, and scroll events

================================================ FILE: examples/sp500.csv ================================================ date,price Jan 2000,1394.46 Feb 2000,1366.42 Mar 2000,1498.58 Apr 2000,1452.43 May 2000,1420.6 Jun 2000,1454.6 Jul 2000,1430.83 Aug 2000,1517.68 Sep 2000,1436.51 Oct 2000,1429.4 Nov 2000,1314.95 Dec 2000,1320.28 Jan 2001,1366.01 Feb 2001,1239.94 Mar 2001,1160.33 Apr 2001,1249.46 May 2001,1255.82 Jun 2001,1224.38 Jul 2001,1211.23 Aug 2001,1133.58 Sep 2001,1040.94 Oct 2001,1059.78 Nov 2001,1139.45 Dec 2001,1148.08 Jan 2002,1130.2 Feb 2002,1106.73 Mar 2002,1147.39 Apr 2002,1076.92 May 2002,1067.14 Jun 2002,989.82 Jul 2002,911.62 Aug 2002,916.07 Sep 2002,815.28 Oct 2002,885.76 Nov 2002,936.31 Dec 2002,879.82 Jan 2003,855.7 Feb 2003,841.15 Mar 2003,848.18 Apr 2003,916.92 May 2003,963.59 Jun 2003,974.5 Jul 2003,990.31 Aug 2003,1008.01 Sep 2003,995.97 Oct 2003,1050.71 Nov 2003,1058.2 Dec 2003,1111.92 Jan 2004,1131.13 Feb 2004,1144.94 Mar 2004,1126.21 Apr 2004,1107.3 May 2004,1120.68 Jun 2004,1140.84 Jul 2004,1101.72 Aug 2004,1104.24 Sep 2004,1114.58 Oct 2004,1130.2 Nov 2004,1173.82 Dec 2004,1211.92 Jan 2005,1181.27 Feb 2005,1203.6 Mar 2005,1180.59 Apr 2005,1156.85 May 2005,1191.5 Jun 2005,1191.33 Jul 2005,1234.18 Aug 2005,1220.33 Sep 2005,1228.81 Oct 2005,1207.01 Nov 2005,1249.48 Dec 2005,1248.29 Jan 2006,1280.08 Feb 2006,1280.66 Mar 2006,1294.87 Apr 2006,1310.61 May 2006,1270.09 Jun 2006,1270.2 Jul 2006,1276.66 Aug 2006,1303.82 Sep 2006,1335.85 Oct 2006,1377.94 Nov 2006,1400.63 Dec 2006,1418.3 Jan 2007,1438.24 Feb 2007,1406.82 Mar 2007,1420.86 Apr 2007,1482.37 May 2007,1530.62 Jun 2007,1503.35 Jul 2007,1455.27 Aug 2007,1473.99 Sep 2007,1526.75 Oct 2007,1549.38 Nov 2007,1481.14 Dec 2007,1468.36 Jan 2008,1378.55 Feb 2008,1330.63 Mar 2008,1322.7 Apr 2008,1385.59 May 2008,1400.38 Jun 2008,1280 Jul 2008,1267.38 Aug 2008,1282.83 Sep 2008,1166.36 Oct 2008,968.75 Nov 2008,896.24 Dec 2008,903.25 Jan 2009,825.88 Feb 2009,735.09 Mar 2009,797.87 Apr 2009,872.81 May 2009,919.14 Jun 2009,919.32 Jul 2009,987.48 Aug 2009,1020.62 Sep 2009,1057.08 Oct 2009,1036.19 Nov 2009,1095.63 Dec 2009,1115.1 Jan 2010,1073.87 Feb 2010,1104.49 Mar 2010,1140.45 ================================================ FILE: examples/timeline.html ================================================

A stacked timeline with hover, click, and scroll events

 
================================================ FILE: examples/timelineStacked.html ================================================

A stacked timeline with hover, click, and scroll events

A timeline with hover, click, and scroll events

================================================ FILE: index.js ================================================ export { default as timelines } from "./src/timelines"; ================================================ FILE: package.json ================================================ { "name": "d3-timelines", "version": "1.3.1", "description": "A d3 v4 version of timeline. Can display single bar timelines, timelines with icons, and timelines that pan.", "keywords": [ "d3", "d3-module", "d3-timelines" ], "license": "BSD-3-Clause", "main": "build/d3-timelines.js", "jsnext:main": "index", "homepage": "https://github.com/denisemauldin/d3-timeline", "repository": { "type": "git", "url": "https://github.com/denisemauldin/d3-timeline.git" }, "scripts": { "pretest": "rm -rf build && mkdir build && rollup -c -f umd -n d3 -o build/d3-timelines.js -- index.js", "test": "tape 'test/**/*-test.js'", "prepare": "npm run test && uglifyjs build/d3-timelines.js -c -m -o build/d3-timelines.min.js", "postpublish": "zip -j build/d3-timelines.zip -- LICENSE README.md build/d3-timelines.js build/d3-timelines.min.js" }, "peerDependencies": { "d3-array": "^1.2.0", "d3-axis": "^1.0.7", "d3-scale": "^1.0.6", "d3-selection": "^1.1.0", "d3-time": "^1.0.6", "d3-time-format": "^2.0.5", "d3-zoom": "^1.2.0" }, "devDependencies": { "@types/d3": "^4.5", "rollup": "0.27", "rollup-plugin-node-resolve": "^3.0.0", "tape": "4", "uglify-js": "2" }, "dependencies": { "d3": "^4.9.1", "d3-array": "^1.2.0", "d3-axis": "^1.0.7", "d3-scale": "^1.0.6", "d3-selection": "^1.1.0", "d3-time": "^1.0.6", "d3-time-format": "^2.0.5", "d3-zoom": "^1.2.0" } } ================================================ FILE: rollup.config.js ================================================ import nodeResolve from 'rollup-plugin-node-resolve'; let pkg = require("./package.json"); let external = Object.keys(pkg.peerDependencies); export default { entry: 'index.js', dest: 'bundle.js', format: 'umd', moduleName: 'd3-timeline', external: external, globals: { "d3-axis": "d3", "d3-array": "d3", "d3-time-format": "d3", "d3-time": "d3", "d3-selection": "d3", "d3-scale": "d3", "d3-zoom": "d3" }, plugins: [nodeResolve({ jsnext: true, main: true})] }; ================================================ FILE: src/timelines.js ================================================ import { axisBottom, axisTop } from 'd3-axis'; import { range } from 'd3-array'; import { timeFormat } from 'd3-time-format'; import { timeHour } from 'd3-time'; import { scaleOrdinal, scaleTime, scaleLinear, schemeCategory20 } from 'd3-scale'; import { event, mouse, namespace, namespaces, select } from 'd3-selection'; import { zoom as d3z } from 'd3-zoom' var timelines = function() { var DISPLAY_TYPES = ["circle", "rect"]; var hover = function () {}, mouseover = function () {}, mouseout = function () {}, click = function () {}, scroll = function () {}, labelFunction = function(label) { return label; }, labelFloat = 0, // floats up this many pixels navigateLeft = function () {}, navigateRight = function () {}, orient = "bottom", width = null, height = null, rowSeparatorsColor = null, backgroundColor = null, tickFormat = { format: timeFormat("%I %p"), tickTime: timeHour, tickInterval: 1, tickSize: 6, tickValues: null }, allowZoom = true, axisBgColor = "white", chartData = {}, colorCycle = scaleOrdinal(schemeCategory20), colorPropertyName = null, display = "rect", beginning = 0, labelMargin = 0, ending = 0, margin = {left: 30, right:30, top: 30, bottom:30}, maxZoom = 5, stacked = false, rotateTicks = false, timeIsRelative = false, timeIsLinear = false, fullLengthBackgrounds = false, itemHeight = 20, itemMargin = 5, navMargin = 60, showTimeAxis = true, showAxisTop = false, showTodayLine = false, timeAxisTick = false, timeAxisTickFormat = {stroke: "stroke-dasharray", spacing: "4 10"}, showTodayFormat = {marginTop: 25, marginBottom: 0, width: 1, color: colorCycle}, showBorderLine = false, showBorderFormat = {marginTop: 25, marginBottom: 0, width: 1, color: colorCycle}, showBorderLineClass = "timeline-border-line", showAxisHeaderBackground = false, showAxisNav = false, showAxisCalendarYear = false, xAxisClass = 'timeline-xAxis', xScale = null, xAxis = null ; var appendTimeAxis = function(g, xAxis, yPosition) { if(showAxisHeaderBackground){ appendAxisHeaderBackground(g, 0, 0); } if(showAxisNav){ appendTimeAxisNav(g); } var axis = g.append("g") .attr("class", xAxisClass) .attr("transform", "translate(" + 0 + "," + yPosition + ")") .call(xAxis); return axis; }; var appendTimeAxisCalendarYear = function (nav) { var calendarLabel = beginning.getFullYear(); if (beginning.getFullYear() != ending.getFullYear()) { calendarLabel = beginning.getFullYear() + "-" + ending.getFullYear(); } nav.append("text") .attr("transform", "translate(" + 20 + ", 0)") .attr("x", 0) .attr("y", 14) .attr("class", "calendarYear") .text(calendarLabel) ; }; var appendTimeAxisNav = function (g) { var timelineBlocks = 6; var leftNavMargin = (margin.left - navMargin); var incrementValue = (width - margin.left)/timelineBlocks; var rightNavMargin = (width - margin.right - incrementValue + navMargin); var nav = g.append('g') .attr("class", "axis") .attr("transform", "translate(0, 20)") ; if(showAxisCalendarYear) { appendTimeAxisCalendarYear(nav); } nav.append("text") .attr("transform", "translate(" + leftNavMargin + ", 0)") .attr("x", 0) .attr("y", 14) .attr("class", "chevron") .text("<") .on("click", function () { return navigateLeft(beginning, chartData); }) ; nav.append("text") .attr("transform", "translate(" + rightNavMargin + ", 0)") .attr("x", 0) .attr("y", 14) .attr("class", "chevron") .text(">") .on("click", function () { return navigateRight(ending, chartData); }) ; }; var appendAxisHeaderBackground = function (g, xAxis, yAxis) { g.insert("rect") .attr("class", "row-green-bar") .attr("x", xAxis) .attr("width", width) .attr("y", yAxis) .attr("height", itemHeight) .attr("fill", axisBgColor); }; var appendTimeAxisTick = function(g, xAxis, maxStack) { g.append("g") .attr("class", "axis") .attr("transform", "translate(" + 0 + "," + (margin.top + (itemHeight + itemMargin) * maxStack) + ")") .attr(timeAxisTickFormat.stroke, timeAxisTickFormat.spacing) .call(xAxis.tickFormat("").tickSize(-(margin.top + (itemHeight + itemMargin) * (maxStack - 1) + 3), 0, 0)); }; var appendBackgroundBar = function (yAxisMapping, index, g, data, datum) { var greenbarYAxis = ((itemHeight + itemMargin) * yAxisMapping[index]) + margin.top; g.selectAll("svg") .data(data).enter() .insert("rect", ":first-child") .attr("class", "row-green-bar") .attr("x", fullLengthBackgrounds ? 0 : margin.left) .attr("width", fullLengthBackgrounds ? width : (width - margin.right - margin.left)) .attr("y", greenbarYAxis) .attr("height", itemHeight) .attr("fill", backgroundColor instanceof Function ? backgroundColor(datum, index) : backgroundColor) ; }; var appendLabel = function (gParent, yAxisMapping, index, hasLabel, datum) { var fullItemHeight = itemHeight + itemMargin; var rowsDown = margin.top + (fullItemHeight/2) + fullItemHeight * (yAxisMapping[index] || 1); gParent.append("text") .attr("class", "timeline-label") .attr("transform", "translate(" + labelMargin + "," + rowsDown + ")") .text(hasLabel ? labelFunction(datum.label) : datum.id) .on("click", function (d, i) { console.log("label click!"); var point = mouse(this); gParent.append("rect") .attr("id", "clickpoint") .attr("x", point[0]) .attr("width", 10) .attr("height", itemHeight); click(d, index, datum, point, xScale.invert(point[0])); }); }; /*########################### #### START timelines ### #############################*/ function timelines (gParent) { var gParentSize = gParent.node().getBoundingClientRect(); // the svg size var gParentItem = select(gParent.node()); // the svg var g = gParent.append("g").attr("class", "container"); var yAxisMapping = {}, maxStack = 1, minTime = 0, maxTime = 0; setWidth(); // check if the user wants relative time // if so, substract the first timestamp from each subsequent timestamps if(timeIsRelative){ g.each(function (d, i) { var originTime = 0; d.forEach(function (datum, index) { datum.times.forEach(function (time, j) { if(index === 0 && j === 0){ originTime = time.starting_time; //Store the timestamp that will serve as origin time.starting_time = 0; //Set tahe origin time.ending_time = time.ending_time - originTime; //Store the relative time (millis) }else{ time.starting_time = time.starting_time - originTime; time.ending_time = time.ending_time - originTime; } }); }); }); } // check how many stacks we're gonna need // do this here so that we can draw the axis before the graph if (stacked || ending === 0 || beginning === 0) { g.each(function (d, i) { d.forEach(function (datum, index) { // create y mapping for stacked graph if (stacked && Object.keys(yAxisMapping).indexOf(index) == -1) { yAxisMapping[index] = maxStack; maxStack++; } // figure out beginning and ending times if they are unspecified datum.times.forEach(function (time, i) { if(beginning === 0) if (time.starting_time < minTime || (minTime === 0 && timeIsRelative === false)) minTime = time.starting_time; if(ending === 0) if (time.ending_time > maxTime) maxTime = time.ending_time; }); }); }); if (ending === 0) { ending = maxTime; } if (beginning === 0) { beginning = minTime; } } var scaleFactor = (1/(ending - beginning)) * (width - margin.left - margin.right); function formatDays(d) { var days = Math.floor(d / 86400), hours = Math.floor((d - (days * 86400)) / 3600), minutes = Math.floor((d - (days * 86400) - (hours * 3600)) / 60), seconds = d - (days * 86400) - (hours * 3600) - (minutes * 60); var output = ''; if (seconds) { output = seconds + 's'; } if (minutes) { output = minutes + 'm ' + output; } if (hours) { output = hours + 'h ' + output; } if (days) { output = days + 'd ' + output; } return output; }; if (orient == "bottom") { xAxis = axisBottom(); } else if (orient == "top") { xAxis = axisTop(); } if (timeIsLinear) { xScale = scaleLinear() .domain([beginning, ending]) .range([margin.left, width - margin.right]); xAxis.scale(xScale) .tickFormat(formatDays) .tickValues(range(0, ending, 86400)); } else { xScale = scaleTime() .domain([beginning, ending]) .range([margin.left, width - margin.right]); xAxis.scale(xScale) .tickFormat(tickFormat.format) .tickSize(tickFormat.tickSize); } if (tickFormat.tickValues !== null) { xAxis.tickValues(tickFormat.tickValues); } else { xAxis.tickArguments(tickFormat.numTicks || [tickFormat.tickTime, tickFormat.tickInterval]); } // append a view for zoom/pan support var view = g.append("g") .attr("class", "view"); // draw the chart g.each(function(d, i) { chartData = d; d.forEach( function(datum, index){ var data = datum.times; data.forEach(function(d) { d.name = datum.name }); var hasLabel = (typeof(datum.label) != "undefined"); // issue warning about using id per data set. Ids should be individual to data elements if (typeof(datum.id) != "undefined") { console.warn("d3Timeline Warning: Ids per dataset is deprecated in favor of a 'class' key. Ids are now per data element."); } if (backgroundColor) { appendBackgroundBar(yAxisMapping, index, g, data, datum); } view.selectAll("svg") .data(data).enter() .append(function(d, i) { return document.createElementNS(namespaces.svg, "display" in d? d.display:display); }) .attr("x", getXPos) .attr("y", getStackPosition) .attr("width", function (d, i) { return (d.ending_time - d.starting_time) * scaleFactor; }) .attr("cy", function(d, i) { return getStackPosition(d, i) + itemHeight/2; }) .attr("cx", getXPos) .attr("r", itemHeight / 2) .attr("height", itemHeight) .style("fill", function(d, i){ var dColorPropName; if (d.color) return d.color; if( colorPropertyName ){ dColorPropName = d[colorPropertyName]; if ( dColorPropName ) { return colorCycle( dColorPropName ); } else { return colorCycle( datum[colorPropertyName] ); } } return colorCycle(index); }) .on("mousemove", function (d, i) { hover(d, index, datum, i); }) .on("mouseover", function (d, i) { mouseover(d, i, datum, i); }) .on("mouseout", function (d, i) { mouseout(d, i, datum, i); }) .on("click", function (d, i) { var point = mouse(this); var selectedRect = select(this).node(); var selectorLabel = "text#" + selectedRect.id + '.textnumbers'; var selectedLabel = select(selectorLabel).node(); click(d, index, datum, selectedLabel, selectedRect, xScale.invert(point[0])); }) .attr("class", function (d, i) { return datum.class ? "timelineSeries_"+datum.class : "timelineSeries_"+index; }) .attr("id", function(d, i) { // use deprecated id field if (datum.id && !d.id) { return 'timelineItem_'+datum.id; } return d.id ? d.id : "timelineItem_"+index+"_"+i; }) ; // appends the labels to the boxes - DAY/HOUR LABEL view.selectAll("svg") .data(data).enter() .append("text") .attr("class", "textlabels") .attr("id", function(d) { return d.id }) .attr("x", function(d, i) { return getXTextPos(d, i, d.label, '.textlabels')}) .attr("y", (getStackTextPosition() - labelFloat)) .text(function(d) { return d.label; }) .on("click", function(d, i){ // when clicking on the label, call the click for the rectangle with the same id var point = mouse(this); var id = this.id; var labelSelector = "text#" + id + ".textnumbers"; var selectedLabel = select(labelSelector).node(); var selector = "rect#" + id; var selectedRect = select(selector).node(); click(d, index, datum, selectedLabel, selectedRect, xScale.invert(point[0])); }) ; // appends the NUMBER LABEL view.selectAll("svg").data(data).enter() .filter(function(d) { return d.labelNumber !== undefined; }) .append("text") .attr("class", "textnumbers") .attr("id", function(d) { return d.id }) .attr("x", function(d, i) { return getXTextPos(d, i, d.labelNumber, '.textnumbers')}) .attr("y", getStackTextPosition) .text(function(d) { return d.labelNumber; }) .on("click", function(d, i){ // when clicking on the label, call the click for the rectangle with the same id var point = mouse(this); var id = this.id; var selectedLabel = select(this).node(); var selector = "rect#" + id; var selectedRect = select(selector).node(); click(d, index, datum, selectedLabel, selectedRect, xScale.invert(point[0])); }) ; if (rowSeparatorsColor) { var lineYAxis = ( itemHeight + itemMargin / 2 + margin.top + (itemHeight + itemMargin) * yAxisMapping[index]); gParent.append("svg:line") .attr("class", "row-separator") .attr("x1", 0 + margin.left) .attr("x2", width - margin.right) .attr("y1", lineYAxis) .attr("y2", lineYAxis) .attr("stroke-width", 1) .attr("stroke", rowSeparatorsColor); } // add the label if (hasLabel) { appendLabel(gParent, yAxisMapping, index, hasLabel, datum); } if (typeof(datum.icon) !== "undefined") { gParent.append("image") .attr("class", "timeline-label") .attr("transform", "translate("+ 0 +","+ (margin.top + (itemHeight + itemMargin) * yAxisMapping[index])+")") .attr("xlink:href", datum.icon) .attr("width", margin.left) .attr("height", itemHeight); } function getStackPosition(d, i) { if (stacked) { return margin.top + (itemHeight + itemMargin) * yAxisMapping[index]; } return margin.top; } function getStackTextPosition(d, i) { if (stacked) { return margin.top + (itemHeight + itemMargin) * yAxisMapping[index] + itemHeight * 0.75; } return margin.top + itemHeight * 0.75; } }); }); var belowLastItem = (margin.top + (itemHeight + itemMargin) * maxStack); var aboveFirstItem = margin.top; var timeAxisYPosition = showAxisTop ? aboveFirstItem : belowLastItem; var gX; if (showTimeAxis) { gX = appendTimeAxis(g, xAxis, timeAxisYPosition); } if (timeAxisTick) { appendTimeAxisTick(g, xAxis, maxStack); } if (width > gParentSize.width) { // only if the scrolling should be allowed var move = function() { g.select(".view") .attr("transform", "translate(" + event.transform.x + ",0)" + "scale(" + event.transform.k + " 1)"); g.selectAll(".timeline-xAxis") .attr("transform", function(d) { return "translate(" + event.transform.x + ", " + timeAxisYPosition + ")" + "scale(" + event.transform.k + " 1)"; }); var new_xScale = event.transform.rescaleX(xScale); g.selectAll('.timeline-xAxis').call(function(d) { xAxis.scale(new_xScale); }); var xpos = -event.transform.x; scroll(xpos, xScale); }; }; if (! allowZoom) { var zoom = d3z() .scaleExtent([0, maxZoom]) // max zoom defaults to 5 .translateExtent([[0, 0], [width, 0]]) // [x0, y0], [x1, y1] don't allow translating y-axis .on("zoom", move); gParent.classed("scrollable", true) .call(zoom); g.on("wheel", function() { event.preventDefault(); event.stopImmediatePropagation(); }); g.on("dblclick.zoom", function() { event.preventDefault(); event.stopImmediatePropagation(); }); } if (rotateTicks) { g.selectAll(".tick text") .attr("transform", function(d) { return "rotate(" + rotateTicks + ")translate(" + (this.getBBox().width / 2 + 10) + "," // TODO: change this 10 + this.getBBox().height / 2 + ")"; }); } // use the size of the elements added to the timeline to set the height //var gSize = g._groups[0][0].getBoundingClientRect(); var gSize = g.node().getBoundingClientRect(); setHeight(); if (showBorderLine) { g.each(function (d, i) { d.forEach(function (datum) { var times = datum.times; times.forEach(function (time) { appendLine(xScale(time.starting_time), showBorderFormat, showBorderLineClass); appendLine(xScale(time.ending_time), showBorderFormat, showBorderLineClass); }); }); }); } if (showTodayLine) { var todayLine = xScale(new Date()); appendLine(todayLine, showTodayFormat); } function getXPos(d, i) { return margin.left + (d.starting_time - beginning) * scaleFactor; } function getTextWidth(text, font) { // re-use canvas object for better performance var canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement("canvas")); var context = canvas.getContext("2d"); context.font = font; var metrics = context.measureText(text); return metrics.width; } function getXTextPos(d, i, text, style) { var width = 0; if (d.ending_time) { width = (((d.ending_time - d.starting_time) / 2) * scaleFactor); } if (text && style) { // get the style data for the class selector pass in var textl = getComputedStyle(document.querySelector(style)); // create a fontsize fontfamily string - 12pt Graphik var fontInfo = textl.fontSize + ' ' + textl.fontFamily; // calculate the width of the text in that fontsize var tl = getTextWidth(text, fontInfo); // subtract half of the text length from the xPosition to keep the text centered var textLength = tl / 2; var xPosition = margin.left + ((d.starting_time - beginning) * scaleFactor) + width - textLength; return xPosition; } else { return margin.left + (d.starting_time - beginning) * scaleFactor + 5; } } function setHeight() { if (!height && !gParentSize.height) { if (itemHeight) { // set height based off of item height height = gSize.height + gSize.top - gParentSize.top; // set bounding rectangle height select(gParent).node().attr("height", height); //select(view).node().attr("height", height); } else { throw "height of the timeline is not set"; } } else { if (!height) { height = gParentSize.height; } else { gParentItem.node().attr("height", height); //view.node().attr("height", height); } } } function setWidth() { if (!width && !gParentSize.width) { try { width = gParentItem.node().attr("width"); if (!width) { throw "width of the timeline is not set. As of Firefox 27, timeline().with(x) needs to be explicitly set in order to render"; } } catch (err) { console.log( err ); } } else if (!width && gParentSize.width) { try { width = gParentSize.width; } catch (err) { console.log( err ); } } // if both are set, do nothing } function appendLine(lineScale, lineFormat, lineClass) { lineClass = lineClass || "timeline-line"; view.append("svg:line") .attr("x1", lineScale) .attr("y1", lineFormat.marginTop) .attr("x2", lineScale) .attr("y2", height - lineFormat.marginBottom) .attr("class", lineClass) .style("stroke", lineFormat.color)//"rgb(6,120,155)" .style("stroke-width", lineFormat.width); } } // SETTINGS timelines.margin = function (p) { if (!arguments.length) return margin; margin = p; return timelines; }; timelines.orient = function (orientation) { if (!arguments.length) return orient; orient = orientation; return timelines; }; timelines.itemHeight = function (h) { if (!arguments.length) return itemHeight; itemHeight = h; return timelines; }; timelines.itemMargin = function (h) { if (!arguments.length) return itemMargin; itemMargin = h; return timelines; }; timelines.navMargin = function (h) { if (!arguments.length) return navMargin; navMargin = h; return timelines; }; timelines.height = function (h) { if (!arguments.length) return height; height = h; return timelines; }; timelines.width = function (w) { if (!arguments.length) return width; width = w; return timelines; }; timelines.display = function (displayType) { if (!arguments.length || (DISPLAY_TYPES.indexOf(displayType) == -1)) return display; display = displayType; return timelines; }; timelines.labelFormat = function(f) { if (!arguments.length) return labelFunction; labelFunction = f; return timelines; }; timelines.tickFormat = function (format) { if (!arguments.length) return tickFormat; tickFormat = format; return timelines; }; timelines.allowZoom = function (zoomSetting) { if (!arguments.length) return allowZoom; allowZoom = zoomSetting; return timelines; }; timelines.maxZoom = function (max) { if (!arguments.length) return maxZoom; maxZoom = max; return timelines; }; timelines.hover = function (hoverFunc) { if (!arguments.length) return hover; hover = hoverFunc; return timelines; }; timelines.mouseover = function (mouseoverFunc) { if (!arguments.length) return mouseover; mouseover = mouseoverFunc; return timelines; }; timelines.mouseout = function (mouseoutFunc) { if (!arguments.length) return mouseout; mouseout = mouseoutFunc; return timelines; }; timelines.click = function (clickFunc) { if (!arguments.length) return click; click = clickFunc; return timelines; }; timelines.scroll = function (scrollFunc) { if (!arguments.length) return scroll; scroll = scrollFunc; return timelines; }; timelines.colors = function (colorFormat) { if (!arguments.length) return colorCycle; colorCycle = colorFormat; return timelines; }; timelines.beginning = function (b) { if (!arguments.length) return beginning; beginning = b; return timelines; }; timelines.ending = function (e) { if (!arguments.length) return ending; ending = e; return timelines; }; timelines.labelMargin = function (m) { if (!arguments.length) return labelMargin; labelMargin = m; return timelines; }; timelines.labelFloat = function (f) { if (!arguments.length) return labelFloat; labelFloat = f; return timelines; }; timelines.rotateTicks = function (degrees) { if (!arguments.length) return rotateTicks; rotateTicks = degrees; return timelines; }; timelines.stack = function () { stacked = !stacked; return timelines; }; timelines.relativeTime = function() { timeIsRelative = !timeIsRelative; return timelines; }; timelines.linearTime = function() { timeIsLinear = !timeIsLinear; return timelines; }; timelines.showBorderLine = function () { showBorderLine = !showBorderLine; return timelines; }; timelines.showBorderFormat = function(borderFormat) { if (!arguments.length) return showBorderFormat; showBorderFormat = borderFormat; return timelines; }; // CSS class for the lines added by showBorder timelines.showBorderLineClass = function(borderClass) { if (!arguments.length) return showBorderLineClass; showBorderLineClass = borderClass; return timelines; }; timelines.showToday = function () { showTodayLine = !showTodayLine; return timelines; }; timelines.showTodayFormat = function(todayFormat) { if (!arguments.length) return showTodayFormat; showTodayFormat = todayFormat; return timelines; }; timelines.colorProperty = function(colorProp) { if (!arguments.length) return colorPropertyName; colorPropertyName = colorProp; return timelines; }; timelines.rowSeparators = function (color) { if (!arguments.length) return rowSeparatorsColor; rowSeparatorsColor = color; return timelines; }; timelines.background = function (color) { if (!arguments.length) return backgroundColor; backgroundColor = color; return timelines; }; timelines.showTimeAxis = function () { showTimeAxis = !showTimeAxis; return timelines; }; timelines.showAxisTop = function () { showAxisTop = !showAxisTop; return timelines; }; timelines.showAxisCalendarYear = function () { showAxisCalendarYear = !showAxisCalendarYear; return timelines; }; timelines.showTimeAxisTick = function () { timeAxisTick = !timeAxisTick; return timelines; }; timelines.fullLengthBackgrounds = function () { fullLengthBackgrounds = !fullLengthBackgrounds; return timelines; }; timelines.showTimeAxisTickFormat = function(format) { if (!arguments.length) return timeAxisTickFormat; timeAxisTickFormat = format; return timelines; }; timelines.showAxisHeaderBackground = function(bgColor) { showAxisHeaderBackground = !showAxisHeaderBackground; if(bgColor) { (axisBgColor = bgColor); } return timelines; }; // CSS class for the x-axis timelines.xAxisClass = function (axisClass) { if (!arguments.length) return xAxisClass; xAxisClass = axisClass; return timelines; }; timelines.navigate = function (navigateBackwards, navigateForwards) { if (!arguments.length) return [navigateLeft, navigateRight]; navigateLeft = navigateBackwards; navigateRight = navigateForwards; showAxisNav = !showAxisNav; return timelines; }; timelines.version = function() { return "1.0.0"; }; return timelines; }; export default timelines; ================================================ FILE: test/timeline-test.js ================================================ var tape = require("tape"), d3 = Object.assign({}, require("../"), require("d3")), timelines = require("../"); tape("timeline() returns the correct version", function(test) { test.equal(timelines.timelines().version(), '1.0.0'); test.end(); });
Assay Type Timepoint Replication