diff --git a/src/component/axisPointer/viewHelper.ts b/src/component/axisPointer/viewHelper.ts index d379c1a027..611eac2fa8 100644 --- a/src/component/axisPointer/viewHelper.ts +++ b/src/component/axisPointer/viewHelper.ts @@ -163,9 +163,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 } ); @@ -173,7 +175,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/component/dataZoom/SliderZoomView.ts b/src/component/dataZoom/SliderZoomView.ts index 8b49c8b331..6bcf441ab7 100644 --- a/src/component/dataZoom/SliderZoomView.ts +++ b/src/component/dataZoom/SliderZoomView.ts @@ -839,7 +839,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 b2dbf7832b..8ebe1c1442 100644 --- a/src/component/timeline/SliderTimelineView.ts +++ b/src/component/timeline/SliderTimelineView.ts @@ -32,7 +32,7 @@ import ExtensionAPI from '../../ExtensionAPI'; import { merge, each, extend, clone, isString, bind, defaults, retrieve2 } 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'; @@ -129,7 +129,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( @@ -344,7 +344,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}; }); }; @@ -420,16 +420,16 @@ class SliderTimelineView extends TimelineView { this._tickSymbols = []; // The value is dataIndex, see the costomized scale. - each(ticks, (value) => { - const tickCoord = axis.dataToCoord(value); - const itemModel = data.getItemModel(value); + each(ticks, (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 progressStyleModel = itemModel.getModel(['progress', '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); el.ensureState('emphasis').style = hoverStyleModel.getItemStyle(); @@ -439,7 +439,7 @@ class SliderTimelineView extends TimelineView { const ecData = getECData(el); if (itemModel.get('tooltip')) { - ecData.dataIndex = value; + ecData.dataIndex = tick.value; ecData.dataModel = timelineModel; } else { 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/axisCommonTypes.ts b/src/coord/axisCommonTypes.ts index fb00c4b72b..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'; @@ -153,6 +153,24 @@ interface AxisTickOption { interval?: 'auto' | number | ((index: number, value: string) => boolean) } +export type AxisLabelFormatterOption = string | ((value: OrdinalRawValue | number, index: number) => string); + +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, // Whether axisLabel is inside the grid or outside the grid. @@ -164,7 +182,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/axisDefault.ts b/src/coord/axisDefault.ts index 3777855754..ab3f0b098a 100644 --- a/src/coord/axisDefault.ts +++ b/src/coord/axisDefault.ts @@ -161,10 +161,22 @@ 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, + rich: { + primary: { + color: '#000', + fontWeight: 'bold' + } + } + }, + splitLine: { + show: false + } }, valueAxis); const logAxis: AxisBaseOption = zrUtil.defaults({ diff --git a/src/coord/axisHelper.ts b/src/coord/axisHelper.ts index 2caa4bfa0b..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 } from '../util/types'; +import { Dictionary, DimensionName, ScaleTick, TimeScaleTick } from '../util/types'; import { ensureScaleRawExtentInfo } from './scaleRawExtentInfo'; @@ -219,23 +219,32 @@ 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') { + if (axis.scale.type === 'time') { return (function (tpl) { - return function (val: number | string) { + 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, // 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); + const text = tpl.replace('{value}', label != null ? label : ''); + + return text; }; })(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 +252,27 @@ 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) as (TimeScaleTick & string) | (TimeScaleTick & number), + 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 +287,7 @@ export function estimateLabelUnionRect(axis: Axis) { return; } - let realNumberScaleTicks; + let realNumberScaleTicks: ScaleTick[]; let tickCount; const categoryScaleExtent = scale.getExtent(); @@ -298,8 +310,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/export.ts b/src/export.ts index 28cfa53590..36ed1a0bdb 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/lang.ts b/src/lang.ts index b369139b8d..2c94fef8ae 100644 --- a/src/lang.ts +++ b/src/lang.ts @@ -22,6 +22,22 @@ */ export default { + time: { + month: [ + '一月', '二月', '三月', '四月', '五月', '六月', + '七月', '八月', '九月', '十月', '十一月', '十二月' + ], + monthAbbr: [ + '一', '二', '三', '四', '五', '六', + '七', '八', '九', '十', '十一', '十二' + ], + 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/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 f2f5734656..6f43e58341 100644 --- a/src/scale/Time.ts +++ b/src/scale/Time.ts @@ -40,17 +40,19 @@ 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'; +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 ( @@ -77,14 +79,65 @@ class TimeScale extends IntervalScale { static type = 'time'; readonly type = 'time'; - private _stepLvl: [string, number]; + _approxInterval: number; - getLabel(val: number): string { - const stepLvl = this._stepLvl; + getLabel(tick: TimeScaleTick): string { + const labelFormatType = timeUtil.getUnitFromValue( + tick.value, + this.getSetting('useUTC') + ); + return formatUtil.formatTime(labelFormatType, tick.value); + } + + getFormattedLabel( + tick: TimeScaleTick, + idx: number, + labelFormatter: TimeAxisLabelFormatterOption + ): string { + const isUTC = this.getSetting('useUTC'); + return timeUtil.leveledFormat(tick, idx, labelFormatter, isUTC); + } + + /** + * @override + * @param expandToNicedExtent Whether expand the ticks to niced extent. + */ + getTicks(expandToNicedExtent?: boolean): 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 unitLen = scaleIntervals.length; + const idx = bisect(scaleIntervals, this._interval, 0, unitLen); + const intervals = scaleIntervals[Math.min(idx, unitLen - 1)]; - const date = new Date(val); + const innerTicks = getIntervalTicks( + intervals[0] as timeUtil.PrimaryTimeUnit, + this._approxInterval, + useUTC, + extent + ); - return formatUtil.formatTime(stepLvl[0], date, this.getSetting('useUTC')); + ticks = ticks.concat(innerTicks); + + ticks.push({ + value: extent[1], + level: 0 + }); + + return ticks; } niceExtent( @@ -111,16 +164,6 @@ class TimeScale extends IntervalScale { } this.niceTicks(opt.splitNumber, opt.minInterval, opt.maxInterval); - - // let extent = this._extent; - const interval = this._interval; - - if (!opt.fixMin) { - extent[0] = numberUtil.round(mathFloor(extent[0] / interval) * interval); - } - if (!opt.fixMax) { - extent[1] = numberUtil.round(mathCeil(extent[1] / interval) * interval); - } } niceTicks(approxTickNum: number, minInterval: number, maxInterval: number): void { @@ -128,44 +171,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 scaleLevelsLen = scaleLevels.length; - const idx = bisect(scaleLevels, approxInterval, 0, scaleLevelsLen); - - const level = scaleLevels[Math.min(idx, scaleLevelsLen - 1)]; - let interval = level[1]; - // Same with interval scale if span is much larger than 1 year - if (level[0] === 'year') { - const yearSpan = span / interval; + const scaleIntervalsLen = scaleIntervals.length; + const idx = bisect(scaleIntervals, this._approxInterval, 0, scaleIntervalsLen); - // 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 intervals = scaleIntervals[Math.min(idx, scaleIntervalsLen - 1)]; + let interval = intervals[1]; - 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 = level; // Interval will be used in getTicks this._interval = interval; - this._niceExtent = niceExtent; } parse(val: number | string | Date): number { @@ -194,44 +216,278 @@ class TimeScale extends IntervalScale { * with some modifications made for this program. * See the license statement at the head of this file. */ -const scaleLevels = [ - // 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 scaleIntervals: [timeUtil.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: 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: timeUtil.PrimaryTimeUnit) => { + return timeUtil.getUnitValue(dateA, unit, isUTC) + === 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'); + 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(); + } +} + + +function getIntervalTicks( + unitName: timeUtil.TimeUnit, + approxInterval: number, + isUTC: boolean, + extent: number[] +): TimeScaleTick[] { + const utc = isUTC ? 'UTC' : ''; + const ticks: TimeScaleTick[] = []; + const unitNames = timeUtil.timeUnits; + 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' || 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']() + ) { + continue; + } + + date['set' + utc + 'Hours'](0); + date['set' + utc + 'Minutes'](0); + date['set' + utc + 'Seconds'](0); + date['set' + utc + 'Milliseconds'](0); + + if (extent[0] === date.getTime()) { + ticks.push({ + value: extent[0], + level: levelId + }); + hasTickInLevel = true; + } + + let isDateWithinExtent = true; + let hasWeekData = false; + while (isDateWithinExtent) { + 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(); + if (dateTime > extent[1]) { + isDateWithinExtent = false; + 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); + } + } + else if (!isUnitValueSame( + timeUtil.getPrimaryTimeUnit(unitNames[i]), + extent[0], extent[1], isUTC + )) { + // Level value changes within extent + let isFirst = true; + while (true) { + switch (unitNames[i]) { + 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 (unitNames[i] === 'half-year' || unitNames[i] === 'quarter') { + 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': + case 'quarter-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 if (unitNames[i] === 'quarter-day') { + date['set' + utc + 'Hours'](date['get' + utc + 'Hours']() + 6); + } + 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; + + 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); + } + + const dateValue = (date as Date).getTime(); + if (dateValue >= extent[0] && dateValue <= extent[1]) { + ticks.push({ + value: dateValue, + level: levelId + }); + hasTickInLevel = true; + } + else if (dateValue > extent[1]) { + break; + } + isFirst = false; + } + if (hasTickInLevel + && timeUtil.isPrimaryTimeUnit(unitNames[i]) + ) { + ++levelId; + } + } + + if (unitNames[i] === unitName) { + break; + } + } + + ticks.sort((a, b) => a.value - b.value); + + let maxLevel = -Number.MAX_VALUE; + for (let i = 0; i < ticks.length; ++i) { + maxLevel = Math.max(maxLevel, ticks[i].level); + } + + // Remove duplicates + 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; +} + Scale.registerClass(TimeScale); diff --git a/src/util/format.ts b/src/util/format.ts index 3317bab87f..e9df40f97a 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..1c4cc5a2e9 --- /dev/null +++ b/src/util/time.ts @@ -0,0 +1,226 @@ +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 = { + year: '{yyyy}', + month: '{MMM}', + day: '{d}', + hour: '{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 | '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', '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); + 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 defaults = zrUtil.extend({}, defaultLeveledFormatter); + if (tick.level > 0) { + for (let i = 0; i < primaryTimeUnits.length; ++i) { + defaults[primaryTimeUnits[i]] = `{primary|${defaults[primaryTimeUnits[i]]}}`; + } + } + + const mergedFormatter = (formatter + ? (formatter.inherit === false + ? formatter // Use formatter with bigger units + : zrUtil.defaults(formatter, defaults) + ) + : defaults) as any; + + const unit = getUnitFromValue(tick.value, isUTC); + 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 || defaults.none; + } + + if (zrUtil.isArray(template)) { + 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]; + } + } + + return format(new Date(tick.value), template, isUTC); +} + +export function getUnitFromValue ( + value: number | string | Date, + isUTC: boolean +): PrimaryTimeUnit { + const date = numberUtil.parseDate(value); + const utc = isUTC ? 'UTC' : ''; + const M = (date as any)['get' + utc + 'Month']() + 1; + 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 isMonth = isDay && d === 1; + const isYear = isMonth && M === 1; + + if (isYear) { + return 'year'; + } + else if (isMonth) { + return 'month'; + } + 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); + const utc = isUTC ? 'UTC' : ''; + + switch (unit) { + 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/src/util/types.ts b/src/util/types.ts index a723cbf1d5..203e5be8df 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -300,6 +300,23 @@ 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 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 +}; // 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. diff --git a/test/timeScale-formatter.html b/test/timeScale-formatter.html new file mode 100644 index 0000000000..806eb8f91c --- /dev/null +++ b/test/timeScale-formatter.html @@ -0,0 +1,364 @@ + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/timeScale.html b/test/timeScale.html index f94e157c10..d6e899d23b 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,8 +158,10 @@ axisLine: {onZero: false}, splitNumber: 20, // minInterval: 3600 * 1000 * 24 - // min: 'dataMin', - // max: 'dataMax' + min: 'dataMin', + max: 'dataMax', + axisLabel: { + } } ], yAxis : [