@fusion.dev/helix-graph 0.118.0 → 0.119.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 CHANGED
@@ -33,6 +33,9 @@ const connection = connectGraph(
33
33
  // Keep where the layout put each note, and pass it back as `x` and `y` next time.
34
34
  frame.addEventListener('helix-graph-layout', (event) => save(event.detail.positions))
35
35
 
36
+ // After replacing a stylesheet, which no attribute shows:
37
+ connection.restyle()
38
+
36
39
  // Later, when the graph goes away:
37
40
  connection.disconnect()
38
41
  ```
@@ -63,6 +63,8 @@ export interface GraphDriver {
63
63
  gestures?(mode: ViewportGestureMode): void;
64
64
  /** The element the renderer made focusable, which the connector takes out of the tab order. */
65
65
  focusTarget?(): HTMLElement | null;
66
+ /** Removes whatever the driver added to the renderer or the page, when the connection ends. */
67
+ stop?(): void;
66
68
  }
67
69
  /** How a connection behaves where the page does not say. */
68
70
  export interface ConnectGraphOptions {
@@ -79,6 +81,12 @@ export interface GraphConnection {
79
81
  disconnect(): void;
80
82
  /** Lays the graph out again from where it starts, ignoring where the last layout put each note. */
81
83
  relayout(): void;
84
+ /**
85
+ * Reads the page's colors again. The connection hears a theme that changes
86
+ * by a class, a mode or the reader's preference on its own; call this after
87
+ * replacing a stylesheet, which no attribute shows.
88
+ */
89
+ restyle(): void;
82
90
  }
83
91
  /**
84
92
  * Connects a Graph frame to whatever draws the graph.
@@ -158,11 +158,16 @@ export const connectGraph = (frame, driver, options = {}) => {
158
158
  }
159
159
  }
160
160
  driver.gestures?.('free');
161
+ driver.stop?.();
161
162
  graph.drawn?.(false);
162
163
  },
163
164
  relayout: () => {
164
165
  known = new Map();
165
166
  draw();
166
167
  },
168
+ restyle: () => {
169
+ colors = null;
170
+ restyle();
171
+ },
167
172
  };
168
173
  };
@@ -11,6 +11,8 @@ export interface GraphColors {
11
11
  ink: string;
12
12
  /** A note in no group, and the links that touch the selected note: `foreground-muted`. */
13
13
  muted: string;
14
+ /** What a note's name is drawn on while the pointer is over it: `surface-overlay`. */
15
+ overlay: string;
14
16
  /** The selected note: `primary`. */
15
17
  primary: string;
16
18
  /** One color for each group, in the order of the groups' names: `series-1` and on. */
@@ -37,8 +39,15 @@ export interface NodeLook {
37
39
  color: string;
38
40
  /** Outside the local graph that `depth` keeps. */
39
41
  hidden: boolean;
40
- /** Its name is drawn whatever the room, as the selected note's and its neighbors' are. */
41
- label: boolean;
42
+ /**
43
+ * When its name is drawn: `always`, whatever the room, as the selected
44
+ * note's is; `room`, where it does not cover another name, as the
45
+ * renderer decides for every note at rest; `never`, as a faded note's is.
46
+ *
47
+ * **The neighbors get room and not always.** Forced, a hub's twenty
48
+ * neighbors drew their names over each other in a knot nobody could read.
49
+ */
50
+ label: 'always' | 'never' | 'room';
42
51
  /** Drawn over the rest, as the selected note and its neighbors are. */
43
52
  raised: boolean;
44
53
  /** The selected note. */
@@ -56,6 +65,8 @@ export interface GraphLooks {
56
65
  font: string;
57
66
  /** The color the labels are drawn in. */
58
67
  ink: string;
68
+ /** What a name is drawn on while the pointer is over its note. */
69
+ overlay: string;
59
70
  link(source: string, target: string): LinkLook;
60
71
  node(id: string): NodeLook;
61
72
  }
@@ -65,7 +76,8 @@ export interface GraphLooks {
65
76
  * **The neighborhood stands out and the rest steps back**, which is what
66
77
  * selecting a note in a graph of thousands is for: the selected note in the
67
78
  * brand color, the notes it links to and from in their groups' colors with
68
- * their names drawn, and every other note faded toward the ground. With
79
+ * their names drawn where there is room, and every other note faded toward
80
+ * the ground, unnamed. With
69
81
  * nothing selected, every note is in its group's color.
70
82
  *
71
83
  * Groups take the series in the order of their names, so a group keeps its
@@ -5,6 +5,7 @@ const TOKENS = {
5
5
  ink: '--helix-color-foreground',
6
6
  line: '--helix-color-border',
7
7
  muted: '--helix-color-foreground-muted',
8
+ overlay: '--helix-color-surface-overlay',
8
9
  primary: '--helix-color-primary',
9
10
  };
10
11
  export const graphColorsFrom = (element) => {
@@ -81,7 +82,7 @@ export const looksOf = (options) => {
81
82
  return {
82
83
  color: colors.primary,
83
84
  hidden: hidden(id),
84
- label: true,
85
+ label: 'always',
85
86
  raised: true,
86
87
  selected: true,
87
88
  };
@@ -90,10 +91,11 @@ export const looksOf = (options) => {
90
91
  return {
91
92
  color: focused && !neighbor ? mixRgb(own(id), colors.ground, FADED) : own(id),
92
93
  hidden: hidden(id),
93
- label: neighbor,
94
+ label: focused && !neighbor ? 'never' : 'room',
94
95
  raised: neighbor,
95
96
  selected: false,
96
97
  };
97
98
  },
99
+ overlay: colors.overlay,
98
100
  };
99
101
  };
@@ -52,7 +52,8 @@ export interface SigmaRenderer {
52
52
  node: string;
53
53
  }) => void): unknown;
54
54
  refresh(): unknown;
55
- setSetting(name: 'edgeReducer' | 'labelColor' | 'labelFont' | 'nodeReducer', value: unknown): unknown;
55
+ resize(force?: boolean): unknown;
56
+ setSetting(name: 'defaultDrawNodeHover' | 'edgeReducer' | 'labelColor' | 'labelFont' | 'labelRenderedSizeThreshold' | 'nodeReducer', value: unknown): unknown;
56
57
  viewportToFramedGraph(point: {
57
58
  x: number;
58
59
  y: number;
@@ -5,6 +5,26 @@ const STILL = 0.004;
5
5
  const HELD = 2;
6
6
  const IDLE = 2000;
7
7
  const pixels = (size) => 2 + 2 * Math.sqrt(Math.max(size, 0));
8
+ const hoverOf = (looks) => (context, data, settings) => {
9
+ if (!data.label) {
10
+ return;
11
+ }
12
+ const size = settings.labelSize;
13
+ context.font = `${settings.labelWeight} ${size}px ${settings.labelFont}`;
14
+ const width = context.measureText(data.label).width;
15
+ const x = data.x + data.size + 2;
16
+ const height = size + 8;
17
+ context.save();
18
+ context.shadowBlur = 8;
19
+ context.shadowColor = 'rgba(0, 0, 0, 0.25)';
20
+ context.fillStyle = looks.overlay;
21
+ context.beginPath();
22
+ context.roundRect(x - 4, data.y - height / 2, width + 8, height, 4);
23
+ context.fill();
24
+ context.restore();
25
+ context.fillStyle = looks.ink;
26
+ context.fillText(data.label, x, data.y + size / 3);
27
+ };
8
28
  const positionsOf = (graph) => {
9
29
  const positions = new Map();
10
30
  graph.forEachNode((node, attributes) => {
@@ -45,6 +65,13 @@ export const sigmaDriver = (sigma, options = {}) => {
45
65
  for (const type of held) {
46
66
  container.addEventListener(type, hold, { capture: true, passive: false });
47
67
  }
68
+ const sized = typeof ResizeObserver === 'function'
69
+ ? new ResizeObserver(() => {
70
+ sigma.resize(true);
71
+ sigma.refresh();
72
+ })
73
+ : undefined;
74
+ sized?.observe(container);
48
75
  return {
49
76
  center: (id, animate) => {
50
77
  const at = sigma.getNodeDisplayData(id);
@@ -87,14 +114,18 @@ export const sigmaDriver = (sigma, options = {}) => {
87
114
  look: (looks) => {
88
115
  sigma.setSetting('labelColor', { color: looks.ink });
89
116
  sigma.setSetting('labelFont', looks.font);
117
+ sigma.setSetting('labelRenderedSizeThreshold', 0);
118
+ sigma.setSetting('defaultDrawNodeHover', hoverOf(looks));
90
119
  sigma.setSetting('nodeReducer', (node, data) => {
91
120
  const look = looks.node(node);
121
+ const grown = look.selected ? 1.5 : look.raised ? 1.25 : 1;
92
122
  return {
93
123
  ...data,
94
124
  color: look.color,
95
- forceLabel: look.label,
125
+ forceLabel: look.label === 'always',
96
126
  hidden: look.hidden,
97
- size: look.selected && typeof data.size === 'number' ? data.size * 1.5 : data.size,
127
+ label: look.label === 'never' ? null : data.label,
128
+ size: typeof data.size === 'number' ? data.size * grown : data.size,
98
129
  zIndex: look.selected ? 2 : look.raised ? 1 : 0,
99
130
  };
100
131
  });
@@ -127,6 +158,13 @@ export const sigmaDriver = (sigma, options = {}) => {
127
158
  camera.setState(to);
128
159
  }
129
160
  },
161
+ stop: () => {
162
+ sized?.disconnect();
163
+ for (const type of held) {
164
+ container.removeEventListener(type, hold, { capture: true });
165
+ }
166
+ container.style.touchAction = touchAction;
167
+ },
130
168
  zoomBy: (direction, animate) => {
131
169
  const ratio = camera.getState().ratio * (direction > 0 ? 1 / ZOOM : ZOOM);
132
170
  if (animate) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-graph",
3
3
  "description": "Connects the Helix Graph frame to a graph renderer: Sigma.js, or a driver of your own.",
4
- "version": "0.118.0",
4
+ "version": "0.119.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -35,11 +35,11 @@
35
35
  },
36
36
  "bugs": "https://github.com/project-fusion/fusion/issues",
37
37
  "dependencies": {
38
- "@fusion.dev/helix-core": "0.118.0"
38
+ "@fusion.dev/helix-core": "0.119.0"
39
39
  },
40
40
  "devDependencies": {
41
- "@fusion.dev/helix-headless": "0.118.0",
42
- "@fusion.dev/helix-testing": "0.118.0",
41
+ "@fusion.dev/helix-headless": "0.119.0",
42
+ "@fusion.dev/helix-testing": "0.119.0",
43
43
  "@types/bun": "^1.3.0",
44
44
  "graphology": "^0.26.0",
45
45
  "graphology-layout-forceatlas2": "^0.10.1",