| <html> | |
| <head> | |
| <style> | |
| rect { | |
| fill: #D2B48C; | |
| } | |
| .axis path, | |
| .axis line { | |
| fill: none; | |
| stroke: black; | |
| shape-rendering: crispEdges; | |
| } | |
| .axis text { | |
| font-size: 0.8em; | |
| } | |
| .description { | |
| margin-top: 1em; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="description"> | |
| Visualization of orders according to these rules: | |
| <ul> | |
| <li>There should be no visually overlapping processing times. | |
| <li>An order whose processing time overlaps with other order(s) should have the same width as those order(s). | |
| <li>An order should occupy the maximum possible width on the canvas while still observing the second rule. | |
| </ul> | |
| </div> | |
| <div id="orders2d"></div> | |
| <div class="description"> | |
| The visualization above wastes space. The width of the bars is visually important, but conveys little information and | |
| obscures other information. For example, the changing widths make it difficult to visually compare order durations; | |
| which took longer, #2 or #7? Order #5 took the longest, but order #2 occupies the most area. It's complicated to calculate the widths, and they aren't even that useful. Maybe this is interesting as a puzzle, but as a visualization, it could be better. | |
| </div> | |
| <div class="description"> | |
| Rotating the time axis and keeping all widths constant makes it easy to see both how orders overlapped in time, and to | |
| compare their durations. This version shows the same or more information in less space. It's also simpler to generate the | |
| data needed to draw the visualization. | |
| </div> | |
| <div id="orders1d"></div> | |
| <script type="text/javascript" src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
| <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.2/underscore-min.js"></script> | |
| <script type="text/javascript"> | |
| var orders = [ | |
| {"orderId" : 1, "packingStart" : 224, "duration" : 69 }, | |
| {"orderId" : 2, "packingStart" : 335, "duration" : 91 }, | |
| {"orderId" : 3, "packingStart" : 23, "duration" : 47 }, | |
| {"orderId" : 4, "packingStart" : 130, "duration" : 52 }, | |
| {"orderId" : 5, "packingStart" : 5, "duration" : 183 }, | |
| {"orderId" : 6, "packingStart" : 253, "duration" : 71 }, | |
| {"orderId" : 7, "packingStart" : 41, "duration" : 68 } | |
| ]; | |
| orders = _.sortBy(orders, "packingStart"); | |
| var maxWidth = 400; | |
| var maxHeight = 300; | |
| var streams = new Array(); | |
| _.each(orders, function(order) { | |
| var ridx = -1; | |
| order.packingEnd = order.packingStart+order.duration; | |
| // find first row where order.packingStart >= end time of the row | |
| var r = _.find(streams, function(row) { | |
| ridx++; | |
| // if end time of last order in this row is less than start, add it | |
| return _.last(row).packingEnd <= order.packingStart; | |
| }); | |
| if (_.isUndefined(r)) { | |
| // not found, make a new row | |
| order.stream = streams.length; | |
| streams.push(new Array(order)); | |
| } else { | |
| // add to existing row | |
| order.stream = ridx; | |
| r.push(order); | |
| } | |
| // calculate widths for 2d version | |
| var width = maxWidth / (order.stream+1) | |
| order.width = width; | |
| var minStart = order.packingStart; | |
| var maxEnd = order.packingEnd; | |
| // look for overlapping orders in the streams to the left | |
| for (var i = 0; i < order.stream; i++) { | |
| _.find(streams[i], function(o) { | |
| if (o.packingStart > maxEnd) | |
| return true; // stop | |
| // overlap | |
| if ((o.packingStart <= minStart && o.packingEnd >= minStart) || | |
| (o.packingStart >= minStart && o.packingStart <= maxEnd)) { | |
| // use the smallest width | |
| var w = Math.min(o.width, width); | |
| o.width = order.width = w; | |
| minStart = Math.min(minStart, o.packingStart); | |
| maxEnd = Math.max(maxEnd, o.packingEnd); | |
| } | |
| return false; // keep looking | |
| }); | |
| } | |
| }); | |
| // draw | |
| var margin = { top: 5, right: 5, bottom: 5, left: 35 }; | |
| var width = maxWidth - margin.left - margin.right; | |
| var height = maxHeight - margin.top - margin.bottom; | |
| var x = d3.scale.linear().range([0, width]); | |
| var y = d3.scale.linear().range([0, height]); | |
| // 2d | |
| x.domain([0, width]); | |
| y.domain([0, 600]); | |
| var svg2d = d3.select("#orders2d").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 + ")"); | |
| svg2d.selectAll(".box") | |
| .data(orders) | |
| .enter() | |
| .append("rect") | |
| .attr("x", function(d) { return x(d.width*d.stream)+1 }) | |
| .attr("y", function(d) { return y(d.packingStart) }) | |
| .attr("width", function(d) { return x(d.width)-1; }) | |
| .attr("height", function(d) { return y(d.duration) } ); | |
| svg2d.selectAll(".box") | |
| .data(orders) | |
| .enter() | |
| .append("text") | |
| .attr("x", function(d) { return x(d.width*d.stream)+10 }) | |
| .attr("y", function(d) { return y(d.packingStart)+y(d.duration)*3/4 }) | |
| .text(function(d) { return "#"+d.orderId }); | |
| var yaxis = d3.svg.axis() | |
| .scale(y) | |
| .orient("left"); | |
| svg2d.append("g") | |
| .attr("class", "axis") | |
| .call(yaxis); | |
| // 1d | |
| maxHeight = 100; | |
| margin = { top: 5, right: 10, bottom: 35, left: 5 }; | |
| height = maxHeight - margin.top - margin.bottom; | |
| x.domain([0, 600]); | |
| y = d3.scale.linear().range([0, height]); | |
| y.domain([0, streams.length]); | |
| var barHeight = streams.length > 1 ? y(1)-y(0) : height; | |
| var svg1d = d3.select("#orders1d").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 + ")"); | |
| svg1d.selectAll(".box") | |
| .data(orders) | |
| .enter() | |
| .append("rect") | |
| .attr("x", function(d) { return x(d.packingStart) }) | |
| .attr("y", function(d) { return y(d.stream) }) | |
| .attr("width", function(d) { return x(d.duration) }) | |
| .attr("height", barHeight-2); | |
| svg1d.selectAll(".box") | |
| .data(orders) | |
| .enter() | |
| .append("text") | |
| .attr("x", function(d) { return x(d.packingStart)+5 }) | |
| .attr("y", function(d) { return y(d.stream)+barHeight*3/4 }) | |
| .text(function(d) { return "#"+d.orderId }); | |
| var xaxis = d3.svg.axis() | |
| .scale(x) | |
| .orient("bottom"); | |
| svg1d.append("g") | |
| .attr("class", "axis") | |
| .attr("transform", "translate(0,"+ (height)+")") | |
| .call(xaxis); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment