resources/todomvc/big-dom-generator/src/tree-generator.js (85 lines of code) (raw):

import { LCG } from "random-seedable"; import { DEFAULT_SEED_FOR_RANDOM_NUMBER_GENERATOR, MAX_GENERATED_DOM_DEPTH, MAX_NUMBER_OF_CHILDREN, PROBABILITY_OF_HAVING_CHILDREN, TARGET_SIZE, MIN_NUMBER_OF_MAX_DEPTH_BRANCHES, PERCENTAGE_OF_DISPLAY_NONE_TREEVIEW_ELEMENTS } from "./../params"; const random = new LCG(DEFAULT_SEED_FOR_RANDOM_NUMBER_GENERATOR); // Recursively depth-first computing subTreeWeight. const fillSubtreeWeights = (node, expandableItemWeight, nonExpandableItemWeight) => { if (node.type === "expandableItem") node.subTreeWeight = node.children.reduce((acc, child) => acc + fillSubtreeWeights(child, expandableItemWeight, nonExpandableItemWeight), expandableItemWeight); else node.subTreeWeight = nonExpandableItemWeight; return node.subTreeWeight; }; /* * Iterate over the exapandableItem nodes in a breadth-first manner until the * sum of weights of the subtrees with root nodes in the queue is less than * the target number of elements we want to have display none. Mark the * nodes in the queue as display none. * Consider the following example with the weights as displayed in the figure * and 10 as the target number of display none elements. The iteration will * stop with the nodes with weights 7 and 2 marked with *. * 20 * / \ * 12 8 * / \ / \ * 5 *7* *2* 6 * / / \ * 7 1 1 */ const markDisplayNoneNodes = (node, expandableItemWeight, nonExpandableItemWeight) => { let currentSubTreesWeights = node.subTreeWeight; let currentIndex = 0; let nodeQueue = [node]; while (currentSubTreesWeights >= TARGET_SIZE * PERCENTAGE_OF_DISPLAY_NONE_TREEVIEW_ELEMENTS) { const currentNode = nodeQueue[currentIndex]; nodeQueue[currentIndex] = null; const expandableChildren = currentNode.children.filter((child) => child.type === "expandableItem"); if (expandableChildren.length) { nodeQueue.push(...expandableChildren); currentSubTreesWeights -= expandableItemWeight; let numberOfNonExpandableChildren = currentNode.children.length - expandableChildren.length; currentSubTreesWeights -= numberOfNonExpandableChildren * nonExpandableItemWeight; } else { currentSubTreesWeights -= currentNode.subTreeWeight; } currentIndex++; } nodeQueue.forEach((node) => { if (node) node.isDisplayNone = true; }); }; /** * Generates the blueprint of the tree-view side panel for the complex DOM shell with expandable and non-expandable items. * It starts with the minimum number of maximum-depth branches and randomly adds * children to the nodes in a breadth-first manner. * The weight parameters represent how many DOM nodes are generated for each type of node. * @param {Object} options - The options object. * @param {number} options.expandableItemWeight - The weight for the "expandableItem" node type. <li></li> with ChevronRight svg. * @param {number} options.nonExpandableItemWeight - The weight for the "nonExpandableItem" node type. <li></li> TaskListIcon svg. * @returns {Object} The generated tree structure with two possible values for the "type" property: "expandableItem" or "nonExpandableItem" * Example structure: * { * type: "expandableItem", * children: [ * { * type: "expandableItem", * children: [ * { * type: "nonExpandableItem", * children: [], * isDisplayNone: false, * subTreeWeight: 0, * } * ], * isDisplayNone: false, * subTreeWeight: 0, * } * ], * isDisplayNone: false, * subTreeWeight: 0, * } **/ export const generateTreeHead = ({ expandableItemWeight, nonExpandableItemWeight }) => { const treeHead = { type: "expandableItem", children: [], isDisplayNone: false, subTreeWeight: 0 }; const nodeWeight = { expandableItem: expandableItemWeight, nonExpandableItem: nonExpandableItemWeight }; let totalNodes = expandableItemWeight; for (let i = 0; i < MIN_NUMBER_OF_MAX_DEPTH_BRANCHES; i++) { let currentDepth = 0; let currentNode = treeHead; while (currentDepth < MAX_GENERATED_DOM_DEPTH) { let childType = "expandableItem"; currentNode.children.push({ type: childType, children: [] }); currentNode = currentNode.children[currentNode.children.length - 1]; currentDepth++; totalNodes += nodeWeight[childType]; } } const treeNodes = [treeHead]; // Do an outer loop because there is a chance that the inner loop ends without // reaching the target size. while (totalNodes < TARGET_SIZE) { let index = 0; // All items start as closed and are marked open if the algorithm adds children. while (index < treeNodes.length && totalNodes < TARGET_SIZE) { let currentNode = treeNodes[index]; switch (currentNode.type) { case "expandableItem": if (random.coin(PROBABILITY_OF_HAVING_CHILDREN) || currentNode.children.length) { const numberOfNewChildren = random.randRange(1, MAX_NUMBER_OF_CHILDREN - currentNode.children.length + 1); for (let i = 0; i < numberOfNewChildren && totalNodes < TARGET_SIZE; i++) { currentNode.children.push({ type: "nonExpandableItem", children: [] }); totalNodes += nodeWeight["nonExpandableItem"]; } random.shuffle(currentNode.children, true); treeNodes.push(...currentNode.children); } break; case "nonExpandableItem": if (random.coin(PROBABILITY_OF_HAVING_CHILDREN)) { currentNode.type = "expandableItem"; // randomly choose the child type between expandableItem and nonExpandableItem let childType = random.choice(["expandableItem", "nonExpandableItem"]); currentNode.children.push({ type: childType, children: [] }); // We changed the node type so we need to update the totalNodes count. totalNodes = totalNodes - nodeWeight["nonExpandableItem"] + nodeWeight["expandableItem"]; // We added a child so we need to update the totalNodes count. totalNodes += nodeWeight[childType]; treeNodes.push(currentNode.children[0]); } break; default: throw new Error(`Unknown node type: ${currentNode.type}`); } index++; } } fillSubtreeWeights(treeHead, expandableItemWeight, nonExpandableItemWeight); markDisplayNoneNodes(treeHead, expandableItemWeight, nonExpandableItemWeight); return treeHead; };