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 = {
|
||||
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 : [];
|
||||
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;
|
||||
if (!y || !area) return;
|
||||
|
||||
const { ctx } = chart;
|
||||
const { left, right, top, bottom } = area;
|
||||
|
||||
ctx.save();
|
||||
ctx.lineWidth = Number.isFinite(opts?.lineWidth) ? opts.lineWidth : 1;
|
||||
ctx.setLineDash(Array.isArray(opts?.dash) ? opts.dash : [6, 6]);
|
||||
ctx.font =
|
||||
const dash = Array.isArray(opts?.dash) ? opts.dash : [6, 6];
|
||||
const lineWidth = Number.isFinite(opts?.lineWidth) ? opts.lineWidth : 1;
|
||||
const baseAlpha = Number.isFinite(opts?.alpha) ? opts.alpha : 0.5; // bump default
|
||||
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 ||
|
||||
"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";
|
||||
|
||||
// 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) {
|
||||
const yVal = Number(m?.y);
|
||||
if (!Number.isFinite(yVal)) continue;
|
||||
|
||||
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)
|
||||
? m.alpha
|
||||
: Number.isFinite(opts?.alpha)
|
||||
? opts.alpha
|
||||
: 0.35;
|
||||
// draw even if slightly out (clamp visibility)
|
||||
if (py < top - 1 || py > bottom + 1) continue;
|
||||
|
||||
ctx.globalAlpha = Number.isFinite(m?.alpha) ? m.alpha : baseAlpha;
|
||||
ctx.strokeStyle = String(m?.color || strokeStyle);
|
||||
|
||||
ctx.strokeStyle = String(m?.color || opts?.color || "rgba(0,0,0,0.9)");
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(left, py);
|
||||
ctx.lineTo(right, py);
|
||||
|
|
@ -95,21 +127,26 @@ const StaticMarkerLinesPlugin = {
|
|||
const text = String(m?.text || "");
|
||||
if (text) {
|
||||
const label = `${text} $${yVal.toFixed(2)}`;
|
||||
ctx.globalAlpha = Number.isFinite(m?.labelAlpha)
|
||||
? m.labelAlpha
|
||||
: Number.isFinite(opts?.labelAlpha)
|
||||
? opts.labelAlpha
|
||||
: 0.55;
|
||||
ctx.fillStyle = String(m?.labelColor || opts?.labelColor || "rgba(0,0,0,0.9)");
|
||||
ctx.globalAlpha = Number.isFinite(m?.labelAlpha) ? m.labelAlpha : labelAlpha;
|
||||
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) {
|
||||
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();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export function destroyChart() {
|
||||
if (CHART) {
|
||||
CHART.destroy();
|
||||
|
|
@ -857,21 +894,36 @@ export async function renderItem($app, skuInput) {
|
|||
}
|
||||
|
||||
const ctx = $canvas.getContext("2d");
|
||||
|
||||
CHART = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: { labels, datasets },
|
||||
// keep instance plugin for v3+, and also safe for many v2 builds
|
||||
plugins: [StaticMarkerLinesPlugin],
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: { mode: "nearest", intersect: false },
|
||||
plugins: {
|
||||
|
||||
// v2 fallback: allow options.staticMarkerLines (plugin reads this)
|
||||
staticMarkerLines: {
|
||||
markers,
|
||||
dash: [6, 6],
|
||||
alpha: 0.28, // faint lines
|
||||
labelAlpha: 0.55,
|
||||
alpha: 0.55, // make it obvious
|
||||
labelAlpha: 0.9,
|
||||
debug: true, // <-- remove later
|
||||
},
|
||||
|
||||
plugins: {
|
||||
// v3+: plugin options live here (plugin reads this too)
|
||||
staticMarkerLines: {
|
||||
markers,
|
||||
dash: [6, 6],
|
||||
alpha: 0.55, // make it obvious
|
||||
labelAlpha: 0.9,
|
||||
debug: true, // <-- remove later
|
||||
},
|
||||
|
||||
legend: { display: true },
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
|
|
@ -897,6 +949,7 @@ export async function renderItem($app, skuInput) {
|
|||
},
|
||||
});
|
||||
|
||||
|
||||
const yScale = CHART.scales?.y;
|
||||
const tickCount = yScale?.ticks?.length || 0;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue