Skip to content

Instantly share code, notes, and snippets.

@vishnuc
Created February 20, 2025 17:11
Show Gist options
  • Select an option

  • Save vishnuc/8cb9fe77367381fe3571d096ecb253a6 to your computer and use it in GitHub Desktop.

Select an option

Save vishnuc/8cb9fe77367381fe3571d096ecb253a6 to your computer and use it in GitHub Desktop.
Trend line
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