diff --git a/draftlogs/8027_fix.md b/draftlogs/8027_fix.md new file mode 100644 index 00000000000..65c4b5b2210 --- /dev/null +++ b/draftlogs/8027_fix.md @@ -0,0 +1 @@ +- Resolve the per-point marker color for hover labels in `scattergl`, `quiver` traces [[#8027](https://github.com/plotly/plotly.js/pull/8027)] diff --git a/src/components/color/index.js b/src/components/color/index.js index 4f500d6be69..8460b966980 100644 --- a/src/components/color/index.js +++ b/src/components/color/index.js @@ -64,6 +64,13 @@ const snap = (c) => ({ ...c, r: snap01(c.r), g: snap01(c.g), b: snap01(c.b) }); const formatRgb = (c) => culoriFormatRgb(snap(c)); const formatHex = (c) => culoriFormatHex(snap(c)); +const describe = (v) => { + if (typeof v === 'string') return `"${v}"`; + if (isArrayOrTypedArray(v)) return `${v.constructor?.name ?? 'Array'}(${v.length})`; + if (typeof v === 'object') return v.constructor?.name ?? 'Object'; + return `${typeof v} ${v}`; +}; + /** * Parse a color specifier string and return it as a culori rgb color object. * @@ -74,7 +81,7 @@ const formatHex = (c) => culoriFormatHex(snap(c)); const parse = (cstr, silent) => { const c = toColor(cstr); if (!c) { - if (!silent && cstr != null) warn(`Invalid color specifier: "${cstr}". Defaulting to "#000"`); + if (!silent && cstr != null) warn(`Invalid color specifier: ${describe(cstr)}. Defaulting to "#000"`); return BLACK; } // `toRgb` omits alpha when it's 1; make sure it's added since we expect it diff --git a/src/traces/quiver/calc.js b/src/traces/quiver/calc.js index 22f7cb21772..1aeb1864c92 100644 --- a/src/traces/quiver/calc.js +++ b/src/traces/quiver/calc.js @@ -94,6 +94,11 @@ module.exports = function calc(gd, trace) { if(hasMarkerColorArray) { var ci = markerColor[i]; + + // Keep the per-point color under the name that the shared hover + // code reads. Without it, `getTraceColor` reads the whole array. + cdi.mc = ci; + if(isNumeric(ci)) { if(ci < cMin) cMin = ci; if(ci > cMax) cMax = ci; diff --git a/src/traces/quiver/style.js b/src/traces/quiver/style.js index 05e9843f097..3d2b00548c9 100644 --- a/src/traces/quiver/style.js +++ b/src/traces/quiver/style.js @@ -24,7 +24,12 @@ function colorscaleStroke(paths, trace) { var vVal = (trace.v && trace.v[cdi.i]) || 0; value = Math.sqrt(uVal * uVal + vVal * vVal); } - return colorFunc(value); + + // Keep the mapped color on the point, under the name that the shared + // hover code reads, so that the hover label matches the arrow + cdi.mcc = colorFunc(value); + + return cdi.mcc; }); } diff --git a/src/traces/scatter/get_trace_color.js b/src/traces/scatter/get_trace_color.js index d20e37affbc..ad496975e01 100644 --- a/src/traces/scatter/get_trace_color.js +++ b/src/traces/scatter/get_trace_color.js @@ -16,7 +16,7 @@ module.exports = function getTraceColor(trace, di) { } else if(trace.mode === 'none') { return trace.fill ? trace.fillcolor : ''; } else { - var mc = di.mcc || (trace.marker || {}).color; + var mc = di.mcc || di.mc || (trace.marker || {}).color; var mlc = di.mlcc || ((trace.marker || {}).line || {}).color; tc = (mc && Color.opacity(mc)) ? mc : diff --git a/src/traces/scattergl/hover.js b/src/traces/scattergl/hover.js index 6a660bb242b..b153fb65434 100644 --- a/src/traces/scattergl/hover.js +++ b/src/traces/scattergl/hover.js @@ -3,6 +3,7 @@ var Registry = require('../../registry'); var Lib = require('../../lib'); var getTraceColor = require('../scatter/get_trace_color'); +const Drawing = require('../../components/drawing'); function hoverPoints(pointData, xval, yval, hovermode) { var cd = pointData.cd; @@ -144,6 +145,9 @@ function calcHover(pointData, x, y, trace) { di.mx = Lib.isArrayOrTypedArray(marker.symbol) ? marker.symbol[id] : marker.symbol; di.ma = Lib.isArrayOrTypedArray(marker.angle) ? marker.angle[id] : marker.angle; di.mc = Lib.isArrayOrTypedArray(marker.color) ? marker.color[id] : marker.color; + + // scattergl has no render step that sets `mcc`, so map the per-point color here + di.mcc = Drawing.tryColorscale(marker, '')(di.mc); } var line = marker && marker.line; diff --git a/test/jasmine/tests/gl2d_click_test.js b/test/jasmine/tests/gl2d_click_test.js index 18f76a098f5..0c59bfe2c06 100644 --- a/test/jasmine/tests/gl2d_click_test.js +++ b/test/jasmine/tests/gl2d_click_test.js @@ -292,6 +292,53 @@ describe('Test hover and click interactions', function() { .then(done, done.fail); }); + it('@gl should use the per-point marker color for the hover label', async () => { + await Plotly.newPlot( + gd, + [ + { + type: 'scattergl', + mode: 'markers', + x: [1, 2, 3], + y: [1, 2, 3], + marker: { + color: ['#1767C2', '#FF4136', '#2ECC40'], + size: 20 + } + } + ], + { + hovermode: 'closest', + width: 400, + height: 400, + xaxis: { showspikes: true }, + yaxis: { showspikes: true } + } + ); + + Plotly.Fx.hover(gd, { xval: 2, yval: 2 }, 'xy'); + Lib.clearThrottle(); + + const withArray = d3Select('g.hovertext path').node(); + expect(window.getComputedStyle(withArray).fill).toBe('rgb(255, 65, 54)', 'color array'); + + const spikeStrokes = Array.from(document.querySelectorAll('line.spikeline')).map( + (line) => window.getComputedStyle(line).stroke + ); + expect(spikeStrokes.filter((stroke) => stroke === 'rgb(255, 65, 54)').length).toBe(2, 'spikelines'); + + await Plotly.restyle(gd, { + 'marker.color': [[0, 5, 10]], + 'marker.colorscale': 'Viridis' + }); + + Plotly.Fx.hover(gd, { xval: 2, yval: 2 }, 'xy'); + Lib.clearThrottle(); + + const withColorscale = d3Select('g.hovertext path').node(); + expect(window.getComputedStyle(withColorscale).fill).toBe('rgb(33, 145, 140)', 'colorscale'); + }); + it('@gl should show correct label for scattergl when hovertext is set', function(done) { var _mock = Lib.extendDeep({}, mock1); _mock.data[0].hovertext = 'text'; diff --git a/test/jasmine/tests/quiver_test.js b/test/jasmine/tests/quiver_test.js index 2a544383338..1ab4482c549 100644 --- a/test/jasmine/tests/quiver_test.js +++ b/test/jasmine/tests/quiver_test.js @@ -270,6 +270,41 @@ describe('Test quiver interactions', function() { .then(done, done.fail); }); + it('should use the per-point arrow color for the hover label', async () => { + const fig = { + data: [ + { + type: 'quiver', + x: [1, 2, 3], + y: [1, 2, 3], + u: [1, 0, -1], + v: [0, 1, 0], + marker: { + color: [0, 5, 10], + colorscale: 'Viridis', + showscale: false + } + } + ], + layout: { + margin: { l: 0, t: 0, r: 0, b: 0 }, + width: 400, + height: 400 + } + }; + + await Plotly.newPlot(gd, fig); + + mouseEvent('mousemove', 200, 200); + await delay(20)(); + + const label = document.querySelector('g.hovertext path'); + const arrow = gd.querySelectorAll('g.trace.quiver path.js-line')[1]; + + expect(window.getComputedStyle(label).fill).toBe('rgb(33, 145, 140)', 'hover label'); + expect(window.getComputedStyle(arrow).stroke).toBe('rgb(33, 145, 140)', 'arrow'); + }); + it('should render multiple quiver traces', function(done) { Plotly.newPlot(gd, [{ type: 'quiver', @@ -344,4 +379,3 @@ describe('Test quiver interactions', function() { .then(done, done.fail); }); }); -