From 14b31a1e4e86b4667a8a0fc3226151e7dc738e4f Mon Sep 17 00:00:00 2001 From: archmoj Date: Fri, 21 May 2021 10:26:33 -0400 Subject: [PATCH 1/7] provide description for hover on other positions --- src/plots/template_attributes.js | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/plots/template_attributes.js b/src/plots/template_attributes.js index 70a0e536d87..0670a11edfe 100644 --- a/src/plots/template_attributes.js +++ b/src/plots/template_attributes.js @@ -4,15 +4,20 @@ var docs = require('../constants/docs'); var FORMAT_LINK = docs.FORMAT_LINK; var DATE_FORMAT_LINK = docs.DATE_FORMAT_LINK; -var templateFormatStringDescription = [ - 'Variables are inserted using %{variable}, for example "y: %{y}".', - 'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".', - FORMAT_LINK, - 'for details on the formatting syntax.', - 'Dates are formatted using d3-time-format\'s syntax %{variable|d3-time-format}, for example "Day: %{2019-01-01|%A}".', - DATE_FORMAT_LINK, - 'for details on the date formatting syntax.' -].join(' '); +function templateFormatStringDescription(opts) { + var supportOther = opts && opts.supportOther; + + return [ + 'Variables are inserted using %{variable},', + 'for example "y: %{y}".' + (supportOther ? ' as well as %{xother} when positions do not match' : ''), + 'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".', + FORMAT_LINK, + 'for details on the formatting syntax.', + 'Dates are formatted using d3-time-format\'s syntax %{variable|d3-time-format}, for example "Day: %{2019-01-01|%A}".', + DATE_FORMAT_LINK, + 'for details on the date formatting syntax.' + ].join(' '); +} function describeVariables(extra) { var descPart = extra.description ? ' ' + extra.description : ''; @@ -45,7 +50,7 @@ exports.hovertemplateAttrs = function(opts, extra) { description: [ 'Template string used for rendering the information that appear on hover box.', 'Note that this will override `hoverinfo`.', - templateFormatStringDescription, + templateFormatStringDescription({supportOther: true}), 'The variables available in `hovertemplate` are the ones emitted as event data described at this link https://plotly.com/javascript/plotlyjs-events/#event-data.', 'Additionally, every attributes that can be specified per-point (the ones that are `arrayOk: true`) are available.', descPart, @@ -74,7 +79,7 @@ exports.texttemplateAttrs = function(opts, extra) { description: [ 'Template string used for rendering the information text that appear on points.', 'Note that this will override `textinfo`.', - templateFormatStringDescription, + templateFormatStringDescription(), 'Every attributes that can be specified per-point (the ones that are `arrayOk: true`) are available.', descPart ].join(' ') From 9841646ac28491e777e4fffb4847e1983b050601 Mon Sep 17 00:00:00 2001 From: archmoj Date: Fri, 28 May 2021 16:58:44 -0400 Subject: [PATCH 2/7] implement (x|y)other hovertemplate --- src/components/fx/hover.js | 23 +++++++++++++------- src/lib/index.js | 44 +++++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 57541162afa..4cc6d19e802 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -1270,14 +1270,17 @@ function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) { name = plainText(d.name, d.nameLength); } + var h0 = hovermode.charAt(0); + var h1 = h0 === 'x' ? 'y' : 'x'; + if(d.zLabel !== undefined) { if(d.xLabel !== undefined) text += 'x: ' + d.xLabel + '
'; if(d.yLabel !== undefined) text += 'y: ' + d.yLabel + '
'; if(d.trace.type !== 'choropleth' && d.trace.type !== 'choroplethmapbox') { text += (text ? 'z: ' : '') + d.zLabel; } - } else if(showCommonLabel && d[hovermode.charAt(0) + 'Label'] === t0) { - text = d[(hovermode.charAt(0) === 'x' ? 'y' : 'x') + 'Label'] || ''; + } else if(showCommonLabel && d[h0 + 'Label'] === t0) { + text = d[h1 + 'Label'] || ''; } else if(d.xLabel === undefined) { if(d.yLabel !== undefined && d.trace.type !== 'scattercarpet') { text = d.yLabel; @@ -1306,16 +1309,20 @@ function getHoverLabelText(d, showCommonLabel, hovermode, fullLayout, t0, g) { } // hovertemplate - var d3locale = fullLayout._d3locale; var hovertemplate = d.hovertemplate || false; - var hovertemplateLabels = d.hovertemplateLabels || d; - var eventData = d.eventData[0] || {}; if(hovertemplate) { + var labels = d.hovertemplateLabels || d; + + if(d[h0 + 'Label'] !== t0) { + labels[h0 + 'other'] = labels[h0 + 'Val']; + labels[h0 + 'otherLabel'] = labels[h0 + 'Label']; + } + text = Lib.hovertemplateString( hovertemplate, - hovertemplateLabels, - d3locale, - eventData, + labels, + fullLayout._d3locale, + d.eventData[0] || {}, d.trace._meta ); diff --git a/src/lib/index.js b/src/lib/index.js index e696a2068ab..49e3430782a 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -1043,21 +1043,33 @@ function templateFormatString(string, labels, d3locale) { // just in case it speeds things up *slightly*: var getterCache = {}; + return string.replace(lib.TEMPLATE_STRING_REGEX, function(match, key, format) { - var obj, value, i; - for(i = 3; i < args.length; i++) { - obj = args[i]; - if(!obj) continue; - if(obj.hasOwnProperty(key)) { - value = obj[key]; - break; - } + var isOther = + key === 'xother' || + key === 'yother'; + + var value; + if(isOther) { + value = labels[key]; + if(value === undefined) return ''; + } else { + var obj, i; + for(i = 3; i < args.length; i++) { + obj = args[i]; + if(!obj) continue; + if(obj.hasOwnProperty(key)) { + value = obj[key]; + break; + } - if(!SIMPLE_PROPERTY_REGEX.test(key)) { - value = getterCache[key] || lib.nestedProperty(obj, key).get(); - if(value) getterCache[key] = value; + if(!SIMPLE_PROPERTY_REGEX.test(key)) { + value = lib.nestedProperty(obj, key).get(); + value = getterCache[key] || lib.nestedProperty(obj, key).get(); + if(value) getterCache[key] = value; + } + if(value !== undefined) break; } - if(value !== undefined) break; } if(value === undefined && opts) { @@ -1087,8 +1099,14 @@ function templateFormatString(string, labels, d3locale) { value = lib.formatDate(ms, format.replace(TEMPLATE_STRING_FORMAT_SEPARATOR, ''), false, fmt); } } else { - if(labels.hasOwnProperty(key + 'Label')) value = labels[key + 'Label']; + var keyLabel = key + 'Label'; + if(labels.hasOwnProperty(keyLabel)) value = labels[keyLabel]; + } + + if(isOther) { + value = '(' + value + ')'; } + return value; }); } From 3fa20fb5e6cadd4f1f243e8ff856b3f8c1c0f5ab Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 31 May 2021 10:32:40 -0400 Subject: [PATCH 3/7] add jasmine test --- test/jasmine/tests/hover_label_test.js | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 9ea0ac24305..41bd8de30c6 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -4831,6 +4831,60 @@ describe('hovermode: (x|y)unified', function() { .then(done, done.fail); }); + it('should format differing position using *xother* `hovertemplate` and in respect to `xhoverformat`', function(done) { + Plotly.newPlot(gd, [{ + type: 'bar', + hovertemplate: '%{y:.1f} %{xother:.2f}', + x: [0, 1.001], + y: [1.001, 2.001] + }, { + x: [0, 0.749], + y: [1.999, 0.999] + }, { + hovertemplate: '%{y:.1f} %{xother}', + xhoverformat: '.1f', + x: [0, 1.251], + y: [2.001, 3.001] + }], { + hoverdistance: -1, + hovermode: 'x unified', + showlegend: false, + width: 500, + height: 500, + margin: { + t: 50, + b: 50, + l: 50, + r: 50 + } + }) + .then(function() { + _hover(gd, { xpx: 100, ypx: 200 }); + assertLabel({title: '0', items: [ + 'trace 0 : 1.0 ', + 'trace 1 : 1.999', + 'trace 2 : 2.0 (0.0)' + ]}); + }) + .then(function() { + _hover(gd, { xpx: 250, ypx: 200 }); + assertLabel({title: '0.749', items: [ + 'trace 0 : 2.0 (1.00)', + 'trace 1 : 0.999', + 'trace 2 : 3.0 (1.3)' + ]}); + }) + .then(function() { + _hover(gd, { xpx: 350, ypx: 200 }); + assertLabel({title: '1.3', items: [ + 'trace 0 : 2.0 (1.00)', + 'trace 1 : (0.749, 0.999)', + 'trace 2 : 3.0 ' + ]}); + }) + .then(done, done.fail); + }); + it('should display hover for two high-res scatter at different various intervals', function(done) { var x1 = []; var y1 = []; From ad2ad740e012eb43f623c041ce0a3204948b8a39 Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 31 May 2021 16:45:31 -0400 Subject: [PATCH 4/7] add space around (x|y)other label only when there is a differing position --- src/lib/index.js | 2 +- test/jasmine/tests/hover_label_test.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lib/index.js b/src/lib/index.js index 49e3430782a..ef564050383 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -1104,7 +1104,7 @@ function templateFormatString(string, labels, d3locale) { } if(isOther) { - value = '(' + value + ')'; + value = ' (' + value + ') '; } return value; diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 41bd8de30c6..1ac6509217a 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -4834,14 +4834,14 @@ describe('hovermode: (x|y)unified', function() { it('should format differing position using *xother* `hovertemplate` and in respect to `xhoverformat`', function(done) { Plotly.newPlot(gd, [{ type: 'bar', - hovertemplate: '%{y:.1f} %{xother:.2f}', + hovertemplate: '%{y:.1f}%{xother:.2f}', x: [0, 1.001], y: [1.001, 2.001] }, { x: [0, 0.749], y: [1.999, 0.999] }, { - hovertemplate: '%{y:.1f} %{xother}', + hovertemplate: '%{y:.1f}%{xother}', xhoverformat: '.1f', x: [0, 1.251], y: [2.001, 3.001] @@ -4861,25 +4861,25 @@ describe('hovermode: (x|y)unified', function() { .then(function() { _hover(gd, { xpx: 100, ypx: 200 }); assertLabel({title: '0', items: [ - 'trace 0 : 1.0 ', + 'trace 0 : 1.0', 'trace 1 : 1.999', - 'trace 2 : 2.0 (0.0)' + 'trace 2 : 2.0 (0.0) ' ]}); }) .then(function() { _hover(gd, { xpx: 250, ypx: 200 }); assertLabel({title: '0.749', items: [ - 'trace 0 : 2.0 (1.00)', + 'trace 0 : 2.0 (1.00) ', 'trace 1 : 0.999', - 'trace 2 : 3.0 (1.3)' + 'trace 2 : 3.0 (1.3) ' ]}); }) .then(function() { _hover(gd, { xpx: 350, ypx: 200 }); assertLabel({title: '1.3', items: [ - 'trace 0 : 2.0 (1.00)', + 'trace 0 : 2.0 (1.00) ', 'trace 1 : (0.749, 0.999)', - 'trace 2 : 3.0 ' + 'trace 2 : 3.0' ]}); }) .then(done, done.fail); From f8c8451aeaf15b986a7dc96fed77247bf6d8bb93 Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 31 May 2021 18:06:14 -0400 Subject: [PATCH 5/7] provide underscore other keys to add optional space before or after --- src/lib/index.js | 33 ++++++++++++++---- src/plots/template_attributes.js | 6 +++- test/jasmine/tests/hover_label_test.js | 48 +++++++++++++++++--------- 3 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/lib/index.js b/src/lib/index.js index ef564050383..3aa60885db9 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -1043,14 +1043,31 @@ function templateFormatString(string, labels, d3locale) { // just in case it speeds things up *slightly*: var getterCache = {}; - - return string.replace(lib.TEMPLATE_STRING_REGEX, function(match, key, format) { + return string.replace(lib.TEMPLATE_STRING_REGEX, function(match, rawKey, format) { var isOther = - key === 'xother' || - key === 'yother'; + rawKey === 'xother' || + rawKey === 'yother'; + + var isSpaceOther = + rawKey === '_xother' || + rawKey === '_yother'; + + var isSpaceOtherSpace = + rawKey === '_xother_' || + rawKey === '_yother_'; + + var isOtherSpace = + rawKey === 'xother_' || + rawKey === 'yother_'; + + var hasOther = isOther || isSpaceOther || isOtherSpace || isSpaceOtherSpace; + + var key = rawKey; + if(isSpaceOther || isSpaceOtherSpace) key = key.substring(1); + if(isOtherSpace || isSpaceOtherSpace) key = key.substring(0, key.length - 1); var value; - if(isOther) { + if(hasOther) { value = labels[key]; if(value === undefined) return ''; } else { @@ -1103,8 +1120,10 @@ function templateFormatString(string, labels, d3locale) { if(labels.hasOwnProperty(keyLabel)) value = labels[keyLabel]; } - if(isOther) { - value = ' (' + value + ') '; + if(hasOther) { + value = '(' + value + ')'; + if(isSpaceOther || isSpaceOtherSpace) value = ' ' + value; + if(isOtherSpace || isSpaceOtherSpace) value = value + ' '; } return value; diff --git a/src/plots/template_attributes.js b/src/plots/template_attributes.js index 0670a11edfe..132f7efd22b 100644 --- a/src/plots/template_attributes.js +++ b/src/plots/template_attributes.js @@ -9,7 +9,11 @@ function templateFormatStringDescription(opts) { return [ 'Variables are inserted using %{variable},', - 'for example "y: %{y}".' + (supportOther ? ' as well as %{xother} when positions do not match' : ''), + 'for example "y: %{y}"' + ( + supportOther ? + ' as well as %{xother}, {%_xother}, {%_xother_}, {%xother_}, when positions do not match. Please note that underscore could be used before or after *(x|y)other* to provide optional space.' : + '.' + ), 'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".', FORMAT_LINK, 'for details on the formatting syntax.', diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index 1ac6509217a..f79109fab15 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -4834,17 +4834,27 @@ describe('hovermode: (x|y)unified', function() { it('should format differing position using *xother* `hovertemplate` and in respect to `xhoverformat`', function(done) { Plotly.newPlot(gd, [{ type: 'bar', - hovertemplate: '%{y:.1f}%{xother:.2f}', + hovertemplate: '%{y}%{_xother:.2f}', x: [0, 1.001], - y: [1.001, 2.001] + y: [2, 1] }, { x: [0, 0.749], - y: [1.999, 0.999] + y: [1, 2] }, { - hovertemplate: '%{y:.1f}%{xother}', + hovertemplate: '(x)y:%{xother}%{y}', xhoverformat: '.1f', x: [0, 1.251], - y: [2.001, 3.001] + y: [2, 3] + }, { + hovertemplate: '(x)y:%{xother_}%{y}', + xhoverformat: '.2f', + x: [0, 1.351], + y: [3, 4] + }, { + hovertemplate: '(x)y:%{_xother_}%{y}', + xhoverformat: '.3f', + x: [0, 1.451], + y: [4, 5] }], { hoverdistance: -1, hovermode: 'x unified', @@ -4860,26 +4870,32 @@ describe('hovermode: (x|y)unified', function() { }) .then(function() { _hover(gd, { xpx: 100, ypx: 200 }); - assertLabel({title: '0', items: [ - 'trace 0 : 1.0', - 'trace 1 : 1.999', - 'trace 2 : 2.0 (0.0) ' + assertLabel({title: '0.000', items: [ + 'trace 0 : 2 (0.00)', + 'trace 1 : (0, 1)', + 'trace 2 : (x)y:(0.0)2', + 'trace 3 : (x)y:(0.00) 3', + 'trace 4 : (x)y:4', ]}); }) .then(function() { _hover(gd, { xpx: 250, ypx: 200 }); assertLabel({title: '0.749', items: [ - 'trace 0 : 2.0 (1.00) ', - 'trace 1 : 0.999', - 'trace 2 : 3.0 (1.3) ' + 'trace 0 : 1 (1.00)', + 'trace 1 : 2', + 'trace 2 : (x)y:(1.3)3', + 'trace 3 : (x)y:(1.35) 4', + 'trace 4 : (x)y: (1.451) 5', ]}); }) .then(function() { _hover(gd, { xpx: 350, ypx: 200 }); - assertLabel({title: '1.3', items: [ - 'trace 0 : 2.0 (1.00) ', - 'trace 1 : (0.749, 0.999)', - 'trace 2 : 3.0' + assertLabel({title: '1.35', items: [ + 'trace 0 : 1 (1.00)', + 'trace 1 : (0.749, 2)', + 'trace 2 : (x)y:(1.3)3', + 'trace 3 : (x)y:4', + 'trace 4 : (x)y: (1.451) 5', ]}); }) .then(done, done.fail); From 062c934db2070e4f10c6f0fee9094300034940df Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi <33888540+archmoj@users.noreply.github.com> Date: Tue, 1 Jun 2021 11:10:07 -0400 Subject: [PATCH 6/7] Update src/plots/template_attributes.js Co-authored-by: Alex Johnson --- src/plots/template_attributes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/template_attributes.js b/src/plots/template_attributes.js index 132f7efd22b..1fb3596110e 100644 --- a/src/plots/template_attributes.js +++ b/src/plots/template_attributes.js @@ -11,7 +11,7 @@ function templateFormatStringDescription(opts) { 'Variables are inserted using %{variable},', 'for example "y: %{y}"' + ( supportOther ? - ' as well as %{xother}, {%_xother}, {%_xother_}, {%xother_}, when positions do not match. Please note that underscore could be used before or after *(x|y)other* to provide optional space.' : + ' as well as %{xother}, {%_xother}, {%_xother_}, {%xother_}. When showing info for several points, *xother* will be added to those with different x positions from the first point. An underscore before or after *(x|y)other* will add a space on that side, only when this field is shown.' : '.' ), 'Numbers are formatted using d3-format\'s syntax %{variable:d3-format}, for example "Price: %{y:$.2f}".', From a6da451a2af3df7cc42f3609f28cffe3c339cab1 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 1 Jun 2021 11:18:38 -0400 Subject: [PATCH 7/7] improve jasmine test --- test/jasmine/tests/hover_label_test.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index f79109fab15..f804dc6e367 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -4834,7 +4834,7 @@ describe('hovermode: (x|y)unified', function() { it('should format differing position using *xother* `hovertemplate` and in respect to `xhoverformat`', function(done) { Plotly.newPlot(gd, [{ type: 'bar', - hovertemplate: '%{y}%{_xother:.2f}', + hovertemplate: 'y(_x):%{y}%{_xother:.2f}', x: [0, 1.001], y: [2, 1] }, { @@ -4846,12 +4846,12 @@ describe('hovermode: (x|y)unified', function() { x: [0, 1.251], y: [2, 3] }, { - hovertemplate: '(x)y:%{xother_}%{y}', + hovertemplate: '(x_)y:%{xother_}%{y}', xhoverformat: '.2f', x: [0, 1.351], y: [3, 4] }, { - hovertemplate: '(x)y:%{_xother_}%{y}', + hovertemplate: '(_x_)y:%{_xother_}%{y}', xhoverformat: '.3f', x: [0, 1.451], y: [4, 5] @@ -4871,31 +4871,31 @@ describe('hovermode: (x|y)unified', function() { .then(function() { _hover(gd, { xpx: 100, ypx: 200 }); assertLabel({title: '0.000', items: [ - 'trace 0 : 2 (0.00)', + 'trace 0 : y(_x):2 (0.00)', 'trace 1 : (0, 1)', 'trace 2 : (x)y:(0.0)2', - 'trace 3 : (x)y:(0.00) 3', - 'trace 4 : (x)y:4', + 'trace 3 : (x_)y:(0.00) 3', + 'trace 4 : (_x_)y:4', ]}); }) .then(function() { _hover(gd, { xpx: 250, ypx: 200 }); assertLabel({title: '0.749', items: [ - 'trace 0 : 1 (1.00)', + 'trace 0 : y(_x):1 (1.00)', 'trace 1 : 2', 'trace 2 : (x)y:(1.3)3', - 'trace 3 : (x)y:(1.35) 4', - 'trace 4 : (x)y: (1.451) 5', + 'trace 3 : (x_)y:(1.35) 4', + 'trace 4 : (_x_)y: (1.451) 5', ]}); }) .then(function() { _hover(gd, { xpx: 350, ypx: 200 }); assertLabel({title: '1.35', items: [ - 'trace 0 : 1 (1.00)', + 'trace 0 : y(_x):1 (1.00)', 'trace 1 : (0.749, 2)', 'trace 2 : (x)y:(1.3)3', - 'trace 3 : (x)y:4', - 'trace 4 : (x)y: (1.451) 5', + 'trace 3 : (x_)y:4', + 'trace 4 : (_x_)y: (1.451) 5', ]}); }) .then(done, done.fail);