From 94f76cb001f05a8269ab5d1e23e018480952162e Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 8 Feb 2025 22:26:28 -0500 Subject: [PATCH 1/4] allow null and NaN values to be shown in hovertemplates and other text templates --- src/lib/index.js | 14 +++++--------- src/lib/nested_property.js | 4 ++-- test/jasmine/tests/lib_test.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/lib/index.js b/src/lib/index.js index a5942961ae2..6e66e47490c 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -1067,7 +1067,7 @@ lib.templateString = function(string, obj) { v = obj[key]; } else { getterCache[key] = getterCache[key] || lib.nestedProperty(obj, key).get; - v = getterCache[key](); + v = getterCache[key](true); // true means don't replace undefined with null } return lib.isValidTextValue(v) ? v : ''; }); @@ -1132,9 +1132,6 @@ function templateFormatString(string, labels, d3locale) { var opts = this; var args = arguments; if(!labels) labels = {}; - // Not all that useful, but cache nestedProperty instantiation - // just in case it speeds things up *slightly*: - var getterCache = {}; return string.replace(lib.TEMPLATE_STRING_REGEX, function(match, rawKey, format) { var isOther = @@ -1185,9 +1182,8 @@ function templateFormatString(string, labels, d3locale) { } 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; + // true here means don't convert null to undefined + value = lib.nestedProperty(obj, key).get(true); } if(value !== undefined) break; } @@ -1310,9 +1306,9 @@ lib.fillText = function(calcPt, trace, contOut) { if(lib.isValidTextValue(tx)) return fill(tx); }; -// accept all truthy values and 0 (which gets cast to '0' in the hover labels) +// accept anything but undefined - was all truthy values and 0 (which gets cast to '0' in the hover labels) lib.isValidTextValue = function(v) { - return v || v === 0; + return v !== undefined; }; /** diff --git a/src/lib/nested_property.js b/src/lib/nested_property.js index a057e732bee..99ca5766835 100644 --- a/src/lib/nested_property.js +++ b/src/lib/nested_property.js @@ -73,7 +73,7 @@ module.exports = function nestedProperty(container, propStr) { }; function npGet(cont, parts) { - return function() { + return function(retainNull) { var curCont = cont; var curPart; var allSame; @@ -105,7 +105,7 @@ function npGet(cont, parts) { if(typeof curCont !== 'object' || curCont === null) return undefined; out = curCont[parts[i]]; - if(out === null) return undefined; + if(!retainNull && (out === null)) return undefined; return out; }; } diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index d49d0a994fa..c2cef7f942d 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -2443,6 +2443,14 @@ describe('Test lib.js:', function() { it('should work with the number *0* (nested case)', function() { expect(Lib.templateString('%{x.y}', {x: {y: 0}})).toEqual('0'); }); + + it('preserves null and NaN', function() { + expect(Lib.templateString( + '%{a} %{b} %{c.d} %{c.e} %{f[0]} %{f[1]}', + {a: null, b: NaN, c: {d: null, e: NaN}, f: [null, NaN]} + )) + .toEqual('null NaN null NaN null NaN'); + }); }); describe('hovertemplateString', function() { @@ -2471,6 +2479,16 @@ describe('Test lib.js:', function() { expect(Lib.hovertemplateString('%{x.y}', {}, locale, {x: {y: 0}})).toEqual('0'); }); + it('preserves null and NaN', function() { + expect(Lib.hovertemplateString( + '%{a} %{b} %{c.d} %{c.e} %{f[0]} %{f[1]}', + {}, + locale, + {a: null, b: NaN, c: {d: null, e: NaN}, f: [null, NaN]} + )) + .toEqual('null NaN null NaN null NaN'); + }); + it('subtitutes multiple matches', function() { expect(Lib.hovertemplateString('foo %{group} %{trace}', {}, locale, {group: 'asdf', trace: 'jkl;'})).toEqual('foo asdf jkl;'); }); @@ -2537,6 +2555,16 @@ describe('Test lib.js:', function() { expect(Lib.texttemplateString('y: %{y}', {yLabel: '0.1'}, locale, {y: 0.123})).toEqual('y: 0.1'); }); + it('preserves null and NaN', function() { + expect(Lib.texttemplateString( + '%{a} %{b} %{c.d} %{c.e} %{f[0]} %{f[1]}', + {}, + locale, + {a: null, b: NaN, c: {d: null, e: NaN}, f: [null, NaN]} + )) + .toEqual('null NaN null NaN null NaN'); + }); + it('warns user up to 10 times if a variable cannot be found', function() { spyOn(Lib, 'warn').and.callThrough(); Lib.texttemplateString('%{idontexist}', {}); From a0b8e17b41b8d0ae500297c0effa8f245a0b3740 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 8 Feb 2025 23:05:16 -0500 Subject: [PATCH 2/4] tighter scope for changes to hover label logic keep the original isValidTextValue except in templating --- src/lib/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/index.js b/src/lib/index.js index 6e66e47490c..816dcab2d31 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -1069,7 +1069,7 @@ lib.templateString = function(string, obj) { getterCache[key] = getterCache[key] || lib.nestedProperty(obj, key).get; v = getterCache[key](true); // true means don't replace undefined with null } - return lib.isValidTextValue(v) ? v : ''; + return (v !== undefined) ? v : ''; }); }; @@ -1306,9 +1306,9 @@ lib.fillText = function(calcPt, trace, contOut) { if(lib.isValidTextValue(tx)) return fill(tx); }; -// accept anything but undefined - was all truthy values and 0 (which gets cast to '0' in the hover labels) +// accept all truthy values and 0 (which gets cast to '0' in the hover labels) lib.isValidTextValue = function(v) { - return v !== undefined; + return v || v === 0; }; /** From 655282ab430a831e6eaf0d2c649f6bf13dc237c7 Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Sat, 8 Feb 2025 23:06:06 -0500 Subject: [PATCH 3/4] draftlog for 7360 --- draftlogs/7360_fix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 draftlogs/7360_fix.md diff --git a/draftlogs/7360_fix.md b/draftlogs/7360_fix.md new file mode 100644 index 00000000000..6fe099ace98 --- /dev/null +++ b/draftlogs/7360_fix.md @@ -0,0 +1 @@ +- Fix hoverlabels and other text labels with null values templated in [[#7360](https://github.com/plotly/plotly.js/pull/7360)] From a348c245a69405dbe70866d1be98596e7a95940d Mon Sep 17 00:00:00 2001 From: alexcjohnson Date: Mon, 17 Feb 2025 15:41:38 -0500 Subject: [PATCH 4/4] nestedProperty: support retainNull for array get all --- src/lib/nested_property.js | 2 +- test/jasmine/tests/lib_test.js | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib/nested_property.js b/src/lib/nested_property.js index 99ca5766835..235405c582f 100644 --- a/src/lib/nested_property.js +++ b/src/lib/nested_property.js @@ -87,7 +87,7 @@ function npGet(cont, parts) { allSame = true; out = []; for(j = 0; j < curCont.length; j++) { - out[j] = npGet(curCont[j], parts.slice(i + 1))(); + out[j] = npGet(curCont[j], parts.slice(i + 1))(retainNull); if(out[j] !== out[0]) allSame = false; } return allSame ? out[0] : out; diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index c2cef7f942d..11b771fef08 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -340,11 +340,12 @@ describe('Test lib.js:', function() { }); it('should access properties of objects in an array with index -1', function() { - var obj = {arr: [{a: 1}, {a: 2}, {b: 3}]}; + var obj = {arr: [{a: 1}, {a: null}, {b: 3}]}; var prop = np(obj, 'arr[-1].a'); - expect(prop.get()).toEqual([1, 2, undefined]); - expect(obj).toEqual({arr: [{a: 1}, {a: 2}, {b: 3}]}); + expect(prop.get()).toEqual([1, undefined, undefined]); + expect(prop.get(true)).toEqual([1, null, undefined]); + expect(obj).toEqual({arr: [{a: 1}, {a: null}, {b: 3}]}); prop.set(5); expect(prop.get()).toBe(5);