I am new to Reactive Extensions and JavaScript. Can someone help me unscrew the following code? It's from Matthew Podwysocki's Introduction to the Reactive Extensions to JavaScript.
<html>
<head>
<title>Learning ReactiveExtensions</title>
<!--scripts-->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js"></script>
<script src="rx.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
var mouseDragMe = $("#mouseDragMe").context;
var mouseMove = Rx.Observable.FromHtmlEvent(mouseDragMe, "mousemove");
var mouseUp = Rx.Observable.FromHtmlEvent(mouseDragMe, "mouseup");
var mouseDown = Rx.Observable.FromHtmlEvent(mouseDragMe, "mousedown");
var mouseMoves = mouseMove
.Skip(1)
.Zip(mouseMove, function(left, right) {
return { x1 : left.clientX,
y1 : left.clientY,
x2 : right.clientX,
y2 : right.clientY };
});
var mouseDrags = mouseDown.SelectMany(function(md) {
return mouseMoves.TakeUntil(mouseUp);
mouseDrags.Subscribe(function(mouseEvents) {
$("#results").html(
"Old (X: " + mouseEvents.x1 + " Y: " + mouseEvents.y1 + ") " +
"New (X: " + mouseEvents.x2 + " Y: " + mouseEvents.y2 + ")");
});
});
});
</script>
</head>
<body>
<div id="mouseDragMe" style="border:solid 1px red;">
i am a rx newbie
</div>
</body>
</html>
There are descriptions of this out there. I would recommend looking at this video about Writing your first Rx Application. The code's in C# but the concepts are exactly the same regardless of the programming language.
Essentially you want to understand 3 things
Conceptualizing of a series of events as a Sequence. Just like an
array is a sequence of data in space, events can be thought of as a
sequence of data in time (or in motion).
The operators (i.e. Skip, Zip, SelectMany and TakeUntil)
Subscription semantics
In this scenario we have 3 source sequences; mouseMove, mouseUp and mouseDown.
The mouseMove sequence will push a value of the mouse coordinates each time the mouse is moved. For example if the mouse started at the top left corner of the screen and moved diagonally down then straight across you may see values like {0,0}, {10,10}, {20,10} published on the sequence.
The values that mouseUp and mouseDown publish are not interesting, just the point in time that they are published is interesting.
The actual problem of "dragging" requires us to know when the mouse button is pressed and the delta of where the mouse was when it was pressed, and where it was when the button was released. The way we can get the delta of these positions is to take values of the final position and minus the values of the original position. Even better is if we can get all of the intermediate delta values so we can animate the movement. If we take our sequence above, to get a delta of movements we want to have the original sequence and an off by one sequence
Original { 0, 0}, {10,10}, {20,10}
offby1 {10,10}, {20,10}
This allows us to calculate the deltas to figure out the movement (not just the location).
Original { 0, 0}, {10,10}, {20,10}
offby1 {10,10}, {20,10}
delta {10,10}, {10, 0}
The way we can achieve this with Rx is first to use Skip(1) to skip one value. This creates our offby1 sequence. Next we want to combine values in pairs. The Zip function gives us this (more in my blog post about Combining sequences with Zip).
We could rewrite the code you have above
var mouseMoves = mouseMove
.Skip(1)
.Zip(mouseMove, function(left, right) {
return { x1 : left.clientX,
y1 : left.clientY,
x2 : right.clientX,
y2 : right.clientY };
});
To be
var offby1 = mouseMove.Skip(1);
var mouseMoves = offby1.Zip(mouseMove, function(left, right) {
return { x1 : left.clientX,
y1 : left.clientY,
x2 : right.clientX,
y2 : right.clientY };
});
Once we have the pairs, we need to apply the simple math of newValue-OldValue=delta.
This gives us our delta sequence which is effectively our Movement sequence.
var offby1 = mouseMove.Skip(1);
var mouseMoves = offby1.Zip(mouseMove, function(current, last) {
return { x : current.clientX-last.clientX,
y : current.clientY-last.clientY };
});
Now we only want to get values when the mouse is down. To do this we use the SelectMany operator. This says for each value from the source, get 0 or more values from this source. In our case each time a mouseDown event happens we want to get all the delta events (as mouseMoves).
var mouseDrags = mouseDown.SelectMany(function(md) { return mouseMoves;});
However we only want to keep receiving them until a corresponding mouseUp event happens. To do this we TakeUntil the mouseUp event produces a value.
var mouseDrags = mouseDown.SelectMany(function(md) {
return mouseMoves.TakeUntil(mouseUp);
});
Now that we have all this plumbing done, we usually apply those movements to the Position/Margin/offset of a UI element. It appears in the example, we just print the value though.
Related
I'm trying to create a function in JQuery that increase and decrease the points clicked. I've managed to create the onclick event that increases the points, but can't figure out how to decrease it.
$("#radarChart").click(
function (evt) {
var activePoints = myRadarChart.getPointsAtEvent(evt);
var Values = activePoints[0].label + ' = ' + activePoints[0].value;
activePoints[0].value++;
}
);
The above function increases the value of a point when clicked, in order to decrease it I need to know if the filled area is clicked.
I looked at ChartJS documentation and didn't come across it. Other charts do have it, for example Polar Area Chart, when you are hovering a section, it highlights, meaning there is a function that detects mouse hovering over segments.
Does a similar function exist in Radar Charts?
My codepen.
If not, any ideas on how I could achieve this, would be appreciated.
The only alternative I can think of, would be to create 10 buttons, 5 labels. The buttons would be + and -, increase and decrease the label. This takes too much space in my opinion, so I'm trying to avoid it.
Thanks,
There really is no specific chart.js API to do what you are wanting, but you can achieve the same results using the canvas API and a little geometry.
Basically, you want to increase the value if the user clicks outside the current value's region, and you want to decrease if the user clicks inside the current value's region.
I've modified your click handler to do just that.
function getElementPosition(obj) {
var curleft = 0, curtop = 0;
if (obj.offsetParent) {
do {
curleft += obj.offsetLeft;
curtop += obj.offsetTop;
} while (obj = obj.offsetParent);
return { x: curleft, y: curtop };
}
return undefined;
};
function getEventLocation(element,event){
// Relies on the getElementPosition function.
var pos = getElementPosition(element);
return {
x: (event.pageX - pos.x),
y: (event.pageY - pos.y)
};
};
function pointDistance(point1, point2) {
return Math.sqrt(Math.pow(point2.x - point1.x, 2) + Math.pow(point2.y - point1.y, 2));
};
//Get the context of the Radar Chart canvas element we want to select
var ctx = document.getElementById("radarChart").getContext("2d");
// Create the Radar Chart
var myRadarChart = new Chart(ctx).Radar(radarData, radarOptions);
$("#radarChart").click(function (evt) {
var eventLocation = getEventLocation(this,evt);
var activePoints = myRadarChart.getPointsAtEvent(evt);
var eventLocDistToCenter = pointDistance({x: myRadarChart.scale.xCenter, y: myRadarChart.scale.yCenter}, eventLocation);
var activePointDistToCenter = pointDistance({x: myRadarChart.scale.xCenter, y: myRadarChart.scale.yCenter}, activePoints[0]);
if (eventLocDistToCenter < activePointDistToCenter) {
activePoints[0].value--;
} else {
activePoints[0].value++;
}
myRadarChart.update();
});
Note, I also added a call to .update() so that the chart renders the change immediately. With the way you had it implemented, you would not see the chart change until the next render (i.e. when the mouse moves).
Here is a codepen forked from yours with the working solution. Click around to check it out.
Lastly, you probably want to think about upgrading to chart.js 2.0 (latest release is 2.5). 1.0 is long since unsupported and the latest version has LOTS of improvements. You should be able to easily port this over. Post a new question if you need help.
I am using a couple of functions from Snap.SVG, mainly path2curve and the functions around it to build a SVG morph plugin.
I've setup a demo here on Codepen to better illustrate the issue. Basically morphing shapes simple to complex and the other way around is working properly as of Javascript functionality, however, the visual isn't very pleasing.
The first shape morph looks awful, the second looks a little better because I changed/rotated it's points a bit, but the last example is perfect.
So I need either a better path2curve or a function to prepare the path string before the other function builds the curves array. Snap.SVG has a function called getClosest that I think may be useful but it's not documented.
There isn't any documentation available on this topic so I would appreciate any suggestion/input from RaphaelJS / SnapSVG / d3.js / three/js developers.
I've provided a runnable code snippet below that uses Snap.svg and that I believe demonstrates one solution to your problem. With respect to trying to find the best way to morph a starting shape into an ending shape, this algorithm essentially rotates the points of the starting shape one position at a time, sums the squares of the distances between corresponding points on the (rotated) starting shape and the (unchanged) ending shape, and finds the minimum of all those sums. i.e. It's basically a least squares approach. The minimum value identifies the rotation that, as a first guess, will provide the "shortest" morph trajectories. In spite of these coordinate reassignments, however, all 'rotations' should result in visually identical starting shapes, as required.
This is, of course, a "blind" mathematical approach, but it might help provide you with a starting point before doing manual visual analysis. As a bonus, even if you don't like the rotation that the algorithm chose, it also provides the path 'd' attribute strings for all the other rotations, so some of that work has already been done for you.
You can modify the snippet to provide any starting and ending shapes you want. The limitations are as follows:
Each shape should have the same number of points (although the point types, e.g. 'lineto', 'cubic bezier curve', 'horizontal lineto', etc., can completely vary)
Each shape should be closed, i.e. end with "Z"
The morph desired should involve only translation. If scaling or rotation is desired, those should be applied after calculating the morph based only on translation.
By the way, in response to some of your comments, while I find Snap.svg intriguing, I also find its documentation to be somewhat lacking.
Update: The code snippet below works in Firefox (Mac or Windows) and Safari. However, Chrome seems to have trouble accessing the Snap.svg library from its external web site as written (<script...github...>). Opera and Internet Explorer also have problems. So, try the snippet in the working browsers, or try copying the snippet code as well as the Snap library code to your own computer. (Is this an issue of accessing third party libraries from within the code snippet? And why browser differences? Insightful comments would be appreciated.)
var
s = Snap(),
colors = ["red", "blue", "green", "orange"], // colour list can be any length
staPath = s.path("M25,35 l-15,-25 C35,20 25,0 40,0 L80,40Z"), // create the "start" shape
endPath = s.path("M10,110 h30 l30,20 C30,120 35,135 25,135Z"), // create the "end" shape
staSegs = getSegs(staPath), // convert the paths to absolute values, using only cubic bezier
endSegs = getSegs(endPath), // segments, & extract the pt coordinates & segment strings
numSegs = staSegs.length, // note: the # of pts is one less than the # of path segments
numPts = numSegs - 1, // b/c the path's initial 'moveto' pt is also the 'close' pt
linePaths = [],
minSumLensSqrd = Infinity,
rotNumOfMin,
rotNum = 0;
document.querySelector('button').addEventListener('click', function() {
if (rotNum < numPts) {
linePaths.forEach(function(linePath) {linePath.remove();}); // erase any previous coloured lines
var sumLensSqrd = 0;
for (var ptNum = 0; ptNum < numPts; ptNum += 1) { // draw new lines, point-to-point
var linePt1 = staSegs[(rotNum + ptNum) % numPts]; // the new line begins on the 'start' shape
var linePt2 = endSegs[ ptNum % numPts]; // and finished on the 'end' shape
var linePathStr = "M" + linePt1.x + "," + linePt1.y + "L" + linePt2.x + "," + linePt2.y;
var linePath = s.path(linePathStr).attr({stroke: colors[ptNum % colors.length]}); // draw it
var lineLen = Snap.path.getTotalLength(linePath); // calculate its length
sumLensSqrd += lineLen * lineLen; // square the length, and add it to the accumulating total
linePaths[ptNum] = linePath; // remember the path to facilitate erasing it later
}
if (sumLensSqrd < minSumLensSqrd) { // keep track of which rotation has the lowest value
minSumLensSqrd = sumLensSqrd; // of the sum of lengths squared (the 'lsq sum')
rotNumOfMin = rotNum; // as well as the corresponding rotation number
}
show("ROTATION OF POINTS #" + rotNum + ":"); // display info about this rotation
var rotInfo = getRotInfo(rotNum);
show(" point coordinates: " + rotInfo.ptsStr); // show point coordinates
show(" path 'd' string: " + rotInfo.dStr); // show 'd' string needed to draw it
show(" sum of (coloured line lengths squared) = " + sumLensSqrd); // the 'lsq sum'
rotNum += 1; // analyze the next rotation of points
} else { // once all the rotations have been analyzed individually...
linePaths.forEach(function(linePath) {linePath.remove();}); // erase any coloured lines
show(" ");
show("BEST ROTATION, i.e. rotation with lowest sum of (lengths squared): #" + rotNumOfMin);
// show which rotation to use
show("Use the shape based on this rotation of points for morphing");
$("button").off("click");
}
});
function getSegs(path) {
var absCubDStr = Snap.path.toCubic(Snap.path.toAbsolute(path.attr("d")));
return Snap.parsePathString(absCubDStr).map(function(seg, segNum) {
return {x: seg[segNum ? 5 : 1], y: seg[segNum ? 6 : 2], seg: seg.toString()};
});
}
function getRotInfo(rotNum) {
var ptsStr = "";
for (var segNum = 0; segNum < numSegs; segNum += 1) {
var oldSegNum = rotNum + segNum;
if (segNum === 0) {
var dStr = "M" + staSegs[oldSegNum].x + "," + staSegs[oldSegNum].y;
} else {
if (oldSegNum >= numSegs) oldSegNum -= numPts;
dStr += staSegs[oldSegNum].seg;
}
if (segNum !== (numSegs - 1)) {
ptsStr += "(" + staSegs[oldSegNum].x + "," + staSegs[oldSegNum].y + "), ";
}
}
ptsStr = ptsStr.slice(0, ptsStr.length - 2);
return {ptsStr: ptsStr, dStr: dStr};
}
function show(msg) {
var m = document.createElement('pre');
m.innerHTML = msg;
document.body.appendChild(m);
}
pre {
margin: 0;
padding: 0;
}
<script src="//cdn.jsdelivr.net/snap.svg/0.4.1/snap.svg-min.js"></script>
<p>Best viewed on full page</p>
<p>Coloured lines show morph trajectories for the points for that particular rotation of points. The algorithm seeks to optimize those trajectories, essentially trying to find the "shortest" cumulative routes.</p>
<p>The order of points can be seen by following the colour of the lines: red, blue, green, orange (at least when this was originally written), repeating if there are more than 4 points.</p>
<p><button>Click to show rotation of points on top shape</button></p>
I asynchronously receive new positions of element. Each time I receive the position I calculate the offset and move the element (set), like this:
asynchReceiveData(id,function(newposition){
var offset = {};
var oldposition = getOldPosition(markerArray[id]); //returns object with old x and y
offset.x = (newposition.x - oldposition.x);
offset.y = (newposition.y - oldposition.y);
markerArray[id].entireSet.stop()
.animate({ transform:"...T"+offset.x+","+offset.y }, 400);
//i also tried without .stop()
});
For example:
with each update the set should move 50px to the right, after 10 slow updates (lets say one update per 2 seconds), the set is 500px to the right, everything is OK.
Problem is, when I receive too many new positions too fast:
(e.g. one update per 200ms),then the set is 300, or 350 or 400 or 450 instead of 500px to the right.
I think the problem is that the animation does not have enough time to finish before new animation is triggered. I tried lowering the animation time from 400ms to 200, but with little success, it still sometimes happened.
Everything works fine, when I do not use animation and do just this:
markerArray[id].entireSet.transform("...T"+offset.x+","+offset.y);
But I would like to have this animations. Do you have any suggestion how to fix this?
So.. after few tries I came to a solution:
After the end of every animation I check the real position of element (with getBBox()) and compare it with expected position. If it differs I move the element by the difference; In code:
asynchReceiveData(id,function(newposition){
var offset = {};
var oldposition = getOldPosition(markerArray[id]); //returns object with old x and y
offset.x = (newposition.x - oldposition.x);
offset.y = (newposition.y - oldposition.y);
markerArray[id].entireSet.stop().animate({ transform:"...T"+offset.x+","+offset.y}, 500,
function () {
var o = {};
o.x = markerArray[id].x - markerArray[id].circleObj.getBBox().cx;
o.y = markerArray[id].y - markerArray[id].circleObj.getBBox().cy;
markerArray[id].entireSet.transform("...T"+o.x+","+o.y);
});
});
Sometimes it's not very smooth (a bit laggy), but anyway, it solves the problem.
I'm trying to trigger an event half-way through the progress (not time) of a transition. It sounds simple, but since the transition can have any curve it's quite tricky. In my particular case it's not going to be paused or anything so that consideration is out of the way.
(Simplified) essentially I could trigger an animation on a modifier like this:
function scaleModifierTo(stateModifier, scale, animationDuration) {
stateModifier.setTransform(
Transform.scale(scale, scale, scale),
{
duration: animationDuration,
curve: this.options.curve
}
);
}
When the interpolated state of the Transitionable hits 0.5 (half-way through) I want to trigger a function.
I haven't dug that deep behind in the source of famo.us yet, but maybe need to do something like
subclass something and add the possibility to listen when the state passes through a certain point?
reverse the curve defined and use a setTimeout (or try to find a proximity using a few iterations of the chosen curve algorithm (ew))
Is it possible to do this easily? What route should I go down?
I can think of a couple of ways to achieve such, and both lend to the use of Modifier over StateModifier. If you are new, and haven't really had the chance to explore the differences, Modifier consumes state from the transformFrom method which takes a function that returns a transform. This is where we can use our own Transitionable to supply state over the lifetime of our modifier.
To achieve what you wish, I used a Modifier with a basic transformFrom that will alter the X position of the surface based on the value of the Transitionable. I can then monitor the transitionable to determine when it is closest, or in my case greater than or equal to half of the final value. The prerender function will be called and checked on every tick of the engine, and is unbinded when we hit the target.
Here is that example..
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var Modifier = require('famous/core/Modifier');
var Transform = require('famous/core/Transform');
var Transitionable = require('famous/transitions/Transitionable');
var SnapTransition = require('famous/transitions/SnapTransition');
Transitionable.registerMethod('snap',SnapTransition);
var snap = { method:'snap', period:1000, damping:0.6};
var context = Engine.createContext();
var surface = new Surface({
size:[200,200],
properties:{
backgroundColor:'green'
}
});
surface.trans = new Transitionable(0);
surface.mod = new Modifier();
surface.mod.transformFrom(function(){
return Transform.translate(surface.trans.get(),0,0);
});
context.add(surface.mod).add(surface);
function triggerTransform(newValue, transition) {
var prerender = function(){
var pos = surface.trans.get();
if (pos >= (newValue / 2.0)) {
// Do Something.. Emit event etc..
console.log("Hello at position: "+pos);
Engine.removeListener('prerender',prerender);
}
}
Engine.on('prerender',prerender);
surface.trans.halt();
surface.trans.set(newValue,transition);
}
surface.on('click',function(){ triggerTransform(400, snap); });
The downside of this example is the fact that you are querying the transitionable twice. An alternative is to add your transitionable check right in the transformFrom method. This could get a bit strange, but essentially we are modifying our transformFrom method until we hit our target value, then we revert back to the original transformFrom method.. triggerTransform would be defined as follows..
Hope this helps!
function triggerTransform(newValue, transition) {
surface.mod.transformFrom(function(){
pos = surface.trans.get()
if (pos >= newValue/2.0) {
// Do something
console.log("Hello from position: " + pos)
surface.mod.transformFrom(function(){
return Transform.translate(surface.trans.get(),0,0);
});
}
return Transform.translate(pos,0,0)
})
surface.trans.set(newValue,transition);
}
Thank you for your responses, especially #johntraver for the prerender event, I wasn't aware of the existence of that event.
I realised it made more sense that I should handle this logic together with my move animation, not the scale one. Then, I ended up using a (very hacky) way of accessing the current state of the transition and by defining a threshold in px I can trigger my function when needed.
/**
* Move view at index to a specified offset
* #param {Number} index
* #param {Number} xOffset xOffset to move to
* #param {Object} animation Animation properties
* #return void
*/
function moveView(index, xOffset, animation) {
var rectModifier = this._views[index].modifiers.rect;
var baseXOffset = rectModifier._transformState.translate.state[0];
// After how long movement is reflow needed?
// for the sake of this example I use half the distance of the animation
var moveThreshold = Math.abs(baseXOffset - xOffset)/2;
/**
* Callback function triggered on each animation frame to see if the view is now covering
* the opposite so we can trigger a reflow of the z index
* #return void
*/
var prerender = function() {
var numPixelsMoved = Math.abs(baseXOffset - rectModifier._transformState.translate.state[0]);
if (numPixelsMoved > moveThreshold) {
Engine.removeListener('prerender', prerender);
// trigger a method when this is reached
_reflowZIndex.call(this);
}
}.bind(this);
rectModifier.setTransform(
Transform.translate(xOffset, 0, 0),
animation,
function() {
Engine.removeListener('prerender', prerender);
}
);
Engine.on('prerender', prerender);
}
Obviously the ._transformState.translate.state[0] is a complete hack, but I couldn't figure out of getting this value in a clean way without adding my own Transitionable, which I don't want. If there is a cleaner way of finding the current state as a Number between 0.0-1.0 that would be ace; anyone knows of one?
I'm using the line-with-focus chart ( View Finder ) example in nvd3. That means there's 3 or 4 lines ( series ) being drawn on the graph. When i hover over any of the lines I want to get back all the y-values for all lines of that given x-axis position ( for the most part these will be interpolated y-values per line ).
I see in the nv.models.lineWithFocusChart source code that using a callback for the elementMouseover.tooltip event I can get my data's x-value back for the data points on the line.
The closest part of the source code that does what i want is with the interactiveGuideline code for the lineChart examples. However, i don't want to create a <rect> overlay with elementMousemove interaction. I think i can modify this code to filter my data and get each line's y-value, but I'm sure there's an easier way I'm not seeing.
I think I'm on the right track, but just wondering if someone had this need before and found a quicker route than the rabbit hole I'm about jump in.
Thanks for feedback
This is the basic functionality you're looking for, it still needs a bit of finesse and styling of the tooltips. (Right now the tooltip blocks the view of the points...)
Key code to call after the drawing the chart in (for example, within the nv.addGraph function on the NVD3 live code site):
d3.selectAll("g.nv-focus g.nv-point-paths")
.on("mouseover.mine", function(dataset){
//console.log("Data: ", dataset);
var singlePoint, pointIndex, pointXLocation, allData = [];
var lines = chart.lines;
var xScale = chart.xAxis.scale();
var yScale = chart.yAxis.scale();
var mouseCoords = d3.mouse(this);
var pointXValue = xScale.invert(mouseCoords[0]);
dataset
.filter(function(series, i) {
series.seriesIndex = i;
return !series.disabled;
})
.forEach(function(series,i) {
pointIndex = nv.interactiveBisect(series.values, pointXValue, lines.x());
lines.highlightPoint(i, pointIndex, true);
var point = series.values[pointIndex];
if (typeof point === 'undefined') return;
if (typeof singlePoint === 'undefined') singlePoint = point;
if (typeof pointXLocation === 'undefined')
pointXLocation = xScale(lines.x()(point,pointIndex));
allData.push({
key: series.key,
value: lines.y()(point, pointIndex),
color: lines.color()(series,series.seriesIndex)
});
});
/*
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 ( delta <= yDistMax && delta < threshold) {
yDistMax = delta;
indexToHighlight = i;
}
});
return indexToHighlight;
};
//Determine which line the mouse is closest to.
if (allData.length > 2) {
var yValue = yScale.invert( mouseCoords[1] );
var domainExtent = Math.abs(yScale.domain()[0] - yScale.domain()[1]);
var threshold = 0.03 * domainExtent;
var indexToHighlight = nv.nearestValueIndex(
allData.map(function(d){ return d.value}), yValue, threshold
);
if (indexToHighlight !== null)
allData[indexToHighlight].highlight = true;
//set a flag you can use when styling the tooltip
}
//console.log("Points for all series", allData);
var xValue = chart.xAxis.tickFormat()( lines.x()(singlePoint,pointIndex) );
d3.select("div.nvtooltip:last-of-type")
.html(
"Point: " + xValue + "<br/>" +
allData.map(function(point){
return "<span style='color:" + point.color +
(point.highlight? ";font-weight:bold" : "") + "'>" +
point.key + ": " +
chart.yAxis.tickFormat()(point.value) +
"</span>";
}).join("<br/><hr/>")
);
}).on("mouseout.mine", function(d,i){
//select all the visible circles and remove the hover class
d3.selectAll("g.nv-focus circle.hover").classed("hover", false);
});
The first thing to figure out was which objects should I bind the events to? The logical choice was the Voronoi path elements, but even when I namespaced the event names to avoid conflict the internal event handlers nothing was triggering my event handling function. It seems that a parent <g> event captures the mouse events before they can reach the individual <path> elements. However, it works just fine if instead I bind the events to the <g> element that contains the Voronoi paths, and it has the added benefit of giving me direct access to the entire dataset as the data object passed to my function. That means that even if the data is later updated, the function is still using the active data.
The rest of the code is based on the Interactive Guideline code for the NVD3 line graphs, but I had to make a couple important changes:
Their code is inside the closure of the chart function and can access private variables, I can't. Also the context+focus graph has slightly different names/functionality for accessing chart components, because it is made up of two charts. Because of that:
chart in the internal code is chart.lines externally,
xScale and yScale have to be accessed from the chart axes,
the color scale and the x and y accessor functions are accessible within lines,
I have to select the tooltip instead of having it in a variable
Their function is called with custom event as the e parameter that has already had the mouse coordinates calculated, I have to calculate them myself.
One of their calculations uses a function (nv.nearestValueIndex) which is only initialized if you create an interactive layer, so I had to copy that function definition into mine.
I think that about covers it. If there's anything else you can't follow, leave a comment.