diff --git a/browser/data-browser/package.json b/browser/data-browser/package.json
index ee5990426..0e842e395 100644
--- a/browser/data-browser/package.json
+++ b/browser/data-browser/package.json
@@ -8,6 +8,7 @@
"@bugsnag/core": "^7.16.1",
"@bugsnag/js": "^7.16.5",
"@bugsnag/plugin-react": "^7.16.5",
+ "@dagrejs/dagre": "^1.0.2",
"@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0",
@@ -35,6 +36,7 @@
"react-router-dom": "^6.9.0",
"react-virtualized-auto-sizer": "^1.0.7",
"react-window": "^1.8.7",
+ "reactflow": "^11.8.3",
"remark-gfm": "^3.0.1",
"styled-components": "^6.0.7",
"stylis": "4.3.0",
diff --git a/browser/data-browser/src/chunks/GraphViewer/FloatingEdge.tsx b/browser/data-browser/src/chunks/GraphViewer/FloatingEdge.tsx
new file mode 100644
index 000000000..16e1d2bd9
--- /dev/null
+++ b/browser/data-browser/src/chunks/GraphViewer/FloatingEdge.tsx
@@ -0,0 +1,123 @@
+import React, { useCallback } from 'react';
+import {
+ useStore as useFlowStore,
+ getBezierPath,
+ EdgeText,
+ EdgeProps,
+ Node,
+} from 'reactflow';
+import styled, { useTheme } from 'styled-components';
+import { getEdgeParams, getSelfReferencePath } from './getEdgeParams';
+import { EdgeData } from './buildGraph';
+
+const getPathData = (
+ sourceNode: Node,
+ targetNode: Node,
+ overlapping: boolean,
+) => {
+ // Self referencing edges use a custom path.
+ if (sourceNode.id === targetNode.id) {
+ return getSelfReferencePath(sourceNode);
+ }
+
+ const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(
+ sourceNode,
+ targetNode,
+ overlapping,
+ );
+
+ return getBezierPath({
+ sourceX: sx,
+ sourceY: sy,
+ sourcePosition: sourcePos,
+ targetPosition: targetPos,
+ targetX: tx,
+ targetY: ty,
+ });
+};
+
+function Label({ text }: { text: string }): JSX.Element | string {
+ const parts = text.split('\n');
+
+ if (parts.length === 1) {
+ return text;
+ }
+
+ // SVG does not have any auto word wrap so we split the lines manually and offset them.
+ return (
+ <>
+ {parts.map((part, i) => (
+
+ {part}
+
+ ))}
+ >
+ );
+}
+
+/**
+ * A custom edge that doesn't clutter the graph as mutch as the default edge.
+ * It casts a ray from the center of the source node to the center of the target node then draws a bezier curve between the two intersecting border of the nodes.
+ */
+export function FloatingEdge({
+ id,
+ source,
+ target,
+ markerEnd,
+ style,
+ label,
+ data,
+}: EdgeProps) {
+ const theme = useTheme();
+ const sourceNode = useFlowStore(
+ useCallback(store => store.nodeInternals.get(source), [source]),
+ );
+ const targetNode = useFlowStore(
+ useCallback(store => store.nodeInternals.get(target), [target]),
+ );
+
+ if (!sourceNode || !targetNode) {
+ return null;
+ }
+
+ const [path, labelX, labelY] = getPathData(
+ sourceNode,
+ targetNode,
+ !!data?.overlapping,
+ );
+
+ return (
+ <>
+
+ }
+ labelStyle={{
+ fill: theme.colors.text,
+ }}
+ labelShowBg
+ labelBgStyle={{ fill: theme.colors.bg1 }}
+ labelBgPadding={[2, 4]}
+ labelBgBorderRadius={2}
+ />
+ >
+ );
+}
+
+const Path = styled.path`
+ flex-direction: column;
+ display: flex;
+ flex-grow: 1;
+ height: 100%;
+
+ & .react-flow__handle {
+ opacity: 0;
+ }
+`;
diff --git a/browser/data-browser/src/chunks/GraphViewer/OntologyGraph.tsx b/browser/data-browser/src/chunks/GraphViewer/OntologyGraph.tsx
new file mode 100644
index 000000000..f1bb34d4c
--- /dev/null
+++ b/browser/data-browser/src/chunks/GraphViewer/OntologyGraph.tsx
@@ -0,0 +1,75 @@
+import { Resource, useStore } from '@tomic/react';
+import React, { useCallback } from 'react';
+import ReactFlow, {
+ Controls,
+ useReactFlow,
+ Node,
+ ReactFlowProvider,
+} from 'reactflow';
+import 'reactflow/dist/style.css';
+import './reactFlowOverrides.css';
+import { buildGraph } from './buildGraph';
+import { FloatingEdge } from './FloatingEdge';
+import { useGraph } from './useGraph';
+import { useEffectOnce } from '../../hooks/useEffectOnce';
+import { toAnchorId } from '../../views/OntologyPage/toAnchorId';
+
+const edgeTypes = {
+ floating: FloatingEdge,
+};
+
+interface OntologyGraphProps {
+ ontology: Resource;
+}
+
+/**
+ * !ASYNC COMPONENT, DO NOT IMPORT DIRECTLY!
+ * Displays an ontology as a graph.
+ */
+export default function OntologyGraph({
+ ...props
+}: OntologyGraphProps): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+function OntologyGraphInner({ ontology }: OntologyGraphProps): JSX.Element {
+ const store = useStore();
+ const { fitView } = useReactFlow();
+
+ const { nodes, edges, setGraph, handleNodeChange, handleNodeDoubleClick } =
+ useGraph(ontology);
+
+ useEffectOnce(() => {
+ buildGraph(ontology, store).then(([n, e]) => {
+ setGraph(n, e);
+
+ requestAnimationFrame(() => {
+ fitView();
+ });
+ });
+ });
+
+ const handleClick = useCallback((_: React.MouseEvent, node: Node) => {
+ const domId = toAnchorId(node.id);
+
+ document.getElementById(domId)?.scrollIntoView({ behavior: 'smooth' });
+ }, []);
+
+ return (
+
+
+
+ );
+}
diff --git a/browser/data-browser/src/chunks/GraphViewer/buildGraph.ts b/browser/data-browser/src/chunks/GraphViewer/buildGraph.ts
new file mode 100644
index 000000000..1bc22e994
--- /dev/null
+++ b/browser/data-browser/src/chunks/GraphViewer/buildGraph.ts
@@ -0,0 +1,188 @@
+import { Datatype, Resource, Store, urls } from '@tomic/react';
+import { Node, Edge, MarkerType } from 'reactflow';
+import { randomString } from '../../helpers/randomString';
+import { DefaultTheme } from 'styled-components';
+
+const RELEVANT_DATATYPES = [Datatype.ATOMIC_URL, Datatype.RESOURCEARRAY];
+
+export interface NodeData {
+ label: string;
+ external: boolean;
+}
+
+export enum OverlapIndex {
+ First,
+ Second,
+}
+
+export interface EdgeData {
+ required: boolean;
+ overlapping: boolean;
+}
+
+interface Routing {
+ source: string;
+ target: string;
+}
+
+const label = (text: string, required: boolean): string =>
+ `${required ? '*' : ''}${text}`;
+
+const newEdge = (
+ routing: Routing,
+ name: string,
+ required: boolean,
+ overlapping: boolean,
+): Edge => ({
+ ...routing,
+ id: randomString(),
+ label: label(name, required),
+ markerEnd: {
+ type: MarkerType.ArrowClosed,
+ width: 15,
+ height: 15,
+ },
+ type: 'floating',
+ data: { required, overlapping },
+});
+
+const findEdgeWithSameRouting = (edges: Edge[], routing: Routing): number =>
+ edges.findIndex(
+ edge => edge.source === routing.source && edge.target === routing.target,
+ );
+
+const findAndTagOverlappingEdges = (
+ edges: Edge[],
+ routing: Routing,
+): boolean => {
+ const index = edges.findIndex(
+ edge => edge.target === routing.source && edge.source === routing.target,
+ );
+
+ if (index !== -1) {
+ edges[index] = {
+ ...edges[index],
+ data: {
+ ...edges[index].data,
+ overlapping: true,
+ },
+ };
+ }
+
+ return index !== -1;
+};
+
+const mergeEdges = (
+ existingEdge: Edge,
+ name: string,
+ isRequired: boolean,
+): Edge => ({
+ ...existingEdge,
+ data: {
+ required: isRequired || (existingEdge.data?.required ?? false),
+ overlapping: existingEdge.data?.overlapping ?? false,
+ },
+ label: `${existingEdge.label},\n${label(name, isRequired)}`,
+});
+
+export async function buildGraph(
+ ontology: Resource,
+ store: Store,
+): Promise<[Node[], Edge[]]> {
+ const classes = ontology.get(urls.properties.classes) as string[];
+ // Any classes that are not in the ontology but are referenced by classes that are in the ontology.
+ const externalClasses: Set = new Set();
+
+ const nodes: Node[] = [];
+ const edges: Edge[] = [];
+
+ const classToNode = async (
+ classSubject: string,
+ isExtra = false,
+ ): Promise> => {
+ const res = await store.getResourceAsync(classSubject);
+
+ if (!isExtra) {
+ await createEdges(res);
+ }
+
+ return {
+ id: classSubject,
+ position: { x: 0, y: 100 },
+ width: 100,
+ height: 100,
+ data: { label: res.title, external: isExtra },
+ };
+ };
+
+ const createEdges = async (classResource: Resource) => {
+ const recommends = (classResource.get(urls.properties.recommends) ??
+ []) as string[];
+ const requires = (classResource.get(urls.properties.requires) ??
+ []) as string[];
+
+ for (const subject of [...recommends, ...requires]) {
+ const property = await store.getProperty(subject);
+
+ const isRequired = requires.includes(subject);
+
+ if (
+ RELEVANT_DATATYPES.includes(property.datatype) &&
+ property.classType
+ ) {
+ const routing = {
+ source: classResource.getSubject(),
+ target: property.classType,
+ };
+
+ const existingEdgeIndex = findEdgeWithSameRouting(edges, routing);
+
+ if (existingEdgeIndex === -1) {
+ const isOverlapping = findAndTagOverlappingEdges(edges, routing);
+
+ edges.push(
+ newEdge(routing, property.shortname, isRequired, isOverlapping),
+ );
+
+ if (!classes.includes(property.classType)) {
+ externalClasses.add(property.classType);
+ }
+
+ continue;
+ }
+
+ edges[existingEdgeIndex] = mergeEdges(
+ edges[existingEdgeIndex],
+ property.shortname,
+ isRequired,
+ );
+ }
+ }
+ };
+
+ for (const item of classes) {
+ nodes.push(await classToNode(item));
+ }
+
+ for (const extra of externalClasses) {
+ nodes.push(await classToNode(extra, true));
+ }
+
+ return [nodes, edges];
+}
+
+export function applyNodeStyling(
+ nodes: Node[],
+ theme: DefaultTheme,
+): Node[] {
+ return nodes.map(node => ({
+ ...node,
+ style: {
+ ...node.style,
+ backgroundColor: theme.colors.bg,
+ borderColor: theme.colors.bg2,
+ color: theme.colors.text,
+ borderStyle: node.data.external ? 'dashed' : 'solid',
+ },
+ }));
+}
diff --git a/browser/data-browser/src/chunks/GraphViewer/getEdgeParams.ts b/browser/data-browser/src/chunks/GraphViewer/getEdgeParams.ts
new file mode 100644
index 000000000..86f81eb79
--- /dev/null
+++ b/browser/data-browser/src/chunks/GraphViewer/getEdgeParams.ts
@@ -0,0 +1,132 @@
+import { Position, Node } from 'reactflow';
+
+// this helper function returns the intersection point
+// of the line between the center of the intersectionNode and the target node
+function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
+ // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
+ const {
+ width: intersectionNodeWidth,
+ height: intersectionNodeHeight,
+ positionAbsolute: intersectionNodePosition,
+ } = intersectionNode;
+ const targetPosition = targetNode.positionAbsolute;
+
+ const w = intersectionNodeWidth! / 2;
+ const h = intersectionNodeHeight! / 2;
+
+ const x2 = intersectionNodePosition!.x + w;
+ const y2 = intersectionNodePosition!.y + h;
+ const x1 = targetPosition!.x + w;
+ const y1 = targetPosition!.y + h;
+
+ const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h);
+ const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h);
+ const a = 1 / (Math.abs(xx1) + Math.abs(yy1));
+ const xx3 = a * xx1;
+ const yy3 = a * yy1;
+ const x = w * (xx3 + yy3) + x2;
+ const y = h * (-xx3 + yy3) + y2;
+
+ return { x, y };
+}
+
+// returns the position (top,right,bottom or right) passed node compared to the intersection point
+function getEdgePosition(
+ node: Node,
+ intersectionPoint: { x: number; y: number },
+) {
+ const n = { ...node.positionAbsolute, ...node };
+ const nx = Math.round(n.x!);
+ const ny = Math.round(n.y!);
+ const px = Math.round(intersectionPoint.x);
+ const py = Math.round(intersectionPoint.y);
+
+ if (px <= nx + 1) {
+ return Position.Left;
+ }
+
+ if (px >= nx + n.width! - 1) {
+ return Position.Right;
+ }
+
+ if (py <= ny + 1) {
+ return Position.Top;
+ }
+
+ if (py >= n.y! + n.height! - 1) {
+ return Position.Bottom;
+ }
+
+ return Position.Top;
+}
+
+export function getEdgeParams(
+ source: Node,
+ target: Node,
+ overlapping: boolean,
+) {
+ const sourceIntersectionPoint = getNodeIntersection(source, target);
+ const targetIntersectionPoint = getNodeIntersection(target, source);
+
+ const sourcePos = getEdgePosition(source, sourceIntersectionPoint);
+ const targetPos = getEdgePosition(target, targetIntersectionPoint);
+
+ let sx = sourceIntersectionPoint.x;
+
+ if (overlapping) {
+ const center = source.positionAbsolute!.x! + source.width! / 2;
+ const diff = Math.abs(sx - center);
+
+ if (sx < center) {
+ sx = center + diff;
+ } else {
+ sx = center - diff;
+ }
+ }
+
+ return {
+ sx: sx,
+ sy: sourceIntersectionPoint.y,
+ tx: targetIntersectionPoint.x,
+ ty: targetIntersectionPoint.y,
+ sourcePos,
+ targetPos,
+ };
+}
+
+export function getSelfReferencePath(
+ node: Node,
+): [path: string, labelX: number, labelY: number] {
+ const { positionAbsolute, width, height } = node;
+
+ const { x, y } = positionAbsolute!;
+ const HORIZONTAL_START_OFFSET = 20;
+ const HORIZONTAL_OFFSET = 50;
+ const VERTICAL_OFFSET = 15;
+ const BORDER_RADIUS = 10;
+
+ const start = { x: x! + width! - HORIZONTAL_START_OFFSET, y: y! + height! };
+
+ const path = [
+ `M ${start.x}, ${start.y}`,
+ line(0, VERTICAL_OFFSET - BORDER_RADIUS),
+ arc(BORDER_RADIUS, BORDER_RADIUS),
+ line(HORIZONTAL_OFFSET - BORDER_RADIUS, 0),
+ arc(BORDER_RADIUS, -BORDER_RADIUS),
+ line(0, (height! + (VERTICAL_OFFSET - BORDER_RADIUS) * 2) * -1),
+ arc(-BORDER_RADIUS, -BORDER_RADIUS),
+ line((HORIZONTAL_OFFSET - BORDER_RADIUS) * -1, 0),
+ arc(-BORDER_RADIUS, BORDER_RADIUS),
+ line(0, VERTICAL_OFFSET - BORDER_RADIUS),
+ ].join(', ');
+
+ const labelX = x + width! + HORIZONTAL_OFFSET - HORIZONTAL_START_OFFSET / 2;
+ const labelY = y + height! / 2;
+
+ return [path, labelX, labelY];
+}
+
+const line = (x: number, y: number) => `l ${x} ${y}`;
+
+const arc = (x: number, y: number, sweep = false) =>
+ `a ${x} ${x} 0 0 ${sweep ? 1 : 0} ${x} ${y}`;
diff --git a/browser/data-browser/src/chunks/GraphViewer/reactFlowOverrides.css b/browser/data-browser/src/chunks/GraphViewer/reactFlowOverrides.css
new file mode 100644
index 000000000..e0f327673
--- /dev/null
+++ b/browser/data-browser/src/chunks/GraphViewer/reactFlowOverrides.css
@@ -0,0 +1,5 @@
+.react-flow__handle {
+ background-color: transparent;
+ border: none;
+ cursor: grab;
+}
diff --git a/browser/data-browser/src/chunks/GraphViewer/useGraph.ts b/browser/data-browser/src/chunks/GraphViewer/useGraph.ts
new file mode 100644
index 000000000..27d3535b2
--- /dev/null
+++ b/browser/data-browser/src/chunks/GraphViewer/useGraph.ts
@@ -0,0 +1,156 @@
+import {
+ Edge,
+ Node,
+ NodeChange,
+ NodePositionChange,
+ applyNodeChanges,
+} from 'reactflow';
+import { EdgeData, NodeData, applyNodeStyling } from './buildGraph';
+import { useCallback, useMemo, useState } from 'react';
+import Dagre from '@dagrejs/dagre';
+import { useTheme } from 'styled-components';
+import { Resource, urls, useString } from '@tomic/react';
+
+interface CustomNodePositioning {
+ [key: string]: [x: number, y: number];
+}
+
+type UseNodeReturn = {
+ nodes: Node[];
+ edges: Edge[];
+ setGraph: (nodes: Node[], edges: Edge[]) => void;
+ handleNodeChange: (changes: NodeChange[]) => void;
+ handleNodeDoubleClick: (event: React.MouseEvent, node: Node) => void;
+};
+
+const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
+
+const getLayoutedElements = (nodes: Node[], edges: Edge[]) => {
+ g.setGraph({ rankdir: 'vertical', ranksep: 70 });
+
+ edges.forEach(edge => g.setEdge(edge.source, edge.target));
+ nodes.forEach(node =>
+ g.setNode(node.id, { label: node, width: 120, height: 100 }),
+ );
+
+ Dagre.layout(g);
+
+ return {
+ positionedNodes: nodes.map(node => {
+ const { x, y } = g.node(node.id);
+
+ return { ...node, position: { x, y } };
+ }),
+ positionedEdges: edges,
+ };
+};
+
+const placeNodesInSpace = (
+ nodes: Node[],
+ edges: Edge[],
+ customPositioning: CustomNodePositioning,
+): [nodes: Node[], edges: Edge[]] => {
+ const { positionedNodes, positionedEdges } = getLayoutedElements(
+ nodes,
+ edges,
+ );
+
+ const ajustedNodes = positionedNodes.map(node => {
+ if (customPositioning[node.id]) {
+ const [x, y] = customPositioning[node.id];
+
+ return { ...node, position: { x, y }, positionAbsolute: { x, y } };
+ }
+
+ return node;
+ });
+
+ return [ajustedNodes, positionedEdges];
+};
+
+export function useGraph(ontology: Resource): UseNodeReturn {
+ const theme = useTheme();
+
+ const [customPositioningSTR, setCustomPositioningSTR] = useString(
+ ontology,
+ urls.properties.ontology.customNodePositioning,
+ { commit: true },
+ );
+
+ const customPositioning = useMemo(
+ () => JSON.parse(customPositioningSTR || '{}'),
+ [customPositioningSTR],
+ );
+
+ const [nodes, setNodes] = useState[]>([]);
+ const [edges, setEdges] = useState[]>([]);
+ const [lastPositionChange, setLastPositionChange] =
+ useState();
+
+ const setGraph = useCallback(
+ (_nodes: Node[], _edges: Edge[]) => {
+ const [positionedNodes, positionedEdges] = placeNodesInSpace(
+ _nodes,
+ _edges,
+ customPositioning,
+ );
+ setNodes(applyNodeStyling(positionedNodes, theme));
+ setEdges(positionedEdges);
+ },
+ [theme, customPositioning],
+ );
+
+ const handleNodeDoubleClick = useCallback(
+ async (_e: React.MouseEvent, node: Node) => {
+ const newCustomPositioning = {
+ ...customPositioning,
+ };
+
+ delete newCustomPositioning[node.id];
+
+ await setCustomPositioningSTR(JSON.stringify(newCustomPositioning));
+
+ const [positionedNodes] = placeNodesInSpace(
+ nodes,
+ edges,
+ newCustomPositioning,
+ );
+
+ setNodes(positionedNodes);
+ },
+ [customPositioning, nodes, edges],
+ );
+
+ const handleNodeChange = useCallback(
+ (changes: NodeChange[]) => {
+ const change = changes[0];
+
+ if (change.type === 'position') {
+ if (change.dragging) {
+ setLastPositionChange(change);
+ } else {
+ setCustomPositioningSTR(
+ JSON.stringify({
+ ...customPositioning,
+ [change.id]: [
+ lastPositionChange!.positionAbsolute?.x,
+ lastPositionChange!.positionAbsolute?.y,
+ ],
+ }),
+ );
+ }
+ }
+
+ setNodes(prev => applyNodeChanges(changes, prev));
+ },
+ [customPositioning, lastPositionChange],
+ );
+
+ return {
+ nodes,
+ edges,
+ setGraph,
+ handleNodeChange,
+ handleNodeDoubleClick,
+ };
+}
diff --git a/browser/data-browser/src/views/OntologyPage/Graph.tsx b/browser/data-browser/src/views/OntologyPage/Graph.tsx
new file mode 100644
index 000000000..edfda9893
--- /dev/null
+++ b/browser/data-browser/src/views/OntologyPage/Graph.tsx
@@ -0,0 +1,33 @@
+import { Resource } from '@tomic/react';
+import React, { Suspense } from 'react';
+import styled from 'styled-components';
+
+const OntologyGraph = React.lazy(
+ () => import('../../chunks/GraphViewer/OntologyGraph'),
+);
+
+interface GraphProps {
+ ontology: Resource;
+}
+
+export function Graph({ ontology }: GraphProps): JSX.Element {
+ return (
+
+
+
+
+
+ );
+}
+
+const GraphWrapper = styled.div`
+ position: var(--ontology-graph-position);
+ display: grid;
+ place-items: center;
+ background-color: ${p => p.theme.colors.bg1};
+ border: 1px solid ${p => p.theme.colors.bg2};
+ aspect-ratio: var(--ontology-graph-ratio);
+ border-radius: ${p => p.theme.radius};
+ top: 1rem;
+ overflow: hidden;
+`;
diff --git a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx
index b5d172527..51be7e0f9 100644
--- a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx
+++ b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx
@@ -15,6 +15,7 @@ import { NewClassButton } from './NewClassButton';
import { toAnchorId } from './toAnchorId';
import { OntologyContextProvider } from './OntologyContext';
import { PropertyCardWrite } from './Property/PropertyCardWrite';
+import { Graph } from './Graph';
export function OntologyPage({ resource }: ResourcePageProps) {
const [classes] = useArray(resource, urls.properties.classes);
@@ -91,7 +92,7 @@ export function OntologyPage({ resource }: ResourcePageProps) {
{!editMode && (
- Placeholder
+
)}
@@ -99,19 +100,10 @@ export function OntologyPage({ resource }: ResourcePageProps) {
);
}
-const TempGraph = styled.div`
- position: sticky;
- display: grid;
- place-items: center;
- background-color: ${p => p.theme.colors.bg1};
- border: 1px solid ${p => p.theme.colors.bg2};
- aspect-ratio: 9 / 16;
- border-radius: ${p => p.theme.radius};
- top: 1rem;
- overflow: hidden;
-`;
-
const FullPageWrapper = styled.div<{ edit: boolean }>`
+ --ontology-graph-position: sticky;
+ --ontology-graph-ratio: 9 / 16;
+
display: grid;
grid-template-areas: ${p =>
p.edit
@@ -130,11 +122,8 @@ const FullPageWrapper = styled.div<{ edit: boolean }>`
grid-template-columns: 1fr 5fr;
grid-template-rows: 4rem auto auto;
-
- ${TempGraph} {
- position: static;
- aspect-ratio: 16/9;
- }
+ --ontology-graph-position: sticky;
+ --ontology-graph-ratio: 16/9;
}
`;
diff --git a/browser/lib/src/urls.ts b/browser/lib/src/urls.ts
index 4b3d58bf4..98f25ac8d 100644
--- a/browser/lib/src/urls.ts
+++ b/browser/lib/src/urls.ts
@@ -142,6 +142,10 @@ export const properties = {
table: {
tableColumnWidths: 'https://atomicdata.dev/properties/tableColumnWidths',
},
+ ontology: {
+ customNodePositioning:
+ 'https://atomicdata.dev/properties/custom-node-positioning',
+ },
color: 'https://atomicdata.dev/properties/color',
emoji: 'https://atomicdata.dev/properties/emoji',
classes: 'https://atomicdata.dev/properties/classes',
diff --git a/browser/pnpm-lock.yaml b/browser/pnpm-lock.yaml
index 50cccecb3..6cabf0cce 100644
--- a/browser/pnpm-lock.yaml
+++ b/browser/pnpm-lock.yaml
@@ -108,6 +108,9 @@ importers:
'@bugsnag/plugin-react':
specifier: ^7.16.5
version: 7.19.0(@bugsnag/core@7.19.0)
+ '@dagrejs/dagre':
+ specifier: ^1.0.2
+ version: 1.0.2
'@dnd-kit/core':
specifier: ^6.0.5
version: 6.0.8(react-dom@18.2.0)(react@18.2.0)
@@ -189,6 +192,9 @@ importers:
react-window:
specifier: ^1.8.7
version: 1.8.9(react-dom@18.2.0)(react@18.2.0)
+ reactflow:
+ specifier: ^11.8.3
+ version: 11.8.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
remark-gfm:
specifier: ^3.0.1
version: 3.0.1
@@ -234,7 +240,7 @@ importers:
version: 1.1.0
vite:
specifier: ^4.0.4
- version: 4.4.8(@types/node@16.18.39)
+ version: 4.4.8
vite-plugin-pwa:
specifier: ^0.14.1
version: 0.14.7(vite@4.4.8)(workbox-build@6.6.0)(workbox-window@6.6.0)
@@ -1656,6 +1662,17 @@ packages:
engines: {node: '>=10'}
dev: false
+ /@dagrejs/dagre@1.0.2:
+ resolution: {integrity: sha512-7N7vEZDlcU4uRHWuL/9RyI8IgM/d4ULR7z2exJALshh7BHF3tFjYL2pW6bQ4mmlDzd2Tr49KJMIY87Be1L6J0w==}
+ dependencies:
+ '@dagrejs/graphlib': 2.1.13
+ dev: false
+
+ /@dagrejs/graphlib@2.1.13:
+ resolution: {integrity: sha512-calbMa7Gcyo+/t23XBaqQqon8LlgE9regey4UVoikoenKBXvUnCUL3s9RP6USCxttfr0XWVICtYUuKMdehKqMw==}
+ engines: {node: '>17.0.0'}
+ dev: false
+
/@dnd-kit/accessibility@3.0.1(react@18.2.0):
resolution: {integrity: sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==}
peerDependencies:
@@ -2758,6 +2775,114 @@ packages:
'@babel/runtime': 7.22.6
dev: false
+ /@reactflow/background@11.2.8(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-5o41N2LygiNC2/Pk8Ak2rIJjXbKHfQ23/Y9LFsnAlufqwdzFqKA8txExpsMoPVHHlbAdA/xpQaMuoChGPqmyDw==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@reactflow/core': 11.8.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ classcat: 5.0.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ zustand: 4.4.1(@types/react@18.2.18)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
+ /@reactflow/controls@11.1.19(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-Vo0LFfAYjiSRMLEII/aeBo+1MT2a0Yc7iLVnkuRTLzChC0EX+A2Fa+JlzeOEYKxXlN4qcDxckRNGR7092v1HOQ==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@reactflow/core': 11.8.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ classcat: 5.0.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ zustand: 4.4.1(@types/react@18.2.18)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
+ /@reactflow/core@11.8.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-y6DN8Wy4V4KQBGHFqlj9zWRjLJU6CgdnVwWaEA/PdDg/YUkFBMpZnXqTs60czinoA2rAcvsz50syLTPsj5e+Wg==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@types/d3': 7.4.0
+ '@types/d3-drag': 3.0.3
+ '@types/d3-selection': 3.0.6
+ '@types/d3-zoom': 3.0.4
+ classcat: 5.0.4
+ d3-drag: 3.0.0
+ d3-selection: 3.0.0
+ d3-zoom: 3.0.0
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ zustand: 4.4.1(@types/react@18.2.18)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
+ /@reactflow/minimap@11.6.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-PSA28dk09RnBHOA1zb45fjQXz3UozSJZmsIpgq49O3trfVFlSgRapxNdGsughWLs7/emg2M5jmi6Vc+ejcfjvQ==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@reactflow/core': 11.8.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ '@types/d3-selection': 3.0.6
+ '@types/d3-zoom': 3.0.4
+ classcat: 5.0.4
+ d3-selection: 3.0.0
+ d3-zoom: 3.0.0
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ zustand: 4.4.1(@types/react@18.2.18)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
+ /@reactflow/node-resizer@2.1.5(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-z/hJlsptd2vTx13wKouqvN/Kln08qbkA+YTJLohc2aJ6rx3oGn9yX4E4IqNxhA7zNqYEdrnc1JTEA//ifh9z3w==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@reactflow/core': 11.8.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ classcat: 5.0.4
+ d3-drag: 3.0.0
+ d3-selection: 3.0.0
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ zustand: 4.4.1(@types/react@18.2.18)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
+ /@reactflow/node-toolbar@1.2.7(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-vs+Wg1tjy3SuD7eoeTqEtscBfE9RY+APqC28urVvftkrtsN7KlnoQjqDG6aE45jWP4z+8bvFizRWjAhxysNLkg==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@reactflow/core': 11.8.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ classcat: 5.0.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ zustand: 4.4.1(@types/react@18.2.18)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
/@remix-run/router@1.7.2:
resolution: {integrity: sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==}
engines: {node: '>=14'}
@@ -2924,6 +3049,185 @@ packages:
resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==}
dev: true
+ /@types/d3-array@3.0.7:
+ resolution: {integrity: sha512-4/Q0FckQ8TBjsB0VdGFemJOG8BLXUB2KKlL0VmZ+eOYeOnTb/wDRQqYWpBmQ6IlvWkXwkYiot+n9Px2aTJ7zGQ==}
+ dev: false
+
+ /@types/d3-axis@3.0.3:
+ resolution: {integrity: sha512-SE3x/pLO/+GIHH17mvs1uUVPkZ3bHquGzvZpPAh4yadRy71J93MJBpgK/xY8l9gT28yTN1g9v3HfGSFeBMmwZw==}
+ dependencies:
+ '@types/d3-selection': 3.0.6
+ dev: false
+
+ /@types/d3-brush@3.0.3:
+ resolution: {integrity: sha512-MQ1/M/B5ifTScHSe5koNkhxn2mhUPqXjGuKjjVYckplAPjP9t2I2sZafb/YVHDwhoXWZoSav+Q726eIbN3qprA==}
+ dependencies:
+ '@types/d3-selection': 3.0.6
+ dev: false
+
+ /@types/d3-chord@3.0.3:
+ resolution: {integrity: sha512-keuSRwO02c7PBV3JMWuctIfdeJrVFI7RpzouehvBWL4/GGUB3PBNg/9ZKPZAgJphzmS2v2+7vr7BGDQw1CAulw==}
+ dev: false
+
+ /@types/d3-color@3.1.0:
+ resolution: {integrity: sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==}
+ dev: false
+
+ /@types/d3-contour@3.0.3:
+ resolution: {integrity: sha512-x7G/tdDZt4m09XZnG2SutbIuQqmkNYqR9uhDMdPlpJbcwepkEjEWG29euFcgVA1k6cn92CHdDL9Z+fOnxnbVQw==}
+ dependencies:
+ '@types/d3-array': 3.0.7
+ '@types/geojson': 7946.0.10
+ dev: false
+
+ /@types/d3-delaunay@6.0.1:
+ resolution: {integrity: sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==}
+ dev: false
+
+ /@types/d3-dispatch@3.0.3:
+ resolution: {integrity: sha512-Df7KW3Re7G6cIpIhQtqHin8yUxUHYAqiE41ffopbmU5+FifYUNV7RVyTg8rQdkEagg83m14QtS8InvNb95Zqug==}
+ dev: false
+
+ /@types/d3-drag@3.0.3:
+ resolution: {integrity: sha512-82AuQMpBQjuXeIX4tjCYfWjpm3g7aGCfx6dFlxX2JlRaiME/QWcHzBsINl7gbHCODA2anPYlL31/Trj/UnjK9A==}
+ dependencies:
+ '@types/d3-selection': 3.0.6
+ dev: false
+
+ /@types/d3-dsv@3.0.2:
+ resolution: {integrity: sha512-DooW5AOkj4AGmseVvbwHvwM/Ltu0Ks0WrhG6r5FG9riHT5oUUTHz6xHsHqJSVU8ZmPkOqlUEY2obS5C9oCIi2g==}
+ dev: false
+
+ /@types/d3-ease@3.0.0:
+ resolution: {integrity: sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==}
+ dev: false
+
+ /@types/d3-fetch@3.0.3:
+ resolution: {integrity: sha512-/EsDKRiQkby3Z/8/AiZq8bsuLDo/tYHnNIZkUpSeEHWV7fHUl6QFBjvMPbhkKGk9jZutzfOkGygCV7eR/MkcXA==}
+ dependencies:
+ '@types/d3-dsv': 3.0.2
+ dev: false
+
+ /@types/d3-force@3.0.5:
+ resolution: {integrity: sha512-EGG+IWx93ESSXBwfh/5uPuR9Hp8M6o6qEGU7bBQslxCvrdUBQZha/EFpu/VMdLU4B0y4Oe4h175nSm7p9uqFug==}
+ dev: false
+
+ /@types/d3-format@3.0.1:
+ resolution: {integrity: sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==}
+ dev: false
+
+ /@types/d3-geo@3.0.4:
+ resolution: {integrity: sha512-kmUK8rVVIBPKJ1/v36bk2aSgwRj2N/ZkjDT+FkMT5pgedZoPlyhaG62J+9EgNIgUXE6IIL0b7bkLxCzhE6U4VQ==}
+ dependencies:
+ '@types/geojson': 7946.0.10
+ dev: false
+
+ /@types/d3-hierarchy@3.1.3:
+ resolution: {integrity: sha512-GpSK308Xj+HeLvogfEc7QsCOcIxkDwLhFYnOoohosEzOqv7/agxwvJER1v/kTC+CY1nfazR0F7gnHo7GE41/fw==}
+ dev: false
+
+ /@types/d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==}
+ dependencies:
+ '@types/d3-color': 3.1.0
+ dev: false
+
+ /@types/d3-path@3.0.0:
+ resolution: {integrity: sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==}
+ dev: false
+
+ /@types/d3-polygon@3.0.0:
+ resolution: {integrity: sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==}
+ dev: false
+
+ /@types/d3-quadtree@3.0.2:
+ resolution: {integrity: sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==}
+ dev: false
+
+ /@types/d3-random@3.0.1:
+ resolution: {integrity: sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==}
+ dev: false
+
+ /@types/d3-scale-chromatic@3.0.0:
+ resolution: {integrity: sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==}
+ dev: false
+
+ /@types/d3-scale@4.0.4:
+ resolution: {integrity: sha512-eq1ZeTj0yr72L8MQk6N6heP603ubnywSDRfNpi5enouR112HzGLS6RIvExCzZTraFF4HdzNpJMwA/zGiMoHUUw==}
+ dependencies:
+ '@types/d3-time': 3.0.0
+ dev: false
+
+ /@types/d3-selection@3.0.6:
+ resolution: {integrity: sha512-2ACr96USZVjXR9KMD9IWi1Epo4rSDKnUtYn6q2SPhYxykvXTw9vR77lkFNruXVg4i1tzQtBxeDMx0oNvJWbF1w==}
+ dev: false
+
+ /@types/d3-shape@3.1.2:
+ resolution: {integrity: sha512-NN4CXr3qeOUNyK5WasVUV8NCSAx/CRVcwcb0BuuS1PiTqwIm6ABi1SyasLZ/vsVCFDArF+W4QiGzSry1eKYQ7w==}
+ dependencies:
+ '@types/d3-path': 3.0.0
+ dev: false
+
+ /@types/d3-time-format@4.0.0:
+ resolution: {integrity: sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==}
+ dev: false
+
+ /@types/d3-time@3.0.0:
+ resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==}
+ dev: false
+
+ /@types/d3-timer@3.0.0:
+ resolution: {integrity: sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==}
+ dev: false
+
+ /@types/d3-transition@3.0.4:
+ resolution: {integrity: sha512-512a4uCOjUzsebydItSXsHrPeQblCVk8IKjqCUmrlvBWkkVh3donTTxmURDo1YPwIVDh5YVwCAO6gR4sgimCPQ==}
+ dependencies:
+ '@types/d3-selection': 3.0.6
+ dev: false
+
+ /@types/d3-zoom@3.0.4:
+ resolution: {integrity: sha512-cqkuY1ah9ZQre2POqjSLcM8g40UVya/qwEUrNYP2/rCVljbmqKCVcv+ebvwhlI5azIbSEL7m+os6n+WlYA43aA==}
+ dependencies:
+ '@types/d3-interpolate': 3.0.1
+ '@types/d3-selection': 3.0.6
+ dev: false
+
+ /@types/d3@7.4.0:
+ resolution: {integrity: sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==}
+ dependencies:
+ '@types/d3-array': 3.0.7
+ '@types/d3-axis': 3.0.3
+ '@types/d3-brush': 3.0.3
+ '@types/d3-chord': 3.0.3
+ '@types/d3-color': 3.1.0
+ '@types/d3-contour': 3.0.3
+ '@types/d3-delaunay': 6.0.1
+ '@types/d3-dispatch': 3.0.3
+ '@types/d3-drag': 3.0.3
+ '@types/d3-dsv': 3.0.2
+ '@types/d3-ease': 3.0.0
+ '@types/d3-fetch': 3.0.3
+ '@types/d3-force': 3.0.5
+ '@types/d3-format': 3.0.1
+ '@types/d3-geo': 3.0.4
+ '@types/d3-hierarchy': 3.1.3
+ '@types/d3-interpolate': 3.0.1
+ '@types/d3-path': 3.0.0
+ '@types/d3-polygon': 3.0.0
+ '@types/d3-quadtree': 3.0.2
+ '@types/d3-random': 3.0.1
+ '@types/d3-scale': 4.0.4
+ '@types/d3-scale-chromatic': 3.0.0
+ '@types/d3-selection': 3.0.6
+ '@types/d3-shape': 3.1.2
+ '@types/d3-time': 3.0.0
+ '@types/d3-time-format': 4.0.0
+ '@types/d3-timer': 3.0.0
+ '@types/d3-transition': 3.0.4
+ '@types/d3-zoom': 3.0.4
+ dev: false
+
/@types/debug@4.1.8:
resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==}
dependencies:
@@ -2944,6 +3248,10 @@ packages:
fast-json-stable-stringify: 2.1.0
dev: true
+ /@types/geojson@7946.0.10:
+ resolution: {integrity: sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==}
+ dev: false
+
/@types/graceful-fs@4.1.6:
resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==}
dependencies:
@@ -3860,6 +4168,10 @@ packages:
resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==}
dev: true
+ /classcat@5.0.4:
+ resolution: {integrity: sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==}
+ dev: false
+
/classnames@2.3.2:
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
dev: false
@@ -4081,6 +4393,71 @@ packages:
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
+ /d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-dispatch@3.0.1:
+ resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-drag@3.0.0:
+ resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-selection: 3.0.0
+ dev: false
+
+ /d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-color: 3.1.0
+ dev: false
+
+ /d3-selection@3.0.0:
+ resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-transition@3.0.1(d3-selection@3.0.0):
+ resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ d3-selection: 2 - 3
+ dependencies:
+ d3-color: 3.1.0
+ d3-dispatch: 3.0.1
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-timer: 3.0.1
+ dev: false
+
+ /d3-zoom@3.0.0:
+ resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+ dev: false
+
/damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
dev: true
@@ -8017,6 +8394,25 @@ packages:
dependencies:
loose-envify: 1.4.0
+ /reactflow@11.8.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-wuVxJOFqi1vhA4WAEJLK0JWx2TsTiWpxTXTRp/wvpqKInQgQcB49I2QNyNYsKJCQ6jjXektS7H+LXoaVK/pG4A==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@reactflow/background': 11.2.8(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ '@reactflow/controls': 11.1.19(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ '@reactflow/core': 11.8.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ '@reactflow/minimap': 11.6.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ '@reactflow/node-resizer': 2.1.5(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ '@reactflow/node-toolbar': 1.2.7(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
/read-pkg-up@7.0.1:
resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==}
engines: {node: '>=8'}
@@ -9198,6 +9594,14 @@ packages:
tslib: 2.6.1
dev: false
+ /use-sync-external-store@1.2.0(react@18.2.0):
+ resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ react: 18.2.0
+ dev: false
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
@@ -9256,7 +9660,7 @@ packages:
fast-glob: 3.3.1
pretty-bytes: 6.1.1
rollup: 3.27.2
- vite: 4.4.8(@types/node@16.18.39)
+ vite: 4.4.8
workbox-build: 6.6.0
workbox-window: 6.6.0
transitivePeerDependencies:
@@ -9297,7 +9701,7 @@ packages:
fsevents: 2.3.2
dev: true
- /vite@4.4.8(@types/node@16.18.39):
+ /vite@4.4.8:
resolution: {integrity: sha512-LONawOUUjxQridNWGQlNizfKH89qPigK36XhMI7COMGztz8KNY0JHim7/xDd71CZwGT4HtSRgI7Hy+RlhG0Gvg==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
@@ -9325,7 +9729,6 @@ packages:
terser:
optional: true
dependencies:
- '@types/node': 16.18.39
esbuild: 0.18.17
postcss: 8.4.27
rollup: 3.27.2
@@ -9727,6 +10130,26 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
+ /zustand@4.4.1(@types/react@18.2.18)(react@18.2.0):
+ resolution: {integrity: sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==}
+ engines: {node: '>=12.7.0'}
+ peerDependencies:
+ '@types/react': '>=16.8'
+ immer: '>=9.0'
+ react: '>=16.8'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ dependencies:
+ '@types/react': 18.2.18
+ react: 18.2.0
+ use-sync-external-store: 1.2.0(react@18.2.0)
+ dev: false
+
/zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: false