diff --git a/module/webui/app.py b/module/webui/app.py index 4feabf7de..7375595f9 100644 --- a/module/webui/app.py +++ b/module/webui/app.py @@ -716,8 +716,8 @@ def _render_ap_chart(): 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 += '黄币' + coins_stats_html += f'
黄币: {yc_cur}变化: {yc_change_sign}{yc_change}最高: {yc_max}最低: {yc_min}
' + coins_legend_html += '黄币' if valid_purple_coins: pc_cur = valid_purple_coins[-1] @@ -731,8 +731,8 @@ def _render_ap_chart(): 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 += '紫币' + coins_stats_html += f'
紫币: {pc_cur}变化: {pc_change_sign}{pc_change}最高: {pc_max}最低: {pc_min}
' + coins_legend_html += '紫币' # Process distance timeline (align with chart_points like coins) if distance_raw_points and chart_points and current_view in ("line", "detail"): @@ -769,8 +769,8 @@ def _render_ap_chart(): d_max = max(valid_distance) d_min = min(valid_distance) - coins_stats_html += f'
海里数: {d_cur}变化: {d_change_sign}{d_change}最高: {d_max}最低: {d_min}
' - coins_legend_html += '海里数' + coins_stats_html += f'
海里数: {d_cur}变化: {d_change_sign}{d_change}最高: {d_max}最低: {d_min}
' + coins_legend_html += '海里数' # Process virtual asset timeline if virtual_asset_timeline and current_view in ("line", "detail"): @@ -803,8 +803,8 @@ def _render_ap_chart(): va_max = max(valid_va) va_min = min(valid_va) - coins_stats_html += f'
虚拟资产: {va_cur:.1f}变化: {va_change_sign}{va_change:.1f}最高: {va_max:.1f}最低: {va_min:.1f}
' - coins_legend_html += '虚拟资产' + coins_stats_html += f'
虚拟资产: {va_cur:.1f}变化: {va_change_sign}{va_change:.1f}最高: {va_max:.1f}最低: {va_min:.1f}
' + coins_legend_html += '虚拟资产' # Process asset timeline (from same ap_snapshots) if asset_list: @@ -819,8 +819,8 @@ def _render_ap_chart(): a_max = max(valid_asset) a_min = min(valid_asset) - coins_stats_html += f'
资产: {a_cur:.1f}变化: {a_change_sign}{a_change:.1f}最高: {a_max:.1f}最低: {a_min:.1f}
' - coins_legend_html += '资产' + coins_stats_html += f'
资产: {a_cur:.1f}变化: {a_change_sign}{a_change:.1f}最高: {a_max:.1f}最低: {a_min:.1f}
' + coins_legend_html += '资产' # 确保 show_coins 在资产/虚拟资产存在时也为 True,以启用右轴绘制 if not show_coins and ( diff --git a/webapp/ap_chart.js b/webapp/ap_chart.js index 708f26c21..bc7854a2b 100644 --- a/webapp/ap_chart.js +++ b/webapp/ap_chart.js @@ -65,6 +65,10 @@ var lineDistance = __DISTANCE__; var hasDistanceSeries = lineDistance && lineDistance.length > 0; + var seriesVisible = [true, true, true, true, true, true]; + var seriesColors = ["#64b5f6", "#ce93d8", "#ffd54f", "#06b6d4", "#22d3ee", "#1565c0"]; + var seriesNames = ["体力", "紫币", "黄币", "虚拟资产", "资产", "海里数"]; + var nn = chartType === 'line' ? ap.length : labels.length; if (nn < 1) return; @@ -185,11 +189,11 @@ // 系列绘制配置(所有线都要画,虚拟/资产用时间戳) var SERIES_DRAW = [ - { has: hasPurpleCoins, data: purpleCoins, yFn: yOfPurple, dash: [4, 2] }, - { has: hasYellowCoins, data: yellowCoins, yFn: yOfCombined, dash: [4, 2] }, - { has: hasVirtualAssetSeries, data: lineVirtualAsset, ts: lineVirtualAssetTs, yFn: yOfCombined, dash: [5, 3] }, - { has: hasAssetSeries, data: lineAsset, ts: lineAssetTs, yFn: yOfCombined, dash: [5, 3] }, - { has: hasDistanceSeries, data: lineDistance, yFn: yOfCombined, dash: [] } + { has: hasPurpleCoins, data: purpleCoins, yFn: yOfPurple, dash: [] }, + { has: hasYellowCoins, data: yellowCoins, yFn: yOfCombined, dash: [] }, + { has: hasVirtualAssetSeries, data: lineVirtualAsset, ts: lineVirtualAssetTs, yFn: yOfCombined, dash: [] }, + { has: hasAssetSeries, data: lineAsset, ts: lineAssetTs, yFn: yOfCombined, dash: [] }, + { has: hasDistanceSeries, data: lineDistance, yFn: yOfCombined, dash: [] }, ]; // Y 坐标映射 @@ -202,11 +206,6 @@ // 时间感知的 x 坐标映射 function xOfLine(i) { - if (chartType === 'line' && apTs && apTs.length === nn && nn > 1) { - var minT = apTs[0], maxT = apTs[nn - 1]; - var tr = maxT - minT || 1; - return pad.l + ((apTs[i] - minT) / tr) * gW; - } return pad.l + (i / Math.max(nn - 1, 1)) * gW; } @@ -231,34 +230,20 @@ for (var ci = 0; ci < SERIES_DRAW.length; ci++) { var sd = SERIES_DRAW[ci]; if (!sd.has) continue; + if (!seriesVisible[ci + 1]) continue; - ctx.lineWidth = 1.5; + ctx.lineWidth = 1; ctx.lineJoin = "round"; ctx.setLineDash(sd.dash); ctx.strokeStyle = ["#ce93d8", "#ffd54f", "#4fc3f7", "#81c784", "#1565c0"][ci]; ctx.beginPath(); var started = false; - if (sd.ts && apTs && apTs.length === nn && nn > 1) { - var minT = apTs[Math.max(0, start)]; - var maxT = apTs[Math.max(0, Math.min(nn - 1, end - 1))]; - var tr = maxT - minT || 1; - for (var j = 0; j < sd.ts.length; j++) { - if (sd.ts[j] < minT || sd.ts[j] > maxT) continue; - var dv = sd.data[j]; - if (dv === null || dv === undefined) { started = false; continue; } - var px = pad.l + ((sd.ts[j] - minT) / tr) * gW; - var py = sd.yFn(dv); - if (!started) { ctx.moveTo(px, py); started = true; } - else { ctx.lineTo(px, py); } - } - } else { - for (var i = start; i < end && i < sd.data.length; i++) { - if (sd.data[i] === null || sd.data[i] === undefined) { started = false; continue; } - var x = xOf(i), y = sd.yFn(sd.data[i]); - if (!started) { ctx.moveTo(x, y); started = true; } - else { ctx.lineTo(x, y); } - } + for (var i = start; i < end && i < sd.data.length; i++) { + if (sd.data[i] === null || sd.data[i] === undefined) { started = false; continue; } + var x = xOf(i), y = sd.yFn(sd.data[i]); + if (!started) { ctx.moveTo(x, y); started = true; } + else { ctx.lineTo(x, y); } } ctx.stroke(); } @@ -320,56 +305,26 @@ } } - if (chartType === 'line') { - var grad = ctx.createLinearGradient(0, pad.t, 0, pad.t + gH); - grad.addColorStop(0, "rgba(100,120,160,0.18)"); - grad.addColorStop(1, "rgba(100,120,160,0.02)"); - ctx.beginPath(); - ctx.moveTo(xOfLine(0), yOf(ap[0])); - for (var i = 1; i < nn; i++) { - if (nn < 30) { - var x0 = xOfLine(i - 1), y0 = yOf(ap[i - 1]), x1 = xOfLine(i), y1 = yOf(ap[i]); - var cpx = (x0 + x1) / 2; - ctx.bezierCurveTo(cpx, y0, cpx, y1, x1, y1); - } else { - ctx.lineTo(xOfLine(i), yOf(ap[i])); - } - } - ctx.lineTo(xOfLine(nn - 1), pad.t + gH); - ctx.lineTo(xOfLine(0), pad.t + gH); - ctx.closePath(); - ctx.fillStyle = grad; - ctx.fill(); - - ctx.lineWidth = 2; + if (chartType === 'line' && seriesVisible[0]) { + ctx.lineWidth = 1; ctx.lineJoin = "round"; for (var i = 1; i < nn; i++) { ctx.beginPath(); ctx.moveTo(xOfLine(i - 1), yOf(ap[i - 1])); - var segmentColor = ap[i] >= ap[i - 1] ? "#ef5350" : "#26a69a"; - ctx.strokeStyle = segmentColor; - if (nn < 30) { - var x0 = xOfLine(i - 1), y0 = yOf(ap[i - 1]), x1 = xOfLine(i), y1 = yOf(ap[i]); - var cpx = (x0 + x1) / 2; - ctx.bezierCurveTo(cpx, y0, cpx, y1, x1, y1); - } else { - ctx.lineTo(xOfLine(i), yOf(ap[i])); - } + ctx.strokeStyle = ap[i] >= ap[i - 1] ? "#ef5350" : "#26a69a"; + ctx.lineTo(xOfLine(i), yOf(ap[i])); ctx.stroke(); } if (nn < 60) { for (var i = 0; i < nn; i++) { ctx.beginPath(); - ctx.arc(xOfLine(i), yOf(ap[i]), 3.5, 0, Math.PI * 2); + ctx.arc(xOfLine(i), yOf(ap[i]), 1.5, 0, Math.PI * 2); var dotColor = (i > 0 && ap[i] < ap[i - 1]) ? "#26a69a" : "#ef5350"; ctx.fillStyle = dotColor; ctx.fill(); - ctx.strokeStyle = "#1a1a2e"; - ctx.lineWidth = 1.5; - ctx.stroke(); } } - } else { + } else if (seriesVisible[0]) { for (var i = 0; i < nn; i++) { var cx = xCenter(i); var o = opens[i], h = highs[i], l = lows[i], c = closes[i]; @@ -483,6 +438,7 @@ } else { px = pad.l + (idx - visibleStart) * xScale; } + if (seriesVisible[0]) { var py = yScale(ap[idx], dMin, dMax); oc.strokeStyle = "rgba(255,255,255,0.18)"; @@ -497,6 +453,7 @@ oc.beginPath(); oc.arc(px, py, 4, 0, Math.PI * 2); oc.fillStyle = "#64b5f6"; oc.fill(); oc.strokeStyle = "#fff"; oc.lineWidth = 2; oc.stroke(); + } // ---- 滚珠:紫币(独立轴)+ 黄币/虚拟资产/资产(共用轴) ---- function hexToRgba(hex, alpha) { @@ -513,42 +470,32 @@ oc.fillStyle = color; oc.fill(); oc.strokeStyle = "#fff"; oc.lineWidth = 1.5; oc.stroke(); } - if (hasPurpleCoins && idx < purpleCoinsLen && purpleCoins[idx] !== null && purpleCoins[idx] !== undefined) + if (hasPurpleCoins && idx < purpleCoinsLen && purpleCoins[idx] !== null && purpleCoins[idx] !== undefined && seriesVisible[1]) drawBead(purpleCoins[idx], "#ce93d8", yOfPurple); - if (hasYellowCoins && idx < yellowCoinsLen && yellowCoins[idx] !== null && yellowCoins[idx] !== undefined) + if (hasYellowCoins && idx < yellowCoinsLen && yellowCoins[idx] !== null && yellowCoins[idx] !== undefined && seriesVisible[2]) drawBead(yellowCoins[idx], "#ffd54f", yOfCombined); - if (hasVirtualAssetSeries) { - var minT_va = apTs[visibleStart]; - var maxT_va = apTs[Math.max(0, visibleEnd - 1)]; - var tr_va = maxT_va - minT_va || 1; + if (seriesVisible[3] && hasVirtualAssetSeries) { var closestIdx_va = -1, closestDist_va = 600000; for (var j = 0; j < lineVirtualAssetTs.length; j++) { - if (lineVirtualAssetTs[j] < minT_va || lineVirtualAssetTs[j] > maxT_va) continue; - var xj = pad.l + ((lineVirtualAssetTs[j] - minT_va) / tr_va) * gW; - var dist = Math.abs(mx_ - xj); + var dist = Math.abs(idx - j); if (dist < closestDist_va) { closestDist_va = dist; closestIdx_va = j; } } - if (closestIdx_va !== -1 && closestDist_va < 600000) + if (closestIdx_va !== -1 && closestDist_va < 5) drawBead(lineVirtualAsset[closestIdx_va], "#4fc3f7", yOfCombined); } - if (hasAssetSeries) { - var minT_a = apTs[visibleStart]; - var maxT_a = apTs[Math.max(0, visibleEnd - 1)]; - var tr_a = maxT_a - minT_a || 1; + if (seriesVisible[4] && hasAssetSeries) { var closestIdx_a = -1, closestDist_a = 600000; for (var j = 0; j < lineAssetTs.length; j++) { - if (lineAssetTs[j] < minT_a || lineAssetTs[j] > maxT_a) continue; - var xj = pad.l + ((lineAssetTs[j] - minT_a) / tr_a) * gW; - var dist = Math.abs(mx_ - xj); + var dist = Math.abs(idx - j); if (dist < closestDist_a) { closestDist_a = dist; closestIdx_a = j; } } - if (closestIdx_a !== -1 && closestDist_a < 600000) + if (closestIdx_a !== -1 && closestDist_a < 5) drawBead(lineAsset[closestIdx_a], "#81c784", yOfCombined); } // 海里数 bead - if (hasDistanceSeries && idx < lineDistance.length && lineDistance[idx] !== null && lineDistance[idx] !== undefined) + if (hasDistanceSeries && idx < lineDistance.length && lineDistance[idx] !== null && lineDistance[idx] !== undefined && seriesVisible[5]) drawBead(lineDistance[idx], "#1565c0", yOfCombined); oc.setTransform(1, 0, 0, 1, 0, 0); @@ -559,9 +506,11 @@ var ds = (isUp ? "+" : "") + diff; var tooltipRows = [ { style: { color: "#888", marginBottom: "4px", fontWeight: "600" }, parts: [{ type: 'text', value: labels[idx] }] }, - { parts: [{ type: 'text', value: "体力: " }, { type: 'bold', value: String(ap[idx]), style: { color: "#64b5f6" } }] }, - { parts: [{ type: 'text', value: "单次变化: " }, { type: 'bold', value: ds, style: { color: dc } }] } ]; + if (seriesVisible[0]) { + tooltipRows.push({ parts: [{ type: 'text', value: "体力: " }, { type: 'bold', value: String(ap[idx]), style: { color: "#64b5f6" } }] }, + { parts: [{ type: 'text', value: "单次变化: " }, { type: 'bold', value: ds, style: { color: dc } }] }); + } if (isDetailMode) { var source = sources && sources[idx] ? sources[idx] : '-'; @@ -570,7 +519,7 @@ } // 黄币 tooltip - if (hasYellowCoins && idx < yellowCoinsLen && yellowCoins[idx] !== null && yellowCoins[idx] !== undefined) { + if (seriesVisible[2] && hasYellowCoins && idx < yellowCoinsLen && yellowCoins[idx] !== null && yellowCoins[idx] !== undefined) { var yc = yellowCoins[idx]; var ycDiff = idx > 0 && yellowCoins[idx - 1] !== null && yellowCoins[idx - 1] !== undefined ? (yc - yellowCoins[idx - 1]) : 0; var ycColor = ycDiff >= 0 ? "#ef5350" : "#26a69a"; @@ -579,7 +528,7 @@ } // 紫币 tooltip - if (hasPurpleCoins && idx < purpleCoinsLen && purpleCoins[idx] !== null && purpleCoins[idx] !== undefined) { + if (seriesVisible[1] && hasPurpleCoins && idx < purpleCoinsLen && purpleCoins[idx] !== null && purpleCoins[idx] !== undefined) { var pc = purpleCoins[idx]; var pcDiff = idx > 0 && purpleCoins[idx - 1] !== null && purpleCoins[idx - 1] !== undefined ? (pc - purpleCoins[idx - 1]) : 0; var pcColor = pcDiff >= 0 ? "#ef5350" : "#26a69a"; @@ -587,42 +536,32 @@ tooltipRows.push({ parts: [{ type: 'text', value: "紫币: " }, { type: 'bold', value: String(pc), style: { color: "#ce93d8" } }, { type: 'text', value: " (" + pcDiffStr + ")", style: { color: pcColor } }] }); } - // 虚拟资产 tooltip(时间戳匹配) - if (hasVirtualAssetSeries) { - var ratio = (mx_ - pad.l) / gW; - var minT_va = apTs[visibleStart]; - var maxT_va = apTs[Math.max(0, visibleEnd - 1)]; - var targetTs = minT_va + ratio * (maxT_va - minT_va); - var closestIdx = -1, closestDist = Infinity; + // 虚拟资产 tooltip + if (seriesVisible[3] && hasVirtualAssetSeries) { + var closestIdx = -1, closestDist = 600000; for (var j = 0; j < lineVirtualAssetTs.length; j++) { - if (lineVirtualAssetTs[j] < minT_va || lineVirtualAssetTs[j] > maxT_va) continue; - var dist = Math.abs(lineVirtualAssetTs[j] - targetTs); + var dist = Math.abs(idx - j); if (dist < closestDist) { closestDist = dist; closestIdx = j; } } - if (closestIdx !== -1 && closestDist < 600000) { + if (closestIdx !== -1 && closestDist < 5) { tooltipRows.push({ parts: [{ type: 'text', value: "虚拟资产: " }, { type: 'bold', value: lineVirtualAsset[closestIdx].toFixed(1), style: { color: "#4fc3f7" } }] }); } } - // 资产 tooltip(时间戳匹配) - if (hasAssetSeries) { - var ratio = (mx_ - pad.l) / gW; - var minT_a = apTs[visibleStart]; - var maxT_a = apTs[Math.max(0, visibleEnd - 1)]; - var targetTs = minT_a + ratio * (maxT_a - minT_a); - var closestIdx = -1, closestDist = Infinity; + // 资产 tooltip + if (seriesVisible[4] && hasAssetSeries) { + var closestIdx = -1, closestDist = 600000; for (var j = 0; j < lineAssetTs.length; j++) { - if (lineAssetTs[j] < minT_a || lineAssetTs[j] > maxT_a) continue; - var dist = Math.abs(lineAssetTs[j] - targetTs); + var dist = Math.abs(idx - j); if (dist < closestDist) { closestDist = dist; closestIdx = j; } } - if (closestIdx !== -1 && closestDist < 600000) { + if (closestIdx !== -1 && closestDist < 5) { tooltipRows.push({ parts: [{ type: 'text', value: "资产: " }, { type: 'bold', value: lineAsset[closestIdx].toFixed(1), style: { color: "#81c784" } }] }); } } // 海里数 tooltip - if (hasDistanceSeries && idx < lineDistance.length && lineDistance[idx] !== null && lineDistance[idx] !== undefined) { + if (seriesVisible[5] && hasDistanceSeries && idx < lineDistance.length && lineDistance[idx] !== null && lineDistance[idx] !== undefined) { var d = lineDistance[idx]; var dDiff = idx > 0 && lineDistance[idx - 1] !== null && lineDistance[idx - 1] !== undefined ? (d - lineDistance[idx - 1]) : 0; var dColor = dDiff >= 0 ? "#ef5350" : "#26a69a"; @@ -708,6 +647,40 @@ oc.clearRect(0, 0, ovCv.width, ovCv.height); }); + // ======== 图例点击切换曲线 ======== + var legendId = chartId + "_legend"; + var legendEl = document.getElementById(legendId); + if (legendEl) { + if (legendEl._legendHandler) { + legendEl.removeEventListener("click", legendEl._legendHandler); + } + legendEl._legendHandler = function (e) { + var item = e.target.closest(".ap-legend-item"); + if (!item) return; + var idx = parseInt(item.getAttribute("data-series"), 10); + if (isNaN(idx) || idx < 0 || idx >= seriesVisible.length) return; + var only = true; + for (var si = 0; si < seriesVisible.length; si++) { + if (si !== idx && seriesVisible[si]) { only = false; break; } + } + if (only && seriesVisible[idx]) { + for (var si = 0; si < seriesVisible.length; si++) seriesVisible[si] = true; + } else { + for (var si = 0; si < seriesVisible.length; si++) seriesVisible[si] = (si === idx); + } + legendEl.querySelectorAll(".ap-legend-item").forEach(function (li, i) { + var si = parseInt(li.getAttribute("data-series"), 10); + li.style.opacity = seriesVisible[si] ? "1" : "0.35"; + }); + (chartType === 'line' && typeof renderDetailChart === 'function' ? renderDetailChart : initChart)(); + }; + legendEl.addEventListener("click", legendEl._legendHandler); + legendEl.querySelectorAll(".ap-legend-item").forEach(function (li, i) { + var si = parseInt(li.getAttribute("data-series"), 10); + li.style.opacity = seriesVisible[si] ? "1" : "0.35"; + }); + } + // ======== 缩放/平移(仅 line 图) ======== if (chartType === 'line') { var zoomLevel = 1.0; @@ -747,35 +720,15 @@ var xScale = gW / Math.max(visibleNn - 1, 1); function dxOf(i) { - if (apTs && apTs.length === nn && visibleEnd > visibleStart) { - var minT = apTs[visibleStart]; - var maxT = apTs[Math.max(0, visibleEnd - 1)]; - var tr = maxT - minT || 1; - return pad.l + ((apTs[i] - minT) / tr) * gW; - } return pad.l + (i - visibleStart) * xScale; } function dyOf(v) { return yScale(v, dMin, dMax); } drawAssetTicks(ctx, dyOf, dMin, dMax); - // Ap 渐变填充 - var dgrad = ctx.createLinearGradient(0, pad.t, 0, pad.t + gH); - dgrad.addColorStop(0, "rgba(100,181,246,0.15)"); - dgrad.addColorStop(1, "rgba(100,181,246,0.02)"); - ctx.beginPath(); - ctx.moveTo(dxOf(visibleStart), dyOf(ap[visibleStart])); - for (var i = visibleStart + 1; i < visibleEnd; i++) { - ctx.lineTo(dxOf(i), dyOf(ap[i])); - } - ctx.lineTo(dxOf(visibleEnd - 1), pad.t + gH); - ctx.lineTo(dxOf(visibleStart), pad.t + gH); - ctx.closePath(); - ctx.fillStyle = dgrad; - ctx.fill(); - // Ap 线 - ctx.lineWidth = 1.5; + if (seriesVisible[0]) { + ctx.lineWidth = 1; ctx.lineJoin = "round"; for (var i = visibleStart + 1; i < visibleEnd; i++) { ctx.beginPath(); @@ -789,11 +742,12 @@ var dotInterval = Math.max(1, Math.floor(visibleNn / 50)); for (var i = visibleStart; i < visibleEnd; i += dotInterval) { ctx.beginPath(); - ctx.arc(dxOf(i), dyOf(ap[i]), 2.5, 0, Math.PI * 2); + ctx.arc(dxOf(i), dyOf(ap[i]), 1.5, 0, Math.PI * 2); var dotColor = (i > visibleStart && ap[i] < ap[i - 1]) ? "#26a69a" : "#ef5350"; ctx.fillStyle = dotColor; ctx.fill(); } + } // 绘制额外系列线 drawSeriesLine(dxOf, visibleStart, visibleEnd); diff --git a/webapp/ap_chart_panel.html b/webapp/ap_chart_panel.html index 620039f1c..0e502a9ab 100644 --- a/webapp/ap_chart_panel.html +++ b/webapp/ap_chart_panel.html @@ -1,17 +1,16 @@
体力变化 - {view_title}
-
- 当前: {ap_cur} +
+ 行动力: {ap_cur} 变化: {change_sign}{ap_change} 最高: {ap_max} 最低: {ap_min} 均值: {ap_avg} - {data_points_text}
{coins_stats_html}
-
- 体力 +
+ 体力 {coins_legend_html}