refactor: refactor axis

This commit is contained in:
liihuu 2024-09-01 03:36:21 +08:00
parent 3752c89124
commit d5779483f1
16 changed files with 479 additions and 298 deletions

View File

@ -19,7 +19,7 @@ import { type KLineData } from './common/Data'
import type Coordinate from './common/Coordinate'
import type Point from './common/Point'
import { UpdateLevel } from './common/Updater'
import { type Styles, YAxisPosition } from './common/Styles'
import { type Styles, YAxisPosition, YAxisType } from './common/Styles'
import type Crosshair from './common/Crosshair'
import { ActionType, type ActionCallback } from './common/Action'
import type LoadMoreCallback from './common/LoadMoreCallback'
@ -154,6 +154,9 @@ export default class ChartImp implements Chart {
this._chartStore = new ChartStore(this, options)
this._initPanes(options)
this.adjustPaneViewport(true, true, true)
if (isValid(options?.styles)) {
this.setStyles(options?.styles)
}
}
private _initContainer (container: HTMLElement): void {
@ -574,6 +577,13 @@ export default class ChartImp implements Chart {
}
if (isValid(realStyles?.yAxis?.type)) {
(this._candlePane?.getAxisComponent() as unknown as AxisImp).setAutoCalcTickFlag(true)
const type = realStyles?.yAxis?.type
this.setPaneOptions({
id: 'candle_pane',
axisOptions: {
name: type === YAxisType.Log ? 'logarithm' : type
}
})
}
this.adjustPaneViewport(true, true, true, true, true)
}

View File

@ -253,15 +253,20 @@ export default class Event implements EventHandler {
const difRange = range * scale
const newFrom = from + difRange
const newTo = to + difRange
const newRealFrom = yAxis.convertToRealValue(newFrom)
const newRealTo = yAxis.convertToRealValue(newTo)
const newRealFrom = yAxis.valueToRealValue(newFrom, { range: this._prevYAxisRange })
const newRealTo = yAxis.valueToRealValue(newTo, { range: this._prevYAxisRange })
const newDisplayFrom = yAxis.realValueToDisplayValue(newRealFrom, { range: this._prevYAxisRange })
const newDisplayTo = yAxis.realValueToDisplayValue(newRealTo, { range: this._prevYAxisRange })
yAxis.setRange({
from: newFrom,
to: newTo,
range: newTo - newFrom,
realFrom: newRealFrom,
realTo: newRealTo,
realRange: newRealTo - newRealFrom
realRange: newRealTo - newRealFrom,
displayFrom: newDisplayFrom,
displayTo: newDisplayTo,
displayRange: newDisplayTo - newDisplayFrom
})
}
const distance = event.x - this._startScrollCoordinate.x
@ -298,15 +303,20 @@ export default class Event implements EventHandler {
const difRange = (newRange - range) / 2
const newFrom = from - difRange
const newTo = to + difRange
const newRealFrom = yAxis.convertToRealValue(newFrom)
const newRealTo = yAxis.convertToRealValue(newTo)
const newRealFrom = yAxis.valueToRealValue(newFrom, { range: this._prevYAxisRange })
const newRealTo = yAxis.valueToRealValue(newTo, { range: this._prevYAxisRange })
const newDisplayFrom = yAxis.realValueToDisplayValue(newRealFrom, { range: this._prevYAxisRange })
const newDisplayTo = yAxis.realValueToDisplayValue(newRealTo, { range: this._prevYAxisRange })
yAxis.setRange({
from: newFrom,
to: newTo,
range: newRange,
realFrom: newRealFrom,
realTo: newRealTo,
realRange: newRealTo - newRealFrom
realRange: newRealTo - newRealFrom,
displayFrom: newDisplayFrom,
displayTo: newDisplayTo,
displayRange: newDisplayTo - newDisplayFrom
})
this._chart.adjustPaneViewport(false, true, true, true)
}

View File

