Skip to content

Commit

Permalink
Add zoom & pan to the graph
Browse files Browse the repository at this point in the history
Zoom into the graph by pressing `ctrl` key and scrolling the mouse wheel
or the trackpad. Zooming widens rows, allowing more space for
individual nodes.

To pan along the graph, click and drag the graph while
pressing the ctrl key.

When ctrl is pressed, the `<MainWindow>` places a transparent 'overlay'
div on top of the graph. It is managed by the MapInteraction component.
Drag and zoom events are captured and handled by functions in the
MainWindow.
This div captures the zoom and drag events.
The overlay is removed when the ctrl key is released.

Scrolling along the graph works as before (without ctrl).
The scrollable container is the 'content' div.

Opening the detail sidebar gives new options to zoom and pan the graph.
The zoom range is set so the whole graph can be seen with the sidebar.
Also, zoomed graphs keep their positions when the sidebar is opened or
closed.

Edges and Arrows are updated on the zoomed graph, because the
AnimationState includes the current zoom/pan state.

In preparation for the zoom behavior, the animations were improved.
The AnimationUpdater is responsible for keeping the connections between
nodes responsive, i. e. the Edges and Arrows are updated when the
nodes change. Nodes change due to expanding their height, expanding
recursion or changing the width of the window. During those changes, the
animation updater changes state, so that the Edges and Arrows rerender.

To manage the performance, use the react hook useResizeObserver, which
uses a single ResizeObserver for tracking all elements used by the
hooks.

The horizontal overflow (showMini) of nodes is now handled by individual
branches, instead of the whole row. This is done by moving the
checkForOverflow function to the Node, while passing a reference to the
branchSpace.

Minor fix to improve loading animation:
This adds a new propery to a node, `loading`,
which is set to `true` when the node is a stand-in while loading data
from the backend. This is used to show a loading animation in the node.
The uuid of the node is not used to identify this anymore.

Minor fix: Large nodes can now be collapsed after being expanded.

Minor fix: Improve the zIndex positioning of the edges
Positioning the edges inside the row_container works now. It is the best
compromise, between available options. The edges sit behind the
row_header in the row_container and stay behind when dragging a row.
They appear in front of other rows' row_headers, but behind the own
row_header.

Contributes: #71
Contributes: #61
  • Loading branch information
stephanzwicknagl committed Apr 4, 2024
1 parent 268230d commit 7263420
Show file tree
Hide file tree
Showing 24 changed files with 1,349 additions and 551 deletions.
1 change: 0 additions & 1 deletion backend/src/viasp/api.py
Expand Up @@ -126,7 +126,6 @@ def viasp(**kwargs) -> None:
head_name = kwargs.get("head_name", "unsat")
no_collect_variables = kwargs.get("no_collect_variables", False)
opt_mode, bounds = kwargs.get("opt_mode") or ('opt', [])
print(f"opt_mode_str: {opt_mode}, {bounds}")
opt_mode_str = f"--opt-mode={opt_mode}" + (f",{','.join(bounds)}"
if len(bounds) > 0 else "")

