<div id="app"></div> <script> var N = { ID:{ Walk:0, Instance:0 }, Create(inMeta) { return { ID:{ Walk:0, Instance:N.ID.Instance++ }, Meta:inMeta||{}, Link:{} }; }, Connect(inNodeMajor, inNodeMinor, inKey) { N.Step(inNodeMajor, inKey, true, true).push(inNodeMinor); N.Step(inNodeMinor, inKey, false, true).push(inNodeMajor); }, Disconnect(inNodeMajor, inNodeMinor, inKey) { let remove = (inArray, inMatch) => inArray.findIndex( (inMember, inIndex, inArray) => (inMember === inMatch) ? inArray.splice(inIndex, 1) : false ); if(inNodeMinor === null) { inNodeMajor.Link[inKey].Set.forEach( inLoopNode => { if(inLoopNode.Link[inKey].Get.length == 1) { // we're about to delete the last get reference on a minor node, just purge the key entirely delete inLoopNode.Link[inKey]; } else { remove( inLoopNode.Link[inKey].Get, inNodeMajor); } }); // we just wiped out all set, if get is empty we can purge the key from major if(inNodeMajor.Link[inKey].Get.length == 0) { delete inNodeMajor.Link[inKey]; } return; } if(inNodeMajor === null) { inNodeMinor.Link[inKey].Get.forEach( inLoopNode => { if(inLoopNode.Link[inKey].Set.length == 1) { delete inLoopNode.Link[inKey]; } else { remove( inLoopNode.Link[inKey].Set, inNodeMinor); } }); // we just wiped out all get, if set is empty we can purge the key from minor if(inNodeMinor.Link[inKey].Set.length == 0) { delete inNodeMinor.Link[inKey]; } return; } if(inNodeMajor.Link[inKey].Set.length == 1) { delete inNodeMajor.Link[inKey]; } else { remove(inNodeMajor.Link[inKey].Set, inNodeMinor); } if(inNodeMinor.Link[inKey].Get.length == 1) { delete inNodeMinor.Link[inKey]; } else { remove(inNodeMinor.Link[inKey].Get, inNodeMajor); } }, Step(inNode, inKey, inForward, inForceCreate) { let connectionGroup = inNode.Link[inKey]; if(!connectionGroup) { if(inForceCreate) { inNode.Link[inKey] = connectionGroup = {Get:[], Set:[]}; } else { return false; } } return (inForward === undefined || inForward === true) ? connectionGroup.Set : connectionGroup.Get; }, Walk(inIterator, inNode, inKey, inForward) { let array = N.Step(inNode, inKey, inForward); for(let i=0; i<array.length; i++) { let next = array[i]; if(next.ID.Walk !== N.ID.Walk) { next.ID.Walk = N.ID.Walk; let results = inIterator(next); if(results !== false) { N.Walk(inIterator, next, inKey, inForward); } } } }, Path(inArray, inNode, inKey, inForward) { var current = inNode; var direction = inForward||true; for(let i=0; i<inArray.length; i++) { current = N.Step(current, inKey, direction)[inArray[i]]; } return current; } }; </script> <script> ` A C \ / \ \ / \ B D ` let a = N.Create({name:"A"}); let b = N.Create({name:"B"}); let c = N.Create({name:"C"}); let d = N.Create({name:"D"}); N.Connect(a, b, "parent"); N.Connect(c, b, "parent"); N.Connect(c, d, "parent"); N.Disconnect(a, null, "parent"); /* console.log(a.Link); console.log(b.Link); console.log(c.Link); console.log(d.Link); */ </script> <script> var Pivot = { Leaves:{}, Root:{}, Init(inRows) { Pivot.Leaves = inRows.map(r => N.Create({Row:r})); Pivot.Init = ()=>{}; }, Pivot(inParent, inColumnIndicies, inSumIndicies, inDepth) { //arguments: // - a Node with leaf Nodes temporarily stored in its Meta.Leaves // - where each leaf Node has a row of table data in it's Meta.Row // - a list of columns to pivot on // - a list of columns to sum // - optional traversal depth, defaults to 0 let depth = inDepth||0; let uniques = {}; inParent.Meta.Leaves.forEach((inLeaf)=> { let row = inLeaf.Meta.Row; // shorthand for the raw "CSV" row in the leaf Node's Meta let value = row[inColumnIndicies[depth]]; // get the pivot column let match = uniques[value]; // check in the uniques list if this pivot column exists if(!match) { // if not, store a value under that key that will be the meta object for a new child match = uniques[value] = { Label:value, Row:inSumIndicies.map((inColumnIndex, inIndex, inArray) => row[inColumnIndex]), // create a Meta.Row also on the new child nodes to store summed totals Leaves:[] }; // grow a child off of the parent using the meta object N.Connect(inParent, N.Create(match), "Hierarchy"); } else { // if a match does exist, sum the appropriate columns inSumIndicies.forEach((inColumnIndex, inIndex, inArray) => match.Row[inIndex] += row[inColumnIndex]); } // move the leaves into the child match.Leaves.push(inLeaf); }); delete inParent.Meta.Leaves; var iterator; if(depth >= inColumnIndicies.length-1) { iterator = inChild => { inChild.Meta.Leaves.forEach( inLeaf => N.Connect(inChild, inLeaf, "Hierarchy") ); delete inChild.Meta.Leaves; } } else { iterator = child => Pivot.Pivot(child, inColumnIndicies, inSumIndicies, depth+1); } N.Step(inParent, "Hierarchy").forEach(iterator); return inParent; }, Create(inColumnIndicies, inSumIndicies) { return Pivot.Pivot(N.Create({Leaves:Pivot.Leaves}), inColumnIndicies, inSumIndicies); }, Modify(inNode) { N.ID.Walk++; let modifier = N.Create({}); let leaves = []; let gatherUp = n => N.Connect(modifier, n, "ModifyUp"); let gatherDown = n => { N.Connect(modifier, n, "ModifyDown"); N.Step(n, "Hierarchy").length == 0 ? leaves.push(n) : null; }; let gatherOut = n => N.Connect(modifier, n, "ModifyOut"); N.Walk(gatherUp, inNode, "Hierarchy", false); N.Connect(modifier, inNode, "ModifyDown"); N.Walk(gatherDown, inNode, "Hierarchy"); leaves.forEach(leaf=>N.Walk(gatherOut, leaf, "Hierarchy", false)); return modifier; }, Unmodify(inNode) { N.Disconnect(inNode, null, "ModifyUp"); N.Disconnect(inNode, null, "ModifyDown"); N.Disconnect(inNode, null, "ModifyOut"); } }; </script> <script type="module"> import {h, render, createContext} from 'https://cdn.skypack.dev/preact'; import {useReducer} from 'https://cdn.skypack.dev/preact/hooks'; import { css, cx } from 'https://cdn.skypack.dev/@emotion/css'; Pivot.Init([ ["#1", "a", "long", 1], ["#2", "b", "long", 2], ["#3", "b", "short", 2], ["#4", "a", "long", 3], ["#5", "b", "short", 1], ["#6", "a", "short", 0], ["#7", "b", "short", 7], ]); let pivotRoot1 = Pivot.Create([1, 2], [3]); let pivotRoot2 = Pivot.Create([2, 1], [3]); let pivots = [pivotRoot1, pivotRoot2]; let select = N.Path([0, 1], pivotRoot1, "Hierarchy"); let mod = Pivot.Modify(select); let Store = createContext(null); let StoreState = { count:7 }; let StoreReducer = (inState, inAction) => { }; let ElNode = ({node}) => { var nodeChildren = N.Step(node, "Hierarchy"); var children = []; var table = []; let styles = { }; if(node.Link["ModifyOut"]) { styles.backgroundColor = "lightgrey"; } if(node.Link["ModifyDown"]) { styles.backgroundColor = "yellow"; } if(node.Link["ModifyUp"]) { styles.backgroundColor = "lavender"; } if(node.Meta.Row) { table = node.Meta.Row.map( cell => h("span", {style:{padding:"10px"}}, cell)); } children = [ h("strong", {style:styles}, node.Meta.Label||"a node"), ...table, h(Store.Consumer, null, value=> { return value.get.count; }) ]; if(nodeChildren.length) { children = [ ...children, ...nodeChildren.map( inChild => h(ElNode, {node:inChild})) ]; } return h("div", {style:{padding:"10px"}}, children); } let ElPivot = ({pivot}) => { return h("div", {style:{display:"inline-block"}}, [ h(ElNode, {node:pivot}) ]); } let ElRoot = ({pivots}) => { let [get, set] = useReducer(StoreReducer, StoreState); return h(Store.Provider, {value:{get, set}}, [ h("div", null, [ h("h3", null, "tree view"), ...pivots.map(pivot=>h(ElPivot, {pivot})) ]) ]); }; render(h(ElRoot, {pivots}, null), document.querySelector("#app")); </script>