wess09/webapp/ap_chart.js
2026-05-12 18:55:06 +08:00

669 lines
27 KiB
JavaScript

(function() {
function setTooltipContent(tipEl, rows) {
if (!tipEl || !Array.isArray(rows)) return;
while (tipEl.firstChild) {
tipEl.removeChild(tipEl.firstChild);
}
rows.forEach(function(row) {
if (!row) return;
var div = document.createElement('div');
if (row.style && typeof row.style === 'object') {
Object.keys(row.style).forEach(function(k) {
div.style[k] = row.style[k];
});
}
if (row.parts && Array.isArray(row.parts)) {
row.parts.forEach(function(part) {
if (!part) return;
if (part.type === 'text') {
var span = document.createElement('span');
span.textContent = part.value != null ? String(part.value) : '';
if (part.style && typeof part.style === 'object') {
Object.keys(part.style).forEach(function(k) {
span.style[k] = part.style[k];
});
}
div.appendChild(span);
} else if (part.type === 'bold') {
var b = document.createElement('b');
b.textContent = part.value != null ? String(part.value) : '';
if (part.style && typeof part.style === 'object') {
Object.keys(part.style).forEach(function(k) {
b.style[k] = part.style[k];
});
}
div.appendChild(b);
}
});
}
tipEl.appendChild(div);
});
}
var chartType = "__CHART_TYPE__";
var labels = __LABELS__;
var opens = __OPENS__;
var highs = __HIGHS__;
var lows = __LOWS__;
var closes = __CLOSES__;
var counts = __COUNTS__;
var ap = __AP__;
var avg = __AVG__;
var isDetailMode = __IS_DETAIL_MODE__;
var sources = __SOURCES__;
var yellowCoins = __YELLOW_COINS__;
var purpleCoins = __PURPLE_COINS__;
var coinsSources = __COINS_SOURCES__;
var showCoins = __SHOW_COINS__;
var nn = chartType === 'line' ? ap.length : labels.length;
if (nn < 1) return;
var chartId = "__CHART_ID__";
var cv = document.getElementById(chartId);
if (!cv) return;
var tipEl = document.getElementById(chartId + "_tip");
var ovCv = document.getElementById(chartId + "_ov");
var dpr = window.devicePixelRatio || 1;
var W = cv.clientWidth, H = cv.clientHeight;
cv.width = W * dpr; cv.height = H * dpr;
ovCv.width = W * dpr; ovCv.height = H * dpr;
ovCv.style.width = W + "px"; ovCv.style.height = H + "px";
var ctx = cv.getContext("2d");
ctx.scale(dpr, dpr);
var oc = ovCv.getContext("2d");
var pad = {t: 20, r: showCoins ? 72 : 20, b: 52, l: 52};
var gW = W - pad.l - pad.r, gH = H - pad.t - pad.b;
var allMin = Infinity, allMax = -Infinity;
if (chartType === 'line') {
for (var i = 0; i < nn; i++) {
if (ap[i] < allMin) allMin = ap[i];
if (ap[i] > allMax) allMax = ap[i];
}
} else {
for (var i = 0; i < nn; i++) {
if (lows[i] < allMin) allMin = lows[i];
if (highs[i] > allMax) allMax = highs[i];
}
}
var rng = allMax - allMin || 1;
allMin -= rng * 0.08;
allMax += rng * 0.08;
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];
}
if (coinsMin === Infinity) coinsMin = 0;
if (coinsMax === -Infinity) coinsMax = 1000;
var coinsRng = coinsMax - coinsMin || 1;
coinsMin -= coinsRng * 0.08;
coinsMax += coinsRng * 0.08;
}
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));
function xCenter(i) { return pad.l + candleSpace * (i + 0.5); }
ctx.fillStyle = "#1a1a2e";
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = "#2a2a3e";
ctx.lineWidth = 1;
ctx.fillStyle = "#666";
ctx.font = "11px -apple-system, sans-serif";
ctx.textAlign = "right";
ctx.textBaseline = "middle";
for (var i = 0; i <= 5; i++) {
var v = allMin + (allMax - allMin) * (i / 5);
var y = yOf(v);
ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(W - pad.r, y); ctx.stroke();
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);
}
}
var avgY = yOf(avg);
ctx.save();
ctx.strokeStyle = "#ff9800";
ctx.lineWidth = 1;
ctx.setLineDash([6, 4]);
ctx.beginPath(); ctx.moveTo(pad.l, avgY); ctx.lineTo(W - pad.r, avgY); ctx.stroke();
ctx.restore();
ctx.fillStyle = "#ff9800";
ctx.font = "10px -apple-system, sans-serif";
ctx.textAlign = "right";
ctx.fillText("均值:" + avg, W - pad.r - 4, avgY - 8);
ctx.fillStyle = "#666";
ctx.font = "10px -apple-system, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "top";
if (chartType === 'line') {
var labelStep = Math.max(1, Math.floor(nn / 8));
for (var i = 0; i < nn; i += labelStep) {
ctx.save();
ctx.translate(xOfLine(i), H - pad.b + 8);
ctx.rotate(0.4);
ctx.fillText(labels[i], 0, 0);
ctx.restore();
}
} else {
var labelStep = Math.max(1, Math.floor(nn / 12));
for (var i = 0; i < nn; i += labelStep) {
ctx.fillText(labels[i], xCenter(i), H - pad.b + 8);
}
}
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;
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.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);
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 {
for (var i = 0; i < nn; i++) {
var cx = xCenter(i);
var o = opens[i], h = highs[i], l = lows[i], c = closes[i];
var isUp = c > o;
var isDown = c < o;
var isFlat = c === o;
var color = isFlat ? "#888" : (isUp ? "#ef5350" : "#26a69a");
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(cx, yOf(h));
ctx.lineTo(cx, yOf(l));
ctx.stroke();
var bodyTop = yOf(Math.max(o, c));
var bodyBot = yOf(Math.min(o, c));
var bodyH = Math.max(bodyBot - bodyTop, 1);
if (isUp || isDown) {
ctx.fillStyle = color;
ctx.fillRect(cx - candleW / 2, bodyTop, candleW, bodyH);
} else {
ctx.beginPath();
ctx.moveTo(cx - candleW / 2, yOf(o));
ctx.lineTo(cx + candleW / 2, yOf(o));
ctx.stroke();
}
}
function drawMA(days, maColor) {
if (nn < days) return;
ctx.beginPath();
ctx.lineWidth = 1.5;
ctx.strokeStyle = maColor;
var started = false;
for (var i = days - 1; i < nn; i++) {
var sum = 0;
for (var j = 0; j < days; j++) sum += closes[i - j];
var maVal = sum / days;
var x = xCenter(i), y = yOf(maVal);
if (!started) { ctx.moveTo(x, y); started = true; }
else { ctx.lineTo(x, y); }
}
ctx.stroke();
}
drawMA(5, "#ffeb3b");
drawMA(10, "#e91e63");
}
drawCoinsLine(xOfLine, 0, nn);
cv.addEventListener("mousemove", function(e) {
var rect = cv.getBoundingClientRect();
var mx_ = e.clientX - rect.left;
var my_ = e.clientY - rect.top;
oc.setTransform(1, 0, 0, 1, 0, 0);
oc.clearRect(0, 0, ovCv.width, ovCv.height);
if (mx_ < pad.l || mx_ > W - pad.r || my_ < pad.t || my_ > pad.t + gH) {
tipEl.style.display = "none";
return;
}
oc.scale(dpr, dpr);
if (chartType === 'line') {
var visibleStart = Math.max(0, Math.floor(panOffset));
var visibleCount = Math.ceil(nn / zoomLevel);
var visibleEnd = Math.min(nn, visibleStart + visibleCount);
var visibleNn = visibleEnd - visibleStart;
var dMin = Infinity, dMax = -Infinity;
for (var i = visibleStart; i < visibleEnd; i++) {
if (ap[i] < dMin) dMin = ap[i];
if (ap[i] > dMax) dMax = ap[i];
}
if (dMin === Infinity) dMin = 0;
if (dMax === -Infinity) dMax = 100;
var drng = dMax - dMin || 1;
dMin -= drng * 0.1;
dMax += drng * 0.1;
var xScale = gW / Math.max(visibleNn - 1, 1);
var idx = Math.round(visibleStart + (mx_ - pad.l) / xScale);
idx = Math.max(0, Math.min(nn - 1, idx));
var px = pad.l + (idx - visibleStart) * xScale;
var py = pad.t + gH - (ap[idx] - dMin) / (dMax - dMin) * gH;
oc.strokeStyle = "rgba(255,255,255,0.18)";
oc.lineWidth = 1;
oc.setLineDash([4, 3]);
oc.beginPath(); oc.moveTo(px, pad.t); oc.lineTo(px, pad.t + gH); oc.stroke();
oc.beginPath(); oc.moveTo(pad.l, py); oc.lineTo(W - pad.r, py); oc.stroke();
oc.setLineDash([]);
oc.beginPath(); oc.arc(px, py, 6, 0, Math.PI * 2);
oc.fillStyle = "rgba(100,181,246,0.3)"; oc.fill();
oc.beginPath(); oc.arc(px, py, 4, 0, Math.PI * 2);
oc.fillStyle = "#64b5f6"; oc.fill();
oc.strokeStyle = "#fff"; oc.lineWidth = 2; oc.stroke();
oc.setTransform(1, 0, 0, 1, 0, 0);
var diff = idx > 0 ? (ap[idx] - ap[idx - 1]) : 0;
var isUp = diff >= 0;
var dc = isUp ? "#ef5350" : "#26a69a";
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 (isDetailMode) {
var source = sources && sources[idx] ? sources[idx] : '-';
var sourceColor = source === 'cl1' ? '#64b5f6' : (source === 'meow' ? '#ff9800' : '#888');
tooltipRows.push({ parts: [{ type: 'text', value: "来源: " }, { type: 'bold', value: source, style: { color: sourceColor } }] });
}
if (showCoins && yellowCoinsLen > 0 && 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";
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 && 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";
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 } }] });
}
setTooltipContent(tipEl, tooltipRows);
} else {
var idx = Math.floor((mx_ - pad.l) / candleSpace);
idx = Math.max(0, Math.min(nn - 1, idx));
var cx = xCenter(idx);
oc.strokeStyle = "rgba(255,255,255,0.18)";
oc.lineWidth = 1;
oc.setLineDash([4, 3]);
oc.beginPath(); oc.moveTo(cx, pad.t); oc.lineTo(cx, pad.t + gH); oc.stroke();
oc.beginPath(); oc.moveTo(pad.l, my_); oc.lineTo(W - pad.r, my_); oc.stroke();
oc.setLineDash([]);
oc.strokeStyle = "#fff";
oc.lineWidth = 1;
oc.globalAlpha = 0.15;
oc.fillStyle = "#fff";
oc.fillRect(cx - candleW / 2 - 2, pad.t, candleW + 4, gH);
oc.globalAlpha = 1.0;
oc.setTransform(1, 0, 0, 1, 0, 0);
var o = opens[idx], h = highs[idx], l = lows[idx], c_ = closes[idx];
var chg = c_ - o;
var chgPct = o !== 0 ? ((chg / o) * 100).toFixed(1) : "0.0";
var isUp = c_ >= o;
var dc = isUp ? "#ef5350" : "#26a69a";
var chgSign = chg >= 0 ? "+" : "";
var ma5Val = "-";
if (idx >= 4) {
var sum5 = 0; for(var j=0; j<5; j++) sum5+=closes[idx-j];
ma5Val = (sum5/5).toFixed(1);
}
var ma10Val = "-";
if (idx >= 9) {
var sum10 = 0; for(var j=0; j<10; j++) sum10+=closes[idx-j];
ma10Val = (sum10/10).toFixed(1);
}
setTooltipContent(tipEl, [
{ style: { color: "#888", marginBottom: "4px", fontWeight: "600" }, parts: [{ type: 'text', value: labels[idx] }] },
{ parts: [
{ type: 'text', value: "开盘: " },
{ type: 'bold', value: String(o) },
{ type: 'text', value: " MA5(5期平均): " + ma5Val, style: { marginLeft: "8px", color: "#ffeb3b" } }
]},
{ parts: [
{ type: 'text', value: "收盘: " },
{ type: 'bold', value: String(c_), style: { color: dc } },
{ type: 'text', value: " MA10(10期平均): " + ma10Val, style: { marginLeft: "8px", color: "#e91e63" } }
]},
{ parts: [{ type: 'text', value: "最高: " }, { type: 'bold', value: String(h), style: { color: "#ef5350" } }] },
{ parts: [{ type: 'text', value: "最低: " }, { type: 'bold', value: String(l), style: { color: "#26a69a" } }] },
{ parts: [{ type: 'text', value: "涨跌: " }, { type: 'bold', value: chgSign + chg + " (" + chgSign + chgPct + "%)", style: { color: dc } }] },
{ style: { color: "#666", marginTop: "4px" }, parts: [{ type: 'text', value: "数据点密度: " + counts[idx] }] }
]);
}
tipEl.style.display = "block";
var tx = (chartType === 'line' ? px : cx) + 18;
var ty = my_ - 60;
if (tx + 180 > W) tx = (chartType === 'line' ? px : cx) - 200;
if (ty < 8) ty = my_ + 18;
tipEl.style.left = tx + "px";
tipEl.style.top = ty + "px";
});
cv.addEventListener("mouseleave", function() {
tipEl.style.display = "none";
oc.setTransform(1, 0, 0, 1, 0, 0);
oc.clearRect(0, 0, ovCv.width, ovCv.height);
});
if (chartType === 'line') {
var zoomLevel = 1.0;
var panOffset = 0;
var maxZoom = 5.0;
var minZoom = 0.5;
function renderDetailChart() {
var visibleStart = Math.max(0, Math.floor(panOffset));
var visibleCount = Math.ceil(nn / zoomLevel);
var visibleEnd = Math.min(nn, visibleStart + visibleCount);
var visibleNn = visibleEnd - visibleStart;
var dMin = Infinity, dMax = -Infinity;
for (var i = visibleStart; i < visibleEnd; i++) {
if (ap[i] < dMin) dMin = ap[i];
if (ap[i] > dMax) dMax = ap[i];
}
if (dMin === Infinity) dMin = 0;
if (dMax === -Infinity) dMax = 100;
var drng = dMax - dMin || 1;
dMin -= drng * 0.1;
dMax += drng * 0.1;
ctx.fillStyle = "#1a1a2e";
ctx.fillRect(0, 0, W, H);
ctx.strokeStyle = "#2a2a3e";
ctx.lineWidth = 1;
ctx.fillStyle = "#666";
ctx.font = "11px -apple-system, sans-serif";
ctx.textAlign = "right";
ctx.textBaseline = "middle";
for (var i = 0; i <= 5; i++) {
var v = dMin + (dMax - dMin) * (i / 5);
var y = pad.t + gH - (v - dMin) / (dMax - dMin) * gH;
ctx.beginPath(); ctx.moveTo(pad.l, y); ctx.lineTo(W - pad.r, y); ctx.stroke();
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";
ctx.textBaseline = "top";
var xScale = gW / Math.max(visibleNn - 1, 1);
function dxOf(i) { return pad.l + (i - visibleStart) * xScale; }
function dyOf(v) { return pad.t + gH - (v - dMin) / (dMax - dMin) * gH; }
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();
ctx.lineWidth = 1.5;
ctx.lineJoin = "round";
for (var i = visibleStart + 1; i < visibleEnd; i++) {
ctx.beginPath();
ctx.moveTo(dxOf(i - 1), dyOf(ap[i - 1]));
ctx.strokeStyle = ap[i] >= ap[i - 1] ? "#ef5350" : "#26a69a";
ctx.lineTo(dxOf(i), dyOf(ap[i]));
ctx.stroke();
}
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);
var dotColor = (i > visibleStart && ap[i] < ap[i - 1]) ? "#26a69a" : "#ef5350";
ctx.fillStyle = dotColor;
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);
ctx.save();
ctx.translate(lx, H - pad.b + 8);
ctx.rotate(0.3);
ctx.fillText(labels[i], 0, 0);
ctx.restore();
}
}
renderDetailChart();
var isDragging = false;
var dragStartX = 0;
var dragStartPan = 0;
cv.addEventListener("mousedown", function(e) {
isDragging = true;
dragStartX = e.clientX;
dragStartPan = panOffset;
cv.style.cursor = "grabbing";
});
document.addEventListener("mousemove", function(e) {
if (!isDragging) return;
var dx = e.clientX - dragStartX;
var visibleCount = Math.ceil(nn / zoomLevel);
var xScale = gW / Math.max(visibleCount - 1, 1);
var newPan = dragStartPan - dx / xScale;
var maxPan = Math.max(0, nn - visibleCount);
panOffset = Math.max(0, Math.min(maxPan, newPan));
renderDetailChart();
});
document.addEventListener("mouseup", function() {
if (isDragging) {
isDragging = false;
cv.style.cursor = "crosshair";
}
});
cv.addEventListener("wheel", function(e) {
e.preventDefault();
var rect = cv.getBoundingClientRect();
var mx = e.clientX - rect.left;
var zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
var newZoom = Math.max(minZoom, Math.min(maxZoom, zoomLevel * zoomFactor));
if (newZoom !== zoomLevel) {
var visibleCountBefore = Math.ceil(nn / zoomLevel);
var visibleCountAfter = Math.ceil(nn / newZoom);
var xScaleBefore = gW / Math.max(visibleCountBefore - 1, 1);
var mouseIdx = panOffset + (mx - pad.l) / xScaleBefore;
zoomLevel = newZoom;
var xScaleAfter = gW / Math.max(visibleCountAfter - 1, 1);
panOffset = Math.max(0, mouseIdx - (mx - pad.l) / xScaleAfter);
var maxPan = Math.max(0, nn - visibleCountAfter);
panOffset = Math.max(0, Math.min(maxPan, panOffset));
renderDetailChart();
}
}, { passive: false });
var zoomInBtn = document.getElementById(chartId + "_zoom_in");
var zoomOutBtn = document.getElementById(chartId + "_zoom_out");
var zoomResetBtn = document.getElementById(chartId + "_reset");
if (zoomInBtn) {
zoomInBtn.addEventListener("click", function() {
zoomLevel = Math.min(maxZoom, zoomLevel * 1.5);
var visibleCount = Math.ceil(nn / zoomLevel);
var maxPan = Math.max(0, nn - visibleCount);
panOffset = Math.min(panOffset, maxPan);
renderDetailChart();
});
}
if (zoomOutBtn) {
zoomOutBtn.addEventListener("click", function() {
zoomLevel = Math.max(minZoom, zoomLevel / 1.5);
renderDetailChart();
});
}
if (zoomResetBtn) {
zoomResetBtn.addEventListener("click", function() {
zoomLevel = 1.0;
panOffset = 0;
renderDetailChart();
});
}
}
})();