Skip to content

Commit

Permalink
feat: x-axis display future time tick
Browse files Browse the repository at this point in the history
  • Loading branch information
liihuu committed Dec 18, 2024
1 parent 50939db commit 475afdd
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 143 deletions.
115 changes: 19 additions & 96 deletions src/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type BarSpace from './common/BarSpace'
import type Precision from './common/Precision'
import Action from './common/Action'
import { ActionType, type ActionCallback } from './common/Action'
import { formatValue, type DateTime, formatTimestampToDateTime, formatTimestampToString, formatBigNumber, formatThousands, formatFoldDecimal } from './common/utils/format'
import { formatValue, formatTimestampToString, formatBigNumber, formatThousands, formatFoldDecimal } from './common/utils/format'
import { getDefaultStyles, type Styles, type TooltipLegend } from './common/Styles'
import { isArray, isString, isValid, isNumber, isBoolean, isFunction, merge } from './common/utils/typeChecks'
import { createId } from './common/utils/id'
Expand All @@ -33,7 +33,8 @@ import { logWarn } from './common/utils/logger'
import { UpdateLevel } from './common/Updater'
import type { MouseTouchEvent } from './common/SyntheticEvent'
import { type LoadDataCallback, type LoadDataParams, LoadDataType } from './common/LoadDataCallback'
import { calcTextWidth } from './common/utils/canvas'
import type TimeWeightTick from './common/TimeWeightTick'
import { classifyTimeWeightTicks, createTimeWeightTickList } from './common/TimeWeightTick'

import type { Options, CustomApi, ThousandsSeparator, DecimalFold } from './Options'

Expand All @@ -52,22 +53,6 @@ import { PaneIdConstants } from './pane/types'

import type Chart from './Chart'

export interface TimeWeightTick {
weight: number
dataIndex: number
dateTime: DateTime
timestamp: number
}

export const TimeWeightConstants = {
Year: 365 * 24 * 3600,
Month: 30 * 24 * 3600,
Day: 24 * 3600,
Hour: 3600,
Minute: 60,
Second: 1
}

