| /* | |
| * 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 * as zrUtil from 'zrender/src/core/util'; | |
| import { | |
| TimeAxisLabelFormatterDictionary, | |
| TimeAxisLabelFormatterDictionaryOption, | |
| TimeAxisLabelFormatterExtraParams, | |
| TimeAxisLabelFormatterOption, | |
| TimeAxisLabelFormatterParsed, | |
| TimeAxisLabelFormatterUpperDictionary, | |
| TimeAxisLabelLeveledFormatterOption, | |
| } from './../coord/axisCommonTypes'; | |
| import * as numberUtil from './number'; | |
| import {NullUndefined, ScaleTick} from './types'; | |
| import { getDefaultLocaleModel, getLocaleModel, SYSTEM_LANG, LocaleOption } from '../core/locale'; | |
| import Model from '../model/Model'; | |
| import { getScaleBreakHelper } from '../scale/break'; | |
| export const ONE_SECOND = 1000; | |
| export const ONE_MINUTE = ONE_SECOND * 60; | |
| export const ONE_HOUR = ONE_MINUTE * 60; | |
| export const ONE_DAY = ONE_HOUR * 24; | |
| export const ONE_YEAR = ONE_DAY * 365; | |
| const primaryTimeUnitFormatterMatchers: {[key in PrimaryTimeUnit]: RegExp} = { | |
| year: /({yyyy}|{yy})/, | |
| month: /({MMMM}|{MMM}|{MM}|{M})/, | |
| day: /({dd}|{d})/, | |
| hour: /({HH}|{H}|{hh}|{h})/, | |
| minute: /({mm}|{m})/, | |
| second: /({ss}|{s})/, | |
| millisecond: /({SSS}|{S})/, | |
| } as const; | |
| const defaultFormatterSeed: {[key in PrimaryTimeUnit]: string} = { | |
| year: '{yyyy}', | |
| month: '{MMM}', | |
| day: '{d}', | |
| hour: '{HH}:{mm}', | |
| minute: '{HH}:{mm}', | |
| second: '{HH}:{mm}:{ss}', | |
| millisecond: '{HH}:{mm}:{ss} {SSS}', | |
| } as const; | |
| const defaultFullFormatter = '{yyyy}-{MM}-{dd} {HH}:{mm}:{ss} {SSS}'; | |
| const fullDayFormatter = '{yyyy}-{MM}-{dd}'; | |
| export const fullLeveledFormatter = { | |
| year: '{yyyy}', | |
| month: '{yyyy}-{MM}', | |
| day: fullDayFormatter, | |
| hour: fullDayFormatter + ' ' + defaultFormatterSeed.hour, | |
| minute: fullDayFormatter + ' ' + defaultFormatterSeed.minute, | |
| second: fullDayFormatter + ' ' + defaultFormatterSeed.second, | |
| millisecond: defaultFullFormatter | |
| }; | |
| export type JSDateGetterNames = | |
| 'getUTCFullYear' | 'getFullYear' | |
| | 'getUTCMonth' | 'getMonth' | |
| | 'getUTCDate' | 'getDate' | |
| | 'getUTCHours' | 'getHours' | |
| | 'getUTCMinutes' | 'getMinutes' | |
| | 'getUTCSeconds' | 'getSeconds' | |
| | 'getUTCMilliseconds' | 'getMilliseconds' | |
| ; | |
| export type JSDateSetterNames = | |
| 'setUTCFullYear' | 'setFullYear' | |
| | 'setUTCMonth' | 'setMonth' | |
| | 'setUTCDate' | 'setDate' | |
| | 'setUTCHours' | 'setHours' | |
| | 'setUTCMinutes' | 'setMinutes' | |
| | 'setUTCSeconds' | 'setSeconds' | |
| | 'setUTCMilliseconds' | 'setMilliseconds' | |
| ; | |
| export type PrimaryTimeUnit = (typeof primaryTimeUnits)[number]; | |
| export type TimeUnit = (typeof timeUnits)[number]; | |
| // Order must be ensured from big to small. | |
| export const primaryTimeUnits = [ | |
| 'year', 'month', 'day', 'hour', 'minute', 'second', 'millisecond' | |
| ] as const; | |
| export const timeUnits = [ | |
| 'year', 'half-year', 'quarter', 'month', 'week', 'half-week', 'day', | |
| 'half-day', 'quarter-day', 'hour', 'minute', 'second', 'millisecond' | |
| ] as const; | |
| export function parseTimeAxisLabelFormatter( | |
| formatter: TimeAxisLabelFormatterOption | |
| ): TimeAxisLabelFormatterParsed { | |
| // Keep the logic the same with function `leveledFormat`. | |
| return (!zrUtil.isString(formatter) && !zrUtil.isFunction(formatter)) | |
| ? parseTimeAxisLabelFormatterDictionary(formatter) | |
| : formatter; | |
| } | |
| /** | |
| * The final generated dictionary is like: | |
| * generated_dict = { | |
| * year: { | |
| * year: ['{yyyy}', ...<higher_levels_if_any>] | |
| * }, | |
| * month: { | |
| * year: ['{yyyy} {MMM}', ...<higher_levels_if_any>], | |
| * month: ['{MMM}', ...<higher_levels_if_any>] | |
| * }, | |
| * day: { | |
| * year: ['{yyyy} {MMM} {d}', ...<higher_levels_if_any>], | |
| * month: ['{MMM} {d}', ...<higher_levels_if_any>], | |
| * day: ['{d}', ...<higher_levels_if_any>] | |
| * }, | |
| * ... | |
| * } | |
| * | |
| * In echarts option, users can specify the entire dictionary or typically just: | |
| * {formatter: { | |
| * year: '{yyyy}', // Or an array of leveled templates: `['{yyyy}', '{bold1|{yyyy}}', ...]`, | |
| * // corresponding to `[level0, level1, level2, ...]`. | |
| * month: '{MMM}', | |
| * day: '{d}', | |
| * hour: '{HH}:{mm}', | |
| * second: '{HH}:{mm}', | |
| * ... | |
| * }} | |
| * If any time unit is not specified in echarts option, the default template is used, | |
| * such as `['{yyyy}', {primary|{yyyy}']`. | |
| * | |
| * The `tick.level` is only used to read string from each array, meaning the style type. | |
| * | |
| * Let `lowerUnit = getUnitFromValue(tick.value)`. | |
| * The non-break axis ticks only use `generated_dict[lowerUnit][lowerUnit][level]`. | |
| * The break axis ticks may use `generated_dict[lowerUnit][upperUnit][level]`, because: | |
| * Consider the case: the non-break ticks are `16th, 23th, Feb, 7th, ...`, where `Feb` is in the break | |
| * range and pruned by breaks, and the break ends might be in lower time unit than day. e.g., break start | |
| * is `Jan 25th 18:00`(in unit `hour`) and break end is `Feb 6th 18:30` (in unit `minute`). Thus the break | |
| * label prefers `Jan 25th 18:00` and `Feb 6th 18:30` rather than only `18:00` and `18:30`, otherwise it | |
| * causes misleading. | |
| * In this case, the tick of the break start and end will both be: | |
| * `{level: 1, lowerTimeUnit: 'minute', upperTimeUnit: 'month'}` | |
| * And get the final template by `generated_dict[lowerTimeUnit][upperTimeUnit][level]`. | |
| * Note that the time unit can not be calculated directly by a single tick value, since the two breaks have | |
| * to be at the same time unit to avoid awkward appearance. i.e., `Jan 25th 18:00` is in the time unit "hour" | |
| * but we need it to be "minute", following `Feb 6th 18:30`. | |
| */ | |
| function parseTimeAxisLabelFormatterDictionary( | |
| dictOption: TimeAxisLabelFormatterDictionaryOption | NullUndefined | |
| ): TimeAxisLabelFormatterDictionary { | |
| dictOption = dictOption || {}; | |
| const dict = {} as TimeAxisLabelFormatterDictionary; | |
| // Currently if any template is specified by user, it may contain rich text tag, | |
| // such as `'{my_bold|{YYYY}}'`, thus we do add highlight style to it. | |
| // (Note that nested tag (`'{some|{some2|xxx}}'`) in rich text is not supported yet.) | |
| let canAddHighlight = true; | |
| zrUtil.each(primaryTimeUnits, lowestUnit => { | |
| canAddHighlight &&= dictOption[lowestUnit] == null; | |
| }); | |
| zrUtil.each(primaryTimeUnits, (lowestUnit, lowestUnitIdx) => { | |
| const upperDictOption = dictOption[lowestUnit]; | |
| dict[lowestUnit] = {} as TimeAxisLabelFormatterUpperDictionary; | |
| let lowerTpl: string | null = null; | |
| for (let upperUnitIdx = lowestUnitIdx; upperUnitIdx >= 0; upperUnitIdx--) { | |
| const upperUnit = primaryTimeUnits[upperUnitIdx]; | |
| const upperDictItemOption: TimeAxisLabelLeveledFormatterOption = | |
| (zrUtil.isObject(upperDictOption) && !zrUtil.isArray(upperDictOption)) | |
| ? upperDictOption[upperUnit] | |
| : upperDictOption; | |
| let tplArr: string[]; | |
| if (zrUtil.isArray(upperDictItemOption)) { | |
| tplArr = upperDictItemOption.slice(); | |
| lowerTpl = tplArr[0] || ''; | |
| } | |
| else if (zrUtil.isString(upperDictItemOption)) { | |
| lowerTpl = upperDictItemOption; | |
| tplArr = [lowerTpl]; | |
| } | |
| else { | |
| if (lowerTpl == null) { | |
| lowerTpl = defaultFormatterSeed[lowestUnit]; | |
| } | |
| // Generate the dict by the rule as follows: | |
| // If the user specify (or by default): | |
| // {formatter: { | |
| // year: '{yyyy}', | |
| // month: '{MMM}', | |
| // day: '{d}', | |
| // ... | |
| // }} | |
| // Concat them to make the final dictionary: | |
| // {formatter: { | |
| // year: {year: ['{yyyy}']}, | |
| // month: {year: ['{yyyy} {MMM}'], month: ['{MMM}']}, | |
| // day: {year: ['{yyyy} {MMM} {d}'], month: ['{MMM} {d}'], day: ['{d}']} | |
| // ... | |
| // }} | |
| // And then add `{primary|...}` to each array if from default template. | |
| // 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)) { | |
| lowerTpl = `${dict[upperUnit][upperUnit][0]} ${lowerTpl}`; | |
| } | |
| tplArr = [lowerTpl]; | |
| if (canAddHighlight) { | |
| tplArr[1] = `{primary|${lowerTpl}}`; | |
| } | |
| } | |
| dict[lowestUnit][upperUnit] = tplArr; | |
| } | |
| }); | |
| return dict; | |
| } | |
| export function pad(str: string | number, len: number): string { | |
| str += ''; | |
| return '0000'.substr(0, len - (str as string).length) + str; | |
| } | |
| export function getPrimaryTimeUnit(timeUnit: TimeUnit): PrimaryTimeUnit { | |
| switch (timeUnit) { | |
| case 'half-year': | |
| case 'quarter': | |
| return 'month'; | |
| case 'week': | |
| case 'half-week': | |
| return 'day'; | |
| case 'half-day': | |
| case 'quarter-day': | |
| return 'hour'; | |
| default: | |
| // year, minutes, second, milliseconds | |
| return timeUnit; | |
| } | |
| } | |
| export function isPrimaryTimeUnit(timeUnit: TimeUnit): boolean { | |
| return timeUnit === getPrimaryTimeUnit(timeUnit); | |
| } | |
| export function getDefaultFormatPrecisionOfInterval(timeUnit: PrimaryTimeUnit): PrimaryTimeUnit { | |
| switch (timeUnit) { | |
| case 'year': | |
| case 'month': | |
| return 'day'; | |
| case 'millisecond': | |
| return 'millisecond'; | |
| default: | |
| // Also for day, hour, minute, second | |
| return 'second'; | |
| } | |
| } | |
| export function format( | |
| // Note: The result based on `isUTC` are totally different, which can not be just simply | |
| // substituted by the result without `isUTC`. So we make the param `isUTC` mandatory. | |
| time: unknown, template: string, isUTC: boolean, lang?: string | Model<LocaleOption> | |
| ): string { | |
| const date = numberUtil.parseDate(time); | |
| const y = date[fullYearGetterName(isUTC)](); | |
| const M = date[monthGetterName(isUTC)]() + 1; | |
| const q = Math.floor((M - 1) / 3) + 1; | |
| const d = date[dateGetterName(isUTC)](); | |
| const e = date['get' + (isUTC ? 'UTC' : '') + 'Day' as 'getDay' | 'getUTCDay'](); | |
| const H = date[hoursGetterName(isUTC)](); | |
| const h = (H - 1) % 12 + 1; | |
| const m = date[minutesGetterName(isUTC)](); | |
| const s = date[secondsGetterName(isUTC)](); | |
| const S = date[millisecondsGetterName(isUTC)](); | |
| const a = H >= 12 ? 'pm' : 'am'; | |
| const A = a.toUpperCase(); | |
| const localeModel = lang instanceof Model ? lang | |
| : getLocaleModel(lang || SYSTEM_LANG) || getDefaultLocaleModel(); | |
| const timeModel = localeModel.getModel('time'); | |
| const month = timeModel.get('month'); | |
| const monthAbbr = timeModel.get('monthAbbr'); | |
| const dayOfWeek = timeModel.get('dayOfWeek'); | |
| const dayOfWeekAbbr = timeModel.get('dayOfWeekAbbr'); | |
| return (template || '') | |
| .replace(/{a}/g, a + '') | |
| .replace(/{A}/g, A + '') | |
| .replace(/{yyyy}/g, y + '') | |
| .replace(/{yy}/g, pad(y % 100 + '', 2)) | |
| .replace(/{Q}/g, q + '') | |
| .replace(/{MMMM}/g, month[M - 1]) | |
| .replace(/{MMM}/g, monthAbbr[M - 1]) | |
| .replace(/{MM}/g, pad(M, 2)) | |
| .replace(/{M}/g, M + '') | |
| .replace(/{dd}/g, pad(d, 2)) | |
| .replace(/{d}/g, d + '') | |
| .replace(/{eeee}/g, dayOfWeek[e]) | |
| .replace(/{ee}/g, dayOfWeekAbbr[e]) | |
| .replace(/{e}/g, e + '') | |
| .replace(/{HH}/g, pad(H, 2)) | |
| .replace(/{H}/g, H + '') | |
| .replace(/{hh}/g, pad(h + '', 2)) | |
| .replace(/{h}/g, h + '') | |
| .replace(/{mm}/g, pad(m, 2)) | |
| .replace(/{m}/g, m + '') | |
| .replace(/{ss}/g, pad(s, 2)) | |
| .replace(/{s}/g, s + '') | |
| .replace(/{SSS}/g, pad(S, 3)) | |
| .replace(/{S}/g, S + ''); | |
| } | |
| export function leveledFormat( | |
| tick: ScaleTick, | |
| idx: number, | |
| formatter: TimeAxisLabelFormatterParsed, | |
| lang: string | Model<LocaleOption>, | |
| isUTC: boolean | |
| ) { | |
| let template = null; | |
| if (zrUtil.isString(formatter)) { | |
| // Single formatter for all units at all levels | |
| 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); | |
| } | |
| else { | |
| const tickTime = tick.time; | |
| if (tickTime) { | |
| const leveledTplArr = formatter[tickTime.lowerTimeUnit][tickTime.upperTimeUnit]; | |
| template = leveledTplArr[Math.min(tickTime.level, leveledTplArr.length - 1)] || ''; | |
| } | |
| else { | |
| // tick may be from customTicks or timeline therefore no tick.time. | |
| const unit = getUnitFromValue(tick.value, isUTC); | |
| template = formatter[unit][unit][0]; | |
| } | |
| } | |
| return format(new Date(tick.value), template, isUTC, lang); | |
| } | |
| export function getUnitFromValue( | |
| value: number | string | Date, | |
| isUTC: boolean | |
| ): PrimaryTimeUnit { | |
| const date = numberUtil.parseDate(value); | |
| const M = (date as any)[monthGetterName(isUTC)]() + 1; | |
| const d = (date as any)[dateGetterName(isUTC)](); | |
| const h = (date as any)[hoursGetterName(isUTC)](); | |
| const m = (date as any)[minutesGetterName(isUTC)](); | |
| const s = (date as any)[secondsGetterName(isUTC)](); | |
| const S = (date as any)[millisecondsGetterName(isUTC)](); | |
| const isSecond = S === 0; | |
| const isMinute = isSecond && s === 0; | |
| const isHour = isMinute && m === 0; | |
| const isDay = isHour && h === 0; | |
| const isMonth = isDay && d === 1; | |
| const isYear = isMonth && M === 1; | |
| if (isYear) { | |
| return 'year'; | |
| } | |
| else if (isMonth) { | |
| return 'month'; | |
| } | |
| else if (isDay) { | |
| return 'day'; | |
| } | |
| else if (isHour) { | |
| return 'hour'; | |
| } | |
| else if (isMinute) { | |
| return 'minute'; | |
| } | |
| else if (isSecond) { | |
| return 'second'; | |
| } | |
| else { | |
| return 'millisecond'; | |
| } | |
| } | |
| // export function getUnitValue( | |
| // value: number | Date, | |
| // unit: TimeUnit, | |
| // isUTC: boolean | |
| // ) : number { | |
| // const date = zrUtil.isNumber(value) | |
| // ? numberUtil.parseDate(value) | |
| // : value; | |
| // unit = unit || getUnitFromValue(value, isUTC); | |
| // switch (unit) { | |
| // case 'year': | |
| // return date[fullYearGetterName(isUTC)](); | |
| // case 'half-year': | |
| // return date[monthGetterName(isUTC)]() >= 6 ? 1 : 0; | |
| // case 'quarter': | |
| // return Math.floor((date[monthGetterName(isUTC)]() + 1) / 4); | |
| // case 'month': | |
| // return date[monthGetterName(isUTC)](); | |
| // case 'day': | |
| // return date[dateGetterName(isUTC)](); | |
| // case 'half-day': | |
| // return date[hoursGetterName(isUTC)]() / 24; | |
| // case 'hour': | |
| // return date[hoursGetterName(isUTC)](); | |
| // case 'minute': | |
| // return date[minutesGetterName(isUTC)](); | |
| // case 'second': | |
| // return date[secondsGetterName(isUTC)](); | |
| // case 'millisecond': | |
| // return date[millisecondsGetterName(isUTC)](); | |
| // } | |
| // } | |
| /** | |
| * e.g., | |
| * If timeUnit is 'year', return the Jan 1st 00:00:00 000 of that year. | |
| * If timeUnit is 'day', return the 00:00:00 000 of that day. | |
| * | |
| * @return The input date. | |
| */ | |
| export function roundTime(date: Date, timeUnit: PrimaryTimeUnit, isUTC: boolean): Date { | |
| switch (timeUnit) { | |
| case 'year': | |
| date[monthSetterName(isUTC)](0); | |
| case 'month': | |
| date[dateSetterName(isUTC)](1); | |
| case 'day': | |
| date[hoursSetterName(isUTC)](0); | |
| case 'hour': | |
| date[minutesSetterName(isUTC)](0); | |
| case 'minute': | |
| date[secondsSetterName(isUTC)](0); | |
| case 'second': | |
| date[millisecondsSetterName(isUTC)](0); | |
| } | |
| return date; | |
| } | |
| export function fullYearGetterName(isUTC: boolean) { | |
| return isUTC ? 'getUTCFullYear' : 'getFullYear'; | |
| } | |
| export function monthGetterName(isUTC: boolean) { | |
| return isUTC ? 'getUTCMonth' : 'getMonth'; | |
| } | |
| export function dateGetterName(isUTC: boolean) { | |
| return isUTC ? 'getUTCDate' : 'getDate'; | |
| } | |
| export function hoursGetterName(isUTC: boolean) { | |
| return isUTC ? 'getUTCHours' : 'getHours'; | |
| } | |
| export function minutesGetterName(isUTC: boolean) { | |
| return isUTC ? 'getUTCMinutes' : 'getMinutes'; | |
| } | |
| export function secondsGetterName(isUTC: boolean) { | |
| return isUTC ? 'getUTCSeconds' : 'getSeconds'; | |
| } | |
| export function millisecondsGetterName(isUTC: boolean) { | |
| return isUTC ? 'getUTCMilliseconds' : 'getMilliseconds'; | |
| } | |
| export function fullYearSetterName(isUTC: boolean) { | |
| return isUTC ? 'setUTCFullYear' : 'setFullYear'; | |
| } | |
| export function monthSetterName(isUTC: boolean) { | |
| return isUTC ? 'setUTCMonth' : 'setMonth'; | |
| } | |
| export function dateSetterName(isUTC: boolean) { | |
| return isUTC ? 'setUTCDate' : 'setDate'; | |
| } | |
| export function hoursSetterName(isUTC: boolean) { | |
| return isUTC ? 'setUTCHours' : 'setHours'; | |
| } | |
| export function minutesSetterName(isUTC: boolean) { | |
| return isUTC ? 'setUTCMinutes' : 'setMinutes'; | |
| } | |
| export function secondsSetterName(isUTC: boolean) { | |
| return isUTC ? 'setUTCSeconds' : 'setSeconds'; | |
| } | |
| export function millisecondsSetterName(isUTC: boolean) { | |
| return isUTC ? 'setUTCMilliseconds' : 'setMilliseconds'; | |
| } |