Skip to content

annotation hover text #1573

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 11, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build/plotcss.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var rules = {
"X svg a:hover": "fill:#3c6dc5;",
"X .main-svg": "position:absolute;top:0;left:0;pointer-events:none;",
"X .main-svg .draglayer": "pointer-events:all;",
"X .cursor-default": "cursor:default;",
"X .cursor-pointer": "cursor:pointer;",
"X .cursor-crosshair": "cursor:crosshair;",
"X .cursor-move": "cursor:move;",
Expand Down
16 changes: 15 additions & 1 deletion src/components/annotations/annotation_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
var Lib = require('../../lib');
var Color = require('../color');
var Axes = require('../../plots/cartesian/axes');
var constants = require('../../plots/cartesian/constants');

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

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

coerce('opacity');
coerce('bgcolor');
var bgColor = coerce('bgcolor');

var borderColor = coerce('bordercolor'),
borderOpacity = Color.opacity(borderColor);
Expand Down Expand Up @@ -108,5 +109,18 @@ module.exports = function handleAnnotationDefaults(annIn, annOut, fullLayout, op
annOut._yclick = (yClick === undefined) ? annOut.y : yClick;
}

var hoverText = coerce('hovertext');
if(hoverText) {
var hoverBG = coerce('hoverlabel.bgcolor',
Color.opacity(bgColor) ? Color.rgb(bgColor) : Color.defaultLine);
var hoverBorder = coerce('hoverlabel.bordercolor', Color.contrast(hoverBG));
Lib.coerceFont(coerce, 'hoverlabel.font', {
family: constants.HOVERFONT,
size: constants.HOVERFONTSIZE,
color: hoverBorder
});
}
coerce('captureevents', !!hoverText);

return annOut;
};
47 changes: 47 additions & 0 deletions src/components/annotations/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,53 @@ module.exports = {
'is `yclick` rather than the annotation\'s `y` value.'
].join(' ')
},
hovertext: {
valType: 'string',
role: 'info',
description: [
'Sets text to appear when hovering over this annotation.',
'If omitted or blank, no hover label will appear.'
].join(' ')
},
hoverlabel: {
bgcolor: {
valType: 'color',
role: 'style',
description: [
'Sets the background color of the hover label.',
'By default uses the annotation\'s `bgcolor` made opaque,',
'or white if it was transparent.'
].join(' ')
},
bordercolor: {
valType: 'color',
role: 'style',
description: [
'Sets the border color of the hover label.',
'By default uses either dark grey or white, for maximum',
'contrast with `hoverlabel.bgcolor`.'
].join(' ')
},
font: extendFlat({}, fontAttrs, {
description: [
'Sets the hover label text font.',
'By default uses the global hover font and size,',
'with color from `hoverlabel.bordercolor`.'
].join(' ')
})
},
captureevents: {
valType: 'boolean',
role: 'info',
description: [
'Determines whether the annotation text box captures mouse move',
'and click events, or allows those events to pass through to data',
'points in the plot that may be behind the annotation. By default',
'`captureevents` is *false* unless `hovertext` is provided.',
'If you use the event `plotly_clickannotation` without `hovertext`',
'you must explicitly enable `captureevents`.'
].join(' ')
},

