diff options
| author | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-06-14 02:41:34 -0700 |
|---|---|---|
| committer | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-06-14 02:41:34 -0700 |
| commit | ad4109791cc3fdf8c0bf15969610741281caaa8f (patch) | |
| tree | 12608a332f072bba5efd6f8c2aa6ac9b9fb7a106 /frontend | |
| parent | 13ce7b912227ea20671465d83bd6ab5eeb4fbe2b (diff) | |
feat: add FilingsPage component
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/components/prism/FilingsPage.tsx | 733 | ||||
| -rw-r--r-- | frontend/package-lock.json | 388 | ||||
| -rw-r--r-- | frontend/package.json | 3 |
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", |
