You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nvd3/src/models/bullet.js

386 lines
13 KiB
JavaScript

// 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;
};