| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <style> | |
| path { | |
| fill: none; | |
| stroke-width: 2px; | |
| stroke-linejoin: round; | |
| stroke: #444; | |
| } | |
| .red path { | |
| fill: #ba3e2d; | |
| } | |
| .orange path { | |
| fill: #ff7d3e; | |
| } | |
| .teal path { | |
| fill: #83dfc3; | |
| } | |
| .blue path { | |
| fill: #4c8da1; | |
| } | |
| .yellow path { | |
| fill: #ffea60; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div></div> | |
| <script src=" | |
| https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script> | |
| <script> | |
| var margin = { top: 10, right: 10, left: 10, bottom: 10 }, | |
| width = 960 - margin.left - margin.right, | |
| height = 500 - margin.top - margin.bottom, | |
| colorOrder = ["teal","red","yellow","orange","blue"]; | |
| var line = d3.svg.line(); | |
| var svg = d3.select("body").append("svg") | |
| .attr("width", width + margin.left + margin.right) | |
| .attr("height", height + margin.top + margin.bottom) | |
| .append("g") | |
| .attr("transform","translate( " + margin.left + " " + margin.top + " )"); | |
| // Make some random data | |
| var ribbons = d3.range(1980,2005, 0.5).map(function(year,i){ | |
| return({ | |
| id: i, // doesn't matter what this is as long as it's unique | |
| color: colorOrder[Math.floor(Math.random()*colorOrder.length)], | |
| startYear: Math.floor(year) | |
| }); | |
| }); | |
| // For each year, get an array of ribbons shown, in order (IDs only) | |
| var positionsByYear = d3.range(1980,2005).map(function(year){ | |
| return ribbons.filter(function(d){ | |
| return d.startYear <= year; // ignore ribbons that haven't started | |
| }).sort(sorter).map(function(d){ | |
| return d.id; | |
| }); | |
| }); | |
| // Pick the one you want to keep flat | |
| // Could set this manually to the ID you care about | |
| var baselineId = positionsByYear[0][0], | |
| baselineRow = positionsByYear[positionsByYear.length - 1].indexOf(baselineId); | |
| // Move positions down so the baseline ID is always at the same vertical index | |
| positionsByYear.forEach(function(year){ | |
| while (year.indexOf(baselineId) < baselineRow) { | |
| year.unshift(null); | |
| } | |
| }); | |
| // Pixel scales | |
| var x = d3.scale.ordinal() | |
| .domain(d3.range(-1,positionsByYear.length)) | |
| .rangeBands([0,width]); | |
| var y = d3.scale.ordinal() | |
| .domain(d3.range(ribbons.length + 1)) | |
| .rangeBands([0,height]); | |
| // Construct X/Y coordinate pairs per ribbon | |
| ribbons.forEach(function(ribbon){ | |
| var top = [], // Line along the top | |
| bottom = []; // Line along the bottom | |
| positionsByYear.forEach(function(year,i){ | |
| // Vertical position | |
| var position = year.indexOf(ribbon.id); | |
| if (position >= 0) { | |
| // Add one extra miter-y point at the beginning | |
| if (!top.length) { | |
| if (ribbon.id === baselineId) { | |
| top.push([x(i-1),(y(position) + y(position + 1)) / 2]); | |
| } else if (position > baselineRow) { | |
| // TODO smarter | |
| var numBefore = ribbons.filter(function(r){ | |
| var index = year.indexOf(r.id); | |
| return index > baselineRow && index < position && r.startYear === ribbon.startYear; | |
| }).length; | |
| top.push([x(i-1),y(position - numBefore)]); | |
| } else { | |
| // TODO smarter | |
| var numAfter = ribbons.filter(function(r){ | |
| var index = year.indexOf(r.id); | |
| return index < baselineRow && index > position && r.startYear === ribbon.startYear; | |
| }).length; | |
| top.push([x(i-1),y(position + 1 + numAfter)]); | |
| } | |
| } | |
| // Add the top and bottom point | |
| top.push([x(i),y(position)]); | |
| bottom.unshift([x(i),y(position + 1)]); | |
| } | |
| }); | |
| // Combine the whole path | |
| ribbon.positions = top.concat(bottom); | |
| }); | |
| var paths = svg.selectAll("g") | |
| .data(ribbons) | |
| .enter() | |
| .append("g") | |
| .attr("class",function(d){ | |
| return d.color; | |
| }); | |
| paths.append("path") | |
| .attr("d",function(d){ | |
| return line(d.positions) + "Z"; | |
| }); | |
| // What order should the ribbons be stacked in for any given X? | |
| function sorter(a,b){ | |
| var ia = colorOrder.indexOf(a.color), | |
| ib = colorOrder.indexOf(b.color), | |
| ga = a.startYear, | |
| gb = b.startYear; | |
| // sort by color group first | |
| if (ia !== ib) { | |
| return ia - ib; | |
| } | |
| // then within the group | |
| if (ga !== gb) { | |
| return ga - gb; | |
| } | |
| // tiebreaker | |
| return a.id - b.id; | |
| } | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
