diff --git a/module/statistics/cl1_database.py b/module/statistics/cl1_database.py
index 2b484f979..42410778f 100644
--- a/module/statistics/cl1_database.py
+++ b/module/statistics/cl1_database.py
@@ -503,6 +503,16 @@ class Cl1Database:
}
snapshots = data.get('coins_snapshots', [])
+ if snapshots:
+ last = snapshots[-1]
+ try:
+ same_yellow = int(last.get('yellow_coins', -1)) == snapshot['yellow_coins']
+ same_purple = int(last.get('purple_coins', -1)) == snapshot['purple_coins']
+ if same_yellow and same_purple and last.get('source') == source:
+ return
+ except (TypeError, ValueError):
+ pass
+
snapshots.append(snapshot)
# 保留最近 500 条记录,避免数据过大
if len(snapshots) > 500:
diff --git a/module/webui/app.py b/module/webui/app.py
index 467a2c5c6..8e0e3eedb 100644
--- a/module/webui/app.py
+++ b/module/webui/app.py
@@ -470,6 +470,7 @@ class AlasGUI(Frame):
counts = []
ap_list = []
detail_sources = []
+ chart_points = []
is_detail_mode = False
today = _dt.now().date()
@@ -486,11 +487,13 @@ class AlasGUI(Frame):
labels.append(p['dt'].strftime('%H:%M'))
ap_list.append(p['ap'])
detail_sources.append(p.get('source', '-'))
+ chart_points.append(p)
view_title = t("Gui.Stat.DetailChartTitle")
else:
for p in raw_points:
labels.append(p['dt'].strftime('%m-%d %H:%M'))
ap_list.append(p['ap'])
+ chart_points.append(p)
view_title = t("Gui.Stat.ViewTitleLine")
is_detail_mode = False
current_view = 'line'
@@ -498,6 +501,7 @@ class AlasGUI(Frame):
for p in raw_points:
labels.append(p['dt'].strftime('%m-%d %H:%M'))
ap_list.append(p['ap'])
+ chart_points.append(p)
view_title = t("Gui.Stat.ViewTitleLine")
else:
from collections import OrderedDict
@@ -561,8 +565,7 @@ class AlasGUI(Frame):
coins_stats_html = ''
coins_legend_html = ''
- if coins_timeline and current_view in ('line', 'detail'):
- show_coins = True
+ if coins_timeline and chart_points and current_view in ('line', 'detail'):
coins_raw_points = []
for pt in coins_timeline:
ts_raw = pt.get('ts', '')
@@ -579,29 +582,42 @@ class AlasGUI(Frame):
if coins_raw_points:
coins_raw_points.sort(key=lambda p: p['dt'])
- for p in coins_raw_points:
- yellow_coins_list.append(p['yellow_coins'])
- purple_coins_list.append(p['purple_coins'])
- coins_sources_list.append(p.get('source', '-'))
+ coins_idx = 0
+ coins_last = len(coins_raw_points) - 1
+ for p in chart_points:
+ while coins_idx < coins_last:
+ cur_delta = abs((coins_raw_points[coins_idx]['dt'] - p['dt']).total_seconds())
+ next_delta = abs((coins_raw_points[coins_idx + 1]['dt'] - p['dt']).total_seconds())
+ if next_delta > cur_delta:
+ break
+ coins_idx += 1
+ coins_point = coins_raw_points[coins_idx]
+ yellow_coins_list.append(coins_point['yellow_coins'])
+ purple_coins_list.append(coins_point['purple_coins'])
+ coins_sources_list.append(coins_point.get('source', '-'))
- if yellow_coins_list:
- yc_cur = yellow_coins_list[-1]
- yc_change = yellow_coins_list[-1] - yellow_coins_list[0] if len(yellow_coins_list) >= 2 else 0
+ valid_yellow_coins = [v for v in yellow_coins_list if v is not None]
+ valid_purple_coins = [v for v in purple_coins_list if v is not None]
+ show_coins = bool(valid_yellow_coins or valid_purple_coins)
+
+ if valid_yellow_coins:
+ yc_cur = valid_yellow_coins[-1]
+ yc_change = valid_yellow_coins[-1] - valid_yellow_coins[0] if len(valid_yellow_coins) >= 2 else 0
yc_change_color = '#ef5350' if yc_change >= 0 else '#26a69a'
yc_change_sign = '+' if yc_change >= 0 else ''
- yc_max = max(yellow_coins_list)
- yc_min = min(yellow_coins_list)
+ yc_max = max(valid_yellow_coins)
+ yc_min = min(valid_yellow_coins)
coins_stats_html += f'
黄币: {yc_cur}变化: {yc_change_sign}{yc_change}最高: {yc_max}最低: {yc_min}
'
coins_legend_html += '黄币'
- if purple_coins_list:
- pc_cur = purple_coins_list[-1]
- pc_change = purple_coins_list[-1] - purple_coins_list[0] if len(purple_coins_list) >= 2 else 0
+ if valid_purple_coins:
+ pc_cur = valid_purple_coins[-1]
+ pc_change = valid_purple_coins[-1] - valid_purple_coins[0] if len(valid_purple_coins) >= 2 else 0
pc_change_color = '#ef5350' if pc_change >= 0 else '#26a69a'
pc_change_sign = '+' if pc_change >= 0 else ''
- pc_max = max(purple_coins_list)
- pc_min = min(purple_coins_list)
+ pc_max = max(valid_purple_coins)
+ pc_min = min(valid_purple_coins)
coins_stats_html += f'紫币: {pc_cur}变化: {pc_change_sign}{pc_change}最高: {pc_max}最低: {pc_min}
'
coins_legend_html += '紫币'
diff --git a/webapp/ap_chart.js b/webapp/ap_chart.js
index 57970ac1c..48d993a5d 100644
--- a/webapp/ap_chart.js
+++ b/webapp/ap_chart.js
@@ -96,12 +96,15 @@
var coinsMin = Infinity, coinsMax = -Infinity;
var yellowCoinsLen = yellowCoins ? yellowCoins.length : 0;
var purpleCoinsLen = purpleCoins ? purpleCoins.length : 0;
+ var hasCoins = showCoins && chartType === 'line' && (yellowCoinsLen > 0 || purpleCoinsLen > 0);
if (showCoins && chartType === 'line') {
for (var i = 0; i < yellowCoinsLen; i++) {
+ if (yellowCoins[i] === null || yellowCoins[i] === undefined) continue;
if (yellowCoins[i] < coinsMin) coinsMin = yellowCoins[i];
if (yellowCoins[i] > coinsMax) coinsMax = yellowCoins[i];
}
for (var i = 0; i < purpleCoinsLen; i++) {
+ if (purpleCoins[i] === null || purpleCoins[i] === undefined) continue;
if (purpleCoins[i] < coinsMin) coinsMin = purpleCoins[i];
if (purpleCoins[i] > coinsMax) coinsMax = purpleCoins[i];
}
@@ -115,6 +118,47 @@
function xOfLine(i) { return pad.l + (i / Math.max(nn - 1, 1)) * gW; }
function yOf(v) { return pad.t + gH - (v - allMin) / (allMax - allMin) * gH; }
function yOfCoins(v) { return pad.t + gH - (v - coinsMin) / (coinsMax - coinsMin) * gH; }
+ function drawCoinsLine(xOf, start, end) {
+ if (!hasCoins) return;
+
+ ctx.lineWidth = 1.5;
+ ctx.lineJoin = "round";
+ ctx.setLineDash([4, 2]);
+
+ if (yellowCoinsLen > 0) {
+ ctx.beginPath();
+ ctx.strokeStyle = "#ffd54f";
+ var startedYellowCoins = false;
+ for (var i = start; i < end && i < yellowCoinsLen; i++) {
+ if (yellowCoins[i] === null || yellowCoins[i] === undefined) {
+ startedYellowCoins = false;
+ continue;
+ }
+ var x = xOf(i), y = yOfCoins(yellowCoins[i]);
+ if (!startedYellowCoins) { ctx.moveTo(x, y); startedYellowCoins = true; }
+ else ctx.lineTo(x, y);
+ }
+ ctx.stroke();
+ }
+
+ if (purpleCoinsLen > 0) {
+ ctx.beginPath();
+ ctx.strokeStyle = "#ce93d8";
+ var startedPurpleCoins = false;
+ for (var i = start; i < end && i < purpleCoinsLen; i++) {
+ if (purpleCoins[i] === null || purpleCoins[i] === undefined) {
+ startedPurpleCoins = false;
+ continue;
+ }
+ var x = xOf(i), y = yOfCoins(purpleCoins[i]);
+ if (!startedPurpleCoins) { ctx.moveTo(x, y); startedPurpleCoins = true; }
+ else ctx.lineTo(x, y);
+ }
+ ctx.stroke();
+ }
+
+ ctx.setLineDash([]);
+ }
var candleSpace = gW / nn;
var candleW = Math.max(3, Math.min(candleSpace * 0.6, 30));
@@ -136,8 +180,8 @@
ctx.fillText(Math.round(v), pad.l - 8, y);
}
- if (showCoins && chartType === 'line' && yellowCoinsLen > 0) {
- ctx.fillStyle = "#ffd54f";
+ if (hasCoins) {
+ ctx.fillStyle = "#999";
ctx.textAlign = "left";
for (var i = 0; i <= 5; i++) {
var v = coinsMin + (coinsMax - coinsMin) * (i / 5);
@@ -278,35 +322,7 @@
drawMA(10, "#e91e63");
}
- if (showCoins && chartType === 'line' && yellowCoinsLen > 0) {
- ctx.lineWidth = 1.5;
- ctx.lineJoin = "round";
- ctx.setLineDash([4, 2]);
-
- if (yellowCoinsLen > 0) {
- ctx.beginPath();
- ctx.strokeStyle = "#ffd54f";
- for (var i = 0; i < yellowCoinsLen; i++) {
- var x = xOfLine(i), y = yOfCoins(yellowCoins[i]);
- if (i === 0) ctx.moveTo(x, y);
- else ctx.lineTo(x, y);
- }
- ctx.stroke();
- }
-
- if (purpleCoinsLen > 0) {
- ctx.beginPath();
- ctx.strokeStyle = "#ce93d8";
- for (var i = 0; i < purpleCoinsLen; i++) {
- var x = xOfLine(i), y = yOfCoins(purpleCoins[i]);
- if (i === 0) ctx.moveTo(x, y);
- else ctx.lineTo(x, y);
- }
- ctx.stroke();
- }
-
- ctx.setLineDash([]);
- }
+ drawCoinsLine(xOfLine, 0, nn);
cv.addEventListener("mousemove", function(e) {
var rect = cv.getBoundingClientRect();
@@ -375,17 +391,17 @@
{ parts: [{ type: 'text', value: "来源: " }, { type: 'bold', value: source, style: { color: sourceColor } }] }
];
- if (showCoins && yellowCoinsLen > 0 && idx < yellowCoinsLen) {
+ if (showCoins && yellowCoinsLen > 0 && idx < yellowCoinsLen && yellowCoins[idx] !== null && yellowCoins[idx] !== undefined) {
var yc = yellowCoins[idx];
- var ycDiff = idx > 0 && idx < yellowCoinsLen ? (yc - yellowCoins[idx - 1]) : 0;
+ var ycDiff = idx > 0 && yellowCoins[idx - 1] !== null && yellowCoins[idx - 1] !== undefined ? (yc - yellowCoins[idx - 1]) : 0;
var ycColor = ycDiff >= 0 ? "#ef5350" : "#26a69a";
var ycDiffStr = (ycDiff >= 0 ? "+" : "") + ycDiff;
tooltipRows.push({ parts: [{ type: 'text', value: "黄币: " }, { type: 'bold', value: String(yc), style: { color: "#ffd54f" } }, { type: 'text', value: " (" + ycDiffStr + ")", style: { color: ycColor } }] });
}
- if (showCoins && purpleCoinsLen > 0 && idx < purpleCoinsLen) {
+ if (showCoins && purpleCoinsLen > 0 && idx < purpleCoinsLen && purpleCoins[idx] !== null && purpleCoins[idx] !== undefined) {
var pc = purpleCoins[idx];
- var pcDiff = idx > 0 && idx < purpleCoinsLen ? (pc - purpleCoins[idx - 1]) : 0;
+ var pcDiff = idx > 0 && purpleCoins[idx - 1] !== null && purpleCoins[idx - 1] !== undefined ? (pc - purpleCoins[idx - 1]) : 0;
var pcColor = pcDiff >= 0 ? "#ef5350" : "#26a69a";
var pcDiffStr = (pcDiff >= 0 ? "+" : "") + pcDiff;
tooltipRows.push({ parts: [{ type: 'text', value: "紫币: " }, { type: 'bold', value: String(pc), style: { color: "#ce93d8" } }, { type: 'text', value: " (" + pcDiffStr + ")", style: { color: pcColor } }] });
@@ -423,17 +439,17 @@
{ parts: [{ type: 'text', value: "单次变化: " }, { type: 'bold', value: ds, style: { color: dc } }] }
];
- if (showCoins && yellowCoinsLen > 0 && idx < yellowCoinsLen) {
+ if (showCoins && yellowCoinsLen > 0 && idx < yellowCoinsLen && yellowCoins[idx] !== null && yellowCoins[idx] !== undefined) {
var yc = yellowCoins[idx];
- var ycDiff = idx > 0 && idx < yellowCoinsLen ? (yc - yellowCoins[idx - 1]) : 0;
+ var ycDiff = idx > 0 && yellowCoins[idx - 1] !== null && yellowCoins[idx - 1] !== undefined ? (yc - yellowCoins[idx - 1]) : 0;
var ycColor = ycDiff >= 0 ? "#ef5350" : "#26a69a";
var ycDiffStr = (ycDiff >= 0 ? "+" : "") + ycDiff;
tooltipRows.push({ parts: [{ type: 'text', value: "黄币: " }, { type: 'bold', value: String(yc), style: { color: "#ffd54f" } }, { type: 'text', value: " (" + ycDiffStr + ")", style: { color: ycColor } }] });
}
- if (showCoins && purpleCoinsLen > 0 && idx < purpleCoinsLen) {
+ if (showCoins && purpleCoinsLen > 0 && idx < purpleCoinsLen && purpleCoins[idx] !== null && purpleCoins[idx] !== undefined) {
var pc = purpleCoins[idx];
- var pcDiff = idx > 0 && idx < purpleCoinsLen ? (pc - purpleCoins[idx - 1]) : 0;
+ var pcDiff = idx > 0 && purpleCoins[idx - 1] !== null && purpleCoins[idx - 1] !== undefined ? (pc - purpleCoins[idx - 1]) : 0;
var pcColor = pcDiff >= 0 ? "#ef5350" : "#26a69a";
var pcDiffStr = (pcDiff >= 0 ? "+" : "") + pcDiff;
tooltipRows.push({ parts: [{ type: 'text', value: "紫币: " }, { type: 'bold', value: String(pc), style: { color: "#ce93d8" } }, { type: 'text', value: " (" + pcDiffStr + ")", style: { color: pcColor } }] });
@@ -552,6 +568,16 @@
ctx.fillText(Math.round(v), pad.l - 8, y);
}
+ if (hasCoins) {
+ ctx.fillStyle = "#999";
+ ctx.textAlign = "left";
+ for (var i = 0; i <= 5; i++) {
+ var v = coinsMin + (coinsMax - coinsMin) * (i / 5);
+ var y = yOfCoins(v);
+ ctx.fillText(Math.round(v), W - pad.r + 8, y);
+ }
+ }
+
ctx.fillStyle = "#666";
ctx.font = "10px -apple-system, sans-serif";
ctx.textAlign = "center";
@@ -594,6 +620,8 @@
ctx.fill();
}
+ drawCoinsLine(dxOf, visibleStart, visibleEnd);
+
var labelInterval = Math.max(1, Math.floor(visibleNn / 8));
for (var i = visibleStart; i < visibleEnd; i += labelInterval) {
var lx = dxOf(i);