1 /* Utility class to handle creation of an interactive layer.
2 This places a rectangle on top of the chart. When you mouse move over it, it sends a dispatch
3 containing the X-coordinate. It can also render a vertical line where the mouse is located.
5 dispatch.elementMousemove is the important event to latch onto. It is fired whenever the mouse moves over
6 the rectangle. The dispatch is given one object which contains the mouseX/Y location.
7 It also has 'pointXValue', which is the conversion of mouseX to the x-axis scale.
9 nv.interactiveGuideline = function() {
11 var tooltip = nv.models.tooltip();
15 //Please pass in the bounding chart's top and left margins
16 //This is important for calculating the correct mouseX/Y positions.
17 , margin = {left: 0, top: 0}
18 , xScale = d3.scale.linear()
19 , yScale = d3.scale.linear()
20 , dispatch = d3.dispatch('elementMousemove', 'elementMouseout','elementDblclick')
21 , showGuideLine = true
23 //Must pass in the bounding chart's <svg> container.
24 //The mousemove event is attached to this container.
28 var isMSIE = navigator.userAgent.indexOf("MSIE") !== -1 //Check user-agent for Microsoft Internet Explorer.
32 function layer(selection) {
33 selection.each(function(data) {
34 var container = d3.select(this);
36 var availableWidth = (width || 960), availableHeight = (height || 400);
38 var wrap = container.selectAll("g.nv-wrap.nv-interactiveLineLayer").data([data]);
39 var wrapEnter = wrap.enter()
40 .append("g").attr("class", " nv-wrap nv-interactiveLineLayer");
43 wrapEnter.append("g").attr("class","nv-interactiveGuideLine");
49 function mouseHandler() {
50 var d3mouse = d3.mouse(this);
51 var mouseX = d3mouse[0];
52 var mouseY = d3mouse[1];
53 var subtractMargin = true;
54 var mouseOutAnyReason = false;
57 D3.js (or maybe SVG.getScreenCTM) has a nasty bug in Internet Explorer 10.
58 d3.mouse() returns incorrect X,Y mouse coordinates when mouse moving
60 However, d3.event.offsetX/Y also returns the mouse coordinates
61 relative to the triggering <rect>. So we use offsetX/Y on IE.
63 mouseX = d3.event.offsetX;
64 mouseY = d3.event.offsetY;
67 On IE, if you attach a mouse event listener to the <svg> container,
68 it will actually trigger it for all the child elements (like <path>, <circle>, etc).
69 When this happens on IE, the offsetX/Y is set to where ever the child element
71 As a result, we do NOT need to subtract margins to figure out the mouse X/Y
72 position under this scenario. Removing the line below *will* cause
73 the interactive layer to not work right on IE.
75 if(d3.event.target.tagName !== "svg")
76 subtractMargin = false;
78 if (d3.event.target.className.baseVal.match("nv-legend"))
79 mouseOutAnyReason = true;
84 mouseX -= margin.left;
88 /* If mouseX/Y is outside of the chart's bounds,
89 trigger a mouseOut event.
91 if (mouseX < 0 || mouseY < 0
92 || mouseX > availableWidth || mouseY > availableHeight
93 || (d3.event.relatedTarget && d3.event.relatedTarget.ownerSVGElement === undefined)
98 if (d3.event.relatedTarget
99 && d3.event.relatedTarget.ownerSVGElement === undefined
100 && d3.event.relatedTarget.className.match(tooltip.nvPointerEventsClass)) {
104 dispatch.elementMouseout({
108 layer.renderGuideLine(null); //hide the guideline
112 var pointXValue = xScale.invert(mouseX);
113 dispatch.elementMousemove({
116 pointXValue: pointXValue
119 //If user double clicks the layer, fire a elementDblclick dispatch.
120 if (d3.event.type === "dblclick") {
121 dispatch.elementDblclick({
124 pointXValue: pointXValue
130 .on("mousemove",mouseHandler, true)
131 .on("mouseout" ,mouseHandler,true)
132 .on("dblclick" ,mouseHandler)
135 //Draws a vertical guideline at the given X postion.
136 layer.renderGuideLine = function(x) {
137 if (!showGuideLine) return;
138 var line = wrap.select(".nv-interactiveGuideLine")
140 .data((x != null) ? [nv.utils.NaNtoZero(x)] : [], String);
144 .attr("class", "nv-guideline")
145 .attr("x1", function(d) { return d;})
146 .attr("x2", function(d) { return d;})
147 .attr("y1", availableHeight)
150 line.exit().remove();
156 layer.dispatch = dispatch;
157 layer.tooltip = tooltip;
159 layer.margin = function(_) {
160 if (!arguments.length) return margin;
161 margin.top = typeof _.top != 'undefined' ? _.top : margin.top;
162 margin.left = typeof _.left != 'undefined' ? _.left : margin.left;
166 layer.width = function(_) {
167 if (!arguments.length) return width;
172 layer.height = function(_) {
173 if (!arguments.length) return height;
178 layer.xScale = function(_) {
179 if (!arguments.length) return xScale;
184 layer.showGuideLine = function(_) {
185 if (!arguments.length) return showGuideLine;
190 layer.svgContainer = function(_) {
191 if (!arguments.length) return svgContainer;
200 /* Utility class that uses d3.bisect to find the index in a given array, where a search value can be inserted.
201 This is different from normal bisectLeft; this function finds the nearest index to insert the search value.
203 For instance, lets say your array is [1,2,3,5,10,30], and you search for 28.
204 Normal d3.bisectLeft will return 4, because 28 is inserted after the number 10. But interactiveBisect will return 5
205 because 28 is closer to 30 than 10.
207 Unit tests can be found in: interactiveBisectTest.html
209 Has the following known issues:
210 * Will not work if the data points move backwards (ie, 10,9,8,7, etc) or if the data points are in random order.
211 * Won't work if there are duplicate x coordinate values.
213 nv.interactiveBisect = function (values, searchVal, xAccessor) {
215 if (! values instanceof Array) return null;
216 if (typeof xAccessor !== 'function') xAccessor = function(d,i) { return d.x;}
218 var bisect = d3.bisector(xAccessor).left;
219 var index = d3.max([0, bisect(values,searchVal) - 1]);
220 var currentValue = xAccessor(values[index], index);
221 if (typeof currentValue === 'undefined') currentValue = index;
223 if (currentValue === searchVal) return index; //found exact match
225 var nextIndex = d3.min([index+1, values.length - 1]);
226 var nextValue = xAccessor(values[nextIndex], nextIndex);
227 if (typeof nextValue === 'undefined') nextValue = nextIndex;
229 if (Math.abs(nextValue - searchVal) >= Math.abs(currentValue - searchVal))
236 Returns the index in the array "values" that is closest to searchVal.
237 Only returns an index if searchVal is within some "threshold".
238 Otherwise, returns null.
240 nv.nearestValueIndex = function (values, searchVal, threshold) {
242 var yDistMax = Infinity, indexToHighlight = null;
243 values.forEach(function(d,i) {
244 var delta = Math.abs(searchVal - d);
245 if ( delta <= yDistMax && delta < threshold) {
247 indexToHighlight = i;
250 return indexToHighlight;