From 129b41ffbe12b786e4871eec34b92e3bd66835e3 Mon Sep 17 00:00:00 2001 From: Doug Martin Date: Thu, 26 Sep 2024 09:23:56 -0400 Subject: [PATCH] feat: Highlight outputs [PT-188319491] This allows items in the the output list to be toggled and when toggled on the nodes and edges of that output are highlighted. --- src/components/app.scss | 11 +++++++ src/components/app.tsx | 34 ++++++++++++++++++++- src/components/dataset.tsx | 4 ++- src/components/drawing.tsx | 4 ++- src/components/graph.tsx | 60 ++++++++++++++++++++++++++++++++++---- 5 files changed, 104 insertions(+), 9 deletions(-) diff --git a/src/components/app.scss b/src/components/app.scss index d068979..f3946d3 100644 --- a/src/components/app.scss +++ b/src/components/app.scss @@ -217,6 +217,17 @@ } .sequences { padding: 5px; + + .sequence { + cursor: pointer; + + &.highlighted { + background-color: #FF00877f; + } + &.disabled { + cursor: default; + } + } } } } diff --git a/src/components/app.tsx b/src/components/app.tsx index 1a331db..c08383d 100644 --- a/src/components/app.tsx +++ b/src/components/app.tsx @@ -141,6 +141,7 @@ export const App = () => { const innerOutputRef = useRef(null); const [fastSimulation, setFastSimulation] = useState(defaultFastSimulation); const fastSimulationRef = useRef(false); + const [highlightOutput, setHighlightOutput] = useState<{group: SequenceGroup, sequence: Node[]}|undefined>(); const handleDimensionChange = ({width, height}: {width: number, height: number}) => { widthRef.current = width; @@ -153,6 +154,7 @@ export const App = () => { const setSelectedNodeId = useCallback((id?: string, skipToggle?: boolean) => { if (!animating) { + setHighlightOutput(undefined); if ((!id || (id === selectedNodeId)) && !skipToggle) { _setSelectedNodeId(undefined); } else { @@ -175,6 +177,10 @@ export const App = () => { const graphEmpty = useMemo(() => graph.nodes.length === 0, [graph]); + const highlightOutputNodes = useMemo(() => { + return animating ? undefined : highlightOutput?.sequence; + }, [animating, highlightOutput]); + const generateNewSequence = useCallback(async () => { currentSequence.current = []; currentSequenceIndex.current = 0; @@ -323,6 +329,7 @@ export const App = () => { }, [finishAnimating, startAnimationInterval]); const handleStep = useCallback(async () => { + setHighlightOutput(undefined); if ((generationMode !== "stepping") && (generationMode !== "paused")) { setGenerationMode("stepping"); await generateNewSequence(); @@ -335,6 +342,7 @@ export const App = () => { }, [generationMode, animateCurrentSequenceIndex, animateNextSequenceIndex, finishAnimating, generateNewSequence]); const handlePlay = useCallback(async () => { + setHighlightOutput(undefined); setGenerationMode("playing"); await generateNewSequence(); animateCurrentSequenceIndex(); @@ -350,6 +358,15 @@ export const App = () => { finishAnimating(true); }; + const toggleHighlightOutput = useCallback((group: SequenceGroup, sequence: Node[]) => { + setSelectedNodeId(); + if (!highlightOutput || (highlightOutput.group !== group) || (highlightOutput.sequence !== sequence)) { + setHighlightOutput({group, sequence}); + } else { + setHighlightOutput(undefined); + } + }, [highlightOutput, setHighlightOutput, setSelectedNodeId]); + const uiForGenerate = () => { const playLabel = generationMode === "playing" ? "Pause" : (generationMode === "paused" ? "Resume" : "Play"); const PlayOrPauseIcon = generationMode === "playing" ? PauseIcon : PlayIcon; @@ -431,7 +448,20 @@ export const App = () => {
- {group.sequences.map((s, j) =>
{s.map(n => n.label).join(group.delimiter)}
)} + {group.sequences.map((s, j) => ( +
toggleHighlightOutput(group, s)} + > + {s.map(n => n.label).join(group.delimiter)} +
+ ))}
); @@ -546,6 +576,7 @@ export const App = () => { highlightLoopOnNode={highlightLoopOnNode} highlightEdge={highlightEdge} highlightAllNextNodes={highlightAllNextNodes} + highlightOutputNodes={highlightOutputNodes} selectedNodeId={selectedNodeId} animating={animating} setGraph={setGraph} @@ -566,6 +597,7 @@ export const App = () => { highlightLoopOnNode={highlightLoopOnNode} highlightEdge={highlightEdge} highlightAllNextNodes={highlightAllNextNodes} + highlightOutputNodes={highlightOutputNodes} selectedNodeId={selectedNodeId} animating={animating} graphEmpty={graphEmpty} diff --git a/src/components/dataset.tsx b/src/components/dataset.tsx index c040261..b7b6065 100644 --- a/src/components/dataset.tsx +++ b/src/components/dataset.tsx @@ -10,6 +10,7 @@ interface Props { highlightLoopOnNode?: Node, highlightEdge?: Edge, highlightAllNextNodes: boolean; + highlightOutputNodes?: Node[]; graph: GraphData; selectedNodeId?: string; animating: boolean; @@ -25,7 +26,7 @@ interface Props { } export const Dataset = (props: Props) => { - const {highlightNode, highlightLoopOnNode, highlightEdge, highlightAllNextNodes, + const {highlightNode, highlightLoopOnNode, highlightEdge, highlightAllNextNodes, highlightOutputNodes, graph, graphEmpty, setSelectedNodeId, selectedNodeId, animating, fitViewAt, recenterViewAt, onReset, onReturnToMainMenu, onFitView, onRecenterView} = props; @@ -79,6 +80,7 @@ export const Dataset = (props: Props) => { highlightLoopOnNode={highlightLoopOnNode} highlightEdge={highlightEdge} highlightAllNextNodes={highlightAllNextNodes} + highlightOutputNodes={highlightOutputNodes} selectedNodeId={selectedNodeId} animating={animating} allowDragging={true && !animating} diff --git a/src/components/drawing.tsx b/src/components/drawing.tsx index c831ed3..4dc8f76 100644 --- a/src/components/drawing.tsx +++ b/src/components/drawing.tsx @@ -19,6 +19,7 @@ interface Props { highlightLoopOnNode?: Node, highlightEdge?: Edge, highlightAllNextNodes: boolean; + highlightOutputNodes?: Node[]; graph: GraphData; selectedNodeId?: string; animating: boolean; @@ -38,7 +39,7 @@ const keepPunctuationRegex = /[.,?!:;]/g; const removePunctuationRegex = /["(){}[\]_+=|\\/><]/g; export const Drawing = (props: Props) => { - const {highlightNode, highlightLoopOnNode, highlightEdge, highlightAllNextNodes, + const {highlightNode, highlightLoopOnNode, highlightEdge, highlightAllNextNodes, highlightOutputNodes, graph, setGraph, setHighlightNode, setSelectedNodeId: _setSelectedNodeId, fitViewAt, recenterViewAt, selectedNodeId, animating, onReset, onReturnToMainMenu, onFitView, onRecenterView} = props; @@ -314,6 +315,7 @@ export const Drawing = (props: Props) => { highlightNode={highlightNode} highlightEdge={highlightEdge} highlightAllNextNodes={highlightAllNextNodes} + highlightOutputNodes={highlightOutputNodes} highlightLoopOnNode={highlightLoopOnNode} allowDragging={drawingMode === "select"} autoArrange={autoArrange} diff --git a/src/components/graph.tsx b/src/components/graph.tsx index 57688e8..2b2e882 100644 --- a/src/components/graph.tsx +++ b/src/components/graph.tsx @@ -52,6 +52,7 @@ type Props = { highlightLoopOnNode?: Node, highlightEdge?: Edge, highlightAllNextNodes: boolean; + highlightOutputNodes?: Node[]; allowDragging: boolean; autoArrange: boolean; rubberBand?: RubberBand; @@ -242,7 +243,7 @@ const calculateNodeFontSize = (d: D3Node) => { const lineDashArray = (edge: D3Edge) => edge.value ? "" : "4"; export const Graph = (props: Props) => { - const {graph, highlightNode, highlightLoopOnNode, highlightEdge, highlightAllNextNodes, + const {graph, highlightNode, highlightLoopOnNode, highlightEdge, highlightAllNextNodes, highlightOutputNodes, allowDragging, autoArrange, rubberBand, drawingMode, onClick, onNodeClick, onNodeDoubleClick, onEdgeClick, onDragStop, fitViewAt, recenterViewAt, @@ -327,6 +328,47 @@ export const Graph = (props: Props) => { }, [selectedNodeId, animating, graph]); + const unhighlightNonOutput = useCallback((svg: d3.Selection) => { + if (animating) { + return; + } + + const nodeIds = highlightOutputNodes?.map(n => n.id) ?? []; + const haveNodes = nodeIds.length > 0; + + // unhighlight non-highlighted nodes + svg + .selectAll("g.node") + .selectAll("ellipse") + .style("opacity", 1) + .filter((d: any) => haveNodes && !nodeIds.includes(d.id)) + .style("opacity", unselectedOpacity); + + // unhighlight non-highlighted edges + svg + .selectAll("line") + .style("opacity", 1) + .filter((d: any) => (( + (d.value > 0) && haveNodes && (!(nodeIds.includes(d.source?.id) && nodeIds.includes(d.target?.id)))))) + .style("opacity", unselectedOpacity); + + // unhighlight loops + svg + .selectAll("path.loop") + .style("opacity", 1) + .filter((d: any) => haveNodes && !nodeIds.includes(d.id)) + .style("opacity", unselectedOpacity); + + // unhighlight text + svg + .selectAll("g.node") + .selectAll("text") + .style("opacity", 1) + .filter((d: any) => haveNodes && !nodeIds.includes(d.id)) + .style("opacity", unselectedOpacity); + + }, [highlightOutputNodes, animating]); + // calculate the svg dimensions useEffect(() => { if (dimensions) { @@ -769,11 +811,11 @@ export const Graph = (props: Props) => { .selectAll("ellipse") .attr("fill", "#fff"); - // highlight animated node + // highlight animated node and output nodes root .selectAll("g.node") .selectAll("ellipse") - .filter((d: any) => highlightNode?.id === d.id) + .filter((d: any) => (highlightNode?.id === d.id) || !!highlightOutputNodes?.find(n => n.id === d.id)) .attr("fill", animatedNodeColor); // highlight animated edges @@ -784,8 +826,12 @@ export const Graph = (props: Props) => { .attr("marker-end", arrowUrl) .filter((d: any) => (( (d.value > 0) && ( - (highlightNode?.id === d.source?.id && highlightAllNextNodes) || - (highlightEdge?.from === d.source?.id && highlightEdge?.to === d.target?.id))))) + (highlightNode?.id === d.source?.id && highlightAllNextNodes) || + (highlightEdge?.from === d.source?.id && highlightEdge?.to === d.target?.id) || + (!!highlightOutputNodes?.find(n => n.id === d.source?.id) + && !!highlightOutputNodes?.find(n => n.id === d.target?.id)) + ) + ))) .attr("stroke", animatedArrowColor) .attr("stroke-dasharray", highlightAllNextNodes ? "4" : "") .attr("marker-end", animatedArrowUrl); @@ -800,9 +846,11 @@ export const Graph = (props: Props) => { .attr("stroke-dasharray", highlightAllNextNodes ? "4" : "") .attr("marker-end", animatedArrowUrl); + unhighlightNonOutput(root); highlightSelected(root); + }, [svgRef, d3Graph.nodes, selectedNodeId, highlightNode, highlightLoopOnNode, - highlightEdge, highlightAllNextNodes, highlightSelected]); + highlightEdge, highlightAllNextNodes, highlightSelected, highlightOutputNodes, unhighlightNonOutput]); const fitOrCenter = useCallback((op: "fit" | "center") => { if (!width || !height || !zoomRef.current) {