@quolu/lattice 0.12.16 → 0.12.17
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/package.json +1 -1
- package/src/todo-gantt-html.mjs +1 -1
- package/src/todo-gantt-layout.mjs +42 -9
package/package.json
CHANGED
package/src/todo-gantt-html.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from './todo-markdown-renderer.mjs';
|
|
7
7
|
import { renderTodoGanttSvg, TODO_GANTT_STATUS_PRESENTATION } from './todo-gantt-svg.mjs';
|
|
8
8
|
|
|
9
|
-
export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.
|
|
9
|
+
export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v14';
|
|
10
10
|
export const TODO_GANTT_PROSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
11
11
|
export const TODO_GANTT_HTML_MAX_BYTES = 24 * 1024 * 1024;
|
|
12
12
|
|
|
@@ -4,6 +4,9 @@ const TASK_LIMIT = 2_000;
|
|
|
4
4
|
const EDGE_LIMIT = 8_000;
|
|
5
5
|
const SWEEP_ROUNDS = 4;
|
|
6
6
|
|
|
7
|
+
/** Columns for a stage made only of ToDos with no registered dependency. */
|
|
8
|
+
const LOOSE_COLUMNS_MINIMUM = 8;
|
|
9
|
+
|
|
7
10
|
const GEOMETRY = Object.freeze({
|
|
8
11
|
left: 16,
|
|
9
12
|
top: 16,
|
|
@@ -11,6 +14,7 @@ const GEOMETRY = Object.freeze({
|
|
|
11
14
|
lane_gap: 296,
|
|
12
15
|
node_width: 272,
|
|
13
16
|
node_height: 68,
|
|
17
|
+
stage_row_gap: 12,
|
|
14
18
|
route_inset: 8,
|
|
15
19
|
route_spacing: 12,
|
|
16
20
|
});
|
|
@@ -410,9 +414,27 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
|
|
|
410
414
|
const nodesByKey = new Map(nodes.map((node) => [node.key, node]));
|
|
411
415
|
const { incoming, outgoing, wave } = assignWaves(nodes, nodesByKey, edges);
|
|
412
416
|
const layers = orderLayers(nodes, wave, incoming, outgoing);
|
|
417
|
+
// A ToDo with no registered dependency has nothing to line up with. Laying
|
|
418
|
+
// every one of them out in a single row makes the canvas as wide as the plan
|
|
419
|
+
// is old — real stores have hundreds — so they wrap into a block underneath
|
|
420
|
+
// the wired ToDos of their stage. Nodes that carry edges keep the top row:
|
|
421
|
+
// edge routing assumes both endpoints of a stage share one baseline.
|
|
422
|
+
const wired = new Set();
|
|
423
|
+
for (const edge of edges) { wired.add(edge.from); wired.add(edge.to); }
|
|
413
424
|
const transversePosition = new Map();
|
|
425
|
+
const stageRow = new Map();
|
|
426
|
+
const stageRowCount = [];
|
|
414
427
|
for (const layer of layers) {
|
|
415
|
-
|
|
428
|
+
const connected = layer.filter((key) => wired.has(key));
|
|
429
|
+
const loose = layer.filter((key) => !wired.has(key));
|
|
430
|
+
connected.forEach((key, index) => { transversePosition.set(key, index); stageRow.set(key, 0); });
|
|
431
|
+
const columns = Math.max(LOOSE_COLUMNS_MINIMUM, connected.length);
|
|
432
|
+
const firstLooseRow = connected.length === 0 ? 0 : 1;
|
|
433
|
+
loose.forEach((key, index) => {
|
|
434
|
+
transversePosition.set(key, index % columns);
|
|
435
|
+
stageRow.set(key, firstLooseRow + Math.floor(index / columns));
|
|
436
|
+
});
|
|
437
|
+
stageRowCount.push(Math.max(0, ...layer.map((key) => stageRow.get(key))) + 1);
|
|
416
438
|
}
|
|
417
439
|
const visibleKeys = new Set(nodes.map(({ key }) => key));
|
|
418
440
|
const displayBranches = edges.flatMap((edge, semanticIndex) => {
|
|
@@ -458,15 +480,25 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
|
|
|
458
480
|
const nodeWidth = Math.max(GEOMETRY.node_width,
|
|
459
481
|
GEOMETRY.route_inset * 2 + (maximumPortTraffic + 1) * GEOMETRY.route_spacing);
|
|
460
482
|
const laneGap = nodeWidth + 24;
|
|
461
|
-
|
|
462
|
-
|
|
483
|
+
// Each routing band is sized for the edges that actually cross it. One
|
|
484
|
+
// crowded band used to set the spacing for every stage in the diagram, so a
|
|
485
|
+
// long plan paid the width of its busiest junction on all of its stages.
|
|
486
|
+
const gapBand = (gap) => Math.max(36, ((gapGroups.get(gap)?.length ?? 0)
|
|
487
|
+
+ (connectorGroupKeysByGap.get(gap)?.size ?? 0) + 1) * GEOMETRY.route_spacing);
|
|
488
|
+
const stageHeight = (index) => stageRowCount[index] * GEOMETRY.node_height
|
|
489
|
+
+ (stageRowCount[index] - 1) * GEOMETRY.stage_row_gap;
|
|
490
|
+
const stageTop = [GEOMETRY.top];
|
|
491
|
+
for (let index = 0; index + 1 < layers.length; index += 1) {
|
|
492
|
+
stageTop.push(stageTop[index] + stageHeight(index) + gapBand(index));
|
|
493
|
+
}
|
|
463
494
|
|
|
464
495
|
const coordinates = new Map();
|
|
465
496
|
const projectedNodes = nodes.map((node) => {
|
|
466
497
|
const visible = visibleKeys.has(node.key);
|
|
467
498
|
const geometry = visible ? {
|
|
468
499
|
x: GEOMETRY.left + transversePosition.get(node.key) * laneGap,
|
|
469
|
-
y:
|
|
500
|
+
y: stageTop[wave.get(node.key)]
|
|
501
|
+
+ stageRow.get(node.key) * (GEOMETRY.node_height + GEOMETRY.stage_row_gap),
|
|
470
502
|
width: nodeWidth,
|
|
471
503
|
height: GEOMETRY.node_height,
|
|
472
504
|
} : null;
|
|
@@ -486,10 +518,11 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
|
|
|
486
518
|
const gapPosition = (edgeKey, waveIndex) => {
|
|
487
519
|
const keys = gapGroups.get(waveIndex);
|
|
488
520
|
const index = keys.indexOf(edgeKey);
|
|
489
|
-
return
|
|
521
|
+
return stageTop[waveIndex] + stageHeight(waveIndex)
|
|
490
522
|
+ GEOMETRY.route_spacing * (index + 1);
|
|
491
523
|
};
|
|
492
|
-
const maximumLayerWidth = Math.max(
|
|
524
|
+
const maximumLayerWidth = Math.max(1,
|
|
525
|
+
...[...transversePosition.values()].map((position) => position + 1));
|
|
493
526
|
const routeRight = GEOMETRY.left + (maximumLayerWidth - 1) * laneGap + nodeWidth + 12;
|
|
494
527
|
const branchCounts = new Map();
|
|
495
528
|
for (const branch of displayBranches) {
|
|
@@ -551,7 +584,7 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
|
|
|
551
584
|
connectorGapRanks.set(targetGap, groupRank + 1);
|
|
552
585
|
const primaryTargetPortX = group[0].route.at(-1)[0];
|
|
553
586
|
const junction = [primaryTargetPortX,
|
|
554
|
-
|
|
587
|
+
stageTop[targetGap] + stageHeight(targetGap)
|
|
555
588
|
+ GEOMETRY.route_spacing * ((gapGroups.get(targetGap)?.length ?? 0) + groupRank + 1)];
|
|
556
589
|
const contacts = [];
|
|
557
590
|
for (const [index, edge] of group.entries()) {
|
|
@@ -602,8 +635,8 @@ export function layoutTodoGantt(readModel, chainProjection, options = {}) {
|
|
|
602
635
|
sweep: { method: 'stable_median', rounds: SWEEP_ROUNDS, tie_break: 'previous_position_then_task_ref' },
|
|
603
636
|
bounds: {
|
|
604
637
|
width: nodes.length === 0 ? 0 : routeMaximumX + GEOMETRY.left,
|
|
605
|
-
height: nodes.length === 0 ? 0
|
|
606
|
-
+ (layers.length - 1)
|
|
638
|
+
height: nodes.length === 0 ? 0
|
|
639
|
+
: GEOMETRY.top + stageTop.at(-1) + stageHeight(layers.length - 1),
|
|
607
640
|
wave_count: layers.length,
|
|
608
641
|
},
|
|
609
642
|
nodes: projectedNodes,
|