Download src/components/data-recharts.tsx from DynamicIntelligence/dynamic_intelligence_sample_data: direct link, hf CLI and curl.
- Browser
- Download file 12.8 kB
-
https://huggingface.co/spaces/DynamicIntelligence/dynamic_intelligence_sample_data/resolve/08eabcff2e38dc7d9251a2d92489cf16d9f752b4/src/components/data-recharts.tsx
- Command line
-
hf download hf://spaces/DynamicIntelligence/dynamic_intelligence_sample_data@08eabcff2e38dc7d9251a2d92489cf16d9f752b4/src/components/data-recharts.tsx
-
curl -L -o data-recharts.tsx https://huggingface.co/spaces/DynamicIntelligence/dynamic_intelligence_sample_data/resolve/08eabcff2e38dc7d9251a2d92489cf16d9f752b4/src/components/data-recharts.tsx
12.8 kB
| "use client"; | |
| import { useEffect, useState } from "react"; | |
| import { useTime } from "../context/time-context"; | |
| import { | |
| LineChart, | |
| Line, | |
| XAxis, | |
| YAxis, | |
| CartesianGrid, | |
| ResponsiveContainer, | |
| Tooltip, | |
| } from "recharts"; | |
| type DataGraphProps = { | |
| data: Array<Array<Record<string, number>>>; | |
| onChartsReady?: () => void; | |
| }; | |
| import React, { useMemo } from "react"; | |
| // Use the same delimiter as the data processing | |
| const SERIES_NAME_DELIMITER = " | "; | |
| export const DataRecharts = React.memo( | |
| ({ data, onChartsReady }: DataGraphProps) => { | |
| // Shared hoveredTime for all graphs | |
| const [hoveredTime, setHoveredTime] = useState<number | null>(null); | |
| if (!Array.isArray(data) || data.length === 0) return null; | |
| useEffect(() => { | |
| if (typeof onChartsReady === "function") { | |
| onChartsReady(); | |
| } | |
| }, [onChartsReady]); | |
| return ( | |
| <div className="grid md:grid-cols-2 grid-cols-1 gap-4"> | |
| {data.map((group, idx) => ( | |
| <SingleDataGraph | |
| key={idx} | |
| data={group} | |
| hoveredTime={hoveredTime} | |
| setHoveredTime={setHoveredTime} | |
| /> | |
| ))} | |
| </div> | |
| ); | |
| }, | |
| ); | |
| const SingleDataGraph = React.memo( | |
| ({ | |
| data, | |
| hoveredTime, | |
| setHoveredTime, | |
| }: { | |
| data: Array<Record<string, number>>; | |
| hoveredTime: number | null; | |
| setHoveredTime: (t: number | null) => void; | |
| }) => { | |
| const { currentTime, setCurrentTime } = useTime(); | |
| function flattenRow(row: Record<string, any>, prefix = ""): Record<string, number> { | |
| const result: Record<string, number> = {}; | |
| for (const [key, value] of Object.entries(row)) { | |
| // Special case: if this is a group value that is a primitive, assign to prefix.key | |
| if (typeof value === "number") { | |
| if (prefix) { | |
| result[`${prefix}${SERIES_NAME_DELIMITER}${key}`] = value; | |
| } else { | |
| result[key] = value; | |
| } | |
| } else if (value !== null && typeof value === "object" && !Array.isArray(value)) { | |
| // If it's an object, recurse | |
| Object.assign(result, flattenRow(value, prefix ? `${prefix}${SERIES_NAME_DELIMITER}${key}` : key)); | |
| } | |
| } | |
| // Always keep timestamp at top level if present | |
| if ("timestamp" in row) { | |
| result["timestamp"] = row["timestamp"]; | |
| } | |
| return result; | |
| } | |
| // Flatten all rows for recharts | |
| const chartData = useMemo(() => data.map(row => flattenRow(row)), [data]); | |
| const [dataKeys, setDataKeys] = useState<string[]>([]); | |
| const [visibleKeys, setVisibleKeys] = useState<string[]>([]); | |
| useEffect(() => { | |
| if (!chartData || chartData.length === 0) return; | |
| // Get all keys except timestamp from the first row | |
| const keys = Object.keys(chartData[0]).filter((k) => k !== "timestamp"); | |
| setDataKeys(keys); | |
| // Show WRIST and TIPS by default for a good overview without performance lag | |
| const defaultVisible = keys.filter(k => | |
| k.toLowerCase().includes("wrist") || | |
| k.toLowerCase().includes("tip") | |
| ); | |
| setVisibleKeys(defaultVisible); | |
| }, [chartData]); | |
| // Parse dataKeys into groups (dot notation) | |
| const groups: Record<string, string[]> = {}; | |
| const singles: string[] = []; | |
| dataKeys.forEach((key) => { | |
| const parts = key.split(SERIES_NAME_DELIMITER); | |
| if (parts.length > 1) { | |
| const group = parts[0]; | |
| if (!groups[group]) groups[group] = []; | |
| groups[group].push(key); | |
| } else { | |
| singles.push(key); | |
| } | |
| }); | |
| // Assign a color per group (and for singles) | |
| const allGroups = [...Object.keys(groups), ...singles]; | |
| const groupColorMap: Record<string, string> = {}; | |
| allGroups.forEach((group, idx) => { | |
| groupColorMap[group] = `hsl(${idx * (360 / allGroups.length)}, 100%, 50%)`; | |
| }); | |
| // Find the closest data point to the current time for highlighting | |
| const findClosestDataIndex = (time: number) => { | |
| if (!chartData.length) return 0; | |
| // Find the index of the first data point whose timestamp is >= time (ceiling) | |
| const idx = chartData.findIndex((point) => point.timestamp >= time); | |
| if (idx !== -1) return idx; | |
| // If all timestamps are less than time, return the last index | |
| return chartData.length - 1; | |
| }; | |
| const handleMouseLeave = () => { | |
| setHoveredTime(null); | |
| }; | |
| const handleClick = (data: any) => { | |
| if (data && data.activePayload && data.activePayload.length) { | |
| const timeValue = data.activePayload[0].payload.timestamp; | |
| setCurrentTime(timeValue); | |
| } | |
| }; | |
| // Custom legend to show current value next to each series | |
| const CustomLegend = () => { | |
| const closestIndex = findClosestDataIndex( | |
| hoveredTime != null ? hoveredTime : currentTime, | |
| ); | |
| const currentData = chartData[closestIndex] || {}; | |
| // Parse dataKeys into groups (dot notation) | |
| const groups: Record<string, string[]> = {}; | |
| const singles: string[] = []; | |
| dataKeys.forEach((key) => { | |
| const parts = key.split(SERIES_NAME_DELIMITER); | |
| if (parts.length > 1) { | |
| const group = parts[0]; | |
| if (!groups[group]) groups[group] = []; | |
| groups[group].push(key); | |
| } else { | |
| singles.push(key); | |
| } | |
| }); | |
| // Assign a color per group (and for singles) | |
| const allGroups = [...Object.keys(groups), ...singles]; | |
| const groupColorMap: Record<string, string> = {}; | |
| allGroups.forEach((group, idx) => { | |
| groupColorMap[group] = `hsl(${idx * (360 / allGroups.length)}, 100%, 50%)`; | |
| }); | |
| const isGroupChecked = (group: string) => groups[group].every(k => visibleKeys.includes(k)); | |
| const isGroupIndeterminate = (group: string) => groups[group].some(k => visibleKeys.includes(k)) && !isGroupChecked(group); | |
| const handleGroupCheckboxChange = (group: string) => { | |
| if (isGroupChecked(group)) { | |
| // Uncheck all children | |
| setVisibleKeys((prev) => prev.filter(k => !groups[group].includes(k))); | |
| } else { | |
| // Check all children | |
| setVisibleKeys((prev) => Array.from(new Set([...prev, ...groups[group]]))); | |
| } | |
| }; | |
| const handleCheckboxChange = (key: string) => { | |
| setVisibleKeys((prev) => | |
| prev.includes(key) ? prev.filter((k) => k !== key) : [...prev, key] | |
| ); | |
| }; | |
| return ( | |
| <div className="grid grid-cols-[repeat(auto-fit,250px)] gap-4 mx-8"> | |
| {/* Grouped keys */} | |
| {Object.entries(groups).map(([group, children]) => { | |
| const color = groupColorMap[group]; | |
| return ( | |
| <div key={group} className="mb-2"> | |
| <label className="flex gap-2 cursor-pointer select-none font-semibold"> | |
| <input | |
| type="checkbox" | |
| checked={isGroupChecked(group)} | |
| ref={el => { if (el) el.indeterminate = isGroupIndeterminate(group); }} | |
| onChange={() => handleGroupCheckboxChange(group)} | |
| className="size-3.5 mt-1" | |
| style={{ accentColor: color }} | |
| /> | |
| <span className="text-sm w-40 text-white">{group}</span> | |
| </label> | |
| <div className="pl-7 flex flex-col gap-1 mt-1"> | |
| {children.map((key) => ( | |
| <label key={key} className="flex gap-2 cursor-pointer select-none"> | |
| <input | |
| type="checkbox" | |
| checked={visibleKeys.includes(key)} | |
| onChange={() => handleCheckboxChange(key)} | |
| className="size-3.5 mt-1" | |
| style={{ accentColor: color }} | |
| /> | |
| <span className={`text-xs break-all w-36 ${visibleKeys.includes(key) ? "text-white" : "text-gray-400"}`}>{key.slice(group.length + 1)}</span> | |
| <span className={`text-xs font-mono ml-auto ${visibleKeys.includes(key) ? "text-orange-300" : "text-gray-500"}`}> | |
| {typeof currentData[key] === "number" ? currentData[key].toFixed(2) : "--"} | |
| </span> | |
| </label> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| })} | |
| {/* Singles (non-grouped) */} | |
| {singles.map((key) => { | |
| const color = groupColorMap[key]; | |
| return ( | |
| <label key={key} className="flex gap-2 cursor-pointer select-none"> | |
| <input | |
| type="checkbox" | |
| checked={visibleKeys.includes(key)} | |
| onChange={() => handleCheckboxChange(key)} | |
| className="size-3.5 mt-1" | |
| style={{ accentColor: color }} | |
| /> | |
| <span className={`text-sm break-all w-40 ${visibleKeys.includes(key) ? "text-white" : "text-gray-400"}`}>{key}</span> | |
| <span className={`text-sm font-mono ml-auto ${visibleKeys.includes(key) ? "text-orange-300" : "text-gray-500"}`}> | |
| {typeof currentData[key] === "number" ? currentData[key].toFixed(2) : "--"} | |
| </span> | |
| </label> | |
| ); | |
| })} | |
| </div> | |
| ); | |
| }; | |
| return ( | |
| <div className="w-full"> | |
| <div className="w-full h-80" onMouseLeave={handleMouseLeave}> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <LineChart | |
| data={chartData} | |
| syncId="episode-sync" | |
| margin={{ top: 24, right: 16, left: 0, bottom: 16 }} | |
| onClick={handleClick} | |
| onMouseMove={(state: any) => { | |
| setHoveredTime( | |
| state?.activePayload?.[0]?.payload?.timestamp ?? | |
| state?.activeLabel ?? | |
| null, | |
| ); | |
| }} | |
| onMouseLeave={handleMouseLeave} | |
| > | |
| <CartesianGrid strokeDasharray="3 3" stroke="#444" /> | |
| <XAxis | |
| dataKey="timestamp" | |
| label={{ | |
| value: "time", | |
| position: "insideBottomLeft", | |
| fill: "#cbd5e1", | |
| }} | |
| domain={[ | |
| chartData.at(0)?.timestamp ?? 0, | |
| chartData.at(-1)?.timestamp ?? 0, | |
| ]} | |
| ticks={useMemo( | |
| () => | |
| Array.from( | |
| new Set(chartData.map((d) => Math.ceil(d.timestamp))), | |
| ), | |
| [chartData], | |
| )} | |
| stroke="#cbd5e1" | |
| minTickGap={20} // Increased for fewer ticks | |
| allowDataOverflow={true} | |
| /> | |
| <YAxis | |
| domain={["auto", "auto"]} | |
| stroke="#cbd5e1" | |
| interval={0} | |
| allowDataOverflow={true} | |
| /> | |
| <Tooltip | |
| content={() => null} | |
| active={true} | |
| isAnimationActive={false} | |
| defaultIndex={ | |
| !hoveredTime ? findClosestDataIndex(currentTime) : undefined | |
| } | |
| /> | |
| {/* Render lines for visible dataKeys only */} | |
| {dataKeys.map((key) => { | |
| // Use group color for all keys in a group | |
| const group = key.includes(SERIES_NAME_DELIMITER) ? key.split(SERIES_NAME_DELIMITER)[0] : key; | |
| const color = groupColorMap[group]; | |
| let strokeDasharray: string | undefined = undefined; | |
| if (groups[group] && groups[group].length > 1) { | |
| const idxInGroup = groups[group].indexOf(key); | |
| if (idxInGroup > 0) strokeDasharray = "5 5"; | |
| } | |
| return ( | |
| visibleKeys.includes(key) && ( | |
| <Line | |
| key={key} | |
| type="monotone" | |
| dataKey={key} | |
| name={key} | |
| stroke={color} | |
| strokeDasharray={strokeDasharray} | |
| dot={false} | |
| activeDot={false} | |
| strokeWidth={1.5} | |
| isAnimationActive={false} | |
| /> | |
| ) | |
| ); | |
| })} | |
| </LineChart> | |
| </ResponsiveContainer> | |
| </div> | |
| <CustomLegend /> | |
| </div> | |
| ); | |
| }, | |
| ); // End React.memo | |
| SingleDataGraph.displayName = "SingleDataGraph"; | |
| DataRecharts.displayName = "DataGraph"; | |
| export default DataRecharts; | |