vizcraft 1.1.0 → 1.2.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # vizcraft
2
2
 
3
+ ## 1.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`f72dc04`](https://github.com/ChipiKaf/vizcraft/commit/f72dc0405ccd752ad13eb746349b6a5945448c79) Thanks [@ChipiKaf](https://github.com/ChipiKaf)! - Added rich text labels to VizCraft with support for mixed formatting and line breaks. Introduced fluent .richLabel() APIs and declarative label.rich support. Improved runtime updates to keep animations stable, extended React rendering, and added test coverage. Docs and READMEs now include a live example.
8
+
3
9
  ## 1.1.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -85,6 +85,12 @@ pnpm -C packages/docs start
85
85
 
86
86
  The heart of VizCraft is the `VizBuilder`. It allows you to construct a `VizScene` which acts as the blueprint for your visualization.
87
87
 
88
+ For exporting frame snapshots during data-only playback, you can export an SVG that includes runtime overrides:
89
+
90
+ ```ts
91
+ const svg = builder.svg({ includeRuntime: true });
92
+ ```
93
+
88
94
  ```typescript
89
95
  b.view(width, height) // Set the coordinate space
90
96
  .grid(cols, rows) // (Optional) Define layout grid
@@ -92,6 +98,52 @@ b.view(width, height) // Set the coordinate space
92
98
  .edge(from, to); // Start defining an edge
93
99
  ```
94
100
 
101
+ ### Plugins
102
+
103
+ Extend the builder's functionality seamlessly using `.use()`. Plugins are functions that take the builder instance and optional configuration, allowing you to encapsulate reusable behaviors, export utilities, or composite nodes.
104
+
105
+ ```typescript
106
+ import { viz, VizPlugin } from 'vizcraft';
107
+
108
+ const watermarkPlugin: VizPlugin<{ text: string }> = (builder, opts) => {
109
+ builder.node('watermark', {
110
+ at: { x: 50, y: 20 },
111
+ rect: { w: 100, h: 20 },
112
+ label: opts?.text ?? 'Draft',
113
+ opacity: 0.5,
114
+ });
115
+ };
116
+
117
+ viz()
118
+ .view(800, 600)
119
+ .node('n1', { circle: { r: 20 } })
120
+ .use(watermarkPlugin, { text: 'Confidential' })
121
+ .build();
122
+ ```
123
+
124
+ **Event Hooks**
125
+
126
+ Plugins (or your own code) can also tap into the builder's lifecycle using `.on()`. This is particularly useful for interactive plugins that need to append HTML elements (like export buttons or tooltips) after VizCraft mounts the SVG to the DOM.
127
+
128
+ ```typescript
129
+ const exportUiPlugin: VizPlugin = (builder) => {
130
+ // Listen for the 'mount' event to inject a button next to the SVG
131
+ builder.on('mount', ({ container }) => {
132
+ const btn = document.createElement('button');
133
+ btn.innerText = 'Download PNG';
134
+ btn.onclick = () => {
135
+ /* export logic */
136
+ };
137
+
138
+ // Position the button absolutely over the container
139
+ btn.style.position = 'absolute';
140
+ btn.style.top = '10px';
141
+ btn.style.right = '10px';
142
+ container.appendChild(btn);
143
+ });
144
+ };
145
+ ```
146
+
95
147
  ### Declarative Options Overloads
96
148
 
97
149
  You can also configure nodes and edges in a single declarative call by passing an options object:
@@ -140,6 +192,10 @@ b.node('n1')
140
192
  .trapezoid(topW, bottomW, h) // Trapezoid
141
193
  .triangle(w, h, [direction]) // Triangle
142
194
  .label('Text', { dy: 5 }) // Label with offset
195
+ .richLabel((l) => l.text('Hello ').bold('World')) // Rich / mixed-format label
196
+ .image(href, w, h, opts?) // Embed an <image> inside the node
197
+ .icon(id, opts?) // Embed an icon from the icon registry (see registerIcon)
198
+ .svgContent(svg, opts) // Embed inline SVG content inside the node
143
199
  .class('css-class') // Custom CSS class
