Skip to content

Commit 1ad47e1

Browse files
Support transitioning between old e-graphs
1 parent 1cef5e4 commit 1ad47e1

File tree

4 files changed

+121
-84
lines changed

4 files changed

+121
-84
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "egraph-visualizer",
3-
"version": "1.4.2",
3+
"version": "2.0.0",
44
"repository": {
55
"type": "git",
66
"url": "git+https://github.com/saulshanabrook/egraph-visualizer.git"

src/App.tsx

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { useState } from "react";
1+
import { useCallback, useState } from "react";
22
import Monaco from "./Monaco";
33
import { Visualizer } from "./Visualizer";
44
import { defaultCode, defaultExample, fetchExample } from "./examples";
5-
import { useQuery } from "@tanstack/react-query";
5+
import { keepPreviousData, useQuery } from "@tanstack/react-query";
66

77
function App() {
88
const [example, setExample] = useState<string>(defaultExample);
@@ -12,18 +12,43 @@ function App() {
1212
staleTime: Infinity,
1313
retry: false,
1414
retryOnMount: false,
15+
placeholderData: keepPreviousData,
1516
});
16-
const [modifiedCode, setModifiedCode] = useState<string | null>(null);
17-
const currentCode = modifiedCode ?? exampleQuery.data;
17+
const [modifications, setModifications] = useState<{ initial: string; updates: string[] }>({ initial: defaultCode, updates: [] });
18+
19+
const data = exampleQuery.data || defaultExample;
20+
const addModification = useCallback(
21+
(change: string) => {
22+
const updates = modifications.initial === data ? modifications.updates : [];
23+
24+
setModifications({
25+
initial: data,
26+
updates: [...updates, change],
27+
});
28+
},
29+
[data, modifications.initial, modifications.updates]
30+
);
31+
const egraphs = [data];
32+
const modificationsUpToDate = modifications.initial === exampleQuery.data;
33+
if (modificationsUpToDate) {
34+
egraphs.push(...modifications.updates);
35+
}
36+
//
1837
return (
1938
<>
2039
<div className="flex min-h-screen">
2140
<div className="flex w-1/3 resize-x overflow-auto">
22-
<Monaco setModifiedCode={setModifiedCode} exampleQuery={exampleQuery} example={example} setExample={setExample} />
41+
<Monaco
42+
addModification={addModification}
43+
initialCode={egraphs[0]}
44+
exampleQuery={exampleQuery}
45+
example={example}
46+
setExample={setExample}
47+
/>
2348
</div>
2449

2550
<div className="flex w-2/3">
26-
<Visualizer egraph={currentCode || defaultCode} />
51+
<Visualizer egraphs={egraphs} />
2752
</div>
2853
</div>
2954
<footer className="p-2 fixed bottom-0 min-w-full text-xs text-gray-500 text-right dark:text-gray-400">

src/Monaco.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,33 @@ import { Loading } from "./Loading";
99

1010
function Monaco({
1111
exampleQuery,
12-
setModifiedCode,
12+
addModification,
13+
initialCode,
1314
example,
1415
setExample,
1516
}: {
1617
exampleQuery: UseQueryResult<string, Error>;
17-
setModifiedCode: (code: string | null) => void;
18+
initialCode: string;
19+
addModification: (code: string) => void;
1820
example: string;
21+
1922
setExample: (example: string) => void;
2023
}) {
24+
// locally modified code that is only saved when the user clicks "Update"
2125
const [code, setCode] = useState<string | null>(null);
2226
const handlePresetChange = useCallback(
2327
(preset: Key) => {
2428
setExample(preset as string);
2529
setCode(null);
26-
setModifiedCode(null);
2730
},
28-
[setExample, setModifiedCode]
31+
[setExample, setCode]
2932
);
3033

31-
const currentValue = code || exampleQuery.data;
34+
const currentValue = code || initialCode;
3235

3336
const handleUpdate = useCallback(() => {
34-
setModifiedCode(currentValue || null);
35-
}, [currentValue, setModifiedCode]);
37+
addModification(currentValue);
38+
}, [currentValue, addModification]);
3639

3740
return (
3841
<div className="flex flex-col h-full w-full">

src/Visualizer.tsx

Lines changed: 79 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { keepPreviousData, QueryClientProvider, useQuery } from "@tanstack/react
4949
import { FlowClass, FlowEdge, FlowNode, layoutGraph, PreviousLayout, SelectedNode } from "./layout";
5050
import { queryClient } from "./queryClient";
5151
import { Loading } from "./Loading";
52+
import { Slider, SliderOutput, SliderTack } from "./react-aria-components-tailwind-starter/src/slider";
5253

5354
export function EClassNode({ data, selected }: NodeProps<FlowClass>) {
5455
return (
@@ -392,114 +393,122 @@ function LayoutFlow({
392393
<>
393394
{layoutQuery.isFetching ? <Loading /> : <></>}
394395
<SetSelectedNodeContext.Provider value={setSelectedNode}>
395-
<Rendering
396-
nodes={nodes}
397-
edges={edges}
398-
nodeToEdges={nodeToEdges}
399-
edgeToNodes={edgeToNodes}
400-
selectedNode={selectedNode}
401-
elkJSON={elkJSON}
402-
useInteractiveLayout={useInteractiveLayout}
403-
setUseInteractiveLayout={setUseInteractiveLayout}
404-
mergeEdges={mergeEdges}
405-
setMergeEdges={setMergeEdges}
406-
/>
396+
<ReactFlowProvider>
397+
<Rendering
398+
nodes={nodes}
399+
edges={edges}
400+
nodeToEdges={nodeToEdges}
401+
edgeToNodes={edgeToNodes}
402+
selectedNode={selectedNode}
403+
elkJSON={elkJSON}
404+
useInteractiveLayout={useInteractiveLayout}
405+
setUseInteractiveLayout={setUseInteractiveLayout}
406+
mergeEdges={mergeEdges}
407+
setMergeEdges={setMergeEdges}
408+
/>
409+
</ReactFlowProvider>
407410
</SetSelectedNodeContext.Provider>
408411
</>
409412
);
410413
}
411414

412-
export function Visualizer({ egraph, height = null, resize = false }: { egraph: string; height?: string | null; resize?: boolean }) {
413-
const [outerElem, setOuterElem] = useState<HTMLDivElement | null>(null);
414-
const [innerElem, setInnerElem] = useState<HTMLDivElement | null>(null);
415+
function SelectSider({ length, onSelect, selected }: { length: number; onSelect: (index: number) => void; selected: number }) {
416+
return (
417+
<div className={`absolute top-0 left-0 p-4 z-50 backdrop-blur-sm ${length > 1 ? "" : "opacity-0"}`}>
418+
<Slider
419+
minValue={0}
420+
maxValue={length - 1}
421+
onChange={onSelect}
422+
value={selected}
423+
aria-label="Select which egraph to display from the history"
424+
>
425+
<div className="flex flex-1 items-end">
426+
<div className="flex flex-1 flex-col">
427+
<SliderOutput className="self-center">
428+
{({ state }) => {
429+
return (
430+
<span className="text-sm">
431+
{state.getThumbValueLabel(0)} / {length - 1}
432+
</span>
433+
);
434+
}}
435+
</SliderOutput>
436+
<div className="flex flex-1 items-center gap-3">
437+
<SliderTack thumbLabels={["volume"]} />
438+
</div>
439+
</div>
440+
</div>
441+
</Slider>
442+
</div>
443+
);
444+
}
415445

446+
export function Visualizer({ egraphs, height = null, resize = false }: { egraphs: string[]; height?: string | null; resize?: boolean }) {
416447
const [rootElem, setRootElem] = useState<HTMLDivElement | null>(null);
417448

418-
const aspectRatio = useMemo(() => {
419-
if (rootElem) {
420-
return rootElem.clientWidth / rootElem.clientHeight;
421-
}
422-
}, [rootElem]);
449+
const [outerElem, setOuterElem] = useState<HTMLDivElement | null>(null);
450+
const [innerElem, setInnerElem] = useState<HTMLDivElement | null>(null);
451+
const aspectRatio = rootElem ? rootElem.clientWidth / rootElem.clientHeight : null;
452+
453+
// If we are at null, then use the last item in the list
454+
// if the last selection was for a list of egraphs that no longer exists, then use the last item in the list
455+
const [selected, setSelected] = useState<null | { egraphs: string[]; index: number }>(null);
456+
const actualSelected = selected && selected.egraphs === egraphs ? selected.index : egraphs.length - 1;
457+
const onSelect = useCallback(
458+
(index: number) => {
459+
setSelected({ egraphs, index });
460+
},
461+
[setSelected, egraphs]
462+
);
463+
423464
return (
424465
<div className={`w-full relative ${resize ? "resize-y" : ""}`} style={{ height: height || "100%" }} ref={setRootElem}>
425466
{/* Hidden node to measure text size */}
426467
<div className="invisible absolute">
427468
<ENode outerRef={setOuterElem} innerRef={setInnerElem} />
428469
</div>
429-
<ReactFlowProvider>
430-
{outerElem && innerElem && aspectRatio && (
431-
<LayoutFlow aspectRatio={aspectRatio} egraph={egraph} outerElem={outerElem} innerElem={innerElem} />
432-
)}
433-
</ReactFlowProvider>
470+
<SelectSider length={egraphs.length} onSelect={onSelect} selected={actualSelected} />
471+
{outerElem && innerElem && aspectRatio && (
472+
<LayoutFlow aspectRatio={aspectRatio} egraph={egraphs[actualSelected]} outerElem={outerElem} innerElem={innerElem} />
473+
)}
434474
</div>
435475
);
436476
}
437477

438478
// Put these both in one file, so its emitted as a single chunk and anywidget doesn't have to import another file
439479

440-
function VisualizerWithTransition({
441-
initialEgraph,
442-
registerChangeEGraph,
443-
resize,
444-
height,
445-
}: {
446-
initialEgraph: string;
447-
registerChangeEGraph: (setEgraph: (egraph: string) => void) => void;
448-
resize?: boolean;
449-
height?: string;
450-
}) {
451-
const [egraph, setEgraph] = useState(initialEgraph);
452-
useEffect(() => {
453-
registerChangeEGraph(setEgraph);
454-
}, [registerChangeEGraph, setEgraph]);
455-
return (
456-
<QueryClientProvider client={queryClient}>
457-
<Visualizer egraph={egraph} height={height} resize={resize} />
458-
</QueryClientProvider>
459-
);
460-
}
461-
462480
/// Render anywidget model to the given element
463481
// Must be named `render` to work as an anywidget module
464482
// https://anywidget.dev/en/afm/#lifecycle-methods
465483
// eslint-disable-next-line react-refresh/only-export-components
466484
export function render({ model, el }: { el: HTMLElement; model: AnyModel }) {
485+
// only render once with data, dont support updating widget yet
467486
const root = createRoot(el);
468-
let callback: () => void;
469-
const registerChangeEGraph = (setEgraph: (egraph: string) => void) => {
470-
callback = () => setEgraph(model.get("egraph"));
471-
model.on("change:egraph", callback);
472-
};
487+
// let callback: () => void;
488+
// const registerChangeEGraph = (setEgraph: (egraph: string) => void) => {
489+
// callback = () => setEgraph(model.get("egraph"));
490+
// model.on("change:egraph", callback);
491+
// };
473492
root.render(
474-
<VisualizerWithTransition initialEgraph={model.get("egraph")} registerChangeEGraph={registerChangeEGraph} height="600px" resize />
493+
<QueryClientProvider client={queryClient}>
494+
<Visualizer egraphs={model.get("egraphs")} height="600px" resize />
495+
</QueryClientProvider>
475496
);
476497

477498
return () => {
478-
model.off("change:egraph", callback);
499+
// model.off("change:egraph", callback);
479500
root.unmount();
480501
};
481502
}
482503

483504
/// Mount the visualizer to the given element
484-
/// Call `render` to render a new egraph
505+
/// Call `render` to render a new list of egraphs
485506
/// Call `unmount` to unmount the visualizer
486507
// eslint-disable-next-line react-refresh/only-export-components
487-
export function mount(element: HTMLElement): { render: (egraph: string) => void; unmount: () => void } {
508+
export function mount(element: HTMLElement): { render: (egraphs: string[]) => void; unmount: () => void } {
488509
const root = createRoot(element);
489-
let setEgraph: null | ((egraph: string) => void) = null;
490-
function render(egraph: string) {
491-
if (setEgraph) {
492-
setEgraph(egraph);
493-
} else {
494-
root.render(
495-
<VisualizerWithTransition
496-
initialEgraph={egraph}
497-
registerChangeEGraph={(setEgraph_) => {
498-
setEgraph = setEgraph_;
499-
}}
500-
/>
501-
);
502-
}
510+
function render(egraphs: string[]) {
511+
root.render(<Visualizer egraphs={egraphs} />);
503512
}
504513

505514
function unmount() {

0 commit comments

Comments
 (0)