@opendata-ai/openchart-engine 6.5.1 → 6.6.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/dist/index.d.ts +42 -17
- package/dist/index.js +1331 -363
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/src/__tests__/dimensions.test.ts +47 -1
- package/src/annotations/__tests__/compute.test.ts +28 -0
- package/src/annotations/compute.ts +0 -8
- package/src/compile.ts +30 -0
- package/src/compiler/normalize.ts +25 -2
- package/src/compiler/types.ts +6 -1
- package/src/compiler/validate.ts +109 -5
- package/src/index.ts +9 -1
- package/src/layout/dimensions.ts +6 -1
- package/src/sankey/__tests__/compile-sankey.test.ts +353 -0
- package/src/sankey/__tests__/layout.test.ts +165 -0
- package/src/sankey/compile-sankey.ts +593 -0
- package/src/sankey/layout.ts +170 -0
- package/src/sankey/types.ts +36 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* d3-sankey layout wrapper.
|
|
3
|
+
*
|
|
4
|
+
* Extracts unique nodes from tabular data rows, configures the d3-sankey
|
|
5
|
+
* generator, and returns computed node/link positions. Clones input data
|
|
6
|
+
* before passing to d3-sankey since it mutates objects in place.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Rect, SankeyNodeAlign } from '@opendata-ai/openchart-core';
|
|
10
|
+
import type { SankeyExtraProperties, SankeyGraph, SankeyLink, SankeyNode } from 'd3-sankey';
|
|
11
|
+
import { sankey, sankeyCenter, sankeyJustify, sankeyLeft, sankeyRight } from 'd3-sankey';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Types
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
/** Extra properties carried on our sankey nodes. */
|
|
18
|
+
interface NodeExtra extends SankeyExtraProperties {
|
|
19
|
+
id: string;
|
|
20
|
+
label: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Extra properties carried on our sankey links. */
|
|
24
|
+
interface LinkExtra extends SankeyExtraProperties {
|
|
25
|
+
/** Original data row for this link. */
|
|
26
|
+
data: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ComputedNode = SankeyNode<NodeExtra, LinkExtra>;
|
|
30
|
+
export type ComputedLink = SankeyLink<NodeExtra, LinkExtra>;
|
|
31
|
+
|
|
32
|
+
export interface SankeyLayoutResult {
|
|
33
|
+
nodes: ComputedNode[];
|
|
34
|
+
links: ComputedLink[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Alignment resolver
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
const ALIGN_MAP: Record<SankeyNodeAlign, typeof sankeyJustify> = {
|
|
42
|
+
justify: sankeyJustify,
|
|
43
|
+
left: sankeyLeft,
|
|
44
|
+
right: sankeyRight,
|
|
45
|
+
center: sankeyCenter,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Public API
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Run the d3-sankey layout algorithm on tabular flow data.
|
|
54
|
+
*
|
|
55
|
+
* @param data - Array of data rows (each row is a source-target-value flow).
|
|
56
|
+
* @param sourceField - Field name for the source node.
|
|
57
|
+
* @param targetField - Field name for the target node.
|
|
58
|
+
* @param valueField - Field name for the flow value.
|
|
59
|
+
* @param area - Drawing area rect (after chrome subtracted).
|
|
60
|
+
* @param nodeWidth - Width of node rectangles in px.
|
|
61
|
+
* @param nodePadding - Vertical padding between nodes in px.
|
|
62
|
+
* @param nodeAlign - Node alignment strategy.
|
|
63
|
+
* @param iterations - Number of layout relaxation iterations.
|
|
64
|
+
* @returns Computed node and link positions.
|
|
65
|
+
*/
|
|
66
|
+
export function computeSankeyLayout(
|
|
67
|
+
data: Record<string, unknown>[],
|
|
68
|
+
sourceField: string,
|
|
69
|
+
targetField: string,
|
|
70
|
+
valueField: string,
|
|
71
|
+
area: Rect,
|
|
72
|
+
nodeWidth: number,
|
|
73
|
+
nodePadding: number,
|
|
74
|
+
nodeAlign: SankeyNodeAlign,
|
|
75
|
+
iterations: number,
|
|
76
|
+
): SankeyLayoutResult {
|
|
77
|
+
// Extract unique node IDs from source and target columns
|
|
78
|
+
const nodeSet = new Set<string>();
|
|
79
|
+
for (const row of data) {
|
|
80
|
+
nodeSet.add(String(row[sourceField]));
|
|
81
|
+
nodeSet.add(String(row[targetField]));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build node and link arrays (cloned so d3-sankey mutations don't affect input)
|
|
85
|
+
const nodes: Array<{ id: string; label: string }> = [...nodeSet].map((id) => ({
|
|
86
|
+
id,
|
|
87
|
+
label: id,
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
const links: Array<{
|
|
91
|
+
source: string;
|
|
92
|
+
target: string;
|
|
93
|
+
value: number;
|
|
94
|
+
data: Record<string, unknown>;
|
|
95
|
+
}> = data.map((row) => ({
|
|
96
|
+
source: String(row[sourceField]),
|
|
97
|
+
target: String(row[targetField]),
|
|
98
|
+
value: Number(row[valueField]) || 0,
|
|
99
|
+
data: { ...row },
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
// Configure and run d3-sankey
|
|
103
|
+
const alignFn = ALIGN_MAP[nodeAlign] ?? sankeyJustify;
|
|
104
|
+
|
|
105
|
+
const generator = sankey<SankeyGraph<NodeExtra, LinkExtra>, NodeExtra, LinkExtra>()
|
|
106
|
+
.nodeId((d) => d.id)
|
|
107
|
+
.nodeAlign(alignFn as unknown as (node: SankeyNode<NodeExtra, LinkExtra>, n: number) => number)
|
|
108
|
+
.nodeWidth(nodeWidth)
|
|
109
|
+
.nodePadding(nodePadding)
|
|
110
|
+
.extent([
|
|
111
|
+
[area.x, area.y],
|
|
112
|
+
[area.x + area.width, area.y + area.height],
|
|
113
|
+
])
|
|
114
|
+
.iterations(iterations);
|
|
115
|
+
|
|
116
|
+
const graph = generator({
|
|
117
|
+
nodes: nodes as unknown as Array<SankeyNode<NodeExtra, LinkExtra>>,
|
|
118
|
+
links: links as unknown as Array<SankeyLink<NodeExtra, LinkExtra>>,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
nodes: graph.nodes,
|
|
123
|
+
links: graph.links,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Generate a filled ribbon SVG path for a sankey link.
|
|
129
|
+
*
|
|
130
|
+
* d3-sankey's sankeyLinkHorizontal() only produces a stroke centerline.
|
|
131
|
+
* This generates a closed area path with two cubic bezier edges (top and
|
|
132
|
+
* bottom) forming a ribbon whose width is proportional to flow value.
|
|
133
|
+
*
|
|
134
|
+
* The link object from d3-sankey provides:
|
|
135
|
+
* - y0: center y at source side
|
|
136
|
+
* - y1: center y at target side
|
|
137
|
+
* - width: thickness of the ribbon
|
|
138
|
+
* - source.x1: right edge of source node
|
|
139
|
+
* - target.x0: left edge of target node
|
|
140
|
+
*/
|
|
141
|
+
export function generateLinkPath(link: ComputedLink): string {
|
|
142
|
+
const source = link.source as ComputedNode;
|
|
143
|
+
const target = link.target as ComputedNode;
|
|
144
|
+
|
|
145
|
+
const x0 = source.x1 ?? 0;
|
|
146
|
+
const x1 = target.x0 ?? 0;
|
|
147
|
+
const y0 = link.y0 ?? 0;
|
|
148
|
+
const y1 = link.y1 ?? 0;
|
|
149
|
+
const halfWidth0 = (link.width ?? 0) / 2;
|
|
150
|
+
const halfWidth1 = halfWidth0;
|
|
151
|
+
|
|
152
|
+
// Control point x at the horizontal midpoint for smooth S-curves
|
|
153
|
+
const mx = (x0 + x1) / 2;
|
|
154
|
+
|
|
155
|
+
// Top edge: left-to-right
|
|
156
|
+
const topY0 = y0 - halfWidth0;
|
|
157
|
+
const topY1 = y1 - halfWidth1;
|
|
158
|
+
|
|
159
|
+
// Bottom edge: right-to-left
|
|
160
|
+
const botY0 = y0 + halfWidth0;
|
|
161
|
+
const botY1 = y1 + halfWidth1;
|
|
162
|
+
|
|
163
|
+
return [
|
|
164
|
+
`M${x0},${topY0}`,
|
|
165
|
+
`C${mx},${topY0} ${mx},${topY1} ${x1},${topY1}`,
|
|
166
|
+
`L${x1},${botY1}`,
|
|
167
|
+
`C${mx},${botY1} ${mx},${botY0} ${x0},${botY0}`,
|
|
168
|
+
'Z',
|
|
169
|
+
].join(' ');
|
|
170
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal normalized sankey spec type used by the compilation pipeline.
|
|
3
|
+
*
|
|
4
|
+
* This mirrors NormalizedChartSpec/NormalizedGraphSpec: all optional fields
|
|
5
|
+
* have been filled with sensible defaults. It's an engine implementation
|
|
6
|
+
* detail, not a public contract.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
AnimationSpec,
|
|
11
|
+
DarkMode,
|
|
12
|
+
LegendConfig,
|
|
13
|
+
SankeyEncoding,
|
|
14
|
+
SankeyLinkColor,
|
|
15
|
+
SankeyNodeAlign,
|
|
16
|
+
ThemeConfig,
|
|
17
|
+
} from '@opendata-ai/openchart-core';
|
|
18
|
+
|
|
19
|
+
import type { NormalizedChrome } from '../compiler/types';
|
|
20
|
+
|
|
21
|
+
/** A SankeySpec with all optional fields filled with defaults. */
|
|
22
|
+
export interface NormalizedSankeySpec {
|
|
23
|
+
type: 'sankey';
|
|
24
|
+
data: Record<string, unknown>[];
|
|
25
|
+
encoding: SankeyEncoding;
|
|
26
|
+
nodeWidth: number;
|
|
27
|
+
nodePadding: number;
|
|
28
|
+
nodeAlign: SankeyNodeAlign;
|
|
29
|
+
iterations: number;
|
|
30
|
+
linkStyle: SankeyLinkColor;
|
|
31
|
+
chrome: NormalizedChrome;
|
|
32
|
+
legend?: LegendConfig;
|
|
33
|
+
theme: ThemeConfig;
|
|
34
|
+
darkMode: DarkMode;
|
|
35
|
+
animation?: AnimationSpec;
|
|
36
|
+
}
|