Skip to content

Commit

Permalink
feat: Add fit/center view [PT-187382788]
Browse files Browse the repository at this point in the history
This adds the fit and center view options.

This also updates the zoom to use the d3 type for of the ZoomTransform class instead of a custom plain object type.
  • Loading branch information
dougmartin committed Apr 8, 2024
1 parent ef46009 commit 3a71dd6
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 17 deletions.
30 changes: 28 additions & 2 deletions src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,20 @@ export const App = () => {
const animationInterval = useRef<number>();
const { graph, updateGraph, setGraph } = useGraph();
const [initialGraph, setInitialGraph] = useState<GraphData>();
const [fitViewAt, setFitViewAt] = useState<number>();
const [recenterViewAt, setRecenterViewAt] = useState<number>();
const onCODAPDataChanged = (values: string[]) => {
updateGraph(values);
setFitViewAt(Date.now());
};
const onSetGraph = (data: GraphData) => {
setGraph(data);
setFitViewAt(Date.now());
};
const { dragging, outputToDataset, viewMode, setViewMode, notifyStateIsDirty, loadState } = useCODAP({
onCODAPDataChanged: updateGraph,
onCODAPDataChanged,
getGraph: useCallback(() => graph, [graph]),
setGraph,
setGraph: onSetGraph,
setInitialGraph
});
const { generate } = useGenerator();
Expand Down Expand Up @@ -419,6 +429,14 @@ export const App = () => {
}
};

const handleFitView = () => {
setFitViewAt(Date.now());
};

const handleRecenterView = () => {
setRecenterViewAt(Date.now());
};

