| … | … | @@ -25,6 +25,14 @@ const DEFAULTS = { |
| 25 | 25 | pad: 16, // container padding around its contents |
| 26 | 26 | labelH: 18, // extra headroom inside a container for its label |
| 27 | 27 | |
| 28 | // Separation between GROUPS, as opposed to between cards. | |
| 29 | // | |
| 30 | // A group is not a card and must not get a card's gap. With both at 26px, two | |
| 31 | // category boxes sat 26px apart and read as one stack — reported twice as the | |
| 32 | // groups overlapping, when in fact they never intersected. Each group needs to | |
| 33 | // occupy its own area of the page. | |
| 34 | groupGap: 90, | |
| 35 | ||
| 28 | 36 | // How tall one layer may grow before it wraps into another column. |
| 29 | 37 | // |
| 30 | 38 | // Layering decides x, but a layer with many members and no edges to spread |
| … | … | @@ -173,23 +181,33 @@ export function layout (input, opts = {}) { |
| 173 | 181 | // layer starts — so a supplier still sits left of its consumer. |
| 174 | 182 | let y = 0 |
| 175 | 183 | let colW = 0 |
| 176 | let layerW = 0 | |
| 184 | let holdsGroup = false | |
| 185 | let prev = null | |
| 177 | 186 | for (const item of col) { |
| 187 | // The gap BEFORE this item: a group on either side of the boundary earns | |
| 188 | // the group gap, otherwise it is a card gap. | |
| 189 | if (prev !== null) { | |
| 190 | const gap = (prev.kind === 'container' || item.kind === 'container') | |
| 191 | ? O.groupGap | |
| 192 | : O.rowGap | |
| 193 | y += gap | |
| 194 | } | |
| 178 | 195 | if (y > 0 && y + item.h > O.maxColumnHeight) { |
| 179 | x += colW + O.colGap | |
| 180 | layerW += colW + O.colGap | |
| 196 | x += colW + (holdsGroup ? O.groupGap : O.colGap) | |
| 181 | 197 | y = 0 |
| 182 | 198 | colW = 0 |
| 199 | holdsGroup = false | |
| 183 | 200 | } |
| 184 | 201 | placed.push({ id: item.id, kind: item.kind, dx: x, dy: y, w: item.w, h: item.h }) |
| 185 | y += item.h + O.rowGap | |
| 202 | y += item.h | |
| 186 | 203 | if (item.w > colW) colW = item.w |
| 187 | if (y - O.rowGap > maxH) maxH = y - O.rowGap | |
| 204 | if (item.kind === 'container') holdsGroup = true | |
| 205 | if (y > maxH) maxH = y | |
| 206 | prev = item | |
| 188 | 207 | } |
| 189 | void layerW | |
| 190 | x += colW + O.colGap | |
| 208 | x += colW + (holdsGroup ? O.groupGap : O.colGap) | |
| 191 | 209 | } |
| 192 | const contentW = Math.max(0, x - O.colGap) | |
| 210 | const contentW = Math.max(0, x - Math.min(O.colGap, O.groupGap)) | |
| 193 | 211 | |
| 194 | 212 | const isRoot = gid === ROOT |
| 195 | 213 | const padL = isRoot ? 0 : O.pad |
| … | … | @@ -91,6 +91,42 @@ test('layout: SIBLING CONTAINERS CANNOT OVERLAP — the bug this exists to kill' |
| 91 | 91 | t.absent(contains(c.zA, n.loose), 'a node outside every zone is not captured by one') |
| 92 | 92 | }) |
| 93 | 93 | |
| 94 | test('layout: sibling containers get their OWN breathing room, not the card gap', (t) => { | |
| 95 | // Reported twice as "the groups overlap" / "boxes stacked on top of each | |
| 96 | // other". They never intersected — they were separated by the same 26px used | |
| 97 | // between cards INSIDE a box, so two groups butted together read as one stack. | |
| 98 | // A group is not a card and does not get a card's gap. | |
| 99 | const out = layout({ | |
| 100 | nodes: [node('a', 'zA'), node('b', 'zB'), node('c', 'zC')], | |
| 101 | links: [], | |
| 102 | containers: [ | |
| 103 | { id: 'zA', parent: null, label: 'A' }, | |
| 104 | { id: 'zB', parent: null, label: 'B' }, | |
| 105 | { id: 'zC', parent: null, label: 'C' } | |
| 106 | ] | |
| 107 | }, { groupGap: 90, rowGap: 26 }) | |
| 108 | ||
| 109 | const boxes = out.containers.slice().sort((p, q) => p.y - q.y) | |
| 110 | for (let i = 1; i < boxes.length; i++) { | |
| 111 | const gap = boxes[i].y - (boxes[i - 1].y + boxes[i - 1].h) | |
| 112 | t.ok(gap >= 90, 'gap between group ' + (i - 1) + ' and ' + i + ' is ' + gap + ', not a card gap') | |
| 113 | } | |
| 114 | }) | |
| 115 | ||
| 116 | test('layout: cards inside a container keep the tight card gap', (t) => { | |
| 117 | // The separation is for GROUPS. Cards within one group stay close together, | |
| 118 | // or every box becomes mostly empty space. | |
| 119 | const out = layout({ | |
| 120 | nodes: [node('a', 'z'), node('b', 'z')], | |
| 121 | links: [], | |
| 122 | containers: [{ id: 'z', parent: null, label: 'z' }] | |
| 123 | }, { groupGap: 90, rowGap: 26 }) | |
| 124 | ||
| 125 | const ns = out.nodes.slice().sort((p, q) => p.y - q.y) | |
| 126 | const gap = ns[1].y - (ns[0].y + ns[0].h) | |
| 127 | t.is(gap, 26, 'cards keep the card gap') | |
| 128 | }) | |
| 129 | ||
| 94 | 130 | test('layout: nesting — a parent container strictly contains its child', (t) => { |
| 95 | 131 | const out = layout({ |
| 96 | 132 | nodes: [node('outer1', 'zOuter'), node('inner1', 'zInner'), node('inner2', 'zInner')], |