@fusion.dev/helix-graph 0.119.0 → 0.121.0
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 +171 -6
- package/dist/connect-graph/arrange.d.ts +18 -0
- package/dist/connect-graph/arrange.js +92 -0
- package/dist/connect-graph/connect-graph.d.ts +58 -17
- package/dist/connect-graph/connect-graph.js +155 -34
- package/dist/connect-graph/diff.d.ts +50 -0
- package/dist/connect-graph/diff.js +48 -0
- package/dist/connect-graph/looks.d.ts +39 -3
- package/dist/connect-graph/looks.js +29 -6
- package/dist/connect-graph/seed.js +11 -2
- package/dist/connect-graph/stencil.d.ts +34 -0
- package/dist/connect-graph/stencil.js +121 -0
- package/dist/cosmos/cosmos-driver.d.ts +67 -0
- package/dist/cosmos/cosmos-driver.js +467 -0
- package/dist/cosmos/index.d.ts +1 -0
- package/dist/cosmos/index.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/overlay/overlay.d.ts +59 -0
- package/dist/overlay/overlay.js +296 -0
- package/dist/sigma/node-program.d.ts +71 -0
- package/dist/sigma/node-program.js +170 -0
- package/dist/sigma/sigma-driver.d.ts +44 -3
- package/dist/sigma/sigma-driver.js +197 -40
- package/package.json +25 -6
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphZoomEvent, isRtl, watchTheme, } from '@fusion.dev/helix-core';
|
|
2
|
-
import {
|
|
1
|
+
import { HelixGraphPanEvent, HelixGraphPartChangeEvent, HelixGraphSignalEvent, HelixGraphZoomEvent, isRtl, watchTheme, } from '@fusion.dev/helix-core';
|
|
2
|
+
import { arrangeByGroup } from './arrange.js';
|
|
3
|
+
import { diffGraph, isEmpty, linkKey, } from './diff.js';
|
|
4
|
+
import { graphColorsFrom, looksOf, toneColor, } from './looks.js';
|
|
3
5
|
import { seedPositions } from './seed.js';
|
|
6
|
+
import { fitToStencil } from './stencil.js';
|
|
4
7
|
const viewportOf = (frame) => {
|
|
5
8
|
const box = (frame.shadowRoot?.querySelector('[part="viewport"]') ?? frame).getBoundingClientRect();
|
|
6
9
|
return { height: box.height, width: box.width };
|
|
@@ -9,7 +12,13 @@ const still = () => typeof matchMedia === 'function' && matchMedia('(prefers-red
|
|
|
9
12
|
export const connectGraph = (frame, driver, options = {}) => {
|
|
10
13
|
const graph = frame;
|
|
11
14
|
let known = new Map();
|
|
15
|
+
let drawnNodes = new Map();
|
|
16
|
+
let drawnLinks = new Map();
|
|
17
|
+
let first = true;
|
|
18
|
+
let waiting = new Set();
|
|
19
|
+
let owed = false;
|
|
12
20
|
let colors = null;
|
|
21
|
+
let current;
|
|
13
22
|
let stopSettling;
|
|
14
23
|
let pressed;
|
|
15
24
|
let pending = 0;
|
|
@@ -33,58 +42,153 @@ export const connectGraph = (frame, driver, options = {}) => {
|
|
|
33
42
|
}
|
|
34
43
|
}
|
|
35
44
|
}
|
|
36
|
-
const
|
|
37
|
-
|
|
45
|
+
const notes = new Map(nodes.map((node) => [node.id, node]));
|
|
46
|
+
current = looksOf({
|
|
38
47
|
colors,
|
|
39
48
|
font: getComputedStyle(frame)
|
|
40
49
|
.fontFamily.split(',')[0]
|
|
41
50
|
?.trim()
|
|
42
51
|
.replace(/^["']|["']$/g, ''),
|
|
43
|
-
groupOf: (id) => groups.get(id),
|
|
44
52
|
groups: nodes.flatMap((node) => (node.group === undefined ? [] : [node.group])),
|
|
45
53
|
near,
|
|
54
|
+
noteOf: (id) => notes.get(id),
|
|
46
55
|
selected,
|
|
47
56
|
shown: graph.shown,
|
|
48
|
-
})
|
|
57
|
+
});
|
|
58
|
+
driver.look(current);
|
|
59
|
+
if (driver.groups) {
|
|
60
|
+
const members = new Map();
|
|
61
|
+
for (const node of nodes) {
|
|
62
|
+
if (node.group !== undefined) {
|
|
63
|
+
const list = members.get(node.group);
|
|
64
|
+
if (list) {
|
|
65
|
+
list.push(node.id);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
members.set(node.group, [node.id]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const own = selected === undefined ? undefined : notes.get(selected)?.group;
|
|
73
|
+
const looks = current;
|
|
74
|
+
driver.groups([...members].map(([name, ids]) => ({
|
|
75
|
+
color: looks.colorOf(ids.find((id) => notes.get(id)?.tone === undefined) ?? ids[0] ?? ''),
|
|
76
|
+
faded: selected !== undefined && own !== name,
|
|
77
|
+
ids,
|
|
78
|
+
name,
|
|
79
|
+
})));
|
|
80
|
+
}
|
|
49
81
|
};
|
|
50
|
-
const draw = () => {
|
|
51
|
-
stopSettling
|
|
52
|
-
|
|
82
|
+
const draw = (whole = false) => {
|
|
83
|
+
if (stopSettling) {
|
|
84
|
+
stopSettling();
|
|
85
|
+
stopSettling = undefined;
|
|
86
|
+
}
|
|
53
87
|
cancelAnimationFrame(pending);
|
|
54
88
|
const nodes = graph.nodes ?? [];
|
|
55
89
|
const ids = new Set(nodes.map((node) => node.id));
|
|
56
|
-
const
|
|
57
|
-
const {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
const nextLinks = new Map();
|
|
91
|
+
for (const { label, source, target } of graph.links ?? []) {
|
|
92
|
+
if (source !== target && ids.has(source) && ids.has(target)) {
|
|
93
|
+
nextLinks.set(linkKey({ source, target }), { label, source, target });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
let { positions, unplaced } = seedPositions(nodes, [...nextLinks.values()], known);
|
|
97
|
+
const shaped = options.stencil && ids.size > 0 && unplaced.size === ids.size
|
|
98
|
+
? fitToStencil(arrangeByGroup(nodes, [...nextLinks.values()]), options.stencil)
|
|
99
|
+
: undefined;
|
|
100
|
+
if (shaped) {
|
|
101
|
+
positions = shaped;
|
|
102
|
+
unplaced = new Set();
|
|
103
|
+
known = new Map(shaped);
|
|
104
|
+
}
|
|
105
|
+
const degree = new Map();
|
|
106
|
+
for (const { source, target } of nextLinks.values()) {
|
|
107
|
+
degree.set(source, (degree.get(source) ?? 0) + 1);
|
|
108
|
+
degree.set(target, (degree.get(target) ?? 0) + 1);
|
|
109
|
+
}
|
|
110
|
+
const nextNodes = new Map();
|
|
111
|
+
for (const node of nodes) {
|
|
112
|
+
const at = positions.get(node.id);
|
|
113
|
+
nextNodes.set(node.id, {
|
|
114
|
+
id: node.id,
|
|
115
|
+
label: node.label || node.id,
|
|
116
|
+
size: Number.isFinite(node.size)
|
|
117
|
+
? node.size
|
|
118
|
+
: 1 + Math.min(Math.sqrt(degree.get(node.id) ?? 0), 6),
|
|
119
|
+
x: at.x,
|
|
120
|
+
y: at.y,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
for (const id of waiting) {
|
|
124
|
+
if (ids.has(id)) {
|
|
125
|
+
unplaced.add(id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const incremental = !first && !whole && driver.update !== undefined;
|
|
129
|
+
if (incremental) {
|
|
130
|
+
const changes = diffGraph({ links: drawnLinks, nodes: drawnNodes }, { links: nextLinks, nodes: nextNodes });
|
|
131
|
+
if (!isEmpty(changes)) {
|
|
132
|
+
driver.update?.(changes, !still());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
graph.drawn?.(false);
|
|
137
|
+
driver.draw({ links: [...nextLinks.values()], nodes: [...nextNodes.values()] });
|
|
138
|
+
}
|
|
139
|
+
drawnNodes = nextNodes;
|
|
140
|
+
drawnLinks = nextLinks;
|
|
141
|
+
known = positions;
|
|
72
142
|
restyle();
|
|
143
|
+
if (first || whole) {
|
|
144
|
+
owed = true;
|
|
145
|
+
}
|
|
146
|
+
first = false;
|
|
73
147
|
if (unplaced.size > 0 && driver.settle) {
|
|
148
|
+
waiting = unplaced;
|
|
74
149
|
stopSettling = driver.settle(unplaced, (settled) => {
|
|
75
150
|
stopSettling = undefined;
|
|
151
|
+
waiting = new Set();
|
|
76
152
|
known = new Map(settled);
|
|
153
|
+
for (const [id, at] of settled) {
|
|
154
|
+
const node = drawnNodes.get(id);
|
|
155
|
+
if (node) {
|
|
156
|
+
drawnNodes.set(id, { ...node, x: at.x, y: at.y });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
77
159
|
graph.laidOut?.(Object.fromEntries([...settled].map(([id, at]) => [id, { ...at }])));
|
|
78
|
-
|
|
160
|
+
if (owed) {
|
|
161
|
+
owed = false;
|
|
162
|
+
graph.drawn?.(true);
|
|
163
|
+
}
|
|
79
164
|
});
|
|
80
165
|
return;
|
|
81
166
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
167
|
+
waiting = new Set();
|
|
168
|
+
if (shaped) {
|
|
169
|
+
graph.laidOut?.(Object.fromEntries([...shaped].map(([id, at]) => [id, { ...at }])));
|
|
170
|
+
}
|
|
171
|
+
if (owed) {
|
|
172
|
+
pending = requestAnimationFrame(() => {
|
|
173
|
+
if (connected && owed) {
|
|
174
|
+
owed = false;
|
|
175
|
+
graph.drawn?.(true);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const handleSignal = (event) => {
|
|
181
|
+
const { from, to, tone } = event.detail;
|
|
182
|
+
if (!driver.signal || !drawnNodes.has(from) || !drawnNodes.has(to)) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
colors ??= graphColorsFrom(frame);
|
|
186
|
+
const toned = tone !== undefined && colors ? toneColor(colors, tone) : undefined;
|
|
187
|
+
driver.signal(from, to, toned
|
|
188
|
+
? { from: toned, to: toned }
|
|
189
|
+
: current
|
|
190
|
+
? { from: current.colorOf(from), to: current.colorOf(to) }
|
|
191
|
+
: undefined, !still());
|
|
88
192
|
};
|
|
89
193
|
const handlePartChange = (event) => {
|
|
90
194
|
const { kind } = event.detail;
|
|
@@ -95,7 +199,21 @@ export const connectGraph = (frame, driver, options = {}) => {
|
|
|
95
199
|
restyle();
|
|
96
200
|
const selected = graph.active;
|
|
97
201
|
if (selected && selected !== pressed && options.center !== false) {
|
|
98
|
-
|
|
202
|
+
const near = [selected];
|
|
203
|
+
for (const { source, target } of drawnLinks.values()) {
|
|
204
|
+
if (source === selected) {
|
|
205
|
+
near.push(target);
|
|
206
|
+
}
|
|
207
|
+
else if (target === selected) {
|
|
208
|
+
near.push(source);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (driver.fit && near.length > 1) {
|
|
212
|
+
driver.fit(near, !still());
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
driver.center?.(selected, !still());
|
|
216
|
+
}
|
|
99
217
|
}
|
|
100
218
|
pressed = undefined;
|
|
101
219
|
}
|
|
@@ -117,6 +235,7 @@ export const connectGraph = (frame, driver, options = {}) => {
|
|
|
117
235
|
}
|
|
118
236
|
};
|
|
119
237
|
frame.addEventListener(HelixGraphPartChangeEvent.eventName, handlePartChange);
|
|
238
|
+
frame.addEventListener(HelixGraphSignalEvent.eventName, handleSignal);
|
|
120
239
|
frame.addEventListener(HelixGraphPanEvent.eventName, handlePan);
|
|
121
240
|
frame.addEventListener(HelixGraphZoomEvent.eventName, handleZoom);
|
|
122
241
|
const stopPress = driver.onPress?.((id) => {
|
|
@@ -147,6 +266,7 @@ export const connectGraph = (frame, driver, options = {}) => {
|
|
|
147
266
|
stopPress?.();
|
|
148
267
|
stopTheme();
|
|
149
268
|
frame.removeEventListener(HelixGraphPartChangeEvent.eventName, handlePartChange);
|
|
269
|
+
frame.removeEventListener(HelixGraphSignalEvent.eventName, handleSignal);
|
|
150
270
|
frame.removeEventListener(HelixGraphPanEvent.eventName, handlePan);
|
|
151
271
|
frame.removeEventListener(HelixGraphZoomEvent.eventName, handleZoom);
|
|
152
272
|
if (focusable) {
|
|
@@ -163,7 +283,8 @@ export const connectGraph = (frame, driver, options = {}) => {
|
|
|
163
283
|
},
|
|
164
284
|
relayout: () => {
|
|
165
285
|
known = new Map();
|
|
166
|
-
|
|
286
|
+
waiting = new Set();
|
|
287
|
+
draw(true);
|
|
167
288
|
},
|
|
168
289
|
restyle: () => {
|
|
169
290
|
colors = null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/** A note as the renderer draws it, placed. */
|
|
2
|
+
export interface DrawnNode {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
/** How large it is drawn, relative to the others: the note's `size`, or from how many links it has. */
|
|
6
|
+
size: number;
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
}
|
|
10
|
+
/** A link as the renderer draws it, between two notes the graph holds. */
|
|
11
|
+
export interface DrawnLink {
|
|
12
|
+
label?: string | undefined;
|
|
13
|
+
source: string;
|
|
14
|
+
target: string;
|
|
15
|
+
}
|
|
16
|
+
/** What changed between two drawings of a graph, which a renderer applies in place. */
|
|
17
|
+
export interface GraphChanges {
|
|
18
|
+
/** Links to draw that were not drawn. */
|
|
19
|
+
addLinks: DrawnLink[];
|
|
20
|
+
/** Notes to draw that were not drawn, placed. */
|
|
21
|
+
addNodes: DrawnNode[];
|
|
22
|
+
/** Notes still drawn whose name, size or place changed. */
|
|
23
|
+
changeNodes: DrawnNode[];
|
|
24
|
+
/** Links no longer drawn. */
|
|
25
|
+
removeLinks: DrawnLink[];
|
|
26
|
+
/** Notes no longer drawn, by `id`. */
|
|
27
|
+
removeNodes: string[];
|
|
28
|
+
}
|
|
29
|
+
/** A link's key: its two ends, which no `id` can contain both of by accident. */
|
|
30
|
+
export declare const linkKey: (link: {
|
|
31
|
+
source: string;
|
|
32
|
+
target: string;
|
|
33
|
+
}) => string;
|
|
34
|
+
/** Whether a diff changed nothing. */
|
|
35
|
+
export declare const isEmpty: (changes: GraphChanges) => boolean;
|
|
36
|
+
/**
|
|
37
|
+
* What changed from one drawing to the next, matched by `id` and by a link's
|
|
38
|
+
* two ends.
|
|
39
|
+
*
|
|
40
|
+
* **Linear in the size of the graph and nothing more**, so a page can hand the
|
|
41
|
+
* frame a new array for every message on a busy feed and the renderer is told
|
|
42
|
+
* only what moved: ten thousand notes with one added is one note drawn.
|
|
43
|
+
*/
|
|
44
|
+
export declare const diffGraph: (before: {
|
|
45
|
+
links: ReadonlyMap<string, DrawnLink>;
|
|
46
|
+
nodes: ReadonlyMap<string, DrawnNode>;
|
|
47
|
+
}, after: {
|
|
48
|
+
links: ReadonlyMap<string, DrawnLink>;
|
|
49
|
+
nodes: ReadonlyMap<string, DrawnNode>;
|
|
50
|
+
}) => GraphChanges;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const linkKey = (link) => `${link.source}\u0000${link.target}`;
|
|
2
|
+
export const isEmpty = (changes) => changes.addLinks.length === 0 &&
|
|
3
|
+
changes.addNodes.length === 0 &&
|
|
4
|
+
changes.changeNodes.length === 0 &&
|
|
5
|
+
changes.removeLinks.length === 0 &&
|
|
6
|
+
changes.removeNodes.length === 0;
|
|
7
|
+
export const diffGraph = (before, after) => {
|
|
8
|
+
const changes = {
|
|
9
|
+
addLinks: [],
|
|
10
|
+
addNodes: [],
|
|
11
|
+
changeNodes: [],
|
|
12
|
+
removeLinks: [],
|
|
13
|
+
removeNodes: [],
|
|
14
|
+
};
|
|
15
|
+
for (const [id, node] of after.nodes) {
|
|
16
|
+
const was = before.nodes.get(id);
|
|
17
|
+
if (!was) {
|
|
18
|
+
changes.addNodes.push(node);
|
|
19
|
+
}
|
|
20
|
+
else if (was.label !== node.label ||
|
|
21
|
+
was.size !== node.size ||
|
|
22
|
+
was.x !== node.x ||
|
|
23
|
+
was.y !== node.y) {
|
|
24
|
+
changes.changeNodes.push(node);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const id of before.nodes.keys()) {
|
|
28
|
+
if (!after.nodes.has(id)) {
|
|
29
|
+
changes.removeNodes.push(id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const [key, link] of after.links) {
|
|
33
|
+
const was = before.links.get(key);
|
|
34
|
+
if (!was) {
|
|
35
|
+
changes.addLinks.push(link);
|
|
36
|
+
}
|
|
37
|
+
else if (was.label !== link.label) {
|
|
38
|
+
changes.removeLinks.push(was);
|
|
39
|
+
changes.addLinks.push(link);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const [key, link] of before.links) {
|
|
43
|
+
if (!after.links.has(key)) {
|
|
44
|
+
changes.removeLinks.push(link);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return changes;
|
|
48
|
+
};
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
* where the frame is.
|
|
4
4
|
*/
|
|
5
5
|
export interface GraphColors {
|
|
6
|
+
/** A signal meaning something to look at: `accent`. */
|
|
7
|
+
accent: string;
|
|
8
|
+
/** A signal meaning something went wrong: `danger`. */
|
|
9
|
+
danger: string;
|
|
6
10
|
/** What the drawing is laid on, which faded notes are mixed toward: `surface-sunken`. */
|
|
7
11
|
ground: string;
|
|
8
12
|
/** The links: `border`. */
|
|
@@ -17,6 +21,12 @@ export interface GraphColors {
|
|
|
17
21
|
primary: string;
|
|
18
22
|
/** One color for each group, in the order of the groups' names: `series-1` and on. */
|
|
19
23
|
series: string[];
|
|
24
|
+
/** A signal meaning something done: `success`. */
|
|
25
|
+
success: string;
|
|
26
|
+
/** A signal meaning something to know: `info`. */
|
|
27
|
+
info: string;
|
|
28
|
+
/** A signal meaning something to watch: `warning`. */
|
|
29
|
+
warning: string;
|
|
20
30
|
}
|
|
21
31
|
/**
|
|
22
32
|
* The page's colors where the frame is, or `null` while they cannot be read.
|
|
@@ -48,11 +58,31 @@ export interface NodeLook {
|
|
|
48
58
|
* neighbors drew their names over each other in a knot nobody could read.
|
|
49
59
|
*/
|
|
50
60
|
label: 'always' | 'never' | 'room';
|
|
61
|
+
/** How heavily it is drawn, where the note says. */
|
|
62
|
+
emphasis?: 'outlined' | 'solid' | 'subtle' | undefined;
|
|
63
|
+
/** How strongly it glows, from 0 to 1: the selected note's halo. */
|
|
64
|
+
glow: number;
|
|
65
|
+
/** What it is drawn as made of, where the note says. */
|
|
66
|
+
material?: 'frosted' | 'glass' | undefined;
|
|
51
67
|
/** Drawn over the rest, as the selected note and its neighbors are. */
|
|
52
68
|
raised: boolean;
|
|
53
69
|
/** The selected note. */
|
|
54
70
|
selected: boolean;
|
|
71
|
+
/** Its shape, where the note says. */
|
|
72
|
+
shape?: 'circle' | 'diamond' | 'ring' | 'square' | undefined;
|
|
55
73
|
}
|
|
74
|
+
/** A tone from the `tone` axis. */
|
|
75
|
+
export type GraphTone = 'accent' | 'danger' | 'info' | 'neutral' | 'primary' | 'success' | 'warning';
|
|
76
|
+
/** What the looks read off one note. */
|
|
77
|
+
export interface NoteFacts {
|
|
78
|
+
emphasis?: NodeLook['emphasis'];
|
|
79
|
+
group?: string | undefined;
|
|
80
|
+
material?: NodeLook['material'];
|
|
81
|
+
shape?: NodeLook['shape'];
|
|
82
|
+
tone?: GraphTone | undefined;
|
|
83
|
+
}
|
|
84
|
+
/** A tone's color: `neutral` is the muted ink, and no tone is `primary`. */
|
|
85
|
+
export declare const toneColor: (colors: GraphColors, tone: GraphTone | undefined) => string;
|
|
56
86
|
/** How one link is drawn. */
|
|
57
87
|
export interface LinkLook {
|
|
58
88
|
color: string;
|
|
@@ -61,12 +91,18 @@ export interface LinkLook {
|
|
|
61
91
|
}
|
|
62
92
|
/** How every note and link is drawn, asked one at a time, so ten thousand cost nothing until drawn. */
|
|
63
93
|
export interface GraphLooks {
|
|
94
|
+
/** A note's own color, its tone's or its group's, whatever the selection fades. */
|
|
95
|
+
colorOf(id: string): string;
|
|
64
96
|
/** The typeface the labels are drawn in: the first of the frame's own. */
|
|
65
97
|
font: string;
|
|
98
|
+
/** What the drawing is laid on, which a name is ringed in so it reads over the links. */
|
|
99
|
+
ground: string;
|
|
66
100
|
/** The color the labels are drawn in. */
|
|
67
101
|
ink: string;
|
|
68
102
|
/** What a name is drawn on while the pointer is over its note. */
|
|
69
103
|
overlay: string;
|
|
104
|
+
/** The quieter ink a name at rest is drawn in, beside the selected note's and its neighbors'. */
|
|
105
|
+
quiet: string;
|
|
70
106
|
link(source: string, target: string): LinkLook;
|
|
71
107
|
node(id: string): NodeLook;
|
|
72
108
|
}
|
|
@@ -74,8 +110,8 @@ export interface GraphLooks {
|
|
|
74
110
|
* How the graph is drawn for one selection.
|
|
75
111
|
*
|
|
76
112
|
* **The neighborhood stands out and the rest steps back**, which is what
|
|
77
|
-
* selecting a note in a graph of thousands is for: the selected note
|
|
78
|
-
*
|
|
113
|
+
* selecting a note in a graph of thousands is for: the selected note
|
|
114
|
+
* glowing, the notes it links to and from in their groups' colors with
|
|
79
115
|
* their names drawn where there is room, and every other note faded toward
|
|
80
116
|
* the ground, unnamed. With
|
|
81
117
|
* nothing selected, every note is in its group's color.
|
|
@@ -86,9 +122,9 @@ export interface GraphLooks {
|
|
|
86
122
|
export declare const looksOf: (options: {
|
|
87
123
|
colors: GraphColors;
|
|
88
124
|
font?: string | undefined;
|
|
89
|
-
groupOf: (id: string) => string | undefined;
|
|
90
125
|
groups: readonly string[];
|
|
91
126
|
near: ReadonlySet<string>;
|
|
127
|
+
noteOf: (id: string) => NoteFacts | undefined;
|
|
92
128
|
selected: string | undefined;
|
|
93
129
|
shown: ReadonlySet<string> | undefined;
|
|
94
130
|
}) => GraphLooks;
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { readColor } from '@fusion.dev/helix-core';
|
|
2
2
|
const SERIES = 24;
|
|
3
3
|
const TOKENS = {
|
|
4
|
+
accent: '--helix-color-accent',
|
|
5
|
+
danger: '--helix-color-danger',
|
|
4
6
|
ground: '--helix-color-surface-sunken',
|
|
7
|
+
info: '--helix-color-info',
|
|
5
8
|
ink: '--helix-color-foreground',
|
|
6
9
|
line: '--helix-color-border',
|
|
7
10
|
muted: '--helix-color-foreground-muted',
|
|
8
11
|
overlay: '--helix-color-surface-overlay',
|
|
9
12
|
primary: '--helix-color-primary',
|
|
13
|
+
success: '--helix-color-success',
|
|
14
|
+
warning: '--helix-color-warning',
|
|
10
15
|
};
|
|
11
16
|
export const graphColorsFrom = (element) => {
|
|
12
17
|
if (typeof document === 'undefined') {
|
|
@@ -49,12 +54,21 @@ export const mixRgb = (from, to, amount) => {
|
|
|
49
54
|
const channel = (at) => Math.round((one[at] ?? 0) + ((other[at] ?? 0) - (one[at] ?? 0)) * amount);
|
|
50
55
|
return `rgb(${channel(0)}, ${channel(1)}, ${channel(2)})`;
|
|
51
56
|
};
|
|
57
|
+
export const toneColor = (colors, tone) => tone === undefined || tone === 'primary'
|
|
58
|
+
? colors.primary
|
|
59
|
+
: tone === 'neutral'
|
|
60
|
+
? colors.muted
|
|
61
|
+
: colors[tone];
|
|
52
62
|
const FADED = 0.75;
|
|
53
63
|
export const looksOf = (options) => {
|
|
54
|
-
const { colors,
|
|
64
|
+
const { colors, near, noteOf, selected, shown } = options;
|
|
55
65
|
const order = new Map([...new Set(options.groups)].sort().map((group, at) => [group, at]));
|
|
56
66
|
const own = (id) => {
|
|
57
|
-
const
|
|
67
|
+
const note = noteOf(id);
|
|
68
|
+
if (note?.tone) {
|
|
69
|
+
return toneColor(colors, note.tone);
|
|
70
|
+
}
|
|
71
|
+
const group = note?.group;
|
|
58
72
|
const at = group === undefined ? undefined : order.get(group);
|
|
59
73
|
return at === undefined || colors.series.length === 0
|
|
60
74
|
? colors.muted
|
|
@@ -63,24 +77,30 @@ export const looksOf = (options) => {
|
|
|
63
77
|
const focused = selected !== undefined;
|
|
64
78
|
const hidden = (id) => shown !== undefined && !shown.has(id);
|
|
65
79
|
return {
|
|
80
|
+
colorOf: own,
|
|
66
81
|
font: options.font || 'sans-serif',
|
|
82
|
+
ground: colors.ground,
|
|
67
83
|
ink: colors.ink,
|
|
68
84
|
link: (source, target) => {
|
|
69
85
|
const touches = focused && (source === selected || target === selected);
|
|
70
86
|
return {
|
|
71
87
|
color: touches
|
|
72
|
-
?
|
|
88
|
+
? own(selected)
|
|
73
89
|
: focused
|
|
74
|
-
? mixRgb(colors.line, colors.ground, 0.
|
|
75
|
-
: colors.
|
|
90
|
+
? mixRgb(colors.line, colors.ground, 0.6)
|
|
91
|
+
: mixRgb(own(source), colors.ground, 0.72),
|
|
76
92
|
hidden: hidden(source) || hidden(target),
|
|
77
93
|
raised: touches,
|
|
78
94
|
};
|
|
79
95
|
},
|
|
80
96
|
node: (id) => {
|
|
97
|
+
const note = noteOf(id);
|
|
98
|
+
const facts = { emphasis: note?.emphasis, material: note?.material, shape: note?.shape };
|
|
81
99
|
if (id === selected) {
|
|
82
100
|
return {
|
|
83
|
-
|
|
101
|
+
...facts,
|
|
102
|
+
color: own(id),
|
|
103
|
+
glow: 1,
|
|
84
104
|
hidden: hidden(id),
|
|
85
105
|
label: 'always',
|
|
86
106
|
raised: true,
|
|
@@ -89,7 +109,9 @@ export const looksOf = (options) => {
|
|
|
89
109
|
}
|
|
90
110
|
const neighbor = focused && near.has(id);
|
|
91
111
|
return {
|
|
112
|
+
...facts,
|
|
92
113
|
color: focused && !neighbor ? mixRgb(own(id), colors.ground, FADED) : own(id),
|
|
114
|
+
glow: 0,
|
|
93
115
|
hidden: hidden(id),
|
|
94
116
|
label: focused && !neighbor ? 'never' : 'room',
|
|
95
117
|
raised: neighbor,
|
|
@@ -97,5 +119,6 @@ export const looksOf = (options) => {
|
|
|
97
119
|
};
|
|
98
120
|
},
|
|
99
121
|
overlay: colors.overlay,
|
|
122
|
+
quiet: colors.muted,
|
|
100
123
|
};
|
|
101
124
|
};
|
|
@@ -23,9 +23,18 @@ export const seedPositions = (nodes, links, known = new Map()) => {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
const neighbors = new Map();
|
|
26
|
+
const add = (from, to) => {
|
|
27
|
+
const list = neighbors.get(from);
|
|
28
|
+
if (list) {
|
|
29
|
+
list.push(to);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
neighbors.set(from, [to]);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
26
35
|
for (const { source, target } of links) {
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
add(source, target);
|
|
37
|
+
add(target, source);
|
|
29
38
|
}
|
|
30
39
|
let spiral = positions.size;
|
|
31
40
|
for (const node of nodes) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { GraphPoint } from './seed.js';
|
|
2
|
+
/**
|
|
3
|
+
* A shape the notes are fitted into once a layout settles, in a box `width`
|
|
4
|
+
* by `height`, as an SVG's `viewBox="0 0 width height"` gives one: an SVG
|
|
5
|
+
* path, whose holes are cut by the even-odd rule, or a test of whether a
|
|
6
|
+
* point in the box is inside.
|
|
7
|
+
*/
|
|
8
|
+
export type GraphStencil = {
|
|
9
|
+
height: number;
|
|
10
|
+
path: string;
|
|
11
|
+
width: number;
|
|
12
|
+
} | {
|
|
13
|
+
height: number;
|
|
14
|
+
inside: (x: number, y: number) => boolean;
|
|
15
|
+
width: number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* `count` places spread evenly over the inside of a shape: a grid sized to
|
|
19
|
+
* the shape's area, each place shaken within its cell so no rows show, and
|
|
20
|
+
* the grid made finer until it holds enough.
|
|
21
|
+
*/
|
|
22
|
+
export declare const placesIn: (inside: (x: number, y: number) => boolean, width: number, height: number, count: number) => GraphPoint[];
|
|
23
|
+
/**
|
|
24
|
+
* Places a layout put anywhere, moved into the stencil's shape and filling it
|
|
25
|
+
* evenly. **The notes keep their order**, left to right and, within each
|
|
26
|
+
* strip, top to bottom, so notes the layout put together land together, and
|
|
27
|
+
* each group takes a region of the shape.
|
|
28
|
+
*
|
|
29
|
+
* Centered where the layout was, and as large. The stencil's y points down,
|
|
30
|
+
* as an SVG's does, and the layout's up, as Sigma and cosmos.gl draw it.
|
|
31
|
+
* Places the shape cannot hold, as when it has no inside, come back as they
|
|
32
|
+
* were.
|
|
33
|
+
*/
|
|
34
|
+
export declare const fitToStencil: (positions: ReadonlyMap<string, GraphPoint>, stencil: GraphStencil) => Map<string, GraphPoint>;
|