// Chart design based on the recommendations of Stephen Few. Implementation // based on the work of Clint Ivy, Jamie Love, and Jason Davies. // http://projects.instantcognition.com/protovis/bulletchart/ nv.models.bullet = function() { var orient = 'left', // TODO top & bottom reverse = false, margin = {top: 0, right: 0, bottom: 0, left: 0}, ranges = function(d) { return d.ranges }, markers = function(d) { return d.markers }, measures = function(d) { return d.measures }, width = 380, height = 30, tickFormat = null; var dispatch = d3.dispatch('elementMouseover', 'elementMouseout'); // For each small multiple… function chart(g) { g.each(function(d, i) { var availableWidth = width - margin.left - margin.right, availableHeight = height - margin.top - margin.bottom; var rangez = ranges.call(this, d, i).slice().sort(d3.descending), markerz = markers.call(this, d, i).slice().sort(d3.descending), measurez = measures.call(this, d, i).slice().sort(d3.descending); var wrap = d3.select(this).selectAll('g.nv-wrap.nv-bullet').data([d]); var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-bullet'); var gEnter = wrapEnter.append('g'); var g = wrap.select('g') wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); // Compute the new x-scale. var x1 = d3.scale.linear() .domain([0, Math.max(rangez[0], markerz[0], measurez[0])]) // TODO: need to allow forceX and forceY, and xDomain, yDomain .range(reverse ? [availableWidth, 0] : [0, availableWidth]); // Retrieve the old x-scale, if this is an update. var x0 = this.__chart__ || d3.scale.linear() .domain([0, Infinity]) .range(x1.range()); // Stash the new scale. this.__chart__ = x1; /* // Derive width-scales from the x-scales. var w0 = bulletWidth(x0), w1 = bulletWidth(x1); function bulletWidth(x) { var x0 = x(0); return function(d) { return Math.abs(x(d) - x(0)); }; } function bulletTranslate(x) { return function(d) { return 'translate(' + x(d) + ',0)'; }; } */ var w0 = function(d) { return Math.abs(x0(d) - x0(0)) }, // TODO: could optimize by precalculating x0(0) and x1(0) w1 = function(d) { return Math.abs(x1(d) - x1(0)) }; // Update the range rects. var range = g.selectAll('rect.nv-range') .data(rangez); range.enter().append('rect') .attr('class', function(d, i) { return 'nv-range nv-s' + i; }) .attr('width', w0) .attr('height', availableHeight) .attr('x', reverse ? x0 : 0) .on('mouseover', function(d,i) { dispatch.elementMouseover({ value: d, label: (i <= 0) ? 'Maximum' : (i > 1) ? 'Minimum' : 'Mean', //TODO: make these labels a variable pos: [x1(d), availableHeight/2] }) }) .on('mouseout', function(d,i) { dispatch.elementMouseout({ value: d, label: (i <= 0) ? 'Minimum' : (i >=1) ? 'Maximum' : 'Mean', //TODO: make these labels a variable }) }) d3.transition(range) .attr('x', reverse ? x1 : 0) .attr('width', w1) .attr('height', availableHeight); // Update the measure rects. var measure = g.selectAll('rect.nv-measure') .data(measurez); measure.enter().append('rect') .attr('class', function(d, i) { return 'nv-measure nv-s' + i; }) .attr('width', w0) .attr('height', availableHeight / 3) .attr('x', reverse ? x0 : 0) .attr('y', availableHeight / 3) .on('mouseover', function(d) { dispatch.elementMouseover({ value: d, label: 'Current', //TODO: make these labels a variable pos: [x1(d), availableHeight/2] }) }) .on('mouseout', function(d) { dispatch.elementMouseout({ value: d, label: 'Current' //TODO: make these labels a variable }) }) d3.transition(measure) .attr('width', w1) .attr('height', availableHeight / 3) .attr('x', reverse ? x1 : 0) .attr('y', availableHeight / 3); // Update the marker lines. var marker = g.selectAll('path.nv-markerTriangle') .data(markerz); var h3 = availableHeight / 6; marker.enter().append('path') .attr('class', 'nv-markerTriangle') .attr('transform', function(d) { return 'translate(' + x0(d) + ',' + (availableHeight / 2) + ')' }) .attr('d', 'M0,' + h3 + 'L' + h3 + ',' + (-h3) + ' ' + (-h3) + ',' + (-h3) + 'Z') .on('mouseover', function(d,i) { dispatch.elementMouseover({ value: d, label: 'Previous', pos: [x1(d), availableHeight/2] }) }) .on('mouseout', function(d,i) { dispatch.elementMouseout({ value: d, label: 'Previous' }) }); d3.transition(marker) .attr('transform', function(d) { return 'translate(' + x1(d) + ',' + (availableHeight / 2) + ')' }); marker.exit().remove(); }); d3.timer.flush(); } chart.dispatch = dispatch; // left, right, top, bottom chart.orient = function(_) { if (!arguments.length) return orient; orient = _; reverse = orient == 'right' || orient == 'bottom'; return chart; }; // ranges (bad, satisfactory, good) chart.ranges = function(_) { if (!arguments.length) return ranges; ranges = _; return chart; }; // markers (previous, goal) chart.markers = function(_) { if (!arguments.length) return markers; markers = _; return chart; }; // measures (actual, forecast) chart.measures = function(_) { if (!arguments.length) return measures; measures = _; return chart; }; chart.width = function(_) { if (!arguments.length) return width; width = _; return chart; }; chart.height = function(_) { if (!arguments.length) return height; height = _; return chart; }; chart.margin = function(_) { if (!arguments.length) return margin; margin = _; return chart; }; chart.tickFormat = function(_) { if (!arguments.length) return tickFormat; tickFormat = _; return chart; }; return chart; };