Skip to content

Commit f4a1203

Browse files
committed
Added tooltip callback function
1 parent bcdc5e8 commit f4a1203

2 files changed

Lines changed: 192 additions & 5 deletions

File tree

src/components/modebar/buttons.js

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,7 @@ modeBarButtons.tooltip = {
702702
gd._tooltipClickHandler = function(data) {
703703
var traceIndex = data.points[0].curveNumber;
704704
var trace = gd.data[traceIndex];
705+
var fullTrace = gd._fullData[traceIndex];
705706
var pts = data.points[0];
706707

707708
// handle missing axis in data.points[0] (in scattercarpet)
@@ -722,13 +723,32 @@ modeBarButtons.tooltip = {
722723

723724
var userTemplate = trace.tooltiptemplate;
724725
if(userTemplate === undefined || userTemplate === null || userTemplate === '') {
725-
userTemplate = gd._fullData[traceIndex].tooltiptemplate;
726+
userTemplate = fullTrace.tooltiptemplate;
726727
}
727728
if(userTemplate === undefined || userTemplate === null || userTemplate === '') {
728729
userTemplate = defaultTemplate;
729730
}
730-
var customStyle = lodash.defaults({}, trace.tooltip, DEFAULT_STYLE); // Merge custom style with default
731-
addTooltip(gd, data, userTemplate, customStyle);
731+
732+
var callbackResult;
733+
if(typeof trace.tooltipfunction === 'function') {
734+
callbackResult = trace.tooltipfunction({
735+
gd: gd,
736+
eventData: data,
737+
event: data.event,
738+
point: pts,
739+
trace: trace,
740+
fullTrace: fullTrace,
741+
calcdata: gd.calcdata && gd.calcdata[traceIndex],
742+
fullLayout: fullLayout,
743+
xaxis: pts.xaxis,
744+
yaxis: pts.yaxis
745+
});
746+
747+
if(callbackResult === false || callbackResult === null) return;
748+
}
749+
750+
var customStyle = trace.tooltip;
751+
addTooltip(gd, data, userTemplate, customStyle, callbackResult);
732752
};
733753
gd.on('plotly_click', gd._tooltipClickHandler);
734754
} else {
@@ -791,11 +811,27 @@ function stackedCoord(gd, pts) {
791811
}
792812
}
793813

794-
function addTooltip(gd, data, userTemplate, customStyle) {
814+
function addTooltip(gd, data, userTemplate, customStyle, callbackResult) {
795815
var pts = data.points[0];
796816
var fullLayout = gd._fullLayout;
797817

798818
if(pts && pts.xaxis && pts.yaxis && fullLayout) {
819+
var annotationOverrides;
820+
var styleOverrides;
821+
822+
if(typeof callbackResult === 'string') {
823+
userTemplate = callbackResult;
824+
} else if(callbackResult && typeof callbackResult === 'object') {
825+
if(callbackResult.point) {
826+
pts = lodash.defaults({}, callbackResult.point, pts);
827+
}
828+
if(callbackResult.text !== undefined) {
829+
userTemplate = callbackResult.text;
830+
}
831+
annotationOverrides = callbackResult.annotation;
832+
styleOverrides = callbackResult.style;
833+
}
834+
799835
userTemplate = userTemplate || '';
800836

801837
// Convert template to text using Plotly hovertemplate formatting method
@@ -845,7 +881,13 @@ function addTooltip(gd, data, userTemplate, customStyle) {
845881
ay: -20
846882
};
847883

848-
lodash.defaults(newAnnotation, customStyle);
884+
if(annotationOverrides) {
885+
newAnnotation = Lib.extendFlat({}, newAnnotation, annotationOverrides);
886+
}
887+
888+
var mergedStyle = lodash.defaults({}, styleOverrides, customStyle, DEFAULT_STYLE);
889+
890+
lodash.defaults(newAnnotation, mergedStyle);
849891

850892
// Prevent having multiple tooltip annotations on the same point (useful when user wants to annotate nearby points)
851893
// Does not prevent multiple tooltips on histogram (would not be useful on bars)

test/jasmine/tests/tooltip_annotation_test.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,148 @@ describe('Log Tooltip interactions', function() {
225225
}, 20);
226226
});
227227
});
228+
229+
describe('Tooltip callback interactions', function() {
230+
var gd;
231+
232+
function pushN(xs, ys, x, y, n) {
233+
for(var i = 0; i < n; i++) {
234+
xs.push(x);
235+
ys.push(y);
236+
}
237+
}
238+
239+
function clickDataPoint(gd, x, y) {
240+
var bb = gd.getBoundingClientRect();
241+
var size = gd._fullLayout._size;
242+
var xa = gd._fullLayout.xaxis;
243+
var ya = gd._fullLayout.yaxis;
244+
245+
click(
246+
bb.left + size.l + xa.c2p(x),
247+
bb.top + size.t + ya.c2p(y)
248+
);
249+
}
250+
251+
beforeAll(function(done) {
252+
var xs = [];
253+
var ys = [];
254+
255+
pushN(xs, ys, 0.5, 0.5, 1);
256+
pushN(xs, ys, 1.5, 1.5, 2);
257+
pushN(xs, ys, 2.5, 1.5, 5);
258+
pushN(xs, ys, 2.5, 2.5, 3);
259+
260+
gd = createGraphDiv();
261+
Plotly.newPlot(gd, [{
262+
type: 'histogram2d',
263+
x: xs,
264+
y: ys,
265+
autobinx: false,
266+
autobiny: false,
267+
xbins: {start: 0, end: 3, size: 1},
268+
ybins: {start: 0, end: 3, size: 1},
269+
colorscale: 'Blues'
270+
}], {
271+
width: 400,
272+
height: 400,
273+
margin: {l: 60, r: 20, t: 20, b: 60},
274+
hovermode: 'closest',
275+
annotations: []
276+
}, {
277+
editable: true
278+
})
279+
.then(function() {
280+
gd.data[0].tooltiptemplate = 'Local max: %{z}<br>x: %{x}<br>y: %{y}<br>kernel: %{kernelSizeX} x %{kernelSizeY}';
281+
gd.data[0].tooltipfunction = function(ctx) {
282+
var kernelSizeX = 2;
283+
var kernelSizeY = 2;
284+
var cd0 = ctx.calcdata[0];
285+
var xs = ctx.fullTrace._x;
286+
var ys = ctx.fullTrace._y;
287+
var z = ctx.fullTrace._z || cd0.z;
288+
var halfX = kernelSizeX / 2;
289+
var halfY = kernelSizeY / 2;
290+
var minX = ctx.point.x - halfX;
291+
var maxX = ctx.point.x + halfX;
292+
var minY = ctx.point.y - halfY;
293+
var maxY = ctx.point.y + halfY;
294+
var best = -Infinity;
295+
var bestX = ctx.point.x;
296+
var bestY = ctx.point.y;
297+
298+
if(!xs || !xs.length) {
299+
xs = cd0.xRanges.map(function(range) { return (range[0] + range[1]) / 2; });
300+
}
301+
if(!ys || !ys.length) {
302+
ys = cd0.yRanges.map(function(range) { return (range[0] + range[1]) / 2; });
303+
}
304+
305+
for(var iy = 0; iy < ys.length; iy++) {
306+
var y = ys[iy];
307+
if(y < minY || y > maxY) continue;
308+
309+
for(var ix = 0; ix < xs.length; ix++) {
310+
var x = xs[ix];
311+
if(x < minX || x > maxX) continue;
312+
313+
var value = z[iy][ix];
314+
if(value > best) {
315+
best = value;
316+
bestX = x;
317+
bestY = y;
318+
}
319+
}
320+
}
321+
322+
return {
323+
point: {
324+
x: bestX,
325+
y: bestY,
326+
z: best,
327+
kernelSizeX: kernelSizeX,
328+
kernelSizeY: kernelSizeY
329+
},
330+
annotation: {
331+
x: bestX,
332+
y: bestY
333+
}
334+
};
335+
};
336+
})
337+
.then(done, done.fail);
338+
});
339+
340+
afterAll(function() {
341+
destroyGraphDiv();
342+
});
343+
344+
it('should create a tooltip annotation at the local maximum inside the callback kernel', function(done) {
345+
modeBarButtons.tooltip.click(gd);
346+
setTimeout(function() {
347+
clickDataPoint(gd, 1.5, 1.5);
348+
349+
setTimeout(function() {
350+
expect(gd._fullLayout.annotations.length).toBe(1);
351+
expect(gd._fullLayout.annotations[0].text).toBe('Local max: 5<br>x: 2.5<br>y: 1.5<br>kernel: 2 x 2');
352+
expect(gd._fullLayout.annotations[0].x).toBe(2.5);
353+
expect(gd._fullLayout.annotations[0].y).toBe(1.5);
354+
done();
355+
}, 30);
356+
}, 30);
357+
});
358+
359+
it('should cancel tooltip creation when tooltipfunction returns false', function(done) {
360+
Plotly.relayout(gd, {annotations: []}).then(function() {
361+
gd.data[0].tooltipfunction = function() { return false; };
362+
363+
clickDataPoint(gd, 1.5, 1.5);
364+
365+
setTimeout(function() {
366+
expect(gd._fullLayout.annotations.length).toBe(0);
367+
done();
368+
}, 30);
369+
})
370+
.catch(done.fail);
371+
});
372+
});

0 commit comments

Comments
 (0)