| |
| // 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() { |
| |
| //============================================================ |
| // 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 } |
| , forceX = [0] // List of numbers to Force into the X scale (ie. 0, or a max / min, etc.) |
| , width = 380 |
| , height = 30 |
| , tickFormat = null |
| , 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), |
| mainGroup = nv.log(this.parentNode.parentNode).getAttribute('transform'), |
| heightFromTop = nv.log(parseInt(mainGroup.replace(/.*,(\d+)\)/,"$1"))); //TODO: There should be a smarter way to get this value |
| |
| 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); |
| |
| |
| //------------------------------------------------------------ |
| // Setup Scales |
| |
| // Compute the new x-scale. |
| var MaxX = Math.max(rangez[0] ? rangez[0]:0 , markerz[0] ? markerz[0] : 0 , measurez[0] ? measurez[0] : 0) |
| var x1 = d3.scale.linear() |
| .domain([0, MaxX]).nice() // 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; |
| |
| //------------------------------------------------------------ |
| |
| |
| //------------------------------------------------------------ |
| // 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'); |
| |
| 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)) }; |
| |
| |
| // 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), heightFromTop] |
| }) |
| }) |
| .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), heightFromTop] |
| }) |
| }) |
| .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), heightFromTop] |
| }) |
| }) |
| .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(); |
| |
| return chart; |
| } |
| |
| |
| //============================================================ |
| // Expose Public Variables |
| //------------------------------------------------------------ |
| |
| 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.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; |
| }; |
| |
| //============================================================ |
| |
| |
| return chart; |
| }; |
| |
| |