const BarSpaceLimitConstants = {
MIN: 1,
MAX: 50
Expand Down Expand Up @@ -449,6 +434,10 @@ export default class StoreImp implements Store {

getTimezone (): string { return this._dateTimeFormat.resolvedOptions().timeZone }

getDateTimeFormat (): Intl.DateTimeFormat {
return this._dateTimeFormat
}

setThousandsSeparator (thousandsSeparator: Partial<ThousandsSeparator>): void {
merge(this._thousandsSeparator, thousandsSeparator)
}
Expand Down Expand Up @@ -577,92 +566,26 @@ export default class StoreImp implements Store {

private _classifyTimeWeightTicks (newDataList: KLineData[], isUpdate?: boolean): void {
let baseDataIndex = 0
let prevDateTime: Nullable<DateTime> = null
let prevTimestamp: Nullable<number> = null
if (isUpdate ?? false) {
baseDataIndex = this._dataList.length
prevTimestamp = this._dataList[baseDataIndex - 1].timestamp
} else {
this._timeWeightTickMap.clear()
this._minTimeDifference = Number.MAX_SAFE_INTEGER
}
for (let i = 0; i < newDataList.length; i++) {
const timestamp = newDataList[i].timestamp
let weight = TimeWeightConstants.Second
const dateTime = formatTimestampToDateTime(this._dateTimeFormat, timestamp)
if (isValid(prevDateTime)) {
if (dateTime.YYYY !== prevDateTime.YYYY) {
weight = TimeWeightConstants.Year
} else if (dateTime.MM !== prevDateTime.MM) {
weight = TimeWeightConstants.Month
} else if (dateTime.DD !== prevDateTime.DD) {
weight = TimeWeightConstants.Day
} else if (dateTime.HH !== prevDateTime.HH) {
weight = TimeWeightConstants.Hour
} else if (dateTime.mm !== prevDateTime.mm) {
weight = TimeWeightConstants.Minute
} else {
weight = TimeWeightConstants.Second
}
}
if (isNumber(prevTimestamp)) {
this._minTimeDifference = Math.min(this._minTimeDifference, timestamp - prevTimestamp)
}
const currentTimeWeightList = this._timeWeightTickMap.get(weight) ?? []
currentTimeWeightList.push({ dataIndex: i + baseDataIndex, weight, dateTime, timestamp })
this._timeWeightTickMap.set(weight, currentTimeWeightList)
prevDateTime = dateTime
prevTimestamp = timestamp
}
this._buildTimeWeightTickList()
}

private _buildTimeWeightTickList (): void {
const styles = this._styles.xAxis.tickText
const space = Math.max(calcTextWidth('0000-00-00 00:00:00', styles.size, styles.weight, styles.family), 146)
const barCount = Math.ceil(
space / this._barSpace
const minTimeDifferenceObj = { value: this._minTimeDifference }
classifyTimeWeightTicks(
this._timeWeightTickMap,
newDataList,
this._dateTimeFormat,
baseDataIndex,
minTimeDifferenceObj,
prevTimestamp
)
let optTimeWeightTickList: TimeWeightTick[] = []
Array.from(this._timeWeightTickMap.keys()).sort((w1, w2) => w2 - w1).forEach(weight => {
const currentTimeWeightTickList = this._timeWeightTickMap.get(weight)!
const prevOptTimeWeightTickList = optTimeWeightTickList
optTimeWeightTickList = []

const prevOptTimeWeightTickListLength = prevOptTimeWeightTickList.length
let prevOptTimeWeightTickListPointer = 0
const currentTimeWeightTickListLength = currentTimeWeightTickList.length

let rightIndex = Infinity
let leftIndex = -Infinity
for (let i = 0; i < currentTimeWeightTickListLength; i++) {
const timeWeightTick = currentTimeWeightTickList[i]
const currentIndex = timeWeightTick.dataIndex

while (prevOptTimeWeightTickListPointer < prevOptTimeWeightTickListLength) {
const lastTimeWeightTick = prevOptTimeWeightTickList[prevOptTimeWeightTickListPointer]
const lastIndex = lastTimeWeightTick.dataIndex
if (lastIndex < currentIndex) {
prevOptTimeWeightTickListPointer++
optTimeWeightTickList.push(lastTimeWeightTick)
leftIndex = lastIndex
rightIndex = Infinity
} else {
rightIndex = lastIndex
break
}
}

if (rightIndex - currentIndex >= barCount && currentIndex - leftIndex >= barCount) {
optTimeWeightTickList.push(timeWeightTick)
leftIndex = currentIndex
}
}

for (; prevOptTimeWeightTickListPointer < prevOptTimeWeightTickListLength; prevOptTimeWeightTickListPointer++) {
optTimeWeightTickList.push(prevOptTimeWeightTickList[prevOptTimeWeightTickListPointer])
}
})
this._timeWeightTickList = optTimeWeightTickList
this._minTimeDifference = minTimeDifferenceObj.value
this._timeWeightTickList = createTimeWeightTickList(this._timeWeightTickMap, this._barSpace, this._styles.xAxis.tickText)
}

getTimeWeightTickList (): TimeWeightTick[] {
Expand Down Expand Up @@ -779,7 +702,7 @@ export default class StoreImp implements Store {
return
}
this._barSpace = barSpace
this._buildTimeWeightTickList()
this._timeWeightTickList = createTimeWeightTickList(this._timeWeightTickMap, this._barSpace, this._styles.xAxis.tickText)
this._calcOptimalBarSpace()
adjustBeforeFunc?.()
this._adjustVisibleRange()
Expand Down
133 changes: 133 additions & 0 deletions src/common/TimeWeightTick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* Licensed 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.
*/

import type { KLineData } from './Data'
import type Nullable from './Nullable'
import type { TextStyle } from './Styles'
import { calcTextWidth } from './utils/canvas'
import { formatTimestampToDateTime, type DateTime } from './utils/format'
import { isNumber, isValid } from './utils/typeChecks'

export default interface TimeWeightTick {
weight: number
dataIndex: number
timestamp: number
}

export const TimeWeightConstants = {
Year: 365 * 24 * 3600,
Month: 30 * 24 * 3600,
Day: 24 * 3600,
Hour: 3600,
Minute: 60,
Second: 1
}

export function classifyTimeWeightTicks (
map: Map<number, TimeWeightTick[]>,
dataList: Array<Pick<KLineData, 'timestamp'>>,
dateTimeFormat: Intl.DateTimeFormat,
baseDataIndex = 0,
minTimeDifference?: { value: number },
startTimestamp?: Nullable<number>
): void {
let prevDateTime: Nullable<DateTime> = null
let prevTimestamp: Nullable<number> = startTimestamp ?? null
for (let i = 0; i < dataList.length; i++) {
const timestamp = dataList[i].timestamp
let weight = TimeWeightConstants.Second
const dateTime = formatTimestampToDateTime(dateTimeFormat, timestamp)
if (isValid(prevDateTime)) {
if (dateTime.YYYY !== prevDateTime.YYYY) {
weight = TimeWeightConstants.Year
} else if (dateTime.MM !== prevDateTime.MM) {
weight = TimeWeightConstants.Month
} else if (dateTime.DD !== prevDateTime.DD) {
weight = TimeWeightConstants.Day
} else if (dateTime.HH !== prevDateTime.HH) {
weight = TimeWeightConstants.Hour
} else if (dateTime.mm !== prevDateTime.mm) {
weight = TimeWeightConstants.Minute
} else {
weight = TimeWeightConstants.Second
}
}
if (isNumber(prevTimestamp) && isNumber(minTimeDifference?.value)) {
minTimeDifference.value = Math.min(minTimeDifference.value, timestamp - prevTimestamp)
}
const currentTimeWeightList = map.get(weight) ?? []
currentTimeWeightList.push({ dataIndex: i + baseDataIndex, weight, timestamp })
map.set(weight, currentTimeWeightList)
prevDateTime = dateTime
prevTimestamp = timestamp
}
}

export function calcBetweenTimeWeightTickBarCount (
barSpace: number,
textStyles: Partial<Pick<TextStyle, 'size' | 'weight' | 'family'>>
): number {
const space = Math.max(calcTextWidth('0000-00-00 00:00:00', textStyles.size, textStyles.weight, textStyles.family), 146)
return Math.ceil(space / barSpace)
}

export function createTimeWeightTickList (
map: Map<number, TimeWeightTick[]>,
barSpace: number,
textStyles: Partial<Pick<TextStyle, 'size' | 'weight' | 'family'>>
): TimeWeightTick[] {
const barCount = calcBetweenTimeWeightTickBarCount(barSpace, textStyles)
let optTimeWeightTickList: TimeWeightTick[] = []
Array.from(map.keys()).sort((w1, w2) => w2 - w1).forEach(weight => {
const currentTimeWeightTickList = map.get(weight)!
const prevOptTimeWeightTickList = optTimeWeightTickList
optTimeWeightTickList = []

const prevOptTimeWeightTickListLength = prevOptTimeWeightTickList.length
let prevOptTimeWeightTickListPointer = 0
const currentTimeWeightTickListLength = currentTimeWeightTickList.length

let rightIndex = Infinity
let leftIndex = -Infinity
for (let i = 0; i < currentTimeWeightTickListLength; i++) {
const timeWeightTick = currentTimeWeightTickList[i]
const currentIndex = timeWeightTick.dataIndex

while (prevOptTimeWeightTickListPointer < prevOptTimeWeightTickListLength) {
const lastTimeWeightTick = prevOptTimeWeightTickList[prevOptTimeWeightTickListPointer]
const lastIndex = lastTimeWeightTick.dataIndex
if (lastIndex < currentIndex) {
prevOptTimeWeightTickListPointer++
optTimeWeightTickList.push(lastTimeWeightTick)
leftIndex = lastIndex
rightIndex = Infinity
} else {
rightIndex = lastIndex
break
}
}

if (rightIndex - currentIndex >= barCount && currentIndex - leftIndex >= barCount) {
optTimeWeightTickList.push(timeWeightTick)
leftIndex = currentIndex
}
}

for (; prevOptTimeWeightTickListPointer < prevOptTimeWeightTickListLength; prevOptTimeWeightTickListPointer++) {
optTimeWeightTickList.push(prevOptTimeWeightTickList[prevOptTimeWeightTickListPointer])
}
})

return optTimeWeightTickList
}
2 changes: 1 addition & 1 deletion src/component/Axis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export interface AxisCreateTicksParams {

export type AxisCreateTicksCallback = (params: AxisCreateTicksParams) => AxisTick[]

export type AxisMinSpanCallback = (value?: number) => number
export type AxisMinSpanCallback = (value: number) => number

export interface AxisTemplate {
name: string
Expand Down
Loading

0 comments on commit 475afdd

Please sign in to comment.