From 48efa708fd8e371dbae2f831cbba2bdb40302aa7 Mon Sep 17 00:00:00 2001 From: Lexachoc <20377719+Lexachoc@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:41:36 +0200 Subject: [PATCH 1/5] add style customization to hoverlabel --- src/components/fx/attributes.js | 18 + src/components/fx/hover.js | 607 ++++++-- src/components/fx/hoverlabel_defaults.js | 10 +- src/components/fx/layout_attributes.js | 52 +- src/types/generated/schema.d.ts | 108 ++ test/image/mocks/line_scatter.json | 16 +- test/jasmine/tests/fx_test.js | 18 + test/jasmine/tests/hover_test.js | 165 +++ test/plot-schema.json | 1665 ++++++++++++++++++++++ 9 files changed, 2533 insertions(+), 126 deletions(-) diff --git a/src/components/fx/attributes.js b/src/components/fx/attributes.js index dc5f867b3a6..21ad2b84543 100644 --- a/src/components/fx/attributes.js +++ b/src/components/fx/attributes.js @@ -14,6 +14,24 @@ module.exports = { arrayOk: true, description: 'Sets the border color of the hover labels for this trace.' }), + borderwidth: extendFlat({}, hoverLabelAttrs.borderwidth, { + description: 'Sets the border width (in px) of the hover labels for this trace.' + }), + borderradius: extendFlat({}, hoverLabelAttrs.borderradius, { + description: 'Sets the border radius (in px) of the hover labels for this trace.' + }), + borderpad: extendFlat({}, hoverLabelAttrs.borderpad, { + description: 'Sets the padding (in px) between the text and the border of the hover labels for this trace.' + }), + namecolor: extendFlat({}, hoverLabelAttrs.namecolor, { + description: 'Sets the text color of the secondary hover label for this trace.' + }), + bgnamecolor: extendFlat({}, hoverLabelAttrs.bgnamecolor, { + description: 'Sets the background color of the secondary hover label for this trace.' + }), + shadow: extendFlat({}, hoverLabelAttrs.shadow, { + description: 'Sets the shadow of the hover labels for this trace.' + }), font: fontAttrs({ arrayOk: true, editType: 'none', diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 47c7749c49a..5e223f5a709 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -104,6 +104,20 @@ exports.hover = function hover(gd, evt, subplot, noHoverEvent) { * '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 + * - borderWidth (optional): + * width of the border in pixels + * - borderRadius (optional): + * radius of the hover label corners in pixels + * - borderPad (optional): + * padding between the label text and border in pixels + * - nameColor (optional): + * color for the secondary label text + * - bgNameColor (optional): + * background color for the secondary label + * - shadow (optional): + * CSS drop-shadow parameters for the hover label + * - showArrow (optional): + * whether to draw the arrow pointing to the data * - fontFamily (optional): * string, the font for this label, defaults to constants.HOVERFONT * - fontSize (optional): @@ -193,6 +207,13 @@ exports.loneHover = function loneHover(hoverItems, opts) { // optional extra bits of styling borderColor: hoverItem.borderColor, + borderWidth: hoverItem.borderWidth, + borderRadius: hoverItem.borderRadius, + borderPad: hoverItem.borderPad, + nameColor: hoverItem.nameColor, + bgNameColor: hoverItem.bgNameColor, + shadow: hoverItem.shadow, + showArrow: hoverItem.showArrow, fontFamily: hoverItem.fontFamily, fontSize: hoverItem.fontSize, fontColor: hoverItem.fontColor, @@ -1056,6 +1077,205 @@ function hoverDataKey(d) { var EXTRA_STRING_REGEX = /([\s\S]*)<\/extra>/; +function applyHoverShadow(selection, shadow) { + var filter = shadow && shadow !== 'none' ? 'drop-shadow(' + shadow + ')' : null; + selection.style('filter', filter); +} + +function roundedRectPath(x0, y0, x1, y1, radius, roundLeft, roundRight, pX, pY) { + var left = Math.min(x0, x1); + var right = Math.max(x0, x1); + var top = Math.min(y0, y1); + var bottom = Math.max(y0, y1); + + var r = Math.max(0, Math.min(radius, (right - left) / 2, (bottom - top) / 2)); + var leftRadius = roundLeft ? r : 0; + var rightRadius = roundRight ? r : 0; + + var path = 'M' + pX(left + leftRadius) + ',' + pY(top); + path += 'H' + pX(right - rightRadius); + + if(rightRadius) { + path += 'Q' + pX(right) + ',' + pY(top) + ' ' + pX(right) + ',' + pY(top + rightRadius); + } + + path += 'V' + pY(bottom - rightRadius); + + if(rightRadius) { + path += 'Q' + pX(right) + ',' + pY(bottom) + ' ' + pX(right - rightRadius) + ',' + pY(bottom); + } + + path += 'H' + pX(left + leftRadius); + + if(leftRadius) { + path += 'Q' + pX(left) + ',' + pY(bottom) + ' ' + pX(left) + ',' + pY(bottom - leftRadius); + } + + path += 'V' + pY(top + leftRadius); + + if(leftRadius) { + path += 'Q' + pX(left) + ',' + pY(top) + ' ' + pX(left + leftRadius) + ',' + pY(top); + } + + return path + 'Z'; +} + +function roundedSideArrowPath(width, height, horzSign, offsetX, offsetY, radius, roundOppositeSide, pX, pY) { + var nearX = horzSign * HOVERARROWSIZE + offsetX; + var top = offsetY - height / 2; + var bottom = offsetY + height / 2; + + // clamp the corners next to the arrow so they do not overlap its base + var arrowSideRadius = Math.max( + 0, + Math.min(radius, width / 2, height / 2 - HOVERARROWSIZE) + ); + var oppositeSideRadius = roundOppositeSide ? Math.max(0, Math.min(radius, width / 2, height / 2)) : 0; + + function x(u) { + return pX(nearX + horzSign * u); + } + + var path = 'M0,0L' + x(0) + ',' + pY(offsetY + HOVERARROWSIZE); + path += 'V' + pY(bottom - arrowSideRadius); + + if(arrowSideRadius) { + path += + 'Q' + + x(0) + + ',' + + pY(bottom) + + ' ' + + x(arrowSideRadius) + + ',' + + pY(bottom); + } + + path += 'H' + x(width - oppositeSideRadius); + + if(oppositeSideRadius) { + path += + 'Q' + + x(width) + + ',' + + pY(bottom) + + ' ' + + x(width) + + ',' + + pY(bottom - oppositeSideRadius); + } + + path += 'V' + pY(top + oppositeSideRadius); + + if(oppositeSideRadius) { + path += + 'Q' + + x(width) + + ',' + + pY(top) + + ' ' + + x(width - oppositeSideRadius) + + ',' + + pY(top); + } + + path += 'H' + x(arrowSideRadius); + + if(arrowSideRadius) { + path += + 'Q' + + x(0) + + ',' + + pY(top) + + ' ' + + x(0) + + ',' + + pY(top + arrowSideRadius); + } + + path += 'V' + pY(offsetY - HOVERARROWSIZE) + 'Z'; + return path; +} + +function roundedTopBottomArrowPath(tipX, width, height, vertSign, radius) { + var left = -width / 2; + var right = width / 2; + var nearY = vertSign * HOVERARROWSIZE; + var farY = vertSign * (HOVERARROWSIZE + height); + + var maxRadius = Math.max(0, Math.min(radius, width / 2, height / 2)); + var nearLeftRadius = Math.min( + maxRadius, + Math.max(0, tipX - HOVERARROWSIZE - left) + ); + var nearRightRadius = Math.min( + maxRadius, + Math.max(0, right - tipX - HOVERARROWSIZE) + ); + + var path = 'M' + tipX + ',0'; + path += 'L' + (tipX + HOVERARROWSIZE) + ',' + nearY; + path += 'H' + (right - nearRightRadius); + + if(nearRightRadius) { + path += + 'Q' + + right + + ',' + + nearY + + ' ' + + right + + ',' + + (nearY + vertSign * nearRightRadius); + } + + path += 'V' + (farY - vertSign * maxRadius); + + if(maxRadius) { + path += + 'Q' + + right + + ',' + + farY + + ' ' + + (right - maxRadius) + + ',' + + farY; + } + + path += 'H' + (left + maxRadius); + + if(maxRadius) { + path += + 'Q' + + left + + ',' + + farY + + ' ' + + left + + ',' + + (farY - vertSign * maxRadius); + } + + path += 'V' + (nearY + vertSign * nearLeftRadius); + + if(nearLeftRadius) { + path += + 'Q' + + left + + ',' + + nearY + + ' ' + + (left + nearLeftRadius) + + ',' + + nearY; + } + + path += 'H' + (tipX - HOVERARROWSIZE) + 'Z'; + return path; +} + + function createHoverText(hoverData, opts) { var gd = opts.gd; var fullLayout = gd._fullLayout; @@ -1152,6 +1372,10 @@ function createHoverText(hoverData, opts) { var commonBgColor = commonLabelOpts.bgcolor || Color.defaultLine; var commonStroke = commonLabelOpts.bordercolor || Color.contrast(commonBgColor); + var commonBorderWidth = commonLabelOpts.borderwidth === undefined ? 1 : commonLabelOpts.borderwidth; + var commonBorderRadius = commonLabelOpts.borderradius || 0; + var commonBorderPad = commonLabelOpts.borderpad ?? HOVERTEXTPAD; + var commonShadow = commonLabelOpts.shadow; var contrastColor = Color.contrast(commonBgColor); var commonLabelOptsFont = commonLabelOpts.font; var commonLabelFont = { @@ -1168,7 +1392,8 @@ function createHoverText(hoverData, opts) { lpath.style({ fill: commonBgColor, - stroke: commonStroke + stroke: commonStroke, + 'stroke-width': commonBorderWidth + 'px' }); ltext @@ -1191,14 +1416,14 @@ function createHoverText(hoverData, opts) { svgTextUtils.positionText, 0, xa.side === 'top' - ? outerTop - tbb.bottom - HOVERARROWSIZE - HOVERTEXTPAD - : outerTop - tbb.top + HOVERARROWSIZE + HOVERTEXTPAD + ? outerTop - tbb.bottom - HOVERARROWSIZE - commonBorderPad + : outerTop - tbb.top + HOVERARROWSIZE + commonBorderPad ); lx = xa._offset + (c0.x0 + c0.x1) / 2; ly = ya._offset + (xa.side === 'top' ? 0 : ya._length); - var halfWidth = tbb.width / 2 + HOVERTEXTPAD; + var halfWidth = tbb.width / 2 + commonBorderPad; var tooltipMidX = lx; if (lx < halfWidth) { @@ -1207,29 +1432,40 @@ function createHoverText(hoverData, opts) { tooltipMidX = fullLayout.width - halfWidth; } + var commonHeight = commonBorderPad * 2 + tbb.height; + var commonTipX = lx - tooltipMidX; + lpath.attr( 'd', - 'M' + - (lx - tooltipMidX) + - ',0' + - 'L' + - (lx - tooltipMidX + HOVERARROWSIZE) + - ',' + - topsign + - HOVERARROWSIZE + - 'H' + - halfWidth + - 'v' + - topsign + - (HOVERTEXTPAD * 2 + tbb.height) + - 'H' + - -halfWidth + - 'V' + - topsign + - HOVERARROWSIZE + - 'H' + - (lx - tooltipMidX - HOVERARROWSIZE) + - 'Z' + commonBorderRadius + ? roundedTopBottomArrowPath( + commonTipX, + halfWidth * 2, + commonHeight, + xa.side === 'top' ? -1 : 1, + commonBorderRadius + ) + : 'M' + + commonTipX + + ',0' + + 'L' + + (commonTipX + HOVERARROWSIZE) + + ',' + + topsign + + HOVERARROWSIZE + + 'H' + + halfWidth + + 'v' + + topsign + + commonHeight + + 'H' + + -halfWidth + + 'V' + + topsign + + HOVERARROWSIZE + + 'H' + + (commonTipX - HOVERARROWSIZE) + + 'Z' ); lx = tooltipMidX; @@ -1237,11 +1473,11 @@ function createHoverText(hoverData, opts) { commonLabelRect.maxX = lx + halfWidth; if (xa.side === 'top') { // label on negative y side - commonLabelRect.minY = ly - (HOVERTEXTPAD * 2 + tbb.height); - commonLabelRect.maxY = ly - HOVERTEXTPAD; + commonLabelRect.minY = ly - (commonBorderPad * 2 + tbb.height); + commonLabelRect.maxY = ly - commonBorderPad; } else { - commonLabelRect.minY = ly + HOVERTEXTPAD; - commonLabelRect.maxY = ly + (HOVERTEXTPAD * 2 + tbb.height); + commonLabelRect.minY = ly + commonBorderPad; + commonLabelRect.maxY = ly + (commonBorderPad * 2 + tbb.height); } } else { var anchor; @@ -1263,37 +1499,52 @@ function createHoverText(hoverData, opts) { ltext.attr('text-anchor', anchor); + var commonWidth = commonBorderPad * 2 + tbb.width; + var commonBoxHeight = commonBorderPad * 2 + tbb.height; + lpath.attr( 'd', - 'M0,0' + - 'L' + - leftsign + - HOVERARROWSIZE + - ',' + - HOVERARROWSIZE + - 'V' + - (HOVERTEXTPAD + tbb.height / 2) + - 'h' + - leftsign + - (HOVERTEXTPAD * 2 + tbb.width) + - 'V-' + - (HOVERTEXTPAD + tbb.height / 2) + - 'H' + - leftsign + - HOVERARROWSIZE + - 'V-' + - HOVERARROWSIZE + - 'Z' + commonBorderRadius + ? roundedSideArrowPath( + commonWidth, + commonBoxHeight, + sgn, + 0, + 0, + commonBorderRadius, + true, + Lib.identity, + Lib.identity + ) + : 'M0,0' + + 'L' + + leftsign + + HOVERARROWSIZE + + ',' + + HOVERARROWSIZE + + 'V' + + (commonBorderPad + tbb.height / 2) + + 'h' + + leftsign + + commonWidth + + 'V-' + + (commonBorderPad + tbb.height / 2) + + 'H' + + leftsign + + HOVERARROWSIZE + + 'V-' + + HOVERARROWSIZE + + 'Z' ); - commonLabelRect.minY = ly - (HOVERTEXTPAD + tbb.height / 2); - commonLabelRect.maxY = ly + (HOVERTEXTPAD + tbb.height / 2); + commonLabelRect.minY = ly - (commonBorderPad + tbb.height / 2); + commonLabelRect.maxY = ly + (commonBorderPad + tbb.height / 2); if (ya.side === 'right') { commonLabelRect.minX = lx + HOVERARROWSIZE; - commonLabelRect.maxX = lx + HOVERARROWSIZE + (HOVERTEXTPAD * 2 + tbb.width); + commonLabelRect.maxX = lx + HOVERARROWSIZE + (commonBorderPad * 2 + tbb.width); } else { // label on negative x side - commonLabelRect.minX = lx - HOVERARROWSIZE - (HOVERTEXTPAD * 2 + tbb.width); + commonLabelRect.minX = lx - HOVERARROWSIZE - (commonBorderPad * 2 + tbb.width); commonLabelRect.maxX = lx - HOVERARROWSIZE; } @@ -1302,21 +1553,22 @@ function createHoverText(hoverData, opts) { var clipId = 'clip' + fullLayout._uid + 'commonlabel' + ya._id; var clipPath; - if (lx < tbb.width + 2 * HOVERTEXTPAD + HOVERARROWSIZE) { + if (lx < tbb.width + 2 * commonBorderPad + HOVERARROWSIZE) { + var clippedTextWidth = Math.max(0, tbb.width - commonBorderPad); clipPath = 'M-' + - (HOVERARROWSIZE + HOVERTEXTPAD) + + (HOVERARROWSIZE + commonBorderPad) + '-' + halfHeight + 'h-' + - (tbb.width - HOVERTEXTPAD) + + clippedTextWidth + 'V' + halfHeight + 'h' + - (tbb.width - HOVERTEXTPAD) + + clippedTextWidth + 'Z'; - var ltx = tbb.width - lx + HOVERTEXTPAD; + var ltx = tbb.width - lx + commonBorderPad; svgTextUtils.positionText(ltext, ltx, lty); // shift each line (except the longest) so that start-of-line @@ -1333,7 +1585,7 @@ function createHoverText(hoverData, opts) { }); } } else { - svgTextUtils.positionText(ltext, sgn * (HOVERTEXTPAD + HOVERARROWSIZE), lty); + svgTextUtils.positionText(ltext, sgn * (commonBorderPad + HOVERARROWSIZE), lty); clipPath = null; } @@ -1345,6 +1597,7 @@ function createHoverText(hoverData, opts) { } label.attr('transform', strTranslate(lx, ly)); + applyHoverShadow(label, commonShadow); }); // Show a single hover label @@ -1358,6 +1611,7 @@ function createHoverText(hoverData, opts) { // mock legend var hoverlabel = fullLayout.hoverlabel; var font = hoverlabel.font; + var unifiedBorderPad = hoverlabel.borderpad ?? HOVERTEXTPAD; var item0 = groupedHoverData[0]; @@ -1380,7 +1634,7 @@ function createHoverText(hoverData, opts) { font: font, bgcolor: hoverlabel.bgcolor, bordercolor: hoverlabel.bordercolor, - borderwidth: 1, + borderwidth: hoverlabel.borderwidth, tracegroupgap: 7, traceorder: fullLayout.legend ? fullLayout.legend.traceorder : undefined, orientation: 'v' @@ -1446,6 +1700,24 @@ function createHoverText(hoverData, opts) { // Position the hover var legendContainer = container.select('g.legend'); + var unifiedBg = legendContainer.select('rect.bg'); + // unified hover reuses the legend renderer, so adjust its background + // relative to the hardcoded hover text padding without changing item layout + var borderPadDelta = unifiedBorderPad - HOVERTEXTPAD; + if(!unifiedBg.empty() && borderPadDelta) { + var bgX = Number(unifiedBg.attr('x')) || 0; + var bgY = Number(unifiedBg.attr('y')) || 0; + var bgWidth = Number(unifiedBg.attr('width')) || 0; + var bgHeight = Number(unifiedBg.attr('height')) || 0; + + unifiedBg.attr({ + x: bgX - borderPadDelta, + y: bgY - borderPadDelta, + width: Math.max(0, bgWidth + 2 * borderPadDelta), + height: Math.max(0, bgHeight + 2 * borderPadDelta) + }); + } + var tbb = getBoundingClientRect(gd, legendContainer.node()); var tWidth = tbb.width + 2 * HOVERTEXTPAD; var tHeight = tbb.height + 2 * HOVERTEXTPAD; @@ -1559,6 +1831,14 @@ function createHoverText(hoverData, opts) { ly += HOVERTEXTPAD; legendContainer.attr('transform', strTranslate(lx - 1, ly - 1)); + + if(!unifiedBg.empty()) { + unifiedBg + .attr('rx', hoverlabel.borderradius || 0) + .attr('ry', hoverlabel.borderradius || 0); + } + applyHoverShadow(legendContainer, hoverlabel.shadow); + return legendContainer; } @@ -1609,9 +1889,19 @@ function createHoverText(hoverData, opts) { // color for 'nums' part of the label var numsColor = Color.combine(Color.opacity(color0) ? color0 : Color.defaultLine, bgColor); // color for 'name' part of the label - var nameColor = Color.combine(Color.opacity(dColor) ? dColor : Color.defaultLine, bgColor); + var fallbackNameColor = Color.combine(Color.opacity(dColor) ? dColor : Color.defaultLine, bgColor); + var hoverlabel = d.trace?.hoverlabel; + var borderPad = d.borderPad ?? hoverlabel?.borderpad ?? HOVERTEXTPAD; + var bgNameColor = d.bgNameColor ?? hoverlabel?.bgnamecolor; + var nameColor = d.nameColor ?? hoverlabel?.namecolor; + var nameBg = bgNameColor || Color.addOpacity(bgColor, 0.8); + if(nameColor === undefined) { + nameColor = bgNameColor ? Color.contrast(nameBg) : fallbackNameColor; + } + // find a contrasting color for border and text var contrastColor = d.borderColor || Color.contrast(numsColor); + var borderWidth = d.borderWidth ?? hoverlabel?.borderwidth ?? 1; var texts = getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g); var text = texts[0]; @@ -1642,6 +1932,8 @@ function createHoverText(hoverData, opts) { // secondary label for non-empty 'name' if (name && name !== text) { + g.select('rect').call(Color.fill, nameBg); + tx2.call(Drawing.font, { family: d.fontFamily || fontFamily, size: d.fontSize || fontSize, @@ -1659,8 +1951,8 @@ function createHoverText(hoverData, opts) { .call(svgTextUtils.convertToTspans, gd); var t2bb = getBoundingClientRect(gd, tx2.node()); - tx2width = t2bb.width + 2 * HOVERTEXTPAD; - tx2height = t2bb.height + 2 * HOVERTEXTPAD; + tx2width = t2bb.width + 2 * borderPad; + tx2height = t2bb.height + 2 * borderPad; } else { tx2.remove(); g.select('rect').remove(); @@ -1668,7 +1960,8 @@ function createHoverText(hoverData, opts) { g.select('path').style({ fill: numsColor, - stroke: contrastColor + stroke: contrastColor, + 'stroke-width': borderWidth + 'px' }); var htx = d.xa._offset + (d.x0 + d.x1) / 2; @@ -1681,14 +1974,15 @@ function createHoverText(hoverData, opts) { var tbbHeight = tbb.height / fullLayout._invScaleY; d.ty0 = (outerTop - tbb.top) / fullLayout._invScaleY; - d.bx = tbbWidth + 2 * HOVERTEXTPAD; - d.by = Math.max(tbbHeight + 2 * HOVERTEXTPAD, tx2height); + d.bx = tbbWidth + 2 * borderPad; + d.by = Math.max(tbbHeight + 2 * borderPad, tx2height); d.anchor = 'start'; d.txwidth = tbbWidth; d.tx2width = tx2width; + d.borderPad = borderPad; d.offset = 0; - var txTotalWidth = (tbbWidth + HOVERARROWSIZE + HOVERTEXTPAD + tx2width) * fullLayout._invScaleX; + var txTotalWidth = (tbbWidth + HOVERARROWSIZE + borderPad + tx2width) * fullLayout._invScaleX; var anchorStartOK, anchorEndOK; if (rotateLabels) { @@ -2097,14 +2391,15 @@ function getHoverLabelOffsets(hoverLabel, rotateLabels) { * Calculate the shift in x for text and text2 elements */ function getTextShiftX(hoverLabel) { + var borderPad = hoverLabel.borderPad ?? HOVERTEXTPAD; var alignShift = { start: 1, end: -1, middle: 0 }[hoverLabel.anchor]; - var textShiftX = alignShift * (HOVERARROWSIZE + HOVERTEXTPAD); - var text2ShiftX = textShiftX + alignShift * (hoverLabel.txwidth + HOVERTEXTPAD); + var textShiftX = alignShift * (HOVERARROWSIZE + borderPad); + var text2ShiftX = textShiftX + alignShift * (hoverLabel.txwidth + borderPad); var isMiddle = hoverLabel.anchor === 'middle'; if (isMiddle) { textShiftX -= hoverLabel.tx2width / 2; - text2ShiftX += hoverLabel.txwidth / 2 + HOVERTEXTPAD; + text2ShiftX += hoverLabel.txwidth / 2 + borderPad; } return { @@ -2139,69 +2434,116 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) { var isMiddle = anchor === 'middle'; // Get 'showarrow' attribute value from trace hoverlabel settings; // if trace has no hoverlabel settings, we should show the arrow by default - var showArrow = 'hoverlabel' in d.trace ? d.trace.hoverlabel.showarrow : true; + var hoverlabel = d.trace?.hoverlabel; + var showArrow = d.showArrow ?? hoverlabel?.showarrow ?? true; + var borderRadius = d.borderRadius ?? hoverlabel?.borderradius ?? 0; + var borderPad = d.borderPad ?? hoverlabel?.borderpad ?? HOVERTEXTPAD; + var shadow = d.shadow ?? hoverlabel?.shadow ?? 'none'; + var hasSecondaryLabel = !!d.tx2width; var pathStr; - if (isMiddle) { - // middle aligned: rect centered on data - pathStr = - 'M-' + - pX(d.bx / 2 + d.tx2width / 2) + - ',' + - pY(offsetY - d.by / 2) + - 'h' + - pX(d.bx) + - 'v' + - pY(d.by) + - 'h-' + - pX(d.bx) + - 'Z'; + if (!borderRadius) { + if (isMiddle) { + // middle aligned: rect centered on data + pathStr = + 'M-' + + pX(d.bx / 2 + d.tx2width / 2) + + ',' + + pY(offsetY - d.by / 2) + + 'h' + + pX(d.bx) + + 'v' + + pY(d.by) + + 'h-' + + pX(d.bx) + + 'Z'; + } else if (showArrow) { + // left or right aligned: side rect with arrow to data + pathStr = + 'M0,0L' + + pX(horzSign * HOVERARROWSIZE + offsetX) + + ',' + + pY(HOVERARROWSIZE + offsetY) + + 'v' + + pY(d.by / 2 - HOVERARROWSIZE) + + 'h' + + pX(horzSign * d.bx) + + 'v-' + + pY(d.by) + + 'H' + + pX(horzSign * HOVERARROWSIZE + offsetX) + + 'V' + + pY(offsetY - HOVERARROWSIZE) + + 'Z'; + } else { + // left or right aligned: side rect without arrow + pathStr = + 'M' + + pX(horzSign * HOVERARROWSIZE + offsetX) + + ',' + + pY(offsetY - d.by / 2) + + 'h' + + pX(horzSign * d.bx) + + 'v' + + pY(d.by) + + 'h' + + pX(-horzSign * d.bx) + + 'Z'; + } + } else if (isMiddle) { + var middleLeft = -d.bx / 2 - d.tx2width / 2; + pathStr = roundedRectPath( + middleLeft, + offsetY - d.by / 2, + middleLeft + d.bx, + offsetY + d.by / 2, + borderRadius, + true, + !hasSecondaryLabel, + pX, + pY + ); } else if (showArrow) { - // left or right aligned: side rect with arrow to data - pathStr = - 'M0,0L' + - pX(horzSign * HOVERARROWSIZE + offsetX) + - ',' + - pY(HOVERARROWSIZE + offsetY) + - 'v' + - pY(d.by / 2 - HOVERARROWSIZE) + - 'h' + - pX(horzSign * d.bx) + - 'v-' + - pY(d.by) + - 'H' + - pX(horzSign * HOVERARROWSIZE + offsetX) + - 'V' + - pY(offsetY - HOVERARROWSIZE) + - 'Z'; + pathStr = roundedSideArrowPath( + d.bx, + d.by, + horzSign, + offsetX, + offsetY, + borderRadius, + !hasSecondaryLabel, + pX, + pY + ); } else { - // left or right aligned: side rect without arrow - pathStr = - 'M' + - pX(horzSign * HOVERARROWSIZE + offsetX) + - ',' + - pY(offsetY - d.by / 2) + - 'h' + - pX(horzSign * d.bx) + - 'v' + - pY(d.by) + - 'h' + - pX(-horzSign * d.bx) + - 'Z'; + var nearX = horzSign * HOVERARROWSIZE + offsetX; + var farX = nearX + horzSign * d.bx; + pathStr = roundedRectPath( + nearX, + offsetY - d.by / 2, + farX, + offsetY + d.by / 2, + borderRadius, + !hasSecondaryLabel || horzSign === 1, + !hasSecondaryLabel || horzSign === -1, + pX, + pY + ); } + g.select('path').attr('d', pathStr); var posX = offsetX + shiftX.textShiftX; - var posY = offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD; + var posY = offsetY + d.ty0 - d.by / 2 + borderPad; var textAlign = d.textAlign || 'auto'; if (textAlign !== 'auto') { if (textAlign === 'left' && anchor !== 'start') { tx.attr('text-anchor', 'start'); - posX = isMiddle ? -d.bx / 2 - d.tx2width / 2 + HOVERTEXTPAD : -d.bx - HOVERTEXTPAD; + posX = isMiddle ? -d.bx / 2 - d.tx2width / 2 + borderPad : -d.bx - borderPad; } else if (textAlign === 'right' && anchor !== 'end') { tx.attr('text-anchor', 'end'); - posX = isMiddle ? d.bx / 2 - d.tx2width / 2 - HOVERTEXTPAD : d.bx + HOVERTEXTPAD; + posX = isMiddle ? d.bx / 2 - d.tx2width / 2 - borderPad : d.bx + borderPad; } } @@ -2210,17 +2552,38 @@ function alignHoverText(hoverLabels, rotateLabels, scaleX, scaleY) { if (d.tx2width) { g.select('text.name').call( svgTextUtils.positionText, - pX(shiftX.text2ShiftX + shiftX.alignShift * HOVERTEXTPAD + offsetX), - pY(offsetY + d.ty0 - d.by / 2 + HOVERTEXTPAD) + pX(shiftX.text2ShiftX + shiftX.alignShift * borderPad + offsetX), + pY(offsetY + d.ty0 - d.by / 2 + borderPad) + ); + + var rectX = shiftX.text2ShiftX + ((shiftX.alignShift - 1) * d.tx2width) / 2 + offsetX; + var rectWidth = d.tx2width; + var nameRectRadius = Math.max( + 0, + Math.min(borderRadius, d.tx2width / 2, (d.by + 2) / 2) ); - g.select('rect').call( + + // Let the secondary rect overlap the main path so only its outer corners remain rounded. + // The main path is drawn on top. + if(nameRectRadius) { + if(isMiddle || anchor === 'start') rectX -= nameRectRadius; + rectWidth += nameRectRadius; + } + + var nameRect = g.select('rect').call( Drawing.setRect, - pX(shiftX.text2ShiftX + ((shiftX.alignShift - 1) * d.tx2width) / 2 + offsetX), + pX(rectX), pY(offsetY - d.by / 2 - 1), - pX(d.tx2width), + pX(rectWidth), pY(d.by + 2) ); + + nameRect + .attr('rx', pX(nameRectRadius)) + .attr('ry', pY(nameRectRadius)); } + + applyHoverShadow(g, shadow); }); } diff --git a/src/components/fx/hoverlabel_defaults.js b/src/components/fx/hoverlabel_defaults.js index 0a0b87ac5c3..4204423003d 100644 --- a/src/components/fx/hoverlabel_defaults.js +++ b/src/components/fx/hoverlabel_defaults.js @@ -35,8 +35,14 @@ module.exports = function handleHoverLabelDefaults(contIn, contOut, coerce, opts coerce('hoverlabel.bgcolor', opts.bgcolor); coerce('hoverlabel.bordercolor', opts.bordercolor); - coerce('hoverlabel.namelength', opts.namelength); - coerce('hoverlabel.showarrow', opts.showarrow); + coerce('hoverlabel.borderwidth', opts.borderwidth); + coerce('hoverlabel.borderradius', opts.borderradius); + coerce('hoverlabel.borderpad', opts.borderpad); + coerce('hoverlabel.namecolor', opts.namecolor); + coerce('hoverlabel.bgnamecolor', opts.bgnamecolor); + coerce('hoverlabel.shadow', opts.shadow); Lib.coerceFont(coerce, 'hoverlabel.font', opts.font); coerce('hoverlabel.align', opts.align); + coerce('hoverlabel.namelength', opts.namelength); + coerce('hoverlabel.showarrow', opts.showarrow); }; diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js index fd7fae8dbb1..7a0f794dde1 100644 --- a/src/components/fx/layout_attributes.js +++ b/src/components/fx/layout_attributes.js @@ -173,6 +173,57 @@ module.exports = { 'Sets the border color of all hover labels on graph.' ].join(' ') }, + borderwidth: { + valType: 'number', + min: 0, + dflt: 1, + editType: 'none', + description: [ + 'Sets the border width (in px) of all hover labels on graph.' + ].join(' ') + }, + borderradius: { + valType: 'number', + min: 0, + dflt: 0, + editType: 'none', + description: [ + 'Sets the border radius (in px) of all hover labels on graph.' + ].join(' ') + }, + borderpad: { + valType: 'number', + min: 0, + dflt: constants.HOVERTEXTPAD, + editType: 'none', + description: [ + 'Sets the padding (in px) between the text and the border of all hover labels on graph.' + ].join(' ') + }, + namecolor: { + valType: 'color', + editType: 'none', + description: [ + 'Sets the text color of all secondary hover labels on graph.' + ].join(' ') + }, + bgnamecolor: { + valType: 'color', + editType: 'none', + description: [ + 'Sets the background color of all secondary hover labels on graph.' + ].join(' ') + }, + shadow: { + valType: 'string', + dflt: 'none', + editType: 'none', + description: [ + 'Sets the shadow of all hover labels on graph.', + 'Use *none* to disable the shadow, or provide CSS `drop-shadow` arguments', + 'such as *0 2px 4px rgba(0, 0, 0, 0.2)*.' + ].join(' ') + }, font: font, grouptitlefont: fontAttrs({ editType: 'none', @@ -214,7 +265,6 @@ module.exports = { 'pointing to the data point.' ].join(' ') }, - editType: 'none' }, selectdirection: { diff --git a/src/types/generated/schema.d.ts b/src/types/generated/schema.d.ts index 6d1004a24f6..0c7ce99be2b 100644 --- a/src/types/generated/schema.d.ts +++ b/src/types/generated/schema.d.ts @@ -444,16 +444,43 @@ export interface HoverLabel { align?: 'left' | 'right' | 'auto' | ('left' | 'right' | 'auto')[]; /** Sets the background color of the hover labels for this trace */ bgcolor?: Color | Color[] | TypedArray; + /** Sets the background color of the secondary hover label for this trace. */ + bgnamecolor?: Color; /** Sets the border color of the hover labels for this trace. */ bordercolor?: Color | Color[] | TypedArray; + /** + * Sets the padding (in px) between the text and the border of the hover labels for this trace. + * @default 3 + * Minimum: 0 + */ + borderpad?: number; + /** + * Sets the border radius (in px) of the hover labels for this trace. + * @default 0 + * Minimum: 0 + */ + borderradius?: number; + /** + * Sets the border width (in px) of the hover labels for this trace. + * @default 1 + * Minimum: 0 + */ + borderwidth?: number; /** Sets the font used in hover labels. */ font?: FontArray; + /** Sets the text color of the secondary hover label for this trace. */ + namecolor?: Color; /** * Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis. * @default 15 * Minimum: -1 */ namelength?: number | number[] | TypedArray; + /** + * Sets the shadow of the hover labels for this trace. + * @default 'none' + */ + shadow?: string; /** * Sets whether or not to show the hover label arrow/triangle pointing to the data point. * @default true @@ -1583,16 +1610,43 @@ export interface CandlestickData { align?: 'left' | 'right' | 'auto' | ('left' | 'right' | 'auto')[]; /** Sets the background color of the hover labels for this trace */ bgcolor?: Color | Color[] | TypedArray; + /** Sets the background color of the secondary hover label for this trace. */ + bgnamecolor?: Color; /** Sets the border color of the hover labels for this trace. */ bordercolor?: Color | Color[] | TypedArray; + /** + * Sets the padding (in px) between the text and the border of the hover labels for this trace. + * @default 3 + * Minimum: 0 + */ + borderpad?: number; + /** + * Sets the border radius (in px) of the hover labels for this trace. + * @default 0 + * Minimum: 0 + */ + borderradius?: number; + /** + * Sets the border width (in px) of the hover labels for this trace. + * @default 1 + * Minimum: 0 + */ + borderwidth?: number; /** Sets the font used in hover labels. */ font?: FontArray; + /** Sets the text color of the secondary hover label for this trace. */ + namecolor?: Color; /** * Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis. * @default 15 * Minimum: -1 */ namelength?: number | number[] | TypedArray; + /** + * Sets the shadow of the hover labels for this trace. + * @default 'none' + */ + shadow?: string; /** * Sets whether or not to show the hover label arrow/triangle pointing to the data point. * @default true @@ -6080,16 +6134,43 @@ export interface OhlcData { align?: 'left' | 'right' | 'auto' | ('left' | 'right' | 'auto')[]; /** Sets the background color of the hover labels for this trace */ bgcolor?: Color | Color[] | TypedArray; + /** Sets the background color of the secondary hover label for this trace. */ + bgnamecolor?: Color; /** Sets the border color of the hover labels for this trace. */ bordercolor?: Color | Color[] | TypedArray; + /** + * Sets the padding (in px) between the text and the border of the hover labels for this trace. + * @default 3 + * Minimum: 0 + */ + borderpad?: number; + /** + * Sets the border radius (in px) of the hover labels for this trace. + * @default 0 + * Minimum: 0 + */ + borderradius?: number; + /** + * Sets the border width (in px) of the hover labels for this trace. + * @default 1 + * Minimum: 0 + */ + borderwidth?: number; /** Sets the font used in hover labels. */ font?: FontArray; + /** Sets the text color of the secondary hover label for this trace. */ + namecolor?: Color; /** * Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis. * @default 15 * Minimum: -1 */ namelength?: number | number[] | TypedArray; + /** + * Sets the shadow of the hover labels for this trace. + * @default 'none' + */ + shadow?: string; /** * Sets whether or not to show the hover label arrow/triangle pointing to the data point. * @default true @@ -16269,18 +16350,45 @@ export interface Layout { align?: 'left' | 'right' | 'auto'; /** Sets the background color of all hover labels on graph */ bgcolor?: Color; + /** Sets the background color of all secondary hover labels on graph. */ + bgnamecolor?: Color; /** Sets the border color of all hover labels on graph. */ bordercolor?: Color; + /** + * Sets the padding (in px) between the text and the border of all hover labels on graph. + * @default 3 + * Minimum: 0 + */ + borderpad?: number; + /** + * Sets the border radius (in px) of all hover labels on graph. + * @default 0 + * Minimum: 0 + */ + borderradius?: number; + /** + * Sets the border width (in px) of all hover labels on graph. + * @default 1 + * Minimum: 0 + */ + borderwidth?: number; /** Sets the default hover label font used by all traces on the graph. */ font?: Font; /** Sets the font for group titles in hover (unified modes). Defaults to `hoverlabel.font`. */ grouptitlefont?: Font; + /** Sets the text color of all secondary hover labels on graph. */ + namecolor?: Color; /** * Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis. * @default 15 * Minimum: -1 */ namelength?: number; + /** + * Sets the shadow of all hover labels on graph. Use *none* to disable the shadow, or provide CSS `drop-shadow` arguments such as *0 2px 4px rgba(0, 0, 0, 0.2)*. + * @default 'none' + */ + shadow?: string; /** * Sets whether or not to show the hover label arrow/triangle pointing to the data point. * @default true diff --git a/test/image/mocks/line_scatter.json b/test/image/mocks/line_scatter.json index 4d940aa23e4..022ec1e55cb 100644 --- a/test/image/mocks/line_scatter.json +++ b/test/image/mocks/line_scatter.json @@ -33,5 +33,19 @@ "line": { "width": 10, "color": "red" }, "marker": { "size": 20, "color": "blue" } } - ] + ], + "layout": { + "hoverlabel": { + "borderwidth": 5, + "borderradius": 20, + "namecolor": "red", + "bgnamecolor": "yellow", + "shadow": "0 10px 4px rgba(0, 0, 0, 0.2)", + "borderpad": 15 + }, + "title": { + "text": "layout.hovermode='['x', 'y', 'closest', false, 'x unified', 'y unified']'" + }, + "hovermode": "y" + } } diff --git a/test/jasmine/tests/fx_test.js b/test/jasmine/tests/fx_test.js index e7b3bc722a1..cbbe691af67 100644 --- a/test/jasmine/tests/fx_test.js +++ b/test/jasmine/tests/fx_test.js @@ -150,6 +150,12 @@ describe('Fx defaults', function() { hoverlabel: { bgcolor: 'white', bordercolor: 'black', + borderwidth: 2, + borderradius: 8, + borderpad: 6, + namecolor: 'purple', + bgnamecolor: 'orange', + shadow: '0 2px 4px rgba(0, 0, 0, 0.2)', font: { family: 'Roboto', size: 20, @@ -167,6 +173,12 @@ describe('Fx defaults', function() { expect(out.data[0].hoverlabel).toEqual({ bgcolor: ['red', 'blue', 'black'], bordercolor: 'black', + borderwidth: 2, + borderradius: 8, + borderpad: 6, + namecolor: 'purple', + bgnamecolor: 'orange', + shadow: '0 2px 4px rgba(0, 0, 0, 0.2)', font: { family: 'Roboto', size: 40, @@ -186,6 +198,12 @@ describe('Fx defaults', function() { expect(out.data[1].hoverlabel).toEqual({ bgcolor: 'white', bordercolor: 'yellow', + borderwidth: 2, + borderradius: 8, + borderpad: 6, + namecolor: 'purple', + bgnamecolor: 'orange', + shadow: '0 2px 4px rgba(0, 0, 0, 0.2)', font: { family: 'Roboto', size: 20, diff --git a/test/jasmine/tests/hover_test.js b/test/jasmine/tests/hover_test.js index bf80831cab6..563cd4dad46 100644 --- a/test/jasmine/tests/hover_test.js +++ b/test/jasmine/tests/hover_test.js @@ -5231,6 +5231,119 @@ describe('Test hover label custom styling:', function () { }) .then(done, done.fail); }); + + it('should apply extended hoverlabel styling', function (done) { + var gd = createGraphDiv(); + var borderPad = 10; + + Plotly.newPlot( + gd, + [ + { + x: [1, 2, 3], + y: [1, 2, 1], + hovertemplate: 'value: %{y}secondary' + } + ], + { + hovermode: 'x', + width: 500, + height: 400, + margin: { l: 50, r: 50, t: 50, b: 50 }, + hoverlabel: { + borderwidth: 4, + borderradius: 8, + borderpad: borderPad, + namecolor: 'white', + bgnamecolor: 'black', + shadow: '0 2px 4px rgba(0, 0, 0, 0.25)' + } + } + ) + .then(function () { + _hover(gd, { xval: 2 }); + + var label = d3Select('g.hovertext'); + var path = label.select('path'); + var nameRect = label.select('rect'); + var nameText = label.select('text.name'); + var labelData = label.datum(); + + expect(path.attr('d')).toContain('Q'); + expect(path.node().style.strokeWidth).toBe('4px'); + expect(labelData.bx - labelData.txwidth).toBeCloseTo(2 * borderPad, 5); + + expect(nameRect.node().style.fill).toBe('rgb(0, 0, 0)'); + expect(nameText.node().style.fill).toBe('rgb(255, 255, 255)'); + expect(Number(nameRect.attr('rx'))).toBeGreaterThan(0); + expect(Number(nameRect.attr('ry'))).toBeGreaterThan(0); + expect(label.node().style.filter).toContain('drop-shadow'); + + var commonLabel = d3Select('g.axistext'); + var commonPath = commonLabel.select('path'); + + expect(commonPath.attr('d')).toContain('Q'); + expect(commonPath.node().style.strokeWidth).toBe('4px'); + expect(commonLabel.node().style.filter).toContain('drop-shadow'); + }) + .then(done, done.fail); + }); + + it('should choose a contrasting secondary-label text color when only bgnamecolor is set', function (done) { + var gd = createGraphDiv(); + + Plotly.newPlot( + gd, + [ + { + x: [1], + y: [1], + hovertemplate: 'value: %{y}secondary' + } + ], + { + hoverlabel: { + bgnamecolor: 'black' + } + } + ) + .then(function () { + _hover(gd, [{ curveNumber: 0, pointNumber: 0 }]); + + var label = d3Select('g.hovertext'); + expect(label.select('rect').node().style.fill).toBe('rgb(0, 0, 0)'); + expect(label.select('text.name').node().style.fill).toBe('rgb(255, 255, 255)'); + }) + .then(done, done.fail); + }); + + it('should round a hoverlabel without an arrow', function(done) { + var gd = createGraphDiv(); + + Plotly.newPlot( + gd, + [ + { + x: [1, 2, 3], + y: [1, 2, 1] + } + ], + { + hoverlabel: { + borderradius: 8, + showarrow: false + } + } + ) + .then(function () { + _hover(gd, [{ curveNumber: 0, pointNumber: 1 }]); + + var path = d3Select('g.hovertext').select('path'); + expect(path.attr('d')).toContain('Q'); + expect(path.attr('d')).not.toMatch(/^M0,0L/); + }) + .then(done, done.fail); + }); }); describe('hover distance', function () { @@ -7250,6 +7363,58 @@ describe('hovermode: (x|y)unified', function () { .then(done, done.fail); }); + it('should apply border width, border radius, padding and shadow to unified hover', function (done) { + var defaultPad = 3; + var customPad = 10; + var initialWidth; + var initialHeight; + + Plotly.newPlot( + gd, + [ + { x: [1, 2, 3], y: [1, 2, 3], name: 'A' }, + { x: [1, 2, 3], y: [3, 2, 1], name: 'B' } + ], + { + hovermode: 'x unified', + showlegend: false, + hoverlabel: { + borderwidth: 4, + borderradius: 9, + borderpad: defaultPad, + shadow: '0 2px 4px rgba(0, 0, 0, 0.25)' + } + } + ) + .then(function () { + _hover(gd, { xval: 2 }); + + var hover = getHoverLabel(); + var bg = hover.select('rect.bg'); + + expect(Number(bg.attr('rx'))).toBe(9); + expect(Number(bg.attr('ry'))).toBe(9); + expect(bg.node().style.strokeWidth).toBe('4px'); + expect(hover.node().style.filter).toContain('drop-shadow'); + + initialWidth = Number(bg.attr('width')); + initialHeight = Number(bg.attr('height')); + + return Plotly.relayout(gd, 'hoverlabel.borderpad', customPad); + }) + .then(function () { + _hover(gd, { xval: 2 }); + + var bg = getHoverLabel().select('rect.bg'); + var padIncrease = 2 * (customPad - defaultPad); + + expect(Number(bg.attr('width'))).toBeCloseTo(initialWidth + padIncrease, 5); + expect(Number(bg.attr('height'))).toBeCloseTo(initialHeight + padIncrease, 5); + }) + .then(done, done.fail); + }); + + it('should use hoverlabel.font or legend.font or layout.font', function (done) { function assertFont(fontFamily, fontSize, fontColor) { var hover = getHoverLabel(); diff --git a/test/plot-schema.json b/test/plot-schema.json index cd4ad209bb2..68ce07161c0 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -2845,11 +2845,37 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of all secondary hover labels on graph.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "description": "Sets the border color of all hover labels on graph.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of all hover labels on graph.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of all hover labels on graph.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of all hover labels on graph.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -3031,6 +3057,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of all secondary hover labels on graph.", + "editType": "none", + "valType": "color" + }, "namelength": { "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", "dflt": 15, @@ -3039,6 +3070,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of all hover labels on graph. Use *none* to disable the shadow, or provide CSS `drop-shadow` arguments such as *0 2px 4px rgba(0, 0, 0, 0.2)*.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -16220,12 +16257,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -16325,6 +16388,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -16334,6 +16402,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -18089,12 +18163,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -18194,6 +18294,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -18203,6 +18308,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -19485,12 +19596,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -19590,6 +19727,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -19599,6 +19741,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -20855,12 +21003,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -20960,6 +21134,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -20969,6 +21148,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -23690,12 +23875,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -23795,6 +24006,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -23804,6 +24020,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -24830,12 +25052,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -24935,6 +25183,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -24944,6 +25197,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -25999,12 +26258,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -26104,6 +26389,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -26113,6 +26403,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -27414,12 +27710,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -27519,6 +27841,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -27528,6 +27855,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -30044,12 +30377,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -30149,6 +30508,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -30158,6 +30522,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -30582,12 +30952,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -30687,6 +31083,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -30696,6 +31097,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -32325,12 +32732,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -32430,6 +32863,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -32439,6 +32877,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -33841,12 +34285,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -33946,6 +34416,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -33955,6 +34430,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -34866,12 +35347,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -34971,6 +35478,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -34980,6 +35492,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -37328,12 +37846,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -37433,6 +37977,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -37442,6 +37991,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -38898,12 +39453,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -39003,6 +39584,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -39012,6 +39598,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -39713,12 +40305,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -39818,6 +40436,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -39827,6 +40450,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -41444,12 +42073,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -41549,6 +42204,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -41558,6 +42218,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -43806,12 +44472,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -43911,6 +44603,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -43920,6 +44617,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -45191,12 +45894,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -45296,6 +46025,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -45305,6 +46039,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -45861,12 +46601,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -45966,6 +46732,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -45975,6 +46746,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -49098,12 +49875,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -49203,6 +50006,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -49212,6 +50020,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -50146,12 +50960,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -50251,6 +51091,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -50260,6 +51105,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -51579,12 +52430,38 @@ "editType": "calc", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "calc", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "calc", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "calc", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "calc", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "calc", + "min": 0, + "valType": "number" + }, "editType": "calc", "font": { "color": { @@ -51684,6 +52561,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "calc", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -51693,6 +52575,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "calc", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -51934,12 +52822,38 @@ "editType": "calc", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "calc", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "calc", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "calc", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "calc", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "calc", + "min": 0, + "valType": "number" + }, "editType": "calc", "font": { "color": { @@ -52039,6 +52953,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "calc", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -52048,6 +52967,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "calc", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -52206,12 +53131,38 @@ "editType": "calc", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "calc", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "calc", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "calc", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "calc", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "calc", + "min": 0, + "valType": "number" + }, "editType": "calc", "font": { "color": { @@ -52311,6 +53262,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "calc", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -52320,6 +53276,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "calc", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -52929,12 +53891,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -53034,6 +54022,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -53043,6 +54036,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -55422,12 +56421,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -55527,6 +56552,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -55536,6 +56566,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -57657,12 +58693,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -57762,6 +58824,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -57771,6 +58838,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -59736,12 +60809,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -59841,6 +60940,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -59850,6 +60954,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -61943,12 +63053,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -62048,6 +63184,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -62057,6 +63198,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -64051,12 +65198,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -64156,6 +65329,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -64165,6 +65343,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -65383,12 +66567,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -65488,6 +66698,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -65497,6 +66712,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -67478,12 +68699,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -67583,6 +68830,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -67592,6 +68844,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -69427,12 +70685,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -69532,6 +70816,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -69541,6 +70830,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -71506,12 +72801,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -71611,6 +72932,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -71620,6 +72946,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -73606,12 +74938,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -73711,6 +75069,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -73720,6 +75083,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -76042,12 +77411,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -76147,6 +77542,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -76156,6 +77556,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -76711,12 +78117,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -76816,6 +78248,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -76825,6 +78262,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -79191,12 +80634,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -79296,6 +80765,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -79305,6 +80779,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -80199,12 +81679,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -80304,6 +81810,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -80313,6 +81824,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -80617,12 +82134,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -80722,6 +82265,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -80731,6 +82279,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -82423,12 +83977,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -82528,6 +84108,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -82537,6 +84122,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -84411,12 +86002,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -84516,6 +86133,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -84525,6 +86147,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, @@ -85220,12 +86848,38 @@ "editType": "none", "valType": "color" }, + "bgnamecolor": { + "description": "Sets the background color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "bordercolor": { "arrayOk": true, "description": "Sets the border color of the hover labels for this trace.", "editType": "none", "valType": "color" }, + "borderpad": { + "description": "Sets the padding (in px) between the text and the border of the hover labels for this trace.", + "dflt": 3, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderradius": { + "description": "Sets the border radius (in px) of the hover labels for this trace.", + "dflt": 0, + "editType": "none", + "min": 0, + "valType": "number" + }, + "borderwidth": { + "description": "Sets the border width (in px) of the hover labels for this trace.", + "dflt": 1, + "editType": "none", + "min": 0, + "valType": "number" + }, "editType": "none", "font": { "color": { @@ -85325,6 +86979,11 @@ "valType": "integer" } }, + "namecolor": { + "description": "Sets the text color of the secondary hover label for this trace.", + "editType": "none", + "valType": "color" + }, "namelength": { "arrayOk": true, "description": "Sets the default length (in number of characters) of the trace name in the hover labels for all traces. -1 shows the whole name regardless of length. 0-3 shows the first 0-3 characters, and an integer >3 will show the whole name if it is less than that many characters, but if it is longer, will truncate to `namelength - 3` characters and add an ellipsis.", @@ -85334,6 +86993,12 @@ "valType": "integer" }, "role": "object", + "shadow": { + "description": "Sets the shadow of the hover labels for this trace.", + "dflt": "none", + "editType": "none", + "valType": "string" + }, "showarrow": { "description": "Sets whether or not to show the hover label arrow/triangle pointing to the data point.", "dflt": true, From 9a10c97bc8d22dade61ea7859c6ce608494298a8 Mon Sep 17 00:00:00 2001 From: Lexachoc <20377719+Lexachoc@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:51:59 +0200 Subject: [PATCH 2/5] add draftlog --- draftlogs/8115_add.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/8115_add.md diff --git a/draftlogs/8115_add.md b/draftlogs/8115_add.md new file mode 100644 index 00000000000..b3072060026 --- /dev/null +++ b/draftlogs/8115_add.md @@ -0,0 +1 @@ +- Add `hoverlabel.borderwidth`, `hoverlabel.borderradius`, `hoverlabel.borderpad`, `hoverlabel.namecolor`, `hoverlabel.bgnamecolor`, and `hoverlabel.shadow` styling options [[#8115](https://github.com/plotly/plotly.js/pull/8115)] \ No newline at end of file From 52afd342a1aee7d6f341833f17d1052df4bc851b Mon Sep 17 00:00:00 2001 From: Lexachoc <20377719+Lexachoc@users.noreply.github.com> Date: Sat, 10 Oct 2026 18:04:28 +0200 Subject: [PATCH 3/5] revert accidental mock change --- test/image/mocks/line_scatter.json | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/test/image/mocks/line_scatter.json b/test/image/mocks/line_scatter.json index 022ec1e55cb..4d940aa23e4 100644 --- a/test/image/mocks/line_scatter.json +++ b/test/image/mocks/line_scatter.json @@ -33,19 +33,5 @@ "line": { "width": 10, "color": "red" }, "marker": { "size": 20, "color": "blue" } } - ], - "layout": { - "hoverlabel": { - "borderwidth": 5, - "borderradius": 20, - "namecolor": "red", - "bgnamecolor": "yellow", - "shadow": "0 10px 4px rgba(0, 0, 0, 0.2)", - "borderpad": 15 - }, - "title": { - "text": "layout.hovermode='['x', 'y', 'closest', false, 'x unified', 'y unified']'" - }, - "hovermode": "y" - } + ] } From 1926941923cda4b90e4258e9e48e8c9702543083 Mon Sep 17 00:00:00 2001 From: Lexachoc <20377719+Lexachoc@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:39:14 +0200 Subject: [PATCH 4/5] improve default secondary hover label text contrast --- src/components/fx/hover.js | 8 +++++++- test/jasmine/tests/hover_test.js | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 5e223f5a709..31cfe4f4e3d 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1895,8 +1895,14 @@ function createHoverText(hoverData, opts) { var bgNameColor = d.bgNameColor ?? hoverlabel?.bgnamecolor; var nameColor = d.nameColor ?? hoverlabel?.namecolor; var nameBg = bgNameColor || Color.addOpacity(bgColor, 0.8); + // if(nameColor === undefined) { + // nameColor = bgNameColor ? Color.contrast(nameBg) : fallbackNameColor; + // } if(nameColor === undefined) { - nameColor = bgNameColor ? Color.contrast(nameBg) : fallbackNameColor; + nameColor = + Color.wcagContrast(fallbackNameColor, nameBg) < 4.5 + ? Color.contrast(nameBg) + : fallbackNameColor; } // find a contrasting color for border and text diff --git a/test/jasmine/tests/hover_test.js b/test/jasmine/tests/hover_test.js index 563cd4dad46..14f7f92f825 100644 --- a/test/jasmine/tests/hover_test.js +++ b/test/jasmine/tests/hover_test.js @@ -5128,14 +5128,14 @@ describe('Test hover label custom styling:', function () { .then(function () { _hover(gd, { xval: gd._fullData[0].x[0] }); assertNameLabel({ - color: ['rgb(31, 119, 180)', 'rgb(255, 127, 14)'] + color: ['rgb(31, 119, 180)', 'rgb(68, 68, 68)'] }); return Plotly.restyle(gd, 'marker.color', ['red', 'blue']); }) .then(function () { _hover(gd, { xval: gd._fullData[0].x[0] }); assertNameLabel({ - color: ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'] + color: ['rgb(68, 68, 68)', 'rgb(0, 0, 255)'] }); return Plotly.relayout(gd, 'hoverlabel.bgcolor', 'white'); }) @@ -5143,15 +5143,15 @@ describe('Test hover label custom styling:', function () { _hover(gd, { xval: gd._fullData[0].x[0] }); // should not affect the name font color assertNameLabel({ - color: ['rgb(255, 0, 0)', 'rgb(0, 0, 255)'] + color: ['rgb(68, 68, 68)', 'rgb(0, 0, 255)'] }); return Plotly.restyle(gd, 'marker.color', ['rgba(255,0,0,0.1)', 'rgba(0,0,255,0.1)']); }) .then(function () { _hover(gd, { xval: gd._fullData[0].x[0] }); - // should blend with plot_bgcolor + // blended trace colors have insufficient contrast, so use a contrasting color assertNameLabel({ - color: ['rgb(255, 179, 179)', 'rgb(179, 179, 255)'] + color: ['rgb(68, 68, 68)', 'rgb(68, 68, 68)'] }); return Plotly.restyle(gd, 'marker.color', ['rgba(255,0,0,0)', 'rgba(0,0,255,0)']); }) From 447e3e3e8ca5e6b08dcb673aad155a38dfe59b03 Mon Sep 17 00:00:00 2001 From: Lexachoc <20377719+Lexachoc@users.noreply.github.com> Date: Tue, 20 Oct 2026 18:58:40 +0200 Subject: [PATCH 5/5] merge changes from PR8051 (MathJax hover rendering) --- src/components/fx/hover.js | 659 ++++++++++++---------- src/components/legend/draw.js | 2 +- src/lib/svg_text_utils.js | 176 ++++-- test/jasmine/bundle_tests/mathjax_test.js | 427 +++++++++++++- 4 files changed, 934 insertions(+), 330 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 31cfe4f4e3d..08fc1efcba4 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -251,35 +251,51 @@ exports.loneHover = function loneHover(hoverItems, opts) { }); var hoverLabel = hoverText.hoverLabels; - // Fix vertical overlap - var tooltipSpacing = 5; - var lastBottomY = 0; - var anchor = 0; - hoverLabel - .sort(function (a, b) { - return a.y0 - b.y0; - }) - .each(function (d, i) { - var topY = d.y0 - d.by / 2; - - if (topY - tooltipSpacing < lastBottomY) { - d.offset = lastBottomY - topY + tooltipSpacing; - } else { - d.offset = 0; - } + function fixVerticalOverlap() { + var tooltipSpacing = 5; + var lastBottomY = 0; + var anchor = 0; + hoverLabel + .sort(function (a, b) { + return a.y0 - b.y0; + }) + .each(function (d, i) { + var topY = d.y0 - d.by / 2; - lastBottomY = topY + d.by + d.offset; + if (topY - tooltipSpacing < lastBottomY) { + d.offset = lastBottomY - topY + tooltipSpacing; + } else { + d.offset = 0; + } - if (i === opts.anchorIndex || 0) anchor = d.offset; - }) - .each(function (d) { - d.offset -= anchor; - }); + lastBottomY = topY + d.by + d.offset; + + if (i === opts.anchorIndex || 0) anchor = d.offset; + }) + .each(function (d) { + d.offset -= anchor; + }); + } var scaleX = gd._fullLayout._invScaleX; var scaleY = gd._fullLayout._invScaleY; + + fixVerticalOverlap(); alignHoverText(hoverLabel, rotateLabels, scaleX, scaleY); + if (hoverText.mathjaxPromise) { + hoverText.mathjaxPromise.then(function () { + fixVerticalOverlap(); + alignHoverText(hoverLabel, rotateLabels, scaleX, scaleY); + + hoverLabel.each(function () { + var g = d3.select(this); + svgTextUtils.repositionMathGroup(g.select('text.nums')); + svgTextUtils.repositionMathGroup(g.select('text.name')); + }); + }); + } + return multiHover ? hoverLabel : hoverLabel.node(); }; @@ -685,7 +701,7 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) { if (Registry.traceIs(trace, 'ternary') && spikedistance !== 0) { const ternary = pointData.subplot; - + // as in Cartesian, 'hovered data' relies only on the normal hover result, while // 'data' and 'cursor' need a closest data point const needsClosestPoint = ['aaxis', 'baxis', 'caxis'].some((name) => @@ -1035,6 +1051,21 @@ function _hover(gd, evt, subplot, noHoverEvent, eventTarget) { if (!helpers.isUnifiedHover(hovermode)) { hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, hoverText.commonLabelBoundingBox); alignHoverText(hoverLabels, rotateLabels, fullLayout._invScaleX, fullLayout._invScaleY); + + if (hoverText.mathjaxPromise) { + hoverText.mathjaxPromise.then(function () { + // A newer hover call already replaced this one; nothing to fix. + if (gd._hoverdata !== newhoverdata) return; + hoverAvoidOverlaps(hoverLabels, rotateLabels, fullLayout, hoverText.commonLabelBoundingBox); + alignHoverText(hoverLabels, rotateLabels, fullLayout._invScaleX, fullLayout._invScaleY); + + hoverLabels.each(function () { + var g = d3.select(this); + svgTextUtils.repositionMathGroup(g.select('text.nums')); + svgTextUtils.repositionMathGroup(g.select('text.name')); + }); + }); + } } // TODO: tagName hack is needed to appease geo.js's hack of using eventTarget=true // we should improve the "fx" API so other plots can use it without these hack. if (eventTarget && eventTarget.tagName) { @@ -1077,6 +1108,10 @@ function hoverDataKey(d) { var EXTRA_STRING_REGEX = /([\s\S]*)<\/extra>/; +function getHoverTextBBox(gd, textSel) { + return getBoundingClientRect(gd, svgTextUtils.getMathOrTextNode(textSel)); +} + function applyHoverShadow(selection, shadow) { var filter = shadow && shadow !== 'none' ? 'drop-shadow(' + shadow + ')' : null; selection.style('filter', filter); @@ -1288,6 +1323,8 @@ function createHoverText(hoverData, opts) { // Early exit if no labels are drawn if (hoverData.length === 0) return [[]]; + var mathjaxPromises = []; + // opts.fontFamily/Size are used for the common label // and as defaults for each hover label, though the individual labels // can override this. @@ -1364,11 +1401,7 @@ function createHoverText(hoverData, opts) { var lpath = Lib.ensureSingle(label, 'path', '', function (s) { s.style({ 'stroke-width': '1px' }); }); - var ltext = Lib.ensureSingle(label, 'text', '', function (s) { - // prohibit tex interpretation until we can handle - // tex and regular text together - s.attr('data-notex', 1); - }); + var ltext = Lib.ensureSingle(label, 'text', ''); var commonBgColor = commonLabelOpts.bgcolor || Color.defaultLine; var commonStroke = commonLabelOpts.bordercolor || Color.contrast(commonBgColor); @@ -1396,208 +1429,234 @@ function createHoverText(hoverData, opts) { 'stroke-width': commonBorderWidth + 'px' }); + var commonLabelTex = svgTextUtils.isPureTex(String(t0)); + var onCommonLabelReady; + + if (commonLabelTex) { + mathjaxPromises.push( + new Promise(function (resolve) { + onCommonLabelReady = function () { + if (!ltext.node().parentNode) { + resolve(); + return; + } + positionCommonLabel(); + svgTextUtils.repositionMathGroup(ltext); + resolve(); + }; + }) + ); + } + ltext .text(t0) .call(Drawing.font, commonLabelFont) + .attr('data-notex', commonLabelTex ? null : 1) .call(svgTextUtils.positionText, 0, 0) - .call(svgTextUtils.convertToTspans, gd); - - label.attr('transform', ''); + .call(svgTextUtils.convertToTspans, gd, onCommonLabelReady); - var tbb = getBoundingClientRect(gd, ltext.node()); - var lx, ly; + positionCommonLabel(); - if (hovermode === 'x') { - var topsign = xa.side === 'top' ? '-' : ''; + function positionCommonLabel() { + label.attr('transform', ''); + svgTextUtils.positionText(ltext, 0, 0); + svgTextUtils.repositionMathGroup(ltext); - ltext - .attr('text-anchor', 'middle') - .call( - svgTextUtils.positionText, - 0, - xa.side === 'top' - ? outerTop - tbb.bottom - HOVERARROWSIZE - commonBorderPad - : outerTop - tbb.top + HOVERARROWSIZE + commonBorderPad - ); - - lx = xa._offset + (c0.x0 + c0.x1) / 2; - ly = ya._offset + (xa.side === 'top' ? 0 : ya._length); + var tbb = getHoverTextBBox(gd, ltext); + var lx, ly; - var halfWidth = tbb.width / 2 + commonBorderPad; + if (hovermode === 'x') { + var topsign = xa.side === 'top' ? '-' : ''; - var tooltipMidX = lx; - if (lx < halfWidth) { - tooltipMidX = halfWidth; - } else if (lx > fullLayout.width - halfWidth) { - tooltipMidX = fullLayout.width - halfWidth; - } - - var commonHeight = commonBorderPad * 2 + tbb.height; - var commonTipX = lx - tooltipMidX; - - lpath.attr( - 'd', - commonBorderRadius - ? roundedTopBottomArrowPath( - commonTipX, - halfWidth * 2, - commonHeight, - xa.side === 'top' ? -1 : 1, - commonBorderRadius - ) - : 'M' + - commonTipX + - ',0' + - 'L' + - (commonTipX + HOVERARROWSIZE) + - ',' + - topsign + - HOVERARROWSIZE + - 'H' + - halfWidth + - 'v' + - topsign + - commonHeight + - 'H' + - -halfWidth + - 'V' + - topsign + - HOVERARROWSIZE + - 'H' + - (commonTipX - HOVERARROWSIZE) + - 'Z' - ); - - lx = tooltipMidX; - commonLabelRect.minX = lx - halfWidth; - commonLabelRect.maxX = lx + halfWidth; - if (xa.side === 'top') { - // label on negative y side - commonLabelRect.minY = ly - (commonBorderPad * 2 + tbb.height); - commonLabelRect.maxY = ly - commonBorderPad; - } else { - commonLabelRect.minY = ly + commonBorderPad; - commonLabelRect.maxY = ly + (commonBorderPad * 2 + tbb.height); - } - } else { - var anchor; - var sgn; - var leftsign; - if (ya.side === 'right') { - anchor = 'start'; - sgn = 1; - leftsign = ''; - lx = xa._offset + xa._length; - } else { - anchor = 'end'; - sgn = -1; - leftsign = '-'; - lx = xa._offset; - } + ltext + .attr('text-anchor', 'middle') + .call( + svgTextUtils.positionText, + 0, + xa.side === 'top' + ? outerTop - tbb.bottom - HOVERARROWSIZE - commonBorderPad + : outerTop - tbb.top + HOVERARROWSIZE + commonBorderPad + ); - ly = ya._offset + (c0.y0 + c0.y1) / 2; + lx = xa._offset + (c0.x0 + c0.x1) / 2; + ly = ya._offset + (xa.side === 'top' ? 0 : ya._length); - ltext.attr('text-anchor', anchor); + var halfWidth = tbb.width / 2 + commonBorderPad; - var commonWidth = commonBorderPad * 2 + tbb.width; - var commonBoxHeight = commonBorderPad * 2 + tbb.height; + var tooltipMidX = lx; + if (lx < halfWidth) { + tooltipMidX = halfWidth; + } else if (lx > fullLayout.width - halfWidth) { + tooltipMidX = fullLayout.width - halfWidth; + } - lpath.attr( - 'd', - commonBorderRadius - ? roundedSideArrowPath( - commonWidth, - commonBoxHeight, - sgn, - 0, - 0, - commonBorderRadius, - true, - Lib.identity, - Lib.identity - ) - : 'M0,0' + - 'L' + - leftsign + - HOVERARROWSIZE + - ',' + - HOVERARROWSIZE + - 'V' + - (commonBorderPad + tbb.height / 2) + - 'h' + - leftsign + - commonWidth + - 'V-' + - (commonBorderPad + tbb.height / 2) + - 'H' + - leftsign + - HOVERARROWSIZE + - 'V-' + - HOVERARROWSIZE + - 'Z' - ); + var commonHeight = commonBorderPad * 2 + tbb.height; + var commonTipX = lx - tooltipMidX; + + lpath.attr( + 'd', + commonBorderRadius + ? roundedTopBottomArrowPath( + commonTipX, + halfWidth * 2, + commonHeight, + xa.side === 'top' ? -1 : 1, + commonBorderRadius + ) + : 'M' + + commonTipX + + ',0' + + 'L' + + (commonTipX + HOVERARROWSIZE) + + ',' + + topsign + + HOVERARROWSIZE + + 'H' + + halfWidth + + 'v' + + topsign + + commonHeight + + 'H' + + -halfWidth + + 'V' + + topsign + + HOVERARROWSIZE + + 'H' + + (commonTipX - HOVERARROWSIZE) + + 'Z' + ); - commonLabelRect.minY = ly - (commonBorderPad + tbb.height / 2); - commonLabelRect.maxY = ly + (commonBorderPad + tbb.height / 2); - if (ya.side === 'right') { - commonLabelRect.minX = lx + HOVERARROWSIZE; - commonLabelRect.maxX = lx + HOVERARROWSIZE + (commonBorderPad * 2 + tbb.width); + lx = tooltipMidX; + commonLabelRect.minX = lx - halfWidth; + commonLabelRect.maxX = lx + halfWidth; + if (xa.side === 'top') { + // label on negative y side + commonLabelRect.minY = ly - (commonBorderPad * 2 + tbb.height); + commonLabelRect.maxY = ly - commonBorderPad; + } else { + commonLabelRect.minY = ly + commonBorderPad; + commonLabelRect.maxY = ly + (commonBorderPad * 2 + tbb.height); + } } else { - // label on negative x side - commonLabelRect.minX = lx - HOVERARROWSIZE - (commonBorderPad * 2 + tbb.width); - commonLabelRect.maxX = lx - HOVERARROWSIZE; - } + var anchor; + var sgn; + var leftsign; + if (ya.side === 'right') { + anchor = 'start'; + sgn = 1; + leftsign = ''; + lx = xa._offset + xa._length; + } else { + anchor = 'end'; + sgn = -1; + leftsign = '-'; + lx = xa._offset; + } - var halfHeight = tbb.height / 2; - var lty = outerTop - tbb.top - halfHeight; - var clipId = 'clip' + fullLayout._uid + 'commonlabel' + ya._id; - var clipPath; + ly = ya._offset + (c0.y0 + c0.y1) / 2; + + ltext.attr('text-anchor', anchor); + + var commonWidth = commonBorderPad * 2 + tbb.width; + var commonBoxHeight = commonBorderPad * 2 + tbb.height; + + lpath.attr( + 'd', + commonBorderRadius + ? roundedSideArrowPath( + commonWidth, + commonBoxHeight, + sgn, + 0, + 0, + commonBorderRadius, + true, + Lib.identity, + Lib.identity + ) + : 'M0,0' + + 'L' + + leftsign + + HOVERARROWSIZE + + ',' + + HOVERARROWSIZE + + 'V' + + (commonBorderPad + tbb.height / 2) + + 'h' + + leftsign + + commonWidth + + 'V-' + + (commonBorderPad + tbb.height / 2) + + 'H' + + leftsign + + HOVERARROWSIZE + + 'V-' + + HOVERARROWSIZE + + 'Z' + ); - if (lx < tbb.width + 2 * commonBorderPad + HOVERARROWSIZE) { - var clippedTextWidth = Math.max(0, tbb.width - commonBorderPad); - clipPath = - 'M-' + - (HOVERARROWSIZE + commonBorderPad) + - '-' + - halfHeight + - 'h-' + - clippedTextWidth + - 'V' + - halfHeight + - 'h' + - clippedTextWidth + - 'Z'; + commonLabelRect.minY = ly - (commonBorderPad + tbb.height / 2); + commonLabelRect.maxY = ly + (commonBorderPad + tbb.height / 2); + if (ya.side === 'right') { + commonLabelRect.minX = lx + HOVERARROWSIZE; + commonLabelRect.maxX = lx + HOVERARROWSIZE + (commonBorderPad * 2 + tbb.width); + } else { + // label on negative x side + commonLabelRect.minX = lx - HOVERARROWSIZE - (commonBorderPad * 2 + tbb.width); + commonLabelRect.maxX = lx - HOVERARROWSIZE; + } - var ltx = tbb.width - lx + commonBorderPad; - svgTextUtils.positionText(ltext, ltx, lty); - - // shift each line (except the longest) so that start-of-line - // is always visible - if (anchor === 'end') { - ltext.selectAll('tspan').each(function () { - var s = d3.select(this); - var dummy = Drawing.tester.append('text').text(s.text()).call(Drawing.font, commonLabelFont); - var dummyBB = getBoundingClientRect(gd, dummy.node()); - if (Math.round(dummyBB.width) < Math.round(tbb.width)) { - s.attr('x', ltx - dummyBB.width); - } - dummy.remove(); - }); + var halfHeight = tbb.height / 2; + var lty = outerTop - tbb.top - halfHeight; + var clipId = 'clip' + fullLayout._uid + 'commonlabel' + ya._id; + var clipPath; + + if (lx < tbb.width + 2 * commonBorderPad + HOVERARROWSIZE) { + var clippedTextWidth = Math.max(0, tbb.width - commonBorderPad); + clipPath = + 'M-' + + (HOVERARROWSIZE + commonBorderPad) + + '-' + + halfHeight + + 'h-' + + clippedTextWidth + + 'V' + + halfHeight + + 'h' + + clippedTextWidth + + 'Z'; + + var ltx = tbb.width - lx + commonBorderPad; + svgTextUtils.positionText(ltext, ltx, lty); + + // shift each line (except the longest) so that start-of-line + // is always visible + if (anchor === 'end') { + ltext.selectAll('tspan').each(function () { + var s = d3.select(this); + var dummy = Drawing.tester.append('text').text(s.text()).call(Drawing.font, commonLabelFont); + var dummyBB = getBoundingClientRect(gd, dummy.node()); + if (Math.round(dummyBB.width) < Math.round(tbb.width)) { + s.attr('x', ltx - dummyBB.width); + } + dummy.remove(); + }); + } + } else { + svgTextUtils.positionText(ltext, sgn * (commonBorderPad + HOVERARROWSIZE), lty); + clipPath = null; } - } else { - svgTextUtils.positionText(ltext, sgn * (commonBorderPad + HOVERARROWSIZE), lty); - clipPath = null; + + var textClip = fullLayout._topclips.selectAll('#' + clipId).data(clipPath ? [0] : []); + textClip.enter().append('clipPath').attr('id', clipId).append('path'); + textClip.exit().remove(); + textClip.select('path').attr('d', clipPath); + Drawing.setClipUrl(ltext, clipPath ? clipId : null, gd); } - var textClip = fullLayout._topclips.selectAll('#' + clipId).data(clipPath ? [0] : []); - textClip.enter().append('clipPath').attr('id', clipId).append('path'); - textClip.exit().remove(); - textClip.select('path').attr('d', clipPath); - Drawing.setClipUrl(ltext, clipPath ? clipId : null, gd); + label.attr('transform', strTranslate(lx, ly)); + applyHoverShadow(label, commonShadow); } - - label.attr('transform', strTranslate(lx, ly)); - applyHoverShadow(label, commonShadow); }); // Show a single hover label @@ -1895,9 +1954,6 @@ function createHoverText(hoverData, opts) { var bgNameColor = d.bgNameColor ?? hoverlabel?.bgnamecolor; var nameColor = d.nameColor ?? hoverlabel?.namecolor; var nameBg = bgNameColor || Color.addOpacity(bgColor, 0.8); - // if(nameColor === undefined) { - // nameColor = bgNameColor ? Color.contrast(nameBg) : fallbackNameColor; - // } if(nameColor === undefined) { nameColor = Color.wcagContrast(fallbackNameColor, nameBg) < 4.5 @@ -1912,6 +1968,25 @@ function createHoverText(hoverData, opts) { var texts = getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g); var text = texts[0]; var name = texts[1]; + var hasName = !!(name && name !== text); + + var numsTex = svgTextUtils.isPureTex(text); + var nameTex = hasName && svgTextUtils.isPureTex(name); + var pendingLabelParts = (numsTex ? 1 : 0) + (nameTex ? 1 : 0); + var onLabelPartReady; + + if (pendingLabelParts) { + mathjaxPromises.push( + new Promise(function (resolve) { + onLabelPartReady = function () { + if (--pendingLabelParts === 0) { + if (g.node().parentNode) finalizePosition(); + resolve(); + } + }; + }) + ); + } // main label var tx = g @@ -1928,18 +2003,15 @@ function createHoverText(hoverData, opts) { shadow: d.fontShadow || fontShadow }) .text(text) - .attr('data-notex', 1) + .attr('data-notex', numsTex ? null : 1) .call(svgTextUtils.positionText, 0, 0) - .call(svgTextUtils.convertToTspans, gd); + .call(svgTextUtils.convertToTspans, gd, numsTex ? onLabelPartReady : undefined); var tx2 = g.select('text.name'); - var tx2width = 0; - var tx2height = 0; // secondary label for non-empty 'name' - if (name && name !== text) { + if (hasName) { g.select('rect').call(Color.fill, nameBg); - tx2.call(Drawing.font, { family: d.fontFamily || fontFamily, size: d.fontSize || fontSize, @@ -1952,90 +2024,107 @@ function createHoverText(hoverData, opts) { shadow: d.fontShadow || fontShadow }) .text(name) - .attr('data-notex', 1) + .attr('data-notex', nameTex ? null : 1) .call(svgTextUtils.positionText, 0, 0) - .call(svgTextUtils.convertToTspans, gd); - - var t2bb = getBoundingClientRect(gd, tx2.node()); - tx2width = t2bb.width + 2 * borderPad; - tx2height = t2bb.height + 2 * borderPad; + .call(svgTextUtils.convertToTspans, gd, nameTex ? onLabelPartReady : undefined); } else { tx2.remove(); g.select('rect').remove(); } - g.select('path').style({ - fill: numsColor, - stroke: contrastColor, - 'stroke-width': borderWidth + 'px' - }); + finalizePosition(); - var htx = d.xa._offset + (d.x0 + d.x1) / 2; - var hty = d.ya._offset + (d.y0 + d.y1) / 2; - var dx = Math.abs(d.x1 - d.x0); - var dy = Math.abs(d.y1 - d.y0); - - var tbb = getBoundingClientRect(gd, tx.node()); - var tbbWidth = tbb.width / fullLayout._invScaleX; - var tbbHeight = tbb.height / fullLayout._invScaleY; - - d.ty0 = (outerTop - tbb.top) / fullLayout._invScaleY; - d.bx = tbbWidth + 2 * borderPad; - d.by = Math.max(tbbHeight + 2 * borderPad, tx2height); - d.anchor = 'start'; - d.txwidth = tbbWidth; - d.tx2width = tx2width; - d.borderPad = borderPad; - d.offset = 0; - - var txTotalWidth = (tbbWidth + HOVERARROWSIZE + borderPad + tx2width) * fullLayout._invScaleX; - var anchorStartOK, anchorEndOK; - - if (rotateLabels) { - d.pos = htx; - anchorStartOK = hty + dy / 2 + txTotalWidth <= outerHeight; - anchorEndOK = hty - dy / 2 - txTotalWidth >= 0; - if ((d.idealAlign === 'top' || !anchorStartOK) && anchorEndOK) { - hty -= dy / 2; - d.anchor = 'end'; - } else if (anchorStartOK) { - hty += dy / 2; - d.anchor = 'start'; - } else { - d.anchor = 'middle'; + function finalizePosition() { + g.attr('transform', ''); + tx.call(svgTextUtils.positionText, 0, 0); + svgTextUtils.repositionMathGroup(tx); + if (hasName) { + tx2.call(svgTextUtils.positionText, 0, 0); + svgTextUtils.repositionMathGroup(tx2); } - d.crossPos = hty; - } else { - d.pos = hty; - anchorStartOK = htx + dx / 2 + txTotalWidth <= outerWidth; - anchorEndOK = htx - dx / 2 - txTotalWidth >= 0; - - if ((d.idealAlign === 'left' || !anchorStartOK) && anchorEndOK) { - htx -= dx / 2; - d.anchor = 'end'; - } else if (anchorStartOK) { - htx += dx / 2; - d.anchor = 'start'; + + g.select('path').style({ + fill: numsColor, + stroke: contrastColor, + 'stroke-width': borderWidth + 'px' + }); + + var htx = d.xa._offset + (d.x0 + d.x1) / 2; + var hty = d.ya._offset + (d.y0 + d.y1) / 2; + var dx = Math.abs(d.x1 - d.x0); + var dy = Math.abs(d.y1 - d.y0); + + var tx2width = 0; + var tx2height = 0; + if (hasName) { + var t2bb = getHoverTextBBox(gd, tx2); + tx2width = t2bb.width + 2 * borderPad; + tx2height = t2bb.height + 2 * borderPad; + } + + var tbb = getHoverTextBBox(gd, tx); + var tbbWidth = tbb.width / fullLayout._invScaleX; + var tbbHeight = tbb.height / fullLayout._invScaleY; + + d.ty0 = (outerTop - tbb.top) / fullLayout._invScaleY; + d.bx = tbbWidth + 2 * borderPad; + d.by = Math.max(tbbHeight + 2 * borderPad, tx2height); + d.anchor = 'start'; + d.txwidth = tbbWidth; + d.tx2width = tx2width; + d.borderPad = borderPad; + d.offset = 0; + + var txTotalWidth = (tbbWidth + HOVERARROWSIZE + borderPad + tx2width) * fullLayout._invScaleX; + var anchorStartOK, anchorEndOK; + + if (rotateLabels) { + d.pos = htx; + anchorStartOK = hty + dy / 2 + txTotalWidth <= outerHeight; + anchorEndOK = hty - dy / 2 - txTotalWidth >= 0; + if ((d.idealAlign === 'top' || !anchorStartOK) && anchorEndOK) { + hty -= dy / 2; + d.anchor = 'end'; + } else if (anchorStartOK) { + hty += dy / 2; + d.anchor = 'start'; + } else { + d.anchor = 'middle'; + } + d.crossPos = hty; } else { - d.anchor = 'middle'; + d.pos = hty; + anchorStartOK = htx + dx / 2 + txTotalWidth <= outerWidth; + anchorEndOK = htx - dx / 2 - txTotalWidth >= 0; + + if ((d.idealAlign === 'left' || !anchorStartOK) && anchorEndOK) { + htx -= dx / 2; + d.anchor = 'end'; + } else if (anchorStartOK) { + htx += dx / 2; + d.anchor = 'start'; + } else { + d.anchor = 'middle'; - var txHalfWidth = txTotalWidth / 2; - var overflowR = htx + txHalfWidth - outerWidth; - var overflowL = htx - txHalfWidth; - if (overflowR > 0) htx -= overflowR; - if (overflowL < 0) htx += -overflowL; + var txHalfWidth = txTotalWidth / 2; + var overflowR = htx + txHalfWidth - outerWidth; + var overflowL = htx - txHalfWidth; + if (overflowR > 0) htx -= overflowR; + if (overflowL < 0) htx += -overflowL; + } + d.crossPos = htx; } - d.crossPos = htx; - } - tx.attr('text-anchor', d.anchor); - if (tx2width) tx2.attr('text-anchor', d.anchor); - g.attr('transform', strTranslate(htx, hty) + (rotateLabels ? strRotate(YANGLE) : '')); + tx.attr('text-anchor', d.anchor); + if (tx2width) tx2.attr('text-anchor', d.anchor); + g.attr('transform', strTranslate(htx, hty) + (rotateLabels ? strRotate(YANGLE) : '')); + } }); return { hoverLabels: hoverLabels, - commonLabelBoundingBox: commonLabelRect + commonLabelBoundingBox: commonLabelRect, + mathjaxPromise: mathjaxPromises.length ? Promise.all(mathjaxPromises) : null }; } diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 8fdc5b5afc2..b85a462bc2b 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -733,7 +733,7 @@ function setupTitleToggle(scrollBox, gd, legendObj, legendId) { function textLayout(s, g, gd, legendObj, aTitle) { - if(legendObj._inHover) s.attr('data-notex', true); // do not process MathJax for unified hover + if(legendObj._inHover) s.attr('data-notex', svgTextUtils.isPureTex(s.text()) ? null : true); svgTextUtils.convertToTspans(s, gd, function() { computeTextDimensions(g, gd, legendObj, aTitle); }); diff --git a/src/lib/svg_text_utils.js b/src/lib/svg_text_utils.js index e2e222b2648..21a0aa17beb 100644 --- a/src/lib/svg_text_utils.js +++ b/src/lib/svg_text_utils.js @@ -24,6 +24,98 @@ var FIND_TEX = /([^$]*)([$]+[^$]*[$]+)([^$]*)/; const matchTex = (str) => str ? str.match(FIND_TEX) : null; exports.matchTex = matchTex; +/** + * Checks whether a string is *entirely* a single tex expression, with no + * literal text before or after the $...$ delimiters. + * + * @param {string} str: the string to check for tex + * @return {boolean} true if the whole string is one tex expression + */ +const isPureTex = (str) => { + var tex = matchTex(str); + return !!tex && !tex[1] && !tex[3]; +}; +exports.isPureTex = isPureTex; + +/** + * Positions a MathJax-typeset group against its source node's x/y + * and text-anchor. + * + * @param {d3 selection} _context: the source element + * @param {string} svgClass: the '-math' class used on newSvg + * @param {d3 selection} mathjaxGroup: the '-group' wrapper + * @param {d3 selection} newSvg: the typeset , already sized + * @param {string} textAnchor: _context's text-anchor to position against + * @param {number} fontSize: _context's font size, for the baseline offset + * @param {number} [width0]: the SVG's own reported width, to detect the + * Firefox v82+ overflow bug (see link below); omit to skip that check + * @param {number} [height0]: same, for height + */ +function positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, width0, height0) { + var g = newSvg.select('g'); + var bb = g.node().getBoundingClientRect(); + var w = bb.width; + var h = bb.height; + var w0 = width0 === undefined ? w : width0; + var h0 = height0 === undefined ? h : height0; + + if(w > w0 || h > h0) { + // this happen in firefox v82+ | see https://bugzilla.mozilla.org/show_bug.cgi?id=1709251 addressed + // temporary fix: + newSvg.style('overflow', 'hidden'); + bb = newSvg.node().getBoundingClientRect(); + w = bb.width; + h = bb.height; + } + + var x = +_context.attr('x'); + var y = +_context.attr('y'); + + // font baseline is about 1/4 fontSize below centerline + var textHeight = fontSize || _context.node().getBoundingClientRect().height; + var dy = -textHeight / 4; + + if(svgClass[0] === 'y') { + mathjaxGroup.attr({ + transform: 'rotate(' + [-90, x, y] + + ')' + strTranslate(-w / 2, dy - h / 2) + }); + } else if(svgClass[0] === 'l') { + y = dy - h / 2; + } else if(svgClass[0] === 'a' && svgClass.indexOf('atitle') !== 0) { + x = 0; + y = dy; + } else { + x = x - w * ( + textAnchor === 'middle' ? 0.5 : + textAnchor === 'end' ? 1 : 0 + ); + y = y + dy - h / 2; + } + + newSvg.attr({ + x: x, + y: y + }); +} + +/** + * Removes an unsafe href/xlink:href from every MathJax's \href{url}{...} + * macro produced inside newSvg. + * + * @param {d3 selection} newSvg: the typeset , already inserted into + * the document (or a fragment), so selectAll can walk its descendants + */ +function sanitizeMathJaxLinks(newSvg) { + newSvg.selectAll('a').each(function() { + var a = d3.select(this); + ['href', 'xlink:href'].forEach(function(attrName) { + var href = a.attr(attrName); + if(href) a.attr(attrName, sanitizeHref(href) || null); + }); + }); +} + exports.convertToTspans = function(_context, gd, _callback) { var str = _context.text(); @@ -113,6 +205,8 @@ exports.convertToTspans = function(_context, gd, _callback) { mathjaxGroup.node().appendChild(newSvg.node()); + sanitizeMathJaxLinks(newSvg); + // stitch the glyph defs if(_glyphDefs && _glyphDefs.node()) { newSvg.node().insertBefore(_glyphDefs.node().cloneNode(true), @@ -137,48 +231,7 @@ exports.convertToTspans = function(_context, gd, _callback) { var g = newSvg.select('g'); g.attr({fill: fill, stroke: fill}); - var bb = g.node().getBoundingClientRect(); - var w = bb.width; - var h = bb.height; - - if(w > w0 || h > h0) { - // this happen in firefox v82+ | see https://bugzilla.mozilla.org/show_bug.cgi?id=1709251 addressed - // temporary fix: - newSvg.style('overflow', 'hidden'); - bb = newSvg.node().getBoundingClientRect(); - w = bb.width; - h = bb.height; - } - - var x = +_context.attr('x'); - var y = +_context.attr('y'); - - // font baseline is about 1/4 fontSize below centerline - var textHeight = fontSize || _context.node().getBoundingClientRect().height; - var dy = -textHeight / 4; - - if(svgClass[0] === 'y') { - mathjaxGroup.attr({ - transform: 'rotate(' + [-90, x, y] + - ')' + strTranslate(-w / 2, dy - h / 2) - }); - } else if(svgClass[0] === 'l') { - y = dy - h / 2; - } else if(svgClass[0] === 'a' && svgClass.indexOf('atitle') !== 0) { - x = 0; - y = dy; - } else { - x = x - w * ( - textAnchor === 'middle' ? 0.5 : - textAnchor === 'end' ? 1 : 0 - ); - y = y + dy - h / 2; - } - - newSvg.attr({ - x: x, - y: y - }); + positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize, w0, h0); if(_callback) _callback.call(_context, mathjaxGroup); resolve(mathjaxGroup); @@ -189,6 +242,45 @@ exports.convertToTspans = function(_context, gd, _callback) { return _context; }; +/** + * Repositions an existing MathJax-typeset group, previously inserted by + * convertToTspans, to match its source node's *current* x/y and + * text-anchor. + * + * @param {d3 selection} _context: the source element + */ +exports.repositionMathGroup = function(_context) { + if(_context.empty()) return; + + var svgClass = (_context.attr('class') ? _context.attr('class').split(' ')[0] : 'text') + '-math'; + var parent = d3.select(_context.node().parentNode); + var mathjaxGroup = parent.select('g.' + svgClass + '-group'); + var newSvg = mathjaxGroup.select('svg.' + svgClass); + if(mathjaxGroup.empty() || newSvg.empty()) return; + + var textAnchor = _context.attr('text-anchor'); + var fontSize = parseInt(_context.node().style.fontSize, 10); + positionMathGroup(_context, svgClass, mathjaxGroup, newSvg, textAnchor, fontSize); +}; + +/** + * Finds the node whose bounding box represents a source element's + * rendered content: the typeset MathJax group if convertToTspans replaced + * the text with one (the source itself is display:none in that case), or + * the node otherwise. + * + * @param {d3 selection} textSel: the source element + * @return {?Element} the node to measure, or null if textSel is empty + */ +exports.getMathOrTextNode = function(textSel) { + var node = textSel.node(); + if(!node) return null; + + var svgClass = (textSel.attr('class') ? textSel.attr('class').split(' ')[0] : 'text') + '-math'; + var mathGroup = d3.select(node.parentNode).select('g.' + svgClass + '-group'); + return mathGroup.empty() ? node : mathGroup.node(); +}; + // MathJax diff --git a/test/jasmine/bundle_tests/mathjax_test.js b/test/jasmine/bundle_tests/mathjax_test.js index 21704fa8130..7ae42087162 100644 --- a/test/jasmine/bundle_tests/mathjax_test.js +++ b/test/jasmine/bundle_tests/mathjax_test.js @@ -1,9 +1,12 @@ var Plotly = require('../../../lib/index'); +var Fx = require('../../../src/components/fx'); +var Lib = require('../../../src/lib'); var d3Select = require('../../strict-d3').select; var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var loadScript = require('../assets/load_script'); +var delay = require('../assets/delay'); // eslint-disable-next-line no-undef var mathjaxVersion = __karma__.config.mathjaxVersion; @@ -16,7 +19,7 @@ describe('Test MathJax v' + mathjaxVersion + ':', function() { // TODO: `?config=` is not needed for MathJax v3 and onward, // should we adjust these tests? - + // N.B. we have to load MathJax "dynamically" as Karma // does not undefined the MathJax's `?config=` parameter. // @@ -202,5 +205,425 @@ describe('Test MathJax v' + mathjaxVersion + ':', function() { }) .then(done, done.fail); }); + + it('should strip a javascript: url from a tex \\href, but keep a safe one', function(done) { + Plotly.newPlot(gd, { + data: [{x: [1, 2, 3], y: [1, 2, 3]}], + layout: { + title: {text: '$\\href{javascript:alert(1)}{unsafe}$'}, + xaxis: {title: {text: '$\\href{https://plotly.com}{safe}$'}} + } + }) + .then(function() { + var gd3 = d3Select(gd); + + var unsafeLink = gd3.select('.gtitle-math-group a'); + expect(unsafeLink.size()).toBe(1, 'title link exists'); + expect(unsafeLink.attr('href')).toBe(null, 'javascript: url stripped'); + + var safeLink = gd3.select('.g-xtitle .xtitle-math-group a'); + expect(safeLink.size()).toBe(1, 'axis title link exists'); + expect(safeLink.attr('href')).toBe('https://plotly.com', 'https: url kept'); + }) + .then(done, done.fail); + }); + + it('should strip a javascript: url from a tex \\href in an annotation', function(done) { + Plotly.newPlot(gd, { + data: [{x: [1, 2, 3], y: [1, 2, 3]}], + layout: { + annotations: [{ + x: 2, + y: 2, + showarrow: false, + text: '$\\href{javascript:alert(1)}{unsafe}$' + }] + } + }) + .then(function() { + var gd3 = d3Select(gd); + var link = gd3.select('.annotation-text-math-group a'); + + expect(link.size()).toBe(1, 'annotation link exists'); + expect(link.attr('href')).toBe(null, 'javascript: url stripped'); + }) + .then(done, done.fail); + }); + + it('should strip a javascript: url from a tex \\href in non-hover legend text', function(done) { + Plotly.newPlot(gd, { + data: [{ + x: [1, 2, 3], + y: [1, 2, 3], + name: '$\\href{javascript:alert(1)}{unsafe}$' + }], + layout: {showlegend: true} + }) + .then(function() { + var gd3 = d3Select(gd); + var link = gd3.select('.legendtext-math-group a'); + + expect(link.size()).toBe(1, 'legend link exists'); + expect(link.attr('href')).toBe(null, 'javascript: url stripped'); + }) + .then(done, done.fail); + }); + }); + + describe('Test hover tex rendering:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function _hover(xpx, ypx) { + // 'xy' is the subplot id, not the hovermode -- hovermode comes + // from the figure's own layout.hovermode. + Fx.hover(gd, {xpx: xpx, ypx: ypx}, 'xy'); + Lib.clearThrottle(); + } + + it('should hand a pure-tex hover label off to MathJax, sized to its final content', function(done) { + Plotly.newPlot(gd, { + data: [{ + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + text: ['$\\alpha^2 + \\beta^2 = \\gamma^2$', 'b', 'c'], + hoverinfo: 'text' + }], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + xaxis: {range: [0, 4]}, + yaxis: {range: [0, 4]} + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + var gd3 = d3Select(gd); + var mathGroup = gd3.select('g.hovertext .nums-math-group'); + + expect(mathGroup.size()).toBe(1, 'hover label math group'); + expect(mathGroup.attr('data-unformatted')).toBe('$\\alpha^2 + \\beta^2 = \\gamma^2$'); + + // A rendered formula this long is much wider than the + // ~15px placeholder box a not-yet-typeset label starts at; + // this is what distinguishes a corrected box from one still + // sized off the pre-MathJax placeholder measurement. + var bg = gd3.select('g.hovertext > path').node().getBBox(); + expect(bg.width).toBeGreaterThan(50, 'hover box width, once corrected for the real label size'); + + // The path/text/math-group coordinates must all be real + // numbers -- this is what distinguishes a corrected box + // from one still carrying NaN from an unresolved layout. + expect(gd3.select('g.hovertext > path').attr('d')).not.toContain('NaN'); + expect(mathGroup.select('svg').attr('x')).not.toBe('NaN'); + expect(mathGroup.select('svg').attr('y')).not.toBe('NaN'); + }) + .then(done, done.fail); + }); + + it('should strip a javascript: url from a tex \\href in a hover label', function(done) { + Plotly.newPlot(gd, { + data: [{ + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + text: ['$\\href{javascript:alert(document.cookie)}{click me}$', 'b', 'c'], + hoverinfo: 'text' + }], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + xaxis: {range: [0, 4]}, + yaxis: {range: [0, 4]} + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + var gd3 = d3Select(gd); + var link = gd3.select('g.hovertext .nums-math-group a'); + + expect(link.size()).toBe(1, 'link exists'); + expect(link.attr('href')).toBe(null, 'javascript: url stripped'); + }) + .then(done, done.fail); + }); + + it('should leave a mixed tex/plain-text hover label as literal text', function(done) { + Plotly.newPlot(gd, { + data: [{ + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + text: ['Value: $\\alpha$ units', 'b', 'c'], + hoverinfo: 'text' + }], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + xaxis: {range: [0, 4]}, + yaxis: {range: [0, 4]} + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + var gd3 = d3Select(gd); + var numsText = gd3.select('g.hovertext text.nums'); + + expect(gd3.select('g.hovertext .nums-math-group').size()).toBe(0, 'no math group'); + expect(numsText.text()).toBe('Value: $\\alpha$ units'); + expect(numsText.node().style.display).not.toBe('none'); + }) + .then(done, done.fail); + }); + + it('should hand a pure-tex common hover label off to MathJax, sized to its final content', function(done) { + Plotly.newPlot(gd, { + data: [{ + type: 'scatter', + mode: 'markers', + x: ['$\\alpha^2 + \\beta^2 = \\gamma^2$', 'b', 'c'], + y: [1, 2, 3] + }], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + // category positions are 0, 1, 2; this range puts + // category 0 at pixel 125, matching the other two tests + xaxis: {range: [-1, 3]}, + yaxis: {range: [0, 4]}, + hovermode: 'x' + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + var gd3 = d3Select(gd); + var mathGroup = gd3.select('g.axistext .text-math-group'); + + expect(mathGroup.size()).toBe(1, 'common label math group'); + expect(mathGroup.attr('data-unformatted')).toBe('$\\alpha^2 + \\beta^2 = \\gamma^2$'); + + var bg = gd3.select('g.axistext > path').node().getBBox(); + expect(bg.width).toBeGreaterThan(50, 'common label width, once corrected for the real label size'); + + expect(gd3.select('g.axistext > path').attr('d')).not.toContain('NaN'); + expect(mathGroup.select('svg').attr('x')).not.toBe('NaN'); + expect(mathGroup.select('svg').attr('y')).not.toBe('NaN'); + }) + .then(done, done.fail); + }); + + it('should hand both the value and the name off to MathJax when both are tex', function(done) { + Plotly.newPlot(gd, { + data: [{ + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + text: ['$\\alpha^2$', 'b', 'c'], + name: '$\\beta^2$', + hoverinfo: 'text+name' + }], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + xaxis: {range: [0, 4]}, + yaxis: {range: [0, 4]} + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + var gd3 = d3Select(gd); + var numsMathGroup = gd3.select('g.hovertext .nums-math-group'); + var nameMathGroup = gd3.select('g.hovertext .name-math-group'); + + expect(numsMathGroup.size()).toBe(1, 'value math group'); + expect(numsMathGroup.attr('data-unformatted')).toBe('$\\alpha^2$'); + expect(nameMathGroup.size()).toBe(1, 'name math group'); + expect(nameMathGroup.attr('data-unformatted')).toBe('$\\beta^2$'); + + expect(gd3.select('g.hovertext > path').attr('d')).not.toContain('NaN'); + }) + .then(done, done.fail); + }); + + it('should size and position two simultaneous tex hover labels correctly', function(done) { + Plotly.newPlot(gd, { + data: [ + {type: 'scatter', mode: 'markers', x: [1], y: [1], text: ['$\\alpha^2$'], hoverinfo: 'text', name: 'A'}, + {type: 'scatter', mode: 'markers', x: [1], y: [1], text: ['$\\beta^2$'], hoverinfo: 'text', name: 'B'} + ], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + xaxis: {range: [0, 4]}, + yaxis: {range: [0, 4]}, + hovermode: 'x' + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + var gd3 = d3Select(gd); + var hoverTexts = gd3.selectAll('g.hovertext'); + var mathGroups = gd3.selectAll('g.hovertext .nums-math-group'); + + expect(hoverTexts.size()).toBe(2, 'two hover labels, both identical points'); + expect(mathGroups.size()).toBe(2, 'both labels typeset'); + + var unformatted = []; + mathGroups.each(function() { unformatted.push(this.getAttribute('data-unformatted')); }); + expect(unformatted.sort()).toEqual(['$\\alpha^2$', '$\\beta^2$']); + + hoverTexts.select('path').each(function() { + expect(d3Select(this).attr('d')).not.toContain('NaN'); + }); + + // hoverAvoidOverlaps must have pushed the two labels apart + var tops = []; + hoverTexts.select('path').each(function() { + tops.push(this.getBoundingClientRect().top); + }); + expect(tops[0]).not.toBeNaN(); + expect(tops[1]).not.toBeNaN(); + expect(Math.abs(tops[0] - tops[1])).toBeGreaterThan(5, 'labels pushed apart, not stacked on each other'); + }) + .then(done, done.fail); + }); + + it('should hand a unified hover title off to MathJax when set through unifiedhovertitle', function(done) { + Plotly.newPlot(gd, { + data: [{ + type: 'scatter', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3] + }], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + xaxis: {range: [0, 4], unifiedhovertitle: {text: '$\\alpha^2$'}}, + yaxis: {range: [0, 4]}, + hovermode: 'x unified' + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + var gd3 = d3Select(gd); + var mathGroup = gd3.select('g.legend [class*="titletext-math-group"]'); + + expect(mathGroup.size()).toBe(1, 'unified hover title math group'); + expect(mathGroup.attr('data-unformatted')).toBe('$\\alpha^2$'); + }) + .then(done, done.fail); + }); + + it('should still typeset a pure-tex unified hover row after hovering a plain-text row', function(done) { + Plotly.newPlot(gd, { + data: [{ + type: 'scatter', + mode: 'markers', + x: [1, 2], + y: [1, 2], + marker: {size: 20}, + hovertemplate: ['plain', '$\\alpha$'] + }], + layout: { + width: 500, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0}, + xaxis: {range: [0, 4]}, + yaxis: {range: [0, 4]}, + hovermode: 'x unified', + hoverdistance: -1 + } + }) + .then(function() { + _hover(125, 300); + return delay(30)(); + }) + .then(function() { + expect(d3Select(gd).selectAll('g.legend [class*="legendtext-math-group"]').size()) + .toBe(0, 'plain-text row is not typeset'); + + _hover(250, 200); + return delay(30)(); + }) + .then(function() { + var mathGroup = d3Select(gd).select('g.legend [class*="legendtext-math-group"]'); + + expect(mathGroup.size()).toBe(1, 'pure-tex row typeset after the plain-text one'); + expect(mathGroup.attr('data-unformatted')).toBe('$\\alpha$'); + }) + .then(done, done.fail); + }); + + it('should hand a pure-tex Fx.loneHover label off to MathJax, sized to its final content', function(done) { + Plotly.newPlot(gd, { + data: [{type: 'scatter', mode: 'markers', x: [1], y: [1]}], + layout: {width: 500, height: 400, margin: {l: 0, t: 0, r: 0, b: 0}} + }) + .then(function() { + var fullLayout = gd._fullLayout; + Fx.loneHover({ + x: 100, + y: 100, + text: '$\\alpha^2 + \\beta^2 = \\gamma^2$', + color: 'blue' + }, { + gd: gd, + container: fullLayout._hoverlayer.node(), + outerContainer: fullLayout._paper.node() + }); + return delay(30)(); + }) + .then(function() { + var gd3 = d3Select(gd); + var mathGroup = gd3.select('g.hovertext .nums-math-group'); + + expect(mathGroup.size()).toBe(1, 'loneHover math group'); + expect(mathGroup.attr('data-unformatted')).toBe('$\\alpha^2 + \\beta^2 = \\gamma^2$'); + + var bg = gd3.select('g.hovertext > path').node().getBBox(); + expect(bg.width).toBeGreaterThan(50, 'loneHover box width, once corrected for the real label size'); + expect(gd3.select('g.hovertext > path').attr('d')).not.toContain('NaN'); + }) + .then(done, done.fail); + }); }); -}); +}); \ No newline at end of file