From bd7c1c09e5f8717bb44842ee1154ea1787df7120 Mon Sep 17 00:00:00 2001 From: Yashvardhan Thanvi Date: Mon, 19 Oct 2026 11:35:05 +0530 Subject: [PATCH] fix(timeline): apply progress and emphasis label formatter. close #18673 `timeline.progress.label.formatter` and `timeline.emphasis.label.formatter` are documented, but the slider timeline only took the text style from those label models, so the label text never changed in those states. Format the text of each state with its own formatter in the same way as `label.formatter`, with the same arguments, and keep the normal text when a state has no formatter. --- src/component/timeline/SliderTimelineView.ts | 35 ++- src/coord/axisHelper.ts | 9 +- test/timeline-case.html | 37 +++ test/ut/spec/component/timeline/label.test.ts | 252 ++++++++++++++++++ 4 files changed, 327 insertions(+), 6 deletions(-) create mode 100644 test/ut/spec/component/timeline/label.test.ts diff --git a/src/component/timeline/SliderTimelineView.ts b/src/component/timeline/SliderTimelineView.ts index b350723649..08e272a087 100644 --- a/src/component/timeline/SliderTimelineView.ts +++ b/src/component/timeline/SliderTimelineView.ts @@ -42,7 +42,8 @@ import { getECData } from '../../util/innerStore'; import { enableHoverEmphasis } from '../../util/states'; import { createTooltipMarkup } from '../tooltip/tooltipMarkup'; import Displayable from 'zrender/src/graphic/Displayable'; -import { createScaleByModel } from '../../coord/axisHelper'; +import { createScaleByModel, makeLabelFormatter } from '../../coord/axisHelper'; +import type Axis from '../../coord/Axis'; import { scaleCalcNiceDirectly } from '../../coord/axisNiceTicks'; const PI = Math.PI; @@ -470,7 +471,7 @@ class SliderTimelineView extends TimelineView { this._tickLabels = []; - each(labels, (labelItem) => { + each(labels, (labelItem, labelIndex) => { if (labelItem.tick.offInterval) { return; } @@ -496,8 +497,12 @@ class SliderTimelineView extends TimelineView { }) }); - textEl.ensureState('emphasis').style = createTextStyle(hoverLabelModel); - textEl.ensureState('progress').style = createTextStyle(progressLabelModel); + textEl.ensureState('emphasis').style = createStateLabelStyle( + hoverLabelModel, axis, labelItem.tick, labelIndex + ); + textEl.ensureState('progress').style = createStateLabelStyle( + progressLabelModel, axis, labelItem.tick, labelIndex + ); group.add(textEl); enableHoverEmphasis(textEl); @@ -765,6 +770,28 @@ function makeControlIcon( return icon; } +/** + * Text style of a label state like `emphasis` or `progress`. + * The `formatter` of that state is applied in the same way as `label.formatter`. + * If it is not specified, the text of the normal state is kept. + */ +function createStateLabelStyle( + stateLabelModel: Model, + axis: TimelineAxis, + tick: ScaleTick, + labelIndex: number +) { + const textStyle = createTextStyle(stateLabelModel); + if (stateLabelModel.get('formatter') != null) { + // The only type mismatch between `TimelineAxis` and `Axis` is `model`, + // which `makeLabelFormatter` does not read when a label model is given. + textStyle.text = makeLabelFormatter( + axis as unknown as Axis, stateLabelModel + )(tick, labelIndex); + } + return textStyle; +} + /** * Create symbol or update symbol * opt: basic position and event handlers diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index c61b975870..6ca561e04f 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -170,14 +170,19 @@ export function isOnAxisZeroDiscouraged(axis: Axis): boolean { /** * @param axis + * @param labelModel Read `formatter` from it instead of `axis.getLabelModel()`, + * e.g., the label model of a state like `progress` in timeline. * @return Label formatter function. * param: {number} tickValue, * param: {number} idx, the index in all ticks. * If category axis, this param is not required. * return: {string} label string. */ -export function makeLabelFormatter(axis: Axis): (tick: ScaleTick, idx?: number) => string { - const labelFormatter = axis.getLabelModel().get('formatter'); +export function makeLabelFormatter( + axis: Axis, + labelModel?: Model +): (tick: ScaleTick, idx?: number) => string { + const labelFormatter = (labelModel || axis.getLabelModel()).get('formatter'); if (axis.type === 'time') { const parsed = parseTimeAxisLabelFormatter(labelFormatter as TimeAxisLabelFormatterOption); diff --git a/test/timeline-case.html b/test/timeline-case.html index d6ba3cfba5..acd1b4ae0a 100644 --- a/test/timeline-case.html +++ b/test/timeline-case.html @@ -35,6 +35,7 @@
+
@@ -107,6 +108,42 @@ }); + + diff --git a/test/ut/spec/component/timeline/label.test.ts b/test/ut/spec/component/timeline/label.test.ts new file mode 100644 index 0000000000..9a589a330b --- /dev/null +++ b/test/ut/spec/component/timeline/label.test.ts @@ -0,0 +1,252 @@ +/* +* 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 ZRText from 'zrender/src/graphic/Text'; +import { createChart, getViewGroup } from '../../../core/utHelper'; +import { EChartsType } from '../../../../../src/echarts'; +import { EChartsOption } from '../../../../../src/export/option'; + + +describe('timeline_label', function () { + + let chart: EChartsType; + + beforeEach(function () { + chart = createChart(); + }); + + afterEach(function () { + chart.dispose(); + }); + + function getLabels(): ZRText[] { + const labels: ZRText[] = []; + getViewGroup(chart, 'timeline').traverse(function (el) { + if (el instanceof ZRText) { + labels.push(el); + } + }); + return labels; + } + + function getTexts(): string[] { + return getLabels().map(label => label.style.text); + } + + function makeOption(timelineOption: EChartsOption['timeline']): EChartsOption { + return { + animation: false, + timeline: timelineOption + }; + } + + it('should use progress.label.formatter for labels in progress', function () { + chart.setOption(makeOption({ + axisType: 'category', + currentIndex: 1, + data: ['2001', '2002', '2003'], + label: { + formatter: 'normal {value}' + }, + progress: { + label: { + formatter: 'progress {value}' + } + } + })); + + expect(getTexts()).toEqual(['progress 2001', 'progress 2002', 'normal 2003']); + }); + + it('should call progress.label.formatter callback like label.formatter', function () { + const normalCalls: unknown[][] = []; + const progressCalls: unknown[][] = []; + + chart.setOption(makeOption({ + axisType: 'category', + currentIndex: 0, + data: ['2001', '2002', '2003'], + label: { + formatter: function (value, index) { + normalCalls.push([value, index]); + return 'normal ' + value; + } + }, + progress: { + label: { + formatter: function (value, index) { + progressCalls.push([value, index]); + return 'progress ' + value + ' ' + index; + } + } + } + })); + + // Same arguments as the ones `label.formatter` gets for each label. + expect(progressCalls).toEqual([['2001', 0], ['2002', 1], ['2003', 2]]); + expect(normalCalls).toEqual(expect.arrayContaining(progressCalls)); + expect(getTexts()).toEqual(['progress 2001 0', 'normal 2002', 'normal 2003']); + }); + + it('should update label text when the current index changes', function () { + chart.setOption(makeOption({ + axisType: 'category', + currentIndex: 0, + data: ['2001', '2002', '2003'], + progress: { + label: { + formatter: '[{value}]' + } + } + })); + + expect(getTexts()).toEqual(['[2001]', '2002', '2003']); + + chart.dispatchAction({ + type: 'timelineChange', + currentIndex: 2 + }); + + expect(getTexts()).toEqual(['[2001]', '[2002]', '[2003]']); + + chart.dispatchAction({ + type: 'timelineChange', + currentIndex: 1 + }); + + expect(getTexts()).toEqual(['[2001]', '[2002]', '2003']); + }); + + it('should pass the same index as label.formatter on a value axis', function () { + const normalCalls: unknown[][] = []; + + chart.setOption(makeOption({ + axisType: 'value', + data: [10, 20, 30], + label: { + formatter: function (value, index) { + normalCalls.push([value, index]); + return 'normal ' + value; + } + }, + progress: { + label: { + formatter: function (value, index) { + return 'progress ' + value + ' ' + index; + } + } + }, + emphasis: { + label: { + formatter: function (value, index) { + return 'emphasis ' + value + ' ' + index; + } + } + } + })); + + const labels = getLabels(); + expect(normalCalls).toEqual(expect.arrayContaining([[10, 0], [20, 1], [30, 2]])); + expect(labels.map(label => label.states.progress.style.text)) + .toEqual(['progress 10 0', 'progress 20 1', 'progress 30 2']); + expect(labels.map(label => label.states.emphasis.style.text)) + .toEqual(['emphasis 10 0', 'emphasis 20 1', 'emphasis 30 2']); + }); + + it('should use emphasis.label.formatter for the emphasis state', function () { + chart.setOption(makeOption({ + axisType: 'category', + currentIndex: 0, + data: ['2001', '2002', '2003'], + progress: { + label: { + formatter: 'progress {value}' + } + }, + emphasis: { + label: { + formatter: 'emphasis {value}' + } + } + })); + + const labels = getLabels(); + expect(labels.map(label => label.states.emphasis.style.text)) + .toEqual(['emphasis 2001', 'emphasis 2002', 'emphasis 2003']); + + // Hover on a label that is not in progress. + labels[1].useState('emphasis', true); + expect(labels[1].style.text).toEqual('emphasis 2002'); + + // Hover on a label in progress, then leave it. + labels[0].useState('emphasis', true); + expect(labels[0].style.text).toEqual('emphasis 2001'); + labels[0].removeState('emphasis'); + expect(labels[0].style.text).toEqual('progress 2001'); + }); + + it('should keep the normal text in progress and emphasis states without their own formatter', function () { + chart.setOption(makeOption({ + axisType: 'category', + currentIndex: 1, + data: ['2001', '2002', '2003'], + label: { + formatter: 'normal {value}' + }, + progress: { + label: { + color: 'red' + } + } + })); + + const labels = getLabels(); + expect(getTexts()).toEqual(['normal 2001', 'normal 2002', 'normal 2003']); + expect(labels[0].style.fill).toEqual('red'); + expect(labels[0].states.progress.style).not.toHaveProperty('text'); + expect(labels[0].states.emphasis.style).not.toHaveProperty('text'); + }); + + it('should use progress.label.formatter of a data item', function () { + chart.setOption(makeOption({ + axisType: 'category', + currentIndex: 2, + data: [ + '2001', + { + value: '2002', + progress: { + label: { + formatter: 'item {value}' + } + } + }, + '2003' + ], + progress: { + label: { + formatter: 'progress {value}' + } + } + })); + + expect(getTexts()).toEqual(['progress 2001', 'item 2002', 'progress 2003']); + }); + +});