opt: opt x-axis tick text display

This commit is contained in:
liihuu 2024-08-29 02:10:36 +08:00
parent e203b858a9
commit 02d5676f65
7 changed files with 200 additions and 131 deletions

View File

@ -13,7 +13,6 @@
*/
import type Nullable from './Nullable'
import { type DateTime } from './utils/format'
export interface KLineData {
timestamp: number
@ -31,10 +30,3 @@ export interface VisibleData {
x: number
data: Nullable<KLineData>
}
export interface TimeWeightData {
weight: number
dataIndex: number
dateTime: DateTime
data: KLineData
}

View File

@ -15,12 +15,12 @@
import { isNumber, isValid } from './typeChecks'
export interface DateTime {
year: string
month: string
day: string
hour: string
minute: string
second: string
YYYY: string
MM: string
DD: string
HH: string
mm: string
ss: string
}
const reEscapeChar = /\\(\\)?/g

View File

@ -15,7 +15,6 @@ import type VisibleRange from '../common/VisibleRange'
import type DrawPane from '../pane/DrawPane'
import { getPrecision, nice, round } from '../common/utils/number'
import type Bounding from '../common/Bounding'
export interface AxisTick {
@ -68,7 +67,7 @@ export default abstract class AxisImp implements Pick<AxisTemplate, 'createTicks
}
if (this._prevRange.from !== this._range.from || this._prevRange.to !== this._range.to || force) {
this._prevRange = this._range
const defaultTicks = this.optimalTicks(this._calcTicks())
const defaultTicks = this.calcTicks()
this._ticks = this.createTicks({
range: this._range,
bounding: this.getSelfBounding(),
@ -100,38 +99,9 @@ export default abstract class AxisImp implements Pick<AxisTemplate, 'createTicks
getAutoCalcTickFlag (): boolean { return this._autoCalcTickFlag }
private _calcTicks (): AxisTick[] {
const { realFrom, realTo, realRange } = this._range
const ticks: AxisTick[] = []
if (realRange >= 0) {
const [interval, precision] = this._calcTickInterval(realRange)
const first = round(Math.ceil(realFrom / interval) * interval, precision)
const last = round(Math.floor(realTo / interval) * interval, precision)
let n = 0
let f = first
if (interval !== 0) {
while (f <= last) {
const v = f.toFixed(precision)
ticks[n] = { text: v, coord: 0, value: v }
++n
f += interval
}
}
}
return ticks
}
private _calcTickInterval (range: number): number[] {
const interval = nice(range / 8.0)
const precision = getPrecision(interval)
return [interval, precision]
}
protected abstract calcRange (): AxisRange
protected abstract optimalTicks (ticks: AxisTick[]): AxisTick[]
protected abstract calcTicks (): AxisTick[]
abstract createTicks (params: AxisCreateTicksParams): AxisTick[]

View File

@ -14,14 +14,11 @@
import type Nullable from '../common/Nullable'
import type Bounding from '../common/Bounding'
import { calcTextWidth } from '../common/utils/canvas'
import { isValid } from '../common/utils/typeChecks'
import { type FormatDate, FormatDateType } from '../Options'
import AxisImp, { type AxisTemplate, type Axis, type AxisRange, type AxisTick, type AxisCreateTicksParams } from './Axis'
import type DrawPane from '../pane/DrawPane'
import { TimeWeightConstants } from '../store/TimeScaleStore'
export type XAxis = Axis
@ -39,77 +36,39 @@ export default abstract class XAxisImp extends AxisImp {
}
}
protected optimalTicks (ticks: AxisTick[]): AxisTick[] {
const chart = this.getParent().getChart()
const chartStore = chart.getChartStore()
const formatDate = chartStore.getCustomApi().formatDate
const optimalTicks: AxisTick[] = []
const tickLength = ticks.length
const dataList = chartStore.getDataList()
if (tickLength > 0) {
const dateTimeFormat = chartStore.getTimeScaleStore().getDateTimeFormat()
const tickTextStyles = chart.getStyles().xAxis.tickText
const defaultLabelWidth = calcTextWidth('00-00 00:00', tickTextStyles.size, tickTextStyles.weight, tickTextStyles.family)
const pos = parseInt(ticks[0].value as string, 10)
const x = this.convertToPixel(pos)
let tickCountDif = 1
if (tickLength > 1) {
const nextPos = parseInt(ticks[1].value as string, 10)
const nextX = this.convertToPixel(nextPos)
const xDif = Math.abs(nextX - x)
if (xDif < defaultLabelWidth) {
tickCountDif = Math.ceil(defaultLabelWidth / xDif)
protected override calcTicks (): AxisTick[] {
const timeTickList = this.getParent().getChart().getChartStore().getTimeScaleStore().getVisibleTimeTickList()
return timeTickList.map(({ dataIndex, dateTime, weight, timestamp }) => {
let text = ''
switch (weight) {
case TimeWeightConstants.Year: {
text = dateTime.YYYY
break
}
case TimeWeightConstants.Month: {
text = `${dateTime.YYYY}-${dateTime.MM}`
break
}
case TimeWeightConstants.Day: {
text = `${dateTime.MM}-${dateTime.DD}`
break
}
case TimeWeightConstants.Hour:
case TimeWeightConstants.Minute: {
text = `${dateTime.HH}-${dateTime.mm}`
break
}
default: {
text = `${dateTime.HH}-${dateTime.mm}-${dateTime.ss}`
break
}
}
for (let i = 0; i < tickLength; i += tickCountDif) {
const pos = parseInt(ticks[i].value as string, 10)
const kLineData = dataList[pos]
const timestamp = kLineData.timestamp
let text = formatDate(dateTimeFormat, timestamp, 'HH:mm', FormatDateType.XAxis)
if (i !== 0) {
const prevPos = parseInt(ticks[i - tickCountDif].value as string, 10)
const prevKLineData = dataList[prevPos]
const prevTimestamp = prevKLineData.timestamp
text = this._optimalTickLabel(formatDate, dateTimeFormat, timestamp, prevTimestamp) ?? text
}
const x = this.convertToPixel(pos)
optimalTicks.push({ text, coord: x, value: timestamp })
return {
coord: this.convertToPixel(dataIndex),
text,
value: timestamp
}
const optimalTickLength = optimalTicks.length
if (optimalTickLength === 1) {
optimalTicks[0].text = formatDate(dateTimeFormat, optimalTicks[0].value as number, 'YYYY-MM-DD HH:mm', FormatDateType.XAxis)
} else {
const firstTimestamp = optimalTicks[0].value as number
const secondTimestamp = optimalTicks[1].value as number
if (isValid(optimalTicks[2])) {
const thirdText = optimalTicks[2].text
if (/^[0-9]{2}-[0-9]{2}$/.test(thirdText)) {
optimalTicks[0].text = formatDate(dateTimeFormat, firstTimestamp, 'MM-DD', FormatDateType.XAxis)
} else if (/^[0-9]{4}-[0-9]{2}$/.test(thirdText)) {
optimalTicks[0].text = formatDate(dateTimeFormat, firstTimestamp, 'YYYY-MM', FormatDateType.XAxis)
} else if (/^[0-9]{4}$/.test(thirdText)) {
optimalTicks[0].text = formatDate(dateTimeFormat, firstTimestamp, 'YYYY', FormatDateType.XAxis)
}
} else {
optimalTicks[0].text = this._optimalTickLabel(formatDate, dateTimeFormat, firstTimestamp, secondTimestamp) ?? optimalTicks[0].text
}
}
}
return optimalTicks
}
private _optimalTickLabel (formatDate: FormatDate, dateTimeFormat: Intl.DateTimeFormat, timestamp: number, comparedTimestamp: number): Nullable<string> {
const year = formatDate(dateTimeFormat, timestamp, 'YYYY', FormatDateType.XAxis)
const month = formatDate(dateTimeFormat, timestamp, 'YYYY-MM', FormatDateType.XAxis)
const day = formatDate(dateTimeFormat, timestamp, 'MM-DD', FormatDateType.XAxis)
if (year !== formatDate(dateTimeFormat, comparedTimestamp, 'YYYY', FormatDateType.XAxis)) {
return year
} else if (month !== formatDate(dateTimeFormat, comparedTimestamp, 'YYYY-MM', FormatDateType.XAxis)) {
return month
} else if (day !== formatDate(dateTimeFormat, comparedTimestamp, 'MM-DD', FormatDateType.XAxis)) {
return day
}
return null
})
}
override getAutoSize (): number {

View File

@ -15,7 +15,7 @@
import { YAxisType, YAxisPosition, CandleType } from '../common/Styles'
import type Bounding from '../common/Bounding'
import { isNumber, isValid } from '../common/utils/typeChecks'
import { index10, log10 } from '../common/utils/number'
import { index10, log10, getPrecision, nice, round } from '../common/utils/number'
import { calcTextWidth } from '../common/utils/canvas'
import { formatPrecision, formatThousands, formatFoldDecimal } from '../common/utils/format'
@ -247,7 +247,32 @@ export default abstract class YAxisImp extends AxisImp implements YAxis {
)
}
protected optimalTicks (ticks: AxisTick[]): AxisTick[] {
calcTicks (): AxisTick[] {
const { realFrom, realTo, realRange } = this.getRange()
const ticks: AxisTick[] = []
if (realRange >= 0) {
const interval = nice(realRange / 8.0)
const precision = getPrecision(interval)
const first = round(Math.ceil(realFrom / interval) * interval, precision)
const last = round(Math.floor(realTo / interval) * interval, precision)
let n = 0
let f = first
if (interval !== 0) {
while (f <= last) {
const v = f.toFixed(precision)
ticks[n] = { text: v, coord: 0, value: v }
++n
f += interval
}
}
}
return this._optimalTicks(ticks)
}
private _optimalTicks (ticks: AxisTick[]): AxisTick[] {
const pane = this.getParent()
const height = pane.getYAxisWidget()?.getBounding().height ?? 0
const chartStore = pane.getChart().getChartStore()

View File

@ -16,9 +16,9 @@ import type Nullable from '../common/Nullable'
import { type KLineData, type VisibleData } from '../common/Data'
import type Precision from '../common/Precision'
import type DeepPartial from '../common/DeepPartial'
import { formatValue } from '../common/utils/format'
import { getDefaultStyles, type Styles, type TooltipLegend } from '../common/Styles'
import { isArray, isNumber, isString, isValid, merge } from '../common/utils/typeChecks'
import { formatValue } from '../common/utils/format'
import type LoadDataCallback from '../common/LoadDataCallback'
import { type LoadDataParams, LoadDataType } from '../common/LoadDataCallback'
import type LoadMoreCallback from '../common/LoadMoreCallback'
@ -35,7 +35,6 @@ import ActionStore from './ActionStore'
import { getStyles } from '../extension/styles/index'
import type Chart from '../Chart'
export default class ChartStore {
/**
* Internal chart
@ -132,8 +131,6 @@ export default class ChartStore {
*/
private _visibleDataList: VisibleData[] = []
// private _dataMap = new Map<number, TimeClassifyData[]>()
constructor (chart: Chart, options?: Options) {
this._chart = chart
this.setOptions(options)
@ -246,11 +243,13 @@ export default class ChartStore {
this.clear()
this._dataList = data
this._forwardMore = more ?? true
this._timeScaleStore.classifyTimeTicks(this._dataList)
this._timeScaleStore.resetOffsetRightDistance()
adjustFlag = true
break
}
case LoadDataType.Backward: {
this._timeScaleStore.classifyTimeTicks(data, true)
this._dataList = this._dataList.concat(data)
this._backwardMore = more ?? false
adjustFlag = dataLengthChange > 0
@ -258,6 +257,7 @@ export default class ChartStore {
}
case LoadDataType.Forward: {
this._dataList = data.concat(this._dataList)
this._timeScaleStore.classifyTimeTicks(this._dataList)
this._forwardMore = more ?? false
adjustFlag = dataLengthChange > 0
}
@ -270,6 +270,7 @@ export default class ChartStore {
const timestamp = data.timestamp
const lastDataTimestamp = formatValue(this._dataList[dataCount - 1], 'timestamp', 0) as number
if (timestamp > lastDataTimestamp) {
this._timeScaleStore.classifyTimeTicks([data], true)
this._dataList.push(data)
let lastBarRightSideDiffBarCount = this._timeScaleStore.getLastBarRightSideDiffBarCount()
if (lastBarRightSideDiffBarCount < 0) {
@ -290,6 +291,7 @@ export default class ChartStore {
this._timeScaleStore.adjustVisibleRange()
this._tooltipStore.recalculateCrosshair(true)
this._indicatorStore.calcInstance()
this._chart.adjustPaneViewport(false, true, true, true)
}
this._actionStore.execute(ActionType.OnDataReady)
}

View File

@ -19,19 +19,29 @@ import type BarSpace from '../common/BarSpace'
import type VisibleRange from '../common/VisibleRange'
import { getDefaultVisibleRange } from '../common/VisibleRange'
import { ActionType } from '../common/Action'
import { type DateTime, formatDateToDateTime } from '../common/utils/format'
import { isValid, isNumber, isString } from '../common/utils/typeChecks'
import { logWarn } from '../common/utils/logger'
import { binarySearchNearest } from '../common/utils/number'
import { isNumber, isString } from '../common/utils/typeChecks'
import { LoadDataType } from '../common/LoadDataCallback'
import { calcTextWidth } from '../common/utils/canvas'
import type ChartStore from './ChartStore'
import { LoadDataType } from '../common/LoadDataCallback'
export interface TimeCategoryDataMapValue {
category: number
export interface TimeTick {
weight: number
dataIndex: number
dateTime: DateTime
timestamp: number
data: KLineData
}
export const TimeWeightConstants = {
Year: 365 * 24 * 3600,
Month: 30 * 24 * 3600,
Day: 24 * 3600,
Hour: 3600,
Minute: 60,
Second: 1
}
interface LeftRightSide {
@ -126,7 +136,13 @@ export default class TimeScaleStore {
/**
* Start and end points of visible area data index
*/
private _visibleRange: VisibleRange = getDefaultVisibleRange()
private _visibleRange = getDefaultVisibleRange()
private _cacheVisibleRange = getDefaultVisibleRange()
private readonly _timeTicks = new Map<number, TimeTick[]>()
private _visibleTimeTickList: TimeTick[] = []
constructor (chartStore: ChartStore) {
this._chartStore = chartStore
@ -144,6 +160,100 @@ export default class TimeScaleStore {
this._gapBarSpace = Math.max(1, gapBarSpace)
}
classifyTimeTicks (newDataList: KLineData[], update?: boolean): void {
let baseDataIndex = 0
let prevKLineData: Nullable<KLineData> = null
if (update ?? false) {
const dataList = this._chartStore.getDataList()
baseDataIndex = dataList.length
prevKLineData = dataList[baseDataIndex - 1]
} else {
this._timeTicks.clear()
}
for (let i = 0; i < newDataList.length; i++) {
const kLineData = newDataList[i]
let weight = TimeWeightConstants.Second
const dateTime = formatDateToDateTime(this._dateTimeFormat, kLineData.timestamp)
if (isValid(prevKLineData)) {
const prevDateTime = formatDateToDateTime(this._dateTimeFormat, prevKLineData.timestamp)
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
}
}
const tickList = this._timeTicks.get(weight) ?? []
tickList.push({ dataIndex: i, weight, dateTime, timestamp: kLineData.timestamp })
this._timeTicks.set(weight, tickList)
prevKLineData = kLineData
}
}
adjustVisibleTimeTickList (): void {
const tickTextStyles = this._chartStore.getStyles().xAxis.tickText
const width = calcTextWidth('0000-00-00 00:00', tickTextStyles.size, tickTextStyles.weight, tickTextStyles.family)
const barCount = Math.ceil(width / this._barSpace)
let tickList: TimeTick[] = []
Array.from(this._timeTicks.keys()).sort((w1, w2) => w2 - w1).forEach(key => {
const prevTickList = tickList
tickList = []
const prevTickListLength = prevTickList.length
let prevTickListPointer = 0
const currentTicks = this._timeTicks.get(key)!
const currentTicksLength = currentTicks.length
let rightIndex = Infinity
let leftIndex = -Infinity
for (let i = 0; i < currentTicksLength; i++) {
const tick = currentTicks[i]
const currentIndex = tick.dataIndex
while (prevTickListPointer < prevTickListLength) {
const lastMark = prevTickList[prevTickListPointer]
const lastIndex = lastMark.dataIndex
if (lastIndex < currentIndex) {
prevTickListPointer++
tickList.push(lastMark)
leftIndex = lastIndex
rightIndex = Infinity
} else {
rightIndex = lastIndex
break
}
}
if (rightIndex - currentIndex >= barCount && currentIndex - leftIndex >= barCount) {
tickList.push(tick)
leftIndex = currentIndex
}
}
for (; prevTickListPointer < prevTickListLength; prevTickListPointer++) {
tickList.push(prevTickList[prevTickListPointer])
}
})
this._visibleTimeTickList = []
for (const tick of tickList) {
if (tick.dataIndex >= this._visibleRange.from && tick.dataIndex <= this._visibleRange.to) {
this._visibleTimeTickList.push(tick)
}
}
}
getVisibleTimeTickList (): TimeTick[] {
return this._visibleTimeTickList
}
/**
* adjust visible range
*/
@ -189,6 +299,13 @@ export default class TimeScaleStore {
this._visibleRange = { from, to, realFrom, realTo }
this._chartStore.getActionStore().execute(ActionType.OnVisibleRangeChange, this._visibleRange)
this._chartStore.adjustVisibleDataList()
if (
this._cacheVisibleRange.from !== this._visibleRange.from &&
this._cacheVisibleRange.to !== this._visibleRange.to
) {
this._cacheVisibleRange = { ...this._visibleRange }
this.adjustVisibleTimeTickList()
}
// More processing and loading, more loading if there are callback methods and no data is being loaded
if (from === 0) {
const firstData = dataList[0]
@ -235,6 +352,8 @@ export default class TimeScaleStore {
setTimezone (timezone: string): void {
const dateTimeFormat: Nullable<Intl.DateTimeFormat> = this._buildDateTimeFormat(timezone)
if (dateTimeFormat !== null) {
this.classifyTimeTicks(this._chartStore.getDataList())
this.adjustVisibleTimeTickList()
this._dateTimeFormat = dateTimeFormat
}
}
@ -432,5 +551,7 @@ export default class TimeScaleStore {
clear (): void {
this._visibleRange = getDefaultVisibleRange()
this._cacheVisibleRange = getDefaultVisibleRange()
this._timeTicks.clear()
}
}