|  | /* | 
|  | * Licensed to the Apache Software Foundation (ASF) under one | 
|  | * or more contributor license agreements.  See the NOTICE file | 
|  | * distributed with this work for additional information | 
|  | * regarding copyright ownership.  The ASF licenses this file | 
|  | * to you under the Apache License, Version 2.0 (the | 
|  | * "License"); you may not use this file except in compliance | 
|  | * with the License.  You may obtain a copy of the License at | 
|  | * | 
|  | *   http://www.apache.org/licenses/LICENSE-2.0 | 
|  | * | 
|  | * Unless required by applicable law or agreed to in writing, | 
|  | * software distributed under the License is distributed on an | 
|  | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | 
|  | * KIND, either express or implied.  See the License for the | 
|  | * specific language governing permissions and limitations | 
|  | * under the License. | 
|  | */ | 
|  |  | 
|  | /* | 
|  | * A third-party license is embedded for some of the code in this file: | 
|  | * The "scaleLevels" was originally copied from "d3.js" with some | 
|  | * modifications made for this project. | 
|  | * (See more details in the comment on the definition of "scaleLevels" below.) | 
|  | * The use of the source code of this file is also subject to the terms | 
|  | * and consitions of the license of "d3.js" (BSD-3Clause, see | 
|  | * </licenses/LICENSE-d3>). | 
|  | */ | 
|  |  | 
|  |  | 
|  | // [About UTC and local time zone]: | 
|  | // In most cases, `number.parseDate` will treat input data string as local time | 
|  | // (except time zone is specified in time string). And `format.formateTime` returns | 
|  | // local time by default. option.useUTC is false by default. This design has | 
|  | // considered these common cases: | 
|  | // (1) Time that is persistent in server is in UTC, but it is needed to be displayed | 
|  | // in local time by default. | 
|  | // (2) By default, the input data string (e.g., '2011-01-02') should be displayed | 
|  | // as its original time, without any time difference. | 
|  |  | 
|  | import * as numberUtil from '../util/number'; | 
|  | import { | 
|  | ONE_SECOND, | 
|  | ONE_MINUTE, | 
|  | ONE_HOUR, | 
|  | ONE_DAY, | 
|  | ONE_YEAR, | 
|  | format, | 
|  | leveledFormat, | 
|  | PrimaryTimeUnit, | 
|  | TimeUnit, | 
|  | getUnitValue, | 
|  | timeUnits, | 
|  | fullLeveledFormatter, | 
|  | getPrimaryTimeUnit, | 
|  | isPrimaryTimeUnit, | 
|  | getDefaultFormatPrecisionOfInterval, | 
|  | fullYearGetterName, | 
|  | monthSetterName, | 
|  | fullYearSetterName, | 
|  | dateSetterName, | 
|  | hoursGetterName, | 
|  | hoursSetterName, | 
|  | minutesSetterName, | 
|  | secondsSetterName, | 
|  | millisecondsSetterName, | 
|  | monthGetterName, | 
|  | dateGetterName, | 
|  | minutesGetterName, | 
|  | secondsGetterName, | 
|  | millisecondsGetterName | 
|  | } from '../util/time'; | 
|  | import * as scaleHelper from './helper'; | 
|  | import IntervalScale from './Interval'; | 
|  | import Scale from './Scale'; | 
|  | import {TimeScaleTick, ScaleTick} from '../util/types'; | 
|  | import {TimeAxisLabelFormatterOption} from '../coord/axisCommonTypes'; | 
|  | import { warn } from '../util/log'; | 
|  | import { LocaleOption } from '../core/locale'; | 
|  | import Model from '../model/Model'; | 
|  | import { filter, isNumber, map } from 'zrender/src/core/util'; | 
|  |  | 
|  | // FIXME 公用? | 
|  | const bisect = function ( | 
|  | a: [string | number, number][], | 
|  | x: number, | 
|  | lo: number, | 
|  | hi: number | 
|  | ): number { | 
|  | while (lo < hi) { | 
|  | const mid = lo + hi >>> 1; | 
|  | if (a[mid][1] < x) { | 
|  | lo = mid + 1; | 
|  | } | 
|  | else { | 
|  | hi = mid; | 
|  | } | 
|  | } | 
|  | return lo; | 
|  | }; | 
|  |  | 
|  | type TimeScaleSetting = { | 
|  | locale: Model<LocaleOption>; | 
|  | useUTC: boolean; | 
|  | }; | 
|  |  | 
|  | class TimeScale extends IntervalScale<TimeScaleSetting> { | 
|  |  | 
|  | static type = 'time'; | 
|  | readonly type = 'time'; | 
|  |  | 
|  | _approxInterval: number; | 
|  |  | 
|  | _minLevelUnit: TimeUnit; | 
|  |  | 
|  | constructor(settings?: TimeScaleSetting) { | 
|  | super(settings); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Get label is mainly for other components like dataZoom, tooltip. | 
|  | */ | 
|  | getLabel(tick: TimeScaleTick): string { | 
|  | const useUTC = this.getSetting('useUTC'); | 
|  | return format( | 
|  | tick.value, | 
|  | fullLeveledFormatter[ | 
|  | getDefaultFormatPrecisionOfInterval(getPrimaryTimeUnit(this._minLevelUnit)) | 
|  | ] || fullLeveledFormatter.second, | 
|  | useUTC, | 
|  | this.getSetting('locale') | 
|  | ); | 
|  | } | 
|  |  | 
|  | getFormattedLabel( | 
|  | tick: TimeScaleTick, | 
|  | idx: number, | 
|  | labelFormatter: TimeAxisLabelFormatterOption | 
|  | ): string { | 
|  | const isUTC = this.getSetting('useUTC'); | 
|  | const lang = this.getSetting('locale'); | 
|  | return leveledFormat(tick, idx, labelFormatter, lang, isUTC); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @override | 
|  | */ | 
|  | getTicks(): TimeScaleTick[] { | 
|  | const interval = this._interval; | 
|  | const extent = this._extent; | 
|  |  | 
|  | let ticks = [] as TimeScaleTick[]; | 
|  | // If interval is 0, return []; | 
|  | if (!interval) { | 
|  | return ticks; | 
|  | } | 
|  |  | 
|  | ticks.push({ | 
|  | value: extent[0], | 
|  | level: 0 | 
|  | }); | 
|  |  | 
|  | const useUTC = this.getSetting('useUTC'); | 
|  |  | 
|  | const innerTicks = getIntervalTicks( | 
|  | this._minLevelUnit, | 
|  | this._approxInterval, | 
|  | useUTC, | 
|  | extent | 
|  | ); | 
|  |  | 
|  | ticks = ticks.concat(innerTicks); | 
|  |  | 
|  | ticks.push({ | 
|  | value: extent[1], | 
|  | level: 0 | 
|  | }); | 
|  |  | 
|  | return ticks; | 
|  | } | 
|  |  | 
|  | calcNiceExtent( | 
|  | opt?: { | 
|  | splitNumber?: number, | 
|  | fixMin?: boolean, | 
|  | fixMax?: boolean, | 
|  | minInterval?: number, | 
|  | maxInterval?: number | 
|  | } | 
|  | ): void { | 
|  | const extent = this._extent; | 
|  | // If extent start and end are same, expand them | 
|  | if (extent[0] === extent[1]) { | 
|  | // Expand extent | 
|  | extent[0] -= ONE_DAY; | 
|  | extent[1] += ONE_DAY; | 
|  | } | 
|  | // If there are no data and extent are [Infinity, -Infinity] | 
|  | if (extent[1] === -Infinity && extent[0] === Infinity) { | 
|  | const d = new Date(); | 
|  | extent[1] = +new Date(d.getFullYear(), d.getMonth(), d.getDate()); | 
|  | extent[0] = extent[1] - ONE_DAY; | 
|  | } | 
|  |  | 
|  | this.calcNiceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval); | 
|  | } | 
|  |  | 
|  | calcNiceTicks(approxTickNum: number, minInterval: number, maxInterval: number): void { | 
|  | approxTickNum = approxTickNum || 10; | 
|  |  | 
|  | const extent = this._extent; | 
|  | const span = extent[1] - extent[0]; | 
|  | this._approxInterval = span / approxTickNum; | 
|  |  | 
|  | if (minInterval != null && this._approxInterval < minInterval) { | 
|  | this._approxInterval = minInterval; | 
|  | } | 
|  | if (maxInterval != null && this._approxInterval > maxInterval) { | 
|  | this._approxInterval = maxInterval; | 
|  | } | 
|  |  | 
|  | const scaleIntervalsLen = scaleIntervals.length; | 
|  | const idx = Math.min( | 
|  | bisect(scaleIntervals, this._approxInterval, 0, scaleIntervalsLen), | 
|  | scaleIntervalsLen - 1 | 
|  | ); | 
|  |  | 
|  | // Interval that can be used to calculate ticks | 
|  | this._interval = scaleIntervals[idx][1]; | 
|  | // Min level used when picking ticks from top down. | 
|  | // We check one more level to avoid the ticks are to sparse in some case. | 
|  | this._minLevelUnit = scaleIntervals[Math.max(idx - 1, 0)][0]; | 
|  | } | 
|  |  | 
|  | parse(val: number | string | Date): number { | 
|  | // val might be float. | 
|  | return isNumber(val) ? val : +numberUtil.parseDate(val); | 
|  | } | 
|  |  | 
|  | contain(val: number): boolean { | 
|  | return scaleHelper.contain(this.parse(val), this._extent); | 
|  | } | 
|  |  | 
|  | normalize(val: number): number { | 
|  | return scaleHelper.normalize(this.parse(val), this._extent); | 
|  | } | 
|  |  | 
|  | scale(val: number): number { | 
|  | return scaleHelper.scale(val, this._extent); | 
|  | } | 
|  |  | 
|  | } | 
|  |  | 
|  |  | 
|  | /** | 
|  | * This implementation was originally copied from "d3.js" | 
|  | * <https://github.com/d3/d3/blob/b516d77fb8566b576088e73410437494717ada26/src/time/scale.js> | 
|  | * with some modifications made for this program. | 
|  | * See the license statement at the head of this file. | 
|  | */ | 
|  | const scaleIntervals: [TimeUnit, number][] = [ | 
|  | // Format                           interval | 
|  | ['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 | 
|  | ['half-year', ONE_YEAR / 2],        // 6M | 
|  | ['year', ONE_YEAR]                  // 1Y | 
|  | ]; | 
|  |  | 
|  | 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 isSame = (unit: PrimaryTimeUnit) => { | 
|  | return getUnitValue(dateA, unit, isUTC) | 
|  | === 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'); | 
|  | const isSameMilliSecond = () => isSameSecond() && isSame('millisecond'); | 
|  |  | 
|  | switch (unit) { | 
|  | case 'year': | 
|  | return isSameYear(); | 
|  | case 'month': | 
|  | return isSameMonth(); | 
|  | case 'day': | 
|  | return isSameDay(); | 
|  | case 'hour': | 
|  | return isSameHour(); | 
|  | case 'minute': | 
|  | return isSameMinute(); | 
|  | case 'second': | 
|  | return isSameSecond(); | 
|  | case 'millisecond': | 
|  | return isSameMilliSecond(); | 
|  | } | 
|  | } | 
|  |  | 
|  | // const primaryUnitGetters = { | 
|  | //     year: fullYearGetterName(), | 
|  | //     month: monthGetterName(), | 
|  | //     day: dateGetterName(), | 
|  | //     hour: hoursGetterName(), | 
|  | //     minute: minutesGetterName(), | 
|  | //     second: secondsGetterName(), | 
|  | //     millisecond: millisecondsGetterName() | 
|  | // }; | 
|  |  | 
|  | // const primaryUnitUTCGetters = { | 
|  | //     year: fullYearGetterName(true), | 
|  | //     month: monthGetterName(true), | 
|  | //     day: dateGetterName(true), | 
|  | //     hour: hoursGetterName(true), | 
|  | //     minute: minutesGetterName(true), | 
|  | //     second: secondsGetterName(true), | 
|  | //     millisecond: millisecondsGetterName(true) | 
|  | // }; | 
|  |  | 
|  | // function moveTick(date: Date, unitName: TimeUnit, step: number, isUTC: boolean) { | 
|  | //     step = step || 1; | 
|  | //     switch (getPrimaryTimeUnit(unitName)) { | 
|  | //         case 'year': | 
|  | //             date[fullYearSetterName(isUTC)](date[fullYearGetterName(isUTC)]() + step); | 
|  | //             break; | 
|  | //         case 'month': | 
|  | //             date[monthSetterName(isUTC)](date[monthGetterName(isUTC)]() + step); | 
|  | //             break; | 
|  | //         case 'day': | 
|  | //             date[dateSetterName(isUTC)](date[dateGetterName(isUTC)]() + step); | 
|  | //             break; | 
|  | //         case 'hour': | 
|  | //             date[hoursSetterName(isUTC)](date[hoursGetterName(isUTC)]() + step); | 
|  | //             break; | 
|  | //         case 'minute': | 
|  | //             date[minutesSetterName(isUTC)](date[minutesGetterName(isUTC)]() + step); | 
|  | //             break; | 
|  | //         case 'second': | 
|  | //             date[secondsSetterName(isUTC)](date[secondsGetterName(isUTC)]() + step); | 
|  | //             break; | 
|  | //         case 'millisecond': | 
|  | //             date[millisecondsSetterName(isUTC)](date[millisecondsGetterName(isUTC)]() + step); | 
|  | //             break; | 
|  | //     } | 
|  | //     return date.getTime(); | 
|  | // } | 
|  |  | 
|  | // const DATE_INTERVALS = [[8, 7.5], [4, 3.5], [2, 1.5]]; | 
|  | // const MONTH_INTERVALS = [[6, 5.5], [3, 2.5], [2, 1.5]]; | 
|  | // const MINUTES_SECONDS_INTERVALS = [[30, 30], [20, 20], [15, 15], [10, 10], [5, 5], [2, 2]]; | 
|  |  | 
|  | function getDateInterval(approxInterval: number, daysInMonth: number) { | 
|  | approxInterval /= ONE_DAY; | 
|  | return approxInterval > 16 ? 16 | 
|  | // Math.floor(daysInMonth / 2) + 1  // In this case we only want one tick between two months. | 
|  | : approxInterval > 7.5 ? 7  // TODO week 7 or day 8? | 
|  | : approxInterval > 3.5 ? 4 | 
|  | : approxInterval > 1.5 ? 2 : 1; | 
|  | } | 
|  |  | 
|  | function getMonthInterval(approxInterval: number) { | 
|  | const APPROX_ONE_MONTH = 30 * ONE_DAY; | 
|  | approxInterval /= APPROX_ONE_MONTH; | 
|  | return approxInterval > 6 ? 6 | 
|  | : approxInterval > 3 ? 3 | 
|  | : approxInterval > 2 ? 2 : 1; | 
|  | } | 
|  |  | 
|  | function getHourInterval(approxInterval: number) { | 
|  | approxInterval /= ONE_HOUR; | 
|  | return approxInterval > 12 ? 12 | 
|  | : approxInterval > 6 ? 6 | 
|  | : approxInterval > 3.5 ? 4 | 
|  | : approxInterval > 2 ? 2 : 1; | 
|  | } | 
|  |  | 
|  | function getMinutesAndSecondsInterval(approxInterval: number, isMinutes?: boolean) { | 
|  | approxInterval /= isMinutes ? ONE_MINUTE : ONE_SECOND; | 
|  | return approxInterval > 30 ? 30 | 
|  | : approxInterval > 20 ? 20 | 
|  | : approxInterval > 15 ? 15 | 
|  | : approxInterval > 10 ? 10 | 
|  | : approxInterval > 5 ? 5 | 
|  | : approxInterval > 2 ? 2 : 1; | 
|  | } | 
|  |  | 
|  | function getMillisecondsInterval(approxInterval: number) { | 
|  | return numberUtil.nice(approxInterval, true); | 
|  | } | 
|  |  | 
|  | function getFirstTimestampOfUnit(date: Date, unitName: TimeUnit, isUTC: boolean) { | 
|  | const outDate = new Date(date); | 
|  | switch (getPrimaryTimeUnit(unitName)) { | 
|  | case 'year': | 
|  | case 'month': | 
|  | outDate[monthSetterName(isUTC)](0); | 
|  | case 'day': | 
|  | outDate[dateSetterName(isUTC)](1); | 
|  | case 'hour': | 
|  | outDate[hoursSetterName(isUTC)](0); | 
|  | case 'minute': | 
|  | outDate[minutesSetterName(isUTC)](0); | 
|  | case 'second': | 
|  | outDate[secondsSetterName(isUTC)](0); | 
|  | outDate[millisecondsSetterName(isUTC)](0); | 
|  | } | 
|  | return outDate.getTime(); | 
|  | } | 
|  |  | 
|  | function getIntervalTicks( | 
|  | bottomUnitName: TimeUnit, | 
|  | approxInterval: number, | 
|  | isUTC: boolean, | 
|  | extent: number[] | 
|  | ): TimeScaleTick[] { | 
|  | const safeLimit = 10000; | 
|  | const unitNames = timeUnits; | 
|  | // const bottomPrimaryUnitName = getPrimaryTimeUnit(bottomUnitName); | 
|  |  | 
|  | interface InnerTimeTick extends TimeScaleTick { | 
|  | notAdd?: boolean | 
|  | } | 
|  |  | 
|  | let iter = 0; | 
|  |  | 
|  | function addTicksInSpan( | 
|  | interval: number, | 
|  | minTimestamp: number, maxTimestamp: number, | 
|  | getMethodName: string, | 
|  | setMethodName: string, | 
|  | isDate: boolean, | 
|  | out: InnerTimeTick[] | 
|  | ) { | 
|  | const date = new Date(minTimestamp) as any; | 
|  | let dateTime = minTimestamp; | 
|  | let d = date[getMethodName](); | 
|  |  | 
|  | // if (isDate) { | 
|  | //     d -= 1; // Starts with 0;   PENDING | 
|  | // } | 
|  |  | 
|  | while (dateTime < maxTimestamp && dateTime <= extent[1]) { | 
|  | out.push({ | 
|  | value: dateTime | 
|  | }); | 
|  |  | 
|  | d += interval; | 
|  | date[setMethodName](d); | 
|  | dateTime = date.getTime(); | 
|  | } | 
|  |  | 
|  | // This extra tick is for calcuating ticks of next level. Will not been added to the final result | 
|  | out.push({ | 
|  | value: dateTime, | 
|  | notAdd: true | 
|  | }); | 
|  | } | 
|  |  | 
|  | function addLevelTicks( | 
|  | unitName: TimeUnit, | 
|  | lastLevelTicks: InnerTimeTick[], | 
|  | levelTicks: InnerTimeTick[] | 
|  | ) { | 
|  | const newAddedTicks: ScaleTick[] = []; | 
|  | const isFirstLevel = !lastLevelTicks.length; | 
|  |  | 
|  | if (isUnitValueSame(getPrimaryTimeUnit(unitName), extent[0], extent[1], isUTC)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (isFirstLevel) { | 
|  | lastLevelTicks = [{ | 
|  | // TODO Optimize. Not include so may ticks. | 
|  | value: getFirstTimestampOfUnit(new Date(extent[0]), unitName, isUTC) | 
|  | }, { | 
|  | value: extent[1] | 
|  | }]; | 
|  | } | 
|  |  | 
|  | for (let i = 0; i < lastLevelTicks.length - 1; i++) { | 
|  | const startTick = lastLevelTicks[i].value; | 
|  | const endTick = lastLevelTicks[i + 1].value; | 
|  | if (startTick === endTick) { | 
|  | continue; | 
|  | } | 
|  |  | 
|  | let interval: number; | 
|  | let getterName; | 
|  | let setterName; | 
|  | let isDate = false; | 
|  |  | 
|  | switch (unitName) { | 
|  | case 'year': | 
|  | interval = Math.max(1, Math.round(approxInterval / ONE_DAY / 365)); | 
|  | getterName = fullYearGetterName(isUTC); | 
|  | setterName = fullYearSetterName(isUTC); | 
|  | break; | 
|  | case 'half-year': | 
|  | case 'quarter': | 
|  | case 'month': | 
|  | interval = getMonthInterval(approxInterval); | 
|  | getterName = monthGetterName(isUTC); | 
|  | setterName = monthSetterName(isUTC); | 
|  | break; | 
|  | case 'week':    // PENDING If week is added. Ignore day. | 
|  | case 'half-week': | 
|  | case 'day': | 
|  | interval = getDateInterval(approxInterval, 31); // Use 32 days and let interval been 16 | 
|  | getterName = dateGetterName(isUTC); | 
|  | setterName = dateSetterName(isUTC); | 
|  | isDate = true; | 
|  | break; | 
|  | case 'half-day': | 
|  | case 'quarter-day': | 
|  | case 'hour': | 
|  | interval = getHourInterval(approxInterval); | 
|  | getterName = hoursGetterName(isUTC); | 
|  | setterName = hoursSetterName(isUTC); | 
|  | break; | 
|  | case 'minute': | 
|  | interval = getMinutesAndSecondsInterval(approxInterval, true); | 
|  | getterName = minutesGetterName(isUTC); | 
|  | setterName = minutesSetterName(isUTC); | 
|  | break; | 
|  | case 'second': | 
|  | interval = getMinutesAndSecondsInterval(approxInterval, false); | 
|  | getterName = secondsGetterName(isUTC); | 
|  | setterName = secondsSetterName(isUTC); | 
|  | break; | 
|  | case 'millisecond': | 
|  | interval = getMillisecondsInterval(approxInterval); | 
|  | getterName = millisecondsGetterName(isUTC); | 
|  | setterName = millisecondsSetterName(isUTC); | 
|  | break; | 
|  | } | 
|  |  | 
|  | addTicksInSpan( | 
|  | interval, startTick, endTick, getterName, setterName, isDate, newAddedTicks | 
|  | ); | 
|  |  | 
|  | if (unitName === 'year' && levelTicks.length > 1 && i === 0) { | 
|  | // Add nearest years to the left extent. | 
|  | levelTicks.unshift({ | 
|  | value: levelTicks[0].value - interval | 
|  | }); | 
|  | } | 
|  | } | 
|  |  | 
|  | for (let i = 0; i < newAddedTicks.length; i++) { | 
|  | levelTicks.push(newAddedTicks[i]); | 
|  | } | 
|  | // newAddedTicks.length && console.log(unitName, newAddedTicks); | 
|  | return newAddedTicks; | 
|  | } | 
|  |  | 
|  | const levelsTicks: InnerTimeTick[][] = []; | 
|  | let currentLevelTicks: InnerTimeTick[] = []; | 
|  |  | 
|  | let tickCount = 0; | 
|  | let lastLevelTickCount = 0; | 
|  | for (let i = 0; i < unitNames.length && iter++ < safeLimit; ++i) { | 
|  | const primaryTimeUnit = getPrimaryTimeUnit(unitNames[i]); | 
|  | if (!isPrimaryTimeUnit(unitNames[i])) { // TODO | 
|  | continue; | 
|  | } | 
|  | addLevelTicks(unitNames[i], levelsTicks[levelsTicks.length - 1] || [], currentLevelTicks); | 
|  |  | 
|  | const nextPrimaryTimeUnit: PrimaryTimeUnit = unitNames[i + 1] ? getPrimaryTimeUnit(unitNames[i + 1]) : null; | 
|  | if (primaryTimeUnit !== nextPrimaryTimeUnit) { | 
|  | if (currentLevelTicks.length) { | 
|  | lastLevelTickCount = tickCount; | 
|  | // Remove the duplicate so the tick count can be precisely. | 
|  | currentLevelTicks.sort((a, b) => a.value - b.value); | 
|  | const levelTicksRemoveDuplicated = []; | 
|  | for (let i = 0; i < currentLevelTicks.length; ++i) { | 
|  | const tickValue = currentLevelTicks[i].value; | 
|  | if (i === 0 || currentLevelTicks[i - 1].value !== tickValue) { | 
|  | levelTicksRemoveDuplicated.push(currentLevelTicks[i]); | 
|  | if (tickValue >= extent[0] && tickValue <= extent[1]) { | 
|  | tickCount++; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | const targetTickNum = (extent[1] - extent[0]) / approxInterval; | 
|  | // Added too much in this level and not too less in last level | 
|  | if (tickCount > targetTickNum * 1.5 && lastLevelTickCount > targetTickNum / 1.5) { | 
|  | break; | 
|  | } | 
|  |  | 
|  | // Only treat primary time unit as one level. | 
|  | levelsTicks.push(levelTicksRemoveDuplicated); | 
|  |  | 
|  | if (tickCount > targetTickNum || bottomUnitName === unitNames[i]) { | 
|  | break; | 
|  | } | 
|  |  | 
|  | } | 
|  | // Reset if next unitName is primary | 
|  | currentLevelTicks = []; | 
|  | } | 
|  |  | 
|  | } | 
|  |  | 
|  | if (__DEV__) { | 
|  | if (iter >= safeLimit) { | 
|  | warn('Exceed safe limit.'); | 
|  | } | 
|  | } | 
|  |  | 
|  | const levelsTicksInExtent = filter(map(levelsTicks, levelTicks => { | 
|  | return filter(levelTicks, tick => tick.value >= extent[0] && tick.value <= extent[1] && !tick.notAdd); | 
|  | }), levelTicks => levelTicks.length > 0); | 
|  |  | 
|  | const ticks: TimeScaleTick[] = []; | 
|  | const maxLevel = levelsTicksInExtent.length - 1; | 
|  | for (let i = 0; i < levelsTicksInExtent.length; ++i) { | 
|  | const levelTicks = levelsTicksInExtent[i]; | 
|  | for (let k = 0; k < levelTicks.length; ++k) { | 
|  | ticks.push({ | 
|  | value: levelTicks[k].value, | 
|  | level: maxLevel - i | 
|  | }); | 
|  | } | 
|  | } | 
|  |  | 
|  | ticks.sort((a, b) => a.value - b.value); | 
|  | // Remove duplicates | 
|  | const result: TimeScaleTick[] = []; | 
|  | for (let i = 0; i < ticks.length; ++i) { | 
|  | if (i === 0 || ticks[i].value !== ticks[i - 1].value) { | 
|  | result.push(ticks[i]); | 
|  | } | 
|  | } | 
|  |  | 
|  | return result; | 
|  | } | 
|  |  | 
|  |  | 
|  | Scale.registerClass(TimeScale); | 
|  |  | 
|  | export default TimeScale; |