// 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() { "use strict"; //============================================================ // Public Variables with Default Settings //------------------------------------------------------------ var margin = {top: 0, right: 0, bottom: 0, left: 0} , orient = 'left' // TODO top & bottom , reverse = false , ranges = function(d) { return d.ranges } , markers = function(d) { return d.markers } , measures = function(d) { return d.measures } , rangeLabels = function(d) { return d.rangeLabels ? d.rangeLabels : [] } , markerLabels = function(d) { return d.markerLabels ? d.markerLabels : [] } , measureLabels = function(d) { return d.measureLabels ? d.measureLabels : [] } , forceX = [0] // List of numbers to Force into the X scale (ie. 0, or a max / min, etc.) , width = 380 , height = 30 , tickFormat = null , color = nv.utils.getColor(['#1f77b4']) , dispatch = d3.dispatch('elementMouseover', 'elementMouseout') ; //============================================================ function chart(selection) { selection.each(function(d, i) { var availableWidth = width - margin.left - margin.right, availableHeight = height - margin.top - margin.bottom, container = d3.select(this); 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), rangeLabelz = rangeLabels.call(this, d, i).slice(), markerLabelz = markerLabels.call(this, d, i).slice(), measureLabelz = measureLabels.call(this, d, i).slice(); //------------------------------------------------------------ // Setup Scales // Compute the new x-scale. var x1 = d3.scale.linear() .domain( d3.extent(d3.merge([forceX, rangez])) ) .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; var rangeMin = d3.min(rangez), //rangez[2] rangeMax = d3.max(rangez), //rangez[0] rangeAvg = rangez[1]; //------------------------------------------------------------ //------------------------------------------------------------ // Setup containers and skeleton of chart var wrap = container.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'); gEnter.append('rect').attr('class', 'nv-range nv-rangeMax'); gEnter.append('rect').attr('class', 'nv-range nv-rangeAvg'); gEnter.append('rect').attr('class', 'nv-range nv-rangeMin'); gEnter.append('rect').attr('class', 'nv-measure'); gEnter.append('path').attr('class', 'nv-markerTriangle'); wrap.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); //------------------------------------------------------------ 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)) }; var xp0 = function(d) { return d < 0 ? x0(d) : x0(0) }, xp1 = function(d) { return d < 0 ? x1(d) : x1(0) }; g.select('rect.nv-rangeMax') .attr('height', availableHeight) .attr('width', w1(rangeMax > 0 ? rangeMax : rangeMin)) .attr('x', xp1(rangeMax > 0 ? rangeMax : rangeMin)) .datum(rangeMax > 0 ? rangeMax : rangeMin) /* .attr('x', rangeMin < 0 ? rangeMax > 0 ? x1(rangeMin) : x1(rangeMax) : x1(0)) */ g.select('rect.nv-rangeAvg') .attr('height', availableHeight) .attr('width', w1(rangeAvg)) .attr('x', xp1(rangeAvg)) .datum(rangeAvg) /* .attr('width', rangeMax <= 0 ? x1(rangeMax) - x1(rangeAvg) : x1(rangeAvg) - x1(rangeMin)) .attr('x', rangeMax <= 0 ? x1(rangeAvg) : x1(rangeMin)) */ g.select('rect.nv-rangeMin') .attr('height', availableHeight) .attr('width', w1(rangeMax)) .attr('x', xp1(rangeMax)) .attr('width', w1(rangeMax > 0 ? rangeMin : rangeMax)) .attr('x', xp1(rangeMax > 0 ? rangeMin : rangeMax)) .datum(rangeMax > 0 ? rangeMin : rangeMax) /* .attr('width', rangeMax <= 0 ? x1(rangeAvg) - x1(rangeMin) : x1(rangeMax) - x1(rangeAvg)) .attr('x', rangeMax <= 0 ? x1(rangeMin) : x1(rangeAvg)) */ g.select('rect.nv-measure') .style('fill', color) .attr('height', availableHeight / 3) .attr('y', availableHeight / 3) .attr('width', measurez < 0 ? x1(0) - x1(measurez[0]) : x1(measurez[0]) - x1(0)) .attr('x', xp1(measurez)) .on('mouseover', function() { dispatch.elementMouseover({ value: measurez[0], label: measureLabelz[0] || 'Current', pos: [x1(measurez[0]), availableHeight/2] }) }) .on('mouseout', function() { dispatch.elementMouseout({ value: measurez[0], label: measureLabelz[0] || 'Current' }) }) var h3 = availableHeight / 6; if (markerz[0]) { g.selectAll('path.nv-markerTriangle') .attr('transform', function(d) { return 'translate(' + x1(markerz[0]) + ',' + (availableHeight / 2) + ')' }) .attr('d', 'M0,' + h3 + 'L' + h3 + ',' + (-h3) + ' ' + (-h3) + ',' + (-h3) + 'Z') .on('mouseover', function() { dispatch.elementMouseover({ value: markerz[0], label: markerLabelz[0] || 'Previous', pos: [x1(markerz[0]), availableHeight/2] }) }) .on('mouseout', function() { dispatch.elementMouseout({ value: markerz[0], label: markerLabelz[0] || 'Previous' }) }); } else { g.selectAll('path.nv-markerTriangle').remove(); } wrap.selectAll('.nv-range') .on('mouseover', function(d,i) { var label = rangeLabelz[i] || (!i ? "Maximum" : i == 1 ? "Mean" : "Minimum"); dispatch.elementMouseover({ value: d, label: label, pos: [x1(d), availableHeight/2] }) }) .on('mouseout', function(d,i) { var label = rangeLabelz[i] || (!i ? "Maximum" : i == 1 ? "Mean" : "Minimum"); dispatch.elementMouseout({ value: d, label: label }) }) /* // THIS IS THE PREVIOUS BULLET IMPLEMENTATION, WILL REMOVE SHORTLY // 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; }) .style('fill', function(d,i) { return color(d,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) - x1(0)) + ',' + (availableHeight / 2) + ')' }); marker.exit().remove(); */ }); // d3.timer.flush(); // Not needed? return chart; } //============================================================ // Expose Public Variables //------------------------------------------------------------ chart.dispatch = dispatch; chart.options = nv.utils.optionsFunc.bind(chart); // 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.forceX = function(_) { if (!arguments.length) return forceX; forceX = _; 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.top = typeof _.top != 'undefined' ? _.top : margin.top; margin.right = typeof _.right != 'undefined' ? _.right : margin.right; margin.bottom = typeof _.bottom != 'undefined' ? _.bottom : margin.bottom; margin.left = typeof _.left != 'undefined' ? _.left : margin.left; return chart; }; chart.tickFormat = function(_) { if (!arguments.length) return tickFormat; tickFormat = _; return chart; }; chart.color = function(_) { if (!arguments.length) return color; color = nv.utils.getColor(_); return chart; }; //============================================================ return chart; };