Kaynağa Gözat

价格同步可以了

skyffire 6 ay önce
ebeveyn
işleme
977112e73d
1 değiştirilmiş dosya ile 137 ekleme ve 43 silme
  1. 137 43
      templates/index.html

+ 137 - 43
templates/index.html

@@ -5,7 +5,6 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>价格与价差监控 MUBARAK/USDT</title>
     <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
-    <!-- 引入 chartjs-plugin-zoom 插件 -->
     <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js"></script>
     <style>
         body { font-family: Arial, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
@@ -21,12 +20,12 @@
         .timestamp { font-size: 0.9em; color: #666; text-align: right; margin-top: 15px; }
         .chart-container {
             position: relative;
-            height: 45vh; /* Slightly increased height for better zoom interaction */
+            height: 45vh;
             width: 90vw;
             margin: auto;
-            margin-bottom: 10px; /* Reduced bottom margin */
+            margin-bottom: 10px;
         }
-        .controls-container { /* Container for buttons */
+        .controls-container {
             text-align: center;
             margin-bottom: 20px;
             margin-top: 5px;
@@ -48,7 +47,7 @@
             background-color: #0056b3;
         }
         .pause-button-active {
-            background-color: #ffc107; /* Yellow when active (paused) */
+            background-color: #ffc107;
             color: #333;
         }
         .pause-button-active:hover {
@@ -57,6 +56,7 @@
     </style>
 </head>
 <body>
+    <!-- HTML 结构与之前相同,此处省略 -->
     <div class="container">
         <h1>MUBARAK/USDT 价格监控</h1>
         <div class="controls-container">
@@ -113,21 +113,44 @@
     <script>
         let priceChartInstance = null;
         let diffChartInstance = null;
-        // const MAX_CHART_POINTS = 60; // MAX_HISTORY_POINTS is defined in backend
 
         let dataUpdateIntervalID = null;
         let isPaused = false;
-        const REFRESH_INTERVAL_MS = 5000; // 5 seconds
+        const REFRESH_INTERVAL_MS = 5000;
         const pauseResumeButton = document.getElementById('pause-resume-button');
 
+        let isSyncingZoomPan = false; // 标志位,防止同步死循环
+
         function formatPrice(priceStr) {
             if (priceStr === null || priceStr === undefined || priceStr === "N/A") return "N/A";
             const price = parseFloat(priceStr);
             return isNaN(price) ? "N/A" : price.toFixed(6);
         }
 
-        function initializeChart(ctx, chartType, datasetsConfig, initialLabels, titleText) {
-            return new Chart(ctx, {
+        // 函数:从源图表同步X轴到目标图表
+        function syncXAxes(sourceChart, targetChart) {
+            if (isSyncingZoomPan || !sourceChart || !targetChart) return; // 如果正在同步或图表未定义,则跳过
+
+            isSyncingZoomPan = true; // 设置标志位
+
+            const sourceXScale = sourceChart.scales.x;
+            const targetXScale = targetChart.scales.x;
+
+            if (sourceXScale && targetXScale) {
+                targetXScale.options.min = sourceXScale.min; // 同步X轴的最小值
+                targetXScale.options.max = sourceXScale.max; // 同步X轴的最大值
+                targetChart.update('none'); // 更新目标图表,'none' 表示不执行动画
+            }
+
+            // 短暂延迟后重置标志位,允许下一次同步
+            // 如果不加延迟,快速连续操作可能会有问题
+            requestAnimationFrame(() => {
+                 isSyncingZoomPan = false;
+            });
+        }
+
+        function initializeChart(ctx, chartIdentifier, chartType, datasetsConfig, initialLabels, titleText) {
+            const chartInstance = new Chart(ctx, {
                 type: 'line',
                 data: {
                     labels: initialLabels,
@@ -137,11 +160,12 @@
                     responsive: true,
                     maintainAspectRatio: false,
                     animation: {
-                        duration: 150 // slightly faster animation
+                        duration: 150
                     },
                     scales: {
                         x: {
-                            title: { display: true, text: '时间' }
+                            title: { display: true, text: '时间' },
+                            // 我们将通过回调函数同步 min/max,这里不设置
                         },
                         y: {
                             title: { display: true, text: chartType === 'price' ? '价格 (USDT)' : '价差 (%)' },
@@ -151,24 +175,31 @@
                     plugins: {
                         legend: { position: 'top' },
                         title: { display: true, text: titleText },
-                        zoom: { // Zoom plugin configuration
+                        zoom: {
                             pan: {
                                 enabled: true,
-                                mode: 'xy', // Allow panning in x and y directions
-                                threshold: 5, // Pixels before pan starts
+                                mode: 'x', // 只在X轴上平移 (Y轴通常不需要同步)
+                                threshold: 5,
+                                onPanComplete({chart}) { // 平移完成回调
+                                    if (chartIdentifier === 'price' && diffChartInstance) {
+                                        syncXAxes(chart, diffChartInstance);
+                                    } else if (chartIdentifier === 'diff' && priceChartInstance) {
+                                        syncXAxes(chart, priceChartInstance);
+                                    }
+                                }
                             },
                             zoom: {
-                                wheel: {
-                                    enabled: true, // Enable zooming with mouse wheel
-                                },
-                                pinch: {
-                                    enabled: true // Enable zooming with pinch gesture (touch devices)
-                                },
-                                drag: { // Enable drag-to-zoom (box select)
-                                    enabled: true,
-                                    backgroundColor: 'rgba(0,123,255,0.25)'
-                                },
-                                mode: 'xy', // Allow zooming in x and y directions
+                                wheel: { enabled: true, speed: 0.1 }, // 调整滚轮速度
+                                pinch: { enabled: true },
+                                drag: { enabled: true, backgroundColor: 'rgba(0,123,255,0.25)'},
+                                mode: 'x', // 只在X轴上缩放
+                                onZoomComplete({chart}) { // 缩放完成回调
+                                    if (chartIdentifier === 'price' && diffChartInstance) {
+                                        syncXAxes(chart, diffChartInstance);
+                                    } else if (chartIdentifier === 'diff' && priceChartInstance) {
+                                        syncXAxes(chart, priceChartInstance);
+                                    }
+                                }
                             }
                         }
                     },
@@ -177,24 +208,53 @@
                     }
                 }
             });
+            return chartInstance;
         }
 
         function updateChartData(chartInstance, newLabels, newDatasetsData) {
+            if (!chartInstance) return; // 确保图表实例存在
+
+            const currentXMin = chartInstance.scales.x.min;
+            const currentXMax = chartInstance.scales.x.max;
+            const isZoomed = (currentXMin !== undefined && currentXMax !== undefined &&
+                             (currentXMin !== chartInstance.data.labels[0] || currentXMax !== chartInstance.data.labels[chartInstance.data.labels.length - 1]));
+
             chartInstance.data.labels = newLabels;
             newDatasetsData.forEach((datasetData, index) => {
                 chartInstance.data.datasets[index].data = datasetData;
             });
+
+            // 如果图表之前是缩放状态,并且新的标签范围与旧的缩放范围不完全匹配,
+            // 尝试保持缩放,否则新的数据可能会导致缩放重置。
+            // 但这里简单处理:如果暂停,不更新 x 轴的 min/max,让用户控制。
+            // 如果正在刷新,则让图表根据新数据自动调整。
+            // 若要更精细控制 (如保持缩放比例滚动),会更复杂。
+            if (!isPaused) {
+                 // 当数据刷新时,如果用户是手动缩放的,我们不应该重置它
+                 // Chart.js 会在数据更新时自动调整范围,除非 min/max 固定
+                 // 为了允许新数据扩展X轴,我们不在这里设置 min/max
+                 // 除非我们想实现固定窗口滚动
+            } else {
+                // 如果暂停了,并且图表被用户缩放了,保持这个缩放
+                if(isZoomed) {
+                    chartInstance.options.scales.x.min = currentXMin;
+                    chartInstance.options.scales.x.max = currentXMax;
+                } else {
+                    // 如果没缩放,允许图表自动调整(尽管暂停时数据不会变)
+                    chartInstance.options.scales.x.min = undefined;
+                    chartInstance.options.scales.x.max = undefined;
+                }
+            }
+
             chartInstance.update('quiet');
         }
 
         function updateDisplayAndCharts() {
-            // If paused by user, don't fetch new data unless it's an initial call or manual resume
-            // This check is implicitly handled by not having setInterval running when paused.
-
             fetch('/data')
                 .then(response => response.json())
                 .then(data => {
                     const current = data.current;
+                    // ... (表格数据更新部分与之前相同,此处省略以保持简洁) ...
                     document.getElementById('oo-price').textContent = formatPrice(current.oo_price);
                     document.getElementById('gate-price').textContent = formatPrice(current.gate_price);
 
@@ -226,30 +286,42 @@
 
                     const history = data.history;
 
+                    // --- 价格历史图表 ---
                     const priceCtx = document.getElementById('priceHistoryChart').getContext('2d');
                     const priceDatasets = [
                         { label: 'OpenOcean', data: history.prices.oo, borderColor: 'rgb(75, 192, 192)', tension: 0.1, fill: false },
                         { label: 'Gate.io', data: history.prices.gate, borderColor: 'rgb(255, 99, 132)', tension: 0.1, fill: false }
                     ];
                     if (!priceChartInstance) {
-                        priceChartInstance = initializeChart(priceCtx, 'price', priceDatasets, history.prices.labels, '价格历史 (MUBARAK/USDT)');
+                        priceChartInstance = initializeChart(priceCtx, 'price', 'price', priceDatasets, history.prices.labels, '价格历史 (MUBARAK/USDT)');
                     } else {
                         updateChartData(priceChartInstance, history.prices.labels, [history.prices.oo, history.prices.gate]);
                     }
 
+                    // --- 价差历史图表 ---
                     const diffCtx = document.getElementById('diffHistoryChart').getContext('2d');
                     const diffDatasets = [{ label: '价差百分比 (OO vs Gate)', data: history.difference.values, borderColor: 'rgb(54, 162, 235)', tension: 0.1, fill: false }];
                     if (!diffChartInstance) {
-                        diffChartInstance = initializeChart(diffCtx, 'diff', diffDatasets, history.difference.labels, '价差百分比历史');
+                        diffChartInstance = initializeChart(diffCtx, 'diff', 'diff', diffDatasets, history.difference.labels, '价差百分比历史');
                     } else {
                          updateChartData(diffChartInstance, history.difference.labels, [history.difference.values]);
                     }
+
+                    // 初始加载或取消暂停时، 手动同步一次 (如果 X 轴不同步)
+                    // 避免初始加载时两个图表的 x 轴范围不一样
+                    if (priceChartInstance && diffChartInstance &&
+                        (priceChartInstance.scales.x.min !== diffChartInstance.scales.x.min ||
+                         priceChartInstance.scales.x.max !== diffChartInstance.scales.x.max) &&
+                        (!isPaused || !dataUpdateIntervalID ) // 只有在首次加载或从暂停恢复时才这样做
+                    ) {
+                        // console.log("Initial or resume sync needed");
+                        // syncXAxes(priceChartInstance, diffChartInstance); // 让价格图表作为主导
+                    }
+
                 })
                 .catch(error => {
                     console.error('Error fetching data:', error);
-                    document.getElementById('oo-price').textContent = '错误';
-                    document.getElementById('gate-price').textContent = '错误';
-                    document.getElementById('diff-percentage').textContent = '无法获取数据';
+                    // ... (错误处理与之前相同) ...
                 });
         }
 
@@ -257,40 +329,62 @@
             isPaused = !isPaused;
             if (isPaused) {
                 clearInterval(dataUpdateIntervalID);
-                dataUpdateIntervalID = null; // Clear the ID
+                dataUpdateIntervalID = null;
                 pauseResumeButton.textContent = '继续刷新';
                 pauseResumeButton.classList.add('pause-button-active');
                 console.log("Data refresh PAUSED");
             } else {
                 pauseResumeButton.textContent = '暂停刷新';
                 pauseResumeButton.classList.remove('pause-button-active');
-                updateDisplayAndCharts(); // Refresh immediately upon resuming
+                updateDisplayAndCharts(); // Refresh immediately
                 dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
                 console.log("Data refresh RESUMED");
             }
         }
 
+        function resetAllZooms() {
+            if (priceChartInstance) {
+                priceChartInstance.resetZoom();
+                // 重置后,手动清除 options 里的 min/max,确保下次数据更新能自动调整
+                priceChartInstance.options.scales.x.min = undefined;
+                priceChartInstance.options.scales.x.max = undefined;
+                priceChartInstance.update('none'); // 立即应用
+            }
+            if (diffChartInstance) {
+                diffChartInstance.resetZoom();
+                diffChartInstance.options.scales.x.min = undefined;
+                diffChartInstance.options.scales.x.max = undefined;
+                diffChartInstance.update('none'); // 立即应用
+            }
+            // 重置后,如果两个图表X轴范围不同,需要再次同步
+            // 简单起见,下次 updateDisplayAndCharts 时会自动尝试同步(如果需要)
+            // 或者我们可以在 updateDisplayAndCharts 中,如果 isZoomed 为 false,则不设置 min/max
+             if (priceChartInstance && diffChartInstance) {
+                 // 在这里,我们希望两个图表都显示完整的数据范围
+                 // 所以清空min/max后,让下次数据更新(如果是暂停状态,手动触发一次)来重建
+                 if(isPaused){ // 如果是暂停状态,主动更新一下以确保X轴重置并数据重绘
+                    updateDisplayAndCharts();
+                 }
+             }
+        }
+
         // --- Event Listeners ---
         pauseResumeButton.addEventListener('click', togglePauseResume);
 
         document.getElementById('reset-price-zoom-button').addEventListener('click', () => {
-            if (priceChartInstance) {
-                priceChartInstance.resetZoom();
-            }
+            resetAllZooms(); // 现在一个按钮重置所有,并尝试同步
         });
 
         document.getElementById('reset-diff-zoom-button').addEventListener('click', () => {
-            if (diffChartInstance) {
-                diffChartInstance.resetZoom();
-            }
+             resetAllZooms(); // 现在一个按钮重置所有,并尝试同步
         });
 
         // Initial data load and start interval
         updateDisplayAndCharts();
-        if (!isPaused) { // Start interval only if not initially paused (though it's false by default)
+        if (!isPaused) {
             dataUpdateIntervalID = setInterval(updateDisplayAndCharts, REFRESH_INTERVAL_MS);
         }
 
     </script>
 </body>
-</html>
+</html>