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