diff --git a/docs/.vitepress/config/share.js b/docs/.vitepress/config/share.js index ad81c1061..c0241a5a9 100644 --- a/docs/.vitepress/config/share.js +++ b/docs/.vitepress/config/share.js @@ -68,7 +68,9 @@ export function getInstanceApiMenus (lang = '') { { text: 'setPaneOptions', link: `${prefix}/setPaneOptions` }, { text: 'getPaneOptions', link: `${prefix}/getPaneOptions` }, { text: 'setZoomEnabled', link: `${prefix}/setZoomEnabled` }, + { text: 'setZoomAnchor', link: `${prefix}/setZoomAnchor` }, { text: 'isZoomEnabled', link: `${prefix}/isZoomEnabled` }, + { text: 'getZoomAnchor', link: `${prefix}/getZoomAnchor` }, { text: 'setScrollEnabled', link: `${prefix}/setScrollEnabled` }, { text: 'isScrollEnabled', link: `${prefix}/isScrollEnabled` }, { text: 'scrollByDistance', link: `${prefix}/scrollByDistance` }, diff --git a/docs/@views/api/references/chart/init.md b/docs/@views/api/references/chart/init.md index 6076fe24d..0b4eebcb7 100644 --- a/docs/@views/api/references/chart/init.md +++ b/docs/@views/api/references/chart/init.md @@ -61,6 +61,7 @@ threshold?: number format?: (value: number | string) => string } + zoomAnchor?: { main?: 'cursor_point' | 'last_bar', xAxis?: 'cursor_point' | 'last_bar' } } ) => Chart ``` \ No newline at end of file diff --git a/docs/@views/api/samples/getZoomAnchor/index.data.js b/docs/@views/api/samples/getZoomAnchor/index.data.js new file mode 100644 index 000000000..031b0d66f --- /dev/null +++ b/docs/@views/api/samples/getZoomAnchor/index.data.js @@ -0,0 +1,20 @@ +import fs from 'fs' + +export default { + watch: ['./index.js'], + load (watchedFiles) { + return watchedFiles.reduce((data, file) => { + const result = fs.readFileSync(file, 'utf-8') + let key + if (file.match('index.js')) { + key = 'js' + } else if (file.match('index.css')) { + key = 'css' + } else { + key = 'html' + } + data[key] = result + return data + }, {}) + } +} diff --git a/docs/@views/api/samples/getZoomAnchor/index.js b/docs/@views/api/samples/getZoomAnchor/index.js new file mode 100644 index 000000000..3dadb3f0f --- /dev/null +++ b/docs/@views/api/samples/getZoomAnchor/index.js @@ -0,0 +1,19 @@ +import { init } from 'klinecharts' + +const chart = init('getZoomAnchor-chart') + +chart.setSymbol({ ticker: 'TestSymbol' }) +chart.setPeriod({ span: 1, type: 'day' }) +chart.setDataLoader({ + getBars: ({ + callback + }) => { + fetch('https://klinecharts.com/datas/kline.json') + .then(res => res.json()) + .then(dataList => { + callback(dataList) + }) + } +}) + +const anchor = chart.zoomAnchor() diff --git a/docs/@views/api/samples/getZoomAnchor/index.vue b/docs/@views/api/samples/getZoomAnchor/index.vue new file mode 100644 index 000000000..afea06cbb --- /dev/null +++ b/docs/@views/api/samples/getZoomAnchor/index.vue @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/docs/@views/api/samples/init-zoomAnchor/index.data.js b/docs/@views/api/samples/init-zoomAnchor/index.data.js new file mode 100644 index 000000000..031b0d66f --- /dev/null +++ b/docs/@views/api/samples/init-zoomAnchor/index.data.js @@ -0,0 +1,20 @@ +import fs from 'fs' + +export default { + watch: ['./index.js'], + load (watchedFiles) { + return watchedFiles.reduce((data, file) => { + const result = fs.readFileSync(file, 'utf-8') + let key + if (file.match('index.js')) { + key = 'js' + } else if (file.match('index.css')) { + key = 'css' + } else { + key = 'html' + } + data[key] = result + return data + }, {}) + } +} diff --git a/docs/@views/api/samples/init-zoomAnchor/index.js b/docs/@views/api/samples/init-zoomAnchor/index.js new file mode 100644 index 000000000..61ee43a03 --- /dev/null +++ b/docs/@views/api/samples/init-zoomAnchor/index.js @@ -0,0 +1,20 @@ +import { init } from 'klinecharts' + +const chart = init( + 'init-zoomAnchor-chart', + { zoomAnchor: { main: 'last_bar', xAxis: 'last_bar' } } +) + +chart.setSymbol({ ticker: 'TestSymbol' }) +chart.setPeriod({ span: 1, type: 'day' }) +chart.setDataLoader({ + getBars: ({ + callback + }) => { + fetch('https://klinecharts.com/datas/kline.json') + .then(res => res.json()) + .then(dataList => { + callback(dataList) + }) + } +}) diff --git a/docs/@views/api/samples/init-zoomAnchor/index.vue b/docs/@views/api/samples/init-zoomAnchor/index.vue new file mode 100644 index 000000000..05c14b896 --- /dev/null +++ b/docs/@views/api/samples/init-zoomAnchor/index.vue @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/docs/@views/api/samples/setZoomAnchor/index.data.js b/docs/@views/api/samples/setZoomAnchor/index.data.js new file mode 100644 index 000000000..031b0d66f --- /dev/null +++ b/docs/@views/api/samples/setZoomAnchor/index.data.js @@ -0,0 +1,20 @@ +import fs from 'fs' + +export default { + watch: ['./index.js'], + load (watchedFiles) { + return watchedFiles.reduce((data, file) => { + const result = fs.readFileSync(file, 'utf-8') + let key + if (file.match('index.js')) { + key = 'js' + } else if (file.match('index.css')) { + key = 'css' + } else { + key = 'html' + } + data[key] = result + return data + }, {}) + } +} diff --git a/docs/@views/api/samples/setZoomAnchor/index.js b/docs/@views/api/samples/setZoomAnchor/index.js new file mode 100644 index 000000000..c58ffbf6c --- /dev/null +++ b/docs/@views/api/samples/setZoomAnchor/index.js @@ -0,0 +1,19 @@ +import { init } from 'klinecharts' + +const chart = init('setZoomAnchor-chart') + +chart.setZoomAnchor({ main: 'last_bar', xAxis: 'last_bar' }) + +chart.setSymbol({ ticker: 'TestSymbol' }) +chart.setPeriod({ span: 1, type: 'day' }) +chart.setDataLoader({ + getBars: ({ + callback + }) => { + fetch('https://klinecharts.com/datas/kline.json') + .then(res => res.json()) + .then(dataList => { + callback(dataList) + }) + } +}) diff --git a/docs/@views/api/samples/setZoomAnchor/index.vue b/docs/@views/api/samples/setZoomAnchor/index.vue new file mode 100644 index 000000000..38bc49059 --- /dev/null +++ b/docs/@views/api/samples/setZoomAnchor/index.vue @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/docs/api/chart/init.md b/docs/api/chart/init.md index d6841897e..92e615e19 100644 --- a/docs/api/chart/init.md +++ b/docs/api/chart/init.md @@ -48,6 +48,7 @@ outline: deep - `decimalFold` 小数 0 折叠配置。 - `threshold` 折叠阈值。 - `format` 自定义格式化方法。 + - `zoomAnchor` 将缩放图表时的锚点位置设置为 `last_bar` 或 `cursor_point` ### 返回值 {#returns} `init` 返回一个图表实例对象 `Chart`。 @@ -68,6 +69,7 @@ import InitThousandsSeparatorNone from '../../@views/api/samples/init-thousandsS import InitThousandsSeparatorFormat from '../../@views/api/samples/init-thousandsSeparator-format/index.vue' import InitDecimalFoldNone from '../../@views/api/samples/init-decimalFold-none/index.vue' import InitDecimalFoldFormat from '../../@views/api/samples/init-decimalFold-format/index.vue' +import InitZoomAnchor from '../../@views/api/samples/init-zoomAnchor/index.vue' ### 基本使用 {#basic} @@ -112,3 +114,6 @@ import InitDecimalFoldFormat from '../../@views/api/samples/init-decimalFold-for ### 小数 0 折叠自定义显示 {#init-decimalFold-format} +### 将缩放图表时的缩放锚点位置设置为“last_bar” {#init-zoomAnchor} + + diff --git a/docs/api/instance/getZoomAnchor.md b/docs/api/instance/getZoomAnchor.md new file mode 100644 index 000000000..58aef6a07 --- /dev/null +++ b/docs/api/instance/getZoomAnchor.md @@ -0,0 +1,23 @@ +--- +outline: deep +--- + +# getZoomAnchor() +`getZoomAnchor` 主窗格和 x 轴窗格的缩放锚点。 + +## 参考 {#reference} + + +### 参数 {#parameters} +`getZoomAnchor` 不接受任何参数。 + +### 返回值 {#returns} +`getZoomAnchor` 回报 `ZoomAnchor` 。 + +## 用法 {#usage} + + +### 基本用法 {#basic} + \ No newline at end of file diff --git a/docs/api/instance/setZoomAnchor.md b/docs/api/instance/setZoomAnchor.md new file mode 100644 index 000000000..7e6653568 --- /dev/null +++ b/docs/api/instance/setZoomAnchor.md @@ -0,0 +1,23 @@ +--- +outline: deep +--- + +# setZoomAnchor(anchor) +`setZoomAnchor` 设置主窗格和 x 轴窗格的缩放锚点。 + +## 参考 {#reference} + + +### 参数 {#parameters} +- `anchor: ZoomAnchor` 主窗格和 x 轴窗格的缩放锚点类型。 + +### 返回值 {#returns} +`setZoomAnchor` 返回 `void` 。 + +## 用法 {#usage} + + +### 基本用法 {#basic} + \ No newline at end of file diff --git a/docs/en-US/api/chart/init.md b/docs/en-US/api/chart/init.md index 034bbb330..7a3a0b680 100644 --- a/docs/en-US/api/chart/init.md +++ b/docs/en-US/api/chart/init.md @@ -48,6 +48,7 @@ When calling, you need to wait until the container `dom` is ready. - `decimalFold` Decimal 0 folds the configuration. - `threshold` Fold threshold. - `format` Custom formatting method. + - `zoomAnchor` Set the anchor position when zooming the chart to `last_bar` or `cursor_point` ### Returns {#returns} `init` returns an object `Chart`。 @@ -68,6 +69,7 @@ import InitThousandsSeparatorNone from '../../../@views/api/samples/init-thousan import InitThousandsSeparatorFormat from '../../../@views/api/samples/init-thousandsSeparator-format/index.vue' import InitDecimalFoldNone from '../../../@views/api/samples/init-decimalFold-none/index.vue' import InitDecimalFoldFormat from '../../../@views/api/samples/init-decimalFold-format/index.vue' +import InitZoomAnchor from '../../../@views/api/samples/init-zoomAnchor/index.vue' ### Basic usage {#basic} @@ -112,3 +114,6 @@ import InitDecimalFoldFormat from '../../../@views/api/samples/init-decimalFold- ### Decimal 0 fold custom display {#init-decimalFold-format} +### Set the zoom anchor postion when zooming the chart to 'last_bar' {#init-zoomAnchor} + + diff --git a/docs/en-US/api/instance/getZoomAnchor.md b/docs/en-US/api/instance/getZoomAnchor.md new file mode 100644 index 000000000..6318da081 --- /dev/null +++ b/docs/en-US/api/instance/getZoomAnchor.md @@ -0,0 +1,23 @@ +--- +outline: deep +--- + +# getZoomAnchor() +`getZoomAnchor` zoom anchor point for main and xAxis panes. + +## Reference {#reference} + + +### Parameters {#parameters} +`getZoomAnchor` does not accept any parameters. + +### Returns {#returns} +`getZoomAnchor` returns `ZoomAnchor` . + +## Usage {#usage} + + +### Basic usage {#basic} + \ No newline at end of file diff --git a/docs/en-US/api/instance/setZoomAnchor.md b/docs/en-US/api/instance/setZoomAnchor.md new file mode 100644 index 000000000..604e5dcb1 --- /dev/null +++ b/docs/en-US/api/instance/setZoomAnchor.md @@ -0,0 +1,23 @@ +--- +outline: deep +--- + +# setZoomAnchor(anchor) +`setZoomAnchor` set zoom anchor point for main and xAxis panes. + +## Reference {#reference} + + +### Parameters {#parameters} +- `anchor: ZoomAnchor` zoom anchor point type for main and xAxis pane. + +### Returns {#returns} +`setZoomAnchor` returns `void` . + +## Usage {#usage} + + +### Basic usage {#basic} + \ No newline at end of file diff --git a/src/Chart.ts b/src/Chart.ts index 43a89a4d1..05b30a60c 100644 --- a/src/Chart.ts +++ b/src/Chart.ts @@ -25,7 +25,7 @@ import type Crosshair from './common/Crosshair' import type { ActionType, ActionCallback } from './common/Action' import type { DataLoader } from './common/DataLoader' import type VisibleRange from './common/VisibleRange' -import type { Formatter, DecimalFold, LayoutChild, Options, ThousandsSeparator } from './Options' +import type { Formatter, DecimalFold, LayoutChild, Options, ThousandsSeparator, ZoomAnchor } from './Options' import Animation from './common/Animation' import { createId } from './common/utils/id' import { createDom } from './common/utils/dom' @@ -969,6 +969,14 @@ export default class ChartImp implements Chart { return this._chartStore.isZoomEnabled() } + setZoomAnchor (anchor: ZoomAnchor): void { + this._chartStore.setZoomAnchor(anchor) + } + + zoomAnchor (): ZoomAnchor { + return this._chartStore.zoomAnchor() + } + setScrollEnabled (enabled: boolean): void { this._chartStore.setScrollEnabled(enabled) } diff --git a/src/Event.ts b/src/Event.ts index 07ff451ad..1cb484d07 100644 --- a/src/Event.ts +++ b/src/Event.ts @@ -72,11 +72,11 @@ export default class Event implements EventHandler { if (event.shiftKey) { switch (event.code) { case 'Equal': { - this._chart.getChartStore().zoom(0.5) + this._chart.getChartStore().zoom(0.5, this._chart.getChartStore().zoomAnchor().main === 'last_bar' ? this.lastDataCoordinate() : undefined) break } case 'Minus': { - this._chart.getChartStore().zoom(-0.5) + this._chart.getChartStore().zoom(-0.5, this._chart.getChartStore().zoomAnchor().main === 'last_bar' ? this.lastDataCoordinate() : undefined) break } case 'ArrowLeft': { @@ -108,6 +108,11 @@ export default class Event implements EventHandler { container.addEventListener('keydown', this._boundKeyBoardDownEvent) } + private lastDataCoordinate (): Partial { + const store = this._chart.getChartStore() + return { x: store.dataIndexToCoordinate(store.getDataList().length - 1) } + } + pinchStartEvent (): boolean { this._touchZoomed = true this._pinchScale = 1 @@ -120,7 +125,7 @@ export default class Event implements EventHandler { const event = this._makeWidgetEvent(e, widget) const zoomScale = (scale - this._pinchScale) * 5 this._pinchScale = scale - this._chart.getChartStore().zoom(zoomScale, { x: event.x, y: event.y }) + this._chart.getChartStore().zoom(zoomScale, this._chart.getChartStore().zoomAnchor().main === 'last_bar' ? this.lastDataCoordinate() : { x: event.x, y: event.y }) return true } return false @@ -138,7 +143,7 @@ export default class Event implements EventHandler { const event = this._makeWidgetEvent(e, widget) const name = widget?.getName() if (name === WidgetNameConstants.MAIN) { - this._chart.getChartStore().zoom(scale, { x: event.x, y: event.y }) + this._chart.getChartStore().zoom(scale, this._chart.getChartStore().zoomAnchor().main === 'last_bar' ? this.lastDataCoordinate() : { x: event.x, y: event.y }) return true } return false @@ -583,7 +588,7 @@ export default class Event implements EventHandler { if (Number.isFinite(scale)) { const zoomScale = (scale - this._xAxisScale) * 10 this._xAxisScale = scale - this._chart.getChartStore().zoom(zoomScale, this._xAxisStartScaleCoordinate ?? undefined) + this._chart.getChartStore().zoom(zoomScale, this._chart.getChartStore().zoomAnchor().xAxis === 'last_bar' ? this.lastDataCoordinate() : (this._xAxisStartScaleCoordinate ?? undefined)) } } } else { diff --git a/src/Options.ts b/src/Options.ts index 413e1c4f9..a64712e17 100644 --- a/src/Options.ts +++ b/src/Options.ts @@ -85,6 +85,11 @@ export interface ThousandsSeparator { format: (value: string | number) => string } +export interface ZoomAnchor { + main: 'cursor_point' | 'last_bar' + xAxis: 'cursor_point' | 'last_bar' +} + export interface Options { locale?: string timezone?: string @@ -93,4 +98,5 @@ export interface Options { thousandsSeparator?: Partial decimalFold?: Partial layout?: LayoutChild[] + zoomAnchor?: Partial } diff --git a/src/Store.ts b/src/Store.ts index fc5a4afae..bbd5d4a50 100644 --- a/src/Store.ts +++ b/src/Store.ts @@ -37,7 +37,7 @@ import { logWarn } from './common/utils/logger' import { UpdateLevel } from './common/Updater' import type { DataLoader, DataLoaderGetBarsParams, DataLoadMore, DataLoadType } from './common/DataLoader' -import type { Options, Formatter, ThousandsSeparator, DecimalFold, FormatDateType, FormatDateParams, FormatBigNumber, FormatExtendText, FormatExtendTextParams } from './Options' +import type { Options, Formatter, ThousandsSeparator, DecimalFold, FormatDateType, FormatDateParams, FormatBigNumber, FormatExtendText, FormatExtendTextParams, ZoomAnchor } from './Options' import type { IndicatorOverride, IndicatorCreate, IndicatorFilter } from './component/Indicator' import type IndicatorImp from './component/Indicator' @@ -122,6 +122,8 @@ export interface Store { removeOverlay: (filter?: OverlayFilter) => boolean setZoomEnabled: (enabled: boolean) => void isZoomEnabled: () => boolean + setZoomAnchor: (behavior: ZoomAnchor) => void + zoomAnchor: () => ZoomAnchor setScrollEnabled: (enabled: boolean) => void isScrollEnabled: () => boolean resetData: () => void @@ -222,6 +224,14 @@ export default class StoreImp implements Store { */ private _zoomEnabled = true + /** + * Zoom anchor point flag + */ + private readonly _zoomAnchor: ZoomAnchor = { + main: 'cursor_point', + xAxis: 'cursor_point' + } + /** * Scroll enabled flag */ @@ -359,7 +369,7 @@ export default class StoreImp implements Store { this._chart = chart this._calcOptimalBarSpace() this._lastBarRightSideDiffBarCount = this._offsetRightDistance / this._barSpace - const { styles, locale, timezone, formatter, thousandsSeparator, decimalFold } = options ?? {} + const { styles, locale, timezone, formatter, thousandsSeparator, decimalFold, zoomAnchor } = options ?? {} if (isValid(styles)) { this.setStyles(styles) } @@ -376,6 +386,9 @@ export default class StoreImp implements Store { if (isValid(decimalFold)) { this.setDecimalFold(decimalFold) } + if (isValid(zoomAnchor)) { + this.setZoomAnchor(zoomAnchor) + } } setStyles (value: string | DeepPartial): void { @@ -1035,6 +1048,19 @@ export default class StoreImp implements Store { return this._zoomEnabled } + setZoomAnchor (anchor: Partial): void { + if (isValid(anchor.main) && isString(anchor.main)) { + this._zoomAnchor.main = anchor.main + } + if (isValid(anchor.xAxis) && isString(anchor.xAxis)) { + this._zoomAnchor.xAxis = anchor.xAxis + } + } + + zoomAnchor (): ZoomAnchor { + return { ...this._zoomAnchor } + } + setScrollEnabled (enabled: boolean): void { this._scrollEnabled = enabled } diff --git a/src/index.ts b/src/index.ts index d89833c04..672f722c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,7 +48,7 @@ import type { ActionType } from './common/Action' import type { IndicatorSeries } from './component/Indicator' import type { OverlayMode } from './component/Overlay' -import type { FormatDateType, Options } from './Options' +import type { FormatDateType, Options, ZoomAnchor } from './Options' import ChartImp, { type Chart, type DomPosition } from './Chart' import { checkCoordinateOnArc } from './extension/figure/arc' @@ -174,6 +174,6 @@ export { registerXAxis, registerYAxis, utils, type LineType, type PolygonType, type TooltipShowRule, type TooltipShowType, type FeatureType, type TooltipFeaturePosition, type CandleTooltipRectPosition, - type CandleType, type FormatDateType, + type CandleType, type FormatDateType, type ZoomAnchor, type DomPosition, type ActionType, type IndicatorSeries, type OverlayMode }