From 73184276772547c2dc51ea9b426eb900ecf681bf Mon Sep 17 00:00:00 2001 From: "lixuefei.1313" Date: Fri, 22 Aug 2025 11:32:43 +0800 Subject: [PATCH] Revert "Revert "add candlestick chart in vchart-extension"" This reverts commit d197e48be7134037f83e632e5cb236fca160cc04. This re-applies the changes from PR #4090, which were previously reverted by commit d197e48. --- docs/assets/examples/menu.json | 30 +++ .../zh/candlestick-chart/candlestick-basic.md | 54 ++++ .../candlestick-chart/candlestick-with-MA.md | 108 ++++++++ .../candlestick-with-volume.md | 123 +++++++++ .../runtime/browser/test-page/candlestick.ts | 44 ++++ .../candlestick/candlestick-transformer.ts | 37 +++ .../src/charts/candlestick/candlestick.ts | 34 +++ .../src/charts/candlestick/index.ts | 3 + .../src/charts/candlestick/interface.ts | 8 + .../charts/candlestick/mark/candlestick.ts | 77 ++++++ .../src/charts/candlestick/mark/interface.ts | 28 ++ .../charts/candlestick/series/animation.ts | 145 ++++++++++ .../charts/candlestick/series/candlestick.ts | 248 ++++++++++++++++++ .../src/charts/candlestick/series/constant.ts | 21 ++ .../charts/candlestick/series/interface.ts | 58 ++++ .../src/charts/candlestick/series/theme.ts | 32 +++ .../candlestick/series/tooltip-helper.ts | 88 +++++++ packages/vchart-extension/src/index.ts | 1 + packages/vchart/src/animation/index.ts | 3 +- packages/vchart/src/chart/index.ts | 2 + packages/vchart/src/index.ts | 1 + packages/vchart/src/mark/index.ts | 5 +- packages/vchart/src/series/index.ts | 1 + 23 files changed, 1149 insertions(+), 2 deletions(-) create mode 100644 docs/assets/examples/zh/candlestick-chart/candlestick-basic.md create mode 100644 docs/assets/examples/zh/candlestick-chart/candlestick-with-MA.md create mode 100644 docs/assets/examples/zh/candlestick-chart/candlestick-with-volume.md create mode 100644 packages/vchart-extension/__tests__/runtime/browser/test-page/candlestick.ts create mode 100644 packages/vchart-extension/src/charts/candlestick/candlestick-transformer.ts create mode 100644 packages/vchart-extension/src/charts/candlestick/candlestick.ts create mode 100644 packages/vchart-extension/src/charts/candlestick/index.ts create mode 100644 packages/vchart-extension/src/charts/candlestick/interface.ts create mode 100644 packages/vchart-extension/src/charts/candlestick/mark/candlestick.ts create mode 100644 packages/vchart-extension/src/charts/candlestick/mark/interface.ts create mode 100644 packages/vchart-extension/src/charts/candlestick/series/animation.ts create mode 100644 packages/vchart-extension/src/charts/candlestick/series/candlestick.ts create mode 100644 packages/vchart-extension/src/charts/candlestick/series/constant.ts create mode 100644 packages/vchart-extension/src/charts/candlestick/series/interface.ts create mode 100644 packages/vchart-extension/src/charts/candlestick/series/theme.ts create mode 100644 packages/vchart-extension/src/charts/candlestick/series/tooltip-helper.ts diff --git a/docs/assets/examples/menu.json b/docs/assets/examples/menu.json index 1971269e40..56a82f9080 100644 --- a/docs/assets/examples/menu.json +++ b/docs/assets/examples/menu.json @@ -2001,6 +2001,36 @@ } ] }, + { + "path": "candlestick-chart", + "title": { + "zh": "k线图", + "en": "candlestick-chart" + }, + "children": [ + { + "path": "candlestick-basic", + "title": { + "zh": "基础K线图", + "en": "Basic Candlestick Chart" + } + }, + { + "path": "candlestick-with-volume", + "title": { + "zh": "K线图与成交量", + "en": "Candlestick Chart with Volume" + } + }, + { + "path": "candlestick-with-MA", + "title": { + "zh": "K线图与日均线", + "en": "Candlestick Chart with MA" + } + } + ] + }, { "path": "chart-3d", "title": { diff --git a/docs/assets/examples/zh/candlestick-chart/candlestick-basic.md b/docs/assets/examples/zh/candlestick-chart/candlestick-basic.md new file mode 100644 index 0000000000..1622fca5c4 --- /dev/null +++ b/docs/assets/examples/zh/candlestick-chart/candlestick-basic.md @@ -0,0 +1,54 @@ +--- +category: examples +group: candlestick chart +title: 基础k线图 +keywords: candlestick +order: 19-0 +option: candlestickChart +--- + +# K 线图 + +K 线图基本用法 + +## 关键配置 + +## 代码演示 + +```javascript livedemo +if (VChartExtension.registerCandlestickChart) { + VChartExtension.registerCandlestickChart(); +} + +const data = [ + { time: '2024-07-01', open: 100, close: 130, high: 135, low: 90 }, + { time: '2024-07-02', open: 130, close: 80, high: 140, low: 75 }, + { time: '2024-07-03', open: 80, close: 150, high: 155, low: 70 }, + { time: '2024-07-04', open: 150, close: 140, high: 160, low: 105 }, + { time: '2024-07-05', open: 140, close: 170, high: 180, low: 115 }, + { time: '2024-07-06', open: 170, close: 170, high: 175, low: 95 }, + { time: '2024-07-07', open: 170, close: 100, high: 175, low: 95 }, + { time: '2024-07-08', open: 100, close: 160, high: 210, low: 90 } +]; + +const spec = { + type: 'candlestick', + xField: 'time', + openField: 'open', + closeField: 'close', + highField: 'high', + lowField: 'low', + data: [ + { + values: data + } + ] +}; + +const vchart = new VChart(spec, { + dom: CONTAINER_ID +}); +vchart.renderSync(); + +window['vchart'] = vchart; +``` diff --git a/docs/assets/examples/zh/candlestick-chart/candlestick-with-MA.md b/docs/assets/examples/zh/candlestick-chart/candlestick-with-MA.md new file mode 100644 index 0000000000..5040812603 --- /dev/null +++ b/docs/assets/examples/zh/candlestick-chart/candlestick-with-MA.md @@ -0,0 +1,108 @@ +--- +category: examples +group: candlestick chart +title: k线图组合显示 +keywords: candlestick MA +order: 19-0 +option: candlestickChart +--- + +# K 线图与均线组合 + +K 线图基本用法 + +## 关键配置 + +- `type: 'candlestick'` +- `xField`, `openField`, `closeField`, `highField`, `lowField` +- `data` + +## 代码演示 + +```javascript livedemo +if (VChartExtension.registerCandlestickChart) { + VChartExtension.registerCandlestickChart(); +} + +const data = [ + { time: '2024-07-01', open: 100, close: 130, high: 135, low: 90 }, + { time: '2024-07-02', open: 130, close: 80, high: 140, low: 75 }, + { time: '2024-07-03', open: 80, close: 150, high: 155, low: 70 }, + { time: '2024-07-04', open: 150, close: 140, high: 160, low: 105 }, + { time: '2024-07-05', open: 140, close: 170, high: 180, low: 115 }, + { time: '2024-07-06', open: 170, close: 170, high: 175, low: 95 }, + { time: '2024-07-07', open: 170, close: 100, high: 175, low: 95 }, + { time: '2024-07-08', open: 100, close: 160, high: 210, low: 90 }, + { time: '2024-07-09', open: 160, close: 180, high: 200, low: 150 }, + { time: '2024-07-10', open: 180, close: 175, high: 190, low: 160 }, + { time: '2024-07-11', open: 175, close: 190, high: 195, low: 170 }, + { time: '2024-07-12', open: 190, close: 210, high: 220, low: 185 }, + { time: '2024-07-13', open: 210, close: 200, high: 215, low: 195 }, + { time: '2024-07-14', open: 200, close: 220, high: 225, low: 198 }, + { time: '2024-07-15', open: 220, close: 230, high: 240, low: 215 } +]; + +function calcMA(data, window) { + const result = []; + for (let i = 0; i < data.length; i++) { + if (i < window - 1) { + result.push({ time: data[i].time, ma: null }); + } else { + let sum = 0; + for (let j = 0; j < window; j++) { + sum += data[i - j].close; + } + result.push({ time: data[i].time, ma: sum / window }); + } + } + return result; +} + +const spec = { + type: 'common', + data: [ + { id: 'k', values: data }, + { id: 'ma5', values: calcMA(data, 5) } + ], + series: [ + { + id: 'candlestick', + type: 'candlestick', + dataIndex: 0, + xField: 'time', + yField: ['open', 'close', 'high', 'low'], + openField: 'open', + closeField: 'close', + highField: 'high', + lowField: 'low' + }, + { + type: 'line', + id: 'line', + dataIndex: 1, + xField: 'time', + yField: 'ma', + line: { + style: { + stroke: 'rgb(229, 193, 160)', + lineWidth: 2 + } + }, + point: { + visible: false + } + } + ], + axes: [ + { orient: 'left', seriesIndex: [0, 1] }, + { orient: 'bottom', label: { visible: true }, type: 'band' } + ] +}; + +const vchart = new VChart(spec, { + dom: CONTAINER_ID +}); +vchart.renderSync(); + +window['vchart'] = vchart; +``` diff --git a/docs/assets/examples/zh/candlestick-chart/candlestick-with-volume.md b/docs/assets/examples/zh/candlestick-chart/candlestick-with-volume.md new file mode 100644 index 0000000000..12c510e612 --- /dev/null +++ b/docs/assets/examples/zh/candlestick-chart/candlestick-with-volume.md @@ -0,0 +1,123 @@ +--- +category: examples +group: candlestick chart +title: 基础k线图 +keywords: candlestick +order: 19-0 +option: candlestickChart +--- + +# K 线图 + +K 线图基本用法 + +## 关键配置 + +- `type: 'candlestick'` +- `xField`, `openField`, `closeField`, `highField`, `lowField` +- `data` + +## 代码演示 + +```javascript livedemo +if (VChartExtension.registerCandlestickChart) { + VChartExtension.registerCandlestickChart(); +} + +const data = [ + { time: '2024-07-01', open: 100, close: 130, high: 135, low: 90, volume: 5000 }, + { time: '2024-07-02', open: 130, close: 80, high: 140, low: 75, volume: 8000 }, + { time: '2024-07-03', open: 80, close: 150, high: 155, low: 70, volume: 6000 }, + { time: '2024-07-04', open: 150, close: 140, high: 160, low: 105, volume: 7000 }, + { time: '2024-07-05', open: 140, close: 170, high: 180, low: 115, volume: 9000 }, + { time: '2024-07-06', open: 170, close: 170, high: 175, low: 95, volume: 4000 }, + { time: '2024-07-07', open: 170, close: 100, high: 175, low: 95, volume: 10000 }, + { time: '2024-07-08', open: 100, close: 160, high: 210, low: 90, volume: 11000 }, + { time: '2024-07-09', open: 160, close: 180, high: 200, low: 150, volume: 9500 }, + { time: '2024-07-10', open: 180, close: 170, high: 185, low: 160, volume: 8700 }, + { time: '2024-07-11', open: 170, close: 200, high: 210, low: 165, volume: 12000 }, + { time: '2024-07-12', open: 200, close: 210, high: 220, low: 190, volume: 10500 }, + { time: '2024-07-13', open: 210, close: 190, high: 215, low: 180, volume: 9800 }, + { time: '2024-07-14', open: 190, close: 195, high: 200, low: 185, volume: 7600 }, + { time: '2024-07-15', open: 195, close: 220, high: 225, low: 190, volume: 13000 }, + { time: '2024-07-16', open: 220, close: 210, high: 230, low: 205, volume: 9000 } +]; + +const spec = { + type: 'common', + data: [{ values: data }], + layout: { + type: 'grid', + col: 2, + row: 3, + elements: [ + { modelId: 'region-k', col: 1, row: 0 }, + { modelId: 'region-volume', col: 1, row: 2 }, + { modelId: 'axis-x', col: 1, row: 1 }, + { modelId: 'axis-y-k', col: 0, row: 0 }, + { modelId: 'axis-y-volume', col: 0, row: 2 } + ] + }, + region: [{ id: 'region-k', height: 0.7 }, { id: 'region-volume' }], + padding: { + top: 10 + }, + series: [ + { + regionId: 'region-k', + type: 'candlestick', + xField: 'time', + yField: ['open', 'close', 'high', 'low'], + openField: 'open', + closeField: 'close', + highField: 'high', + lowField: 'low', + data: { values: data } + }, + { + regionId: 'region-volume', + type: 'bar', + xField: 'time', + yField: 'volume', + data: { values: data }, + bar: { + style: { + fill: (datum: Datum) => { + if (datum.open < datum.close) { + return '#FF0000'; + } else if (datum.open > datum.close) { + return '#00AA00'; + } else { + return '#000000'; + } + } + } + } + } + ], + axes: [ + { + id: 'axis-y-k', + regionId: 'region-k', + orient: 'left' + }, + { + id: 'axis-y-volume', + regionId: 'region-volume', + orient: 'left' + }, + { + id: 'axis-x', + regionId: ['region-volume', 'region-k'], + orient: 'bottom' + } + ] +}; + +const vchart = new VChart(spec, { + dom: CONTAINER_ID +}); +vchart.renderSync(); + +window['vchart'] = vchart; +``` diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/candlestick.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/candlestick.ts new file mode 100644 index 0000000000..321755b52e --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/candlestick.ts @@ -0,0 +1,44 @@ +import { ICandlestickChartSpec } from '../../../../src/charts/candlestick/interface'; +import { registerCandlestickChart } from '../../../../src/charts/candlestick/candlestick'; +import VChart from '@visactor/vchart'; + +const data = [ + { time: '2024-07-01', open: 100, close: 130, high: 135, low: 90 }, + { time: '2024-07-02', open: 130, close: 80, high: 140, low: 75 }, + { time: '2024-07-03', open: 80, close: 150, high: 155, low: 70 }, + { time: '2024-07-04', open: 150, close: 140, high: 160, low: 105 }, + { time: '2024-07-05', open: 140, close: 170, high: 180, low: 115 }, + { time: '2024-07-06', open: 170, close: 170, high: 175, low: 95 }, + { time: '2024-07-07', open: 170, close: 100, high: 175, low: 95 }, + { time: '2024-07-08', open: 100, close: 160, high: 210, low: 90 } +]; + +const spec: ICandlestickChartSpec = { + type: 'candlestick', + xField: 'time', + openField: 'open', + closeField: 'close', + highField: 'high', + lowField: 'low', + data: [ + { + values: data + } + ] +}; + +const run = () => { + registerCandlestickChart(); + const cs = new VChart(spec, { + dom: document.getElementById('chart') as HTMLElement, + onError: err => { + console.error(err); + } + }); + console.time('renderTime'); + cs.renderSync(); + console.timeEnd('renderTime'); + window['vchart'] = cs; + console.log(cs); +}; +run(); diff --git a/packages/vchart-extension/src/charts/candlestick/candlestick-transformer.ts b/packages/vchart-extension/src/charts/candlestick/candlestick-transformer.ts new file mode 100644 index 0000000000..4459601609 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/candlestick-transformer.ts @@ -0,0 +1,37 @@ +import { CartesianChartSpecTransformer, setDefaultCrosshairForCartesianChart } from '@visactor/vchart'; +import type { ICandlestickChartSpec } from './interface'; + +export class CandlestickChartSpecTransformer< + T extends ICandlestickChartSpec = ICandlestickChartSpec +> extends CartesianChartSpecTransformer { + protected _getDefaultSeriesSpec(spec: T): any { + const dataFields = [spec.openField, spec.highField, spec.lowField, spec.closeField]; + const seriesSpec = super._getDefaultSeriesSpec(spec, [ + 'candlestick', + 'openField', + 'highField', + 'lowField', + 'closeField', + 'rising', + 'falling', + 'doji' + ]); + seriesSpec.yField = dataFields; + return seriesSpec; + } + + transformSpec(spec: T): void { + super.transformSpec(spec); + if (!spec.axes) { + spec.axes = [ + { + orient: 'bottom' + }, + { + orient: 'left' + } + ]; + } + setDefaultCrosshairForCartesianChart(spec); + } +} diff --git a/packages/vchart-extension/src/charts/candlestick/candlestick.ts b/packages/vchart-extension/src/charts/candlestick/candlestick.ts new file mode 100644 index 0000000000..0685141c08 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/candlestick.ts @@ -0,0 +1,34 @@ +import { CandlestickChartSpecTransformer } from './candlestick-transformer'; +import { ICandlestickChartSpec } from './interface'; +import { registerCandlestickSeries } from './series/candlestick'; +import { + BaseChart, + Factory, + registerMarkTooltipProcessor, + registerDimensionTooltipProcessor, + registerDimensionEvents, + registerDimensionHover, + getCartesianDimensionInfo, + getDimensionInfoByValue, + getCartesianCrosshairRect +} from '@visactor/vchart'; +import { CANDLESTICK_CHART_TYPE, CANDLESTICK_SERIES_TYPE } from './series/constant'; +export class CandlestickChart extends BaseChart { + static readonly type: string = CANDLESTICK_CHART_TYPE; + static readonly seriesType: string = CANDLESTICK_SERIES_TYPE; + static readonly transformerConstructor = CandlestickChartSpecTransformer; // CandlestickChartSpecTransformer; + protected _setModelOption() { + this._modelOption.getDimensionInfo = getCartesianDimensionInfo; + this._modelOption.getDimensionInfoByValue = getDimensionInfoByValue; + this._modelOption.getRectByDimensionData = getCartesianCrosshairRect; + } +} + +export const registerCandlestickChart = () => { + registerDimensionTooltipProcessor(); + registerMarkTooltipProcessor(); + registerDimensionEvents(); + registerDimensionHover(); + registerCandlestickSeries(); + Factory.registerChart(CandlestickChart.type, CandlestickChart); +}; diff --git a/packages/vchart-extension/src/charts/candlestick/index.ts b/packages/vchart-extension/src/charts/candlestick/index.ts new file mode 100644 index 0000000000..96031419c2 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/index.ts @@ -0,0 +1,3 @@ +export * from './candlestick'; +export * from './interface'; +export * from './candlestick-transformer'; diff --git a/packages/vchart-extension/src/charts/candlestick/interface.ts b/packages/vchart-extension/src/charts/candlestick/interface.ts new file mode 100644 index 0000000000..5b9805d69e --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/interface.ts @@ -0,0 +1,8 @@ +import type { IChartExtendsSeriesSpec, ICartesianChartSpec } from '@visactor/vchart'; +import type { ICandlestickSeriesSpec } from './series/interface'; + +export interface ICandlestickChartSpec extends ICartesianChartSpec, IChartExtendsSeriesSpec { + type: 'candlestick'; + /** 系列配置 */ + series?: ICandlestickSeriesSpec[]; +} diff --git a/packages/vchart-extension/src/charts/candlestick/mark/candlestick.ts b/packages/vchart-extension/src/charts/candlestick/mark/candlestick.ts new file mode 100644 index 0000000000..e73199da8f --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/mark/candlestick.ts @@ -0,0 +1,77 @@ +import { registerLine, registerRect } from '@visactor/vrender-kits'; +import { GlyphMark, registerGlyphMark, IMarkRaw, IMarkStyle } from '@visactor/vchart'; +import { + createLine, + createRect, + type IGlyph, + type ILineGraphicAttribute, + IRectGraphicAttribute +} from '@visactor/vrender-core'; +import { Factory, Datum } from '@visactor/vchart'; +import type { ICandlestickMarkSpec } from './interface'; + +export type ICandlestickMark = IMarkRaw; +export const CANDLESTICK_MARK_TYPE = 'candlestick'; + +export class CandlestickMark extends GlyphMark implements ICandlestickMark { + static readonly type = CANDLESTICK_MARK_TYPE; + readonly type = CandlestickMark.type; + + setGlyphConfig(cfg: any): void { + super.setGlyphConfig(cfg); + this._subMarks = { + line: { type: 'line', defaultAttributes: { x: 0, y: 0 } }, + box: { type: 'rect' } + }; + this._positionChannels = ['x', 'boxWidth', 'open', 'close', 'high', 'low']; + this._channelEncoder = null; + this._positionEncoder = (glyphAttrs: any, datum: Datum, g: IGlyph) => { + const { + x = g.attribute.x, + boxWidth = (g.attribute as any).boxWidth, + open = (g.attribute as any).open, + close = (g.attribute as any).close, + low = (g.attribute as any).low, + high = (g.attribute as any).high + } = glyphAttrs; + const attributes: any = {}; + attributes.line = { + points: [ + { + x: x, + y: low + }, + { + x: x, + y: high + } + ] + }; + attributes.box = { + x: x - boxWidth / 2, + x1: x + boxWidth / 2, + y: Math.min(open, close), + y1: Math.max(open, close), + // 开盘收盘相同时绘制水平线 + drawStrokeWhenZeroWH: true + }; + return attributes; + }; + } + + protected _getDefaultStyle() { + const defaultStyle: IMarkStyle = { + ...super._getDefaultStyle() + }; + return defaultStyle; + } +} + +export const registerCandlestickMark = () => { + registerGlyphMark(); + registerLine(); + registerRect(); + Factory.registerGraphicComponent('line', (attrs: ILineGraphicAttribute) => createLine(attrs)); + Factory.registerGraphicComponent('rect', (attrs: IRectGraphicAttribute) => createRect(attrs)); + Factory.registerMark(CandlestickMark.type, CandlestickMark); +}; diff --git a/packages/vchart-extension/src/charts/candlestick/mark/interface.ts b/packages/vchart-extension/src/charts/candlestick/mark/interface.ts new file mode 100644 index 0000000000..a821c0a47e --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/mark/interface.ts @@ -0,0 +1,28 @@ +import type { Datum, ICommonSpec } from '@visactor/vchart'; + +export interface ICandlestickMarkSpec extends ICommonSpec { + /** + * box宽度 + */ + boxWidth?: number; + /** + * 盒子填充颜色,为空则不填充 + */ + boxFill?: string | ((datum: Datum) => string); + /** + * 最低价 + */ + low?: (datum: Datum) => number; + /** + * 收盘价 + */ + close?: (datum: Datum) => number; + /** + * 开盘价 + */ + open?: (datum: Datum) => number; + /** + * 最高价 + */ + high?: (datum: Datum) => number; +} diff --git a/packages/vchart-extension/src/charts/candlestick/series/animation.ts b/packages/vchart-extension/src/charts/candlestick/series/animation.ts new file mode 100644 index 0000000000..42c5050a65 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/series/animation.ts @@ -0,0 +1,145 @@ +import type { EasingType } from '@visactor/vrender-core'; +import type { IGlyph } from '@visactor/vrender-core'; +import type { IAnimationParameters } from '@visactor/vchart'; +import { isValidNumber } from '@visactor/vutils'; +import { ACustomAnimate, AnimateExecutor } from '@visactor/vrender-animate'; + +export interface ICandlestickScaleAnimationOptions { + center?: number; +} + +type TypeAnimation = ( + graphic: IGlyph, + options: ICandlestickScaleAnimationOptions, + animationParameters: IAnimationParameters +) => { from?: { [channel: string]: any }; to?: { [channel: string]: any } }; + +const scaleIn = (): TypeAnimation => { + return (graphic: IGlyph) => { + const finalAttribute = graphic.getFinalAttribute(); + const { x, y, open, high, low, close } = finalAttribute; + const animateAttributes: any = { from: { x, y }, to: { x, y } }; + if (isValidNumber(open) && isValidNumber(close)) { + if (open > close) { + animateAttributes.from.open = low; + animateAttributes.to.open = open; + animateAttributes.from.close = low; + animateAttributes.to.close = close; + if (isValidNumber(high)) { + animateAttributes.from.high = low; + animateAttributes.to.high = high; + } + } else { + animateAttributes.from.open = high; + animateAttributes.to.open = open; + animateAttributes.from.close = high; + animateAttributes.to.close = close; + if (isValidNumber(low)) { + animateAttributes.from.low = high; + animateAttributes.to.low = low; + } + } + } + return animateAttributes; + }; +}; + +const scaleOut = (): TypeAnimation => { + return (graphic: IGlyph) => { + const finalAttribute = graphic.getFinalAttribute(); + const { x, y, open, high, low, close } = finalAttribute; + + const animateAttributes: any = { from: { x, y }, to: { x, y } }; + if (isValidNumber(open) && isValidNumber(close)) { + if (open > close) { + animateAttributes.from.open = open; + animateAttributes.to.open = low; + animateAttributes.from.close = close; + animateAttributes.to.close = low; + if (isValidNumber(high)) { + animateAttributes.from.high = high; + animateAttributes.to.high = low; + } + } else { + animateAttributes.from.open = open; + animateAttributes.to.open = high; + animateAttributes.from.close = close; + animateAttributes.to.close = high; + if (isValidNumber(low)) { + animateAttributes.from.low = low; + animateAttributes.to.low = high; + } + } + } + return animateAttributes; + }; +}; + +export class CandlestickScaleIn extends ACustomAnimate> { + constructor(from: null, to: null, duration: number, easing: EasingType, params?: ICandlestickScaleAnimationOptions) { + super(from, to, duration, easing, params); + } + + onBind(): void { + super.onBind(); + const finalAttribute = this.target.getFinalAttribute(); + if (finalAttribute) { + this.target.setAttributes(finalAttribute); + } + const { from, to } = this.computeAttribute(); + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = from; + this.to = to; + this.target.setAttributes(this.from); + } + + computeAttribute() { + const attr = scaleIn()(this.target as IGlyph, this.params, this.params.options); + return attr; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; + this.propKeys.forEach(key => { + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(attribute); + } +} + +export class CandlestickScaleOut extends ACustomAnimate> { + constructor(from: null, to: null, duration: number, easing: EasingType, params?: ICandlestickScaleAnimationOptions) { + super(from, to, duration, easing, params); + } + + onBind(): void { + if (this.params?.diffAttrs) { + this.target.setAttributes(this.params.diffAttrs); + } + const { from, to } = this.computeAttribute(); + this.propKeys = Object.keys(to).filter(key => to[key] != null); + this.animate.reSyncProps(); + this.from = from; + this.to = to; + this.target.setAttributes(this.from); + } + + computeAttribute() { + const attr = scaleOut()(this.target as IGlyph, this.params, this.params.options); + return attr; + } + + onUpdate(end: boolean, ratio: number, out: Record): void { + const attribute: Record = this.target.attribute; + this.propKeys.forEach(key => { + attribute[key] = this.from[key] + (this.to[key] - this.from[key]) * ratio; + }); + this.target.setAttributes(attribute); + } +} + +export const registerCandlestickScaleAnimation = () => { + AnimateExecutor.registerBuiltInAnimate('candlestickScaleIn', CandlestickScaleIn); + AnimateExecutor.registerBuiltInAnimate('candlestickScaleOut', CandlestickScaleOut); +}; diff --git a/packages/vchart-extension/src/charts/candlestick/series/candlestick.ts b/packages/vchart-extension/src/charts/candlestick/series/candlestick.ts new file mode 100644 index 0000000000..d815c954c4 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/series/candlestick.ts @@ -0,0 +1,248 @@ +import { registerCandlestickMark, ICandlestickMark } from '../mark/candlestick'; +import { + registerSymbolMark, + registerScaleInOutAnimation, + registerCartesianBandAxis, + registerCartesianLinearAxis, + CartesianSeries, + IMark, + Factory, + STATE_VALUE_ENUM, + AttributeLevel, + Datum, + IModelInitOption, + getGroupAnimationParams, + animationConfig, + userAnimationConfig +} from '@visactor/vchart'; +import { valueInScaleRange } from '@visactor/vchart'; +import { IGlyphMark } from '@visactor/vchart'; +import { merge } from '@visactor/vutils'; +import type { ICandlestickSeriesSpec } from './interface'; +import { registerCandlestickScaleAnimation } from './animation'; +import { CANDLESTICK_SERIES_TYPE, CandlestickSeriesMark } from './constant'; +import { CandlestickSeriesTooltipHelper } from './tooltip-helper'; +import { candlestick } from './theme'; + +export const DEFAULT_STROKE_COLOR = '#000'; +export class CandlestickSeries extends CartesianSeries { + static readonly type: string = CANDLESTICK_SERIES_TYPE; + type = CANDLESTICK_SERIES_TYPE; + + static readonly builtInTheme = { candlestick }; + static readonly mark = CandlestickSeriesMark; + protected _openField: string; + getOpenField(): string { + return this._openField; + } + protected _highField: string; + getHighField(): string { + return this._highField; + } + protected _lowField: string; + getLowField(): string { + return this._lowField; + } + protected _closeField: string; + getCloseField(): string { + return this._closeField; + } + protected _boxWidth: number; + protected _boxFill: string | ((datum: Datum) => string); + getBoxFill(): string | ((datum: Datum) => string) { + return this._boxFill; + } + protected _strokeColor: string; + getStrokeColor(): string { + return this._strokeColor; + } + private _autoBoxWidth: number; + private _mergedStyles: { rising: any; falling: any; doji: any } = { + rising: {}, + falling: {}, + doji: {} + }; + + setAttrFromSpec() { + super.setAttrFromSpec(); + const spec = this._spec; + const CandlestickStyle: any = spec.candlestick?.style ?? {}; + this._openField = spec.openField; + this._highField = spec.highField; + this._lowField = spec.lowField; + this._closeField = spec.closeField; + this._boxWidth = CandlestickStyle.boxWidth; + this._boxFill = CandlestickStyle.boxFill; + this._strokeColor = CandlestickStyle.strokeColor; + this._buildMergedStyles( + CandlestickStyle, + spec.rising?.style ?? {}, + spec.falling?.style ?? {}, + spec.doji?.style ?? {} + ); + } + + private _candlestickMark?: ICandlestickMark; + + initMark(): void { + this._candlestickMark = this._createMark(CandlestickSeries.mark.candlestick, { + groupKey: this._seriesField, + isSeriesMark: true + }) as ICandlestickMark; + } + + initMarkStyle(): void { + const candlestickMark = this._candlestickMark; + if (candlestickMark) { + const CandlestickStyles = { + fill: (datum: Datum) => { + const boxFill = this.mergeStyle(datum).boxFill; + return boxFill; + }, + stroke: (datum: Datum) => { + const strokeColor = this.mergeStyle(datum).stroke; + return strokeColor; + }, + lineWidth: (datum: Datum) => { + const lineWidth = this.mergeStyle(datum).lineWidth; + return lineWidth; + }, + boxWidth: this._boxWidth ?? this._getMarkWidth.bind(this), + x: this.dataToPositionX.bind(this) + }; + (candlestickMark as IGlyphMark).setGlyphConfig({}); + this.setMarkStyle(candlestickMark, CandlestickStyles, STATE_VALUE_ENUM.STATE_NORMAL, AttributeLevel.Series); + } + } + + initCandlestickMarkStyle() { + const candlestickMark = this._candlestickMark; + const axisHelper = this._yAxisHelper; + if (candlestickMark && axisHelper) { + const { dataToPosition } = axisHelper; + const scale = axisHelper?.getScale?.(0); + this.setMarkStyle( + candlestickMark, + { + open: (datum: Datum) => + valueInScaleRange( + dataToPosition(this.getDatumPositionValues(datum, this._openField), { + bandPosition: this._bandPosition + }), + scale + ), + high: (datum: Datum) => + valueInScaleRange( + dataToPosition(this.getDatumPositionValues(datum, this._highField), { + bandPosition: this._bandPosition + }), + scale + ), + low: (datum: Datum) => + valueInScaleRange( + dataToPosition(this.getDatumPositionValues(datum, this._lowField), { + bandPosition: this._bandPosition + }), + scale + ), + close: (datum: Datum) => + valueInScaleRange( + dataToPosition(this.getDatumPositionValues(datum, this._closeField), { + bandPosition: this._bandPosition + }), + scale + ) + }, + STATE_VALUE_ENUM.STATE_NORMAL, + AttributeLevel.Series + ); + } + } + + init(option: IModelInitOption): void { + super.init(option); + //init在axis初始化之后才被执行,此时axisHelper不为空 + this.initCandlestickMarkStyle(); + } + + private _initAnimationSpec(config: any = {}) { + const newConfig = merge({}, config); + ['appear', 'enter', 'update', 'exit', 'disappear'].forEach(state => { + if (newConfig[state] && newConfig[state].type === 'scaleIn') { + newConfig[state].type = 'candlestickScaleIn'; + } else if (newConfig[state] && newConfig[state].type === 'scaleOut') { + newConfig[state].type = 'candlestickScaleOut'; + } + }); + return newConfig; + } + + initAnimation() { + const animationParams = getGroupAnimationParams(this); + + if (this._candlestickMark) { + const newDefaultConfig = this._initAnimationSpec(Factory.getAnimationInKey('scaleInOut')?.()); + const newConfig = this._initAnimationSpec( + userAnimationConfig(CANDLESTICK_SERIES_TYPE, this._spec, this._markAttributeContext) + ); + this._candlestickMark.setAnimationConfig(animationConfig(newDefaultConfig, newConfig, animationParams)); + } + } + + protected initTooltip() { + this._tooltipHelper = new CandlestickSeriesTooltipHelper(this); + this._candlestickMark && this._tooltipHelper.activeTriggerSet.mark.add(this._candlestickMark); + } + + private _buildMergedStyles(baseStyle: any, risingStyle: any, fallingStyle: any, dojiStyle: any) { + this._mergedStyles.rising = merge({}, baseStyle, risingStyle); + this._mergedStyles.falling = merge({}, baseStyle, fallingStyle); + this._mergedStyles.doji = merge({}, baseStyle, dojiStyle); + } + + protected mergeStyle(datum: Datum): any { + const open = this.getDatumPositionValues(datum, this._openField)[0]; + const close = this.getDatumPositionValues(datum, this._closeField)[0]; + if (open < close) { + return this._mergedStyles.rising; + } else if (open > close) { + return this._mergedStyles.falling; + } else { + return this._mergedStyles.doji; + } + } + + private _getMarkWidth() { + if (this._autoBoxWidth) { + return this._autoBoxWidth; + } + //获取自适应的图元宽度 + const bandAxisHelper = this._xAxisHelper; + const xField = this._fieldX; + + const innerBandWidth = bandAxisHelper.getBandwidth(xField.length - 1); + const autoBoxWidth = innerBandWidth / xField.length; + this._autoBoxWidth = autoBoxWidth; + + return this._autoBoxWidth; + } + + onLayoutEnd() { + super.onLayoutEnd(); + this._autoBoxWidth = null; + } + + getActiveMarks(): IMark[] { + return [this._candlestickMark]; + } +} + +export const registerCandlestickSeries = () => { + registerCandlestickMark(); + registerSymbolMark(); + registerScaleInOutAnimation(); + registerCartesianBandAxis(); + registerCartesianLinearAxis(); + registerCandlestickScaleAnimation(); + Factory.registerSeries(CandlestickSeries.type, CandlestickSeries); +}; diff --git a/packages/vchart-extension/src/charts/candlestick/series/constant.ts b/packages/vchart-extension/src/charts/candlestick/series/constant.ts new file mode 100644 index 0000000000..1164265322 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/series/constant.ts @@ -0,0 +1,21 @@ +import { baseSeriesMark } from '@visactor/vchart'; + +export const CANDLESTICK_CHART_TYPE = 'candlestick'; +export const CANDLESTICK_SERIES_TYPE = 'candlestick'; + +export enum CANDLESTICK_TOOLTIP_KEYS { + OPEN = 'open', + HIGH = 'high', + LOW = 'low', + CLOSE = 'close', + SERIES_FIELD = 'seriesField' +} + +export const enum CandlestickMarkNameEnum { + candlestick = 'candlestick' +} + +export const CandlestickSeriesMark = { + ...baseSeriesMark, + [CandlestickMarkNameEnum.candlestick]: { name: CandlestickMarkNameEnum.candlestick, type: 'candlestick' } +}; diff --git a/packages/vchart-extension/src/charts/candlestick/series/interface.ts b/packages/vchart-extension/src/charts/candlestick/series/interface.ts new file mode 100644 index 0000000000..b60a1bccc6 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/series/interface.ts @@ -0,0 +1,58 @@ +import type { + IAnimationSpec, + IMarkSpec, + ICartesianSeriesSpec, + SeriesMarkNameEnum, + IMarkTheme, + ICartesianSeriesTheme +} from '@visactor/vchart'; +import type { ICandlestickMarkSpec } from '../mark/interface'; + +export interface ICandlestickSeriesSpec + extends Omit, + IAnimationSpec { + type: 'candlestick'; + /** + * 时间轴字段 + */ + xField: string | string[]; + /** + * 开盘价字段 + */ + openField?: string; + /** + * 最高价字段 + */ + highField?: string; + /** + * 最低价字段 + */ + lowField?: string; + /** + * 收盘价字段 + */ + closeField?: string; + /** + * 上涨蜡烛图颜色 + */ + rising?: IMarkSpec; + /** + * 下跌蜡烛图颜色 + */ + falling?: IMarkSpec; + /** + * 平盘蜡烛图颜色 + */ + doji?: IMarkSpec; + /** + * 蜡烛图标记配置 + */ + candlestick?: IMarkSpec; +} + +export interface ICandlestickSeriesTheme extends ICartesianSeriesTheme { + candlestick?: Partial>; + rising?: Partial>; + falling?: Partial>; + doji?: Partial>; +} diff --git a/packages/vchart-extension/src/charts/candlestick/series/theme.ts b/packages/vchart-extension/src/charts/candlestick/series/theme.ts new file mode 100644 index 0000000000..4cdacbd367 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/series/theme.ts @@ -0,0 +1,32 @@ +import type { ICandlestickSeriesTheme } from '../series/interface'; + +export const getCandlestickTheme = (): ICandlestickSeriesTheme => { + const res: ICandlestickSeriesTheme = { + rising: { + style: { + boxFill: '#FF0000', + stroke: '#FF0000' + } + }, + falling: { + style: { + boxFill: '#00AA00', + stroke: '#00AA00' + } + }, + doji: { + style: { + boxFill: '#000000', + stroke: '#000000' + } + }, + candlestick: { + style: { + lineWidth: 1 + } + } + }; + return res; +}; + +export const candlestick: ICandlestickSeriesTheme = getCandlestickTheme(); diff --git a/packages/vchart-extension/src/charts/candlestick/series/tooltip-helper.ts b/packages/vchart-extension/src/charts/candlestick/series/tooltip-helper.ts new file mode 100644 index 0000000000..03b886a9ba --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/series/tooltip-helper.ts @@ -0,0 +1,88 @@ +import type { ISeriesTooltipHelper, Datum, ITooltipLinePattern, TooltipActiveType } from '@visactor/vchart'; +import { BaseSeriesTooltipHelper } from '@visactor/vchart'; +import { CANDLESTICK_TOOLTIP_KEYS } from './constant'; +import type { CandlestickSeries } from './candlestick'; + +export class CandlestickSeriesTooltipHelper extends BaseSeriesTooltipHelper implements ISeriesTooltipHelper { + /** 获取默认的tooltip pattern */ + protected getDefaultContentList(activeType: TooltipActiveType): ITooltipLinePattern[] { + return [ + { + key: this.getContentKey(CANDLESTICK_TOOLTIP_KEYS.OPEN), + value: this.getContentValue(CANDLESTICK_TOOLTIP_KEYS.OPEN) + }, + { + key: this.getContentKey(CANDLESTICK_TOOLTIP_KEYS.HIGH), + value: this.getContentValue(CANDLESTICK_TOOLTIP_KEYS.HIGH) + }, + { + key: this.getContentKey(CANDLESTICK_TOOLTIP_KEYS.LOW), + value: this.getContentValue(CANDLESTICK_TOOLTIP_KEYS.LOW) + }, + { + key: this.getContentKey(CANDLESTICK_TOOLTIP_KEYS.CLOSE), + value: this.getContentValue(CANDLESTICK_TOOLTIP_KEYS.CLOSE) + }, + { + key: this.getContentKey(CANDLESTICK_TOOLTIP_KEYS.SERIES_FIELD), + value: this.getContentValue(CANDLESTICK_TOOLTIP_KEYS.SERIES_FIELD) + } + ]; + } + getContentKey = (contentType: CANDLESTICK_TOOLTIP_KEYS) => (datum: any) => { + switch (contentType) { + case CANDLESTICK_TOOLTIP_KEYS.OPEN: { + const openField = (this.series as CandlestickSeries).getOpenField(); + return openField; + } + case CANDLESTICK_TOOLTIP_KEYS.HIGH: { + const highField = (this.series as CandlestickSeries).getHighField(); + return highField; + } + case CANDLESTICK_TOOLTIP_KEYS.LOW: { + const lowField = (this.series as CandlestickSeries).getLowField(); + return lowField; + } + case CANDLESTICK_TOOLTIP_KEYS.CLOSE: { + const closeField = (this.series as CandlestickSeries).getCloseField(); + return closeField; + } + case CANDLESTICK_TOOLTIP_KEYS.SERIES_FIELD: { + const seriesField = (this.series as CandlestickSeries).getSeriesField(); + return seriesField; + } + } + + return null; + }; + + getContentValue = (contentType: CANDLESTICK_TOOLTIP_KEYS) => (datum: any) => { + switch (contentType) { + case CANDLESTICK_TOOLTIP_KEYS.OPEN: { + const openField = (this.series as CandlestickSeries).getOpenField(); + return datum[openField]; + } + case CANDLESTICK_TOOLTIP_KEYS.HIGH: { + const highField = (this.series as CandlestickSeries).getHighField(); + return datum[highField]; + } + case CANDLESTICK_TOOLTIP_KEYS.LOW: { + const lowField = (this.series as CandlestickSeries).getLowField(); + return datum[lowField]; + } + case CANDLESTICK_TOOLTIP_KEYS.CLOSE: { + const closeField = (this.series as CandlestickSeries).getCloseField(); + return datum[closeField]; + } + case CANDLESTICK_TOOLTIP_KEYS.SERIES_FIELD: { + const seriesField = (this.series as CandlestickSeries).getSeriesField(); + return datum[seriesField]; + } + } + + return null; + }; + shapeColorCallback = (datum: Datum) => { + return this.series.getMarkInName('candlestick').getAttribute('stroke' as any, datum) as any; + }; +} diff --git a/packages/vchart-extension/src/index.ts b/packages/vchart-extension/src/index.ts index 9e580565f6..24e6715dce 100644 --- a/packages/vchart-extension/src/index.ts +++ b/packages/vchart-extension/src/index.ts @@ -15,6 +15,7 @@ export * from './charts/range-column-3d'; export { register3DPlugin } from './charts/3d/plugin'; export * from './charts/pictogram'; export * from './charts/image-cloud'; +export * from './charts/candlestick'; export * from './components/series-break'; export * from './components/bar-link'; diff --git a/packages/vchart/src/animation/index.ts b/packages/vchart/src/animation/index.ts index 84cd5fac85..1305461a69 100644 --- a/packages/vchart/src/animation/index.ts +++ b/packages/vchart/src/animation/index.ts @@ -5,8 +5,9 @@ export { registerPolygonAnimation, registerRectAnimation, registerArcAnimation, + registerScaleInOutAnimation, DEFAULT_ANIMATION_CONFIG } from './config'; export { animationConfig, userAnimationConfig, shouldMarkDoMorph } from './utils'; export type { IAnimationSpec } from './spec'; -export type { IAnimationTypeConfig, IAnimationConfig } from './interface'; +export type { IAnimationTypeConfig, IAnimationConfig, IAnimationParameters } from './interface'; diff --git a/packages/vchart/src/chart/index.ts b/packages/vchart/src/chart/index.ts index f87027eb1a..4eca053d91 100644 --- a/packages/vchart/src/chart/index.ts +++ b/packages/vchart/src/chart/index.ts @@ -176,3 +176,5 @@ export type { IVennChartSpec, IMosaicChartSpec }; + +export { setDefaultCrosshairForCartesianChart } from './util'; diff --git a/packages/vchart/src/index.ts b/packages/vchart/src/index.ts index faeddf15d7..67948da7ff 100644 --- a/packages/vchart/src/index.ts +++ b/packages/vchart/src/index.ts @@ -27,6 +27,7 @@ export * from './util/data'; export * from './util/spec/transform'; export * from './util/mark'; export * from './util/region'; +export * from './util/scale'; // base component model for extension export * from './component/base'; diff --git a/packages/vchart/src/mark/index.ts b/packages/vchart/src/mark/index.ts index 4dfc85b3aa..c9cc10fc89 100644 --- a/packages/vchart/src/mark/index.ts +++ b/packages/vchart/src/mark/index.ts @@ -14,6 +14,7 @@ import { ComponentMark, registerComponentMark } from './component'; import { LinkPathMark, registerLinkPathMark } from './link-path'; import { RippleMark, registerRippleMark } from './ripple'; import { CellMark, registerCellMark } from './cell'; +import { GlyphMark, registerGlyphMark } from './glyph'; import { BaseMark } from './base'; import { PolygonMark, registerPolygonMark } from './polygon/polygon'; import { ImageMark, registerImageMark } from './image'; @@ -45,7 +46,7 @@ export type { } from '../typings/visual'; export type { IMarkRaw, IMark, IMarkStyle } from './interface/common'; -export type { ITextMark, ILabelMark, IRectMark, IRuleMark, IImageMark, IGroupMark } from './interface/mark'; +export type { ITextMark, ILabelMark, IRectMark, IRuleMark, IImageMark, IGroupMark, IGlyphMark } from './interface/mark'; export { MarkTypeEnum, @@ -57,6 +58,7 @@ export { AreaMark, RectMark, PathMark, + GlyphMark, BaseArcMark, ArcMark, ComponentMark, @@ -78,6 +80,7 @@ export { registerPathMark, registerArcMark, registerPolygonMark, + registerGlyphMark, registerRippleMark, registerImageMark, registerComponentMark, diff --git a/packages/vchart/src/series/index.ts b/packages/vchart/src/series/index.ts index df355a28af..2be7d5ffb8 100644 --- a/packages/vchart/src/series/index.ts +++ b/packages/vchart/src/series/index.ts @@ -221,3 +221,4 @@ export type { }; export * from './interface'; +export * from './util/utils';