diff --git a/components/dashboard/Chart.tsx b/components/dashboard/Chart.tsx index e94e03a..8e4938d 100644 --- a/components/dashboard/Chart.tsx +++ b/components/dashboard/Chart.tsx @@ -2,8 +2,7 @@ import { useState, useMemo, useRef, useCallback, useEffect } from 'react' import { useTheme } from '@ciphera-net/ui' -import { Area, CartesianGrid, ComposedChart, Line, XAxis, YAxis, ReferenceLine } from 'recharts' -import { ChartContainer, ChartTooltip, type ChartConfig } from '@/components/ui/line-charts-6' +import { AreaChart as VisxAreaChart, Area as VisxArea, Grid as VisxGrid, XAxis as VisxXAxis, YAxis as VisxYAxis, ChartTooltip as VisxChartTooltip, type TooltipRow } from '@/components/ui/area-chart' import { Card, CardContent, CardHeader } from '@/components/ui/card' import { formatNumber, formatDuration, formatUpdatedAgo, DatePicker } from '@ciphera-net/ui' import { Select, DownloadIcon, PlusIcon, XIcon } from '@ciphera-net/ui' @@ -103,40 +102,11 @@ const METRIC_CONFIGS: { { key: 'avg_duration', label: 'Visit Duration', format: (v) => formatDuration(v) }, ] -const chartConfig = { - visitors: { label: 'Unique Visitors', color: '#FD5E0F' }, - pageviews: { label: 'Total Pageviews', color: '#FD5E0F' }, - bounce_rate: { label: 'Bounce Rate', color: '#FD5E0F' }, - avg_duration: { label: 'Visit Duration', color: '#FD5E0F' }, -} satisfies ChartConfig - -// ─── Custom Tooltip ───────────────────────────────────────────────── - -interface TooltipProps { - active?: boolean - payload?: Array<{ dataKey: string; value: number; color: string }> - label?: string - metric: MetricType -} - -function CustomTooltip({ active, payload, metric }: TooltipProps) { - if (active && payload && payload.length) { - const entry = payload[0] - const config = METRIC_CONFIGS.find((m) => m.key === metric) - - if (config) { - return ( -
-
-
- {config.label}: - {config.format(entry.value)} -
-
- ) - } - } - return null +const CHART_COLORS: Record = { + visitors: '#FD5E0F', + pageviews: '#FD5E0F', + bounce_rate: '#FD5E0F', + avg_duration: '#FD5E0F', } // ─── Chart Component ───────────────────────────────────────────────── @@ -227,6 +197,7 @@ export default function Chart({ return { date: formattedDate, + dateObj: new Date(item.date), originalDate: item.date, pageviews: item.pageviews, visitors: item.visitors, @@ -450,103 +421,41 @@ export default function Chart({ ) : (
- []} + xDataKey="dateObj" + aspectRatio="2.5 / 1" + margin={{ top: 20, right: 20, bottom: 40, left: 50 }} > - - - - - - - - - - - - - - - - - - - { - const config = METRIC_CONFIGS.find((m) => m.key === metric) - return config ? config.format(value) : value.toString() - }} - /> - - } cursor={{ strokeDasharray: '3 3', stroke: '#9ca3af' }} /> - - - {/* Annotation reference lines */} - {visibleAnnotationMarkers.map((marker) => { - const primaryCategory = marker.annotations[0].category - const color = ANNOTATION_COLORS[primaryCategory] || ANNOTATION_COLORS.other - return ( - - ) - })} - - - - - + + + + { + const config = METRIC_CONFIGS.find((m) => m.key === metric) + return config ? config.format(v) : v.toString() + }} + /> + { + const config = METRIC_CONFIGS.find((m) => m.key === metric) + const value = point[metric] as number + return [{ + color: CHART_COLORS[metric], + label: config?.label || metric, + value: config ? config.format(value) : value, + }] + }} + /> +
)} diff --git a/components/ui/area-chart.tsx b/components/ui/area-chart.tsx new file mode 100644 index 0000000..0515ed9 --- /dev/null +++ b/components/ui/area-chart.tsx @@ -0,0 +1,2292 @@ +"use client"; + +import { localPoint } from "@visx/event"; +import { curveMonotoneX } from "@visx/curve"; +import { GridColumns, GridRows } from "@visx/grid"; +import { ParentSize } from "@visx/responsive"; +import { scaleLinear, scaleTime, type scaleBand } from "@visx/scale"; +import { AreaClosed, LinePath } from "@visx/shape"; +import { bisector } from "d3-array"; +import { + AnimatePresence, + motion, + useMotionTemplate, + useSpring, +} from "motion/react"; +import { + Children, + createContext, + isValidElement, + useCallback, + useContext, + useEffect, + useId, + useLayoutEffect, + useMemo, + useRef, + useState, + type Dispatch, + type ReactElement, + type ReactNode, + type RefObject, + type SetStateAction, +} from "react"; +import useMeasure from "react-use-measure"; +import { createPortal } from "react-dom"; +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +// ─── Utils ─────────────────────────────────────────────────────────────────── + +function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// ─── Chart Context ─────────────────────────────────────────────────────────── + +// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory type +type CurveFactory = any; + +type ScaleLinearType = ReturnType< + typeof scaleLinear +>; +type ScaleTimeType = ReturnType< + typeof scaleTime +>; +type ScaleBandType = ReturnType< + typeof scaleBand +>; + +export const chartCssVars = { + background: "var(--chart-background)", + foreground: "var(--chart-foreground)", + foregroundMuted: "var(--chart-foreground-muted)", + label: "var(--chart-label)", + linePrimary: "var(--chart-line-primary)", + lineSecondary: "var(--chart-line-secondary)", + crosshair: "var(--chart-crosshair)", + grid: "var(--chart-grid)", + indicatorColor: "var(--chart-indicator-color)", + indicatorSecondaryColor: "var(--chart-indicator-secondary-color)", + markerBackground: "var(--chart-marker-background)", + markerBorder: "var(--chart-marker-border)", + markerForeground: "var(--chart-marker-foreground)", + badgeBackground: "var(--chart-marker-badge-background)", + badgeForeground: "var(--chart-marker-badge-foreground)", + segmentBackground: "var(--chart-segment-background)", + segmentLine: "var(--chart-segment-line)", +}; + +export interface Margin { + top: number; + right: number; + bottom: number; + left: number; +} + +export interface TooltipData { + point: Record; + index: number; + x: number; + yPositions: Record; + xPositions?: Record; +} + +export interface LineConfig { + dataKey: string; + stroke: string; + strokeWidth: number; +} + +export interface ChartSelection { + startX: number; + endX: number; + startIndex: number; + endIndex: number; + active: boolean; +} + +export interface ChartContextValue { + data: Record[]; + xScale: ScaleTimeType; + yScale: ScaleLinearType; + width: number; + height: number; + innerWidth: number; + innerHeight: number; + margin: Margin; + columnWidth: number; + tooltipData: TooltipData | null; + setTooltipData: Dispatch>; + containerRef: RefObject; + lines: LineConfig[]; + isLoaded: boolean; + animationDuration: number; + xAccessor: (d: Record) => Date; + dateLabels: string[]; + selection?: ChartSelection | null; + clearSelection?: () => void; + barScale?: ScaleBandType; + bandWidth?: number; + hoveredBarIndex?: number | null; + setHoveredBarIndex?: (index: number | null) => void; + barXAccessor?: (d: Record) => string; + orientation?: "vertical" | "horizontal"; + stacked?: boolean; + stackOffsets?: Map>; +} + +const ChartContext = createContext(null); + +function ChartProvider({ + children, + value, +}: { + children: ReactNode; + value: ChartContextValue; +}) { + return ( + {children} + ); +} + +function useChart(): ChartContextValue { + const context = useContext(ChartContext); + if (!context) { + throw new Error( + "useChart must be used within a ChartProvider. " + + "Make sure your component is wrapped in ." + ); + } + return context; +} + +// ─── useChartInteraction ───────────────────────────────────────────────────── + +type ScaleTime = ReturnType>; +type ScaleLinear = ReturnType>; + +interface UseChartInteractionParams { + xScale: ScaleTime; + yScale: ScaleLinear; + data: Record[]; + lines: LineConfig[]; + margin: Margin; + xAccessor: (d: Record) => Date; + bisectDate: ( + data: Record[], + date: Date, + lo: number + ) => number; + canInteract: boolean; +} + +interface ChartInteractionResult { + tooltipData: TooltipData | null; + setTooltipData: Dispatch>; + selection: ChartSelection | null; + clearSelection: () => void; + interactionHandlers: { + onMouseMove?: (event: React.MouseEvent) => void; + onMouseLeave?: () => void; + onMouseDown?: (event: React.MouseEvent) => void; + onMouseUp?: () => void; + onTouchStart?: (event: React.TouchEvent) => void; + onTouchMove?: (event: React.TouchEvent) => void; + onTouchEnd?: () => void; + }; + interactionStyle: React.CSSProperties; +} + +function useChartInteraction({ + xScale, + yScale, + data, + lines, + margin, + xAccessor, + bisectDate, + canInteract, +}: UseChartInteractionParams): ChartInteractionResult { + const [tooltipData, setTooltipData] = useState(null); + const [selection, setSelection] = useState(null); + + const isDraggingRef = useRef(false); + const dragStartXRef = useRef(0); + + const resolveTooltipFromX = useCallback( + (pixelX: number): TooltipData | null => { + const x0 = xScale.invert(pixelX); + const index = bisectDate(data, x0, 1); + const d0 = data[index - 1]; + const d1 = data[index]; + + if (!d0) { + return null; + } + + let d = d0; + let finalIndex = index - 1; + if (d1) { + const d0Time = xAccessor(d0).getTime(); + const d1Time = xAccessor(d1).getTime(); + if (x0.getTime() - d0Time > d1Time - x0.getTime()) { + d = d1; + finalIndex = index; + } + } + + const yPositions: Record = {}; + for (const line of lines) { + const value = d[line.dataKey]; + if (typeof value === "number") { + yPositions[line.dataKey] = yScale(value) ?? 0; + } + } + + return { + point: d, + index: finalIndex, + x: xScale(xAccessor(d)) ?? 0, + yPositions, + }; + }, + [xScale, yScale, data, lines, xAccessor, bisectDate] + ); + + const resolveIndexFromX = useCallback( + (pixelX: number): number => { + const x0 = xScale.invert(pixelX); + const index = bisectDate(data, x0, 1); + const d0 = data[index - 1]; + const d1 = data[index]; + if (!d0) { + return 0; + } + if (d1) { + const d0Time = xAccessor(d0).getTime(); + const d1Time = xAccessor(d1).getTime(); + if (x0.getTime() - d0Time > d1Time - x0.getTime()) { + return index; + } + } + return index - 1; + }, + [xScale, data, xAccessor, bisectDate] + ); + + const getChartX = useCallback( + ( + event: React.MouseEvent | React.TouchEvent, + touchIndex = 0 + ): number | null => { + let point: { x: number; y: number } | null = null; + + if ("touches" in event) { + const touch = event.touches[touchIndex]; + if (!touch) { + return null; + } + const svg = event.currentTarget.ownerSVGElement; + if (!svg) { + return null; + } + point = localPoint(svg, touch as unknown as MouseEvent); + } else { + point = localPoint(event); + } + + if (!point) { + return null; + } + return point.x - margin.left; + }, + [margin.left] + ); + + const handleMouseMove = useCallback( + (event: React.MouseEvent) => { + const chartX = getChartX(event); + if (chartX === null) { + return; + } + + if (isDraggingRef.current) { + const startX = Math.min(dragStartXRef.current, chartX); + const endX = Math.max(dragStartXRef.current, chartX); + setSelection({ + startX, + endX, + startIndex: resolveIndexFromX(startX), + endIndex: resolveIndexFromX(endX), + active: true, + }); + return; + } + + const tooltip = resolveTooltipFromX(chartX); + if (tooltip) { + setTooltipData(tooltip); + } + }, + [getChartX, resolveTooltipFromX, resolveIndexFromX] + ); + + const handleMouseLeave = useCallback(() => { + setTooltipData(null); + if (isDraggingRef.current) { + isDraggingRef.current = false; + } + setSelection(null); + }, []); + + const handleMouseDown = useCallback( + (event: React.MouseEvent) => { + const chartX = getChartX(event); + if (chartX === null) { + return; + } + isDraggingRef.current = true; + dragStartXRef.current = chartX; + setTooltipData(null); + setSelection(null); + }, + [getChartX] + ); + + const handleMouseUp = useCallback(() => { + if (isDraggingRef.current) { + isDraggingRef.current = false; + } + setSelection(null); + }, []); + + const handleTouchStart = useCallback( + (event: React.TouchEvent) => { + if (event.touches.length === 1) { + event.preventDefault(); + const chartX = getChartX(event, 0); + if (chartX === null) { + return; + } + const tooltip = resolveTooltipFromX(chartX); + if (tooltip) { + setTooltipData(tooltip); + } + } else if (event.touches.length === 2) { + event.preventDefault(); + setTooltipData(null); + const x0 = getChartX(event, 0); + const x1 = getChartX(event, 1); + if (x0 === null || x1 === null) { + return; + } + const startX = Math.min(x0, x1); + const endX = Math.max(x0, x1); + setSelection({ + startX, + endX, + startIndex: resolveIndexFromX(startX), + endIndex: resolveIndexFromX(endX), + active: true, + }); + } + }, + [getChartX, resolveTooltipFromX, resolveIndexFromX] + ); + + const handleTouchMove = useCallback( + (event: React.TouchEvent) => { + if (event.touches.length === 1) { + event.preventDefault(); + const chartX = getChartX(event, 0); + if (chartX === null) { + return; + } + const tooltip = resolveTooltipFromX(chartX); + if (tooltip) { + setTooltipData(tooltip); + } + } else if (event.touches.length === 2) { + event.preventDefault(); + const x0 = getChartX(event, 0); + const x1 = getChartX(event, 1); + if (x0 === null || x1 === null) { + return; + } + const startX = Math.min(x0, x1); + const endX = Math.max(x0, x1); + setSelection({ + startX, + endX, + startIndex: resolveIndexFromX(startX), + endIndex: resolveIndexFromX(endX), + active: true, + }); + } + }, + [getChartX, resolveTooltipFromX, resolveIndexFromX] + ); + + const handleTouchEnd = useCallback(() => { + setTooltipData(null); + setSelection(null); + }, []); + + const clearSelection = useCallback(() => { + setSelection(null); + }, []); + + const interactionHandlers = canInteract + ? { + onMouseMove: handleMouseMove, + onMouseLeave: handleMouseLeave, + onMouseDown: handleMouseDown, + onMouseUp: handleMouseUp, + onTouchStart: handleTouchStart, + onTouchMove: handleTouchMove, + onTouchEnd: handleTouchEnd, + } + : {}; + + const interactionStyle: React.CSSProperties = { + cursor: canInteract ? "crosshair" : "default", + touchAction: "none", + }; + + return { + tooltipData, + setTooltipData, + selection, + clearSelection, + interactionHandlers, + interactionStyle, + }; +} + +// ─── Tooltip Components ────────────────────────────────────────────────────── + +// DateTicker + +const TICKER_ITEM_HEIGHT = 24; + +interface DateTickerProps { + currentIndex: number; + labels: string[]; + visible: boolean; +} + +function DateTicker({ currentIndex, labels, visible }: DateTickerProps) { + const parsedLabels = useMemo(() => { + return labels.map((label) => { + const parts = label.split(" "); + const month = parts[0] || ""; + const day = parts[1] || ""; + return { month, day, full: label }; + }); + }, [labels]); + + const monthIndices = useMemo(() => { + const uniqueMonths: string[] = []; + const indices: number[] = []; + + parsedLabels.forEach((label, index) => { + if (uniqueMonths.length === 0 || uniqueMonths.at(-1) !== label.month) { + uniqueMonths.push(label.month); + indices.push(index); + } + }); + + return { uniqueMonths, indices }; + }, [parsedLabels]); + + const currentMonthIndex = useMemo(() => { + if (currentIndex < 0 || currentIndex >= parsedLabels.length) { + return 0; + } + const currentMonth = parsedLabels[currentIndex]?.month; + return monthIndices.uniqueMonths.indexOf(currentMonth || ""); + }, [currentIndex, parsedLabels, monthIndices]); + + const prevMonthIndexRef = useRef(-1); + + const dayY = useSpring(0, { stiffness: 400, damping: 35 }); + const monthY = useSpring(0, { stiffness: 400, damping: 35 }); + + useEffect(() => { + dayY.set(-currentIndex * TICKER_ITEM_HEIGHT); + }, [currentIndex, dayY]); + + useEffect(() => { + if (currentMonthIndex >= 0) { + const isFirstRender = prevMonthIndexRef.current === -1; + const monthChanged = prevMonthIndexRef.current !== currentMonthIndex; + + if (isFirstRender || monthChanged) { + monthY.set(-currentMonthIndex * TICKER_ITEM_HEIGHT); + prevMonthIndexRef.current = currentMonthIndex; + } + } + }, [currentMonthIndex, monthY]); + + if (!visible || labels.length === 0) { + return null; + } + + return ( + +
+
+
+ + {monthIndices.uniqueMonths.map((month) => ( +
+ + {month} + +
+ ))} +
+
+
+ + {parsedLabels.map((label, index) => ( +
+ + {label.day} + +
+ ))} +
+
+
+
+
+ ); +} + +DateTicker.displayName = "DateTicker"; + +// TooltipDot + +interface TooltipDotProps { + x: number; + y: number; + visible: boolean; + color: string; + size?: number; + strokeColor?: string; + strokeWidth?: number; +} + +function TooltipDot({ + x, + y, + visible, + color, + size = 5, + strokeColor = chartCssVars.background, + strokeWidth = 2, +}: TooltipDotProps) { + const crosshairSpringConfig = { stiffness: 300, damping: 30 }; + const animatedX = useSpring(x, crosshairSpringConfig); + const animatedY = useSpring(y, crosshairSpringConfig); + + useEffect(() => { + animatedX.set(x); + animatedY.set(y); + }, [x, y, animatedX, animatedY]); + + if (!visible) { + return null; + } + + return ( + + ); +} + +TooltipDot.displayName = "TooltipDot"; + +// TooltipIndicator + +type IndicatorWidth = number | "line" | "thin" | "medium" | "thick"; + +interface TooltipIndicatorProps { + x: number; + height: number; + visible: boolean; + width?: IndicatorWidth; + span?: number; + columnWidth?: number; + colorEdge?: string; + colorMid?: string; + fadeEdges?: boolean; + gradientId?: string; +} + +function resolveWidth(width: IndicatorWidth): number { + if (typeof width === "number") { + return width; + } + switch (width) { + case "line": + return 1; + case "thin": + return 2; + case "medium": + return 4; + case "thick": + return 8; + default: + return 1; + } +} + +function TooltipIndicator({ + x, + height, + visible, + width = "line", + span, + columnWidth, + colorEdge = chartCssVars.crosshair, + colorMid = chartCssVars.crosshair, + fadeEdges = true, + gradientId = "tooltip-indicator-gradient", +}: TooltipIndicatorProps) { + const pixelWidth = + span !== undefined && columnWidth !== undefined + ? span * columnWidth + : resolveWidth(width); + + const crosshairSpringConfig = { stiffness: 300, damping: 30 }; + const animatedX = useSpring(x - pixelWidth / 2, crosshairSpringConfig); + + useEffect(() => { + animatedX.set(x - pixelWidth / 2); + }, [x, animatedX, pixelWidth]); + + if (!visible) { + return null; + } + + const edgeOpacity = fadeEdges ? 0 : 1; + + return ( + + + + + + + + + + + + + ); +} + +TooltipIndicator.displayName = "TooltipIndicator"; + +// TooltipContent + +export interface TooltipRow { + color: string; + label: string; + value: string | number; +} + +interface TooltipContentProps { + title?: string; + rows: TooltipRow[]; + children?: ReactNode; +} + +function TooltipContent({ title, rows, children }: TooltipContentProps) { + const [measureRef, bounds] = useMeasure({ debounce: 0, scroll: false }); + const [committedHeight, setCommittedHeight] = useState(null); + const committedChildrenStateRef = useRef(null); + const frameRef = useRef(null); + + const hasChildren = !!children; + const markerKey = hasChildren ? "has-marker" : "no-marker"; + + const isWaitingForSettlement = + committedChildrenStateRef.current !== null && + committedChildrenStateRef.current !== hasChildren; + + useEffect(() => { + if (bounds.height <= 0) { + return; + } + + if (frameRef.current) { + cancelAnimationFrame(frameRef.current); + frameRef.current = null; + } + + if (isWaitingForSettlement) { + frameRef.current = requestAnimationFrame(() => { + frameRef.current = requestAnimationFrame(() => { + setCommittedHeight(bounds.height); + committedChildrenStateRef.current = hasChildren; + }); + }); + } else { + setCommittedHeight(bounds.height); + committedChildrenStateRef.current = hasChildren; + } + + return () => { + if (frameRef.current) { + cancelAnimationFrame(frameRef.current); + } + }; + }, [bounds.height, hasChildren, isWaitingForSettlement]); + + const shouldAnimate = committedHeight !== null; + + return ( + +
+ {title && ( +
+ {title} +
+ )} +
+ {rows.map((row) => ( +
+
+ + + {row.label} + +
+ + {typeof row.value === "number" + ? row.value.toLocaleString() + : row.value} + +
+ ))} +
+ + + {children && ( + + {children} + + )} + +
+
+ ); +} + +TooltipContent.displayName = "TooltipContent"; + +// TooltipBox + +interface TooltipBoxProps { + x: number; + y: number; + visible: boolean; + containerRef: RefObject; + containerWidth: number; + containerHeight: number; + offset?: number; + className?: string; + children: ReactNode; + left?: number | ReturnType; + top?: number | ReturnType; + flipped?: boolean; +} + +function TooltipBox({ + x, + y, + visible, + containerRef, + containerWidth, + containerHeight, + offset = 16, + className = "", + children, + left: leftOverride, + top: topOverride, + flipped: flippedOverride, +}: TooltipBoxProps) { + const tooltipRef = useRef(null); + const [tooltipWidth, setTooltipWidth] = useState(180); + const [tooltipHeight, setTooltipHeight] = useState(80); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useLayoutEffect(() => { + if (tooltipRef.current) { + const w = tooltipRef.current.offsetWidth; + const h = tooltipRef.current.offsetHeight; + if (w > 0 && w !== tooltipWidth) { + setTooltipWidth(w); + } + if (h > 0 && h !== tooltipHeight) { + setTooltipHeight(h); + } + } + }, [tooltipWidth, tooltipHeight]); + + const shouldFlipX = x + tooltipWidth + offset > containerWidth; + const targetX = shouldFlipX ? x - offset - tooltipWidth : x + offset; + + const targetY = Math.max( + offset, + Math.min(y - tooltipHeight / 2, containerHeight - tooltipHeight - offset) + ); + + const prevFlipRef = useRef(shouldFlipX); + const [flipKey, setFlipKey] = useState(0); + + useEffect(() => { + if (prevFlipRef.current !== shouldFlipX) { + setFlipKey((k) => k + 1); + prevFlipRef.current = shouldFlipX; + } + }, [shouldFlipX]); + + const springConfig = { stiffness: 100, damping: 20 }; + const animatedLeft = useSpring(targetX, springConfig); + const animatedTop = useSpring(targetY, springConfig); + + useEffect(() => { + animatedLeft.set(targetX); + }, [targetX, animatedLeft]); + + useEffect(() => { + animatedTop.set(targetY); + }, [targetY, animatedTop]); + + const finalLeft = leftOverride ?? animatedLeft; + const finalTop = topOverride ?? animatedTop; + const isFlipped = flippedOverride ?? shouldFlipX; + const transformOrigin = isFlipped ? "right top" : "left top"; + + const container = containerRef.current; + if (!(mounted && container)) { + return null; + } + + + if (!visible) { + return null; + } + + return createPortal( + + + {children} + + , + container + ); +} + +TooltipBox.displayName = "TooltipBox"; + +// ChartTooltip + +export interface ChartTooltipProps { + showDatePill?: boolean; + showCrosshair?: boolean; + showDots?: boolean; + content?: (props: { + point: Record; + index: number; + }) => ReactNode; + rows?: (point: Record) => TooltipRow[]; + children?: ReactNode; + className?: string; +} + +export function ChartTooltip({ + showDatePill = true, + showCrosshair = true, + showDots = true, + content, + rows: rowsRenderer, + children, + className = "", +}: ChartTooltipProps) { + const { + tooltipData, + width, + height, + innerHeight, + margin, + columnWidth, + lines, + xAccessor, + dateLabels, + containerRef, + orientation, + barXAccessor, + } = useChart(); + + const isHorizontal = orientation === "horizontal"; + + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const visible = tooltipData !== null; + const x = tooltipData?.x ?? 0; + const xWithMargin = x + margin.left; + + const firstLineDataKey = lines[0]?.dataKey; + const firstLineY = firstLineDataKey + ? (tooltipData?.yPositions[firstLineDataKey] ?? 0) + : 0; + const yWithMargin = firstLineY + margin.top; + + const crosshairSpringConfig = { stiffness: 300, damping: 30 }; + const animatedX = useSpring(xWithMargin, crosshairSpringConfig); + + useEffect(() => { + animatedX.set(xWithMargin); + }, [xWithMargin, animatedX]); + + const tooltipRows = useMemo(() => { + if (!tooltipData) { + return []; + } + + if (rowsRenderer) { + return rowsRenderer(tooltipData.point); + } + + return lines.map((line) => ({ + color: line.stroke, + label: line.dataKey, + value: (tooltipData.point[line.dataKey] as number) ?? 0, + })); + }, [tooltipData, lines, rowsRenderer]); + + const title = useMemo(() => { + if (!tooltipData) { + return undefined; + } + if (barXAccessor) { + return barXAccessor(tooltipData.point); + } + return xAccessor(tooltipData.point).toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + }); + }, [tooltipData, barXAccessor, xAccessor]); + + const container = containerRef.current; + if (!(mounted && container)) { + return null; + } + + + const tooltipContent = ( + <> + {showCrosshair && ( + + )} + + {showDots && visible && !isHorizontal && ( + + )} + + + {content ? ( + content({ + point: tooltipData?.point ?? {}, + index: tooltipData?.index ?? 0, + }) + ) : ( + + {children} + + )} + + + {showDatePill && dateLabels.length > 0 && visible && !isHorizontal && ( + + + + )} + + ); + + return createPortal(tooltipContent, container); +} + +ChartTooltip.displayName = "ChartTooltip"; + +// ─── Grid ──────────────────────────────────────────────────────────────────── + +export interface GridProps { + horizontal?: boolean; + vertical?: boolean; + numTicksRows?: number; + numTicksColumns?: number; + rowTickValues?: number[]; + stroke?: string; + strokeOpacity?: number; + strokeWidth?: number; + strokeDasharray?: string; + fadeHorizontal?: boolean; + fadeVertical?: boolean; +} + +export function Grid({ + horizontal = true, + vertical = false, + numTicksRows = 5, + numTicksColumns = 10, + rowTickValues, + stroke = chartCssVars.grid, + strokeOpacity = 1, + strokeWidth = 1, + strokeDasharray = "4,4", + fadeHorizontal = true, + fadeVertical = false, +}: GridProps) { + const { xScale, yScale, innerWidth, innerHeight, orientation, barScale } = + useChart(); + + const isHorizontalBarChart = orientation === "horizontal" && barScale; + const columnScale = isHorizontalBarChart ? yScale : xScale; + const uniqueId = useId(); + + const hMaskId = `grid-rows-fade-${uniqueId}`; + const hGradientId = `${hMaskId}-gradient`; + const vMaskId = `grid-cols-fade-${uniqueId}`; + const vGradientId = `${vMaskId}-gradient`; + + return ( + + {horizontal && fadeHorizontal && ( + + + + + + + + + + + + )} + + {vertical && fadeVertical && ( + + + + + + + + + + + + )} + + {horizontal && ( + + + + )} + {vertical && columnScale && typeof columnScale === "function" && ( + + + + )} + + ); +} + +Grid.displayName = "Grid"; + +// ─── XAxis ─────────────────────────────────────────────────────────────────── + +export interface XAxisProps { + numTicks?: number; + tickerHalfWidth?: number; +} + +interface XAxisLabelProps { + label: string; + x: number; + crosshairX: number | null; + isHovering: boolean; + tickerHalfWidth: number; +} + +function XAxisLabel({ + label, + x, + crosshairX, + isHovering, + tickerHalfWidth, +}: XAxisLabelProps) { + const fadeBuffer = 20; + const fadeRadius = tickerHalfWidth + fadeBuffer; + + let opacity = 1; + if (isHovering && crosshairX !== null) { + const distance = Math.abs(x - crosshairX); + if (distance < tickerHalfWidth) { + opacity = 0; + } else if (distance < fadeRadius) { + opacity = (distance - tickerHalfWidth) / fadeBuffer; + } + } + + return ( +
+ + {label} + +
+ ); +} + +export function XAxis({ numTicks = 5, tickerHalfWidth = 50 }: XAxisProps) { + const { xScale, margin, tooltipData, containerRef } = useChart(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const labelsToShow = useMemo(() => { + const domain = xScale.domain(); + const startDate = domain[0]; + const endDate = domain[1]; + + if (!(startDate && endDate)) { + return []; + } + + const startTime = startDate.getTime(); + const endTime = endDate.getTime(); + const timeRange = endTime - startTime; + + const tickCount = Math.max(2, numTicks); + const dates: Date[] = []; + + for (let i = 0; i < tickCount; i++) { + const t = i / (tickCount - 1); + const time = startTime + t * timeRange; + dates.push(new Date(time)); + } + + return dates.map((date) => ({ + date, + x: (xScale(date) ?? 0) + margin.left, + label: date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }), + })); + }, [xScale, margin.left, numTicks]); + + const isHovering = tooltipData !== null; + const crosshairX = tooltipData ? tooltipData.x + margin.left : null; + + const container = containerRef.current; + if (!(mounted && container)) { + return null; + } + + + return createPortal( +
+ {labelsToShow.map((item) => ( + + ))} +
, + container + ); +} + +XAxis.displayName = "XAxis"; + +// ─── YAxis ─────────────────────────────────────────────────────────────────── + +export interface YAxisProps { + numTicks?: number; + formatValue?: (value: number) => string; +} + +export function YAxis({ + numTicks = 5, + formatValue, +}: YAxisProps) { + const { yScale, margin, containerRef } = useChart(); + const [container, setContainer] = useState(null); + + useEffect(() => { + setContainer(containerRef.current); + }, [containerRef]); + + const ticks = useMemo(() => { + const domain = yScale.domain() as [number, number]; + const min = domain[0]; + const max = domain[1]; + const step = (max - min) / (numTicks - 1); + + return Array.from({ length: numTicks }, (_, i) => { + const value = min + step * i; + return { + value, + y: (yScale(value) ?? 0) + margin.top, + label: formatValue + ? formatValue(value) + : value >= 1000 + ? `${(value / 1000).toFixed(value % 1000 === 0 ? 0 : 1)}k` + : value.toLocaleString(), + }; + }); + }, [yScale, margin.top, numTicks, formatValue]); + + if (!container) { + return null; + } + + return createPortal( +
+ {ticks.map((tick) => ( +
+ + {tick.label} + +
+ ))} +
, + container + ); +} + +YAxis.displayName = "YAxis"; + +// ─── Area ──────────────────────────────────────────────────────────────────── + +export interface AreaProps { + dataKey: string; + fill?: string; + fillOpacity?: number; + stroke?: string; + strokeWidth?: number; + curve?: CurveFactory; + animate?: boolean; + showLine?: boolean; + showHighlight?: boolean; + gradientToOpacity?: number; + fadeEdges?: boolean; +} + +export function Area({ + dataKey, + fill = chartCssVars.linePrimary, + fillOpacity = 0.4, + stroke, + strokeWidth = 2, + curve = curveMonotoneX, + animate = true, + showLine = true, + showHighlight = true, + gradientToOpacity = 0, + fadeEdges = false, +}: AreaProps) { + const { + data, + xScale, + yScale, + innerHeight, + innerWidth, + tooltipData, + selection, + isLoaded, + animationDuration, + xAccessor, + } = useChart(); + + const pathRef = useRef(null); + const [pathLength, setPathLength] = useState(0); + const [clipWidth, setClipWidth] = useState(0); + + const uniqueId = useId(); + const gradientId = useMemo( + () => `area-gradient-${dataKey}-${Math.random().toString(36).slice(2, 9)}`, + [dataKey] + ); + const strokeGradientId = useMemo( + () => + `area-stroke-gradient-${dataKey}-${Math.random().toString(36).slice(2, 9)}`, + [dataKey] + ); + const edgeMaskId = `area-edge-mask-${dataKey}-${uniqueId}`; + const edgeGradientId = `${edgeMaskId}-gradient`; + + const resolvedStroke = stroke || fill; + + useEffect(() => { + if (pathRef.current && animate) { + const len = pathRef.current.getTotalLength(); + if (len > 0) { + setPathLength(len); + if (!isLoaded) { + requestAnimationFrame(() => { + setClipWidth(innerWidth); + }); + } + } + } + }, [animate, innerWidth, isLoaded]); + + const findLengthAtX = useCallback( + (targetX: number): number => { + const path = pathRef.current; + if (!path || pathLength === 0) { + return 0; + } + let low = 0; + let high = pathLength; + const tolerance = 0.5; + + while (high - low > tolerance) { + const mid = (low + high) / 2; + const point = path.getPointAtLength(mid); + if (point.x < targetX) { + low = mid; + } else { + high = mid; + } + } + return (low + high) / 2; + }, + [pathLength] + ); + + const segmentBounds = useMemo(() => { + if (!pathRef.current || pathLength === 0) { + return { startLength: 0, segmentLength: 0, isActive: false }; + } + + if (selection?.active) { + const startLength = findLengthAtX(selection.startX); + const endLength = findLengthAtX(selection.endX); + return { + startLength, + segmentLength: endLength - startLength, + isActive: true, + }; + } + + if (!tooltipData) { + return { startLength: 0, segmentLength: 0, isActive: false }; + } + + const idx = tooltipData.index; + const startIdx = Math.max(0, idx - 1); + const endIdx = Math.min(data.length - 1, idx + 1); + + const startPoint = data[startIdx]; + const endPoint = data[endIdx]; + if (!(startPoint && endPoint)) { + return { startLength: 0, segmentLength: 0, isActive: false }; + } + + const startX = xScale(xAccessor(startPoint)) ?? 0; + const endX = xScale(xAccessor(endPoint)) ?? 0; + + const startLength = findLengthAtX(startX); + const endLength = findLengthAtX(endX); + + return { + startLength, + segmentLength: endLength - startLength, + isActive: true, + }; + }, [ + tooltipData, + selection, + data, + xScale, + pathLength, + xAccessor, + findLengthAtX, + ]); + + const springConfig = { stiffness: 180, damping: 28 }; + const offsetSpring = useSpring(0, springConfig); + const segmentLengthSpring = useSpring(0, springConfig); + + const animatedDasharray = useMotionTemplate`${segmentLengthSpring} ${pathLength}`; + + useEffect(() => { + offsetSpring.set(-segmentBounds.startLength); + segmentLengthSpring.set(segmentBounds.segmentLength); + }, [ + segmentBounds.startLength, + segmentBounds.segmentLength, + offsetSpring, + segmentLengthSpring, + ]); + + const getY = useCallback( + (d: Record) => { + const value = d[dataKey]; + return typeof value === "number" ? (yScale(value) ?? 0) : 0; + }, + [dataKey, yScale] + ); + + const isHovering = tooltipData !== null || selection?.active === true; + const easing = "cubic-bezier(0.85, 0, 0.15, 1)"; + + return ( + <> + + + + + + + + + + + + + + {fadeEdges && ( + <> + + + + + + + + + + + )} + + + {animate && ( + + + 0 + ? `width ${animationDuration}ms ${easing}` + : "none", + }} + width={isLoaded ? innerWidth : clipWidth} + x={0} + y={0} + /> + + + )} + + + + + xScale(xAccessor(d)) ?? 0} + y={getY} + yScale={yScale} + /> + + + {showLine && ( + xScale(xAccessor(d)) ?? 0} + y={getY} + /> + )} + + + + {showHighlight && + showLine && + isHovering && + isLoaded && + pathRef.current && ( + + )} + + ); +} + +Area.displayName = "Area"; + +// ─── Segment Components ────────────────────────────────────────────────────── + +export function SegmentBackground() { + const { selection, innerHeight } = useChart(); + + if (!selection?.active) { + return null; + } + + const x = Math.min(selection.startX, selection.endX); + const width = Math.abs(selection.endX - selection.startX); + + return ( + + ); +} + +SegmentBackground.displayName = "SegmentBackground"; + +export function SegmentLineFrom() { + const { selection, innerHeight } = useChart(); + + if (!selection?.active) { + return null; + } + + const x = Math.min(selection.startX, selection.endX); + + return ( + + ); +} + +SegmentLineFrom.displayName = "SegmentLineFrom"; + +export function SegmentLineTo() { + const { selection, innerHeight } = useChart(); + + if (!selection?.active) { + return null; + } + + const x = Math.max(selection.startX, selection.endX); + + return ( + + ); +} + +SegmentLineTo.displayName = "SegmentLineTo"; + +// ─── Pattern Components ────────────────────────────────────────────────────── + +export interface PatternLinesProps { + id: string; + width?: number; + height?: number; + stroke?: string; + strokeWidth?: number; + orientation?: ("diagonal" | "horizontal" | "vertical")[]; +} + +export function PatternLines({ + id, + width = 6, + height = 6, + stroke = "var(--chart-line-primary)", + strokeWidth = 1, + orientation = ["diagonal"], +}: PatternLinesProps) { + const paths: string[] = []; + + for (const o of orientation) { + if (o === "diagonal") { + paths.push(`M0,${height}l${width},${-height}`); + paths.push(`M${-width / 4},${height / 4}l${width / 2},${-height / 2}`); + paths.push(`M${(3 * width) / 4},${height + height / 4}l${width / 2},${-height / 2}`); + } else if (o === "horizontal") { + paths.push(`M0,${height / 2}l${width},0`); + } else if (o === "vertical") { + paths.push(`M${width / 2},0l0,${height}`); + } + } + + return ( + + + + + + ); +} + +PatternLines.displayName = "PatternLines"; + +export interface PatternAreaProps { + dataKey: string; + fill?: string; + curve?: CurveFactory; +} + +export function PatternArea({ + dataKey, + fill = "url(#area-pattern)", + curve = curveMonotoneX, +}: PatternAreaProps) { + const { data, xScale, yScale, xAccessor } = useChart(); + + const getY = useCallback( + (d: Record) => { + const value = d[dataKey]; + return typeof value === "number" ? (yScale(value) ?? 0) : 0; + }, + [dataKey, yScale] + ); + + return ( + xScale(xAccessor(d)) ?? 0} + y={getY} + yScale={yScale} + /> + ); +} + +PatternArea.displayName = "PatternArea"; + +// ─── AreaChart ─────────────────────────────────────────────────────────────── + +function isPostOverlayComponent(child: ReactElement): boolean { + const childType = child.type as { + displayName?: string; + name?: string; + __isChartMarkers?: boolean; + }; + + if (childType.__isChartMarkers) { + return true; + } + + const componentName = + typeof child.type === "function" + ? childType.displayName || childType.name || "" + : ""; + + return componentName === "ChartMarkers" || componentName === "MarkerGroup"; +} + +function extractAreaConfigs(children: ReactNode): LineConfig[] { + const configs: LineConfig[] = []; + + Children.forEach(children, (child) => { + if (!isValidElement(child)) { + return; + } + + const childType = child.type as { + displayName?: string; + name?: string; + }; + const componentName = + typeof child.type === "function" + ? childType.displayName || childType.name || "" + : ""; + + const props = child.props as AreaProps | undefined; + const isAreaComponent = + componentName === "Area" || + child.type === Area || + (props && typeof props.dataKey === "string" && props.dataKey.length > 0); + + if (isAreaComponent && props?.dataKey) { + configs.push({ + dataKey: props.dataKey, + stroke: props.stroke || props.fill || "var(--chart-line-primary)", + strokeWidth: props.strokeWidth || 2, + }); + } + }); + + return configs; +} + +export interface AreaChartProps { + data: Record[]; + xDataKey?: string; + margin?: Partial; + animationDuration?: number; + aspectRatio?: string; + className?: string; + children: ReactNode; +} + +const DEFAULT_MARGIN: Margin = { top: 40, right: 40, bottom: 40, left: 40 }; + +interface ChartInnerProps { + width: number; + height: number; + data: Record[]; + xDataKey: string; + margin: Margin; + animationDuration: number; + children: ReactNode; + containerRef: RefObject; +} + +function ChartInner({ + width, + height, + data, + xDataKey, + margin, + animationDuration, + children, + containerRef, +}: ChartInnerProps) { + const [isLoaded, setIsLoaded] = useState(false); + + const lines = useMemo(() => extractAreaConfigs(children), [children]); + + const innerWidth = width - margin.left - margin.right; + const innerHeight = height - margin.top - margin.bottom; + + const xAccessor = useCallback( + (d: Record): Date => { + const value = d[xDataKey]; + return value instanceof Date ? value : new Date(value as string | number); + }, + [xDataKey] + ); + + const bisectDate = useMemo( + () => bisector, Date>((d) => xAccessor(d)).left, + [xAccessor] + ); + + const xScale = useMemo(() => { + const dates = data.map((d) => xAccessor(d)); + const minTime = Math.min(...dates.map((d) => d.getTime())); + const maxTime = Math.max(...dates.map((d) => d.getTime())); + + return scaleTime({ + range: [0, innerWidth], + domain: [minTime, maxTime], + }); + }, [innerWidth, data, xAccessor]); + + const columnWidth = useMemo(() => { + if (data.length < 2) { + return 0; + } + return innerWidth / (data.length - 1); + }, [innerWidth, data.length]); + + const yScale = useMemo(() => { + let maxValue = 0; + for (const line of lines) { + for (const d of data) { + const value = d[line.dataKey]; + if (typeof value === "number" && value > maxValue) { + maxValue = value; + } + } + } + + if (maxValue === 0) { + maxValue = 100; + } + + return scaleLinear({ + range: [innerHeight, 0], + domain: [0, maxValue * 1.1], + nice: true, + }); + }, [innerHeight, data, lines]); + + const dateLabels = useMemo( + () => + data.map((d) => + xAccessor(d).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }) + ), + [data, xAccessor] + ); + + useEffect(() => { + const timer = setTimeout(() => { + setIsLoaded(true); + }, animationDuration); + return () => clearTimeout(timer); + }, [animationDuration]); + + const canInteract = isLoaded; + + const { + tooltipData, + setTooltipData, + selection, + clearSelection, + interactionHandlers, + interactionStyle, + } = useChartInteraction({ + xScale, + yScale, + data, + lines, + margin, + xAccessor, + bisectDate, + canInteract, + }); + + if (width < 10 || height < 10) { + return null; + } + + const preOverlayChildren: ReactElement[] = []; + const postOverlayChildren: ReactElement[] = []; + + Children.forEach(children, (child) => { + if (!isValidElement(child)) { + return; + } + + if (isPostOverlayComponent(child)) { + postOverlayChildren.push(child); + } else { + preOverlayChildren.push(child); + } + }); + + const contextValue = { + data, + xScale, + yScale, + width, + height, + innerWidth, + innerHeight, + margin, + columnWidth, + tooltipData, + setTooltipData, + containerRef, + lines, + isLoaded, + animationDuration, + xAccessor, + dateLabels, + selection, + clearSelection, + }; + + return ( + + + + ); +} + +export function AreaChart({ + data, + xDataKey = "date", + margin: marginProp, + animationDuration = 1100, + aspectRatio = "2 / 1", + className = "", + children, +}: AreaChartProps) { + const containerRef = useRef(null); + const margin = { ...DEFAULT_MARGIN, ...marginProp }; + + return ( +
+ + {({ width, height }) => ( + + {children} + + )} + +
+ ); +} + +export default AreaChart; diff --git a/package-lock.json b/package-lock.json index d963c47..bafe5ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,11 +19,18 @@ "@stripe/stripe-js": "^8.7.0", "@tanstack/react-virtual": "^3.13.21", "@types/d3": "^7.4.3", + "@visx/curve": "^3.12.0", + "@visx/event": "^3.12.0", + "@visx/grid": "^3.12.0", + "@visx/responsive": "^3.12.0", + "@visx/scale": "^3.12.0", + "@visx/shape": "^3.12.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cobe": "^0.6.5", "country-flag-icons": "^1.6.4", "d3": "^7.9.0", + "d3-array": "^3.2.4", "d3-scale": "^4.0.2", "framer-motion": "^12.23.26", "html-to-image": "^1.11.13", @@ -37,6 +44,7 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "react-markdown": "^10.1.0", + "react-use-measure": "^2.1.7", "recharts": "^2.15.0", "sonner": "^2.0.7", "svg-dotted-map": "^2.0.1", @@ -48,6 +56,7 @@ "@tailwindcss/typography": "^0.5.19", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@types/d3-array": "^3.2.2", "@types/d3-scale": "^4.0.9", "@types/node": "^20.14.12", "@types/react": "^19.2.14", @@ -176,7 +185,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1788,7 +1796,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" }, @@ -1829,7 +1836,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=20.19.0" } @@ -5537,7 +5543,6 @@ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.7.0.tgz", "integrity": "sha512-tNUerSstwNC1KuHgX4CASGO0Md3CB26IJzSXmVlSuFvhsBP4ZaEPpY4jxWOn9tfdDscuVT4Kqb8cZ2o9nLCgRQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=12.16" } @@ -5603,36 +5608,6 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", @@ -5699,20 +5674,12 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -5725,7 +5692,7 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" @@ -5735,7 +5702,7 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", @@ -5746,7 +5713,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.2" @@ -6032,26 +5999,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -6086,6 +6033,7 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, "license": "MIT" }, "node_modules/@types/json5": { @@ -6095,6 +6043,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -6114,6 +6068,7 @@ "version": "20.19.33", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -6137,7 +6092,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6146,9 +6100,8 @@ "version": "19.2.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6216,7 +6169,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -6729,6 +6681,298 @@ "win32" ] }, + "node_modules/@visx/curve": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@visx/curve/-/curve-3.12.0.tgz", + "integrity": "sha512-Ng1mefXIzoIoAivw7dJ+ZZYYUbfuwXgZCgQynShr6ZIVw7P4q4HeQfJP3W24ON+1uCSrzoycHSXRelhR9SBPcw==", + "license": "MIT", + "dependencies": { + "@types/d3-shape": "^1.3.1", + "d3-shape": "^1.0.6" + } + }, + "node_modules/@visx/curve/node_modules/@types/d3-path": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==", + "license": "MIT" + }, + "node_modules/@visx/curve/node_modules/@types/d3-shape": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", + "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "^1" + } + }, + "node_modules/@visx/curve/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/@visx/curve/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/@visx/event": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@visx/event/-/event-3.12.0.tgz", + "integrity": "sha512-9Lvw6qJ0Fi+y1vsC1WspfdIKCxHTb7oy59Uql1uBdPGT8zChP0vuxW0jQNQRDbKgoefj4pCXAFi8+MF1mEtVTw==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/point": "3.12.0" + } + }, + "node_modules/@visx/grid": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@visx/grid/-/grid-3.12.0.tgz", + "integrity": "sha512-L4ex2ooSYhwNIxJ3XFIKRhoSvEGjPc2Y3YCrtNB4TV5Ofdj4q0UMOsxfrH23Pr8HSHuQhb6VGMgYoK0LuWqDmQ==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "@visx/curve": "3.12.0", + "@visx/group": "3.12.0", + "@visx/point": "3.12.0", + "@visx/scale": "3.12.0", + "@visx/shape": "3.12.0", + "classnames": "^2.3.1", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" + } + }, + "node_modules/@visx/group": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@visx/group/-/group-3.12.0.tgz", + "integrity": "sha512-Dye8iS1alVXPv7nj/7M37gJe6sSKqJLH7x6sEWAsRQ9clI0kFvjbKcKgF+U3aAVQr0NCohheFV+DtR8trfK/Ag==", + "license": "MIT", + "dependencies": { + "@types/react": "*", + "classnames": "^2.3.1", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" + } + }, + "node_modules/@visx/point": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@visx/point/-/point-3.12.0.tgz", + "integrity": "sha512-I6UrHoJAEVbx3RORQNupgTiX5EzjuZpiwLPxn8L2mI5nfERotPKi1Yus12Cq2WtXqEBR/WgqTnoImlqOXBykcA==", + "license": "MIT" + }, + "node_modules/@visx/responsive": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@visx/responsive/-/responsive-3.12.0.tgz", + "integrity": "sha512-GV1BTYwAGlk/K5c9vH8lS2syPnTuIqEacI7L6LRPbsuaLscXMNi+i9fZyzo0BWvAdtRV8v6Urzglo++lvAXT1Q==", + "license": "MIT", + "dependencies": { + "@types/lodash": "^4.14.172", + "@types/react": "*", + "lodash": "^4.17.21", + "prop-types": "^15.6.1" + }, + "peerDependencies": { + "react": "^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0" + } + }, + "node_modules/@visx/scale": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@visx/scale/-/scale-3.12.0.tgz", + "integrity": "sha512-+ubijrZ2AwWCsNey0HGLJ0YKNeC/XImEFsr9rM+Uef1CM3PNM43NDdNTrdBejSlzRq0lcfQPWYMYQFSlkLcPOg==", + "license": "MIT", + "dependencies": { + "@visx/vendor": "3.12.0" + } + }, + "node_modules/@visx/shape": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@visx/shape/-/shape-3.12.0.tgz", + "integrity": "sha512-/1l0lrpX9tPic6SJEalryBKWjP/ilDRnQA+BGJTI1tj7i23mJ/J0t4nJHyA1GrL4QA/bM/qTJ35eyz5dEhJc4g==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "^1.0.8", + "@types/d3-shape": "^1.3.1", + "@types/lodash": "^4.14.172", + "@types/react": "*", + "@visx/curve": "3.12.0", + "@visx/group": "3.12.0", + "@visx/scale": "3.12.0", + "classnames": "^2.3.1", + "d3-path": "^1.0.5", + "d3-shape": "^1.2.0", + "lodash": "^4.17.21", + "prop-types": "^15.5.10" + }, + "peerDependencies": { + "react": "^16.3.0-0 || ^17.0.0-0 || ^18.0.0-0" + } + }, + "node_modules/@visx/shape/node_modules/@types/d3-path": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-1.0.11.tgz", + "integrity": "sha512-4pQMp8ldf7UaB/gR8Fvvy69psNHkTpD/pVw3vmEi8iZAB9EPMBruB1JvHO4BIq9QkUUd2lV1F5YXpMNj7JPBpw==", + "license": "MIT" + }, + "node_modules/@visx/shape/node_modules/@types/d3-shape": { + "version": "1.3.12", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", + "integrity": "sha512-8oMzcd4+poSLGgV0R1Q1rOlx/xdmozS4Xab7np0eamFFUYq71AU9pOCJEFnkXW2aI/oXdVYJzw6pssbSut7Z9Q==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "^1" + } + }, + "node_modules/@visx/shape/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/@visx/shape/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/@visx/vendor": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@visx/vendor/-/vendor-3.12.0.tgz", + "integrity": "sha512-SVO+G0xtnL9dsNpGDcjCgoiCnlB3iLSM9KLz1sLbSrV7RaVXwY3/BTm2X9OWN1jH2a9M+eHt6DJ6sE6CXm4cUg==", + "license": "MIT and ISC", + "dependencies": { + "@types/d3-array": "3.0.3", + "@types/d3-color": "3.1.0", + "@types/d3-delaunay": "6.0.1", + "@types/d3-format": "3.0.1", + "@types/d3-geo": "3.1.0", + "@types/d3-interpolate": "3.0.1", + "@types/d3-scale": "4.0.2", + "@types/d3-time": "3.0.0", + "@types/d3-time-format": "2.1.0", + "d3-array": "3.2.1", + "d3-color": "3.1.0", + "d3-delaunay": "6.0.2", + "d3-format": "3.1.0", + "d3-geo": "3.1.0", + "d3-interpolate": "3.0.1", + "d3-scale": "4.0.2", + "d3-time": "3.1.0", + "d3-time-format": "4.1.0", + "internmap": "2.0.3" + } + }, + "node_modules/@visx/vendor/node_modules/@types/d3-array": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz", + "integrity": "sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==", + "license": "MIT" + }, + "node_modules/@visx/vendor/node_modules/@types/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==", + "license": "MIT" + }, + "node_modules/@visx/vendor/node_modules/@types/d3-delaunay": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz", + "integrity": "sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==", + "license": "MIT" + }, + "node_modules/@visx/vendor/node_modules/@types/d3-format": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz", + "integrity": "sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==", + "license": "MIT" + }, + "node_modules/@visx/vendor/node_modules/@types/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@visx/vendor/node_modules/@types/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@visx/vendor/node_modules/@types/d3-time": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz", + "integrity": "sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==", + "license": "MIT" + }, + "node_modules/@visx/vendor/node_modules/@types/d3-time-format": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-2.1.0.tgz", + "integrity": "sha512-/myT3I7EwlukNOX2xVdMzb8FRgNzRMpsZddwst9Ld/VFe6LyJyRp0s32l/V9XoUzk+Gqu56F/oGk6507+8BxrA==", + "license": "MIT" + }, + "node_modules/@visx/vendor/node_modules/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@visx/vendor/node_modules/d3-delaunay": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.2.tgz", + "integrity": "sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@visx/vendor/node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@visx/vendor/node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", @@ -6891,170 +7135,11 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" - }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7062,18 +7147,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -7120,55 +7193,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -7660,7 +7684,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -7916,15 +7939,6 @@ "node": ">= 6" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, "node_modules/class-variance-authority": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", @@ -7937,6 +7951,12 @@ "url": "https://polar.sh/cva" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -8500,7 +8520,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -8862,13 +8881,6 @@ "node": ">=0.10.0" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT" - }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -8931,19 +8943,6 @@ "dev": true, "license": "MIT" }, - "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -9071,12 +9070,6 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "license": "MIT" - }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -9204,7 +9197,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9390,7 +9382,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9621,6 +9612,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -9633,6 +9625,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -9669,15 +9662,6 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -10157,12 +10141,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -10226,6 +10204,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11097,42 +11076,12 @@ "node": ">=10" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -11254,12 +11203,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -11318,7 +11261,6 @@ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.1.0.tgz", "integrity": "sha512-xd1d/XRkwqnsq6FP3zH1Q+Ejqn2ULIJeDZ+FTKpaabVpZREjsJKRJwuokTNgdqOU+fl55KgbvgZ1pRTSWCP2kQ==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "fast-png": "^6.2.0", @@ -11429,19 +11371,6 @@ "dev": true, "license": "MIT" }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "license": "MIT", - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -11523,16 +11452,6 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" - } - }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -11711,12 +11630,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -12181,27 +12094,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -12334,18 +12226,11 @@ "dev": true, "license": "MIT" }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, "node_modules/next": { "version": "16.1.6", "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "16.1.6", "@swc/helpers": "0.5.15", @@ -12862,7 +12747,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13042,41 +12926,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT" - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -13246,7 +13095,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -13256,7 +13104,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13407,6 +13254,21 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-use-measure": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", + "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.13", + "react-dom": ">=16.13" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -13697,7 +13559,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "license": "MIT", - "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -13834,60 +13695,6 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/schema-utils/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/schema-utils/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -14563,7 +14370,6 @@ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -14610,19 +14416,6 @@ "node": ">=4" } }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -14668,40 +14461,6 @@ "node": ">=10" } }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -14805,7 +14564,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15054,7 +14812,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15119,6 +14876,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -15479,7 +15237,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -15573,7 +15330,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15747,104 +15503,12 @@ "node": ">=18" } }, - "node_modules/watchpack": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", - "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", "license": "BSD-2-Clause" }, - "node_modules/webpack": { - "version": "5.105.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz", - "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.19.0", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.5.1", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", - "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/whatwg-mimetype": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", @@ -16101,7 +15765,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16287,7 +15950,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -16455,7 +16117,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index fe3897a..eaaaac7 100644 --- a/package.json +++ b/package.json @@ -23,11 +23,18 @@ "@stripe/stripe-js": "^8.7.0", "@tanstack/react-virtual": "^3.13.21", "@types/d3": "^7.4.3", + "@visx/curve": "^3.12.0", + "@visx/event": "^3.12.0", + "@visx/grid": "^3.12.0", + "@visx/responsive": "^3.12.0", + "@visx/scale": "^3.12.0", + "@visx/shape": "^3.12.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cobe": "^0.6.5", "country-flag-icons": "^1.6.4", "d3": "^7.9.0", + "d3-array": "^3.2.4", "d3-scale": "^4.0.2", "framer-motion": "^12.23.26", "html-to-image": "^1.11.13", @@ -41,6 +48,7 @@ "react": "^19.2.3", "react-dom": "^19.2.3", "react-markdown": "^10.1.0", + "react-use-measure": "^2.1.7", "recharts": "^2.15.0", "sonner": "^2.0.7", "svg-dotted-map": "^2.0.1", @@ -58,6 +66,7 @@ "@tailwindcss/typography": "^0.5.19", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", + "@types/d3-array": "^3.2.2", "@types/d3-scale": "^4.0.9", "@types/node": "^20.14.12", "@types/react": "^19.2.14", diff --git a/styles/globals.css b/styles/globals.css index eb0c354..1937543 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -18,6 +18,18 @@ --chart-grid: #262626; --chart-axis: #737373; + /* * visx area chart tokens (dark-only) */ + --chart-background: #0a0a0a; + --chart-foreground: #404040; + --chart-foreground-muted: #a3a3a3; + --chart-line-primary: #FD5E0F; + --chart-line-secondary: #737373; + --chart-crosshair: #404040; + --chart-label: #a3a3a3; + --chart-marker-background: #262626; + --chart-marker-border: #404040; + --chart-marker-foreground: #fafafa; + /* * shadcn-compatible semantic tokens (dark-only) */ --background: 10 10 10; --foreground: 250 250 250;