From 1402573cf9617121d2b7d7c7ba901b53c967f3b7 Mon Sep 17 00:00:00 2001 From: netil Date: Thu, 31 Aug 2023 15:34:32 +0900 Subject: [PATCH] fix(tooltip): Fix tooltip.format.value call - Unify the call of sanitize for all formatter function - Fix the unnecessary call and pass correct arguments for bar range type Ref #3371 --- src/ChartInternal/internals/tooltip.ts | 47 ++++---- src/config/Options/common/tooltip.ts | 7 +- test/internals/tooltip-spec.ts | 143 +++++++++++++++++++++++++ 3 files changed, 176 insertions(+), 21 deletions(-) diff --git a/src/ChartInternal/internals/tooltip.ts b/src/ChartInternal/internals/tooltip.ts index 435f7a451..1f916f56a 100644 --- a/src/ChartInternal/internals/tooltip.ts +++ b/src/ChartInternal/internals/tooltip.ts @@ -93,17 +93,23 @@ export default { const $$ = this; const {api, config, state, $el} = $$; - let [titleFormat, nameFormat, valueFormat] = ["title", "name", "value"].map(v => { + // get formatter function + const [titleFn, nameFn, valueFn] = ["title", "name", "value"].map(v => { const fn = config[`tooltip_format_${v}`]; return isFunction(fn) ? fn.bind(api) : fn; }); - titleFormat = titleFormat || defaultTitleFormat; - nameFormat = nameFormat || (name => name); - valueFormat = valueFormat || ( - state.hasTreemap || $$.isStackNormalized() ? (v, ratio) => `${(ratio * 100).toFixed(2)}%` : defaultValueFormat - ); + // determine fotmatter function with sanitization + const titleFormat = (...arg) => sanitize((titleFn || defaultTitleFormat)(...arg)); + const nameFormat = (...arg) => sanitize((nameFn || (name => name))(...arg)); + const valueFormat = (...arg) => { + const fn = valueFn || ( + state.hasTreemap || $$.isStackNormalized() ? (v, ratio) => `${(ratio * 100).toFixed(2)}%` : defaultValueFormat + ); + + return sanitize(fn(...arg)); + }; const order = config.tooltip_order; const getRowValue = row => ($$.axis && $$.isBubbleZType(row) ? $$.getBubbleZData(row.value, "z") : $$.getBaseValue(row)); @@ -158,8 +164,7 @@ export default { } if (isUndefined(text)) { - const title = (state.hasAxis || state.hasRadar) && - sanitize(titleFormat ? titleFormat(row.x) : row.x); + const title = (state.hasAxis || state.hasRadar) && titleFormat(row.x); text = tplProcess(tpl[0], { CLASS_TOOLTIP: $TOOLTIP.tooltip, @@ -175,24 +180,28 @@ export default { } param = [row.ratio, row.id, row.index, d]; - value = sanitize(valueFormat(getRowValue(row), ...param)); if ($$.isAreaRangeType(row)) { - const [high, low] = ["high", "low"].map(v => sanitize( - valueFormat($$.getRangedData(row, v), ...param) - )); + const [high, low] = ["high", "low"].map(v => valueFormat($$.getRangedData(row, v), ...param)); + const mid = valueFormat(getRowValue(row), ...param); - value = `Mid: ${value} High: ${high} Low: ${low}`; + value = `Mid: ${mid} High: ${high} Low: ${low}`; } else if ($$.isCandlestickType(row)) { - const [open, high, low, close, volume] = ["open", "high", "low", "close", "volume"].map(v => sanitize( - valueFormat($$.getRangedData(row, v, "candlestick"), ...param) - )); + const [open, high, low, close, volume] = ["open", "high", "low", "close", "volume"].map(v => { + const value = $$.getRangedData(row, v, "candlestick"); + + return value ? valueFormat( + $$.getRangedData(row, v, "candlestick"), ...param + ) : undefined; + }); value = `Open: ${open} High: ${high} Low: ${low} Close: ${close}${volume ? ` Volume: ${volume}` : ""}`; } else if ($$.isBarRangeType(row)) { - const [start, end] = row.value; + const {value: [start, end], id, index} = row; - value = `${valueFormat(start)} ~ ${valueFormat(end)}`; + value = `${valueFormat(start, undefined, id, index)} ~ ${valueFormat(end, undefined, id, index)}`; + } else { + value = valueFormat(getRowValue(row), ...param); } if (value !== undefined) { @@ -201,7 +210,7 @@ export default { continue; } - const name = sanitize(nameFormat(row.name, ...param)); + const name = nameFormat(row.name, ...param); const color = getBgColor(row); const contentValue = { CLASS_TOOLTIP_NAME: $TOOLTIP.tooltipName + $$.getTargetSelectorSuffix(row.id), diff --git a/src/config/Options/common/tooltip.ts b/src/config/Options/common/tooltip.ts index 69926089d..0927472c1 100644 --- a/src/config/Options/common/tooltip.ts +++ b/src/config/Options/common/tooltip.ts @@ -22,9 +22,9 @@ export default { * Specified function receives x of the data point to show. * @property {Function} [tooltip.format.name] Set format for the name of each data in tooltip.
* Specified function receives name, ratio, id and index of the data point to show. ratio will be undefined if the chart is not donut/pie/gauge. - * @property {Function} [tooltip.format.value] Set format for the value of each data in tooltip. If undefined returned, the row of that value will be skipped to be called. + * @property {Function} [tooltip.format.value] Set format for the value of each data value in tooltip. If undefined returned, the row of that value will be skipped to be called. * - Will pass following arguments to the given function: - * - `value {string}`: Value of the data point + * - `value {string}`: Value of the data point. If data row contains multiple or ranged(ex. candlestick, area range, etc.) value, formatter will be called as value length. * - `ratio {number}`: Ratio of the data point in the `pie/donut/gauge` and `area/bar` when contains grouped data. Otherwise is `undefined`. * - `id {string}`: id of the data point * - `index {number}`: Index of the data point @@ -89,6 +89,9 @@ export default { * format: { * title: function(x) { return "Data " + x; }, * name: function(name, ratio, id, index) { return name; }, + * + * // If data row contains multiple or ranged(ex. candlestick, area range, etc.) value, + * // formatter will be called as value length times. * value: function(value, ratio, id, index) { return ratio; } * }, * position: function(data, width, height, element, pos) { diff --git a/test/internals/tooltip-spec.ts b/test/internals/tooltip-spec.ts index d82d5cc04..b2800a170 100644 --- a/test/internals/tooltip-spec.ts +++ b/test/internals/tooltip-spec.ts @@ -1795,6 +1795,8 @@ describe("TOOLTIP", function() { }); describe("tooltip: format", () => { + const spyTitle = sinon.spy(); + const spyName = sinon.spy(); const spy = sinon.spy(function(value, ratio, id, index) { return [value, ratio, id, index]; }); @@ -1813,11 +1815,19 @@ describe("TOOLTIP", function() { }, tooltip: { format: { + title: spyTitle, + name: spyName, value: spy } } }; }); + + after(() => { + spyTitle.resetHistory(); + spyName.resetHistory(); + spy.resetHistory(); + }); it("check if ratio value is given to format function for 'bar' type.", () => { chart.data.values("data1").forEach((v, i) => { @@ -1827,6 +1837,12 @@ describe("TOOLTIP", function() { // check ratio expect(spy.returnValues.reduce((p, a) => p?.[1] ?? p + a[1], 0)).to.be.equal(1); + + // title formatter should be called only once + expect(spyTitle.callCount).to.be.equal(i + 1); + + // name formatter should be called as row's data length times + expect(spyName.callCount).to.be.equal((i + 1) * 2); spy.resetHistory(); }); @@ -1865,5 +1881,132 @@ describe("TOOLTIP", function() { spy.resetHistory(); }); }); + + it("set options", () => { + spy.resetHistory(); + + args = { + data: { + columns: [ + ["data1", [0, 100], [100, 250], 30] + ], + type: "bar" + }, + tooltip: { + format: { + value: spy + } + } + }; + }); + + it("check bar ranged data", () => { + // when + chart.tooltip.show({x: 1}); + + expect(spy.callCount).to.be.equal(2); + + spy.resetHistory(); + + // when + chart.tooltip.show({x: 2}); + + expect(spy.callCount).to.be.equal(1); + }); + + it("set options: area-line-range type", () => { + spy.resetHistory(); + + args = { + data: { + columns: [ + ["data1", [199, 160, 125], [180, 150, 130], [135, 120, 110]] + ], + type: "area-line-range" + }, + tooltip: { + format: { + value: spy + } + } + }; + }); + + it("check for area-line-range data", () => { + // when + chart.tooltip.show({x: 2}); + + expect(spy.callCount).to.be.equal(3); + spy.resetHistory(); + + // when + chart.tooltip.show({x: 1}); + + expect(spy.callCount).to.be.equal(3); + }); + + it("set options: candlestick type", () => { + spy.resetHistory(); + + args = { + data: { + columns: [ + ["data1", + [1327, 1369, 1289, 1348], + [1348, 1371, 1314, 1320], + [1320, 1412, 1314, 1394, 500] + ] + ], + type: "candlestick" + }, + tooltip: { + format: { + value: spy + } + } + }; + }); + + it("check for candlestick data", () => { + const data = chart.data.values("data1"); + + // when data contains volume data + chart.tooltip.show({x: 2}); + + expect(spy.callCount).to.be.equal(data[2].length); + spy.resetHistory(); + + // when + chart.tooltip.show({x: 1}); + + expect(spy.callCount).to.be.equal(data[1].length); + }); + + it("set options: pie type", () => { + spy.resetHistory(); + + args = { + data: { + columns: [ + ["data1", 50], + ["data2", 50], + ], + type: "pie" + }, + tooltip: { + format: { + value: spy + } + } + }; + }); + + it("check for pie data", () => { + // when + chart.tooltip.show({data: {index: 1}}); + + expect(spy.callCount).to.be.equal(1); + spy.resetHistory(); + }); }); });