@dxos/plugin-explorer 0.8.1 → 0.8.2-main.10c050d

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.
Files changed (170) hide show
  1. package/dist/lib/browser/ExplorerContainer-BBZ54DJS.mjs +37 -0
  2. package/dist/lib/browser/ExplorerContainer-BBZ54DJS.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-B2DMPSP5.mjs → chunk-73GQ46YO.mjs} +430 -170
  4. package/dist/lib/{node-esm/chunk-PZOORWFE.mjs.map → browser/chunk-73GQ46YO.mjs.map} +4 -4
  5. package/dist/lib/browser/{chunk-QLQLPZNI.mjs → chunk-73YTQHOT.mjs} +12 -11
  6. package/dist/lib/browser/chunk-73YTQHOT.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-M2BGAY6H.mjs +177 -0
  8. package/dist/lib/browser/chunk-M2BGAY6H.mjs.map +7 -0
  9. package/dist/lib/browser/{chunk-Z2SDLMQM.mjs → chunk-OBAFAA5V.mjs} +3 -3
  10. package/dist/lib/browser/{chunk-Z2SDLMQM.mjs.map → chunk-OBAFAA5V.mjs.map} +1 -1
  11. package/dist/lib/browser/chunk-SLB2F5AO.mjs +30 -0
  12. package/dist/lib/browser/chunk-SLB2F5AO.mjs.map +7 -0
  13. package/dist/lib/browser/index.mjs +15 -11
  14. package/dist/lib/browser/index.mjs.map +1 -1
  15. package/dist/lib/browser/{intent-resolver-NHHRWTOP.mjs → intent-resolver-FJDVBDE3.mjs} +5 -5
  16. package/dist/lib/browser/intent-resolver-FJDVBDE3.mjs.map +7 -0
  17. package/dist/lib/browser/meta.json +1 -1
  18. package/dist/lib/browser/meta.mjs +1 -1
  19. package/dist/lib/browser/{react-surface-N7TC6BMF.mjs → react-surface-H3YDMXAQ.mjs} +5 -5
  20. package/dist/lib/browser/types/index.mjs +2 -2
  21. package/dist/lib/node/{ExplorerContainer-73AHSBAG.cjs → ExplorerContainer-MVP2AM7R.cjs} +24 -16
  22. package/dist/lib/node/ExplorerContainer-MVP2AM7R.cjs.map +7 -0
  23. package/dist/lib/node/chunk-4T4LCT5R.cjs +52 -0
  24. package/dist/lib/node/chunk-4T4LCT5R.cjs.map +7 -0
  25. package/dist/lib/node/{chunk-HCDBN6NN.cjs → chunk-72H5HBTK.cjs} +429 -168
  26. package/dist/lib/node/chunk-72H5HBTK.cjs.map +7 -0
  27. package/dist/lib/node/{chunk-VB3QE6XY.cjs → chunk-BCDVG2CH.cjs} +6 -6
  28. package/dist/lib/node/{chunk-VB3QE6XY.cjs.map → chunk-BCDVG2CH.cjs.map} +1 -1
  29. package/dist/lib/node/{chunk-YLL7H7CZ.cjs → chunk-MLRYW4WQ.cjs} +15 -14
  30. package/dist/lib/node/chunk-MLRYW4WQ.cjs.map +7 -0
  31. package/dist/lib/node/chunk-NELWWGBU.cjs +204 -0
  32. package/dist/lib/node/chunk-NELWWGBU.cjs.map +7 -0
  33. package/dist/lib/node/index.cjs +34 -31
  34. package/dist/lib/node/index.cjs.map +1 -1
  35. package/dist/lib/node/{intent-resolver-EEOTX3OZ.cjs → intent-resolver-DRT67ZU4.cjs} +8 -8
  36. package/dist/lib/node/intent-resolver-DRT67ZU4.cjs.map +7 -0
  37. package/dist/lib/node/meta.cjs +3 -3
  38. package/dist/lib/node/meta.cjs.map +1 -1
  39. package/dist/lib/node/meta.json +1 -1
  40. package/dist/lib/node/{react-surface-WD4G2NRS.cjs → react-surface-6ESLSM33.cjs} +11 -11
  41. package/dist/lib/node/types/index.cjs +4 -4
  42. package/dist/lib/node/types/index.cjs.map +1 -1
  43. package/dist/lib/node-esm/ExplorerContainer-APGUQI4M.mjs +38 -0
  44. package/dist/lib/node-esm/ExplorerContainer-APGUQI4M.mjs.map +7 -0
  45. package/dist/lib/node-esm/{chunk-PZOORWFE.mjs → chunk-34X2VFQN.mjs} +430 -169
  46. package/dist/lib/{browser/chunk-B2DMPSP5.mjs.map → node-esm/chunk-34X2VFQN.mjs.map} +4 -4
  47. package/dist/lib/node-esm/{chunk-PUFSCMN4.mjs → chunk-3CMBLK6W.mjs} +3 -3
  48. package/dist/lib/node-esm/{chunk-PUFSCMN4.mjs.map → chunk-3CMBLK6W.mjs.map} +1 -1
  49. package/dist/lib/node-esm/{chunk-QZH2GDN5.mjs → chunk-N6VEANUZ.mjs} +12 -11
  50. package/dist/lib/node-esm/chunk-N6VEANUZ.mjs.map +7 -0
  51. package/dist/lib/node-esm/chunk-PVII2K2B.mjs +179 -0
  52. package/dist/lib/node-esm/chunk-PVII2K2B.mjs.map +7 -0
  53. package/dist/lib/node-esm/chunk-VSORIAHH.mjs +32 -0
  54. package/dist/lib/node-esm/chunk-VSORIAHH.mjs.map +7 -0
  55. package/dist/lib/node-esm/index.mjs +15 -11
  56. package/dist/lib/node-esm/index.mjs.map +1 -1
  57. package/dist/lib/node-esm/{intent-resolver-PTFKXAT4.mjs → intent-resolver-4RBV644N.mjs} +5 -5
  58. package/dist/lib/node-esm/intent-resolver-4RBV644N.mjs.map +7 -0
  59. package/dist/lib/node-esm/meta.json +1 -1
  60. package/dist/lib/node-esm/meta.mjs +1 -1
  61. package/dist/lib/node-esm/{react-surface-J5SW7VF2.mjs → react-surface-ZEVL3FXG.mjs} +5 -5
  62. package/dist/lib/node-esm/types/index.mjs +2 -2
  63. package/dist/types/src/components/Chart/Chart.d.ts.map +1 -1
  64. package/dist/types/src/components/ExplorerContainer.d.ts +4 -3
  65. package/dist/types/src/components/ExplorerContainer.d.ts.map +1 -1
  66. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  67. package/dist/types/src/components/Graph/D3ForceGraph.d.ts +14 -0
  68. package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +1 -0
  69. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts +6 -0
  70. package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts.map +1 -0
  71. package/dist/types/src/components/Graph/ForceGraph.d.ts +8 -0
  72. package/dist/types/src/components/Graph/ForceGraph.d.ts.map +1 -0
  73. package/dist/types/src/components/Graph/{Graph.stories.d.ts → ForceGraph.stories.d.ts} +1 -1
  74. package/dist/types/src/components/Graph/ForceGraph.stories.d.ts.map +1 -0
  75. package/dist/types/src/components/Graph/adapter.d.ts +21 -0
  76. package/dist/types/src/components/Graph/adapter.d.ts.map +1 -0
  77. package/dist/types/src/components/Graph/index.d.ts +2 -2
  78. package/dist/types/src/components/Graph/index.d.ts.map +1 -1
  79. package/dist/types/src/components/Graph/testing.d.ts +14 -0
  80. package/dist/types/src/components/Graph/testing.d.ts.map +1 -0
  81. package/dist/types/src/components/Tree/Tree.d.ts.map +1 -1
  82. package/dist/types/src/components/Tree/layout/HierarchicalEdgeBundling.d.ts.map +1 -1
  83. package/dist/types/src/components/Tree/layout/RadialTree.d.ts.map +1 -1
  84. package/dist/types/src/components/Tree/layout/TidyTree.d.ts.map +1 -1
  85. package/dist/types/src/components/Tree/testing/generator.d.ts +8 -0
  86. package/dist/types/src/components/Tree/testing/generator.d.ts.map +1 -0
  87. package/dist/types/src/components/Tree/testing/index.d.ts +2 -0
  88. package/dist/types/src/components/Tree/testing/index.d.ts.map +1 -0
  89. package/dist/types/src/components/Tree/types/index.d.ts +3 -0
  90. package/dist/types/src/components/Tree/types/index.d.ts.map +1 -0
  91. package/dist/types/src/components/Tree/types/tree.d.ts +83 -0
  92. package/dist/types/src/components/Tree/types/tree.d.ts.map +1 -0
  93. package/dist/types/src/components/Tree/types/tree.test.d.ts +2 -0
  94. package/dist/types/src/components/Tree/types/tree.test.d.ts.map +1 -0
  95. package/dist/types/src/components/Tree/types/types.d.ts +8 -0
  96. package/dist/types/src/components/Tree/types/types.d.ts.map +1 -0
  97. package/dist/types/src/components/index.d.ts +2 -2
  98. package/dist/types/src/components/plot.d.ts.map +1 -1
  99. package/dist/types/src/hooks/index.d.ts +2 -0
  100. package/dist/types/src/hooks/index.d.ts.map +1 -0
  101. package/dist/types/src/hooks/useGraphModel.d.ts +4 -0
  102. package/dist/types/src/hooks/useGraphModel.d.ts.map +1 -0
  103. package/dist/types/src/index.d.ts +2 -1
  104. package/dist/types/src/index.d.ts.map +1 -1
  105. package/dist/types/src/translations.d.ts +2 -8
  106. package/dist/types/src/translations.d.ts.map +1 -1
  107. package/dist/types/src/types/schema.d.ts +6 -6
  108. package/dist/types/src/types/schema.d.ts.map +1 -1
  109. package/dist/types/src/types/types.d.ts +6 -6
  110. package/dist/types/src/types/types.d.ts.map +1 -1
  111. package/dist/types/tsconfig.tsbuildinfo +1 -1
  112. package/package.json +30 -27
  113. package/src/capabilities/intent-resolver.ts +2 -2
  114. package/src/components/ExplorerContainer.tsx +11 -4
  115. package/src/components/Globe/Globe.stories.tsx +13 -13
  116. package/src/components/Graph/D3ForceGraph.stories.tsx +64 -0
  117. package/src/components/Graph/D3ForceGraph.tsx +101 -0
  118. package/src/components/Graph/ForceGraph.stories.tsx +64 -0
  119. package/src/components/Graph/{Graph.tsx → ForceGraph.tsx} +19 -26
  120. package/src/components/Graph/adapter.ts +47 -0
  121. package/src/components/Graph/index.ts +2 -3
  122. package/src/components/Graph/testing.ts +57 -0
  123. package/src/components/Tree/Tree.stories.tsx +1 -1
  124. package/src/components/Tree/Tree.tsx +11 -18
  125. package/src/components/Tree/layout/HierarchicalEdgeBundling.ts +17 -19
  126. package/src/components/Tree/layout/RadialTree.ts +5 -7
  127. package/src/components/Tree/layout/TidyTree.ts +5 -6
  128. package/src/components/Tree/testing/generator.ts +46 -0
  129. package/src/components/Tree/testing/index.ts +5 -0
  130. package/src/components/Tree/types/index.ts +6 -0
  131. package/src/components/Tree/types/tree.test.ts +133 -0
  132. package/src/components/Tree/types/tree.ts +287 -0
  133. package/src/components/Tree/types/types.ts +41 -0
  134. package/src/hooks/index.ts +5 -0
  135. package/src/hooks/useGraphModel.ts +35 -0
  136. package/src/index.ts +2 -2
  137. package/src/meta.ts +2 -2
  138. package/src/types/schema.ts +5 -3
  139. package/src/types/types.ts +5 -5
  140. package/dist/lib/browser/ExplorerContainer-HL532ODG.mjs +0 -27
  141. package/dist/lib/browser/ExplorerContainer-HL532ODG.mjs.map +0 -7
  142. package/dist/lib/browser/chunk-QLQLPZNI.mjs.map +0 -7
  143. package/dist/lib/browser/chunk-SBLNE7FL.mjs +0 -205
  144. package/dist/lib/browser/chunk-SBLNE7FL.mjs.map +0 -7
  145. package/dist/lib/browser/intent-resolver-NHHRWTOP.mjs.map +0 -7
  146. package/dist/lib/node/ExplorerContainer-73AHSBAG.cjs.map +0 -7
  147. package/dist/lib/node/chunk-HCDBN6NN.cjs.map +0 -7
  148. package/dist/lib/node/chunk-OIHH6TVE.cjs +0 -236
  149. package/dist/lib/node/chunk-OIHH6TVE.cjs.map +0 -7
  150. package/dist/lib/node/chunk-YLL7H7CZ.cjs.map +0 -7
  151. package/dist/lib/node/intent-resolver-EEOTX3OZ.cjs.map +0 -7
  152. package/dist/lib/node-esm/ExplorerContainer-NMI55PYM.mjs +0 -28
  153. package/dist/lib/node-esm/ExplorerContainer-NMI55PYM.mjs.map +0 -7
  154. package/dist/lib/node-esm/chunk-QZH2GDN5.mjs.map +0 -7
  155. package/dist/lib/node-esm/chunk-SZRRNWYT.mjs +0 -207
  156. package/dist/lib/node-esm/chunk-SZRRNWYT.mjs.map +0 -7
  157. package/dist/lib/node-esm/intent-resolver-PTFKXAT4.mjs.map +0 -7
  158. package/dist/types/src/components/Graph/Graph.d.ts +0 -8
  159. package/dist/types/src/components/Graph/Graph.d.ts.map +0 -1
  160. package/dist/types/src/components/Graph/Graph.stories.d.ts.map +0 -1
  161. package/dist/types/src/components/Graph/graph-model.d.ts +0 -39
  162. package/dist/types/src/components/Graph/graph-model.d.ts.map +0 -1
  163. package/dist/types/src/components/Tree/types.d.ts +0 -8
  164. package/dist/types/src/components/Tree/types.d.ts.map +0 -1
  165. package/src/components/Graph/Graph.stories.tsx +0 -62
  166. package/src/components/Graph/graph-model.ts +0 -193
  167. package/src/components/Tree/types.ts +0 -40
  168. /package/dist/lib/browser/{react-surface-N7TC6BMF.mjs.map → react-surface-H3YDMXAQ.mjs.map} +0 -0
  169. /package/dist/lib/node/{react-surface-WD4G2NRS.cjs.map → react-surface-6ESLSM33.cjs.map} +0 -0
  170. /package/dist/lib/node-esm/{react-surface-J5SW7VF2.mjs.map → react-surface-ZEVL3FXG.mjs.map} +0 -0
