Skip to content

Commit

Permalink
feat: candle.area support last data point mark
Browse files Browse the repository at this point in the history
  • Loading branch information
liihuu committed Mar 17, 2024
1 parent 86fb6d2 commit 2322053
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 13 deletions.
93 changes: 93 additions & 0 deletions src/common/Animation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type Nullable from './Nullable'
import { requestAnimationFrame } from './utils/compatible'
import { merge } from './utils/typeChecks'

type AnimationDoFrameCallback = (frameTime: number) => void

interface AnimationOptions {
duration: number
iterationCount: number
}

export default class Animation {
private readonly _options = { duration: 500, iterationCount: 1 }

private _doFrameCallback: Nullable<AnimationDoFrameCallback>

private _currentIterationCount = 0
private _running = false

private _time = 0

constructor (options?: Partial<AnimationOptions>) {
merge(this._options, options)
}

_getTime (): number {
return new Date().getTime()
}

_loop (): void {
this._running = true
const step: (() => void) = () => {
if (this._running) {
const time = this._getTime()
const diffTime = time - this._time
if (diffTime < this._options.duration) {
this._doFrameCallback?.(diffTime)
requestAnimationFrame(step)
} else {
this.stop()
this._currentIterationCount++
if (this._currentIterationCount < this._options.iterationCount) {
this.start()
}
}
}
}
requestAnimationFrame(step)
}

doFrame (callback: AnimationDoFrameCallback): this {
this._doFrameCallback = callback
return this
}

setDuration (duration: number): this {
this._options.duration = duration
return this
}

setIterationCount (iterationCount: number): this {
this._options.iterationCount = iterationCount
return this
}

start (): void {
if (!this._running) {
this._time = new Date().getTime()
this._loop()
}
}

stop (): void {
if (this._running) {
this._doFrameCallback?.(this._options.duration)
}
this._running = false
}
}
22 changes: 21 additions & 1 deletion src/common/Styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,23 @@ export interface TooltipStyle {
icons: TooltipIconStyle[]
}

export interface CandleAreaPointStyle {
show: boolean
color: string
radius: number
rippleColor: string
rippleRadius: number
animation: boolean
animationDuration: number
}

export interface CandleAreaStyle {
lineSize: number
lineColor: string
value: string
smooth: boolean
backgroundColor: string | GradientColor[]
point: CandleAreaPointStyle
}

