-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Open
Description
Hi , Is there a way to add some session breaks natively or a plugin example so we can add it to charts , I tried creating via help of AI , but its very laggy and not feeling native.. If it comes from the professional it will be better I guess. Below is my code
import type {
ISeriesPrimitive,
IPrimitivePaneView,
IPrimitivePaneRenderer,
PrimitivePaneViewZOrder
} from 'lightweight-charts';
import { PluginBase } from '../../tvplugin/plugin-base';
import {
drawHolder,
setActiveDrawingUid,
setHoverDrawingUid,
co2points,
time2co
} from '@/addon/drawings/_drawIndex.svelte';
import type { VerticalSeparatorOptions } from '../../_options';
import { defaultVerticalSeparatorOptions } from '../../_options';
export class VerticalSeparatorPlugin extends PluginBase implements ISeriesPrimitive {
_separatorLines: any[];
myChart: any;
mySeries: any;
done: boolean;
clickCount: number;
_paneViews: VerticalSeparatorPaneView[];
isActive: boolean = false;
isHovered: boolean = false;
isNear: 'line' | null = null;
uid: string;
initdrag: boolean = true;
lastNear: 'line' | null = null;
paneIndex: number;
grid: string;
options: VerticalSeparatorOptions = defaultVerticalSeparatorOptions;
autoDetectEnabled: boolean = true;
constructor(chart, series, uid, paneNum, grid, autoDetect = true) {
super();
this.uid = uid;
this.myChart = chart;
this.mySeries = series;
this.done = true; // Auto-separators are always done
this.clickCount = 0;
this.autoDetectEnabled = autoDetect;
this._separatorLines = [];
this.paneIndex = paneNum;
this.grid = grid;
this._paneViews = [new VerticalSeparatorPaneView(this)];
if (this.autoDetectEnabled) {
this._detectDayChanges();
}
}
_detectDayChanges() {
if (!this.mySeries || !this.myChart) return;
// Get the visible data range
const timeScale = this.myChart.timeScale();
const visibleRange = timeScale.getVisibleRange();
if (!visibleRange) return;
// Get series data
const seriesData = this.mySeries.data();
if (!seriesData || seriesData.length === 0) return;
// Find day boundaries within visible range
const dayBoundaries = [];
let currentDay = null;
seriesData.forEach((dataPoint, index) => {
if (!dataPoint.time) return;
// Convert time to date
const date = new Date(dataPoint.time * 1000);
const dayString = date.toDateString();
if (currentDay && currentDay !== dayString) {
// Day changed, add separator at this time
dayBoundaries.push({
time: dataPoint.time,
price: dataPoint.close || dataPoint.value || 0
});
}
currentDay = dayString;
});
// Convert to separator lines
this._separatorLines = dayBoundaries.map(boundary => ({
time: boundary.time,
price: boundary.price,
lastTime: boundary.time,
barDiff: 0
}));
this.requestUpdate();
}
refreshSeparators() {
if (this.autoDetectEnabled) {
this._detectDayChanges();
}
}
applyOptions(options: Partial<VerticalSeparatorOptions>) {
this.options = { ...this.options, ...options };
this.requestUpdate();
}
isDone() {
return this.done;
}
addPoint(point, count) {
// Auto-detect mode doesn't use manual point addition
if (this.autoDetectEnabled) return true;
this.clickCount = count;
const newPoint = co2points(point, this.myChart, this.mySeries);
this._separatorLines = [newPoint];
this.requestUpdate();
if (count == 1) {
this.done = true;
this.setActive(true);
setActiveDrawingUid(this.uid);
return true;
}
return false;
}
_dragStart() {
this.initdrag = true;
this.lastNear = null;
}
_dragDone() {
this.initdrag = false;
this.lastNear = null;
}
addPoint1(p) {
if (this.autoDetectEnabled) return;
const newPoint = co2points(p, this.myChart, this.mySeries);
this._separatorLines = [newPoint];
this.requestUpdate();
}
reposition(param, ogGrid) {
// Auto-detect mode doesn't allow repositioning
if (this.autoDetectEnabled) return;
const paneNum = param.paneIndex;
if (!param || !this.mySeries) return;
if (!this._paneViews[0]?._data) return;
if (param.point.x <= 0) return false;
if (param.point.y <= 0) return false;
if (paneNum !== this.paneIndex) return false;
if (ogGrid !== this.grid) return false;
const pointData = {
x: param.point.x,
y: param.point.y
};
// Only allow horizontal movement for vertical separator
this.addPoint1({ x: pointData.x, y: this._paneViews[0]._data.y || pointData.y });
this.requestUpdate();
}
paneViews() {
return this._paneViews;
}
updateAllViews() {
this._paneViews.forEach((view) => view.update());
}
setActive(active: boolean): void {
drawHolder.inactiveAllDrawings();
this.isActive = active;
this.requestUpdate();
}
setInactive() {
this.isActive = false;
this.requestUpdate();
}
setHover(hover: boolean, nearPart: 'line' | null = null): void {
if (!hover) {
setHoverDrawingUid(this.uid, false);
this.isHovered = false;
return;
}
this.isHovered = hover;
this.isNear = nearPart;
this.requestUpdate();
setHoverDrawingUid(this.uid, true);
}
hitTest(x: number, y: number) {
if (!this._paneViews[0]?._data?.lines || this._paneViews[0]._data.lines.length === 0) {
return null;
}
const lineWidth = 10;
const lines = this._paneViews[0]._data.lines;
// Check if hovering over any vertical line
for (const line of lines) {
if (line.x && Math.abs(x - line.x) <= lineWidth) {
this.setHover(true, 'line');
return {
cursorStyle: this.autoDetectEnabled ? 'default' : 'pointer',
externalId: 'line',
zOrder: 'top' as PrimitivePaneViewZOrder
};
}
}
this.setHover(false, null);
return null;
}
/**
* Get the current drawing configuration for server storage
*/
getDrawing() {
return {
uid: this.uid,
type: 'verticalseparator',
paneIndex: this.paneIndex,
grid: this.grid,
done: this.done,
clickCount: this.clickCount,
isActive: this.isActive,
autoDetectEnabled: this.autoDetectEnabled,
separatorLines: this._separatorLines.map(line => ({
price: line.price,
time: line.time,
lastTime: line.lastTime,
barDiff: line.barDiff
})),
options: { ...this.options },
timestamp: Date.now()
};
}
/**
* Restore drawing from server-stored configuration
*/
static setDrawing(drawingConfig: any, chart: any, series: any) {
if (!drawingConfig || !drawingConfig.point) {
throw new Error('Invalid drawing configuration: missing point data');
}
if (!chart || !series) {
throw new Error('Invalid chart or series instance');
}
const plugin = new VerticalSeparatorPlugin(
chart,
series,
drawingConfig.point,
drawingConfig.uid,
drawingConfig.paneIndex,
drawingConfig.grid
);
plugin.done = drawingConfig.done || false;
plugin.clickCount = drawingConfig.clickCount || 0;
plugin.isActive = drawingConfig.isActive || false;
if (drawingConfig.options) {
plugin.applyOptions(drawingConfig.options);
}
plugin._point = {
price: drawingConfig.point.price,
time: drawingConfig.point.time,
lastTime: drawingConfig.point.lastTime,
barDiff: drawingConfig.point.barDiff
};
series.attachPrimitive(plugin);
plugin.updateAllViews();
plugin.requestUpdate();
return plugin;
}
/**
* Update drawing from partial configuration
*/
updateFromConfig(partialConfig: any) {
if (partialConfig.point) {
this._point = {
price: partialConfig.point.price,
time: partialConfig.point.time,
lastTime: partialConfig.point.lastTime,
barDiff: partialConfig.point.barDiff
};
}
if (partialConfig.options) {
this.applyOptions(partialConfig.options);
}
if (partialConfig.done !== undefined) {
this.done = partialConfig.done;
}
if (partialConfig.clickCount !== undefined) {
this.clickCount = partialConfig.clickCount;
}
if (partialConfig.isActive !== undefined) {
this.isActive = partialConfig.isActive;
}
this.updateAllViews();
this.requestUpdate();
}
}
class VerticalSeparatorPaneView implements IPrimitivePaneView {
_source: VerticalSeparatorPlugin;
_data: { lines?: any[]; paneHeight?: number };
constructor(source) {
this._source = source;
this._data = {};
}
_calculatePoint(sourcePoint, price) {
if (!sourcePoint) return { x: 0, y: 0 };
const x = time2co(sourcePoint, this._source.myChart);
const series = this._source.mySeries;
// For vertical separator, we don't need a specific y coordinate
// We'll draw from top to bottom of the pane
let y = 0;
if (series && typeof price === 'number') {
y = series.priceToCoordinate(price);
if (y === null) {
y = 0;
}
}
return { x, y };
}
update() {
const { _separatorLines } = this._source;
// Get pane height for drawing full vertical lines
const paneHeight = this._source.myChart?.paneSize()?.height || 400;
// Calculate positions for all separator lines
const lines = _separatorLines.map(line => {
const p = this._calculatePoint(line, line?.price);
return {
x: p.x,
y: p.y,
time: line.time
};
});
this._data = {
lines,
paneHeight
};
}
renderer() {
return new VerticalSeparatorRenderer(this._data, this._source);
}
}
class VerticalSeparatorRenderer implements IPrimitivePaneRenderer {
_data: { lines?: any[]; paneHeight?: number };
_source: VerticalSeparatorPlugin;
constructor(data, source) {
this._data = data;
this._source = source;
}
draw(target) {
target.useBitmapCoordinateSpace((scope) => {
const ctx = scope.context;
const { lines, paneHeight } = this._data;
if (!lines || lines.length === 0 || paneHeight == null) return;
const { horizontalPixelRatio, verticalPixelRatio } = scope;
const { lineWidth, lineColor, lineStyle, showLabel, labelText, labelBackgroundColor, labelTextColor } = this._source.options;
// Draw each vertical separator line
lines.forEach((line, index) => {
const { x } = line;
if (x == null) return;
// Draw vertical line from top to bottom of pane
ctx.beginPath();
ctx.moveTo(Math.round(x * horizontalPixelRatio), 0);
ctx.lineTo(Math.round(x * horizontalPixelRatio), Math.round(paneHeight * verticalPixelRatio));
// Apply line style
if (lineStyle === 'dashed') {
ctx.setLineDash([6 * horizontalPixelRatio, 3 * horizontalPixelRatio]);
} else if (lineStyle === 'dotted') {
ctx.setLineDash([2 * horizontalPixelRatio, 2 * horizontalPixelRatio]);
} else {
ctx.setLineDash([]);
}
ctx.strokeStyle = lineColor;
ctx.lineWidth = lineWidth * horizontalPixelRatio;
ctx.stroke();
ctx.setLineDash([]);
// Draw glowing effect if hovered
if (this._source.isHovered && this._source.done) {
ctx.save();
ctx.beginPath();
ctx.moveTo(Math.round(x * horizontalPixelRatio), 0);
ctx.lineTo(Math.round(x * horizontalPixelRatio), Math.round(paneHeight * verticalPixelRatio));
ctx.strokeStyle = 'rgba(30, 144, 255, 0.3)';
ctx.lineWidth = lineWidth * 2 * horizontalPixelRatio;
ctx.shadowColor = 'rgba(30, 144, 255, 0.5)';
ctx.shadowBlur = lineWidth * 4 * horizontalPixelRatio;
ctx.stroke();
ctx.restore();
}
// Draw label if enabled (only on first line to avoid clutter)
if (showLabel && labelText && this._source.done && index === 0) {
ctx.save();
ctx.font = `${12 * verticalPixelRatio}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Measure text for background
const textMetrics = ctx.measureText(labelText);
const textWidth = textMetrics.width;
const textHeight = 12 * verticalPixelRatio;
const padding = 4 * horizontalPixelRatio;
// Position label at top of the line
const labelY = 20 * verticalPixelRatio;
const labelX = Math.round(x * horizontalPixelRatio);
// Draw background
ctx.fillStyle = labelBackgroundColor;
ctx.fillRect(
labelX - textWidth / 2 - padding,
labelY - textHeight / 2 - padding,
textWidth + padding * 2,
textHeight + padding * 2
);
// Draw text
ctx.fillStyle = labelTextColor;
ctx.fillText(labelText, labelX, labelY);
ctx.restore();
}
});
});
}
}
Metadata
Metadata
Assignees
Labels
No labels