Загрузка данных
import { BitmapCoordinatesRenderingScope, CanvasRenderingTarget2D } from 'fancy-canvas';
import { IPrimitivePaneRenderer } from 'lightweight-charts';
import type { ViewPoint } from '../common';
import type { TrendLineOptions } from './trendLine';
interface TrendLinePaneRendererParams {
p1: ViewPoint;
p2: ViewPoint;
text1: string;
text2: string;
options: TrendLineOptions;
visible: boolean;
}
export class TrendLinePaneRenderer implements IPrimitivePaneRenderer {
private _p1: ViewPoint;
private _p2: ViewPoint;
private _text1: string;
private _text2: string;
private _options: TrendLineOptions;
private _visible: boolean;
constructor({ p1, p2, text1, text2, options, visible }: TrendLinePaneRendererParams) {
this._p1 = p1;
this._p2 = p2;
this._text1 = text1;
this._text2 = text2;
this._options = options;
this._visible = visible;
}
public draw(target: CanvasRenderingTarget2D): void {
target.useBitmapCoordinateSpace((scope) => {
if (this._p1.x === null || this._p1.y === null || this._p2.x === null || this._p2.y === null || !this._visible) {
return;
}
const ctx = scope.context;
const x1Scaled = Math.round(this._p1.x * scope.horizontalPixelRatio);
const y1Scaled = Math.round(this._p1.y * scope.verticalPixelRatio);
const x2Scaled = Math.round(this._p2.x * scope.horizontalPixelRatio);
const y2Scaled = Math.round(this._p2.y * scope.verticalPixelRatio);
ctx.lineWidth = this._options.width;
ctx.strokeStyle = this._options.lineColor;
ctx.beginPath();
ctx.moveTo(x1Scaled, y1Scaled);
ctx.lineTo(x2Scaled, y2Scaled);
ctx.stroke();
if (this._options.showLabels) {
this._drawTextLabel(scope, this._text1, x1Scaled, y1Scaled, true);
this._drawTextLabel(scope, this._text2, x2Scaled, y2Scaled, false);
}
});
}
private _drawTextLabel(
scope: BitmapCoordinatesRenderingScope,
text: string,
x: number,
y: number,
left: boolean,
): void {
scope.context.font = '24px Arial';
scope.context.beginPath();
const offset = 5 * scope.horizontalPixelRatio;
const textWidth = scope.context.measureText(text);
const leftAdjustment = left ? textWidth.width + offset * 4 : 0;
const padding = 24;
scope.context.fillStyle = this._options.labelBackgroundColor;
scope.context.roundRect(
x + offset - leftAdjustment,
y - padding,
textWidth.width + offset * 2,
padding + offset,
5,
);
scope.context.fill();
scope.context.beginPath();
scope.context.fillStyle = this._options.labelTextColor;
scope.context.save();
scope.context.font = '14px serif';
scope.context.fillText(text, x + offset * 2 - leftAdjustment, y);
scope.context.restore();
}
}
import { IPrimitivePaneRenderer, IPrimitivePaneView } from 'lightweight-charts';
import { getXCoordinateFromTime, getYCoordinateFromPrice } from '../helpers';
import { TrendLinePaneRenderer } from './paneRenderer';
import type { ViewPoint } from '../common';
import type { TrendLine } from './trendLine';
export class TrendLinePaneView implements IPrimitivePaneView {
_source: TrendLine;
_p1: ViewPoint = { x: null, y: null };
_p2: ViewPoint = { x: null, y: null };
constructor(source: TrendLine) {
this._source = source;
}
public update(): void {
if (!this._source._p1 || !this._source._p2) {
return;
}
const x1 = getXCoordinateFromTime(this._source._chart, this._source._p1.time);
const x2 = getXCoordinateFromTime(this._source._chart, this._source._p2.time);
const y1 = getYCoordinateFromPrice(this._source._series, this._source._p1.price);
const y2 = getYCoordinateFromPrice(this._source._series, this._source._p2.price);
if (x1 == null || x2 == null || y1 == null || y2 == null) {
throw new Error('[Drawings] - не удалось получить координаты точки');
}
this._p1 = { x: x1, y: y1 };
this._p2 = { x: x2, y: y2 };
}
public renderer(): IPrimitivePaneRenderer {
return new TrendLinePaneRenderer({
p1: this._p1,
p2: this._p2,
text1: `${this._source._p1.price.toFixed(1)}`,
text2: `${this._source._p2.price.toFixed(1)}`,
options: this._source._options,
visible: this._source._visible,
});
}
}
import {
AutoscaleInfo,
IChartApi,
ISeriesApi,
Logical,
MouseEventParams,
SeriesOptionsMap,
SeriesType,
Time,
} from 'lightweight-charts';
import { ISeriesDrawing, Point } from '../common';
import { getPriceFromYCoordinate, getTimeFromXCoordinate, getXCoordinateFromTime } from '../helpers';
import { TrendLinePaneView } from './paneView';
export interface TrendLineOptions {
lineColor: string;
width: number;
showLabels: boolean;
labelBackgroundColor: string;
labelTextColor: string;
}
const defaultOptions: TrendLineOptions = {
lineColor: 'gray',
width: 3,
showLabels: true,
labelBackgroundColor: 'rgba(255, 255, 255, 0)',
labelTextColor: 'gray',
};
export class TrendLine implements ISeriesDrawing {
private _attached = false;
_chart: IChartApi;
_series: ISeriesApi<keyof SeriesOptionsMap>;
_p1!: Point;
_p2!: Point;
_paneViews: TrendLinePaneView[];
_options: TrendLineOptions;
_minPrice!: number;
_maxPrice!: number;
_visible = true;
constructor(chart: IChartApi, series: ISeriesApi<SeriesType>, options?: Partial<TrendLineOptions>) {
this._chart = chart;
this._series = series;
chart.subscribeClick(this.startDrawing);
this._options = {
...defaultOptions,
...options,
};
this._paneViews = [new TrendLinePaneView(this)];
}
private startDrawing = (param: MouseEventParams<Time>) => {
this._chart.unsubscribeClick(this.startDrawing);
if (!param.point) {
throw new Error('[Drawings] - не удалось получить координаты точки');
}
const price = getPriceFromYCoordinate(this._series, param.point.y);
const time = getTimeFromXCoordinate(this._chart, param.point.x);
if (time == null || price == null) {
throw new Error('[Drawings] - не удалось получить координаты точки');
}
this._p1 = { time, price };
this._p2 = { time, price };
this._minPrice = Math.min(this._p1.price, this._p2.price);
this._maxPrice = Math.max(this._p1.price, this._p2.price);
this._series.attachPrimitive(this);
this._attached = true;
this._chart.subscribeClick(this.endDrawing);
this._chart.subscribeCrosshairMove(this.drawing);
this.updateAllViews();
};
private drawing = (param: MouseEventParams<Time>) => {
if (!param.point) {
return;
}
const price = getPriceFromYCoordinate(this._series, param.point.y);
const time = getTimeFromXCoordinate(this._chart, param.point.x);
if (time == null || price == null) {
return;
}
this._p2 = { time, price };
this._minPrice = Math.min(this._p1.price, this._p2.price);
this._maxPrice = Math.max(this._p1.price, this._p2.price);
this.updateAllViews();
};
private endDrawing = (param: MouseEventParams<Time>) => {
if (!param.point) {
throw new Error('[Drawings] - не удалось получить координаты точки');
}
const price = getPriceFromYCoordinate(this._series, param.point.y);
const time = getTimeFromXCoordinate(this._chart, param.point.x);
if (time == null || price == null) {
throw new Error('[Drawings] - не удалось получить координаты точки');
}
this._p2 = { time, price };
this._minPrice = Math.min(this._p1.price, this._p2.price);
this._maxPrice = Math.max(this._p1.price, this._p2.price);
this._chart.unsubscribeCrosshairMove(this.drawing);
this._chart.unsubscribeClick(this.endDrawing);
this.updateAllViews();
};
public autoscaleInfo(startTimePoint: Logical, endTimePoint: Logical): AutoscaleInfo | null {
const p1Index = this._pointIndex(this._p1);
const p2Index = this._pointIndex(this._p2);
if (p1Index === null || p2Index === null) {
return null;
}
const fromIndex = Math.min(p1Index, p2Index);
const toIndex = Math.max(p1Index, p2Index);
if (endTimePoint < fromIndex || startTimePoint > toIndex) {
return null;
}
return {
priceRange: {
minValue: this._minPrice,
maxValue: this._maxPrice,
},
};
}
public updateAllViews() {
this._paneViews.forEach((pw) => pw.update());
}
public paneViews() {
return this._paneViews;
}
public _pointIndex(p: Point): number | null {
const xCoordinate = getXCoordinateFromTime(this._chart, p.time);
if (xCoordinate === null) {
return null;
}
const index = this._chart.timeScale().coordinateToLogical(xCoordinate);
return index;
}
public hide(): void {
this._visible = false;
this.updateAllViews();
}
public show(): void {
this._visible = true;
this.updateAllViews();
}
public rebind(series: ISeriesApi<SeriesType>): void {
if (this._attached) {
this._series.detachPrimitive(this);
}
this._series = series;
this.updateAllViews();
if (this._attached) {
this._series.attachPrimitive(this);
}
}
public destroy(): void {
this._chart.unsubscribeClick(this.startDrawing);
this._chart.unsubscribeClick(this.endDrawing);
this._chart.unsubscribeCrosshairMove(this.drawing);
if (this._attached) {
this._series.detachPrimitive(this);
this._attached = false;
}
}
}