From 124c97495df958569db3201e40256b780c44dc8f Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 5 Dec 2024 19:18:25 -0800 Subject: [PATCH] refactor: Moved tooltip rendering logic into a tooltip (cherry picked from commit c8e5170ddcf4ade4bc6374a6a67db253ec551e77) --- src/Viewer.tsx | 83 ++-------------- .../Tooltips/CanvasHoverTooltip.tsx | 98 +++++++++++++++++++ .../{ => Tooltips}/HoverTooltip.tsx | 0 3 files changed, 108 insertions(+), 73 deletions(-) create mode 100644 src/components/Tooltips/CanvasHoverTooltip.tsx rename src/components/{ => Tooltips}/HoverTooltip.tsx (100%) diff --git a/src/Viewer.tsx b/src/Viewer.tsx index 17b04ad4a..c02ac416f 100644 --- a/src/Viewer.tsx +++ b/src/Viewer.tsx @@ -28,13 +28,10 @@ import { ScatterPlotConfig, TabType, Track, - VECTOR_KEY_MOTION_DELTA, - VectorTooltipMode, ViewerConfig, } from "./colorizer"; import { AnalyticsEvent, triggerAnalyticsEvent } from "./colorizer/utils/analytics"; import { getColorMap, getInRangeLUT, thresholdMatchFinder, validateThresholds } from "./colorizer/utils/data_utils"; -import { numberToStringDecimal } from "./colorizer/utils/math_utils"; import { useConstructor, useDebounce, useMotionDeltas, useRecentCollections } from "./colorizer/utils/react_utils"; import * as urlUtils from "./colorizer/utils/url_utils"; import { SCATTERPLOT_TIME_FEATURE } from "./components/Tabs/scatter_plot_data_utils"; @@ -60,7 +57,6 @@ import SelectionDropdown from "./components/Dropdowns/SelectionDropdown"; import Export from "./components/Export"; import GlossaryPanel from "./components/GlossaryPanel"; import Header from "./components/Header"; -import HoverTooltip from "./components/HoverTooltip"; import IconButton from "./components/IconButton"; import LabeledSlider from "./components/LabeledSlider"; import LoadDatasetButton from "./components/LoadDatasetButton"; @@ -68,6 +64,7 @@ import SmallScreenWarning from "./components/Modals/SmallScreenWarning"; import PlaybackSpeedControl from "./components/PlaybackSpeedControl"; import SpinBox from "./components/SpinBox"; import { FeatureThresholdsTab, PlotTab, ScatterPlotTab, SettingsTab } from "./components/Tabs"; +import CanvasHoverTooltip from "./components/Tooltips/CanvasHoverTooltip"; // TODO: Refactor with styled-components import styles from "./Viewer.module.css"; @@ -740,24 +737,6 @@ function Viewer(): ReactElement { [replaceDataset] ); - const getFeatureValue = useCallback( - (id: number): string => { - if (!featureKey || !dataset) { - return ""; - } - // Look up feature value from id - const featureData = dataset.getFeatureData(featureKey); - // ?? is a nullish coalescing operator; it checks for null + undefined values - // (safe for falsy values like 0 or NaN, which are valid feature values) - let featureValue = featureData?.data[id] ?? -1; - featureValue = isFinite(featureValue) ? featureValue : NaN; - const unitsLabel = featureData?.unit ? ` ${featureData?.unit}` : ""; - // Check if int, otherwise return float - return numberToStringDecimal(featureValue, 3) + unitsLabel; - }, - [featureKey, dataset] - ); - // SCRUBBING CONTROLS //////////////////////////////////////////////////// timeControls.setFrameCallback(setFrame); @@ -864,55 +843,6 @@ function Viewer(): ReactElement { return [threshold.min, threshold.max]; }; - let hoveredFeatureValue = ""; - if (lastHoveredId !== null && dataset) { - const featureVal = getFeatureValue(lastHoveredId); - const categories = dataset.getFeatureCategories(featureKey); - if (categories !== null) { - hoveredFeatureValue = categories[Number.parseInt(featureVal, 10)]; - } else { - hoveredFeatureValue = featureVal; - } - } - - const getVectorTooltipText = (): string | null => { - if (!config.vectorConfig.visible || lastHoveredId === null || !motionDeltas) { - return null; - } - const motionDelta = [motionDeltas[2 * lastHoveredId], motionDeltas[2 * lastHoveredId + 1]]; - - if (Number.isNaN(motionDelta[0]) || Number.isNaN(motionDelta[1])) { - return null; - } - - const vectorKey = config.vectorConfig.key; - const vectorName = vectorKey === VECTOR_KEY_MOTION_DELTA ? "Avg. motion delta" : vectorKey; - if (config.vectorConfig.tooltipMode === VectorTooltipMode.MAGNITUDE) { - const magnitude = Math.sqrt(motionDelta[0] ** 2 + motionDelta[1] ** 2); - const angleDegrees = (360 + Math.atan2(-motionDelta[1], motionDelta[0]) * (180 / Math.PI)) % 360; - const magnitudeText = numberToStringDecimal(magnitude, 3); - const angleText = numberToStringDecimal(angleDegrees, 1); - return `${vectorName}: ${magnitudeText} px, ${angleText}°`; - } else { - const allowIntegerTruncation = Number.isInteger(motionDelta[0]) && Number.isInteger(motionDelta[1]); - const x = numberToStringDecimal(motionDelta[0], 3, allowIntegerTruncation); - const y = numberToStringDecimal(motionDelta[1], 3, allowIntegerTruncation); - return `${vectorName}: (${x}, ${y}) px - `; - } - }; - - // TODO: Move to a separate component? - const vectorTooltipText = getVectorTooltipText(); - const hoverTooltipContent = [ -

