Skip to content

Commit 58966ed

Browse files
committed
annotation hover text
1 parent b4318ad commit 58966ed

File tree

8 files changed

+340
-81
lines changed

8 files changed

+340
-81
lines changed

build/plotcss.js

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ var rules = {
1414
"X svg a:hover": "fill:#3c6dc5;",
1515
"X .main-svg": "position:absolute;top:0;left:0;pointer-events:none;",
1616
"X .main-svg .draglayer": "pointer-events:all;",
17+
"X .cursor-default": "cursor:default;",
1718
"X .cursor-pointer": "cursor:pointer;",
1819
"X .cursor-crosshair": "cursor:crosshair;",
1920
"X .cursor-move": "cursor:move;",

src/components/annotations/annotation_defaults.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
var Lib = require('../../lib');
1313
var Color = require('../color');
1414
var Axes = require('../../plots/cartesian/axes');
15+
var constants = require('../../plots/cartesian/constants');
1516

1617
var attributes = require('./attributes');
1718

@@ -30,7 +31,7 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
3031
if(!(visible || clickToShow)) return annOut;
3132

3233
coerce('opacity');
33-
coerce('bgcolor');
34+
var bgColor = coerce('bgcolor');
3435

3536
var borderColor = coerce('bordercolor'),
3637
borderOpacity = Color.opacity(borderColor);
@@ -108,5 +109,17 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
108109
annOut._yclick = (yClick === undefined) ? annOut.y : yClick;
109110
}
110111

112+
var hoverText = coerce('hovertext');
113+
if(hoverText) {
114+
var hoverBG = coerce('hoverlabel.bgcolor',
115+
Color.opacity(bgColor) ? Color.rgb(bgColor) : Color.defaultLine);
116+
var hoverBorder = coerce('hoverlabel.bordercolor', Color.contrast(hoverBG));
117+
Lib.coerceFont(coerce, 'hoverlabel.font', {
118+
family: constants.HOVERFONT,
119+
size: constants.HOVERFONTSIZE,
120+
color: hoverBorder
121+
});
122+
}
123+
111124
return annOut;
112125
};

src/components/annotations/attributes.js

+35
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,41 @@ module.exports = {
378378
'is `yclick` rather than the annotation\'s `y` value.'
379379
].join(' ')
380380
},
381+
hovertext: {
382+
valType: 'string',
383+
role: 'info',
384+
description: [
385+
'Sets text to appear when hovering over this annotation.',
386+
'If omitted or blank, no hover label will appear.'
387+
].join(' ')
388+
},
389+
hoverlabel: {
390+
bgcolor: {
391+
valType: 'color',
392+
role: 'style',
393+
description: [
394+
'Sets the background color of the hover label.',
395+
'By default uses the annotation\'s `bgcolor` made opaque,',
396+
'or white if it was transparent.'
397+
].join(' ')
398+
},
399+
bordercolor: {
400+
valType: 'color',
401+
role: 'style',
402+
description: [
403+
'Sets the border color of the hover label.',
404+
'By default uses either dark grey or white, for maximum',
405+
'contrast with `hoverlabel.bgcolor`.'
406+
].join(' ')
407+
},
408+
font: extendFlat({}, fontAttrs, {
409+
description: [
410+
'Sets the hover label text font.',
411+
'By default uses the global hover font and size,',
412+
'with color from `hoverlabel.bordercolor`.'
413+
].join(' ')
414+
})
415+
},
381416

