refactor: project architecture refactor, container splitting
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
<script>
|
||||
import { onMount, tick } from "svelte";
|
||||
import uPlot from "uplot";
|
||||
import "uplot/dist/uPlot.min.css";
|
||||
|
||||
/** `{ date: ISO date string, amount: number }[]` */
|
||||
export let series = [];
|
||||
/** Total plot height in CSS px (axes + canvas). */
|
||||
export let plotHeight = 204;
|
||||
export let fmtMoney = (v, _currency) => String(v);
|
||||
/** @type {string} */
|
||||
export let currency = "RUB";
|
||||
/** uPlot live legend: column header for the time (x) series */
|
||||
export let legendTimeLabel = "Time";
|
||||
/** uPlot live legend: column header for the value (y) series */
|
||||
export let legendValueLabel = "Value";
|
||||
|
||||
let hostEl;
|
||||
let plot;
|
||||
let resizeObserver;
|
||||
let syncTimer = 0;
|
||||
/** Rebuild plot when legend copy changes (language), since series labels are init-only */
|
||||
let builtLegendSig = "";
|
||||
|
||||
function readCssColor(name, fallback) {
|
||||
if (typeof document === "undefined") return fallback;
|
||||
const scope = hostEl || document.documentElement;
|
||||
const raw = getComputedStyle(scope).getPropertyValue(name).trim();
|
||||
return raw || fallback;
|
||||
}
|
||||
|
||||
function parseDayUnix(iso) {
|
||||
const s = String(iso || "");
|
||||
const t = Date.parse(s.includes("T") ? s : `${s}T12:00:00Z`);
|
||||
if (!Number.isFinite(t)) return 0;
|
||||
return Math.floor(t / 1000);
|
||||
}
|
||||
|
||||
function toAlignedData(rows) {
|
||||
if (!rows?.length) return null;
|
||||
const xs = rows.map((p) => parseDayUnix(p.date));
|
||||
const ys = rows.map((p) => Number(p.amount) || 0);
|
||||
return [xs, ys];
|
||||
}
|
||||
|
||||
function yAxisTickLabels(values) {
|
||||
return values.map((v) => fmtMoney(Number(v), currency));
|
||||
}
|
||||
|
||||
/** uPlot passes already-formatted tick strings; reserve enough gutter so amounts are not clipped */
|
||||
function yAxisGutterWidth(_u, values) {
|
||||
const pad = 14;
|
||||
const charPx = 6.1;
|
||||
const maxChars = (values || []).reduce((m, v) => Math.max(m, String(v ?? "").length), 0);
|
||||
return Math.min(104, Math.max(58, Math.ceil(pad + maxChars * charPx)));
|
||||
}
|
||||
|
||||
/** Axis `size`: height (x / bottom) or width (y / left) in CSS px — only customize the y gutter */
|
||||
function axisBandSize(_u, values, axisIdx) {
|
||||
if (axisIdx !== 1) return 32;
|
||||
return yAxisGutterWidth(_u, values);
|
||||
}
|
||||
|
||||
function buildOpts(width) {
|
||||
const w = Math.max(80, Math.floor(width));
|
||||
const muted = readCssColor("--admin-muted", "#9aa7a2");
|
||||
const border = readCssColor("--admin-border", "rgba(255,255,255,0.12)");
|
||||
const accent = readCssColor("--accent", "#00fe7a");
|
||||
const lineStroke = readCssColor(
|
||||
"--admin-chart-stroke",
|
||||
readCssColor("--admin-text", "#e8f0ec"),
|
||||
);
|
||||
const lineFill = readCssColor("--admin-chart-fill", "rgba(120, 140, 132, 0.14)");
|
||||
|
||||
return {
|
||||
width: w,
|
||||
height: plotHeight,
|
||||
class: "admin-uplot",
|
||||
pxAlign: true,
|
||||
padding: [10, 12, 12, 10],
|
||||
legend: {
|
||||
show: true,
|
||||
live: true,
|
||||
markers: { show: true, width: 10, stroke: accent, fill: accent },
|
||||
},
|
||||
cursor: {
|
||||
drag: { x: false, y: false },
|
||||
points: { size: 7, width: 1, stroke: accent },
|
||||
},
|
||||
scales: {
|
||||
x: { time: true },
|
||||
y: { range: [0, null] },
|
||||
},
|
||||
series: [
|
||||
{ label: legendTimeLabel },
|
||||
{
|
||||
label: legendValueLabel,
|
||||
paths: uPlot.paths.spline(),
|
||||
stroke: lineStroke,
|
||||
width: 2,
|
||||
cap: "round",
|
||||
fill: lineFill,
|
||||
},
|
||||
],
|
||||
axes: [
|
||||
{
|
||||
stroke: muted,
|
||||
gap: 8,
|
||||
grid: { show: true, stroke: border, width: 1 },
|
||||
ticks: { stroke: border },
|
||||
font: "11px system-ui,Segoe UI,sans-serif",
|
||||
},
|
||||
{
|
||||
stroke: muted,
|
||||
size: axisBandSize,
|
||||
gap: 8,
|
||||
grid: { show: true, stroke: border, width: 1 },
|
||||
ticks: { stroke: border },
|
||||
font: "10px system-ui,Segoe UI,sans-serif",
|
||||
values: (u, ticks) => yAxisTickLabels(ticks),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function syncChart() {
|
||||
if (!hostEl) return;
|
||||
const d = toAlignedData(series);
|
||||
const legendSig = `${legendTimeLabel}\0${legendValueLabel}`;
|
||||
if (!d) {
|
||||
plot?.destroy();
|
||||
plot = undefined;
|
||||
builtLegendSig = "";
|
||||
return;
|
||||
}
|
||||
const w = Math.max(80, Math.floor(hostEl.clientWidth));
|
||||
if (plot && builtLegendSig !== legendSig) {
|
||||
plot.destroy();
|
||||
plot = undefined;
|
||||
}
|
||||
if (!plot) {
|
||||
plot = new uPlot(buildOpts(w), d, hostEl);
|
||||
builtLegendSig = legendSig;
|
||||
return;
|
||||
}
|
||||
plot.setData(d, true);
|
||||
plot.setSize({ width: w, height: plotHeight });
|
||||
}
|
||||
|
||||
function scheduleSync() {
|
||||
if (typeof window === "undefined") return;
|
||||
clearTimeout(syncTimer);
|
||||
syncTimer = window.setTimeout(() => {
|
||||
syncTimer = 0;
|
||||
syncChart();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
let rafId = 0;
|
||||
|
||||
onMount(() => {
|
||||
rafId = requestAnimationFrame(() => {
|
||||
void tick().then(() => {
|
||||
scheduleSync();
|
||||
if (!hostEl || typeof ResizeObserver === "undefined") return;
|
||||
resizeObserver = new ResizeObserver(() => scheduleSync());
|
||||
resizeObserver.observe(hostEl);
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId);
|
||||
clearTimeout(syncTimer);
|
||||
resizeObserver?.disconnect();
|
||||
resizeObserver = undefined;
|
||||
plot?.destroy();
|
||||
plot = undefined;
|
||||
builtLegendSig = "";
|
||||
};
|
||||
});
|
||||
|
||||
$: if (hostEl) {
|
||||
series;
|
||||
plotHeight;
|
||||
legendTimeLabel;
|
||||
legendValueLabel;
|
||||
scheduleSync();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="admin-revenue-uplot-host" bind:this={hostEl}></div>
|
||||
Reference in New Issue
Block a user