@prnt/dagr-explorer 0.1.3
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/CHANGELOG.md +93 -0
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/dist/base.d.ts +63 -0
- package/dist/base.d.ts.map +1 -0
- package/dist/base.js +20 -0
- package/dist/base.js.map +1 -0
- package/dist/camera.d.ts +61 -0
- package/dist/camera.d.ts.map +1 -0
- package/dist/camera.js +142 -0
- package/dist/camera.js.map +1 -0
- package/dist/context.d.ts +110 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +71 -0
- package/dist/context.js.map +1 -0
- package/dist/dagr-explorer.d.ts +41 -0
- package/dist/dagr-explorer.d.ts.map +1 -0
- package/dist/dagr-explorer.js +13 -0
- package/dist/dagr-explorer.js.map +1 -0
- package/dist/errors.d.ts +55 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +57 -0
- package/dist/errors.js.map +1 -0
- package/dist/explorer-details.d.ts +44 -0
- package/dist/explorer-details.d.ts.map +1 -0
- package/dist/explorer-details.js +76 -0
- package/dist/explorer-details.js.map +1 -0
- package/dist/explorer-search.d.ts +31 -0
- package/dist/explorer-search.d.ts.map +1 -0
- package/dist/explorer-search.js +86 -0
- package/dist/explorer-search.js.map +1 -0
- package/dist/explorer-toolbar.d.ts +17 -0
- package/dist/explorer-toolbar.d.ts.map +1 -0
- package/dist/explorer-toolbar.js +44 -0
- package/dist/explorer-toolbar.js.map +1 -0
- package/dist/explorer-trace-toggle.d.ts +16 -0
- package/dist/explorer-trace-toggle.d.ts.map +1 -0
- package/dist/explorer-trace-toggle.js +8 -0
- package/dist/explorer-trace-toggle.js.map +1 -0
- package/dist/explorer-viewport.d.ts +69 -0
- package/dist/explorer-viewport.d.ts.map +1 -0
- package/dist/explorer-viewport.js +69 -0
- package/dist/explorer-viewport.js.map +1 -0
- package/dist/explorer-views.d.ts +24 -0
- package/dist/explorer-views.d.ts.map +1 -0
- package/dist/explorer-views.js +16 -0
- package/dist/explorer-views.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/isomorphic-layout-effect.d.ts +7 -0
- package/dist/isomorphic-layout-effect.d.ts.map +1 -0
- package/dist/isomorphic-layout-effect.js +7 -0
- package/dist/isomorphic-layout-effect.js.map +1 -0
- package/dist/labels.d.ts +67 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +50 -0
- package/dist/labels.js.map +1 -0
- package/dist/layout.d.ts +82 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +244 -0
- package/dist/layout.js.map +1 -0
- package/dist/navigation.d.ts +20 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +44 -0
- package/dist/navigation.js.map +1 -0
- package/dist/root.d.ts +89 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +401 -0
- package/dist/root.js.map +1 -0
- package/dist/search.d.ts +19 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +31 -0
- package/dist/search.js.map +1 -0
- package/dist/size.d.ts +12 -0
- package/dist/size.d.ts.map +1 -0
- package/dist/size.js +20 -0
- package/dist/size.js.map +1 -0
- package/dist/svg-base.d.ts +33 -0
- package/dist/svg-base.d.ts.map +1 -0
- package/dist/svg-base.js +89 -0
- package/dist/svg-base.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/use-explorer-camera.d.ts +56 -0
- package/dist/use-explorer-camera.d.ts.map +1 -0
- package/dist/use-explorer-camera.js +641 -0
- package/dist/use-explorer-camera.js.map +1 -0
- package/dist/use-explorer.d.ts +23 -0
- package/dist/use-explorer.d.ts.map +1 -0
- package/dist/use-explorer.js +26 -0
- package/dist/use-explorer.js.map +1 -0
- package/dist/validate.d.ts +20 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +99 -0
- package/dist/validate.js.map +1 -0
- package/dist/viewport-surface.d.ts +90 -0
- package/dist/viewport-surface.d.ts.map +1 -0
- package/dist/viewport-surface.js +485 -0
- package/dist/viewport-surface.js.map +1 -0
- package/dist/visible-set.d.ts +65 -0
- package/dist/visible-set.d.ts.map +1 -0
- package/dist/visible-set.js +168 -0
- package/dist/visible-set.js.map +1 -0
- package/package.json +62 -0
- package/src/base.ts +67 -0
- package/src/camera.ts +206 -0
- package/src/context.ts +168 -0
- package/src/dagr-explorer.tsx +85 -0
- package/src/errors.ts +85 -0
- package/src/explorer-details.tsx +156 -0
- package/src/explorer-search.tsx +140 -0
- package/src/explorer-toolbar.tsx +77 -0
- package/src/explorer-trace-toggle.tsx +34 -0
- package/src/explorer-viewport.tsx +181 -0
- package/src/explorer-views.tsx +61 -0
- package/src/index.ts +76 -0
- package/src/isomorphic-layout-effect.ts +7 -0
- package/src/labels.ts +109 -0
- package/src/layout.ts +298 -0
- package/src/navigation.ts +51 -0
- package/src/root.tsx +544 -0
- package/src/search.ts +36 -0
- package/src/size.ts +23 -0
- package/src/svg-base.tsx +173 -0
- package/src/types.ts +69 -0
- package/src/use-explorer-camera.ts +675 -0
- package/src/use-explorer.ts +32 -0
- package/src/validate.ts +173 -0
- package/src/viewport-surface.tsx +650 -0
- package/src/visible-set.ts +225 -0
- package/styles.css +253 -0
package/src/svg-base.tsx
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default base layer: marks, edges and group outlines as one SVG.
|
|
3
|
+
*
|
|
4
|
+
* It sits inside the transformed plane, in world coordinates, so the camera
|
|
5
|
+
* never touches it: a pan or a zoom is the plane's transform and nothing
|
|
6
|
+
* here. It renders when the visible set or the emphasis changes, which is
|
|
7
|
+
* when the viewport hands it a new prop, and at no other time.
|
|
8
|
+
*
|
|
9
|
+
* It is decoration for sighted users, so it is `aria-hidden`. Every node a
|
|
10
|
+
* user can reach is reachable through the overlay and search, and a mark
|
|
11
|
+
* is clicked through the viewport's delegated listener, which resolves the
|
|
12
|
+
* point against the layout rather than against these elements.
|
|
13
|
+
*
|
|
14
|
+
* **One arrowhead marker per edge color.** An SVG marker cannot take the
|
|
15
|
+
* stroke of the path that uses it without `context-stroke`, which not every
|
|
16
|
+
* browser this package supports draws. So the arrowhead for a red edge is a
|
|
17
|
+
* red marker, and the markers are as many as the colors in view.
|
|
18
|
+
*
|
|
19
|
+
* **A marker's id is its color, escaped.** Ids live in the document, not
|
|
20
|
+
* in a React root, and two roots (two explorers, or two copies of React)
|
|
21
|
+
* can count `useId` the same way. Named by color, a marker one root reaches
|
|
22
|
+
* in another root's tree is the same arrowhead.
|
|
23
|
+
*
|
|
24
|
+
* **Strokes keep their width at any zoom** (`vector-effect`), so an edge
|
|
25
|
+
* never thins to nothing zoomed out or thickens to a bar zoomed in.
|
|
26
|
+
* Arrowheads are not strokes: the marker is in user space, so they scale
|
|
27
|
+
* with the camera like the marks they point at.
|
|
28
|
+
*
|
|
29
|
+
* Internal to the package. Nothing here is exported from the entry.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { useMemo } from 'react';
|
|
33
|
+
import type { ReactElement } from 'react';
|
|
34
|
+
import type { ExplorerBase, ExplorerBaseProps } from './base.js';
|
|
35
|
+
import type { ExplorerEdge, ExplorerNode } from './types.js';
|
|
36
|
+
|
|
37
|
+
const DEFAULT_COLOR = 'currentColor';
|
|
38
|
+
const DASH = '6 4';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The marker id for a color: letters and digits as they are, and every other
|
|
42
|
+
* character, `-` included, as `-<hex code point>-`. Escaped rather than
|
|
43
|
+
* stripped, so two colors never share an id (`rgb(1, 23, 4)` and
|
|
44
|
+
* `rgb(12, 3, 4)` strip to the same characters), and the result is a valid
|
|
45
|
+
* `url(#...)` fragment for any CSS color.
|
|
46
|
+
*/
|
|
47
|
+
function markerIdOf(color: string): string {
|
|
48
|
+
const escaped = color.replace(/[^A-Za-z0-9]/gu, (char) => `-${(char.codePointAt(0) ?? 0).toString(16)}-`);
|
|
49
|
+
return `dagr-explorer-arrow-${escaped}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function SvgBaseLayer({
|
|
53
|
+
view,
|
|
54
|
+
layout,
|
|
55
|
+
visible,
|
|
56
|
+
emphasis,
|
|
57
|
+
}: ExplorerBaseProps<ExplorerNode, ExplorerEdge>): ReactElement {
|
|
58
|
+
const nodes = useMemo(() => new Map(view.nodes.map((node) => [node.id, node])), [view.nodes]);
|
|
59
|
+
const edges = useMemo(() => new Map(view.edges.map((edge) => [edge.id, edge])), [view.edges]);
|
|
60
|
+
|
|
61
|
+
const markers = new Map<string, string>();
|
|
62
|
+
const markerFor = (color: string): string => {
|
|
63
|
+
let id = markers.get(color);
|
|
64
|
+
if (id === undefined) {
|
|
65
|
+
id = markerIdOf(color);
|
|
66
|
+
markers.set(color, id);
|
|
67
|
+
}
|
|
68
|
+
return id;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const paths: ReactElement[] = [];
|
|
72
|
+
for (const id of visible.edges) {
|
|
73
|
+
const edge = edges.get(id);
|
|
74
|
+
const route = layout.routes.get(id);
|
|
75
|
+
const first = route?.[0];
|
|
76
|
+
if (edge === undefined || route === undefined || first === undefined) continue;
|
|
77
|
+
const d = route.map((p, i) => `${i === 0 ? 'M' : 'L'}${String(p.x)} ${String(p.y)}`).join(' ');
|
|
78
|
+
const color = edge.color ?? DEFAULT_COLOR;
|
|
79
|
+
const dimmed = emphasis.dimmed.has(edge.source) || emphasis.dimmed.has(edge.target);
|
|
80
|
+
paths.push(
|
|
81
|
+
<path
|
|
82
|
+
key={id}
|
|
83
|
+
data-edge-id={id}
|
|
84
|
+
data-dimmed={dimmed ? 'true' : undefined}
|
|
85
|
+
d={d}
|
|
86
|
+
fill="none"
|
|
87
|
+
stroke={color}
|
|
88
|
+
strokeWidth={1.5}
|
|
89
|
+
vectorEffect="non-scaling-stroke"
|
|
90
|
+
strokeDasharray={edge.dash === true ? DASH : undefined}
|
|
91
|
+
markerEnd={`url(#${markerFor(color)})`}
|
|
92
|
+
/>,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const marks: ReactElement[] = [];
|
|
97
|
+
for (const id of visible.baseNodes) {
|
|
98
|
+
const node = nodes.get(id);
|
|
99
|
+
const box = layout.boxes.get(id);
|
|
100
|
+
if (node === undefined || box === undefined) continue;
|
|
101
|
+
marks.push(
|
|
102
|
+
<rect
|
|
103
|
+
key={id}
|
|
104
|
+
data-node-id={id}
|
|
105
|
+
data-dimmed={emphasis.dimmed.has(id) ? 'true' : undefined}
|
|
106
|
+
x={box.x}
|
|
107
|
+
y={box.y}
|
|
108
|
+
width={box.width}
|
|
109
|
+
height={box.height}
|
|
110
|
+
rx={4}
|
|
111
|
+
fill={node.color ?? DEFAULT_COLOR}
|
|
112
|
+
/>,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const groups: ReactElement[] = [];
|
|
117
|
+
for (const group of view.groups ?? []) {
|
|
118
|
+
const box = layout.groups.get(group.id);
|
|
119
|
+
if (box === undefined) continue;
|
|
120
|
+
const color = group.color ?? DEFAULT_COLOR;
|
|
121
|
+
groups.push(
|
|
122
|
+
<g key={group.id} data-group-id={group.id}>
|
|
123
|
+
<rect
|
|
124
|
+
x={box.x}
|
|
125
|
+
y={box.y}
|
|
126
|
+
width={box.width}
|
|
127
|
+
height={box.height}
|
|
128
|
+
rx={8}
|
|
129
|
+
fill="none"
|
|
130
|
+
stroke={color}
|
|
131
|
+
vectorEffect="non-scaling-stroke"
|
|
132
|
+
strokeDasharray="4 4"
|
|
133
|
+
/>
|
|
134
|
+
<text x={box.x + 12} y={box.y + 17} fill={color} fontSize={12}>
|
|
135
|
+
{group.label}
|
|
136
|
+
</text>
|
|
137
|
+
</g>,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<svg
|
|
143
|
+
aria-hidden="true"
|
|
144
|
+
width={layout.width}
|
|
145
|
+
height={layout.height}
|
|
146
|
+
viewBox={`0 0 ${String(layout.width)} ${String(layout.height)}`}
|
|
147
|
+
style={{ position: 'absolute', left: 0, top: 0, overflow: 'visible' }}
|
|
148
|
+
>
|
|
149
|
+
<defs>
|
|
150
|
+
{[...markers].map(([color, id]) => (
|
|
151
|
+
<marker
|
|
152
|
+
key={id}
|
|
153
|
+
id={id}
|
|
154
|
+
viewBox="0 0 10 10"
|
|
155
|
+
refX={10}
|
|
156
|
+
refY={5}
|
|
157
|
+
markerWidth={8}
|
|
158
|
+
markerHeight={8}
|
|
159
|
+
markerUnits="userSpaceOnUse"
|
|
160
|
+
orient="auto"
|
|
161
|
+
>
|
|
162
|
+
<path d="M0 0L10 5L0 10z" fill={color} />
|
|
163
|
+
</marker>
|
|
164
|
+
))}
|
|
165
|
+
</defs>
|
|
166
|
+
{groups}
|
|
167
|
+
{paths}
|
|
168
|
+
{marks}
|
|
169
|
+
</svg>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const svgBase: ExplorerBase = Object.freeze({ Layer: SvgBaseLayer, space: 'plane' });
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Size } from '@prnt/dagr-render/core';
|
|
3
|
+
|
|
4
|
+
export type { Size };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One node. `id` and `label` are all the explorer itself reads: the label is
|
|
8
|
+
* the accessible name, the search result text, and the default content.
|
|
9
|
+
* Everything else about a node is the caller's own fields on a type that
|
|
10
|
+
* extends this one, and every slot receives that type.
|
|
11
|
+
*
|
|
12
|
+
* Optional fields are spelled `T | undefined` so a caller compiling with
|
|
13
|
+
* `exactOptionalPropertyTypes` can pass a value it computed, including
|
|
14
|
+
* `undefined`, without a conditional spread.
|
|
15
|
+
*/
|
|
16
|
+
export interface ExplorerNode {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly label: string;
|
|
19
|
+
/** World size in CSS pixels at zoom 1. Declared, never measured. */
|
|
20
|
+
readonly size?: Size | undefined;
|
|
21
|
+
/** Any CSS color. */
|
|
22
|
+
readonly color?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ExplorerEdge {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly source: string;
|
|
28
|
+
readonly target: string;
|
|
29
|
+
readonly label?: string | undefined;
|
|
30
|
+
/** Any CSS color. */
|
|
31
|
+
readonly color?: string | undefined;
|
|
32
|
+
/** Draw the edge dashed. */
|
|
33
|
+
readonly dash?: boolean | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A labeled outline around its members. An annotation: it moves no node. */
|
|
37
|
+
export interface ExplorerGroup {
|
|
38
|
+
readonly id: string;
|
|
39
|
+
readonly label: string;
|
|
40
|
+
readonly nodeIds: readonly string[];
|
|
41
|
+
/** Any CSS color. */
|
|
42
|
+
readonly color?: string | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ExplorerLayoutOptions<N extends ExplorerNode = ExplorerNode> {
|
|
46
|
+
/** Which way edges flow. Default `'right'`. */
|
|
47
|
+
readonly direction?: 'right' | 'down' | undefined;
|
|
48
|
+
/** Size for nodes with no `size` of their own. Default 240 by 120. */
|
|
49
|
+
readonly nodeSize?: Size | ((node: N) => Size) | undefined;
|
|
50
|
+
/** Gap between neighbors across the flow. Default 40. */
|
|
51
|
+
readonly nodeSep?: number | undefined;
|
|
52
|
+
/** Gap between ranks along the flow. Default 120. */
|
|
53
|
+
readonly rankSep?: number | undefined;
|
|
54
|
+
/** Default `'smooth'`. */
|
|
55
|
+
readonly edgeStyle?: 'smooth' | 'orthogonal' | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ExplorerView<
|
|
59
|
+
N extends ExplorerNode = ExplorerNode,
|
|
60
|
+
E extends ExplorerEdge = ExplorerEdge,
|
|
61
|
+
> {
|
|
62
|
+
readonly id: string;
|
|
63
|
+
readonly label: string;
|
|
64
|
+
readonly description?: ReactNode | undefined;
|
|
65
|
+
readonly nodes: readonly N[];
|
|
66
|
+
readonly edges: readonly E[];
|
|
67
|
+
readonly groups?: readonly ExplorerGroup[] | undefined;
|
|
68
|
+
readonly layout?: ExplorerLayoutOptions<N> | undefined;
|
|
69
|
+
}
|