From 5db8f8a68ece5e41f8c9b0031073e87687b85357 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Sun, 15 Mar 2026 19:54:51 +0100 Subject: [PATCH] Enable React Compiler for SimulationTimeline chart components Remove "use no memo" directives and manual useMemo/useCallback from CompartmentTimeSeries and StackedAreaChart. The React Compiler handles memoization automatically for these components. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../subviews/simulation-timeline.tsx | 501 ++++++++---------- 1 file changed, 223 insertions(+), 278 deletions(-) diff --git a/libs/@hashintel/petrinaut/src/views/Editor/panels/BottomPanel/subviews/simulation-timeline.tsx b/libs/@hashintel/petrinaut/src/views/Editor/panels/BottomPanel/subviews/simulation-timeline.tsx index 304cefa0a4b..2a23d7ba31f 100644 --- a/libs/@hashintel/petrinaut/src/views/Editor/panels/BottomPanel/subviews/simulation-timeline.tsx +++ b/libs/@hashintel/petrinaut/src/views/Editor/panels/BottomPanel/subviews/simulation-timeline.tsx @@ -640,7 +640,6 @@ const CompartmentTimeSeries: React.FC = ({ onTooltipChange, onPlaceHover, }) => { - "use no memo"; // Complex chart with manual memoization — compiler cannot preserve existing useMemo/useCallback patterns const { totalFrames } = use(SimulationContext); const { setCurrentViewedFrame } = use(PlaybackContext); @@ -658,174 +657,142 @@ const CompartmentTimeSeries: React.FC = ({ const activeHoveredPlaceId = localHoveredPlaceId ?? hoveredPlaceId; // Calculate chart dimensions and scales - const chartMetrics = useMemo(() => { - if (compartmentData.length === 0 || totalFrames === 0) { - return null; - } - - return { - totalFrames, - xScale: (frameIndex: number, width: number) => - (frameIndex / Math.max(1, totalFrames - 1)) * width, - yScale: (value: number, height: number) => - height - (value / yAxisScale.yMax) * height, - }; - }, [compartmentData, totalFrames, yAxisScale.yMax]); + const chartMetrics = + compartmentData.length === 0 || totalFrames === 0 + ? null + : { + totalFrames, + xScale: (frameIndex: number, width: number) => + (frameIndex / Math.max(1, totalFrames - 1)) * width, + yScale: (value: number, height: number) => + height - (value / yAxisScale.yMax) * height, + }; // Calculate frame index from mouse position - const getFrameFromEvent = useCallback( - (event: React.MouseEvent) => { - if (!chartRef.current || !chartMetrics) { - return null; - } + const getFrameFromEvent = (event: React.MouseEvent) => { + if (!chartRef.current || !chartMetrics) { + return null; + } - const rect = chartRef.current.getBoundingClientRect(); - const x = event.clientX - rect.left; - const width = rect.width; + const rect = chartRef.current.getBoundingClientRect(); + const x = event.clientX - rect.left; + const width = rect.width; - const progress = Math.max(0, Math.min(1, x / width)); - return Math.round(progress * (chartMetrics.totalFrames - 1)); - }, - [chartMetrics], - ); + const progress = Math.max(0, Math.min(1, x / width)); + return Math.round(progress * (chartMetrics.totalFrames - 1)); + }; // Handle mouse interaction for scrubbing - const handleScrub = useCallback( - (event: React.MouseEvent) => { - const frameIndex = getFrameFromEvent(event); - if (frameIndex !== null) { - setCurrentViewedFrame(frameIndex); - } - }, - [getFrameFromEvent, setCurrentViewedFrame], - ); + const handleScrub = (event: React.MouseEvent) => { + const frameIndex = getFrameFromEvent(event); + if (frameIndex !== null) { + setCurrentViewedFrame(frameIndex); + } + }; // Update tooltip based on mouse position and hovered place - const updateTooltip = useCallback( - (event: React.MouseEvent, hoveredId: string | null) => { - if (!hoveredId || frameTimes.length === 0) { - onTooltipChange(null); - return; - } + const updateTooltip = ( + event: React.MouseEvent, + hoveredId: string | null, + ) => { + if (!hoveredId || frameTimes.length === 0) { + onTooltipChange(null); + return; + } - const frameIndex = getFrameFromEvent(event); - if (frameIndex === null) { - onTooltipChange(null); - return; - } + const frameIndex = getFrameFromEvent(event); + if (frameIndex === null) { + onTooltipChange(null); + return; + } - const placeData = compartmentData.find( - (data) => data.placeId === hoveredId, - ); - if (!placeData || hiddenPlaces.has(hoveredId)) { - onTooltipChange(null); - return; - } + const placeData = compartmentData.find( + (data) => data.placeId === hoveredId, + ); + if (!placeData || hiddenPlaces.has(hoveredId)) { + onTooltipChange(null); + return; + } - const value = placeData.values[frameIndex] ?? 0; - const time = frameTimes[frameIndex] ?? 0; - - onTooltipChange({ - visible: true, - x: event.clientX, - y: event.clientY, - placeName: placeData.placeName, - color: placeData.color, - value, - frameIndex, - time, - }); - }, - [ - compartmentData, - hiddenPlaces, - frameTimes, - getFrameFromEvent, - onTooltipChange, - ], - ); + const value = placeData.values[frameIndex] ?? 0; + const time = frameTimes[frameIndex] ?? 0; + + onTooltipChange({ + visible: true, + x: event.clientX, + y: event.clientY, + placeName: placeData.placeName, + color: placeData.color, + value, + frameIndex, + time, + }); + }; /** * Extract placeId from an event target using event delegation. * Walks up the DOM to find the nearest element with data-place-id. */ - const getPlaceIdFromEvent = useCallback( - (event: React.MouseEvent): string | null => { - const target = event.target as SVGElement; - const placeGroup = target.closest("[data-place-id]"); - return placeGroup?.getAttribute("data-place-id") ?? null; - }, - [], - ); - - const handleMouseDown = useCallback( - (event: React.MouseEvent) => { - isDraggingRef.current = true; - handleScrub(event); - }, - [handleScrub], - ); + const getPlaceIdFromEvent = ( + event: React.MouseEvent, + ): string | null => { + const target = event.target as SVGElement; + const placeGroup = target.closest("[data-place-id]"); + return placeGroup?.getAttribute("data-place-id") ?? null; + }; + + const handleMouseDown = (event: React.MouseEvent) => { + isDraggingRef.current = true; + handleScrub(event); + }; /** * Event delegation handler for mouse movement. * Detects which place is being hovered by walking up the DOM tree. */ - const handleMouseMove = useCallback( - (event: React.MouseEvent) => { - if (isDraggingRef.current) { - handleScrub(event); - } + const handleMouseMove = (event: React.MouseEvent) => { + if (isDraggingRef.current) { + handleScrub(event); + } - // Event delegation: extract placeId from the event target - const placeId = getPlaceIdFromEvent(event); + // Event delegation: extract placeId from the event target + const placeId = getPlaceIdFromEvent(event); - // Only update state if hover target changed - if (placeId !== localHoveredPlaceId) { - setLocalHoveredPlaceId(placeId); - onPlaceHover(placeId); - } + // Only update state if hover target changed + if (placeId !== localHoveredPlaceId) { + setLocalHoveredPlaceId(placeId); + onPlaceHover(placeId); + } - // Update tooltip with current hover state - updateTooltip(event, placeId ?? hoveredPlaceId); - }, - [ - handleScrub, - getPlaceIdFromEvent, - localHoveredPlaceId, - onPlaceHover, - updateTooltip, - hoveredPlaceId, - ], - ); + // Update tooltip with current hover state + updateTooltip(event, placeId ?? hoveredPlaceId); + }; - const handleMouseUp = useCallback(() => { + const handleMouseUp = () => { isDraggingRef.current = false; - }, []); + }; - const handleMouseLeave = useCallback(() => { + const handleMouseLeave = () => { isDraggingRef.current = false; setLocalHoveredPlaceId(null); onPlaceHover(null); onTooltipChange(null); - }, [onPlaceHover, onTooltipChange]); + }; // Generate SVG path for a data series - const generatePath = useCallback( - (values: number[], width: number, height: number) => { - if (!chartMetrics || values.length === 0) { - return ""; - } + const generatePath = (values: number[], width: number, height: number) => { + if (!chartMetrics || values.length === 0) { + return ""; + } - const points = values.map((value, index) => { - const x = chartMetrics.xScale(index, width); - const y = chartMetrics.yScale(value, height); - return `${x},${y}`; - }); + const points = values.map((value, index) => { + const x = chartMetrics.xScale(index, width); + const y = chartMetrics.yScale(value, height); + return `${x},${y}`; + }); - return `M ${points.join(" L ")}`; - }, - [chartMetrics], - ); + return `M ${points.join(" L ")}`; + }; if (totalFrames === 0 || compartmentData.length === 0 || !chartMetrics) { return null; @@ -964,7 +931,6 @@ const StackedAreaChart: React.FC = ({ onTooltipChange, onPlaceHover, }) => { - "use no memo"; // Complex chart with manual memoization — compiler cannot preserve existing useMemo/useCallback patterns const { totalFrames } = use(SimulationContext); const { setCurrentViewedFrame } = use(PlaybackContext); @@ -982,18 +948,26 @@ const StackedAreaChart: React.FC = ({ const activeHoveredPlaceId = localHoveredPlaceId ?? hoveredPlaceId; // Filter visible compartment data - const visibleCompartmentData = useMemo(() => { - return compartmentData.filter((data) => !hiddenPlaces.has(data.placeId)); - }, [compartmentData, hiddenPlaces]); + const visibleCompartmentData = compartmentData.filter( + (data) => !hiddenPlaces.has(data.placeId), + ); // Calculate stacked values and chart metrics - const { stackedData, chartMetrics } = useMemo(() => { + const { stackedData, chartMetrics } = (() => { if (visibleCompartmentData.length === 0 || totalFrames === 0) { - return { stackedData: [], chartMetrics: null }; + return { + stackedData: [] as Array<{ + placeId: string; + placeName: string; + color: string; + baseValues: number[]; + topValues: number[]; + }>, + chartMetrics: null, + }; } // Calculate stacked values: for each frame, accumulate the values - // stackedData[i] contains { placeId, color, baseValues[], topValues[] } const stacked: Array<{ placeId: string; placeName: string; @@ -1032,182 +1006,153 @@ const StackedAreaChart: React.FC = ({ height - (value / yAxisScale.yMax) * height, }, }; - }, [visibleCompartmentData, totalFrames, yAxisScale.yMax]); + })(); // Calculate frame index from mouse position - const getFrameFromEvent = useCallback( - (event: React.MouseEvent) => { - if (!chartRef.current || !chartMetrics) { - return null; - } + const getFrameFromEvent = (event: React.MouseEvent) => { + if (!chartRef.current || !chartMetrics) { + return null; + } - const rect = chartRef.current.getBoundingClientRect(); - const x = event.clientX - rect.left; - const width = rect.width; + const rect = chartRef.current.getBoundingClientRect(); + const x = event.clientX - rect.left; + const width = rect.width; - const progress = Math.max(0, Math.min(1, x / width)); - return Math.round(progress * (chartMetrics.totalFrames - 1)); - }, - [chartMetrics], - ); + const progress = Math.max(0, Math.min(1, x / width)); + return Math.round(progress * (chartMetrics.totalFrames - 1)); + }; // Handle mouse interaction for scrubbing - const handleScrub = useCallback( - (event: React.MouseEvent) => { - const frameIndex = getFrameFromEvent(event); - if (frameIndex !== null) { - setCurrentViewedFrame(frameIndex); - } - }, - [getFrameFromEvent, setCurrentViewedFrame], - ); + const handleScrub = (event: React.MouseEvent) => { + const frameIndex = getFrameFromEvent(event); + if (frameIndex !== null) { + setCurrentViewedFrame(frameIndex); + } + }; // Update tooltip based on mouse position and hovered place - const updateTooltip = useCallback( - (event: React.MouseEvent, hoveredId: string | null) => { - if (!hoveredId || frameTimes.length === 0) { - onTooltipChange(null); - return; - } + const updateTooltip = ( + event: React.MouseEvent, + hoveredId: string | null, + ) => { + if (!hoveredId || frameTimes.length === 0) { + onTooltipChange(null); + return; + } - const frameIndex = getFrameFromEvent(event); - if (frameIndex === null) { - onTooltipChange(null); - return; - } + const frameIndex = getFrameFromEvent(event); + if (frameIndex === null) { + onTooltipChange(null); + return; + } - // For stacked chart, get the original (non-stacked) value - const placeData = compartmentData.find( - (data) => data.placeId === hoveredId, - ); - if (!placeData || hiddenPlaces.has(hoveredId)) { - onTooltipChange(null); - return; - } + // For stacked chart, get the original (non-stacked) value + const placeData = compartmentData.find( + (data) => data.placeId === hoveredId, + ); + if (!placeData || hiddenPlaces.has(hoveredId)) { + onTooltipChange(null); + return; + } - const value = placeData.values[frameIndex] ?? 0; - const time = frameTimes[frameIndex] ?? 0; - - onTooltipChange({ - visible: true, - x: event.clientX, - y: event.clientY, - placeName: placeData.placeName, - color: placeData.color, - value, - frameIndex, - time, - }); - }, - [ - compartmentData, - hiddenPlaces, - frameTimes, - getFrameFromEvent, - onTooltipChange, - ], - ); + const value = placeData.values[frameIndex] ?? 0; + const time = frameTimes[frameIndex] ?? 0; + + onTooltipChange({ + visible: true, + x: event.clientX, + y: event.clientY, + placeName: placeData.placeName, + color: placeData.color, + value, + frameIndex, + time, + }); + }; /** * Extract placeId from an event target using event delegation. * For stacked chart, paths have data-place-id directly on them. */ - const getPlaceIdFromEvent = useCallback( - (event: React.MouseEvent): string | null => { - const target = event.target as SVGElement; - // First check if the target itself has data-place-id (for path elements) - if (target.hasAttribute("data-place-id")) { - return target.getAttribute("data-place-id"); - } - // Fall back to walking up the DOM - const placeElement = target.closest("[data-place-id]"); - return placeElement?.getAttribute("data-place-id") ?? null; - }, - [], - ); + const getPlaceIdFromEvent = ( + event: React.MouseEvent, + ): string | null => { + const target = event.target as SVGElement; + // First check if the target itself has data-place-id (for path elements) + if (target.hasAttribute("data-place-id")) { + return target.getAttribute("data-place-id"); + } + // Fall back to walking up the DOM + const placeElement = target.closest("[data-place-id]"); + return placeElement?.getAttribute("data-place-id") ?? null; + }; - const handleMouseDown = useCallback( - (event: React.MouseEvent) => { - isDraggingRef.current = true; - handleScrub(event); - }, - [handleScrub], - ); + const handleMouseDown = (event: React.MouseEvent) => { + isDraggingRef.current = true; + handleScrub(event); + }; /** * Event delegation handler for mouse movement. * Detects which place is being hovered by checking data-place-id attributes. */ - const handleMouseMove = useCallback( - (event: React.MouseEvent) => { - if (isDraggingRef.current) { - handleScrub(event); - } + const handleMouseMove = (event: React.MouseEvent) => { + if (isDraggingRef.current) { + handleScrub(event); + } - // Event delegation: extract placeId from the event target - const placeId = getPlaceIdFromEvent(event); + // Event delegation: extract placeId from the event target + const placeId = getPlaceIdFromEvent(event); - // Only update state if hover target changed - if (placeId !== localHoveredPlaceId) { - setLocalHoveredPlaceId(placeId); - onPlaceHover(placeId); - } + // Only update state if hover target changed + if (placeId !== localHoveredPlaceId) { + setLocalHoveredPlaceId(placeId); + onPlaceHover(placeId); + } - // Update tooltip with current hover state - updateTooltip(event, placeId ?? hoveredPlaceId); - }, - [ - handleScrub, - getPlaceIdFromEvent, - localHoveredPlaceId, - onPlaceHover, - updateTooltip, - hoveredPlaceId, - ], - ); + // Update tooltip with current hover state + updateTooltip(event, placeId ?? hoveredPlaceId); + }; - const handleMouseUp = useCallback(() => { + const handleMouseUp = () => { isDraggingRef.current = false; - }, []); + }; - const handleMouseLeave = useCallback(() => { + const handleMouseLeave = () => { isDraggingRef.current = false; setLocalHoveredPlaceId(null); onPlaceHover(null); onTooltipChange(null); - }, [onPlaceHover, onTooltipChange]); + }; // Generate SVG path for a stacked area - const generateAreaPath = useCallback( - ( - baseValues: number[], - topValues: number[], - width: number, - height: number, - ) => { - if (!chartMetrics || topValues.length === 0) { - return ""; - } + const generateAreaPath = ( + baseValues: number[], + topValues: number[], + width: number, + height: number, + ) => { + if (!chartMetrics || topValues.length === 0) { + return ""; + } + + // Build the path: top line forward, then bottom line backward + const topPoints = topValues.map((value, index) => { + const x = chartMetrics.xScale(index, width); + const y = chartMetrics.yScale(value, height); + return `${x},${y}`; + }); - // Build the path: top line forward, then bottom line backward - const topPoints = topValues.map((value, index) => { + const basePoints = baseValues + .map((value, index) => { const x = chartMetrics.xScale(index, width); const y = chartMetrics.yScale(value, height); return `${x},${y}`; - }); + }) + .reverse(); - const basePoints = baseValues - .map((value, index) => { - const x = chartMetrics.xScale(index, width); - const y = chartMetrics.yScale(value, height); - return `${x},${y}`; - }) - .reverse(); - - return `M ${topPoints.join(" L ")} L ${basePoints.join(" L ")} Z`; - }, - [chartMetrics], - ); + return `M ${topPoints.join(" L ")} L ${basePoints.join(" L ")} Z`; + }; if (totalFrames === 0 || compartmentData.length === 0 || !chartMetrics) { return null;