From a9a39296669299ed5951526bfe8c6cc2859296e7 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Mon, 4 Jul 2022 15:04:01 +0800 Subject: [PATCH 1/4] fix(axis): add time axis special tick logic --- src/coord/Axis.ts | 1 + src/coord/axisTickLabelBuilder.ts | 112 ++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/src/coord/Axis.ts b/src/coord/Axis.ts index 7a334a0a4b..ebbfbb2219 100644 --- a/src/coord/Axis.ts +++ b/src/coord/Axis.ts @@ -193,6 +193,7 @@ class Axis { this, ticksCoords, alignWithLabel, opt.clamp ); + console.log('ticksCoords', ticksCoords); return ticksCoords; } diff --git a/src/coord/axisTickLabelBuilder.ts b/src/coord/axisTickLabelBuilder.ts index fbc2e5c975..b564425732 100644 --- a/src/coord/axisTickLabelBuilder.ts +++ b/src/coord/axisTickLabelBuilder.ts @@ -31,6 +31,7 @@ import { AxisBaseOption } from './axisCommonTypes'; import OrdinalScale from '../scale/Ordinal'; import { AxisBaseModel } from './AxisBaseModel'; import type Axis2D from './cartesian/Axis2D'; +import { TimeScaleTick } from '../util/types'; type CacheKey = string | number; @@ -72,6 +73,8 @@ export function createAxisLabels(axis: Axis): { // Only ordinal scale support tick interval return axis.type === 'category' ? makeCategoryLabels(axis) + : axis.type === 'time' + ? makeTimeLabels(axis) : makeRealNumberLabels(axis); } @@ -90,6 +93,8 @@ export function createAxisTicks(axis: Axis, tickModel: AxisBaseModel): { // Only ordinal scale support tick interval return axis.type === 'category' ? makeCategoryTicks(axis, tickModel) + : axis.type === 'time' + ? makeTimeTicks(axis, tickModel) : {ticks: zrUtil.map(axis.scale.getTicks(), tick => tick.value) }; } @@ -102,6 +107,15 @@ function makeCategoryLabels(axis: Axis) { : result; } +function makeTimeLabels(axis: Axis) { + const labelModel = axis.getLabelModel(); + const result = makeTimeLabelsActually(axis, labelModel); + + return (!labelModel.get('show') || axis.scale.isBlank()) + ? {labels: []} + : result; +} + function makeCategoryLabelsActually(axis: Axis, labelModel: Model) { const labelsCache = getListCache(axis, 'labels'); const optionLabelInterval = getOptionCategoryInterval(labelModel); @@ -129,6 +143,104 @@ function makeCategoryLabelsActually(axis: Axis, labelModel: Model) { + const labelsCache = getListCache(axis, 'labels'); + const timeKey = 'time'; // TODO: change key name + const result = listCacheGet(labelsCache, timeKey); + + if (result) { + return result; + } + + const labels = makeNonOverlappedTimeLabels(axis); + + // Cache to avoid calling interval function repeatly. + return listCacheSet(labelsCache, timeKey, { + labels: labels + }); +} + +function makeNonOverlappedTimeLabels(axis: Axis): MakeLabelsResultObj[]; +function makeNonOverlappedTimeLabels(axis: Axis, onlyTick: false): MakeLabelsResultObj[]; +function makeNonOverlappedTimeLabels(axis: Axis, onlyTick: true): number[]; +function makeNonOverlappedTimeLabels(axis: Axis, onlyTick?: boolean) { + const ticks = axis.scale.getTicks() as TimeScaleTick[]; + const ordinalScale = axis.scale as OrdinalScale; + const labelFormatter = makeLabelFormatter(axis); + + const result: (MakeLabelsResultObj | number)[] = []; + + function addItem(tickValue: number) { + const tickObj = { value: tickValue }; + result.push(onlyTick + ? tickValue + : { + formattedLabel: labelFormatter(tickObj), + rawLabel: ordinalScale.getLabel(tickObj), // TODO: ? + tickValue: tickValue + } + ); + } + + let lastMaxLevel = Number.MAX_VALUE; + let maxLevel; + while (true) { + maxLevel = -1; + + for (let i = 0; i < ticks.length; i++) { + if (ticks[i].level > maxLevel && ticks[i].level < lastMaxLevel) { + maxLevel = ticks[i].level; + } + } + + if (maxLevel < 0) { + break; + } + + for (let i = 0; i < ticks.length; i++) { + const tick = ticks[i]; + if (tick.level === maxLevel) { + // Add the tick only if it has no overlap with current ones + // TODO: + addItem(tick.value); + } + } + + if (maxLevel <= 0) { + break; + } + lastMaxLevel = maxLevel; + } + return result; +} + +function makeTimeTicks(axis: Axis, tickModel: AxisBaseModel) { + const ticksCache = getListCache(axis, 'ticks'); + const result = listCacheGet(ticksCache, 'time'); + + if (result) { + return result; + } + + let ticks: number[]; + + // Optimize for the case that large category data and no label displayed, + // we should not return all ticks. + if (!tickModel.get('show') || axis.scale.isBlank()) { + ticks = []; + } + + const labelsResult = makeTimeLabelsActually(axis, axis.getLabelModel()); + ticks = zrUtil.map(labelsResult.labels, function (labelItem) { + return labelItem.tickValue; + }); + + // Cache to avoid calling interval function repeatly. + return listCacheSet(ticksCache, 'time', { + ticks: ticks + }); +} + function makeCategoryTicks(axis: Axis, tickModel: AxisBaseModel) { const ticksCache = getListCache(axis, 'ticks'); const optionTickInterval = getOptionCategoryInterval(tickModel); From 0c594936a71351e73eadff90f55d491a772a2613 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Mon, 4 Jul 2022 17:31:17 +0800 Subject: [PATCH 2/4] fix(axis): add time ticks with larger levels first --- src/coord/Axis.ts | 4 +- src/coord/axisTickLabelBuilder.ts | 53 ++++++++++++-- test/timeScale-formatter.html | 115 +++++++++++++++++++++++++++--- 3 files changed, 153 insertions(+), 19 deletions(-) diff --git a/src/coord/Axis.ts b/src/coord/Axis.ts index ebbfbb2219..59eaa3122d 100644 --- a/src/coord/Axis.ts +++ b/src/coord/Axis.ts @@ -193,7 +193,6 @@ class Axis { this, ticksCoords, alignWithLabel, opt.clamp ); - console.log('ticksCoords', ticksCoords); return ticksCoords; } @@ -222,7 +221,8 @@ class Axis { } getViewLabels(): ReturnType['labels'] { - return createAxisLabels(this).labels; + const labels = createAxisLabels(this).labels; + return labels; } getLabelModel(): Model { diff --git a/src/coord/axisTickLabelBuilder.ts b/src/coord/axisTickLabelBuilder.ts index b564425732..9c6be6c30e 100644 --- a/src/coord/axisTickLabelBuilder.ts +++ b/src/coord/axisTickLabelBuilder.ts @@ -32,6 +32,7 @@ import OrdinalScale from '../scale/Ordinal'; import { AxisBaseModel } from './AxisBaseModel'; import type Axis2D from './cartesian/Axis2D'; import { TimeScaleTick } from '../util/types'; +import { BoundingRect } from 'zrender'; type CacheKey = string | number; @@ -167,23 +168,46 @@ function makeNonOverlappedTimeLabels(axis: Axis, onlyTick?: boolean) { const ticks = axis.scale.getTicks() as TimeScaleTick[]; const ordinalScale = axis.scale as OrdinalScale; const labelFormatter = makeLabelFormatter(axis); + const font = axis.getLabelModel().getFont(); const result: (MakeLabelsResultObj | number)[] = []; + const boundingRects: BoundingRect[] = []; + + function isOverlap(rect: BoundingRect) { + /** + * `rotate` is not considered because for time axis, + * the interval is a suggestion value, not a precise value. + * So if there is no overlap without rotate, there should be + * no overlap with rotate and we don't have to make tick labels + * as condense as possible as in the case of category axes. + */ + for (let i = 0; i < boundingRects.length; i++) { + if (rect.intersect(boundingRects[i])) { + return true; + } + } + return false; + } - function addItem(tickValue: number) { - const tickObj = { value: tickValue }; + function addItem(tickValue: number, tickLevel: number) { + const tickObj = { value: tickValue, level: tickLevel }; result.push(onlyTick ? tickValue : { formattedLabel: labelFormatter(tickObj), rawLabel: ordinalScale.getLabel(tickObj), // TODO: ? - tickValue: tickValue + tickValue: tickValue, + level: tickLevel } ); } let lastMaxLevel = Number.MAX_VALUE; let maxLevel; + /** + * Loop through the ticks with larger levels to smaller levels so that if + * the ticks are overlapped, we can use the level of the higher level. + */ while (true) { maxLevel = -1; @@ -200,9 +224,25 @@ function makeNonOverlappedTimeLabels(axis: Axis, onlyTick?: boolean) { for (let i = 0; i < ticks.length; i++) { const tick = ticks[i]; if (tick.level === maxLevel) { - // Add the tick only if it has no overlap with current ones - // TODO: - addItem(tick.value); + // Check if this tick is overlapped with added ticks + const rect = textContain.getBoundingRect( + labelFormatter({ + value: tick.value, + level: tick.level + }), + font, + 'center', + 'top' + ); + // The same magic number as in calculateCategoryInterval + const padding = 0.15; + rect.x += axis.dataToCoord(tick.value) - rect.width * padding; + rect.width *= (1 + padding * 2); + if (!isOverlap(rect)) { + // Add the tick only if it has no overlap with current ones + addItem(tick.value, tick.level); + boundingRects.push(rect); + } } } @@ -443,6 +483,7 @@ interface MakeLabelsResultObj { formattedLabel: string rawLabel: string tickValue: number + level?: number } function makeLabelsByNumericCategoryInterval(axis: Axis, categoryInterval: number): MakeLabelsResultObj[]; diff --git a/test/timeScale-formatter.html b/test/timeScale-formatter.html index 128548bf13..c139eab53d 100644 --- a/test/timeScale-formatter.html +++ b/test/timeScale-formatter.html @@ -38,21 +38,12 @@
- -
- -
- -
- -
- -
+
+ + + + + + + + - From 3034443cfee531898b4af5d67430f2ef0d3f4cf0 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Tue, 22 Nov 2022 16:52:47 +0800 Subject: [PATCH 3/4] fix(time): consider padding when check time axis overlap --- src/coord/axisDefault.ts | 3 -- src/coord/axisTickLabelBuilder.ts | 12 ++++--- src/scale/Time.ts | 52 ------------------------------- 3 files changed, 7 insertions(+), 60 deletions(-) diff --git a/src/coord/axisDefault.ts b/src/coord/axisDefault.ts index 4d2c6674b3..1d82498cd8 100644 --- a/src/coord/axisDefault.ts +++ b/src/coord/axisDefault.ts @@ -173,9 +173,6 @@ const valueAxis: AxisBaseOption = zrUtil.merge({ const timeAxis: AxisBaseOption = zrUtil.merge({ splitNumber: 6, axisLabel: { - // To eliminate labels that are not nice - showMinLabel: false, - showMaxLabel: false, rich: { primary: { fontWeight: 'bold' diff --git a/src/coord/axisTickLabelBuilder.ts b/src/coord/axisTickLabelBuilder.ts index 315403f966..12ad897ec8 100644 --- a/src/coord/axisTickLabelBuilder.ts +++ b/src/coord/axisTickLabelBuilder.ts @@ -32,6 +32,7 @@ import OrdinalScale from '../scale/Ordinal'; import { AxisBaseModel } from './AxisBaseModel'; import type Axis2D from './cartesian/Axis2D'; import { TimeScaleTick } from '../util/types'; +import * as formatUtil from '../util/format'; import { BoundingRect } from 'zrender'; type CacheKey = string | number; @@ -168,7 +169,10 @@ function makeNonOverlappedTimeLabels(axis: Axis, onlyTick?: boolean) { const ticks = axis.scale.getTicks() as TimeScaleTick[]; const ordinalScale = axis.scale as OrdinalScale; const labelFormatter = makeLabelFormatter(axis); - const font = axis.getLabelModel().getFont(); + const labelModel = axis.getLabelModel(); + const font = labelModel.getFont(); + const padding = formatUtil.normalizeCssArray(labelModel.get('padding') || 0); + const paddingH = padding[1] + padding[3]; const result: (MakeLabelsResultObj | number)[] = []; const boundingRects: BoundingRect[] = []; @@ -234,10 +238,8 @@ function makeNonOverlappedTimeLabels(axis: Axis, onlyTick?: boolean) { 'center', 'top' ); - // The same magic number as in calculateCategoryInterval - const padding = 0.15; - rect.x += axis.dataToCoord(tick.value) - rect.width * padding; - rect.width *= (1 + padding * 2); + rect.x += axis.dataToCoord(tick.value) - padding[3]; + rect.width += paddingH; if (!isOverlap(rect)) { // Add the tick only if it has no overlap with current ones addItem(tick.value, tick.level); diff --git a/src/scale/Time.ts b/src/scale/Time.ts index 2458490b2e..bba8b1bb1b 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -316,58 +316,6 @@ function isUnitValueSame( } } -// 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 From eda1b9f988200a9ee223b93f696af18868622868 Mon Sep 17 00:00:00 2001 From: Ovilia Date: Wed, 23 Nov 2022 10:28:28 +0800 Subject: [PATCH 4/4] feat(time): support formatterMinUnit and reduce day approximate interval --- src/coord/axisCommonTypes.ts | 7 +- src/coord/axisHelper.ts | 5 +- src/scale/Time.ts | 28 +++++--- src/util/time.ts | 34 ++++++++-- src/util/types.ts | 2 +- test/timeScale-formatter.html | 124 ++++++++++++++++++++++++++++++---- test/timeScale2.html | 3 +- 7 files changed, 170 insertions(+), 33 deletions(-) diff --git a/src/coord/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts index bb4d1b7bf1..7d53c04a25 100644 --- a/src/coord/axisCommonTypes.ts +++ b/src/coord/axisCommonTypes.ts @@ -22,6 +22,7 @@ import { AreaStyleOption, ComponentOption, ColorString, AnimationOptionMixin, Dictionary, ScaleDataValue, CommonAxisPointerOption } from '../util/types'; +import { PrimaryTimeUnit } from '../util/time'; import { TextStyleProps } from 'zrender/src/graphic/Text'; @@ -159,9 +160,13 @@ export interface LogAxisBaseOption extends NumericAxisBaseOptionCommon { axisLabel?: AxisLabelOption<'log'>; logBase?: number; } + +export interface TimeAxisLabelOption extends AxisLabelOption<'time'> { + formatterMinUnit?: PrimaryTimeUnit +} export interface TimeAxisBaseOption extends NumericAxisBaseOptionCommon { type?: 'time'; - axisLabel?: AxisLabelOption<'time'>; + axisLabel?: TimeAxisLabelOption; } interface AxisNameTextStyleOption extends TextCommonOption { rich?: Dictionary diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 53485fb31e..d6758a4272 100644 --- a/src/coord/axisHelper.ts +++ b/src/coord/axisHelper.ts @@ -38,6 +38,7 @@ import { CategoryAxisBaseOption, LogAxisBaseOption, TimeAxisLabelFormatterOption, + TimeAxisLabelOption, ValueAxisBaseOption } from './axisCommonTypes'; import type CartesianAxisModel from './cartesian/AxisModel'; @@ -201,9 +202,11 @@ export function createScaleByModel(model: AxisBaseModel, axisType?: string): Sca extent: [Infinity, -Infinity] }); case 'time': + const axisLabel = model.getModel('axisLabel') as Model; return new TimeScale({ locale: model.ecModel.getLocaleModel(), - useUTC: model.ecModel.get('useUTC') + useUTC: model.ecModel.get('useUTC'), + formatterMinUnit: axisLabel.get('formatterMinUnit') }); default: // case 'value'/'interval', 'log', or others. diff --git a/src/scale/Time.ts b/src/scale/Time.ts index bba8b1bb1b..ea3c5cd20f 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -78,7 +78,7 @@ 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'; +import { filter, isNumber, map, indexOf } from 'zrender/src/core/util'; // FIXME 公用? const bisect = function ( @@ -102,6 +102,7 @@ const bisect = function ( type TimeScaleSetting = { locale: Model; useUTC: boolean; + formatterMinUnit?: PrimaryTimeUnit; }; class TimeScale extends IntervalScale { @@ -139,7 +140,8 @@ class TimeScale extends IntervalScale { ): string { const isUTC = this.getSetting('useUTC'); const lang = this.getSetting('locale'); - return leveledFormat(tick, idx, labelFormatter, lang, isUTC); + const formatterMinUnit = this.getSetting('formatterMinUnit'); + return leveledFormat(tick, idx, labelFormatter, lang, isUTC, formatterMinUnit); } /** @@ -161,12 +163,14 @@ class TimeScale extends IntervalScale { }); const useUTC = this.getSetting('useUTC'); + const formatterMinUnit = this.getSetting('formatterMinUnit'); const innerTicks = getIntervalTicks( this._minLevelUnit, this._approxInterval, useUTC, - extent + extent, + formatterMinUnit ); ticks = ticks.concat(innerTicks); @@ -318,11 +322,10 @@ function isUnitValueSame( function getDateInterval(approxInterval: number, daysInMonth: number) { approxInterval /= ONE_DAY; + // Don't return an interval too large, that would cause too many ticks. + // Even if they don't overlap, it's not good for readability. 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; + : approxInterval > 1.5 ? 7 : 1; } function getMonthInterval(approxInterval: number) { @@ -378,7 +381,8 @@ function getIntervalTicks( bottomUnitName: TimeUnit, approxInterval: number, isUTC: boolean, - extent: number[] + extent: number[], + formatterMinUnit?: PrimaryTimeUnit ): TimeScaleTick[] { const safeLimit = 10000; const unitNames = timeUnits; @@ -523,9 +527,17 @@ function getIntervalTicks( const levelsTicks: InnerTimeTick[][] = []; let currentLevelTicks: InnerTimeTick[] = []; + const minUnitId = formatterMinUnit + ? indexOf(unitNames, formatterMinUnit) + : -1; + let tickCount = 0; let lastLevelTickCount = 0; for (let i = 0; i < unitNames.length && iter++ < safeLimit; ++i) { + if (minUnitId >= 0 && i > minUnitId) { + break; + } + const primaryTimeUnit = getPrimaryTimeUnit(unitNames[i]); if (!isPrimaryTimeUnit(unitNames[i])) { // TODO continue; diff --git a/src/util/time.ts b/src/util/time.ts index 9a71f44cfc..2fb95c17e4 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -161,9 +161,11 @@ export function leveledFormat( idx: number, formatter: TimeAxisLabelFormatterOption, lang: string | Model, - isUTC: boolean + isUTC: boolean, + formatterMinUnit: PrimaryTimeUnit ) { let template = null; + const level = tick.level; if (zrUtil.isString(formatter)) { // Single formatter for all units at all levels template = formatter; @@ -171,12 +173,14 @@ export function leveledFormat( else if (zrUtil.isFunction(formatter)) { // Callback formatter template = formatter(tick.value, idx, { - level: tick.level + level }); } else { const defaults = zrUtil.extend({}, defaultLeveledFormatter); - if (tick.level > 0) { + if (level > 0) { + // When there are multiple levels and this is the more significant + // one, emphasis this level for (let i = 0; i < primaryTimeUnits.length; ++i) { defaults[primaryTimeUnits[i]] = `{primary|${defaults[primaryTimeUnits[i]]}}`; } @@ -189,13 +193,29 @@ export function leveledFormat( ) : defaults) as any; - const unit = getUnitFromValue(tick.value, isUTC); + let unit = getUnitFromValue(tick.value, isUTC); + if (formatterMinUnit) { + // When formatterMinUnit is defined and larger than unit, + // use formatterMinUnit instead + // For example, when formatterMinUnit is 'day', and unit is 'hour', + // use 'day' instead of 'hour' + const formatterMinUnitIdx = zrUtil.indexOf(primaryTimeUnits, formatterMinUnit); + if (formatterMinUnitIdx >= 0) { + unit = primaryTimeUnits[ + Math.min( + zrUtil.indexOf(primaryTimeUnits, unit), + formatterMinUnitIdx + ) + ]; + } + } + 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); + const targetId = zrUtil.indexOf(timeUnits, unit); for (let i = targetId - 1; i >= 0; --i) { if (mergedFormatter[unit]) { template = mergedFormatter[unit]; @@ -206,9 +226,9 @@ export function leveledFormat( } if (zrUtil.isArray(template)) { - let levelId = tick.level == null + let levelId = level == null ? 0 - : (tick.level >= 0 ? tick.level : template.length + tick.level); + : (level >= 0 ? level : template.length + level); levelId = Math.min(levelId, template.length - 1); template = template[levelId]; } diff --git a/src/util/types.ts b/src/util/types.ts index 56c7b6983b..9c387f348b 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -395,7 +395,7 @@ export interface TimeScaleTick extends ScaleTick { * 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 + * `level` is set to be larger when it's more significant, like month * labels in the above case. */ level?: number diff --git a/test/timeScale-formatter.html b/test/timeScale-formatter.html index c139eab53d..3d9d67f4ef 100644 --- a/test/timeScale-formatter.html +++ b/test/timeScale-formatter.html @@ -40,10 +40,13 @@
+
+
+
+ + + + + + +