2013-07-03 14:33:48 +00:00
|
|
|
/* 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.
|
|
|
|
|
2013-07-08 13:36:29 +00:00
|
|
|
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.
|
2013-07-03 14:33:48 +00:00
|
|
|
*/
|
2013-07-04 03:17:08 +00:00
|
|
|
nv.interactiveGuideline = function() {
|
2013-08-13 01:58:17 +00:00
|
|
|
"use strict";
|
2013-07-04 03:17:08 +00:00
|
|
|
var tooltip = nv.models.tooltip();
|
2013-07-03 14:33:48 +00:00
|
|
|
//Public settings
|
|
|
|
var width = null
|
|
|
|
, height = null
|
2013-07-25 18:23:51 +00:00
|
|
|
//Please pass in the bounding chart's top and left margins
|
|
|
|
//This is important for calculating the correct mouseX/Y positions.
|
|
|
|
, margin = {left: 0, top: 0}
|
2013-07-03 14:33:48 +00:00
|
|
|
, xScale = d3.scale.linear()
|
2013-07-04 03:17:08 +00:00
|
|
|
, yScale = d3.scale.linear()
|
2013-09-03 14:28:26 +00:00
|
|
|
, dispatch = d3.dispatch('elementMousemove', 'elementMouseout','elementDblclick')
|
2013-07-04 03:17:08 +00:00
|
|
|
, showGuideLine = true
|
2013-07-25 18:23:51 +00:00
|
|
|
, svgContainer = null
|
|
|
|
//Must pass in the bounding chart's <svg> container.
|
|
|
|
//The mousemove event is attached to this container.
|
2013-07-03 14:33:48 +00:00
|
|
|
;
|
|
|
|
|
|
|
|
//Private variables
|
2013-08-13 01:58:17 +00:00
|
|
|
var isMSIE = navigator.userAgent.indexOf("MSIE") !== -1 //Check user-agent for Microsoft Internet Explorer.
|
2013-07-03 14:33:48 +00:00
|
|
|
;
|
|
|
|
|
2013-07-06 20:44:06 +00:00
|
|
|
|
2013-07-03 14:33:48 +00:00
|
|
|
function layer(selection) {
|
|
|
|
selection.each(function(data) {
|
|
|
|
var container = d3.select(this);
|
2013-07-07 03:32:28 +00:00
|
|
|
|
2013-07-03 14:33:48 +00:00
|
|
|
var availableWidth = (width || 960), availableHeight = (height || 400);
|
|
|
|
|
|
|
|
var wrap = container.selectAll("g.nv-wrap.nv-interactiveLineLayer").data([data]);
|
2013-07-03 17:43:32 +00:00
|
|
|
var wrapEnter = wrap.enter()
|
|
|
|
.append("g").attr("class", " nv-wrap nv-interactiveLineLayer");
|
|
|
|
|
2013-07-03 14:33:48 +00:00
|
|
|
|
2013-07-25 20:03:05 +00:00
|
|
|
wrapEnter.append("g").attr("class","nv-interactiveGuideLine");
|
2013-07-03 14:33:48 +00:00
|
|
|
|
2013-07-25 18:23:51 +00:00
|
|
|
if (!svgContainer) {
|
|
|
|
return;
|
|
|
|
}
|
2013-07-03 14:33:48 +00:00
|
|
|
|
2013-07-25 20:03:05 +00:00
|
|
|
function mouseHandler() {
|
|
|
|
var d3mouse = d3.mouse(this);
|
|
|
|
var mouseX = d3mouse[0];
|
|
|
|
var mouseY = d3mouse[1];
|
|
|
|
var subtractMargin = true;
|
2013-08-15 02:25:28 +00:00
|
|
|
var mouseOutAnyReason = false;
|
2013-07-25 20:03:05 +00:00
|
|
|
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;
|
2013-08-15 02:25:28 +00:00
|
|
|
|
|
|
|
if (d3.event.target.className.baseVal.match("nv-legend"))
|
|
|
|
mouseOutAnyReason = true;
|
2013-07-25 18:23:51 +00:00
|
|
|
|
2013-07-25 20:03:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if(subtractMargin) {
|
|
|
|
mouseX -= margin.left;
|
|
|
|
mouseY -= margin.top;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* If mouseX/Y is outside of the chart's bounds,
|
|
|
|
trigger a mouseOut event.
|
|
|
|
*/
|
|
|
|
if (mouseX < 0 || mouseY < 0
|
2013-08-02 19:25:37 +00:00
|
|
|
|| mouseX > availableWidth || mouseY > availableHeight
|
|
|
|
|| (d3.event.relatedTarget && d3.event.relatedTarget.ownerSVGElement === undefined)
|
2013-08-15 02:25:28 +00:00
|
|
|
|| mouseOutAnyReason
|
2013-08-06 12:56:00 +00:00
|
|
|
)
|
|
|
|
{
|
|
|
|
if (isMSIE) {
|
|
|
|
if (d3.event.relatedTarget
|
|
|
|
&& d3.event.relatedTarget.ownerSVGElement === undefined
|
|
|
|
&& d3.event.relatedTarget.className.match(tooltip.nvPointerEventsClass)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
2013-07-25 20:03:05 +00:00
|
|
|
dispatch.elementMouseout({
|
|
|
|
mouseX: mouseX,
|
|
|
|
mouseY: mouseY
|
|
|
|
});
|
|
|
|
layer.renderGuideLine(null); //hide the guideline
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var pointXValue = xScale.invert(mouseX);
|
|
|
|
dispatch.elementMousemove({
|
|
|
|
mouseX: mouseX,
|
|
|
|
mouseY: mouseY,
|
|
|
|
pointXValue: pointXValue
|
|
|
|
});
|
2013-09-03 14:28:26 +00:00
|
|
|
|
|
|
|
//If user double clicks the layer, fire a elementDblclick dispatch.
|
|
|
|
if (d3.event.type === "dblclick") {
|
|
|
|
dispatch.elementDblclick({
|
|
|
|
mouseX: mouseX,
|
|
|
|
mouseY: mouseY,
|
|
|
|
pointXValue: pointXValue
|
|
|
|
});
|
|
|
|
}
|
2013-07-25 20:03:05 +00:00
|
|
|
}
|
2013-07-03 14:33:48 +00:00
|
|
|
|
2013-07-25 20:03:05 +00:00
|
|
|
svgContainer
|
|
|
|
.on("mousemove",mouseHandler, true)
|
2013-09-03 14:28:26 +00:00
|
|
|
.on("mouseout" ,mouseHandler,true)
|
|
|
|
.on("dblclick" ,mouseHandler)
|
2013-07-03 14:33:48 +00:00
|
|
|
;
|
|
|
|
|
2013-07-04 16:21:12 +00:00
|
|
|
//Draws a vertical guideline at the given X postion.
|
2013-07-25 20:03:05 +00:00
|
|
|
layer.renderGuideLine = function(x) {
|
2013-07-04 03:17:08 +00:00
|
|
|
if (!showGuideLine) return;
|
2013-07-03 14:33:48 +00:00
|
|
|
var line = wrap.select(".nv-interactiveGuideLine")
|
|
|
|
.selectAll("line")
|
2013-07-05 20:14:48 +00:00
|
|
|
.data((x != null) ? [nv.utils.NaNtoZero(x)] : [], String);
|
2013-07-03 14:33:48 +00:00
|
|
|
|
|
|
|
line.enter()
|
|
|
|
.append("line")
|
2013-07-04 03:17:08 +00:00
|
|
|
.attr("class", "nv-guideline")
|
2013-07-03 14:33:48 +00:00
|
|
|
.attr("x1", function(d) { return d;})
|
|
|
|
.attr("x2", function(d) { return d;})
|
|
|
|
.attr("y1", availableHeight)
|
|
|
|
.attr("y2",0)
|
|
|
|
;
|
|
|
|
line.exit().remove();
|
|
|
|
|
2013-07-25 20:03:05 +00:00
|
|
|
}
|
2013-07-03 14:33:48 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
layer.dispatch = dispatch;
|
2013-07-04 03:17:08 +00:00
|
|
|
layer.tooltip = tooltip;
|
2013-07-03 14:33:48 +00:00
|
|
|
|
2013-07-25 18:23:51 +00:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2013-07-03 14:33:48 +00:00
|
|
|
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;
|
|
|
|
};
|
|
|
|
|
2013-07-04 03:17:08 +00:00
|
|
|
layer.showGuideLine = function(_) {
|
|
|
|
if (!arguments.length) return showGuideLine;
|
|
|
|
showGuideLine = _;
|
|
|
|
return layer;
|
|
|
|
};
|
|
|
|
|
2013-07-25 18:23:51 +00:00
|
|
|
layer.svgContainer = function(_) {
|
|
|
|
if (!arguments.length) return svgContainer;
|
|
|
|
svgContainer = _;
|
|
|
|
return layer;
|
|
|
|
};
|
|
|
|
|
2013-07-03 14:33:48 +00:00
|
|
|
|
|
|
|
return layer;
|
2013-07-04 16:21:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/* 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.
|
2013-07-04 21:36:02 +00:00
|
|
|
|
2013-07-08 13:36:29 +00:00
|
|
|
Unit tests can be found in: interactiveBisectTest.html
|
|
|
|
|
2013-07-04 21:36:02 +00:00
|
|
|
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.
|
2013-07-04 16:21:12 +00:00
|
|
|
*/
|
|
|
|
nv.interactiveBisect = function (values, searchVal, xAccessor) {
|
2013-08-13 01:58:17 +00:00
|
|
|
"use strict";
|
2013-07-04 16:21:12 +00:00
|
|
|
if (! values instanceof Array) return null;
|
2013-07-06 17:25:52 +00:00
|
|
|
if (typeof xAccessor !== 'function') xAccessor = function(d,i) { return d.x;}
|
2013-07-04 16:21:12 +00:00
|
|
|
|
|
|
|
var bisect = d3.bisector(xAccessor).left;
|
|
|
|
var index = d3.max([0, bisect(values,searchVal) - 1]);
|
2013-07-06 17:25:52 +00:00
|
|
|
var currentValue = xAccessor(values[index], index);
|
2013-07-04 16:21:12 +00:00
|
|
|
if (typeof currentValue === 'undefined') currentValue = index;
|
|
|
|
|
|
|
|
if (currentValue === searchVal) return index; //found exact match
|
|
|
|
|
|
|
|
var nextIndex = d3.min([index+1, values.length - 1]);
|
2013-07-06 17:25:52 +00:00
|
|
|
var nextValue = xAccessor(values[nextIndex], nextIndex);
|
2013-07-04 16:21:12 +00:00
|
|
|
if (typeof nextValue === 'undefined') nextValue = nextIndex;
|
|
|
|
|
|
|
|
if (Math.abs(nextValue - searchVal) >= Math.abs(currentValue - searchVal))
|
|
|
|
return index;
|
|
|
|
else
|
|
|
|
return nextIndex
|
2013-09-07 03:31:54 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
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);
|
2013-09-09 19:24:57 +00:00
|
|
|
if ( delta <= yDistMax && delta < threshold) {
|
2013-09-07 03:31:54 +00:00
|
|
|
yDistMax = delta;
|
|
|
|
indexToHighlight = i;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return indexToHighlight;
|
2013-07-03 14:33:48 +00:00
|
|
|
};
|