export interface CandleHighLowPriceMarkStyle {
Expand Down Expand Up @@ -437,7 +448,16 @@ function getDefaultCandleStyle (): CandleStyle {
}, {
offset: 1,
color: getAlphaBlue(0.2)
}]
}],
point: {
show: true,
color: blue,
radius: 4,
rippleColor: getAlphaBlue(0.3),
rippleRadius: 8,
animation: true,
animationDuration: 1000
}
},
priceMark: {
show: true,
Expand Down
14 changes: 12 additions & 2 deletions src/component/Figure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export type FigureInnerConstructor<A = any, S = any> = new (figure: FigureCreate
export type FigureConstructor<A = any, S = any> = new (figure: FigureCreate<A, S>) => ({ draw: (ctx: CanvasRenderingContext2D) => void })

export default abstract class FigureImp<A = any, S = any> extends Eventful implements Omit<Figure<A, S>, 'name' | 'draw' | 'checkEventOn'> {
readonly attrs: A
readonly styles: S
attrs: A
styles: S

constructor (figure: FigureCreate) {
super()
Expand All @@ -49,6 +49,16 @@ export default abstract class FigureImp<A = any, S = any> extends Eventful imple
return this.checkEventOnImp(event, this.attrs, this.styles)
}

setAttrs (attrs: A): this {
this.attrs = attrs
return this
}

setStyles (styles: S): this {
this.styles = styles
return this
}

draw (ctx: CanvasRenderingContext2D): void {
this.drawImp(ctx, this.attrs, this.styles)
}
Expand Down
86 changes: 76 additions & 10 deletions src/view/CandleAreaView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,55 @@
import type Coordinate from '../common/Coordinate'
import type VisibleData from '../common/VisibleData'
import type BarSpace from '../common/BarSpace'
import { type GradientColor } from '../common/Styles'
import { type GradientColor, type PolygonStyle } from '../common/Styles'
import Animation from '../common/Animation'
import { isNumber, isArray, isValid } from '../common/utils/typeChecks'
import { UpdateLevel } from '../common/Updater'

import ChildrenView from './ChildrenView'

import { isNumber, isArray } from '../common/utils/typeChecks'

import { lineTo } from '../extension/figure/line'
import type Figure from '../component/Figure'
import type Nullable from '../common/Nullable'
import { type CircleAttrs } from '../extension/figure/circle'

export default class CandleAreaView extends ChildrenView {
private _figure: Nullable<Figure<CircleAttrs, Partial<PolygonStyle>>> = null
private _animationFrameTime = 0

private readonly _animation = new Animation({ iterationCount: Infinity }).doFrame((time) => {
this._animationFrameTime = time
const pane = this.getWidget().getPane()
pane.getChart().updatePane(UpdateLevel.Main, pane.getId())
})

override drawImp (ctx: CanvasRenderingContext2D): void {
const widget = this.getWidget()
const pane = widget.getPane()
const chart = pane.getChart()
const dataList = chart.getDataList()
const lastDataIndex = dataList.length - 1
const bounding = widget.getBounding()
const yAxis = pane.getAxisComponent()
const candleAreaStyles = chart.getStyles().candle.area
const styles = chart.getStyles().candle.area
const coordinates: Coordinate[] = []
let minY = Number.MAX_SAFE_INTEGER
let areaStartX: number = 0
let indicatePointCoordinate: Nullable<Coordinate> = null
this.eachChildren((data: VisibleData, _: BarSpace, i: number) => {
const { data: kLineData, x } = data
// const { halfGapBar } = barSpace
const value = kLineData?.[candleAreaStyles.value]
const value = kLineData?.[styles.value]
if (isNumber(value)) {
const y = yAxis.convertToPixel(value)
if (i === 0) {
areaStartX = x
}
coordinates.push({ x, y })
minY = Math.min(minY, y)
if (data.dataIndex === lastDataIndex) {
indicatePointCoordinate = { x, y }
}
}
})

Expand All @@ -53,15 +72,15 @@ export default class CandleAreaView extends ChildrenView {
name: 'line',
attrs: { coordinates },
styles: {
color: candleAreaStyles.lineColor,
size: candleAreaStyles.lineSize,
smooth: candleAreaStyles.smooth
color: styles.lineColor,
size: styles.lineSize,
smooth: styles.smooth
}
}
)?.draw(ctx)

// render area
const backgroundColor = candleAreaStyles.backgroundColor
const backgroundColor = styles.backgroundColor
let color: string | CanvasGradient
if (isArray<GradientColor>(backgroundColor)) {
const gradient = ctx.createLinearGradient(0, bounding.height, 0, minY)
Expand All @@ -79,10 +98,57 @@ export default class CandleAreaView extends ChildrenView {
ctx.beginPath()
ctx.moveTo(areaStartX, bounding.height)
ctx.lineTo(coordinates[0].x, coordinates[0].y)
lineTo(ctx, coordinates, candleAreaStyles.smooth)
lineTo(ctx, coordinates, styles.smooth)
ctx.lineTo(coordinates[coordinates.length - 1].x, bounding.height)
ctx.closePath()
ctx.fill()
}

const pointStyles = styles.point
if (pointStyles.show && isValid(indicatePointCoordinate)) {
this.createFigure({
name: 'circle',
attrs: {
x: indicatePointCoordinate!.x,
y: indicatePointCoordinate!.y,
r: pointStyles.radius
},
styles: {
style: 'fill',
color: pointStyles.color
}
})?.draw(ctx)
let rippleRadius = pointStyles.rippleRadius
if (pointStyles.animation) {
rippleRadius = pointStyles.radius + this._animationFrameTime / pointStyles.animationDuration * (pointStyles.rippleRadius - pointStyles.radius)
this._animation.setDuration(pointStyles.animationDuration).start()
}
if (this._figure === null) {
this._figure = this.createFigure({
name: 'circle',
attrs: {
x: indicatePointCoordinate!.x,
y: indicatePointCoordinate!.y,
r: pointStyles.rippleRadius
},
styles: {
style: 'fill',
color: pointStyles.rippleColor
}
})
} else {
this._figure.setAttrs({
x: indicatePointCoordinate!.x,
y: indicatePointCoordinate!.y,
r: rippleRadius
})
}
this._figure?.draw(ctx)
if (pointStyles.animation) {
this._animation.setDuration(pointStyles.animationDuration).start()
}
} else {
this._animation.stop()
}
}
}

0 comments on commit 2322053

Please sign in to comment.