mirror of
https://github.com/samsonjs/spirit-tracker.git
synced 2026-03-25 09:25:51 +00:00
UX Improvements
This commit is contained in:
parent
57ea7f9002
commit
1a49a85026
1 changed files with 74 additions and 21 deletions
|
|
@ -54,39 +54,71 @@ function medianFinite(nums) {
|
||||||
|
|
||||||
const StaticMarkerLinesPlugin = {
|
const StaticMarkerLinesPlugin = {
|
||||||
id: "staticMarkerLines",
|
id: "staticMarkerLines",
|
||||||
afterDatasetsDraw(chart, _args, opts) {
|
// use afterDraw so it works regardless of dataset order / animations
|
||||||
|
afterDraw(chart, _args, passedOpts) {
|
||||||
|
// Chart.js v2 vs v3 option plumbing
|
||||||
|
const opts =
|
||||||
|
(chart?.options?.plugins && chart.options.plugins.staticMarkerLines) ||
|
||||||
|
chart?.options?.staticMarkerLines ||
|
||||||
|
passedOpts ||
|
||||||
|
{};
|
||||||
|
|
||||||
const markers = Array.isArray(opts?.markers) ? opts.markers : [];
|
const markers = Array.isArray(opts?.markers) ? opts.markers : [];
|
||||||
if (!markers.length) return;
|
if (!markers.length) return;
|
||||||
|
|
||||||
const y = chart?.scales?.y;
|
// Find a y-scale in a version-tolerant way
|
||||||
|
const scalesObj = chart?.scales || {};
|
||||||
|
const scales = Object.values(scalesObj);
|
||||||
|
const y =
|
||||||
|
scalesObj.y ||
|
||||||
|
scales.find((s) => s && s.axis === "y") ||
|
||||||
|
scales.find((s) => s && typeof s.getPixelForValue === "function" && s.isHorizontal === false) ||
|
||||||
|
scales.find((s) => s && typeof s.getPixelForValue === "function" && String(s.id || "").toLowerCase().includes("y"));
|
||||||
|
|
||||||
const area = chart?.chartArea;
|
const area = chart?.chartArea;
|
||||||
if (!y || !area) return;
|
if (!y || !area) return;
|
||||||
|
|
||||||
const { ctx } = chart;
|
const { ctx } = chart;
|
||||||
const { left, right, top, bottom } = area;
|
const { left, right, top, bottom } = area;
|
||||||
|
|
||||||
ctx.save();
|
const dash = Array.isArray(opts?.dash) ? opts.dash : [6, 6];
|
||||||
ctx.lineWidth = Number.isFinite(opts?.lineWidth) ? opts.lineWidth : 1;
|
const lineWidth = Number.isFinite(opts?.lineWidth) ? opts.lineWidth : 1;
|
||||||
ctx.setLineDash(Array.isArray(opts?.dash) ? opts.dash : [6, 6]);
|
const baseAlpha = Number.isFinite(opts?.alpha) ? opts.alpha : 0.5; // bump default
|
||||||
ctx.font =
|
const labelAlpha = Number.isFinite(opts?.labelAlpha) ? opts.labelAlpha : 0.85;
|
||||||
|
const strokeStyle = String(opts?.color || "rgba(0,0,0,0.95)");
|
||||||
|
const labelColor = String(opts?.labelColor || "rgba(0,0,0,0.95)");
|
||||||
|
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();
|
||||||
|
ctx.lineWidth = lineWidth;
|
||||||
|
ctx.setLineDash(dash);
|
||||||
|
ctx.font = font;
|
||||||
ctx.textBaseline = "bottom";
|
ctx.textBaseline = "bottom";
|
||||||
|
|
||||||
|
// Optional debug overlay
|
||||||
|
if (opts?.debug) {
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.fillStyle = "rgba(0,0,0,0.9)";
|
||||||
|
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) || py < top || py > bottom) continue;
|
if (!Number.isFinite(py)) continue;
|
||||||
|
|
||||||
ctx.globalAlpha = Number.isFinite(m?.alpha)
|
// draw even if slightly out (clamp visibility)
|
||||||
? m.alpha
|
if (py < top - 1 || py > bottom + 1) continue;
|
||||||
: Number.isFinite(opts?.alpha)
|
|
||||||
? opts.alpha
|
ctx.globalAlpha = Number.isFinite(m?.alpha) ? m.alpha : baseAlpha;
|
||||||
: 0.35;
|
ctx.strokeStyle = String(m?.color || strokeStyle);
|
||||||
|
|
||||||
ctx.strokeStyle = String(m?.color || opts?.color || "rgba(0,0,0,0.9)");
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(left, py);
|
ctx.moveTo(left, py);
|
||||||
ctx.lineTo(right, py);
|
ctx.lineTo(right, py);
|
||||||
|
|
@ -95,21 +127,26 @@ const StaticMarkerLinesPlugin = {
|
||||||
const text = String(m?.text || "");
|
const text = String(m?.text || "");
|
||||||
if (text) {
|
if (text) {
|
||||||
const label = `${text} $${yVal.toFixed(2)}`;
|
const label = `${text} $${yVal.toFixed(2)}`;
|
||||||
ctx.globalAlpha = Number.isFinite(m?.labelAlpha)
|
ctx.globalAlpha = Number.isFinite(m?.labelAlpha) ? m.labelAlpha : labelAlpha;
|
||||||
? m.labelAlpha
|
ctx.fillStyle = String(m?.labelColor || labelColor);
|
||||||
: Number.isFinite(opts?.labelAlpha)
|
|
||||||
? opts.labelAlpha
|
|
||||||
: 0.55;
|
|
||||||
ctx.fillStyle = String(m?.labelColor || opts?.labelColor || "rgba(0,0,0,0.9)");
|
|
||||||
const w = ctx.measureText(label).width;
|
const w = ctx.measureText(label).width;
|
||||||
ctx.fillText(label, Math.max(left + 4, right - 4 - w), py - 3);
|
ctx.fillText(label, Math.max(left + 4, right - 4 - w), py - 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts?.debug) {
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
ctx.fillStyle = "rgba(0,0,0,0.9)";
|
||||||
|
ctx.fillText(`y=${yVal.toFixed(2)}`, left + 6, Math.max(top + 28, py + 12));
|
||||||
|
ctx.setLineDash(dash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export function destroyChart() {
|
export function destroyChart() {
|
||||||
if (CHART) {
|
if (CHART) {
|
||||||
CHART.destroy();
|
CHART.destroy();
|
||||||
|
|
@ -857,21 +894,36 @@ export async function renderItem($app, skuInput) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ctx = $canvas.getContext("2d");
|
const ctx = $canvas.getContext("2d");
|
||||||
|
|
||||||
CHART = new Chart(ctx, {
|
CHART = new Chart(ctx, {
|
||||||
type: "line",
|
type: "line",
|
||||||
data: { labels, datasets },
|
data: { labels, datasets },
|
||||||
|
// keep instance plugin for v3+, and also safe for many v2 builds
|
||||||
plugins: [StaticMarkerLinesPlugin],
|
plugins: [StaticMarkerLinesPlugin],
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
interaction: { mode: "nearest", intersect: false },
|
interaction: { mode: "nearest", intersect: false },
|
||||||
|
|
||||||
|
// v2 fallback: allow options.staticMarkerLines (plugin reads this)
|
||||||
|
staticMarkerLines: {
|
||||||
|
markers,
|
||||||
|
dash: [6, 6],
|
||||||
|
alpha: 0.55, // make it obvious
|
||||||
|
labelAlpha: 0.9,
|
||||||
|
debug: true, // <-- remove later
|
||||||
|
},
|
||||||
|
|
||||||
plugins: {
|
plugins: {
|
||||||
|
// v3+: plugin options live here (plugin reads this too)
|
||||||
staticMarkerLines: {
|
staticMarkerLines: {
|
||||||
markers,
|
markers,
|
||||||
dash: [6, 6],
|
dash: [6, 6],
|
||||||
alpha: 0.28, // faint lines
|
alpha: 0.55, // make it obvious
|
||||||
labelAlpha: 0.55,
|
labelAlpha: 0.9,
|
||||||
|
debug: true, // <-- remove later
|
||||||
},
|
},
|
||||||
|
|
||||||
legend: { display: true },
|
legend: { display: true },
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
|
|
@ -896,6 +948,7 @@ export async function renderItem($app, skuInput) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const yScale = CHART.scales?.y;
|
const yScale = CHART.scales?.y;
|
||||||
const tickCount = yScale?.ticks?.length || 0;
|
const tickCount = yScale?.ticks?.length || 0;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue