@carlonicora/nextjs-jsonapi 1.99.0 → 1.100.1
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/{BlockNoteEditor-IBV3KBQM.mjs → BlockNoteEditor-OBWSYICZ.mjs} +2 -2
- package/dist/{BlockNoteEditor-LYJUF5N4.js → BlockNoteEditor-Z4YI6XFJ.js} +9 -9
- package/dist/{BlockNoteEditor-LYJUF5N4.js.map → BlockNoteEditor-Z4YI6XFJ.js.map} +1 -1
- package/dist/billing/index.js +299 -299
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-CDNVUON3.mjs → chunk-J767HTWG.mjs} +102 -8
- package/dist/chunk-J767HTWG.mjs.map +1 -0
- package/dist/{chunk-TRTKIQUB.js → chunk-JQ6O7OEZ.js} +333 -239
- package/dist/chunk-JQ6O7OEZ.js.map +1 -0
- package/dist/client/index.d.mts +17 -1
- package/dist/client/index.d.ts +17 -1
- package/dist/client/index.js +4 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +3 -1
- package/dist/components/index.js +2 -2
- package/dist/components/index.mjs +1 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/package.json +1 -1
- package/src/hooks/__tests__/computeLayeredLayout.spec.ts +135 -1
- package/src/hooks/computeLayeredLayout.ts +126 -13
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useCustomD3Graph.tsx +47 -11
- package/dist/chunk-CDNVUON3.mjs.map +0 -1
- package/dist/chunk-TRTKIQUB.js.map +0 -1
- /package/dist/{BlockNoteEditor-IBV3KBQM.mjs.map → BlockNoteEditor-OBWSYICZ.mjs.map} +0 -0
|
@@ -16,10 +16,25 @@ export interface LayeredLayoutPosition {
|
|
|
16
16
|
y: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export interface FitLayeredLayoutOptions extends LayeredLayoutOptions {
|
|
20
|
+
targetAspectRatio: number;
|
|
21
|
+
maxIterations?: number;
|
|
22
|
+
tolerance?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
19
25
|
const DEFAULT_RANKDIR: LayeredRankDir = "LR";
|
|
20
26
|
const DEFAULT_NODESEP = 50;
|
|
21
27
|
const DEFAULT_RANKSEP = 120;
|
|
22
28
|
|
|
29
|
+
const MIN_NODESEP = 20;
|
|
30
|
+
const MAX_NODESEP = 400;
|
|
31
|
+
const MIN_RANKSEP = 40;
|
|
32
|
+
const MAX_RANKSEP = 600;
|
|
33
|
+
const MIN_FACTOR = 0.25;
|
|
34
|
+
const MAX_FACTOR = 4;
|
|
35
|
+
const DEFAULT_TOLERANCE = 0.05;
|
|
36
|
+
const DEFAULT_MAX_ITERATIONS = 4;
|
|
37
|
+
|
|
23
38
|
const TITLE_PX_PER_CHAR_16 = 8;
|
|
24
39
|
const NAME_PX_PER_CHAR_12 = 6.5;
|
|
25
40
|
const NAME_PX_PER_CHAR_16_BOLD = 8;
|
|
@@ -40,20 +55,22 @@ function linkEndpointId(end: D3Link["source"] | D3Link["target"]): string {
|
|
|
40
55
|
return typeof end === "string" ? end : end.id;
|
|
41
56
|
}
|
|
42
57
|
|
|
58
|
+
function clamp(value: number, min: number, max: number): number {
|
|
59
|
+
return Math.min(Math.max(value, min), max);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface DagreRunResult {
|
|
63
|
+
positions: Map<string, LayeredLayoutPosition>;
|
|
64
|
+
bbox: { width: number; height: number };
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* should fall back to their previous layout in that case.
|
|
68
|
+
* Run dagre once for the given nodes/links/opts and return both the
|
|
69
|
+
* computed positions and the bounding box of the laid-out graph.
|
|
70
|
+
* Internal helper — callers should use `computeLayeredLayout` or
|
|
71
|
+
* `fitLayeredLayoutToAspectRatio`.
|
|
49
72
|
*/
|
|
50
|
-
|
|
51
|
-
nodes: D3Node[],
|
|
52
|
-
links: D3Link[],
|
|
53
|
-
opts: LayeredLayoutOptions,
|
|
54
|
-
): Map<string, LayeredLayoutPosition> | null {
|
|
55
|
-
if (nodes.length === 0) return new Map();
|
|
56
|
-
|
|
73
|
+
function runDagreOnce(nodes: D3Node[], links: D3Link[], opts: LayeredLayoutOptions): DagreRunResult | null {
|
|
57
74
|
const rankdir = opts.rankdir ?? DEFAULT_RANKDIR;
|
|
58
75
|
const nodesep = opts.nodesep ?? DEFAULT_NODESEP;
|
|
59
76
|
const ranksep = opts.ranksep ?? DEFAULT_RANKSEP;
|
|
@@ -86,11 +103,107 @@ export function computeLayeredLayout(
|
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
const positions = new Map<string, LayeredLayoutPosition>();
|
|
106
|
+
let xMin = Infinity;
|
|
107
|
+
let xMax = -Infinity;
|
|
108
|
+
let yMin = Infinity;
|
|
109
|
+
let yMax = -Infinity;
|
|
110
|
+
|
|
89
111
|
for (const node of nodes) {
|
|
90
112
|
const laid = g.node(node.id);
|
|
91
113
|
if (laid && Number.isFinite(laid.x) && Number.isFinite(laid.y)) {
|
|
92
114
|
positions.set(node.id, { x: laid.x, y: laid.y });
|
|
115
|
+
const halfW = (laid.width ?? opts.minNodeWidth) / 2;
|
|
116
|
+
const halfH = (laid.height ?? opts.minNodeHeight) / 2;
|
|
117
|
+
xMin = Math.min(xMin, laid.x - halfW);
|
|
118
|
+
xMax = Math.max(xMax, laid.x + halfW);
|
|
119
|
+
yMin = Math.min(yMin, laid.y - halfH);
|
|
120
|
+
yMax = Math.max(yMax, laid.y + halfH);
|
|
93
121
|
}
|
|
94
122
|
}
|
|
95
|
-
|
|
123
|
+
|
|
124
|
+
const bbox =
|
|
125
|
+
positions.size === 0
|
|
126
|
+
? { width: 0, height: 0 }
|
|
127
|
+
: { width: Math.max(0, xMax - xMin), height: Math.max(0, yMax - yMin) };
|
|
128
|
+
|
|
129
|
+
return { positions, bbox };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Compute a layered DAG layout using dagre. Pure function: no DOM, no React,
|
|
134
|
+
* no d3 globals. Returns a Map of node.id -> { x, y } in graph coordinates.
|
|
135
|
+
*
|
|
136
|
+
* Returns null if dagre.layout throws (e.g. an unexpected cycle). Callers
|
|
137
|
+
* should fall back to their previous layout in that case.
|
|
138
|
+
*/
|
|
139
|
+
export function computeLayeredLayout(
|
|
140
|
+
nodes: D3Node[],
|
|
141
|
+
links: D3Link[],
|
|
142
|
+
opts: LayeredLayoutOptions,
|
|
143
|
+
): Map<string, LayeredLayoutPosition> | null {
|
|
144
|
+
if (nodes.length === 0) return new Map();
|
|
145
|
+
const result = runDagreOnce(nodes, links, opts);
|
|
146
|
+
return result ? result.positions : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Compute a layered layout, then iteratively re-run dagre with adjusted
|
|
151
|
+
* `nodesep`/`ranksep` until the bounding-box aspect ratio is within
|
|
152
|
+
* `tolerance` of `targetAspectRatio` (or `maxIterations` is reached).
|
|
153
|
+
*
|
|
154
|
+
* Degenerate cases (empty graph, single-rank graph where one axis has
|
|
155
|
+
* zero extent, missing target ratio) skip fitting and return the
|
|
156
|
+
* single-pass result.
|
|
157
|
+
*/
|
|
158
|
+
export function fitLayeredLayoutToAspectRatio(
|
|
159
|
+
nodes: D3Node[],
|
|
160
|
+
links: D3Link[],
|
|
161
|
+
opts: FitLayeredLayoutOptions,
|
|
162
|
+
): Map<string, LayeredLayoutPosition> | null {
|
|
163
|
+
if (nodes.length === 0) return new Map();
|
|
164
|
+
if (!Number.isFinite(opts.targetAspectRatio) || opts.targetAspectRatio <= 0) {
|
|
165
|
+
return computeLayeredLayout(nodes, links, opts);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const maxIterations = opts.maxIterations ?? DEFAULT_MAX_ITERATIONS;
|
|
169
|
+
const tolerance = opts.tolerance ?? DEFAULT_TOLERANCE;
|
|
170
|
+
const rankdir = opts.rankdir ?? DEFAULT_RANKDIR;
|
|
171
|
+
const isHorizontalFlow = rankdir === "LR" || rankdir === "RL";
|
|
172
|
+
|
|
173
|
+
let nodesep = opts.nodesep ?? DEFAULT_NODESEP;
|
|
174
|
+
let ranksep = opts.ranksep ?? DEFAULT_RANKSEP;
|
|
175
|
+
|
|
176
|
+
let best: DagreRunResult | null = null;
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
179
|
+
const result = runDagreOnce(nodes, links, {
|
|
180
|
+
...opts,
|
|
181
|
+
nodesep,
|
|
182
|
+
ranksep,
|
|
183
|
+
});
|
|
184
|
+
if (!result) return best ? best.positions : null;
|
|
185
|
+
best = result;
|
|
186
|
+
|
|
187
|
+
if (result.positions.size <= 1) return result.positions;
|
|
188
|
+
|
|
189
|
+
const { width, height } = result.bbox;
|
|
190
|
+
if (width === 0 || height === 0) return result.positions;
|
|
191
|
+
|
|
192
|
+
const currentAspect = width / height;
|
|
193
|
+
const ratio = opts.targetAspectRatio / currentAspect;
|
|
194
|
+
|
|
195
|
+
if (Math.abs(ratio - 1) < tolerance) return result.positions;
|
|
196
|
+
|
|
197
|
+
const factor = clamp(Math.sqrt(ratio), MIN_FACTOR, MAX_FACTOR);
|
|
198
|
+
|
|
199
|
+
if (isHorizontalFlow) {
|
|
200
|
+
ranksep = clamp(ranksep * factor, MIN_RANKSEP, MAX_RANKSEP);
|
|
201
|
+
nodesep = clamp(nodesep / factor, MIN_NODESEP, MAX_NODESEP);
|
|
202
|
+
} else {
|
|
203
|
+
ranksep = clamp(ranksep / factor, MIN_RANKSEP, MAX_RANKSEP);
|
|
204
|
+
nodesep = clamp(nodesep * factor, MIN_NODESEP, MAX_NODESEP);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return best ? best.positions : null;
|
|
96
209
|
}
|
package/src/hooks/index.ts
CHANGED
|
@@ -37,6 +37,8 @@ export * from "./usePushNotifications";
|
|
|
37
37
|
export * from "./useSocket";
|
|
38
38
|
export {
|
|
39
39
|
computeLayeredLayout,
|
|
40
|
+
fitLayeredLayoutToAspectRatio,
|
|
41
|
+
type FitLayeredLayoutOptions,
|
|
40
42
|
type LayeredLayoutOptions,
|
|
41
43
|
type LayeredLayoutPosition,
|
|
42
44
|
type LayeredRankDir,
|
|
@@ -5,7 +5,28 @@ import { Loader2 } from "lucide-react";
|
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
6
6
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
7
7
|
import { D3Link, D3Node } from "../interfaces";
|
|
8
|
-
import { computeLayeredLayout, type LayeredRankDir } from "./computeLayeredLayout";
|
|
8
|
+
import { computeLayeredLayout, fitLayeredLayoutToAspectRatio, type LayeredRankDir } from "./computeLayeredLayout";
|
|
9
|
+
|
|
10
|
+
// Heuristic estimate of a node label's rendered width, used to size the
|
|
11
|
+
// force-collision radius so that long titles like "Investigative Journalism
|
|
12
|
+
// Class" do not overlap neighbouring circles. Mirrors the constants in
|
|
13
|
+
// computeLayeredLayout.ts — the small duplication is intentional, the
|
|
14
|
+
// heuristic may evolve per-layout-context.
|
|
15
|
+
const TITLE_PX_PER_CHAR_16 = 8;
|
|
16
|
+
const NAME_PX_PER_CHAR_12 = 6.5;
|
|
17
|
+
const NAME_PX_PER_CHAR_16_BOLD = 8;
|
|
18
|
+
const SUBTITLE_PX_PER_CHAR_11 = 6;
|
|
19
|
+
const LABEL_PADDING_PX = 16;
|
|
20
|
+
|
|
21
|
+
function estimateLabelWidth(node: D3Node): number {
|
|
22
|
+
if (node.subtitle) {
|
|
23
|
+
const titleWidth = (node.name?.length ?? 0) * TITLE_PX_PER_CHAR_16;
|
|
24
|
+
const subtitleWidth = node.subtitle.length * SUBTITLE_PX_PER_CHAR_11;
|
|
25
|
+
return Math.max(titleWidth, subtitleWidth) + LABEL_PADDING_PX;
|
|
26
|
+
}
|
|
27
|
+
const perChar = node.bold ? NAME_PX_PER_CHAR_16_BOLD : NAME_PX_PER_CHAR_12;
|
|
28
|
+
return (node.name?.length ?? 0) * perChar + LABEL_PADDING_PX;
|
|
29
|
+
}
|
|
9
30
|
|
|
10
31
|
/**
|
|
11
32
|
* Custom hook for D3 graph visualization with larger circles and more interactive features
|
|
@@ -22,6 +43,7 @@ export function useCustomD3Graph(
|
|
|
22
43
|
rankdir?: LayeredRankDir;
|
|
23
44
|
nodesep?: number;
|
|
24
45
|
ranksep?: number;
|
|
46
|
+
fitContainer?: boolean;
|
|
25
47
|
};
|
|
26
48
|
},
|
|
27
49
|
loadingNodeIds?: Set<string>,
|
|
@@ -244,13 +266,23 @@ export function useCustomD3Graph(
|
|
|
244
266
|
|
|
245
267
|
if (layoutMode === "layered") {
|
|
246
268
|
const layeredOpts = options?.layered ?? {};
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
269
|
+
const useFit = layeredOpts.fitContainer === true && width > 0 && height > 0;
|
|
270
|
+
const positions = useFit
|
|
271
|
+
? fitLayeredLayoutToAspectRatio(visibleNodes, visibleLinks, {
|
|
272
|
+
rankdir: layeredOpts.rankdir ?? "LR",
|
|
273
|
+
nodesep: layeredOpts.nodesep,
|
|
274
|
+
ranksep: layeredOpts.ranksep,
|
|
275
|
+
minNodeWidth: nodeRadius * 2,
|
|
276
|
+
minNodeHeight: nodeRadius * 2,
|
|
277
|
+
targetAspectRatio: width / height,
|
|
278
|
+
})
|
|
279
|
+
: computeLayeredLayout(visibleNodes, visibleLinks, {
|
|
280
|
+
rankdir: layeredOpts.rankdir ?? "LR",
|
|
281
|
+
nodesep: layeredOpts.nodesep,
|
|
282
|
+
ranksep: layeredOpts.ranksep,
|
|
283
|
+
minNodeWidth: nodeRadius * 2,
|
|
284
|
+
minNodeHeight: nodeRadius * 2,
|
|
285
|
+
});
|
|
254
286
|
|
|
255
287
|
if (positions) {
|
|
256
288
|
visibleNodes.forEach((node) => {
|
|
@@ -445,12 +477,15 @@ export function useCustomD3Graph(
|
|
|
445
477
|
.distance(nodeRadius * 3)
|
|
446
478
|
.strength(0.1),
|
|
447
479
|
)
|
|
448
|
-
.force("charge", d3.forceManyBody().strength(-
|
|
449
|
-
.force(
|
|
480
|
+
.force("charge", d3.forceManyBody().strength(-2500).distanceMax(800))
|
|
481
|
+
.force(
|
|
482
|
+
"collision",
|
|
483
|
+
d3.forceCollide<D3Node>().radius((d) => Math.max(nodeRadius * 1.1, estimateLabelWidth(d) / 2 + 8)),
|
|
484
|
+
)
|
|
450
485
|
.force("center", d3.forceCenter(width / 2, height / 2).strength(0.1));
|
|
451
486
|
|
|
452
487
|
simulation.stop();
|
|
453
|
-
for (let i = 0; i <
|
|
488
|
+
for (let i = 0; i < 300; i++) {
|
|
454
489
|
simulation.tick();
|
|
455
490
|
}
|
|
456
491
|
|
|
@@ -785,6 +820,7 @@ export function useCustomD3Graph(
|
|
|
785
820
|
options?.layered?.rankdir,
|
|
786
821
|
options?.layered?.nodesep,
|
|
787
822
|
options?.layered?.ranksep,
|
|
823
|
+
options?.layered?.fitContainer,
|
|
788
824
|
loadingNodeIds,
|
|
789
825
|
onNodeClick,
|
|
790
826
|
]);
|