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']); + }); + +});