@yuneta/gobj-ui 7.23.156 → 7.23.157
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -57
- package/dist/gobj-ui.cjs.js +55 -877
- package/dist/gobj-ui.es.js +56 -878
- package/package.json +1 -1
- package/src/c_g6_nodes_tree.js +10 -148
- package/src/c_yui_treedb_graph.js +0 -2
- package/src/treedb_elbow.js +13 -344
- package/src/treedb_elbow.test.js +1 -236
- package/src/treedb_layout.js +39 -620
- package/src/treedb_layout.test.js +3 -269
package/src/treedb_layout.js
CHANGED
|
@@ -274,414 +274,6 @@ function tidy_tree_lr(nodes, edges, o)
|
|
|
274
274
|
return pos;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
/************************************************************
|
|
278
|
-
* The compact tree.
|
|
279
|
-
*
|
|
280
|
-
* The tidy tree above gives every subtree its whole BLOCK: open a
|
|
281
|
-
* hall with twenty-four devices and its block widens, and with it
|
|
282
|
-
* every block above it, so the siblings of its ancestors move apart
|
|
283
|
-
* at depths where nothing of theirs touches. This one keeps the
|
|
284
|
-
* CONTOUR of each subtree instead -- its outline on each side, depth
|
|
285
|
-
* by depth -- and slides every sibling against the outline of the
|
|
286
|
-
* ones before it. A folded region then sits as close to an open one
|
|
287
|
-
* as their cards allow at each depth, which is what a folded treedb
|
|
288
|
-
* is: one branch open among closed ones.
|
|
289
|
-
*
|
|
290
|
-
* Same spanning tree, same order, same rows as the tidy tree: only
|
|
291
|
-
* the packing across the depths differs, so switching between the
|
|
292
|
-
* two moves cards sideways and never reorders them.
|
|
293
|
-
*
|
|
294
|
-
* And a run of three or more LEAF children -- a hall's devices, a
|
|
295
|
-
* region's places when none is open -- is STACKED: two columns
|
|
296
|
-
* under the parent with a corridor between them, row after row.
|
|
297
|
-
* That is what makes it compact when a whole level is open, where
|
|
298
|
-
* the contour alone gains nothing: every parent needed the whole
|
|
299
|
-
* row of its children beside it. The tree grows taller instead of
|
|
300
|
-
* wider. Its elbow edges run down the corridor (elbow_combs).
|
|
301
|
-
*
|
|
302
|
-
* The contour arithmetic is Moen's ("Drawing dynamic trees", IEEE
|
|
303
|
-
* Software, 1990) in the form mxGraph gave it in mxCompactTreeLayout
|
|
304
|
-
* -- Copyright (c) 2006-2015 JGraph Ltd, Apache License 2.0 -- read in
|
|
305
|
-
* maxGraph's CompactTreeLayout.ts: leaf, join, merge, offset and
|
|
306
|
-
* bridge below follow that form. What is ours: it runs on the
|
|
307
|
-
* spanning tree of a FOREST, every depth is a row as wide as its
|
|
308
|
-
* widest card (mxGraph's `alignRanks`), several roots sit side by
|
|
309
|
-
* side, and there is no recursion.
|
|
310
|
-
************************************************************/
|
|
311
|
-
export function layout_compact(nodes, edges, opts)
|
|
312
|
-
{
|
|
313
|
-
let o = Object.assign({}, DEFAULTS, opts || {});
|
|
314
|
-
if(o.direction === "LR") {
|
|
315
|
-
return compact_tree_lr(nodes, edges, o);
|
|
316
|
-
}
|
|
317
|
-
let transposed = nodes.map((n) => Object.assign({}, n, {w: n.h, h: n.w}));
|
|
318
|
-
let lr = compact_tree_lr(transposed, edges, o);
|
|
319
|
-
let pos = new Map();
|
|
320
|
-
for(let [id, p] of lr) {
|
|
321
|
-
pos.set(id, {x: p.y, y: p.x});
|
|
322
|
-
}
|
|
323
|
-
return pos;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/* One segment of a contour: a step of `dx` along the depth and `dy`
|
|
327
|
-
* across it, then the next segment. */
|
|
328
|
-
function contour_line(dx, dy, next)
|
|
329
|
-
{
|
|
330
|
-
return {dx: dx, dy: dy, next: next || null};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/* How far the segment (a1, a2) at (p1, p2) has to move across to
|
|
334
|
-
* clear the segment (b1, b2) at the origin. */
|
|
335
|
-
function contour_offset(p1, p2, a1, a2, b1, b2)
|
|
336
|
-
{
|
|
337
|
-
if(b1 <= p1 || p1 + a1 <= 0) {
|
|
338
|
-
return 0;
|
|
339
|
-
}
|
|
340
|
-
let d;
|
|
341
|
-
let t = b1 * a2 - a1 * b2;
|
|
342
|
-
if(t > 0) {
|
|
343
|
-
if(p1 < 0) {
|
|
344
|
-
d = (p1 * a2) / a1 - p2;
|
|
345
|
-
} else if(p1 > 0) {
|
|
346
|
-
d = (p1 * b2) / b1 - p2;
|
|
347
|
-
} else {
|
|
348
|
-
d = -p2;
|
|
349
|
-
}
|
|
350
|
-
} else if(b1 < p1 + a1) {
|
|
351
|
-
d = b2 - (p2 + ((b1 - p1) * a2) / a1);
|
|
352
|
-
} else if(b1 > p1 + a1) {
|
|
353
|
-
d = ((a1 + p1) * b2) / b1 - (p2 + a2);
|
|
354
|
-
} else {
|
|
355
|
-
d = b2 - (p2 + a2);
|
|
356
|
-
}
|
|
357
|
-
return (d > 0)? d : 0;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function contour_bridge(line1, x1, y1, line2, x2, y2)
|
|
361
|
-
{
|
|
362
|
-
let dx = x2 + line2.dx - x1;
|
|
363
|
-
let dy = (line2.dx === 0)? line2.dy : (dx * line2.dy) / line2.dx;
|
|
364
|
-
let r = contour_line(dx, dy, line2.next);
|
|
365
|
-
line1.next = contour_line(0, y2 + line2.dy - dy - y1, r);
|
|
366
|
-
return r;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/* Slide contour `p2` against `p1` until they clear; returns how far,
|
|
370
|
-
* and leaves in `p1` the outline of the two together. */
|
|
371
|
-
function contour_merge(p1, p2)
|
|
372
|
-
{
|
|
373
|
-
let x = 0;
|
|
374
|
-
let y = 0;
|
|
375
|
-
let total = 0;
|
|
376
|
-
let upper = p1.lower_head;
|
|
377
|
-
let lower = p2.upper_head;
|
|
378
|
-
|
|
379
|
-
while(lower && upper) {
|
|
380
|
-
let d = contour_offset(x, y, lower.dx, lower.dy, upper.dx, upper.dy);
|
|
381
|
-
y += d;
|
|
382
|
-
total += d;
|
|
383
|
-
if(x + lower.dx <= upper.dx) {
|
|
384
|
-
x += lower.dx;
|
|
385
|
-
y += lower.dy;
|
|
386
|
-
lower = lower.next;
|
|
387
|
-
} else {
|
|
388
|
-
x -= upper.dx;
|
|
389
|
-
y -= upper.dy;
|
|
390
|
-
upper = upper.next;
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if(lower) {
|
|
395
|
-
let b = contour_bridge(p1.upper_tail, 0, 0, lower, x, y);
|
|
396
|
-
p1.upper_tail = b.next? p2.upper_tail : b;
|
|
397
|
-
p1.lower_tail = p2.lower_tail;
|
|
398
|
-
} else {
|
|
399
|
-
let b = contour_bridge(p2.lower_tail, x, y, upper, 0, 0);
|
|
400
|
-
if(!b.next) {
|
|
401
|
-
p1.lower_tail = b;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
p1.lower_head = p2.lower_head;
|
|
405
|
-
return total;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/* How the compact tree STACKS a run of leaf children: two columns
|
|
409
|
-
* under their parent, a CORRIDOR between them that the elbow edges
|
|
410
|
-
* run down (treedb_elbow.js, elbow_combs), a gap between the rows
|
|
411
|
-
* that they turn into. The corridor is sized here from how many
|
|
412
|
-
* lines will run down it; the edges count the lanes from where the
|
|
413
|
-
* cards stand. */
|
|
414
|
-
export const STACK_MIN_RUN = 3; /* shorter runs stay in the row */
|
|
415
|
-
export const STACK_GAP = 40; /* between two rows of a stack */
|
|
416
|
-
export const STACK_LANE_PAD = 10; /* from a column to its first lane */
|
|
417
|
-
export const STACK_LANE_STEP = 6; /* between two lanes */
|
|
418
|
-
export const STACK_TRUNK_MIN = 36;
|
|
419
|
-
|
|
420
|
-
/* The corridor of a stack of `k` cards: every card below the first
|
|
421
|
-
* row gets a lane. */
|
|
422
|
-
export function stack_trunk_width(k)
|
|
423
|
-
{
|
|
424
|
-
let lanes = Math.max(0, k - 2);
|
|
425
|
-
return Math.max(STACK_TRUNK_MIN,
|
|
426
|
-
2 * STACK_LANE_PAD + Math.max(0, lanes - 1) * STACK_LANE_STEP);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/************************************************************
|
|
430
|
-
* The children of every node, with each run of STACK_MIN_RUN or
|
|
431
|
-
* more consecutive LEAF children replaced by one STACK. Runs, not
|
|
432
|
-
* all the leaves of a parent: the children keep their order. A
|
|
433
|
-
* stack's key is an OBJECT, so it can never be taken for a node id,
|
|
434
|
-
* which is a string. Shared by the compact tree and the radial.
|
|
435
|
-
*
|
|
436
|
-
* Returns {kids_of: Map<id, [id | stack]>, stacks: Map<stack, {ids, depth}>}
|
|
437
|
-
************************************************************/
|
|
438
|
-
function leaf_runs(t)
|
|
439
|
-
{
|
|
440
|
-
let kids_of = new Map();
|
|
441
|
-
let stacks = new Map();
|
|
442
|
-
for(let [id, kids] of t.children) {
|
|
443
|
-
let out = [];
|
|
444
|
-
let run = [];
|
|
445
|
-
let flush = () => {
|
|
446
|
-
if(run.length >= STACK_MIN_RUN) {
|
|
447
|
-
let key = {stack: stacks.size};
|
|
448
|
-
stacks.set(key, {ids: run, depth: t.depth.get(run[0])});
|
|
449
|
-
out.push(key);
|
|
450
|
-
} else {
|
|
451
|
-
out.push(...run);
|
|
452
|
-
}
|
|
453
|
-
run = [];
|
|
454
|
-
};
|
|
455
|
-
for(let kid of kids) {
|
|
456
|
-
if((t.children.get(kid) || []).length === 0) {
|
|
457
|
-
run.push(kid);
|
|
458
|
-
} else {
|
|
459
|
-
flush();
|
|
460
|
-
out.push(kid);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
flush();
|
|
464
|
-
kids_of.set(id, out);
|
|
465
|
-
}
|
|
466
|
-
return {kids_of: kids_of, stacks: stacks};
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/* Parents before children, a stack being one child (walk_order's
|
|
470
|
-
* form, over `kids_of`). */
|
|
471
|
-
function stacked_order(t, kids_of)
|
|
472
|
-
{
|
|
473
|
-
let order = [];
|
|
474
|
-
let todo = t.roots.slice().reverse();
|
|
475
|
-
while(todo.length) {
|
|
476
|
-
let id = todo.pop();
|
|
477
|
-
order.push(id);
|
|
478
|
-
let kids = kids_of.get(id) || [];
|
|
479
|
-
for(let i = kids.length - 1; i >= 0; i--) {
|
|
480
|
-
todo.push(kids[i]);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
return order;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/* A stack in the LR frame (depth along x, siblings along y): `side`
|
|
487
|
-
* is how far each column reaches across, `starts`/`row_len` where
|
|
488
|
-
* each row begins and how long it is along the depth. Its first row
|
|
489
|
-
* IS the depth's column, so it lines up with every other card there. */
|
|
490
|
-
function stack_geometry(t, st, col_x, col_w)
|
|
491
|
-
{
|
|
492
|
-
let d = st.depth;
|
|
493
|
-
let k = st.ids.length;
|
|
494
|
-
let rows = Math.ceil(k / 2);
|
|
495
|
-
let side = [0, 0];
|
|
496
|
-
let row_len = [];
|
|
497
|
-
for(let i = 0; i < k; i++) {
|
|
498
|
-
let n = t.by_id.get(st.ids[i]);
|
|
499
|
-
let r = Math.floor(i / 2);
|
|
500
|
-
side[i % 2] = Math.max(side[i % 2], n.h);
|
|
501
|
-
row_len[r] = Math.max(row_len[r] || 0, n.w);
|
|
502
|
-
}
|
|
503
|
-
row_len[0] = col_w[d];
|
|
504
|
-
let trunk = stack_trunk_width(k);
|
|
505
|
-
let starts = [col_x[d]];
|
|
506
|
-
for(let r = 1; r < rows; r++) {
|
|
507
|
-
starts[r] = starts[r - 1] + row_len[r - 1] + STACK_GAP;
|
|
508
|
-
}
|
|
509
|
-
return {
|
|
510
|
-
side: side,
|
|
511
|
-
trunk: trunk,
|
|
512
|
-
starts: starts,
|
|
513
|
-
row_len: row_len,
|
|
514
|
-
len: starts[rows - 1] + row_len[rows - 1] - col_x[d],
|
|
515
|
-
ext: side[0] + trunk + side[1],
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/* The cards of a stack whose block starts across at `top`: row by
|
|
520
|
-
* row, the first of each row against the corridor from one side,
|
|
521
|
-
* the second against it from the other. */
|
|
522
|
-
function place_stack(t, st, g, top, pos)
|
|
523
|
-
{
|
|
524
|
-
for(let i = 0; i < st.ids.length; i++) {
|
|
525
|
-
let n = t.by_id.get(st.ids[i]);
|
|
526
|
-
let r = Math.floor(i / 2);
|
|
527
|
-
let across = (i % 2 === 0)
|
|
528
|
-
? top + g.side[0] - n.h / 2
|
|
529
|
-
: top + g.side[0] + g.trunk + n.h / 2;
|
|
530
|
-
pos.set(st.ids[i], {x: g.starts[r] + g.row_len[r] / 2, y: across});
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function compact_tree_lr(nodes, edges, o)
|
|
535
|
-
{
|
|
536
|
-
let t = spanning_tree(nodes, edges);
|
|
537
|
-
let pos = new Map();
|
|
538
|
-
|
|
539
|
-
/* Runs of leaf children become STACKS (leaf_runs); the contour
|
|
540
|
-
* packing below takes a stack as one leaf of its own length, so
|
|
541
|
-
* nothing is packed under it. */
|
|
542
|
-
let {kids_of, stacks} = leaf_runs(t);
|
|
543
|
-
let order = stacked_order(t, kids_of);
|
|
544
|
-
|
|
545
|
-
/* Columns, as in the tidy tree: a depth is as wide as its widest
|
|
546
|
-
* card, and a column's gap is `ranksep`. */
|
|
547
|
-
let col_w = [];
|
|
548
|
-
for(let [id, d] of t.depth) {
|
|
549
|
-
col_w[d] = Math.max(col_w[d] || 0, t.by_id.get(id).w);
|
|
550
|
-
}
|
|
551
|
-
let col_x = [];
|
|
552
|
-
let x = 0;
|
|
553
|
-
for(let d = 0; d < col_w.length; d++) {
|
|
554
|
-
col_x[d] = x;
|
|
555
|
-
x += (col_w[d] || 0) + o.ranksep;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/* mxGraph's nodeDistance is padding on EACH side of a card, so two
|
|
559
|
-
* neighbours end `nodesep` apart; its levelDistance is what is
|
|
560
|
-
* left of the column gap after that padding. */
|
|
561
|
-
let pad = o.nodesep / 2;
|
|
562
|
-
let dist = o.nodesep;
|
|
563
|
-
let level_distance = o.ranksep - pad;
|
|
564
|
-
|
|
565
|
-
/* `len` along the depth (the column's width: the rows are aligned;
|
|
566
|
-
* a stack's own length), `ext` across it. */
|
|
567
|
-
let tn = new Map();
|
|
568
|
-
for(let id of order) {
|
|
569
|
-
let st = stacks.get(id);
|
|
570
|
-
if(st) {
|
|
571
|
-
let g = stack_geometry(t, st, col_x, col_w);
|
|
572
|
-
tn.set(id, {len: g.len, ext: g.ext, geom: g, off_x: 0, off_y: 0, y: 0, contour: null});
|
|
573
|
-
} else {
|
|
574
|
-
tn.set(id, {
|
|
575
|
-
len: col_w[t.depth.get(id)],
|
|
576
|
-
ext: t.by_id.get(id).h,
|
|
577
|
-
off_x: 0,
|
|
578
|
-
off_y: 0,
|
|
579
|
-
y: 0,
|
|
580
|
-
contour: null,
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
/* Contours, children before parents. */
|
|
586
|
-
for(let i = order.length - 1; i >= 0; i--) {
|
|
587
|
-
let id = order[i];
|
|
588
|
-
let n = tn.get(id);
|
|
589
|
-
let kids = kids_of.get(id) || [];
|
|
590
|
-
|
|
591
|
-
if(!kids.length) {
|
|
592
|
-
let upper = contour_line(n.len + dist, 0);
|
|
593
|
-
let lower_tail = contour_line(0, -n.ext - dist);
|
|
594
|
-
n.contour = {
|
|
595
|
-
upper_head: upper,
|
|
596
|
-
upper_tail: upper,
|
|
597
|
-
lower_tail: lower_tail,
|
|
598
|
-
lower_head: contour_line(n.len + dist, 0, lower_tail),
|
|
599
|
-
};
|
|
600
|
-
continue;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
/* join: the children side by side, each slid against the
|
|
604
|
-
* outline of the ones before it. */
|
|
605
|
-
let first = tn.get(kids[0]);
|
|
606
|
-
n.contour = first.contour;
|
|
607
|
-
let h = first.ext + dist;
|
|
608
|
-
let span = h;
|
|
609
|
-
for(let k = 1; k < kids.length; k++) {
|
|
610
|
-
let kid = tn.get(kids[k]);
|
|
611
|
-
let d = contour_merge(n.contour, kid.contour);
|
|
612
|
-
kid.off_y = d + h;
|
|
613
|
-
kid.off_x = 0;
|
|
614
|
-
h = kid.ext + dist;
|
|
615
|
-
span += d + h;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/* attach: the parent centred across the span of its children,
|
|
619
|
-
* one column before them. */
|
|
620
|
-
let gap = pad + level_distance;
|
|
621
|
-
let y2 = (span - n.ext) / 2 - pad;
|
|
622
|
-
let y1 = y2 + n.ext + 2 * pad - span;
|
|
623
|
-
first.off_x = gap + n.len;
|
|
624
|
-
first.off_y = y1;
|
|
625
|
-
n.contour.upper_head = contour_line(n.len, 0,
|
|
626
|
-
contour_line(gap, y1, n.contour.upper_head));
|
|
627
|
-
n.contour.lower_head = contour_line(n.len, 0,
|
|
628
|
-
contour_line(gap, y2, n.contour.lower_head));
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/* Positions across, parents before children: the first child at
|
|
632
|
-
* its offset from the parent, each next one at its offset from
|
|
633
|
-
* the one before. */
|
|
634
|
-
let root_of = new Map();
|
|
635
|
-
for(let id of order) {
|
|
636
|
-
let n = tn.get(id);
|
|
637
|
-
if(!root_of.has(id)) {
|
|
638
|
-
root_of.set(id, id);
|
|
639
|
-
}
|
|
640
|
-
let kids = kids_of.get(id) || [];
|
|
641
|
-
if(!kids.length) {
|
|
642
|
-
continue;
|
|
643
|
-
}
|
|
644
|
-
let running = n.y + tn.get(kids[0]).off_y;
|
|
645
|
-
tn.get(kids[0]).y = running;
|
|
646
|
-
root_of.set(kids[0], root_of.get(id));
|
|
647
|
-
for(let k = 1; k < kids.length; k++) {
|
|
648
|
-
let kid = tn.get(kids[k]);
|
|
649
|
-
kid.y = running + kid.off_y;
|
|
650
|
-
running = kid.y;
|
|
651
|
-
root_of.set(kids[k], root_of.get(id));
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/* Several roots: each tree after the one before, by its extent. */
|
|
656
|
-
let lo = new Map();
|
|
657
|
-
let hi = new Map();
|
|
658
|
-
for(let id of order) {
|
|
659
|
-
let r = root_of.get(id);
|
|
660
|
-
let n = tn.get(id);
|
|
661
|
-
lo.set(r, Math.min(lo.has(r)? lo.get(r) : Infinity, n.y));
|
|
662
|
-
hi.set(r, Math.max(hi.has(r)? hi.get(r) : -Infinity, n.y + n.ext));
|
|
663
|
-
}
|
|
664
|
-
let shift = new Map();
|
|
665
|
-
let top = 0;
|
|
666
|
-
for(let r of t.roots) {
|
|
667
|
-
shift.set(r, top - lo.get(r));
|
|
668
|
-
top += hi.get(r) - lo.get(r) + o.nodesep * 2;
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
for(let id of order) {
|
|
672
|
-
let n = tn.get(id);
|
|
673
|
-
let y = n.y + shift.get(root_of.get(id));
|
|
674
|
-
let st = stacks.get(id);
|
|
675
|
-
if(st) {
|
|
676
|
-
place_stack(t, st, n.geom, y, pos);
|
|
677
|
-
continue;
|
|
678
|
-
}
|
|
679
|
-
let d = t.depth.get(id);
|
|
680
|
-
pos.set(id, {x: col_x[d] + col_w[d] / 2, y: y + n.ext / 2});
|
|
681
|
-
}
|
|
682
|
-
return pos;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
277
|
/* How much of a card lies along the direction `a`: the projection
|
|
686
278
|
* of its rectangle on that direction. Full width when the card is
|
|
687
279
|
* read along its width, full height across it, and the honest
|
|
@@ -711,148 +303,76 @@ function extent_at(n, a)
|
|
|
711
303
|
*
|
|
712
304
|
* ranksep the step between rings
|
|
713
305
|
* nodesep the gap between neighbours on a ring
|
|
714
|
-
* stack (default true) a run of three or more LEAF children
|
|
715
|
-
* is a STACK, as in the compact tree: it asks its
|
|
716
|
-
* ring for one row of about the square root of its
|
|
717
|
-
* cards, not for every leaf -- a hall with a hundred
|
|
718
|
-
* devices no longer blows the ring up -- and lays
|
|
719
|
-
* them in rows going OUTWARD, each row as full as
|
|
720
|
-
* the room it has: rings round the parent when it is
|
|
721
|
-
* alone, a fan inside its sector when it is not.
|
|
722
|
-
* false keeps every leaf on the ring.
|
|
723
306
|
************************************************************/
|
|
724
307
|
export function layout_radial(nodes, edges, opts)
|
|
725
308
|
{
|
|
726
|
-
let o = Object.assign({}, DEFAULTS,
|
|
309
|
+
let o = Object.assign({}, DEFAULTS, opts || {});
|
|
727
310
|
let t = spanning_tree(nodes, edges);
|
|
728
311
|
let pos = new Map();
|
|
729
312
|
if(!t.roots.length) {
|
|
730
313
|
return pos;
|
|
731
314
|
}
|
|
732
315
|
|
|
733
|
-
|
|
734
|
-
let
|
|
735
|
-
let
|
|
736
|
-
let order = stacked_order(t, kids_of);
|
|
737
|
-
|
|
738
|
-
/* A stack asks its ring for one row of about the square root of
|
|
739
|
-
* its cards; how many each row really holds is decided once the
|
|
740
|
-
* ring's radius is known (pass 2), by the room it has there. */
|
|
741
|
-
for(let st of stacks.values()) {
|
|
742
|
-
st.per_row = Math.ceil(Math.sqrt(st.ids.length));
|
|
743
|
-
st.cards = st.ids.map((id) => t.by_id.get(id));
|
|
744
|
-
}
|
|
745
|
-
/* A stack across its ring, at angle `a`: a card's width there, and
|
|
746
|
-
* the row it asks for. Along the radius: how far one row reaches. */
|
|
747
|
-
let stack_across = (st, a) => {
|
|
748
|
-
let w = 0;
|
|
749
|
-
for(let c of st.cards) {
|
|
750
|
-
w = Math.max(w, extent_at(c, a + Math.PI / 2));
|
|
751
|
-
}
|
|
752
|
-
return {card: w, row: st.per_row * w + (st.per_row - 1) * o.nodesep};
|
|
753
|
-
};
|
|
754
|
-
let stack_along = (st, a) => {
|
|
755
|
-
let e = 0;
|
|
756
|
-
for(let c of st.cards) {
|
|
757
|
-
e = Math.max(e, extent_at(c, a));
|
|
758
|
-
}
|
|
759
|
-
return e;
|
|
760
|
-
};
|
|
761
|
-
/* The widest a set of cards is along the direction `th`, over
|
|
762
|
-
* the arc a-half..a+half: a stack's cards stand anywhere in its
|
|
763
|
-
* sector, and a card is wider slanted than square. */
|
|
764
|
-
let widest_along = (cards, a, half) => {
|
|
765
|
-
let e = 0;
|
|
766
|
-
for(let k = 0; k <= 16; k++) {
|
|
767
|
-
let th = a - half + 2 * half * k / 16;
|
|
768
|
-
for(let c of cards) {
|
|
769
|
-
e = Math.max(e, extent_at(c, th));
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
return e;
|
|
773
|
-
};
|
|
774
|
-
let depth_of = (id) => stacks.has(id)? stacks.get(id).depth : t.depth.get(id);
|
|
775
|
-
let circ = (a, b) => {
|
|
776
|
-
let d = Math.abs(a - b) % (2 * Math.PI);
|
|
777
|
-
return (d > Math.PI)? 2 * Math.PI - d : d;
|
|
778
|
-
};
|
|
779
|
-
|
|
780
|
-
/* What each node needs across its ring, the sector it deserves: a
|
|
781
|
-
* card is one, a stack is one ROW of cards, a parent the sum of
|
|
782
|
-
* its children. Post-order. */
|
|
783
|
-
let weight = new Map();
|
|
316
|
+
/* Leaves under each node: the sector it deserves. Post-order. */
|
|
317
|
+
let order = walk_order(t);
|
|
318
|
+
let leaves = new Map();
|
|
784
319
|
for(let i = order.length - 1; i >= 0; i--) {
|
|
785
320
|
let id = order[i];
|
|
786
|
-
let
|
|
787
|
-
if(st) {
|
|
788
|
-
weight.set(id, st.per_row);
|
|
789
|
-
continue;
|
|
790
|
-
}
|
|
791
|
-
let kids = kids_of.get(id) || [];
|
|
321
|
+
let kids = t.children.get(id) || [];
|
|
792
322
|
if(!kids.length) {
|
|
793
|
-
|
|
323
|
+
leaves.set(id, 1);
|
|
794
324
|
continue;
|
|
795
325
|
}
|
|
796
326
|
let n = 0;
|
|
797
327
|
for(let kid of kids) {
|
|
798
|
-
n +=
|
|
328
|
+
n += leaves.get(kid);
|
|
799
329
|
}
|
|
800
|
-
|
|
330
|
+
leaves.set(id, n);
|
|
801
331
|
}
|
|
802
332
|
let total = 0;
|
|
803
333
|
for(let root of t.roots) {
|
|
804
|
-
total +=
|
|
334
|
+
total += leaves.get(root);
|
|
805
335
|
}
|
|
806
336
|
|
|
807
337
|
/* One root sits at the centre; several sit on the first ring
|
|
808
338
|
* around an empty centre, so every depth shifts out by one. */
|
|
809
339
|
let single = (t.roots.length === 1);
|
|
810
|
-
let ring_of = (id) =>
|
|
340
|
+
let ring_of = (id) => t.depth.get(id) + (single? 0 : 1);
|
|
811
341
|
|
|
812
342
|
/* Pass 1, the ANGLES: a node's children split its sector by
|
|
813
|
-
* their
|
|
343
|
+
* their leaves, and a node sits at the middle of its sector.
|
|
814
344
|
* Angles do not depend on the radii, so they come first. */
|
|
815
345
|
let angle = new Map();
|
|
816
|
-
let rings = []; /* ring -> [{id, angle,
|
|
346
|
+
let rings = []; /* ring -> [{id, angle, diag}] */
|
|
817
347
|
/* An explicit stack, not recursion: the sectors of a ring are
|
|
818
348
|
* sorted by angle in pass 2, so the order they are pushed in does
|
|
819
349
|
* not matter -- only that every node gets its own. */
|
|
820
350
|
let sector = (root_id, root_from, root_span, root_parent) => {
|
|
821
|
-
let
|
|
822
|
-
while(
|
|
823
|
-
let {id, from, span, parent} =
|
|
351
|
+
let stack = [{id: root_id, from: root_from, span: root_span, parent: root_parent}];
|
|
352
|
+
while(stack.length) {
|
|
353
|
+
let {id, from, span, parent} = stack.pop();
|
|
824
354
|
let a = from + span / 2;
|
|
825
355
|
angle.set(id, a);
|
|
826
356
|
let ring = ring_of(id);
|
|
827
357
|
if(!rings[ring]) {
|
|
828
358
|
rings[ring] = [];
|
|
829
359
|
}
|
|
830
|
-
let
|
|
831
|
-
let n = st? null : t.by_id.get(id);
|
|
360
|
+
let n = t.by_id.get(id);
|
|
832
361
|
rings[ring].push({
|
|
833
362
|
id: id,
|
|
834
363
|
angle: a,
|
|
835
|
-
|
|
836
|
-
/* How much it needs along the ring: a card's diagonal,
|
|
837
|
-
* a stack's first row. */
|
|
838
|
-
diag: st? stack_across(st, a).row : Math.hypot(n.w, n.h),
|
|
364
|
+
diag: Math.hypot(n.w, n.h),
|
|
839
365
|
/* How much of the card lies along the RADIUS at this
|
|
840
366
|
* angle -- the projection of the rectangle on that
|
|
841
367
|
* direction -- for itself and for the node it hangs
|
|
842
368
|
* from, which is the one it has to clear. */
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
* first row stands at every angle. */
|
|
846
|
-
ext: st? widest_along(st.cards, a, Math.min(span / 2, Math.PI))
|
|
847
|
-
: extent_at(n, a),
|
|
848
|
-
parent_ext: !parent? 0
|
|
849
|
-
: (st? widest_along([t.by_id.get(parent)], a, Math.min(span / 2, Math.PI))
|
|
850
|
-
: extent_at(t.by_id.get(parent), a)),
|
|
369
|
+
ext: extent_at(n, a),
|
|
370
|
+
parent_ext: parent? extent_at(t.by_id.get(parent), a) : 0,
|
|
851
371
|
});
|
|
852
372
|
let cursor = from;
|
|
853
|
-
for(let kid of (
|
|
854
|
-
let part = span *
|
|
855
|
-
|
|
373
|
+
for(let kid of (t.children.get(id) || [])) {
|
|
374
|
+
let part = span * leaves.get(kid) / leaves.get(id);
|
|
375
|
+
stack.push({id: kid, from: cursor, span: part, parent: id});
|
|
856
376
|
cursor += part;
|
|
857
377
|
}
|
|
858
378
|
}
|
|
@@ -861,15 +381,15 @@ export function layout_radial(nodes, edges, opts)
|
|
|
861
381
|
let root = t.roots[0];
|
|
862
382
|
angle.set(root, 0);
|
|
863
383
|
let cursor = -Math.PI / 2; /* the first child at the top */
|
|
864
|
-
for(let kid of (
|
|
865
|
-
let part = 2 * Math.PI *
|
|
384
|
+
for(let kid of (t.children.get(root) || [])) {
|
|
385
|
+
let part = 2 * Math.PI * leaves.get(kid) / leaves.get(root);
|
|
866
386
|
sector(kid, cursor, part, root);
|
|
867
387
|
cursor += part;
|
|
868
388
|
}
|
|
869
389
|
} else {
|
|
870
390
|
let cursor = -Math.PI / 2;
|
|
871
391
|
for(let root of t.roots) {
|
|
872
|
-
let part = 2 * Math.PI *
|
|
392
|
+
let part = 2 * Math.PI * leaves.get(root) / total;
|
|
873
393
|
sector(root, cursor, part, null);
|
|
874
394
|
cursor += part;
|
|
875
395
|
}
|
|
@@ -893,132 +413,31 @@ export function layout_radial(nodes, edges, opts)
|
|
|
893
413
|
* the one it HANGS FROM, in the direction it hangs in -- the
|
|
894
414
|
* two are radially aligned, or nearly, since a child lives
|
|
895
415
|
* inside its parent's sector -- and the ring goes out far
|
|
896
|
-
* enough for the worst of them.
|
|
897
|
-
*
|
|
898
|
-
* Then the STACKS of the ring get their rows: each row holds as
|
|
899
|
-
* many cards as fit in the room the stack has -- its own sector,
|
|
900
|
-
* less what its neighbours on the ring take up -- at that row's
|
|
901
|
-
* radius. A stack alone on its ring has the whole circle, and its
|
|
902
|
-
* rows are rings round its parent; one between neighbours fans
|
|
903
|
-
* out inside its sector, wider as it goes. The rows reaching out
|
|
904
|
-
* are neighbours of the outer rings too, by the ANGLE they cover. */
|
|
905
|
-
let fit = (list, r0) => {
|
|
906
|
-
let r = r0;
|
|
907
|
-
let sorted = list.slice().sort((a, b) => a.angle - b.angle);
|
|
908
|
-
for(let i = 0; i < sorted.length && sorted.length > 1; i++) {
|
|
909
|
-
let a = sorted[i];
|
|
910
|
-
let b = sorted[(i + 1) % sorted.length];
|
|
911
|
-
let gap = (i + 1 < sorted.length)
|
|
912
|
-
? b.angle - a.angle
|
|
913
|
-
: b.angle + 2 * Math.PI - a.angle;
|
|
914
|
-
if(gap > 1e-9) {
|
|
915
|
-
r = Math.max(r, (a.diag / 2 + b.diag / 2 + o.nodesep) / gap);
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
return r;
|
|
919
|
-
};
|
|
416
|
+
* enough for the worst of them. */
|
|
920
417
|
let radius = [0];
|
|
921
|
-
let reaching = []; /* planned stacks: {angle, half, reach} */
|
|
922
418
|
for(let ring = 1; ring < rings.length; ring++) {
|
|
923
|
-
let members = rings[ring] || [];
|
|
924
419
|
let r = radius[ring - 1] + o.ranksep;
|
|
925
|
-
let
|
|
926
|
-
for(let n of members) {
|
|
420
|
+
for(let n of (rings[ring] || [])) {
|
|
927
421
|
r = Math.max(r, radius[ring - 1] + (n.parent_ext + n.ext) / 2 + o.nodesep);
|
|
928
|
-
band = Math.max(band, n.ext);
|
|
929
422
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
for(let m of members) {
|
|
940
|
-
let d = circ(g.angle, m.angle);
|
|
941
|
-
if(d > g.half + 1e-9) {
|
|
942
|
-
r = Math.max(r, (m.diag / 2 + o.nodesep) / (d - g.half));
|
|
943
|
-
} else {
|
|
944
|
-
r = Math.max(r, g.reach + m.ext / 2 + o.nodesep);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
423
|
+
let list = (rings[ring] || []).slice().sort((a, b) => a.angle - b.angle);
|
|
424
|
+
for(let i = 0; i < list.length && list.length > 1; i++) {
|
|
425
|
+
let a = list[i];
|
|
426
|
+
let b = list[(i + 1) % list.length];
|
|
427
|
+
let gap = (i + 1 < list.length)
|
|
428
|
+
? b.angle - a.angle
|
|
429
|
+
: b.angle + 2 * Math.PI - a.angle;
|
|
430
|
+
if(gap > 1e-9) {
|
|
431
|
+
r = Math.max(r, (a.diag / 2 + b.diag / 2 + o.nodesep) / gap);
|
|
947
432
|
}
|
|
948
433
|
}
|
|
949
434
|
radius[ring] = r;
|
|
950
|
-
|
|
951
|
-
let sorted = members.slice().sort((a, b) => a.angle - b.angle);
|
|
952
|
-
for(let i = 0; i < sorted.length; i++) {
|
|
953
|
-
let m = sorted[i];
|
|
954
|
-
let st = stacks.get(m.id);
|
|
955
|
-
if(!st) {
|
|
956
|
-
continue;
|
|
957
|
-
}
|
|
958
|
-
/* The room each side: to the neighbour, less its half and
|
|
959
|
-
* the gap; never past the stack's own sector. */
|
|
960
|
-
let room = m.span / 2;
|
|
961
|
-
if(sorted.length > 1) {
|
|
962
|
-
let left = sorted[(i - 1 + sorted.length) % sorted.length];
|
|
963
|
-
let right = sorted[(i + 1) % sorted.length];
|
|
964
|
-
room = Math.min(room,
|
|
965
|
-
circ(m.angle, left.angle) - (left.diag / 2 + o.nodesep) / r,
|
|
966
|
-
circ(m.angle, right.angle) - (right.diag / 2 + o.nodesep) / r);
|
|
967
|
-
}
|
|
968
|
-
room = Math.min(room, Math.PI);
|
|
969
|
-
/* Measured over the whole arc it will cover, not at its
|
|
970
|
-
* middle: round a parent a card stands at every angle,
|
|
971
|
-
* and a 172x96 card is wider at 45 degrees than facing
|
|
972
|
-
* down. */
|
|
973
|
-
let across = 0;
|
|
974
|
-
let along = 0;
|
|
975
|
-
for(let k = 0; k <= 16; k++) {
|
|
976
|
-
let th = m.angle - room + 2 * room * k / 16;
|
|
977
|
-
for(let c of st.cards) {
|
|
978
|
-
across = Math.max(across, extent_at(c, th + Math.PI / 2));
|
|
979
|
-
along = Math.max(along, extent_at(c, th));
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
let rows = [];
|
|
983
|
-
let left_to_place = st.ids.length;
|
|
984
|
-
let rr = r;
|
|
985
|
-
let half = 0;
|
|
986
|
-
while(left_to_place > 0) {
|
|
987
|
-
let fits = Math.floor(2 * room * rr / (across + o.nodesep));
|
|
988
|
-
let cnt = Math.max(1, Math.min(left_to_place, fits));
|
|
989
|
-
rows.push({rr: rr, cnt: cnt});
|
|
990
|
-
half = Math.max(half, ((cnt * (across + o.nodesep) - o.nodesep) / 2) / rr);
|
|
991
|
-
left_to_place -= cnt;
|
|
992
|
-
rr += along + STACK_GAP;
|
|
993
|
-
}
|
|
994
|
-
st.plan = {angle: m.angle, across: across, rows: rows};
|
|
995
|
-
reaching.push({
|
|
996
|
-
angle: m.angle,
|
|
997
|
-
half: half,
|
|
998
|
-
reach: rows[rows.length - 1].rr + along / 2,
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
435
|
}
|
|
1002
436
|
|
|
1003
|
-
/* Pass 3, the positions
|
|
1004
|
-
* stack's cards row by row outward, each row centred on the
|
|
1005
|
-
* stack's angle, a card's width plus the gap apart along the arc. */
|
|
437
|
+
/* Pass 3, the positions. */
|
|
1006
438
|
for(let [id, a] of angle) {
|
|
1007
|
-
let
|
|
1008
|
-
|
|
1009
|
-
let r0 = radius[ring_of(id)] || 0;
|
|
1010
|
-
pos.set(id, {x: r0 * Math.cos(a), y: r0 * Math.sin(a)});
|
|
1011
|
-
continue;
|
|
1012
|
-
}
|
|
1013
|
-
let i = 0;
|
|
1014
|
-
for(let row of st.plan.rows) {
|
|
1015
|
-
let step = (st.plan.across + o.nodesep) / row.rr;
|
|
1016
|
-
for(let j = 0; j < row.cnt; j++) {
|
|
1017
|
-
let th = a + (j - (row.cnt - 1) / 2) * step;
|
|
1018
|
-
pos.set(st.ids[i], {x: row.rr * Math.cos(th), y: row.rr * Math.sin(th)});
|
|
1019
|
-
i++;
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
439
|
+
let r = radius[ring_of(id)] || 0;
|
|
440
|
+
pos.set(id, {x: r * Math.cos(a), y: r * Math.sin(a)});
|
|
1022
441
|
}
|
|
1023
442
|
|
|
1024
443
|
return pos;
|