_deprecated: {
ref: {
Expand Down
45 changes: 38 additions & 7 deletions src/components/annotations/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var Plotly = require('../../plotly');
var Plots = require('../../plots/plots');
var Lib = require('../../lib');
var Axes = require('../../plots/cartesian/axes');
var Fx = require('../../plots/cartesian/graph_interact');
var Color = require('../color');
var Drawing = require('../drawing');
var svgTextUtils = require('../../lib/svg_text_utils');
Expand Down Expand Up @@ -96,7 +97,16 @@ function drawOne(gd, index) {
var annGroup = fullLayout._infolayer.append('g')
.classed('annotation', true)
.attr('data-index', String(index))
.style('opacity', options.opacity)
.style('opacity', options.opacity);

// another group for text+background so that they can rotate together
var annTextGroup = annGroup.append('g')
.classed('annotation-text-g', true)
.attr('data-index', String(index));

var annTextGroupInner = annTextGroup.append('g')
.style('pointer-events', options.captureevents ? 'all' : null)
.call(setCursor, 'default')
.on('click', function() {
gd._dragging = false;
gd.emit('plotly_clickannotation', {
Expand All @@ -106,12 +116,33 @@ function drawOne(gd, index) {
});
});

// another group for text+background so that they can rotate together
var annTextGroup = annGroup.append('g')
.classed('annotation-text-g', true)
.attr('data-index', String(index));

var annTextGroupInner = annTextGroup.append('g');
if(options.hovertext) {
annTextGroupInner
.on('mouseover', function() {
var hoverOptions = options.hoverlabel;
var hoverFont = hoverOptions.font;
var bBox = this.getBoundingClientRect();
var bBoxRef = gd.getBoundingClientRect();

Fx.loneHover({
Copy link
Contributor

@etpinard etpinard Apr 10, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

peek 2017-04-10 12-07

so this means the annotation hover takes precedence over trace hover. I don't think this a big deal. But perhaps we could make annotation hover pass by Fx.hover so that they don't conflict sort of speak.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a new attribute captureevents in da1e9d7

This one defaults to false (so data point hover comes through) unless hovertext is provided. But as noted in the description, if you use plotly_clickannotation events you must explicitly turn this on.

@cldougl once this is merged we should update the (currently broken) example in https://plot.ly/javascript/text-and-annotations/#styling-and-formatting-annotations, and add a section about plotly_clickannotation in https://plot.ly/javascript/plotlyjs-events/ that specifically mentions captureevents.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it-- just made an issue to keep track.
@alexcjohnson was there something specifically broken in https://plot.ly/javascript/text-and-annotations/#styling-and-formatting-annotations (just took a quick look and it seemed to work as expected) or should it just be updated to use plotly_clickannotation?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @cldougl - I moved further discussion of this into the docs issue you made.

x0: bBox.left - bBoxRef.left,
x1: bBox.right - bBoxRef.left,
y: (bBox.top + bBox.bottom) / 2 - bBoxRef.top,
text: options.hovertext,
color: hoverOptions.bgcolor,
borderColor: hoverOptions.bordercolor,
fontFamily: hoverFont.family,
fontSize: hoverFont.size,
fontColor: hoverFont.color
}, {
container: fullLayout._hoverlayer.node(),
outerContainer: fullLayout._paper.node()
});
})
.on('mouseout', function() {
Fx.loneUnhover(fullLayout._hoverlayer.node());
});
}

var borderwidth = options.borderwidth,
borderpad = options.borderpad,
Expand Down
26 changes: 20 additions & 6 deletions src/components/color/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ var color = module.exports = {};

var colorAttrs = require('./attributes');
color.defaults = colorAttrs.defaults;
color.defaultLine = colorAttrs.defaultLine;
var defaultLine = color.defaultLine = colorAttrs.defaultLine;
color.lightLine = colorAttrs.lightLine;
color.background = colorAttrs.background;
var background = color.background = colorAttrs.background;

/*
* tinyRGB: turn a tinycolor into an rgb string, but
* unlike the built-in tinycolor.toRgbString this never includes alpha
*/
color.tinyRGB = function(tc) {
var c = tc.toRgb();
return 'rgb(' + Math.round(c.r) + ', ' +
Expand All @@ -43,7 +47,7 @@ color.combine = function(front, back) {
var fc = tinycolor(front).toRgb();
if(fc.a === 1) return tinycolor(front).toRgbString();

var bc = tinycolor(back || color.background).toRgb(),
var bc = tinycolor(back || background).toRgb(),
bcflat = bc.a === 1 ? bc : {
r: 255 * (1 - bc.a) + bc.r * bc.a,
g: 255 * (1 - bc.a) + bc.g * bc.a,
Expand All @@ -57,12 +61,22 @@ color.combine = function(front, back) {
return tinycolor(fcflat).toRgbString();
};

/*
* Create a color that contrasts with cstr.
*
* If cstr is a dark color, we lighten it; if it's light, we darken.
*
* If lightAmount / darkAmount are used, we adjust by these percentages,
* otherwise we go all the way to white or black.
*/
color.contrast = function(cstr, lightAmount, darkAmount) {
var tc = tinycolor(cstr);

var newColor = tc.isLight() ?
tc.darken(darkAmount) :
tc.lighten(lightAmount);
if(tc.getAlpha() !== 1) tc = tinycolor(color.combine(cstr, background));

var newColor = tc.isDark() ?
(lightAmount ? tc.lighten(lightAmount) : background) :
(darkAmount ? tc.darken(darkAmount) : defaultLine);

return newColor.toString();
};
Expand Down
1 change: 1 addition & 0 deletions src/css/_cursor.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.cursor-default { cursor: default; }
.cursor-pointer { cursor: pointer; }
.cursor-crosshair { cursor: crosshair; }
.cursor-move { cursor: move; }
Expand Down
82 changes: 54 additions & 28 deletions src/plots/cartesian/graph_interact.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
'use strict';

var d3 = require('d3');
var tinycolor = require('tinycolor2');
var isNumeric = require('fast-isnumeric');

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

// size and display constants for hover text
var HOVERARROWSIZE = constants.HOVERARROWSIZE,
HOVERTEXTPAD = constants.HOVERTEXTPAD,
HOVERFONTSIZE = constants.HOVERFONTSIZE,
HOVERFONT = constants.HOVERFONT;
HOVERTEXTPAD = constants.HOVERTEXTPAD;

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

/*
* Draw a single hover item in a pre-existing svg container somewhere
* hoverItem should have keys:
* - x and y (or x0, x1, y0, and y1):
* the pixel position to mark, relative to opts.container
* - xLabel, yLabel, zLabel, text, and name:
* info to go in the label
* - color:
* the background color for the label.
* - idealAlign (optional):
* 'left' or 'right' for which side of the x/y box to try to put this on first
* - borderColor (optional):
* color for the border, defaults to strongest contrast with color
* - fontFamily (optional):
* string, the font for this label, defaults to constants.HOVERFONT
* - fontSize (optional):
* the label font size, defaults to constants.HOVERFONTSIZE
* - fontColor (optional):
* defaults to borderColor
* opts should have keys:
* - bgColor:
* the background color this is against, used if the trace is
* non-opaque, and for the name, which goes outside the box
* - container:
* a <svg> or <g> element to add the hover label to
* - outerContainer:
* normally a parent of `container`, sets the bounding box to use to
* constrain the hover label and determine whether to show it on the left or right
*/
fx.loneHover = function(hoverItem, opts) {
// draw a single hover item in a pre-existing svg container somewhere
// hoverItem should have keys:
// - x and y (or x0, x1, y0, and y1):
// the pixel position to mark, relative to opts.container
// - xLabel, yLabel, zLabel, text, and name:
// info to go in the label
// - color:
// the background color for the label. text & outline color will
// be chosen black or white to contrast with this
// opts should have keys:
// - bgColor:
// the background color this is against, used if the trace is
// non-opaque, and for the name, which goes outside the box
// - container:
// a dom <svg> element - must be big enough to contain the whole
// hover label
var pointData = {
color: hoverItem.color || Color.defaultLine,
x0: hoverItem.x0 || hoverItem.x || 0,
Expand All @@ -785,6 +795,12 @@ fx.loneHover = function(hoverItem, opts) {
name: hoverItem.name,
idealAlign: hoverItem.idealAlign,

// optional extra bits of styling
borderColor: hoverItem.borderColor,
fontFamily: hoverItem.fontFamily,
fontSize: hoverItem.fontSize,
fontColor: hoverItem.fontColor,

// filler to make createHoverText happy
trace: {
index: 0,
Expand Down Expand Up @@ -830,6 +846,12 @@ function createHoverText(hoverData, opts) {
container = opts.container,
outerContainer = opts.outerContainer,

// opts.fontFamily/Size are used for the common label
// and as defaults for each hover label, though the individual labels
// can override this.
fontFamily = opts.fontFamily || constants.HOVERFONT,
fontSize = opts.fontSize || constants.HOVERFONTSIZE,

c0 = hoverData[0],
xa = c0.xa,
ya = c0.ya,
Expand Down Expand Up @@ -874,7 +896,7 @@ function createHoverText(hoverData, opts) {
lpath.enter().append('path')
.style({fill: Color.defaultLine, 'stroke-width': '1px', stroke: Color.background});
ltext.enter().append('text')
.call(Drawing.font, HOVERFONT, HOVERFONTSIZE, Color.background)
.call(Drawing.font, fontFamily, fontSize, Color.background)
// prohibit tex interpretation until we can handle
// tex and regular text together
.attr('data-notex', 1);
Expand Down Expand Up @@ -955,13 +977,12 @@ function createHoverText(hoverData, opts) {
// trace name label (rect and text.name)
g.append('rect')
.call(Color.fill, Color.addOpacity(bgColor, 0.8));
g.append('text').classed('name', true)
.call(Drawing.font, HOVERFONT, HOVERFONTSIZE);
g.append('text').classed('name', true);
// trace data label (path and text.nums)
g.append('path')
.style('stroke-width', '1px');
g.append('text').classed('nums', true)
.call(Drawing.font, HOVERFONT, HOVERFONTSIZE);
.call(Drawing.font, fontFamily, fontSize);
});
hoverLabels.exit().remove();

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

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

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

// main label
var tx = g.select('text.nums')
.style('fill', contrastColor)
.call(Drawing.font,
d.fontFamily || fontFamily,
d.fontSize || fontSize,
d.fontColor || contrastColor)
.call(Drawing.setPosition, 0, 0)
.text(text)
.attr('data-notex', 1)
Expand All @@ -1036,7 +1059,10 @@ function createHoverText(hoverData, opts) {

// secondary label for non-empty 'name'
if(name && name !== text) {
tx2.style('fill', traceColor)
tx2.call(Drawing.font,
d.fontFamily || fontFamily,
d.fontSize || fontSize,
traceColor)
.text(name)
.call(Drawing.setPosition, 0, 0)
.attr('data-notex', 1)
Expand Down
Loading