@ -17,6 +17,8 @@ import type VisibleRange from '../common/VisibleRange'
import type DrawPane from '../pane/DrawPane'
import type Bounding from '../common/Bounding'
import { type KLineData } from '../common/Data'
import { type Indicator } from './Indicator'
export interface AxisTick {
coord: number
@ -27,41 +29,90 @@ export interface AxisTick {
export interface AxisRange extends VisibleRange {
readonly range: number
readonly realRange: number
readonly displayFrom: number
readonly displayTo: number
readonly displayRange: number
}
export interface Axis {
getTicks: () => AxisTick[]
getRange: () => AxisRange
getAutoSize: () => number
convertToPixel: (value: number) => number
convertFromPixel: (px: number) => number
export interface AxisGap {
top?: number
bottom?: number
}
export interface AxisValueToValueParams {
range: AxisRange
}
export type AxisValueToValueCallback = (value: number, params: AxisValueToValueParams) => number
export interface AxisCreateRangeParams {
kLineDataList: KLineData[]
indicators: Indicator[]
visibleDataRange: VisibleRange
defaultRange: AxisRange
}
export type AxisCreateRangeCallback = (params: AxisCreateRangeParams) => AxisRange
export interface AxisCreateTicksParams {
range: AxisRange
bounding: Bounding
defaultTicks: AxisTick[]
}
export interface AxisCreateRangeParams {
defaultRange: AxisRange
}
export type AxisCreateRangeCallback = (params: AxisCreateRangeParams) => AxisRange
export type AxisCreateTicksCallback = (params: AxisCreateTicksParams) => AxisTick[]
export type AxisMinSpanCallback = (precision: number) => number
export interface Axis {
override: (axis: AxisTemplate) => void
getTicks: () => AxisTick[]
getRange: () => AxisRange
getAutoSize: () => number
convertToPixel: (value: number) => number
convertFromPixel: (px: number) => number
}
export interface AxisTemplate {
name: string
reverse?: boolean
scrollZoomEnabled?: boolean
gap?: AxisGap
valueToRealValue?: AxisValueToValueCallback
realValueToDisplayValue?: AxisValueToValueCallback
displayValueToRealValue?: AxisValueToValueCallback
realValueToValue?: AxisValueToValueCallback
displayValueToText?: (value: number, precision: number) => string
minSpan?: AxisMinSpanCallback
createRange?: AxisCreateRangeCallback
createTicks?: AxisCreateTicksCallback
}
export default abstract class AxisImp implements AxisTemplate, Axis {
export type AxisCreate = Omit<AxisTemplate, 'valueToRealValue' | 'realValueToDisplayValue' | 'displayValueToRealValue' | 'realValueToValue' | 'minSpan'>
function getDefaultAxisRange (): AxisRange {
return {
from: 0,
to: 0,
range: 0,
realFrom: 0,
realTo: 0,
realRange: 0,
displayFrom: 0,
displayTo: 0,
displayRange: 0
}
}
export default abstract class AxisImp implements Axis {
name: string
scrollZoomEnabled = true
createTicks: AxisCreateTicksCallback
private readonly _parent: DrawPane<Axis>
private _range: AxisRange = { from: 0, to: 0, range: 0, realFrom: 0, realTo: 0, realRange: 0 }
private _prevRange: AxisRange = { from: 0, to: 0, range: 0, realFrom: 0, realTo: 0, realRange: 0 }
private _range = getDefaultAxisRange()
private _prevRange = getDefaultAxisRange()
private _ticks: AxisTick[] = []
private _autoCalcTickFlag = true
@ -70,23 +121,15 @@ export default abstract class AxisImp implements AxisTemplate, Axis {
this._parent = parent
}
name: string
getParent (): DrawPane<Axis> { return this._parent }
buildTicks (force: boolean): boolean {
if (this._autoCalcTickFlag) {
const defaultRange = this.createDefaultRange()
this._range = this.createRange?.({ defaultRange }) ?? defaultRange
this._range = this.createRangeImp()
}
if (this._prevRange.from !== this._range.from || this._prevRange.to !== this._range.to || force) {
this._prevRange = this._range
const defaultTicks = this.createDefaultTicks()
this._ticks = this.createTicks?.({
range: this._range,
bounding: this.getBounding(),
defaultTicks
}) ?? defaultTicks
this._ticks = this.createTicksImp()
return true
}
return false
@ -113,15 +156,13 @@ export default abstract class AxisImp implements AxisTemplate, Axis {
getAutoCalcTickFlag (): boolean { return this._autoCalcTickFlag }
protected abstract createDefaultRange (): AxisRange
protected abstract createRangeImp (): AxisRange
protected abstract createDefaultTicks (): AxisTick[]
protected abstract createTicksImp (): AxisTick[]
protected abstract getBounding (): Bounding
abstract createRange (params: AxisCreateRangeParams): AxisRange
abstract createTicks (params: AxisCreateTicksParams): AxisTick[]
abstract override (axis: AxisTemplate): void
abstract getAutoSize (): number

View File

@ -14,13 +14,17 @@
import type Nullable from '../common/Nullable'
import type Bounding from '../common/Bounding'
import { isFunction, isString } from '../common/utils/typeChecks'
import AxisImp, { type AxisTemplate, type Axis, type AxisRange, type AxisTick, type AxisCreateTicksParams, type AxisCreateRangeParams } from './Axis'
import AxisImp, { type AxisTemplate, type Axis, type AxisRange, type AxisTick } from './Axis'
import type DrawPane from '../pane/DrawPane'
import { TimeWeightConstants } from '../store/TimeScaleStore'
import { FormatDateType } from '../Options'
export interface XAxis extends Axis {
export type XAxisTemplate = Pick<AxisTemplate, 'name' | 'scrollZoomEnabled' | 'createTicks'>
export interface XAxis extends Axis, AxisTemplate {
convertTimestampFromPixel: (pixel: number) => Nullable<number>
convertTimestampToPixel: (timestamp: number) => number
}
@ -28,41 +32,73 @@ export interface XAxis extends Axis {
export type XAxisConstructor = new (parent: DrawPane<Axis>) => XAxis
export default abstract class XAxisImp extends AxisImp implements XAxis {
protected override createDefaultRange (): AxisRange {
const chartStore = this.getParent().getChart().getChartStore()
const { from, to } = chartStore.getTimeScaleStore().getVisibleRange()
const af = from
const at = to - 1
const range = to - from
return {
from: af, to: at, range, realFrom: af, realTo: at, realRange: range
}
constructor (parent: DrawPane<Axis>, xAxis: XAxisTemplate) {
super(parent)
this.override(xAxis)
}
protected override createDefaultTicks (): AxisTick[] {
const timeTickList = this.getParent().getChart().getChartStore().getTimeScaleStore().getVisibleTimeTickList()
return timeTickList.map(({ dataIndex, dateTime, weight, timestamp }) => {
override (xAxis: XAxisTemplate): void {
const {
name,
scrollZoomEnabled,
createTicks
} = xAxis
if (!isString(name)) {
this.name = name
}
this.scrollZoomEnabled = scrollZoomEnabled ?? this.scrollZoomEnabled
this.createTicks = createTicks ?? this.createTicks
}
protected override createRangeImp (): AxisRange {
const chartStore = this.getParent().getChart().getChartStore()
const visibleDataRange = chartStore.getTimeScaleStore().getVisibleRange()
const { from, to } = visibleDataRange
const af = from
const at = to - 1
const diff = to - from
const range = {
from: af,
to: at,
range: diff,
realFrom: af,
realTo: at,
realRange: diff,
displayFrom: af,
displayTo: at,
displayRange: diff
}
return range
}
protected override createTicksImp (): AxisTick[] {
const chartStore = this.getParent().getChart().getChartStore()
const timeScaleStore = chartStore.getTimeScaleStore()
const formatDate = chartStore.getCustomApi().formatDate
const timeTickList = timeScaleStore.getVisibleTimeTickList()
const dateTimeFormat = timeScaleStore.getDateTimeFormat()
const ticks = timeTickList.map(({ dataIndex, weight, timestamp }) => {
let text = ''
switch (weight) {
case TimeWeightConstants.Year: {
text = dateTime.YYYY + '年'
text = formatDate(dateTimeFormat, timestamp, 'YYYY', FormatDateType.XAxis)
break
}
case TimeWeightConstants.Month: {
text = `${dateTime.YYYY}${dateTime.MM}`
text = formatDate(dateTimeFormat, timestamp, 'YYYY-MM', FormatDateType.XAxis)
break
}
case TimeWeightConstants.Day: {
text = `${dateTime.MM}${dateTime.DD}`
text = formatDate(dateTimeFormat, timestamp, 'MM-DD', FormatDateType.XAxis)
break
}
case TimeWeightConstants.Hour:
case TimeWeightConstants.Minute: {
text = `${dateTime.HH}-${dateTime.mm}`
text = formatDate(dateTimeFormat, timestamp, 'HH:mm', FormatDateType.XAxis)
break
}
default: {
text = `${dateTime.HH}-${dateTime.mm}-${dateTime.ss}`
text = formatDate(dateTimeFormat, timestamp, 'HH:mm:ss', FormatDateType.XAxis)
break
}
}
@ -72,6 +108,14 @@ export default abstract class XAxisImp extends AxisImp implements XAxis {
value: timestamp
}
})
if (isFunction(this.createTicks)) {
return this.createTicks({
range: this.getRange(),
bounding: this.getBounding(),
defaultTicks: ticks
})
}
return ticks
}
override getAutoSize (): number {
@ -134,14 +178,10 @@ export default abstract class XAxisImp extends AxisImp implements XAxis {
return this.getParent().getChart().getChartStore().getTimeScaleStore().dataIndexToCoordinate(value)
}
static extend (template: AxisTemplate): XAxisConstructor {
static extend (template: XAxisTemplate): XAxisConstructor {
class Custom extends XAxisImp {
createRange (params: AxisCreateRangeParams): AxisRange {
return template.createRange?.(params) ?? params.defaultRange
}
createTicks (params: AxisCreateTicksParams): AxisTick[] {
return template.createTicks?.(params) ?? params.defaultTicks
constructor (parent: DrawPane<Axis>) {
super(parent, template)
}
}
return Custom

View File

@ -12,43 +12,92 @@
* limitations under the License.
*/
import { YAxisType, YAxisPosition, CandleType } from '../common/Styles'
import { YAxisPosition, CandleType } from '../common/Styles'
import type Bounding from '../common/Bounding'
import { isNumber, isValid } from '../common/utils/typeChecks'
import { index10, log10, getPrecision, nice, round } from '../common/utils/number'
import { isNumber, isString, isValid, merge } from '../common/utils/typeChecks'
import { index10, getPrecision, nice, round } from '../common/utils/number'
import { calcTextWidth } from '../common/utils/canvas'
import { formatPrecision, formatThousands, formatFoldDecimal } from '../common/utils/format'
import AxisImp, { type AxisTemplate, type Axis, type AxisRange, type AxisTick, type AxisCreateTicksParams, type AxisCreateRangeParams } from './Axis'
import { type IndicatorFigure } from './Indicator'
import AxisImp, {
type AxisTemplate, type Axis, type AxisRange,
type AxisTick, type AxisValueToValueCallback,
type AxisMinSpanCallback, type AxisCreateRangeCallback
} from './Axis'
import type DrawPane from '../pane/DrawPane'
import { PaneIdConstants } from '../pane/types'
interface FiguresResult {
figures: IndicatorFigure[]
result: any[]
}
export type YAxisTemplate = AxisTemplate
export interface YAxis extends Axis {
const TICK_COUNT = 10
export interface YAxis extends Axis, YAxisTemplate {
isFromZero: () => boolean
isInCandle: () => boolean
getType: () => YAxisType
convertToNicePixel: (value: number) => number
}
export type YAxisConstructor = new (parent: DrawPane<Axis>) => YAxis
export default abstract class YAxisImp extends AxisImp implements YAxis {
protected override createDefaultRange (): AxisRange {
reverse = false
gap = {
top: 0.2,
bottom: 0.1
}
createRange: AxisCreateRangeCallback = params => params.defaultRange
minSpan: AxisMinSpanCallback = precision => index10(-precision)
valueToRealValue: AxisValueToValueCallback = value => value
realValueToDisplayValue: AxisValueToValueCallback = value => value
displayValueToRealValue: AxisValueToValueCallback = value => value
realValueToValue: AxisValueToValueCallback = value => value
displayValueToText: ((value: number, precision: number) => string) = (value, precision) => formatPrecision(value, precision)
constructor (parent: DrawPane<Axis>, yAxis: YAxisTemplate) {
super(parent)
this.override(yAxis)
}
override (yAxis: YAxisTemplate): void {
const {
name,
reverse,
scrollZoomEnabled,
gap,
minSpan,
displayValueToText,
valueToRealValue,
realValueToDisplayValue,
displayValueToRealValue,
realValueToValue,
createRange,
createTicks
} = yAxis
if (!isString(name)) {
this.name = name
}
this.scrollZoomEnabled = scrollZoomEnabled ?? this.scrollZoomEnabled
this.reverse = reverse ?? this.reverse
merge(this.gap, gap)
this.displayValueToText = displayValueToText ?? this.displayValueToText
this.minSpan = minSpan ?? this.minSpan
this.valueToRealValue = valueToRealValue ?? this.valueToRealValue
this.realValueToDisplayValue = realValueToDisplayValue ?? this.realValueToDisplayValue
this.displayValueToRealValue = displayValueToRealValue ?? this.displayValueToRealValue
this.realValueToValue = realValueToValue ?? this.realValueToValue
this.createRange = createRange ?? this.createRange
this.createTicks = createTicks ?? this.createTicks
}
protected override createRangeImp (): AxisRange {
const parent = this.getParent()
const chart = parent.getChart()
const chartStore = chart.getChartStore()
let min = Number.MAX_SAFE_INTEGER
let max = Number.MIN_SAFE_INTEGER
const figuresResultList: FiguresResult[] = []
let shouldOhlc = false
let specifyMin = Number.MAX_SAFE_INTEGER
let specifyMax = Number.MIN_SAFE_INTEGER
@ -65,10 +114,6 @@ export default abstract class YAxisImp extends AxisImp implements YAxis {
if (isNumber(indicator.maxValue)) {
specifyMax = Math.max(specifyMax, indicator.maxValue)
}
figuresResultList.push({
figures: indicator.figures ?? [],
result: indicator.result ?? []
})
})
let precision = 4
@ -104,10 +149,10 @@ export default abstract class YAxisImp extends AxisImp implements YAxis {
}
}
}
figuresResultList.forEach(({ figures, result }) => {
const indicatorData = result[dataIndex] ?? {}
indicators.forEach(({ result, figures }) => {
const data = result[dataIndex] ?? {}
figures.forEach(figure => {
const value = indicatorData[figure.key]
const value = data[figure.key]
if (isNumber(value)) {
min = Math.min(min, value)
max = Math.max(max, value)
@ -123,85 +168,70 @@ export default abstract class YAxisImp extends AxisImp implements YAxis {
min = 0
max = 10
}
const type = this.getType()
let dif: number
switch (type) {
case YAxisType.Percentage: {
const fromData = chartStore.getVisibleFirstData()
if (isValid(fromData) && isNumber(fromData.close)) {
min = (min - fromData.close) / fromData.close * 100
max = (max - fromData.close) / fromData.close * 100
}
dif = Math.pow(10, -2)
break
}
case YAxisType.Log: {
min = log10(min)
max = log10(max)
dif = 0.05 * index10(-precision)
break
}
default: {
dif = index10(-precision)
}
const defaultDiff = max - min
const defaultRange = {
from: min,
to: max,
range: defaultDiff,
realFrom: min,
realTo: max,
realRange: defaultDiff,
displayFrom: min,
displayTo: max,
displayRange: defaultDiff
}
const range = this.createRange?.({
kLineDataList: chartStore.getDataList(),
visibleDataRange: chartStore.getTimeScaleStore().getVisibleRange(),
indicators,
defaultRange
})
let realFrom = range.realFrom
let realTo = range.realTo
let realRange = range.realRange
const minSpan = this.minSpan(precision)
if (
min === max ||
Math.abs(min - max) < dif
realFrom === realTo || realRange < minSpan
) {
const minCheck = specifyMin === min
const maxCheck = specifyMax === max
min = minCheck ? min : (maxCheck ? min - 8 * dif : min - 4 * dif)
max = maxCheck ? max : (minCheck ? max + 8 * dif : max + 4 * dif)
const minCheck = specifyMin === realFrom
const maxCheck = specifyMax === realTo
const halfTickCount = TICK_COUNT / 2
realFrom = minCheck ? realFrom : (maxCheck ? realFrom - TICK_COUNT * minSpan : realFrom - halfTickCount * minSpan)
realTo = maxCheck ? realTo : (minCheck ? realTo + TICK_COUNT * minSpan : realTo + halfTickCount * minSpan)
}
const height = this.getParent().getYAxisWidget()?.getBounding().height ?? 0
const { gap: paneGap } = parent.getOptions()
let topRate = paneGap?.top ?? 0.2
const height = this.getBounding().height
const { top, bottom } = this.gap
let topRate = top
if (topRate >= 1) {
topRate = topRate / height
}
let bottomRate = paneGap?.bottom ?? 0.1
let bottomRate = bottom
if (bottomRate >= 1) {
bottomRate = bottomRate / height
}
let range = Math.abs(max - min)
// gap
min = min - range * bottomRate
max = max + range * topRate
range = Math.abs(max - min)
let realMin: number
let realMax: number
let realRange: number
if (type === YAxisType.Log) {
realMin = index10(min)
realMax = index10(max)
realRange = Math.abs(realMax - realMin)
} else {
realMin = min
realMax = max
realRange = range
}
realRange = realTo - realFrom
realFrom = realFrom - realRange * bottomRate
realTo = realTo + realRange * topRate
const from = this.realValueToValue(realFrom, { range })
const to = this.realValueToValue(realTo, { range })
const displayFrom = this.realValueToDisplayValue(realFrom, { range })
const displayTo = this.realValueToDisplayValue(realTo, { range })
return {
from: min, to: max, range, realFrom: realMin, realTo: realMax, realRange
from,
to,
range: to - from,
realFrom,
realTo,
realRange: realTo - realFrom,
displayFrom,
displayTo,
displayRange: displayTo - displayFrom
}
}
/**
*
* @param value
* @return {number}
* @private
*/
_innerConvertToPixel (value: number): number {
const height = this.getParent().getYAxisWidget()?.getBounding().height ?? 0
const { from, range } = this.getRange()
const rate = (value - from) / range
return this.isReverse() ? Math.round(rate * height) : Math.round((1 - rate) * height)
}
/**
*
* @return {boolean}
@ -210,17 +240,6 @@ export default abstract class YAxisImp extends AxisImp implements YAxis {
return this.getParent().getId() === PaneIdConstants.CANDLE
}
/**
* y轴类型
* @return {YAxisType}
*/
getType (): YAxisType {
if (this.isInCandle()) {
return this.getParent().getChart().getStyles().yAxis.type
}
return YAxisType.Normal
}
getPosition (): string {
return this.getParent().getChart().getStyles().yAxis.position
}
@ -249,16 +268,17 @@ export default abstract class YAxisImp extends AxisImp implements YAxis {
)
}
protected override createDefaultTicks (): AxisTick[] {
const { realFrom, realTo, realRange } = this.getRange()
protected override createTicksImp (): AxisTick[] {
const range = this.getRange()
const { displayFrom, displayTo, displayRange } = range
const ticks: AxisTick[] = []
if (realRange >= 0) {
const interval = nice(realRange / 10)
if (displayRange >= 0) {
const interval = nice(displayRange / TICK_COUNT)
const precision = getPrecision(interval)
const first = round(Math.ceil(realFrom / interval) * interval, precision)
const last = round(Math.floor(realTo / interval) * interval, precision)
const first = round(Math.ceil(displayFrom / interval) * interval, precision)
const last = round(Math.floor(displayTo / interval) * interval, precision)
let n = 0
let f = first
@ -271,16 +291,12 @@ export default abstract class YAxisImp extends AxisImp implements YAxis {
}
}
}
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()
const customApi = chartStore.getCustomApi()
const optimalTicks: AxisTick[] = []
const type = this.getType()
const indicators = chartStore.getIndicatorStore().getInstances(pane.getId())
const thousandsSeparator = chartStore.getThousandsSeparator()
const decimalFoldThreshold = chartStore.getDecimalFoldThreshold()
@ -299,25 +315,15 @@ export default abstract class YAxisImp extends AxisImp implements YAxis {
const textHeight = chartStore.getStyles().xAxis.tickText.size
let validY: number
ticks.forEach(({ value }) => {
let v: string
let y = this._innerConvertToPixel(+value)
switch (type) {
case YAxisType.Percentage: {
v = `${formatPrecision(value, 2)}%`
break
}
case YAxisType.Log: {
y = this._innerConvertToPixel(log10(+value))
v = formatPrecision(value, precision)
break
}
default: {
v = formatPrecision(value, precision)
if (shouldFormatBigNumber) {
v = customApi.formatBigNumber(value)
}
break
}
let v = this.displayValueToText(+value, precision)
const y = this.convertToPixel(
this.realValueToValue(
this.displayValueToRealValue(+value, { range }),
{ range }
)
)
if (shouldFormatBigNumber) {
v = customApi.formatBigNumber(value)
}
v = formatFoldDecimal(formatThousands(v, thousandsSeparator), decimalFoldThreshold)
const validYNumber = isNumber(validY)
@ -376,18 +382,16 @@ export default abstract class YAxisImp extends AxisImp implements YAxis {
}
})
let precision = 2
if (this.getType() !== YAxisType.Percentage) {
if (this.isInCandle()) {
const { price: pricePrecision } = chartStore.getPrecision()
const lastValueMarkStyles = styles.indicator.lastValueMark
if (lastValueMarkStyles.show && lastValueMarkStyles.text.show) {
precision = Math.max(techPrecision, pricePrecision)
} else {
precision = pricePrecision
}
if (this.isInCandle()) {
const { price: pricePrecision } = chartStore.getPrecision()
const lastValueMarkStyles = styles.indicator.lastValueMark
if (lastValueMarkStyles.show && lastValueMarkStyles.text.show) {
precision = Math.max(techPrecision, pricePrecision)
} else {
precision = techPrecision
precision = pricePrecision
}
} else {
precision = techPrecision
}
let valueText = formatPrecision(this.getRange().to, precision)
if (shouldFormatBigNumber) {
@ -414,54 +418,21 @@ export default abstract class YAxisImp extends AxisImp implements YAxis {
}
convertFromPixel (pixel: number): number {
const height = this.getParent().getYAxisWidget()?.getBounding().height ?? 0
const { from, range } = this.getRange()
const height = this.getBounding().height
const range = this.getRange()
const { realFrom, realRange } = range
const rate = this.isReverse() ? pixel / height : 1 - pixel / height
const value = rate * range + from
switch (this.getType()) {
case YAxisType.Percentage: {
const fromData = this.getParent().getChart().getChartStore().getVisibleFirstData()
if (isValid(fromData) && isNumber(fromData.close)) {
return fromData.close * value / 100 + fromData.close
}
return 0
}
case YAxisType.Log: {
return index10(value)
}
default: {
return value
}
}
}
convertToRealValue (value: number): number {
let v = value
if (this.getType() === YAxisType.Log) {
v = index10(value)
}
return v
const realValue = rate * realRange + realFrom
return this.realValueToValue(realValue, { range })
}
convertToPixel (value: number): number {
let v = value
switch (this.getType()) {
case YAxisType.Percentage: {
const fromData = this.getParent().getChart().getChartStore().getVisibleFirstData()
if (isValid(fromData) && isNumber(fromData.close)) {
v = (value - fromData.close) / fromData.close * 100
}
break
}
case YAxisType.Log: {
v = log10(value)
break
}
default: {
v = value
}
}
return this._innerConvertToPixel(v)
const range = this.getRange()
const realValue = this.valueToRealValue(value, { range })
const height = this.getParent().getYAxisWidget()?.getBounding().height ?? 0
const { realFrom, realRange } = range
const rate = (realValue - realFrom) / realRange
return this.isReverse() ? Math.round(rate * height) : Math.round((1 - rate) * height)
}
convertToNicePixel (value: number): number {
@ -470,14 +441,10 @@ export default abstract class YAxisImp extends AxisImp implements YAxis {
return Math.round(Math.max(height * 0.05, Math.min(pixel, height * 0.98)))
}
static extend (template: AxisTemplate): YAxisConstructor {
static extend (template: YAxisTemplate): YAxisConstructor {
class Custom extends YAxisImp {
createRange (params: AxisCreateRangeParams): AxisRange {
return template.createRange?.(params) ?? params.defaultRange
}
createTicks (params: AxisCreateTicksParams): AxisTick[] {
return template.createTicks?.(params) ?? params.defaultTicks
constructor (parent: DrawPane<Axis>) {
super(parent, template)
}
}
return Custom

View File

@ -15,10 +15,10 @@
import { type AxisTemplate } from '../../component/Axis'
import XAxisImp, { type XAxisConstructor } from '../../component/XAxis'
import defaultXAxis from './default'
import normal from './normal'
const xAxises: Record<string, XAxisConstructor> = {
default: XAxisImp.extend(defaultXAxis)
normal: XAxisImp.extend(normal)
}
function registerXAxis (axis: AxisTemplate): void {

View File

@ -15,8 +15,7 @@
import { type AxisTemplate } from '../../component/Axis'
const defaultXAxis: AxisTemplate = {
name: 'default',
createTicks: ({ defaultTicks }) => defaultTicks
name: 'default'
}
export default defaultXAxis

View File

@ -15,10 +15,14 @@
import { type AxisTemplate } from '../../component/Axis'
import YAxisImp, { type YAxisConstructor } from '../../component/YAxis'
import defaultYAxis from './default'
import normal from './normal'
import percentage from './percentage'
import logarithm from './logarithm'
const yAxises: Record<string, YAxisConstructor> = {
default: YAxisImp.extend(defaultYAxis)
normal: YAxisImp.extend(normal),
percentage: YAxisImp.extend(percentage),
logarithm: YAxisImp.extend(logarithm)
}
function registerYAxis (axis: AxisTemplate): void {

View File

@ -0,0 +1,51 @@
/**
* 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 { log10, index10 } from '../../common/utils/number'
import { type AxisTemplate } from '../../component/Axis'
const logarithm: AxisTemplate = {
name: 'logarithm',
minSpan: (precision) => 0.05 * index10(-precision),
valueToRealValue: (value) => {
return log10(value)
},
realValueToDisplayValue: (value) => {
return index10(value)
},
displayValueToRealValue: (value) => {
return log10(value)
},
realValueToValue: (value) => {
return index10(value)
},
createRange: ({ defaultRange }) => {
const { from, to, range } = defaultRange
const realFrom = log10(from)
const realTo = log10(to)
return {
from,
to,
range,
realFrom,
realTo,
realRange: realTo - realFrom,
displayFrom: from,
displayTo: to,
displayRange: range
}
}
}
export default logarithm

View File

@ -14,9 +14,8 @@
import { type AxisTemplate } from '../../component/Axis'
const defaultYAxis: AxisTemplate = {
name: 'default',
createTicks: ({ defaultTicks }) => defaultTicks
const normal: AxisTemplate = {
name: 'normal'
}
export default defaultYAxis
export default normal

View File

@ -0,0 +1,52 @@
/**
* 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 { formatPrecision } from '../../common/utils/format'
import { isValid } from '../../common/utils/typeChecks'
import { type AxisTemplate } from '../../component/Axis'
const percentage: AxisTemplate = {
name: 'percentage',
minSpan: () => Math.pow(10, -2),
displayValueToText: value => `${formatPrecision(value, 2)}%`,
valueToRealValue: (value, { range }) => {
return (value - range.from) / range.range * range.realRange + range.realFrom
},
realValueToValue: (value, { range }) => {
return (value - range.realFrom) / range.realRange * range.range + range.from
},
createRange: ({ defaultRange, visibleDataRange, kLineDataList }) => {
const kLineData = kLineDataList[visibleDataRange.from]
if (isValid(kLineData)) {
const { from, to, range } = defaultRange
const realFrom = (defaultRange.from - kLineData.close) / kLineData.close * 100
const realTo = (defaultRange.to - kLineData.close) / kLineData.close * 100
const realRange = realTo - realFrom
return {
from,
to,
range,
realFrom,
realTo,
realRange,
displayFrom: realFrom,
displayTo: realTo,
displayRange: realRange
}
}
return defaultRange
}
}
export default percentage

View File

@ -39,7 +39,7 @@ export default abstract class DrawPane<C extends Axis = Axis> extends Pane {
private _axis: C
private readonly _options: PickPartial<DeepRequired<Omit<PaneOptions, 'id' | 'height'>>, 'position'> = { minHeight: PANE_MIN_HEIGHT, dragEnabled: true, gap: { top: 0.2, bottom: 0.1 }, axisOptions: { name: 'default', scrollZoomEnabled: true } }
private readonly _options: PickPartial<DeepRequired<Omit<PaneOptions, 'id' | 'height'>>, 'position'> = { minHeight: PANE_MIN_HEIGHT, dragEnabled: true, gap: { top: 0.2, bottom: 0.1 }, axisOptions: { name: 'normal', scrollZoomEnabled: true } }
constructor (rootContainer: HTMLElement, afterElement: Nullable<HTMLElement>, chart: Chart, id: string, options: Omit<PaneOptions, 'id' | 'height'>) {
super(rootContainer, afterElement, chart, id)
@ -55,9 +55,14 @@ export default abstract class DrawPane<C extends Axis = Axis> extends Pane {
(this._options.axisOptions.name !== name && isString(name)) ||
!isValid(this._axis)
) {
this._axis = this.createAxisComponent(name ?? 'default')
this._axis = this.createAxisComponent(name ?? 'normal')
}
merge(this._options, options)
this._axis.override({
name: name ?? 'normal',
gap: this._options.gap,
...this._options.axisOptions
})
let container: HTMLElement
let cursor: string
if (this.getId() === PaneIdConstants.X_AXIS) {

View File

@ -12,15 +12,14 @@
* limitations under the License.
*/
import { type AxisCreate } from '../component/Axis'
export interface PaneGap {
top?: number
bottom?: number
}
export interface PaneAxisOptions {
name?: string
scrollZoomEnabled?: boolean
}
export type PaneAxisOptions = Partial<AxisCreate>
export const enum PanePosition {
Top = 'top',

View File

@ -200,7 +200,10 @@ export default class TimeScaleStore {
adjustVisibleTimeTickList (): void {
const tickTextStyles = this._chartStore.getStyles().xAxis.tickText
const width = calcTextWidth('0000-00-00 00:00', tickTextStyles.size, tickTextStyles.weight, tickTextStyles.family)
const width = Math.max(
Math.ceil(this._totalBarSpace / 10),
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 => {

View File

@ -12,8 +12,7 @@
* limitations under the License.
*/
import { YAxisType } from '../common/Styles'
import { formatPrecision, formatThousands, formatFoldDecimal } from '../common/utils/format'
import { formatThousands, formatFoldDecimal } from '../common/utils/format'
import { isValid } from '../common/utils/typeChecks'
import View from './View'
@ -45,14 +44,14 @@ export default class CandleLastPriceLabelView extends View {
} else {
backgroundColor = lastPriceMarkStyles.noChangeColor
}
let text: string
if (yAxis.getType() === YAxisType.Percentage) {
const fromData = chartStore.getVisibleFirstData()
const fromClose = fromData!.close
text = `${((close - fromClose) / fromClose * 100).toFixed(2)}%`
} else {
text = formatPrecision(close, precision.price)
}
const yAxisRange = yAxis.getRange()
let text = yAxis.displayValueToText(
yAxis.realValueToDisplayValue(
yAxis.valueToRealValue(close, { range: yAxisRange }),
{ range: yAxisRange }
),
precision.price
)
text = formatFoldDecimal(formatThousands(text, chartStore.getThousandsSeparator()), chartStore.getDecimalFoldThreshold())
let x: number
let textAlgin: CanvasTextAlign

View File

@ -14,13 +14,13 @@
import type Bounding from '../common/Bounding'
import type Crosshair from '../common/Crosshair'
import { type CrosshairStyle, type CrosshairDirectionStyle, YAxisType, type StateTextStyle } from '../common/Styles'
import { type CrosshairStyle, type CrosshairDirectionStyle, type StateTextStyle } from '../common/Styles'
import { isString } from '../common/utils/typeChecks'
import { formatPrecision, formatThousands, formatFoldDecimal } from '../common/utils/format'
import { formatThousands, formatFoldDecimal } from '../common/utils/format'
import { createFont } from '../common/utils/canvas'
import { type Axis } from '../component/Axis'
import { type YAxis } from '../component/YAxis'
import type YAxis from '../component/YAxis'
import { type TextAttrs } from '../extension/figure/text'
@ -65,28 +65,30 @@ export default class CrosshairHorizontalLabelView<C extends Axis = YAxis> extend
protected getText (crosshair: Crosshair, chartStore: ChartStore, axis: Axis): string {
const yAxis = axis as unknown as YAxis
const value = axis.convertFromPixel(crosshair.y!)
let text: string
if (yAxis.getType() === YAxisType.Percentage) {
const fromData = chartStore.getVisibleFirstData()
text = `${((value - fromData!.close) / fromData!.close * 100).toFixed(2)}%`
let precision = 0
let shouldFormatBigNumber = false
if (yAxis.isInCandle()) {
precision = chartStore.getPrecision().price
} else {
const indicators = chartStore.getIndicatorStore().getInstances(crosshair.paneId!)
let precision = 0
let shouldFormatBigNumber = false
if (yAxis.isInCandle()) {
precision = chartStore.getPrecision().price
} else {
indicators.forEach(indicator => {
precision = Math.max(indicator.precision, precision)
if (!shouldFormatBigNumber) {
shouldFormatBigNumber = indicator.shouldFormatBigNumber
}
})
}
text = formatPrecision(value, precision)
if (shouldFormatBigNumber) {
text = chartStore.getCustomApi().formatBigNumber(text)
}
indicators.forEach(indicator => {
precision = Math.max(indicator.precision, precision)
if (!shouldFormatBigNumber) {
shouldFormatBigNumber = indicator.shouldFormatBigNumber
}
})
}
const yAxisRange = yAxis.getRange()
let text = yAxis.displayValueToText(
yAxis.realValueToDisplayValue(
yAxis.valueToRealValue(value, { range: yAxisRange }),
{ range: yAxisRange }
),
precision
)
if (shouldFormatBigNumber) {
text = chartStore.getCustomApi().formatBigNumber(text)
}
return formatFoldDecimal(formatThousands(text, chartStore.getThousandsSeparator()), chartStore.getDecimalFoldThreshold())
}