fix: indicator panel overlap and performance optimizations
This commit is contained in:
@ -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;
|
||||
|
||||
BIN
ignore/indicator_panel issue.PNG
Normal file
BIN
ignore/indicator_panel issue.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 181 KiB |
@ -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">
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user