Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"scripts": {
"lint": "eslint . --fix",
"lint-test": "eslint .",
"pretest": "pnpm --filter @tableau/query-graphs build",
"test": "node --import=tsx --test --test-isolation=none \"query-graphs/test/*.test.ts\" \"standalone-app/test/*.test.ts\"",
"pretest:update": "pnpm --filter @tableau/query-graphs build",
"test:update": "node --import=tsx --test --test-isolation=none --test-update-snapshots \"query-graphs/test/*.test.ts\" \"standalone-app/test/*.test.ts\""
},
"devDependencies": {
Expand Down
62 changes: 62 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 19 additions & 8 deletions query-graphs/src/ui/QueryGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import "@xyflow/react/dist/style.css";

import type {TreeDescription, TreeNode} from "../tree-description";
import type {MouseEvent, ReactNode} from "react";
import {useId, useMemo} from "react";
import {useMemo} from "react";
import cc from "classcat";
import {QueryNode} from "./QueryNode";
import type {QueryGraphNode} from "./QueryNode";
Expand All @@ -13,14 +13,20 @@ import {createGraphRenderingStore, GraphRenderingStoreContext, useGraphRendering
import {AnimateGraphChangeContext, useAnimatedGraphLayout, useAnimateGraphChange} from "./useAnimatedGraphLayout";
import {indexGraph} from "./graph-index";
import type {TreeParents} from "./tree-topology";
import {SettingsButton, SettingsPanel} from "./SettingsPanel";
import type {AnimationSpeed} from "./animation-timing";
import "./QueryGraph.css";

interface QueryGraphProps {
export interface QueryGraphProps {
treeDescription: TreeDescription;
children: ReactNode | ReactNode[];
/** Selects the duration of graph layout transitions. Defaults to medium. */
animationSpeed?: AnimationSpeed;
/** Additional buttons, normally `QueryGraphControlButton`s, appended to the built-in graph controls. */
additionalControls?: ReactNode;
}

export const QueryGraphControlButton = ControlButton;

interface QueryGraphInternalProps extends QueryGraphProps {
nodeIdMapping: ReadonlyMap<TreeNode, string>;
treeParents: TreeParents;
Expand Down Expand Up @@ -83,10 +89,16 @@ function ExpandedNodesControl() {
);
}

function QueryGraphInternal({treeDescription, children, nodeIdMapping, treeParents}: QueryGraphInternalProps) {
const settingsPopoverId = useId();
function QueryGraphInternal({
treeDescription,
children,
animationSpeed = "medium",
additionalControls,
nodeIdMapping,
treeParents,
}: QueryGraphInternalProps) {
const expandedSubtrees = useGraphRenderingStore((s) => s.expandedSubtrees);
const animatedLayout = useAnimatedGraphLayout(treeDescription, nodeIdMapping, treeParents, expandedSubtrees);
const animatedLayout = useAnimatedGraphLayout(treeDescription, nodeIdMapping, treeParents, expandedSubtrees, animationSpeed);
// Hide the full tree initially to avoid flickering. We use `opacity` instead
// of `visibility: hidden` because React Flow overrides inherited
// visibility on nodes after measuring them.
Expand Down Expand Up @@ -116,9 +128,8 @@ function QueryGraphInternal({treeDescription, children, nodeIdMapping, treeParen
<MiniMap zoomable={true} pannable={true} nodeColor={minimapNodeColor} nodeComponent={QueryGraphMiniMapNode} />
<Controls showInteractive={false}>
<ExpandedNodesControl />
<SettingsButton popoverId={settingsPopoverId} />
{additionalControls}
</Controls>
<SettingsPanel popoverId={settingsPopoverId} />
</ReactFlow>
</AnimateGraphChangeContext.Provider>
);
Expand Down
14 changes: 11 additions & 3 deletions query-graphs/src/ui/animation-timing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import {useSyncExternalStore} from "react";
import {animationSpeedOptions, settingsStore} from "./settings";

export const animationSpeedOptions = [
{value: "off", label: "Off", duration: undefined},
{value: "fast", label: "Fast", duration: 100},
{value: "medium", label: "Medium", duration: 200},
{value: "slow", label: "Slow", duration: 500},
{value: "debug", label: "Debug", duration: 5000},
] as const;

export type AnimationSpeed = (typeof animationSpeedOptions)[number]["value"];

const reducedMotionQuery = "(prefers-reduced-motion: reduce)";

Expand All @@ -16,9 +25,8 @@ export function usePrefersReducedMotion(): boolean {
);
}

export function getGraphAnimationDuration(): number | undefined {
export function getGraphAnimationDuration(animationSpeed: AnimationSpeed): number | undefined {
if (window.matchMedia(reducedMotionQuery).matches) return undefined;
const animationSpeed = settingsStore.getState().values.animationSpeed;
return animationSpeedOptions.find(({value}) => value === animationSpeed)?.duration;
}

Expand Down
6 changes: 4 additions & 2 deletions query-graphs/src/ui/useAnimatedGraphLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {TreeDescription, TreeNode} from "../tree-description";
import type {QueryGraphNode} from "./QueryNode";
import {layoutTree} from "./tree-layout";
import {getGraphAnimationDuration, graphAnimationProgress} from "./animation-timing";
import type {AnimationSpeed} from "./animation-timing";
import type {GraphLayout} from "./animated-layout";
import {
closestAnimationAnchor,
Expand Down Expand Up @@ -181,6 +182,7 @@ export function useAnimatedGraphLayout(
nodeIds: ReadonlyMap<TreeNode, string>,
treeParents: TreeParents,
expandedSubtrees: Record<string, boolean>,
animationSpeed: AnimationSpeed,
): GraphLayout & {
onNodesChange: (changes: NodeChange<QueryGraphNode>[]) => void;
animateGraphChange: AnimateGraphChange;
Expand Down Expand Up @@ -259,11 +261,11 @@ export function useAnimatedGraphLayout(
for (const {nodeId} of resizingNodes) finishNodeResize(nodeResizesRef.current, nodeId);
for (const [nodeId, resize] of resizes) nodeResizesRef.current.set(nodeId, resize);
animationAnchorNodeIdsRef.current = anchorAllVisibleNodes ? new Set() : undefined;
animationDurationRef.current = getGraphAnimationDuration();
animationDurationRef.current = getGraphAnimationDuration(animationSpeed);
applyChange();
setGraphChangeRevision((revision) => revision + 1);
},
[cancelLayoutFrame],
[animationSpeed, cancelLayoutFrame],
);

// Apply each computed target to the currently rendered frame. New
Expand Down
7 changes: 2 additions & 5 deletions query-graphs/test/animation-timing.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
import {getGraphAnimationDuration, graphAnimationProgress} from "../src/ui/animation-timing";
import {settingsStore} from "../src/ui/settings";

test("the selected animation speed resolves to its duration unless motion is disabled", () => {
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
Expand All @@ -11,12 +10,10 @@ test("the selected animation speed resolves to its duration unless motion is dis
value: {matchMedia: () => ({matches: reducedMotion})},
});
try {
settingsStore.getState().setSettings({animationSpeed: "slow"});
assert.equal(getGraphAnimationDuration(), 500);
assert.equal(getGraphAnimationDuration("slow"), 500);
reducedMotion = true;
assert.equal(getGraphAnimationDuration(), undefined);
assert.equal(getGraphAnimationDuration("slow"), undefined);
} finally {
settingsStore.getState().setSettings({animationSpeed: "medium"});
if (previousWindow === undefined) Reflect.deleteProperty(globalThis, "window");
else Object.defineProperty(globalThis, "window", previousWindow);
}
Expand Down
6 changes: 5 additions & 1 deletion standalone-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
"@codemirror/lint": "^6.9.7",
"@codemirror/search": "^6.7.2",
"@codemirror/state": "^6.7.5",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.43.12",
"@replit/codemirror-emacs": "^6.1.0",
"@replit/codemirror-vim": "^6.4.0",
"@tableau/query-graphs": "workspace:^",
"@types/node": "^24.13.3",
"@types/react": "^19.2.18",
Expand All @@ -58,6 +61,7 @@
"webpack-cli": "^7.2.2",
"webpack-dev-server": "^6.0.0",
"webpack-merge": "^6.0.1",
"workbox-webpack-plugin": "^7.4.1"
"workbox-webpack-plugin": "^7.4.1",
"zustand": "^5.0.0"
}
}
Loading
Loading