@@ -0,0 +1,57 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type AnyLiveObject, live, type Space } from '@dxos/client/echo';
6
+ import { Query, RelationSourceId, RelationTargetId } from '@dxos/echo-schema';
7
+ import { DataType } from '@dxos/schema';
8
+ import { createObjectFactory, type ValueGenerator, type TypeSpec } from '@dxos/schema/testing';
9
+ import { range } from '@dxos/util';
10
+
11
+ const getObject = (objects: AnyLiveObject[]) => objects[Math.floor(Math.random() * objects.length)];
12
+
13
+ const defaultTypes: TypeSpec[] = [
14
+ { type: DataType.Organization, count: 5 },
15
+ { type: DataType.Project, count: 5 },
16
+ { type: DataType.Person, count: 10 },
17
+ ];
18
+
19
+ export type GenerateOptions = {
20
+ spec?: TypeSpec[];
21
+ relations?: {
22
+ count: number;
23
+ kind: string;
24
+ };
25
+ };
26
+
27
+ const defaultRelations: GenerateOptions['relations'] = { count: 10, kind: 'friend' };
28
+
29
+ /**
30
+ * @deprecated Use @dxos/schema.
31
+ */
32
+ export const generate = async (
33
+ space: Space,
34
+ generator: ValueGenerator,
35
+ { spec = defaultTypes, relations = defaultRelations }: GenerateOptions = {},
36
+ ) => {
37
+ const createObjects = createObjectFactory(space.db, generator);
38
+ await createObjects(spec);
39
+
40
+ // Add relations between objects.
41
+ const { objects: contacts } = await space.db.query(Query.type(DataType.Person)).run();
42
+ for (const _ of range(relations.count)) {
43
+ const source = getObject(contacts);
44
+ const target = getObject(contacts);
45
+ if (source.id === target.id) {
46
+ continue;
47
+ }
48
+
49
+ space.db.add(
50
+ live(DataType.HasRelationship, {
51
+ kind: relations.kind,
52
+ [RelationSourceId]: source,
53
+ [RelationTargetId]: target,
54
+ }),
55
+ );
56
+ }
57
+ };
@@ -7,13 +7,13 @@ import '@dxos-theme';
7
7
  import { type Meta } from '@storybook/react';
8
8
  import React, { type FC, useEffect, useState } from 'react';
9
9
 
10
- import { TreeType, Tree as TreeModel } from '@dxos/plugin-outliner/types';
11
10
  import { faker } from '@dxos/random';
12
11
  import { useClient } from '@dxos/react-client';
13
12
  import { type ClientRepeatedComponentProps, ClientRepeater } from '@dxos/react-client/testing';
14
13
  import { withLayout, withTheme } from '@dxos/storybook-utils';
15
14
 
16
15
  import { Tree, type TreeComponentProps } from './Tree';
16
+ import { TreeType, Tree as TreeModel } from './types';
17
17
 
18
18
  // TODO(burdon): Storybook for Graph/Tree/Plot (generics); incl. GraphModel.
19
19
  // TODO(burdon): Type for all Explorer components (Space, Object, Query, etc.) incl.
@@ -2,16 +2,15 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import React, { useEffect, useState } from 'react';
6
- import { useResizeDetector } from 'react-resize-detector';
5
+ import React, { useEffect, useRef, useState } from 'react';
7
6
 
8
7
  import { type Space } from '@dxos/client/echo';
9
- import { createSvgContext, SVG, SVGRoot } from '@dxos/gem-core';
10
8
  import { useAsyncState } from '@dxos/react-ui';