Track ID: {lastHoveredId && dataset?.getTrackId(lastHoveredId)}

, -

- {dataset?.getFeatureName(featureKey) || "Feature"}:{" "} - {hoveredFeatureValue} -

, - vectorTooltipText ?

{vectorTooltipText}

: null, - ]; - return (
{notificationContextHolder}
@@ -1063,7 +993,14 @@ function Viewer(): ReactElement {
- + setShowHoveredId(false)} showAlert={isInitialDatasetLoaded ? showAlert : undefined} /> - + {/** Time Control Bar */} diff --git a/src/components/Tooltips/CanvasHoverTooltip.tsx b/src/components/Tooltips/CanvasHoverTooltip.tsx new file mode 100644 index 000000000..5646ba11f --- /dev/null +++ b/src/components/Tooltips/CanvasHoverTooltip.tsx @@ -0,0 +1,98 @@ +import React, { PropsWithChildren, ReactElement, useCallback } from "react"; + +import { Dataset, VECTOR_KEY_MOTION_DELTA, VectorTooltipMode, ViewerConfig } from "../../colorizer"; +import { numberToStringDecimal } from "../../colorizer/utils/math_utils"; + +import HoverTooltip from "./HoverTooltip"; + +type CanvasHoverTooltipProps = { + dataset: Dataset | null; + featureKey: string; + lastHoveredId: number | null; + showHoveredId: boolean; + motionDeltas: Float32Array | null; + config: ViewerConfig; +}; + +/** + * Sets up and configures the hover tooltip for the main viewport canvas. + * By default, displays the track ID and the value of the feature at the hovered point. + * + * Additional data will be displayed depending on the current viewer configuration: + * - If vectors are enabled, the vector value (either magnitude or components) will be displayed. + */ +export default function CanvasHoverTooltip(props: PropsWithChildren): ReactElement { + const { dataset, featureKey, lastHoveredId, motionDeltas, config } = props; + + const getFeatureValue = useCallback( + (id: number): string => { + if (!featureKey || !dataset) { + return ""; + } + // Look up feature value from id + const featureData = dataset.getFeatureData(featureKey); + // ?? is a nullish coalescing operator; it checks for null + undefined values + // (safe for falsy values like 0 or NaN, which are valid feature values) + let featureValue = featureData?.data[id] ?? -1; + featureValue = isFinite(featureValue) ? featureValue : NaN; + const unitsLabel = featureData?.unit ? ` ${featureData?.unit}` : ""; + // Check if int, otherwise return float + return numberToStringDecimal(featureValue, 3) + unitsLabel; + }, + [featureKey, dataset] + ); + + let hoveredFeatureValue = ""; + if (lastHoveredId !== null && dataset) { + const featureVal = getFeatureValue(lastHoveredId); + const categories = dataset.getFeatureCategories(featureKey); + if (categories !== null) { + hoveredFeatureValue = categories[Number.parseInt(featureVal, 10)]; + } else { + hoveredFeatureValue = featureVal; + } + } + + const getVectorTooltipText = (): string | null => { + if (!config.vectorConfig.visible || lastHoveredId === null || !motionDeltas) { + return null; + } + const motionDelta = [motionDeltas[2 * lastHoveredId], motionDeltas[2 * lastHoveredId + 1]]; + + if (Number.isNaN(motionDelta[0]) || Number.isNaN(motionDelta[1])) { + return null; + } + + const vectorKey = config.vectorConfig.key; + const vectorName = vectorKey === VECTOR_KEY_MOTION_DELTA ? "Avg. motion delta" : vectorKey; + if (config.vectorConfig.tooltipMode === VectorTooltipMode.MAGNITUDE) { + const magnitude = Math.sqrt(motionDelta[0] ** 2 + motionDelta[1] ** 2); + const angleDegrees = (360 + Math.atan2(-motionDelta[1], motionDelta[0]) * (180 / Math.PI)) % 360; + const magnitudeText = numberToStringDecimal(magnitude, 3); + const angleText = numberToStringDecimal(angleDegrees, 1); + return `${vectorName}: ${magnitudeText} px, ${angleText}°`; + } else { + const allowIntegerTruncation = Number.isInteger(motionDelta[0]) && Number.isInteger(motionDelta[1]); + const x = numberToStringDecimal(motionDelta[0], 3, allowIntegerTruncation); + const y = numberToStringDecimal(motionDelta[1], 3, allowIntegerTruncation); + return `${vectorName}: (${x}, ${y}) px + `; + } + }; + + const vectorTooltipText = getVectorTooltipText(); + const hoverTooltipContent = [ +

Track ID: {lastHoveredId && dataset?.getTrackId(lastHoveredId)}

, +

+ {dataset?.getFeatureName(featureKey) || "Feature"}:{" "} + {hoveredFeatureValue} +

, + vectorTooltipText ?

{vectorTooltipText}

: null, + ]; + + return ( + + {props.children} + + ); +} diff --git a/src/components/HoverTooltip.tsx b/src/components/Tooltips/HoverTooltip.tsx similarity index 100% rename from src/components/HoverTooltip.tsx rename to src/components/Tooltips/HoverTooltip.tsx