mirror of
https://github.com/samsonjs/spirit-tracker.git
synced 2026-04-27 15:07:43 +00:00
UX Improvements
This commit is contained in:
parent
1a49a85026
commit
cec8f1f35c
1 changed files with 98 additions and 69 deletions
|
|
@ -54,9 +54,7 @@ function medianFinite(nums) {
|
||||||
|
|
||||||
const StaticMarkerLinesPlugin = {
|
const StaticMarkerLinesPlugin = {
|
||||||
id: "staticMarkerLines",
|
id: "staticMarkerLines",
|
||||||
// use afterDraw so it works regardless of dataset order / animations
|
|
||||||
afterDraw(chart, _args, passedOpts) {
|
afterDraw(chart, _args, passedOpts) {
|
||||||
// Chart.js v2 vs v3 option plumbing
|
|
||||||
const opts =
|
const opts =
|
||||||
(chart?.options?.plugins && chart.options.plugins.staticMarkerLines) ||
|
(chart?.options?.plugins && chart.options.plugins.staticMarkerLines) ||
|
||||||
chart?.options?.staticMarkerLines ||
|
chart?.options?.staticMarkerLines ||
|
||||||
|
|
@ -66,7 +64,7 @@ const StaticMarkerLinesPlugin = {
|
||||||
const markers = Array.isArray(opts?.markers) ? opts.markers : [];
|
const markers = Array.isArray(opts?.markers) ? opts.markers : [];
|
||||||
if (!markers.length) return;
|
if (!markers.length) return;
|
||||||
|
|
||||||
// Find a y-scale in a version-tolerant way
|
// Find y-scale (v2/v3 tolerant)
|
||||||
const scalesObj = chart?.scales || {};
|
const scalesObj = chart?.scales || {};
|
||||||
const scales = Object.values(scalesObj);
|
const scales = Object.values(scalesObj);
|
||||||
const y =
|
const y =
|
||||||
|
|
@ -81,68 +79,100 @@ const StaticMarkerLinesPlugin = {
|
||||||
const { ctx } = chart;
|
const { ctx } = chart;
|
||||||
const { left, right, top, bottom } = area;
|
const { left, right, top, bottom } = area;
|
||||||
|
|
||||||
const dash = Array.isArray(opts?.dash) ? opts.dash : [6, 6];
|
const lineWidth = Number.isFinite(opts?.lineWidth) ? opts.lineWidth : 2;
|
||||||
const lineWidth = Number.isFinite(opts?.lineWidth) ? opts.lineWidth : 1;
|
const baseAlpha = Number.isFinite(opts?.alpha) ? opts.alpha : 0.22; // faint
|
||||||
const baseAlpha = Number.isFinite(opts?.alpha) ? opts.alpha : 0.5; // bump default
|
const labelAlpha = Number.isFinite(opts?.labelAlpha) ? opts.labelAlpha : 0.75;
|
||||||
const labelAlpha = Number.isFinite(opts?.labelAlpha) ? opts.labelAlpha : 0.85;
|
const strokeStyle = String(opts?.color || "#9aa4b2"); // light grey-blue
|
||||||
const strokeStyle = String(opts?.color || "rgba(0,0,0,0.95)");
|
|
||||||
const labelColor = String(opts?.labelColor || "rgba(0,0,0,0.95)");
|
|
||||||
const font =
|
const font =
|
||||||
opts?.font ||
|
opts?.font ||
|
||||||
"12px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif";
|
"12px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif";
|
||||||
|
|
||||||
ctx.save();
|
// Put labels "on the Y axis" (left side of plot) with a small background pill
|
||||||
ctx.lineWidth = lineWidth;
|
const labelPadX = 6;
|
||||||
ctx.setLineDash(dash);
|
const labelPadY = 3;
|
||||||
ctx.font = font;
|
const labelInset = 4; // from chartArea.left
|
||||||
ctx.textBaseline = "bottom";
|
const labelBgAlpha = Number.isFinite(opts?.labelBgAlpha) ? opts.labelBgAlpha : 0.9;
|
||||||
|
const labelBg = String(opts?.labelBg || "rgba(255,255,255,0.92)");
|
||||||
|
const labelBorderAlpha = Number.isFinite(opts?.labelBorderAlpha) ? opts.labelBorderAlpha : 0.25;
|
||||||
|
|
||||||
// Optional debug overlay
|
ctx.save();
|
||||||
if (opts?.debug) {
|
ctx.setLineDash([]); // SOLID
|
||||||
ctx.setLineDash([]);
|
ctx.lineWidth = lineWidth;
|
||||||
ctx.globalAlpha = 1;
|
ctx.font = font;
|
||||||
ctx.fillStyle = "rgba(0,0,0,0.9)";
|
ctx.textBaseline = "middle";
|
||||||
ctx.fillText(`markers=${markers.length}`, left + 6, top + 14);
|
|
||||||
ctx.setLineDash(dash);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const m of markers) {
|
for (const m of markers) {
|
||||||
const yVal = Number(m?.y);
|
const yVal = Number(m?.y);
|
||||||
if (!Number.isFinite(yVal)) continue;
|
if (!Number.isFinite(yVal)) continue;
|
||||||
|
|
||||||
const py = y.getPixelForValue(yVal);
|
const py = y.getPixelForValue(yVal);
|
||||||
if (!Number.isFinite(py)) continue;
|
if (!Number.isFinite(py) || py < top || py > bottom) continue;
|
||||||
|
|
||||||
// draw even if slightly out (clamp visibility)
|
|
||||||
if (py < top - 1 || py > bottom + 1) continue;
|
|
||||||
|
|
||||||
|
// line
|
||||||
ctx.globalAlpha = Number.isFinite(m?.alpha) ? m.alpha : baseAlpha;
|
ctx.globalAlpha = Number.isFinite(m?.alpha) ? m.alpha : baseAlpha;
|
||||||
ctx.strokeStyle = String(m?.color || strokeStyle);
|
ctx.strokeStyle = String(m?.color || strokeStyle);
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(left, py);
|
ctx.moveTo(left, py);
|
||||||
ctx.lineTo(right, py);
|
ctx.lineTo(right, py);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
|
|
||||||
|
// label at Y axis (left edge)
|
||||||
const text = String(m?.text || "");
|
const text = String(m?.text || "");
|
||||||
if (text) {
|
if (text) {
|
||||||
const label = `${text} $${yVal.toFixed(2)}`;
|
const label = `${text}`;
|
||||||
ctx.globalAlpha = Number.isFinite(m?.labelAlpha) ? m.labelAlpha : labelAlpha;
|
const value = `$${yVal.toFixed(2)}`;
|
||||||
ctx.fillStyle = String(m?.labelColor || labelColor);
|
|
||||||
const w = ctx.measureText(label).width;
|
|
||||||
ctx.fillText(label, Math.max(left + 4, right - 4 - w), py - 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts?.debug) {
|
// two-line label: name + value
|
||||||
ctx.globalAlpha = 1;
|
const line1 = label;
|
||||||
ctx.setLineDash([]);
|
const line2 = value;
|
||||||
ctx.fillStyle = "rgba(0,0,0,0.9)";
|
|
||||||
ctx.fillText(`y=${yVal.toFixed(2)}`, left + 6, Math.max(top + 28, py + 12));
|
const w1 = ctx.measureText(line1).width;
|
||||||
ctx.setLineDash(dash);
|
const w2 = ctx.measureText(line2).width;
|
||||||
|
const w = Math.max(w1, w2);
|
||||||
|
|
||||||
|
const boxW = w + labelPadX * 2;
|
||||||
|
const boxH = 24; // fits 2 lines at 12px-ish
|
||||||
|
const boxX = left + labelInset;
|
||||||
|
const boxY = py - boxH / 2;
|
||||||
|
|
||||||
|
// keep box within chartArea
|
||||||
|
const clampedY = Math.min(Math.max(boxY, top), bottom - boxH);
|
||||||
|
|
||||||
|
// background pill
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = labelBgAlpha;
|
||||||
|
ctx.fillStyle = labelBg;
|
||||||
|
roundRect(ctx, boxX, clampedY, boxW, boxH, 6);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// subtle border
|
||||||
|
ctx.globalAlpha = labelBorderAlpha;
|
||||||
|
ctx.strokeStyle = String(m?.color || strokeStyle);
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
// text
|
||||||
|
ctx.globalAlpha = Number.isFinite(m?.labelAlpha) ? m.labelAlpha : labelAlpha;
|
||||||
|
ctx.fillStyle = String(m?.labelColor || "#2b2f36");
|
||||||
|
ctx.textBaseline = "alphabetic";
|
||||||
|
ctx.fillText(line1, boxX + labelPadX, clampedY + 11);
|
||||||
|
ctx.fillText(line2, boxX + labelPadX, clampedY + 22);
|
||||||
|
ctx.restore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
|
function roundRect(ctx2, x, y, w, h, r) {
|
||||||
|
const rr = Math.min(r, w / 2, h / 2);
|
||||||
|
ctx2.beginPath();
|
||||||
|
ctx2.moveTo(x + rr, y);
|
||||||
|
ctx2.arcTo(x + w, y, x + w, y + h, rr);
|
||||||
|
ctx2.arcTo(x + w, y + h, x, y + h, rr);
|
||||||
|
ctx2.arcTo(x, y + h, x, y, rr);
|
||||||
|
ctx2.arcTo(x, y, x + w, y, rr);
|
||||||
|
ctx2.closePath();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -905,36 +935,35 @@ export async function renderItem($app, skuInput) {
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
interaction: { mode: "nearest", intersect: false },
|
interaction: { mode: "nearest", intersect: false },
|
||||||
|
|
||||||
// v2 fallback: allow options.staticMarkerLines (plugin reads this)
|
// v2 fallback (plugin reads this)
|
||||||
staticMarkerLines: {
|
staticMarkerLines: {
|
||||||
markers,
|
markers,
|
||||||
dash: [6, 6],
|
color: "#9aa4b2",
|
||||||
alpha: 0.55, // make it obvious
|
alpha: 0.20,
|
||||||
labelAlpha: 0.9,
|
lineWidth: 2,
|
||||||
debug: true, // <-- remove later
|
labelAlpha: 0.85,
|
||||||
},
|
labelBg: "rgba(255,255,255,0.92)",
|
||||||
|
labelBgAlpha: 0.95,
|
||||||
plugins: {
|
labelBorderAlpha: 0.22,
|
||||||
// v3+: plugin options live here (plugin reads this too)
|
},
|
||||||
staticMarkerLines: {
|
|
||||||
markers,
|
plugins: {
|
||||||
dash: [6, 6],
|
// v3+ (plugin reads this too)
|
||||||
alpha: 0.55, // make it obvious
|
staticMarkerLines: {
|
||||||
labelAlpha: 0.9,
|
markers,
|
||||||
debug: true, // <-- remove later
|
color: "#9aa4b2",
|
||||||
},
|
alpha: 0.20,
|
||||||
|
lineWidth: 2,
|
||||||
legend: { display: true },
|
labelAlpha: 0.85,
|
||||||
tooltip: {
|
labelBg: "rgba(255,255,255,0.92)",
|
||||||
callbacks: {
|
labelBgAlpha: 0.95,
|
||||||
label: (ctx) => {
|
labelBorderAlpha: 0.22,
|
||||||
const v = ctx.parsed?.y;
|
},
|
||||||
if (!Number.isFinite(v)) return `${ctx.dataset.label}: (no data)`;
|
|
||||||
return `${ctx.dataset.label}: $${v.toFixed(2)}`;
|
legend: { display: true },
|
||||||
},
|
tooltip: { /* unchanged */ },
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
scales: {
|
||||||
x: { ticks: { maxRotation: 0, autoSkip: true, maxTicksLimit: 12 }, grid: { display: false } },
|
x: { ticks: { maxRotation: 0, autoSkip: true, maxTicksLimit: 12 }, grid: { display: false } },
|
||||||
y: {
|
y: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue