diff --git a/src/coord/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts index 058dea900e..9dc0e88603 100644 --- a/src/coord/axisCommonTypes.ts +++ b/src/coord/axisCommonTypes.ts @@ -299,7 +299,9 @@ export type TimeAxisLabelFormatterExtraParams = { level: number, } & AxisLabelFormatterExtraParams; -export type TimeAxisLabelLeveledFormatterOption = string[] | string; +export type TimeAxisLabelLeveledFormatterOption = string + | AxisLabelTimeFormatter + | (string | AxisLabelTimeFormatter)[]; export type TimeAxisLabelFormatterUpperDictionaryOption = {[key in PrimaryTimeUnit]?: TimeAxisLabelLeveledFormatterOption}; /** @@ -318,7 +320,7 @@ export type TimeAxisLabelFormatterParsed = string // This is the parsed result from TimeAxisLabelFormatterDictionaryOption. export type TimeAxisLabelFormatterDictionary = {[key in PrimaryTimeUnit]: TimeAxisLabelFormatterUpperDictionary}; -export type TimeAxisLabelFormatterUpperDictionary = {[key in PrimaryTimeUnit]: string[]}; +export type TimeAxisLabelFormatterUpperDictionary = {[key in PrimaryTimeUnit]: (string | AxisLabelTimeFormatter)[]}; type LabelFormatters = { value: AxisLabelValueFormatter | string diff --git a/src/util/time.ts b/src/util/time.ts index 6c6ed6bfe8..15d1168a15 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -19,12 +19,14 @@ import * as zrUtil from 'zrender/src/core/util'; import { + AxisLabelTimeFormatter, TimeAxisLabelFormatterDictionary, TimeAxisLabelFormatterDictionaryOption, TimeAxisLabelFormatterExtraParams, TimeAxisLabelFormatterOption, TimeAxisLabelFormatterParsed, TimeAxisLabelFormatterUpperDictionary, + TimeAxisLabelFormatterUpperDictionaryOption, TimeAxisLabelLeveledFormatterOption, } from './../coord/axisCommonTypes'; import * as numberUtil from './number'; @@ -137,6 +139,9 @@ export function parseTimeAxisLabelFormatter( * {formatter: { * year: '{yyyy}', // Or an array of leveled templates: `['{yyyy}', '{bold1|{yyyy}}', ...]`, * // corresponding to `[level0, level1, level2, ...]`. + * // A formatter function `(value, index, extra) => string` can be + * // used in place of a string template at any of these positions; + * // its return value is then used as the template. * month: '{MMM}', * day: '{d}', * hour: '{HH}:{mm}', @@ -181,15 +186,16 @@ function parseTimeAxisLabelFormatterDictionary( const upperDictOption = dictOption[lowestUnit]; dict[lowestUnit] = {} as TimeAxisLabelFormatterUpperDictionary; - let lowerTpl: string | null = null; + let lowerTpl: string | AxisLabelTimeFormatter | null = null; for (let upperUnitIdx = lowestUnitIdx; upperUnitIdx >= 0; upperUnitIdx--) { const upperUnit = primaryTimeUnits[upperUnitIdx]; const upperDictItemOption: TimeAxisLabelLeveledFormatterOption = - (zrUtil.isObject(upperDictOption) && !zrUtil.isArray(upperDictOption)) - ? upperDictOption[upperUnit] + (zrUtil.isObject(upperDictOption) && !zrUtil.isArray(upperDictOption) + && !zrUtil.isFunction(upperDictOption)) + ? (upperDictOption as TimeAxisLabelFormatterUpperDictionaryOption)[upperUnit] : upperDictOption; - let tplArr: string[]; + let tplArr: (string | AxisLabelTimeFormatter)[]; if (zrUtil.isArray(upperDictItemOption)) { tplArr = upperDictItemOption.slice(); lowerTpl = tplArr[0] || ''; @@ -198,6 +204,12 @@ function parseTimeAxisLabelFormatterDictionary( lowerTpl = upperDictItemOption; tplArr = [lowerTpl]; } + else if (zrUtil.isFunction(upperDictItemOption)) { + // A formatter function is kept as-is and called at formatting + // time, the same as a top-level formatter function. + lowerTpl = upperDictItemOption; + tplArr = [lowerTpl]; + } else { if (lowerTpl == null) { lowerTpl = defaultFormatterSeed[lowestUnit]; @@ -221,11 +233,13 @@ function parseTimeAxisLabelFormatterDictionary( // This strategy is convinient for user configurating and works for most cases. // If bad cases encountered, users can specify the entire dictionary themselves // instead of going through this logic. - else if (!primaryTimeUnitFormatterMatchers[upperUnit].test(lowerTpl)) { + else if (zrUtil.isString(lowerTpl) + && zrUtil.isString(dict[upperUnit][upperUnit][0]) + && !primaryTimeUnitFormatterMatchers[upperUnit].test(lowerTpl)) { lowerTpl = `${dict[upperUnit][upperUnit][0]} ${lowerTpl}`; } tplArr = [lowerTpl]; - if (canAddHighlight) { + if (canAddHighlight && zrUtil.isString(lowerTpl)) { tplArr[1] = `{primary|${lowerTpl}}`; } } @@ -341,15 +355,7 @@ export function leveledFormat( template = formatter; } else if (zrUtil.isFunction(formatter)) { - const extra: TimeAxisLabelFormatterExtraParams = { - time: tick.time, - level: tick.time ? tick.time.level : 0, - }; - const scaleBreakHelper = getScaleBreakHelper(); - if (scaleBreakHelper) { - scaleBreakHelper.makeAxisLabelFormatterParamBreak(extra, tick.break); - } - template = formatter(tick.value, idx, extra); + template = formatter(tick.value, idx, getTimeAxisLabelFormatterExtraParams(tick)); } else { const tickTime = tick.time; @@ -362,11 +368,29 @@ export function leveledFormat( const unit = getUnitFromValue(tick.value, isUTC); template = formatter[unit][unit][0]; } + if (zrUtil.isFunction(template)) { + // A formatter function specified for a single time unit or level + // is called the same as a top-level formatter function, and its + // return value is used as the template for `format`. + template = template(tick.value, idx, getTimeAxisLabelFormatterExtraParams(tick)); + } } return format(new Date(tick.value), template, isUTC, lang); } +function getTimeAxisLabelFormatterExtraParams(tick: ScaleTick): TimeAxisLabelFormatterExtraParams { + const extra: TimeAxisLabelFormatterExtraParams = { + time: tick.time, + level: tick.time ? tick.time.level : 0, + }; + const scaleBreakHelper = getScaleBreakHelper(); + if (scaleBreakHelper) { + scaleBreakHelper.makeAxisLabelFormatterParamBreak(extra, tick.break); + } + return extra; +} + export function getUnitFromValue( value: number | string | Date, isUTC: boolean diff --git a/test/ut/spec/util/time.test.ts b/test/ut/spec/util/time.test.ts old mode 100755 new mode 100644 index 60d5467fc7..d1dafdd614 --- a/test/ut/spec/util/time.test.ts +++ b/test/ut/spec/util/time.test.ts @@ -19,8 +19,9 @@ */ import { - format, roundTime + format, leveledFormat, parseTimeAxisLabelFormatter, roundTime } from '@/src/util/time'; +import {TimeScaleTick} from '@/src/util/types'; describe('util/time', function () { @@ -142,6 +143,89 @@ describe('util/time', function () { }); }); + describe('leveledFormat', function () { + + const dayTick: TimeScaleTick = { + value: new Date('2024-06-13T10:20:30.000Z').getTime(), + time: {level: 0, lowerTimeUnit: 'day', upperTimeUnit: 'day'} + }; + const monthTick: TimeScaleTick = { + value: new Date('2024-06-13T10:20:30.000Z').getTime(), + time: {level: 0, lowerTimeUnit: 'month', upperTimeUnit: 'month'} + }; + const yearTick: TimeScaleTick = { + value: new Date('2024-01-01T00:00:00.000Z').getTime(), + time: {level: 0, lowerTimeUnit: 'year', upperTimeUnit: 'year'} + }; + + // See https://github.com/apache/echarts/issues/20027 + it('should support formatter functions for each time unit', function () { + const formatter = parseTimeAxisLabelFormatter({ + year: (value: number) => value + ' {yyyy}', + month: (value: number) => value + '{MMM}', + day: (value: number) => value + '{d}. {MMM}', + hour: (value: number) => value + '{HH}:{mm}\n{d}. {MMM}', + minute: (value: number) => value + '{HH}:{mm}\n{d}. {MMM}', + second: (value: number) => value + '{HH}:{mm}:{ss}\n{d}. {MMM}', + millisecond: (value: number) => value + '{hh}:{mm}:{ss} {SSS}\n{d}. {MMM}' + }); + expect(leveledFormat(dayTick, 0, formatter, 'en', true)) + .toEqual('171827403000013. Jun'); + expect(leveledFormat(yearTick, 0, formatter, 'en', true)) + .toEqual('1704067200000 2024'); + }); + + it('should support formatter functions in leveled template arrays', function () { + const formatter = parseTimeAxisLabelFormatter({ + day: ['{d}', (value: number) => 'LEVEL1:' + value] + }); + expect(leveledFormat(dayTick, 0, formatter, 'en', true)).toEqual('13'); + const level1Tick: TimeScaleTick = { + value: dayTick.value, + time: {level: 1, lowerTimeUnit: 'day', upperTimeUnit: 'day'} + }; + expect(leveledFormat(level1Tick, 0, formatter, 'en', true)) + .toEqual('LEVEL1:1718274030000'); + }); + + it('should support formatter functions in the nested dictionary form', function () { + const formatter = parseTimeAxisLabelFormatter({ + day: {year: (value: number) => 'Y:' + value} + }); + // The day tick itself still uses the default day template. + expect(leveledFormat(dayTick, 0, formatter, 'en', true)).toEqual('13'); + const breakTick: TimeScaleTick = { + value: dayTick.value, + time: {level: 0, lowerTimeUnit: 'day', upperTimeUnit: 'year'} + }; + expect(leveledFormat(breakTick, 0, formatter, 'en', true)) + .toEqual('Y:1718274030000'); + }); + + it('should pass value, index and extra params to formatter functions', function () { + const formatter = parseTimeAxisLabelFormatter({ + day: (value: number, index: number, extra: any) => + 'v' + value + ' i' + index + ' l' + extra.level + }); + const level1Tick: TimeScaleTick = { + value: dayTick.value, + time: {level: 1, lowerTimeUnit: 'day', upperTimeUnit: 'day'} + }; + expect(leveledFormat(level1Tick, 2, formatter, 'en', true)) + .toEqual('v1718274030000 i2 l1'); + }); + + it('should keep default templates for units without a formatter function', function () { + const formatter = parseTimeAxisLabelFormatter({ + day: (value: number) => 'D:' + value + }); + expect(leveledFormat(dayTick, 0, formatter, 'en', true)) + .toEqual('D:1718274030000'); + expect(leveledFormat(monthTick, 0, formatter, 'en', true)).toEqual('Jun'); + expect(leveledFormat(yearTick, 0, formatter, 'en', true)).toEqual('2024'); + }); + }); + describe('roundTime', function () { it('roundTime_UTC', function () { expect(roundTime(new Date(0), 'year', true).toISOString()).toEqual('1970-01-01T00:00:00.000Z');