From e44445dac2f0efee032d674ee0648d14d31a397e Mon Sep 17 00:00:00 2001 From: Ovilia Date: Wed, 24 Jun 2020 17:42:41 +0800 Subject: [PATCH 01/11] feat(time): improve time axis formatter --- src/coord/axisDefault.ts | 4 +- src/scale/Time.ts | 304 ++++++++++++++++++++++++++++++++++++++- src/util/format.ts | 23 ++- 3 files changed, 319 insertions(+), 12 deletions(-) diff --git a/src/coord/axisDefault.ts b/src/coord/axisDefault.ts index 3777855754..770b30acf1 100644 --- a/src/coord/axisDefault.ts +++ b/src/coord/axisDefault.ts @@ -163,8 +163,8 @@ const valueAxis: AxisBaseOption = zrUtil.merge({ const timeAxis: AxisBaseOption = zrUtil.defaults({ scale: true, - min: 'dataMin', - max: 'dataMax' + // min: 'dataMin', + // max: 'dataMax' }, valueAxis); const logAxis: AxisBaseOption = zrUtil.defaults({ diff --git a/src/scale/Time.ts b/src/scale/Time.ts index f2f5734656..987a9ddc1e 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -82,9 +82,63 @@ class TimeScale extends IntervalScale { getLabel(val: number): string { const stepLvl = this._stepLvl; - const date = new Date(val); + const labelFormatType = getLabelFormatType(val, this.getSetting('useUTC'), false); + return formatUtil.formatTime(labelFormatType, val); + } + + /** + * @override + * @param expandToNicedExtent Whether expand the ticks to niced extent. + */ + getTicks(expandToNicedExtent?: boolean): number[] { + const interval = this._interval; + const extent = this._extent; + const niceTickExtent = this._niceExtent; + + let ticks = [] as number[]; + // If interval is 0, return []; + if (!interval) { + return ticks; + } + + const safeLimit = 10000; - return formatUtil.formatTime(stepLvl[0], date, this.getSetting('useUTC')); + if (extent[0] < niceTickExtent[0]) { + if (expandToNicedExtent) { + ticks.push(numberUtil.round(niceTickExtent[0] - interval, 0)); + } + else { + ticks.push(extent[0]); + } + } + + const useUTC = this.getSetting('useUTC'); + + const scaleLevelsLen = primaryScaleLevels.length; + const idx = bisect(primaryScaleLevels, this._interval, 0, scaleLevelsLen); + const level = primaryScaleLevels[Math.min(idx, scaleLevelsLen - 1)]; + + const innerTicks = getLevelTicks( + level[0] as TimeAxisLabelPrimaryLevel, + useUTC, + extent + ); + console.log(innerTicks); + ticks = ticks.concat(innerTicks); + + // Consider this case: the last item of ticks is smaller + // than niceTickExtent[1] and niceTickExtent[1] === extent[1]. + const lastNiceTick = ticks.length ? ticks[ticks.length - 1] : niceTickExtent[1]; + if (extent[1] > lastNiceTick) { + if (expandToNicedExtent) { + ticks.push(numberUtil.round(lastNiceTick + interval, 0)); + } + else { + ticks.push(extent[1]); + } + } + + return ticks; } niceExtent( @@ -115,11 +169,13 @@ class TimeScale extends IntervalScale { // let extent = this._extent; const interval = this._interval; + const timezoneOffset = this.getSetting('useUTC') + ? 0 : (new Date(+extent[0] || +extent[1])).getTimezoneOffset() * 60 * 1000; if (!opt.fixMin) { - extent[0] = numberUtil.round(mathFloor(extent[0] / interval) * interval); + extent[0] = numberUtil.round(mathFloor((extent[0] - timezoneOffset) / interval) * interval) + timezoneOffset; } if (!opt.fixMax) { - extent[1] = numberUtil.round(mathCeil(extent[1] / interval) * interval); + extent[1] = numberUtil.round(mathCeil((extent[1] - timezoneOffset) / interval) * interval) + timezoneOffset; } } @@ -233,6 +289,246 @@ const scaleLevels = [ ['year', ONE_DAY * 380] // 1Y ] as [string, number][]; +const primaryScaleLevels = [ + // Format interval + ['second', ONE_SECOND], // 1s + ['minute', ONE_MINUTE], // 1m + ['hour', ONE_HOUR], // 1h + ['day', ONE_DAY], // 1d + ['week', ONE_DAY * 7], // 7d + ['month', ONE_DAY * 31], // 1M + ['year', ONE_DAY * 380] // 1Y +] as [string, number][]; + + +type TimeAxisLabelPrimaryLevel = 'millisecond' + | 'second' | 'minute' | 'hour' + | 'day' | 'month' | 'year'; +type TimeAxisLabelLevel = TimeAxisLabelPrimaryLevel + | 'week' | 'quarter' | 'half-year'; + +const primaryLevels = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond']; + +function getLabelFormatType( + value: number | string | Date, + isUTC: boolean, + primaryOnly: boolean +): TimeAxisLabelLevel { + const date = numberUtil.parseDate(value); + const utc = isUTC ? 'UTC' : ''; + const M = (date as any)['get' + utc + 'Month']() + 1; + const w = (date as any)['get' + utc + 'Day'](); + const d = (date as any)['get' + utc + 'Date'](); + const h = (date as any)['get' + utc + 'Hours'](); + const m = (date as any)['get' + utc + 'Minutes'](); + const s = (date as any)['get' + utc + 'Seconds'](); + const S = (date as any)['get' + utc + 'Milliseconds'](); + + const isSecond = S === 0; + const isMinute = isSecond && s === 0; + const isHour = isMinute && m === 0; + const isDay = isHour && h === 0; + const isWeek = isDay && w === 0; // TODO: first day to be configured + const isMonth = isDay && d === 1; + const isQuarter = isMonth && (M % 3 === 1); + const isHalfYear = isMonth && (M % 6 === 1); + const isYear = isMonth && M === 1; + + if (isYear) { + return 'year'; + } + else if (isHalfYear && !primaryOnly) { + return 'half-year'; + } + else if (isQuarter && !primaryOnly) { + return 'quarter'; + } + else if (isMonth) { + return 'month'; + } + else if (isWeek && !primaryOnly) { + return 'week'; + } + else if (isDay) { + return 'day'; + } + else if (isHour) { + return 'hour'; + } + else if (isMinute) { + return 'minute'; + } + else if (isSecond) { + return 'second'; + } + else { + return 'millisecond'; + } +} + + +function getLabelFormatValueFromLevel(value: number | Date, isUTC: boolean, level?: TimeAxisLabelLevel) : number { + const date = typeof value === 'number' + ? numberUtil.parseDate(value) as any + : value; + level = level || getLabelFormatType(value, isUTC, true); + const utc = isUTC ? 'UTC' : ''; + + switch (level) { + case 'millisecond': + return date['get' + utc + 'Milliseconds'](); + case 'second': + return date['get' + utc + 'Seconds'](); + case 'minute': + return date['get' + utc + 'Minutes'](); + case 'hour': + return date['get' + utc + 'Hours'](); + case 'day': + return date['get' + utc + 'Date'](); + case 'month': + return date['get' + utc + 'Month'](); + case 'year': + return date['get' + utc + 'FullYear'](); + } +} + + +function isLevelValueSame(level: TimeAxisLabelPrimaryLevel, valueA: number, valueB: number, isUTC: boolean): boolean { + const dateA = numberUtil.parseDate(valueA) as any; + const dateB = numberUtil.parseDate(valueB) as any; + const utc = isUTC ? 'UTC' : ''; + const isSame = (compareLevel: TimeAxisLabelPrimaryLevel) => { + console.log(getLabelFormatValueFromLevel(dateA, isUTC, compareLevel), getLabelFormatValueFromLevel(dateB, isUTC, compareLevel), dateA, dateB); + return getLabelFormatValueFromLevel(dateA, isUTC, compareLevel) + === getLabelFormatValueFromLevel(dateB, isUTC, compareLevel); + }; + + switch (level) { + case 'year': + return isSame('year'); + case 'month': + return isSame('year') && isSame('month'); + case 'day': + return isSame('year') && isSame('month') && isSame('day'); + case 'hour': + return isSame('year') && isSame('month') && isSame('day') + && isSame('hour'); + case 'minute': + return isSame('year') && isSame('month') && isSame('day') + && isSame('hour') && isSame('minute'); + case 'second': + return isSame('year') && isSame('month') && isSame('day') + && isSame('hour') && isSame('minute') && isSame('second'); + case 'millisecond': + return isSame('year') && isSame('month') && isSame('day') + && isSame('hour') && isSame('minute') && isSame('second') + && isSame('millisecond'); + } +} + + +function getLevelTicks(level: TimeAxisLabelLevel, isUTC: boolean, extent: number[]) { + const utc = isUTC ? 'UTC' : ''; + const ticks: number[] = []; + for (let i = 0; i < primaryLevels.length; ++i) { + let date = new Date(extent[0]) as any; + + if (primaryLevels[i] === 'week') { + date['set' + utc + 'Hours'](0); + date['set' + utc + 'Minutes'](0); + date['set' + utc + 'Seconds'](0); + date['set' + utc + 'Milliseconds'](0); + + let isDateWithinExtent = true; + while (isDateWithinExtent) { + const dates = date['get' + utc + 'Month']() + 1 === 2 + ? [8, 15, 22] + : [8, 16, 23]; + for (let d = 0; d < dates.length; ++d) { + date['set' + utc + 'Date'](dates[d]); + const dateTime = (date as Date).getTime(); + if (dateTime > extent[1]) { + isDateWithinExtent = false; + break; + } + else if (dateTime >= extent[0]) { + ticks.push(dateTime); + } + } + date['set' + utc + 'Month'](date['get' + utc + 'Month']() + 1); + } + } + else if (!isLevelValueSame(level as TimeAxisLabelPrimaryLevel, extent[0], extent[1], isUTC)) { + // Level value changes within extent + while (true) { + if (primaryLevels[i] === 'year') { + date['set' + utc + 'FullYear'](date['get' + utc + 'FullYear']() + 1); + date['set' + utc + 'Month'](0); + date['set' + utc + 'Date'](1); + date['set' + utc + 'Hours'](0); + date['set' + utc + 'Minutes'](0); + date['set' + utc + 'Seconds'](0); + } + else if (primaryLevels[i] === 'month') { + // This also works with Dec. + date['set' + utc + 'Month'](date['get' + utc + 'Month']() + 1); + date['set' + utc + 'Date'](1); + date['set' + utc + 'Hours'](0); + date['set' + utc + 'Minutes'](0); + date['set' + utc + 'Seconds'](0); + } + else if (primaryLevels[i] === 'day') { + date['set' + utc + 'Date'](date['get' + utc + 'Day']() + 1); + date['set' + utc + 'Hours'](0); + date['set' + utc + 'Minutes'](0); + date['set' + utc + 'Seconds'](0); + } + else if (primaryLevels[i] === 'hour') { + date['set' + utc + 'Hours'](date['get' + utc + 'Hours']() + 1); + date['set' + utc + 'Minutes'](0); + date['set' + utc + 'Seconds'](0); + } + else if (primaryLevels[i] === 'minute') { + date['set' + utc + 'Minutes'](date['get' + utc + 'Minutes']() + 1); + date['set' + utc + 'Minutes'](0); + date['set' + utc + 'Seconds'](0); + } + else if (primaryLevels[i] === 'second') { + date['set' + utc + 'Seconds'](date['get' + utc + 'Seconds']() + 1); + } + date['set' + utc + 'Milliseconds'](0); // TODO: not sure + + const dateValue = (date as Date).getTime(); + if (dateValue < extent[1]) { + ticks.push(dateValue); + } + else { + break; + } + } + } + + if (primaryLevels[i] === level) { + break; + } + } + + ticks.sort((a, b) => a - b); + if (ticks.length <= 1) { + return ticks; + } + + // Remove duplicates + const result = []; + for (let i = 1; i < ticks.length; ++i) { + if (ticks[i] !== ticks[i - 1]) { + result.push(ticks[i]); + } + } + return result; +} + + Scale.registerClass(TimeScale); export default TimeScale; diff --git a/src/util/format.ts b/src/util/format.ts index 8a5b15226b..953f644873 100644 --- a/src/util/format.ts +++ b/src/util/format.ts @@ -199,14 +199,25 @@ function pad(str: string, len: number): string { * @inner */ export function formatTime(tpl: string, value: number | string | Date, isUTC?: boolean) { - if (tpl === 'week' - || tpl === 'month' - || tpl === 'quarter' - || tpl === 'half-year' - || tpl === 'year' - ) { + if (tpl === 'year') { tpl = 'MM-dd\nyyyy'; } + else if (tpl === 'month' || tpl === 'quarter' || tpl === 'half-year' + ) { + tpl = 'M月'; + } + else if (tpl === 'week' || tpl === 'day') { + tpl = 'M/d'; + } + else if (tpl === 'hour' || tpl === 'minute') { + tpl = 'hh:mm'; + } + else if (tpl === 'second') { + tpl = 'hh:mm:ss'; + } + else if (tpl === 'millisecond') { + tpl = 'hh:mm:ss SSS'; + } const date = numberUtil.parseDate(value); const utc = isUTC ? 'UTC' : ''; From 6c6917db8bb076cd8d14eb7963148306095acc5f Mon Sep 17 00:00:00 2001 From: Ovilia Date: Mon, 29 Jun 2020 11:12:19 +0800 Subject: [PATCH 02/11] WIP --- src/scale/Time.ts | 1 + src/util/format.ts | 2 +- test/timeScale.html | 9 +++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/scale/Time.ts b/src/scale/Time.ts index 987a9ddc1e..ccaff53d10 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -83,6 +83,7 @@ class TimeScale extends IntervalScale { const stepLvl = this._stepLvl; const labelFormatType = getLabelFormatType(val, this.getSetting('useUTC'), false); + console.log(formatUtil.formatTime(labelFormatType, val)); return formatUtil.formatTime(labelFormatType, val); } diff --git a/src/util/format.ts b/src/util/format.ts index 953f644873..a25deea690 100644 --- a/src/util/format.ts +++ b/src/util/format.ts @@ -207,7 +207,7 @@ export function formatTime(tpl: string, value: number | string | Date, isUTC?: b tpl = 'M月'; } else if (tpl === 'week' || tpl === 'day') { - tpl = 'M/d'; + tpl = 'd'; } else if (tpl === 'hour' || tpl === 'minute') { tpl = 'hh:mm'; diff --git a/test/timeScale.html b/test/timeScale.html index f94e157c10..8f10bb415d 100644 --- a/test/timeScale.html +++ b/test/timeScale.html @@ -157,6 +157,15 @@ // minInterval: 3600 * 1000 * 24 // min: 'dataMin', // max: 'dataMax' + axisLabel: { + formatter: { + second: 'hh:mm:ss', + minute: 'hh:mm', + hour: function (time) { + return time.getHours(); + } + } + } } ], yAxis : [ From 7f0f8bbb1c119b349067f9ba360c23e617cbe4bd Mon Sep 17 00:00:00 2001 From: Ovilia Date: Mon, 6 Jul 2020 18:15:38 +0800 Subject: [PATCH 03/11] fix(time): eliminate labels that are not nice --- src/coord/axisDefault.ts | 9 ++++++--- src/scale/Time.ts | 11 +++-------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/coord/axisDefault.ts b/src/coord/axisDefault.ts index 770b30acf1..48a407e4f4 100644 --- a/src/coord/axisDefault.ts +++ b/src/coord/axisDefault.ts @@ -161,10 +161,13 @@ const valueAxis: AxisBaseOption = zrUtil.merge({ } }, defaultOption); -const timeAxis: AxisBaseOption = zrUtil.defaults({ +const timeAxis: AxisBaseOption = zrUtil.merge({ scale: true, - // min: 'dataMin', - // max: 'dataMax' + axisLabel: { + // To eliminate labels that are not nice + showMinLabel: false, + showMaxLabel: false + } }, valueAxis); const logAxis: AxisBaseOption = zrUtil.defaults({ diff --git a/src/scale/Time.ts b/src/scale/Time.ts index ccaff53d10..26168c9c9a 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -83,7 +83,6 @@ class TimeScale extends IntervalScale { const stepLvl = this._stepLvl; const labelFormatType = getLabelFormatType(val, this.getSetting('useUTC'), false); - console.log(formatUtil.formatTime(labelFormatType, val)); return formatUtil.formatTime(labelFormatType, val); } @@ -102,8 +101,6 @@ class TimeScale extends IntervalScale { return ticks; } - const safeLimit = 10000; - if (extent[0] < niceTickExtent[0]) { if (expandToNicedExtent) { ticks.push(numberUtil.round(niceTickExtent[0] - interval, 0)); @@ -124,7 +121,6 @@ class TimeScale extends IntervalScale { useUTC, extent ); - console.log(innerTicks); ticks = ticks.concat(innerTicks); // Consider this case: the last item of ticks is smaller @@ -399,7 +395,6 @@ function isLevelValueSame(level: TimeAxisLabelPrimaryLevel, valueA: number, valu const dateB = numberUtil.parseDate(valueB) as any; const utc = isUTC ? 'UTC' : ''; const isSame = (compareLevel: TimeAxisLabelPrimaryLevel) => { - console.log(getLabelFormatValueFromLevel(dateA, isUTC, compareLevel), getLabelFormatValueFromLevel(dateB, isUTC, compareLevel), dateA, dateB); return getLabelFormatValueFromLevel(dateA, isUTC, compareLevel) === getLabelFormatValueFromLevel(dateB, isUTC, compareLevel); }; @@ -459,7 +454,7 @@ function getLevelTicks(level: TimeAxisLabelLevel, isUTC: boolean, extent: number date['set' + utc + 'Month'](date['get' + utc + 'Month']() + 1); } } - else if (!isLevelValueSame(level as TimeAxisLabelPrimaryLevel, extent[0], extent[1], isUTC)) { + else if (!isLevelValueSame(primaryLevels[i] as TimeAxisLabelPrimaryLevel, extent[0], extent[1], isUTC)) { // Level value changes within extent while (true) { if (primaryLevels[i] === 'year') { @@ -479,7 +474,7 @@ function getLevelTicks(level: TimeAxisLabelLevel, isUTC: boolean, extent: number date['set' + utc + 'Seconds'](0); } else if (primaryLevels[i] === 'day') { - date['set' + utc + 'Date'](date['get' + utc + 'Day']() + 1); + date['set' + utc + 'Date'](date['get' + utc + 'Date']() + 1); date['set' + utc + 'Hours'](0); date['set' + utc + 'Minutes'](0); date['set' + utc + 'Seconds'](0); @@ -520,7 +515,7 @@ function getLevelTicks(level: TimeAxisLabelLevel, isUTC: boolean, extent: number } // Remove duplicates - const result = []; + const result = ticks.length ? [ticks[0]] : []; for (let i = 1; i < ticks.length; ++i) { if (ticks[i] !== ticks[i - 1]) { result.push(ticks[i]); From 9a80b830021e0b568f25defaa2284115462befc9 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Tue, 7 Jul 2020 16:56:43 +0800 Subject: [PATCH 04/11] feat(time): include level information in time axis --- src/component/dataZoom/SliderZoomView.ts | 4 +- src/component/timeline/SliderTimelineView.ts | 18 +++--- src/coord/Axis.ts | 8 ++- src/coord/axisHelper.ts | 32 +++++----- src/coord/axisTickLabelBuilder.ts | 13 ++-- src/scale/Interval.ts | 39 +++++++----- src/scale/Log.ts | 11 ++-- src/scale/Ordinal.ts | 12 ++-- src/scale/Scale.ts | 6 +- src/scale/Time.ts | 65 +++++++++++++++----- src/util/types.ts | 9 +++ 11 files changed, 141 insertions(+), 76 deletions(-) diff --git a/src/component/dataZoom/SliderZoomView.ts b/src/component/dataZoom/SliderZoomView.ts index d49e9a8db4..fe181410ae 100644 --- a/src/component/dataZoom/SliderZoomView.ts +++ b/src/component/dataZoom/SliderZoomView.ts @@ -667,7 +667,9 @@ class SliderZoomView extends DataZoomView { ? '' // FIXME Glue code : (axis.type === 'category' || axis.type === 'time') - ? axis.scale.getLabel(Math.round(value as number)) + ? axis.scale.getLabel({ + value: Math.round(value as number) + }) // param of toFixed should less then 20. : (value as number).toFixed(Math.min(labelPrecision as number, 20)); diff --git a/src/component/timeline/SliderTimelineView.ts b/src/component/timeline/SliderTimelineView.ts index e8dc40d536..19f7d3dc97 100644 --- a/src/component/timeline/SliderTimelineView.ts +++ b/src/component/timeline/SliderTimelineView.ts @@ -31,7 +31,7 @@ import ExtensionAPI from '../../ExtensionAPI'; import { merge, each, extend, clone, isString, bind } from 'zrender/src/core/util'; import SliderTimelineModel from './SliderTimelineModel'; import ComponentView from '../../view/Component'; -import { LayoutOrient, ZRTextAlign, ZRTextVerticalAlign, ZRElementEvent } from '../../util/types'; +import { LayoutOrient, ZRTextAlign, ZRTextVerticalAlign, ZRElementEvent, ScaleTick } from '../../util/types'; import TimelineModel, { TimelineDataItemOption, TimelineCheckpointStyle } from './TimelineModel'; import { TimelineChangePayload, TimelinePlayChangePayload } from './timelineAction'; import Model from '../../model/Model'; @@ -115,7 +115,7 @@ class SliderTimelineView extends TimelineView { const axis = this._axis = this._createAxis(layoutInfo, timelineModel); timelineModel.formatTooltip = function (dataIndex: number) { - return encodeHTML(axis.scale.getLabel(dataIndex)); + return encodeHTML(axis.scale.getLabel({value: dataIndex})); }; each( @@ -328,7 +328,7 @@ class SliderTimelineView extends TimelineView { // Customize scale. The `tickValue` is `dataIndex`. scale.getTicks = function () { return data.mapArray(['value'], function (value: number) { - return value; + return {value}; }); }; @@ -388,21 +388,21 @@ class SliderTimelineView extends TimelineView { const ticks = axis.scale.getTicks(); // The value is dataIndex, see the costomized scale. - each(ticks, function (value) { - const tickCoord = axis.dataToCoord(value); - const itemModel = data.getItemModel(value); + each(ticks, function (tick: ScaleTick) { + const tickCoord = axis.dataToCoord(tick.value); + const itemModel = data.getItemModel(tick.value); const itemStyleModel = itemModel.getModel('itemStyle'); const hoverStyleModel = itemModel.getModel(['emphasis', 'itemStyle']); const symbolOpt = { position: [tickCoord, 0], - onclick: bind(this._changeTimeline, this, value) + onclick: bind(this._changeTimeline, this, tick.value) }; const el = giveSymbol(itemModel, itemStyleModel, group, symbolOpt); graphic.enableHoverEmphasis(el, hoverStyleModel.getItemStyle()); const ecData = graphic.getECData(el); if (itemModel.get('tooltip')) { - ecData.dataIndex = value; + ecData.dataIndex = tick.value; ecData.dataModel = timelineModel; } else { @@ -795,7 +795,7 @@ function pointerMoveTo( pointer.y = 0; } else { - pointer.stopAnimation(true); + pointer.stopAnimation(null, true); pointer.animateTo({ x: toCoord, y: 0 diff --git a/src/coord/Axis.ts b/src/coord/Axis.ts index 698e322d01..e700e9b483 100644 --- a/src/coord/Axis.ts +++ b/src/coord/Axis.ts @@ -175,10 +175,12 @@ class Axis { const result = createAxisTicks(this, tickModel); const ticks = result.ticks; - const ticksCoords = map(ticks, function (tickValue) { + const ticksCoords = map(ticks, function (tick) { return { - coord: this.dataToCoord(tickValue), - tickValue: this.scale instanceof OrdinalScale ? this.scale.getCategoryIndex(tickValue) : tickValue + coord: this.dataToCoord(tick.value), + tickValue: this.scale instanceof OrdinalScale + ? this.scale.getCategoryIndex(tick.value) + : tick.value }; }, this); diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 2caa4bfa0b..714dc57f40 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -37,7 +37,7 @@ import { AxisBaseOption } from './axisCommonTypes'; import type CartesianAxisModel from './cartesian/AxisModel'; import List from '../data/List'; import { getStackedDimension } from '../data/helper/dataStackHelper'; -import { Dictionary, ScaleDataValue, DimensionName } from '../util/types'; +import { Dictionary, ScaleDataValue, DimensionName, ScaleTick } from '../util/types'; import { ensureScaleRawExtentInfo } from './scaleRawExtentInfo'; @@ -219,23 +219,23 @@ export function ifAxisCrossZero(axis: Axis) { * If category axis, this param is not requied. * return: {string} label string. */ -export function makeLabelFormatter(axis: Axis) { +export function makeLabelFormatter(axis: Axis): (tick: ScaleTick, idx: number) => string { const labelFormatter = axis.getLabelModel().get('formatter'); const categoryTickStart = axis.type === 'category' ? axis.scale.getExtent()[0] : null; if (typeof labelFormatter === 'string') { return (function (tpl) { - return function (val: number | string) { + return function (tick: ScaleTick) { // For category axis, get raw value; for numeric axis, // get foramtted label like '1,333,444'. - val = axis.scale.getLabel(val); - return tpl.replace('{value}', val != null ? val : ''); + const label = axis.scale.getLabel(tick); + return tpl.replace('{value}', label != null ? label : ''); }; })(labelFormatter); } else if (typeof labelFormatter === 'function') { return (function (cb) { - return function (tickValue: number, idx: number) { + return function (tick: ScaleTick, idx: number) { // The original intention of `idx` is "the index of the tick in all ticks". // But the previous implementation of category axis do not consider the // `axisLabel.interval`, which cause that, for example, the `interval` is @@ -243,24 +243,24 @@ export function makeLabelFormatter(axis: Axis) { // corresponding `idx` are `0`, `2`, `4`, but not `0`, `1`, `2`. So we keep // the definition here for back compatibility. if (categoryTickStart != null) { - idx = tickValue - categoryTickStart; + idx = tick.value - categoryTickStart; } - return cb(getAxisRawValue(axis, tickValue), idx); + return cb(getAxisRawValue(axis, tick), idx); }; })(labelFormatter); } else { - return function (tick: number) { + return function (tick: ScaleTick) { return axis.scale.getLabel(tick); }; } } -export function getAxisRawValue(axis: Axis, value: number | string): number | string { +export function getAxisRawValue(axis: Axis, tick: ScaleTick): number | string { // In category axis with data zoom, tick is not the original // index of axis.data. So tick should not be exposed to user // in category axis. - return axis.type === 'category' ? axis.scale.getLabel(value) : value; + return axis.type === 'category' ? axis.scale.getLabel(tick) : tick.value; } /** @@ -275,7 +275,7 @@ export function estimateLabelUnionRect(axis: Axis) { return; } - let realNumberScaleTicks; + let realNumberScaleTicks: ScaleTick[]; let tickCount; const categoryScaleExtent = scale.getExtent(); @@ -298,8 +298,12 @@ export function estimateLabelUnionRect(axis: Axis) { step = Math.ceil(tickCount / 40); } for (let i = 0; i < tickCount; i += step) { - const tickValue = realNumberScaleTicks ? realNumberScaleTicks[i] : categoryScaleExtent[0] + i; - const label = labelFormatter(tickValue, i); + const tick = realNumberScaleTicks + ? realNumberScaleTicks[i] + : { + value: categoryScaleExtent[0] + i + }; + const label = labelFormatter(tick, i); const unrotatedSingleRect = axisLabelModel.getTextRect(label); const singleRect = rotateTextRect(unrotatedSingleRect, axisLabelModel.get('rotate') || 0); diff --git a/src/coord/axisTickLabelBuilder.ts b/src/coord/axisTickLabelBuilder.ts index 69bebfa1f9..0d0a1ef5c9 100644 --- a/src/coord/axisTickLabelBuilder.ts +++ b/src/coord/axisTickLabelBuilder.ts @@ -29,6 +29,7 @@ import { } from './axisHelper'; import Axis from './Axis'; import Model from '../model/Model'; +import {ScaleTick} from '../util/types'; const inner = makeInner(); @@ -55,7 +56,7 @@ export function createAxisLabels(axis: Axis): { * } */ export function createAxisTicks(axis: Axis, tickModel: Model): { - ticks: number[], + ticks: ScaleTick[], tickCategoryInterval?: number } { // Only ordinal scale support tick interval @@ -142,15 +143,15 @@ function makeCategoryTicks(axis, tickModel) { }); } -function makeRealNumberLabels(axis) { +function makeRealNumberLabels(axis: Axis) { const ticks = axis.scale.getTicks(); const labelFormatter = makeLabelFormatter(axis); return { - labels: zrUtil.map(ticks, function (tickValue, idx) { + labels: zrUtil.map(ticks, function (tick, idx) { return { - formattedLabel: labelFormatter(tickValue, idx), - rawLabel: axis.scale.getLabel(tickValue), - tickValue: tickValue + formattedLabel: labelFormatter(tick, idx), + rawLabel: axis.scale.getLabel(tick), + tickValue: tick.value }; }) }; diff --git a/src/scale/Interval.ts b/src/scale/Interval.ts index 775c1a5dbd..53e9fe5749 100644 --- a/src/scale/Interval.ts +++ b/src/scale/Interval.ts @@ -22,6 +22,7 @@ import * as numberUtil from '../util/number'; import * as formatUtil from '../util/format'; import Scale from './Scale'; import * as helper from './helper'; +import {ScaleTick, ParsedValue} from '../util/types'; const roundNumber = numberUtil.round; @@ -88,13 +89,13 @@ class IntervalScale extends Scale { /** * @param expandToNicedExtent Whether expand the ticks to niced extent. */ - getTicks(expandToNicedExtent?: boolean): number[] { + getTicks(expandToNicedExtent?: boolean): ScaleTick[] { const interval = this._interval; const extent = this._extent; const niceTickExtent = this._niceExtent; const intervalPrecision = this._intervalPrecision; - const ticks = [] as number[]; + const ticks = [] as ScaleTick[]; // If interval is 0, return []; if (!interval) { return ticks; @@ -105,19 +106,25 @@ class IntervalScale extends Scale { if (extent[0] < niceTickExtent[0]) { if (expandToNicedExtent) { - ticks.push(roundNumber(niceTickExtent[0] - interval, intervalPrecision)); + ticks.push({ + value: roundNumber(niceTickExtent[0] - interval, intervalPrecision) + }); } else { - ticks.push(extent[0]); + ticks.push({ + value: extent[0] + }); } } let tick = niceTickExtent[0]; while (tick <= niceTickExtent[1]) { - ticks.push(tick); + ticks.push({ + value: tick + }); // Avoid rounding error tick = roundNumber(tick + interval, intervalPrecision); - if (tick === ticks[ticks.length - 1]) { + if (tick === ticks[ticks.length - 1].value) { // Consider out of safe float point, e.g., // -3711126.9907707 + 2e-10 === -3711126.9907707 break; @@ -128,13 +135,17 @@ class IntervalScale extends Scale { } // Consider this case: the last item of ticks is smaller // than niceTickExtent[1] and niceTickExtent[1] === extent[1]. - const lastNiceTick = ticks.length ? ticks[ticks.length - 1] : niceTickExtent[1]; + const lastNiceTick = ticks.length ? ticks[ticks.length - 1].value : niceTickExtent[1]; if (extent[1] > lastNiceTick) { if (expandToNicedExtent) { - ticks.push(roundNumber(lastNiceTick + interval, intervalPrecision)); + ticks.push({ + value: roundNumber(lastNiceTick + interval, intervalPrecision) + }); } else { - ticks.push(extent[1]); + ticks.push({ + value: extent[1] + }); } } @@ -151,11 +162,11 @@ class IntervalScale extends Scale { const prevTick = ticks[i - 1]; let count = 0; const minorTicksGroup = []; - const interval = nextTick - prevTick; + const interval = nextTick.value - prevTick.value; const minorInterval = interval / splitNumber; while (count < splitNumber - 1) { - const minorTick = roundNumber(prevTick + (count + 1) * minorInterval); + const minorTick = roundNumber(prevTick.value + (count + 1) * minorInterval); // For the first and last interval. The count may be less than splitNumber. if (minorTick > extent[0] && minorTick < extent[1]) { @@ -174,7 +185,7 @@ class IntervalScale extends Scale { * @param opt.pad returns 1.50 but not 1.5 if precision is 2. */ getLabel( - data: number, + data: ScaleTick, opt?: { precision?: 'auto' | number, pad?: boolean @@ -187,7 +198,7 @@ class IntervalScale extends Scale { let precision = opt && opt.precision; if (precision == null) { - precision = numberUtil.getPrecisionSafe(data) || 0; + precision = numberUtil.getPrecisionSafe(data.value) || 0; } else if (precision === 'auto') { // Should be more precise then tick. @@ -196,7 +207,7 @@ class IntervalScale extends Scale { // (1) If `precision` is set, 12.005 should be display as '12.00500'. // (2) Use roundNumber (toFixed) to avoid scientific notation like '3.5e-7'. - const dataNum = roundNumber(data, precision as number, true); + const dataNum = roundNumber(data.value, precision as number, true); return formatUtil.addCommas(dataNum); } diff --git a/src/scale/Log.ts b/src/scale/Log.ts index 43cf0d0df5..cd510f5cd7 100644 --- a/src/scale/Log.ts +++ b/src/scale/Log.ts @@ -25,7 +25,7 @@ import * as scaleHelper from './helper'; // Use some method of IntervalScale import IntervalScale from './Interval'; import List from '../data/List'; -import { DimensionName } from '../util/types'; +import { DimensionName, ScaleTick } from '../util/types'; const scaleProto = Scale.prototype; // FIXME:TS refactor: not good to call it directly with `this`? @@ -60,14 +60,15 @@ class LogScale extends Scale { /** * @param Whether expand the ticks to niced extent. */ - getTicks(expandToNicedExtent: boolean): number[] { + getTicks(expandToNicedExtent: boolean): ScaleTick[] { const originalScale = this._originalScale; const extent = this._extent; const originalExtent = originalScale.getExtent(); const ticks = intervalScaleProto.getTicks.call(this, expandToNicedExtent); - return zrUtil.map(ticks, function (val) { + return zrUtil.map(ticks, function (tick) { + const val = tick.value; let powVal = numberUtil.round(mathPow(this.base, val)); // Fix #4158 @@ -78,7 +79,9 @@ class LogScale extends Scale { ? fixRoundingError(powVal, originalExtent[1]) : powVal; - return powVal; + return { + value: powVal + }; }, this); } diff --git a/src/scale/Ordinal.ts b/src/scale/Ordinal.ts index 7a9d2b38f2..a0374f4d7f 100644 --- a/src/scale/Ordinal.ts +++ b/src/scale/Ordinal.ts @@ -28,7 +28,7 @@ import Scale from './Scale'; import OrdinalMeta from '../data/OrdinalMeta'; import List from '../data/List'; import * as scaleHelper from './helper'; -import { OrdinalRawValue, OrdinalNumber, DimensionLoose, OrdinalSortInfo } from '../util/types'; +import { OrdinalRawValue, OrdinalNumber, DimensionLoose, OrdinalSortInfo, OrdinalScaleTick, ScaleTick } from '../util/types'; import { AxisBaseOption } from '../coord/axisCommonTypes'; import { isArray } from 'zrender/src/core/util'; @@ -85,13 +85,15 @@ class OrdinalScale extends Scale { return Math.round(scaleHelper.scale(val, this._extent)); } - getTicks(): OrdinalNumber[] { + getTicks(): OrdinalScaleTick[] { const ticks = []; const extent = this._extent; let rank = extent[0]; while (rank <= extent[1]) { - ticks.push(rank); + ticks.push({ + value: rank + }); rank++; } @@ -123,9 +125,9 @@ class OrdinalScale extends Scale { /** * Get item on rank n */ - getLabel(n: OrdinalNumber): string { + getLabel(tick: ScaleTick): string { if (!this.isBlank()) { - const cateogry = this._ordinalMeta.categories[n]; + const cateogry = this._ordinalMeta.categories[tick.value]; // Note that if no data, ordinalMeta.categories is an empty array. // Return empty if it's not exist. return cateogry == null ? '' : cateogry + ''; diff --git a/src/scale/Scale.ts b/src/scale/Scale.ts index 4075785626..0bfd5fcfd0 100644 --- a/src/scale/Scale.ts +++ b/src/scale/Scale.ts @@ -21,7 +21,7 @@ import * as clazzUtil from '../util/clazz'; import { Dictionary } from 'zrender/src/core/types'; import List from '../data/List'; -import { DimensionName, ScaleDataValue, OptionDataValue, DimensionLoose } from '../util/types'; +import { DimensionName, ScaleDataValue, OptionDataValue, DimensionLoose, ScaleTick, ParsedValue } from '../util/types'; import { ScaleRawExtentInfo } from '../coord/scaleRawExtentInfo'; @@ -154,9 +154,9 @@ abstract class Scale { /** * @return label of the tick. */ - abstract getLabel(tick: any): string; + abstract getLabel(tick: ScaleTick): string; - abstract getTicks(expandToNicedExtent?: boolean): number[]; + abstract getTicks(expandToNicedExtent?: boolean): ScaleTick[]; abstract getMinorTicks(splitNumber: number): number[][]; diff --git a/src/scale/Time.ts b/src/scale/Time.ts index 26168c9c9a..adc0d99a75 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -43,6 +43,7 @@ import * as formatUtil from '../util/format'; import * as scaleHelper from './helper'; import IntervalScale from './Interval'; import Scale from './Scale'; +import {TimeScaleTick} from '../util/types'; const mathCeil = Math.ceil; @@ -79,23 +80,23 @@ class TimeScale extends IntervalScale { private _stepLvl: [string, number]; - getLabel(val: number): string { + getLabel(tick: TimeScaleTick): string { const stepLvl = this._stepLvl; - const labelFormatType = getLabelFormatType(val, this.getSetting('useUTC'), false); - return formatUtil.formatTime(labelFormatType, val); + const labelFormatType = getLabelFormatType(tick.value, this.getSetting('useUTC'), false); + return formatUtil.formatTime(labelFormatType, tick.value); } /** * @override * @param expandToNicedExtent Whether expand the ticks to niced extent. */ - getTicks(expandToNicedExtent?: boolean): number[] { + getTicks(expandToNicedExtent?: boolean): TimeScaleTick[] { const interval = this._interval; const extent = this._extent; const niceTickExtent = this._niceExtent; - let ticks = [] as number[]; + let ticks = [] as TimeScaleTick[]; // If interval is 0, return []; if (!interval) { return ticks; @@ -103,10 +104,16 @@ class TimeScale extends IntervalScale { if (extent[0] < niceTickExtent[0]) { if (expandToNicedExtent) { - ticks.push(numberUtil.round(niceTickExtent[0] - interval, 0)); + ticks.push({ + value: numberUtil.round(niceTickExtent[0] - interval, 0), + level: -1 // TODO: + }); } else { - ticks.push(extent[0]); + ticks.push({ + value: extent[0], + level: -1 + }); } } @@ -125,13 +132,21 @@ class TimeScale extends IntervalScale { // Consider this case: the last item of ticks is smaller // than niceTickExtent[1] and niceTickExtent[1] === extent[1]. - const lastNiceTick = ticks.length ? ticks[ticks.length - 1] : niceTickExtent[1]; + const lastNiceTick = ticks.length + ? ticks[ticks.length - 1].value + : niceTickExtent[1]; if (extent[1] > lastNiceTick) { if (expandToNicedExtent) { - ticks.push(numberUtil.round(lastNiceTick + interval, 0)); + ticks.push({ + value: numberUtil.round(lastNiceTick + interval, 0), + level: -1 + }); } else { - ticks.push(extent[1]); + ticks.push({ + value: extent[1], + level: -1 + }); } } @@ -423,10 +438,14 @@ function isLevelValueSame(level: TimeAxisLabelPrimaryLevel, valueA: number, valu } -function getLevelTicks(level: TimeAxisLabelLevel, isUTC: boolean, extent: number[]) { +function getLevelTicks( + level: TimeAxisLabelLevel, + isUTC: boolean, + extent: number[] +): TimeScaleTick[] { const utc = isUTC ? 'UTC' : ''; - const ticks: number[] = []; - for (let i = 0; i < primaryLevels.length; ++i) { + const ticks: TimeScaleTick[] = []; + for (let i = 0, levelId = 0; i < primaryLevels.length; ++i) { let date = new Date(extent[0]) as any; if (primaryLevels[i] === 'week') { @@ -448,13 +467,21 @@ function getLevelTicks(level: TimeAxisLabelLevel, isUTC: boolean, extent: number break; } else if (dateTime >= extent[0]) { - ticks.push(dateTime); + ticks.push({ + value: dateTime, + level: levelId + }); } } date['set' + utc + 'Month'](date['get' + utc + 'Month']() + 1); } } - else if (!isLevelValueSame(primaryLevels[i] as TimeAxisLabelPrimaryLevel, extent[0], extent[1], isUTC)) { + else if ( + !isLevelValueSame( + primaryLevels[i] as TimeAxisLabelPrimaryLevel, + extent[0], extent[1], isUTC + ) + ) { // Level value changes within extent while (true) { if (primaryLevels[i] === 'year') { @@ -496,12 +523,16 @@ function getLevelTicks(level: TimeAxisLabelLevel, isUTC: boolean, extent: number const dateValue = (date as Date).getTime(); if (dateValue < extent[1]) { - ticks.push(dateValue); + ticks.push({ + value: dateValue, + level: levelId + }); } else { break; } } + ++levelId; } if (primaryLevels[i] === level) { @@ -509,7 +540,7 @@ function getLevelTicks(level: TimeAxisLabelLevel, isUTC: boolean, extent: number } } - ticks.sort((a, b) => a - b); + ticks.sort((a, b) => a.value - b.value); if (ticks.length <= 1) { return ticks; } diff --git a/src/util/types.ts b/src/util/types.ts index e0077bbde2..ec2b56138b 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -252,6 +252,15 @@ export type ParsedValue = ParsedValueNumeric | OrdinalRawValue; // This is not `OptionDataPrimitive` because the "dataProvider parse" // will not be performed. But "scale parse" will be performed. export type ScaleDataValue = ParsedValue | Date; +export interface ScaleTick { + value: number +}; +export interface TimeScaleTick extends ScaleTick { + level: number +}; +export interface OrdinalScaleTick extends ScaleTick { + value: OrdinalNumber +}; // Can only be string or index, because it is used in object key in some code. // Making the type alias here just intending to show the meaning clearly in code. From 5da563ec815f14eecb2804be95cd2214987149d5 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Wed, 8 Jul 2020 15:05:49 +0800 Subject: [PATCH 05/11] refactor(time): rename variables --- src/component/axisPointer/viewHelper.ts | 10 +- src/coord/axisCommonTypes.ts | 17 ++- src/coord/axisHelper.ts | 4 +- src/scale/Time.ts | 135 +++++++++++++----------- src/util/types.ts | 10 +- 5 files changed, 106 insertions(+), 70 deletions(-) diff --git a/src/component/axisPointer/viewHelper.ts b/src/component/axisPointer/viewHelper.ts index 1b6f639552..fa38788f66 100644 --- a/src/component/axisPointer/viewHelper.ts +++ b/src/component/axisPointer/viewHelper.ts @@ -164,9 +164,11 @@ export function getValueLabel( ): string { value = axis.scale.parse(value); let text = (axis.scale as IntervalScale).getLabel( - // If `precision` is set, width can be fixed (like '12.00500'), which - // helps to debounce when when moving label. - value, { + { + value + }, { + // If `precision` is set, width can be fixed (like '12.00500'), which + // helps to debounce when when moving label. precision: opt.precision } ); @@ -174,7 +176,7 @@ export function getValueLabel( if (formatter) { const params = { - value: axisHelper.getAxisRawValue(axis, value), + value: axisHelper.getAxisRawValue(axis, {value}), axisDimension: axis.dim, axisIndex: (axis as Axis2D).index, // Only Carteian Axis has index seriesData: [] as CallbackDataParams[] diff --git a/src/coord/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts index fb00c4b72b..9a09e5d664 100644 --- a/src/coord/axisCommonTypes.ts +++ b/src/coord/axisCommonTypes.ts @@ -153,6 +153,21 @@ interface AxisTickOption { interval?: 'auto' | number | ((index: number, value: string) => boolean) } +export type AxisLabelFormatterOption = string | ((value: OrdinalRawValue | number, index: number) => string); + +type TimeAxisLabelFormatterLevel = AxisLabelFormatterOption | string[]; + +export type TimeAxisLabelFormatterOption = AxisLabelFormatterOption | { + year?: TimeAxisLabelFormatterLevel, + month?: TimeAxisLabelFormatterLevel, + week?: TimeAxisLabelFormatterLevel, + day?: TimeAxisLabelFormatterLevel, + hour?: TimeAxisLabelFormatterLevel, + minute?: TimeAxisLabelFormatterLevel, + second?: TimeAxisLabelFormatterLevel, + millisecond?: TimeAxisLabelFormatterLevel +}; + interface AxisLabelOption extends Omit { show?: boolean, // Whether axisLabel is inside the grid or outside the grid. @@ -164,7 +179,7 @@ interface AxisLabelOption extends Omit { showMaxLabel?: boolean, margin?: number, // value is supposed to be OptionDataPrimitive but for time axis, it is time stamp. - formatter?: string | ((value: OrdinalRawValue | number, index: number) => string), + formatter?: AxisLabelFormatterOption | TimeAxisLabelFormatterOption, // -------------------------------------------- // [Properties below only for 'category' axis]: diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 714dc57f40..7b878f8ea1 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -229,7 +229,9 @@ export function makeLabelFormatter(axis: Axis): (tick: ScaleTick, idx: number) = // For category axis, get raw value; for numeric axis, // get foramtted label like '1,333,444'. const label = axis.scale.getLabel(tick); - return tpl.replace('{value}', label != null ? label : ''); + const text = tpl.replace('{value}', label != null ? label : ''); + + return text; }; })(labelFormatter); } diff --git a/src/scale/Time.ts b/src/scale/Time.ts index adc0d99a75..dfc5a8ccee 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -44,6 +44,7 @@ import * as scaleHelper from './helper'; import IntervalScale from './Interval'; import Scale from './Scale'; import {TimeScaleTick} from '../util/types'; +import {TimeAxisLabelFormatterOption} from '../coord/axisCommonTypes'; const mathCeil = Math.ceil; @@ -81,12 +82,23 @@ class TimeScale extends IntervalScale { private _stepLvl: [string, number]; getLabel(tick: TimeScaleTick): string { - const stepLvl = this._stepLvl; + // const stepLvl = this._stepLvl; const labelFormatType = getLabelFormatType(tick.value, this.getSetting('useUTC'), false); return formatUtil.formatTime(labelFormatType, tick.value); } + getFormattedLabel( + tick: TimeScaleTick, + labelFormatter: TimeAxisLabelFormatterOption + ): string { + const durationName = getLabelFormatType(tick.value, this.getSetting('useUTC'), true); + if (typeof labelFormatter === 'string') { + // Single formatter for all durations and levels + return formatUtil.formatTime(durationName, tick.value); + } + } + /** * @override * @param expandToNicedExtent Whether expand the ticks to niced extent. @@ -119,12 +131,12 @@ class TimeScale extends IntervalScale { const useUTC = this.getSetting('useUTC'); - const scaleLevelsLen = primaryScaleLevels.length; - const idx = bisect(primaryScaleLevels, this._interval, 0, scaleLevelsLen); - const level = primaryScaleLevels[Math.min(idx, scaleLevelsLen - 1)]; + const unitLen = primaryUnitIntervals.length; + const idx = bisect(primaryUnitIntervals, this._interval, 0, unitLen); + const intervals = primaryUnitIntervals[Math.min(idx, unitLen - 1)]; - const innerTicks = getLevelTicks( - level[0] as TimeAxisLabelPrimaryLevel, + const innerTicks = getIntervalTicks( + intervals[0] as PrimaryTimeUnit, useUTC, extent ); @@ -205,13 +217,13 @@ class TimeScale extends IntervalScale { approxInterval = maxInterval; } - const scaleLevelsLen = scaleLevels.length; - const idx = bisect(scaleLevels, approxInterval, 0, scaleLevelsLen); + const scaleIntervalsLen = scaleIntervals.length; + const idx = bisect(scaleIntervals, approxInterval, 0, scaleIntervalsLen); - const level = scaleLevels[Math.min(idx, scaleLevelsLen - 1)]; - let interval = level[1]; + const intervals = scaleIntervals[Math.min(idx, scaleIntervalsLen - 1)]; + let interval = intervals[1]; // Same with interval scale if span is much larger than 1 year - if (level[0] === 'year') { + if (intervals[0] === 'year') { const yearSpan = span / interval; // From "Nice Numbers for Graph Labels" of Graphic Gems @@ -230,7 +242,7 @@ class TimeScale extends IntervalScale { scaleHelper.fixExtent(niceExtent, extent); - this._stepLvl = level; + this._stepLvl = intervals; // Interval will be used in getTicks this._interval = interval; this._niceExtent = niceExtent; @@ -262,7 +274,7 @@ class TimeScale extends IntervalScale { * with some modifications made for this program. * See the license statement at the head of this file. */ -const scaleLevels = [ +const scaleIntervals = [ // Format interval ['hh:mm:ss', ONE_SECOND], // 1s ['hh:mm:ss', ONE_SECOND * 5], // 5s @@ -301,31 +313,27 @@ const scaleLevels = [ ['year', ONE_DAY * 380] // 1Y ] as [string, number][]; -const primaryScaleLevels = [ +const primaryUnitIntervals = [ // Format interval - ['second', ONE_SECOND], // 1s - ['minute', ONE_MINUTE], // 1m - ['hour', ONE_HOUR], // 1h - ['day', ONE_DAY], // 1d + ['second', ONE_SECOND], // 1s + ['minute', ONE_MINUTE], // 1m + ['hour', ONE_HOUR], // 1h + ['day', ONE_DAY], // 1d ['week', ONE_DAY * 7], // 7d ['month', ONE_DAY * 31], // 1M ['year', ONE_DAY * 380] // 1Y ] as [string, number][]; -type TimeAxisLabelPrimaryLevel = 'millisecond' - | 'second' | 'minute' | 'hour' +type PrimaryTimeUnit = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year'; -type TimeAxisLabelLevel = TimeAxisLabelPrimaryLevel - | 'week' | 'quarter' | 'half-year'; - -const primaryLevels = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond']; +type TimeUnit = PrimaryTimeUnit | 'week' | 'quarter' | 'half-year'; function getLabelFormatType( value: number | string | Date, isUTC: boolean, primaryOnly: boolean -): TimeAxisLabelLevel { +): TimeUnit { const date = numberUtil.parseDate(value); const utc = isUTC ? 'UTC' : ''; const M = (date as any)['get' + utc + 'Month']() + 1; @@ -379,7 +387,7 @@ function getLabelFormatType( } -function getLabelFormatValueFromLevel(value: number | Date, isUTC: boolean, level?: TimeAxisLabelLevel) : number { +function getLabelFormatValueFromUnit(value: number | Date, isUTC: boolean, level?: TimeUnit) : number { const date = typeof value === 'number' ? numberUtil.parseDate(value) as any : value; @@ -405,50 +413,53 @@ function getLabelFormatValueFromLevel(value: number | Date, isUTC: boolean, leve } -function isLevelValueSame(level: TimeAxisLabelPrimaryLevel, valueA: number, valueB: number, isUTC: boolean): boolean { +function isUnitValueSame(unit: PrimaryTimeUnit, valueA: number, valueB: number, isUTC: boolean): boolean { const dateA = numberUtil.parseDate(valueA) as any; const dateB = numberUtil.parseDate(valueB) as any; - const utc = isUTC ? 'UTC' : ''; - const isSame = (compareLevel: TimeAxisLabelPrimaryLevel) => { - return getLabelFormatValueFromLevel(dateA, isUTC, compareLevel) - === getLabelFormatValueFromLevel(dateB, isUTC, compareLevel); - }; - switch (level) { + const isSame = (unit: PrimaryTimeUnit) => { + return getLabelFormatValueFromUnit(dateA, isUTC, unit) + === getLabelFormatValueFromUnit(dateB, isUTC, unit); + }; + const isSameYear = () => isSame('year'); + const isSameMonth = () => isSameYear() && isSame('month'); + const isSameDay = () => isSameMonth() && isSame('day'); + const isSameHour = () => isSameDay() && isSame('hour'); + const isSameMinute = () => isSameHour() && isSame('minute'); + const isSameSecond = () => isSameMinute() && isSame('second'); + const isSameMilliSecond = () => isSameSecond() && isSame('millisecond'); + + switch (unit) { case 'year': - return isSame('year'); + return isSameYear(); case 'month': - return isSame('year') && isSame('month'); + return isSameMonth(); case 'day': - return isSame('year') && isSame('month') && isSame('day'); + return isSameDay(); case 'hour': - return isSame('year') && isSame('month') && isSame('day') - && isSame('hour'); + return isSameHour(); case 'minute': - return isSame('year') && isSame('month') && isSame('day') - && isSame('hour') && isSame('minute'); + return isSameMinute(); case 'second': - return isSame('year') && isSame('month') && isSame('day') - && isSame('hour') && isSame('minute') && isSame('second'); + return isSameSecond(); case 'millisecond': - return isSame('year') && isSame('month') && isSame('day') - && isSame('hour') && isSame('minute') && isSame('second') - && isSame('millisecond'); + return isSameMilliSecond(); } } -function getLevelTicks( - level: TimeAxisLabelLevel, +function getIntervalTicks( + unitName: TimeUnit, isUTC: boolean, extent: number[] ): TimeScaleTick[] { const utc = isUTC ? 'UTC' : ''; const ticks: TimeScaleTick[] = []; - for (let i = 0, levelId = 0; i < primaryLevels.length; ++i) { + const unitNames = primaryUnitIntervals.map(i => i[0]); + for (let i = unitNames.length - 1, levelId = 0; i >= 0; --i) { let date = new Date(extent[0]) as any; - if (primaryLevels[i] === 'week') { + if (unitNames[i] === 'week') { date['set' + utc + 'Hours'](0); date['set' + utc + 'Minutes'](0); date['set' + utc + 'Seconds'](0); @@ -458,7 +469,7 @@ function getLevelTicks( while (isDateWithinExtent) { const dates = date['get' + utc + 'Month']() + 1 === 2 ? [8, 15, 22] - : [8, 16, 23]; + : [8, 16, 24]; for (let d = 0; d < dates.length; ++d) { date['set' + utc + 'Date'](dates[d]); const dateTime = (date as Date).getTime(); @@ -476,15 +487,13 @@ function getLevelTicks( date['set' + utc + 'Month'](date['get' + utc + 'Month']() + 1); } } - else if ( - !isLevelValueSame( - primaryLevels[i] as TimeAxisLabelPrimaryLevel, - extent[0], extent[1], isUTC - ) - ) { + else if (!isUnitValueSame( + unitNames[i] as PrimaryTimeUnit, + extent[0], extent[1], isUTC + )) { // Level value changes within extent while (true) { - if (primaryLevels[i] === 'year') { + if (unitNames[i] === 'year') { date['set' + utc + 'FullYear'](date['get' + utc + 'FullYear']() + 1); date['set' + utc + 'Month'](0); date['set' + utc + 'Date'](1); @@ -492,7 +501,7 @@ function getLevelTicks( date['set' + utc + 'Minutes'](0); date['set' + utc + 'Seconds'](0); } - else if (primaryLevels[i] === 'month') { + else if (unitNames[i] === 'month') { // This also works with Dec. date['set' + utc + 'Month'](date['get' + utc + 'Month']() + 1); date['set' + utc + 'Date'](1); @@ -500,23 +509,23 @@ function getLevelTicks( date['set' + utc + 'Minutes'](0); date['set' + utc + 'Seconds'](0); } - else if (primaryLevels[i] === 'day') { + else if (unitNames[i] === 'day') { date['set' + utc + 'Date'](date['get' + utc + 'Date']() + 1); date['set' + utc + 'Hours'](0); date['set' + utc + 'Minutes'](0); date['set' + utc + 'Seconds'](0); } - else if (primaryLevels[i] === 'hour') { + else if (unitNames[i] === 'hour') { date['set' + utc + 'Hours'](date['get' + utc + 'Hours']() + 1); date['set' + utc + 'Minutes'](0); date['set' + utc + 'Seconds'](0); } - else if (primaryLevels[i] === 'minute') { + else if (unitNames[i] === 'minute') { date['set' + utc + 'Minutes'](date['get' + utc + 'Minutes']() + 1); date['set' + utc + 'Minutes'](0); date['set' + utc + 'Seconds'](0); } - else if (primaryLevels[i] === 'second') { + else if (unitNames[i] === 'second') { date['set' + utc + 'Seconds'](date['get' + utc + 'Seconds']() + 1); } date['set' + utc + 'Milliseconds'](0); // TODO: not sure @@ -535,7 +544,7 @@ function getLevelTicks( ++levelId; } - if (primaryLevels[i] === level) { + if (unitNames[i] === unitName) { break; } } diff --git a/src/util/types.ts b/src/util/types.ts index 37e29874f1..383bdd7d9b 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -257,7 +257,15 @@ export interface ScaleTick { value: number }; export interface TimeScaleTick extends ScaleTick { - level: number + /** + * Level information is used for label formatting. + * For example, a time axis may contain labels like: Jan, 8th, 16th, 23th, + * Feb, and etc. In this case, month labels like Jan and Feb should be + * displayed in a more significant way than days. + * `level` is set to be 0 when it's the most significant level, like month + * labels in the above case. + */ + level?: number }; export interface OrdinalScaleTick extends ScaleTick { value: OrdinalNumber From e0f05846b9b62854b8eb233d6349c3764bbca859 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Tue, 14 Jul 2020 15:22:48 +0800 Subject: [PATCH 06/11] feat(time): label style for different levels and units --- src/coord/axisCommonTypes.ts | 29 ++--- src/coord/axisDefault.ts | 2 +- src/coord/axisHelper.ts | 18 +++- src/lang.ts | 16 +++ src/langEN.ts | 16 +++ src/scale/Time.ts | 123 ++++----------------- src/util/format.ts | 2 +- src/util/time.ts | 203 +++++++++++++++++++++++++++++++++++ test/timeScale.html | 22 +++- 9 files changed, 306 insertions(+), 125 deletions(-) create mode 100644 src/util/time.ts diff --git a/src/coord/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts index 9a09e5d664..b99d5715f0 100644 --- a/src/coord/axisCommonTypes.ts +++ b/src/coord/axisCommonTypes.ts @@ -20,7 +20,7 @@ import { TextCommonOption, LineStyleOption, OrdinalRawValue, ZRColor, AreaStyleOption, ComponentOption, ColorString, - AnimationOptionMixin, Dictionary, ScaleDataValue + AnimationOptionMixin, Dictionary, ScaleDataValue, TimeScaleTick } from '../util/types'; @@ -155,18 +155,21 @@ interface AxisTickOption { export type AxisLabelFormatterOption = string | ((value: OrdinalRawValue | number, index: number) => string); -type TimeAxisLabelFormatterLevel = AxisLabelFormatterOption | string[]; - -export type TimeAxisLabelFormatterOption = AxisLabelFormatterOption | { - year?: TimeAxisLabelFormatterLevel, - month?: TimeAxisLabelFormatterLevel, - week?: TimeAxisLabelFormatterLevel, - day?: TimeAxisLabelFormatterLevel, - hour?: TimeAxisLabelFormatterLevel, - minute?: TimeAxisLabelFormatterLevel, - second?: TimeAxisLabelFormatterLevel, - millisecond?: TimeAxisLabelFormatterLevel -}; +type TimeAxisLabelUnitFormatter = AxisLabelFormatterOption | string[]; + +export type TimeAxisLabelFormatterOption = string + | ((value: TimeScaleTick, index: number) => string) + | { + year?: TimeAxisLabelUnitFormatter, + month?: TimeAxisLabelUnitFormatter, + week?: TimeAxisLabelUnitFormatter, + day?: TimeAxisLabelUnitFormatter, + hour?: TimeAxisLabelUnitFormatter, + minute?: TimeAxisLabelUnitFormatter, + second?: TimeAxisLabelUnitFormatter, + millisecond?: TimeAxisLabelUnitFormatter, + inherit?: boolean + }; interface AxisLabelOption extends Omit { show?: boolean, diff --git a/src/coord/axisDefault.ts b/src/coord/axisDefault.ts index 48a407e4f4..0fc6d74f06 100644 --- a/src/coord/axisDefault.ts +++ b/src/coord/axisDefault.ts @@ -165,7 +165,7 @@ const timeAxis: AxisBaseOption = zrUtil.merge({ scale: true, axisLabel: { // To eliminate labels that are not nice - showMinLabel: false, + showMinLabel: true, showMaxLabel: false } }, valueAxis); diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 7b878f8ea1..5b0f6d4e39 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -33,11 +33,11 @@ import Model from '../model/Model'; import { AxisBaseModel } from './AxisBaseModel'; import LogScale from '../scale/Log'; import Axis from './Axis'; -import { AxisBaseOption } from './axisCommonTypes'; +import { AxisBaseOption, TimeAxisLabelFormatterOption } from './axisCommonTypes'; import type CartesianAxisModel from './cartesian/AxisModel'; import List from '../data/List'; import { getStackedDimension } from '../data/helper/dataStackHelper'; -import { Dictionary, ScaleDataValue, DimensionName, ScaleTick } from '../util/types'; +import { Dictionary, DimensionName, ScaleTick, TimeScaleTick } from '../util/types'; import { ensureScaleRawExtentInfo } from './scaleRawExtentInfo'; @@ -223,7 +223,14 @@ export function makeLabelFormatter(axis: Axis): (tick: ScaleTick, idx: number) = const labelFormatter = axis.getLabelModel().get('formatter'); const categoryTickStart = axis.type === 'category' ? axis.scale.getExtent()[0] : null; - if (typeof labelFormatter === 'string') { + if (axis.scale.type === 'time') { + return (function (tpl) { + return function (tick: ScaleTick, idx: number) { + return (axis.scale as TimeScale).getFormattedLabel(tick, idx, tpl); + } + })(labelFormatter as TimeAxisLabelFormatterOption); + } + else if (typeof labelFormatter === 'string') { return (function (tpl) { return function (tick: ScaleTick) { // For category axis, get raw value; for numeric axis, @@ -247,7 +254,10 @@ export function makeLabelFormatter(axis: Axis): (tick: ScaleTick, idx: number) = if (categoryTickStart != null) { idx = tick.value - categoryTickStart; } - return cb(getAxisRawValue(axis, tick), idx); + return cb( + getAxisRawValue(axis, tick) as (TimeScaleTick & string) | (TimeScaleTick & number), + idx + ); }; })(labelFormatter); } diff --git a/src/lang.ts b/src/lang.ts index b369139b8d..fc384a0990 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -22,6 +22,22 @@ */ export default { + time: { + month: [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ], + monthAbbr: [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ], + dayOfWeek: [ + '星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六' + ], + dayOfWeekAbbr: [ + '日', '一', '二', '三', '四', '五', '六' + ] + }, legend: { selector: { all: '全选', diff --git a/src/langEN.ts b/src/langEN.ts index 0df1872d6f..64ac81c84d 100644 --- a/src/langEN.ts +++ b/src/langEN.ts @@ -22,6 +22,22 @@ */ export default { + time: { + month: [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' + ], + monthAbbr: [ + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + ], + dayOfWeek: [ + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' + ], + dayOfWeekAbbr: [ + 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' + ] + }, legend: { selector: { all: 'All', diff --git a/src/scale/Time.ts b/src/scale/Time.ts index dfc5a8ccee..03c20d614b 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -40,6 +40,7 @@ import * as numberUtil from '../util/number'; import * as formatUtil from '../util/format'; +import * as timeUtil from '../util/time'; import * as scaleHelper from './helper'; import IntervalScale from './Interval'; import Scale from './Scale'; @@ -84,19 +85,21 @@ class TimeScale extends IntervalScale { getLabel(tick: TimeScaleTick): string { // const stepLvl = this._stepLvl; - const labelFormatType = getLabelFormatType(tick.value, this.getSetting('useUTC'), false); + const labelFormatType = timeUtil.getUnitFromValue( + tick.value, + this.getSetting('useUTC'), + false + ); return formatUtil.formatTime(labelFormatType, tick.value); } getFormattedLabel( tick: TimeScaleTick, + idx: number, labelFormatter: TimeAxisLabelFormatterOption ): string { - const durationName = getLabelFormatType(tick.value, this.getSetting('useUTC'), true); - if (typeof labelFormatter === 'string') { - // Single formatter for all durations and levels - return formatUtil.formatTime(durationName, tick.value); - } + const isUTC = this.getSetting('useUTC'); + return timeUtil.leveledFormat(tick, idx, labelFormatter, isUTC); } /** @@ -136,7 +139,7 @@ class TimeScale extends IntervalScale { const intervals = primaryUnitIntervals[Math.min(idx, unitLen - 1)]; const innerTicks = getIntervalTicks( - intervals[0] as PrimaryTimeUnit, + intervals[0] as timeUtil.PrimaryTimeUnit, useUTC, extent ); @@ -324,102 +327,18 @@ const primaryUnitIntervals = [ ['year', ONE_DAY * 380] // 1Y ] as [string, number][]; - -type PrimaryTimeUnit = 'millisecond' | 'second' | 'minute' | 'hour' - | 'day' | 'month' | 'year'; -type TimeUnit = PrimaryTimeUnit | 'week' | 'quarter' | 'half-year'; - -function getLabelFormatType( - value: number | string | Date, - isUTC: boolean, - primaryOnly: boolean -): TimeUnit { - const date = numberUtil.parseDate(value); - const utc = isUTC ? 'UTC' : ''; - const M = (date as any)['get' + utc + 'Month']() + 1; - const w = (date as any)['get' + utc + 'Day'](); - const d = (date as any)['get' + utc + 'Date'](); - const h = (date as any)['get' + utc + 'Hours'](); - const m = (date as any)['get' + utc + 'Minutes'](); - const s = (date as any)['get' + utc + 'Seconds'](); - const S = (date as any)['get' + utc + 'Milliseconds'](); - - const isSecond = S === 0; - const isMinute = isSecond && s === 0; - const isHour = isMinute && m === 0; - const isDay = isHour && h === 0; - const isWeek = isDay && w === 0; // TODO: first day to be configured - const isMonth = isDay && d === 1; - const isQuarter = isMonth && (M % 3 === 1); - const isHalfYear = isMonth && (M % 6 === 1); - const isYear = isMonth && M === 1; - - if (isYear) { - return 'year'; - } - else if (isHalfYear && !primaryOnly) { - return 'half-year'; - } - else if (isQuarter && !primaryOnly) { - return 'quarter'; - } - else if (isMonth) { - return 'month'; - } - else if (isWeek && !primaryOnly) { - return 'week'; - } - else if (isDay) { - return 'day'; - } - else if (isHour) { - return 'hour'; - } - else if (isMinute) { - return 'minute'; - } - else if (isSecond) { - return 'second'; - } - else { - return 'millisecond'; - } -} - - -function getLabelFormatValueFromUnit(value: number | Date, isUTC: boolean, level?: TimeUnit) : number { - const date = typeof value === 'number' - ? numberUtil.parseDate(value) as any - : value; - level = level || getLabelFormatType(value, isUTC, true); - const utc = isUTC ? 'UTC' : ''; - - switch (level) { - case 'millisecond': - return date['get' + utc + 'Milliseconds'](); - case 'second': - return date['get' + utc + 'Seconds'](); - case 'minute': - return date['get' + utc + 'Minutes'](); - case 'hour': - return date['get' + utc + 'Hours'](); - case 'day': - return date['get' + utc + 'Date'](); - case 'month': - return date['get' + utc + 'Month'](); - case 'year': - return date['get' + utc + 'FullYear'](); - } -} - - -function isUnitValueSame(unit: PrimaryTimeUnit, valueA: number, valueB: number, isUTC: boolean): boolean { +function isUnitValueSame( + unit: timeUtil.PrimaryTimeUnit, + valueA: number, + valueB: number, + isUTC: boolean +): boolean { const dateA = numberUtil.parseDate(valueA) as any; const dateB = numberUtil.parseDate(valueB) as any; - const isSame = (unit: PrimaryTimeUnit) => { - return getLabelFormatValueFromUnit(dateA, isUTC, unit) - === getLabelFormatValueFromUnit(dateB, isUTC, unit); + const isSame = (unit: timeUtil.PrimaryTimeUnit) => { + return timeUtil.getUnitValue(dateA, unit, isUTC) + === timeUtil.getUnitValue(dateB, unit, isUTC); }; const isSameYear = () => isSame('year'); const isSameMonth = () => isSameYear() && isSame('month'); @@ -449,7 +368,7 @@ function isUnitValueSame(unit: PrimaryTimeUnit, valueA: number, valueB: number, function getIntervalTicks( - unitName: TimeUnit, + unitName: timeUtil.TimeUnit, isUTC: boolean, extent: number[] ): TimeScaleTick[] { @@ -488,7 +407,7 @@ function getIntervalTicks( } } else if (!isUnitValueSame( - unitNames[i] as PrimaryTimeUnit, + unitNames[i] as timeUtil.PrimaryTimeUnit, extent[0], extent[1], isUTC )) { // Level value changes within extent diff --git a/src/util/format.ts b/src/util/format.ts index a25deea690..52427cc1e3 100644 --- a/src/util/format.ts +++ b/src/util/format.ts @@ -183,7 +183,7 @@ export function getTooltipMarker(inOpt: ColorString | GetTooltipMarkerOpt, extra } } -function pad(str: string, len: number): string { +export function pad(str: string, len: number): string { str += ''; return '0000'.substr(0, len - str.length) + str; } diff --git a/src/util/time.ts b/src/util/time.ts new file mode 100644 index 0000000000..462fcc7728 --- /dev/null +++ b/src/util/time.ts @@ -0,0 +1,203 @@ +import * as zrUtil from 'zrender/src/core/util'; +import {TimeAxisLabelFormatterOption} from './../coord/axisCommonTypes'; +import * as numberUtil from './number'; +import {pad} from './format'; +import lang from '../lang'; +import {TimeScaleTick} from './types'; + +export const defaultLeveledFormatter = { + millisecond: '{hh}:{mm}:{ss} {SSS}', + second: '{hh}:{mm}:{ss}', + minute: '{hh}:{mm}', + hour: '{hh}:{mm}', + day: '{d}', + week: '{d}', + month: '{MMM}', + quarter: 'Q{Q}', + year: '{YYYY}', + none: '{YYYY}-{MM}-{dd} {hh}:{mm}' +}; + +export type PrimaryTimeUnit = 'millisecond' | 'second' | 'minute' | 'hour' + | 'day' | 'month' | 'year'; +export type TimeUnit = PrimaryTimeUnit | 'week' | 'quarter'; + +export const timeUnits: TimeUnit[] = [ + 'year', 'quarter', 'month', 'week', 'day', + 'hour', 'minute', 'second', 'millisecond' +]; + + +export function format(time: Date, template: string, isUTC?: boolean): string { + const date = numberUtil.parseDate(time); + const utc = isUTC ? 'UTC' : ''; + const y = (date as any)['get' + utc + 'FullYear'](); + const M = (date as any)['get' + utc + 'Month']() + 1; + const q = Math.floor((M - 1) / 4) + 1; + const d = (date as any)['get' + utc + 'Date'](); + const e = (date as any)['get' + utc + 'Day'](); + const H = (date as any)['get' + utc + 'Hours'](); + const h = (H - 1) % 12 + 1; + const m = (date as any)['get' + utc + 'Minutes'](); + const s = (date as any)['get' + utc + 'Seconds'](); + const S = (date as any)['get' + utc + 'Milliseconds'](); + + return template + .replace('{yyyy}', y) + .replace('{yy}', y % 100 + '') + .replace('{Q}', q + '') + .replace('{MMMM}', lang.time.month[M - 1]) + .replace('{MMM}', lang.time.monthAbbr[M - 1]) + .replace('{MM}', pad(M, 2)) + .replace('{M}', M) + .replace('{dd}', pad(d, 2)) + .replace('{d}', d) + .replace('{eeee}', lang.time.dayOfWeek[e]) + .replace('{ee}', lang.time.dayOfWeekAbbr[e]) + .replace('{e}', e) + .replace('{HH}', pad(H, 2)) + .replace('{H}', H) + .replace('{hh}', pad(h + '', 2)) + .replace('{h}', h + '') + .replace('{mm}', pad(m, 2)) + .replace('{m}', m) + .replace('{ss}', pad(s, 2)) + .replace('{s}', s) + .replace('{SSS}', pad(S, 3)) + .replace('{S}', S); +} + +export function leveledFormat( + tick: TimeScaleTick, + idx: number, + formatter: TimeAxisLabelFormatterOption, + isUTC?: boolean +) { + let template = null; + if (typeof formatter === 'string') { + // Single formatter for all units at all levels + template = formatter; + } + else if (typeof formatter === 'function') { + // Callback formatter + template = formatter(tick, idx); + } + else { + const mergedFormatter = (formatter + ? (formatter.inherit + ? formatter // Use formatter with bigger units + : zrUtil.defaults(formatter, defaultLeveledFormatter) + ) + : defaultLeveledFormatter) as any; + + const unit = getUnitFromValue(tick.value, isUTC, true); + if (idx === 0 && mergedFormatter.first) { + template = mergedFormatter.first; + } + else if (mergedFormatter[unit]) { + template = mergedFormatter[unit]; + } + else if (mergedFormatter.inherit) { + // Unit formatter is not defined and should inherit from bigger units + const targetId = timeUnits.indexOf(unit); + for (let i = targetId - 1; i >= 0; --i) { + if (mergedFormatter[unit]) { + template = mergedFormatter[unit]; + break; + } + } + template = template || defaultLeveledFormatter.none; + } + + if (zrUtil.isArray(template)) { + const levelId = tick.level == null + ? 0 + : (tick.level >= 0 ? tick.level : template.length + tick.level); + template = template[levelId]; + } + } + + return format(new Date(tick.value), template, isUTC); +} + +export function getUnitFromValue ( + value: number | string | Date, + isUTC: boolean, + primaryOnly: boolean +): TimeUnit { + const date = numberUtil.parseDate(value); + const utc = isUTC ? 'UTC' : ''; + const M = (date as any)['get' + utc + 'Month']() + 1; + const w = (date as any)['get' + utc + 'Day'](); + const d = (date as any)['get' + utc + 'Date'](); + const h = (date as any)['get' + utc + 'Hours'](); + const m = (date as any)['get' + utc + 'Minutes'](); + const s = (date as any)['get' + utc + 'Seconds'](); + const S = (date as any)['get' + utc + 'Milliseconds'](); + + const isSecond = S === 0; + const isMinute = isSecond && s === 0; + const isHour = isMinute && m === 0; + const isDay = isHour && h === 0; + const isWeek = isDay && w === 0; // TODO: first day to be configured + const isMonth = isDay && d === 1; + const isQuarter = isMonth && (M % 3 === 1); + const isYear = isMonth && M === 1; + + if (isYear) { + return 'year'; + } + else if (isQuarter && !primaryOnly) { + return 'quarter'; + } + else if (isMonth) { + return 'month'; + } + else if (isWeek && !primaryOnly) { + return 'week'; + } + 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 = typeof value === 'number' + ? numberUtil.parseDate(value) as any + : value; + unit = unit || getUnitFromValue(value, isUTC, true); + const utc = isUTC ? 'UTC' : ''; + + switch (unit) { + case 'millisecond': + return date['get' + utc + 'Milliseconds'](); + case 'second': + return date['get' + utc + 'Seconds'](); + case 'minute': + return date['get' + utc + 'Minutes'](); + case 'hour': + return date['get' + utc + 'Hours'](); + case 'day': + return date['get' + utc + 'Date'](); + case 'month': + return date['get' + utc + 'Month'](); + case 'year': + return date['get' + utc + 'FullYear'](); + } +} diff --git a/test/timeScale.html b/test/timeScale.html index 8f10bb415d..aa525ddc68 100644 --- a/test/timeScale.html +++ b/test/timeScale.html @@ -142,6 +142,9 @@ saveAsImage : {show: true} } }, + grid: { + bottom: 100 + }, dataZoom : { show : true, realtime : true, @@ -155,14 +158,25 @@ axisLine: {onZero: false}, splitNumber: 20, // minInterval: 3600 * 1000 * 24 - // min: 'dataMin', - // max: 'dataMax' + min: 'dataMin', + max: 'dataMax', axisLabel: { formatter: { second: 'hh:mm:ss', minute: 'hh:mm', - hour: function (time) { - return time.getHours(); + day: ['{red|{d}}', '{gray|{d}}'], + first: '{bold|{MMM} {d}\n{yyyy}}', + // last: '{MMM} {d}' + }, + rich: { + red: { + color: 'red' + }, + gray: { + color: '#aaa' + }, + bold: { + fontWeight: 'bold' } } } From 0466be22ccf5e43debce90350034b49f0360995b Mon Sep 17 00:00:00 2001 From: Ovilia Date: Mon, 20 Jul 2020 16:48:44 +0800 Subject: [PATCH 07/11] feat(time): fix time axis extent --- src/coord/axisDefault.ts | 5 +- src/scale/Time.ts | 309 ++++++++++++++++++--------------------- src/util/time.ts | 76 ++++++---- test/timeScale.html | 4 +- 4 files changed, 192 insertions(+), 202 deletions(-) diff --git a/src/coord/axisDefault.ts b/src/coord/axisDefault.ts index 0fc6d74f06..b42fac1232 100644 --- a/src/coord/axisDefault.ts +++ b/src/coord/axisDefault.ts @@ -165,8 +165,11 @@ const timeAxis: AxisBaseOption = zrUtil.merge({ scale: true, axisLabel: { // To eliminate labels that are not nice - showMinLabel: true, + showMinLabel: false, showMaxLabel: false + }, + splitLine: { + show: false } }, valueAxis); diff --git a/src/scale/Time.ts b/src/scale/Time.ts index 03c20d614b..3f423db913 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -48,12 +48,11 @@ import {TimeScaleTick} from '../util/types'; import {TimeAxisLabelFormatterOption} from '../coord/axisCommonTypes'; -const mathCeil = Math.ceil; -const mathFloor = Math.floor; const ONE_SECOND = 1000; const ONE_MINUTE = ONE_SECOND * 60; const ONE_HOUR = ONE_MINUTE * 60; const ONE_DAY = ONE_HOUR * 24; +const ONE_YEAR = ONE_DAY * 365; // FIXME 公用? const bisect = function ( @@ -80,7 +79,7 @@ class TimeScale extends IntervalScale { static type = 'time'; readonly type = 'time'; - private _stepLvl: [string, number]; + _approxInterval: number; getLabel(tick: TimeScaleTick): string { // const stepLvl = this._stepLvl; @@ -109,7 +108,6 @@ class TimeScale extends IntervalScale { getTicks(expandToNicedExtent?: boolean): TimeScaleTick[] { const interval = this._interval; const extent = this._extent; - const niceTickExtent = this._niceExtent; let ticks = [] as TimeScaleTick[]; // If interval is 0, return []; @@ -117,53 +115,30 @@ class TimeScale extends IntervalScale { return ticks; } - if (extent[0] < niceTickExtent[0]) { - if (expandToNicedExtent) { - ticks.push({ - value: numberUtil.round(niceTickExtent[0] - interval, 0), - level: -1 // TODO: - }); - } - else { - ticks.push({ - value: extent[0], - level: -1 - }); - } - } + ticks.push({ + value: extent[0], + level: -1 // TODO: + }); const useUTC = this.getSetting('useUTC'); - const unitLen = primaryUnitIntervals.length; - const idx = bisect(primaryUnitIntervals, this._interval, 0, unitLen); - const intervals = primaryUnitIntervals[Math.min(idx, unitLen - 1)]; + const unitLen = scaleIntervals.length; + const idx = bisect(scaleIntervals, this._interval, 0, unitLen); + const intervals = scaleIntervals[Math.min(idx, unitLen - 1)]; const innerTicks = getIntervalTicks( intervals[0] as timeUtil.PrimaryTimeUnit, + this._approxInterval, useUTC, extent ); + ticks = ticks.concat(innerTicks); - // Consider this case: the last item of ticks is smaller - // than niceTickExtent[1] and niceTickExtent[1] === extent[1]. - const lastNiceTick = ticks.length - ? ticks[ticks.length - 1].value - : niceTickExtent[1]; - if (extent[1] > lastNiceTick) { - if (expandToNicedExtent) { - ticks.push({ - value: numberUtil.round(lastNiceTick + interval, 0), - level: -1 - }); - } - else { - ticks.push({ - value: extent[1], - level: -1 - }); - } - } + ticks.push({ + value: extent[1], + level: -1 + }); return ticks; } @@ -192,18 +167,6 @@ class TimeScale extends IntervalScale { } this.niceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval); - - // let extent = this._extent; - const interval = this._interval; - - const timezoneOffset = this.getSetting('useUTC') - ? 0 : (new Date(+extent[0] || +extent[1])).getTimezoneOffset() * 60 * 1000; - if (!opt.fixMin) { - extent[0] = numberUtil.round(mathFloor((extent[0] - timezoneOffset) / interval) * interval) + timezoneOffset; - } - if (!opt.fixMax) { - extent[1] = numberUtil.round(mathCeil((extent[1] - timezoneOffset) / interval) * interval) + timezoneOffset; - } } niceTicks(approxTickNum: number, minInterval: number, maxInterval: number): void { @@ -211,44 +174,23 @@ class TimeScale extends IntervalScale { const extent = this._extent; const span = extent[1] - extent[0]; - let approxInterval = span / approxTickNum; + this._approxInterval = span / approxTickNum; - if (minInterval != null && approxInterval < minInterval) { - approxInterval = minInterval; + if (minInterval != null && this._approxInterval < minInterval) { + this._approxInterval = minInterval; } - if (maxInterval != null && approxInterval > maxInterval) { - approxInterval = maxInterval; + if (maxInterval != null && this._approxInterval > maxInterval) { + this._approxInterval = maxInterval; } const scaleIntervalsLen = scaleIntervals.length; - const idx = bisect(scaleIntervals, approxInterval, 0, scaleIntervalsLen); + const idx = bisect(scaleIntervals, this._approxInterval, 0, scaleIntervalsLen); const intervals = scaleIntervals[Math.min(idx, scaleIntervalsLen - 1)]; let interval = intervals[1]; - // Same with interval scale if span is much larger than 1 year - if (intervals[0] === 'year') { - const yearSpan = span / interval; - - // From "Nice Numbers for Graph Labels" of Graphic Gems - // let niceYearSpan = numberUtil.nice(yearSpan, false); - const yearStep = numberUtil.nice(yearSpan / approxTickNum, true); - - interval *= yearStep; - } - const timezoneOffset = this.getSetting('useUTC') - ? 0 : (new Date(+extent[0] || +extent[1])).getTimezoneOffset() * 60 * 1000; - const niceExtent = [ - Math.round(mathCeil((extent[0] - timezoneOffset) / interval) * interval + timezoneOffset), - Math.round(mathFloor((extent[1] - timezoneOffset) / interval) * interval + timezoneOffset) - ] as [number, number]; - - scaleHelper.fixExtent(niceExtent, extent); - - this._stepLvl = intervals; // Interval will be used in getTicks this._interval = interval; - this._niceExtent = niceExtent; } parse(val: number | string | Date): number { @@ -277,55 +219,19 @@ class TimeScale extends IntervalScale { * with some modifications made for this program. * See the license statement at the head of this file. */ -const scaleIntervals = [ - // Format interval - ['hh:mm:ss', ONE_SECOND], // 1s - ['hh:mm:ss', ONE_SECOND * 5], // 5s - ['hh:mm:ss', ONE_SECOND * 10], // 10s - ['hh:mm:ss', ONE_SECOND * 15], // 15s - ['hh:mm:ss', ONE_SECOND * 30], // 30s - ['hh:mm\nMM-dd', ONE_MINUTE], // 1m - ['hh:mm\nMM-dd', ONE_MINUTE * 5], // 5m - ['hh:mm\nMM-dd', ONE_MINUTE * 10], // 10m - ['hh:mm\nMM-dd', ONE_MINUTE * 15], // 15m - ['hh:mm\nMM-dd', ONE_MINUTE * 30], // 30m - ['hh:mm\nMM-dd', ONE_HOUR], // 1h - ['hh:mm\nMM-dd', ONE_HOUR * 2], // 2h - ['hh:mm\nMM-dd', ONE_HOUR * 6], // 6h - ['hh:mm\nMM-dd', ONE_HOUR * 12], // 12h - ['MM-dd\nyyyy', ONE_DAY], // 1d - ['MM-dd\nyyyy', ONE_DAY * 2], // 2d - ['MM-dd\nyyyy', ONE_DAY * 3], // 3d - ['MM-dd\nyyyy', ONE_DAY * 4], // 4d - ['MM-dd\nyyyy', ONE_DAY * 5], // 5d - ['MM-dd\nyyyy', ONE_DAY * 6], // 6d - ['week', ONE_DAY * 7], // 7d - ['MM-dd\nyyyy', ONE_DAY * 10], // 10d - ['week', ONE_DAY * 14], // 2w - ['week', ONE_DAY * 21], // 3w - ['month', ONE_DAY * 31], // 1M - ['week', ONE_DAY * 42], // 6w - ['month', ONE_DAY * 62], // 2M - ['week', ONE_DAY * 70], // 10w - ['quarter', ONE_DAY * 95], // 3M - ['month', ONE_DAY * 31 * 4], // 4M - ['month', ONE_DAY * 31 * 5], // 5M - ['half-year', ONE_DAY * 380 / 2], // 6M - ['month', ONE_DAY * 31 * 8], // 8M - ['month', ONE_DAY * 31 * 10], // 10M - ['year', ONE_DAY * 380] // 1Y -] as [string, number][]; - -const primaryUnitIntervals = [ - // Format interval - ['second', ONE_SECOND], // 1s - ['minute', ONE_MINUTE], // 1m - ['hour', ONE_HOUR], // 1h - ['day', ONE_DAY], // 1d - ['week', ONE_DAY * 7], // 7d - ['month', ONE_DAY * 31], // 1M - ['year', ONE_DAY * 380] // 1Y -] as [string, number][]; +const scaleIntervals: [timeUtil.TimeUnit, number][] = [ + // Format interval + ['second', ONE_SECOND], // 1s + ['minute', ONE_MINUTE], // 1m + ['hour', ONE_HOUR], // 1h + ['half-day', ONE_HOUR * 12], // 12h + ['day', ONE_DAY * 1.2], // 1d + ['week', ONE_DAY * 7], // 7d + ['month', ONE_DAY * 31], // 1M + ['quarter', ONE_DAY * 95], // 3M + ['half-year', ONE_YEAR / 2], // 6M + ['year', ONE_YEAR] // 1Y +]; function isUnitValueSame( unit: timeUtil.PrimaryTimeUnit, @@ -341,8 +247,11 @@ function isUnitValueSame( === timeUtil.getUnitValue(dateB, unit, isUTC); }; const isSameYear = () => isSame('year'); + const isSameHalfYear = () => isSameYear() && isSame('half-year'); + const isSameQuater = () => isSameYear() && isSame('quarter'); const isSameMonth = () => isSameYear() && isSame('month'); const isSameDay = () => isSameMonth() && isSame('day'); + const isSameHalfDay = () => isSameDay && isSame('half-day'); const isSameHour = () => isSameDay() && isSame('hour'); const isSameMinute = () => isSameHour() && isSame('minute'); const isSameSecond = () => isSameMinute() && isSame('second'); @@ -351,10 +260,16 @@ function isUnitValueSame( switch (unit) { case 'year': return isSameYear(); + case 'half-year': + return isSameHalfYear(); + case 'quarter': + return isSameQuater(); case 'month': return isSameMonth(); case 'day': return isSameDay(); + case 'half-day': + return isSameHalfDay(); case 'hour': return isSameHour(); case 'minute': @@ -369,13 +284,14 @@ function isUnitValueSame( function getIntervalTicks( unitName: timeUtil.TimeUnit, + approxInterval: number, isUTC: boolean, extent: number[] ): TimeScaleTick[] { const utc = isUTC ? 'UTC' : ''; const ticks: TimeScaleTick[] = []; - const unitNames = primaryUnitIntervals.map(i => i[0]); - for (let i = unitNames.length - 1, levelId = 0; i >= 0; --i) { + const unitNames = timeUtil.timeUnits; + for (let i = 0, levelId = 0; i < unitNames.length; ++i) { let date = new Date(extent[0]) as any; if (unitNames[i] === 'week') { @@ -384,6 +300,13 @@ function getIntervalTicks( date['set' + utc + 'Seconds'](0); date['set' + utc + 'Milliseconds'](0); + if (extent[0] === date.getTime()) { + ticks.push({ + value: extent[0], + level: levelId + }); + } + let isDateWithinExtent = true; while (isDateWithinExtent) { const dates = date['get' + utc + 'Month']() + 1 === 2 @@ -411,54 +334,106 @@ function getIntervalTicks( extent[0], extent[1], isUTC )) { // Level value changes within extent + let isFirst = true; while (true) { - if (unitNames[i] === 'year') { - date['set' + utc + 'FullYear'](date['get' + utc + 'FullYear']() + 1); - date['set' + utc + 'Month'](0); - date['set' + utc + 'Date'](1); - date['set' + utc + 'Hours'](0); - date['set' + utc + 'Minutes'](0); - date['set' + utc + 'Seconds'](0); - } - else if (unitNames[i] === 'month') { - // This also works with Dec. - date['set' + utc + 'Month'](date['get' + utc + 'Month']() + 1); - date['set' + utc + 'Date'](1); - date['set' + utc + 'Hours'](0); - date['set' + utc + 'Minutes'](0); - date['set' + utc + 'Seconds'](0); - } - else if (unitNames[i] === 'day') { - date['set' + utc + 'Date'](date['get' + utc + 'Date']() + 1); - date['set' + utc + 'Hours'](0); - date['set' + utc + 'Minutes'](0); - date['set' + utc + 'Seconds'](0); - } - else if (unitNames[i] === 'hour') { - date['set' + utc + 'Hours'](date['get' + utc + 'Hours']() + 1); - date['set' + utc + 'Minutes'](0); - date['set' + utc + 'Seconds'](0); - } - else if (unitNames[i] === 'minute') { - date['set' + utc + 'Minutes'](date['get' + utc + 'Minutes']() + 1); - date['set' + utc + 'Minutes'](0); - date['set' + utc + 'Seconds'](0); - } - else if (unitNames[i] === 'second') { - date['set' + utc + 'Seconds'](date['get' + utc + 'Seconds']() + 1); + switch (unitNames[i] as timeUtil.PrimaryTimeUnit) { + case 'year': + case 'half-year': + case 'quarter': + if (isFirst) { + date['set' + utc + 'Month'](0); + date['set' + utc + 'Date'](1); + date['set' + utc + 'Hours'](0); + date['set' + utc + 'Minutes'](0); + date['set' + utc + 'Seconds'](0); + } + else { + const months = unitNames[i] === 'year' + ? 12 : (unitNames[i] === 'half-year' ? 6 : 3); + if (date['get' + utc + 'Month']() + 1 + months < 12) { + date['set' + utc + 'Month'](date['get' + utc + 'Month']() + months); + } + else { + const yearSpan = Math.max(1, Math.round(approxInterval / ONE_DAY / 365)); + date['set' + utc + 'FullYear'](date['get' + utc + 'FullYear']() + yearSpan); + if (date.getTime() > extent[1] && yearSpan > 1) { + // For the last data + date['set' + utc + 'FullYear'](date['get' + utc + 'FullYear']() - yearSpan + 1); + if (date.getTime() < extent[1]) { + // The last data is not in year unit, make it invalid by larger than extent[1] + date['set' + utc + 'FullYear'](date['get' + utc + 'FullYear']() + yearSpan); + } + } + } + } + break; + + case 'month': + if (isFirst) { + date['set' + utc + 'Date'](1); + date['set' + utc + 'Hours'](0); + date['set' + utc + 'Minutes'](0); + date['set' + utc + 'Seconds'](0); + } + else { + date['set' + utc + 'Month'](date['get' + utc + 'Month']() + 1); + } + break; + + case 'day': + case 'half-day': + if (isFirst) { + date['set' + utc + 'Hours'](0); + date['set' + utc + 'Minutes'](0); + date['set' + utc + 'Seconds'](0); + } + else if (unitNames[i] === 'half-day') { + date['set' + utc + 'Hours'](date['get' + utc + 'Hours']() + 12); + } + else { + date['set' + utc + 'Date'](date['get' + utc + 'Date']() + 1); + } + break; + + case 'hour': + if (isFirst) { + date['set' + utc + 'Minutes'](0); + date['set' + utc + 'Seconds'](0); + } + else { + date['set' + utc + 'Hours'](date['get' + utc + 'Hours']() + 1); + } + break; + + case 'minute': + if (isFirst) { + date['set' + utc + 'Minutes'](0); + date['set' + utc + 'Seconds'](0); + } + else { + date['set' + utc + 'Minutes'](date['get' + utc + 'Minutes']() + 1); + } + break; + + case 'second': + if (!isFirst) { + date['set' + utc + 'Seconds'](date['get' + utc + 'Seconds']() + 1); + } + break; } - date['set' + utc + 'Milliseconds'](0); // TODO: not sure + date['set' + utc + 'Milliseconds'](0); const dateValue = (date as Date).getTime(); - if (dateValue < extent[1]) { + if (dateValue >= extent[0] && dateValue <= extent[1]) { ticks.push({ value: dateValue, level: levelId }); } - else { + else if (dateValue > extent[1]) { break; } + isFirst = false; } ++levelId; } diff --git a/src/util/time.ts b/src/util/time.ts index 462fcc7728..c53046db0f 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -6,24 +6,26 @@ import lang from '../lang'; import {TimeScaleTick} from './types'; export const defaultLeveledFormatter = { - millisecond: '{hh}:{mm}:{ss} {SSS}', - second: '{hh}:{mm}:{ss}', - minute: '{hh}:{mm}', - hour: '{hh}:{mm}', - day: '{d}', - week: '{d}', + year: '{yyyy}', + 'half-year': '{MMM}', + quarter: '{MMM}', month: '{MMM}', - quarter: 'Q{Q}', - year: '{YYYY}', - none: '{YYYY}-{MM}-{dd} {hh}:{mm}' + week: '{d}', + day: '{d}', + hour: '{hh}:{mm}', + 'half-day': '{hh}:{mm}', + minute: '{hh}:{mm}', + second: '{hh}:{mm}:{ss}', + millisecond: '{hh}:{mm}:{ss} {SSS}', + none: '{yyyy}-{MM}-{dd} {hh}:{mm}' }; export type PrimaryTimeUnit = 'millisecond' | 'second' | 'minute' | 'hour' - | 'day' | 'month' | 'year'; -export type TimeUnit = PrimaryTimeUnit | 'week' | 'quarter'; + | 'half-day' | 'day' | 'month' | 'half-year' | 'quarter' | 'year'; +export type TimeUnit = PrimaryTimeUnit | 'week'; export const timeUnits: TimeUnit[] = [ - 'year', 'quarter', 'month', 'week', 'day', + 'year', 'half-year', 'quarter', 'month', 'week', 'day', 'half-day', 'hour', 'minute', 'second', 'millisecond' ]; @@ -84,17 +86,14 @@ export function leveledFormat( } else { const mergedFormatter = (formatter - ? (formatter.inherit + ? (formatter.inherit === false ? formatter // Use formatter with bigger units : zrUtil.defaults(formatter, defaultLeveledFormatter) ) : defaultLeveledFormatter) as any; const unit = getUnitFromValue(tick.value, isUTC, true); - if (idx === 0 && mergedFormatter.first) { - template = mergedFormatter.first; - } - else if (mergedFormatter[unit]) { + if (mergedFormatter[unit]) { template = mergedFormatter[unit]; } else if (mergedFormatter.inherit) { @@ -110,9 +109,10 @@ export function leveledFormat( } if (zrUtil.isArray(template)) { - const levelId = tick.level == null + let levelId = tick.level == null ? 0 : (tick.level >= 0 ? tick.level : template.length + tick.level); + levelId = Math.min(levelId, template.length - 1); template = template[levelId]; } } @@ -138,16 +138,21 @@ export function getUnitFromValue ( const isSecond = S === 0; const isMinute = isSecond && s === 0; const isHour = isMinute && m === 0; + const isHalfDay = isHour && (h === 0 || h === 11); const isDay = isHour && h === 0; const isWeek = isDay && w === 0; // TODO: first day to be configured const isMonth = isDay && d === 1; const isQuarter = isMonth && (M % 3 === 1); + const isHalfYear = isMonth && (M % 6 === 1); const isYear = isMonth && M === 1; if (isYear) { return 'year'; } - else if (isQuarter && !primaryOnly) { + else if (isHalfYear) { + return 'half-year'; + } + else if (isQuarter) { return 'quarter'; } else if (isMonth) { @@ -159,6 +164,9 @@ export function getUnitFromValue ( else if (isDay) { return 'day'; } + else if (isHalfDay) { + return 'half-day'; + } else if (isHour) { return 'hour'; } @@ -185,19 +193,25 @@ export function getUnitValue ( const utc = isUTC ? 'UTC' : ''; switch (unit) { - case 'millisecond': - return date['get' + utc + 'Milliseconds'](); - case 'second': - return date['get' + utc + 'Seconds'](); - case 'minute': - return date['get' + utc + 'Minutes'](); - case 'hour': - return date['get' + utc + 'Hours'](); - case 'day': - return date['get' + utc + 'Date'](); - case 'month': - return date['get' + utc + 'Month'](); case 'year': return date['get' + utc + 'FullYear'](); + case 'half-year': + return date['get' + utc + 'Month']() >= 6 ? 1 : 0; + case 'quarter': + return Math.floor((date['get' + utc + 'Month']() + 1) / 4); + case 'month': + return date['get' + utc + 'Month'](); + case 'day': + return date['get' + utc + 'Date'](); + case 'half-day': + return date['get' + utc + 'Hours']() / 24; + case 'hour': + return date['get' + utc + 'Hours'](); + case 'minute': + return date['get' + utc + 'Minutes'](); + case 'second': + return date['get' + utc + 'Seconds'](); + case 'millisecond': + return date['get' + utc + 'Milliseconds'](); } } diff --git a/test/timeScale.html b/test/timeScale.html index aa525ddc68..a2c253f799 100644 --- a/test/timeScale.html +++ b/test/timeScale.html @@ -164,9 +164,7 @@ formatter: { second: 'hh:mm:ss', minute: 'hh:mm', - day: ['{red|{d}}', '{gray|{d}}'], - first: '{bold|{MMM} {d}\n{yyyy}}', - // last: '{MMM} {d}' + day: ['{red|{d}}', '{gray|{d}}'] }, rich: { red: { From a14c8cd95c70559f79eea04808528e1afdbd6dc8 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Mon, 20 Jul 2020 19:16:49 +0800 Subject: [PATCH 08/11] feat(time): level formatters --- src/coord/axisDefault.ts | 8 +- src/scale/Time.ts | 48 +++++-- src/util/time.ts | 14 +- test/timeScale-formatter.html | 253 ++++++++++++++++++++++++++++++++++ test/timeScale.html | 32 ++--- 5 files changed, 324 insertions(+), 31 deletions(-) create mode 100644 test/timeScale-formatter.html diff --git a/src/coord/axisDefault.ts b/src/coord/axisDefault.ts index b42fac1232..ab3f0b098a 100644 --- a/src/coord/axisDefault.ts +++ b/src/coord/axisDefault.ts @@ -166,7 +166,13 @@ const timeAxis: AxisBaseOption = zrUtil.merge({ axisLabel: { // To eliminate labels that are not nice showMinLabel: false, - showMaxLabel: false + showMaxLabel: false, + rich: { + primary: { + color: '#000', + fontWeight: 'bold' + } + } }, splitLine: { show: false diff --git a/src/scale/Time.ts b/src/scale/Time.ts index 3f423db913..891f3234c5 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -117,7 +117,7 @@ class TimeScale extends IntervalScale { ticks.push({ value: extent[0], - level: -1 // TODO: + level: 0 }); const useUTC = this.getSetting('useUTC'); @@ -137,7 +137,7 @@ class TimeScale extends IntervalScale { ticks.push({ value: extent[1], - level: -1 + level: 0 }); return ticks; @@ -251,7 +251,7 @@ function isUnitValueSame( const isSameQuater = () => isSameYear() && isSame('quarter'); const isSameMonth = () => isSameYear() && isSame('month'); const isSameDay = () => isSameMonth() && isSame('day'); - const isSameHalfDay = () => isSameDay && isSame('half-day'); + const isSameHalfDay = () => isSameDay() && isSame('half-day'); const isSameHour = () => isSameDay() && isSame('hour'); const isSameMinute = () => isSameHour() && isSame('minute'); const isSameSecond = () => isSameMinute() && isSame('second'); @@ -291,10 +291,18 @@ function getIntervalTicks( const utc = isUTC ? 'UTC' : ''; const ticks: TimeScaleTick[] = []; const unitNames = timeUtil.timeUnits; - for (let i = 0, levelId = 0; i < unitNames.length; ++i) { + let levelId = 0; + for (let i = 0, hasTickInLevel = false; i < unitNames.length; ++i) { let date = new Date(extent[0]) as any; if (unitNames[i] === 'week') { + const endDate = new Date(extent[1]) as any; + if (date['get' + utc + 'FullYear']() === endDate['get' + utc + 'FullYear']() + && date['get' + utc + 'Month']() === endDate['get' + utc + 'Month']() + ) { + continue; + } + date['set' + utc + 'Hours'](0); date['set' + utc + 'Minutes'](0); date['set' + utc + 'Seconds'](0); @@ -305,9 +313,11 @@ function getIntervalTicks( value: extent[0], level: levelId }); + hasTickInLevel = true; } let isDateWithinExtent = true; + let hasWeekData = false; while (isDateWithinExtent) { const dates = date['get' + utc + 'Month']() + 1 === 2 ? [8, 15, 22] @@ -320,14 +330,19 @@ function getIntervalTicks( break; } else if (dateTime >= extent[0]) { + hasWeekData = true; ticks.push({ value: dateTime, level: levelId }); + hasTickInLevel = true; } } date['set' + utc + 'Month'](date['get' + utc + 'Month']() + 1); } + if (hasWeekData) { + ++levelId; + } } else if (!isUnitValueSame( unitNames[i] as timeUtil.PrimaryTimeUnit, @@ -429,13 +444,18 @@ function getIntervalTicks( value: dateValue, level: levelId }); + hasTickInLevel = true; } else if (dateValue > extent[1]) { break; } isFirst = false; } - ++levelId; + if (hasTickInLevel + && (['half-year', 'quarter', 'half-day'].indexOf(unitNames[i]) < 0) + ) { + ++levelId; + } } if (unitNames[i] === unitName) { @@ -444,17 +464,23 @@ function getIntervalTicks( } ticks.sort((a, b) => a.value - b.value); - if (ticks.length <= 1) { - return ticks; + + let maxLevel = -Number.MAX_VALUE; + for (let i = 0; i < ticks.length; ++i) { + maxLevel = Math.max(maxLevel, ticks[i].level); } // Remove duplicates - const result = ticks.length ? [ticks[0]] : []; - for (let i = 1; i < ticks.length; ++i) { - if (ticks[i] !== ticks[i - 1]) { - result.push(ticks[i]); + const result = []; + for (let i = 0; i < ticks.length; ++i) { + if (i === 0 || ticks[i].value !== ticks[i - 1].value) { + result.push({ + value: ticks[i].value, + level: maxLevel - ticks[i].level + }); } } + return result; } diff --git a/src/util/time.ts b/src/util/time.ts index c53046db0f..5dfb193e7a 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -85,12 +85,19 @@ export function leveledFormat( template = formatter(tick, idx); } else { + const defaults = zrUtil.extend({}, defaultLeveledFormatter); + if (tick.level > 0) { + for (let i = 0; i < timeUnits.length; ++i) { + defaults[timeUnits[i]] = `{primary|${defaults[timeUnits[i]]}}`; + } + } + const mergedFormatter = (formatter ? (formatter.inherit === false ? formatter // Use formatter with bigger units - : zrUtil.defaults(formatter, defaultLeveledFormatter) + : zrUtil.defaults(formatter, defaults) ) - : defaultLeveledFormatter) as any; + : defaults) as any; const unit = getUnitFromValue(tick.value, isUTC, true); if (mergedFormatter[unit]) { @@ -105,7 +112,7 @@ export function leveledFormat( break; } } - template = template || defaultLeveledFormatter.none; + template = template || defaults.none; } if (zrUtil.isArray(template)) { @@ -117,6 +124,7 @@ export function leveledFormat( } } + // console.log(tick.level, new Date(tick.value), template); return format(new Date(tick.value), template, isUTC); } diff --git a/test/timeScale-formatter.html b/test/timeScale-formatter.html new file mode 100644 index 0000000000..dd2179feda --- /dev/null +++ b/test/timeScale-formatter.html @@ -0,0 +1,253 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/timeScale.html b/test/timeScale.html index a2c253f799..98f78b4201 100644 --- a/test/timeScale.html +++ b/test/timeScale.html @@ -161,22 +161,22 @@ min: 'dataMin', max: 'dataMax', axisLabel: { - formatter: { - second: 'hh:mm:ss', - minute: 'hh:mm', - day: ['{red|{d}}', '{gray|{d}}'] - }, - rich: { - red: { - color: 'red' - }, - gray: { - color: '#aaa' - }, - bold: { - fontWeight: 'bold' - } - } + // formatter: { + // second: 'hh:mm:ss', + // minute: 'hh:mm', + // day: ['{red|{d}}', '{gray|{d}}'] + // }, + // rich: { + // red: { + // color: 'red' + // }, + // gray: { + // color: '#aaa' + // }, + // bold: { + // fontWeight: 'bold' + // } + // } } } ], From 3bee422bbc8d56ed607712ee6a644e2a28331579 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Tue, 21 Jul 2020 17:06:19 +0800 Subject: [PATCH 09/11] feat(time): add half day and more --- src/scale/Time.ts | 50 ++++++++++++++++++++++++++------------------- src/util/time.ts | 40 ++++++++++++++++++++++++++++-------- test/timeScale.html | 16 --------------- 3 files changed, 61 insertions(+), 45 deletions(-) diff --git a/src/scale/Time.ts b/src/scale/Time.ts index 891f3234c5..4627e9cafd 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -224,8 +224,10 @@ const scaleIntervals: [timeUtil.TimeUnit, number][] = [ ['second', ONE_SECOND], // 1s ['minute', ONE_MINUTE], // 1m ['hour', ONE_HOUR], // 1h + ['quarter-day', ONE_HOUR * 6], // 6h ['half-day', ONE_HOUR * 12], // 12h ['day', ONE_DAY * 1.2], // 1d + ['half-week', ONE_DAY * 3.5], // 3.5d ['week', ONE_DAY * 7], // 7d ['month', ONE_DAY * 31], // 1M ['quarter', ONE_DAY * 95], // 3M @@ -247,11 +249,11 @@ function isUnitValueSame( === timeUtil.getUnitValue(dateB, unit, isUTC); }; const isSameYear = () => isSame('year'); - const isSameHalfYear = () => isSameYear() && isSame('half-year'); - const isSameQuater = () => isSameYear() && isSame('quarter'); + // const isSameHalfYear = () => isSameYear() && isSame('half-year'); + // const isSameQuater = () => isSameYear() && isSame('quarter'); const isSameMonth = () => isSameYear() && isSame('month'); const isSameDay = () => isSameMonth() && isSame('day'); - const isSameHalfDay = () => isSameDay() && isSame('half-day'); + // const isSameHalfDay = () => isSameDay() && isSame('half-day'); const isSameHour = () => isSameDay() && isSame('hour'); const isSameMinute = () => isSameHour() && isSame('minute'); const isSameSecond = () => isSameMinute() && isSame('second'); @@ -260,16 +262,10 @@ function isUnitValueSame( switch (unit) { case 'year': return isSameYear(); - case 'half-year': - return isSameHalfYear(); - case 'quarter': - return isSameQuater(); case 'month': return isSameMonth(); case 'day': return isSameDay(); - case 'half-day': - return isSameHalfDay(); case 'hour': return isSameHour(); case 'minute': @@ -295,7 +291,7 @@ function getIntervalTicks( for (let i = 0, hasTickInLevel = false; i < unitNames.length; ++i) { let date = new Date(extent[0]) as any; - if (unitNames[i] === 'week') { + if (unitNames[i] === 'week' || unitNames[i] === 'half-week') { const endDate = new Date(extent[1]) as any; if (date['get' + utc + 'FullYear']() === endDate['get' + utc + 'FullYear']() && date['get' + utc + 'Month']() === endDate['get' + utc + 'Month']() @@ -319,9 +315,8 @@ function getIntervalTicks( let isDateWithinExtent = true; let hasWeekData = false; while (isDateWithinExtent) { - const dates = date['get' + utc + 'Month']() + 1 === 2 - ? [8, 15, 22] - : [8, 16, 24]; + const dates = approxInterval > ONE_DAY * 8 ? [] + : (approxInterval > ONE_DAY * 3.5 ? [8, 16, 24] : [4, 8, 12, 16, 20, 24, 28]); for (let d = 0; d < dates.length; ++d) { date['set' + utc + 'Date'](dates[d]); const dateTime = (date as Date).getTime(); @@ -340,18 +335,15 @@ function getIntervalTicks( } date['set' + utc + 'Month'](date['get' + utc + 'Month']() + 1); } - if (hasWeekData) { - ++levelId; - } } else if (!isUnitValueSame( - unitNames[i] as timeUtil.PrimaryTimeUnit, + timeUtil.getPrimaryTimeUnit(unitNames[i]), extent[0], extent[1], isUTC )) { // Level value changes within extent let isFirst = true; while (true) { - switch (unitNames[i] as timeUtil.PrimaryTimeUnit) { + switch (unitNames[i]) { case 'year': case 'half-year': case 'quarter': @@ -365,7 +357,7 @@ function getIntervalTicks( else { const months = unitNames[i] === 'year' ? 12 : (unitNames[i] === 'half-year' ? 6 : 3); - if (date['get' + utc + 'Month']() + 1 + months < 12) { + if (unitNames[i] === 'half-year' || unitNames[i] === 'quarter') { date['set' + utc + 'Month'](date['get' + utc + 'Month']() + months); } else { @@ -397,6 +389,7 @@ function getIntervalTicks( case 'day': case 'half-day': + case 'quarter-day': if (isFirst) { date['set' + utc + 'Hours'](0); date['set' + utc + 'Minutes'](0); @@ -405,6 +398,9 @@ function getIntervalTicks( else if (unitNames[i] === 'half-day') { date['set' + utc + 'Hours'](date['get' + utc + 'Hours']() + 12); } + else if (unitNames[i] === 'quarter-day') { + date['set' + utc + 'Hours'](date['get' + utc + 'Hours']() + 6); + } else { date['set' + utc + 'Date'](date['get' + utc + 'Date']() + 1); } @@ -435,8 +431,19 @@ function getIntervalTicks( date['set' + utc + 'Seconds'](date['get' + utc + 'Seconds']() + 1); } break; + + case 'millisecond': + if (isFirst) { + date['set' + utc + 'Milliseconds'](0); + } + else { + date['set' + utc + 'MilliSeconds'](date['get' + utc + 'MilliSeconds']() + 100); + } + break; + } + if (isFirst && unitNames[i] !== 'millisecond') { + date['set' + utc + 'Milliseconds'](0); } - date['set' + utc + 'Milliseconds'](0); const dateValue = (date as Date).getTime(); if (dateValue >= extent[0] && dateValue <= extent[1]) { @@ -452,7 +459,7 @@ function getIntervalTicks( isFirst = false; } if (hasTickInLevel - && (['half-year', 'quarter', 'half-day'].indexOf(unitNames[i]) < 0) + && timeUtil.isPrimaryTimeUnit(unitNames[i]) ) { ++levelId; } @@ -465,6 +472,7 @@ function getIntervalTicks( ticks.sort((a, b) => a.value - b.value); + console.log(ticks); let maxLevel = -Number.MAX_VALUE; for (let i = 0; i < ticks.length; ++i) { maxLevel = Math.max(maxLevel, ticks[i].level); diff --git a/src/util/time.ts b/src/util/time.ts index 5dfb193e7a..fbb96d659a 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -12,23 +12,47 @@ export const defaultLeveledFormatter = { month: '{MMM}', week: '{d}', day: '{d}', - hour: '{hh}:{mm}', - 'half-day': '{hh}:{mm}', - minute: '{hh}:{mm}', - second: '{hh}:{mm}:{ss}', + hour: '{HH}:{mm}', + 'half-day': '{HH}:{mm}', + minute: '{HH}:{mm}', + second: '{HH}:{mm}:{ss}', millisecond: '{hh}:{mm}:{ss} {SSS}', none: '{yyyy}-{MM}-{dd} {hh}:{mm}' }; export type PrimaryTimeUnit = 'millisecond' | 'second' | 'minute' | 'hour' - | 'half-day' | 'day' | 'month' | 'half-year' | 'quarter' | 'year'; -export type TimeUnit = PrimaryTimeUnit | 'week'; + | 'day' | 'month' | 'year'; +export type TimeUnit = PrimaryTimeUnit | 'half-year' | 'quarter' | 'week' + | 'half-week' | 'half-day' | 'quarter-day'; +export const primaryTimeUnits: PrimaryTimeUnit[] = [ + 'year', 'month', 'day', 'hour', 'minute', 'second', 'millisecond' +]; export const timeUnits: TimeUnit[] = [ - 'year', 'half-year', 'quarter', 'month', 'week', 'day', 'half-day', - 'hour', 'minute', 'second', 'millisecond' + 'year', 'half-year', 'quarter', 'month', 'week', 'half-week', 'day', + 'half-day', 'quarter-day', 'hour', 'minute', 'second', 'millisecond' ]; +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: + return timeUnit; + } +} + +export function isPrimaryTimeUnit(timeUnit: TimeUnit): boolean { + return timeUnit === getPrimaryTimeUnit(timeUnit); +} + export function format(time: Date, template: string, isUTC?: boolean): string { const date = numberUtil.parseDate(time); diff --git a/test/timeScale.html b/test/timeScale.html index 98f78b4201..d6e899d23b 100644 --- a/test/timeScale.html +++ b/test/timeScale.html @@ -161,22 +161,6 @@ min: 'dataMin', max: 'dataMax', axisLabel: { - // formatter: { - // second: 'hh:mm:ss', - // minute: 'hh:mm', - // day: ['{red|{d}}', '{gray|{d}}'] - // }, - // rich: { - // red: { - // color: 'red' - // }, - // gray: { - // color: '#aaa' - // }, - // bold: { - // fontWeight: 'bold' - // } - // } } } ], From cca2853d0766292f20284a63fd074cb7054a1a5c Mon Sep 17 00:00:00 2001 From: Ovilia Date: Tue, 21 Jul 2020 17:44:47 +0800 Subject: [PATCH 10/11] feat(time): remove unnecessary code and update test cases --- src/export.ts | 2 + src/scale/Time.ts | 6 +- src/util/format.ts | 23 ++----- src/util/time.ts | 37 ++-------- test/timeScale-formatter.html | 123 ++++++++++++++++++++++++++++++++-- 5 files changed, 133 insertions(+), 58 deletions(-) diff --git a/src/export.ts b/src/export.ts index f45beb3489..3f9c8d65a6 100644 --- a/src/export.ts +++ b/src/export.ts @@ -29,6 +29,7 @@ import * as colorTool from 'zrender/src/tool/color'; import * as graphicUtil from './util/graphic'; import * as numberUtil from './util/number'; import * as formatUtil from './util/format'; +import * as timeUtil from './util/time'; import {throttle} from './util/throttle'; import * as ecHelper from './helper'; import parseGeoJSON from './coord/geo/parseGeoJson'; @@ -40,6 +41,7 @@ export {default as Model} from './model/Model'; export {default as Axis} from './coord/Axis'; export {numberUtil as number}; export {formatUtil as format}; +export {timeUtil as time}; export {throttle}; export {ecHelper as helper}; export {matrix}; diff --git a/src/scale/Time.ts b/src/scale/Time.ts index 4627e9cafd..6f43e58341 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -82,12 +82,9 @@ class TimeScale extends IntervalScale { _approxInterval: number; getLabel(tick: TimeScaleTick): string { - // const stepLvl = this._stepLvl; - const labelFormatType = timeUtil.getUnitFromValue( tick.value, - this.getSetting('useUTC'), - false + this.getSetting('useUTC') ); return formatUtil.formatTime(labelFormatType, tick.value); } @@ -472,7 +469,6 @@ function getIntervalTicks( ticks.sort((a, b) => a.value - b.value); - console.log(ticks); let maxLevel = -Number.MAX_VALUE; for (let i = 0; i < ticks.length; ++i) { maxLevel = Math.max(maxLevel, ticks[i].level); diff --git a/src/util/format.ts b/src/util/format.ts index 52427cc1e3..5b5a980848 100644 --- a/src/util/format.ts +++ b/src/util/format.ts @@ -199,24 +199,13 @@ export function pad(str: string, len: number): string { * @inner */ export function formatTime(tpl: string, value: number | string | Date, isUTC?: boolean) { - if (tpl === 'year') { - tpl = 'MM-dd\nyyyy'; - } - else if (tpl === 'month' || tpl === 'quarter' || tpl === 'half-year' + if (tpl === 'week' + || tpl === 'month' + || tpl === 'quarter' + || tpl === 'half-year' + || tpl === 'year' ) { - tpl = 'M月'; - } - else if (tpl === 'week' || tpl === 'day') { - tpl = 'd'; - } - else if (tpl === 'hour' || tpl === 'minute') { - tpl = 'hh:mm'; - } - else if (tpl === 'second') { - tpl = 'hh:mm:ss'; - } - else if (tpl === 'millisecond') { - tpl = 'hh:mm:ss SSS'; + tpl = 'MM-dd\nyyyy'; } const date = numberUtil.parseDate(value); diff --git a/src/util/time.ts b/src/util/time.ts index fbb96d659a..1c4cc5a2e9 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -7,13 +7,9 @@ import {TimeScaleTick} from './types'; export const defaultLeveledFormatter = { year: '{yyyy}', - 'half-year': '{MMM}', - quarter: '{MMM}', month: '{MMM}', - week: '{d}', day: '{d}', hour: '{HH}:{mm}', - 'half-day': '{HH}:{mm}', minute: '{HH}:{mm}', second: '{HH}:{mm}:{ss}', millisecond: '{hh}:{mm}:{ss} {SSS}', @@ -68,7 +64,7 @@ export function format(time: Date, template: string, isUTC?: boolean): string { const s = (date as any)['get' + utc + 'Seconds'](); const S = (date as any)['get' + utc + 'Milliseconds'](); - return template + return (template || '') .replace('{yyyy}', y) .replace('{yy}', y % 100 + '') .replace('{Q}', q + '') @@ -111,8 +107,8 @@ export function leveledFormat( else { const defaults = zrUtil.extend({}, defaultLeveledFormatter); if (tick.level > 0) { - for (let i = 0; i < timeUnits.length; ++i) { - defaults[timeUnits[i]] = `{primary|${defaults[timeUnits[i]]}}`; + for (let i = 0; i < primaryTimeUnits.length; ++i) { + defaults[primaryTimeUnits[i]] = `{primary|${defaults[primaryTimeUnits[i]]}}`; } } @@ -123,7 +119,7 @@ export function leveledFormat( ) : defaults) as any; - const unit = getUnitFromValue(tick.value, isUTC, true); + const unit = getUnitFromValue(tick.value, isUTC); if (mergedFormatter[unit]) { template = mergedFormatter[unit]; } @@ -148,19 +144,16 @@ export function leveledFormat( } } - // console.log(tick.level, new Date(tick.value), template); return format(new Date(tick.value), template, isUTC); } export function getUnitFromValue ( value: number | string | Date, - isUTC: boolean, - primaryOnly: boolean -): TimeUnit { + isUTC: boolean +): PrimaryTimeUnit { const date = numberUtil.parseDate(value); const utc = isUTC ? 'UTC' : ''; const M = (date as any)['get' + utc + 'Month']() + 1; - const w = (date as any)['get' + utc + 'Day'](); const d = (date as any)['get' + utc + 'Date'](); const h = (date as any)['get' + utc + 'Hours'](); const m = (date as any)['get' + utc + 'Minutes'](); @@ -170,35 +163,19 @@ export function getUnitFromValue ( const isSecond = S === 0; const isMinute = isSecond && s === 0; const isHour = isMinute && m === 0; - const isHalfDay = isHour && (h === 0 || h === 11); const isDay = isHour && h === 0; - const isWeek = isDay && w === 0; // TODO: first day to be configured const isMonth = isDay && d === 1; - const isQuarter = isMonth && (M % 3 === 1); - const isHalfYear = isMonth && (M % 6 === 1); const isYear = isMonth && M === 1; if (isYear) { return 'year'; } - else if (isHalfYear) { - return 'half-year'; - } - else if (isQuarter) { - return 'quarter'; - } else if (isMonth) { return 'month'; } - else if (isWeek && !primaryOnly) { - return 'week'; - } else if (isDay) { return 'day'; } - else if (isHalfDay) { - return 'half-day'; - } else if (isHour) { return 'hour'; } @@ -221,7 +198,7 @@ export function getUnitValue ( const date = typeof value === 'number' ? numberUtil.parseDate(value) as any : value; - unit = unit || getUnitFromValue(value, isUTC, true); + unit = unit || getUnitFromValue(value, isUTC); const utc = isUTC ? 'UTC' : ''; switch (unit) { diff --git a/test/timeScale-formatter.html b/test/timeScale-formatter.html index dd2179feda..806eb8f91c 100644 --- a/test/timeScale-formatter.html +++ b/test/timeScale-formatter.html @@ -55,12 +55,6 @@
-
- - -
- - + + + + + + + + + From f2d34e0d5d1c250e23d0de16d978e278e1548149 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Wed, 29 Jul 2020 09:56:37 +0800 Subject: [PATCH 11/11] feat(time): update lang --- src/lang.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lang.ts b/src/lang.ts index fc384a0990..2c94fef8ae 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -24,12 +24,12 @@ export default { time: { month: [ - 'January', 'February', 'March', 'April', 'May', 'June', - 'July', 'August', 'September', 'October', 'November', 'December' + '一月', '二月', '三月', '四月', '五月', '六月', + '七月', '八月', '九月', '十月', '十一月', '十二月' ], monthAbbr: [ - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' + '一', '二', '三', '四', '五', '六', + '七', '八', '九', '十', '十一', '十二' ], dayOfWeek: [ '星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'