@rokkit/graph 1.7.0 → 1.8.1
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/README.md +67 -1
- package/dist/Graph.svelte +234 -259
- package/dist/Graph.svelte.d.ts +1 -1
- package/dist/GraphLegend.svelte +81 -10
- package/dist/GraphLegend.svelte.d.ts +15 -0
- package/dist/GraphState.svelte.d.ts +168 -51
- package/dist/GraphState.svelte.js +406 -305
- package/dist/actions/canvas.d.ts +18 -0
- package/dist/actions/canvas.js +59 -0
- package/dist/actions/choices.d.ts +12 -0
- package/dist/actions/choices.js +23 -0
- package/dist/actions/interactions.d.ts +20 -0
- package/dist/actions/interactions.js +57 -0
- package/dist/actions/listen.d.ts +2 -0
- package/dist/actions/listen.js +10 -0
- package/dist/canvas/frame.d.ts +41 -0
- package/dist/canvas/frame.js +34 -0
- package/dist/controls/BundleControl.svelte +11 -10
- package/dist/controls/BundleControl.svelte.d.ts +0 -7
- package/dist/controls/DensityControl.svelte +17 -9
- package/dist/controls/DensityControl.svelte.d.ts +0 -8
- package/dist/controls/DepthControl.svelte +11 -4
- package/dist/controls/DepthControl.svelte.d.ts +0 -9
- package/dist/controls/DrillBar.svelte +63 -0
- package/dist/controls/DrillBar.svelte.d.ts +9 -0
- package/dist/controls/EdgeStyleControl.svelte +10 -10
- package/dist/controls/EdgeStyleControl.svelte.d.ts +0 -6
- package/dist/controls/HiddenControl.svelte +27 -0
- package/dist/controls/HiddenControl.svelte.d.ts +7 -0
- package/dist/controls/MeasureControl.svelte +39 -0
- package/dist/controls/MeasureControl.svelte.d.ts +12 -0
- package/dist/controls/ToggleControl.svelte +33 -0
- package/dist/controls/ToggleControl.svelte.d.ts +13 -0
- package/dist/controls/ViolationsControl.svelte +27 -0
- package/dist/controls/ViolationsControl.svelte.d.ts +7 -0
- package/dist/controls/ZoomControl.svelte +21 -18
- package/dist/controls/zoom.d.ts +3 -0
- package/dist/controls/zoom.js +6 -0
- package/dist/diagrams/ArcDiagram.svelte +116 -0
- package/dist/diagrams/ArcDiagram.svelte.d.ts +31 -0
- package/dist/diagrams/CallTree.svelte +17 -5
- package/dist/diagrams/CallTree.svelte.d.ts +5 -1
- package/dist/diagrams/DependencyDiagram.svelte +18 -6
- package/dist/diagrams/DependencyDiagram.svelte.d.ts +5 -1
- package/dist/diagrams/DependencyMatrix.svelte +156 -0
- package/dist/diagrams/DependencyMatrix.svelte.d.ts +34 -0
- package/dist/diagrams/ErDiagram.svelte +18 -6
- package/dist/diagrams/ErDiagram.svelte.d.ts +5 -1
- package/dist/diagrams/LayersDiagram.svelte +124 -0
- package/dist/diagrams/LayersDiagram.svelte.d.ts +30 -0
- package/dist/diagrams/Neighborhood.svelte +26 -6
- package/dist/diagrams/Neighborhood.svelte.d.ts +11 -1
- package/dist/diagrams/PolymetricTree.svelte +116 -0
- package/dist/diagrams/PolymetricTree.svelte.d.ts +28 -0
- package/dist/diagrams/StructureDiagram.svelte +38 -8
- package/dist/diagrams/StructureDiagram.svelte.d.ts +9 -1
- package/dist/diagrams/Sunburst.svelte +44 -8
- package/dist/diagrams/Sunburst.svelte.d.ts +12 -2
- package/dist/diagrams/Treemap.svelte +44 -8
- package/dist/diagrams/Treemap.svelte.d.ts +12 -2
- package/dist/index.d.ts +13 -3
- package/dist/index.js +11 -2
- package/dist/layout/arcs.d.ts +6 -0
- package/dist/layout/arcs.js +141 -0
- package/dist/layout/cards.js +9 -2
- package/dist/layout/edges.d.ts +13 -1
- package/dist/layout/edges.js +35 -0
- package/dist/layout/extent.d.ts +17 -0
- package/dist/layout/extent.js +27 -0
- package/dist/layout/flow.js +3 -1
- package/dist/layout/index.d.ts +4 -1
- package/dist/layout/index.js +17 -2
- package/dist/layout/layers.d.ts +6 -0
- package/dist/layout/layers.js +180 -0
- package/dist/layout/matrix.d.ts +50 -0
- package/dist/layout/matrix.js +110 -0
- package/dist/layout/neighborhood.js +16 -7
- package/dist/layout/options.d.ts +1 -1
- package/dist/layout/options.js +16 -3
- package/dist/layout/percentile.d.ts +8 -0
- package/dist/layout/percentile.js +13 -0
- package/dist/layout/polymetric.d.ts +3 -0
- package/dist/layout/polymetric.js +221 -0
- package/dist/layout/radial.js +3 -1
- package/dist/layout/structure.js +7 -1
- package/dist/layout/sunburst.js +11 -4
- package/dist/layout/types.d.ts +83 -0
- package/dist/layout/world.js +11 -4
- package/dist/messages.d.ts +14 -0
- package/dist/messages.js +17 -0
- package/dist/model/condense.d.ts +8 -0
- package/dist/model/condense.js +114 -0
- package/dist/model/entities.d.ts +12 -0
- package/dist/model/entities.js +12 -0
- package/dist/model/normalize.d.ts +2 -6
- package/dist/model/normalize.js +83 -5
- package/dist/model/relationships.d.ts +30 -0
- package/dist/model/relationships.js +51 -0
- package/dist/model/share.d.ts +9 -0
- package/dist/model/share.js +43 -0
- package/dist/model/tree.d.ts +7 -1
- package/dist/model/tree.js +11 -4
- package/dist/preset.d.ts +18 -0
- package/dist/preset.js +34 -3
- package/dist/schema/EntitiesView.svelte +8 -6
- package/dist/schema/EntityView.svelte +13 -10
- package/dist/state/GraphConfig.svelte.d.ts +124 -0
- package/dist/state/GraphConfig.svelte.js +253 -0
- package/dist/state/GraphDrill.svelte.d.ts +52 -0
- package/dist/state/GraphDrill.svelte.js +115 -0
- package/dist/state/GraphGroups.svelte.d.ts +35 -0
- package/dist/state/GraphGroups.svelte.js +73 -0
- package/dist/state/GraphSelection.svelte.d.ts +53 -0
- package/dist/state/GraphSelection.svelte.js +94 -0
- package/dist/state/intents.d.ts +33 -0
- package/dist/state/intents.js +30 -0
- package/dist/state/text.d.ts +28 -0
- package/dist/state/text.js +56 -0
- package/dist/types.d.ts +92 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -66,7 +66,8 @@ one layout, the controls that mean something for it, an opt-in legend. The same
|
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
`ErDiagram` · `DependencyDiagram` · `CallTree` · `Treemap` · `Sunburst` · `StructureDiagram` ·
|
|
69
|
-
`Neighborhood
|
|
69
|
+
`Neighborhood` · `DependencyMatrix` (a dependency structure matrix — cells above the diagonal are
|
|
70
|
+
dependencies against the grain).
|
|
70
71
|
Both `controls` and `legend` default to off: the default is the bare picture.
|
|
71
72
|
|
|
72
73
|
Composing your own is the same parts in a different box — `DensityControl`, `EdgeStyleControl`,
|
|
@@ -111,6 +112,12 @@ import { appliesTo } from '@rokkit/graph'
|
|
|
111
112
|
appliesTo('world', 'density') // false — a treemap box has no row list to thin
|
|
112
113
|
```
|
|
113
114
|
|
|
115
|
+
## Overlay edges
|
|
116
|
+
|
|
117
|
+
An edge with `overlay: true` (or `fields.overlay`) is drawn over the picture but never shapes it —
|
|
118
|
+
co-change pairs, a broken layering rule. It never reaches the layout, neighbours or relationships,
|
|
119
|
+
renders with `data-edge-overlay`, and its `weight` thickens the stroke via `--edge-weight`.
|
|
120
|
+
|
|
114
121
|
## Two things worth knowing
|
|
115
122
|
|
|
116
123
|
**An unplaced edge is dimmed, never dropped.** An endpoint the map cannot resolve is a normal
|
|
@@ -129,6 +136,57 @@ const er = toGraphInput(model, 'er') // tables + refs
|
|
|
129
136
|
const deps = toGraphInput(model, 'dependencies') // tables + entities, deps as edges
|
|
130
137
|
```
|
|
131
138
|
|
|
139
|
+
## Imports against shared commits
|
|
140
|
+
|
|
141
|
+
`ArcDiagram` puts items on one axis, with imports arcing left and shared commits right. Flag a
|
|
142
|
+
co-change pair with no import `hidden` (rokkit computes no history) and it draws in red; *Hidden
|
|
143
|
+
only* leaves just those. Arcs are as thick as their weight, and each side is scaled on its own.
|
|
144
|
+
The legend names each side from the relations drawn there; `sideLabels` names them yourself.
|
|
145
|
+
|
|
146
|
+
## Shading by a share
|
|
147
|
+
|
|
148
|
+
`Treemap` and `Sunburst` take `shadeBy`, a 0..1 share from each node's measures, and paint it
|
|
149
|
+
on a ramp beside the size. A container takes the size-weighted mean of what it holds, and
|
|
150
|
+
unmeasured boxes are hatched, not zeroed. The label colour is measured for every step.
|
|
151
|
+
|
|
152
|
+
## Three measures per box
|
|
153
|
+
|
|
154
|
+
`PolymetricTree` draws containment top down, each file a box whose width, height and shade are
|
|
155
|
+
three measures you name: `widthBy="fns" heightBy="loc" colorBy="churn"`. Send containment as
|
|
156
|
+
`parent` ids or as `path`. Each channel caps at its 95th percentile, so one huge file does not
|
|
157
|
+
flatten the rest; a capped box and a missing measure are both marked, and a missing value is
|
|
158
|
+
never drawn as a zero. The legend names each channel.
|
|
159
|
+
|
|
160
|
+
## Checking a layered architecture
|
|
161
|
+
|
|
162
|
+
`LayersDiagram` draws the layers you intend (`layer` on each node, 0 at the top) and marks
|
|
163
|
+
every edge `down`, `skip` (dashed), `up` (red, the violation) or `level`. *Violations only*
|
|
164
|
+
leaves just the edges that climb. rokkit computes no depth; it compares the layers you gave.
|
|
165
|
+
|
|
166
|
+
## Condensing cycles into group nodes
|
|
167
|
+
|
|
168
|
+
Send each strongly-connected component as a node with `members` (rokkit computes none). Its
|
|
169
|
+
members collapse into it, their edges reroute and aggregate onto it, and it expands back to
|
|
170
|
+
show the cycle. Flag the cheapest edge to cut `weakest: true` and it is drawn dashed in the
|
|
171
|
+
danger colour. `onexpand` / `oncollapse` tell you when the reader opens or folds one.
|
|
172
|
+
|
|
173
|
+
## Drilling a tree too large to load
|
|
174
|
+
|
|
175
|
+
`Treemap`, `Sunburst` and `StructureDiagram` draw the subtree at `focusPath`. The reader
|
|
176
|
+
opens a box (a click, or Enter on it), and `ondrill(path, node)` tells you, so you can load that
|
|
177
|
+
level on demand:
|
|
178
|
+
|
|
179
|
+
```svelte
|
|
180
|
+
<Treemap {nodes} {fields} bind:focusPath ondrill={(path) => load(path)} ondrillup={(path) => load(path)} />
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- Return a promise while you fetch. The old level dims and the drill bar says *Loading…*. A
|
|
184
|
+
rejection returns to the previous level and says why.
|
|
185
|
+
- `path` is the path in YOUR data. Send one level at a time: the containers on the focus path
|
|
186
|
+
are kept even when nothing declares them.
|
|
187
|
+
- `bind:focusPath` (or omit it). An unbound prop is yours, and a re-render puts it back.
|
|
188
|
+
- Drilling never selects. `onselect` is not called.
|
|
189
|
+
|
|
132
190
|
## Icons need a safelist
|
|
133
191
|
|
|
134
192
|
The component picks a node's icon at runtime from its kind, so the class names never appear in
|
|
@@ -155,3 +213,11 @@ from `createGraphPreset({ using: 'color' | 'pattern' })`.
|
|
|
155
213
|
- [Package reference](../../docs/llms/packages/graph.txt) — data contract, layouts, dbd adapter
|
|
156
214
|
- [Component reference](../../docs/llms/components/graph.txt) — props, interaction, attributes
|
|
157
215
|
- [Design](../../docs/design/23-graph.md) — why it is its own package, and what is locked
|
|
216
|
+
|
|
217
|
+
## Words and interactions
|
|
218
|
+
|
|
219
|
+
Every string graph shows is a key in `messages.graph` (`@rokkit/states`), so a locale replaces
|
|
220
|
+
it: `messages.register('de', { graph: { zoomIn: 'Vergrößern' } })`. No element carries an event
|
|
221
|
+
handler: the state writes each element's intent (`data-graph-press`, `data-graph-open`) and
|
|
222
|
+
the `interactions`, `choices` and `canvasNavigation` actions perform it through
|
|
223
|
+
`GraphState.act`.
|
package/dist/Graph.svelte
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
import type { GraphStateConfig } from './GraphState.svelte.js'
|
|
5
5
|
import type { GraphProps } from './types.js'
|
|
6
6
|
import { DEFAULT_ICONS } from './icons.js'
|
|
7
|
-
import {
|
|
7
|
+
import { nextZoom } from './controls/zoom.js'
|
|
8
|
+
import { anchoredScroll, frameOf } from './canvas/frame.js'
|
|
9
|
+
import type { Frame } from './canvas/frame.js'
|
|
10
|
+
import { interactions } from './actions/interactions.js'
|
|
11
|
+
import { canvasNavigation } from './actions/canvas.js'
|
|
12
|
+
import { say } from './messages.js'
|
|
13
|
+
import { readable } from './state/text.js'
|
|
8
14
|
|
|
9
15
|
// Aliased: a local binding literally named `state` makes the compiler read the `$state`
|
|
10
16
|
// rune below as a store subscription on it. The PUBLIC prop name is still `state`.
|
|
@@ -31,6 +37,11 @@
|
|
|
31
37
|
arrows = true,
|
|
32
38
|
label = undefined,
|
|
33
39
|
onselect = undefined,
|
|
40
|
+
focusPath = $bindable([]),
|
|
41
|
+
ondrill = undefined,
|
|
42
|
+
ondrillup = undefined,
|
|
43
|
+
onexpand = undefined,
|
|
44
|
+
oncollapse = undefined,
|
|
34
45
|
icons: userIcons = undefined,
|
|
35
46
|
class: className = ''
|
|
36
47
|
}: GraphProps = $props()
|
|
@@ -55,7 +66,15 @@
|
|
|
55
66
|
preset,
|
|
56
67
|
mode,
|
|
57
68
|
label,
|
|
58
|
-
onselect
|
|
69
|
+
onselect,
|
|
70
|
+
focusPath,
|
|
71
|
+
ondrill,
|
|
72
|
+
ondrillup,
|
|
73
|
+
onexpand,
|
|
74
|
+
oncollapse,
|
|
75
|
+
// Drilling moves focusPath inside the state; keep the bindable prop in step, or the next
|
|
76
|
+
// update() would put the old value back.
|
|
77
|
+
onfocuspath: (path) => (focusPath = path)
|
|
59
78
|
})
|
|
60
79
|
|
|
61
80
|
// untrack: the constructor's initial read must not register as a dependency of whatever
|
|
@@ -93,57 +112,42 @@
|
|
|
93
112
|
let vh = $state(0)
|
|
94
113
|
let paper = $state<HTMLElement | null>(null)
|
|
95
114
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
// The frame — scale, placement and scroll extent — is pure arithmetic over the measured
|
|
116
|
+
// viewport (`canvas/frame.ts`, #171). The world sits inside an EXTENT box sized to the
|
|
117
|
+
// scaled drawing plus PAD all round: a CSS transform does not change layout size, so left
|
|
118
|
+
// to the transformed world the paper's scroll area was the unscaled box — unreachable past
|
|
119
|
+
// fit, phantom scroll at it.
|
|
120
|
+
const frame = $derived(frameOf({ vw, vh, content: graph.contentSize, zoom, pad: PAD }))
|
|
121
|
+
const scale = $derived(frame.scale)
|
|
99
122
|
|
|
100
|
-
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* hits first and reads as the component ignoring them.
|
|
104
|
-
*
|
|
105
|
-
* A plain wheel is left alone: the canvas scrolls, which is how panning works here.
|
|
106
|
-
*/
|
|
107
|
-
function onWheel(event: WheelEvent) {
|
|
108
|
-
if (!zoomable || !(event.ctrlKey || event.metaKey)) return
|
|
109
|
-
event.preventDefault()
|
|
110
|
-
zoomBy(event.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP)
|
|
111
|
-
}
|
|
123
|
+
/** Where the next zoom should hold still: the pointer for a pinch; the centre otherwise. */
|
|
124
|
+
let anchor: { x: number; y: number } | null = null
|
|
125
|
+
let previous: Frame | null = null
|
|
112
126
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
let panFrom = { x: 0, y: 0, left: 0, top: 0 }
|
|
117
|
-
|
|
118
|
-
function onPointerDown(event: PointerEvent) {
|
|
119
|
-
// Only the background drags — a press that starts on a card is a selection.
|
|
120
|
-
if (!paper || (event.target as HTMLElement).closest('[data-graph-node]')) return
|
|
121
|
-
panning = true
|
|
122
|
-
panFrom = { x: event.clientX, y: event.clientY, left: paper.scrollLeft, top: paper.scrollTop }
|
|
123
|
-
paper.setPointerCapture(event.pointerId)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function onPointerMove(event: PointerEvent) {
|
|
127
|
-
if (!panning || !paper) return
|
|
128
|
-
paper.scrollLeft = panFrom.left - (event.clientX - panFrom.x)
|
|
129
|
-
paper.scrollTop = panFrom.top - (event.clientY - panFrom.y)
|
|
127
|
+
const onzoom = (direction: 'in' | 'out', at: { x: number; y: number }) => {
|
|
128
|
+
anchor = at
|
|
129
|
+
zoom = nextZoom(zoom, direction)
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
132
|
+
// After a change of scale, scroll so the anchored content point is where it was. Runs after
|
|
133
|
+
// the DOM update, so the extent has already grown or shrunk to the new size.
|
|
134
|
+
$effect(() => {
|
|
135
|
+
const next = frame
|
|
136
|
+
untrack(() => {
|
|
137
|
+
if (previous && paper && next.scale !== previous.scale) {
|
|
138
|
+
const scroll = anchoredScroll({
|
|
139
|
+
before: previous,
|
|
140
|
+
after: next,
|
|
141
|
+
scroll: { left: paper.scrollLeft, top: paper.scrollTop },
|
|
142
|
+
anchor: anchor ?? { x: vw / 2, y: vh / 2 }
|
|
143
|
+
})
|
|
144
|
+
paper.scrollLeft = scroll.left
|
|
145
|
+
paper.scrollTop = scroll.top
|
|
146
|
+
}
|
|
147
|
+
previous = next
|
|
148
|
+
anchor = null
|
|
149
|
+
})
|
|
150
|
+
})
|
|
147
151
|
|
|
148
152
|
/**
|
|
149
153
|
* Level of detail, from the EFFECTIVE scale rather than from `zoom` — a diagram fitted to
|
|
@@ -175,244 +179,215 @@
|
|
|
175
179
|
data-graph-paper
|
|
176
180
|
data-graph-detail={detail}
|
|
177
181
|
data-graph-group-tint={graph.groupTint ? '' : undefined}
|
|
178
|
-
data-graph-
|
|
182
|
+
data-graph-pending={graph.pending ? '' : undefined}
|
|
183
|
+
aria-busy={graph.pending}
|
|
179
184
|
role="presentation"
|
|
180
185
|
bind:this={paper}
|
|
181
186
|
bind:clientWidth={vw}
|
|
182
187
|
bind:clientHeight={vh}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
onwheel={onWheel}
|
|
186
|
-
onpointerdown={onPointerDown}
|
|
187
|
-
onpointermove={onPointerMove}
|
|
188
|
-
onpointerup={endPan}
|
|
189
|
-
onpointercancel={endPan}
|
|
188
|
+
use:interactions={{ state: graph, clearOnBackground: true }}
|
|
189
|
+
use:canvasNavigation={{ zoomable, onzoom }}
|
|
190
190
|
tabindex="-1"
|
|
191
191
|
>
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
192
|
+
<div data-graph-extent style:width="{frame.extent.w}px" style:height="{frame.extent.h}px">
|
|
193
|
+
<div
|
|
194
|
+
data-graph-world
|
|
195
|
+
data-graph-node-shape={graph.nodeShape}
|
|
196
|
+
data-graph-layout={graph.layoutName}
|
|
197
|
+
style="width: {graph.size.w}px; height: {graph.size
|
|
198
|
+
.h}px; transform: translate({frame.tx}px, {frame.ty}px) scale({scale}); --graph-label-counter-scale: {(
|
|
199
|
+
1 / scale
|
|
200
|
+
).toFixed(3)};"
|
|
201
|
+
>
|
|
202
|
+
{#each graph.columns as column (column.side + column.depth)}
|
|
203
|
+
<span
|
|
204
|
+
data-graph-column
|
|
205
|
+
data-column-side={column.side}
|
|
206
|
+
data-column-depth={column.depth}
|
|
207
|
+
style:left="{column.x}px"
|
|
208
|
+
style:width="{column.w}px">{column.label}</span
|
|
209
|
+
>
|
|
210
|
+
{/each}
|
|
210
211
|
|
|
211
|
-
|
|
212
|
+
<!-- A wedge cannot be a positioned <div> — an annulus sector has no box. It is drawn in
|
|
212
213
|
its own SVG UNDER the boxes, so a sunburst renders through the same cluster list as
|
|
213
214
|
every other containment layout and nothing downstream learns a second vocabulary. -->
|
|
214
|
-
|
|
215
|
+
{#if graph.hasWedges}
|
|
216
|
+
<svg
|
|
217
|
+
data-graph-wedges
|
|
218
|
+
width={graph.size.w}
|
|
219
|
+
height={graph.size.h}
|
|
220
|
+
role="img"
|
|
221
|
+
aria-label={graph.label}
|
|
222
|
+
style="position: absolute; top: 0; left: 0;"
|
|
223
|
+
>
|
|
224
|
+
<title>{graph.label}</title>
|
|
225
|
+
{#each graph.clusters as cluster (graph.clusterKey(cluster))}
|
|
226
|
+
<!-- Two branches so `role` is STATIC: an interactive wedge is a button and says so;
|
|
227
|
+
a region is scenery. What a press on it means comes from state, in the
|
|
228
|
+
shared attributes, and the canvas's one `interactions` action acts on it. -->
|
|
229
|
+
{#if graph.interactive(cluster)}
|
|
230
|
+
<path
|
|
231
|
+
data-graph-wedge
|
|
232
|
+
{...graph.boxAttrs(cluster)}
|
|
233
|
+
style={graph.boxStyleAttr(cluster)}
|
|
234
|
+
d={graph.wedgePath(cluster)}
|
|
235
|
+
role="button"
|
|
236
|
+
tabindex={0}><title>{graph.caption(cluster)}</title></path
|
|
237
|
+
>
|
|
238
|
+
{:else}
|
|
239
|
+
<path
|
|
240
|
+
data-graph-wedge
|
|
241
|
+
{...graph.boxAttrs(cluster)}
|
|
242
|
+
style={graph.boxStyleAttr(cluster)}
|
|
243
|
+
d={graph.wedgePath(cluster)}><title>{graph.caption(cluster)}</title></path
|
|
244
|
+
>
|
|
245
|
+
{/if}
|
|
246
|
+
{/each}
|
|
247
|
+
</svg>
|
|
248
|
+
{/if}
|
|
249
|
+
|
|
250
|
+
{#each graph.boxes as cluster (graph.clusterKey(cluster))}
|
|
251
|
+
<!-- One box shape at every depth: a containment layout nests boxes inside boxes, so a
|
|
252
|
+
childless one is still a box, not a node card.
|
|
253
|
+
|
|
254
|
+
Two branches, not one element with a conditional tag: an interactive box is a real
|
|
255
|
+
<button> and gets Enter/Space, focus order and the right announcement for free; a
|
|
256
|
+
region is scenery and is a <div> that says so. What its press means — select a leaf,
|
|
257
|
+
open a container — comes from state in the shared attributes. -->
|
|
258
|
+
{#if graph.interactive(cluster)}
|
|
259
|
+
<button
|
|
260
|
+
type="button"
|
|
261
|
+
data-graph-cluster
|
|
262
|
+
{...graph.boxAttrs(cluster)}
|
|
263
|
+
style:left="{cluster.x}px"
|
|
264
|
+
style:top="{cluster.y}px"
|
|
265
|
+
style:width="{cluster.w}px"
|
|
266
|
+
style:height="{cluster.h}px"
|
|
267
|
+
style={graph.boxStyleAttr(cluster)}
|
|
268
|
+
style:--shade={cluster.shade}
|
|
269
|
+
>
|
|
270
|
+
<span data-graph-cluster-label>{graph.caption(cluster)}</span>
|
|
271
|
+
</button>
|
|
272
|
+
{:else}
|
|
273
|
+
<div
|
|
274
|
+
data-graph-cluster
|
|
275
|
+
{...graph.boxAttrs(cluster)}
|
|
276
|
+
style:left="{cluster.x}px"
|
|
277
|
+
style:top="{cluster.y}px"
|
|
278
|
+
style:width="{cluster.w}px"
|
|
279
|
+
style:height="{cluster.h}px"
|
|
280
|
+
style={graph.boxStyleAttr(cluster)}
|
|
281
|
+
>
|
|
282
|
+
<span data-graph-cluster-label>{graph.caption(cluster)}</span>
|
|
283
|
+
</div>
|
|
284
|
+
{/if}
|
|
285
|
+
{/each}
|
|
286
|
+
|
|
215
287
|
<svg
|
|
216
|
-
data-graph-wedges
|
|
217
288
|
width={graph.size.w}
|
|
218
289
|
height={graph.size.h}
|
|
219
290
|
role="img"
|
|
220
291
|
aria-label={graph.label}
|
|
221
|
-
style="position: absolute; top: 0; left: 0;"
|
|
292
|
+
style="position: absolute; top: 0; left: 0; pointer-events: none;"
|
|
222
293
|
>
|
|
223
294
|
<title>{graph.label}</title>
|
|
224
|
-
{#each graph.
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
{
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
>
|
|
258
|
-
{/if}
|
|
295
|
+
{#each graph.routedEdges as edge (edge.id)}
|
|
296
|
+
<g
|
|
297
|
+
data-graph-edge
|
|
298
|
+
data-edge-kind={edge.kind}
|
|
299
|
+
data-edge-relation={edge.relation}
|
|
300
|
+
data-edge-from={edge.fromKey}
|
|
301
|
+
data-edge-to={edge.toKey}
|
|
302
|
+
data-edge-state={graph.edgeState(edge)}
|
|
303
|
+
data-edge-overlay={edge.overlay ? '' : undefined}
|
|
304
|
+
data-edge-weakest={edge.weakest ? '' : undefined}
|
|
305
|
+
data-edge-hidden={edge.hidden ? '' : undefined}
|
|
306
|
+
data-edge-side={edge.side}
|
|
307
|
+
data-edge-conformance={edge.conformance}
|
|
308
|
+
data-edge-count={edge.count}
|
|
309
|
+
style:--edge-weight={graph.edgeWeight(edge)}
|
|
310
|
+
>
|
|
311
|
+
<path d={graph.edgePath(edge)} />
|
|
312
|
+
<circle data-graph-edge-dot="from" cx={edge.x1} cy={edge.y1} r="3.2" />
|
|
313
|
+
{#if arrows}
|
|
314
|
+
<!-- An edge HAS a direction — source to target — and two identical dots threw
|
|
315
|
+
that away. The tangent of both path shapes is horizontal at the endpoint,
|
|
316
|
+
so the heading is just the side the curve arrives from: -s2. No angle
|
|
317
|
+
maths, and it stays correct for a self-loop. -->
|
|
318
|
+
{@const d = -edge.s2}
|
|
319
|
+
<polygon
|
|
320
|
+
data-graph-edge-arrow
|
|
321
|
+
points="{edge.x2},{edge.y2} {edge.x2 - d * 9},{edge.y2 - 4.5} {edge.x2 -
|
|
322
|
+
d * 9},{edge.y2 + 4.5}"
|
|
323
|
+
/>
|
|
324
|
+
{:else}
|
|
325
|
+
<circle data-graph-edge-dot="to" cx={edge.x2} cy={edge.y2} r="3.2" />
|
|
326
|
+
{/if}
|
|
327
|
+
</g>
|
|
259
328
|
{/each}
|
|
260
329
|
</svg>
|
|
261
|
-
{/if}
|
|
262
|
-
|
|
263
|
-
{#each graph.boxes as cluster (graph.clusterKey(cluster))}
|
|
264
|
-
{@const shared = graph.boxAttrs(cluster)}
|
|
265
|
-
{@const caption = `${cluster.name} · ${cluster.caption ?? cluster.count}`}
|
|
266
|
-
<!-- One box shape at every depth. A containment layout nests boxes inside boxes, so a
|
|
267
|
-
childless one is still a box — rendering it as a node CARD instead put two
|
|
268
|
-
structures in one hierarchy and brought the card's furniture with it, down to a
|
|
269
|
-
row count that is `0` for anything without rows.
|
|
270
330
|
|
|
271
|
-
|
|
272
|
-
focusable, so it is a real <button> and gets Enter/Space, focus order and the right
|
|
273
|
-
announcement for free. A region is scenery and is a <div> that says so. The shared
|
|
274
|
-
attributes come from state, so the two cannot drift. -->
|
|
275
|
-
{#if cluster.nodeId}
|
|
331
|
+
{#each Object.entries(graph.cards) as [key, card] (key)}
|
|
276
332
|
<button
|
|
277
333
|
type="button"
|
|
278
|
-
data-graph-
|
|
279
|
-
{...
|
|
280
|
-
|
|
281
|
-
style
|
|
282
|
-
style:
|
|
283
|
-
style:
|
|
284
|
-
style={
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
graph.select(cluster.nodeId!)
|
|
288
|
-
}}
|
|
289
|
-
>
|
|
290
|
-
<span data-graph-cluster-label>{caption}</span>
|
|
291
|
-
</button>
|
|
292
|
-
{:else}
|
|
293
|
-
<div
|
|
294
|
-
data-graph-cluster
|
|
295
|
-
{...shared}
|
|
296
|
-
style:left="{cluster.x}px"
|
|
297
|
-
style:top="{cluster.y}px"
|
|
298
|
-
style:width="{cluster.w}px"
|
|
299
|
-
style:height="{cluster.h}px"
|
|
300
|
-
style={graph.groupStyleAttr(cluster.ramp ?? cluster.name)}
|
|
334
|
+
data-graph-node={key}
|
|
335
|
+
{...graph.cardAttrs(key)}
|
|
336
|
+
data-label-side={card.labelSide}
|
|
337
|
+
style:--label-angle={card.labelAngle === undefined ? undefined : `${card.labelAngle}deg`}
|
|
338
|
+
style:left="{card.x}px"
|
|
339
|
+
style:top="{card.y}px"
|
|
340
|
+
style:width="{card.w}px"
|
|
341
|
+
style:height="{card.h}px"
|
|
342
|
+
style={graph.groupStyleAttr(card.node.group)}
|
|
301
343
|
>
|
|
302
|
-
<span data-graph-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
<svg
|
|
308
|
-
width={graph.size.w}
|
|
309
|
-
height={graph.size.h}
|
|
310
|
-
role="img"
|
|
311
|
-
aria-label={graph.label}
|
|
312
|
-
style="position: absolute; top: 0; left: 0; pointer-events: none;"
|
|
313
|
-
>
|
|
314
|
-
<title>{graph.label}</title>
|
|
315
|
-
{#each graph.routedEdges as edge (edge.id)}
|
|
316
|
-
<g
|
|
317
|
-
data-graph-edge
|
|
318
|
-
data-edge-kind={edge.kind}
|
|
319
|
-
data-edge-relation={edge.relation}
|
|
320
|
-
data-edge-from={edge.fromKey}
|
|
321
|
-
data-edge-to={edge.toKey}
|
|
322
|
-
data-edge-state={graph.edgeState(edge)}
|
|
323
|
-
>
|
|
324
|
-
<path d={graph.edgePath(edge)} />
|
|
325
|
-
<circle data-graph-edge-dot="from" cx={edge.x1} cy={edge.y1} r="3.2" />
|
|
326
|
-
{#if arrows}
|
|
327
|
-
<!-- An edge HAS a direction — source to target — and two identical dots threw
|
|
328
|
-
that away. The tangent of both path shapes is horizontal at the endpoint,
|
|
329
|
-
so the heading is just the side the curve arrives from: -s2. No angle
|
|
330
|
-
maths, and it stays correct for a self-loop. -->
|
|
331
|
-
{@const d = -edge.s2}
|
|
332
|
-
<polygon
|
|
333
|
-
data-graph-edge-arrow
|
|
334
|
-
points="{edge.x2},{edge.y2} {edge.x2 - d * 9},{edge.y2 - 4.5} {edge.x2 -
|
|
335
|
-
d * 9},{edge.y2 + 4.5}"
|
|
336
|
-
/>
|
|
337
|
-
{:else}
|
|
338
|
-
<circle data-graph-edge-dot="to" cx={edge.x2} cy={edge.y2} r="3.2" />
|
|
339
|
-
{/if}
|
|
340
|
-
</g>
|
|
341
|
-
{/each}
|
|
342
|
-
</svg>
|
|
343
|
-
|
|
344
|
-
{#each Object.entries(graph.cards) as [key, card] (key)}
|
|
345
|
-
<button
|
|
346
|
-
type="button"
|
|
347
|
-
data-graph-node={key}
|
|
348
|
-
data-node-kind={card.node.kind}
|
|
349
|
-
data-node-group={card.node.group}
|
|
350
|
-
data-node-state={graph.nodeState(key)}
|
|
351
|
-
data-label-side={card.labelSide}
|
|
352
|
-
style:--label-angle={card.labelAngle === undefined
|
|
353
|
-
? undefined
|
|
354
|
-
: `${card.labelAngle}deg`}
|
|
355
|
-
data-node-headonly={card.vis.length === 0 && card.more <= 0 ? '' : undefined}
|
|
356
|
-
style:left="{card.x}px"
|
|
357
|
-
style:top="{card.y}px"
|
|
358
|
-
style:width="{card.w}px"
|
|
359
|
-
style:height="{card.h}px"
|
|
360
|
-
style={graph.groupStyleAttr(card.node.group)}
|
|
361
|
-
onclick={(event) => {
|
|
362
|
-
event.stopPropagation()
|
|
363
|
-
graph.select(key)
|
|
364
|
-
}}
|
|
365
|
-
>
|
|
366
|
-
<span data-graph-node-head>
|
|
367
|
-
<span data-graph-node-icon class={icons[card.node.kind ?? ''] ?? icons.fallback}></span>
|
|
368
|
-
<span data-graph-node-title>{card.node.label}</span>
|
|
369
|
-
{#if card.node.kind}
|
|
370
|
-
<!-- The glyph alone is a guess: layers vs eye vs bolt does not tell anyone what a
|
|
344
|
+
<span data-graph-node-head>
|
|
345
|
+
<span data-graph-node-icon class={icons[card.node.kind ?? ''] ?? icons.fallback}></span>
|
|
346
|
+
<span data-graph-node-title>{card.node.label}</span>
|
|
347
|
+
{#if card.node.kind}
|
|
348
|
+
<!-- The glyph alone is a guess: layers vs eye vs bolt does not tell anyone what a
|
|
371
349
|
materialized view is. Icon for the glance, tag for the answer. Underscores
|
|
372
350
|
are the WIRE format (dbd sends `materialized_view`), not something to
|
|
373
351
|
read. -->
|
|
374
|
-
|
|
375
|
-
{/if}
|
|
376
|
-
<span data-graph-node-count>{card.node.rows.length}</span>
|
|
377
|
-
</span>
|
|
378
|
-
{#each card.vis as row (row.name)}
|
|
379
|
-
<span data-graph-row data-graph-row-key={row.badges.length > 0 ? '' : undefined}>
|
|
380
|
-
{#if row.badges.length > 0}
|
|
381
|
-
<span data-row-badge={row.badges[0]} class={icons[row.badges[0]]}></span>
|
|
382
|
-
{:else}
|
|
383
|
-
<span data-row-badge-empty></span>
|
|
352
|
+
<span data-graph-node-kind>{readable(card.node.kind)}</span>
|
|
384
353
|
{/if}
|
|
385
|
-
|
|
386
|
-
<span data-graph-
|
|
354
|
+
<!-- A group counts what it stands for; a table counts its rows. -->
|
|
355
|
+
<span data-graph-node-count>{graph.cardCount(key)}</span>
|
|
387
356
|
</span>
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
357
|
+
{#each card.vis as row (row.name)}
|
|
358
|
+
<span data-graph-row data-graph-row-key={row.badges.length > 0 ? '' : undefined}>
|
|
359
|
+
{#if row.badges.length > 0}
|
|
360
|
+
<span data-row-badge={row.badges[0]} class={icons[row.badges[0]]}></span>
|
|
361
|
+
{:else}
|
|
362
|
+
<span data-row-badge-empty></span>
|
|
363
|
+
{/if}
|
|
364
|
+
<span data-graph-row-name>{row.name}</span>
|
|
365
|
+
<span data-graph-row-type>{row.type}</span>
|
|
366
|
+
</span>
|
|
367
|
+
{/each}
|
|
368
|
+
{#if graph.isCollapsed(key)}
|
|
369
|
+
<!-- The way in, as a real control inside the card — the same pattern as "+3 more":
|
|
370
|
+
the card's own press still selects, this one expands. -->
|
|
371
|
+
<span
|
|
372
|
+
role="button"
|
|
373
|
+
tabindex="0"
|
|
374
|
+
data-graph-group-toggle
|
|
375
|
+
data-graph-press="group"
|
|
376
|
+
data-graph-key={key}
|
|
377
|
+
aria-label={say('expand', { name: card.node.label })}
|
|
378
|
+
>{say('expandCount', { n: graph.memberCount(key) })}</span
|
|
379
|
+
>
|
|
380
|
+
{/if}
|
|
381
|
+
{#if graph.moreLabel(key)}
|
|
382
|
+
<!-- A real control. "+3 more" that does nothing is a statement dressed as an
|
|
391
383
|
affordance; this expands just this card. -->
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
data-expanded={graph.isExpanded(key) ? '' : undefined}
|
|
400
|
-
onclick={(event) => {
|
|
401
|
-
event.stopPropagation()
|
|
402
|
-
graph.toggleExpanded(key)
|
|
403
|
-
}}
|
|
404
|
-
onkeydown={(event) => {
|
|
405
|
-
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
406
|
-
event.preventDefault()
|
|
407
|
-
event.stopPropagation()
|
|
408
|
-
graph.toggleExpanded(key)
|
|
409
|
-
}}
|
|
410
|
-
>{graph.moreLabel(key)}</span
|
|
411
|
-
>
|
|
412
|
-
{/if}
|
|
413
|
-
</button>
|
|
414
|
-
{/each}
|
|
384
|
+
<span role="button" tabindex="0" data-graph-more {...graph.moreAttrs(key)}
|
|
385
|
+
>{graph.moreLabel(key)}</span
|
|
386
|
+
>
|
|
387
|
+
{/if}
|
|
388
|
+
</button>
|
|
389
|
+
{/each}
|
|
390
|
+
</div>
|
|
415
391
|
</div>
|
|
416
392
|
</div>
|
|
417
|
-
|
|
418
393
|
</div>
|
package/dist/Graph.svelte.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { GraphProps } from './types.js';
|
|
2
|
-
declare const Graph: import("svelte").Component<GraphProps, {}, "density" | "zoom">;
|
|
2
|
+
declare const Graph: import("svelte").Component<GraphProps, {}, "density" | "focusPath" | "zoom">;
|
|
3
3
|
type Graph = ReturnType<typeof Graph>;
|
|
4
4
|
export default Graph;
|