From 2693f1ca10e54f5b012f4835ca45340a9e7e919f Mon Sep 17 00:00:00 2001 From: Soujas K Date: Wed, 28 Oct 2026 01:11:00 +0530 Subject: [PATCH] fix(format): preserve literal dollar sequences in component formatter templates --- src/component/axisPointer/viewHelper.ts | 3 +- src/component/dataZoom/SliderZoomView.ts | 2 +- src/component/legend/LegendView.ts | 2 +- src/component/visualMap/VisualMapModel.ts | 4 +- src/coord/axisHelper.ts | 2 +- src/coord/geo/GeoModel.ts | 2 +- src/coord/radar/RadarModel.ts | 2 +- .../component/formatterLiteralDollar.test.ts | 97 +++++++++++++++++++ 8 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 test/ut/spec/component/formatterLiteralDollar.test.ts diff --git a/src/component/axisPointer/viewHelper.ts b/src/component/axisPointer/viewHelper.ts index 762869fdc2..3a383d6fcd 100644 --- a/src/component/axisPointer/viewHelper.ts +++ b/src/component/axisPointer/viewHelper.ts @@ -181,7 +181,8 @@ export function getValueLabel( }); if (zrUtil.isString(formatter)) { - text = formatter.replace('{value}', text); + const value = text; + text = formatter.replace('{value}', () => value); } else if (zrUtil.isFunction(formatter)) { text = formatter(params); diff --git a/src/component/dataZoom/SliderZoomView.ts b/src/component/dataZoom/SliderZoomView.ts index 7d71520372..653cdcf2e6 100644 --- a/src/component/dataZoom/SliderZoomView.ts +++ b/src/component/dataZoom/SliderZoomView.ts @@ -1156,7 +1156,7 @@ function formatLabel( return isFunction(labelFormatter) ? labelFormatter(value, valueStr) : isString(labelFormatter) - ? labelFormatter.replace('{value}', valueStr) + ? labelFormatter.replace('{value}', () => valueStr) : valueStr; } diff --git a/src/component/legend/LegendView.ts b/src/component/legend/LegendView.ts index 9a79765089..f5bbb2ddcb 100644 --- a/src/component/legend/LegendView.ts +++ b/src/component/legend/LegendView.ts @@ -458,7 +458,7 @@ class LegendView extends ComponentView { const formatter = legendModel.get('formatter'); let content = name; if (zrUtil.isString(formatter) && formatter) { - content = formatter.replace('{name}', name != null ? name : ''); + content = formatter.replace('{name}', () => (name != null ? name : '')); } else if (zrUtil.isFunction(formatter)) { content = formatter(name); diff --git a/src/component/visualMap/VisualMapModel.ts b/src/component/visualMap/VisualMapModel.ts index 843387a465..ac57de5a5d 100644 --- a/src/component/visualMap/VisualMapModel.ts +++ b/src/component/visualMap/VisualMapModel.ts @@ -369,8 +369,8 @@ class VisualMapModel extends Com if (zrUtil.isString(formatter)) { return formatter - .replace('{value}', isMinMax ? (textValue as string[])[0] : textValue as string) - .replace('{value2}', isMinMax ? (textValue as string[])[1] : textValue as string); + .replace('{value}', () => (isMinMax ? (textValue as string[])[0] : textValue as string)) + .replace('{value2}', () => (isMinMax ? (textValue as string[])[1] : textValue as string)); } else if (zrUtil.isFunction(formatter)) { return isMinMax diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index c61b975870..c0cbd366dc 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -190,7 +190,7 @@ export function makeLabelFormatter(axis: Axis): (tick: ScaleTick, idx?: number) // For category axis, get raw value; for numeric axis, // get formatted label like '1,333,444'. const label = axis.scale.getLabel(tick); - const text = labelFormatter.replace('{value}', label != null ? label : ''); + const text = labelFormatter.replace('{value}', () => (label != null ? label : '')); return text; }; } diff --git a/src/coord/geo/GeoModel.ts b/src/coord/geo/GeoModel.ts index 66c659ef8d..4c103de6c9 100644 --- a/src/coord/geo/GeoModel.ts +++ b/src/coord/geo/GeoModel.ts @@ -307,7 +307,7 @@ class GeoModel extends ComponentModel implements RoamHostModel { return formatter(params); } else if (zrUtil.isString(formatter)) { - return formatter.replace('{a}', name != null ? name : ''); + return formatter.replace('{a}', () => (name != null ? name : '')); } } diff --git a/src/coord/radar/RadarModel.ts b/src/coord/radar/RadarModel.ts index de7254f4e8..34d6c668eb 100644 --- a/src/coord/radar/RadarModel.ts +++ b/src/coord/radar/RadarModel.ts @@ -167,7 +167,7 @@ class RadarModel extends ComponentModel implements CoordinateSystem } as InnerIndicatorAxisOption, false); if (zrUtil.isString(nameFormatter)) { const indName = innerIndicatorOpt.name; - innerIndicatorOpt.name = nameFormatter.replace('{value}', indName != null ? indName : ''); + innerIndicatorOpt.name = nameFormatter.replace('{value}', () => (indName != null ? indName : '')); } else if (zrUtil.isFunction(nameFormatter)) { innerIndicatorOpt.name = nameFormatter( diff --git a/test/ut/spec/component/formatterLiteralDollar.test.ts b/test/ut/spec/component/formatterLiteralDollar.test.ts new file mode 100644 index 0000000000..39e286a9de --- /dev/null +++ b/test/ut/spec/component/formatterLiteralDollar.test.ts @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { EChartsType } from '@/src/echarts'; +import { createChart, getECModel, getGraphicElements } from '../../core/utHelper'; +import CartesianAxisModel from '@/src/coord/cartesian/AxisModel'; +import RadarModel from '@/src/coord/radar/RadarModel'; +import VisualMapModel from '@/src/component/visualMap/VisualMapModel'; +import ZRText from 'zrender/src/graphic/Text'; + +// Sequences that String.prototype.replace interprets in a replacement string. +const specialTexts = ['$&', '$$', '$`', '$\'']; + +describe('string formatter templates keep literal dollar sequences', function () { + let chart: EChartsType; + + beforeEach(function () { + chart = createChart(); + }); + + afterEach(function () { + chart.dispose(); + }); + + it.each(specialTexts)('axis label %s', text => { + const category = 'Tier ' + text; + chart.setOption({ + animation: false, + xAxis: { type: 'category', data: [category], axisLabel: { formatter: '<{value}>' } }, + yAxis: {}, + series: [{ type: 'bar', data: [1] }] + }); + + const axis = getECModel(chart).getComponent('xAxis', 0) as CartesianAxisModel; + const labels = axis.axis.getViewLabels().map(label => label.formattedLabel); + expect(labels).toEqual(['<' + category + '>']); + }); + + it.each(specialTexts)('legend %s', text => { + const name = 'Series ' + text; + chart.setOption({ + animation: false, + legend: { formatter: '[{name}]' }, + series: [{ type: 'pie', name: name, data: [{ name: name, value: 1 }] }] + }); + + const texts = getGraphicElements(chart, 'legend') + .filter(el => el instanceof ZRText) + .map(el => (el as ZRText).style.text); + expect(texts).toContain('[' + name + ']'); + }); + + it.each(specialTexts)('radar indicator name %s', text => { + const name = 'Price ' + text; + chart.setOption({ + animation: false, + radar: { + indicator: [{ name: name, max: 10 }, { name: 'b', max: 10 }], + axisName: { formatter: '({value})' } + }, + series: [{ type: 'radar', data: [{ value: [1, 2] }] }] + }); + + const radar = getECModel(chart).getComponent('radar', 0) as RadarModel; + expect(radar.getIndicatorModels()[0].get('name')).toBe('(' + name + ')'); + }); + + it.each(specialTexts)('visualMap category text %s', text => { + const category = 'Tier ' + text; + chart.setOption({ + animation: false, + visualMap: { type: 'piecewise', categories: [category], formatter: '<{value}>' }, + xAxis: {}, + yAxis: {}, + series: [{ type: 'scatter', data: [[1, 1]] }] + }); + + const visualMap = getECModel(chart).getComponent('visualMap', 0) as VisualMapModel; + expect(visualMap.formatValueText(category, true)).toBe('<' + category + '>'); + }); +});