@fusion.dev/helix-graph 0.120.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 +141 -8
- 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 +40 -5
- package/dist/connect-graph/connect-graph.js +81 -18
- package/dist/connect-graph/diff.d.ts +1 -1
- package/dist/connect-graph/looks.d.ts +29 -3
- package/dist/connect-graph/looks.js +24 -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 +2 -1
- package/dist/index.js +1 -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 +31 -3
- package/dist/sigma/sigma-driver.js +125 -140
- package/package.json +25 -6
|
@@ -54,12 +54,21 @@ export const mixRgb = (from, to, amount) => {
|
|
|
54
54
|
const channel = (at) => Math.round((one[at] ?? 0) + ((other[at] ?? 0) - (one[at] ?? 0)) * amount);
|
|
55
55
|
return `rgb(${channel(0)}, ${channel(1)}, ${channel(2)})`;
|
|
56
56
|
};
|
|
57
|
+
export const toneColor = (colors, tone) => tone === undefined || tone === 'primary'
|
|
58
|
+
? colors.primary
|
|
59
|
+
: tone === 'neutral'
|
|
60
|
+
? colors.muted
|
|
61
|
+
: colors[tone];
|
|
57
62
|
const FADED = 0.75;
|
|
58
63
|
export const looksOf = (options) => {
|
|
59
|
-
const { colors,
|
|
64
|
+
const { colors, near, noteOf, selected, shown } = options;
|
|
60
65
|
const order = new Map([...new Set(options.groups)].sort().map((group, at) => [group, at]));
|
|
61
66
|
const own = (id) => {
|
|
62
|
-
const
|
|
67
|
+
const note = noteOf(id);
|
|
68
|
+
if (note?.tone) {
|
|
69
|
+
return toneColor(colors, note.tone);
|
|
70
|
+
}
|
|
71
|
+
const group = note?.group;
|
|
63
72
|
const at = group === undefined ? undefined : order.get(group);
|
|
64
73
|
return at === undefined || colors.series.length === 0
|
|
65
74
|
? colors.muted
|
|
@@ -68,24 +77,30 @@ export const looksOf = (options) => {
|
|
|
68
77
|
const focused = selected !== undefined;
|
|
69
78
|
const hidden = (id) => shown !== undefined && !shown.has(id);
|
|
70
79
|
return {
|
|
80
|
+
colorOf: own,
|
|
71
81
|
font: options.font || 'sans-serif',
|
|
82
|
+
ground: colors.ground,
|
|
72
83
|
ink: colors.ink,
|
|
73
84
|
link: (source, target) => {
|
|
74
85
|
const touches = focused && (source === selected || target === selected);
|
|
75
86
|
return {
|
|
76
87
|
color: touches
|
|
77
|
-
?
|
|
88
|
+
? own(selected)
|
|
78
89
|
: focused
|
|
79
|
-
? mixRgb(colors.line, colors.ground, 0.
|
|
80
|
-
: colors.
|
|
90
|
+
? mixRgb(colors.line, colors.ground, 0.6)
|
|
91
|
+
: mixRgb(own(source), colors.ground, 0.72),
|
|
81
92
|
hidden: hidden(source) || hidden(target),
|
|
82
93
|
raised: touches,
|
|
83
94
|
};
|
|
84
95
|
},
|
|
85
96
|
node: (id) => {
|
|
97
|
+
const note = noteOf(id);
|
|
98
|
+
const facts = { emphasis: note?.emphasis, material: note?.material, shape: note?.shape };
|
|
86
99
|
if (id === selected) {
|
|
87
100
|
return {
|
|
88
|
-
|
|
101
|
+
...facts,
|
|
102
|
+
color: own(id),
|
|
103
|
+
glow: 1,
|
|
89
104
|
hidden: hidden(id),
|
|
90
105
|
label: 'always',
|
|
91
106
|
raised: true,
|
|
@@ -94,7 +109,9 @@ export const looksOf = (options) => {
|
|
|
94
109
|
}
|
|
95
110
|
const neighbor = focused && near.has(id);
|
|
96
111
|
return {
|
|
112
|
+
...facts,
|
|
97
113
|
color: focused && !neighbor ? mixRgb(own(id), colors.ground, FADED) : own(id),
|
|
114
|
+
glow: 0,
|
|
98
115
|
hidden: hidden(id),
|
|
99
116
|
label: focused && !neighbor ? 'never' : 'room',
|
|
100
117
|
raised: neighbor,
|
|
@@ -102,5 +119,6 @@ export const looksOf = (options) => {
|
|
|
102
119
|
};
|
|
103
120
|
},
|
|
104
121
|
overlay: colors.overlay,
|
|
122
|
+
quiet: colors.muted,
|
|
105
123
|
};
|
|
106
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>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const RASTER = 1024;
|
|
2
|
+
const PROBE = 64;
|
|
3
|
+
const WAVE = 0.07;
|
|
4
|
+
const seeded = (seed) => {
|
|
5
|
+
let state = seed >>> 0 || 1;
|
|
6
|
+
return () => {
|
|
7
|
+
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
|
|
8
|
+
return state / 4294967296;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
const insideOf = (stencil) => {
|
|
12
|
+
if ('inside' in stencil) {
|
|
13
|
+
return stencil.inside;
|
|
14
|
+
}
|
|
15
|
+
const scale = RASTER / Math.max(stencil.width, stencil.height, 1);
|
|
16
|
+
const width = Math.max(1, Math.ceil(stencil.width * scale));
|
|
17
|
+
const height = Math.max(1, Math.ceil(stencil.height * scale));
|
|
18
|
+
const canvas = document.createElement('canvas');
|
|
19
|
+
canvas.width = width;
|
|
20
|
+
canvas.height = height;
|
|
21
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
22
|
+
if (!context) {
|
|
23
|
+
return () => true;
|
|
24
|
+
}
|
|
25
|
+
context.scale(scale, scale);
|
|
26
|
+
context.fill(new Path2D(stencil.path), 'evenodd');
|
|
27
|
+
const alpha = context.getImageData(0, 0, width, height).data;
|
|
28
|
+
return (x, y) => {
|
|
29
|
+
const column = Math.floor(x * scale);
|
|
30
|
+
const row = Math.floor(y * scale);
|
|
31
|
+
return (column >= 0 &&
|
|
32
|
+
row >= 0 &&
|
|
33
|
+
column < width &&
|
|
34
|
+
row < height &&
|
|
35
|
+
(alpha[(row * width + column) * 4 + 3] ?? 0) > 127);
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export const placesIn = (inside, width, height, count) => {
|
|
39
|
+
let filled = 0;
|
|
40
|
+
for (let row = 0; row < PROBE; row++) {
|
|
41
|
+
for (let column = 0; column < PROBE; column++) {
|
|
42
|
+
if (inside(((column + 0.5) / PROBE) * width, ((row + 0.5) / PROBE) * height)) {
|
|
43
|
+
filled += 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (count === 0 || filled === 0) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
const random = seeded(count);
|
|
51
|
+
let step = Math.sqrt(((filled / PROBE ** 2) * width * height) / count);
|
|
52
|
+
for (let tries = 0; tries < 8; tries++) {
|
|
53
|
+
const places = [];
|
|
54
|
+
for (let y = step / 2; y < height; y += step) {
|
|
55
|
+
for (let x = step / 2; x < width; x += step) {
|
|
56
|
+
const place = {
|
|
57
|
+
x: x + (random() - 0.5) * step * 0.8,
|
|
58
|
+
y: y + (random() - 0.5) * step * 0.8,
|
|
59
|
+
};
|
|
60
|
+
if (inside(place.x, place.y)) {
|
|
61
|
+
places.push(place);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (places.length >= count) {
|
|
66
|
+
for (let at = places.length - 1; at > 0; at--) {
|
|
67
|
+
const other = Math.floor(random() * (at + 1));
|
|
68
|
+
[places[at], places[other]] = [places[other], places[at]];
|
|
69
|
+
}
|
|
70
|
+
return places.slice(0, count);
|
|
71
|
+
}
|
|
72
|
+
step *= Math.sqrt(places.length / count) * 0.97 || 0.5;
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
};
|
|
76
|
+
export const fitToStencil = (positions, stencil) => {
|
|
77
|
+
const notes = [...positions].map(([id, at]) => ({ at, id }));
|
|
78
|
+
if (notes.length === 0) {
|
|
79
|
+
return new Map();
|
|
80
|
+
}
|
|
81
|
+
const places = placesIn(insideOf(stencil), stencil.width, stencil.height, notes.length);
|
|
82
|
+
if (places.length < notes.length) {
|
|
83
|
+
return new Map(positions);
|
|
84
|
+
}
|
|
85
|
+
let left = Number.POSITIVE_INFINITY;
|
|
86
|
+
let right = Number.NEGATIVE_INFINITY;
|
|
87
|
+
let bottom = Number.POSITIVE_INFINITY;
|
|
88
|
+
let top = Number.NEGATIVE_INFINITY;
|
|
89
|
+
for (const { at } of notes) {
|
|
90
|
+
left = Math.min(left, at.x);
|
|
91
|
+
right = Math.max(right, at.x);
|
|
92
|
+
bottom = Math.min(bottom, at.y);
|
|
93
|
+
top = Math.max(top, at.y);
|
|
94
|
+
}
|
|
95
|
+
const scale = Math.max(right - left, top - bottom, 1) / Math.max(stencil.width, stencil.height);
|
|
96
|
+
const { height, width } = stencil;
|
|
97
|
+
const spots = places
|
|
98
|
+
.map((place) => ({
|
|
99
|
+
across: place.x + WAVE * width * Math.sin((place.y / height) * Math.PI * 3 + 1.3),
|
|
100
|
+
at: {
|
|
101
|
+
x: (left + right) / 2 + (place.x - width / 2) * scale,
|
|
102
|
+
y: (bottom + top) / 2 - (place.y - height / 2) * scale,
|
|
103
|
+
},
|
|
104
|
+
down: -place.y + WAVE * height * Math.sin((place.x / width) * Math.PI * 3 + 0.4),
|
|
105
|
+
}))
|
|
106
|
+
.sort((one, other) => one.across - other.across);
|
|
107
|
+
notes.sort((one, other) => one.at.x - other.at.x || one.at.y - other.at.y);
|
|
108
|
+
const strip = Math.ceil(Math.sqrt(notes.length));
|
|
109
|
+
const fitted = new Map();
|
|
110
|
+
for (let from = 0; from < notes.length; from += strip) {
|
|
111
|
+
const ours = notes.slice(from, from + strip).sort((one, other) => one.at.y - other.at.y);
|
|
112
|
+
const theirs = spots
|
|
113
|
+
.slice(from, from + strip)
|
|
114
|
+
.sort((one, other) => one.down - other.down)
|
|
115
|
+
.map((spot) => spot.at);
|
|
116
|
+
ours.forEach((note, at) => {
|
|
117
|
+
fitted.set(note.id, theirs[at]);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return fitted;
|
|
121
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { GraphDriver } from '../connect-graph/connect-graph.js';
|
|
2
|
+
import type { NodeLook } from '../connect-graph/looks.js';
|
|
3
|
+
/** A cosmos.gl graph, as far as the driver calls it. */
|
|
4
|
+
export interface CosmosGraph {
|
|
5
|
+
fitView(duration?: number, padding?: number): void;
|
|
6
|
+
fitViewByPointIndices(indices: number[], duration?: number, padding?: number): void;
|
|
7
|
+
getPointPositions(): number[];
|
|
8
|
+
getTrackedPointPositionsMap(): ReadonlyMap<number, [number, number]>;
|
|
9
|
+
getZoomLevel(): number;
|
|
10
|
+
readonly isReady: boolean;
|
|
11
|
+
readonly ready: Promise<void>;
|
|
12
|
+
pause(): void;
|
|
13
|
+
render(simulationAlpha?: number, transitionDuration?: number): void;
|
|
14
|
+
screenToSpacePosition(screenPosition: [number, number]): [number, number];
|
|
15
|
+
setConfigPartial(config: Record<string, unknown>): void;
|
|
16
|
+
setLinkColors(linkColors: Float32Array): void;
|
|
17
|
+
setLinks(links: Float32Array): void;
|
|
18
|
+
setLinkWidths(linkWidths: Float32Array): void;
|
|
19
|
+
setClusterPositions(clusterPositions: (number | undefined)[]): void;
|
|
20
|
+
setPinnedPoints(pinnedIndices: number[] | null): void;
|
|
21
|
+
setPointClusters(pointClusters: (number | undefined)[]): void;
|
|
22
|
+
setPointColors(pointColors: Float32Array): void;
|
|
23
|
+
setPointPositions(pointPositions: Float32Array, dontRescale?: boolean): void;
|
|
24
|
+
setPointShapes(pointShapes: Float32Array): void;
|
|
25
|
+
setPointSizes(pointSizes: Float32Array): void;
|
|
26
|
+
setZoomLevel(value: number, duration?: number): void;
|
|
27
|
+
setZoomTransformByPointPositions(positions: Float32Array, duration?: number, scale?: number): void;
|
|
28
|
+
spaceToScreenPosition(spacePosition: [number, number]): [number, number];
|
|
29
|
+
spaceToScreenRadius(spaceRadius: number): number;
|
|
30
|
+
start(alpha?: number): void;
|
|
31
|
+
trackPointPositionsByIndices(indices: number[]): void;
|
|
32
|
+
zoomToPointByIndex(index: number, duration?: number, scale?: number, canZoomOut?: boolean): void;
|
|
33
|
+
}
|
|
34
|
+
/** How the cosmos.gl driver behaves where the page does not say. */
|
|
35
|
+
export interface CosmosDriverOptions {
|
|
36
|
+
/** Whether each group's name is drawn over its notes. On by default. */
|
|
37
|
+
groupLabels?: boolean | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The longest a layout runs, in milliseconds, settled or not. 15 seconds by
|
|
40
|
+
* default, past where twenty thousand notes settle on a laptop's GPU.
|
|
41
|
+
*/
|
|
42
|
+
layoutLimit?: number | undefined;
|
|
43
|
+
/** A note's shape where it does not say: `circle` by default, `square` or `diamond`. */
|
|
44
|
+
shape?: NodeLook['shape'];
|
|
45
|
+
/** The size of cosmos.gl's space, as its own `spaceSize` says. 4096 by default. */
|
|
46
|
+
spaceSize?: number | undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Drives cosmos.gl for a Graph frame: the layout and the drawing both run on
|
|
50
|
+
* the GPU, which is the renderer to reach for past fifty thousand notes. Make
|
|
51
|
+
* the graph in a box inside the frame and pass both here, and
|
|
52
|
+
* `connectGraph` fills it:
|
|
53
|
+
*
|
|
54
|
+
* ```js
|
|
55
|
+
* const graph = new Graph(box, { enableSimulation: true })
|
|
56
|
+
* connectGraph(frame, cosmosDriver(graph, box))
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* **cosmos.gl draws notes with its own shaders**, so a note's shape is
|
|
60
|
+
* honored and its `material` and `emphasis` are not, and it names nothing:
|
|
61
|
+
* the driver names the hovered note, the selected one and its neighbors, and
|
|
62
|
+
* each group, over the drawing, as the Sigma driver does.
|
|
63
|
+
*
|
|
64
|
+
* **It is `@cosmos.gl/graph`, which is MIT**, and not Cosmograph, which is
|
|
65
|
+
* licensed for noncommercial use only.
|
|
66
|
+
*/
|
|
67
|
+
export declare const cosmosDriver: (graph: CosmosGraph, container: HTMLElement, options?: CosmosDriverOptions) => GraphDriver;
|