fix: indicator panel overlap and performance optimizations

This commit is contained in:
DiTus
2026-03-20 22:32:00 +01:00
parent 0e8bf8e3bd
commit 62eeffaf1d
5 changed files with 110 additions and 101 deletions

View File

@ -167,15 +167,11 @@ body {
/* Indicator Panel (Sidebar/Modal Adaptation) */ /* Indicator Panel (Sidebar/Modal Adaptation) */
/* We will float the sidebar over the content or use it as a modal */ /* We will float the sidebar over the content or use it as a modal */
#rightSidebar { #rightSidebar {
position: fixed; /* Position handled by Tailwind classes in HTML */
top: 64px; /* Below header */
right: 0;
bottom: 64px; /* Above bottom nav */
width: 350px;
background-color: #1a2333; background-color: #1a2333;
border-left: 1px solid #2d3a4f; border-left: 1px solid #2d3a4f;
z-index: 40; z-index: 40;
transform: translateX(100%); /* Transform handled by Tailwind/Inline styles */
transition: transform 0.3s ease-in-out; transition: transform 0.3s ease-in-out;
box-shadow: -4px 0 20px rgba(0,0,0,0.5); box-shadow: -4px 0 20px rgba(0,0,0,0.5);
display: flex; display: flex;

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

View File

@ -215,7 +215,7 @@
</nav> </nav>
<!-- Sidebar Overlay (Acts as Modal) --> <!-- Sidebar Overlay (Acts as Modal) -->
<div id="rightSidebar" class="collapsed fixed top-[64px] right-0 md:right-[72px] bottom-[64px] md:bottom-0 w-full max-w-[350px] bg-[#1a2333] border-l border-[#2d3a4f] shadow-2xl z-40 flex flex-col"> <div id="rightSidebar" class="collapsed fixed top-[64px] right-0 md:right-[72px] bottom-[64px] md:bottom-0 w-full max-w-[370px] bg-[#1a2333] border-l border-[#2d3a4f] shadow-2xl z-40 flex flex-col">
<div class="flex justify-between items-center p-3 border-b border-[#2d3a4f] bg-[#1a2333]"> <div class="flex justify-between items-center p-3 border-b border-[#2d3a4f] bg-[#1a2333]">
<!-- Hidden tab buttons triggers --> <!-- Hidden tab buttons triggers -->
<div class="flex space-x-2"> <div class="flex space-x-2">

View File

@ -136,6 +136,19 @@ function formatDate(timestamp) {
return TimezoneConfig.formatDate(timestamp); return TimezoneConfig.formatDate(timestamp);
} }
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
}
}
export class TradingDashboard { export class TradingDashboard {
constructor() { constructor() {
this.chart = null; this.chart = null;
@ -157,6 +170,9 @@ constructor() {
this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price } this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price }
this.currentMouseTime = null; this.currentMouseTime = null;
// Throttled versions of heavy functions
this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150);
this.init(); this.init();
} }
@ -756,7 +772,6 @@ onVisibleRangeChange() {
console.log(`[VisibleRange] Chart data (${data.length}) vs dataset (${allData?.length || 0}) differ, redrawing indicators...`); console.log(`[VisibleRange] Chart data (${data.length}) vs dataset (${allData?.length || 0}) differ, redrawing indicators...`);
} }
window.drawIndicatorsOnChart?.();
this.loadSignals().catch(e => console.error('Error loading signals:', e)); this.loadSignals().catch(e => console.error('Error loading signals:', e));
} }

View File

