diff --git a/docs/assets/examples/zh/candlestick/candlestick-basic.md b/docs/assets/examples/zh/candlestick/candlestick-basic.md new file mode 100644 index 0000000000..1ff7d4d2be --- /dev/null +++ b/docs/assets/examples/zh/candlestick/candlestick-basic.md @@ -0,0 +1,46 @@ +--- +category: examples +group: candlestick chart +title: K线图 +keywords: candlestick, k线, 股票, 金融 +link: '../guide/candlestick/introduction' +option: Candlestick#basic +--- + +# K 线图 + +K 线图基本用法 + +## 关键配置 + +- `type: 'candlestick'` +- `xField`, `openField`, `closeField`, `highField`, `lowField` +- `data` + +## 代码演示 + +```javascript livedemo template=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 } +]; + +const spec = { + type: 'candlestick', + xField: 'time', + openField: 'open', + closeField: 'close', + highField: 'high', + lowField: 'low', + data: [{ values: data }] +}; + +const chart = new VChart(spec, { + dom: document.getElementById(CONTAINER_ID) +}); + +window['chart'] = chart; +``` 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..38dc997031 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/candlestick-transformer.ts @@ -0,0 +1,35 @@ +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', + 'candlestickColor' + ]); + 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..f59416d73c --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/mark/candlestick.ts @@ -0,0 +1,78 @@ +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(), + boxWidth: 40 + }; + 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..eb52c0ba10 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/mark/interface.ts @@ -0,0 +1,32 @@ +import type { Datum, ICommonSpec } from '@visactor/vchart'; + +export interface ICandlestickMarkSpec extends ICommonSpec { + /** + * box描边宽度 + */ + lineWidth?: number; + /** + * 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..95b6e38f18 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/series/animation.ts @@ -0,0 +1,182 @@ +import type { EasingType, ILineAttribute, IRectAttribute } 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 = ( + computeCenter: (graphic: IGlyph, options: ICandlestickScaleAnimationOptions) => number +): TypeAnimation => { + return (graphic: IGlyph, options: ICandlestickScaleAnimationOptions, animationParameters: IAnimationParameters) => { + const finalAttribute = graphic.getFinalAttribute(); + const center = computeCenter(graphic, options); + if (!isValidNumber(center)) { + return {}; + } + 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 = ( + computeCenter: (mark: IGlyph, options: ICandlestickScaleAnimationOptions) => number +): TypeAnimation => { + return (graphic: IGlyph, options: ICandlestickScaleAnimationOptions, animationParameters: IAnimationParameters) => { + const finalAttribute = graphic.getFinalAttribute(); + const center = computeCenter(graphic, options); + if (!isValidNumber(center)) { + return {}; + } + 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; + }; +}; +const getGlyphChildByName = (mark: IGlyph, name: string) => { + return mark.getSubGraphic().find(child => child.name === name); +}; + +const computeCandlestickCenter = (glyphMark: IGlyph, options: ICandlestickScaleAnimationOptions) => { + if (options && isValidNumber(options.center)) { + return options.center; + } + const lineAttr = getGlyphChildByName(glyphMark, 'line')?.attribute as ILineAttribute | undefined; + const boxAttr = getGlyphChildByName(glyphMark, 'box')?.attribute as IRectAttribute | undefined; + + if (boxAttr && isValidNumber(boxAttr.y1) && isValidNumber(boxAttr.height)) { + const y0 = boxAttr.y1 - boxAttr.height; + const y1 = boxAttr.y1; + return (y0 + y1) / 2; + } + + if (lineAttr?.points?.length === 2) { + const y0 = lineAttr.points[0].y; + const y1 = lineAttr.points[1].y; + return (y0 + y1) / 2; + } + + return NaN; +}; + +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(computeCandlestickCenter)(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(computeCandlestickCenter)(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..0bfb665749 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/series/candlestick.ts @@ -0,0 +1,237 @@ +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'; + +const DEFAULT_STROKE_WIDTH = 2; +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 _lineWidth: number; + 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; + } + protected _riseFill: string; + getRiseFill(): string { + return this._riseFill; + } + protected _riseStroke: string; + getRiseStroke(): string { + return this._riseStroke; + } + protected _fallFill: string; + getFallFill(): string { + return this._fallFill; + } + protected _fallStroke: string; + getFallStroke(): string { + return this._fallStroke; + } + protected _dojiFill: string; + getDojiFill(): string { + return this._dojiFill; + } + protected _dojiStroke: string; + getDojiStroke(): string { + return this._dojiStroke; + } + + setAttrFromSpec() { + super.setAttrFromSpec(); + const spec = this._spec; + const CandlestickStyle: any = spec.candlestick?.style ?? {}; + const risingStyle: any = spec.rising?.style ?? {}; + const fallingStyle: any = spec.falling?.style ?? {}; + const dojiStyle: any = spec.doji?.style ?? {}; + this._openField = spec.openField; + this._highField = spec.highField; + this._lowField = spec.lowField; + this._closeField = spec.closeField; + this._lineWidth = CandlestickStyle.lineWidth ?? DEFAULT_STROKE_WIDTH; + this._boxWidth = CandlestickStyle.boxWidth; + this._boxFill = CandlestickStyle.boxFill; + this._riseFill = risingStyle.boxFill; + this._riseStroke = risingStyle.stroke; + this._fallFill = fallingStyle.boxFill; + this._fallStroke = fallingStyle.stroke; + this._dojiFill = dojiStyle.boxFill; + this._dojiStroke = dojiStyle.stroke; + this._strokeColor = CandlestickStyle.strokeColor; + } + + 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 = { + boxWidth: this._boxWidth, + fill: this._boxFill ?? this.getCandlestickColorAttribute.bind(this), + stroke: this._strokeColor ?? this.getCandlestickColorAttribute.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); + } + + getCandlestickColorAttribute(datum: Datum): string { + const openArr = this.getDatumPositionValues(datum, this._openField); + const closeArr = this.getDatumPositionValues(datum, this._closeField); + const open = openArr[0]; + const close = closeArr[0]; + if (open < close) { + return this._riseFill; + } else if (open > close) { + return this._fallFill; + } + return this._strokeColor ?? DEFAULT_STROKE_COLOR; + } + + 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..10711da3b0 --- /dev/null +++ b/packages/vchart-extension/src/charts/candlestick/series/theme.ts @@ -0,0 +1,27 @@ +import type { ICandlestickSeriesTheme } from '../series/interface'; + +export const getCandlestickTheme = (is3d?: boolean): ICandlestickSeriesTheme => { + const res: ICandlestickSeriesTheme = { + rising: { + style: { + boxFill: '#FF0000', + stroke: '#FF0000' + } + }, + falling: { + style: { + boxFill: '#00AA00', + stroke: '#00AA00' + } + }, + doji: { + style: { + boxFill: '#0000FF', + stroke: '#0000FF' + } + } + }; + 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/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';