import { BitmapCoordinatesRenderingScope, CanvasRenderingTarget2D } from 'fancy-canvas'; import { AutoscaleInfo, Coordinate, IChartApi, ISeriesApi, ISeriesPrimitive, IPrimitivePaneRenderer, IPrimitivePaneView, Logical, SeriesOptionsMap, SeriesType, Time, SeriesAttachedParameter, PrimitiveHoveredItem, } from 'lightweight-charts'; class TrendLinePaneRenderer implements IPrimitivePaneRenderer { _p1: ViewPoint; _p2: ViewPoint; _text1: string; _text2: string; _options: TrendLineOptions; _selected: boolean; constructor(p1: ViewPoint, p2: ViewPoint, text1: string, text2: string, options: TrendLineOptions, selected: boolean) { this._p1 = p1; this._p2 = p2; this._text1 = text1; this._text2 = text2; this._options = options; this._selected = selected; } draw(target: CanvasRenderingTarget2D) { target.useBitmapCoordinateSpace(scope => { if ( this._p1.x === null || this._p1.y === null || this._p2.x === null || this._p2.y === null ) 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); // Apply preview styling if needed const lineWidth = this._options.isPreview ? this._options.width : (this._selected ? this._options.width * 1.5 : this._options.width); const opacity = this._options.isPreview ? 0.5 : 1.0; ctx.lineWidth = lineWidth; ctx.strokeStyle = this._options.lineColor; ctx.globalAlpha = opacity; // Apply dashed style for preview if (this._options.isPreview) { ctx.setLineDash([5 * scope.horizontalPixelRatio, 5 * scope.horizontalPixelRatio]); } else { ctx.setLineDash([]); } ctx.beginPath(); ctx.moveTo(x1Scaled, y1Scaled); ctx.lineTo(x2Scaled, y2Scaled); ctx.stroke(); ctx.setLineDash([]); ctx.globalAlpha = 1.0; // Draw endpoint handles when selected if (this._selected) { const handleRadius = 6 * scope.horizontalPixelRatio; ctx.fillStyle = 'white'; ctx.strokeStyle = this._options.lineColor; ctx.lineWidth = 2 * scope.horizontalPixelRatio; // Handle at p1 ctx.beginPath(); ctx.arc(x1Scaled, y1Scaled, handleRadius, 0, 2 * Math.PI); ctx.fill(); ctx.stroke(); // Handle at p2 ctx.beginPath(); ctx.arc(x2Scaled, y2Scaled, handleRadius, 0, 2 * Math.PI); ctx.fill(); ctx.stroke(); } if (this._options.showLabels) { this._drawTextLabel(scope, this._text1, x1Scaled, y1Scaled, true); this._drawTextLabel(scope, this._text2, x2Scaled, y2Scaled, false); } }); } _drawTextLabel(scope: BitmapCoordinatesRenderingScope, text: string, x: number, y: number, left: boolean) { 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; scope.context.fillStyle = this._options.labelBackgroundColor; scope.context.roundRect(x + offset - leftAdjustment, y - 24, textWidth.width + offset * 2, 24 + offset, 5); scope.context.fill(); scope.context.beginPath(); scope.context.fillStyle = this._options.labelTextColor; scope.context.fillText(text, x + offset * 2 - leftAdjustment, y); } } interface ViewPoint { x: Coordinate | null; y: Coordinate | null; } class TrendLinePaneView implements IPrimitivePaneView { _source: TrendLine; _p1: ViewPoint = { x: null, y: null }; _p2: ViewPoint = { x: null, y: null }; constructor(source: TrendLine) { this._source = source; } update() { const series = this._source._series; const y1 = series.priceToCoordinate(this._source._p1.price); const y2 = series.priceToCoordinate(this._source._p2.price); const timeScale = this._source._chart.timeScale(); const x1 = timeScale.timeToCoordinate(this._source._p1.time); const x2 = timeScale.timeToCoordinate(this._source._p2.time); this._p1 = { x: x1, y: y1 }; this._p2 = { x: x2, y: y2 }; } renderer() { return new TrendLinePaneRenderer( this._p1, this._p2, '' + this._source._p1.price.toFixed(1), '' + this._source._p2.price.toFixed(1), this._source._options, this._source._selected ); } } interface Point { time: Time; price: number; } export interface TrendLineOptions { lineColor: string; width: number; showLabels: boolean; labelBackgroundColor: string; labelTextColor: string; isPreview?: boolean; annotationId?: string; } const defaultOptions: TrendLineOptions = { lineColor: 'rgb(0, 0, 0)', width: 2, showLabels: false, labelBackgroundColor: 'rgba(255, 255, 255, 0.85)', labelTextColor: 'rgb(0, 0, 0)', }; export class TrendLine implements ISeriesPrimitive