@dxos/plugin-explorer 0.8.4-main.fcfe5033a5 → 0.8.4-staging.60fe92afc8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +102 -5
- package/PLUGIN.mdl +340 -0
- package/dist/lib/neutral/ExplorerArticle-4I7PNGDC.mjs +459 -0
- package/dist/lib/neutral/ExplorerArticle-4I7PNGDC.mjs.map +7 -0
- package/dist/lib/neutral/ExplorerPlugin.mjs +10 -0
- package/dist/lib/neutral/capabilities/index.mjs +11 -0
- package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
- package/dist/lib/neutral/chunk-3D7BYXOR.mjs +37 -0
- package/dist/lib/neutral/chunk-3D7BYXOR.mjs.map +7 -0
- package/dist/lib/neutral/chunk-42BYLQQA.mjs +42 -0
- package/dist/lib/neutral/chunk-42BYLQQA.mjs.map +7 -0
- package/dist/lib/neutral/chunk-7XUDLV6E.mjs +287 -0
- package/dist/lib/neutral/chunk-7XUDLV6E.mjs.map +7 -0
- package/dist/lib/neutral/chunk-IKHJV3Q4.mjs +20 -0
- package/dist/lib/neutral/chunk-IKHJV3Q4.mjs.map +7 -0
- package/dist/lib/{browser/types/index.mjs → neutral/chunk-YBCHBVCJ.mjs} +13 -14
- package/dist/lib/neutral/chunk-YBCHBVCJ.mjs.map +7 -0
- package/dist/lib/{browser → neutral/components}/index.mjs +730 -437
- package/dist/lib/neutral/components/index.mjs.map +7 -0
- package/dist/lib/neutral/containers/index.mjs +9 -0
- package/dist/lib/neutral/containers/index.mjs.map +7 -0
- package/dist/lib/neutral/create-object-F6TKVAGV.mjs +39 -0
- package/dist/lib/neutral/create-object-F6TKVAGV.mjs.map +7 -0
- package/dist/lib/neutral/hooks/index.mjs +45 -0
- package/dist/lib/neutral/hooks/index.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +14 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/lib/{browser → neutral}/meta.mjs +1 -1
- package/dist/lib/neutral/plugin.mjs +12 -0
- package/dist/lib/neutral/plugin.mjs.map +7 -0
- package/dist/lib/neutral/react-surface-APBW2VQG.mjs +26 -0
- package/dist/lib/neutral/react-surface-APBW2VQG.mjs.map +7 -0
- package/dist/lib/neutral/testing/index.mjs +139 -0
- package/dist/lib/neutral/testing/index.mjs.map +7 -0
- package/dist/lib/neutral/translations.mjs +33 -0
- package/dist/lib/neutral/translations.mjs.map +7 -0
- package/dist/lib/neutral/types/index.mjs +10 -0
- package/dist/lib/neutral/types/index.mjs.map +7 -0
- package/dist/types/data/cities.d.ts +4 -4
- package/dist/types/data/cities.d.ts.map +1 -1
- package/dist/types/data/countries-110m.d.ts +19 -22
- package/dist/types/data/countries-110m.d.ts.map +1 -1
- package/dist/types/src/ExplorerPlugin.d.ts +1 -0
- package/dist/types/src/ExplorerPlugin.d.ts.map +1 -1
- package/dist/types/src/ExplorerPlugin.test.d.ts +2 -0
- package/dist/types/src/ExplorerPlugin.test.d.ts.map +1 -0
- package/dist/types/src/capabilities/create-object.d.ts +11 -0
- package/dist/types/src/capabilities/create-object.d.ts.map +1 -0
- package/dist/types/src/capabilities/index.d.ts +6 -0
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
- package/dist/types/src/components/Chart/Chart.d.ts +1 -1
- package/dist/types/src/components/Chart/Chart.d.ts.map +1 -1
- package/dist/types/src/components/Chart/Chart.stories.d.ts +4 -1
- package/dist/types/src/components/Chart/Chart.stories.d.ts.map +1 -1
- package/dist/types/src/components/Globe/Globe.d.ts +1 -1
- package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
- package/dist/types/src/components/Globe/Globe.stories.d.ts +5 -2
- package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
- package/dist/types/src/components/Graph/CanvasForceGraph.d.ts +13 -0
- package/dist/types/src/components/Graph/CanvasForceGraph.d.ts.map +1 -0
- package/dist/types/src/components/Graph/{D3ForceGraph.stories.d.ts → CanvasForceGraph.stories.d.ts} +3 -3
- package/dist/types/src/components/Graph/CanvasForceGraph.stories.d.ts.map +1 -0
- package/dist/types/src/components/Graph/ForceGraph.d.ts +12 -5
- package/dist/types/src/components/Graph/ForceGraph.d.ts.map +1 -1
- package/dist/types/src/components/Graph/ForceGraph.stories.d.ts +3 -1
- package/dist/types/src/components/Graph/ForceGraph.stories.d.ts.map +1 -1
- package/dist/types/src/components/Graph/{adapter.d.ts → graph-adapter.d.ts} +1 -1
- package/dist/types/src/components/Graph/graph-adapter.d.ts.map +1 -0
- package/dist/types/src/components/Graph/index.d.ts +1 -1
- package/dist/types/src/components/Graph/index.d.ts.map +1 -1
- package/dist/types/src/components/Lattice/Lattice.d.ts +20 -0
- package/dist/types/src/components/Lattice/Lattice.d.ts.map +1 -0
- package/dist/types/src/components/Lattice/Lattice.stories.d.ts +8 -0
- package/dist/types/src/components/Lattice/Lattice.stories.d.ts.map +1 -0
- package/dist/types/src/components/Lattice/index.d.ts +2 -0
- package/dist/types/src/components/Lattice/index.d.ts.map +1 -0
- package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts +21 -0
- package/dist/types/src/components/Tree/EdgeBundling.stories.d.ts.map +1 -0
- package/dist/types/src/components/Tree/Tree.d.ts +20 -23
- package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/Tree.stories.d.ts +5 -12
- package/dist/types/src/components/Tree/Tree.stories.d.ts.map +1 -1
- package/dist/types/src/components/Tree/index.d.ts +2 -0
- package/dist/types/src/components/Tree/index.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts +37 -2
- package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/RadialTree.d.ts +35 -2
- package/dist/types/src/components/Tree/layout/RadialTree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/TidyTree.d.ts +24 -2
- package/dist/types/src/components/Tree/layout/TidyTree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/hierarchy.d.ts +17 -0
- package/dist/types/src/components/Tree/layout/hierarchy.d.ts.map +1 -0
- package/dist/types/src/components/Tree/layout/index.d.ts +5 -4
- package/dist/types/src/components/Tree/layout/index.d.ts.map +1 -1
- package/dist/types/src/components/Tree/layout/slots.d.ts +7 -0
- package/dist/types/src/components/Tree/layout/slots.d.ts.map +1 -0
- package/dist/types/src/components/Tree/layout/useContainerSize.d.ts +15 -0
- package/dist/types/src/components/Tree/layout/useContainerSize.d.ts.map +1 -0
- package/dist/types/src/components/Tree/types/tree.d.ts +41 -20
- package/dist/types/src/components/Tree/types/tree.d.ts.map +1 -1
- package/dist/types/src/components/Tree/types/types.d.ts +14 -4
- package/dist/types/src/components/Tree/types/types.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts +8 -0
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts.map +1 -0
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts +15 -0
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/ExplorerArticle/Visualization.d.ts +18 -0
- package/dist/types/src/containers/ExplorerArticle/Visualization.d.ts.map +1 -0
- package/dist/types/src/containers/ExplorerArticle/index.d.ts +2 -0
- package/dist/types/src/containers/ExplorerArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/ExplorerArticle/variants.d.ts +9 -0
- package/dist/types/src/containers/ExplorerArticle/variants.d.ts.map +1 -0
- package/dist/types/src/containers/index.d.ts +1 -1
- package/dist/types/src/containers/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useGraphModel.d.ts +2 -2
- package/dist/types/src/hooks/useGraphModel.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -3
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/meta.d.ts +1 -1
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/plugin.d.ts +3 -0
- package/dist/types/src/plugin.d.ts.map +1 -0
- package/dist/types/src/{components/Tree/testing → testing}/generator.d.ts +1 -1
- package/dist/types/src/testing/generator.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +4 -0
- package/dist/types/src/testing/index.d.ts.map +1 -0
- package/dist/types/src/testing/relations.d.ts +32 -0
- package/dist/types/src/testing/relations.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +17 -17
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/Graph.d.ts +5 -6
- package/dist/types/src/types/Graph.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +3 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/node-color.d.ts +13 -0
- package/dist/types/src/util/node-color.d.ts.map +1 -0
- package/dist/types/src/{components → util}/plot.d.ts +1 -1
- package/dist/types/src/util/plot.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +102 -68
- package/src/ExplorerPlugin.test.ts +26 -0
- package/src/ExplorerPlugin.tsx +11 -34
- package/src/capabilities/create-object.ts +36 -0
- package/src/capabilities/index.ts +1 -0
- package/src/capabilities/react-surface.tsx +2 -2
- package/src/components/Chart/Chart.stories.tsx +14 -20
- package/src/components/Chart/Chart.tsx +1 -1
- package/src/components/Globe/Globe.stories.tsx +17 -19
- package/src/components/Globe/Globe.tsx +1 -1
- package/src/components/Graph/CanvasForceGraph.stories.tsx +97 -0
- package/src/components/Graph/CanvasForceGraph.tsx +124 -0
- package/src/components/Graph/ForceGraph.stories.tsx +88 -38
- package/src/components/Graph/ForceGraph.tsx +105 -85
- package/src/components/Graph/index.ts +1 -1
- package/src/components/Lattice/Lattice.stories.tsx +104 -0
- package/src/components/Lattice/Lattice.tsx +182 -0
- package/src/components/Lattice/index.ts +5 -0
- package/src/components/Tree/EdgeBundling.stories.tsx +144 -0
- package/src/components/Tree/Tree.stories.tsx +17 -38
- package/src/components/Tree/Tree.tsx +69 -100
- package/src/components/Tree/index.ts +2 -0
- package/src/components/Tree/layout/HierarchicalEdgeBundling.tsx +335 -0
- package/src/components/Tree/layout/RadialTree.tsx +242 -0
- package/src/components/Tree/layout/TidyTree.tsx +246 -0
- package/src/components/Tree/layout/hierarchy.ts +32 -0
- package/src/components/Tree/layout/index.ts +5 -5
- package/src/components/Tree/layout/slots.ts +19 -0
- package/src/components/Tree/layout/useContainerSize.ts +43 -0
- package/src/components/Tree/types/tree.test.ts +2 -2
- package/src/components/Tree/types/tree.ts +23 -28
- package/src/components/Tree/types/types.ts +38 -29
- package/src/components/index.ts +1 -0
- package/src/containers/ExplorerArticle/ExplorerArticle.stories.tsx +152 -0
- package/src/containers/ExplorerArticle/ExplorerArticle.tsx +120 -0
- package/src/containers/ExplorerArticle/Visualization.tsx +523 -0
- package/src/containers/ExplorerArticle/index.ts +5 -0
- package/src/containers/ExplorerArticle/variants.ts +47 -0
- package/src/containers/index.ts +1 -1
- package/src/hooks/useGraphModel.ts +10 -6
- package/src/index.ts +1 -4
- package/src/meta.ts +26 -7
- package/src/plugin.ts +9 -0
- package/src/{components/Tree/testing → testing}/generator.ts +3 -3
- package/src/testing/index.ts +9 -0
- package/src/testing/relations.ts +117 -0
- package/src/translations.ts +1 -1
- package/src/types/ExplorerAction.ts +1 -1
- package/src/types/Graph.ts +7 -15
- package/src/util/index.ts +6 -0
- package/src/util/node-color.ts +23 -0
- package/src/{components → util}/plot.ts +16 -4
- package/src/vite-env.d.ts +10 -0
- package/dist/lib/browser/chunk-LSUP47BZ.mjs +0 -24
- package/dist/lib/browser/chunk-LSUP47BZ.mjs.map +0 -7
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/types/index.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-EN3JZNEY.mjs +0 -26
- package/dist/lib/node-esm/chunk-EN3JZNEY.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/index.mjs +0 -11375
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/meta.mjs +0 -9
- package/dist/lib/node-esm/types/index.mjs +0 -71
- package/dist/lib/node-esm/types/index.mjs.map +0 -7
- package/dist/types/src/components/Graph/D3ForceGraph.d.ts +0 -15
- package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +0 -1
- package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts.map +0 -1
- package/dist/types/src/components/Graph/adapter.d.ts.map +0 -1
- package/dist/types/src/components/Graph/testing.d.ts +0 -14
- package/dist/types/src/components/Graph/testing.d.ts.map +0 -1
- package/dist/types/src/components/Tree/testing/generator.d.ts.map +0 -1
- package/dist/types/src/components/Tree/testing/index.d.ts +0 -2
- package/dist/types/src/components/Tree/testing/index.d.ts.map +0 -1
- package/dist/types/src/components/plot.d.ts.map +0 -1
- package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts +0 -6
- package/dist/types/src/containers/ExplorerContainer/ExplorerContainer.d.ts.map +0 -1
- package/dist/types/src/containers/ExplorerContainer/index.d.ts +0 -2
- package/dist/types/src/containers/ExplorerContainer/index.d.ts.map +0 -1
- package/src/components/Graph/D3ForceGraph.stories.tsx +0 -83
- package/src/components/Graph/D3ForceGraph.tsx +0 -108
- package/src/components/Graph/testing.ts +0 -58
- package/src/components/Tree/layout/HierarchicalEdgeBundling.ts +0 -162
- package/src/components/Tree/layout/RadialTree.ts +0 -94
- package/src/components/Tree/layout/TidyTree.ts +0 -101
- package/src/components/Tree/testing/index.ts +0 -5
- package/src/containers/ExplorerContainer/ExplorerContainer.tsx +0 -53
- package/src/containers/ExplorerContainer/index.ts +0 -5
- /package/dist/lib/{browser/chunk-J5LGTIGS.mjs.map → neutral/ExplorerPlugin.mjs.map} +0 -0
- /package/dist/lib/{browser → neutral}/chunk-J5LGTIGS.mjs +0 -0
- /package/dist/lib/{browser/meta.mjs.map → neutral/chunk-J5LGTIGS.mjs.map} +0 -0
- /package/dist/lib/{node-esm/chunk-HSLMI22Q.mjs.map → neutral/index.mjs.map} +0 -0
- /package/dist/lib/{node-esm → neutral}/meta.mjs.map +0 -0
- /package/src/components/Graph/{adapter.ts → graph-adapter.ts} +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { select } from 'd3';
|
|
6
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
7
|
+
|
|
8
|
+
import { Obj } from '@dxos/echo';
|
|
9
|
+
import { type SpaceGraphNode } from '@dxos/schema';
|
|
10
|
+
|
|
11
|
+
import { getNodeFillForObject } from '../../util/node-color';
|
|
12
|
+
import { useContainerSize } from '../Tree/layout/useContainerSize';
|
|
13
|
+
import { type TreeNode } from '../Tree/types';
|
|
14
|
+
|
|
15
|
+
const TRANSITION_MS = 350;
|
|
16
|
+
|
|
17
|
+
export type LatticeProps = {
|
|
18
|
+
/** Object nodes from the space graph (typically `model.graph.nodes` filtered to `type === 'object'`). */
|
|
19
|
+
nodes: SpaceGraphNode[];
|
|
20
|
+
/** Padding (in screen pixels) reserved around the lattice. */
|
|
21
|
+
padding?: number;
|
|
22
|
+
/** Mirrors the hover preview contract used by the other variants. */
|
|
23
|
+
onNodeHover?: (node: TreeNode<Obj.Unknown> | null, event?: MouseEvent) => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type LatticeCell = {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
typename: string;
|
|
30
|
+
object: Obj.Unknown;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Renders objects as an SVG lattice that fits the container without scrolling.
|
|
35
|
+
* Each object is a rounded rect, colored by typename via the shared hue-hash used by every
|
|
36
|
+
* other variant. Cells are sorted by typename then label so objects of the same type cluster.
|
|
37
|
+
* Hover dispatches the standard preview event — there is no rendered label.
|
|
38
|
+
*/
|
|
39
|
+
export const Lattice = ({ nodes, padding = 16, onNodeHover }: LatticeProps) => {
|
|
40
|
+
const svgRef = useRef<SVGSVGElement | null>(null);
|
|
41
|
+
const { setRef, width, height } = useContainerSize();
|
|
42
|
+
|
|
43
|
+
const cells = useMemo<LatticeCell[]>(() => {
|
|
44
|
+
return nodes
|
|
45
|
+
.map((node): LatticeCell | undefined => {
|
|
46
|
+
const object = node.data?.object;
|
|
47
|
+
if (!object) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const label = node.data?.label ?? Obj.getLabel(object) ?? node.id;
|
|
51
|
+
const typename = Obj.getTypename(object) ?? '(untyped)';
|
|
52
|
+
return { id: node.id, label, typename, object };
|
|
53
|
+
})
|
|
54
|
+
.filter((cell): cell is LatticeCell => cell !== undefined)
|
|
55
|
+
.sort((a, b) => a.typename.localeCompare(b.typename) || a.label.localeCompare(b.label));
|
|
56
|
+
}, [nodes]);
|
|
57
|
+
|
|
58
|
+
// Stable hover ref so the effect doesn't rebind handlers on every render.
|
|
59
|
+
const handleHoverRef = useRef<LatticeProps['onNodeHover']>(undefined);
|
|
60
|
+
handleHoverRef.current = onNodeHover;
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!svgRef.current || !width || !height) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
renderLattice(svgRef.current, cells, {
|
|
67
|
+
width,
|
|
68
|
+
height,
|
|
69
|
+
padding,
|
|
70
|
+
onNodeHover: (n, e) => handleHoverRef.current?.(n, e),
|
|
71
|
+
});
|
|
72
|
+
// Clear any pinned preview when the lattice unmounts or re-renders, so the
|
|
73
|
+
// shared hover target doesn't keep pointing at a cell that no longer exists.
|
|
74
|
+
return () => {
|
|
75
|
+
handleHoverRef.current?.(null);
|
|
76
|
+
};
|
|
77
|
+
}, [cells, width, height, padding]);
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div ref={setRef} className='dx-expander relative'>
|
|
81
|
+
{width > 0 && height > 0 && (
|
|
82
|
+
<svg
|
|
83
|
+
ref={svgRef}
|
|
84
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
85
|
+
width={width}
|
|
86
|
+
height={height}
|
|
87
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type RenderOptions = {
|
|
95
|
+
width: number;
|
|
96
|
+
height: number;
|
|
97
|
+
padding: number;
|
|
98
|
+
onNodeHover: (node: TreeNode<Obj.Unknown> | null, event?: MouseEvent) => void;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const renderLattice = (svgElement: SVGSVGElement, cells: LatticeCell[], options: RenderOptions) => {
|
|
102
|
+
const { width, height, padding, onNodeHover } = options;
|
|
103
|
+
const svg = select(svgElement);
|
|
104
|
+
|
|
105
|
+
if (!cells.length) {
|
|
106
|
+
onNodeHover(null);
|
|
107
|
+
svg.selectAll('g.dx-lattice-root').remove();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Columns ≈ √N so the grid stays as square as possible.
|
|
112
|
+
const count = cells.length;
|
|
113
|
+
const columns = Math.max(1, Math.ceil(Math.sqrt(count)));
|
|
114
|
+
const rows = Math.ceil(count / columns);
|
|
115
|
+
|
|
116
|
+
// Derive a cell size that fits both axes after reserving padding around the whole lattice.
|
|
117
|
+
const innerW = Math.max(0, width - 2 * padding);
|
|
118
|
+
const innerH = Math.max(0, height - 2 * padding);
|
|
119
|
+
const cellSize = Math.max(0, Math.min(innerW / columns, innerH / rows));
|
|
120
|
+
// Leave a little gutter between cells; the rect occupies the inner area.
|
|
121
|
+
const gutter = Math.max(2, cellSize * 0.12);
|
|
122
|
+
const rectSize = Math.max(0, cellSize - gutter);
|
|
123
|
+
const radius = Math.max(2, rectSize * 0.18);
|
|
124
|
+
|
|
125
|
+
// Center the lattice in the container.
|
|
126
|
+
const gridW = cellSize * columns;
|
|
127
|
+
const gridH = cellSize * rows;
|
|
128
|
+
const offsetX = (width - gridW) / 2;
|
|
129
|
+
const offsetY = (height - gridH) / 2;
|
|
130
|
+
|
|
131
|
+
const g = svg
|
|
132
|
+
.selectAll<SVGGElement, null>('g.dx-lattice-root')
|
|
133
|
+
.data([null])
|
|
134
|
+
.join('g')
|
|
135
|
+
.classed('dx-lattice-root', true);
|
|
136
|
+
|
|
137
|
+
type Positioned = LatticeCell & { x: number; y: number };
|
|
138
|
+
const positioned: Positioned[] = cells.map((cell, i) => ({
|
|
139
|
+
...cell,
|
|
140
|
+
x: offsetX + (i % columns) * cellSize + gutter / 2,
|
|
141
|
+
y: offsetY + Math.floor(i / columns) * cellSize + gutter / 2,
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
const node = g
|
|
145
|
+
.selectAll<SVGGElement, Positioned>('g.dx-lattice-cell')
|
|
146
|
+
.data(positioned, (d) => d.id)
|
|
147
|
+
.join(
|
|
148
|
+
(enter) => {
|
|
149
|
+
const ge = enter.append('g').classed('dx-lattice-cell', true).attr('opacity', 0);
|
|
150
|
+
ge.append('rect').style('cursor', 'pointer');
|
|
151
|
+
return ge;
|
|
152
|
+
},
|
|
153
|
+
(update) => update,
|
|
154
|
+
(exit) =>
|
|
155
|
+
exit
|
|
156
|
+
.each(function () {
|
|
157
|
+
select(this).interrupt();
|
|
158
|
+
})
|
|
159
|
+
.transition()
|
|
160
|
+
.duration(TRANSITION_MS)
|
|
161
|
+
.attr('opacity', 0)
|
|
162
|
+
.remove(),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
node
|
|
166
|
+
.transition()
|
|
167
|
+
.duration(TRANSITION_MS)
|
|
168
|
+
.attr('opacity', 1)
|
|
169
|
+
.attr('transform', (d) => `translate(${d.x},${d.y})`);
|
|
170
|
+
|
|
171
|
+
node
|
|
172
|
+
.select<SVGRectElement>('rect')
|
|
173
|
+
.attr('width', rectSize)
|
|
174
|
+
.attr('height', rectSize)
|
|
175
|
+
.attr('rx', radius)
|
|
176
|
+
.attr('ry', radius)
|
|
177
|
+
.style('fill', (d) => getNodeFillForObject(d.object))
|
|
178
|
+
.on('pointerenter', (event: MouseEvent, d: Positioned) =>
|
|
179
|
+
onNodeHover({ id: d.id, label: d.label, data: d.object }, event),
|
|
180
|
+
)
|
|
181
|
+
.on('pointerleave', () => onNodeHover(null));
|
|
182
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { withPluginManager } from '@dxos/app-framework/testing';
|
|
10
|
+
import { Query } from '@dxos/echo';
|
|
11
|
+
import { ClientPlugin, initializeIdentity } from '@dxos/plugin-client/testing';
|
|
12
|
+
import { corePlugins } from '@dxos/plugin-testing';
|
|
13
|
+
import { random } from '@dxos/random';
|
|
14
|
+
import { useSpaces } from '@dxos/react-client/echo';
|
|
15
|
+
import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
16
|
+
import { type ValueGenerator } from '@dxos/schema/testing';
|
|
17
|
+
import { HasConnection, Organization, Person } from '@dxos/types';
|
|
18
|
+
|
|
19
|
+
import { buildOrgHierarchy, connectionsToEdges, generateConnectedOrgs } from '../../testing';
|
|
20
|
+
import { Tree, type TreeComponentProps } from './Tree';
|
|
21
|
+
import { type TreeNode } from './types';
|
|
22
|
+
|
|
23
|
+
const generator = random as any as ValueGenerator;
|
|
24
|
+
|
|
25
|
+
random.seed(42);
|
|
26
|
+
|
|
27
|
+
const DefaultStory = ({ variant = 'edge', tension }: { variant?: TreeComponentProps['variant']; tension?: number }) => {
|
|
28
|
+
const [space] = useSpaces();
|
|
29
|
+
const [{ data, edges }, setState] = useState<{ data?: TreeNode; edges: TreeComponentProps['edges'] }>({
|
|
30
|
+
data: undefined,
|
|
31
|
+
edges: [],
|
|
32
|
+
});
|
|
33
|
+
const [hovered, setHovered] = useState<TreeNode | null>(null);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!space) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let cancelled = false;
|
|
40
|
+
void (async () => {
|
|
41
|
+
const orgs = await space.db.query(Query.type(Organization.Organization)).run();
|
|
42
|
+
if (cancelled || !orgs.length) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const connections = await space.db.query(Query.type(HasConnection.HasConnection)).run();
|
|
46
|
+
if (cancelled) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
setState({
|
|
50
|
+
data: buildOrgHierarchy(orgs as any),
|
|
51
|
+
edges: connectionsToEdges(connections as any),
|
|
52
|
+
});
|
|
53
|
+
})();
|
|
54
|
+
return () => {
|
|
55
|
+
cancelled = true;
|
|
56
|
+
};
|
|
57
|
+
}, [space]);
|
|
58
|
+
|
|
59
|
+
const slots = useMemo<TreeComponentProps['slots']>(
|
|
60
|
+
() => ({
|
|
61
|
+
node: 'fill-neutral-700 dark:fill-neutral-300',
|
|
62
|
+
path: 'stroke-orange-400/40 dark:stroke-orange-500/40',
|
|
63
|
+
text: 'fill-neutral-700 dark:fill-neutral-200 text-xs',
|
|
64
|
+
}),
|
|
65
|
+
[],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (!space || !data) {
|
|
69
|
+
return <Loading data={{ space: !!space, data: !!data }} />;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className='relative flex h-full w-full'>
|
|
74
|
+
<Tree
|
|
75
|
+
data={data}
|
|
76
|
+
edges={edges}
|
|
77
|
+
variant={variant}
|
|
78
|
+
slots={slots}
|
|
79
|
+
onNodeHover={setHovered}
|
|
80
|
+
// Pass-through; only `edge` variant uses `edges`/`onNodeHover`.
|
|
81
|
+
/>
|
|
82
|
+
{hovered && (
|
|
83
|
+
<div className='pointer-events-none absolute left-2 top-2 rounded bg-neutral-900/80 px-2 py-1 text-xs text-white dark:bg-white/80 dark:text-neutral-900'>
|
|
84
|
+
{hovered.label ?? hovered.id}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const meta = {
|
|
92
|
+
title: 'plugins/plugin-explorer/components/EdgeBundling',
|
|
93
|
+
component: Tree as any,
|
|
94
|
+
render: DefaultStory,
|
|
95
|
+
decorators: [
|
|
96
|
+
withTheme(),
|
|
97
|
+
withLayout({ layout: 'fullscreen' }),
|
|
98
|
+
withPluginManager({
|
|
99
|
+
plugins: [
|
|
100
|
+
...corePlugins(),
|
|
101
|
+
ClientPlugin({
|
|
102
|
+
types: [Organization.Organization, Person.Person, HasConnection.HasConnection],
|
|
103
|
+
onClientInitialized: ({ client }) =>
|
|
104
|
+
Effect.gen(function* () {
|
|
105
|
+
const { personalSpace } = yield* initializeIdentity(client);
|
|
106
|
+
yield* Effect.promise(() =>
|
|
107
|
+
generateConnectedOrgs(personalSpace, generator, {
|
|
108
|
+
organizationCount: 16,
|
|
109
|
+
personCount: 24,
|
|
110
|
+
connectionCount: 22,
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
yield* Effect.promise(() => personalSpace.db.flush({ indexes: true }));
|
|
114
|
+
}),
|
|
115
|
+
}),
|
|
116
|
+
],
|
|
117
|
+
}),
|
|
118
|
+
],
|
|
119
|
+
parameters: {
|
|
120
|
+
layout: 'fullscreen',
|
|
121
|
+
},
|
|
122
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
123
|
+
|
|
124
|
+
export default meta;
|
|
125
|
+
|
|
126
|
+
type Story = StoryObj<typeof meta>;
|
|
127
|
+
|
|
128
|
+
export const Default: Story = {
|
|
129
|
+
args: {
|
|
130
|
+
variant: 'edge',
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const Tidy: Story = {
|
|
135
|
+
args: {
|
|
136
|
+
variant: 'tidy',
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const Radial: Story = {
|
|
141
|
+
args: {
|
|
142
|
+
variant: 'radial',
|
|
143
|
+
},
|
|
144
|
+
};
|
|
@@ -3,76 +3,55 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
-
import React, {
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
7
|
|
|
8
|
-
import { invariant } from '@dxos/invariant';
|
|
9
8
|
import { random } from '@dxos/random';
|
|
10
|
-
import {
|
|
11
|
-
import { type ClientRepeatedComponentProps, ClientRepeater } from '@dxos/react-client/testing';
|
|
12
|
-
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
13
|
-
import { withRegistry } from '@dxos/storybook-utils';
|
|
9
|
+
import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
14
10
|
|
|
11
|
+
import { createTree } from '../../testing';
|
|
15
12
|
import { Tree, type TreeComponentProps } from './Tree';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
// TODO(burdon): Storybook for Graph/Tree/Plot (generics); incl. GraphModel.
|
|
19
|
-
// TODO(burdon): Type for all Explorer components (Space, Object, Query, etc.) incl.
|
|
13
|
+
import { treeTypeToTreeNode } from './types';
|
|
20
14
|
|
|
21
15
|
random.seed(1);
|
|
22
16
|
|
|
23
|
-
type
|
|
24
|
-
|
|
25
|
-
const Component = ({ type }: ComponentProps) => {
|
|
26
|
-
const client = useClient();
|
|
27
|
-
const space = client.spaces.get()[0];
|
|
28
|
-
invariant(space, 'Tree story requires at least one space');
|
|
29
|
-
const [object, setObject] = useState<TreeType>();
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
setTimeout(() => {
|
|
32
|
-
const tree = space.db.add(TreeModel.create());
|
|
33
|
-
setObject(tree);
|
|
34
|
-
});
|
|
35
|
-
}, []);
|
|
17
|
+
type StoryArgs = Pick<TreeComponentProps, 'variant'>;
|
|
36
18
|
|
|
37
|
-
|
|
38
|
-
|
|
19
|
+
const DefaultStory = ({ variant }: StoryArgs) => {
|
|
20
|
+
const data = useMemo(() => treeTypeToTreeNode(createTree([3, [2, 4], [1, 3]]).tree), []);
|
|
21
|
+
if (!data) {
|
|
22
|
+
return <Loading />;
|
|
39
23
|
}
|
|
40
24
|
|
|
41
|
-
return <Tree
|
|
25
|
+
return <Tree data={data} variant={variant} />;
|
|
42
26
|
};
|
|
43
27
|
|
|
44
|
-
const
|
|
45
|
-
return <ClientRepeater component={Component} types={[TreeType]} createSpace />;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const meta = {
|
|
28
|
+
const meta: Meta<StoryArgs> = {
|
|
49
29
|
title: 'plugins/plugin-explorer/components/Tree',
|
|
50
|
-
component: Tree as any,
|
|
51
30
|
render: DefaultStory,
|
|
52
|
-
decorators: [
|
|
31
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
53
32
|
parameters: {
|
|
54
33
|
layout: 'fullscreen',
|
|
55
34
|
},
|
|
56
|
-
}
|
|
35
|
+
};
|
|
57
36
|
|
|
58
37
|
export default meta;
|
|
59
38
|
|
|
60
|
-
type Story = StoryObj<
|
|
39
|
+
type Story = StoryObj<StoryArgs>;
|
|
61
40
|
|
|
62
41
|
export const Tidy: Story = {
|
|
63
42
|
args: {
|
|
64
|
-
|
|
43
|
+
variant: 'tidy',
|
|
65
44
|
},
|
|
66
45
|
};
|
|
67
46
|
|
|
68
47
|
export const Radial: Story = {
|
|
69
48
|
args: {
|
|
70
|
-
|
|
49
|
+
variant: 'radial',
|
|
71
50
|
},
|
|
72
51
|
};
|
|
73
52
|
|
|
74
53
|
export const Edge: Story = {
|
|
75
54
|
args: {
|
|
76
|
-
|
|
55
|
+
variant: 'edge',
|
|
77
56
|
},
|
|
78
57
|
};
|
|
@@ -2,111 +2,80 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import React, { useContext, useEffect, useRef, useState } from 'react';
|
|
5
|
+
import React, { useMemo } from 'react';
|
|
7
6
|
|
|
8
|
-
import { type
|
|
9
|
-
import { useAsyncState } from '@dxos/react-ui';
|
|
10
|
-
import { SVG, type SVGContext } from '@dxos/react-ui-graph';
|
|
11
|
-
import { SpaceGraphModel } from '@dxos/schema';
|
|
7
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
12
8
|
|
|
13
|
-
import { HierarchicalEdgeBundling, RadialTree, TidyTree } from './layout';
|
|
14
|
-
import { type TreeNode
|
|
15
|
-
|
|
16
|
-
// TODO(burdon): Create dge bundling graph using d3.hierarchy.
|
|
17
|
-
// https://observablehq.com/@d3/hierarchical-edge-bundling?intent=fork
|
|
18
|
-
|
|
19
|
-
type Renderer = (svg: SVGSVGElement, data: any, options: any) => void;
|
|
9
|
+
import { type BundleEdge, HierarchicalEdgeBundling, RadialTree, TidyTree, type TreeLayoutSlots } from './layout';
|
|
10
|
+
import { type TreeNode } from './types';
|
|
20
11
|
|
|
21
12
|
export type LayoutVariant = 'tidy' | 'radial' | 'edge';
|
|
22
13
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
text?: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type TreeOptions = {
|
|
31
|
-
label: (d: any) => string;
|
|
32
|
-
|
|
33
|
-
slots?: TreeLayoutSlots;
|
|
34
|
-
radius?: number;
|
|
35
|
-
|
|
36
|
-
width: number;
|
|
37
|
-
height: number;
|
|
38
|
-
margin?: number;
|
|
39
|
-
|
|
40
|
-
padding?: number;
|
|
41
|
-
// Radius of nodes.
|
|
42
|
-
r?: number;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export const defaultTreeLayoutSlots: TreeLayoutSlots = {
|
|
46
|
-
node: 'fill-blue-600',
|
|
47
|
-
path: 'fill-none stroke-blue-400 stroke-[0.5px]',
|
|
48
|
-
text: 'stroke-[0.5px] stroke-neutral-700 text-xs', // TODO(burdon): Create box instead of halo.
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const renderers = new Map<LayoutVariant, Renderer>([
|
|
52
|
-
['tidy', TidyTree],
|
|
53
|
-
['radial', RadialTree],
|
|
54
|
-
['edge', HierarchicalEdgeBundling],
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
|
-
export type TreeComponentProps<N = unknown> = {
|
|
58
|
-
space: Space;
|
|
59
|
-
selected?: string;
|
|
14
|
+
export type TreeComponentProps = ThemedClassName<{
|
|
15
|
+
data: TreeNode;
|
|
16
|
+
/** Optional edges for the `edge` variant (hierarchical edge bundling). */
|
|
17
|
+
edges?: BundleEdge[];
|
|
60
18
|
variant?: LayoutVariant;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
19
|
+
label?: (node: TreeNode) => string;
|
|
20
|
+
slots?: TreeLayoutSlots;
|
|
21
|
+
initialCollapsed?: Iterable<string>;
|
|
22
|
+
onNodeClick?: (node: TreeNode) => void;
|
|
23
|
+
onNodeHover?: (node: TreeNode | null, event?: MouseEvent) => void;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Tree visualization wrapping the three layout variants.
|
|
28
|
+
* - `tidy` — horizontal tidy tree (collapsible)
|
|
29
|
+
* - `radial` — radial tree (collapsible)
|
|
30
|
+
* - `edge` — hierarchical edge bundling (`edges` connect leaves)
|
|
31
|
+
*/
|
|
32
|
+
export const Tree = ({
|
|
33
|
+
classNames,
|
|
34
|
+
data,
|
|
35
|
+
edges,
|
|
36
|
+
variant = 'tidy',
|
|
37
|
+
label,
|
|
38
|
+
slots,
|
|
39
|
+
initialCollapsed,
|
|
40
|
+
onNodeClick,
|
|
41
|
+
onNodeHover,
|
|
42
|
+
}: TreeComponentProps) => {
|
|
43
|
+
return useMemo(() => {
|
|
44
|
+
switch (variant) {
|
|
45
|
+
case 'tidy':
|
|
46
|
+
return (
|
|
47
|
+
<TidyTree
|
|
48
|
+
classNames={classNames}
|
|
49
|
+
data={data}
|
|
50
|
+
label={label}
|
|
51
|
+
slots={slots}
|
|
52
|
+
initialCollapsed={initialCollapsed}
|
|
53
|
+
onNodeClick={onNodeClick}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
case 'radial':
|
|
57
|
+
return (
|
|
58
|
+
<RadialTree
|
|
59
|
+
classNames={classNames}
|
|
60
|
+
data={data}
|
|
61
|
+
label={label}
|
|
62
|
+
slots={slots}
|
|
63
|
+
initialCollapsed={initialCollapsed}
|
|
64
|
+
onNodeClick={onNodeClick}
|
|
65
|
+
onNodeHover={onNodeHover}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
case 'edge':
|
|
69
|
+
return (
|
|
70
|
+
<HierarchicalEdgeBundling
|
|
71
|
+
classNames={classNames}
|
|
72
|
+
data={data}
|
|
73
|
+
edges={edges ?? []}
|
|
74
|
+
label={label}
|
|
75
|
+
slots={slots}
|
|
76
|
+
onNodeHover={onNodeHover}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
104
79
|
}
|
|
105
|
-
}, [
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<div className='grow' onClick={() => onNodeClick?.()}>
|
|
109
|
-
<SVG.Root ref={context} />
|
|
110
|
-
</div>
|
|
111
|
-
);
|
|
80
|
+
}, [variant, classNames, data, edges, label, slots, initialCollapsed, onNodeClick, onNodeHover]);
|
|
112
81
|
};
|