@ -966,16 +966,30 @@ class SeriesAreaFillRenderer {
const color = this._source._color; const color = this._source._color;
const ratio = scope.horizontalPixelRatio; const ratio = scope.horizontalPixelRatio;
// Optimization: Get visible range to avoid iterating over thousands of historical points
const timeScale = chart.timeScale();
const visibleRange = timeScale.getVisibleLogicalRange();
if (!visibleRange) return;
ctx.save(); ctx.save();
ctx.beginPath(); ctx.beginPath();
let started = false; let started = false;
// Find start and end indices in data based on visible range for massive performance boost
// Since data is sorted by time, we could use binary search, but even a linear scan
// with visibility check is better than drawing everything.
// Draw top line (upper) forward // Draw top line (upper) forward
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const point = data[i]; const point = data[i];
const timeCoordinate = chart.timeScale().timeToCoordinate(point.time); const timeCoordinate = timeScale.timeToCoordinate(point.time);
if (timeCoordinate === null) continue;
// Skip points far outside the visible area
if (timeCoordinate === null) {
if (started) break; // We've passed the visible range
continue;
}
const upperY = series.priceToCoordinate(point.upper); const upperY = series.priceToCoordinate(point.upper);
if (upperY === null) continue; if (upperY === null) continue;
@ -994,8 +1008,12 @@ class SeriesAreaFillRenderer {
// Draw bottom line (lower) backward // Draw bottom line (lower) backward
for (let i = data.length - 1; i >= 0; i--) { for (let i = data.length - 1; i >= 0; i--) {
const point = data[i]; const point = data[i];
const timeCoordinate = chart.timeScale().timeToCoordinate(point.time); const timeCoordinate = timeScale.timeToCoordinate(point.time);
if (timeCoordinate === null) continue;
if (timeCoordinate === null) {
if (started && i < data.length / 2) break;
continue;
}
const lowerY = series.priceToCoordinate(point.lower); const lowerY = series.priceToCoordinate(point.lower);
if (lowerY === null) continue; if (lowerY === null) continue;
@ -1021,30 +1039,15 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
// Recalculate with current TF candles (or use cached if they exist and are the correct length) // Recalculate with current TF candles (or use cached if they exist and are the correct length)
let results = indicator.cachedResults; let results = indicator.cachedResults;
if (!results || !Array.isArray(results) || results.length !== candles.length) { if (!results || !Array.isArray(results) || results.length !== candles.length) {
console.log(`[renderIndicatorOnPane] ${indicator.name}: Calling instance.calculate()...`); // console.log(`[renderIndicatorOnPane] ${indicator.name}: Recalculating... (${candles.length} candles)`);
results = instance.calculate(candles); results = instance.calculate(candles);
indicator.cachedResults = results; indicator.cachedResults = results;
} }
if (!results || !Array.isArray(results)) { if (!results || !Array.isArray(results)) {
console.error(`[renderIndicatorOnPane] ${indicator.name}: Failed to get valid results (got ${typeof results})`);
return; return;
} }
if (results.length !== candles.length) {
console.error(`[renderIndicatorOnPane] ${indicator.name}: MISMATCH! Expected ${candles.length} results but got ${results.length}`);
}
// Clear previous series for this indicator
if (indicator.series && indicator.series.length > 0) {
indicator.series.forEach(s => {
try {
window.dashboard.chart.removeSeries(s);
} catch(e) {}
});
}
indicator.series = [];
const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid; const lineStyle = lineStyleMap[indicator.params._lineType] || LightweightCharts.LineStyle.Solid;
const lineWidth = indicator.params._lineWidth || 1; const lineWidth = indicator.params._lineWidth || 1;
@ -1052,21 +1055,19 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
const firstNonNull = Array.isArray(results) ? results.find(r => r !== null && r !== undefined) : null; const firstNonNull = Array.isArray(results) ? results.find(r => r !== null && r !== undefined) : null;
let isObjectResult = firstNonNull && typeof firstNonNull === 'object' && !Array.isArray(firstNonNull); let isObjectResult = firstNonNull && typeof firstNonNull === 'object' && !Array.isArray(firstNonNull);
// Fallback: If results are all null (e.g. during warmup or MTF fetch),
// use metadata to determine if it SHOULD be an object result
if (!firstNonNull && meta.plots && meta.plots.length > 1) { if (!firstNonNull && meta.plots && meta.plots.length > 1) {
isObjectResult = true; isObjectResult = true;
} }
// Also check if the only plot has a specific ID that isn't just a number
if (!firstNonNull && meta.plots && meta.plots.length === 1 && meta.plots[0].id !== 'value') { if (!firstNonNull && meta.plots && meta.plots.length === 1 && meta.plots[0].id !== 'value') {
isObjectResult = true; isObjectResult = true;
} }
let plotsCreated = 0; indicator.series = indicator.series || [];
let seriesIdx = 0;
// Special logic for Hurst fill // Special logic for Hurst fill
let hurstFillData = []; let hurstFillData = [];
const isFirstHurst = indicator.type === 'hurst' && activeIndicators.filter(ind => ind.type === 'hurst')[0].id === indicator.id; const isFirstHurst = indicator.type === 'hurst' && activeIndicators.filter(ind => ind.type === 'hurst')[0]?.id === indicator.id;
meta.plots.forEach((plot, plotIdx) => { meta.plots.forEach((plot, plotIdx) => {
if (isObjectResult) { if (isObjectResult) {
@ -1075,17 +1076,13 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
} }
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff'; const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
const data = []; const data = [];
for (let i = 0; i < candles.length; i++) { for (let i = 0; i < candles.length; i++) {
let value; let value;
if (isObjectResult) { if (isObjectResult) {
value = results[i]?.[plot.id]; value = results[i]?.[plot.id];
// Collect fill data if this is Hurst
if (isFirstHurst && results[i]) { if (isFirstHurst && results[i]) {
// Ensure we only add once per index
if (!hurstFillData[i]) hurstFillData[i] = { time: candles[i].time }; if (!hurstFillData[i]) hurstFillData[i] = { time: candles[i].time };
if (plot.id === 'upper') hurstFillData[i].upper = value; if (plot.id === 'upper') hurstFillData[i].upper = value;
if (plot.id === 'lower') hurstFillData[i].lower = value; if (plot.id === 'lower') hurstFillData[i].lower = value;
@ -1095,45 +1092,19 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
} }
if (value !== null && value !== undefined && typeof value === 'number' && Number.isFinite(value)) { if (value !== null && value !== undefined && typeof value === 'number' && Number.isFinite(value)) {
data.push({ data.push({ time: candles[i].time, value: value });
time: candles[i].time,
value: value
});
} }
} }
if (data.length === 0) return; if (data.length === 0) return;
let series; let series = indicator.series[seriesIdx];
let plotLineStyle = lineStyle; let plotLineStyle = lineStyle;
if (plot.style === 'dashed') plotLineStyle = LightweightCharts.LineStyle.Dashed; if (plot.style === 'dashed') plotLineStyle = LightweightCharts.LineStyle.Dashed;
else if (plot.style === 'dotted') plotLineStyle = LightweightCharts.LineStyle.Dotted; else if (plot.style === 'dotted') plotLineStyle = LightweightCharts.LineStyle.Dotted;
else if (plot.style === 'solid') plotLineStyle = LightweightCharts.LineStyle.Solid; else if (plot.style === 'solid') plotLineStyle = LightweightCharts.LineStyle.Solid;
if (plot.type === 'histogram') { const seriesOptions = {
series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, {
color: plotColor,
priceFormat: { type: 'price', precision: 0, minMove: 1 },
priceLineVisible: false,
lastValueVisible: false
}, paneIndex);
} else if (plot.type === 'baseline') {
series = window.dashboard.chart.addSeries(LightweightCharts.BaselineSeries, {
baseValue: { type: 'price', price: plot.baseValue || 0 },
topLineColor: plot.topLineColor || plotColor,
topFillColor1: plot.topFillColor1 || plotColor,
topFillColor2: '#00000000',
bottomFillColor1: '#00000000',
bottomColor: plot.bottomColor || '#00000000',
lineWidth: plot.width || indicator.params._lineWidth || lineWidth,
lineStyle: plotLineStyle,
title: plot.title || '',
priceLineVisible: false,
lastValueVisible: plot.lastValueVisible !== false,
priceFormat: { type: 'price', precision: 0, minMove: 1 }
}, paneIndex);
} else {
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, {
color: plotColor, color: plotColor,
lineWidth: plot.width || indicator.params._lineWidth || lineWidth, lineWidth: plot.width || indicator.params._lineWidth || lineWidth,
lineStyle: plotLineStyle, lineStyle: plotLineStyle,
@ -1141,14 +1112,35 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
priceLineVisible: false, priceLineVisible: false,
lastValueVisible: plot.lastValueVisible !== false, lastValueVisible: plot.lastValueVisible !== false,
priceFormat: { type: 'price', precision: 0, minMove: 1 } priceFormat: { type: 'price', precision: 0, minMove: 1 }
};
if (!series) {
// Create new series if it doesn't exist
if (plot.type === 'histogram') {
series = window.dashboard.chart.addSeries(LightweightCharts.HistogramSeries, seriesOptions, paneIndex);
} else if (plot.type === 'baseline') {
series = window.dashboard.chart.addSeries(LightweightCharts.BaselineSeries, {
...seriesOptions,
baseValue: { type: 'price', price: plot.baseValue || 0 },
topLineColor: plot.topLineColor || plotColor,
topFillColor1: plot.topFillColor1 || plotColor,
topFillColor2: '#00000000',
bottomFillColor1: '#00000000',
bottomColor: plot.bottomColor || '#00000000',
}, paneIndex); }, paneIndex);
} else {
series = window.dashboard.chart.addSeries(LightweightCharts.LineSeries, seriesOptions, paneIndex);
}
indicator.series[seriesIdx] = series;
} else {
// Update existing series options
series.applyOptions(seriesOptions);
} }
series.setData(data); series.setData(data);
indicator.series.push(series); seriesIdx++;
plotsCreated++;
// Attach RSI bands // RSI bands
if (meta.name === 'RSI' && indicator.series.length > 0) { if (meta.name === 'RSI' && indicator.series.length > 0) {
const mainSeries = indicator.series[0]; const mainSeries = indicator.series[0];
const overbought = indicator.params.overbought || 70; const overbought = indicator.params.overbought || 70;
@ -1160,32 +1152,31 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
indicator.bands = indicator.bands || []; indicator.bands = indicator.bands || [];
indicator.bands.push(mainSeries.createPriceLine({ indicator.bands.push(mainSeries.createPriceLine({
price: overbought, price: overbought, color: '#787B86', lineWidth: 1,
color: '#787B86', lineStyle: LightweightCharts.LineStyle.Dashed, axisLabelVisible: false, title: ''
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed,
axisLabelVisible: false,
title: ''
})); }));
indicator.bands.push(mainSeries.createPriceLine({ indicator.bands.push(mainSeries.createPriceLine({
price: oversold, price: oversold, color: '#787B86', lineWidth: 1,
color: '#787B86', lineStyle: LightweightCharts.LineStyle.Dashed, axisLabelVisible: false, title: ''
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed,
axisLabelVisible: false,
title: ''
})); }));
} }
}); });
// Cleanup extra series if any
while (indicator.series.length > seriesIdx) {
const extra = indicator.series.pop();
try { window.dashboard.chart.removeSeries(extra); } catch(e) {}
}
// Attach Hurst Fill Primitive // Attach Hurst Fill Primitive
if (isFirstHurst && hurstFillData.length > 0 && indicator.series.length > 0) { if (isFirstHurst && hurstFillData.length > 0 && indicator.series.length > 0) {
// Filter out incomplete data points
const validFillData = hurstFillData.filter(d => d && d.time && d.upper !== undefined && d.lower !== undefined); const validFillData = hurstFillData.filter(d => d && d.time && d.upper !== undefined && d.lower !== undefined);
if (!indicator.fillPrimitive) {
// Attach to the first series (usually upper or lower band) indicator.fillPrimitive = new SeriesAreaFillPrimitive(validFillData, 'rgba(128, 128, 128, 0.05)');
const fillPrimitive = new SeriesAreaFillPrimitive(validFillData, 'rgba(128, 128, 128, 0.05)'); indicator.series[0].attachPrimitive(indicator.fillPrimitive);
indicator.series[0].attachPrimitive(fillPrimitive); } else {
indicator.fillPrimitive.setData(validFillData);
}
} }
} }
@ -1246,13 +1237,13 @@ export function drawIndicatorsOnChart() {
const activeIndicators = getActiveIndicators(); const activeIndicators = getActiveIndicators();
// Remove all existing series // Remove all existing series - OPTIMIZATION: Removed aggressive clearing to allow reuse
activeIndicators.forEach(ind => { // activeIndicators.forEach(ind => {
ind.series?.forEach(s => { // ind.series?.forEach(s => {
try { window.dashboard.chart.removeSeries(s); } catch(e) {} // try { window.dashboard.chart.removeSeries(s); } catch(e) {}
}); // });
ind.series = []; // ind.series = [];
}); // });
const lineStyleMap = { const lineStyleMap = {
'solid': LightweightCharts.LineStyle.Solid, 'solid': LightweightCharts.LineStyle.Solid,
@ -1271,6 +1262,13 @@ export function drawIndicatorsOnChart() {
// Process all indicators, filtering by visibility // Process all indicators, filtering by visibility
activeIndicators.forEach(ind => { activeIndicators.forEach(ind => {
if (ind.visible === false || ind.visible === 'false') { if (ind.visible === false || ind.visible === 'false') {
// Hide existing series if they exist by setting empty data
if (ind.series && ind.series.length > 0) {
ind.series.forEach(s => s.setData([]));
}
if (ind.fillPrimitive) {
ind.fillPrimitive.setData([]);
}
return; return;
} }