You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@echarts.apache.org by ov...@apache.org on 2020/06/28 07:09:16 UTC
[incubator-echarts] 01/01: feat(time): improve time axis formatter
This is an automated email from the ASF dual-hosted git repository.
ovilia pushed a commit to branch time-formatter
in repository https://gitbox.apache.org/repos/asf/incubator-echarts.git
commit e44445dac2f0efee032d674ee0648d14d31a397e
Author: Ovilia <zw...@gmail.com>
AuthorDate: Wed Jun 24 17:42:41 2020 +0800
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 3777855..770b30a 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 f2f5734..987a9dd 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 8a5b152..953f644 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' : '';
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@echarts.apache.org
For additional commands, e-mail: commits-help@echarts.apache.org