summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--frontend/components/prism/FilingsPage.tsx733
-rw-r--r--frontend/package-lock.json388
-rw-r--r--frontend/package.json3
3 files changed, 1120 insertions, 4 deletions
diff --git a/frontend/components/prism/FilingsPage.tsx b/frontend/components/prism/FilingsPage.tsx
new file mode 100644
index 0000000..6b3a9f8
--- /dev/null
+++ b/frontend/components/prism/FilingsPage.tsx
@@ -0,0 +1,733 @@
+"use client";
+import { useEffect, useState } from "react";
+import {
+ BarChart,
+ Bar,
+ XAxis,
+ YAxis,
+ Tooltip,
+ ResponsiveContainer,
+ Legend,
+} from "recharts";
+import { api } from "@/lib/api";
+import { buildKpis } from "@/lib/overview";
+import { KPIStrip } from "@/components/prism/KPIStrip";
+import { TickerHeader } from "@/components/prism/TickerHeader";
+import type {
+ FilingsResponse,
+ FilingItem,
+ CadencePoint,
+ FormMixRow,
+ TickerOverview,
+} from "@/types/api";
+
+type FilingsState = "loading" | "ready" | "error" | "empty";
+type FormFilter = "all" | "10-K" | "10-Q" | "8-K";
+type DateRange = "6m" | "1y" | "all";
+
+const PAGE_SIZE = 25;
+
+type Props = {
+ ticker: string;
+ overview: TickerOverview;
+ isSaved: boolean;
+ onToggleWatchlist: () => void;
+};
+
+// ── Skeleton ──────────────────────────────────────────────────────────────────
+
+function SkeletonBlock({ height, className = "" }: { height: number; className?: string }) {
+ return (
+ <div
+ className={className}
+ style={{
+ height,
+ borderRadius: "var(--r-3)",
+ background:
+ "linear-gradient(90deg, var(--ink-2, #181d26) 25%, var(--ink-3, #1e232e) 50%, var(--ink-2, #181d26) 75%)",
+ backgroundSize: "200% 100%",
+ animation: "psm-shimmer 1.4s ease infinite",
+ }}
+ />
+ );
+}
+
+function FilingsSkeleton() {
+ return (
+ <>
+ <div
+ style={{
+ display: "grid",
+ gridTemplateColumns: "repeat(5, 1fr)",
+ gap: "var(--sp-4)",
+ marginBottom: "var(--sp-5)",
+ }}
+ >
+ {[0, 1, 2, 3, 4].map((i) => (
+ <SkeletonBlock key={i} height={64} />
+ ))}
+ </div>
+ <div className="psm-card" style={{ marginBottom: "var(--sp-4)" }}>
+ <SkeletonBlock height={220} />
+ </div>
+ <div className="psm-card" style={{ marginBottom: "var(--sp-4)" }}>
+ <SkeletonBlock height={160} />
+ </div>
+ <div className="psm-card">
+ <SkeletonBlock height={32} />
+ <div style={{ marginTop: "var(--sp-4)", display: "flex", flexDirection: "column", gap: "var(--sp-3)" }}>
+ {[0, 1, 2, 3, 4].map((i) => (
+ <SkeletonBlock key={i} height={44} />
+ ))}
+ </div>
+ </div>
+ </>
+ );
+}
+
+// ── KPI strip ─────────────────────────────────────────────────────────────────
+
+function FilingKPIs({ kpis }: { kpis: FilingsResponse["kpis"] }) {
+ const items = [
+ { label: "TOTAL FILINGS", value: String(kpis.total) },
+ { label: "10-K ANNUAL", value: String(kpis.count_10k) },
+ { label: "10-Q QUARTERLY", value: String(kpis.count_10q) },
+ { label: "8-K EVENTS", value: String(kpis.count_8k) },
+ { label: "DISTINCT FORMS", value: String(kpis.distinct_forms) },
+ ];
+
+ return (
+ <div
+ style={{
+ display: "grid",
+ gridTemplateColumns: "repeat(5, 1fr)",
+ gap: "var(--sp-4)",
+ marginBottom: "var(--sp-5)",
+ }}
+ >
+ {items.map((item) => (
+ <div
+ key={item.label}
+ className="psm-card"
+ style={{ padding: "var(--sp-4) var(--sp-5)" }}
+ >
+ <div
+ style={{
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-11)",
+ color: "var(--fg-3)",
+ letterSpacing: "0.08em",
+ textTransform: "uppercase",
+ marginBottom: "var(--sp-2)",
+ }}
+ >
+ {item.label}
+ </div>
+ <div
+ style={{
+ fontFamily: "var(--font-mono)",
+ fontSize: "var(--fs-20)",
+ fontVariantNumeric: "tabular-nums",
+ color: "var(--fg-1)",
+ }}
+ >
+ {item.value}
+ </div>
+ </div>
+ ))}
+ </div>
+ );
+}
+
+// ── Cadence chart (Recharts stacked bars) ─────────────────────────────────────
+
+const FORM_COLORS: Record<string, string> = {
+ "10-K": "var(--info, #4A78B5)",
+ "10-Q": "#5B9BD5",
+ "8-K": "var(--caution, #C49545)",
+ other: "var(--fg-3, #8A9AAF)",
+};
+
+function CadenceChart({ data }: { data: CadencePoint[] }) {
+ if (!data.length) {
+ return (
+ <div
+ style={{
+ textAlign: "center",
+ padding: "var(--sp-8) 0",
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-13)",
+ color: "var(--fg-3)",
+ }}
+ >
+ No filing history available
+ </div>
+ );
+ }
+
+ const chartData = data.map((d) => ({
+ month: d.month.slice(5), // "MM"
+ "10-K": d.count_10k,
+ "10-Q": d.count_10q,
+ "8-K": d.count_8k,
+ Other: d.count_other,
+ }));
+
+ return (
+ <ResponsiveContainer width="100%" height={220}>
+ <BarChart data={chartData} margin={{ top: 8, right: 8, left: -20, bottom: 0 }}>
+ <XAxis
+ dataKey="month"
+ tick={{ fontFamily: "var(--font-mono)", fontSize: 10, fill: "var(--fg-3)" }}
+ axisLine={false}
+ tickLine={false}
+ />
+ <YAxis
+ tick={{ fontFamily: "var(--font-mono)", fontSize: 10, fill: "var(--fg-3)" }}
+ axisLine={false}
+ tickLine={false}
+ allowDecimals={false}
+ />
+ <Tooltip
+ contentStyle={{
+ background: "var(--ink-2, #181d26)",
+ border: "1px solid var(--line-1, #232934)",
+ borderRadius: "var(--r-3)",
+ fontFamily: "var(--font-mono)",
+ fontSize: 12,
+ }}
+ labelStyle={{ color: "var(--fg-2)" }}
+ itemStyle={{ color: "var(--fg-1)" }}
+ />
+ <Legend
+ wrapperStyle={{
+ fontFamily: "var(--font-sans)",
+ fontSize: 12,
+ color: "var(--fg-3)",
+ }}
+ />
+ <Bar dataKey="10-K" stackId="a" fill={FORM_COLORS["10-K"]} radius={[0, 0, 0, 0]} />
+ <Bar dataKey="10-Q" stackId="a" fill={FORM_COLORS["10-Q"]} radius={[0, 0, 0, 0]} />
+ <Bar dataKey="8-K" stackId="a" fill={FORM_COLORS["8-K"]} radius={[0, 0, 0, 0]} />
+ <Bar dataKey="Other" stackId="a" fill={FORM_COLORS.other} radius={[2, 2, 0, 0]} />
+ </BarChart>
+ </ResponsiveContainer>
+ );
+}
+
+// ── Form mix table ─────────────────────────────────────────────────────────────
+
+function FormMixTable({ rows }: { rows: FormMixRow[] }) {
+ if (!rows.length) return null;
+
+ return (
+ <div>
+ <div
+ style={{
+ display: "grid",
+ gridTemplateColumns: "1fr auto auto",
+ gap: "var(--sp-3) var(--sp-5)",
+ marginBottom: "var(--sp-2)",
+ }}
+ >
+ <span
+ style={{
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-11)",
+ color: "var(--fg-3)",
+ letterSpacing: "0.08em",
+ textTransform: "uppercase",
+ }}
+ >
+ Form
+ </span>
+ <span
+ style={{
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-11)",
+ color: "var(--fg-3)",
+ letterSpacing: "0.08em",
+ textTransform: "uppercase",
+ textAlign: "right",
+ }}
+ >
+ Count
+ </span>
+ <span
+ style={{
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-11)",
+ color: "var(--fg-3)",
+ letterSpacing: "0.08em",
+ textTransform: "uppercase",
+ textAlign: "right",
+ }}
+ >
+ Mix
+ </span>
+ </div>
+ {rows.map((row) => (
+ <div
+ key={row.form}
+ style={{
+ display: "grid",
+ gridTemplateColumns: "1fr auto auto",
+ gap: "var(--sp-3) var(--sp-5)",
+ padding: "var(--sp-2) 0",
+ borderTop: "1px solid var(--line-1, #232934)",
+ }}
+ >
+ <span
+ style={{
+ fontFamily: "var(--font-mono)",
+ fontSize: "var(--fs-13)",
+ color: "var(--fg-1)",
+ fontVariantNumeric: "tabular-nums",
+ }}
+ >
+ {row.form}
+ </span>
+ <span
+ style={{
+ fontFamily: "var(--font-mono)",
+ fontSize: "var(--fs-13)",
+ color: "var(--fg-2)",
+ fontVariantNumeric: "tabular-nums",
+ textAlign: "right",
+ }}
+ >
+ {row.count}
+ </span>
+ <span
+ style={{
+ fontFamily: "var(--font-mono)",
+ fontSize: "var(--fs-13)",
+ color: "var(--fg-3)",
+ fontVariantNumeric: "tabular-nums",
+ textAlign: "right",
+ }}
+ >
+ {row.pct != null ? `${row.pct.toFixed(1)}%` : "—"}
+ </span>
+ </div>
+ ))}
+ </div>
+ );
+}
+
+// ── Filing badge ───────────────────────────────────────────────────────────────
+
+function FormBadge({ form }: { form: string }) {
+ let bg = "var(--ink-2, #181d26)";
+ let color = "var(--fg-3)";
+ let border = "1px solid var(--line-1, #232934)";
+
+ if (form === "10-K" || form === "10-Q") {
+ bg = "rgba(74, 120, 181, 0.15)";
+ color = "var(--info, #4A78B5)";
+ border = "1px solid rgba(74,120,181,0.3)";
+ } else if (form === "8-K") {
+ bg = "rgba(196, 149, 69, 0.15)";
+ color = "var(--caution, #C49545)";
+ border = "1px solid rgba(196,149,69,0.3)";
+ }
+
+ return (
+ <span
+ style={{
+ display: "inline-block",
+ padding: "2px 8px",
+ borderRadius: 999,
+ fontFamily: "var(--font-mono)",
+ fontSize: "var(--fs-11)",
+ fontVariantNumeric: "tabular-nums",
+ background: bg,
+ color,
+ border,
+ whiteSpace: "nowrap",
+ }}
+ >
+ {form}
+ </span>
+ );
+}
+
+// ── Filing table row ───────────────────────────────────────────────────────────
+
+function FilingRow({ filing }: { filing: FilingItem }) {
+ return (
+ <div
+ style={{
+ display: "grid",
+ gridTemplateColumns: "110px 90px 1fr 32px",
+ gap: "var(--sp-3)",
+ alignItems: "center",
+ padding: "var(--sp-3) 0",
+ borderTop: "1px solid var(--line-1, #232934)",
+ }}
+ >
+ <span
+ style={{
+ fontFamily: "var(--font-mono)",
+ fontSize: "var(--fs-12)",
+ color: "var(--fg-3)",
+ fontVariantNumeric: "tabular-nums",
+ }}
+ >
+ {filing.date}
+ </span>
+ <FormBadge form={filing.form} />
+ <span
+ style={{
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-13)",
+ color: "var(--fg-2)",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ }}
+ title={filing.title}
+ >
+ {filing.title}
+ </span>
+ {filing.url ? (
+ <a
+ href={filing.url}
+ target="_blank"
+ rel="noopener noreferrer"
+ style={{
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-14)",
+ color: "var(--champagne, #C2AA7A)",
+ textDecoration: "none",
+ textAlign: "center",
+ }}
+ aria-label="Open in EDGAR"
+ >
+ ↗
+ </a>
+ ) : (
+ <span style={{ color: "var(--fg-4, #4a5568)", textAlign: "center" }}>—</span>
+ )}
+ </div>
+ );
+}
+
+// ── Filing table with filters + pagination ─────────────────────────────────────
+
+function FilingTable({ filings }: { filings: FilingItem[] }) {
+ const [formFilter, setFormFilter] = useState<FormFilter>("all");
+ const [dateRange, setDateRange] = useState<DateRange>("all");
+ const [page, setPage] = useState(0);
+
+ const FORM_FILTERS: { key: FormFilter; label: string }[] = [
+ { key: "all", label: "ALL" },
+ { key: "10-K", label: "10-K" },
+ { key: "10-Q", label: "10-Q" },
+ { key: "8-K", label: "8-K" },
+ ];
+
+ const DATE_FILTERS: { key: DateRange; label: string }[] = [
+ { key: "6m", label: "6M" },
+ { key: "1y", label: "1Y" },
+ { key: "all", label: "ALL" },
+ ];
+
+ const cutoff: Record<DateRange, string | null> = {
+ "6m": new Date(Date.now() - 180 * 86400 * 1000).toISOString().slice(0, 10),
+ "1y": new Date(Date.now() - 365 * 86400 * 1000).toISOString().slice(0, 10),
+ "all": null,
+ };
+
+ const filtered = filings.filter((f) => {
+ if (formFilter !== "all" && f.form !== formFilter) return false;
+ const cut = cutoff[dateRange];
+ if (cut && f.date < cut) return false;
+ return true;
+ });
+
+ const totalPages = Math.ceil(filtered.length / PAGE_SIZE);
+ const pageItems = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
+
+ // Reset to page 0 when filters change
+ const setFormFilterAndReset = (f: FormFilter) => { setFormFilter(f); setPage(0); };
+ const setDateRangeAndReset = (r: DateRange) => { setDateRange(r); setPage(0); };
+
+ return (
+ <div>
+ {/* Controls row */}
+ <div
+ style={{
+ display: "flex",
+ gap: "var(--sp-3)",
+ flexWrap: "wrap",
+ marginBottom: "var(--sp-4)",
+ justifyContent: "space-between",
+ alignItems: "center",
+ }}
+ >
+ <div style={{ display: "flex", gap: "var(--sp-2)" }}>
+ {FORM_FILTERS.map(({ key, label }) => (
+ <button
+ key={key}
+ onClick={() => setFormFilterAndReset(key)}
+ className={`psm-tab${formFilter === key ? " active" : ""}`}
+ style={{ cursor: "pointer" }}
+ >
+ {label}
+ </button>
+ ))}
+ </div>
+ <div style={{ display: "flex", gap: "var(--sp-2)" }}>
+ {DATE_FILTERS.map(({ key, label }) => (
+ <button
+ key={key}
+ onClick={() => setDateRangeAndReset(key)}
+ className={`psm-tab${dateRange === key ? " active" : ""}`}
+ style={{ cursor: "pointer" }}
+ >
+ {label}
+ </button>
+ ))}
+ </div>
+ </div>
+
+ {/* Table header */}
+ <div
+ style={{
+ display: "grid",
+ gridTemplateColumns: "110px 90px 1fr 32px",
+ gap: "var(--sp-3)",
+ padding: "var(--sp-2) 0",
+ borderBottom: "1px solid var(--line-1, #232934)",
+ }}
+ >
+ {["DATE", "FORM", "TITLE", ""].map((h) => (
+ <span
+ key={h}
+ style={{
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-11)",
+ color: "var(--fg-3)",
+ letterSpacing: "0.08em",
+ textTransform: "uppercase",
+ }}
+ >
+ {h}
+ </span>
+ ))}
+ </div>
+
+ {/* Rows */}
+ {pageItems.length === 0 ? (
+ <div
+ style={{
+ textAlign: "center",
+ padding: "var(--sp-8) 0",
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-13)",
+ color: "var(--fg-3)",
+ }}
+ >
+ No filings match selected filters
+ </div>
+ ) : (
+ pageItems.map((f, i) => <FilingRow key={`${f.date}-${f.form}-${i}`} filing={f} />)
+ )}
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginTop: "var(--sp-4)",
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-12)",
+ color: "var(--fg-3)",
+ }}
+ >
+ <span>
+ {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, filtered.length)} of {filtered.length}
+ </span>
+ <div style={{ display: "flex", gap: "var(--sp-2)" }}>
+ <button
+ className="psm-tab"
+ onClick={() => setPage((p) => Math.max(0, p - 1))}
+ disabled={page === 0}
+ style={{ cursor: page === 0 ? "default" : "pointer", opacity: page === 0 ? 0.4 : 1 }}
+ >
+ ←
+ </button>
+ <button
+ className="psm-tab"
+ onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
+ disabled={page >= totalPages - 1}
+ style={{ cursor: page >= totalPages - 1 ? "default" : "pointer", opacity: page >= totalPages - 1 ? 0.4 : 1 }}
+ >
+ →
+ </button>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+}
+
+// ── Page ──────────────────────────────────────────────────────────────────────
+
+export function FilingsPage({ ticker, overview, isSaved, onToggleWatchlist }: Props) {
+ const [data, setData] = useState<FilingsResponse | null>(null);
+ const [state, setState] = useState<FilingsState>("loading");
+ const kpis = buildKpis(overview);
+
+ useEffect(() => {
+ let cancelled = false;
+ setState("loading");
+ setData(null);
+
+ api
+ .filings(ticker)
+ .then((res) => {
+ if (cancelled) return;
+ if (!res.filings.length) {
+ setState("empty");
+ } else {
+ setData(res);
+ setState("ready");
+ }
+ })
+ .catch(() => {
+ if (!cancelled) setState("error");
+ });
+
+ return () => {
+ cancelled = true;
+ };
+ }, [ticker]);
+
+ return (
+ <>
+ <TickerHeader overview={overview} onToggleWatchlist={onToggleWatchlist} isSaved={isSaved} />
+ <KPIStrip items={kpis} />
+
+ <div style={{ padding: "0 var(--sp-1)" }}>
+ {state === "loading" && <FilingsSkeleton />}
+
+ {state === "error" && (
+ <div className="psm-card" style={{ textAlign: "center", padding: "var(--sp-8)" }}>
+ <div
+ style={{
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-14)",
+ color: "var(--fg-3)",
+ }}
+ >
+ Failed to load filings data
+ </div>
+ </div>
+ )}
+
+ {state === "empty" && (
+ <div className="psm-card" style={{ textAlign: "center", padding: "var(--sp-8)" }}>
+ <div
+ style={{
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-14)",
+ color: "var(--fg-3)",
+ }}
+ >
+ No SEC filings found for this ticker
+ </div>
+ </div>
+ )}
+
+ {state === "ready" && data && (
+ <>
+ <FilingKPIs kpis={data.kpis} />
+
+ {/* Two-column: cadence chart + form mix */}
+ <div
+ style={{
+ display: "grid",
+ gridTemplateColumns: "2fr 1fr",
+ gap: "var(--sp-4)",
+ marginBottom: "var(--sp-4)",
+ }}
+ >
+ <div className="psm-card">
+ <div
+ style={{
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-11)",
+ color: "var(--fg-3)",
+ letterSpacing: "0.08em",
+ textTransform: "uppercase",
+ marginBottom: "var(--sp-4)",
+ }}
+ >
+ Filing Cadence — Monthly
+ </div>
+ <CadenceChart data={data.cadence} />
+ </div>
+ <div className="psm-card">
+ <div
+ style={{
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-11)",
+ color: "var(--fg-3)",
+ letterSpacing: "0.08em",
+ textTransform: "uppercase",
+ marginBottom: "var(--sp-4)",
+ }}
+ >
+ Form Mix
+ </div>
+ <FormMixTable rows={data.form_mix} />
+ </div>
+ </div>
+
+ {/* Readout bar */}
+ <div
+ className="psm-card"
+ style={{
+ marginBottom: "var(--sp-4)",
+ padding: "var(--sp-3) var(--sp-5)",
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-13)",
+ color: "var(--fg-3)",
+ }}
+ >
+ {data.readout}
+ </div>
+
+ {/* Filing table */}
+ <div className="psm-card">
+ <div
+ style={{
+ fontFamily: "var(--font-sans)",
+ fontSize: "var(--fs-11)",
+ color: "var(--fg-3)",
+ letterSpacing: "0.08em",
+ textTransform: "uppercase",
+ marginBottom: "var(--sp-4)",
+ }}
+ >
+ All Filings
+ </div>
+ <FilingTable filings={data.filings} />
+ </div>
+ </>
+ )}
+ </div>
+
+ <style>{`
+ @keyframes psm-shimmer {
+ 0% { background-position: 200% 0; }
+ 100% { background-position: -200% 0; }
+ }
+ `}</style>
+ </>
+ );
+}
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 28aa7e8..8e581d0 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,7 +13,8 @@
"plotly.js": "^2.35.2",
"react": "19.0.0",
"react-dom": "19.0.0",
- "react-plotly.js": "^2.6.0"
+ "react-plotly.js": "^2.6.0",
+ "recharts": "^3.8.1"
},
"devDependencies": {
"@types/node": "^22.10.5",
@@ -1526,6 +1527,42 @@
"pick-by-alias": "^1.2.0"
}
},
+ "node_modules/@reduxjs/toolkit": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz",
+ "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@standard-schema/utils": "^0.3.0",
+ "immer": "^11.0.0",
+ "redux": "^5.0.1",
+ "redux-thunk": "^3.1.0",
+ "reselect": "^5.1.0"
+ },
+ "peerDependencies": {
+ "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
+ "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@reduxjs/toolkit/node_modules/immer": {
+ "version": "11.1.8",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz",
+ "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1533,6 +1570,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/utils": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
+ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
+ "license": "MIT"
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1625,6 +1674,69 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@types/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
@@ -1724,7 +1836,7 @@
"version": "19.2.14",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -1760,6 +1872,12 @@
"@types/geojson": "*"
}
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
+ "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.59.3",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
@@ -3139,6 +3257,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-alpha": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/color-alpha/-/color-alpha-1.0.4.tgz",
@@ -3435,7 +3562,7 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/d": {
@@ -3478,6 +3605,15 @@
"integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==",
"license": "BSD-3-Clause"
},
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-force": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz",
@@ -3575,6 +3711,46 @@
"integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==",
"license": "BSD-3-Clause"
},
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale/node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale/node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/d3-shape": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
@@ -3684,6 +3860,12 @@
}
}
},
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -4057,6 +4239,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/es-toolkit": {
+ "version": "1.47.1",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.1.tgz",
+ "integrity": "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks"
+ ]
+ },
"node_modules/es5-ext": {
"version": "0.10.64",
"resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz",
@@ -4613,6 +4805,12 @@
"es5-ext": "~0.10.14"
}
},
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -5608,6 +5806,16 @@
"node": ">= 4"
}
},
+ "node_modules/immer": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
+ "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/immer"
+ }
+ },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -5665,6 +5873,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -7520,6 +7737,29 @@
"react": ">0.13.0"
}
},
+ "node_modules/react-redux": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
+ "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/use-sync-external-store": "^0.0.6",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^18.2.25 || ^19",
+ "react": "^18.0 || ^19",
+ "redux": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
@@ -7547,6 +7787,51 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
+ "node_modules/recharts": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz",
+ "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==",
+ "license": "MIT",
+ "workspaces": [
+ "www"
+ ],
+ "dependencies": {
+ "@reduxjs/toolkit": "^1.9.0 || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^10.1.1",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.1.1",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
+ "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^5.0.0"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -7676,6 +7961,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/reselect": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
+ "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "2.0.0-next.7",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz",
@@ -8638,6 +8929,12 @@
"xtend": "~4.0.1"
}
},
+ "node_modules/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
"node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
@@ -9035,12 +9332,97 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "node_modules/victory-vendor/node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/victory-vendor/node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/victory-vendor/node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/victory-vendor/node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/victory-vendor/node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/vt-pbf": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index f754927..fdd6d96 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -14,7 +14,8 @@
"plotly.js": "^2.35.2",
"react": "19.0.0",
"react-dom": "19.0.0",
- "react-plotly.js": "^2.6.0"
+ "react-plotly.js": "^2.6.0",
+ "recharts": "^3.8.1"
},
"devDependencies": {
"@types/node": "^22.10.5",