@pixerate/editor-svelte 0.119.1 → 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 +1 -1
- package/dist/canvas/createCanvasExplosion.svelte.d.ts +7 -2
- package/dist/canvas/createCanvasExplosion.svelte.js +33 -76
- package/dist/canvas/createCanvasExplosionState.svelte.d.ts +17 -0
- package/dist/canvas/createCanvasExplosionState.svelte.js +44 -0
- package/dist/canvas/index.d.ts +2 -1
- package/dist/canvas/index.js +5 -3
- package/dist/canvas/types.d.ts +3 -22
- package/dist/chart/ChartDataGrid.svelte +15 -1
- package/dist/chart/ChartInspector.svelte +125 -0
- package/dist/chart/ChartTreeGrid.svelte +93 -0
- package/dist/chart/ChartTreeGrid.svelte.d.ts +10 -0
- package/dist/chart/index.d.ts +1 -0
- package/dist/chart/index.js +1 -0
- package/dist/chart/types.d.ts +1 -1
- package/package.json +2 -2
- package/dist/canvas/createCanvasDisplacement.d.ts +0 -31
- package/dist/canvas/createCanvasDisplacement.js +0 -170
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ Tables are on by default in `createRichTextPreset` (`table: false` turns them of
|
|
|
89
89
|
| `@pixerate/editor-svelte/parameters` | `ParametersPanel` (a whole schema with groups, presets, undo / reset / randomize and keyframe diamonds), `createParameters` (call `destroy()` when done), `useParameterCommands`, and the field controls `ParameterFieldControl`, `ParameterSlider`, `ParameterToggle`, `ParameterSelect`, `ParameterColor`, `ParameterVector`, `ParameterImage`, `ParameterText`, `KeyframeDiamond` over `@pixerate/editor/parameters` (see [the parameters guide](../../docs/parameters.md)) |
|
|
90
90
|
| `@pixerate/editor-svelte/color` | `ColorPicker` (interactive hex/rgb/hsl/oklch picker with alpha, swatches, eyedropper, recent history), `DualColorPicker` (dual light/dark mode color picker with theme preview and switch), `createColorScheme` / `resolvedColor` (the active scheme from the system, a root attribute or an override) |
|
|
91
91
|
| `@pixerate/editor-svelte/gradient` | `GradientEditor` (interactive stops bar, midpoints, presets, angle dial, type selector, OKLab/OKLCH/sRGB/linear-sRGB interpolation, CSS output), `FillPicker` (controlled solid-or-gradient fill picker), `createGradientController` |
|
|
92
|
-
| `@pixerate/editor-svelte/canvas` | Runes for `@xyflow/svelte`: `createCanvasGraph`, `createCanvasClipboard`, `createCanvasDocking`, `createCanvasShortcuts`, `createCanvasInteractions`, `createCanvasExplosion`, `createCanvasNodeSync`, `createCanvasMultiDrag`, `createStreamingCanvas`, `AgentPresenceLayer`, `FloatingHorizontalScrollbar`, plus layout helpers |
|
|
92
|
+
| `@pixerate/editor-svelte/canvas` | Runes for `@xyflow/svelte`: `createCanvasGraph`, `createCanvasClipboard`, `createCanvasDocking`, `createCanvasShortcuts`, `createCanvasInteractions`, `createCanvasExplosion`, `createCanvasExplosionState`, `createCanvasNodeSync`, `createCanvasMultiDrag`, `createStreamingCanvas`, `AgentPresenceLayer`, `FloatingHorizontalScrollbar`, plus layout helpers |
|
|
93
93
|
|
|
94
94
|
Spreadsheet persistence: `createReactiveSpreadsheet` exposes `document` (and passes it to `onDocumentChange`) with the defined names its formulas use in `document.names`, so saving that document and passing it back as `document` restores workbook, sheet-scoped and LAMBDA names. For several sheets, save `controller.getWorkbook()` instead.
|
|
95
95
|
|
|
@@ -17,8 +17,13 @@ export interface CreateCanvasExplosionOptions<TNode extends CanvasNode = CanvasN
|
|
|
17
17
|
onSave?: () => void;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* Imperative explosion coordinator for a mutable graph (whiteboards, diagrams): adds child
|
|
21
|
+
* nodes and edges, animates them out along a trajectory while surrounding nodes slide aside,
|
|
22
|
+
* and collapses back exactly. The layout math is `computeCanvasExplosionLayout` from
|
|
23
|
+
* `@pixerate/editor/canvas`; several nodes can be exploded at once. For a canvas derived from
|
|
24
|
+
* state (`$derived`), use `createCanvasExplosionState` and the layout functions instead.
|
|
25
|
+
*
|
|
26
|
+
* `explodeNode` throws when a child id is already on the canvas or repeated.
|
|
22
27
|
*/
|
|
23
28
|
export declare function createCanvasExplosion<TNode extends CanvasNode = CanvasNode, TEdge extends CanvasEdge = CanvasEdge>(graph: CanvasGraphTarget<TNode, TEdge>, options?: CreateCanvasExplosionOptions<TNode>): {
|
|
24
29
|
readonly explodedNodeIds: string[];
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { cubicOut } from 'svelte/easing';
|
|
2
|
-
import {
|
|
2
|
+
import { computeCanvasExplosionLayout } from '@pixerate/editor/canvas';
|
|
3
3
|
import { createFanOutTrajectory, linearPositionTrajectory, runMultiNodeTransition } from './createCanvasTrajectory.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Imperative explosion coordinator for a mutable graph (whiteboards, diagrams): adds child
|
|
6
|
+
* nodes and edges, animates them out along a trajectory while surrounding nodes slide aside,
|
|
7
|
+
* and collapses back exactly. The layout math is `computeCanvasExplosionLayout` from
|
|
8
|
+
* `@pixerate/editor/canvas`; several nodes can be exploded at once. For a canvas derived from
|
|
9
|
+
* state (`$derived`), use `createCanvasExplosionState` and the layout functions instead.
|
|
10
|
+
*
|
|
11
|
+
* `explodeNode` throws when a child id is already on the canvas or repeated.
|
|
7
12
|
*/
|
|
8
13
|
export function createCanvasExplosion(graph, options = {}) {
|
|
9
14
|
let explodedNodeIds = $state([]);
|
|
@@ -36,98 +41,50 @@ export function createCanvasExplosion(graph, options = {}) {
|
|
|
36
41
|
return explodedNodeIds.includes(nodeId);
|
|
37
42
|
}
|
|
38
43
|
function explodeNode(parentId, explodeOptions) {
|
|
39
|
-
|
|
44
|
+
// Already exploded, or its explosion is still animating
|
|
45
|
+
if (isExploded(parentId) || snapshots.has(parentId)) {
|
|
40
46
|
return false;
|
|
41
47
|
}
|
|
42
48
|
const parentNode = graph.nodes.find((n) => n.id === parentId);
|
|
43
49
|
if (!parentNode) {
|
|
44
50
|
return false;
|
|
45
51
|
}
|
|
46
|
-
const { childNodes, childEdges = [], connectParentToChildren = 'all', displacementStrategy = 'directional', directionalOptions = {}, reflowOptions = {}, trajectory, duration = 500, staggerDelay = 20, easing = cubicOut, onComplete } = explodeOptions;
|
|
47
|
-
if (
|
|
52
|
+
const { childNodes: requestedChildren, childEdges = [], connectParentToChildren = 'all', displacementStrategy = 'directional', directionalOptions = {}, reflowOptions = {}, trajectory, duration = 500, staggerDelay = 20, easing = cubicOut, onComplete } = explodeOptions;
|
|
53
|
+
if (requestedChildren.length === 0) {
|
|
48
54
|
return false;
|
|
49
55
|
}
|
|
50
|
-
// 1.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
else if (displacementStrategy === 'none') {
|
|
67
|
-
displacementMap = new Map();
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
displacementMap = calculateDirectionalDisplacement({
|
|
71
|
-
existingNodes: graph.nodes,
|
|
72
|
-
originNode: parentNode,
|
|
73
|
-
childNodes,
|
|
74
|
-
getNodesBounds: options.getNodesBounds,
|
|
75
|
-
options: directionalOptions
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
// 2. Build snapshot of displaced nodes before mutating positions
|
|
79
|
-
const displacedNodesSnapshot = Array.from(displacementMap.entries()).map(([id]) => {
|
|
80
|
-
const node = graph.nodes.find((n) => n.id === id);
|
|
81
|
-
return {
|
|
82
|
-
id,
|
|
83
|
-
originalPosition: { ...(node?.position ?? { x: 0, y: 0 }) }
|
|
84
|
-
};
|
|
56
|
+
// 1-3. Pure layout: displacement of existing nodes, child copies and the edges to add
|
|
57
|
+
// Loose type parameter: SvelteFlow `class` is ClassValue, core `CanvasNode.class` is a string (see GOTCHAS).
|
|
58
|
+
const layout = computeCanvasExplosionLayout({
|
|
59
|
+
existingNodes: graph.nodes,
|
|
60
|
+
existingEdges: graph.edges,
|
|
61
|
+
originNode: parentNode,
|
|
62
|
+
childNodes: requestedChildren,
|
|
63
|
+
childEdges,
|
|
64
|
+
strategy: displacementStrategy,
|
|
65
|
+
options: directionalOptions,
|
|
66
|
+
reflowOptions,
|
|
67
|
+
connectParentToChildren,
|
|
68
|
+
createEdge: graph.createEdge ? (source, target) => graph.createEdge(source, target) : undefined,
|
|
69
|
+
getNodesBounds: options.getNodesBounds
|
|
85
70
|
});
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
if (graph.createEdge) {
|
|
90
|
-
return graph.createEdge(sourceId, targetId);
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
id: crypto.randomUUID(),
|
|
94
|
-
type: 'default',
|
|
95
|
-
source: sourceId,
|
|
96
|
-
target: targetId
|
|
97
|
-
};
|
|
98
|
-
};
|
|
99
|
-
if (connectParentToChildren === 'all' || connectParentToChildren === true) {
|
|
100
|
-
childNodes.forEach((child) => {
|
|
101
|
-
const exists = edgesToAdd.some((e) => e.source === parentId && e.target === child.id);
|
|
102
|
-
if (!exists) {
|
|
103
|
-
edgesToAdd.push(defaultCreateEdge(parentId, child.id));
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
else if (connectParentToChildren === 'first' && childNodes.length > 0) {
|
|
108
|
-
const firstChild = childNodes[0];
|
|
109
|
-
const exists = edgesToAdd.some((e) => e.source === parentId && e.target === firstChild.id);
|
|
110
|
-
if (!exists) {
|
|
111
|
-
edgesToAdd.push(defaultCreateEdge(parentId, firstChild.id));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
71
|
+
const displacementMap = layout.displacedPositions;
|
|
72
|
+
const childNodes = layout.childNodes;
|
|
73
|
+
const edgesToAdd = layout.childEdges.map((edge) => ({ ...edge }));
|
|
114
74
|
const snapshot = {
|
|
115
75
|
parentId,
|
|
116
76
|
childNodeIds: childNodes.map((n) => n.id),
|
|
117
77
|
childEdgeIds: edgesToAdd.map((e) => e.id),
|
|
118
|
-
displacedNodes:
|
|
78
|
+
displacedNodes: [...layout.originalPositions].map(([id, originalPosition]) => ({ id, originalPosition })),
|
|
119
79
|
timestamp: Date.now()
|
|
120
80
|
};
|
|
121
81
|
snapshots.set(parentId, snapshot);
|
|
122
|
-
// 4. Position child nodes
|
|
82
|
+
// 4. Position child nodes (copies, so the caller's objects stay untouched) at the origin
|
|
123
83
|
const originPos = { ...parentNode.position };
|
|
124
84
|
const targetPositions = childNodes.map((child) => ({ ...child.position }));
|
|
125
85
|
childNodes.forEach((child) => {
|
|
126
86
|
child.position = { ...originPos };
|
|
127
|
-
|
|
128
|
-
child.data = {};
|
|
129
|
-
child.data.scale = 0;
|
|
130
|
-
child.data.opacity = 0;
|
|
87
|
+
child.data = { ...(child.data ?? {}), scale: 0, opacity: 0 };
|
|
131
88
|
const currentStyle = (child.style || '').replace(/opacity:\s*[^;]+;?/g, '').trim();
|
|
132
89
|
child.style = `${currentStyle ? currentStyle + '; ' : ''}opacity: 0;`;
|
|
133
90
|
});
|
|
@@ -330,7 +287,7 @@ export function createCanvasExplosion(graph, options = {}) {
|
|
|
330
287
|
return true;
|
|
331
288
|
}
|
|
332
289
|
function toggleExplode(parentId, explodeOptions, collapseOptions) {
|
|
333
|
-
if (
|
|
290
|
+
if (snapshots.has(parentId)) {
|
|
334
291
|
return collapseNode(parentId, collapseOptions);
|
|
335
292
|
}
|
|
336
293
|
return explodeNode(parentId, explodeOptions);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CanvasExplosionState, type CanvasExplosionStateController, type CanvasExplosionStateOptions } from '@pixerate/editor/canvas';
|
|
2
|
+
export type { CanvasExplosionState, CanvasExplosionStateOptions };
|
|
3
|
+
export interface CanvasExplosionStateRune extends CanvasExplosionState {
|
|
4
|
+
/** The framework-free controller underneath (for `canvasExplosionToolTarget` and agent tools). */
|
|
5
|
+
readonly controller: CanvasExplosionStateController;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Headless, declarative explosion state as a Svelte 5 rune: which node is exploded
|
|
9
|
+
* (`activeId`), which is still collapsing (`collapsingId`, so its children can play a collapse
|
|
10
|
+
* animation before unmounting), and the collapse timer. It never touches a graph: derive the
|
|
11
|
+
* canvas from it with `$derived` and `computeCanvasExplosionLayout` /
|
|
12
|
+
* `calculateDirectionalDisplacement`.
|
|
13
|
+
*
|
|
14
|
+
* Created during component initialisation it cleans up its timer when the component unmounts;
|
|
15
|
+
* created elsewhere, call `destroy()` yourself.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createCanvasExplosionState(options?: CanvasExplosionStateOptions): CanvasExplosionStateRune;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createCanvasExplosionStateController } from '@pixerate/editor/canvas';
|
|
2
|
+
/**
|
|
3
|
+
* Headless, declarative explosion state as a Svelte 5 rune: which node is exploded
|
|
4
|
+
* (`activeId`), which is still collapsing (`collapsingId`, so its children can play a collapse
|
|
5
|
+
* animation before unmounting), and the collapse timer. It never touches a graph: derive the
|
|
6
|
+
* canvas from it with `$derived` and `computeCanvasExplosionLayout` /
|
|
7
|
+
* `calculateDirectionalDisplacement`.
|
|
8
|
+
*
|
|
9
|
+
* Created during component initialisation it cleans up its timer when the component unmounts;
|
|
10
|
+
* created elsewhere, call `destroy()` yourself.
|
|
11
|
+
*/
|
|
12
|
+
export function createCanvasExplosionState(options = {}) {
|
|
13
|
+
const controller = createCanvasExplosionStateController(options);
|
|
14
|
+
let snapshot = $state(controller.getSnapshot());
|
|
15
|
+
controller.subscribe(() => {
|
|
16
|
+
snapshot = controller.getSnapshot();
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
$effect(() => () => controller.destroy());
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Not inside a component or effect: the caller owns destroy().
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
controller,
|
|
26
|
+
get activeId() {
|
|
27
|
+
return snapshot.activeId;
|
|
28
|
+
},
|
|
29
|
+
get collapsingId() {
|
|
30
|
+
return snapshot.collapsingId;
|
|
31
|
+
},
|
|
32
|
+
get isExplodingOrCollapsing() {
|
|
33
|
+
return snapshot.activeId !== null || snapshot.collapsingId !== null;
|
|
34
|
+
},
|
|
35
|
+
isExploded: (nodeId) => snapshot.activeId === nodeId,
|
|
36
|
+
isCollapsing: (nodeId) => snapshot.collapsingId === nodeId,
|
|
37
|
+
shouldRenderChildren: (nodeId) => snapshot.activeId === nodeId || snapshot.collapsingId === nodeId,
|
|
38
|
+
explode: (nodeId) => controller.explode(nodeId),
|
|
39
|
+
collapse: (nodeId) => controller.collapse(nodeId),
|
|
40
|
+
toggle: (nodeId) => controller.toggle(nodeId),
|
|
41
|
+
finishCollapse: () => controller.finishCollapse(),
|
|
42
|
+
destroy: () => controller.destroy()
|
|
43
|
+
};
|
|
44
|
+
}
|
package/dist/canvas/index.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export { createCanvasDocking, type CreateCanvasDockingOptions, type DockStrategy
|
|
|
6
6
|
export { createCanvasShortcuts } from './createCanvasShortcuts.js';
|
|
7
7
|
export { createCanvasInteractions, defaultSvelteFlowPreset, miroCompatiblePreset, restorePanelPointerEvents, isolateCanvasGestures, type CreateCanvasInteractionsOptions } from './createCanvasInteractions.svelte.js';
|
|
8
8
|
export { linearTrajectory, linearPositionTrajectory, createFanOutTrajectory, createBezierTrajectory, runMultiNodeTransition } from './createCanvasTrajectory.js';
|
|
9
|
-
export { computeNodesBoundingBox, calculateDirectionalDisplacement, calculateReflowDisplacement } from '
|
|
9
|
+
export { computeNodesBoundingBox, nodeSizeOf, calculateDirectionalDisplacement, calculateReflowDisplacement, calculateDagreReflowDisplacement, computeCanvasExplosionLayout, explosionEdgeId, createCanvasExplosionStateController, createCanvasExplosionToolProvider, canvasExplosionToolTarget, type ComputeCanvasExplosionLayoutParams, type CanvasExplosionLayoutResult, type CanvasExplosionStateController, type CanvasExplosionStateSnapshot, type CanvasExplosionToolTarget, type CanvasExplosionToolProviderOptions } from '@pixerate/editor/canvas';
|
|
10
|
+
export { createCanvasExplosionState, type CanvasExplosionState, type CanvasExplosionStateOptions, type CanvasExplosionStateRune } from './createCanvasExplosionState.svelte.js';
|
|
10
11
|
export { createCanvasExplosion, type CanvasGraphTarget, type CreateCanvasExplosionOptions } from './createCanvasExplosion.svelte.js';
|
|
11
12
|
export { createCanvasNodeSync, preserveNodeMeasurements, normalizeNodeHandles, snapshotCanvasGraph, type HandleConfig, type NodeMeasurement, type CreateCanvasNodeSyncOptions, type CanvasGraphSnapshotInput } from './createCanvasNodeSync.svelte.js';
|
|
12
13
|
export { createCanvasMultiDrag, type MultiDragState, type MultiDragFollowerOffset, type CreateCanvasMultiDragOptions, type FollowerTransform } from './createCanvasMultiDrag.svelte.js';
|
package/dist/canvas/index.js
CHANGED
|
@@ -14,9 +14,11 @@ export { createCanvasShortcuts } from './createCanvasShortcuts.js';
|
|
|
14
14
|
export { createCanvasInteractions, defaultSvelteFlowPreset, miroCompatiblePreset, restorePanelPointerEvents, isolateCanvasGestures } from './createCanvasInteractions.svelte.js';
|
|
15
15
|
// Trajectory calculation & multi-node transitions
|
|
16
16
|
export { linearTrajectory, linearPositionTrajectory, createFanOutTrajectory, createBezierTrajectory, runMultiNodeTransition } from './createCanvasTrajectory.js';
|
|
17
|
-
// Spatial layout displacement
|
|
18
|
-
export { computeNodesBoundingBox, calculateDirectionalDisplacement, calculateReflowDisplacement } from '
|
|
19
|
-
//
|
|
17
|
+
// Spatial layout displacement & pure explosion layout (core)
|
|
18
|
+
export { computeNodesBoundingBox, nodeSizeOf, calculateDirectionalDisplacement, calculateReflowDisplacement, calculateDagreReflowDisplacement, computeCanvasExplosionLayout, explosionEdgeId, createCanvasExplosionStateController, createCanvasExplosionToolProvider, canvasExplosionToolTarget } from '@pixerate/editor/canvas';
|
|
19
|
+
// Declarative explosion state rune
|
|
20
|
+
export { createCanvasExplosionState } from './createCanvasExplosionState.svelte.js';
|
|
21
|
+
// Imperative explosion coordinator rune
|
|
20
22
|
export { createCanvasExplosion } from './createCanvasExplosion.svelte.js';
|
|
21
23
|
// Node synchronization & handle normalization
|
|
22
24
|
export { createCanvasNodeSync, preserveNodeMeasurements, normalizeNodeHandles, snapshotCanvasGraph } from './createCanvasNodeSync.svelte.js';
|
package/dist/canvas/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Node, Edge, XYPosition, ColorMode } from '@xyflow/svelte';
|
|
2
|
+
import type { DirectionalDisplacementOptions, ExplosionDisplacementStrategy, ReflowDisplacementOptions } from '@pixerate/editor/canvas';
|
|
2
3
|
/**
|
|
3
4
|
* Generic node interface representing an element on the canvas.
|
|
4
5
|
*/
|
|
@@ -120,26 +121,7 @@ export interface NodeTransition<TNode extends CanvasNode = CanvasNode> {
|
|
|
120
121
|
index?: number;
|
|
121
122
|
total?: number;
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
-
* Displacement axis / direction.
|
|
125
|
-
*/
|
|
126
|
-
export type DisplacementDirection = 'right' | 'left' | 'down' | 'up' | 'vertical';
|
|
127
|
-
/**
|
|
128
|
-
* Options for directional displacement calculation.
|
|
129
|
-
*/
|
|
130
|
-
export interface DirectionalDisplacementOptions {
|
|
131
|
-
direction?: DisplacementDirection;
|
|
132
|
-
gap?: number;
|
|
133
|
-
spreadSiblingLanes?: boolean;
|
|
134
|
-
siblingTolerance?: number;
|
|
135
|
-
minY?: number;
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Options for Dagre reflow displacement calculation.
|
|
139
|
-
*/
|
|
140
|
-
export interface ReflowDisplacementOptions extends CanvasLayoutOptions {
|
|
141
|
-
anchorNodeId: string;
|
|
142
|
-
}
|
|
124
|
+
export type { DisplacementDirection, DirectionalDisplacementOptions, ReflowDisplacementOptions, ExplosionDisplacementStrategy } from '@pixerate/editor/canvas';
|
|
143
125
|
/**
|
|
144
126
|
* Snapshot of canvas graph state prior to node explosion, enabling exact reversibility.
|
|
145
127
|
*/
|
|
@@ -160,8 +142,7 @@ export interface ExplodeNodeOptions<TNode extends CanvasNode = CanvasNode, TEdge
|
|
|
160
142
|
childNodes: TNode[];
|
|
161
143
|
childEdges?: TEdge[];
|
|
162
144
|
connectParentToChildren?: boolean | 'first' | 'all';
|
|
163
|
-
|
|
164
|
-
displacementStrategy?: 'directional' | 'dagre' | 'none';
|
|
145
|
+
displacementStrategy?: ExplosionDisplacementStrategy;
|
|
165
146
|
directionalOptions?: DirectionalDisplacementOptions;
|
|
166
147
|
reflowOptions?: Partial<ReflowDisplacementOptions>;
|
|
167
148
|
trajectory?: TrajectoryFunction;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { chartColorInputValue, chartSeriesColor, formatChartNumber, type ChartController } from '@pixerate/editor/chart';
|
|
2
|
+
import { chartColorInputValue, chartSeriesColor, chartTreeFromSeries, formatChartNumber, type ChartController } from '@pixerate/editor/chart';
|
|
3
3
|
import ChartCommitInput from './ChartCommitInput.svelte';
|
|
4
|
+
import ChartTreeGrid from './ChartTreeGrid.svelte';
|
|
4
5
|
import { chartStateOf } from './createChartEditor.svelte.js';
|
|
5
6
|
import { chartButtonClass, chartCellClass, chartIconButtonClass, chartInputClass } from './styles.js';
|
|
6
7
|
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
const view = chartStateOf(() => controller);
|
|
19
20
|
const spec = $derived(view.current.spec);
|
|
20
21
|
const selection = $derived(view.current.selection);
|
|
22
|
+
const canGroup = $derived(spec.type === 'treemap' || spec.type === 'sunburst');
|
|
21
23
|
|
|
22
24
|
const cellText = (value: number | null | undefined) => (value === null || value === undefined ? '' : String(value));
|
|
23
25
|
|
|
@@ -30,6 +32,9 @@
|
|
|
30
32
|
};
|
|
31
33
|
</script>
|
|
32
34
|
|
|
35
|
+
{#if spec.tree.length}
|
|
36
|
+
<ChartTreeGrid {controller} {readOnly} class={className} />
|
|
37
|
+
{:else}
|
|
33
38
|
<div class="overflow-auto rounded-md border border-border {className}" data-chart-grid="">
|
|
34
39
|
<table class="w-full border-collapse text-xs">
|
|
35
40
|
<thead class="bg-muted/50">
|
|
@@ -121,6 +126,15 @@
|
|
|
121
126
|
{#if !readOnly}
|
|
122
127
|
<div class="p-1">
|
|
123
128
|
<button type="button" class="{chartButtonClass} h-6 border-dashed" data-action="add-category" onclick={() => controller.addCategory()}>+ Add category</button>
|
|
129
|
+
{#if canGroup}
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
class="{chartButtonClass} ml-1 h-6"
|
|
133
|
+
data-action="tree-from-table"
|
|
134
|
+
title="Edit the categories as groups of items"
|
|
135
|
+
onclick={() => controller.setTree(chartTreeFromSeries(spec.categories, spec.series.find((s) => !s.hidden)?.values ?? []))}>Group items</button>
|
|
136
|
+
{/if}
|
|
124
137
|
</div>
|
|
125
138
|
{/if}
|
|
126
139
|
</div>
|
|
140
|
+
{/if}
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
CHART_LABEL_POSITIONS,
|
|
19
19
|
CHART_LAYOUT_PRESETS,
|
|
20
20
|
CHART_LEGEND_POSITIONS,
|
|
21
|
+
CHART_LIKERT_NEUTRALS,
|
|
22
|
+
CHART_LIKERT_SORTS,
|
|
21
23
|
CHART_MARKERS,
|
|
22
24
|
CHART_NUMBER_STYLES,
|
|
23
25
|
CHART_SERIES_TYPES,
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
defaultChartErrorBarValue,
|
|
37
39
|
formatChartAmountList,
|
|
38
40
|
isCartesianChart,
|
|
41
|
+
suggestChartReferences,
|
|
39
42
|
parseChartAmountList,
|
|
40
43
|
type ChartAxis,
|
|
41
44
|
type ChartController,
|
|
@@ -66,6 +69,7 @@
|
|
|
66
69
|
const view = chartStateOf(() => controller);
|
|
67
70
|
const spec = $derived(view.current.spec);
|
|
68
71
|
const selectedSeries = $derived(view.current.selectedSeries);
|
|
72
|
+
const selection = $derived(view.current.selection);
|
|
69
73
|
const show = (section: ChartInspectorSection) => !sections || sections.includes(section);
|
|
70
74
|
const cartesian = $derived(isCartesianChart(spec.type));
|
|
71
75
|
const family = $derived(chartFamily(spec));
|
|
@@ -80,6 +84,10 @@
|
|
|
80
84
|
const horizontal = $derived(spec.orientation === 'horizontal');
|
|
81
85
|
// Log and reversed axes and error bars are drawn by the bar / line / area / scatter layouts.
|
|
82
86
|
const scales = $derived(family === 'cartesian' || family === 'xy');
|
|
87
|
+
// Reference lines and bands, callouts and colour by category are drawn by the same layouts.
|
|
88
|
+
const annotated = $derived(scales);
|
|
89
|
+
const oneSeries = $derived(cartesian && spec.series.filter((s) => !s.hidden).length === 1);
|
|
90
|
+
const calloutIndex = $derived(selection?.kind === 'point' && selection.seriesId === selectedSeries?.id ? selection.index : null);
|
|
83
91
|
const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
84
92
|
const stackingLabels: Record<string, string> = { none: 'None', stacked: 'Stacked', percent: '100%' };
|
|
85
93
|
|
|
@@ -332,6 +340,12 @@
|
|
|
332
340
|
<ChartCommitInput id="{idPrefix}-subtitle" value={spec.subtitle} data-option="subtitle" oncommit={(subtitle) => void controller.setSubtitle(subtitle)} />
|
|
333
341
|
</div>
|
|
334
342
|
</div>
|
|
343
|
+
<div class="flex items-center gap-2">
|
|
344
|
+
<label for="{idPrefix}-note" class="{chartLabelClass} w-20 shrink-0">Note</label>
|
|
345
|
+
<div class="min-w-0 flex-1">
|
|
346
|
+
<ChartCommitInput id="{idPrefix}-note" value={spec.note} placeholder="Source or footnote" data-option="note" oncommit={(note) => void controller.setNote(note)} />
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
335
349
|
</div>
|
|
336
350
|
{/if}
|
|
337
351
|
|
|
@@ -409,6 +423,62 @@
|
|
|
409
423
|
</div>
|
|
410
424
|
</div>
|
|
411
425
|
{/if}
|
|
426
|
+
{#if spec.type === 'sunburst'}
|
|
427
|
+
<div class="flex items-center gap-2">
|
|
428
|
+
<label for="{idPrefix}-sunburst-label" class="{chartLabelClass} w-20 shrink-0">Caption</label>
|
|
429
|
+
<div class="min-w-0 flex-1">
|
|
430
|
+
<ChartCommitInput id="{idPrefix}-sunburst-label" value={spec.sunburstLabel} placeholder="Under the total" data-option="sunburstLabel" oncommit={(sunburstLabel) => controller.update({ sunburstLabel }).ok} />
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
{/if}
|
|
434
|
+
{#if spec.type === 'likert'}
|
|
435
|
+
<div class="flex items-center gap-2">
|
|
436
|
+
<label for="{idPrefix}-likert-neutral" class="{chartLabelClass} w-20 shrink-0">Neutral</label>
|
|
437
|
+
<select id="{idPrefix}-likert-neutral" class={chartInputClass} value={spec.likertNeutral} data-option="likertNeutral" onchange={(e) => controller.update({ likertNeutral: e.currentTarget.value as (typeof CHART_LIKERT_NEUTRALS)[number] })}>
|
|
438
|
+
{#each CHART_LIKERT_NEUTRALS as n (n)}
|
|
439
|
+
<option value={n}>{n === 'split' ? 'Across the centre' : 'In its own column'}</option>
|
|
440
|
+
{/each}
|
|
441
|
+
</select>
|
|
442
|
+
</div>
|
|
443
|
+
<div class="flex items-center gap-2">
|
|
444
|
+
<label for="{idPrefix}-likert-sort" class="{chartLabelClass} w-20 shrink-0">Order</label>
|
|
445
|
+
<select id="{idPrefix}-likert-sort" class={chartInputClass} value={spec.likertSort} data-option="likertSort" onchange={(e) => controller.update({ likertSort: e.currentTarget.value as (typeof CHART_LIKERT_SORTS)[number] })}>
|
|
446
|
+
{#each CHART_LIKERT_SORTS as o (o)}
|
|
447
|
+
<option value={o}>{o === 'none' ? 'As entered' : o === 'positive' ? 'Most agreement first' : 'Most disagreement first'}</option>
|
|
448
|
+
{/each}
|
|
449
|
+
</select>
|
|
450
|
+
</div>
|
|
451
|
+
{/if}
|
|
452
|
+
{#if spec.type === 'parliament'}
|
|
453
|
+
<div class="flex items-center gap-2">
|
|
454
|
+
<label for="{idPrefix}-parliament-arc" class="{chartLabelClass} w-20 shrink-0">Sweep</label>
|
|
455
|
+
<input
|
|
456
|
+
id="{idPrefix}-parliament-arc"
|
|
457
|
+
type="range"
|
|
458
|
+
min="90"
|
|
459
|
+
max="360"
|
|
460
|
+
step="10"
|
|
461
|
+
value={spec.parliamentArc}
|
|
462
|
+
data-option="parliamentArc"
|
|
463
|
+
class="w-full accent-primary"
|
|
464
|
+
onpointerdown={() => controller.beginGesture('parliamentArc')}
|
|
465
|
+
onpointerup={() => controller.endGesture()}
|
|
466
|
+
oninput={(e) => controller.update({ parliamentArc: Number(e.currentTarget.value) })}
|
|
467
|
+
/>
|
|
468
|
+
</div>
|
|
469
|
+
<div class="flex items-center gap-2">
|
|
470
|
+
<label for="{idPrefix}-parliament-majority" class="{chartLabelClass} w-20 shrink-0">Majority</label>
|
|
471
|
+
<div class="min-w-0 flex-1">
|
|
472
|
+
<ChartCommitInput id="{idPrefix}-parliament-majority" value={spec.parliamentMajority === null ? '' : String(spec.parliamentMajority)} placeholder="Half plus one" inputmode="numeric" data-option="parliamentMajority" oncommit={(text) => controller.update({ parliamentMajority: text.trim() === '' ? null : Number(text) }).ok} />
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
<div class="flex items-center gap-2">
|
|
476
|
+
<label for="{idPrefix}-parliament-label" class="{chartLabelClass} w-20 shrink-0">Caption</label>
|
|
477
|
+
<div class="min-w-0 flex-1">
|
|
478
|
+
<ChartCommitInput id="{idPrefix}-parliament-label" value={spec.parliamentLabel} placeholder="seats" data-option="parliamentLabel" oncommit={(parliamentLabel) => controller.update({ parliamentLabel }).ok} />
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
{/if}
|
|
412
482
|
{#if spec.type === 'radar'}
|
|
413
483
|
<div class="flex items-center gap-2">
|
|
414
484
|
<label for="{idPrefix}-radar-ticks" class="{chartLabelClass} w-20 shrink-0">Rings</label>
|
|
@@ -450,6 +520,9 @@
|
|
|
450
520
|
{#if hasLines}
|
|
451
521
|
{@render toggle('Points', 'points', spec.points, (points) => controller.update({ points }))}
|
|
452
522
|
{/if}
|
|
523
|
+
{#if oneSeries}
|
|
524
|
+
{@render toggle('Colour by category', 'varyColors', spec.varyColors, (varyColors) => controller.update({ varyColors }))}
|
|
525
|
+
{/if}
|
|
453
526
|
{@render toggle('Data labels', 'dataLabels', spec.dataLabels, (dataLabels) => controller.update({ dataLabels }))}
|
|
454
527
|
</div>
|
|
455
528
|
</div>
|
|
@@ -483,6 +556,43 @@
|
|
|
483
556
|
{/if}
|
|
484
557
|
{/if}
|
|
485
558
|
|
|
559
|
+
{#if show('annotations') && annotated}
|
|
560
|
+
<div class={chartSectionClass} data-section="annotations">
|
|
561
|
+
<h4 class="text-xs font-semibold text-foreground">Annotations</h4>
|
|
562
|
+
<div class="flex items-center gap-2">
|
|
563
|
+
<label for="{idPrefix}-highlight" class="{chartLabelClass} w-20 shrink-0">Highlight</label>
|
|
564
|
+
<div class="min-w-0 flex-1">
|
|
565
|
+
<ChartCommitInput
|
|
566
|
+
id="{idPrefix}-highlight"
|
|
567
|
+
value={spec.highlight.join(', ')}
|
|
568
|
+
placeholder="Categories or series"
|
|
569
|
+
data-option="highlight"
|
|
570
|
+
oncommit={(text) => controller.setHighlight(text.split(',').map((n) => n.trim()).filter(Boolean)).ok}
|
|
571
|
+
/>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
{#each spec.referenceLines as line, i (i)}
|
|
575
|
+
<div class="flex items-center gap-1" data-reference-line={i}>
|
|
576
|
+
<ChartCommitInput value={String(line.value)} aria-label="Line value" inputmode="decimal" data-option="referenceLine.{i}.value" class="{chartInputClass} w-16 shrink-0" oncommit={(text) => controller.updateReferenceLine(i, { value: Number(text) }).ok} />
|
|
577
|
+
<ChartCommitInput value={line.label ?? ''} aria-label="Line label" placeholder="Label" data-option="referenceLine.{i}.label" class="{chartInputClass} flex-1" oncommit={(text) => controller.updateReferenceLine(i, { label: text || null }).ok} />
|
|
578
|
+
<button type="button" class={chartButtonClass} aria-label="Delete reference line" data-action="reference-line-delete" onclick={() => controller.removeReferenceLine(i)}>×</button>
|
|
579
|
+
</div>
|
|
580
|
+
{/each}
|
|
581
|
+
{#each spec.referenceBands as band, i (i)}
|
|
582
|
+
<div class="flex items-center gap-1" data-reference-band={i}>
|
|
583
|
+
<ChartCommitInput value={String(band.from)} aria-label="Band from" inputmode="decimal" data-option="referenceBand.{i}.from" class="{chartInputClass} w-14 shrink-0" oncommit={(text) => controller.updateReferenceBand(i, { from: Number(text) }).ok} />
|
|
584
|
+
<ChartCommitInput value={String(band.to)} aria-label="Band to" inputmode="decimal" data-option="referenceBand.{i}.to" class="{chartInputClass} w-14 shrink-0" oncommit={(text) => controller.updateReferenceBand(i, { to: Number(text) }).ok} />
|
|
585
|
+
<ChartCommitInput value={band.label ?? ''} aria-label="Band label" placeholder="Label" data-option="referenceBand.{i}.label" class="{chartInputClass} flex-1" oncommit={(text) => controller.updateReferenceBand(i, { label: text || null }).ok} />
|
|
586
|
+
<button type="button" class={chartButtonClass} aria-label="Delete reference band" data-action="reference-band-delete" onclick={() => controller.removeReferenceBand(i)}>×</button>
|
|
587
|
+
</div>
|
|
588
|
+
{/each}
|
|
589
|
+
<div class="flex gap-1">
|
|
590
|
+
<button type="button" class={chartButtonClass} data-option="referenceLine.add" onclick={() => controller.addReferenceLine(suggestChartReferences(spec).line)}>Add line</button>
|
|
591
|
+
<button type="button" class={chartButtonClass} data-option="referenceBand.add" onclick={() => controller.addReferenceBand(suggestChartReferences(spec).band)}>Add band</button>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
{/if}
|
|
595
|
+
|
|
486
596
|
{#if show('series') && selectedSeries}
|
|
487
597
|
{@const series = selectedSeries}
|
|
488
598
|
{@const drawnAs = chartSeriesType(spec, series)}
|
|
@@ -546,6 +656,21 @@
|
|
|
546
656
|
</select>
|
|
547
657
|
</div>
|
|
548
658
|
{/if}
|
|
659
|
+
{#if annotated && calloutIndex !== null}
|
|
660
|
+
{@const at = calloutIndex}
|
|
661
|
+
<div class="flex items-center gap-2">
|
|
662
|
+
<label for="{idPrefix}-series-callout" class="{chartLabelClass} w-20 shrink-0">Callout</label>
|
|
663
|
+
<div class="min-w-0 flex-1">
|
|
664
|
+
<ChartCommitInput
|
|
665
|
+
id="{idPrefix}-series-callout"
|
|
666
|
+
value={series.callouts?.[at] ?? ''}
|
|
667
|
+
placeholder="Label for {spec.categories[at] ?? 'this point'}"
|
|
668
|
+
data-option="series.callout"
|
|
669
|
+
oncommit={(text) => controller.setCallout(series.id, at, text || null).ok}
|
|
670
|
+
/>
|
|
671
|
+
</div>
|
|
672
|
+
</div>
|
|
673
|
+
{/if}
|
|
549
674
|
{#if scales}
|
|
550
675
|
{@render errorBarFields(series, 'errorBars')}
|
|
551
676
|
{/if}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
addChartTreeRow,
|
|
4
|
+
chartTreeColumnLabels,
|
|
5
|
+
chartTreeDepth,
|
|
6
|
+
chartTreeToRows,
|
|
7
|
+
formatChartNumber,
|
|
8
|
+
removeChartTreeRow,
|
|
9
|
+
setChartTreeRowName,
|
|
10
|
+
setChartTreeRowValue,
|
|
11
|
+
type ChartController
|
|
12
|
+
} from '@pixerate/editor/chart';
|
|
13
|
+
import ChartCommitInput from './ChartCommitInput.svelte';
|
|
14
|
+
import { chartStateOf } from './createChartEditor.svelte.js';
|
|
15
|
+
import { chartButtonClass, chartCellClass, chartIconButtonClass } from './styles.js';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
controller: ChartController;
|
|
19
|
+
/** Hide the add / delete buttons. */
|
|
20
|
+
readOnly?: boolean;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let { controller, readOnly = false, class: className = '' }: Props = $props();
|
|
25
|
+
|
|
26
|
+
const view = chartStateOf(() => controller);
|
|
27
|
+
const spec = $derived(view.current.spec);
|
|
28
|
+
// `+ Level` raises the column count; it stays until the grid is unmounted, so a new column can be filled in.
|
|
29
|
+
let columns = $state(1);
|
|
30
|
+
const rows = $derived(chartTreeToRows(spec.tree));
|
|
31
|
+
const depth = $derived(Math.max(columns, chartTreeDepth(spec.tree)));
|
|
32
|
+
const labels = $derived(chartTreeColumnLabels(depth));
|
|
33
|
+
|
|
34
|
+
const commit = (next: Parameters<ChartController['setTreeRows']>[0]) => controller.setTreeRows(next).ok;
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<!-- Grouped data as a table: the names from the group down to each item, then its value. -->
|
|
38
|
+
<div class="overflow-auto rounded-md border border-border {className}" data-chart-grid="" data-chart-tree-grid="">
|
|
39
|
+
<table class="w-full border-collapse text-xs">
|
|
40
|
+
<thead class="bg-muted/50">
|
|
41
|
+
<tr>
|
|
42
|
+
{#each labels as label (label)}
|
|
43
|
+
<th class="border-b border-r border-border px-2 py-1 text-left font-medium text-muted-foreground">{label}</th>
|
|
44
|
+
{/each}
|
|
45
|
+
<th class="border-b border-r border-border px-2 py-1 text-right font-medium text-muted-foreground">Value</th>
|
|
46
|
+
{#if !readOnly}<th class="w-8 border-b border-border"></th>{/if}
|
|
47
|
+
</tr>
|
|
48
|
+
</thead>
|
|
49
|
+
<tbody>
|
|
50
|
+
{#each rows as row, r (r)}
|
|
51
|
+
<tr data-tree-row={r}>
|
|
52
|
+
{#each labels as label, k (k)}
|
|
53
|
+
<td class="border-b border-r border-border p-0">
|
|
54
|
+
<ChartCommitInput
|
|
55
|
+
value={row.path[k] ?? ''}
|
|
56
|
+
aria-label="{label}, row {r + 1}"
|
|
57
|
+
data-tree-cell="{r}:{k}"
|
|
58
|
+
readonly={readOnly}
|
|
59
|
+
oncommit={(name) => commit(setChartTreeRowName(rows, r, k, name))}
|
|
60
|
+
class="{chartCellClass} text-left"
|
|
61
|
+
/>
|
|
62
|
+
</td>
|
|
63
|
+
{/each}
|
|
64
|
+
<td class="border-b border-r border-border p-0">
|
|
65
|
+
<ChartCommitInput
|
|
66
|
+
value={String(row.value)}
|
|
67
|
+
inputmode="decimal"
|
|
68
|
+
aria-label="Value of {row.path.join(' / ')}"
|
|
69
|
+
title={formatChartNumber(row.value, spec.yAxis.format)}
|
|
70
|
+
data-tree-value={r}
|
|
71
|
+
readonly={readOnly}
|
|
72
|
+
oncommit={(text) => commit(setChartTreeRowValue(rows, r, text))}
|
|
73
|
+
class={chartCellClass}
|
|
74
|
+
/>
|
|
75
|
+
</td>
|
|
76
|
+
{#if !readOnly}
|
|
77
|
+
<td class="border-b border-border px-1">
|
|
78
|
+
<button type="button" class={chartIconButtonClass} title="Delete item" aria-label="Delete {row.path.join(' / ')}" data-action="tree-remove-row" onclick={() => commit(removeChartTreeRow(rows, r))}>×</button>
|
|
79
|
+
</td>
|
|
80
|
+
{/if}
|
|
81
|
+
</tr>
|
|
82
|
+
{/each}
|
|
83
|
+
</tbody>
|
|
84
|
+
</table>
|
|
85
|
+
{#if !readOnly}
|
|
86
|
+
<div class="flex flex-wrap gap-1 p-1">
|
|
87
|
+
<button type="button" class="{chartButtonClass} h-6 border-dashed" data-action="tree-add-row" onclick={() => commit(addChartTreeRow(rows))}>+ Add item</button>
|
|
88
|
+
<button type="button" class="{chartButtonClass} h-6 border-dashed" data-action="tree-add-level" disabled={depth >= 6} onclick={() => (columns = depth + 1)}>+ Level</button>
|
|
89
|
+
<span class="flex-1"></span>
|
|
90
|
+
<button type="button" class="{chartButtonClass} h-6" data-action="tree-ungroup" title="Keep the group totals as plain data" onclick={() => controller.setTree([])}>Ungroup</button>
|
|
91
|
+
</div>
|
|
92
|
+
{/if}
|
|
93
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ChartController } from '@pixerate/editor/chart';
|
|
2
|
+
interface Props {
|
|
3
|
+
controller: ChartController;
|
|
4
|
+
/** Hide the add / delete buttons. */
|
|
5
|
+
readOnly?: boolean;
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
declare const ChartTreeGrid: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type ChartTreeGrid = ReturnType<typeof ChartTreeGrid>;
|
|
10
|
+
export default ChartTreeGrid;
|
package/dist/chart/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type ChartSparklineComponent from "./ChartSparkline.svelte";
|
|
|
8
8
|
export { default as ChartEditor } from "./ChartEditor.svelte";
|
|
9
9
|
export { default as ChartCanvas } from "./ChartCanvas.svelte";
|
|
10
10
|
export { default as ChartDataGrid } from "./ChartDataGrid.svelte";
|
|
11
|
+
export { default as ChartTreeGrid } from "./ChartTreeGrid.svelte";
|
|
11
12
|
export { default as ChartInspector } from "./ChartInspector.svelte";
|
|
12
13
|
export { default as ChartCommitInput } from "./ChartCommitInput.svelte";
|
|
13
14
|
export { default as ChartMarks } from "./ChartMarks.svelte";
|
package/dist/chart/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as ChartEditor } from "./ChartEditor.svelte";
|
|
2
2
|
export { default as ChartCanvas } from "./ChartCanvas.svelte";
|
|
3
3
|
export { default as ChartDataGrid } from "./ChartDataGrid.svelte";
|
|
4
|
+
export { default as ChartTreeGrid } from "./ChartTreeGrid.svelte";
|
|
4
5
|
export { default as ChartInspector } from "./ChartInspector.svelte";
|
|
5
6
|
export { default as ChartCommitInput } from "./ChartCommitInput.svelte";
|
|
6
7
|
export { default as ChartMarks } from "./ChartMarks.svelte";
|
package/dist/chart/types.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type ChartInspectorSection = 'type' | 'style' | 'titles' | 'options' | 'legend' | 'axes' | 'series';
|
|
1
|
+
export type ChartInspectorSection = 'type' | 'style' | 'titles' | 'options' | 'legend' | 'axes' | 'annotations' | 'series';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixerate/editor-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.121.0",
|
|
4
4
|
"description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
|
|
5
5
|
"author": "Pixerate",
|
|
6
6
|
"license": "MIT",
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"@tiptap/extension-bubble-menu": "^2.27.3",
|
|
169
169
|
"@tiptap/extension-placeholder": "^2.27.3",
|
|
170
170
|
"@tiptap/starter-kit": "^2.27.3",
|
|
171
|
-
"@pixerate/editor": "0.
|
|
171
|
+
"@pixerate/editor": "0.164.0"
|
|
172
172
|
},
|
|
173
173
|
"peerDependencies": {
|
|
174
174
|
"@dagrejs/dagre": "^1.1.4",
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import type { Rect, XYPosition } from '@xyflow/svelte';
|
|
2
|
-
import type { CanvasEdge, CanvasLayoutOptions, CanvasNode, DirectionalDisplacementOptions } from './types.js';
|
|
3
|
-
/**
|
|
4
|
-
* Calculates a bounding rectangle for a set of nodes if not provided by getNodesBounds.
|
|
5
|
-
*/
|
|
6
|
-
export declare function computeNodesBoundingBox<TNode extends CanvasNode = CanvasNode>(nodes: TNode[], defaultWidth?: number, defaultHeight?: number): Rect;
|
|
7
|
-
/**
|
|
8
|
-
* Pure directional displacement calculation.
|
|
9
|
-
* Shifts downstream or surrounding nodes along an axis to clear space for newly inserted child nodes.
|
|
10
|
-
*/
|
|
11
|
-
export declare function calculateDirectionalDisplacement<TNode extends CanvasNode = CanvasNode>(params: {
|
|
12
|
-
existingNodes: TNode[];
|
|
13
|
-
originNode: TNode;
|
|
14
|
-
childNodes: TNode[];
|
|
15
|
-
getNodesBounds?: (nodes: TNode[]) => Rect;
|
|
16
|
-
options?: DirectionalDisplacementOptions;
|
|
17
|
-
}): Map<string, XYPosition>;
|
|
18
|
-
/**
|
|
19
|
-
* Pure hierarchical Dagre reflow displacement calculation.
|
|
20
|
-
* Calculates new coordinates for surrounding nodes using Dagre hierarchical layout,
|
|
21
|
-
* anchoring the origin node at its current position to eliminate layout jumping.
|
|
22
|
-
*/
|
|
23
|
-
export declare function calculateReflowDisplacement<TNode extends CanvasNode = CanvasNode, TEdge extends CanvasEdge = CanvasEdge>(params: {
|
|
24
|
-
existingNodes: TNode[];
|
|
25
|
-
existingEdges: TEdge[];
|
|
26
|
-
originNode: TNode;
|
|
27
|
-
childNodes: TNode[];
|
|
28
|
-
childEdges?: TEdge[];
|
|
29
|
-
getNodesBounds?: (nodes: TNode[]) => Rect;
|
|
30
|
-
options?: CanvasLayoutOptions;
|
|
31
|
-
}): Map<string, XYPosition>;
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import dagre from '@dagrejs/dagre';
|
|
2
|
-
/**
|
|
3
|
-
* Calculates a bounding rectangle for a set of nodes if not provided by getNodesBounds.
|
|
4
|
-
*/
|
|
5
|
-
export function computeNodesBoundingBox(nodes, defaultWidth = 200, defaultHeight = 60) {
|
|
6
|
-
if (nodes.length === 0) {
|
|
7
|
-
return { x: 0, y: 0, width: 0, height: 0 };
|
|
8
|
-
}
|
|
9
|
-
let minX = Infinity;
|
|
10
|
-
let minY = Infinity;
|
|
11
|
-
let maxX = -Infinity;
|
|
12
|
-
let maxY = -Infinity;
|
|
13
|
-
for (const node of nodes) {
|
|
14
|
-
const width = node.measured?.width ?? node.width ?? defaultWidth;
|
|
15
|
-
const height = node.measured?.height ?? node.height ?? defaultHeight;
|
|
16
|
-
minX = Math.min(minX, node.position.x);
|
|
17
|
-
minY = Math.min(minY, node.position.y);
|
|
18
|
-
maxX = Math.max(maxX, node.position.x + width);
|
|
19
|
-
maxY = Math.max(maxY, node.position.y + height);
|
|
20
|
-
}
|
|
21
|
-
return {
|
|
22
|
-
x: minX,
|
|
23
|
-
y: minY,
|
|
24
|
-
width: maxX - minX,
|
|
25
|
-
height: maxY - minY
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Pure directional displacement calculation.
|
|
30
|
-
* Shifts downstream or surrounding nodes along an axis to clear space for newly inserted child nodes.
|
|
31
|
-
*/
|
|
32
|
-
export function calculateDirectionalDisplacement(params) {
|
|
33
|
-
const { existingNodes, originNode, childNodes, getNodesBounds, options = {} } = params;
|
|
34
|
-
const { direction = 'right', gap = 40, spreadSiblingLanes = false, siblingTolerance = 50, minY } = options;
|
|
35
|
-
const displacedMap = new Map();
|
|
36
|
-
if (childNodes.length === 0 || existingNodes.length === 0) {
|
|
37
|
-
return displacedMap;
|
|
38
|
-
}
|
|
39
|
-
const childBounds = getNodesBounds
|
|
40
|
-
? getNodesBounds(childNodes)
|
|
41
|
-
: computeNodesBoundingBox(childNodes);
|
|
42
|
-
const originWidth = originNode.measured?.width ?? originNode.width ?? 200;
|
|
43
|
-
const originHeight = originNode.measured?.height ?? originNode.height ?? 60;
|
|
44
|
-
// The delta space required along primary axis
|
|
45
|
-
const deltaPrimary = (direction === 'right' || direction === 'left')
|
|
46
|
-
? childBounds.width + gap
|
|
47
|
-
: childBounds.height + gap;
|
|
48
|
-
// Vertical displacement parameters: upward and downward row clearance
|
|
49
|
-
const deltaUp = Math.max(0, originNode.position.y - childBounds.y + gap);
|
|
50
|
-
const deltaDown = Math.max(0, childBounds.y + childBounds.height - (originNode.position.y + originHeight) + gap);
|
|
51
|
-
// Both above and below displace equally so the row expands symmetrically
|
|
52
|
-
const equalDelta = Math.max(deltaUp, deltaDown);
|
|
53
|
-
for (const node of existingNodes) {
|
|
54
|
-
if (node.id === originNode.id)
|
|
55
|
-
continue;
|
|
56
|
-
let newX = node.position.x;
|
|
57
|
-
let newY = node.position.y;
|
|
58
|
-
let moved = false;
|
|
59
|
-
if (direction === 'right') {
|
|
60
|
-
// Downstream nodes strictly to the right of the origin
|
|
61
|
-
if (node.position.x >= originNode.position.x + originWidth * 0.5) {
|
|
62
|
-
newX += deltaPrimary;
|
|
63
|
-
moved = true;
|
|
64
|
-
}
|
|
65
|
-
else if (spreadSiblingLanes) {
|
|
66
|
-
// Sibling nodes occupying the same vertical space as the exploded cluster
|
|
67
|
-
const isVerticallyOverlapping = node.position.y + (node.height ?? 60) >= childBounds.y - gap &&
|
|
68
|
-
node.position.y <= childBounds.y + childBounds.height + gap;
|
|
69
|
-
if (isVerticallyOverlapping && Math.abs(node.position.x - originNode.position.x) <= siblingTolerance) {
|
|
70
|
-
const pushDirection = node.position.y >= originNode.position.y ? 1 : -1;
|
|
71
|
-
newY += pushDirection * (childBounds.height * 0.5 + gap);
|
|
72
|
-
moved = true;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
else if (direction === 'left') {
|
|
77
|
-
if (node.position.x <= originNode.position.x) {
|
|
78
|
-
newX -= deltaPrimary;
|
|
79
|
-
moved = true;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
else if (direction === 'down') {
|
|
83
|
-
if (node.position.y >= originNode.position.y + originHeight * 0.5) {
|
|
84
|
-
newY += deltaPrimary;
|
|
85
|
-
moved = true;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
else if (direction === 'up') {
|
|
89
|
-
if (node.position.y <= originNode.position.y) {
|
|
90
|
-
newY -= deltaPrimary;
|
|
91
|
-
moved = true;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
else if (direction === 'vertical') {
|
|
95
|
-
// Row grows vertically:
|
|
96
|
-
// Nodes strictly above the origin node move up by equalDelta
|
|
97
|
-
// Nodes strictly below the origin node move down by equalDelta
|
|
98
|
-
// Both above and below displace equally; horizontal position (x) is strictly preserved
|
|
99
|
-
if (node.position.y < originNode.position.y) {
|
|
100
|
-
if (equalDelta > 0) {
|
|
101
|
-
newY -= equalDelta;
|
|
102
|
-
moved = true;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
else if (node.position.y >= originNode.position.y + originHeight * 0.5) {
|
|
106
|
-
if (equalDelta > 0) {
|
|
107
|
-
newY += equalDelta;
|
|
108
|
-
moved = true;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (moved) {
|
|
113
|
-
displacedMap.set(node.id, { x: newX, y: newY });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
return displacedMap;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Pure hierarchical Dagre reflow displacement calculation.
|
|
120
|
-
* Calculates new coordinates for surrounding nodes using Dagre hierarchical layout,
|
|
121
|
-
* anchoring the origin node at its current position to eliminate layout jumping.
|
|
122
|
-
*/
|
|
123
|
-
export function calculateReflowDisplacement(params) {
|
|
124
|
-
const { existingNodes, existingEdges, originNode, childNodes, childEdges = [], options = {} } = params;
|
|
125
|
-
const { direction = 'LR', ranksep = 80, nodesep = 30, defaultNodeWidth = 200, defaultNodeHeight = 60 } = options;
|
|
126
|
-
const displacedMap = new Map();
|
|
127
|
-
if (existingNodes.length === 0)
|
|
128
|
-
return displacedMap;
|
|
129
|
-
const allNodes = [...existingNodes, ...childNodes];
|
|
130
|
-
const allEdges = [...existingEdges, ...childEdges];
|
|
131
|
-
const dagreGraph = new dagre.graphlib.Graph();
|
|
132
|
-
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
|
133
|
-
dagreGraph.setGraph({ rankdir: direction, ranksep, nodesep });
|
|
134
|
-
const allNodeIds = new Set(allNodes.map((n) => n.id));
|
|
135
|
-
allNodes.forEach((node) => {
|
|
136
|
-
const nodeWidth = node.measured?.width ?? node.width ?? defaultNodeWidth;
|
|
137
|
-
const nodeHeight = node.measured?.height ?? node.height ?? defaultNodeHeight;
|
|
138
|
-
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
|
|
139
|
-
});
|
|
140
|
-
allEdges.forEach((edge) => {
|
|
141
|
-
if (allNodeIds.has(edge.source) && allNodeIds.has(edge.target)) {
|
|
142
|
-
dagreGraph.setEdge(edge.source, edge.target);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
dagre.layout(dagreGraph);
|
|
146
|
-
const dagreOrigin = dagreGraph.node(originNode.id);
|
|
147
|
-
if (!dagreOrigin)
|
|
148
|
-
return displacedMap;
|
|
149
|
-
const originWidth = originNode.measured?.width ?? originNode.width ?? defaultNodeWidth;
|
|
150
|
-
const originHeight = originNode.measured?.height ?? originNode.height ?? defaultNodeHeight;
|
|
151
|
-
// Calculate offset so origin node remains anchored at its exact initial coordinates
|
|
152
|
-
const anchorOffsetX = originNode.position.x - (dagreOrigin.x - originWidth / 2);
|
|
153
|
-
const anchorOffsetY = originNode.position.y - (dagreOrigin.y - originHeight / 2);
|
|
154
|
-
existingNodes.forEach((node) => {
|
|
155
|
-
if (node.id === originNode.id)
|
|
156
|
-
return;
|
|
157
|
-
const dagreNode = dagreGraph.node(node.id);
|
|
158
|
-
if (!dagreNode)
|
|
159
|
-
return;
|
|
160
|
-
const nodeWidth = node.measured?.width ?? node.width ?? defaultNodeWidth;
|
|
161
|
-
const nodeHeight = node.measured?.height ?? node.height ?? defaultNodeHeight;
|
|
162
|
-
const targetX = dagreNode.x - nodeWidth / 2 + anchorOffsetX;
|
|
163
|
-
const targetY = dagreNode.y - nodeHeight / 2 + anchorOffsetY;
|
|
164
|
-
// Only include if position actually shifted
|
|
165
|
-
if (Math.abs(targetX - node.position.x) > 0.5 || Math.abs(targetY - node.position.y) > 0.5) {
|
|
166
|
-
displacedMap.set(node.id, { x: targetX, y: targetY });
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
return displacedMap;
|
|
170
|
-
}
|