Skip to content

Commit

Permalink
Polished OCELGraphViewer (Zoom + Download Button, Adjust D3 Forces)
Browse files Browse the repository at this point in the history
  • Loading branch information
aarkue committed May 29, 2024
1 parent 299406c commit 982ef52
Showing 1 changed file with 171 additions and 117 deletions.
288 changes: 171 additions & 117 deletions frontend/src/routes/OcelGraphViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { downloadURL } from "@/lib/download-url";
import type { OCELGraphOptions } from "@/types/generated/OCELGraphOptions";
import type { OCELEvent, OCELObject } from "@/types/ocel";
import { useContext, useMemo, useRef, useState } from "react";
import { ImageIcon } from "@radix-ui/react-icons";
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import ForceGraph2D, {
type ForceGraphMethods,
type LinkObject,
type NodeObject,
} from "react-force-graph-2d";
import toast from "react-hot-toast";
import { LuClipboardCopy } from "react-icons/lu";
import {
MdOutlineZoomInMap
} from "react-icons/md";

import AutoSizer from "react-virtualized-auto-sizer";

type GraphNode = (OCELEvent | OCELObject) & {
neighbors?: GraphNode[];
links?: GraphLink[];
Expand All @@ -31,7 +38,10 @@ type GraphData = {
};
export default function OcelGraphViewer() {
const ocelInfo = useContext(OcelInfoContext);
const [graphData, setGraphData] = useState<GraphData>();
const [graphData, setGraphData] = useState<GraphData>({
nodes: [],
links: [],
});

const data = useMemo(() => {
const gData = graphData;
Expand All @@ -58,6 +68,12 @@ export default function OcelGraphViewer() {
return gData;
}, [graphData]);

useEffect(() => {
setTimeout(() => {
graphRef.current?.zoomToFit(200,100);
},300);
}, [data]);

const [highlightNodes, setHighlightNodes] = useState(new Set());
const [highlightLinks, setHighlightLinks] = useState(new Set());

Expand Down Expand Up @@ -105,10 +121,35 @@ export default function OcelGraphViewer() {
return (
<div className="my-4 text-lg text-left w-full h-full flex flex-col">
<h2 className="text-4xl font-semibold mb-4">OCEL Graph</h2>
<GraphOptions setGraphData={setGraphData} />
<GraphOptions
setGraphData={(gd) => {
console.log(graphRef.current?.d3Force);
graphRef.current!.d3Force("link")!.distance(10);

if (gd === undefined) {
setGraphData({ nodes: [], links: [] });
} else {
setGraphData(gd);
}
}}
/>
<div className="border w-full h-full my-4 overflow-hidden relative">
<Button
className="absolute top-0 right-0 z-10"
title="Zoom to Fit"
size="icon"
variant="outline"
className="absolute top-1 right-1 z-10 -translate-x-full mr-2"
onClick={() => {
graphRef.current?.zoomToFit(200);
}}
>
<MdOutlineZoomInMap size={24} />
</Button>
<Button
title="Download Graph as PNG Image"
size="icon"
variant="outline"
className="absolute top-1 right-1 z-10"
onClick={(ev) => {
const canvas =
ev.currentTarget.parentElement?.querySelector("canvas");
Expand All @@ -118,124 +159,136 @@ export default function OcelGraphViewer() {
}
}}
>
Download
<ImageIcon width={24} height={24} />
</Button>
{data !== undefined && (
<ForceGraph2D
ref={graphRef}
nodeAutoColorBy={"type"}
linkColor={() => "#d6d6d6"}
backgroundColor="white"
linkWidth={(link) => (highlightLinks.has(link) ? 5 : 2)}
linkDirectionalParticles={4}
linkDirectionalParticleWidth={(link) =>
highlightLinks.has(link) ? 4 : 0
}
onNodeHover={handleNodeHover}
onLinkHover={handleLinkHover}
nodeLabel={(x) =>
`<div style="color: #3f3f3f; font-weight: bold; font-size: 12pt; background: #fef4f4b5; padding: 4px; border-radius: 8px;display: block; text-align: center;width: fit-content;">${
x.id
}<br/><span style="font-weight: normal; font-size: 12pt;">${
x.type
} (${"time" in x ? "Event" : "Object"})</span></div>`
}
nodeCanvasObject={(node, ctx) => {
if (node.x === undefined || node.y === undefined) {
return;
}
const isFirstNode = node.id === graphData?.nodes[0].id;
let width = 4;
let height = 4;
const fillStyle = isFirstNode ? node.color : node.color + "a4";
ctx.lineWidth = isFirstNode ? 0.2 : 0.2;
ctx.strokeStyle = highlightNodes.has(node)
? "black"
: isFirstNode
? "#707070"
: node.color;
if ("time" in node) {
width = 7;
height = 7;
ctx.beginPath();
ctx.fillStyle = "white";
ctx.roundRect(
node.x - width / 2,
node.y - height / 2,
width,
height,
0.2,
);
ctx.fill();
ctx.fillStyle = fillStyle;
ctx.roundRect(
node.x - width / 2,
node.y - height / 2,
width,
height,
0.2,
);
ctx.fill();
ctx.stroke();
node.__bckgDimensions = [2 * width, 2 * height]; // save for nodePointerAreaPaint
} else {
ctx.beginPath();
ctx.fillStyle = "white";
ctx.arc(node.x, node.y, width, 0, 2 * Math.PI, false);
ctx.fill();
ctx.fillStyle = fillStyle;
ctx.arc(node.x, node.y, width, 0, 2 * Math.PI, false);
ctx.fill();
ctx.stroke();
node.__bckgDimensions = [2 * width, 2 * height]; // save for nodePointerAreaPaint
}
<AutoSizer>
{({ height, width }) => (
<ForceGraph2D
ref={graphRef}
graphData={data}
width={width}
height={height}
nodeAutoColorBy={"type"}
linkColor={() => "#d6d6d6"}
backgroundColor="white"
linkWidth={(link) => (highlightLinks.has(link) ? 5 : 2)}
linkDirectionalParticleColor={() => "#556166"}
linkDirectionalParticles={4}
linkDirectionalParticleWidth={(link) =>
highlightLinks.has(link) ? 4 : 0
}
onNodeHover={handleNodeHover}
onLinkHover={handleLinkHover}
nodeLabel={(x) =>
`<div style="color: #3f3f3f; font-weight: bold; font-size: 12pt; background: #fef4f4b5; padding: 4px; border-radius: 8px;display: block; text-align: center;width: fit-content;">${
x.id
}<br/><span style="font-weight: normal; font-size: 12pt;">${
x.type
} (${"time" in x ? "Event" : "Object"})</span></div>`
}
nodeCanvasObject={(node, ctx) => {
if (node.x === undefined || node.y === undefined) {
return;
}
const isFirstNode = node.id === graphData?.nodes[0].id;
let width = 4;
let height = 4;
const fillStyle = isFirstNode
? node.color
: node.color + "a4";
ctx.lineWidth = isFirstNode ? 0.4 : 0.2;
ctx.strokeStyle = highlightNodes.has(node)
? "black"
: isFirstNode
? "#515151"
: node.color;
if ("time" in node) {
width = 7;
height = 7;
ctx.beginPath();
ctx.fillStyle = "white";
ctx.roundRect(
node.x - width / 2,
node.y - height / 2,
width,
height,
0.2,
);
ctx.fill();
ctx.fillStyle = fillStyle;
ctx.roundRect(
node.x - width / 2,
node.y - height / 2,
width,
height,
0.2,
);
ctx.fill();
ctx.stroke();
node.__bckgDimensions = [2 * width, 2 * height]; // save for nodePointerAreaPaint
} else {
ctx.beginPath();
ctx.fillStyle = "white";
ctx.arc(node.x, node.y, width, 0, 2 * Math.PI, false);
ctx.fill();
ctx.fillStyle = fillStyle;
ctx.arc(node.x, node.y, width, 0, 2 * Math.PI, false);
ctx.fill();
ctx.stroke();
node.__bckgDimensions = [2 * width, 2 * height]; // save for nodePointerAreaPaint
}

// Web browser used in Tauri under Linux butchers this text terribly >:(
// Maybe because of the very small font size?
// Web browser used in Tauri under Linux butchers this text terribly >:(
// Maybe because of the very small font size?

// if ((window as any).__TAURI__ === undefined) {
let fontSize = 1;
ctx.font = `${fontSize}px Sans-Serif`;
const label = node.id;
const maxLength = 13;
const text =
label.length > maxLength
? label.substring(0, maxLength - 3) + "..."
: label;
ctx.fillStyle = "black";
// if ((window as any).__TAURI__ === undefined) {
let fontSize = 1;
ctx.font = `${fontSize}px Sans-Serif`;
const label = node.id;
const maxLength = 13;
const text =
label.length > maxLength
? label.substring(0, maxLength - 3) + "..."
: label;
ctx.fillStyle = "black";

ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillText(text, node.x, node.y);
fontSize = 0.8;
ctx.font = `${fontSize}px Sans-Serif`;
ctx.fillStyle = "#3f3f3f";
const typeText =
node.type.length > maxLength
? node.type.substring(0, maxLength - 3) + "..."
: node.type;
ctx.fillText(typeText, node.x, node.y + 1.5 * fontSize);
// }
}}
nodePointerAreaPaint={(node, color, ctx) => {
if (node.x === undefined || node.y === undefined) {
return;
}
ctx.fillStyle = color;
const bckgDimensions: [number, number] = node.__bckgDimensions;
Boolean(bckgDimensions) &&
ctx.fillRect(
node.x - bckgDimensions[0] / 2,
node.y - bckgDimensions[1] / 2,
...bckgDimensions,
);
}}
onNodeClick={async (node) => {
await navigator.clipboard.writeText(node.id);
toast("Copied ID to clipboard!", { icon: <LuClipboardCopy /> });
}}
graphData={data}
/>
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
ctx.fillText(text, node.x, node.y);
fontSize = 0.8;
ctx.font = `${fontSize}px Sans-Serif`;
ctx.fillStyle = "#3f3f3f";
const typeText =
node.type.length > maxLength
? node.type.substring(0, maxLength - 3) + "..."
: node.type;
ctx.fillText(typeText, node.x, node.y + 1.5 * fontSize);
// }
}}
nodePointerAreaPaint={(node, color, ctx) => {
if (node.x === undefined || node.y === undefined) {
return;
}
ctx.fillStyle = color;
const bckgDimensions: [number, number] =
node.__bckgDimensions;
Boolean(bckgDimensions) &&
ctx.fillRect(
node.x - bckgDimensions[0] / 2,
node.y - bckgDimensions[1] / 2,
...bckgDimensions,
);
}}
onNodeClick={async (node) => {
await navigator.clipboard.writeText(node.id);
toast("Copied ID to clipboard!", {
icon: <LuClipboardCopy />,
});
}}
/>
)}
</AutoSizer>
)}
</div>
</div>
Expand Down Expand Up @@ -334,6 +387,7 @@ function GraphOptions({
</div>
</div>
<Button
size="lg"
disabled={loading}
onClick={() => {
setLoading(true);
Expand Down

0 comments on commit 982ef52

Please sign in to comment.