Reputation: 14594
I'm creating a timeline of events using d3.js. Some of the events on a row overlap each other. i.e., the next begins before the previous one ends. e.g.:
For each bar, if it's going to overlap a previous one, I'd like to move it down:
How would I do this? Here's a JSfiddle that generates the first example.
Here's some relevant parts:
var spanX = function(d, i) { return xScale(d['begin']); },
spanY = function(d) { return 0; },
spanW = function(d, i) {
return xScale(d['end']) - xScale(d['begin']);
},
spanH = 10;
var span = chart.selectAll('.chart-span')
.data(data)
.enter().append('rect')
.classed('chart-span', true)
.attr('x', spanX)
.attr('y', spanY)
.attr('width', spanW)
.attr('height', spanH);
(There are other issues, such as displaying more timelines under each other on the same chart, and displaying a label describing each timeline on the left... but one problem at a time!)
UPDATE: For those following along, here's my finished chart: http://spelunker.moma.org/departments/ (2017-02-08)
Upvotes: 1
Views: 1815
Reputation: 102194
The best solution is creating an algorithm to check the end of the previous bar and setting the y position of the next bar according to that previous bar ending (and such algorithm will be fundamental to your two next questions, "...displaying more timelines under each other on the same chart, and displaying a label describing each timeline on the left").
But, due to simplicity (and laziness), if your data is not dynamic or your chart not terribly complex, you can set (hardcode) the positions in the data:
var data = [
{'begin': 2001, 'end': 2005, 'pos': 1},
{'begin': 2006, 'end': 2010, 'pos': 1},
{'begin': 2008, 'end': 2012, 'pos': 2},
{'begin': 2011, 'end': 2015, 'pos': 3}
];
And then defining your y scale and your spanY
:
yScale.domain(data.map(d=> d.pos))
.range([0, height]);
spanY = function(d) { return yScale(d.pos); }
Here is a demo:
var data = [
{'begin': 2001, 'end': 2005, 'pos': 1},
{'begin': 2006, 'end': 2010, 'pos': 1},
{'begin': 2008, 'end': 2012, 'pos': 2},
{'begin': 2011, 'end': 2015, 'pos': 3}
];
var margin = {top: 20, right: 30, bottom: 50, left: 40},
width = 500 - margin.left - margin.right,
height = 120 - margin.top - margin.bottom,
xScale = d3.scaleBand(),
yScale = d3.scaleBand(),
xAxis = d3.axisBottom(xScale),
minX = d3.min(data, function(d) { return d['begin']; }),
maxX = d3.max(data, function(d) { return d['end']; });
xScale.domain(d3.range(minX, maxX+1));
xScale.rangeRound([0, width]);
yScale.domain(data.map(d=> d.pos))
.range([0, height]);
var spanX = function(d, i) { return xScale(d['begin']); },
spanY = function(d) { return yScale(d.pos); },
spanW = function(d, i) {
return xScale(d['end']) - xScale(d['begin']);
},
spanH = 10;
var chart = d3.select('.chart')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.left + margin.right)
.append('g')
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Add x axis
chart.append("g")
.attr("class", "axis axis-x");
chart.select('.axis-x')
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add spans
var span = chart.selectAll('.chart-span')
.data(data)
.enter().append('rect')
.classed('chart-span', true)
.attr('x', spanX)
.attr('y', spanY)
.attr('width', spanW)
.attr('height', spanH);
.chart-span {
fill: #333;
opacity: 0.5;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg class="chart"></svg>
PS: Again, this hardcoded solution will not be a good one for your next two questions ("...displaying more timelines under each other on the same chart, and displaying a label describing each timeline on the left"). When you post these questions, describe in better details how the data can be, so people can think about a good algorithm to calculate the bars' positions.
Upvotes: 1