Created
February 20, 2025 17:11
-
-
Save vishnuc/8cb9fe77367381fe3571d096ecb253a6 to your computer and use it in GitHub Desktop.
Trend line
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { isBusinessDay } from 'lightweight-charts'; | |
| import { PluginBase } from './tvplugin/plugin-base'; | |
| import { drawInit } from '@chart/drawings/_drawindex.svelte'; | |
| import { getUid } from '../../../lib/helper/uid'; | |
| const defaultOptions = { | |
| color: 'rgba(200, 50, 100, 0.75)', | |
| previewColor: 'rgba(200, 50, 100, 0.25)', | |
| lineWidth: 2, | |
| labelColor: 'rgba(200, 50, 100, 1)', | |
| labelTextColor: 'white', | |
| showLabels: false, | |
| priceLabelFormatter: (price) => price.toFixed(2), | |
| timeLabelFormatter: (time) => { | |
| if (typeof time === 'string') return time; | |
| const date = isBusinessDay(time) | |
| ? new Date(time.year, time.month, time.day) | |
| : new Date(time * 1000); | |
| return date.toLocaleDateString(); | |
| } | |
| }; | |
| class TrendLinePaneRenderer { | |
| constructor(p1, p2, color, lineWidth) { | |
| this._p1 = p1; | |
| this._p2 = p2; | |
| this._color = color; | |
| this._lineWidth = lineWidth; | |
| } | |
| draw(target) { | |
| target.useBitmapCoordinateSpace((scope) => { | |
| if (!this._p1?.x || !this._p1?.y || !this._p2?.x || !this._p2?.y) return; | |
| const ctx = scope.context; | |
| const { horizontalPixelRatio, verticalPixelRatio } = scope; | |
| // Draw the line | |
| ctx.strokeStyle = this._color; | |
| ctx.lineWidth = this._lineWidth * horizontalPixelRatio; | |
| ctx.beginPath(); | |
| const x1Scaled = Math.round(this._p1.x * horizontalPixelRatio); | |
| const y1Scaled = Math.round(this._p1.y * verticalPixelRatio); | |
| const x2Scaled = Math.round(this._p2.x * horizontalPixelRatio); | |
| const y2Scaled = Math.round(this._p2.y * verticalPixelRatio); | |
| ctx.moveTo(x1Scaled, y1Scaled); | |
| ctx.lineTo(x2Scaled, y2Scaled); | |
| ctx.stroke(); | |
| // Draw circles at endpoints | |
| const radius = 3 * horizontalPixelRatio; | |
| ctx.fillStyle = this._color; | |
| ctx.beginPath(); | |
| ctx.arc(x1Scaled, y1Scaled, radius, 0, 2 * Math.PI); | |
| ctx.fill(); | |
| ctx.beginPath(); | |
| ctx.arc(x2Scaled, y2Scaled, radius, 0, 2 * Math.PI); | |
| ctx.fill(); | |
| }); | |
| } | |
| } | |
| class TrendLinePaneView { | |
| constructor(source) { | |
| this._source = source; | |
| this._p1 = { x: null, y: null }; | |
| this._p2 = { x: null, y: null }; | |
| } | |
| _calculateCoordinate(point) { | |
| const timeScale = this._source.chart.timeScale(); | |
| if (point.time) { | |
| return timeScale.timeToCoordinate(point.time); | |
| } | |
| const time2coordinate = timeScale.timeToCoordinate(point.lastTime); | |
| const coordinate2logical = timeScale.coordinateToLogical(time2coordinate); | |
| const logical = coordinate2logical + point.barDiff; | |
| return timeScale.logicalToCoordinate(logical); | |
| } | |
| _calculatePoint(sourcePoint, price) { | |
| return { | |
| x: this._calculateCoordinate(sourcePoint), | |
| y: this._source.series.priceToCoordinate(price) | |
| }; | |
| } | |
| update() { | |
| this._p1 = this._calculatePoint(this._source._p1, this._source._p1.price); | |
| this._p2 = this._calculatePoint(this._source._p2, this._source._p2.price); | |
| } | |
| renderer() { | |
| return new TrendLinePaneRenderer( | |
| this._p1, | |
| this._p2, | |
| this._source._options.color, | |
| this._source._options.lineWidth | |
| ); | |
| } | |
| } | |
| class TrendLine extends PluginBase { | |
| constructor(p1, p2, options) { | |
| super(); | |
| this._p1 = p1; | |
| this._p2 = p2; | |
| this._options = { ...defaultOptions, ...options }; | |
| this._paneViews = [new TrendLinePaneView(this)]; | |
| } | |
| updateAllViews() { | |
| this._paneViews.forEach((pw) => pw.update()); | |
| } | |
| hitTest() { | |
| return { | |
| externalId: 'trendline-hover', | |
| zOrder: 'top' | |
| }; | |
| } | |
| paneViews() { | |
| return this._paneViews; | |
| } | |
| applyOptions(options) { | |
| this._options = { ...this._options, ...options }; | |
| this.requestUpdate(); | |
| } | |
| } | |
| class PreviewTrendLine extends TrendLine { | |
| constructor(p1, p2, options) { | |
| super(p1, p2, options); | |
| this._options.color = this._options.previewColor; | |
| } | |
| updateEndPoint(p) { | |
| this._p2 = p; | |
| this._paneViews[0].update(); | |
| this.requestUpdate(); | |
| } | |
| } | |
| export class trendLineDrawingClass { | |
| constructor(chartInstance, param, drawType) { | |
| this._uid = getUid(drawType); | |
| this._chartInstance = chartInstance; | |
| this._chartid = chartInstance._uid; | |
| this._chart = chartInstance.getChart(); | |
| this._series = chartInstance.getSeries(); | |
| this._drawItemArray = {}; | |
| this._previewTrendLine = null; | |
| this._points = []; | |
| this._drawing = false; | |
| this._drawType = drawType; | |
| this._param = { ...param, isInitial: true }; | |
| this._chart.subscribeClick(this._clickHandler); | |
| this._chart.subscribeCrosshairMove(this._moveHandler); | |
| this.startDrawing(); | |
| this._onClick(this._param); | |
| } | |
| _clickHandler = (param) => this._onClick(param); | |
| _moveHandler = (param) => this._onMouseMove(param); | |
| getDrawItemArraybyId(id) { | |
| return this._drawItemArray[id]; | |
| } | |
| removeAll() { | |
| if (this._chart) { | |
| this._chart.unsubscribeClick(this._clickHandler); | |
| this._chart.unsubscribeCrosshairMove(this._moveHandler); | |
| } | |
| this._removePreviewTrendLine(); | |
| this._drawItemArray = {}; | |
| this._points = []; | |
| this._chart = null; | |
| this._series = null; | |
| this._chartInstance = null; | |
| this._clickHandler = null; | |
| this._moveHandler = null; | |
| } | |
| startDrawing() { | |
| this._uid = getUid(this._drawType); | |
| console.log('start drawing..'); | |
| this._drawing = true; | |
| this._points = []; | |
| } | |
| finishDrawing() { | |
| console.log('finished drawing..'); | |
| this._drawing = false; | |
| this._points = []; | |
| this._removePreviewTrendLine(); | |
| } | |
| isDrawing() { | |
| return this._drawing; | |
| } | |
| _onClick(param) { | |
| if (!this._drawing || !param?.point || !this._series) return; | |
| // Immediately calculate and store the point data | |
| const price = this._series.coordinateToPrice(param.point.y); | |
| if (price === null) return; | |
| const pointData = { | |
| time: param.time, | |
| price, | |
| lastTime: undefined, | |
| barDiff: undefined | |
| }; | |
| if (param.time === undefined) { | |
| const diff = this._getBarDiff(param); | |
| pointData.lastTime = diff.lastTime; | |
| pointData.barDiff = diff.barDiff; | |
| } | |
| // Immediately add the point and handle drawing state | |
| this._addPoint(pointData); | |
| } | |
| _onMouseMove(param) { | |
| if (!this._drawing || !param?.point || !this._series) return; | |
| const price = this._series.coordinateToPrice(param.point.y); | |
| if (price === null) return; | |
| let lastTime, barDiff; | |
| if (param.time === undefined) { | |
| const diff = this._getBarDiff(param); | |
| lastTime = diff.lastTime; | |
| barDiff = diff.barDiff; | |
| } | |
| if (this._previewTrendLine) { | |
| this._previewTrendLine.updateEndPoint({ | |
| time: param.time, | |
| price, | |
| lastTime, | |
| barDiff | |
| }); | |
| } | |
| } | |
| _getBarDiff(param) { | |
| const timeScale = this._chart.timeScale(); | |
| const lastTime = this._series.data()[this._series.data().length - 1].time; | |
| const time2coordinate = timeScale.timeToCoordinate(lastTime); | |
| const coordinate2logical = timeScale.coordinateToLogical(time2coordinate); | |
| const logical = timeScale.coordinateToLogical(param.point.x); | |
| return { | |
| lastTime, | |
| barDiff: logical - coordinate2logical | |
| }; | |
| } | |
| _addPoint(p) { | |
| this._points.push(p); | |
| if (this._points.length === 1) { | |
| // For first point, immediately add preview line | |
| this._previewTrendLine = new PreviewTrendLine(p, p, {}); | |
| this._series.attachPrimitive(this._previewTrendLine); | |
| } else if (this._points.length === 2) { | |
| // For second point, immediately create final trend line | |
| const trendLine = new TrendLine(this._points[0], this._points[1], {}); | |
| const itemuid = getUid('primitive_' + this._drawType); | |
| this._drawItemArray[itemuid] = trendLine; | |
| // Remove preview and attach final line in one batch | |
| if (this._previewTrendLine) { | |
| this._series.detachPrimitive(this._previewTrendLine); | |
| this._previewTrendLine = null; | |
| } | |
| this._series.attachPrimitive(trendLine); | |
| // Complete the drawing process | |
| this._drawing = false; | |
| this._points = []; | |
| drawInit('close', this, itemuid); | |
| } | |
| } | |
| _removePreviewTrendLine() { | |
| if (this._previewTrendLine && this._series) { | |
| this._series.detachPrimitive(this._previewTrendLine); | |
| this._previewTrendLine = null; | |
| } | |
| } | |
| removeById(id) { | |
| if (this._drawItemArray[id] && this._series) { | |
| console.log(id, 'removed'); | |
| this._series.detachPrimitive(this._drawItemArray[id]); | |
| delete this._drawItemArray[id]; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment