@dxos/plugin-explorer 0.8.4-main.7ace549 → 0.8.4-main.8baae0fced
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/dist/lib/neutral/ExplorerArticle-EAKRB55W.mjs +277 -0
- package/dist/lib/neutral/ExplorerArticle-EAKRB55W.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-7SPMPHRS.mjs +72 -0
- package/dist/lib/neutral/chunk-7SPMPHRS.mjs.map +7 -0
- package/dist/lib/{browser/chunk-UBHZGWZQ.mjs → neutral/chunk-DXIWQFYO.mjs} +3 -5
- package/dist/lib/neutral/chunk-DXIWQFYO.mjs.map +7 -0
- package/dist/lib/neutral/chunk-EM2BV4PF.mjs +290 -0
- package/dist/lib/neutral/chunk-EM2BV4PF.mjs.map +7 -0
- package/dist/lib/neutral/chunk-GRJXLL4Z.mjs +25 -0
- package/dist/lib/neutral/chunk-GRJXLL4Z.mjs.map +7 -0
- package/dist/lib/neutral/chunk-V2OFO6PI.mjs +14 -0
- package/dist/lib/neutral/chunk-V2OFO6PI.mjs.map +7 -0
- package/dist/lib/{browser/chunk-ARBGXQFH.mjs → neutral/components/index.mjs} +858 -307
- 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 +193 -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/{browser → neutral}/types/index.mjs +1 -2
- 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 +3 -1
- 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 +8 -2
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts +3 -2
- 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/CanvasForceGraph.stories.d.ts +17 -0
- 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 +4 -2
- 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} +2 -2
- 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 +18 -16
- 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 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts +9 -0
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.d.ts.map +1 -0
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts +29 -0
- package/dist/types/src/containers/ExplorerArticle/ExplorerArticle.stories.d.ts.map +1 -0
- package/dist/types/src/containers/ExplorerArticle/experimental.stories.d.ts +7 -0
- package/dist/types/src/containers/ExplorerArticle/experimental.stories.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/index.d.ts +3 -0
- package/dist/types/src/containers/index.d.ts.map +1 -0
- 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 +2 -2
- 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 +47 -0
- package/dist/types/src/testing/relations.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +29 -28
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/ExplorerAction.d.ts +0 -17
- package/dist/types/src/types/ExplorerAction.d.ts.map +1 -1
- package/dist/types/src/types/Graph.d.ts +10 -20
- 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 +113 -62
- package/src/ExplorerPlugin.test.ts +26 -0
- package/src/ExplorerPlugin.tsx +15 -45
- package/src/capabilities/create-object.ts +36 -0
- package/src/capabilities/index.ts +3 -3
- package/src/capabilities/react-surface.tsx +24 -18
- package/src/components/Chart/Chart.stories.tsx +16 -23
- package/src/components/Chart/Chart.tsx +1 -1
- package/src/components/Globe/Globe.stories.tsx +19 -22
- package/src/components/Globe/Globe.tsx +1 -1
- package/src/components/Graph/CanvasForceGraph.stories.tsx +83 -0
- package/src/components/Graph/CanvasForceGraph.tsx +124 -0
- package/src/components/Graph/ForceGraph.stories.tsx +83 -44
- package/src/components/Graph/ForceGraph.tsx +104 -85
- package/src/components/Graph/{adapter.ts → graph-adapter.ts} +14 -8
- package/src/components/Graph/index.ts +1 -1
- package/src/components/Lattice/Lattice.stories.tsx +90 -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 +20 -38
- package/src/components/Tree/Tree.tsx +69 -95
- package/src/components/Tree/index.ts +2 -0
- package/src/components/Tree/layout/HierarchicalEdgeBundling.tsx +296 -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 +6 -5
- package/src/components/Tree/types/tree.ts +41 -20
- package/src/components/Tree/types/types.ts +38 -29
- package/src/components/index.ts +1 -4
- package/src/containers/ExplorerArticle/ExplorerArticle.stories.tsx +136 -0
- package/src/containers/ExplorerArticle/ExplorerArticle.tsx +465 -0
- package/src/containers/ExplorerArticle/experimental.stories.tsx +446 -0
- package/src/containers/ExplorerArticle/index.ts +5 -0
- package/src/containers/index.ts +7 -0
- package/src/hooks/useGraphModel.ts +25 -14
- package/src/index.ts +1 -4
- package/src/meta.ts +4 -4
- package/src/plugin.ts +9 -0
- package/src/{components/Tree/testing → testing}/generator.ts +5 -3
- package/src/testing/index.ts +9 -0
- package/src/testing/relations.ts +192 -0
- package/src/translations.ts +14 -13
- package/src/types/ExplorerAction.ts +1 -18
- package/src/types/Graph.ts +13 -28
- package/src/typings.d.ts +8 -0
- package/src/util/index.ts +6 -0
- package/src/util/node-color.ts +23 -0
- package/src/{components → util}/plot.ts +16 -4
- package/dist/lib/browser/ExplorerContainer-NOLLVUTE.mjs +0 -50
- package/dist/lib/browser/ExplorerContainer-NOLLVUTE.mjs.map +0 -7
- package/dist/lib/browser/chunk-2MKBRIUT.mjs +0 -31
- package/dist/lib/browser/chunk-2MKBRIUT.mjs.map +0 -7
- package/dist/lib/browser/chunk-6BVXZQPP.mjs +0 -188
- package/dist/lib/browser/chunk-6BVXZQPP.mjs.map +0 -7
- package/dist/lib/browser/chunk-ARBGXQFH.mjs.map +0 -7
- package/dist/lib/browser/chunk-P6FFFVPM.mjs +0 -100
- package/dist/lib/browser/chunk-P6FFFVPM.mjs.map +0 -7
- package/dist/lib/browser/chunk-UBHZGWZQ.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -112
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/intent-resolver-EWB3H5KH.mjs +0 -35
- package/dist/lib/browser/intent-resolver-EWB3H5KH.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/react-surface-BY2DYCTH.mjs +0 -34
- package/dist/lib/browser/react-surface-BY2DYCTH.mjs.map +0 -7
- package/dist/lib/node-esm/ExplorerContainer-N3S5KSUX.mjs +0 -51
- package/dist/lib/node-esm/ExplorerContainer-N3S5KSUX.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-3ODK27PU.mjs +0 -33
- package/dist/lib/node-esm/chunk-3ODK27PU.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-4BY2XZET.mjs +0 -101
- package/dist/lib/node-esm/chunk-4BY2XZET.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-CRSVAZNA.mjs +0 -190
- package/dist/lib/node-esm/chunk-CRSVAZNA.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
- package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-NPIP4VEH.mjs +0 -11091
- package/dist/lib/node-esm/chunk-NPIP4VEH.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-UXZM5VJB.mjs +0 -26
- package/dist/lib/node-esm/chunk-UXZM5VJB.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -113
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/intent-resolver-SH6PW7VF.mjs +0 -36
- package/dist/lib/node-esm/intent-resolver-SH6PW7VF.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/react-surface-7AAV7GBG.mjs +0 -35
- package/dist/lib/node-esm/react-surface-7AAV7GBG.mjs.map +0 -7
- package/dist/lib/node-esm/types/index.mjs +0 -12
- package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
- package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
- package/dist/types/src/components/ExplorerContainer.d.ts +0 -9
- package/dist/types/src/components/ExplorerContainer.d.ts.map +0 -1
- package/dist/types/src/components/Graph/D3ForceGraph.d.ts +0 -14
- package/dist/types/src/components/Graph/D3ForceGraph.d.ts.map +0 -1
- package/dist/types/src/components/Graph/D3ForceGraph.stories.d.ts +0 -15
- 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/src/capabilities/intent-resolver.ts +0 -23
- package/src/components/ExplorerContainer.tsx +0 -54
- package/src/components/Graph/D3ForceGraph.stories.tsx +0 -80
- package/src/components/Graph/D3ForceGraph.tsx +0 -101
- package/src/components/Graph/testing.ts +0 -55
- 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/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/{browser/types → neutral}/index.mjs.map +0 -0
- /package/dist/lib/{node-esm → neutral}/meta.mjs.map +0 -0
- /package/dist/lib/{node-esm → neutral}/types/index.mjs.map +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
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 from 'react';
|
|
8
|
+
|
|
9
|
+
import { withPluginManager } from '@dxos/app-framework/testing';
|
|
10
|
+
import { Filter, Query, Type, View } from '@dxos/echo';
|
|
11
|
+
import { ClientPlugin, initializeIdentity } from '@dxos/plugin-client/testing';
|
|
12
|
+
import { PreviewPlugin } from '@dxos/plugin-preview/testing';
|
|
13
|
+
import { StorybookPlugin, corePlugins } from '@dxos/plugin-testing';
|
|
14
|
+
import { random } from '@dxos/random';
|
|
15
|
+
import { useQuery, useSpaces } from '@dxos/react-client/echo';
|
|
16
|
+
import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
17
|
+
import { ViewModel } from '@dxos/schema';
|
|
18
|
+
import { type ValueGenerator } from '@dxos/schema/testing';
|
|
19
|
+
import { HasRelationship, Organization, Person, Pipeline } from '@dxos/types';
|
|
20
|
+
|
|
21
|
+
import { generate } from '../../testing';
|
|
22
|
+
import { Graph } from '../../types';
|
|
23
|
+
import { ExplorerArticle, type ExplorerArticleVariant } from './ExplorerArticle';
|
|
24
|
+
|
|
25
|
+
const generator = random as any as ValueGenerator;
|
|
26
|
+
|
|
27
|
+
random.seed(7);
|
|
28
|
+
|
|
29
|
+
type StoryArgs = { variant: ExplorerArticleVariant };
|
|
30
|
+
|
|
31
|
+
const DefaultStory = ({ variant }: StoryArgs) => {
|
|
32
|
+
const [space] = useSpaces();
|
|
33
|
+
const [graph] = useQuery(space?.db, Filter.type(Graph.Graph));
|
|
34
|
+
if (!space || !graph) {
|
|
35
|
+
return <Loading data={{ space: !!space, graph: !!graph }} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return <ExplorerArticle role='article' subject={graph as any} attendableId={graph.id} variant={variant} />;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const meta: Meta<StoryArgs> = {
|
|
42
|
+
title: 'plugins/plugin-explorer/containers/ExplorerArticle',
|
|
43
|
+
render: DefaultStory,
|
|
44
|
+
decorators: [
|
|
45
|
+
withTheme(),
|
|
46
|
+
withLayout({ layout: 'fullscreen' }),
|
|
47
|
+
withPluginManager({
|
|
48
|
+
plugins: [
|
|
49
|
+
...corePlugins(),
|
|
50
|
+
StorybookPlugin({}),
|
|
51
|
+
ClientPlugin({
|
|
52
|
+
types: [
|
|
53
|
+
Graph.Graph,
|
|
54
|
+
View.View,
|
|
55
|
+
HasRelationship.HasRelationship,
|
|
56
|
+
Organization.Organization,
|
|
57
|
+
Pipeline.Pipeline,
|
|
58
|
+
Person.Person,
|
|
59
|
+
],
|
|
60
|
+
onClientInitialized: ({ client }) =>
|
|
61
|
+
Effect.gen(function* () {
|
|
62
|
+
const { personalSpace } = yield* initializeIdentity(client);
|
|
63
|
+
yield* Effect.promise(() => generate(personalSpace, generator));
|
|
64
|
+
const { view } = yield* Effect.promise(() =>
|
|
65
|
+
ViewModel.makeFromDatabase({
|
|
66
|
+
db: personalSpace.db,
|
|
67
|
+
typename: Type.getTypename(Graph.Graph),
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const graph = personalSpace.db.add(
|
|
72
|
+
Graph.make({
|
|
73
|
+
name: 'Root',
|
|
74
|
+
view,
|
|
75
|
+
query: {
|
|
76
|
+
ast: Query.select(Filter.everything()).ast,
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
yield* Effect.promise(() => personalSpace.db.flush({ indexes: true }));
|
|
82
|
+
return graph;
|
|
83
|
+
}),
|
|
84
|
+
}),
|
|
85
|
+
PreviewPlugin(),
|
|
86
|
+
],
|
|
87
|
+
}),
|
|
88
|
+
],
|
|
89
|
+
parameters: {
|
|
90
|
+
layout: 'fullscreen',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export default meta;
|
|
95
|
+
|
|
96
|
+
type Story = StoryObj<StoryArgs>;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Default force-directed view (the production layout).
|
|
100
|
+
*/
|
|
101
|
+
export const Force: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
variant: 'force',
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Radial cluster: every object on the perimeter, grouped by its schema, all under a single database root.
|
|
109
|
+
* Inspired by https://observablehq.com/@d3/radial-cluster.
|
|
110
|
+
*/
|
|
111
|
+
export const Cluster: Story = {
|
|
112
|
+
args: {
|
|
113
|
+
variant: 'cluster',
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Hierarchical edge bundling: same hierarchy as `cluster`, with bundled curves
|
|
119
|
+
* routed through the lowest common ancestor for every relation / ref in the space.
|
|
120
|
+
* Inspired by https://observablehq.com/@d3/hierarchical-edge-bundling.
|
|
121
|
+
*/
|
|
122
|
+
export const Bundle: Story = {
|
|
123
|
+
args: {
|
|
124
|
+
variant: 'bundle',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Lattice: every object as a cell in a square-as-possible CSS grid, sorted by typename so
|
|
130
|
+
* objects of the same type cluster together. Each cell is colored by its typename.
|
|
131
|
+
*/
|
|
132
|
+
export const Lattice: Story = {
|
|
133
|
+
args: {
|
|
134
|
+
variant: 'lattice',
|
|
135
|
+
},
|
|
136
|
+
};
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type AppSurface } from '@dxos/app-toolkit/ui';
|
|
8
|
+
import { type Filter, Obj, type View } from '@dxos/echo';
|
|
9
|
+
import { QueryBuilder } from '@dxos/echo-query';
|
|
10
|
+
import { useObject } from '@dxos/react-client/echo';
|
|
11
|
+
import { DxAnchorActivate, Icon, Panel, Toolbar } from '@dxos/react-ui';
|
|
12
|
+
import { QueryEditor, type QueryEditorProps } from '@dxos/react-ui-components';
|
|
13
|
+
import {
|
|
14
|
+
CLUSTER_NODE_TYPE_GROUP,
|
|
15
|
+
CLUSTER_NODE_TYPE_LEAF,
|
|
16
|
+
CLUSTER_NODE_TYPE_ROOT,
|
|
17
|
+
GraphBundleProjector,
|
|
18
|
+
GraphClusterProjector,
|
|
19
|
+
GraphForceProjector,
|
|
20
|
+
type GraphLayout,
|
|
21
|
+
type GraphLayoutNode,
|
|
22
|
+
GraphLatticeProjector,
|
|
23
|
+
type GraphProjector,
|
|
24
|
+
type RenderNode,
|
|
25
|
+
SVG,
|
|
26
|
+
type SVGContext,
|
|
27
|
+
} from '@dxos/react-ui-graph';
|
|
28
|
+
import { type SpaceGraphEdge, type SpaceGraphNode } from '@dxos/schema';
|
|
29
|
+
// Side-effect import: ExplorerArticle drives `SVG.Graph` directly (previously the CSS
|
|
30
|
+
// was pulled in transitively via `ForceGraph.tsx`, which we no longer use). Without it
|
|
31
|
+
// the `g.dx-edge path` rules — including `fill: none` — never reach the bundle and SVG
|
|
32
|
+
// defaults (stroke: none, fill: black) make every edge invisible.
|
|
33
|
+
import '@dxos/react-ui-graph/styles/graph.css';
|
|
34
|
+
|
|
35
|
+
import { type TreeNode } from '#components';
|
|
36
|
+
import { useGraphModel } from '#hooks';
|
|
37
|
+
|
|
38
|
+
import { getNodeFillForObject } from '../../util/node-color';
|
|
39
|
+
|
|
40
|
+
/** Visualization variants exposed by `ExplorerArticle`. */
|
|
41
|
+
export type ExplorerArticleVariant = 'force' | 'cluster' | 'bundle' | 'lattice';
|
|
42
|
+
|
|
43
|
+
const VARIANTS: { value: ExplorerArticleVariant; icon: string; label: string }[] = [
|
|
44
|
+
{
|
|
45
|
+
value: 'force',
|
|
46
|
+
icon: 'ph--graph--regular',
|
|
47
|
+
label: 'Force-directed',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: 'cluster',
|
|
51
|
+
icon: 'ph--asterisk-simple--regular',
|
|
52
|
+
label: 'Radial cluster',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
value: 'bundle',
|
|
56
|
+
icon: 'ph--circles-three-plus--regular',
|
|
57
|
+
label: 'Edge bundling',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
value: 'lattice',
|
|
61
|
+
icon: 'ph--grid-four--regular',
|
|
62
|
+
label: 'Lattice',
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
export type ExplorerArticleProps = AppSurface.ObjectArticleProps<View.View>;
|
|
67
|
+
|
|
68
|
+
export const ExplorerArticle = ({ role, subject, variant }: ExplorerArticleProps) => {
|
|
69
|
+
const [view] = useObject(subject);
|
|
70
|
+
const db = view && Obj.getDatabase(view);
|
|
71
|
+
const [filter, setFilter] = useState<Filter.Any>();
|
|
72
|
+
const model = useGraphModel(db, filter);
|
|
73
|
+
|
|
74
|
+
const builder = useMemo(() => new QueryBuilder(), []);
|
|
75
|
+
const handleChange = useCallback<NonNullable<QueryEditorProps['onChange']>>((value) => {
|
|
76
|
+
setFilter(builder.build(value).filter);
|
|
77
|
+
}, []);
|
|
78
|
+
|
|
79
|
+
// The `variant` prop is the initial value; user can toggle via the toolbar tabs.
|
|
80
|
+
const [selected, setSelected] = useState<ExplorerArticleVariant>(isVariant(variant) ? variant : 'force');
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (isVariant(variant)) {
|
|
83
|
+
setSelected(variant);
|
|
84
|
+
}
|
|
85
|
+
}, [variant]);
|
|
86
|
+
const handleVariantChange = useCallback((value: string) => {
|
|
87
|
+
if (isVariant(value)) {
|
|
88
|
+
setSelected(value);
|
|
89
|
+
}
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const handleHoverPreview = useCallback((node: TreeNode | null, event?: MouseEvent) => {
|
|
93
|
+
if (!node || !event) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const obj = node.data;
|
|
97
|
+
if (!obj || !Obj.isObject(obj)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const dxn = Obj.getDXN(obj)?.toString();
|
|
101
|
+
if (!dxn) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const target = event.target as HTMLElement;
|
|
105
|
+
target.dispatchEvent(
|
|
106
|
+
new DxAnchorActivate({
|
|
107
|
+
dxn,
|
|
108
|
+
label: Obj.getLabel(obj) ?? dxn,
|
|
109
|
+
trigger: target,
|
|
110
|
+
kind: 'card',
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
const showToolbar = role === 'article';
|
|
116
|
+
|
|
117
|
+
if (!db || !model) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<Panel.Root role={role}>
|
|
123
|
+
{showToolbar && (
|
|
124
|
+
<Panel.Toolbar asChild>
|
|
125
|
+
<Toolbar.Root>
|
|
126
|
+
<QueryEditor db={db} onChange={handleChange} />
|
|
127
|
+
<Toolbar.ToggleGroup type='single' value={selected} onValueChange={handleVariantChange}>
|
|
128
|
+
{VARIANTS.map(({ value, icon, label }) => (
|
|
129
|
+
<Toolbar.ToggleGroupItem key={value} value={value} aria-label={label} title={label}>
|
|
130
|
+
<Icon icon={icon} size={4} />
|
|
131
|
+
</Toolbar.ToggleGroupItem>
|
|
132
|
+
))}
|
|
133
|
+
</Toolbar.ToggleGroup>
|
|
134
|
+
</Toolbar.Root>
|
|
135
|
+
</Panel.Toolbar>
|
|
136
|
+
)}
|
|
137
|
+
<Panel.Content>
|
|
138
|
+
<Visualization variant={selected} model={model} onNodeHover={handleHoverPreview} />
|
|
139
|
+
</Panel.Content>
|
|
140
|
+
</Panel.Root>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const isVariant = (value: unknown): value is ExplorerArticleVariant =>
|
|
145
|
+
value === 'force' || value === 'cluster' || value === 'bundle' || value === 'lattice';
|
|
146
|
+
|
|
147
|
+
type VisualizationProps = {
|
|
148
|
+
variant: ExplorerArticleVariant;
|
|
149
|
+
model: NonNullable<ReturnType<typeof useGraphModel>>;
|
|
150
|
+
onNodeHover?: (node: TreeNode | null, event?: MouseEvent) => void;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* One persistent `<SVG.Graph>` mount for all four variants. When the variant
|
|
155
|
+
* changes, a new projector is instantiated and seeded with the previous
|
|
156
|
+
* projector's layout so node x/y survive the swap — the new projector's
|
|
157
|
+
* `animate()` then tweens each node from its current position to the new
|
|
158
|
+
* target, and per-frame edge generators (cluster, bundle) keep curves glued
|
|
159
|
+
* to the moving endpoints.
|
|
160
|
+
*/
|
|
161
|
+
const Visualization = ({ variant, model, onNodeHover }: VisualizationProps) => {
|
|
162
|
+
const svgRef = useRef<SVGContext>(null);
|
|
163
|
+
const [projector, setProjector] = useState<GraphProjector<SpaceGraphNode> | undefined>();
|
|
164
|
+
const projectorRef = useRef<GraphProjector<SpaceGraphNode> | undefined>(undefined);
|
|
165
|
+
projectorRef.current = projector;
|
|
166
|
+
|
|
167
|
+
// Recreate the projector when the variant changes. Pass the previous projector's
|
|
168
|
+
// layout to the constructor so existing node x/y persist across the swap, then
|
|
169
|
+
// the new projector's animate() tweens to its target positions.
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (!svgRef.current) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const prev = projectorRef.current?.layout as GraphLayout<SpaceGraphNode> | undefined;
|
|
175
|
+
setProjector(createProjector(variant, svgRef.current, prev));
|
|
176
|
+
}, [variant]);
|
|
177
|
+
|
|
178
|
+
const renderNode = useMemo(() => createRenderNode(variant), [variant]);
|
|
179
|
+
|
|
180
|
+
const handleInspect = useCallback(
|
|
181
|
+
(node: GraphLayoutNode<SpaceGraphNode> | null, event: MouseEvent) => {
|
|
182
|
+
// null = pointerleave: forward to the shared hover handler so it can clear any preview.
|
|
183
|
+
if (!node) {
|
|
184
|
+
onNodeHover?.(null);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
onNodeHover?.({ id: node.id, data: node.data?.data?.object }, event);
|
|
188
|
+
},
|
|
189
|
+
[onNodeHover],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// Cluster-only: clicking a root / group node toggles its subtree open/closed.
|
|
193
|
+
const handleSelect = useCallback(
|
|
194
|
+
(node: GraphLayoutNode<SpaceGraphNode>) => {
|
|
195
|
+
if (
|
|
196
|
+
variant !== 'cluster' ||
|
|
197
|
+
!node ||
|
|
198
|
+
(node.type !== CLUSTER_NODE_TYPE_ROOT && node.type !== CLUSTER_NODE_TYPE_GROUP)
|
|
199
|
+
) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const cluster = projector as GraphClusterProjector<SpaceGraphNode> | undefined;
|
|
203
|
+
cluster?.toggleCollapsed(node.id);
|
|
204
|
+
},
|
|
205
|
+
[variant, projector],
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Force needs SVG.Zoom (drag interaction). Cluster/lattice don't, AND including the zoom
|
|
209
|
+
// wrapper makes their curve edges render incorrectly in some contexts (see iteration
|
|
210
|
+
// history in graph-cluster-projector.ts). So mount with vs. without zoom conditionally.
|
|
211
|
+
const inner = (
|
|
212
|
+
<SVG.Graph<SpaceGraphNode, SpaceGraphEdge>
|
|
213
|
+
model={model}
|
|
214
|
+
projector={projector}
|
|
215
|
+
renderNode={renderNode}
|
|
216
|
+
drag={variant === 'force'}
|
|
217
|
+
onInspect={handleInspect}
|
|
218
|
+
onSelect={handleSelect}
|
|
219
|
+
/>
|
|
220
|
+
);
|
|
221
|
+
return (
|
|
222
|
+
<SVG.Root ref={svgRef}>{variant === 'force' ? <SVG.Zoom extent={[1 / 2, 2]}>{inner}</SVG.Zoom> : inner}</SVG.Root>
|
|
223
|
+
);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/** Cross-variant tween duration. Matches the renderer's edge fade timing so node
|
|
227
|
+
* movement and edge enter/exit complete together. */
|
|
228
|
+
const TWEEN_MS = 500;
|
|
229
|
+
|
|
230
|
+
const createProjector = (
|
|
231
|
+
variant: ExplorerArticleVariant,
|
|
232
|
+
ctx: SVGContext,
|
|
233
|
+
prev?: GraphLayout<SpaceGraphNode>,
|
|
234
|
+
): GraphProjector<SpaceGraphNode> => {
|
|
235
|
+
switch (variant) {
|
|
236
|
+
case 'force':
|
|
237
|
+
// Force has no `duration` — its own simulation drives motion via ticks.
|
|
238
|
+
return new GraphForceProjector<SpaceGraphNode>(ctx, undefined, undefined, prev);
|
|
239
|
+
case 'lattice':
|
|
240
|
+
return new GraphLatticeProjector<SpaceGraphNode>(
|
|
241
|
+
ctx,
|
|
242
|
+
{
|
|
243
|
+
duration: TWEEN_MS,
|
|
244
|
+
// Plugin-explorer overrides the projector's force-matched default (6)
|
|
245
|
+
// with a smaller node so the lattice reads as a dense matrix.
|
|
246
|
+
radius: 4,
|
|
247
|
+
// Cluster by typename first so same-type rects sit together; break ties by label.
|
|
248
|
+
sortBy: (node: GraphLayoutNode<SpaceGraphNode>) => {
|
|
249
|
+
const obj = node.data?.data?.object;
|
|
250
|
+
const typename = obj ? (Obj.getTypename(obj) ?? '(untyped)') : '(untyped)';
|
|
251
|
+
const label = (obj && Obj.getLabel(obj)) ?? node.data?.data?.label ?? node.id;
|
|
252
|
+
return `${typename} ${label}`;
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
undefined,
|
|
256
|
+
prev,
|
|
257
|
+
);
|
|
258
|
+
case 'cluster':
|
|
259
|
+
return new GraphClusterProjector<SpaceGraphNode>(
|
|
260
|
+
ctx,
|
|
261
|
+
{
|
|
262
|
+
duration: TWEEN_MS,
|
|
263
|
+
groupOf: typenameGroupOf,
|
|
264
|
+
rootLabel: 'Database',
|
|
265
|
+
groupLabel: shortTypename,
|
|
266
|
+
// All three node kinds share the same radius — leaves, groups, and root read
|
|
267
|
+
// as members of the same circle rather than ranked by size.
|
|
268
|
+
rootRadius: 4,
|
|
269
|
+
groupRadius: 4,
|
|
270
|
+
},
|
|
271
|
+
undefined,
|
|
272
|
+
prev,
|
|
273
|
+
);
|
|
274
|
+
case 'bundle':
|
|
275
|
+
return new GraphBundleProjector<SpaceGraphNode>(
|
|
276
|
+
ctx,
|
|
277
|
+
{
|
|
278
|
+
duration: TWEEN_MS,
|
|
279
|
+
groupOf: typenameGroupOf,
|
|
280
|
+
},
|
|
281
|
+
undefined,
|
|
282
|
+
prev,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/** Group leaves by typename so same-type leaves cluster together — used by both the
|
|
288
|
+
* cluster (visible structural nodes) and bundle (invisible routing anchors) projectors. */
|
|
289
|
+
const typenameGroupOf = (node: GraphLayoutNode<SpaceGraphNode>): string | undefined => {
|
|
290
|
+
const obj = node.data?.data?.object;
|
|
291
|
+
return obj ? (Obj.getTypename(obj) ?? '(untyped)') : undefined;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const createRenderNode = (variant: ExplorerArticleVariant): RenderNode<SpaceGraphNode> | undefined => {
|
|
295
|
+
switch (variant) {
|
|
296
|
+
case 'force':
|
|
297
|
+
return (group, node) => {
|
|
298
|
+
const r = node.r ?? 6;
|
|
299
|
+
group
|
|
300
|
+
.append('circle')
|
|
301
|
+
.attr('r', r)
|
|
302
|
+
.style('cursor', 'pointer')
|
|
303
|
+
.style('fill', getNodeFillForObject(node.data?.data?.object as Obj.Unknown | undefined));
|
|
304
|
+
};
|
|
305
|
+
case 'lattice':
|
|
306
|
+
return (group, node) => {
|
|
307
|
+
const r = node.r ?? 6;
|
|
308
|
+
const size = r * 2;
|
|
309
|
+
group
|
|
310
|
+
.append('rect')
|
|
311
|
+
.attr('x', -r)
|
|
312
|
+
.attr('y', -r)
|
|
313
|
+
.attr('width', size)
|
|
314
|
+
.attr('height', size)
|
|
315
|
+
.attr('rx', r * 0.3)
|
|
316
|
+
.attr('ry', r * 0.3)
|
|
317
|
+
.style('cursor', 'pointer')
|
|
318
|
+
.style('fill', getNodeFillForObject(node.data?.data?.object as Obj.Unknown | undefined));
|
|
319
|
+
};
|
|
320
|
+
case 'cluster':
|
|
321
|
+
return (group, node) => {
|
|
322
|
+
const obj = node.data?.data?.object as Obj.Unknown | undefined;
|
|
323
|
+
const r = node.r ?? 4;
|
|
324
|
+
// Synthetic root / group nodes have no underlying ECHO object; render them as
|
|
325
|
+
// smaller, neutral circles so the hierarchy reads as "structure + leaves".
|
|
326
|
+
group
|
|
327
|
+
.append('circle')
|
|
328
|
+
.attr('r', r)
|
|
329
|
+
.style('cursor', 'pointer')
|
|
330
|
+
.style('fill', obj ? getNodeFillForObject(obj) : 'var(--color-neutral-500)');
|
|
331
|
+
if (node.type === CLUSTER_NODE_TYPE_LEAF) {
|
|
332
|
+
appendRadialLeafLabel(group, node, obj, r);
|
|
333
|
+
} else if (node.type === CLUSTER_NODE_TYPE_ROOT) {
|
|
334
|
+
appendRootLabel(group, node, r);
|
|
335
|
+
} else if (node.type === CLUSTER_NODE_TYPE_GROUP) {
|
|
336
|
+
appendRadialGroupLabel(group, node, r);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
case 'bundle':
|
|
340
|
+
// Bundle layout renders ONLY leaves (root/group are invisible routing anchors).
|
|
341
|
+
// Every node here is a leaf — same circle + radial label shape as cluster.
|
|
342
|
+
return (group, node) => {
|
|
343
|
+
const obj = node.data?.data?.object as Obj.Unknown | undefined;
|
|
344
|
+
const r = node.r ?? 4;
|
|
345
|
+
group
|
|
346
|
+
.append('circle')
|
|
347
|
+
.attr('r', r)
|
|
348
|
+
.style('cursor', 'pointer')
|
|
349
|
+
.style('fill', obj ? getNodeFillForObject(obj) : 'var(--color-neutral-500)');
|
|
350
|
+
appendRadialLeafLabel(group, node, obj, r);
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
/** Fade-in duration applied to labels after the layout tween completes. */
|
|
356
|
+
const LABEL_FADE_MS = 200;
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Append a radial leaf label outside the ring, oriented outward. Compute orientation
|
|
360
|
+
* from the TARGET position (tx/ty) — at enter time node.x/y is still at the previous
|
|
361
|
+
* projector's coordinates, so using current x/y would orient the label by the
|
|
362
|
+
* pre-transition layout (wrong) and the rotation wouldn't update during the tween.
|
|
363
|
+
*
|
|
364
|
+
* Label appears with opacity 0 and fades in after a `TWEEN_MS` delay so the text isn't
|
|
365
|
+
* sliding across the screen mid-tween — leaves first, labels after.
|
|
366
|
+
*/
|
|
367
|
+
const appendRadialLeafLabel = (
|
|
368
|
+
group: Parameters<RenderNode<SpaceGraphNode>>[0],
|
|
369
|
+
node: GraphLayoutNode<SpaceGraphNode>,
|
|
370
|
+
obj: Obj.Unknown | undefined,
|
|
371
|
+
r: number,
|
|
372
|
+
): void => {
|
|
373
|
+
const label = (obj && Obj.getLabel(obj)) ?? node.data?.data?.label ?? node.id;
|
|
374
|
+
if (!label) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const targetX = (node as any).tx ?? node.x ?? 0;
|
|
378
|
+
const targetY = (node as any).ty ?? node.y ?? 0;
|
|
379
|
+
const angleDeg = (Math.atan2(targetY, targetX) * 180) / Math.PI;
|
|
380
|
+
// Flip text 180° on the left half of the layout so it still reads left-to-right.
|
|
381
|
+
const flipped = angleDeg > 90 || angleDeg < -90;
|
|
382
|
+
group
|
|
383
|
+
.append('text')
|
|
384
|
+
.classed('dx-cluster-label', true)
|
|
385
|
+
.attr('dy', '0.32em')
|
|
386
|
+
.attr('transform', `rotate(${flipped ? angleDeg + 180 : angleDeg})`)
|
|
387
|
+
.attr('x', flipped ? -(r + 4) : r + 4)
|
|
388
|
+
.attr('text-anchor', flipped ? 'end' : 'start')
|
|
389
|
+
.attr('opacity', 0)
|
|
390
|
+
.style('pointer-events', 'none')
|
|
391
|
+
.text(label)
|
|
392
|
+
.transition()
|
|
393
|
+
.delay(TWEEN_MS)
|
|
394
|
+
.duration(LABEL_FADE_MS)
|
|
395
|
+
.attr('opacity', 1);
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Append a radial label INSIDE the ring (toward origin) for a synthetic group node.
|
|
400
|
+
* Same rotation/flip rules as the leaf label, but offset and anchor inverted so the
|
|
401
|
+
* text reads from the group circle back toward the center rather than outward.
|
|
402
|
+
*/
|
|
403
|
+
const appendRadialGroupLabel = (
|
|
404
|
+
group: Parameters<RenderNode<SpaceGraphNode>>[0],
|
|
405
|
+
node: GraphLayoutNode<SpaceGraphNode>,
|
|
406
|
+
r: number,
|
|
407
|
+
): void => {
|
|
408
|
+
if (!node.label) {
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const targetX = (node as any).tx ?? node.x ?? 0;
|
|
412
|
+
const targetY = (node as any).ty ?? node.y ?? 0;
|
|
413
|
+
const angleDeg = (Math.atan2(targetY, targetX) * 180) / Math.PI;
|
|
414
|
+
const flipped = angleDeg > 90 || angleDeg < -90;
|
|
415
|
+
group
|
|
416
|
+
.append('text')
|
|
417
|
+
.classed('dx-cluster-label', true)
|
|
418
|
+
.classed('dx-cluster-label-group', true)
|
|
419
|
+
.attr('dy', '0.32em')
|
|
420
|
+
.attr('transform', `rotate(${flipped ? angleDeg + 180 : angleDeg})`)
|
|
421
|
+
// Inverse of the leaf offset / anchor — push the text inward, toward the origin.
|
|
422
|
+
.attr('x', flipped ? r + 4 : -(r + 4))
|
|
423
|
+
.attr('text-anchor', flipped ? 'start' : 'end')
|
|
424
|
+
.attr('opacity', 0)
|
|
425
|
+
.style('pointer-events', 'none')
|
|
426
|
+
.text(node.label)
|
|
427
|
+
.transition()
|
|
428
|
+
.delay(TWEEN_MS)
|
|
429
|
+
.duration(LABEL_FADE_MS)
|
|
430
|
+
.attr('opacity', 1);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Append a centered label below the root node. Root sits at origin where there's no
|
|
435
|
+
* meaningful radial direction; render the label as a plain horizontal caption with
|
|
436
|
+
* the standard halo style.
|
|
437
|
+
*/
|
|
438
|
+
const appendRootLabel = (
|
|
439
|
+
group: Parameters<RenderNode<SpaceGraphNode>>[0],
|
|
440
|
+
node: GraphLayoutNode<SpaceGraphNode>,
|
|
441
|
+
r: number,
|
|
442
|
+
): void => {
|
|
443
|
+
if (!node.label) {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
group
|
|
447
|
+
.append('text')
|
|
448
|
+
.classed('dx-cluster-label', true)
|
|
449
|
+
.classed('dx-cluster-label-root', true)
|
|
450
|
+
.attr('text-anchor', 'middle')
|
|
451
|
+
.attr('y', -(r + 6))
|
|
452
|
+
.attr('opacity', 0)
|
|
453
|
+
.style('pointer-events', 'none')
|
|
454
|
+
.text(node.label)
|
|
455
|
+
.transition()
|
|
456
|
+
.delay(TWEEN_MS)
|
|
457
|
+
.duration(LABEL_FADE_MS)
|
|
458
|
+
.attr('opacity', 1);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
/** Drop the package prefix from a typename for display: `org.dxos.type.Person` → `Person`. */
|
|
462
|
+
const shortTypename = (typename: string): string => {
|
|
463
|
+
const last = typename.split('.').pop() ?? typename;
|
|
464
|
+
return last.charAt(0).toUpperCase() + last.slice(1);
|
|
465
|
+
};
|