144
200
  .data({ ... }) // Attach custom data
145
201
  .port('out', { x: 50, y: 0 }) // Named connection port
@@ -174,6 +230,7 @@ b.edge('n1', 'n2')
174
230
  .arrow() // Add an arrowhead
175
231
  .straight() // (Default) Straight line
176
232
  .label('Connection')
233
+ .richLabel((l) => l.text('p').sup('95').text(' = ').bold('10ms'))
177
234
  .animate('flow'); // Add animation
178
235
 
179
236
  // Curved edge
@@ -185,6 +242,18 @@ b.edge('a', 'c').orthogonal().arrow();
185
242
  // Waypoints — intermediate points the edge passes through
186
243
  b.edge('x', 'y').curved().via(150, 50).via(200, 100).arrow();
187
244
 
245
+ // Arbitrary edge metadata (for routing flags, categories, etc.)
246
+ b.edge('a', 'b').meta({ customRouting: true, padding: 10 });
247
+
248
+ // Override edge path computation with a resolver hook
249
+ b.setEdgePathResolver((edge, scene, defaultResolver) => {
250
+ if (edge.meta?.customRouting) {
251
+ // Return an SVG path `d` string
252
+ return `M 0 0 L 10 10`;
253
+ }
254
+ return defaultResolver(edge, scene);
255
+ });
256
+
188
257
  // Per-edge styling (overrides CSS defaults)
189
258
  b.edge('a', 'b').stroke('#ff0000', 3).fill('none').opacity(0.8);
190
259
 
@@ -199,12 +268,19 @@ b.edge('a', 'b')
199
268
  .label('*', { position: 'end' })
200
269
  .arrow();
201
270
 
271
+ // Rich text labels (mixed formatting)
272
+ b.edge('a', 'b')
273
+ .richLabel((l) => l.text('p').sup('95').text(' ').bold('12ms'))
274
+ .arrow();
275
+
202
276
  // Edge markers / arrowhead types
203
277
  b.edge('a', 'b').markerEnd('arrowOpen'); // Open arrow (inheritance)
204
278
  b.edge('a', 'b').markerStart('diamond').markerEnd('arrow'); // UML composition
205
279
  b.edge('a', 'b').markerStart('diamondOpen').markerEnd('arrow'); // UML aggregation
206
280
  b.edge('a', 'b').arrow('both'); // Bidirectional arrows
207
281
  b.edge('a', 'b').markerStart('circleOpen').markerEnd('arrow'); // Association
282
+ // Self-loops (exits and enters the same node)
283
+ b.edge('n1', 'n1').loopSide('right').loopSize(40).arrow();
208
284
  b.edge('a', 'b').markerEnd('bar'); // ER cardinality
209
285
 
210
286
  // Connection ports — edges attach to specific points on nodes
@@ -228,6 +304,7 @@ b.edge('a', 'b').fromPort('right').toPort('left').arrow();
228
304
  | `.routing(mode)` | Set mode programmatically. |
229
305
  | `.via(x, y)` | Add an intermediate waypoint (chainable). |
230
306
  | `.label(text, opts?)` | Add a text label. Chain multiple calls for multi-position labels. `opts.position` can be `'start'`, `'mid'` (default), or `'end'`. |
307
+ | `.richLabel(cb, opts?)` | Add a rich / mixed-format label (nested SVG `<tspan>`s). Use `.newline()` in the callback to control line breaks. |
231
308
  | `.arrow([enabled])` | Shorthand for arrow markers. `true`/no-arg → markerEnd arrow. `'both'` → both ends. `'start'`/`'end'` → specific end. `false` → none. |
232
309
  | `.markerEnd(type)` | Set marker type at the target end. See `EdgeMarkerType`. |
233
310
  | `.markerStart(type)` | Set marker type at the source end. See `EdgeMarkerType`. |
