@behindthemusictree/genre-tree-view 6.2.0 → 6.5.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.cjs +447 -178
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -3
- package/dist/index.d.ts +23 -3
- package/dist/index.js +454 -185
- package/dist/index.js.map +1 -1
- package/dist/styles.css +104 -47
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -131,6 +131,9 @@ var MIN_NODE_WIDTH = 180;
|
|
|
131
131
|
var MAX_NODE_WIDTH = 700;
|
|
132
132
|
var MIN_NODE_HEIGHT = 35;
|
|
133
133
|
var MAX_NODE_HEIGHT = 120;
|
|
134
|
+
var NODE_SIZE_REFERENCE_COUNT = 100;
|
|
135
|
+
var HIGH_DENSITY_NODE_MAX_FLOOR_SCALE = 1.5;
|
|
136
|
+
var LOW_DENSITY_DOT_MAX_RADIUS = 70;
|
|
134
137
|
var TOOLBAR_MENU_X_GAP = 4;
|
|
135
138
|
var ACTIONS_OVERLAY_WIDTH = MENU_WIDTH + TOOLBAR_MENU_X_GAP;
|
|
136
139
|
var ACTIONS_OVERLAY_HEIGHT = RECT_BASE_DIMENSIONS.HEIGHT * 7;
|
|
@@ -145,23 +148,47 @@ var HORIZONTAL_SEPARATION_BETWEEN_NODES = (NODE_DIMENSIONS.WIDTH + HORIZONTAL_SE
|
|
|
145
148
|
var VERTICAL_SEPARATION_BETWEEN_NODES = NODE_DIMENSIONS.HEIGHT + VERTICAL_SEPARATION_BETWEEN_RECTANGLES;
|
|
146
149
|
var VERTICAL_ORIENTATION_DEPTH_SEPARATION = VERTICAL_SEPARATION_BETWEEN_NODES * 2 * DEPTH_AXIS_SPACING_FACTOR;
|
|
147
150
|
var SIBLING_SEPARATION_BETWEEN_NODES = MAX_NODE_WIDTH + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES;
|
|
148
|
-
function
|
|
149
|
-
|
|
151
|
+
function getDensityAdjustedMax(baseMax, min, nodeCount) {
|
|
152
|
+
const safeNodeCount = Number.isFinite(nodeCount) ? Math.max(1, nodeCount) : 1;
|
|
153
|
+
const availableRangeScale = Math.min(1, NODE_SIZE_REFERENCE_COUNT / safeNodeCount);
|
|
154
|
+
const denseTreeFloor = min * HIGH_DENSITY_NODE_MAX_FLOOR_SCALE;
|
|
155
|
+
return denseTreeFloor + (baseMax - denseTreeFloor) * availableRangeScale;
|
|
156
|
+
}
|
|
157
|
+
function getDensityAdjustedDotMax(nodeCount) {
|
|
158
|
+
const safeNodeCount = Number.isFinite(nodeCount) ? Math.max(1, nodeCount) : 1;
|
|
159
|
+
const inverseDensityRadius = NODE_DOT_MAX_RADIUS * NODE_SIZE_REFERENCE_COUNT / safeNodeCount;
|
|
160
|
+
const denseTreeFloor = NODE_DOT_MIN_RADIUS * HIGH_DENSITY_NODE_MAX_FLOOR_SCALE;
|
|
161
|
+
return Math.max(denseTreeFloor, Math.min(LOW_DENSITY_DOT_MAX_RADIUS, inverseDensityRadius));
|
|
162
|
+
}
|
|
163
|
+
function getItemCountRange(nodes, options = {}) {
|
|
150
164
|
const counts = nodes.map((node) => node.itemCount);
|
|
151
|
-
|
|
165
|
+
const validScaleMax = options.itemCountScaleMax !== void 0 && Number.isFinite(options.itemCountScaleMax) && options.itemCountScaleMax >= 0;
|
|
166
|
+
const nodeCount = options.nodeCountForSizing ?? nodes.length;
|
|
167
|
+
const observedMin = counts.length === 0 ? 0 : Math.min(...counts);
|
|
168
|
+
const observedMax = counts.length === 0 ? 0 : Math.max(...counts);
|
|
169
|
+
const includeDensityCaps = options.nodeCountForSizing !== void 0 || validScaleMax;
|
|
170
|
+
return {
|
|
171
|
+
min: validScaleMax ? 0 : observedMin,
|
|
172
|
+
max: validScaleMax ? options.itemCountScaleMax : observedMax,
|
|
173
|
+
...includeDensityCaps && {
|
|
174
|
+
maxNodeWidth: getDensityAdjustedMax(MAX_NODE_WIDTH, MIN_NODE_WIDTH, nodeCount),
|
|
175
|
+
maxNodeHeight: getDensityAdjustedMax(MAX_NODE_HEIGHT, MIN_NODE_HEIGHT, nodeCount),
|
|
176
|
+
maxDotRadius: getDensityAdjustedDotMax(nodeCount)
|
|
177
|
+
}
|
|
178
|
+
};
|
|
152
179
|
}
|
|
153
180
|
function logarithmicPosition(itemCount, range) {
|
|
154
181
|
const { min, max } = range;
|
|
155
182
|
const logMin = Math.log1p(Math.max(min, 0));
|
|
156
183
|
const logMax = Math.log1p(Math.max(max, 0));
|
|
157
184
|
const logValue = Math.log1p(Math.max(itemCount, 0));
|
|
158
|
-
return logMax > logMin ? (logValue - logMin) / (logMax - logMin) : 0;
|
|
185
|
+
return logMax > logMin ? Math.max(0, Math.min(1, (logValue - logMin) / (logMax - logMin))) : 0;
|
|
159
186
|
}
|
|
160
187
|
function calculateNodeDimensions(itemCount, range) {
|
|
161
188
|
const t = logarithmicPosition(itemCount, range);
|
|
162
189
|
return {
|
|
163
|
-
WIDTH: Math.round(MIN_NODE_WIDTH + t * (MAX_NODE_WIDTH - MIN_NODE_WIDTH)),
|
|
164
|
-
HEIGHT: Math.round(MIN_NODE_HEIGHT + t * (MAX_NODE_HEIGHT - MIN_NODE_HEIGHT))
|
|
190
|
+
WIDTH: Math.round(MIN_NODE_WIDTH + t * ((range.maxNodeWidth ?? MAX_NODE_WIDTH) - MIN_NODE_WIDTH)),
|
|
191
|
+
HEIGHT: Math.round(MIN_NODE_HEIGHT + t * ((range.maxNodeHeight ?? MAX_NODE_HEIGHT) - MIN_NODE_HEIGHT))
|
|
165
192
|
};
|
|
166
193
|
}
|
|
167
194
|
var NODE_MIN_BORDER_WIDTH = 1;
|
|
@@ -206,7 +233,7 @@ var NODE_DOT_LABEL_GAP = 6;
|
|
|
206
233
|
var NODE_LAYOUT_SLOT_RADIUS = 8;
|
|
207
234
|
function calculateNodeDotRadius(itemCount, range) {
|
|
208
235
|
const minArea = NODE_DOT_MIN_RADIUS ** 2;
|
|
209
|
-
const maxArea = NODE_DOT_MAX_RADIUS ** 2;
|
|
236
|
+
const maxArea = (range.maxDotRadius ?? NODE_DOT_MAX_RADIUS) ** 2;
|
|
210
237
|
return Math.sqrt(minArea + logarithmicPosition(itemCount, range) * (maxArea - minArea));
|
|
211
238
|
}
|
|
212
239
|
var WHEEL_RADIUS = 260;
|
|
@@ -241,19 +268,22 @@ var PAN_MIN_VISIBLE_PX = 80;
|
|
|
241
268
|
var PAN_ZOOM_SETTLE_MS = 100;
|
|
242
269
|
var DEFAULT_VIEWPORT_CENTER_GUARD_MS = 1e3;
|
|
243
270
|
var INFO_PANEL_WIDTH = 280;
|
|
271
|
+
var INFO_PANEL_SHEET_MAX_WIDTH = 767;
|
|
272
|
+
var INFO_PANEL_SHEET_PEEK_FRACTION = 0.4;
|
|
273
|
+
var INFO_PANEL_SHEET_DRAG_THRESHOLD_PX = 32;
|
|
244
274
|
|
|
245
275
|
// src/NodeHelper.tsx
|
|
246
276
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
247
277
|
function buildTreeHierarchyStructure(d3Lib, nodes) {
|
|
248
278
|
return d3Lib.stratify().id((d) => d.id).parentId((d) => d.parentId)(nodes);
|
|
249
279
|
}
|
|
250
|
-
function calculateLocalRootDimensions(d3Lib, nodes) {
|
|
280
|
+
function calculateLocalRootDimensions(d3Lib, nodes, options = {}) {
|
|
251
281
|
const root = buildTreeHierarchyStructure(d3Lib, nodes);
|
|
252
|
-
const range = getItemCountRange(root.descendants().map((d) => d.data));
|
|
282
|
+
const range = getItemCountRange(root.descendants().map((d) => d.data), options);
|
|
253
283
|
return calculateNodeDimensions(root.data.itemCount, range);
|
|
254
284
|
}
|
|
255
|
-
function calculateRootAnchorClearance(d3Lib, nodes, rootItemCount, rootItemCountRange, axis) {
|
|
256
|
-
const localExtent = calculateLocalRootDimensions(d3Lib, nodes)[axis];
|
|
285
|
+
function calculateRootAnchorClearance(d3Lib, nodes, rootItemCount, rootItemCountRange, axis, options = {}) {
|
|
286
|
+
const localExtent = calculateLocalRootDimensions(d3Lib, nodes, options)[axis];
|
|
257
287
|
const displayExtent = calculateNodeDimensions(rootItemCount, rootItemCountRange)[axis];
|
|
258
288
|
return (localExtent + displayExtent) / 2;
|
|
259
289
|
}
|
|
@@ -337,13 +367,31 @@ function appendPaths(d3Lib, svg, treeData, itemCountRange, orientation = "horizo
|
|
|
337
367
|
}
|
|
338
368
|
|
|
339
369
|
// src/tree-renderer.ts
|
|
340
|
-
function
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
const
|
|
370
|
+
function getLayoutSpacings(itemCountRange, depthSpacingScale = 1) {
|
|
371
|
+
const maxWidth = itemCountRange.maxNodeWidth ?? MAX_NODE_WIDTH;
|
|
372
|
+
const maxHeight = itemCountRange.maxNodeHeight ?? MAX_NODE_HEIGHT;
|
|
373
|
+
const verticalSeparation = maxHeight + VERTICAL_SEPARATION_BETWEEN_RECTANGLES;
|
|
374
|
+
return {
|
|
375
|
+
horizontalDepth: (maxWidth + ACTIONS_OVERLAY_WIDTH + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES) * DEPTH_AXIS_SPACING_FACTOR * depthSpacingScale,
|
|
376
|
+
verticalBreadth: verticalSeparation,
|
|
377
|
+
verticalDepth: verticalSeparation * 2 * DEPTH_AXIS_SPACING_FACTOR * depthSpacingScale,
|
|
378
|
+
verticalSibling: maxWidth + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function calculateSvgDimensions(d3Lib, treeData, orientation = "horizontal", hideRoot = false, depthSpacingScale = 1, suppliedItemCountRange) {
|
|
382
|
+
const treeNodes = treeData.descendants();
|
|
383
|
+
const nodes = treeNodes;
|
|
384
|
+
const itemCountRange = suppliedItemCountRange ?? getItemCountRange(
|
|
385
|
+
treeNodes.map((d) => d.data)
|
|
386
|
+
);
|
|
387
|
+
const maxNodeDimensions = {
|
|
388
|
+
WIDTH: itemCountRange.maxNodeWidth ?? MAX_NODE_WIDTH,
|
|
389
|
+
HEIGHT: itemCountRange.maxNodeHeight ?? MAX_NODE_HEIGHT
|
|
390
|
+
};
|
|
344
391
|
const maximumLevel = d3Lib.max(nodes, (d) => d.depth);
|
|
345
|
-
const
|
|
346
|
-
const
|
|
392
|
+
const layoutSpacings = getLayoutSpacings(itemCountRange, depthSpacingScale);
|
|
393
|
+
const depthSeparation = layoutSpacings.verticalDepth;
|
|
394
|
+
const depthSeparationHorizontal = layoutSpacings.horizontalDepth;
|
|
347
395
|
if (isVerticalOrientation(orientation)) {
|
|
348
396
|
const svgHeight2 = hideRoot ? maximumLevel * depthSeparation : maximumLevel * depthSeparation + maxNodeDimensions.HEIGHT + ACTIONS_OVERLAY_HEIGHT / 2;
|
|
349
397
|
const rootWidth = calculateNodeDimensions(
|
|
@@ -405,10 +453,15 @@ function calculateSvgDimensions(d3Lib, treeData, orientation = "horizontal", hid
|
|
|
405
453
|
const svgWidth = maximumLevel * depthSeparationHorizontal + maxNodeDimensions.WIDTH + ACTIONS_OVERLAY_WIDTH;
|
|
406
454
|
return { svgWidth, svgHeight, highestVerticalCoordinate, rootDepthOffset: 0 };
|
|
407
455
|
}
|
|
408
|
-
function setupTreeLayout(d3Lib, treeData, highestVerticalCoordinate, orientation = "horizontal", rootDepthOffset = 0, svgWidth = 0, depthSpacingScale = 1) {
|
|
456
|
+
function setupTreeLayout(d3Lib, treeData, highestVerticalCoordinate, orientation = "horizontal", rootDepthOffset = 0, svgWidth = 0, depthSpacingScale = 1, suppliedItemCountRange) {
|
|
457
|
+
const treeNodes = treeData.descendants();
|
|
458
|
+
const itemCountRange = suppliedItemCountRange ?? getItemCountRange(
|
|
459
|
+
treeNodes.map((d) => d.data)
|
|
460
|
+
);
|
|
461
|
+
const layoutSpacings = getLayoutSpacings(itemCountRange, depthSpacingScale);
|
|
409
462
|
if (isVerticalOrientation(orientation)) {
|
|
410
463
|
const maximumLevel = d3Lib.max(treeData.descendants(), (d) => d.depth);
|
|
411
|
-
const depthSeparation =
|
|
464
|
+
const depthSeparation = layoutSpacings.verticalDepth;
|
|
412
465
|
treeData.each(function(d) {
|
|
413
466
|
d.x = d.x + highestVerticalCoordinate;
|
|
414
467
|
d.y = depthAxisSign(orientation) === -1 ? (maximumLevel - d.depth) * depthSeparation : d.depth * depthSeparation + rootDepthOffset;
|
|
@@ -430,24 +483,30 @@ function setupTreeLayout(d3Lib, treeData, highestVerticalCoordinate, orientation
|
|
|
430
483
|
});
|
|
431
484
|
return treeData;
|
|
432
485
|
}
|
|
433
|
-
function createTreeLayout(d3Lib, root, orientation = "horizontal", depthSpacingScale = 1) {
|
|
486
|
+
function createTreeLayout(d3Lib, root, orientation = "horizontal", depthSpacingScale = 1, suppliedItemCountRange) {
|
|
487
|
+
const nodes = root.descendants();
|
|
488
|
+
const itemCountRange = suppliedItemCountRange ?? getItemCountRange(
|
|
489
|
+
nodes.map((d) => d.data)
|
|
490
|
+
);
|
|
491
|
+
const layoutSpacings = getLayoutSpacings(itemCountRange, depthSpacingScale);
|
|
434
492
|
const nodeSize = isVerticalOrientation(orientation) ? [
|
|
435
|
-
|
|
436
|
-
|
|
493
|
+
layoutSpacings.verticalSibling,
|
|
494
|
+
layoutSpacings.verticalDepth
|
|
437
495
|
] : [
|
|
438
|
-
|
|
439
|
-
|
|
496
|
+
layoutSpacings.verticalBreadth,
|
|
497
|
+
layoutSpacings.horizontalDepth
|
|
440
498
|
];
|
|
441
499
|
const treeLayout = d3Lib.tree().nodeSize(nodeSize).separation(() => 1);
|
|
442
500
|
return treeLayout(root);
|
|
443
501
|
}
|
|
444
|
-
function renderTree(d3Lib, svgRef, treeData, svgWidth, svgHeight, reparentingNodeId, reparentForbiddenIds, rootColor, callbacks, orientation = "horizontal", hideRoot = false, selectedNodeId = null) {
|
|
502
|
+
function renderTree(d3Lib, svgRef, treeData, svgWidth, svgHeight, reparentingNodeId, reparentForbiddenIds, rootColor, callbacks, orientation = "horizontal", hideRoot = false, selectedNodeId = null, suppliedItemCountRange) {
|
|
445
503
|
const { onReparentTargetSelect, onNodeClick } = callbacks;
|
|
446
504
|
if (!svgRef.current) {
|
|
447
505
|
throw new Error("SVG reference is null");
|
|
448
506
|
}
|
|
449
|
-
const
|
|
450
|
-
|
|
507
|
+
const treeNodes = treeData.descendants();
|
|
508
|
+
const itemCountRange = suppliedItemCountRange ?? getItemCountRange(
|
|
509
|
+
treeNodes.map((d) => d.data)
|
|
451
510
|
);
|
|
452
511
|
const svg = d3Lib.select(svgRef.current).append("svg").attr("width", svgWidth).attr("height", svgHeight).style("overflow", "visible").append("g");
|
|
453
512
|
const safeRootId = treeData.data.id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
@@ -595,7 +654,7 @@ function renderTree(d3Lib, svgRef, treeData, svgWidth, svgHeight, reparentingNod
|
|
|
595
654
|
}
|
|
596
655
|
|
|
597
656
|
// src/FloatingControls.tsx
|
|
598
|
-
import {
|
|
657
|
+
import { ZoomControls } from "@behindthemusictree/ui";
|
|
599
658
|
|
|
600
659
|
// src/BusyIndicator.tsx
|
|
601
660
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
@@ -608,7 +667,7 @@ function BusyIndicator() {
|
|
|
608
667
|
|
|
609
668
|
// src/FloatingControls.tsx
|
|
610
669
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
611
|
-
function FloatingControls({ panZoom, onFit, orientation, leading, busy }) {
|
|
670
|
+
function FloatingControls({ panZoom, onFit, orientation, zoomControls, leading, busy }) {
|
|
612
671
|
return /* @__PURE__ */ jsxs3(
|
|
613
672
|
"div",
|
|
614
673
|
{
|
|
@@ -619,31 +678,18 @@ function FloatingControls({ panZoom, onFit, orientation, leading, busy }) {
|
|
|
619
678
|
children: [
|
|
620
679
|
busy && /* @__PURE__ */ jsx3(BusyIndicator, {}),
|
|
621
680
|
leading && /* @__PURE__ */ jsx3("div", { className: "gtv-floating-controls-leading", children: leading }),
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
/* @__PURE__ */ jsx3(
|
|
635
|
-
"button",
|
|
636
|
-
{
|
|
637
|
-
type: "button",
|
|
638
|
-
className: ["gtv-zoom-btn", !panZoom.canZoomOut && "gtv-zoom-btn--disabled"].filter(Boolean).join(" "),
|
|
639
|
-
disabled: !panZoom.canZoomOut,
|
|
640
|
-
onClick: panZoom.zoomOut,
|
|
641
|
-
"aria-label": "Zoom out",
|
|
642
|
-
children: /* @__PURE__ */ jsx3(MdZoomOut, { className: "gtv-icon", size: 18 })
|
|
643
|
-
}
|
|
644
|
-
),
|
|
645
|
-
/* @__PURE__ */ jsx3("button", { type: "button", className: "gtv-zoom-btn", onClick: onFit, "aria-label": "Fit to frame", children: /* @__PURE__ */ jsx3(MdFitScreen, { className: "gtv-icon", size: 18 }) })
|
|
646
|
-
] })
|
|
681
|
+
zoomControls !== "never" && /* @__PURE__ */ jsx3(
|
|
682
|
+
ZoomControls,
|
|
683
|
+
{
|
|
684
|
+
onZoomIn: panZoom.zoomIn,
|
|
685
|
+
onZoomOut: panZoom.zoomOut,
|
|
686
|
+
onFit,
|
|
687
|
+
canZoomIn: panZoom.canZoomIn,
|
|
688
|
+
canZoomOut: panZoom.canZoomOut,
|
|
689
|
+
orientation,
|
|
690
|
+
className: zoomControls === "auto" ? "gtv-zoom-controls gtv-zoom-controls--auto" : "gtv-zoom-controls"
|
|
691
|
+
}
|
|
692
|
+
)
|
|
647
693
|
]
|
|
648
694
|
}
|
|
649
695
|
);
|
|
@@ -707,6 +753,18 @@ function computeFitScale(contentWidth, contentHeight, viewportWidth, viewportHei
|
|
|
707
753
|
);
|
|
708
754
|
}
|
|
709
755
|
|
|
756
|
+
// src/info-panel-geometry.ts
|
|
757
|
+
function resolveInfoPanelSide() {
|
|
758
|
+
return "left";
|
|
759
|
+
}
|
|
760
|
+
function resolveInfoPanelObscuredArea(element, viewport, panelWidth) {
|
|
761
|
+
if (!element || !viewport) return null;
|
|
762
|
+
if (viewport.clientWidth > 0 && viewport.clientWidth <= INFO_PANEL_SHEET_MAX_WIDTH) {
|
|
763
|
+
return { side: "bottom", height: viewport.clientHeight * INFO_PANEL_SHEET_PEEK_FRACTION };
|
|
764
|
+
}
|
|
765
|
+
return { width: panelWidth, side: resolveInfoPanelSide() };
|
|
766
|
+
}
|
|
767
|
+
|
|
710
768
|
// src/use-pan-zoom.ts
|
|
711
769
|
function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
712
770
|
const [panX, setPanX] = useState(0);
|
|
@@ -717,6 +775,17 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
717
775
|
const settleTimerRef = useRef(null);
|
|
718
776
|
const stageAnimationRef = useRef(null);
|
|
719
777
|
const defaultViewportRef = useRef(options.defaultViewport);
|
|
778
|
+
const operationRef = useRef(0);
|
|
779
|
+
const restoredSelectionRef = useRef(void 0);
|
|
780
|
+
const restoredRevisionRef = useRef(void 0);
|
|
781
|
+
const restoredFrameRef = useRef(void 0);
|
|
782
|
+
const pendingRestoreRef = useRef(null);
|
|
783
|
+
const measuredFrameRef = useRef(void 0);
|
|
784
|
+
const fittedFrameRef = useRef(null);
|
|
785
|
+
const frameElementsRef = useRef([]);
|
|
786
|
+
const retryFrameRef = useRef(() => {
|
|
787
|
+
});
|
|
788
|
+
const pendingCenterRef = useRef(null);
|
|
720
789
|
const centerGuardUntilRef = useRef(0);
|
|
721
790
|
const onViewportChangeRef = useRef(options.onViewportChange);
|
|
722
791
|
useEffect(() => {
|
|
@@ -790,6 +859,7 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
790
859
|
}, [viewportRef]);
|
|
791
860
|
const zoomAtPoint = useCallback(
|
|
792
861
|
(computeNewScale, clientX, clientY) => {
|
|
862
|
+
operationRef.current += 1;
|
|
793
863
|
const viewport = viewportRef.current;
|
|
794
864
|
if (!viewport) {
|
|
795
865
|
return;
|
|
@@ -873,19 +943,26 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
873
943
|
const viewport = viewportRef.current;
|
|
874
944
|
if (!viewport) return;
|
|
875
945
|
const observer = new ResizeObserver((entries) => {
|
|
876
|
-
const entry = entries[0];
|
|
877
|
-
if (!entry)
|
|
946
|
+
const entry = entries?.[0];
|
|
947
|
+
if (!entry) {
|
|
948
|
+
retryFrameRef.current();
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
878
951
|
const { width, height } = entry.contentRect;
|
|
879
952
|
const previous = viewportSizeRef.current;
|
|
880
953
|
viewportSizeRef.current = { width, height };
|
|
881
|
-
if (!previous)
|
|
954
|
+
if (!previous || previous.width <= 0 || previous.height <= 0) {
|
|
955
|
+
retryFrameRef.current();
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
882
958
|
const dx = (width - previous.width) / 2;
|
|
883
959
|
const dy = (height - previous.height) / 2;
|
|
884
960
|
if (dx === 0 && dy === 0) return;
|
|
885
961
|
const view = viewRef.current;
|
|
886
962
|
view.x = clampPanAxis(view.x + dx, view.k, "x");
|
|
887
963
|
view.y = clampPanAxis(view.y + dy, view.k, "y");
|
|
888
|
-
commitView();
|
|
964
|
+
commitView(false);
|
|
965
|
+
retryFrameRef.current();
|
|
889
966
|
});
|
|
890
967
|
observer.observe(viewport);
|
|
891
968
|
return () => observer.disconnect();
|
|
@@ -896,6 +973,7 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
896
973
|
return;
|
|
897
974
|
}
|
|
898
975
|
const handleWheel = (event) => {
|
|
976
|
+
operationRef.current += 1;
|
|
899
977
|
if (event.target.closest(".gtv-info-panel") !== null) {
|
|
900
978
|
return;
|
|
901
979
|
}
|
|
@@ -934,6 +1012,7 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
934
1012
|
}, [viewportRef, wheelZoom]);
|
|
935
1013
|
const zoomByButton = useCallback(
|
|
936
1014
|
(direction) => {
|
|
1015
|
+
operationRef.current += 1;
|
|
937
1016
|
const viewport = viewportRef.current;
|
|
938
1017
|
if (!viewport) return;
|
|
939
1018
|
const rect = viewport.getBoundingClientRect();
|
|
@@ -948,13 +1027,14 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
948
1027
|
);
|
|
949
1028
|
const fitToFrame = useCallback(
|
|
950
1029
|
(elements, { silent = false } = {}) => {
|
|
1030
|
+
if (!silent) operationRef.current += 1;
|
|
1031
|
+
frameElementsRef.current = elements;
|
|
951
1032
|
const viewport = viewportRef.current;
|
|
952
1033
|
const present = elements.filter((el) => Boolean(el));
|
|
953
|
-
if (!viewport || present.length === 0) return;
|
|
1034
|
+
if (!viewport || present.length === 0) return false;
|
|
954
1035
|
const viewportRect = viewport.getBoundingClientRect();
|
|
955
|
-
if (viewportRect.width <= 0 || viewportRect.height <= 0) return;
|
|
956
|
-
|
|
957
|
-
if (viewportRect.width <= ZOOM_FIT_PADDING * 2 || viewportRect.height <= ZOOM_FIT_PADDING * 2) return;
|
|
1036
|
+
if (viewportRect.width <= 0 || viewportRect.height <= 0) return false;
|
|
1037
|
+
if (viewportRect.width <= ZOOM_FIT_PADDING * 2 || viewportRect.height <= ZOOM_FIT_PADDING * 2) return false;
|
|
958
1038
|
const rects = present.map((el) => el.getBoundingClientRect());
|
|
959
1039
|
const contentLeft = Math.min(...rects.map((r) => r.left));
|
|
960
1040
|
const contentTop = Math.min(...rects.map((r) => r.top));
|
|
@@ -963,7 +1043,7 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
963
1043
|
const view = viewRef.current;
|
|
964
1044
|
const contentWidth = (contentRight - contentLeft) / view.k;
|
|
965
1045
|
const contentHeight = (contentBottom - contentTop) / view.k;
|
|
966
|
-
if (contentWidth <= 0 || contentHeight <= 0) return;
|
|
1046
|
+
if (contentWidth <= 0 || contentHeight <= 0) return false;
|
|
967
1047
|
const contentOriginX = (contentLeft - viewportRect.left - view.x) / view.k;
|
|
968
1048
|
const contentOriginY = (contentTop - viewportRect.top - view.y) / view.k;
|
|
969
1049
|
contentBoundsRef.current = {
|
|
@@ -982,6 +1062,41 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
982
1062
|
const newMinScale = Math.min(ZOOM_MIN_SCALE, fitScale);
|
|
983
1063
|
minScaleRef.current = newMinScale;
|
|
984
1064
|
setMinScale(newMinScale);
|
|
1065
|
+
measuredFrameRef.current = options.frameKey;
|
|
1066
|
+
const pending = pendingRestoreRef.current;
|
|
1067
|
+
if (pending && pending.operation !== operationRef.current) {
|
|
1068
|
+
pendingRestoreRef.current = null;
|
|
1069
|
+
pendingCenterRef.current = null;
|
|
1070
|
+
fittedFrameRef.current = { frame: options.frameKey };
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
if (pending && pending.frame === options.frameKey && pending.operation === operationRef.current) {
|
|
1074
|
+
pendingRestoreRef.current = null;
|
|
1075
|
+
restoredRevisionRef.current = pending.revision;
|
|
1076
|
+
restoredFrameRef.current = pending.frame;
|
|
1077
|
+
restoredSelectionRef.current = options.selectedNodeId;
|
|
1078
|
+
defaultViewportRef.current = void 0;
|
|
1079
|
+
viewRef.current = viewportToView({ ...pending.viewport, zoom: clampZoomScale(pending.viewport.zoom, newMinScale) }, viewportRect.width, viewportRect.height);
|
|
1080
|
+
fittedFrameRef.current = { frame: options.frameKey };
|
|
1081
|
+
pendingCenterRef.current = null;
|
|
1082
|
+
commitView(false);
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
if (silent && fittedFrameRef.current && fittedFrameRef.current.frame === options.frameKey) {
|
|
1086
|
+
const center2 = pendingCenterRef.current;
|
|
1087
|
+
pendingCenterRef.current = null;
|
|
1088
|
+
center2?.();
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
fittedFrameRef.current = { frame: options.frameKey };
|
|
1092
|
+
const center = pendingCenterRef.current;
|
|
1093
|
+
pendingCenterRef.current = null;
|
|
1094
|
+
if (silent && center && !defaultViewportRef.current) {
|
|
1095
|
+
center();
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
zoomAnimationRef.current = null;
|
|
1099
|
+
centerAnimationRef.current = null;
|
|
985
1100
|
const seed = defaultViewportRef.current;
|
|
986
1101
|
if (seed) {
|
|
987
1102
|
defaultViewportRef.current = void 0;
|
|
@@ -995,14 +1110,15 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
995
1110
|
view.y = seeded.y;
|
|
996
1111
|
centerGuardUntilRef.current = performance.now() + DEFAULT_VIEWPORT_CENTER_GUARD_MS;
|
|
997
1112
|
commitView(false);
|
|
998
|
-
return;
|
|
1113
|
+
return true;
|
|
999
1114
|
}
|
|
1000
1115
|
view.k = fitScale;
|
|
1001
1116
|
view.x = viewportRect.width / 2 - (contentOriginX + contentWidth / 2) * fitScale;
|
|
1002
1117
|
view.y = viewportRect.height / 2 - (contentOriginY + contentHeight / 2) * fitScale;
|
|
1003
1118
|
commitView(!silent);
|
|
1119
|
+
return true;
|
|
1004
1120
|
},
|
|
1005
|
-
[viewportRef, commitView]
|
|
1121
|
+
[viewportRef, commitView, options.frameKey, options.selectedNodeId]
|
|
1006
1122
|
);
|
|
1007
1123
|
const centerAnimationRef = useRef(null);
|
|
1008
1124
|
const centerAnimationFrameIdRef = useRef(null);
|
|
@@ -1036,7 +1152,19 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1036
1152
|
stepCenterAnimationRef.current = stepCenterAnimation;
|
|
1037
1153
|
}, [stepCenterAnimation]);
|
|
1038
1154
|
const centerOnElement = useCallback(
|
|
1039
|
-
(element, targetScale, obscured) => {
|
|
1155
|
+
(element, targetScale, obscured, automatic = false) => {
|
|
1156
|
+
if (automatic && restoredRevisionRef.current !== void 0 && restoredFrameRef.current === options.frameKey && restoredSelectionRef.current === options.selectedNodeId) return;
|
|
1157
|
+
if (!automatic) operationRef.current += 1;
|
|
1158
|
+
const viewportSize = viewportRef.current?.getBoundingClientRect();
|
|
1159
|
+
if (automatic && (measuredFrameRef.current !== options.frameKey || fittedFrameRef.current === null || !viewportSize?.width || !viewportSize.height)) {
|
|
1160
|
+
const operation = operationRef.current;
|
|
1161
|
+
pendingCenterRef.current = () => {
|
|
1162
|
+
if (operation !== operationRef.current) return;
|
|
1163
|
+
const currentElement = element?.id ? viewportRef.current?.querySelector(`#${CSS.escape(element.id)}`) : element;
|
|
1164
|
+
centerLatestRef.current(currentElement, targetScale, obscured ? resolveInfoPanelObscuredArea(currentElement, viewportRef.current, INFO_PANEL_WIDTH) : obscured, true);
|
|
1165
|
+
};
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1040
1168
|
const viewport = viewportRef.current;
|
|
1041
1169
|
if (!viewport || !element || performance.now() < centerGuardUntilRef.current) return;
|
|
1042
1170
|
const viewportRect = viewport.getBoundingClientRect();
|
|
@@ -1047,9 +1175,10 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1047
1175
|
const newScale = clampZoomScale(Math.max(targetScale, currentScale), minScaleRef.current);
|
|
1048
1176
|
const centerX = (rect.left + rect.width / 2 - viewportRect.left - view.x) / currentScale;
|
|
1049
1177
|
const centerY = (rect.top + rect.height / 2 - viewportRect.top - view.y) / currentScale;
|
|
1050
|
-
const availableCenterX =
|
|
1178
|
+
const availableCenterX = obscured?.side === "left" ? obscured.width + (viewportRect.width - obscured.width) / 2 : obscured?.side === "right" ? (viewportRect.width - obscured.width) / 2 : viewportRect.width / 2;
|
|
1179
|
+
const availableCenterY = obscured?.side === "bottom" ? (viewportRect.height - obscured.height) / 2 : viewportRect.height / 2;
|
|
1051
1180
|
const targetPanX = availableCenterX - centerX * newScale;
|
|
1052
|
-
const targetPanY =
|
|
1181
|
+
const targetPanY = availableCenterY - centerY * newScale;
|
|
1053
1182
|
zoomAnimationRef.current = null;
|
|
1054
1183
|
if (animationFrameIdRef.current !== null) {
|
|
1055
1184
|
cancelAnimationFrame(animationFrameIdRef.current);
|
|
@@ -1068,8 +1197,17 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1068
1197
|
centerAnimationFrameIdRef.current = requestAnimationFrame(() => stepCenterAnimationRef.current());
|
|
1069
1198
|
}
|
|
1070
1199
|
},
|
|
1071
|
-
[viewportRef]
|
|
1200
|
+
[viewportRef, options.frameKey, options.selectedNodeId]
|
|
1072
1201
|
);
|
|
1202
|
+
const centerLatestRef = useRef(centerOnElement);
|
|
1203
|
+
useEffect(() => {
|
|
1204
|
+
centerLatestRef.current = centerOnElement;
|
|
1205
|
+
}, [centerOnElement]);
|
|
1206
|
+
useEffect(() => {
|
|
1207
|
+
retryFrameRef.current = () => {
|
|
1208
|
+
fitToFrame(frameElementsRef.current, { silent: true });
|
|
1209
|
+
};
|
|
1210
|
+
}, [fitToFrame]);
|
|
1073
1211
|
const lastPointRef = useRef({ x: 0, y: 0 });
|
|
1074
1212
|
const minScaleRef = useRef(minScale);
|
|
1075
1213
|
const suppressedPointersRef = useRef(/* @__PURE__ */ new Set());
|
|
@@ -1144,6 +1282,7 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1144
1282
|
}, [handlePointerUp]);
|
|
1145
1283
|
const handlePointerDown = useCallback((event) => {
|
|
1146
1284
|
if (event.button !== 0) return;
|
|
1285
|
+
operationRef.current += 1;
|
|
1147
1286
|
const isInteractive = event.target.closest("g.node, foreignObject, .gtv-zoom-controls, .gtv-wheel-chip, .gtv-info-panel") !== null;
|
|
1148
1287
|
const isPageScrollTouch = wheelZoom === "modifier" && event.pointerType === "touch";
|
|
1149
1288
|
const pointers = activePointersRef.current;
|
|
@@ -1162,6 +1301,42 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1162
1301
|
window.addEventListener("pointercancel", stablePointerUp);
|
|
1163
1302
|
}
|
|
1164
1303
|
}, [stablePointerMove, stablePointerUp, wheelZoom]);
|
|
1304
|
+
const restore = options.viewportRestore;
|
|
1305
|
+
const restoreRevision = restore?.revision;
|
|
1306
|
+
const restoreX = restore?.viewport.x;
|
|
1307
|
+
const restoreY = restore?.viewport.y;
|
|
1308
|
+
const restoreZoom = restore?.viewport.zoom;
|
|
1309
|
+
useEffect(() => {
|
|
1310
|
+
if (restoreRevision === restoredRevisionRef.current || restoreRevision === void 0 || restoreX === void 0 || restoreY === void 0 || restoreZoom === void 0) return;
|
|
1311
|
+
if (!Number.isFinite(restoreX) || !Number.isFinite(restoreY) || !Number.isFinite(restoreZoom) || restoreZoom <= 0) return;
|
|
1312
|
+
const viewportElement = viewportRef.current;
|
|
1313
|
+
if (!viewportElement) return;
|
|
1314
|
+
restoredRevisionRef.current = restoreRevision;
|
|
1315
|
+
const operation = ++operationRef.current;
|
|
1316
|
+
zoomAnimationRef.current = null;
|
|
1317
|
+
centerAnimationRef.current = null;
|
|
1318
|
+
if (animationFrameIdRef.current !== null) cancelAnimationFrame(animationFrameIdRef.current);
|
|
1319
|
+
if (centerAnimationFrameIdRef.current !== null) cancelAnimationFrame(centerAnimationFrameIdRef.current);
|
|
1320
|
+
if (frameIdRef.current !== null) cancelAnimationFrame(frameIdRef.current);
|
|
1321
|
+
if (settleTimerRef.current !== null) clearTimeout(settleTimerRef.current);
|
|
1322
|
+
animationFrameIdRef.current = null;
|
|
1323
|
+
centerAnimationFrameIdRef.current = null;
|
|
1324
|
+
frameIdRef.current = null;
|
|
1325
|
+
settleTimerRef.current = null;
|
|
1326
|
+
activePointersRef.current.clear();
|
|
1327
|
+
suppressedPointersRef.current.clear();
|
|
1328
|
+
pinchStartRef.current = null;
|
|
1329
|
+
window.removeEventListener("pointermove", stablePointerMove);
|
|
1330
|
+
window.removeEventListener("pointerup", stablePointerUp);
|
|
1331
|
+
window.removeEventListener("pointercancel", stablePointerUp);
|
|
1332
|
+
pendingRestoreRef.current = {
|
|
1333
|
+
viewport: { x: restoreX, y: restoreY, zoom: restoreZoom },
|
|
1334
|
+
revision: restoreRevision,
|
|
1335
|
+
frame: options.frameKey,
|
|
1336
|
+
operation
|
|
1337
|
+
};
|
|
1338
|
+
if (measuredFrameRef.current === options.frameKey) retryFrameRef.current();
|
|
1339
|
+
}, [restoreRevision, restoreX, restoreY, restoreZoom, options.frameKey, options.selectedNodeId, viewportRef, commitView, stablePointerMove, stablePointerUp]);
|
|
1165
1340
|
useEffect(() => {
|
|
1166
1341
|
const pointers = activePointersRef.current;
|
|
1167
1342
|
const suppressed = suppressedPointersRef.current;
|
|
@@ -1196,17 +1371,6 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
|
|
|
1196
1371
|
|
|
1197
1372
|
// src/use-node-info-panel.ts
|
|
1198
1373
|
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
1199
|
-
|
|
1200
|
-
// src/info-panel-geometry.ts
|
|
1201
|
-
function resolveInfoPanelSide() {
|
|
1202
|
-
return "left";
|
|
1203
|
-
}
|
|
1204
|
-
function resolveInfoPanelObscuredArea(element, viewport, panelWidth) {
|
|
1205
|
-
if (!element || !viewport) return null;
|
|
1206
|
-
return { width: panelWidth, side: resolveInfoPanelSide() };
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
// src/use-node-info-panel.ts
|
|
1210
1374
|
function useNodeInfoPanel({
|
|
1211
1375
|
selectedNodeId,
|
|
1212
1376
|
onSelectedNodeChange
|
|
@@ -1215,7 +1379,7 @@ function useNodeInfoPanel({
|
|
|
1215
1379
|
const [prevSelectedNodeId, setPrevSelectedNodeId] = useState2(selectedNodeId);
|
|
1216
1380
|
if (selectedNodeId !== prevSelectedNodeId) {
|
|
1217
1381
|
setPrevSelectedNodeId(selectedNodeId);
|
|
1218
|
-
if (prevSelectedNodeId
|
|
1382
|
+
if (prevSelectedNodeId !== void 0) setPanel(null);
|
|
1219
1383
|
}
|
|
1220
1384
|
const shownNodeIdRef = useRef2(null);
|
|
1221
1385
|
const onSelectedNodeChangeRef = useRef2(onSelectedNodeChange);
|
|
@@ -1225,20 +1389,32 @@ function useNodeInfoPanel({
|
|
|
1225
1389
|
});
|
|
1226
1390
|
const showNodeInfo = useCallback2(
|
|
1227
1391
|
(node, element, viewport, { notify = true } = {}) => {
|
|
1392
|
+
if (notify && selectedNodeId !== void 0) {
|
|
1393
|
+
if (node.id !== selectedNodeId) onSelectedNodeChangeRef.current?.(node);
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1228
1396
|
if (!element || !viewport) return;
|
|
1229
1397
|
setPanel({ node, side: resolveInfoPanelSide() });
|
|
1230
1398
|
const changed = node.id !== shownNodeIdRef.current;
|
|
1231
1399
|
shownNodeIdRef.current = node.id;
|
|
1232
1400
|
if (changed && notify) onSelectedNodeChangeRef.current?.(node);
|
|
1233
1401
|
},
|
|
1234
|
-
[]
|
|
1402
|
+
[selectedNodeId]
|
|
1235
1403
|
);
|
|
1236
1404
|
const closeNodeInfo = useCallback2(() => {
|
|
1405
|
+
if (selectedNodeId !== void 0) {
|
|
1406
|
+
if (selectedNodeId !== null) onSelectedNodeChangeRef.current?.(null);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1237
1409
|
setPanel(null);
|
|
1238
1410
|
shownNodeIdRef.current = null;
|
|
1239
1411
|
onSelectedNodeChangeRef.current?.(null);
|
|
1240
|
-
}, []);
|
|
1241
|
-
return {
|
|
1412
|
+
}, [selectedNodeId]);
|
|
1413
|
+
return {
|
|
1414
|
+
panel: selectedNodeId !== void 0 && panel?.node.id !== selectedNodeId ? null : panel,
|
|
1415
|
+
showNodeInfo,
|
|
1416
|
+
closeNodeInfo
|
|
1417
|
+
};
|
|
1242
1418
|
}
|
|
1243
1419
|
|
|
1244
1420
|
// src/use-tree-index.ts
|
|
@@ -1284,8 +1460,8 @@ function useTreeIndex(nodes) {
|
|
|
1284
1460
|
}
|
|
1285
1461
|
|
|
1286
1462
|
// src/InfoPanel.tsx
|
|
1287
|
-
import { useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
1288
|
-
import { MdArrowBack, MdClose, MdZoomIn
|
|
1463
|
+
import { useEffect as useEffect3, useRef as useRef3, useState as useState5 } from "react";
|
|
1464
|
+
import { MdArrowBack, MdClose, MdZoomIn } from "react-icons/md";
|
|
1289
1465
|
|
|
1290
1466
|
// src/InfoPanelExpandButton.tsx
|
|
1291
1467
|
import { MdCloseFullscreen, MdOpenInFull } from "react-icons/md";
|
|
@@ -1524,6 +1700,27 @@ function InfoPanel({
|
|
|
1524
1700
|
hideExpandButton = false
|
|
1525
1701
|
}) {
|
|
1526
1702
|
const panelRef = useRef3(null);
|
|
1703
|
+
const [sheetFull, setSheetFull] = useState5(false);
|
|
1704
|
+
const grabberDragRef = useRef3(null);
|
|
1705
|
+
const onGrabberPointerDown = (event) => {
|
|
1706
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
1707
|
+
grabberDragRef.current = { startY: event.clientY, dragged: false };
|
|
1708
|
+
};
|
|
1709
|
+
const onGrabberPointerUp = (event) => {
|
|
1710
|
+
const drag = grabberDragRef.current;
|
|
1711
|
+
if (!drag) return;
|
|
1712
|
+
const dy = event.clientY - drag.startY;
|
|
1713
|
+
if (Math.abs(dy) < INFO_PANEL_SHEET_DRAG_THRESHOLD_PX) return;
|
|
1714
|
+
drag.dragged = true;
|
|
1715
|
+
if (dy < 0) setSheetFull(true);
|
|
1716
|
+
else if (sheetFull) setSheetFull(false);
|
|
1717
|
+
else if (!hideClose) onClose();
|
|
1718
|
+
};
|
|
1719
|
+
const onGrabberClick = () => {
|
|
1720
|
+
const dragged = grabberDragRef.current?.dragged ?? false;
|
|
1721
|
+
grabberDragRef.current = null;
|
|
1722
|
+
if (!dragged) setSheetFull((full) => !full);
|
|
1723
|
+
};
|
|
1527
1724
|
useEffect3(() => {
|
|
1528
1725
|
panelRef.current?.toggleAttribute("inert", collapsed);
|
|
1529
1726
|
}, [collapsed]);
|
|
@@ -1626,9 +1823,21 @@ function InfoPanel({
|
|
|
1626
1823
|
return /* @__PURE__ */ jsxs6(
|
|
1627
1824
|
"div",
|
|
1628
1825
|
{
|
|
1629
|
-
className: `gtv-info-panel gtv-info-panel--${side}${expanded ? " gtv-info-panel--expanded" : ""}${expanded && !hasAside ? " gtv-info-panel--expanded-solo" : ""}${collapsed ? " gtv-info-panel--collapsed" : ""}`,
|
|
1826
|
+
className: `gtv-info-panel gtv-info-panel--sheet${sheetFull ? " gtv-info-panel--sheet-full" : ""} gtv-info-panel--${side}${expanded ? " gtv-info-panel--expanded" : ""}${expanded && !hasAside ? " gtv-info-panel--expanded-solo" : ""}${collapsed ? " gtv-info-panel--collapsed" : ""}`,
|
|
1630
1827
|
ref: panelRef,
|
|
1631
1828
|
children: [
|
|
1829
|
+
/* @__PURE__ */ jsx7(
|
|
1830
|
+
"button",
|
|
1831
|
+
{
|
|
1832
|
+
type: "button",
|
|
1833
|
+
className: "gtv-info-panel-grabber",
|
|
1834
|
+
"aria-label": sheetFull ? "Lower panel" : "Raise panel",
|
|
1835
|
+
"aria-expanded": sheetFull,
|
|
1836
|
+
onPointerDown: onGrabberPointerDown,
|
|
1837
|
+
onPointerUp: onGrabberPointerUp,
|
|
1838
|
+
onClick: onGrabberClick
|
|
1839
|
+
}
|
|
1840
|
+
),
|
|
1632
1841
|
/* @__PURE__ */ jsxs6(
|
|
1633
1842
|
"div",
|
|
1634
1843
|
{
|
|
@@ -1667,7 +1876,7 @@ function InfoPanel({
|
|
|
1667
1876
|
onClick: onZoomIn,
|
|
1668
1877
|
"aria-label": "Zoom in",
|
|
1669
1878
|
title: "Zoom in",
|
|
1670
|
-
children: /* @__PURE__ */ jsx7(
|
|
1879
|
+
children: /* @__PURE__ */ jsx7(MdZoomIn, { className: "gtv-icon", size: 16 })
|
|
1671
1880
|
}
|
|
1672
1881
|
),
|
|
1673
1882
|
onExpandedChange && !hideExpandButton && /* @__PURE__ */ jsx7(InfoPanelExpandButton, { expanded, onExpandedChange, style: { color: textColor } }),
|
|
@@ -1780,6 +1989,8 @@ function GenreTree({
|
|
|
1780
1989
|
hideInfoPanelClose = false,
|
|
1781
1990
|
hideInfoPanelTitle = false,
|
|
1782
1991
|
itemCountLabel,
|
|
1992
|
+
itemCountScaleMax,
|
|
1993
|
+
nodeCountForSizing,
|
|
1783
1994
|
readOnly,
|
|
1784
1995
|
renderDangerZone,
|
|
1785
1996
|
expanded,
|
|
@@ -1789,8 +2000,10 @@ function GenreTree({
|
|
|
1789
2000
|
overlayPanel,
|
|
1790
2001
|
wheelZoom,
|
|
1791
2002
|
defaultViewport,
|
|
2003
|
+
viewportRestore,
|
|
1792
2004
|
onViewportChange,
|
|
1793
2005
|
controlsOrientation = "vertical",
|
|
2006
|
+
showZoomControls = "auto",
|
|
1794
2007
|
floatingControlsLeading,
|
|
1795
2008
|
busy
|
|
1796
2009
|
}) {
|
|
@@ -1798,8 +2011,13 @@ function GenreTree({
|
|
|
1798
2011
|
const svgRef = useRef5(null);
|
|
1799
2012
|
const viewportRef = useRef5(null);
|
|
1800
2013
|
const stageRef = useRef5(null);
|
|
1801
|
-
const
|
|
2014
|
+
const viewportFrame = useMemo2(() => JSON.stringify([orientation, nodes.map(({ id, parentId, side }) => [id, parentId, side])]), [nodes, orientation]);
|
|
2015
|
+
const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, viewportRestore, selectedNodeId: selectedNodeIdProp, onViewportChange, frameKey: viewportFrame });
|
|
1802
2016
|
const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId: selectedNodeIdProp, onSelectedNodeChange });
|
|
2017
|
+
const itemCountRange = useMemo2(
|
|
2018
|
+
() => getItemCountRange(nodes, { itemCountScaleMax, nodeCountForSizing: nodeCountForSizing ?? nodes.length }),
|
|
2019
|
+
[nodes, itemCountScaleMax, nodeCountForSizing]
|
|
2020
|
+
);
|
|
1803
2021
|
const showNodeInfoRef = useRef5(showNodeInfo);
|
|
1804
2022
|
useEffect5(() => {
|
|
1805
2023
|
showNodeInfoRef.current = showNodeInfo;
|
|
@@ -1814,7 +2032,8 @@ function GenreTree({
|
|
|
1814
2032
|
d3,
|
|
1815
2033
|
root,
|
|
1816
2034
|
orientation,
|
|
1817
|
-
depthSpacingScale
|
|
2035
|
+
depthSpacingScale,
|
|
2036
|
+
itemCountRange
|
|
1818
2037
|
);
|
|
1819
2038
|
const {
|
|
1820
2039
|
svgWidth: width,
|
|
@@ -1826,7 +2045,8 @@ function GenreTree({
|
|
|
1826
2045
|
originalTreeData,
|
|
1827
2046
|
orientation,
|
|
1828
2047
|
hideRoot,
|
|
1829
|
-
depthSpacingScale
|
|
2048
|
+
depthSpacingScale,
|
|
2049
|
+
itemCountRange
|
|
1830
2050
|
);
|
|
1831
2051
|
const reshapedTreeData = setupTreeLayout(
|
|
1832
2052
|
d3,
|
|
@@ -1835,7 +2055,8 @@ function GenreTree({
|
|
|
1835
2055
|
orientation,
|
|
1836
2056
|
rootDepthOffset,
|
|
1837
2057
|
width,
|
|
1838
|
-
depthSpacingScale
|
|
2058
|
+
depthSpacingScale,
|
|
2059
|
+
itemCountRange
|
|
1839
2060
|
);
|
|
1840
2061
|
return {
|
|
1841
2062
|
treeData: reshapedTreeData,
|
|
@@ -1843,7 +2064,7 @@ function GenreTree({
|
|
|
1843
2064
|
svgWidth: width,
|
|
1844
2065
|
svgHeight: height
|
|
1845
2066
|
};
|
|
1846
|
-
}, [nodes, rootColor, orientation, hideRoot, depthSpacingScale]);
|
|
2067
|
+
}, [nodes, rootColor, orientation, hideRoot, depthSpacingScale, itemCountRange]);
|
|
1847
2068
|
const reparentForbiddenIds = useMemo2(() => {
|
|
1848
2069
|
if (!reparentingNodeId) return [];
|
|
1849
2070
|
const reparentingHierarchyNode = treeData.descendants().find((d) => d.data.id === reparentingNodeId);
|
|
@@ -1894,7 +2115,8 @@ function GenreTree({
|
|
|
1894
2115
|
},
|
|
1895
2116
|
orientation,
|
|
1896
2117
|
hideRoot,
|
|
1897
|
-
effectiveSelectedNodeId
|
|
2118
|
+
effectiveSelectedNodeId,
|
|
2119
|
+
itemCountRange
|
|
1898
2120
|
);
|
|
1899
2121
|
}, [
|
|
1900
2122
|
treeData,
|
|
@@ -1915,7 +2137,8 @@ function GenreTree({
|
|
|
1915
2137
|
additionalActions,
|
|
1916
2138
|
orientation,
|
|
1917
2139
|
hideRoot,
|
|
1918
|
-
effectiveSelectedNodeId
|
|
2140
|
+
effectiveSelectedNodeId,
|
|
2141
|
+
itemCountRange
|
|
1919
2142
|
]);
|
|
1920
2143
|
useEffect5(() => {
|
|
1921
2144
|
if (!interactive || !selectedNodeIdProp) return;
|
|
@@ -1928,7 +2151,8 @@ function GenreTree({
|
|
|
1928
2151
|
centerOnElementRef.current(
|
|
1929
2152
|
element,
|
|
1930
2153
|
ZOOM_FOCUS_SCALE,
|
|
1931
|
-
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH)
|
|
2154
|
+
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),
|
|
2155
|
+
true
|
|
1932
2156
|
);
|
|
1933
2157
|
showNodeInfoRef.current(targetNode, element, viewportRef.current, { notify: false });
|
|
1934
2158
|
}, [interactive, selectedNodeIdProp, index, panel]);
|
|
@@ -1940,12 +2164,10 @@ function GenreTree({
|
|
|
1940
2164
|
}
|
|
1941
2165
|
};
|
|
1942
2166
|
}, []);
|
|
1943
|
-
const hasInitialFitRef = useRef5(false);
|
|
1944
2167
|
useEffect5(() => {
|
|
1945
|
-
if (
|
|
2168
|
+
if (!interactive) return;
|
|
1946
2169
|
const elements = queryTreeContentElements(svgRef.current);
|
|
1947
2170
|
if (elements.length === 0) return;
|
|
1948
|
-
hasInitialFitRef.current = true;
|
|
1949
2171
|
panZoom.fitToFrame(elements, { silent: true });
|
|
1950
2172
|
});
|
|
1951
2173
|
const svg = /* @__PURE__ */ jsx9(
|
|
@@ -2076,6 +2298,7 @@ function GenreTree({
|
|
|
2076
2298
|
{
|
|
2077
2299
|
panZoom,
|
|
2078
2300
|
orientation: controlsOrientation,
|
|
2301
|
+
zoomControls: showZoomControls,
|
|
2079
2302
|
leading: floatingControlsLeading,
|
|
2080
2303
|
busy,
|
|
2081
2304
|
onFit: () => panZoom.fitToFrame(queryTreeContentElements(svgRef.current))
|
|
@@ -2087,7 +2310,7 @@ function GenreTree({
|
|
|
2087
2310
|
}
|
|
2088
2311
|
|
|
2089
2312
|
// src/GenreTreeWheelBase.tsx
|
|
2090
|
-
import { useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as
|
|
2313
|
+
import { useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState6 } from "react";
|
|
2091
2314
|
import * as d32 from "d3";
|
|
2092
2315
|
|
|
2093
2316
|
// src/root-grouping.ts
|
|
@@ -2188,6 +2411,8 @@ function WheelCore({
|
|
|
2188
2411
|
hideInfoPanelClose = false,
|
|
2189
2412
|
hideInfoPanelTitle = false,
|
|
2190
2413
|
itemCountLabel,
|
|
2414
|
+
itemCountScaleMax,
|
|
2415
|
+
nodeCountForSizing,
|
|
2191
2416
|
readOnly,
|
|
2192
2417
|
renderDangerZone,
|
|
2193
2418
|
expanded,
|
|
@@ -2197,12 +2422,19 @@ function WheelCore({
|
|
|
2197
2422
|
overlayPanel,
|
|
2198
2423
|
wheelZoom,
|
|
2199
2424
|
defaultViewport,
|
|
2425
|
+
viewportRestore,
|
|
2200
2426
|
onViewportChange,
|
|
2201
2427
|
controlsOrientation = "vertical",
|
|
2428
|
+
showZoomControls = "auto",
|
|
2202
2429
|
floatingControlsLeading,
|
|
2203
2430
|
busy,
|
|
2204
2431
|
selectedNodeId
|
|
2205
2432
|
}) {
|
|
2433
|
+
const fullNodeCountForSizing = nodeCountForSizing ?? nodes.length;
|
|
2434
|
+
const wheelSizeRange = useMemo3(
|
|
2435
|
+
() => getItemCountRange(nodes, { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }),
|
|
2436
|
+
[nodes, itemCountScaleMax, fullNodeCountForSizing]
|
|
2437
|
+
);
|
|
2206
2438
|
const treeOrientation = direction === "left" ? "horizontal-anchored" : "vertical";
|
|
2207
2439
|
const landingAngle = direction === "left" ? 90 : 0;
|
|
2208
2440
|
const chipHalfExtentVar = direction === "left" ? "--gtv-wheel-chip-half-width" : "--gtv-wheel-chip-half-height";
|
|
@@ -2212,18 +2444,18 @@ function WheelCore({
|
|
|
2212
2444
|
return getGenreTreeColor(nodes.find((n) => n.id === rootId)?.wikidataId);
|
|
2213
2445
|
};
|
|
2214
2446
|
const wheelRadius = useMemo3(
|
|
2215
|
-
() => calculateWheelRadius(groups.length, MAX_NODE_WIDTH, WHEEL_RADIUS),
|
|
2216
|
-
[groups.length]
|
|
2447
|
+
() => calculateWheelRadius(groups.length, wheelSizeRange.maxNodeWidth ?? MAX_NODE_WIDTH, WHEEL_RADIUS),
|
|
2448
|
+
[groups.length, wheelSizeRange.maxNodeWidth]
|
|
2217
2449
|
);
|
|
2218
|
-
const wheelViewportHeight = wheelRadius * 2 + MAX_NODE_HEIGHT / 2;
|
|
2450
|
+
const wheelViewportHeight = wheelRadius * 2 + (wheelSizeRange.maxNodeHeight ?? MAX_NODE_HEIGHT) / 2;
|
|
2219
2451
|
const rootItemCountById = useMemo3(
|
|
2220
2452
|
() => new Map(groups.map((group) => [group.root.id, group.root.itemCount])),
|
|
2221
2453
|
[groups]
|
|
2222
2454
|
);
|
|
2223
|
-
const [selectedRootId, setSelectedRootId] =
|
|
2455
|
+
const [selectedRootId, setSelectedRootId] = useState6(
|
|
2224
2456
|
groups[0]?.root.id ?? null
|
|
2225
2457
|
);
|
|
2226
|
-
const [rotationDeg, setRotationDeg] =
|
|
2458
|
+
const [rotationDeg, setRotationDeg] = useState6(
|
|
2227
2459
|
() => groups.length > 0 ? computeRotationForSelection(
|
|
2228
2460
|
0,
|
|
2229
2461
|
getChipAngle(0, groups.length),
|
|
@@ -2232,7 +2464,8 @@ function WheelCore({
|
|
|
2232
2464
|
);
|
|
2233
2465
|
const viewportRef = useRef6(null);
|
|
2234
2466
|
const stageRef = useRef6(null);
|
|
2235
|
-
const
|
|
2467
|
+
const viewportFrame = useMemo3(() => JSON.stringify(nodes.map(({ id, parentId, side }) => [id, parentId, side])), [nodes]);
|
|
2468
|
+
const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, viewportRestore, selectedNodeId, onViewportChange, frameKey: viewportFrame });
|
|
2236
2469
|
const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });
|
|
2237
2470
|
const wheelCircleRef = useRef6(null);
|
|
2238
2471
|
const treeAnchorRef = useRef6(null);
|
|
@@ -2246,15 +2479,12 @@ function WheelCore({
|
|
|
2246
2479
|
useEffect6(() => {
|
|
2247
2480
|
if (effectiveRootId) onRootSelectRef.current?.(effectiveRootId);
|
|
2248
2481
|
}, [effectiveRootId]);
|
|
2249
|
-
const hasInitialFitRef = useRef6(false);
|
|
2250
2482
|
useEffect6(() => {
|
|
2251
|
-
if (hasInitialFitRef.current) return;
|
|
2252
2483
|
const elements = [
|
|
2253
2484
|
wheelCircleRef.current,
|
|
2254
2485
|
...queryTreeContentElements(treeAnchorRef.current)
|
|
2255
2486
|
];
|
|
2256
2487
|
if (!elements.some(Boolean)) return;
|
|
2257
|
-
hasInitialFitRef.current = true;
|
|
2258
2488
|
panZoom.fitToFrame(elements, { silent: true });
|
|
2259
2489
|
});
|
|
2260
2490
|
const selectedGroup = groups.find((group) => group.root.id === effectiveRootId) ?? null;
|
|
@@ -2262,16 +2492,18 @@ function WheelCore({
|
|
|
2262
2492
|
() => getItemCountRange(
|
|
2263
2493
|
groups.map((group) => ({
|
|
2264
2494
|
itemCount: rootItemCountById.get(group.root.id)
|
|
2265
|
-
}))
|
|
2495
|
+
})),
|
|
2496
|
+
{ itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }
|
|
2266
2497
|
),
|
|
2267
|
-
[groups, rootItemCountById]
|
|
2498
|
+
[groups, rootItemCountById, itemCountScaleMax, fullNodeCountForSizing]
|
|
2268
2499
|
);
|
|
2269
2500
|
const rootChipHalfExtent = selectedGroup ? calculateRootAnchorClearance(
|
|
2270
2501
|
d32,
|
|
2271
2502
|
selectedGroup.nodes,
|
|
2272
2503
|
rootItemCountById.get(selectedGroup.root.id),
|
|
2273
2504
|
rootItemCountRange,
|
|
2274
|
-
direction === "left" ? "WIDTH" : "HEIGHT"
|
|
2505
|
+
direction === "left" ? "WIDTH" : "HEIGHT",
|
|
2506
|
+
{ itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }
|
|
2275
2507
|
) : 0;
|
|
2276
2508
|
const sectorFillGradient = useMemo3(
|
|
2277
2509
|
() => buildWheelSectorGradient(
|
|
@@ -2289,7 +2521,7 @@ function WheelCore({
|
|
|
2289
2521
|
);
|
|
2290
2522
|
}
|
|
2291
2523
|
};
|
|
2292
|
-
const [appliedSelectedNodeId, setAppliedSelectedNodeId] =
|
|
2524
|
+
const [appliedSelectedNodeId, setAppliedSelectedNodeId] = useState6(selectedNodeId);
|
|
2293
2525
|
if (selectedNodeId !== appliedSelectedNodeId) {
|
|
2294
2526
|
setAppliedSelectedNodeId(selectedNodeId);
|
|
2295
2527
|
const owningGroupIndex = selectedNodeId ? groups.findIndex(
|
|
@@ -2312,7 +2544,8 @@ function WheelCore({
|
|
|
2312
2544
|
panZoom.centerOnElement(
|
|
2313
2545
|
element,
|
|
2314
2546
|
ZOOM_FOCUS_SCALE,
|
|
2315
|
-
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH)
|
|
2547
|
+
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),
|
|
2548
|
+
true
|
|
2316
2549
|
);
|
|
2317
2550
|
showNodeInfo(targetNode, element, viewportRef.current, { notify: false });
|
|
2318
2551
|
}, [selectedNodeId, effectiveRootId, nodes, panel, showNodeInfo]);
|
|
@@ -2361,6 +2594,8 @@ function WheelCore({
|
|
|
2361
2594
|
{
|
|
2362
2595
|
nodes: selectedGroup.nodes,
|
|
2363
2596
|
itemCountLabel,
|
|
2597
|
+
itemCountScaleMax,
|
|
2598
|
+
nodeCountForSizing: fullNodeCountForSizing,
|
|
2364
2599
|
orientation: treeOrientation,
|
|
2365
2600
|
hideRoot: true,
|
|
2366
2601
|
interactive: false,
|
|
@@ -2572,6 +2807,7 @@ function WheelCore({
|
|
|
2572
2807
|
{
|
|
2573
2808
|
panZoom,
|
|
2574
2809
|
orientation: controlsOrientation,
|
|
2810
|
+
zoomControls: showZoomControls,
|
|
2575
2811
|
leading: floatingControlsLeading,
|
|
2576
2812
|
busy,
|
|
2577
2813
|
onFit: () => panZoom.fitToFrame([
|
|
@@ -2598,24 +2834,38 @@ function GenreTreeWheelRight(props) {
|
|
|
2598
2834
|
}
|
|
2599
2835
|
|
|
2600
2836
|
// src/GenreTreeWheelRadialBase.tsx
|
|
2601
|
-
import { useCallback as useCallback3, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef7, useState as
|
|
2837
|
+
import { useCallback as useCallback3, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef7, useState as useState7 } from "react";
|
|
2602
2838
|
import * as d33 from "d3";
|
|
2603
2839
|
|
|
2604
2840
|
// src/defer-click.ts
|
|
2605
2841
|
function createDeferredClick() {
|
|
2606
2842
|
let timeoutId = null;
|
|
2843
|
+
let pendingKey = void 0;
|
|
2844
|
+
let doubledAt = -Infinity;
|
|
2607
2845
|
const cancel = () => {
|
|
2608
2846
|
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
2609
2847
|
timeoutId = null;
|
|
2610
2848
|
};
|
|
2611
|
-
const schedule = (action) => {
|
|
2849
|
+
const schedule = (action, onDouble, key) => {
|
|
2850
|
+
if (onDouble && timeoutId !== null && key === pendingKey) {
|
|
2851
|
+
cancel();
|
|
2852
|
+
doubledAt = Date.now();
|
|
2853
|
+
onDouble();
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2612
2856
|
cancel();
|
|
2857
|
+
pendingKey = key;
|
|
2613
2858
|
timeoutId = setTimeout(() => {
|
|
2614
2859
|
timeoutId = null;
|
|
2615
2860
|
action();
|
|
2616
2861
|
}, DOUBLE_CLICK_DELAY_MS);
|
|
2617
2862
|
};
|
|
2618
|
-
|
|
2863
|
+
const doubleClick = (onDouble) => {
|
|
2864
|
+
cancel();
|
|
2865
|
+
if (Date.now() - doubledAt < DOUBLE_CLICK_DELAY_MS) return;
|
|
2866
|
+
onDouble();
|
|
2867
|
+
};
|
|
2868
|
+
return { schedule, cancel, doubleClick };
|
|
2619
2869
|
}
|
|
2620
2870
|
|
|
2621
2871
|
// src/radial-wheel-geometry.ts
|
|
@@ -2805,13 +3055,15 @@ function calculatePopSubtreeRadialExtent(hierarchy, coreRootCircleRadius = 0) {
|
|
|
2805
3055
|
POP_TREE_DEPTH_RADIAL_SPACING
|
|
2806
3056
|
) + MAX_NODE_WIDTH / 2 + POP_SUBTREE_OUTER_MARGIN;
|
|
2807
3057
|
}
|
|
2808
|
-
function computePopRadialLayout(d3Lib, hierarchy, wedgeCenterAngleDegrees, coreRootCircleRadius, wedgeSpanDegrees = POP_WEDGE_SPAN_DEGREES, depthSpacing = POP_TREE_DEPTH_RADIAL_SPACING) {
|
|
3058
|
+
function computePopRadialLayout(d3Lib, hierarchy, wedgeCenterAngleDegrees, coreRootCircleRadius, wedgeSpanDegrees = POP_WEDGE_SPAN_DEGREES, depthSpacing = POP_TREE_DEPTH_RADIAL_SPACING, dotRadius = NODE_LAYOUT_SLOT_RADIUS) {
|
|
2809
3059
|
return layoutWedge(
|
|
2810
3060
|
d3Lib,
|
|
2811
3061
|
hierarchy,
|
|
2812
3062
|
wedgeCenterAngleDegrees,
|
|
2813
3063
|
wedgeSpanDegrees,
|
|
2814
|
-
popRadiusOfDepth(coreRootCircleRadius, depthSpacing)
|
|
3064
|
+
popRadiusOfDepth(coreRootCircleRadius, depthSpacing),
|
|
3065
|
+
"center",
|
|
3066
|
+
dotRadius
|
|
2815
3067
|
);
|
|
2816
3068
|
}
|
|
2817
3069
|
var MIN_DOT_GAP = 5.6;
|
|
@@ -2821,15 +3073,15 @@ function popRadiusOfDepth(coreRootCircleRadius, depthSpacing) {
|
|
|
2821
3073
|
function coreRadiusOfDepth(coreRootCircleRadius, depthSpacing) {
|
|
2822
3074
|
return (depth) => getRadialDepthRadius(depth + 1, coreRootCircleRadius, depthSpacing);
|
|
2823
3075
|
}
|
|
2824
|
-
function measureWedge(_d3Lib, hierarchy, radiusOfDepth) {
|
|
2825
|
-
const
|
|
3076
|
+
function measureWedge(_d3Lib, hierarchy, radiusOfDepth, dotRadius = NODE_LAYOUT_SLOT_RADIUS) {
|
|
3077
|
+
const slotRadius = () => dotRadius;
|
|
2826
3078
|
const offsetFromParent = /* @__PURE__ */ new Map();
|
|
2827
3079
|
const shiftLevel = (level, by) => ({
|
|
2828
3080
|
left: { ...level.left, x: level.left.x + by },
|
|
2829
3081
|
right: { ...level.right, x: level.right.x + by }
|
|
2830
3082
|
});
|
|
2831
3083
|
const pack = (node) => {
|
|
2832
|
-
const own = { x: 0, dotRadius:
|
|
3084
|
+
const own = { x: 0, dotRadius: slotRadius() };
|
|
2833
3085
|
if (!node.children) return [{ left: own, right: own }];
|
|
2834
3086
|
const below = [];
|
|
2835
3087
|
const childOffsets = [];
|
|
@@ -2861,7 +3113,7 @@ function measureWedge(_d3Lib, hierarchy, radiusOfDepth) {
|
|
|
2861
3113
|
let min = Infinity;
|
|
2862
3114
|
let max = -Infinity;
|
|
2863
3115
|
hierarchy.each((d) => {
|
|
2864
|
-
const inset = (
|
|
3116
|
+
const inset = (slotRadius() + MIN_DOT_GAP / 2) / radiusOfDepth(d.depth);
|
|
2865
3117
|
min = Math.min(min, d.x - inset);
|
|
2866
3118
|
max = Math.max(max, d.x + inset);
|
|
2867
3119
|
});
|
|
@@ -2881,8 +3133,8 @@ function splitAtBranching(hierarchy) {
|
|
|
2881
3133
|
const children = branching.children;
|
|
2882
3134
|
return children && children.length > 1 ? { chain, branching, children } : null;
|
|
2883
3135
|
}
|
|
2884
|
-
function layoutWedge(d3Lib, hierarchy, wedgeCenterAngleDegrees, wedgeSpanDegrees, radiusOfDepth, slack = "center") {
|
|
2885
|
-
const neededRad = measureWedge(d3Lib, hierarchy, radiusOfDepth);
|
|
3136
|
+
function layoutWedge(d3Lib, hierarchy, wedgeCenterAngleDegrees, wedgeSpanDegrees, radiusOfDepth, slack = "center", dotRadius = NODE_LAYOUT_SLOT_RADIUS) {
|
|
3137
|
+
const neededRad = measureWedge(d3Lib, hierarchy, radiusOfDepth, dotRadius);
|
|
2886
3138
|
const wedgeSpanRad = wedgeSpanDegrees * Math.PI / 180;
|
|
2887
3139
|
const scale = Math.min(MAX_WEDGE_STRETCH, wedgeSpanRad / neededRad);
|
|
2888
3140
|
const wedgeCenterRad = wedgeCenterAngleDegrees * Math.PI / 180;
|
|
@@ -3111,12 +3363,12 @@ function renderPopSubtree(d3Lib, svg, hierarchy, rootColor, reparentingNodeId, r
|
|
|
3111
3363
|
group.on("click", (event) => {
|
|
3112
3364
|
if (reparentingNodeId) return;
|
|
3113
3365
|
const element = event.currentTarget;
|
|
3114
|
-
deferredClick.schedule(
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
onNodeDoubleClick?.(d.data);
|
|
3366
|
+
deferredClick.schedule(
|
|
3367
|
+
() => onNodeClick?.(d.data, event, element),
|
|
3368
|
+
() => onNodeDoubleClick?.(d.data)
|
|
3369
|
+
);
|
|
3119
3370
|
});
|
|
3371
|
+
group.on("dblclick", () => deferredClick.doubleClick(() => onNodeDoubleClick?.(d.data)));
|
|
3120
3372
|
});
|
|
3121
3373
|
}
|
|
3122
3374
|
function hideCollidingLabels(d3Lib, container, itemCountRange, zoomScale) {
|
|
@@ -3193,14 +3445,15 @@ function buildCoreHierarchy(d3Lib, coreNodes) {
|
|
|
3193
3445
|
});
|
|
3194
3446
|
return hierarchy;
|
|
3195
3447
|
}
|
|
3196
|
-
function computeCoreRadialLayout(d3Lib, hierarchy, wedgeCenterAngleDegrees, wedgeSpanDegrees, coreRootCircleRadius, depthSpacing) {
|
|
3448
|
+
function computeCoreRadialLayout(d3Lib, hierarchy, wedgeCenterAngleDegrees, wedgeSpanDegrees, coreRootCircleRadius, depthSpacing, dotRadius = NODE_LAYOUT_SLOT_RADIUS) {
|
|
3197
3449
|
return layoutWedge(
|
|
3198
3450
|
d3Lib,
|
|
3199
3451
|
hierarchy,
|
|
3200
3452
|
wedgeCenterAngleDegrees,
|
|
3201
3453
|
wedgeSpanDegrees,
|
|
3202
3454
|
coreRadiusOfDepth(coreRootCircleRadius, depthSpacing),
|
|
3203
|
-
"between-children"
|
|
3455
|
+
"between-children",
|
|
3456
|
+
dotRadius
|
|
3204
3457
|
);
|
|
3205
3458
|
}
|
|
3206
3459
|
function calculateCoreSubtreeRadialExtent(hierarchy, depthSpacing, coreRootCircleRadius = 0) {
|
|
@@ -3274,6 +3527,8 @@ function WheelRadialCore({
|
|
|
3274
3527
|
hideInfoPanelClose = false,
|
|
3275
3528
|
hideInfoPanelTitle = false,
|
|
3276
3529
|
itemCountLabel,
|
|
3530
|
+
itemCountScaleMax,
|
|
3531
|
+
nodeCountForSizing,
|
|
3277
3532
|
readOnly,
|
|
3278
3533
|
renderDangerZone,
|
|
3279
3534
|
expanded,
|
|
@@ -3283,21 +3538,25 @@ function WheelRadialCore({
|
|
|
3283
3538
|
overlayPanel,
|
|
3284
3539
|
wheelZoom,
|
|
3285
3540
|
defaultViewport,
|
|
3541
|
+
viewportRestore,
|
|
3286
3542
|
onViewportChange,
|
|
3287
3543
|
controlsOrientation = "vertical",
|
|
3544
|
+
showZoomControls = "auto",
|
|
3288
3545
|
floatingControlsLeading,
|
|
3289
3546
|
busy,
|
|
3290
3547
|
selectedNodeId
|
|
3291
3548
|
}) {
|
|
3549
|
+
const fullNodeCountForSizing = nodeCountForSizing ?? nodes.length;
|
|
3292
3550
|
const index = useTreeIndex(nodes);
|
|
3293
3551
|
const groups = useMemo4(() => groupNodesByRoot(nodes), [nodes]);
|
|
3294
3552
|
const rootColorOf = (node) => getGenreTreeColor(index.byId.get(index.rootIdById.get(node.id) ?? node.id)?.wikidataId);
|
|
3295
|
-
const [topRootId, setTopRootId] =
|
|
3553
|
+
const [topRootId, setTopRootId] = useState7(
|
|
3296
3554
|
groups[0]?.root.id ?? null
|
|
3297
3555
|
);
|
|
3298
3556
|
const viewportRef = useRef7(null);
|
|
3299
3557
|
const stageRef = useRef7(null);
|
|
3300
|
-
const
|
|
3558
|
+
const viewportFrame = useMemo4(() => JSON.stringify(nodes.map(({ id, parentId, side }) => [id, parentId, side])), [nodes]);
|
|
3559
|
+
const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, viewportRestore, selectedNodeId, onViewportChange, frameKey: viewportFrame });
|
|
3301
3560
|
const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });
|
|
3302
3561
|
const centerOnElementRef = useRef7(panZoom.centerOnElement);
|
|
3303
3562
|
useEffect7(() => {
|
|
@@ -3322,7 +3581,7 @@ function WheelRadialCore({
|
|
|
3322
3581
|
() => computeRadialLayout(rootWeights, topIndex, LANDING_ANGLE),
|
|
3323
3582
|
[rootWeights, topIndex]
|
|
3324
3583
|
);
|
|
3325
|
-
const [angleMemo, setAngleMemo] =
|
|
3584
|
+
const [angleMemo, setAngleMemo] = useState7(() => ({
|
|
3326
3585
|
layout,
|
|
3327
3586
|
angles: computeContinuousAngles(groups, layout, /* @__PURE__ */ new Map())
|
|
3328
3587
|
}));
|
|
@@ -3341,11 +3600,15 @@ function WheelRadialCore({
|
|
|
3341
3600
|
() => getItemCountRange(
|
|
3342
3601
|
groups.map((group) => ({
|
|
3343
3602
|
itemCount: rootItemCountById.get(group.root.id)
|
|
3344
|
-
}))
|
|
3603
|
+
})),
|
|
3604
|
+
{ itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }
|
|
3345
3605
|
),
|
|
3346
|
-
[groups, rootItemCountById]
|
|
3606
|
+
[groups, rootItemCountById, itemCountScaleMax, fullNodeCountForSizing]
|
|
3607
|
+
);
|
|
3608
|
+
const wheelItemCountRange = useMemo4(
|
|
3609
|
+
() => getItemCountRange(nodes, { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }),
|
|
3610
|
+
[nodes, itemCountScaleMax, fullNodeCountForSizing]
|
|
3347
3611
|
);
|
|
3348
|
-
const wheelItemCountRange = useMemo4(() => getItemCountRange(nodes), [nodes]);
|
|
3349
3612
|
const sectorSpanByRootId = useMemo4(() => {
|
|
3350
3613
|
const map = /* @__PURE__ */ new Map();
|
|
3351
3614
|
if (groups.length <= 1) return map;
|
|
@@ -3395,10 +3658,10 @@ function WheelRadialCore({
|
|
|
3395
3658
|
const chipClearanceFloor = useMemo4(
|
|
3396
3659
|
() => calculateWheelRadiusForAngles(
|
|
3397
3660
|
layout.map((slot) => slot.angle),
|
|
3398
|
-
MAX_NODE_WIDTH,
|
|
3661
|
+
wheelItemCountRange.maxNodeWidth ?? MAX_NODE_WIDTH,
|
|
3399
3662
|
WHEEL_RADIUS
|
|
3400
3663
|
),
|
|
3401
|
-
[layout]
|
|
3664
|
+
[layout, wheelItemCountRange.maxNodeWidth]
|
|
3402
3665
|
);
|
|
3403
3666
|
const maxCoreExtentDelta = useMemo4(() => {
|
|
3404
3667
|
let extent = 0;
|
|
@@ -3512,12 +3775,9 @@ function WheelRadialCore({
|
|
|
3512
3775
|
wheelItemCountRange,
|
|
3513
3776
|
panel?.node.id
|
|
3514
3777
|
]);
|
|
3515
|
-
const hasInitialFitRef = useRef7(false);
|
|
3516
3778
|
useEffect7(() => {
|
|
3517
|
-
if (hasInitialFitRef.current) return;
|
|
3518
3779
|
const elements = [wheelCircleRef.current, coreSvgRef.current];
|
|
3519
3780
|
if (!elements.some(Boolean)) return;
|
|
3520
|
-
hasInitialFitRef.current = true;
|
|
3521
3781
|
panZoom.fitToFrame(elements, { silent: true });
|
|
3522
3782
|
});
|
|
3523
3783
|
const handleChipClick = (rootId) => {
|
|
@@ -3533,7 +3793,8 @@ function WheelRadialCore({
|
|
|
3533
3793
|
panZoom.centerOnElement(
|
|
3534
3794
|
element,
|
|
3535
3795
|
ZOOM_FOCUS_SCALE,
|
|
3536
|
-
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH)
|
|
3796
|
+
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),
|
|
3797
|
+
true
|
|
3537
3798
|
);
|
|
3538
3799
|
showNodeInfo(targetNode, element, viewportRef.current, { notify: false });
|
|
3539
3800
|
}, [selectedNodeId, index, panel, showNodeInfo]);
|
|
@@ -3768,6 +4029,7 @@ function WheelRadialCore({
|
|
|
3768
4029
|
{
|
|
3769
4030
|
panZoom,
|
|
3770
4031
|
orientation: controlsOrientation,
|
|
4032
|
+
zoomControls: showZoomControls,
|
|
3771
4033
|
leading: floatingControlsLeading,
|
|
3772
4034
|
busy,
|
|
3773
4035
|
onFit: () => panZoom.fitToFrame([wheelCircleRef.current, coreSvgRef.current])
|
|
@@ -3785,13 +4047,13 @@ function GenreTreeWheelRadial(props) {
|
|
|
3785
4047
|
}
|
|
3786
4048
|
|
|
3787
4049
|
// src/GenreTreeWheelRadialPopCoreBase.tsx
|
|
3788
|
-
import { useCallback as useCallback4, useEffect as useEffect8, useMemo as useMemo5, useRef as useRef8, useState as
|
|
4050
|
+
import { useCallback as useCallback4, useEffect as useEffect8, useMemo as useMemo5, useRef as useRef8, useState as useState10 } from "react";
|
|
3789
4051
|
import * as d34 from "d3";
|
|
3790
4052
|
|
|
3791
4053
|
// src/use-focus-history.ts
|
|
3792
|
-
import { useState as
|
|
4054
|
+
import { useState as useState8 } from "react";
|
|
3793
4055
|
function useFocusHistory(focusNodeId, isFocusable) {
|
|
3794
|
-
const [state, setState] =
|
|
4056
|
+
const [state, setState] = useState8({ seen: focusNodeId, stack: [], pending: void 0 });
|
|
3795
4057
|
if (state.seen !== focusNodeId) {
|
|
3796
4058
|
const popTo = state.pending === focusNodeId ? state.stack.lastIndexOf(focusNodeId) : -1;
|
|
3797
4059
|
setState({
|
|
@@ -3813,13 +4075,13 @@ function useFocusHistory(focusNodeId, isFocusable) {
|
|
|
3813
4075
|
}
|
|
3814
4076
|
|
|
3815
4077
|
// src/WheelBreadcrumb.tsx
|
|
3816
|
-
import { Fragment as Fragment3, useState as
|
|
4078
|
+
import { Fragment as Fragment3, useState as useState9 } from "react";
|
|
3817
4079
|
import { MdHome } from "react-icons/md";
|
|
3818
4080
|
import { Fragment as Fragment4, jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
3819
4081
|
var MAX_VISIBLE_ANCESTORS = 3;
|
|
3820
4082
|
var ANCESTORS_KEPT_WHEN_COLLAPSED = 2;
|
|
3821
4083
|
function WheelBreadcrumb({ ancestors, currentName, onHome, onAncestor }) {
|
|
3822
|
-
const [expanded, setExpanded] =
|
|
4084
|
+
const [expanded, setExpanded] = useState9(false);
|
|
3823
4085
|
const collapsed = !expanded && ancestors.length > MAX_VISIBLE_ANCESTORS;
|
|
3824
4086
|
const visibleAncestors = collapsed ? ancestors.slice(-ANCESTORS_KEPT_WHEN_COLLAPSED) : ancestors;
|
|
3825
4087
|
const separator = /* @__PURE__ */ jsx15("span", { "aria-hidden": "true", children: "\u203A" });
|
|
@@ -3951,6 +4213,8 @@ function WheelRadialPopCoreCore({
|
|
|
3951
4213
|
hideInfoPanelClose = false,
|
|
3952
4214
|
hideInfoPanelTitle = false,
|
|
3953
4215
|
itemCountLabel,
|
|
4216
|
+
itemCountScaleMax,
|
|
4217
|
+
nodeCountForSizing,
|
|
3954
4218
|
readOnly,
|
|
3955
4219
|
renderDangerZone,
|
|
3956
4220
|
expanded,
|
|
@@ -3960,8 +4224,10 @@ function WheelRadialPopCoreCore({
|
|
|
3960
4224
|
overlayPanel,
|
|
3961
4225
|
wheelZoom,
|
|
3962
4226
|
defaultViewport,
|
|
4227
|
+
viewportRestore,
|
|
3963
4228
|
onViewportChange,
|
|
3964
4229
|
controlsOrientation = "vertical",
|
|
4230
|
+
showZoomControls = "auto",
|
|
3965
4231
|
floatingControlsLeading,
|
|
3966
4232
|
busy,
|
|
3967
4233
|
allowWheelRotation = true,
|
|
@@ -3969,6 +4235,7 @@ function WheelRadialPopCoreCore({
|
|
|
3969
4235
|
focusNodeId: requestedFocusNodeId = null,
|
|
3970
4236
|
onFocusChange
|
|
3971
4237
|
}) {
|
|
4238
|
+
const fullNodeCountForSizing = nodeCountForSizing ?? nodes.length;
|
|
3972
4239
|
const index = useTreeIndex(nodes);
|
|
3973
4240
|
const focusNodeId = useMemo5(
|
|
3974
4241
|
() => requestedFocusNodeId !== null && isZoomableNode(nodes, requestedFocusNodeId) ? requestedFocusNodeId : null,
|
|
@@ -4022,13 +4289,13 @@ function WheelRadialPopCoreCore({
|
|
|
4022
4289
|
(node) => getGenreNodeStyle(index, node.id),
|
|
4023
4290
|
[index]
|
|
4024
4291
|
);
|
|
4025
|
-
const [topRootId, setTopRootId] =
|
|
4292
|
+
const [topRootId, setTopRootId] = useState10(
|
|
4026
4293
|
groups[0]?.root.id ?? null
|
|
4027
4294
|
);
|
|
4028
|
-
const [rotationTopRootId, setRotationTopRootId] =
|
|
4295
|
+
const [rotationTopRootId, setRotationTopRootId] = useState10(
|
|
4029
4296
|
groups[0]?.root.id ?? null
|
|
4030
4297
|
);
|
|
4031
|
-
const [renderedFocusNodeId, setRenderedFocusNodeId] =
|
|
4298
|
+
const [renderedFocusNodeId, setRenderedFocusNodeId] = useState10(focusNodeId);
|
|
4032
4299
|
if (renderedFocusNodeId !== focusNodeId) {
|
|
4033
4300
|
setRenderedFocusNodeId(focusNodeId);
|
|
4034
4301
|
setTopRootId(groups[0]?.root.id ?? null);
|
|
@@ -4037,7 +4304,8 @@ function WheelRadialPopCoreCore({
|
|
|
4037
4304
|
const viewportRef = useRef8(null);
|
|
4038
4305
|
const stageRef = useRef8(null);
|
|
4039
4306
|
const dotRadiusMemoryRef = useRef8(/* @__PURE__ */ new Map());
|
|
4040
|
-
const
|
|
4307
|
+
const viewportFrame = useMemo5(() => JSON.stringify([focusNodeId, nodes.map(({ id, parentId, side }) => [id, parentId, side])]), [nodes, focusNodeId]);
|
|
4308
|
+
const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, viewportRestore, selectedNodeId, onViewportChange, frameKey: viewportFrame });
|
|
4041
4309
|
const { panel: openPanel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });
|
|
4042
4310
|
const isInFrame = (id) => id === focusNodeId || centerSubtreeNodeIds.has(id) || ringRootById.has(id);
|
|
4043
4311
|
const panel = openPanel && isInFrame(openPanel.node.id) ? openPanel : null;
|
|
@@ -4091,7 +4359,7 @@ function WheelRadialPopCoreCore({
|
|
|
4091
4359
|
const popSvgRef = useRef8(null);
|
|
4092
4360
|
const centerChipRef = useRef8(null);
|
|
4093
4361
|
const regionLabelRef = useRef8(null);
|
|
4094
|
-
const [regionLabel, setRegionLabel] =
|
|
4362
|
+
const [regionLabel, setRegionLabel] = useState10(null);
|
|
4095
4363
|
const effectiveTopRootId = groups.some((group) => group.root.id === topRootId) ? topRootId : groups[0]?.root.id ?? null;
|
|
4096
4364
|
const effectiveRotationTopRootId = groups.some(
|
|
4097
4365
|
(group) => group.root.id === rotationTopRootId
|
|
@@ -4108,8 +4376,8 @@ function WheelRadialPopCoreCore({
|
|
|
4108
4376
|
() => getItemCountRange([
|
|
4109
4377
|
...nodes,
|
|
4110
4378
|
...[...rootItemCountById.values()].map((itemCount) => ({ itemCount }))
|
|
4111
|
-
]),
|
|
4112
|
-
[nodes, rootItemCountById]
|
|
4379
|
+
], { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }),
|
|
4380
|
+
[nodes, rootItemCountById, itemCountScaleMax, fullNodeCountForSizing]
|
|
4113
4381
|
);
|
|
4114
4382
|
const isCenterMainstream = isMainstreamPopLineage(index, centerNode.id);
|
|
4115
4383
|
const centerNodeColor = isCenterMainstream ? "#ffffff" : getGenreNodeStyle(index, centerNode.id).fill;
|
|
@@ -4143,8 +4411,8 @@ function WheelRadialPopCoreCore({
|
|
|
4143
4411
|
centerSubtreeHierarchy,
|
|
4144
4412
|
0,
|
|
4145
4413
|
POP_TREE_DEPTH_RADIAL_SPACING
|
|
4146
|
-
) : NODE_LAYOUT_SLOT_RADIUS,
|
|
4147
|
-
[centerSubtreeHierarchy]
|
|
4414
|
+
) : wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS,
|
|
4415
|
+
[centerSubtreeHierarchy, wheelItemCountRange.maxDotRadius]
|
|
4148
4416
|
);
|
|
4149
4417
|
const maxCoreExtentDelta = useMemo5(() => {
|
|
4150
4418
|
let extent = 0;
|
|
@@ -4166,7 +4434,8 @@ function WheelRadialPopCoreCore({
|
|
|
4166
4434
|
(radius) => new Map(
|
|
4167
4435
|
groups.map((group) => {
|
|
4168
4436
|
const { pop, core } = branchHierarchiesByRootId.get(group.root.id);
|
|
4169
|
-
const
|
|
4437
|
+
const dotRadius = wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS;
|
|
4438
|
+
const chipWidth = 2 * dotRadius;
|
|
4170
4439
|
return [
|
|
4171
4440
|
group.root.id,
|
|
4172
4441
|
{
|
|
@@ -4174,14 +4443,16 @@ function WheelRadialPopCoreCore({
|
|
|
4174
4443
|
(hierarchy) => measureWedge(
|
|
4175
4444
|
d34,
|
|
4176
4445
|
hierarchy,
|
|
4177
|
-
popRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING)
|
|
4446
|
+
popRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING),
|
|
4447
|
+
dotRadius
|
|
4178
4448
|
)
|
|
4179
4449
|
),
|
|
4180
4450
|
core: core.map(
|
|
4181
4451
|
(hierarchy) => measureWedge(
|
|
4182
4452
|
d34,
|
|
4183
4453
|
hierarchy,
|
|
4184
|
-
coreRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING)
|
|
4454
|
+
coreRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING),
|
|
4455
|
+
dotRadius
|
|
4185
4456
|
)
|
|
4186
4457
|
),
|
|
4187
4458
|
chip: 2 * Math.asin(Math.min(1, (chipWidth / 2 + CHIP_GAP) / radius))
|
|
@@ -4189,7 +4460,7 @@ function WheelRadialPopCoreCore({
|
|
|
4189
4460
|
];
|
|
4190
4461
|
})
|
|
4191
4462
|
),
|
|
4192
|
-
[groups, branchHierarchiesByRootId]
|
|
4463
|
+
[groups, branchHierarchiesByRootId, wheelItemCountRange.maxDotRadius]
|
|
4193
4464
|
);
|
|
4194
4465
|
const coreRootCircleRadius = useMemo5(
|
|
4195
4466
|
() => solveRingRadius(
|
|
@@ -4199,11 +4470,11 @@ function WheelRadialPopCoreCore({
|
|
|
4199
4470
|
),
|
|
4200
4471
|
Math.max(
|
|
4201
4472
|
WHEEL_POP_CORE_RADIUS,
|
|
4202
|
-
mainstreamCircleRadius + NODE_DOT_MAX_RADIUS + POP_SUBTREE_OUTER_MARGIN,
|
|
4473
|
+
mainstreamCircleRadius + (wheelItemCountRange.maxDotRadius ?? NODE_DOT_MAX_RADIUS) + POP_SUBTREE_OUTER_MARGIN,
|
|
4203
4474
|
popReachRequiredRadius
|
|
4204
4475
|
)
|
|
4205
4476
|
),
|
|
4206
|
-
[measureBranchNeedsAt, mainstreamCircleRadius, popReachRequiredRadius]
|
|
4477
|
+
[measureBranchNeedsAt, mainstreamCircleRadius, popReachRequiredRadius, wheelItemCountRange.maxDotRadius]
|
|
4207
4478
|
);
|
|
4208
4479
|
const branchNeedsByRootId = useMemo5(
|
|
4209
4480
|
() => measureBranchNeedsAt(coreRootCircleRadius),
|
|
@@ -4220,7 +4491,7 @@ function WheelRadialPopCoreCore({
|
|
|
4220
4491
|
() => computeRadialLayout(rootWeights, topIndex, LANDING_ANGLE2),
|
|
4221
4492
|
[rootWeights, topIndex]
|
|
4222
4493
|
);
|
|
4223
|
-
const [angleMemo, setAngleMemo] =
|
|
4494
|
+
const [angleMemo, setAngleMemo] = useState10(() => ({
|
|
4224
4495
|
layout,
|
|
4225
4496
|
angles: computeContinuousAngles2(groups, layout, /* @__PURE__ */ new Map())
|
|
4226
4497
|
}));
|
|
@@ -4307,7 +4578,8 @@ function WheelRadialPopCoreCore({
|
|
|
4307
4578
|
angle,
|
|
4308
4579
|
coreRootCircleRadius,
|
|
4309
4580
|
span,
|
|
4310
|
-
POP_TREE_DEPTH_RADIAL_SPACING
|
|
4581
|
+
POP_TREE_DEPTH_RADIAL_SPACING,
|
|
4582
|
+
wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS
|
|
4311
4583
|
);
|
|
4312
4584
|
const reparentForbiddenIds = reparentingNodeId ? laidOut.descendants().find((d) => d.data.id === reparentingNodeId)?.descendants().map((d) => d.data.id) ?? [] : [];
|
|
4313
4585
|
const branchGroup = sectorGroup.append("g").attr("class", "gtv-wheel-pop-branch").attr("data-gtv-branch-index", branchIndex);
|
|
@@ -4373,7 +4645,8 @@ function WheelRadialPopCoreCore({
|
|
|
4373
4645
|
angle,
|
|
4374
4646
|
span,
|
|
4375
4647
|
coreRootCircleRadius,
|
|
4376
|
-
POP_TREE_DEPTH_RADIAL_SPACING
|
|
4648
|
+
POP_TREE_DEPTH_RADIAL_SPACING,
|
|
4649
|
+
wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS
|
|
4377
4650
|
);
|
|
4378
4651
|
const reparentForbiddenIds = reparentingNodeId ? laidOut.descendants().find((d) => d.data.id === reparentingNodeId)?.descendants().map((d) => d.data.id) ?? [] : [];
|
|
4379
4652
|
const angleOf = (d) => {
|
|
@@ -4537,7 +4810,7 @@ function WheelRadialPopCoreCore({
|
|
|
4537
4810
|
originGroup.selectAll("g.gtv-root-label").select("text").style("pointer-events", "all").style("cursor", "pointer").on("click", (_event, d) => {
|
|
4538
4811
|
const chip = d.id === centerNode.id ? centerChipRef.current : viewportRef.current?.querySelector(`#group-${CSS.escape(d.id)}`);
|
|
4539
4812
|
chip?.click();
|
|
4540
|
-
}).on("dblclick", (_event, d) => focusOnNodeRef.current(d.id));
|
|
4813
|
+
}).on("dblclick", (_event, d) => chipClickRef.current.doubleClick(() => focusOnNodeRef.current(d.id)));
|
|
4541
4814
|
}, [
|
|
4542
4815
|
centerNode,
|
|
4543
4816
|
centerHasPopDisc,
|
|
@@ -4636,18 +4909,12 @@ function WheelRadialPopCoreCore({
|
|
|
4636
4909
|
index,
|
|
4637
4910
|
regionLabel
|
|
4638
4911
|
]);
|
|
4639
|
-
const hasInitialFitRef = useRef8(false);
|
|
4640
4912
|
useEffect8(() => {
|
|
4641
|
-
hasInitialFitRef.current = false;
|
|
4642
|
-
}, [focusNodeId]);
|
|
4643
|
-
useEffect8(() => {
|
|
4644
|
-
if (hasInitialFitRef.current) return;
|
|
4645
4913
|
const elements = [
|
|
4646
4914
|
wheelCircleRef.current,
|
|
4647
4915
|
...queryTreeContentElements(popSvgRef.current)
|
|
4648
4916
|
];
|
|
4649
4917
|
if (!elements.some(Boolean)) return;
|
|
4650
|
-
hasInitialFitRef.current = true;
|
|
4651
4918
|
panZoom.fitToFrame(elements, { silent: true });
|
|
4652
4919
|
});
|
|
4653
4920
|
const handleChipClick = (rootId) => {
|
|
@@ -4669,7 +4936,8 @@ function WheelRadialPopCoreCore({
|
|
|
4669
4936
|
panZoom.centerOnElement(
|
|
4670
4937
|
element,
|
|
4671
4938
|
ZOOM_FOCUS_SCALE,
|
|
4672
|
-
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH)
|
|
4939
|
+
resolveInfoPanelObscuredArea(element, viewportRef.current, INFO_PANEL_WIDTH),
|
|
4940
|
+
true
|
|
4673
4941
|
);
|
|
4674
4942
|
showNodeInfo(targetNode, element, viewportRef.current, { notify: false });
|
|
4675
4943
|
}, [selectedNodeId, index, openPanel, showNodeInfo, centerSubtreeNodeIds, ringRootById]);
|
|
@@ -4696,7 +4964,7 @@ function WheelRadialPopCoreCore({
|
|
|
4696
4964
|
)
|
|
4697
4965
|
};
|
|
4698
4966
|
});
|
|
4699
|
-
}, [groups, continuousAngleByRootId, sectorSpanByRootId]);
|
|
4967
|
+
}, [groups, continuousAngleByRootId, sectorSpanByRootId, index]);
|
|
4700
4968
|
return /* @__PURE__ */ jsxs12(
|
|
4701
4969
|
"div",
|
|
4702
4970
|
{
|
|
@@ -4806,9 +5074,9 @@ function WheelRadialPopCoreCore({
|
|
|
4806
5074
|
showNodeInfo(centerNode, chip, viewportRef.current);
|
|
4807
5075
|
onNodeClick?.(centerNode, nativeEvent);
|
|
4808
5076
|
}
|
|
4809
|
-
});
|
|
5077
|
+
}, () => focusOnNode(centerNode.id), centerNode.id);
|
|
4810
5078
|
},
|
|
4811
|
-
onDoubleClick: () => focusOnNode(centerNode.id),
|
|
5079
|
+
onDoubleClick: () => chipClickRef.current.doubleClick(() => focusOnNode(centerNode.id)),
|
|
4812
5080
|
"aria-label": centerNode.name
|
|
4813
5081
|
}
|
|
4814
5082
|
) }) }),
|
|
@@ -4868,9 +5136,9 @@ function WheelRadialPopCoreCore({
|
|
|
4868
5136
|
showNodeInfo(root, chip, viewportRef.current);
|
|
4869
5137
|
onNodeClick?.(root, nativeEvent);
|
|
4870
5138
|
}
|
|
4871
|
-
});
|
|
5139
|
+
}, () => focusOnNode(group.root.id), group.root.id);
|
|
4872
5140
|
},
|
|
4873
|
-
onDoubleClick: () => focusOnNode(group.root.id)
|
|
5141
|
+
onDoubleClick: () => chipClickRef.current.doubleClick(() => focusOnNode(group.root.id))
|
|
4874
5142
|
}
|
|
4875
5143
|
) })
|
|
4876
5144
|
},
|
|
@@ -4965,6 +5233,7 @@ function WheelRadialPopCoreCore({
|
|
|
4965
5233
|
{
|
|
4966
5234
|
panZoom,
|
|
4967
5235
|
orientation: controlsOrientation,
|
|
5236
|
+
zoomControls: showZoomControls,
|
|
4968
5237
|
leading: floatingControlsLeading,
|
|
4969
5238
|
busy,
|
|
4970
5239
|
onFit: () => panZoom.fitToFrame([
|
|
@@ -4991,7 +5260,7 @@ import {
|
|
|
4991
5260
|
useEffect as useEffect9,
|
|
4992
5261
|
useMemo as useMemo6,
|
|
4993
5262
|
useRef as useRef9,
|
|
4994
|
-
useState as
|
|
5263
|
+
useState as useState11,
|
|
4995
5264
|
useSyncExternalStore
|
|
4996
5265
|
} from "react";
|
|
4997
5266
|
import { Fragment as Fragment5, jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
@@ -5149,7 +5418,7 @@ function GenreTreeOutline(props) {
|
|
|
5149
5418
|
}
|
|
5150
5419
|
return ids;
|
|
5151
5420
|
}, [reparentingNodeId, index]);
|
|
5152
|
-
const [store] =
|
|
5421
|
+
const [store] = useState11(
|
|
5153
5422
|
() => createOutlineStore({
|
|
5154
5423
|
selectedId: null,
|
|
5155
5424
|
openIds: /* @__PURE__ */ new Set(),
|
|
@@ -5174,7 +5443,7 @@ function GenreTreeOutline(props) {
|
|
|
5174
5443
|
return;
|
|
5175
5444
|
}
|
|
5176
5445
|
const changed = store.get().selectedId !== node.id;
|
|
5177
|
-
store.set({ selectedId: node.id });
|
|
5446
|
+
if (latestProps.current.selectedNodeId === void 0) store.set({ selectedId: node.id });
|
|
5178
5447
|
if (changed) latestProps.current.onSelectedNodeChange?.(node);
|
|
5179
5448
|
onNodeClick?.(node, event.nativeEvent);
|
|
5180
5449
|
},
|
|
@@ -5206,7 +5475,7 @@ function GenreTreeOutline(props) {
|
|
|
5206
5475
|
}
|
|
5207
5476
|
const changed = store.get().selectedId !== nodeId;
|
|
5208
5477
|
if (changed) pendingScrollIdRef.current = nodeId;
|
|
5209
|
-
store.set({ openIds, selectedId: nodeId });
|
|
5478
|
+
store.set({ openIds, ...notify && latestProps.current.selectedNodeId !== void 0 ? {} : { selectedId: nodeId } });
|
|
5210
5479
|
if (changed && notify) latestProps.current.onSelectedNodeChange?.(index.byId.get(nodeId));
|
|
5211
5480
|
};
|
|
5212
5481
|
const selectNodeRef = useRef9(selectNode);
|
|
@@ -5247,7 +5516,7 @@ function GenreTreeOutline(props) {
|
|
|
5247
5516
|
ancestorNodes: index.ancestorsOf(selectedNode.id).map((n) => ({ node: n, ...model.styleOf(n) })),
|
|
5248
5517
|
side: "left",
|
|
5249
5518
|
onClose: () => {
|
|
5250
|
-
store.set({ selectedId: null });
|
|
5519
|
+
if (latestProps.current.selectedNodeId === void 0) store.set({ selectedId: null });
|
|
5251
5520
|
latestProps.current.onSelectedNodeChange?.(null);
|
|
5252
5521
|
},
|
|
5253
5522
|
onSelectNode: (id) => selectNodeRef.current(id),
|