aboutsummaryrefslogtreecommitdiff
path: root/src/layout.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/layout.js')
-rw-r--r--src/layout.js171
1 files changed, 171 insertions, 0 deletions
diff --git a/src/layout.js b/src/layout.js
new file mode 100644
index 0000000..9cfd210
--- /dev/null
+++ b/src/layout.js
@@ -0,0 +1,171 @@
+export {defaultLayout, initTree};
+
+const DEFAULT_TILE_SPACING = 5;
+const DEFAULT_HEADER_SZ = 20;
+
+const staticSqrLayout = { //determines layout for squares in a specified rectangle, with spacing
+ TILE_SPACING: DEFAULT_TILE_SPACING,
+ HEADER_SZ: DEFAULT_HEADER_SZ,
+ genLayout(nodes, x0, y0, w, h){
+ //get number-of-columns with highest occupied-fraction of rectangles with aspect-ratio w/h
+ //account for tile-spacing?, account for parent-box-border?,
+ let numTiles = nodes.length, ar = w/h;
+ let numCols, numRows, bestFrac = 0;
+ for (let nc = 1; nc <= numTiles; nc++){
+ let nr = Math.ceil(numTiles/nc);
+ let ar2 = nc/nr;
+ let frac = ar > ar2 ? ar2/ar : ar/ar2;
+ if (frac > bestFrac){
+ bestFrac = frac;
+ numCols = nc;
+ numRows = nr;
+ }
+ }
+ //compute other parameters
+ let tileSz = Math.min(
+ ((w - this.TILE_SPACING) / numCols) - this.TILE_SPACING,
+ ((h - this.TILE_SPACING) / numRows) - this.TILE_SPACING);
+ //determine layout
+ return Object.fromEntries(
+ nodes.map((el, idx) => [el.tolNode.name, {
+ x: x0 + (idx % numCols)*(tileSz + this.TILE_SPACING) + this.TILE_SPACING,
+ y: y0 + Math.floor(idx / numCols)*(tileSz + this.TILE_SPACING) + this.TILE_SPACING,
+ w: tileSz,
+ h: tileSz
+ }])
+ );
+ },
+ initLayoutInfo(tree){
+ return;
+ },
+ updateLayoutInfoOnExpand(nodeList){
+ return;
+ },
+ updateLayoutInfoOnCollapse(nodeList){
+ return;
+ }
+};
+const staticRectLayout = {
+ TILE_SPACING: DEFAULT_TILE_SPACING,
+ HEADER_SZ: DEFAULT_HEADER_SZ,
+ genLayout(nodes, x0, y0, w, h){
+ //get number-of-columns with highest tileCount-proportions-alignment
+ let numTiles = nodes.length;
+ let tileCounts = nodes.map(e => e.tileCount);
+ let tileCountTotal = tileCounts.reduce((x, y) => x+y);
+ let numCols, bestScore = Number.NEGATIVE_INFINITY, rowProportions, colProportions;
+ for (let nc = 1; nc <= numTiles; nc++){
+ let nr = Math.ceil(numTiles/nc);
+ //create grid representing each tile's tileCount (0 for no tile)
+ let grid = Array(nr).fill().map(e => Array(nc).fill(0));
+ for (let i = 0; i < tileCounts.length; i++){
+ grid[Math.floor(i / nc)][i % nc] = tileCounts[i];
+ }
+ //get totals across each row/column divided by tileCountTotal
+ let rowProp = grid.map(e => e.reduce((x, y) => x+y) / tileCountTotal);
+ let colProp = [...Array(nc).keys()].map(c =>
+ [...Array(nr).keys()].map(r => grid[r][c]).reduce((x,y) => x+y) / tileCountTotal);
+ //get score
+ let score = 0;
+ for (let r = 0; r < nr; r++){
+ for (let c = 0; c < nc; c++){
+ if (grid[r][c] > 0){
+ score -= Math.abs(grid[r][c] - (rowProp[r] * colProp[c]));
+ }
+ }
+ }
+ //also score for w/h occupation?
+ if (score > bestScore){
+ bestScore = score;
+ numCols = nc;
+ rowProportions = rowProp;
+ colProportions = colProp;
+ }
+ }
+ //determine layout
+ let rowNetProps = [0];
+ for (let i = 0; i < rowProportions.length-1; i++){
+ rowNetProps.push(rowNetProps[i] + rowProportions[i]);
+ }
+ let colNetProps = [0];
+ for (let i = 0; i < colProportions.length-1; i++){
+ colNetProps.push(colNetProps[i] + colProportions[i]);
+ }
+ let retVal = Object.fromEntries(
+ nodes.map((el, idx) => [el.tolNode.name, {
+ x: x0 + colNetProps[idx % numCols]*(w - this.TILE_SPACING) + this.TILE_SPACING,
+ y: y0 + rowNetProps[Math.floor(idx / numCols)]*(h - this.TILE_SPACING) + this.TILE_SPACING,
+ w: colProportions[idx % numCols]*(w - this.TILE_SPACING) - this.TILE_SPACING,
+ h: rowProportions[Math.floor(idx / numCols)]*(h - this.TILE_SPACING) - this.TILE_SPACING
+ }]));
+ return retVal;
+ },
+ initLayoutInfo(tree){ //initialise node-tree with tile-counts
+ if (tree.children.length == 0){
+ tree.tileCount = 1;
+ } else {
+ tree.children.forEach(e => this.initLayoutInfo(e));
+ tree.tileCount = tree.children.map(e => e.tileCount).reduce((x,y) => x+y);
+ }
+ },
+ updateLayoutInfoOnExpand(nodeList){ //given list of tree-nodes from expanded_child-to-parent, update tile-counts
+ nodeList[0].children.forEach(e => {e.tileCount = 1});
+ nodeList[0].tileCount = nodeList[0].children.length;
+ for (let i = 1; i < nodeList.length; i++){
+ nodeList[i].tileCount += nodeList[0].tileCount - 1;
+ }
+ },
+ updateLayoutInfoOnCollapse(nodeList){ //given list of tree-nodes from child_to_collapse-to-parent, update tile-counts
+ let tc = nodeList[0].tileCount;
+ nodeList[0].tileCount = 1;
+ for (let i = 1; i < nodeList.length; i++){
+ nodeList[i].tileCount -= tc - 1;
+ }
+ }
+};
+const sweepToSideLayout = {
+ TILE_SPACING: DEFAULT_TILE_SPACING,
+ HEADER_SZ: DEFAULT_HEADER_SZ,
+ genLayout(nodes, x0, y0, w, h){
+ //separate leaf and non-leaf nodes
+ let leaves = [], nonLeaves = [];
+ nodes.forEach(e => (e.children.length == 0 ? leaves : nonLeaves).push(e));
+ //determine layout
+ if (nonLeaves.length == 0){ //if all leaves, use squares-layout
+ return staticSqrLayout.genLayout(nodes, x0, y0, w, h);
+ } else { //if some non-leaves, use rect-layout
+ let retVal = {};
+ if (leaves.length > 0){
+ let ratio = leaves.length / (leaves.length + nonLeaves.map(e => e.tileCount).reduce((x,y) => x+y));
+ retVal = staticSqrLayout.genLayout(leaves, x0, y0, w*ratio, h);
+ x0 += w*ratio - this.TILE_SPACING;
+ w -= (w*ratio - this.TILE_SPACING);
+ }
+ //return {...retVal, ...staticSqrLayout.genLayout(nonLeaves, x0, y0, w, h)};
+ return {...retVal, ...staticRectLayout.genLayout(nonLeaves, x0, y0, w, h)};
+ }
+ },
+ initLayoutInfo(tree){
+ staticRectLayout.initLayoutInfo(tree);
+ },
+ updateLayoutInfoOnExpand(nodeList){
+ staticRectLayout.updateLayoutInfoOnExpand(nodeList);
+ },
+ updateLayoutInfoOnCollapse(nodeList){
+ staticRectLayout.updateLayoutInfoOnCollapse(nodeList);
+ }
+};
+let defaultLayout = sweepToSideLayout;
+
+function initTree(tol, lvl, layout = defaultLayout){
+ let tree = {tolNode: tol, children: []};
+ initTreeRec(tree, lvl);
+ layout.initLayoutInfo(tree)
+ return tree;
+}
+function initTreeRec(tree, lvl){
+ if (lvl > 0){
+ tree.children = tree.tolNode.children.map(e => initTreeRec({tolNode: e, children: []}, lvl-1));
+ }
+ return tree;
+}