vizcraft 0.2.1 → 0.3.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/CHANGELOG.md +12 -0
- package/README.md +21 -20
- package/dist/anim/animationBuilder.d.ts +2 -0
- package/dist/anim/animationBuilder.js +6 -1
- package/dist/anim/spec.d.ts +1 -1
- package/dist/anim/vizcraftAdapter.js +68 -1
- package/dist/builder.d.ts +9 -1
- package/dist/builder.js +116 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/overlayBuilder.d.ts +50 -0
- package/dist/overlayBuilder.js +80 -0
- package/dist/overlays.d.ts +113 -21
- package/dist/overlays.js +319 -1
- package/dist/styles.d.ts +1 -1
- package/dist/styles.js +2 -1
- package/dist/types.d.ts +28 -0
- package/dist/types.js +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# vizcraft
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`c5ffe75`](https://github.com/ChipiKaf/vizcraft/commit/c5ffe7546a2e2148618db057c24aea01ecf097e0) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - Add improved overlay api
|
|
8
|
+
|
|
9
|
+
## 0.2.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [`7c9eb18`](https://github.com/ChipiKaf/vizcraft/commit/7c9eb185e727bde899b4779c4661d8b176db8549) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - update documentation
|
|
14
|
+
|
|
3
15
|
## 0.2.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/ChipiKaf/vizcraft/actions/workflows/snapshot.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
-
📖 Full documentation: https://vizcraft-docs.vercel.app/
|
|
10
|
+
📖 Full documentation: [docs here](https://vizcraft-docs.vercel.app/docs/intro)
|
|
11
11
|
|
|
12
12
|
**A declarative, builder-based library for creating animated SVG network visualizations and algorithm demos.**
|
|
13
13
|
|
|
@@ -41,7 +41,6 @@ import { viz } from 'vizcraft';
|
|
|
41
41
|
const builder = viz().view(800, 600);
|
|
42
42
|
|
|
43
43
|
builder
|
|
44
|
-
.view(500, 500)
|
|
45
44
|
.node('a')
|
|
46
45
|
.at(100, 100)
|
|
47
46
|
.circle(15)
|
|
@@ -57,21 +56,21 @@ const container = document.getElementById('viz-basic');
|
|
|
57
56
|
if (container) builder.mount(container);
|
|
58
57
|
```
|
|
59
58
|
|
|
60
|
-
More walkthroughs and examples: https://vizcraft-docs.vercel.app/
|
|
59
|
+
More walkthroughs and examples: [docs here](https://vizcraft-docs.vercel.app/docs/examples).
|
|
61
60
|
|
|
62
61
|
## 📚 Documentation (Topics)
|
|
63
62
|
|
|
64
|
-
Full documentation site: https://vizcraft-docs.vercel.app/
|
|
63
|
+
Full documentation site: [docs here](https://vizcraft-docs.vercel.app/docs/intro)
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
Docs topics (same as the sidebar):
|
|
67
66
|
|
|
68
|
-
- [Introduction](
|
|
69
|
-
- [Examples](
|
|
70
|
-
- [Essentials](
|
|
71
|
-
- [Animations](
|
|
72
|
-
- [Animation Builder API](
|
|
73
|
-
- [Advanced](
|
|
74
|
-
- [Types](
|
|
67
|
+
- [Introduction](https://vizcraft-docs.vercel.app/docs/intro)
|
|
68
|
+
- [Examples](https://vizcraft-docs.vercel.app/docs/examples)
|
|
69
|
+
- [Essentials](https://vizcraft-docs.vercel.app/docs/essentials)
|
|
70
|
+
- [Animations](https://vizcraft-docs.vercel.app/docs/animations)
|
|
71
|
+
- [Animation Builder API](https://vizcraft-docs.vercel.app/docs/animations/animation-builder-api)
|
|
72
|
+
- [Advanced](https://vizcraft-docs.vercel.app/docs/advanced)
|
|
73
|
+
- [Types](https://vizcraft-docs.vercel.app/docs/types)
|
|
75
74
|
|
|
76
75
|
Run the docs locally (monorepo):
|
|
77
76
|
|
|
@@ -122,7 +121,7 @@ b.edge('n1', 'n2')
|
|
|
122
121
|
|
|
123
122
|
### Animations
|
|
124
123
|
|
|
125
|
-
See the full Animations guide
|
|
124
|
+
See the full Animations guide [docs here](https://vizcraft-docs.vercel.app/docs/animations).
|
|
126
125
|
|
|
127
126
|
VizCraft supports **two complementary animation approaches**:
|
|
128
127
|
|
|
@@ -153,7 +152,7 @@ b.node('a')
|
|
|
153
152
|
|
|
154
153
|
2. **Data-only timeline animations (`AnimationSpec`)** (sequenced tweens)
|
|
155
154
|
|
|
156
|
-
- Author with `builder.animate((
|
|
155
|
+
- Author with `builder.animate((aBuilder) => ...)`.
|
|
157
156
|
- VizCraft stores compiled specs on the scene as `scene.animationSpecs`.
|
|
158
157
|
- Play them with `builder.play()`.
|
|
159
158
|
|
|
@@ -174,8 +173,8 @@ b.node('a')
|
|
|
174
173
|
.arrow()
|
|
175
174
|
.done();
|
|
176
175
|
|
|
177
|
-
b.animate((
|
|
178
|
-
|
|
176
|
+
b.animate((aBuilder) =>
|
|
177
|
+
aBuilder
|
|
179
178
|
.node('a')
|
|
180
179
|
.to({ x: 200, opacity: 0.35 }, { duration: 600 })
|
|
181
180
|
.node('b')
|
|
@@ -206,8 +205,10 @@ b.node('a')
|
|
|
206
205
|
.edge('a', 'b', 'e1')
|
|
207
206
|
.done();
|
|
208
207
|
|
|
209
|
-
b.animate((
|
|
210
|
-
|
|
208
|
+
b.animate((aBuilder) =>
|
|
209
|
+
aBuilder
|
|
210
|
+
.edge('a', 'b', 'e1')
|
|
211
|
+
.to({ strokeDashoffset: -120 }, { duration: 900 })
|
|
211
212
|
);
|
|
212
213
|
```
|
|
213
214
|
|
|
@@ -216,8 +217,8 @@ b.animate((anim) =>
|
|
|
216
217
|
Specs can carry adapter extensions so you can animate your own numeric properties:
|
|
217
218
|
|
|
218
219
|
```ts
|
|
219
|
-
b.animate((
|
|
220
|
-
|
|
220
|
+
b.animate((aBuilder) =>
|
|
221
|
+
aBuilder
|
|
221
222
|
.extendAdapter((adapter) => {
|
|
222
223
|
adapter.register?.('node', 'r', {
|
|
223
224
|
get: (target) => adapter.get(target, 'r'),
|
|
@@ -38,6 +38,8 @@ export declare class AnimationBuilder {
|
|
|
38
38
|
extendAdapter(cb: ExtendAdapter): this;
|
|
39
39
|
/** Select a node by id (compiles to target `node:<id>`). */
|
|
40
40
|
node(id: string): this;
|
|
41
|
+
/** Select an overlay by key (compiles to target `overlay:<key>`). */
|
|
42
|
+
overlay(key: string): this;
|
|
41
43
|
/**
|
|
42
44
|
* Select an edge.
|
|
43
45
|
*
|
|
@@ -30,6 +30,11 @@ export class AnimationBuilder {
|
|
|
30
30
|
this.currentTarget = toTarget('node', id);
|
|
31
31
|
return this;
|
|
32
32
|
}
|
|
33
|
+
/** Select an overlay by key (compiles to target `overlay:<key>`). */
|
|
34
|
+
overlay(key) {
|
|
35
|
+
this.currentTarget = toTarget('overlay', key);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
33
38
|
edge(a, b, c) {
|
|
34
39
|
const id = b === undefined ? a : (c ?? `${a}->${b}`);
|
|
35
40
|
this.currentTarget = toTarget('edge', id);
|
|
@@ -56,7 +61,7 @@ export class AnimationBuilder {
|
|
|
56
61
|
*/
|
|
57
62
|
to(props, opts) {
|
|
58
63
|
if (!this.currentTarget) {
|
|
59
|
-
throw new Error('AnimationBuilder.to(): no target selected (call node(...) or
|
|
64
|
+
throw new Error('AnimationBuilder.to(): no target selected (call node(...), edge(...), or overlay(...))');
|
|
60
65
|
}
|
|
61
66
|
const duration = Math.max(0, opts.duration);
|
|
62
67
|
const easing = opts.easing;
|
package/dist/anim/spec.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AnimationTarget = (`node:${string}` | `edge:${string}`) | (string & {});
|
|
1
|
+
export type AnimationTarget = (`node:${string}` | `edge:${string}` | `overlay:${string}`) | (string & {});
|
|
2
2
|
export type CoreAnimProperty = 'x' | 'y' | 'opacity' | 'scale' | 'rotation' | 'strokeDashoffset';
|
|
3
3
|
export type AnimProperty = CoreAnimProperty | (string & {});
|
|
4
4
|
export type Ease = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
+
import { OVERLAY_RUNTIME_DIRTY } from '../types';
|
|
1
2
|
import { createRegistryAdapter } from './registryAdapter';
|
|
2
3
|
export function createVizCraftAdapter(scene, requestRender) {
|
|
3
4
|
const nodesById = new Map(scene.nodes.map((n) => [n.id, n]));
|
|
4
5
|
const edgesById = new Map(scene.edges.map((e) => [e.id, e]));
|
|
6
|
+
const overlays = scene.overlays ?? [];
|
|
7
|
+
const overlaysByKey = new Map();
|
|
8
|
+
for (const spec of overlays) {
|
|
9
|
+
const key = spec.key ?? spec.id;
|
|
10
|
+
overlaysByKey.set(key, spec);
|
|
11
|
+
}
|
|
5
12
|
const adapter = createRegistryAdapter({
|
|
6
13
|
flush: requestRender,
|
|
7
14
|
});
|
|
8
15
|
// register node/edge target resolvers and props using ergonomic handles
|
|
9
16
|
const node = adapter.kind('node', (id) => nodesById.get(id));
|
|
10
17
|
const edge = adapter.kind('edge', (id) => edgesById.get(id));
|
|
18
|
+
const overlay = adapter.kind('overlay', (key) => overlaysByKey.get(key));
|
|
11
19
|
node
|
|
12
20
|
.prop('x', {
|
|
13
21
|
get: (el) => {
|
|
@@ -75,5 +83,64 @@ export function createVizCraftAdapter(scene, requestRender) {
|
|
|
75
83
|
e.runtime.strokeDashoffset = v;
|
|
76
84
|
},
|
|
77
85
|
});
|
|
78
|
-
|
|
86
|
+
// Overlay params: allow animating arbitrary numeric fields on `spec.params`.
|
|
87
|
+
//
|
|
88
|
+
// This intentionally uses a generic reader/writer so users can animate
|
|
89
|
+
// custom overlays without needing adapter extensions.
|
|
90
|
+
const overlayParamReader = (el, prop) => {
|
|
91
|
+
const spec = el;
|
|
92
|
+
const params = spec.params;
|
|
93
|
+
const v = params?.[prop];
|
|
94
|
+
return typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
95
|
+
};
|
|
96
|
+
const overlayParamWriter = (el, prop, value) => {
|
|
97
|
+
const spec = el;
|
|
98
|
+
const existing = spec.params;
|
|
99
|
+
const params = existing && typeof existing === 'object' && !Array.isArray(existing)
|
|
100
|
+
? existing
|
|
101
|
+
: {};
|
|
102
|
+
params[prop] = value;
|
|
103
|
+
// Ensure we keep reference stable in case params was undefined or non-object.
|
|
104
|
+
spec.params = params;
|
|
105
|
+
// Mark dirty so patchRuntime can avoid re-rendering unaffected overlays.
|
|
106
|
+
spec[OVERLAY_RUNTIME_DIRTY] = true;
|
|
107
|
+
};
|
|
108
|
+
const resolveOverlayFromTarget = (target) => {
|
|
109
|
+
const t = String(target);
|
|
110
|
+
if (!t.startsWith('overlay:'))
|
|
111
|
+
return undefined;
|
|
112
|
+
const key = t.slice('overlay:'.length);
|
|
113
|
+
return overlaysByKey.get(key);
|
|
114
|
+
};
|
|
115
|
+
// Register core overlay props we know are numeric today.
|
|
116
|
+
// Users can still register more via adapter extensions if they prefer explicitness.
|
|
117
|
+
overlay.prop('progress', {
|
|
118
|
+
get: (el) => overlayParamReader(el, 'progress'),
|
|
119
|
+
set: (el, v) => overlayParamWriter(el, 'progress', v),
|
|
120
|
+
});
|
|
121
|
+
// Make overlays fully generic: any numeric `spec.params[prop]` is animatable.
|
|
122
|
+
//
|
|
123
|
+
// `createRegistryAdapter` requires per-prop registration; for overlays we provide
|
|
124
|
+
// a fallback so custom overlays don't need adapter extensions.
|
|
125
|
+
const baseGet = adapter.get;
|
|
126
|
+
const baseSet = adapter.set;
|
|
127
|
+
return {
|
|
128
|
+
...adapter,
|
|
129
|
+
get(target, prop) {
|
|
130
|
+
const v = baseGet(target, prop);
|
|
131
|
+
if (v !== undefined)
|
|
132
|
+
return v;
|
|
133
|
+
const spec = resolveOverlayFromTarget(target);
|
|
134
|
+
if (!spec)
|
|
135
|
+
return undefined;
|
|
136
|
+
return overlayParamReader(spec, String(prop));
|
|
137
|
+
},
|
|
138
|
+
set(target, prop, value) {
|
|
139
|
+
baseSet(target, prop, value);
|
|
140
|
+
const spec = resolveOverlayFromTarget(target);
|
|
141
|
+
if (!spec)
|
|
142
|
+
return;
|
|
143
|
+
overlayParamWriter(spec, String(prop), value);
|
|
144
|
+
},
|
|
145
|
+
};
|
|
79
146
|
}
|
package/dist/builder.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { VizScene, VizNode, VizEdge, NodeLabel, EdgeLabel, AnimationConfig, VizGridConfig } from './types';
|
|
1
|
+
import type { VizScene, VizNode, VizEdge, NodeLabel, EdgeLabel, AnimationConfig, OverlayId, OverlayParams, VizGridConfig } from './types';
|
|
2
|
+
import { OverlayBuilder } from './overlayBuilder';
|
|
2
3
|
import type { AnimationSpec } from './anim/spec';
|
|
3
4
|
import { type AnimationBuilder, type AnimatableProps, type TweenOptions } from './anim/animationBuilder';
|
|
4
5
|
import { type PlaybackController } from './anim/playback';
|
|
@@ -13,6 +14,9 @@ interface VizBuilder {
|
|
|
13
14
|
* The compiled spec is also stored on the built scene as `scene.animationSpecs`.
|
|
14
15
|
*/
|
|
15
16
|
animate(cb: (anim: AnimationBuilder) => unknown): AnimationSpec;
|
|
17
|
+
/** Fluent overlay authoring (compiles to overlay specs and stores on the built scene). */
|
|
18
|
+
overlay(cb: (overlay: OverlayBuilder) => unknown): VizBuilder;
|
|
19
|
+
overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
|
|
16
20
|
overlay<T>(id: string, params: T, key?: string): VizBuilder;
|
|
17
21
|
node(id: string): NodeBuilder;
|
|
18
22
|
edge(from: string, to: string, id?: string): EdgeBuilder;
|
|
@@ -67,6 +71,8 @@ interface NodeBuilder {
|
|
|
67
71
|
done(): VizBuilder;
|
|
68
72
|
node(id: string): NodeBuilder;
|
|
69
73
|
edge(from: string, to: string, id?: string): EdgeBuilder;
|
|
74
|
+
overlay(cb: (overlay: OverlayBuilder) => unknown): VizBuilder;
|
|
75
|
+
overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
|
|
70
76
|
overlay<T>(id: string, params: T, key?: string): VizBuilder;
|
|
71
77
|
build(): VizScene;
|
|
72
78
|
svg(): string;
|
|
@@ -87,6 +93,8 @@ interface EdgeBuilder {
|
|
|
87
93
|
done(): VizBuilder;
|
|
88
94
|
node(id: string): NodeBuilder;
|
|
89
95
|
edge(from: string, to: string, id?: string): EdgeBuilder;
|
|
96
|
+
overlay(cb: (overlay: OverlayBuilder) => unknown): VizBuilder;
|
|
97
|
+
overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
|
|
90
98
|
overlay<T>(id: string, params: T, key?: string): VizBuilder;
|
|
91
99
|
build(): VizScene;
|
|
92
100
|
svg(): string;
|
package/dist/builder.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { OVERLAY_RUNTIME_DIRTY } from './types';
|
|
1
2
|
import { DEFAULT_VIZ_CSS } from './styles';
|
|
2
3
|
import { defaultCoreAnimationRegistry } from './animations';
|
|
3
4
|
import { defaultCoreOverlayRegistry } from './overlays';
|
|
5
|
+
import { OverlayBuilder } from './overlayBuilder';
|
|
4
6
|
import { createRuntimePatchCtx, patchRuntime, } from './runtimePatcher';
|
|
5
7
|
import { buildAnimationSpec, } from './anim/animationBuilder';
|
|
6
8
|
import { createBuilderPlayback, } from './anim/playback';
|
|
@@ -75,14 +77,16 @@ class VizBuilderImpl {
|
|
|
75
77
|
this._gridConfig = { cols, rows, padding };
|
|
76
78
|
return this;
|
|
77
79
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
overlay(arg1, arg2, arg3) {
|
|
81
|
+
if (typeof arg1 === 'function') {
|
|
82
|
+
const overlay = new OverlayBuilder();
|
|
83
|
+
arg1(overlay);
|
|
84
|
+
this._overlays.push(...overlay.build());
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
const id = arg1;
|
|
88
|
+
const params = arg2;
|
|
89
|
+
const key = arg3;
|
|
86
90
|
this._overlays.push({ id, params, key });
|
|
87
91
|
return this;
|
|
88
92
|
}
|
|
@@ -274,6 +278,92 @@ class VizBuilderImpl {
|
|
|
274
278
|
runtimePatchCtxBySvg.set(svg, ctx);
|
|
275
279
|
}
|
|
276
280
|
patchRuntime(scene, ctx);
|
|
281
|
+
// Keep overlays in sync during animation playback.
|
|
282
|
+
//
|
|
283
|
+
// Animations flush via `patchRuntime()` (to avoid full re-mounts). Nodes/edges
|
|
284
|
+
// are patched via `runtimePatcher`, but overlays are registry-rendered and
|
|
285
|
+
// need an explicit reconcile pass to reflect animated `spec.params` changes.
|
|
286
|
+
const overlayLayer = svg.querySelector('[data-viz-layer="overlays"]') ||
|
|
287
|
+
svg.querySelector('.viz-layer-overlays');
|
|
288
|
+
if (overlayLayer) {
|
|
289
|
+
const overlays = scene.overlays ?? [];
|
|
290
|
+
const nodesById = new Map(scene.nodes.map((n) => [n.id, n]));
|
|
291
|
+
const edgesById = new Map(scene.edges.map((e) => [e.id, e]));
|
|
292
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
293
|
+
// 1) Map existing overlay groups
|
|
294
|
+
const existingOverlayGroups = Array.from(overlayLayer.children).filter((el) => el.tagName === 'g');
|
|
295
|
+
const existingOverlaysMap = new Map();
|
|
296
|
+
existingOverlayGroups.forEach((el) => {
|
|
297
|
+
const id = el.getAttribute('data-overlay-id');
|
|
298
|
+
if (id)
|
|
299
|
+
existingOverlaysMap.set(id, el);
|
|
300
|
+
});
|
|
301
|
+
// Fast decision: if nothing is dirty and keys match, skip overlay work.
|
|
302
|
+
const overlayKeyCountMatches = overlays.length === existingOverlaysMap.size;
|
|
303
|
+
let needsOverlayPass = !overlayKeyCountMatches;
|
|
304
|
+
const dirtyOverlays = [];
|
|
305
|
+
if (!needsOverlayPass) {
|
|
306
|
+
for (const spec of overlays) {
|
|
307
|
+
const uniqueKey = spec.key || spec.id;
|
|
308
|
+
if (!existingOverlaysMap.has(uniqueKey)) {
|
|
309
|
+
needsOverlayPass = true;
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
if (spec[OVERLAY_RUNTIME_DIRTY]) {
|
|
313
|
+
dirtyOverlays.push(spec);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (!needsOverlayPass && dirtyOverlays.length === 0)
|
|
318
|
+
return;
|
|
319
|
+
const processedOverlayIds = new Set();
|
|
320
|
+
// 2) Render/update overlays
|
|
321
|
+
const toUpdate = needsOverlayPass ? overlays : dirtyOverlays;
|
|
322
|
+
toUpdate.forEach((spec) => {
|
|
323
|
+
const renderer = defaultCoreOverlayRegistry.get(spec.id);
|
|
324
|
+
if (!renderer)
|
|
325
|
+
return;
|
|
326
|
+
const uniqueKey = spec.key || spec.id;
|
|
327
|
+
processedOverlayIds.add(uniqueKey);
|
|
328
|
+
let group = existingOverlaysMap.get(uniqueKey);
|
|
329
|
+
if (!group) {
|
|
330
|
+
group = document.createElementNS(svgNS, 'g');
|
|
331
|
+
group.setAttribute('data-overlay-id', uniqueKey);
|
|
332
|
+
group.setAttribute('data-viz-role', 'overlay-group');
|
|
333
|
+
overlayLayer.appendChild(group);
|
|
334
|
+
}
|
|
335
|
+
// Keep wrapper class in sync even when reusing an existing group.
|
|
336
|
+
const expectedClass = `viz-overlay-${spec.id}${spec.className ? ` ${spec.className}` : ''}`;
|
|
337
|
+
const currentClass = group.getAttribute('class');
|
|
338
|
+
if (currentClass !== expectedClass) {
|
|
339
|
+
group.setAttribute('class', expectedClass);
|
|
340
|
+
}
|
|
341
|
+
const overlayCtx = {
|
|
342
|
+
spec,
|
|
343
|
+
nodesById,
|
|
344
|
+
edgesById,
|
|
345
|
+
scene,
|
|
346
|
+
registry: defaultCoreOverlayRegistry,
|
|
347
|
+
};
|
|
348
|
+
if (renderer.update) {
|
|
349
|
+
renderer.update(overlayCtx, group);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
group.innerHTML = renderer.render(overlayCtx);
|
|
353
|
+
}
|
|
354
|
+
// Clear dirty flag after successful update.
|
|
355
|
+
delete spec[OVERLAY_RUNTIME_DIRTY];
|
|
356
|
+
});
|
|
357
|
+
// 3) Remove stale overlays only if keys may have changed.
|
|
358
|
+
if (!overlayKeyCountMatches) {
|
|
359
|
+
existingOverlayGroups.forEach((el) => {
|
|
360
|
+
const id = el.getAttribute('data-overlay-id');
|
|
361
|
+
if (id && !processedOverlayIds.has(id)) {
|
|
362
|
+
el.remove();
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
277
367
|
}
|
|
278
368
|
/**
|
|
279
369
|
* Renders the scene to the DOM.
|
|
@@ -620,15 +710,17 @@ class VizBuilderImpl {
|
|
|
620
710
|
if (!group) {
|
|
621
711
|
group = document.createElementNS(svgNS, 'g');
|
|
622
712
|
group.setAttribute('data-overlay-id', uniqueKey);
|
|
623
|
-
group.setAttribute('class', `viz-overlay-${spec.id}`);
|
|
624
713
|
group.setAttribute('data-viz-role', 'overlay-group');
|
|
625
714
|
overlayLayer.appendChild(group);
|
|
626
715
|
}
|
|
716
|
+
// Keep wrapper class in sync even when reusing an existing group.
|
|
717
|
+
group.setAttribute('class', `viz-overlay-${spec.id}${spec.className ? ` ${spec.className}` : ''}`);
|
|
627
718
|
const ctx = {
|
|
628
719
|
spec,
|
|
629
720
|
nodesById,
|
|
630
721
|
edgesById: new Map(edges.map((e) => [e.id, e])),
|
|
631
722
|
scene,
|
|
723
|
+
registry: defaultCoreOverlayRegistry,
|
|
632
724
|
};
|
|
633
725
|
if (renderer.update) {
|
|
634
726
|
renderer.update(ctx, group);
|
|
@@ -798,7 +890,13 @@ class VizBuilderImpl {
|
|
|
798
890
|
overlays.forEach((spec) => {
|
|
799
891
|
const renderer = defaultCoreOverlayRegistry.get(spec.id);
|
|
800
892
|
if (renderer) {
|
|
801
|
-
svgContent += renderer.render({
|
|
893
|
+
svgContent += renderer.render({
|
|
894
|
+
spec,
|
|
895
|
+
nodesById,
|
|
896
|
+
edgesById,
|
|
897
|
+
scene,
|
|
898
|
+
registry: defaultCoreOverlayRegistry,
|
|
899
|
+
});
|
|
802
900
|
}
|
|
803
901
|
});
|
|
804
902
|
svgContent += '</g>';
|
|
@@ -932,8 +1030,10 @@ class NodeBuilderImpl {
|
|
|
932
1030
|
edge(from, to, id) {
|
|
933
1031
|
return this.parent.edge(from, to, id);
|
|
934
1032
|
}
|
|
935
|
-
overlay(
|
|
936
|
-
|
|
1033
|
+
overlay(arg1, arg2, arg3) {
|
|
1034
|
+
if (typeof arg1 === 'function')
|
|
1035
|
+
return this.parent.overlay(arg1);
|
|
1036
|
+
return this.parent.overlay(arg1, arg2, arg3);
|
|
937
1037
|
}
|
|
938
1038
|
build() {
|
|
939
1039
|
return this.parent.build();
|
|
@@ -1026,8 +1126,10 @@ class EdgeBuilderImpl {
|
|
|
1026
1126
|
edge(from, to, id) {
|
|
1027
1127
|
return this.parent.edge(from, to, id || `${from}->${to}`); // Default ID to from->to
|
|
1028
1128
|
}
|
|
1029
|
-
overlay(
|
|
1030
|
-
|
|
1129
|
+
overlay(arg1, arg2, arg3) {
|
|
1130
|
+
if (typeof arg1 === 'function')
|
|
1131
|
+
return this.parent.overlay(arg1);
|
|
1132
|
+
return this.parent.overlay(arg1, arg2, arg3);
|
|
1031
1133
|
}
|
|
1032
1134
|
build() {
|
|
1033
1135
|
return this.parent.build();
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { OverlayId, OverlayParams, VizOverlaySpec } from './types';
|
|
2
|
+
import type { CircleOverlayParams, GroupOverlayParams, RectOverlayParams, TextOverlayParams } from './overlays';
|
|
3
|
+
export type OverlayAddOptions = {
|
|
4
|
+
/** Optional stable key used to uniquely identify an overlay instance. */
|
|
5
|
+
key?: string;
|
|
6
|
+
/** Optional class applied by the overlay renderer. */
|
|
7
|
+
className?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Fluent overlay authoring.
|
|
11
|
+
*
|
|
12
|
+
* - Produces portable `VizOverlaySpec[]` data (rendering remains registry-driven).
|
|
13
|
+
* - Typed by `OverlayKindRegistry` when available, with a back-compat escape hatch.
|
|
14
|
+
*/
|
|
15
|
+
export declare class OverlayBuilder {
|
|
16
|
+
private readonly specs;
|
|
17
|
+
private readonly keyCounters;
|
|
18
|
+
/**
|
|
19
|
+
* Add an overlay spec.
|
|
20
|
+
*
|
|
21
|
+
* Overload 1: typed overlay ids via `OverlayKindRegistry`.
|
|
22
|
+
*/
|
|
23
|
+
add<K extends OverlayId>(id: K, params: OverlayParams<K>, options?: OverlayAddOptions): this;
|
|
24
|
+
/**
|
|
25
|
+
* Add an overlay spec.
|
|
26
|
+
*
|
|
27
|
+
* Overload 2: back-compat escape hatch for arbitrary ids.
|
|
28
|
+
*/
|
|
29
|
+
add(id: string, params: any, options?: OverlayAddOptions): this;
|
|
30
|
+
/** Remove overlays by key, or (if unkeyed) by id. */
|
|
31
|
+
remove(keyOrId: string): this;
|
|
32
|
+
/** Remove all overlays. */
|
|
33
|
+
clear(): this;
|
|
34
|
+
build(): VizOverlaySpec[];
|
|
35
|
+
/** Add a generic rectangle overlay (built-in, no custom registry needed). */
|
|
36
|
+
rect(params: RectOverlayParams, options?: OverlayAddOptions): this;
|
|
37
|
+
/** Add a generic circle overlay (built-in, no custom registry needed). */
|
|
38
|
+
circle(params: CircleOverlayParams, options?: OverlayAddOptions): this;
|
|
39
|
+
/** Add a generic text overlay (built-in, no custom registry needed). */
|
|
40
|
+
text(params: TextOverlayParams, options?: OverlayAddOptions): this;
|
|
41
|
+
/**
|
|
42
|
+
* Add a composite group overlay (built-in).
|
|
43
|
+
*
|
|
44
|
+
* Children are authored via the callback and rendered inside a single SVG <g>.
|
|
45
|
+
* You can animate the group by targeting its key and tweening `x`, `y`, `scale`, `rotation`, `opacity`.
|
|
46
|
+
*/
|
|
47
|
+
group(params: Omit<GroupOverlayParams, 'children'>, buildChildren: (overlay: OverlayBuilder) => unknown, options?: OverlayAddOptions): this;
|
|
48
|
+
}
|
|
49
|
+
/** Convenience helper for one-off overlay list compilation. */
|
|
50
|
+
export declare function buildOverlaySpecs(cb: (overlay: OverlayBuilder) => unknown): VizOverlaySpec[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fluent overlay authoring.
|
|
3
|
+
*
|
|
4
|
+
* - Produces portable `VizOverlaySpec[]` data (rendering remains registry-driven).
|
|
5
|
+
* - Typed by `OverlayKindRegistry` when available, with a back-compat escape hatch.
|
|
6
|
+
*/
|
|
7
|
+
export class OverlayBuilder {
|
|
8
|
+
specs = [];
|
|
9
|
+
keyCounters = new Map();
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
11
|
+
add(id, params, options) {
|
|
12
|
+
const className = options?.className;
|
|
13
|
+
// If a user adds multiple overlays of the same id without keys, they will
|
|
14
|
+
// collide at reconcile time (since the DOM layer uses `key || id`).
|
|
15
|
+
// We auto-generate a stable-ish key in that case.
|
|
16
|
+
let key = options?.key;
|
|
17
|
+
if (!key) {
|
|
18
|
+
const hasUnkeyedSameId = this.specs.some((s) => s.id === id && (s.key === undefined || s.key === ''));
|
|
19
|
+
if (hasUnkeyedSameId) {
|
|
20
|
+
const next = (this.keyCounters.get(id) ?? 0) + 1;
|
|
21
|
+
this.keyCounters.set(id, next);
|
|
22
|
+
key = `${id}#${next}`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
this.specs.push({ id, params, key, className });
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
/** Remove overlays by key, or (if unkeyed) by id. */
|
|
29
|
+
remove(keyOrId) {
|
|
30
|
+
for (let i = this.specs.length - 1; i >= 0; i--) {
|
|
31
|
+
const s = this.specs[i];
|
|
32
|
+
if (!s)
|
|
33
|
+
continue;
|
|
34
|
+
const matchesKey = s.key === keyOrId;
|
|
35
|
+
const matchesUnkeyedId = !s.key && s.id === keyOrId;
|
|
36
|
+
if (matchesKey || matchesUnkeyedId)
|
|
37
|
+
this.specs.splice(i, 1);
|
|
38
|
+
}
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
/** Remove all overlays. */
|
|
42
|
+
clear() {
|
|
43
|
+
this.specs.length = 0;
|
|
44
|
+
this.keyCounters.clear();
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
build() {
|
|
48
|
+
return [...this.specs];
|
|
49
|
+
}
|
|
50
|
+
/** Add a generic rectangle overlay (built-in, no custom registry needed). */
|
|
51
|
+
rect(params, options) {
|
|
52
|
+
return this.add('rect', params, options);
|
|
53
|
+
}
|
|
54
|
+
/** Add a generic circle overlay (built-in, no custom registry needed). */
|
|
55
|
+
circle(params, options) {
|
|
56
|
+
return this.add('circle', params, options);
|
|
57
|
+
}
|
|
58
|
+
/** Add a generic text overlay (built-in, no custom registry needed). */
|
|
59
|
+
text(params, options) {
|
|
60
|
+
return this.add('text', params, options);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Add a composite group overlay (built-in).
|
|
64
|
+
*
|
|
65
|
+
* Children are authored via the callback and rendered inside a single SVG <g>.
|
|
66
|
+
* You can animate the group by targeting its key and tweening `x`, `y`, `scale`, `rotation`, `opacity`.
|
|
67
|
+
*/
|
|
68
|
+
group(params, buildChildren, options) {
|
|
69
|
+
const childOverlay = new OverlayBuilder();
|
|
70
|
+
buildChildren(childOverlay);
|
|
71
|
+
const children = childOverlay.build();
|
|
72
|
+
return this.add('group', { ...params, children }, options);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Convenience helper for one-off overlay list compilation. */
|
|
76
|
+
export function buildOverlaySpecs(cb) {
|
|
77
|
+
const overlay = new OverlayBuilder();
|
|
78
|
+
cb(overlay);
|
|
79
|
+
return overlay.build();
|
|
80
|
+
}
|
package/dist/overlays.d.ts
CHANGED
|
@@ -1,9 +1,115 @@
|
|
|
1
1
|
import type { VizNode, VizEdge, VizOverlaySpec, VizScene } from './types';
|
|
2
|
+
export type SignalOverlayParams = {
|
|
3
|
+
from: string;
|
|
4
|
+
to: string;
|
|
5
|
+
progress: number;
|
|
6
|
+
magnitude?: number;
|
|
7
|
+
};
|
|
8
|
+
export type GridLabelsOverlayParams = {
|
|
9
|
+
colLabels?: Record<number, string>;
|
|
10
|
+
rowLabels?: Record<number, string>;
|
|
11
|
+
yOffset?: number;
|
|
12
|
+
xOffset?: number;
|
|
13
|
+
};
|
|
14
|
+
export interface DataPoint {
|
|
15
|
+
id: string;
|
|
16
|
+
currentNodeId: string;
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
}
|
|
19
|
+
export type DataPointsOverlayParams = {
|
|
20
|
+
points: DataPoint[];
|
|
21
|
+
};
|
|
22
|
+
export type RectOverlayParams = {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
w: number;
|
|
26
|
+
h: number;
|
|
27
|
+
rx?: number;
|
|
28
|
+
ry?: number;
|
|
29
|
+
opacity?: number;
|
|
30
|
+
/** SVG fill (defaults to a visible blue). Can be overridden by CSS via className. */
|
|
31
|
+
fill?: string;
|
|
32
|
+
/** SVG stroke (defaults to a visible blue). Can be overridden by CSS via className. */
|
|
33
|
+
stroke?: string;
|
|
34
|
+
/** SVG stroke-width (defaults to 3). Can be overridden by CSS via className. */
|
|
35
|
+
strokeWidth?: number;
|
|
36
|
+
};
|
|
37
|
+
export type CircleOverlayParams = {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
r: number;
|
|
41
|
+
opacity?: number;
|
|
42
|
+
/** SVG fill (defaults to a visible blue). Can be overridden by CSS via className. */
|
|
43
|
+
fill?: string;
|
|
44
|
+
/** SVG stroke (defaults to a visible blue). Can be overridden by CSS via className. */
|
|
45
|
+
stroke?: string;
|
|
46
|
+
/** SVG stroke-width (defaults to 3). Can be overridden by CSS via className. */
|
|
47
|
+
strokeWidth?: number;
|
|
48
|
+
};
|
|
49
|
+
export type TextOverlayParams = {
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
text: string;
|
|
53
|
+
opacity?: number;
|
|
54
|
+
/** SVG fill color (defaults to #111). Can be overridden by CSS via className. */
|
|
55
|
+
fill?: string;
|
|
56
|
+
fontSize?: number;
|
|
57
|
+
fontWeight?: string | number;
|
|
58
|
+
textAnchor?: 'start' | 'middle' | 'end';
|
|
59
|
+
dominantBaseline?: string;
|
|
60
|
+
};
|
|
61
|
+
export type GroupOverlayParams = {
|
|
62
|
+
/**
|
|
63
|
+
* Translate (group-local origin).
|
|
64
|
+
*
|
|
65
|
+
* If `from`/`to` are provided, these act as an additional offset.
|
|
66
|
+
*/
|
|
67
|
+
x?: number;
|
|
68
|
+
y?: number;
|
|
69
|
+
/**
|
|
70
|
+
* Optional node ids used to drive the group's position via `progress`.
|
|
71
|
+
*
|
|
72
|
+
* When set, the group will translate along the line from `from` to `to`.
|
|
73
|
+
*/
|
|
74
|
+
from?: string;
|
|
75
|
+
to?: string;
|
|
76
|
+
/** Interpolation 0..1 used when `from`/`to` are set. */
|
|
77
|
+
progress?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Optional "pulse" value 0..1.
|
|
80
|
+
*
|
|
81
|
+
* When provided, it scales the group slightly (in addition to `scale`).
|
|
82
|
+
*/
|
|
83
|
+
magnitude?: number;
|
|
84
|
+
/** Scale around group origin. */
|
|
85
|
+
scale?: number;
|
|
86
|
+
/** Rotate (degrees) around group origin. */
|
|
87
|
+
rotation?: number;
|
|
88
|
+
/** Group opacity (multiplies with child opacity). */
|
|
89
|
+
opacity?: number;
|
|
90
|
+
/** Child overlays rendered inside this group. Coordinates are group-local. */
|
|
91
|
+
children: VizOverlaySpec[];
|
|
92
|
+
};
|
|
93
|
+
declare module './types' {
|
|
94
|
+
interface OverlayKindRegistry {
|
|
95
|
+
signal: SignalOverlayParams;
|
|
96
|
+
'grid-labels': GridLabelsOverlayParams;
|
|
97
|
+
'data-points': DataPointsOverlayParams;
|
|
98
|
+
/** Generic overlay primitives (no custom registry needed). */
|
|
99
|
+
rect: RectOverlayParams;
|
|
100
|
+
circle: CircleOverlayParams;
|
|
101
|
+
text: TextOverlayParams;
|
|
102
|
+
/** Overlay container that can hold child overlays and be animated as a unit. */
|
|
103
|
+
group: GroupOverlayParams;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
2
106
|
export interface CoreOverlayRenderContext<T = any> {
|
|
3
107
|
spec: VizOverlaySpec<T>;
|
|
4
108
|
nodesById: Map<string, VizNode>;
|
|
5
109
|
edgesById: Map<string, VizEdge>;
|
|
6
110
|
scene: VizScene;
|
|
111
|
+
/** Registry reference (useful for composite overlays like `group`). */
|
|
112
|
+
registry?: CoreOverlayRegistry;
|
|
7
113
|
}
|
|
8
114
|
export interface CoreOverlayRenderer<T = any> {
|
|
9
115
|
render: (ctx: CoreOverlayRenderContext<T>) => string;
|
|
@@ -14,25 +120,11 @@ export declare class CoreOverlayRegistry {
|
|
|
14
120
|
register(id: string, renderer: CoreOverlayRenderer): this;
|
|
15
121
|
get(id: string): CoreOverlayRenderer<any> | undefined;
|
|
16
122
|
}
|
|
17
|
-
export declare const coreSignalOverlay: CoreOverlayRenderer<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export declare const
|
|
24
|
-
colLabels?: Record<number, string>;
|
|
25
|
-
rowLabels?: Record<number, string>;
|
|
26
|
-
yOffset?: number;
|
|
27
|
-
xOffset?: number;
|
|
28
|
-
}>;
|
|
29
|
-
interface DataPoint {
|
|
30
|
-
id: string;
|
|
31
|
-
currentNodeId: string;
|
|
32
|
-
[key: string]: any;
|
|
33
|
-
}
|
|
34
|
-
export declare const coreDataPointOverlay: CoreOverlayRenderer<{
|
|
35
|
-
points: DataPoint[];
|
|
36
|
-
}>;
|
|
123
|
+
export declare const coreSignalOverlay: CoreOverlayRenderer<SignalOverlayParams>;
|
|
124
|
+
export declare const coreGridLabelsOverlay: CoreOverlayRenderer<GridLabelsOverlayParams>;
|
|
125
|
+
export declare const coreDataPointOverlay: CoreOverlayRenderer<DataPointsOverlayParams>;
|
|
126
|
+
export declare const coreRectOverlay: CoreOverlayRenderer<RectOverlayParams>;
|
|
127
|
+
export declare const coreCircleOverlay: CoreOverlayRenderer<CircleOverlayParams>;
|
|
128
|
+
export declare const coreTextOverlay: CoreOverlayRenderer<TextOverlayParams>;
|
|
129
|
+
export declare const coreGroupOverlay: CoreOverlayRenderer<GroupOverlayParams>;
|
|
37
130
|
export declare const defaultCoreOverlayRegistry: CoreOverlayRegistry;
|
|
38
|
-
export {};
|
package/dist/overlays.js
CHANGED
|
@@ -64,6 +64,7 @@ export const coreGridLabelsOverlay = {
|
|
|
64
64
|
return output;
|
|
65
65
|
},
|
|
66
66
|
};
|
|
67
|
+
// ... (OverlayRegistry and other exports remain unchanged) ...
|
|
67
68
|
// Built-in Overlay: Data Points
|
|
68
69
|
export const coreDataPointOverlay = {
|
|
69
70
|
render: ({ spec, nodesById }) => {
|
|
@@ -133,7 +134,324 @@ export const coreDataPointOverlay = {
|
|
|
133
134
|
});
|
|
134
135
|
},
|
|
135
136
|
};
|
|
137
|
+
// Generic Overlay: Rect
|
|
138
|
+
export const coreRectOverlay = {
|
|
139
|
+
render: ({ spec }) => {
|
|
140
|
+
const { x, y, w, h, rx, ry, opacity, fill, stroke, strokeWidth } = spec.params;
|
|
141
|
+
const cls = spec.className ?? 'viz-overlay-rect';
|
|
142
|
+
const rxAttr = rx !== undefined ? ` rx="${rx}"` : '';
|
|
143
|
+
const ryAttr = ry !== undefined ? ` ry="${ry}"` : '';
|
|
144
|
+
const opAttr = opacity !== undefined ? ` opacity="${opacity}"` : '';
|
|
145
|
+
const usingDefaultFill = fill === undefined;
|
|
146
|
+
const usingDefaultStroke = stroke === undefined;
|
|
147
|
+
const resolvedFill = fill ?? '#3b82f6';
|
|
148
|
+
const resolvedStroke = stroke ?? '#3b82f6';
|
|
149
|
+
const resolvedStrokeWidth = strokeWidth ?? 3;
|
|
150
|
+
const fillOpacityAttr = usingDefaultFill ? ' fill-opacity="0.12"' : '';
|
|
151
|
+
const strokeOpacityAttr = usingDefaultStroke ? ' stroke-opacity="0.9"' : '';
|
|
152
|
+
return `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${resolvedFill}"${fillOpacityAttr} stroke="${resolvedStroke}"${strokeOpacityAttr} stroke-width="${resolvedStrokeWidth}"${rxAttr}${ryAttr}${opAttr} class="${cls}" />`;
|
|
153
|
+
},
|
|
154
|
+
update: ({ spec }, container) => {
|
|
155
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
156
|
+
const { x, y, w, h, rx, ry, opacity, fill, stroke, strokeWidth } = spec.params;
|
|
157
|
+
const cls = spec.className ?? 'viz-overlay-rect';
|
|
158
|
+
let rect = container.querySelector('rect');
|
|
159
|
+
if (!rect) {
|
|
160
|
+
rect = document.createElementNS(svgNS, 'rect');
|
|
161
|
+
container.appendChild(rect);
|
|
162
|
+
}
|
|
163
|
+
rect.setAttribute('x', String(x));
|
|
164
|
+
rect.setAttribute('y', String(y));
|
|
165
|
+
rect.setAttribute('width', String(w));
|
|
166
|
+
rect.setAttribute('height', String(h));
|
|
167
|
+
if (fill === undefined) {
|
|
168
|
+
rect.setAttribute('fill', '#3b82f6');
|
|
169
|
+
rect.setAttribute('fill-opacity', '0.12');
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
rect.setAttribute('fill', fill);
|
|
173
|
+
rect.removeAttribute('fill-opacity');
|
|
174
|
+
}
|
|
175
|
+
if (stroke === undefined) {
|
|
176
|
+
rect.setAttribute('stroke', '#3b82f6');
|
|
177
|
+
rect.setAttribute('stroke-opacity', '0.9');
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
rect.setAttribute('stroke', stroke);
|
|
181
|
+
rect.removeAttribute('stroke-opacity');
|
|
182
|
+
}
|
|
183
|
+
rect.setAttribute('stroke-width', String(strokeWidth ?? 3));
|
|
184
|
+
if (rx !== undefined)
|
|
185
|
+
rect.setAttribute('rx', String(rx));
|
|
186
|
+
else
|
|
187
|
+
rect.removeAttribute('rx');
|
|
188
|
+
if (ry !== undefined)
|
|
189
|
+
rect.setAttribute('ry', String(ry));
|
|
190
|
+
else
|
|
191
|
+
rect.removeAttribute('ry');
|
|
192
|
+
if (opacity !== undefined)
|
|
193
|
+
rect.setAttribute('opacity', String(opacity));
|
|
194
|
+
else
|
|
195
|
+
rect.removeAttribute('opacity');
|
|
196
|
+
rect.setAttribute('class', cls);
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
// Generic Overlay: Circle
|
|
200
|
+
export const coreCircleOverlay = {
|
|
201
|
+
render: ({ spec }) => {
|
|
202
|
+
const { x, y, r, opacity, fill, stroke, strokeWidth } = spec.params;
|
|
203
|
+
const cls = spec.className ?? 'viz-overlay-circle';
|
|
204
|
+
const opAttr = opacity !== undefined ? ` opacity="${opacity}"` : '';
|
|
205
|
+
const usingDefaultFill = fill === undefined;
|
|
206
|
+
const usingDefaultStroke = stroke === undefined;
|
|
207
|
+
const resolvedFill = fill ?? '#3b82f6';
|
|
208
|
+
const resolvedStroke = stroke ?? '#3b82f6';
|
|
209
|
+
const resolvedStrokeWidth = strokeWidth ?? 3;
|
|
210
|
+
const fillOpacityAttr = usingDefaultFill ? ' fill-opacity="0.12"' : '';
|
|
211
|
+
const strokeOpacityAttr = usingDefaultStroke ? ' stroke-opacity="0.9"' : '';
|
|
212
|
+
return `<circle cx="${x}" cy="${y}" r="${r}" fill="${resolvedFill}"${fillOpacityAttr} stroke="${resolvedStroke}"${strokeOpacityAttr} stroke-width="${resolvedStrokeWidth}"${opAttr} class="${cls}" />`;
|
|
213
|
+
},
|
|
214
|
+
update: ({ spec }, container) => {
|
|
215
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
216
|
+
const { x, y, r, opacity, fill, stroke, strokeWidth } = spec.params;
|
|
217
|
+
const cls = spec.className ?? 'viz-overlay-circle';
|
|
218
|
+
let circle = container.querySelector('circle');
|
|
219
|
+
if (!circle) {
|
|
220
|
+
circle = document.createElementNS(svgNS, 'circle');
|
|
221
|
+
container.appendChild(circle);
|
|
222
|
+
}
|
|
223
|
+
circle.setAttribute('cx', String(x));
|
|
224
|
+
circle.setAttribute('cy', String(y));
|
|
225
|
+
circle.setAttribute('r', String(r));
|
|
226
|
+
if (fill === undefined) {
|
|
227
|
+
circle.setAttribute('fill', '#3b82f6');
|
|
228
|
+
circle.setAttribute('fill-opacity', '0.12');
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
circle.setAttribute('fill', fill);
|
|
232
|
+
circle.removeAttribute('fill-opacity');
|
|
233
|
+
}
|
|
234
|
+
if (stroke === undefined) {
|
|
235
|
+
circle.setAttribute('stroke', '#3b82f6');
|
|
236
|
+
circle.setAttribute('stroke-opacity', '0.9');
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
circle.setAttribute('stroke', stroke);
|
|
240
|
+
circle.removeAttribute('stroke-opacity');
|
|
241
|
+
}
|
|
242
|
+
circle.setAttribute('stroke-width', String(strokeWidth ?? 3));
|
|
243
|
+
if (opacity !== undefined)
|
|
244
|
+
circle.setAttribute('opacity', String(opacity));
|
|
245
|
+
else
|
|
246
|
+
circle.removeAttribute('opacity');
|
|
247
|
+
circle.setAttribute('class', cls);
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
// Generic Overlay: Text
|
|
251
|
+
export const coreTextOverlay = {
|
|
252
|
+
render: ({ spec }) => {
|
|
253
|
+
const { x, y, text, opacity, fill, fontSize, fontWeight, textAnchor, dominantBaseline, } = spec.params;
|
|
254
|
+
const cls = spec.className ?? 'viz-overlay-text';
|
|
255
|
+
const opAttr = opacity !== undefined ? ` opacity="${opacity}"` : '';
|
|
256
|
+
const fsAttr = fontSize !== undefined ? ` font-size="${fontSize}"` : '';
|
|
257
|
+
const fwAttr = fontWeight !== undefined ? ` font-weight="${fontWeight}"` : '';
|
|
258
|
+
const taAttr = textAnchor !== undefined ? ` text-anchor="${textAnchor}"` : '';
|
|
259
|
+
const dbAttr = dominantBaseline !== undefined
|
|
260
|
+
? ` dominant-baseline="${dominantBaseline}"`
|
|
261
|
+
: '';
|
|
262
|
+
// Basic text rendering; users should avoid untrusted HTML here.
|
|
263
|
+
const resolvedFill = fill ?? '#111';
|
|
264
|
+
return `<text x="${x}" y="${y}" fill="${resolvedFill}"${opAttr}${fsAttr}${fwAttr}${taAttr}${dbAttr} class="${cls}">${text}</text>`;
|
|
265
|
+
},
|
|
266
|
+
update: ({ spec }, container) => {
|
|
267
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
268
|
+
const { x, y, text, opacity, fill, fontSize, fontWeight, textAnchor, dominantBaseline, } = spec.params;
|
|
269
|
+
const cls = spec.className ?? 'viz-overlay-text';
|
|
270
|
+
let el = container.querySelector('text');
|
|
271
|
+
if (!el) {
|
|
272
|
+
el = document.createElementNS(svgNS, 'text');
|
|
273
|
+
container.appendChild(el);
|
|
274
|
+
}
|
|
275
|
+
el.setAttribute('x', String(x));
|
|
276
|
+
el.setAttribute('y', String(y));
|
|
277
|
+
el.setAttribute('fill', fill ?? '#111');
|
|
278
|
+
if (opacity !== undefined)
|
|
279
|
+
el.setAttribute('opacity', String(opacity));
|
|
280
|
+
else
|
|
281
|
+
el.removeAttribute('opacity');
|
|
282
|
+
if (fontSize !== undefined)
|
|
283
|
+
el.setAttribute('font-size', String(fontSize));
|
|
284
|
+
else
|
|
285
|
+
el.removeAttribute('font-size');
|
|
286
|
+
if (fontWeight !== undefined)
|
|
287
|
+
el.setAttribute('font-weight', String(fontWeight));
|
|
288
|
+
else
|
|
289
|
+
el.removeAttribute('font-weight');
|
|
290
|
+
if (textAnchor !== undefined)
|
|
291
|
+
el.setAttribute('text-anchor', textAnchor);
|
|
292
|
+
else
|
|
293
|
+
el.removeAttribute('text-anchor');
|
|
294
|
+
if (dominantBaseline !== undefined)
|
|
295
|
+
el.setAttribute('dominant-baseline', dominantBaseline);
|
|
296
|
+
else
|
|
297
|
+
el.removeAttribute('dominant-baseline');
|
|
298
|
+
el.setAttribute('class', cls);
|
|
299
|
+
el.textContent = text;
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
function groupTransform(params) {
|
|
303
|
+
const tx = params.x ?? 0;
|
|
304
|
+
const ty = params.y ?? 0;
|
|
305
|
+
const s = params.scale ?? 1;
|
|
306
|
+
const r = params.rotation ?? 0;
|
|
307
|
+
// translate first so scale/rotation occur around the group origin.
|
|
308
|
+
const parts = [`translate(${tx}, ${ty})`];
|
|
309
|
+
if (r)
|
|
310
|
+
parts.push(`rotate(${r})`);
|
|
311
|
+
if (s !== 1)
|
|
312
|
+
parts.push(`scale(${s})`);
|
|
313
|
+
return parts.join(' ');
|
|
314
|
+
}
|
|
315
|
+
function clamp01(v) {
|
|
316
|
+
if (v < 0)
|
|
317
|
+
return 0;
|
|
318
|
+
if (v > 1)
|
|
319
|
+
return 1;
|
|
320
|
+
return v;
|
|
321
|
+
}
|
|
322
|
+
function effectiveNodePos(node) {
|
|
323
|
+
return {
|
|
324
|
+
x: node.runtime?.x ?? node.pos.x,
|
|
325
|
+
y: node.runtime?.y ?? node.pos.y,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function resolveGroupTransformInputs(params, nodesById) {
|
|
329
|
+
const baseX = params.x ?? 0;
|
|
330
|
+
const baseY = params.y ?? 0;
|
|
331
|
+
let x = baseX;
|
|
332
|
+
let y = baseY;
|
|
333
|
+
if (params.from && params.to) {
|
|
334
|
+
const start = nodesById.get(params.from);
|
|
335
|
+
const end = nodesById.get(params.to);
|
|
336
|
+
if (start && end) {
|
|
337
|
+
const p = clamp01(params.progress ?? 0);
|
|
338
|
+
const a = effectiveNodePos(start);
|
|
339
|
+
const b = effectiveNodePos(end);
|
|
340
|
+
x = a.x + (b.x - a.x) * p + baseX;
|
|
341
|
+
y = a.y + (b.y - a.y) * p + baseY;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const userScale = params.scale ?? 1;
|
|
345
|
+
const m = params.magnitude;
|
|
346
|
+
const magScale = m === undefined ? 1 : 0.85 + 0.3 * clamp01(Math.abs(m));
|
|
347
|
+
const scale = userScale * magScale;
|
|
348
|
+
return {
|
|
349
|
+
x,
|
|
350
|
+
y,
|
|
351
|
+
scale,
|
|
352
|
+
rotation: params.rotation ?? 0,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
// Composite Overlay: Group
|
|
356
|
+
export const coreGroupOverlay = {
|
|
357
|
+
render: ({ spec, nodesById, edgesById, scene, registry }) => {
|
|
358
|
+
const { children, opacity } = spec.params;
|
|
359
|
+
const inputs = resolveGroupTransformInputs(spec.params, nodesById);
|
|
360
|
+
const tr = groupTransform(inputs);
|
|
361
|
+
const opAttr = opacity !== undefined ? ` opacity="${opacity}"` : '';
|
|
362
|
+
const reg = registry;
|
|
363
|
+
if (!reg) {
|
|
364
|
+
// Best-effort render even if registry is missing.
|
|
365
|
+
return `<g transform="${tr}"${opAttr}></g>`;
|
|
366
|
+
}
|
|
367
|
+
let output = `<g transform="${tr}"${opAttr}>`;
|
|
368
|
+
children.forEach((childSpec, idx) => {
|
|
369
|
+
const renderer = reg.get(childSpec.id);
|
|
370
|
+
if (!renderer)
|
|
371
|
+
return;
|
|
372
|
+
const childCtx = {
|
|
373
|
+
spec: childSpec,
|
|
374
|
+
nodesById,
|
|
375
|
+
edgesById,
|
|
376
|
+
scene,
|
|
377
|
+
registry: reg,
|
|
378
|
+
};
|
|
379
|
+
// Wrap children in their own <g> so update() has stable containers.
|
|
380
|
+
const key = childSpec.key
|
|
381
|
+
? `key:${childSpec.key}`
|
|
382
|
+
: `idx:${idx}:${childSpec.id}`;
|
|
383
|
+
output += `<g data-viz-role="overlay-child" data-overlay-child-id="${key}">`;
|
|
384
|
+
output += renderer.render(childCtx);
|
|
385
|
+
output += '</g>';
|
|
386
|
+
});
|
|
387
|
+
output += '</g>';
|
|
388
|
+
return output;
|
|
389
|
+
},
|
|
390
|
+
update: ({ spec, nodesById, edgesById, scene, registry }, container) => {
|
|
391
|
+
const reg = registry;
|
|
392
|
+
if (!reg)
|
|
393
|
+
return;
|
|
394
|
+
const { children, opacity } = spec.params;
|
|
395
|
+
const inputs = resolveGroupTransformInputs(spec.params, nodesById);
|
|
396
|
+
container.setAttribute('transform', groupTransform(inputs));
|
|
397
|
+
if (opacity !== undefined) {
|
|
398
|
+
container.setAttribute('opacity', String(opacity));
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
container.removeAttribute('opacity');
|
|
402
|
+
}
|
|
403
|
+
const svgNS = 'http://www.w3.org/2000/svg';
|
|
404
|
+
const existing = new Map();
|
|
405
|
+
Array.from(container.children).forEach((child) => {
|
|
406
|
+
if (child instanceof SVGGElement) {
|
|
407
|
+
const id = child.getAttribute('data-overlay-child-id');
|
|
408
|
+
if (id)
|
|
409
|
+
existing.set(id, child);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
const keep = new Set();
|
|
413
|
+
children.forEach((childSpec, idx) => {
|
|
414
|
+
const renderer = reg.get(childSpec.id);
|
|
415
|
+
if (!renderer)
|
|
416
|
+
return;
|
|
417
|
+
const key = childSpec.key
|
|
418
|
+
? `key:${childSpec.key}`
|
|
419
|
+
: `idx:${idx}:${childSpec.id}`;
|
|
420
|
+
keep.add(key);
|
|
421
|
+
let childGroup = existing.get(key);
|
|
422
|
+
if (!childGroup) {
|
|
423
|
+
childGroup = document.createElementNS(svgNS, 'g');
|
|
424
|
+
childGroup.setAttribute('data-viz-role', 'overlay-child');
|
|
425
|
+
childGroup.setAttribute('data-overlay-child-id', key);
|
|
426
|
+
container.appendChild(childGroup);
|
|
427
|
+
}
|
|
428
|
+
const childCtx = {
|
|
429
|
+
spec: childSpec,
|
|
430
|
+
nodesById,
|
|
431
|
+
edgesById,
|
|
432
|
+
scene,
|
|
433
|
+
registry: reg,
|
|
434
|
+
};
|
|
435
|
+
if (renderer.update) {
|
|
436
|
+
renderer.update(childCtx, childGroup);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
childGroup.innerHTML = renderer.render(childCtx);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
existing.forEach((el, id) => {
|
|
443
|
+
if (!keep.has(id))
|
|
444
|
+
el.remove();
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
};
|
|
136
448
|
export const defaultCoreOverlayRegistry = new CoreOverlayRegistry()
|
|
137
449
|
.register('signal', coreSignalOverlay)
|
|
138
450
|
.register('grid-labels', coreGridLabelsOverlay)
|
|
139
|
-
.register('data-points', coreDataPointOverlay)
|
|
451
|
+
.register('data-points', coreDataPointOverlay)
|
|
452
|
+
// Generic primitives
|
|
453
|
+
.register('rect', coreRectOverlay)
|
|
454
|
+
.register('circle', coreCircleOverlay)
|
|
455
|
+
.register('text', coreTextOverlay)
|
|
456
|
+
// Composite overlays
|
|
457
|
+
.register('group', coreGroupOverlay);
|
package/dist/styles.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const DEFAULT_VIZ_CSS = "\n.viz-canvas {\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.viz-node-label,\n.viz-edge-label {\n dominant-baseline: middle;\n alignment-baseline: middle;\n transform: translateY(
|
|
1
|
+
export declare const DEFAULT_VIZ_CSS = "\n.viz-canvas {\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.viz-node-label,\n.viz-edge-label {\n text-anchor: middle;\n dominant-baseline: middle;\n alignment-baseline: middle;\n transform: translateY(0);\n}\n\n.viz-canvas svg {\n width: 100%;\n height: 100%;\n overflow: visible;\n}\n\n/* Keyframes */\n@keyframes vizFlow {\n from {\n stroke-dashoffset: 20;\n }\n to {\n stroke-dashoffset: 0;\n }\n}\n\n/* Animation Classes */\n\n/* Flow Animation (Dashed line moving) */\n.viz-anim-flow .viz-edge {\n stroke-dasharray: 5, 5;\n animation: vizFlow var(--viz-anim-duration, 2s) linear infinite;\n}\n\n/* Node Transition */\n.viz-node-group {\n transition: transform 0.3s ease-out, opacity 0.3s ease-out;\n}\n\n/* Overlay Classes */\n.viz-grid-label {\n fill: #6B7280;\n font-size: 14px;\n font-weight: 600;\n opacity: 1;\n}\n\n.viz-signal {\n fill: #3B82F6;\n cursor: pointer;\n pointer-events: all; \n transition: transform 0.2s ease-out, fill 0.2s ease-out;\n}\n\n.viz-signal .viz-signal-shape {\n fill: inherit;\n}\n\n.viz-signal:hover {\n fill: #60A5FA;\n transform: scale(1.5);\n}\n\n.viz-data-point {\n fill: #F59E0B;\n transition: cx 0.3s ease-out, cy 0.3s ease-out;\n}\n";
|
package/dist/styles.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -86,6 +86,34 @@ export interface VizEdge {
|
|
|
86
86
|
onClick?: (id: string, edge: VizEdge) => void;
|
|
87
87
|
animations?: VizAnimSpec[];
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Overlay kind -> params mapping.
|
|
91
|
+
*
|
|
92
|
+
* This interface is intentionally empty in core and is meant to be augmented by:
|
|
93
|
+
* - core overlays (in this repo)
|
|
94
|
+
* - downstream libraries/apps (via TS module augmentation)
|
|
95
|
+
*/
|
|
96
|
+
export interface OverlayKindRegistry {
|
|
97
|
+
}
|
|
98
|
+
/** Internal runtime flag used to mark overlays as needing a DOM update. */
|
|
99
|
+
export declare const OVERLAY_RUNTIME_DIRTY: unique symbol;
|
|
100
|
+
/** String overlay ids that are known/typed via `OverlayKindRegistry`. */
|
|
101
|
+
export type KnownOverlayId = Extract<keyof OverlayKindRegistry, string>;
|
|
102
|
+
/** Any overlay id (typed known ids + arbitrary custom ids). */
|
|
103
|
+
export type OverlayId = KnownOverlayId | (string & {});
|
|
104
|
+
/**
|
|
105
|
+
* Params type for a given overlay id.
|
|
106
|
+
* - Known ids resolve to their registered params type.
|
|
107
|
+
* - Unknown/custom ids fall back to `unknown` (escape hatch).
|
|
108
|
+
*/
|
|
109
|
+
export type OverlayParams<K extends string> = K extends KnownOverlayId ? OverlayKindRegistry[K] : unknown;
|
|
110
|
+
/** A type-safe overlay spec keyed by overlay id. */
|
|
111
|
+
export type TypedVizOverlaySpec<K extends OverlayId = OverlayId> = {
|
|
112
|
+
id: K;
|
|
113
|
+
key?: string;
|
|
114
|
+
params: OverlayParams<K>;
|
|
115
|
+
className?: string;
|
|
116
|
+
};
|
|
89
117
|
export type VizOverlaySpec<T = any> = {
|
|
90
118
|
id: string;
|
|
91
119
|
key?: string;
|
package/dist/types.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
/** Internal runtime flag used to mark overlays as needing a DOM update. */
|
|
2
|
+
export const OVERLAY_RUNTIME_DIRTY = Symbol('vizcraft.overlay.runtimeDirty');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vizcraft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "A fluent, type-safe SVG scene builder for composing nodes, edges, animations, and overlays with incremental DOM updates and no framework dependency.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"visualization",
|