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

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

View File

@ -215,7 +215,7 @@
</nav>
<!-- 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]">
<!-- Hidden tab buttons triggers -->
<div class="flex space-x-2">

View File

@ -136,6 +136,19 @@ function 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 {
constructor() {
this.chart = null;
@ -157,6 +170,9 @@ constructor() {
this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price }
this.currentMouseTime = null;
// Throttled versions of heavy functions
this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150);
this.init();
}
@ -756,7 +772,6 @@ onVisibleRangeChange() {
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));
}

View File

@ -966,16 +966,30 @@ class SeriesAreaFillRenderer {
const color = this._source._color;
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.beginPath();
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
for (let i = 0; i < data.length; i++) {
const point = data[i];
const timeCoordinate = chart.timeScale().timeToCoordinate(point.time);
if (timeCoordinate === null) continue;
const timeCoordinate = timeScale.timeToCoordinate(point.time);
// 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);
if (upperY === null) continue;
@ -994,8 +1008,12 @@ class SeriesAreaFillRenderer {
// Draw bottom line (lower) backward
for (let i = data.length - 1; i >= 0; i--) {
const point = data[i];
const timeCoordinate = chart.timeScale().timeToCoordinate(point.time);
if (timeCoordinate === null) continue;
const timeCoordinate = timeScale.timeToCoordinate(point.time);
if (timeCoordinate === null) {
if (started && i < data.length / 2) break;
continue;
}
const lowerY = series.priceToCoordinate(point.lower);
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)
let results = indicator.cachedResults;
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);
indicator.cachedResults = results;
}
if (!results || !Array.isArray(results)) {
console.error(`[renderIndicatorOnPane] ${indicator.name}: Failed to get valid results (got ${typeof results})`);
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 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;
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) {
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') {
isObjectResult = true;
}
let plotsCreated = 0;
indicator.series = indicator.series || [];
let seriesIdx = 0;
// Special logic for Hurst fill
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) => {
if (isObjectResult) {
@ -1075,17 +1076,13 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
}
const plotColor = indicator.params[`_color_${plotIdx}`] || plot.color || '#2962ff';
const data = [];
for (let i = 0; i < candles.length; i++) {
let value;
if (isObjectResult) {
value = results[i]?.[plot.id];
// Collect fill data if this is Hurst
if (isFirstHurst && results[i]) {
// Ensure we only add once per index
if (!hurstFillData[i]) hurstFillData[i] = { time: candles[i].time };
if (plot.id === 'upper') hurstFillData[i].upper = 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)) {
data.push({
time: candles[i].time,
value: value
});
data.push({ time: candles[i].time, value: value });
}
}
if (data.length === 0) return;
let series;
let series = indicator.series[seriesIdx];
let plotLineStyle = lineStyle;
if (plot.style === 'dashed') plotLineStyle = LightweightCharts.LineStyle.Dashed;
else if (plot.style === 'dotted') plotLineStyle = LightweightCharts.LineStyle.Dotted;
else if (plot.style === 'solid') plotLineStyle = LightweightCharts.LineStyle.Solid;
if (plot.type === 'histogram') {
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, {
const seriesOptions = {
color: plotColor,
lineWidth: plot.width || indicator.params._lineWidth || lineWidth,
lineStyle: plotLineStyle,
@ -1141,14 +1112,35 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
priceLineVisible: false,
lastValueVisible: plot.lastValueVisible !== false,
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);
} 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);
indicator.series.push(series);
plotsCreated++;
seriesIdx++;
// Attach RSI bands
// RSI bands
if (meta.name === 'RSI' && indicator.series.length > 0) {
const mainSeries = indicator.series[0];
const overbought = indicator.params.overbought || 70;
@ -1160,32 +1152,31 @@ function renderIndicatorOnPane(indicator, meta, instance, candles, paneIndex, li
indicator.bands = indicator.bands || [];
indicator.bands.push(mainSeries.createPriceLine({
price: overbought,
color: '#787B86',
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed,
axisLabelVisible: false,
title: ''
price: overbought, color: '#787B86', lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed, axisLabelVisible: false, title: ''
}));
indicator.bands.push(mainSeries.createPriceLine({
price: oversold,
color: '#787B86',
lineWidth: 1,
lineStyle: LightweightCharts.LineStyle.Dashed,
axisLabelVisible: false,
title: ''
price: oversold, color: '#787B86', 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
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);
// Attach to the first series (usually upper or lower band)
const fillPrimitive = new SeriesAreaFillPrimitive(validFillData, 'rgba(128, 128, 128, 0.05)');
indicator.series[0].attachPrimitive(fillPrimitive);
if (!indicator.fillPrimitive) {
indicator.fillPrimitive = new SeriesAreaFillPrimitive(validFillData, 'rgba(128, 128, 128, 0.05)');
indicator.series[0].attachPrimitive(indicator.fillPrimitive);
} else {
indicator.fillPrimitive.setData(validFillData);
}
}
}
@ -1246,13 +1237,13 @@ export function drawIndicatorsOnChart() {
const activeIndicators = getActiveIndicators();
// Remove all existing series
activeIndicators.forEach(ind => {
ind.series?.forEach(s => {
try { window.dashboard.chart.removeSeries(s); } catch(e) {}
});
ind.series = [];
});
// Remove all existing series - OPTIMIZATION: Removed aggressive clearing to allow reuse
// activeIndicators.forEach(ind => {
// ind.series?.forEach(s => {
// try { window.dashboard.chart.removeSeries(s); } catch(e) {}
// });
// ind.series = [];
// });
const lineStyleMap = {
'solid': LightweightCharts.LineStyle.Solid,
@ -1271,6 +1262,13 @@ export function drawIndicatorsOnChart() {
// Process all indicators, filtering by visibility
activeIndicators.forEach(ind => {
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;
}