nvd3
Version:
A reusable charting library written in d3.js
338 lines (294 loc) • 13 kB
JavaScript
/* Utility class to handle creation of an interactive layer.
This places a rectangle on top of the chart. When you mouse move over it, it sends a dispatch
containing the X-coordinate. It can also render a vertical line where the mouse is located.
dispatch.elementMousemove is the important event to latch onto. It is fired whenever the mouse moves over
the rectangle. The dispatch is given one object which contains the mouseX/Y location.
It also has 'pointXValue', which is the conversion of mouseX to the x-axis scale.
*/
nv.interactiveGuideline = function() {
"use strict";
var margin = { left: 0, top: 0 } //Pass the chart's top and left magins. Used to calculate the mouseX/Y.
, width = null
, height = null
, xScale = d3.scale.linear()
, dispatch = d3.dispatch('elementMousemove', 'elementMouseout', 'elementClick', 'elementDblclick', 'elementMouseDown', 'elementMouseUp')
, showGuideLine = true
, svgContainer = null // Must pass the chart's svg, we'll use its mousemove event.
, tooltip = nv.models.tooltip()
, isMSIE = window.ActiveXObject// Checkt if IE by looking for activeX. (excludes IE11)
;
tooltip
.duration(0)
.hideDelay(0)
.hidden(false);
function layer(selection) {
selection.each(function(data) {
var container = d3.select(this);
var availableWidth = (width || 960), availableHeight = (height || 400);
var wrap = container.selectAll("g.nv-wrap.nv-interactiveLineLayer")
.data([data]);
var wrapEnter = wrap.enter()
.append("g").attr("class", " nv-wrap nv-interactiveLineLayer");
wrapEnter.append("g").attr("class","nv-interactiveGuideLine");
if (!svgContainer) {
return;
}
function mouseHandler() {
var mouseX = d3.event.clientX - this.getBoundingClientRect().left;
var mouseY = d3.event.clientY - this.getBoundingClientRect().top;
var subtractMargin = true;
var mouseOutAnyReason = false;
if (isMSIE) {
/*
D3.js (or maybe SVG.getScreenCTM) has a nasty bug in Internet Explorer 10.
d3.mouse() returns incorrect X,Y mouse coordinates when mouse moving
over a rect in IE 10.
However, d3.event.offsetX/Y also returns the mouse coordinates
relative to the triggering <rect>. So we use offsetX/Y on IE.
*/
mouseX = d3.event.offsetX;
mouseY = d3.event.offsetY;
/*
On IE, if you attach a mouse event listener to the <svg> container,
it will actually trigger it for all the child elements (like <path>, <circle>, etc).
When this happens on IE, the offsetX/Y is set to where ever the child element
is located.
As a result, we do NOT need to subtract margins to figure out the mouse X/Y
position under this scenario. Removing the line below *will* cause
the interactive layer to not work right on IE.
*/
if(d3.event.target.tagName !== "svg") {
subtractMargin = false;
}
if (d3.event.target.className.baseVal.match("nv-legend")) {
mouseOutAnyReason = true;
}
}
if(subtractMargin) {
mouseX -= margin.left;
mouseY -= margin.top;
}
/* If mouseX/Y is outside of the chart's bounds,
trigger a mouseOut event.
*/
if (d3.event.type === 'mouseout'
|| mouseX < 0 || mouseY < 0
|| mouseX > availableWidth || mouseY > availableHeight
|| (d3.event.relatedTarget && d3.event.relatedTarget.ownerSVGElement === undefined)
|| mouseOutAnyReason
) {
if (isMSIE) {
if (d3.event.relatedTarget
&& d3.event.relatedTarget.ownerSVGElement === undefined
&& (d3.event.relatedTarget.className === undefined
|| d3.event.relatedTarget.className.match(tooltip.nvPointerEventsClass))) {
return;
}
}
dispatch.elementMouseout({
mouseX: mouseX,
mouseY: mouseY
});
layer.renderGuideLine(null); //hide the guideline
tooltip.hidden(true);
return;
} else {
tooltip.hidden(false);
}
var scaleIsOrdinal = typeof xScale.rangeBands === 'function';
var pointXValue = undefined;
// Ordinal scale has no invert method
if (scaleIsOrdinal) {
var elementIndex = d3.bisect(xScale.range(), mouseX) - 1;
// Check if mouseX is in the range band
if (xScale.range()[elementIndex] + xScale.rangeBand() >= mouseX) {
pointXValue = xScale.domain()[d3.bisect(xScale.range(), mouseX) - 1];
}
else {
dispatch.elementMouseout({
mouseX: mouseX,
mouseY: mouseY
});
layer.renderGuideLine(null); //hide the guideline
tooltip.hidden(true);
return;
}
}
else {
pointXValue = xScale.invert(mouseX);
}
dispatch.elementMousemove({
mouseX: mouseX,
mouseY: mouseY,
pointXValue: pointXValue
});
//If user double clicks the layer, fire a elementDblclick
if (d3.event.type === "dblclick") {
dispatch.elementDblclick({
mouseX: mouseX,
mouseY: mouseY,
pointXValue: pointXValue
});
}
// if user single clicks the layer, fire elementClick
if (d3.event.type === 'click') {
dispatch.elementClick({
mouseX: mouseX,
mouseY: mouseY,
pointXValue: pointXValue
});
}
// if user presses mouse down the layer, fire elementMouseDown
if (d3.event.type === 'mousedown') {
dispatch.elementMouseDown({
mouseX: mouseX,
mouseY: mouseY,
pointXValue: pointXValue
});
}
// if user presses mouse down the layer, fire elementMouseUp
if (d3.event.type === 'mouseup') {
dispatch.elementMouseUp({
mouseX: mouseX,
mouseY: mouseY,
pointXValue: pointXValue
});
}
}
svgContainer
.on("touchmove",mouseHandler)
.on("mousemove",mouseHandler, true)
.on("mouseout" ,mouseHandler,true)
.on("mousedown" ,mouseHandler,true)
.on("mouseup" ,mouseHandler,true)
.on("dblclick" ,mouseHandler)
.on("click", mouseHandler)
;
layer.guideLine = null;
//Draws a vertical guideline at the given X postion.
layer.renderGuideLine = function(x) {
if (!showGuideLine) return;
if (layer.guideLine && layer.guideLine.attr("x1") === x) return;
nv.dom.write(function() {
var line = wrap.select(".nv-interactiveGuideLine")
.selectAll("line")
.data((x != null) ? [nv.utils.NaNtoZero(x)] : [], String);
line.enter()
.append("line")
.attr("class", "nv-guideline")
.attr("x1", function(d) { return d;})
.attr("x2", function(d) { return d;})
.attr("y1", availableHeight)
.attr("y2",0);
line.exit().remove();
});
}
});
}
layer.dispatch = dispatch;
layer.tooltip = tooltip;
layer.margin = function(_) {
if (!arguments.length) return margin;
margin.top = typeof _.top != 'undefined' ? _.top : margin.top;
margin.left = typeof _.left != 'undefined' ? _.left : margin.left;
return layer;
};
layer.width = function(_) {
if (!arguments.length) return width;
width = _;
return layer;
};
layer.height = function(_) {
if (!arguments.length) return height;
height = _;
return layer;
};
layer.xScale = function(_) {
if (!arguments.length) return xScale;
xScale = _;
return layer;
};
layer.showGuideLine = function(_) {
if (!arguments.length) return showGuideLine;
showGuideLine = _;
return layer;
};
layer.svgContainer = function(_) {
if (!arguments.length) return svgContainer;
svgContainer = _;
return layer;
};
return layer;
};
/* Utility class that uses d3.bisect to find the index in a given array, where a search value can be inserted.
This is different from normal bisectLeft; this function finds the nearest index to insert the search value.
For instance, lets say your array is [1,2,3,5,10,30], and you search for 28.
Normal d3.bisectLeft will return 4, because 28 is inserted after the number 10. But interactiveBisect will return 5
because 28 is closer to 30 than 10.
Unit tests can be found in: interactiveBisectTest.html
Has the following known issues:
* Will not work if the data points move backwards (ie, 10,9,8,7, etc) or if the data points are in random order.
* Won't work if there are duplicate x coordinate values.
*/
nv.interactiveBisect = function (values, searchVal, xAccessor) {
"use strict";
if (! (values instanceof Array)) {
return null;
}
var _xAccessor;
if (typeof xAccessor !== 'function') {
_xAccessor = function(d) {
return d.x;
}
} else {
_xAccessor = xAccessor;
}
var _cmp = function(d, v) {
// Accessors are no longer passed the index of the element along with
// the element itself when invoked by d3.bisector.
//
// Starting at D3 v3.4.4, d3.bisector() started inspecting the
// function passed to determine if it should consider it an accessor
// or a comparator. This meant that accessors that take two arguments
// (expecting an index as the second parameter) are treated as
// comparators where the second argument is the search value against
// which the first argument is compared.
return _xAccessor(d) - v;
};
var bisect = d3.bisector(_cmp).left;
var index = d3.max([0, bisect(values,searchVal) - 1]);
var currentValue = _xAccessor(values[index]);
if (typeof currentValue === 'undefined') {
currentValue = index;
}
if (currentValue === searchVal) {
return index; //found exact match
}
var nextIndex = d3.min([index+1, values.length - 1]);
var nextValue = _xAccessor(values[nextIndex]);
if (typeof nextValue === 'undefined') {
nextValue = nextIndex;
}
if (Math.abs(nextValue - searchVal) >= Math.abs(currentValue - searchVal)) {
return index;
} else {
return nextIndex
}
};
/*
Returns the index in the array "values" that is closest to searchVal.
Only returns an index if searchVal is within some "threshold".
Otherwise, returns null.
*/
nv.nearestValueIndex = function (values, searchVal, threshold) {
"use strict";
var yDistMax = Infinity, indexToHighlight = null;
values.forEach(function(d,i) {
var delta = Math.abs(searchVal - d);
if ( d != null && delta <= yDistMax && delta < threshold) {
yDistMax = delta;
indexToHighlight = i;
}
});
return indexToHighlight;
};