@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
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** One row per node for an entity list: its rows and the structural edges touching it. */
|
|
2
|
+
export function entityRows(model) {
|
|
3
|
+
return model.nodes.map((node) => ({
|
|
4
|
+
id: node.id,
|
|
5
|
+
label: node.label,
|
|
6
|
+
group: node.group,
|
|
7
|
+
kind: node.kind,
|
|
8
|
+
rowCount: node.rows.length,
|
|
9
|
+
refCount: model.edges.filter((e) => e.source === node.id || e.target === node.id).length,
|
|
10
|
+
note: node.note
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import type { GraphFields, GraphModel } from '../types.js';
|
|
2
|
-
|
|
3
|
-
* Resolves a consumer's `nodes`/`edges`/`fields` into the canonical model — ONCE.
|
|
4
|
-
* Everything downstream (layouts, routing, components) sees concrete types, so no
|
|
5
|
-
* field mapping leaks past this function.
|
|
6
|
-
*/
|
|
1
|
+
import type { GraphEdge, GraphFields, GraphModel } from '../types.js';
|
|
2
|
+
export declare function buildNeighbors(edges: GraphEdge[]): Map<string, Set<string>>;
|
|
7
3
|
export declare function normalizeGraph(nodes: unknown[], edges: unknown[], fields?: GraphFields): GraphModel;
|
package/dist/model/normalize.js
CHANGED
|
@@ -68,7 +68,11 @@ const CLAIMED_NODE_KEYS = [
|
|
|
68
68
|
'path',
|
|
69
69
|
'measures',
|
|
70
70
|
'rows',
|
|
71
|
-
'note'
|
|
71
|
+
'note',
|
|
72
|
+
'members',
|
|
73
|
+
'collapsed',
|
|
74
|
+
'layer',
|
|
75
|
+
'parent'
|
|
72
76
|
];
|
|
73
77
|
function buildMeta(source, fields) {
|
|
74
78
|
if (source === null || typeof source !== 'object')
|
|
@@ -99,9 +103,27 @@ function buildNode(source, fields) {
|
|
|
99
103
|
measures: measures(pick(source, fields.measures, 'measures')),
|
|
100
104
|
rows: buildRows(source, fields),
|
|
101
105
|
note: str(pick(source, fields.note, 'note')),
|
|
106
|
+
...nodeExtras(source, fields),
|
|
102
107
|
meta: buildMeta(source, fields)
|
|
103
108
|
};
|
|
104
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* `members` / `collapsed` (#166) and `layer` (#167), added only when present — like the edge
|
|
112
|
+
* extras, so every node a consumer already has stays key-for-key identical.
|
|
113
|
+
*/
|
|
114
|
+
function nodeExtras(source, fields) {
|
|
115
|
+
const extras = {};
|
|
116
|
+
const layer = num(pick(source, fields.layer, 'layer'));
|
|
117
|
+
if (layer !== undefined)
|
|
118
|
+
extras.layer = layer;
|
|
119
|
+
const members = pick(source, fields.members, 'members');
|
|
120
|
+
if (Array.isArray(members))
|
|
121
|
+
extras.members = members.map(String);
|
|
122
|
+
const collapsed = pick(source, fields.collapsed, 'collapsed');
|
|
123
|
+
if (typeof collapsed === 'boolean')
|
|
124
|
+
extras.collapsed = collapsed;
|
|
125
|
+
return extras;
|
|
126
|
+
}
|
|
105
127
|
function resolveEndpoint(source, spec, byId) {
|
|
106
128
|
const raw = str(pick(source, spec.path, spec.fallbackKey));
|
|
107
129
|
if (raw === undefined)
|
|
@@ -202,9 +224,32 @@ function buildEdge(source, fields, byId, seen) {
|
|
|
202
224
|
relation,
|
|
203
225
|
cardinality: str(pick(source, fields.cardinality, 'cardinality')),
|
|
204
226
|
action: str(pick(source, fields.action, 'action')),
|
|
205
|
-
unplaced
|
|
227
|
+
unplaced,
|
|
228
|
+
...edgeExtras(source, fields)
|
|
206
229
|
};
|
|
207
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* `overlay` and `weight`, added only when present so every edge a v1 consumer already has
|
|
233
|
+
* stays key-for-key identical — `toEqual` on an edge is a contract downstream specs rely on.
|
|
234
|
+
*/
|
|
235
|
+
const CONFORMANCE = new Set(['down', 'skip', 'up', 'level']);
|
|
236
|
+
function edgeExtras(source, fields) {
|
|
237
|
+
const extras = {};
|
|
238
|
+
// Only the vocabulary — an unknown word would be a style hook no theme knows.
|
|
239
|
+
const conformance = str(pick(source, fields.conformance, 'conformance'));
|
|
240
|
+
if (conformance && CONFORMANCE.has(conformance))
|
|
241
|
+
extras.conformance = conformance;
|
|
242
|
+
if (pick(source, fields.overlay, 'overlay'))
|
|
243
|
+
extras.overlay = true;
|
|
244
|
+
if (pick(source, fields.weakest, 'weakest'))
|
|
245
|
+
extras.weakest = true;
|
|
246
|
+
if (pick(source, fields.hidden, 'hidden'))
|
|
247
|
+
extras.hidden = true;
|
|
248
|
+
const weight = num(pick(source, fields.edgeWeight, 'weight'));
|
|
249
|
+
if (weight !== undefined)
|
|
250
|
+
extras.weight = weight;
|
|
251
|
+
return extras;
|
|
252
|
+
}
|
|
208
253
|
/**
|
|
209
254
|
* The source row an edge makes a foreign key, if any.
|
|
210
255
|
*
|
|
@@ -227,7 +272,7 @@ function markForeignKeys(byId, edges) {
|
|
|
227
272
|
}
|
|
228
273
|
}
|
|
229
274
|
}
|
|
230
|
-
function buildNeighbors(edges) {
|
|
275
|
+
export function buildNeighbors(edges) {
|
|
231
276
|
const neighbors = new Map();
|
|
232
277
|
const link = (a, b) => {
|
|
233
278
|
const set = neighbors.get(a) ?? new Set();
|
|
@@ -249,13 +294,46 @@ function buildNeighbors(edges) {
|
|
|
249
294
|
* Everything downstream (layouts, routing, components) sees concrete types, so no
|
|
250
295
|
* field mapping leaks past this function.
|
|
251
296
|
*/
|
|
297
|
+
/** A node's ancestry as ids, root first and itself last — cut where a cycle closes. */
|
|
298
|
+
function chainOf(id, parentOf) {
|
|
299
|
+
const chain = [id];
|
|
300
|
+
const seen = new Set(chain);
|
|
301
|
+
let parent = parentOf.get(id);
|
|
302
|
+
while (parent !== undefined && !seen.has(parent)) {
|
|
303
|
+
chain.unshift(parent);
|
|
304
|
+
seen.add(parent);
|
|
305
|
+
// A parent that is not a node ends the walk: it is kept as the container it names.
|
|
306
|
+
parent = parentOf.get(parent);
|
|
307
|
+
}
|
|
308
|
+
return chain;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Containment given as `parent` ids (#168) — the shape many hosts send — becomes a `path`.
|
|
312
|
+
*
|
|
313
|
+
* Only for a node with no `path` of its own, and only where the parent relation is in play: a
|
|
314
|
+
* node that HAS a parent, or that another node names as one (so a root still heads the paths
|
|
315
|
+
* of its children). Segments are node ids, since labels repeat across branches.
|
|
316
|
+
*/
|
|
317
|
+
function pathsFromParents(built, sources, fields) {
|
|
318
|
+
const parentOf = new Map();
|
|
319
|
+
built.forEach((node, i) => parentOf.set(node.id, str(pick(sources[i], fields.parent, 'parent'))));
|
|
320
|
+
const named = new Set([...parentOf.values()].filter((p) => p !== undefined));
|
|
321
|
+
for (const node of built) {
|
|
322
|
+
if (node.path || (parentOf.get(node.id) === undefined && !named.has(node.id)))
|
|
323
|
+
continue;
|
|
324
|
+
node.path = chainOf(node.id, parentOf);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
252
327
|
export function normalizeGraph(nodes, edges, fields = {}) {
|
|
253
328
|
const built = nodes.map((source) => buildNode(source, fields));
|
|
329
|
+
pathsFromParents(built, nodes, fields);
|
|
254
330
|
const byId = new Map(built.map((node) => [node.id, node]));
|
|
255
331
|
const seen = new Map();
|
|
256
332
|
const resolved = edges
|
|
257
333
|
.map((source) => buildEdge(source, fields, byId, seen))
|
|
258
334
|
.filter((edge) => edge !== null);
|
|
259
|
-
|
|
260
|
-
|
|
335
|
+
const structural = resolved.filter((edge) => !edge.overlay);
|
|
336
|
+
const overlays = resolved.filter((edge) => edge.overlay);
|
|
337
|
+
markForeignKeys(byId, structural);
|
|
338
|
+
return { nodes: built, edges: structural, overlays, byId, neighbors: buildNeighbors(structural) };
|
|
261
339
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RoutedEdge } from '../layout/types.js';
|
|
2
|
+
import type { GraphEdge, GraphModel } from '../types.js';
|
|
3
|
+
export type Relationship = {
|
|
4
|
+
direction: 'in' | 'out';
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
group?: string;
|
|
8
|
+
/** The canonical edge — always present. A relationship is a fact about the model. */
|
|
9
|
+
edge: GraphEdge;
|
|
10
|
+
/** Routed geometry, present only when the ACTIVE layout placed this edge. */
|
|
11
|
+
routed?: RoutedEdge;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* The selected node's relationships — every edge touching it, in model order.
|
|
15
|
+
*
|
|
16
|
+
* Read from `model.edges`, the canonical unfiltered model, NOT from the layout's edges. A layout
|
|
17
|
+
* filters: `cluster` drops edges whose endpoints were not laid out, and `neighborhood` lays out
|
|
18
|
+
* only the focus's neighbourhood. Since `focus` and `value` are independent, reading the
|
|
19
|
+
* layout's edges lets the state contradict itself: with `focus: 'audit.log'` (no neighbours) and
|
|
20
|
+
* `value: 'public.orders'`, `relationships` would be `[]` while the entity row's `refCount` for
|
|
21
|
+
* the same node is 1.
|
|
22
|
+
*
|
|
23
|
+
* Routed geometry is attached opportunistically — only for edges the active layout placed. A
|
|
24
|
+
* relationship is a fact about the model; its geometry is a fact about the current view.
|
|
25
|
+
*
|
|
26
|
+
* An UNPLACED far end is reported too, with the raw name as the label — it is not a node, so
|
|
27
|
+
* there is nothing to look up. Hiding it would be worse: "this references something we have not
|
|
28
|
+
* indexed" is exactly what a reader of a partial graph needs to see.
|
|
29
|
+
*/
|
|
30
|
+
export declare function relationshipsOf(model: GraphModel, id: string | null, routedEdges: RoutedEdge[]): Relationship[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** How `id` sits on an edge: not on it, both ends of it, or one end with a far side. */
|
|
2
|
+
function endpointsFor(edge, id) {
|
|
3
|
+
const isSource = edge.source === id;
|
|
4
|
+
const isTarget = edge.target === id;
|
|
5
|
+
if (!isSource && !isTarget)
|
|
6
|
+
return null;
|
|
7
|
+
return {
|
|
8
|
+
self: isSource && isTarget,
|
|
9
|
+
other: isSource ? edge.target : edge.source,
|
|
10
|
+
direction: isSource ? 'out' : 'in'
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The selected node's relationships — every edge touching it, in model order.
|
|
15
|
+
*
|
|
16
|
+
* Read from `model.edges`, the canonical unfiltered model, NOT from the layout's edges. A layout
|
|
17
|
+
* filters: `cluster` drops edges whose endpoints were not laid out, and `neighborhood` lays out
|
|
18
|
+
* only the focus's neighbourhood. Since `focus` and `value` are independent, reading the
|
|
19
|
+
* layout's edges lets the state contradict itself: with `focus: 'audit.log'` (no neighbours) and
|
|
20
|
+
* `value: 'public.orders'`, `relationships` would be `[]` while the entity row's `refCount` for
|
|
21
|
+
* the same node is 1.
|
|
22
|
+
*
|
|
23
|
+
* Routed geometry is attached opportunistically — only for edges the active layout placed. A
|
|
24
|
+
* relationship is a fact about the model; its geometry is a fact about the current view.
|
|
25
|
+
*
|
|
26
|
+
* An UNPLACED far end is reported too, with the raw name as the label — it is not a node, so
|
|
27
|
+
* there is nothing to look up. Hiding it would be worse: "this references something we have not
|
|
28
|
+
* indexed" is exactly what a reader of a partial graph needs to see.
|
|
29
|
+
*/
|
|
30
|
+
export function relationshipsOf(model, id, routedEdges) {
|
|
31
|
+
if (!id)
|
|
32
|
+
return [];
|
|
33
|
+
// A plain Map on purpose: a call-local lookup, discarded when the caller re-derives.
|
|
34
|
+
const routed = new Map(routedEdges.map((e) => [e.id, e]));
|
|
35
|
+
const describe = (other, direction, edge) => {
|
|
36
|
+
// normalizeGraph drops any edge whose endpoints do not resolve, so both ends of every
|
|
37
|
+
// edge in model.edges are in byId. No `?? other` fallback: an unreachable branch cannot
|
|
38
|
+
// be tested, and the package's 100% statement bar exists to keep that honest.
|
|
39
|
+
const node = model.byId.get(other);
|
|
40
|
+
return { direction, id: other, label: node.label, group: node.group, edge, routed: routed.get(edge.id) };
|
|
41
|
+
};
|
|
42
|
+
return model.edges.flatMap((edge) => {
|
|
43
|
+
const ends = endpointsFor(edge, id);
|
|
44
|
+
if (!ends)
|
|
45
|
+
return [];
|
|
46
|
+
if (ends.self)
|
|
47
|
+
return [describe(id, 'out', edge)];
|
|
48
|
+
const { other, direction } = ends;
|
|
49
|
+
return edge.unplaced ? [{ direction, id: other, label: other, edge }] : [describe(other, direction, edge)];
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TreeNode } from './tree.js';
|
|
2
|
+
import type { Cluster } from '../layout/types.js';
|
|
3
|
+
/** Every box's share of `measure`, by tree id. A box with none is absent from the map. */
|
|
4
|
+
export declare function shares(root: TreeNode, measure: string): Map<string, number>;
|
|
5
|
+
/**
|
|
6
|
+
* What a box carries for the shade channel: its share, or — when shading is on and it has
|
|
7
|
+
* none — a mark that the value is missing, so the theme never paints it as a zero.
|
|
8
|
+
*/
|
|
9
|
+
export declare function shadeFields(all: Map<string, number> | undefined, id: string): Pick<Cluster, 'shade' | 'missing'>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const clamp = (value) => Math.min(1, Math.max(0, value));
|
|
2
|
+
/** The box's own share, when its node declares one. */
|
|
3
|
+
const ownShare = (box, measure) => {
|
|
4
|
+
const value = box.node?.measures?.[measure];
|
|
5
|
+
return value === undefined ? undefined : clamp(value);
|
|
6
|
+
};
|
|
7
|
+
/** The mean of the children's shares, weighted by their size; a plain mean if none has size. */
|
|
8
|
+
function meanOf(children) {
|
|
9
|
+
if (children.length === 0)
|
|
10
|
+
return undefined;
|
|
11
|
+
const weight = children.reduce((total, child) => total + Math.max(0, child.size), 0);
|
|
12
|
+
if (weight === 0)
|
|
13
|
+
return children.reduce((total, child) => total + child.share, 0) / children.length;
|
|
14
|
+
return children.reduce((total, child) => total + child.share * Math.max(0, child.size), 0) / weight;
|
|
15
|
+
}
|
|
16
|
+
function visit(box, measure, out) {
|
|
17
|
+
const measured = [];
|
|
18
|
+
for (const child of box.children) {
|
|
19
|
+
const share = visit(child, measure, out);
|
|
20
|
+
if (share !== undefined)
|
|
21
|
+
measured.push({ share, size: child.value });
|
|
22
|
+
}
|
|
23
|
+
const share = ownShare(box, measure) ?? meanOf(measured);
|
|
24
|
+
if (share !== undefined)
|
|
25
|
+
out.set(box.id, share);
|
|
26
|
+
return share;
|
|
27
|
+
}
|
|
28
|
+
/** Every box's share of `measure`, by tree id. A box with none is absent from the map. */
|
|
29
|
+
export function shares(root, measure) {
|
|
30
|
+
const out = new Map();
|
|
31
|
+
visit(root, measure, out);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What a box carries for the shade channel: its share, or — when shading is on and it has
|
|
36
|
+
* none — a mark that the value is missing, so the theme never paints it as a zero.
|
|
37
|
+
*/
|
|
38
|
+
export function shadeFields(all, id) {
|
|
39
|
+
if (!all)
|
|
40
|
+
return {};
|
|
41
|
+
const share = all.get(id);
|
|
42
|
+
return share === undefined ? { missing: ['color'] } : { shade: share };
|
|
43
|
+
}
|
package/dist/model/tree.d.ts
CHANGED
|
@@ -3,8 +3,13 @@ export type TreeNode = {
|
|
|
3
3
|
/** Path joined by `/`. The root is `''`. */
|
|
4
4
|
id: string;
|
|
5
5
|
label: string;
|
|
6
|
-
/** Full path to this node, outermost first. Empty at the root. */
|
|
6
|
+
/** Full path to this node, outermost first. Empty at the root. Folding splices it. */
|
|
7
7
|
path: string[];
|
|
8
|
+
/**
|
|
9
|
+
* The node's path in the DATA — never folded. What a host knows a node by, so what a drill
|
|
10
|
+
* reports and what `focusPath` names; `path` is only where the box sits in the picture.
|
|
11
|
+
*/
|
|
12
|
+
address: string[];
|
|
8
13
|
/** 0 at the root. A layout materialising N levels reads this. */
|
|
9
14
|
depth: number;
|
|
10
15
|
/** The real node behind this box. Absent only on a container nothing declared. */
|
|
@@ -22,6 +27,7 @@ export type TreeNode = {
|
|
|
22
27
|
*/
|
|
23
28
|
export declare function buildTree(model: GraphModel, options?: {
|
|
24
29
|
measure?: string;
|
|
30
|
+
keep?: string[];
|
|
25
31
|
}): TreeNode;
|
|
26
32
|
/** The node at `path`, or undefined. */
|
|
27
33
|
export declare function findNode(root: TreeNode, path: string[]): TreeNode | undefined;
|
package/dist/model/tree.js
CHANGED
|
@@ -22,6 +22,7 @@ function container(path) {
|
|
|
22
22
|
id: idOf(path),
|
|
23
23
|
label: path[path.length - 1] ?? ROOT,
|
|
24
24
|
path,
|
|
25
|
+
address: path,
|
|
25
26
|
depth: path.length,
|
|
26
27
|
children: [],
|
|
27
28
|
value: 0
|
|
@@ -95,6 +96,8 @@ function attach(root, node, measure, degree) {
|
|
|
95
96
|
id: node.id,
|
|
96
97
|
label: node.label,
|
|
97
98
|
path: [],
|
|
99
|
+
// No address either: a pathless node has no place in the containment tree to drill to.
|
|
100
|
+
address: [],
|
|
98
101
|
depth: 1,
|
|
99
102
|
node,
|
|
100
103
|
children: [],
|
|
@@ -131,9 +134,13 @@ function summarise(node, depth) {
|
|
|
131
134
|
* edges, and folding it would silently drop them. The rule is "a wrapper is not a level", and
|
|
132
135
|
* a thing the data names is not a wrapper.
|
|
133
136
|
*/
|
|
134
|
-
function fold(node) {
|
|
135
|
-
node.children = node.children.map(fold);
|
|
136
|
-
|
|
137
|
+
function fold(node, keep) {
|
|
138
|
+
node.children = node.children.map((child) => fold(child, keep));
|
|
139
|
+
// A container ON the focus chain is the scope the reader asked for, not punctuation: folding
|
|
140
|
+
// it would make the focusPath naming it stop resolving. A host loading one level at a time
|
|
141
|
+
// sends exactly such a chain — a crate's modules, with nothing declaring the crate.
|
|
142
|
+
const onFocusChain = node.address.every((segment, i) => keep[i] === segment);
|
|
143
|
+
if (node.children.length === 1 && node.node === undefined && node.depth > 0 && !onFocusChain) {
|
|
137
144
|
const only = node.children[0];
|
|
138
145
|
// The child takes this box's PLACE, so the segment this box contributed is spliced out
|
|
139
146
|
// of its path — not merely trimmed from the end, which would leave a leaf sitting at
|
|
@@ -165,7 +172,7 @@ export function buildTree(model, options = {}) {
|
|
|
165
172
|
// a descendant arriving first synthesises the box, and its owner fills in the identity.
|
|
166
173
|
for (const node of model.nodes)
|
|
167
174
|
attach(root, node, measure, degree);
|
|
168
|
-
const folded = fold(root);
|
|
175
|
+
const folded = fold(root, options.keep ?? []);
|
|
169
176
|
sort(folded);
|
|
170
177
|
summarise(folded, 0);
|
|
171
178
|
return folded;
|
package/dist/preset.d.ts
CHANGED
|
@@ -12,6 +12,15 @@ export type GraphShades = {
|
|
|
12
12
|
stroke: string;
|
|
13
13
|
label: string;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* The sequential ramp a share is shaded on (#164): a palette family and the two steps it runs
|
|
17
|
+
* between. Unlike `groups` this is ORDINAL — more of the measure is more of the colour.
|
|
18
|
+
*/
|
|
19
|
+
export type GraphShadeRamp = {
|
|
20
|
+
family: string;
|
|
21
|
+
from: string;
|
|
22
|
+
to: string;
|
|
23
|
+
};
|
|
15
24
|
export type GraphPreset = {
|
|
16
25
|
/** Ordered ramp assigned to open-ended group names, wrapping when exhausted. */
|
|
17
26
|
groups: string[];
|
|
@@ -21,6 +30,8 @@ export type GraphPreset = {
|
|
|
21
30
|
};
|
|
22
31
|
patterns: string[];
|
|
23
32
|
using: GraphChannel;
|
|
33
|
+
/** The ramp a `shadeBy` share is painted on (#164). Independent of `using`. */
|
|
34
|
+
shade: GraphShadeRamp;
|
|
24
35
|
};
|
|
25
36
|
/**
|
|
26
37
|
* Mirrors `createChartPreset`'s shape on purpose — a consumer who has met one
|
|
@@ -32,6 +43,13 @@ export type GraphPreset = {
|
|
|
32
43
|
*/
|
|
33
44
|
export declare const defaultGraphPreset: GraphPreset;
|
|
34
45
|
export declare function createGraphPreset(overrides?: Partial<GraphPreset>): GraphPreset;
|
|
46
|
+
/**
|
|
47
|
+
* A share in 0..1 as custom properties, the way `resolveGroupStyles` resolves a group: the
|
|
48
|
+
* ramp step nearest the share for the fill, two steps on for the stroke, and a label MEASURED
|
|
49
|
+
* against that fill — rokkit's auto on-color (near-black or near-white by luminance), so the
|
|
50
|
+
* flip point falls out of the ramp instead of being guessed.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveShade(share: number, mode: 'light' | 'dark', preset?: GraphPreset): Record<'--group-fill' | '--group-stroke' | '--group-label', string>;
|
|
35
53
|
/**
|
|
36
54
|
* Resolves group names to CSS custom properties.
|
|
37
55
|
*
|
package/dist/preset.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { categoricalPalette } from '@rokkit/core';
|
|
1
|
+
import { categoricalPalette, pickOnColor, relativeLuminance } from '@rokkit/core';
|
|
2
2
|
/**
|
|
3
3
|
* Mirrors `createChartPreset`'s shape on purpose — a consumer who has met one
|
|
4
4
|
* has met both. Shade picks differ from chart's because a cluster is a large
|
|
@@ -17,7 +17,9 @@ export const defaultGraphPreset = {
|
|
|
17
17
|
dark: { fill: '900', stroke: '600', label: '100' }
|
|
18
18
|
},
|
|
19
19
|
patterns: ['diagonal', 'dots', 'triangles', 'hatch', 'lattice', 'swell', 'checkerboard', 'waves'],
|
|
20
|
-
using: 'color'
|
|
20
|
+
using: 'color',
|
|
21
|
+
// Neutral, so a shade never reads as a group's hue. The palette has no `slate`.
|
|
22
|
+
shade: { family: 'gray', from: '50', to: '900' }
|
|
21
23
|
};
|
|
22
24
|
export function createGraphPreset(overrides = {}) {
|
|
23
25
|
return {
|
|
@@ -26,7 +28,36 @@ export function createGraphPreset(overrides = {}) {
|
|
|
26
28
|
shades: {
|
|
27
29
|
light: { ...defaultGraphPreset.shades.light, ...overrides.shades?.light },
|
|
28
30
|
dark: { ...defaultGraphPreset.shades.dark, ...overrides.shades?.dark }
|
|
29
|
-
}
|
|
31
|
+
},
|
|
32
|
+
shade: { ...defaultGraphPreset.shade, ...overrides.shade }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** The family's steps from `from` to `to`, in that order — reversed in dark mode. */
|
|
36
|
+
function rampSteps(ramp, mode) {
|
|
37
|
+
const family = categoricalPalette[ramp.family] ?? categoricalPalette[defaultGraphPreset.shade.family];
|
|
38
|
+
const [lo, hi] = [Number(ramp.from), Number(ramp.to)].sort((a, b) => a - b);
|
|
39
|
+
const steps = Object.keys(family)
|
|
40
|
+
.map(Number)
|
|
41
|
+
.filter((step) => step >= lo && step <= hi)
|
|
42
|
+
.sort((a, b) => a - b)
|
|
43
|
+
.map((step) => family[String(step)]);
|
|
44
|
+
// In dark mode the paper is dark, so "more ink" runs toward the light end.
|
|
45
|
+
return mode === 'dark' ? steps.reverse() : steps;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A share in 0..1 as custom properties, the way `resolveGroupStyles` resolves a group: the
|
|
49
|
+
* ramp step nearest the share for the fill, two steps on for the stroke, and a label MEASURED
|
|
50
|
+
* against that fill — rokkit's auto on-color (near-black or near-white by luminance), so the
|
|
51
|
+
* flip point falls out of the ramp instead of being guessed.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveShade(share, mode, preset = defaultGraphPreset) {
|
|
54
|
+
const steps = rampSteps(preset.shade, mode);
|
|
55
|
+
const at = Math.round(Math.min(1, Math.max(0, share)) * (steps.length - 1));
|
|
56
|
+
const fill = steps[at];
|
|
57
|
+
return {
|
|
58
|
+
'--group-fill': fill,
|
|
59
|
+
'--group-stroke': steps[Math.min(steps.length - 1, at + 2)],
|
|
60
|
+
'--group-label': pickOnColor(relativeLuminance(fill))
|
|
30
61
|
};
|
|
31
62
|
}
|
|
32
63
|
/**
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { EntityRow, GraphStateConfig } from '../GraphState.svelte.js'
|
|
7
7
|
import NoteBlocks from './NoteBlocks.svelte'
|
|
8
8
|
import type { GraphFields } from '../types.js'
|
|
9
|
+
import { say } from '../messages.js'
|
|
9
10
|
|
|
10
11
|
let {
|
|
11
12
|
state: provided,
|
|
@@ -40,12 +41,13 @@
|
|
|
40
41
|
if (graph === own) own.update(config())
|
|
41
42
|
})
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
{ name: '
|
|
46
|
-
{ name: '
|
|
47
|
-
{ name: '
|
|
48
|
-
|
|
44
|
+
// $derived, so the headings follow a locale change.
|
|
45
|
+
const columns: TableColumn[] = $derived([
|
|
46
|
+
{ name: 'label', label: say('entity'), snippet: 'entity' },
|
|
47
|
+
{ name: 'rowCount', label: say('rows'), align: 'right', snippet: 'rows' },
|
|
48
|
+
{ name: 'refCount', label: say('refs'), align: 'right', snippet: 'refs' },
|
|
49
|
+
{ name: 'note', label: say('comment'), sortable: false, snippet: 'comment' }
|
|
50
|
+
])
|
|
49
51
|
</script>
|
|
50
52
|
|
|
51
53
|
{#snippet entity(_value: unknown, _column: TableColumn, row: Record<string, unknown>)}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import NoteBlocks from './NoteBlocks.svelte'
|
|
6
6
|
import { baseType, typeSize } from './column-type.js'
|
|
7
7
|
import type { GraphFields } from '../types.js'
|
|
8
|
+
import { counted, say } from '../messages.js'
|
|
9
|
+
import { interactions } from '../actions/interactions.js'
|
|
8
10
|
|
|
9
11
|
let {
|
|
10
12
|
state: provided,
|
|
@@ -44,22 +46,22 @@
|
|
|
44
46
|
</script>
|
|
45
47
|
|
|
46
48
|
{#if entity}
|
|
47
|
-
<article data-graph-entity data-node-kind={entity.kind} class={className}>
|
|
49
|
+
<article data-graph-entity data-node-kind={entity.kind} class={className} use:interactions={{ state: graph }}>
|
|
48
50
|
<header data-graph-entity-head>
|
|
49
51
|
{#if entity.group}<span data-graph-entity-group>{entity.group}.</span>{/if}
|
|
50
52
|
<h2 data-graph-entity-title>{entity.label}</h2>
|
|
51
|
-
<span data-graph-entity-count>{entity.rows.length}
|
|
53
|
+
<span data-graph-entity-count>{counted(entity.rows.length, 'columnOne', 'columnMany')}</span>
|
|
52
54
|
</header>
|
|
53
55
|
|
|
54
56
|
{#if entity.note}
|
|
55
57
|
<section data-graph-section="note">
|
|
56
|
-
<h3 data-graph-section-title>
|
|
58
|
+
<h3 data-graph-section-title>{say('comment')}</h3>
|
|
57
59
|
<NoteBlocks note={entity.note} />
|
|
58
60
|
</section>
|
|
59
61
|
{/if}
|
|
60
62
|
|
|
61
63
|
<section data-graph-section="columns">
|
|
62
|
-
<h3 data-graph-section-title>
|
|
64
|
+
<h3 data-graph-section-title>{say('columns')}</h3>
|
|
63
65
|
{#each entity.rows as row (row.name)}
|
|
64
66
|
<div data-graph-column>
|
|
65
67
|
<span data-graph-column-name>{row.name}</span>
|
|
@@ -77,11 +79,11 @@
|
|
|
77
79
|
|
|
78
80
|
{#if indexes.length > 0}
|
|
79
81
|
<section data-graph-section="indexes">
|
|
80
|
-
<h3 data-graph-section-title>
|
|
82
|
+
<h3 data-graph-section-title>{say('indexes')}</h3>
|
|
81
83
|
{#each indexes as index, i (i)}
|
|
82
84
|
<div data-graph-index>
|
|
83
85
|
<span data-graph-index-def>{index.def}</span>
|
|
84
|
-
{#if index.unique}<span data-graph-index-unique>unique</span>{/if}
|
|
86
|
+
{#if index.unique}<span data-graph-index-unique>{say('unique')}</span>{/if}
|
|
85
87
|
{#if index.name}<span data-graph-index-name>{index.name}</span>{/if}
|
|
86
88
|
</div>
|
|
87
89
|
{/each}
|
|
@@ -89,15 +91,16 @@
|
|
|
89
91
|
{/if}
|
|
90
92
|
|
|
91
93
|
<section data-graph-section="relationships">
|
|
92
|
-
<h3 data-graph-section-title>
|
|
94
|
+
<h3 data-graph-section-title>{say('relationships')}</h3>
|
|
93
95
|
{#if graph.relationships.length === 0}
|
|
94
|
-
<p data-graph-relationships-empty>
|
|
96
|
+
<p data-graph-relationships-empty>{say('noRelationships')}</p>
|
|
95
97
|
{:else}
|
|
96
98
|
{#each graph.relationships as rel, i (rel.edge.id + i)}
|
|
97
99
|
<button
|
|
98
100
|
type="button"
|
|
99
101
|
data-graph-relationship={rel.direction}
|
|
100
|
-
|
|
102
|
+
data-graph-press="select"
|
|
103
|
+
data-graph-key={rel.id}
|
|
101
104
|
>
|
|
102
105
|
{#if rel.group}<span data-graph-relationship-group>{rel.group}.</span>{/if}
|
|
103
106
|
<span data-graph-relationship-label>{rel.label}</span>
|
|
@@ -111,6 +114,6 @@
|
|
|
111
114
|
</article>
|
|
112
115
|
{:else}
|
|
113
116
|
<article data-graph-entity data-graph-entity-empty class={className}>
|
|
114
|
-
<p>
|
|
117
|
+
<p>{say('noEntity')}</p>
|
|
115
118
|
</article>
|
|
116
119
|
{/if}
|