Skip to content

Commit

Permalink
add to_html method
Browse files Browse the repository at this point in the history
  • Loading branch information
superstar54 committed May 29, 2024
1 parent df22cf7 commit 8b62b14
Show file tree
Hide file tree
Showing 22 changed files with 3,577 additions and 1,198 deletions.
5 changes: 5 additions & 0 deletions aiida_workgraph/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,8 @@ def _repr_mimebundle_(self, *args: Any, **kwargs: Any) -> any:
return self._widget._repr_mimebundle_(*args, **kwargs)
else:
return self._widget._ipython_display_(*args, **kwargs)

def to_html(self, output: str = None, **kwargs):
"""Write a standalone html file to visualize the node."""
self._widget.from_node(self)
return self._widget.to_html(output=output, **kwargs)
18 changes: 17 additions & 1 deletion aiida_workgraph/widget/src/widget/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,25 @@ def from_node(self, node: Any) -> None:
ndata.pop("node_class", None)
ndata.pop("process", None)
ndata["label"] = ndata["metadata"]["identifier"]
wgdata = {"nodes": {node.name: ndata}, "links": []}
wgdata = {"name": node.name, "nodes": {node.name: ndata}, "links": []}
self.value = wgdata

def to_html(self, output: str = None, width: str = "100%", height: str = "600px"):
"""Write a standalone html file to visualize the workgraph."""
from IPython.display import IFrame
from .html_template import html_template
import json

if output is None:
output = f"{self.value['name']}.html"
# Replace the placeholder with the actual workgraphData
html_content = html_template.replace(
"__WORKGRAPH_DATA__", json.dumps(self.value)
)
with open(output, "w") as f:
f.write(html_content)
return IFrame(output, width=width, height=height)