package/dist/builder.d.ts CHANGED
@@ -1,9 +1,37 @@
1
- import type { VizScene, VizNode, VizEdge, NodeLabel, EdgeLabel, AnimationConfig, OverlayId, OverlayParams, VizGridConfig, ContainerConfig, EdgeRouting, EdgeMarkerType, NodeOptions, EdgeOptions, PanZoomOptions, PanZoomController, VizSceneMutator } from './types';
1
+ import type { VizScene, VizNode, VizEdge, NodeLabel, EdgeLabel, RichText, RichTextToken, AnimationConfig, OverlayId, OverlayParams, VizGridConfig, ContainerConfig, EdgeRouting, EdgeMarkerType, EdgePathResolver, NodeOptions, EdgeOptions, PanZoomOptions, PanZoomController, VizSceneMutator, VizPlugin, VizEventMap, LayoutAlgorithm, SvgExportOptions } from './types';
2
2
  import { OverlayBuilder } from './overlayBuilder';
3
3
  import type { AnimationSpec } from './anim/spec';
4
4
  import { type AnimationBuilder, type AnimatableProps, type TweenOptions } from './anim/animationBuilder';
5
5
  import { type PlaybackController } from './anim/playback';
6
6
  export interface VizBuilder extends VizSceneMutator {
7
+ /**
8
+ * Applies a plugin to the builder fluently.
9
+ * @param plugin The plugin function to execute
10
+ * @param options Optional configuration for the plugin
11
+ * @returns The builder, for fluent chaining
12
+ */
13
+ use<O>(plugin: VizPlugin<O>, options?: O): VizBuilder;
14
+ /**
15
+ * Applies a layout algorithm to the current nodes and edges.
16
+ * @param algorithm The layout function to execute
17
+ * @param options Optional configuration for the layout algorithm
18
+ * @returns The builder, for fluent chaining
19
+ */
20
+ layout<O>(algorithm: LayoutAlgorithm<O>, options?: O): VizBuilder;
21
+ /**
22
+ * Listen for lifecycle events (e.g. 'build', 'mount').
23
+ * @param event The event name
24
+ * @param callback The callback to execute when the event fires
25
+ * @returns An unsubscribe function
26
+ */
27
+ on<K extends keyof VizEventMap>(event: K, callback: (ev: VizEventMap[K]) => void): () => void;
28
+ /**
29
+ * Override edge SVG path computation.
30
+ *
31
+ * Intended to be installed before `mount()`. Applies to DOM reconciliation and
32
+ * `patchRuntime()`.
33
+ */
34
+ setEdgePathResolver(resolver: EdgePathResolver | null): VizBuilder;
7
35
  view(w: number, h: number): VizBuilder;
8
36
  grid(cols: number, rows: number, padding?: {
9
37
  x: number;
@@ -34,7 +62,7 @@ export interface VizBuilder extends VizSceneMutator {
34
62
  w: number;
35
63
  h: number;
36
64
  };
37
- svg(): string;
65
+ svg(opts?: SvgExportOptions): string;
38
66
  mount(container: HTMLElement): PanZoomController | undefined;
39
67
  mount(container: HTMLElement, opts: {
40
68
  autoplay?: boolean;
@@ -66,6 +94,46 @@ export interface VizBuilder extends VizSceneMutator {
66
94
  * This avoids full DOM reconciliation and is intended for animation frame updates.
67
95
  */
68
96
  patchRuntime(container: HTMLElement): void;
97
+ /**
98
+ * Tear down a previously mounted scene.
99
+ *
100
+ * - Removes the SVG tree from the container.
101
+ * - Destroys the PanZoomController (if created).
102
+ * - Cancels any pending requestAnimationFrame / animation loops.
103
+ * - Removes any internal event listeners (resize, mutation, etc.).
104
+ *
105
+ * Safe to call multiple times (no-op after first call).
106
+ * Safe to call even if `mount()` was never called.
107
+ */
108
+ destroy(): void;
109
+ }
110
+ export interface RichLabelBuilder {
111
+ text(text: string, opts?: Partial<Omit<Extract<RichTextToken, {
112
+ kind: 'span';
113
+ }>, 'kind' | 'text'>>): RichLabelBuilder;
114
+ bold(text: string, opts?: Partial<Omit<Extract<RichTextToken, {
115
+ kind: 'span';
116
+ }>, 'kind' | 'text'>>): RichLabelBuilder;
117
+ italic(text: string, opts?: Partial<Omit<Extract<RichTextToken, {
118
+ kind: 'span';
119
+ }>, 'kind' | 'text'>>): RichLabelBuilder;
120
+ code(text: string, opts?: Partial<Omit<Extract<RichTextToken, {
121
+ kind: 'span';
122
+ }>, 'kind' | 'text'>>): RichLabelBuilder;
123
+ color(text: string, fill: string, opts?: Partial<Omit<Extract<RichTextToken, {
124
+ kind: 'span';
125
+ }>, 'kind' | 'text' | 'fill'>>): RichLabelBuilder;
126
+ link(text: string, href: string, opts?: Partial<Omit<Extract<RichTextToken, {
127
+ kind: 'span';
128
+ }>, 'kind' | 'text' | 'href'>>): RichLabelBuilder;
129
+ sup(text: string, opts?: Partial<Omit<Extract<RichTextToken, {
130
+ kind: 'span';
131
+ }>, 'kind' | 'text' | 'baselineShift'>>): RichLabelBuilder;
132
+ sub(text: string, opts?: Partial<Omit<Extract<RichTextToken, {
133
+ kind: 'span';
134
+ }>, 'kind' | 'text' | 'baselineShift'>>): RichLabelBuilder;
135
+ newline(): RichLabelBuilder;
136
+ build(): RichText;
69
137
  }
70
138
  interface NodeBuilder {
71
139
  at(x: number, y: number): NodeBuilder;
@@ -95,11 +163,54 @@ interface NodeBuilder {
95
163
  star(points: number, outerR: number, innerR?: number): NodeBuilder;
96
164
  trapezoid(topW: number, bottomW: number, h: number): NodeBuilder;
97
165
  triangle(w: number, h: number, direction?: 'up' | 'down' | 'left' | 'right'): NodeBuilder;
166
+ /** Embed an SVG <image> inside/around the node. */
167
+ image(href: string, w: number, h: number, opts?: {
168
+ dx?: number;
169
+ dy?: number;
170
+ position?: 'center' | 'above' | 'below' | 'left' | 'right';
171
+ preserveAspectRatio?: string;
172
+ }): NodeBuilder;
173
+ image(href: string, opts: {
174
+ w: number;
175
+ h: number;
176
+ dx?: number;
177
+ dy?: number;
178
+ position?: 'center' | 'above' | 'below' | 'left' | 'right';
179
+ preserveAspectRatio?: string;
180
+ }): NodeBuilder;
181
+ /** Render a registered SVG icon inside/around the node. */
182
+ icon(id: string, opts: {
183
+ size: number;
184
+ color?: string;
185
+ dx?: number;
186
+ dy?: number;
187
+ position?: 'center' | 'above' | 'below' | 'left' | 'right';
188
+ }): NodeBuilder;
189
+ /** Render inline SVG content inside/around the node. */
190
+ svgContent(content: string, w: number, h: number, opts?: {
191
+ dx?: number;
192
+ dy?: number;
193
+ position?: 'center' | 'above' | 'below' | 'left' | 'right';
194
+ }): NodeBuilder;
195
+ svgContent(content: string, opts: {
196
+ w: number;
197
+ h: number;
198
+ dx?: number;
199
+ dy?: number;
200
+ position?: 'center' | 'above' | 'below' | 'left' | 'right';
201
+ }): NodeBuilder;
98
202
  label(text: string, opts?: Partial<NodeLabel>): NodeBuilder;
203
+ /**
204
+ * Create a rich text label (mixed formatting) using nested SVG <tspan> spans.
205
+ *
206
+ * Note: Rich labels currently support explicit newlines via `l.newline()`.
207
+ */
208
+ richLabel(cb: (l: RichLabelBuilder) => unknown, opts?: Partial<Omit<NodeLabel, 'text' | 'rich'>>): NodeBuilder;
99
209
  fill(color: string): NodeBuilder;
100
210
  stroke(color: string, width?: number): NodeBuilder;
101
211
  opacity(value: number): NodeBuilder;
102
212
  class(name: string): NodeBuilder;
213
+ zIndex(value: number): NodeBuilder;
103
214
  animate(type: string, config?: AnimationConfig): NodeBuilder;
104
215
  animate(cb: (anim: AnimationBuilder) => unknown): NodeBuilder;
105
216
  /** Sugar for `animate(a => a.to(...))`. */
@@ -127,7 +238,7 @@ interface NodeBuilder {
127
238
  overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
128
239
  overlay<T>(id: string, params: T, key?: string): VizBuilder;
129
240
  build(): VizScene;
130
- svg(): string;
241
+ svg(opts?: SvgExportOptions): string;
131
242
  }
132
243
  interface EdgeBuilder {
133
244
  straight(): EdgeBuilder;
@@ -136,6 +247,12 @@ interface EdgeBuilder {
136
247
  routing(mode: EdgeRouting): EdgeBuilder;
137
248
  via(x: number, y: number): EdgeBuilder;
138
249
  label(text: string, opts?: Partial<EdgeLabel>): EdgeBuilder;
250
+ /**
251
+ * Create a rich text label (mixed formatting) using nested SVG <tspan> spans.
252
+ *
253
+ * Note: Rich labels currently support explicit newlines via `l.newline()`.
254
+ */
255
+ richLabel(cb: (l: RichLabelBuilder) => unknown, opts?: Partial<Omit<EdgeLabel, 'text' | 'rich'>>): EdgeBuilder;
139
256
  /**
140
257
  * Set arrow markers. Convenience method.
141
258
  * - `arrow(true)` or `arrow()` sets markerEnd to 'arrow'
@@ -172,8 +289,20 @@ interface EdgeBuilder {
172
289
  animate(cb: (anim: AnimationBuilder) => unknown): EdgeBuilder;
173
290
  /** Sugar for `animate(a => a.to(...))`. */
174
291
  animateTo(props: AnimatableProps, opts: TweenOptions): EdgeBuilder;
292
+ /** Attach arbitrary consumer-defined metadata to the edge. */
293
+ meta(meta: Record<string, unknown>): EdgeBuilder;
175
294
  data(payload: unknown): EdgeBuilder;
176
295
  onClick(handler: (id: string, edge: VizEdge) => void): EdgeBuilder;
296
+ /**
297
+ * For self-loops: which side the loop exits from.
298
+ * @default 'top'
299
+ */
300
+ loopSide(side: 'top' | 'right' | 'bottom' | 'left'): EdgeBuilder;
301
+ /**
302
+ * For self-loops: how far the loop extends from the shape boundary.
303
+ * @default 30
304
+ */
305
+ loopSize(size: number): EdgeBuilder;
177
306
  done(): VizBuilder;
178
307
  node(id: string): NodeBuilder;
179
308
  node(id: string, opts: NodeOptions): VizBuilder;
@@ -183,7 +312,7 @@ interface EdgeBuilder {
183
312
  overlay<K extends OverlayId>(id: K, params: OverlayParams<K>, key?: string): VizBuilder;
184
313
  overlay<T>(id: string, params: T, key?: string): VizBuilder;
185
314
  build(): VizScene;
186
- svg(): string;
315
+ svg(opts?: SvgExportOptions): string;
187
316
  }
188
317
  export declare function viz(): VizBuilder;
189
318
  export {};