@devexperts/dxcharts-lite
Version:
172 lines (171 loc) • 8.85 kB
JavaScript
/*
* Copyright (C) 2019 - 2025 Devexperts Solutions IE Limited
* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
* If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { CandleSeriesModel } from '../../model/candle-series.model';
import { avoidAntialiasing } from '../../utils/canvas/canvas-drawing-functions.utils';
import { dpr } from '../../utils/device/device-pixel-ratio.utils';
import { setLineWidth } from '../data-series.drawer';
import { flat } from '../../utils/array.utils';
export class CandleDrawer {
constructor(config) {
this.config = config;
//#region
// These properties are some kind of optimization hack
// we can calculate them inside drawCandle method, but it can affect performance since there are a lot of candles - lots of recalculations
// this value is calculate in following way: 1 / devicePixelRatio
// pixelLength is a length of one pixel in canvas units (CU), for example if dpr is 1 when in 1 CU we have 1 pixel
// for dpr 2 we have pixel length equal 0.5 CU, so when you draw a line with width 2, the actual width in pixels will be equal 4
this.pixelLength = 1;
// lineWidth and halfLineWidth are calculated using ctx.lineWidth (CU - canvas unit)
// why do we need half line width and line width? - to correctly draw candle border
// Canvas stroke draws outline border - for example, if we have a rectangle with height=10, width=20
// and we try to draw a border for this square like this: ctx.strokeRect(0, 0, 10, 20), lineWidth=2
// as a result on the canvas we have a rectangle which edge points are (-1, -1), (21, 11);
// for lineWidth=4 we get this edge points as a result: (-2, -2), (22, 12).
// However the border should not exceed candle width, so we add halfLine width to start point for rectangle
// and subtract lineWidth from width/height. As a result, candle border on canvas won't exceed candle width
this.lineWidthCU = 1;
this.halfLineWidthCU = 1;
}
//#endregion
draw(ctx,
/**
* You can pass two-dimension array to divide series into multiple parts
*/
points, model, hitTestDrawerConfig) {
if (model instanceof CandleSeriesModel) {
// @ts-ignore
const visualCandles = flat(points);
// TODO FIXME draw called 3-4 times on single candle update even if multichart is off
setLineWidth(ctx, this.config.candleLineWidth, model, hitTestDrawerConfig, this.config.candleLineWidth);
avoidAntialiasing(ctx, () => {
this.pixelLength = 1 / dpr;
this.halfLineWidthCU = ctx.lineWidth / 2;
this.lineWidthCU = ctx.lineWidth;
for (const visualCandle of visualCandles) {
const { candleTheme, activeCandleTheme } = model.colors;
if (candleTheme && activeCandleTheme) {
this.drawCandle(ctx, hitTestDrawerConfig, model, visualCandle);
}
}
});
}
}
drawCandle(ctx, hitTestDrawerConfig, candleSeries, visualCandle) {
const { candleTheme, activeCandleTheme } = candleSeries.colors;
const direction = visualCandle.name;
const currentCandleTheme = visualCandle.isActive ? activeCandleTheme : candleTheme;
const isHollow = visualCandle.isHollow;
// choose candle filling color
if (hitTestDrawerConfig.color) {
ctx.fillStyle = hitTestDrawerConfig.color;
}
else if (isHollow) {
ctx.fillStyle = currentCandleTheme[`${direction}WickColor`];
}
else {
ctx.fillStyle = currentCandleTheme[`${direction}Color`];
}
const baseX = candleSeries.view.toX(visualCandle.startUnit);
const width = candleSeries.view.xPixels(visualCandle.width);
const bodyH = visualCandle.bodyHeight(candleSeries.view);
const [lineStart, bodyStart, _bodyEnd, _lineEnd] = visualCandle.yBodyKeyPoints(candleSeries.view);
const bodyEnd = bodyStart === _bodyEnd ? bodyStart + 1 : _bodyEnd;
const lineEnd = lineStart === _lineEnd ? lineStart + 1 : _lineEnd;
const candleColor = currentCandleTheme[`${direction}Color`];
const wickColor = direction === 'none' ? candleColor : currentCandleTheme[`${direction}WickColor`];
ctx.fillStyle = candleColor;
// wick style, borders are drawn after the wicks, so style for borders will be changed in drawBorder method
if (hitTestDrawerConfig.color) {
ctx.strokeStyle = hitTestDrawerConfig.color;
}
else {
ctx.strokeStyle = wickColor;
}
const showCandleBorder = isHollow ||
(visualCandle.hasBorder && visualCandle.isActive
? this.config.showActiveCandlesBorder
: this.config.showCandlesBorder);
// just draw a vertical line
const showWicks = this.config.showWicks;
// separate logic for each candle width in pixels
if (width < 2) {
ctx.beginPath();
ctx.moveTo(baseX, showWicks ? lineStart : bodyStart);
ctx.lineTo(baseX, showWicks ? lineEnd : bodyEnd);
ctx.stroke();
}
else if (width < 3) {
// draw 2 vertical lines for each px width
ctx.beginPath();
ctx.moveTo(baseX, showWicks ? lineStart : bodyStart);
ctx.lineTo(baseX, showWicks ? lineEnd : bodyEnd);
ctx.moveTo(baseX + 1, bodyStart);
ctx.lineTo(baseX + 1, bodyEnd);
ctx.stroke();
}
else if (width === 3) {
const offset = width / dpr;
this.drawCandlesWicks(ctx, baseX + offset, lineStart, lineEnd, bodyStart, bodyEnd);
// border = 2px + 1px line
if (!isHollow) {
ctx.beginPath();
ctx.moveTo(baseX + offset, bodyStart);
ctx.lineTo(baseX + offset, bodyEnd);
ctx.stroke();
}
this.drawCandleBorder(ctx, hitTestDrawerConfig, currentCandleTheme, visualCandle, baseX + this.halfLineWidthCU, bodyStart + this.halfLineWidthCU, width - this.lineWidthCU, bodyH - this.lineWidthCU);
}
else {
// add paddings if exist
const wickX = visualCandle.x(candleSeries.view);
// candles' wick doesn't touch body end, so subtract 1
// we will rework the drawer in future, so let's keep it this way for now
this.drawCandlesWicks(ctx, wickX, lineStart, lineEnd, bodyStart, bodyEnd);
const paddingPercent = this.config.candlePaddingPercent;
const paddingWidthOffset = Math.max((width * paddingPercent) / 2, this.pixelLength);
const paddingBaseX = baseX + paddingWidthOffset;
const paddingWidth = width - paddingWidthOffset * 2;
if (!isHollow) {
if (hitTestDrawerConfig.color) {
ctx.fillStyle = hitTestDrawerConfig.color;
}
const bodyWidth = hitTestDrawerConfig.hoverWidth ? width + paddingWidthOffset : paddingWidth;
const bodyHeight = hitTestDrawerConfig.hoverWidth
? bodyH + hitTestDrawerConfig.hoverWidth + paddingWidthOffset
: bodyH;
ctx.fillRect(paddingBaseX, bodyStart, bodyWidth, bodyHeight);
}
// choose border color around candle and draw candle
if (showCandleBorder) {
this.drawCandleBorder(ctx, hitTestDrawerConfig, currentCandleTheme, visualCandle, paddingBaseX + this.halfLineWidthCU, bodyStart + this.halfLineWidthCU, paddingWidth - this.lineWidthCU, bodyH - this.lineWidthCU);
}
}
}
drawCandlesWicks(ctx, x, lineStart, lineEnd, bodyStart, bodyEnd) {
// draw wick vertical line
if (this.config.showWicks) {
ctx.beginPath();
// upper wick
ctx.moveTo(x, lineStart);
ctx.lineTo(x, bodyStart);
// lower wick
ctx.moveTo(x, bodyEnd);
ctx.lineTo(x, lineEnd);
ctx.stroke();
}
}
drawCandleBorder(ctx, hitTestDrawerConfig, candleTheme, visualCandle, x, y, w, h) {
if (hitTestDrawerConfig.color) {
ctx.strokeStyle = hitTestDrawerConfig.color;
}
else {
const direction = visualCandle.name;
ctx.strokeStyle =
direction === 'none' ? candleTheme[`${direction}Color`] : candleTheme[`${direction}WickColor`];
}
ctx.strokeRect(x, y, w, h);
}
}