382417
_deprecated: {
383418
ref: {

src/components/annotations/draw.js

+38-7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ var Plotly = require('../../plotly');
1515
var Plots = require('../../plots/plots');
1616
var Lib = require('../../lib');
1717
var Axes = require('../../plots/cartesian/axes');
18+
var Fx = require('../../plots/cartesian/graph_interact');
1819
var Color = require('../color');
1920
var Drawing = require('../drawing');
2021
var svgTextUtils = require('../../lib/svg_text_utils');
@@ -96,7 +97,16 @@ function drawOne(gd, index) {
9697
var annGroup = fullLayout._infolayer.append('g')
9798
.classed('annotation', true)
9899
.attr('data-index', String(index))
99-
.style('opacity', options.opacity)
100+
.style('opacity', options.opacity);
101+
102+
// another group for text+background so that they can rotate together
103+
var annTextGroup = annGroup.append('g')
104+
.classed('annotation-text-g', true)
105+
.attr('data-index', String(index));
106+
107+
var annTextGroupInner = annTextGroup.append('g')
108+
.style('pointer-events', 'all')
109+
.call(setCursor, 'default')
100110
.on('click', function() {
101111
gd._dragging = false;
102112
gd.emit('plotly_clickannotation', {
@@ -106,12 +116,33 @@ function drawOne(gd, index) {
106116
});
107117
});
108118

109-
// another group for text+background so that they can rotate together
110-
var annTextGroup = annGroup.append('g')
111-
.classed('annotation-text-g', true)
112-
.attr('data-index', String(index));
113-
114-
var annTextGroupInner = annTextGroup.append('g');
119+
if(options.hovertext) {
120+
annTextGroupInner
121+
.on('mouseover', function() {
122+
var hoverOptions = options.hoverlabel;
123+
var hoverFont = hoverOptions.font;
124+
var bBox = this.getBoundingClientRect();
125+
var bBoxRef = gd.getBoundingClientRect();
126+
127+
Fx.loneHover({
128+
x0: bBox.left - bBoxRef.left,
129+
x1: bBox.right - bBoxRef.left,
130+
y: (bBox.top + bBox.bottom) / 2 - bBoxRef.top,
131+
text: options.hovertext,
132+
color: hoverOptions.bgcolor,
133+
borderColor: hoverOptions.bordercolor,
134+
fontFamily: hoverFont.family,
135+
fontSize: hoverFont.size,
136+
fontColor: hoverFont.color
137+
}, {
138+
container: fullLayout._hoverlayer.node(),
139+
outerContainer: fullLayout._paper.node()
140+
});
141+
})
142+
.on('mouseout', function() {
143+
Fx.loneUnhover(fullLayout._hoverlayer.node());
144+
});
145+
}
115146

116147
var borderwidth = options.borderwidth,
117148
borderpad = options.borderpad,

src/components/color/index.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ color.defaultLine = colorAttrs.defaultLine;
2020
color.lightLine = colorAttrs.lightLine;
2121
color.background = colorAttrs.background;
2222

23+
/*
24+
* tinyRGB: turn a tinycolor into an rgb string, but
25+
* unlike the built-in tinycolor.toRgbString this never includes alpha
26+
*/
2327
color.tinyRGB = function(tc) {
2428
var c = tc.toRgb();
2529
return 'rgb(' + Math.round(c.r) + ', ' +
@@ -57,12 +61,23 @@ color.combine = function(front, back) {
5761
return tinycolor(fcflat).toRgbString();
5862
};
5963

64+
/*
65+
* Create a color that contrasts with cstr.
66+
*
67+
* If cstr is a dark color, we lighten it; if it's light, we darken.
68+
*
69+
* If lightAmount / darkAmount are used, we adjust by these percentages,
70+
* otherwise we go all the way to white or black.
71+
* TODO: black is what we've always done for hover, but should it be #444 instead?
72+
*/
6073
color.contrast = function(cstr, lightAmount, darkAmount) {
6174
var tc = tinycolor(cstr);
6275

63-
var newColor = tc.isLight() ?
64-
tc.darken(darkAmount) :
65-
tc.lighten(lightAmount);
76+
if(tc.getAlpha() !== 1) tc = tinycolor(color.combine(cstr, '#fff'));
77+
78+
var newColor = tc.isDark() ?
79+
(lightAmount ? tc.lighten(lightAmount) : '#fff') :
80+
(darkAmount ? tc.darken(darkAmount) : '#000');
6681

6782
return newColor.toString();
6883
};

src/css/_cursor.scss

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.cursor-default { cursor: default; }
12
.cursor-pointer { cursor: pointer; }
23
.cursor-crosshair { cursor: crosshair; }
34
.cursor-move { cursor: move; }

src/plots/cartesian/graph_interact.js

+54-28
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
'use strict';
1111

1212
var d3 = require('d3');
13-
var tinycolor = require('tinycolor2');
1413
var isNumeric = require('fast-isnumeric');
1514

1615
var Lib = require('../../lib');
@@ -246,9 +245,7 @@ function quadrature(dx, dy) {
246245

247246
// size and display constants for hover text
248247
var HOVERARROWSIZE = constants.HOVERARROWSIZE,
249-
HOVERTEXTPAD = constants.HOVERTEXTPAD,
250-
HOVERFONTSIZE = constants.HOVERFONTSIZE,
251-
HOVERFONT = constants.HOVERFONT;
248+
HOVERTEXTPAD = constants.HOVERTEXTPAD;
252249

253250
// fx.hover: highlight data on hover
254251
// evt can be a mousemove event, or an object with data about what points
@@ -755,23 +752,36 @@ function cleanPoint(d, hovermode) {
755752
return d;
756753
}
757754

755+
/*
756+
* Draw a single hover item in a pre-existing svg container somewhere
757+
* hoverItem should have keys:
758+
* - x and y (or x0, x1, y0, and y1):
759+
* the pixel position to mark, relative to opts.container
760+
* - xLabel, yLabel, zLabel, text, and name:
761+
* info to go in the label
762+
* - color:
763+
* the background color for the label.
764+
* - idealAlign (optional):
765+
* 'left' or 'right' for which side of the x/y box to try to put this on first
766+
* - borderColor (optional):
767+
* color for the border, defaults to strongest contrast with color
768+
* - fontFamily (optional):
769+
* string, the font for this label, defaults to constants.HOVERFONT
770+
* - fontSize (optional):
771+
* the label font size, defaults to constants.HOVERFONTSIZE
772+
* - fontColor (optional):
773+
* defaults to borderColor
774+
* opts should have keys:
775+
* - bgColor:
776+
* the background color this is against, used if the trace is
777+
* non-opaque, and for the name, which goes outside the box
778+
* - container:
779+
* a dom <svg> element - must be big enough to contain the whole
780+
* hover label
781+
* - outerContainer:
782+
* TODO: what exactly is container vs outerContainer?
783+
*/
758784
fx.loneHover = function(hoverItem, opts) {
759-
// draw a single hover item in a pre-existing svg container somewhere
760-
// hoverItem should have keys:
761-
// - x and y (or x0, x1, y0, and y1):
762-
// the pixel position to mark, relative to opts.container
763-
// - xLabel, yLabel, zLabel, text, and name:
764-
// info to go in the label
765-
// - color:
766-
// the background color for the label. text & outline color will
767-
// be chosen black or white to contrast with this
768-
// opts should have keys:
769-
// - bgColor:
770-
// the background color this is against, used if the trace is
771-
// non-opaque, and for the name, which goes outside the box
772-
// - container:
773-
// a dom <svg> element - must be big enough to contain the whole
774-
// hover label
775785
var pointData = {
776786
color: hoverItem.color || Color.defaultLine,
777787
x0: hoverItem.x0 || hoverItem.x || 0,
@@ -785,6 +795,12 @@ fx.loneHover = function(hoverItem, opts) {
785795
name: hoverItem.name,
786796
idealAlign: hoverItem.idealAlign,
787797

798+
// optional extra bits of styling
799+
borderColor: hoverItem.borderColor,
800+
fontFamily: hoverItem.fontFamily,
801+
fontSize: hoverItem.fontSize,
802+
fontColor: hoverItem.fontColor,
803+
788804
// filler to make createHoverText happy
789805
trace: {
790806
index: 0,
@@ -830,6 +846,12 @@ function createHoverText(hoverData, opts) {
830846
container = opts.container,
831847
outerContainer = opts.outerContainer,
832848

849+
// opts.fontFamily/Size are used for the common label
850+
// and as defaults for each hover label, though the individual labels
851+
// can override this.
852+
fontFamily = opts.fontFamily || constants.HOVERFONT,
853+
fontSize = opts.fontSize || constants.HOVERFONTSIZE,
854+
833855
c0 = hoverData[0],
834856
xa = c0.xa,
835857
ya = c0.ya,
@@ -874,7 +896,7 @@ function createHoverText(hoverData, opts) {
874896
lpath.enter().append('path')
875897
.style({fill: Color.defaultLine, 'stroke-width': '1px', stroke: Color.background});
876898
ltext.enter().append('text')
877-
.call(Drawing.font, HOVERFONT, HOVERFONTSIZE, Color.background)
899+
.call(Drawing.font, fontFamily, fontSize, Color.background)
878900
// prohibit tex interpretation until we can handle
879901
// tex and regular text together
880902
.attr('data-notex', 1);
@@ -955,13 +977,12 @@ function createHoverText(hoverData, opts) {
955977
// trace name label (rect and text.name)
956978
g.append('rect')
957979
.call(Color.fill, Color.addOpacity(bgColor, 0.8));
958-
g.append('text').classed('name', true)
959-
.call(Drawing.font, HOVERFONT, HOVERFONTSIZE);
980+
g.append('text').classed('name', true);
960981
// trace data label (path and text.nums)
961982
g.append('path')
962983
.style('stroke-width', '1px');
963984
g.append('text').classed('nums', true)
964-
.call(Drawing.font, HOVERFONT, HOVERFONTSIZE);
985+
.call(Drawing.font, fontFamily, fontSize);
965986
});
966987
hoverLabels.exit().remove();
967988

@@ -977,8 +998,7 @@ function createHoverText(hoverData, opts) {
977998
traceColor = Color.combine(baseColor, bgColor),
978999

9791000
// find a contrasting color for border and text
980-
contrastColor = tinycolor(traceColor).getBrightness() > 128 ?
981-
'#000' : Color.background;
1001+
contrastColor = d.borderColor || Color.contrast(traceColor);
9821002

9831003
// to get custom 'name' labels pass cleanPoint
9841004
if(d.nameOverride !== undefined) d.name = d.nameOverride;
@@ -1023,7 +1043,10 @@ function createHoverText(hoverData, opts) {
10231043

10241044
// main label
10251045
var tx = g.select('text.nums')
1026-
.style('fill', contrastColor)
1046+
.call(Drawing.font,
1047+
d.fontFamily || fontFamily,
1048+
d.fontSize || fontSize,
1049+
d.fontColor || contrastColor)
10271050
.call(Drawing.setPosition, 0, 0)
10281051
.text(text)
10291052
.attr('data-notex', 1)
@@ -1036,7 +1059,10 @@ function createHoverText(hoverData, opts) {
10361059

10371060
// secondary label for non-empty 'name'
10381061
if(name && name !== text) {
1039-
tx2.style('fill', traceColor)
1062+
tx2.call(Drawing.font,
1063+
d.fontFamily || fontFamily,
1064+
d.fontSize || fontSize,
1065+
traceColor)
10401066
.text(name)
10411067
.call(Drawing.setPosition, 0, 0)
10421068
.attr('data-notex', 1)

0 commit comments

Comments
 (0)