infinite scroll fix
This commit is contained in:
@@ -334,6 +334,11 @@ class ChartManager {
|
||||
this.updateChartInfo(timeframe, eventData);
|
||||
});
|
||||
|
||||
// Add relayout handler for infinite scroll (load more data when zooming/panning)
|
||||
plotElement.on('plotly_relayout', (eventData) => {
|
||||
this.handleChartRelayout(timeframe, eventData);
|
||||
});
|
||||
|
||||
console.log(`Chart created for ${timeframe} with ${data.timestamps.length} candles`);
|
||||
}
|
||||
|
||||
@@ -1088,4 +1093,234 @@ class ChartManager {
|
||||
infoElement.textContent = `O: ${open.toFixed(2)} H: ${high.toFixed(2)} L: ${low.toFixed(2)} C: ${close.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle chart relayout for infinite scroll
|
||||
* Detects when user scrolls/zooms to edges and loads more data
|
||||
*/
|
||||
handleChartRelayout(timeframe, eventData) {
|
||||
const chart = this.charts[timeframe];
|
||||
if (!chart || !chart.data) return;
|
||||
|
||||
// Check if this is a range change (zoom/pan)
|
||||
if (!eventData['xaxis.range[0]'] && !eventData['xaxis.range']) return;
|
||||
|
||||
// Get current visible range
|
||||
const xRange = eventData['xaxis.range'] || [eventData['xaxis.range[0]'], eventData['xaxis.range[1]']];
|
||||
if (!xRange || xRange.length !== 2) return;
|
||||
|
||||
const visibleStart = new Date(xRange[0]);
|
||||
const visibleEnd = new Date(xRange[1]);
|
||||
|
||||
// Get data boundaries
|
||||
const dataStart = new Date(chart.data.timestamps[0]);
|
||||
const dataEnd = new Date(chart.data.timestamps[chart.data.timestamps.length - 1]);
|
||||
|
||||
// Calculate threshold (10% of visible range from edge)
|
||||
const visibleRange = visibleEnd - visibleStart;
|
||||
const threshold = visibleRange * 0.1;
|
||||
|
||||
// Check if we're near the left edge (need older data)
|
||||
const nearLeftEdge = (visibleStart - dataStart) < threshold;
|
||||
|
||||
// Check if we're near the right edge (need newer data)
|
||||
const nearRightEdge = (dataEnd - visibleEnd) < threshold;
|
||||
|
||||
console.log(`Relayout ${timeframe}: visible=${visibleStart.toISOString()} to ${visibleEnd.toISOString()}, data=${dataStart.toISOString()} to ${dataEnd.toISOString()}, nearLeft=${nearLeftEdge}, nearRight=${nearRightEdge}`);
|
||||
|
||||
// Load more data if near edges
|
||||
if (nearLeftEdge) {
|
||||
this.loadMoreData(timeframe, 'before', dataStart);
|
||||
} else if (nearRightEdge) {
|
||||
this.loadMoreData(timeframe, 'after', dataEnd);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load more historical data for a timeframe
|
||||
*/
|
||||
async loadMoreData(timeframe, direction, referenceTime) {
|
||||
const chart = this.charts[timeframe];
|
||||
if (!chart) return;
|
||||
|
||||
// Prevent multiple simultaneous loads
|
||||
if (chart.loading) {
|
||||
console.log(`Already loading data for ${timeframe}, skipping...`);
|
||||
return;
|
||||
}
|
||||
|
||||
chart.loading = true;
|
||||
this.showLoadingIndicator(timeframe, direction);
|
||||
|
||||
try {
|
||||
// Calculate time range to fetch
|
||||
const limit = 500; // Fetch 500 more candles
|
||||
let startTime, endTime;
|
||||
|
||||
if (direction === 'before') {
|
||||
// Load older data
|
||||
endTime = referenceTime.toISOString();
|
||||
startTime = null; // Let backend calculate based on limit
|
||||
} else {
|
||||
// Load newer data
|
||||
startTime = referenceTime.toISOString();
|
||||
endTime = null;
|
||||
}
|
||||
|
||||
console.log(`Loading ${limit} more candles ${direction} ${referenceTime.toISOString()} for ${timeframe}`);
|
||||
|
||||
// Fetch more data from backend
|
||||
const response = await fetch('/api/chart-data', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
symbol: window.appState?.currentSymbol || 'ETH/USDT',
|
||||
timeframes: [timeframe],
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
limit: limit,
|
||||
direction: direction
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.chart_data && result.chart_data[timeframe]) {
|
||||
const newData = result.chart_data[timeframe];
|
||||
|
||||
// Merge with existing data
|
||||
this.mergeChartData(timeframe, newData, direction);
|
||||
|
||||
console.log(`Loaded ${newData.timestamps.length} new candles for ${timeframe}`);
|
||||
window.showSuccess(`Loaded ${newData.timestamps.length} more candles`);
|
||||
} else {
|
||||
console.warn(`No more data available for ${timeframe} ${direction}`);
|
||||
window.showWarning('No more historical data available');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error loading more data for ${timeframe}:`, error);
|
||||
window.showError('Failed to load more data');
|
||||
} finally {
|
||||
chart.loading = false;
|
||||
this.hideLoadingIndicator(timeframe);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge new data with existing chart data
|
||||
*/
|
||||
mergeChartData(timeframe, newData, direction) {
|
||||
const chart = this.charts[timeframe];
|
||||
if (!chart || !chart.data) return;
|
||||
|
||||
const existingData = chart.data;
|
||||
let mergedData;
|
||||
|
||||
if (direction === 'before') {
|
||||
// Prepend older data
|
||||
mergedData = {
|
||||
timestamps: [...newData.timestamps, ...existingData.timestamps],
|
||||
open: [...newData.open, ...existingData.open],
|
||||
high: [...newData.high, ...existingData.high],
|
||||
low: [...newData.low, ...existingData.low],
|
||||
close: [...newData.close, ...existingData.close],
|
||||
volume: [...newData.volume, ...existingData.volume],
|
||||
pivot_markers: { ...newData.pivot_markers, ...existingData.pivot_markers }
|
||||
};
|
||||
} else {
|
||||
// Append newer data
|
||||
mergedData = {
|
||||
timestamps: [...existingData.timestamps, ...newData.timestamps],
|
||||
open: [...existingData.open, ...newData.open],
|
||||
high: [...existingData.high, ...newData.high],
|
||||
low: [...existingData.low, ...newData.low],
|
||||
close: [...existingData.close, ...newData.close],
|
||||
volume: [...existingData.volume, ...newData.volume],
|
||||
pivot_markers: { ...existingData.pivot_markers, ...newData.pivot_markers }
|
||||
};
|
||||
}
|
||||
|
||||
// Update stored data
|
||||
chart.data = mergedData;
|
||||
|
||||
// Update the chart with merged data
|
||||
this.updateSingleChart(timeframe, mergedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a single chart with new data
|
||||
*/
|
||||
updateSingleChart(timeframe, data) {
|
||||
const chart = this.charts[timeframe];
|
||||
if (!chart) return;
|
||||
|
||||
const plotId = chart.plotId;
|
||||
|
||||
// Create volume colors
|
||||
const volumeColors = data.close.map((close, i) => {
|
||||
if (i === 0) return '#3b82f6';
|
||||
return close >= data.open[i] ? '#10b981' : '#ef4444';
|
||||
});
|
||||
|
||||
// Update traces
|
||||
const update = {
|
||||
x: [data.timestamps, data.timestamps],
|
||||
open: [data.open],
|
||||
high: [data.high],
|
||||
low: [data.low],
|
||||
close: [data.close],
|
||||
y: [undefined, data.volume],
|
||||
'marker.color': [undefined, volumeColors]
|
||||
};
|
||||
|
||||
Plotly.restyle(plotId, update, [0, 1]);
|
||||
|
||||
console.log(`Updated ${timeframe} chart with ${data.timestamps.length} candles`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show loading indicator on chart
|
||||
*/
|
||||
showLoadingIndicator(timeframe, direction) {
|
||||
const chart = this.charts[timeframe];
|
||||
if (!chart) return;
|
||||
|
||||
const plotElement = chart.element;
|
||||
const loadingDiv = document.createElement('div');
|
||||
loadingDiv.id = `loading-${timeframe}`;
|
||||
loadingDiv.className = 'chart-loading-indicator';
|
||||
loadingDiv.innerHTML = `
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<span class="ms-2">Loading ${direction === 'before' ? 'older' : 'newer'} data...</span>
|
||||
`;
|
||||
loadingDiv.style.cssText = `
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
${direction === 'before' ? 'left' : 'right'}: 10px;
|
||||
background: rgba(31, 41, 55, 0.9);
|
||||
color: #f8f9fa;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
plotElement.parentElement.style.position = 'relative';
|
||||
plotElement.parentElement.appendChild(loadingDiv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide loading indicator
|
||||
*/
|
||||
hideLoadingIndicator(timeframe) {
|
||||
const loadingDiv = document.getElementById(`loading-${timeframe}`);
|
||||
if (loadingDiv) {
|
||||
loadingDiv.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user