9
+ import { SVG, type SVGContext } from '@dxos/react-ui-graph';
10
+ import { SpaceGraphModel } from '@dxos/schema';
11
11
 
12
12
  import { HierarchicalEdgeBundling, RadialTree, TidyTree } from './layout';
13
13
  import { mapGraphToTreeData, type TreeNode } from './types';
14
- import { SpaceGraphModel } from '../Graph';
15
14
 
16
15
  // TODO(burdon): Create dge bundling graph using d3.hierarchy.
17
16
  // https://observablehq.com/@d3/hierarchical-edge-bundling?intent=fork
@@ -63,10 +62,7 @@ export type TreeComponentProps<N = unknown> = {
63
62
 
64
63
  // TODO(burdon): Label accessor.
65
64
  export const Tree = <N,>({ space, selected, variant = 'tidy', onNodeClick }: TreeComponentProps<N>) => {
66
- const [model] = useAsyncState(
67
- async () => (space ? new SpaceGraphModel().open(space, selected) : undefined),
68
- [space, selected],
69
- );
65
+ const [model] = useAsyncState(async () => (space ? new SpaceGraphModel().open(space) : undefined), [space, selected]);
70
66
 
71
67
  const [tree, setTree] = useState<TreeNode>();
72
68
  useEffect(() => {
@@ -76,11 +72,11 @@ export const Tree = <N,>({ space, selected, variant = 'tidy', onNodeClick }: Tre
76
72
  }, true);
77
73
  }, [model]);
78
74
 
79
- const context = createSvgContext();
80
- const { ref, width = 0, height = 0 } = useResizeDetector();
75
+ const context = useRef<SVGContext>(null);
81
76
 
82
77
  useEffect(() => {
83
- if (width && height) {
78
+ if (context.current) {
79
+ const { width, height } = context.current.size!;
84
80
  const size = Math.min(width, height);
85
81
  const radius = size * 0.4;
86
82
  const options = {
@@ -98,17 +94,14 @@ export const Tree = <N,>({ space, selected, variant = 'tidy', onNodeClick }: Tre
98
94
 
99
95
  if (tree) {
100
96
  const renderer = renderers.get(variant);
101
- renderer?.(context.ref.current!, tree, options);
97
+ renderer?.(context.current!.svg, tree, options);
102
98
  }
103
99
  }
104
- }, [tree, width, height]);
100
+ }, [context.current, tree]);
105
101
 
106
- // TODO(burdon): Provider should expand.
107
102
  return (
108
- <div ref={ref} className='flex grow overflow-hidden' onClick={() => onNodeClick?.()}>
109
- <SVGRoot context={context}>
110
- <SVG />
111
- </SVGRoot>
103
+ <div onClick={() => onNodeClick?.()}>
104
+ <SVG.Root ref={context} />
112
105
  </div>
113
106
  );
114
107
  };
@@ -3,7 +3,7 @@
3
3
  // Copyright 2022 Observable, Inc.
4
4
  //
5
5
 
6
- import * as d3 from 'd3';
6
+ import { cluster, curveBundle, hierarchy, lineRadial, select } from 'd3';
7
7
  import { type HierarchyNode } from 'd3-hierarchy';
8
8
 
9
9
  import { type TreeOptions } from '../Tree';
@@ -17,16 +17,16 @@ const getId = (node: HierarchyNode<TreeNode>): string =>
17
17
  // https://github.com/d3/d3-hierarchy
18
18
  // https://observablehq.com/@d3/hierarchical-edge-bundling?intent=fork
19
19
  const HierarchicalEdgeBundling = (s: SVGSVGElement, data: TreeNode, options: TreeOptions) => {
20
- const svg = d3.select(s);
20
+ const svg = select(s);
21
21
  svg.selectAll('*').remove();
22
22
 
23
23
  const { radius = 600, padding = 100, slots } = options;
24
24
 
25
25
  // https://d3js.org/d3-hierarchy/hierarchy
26
- const root = d3.hierarchy(flatten(data));
27
- // .sort((a, b) => d3.ascending(a.height, b.height) || d3.ascending(getName(a.data), getName(b.data)));
26
+ const root = hierarchy(flatten(data));
27
+ // .sort((a, b) => ascending(a.height, b.height) || ascending(getName(a.data), getName(b.data)));
28
28
 
29
- const tree = d3.cluster<TreeNode>().size([2 * Math.PI, radius - padding]);
29
+ const tree = cluster<TreeNode>().size([2 * Math.PI, radius - padding]);
30
30
  const layout = tree(addLinks(root));
31
31
 
32
32
  // eslint-disable-next-line unused-imports/no-unused-vars
@@ -54,9 +54,8 @@ const HierarchicalEdgeBundling = (s: SVGSVGElement, data: TreeNode, options: Tre
54
54
  );
55
55
 
56
56
  // https://d3js.org/d3-shape/radial-line
57
- const line = d3
58
- .lineRadial()
59
- .curve(d3.curveBundle.beta(0.85))
57
+ const line = lineRadial()
58
+ .curve(curveBundle.beta(0.85))
60
59
  .radius((d: any) => d.y)
61
60
  .angle((d: any) => d.x);
62
61
 
@@ -77,30 +76,30 @@ const HierarchicalEdgeBundling = (s: SVGSVGElement, data: TreeNode, options: Tre
77
76
 
78
77
  // function overed(event: any, d: X) {
79
78
  // link.style('mix-blend-mode', null);
80
- // d3.select(this).attr('font-weight', 'bold');
81
- // d3.selectAll(d.incoming.map((d) => d.path))
79
+ // select(this).attr('font-weight', 'bold');
80
+ // selectAll(d.incoming.map((d) => d.path))
82
81
  // .attr('stroke', color.in)
83
82
  // .raise();
84
- // d3.selectAll((d as any).incoming.map(([d]) => d.text))
83
+ // selectAll((d as any).incoming.map(([d]) => d.text))
85
84
  // .attr('fill', color.in)
86
85
  // .attr('font-weight', 'bold');
87
- // d3.selectAll(d.outgoing.map((d) => d.path))
86
+ // selectAll(d.outgoing.map((d) => d.path))
88
87
  // .attr('stroke', color.out)
89
88
  // .raise();
90
- // d3.selectAll(d.outgoing.map(([, d]) => d.text))
89
+ // selectAll(d.outgoing.map(([, d]) => d.text))
91
90
  // .attr('fill', color.out)
92
91
  // .attr('font-weight', 'bold');
93
92
  // }
94
93
 
95
94
  // function outed(event: any, d: HierarchyNode<Datum>) {
96
95
  // // @ts-ignore
97
- // d3.select(this).attr('font-weight', null);
98
- // d3.selectAll(d.incoming.map((d) => d.path)).attr('stroke', null);
99
- // d3.selectAll(d.incoming.map(([d]) => d.text))
96
+ // select(this).attr('font-weight', null);
97
+ // selectAll(d.incoming.map((d) => d.path)).attr('stroke', null);
98
+ // selectAll(d.incoming.map(([d]) => d.text))
100
99
  // .attr('fill', null)
101
100
  // .attr('font-weight', null);
102
- // d3.selectAll(d.outgoing.map((d) => d.path)).attr('stroke', null);
103
- // d3.selectAll(d.outgoing.map(([, d]) => d.text))
101
+ // selectAll(d.outgoing.map((d) => d.path)).attr('stroke', null);
102
+ // selectAll(d.outgoing.map(([, d]) => d.text))
104
103
  // .attr('fill', null)
105
104
  // .attr('font-weight', null);
106
105
  // }
@@ -119,7 +118,6 @@ const addLinks = (root: HierarchyNode<TreeNode>) => {
119
118
 
120
119
  for (const d of root.leaves()) {
121
120
  // (d as any).incoming = [];
122
-
123
121
  const parent = parents.get(d.data.id);
124
122
  if (parent) {
125
123
  // Skip the first node which is a placeholder created by flatten().
@@ -3,7 +3,7 @@
3
3
  // Copyright 2022 Observable, Inc.
4
4
  //
5
5
 
6
- import * as d3 from 'd3';
6
+ import { hierarchy, linkRadial, select, tree } from 'd3';
7
7
 
8
8
  import { type TreeOptions } from '../Tree';
9
9
 
@@ -11,7 +11,7 @@ import { type TreeOptions } from '../Tree';
11
11
  // https://observablehq.com/@d3/radial-tree
12
12
  // https://observablehq.com/@d3/tree
13
13
  const RadialTree = (s: SVGSVGElement, data: any, options: TreeOptions) => {
14
- const svg = d3.select(s);
14
+ const svg = select(s);
15
15
  svg.selectAll('*').remove();
16
16
 
17
17
  const {
@@ -23,7 +23,7 @@ const RadialTree = (s: SVGSVGElement, data: any, options: TreeOptions) => {
23
23
 
24
24
  const arc = 2 * Math.PI;
25
25
 
26
- const root = d3.hierarchy(data);
26
+ const root = hierarchy(data);
27
27
 
28
28
  // Sort the nodes.
29
29
  // if (sort) {
@@ -35,8 +35,7 @@ const RadialTree = (s: SVGSVGElement, data: any, options: TreeOptions) => {
35
35
  const getLabel = label === null ? null : descendants.map((d) => label(d.data));
36
36
 
37
37
  // Compute the layout.
38
- const layout = d3
39
- .tree()
38
+ const layout = tree()
40
39
  .size([arc, radius])
41
40
  .separation((a: any, b: any) => (a.parent === b.parent ? 1 : 2) / a.depth);
42
41
  layout(root);
@@ -50,8 +49,7 @@ const RadialTree = (s: SVGSVGElement, data: any, options: TreeOptions) => {
50
49
  .attr('class', slots?.path ?? '')
51
50
  .attr(
52
51
  'd',
53
- d3
54
- .linkRadial()
52
+ linkRadial()
55
53
  .angle((d: any) => d.x + Math.PI / 2)
56
54
  .radius((d: any) => d.y) as any,
57
55
  );
@@ -3,19 +3,19 @@
3
3
  // Copyright 2021 Observable, Inc.
4
4
  //
5
5
 
6
- import * as d3 from 'd3';
6
+ import { curveBumpX, hierarchy, link, select, tree } from 'd3';
7
7
 
8
8
  import { type TreeOptions } from '../Tree';
9
9
 
10
10
  // Released under the ISC license.
11
11
  // https://observablehq.com/@d3/tree
12
12
  const TidyTree = (s: SVGSVGElement, data: any, options: TreeOptions) => {
13
- const svg = d3.select(s);
13
+ const svg = select(s);
14
14
  svg.selectAll('*').remove();
15
15
 
16
16
  const { label, width, height, r = 4, padding = 4, margin = 60, slots } = options;
17
17
 
18
- const root = d3.hierarchy(data);
18
+ const root = hierarchy(data);
19
19
 
20
20
  // Compute labels and titles.
21
21
  const descendants = root.descendants();
@@ -24,7 +24,7 @@ const TidyTree = (s: SVGSVGElement, data: any, options: TreeOptions) => {
24
24
  // Compute the layout.
25
25
  const dx = 16;
26
26
  const dy = width / (root.height + padding);
27
- const layout = d3.tree().nodeSize([dx, dy]);
27
+ const layout = tree().nodeSize([dx, dy]);
28
28
  layout(root);
29
29
 
30
30
  // Center the tree.
@@ -61,8 +61,7 @@ const TidyTree = (s: SVGSVGElement, data: any, options: TreeOptions) => {
61
61
  .attr('class', slots?.path ?? '')
62
62
  .attr(
63
63
  'd',
64
- d3
65
- .link(d3.curveBumpX)
64
+ link(curveBumpX)
66
65
  .x((d: any) => d.y + oy)
67
66
  .y((d: any) => d.x * sx) as any,
68
67
  );
@@ -0,0 +1,46 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { ObjectId } from '@dxos/echo-schema';
6
+ import { range } from '@dxos/util';
7
+
8
+ import { Tree, type TreeNodeType } from '../types';
9
+
10
+ type NumberOrNumberArray = number | number[];
11
+
12
+ const random = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
13
+
14
+ /**
15
+ * Create hierarchical tree.
16
+ */
17
+ export const createTree = (spec: NumberOrNumberArray[] = [], createText?: () => string): Tree => {
18
+ const tree = new Tree();
19
+ tree.root.data = { text: 'root' };
20
+
21
+ const createNodes = (parent: TreeNodeType, spec: NumberOrNumberArray = 0): TreeNodeType[] => {
22
+ const count = Array.isArray(spec) ? random(spec[0], spec[1]) : spec;
23
+ return range(count, (i) => ({
24
+ id: ObjectId.random(),
25
+ children: [],
26
+ data: {
27
+ text: createText?.() ?? [parent.data.text, i + 1].join('.'),
28
+ },
29
+ }));
30
+ };
31
+
32
+ const createChildNodes = (parent: TreeNodeType, [count = 0, ...rest]: NumberOrNumberArray[]): TreeNodeType => {
33
+ const nodes = createNodes(parent, count);
34
+ nodes.forEach((n) => tree.addNode(parent, n));
35
+ if (rest.length) {
36
+ for (const node of nodes) {
37
+ createChildNodes(node, rest);
38
+ }
39
+ }
40
+
41
+ return parent;
42
+ };
43
+
44
+ createChildNodes(tree.root, spec);
45
+ return tree;
46
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './generator';
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './tree';
6
+ export * from './types';
@@ -0,0 +1,133 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { describe, test } from 'vitest';
6
+
7
+ import { live, makeRef } from '@dxos/live-object';
8
+ import { faker } from '@dxos/random';
9
+ import { DataType } from '@dxos/schema';
10
+
11
+ import { type Tree } from './tree';
12
+ import { createTree } from '../testing';
13
+
14
+ faker.seed(0);
15
+
16
+ const print = (tree: Tree) => {
17
+ let count = 0;
18
+ tree.tranverse((node, i) => {
19
+ console.log(''.padStart(i * 2, ' '), node.data);
20
+ count++;
21
+ });
22
+
23
+ return count;
24
+ };
25
+
26
+ describe('tree', () => {
27
+ test('tree', ({ expect }) => {
28
+ {
29
+ const tree = createTree();
30
+ let count = 0;
31
+ tree.tranverse(() => {
32
+ count++;
33
+ });
34
+ expect(count).to.eq(tree.size);
35
+ expect(count).to.eq(1);
36
+ }
37
+ {
38
+ const tree = createTree([10]);
39
+ let count = 0;
40
+ tree.tranverse(() => {
41
+ count++;
42
+ });
43
+ expect(count).to.eq(tree.size);
44
+ expect(count).to.eq(1 + 10);
45
+ }
46
+ {
47
+ const tree = createTree([10, 3, 1]);
48
+ let count = 0;
49
+ tree.tranverse(() => {
50
+ count++;
51
+ });
52
+ expect(count).to.eq(tree.size);
53
+ expect(count).to.eq(1 + 10 * (1 + 3 * (1 + 1))); // 71
54
+ }
55
+ });
56
+
57
+ test('tree navigation', ({ expect }) => {
58
+ const tree = createTree([2, 3, 1]);
59
+ expect(tree.getParent(tree.root)).to.be.null;
60
+
61
+ const nodes = tree.getChildNodes(tree.root);
62
+ expect(nodes).to.have.length(2);
63
+
64
+ const first = nodes[0];
65
+ expect(first.children).to.have.length(3);
66
+ const parent = tree.getParent(first);
67
+ expect(parent).to.eq(tree.root);
68
+
69
+ const [c1, c2, c3] = tree.getChildNodes(first);
70
+ expect(tree.getParent(c1)).to.eq(first);
71
+ expect(tree.getParent(c2)).to.eq(first);
72
+ expect(tree.getParent(c3)).to.eq(first);
73
+
74
+ const [g1] = tree.getChildNodes(c1);
75
+ expect(tree.getParent(g1)).to.eq(c1);
76
+ });
77
+
78
+ /**
79
+ * root root root
80
+ * └── 1 └── 1 └── 1
81
+ * ├── 1.1 ├── 1.1 ├── 1.1
82
+ * ├── 1.2 <- indent │ ├── 1.2 <- unindent ├── 1.2
83
+ * ├── 1.3 <- indent │ └── 1.3 │ └── 1.3
84
+ * ├── 1.4 ├── 1.4 ├── 1.4
85
+ * └── 1.5 └── 1.5 └── 1.5
86
+ */
87
+ test('indent and unindent', async ({ expect }) => {
88
+ const tree = createTree([1, 5]);
89
+ const parent = tree.getNode(tree.root.children[0]);
90
+
91
+ {
92
+ const count = print(tree);
93
+ expect(count).to.eq(1 + 1 * (1 + 5));
94
+ }
95
+
96
+ // Indent
97
+ {
98
+ const child = tree.getChildNodes(parent);
99
+ tree.indentNode(child[1]);
100
+ tree.indentNode(child[2]);
101
+ expect(parent.children).to.have.length(3);
102
+ expect(child[0].children).to.have.length(2);
103
+ }
104
+
105
+ {
106
+ const count = print(tree);
107
+ expect(count).to.eq(1 + 1 * (1 + 5));
108
+ }
109
+
110
+ // Unindent
111
+ {
112
+ const child = tree.getChildNodes(parent);
113
+ const grandchild = tree.getChildNodes(child[0]);
114
+ tree.unindentNode(grandchild[0]);
115
+ expect(grandchild[0].children).to.have.length(1);
116
+ expect(parent.children).to.have.length(4);
117
+ }
118
+
119
+ {
120
+ const count = print(tree);
121
+ expect(count).to.eq(1 + 1 * (1 + 5));
122
+ }
123
+ });
124
+
125
+ test('task', ({ expect }) => {
126
+ const task = live(DataType.Task, { text: 'Test task.' });
127
+ expect(task.text).to.eq('Test task.');
128
+
129
+ const tree = createTree();
130
+ const node = tree.addNode(tree.root);
131
+ node.ref = makeRef(task);
132
+ });
133
+ });