Expand Down
21 changes: 14 additions & 7 deletions backend/src/viasp/server/blueprints/dag_api.py
Expand Up @@ -94,20 +94,23 @@ def get_src_tgt_mapping_from_graph(shown_recursive_ids=[],

to_be_added = []

for source, target in graph.edges:
for source, target, edge in graph.edges(data=True):
to_be_added.append({
"src": source.uuid,
"tgt": target.uuid,
"transformation": edge["transformation"].hash,
"style": "solid"
})

for recursive_uuid in shown_recursive_ids:
# get node from graph where node attribute uuid is uuid
node = next(n for n in graph.nodes if n.uuid == recursive_uuid)
_, _, edge = next(e for e in graph.in_edges(node, data=True))
for source, target in node.recursive.edges:
to_be_added.append({
"src": source.uuid,
"tgt": target.uuid,
"transformation": edge["transformation"].hash,
"style": "solid"
})
# add connections to outer node
Expand All @@ -121,6 +124,7 @@ def get_src_tgt_mapping_from_graph(shown_recursive_ids=[],
to_be_added.extend([{
"src": node.uuid,
"tgt": first_node.uuid,
"transformation": edge["transformation"].hash,
"recursion": "in",
"style": "solid"
} for first_node in first_nodes])
Expand All @@ -129,17 +133,20 @@ def get_src_tgt_mapping_from_graph(shown_recursive_ids=[],
to_be_added.extend([{
"src": last_node.uuid,
"tgt": node.uuid,
"transformation": edge["transformation"].hash,
"recursion": "out",
"style": "solid"
} for last_node in last_nodes])

if shown_clingraph:
clingraph = load_clingraph_names()
to_be_added += [{
"src": src,
"tgt": tgt,
"style": "dashed"
} for src, tgt in list(zip(last_nodes_in_graph(graph), clingraph))]
for src, tgt in list(zip(last_nodes_in_graph(graph), clingraph)):
to_be_added.append({
"src": src,
"tgt": tgt,
"transformation": "boxrow_container",
"style": "dashed"
})
return to_be_added


Expand Down Expand Up @@ -218,7 +225,7 @@ def get_node(uuid):
@bp.route("/graph/facts", methods=["GET"])
def get_facts():
graph = _get_graph()
facts = get_start_node_from_graph(graph)
facts = [get_start_node_from_graph(graph)]
r = jsonify(facts)
return r

Expand Down
4 changes: 2 additions & 2 deletions backend/src/viasp/server/colorPalette.json
Expand Up @@ -6,8 +6,8 @@
"error": "#F44336",
"infoBackground": "#B3BAC5",
"rowShading": [
"#a9a9a92f",
"#ffffff"
"#c9c9c99f",
"#ffffff9f"
],
"explanationSuccess": "#e2b38a",
"explanationHighlights": [
Expand Down
85 changes: 53 additions & 32 deletions frontend/src/lib/components/Arrows.react.js
@@ -1,47 +1,68 @@
import React from "react";
import { useHighlightedSymbol } from "../contexts/HighlightedSymbol";
import { useShownRecursion } from "../contexts/ShownRecursion";
import Xarrow from "react-xarrows";
import { useAnimationUpdater } from "../contexts/AnimationUpdater";
import PropTypes from 'prop-types'
import { v4 as uuidv4 } from 'uuid';


export function Arrows() {
const { highlightedSymbol } = useHighlightedSymbol();
const [shownRecursion, , ] = useShownRecursion();
// state to update Arrows after height animation of node
const [value, , ,] = useAnimationUpdater();
const {highlightedSymbol} = useHighlightedSymbol();
const [arrows, setArrows] = React.useState([]);
const {animationState} = useAnimationUpdater();

const calculateArrows = React.useCallback(() => {
return highlightedSymbol.map(arrow => {
const suffix1 = `_${document.getElementById(arrow.src+"_main")?"main":"sub"}`;
const suffix2 = `_${document.getElementById(arrow.tgt+"_main")?"main":"sub"}`;
return {"src": arrow.src + suffix1, "tgt": arrow.tgt + suffix2, "color": arrow.color};
}).filter(arrow => {
// filter false arrows that are not in the DOM
return document.getElementById(arrow.src) && document.getElementById(arrow.tgt)
}).map(arrow => {
return <Xarrow
key={arrow.src + "-" + arrow.tgt} start={arrow.src} end={arrow.tgt} startAnchor={"top"} endAnchor={"bottom"} color={arrow.color} strokeWidth={2} headSize={5} zIndex={10} />
})
return highlightedSymbol
.map((arrow) => {
const suffix1 = `_${
document.getElementById(arrow.src + '_main')
? 'main'
: 'sub'
}`;
const suffix2 = `_${
document.getElementById(arrow.tgt + '_main')
? 'main'
: 'sub'
}`;
return {
src: arrow.src + suffix1,
tgt: arrow.tgt + suffix2,
color: arrow.color,
};
})
.filter((arrow) => {
// filter false arrows that are not in the DOM
return (
document.getElementById(arrow.src) &&
document.getElementById(arrow.tgt)
);
})
.map((arrow) => {
return (
<Xarrow
key={uuidv4()}
start={arrow.src}
end={arrow.tgt}
startAnchor={'auto'}
endAnchor={'auto'}
color={arrow.color}
strokeWidth={2}
headSize={5}
zIndex={10}
/>
);
});
}, [highlightedSymbol]);

const arrows = calculateArrows();


// React.useEffect(() => {
// // arrows = [];
// arrows = calculateArrows();
// onFullyLoaded(() => {arrows = calculateArrows()});
// }, [ highlightedSymbol, shownRecursion ])
React.useEffect(() => {
setArrows(calculateArrows());
}, [animationState, highlightedSymbol, calculateArrows]);

function onFullyLoaded(callback) {
setTimeout(function () {
requestAnimationFrame(callback)
})
}
return <div className="arrows_container">
{arrows.length > 0 ? arrows : null}
</div>
return (
<div className="arrows_container">
{arrows.length > 0 ? arrows : null}
</div>
);
}

Arrows.propTypes = {
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/lib/components/DragHandle.react.js
@@ -0,0 +1,34 @@
import React from 'react';
import './draghandle.css';
import PropTypes from 'prop-types';
import dragHandleRounded from '@iconify/icons-material-symbols/drag-handle-rounded';
import {IconWrapper} from '../LazyLoader';

export class DragHandle extends React.Component {
constructor(props) {
super(props);
}

render() {
const {dragHandleProps} = this.props;
return (
<div
className="dragHandle"
{...dragHandleProps}
>
<React.Suspense fallback={<div>=</div>}>
<IconWrapper icon={dragHandleRounded} width="24" />
</React.Suspense>
</div>
);
}
}

DragHandle.propTypes = {
/**
* an object which should be spread as props on the HTML element to be used as the drag handle.
* The whole item will be draggable by the wrapped element.
**/
dragHandleProps: PropTypes.object,
};

67 changes: 50 additions & 17 deletions frontend/src/lib/components/Edges.react.js
Expand Up @@ -2,33 +2,66 @@ import React from "react";
import LineTo from "react-lineto";
import PropTypes from "prop-types";
import {useColorPalette} from "../contexts/ColorPalette";
import { useAnimationUpdater } from "../contexts/AnimationUpdater";
import { useEdges } from "../contexts/Edges";


export function Edges(props) {
const colorPalete = useColorPalette();
const { edges } = useEdges();
const [value, , ,] = useAnimationUpdater();


return <div className="edge_container">

return <>
{edges.map(link => {
if (link.recursion === "in") {
return <LineTo
key={link.src + "-" + link.tgt} from={link.src} fromAnchor={"top center"} toAnchor={"top center"}
to={link.tgt} zIndex={8} borderColor={colorPalete.dark} borderStyle={link.style} borderWidth={1} />
} else if (link.recursion === "out") {
return <LineTo
key={link.src + "-" + link.tgt} from={link.src} fromAnchor={"bottom center"} toAnchor={"bottom center"}
to={link.tgt} zIndex={8} borderColor={colorPalete.dark} borderStyle={link.style} borderWidth={1} />
return (
<LineTo
key={link.src + '-' + link.tgt}
from={link.src}
fromAnchor={'top center'}
toAnchor={'top center'}
to={link.tgt}
zIndex={1}
borderColor={colorPalete.dark}
borderStyle={link.style}
borderWidth={1}
delay={1000}
within={`row_container ${link.transformation}`}
/>
);
}
if (link.recursion === "out") {
return (
<LineTo
key={link.src + '-' + link.tgt}
from={link.src}
fromAnchor={'bottom center'}
toAnchor={'bottom center'}
to={link.tgt}
zIndex={1}
borderColor={colorPalete.dark}
borderStyle={link.style}
borderWidth={1}
delay={1000}
within={`row_container ${link.transformation}`}
/>
);
}
return <LineTo
key={link.src + "-" + link.tgt} from={link.src} fromAnchor={"bottom center"} toAnchor={"top center"}
to={link.tgt} zIndex={8} borderColor={colorPalete.dark} borderStyle={link.style} borderWidth={1} />
return (
<LineTo
key={link.src + '-' + link.tgt}
from={link.src}
fromAnchor={'bottom center'}
toAnchor={'top center'}
to={link.tgt}
zIndex={5}
borderColor={colorPalete.dark}
borderStyle={link.style}
borderWidth={1}
delay={1000}
within={`row_container ${link.transformation}`}
/>
);
}
)}
</div>
</>
}


Expand Down
44 changes: 41 additions & 3 deletions frontend/src/lib/components/Facts.react.js
Expand Up @@ -3,19 +3,26 @@ import React from "react";
import {Node} from "./Node.react";
import {useTransformations} from "../contexts/transformations";
import {make_default_nodes} from "../utils";
import {useAnimationUpdater} from "../contexts/AnimationUpdater";
import useResizeObserver from '@react-hook/resize-observer';


export function Facts() {
const { state: {currentDragged, transformationNodesMap} } = useTransformations();
const [fact, setFact] = React.useState(make_default_nodes()[0]);
const [style, setStyle] = React.useState({opacity: 1.0});
const opacityMultiplier = 0.8;
const branchSpaceRef = React.useRef(null);
const rowbodyRef = React.useRef(null);
const {setAnimationState} = useAnimationUpdater();
const setAnimationStateRef = React.useRef(setAnimationState);

React.useEffect(() => {
if (
transformationNodesMap &&
transformationNodesMap["-1"]
) {
setFact(transformationNodesMap["-1"]);
setFact(transformationNodesMap["-1"][0]);
}
}, [transformationNodesMap]);

Expand All @@ -28,17 +35,48 @@ export function Facts() {
}
}, [currentDragged, opacityMultiplier]);

const animateResize = React.useCallback(() => {
const setAnimationState = setAnimationStateRef.current;
const element = rowbodyRef.current;
setAnimationState((oldValue) => ({
...oldValue,
"-1": {
...oldValue["-1"],
width: element.clientWidth,
height: element.clientHeight,
top: element.offsetTop,
left: element.offsetLeft,
},
}));
}, []);
useResizeObserver(rowbodyRef, animateResize);

if (fact === null) {
return (
<div className="row_container">
</div>
)
}
return <div className="row_row" style={style}><Node
return (
<div
className="row_row"
style={style}
ref={rowbodyRef}
>
<div
className="branch_space"
key={fact.uuid}
style={{flex: '0 0 100%'}}
ref={branchSpaceRef}
>
<Node
key={fact.uuid}
node={fact}
showMini={false}/>
isSubnode={false}
branchSpace={branchSpaceRef}/>
</div>
</div>
);
}

Facts.propTypes = {}
Expand Down

0 comments on commit 7263420

Please sign in to comment.