@traitlets.observe("positions")
def _observe_positions(self, change: Dict[str, Any]) -> None:
if not self.parent:
Expand Down
244 changes: 244 additions & 0 deletions aiida_workgraph/widget/src/widget/html_template.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
html_template = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rete.js with React in Vanilla JS</title>
<!-- Import React, ReactDOM, and Babel from CDN -->
<script src="https://unpkg.com/react@18.2.0/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-is/18.2.0/umd/react-is.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/styled-components@5.3.6/dist/styled-components.min.js"></script>
<script src="https://unpkg.com/elkjs@0.8.2/lib/elk.bundled.js"></script>
<!-- Import Rete.js and its plugins from CDN -->
<script src="https://cdn.jsdelivr.net/npm/rete@2.0.3/rete.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rete-area-plugin@2.0.3/rete-area-plugin.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rete-connection-plugin@2.0.2/rete-connection-plugin.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rete-render-utils@2.0.2/rete-render-utils.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rete-react-plugin@2.0.5/rete-react-plugin.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rete-auto-arrange-plugin@2.0.1/rete-auto-arrange-plugin.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/rete-minimap-plugin@2.0.1/rete-minimap-plugin.min.js"></script>
<style>
.App {
font-family: sans-serif;
background: rgb(200, 190, 190);
}
.rete {
position: relative;
font-size: 1rem;
margin: 1em;
border-radius: 1em;
text-align: left;
}
body {
overflow: hidden;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useRef, useEffect } = React;
const { createRoot } = ReactDOM;
const { NodeEditor, ClassicPreset } = Rete;
const { AreaPlugin, AreaExtensions } = ReteAreaPlugin;
const { ConnectionPlugin, Presets: ConnectionPresets } = ReteConnectionPlugin;
const { ReactPlugin, Presets } = ReteReactPlugin;
const { AutoArrangePlugin, Presets: ArrangePresets, ArrangeAppliers} = ReteAutoArrangePlugin;
const { MinimapExtra, MinimapPlugin } = ReteMinimapPlugin;
const { RenderUtils } = ReteRenderUtils;
const styled = window.styled;
const workgraphData = __WORKGRAPH_DATA__
// Define Schemes to use in vanilla JS
const Schemes = {
Node: ClassicPreset.Node,
Connection: ClassicPreset.Connection
};
class Node extends ClassicPreset.Node {
width = 180;
height = 100;
}
class Connection extends ClassicPreset.Connection {}
function createDynamicNode(nodeData) {
const node = new Node(nodeData.label);
// resize the node based on the max length of the input/output names
let maxSocketNameLength = 0;
nodeData.inputs.forEach((input) => {
let socket = new ClassicPreset.Socket(input.name);
if (!node.inputs.hasOwnProperty(input.name)) {
node.addInput(input.name, new ClassicPreset.Input(socket, input.name));
maxSocketNameLength = Math.max(maxSocketNameLength, input.name.length);
}
});
nodeData.outputs.forEach((output) => {
let socket = new ClassicPreset.Socket(output.name);
if (!node.outputs.hasOwnProperty(output.name)) {
node.addOutput(output.name, new ClassicPreset.Output(socket, output.name));
maxSocketNameLength = Math.max(maxSocketNameLength, output.name.length);
}
});
node.height = Math.max(140, node.height + (nodeData.inputs.length + nodeData.outputs.length) * 35)
node.width += maxSocketNameLength * 5;
return node;
}
async function addNode(editor, area, nodeData) {
console.log("Adding node", nodeData);
const node = createDynamicNode(nodeData);
await editor.addNode(node);
editor.nodeMap[nodeData.label] = node; // Assuming each nodeData has a unique ID
await area.translate(node.id, { x: nodeData.position[0], y: nodeData.position[1] });
}
async function addLink(editor, area, layout, linkData) {
const fromNode = editor.nodeMap[linkData.from_node];
const toNode = editor.nodeMap[linkData.to_node];
console.log("fromNode", fromNode, "toNode", toNode);
let socket;
if (fromNode && toNode) {
socket = new ClassicPreset.Socket(linkData.from_socket);
if (!fromNode.outputs.hasOwnProperty(linkData.from_socket)) {
fromNode.addOutput(linkData.from_socket, new ClassicPreset.Output(socket, linkData.from_socket));
fromNode.height += 25; // Increase height of node for each output
area.update('node', fromNode.id);
}
socket = new ClassicPreset.Socket(linkData.to_socket);
if (!toNode.inputs.hasOwnProperty(linkData.to_socket)) {
toNode.addInput(linkData.to_socket, new ClassicPreset.Input(socket, linkData.to_socket));
toNode.height += 25; // Increase height of node for each input
area.update('node', toNode.id);
}
await editor.addConnection(new Connection(fromNode, linkData.from_socket, toNode, linkData.to_socket));
// await layout(true);
}
}
async function loadJSON(editor, area, layout, workgraphData) {
for (const nodeId in workgraphData.nodes) {
const nodeData = workgraphData.nodes[nodeId];
await addNode(editor, area, nodeData);
}
// Adding connections based on workgraphData
workgraphData.links.forEach(async (link) => { // Specify the type of link here
await addLink(editor, area, layout, link);
});
}
async function createEditor(container) {
const socket = new ClassicPreset.Socket("socket");
const editor = new NodeEditor(Schemes);
const area = new AreaPlugin(container);
const connection = new ConnectionPlugin();
const render = new ReactPlugin({ createRoot });
const arrange = new AutoArrangePlugin();
const minimap = new MinimapPlugin({
boundViewport: true
});
AreaExtensions.selectableNodes(area, AreaExtensions.selector(), {
accumulating: AreaExtensions.accumulateOnCtrl(),
});
render.addPreset(Presets.classic.setup());
render.addPreset(Presets.minimap.setup({ size: 200 }));
connection.addPreset(ConnectionPresets.classic.setup());
const applier = new ArrangeAppliers.TransitionApplier({
duration: 500,
timingFunction: (t) => t,
async onTick() {
await AreaExtensions.zoomAt(area, editor.getNodes());
}
});
arrange.addPreset(ArrangePresets.classic.setup());
editor.use(area);
area.use(connection);
area.use(render);
area.use(arrange);
area.use(minimap);
AreaExtensions.simpleNodesOrder(area);
async function layout(animate) {
await arrange.layout({ applier: animate ? applier : undefined });
AreaExtensions.zoomAt(area, editor.getNodes());
}
// Adding nodes based on workgraphData
const nodeMap = {}; // To keep track of created nodes for linking
editor.nodeMap = nodeMap;
return {
editor: editor,
area: area,
layout: layout,
destroy: () => area.destroy()
};
}
function App() {
const [editor, setEditor] = useState(null);
const containerRef = useRef(null);
useEffect(() => {
if (containerRef.current && !editor) {
createEditor(containerRef.current).then((editor) => {
setEditor(editor);
loadJSON(editor.editor, editor.area, editor.layout, workgraphData).then(() => {
// aplly layout twice to ensure all nodes are arranged
editor?.layout(false).then(() => editor?.layout(true));
});
});
}
return () => {
if (editor) {
editor.destroy();
}
};
}, [containerRef, editor]);
return (
<div className="App">
<div>
<button onClick={() => editor?.layout(true)}>Arrange</button>
</div>
<div ref={containerRef} className="rete" style={{ height: "100vh", width: "100vh" }}></div>
</div>
);
}
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<App />
);
</script>
</body>
</html>
"""
5 changes: 5 additions & 0 deletions aiida_workgraph/workgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,8 @@ def _repr_mimebundle_(self, *args, **kwargs):
return self._widget._repr_mimebundle_(*args, **kwargs)
else:
return self._widget._ipython_display_(*args, **kwargs)

def to_html(self, output: str = None, **kwargs):
"""Write a standalone html file to visualize the workgraph."""
self._widget.from_workgraph(self)
return self._widget.to_html(output=output, **kwargs)
Loading

0 comments on commit 8b62b14

Please sign in to comment.