@rokkit/graph 1.8.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 +59 -0
- package/dist/Graph.svelte +108 -134
- 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 +161 -52
- package/dist/GraphState.svelte.js +380 -304
- 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 +11 -32
- 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 +10 -3
- package/dist/index.js +9 -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 +26 -2
- 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 +12 -0
- package/dist/layout/matrix.js +20 -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 +79 -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 +64 -2
- 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 +72 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -136,6 +136,57 @@ const er = toGraphInput(model, 'er') // tables + refs
|
|
|
136
136
|
const deps = toGraphInput(model, 'dependencies') // tables + entities, deps as edges
|
|
137
137
|
```
|
|
138
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
|
+
|
|
139
190
|
## Icons need a safelist
|
|
140
191
|
|
|
141
192
|
The component picks a node's icon at runtime from its kind, so the class names never appear in
|
|
@@ -162,3 +213,11 @@ from `createGraphPreset({ using: 'color' | 'pattern' })`.
|
|
|
162
213
|
- [Package reference](../../docs/llms/packages/graph.txt) — data contract, layouts, dbd adapter
|
|
163
214
|
- [Component reference](../../docs/llms/components/graph.txt) — props, interaction, attributes
|
|
164
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
|
-
|
|
99
|
-
|
|
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
|
-
}
|
|
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)
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
let
|
|
116
|
-
let panFrom = { x: 0, y: 0, left: 0, top: 0 }
|
|
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
|
|
117
126
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
panning = true
|
|
122
|
-
panFrom = { x: event.clientX, y: event.clientY, left: paper.scrollLeft, top: paper.scrollTop }
|
|
123
|
-
paper.setPointerCapture(event.pointerId)
|
|
127
|
+
const onzoom = (direction: 'in' | 'out', at: { x: number; y: number }) => {
|
|
128
|
+
anchor = at
|
|
129
|
+
zoom = nextZoom(zoom, direction)
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const tx = $derived(Math.max(PAD, (vw - graph.contentSize.w * scale) / 2))
|
|
146
|
-
const ty = $derived(Math.max(PAD, (vh - graph.contentSize.h * scale) / 2))
|
|
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,32 +179,26 @@
|
|
|
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
|
+
<div data-graph-extent style:width="{frame.extent.w}px" style:height="{frame.extent.h}px">
|
|
192
193
|
<div
|
|
193
194
|
data-graph-world
|
|
194
195
|
data-graph-node-shape={graph.nodeShape}
|
|
195
196
|
data-graph-layout={graph.layoutName}
|
|
196
197
|
style="width: {graph.size.w}px; height: {graph.size
|
|
197
|
-
.h}px; transform: translate({tx}px, {ty}px) scale({scale}); --graph-label-counter-scale: {(
|
|
198
|
+
.h}px; transform: translate({frame.tx}px, {frame.ty}px) scale({scale}); --graph-label-counter-scale: {(
|
|
198
199
|
1 / scale
|
|
199
200
|
).toFixed(3)};"
|
|
200
201
|
>
|
|
201
|
-
<!-- Column headings. Three columns get away without them — the focus is visibly
|
|
202
|
-
central — but a depth-2 portrait is five columns and ambiguous without one.
|
|
203
|
-
Position, width and wording all come from the layout; this only prints them. -->
|
|
204
202
|
{#each graph.columns as column (column.side + column.depth)}
|
|
205
203
|
<span
|
|
206
204
|
data-graph-column
|
|
@@ -225,38 +223,24 @@
|
|
|
225
223
|
>
|
|
226
224
|
<title>{graph.label}</title>
|
|
227
225
|
{#each graph.clusters as cluster (graph.clusterKey(cluster))}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
compiler cannot tell which it is and neither can a screen reader. The shared
|
|
233
|
-
attributes come from state, so the branches cannot drift apart. -->
|
|
234
|
-
{#if cluster.nodeId}
|
|
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)}
|
|
235
230
|
<path
|
|
236
231
|
data-graph-wedge
|
|
237
|
-
{...
|
|
238
|
-
style={graph.
|
|
232
|
+
{...graph.boxAttrs(cluster)}
|
|
233
|
+
style={graph.boxStyleAttr(cluster)}
|
|
239
234
|
d={graph.wedgePath(cluster)}
|
|
240
235
|
role="button"
|
|
241
|
-
tabindex={0}
|
|
242
|
-
aria-label={caption}
|
|
243
|
-
onclick={(event: Event) => {
|
|
244
|
-
event.stopPropagation()
|
|
245
|
-
graph.select(cluster.nodeId!)
|
|
246
|
-
}}
|
|
247
|
-
onkeydown={(event: KeyboardEvent) => {
|
|
248
|
-
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
249
|
-
event.preventDefault()
|
|
250
|
-
event.stopPropagation()
|
|
251
|
-
graph.select(cluster.nodeId!)
|
|
252
|
-
}}><title>{caption}</title></path
|
|
236
|
+
tabindex={0}><title>{graph.caption(cluster)}</title></path
|
|
253
237
|
>
|
|
254
238
|
{:else}
|
|
255
239
|
<path
|
|
256
240
|
data-graph-wedge
|
|
257
|
-
{...
|
|
258
|
-
style={graph.
|
|
259
|
-
d={graph.wedgePath(cluster)}><title>{caption}</title></path
|
|
241
|
+
{...graph.boxAttrs(cluster)}
|
|
242
|
+
style={graph.boxStyleAttr(cluster)}
|
|
243
|
+
d={graph.wedgePath(cluster)}><title>{graph.caption(cluster)}</title></path
|
|
260
244
|
>
|
|
261
245
|
{/if}
|
|
262
246
|
{/each}
|
|
@@ -264,45 +248,38 @@
|
|
|
264
248
|
{/if}
|
|
265
249
|
|
|
266
250
|
{#each graph.boxes as cluster (graph.clusterKey(cluster))}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
<!-- One box shape at every depth. A containment layout nests boxes inside boxes, so a
|
|
270
|
-
childless one is still a box — rendering it as a node CARD instead put two
|
|
271
|
-
structures in one hierarchy and brought the card's furniture with it, down to a
|
|
272
|
-
row count that is `0` for anything without rows.
|
|
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.
|
|
273
253
|
|
|
274
|
-
Two branches, not one element with a conditional tag:
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
{#if cluster
|
|
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)}
|
|
279
259
|
<button
|
|
280
260
|
type="button"
|
|
281
261
|
data-graph-cluster
|
|
282
|
-
{...
|
|
262
|
+
{...graph.boxAttrs(cluster)}
|
|
283
263
|
style:left="{cluster.x}px"
|
|
284
264
|
style:top="{cluster.y}px"
|
|
285
265
|
style:width="{cluster.w}px"
|
|
286
266
|
style:height="{cluster.h}px"
|
|
287
|
-
style={graph.
|
|
288
|
-
|
|
289
|
-
event.stopPropagation()
|
|
290
|
-
graph.select(cluster.nodeId!)
|
|
291
|
-
}}
|
|
267
|
+
style={graph.boxStyleAttr(cluster)}
|
|
268
|
+
style:--shade={cluster.shade}
|
|
292
269
|
>
|
|
293
|
-
<span data-graph-cluster-label>{caption}</span>
|
|
270
|
+
<span data-graph-cluster-label>{graph.caption(cluster)}</span>
|
|
294
271
|
</button>
|
|
295
272
|
{:else}
|
|
296
273
|
<div
|
|
297
274
|
data-graph-cluster
|
|
298
|
-
{...
|
|
275
|
+
{...graph.boxAttrs(cluster)}
|
|
299
276
|
style:left="{cluster.x}px"
|
|
300
277
|
style:top="{cluster.y}px"
|
|
301
278
|
style:width="{cluster.w}px"
|
|
302
279
|
style:height="{cluster.h}px"
|
|
303
|
-
style={graph.
|
|
280
|
+
style={graph.boxStyleAttr(cluster)}
|
|
304
281
|
>
|
|
305
|
-
<span data-graph-cluster-label>{caption}</span>
|
|
282
|
+
<span data-graph-cluster-label>{graph.caption(cluster)}</span>
|
|
306
283
|
</div>
|
|
307
284
|
{/if}
|
|
308
285
|
{/each}
|
|
@@ -324,6 +301,11 @@
|
|
|
324
301
|
data-edge-to={edge.toKey}
|
|
325
302
|
data-edge-state={graph.edgeState(edge)}
|
|
326
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}
|
|
327
309
|
style:--edge-weight={graph.edgeWeight(edge)}
|
|
328
310
|
>
|
|
329
311
|
<path d={graph.edgePath(edge)} />
|
|
@@ -350,21 +332,14 @@
|
|
|
350
332
|
<button
|
|
351
333
|
type="button"
|
|
352
334
|
data-graph-node={key}
|
|
353
|
-
|
|
354
|
-
data-node-group={card.node.group}
|
|
355
|
-
data-node-state={graph.nodeState(key)}
|
|
335
|
+
{...graph.cardAttrs(key)}
|
|
356
336
|
data-label-side={card.labelSide}
|
|
357
337
|
style:--label-angle={card.labelAngle === undefined ? undefined : `${card.labelAngle}deg`}
|
|
358
|
-
data-node-headonly={card.vis.length === 0 && card.more <= 0 ? '' : undefined}
|
|
359
338
|
style:left="{card.x}px"
|
|
360
339
|
style:top="{card.y}px"
|
|
361
340
|
style:width="{card.w}px"
|
|
362
341
|
style:height="{card.h}px"
|
|
363
342
|
style={graph.groupStyleAttr(card.node.group)}
|
|
364
|
-
onclick={(event) => {
|
|
365
|
-
event.stopPropagation()
|
|
366
|
-
graph.select(key)
|
|
367
|
-
}}
|
|
368
343
|
>
|
|
369
344
|
<span data-graph-node-head>
|
|
370
345
|
<span data-graph-node-icon class={icons[card.node.kind ?? ''] ?? icons.fallback}></span>
|
|
@@ -374,9 +349,10 @@
|
|
|
374
349
|
materialized view is. Icon for the glance, tag for the answer. Underscores
|
|
375
350
|
are the WIRE format (dbd sends `materialized_view`), not something to
|
|
376
351
|
read. -->
|
|
377
|
-
<span data-graph-node-kind>{card.node.kind
|
|
352
|
+
<span data-graph-node-kind>{readable(card.node.kind)}</span>
|
|
378
353
|
{/if}
|
|
379
|
-
|
|
354
|
+
<!-- A group counts what it stands for; a table counts its rows. -->
|
|
355
|
+
<span data-graph-node-count>{graph.cardCount(key)}</span>
|
|
380
356
|
</span>
|
|
381
357
|
{#each card.vis as row (row.name)}
|
|
382
358
|
<span data-graph-row data-graph-row-key={row.badges.length > 0 ? '' : undefined}>
|
|
@@ -389,31 +365,29 @@
|
|
|
389
365
|
<span data-graph-row-type>{row.type}</span>
|
|
390
366
|
</span>
|
|
391
367
|
{/each}
|
|
392
|
-
{#if graph.
|
|
393
|
-
<!--
|
|
394
|
-
|
|
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. -->
|
|
395
371
|
<span
|
|
396
372
|
role="button"
|
|
397
373
|
tabindex="0"
|
|
398
|
-
data-graph-
|
|
399
|
-
data-graph-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
event.stopPropagation()
|
|
411
|
-
graph.toggleExpanded(key)
|
|
412
|
-
}}>{graph.moreLabel(key)}</span
|
|
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
|
|
383
|
+
affordance; this expands just this card. -->
|
|
384
|
+
<span role="button" tabindex="0" data-graph-more {...graph.moreAttrs(key)}
|
|
385
|
+
>{graph.moreLabel(key)}</span
|
|
413
386
|
>
|
|
414
387
|
{/if}
|
|
415
388
|
</button>
|
|
416
389
|
{/each}
|
|
417
390
|
</div>
|
|
391
|
+
</div>
|
|
418
392
|
</div>
|
|
419
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;
|
package/dist/GraphLegend.svelte
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import type { GraphState } from './GraphState.svelte.js'
|
|
16
16
|
import { DEFAULT_ICONS } from './icons.js'
|
|
17
|
+
import { choices } from './actions/choices.js'
|
|
18
|
+
import { say } from './messages.js'
|
|
19
|
+
import { readable } from './state/text.js'
|
|
17
20
|
|
|
18
21
|
type Section = 'kind' | 'relation' | 'group'
|
|
19
22
|
|
|
@@ -25,6 +28,21 @@
|
|
|
25
28
|
relations?: boolean
|
|
26
29
|
/** Group names present, each with its ramp colour. */
|
|
27
30
|
groups?: boolean
|
|
31
|
+
/**
|
|
32
|
+
* Which measure each channel of a polymetric view carries (#168) — three encodings at
|
|
33
|
+
* once are unreadable without a key.
|
|
34
|
+
*/
|
|
35
|
+
channels?: boolean
|
|
36
|
+
/**
|
|
37
|
+
* The two sides of an arc diagram, left then right (#169) — which relation is which is
|
|
38
|
+
* not something a reader can guess from the arcs. `true` names them from the data
|
|
39
|
+
* (`GraphState.sideNames`); a pair names them yourself.
|
|
40
|
+
*/
|
|
41
|
+
sides?: boolean | [string, string]
|
|
42
|
+
/** A key for the hidden-coupling stroke (#169). */
|
|
43
|
+
hidden?: boolean
|
|
44
|
+
/** What the shade means in a containment view (#164): the measure, on its ramp. */
|
|
45
|
+
shade?: boolean
|
|
28
46
|
icons?: Record<string, string>
|
|
29
47
|
/**
|
|
30
48
|
* Fires with the entry a reader activated. Given one, entries become real buttons;
|
|
@@ -38,15 +56,29 @@
|
|
|
38
56
|
kinds = false,
|
|
39
57
|
relations = false,
|
|
40
58
|
groups = false,
|
|
59
|
+
channels = false,
|
|
60
|
+
sides = undefined,
|
|
61
|
+
hidden = false,
|
|
62
|
+
shade = false,
|
|
41
63
|
icons: userIcons,
|
|
42
64
|
onpick
|
|
43
65
|
}: Props = $props()
|
|
44
66
|
|
|
45
67
|
const icons = $derived<Record<string, string>>({ ...DEFAULT_ICONS, ...userIcons })
|
|
46
|
-
const
|
|
68
|
+
const channelRows = $derived(channels ? graph.channelRows : [])
|
|
69
|
+
const sideLabels = $derived(
|
|
70
|
+
sides === true ? [graph.sideNames.below, graph.sideNames.above] : sides || null
|
|
71
|
+
)
|
|
72
|
+
const shadeRow = $derived(shade ? graph.shadeLegend : null)
|
|
73
|
+
const shown = $derived(
|
|
74
|
+
kinds || relations || groups || channelRows.length > 0 || Boolean(sideLabels) || hidden || Boolean(shadeRow)
|
|
75
|
+
)
|
|
47
76
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
77
|
+
/** A pick names its section and value in one choice; split at the first colon. */
|
|
78
|
+
function pick(choice: string) {
|
|
79
|
+
const at = choice.indexOf(':')
|
|
80
|
+
onpick?.(choice.slice(0, at) as Section, choice.slice(at + 1))
|
|
81
|
+
}
|
|
50
82
|
|
|
51
83
|
/** `data-legend-kind` / `-relation` / `-group`, so CSS and tests can address a section. */
|
|
52
84
|
const attrs = (section: Section, value: string) => ({ [`data-legend-${section}`]: value })
|
|
@@ -72,12 +104,7 @@
|
|
|
72
104
|
a screen reader — and a click handler on a `<span>` is unreachable by keyboard. -->
|
|
73
105
|
{#snippet row(section: Section, value: string)}
|
|
74
106
|
{#if onpick}
|
|
75
|
-
<button
|
|
76
|
-
type="button"
|
|
77
|
-
data-graph-legend-entry
|
|
78
|
-
{...attrs(section, value)}
|
|
79
|
-
onclick={() => onpick(section, value)}
|
|
80
|
-
>
|
|
107
|
+
<button type="button" data-graph-legend-entry {...attrs(section, value)} data-graph-choice="{section}:{value}">
|
|
81
108
|
{@render swatch(section, value)}
|
|
82
109
|
<span data-legend-label>{readable(value)}</span>
|
|
83
110
|
</button>
|
|
@@ -90,7 +117,7 @@
|
|
|
90
117
|
{/snippet}
|
|
91
118
|
|
|
92
119
|
{#if shown}
|
|
93
|
-
<div data-graph-legend aria-label=
|
|
120
|
+
<div data-graph-legend aria-label={say('legend')} use:choices={{ onchoose: pick }}>
|
|
94
121
|
{#if kinds}
|
|
95
122
|
{#each graph.kindsPresent as kind (kind)}
|
|
96
123
|
{@render row('kind', kind)}
|
|
@@ -108,5 +135,49 @@
|
|
|
108
135
|
{@render row('group', group)}
|
|
109
136
|
{/each}
|
|
110
137
|
{/if}
|
|
138
|
+
|
|
139
|
+
{#if sideLabels}
|
|
140
|
+
<span data-graph-legend-entry data-legend-side="below">
|
|
141
|
+
<span data-legend-label>← {sideLabels[0]}</span>
|
|
142
|
+
</span>
|
|
143
|
+
<span data-graph-legend-entry data-legend-side="above">
|
|
144
|
+
<span data-legend-label>{sideLabels[1]} →</span>
|
|
145
|
+
</span>
|
|
146
|
+
{/if}
|
|
147
|
+
|
|
148
|
+
{#if hidden}
|
|
149
|
+
<span data-graph-legend-entry data-legend-hidden>
|
|
150
|
+
<svg data-legend-swatch data-edge-hidden aria-hidden="true">
|
|
151
|
+
<line x1="2" y1="7" x2="26" y2="7" />
|
|
152
|
+
</svg>
|
|
153
|
+
<span data-legend-label>{say('legendHidden')}</span>
|
|
154
|
+
</span>
|
|
155
|
+
{/if}
|
|
156
|
+
|
|
157
|
+
{#if shadeRow}
|
|
158
|
+
<!-- The swatch is the ramp itself, low to high: the colours on the canvas, not a sample. -->
|
|
159
|
+
<span data-graph-legend-entry data-legend-shade={shadeRow.measure}>
|
|
160
|
+
<span
|
|
161
|
+
data-legend-swatch
|
|
162
|
+
data-legend-shade-swatch
|
|
163
|
+
style:--shade-low={shadeRow.low}
|
|
164
|
+
style:--shade-high={shadeRow.high}
|
|
165
|
+
aria-hidden="true"
|
|
166
|
+
></span>
|
|
167
|
+
<span data-legend-label>{say('shade')}</span>
|
|
168
|
+
<span data-legend-measure>{readable(shadeRow.measure)}</span>
|
|
169
|
+
<span data-legend-cap>{say('shadeScale')}</span>
|
|
170
|
+
</span>
|
|
171
|
+
{/if}
|
|
172
|
+
|
|
173
|
+
{#each channelRows as channel (channel.key)}
|
|
174
|
+
<!-- The cap is what the full extent of the channel stands for: past it, a box clamps. -->
|
|
175
|
+
<span data-graph-legend-entry data-legend-channel={channel.key}>
|
|
176
|
+
<span data-legend-swatch data-legend-channel-swatch={channel.key} aria-hidden="true"></span>
|
|
177
|
+
<span data-legend-label>{channel.label}</span>
|
|
178
|
+
<span data-legend-measure>{readable(channel.scale.measure)}</span>
|
|
179
|
+
<span data-legend-cap>{say('legendCap', { cap: channel.scale.cap })}</span>
|
|
180
|
+
</span>
|
|
181
|
+
{/each}
|
|
111
182
|
</div>
|
|
112
183
|
{/if}
|
|
@@ -21,6 +21,21 @@ type Props = {
|
|
|
21
21
|
relations?: boolean;
|
|
22
22
|
/** Group names present, each with its ramp colour. */
|
|
23
23
|
groups?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Which measure each channel of a polymetric view carries (#168) — three encodings at
|
|
26
|
+
* once are unreadable without a key.
|
|
27
|
+
*/
|
|
28
|
+
channels?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The two sides of an arc diagram, left then right (#169) — which relation is which is
|
|
31
|
+
* not something a reader can guess from the arcs. `true` names them from the data
|
|
32
|
+
* (`GraphState.sideNames`); a pair names them yourself.
|
|
33
|
+
*/
|
|
34
|
+
sides?: boolean | [string, string];
|
|
35
|
+
/** A key for the hidden-coupling stroke (#169). */
|
|
36
|
+
hidden?: boolean;
|
|
37
|
+
/** What the shade means in a containment view (#164): the measure, on its ramp. */
|
|
38
|
+
shade?: boolean;
|
|
24
39
|
icons?: Record<string, string>;
|
|
25
40
|
/**
|
|
26
41
|
* Fires with the entry a reader activated. Given one, entries become real buttons;
|