var TimeKnots = { draw: function(id, events, options) { // remove any exisitng elements d3.select(id).select("div").html(""); var TODAY = Date.now(); var cfg = { width: 600, height: 200, radius: 10, lineWidth: 4, color: "#999", background: "#FFF", dateFormat: "%Y/%m/%d %H:%M:%S", horizontalLayout: true, showLabels: false, labelFormat: "%Y/%m/%d %H:%M:%S", addNow: false, seriesColor: d3.scale.category20(), dateDimension: true }; //default configuration overrid if (options != undefined) { for (var i in options) { cfg[i] = options[i]; } } if (cfg.addNow != false) { events.push({ date: new Date(), name: cfg.addNowLabel || "Today" }); } //sort events ascending events.sort(function(a,b){ return new Date(a.date) - new Date(b.date); }); var svg = d3.select(id).select("div").append('svg').attr("width", cfg.width).attr("height", cfg.height); //Calculate times in terms of timestamps if (!cfg.dateDimension) { var timestamps = events.map(function(d) { return d.value }); //new Date(d.date).getTime()}); var maxValue = d3.max(timestamps); var minValue = d3.min(timestamps); } else { var timestamps = events.map(function(d) { return Date.parse(d.date); }); //new Date(d.date).getTime()}); var maxValue = d3.max(timestamps); var minValue = d3.min(timestamps); } var margin = (d3.max(events.map(function(d) { return d.radius })) || cfg.radius) * 1.5 + cfg.lineWidth; var step = (cfg.horizontalLayout) ? ((cfg.width - 2 * margin) / (maxValue - minValue)) : ((cfg.height - 2 * margin) / (maxValue - minValue)); var series = []; if (maxValue == minValue) { step = 0; if (cfg.horizontalLayout) { margin = cfg.width / 2 } else { margin = cfg.height / 2 } } linePrevious = { x1: null, x2: null, y1: null, y2: null } TIMELINE_LEFT_MARGIN = 130; TEXT_LEFT_MARGIN = 110; DATE_LEFT_MARGIN = 100; svg.selectAll("line") .data(events).enter().append("line") .attr("class", "timeline-line") .attr("x1", function(d) { var ret; if (cfg.horizontalLayout) { var datum = (cfg.dateDimension) ? new Date(d.date).getTime() : d.value; ret = Math.floor(step * (datum - minValue) + margin) - TIMELINE_LEFT_MARGIN } else { ret = Math.floor(cfg.width / 2) - TIMELINE_LEFT_MARGIN } linePrevious.x1 = ret return ret }) .attr("x2", function(d) { if (linePrevious.x1 != null) { return linePrevious.x1 } if (cfg.horizontalLayout) { var datum = (cfg.dateDimension) ? new Date(d.date).getTime() : d.value; ret = Math.floor(step * (datum - minValue)) } return Math.floor(cfg.width / 2) - TIMELINE_LEFT_MARGIN }) .attr("y1", function(d) { var ret; if (cfg.horizontalLayout) { ret = Math.floor(cfg.height / 2) } else { var datum = (cfg.dateDimension) ? new Date(d.date).getTime() : d.value; ret = Math.floor(step * (datum - minValue)) + margin } linePrevious.y1 = ret return ret }) .attr("y2", function(d) { if (linePrevious.y1 != null) { return linePrevious.y1 } if (cfg.horizontalLayout) { return Math.floor(cfg.height / 2) } var datum = (cfg.dateDimension) ? new Date(d.date).getTime() : d.value; return Math.floor(step * (datum - minValue)) }) .style("stroke", function(d) { if (d.color != undefined) { return d.color } if (d.series != undefined) { if (series.indexOf(d.series) < 0) { series.push(d.series); } return cfg.seriesColor(series.indexOf(d.series)); } return cfg.color }) .style("stroke-width", cfg.lineWidth); svg.selectAll("circle") .data(events).enter() .append("circle") .attr("class", "timeline-event") .attr("r", function(d) { if (d.radius != undefined) { return d.radius } return cfg.radius }) .style("stroke", function(d) { if (d.color != undefined) { return d.color } if (d.series != undefined) { if (series.indexOf(d.series) < 0) { series.push(d.series); } console.log(d.series, series, series.indexOf(d.series)); return cfg.seriesColor(series.indexOf(d.series)); } return cfg.color }) .style("stroke-width", function(d) { if (d.lineWidth != undefined) { return d.lineWidth } return cfg.lineWidth }) .style("fill", function(d) { if (d.background != undefined) { return d.background } return cfg.background }) .attr("cy", function(d) { if (cfg.horizontalLayout) { return Math.floor(cfg.height / 2) } var datum = (cfg.dateDimension) ? new Date(d.date).getTime() : d.value; return Math.floor(step * (datum - minValue) + margin) }) .attr("cx", function(d) { if (cfg.horizontalLayout) { var datum = (cfg.dateDimension) ? new Date(d.date).getTime() : d.value; var x = Math.floor(step * (datum - minValue) + margin); return x; } return Math.floor(cfg.width / 2) - TIMELINE_LEFT_MARGIN }); svg.selectAll("text") .data(events).enter() .append('text').attr("class", "event-description") .html(function(d){ return d.name; }) .attr("x", function(d) { return Math.floor(cfg.width / 2) - TEXT_LEFT_MARGIN }) .attr("y", function(d) { var datum = (cfg.dateDimension) ? new Date(d.date).getTime() : d.value; var y = Math.floor(step * (datum - minValue) + margin); return TimeKnots.checkAndFixCollision(svg, ".event-description", y, this); }) .attr("text-anchor", "center") .style("opacity", function(d) { if (d.opacity != undefined) { return d.opacity } return 1 }); svg.selectAll("p") .data(events).enter() .append('text').attr("class", "event-description-date") .html(function(d){ if (moment(d.date).isSame(TODAY, 'day') && d.name !== "TODAY") { return "TODAY"; } return moment(new Date(d.date)).format("MMM D"); }) .attr("x", function(d) { return Math.floor(cfg.width / 2) + DATE_LEFT_MARGIN }) .attr("y", function(d) { var datum = (cfg.dateDimension) ? new Date(d.date).getTime() : d.value; var y = Math.floor(step * (datum - minValue) + margin); return TimeKnots.checkAndFixCollision(svg, ".event-description-date", y, this); }) .attr("text-anchor", "right") .style("opacity", function(d) { if (d.opacity != undefined) { return d.opacity } return 1 }); }, checkAndFixCollision: function(svg, id, y, that) { svg.selectAll(id).each(function() { if (this != that) { if (this.getAttribute('y') != null && (this.getAttribute('y') >= (y-10) && this.getAttribute('y') <= (y+10))) { y += 15 - 0.05*(this.getAttribute('y') - y); return y; } } }); return y; } }