turbometro 0.1.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/LICENSE +21 -0
- package/README.md +80 -0
- package/bin/turbometro.js +2 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +107 -0
- package/dist/find-root.d.ts +2 -0
- package/dist/find-root.js +14 -0
- package/dist/graph.d.ts +14 -0
- package/dist/graph.js +74 -0
- package/dist/layout.d.ts +19 -0
- package/dist/layout.js +15 -0
- package/dist/parse-turbo.d.ts +5 -0
- package/dist/parse-turbo.js +29 -0
- package/dist/parse-workspace.d.ts +4 -0
- package/dist/parse-workspace.js +100 -0
- package/dist/render-html.d.ts +8 -0
- package/dist/render-html.js +699 -0
- package/dist/render-map.d.ts +29 -0
- package/dist/render-map.js +287 -0
- package/dist/replay.d.ts +9 -0
- package/dist/replay.js +74 -0
- package/dist/scale.d.ts +7 -0
- package/dist/scale.js +19 -0
- package/dist/scene-data.d.ts +40 -0
- package/dist/scene-data.js +146 -0
- package/dist/theme.d.ts +15 -0
- package/dist/theme.js +15 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +1 -0
- package/fixtures/mini-mono/apps/web/package.json +11 -0
- package/fixtures/mini-mono/package.json +5 -0
- package/fixtures/mini-mono/packages/ui/package.json +11 -0
- package/fixtures/mini-mono/packages/utils/package.json +8 -0
- package/fixtures/mini-mono/pnpm-workspace.yaml +3 -0
- package/fixtures/mini-mono/turbo.json +10 -0
- package/package.json +44 -0
- package/vendor/dag-map/LICENSE +201 -0
- package/vendor/dag-map/NOTICE +6 -0
- package/vendor/dag-map/src/color-scales.js +61 -0
- package/vendor/dag-map/src/dag-map.css +63 -0
- package/vendor/dag-map/src/events.js +108 -0
- package/vendor/dag-map/src/graph-utils.js +185 -0
- package/vendor/dag-map/src/hasse.css +61 -0
- package/vendor/dag-map/src/index.js +55 -0
- package/vendor/dag-map/src/layout-flow.js +1066 -0
- package/vendor/dag-map/src/layout-hasse.js +485 -0
- package/vendor/dag-map/src/layout-metro.js +542 -0
- package/vendor/dag-map/src/occupancy.js +138 -0
- package/vendor/dag-map/src/render-flow-station.js +132 -0
- package/vendor/dag-map/src/render.js +360 -0
- package/vendor/dag-map/src/route-angular.js +137 -0
- package/vendor/dag-map/src/route-bezier.js +51 -0
- package/vendor/dag-map/src/route-metro.js +122 -0
- package/vendor/dag-map/src/themes.js +49 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// ================================================================
|
|
2
|
+
// graph-utils.js — Shared graph primitives for dag-map layout engines
|
|
3
|
+
// ================================================================
|
|
4
|
+
// Adjacency map construction and Kahn's algorithm topological sort
|
|
5
|
+
// with longest-path rank assignment. Used by all three layout engines.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build adjacency maps from nodes and edges.
|
|
9
|
+
* @param {Array<{id: string}>} nodes
|
|
10
|
+
* @param {Array<[string, string]>} edges
|
|
11
|
+
* @returns {{ nodeMap: Map, childrenOf: Map, parentsOf: Map }}
|
|
12
|
+
*/
|
|
13
|
+
export function buildGraph(nodes, edges) {
|
|
14
|
+
const nodeMap = new Map(nodes.map(n => [n.id, n]));
|
|
15
|
+
const childrenOf = new Map();
|
|
16
|
+
const parentsOf = new Map();
|
|
17
|
+
nodes.forEach(n => { childrenOf.set(n.id, []); parentsOf.set(n.id, []); });
|
|
18
|
+
edges.forEach(([f, t], edgeIdx) => {
|
|
19
|
+
const srcChildren = childrenOf.get(f);
|
|
20
|
+
const dstParents = parentsOf.get(t);
|
|
21
|
+
if (!srcChildren || !dstParents) {
|
|
22
|
+
const parts = [];
|
|
23
|
+
if (!srcChildren) parts.push(`source "${f}"`);
|
|
24
|
+
if (!dstParents) parts.push(`target "${t}"`);
|
|
25
|
+
throw new Error(`buildGraph: edge[${edgeIdx}] references unknown ${parts.join(' and ')}`);
|
|
26
|
+
}
|
|
27
|
+
srcChildren.push(t);
|
|
28
|
+
dstParents.push(f);
|
|
29
|
+
});
|
|
30
|
+
return { nodeMap, childrenOf, parentsOf };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Topological sort via Kahn's algorithm with longest-path rank assignment.
|
|
35
|
+
* @param {Array<{id: string}>} nodes
|
|
36
|
+
* @param {Map} childrenOf
|
|
37
|
+
* @param {Map} parentsOf
|
|
38
|
+
* @returns {{ topo: string[], rank: Map<string, number>, maxRank: number }}
|
|
39
|
+
*/
|
|
40
|
+
export function topoSortAndRank(nodes, childrenOf, parentsOf) {
|
|
41
|
+
const rank = new Map();
|
|
42
|
+
const inDeg = new Map();
|
|
43
|
+
nodes.forEach(nd => inDeg.set(nd.id, parentsOf.get(nd.id).length));
|
|
44
|
+
|
|
45
|
+
const queue = nodes.filter(nd => inDeg.get(nd.id) === 0).map(nd => nd.id);
|
|
46
|
+
queue.forEach(id => rank.set(id, 0));
|
|
47
|
+
|
|
48
|
+
const topo = [];
|
|
49
|
+
while (queue.length) {
|
|
50
|
+
const u = queue.shift();
|
|
51
|
+
topo.push(u);
|
|
52
|
+
for (const v of childrenOf.get(u)) {
|
|
53
|
+
rank.set(v, Math.max(rank.get(v) || 0, rank.get(u) + 1));
|
|
54
|
+
inDeg.set(v, inDeg.get(v) - 1);
|
|
55
|
+
if (inDeg.get(v) === 0) queue.push(v);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const maxRank = topo.length > 0 ? Math.max(...topo.map(id => rank.get(id))) : 0;
|
|
60
|
+
return { topo, rank, maxRank };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate a DAG definition and return warnings for common issues.
|
|
65
|
+
* Non-throwing — returns an array of human-readable warning strings.
|
|
66
|
+
* @param {Array<{id: string}>} nodes
|
|
67
|
+
* @param {Array<[string, string]>} edges
|
|
68
|
+
* @returns {string[]} warnings (empty if valid)
|
|
69
|
+
*/
|
|
70
|
+
export function validateDag(nodes, edges) {
|
|
71
|
+
const warnings = [];
|
|
72
|
+
const ids = new Set();
|
|
73
|
+
|
|
74
|
+
// Duplicate node IDs
|
|
75
|
+
for (const n of nodes) {
|
|
76
|
+
if (ids.has(n.id)) warnings.push(`Duplicate node ID: "${n.id}"`);
|
|
77
|
+
ids.add(n.id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Edges referencing unknown nodes
|
|
81
|
+
for (const [f, t] of edges) {
|
|
82
|
+
if (!ids.has(f)) warnings.push(`Edge source "${f}" is not a known node`);
|
|
83
|
+
if (!ids.has(t)) warnings.push(`Edge target "${t}" is not a known node`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Cycle detection via topo sort
|
|
87
|
+
if (warnings.length === 0 && nodes.length > 0) {
|
|
88
|
+
const { childrenOf, parentsOf } = buildGraph(nodes, edges);
|
|
89
|
+
const { topo } = topoSortAndRank(nodes, childrenOf, parentsOf);
|
|
90
|
+
if (topo.length < nodes.length) {
|
|
91
|
+
const missing = nodes.filter(n => !topo.includes(n.id)).map(n => n.id);
|
|
92
|
+
warnings.push(`Cycle detected — ${missing.length} node(s) unreachable: ${missing.join(', ')}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return warnings;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate a DAG definition and throw with a useful message if invalid.
|
|
101
|
+
* @param {Array<{id: string}>} nodes
|
|
102
|
+
* @param {Array<[string, string]>} edges
|
|
103
|
+
* @param {string} [context='DAG']
|
|
104
|
+
*/
|
|
105
|
+
export function assertValidDag(nodes, edges, context = 'DAG') {
|
|
106
|
+
const warnings = validateDag(nodes, edges);
|
|
107
|
+
if (warnings.length > 0) {
|
|
108
|
+
throw new Error(`${context}: invalid DAG input. ${warnings.join('; ')}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ================================================================
|
|
113
|
+
// SVG path coordinate swap (X↔Y) for orientation transforms
|
|
114
|
+
// ================================================================
|
|
115
|
+
|
|
116
|
+
// How many coordinate values each SVG command consumes per repetition.
|
|
117
|
+
// Commands that take (x,y) pairs: values are swapped pairwise.
|
|
118
|
+
// H↔V are single-axis and swap command letter instead.
|
|
119
|
+
// A (arc) is not supported — its 7-param layout doesn't pair-swap cleanly.
|
|
120
|
+
const CMD_PARAMS = {
|
|
121
|
+
M: 2, L: 2, T: 2, // 1 pair
|
|
122
|
+
Q: 4, S: 4, // 2 pairs
|
|
123
|
+
C: 6, // 3 pairs
|
|
124
|
+
H: 1, V: 1, // single axis — letter swaps
|
|
125
|
+
Z: 0, // no params
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Swap X↔Y coordinates in an SVG path string.
|
|
130
|
+
* Handles M, L, C, Q, S, T, H, V, Z (both absolute and relative).
|
|
131
|
+
* Throws on A/a (arc) — arc parameter layout requires special handling.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} d - SVG path data string
|
|
134
|
+
* @returns {string} path with all X and Y coordinates swapped
|
|
135
|
+
*/
|
|
136
|
+
export function swapPathXY(d) {
|
|
137
|
+
if (!d) return '';
|
|
138
|
+
|
|
139
|
+
// Tokenize: split into command + numbers sequences.
|
|
140
|
+
// Regex captures a command letter followed by its numeric arguments.
|
|
141
|
+
const tokens = [];
|
|
142
|
+
const re = /([MLCSQTHVZAmlcsqthvza])\s*([^MLCSQTHVZAmlcsqthvza]*)/g;
|
|
143
|
+
let m;
|
|
144
|
+
while ((m = re.exec(d)) !== null) {
|
|
145
|
+
const cmd = m[1];
|
|
146
|
+
const argStr = m[2].trim();
|
|
147
|
+
const nums = argStr.length > 0 ? argStr.split(/[\s,]+/).map(Number) : [];
|
|
148
|
+
tokens.push({ cmd, nums });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const parts = [];
|
|
152
|
+
for (const { cmd, nums } of tokens) {
|
|
153
|
+
const upper = cmd.toUpperCase();
|
|
154
|
+
|
|
155
|
+
if (upper === 'A') {
|
|
156
|
+
throw new Error('swapPathXY: arc commands (A/a) are not supported');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (upper === 'Z') {
|
|
160
|
+
parts.push(cmd);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (upper === 'H') {
|
|
165
|
+
// H x → V x (swap command letter, keep value)
|
|
166
|
+
parts.push((cmd === 'H' ? 'V' : 'v') + ' ' + nums.join(' '));
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (upper === 'V') {
|
|
171
|
+
// V y → H y
|
|
172
|
+
parts.push((cmd === 'V' ? 'H' : 'h') + ' ' + nums.join(' '));
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Pair-swapping commands: swap every (x, y) → (y, x)
|
|
177
|
+
const swapped = [];
|
|
178
|
+
for (let i = 0; i < nums.length; i += 2) {
|
|
179
|
+
swapped.push(nums[i + 1], nums[i]);
|
|
180
|
+
}
|
|
181
|
+
parts.push(cmd + ' ' + swapped.join(' '));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return parts.join(' ');
|
|
185
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* =================================================================
|
|
2
|
+
* hasse.css — Library stylesheet for dag-map Hasse diagrams
|
|
3
|
+
* =================================================================
|
|
4
|
+
* The minimal CSS needed to use layoutHasse in your application.
|
|
5
|
+
* Provides CSS custom properties for theming and base container styles.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* <link rel="stylesheet" href="hasse.css">
|
|
9
|
+
* <div class="dm-container" id="my-diagram"></div>
|
|
10
|
+
*
|
|
11
|
+
* Override theme via CSS custom properties:
|
|
12
|
+
* :root { --dm-paper: #1E1E2E; --dm-ink: #CDD6F4; }
|
|
13
|
+
*
|
|
14
|
+
* Or via the JS theme system (takes precedence):
|
|
15
|
+
* layoutHasse(dag, { theme: 'dark' });
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/* ── CSS Custom Properties (defaults = cream theme) ── */
|
|
19
|
+
:root {
|
|
20
|
+
--dm-paper: #F5F0E8;
|
|
21
|
+
--dm-ink: #2C2C2C;
|
|
22
|
+
--dm-muted: #8C8680;
|
|
23
|
+
--dm-border: #D4CFC7;
|
|
24
|
+
--dm-font: 'IBM Plex Mono', 'Courier New', monospace;
|
|
25
|
+
--dm-radius: 4px;
|
|
26
|
+
|
|
27
|
+
/* Node class colors (used when cssVars: true) */
|
|
28
|
+
--dm-cls-top: #C45B4A; /* ⊤ — maximum element */
|
|
29
|
+
--dm-cls-type: #2B8A8E; /* regular elements */
|
|
30
|
+
--dm-cls-bottom: #5B7FA8; /* ⊥ — minimum element */
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* ── Container ── */
|
|
34
|
+
.dm-container {
|
|
35
|
+
background: var(--dm-paper);
|
|
36
|
+
font-family: var(--dm-font);
|
|
37
|
+
color: var(--dm-ink);
|
|
38
|
+
transition: background 0.2s;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.dm-container svg {
|
|
42
|
+
display: block;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ── Code snippet (optional — for showing generated options) ── */
|
|
46
|
+
.dm-code {
|
|
47
|
+
margin: 12px 24px;
|
|
48
|
+
padding: 12px 16px;
|
|
49
|
+
background: rgba(0, 0, 0, 0.03);
|
|
50
|
+
border: 1px solid var(--dm-border);
|
|
51
|
+
border-radius: var(--dm-radius);
|
|
52
|
+
font-family: var(--dm-font);
|
|
53
|
+
font-size: 11px;
|
|
54
|
+
line-height: 1.5;
|
|
55
|
+
color: var(--dm-ink);
|
|
56
|
+
white-space: pre;
|
|
57
|
+
overflow-x: auto;
|
|
58
|
+
cursor: text;
|
|
59
|
+
user-select: all;
|
|
60
|
+
-webkit-user-select: all;
|
|
61
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// ================================================================
|
|
2
|
+
// dag-map — DAG visualization as metro maps
|
|
3
|
+
// ================================================================
|
|
4
|
+
// Public API
|
|
5
|
+
|
|
6
|
+
import { layoutMetro, dominantClass } from './layout-metro.js';
|
|
7
|
+
import { layoutHasse } from './layout-hasse.js';
|
|
8
|
+
import { layoutFlow } from './layout-flow.js';
|
|
9
|
+
import { renderSVG } from './render.js';
|
|
10
|
+
import { bezierPath } from './route-bezier.js';
|
|
11
|
+
import { angularPath, progressiveCurve } from './route-angular.js';
|
|
12
|
+
import { metroPath } from './route-metro.js';
|
|
13
|
+
import { THEMES, resolveTheme } from './themes.js';
|
|
14
|
+
import { createStationRenderer, createEdgeRenderer } from './render-flow-station.js';
|
|
15
|
+
import { validateDag, swapPathXY } from './graph-utils.js';
|
|
16
|
+
import { colorScales } from './color-scales.js';
|
|
17
|
+
import { bindEvents } from './events.js';
|
|
18
|
+
|
|
19
|
+
export { layoutMetro, dominantClass };
|
|
20
|
+
export { layoutHasse };
|
|
21
|
+
export { layoutFlow };
|
|
22
|
+
export { renderSVG };
|
|
23
|
+
export { bindEvents };
|
|
24
|
+
export { bezierPath };
|
|
25
|
+
export { angularPath, progressiveCurve };
|
|
26
|
+
export { metroPath };
|
|
27
|
+
export { THEMES, resolveTheme };
|
|
28
|
+
export { createStationRenderer, createEdgeRenderer };
|
|
29
|
+
export { validateDag, swapPathXY };
|
|
30
|
+
export { colorScales };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convenience function: compute layout and render SVG in one call.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} dag - { nodes: [{id, label, cls}], edges: [[from, to]] }
|
|
36
|
+
* @param {object} [options] - combined layout + render options
|
|
37
|
+
* @param {'bezier'|'angular'|'metro'} [options.routing='angular']
|
|
38
|
+
* @param {string|object} [options.theme='cream'] - theme name or custom theme object
|
|
39
|
+
* @param {string} [options.title]
|
|
40
|
+
* @param {boolean} [options.diagonalLabels=false]
|
|
41
|
+
* @param {boolean} [options.showLegend=true]
|
|
42
|
+
* @param {number} [options.trunkY=160]
|
|
43
|
+
* @param {number} [options.mainSpacing=34]
|
|
44
|
+
* @param {number} [options.subSpacing=16]
|
|
45
|
+
* @param {number} [options.layerSpacing=38]
|
|
46
|
+
* @param {number} [options.progressivePower=2.2]
|
|
47
|
+
* @param {number} [options.scale=1.5]
|
|
48
|
+
* @param {number} [options.maxLanes] - max number of lanes to search
|
|
49
|
+
* @returns {{ layout: object, svg: string }}
|
|
50
|
+
*/
|
|
51
|
+
export function dagMap(dag, options = {}) {
|
|
52
|
+
const layout = layoutMetro(dag, options);
|
|
53
|
+
const svg = renderSVG(dag, layout, options);
|
|
54
|
+
return { layout, svg };
|
|
55
|
+
}
|