refactor: project architecture refactor, container splitting

This commit is contained in:
3252a8
2026-05-17 00:01:28 +03:00
parent e0b5218037
commit 30fb774d93
367 changed files with 2609 additions and 864 deletions
@@ -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>