// 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.bulletChart = function() { //============================================================ // Public Variables with Default Settings //------------------------------------------------------------ var bullet = nv.models.bullet() ; var orient = 'left' // TODO top & bottom , reverse = false , margin = {top: 5, right: 40, bottom: 20, left: 120} , ranges = function(d) { return d.ranges } , markers = function(d) { return d.markers } , measures = function(d) { return d.measures } , width = null , height = 55 , tickFormat = null , tooltips = true , tooltip = function(key, x, y, e, graph) { return '

' + e.label + '

' + '

' + e.value + '

' } , noData = "No Data Available." , dispatch = d3.dispatch('tooltipShow', 'tooltipHide') ; //============================================================ //============================================================ // Private Variables //------------------------------------------------------------ var showTooltip = function(e, parentElement) { var offsetElement = parentElement.parentNode.parentNode, left = e.pos[0] + offsetElement.offsetLeft + margin.left, top = e.pos[1] + offsetElement.offsetTop + margin.top; var content = '

' + e.label + '

' + '

' + e.value + '

'; nv.tooltip.show([left, top], content, e.value < 0 ? 'e' : 'w', null, offsetElement.parentNode); }; //============================================================ function chart(selection) { selection.each(function(d, i) { var container = d3.select(this); var availableWidth = (width || parseInt(container.style('width')) || 960) - margin.left - margin.right, availableHeight = height - margin.top - margin.bottom, that = this; chart.update = function() { chart(selection) }; chart.container = this; //------------------------------------------------------------ // Display No Data message if there's nothing to show. /* // Disabled until I figure out a better way to check for no data with the bullet chart if (!data || !data.length || !data.filter(function(d) { return d.values.length }).length) { var noDataText = container.selectAll('.nv-noData').data([noData]); noDataText.enter().append('text') .attr('class', 'nvd3 nv-noData') .attr('dy', '-.7em') .style('text-anchor', 'middle'); noDataText .attr('x', margin.left + availableWidth / 2) .attr('y', margin.top + availableHeight / 2) .text(function(d) { return d }); return chart; } else { container.selectAll('.nv-noData').remove(); } */ //------------------------------------------------------------ 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 containers and skeleton of chart var wrap = container.selectAll('g.nv-wrap.nv-bulletChart').data([d]); var wrapEnter = wrap.enter().append('g').attr('class', 'nvd3 nv-wrap nv-bulletChart'); var gEnter = wrapEnter.append('g'); var g = wrap.select('g'); gEnter.append('g').attr('class', 'nv-bulletWrap'); gEnter.append('g').attr('class', 'nv-titles'); wrap.attr('transform', 'translate(' + margin.left + ',' + ( margin.top + i*height )+ ')'); //------------------------------------------------------------ // 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; /* // 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)) }; var title = gEnter.select('.nv-titles').append("g") .attr("text-anchor", "end") .attr("transform", "translate(-6," + (height - margin.top - margin.bottom) / 2 + ")"); title.append("text") .attr("class", "nv-title") .text(function(d) { return d.title; }); title.append("text") .attr("class", "nv-subtitle") .attr("dy", "1em") .text(function(d) { return d.subtitle; }); bullet .width(availableWidth) .height(availableHeight) var bulletWrap = g.select('.nv-bulletWrap'); d3.transition(bulletWrap).call(bullet); // Compute the tick format. var format = tickFormat || x1.tickFormat(8); // Update the tick groups. var tick = g.selectAll('g.nv-tick') .data(x1.ticks(8), function(d) { return this.textContent || format(d); }); // Initialize the ticks with the old scale, x0. var tickEnter = tick.enter().append('g') .attr('class', 'nv-tick') .attr('transform', function(d) { return 'translate(' + x0(d) + ',0)' }) .style('opacity', 1e-6); tickEnter.append('line') .attr('y1', availableHeight) .attr('y2', availableHeight * 7 / 6); tickEnter.append('text') .attr('text-anchor', 'middle') .attr('dy', '1em') .attr('y', availableHeight * 7 / 6) .text(format); // Transition the entering ticks to the new scale, x1. d3.transition(tickEnter) .attr('transform', function(d) { return 'translate(' + x1(d) + ',0)' }) .style('opacity', 1); // Transition the updating ticks to the new scale, x1. var tickUpdate = d3.transition(tick) .attr('transform', function(d) { return 'translate(' + x1(d) + ',0)' }) .style('opacity', 1); tickUpdate.select('line') .attr('y1', availableHeight) .attr('y2', availableHeight * 7 / 6); tickUpdate.select('text') .attr('y', availableHeight * 7 / 6); // Transition the exiting ticks to the new scale, x1. d3.transition(tick.exit()) .attr('transform', function(d) { return 'translate(' + x1(d) + ',0)' }) .style('opacity', 1e-6) .remove(); //============================================================ // Event Handling/Dispatching (in chart's scope) //------------------------------------------------------------ dispatch.on('tooltipShow', function(e) { if (tooltips) showTooltip(e, that.parentNode); }); //============================================================ }); d3.timer.flush(); return chart; } //============================================================ // Event Handling/Dispatching (out of chart's scope) //------------------------------------------------------------ bullet.dispatch.on('elementMouseover.tooltip', function(e) { dispatch.tooltipShow(e); }); bullet.dispatch.on('elementMouseout.tooltip', function(e) { dispatch.tooltipHide(e); }); dispatch.on('tooltipHide', function() { if (tooltips) nv.tooltip.cleanup(); }); //============================================================ //============================================================ // Expose Public Variables //------------------------------------------------------------ chart.dispatch = dispatch; chart.bullet = bullet; // left, right, top, bottom chart.orient = function(x) { if (!arguments.length) return orient; orient = x; reverse = orient == 'right' || orient == 'bottom'; return chart; }; // ranges (bad, satisfactory, good) chart.ranges = function(x) { if (!arguments.length) return ranges; ranges = x; return chart; }; // markers (previous, goal) chart.markers = function(x) { if (!arguments.length) return markers; markers = x; return chart; }; // measures (actual, forecast) chart.measures = function(x) { if (!arguments.length) return measures; measures = x; return chart; }; chart.width = function(x) { if (!arguments.length) return width; width = x; return chart; }; chart.height = function(x) { if (!arguments.length) return height; height = x; 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(x) { if (!arguments.length) return tickFormat; tickFormat = x; return chart; }; chart.tooltips = function(_) { if (!arguments.length) return tooltips; tooltips = _; return chart; }; chart.tooltipContent = function(_) { if (!arguments.length) return tooltip; tooltip = _; return chart; }; chart.noData = function(_) { if (!arguments.length) return noData; noData = _; return chart; }; //============================================================ return chart; };