<html> <head> <title>Swimlane using d3.js</title> <script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.js"></script> <script type="text/javascript" src="randomData.js"></script> <style> .chart { shape-rendering: crispEdges; } .mini text { font: 9px sans-serif; } .main text { font: 12px sans-serif; } .month text { text-anchor: start; } .todayLine { stroke: blue; stroke-width: 1.5; } .axis line, .axis path { stroke: black; } .miniItem { stroke-width: 6; } .item { stroke: gray; fill: #ddd; } .brush .extent { stroke: gray; fill: blue; fill-opacity: .165; } </style> </head> <body> <script type="text/javascript"> // helper function to create dates prior to 1000 var yr = function(year) { var date = new Date(2000,1,1); date.setFullYear(year); return date; } // lanes is an array of lane objects that have the following properties // id: the unique id for this swimlane // label: the text label for this swimlane // // these determine how many horizontal lanes there will be in the chart // and what their names will be var lanes = [ {id: 0, label: 'Chinese'}, {id: 1, label: 'Japanese'}, {id: 2, label: 'Korean'} ]; // items is an array of item objects that have the following properties // id: the unique id for this item // lane: the id of the lane that this item belongs in // desc: the description for this item // start: the starting value for this item // end: the end value for this item // class: the css class that should be applied to this item // // these define the actual items that are displayed on the chart var items = [ {id: 0, lane: 0, desc: 'Qin', start: yr(5), end: yr(205), class: 'item'}, {id: 1, lane: 0, desc: 'Jin', start: yr(265), end: yr(420), class: 'item'} ]; // define the chart extents var margin = {top: 20, right: 15, bottom: 15, left: 70} , width = 960 - margin.left - margin.right , height = 500 - margin.top - margin.bottom , miniHeight = lanes.length * 12 + 50 , mainHeight = height - miniHeight - 50; var x = d3.time.scale() .domain([d3.min(items, function(d) { return d.start - 100000; }), d3.max(items, function(d) { return d.end; })]) .range([0, width]); var x1 = d3.time.scale().range([0, width]); var ext = d3.extent(lanes, function(d) { return d.id; }); var y1 = d3.scale.linear().domain([ext[0], ext[1] + 1]).range([0, mainHeight]); var y2 = d3.scale.linear().domain([ext[0], ext[1] + 1]).range([0, miniHeight]); var chart = d3.select('body') .append('svg:svg') .attr('width', width + margin.right + margin.left) .attr('height', height + margin.top + margin.bottom) .attr('class', 'chart'); chart.append('defs').append('clipPath') .attr('id', 'clip') .append('rect') .attr('width', width) .attr('height', mainHeight); var main = chart.append('g') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') .attr('width', width) .attr('height', mainHeight) .attr('class', 'main'); var mini = chart.append('g') .attr('transform', 'translate(' + margin.left + ',' + (mainHeight + 60) + ')') .attr('width', width) .attr('height', miniHeight) .attr('class', 'mini'); // draw the lanes for the main chart main.append('g').selectAll('.laneLines') .data(lanes) .enter().append('line') .attr('x1', 0) .attr('y1', function(d) { return d3.round(y1(d.id)) + 0.5; }) .attr('x2', width) .attr('y2', function(d) { return d3.round(y1(d.id)) + 0.5; }) .attr('stroke', function(d) { return d.label === '' ? 'white' : 'lightgray' }); main.append('g').selectAll('.laneText') .data(lanes) .enter().append('text') .text(function(d) { return d.label; }) .attr('x', -10) .attr('y', function(d) { return y1(d.id + .5); }) .attr('dy', '0.5ex') .attr('text-anchor', 'end') .attr('class', 'laneText'); // draw the lanes for the mini chart mini.append('g').selectAll('.laneLines') .data(lanes) .enter().append('line') .attr('x1', 0) .attr('y1', function(d) { return d3.round(y2(d.id)) + 0.5; }) .attr('x2', width) .attr('y2', function(d) { return d3.round(y2(d.id)) + 0.5; }) .attr('stroke', function(d) { return d.label === '' ? 'white' : 'lightgray' }); mini.append('g').selectAll('.laneText') .data(lanes) .enter().append('text') .text(function(d) { return d.label; }) .attr('x', -10) .attr('y', function(d) { return y2(d.id + .5); }) .attr('dy', '0.5ex') .attr('text-anchor', 'end') .attr('class', 'laneText'); // draw the x axis var xDateAxis = d3.svg.axis() .scale(x) .orient('bottom'); var x1DateAxis = d3.svg.axis() .scale(x1) .orient('bottom'); main.append('g') .attr('transform', 'translate(0,' + mainHeight + ')') .attr('class', 'main axis date') .call(x1DateAxis); mini.append('g') .attr('transform', 'translate(0,' + miniHeight + ')') .attr('class', 'axis date') .call(xDateAxis); // draw the items var itemRects = main.append('g') .attr('clip-path', 'url(#clip)'); mini.append('g').selectAll('miniItems') .data(getPaths(items)) .enter().append('path') .attr('class', function(d) { return 'miniItem ' + d.class; }) .attr('d', function(d) { return d.path; }); // invisible hit area to move around the selection window mini.append('rect') .attr('pointer-events', 'painted') .attr('width', width) .attr('height', miniHeight) .attr('visibility', 'hidden') .on('mouseup', moveBrush); // draw the selection area var brush = d3.svg.brush() .x(x) .extent([yr(150), yr(300)]) .on("brush", display); mini.append('g') .attr('class', 'x brush') .call(brush) .selectAll('rect') .attr('y', 1) .attr('height', miniHeight - 1); mini.selectAll('rect.background').remove(); display(); function display () { var rects, labels , minExtent = brush.extent()[0] , maxExtent = brush.extent()[1] , visItems = items.filter(function (d) { return d.start < maxExtent && d.end > minExtent}); mini.select('.brush').call(brush.extent([minExtent, maxExtent])); x1.domain([minExtent, maxExtent]); // update the axis main.select('.main.axis.date').call(x1DateAxis); // upate the item rects rects = itemRects.selectAll('rect') .data(visItems, function (d) { return d.id; }) .attr('x', function(d) { return x1(d.start); }) .attr('width', function(d) { return x1(d.end) - x1(d.start); }); rects.enter().append('rect') .attr('x', function(d) { return x1(d.start); }) .attr('y', function(d) { return y1(d.lane) + .1 * y1(1) + 0.5; }) .attr('width', function(d) { return x1(d.end) - x1(d.start); }) .attr('height', function(d) { return .8 * y1(1); }) .attr('class', function(d) { return 'mainItem ' + d.class; }); rects.exit().remove(); // update the item labels labels = itemRects.selectAll('text') .data(visItems, function (d) { return d.id; }) .attr('x', function(d) { return x1(Math.max(d.start, minExtent)) + 2; }); labels.enter().append('text') .text(function (d) { return 'Item\n\n\n\n Id: ' + d.id; }) .attr('x', function(d) { return x1(Math.max(d.start, minExtent)) + 2; }) .attr('y', function(d) { return y1(d.lane) + .4 * y1(1) + 0.5; }) .attr('text-anchor', 'start') .attr('class', 'itemLabel'); labels.exit().remove(); } function moveBrush () { var origin = d3.mouse(this) , point = x.invert(origin[0]) , halfExtent = (brush.extent()[1].getTime() - brush.extent()[0].getTime()) / 2 , start = new Date(point.getTime() - halfExtent) , end = new Date(point.getTime() + halfExtent); brush.extent([start,end]); display(); } // generates a single path for each item class in the mini display // ugly - but draws mini 2x faster than append lines or line generator // is there a better way to do a bunch of lines as a single path with d3? function getPaths(items) { var paths = {}, d, offset = .5 * y2(1) + 0.5, result = []; for (var i = 0; i < items.length; i++) { d = items[i]; if (!paths[d.class]) paths[d.class] = ''; paths[d.class] += ['M',x(d.start),(y2(d.lane) + offset),'H',x(d.end)].join(' '); } for (var className in paths) { result.push({class: className, path: paths[className]}); } return result; } </script> </body> </html>