if (loadState === "loading") {
return <div className="loading">Loading ...</div>;
}
Expand Down Expand Up @@ -479,6 +497,10 @@ export const App = () => {
setSelectedNodeId={setSelectedNodeId}
onReset={handleReset}
onReturnToMainMenu={handleReturnToMainMenu}
onFitView={handleFitView}
onRecenterView={handleRecenterView}
fitViewAt={fitViewAt}
recenterViewAt={recenterViewAt}
/>
:
<Dataset
Expand All @@ -493,6 +515,10 @@ export const App = () => {
setSelectedNodeId={setSelectedNodeId}
onReset={handleReset}
onReturnToMainMenu={handleReturnToMainMenu}
onFitView={handleFitView}
onRecenterView={handleRecenterView}
fitViewAt={fitViewAt}
recenterViewAt={recenterViewAt}
/>
}
</div>
Expand Down
15 changes: 13 additions & 2 deletions src/components/dataset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,20 @@ interface Props {
selectedNodeId?: string;
animating: boolean;
graphEmpty: boolean;
fitViewAt?: number;
recenterViewAt?: number;
setSelectedNodeId: (id?: string, skipToggle?: boolean) => void;
onReset: () => void;
onReturnToMainMenu: () => void;
onFitView: () => void;
onRecenterView: () => void;
}

export const Dataset = (props: Props) => {
const {highlightNode, highlightLoopOnNode, highlightEdge, highlightAllNextNodes,
graph, graphEmpty, setSelectedNodeId, selectedNodeId, animating, onReset, onReturnToMainMenu} = props;

graph, graphEmpty, setSelectedNodeId, selectedNodeId, animating,
fitViewAt, recenterViewAt,
onReset, onReturnToMainMenu, onFitView, onRecenterView} = props;

const handleToolSelected = (tool: Tool) => {
// TBD
Expand All @@ -36,6 +41,8 @@ export const Dataset = (props: Props) => {
onToolSelected={handleToolSelected}
onReset={onReset}
onReturnToMainMenu={onReturnToMainMenu}
onFitView={onFitView}
onRecenterView={onRecenterView}
/>
<div className="instructions">
<h2>Markov Chains</h2>
Expand All @@ -59,6 +66,8 @@ export const Dataset = (props: Props) => {
onToolSelected={handleToolSelected}
onReset={onReset}
onReturnToMainMenu={onReturnToMainMenu}
onFitView={onFitView}
onRecenterView={onRecenterView}
/>
<Graph
mode="dataset"
Expand All @@ -72,6 +81,8 @@ export const Dataset = (props: Props) => {
allowDragging={true && !animating}
autoArrange={true}
setSelectedNodeId={setSelectedNodeId}
fitViewAt={fitViewAt}
recenterViewAt={recenterViewAt}
/>
</div>
);
Expand Down
11 changes: 10 additions & 1 deletion src/components/drawing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,22 @@ interface Props {
graph: GraphData;
selectedNodeId?: string;
animating: boolean;
fitViewAt?: number;
recenterViewAt?: number;
setGraph: React.Dispatch<React.SetStateAction<GraphData>>;
setHighlightNode: React.Dispatch<React.SetStateAction<Node | undefined>>
setSelectedNodeId: (id?: string, skipToggle?: boolean) => void;
onReset: () => void;
onReturnToMainMenu: () => void;
onFitView: () => void;
onRecenterView: () => void;
}

export const Drawing = (props: Props) => {
const {highlightNode, highlightLoopOnNode, highlightEdge, highlightAllNextNodes,
graph, setGraph, setHighlightNode, setSelectedNodeId: _setSelectedNodeId,
selectedNodeId, animating, onReset, onReturnToMainMenu} = props;
fitViewAt, recenterViewAt,
selectedNodeId, animating, onReset, onReturnToMainMenu, onFitView, onRecenterView} = props;
const [drawingMode, setDrawingMode] = useState<DrawingMode>("select");
const [firstEdgeNode, setFirstEdgeNode] = useState<Node|undefined>(undefined);
const [rubberBand, setRubberBand] = useState<RubberBand|undefined>(undefined);
Expand Down Expand Up @@ -263,6 +268,8 @@ export const Drawing = (props: Props) => {
onToolSelected={handleToolSelected}
onReset={onReset}
onReturnToMainMenu={onReturnToMainMenu}
onFitView={onFitView}
onRecenterView={onRecenterView}
/>
<Graph
mode="drawing"
Expand All @@ -285,6 +292,8 @@ export const Drawing = (props: Props) => {
setSelectedNodeId={setSelectedNodeId}
onDimensions={handleDimensionChange}
onTransformed={handleTransformed}
fitViewAt={fitViewAt}
recenterViewAt={recenterViewAt}
/>
<DragIcon drawingMode={drawingMode} />
<NodeModal
Expand Down
67 changes: 59 additions & 8 deletions src/components/graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const unselectedLoopArrowUrl = "url(#unselectedLoopArrow)";

export type DrawingMode = "select"|"addNode"|"addEdge"|"delete";

export type Transform = {x: number, y: number, k: number};
export type Transform = d3.ZoomTransform;

export type GraphSettings = {
minRadius: number;
Expand Down Expand Up @@ -55,6 +55,8 @@ type Props = {
drawingMode?: DrawingMode;
selectedNodeId?: string;
animating: boolean;
fitViewAt?: number;
recenterViewAt?: number;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
onNodeClick?: (id: string, onLoop?: boolean) => void;
onNodeDoubleClick?: (id: string) => void;
Expand Down Expand Up @@ -240,6 +242,7 @@ export const Graph = (props: Props) => {
const {graph, highlightNode, highlightLoopOnNode, highlightEdge, highlightAllNextNodes,
allowDragging, autoArrange, rubberBand, drawingMode,
onClick, onNodeClick, onNodeDoubleClick, onEdgeClick, onDragStop,
fitViewAt, recenterViewAt,
selectedNodeId, setSelectedNodeId, animating, onDimensions, onTransformed} = props;
const svgRef = useRef<SVGSVGElement | null>(null);
const wrapperRef = useRef<HTMLDivElement | null>(null);
Expand All @@ -250,7 +253,10 @@ export const Graph = (props: Props) => {
const lastClickTimeRef = useRef<number|undefined>(undefined);
const lastClickIdRef = useRef<string|undefined>(undefined);
const draggedRef = useRef(false);
const transformRef = useRef<any>(undefined);
const transformRef = useRef<Transform>();
const zoomRef = useRef<d3.ZoomBehavior<any, any>>();
const lastFitViewAtRef = useRef<number>();
const lastRecenterViewAtRef = useRef<number>();

const highlightSelected = useCallback((svg: d3.Selection<any, unknown, null, undefined>) => {
if (animating || !selectedNodeId) {
Expand Down Expand Up @@ -382,15 +388,18 @@ export const Graph = (props: Props) => {
const svg = d3.select(svgRef.current);

// add zoom
const zoom = d3.zoom()
zoomRef.current = d3.zoom()
//.scaleExtent([0.5, 10]) // This defines the zoom levels (min, max)
.on("zoom", (e) => {
const root = svg.select("g.root");
root.attr("transform", e.transform);
transformRef.current = e.transform;
onTransformed?.(e.transform);
const transform = e.transform as d3.ZoomTransform;
root.attr("transform", transform.toString());
transformRef.current = transform;
onTransformed?.(transform);
});
svg.call(zoom as any);
svg
.call(zoomRef.current)
.on("dblclick.zoom", null); // disable double click to zoom
});

// draw the graph
Expand All @@ -409,7 +418,7 @@ export const Graph = (props: Props) => {
const root = svg
.append("g")
.attr("class", "root")
.attr("transform", transformRef.current)
.attr("transform", transformRef.current?.toString() ?? "")
;

const addArrowMarker = (id: string, color: string, opacity?: number) => {
Expand Down Expand Up @@ -785,6 +794,48 @@ export const Graph = (props: Props) => {
}, [svgRef, d3Graph.nodes, selectedNodeId, highlightNode, highlightLoopOnNode,
highlightEdge, highlightAllNextNodes, highlightSelected]);

const fitOrCenter = useCallback((op: "fit" | "center") => {
if (!width || !height || !zoomRef.current) {
return false;
}

const svg = d3.select(svgRef.current);
const root = svg.select("g.root");
const bounds = (root.node() as SVGGElement).getBBox();
const center = {
x: bounds.x + (bounds.width / 2),
y: bounds.y + (bounds.height / 2),
};
const currentScale = transformRef.current?.k ?? 1;
const fitScale = Math.min(width / bounds.width, height / bounds.height) * 0.8;
const scale = op === "center" ? currentScale : fitScale;
const x = scale * (-center.x / 2);
const y = scale * (-center.y / 2);
const transform = new d3.ZoomTransform(scale, x, y);

svg.call(zoomRef.current.transform, transform);

return true;
}, [width, height]);

// listen for fit view requests
useEffect(() => {
if ((fitViewAt ?? 0) > (lastFitViewAtRef.current ?? 0)) {
if (fitOrCenter("fit")) {
lastFitViewAtRef.current = fitViewAt;
}
}
}, [fitViewAt, fitOrCenter]);

// listen for recenter view requests
useEffect(() => {
if ((recenterViewAt ?? 0) > (lastRecenterViewAtRef.current ?? 0)) {
if (fitOrCenter("center")) {
lastRecenterViewAtRef.current = recenterViewAt;
}
}
}, [recenterViewAt, fitOrCenter]);

const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
if (!autoArrange && onClick) {
onClick(e);
Expand Down
13 changes: 10 additions & 3 deletions src/components/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import "./toolbar.scss";
export const allTools = ["select","addNode","addEdge","addText","delete","fitView","recenter","reset","home"] as const;
const toggleableTools: Tool[] = ["select","addNode","addEdge","addText","delete"];
const nonTopTools: Tool[] = ["reset","home"];
const notImplementedTools: Tool[] = ["addText","fitView","recenter"];
const notImplementedTools: Tool[] = ["addText"];

export type Tool = typeof allTools[number];

Expand Down Expand Up @@ -55,6 +55,8 @@ interface ToolbarProps {
onToolSelected: (tool: Tool) => void;
onReset: () => void;
onReturnToMainMenu: () => void;
onFitView: () => void;
onRecenterView: () => void;
}

export const ToolbarButton = ({tool, selectedTool, onClick}: ToolbarButtonProps) => {
Expand All @@ -76,7 +78,8 @@ export const ToolbarButton = ({tool, selectedTool, onClick}: ToolbarButtonProps)
);
};

export const Toolbar = ({tools, onToolSelected, onReset, onReturnToMainMenu}: ToolbarProps) => {
export const Toolbar = (props: ToolbarProps) => {
const {tools, onToolSelected, onReset, onReturnToMainMenu, onFitView, onRecenterView} = props;
const [selectedTool, setSelectedTool] = useState<Tool>("select");

const handleToolSelected = useCallback((tool: Tool) => {
Expand All @@ -90,9 +93,13 @@ export const Toolbar = ({tools, onToolSelected, onReset, onReturnToMainMenu}: To
onReset();
} else if (tool === "home") {
onReturnToMainMenu();
} else if (tool === "fitView") {
onFitView();
} else if (tool === "recenter") {
onRecenterView();
}
onToolSelected(tool);
}, [selectedTool, onReset, onReturnToMainMenu, onToolSelected]);
}, [selectedTool, onReset, onReturnToMainMenu, onFitView, onRecenterView, onToolSelected]);

const topTools = tools.filter(tool => !nonTopTools.includes(tool));
const bottomTools = tools.filter(tool => nonTopTools.includes(tool));
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/use-codap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export type OutputTextMode = "replace"|"append";
export type UseCODAPOptions = {
onCODAPDataChanged: OnCODAPDataChanged;
getGraph: GetGraphCallback;
setGraph: React.Dispatch<React.SetStateAction<GraphData>>
setGraph: (data: GraphData) => void;
setInitialGraph: React.Dispatch<React.SetStateAction<GraphData|undefined>>
};

Expand Down

0 comments on commit 3a71dd6

Please sign in to comment.