<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, inUnique) { if(inUnique) // bail if the nodes are already connected { let check = N.Step(inNodeMajor, inKey, true); if(check) { if(check.indexOf(inNodeMinor) < 0) { return; } } } 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 no specific child was passed if(inNodeMinor === null) { // get all the children let check = N.Step(inNodeMajor, inKey); if(!check){ return; } // go down to each child ... check.forEach( inNodeMinor => { let connections = inNodeMinor.Link[inKey]; remove( connections.Get, inNodeMajor); // ... and remove any reference to the parent // if after the remove operation, this child has no connections on inKey, scrub the key if(!connections.Set.length && !connections.Get.length) { delete inNodeMinor.Link[inKey]; } }); // we just wiped out all outgoing connections to the parent, if incoming connections are empty too we can purge the key there as well if(inNodeMajor.Link[inKey].Get.length == 0) { delete inNodeMajor.Link[inKey]; } return; } // if no specific parent was passed if(inNodeMajor === null) { // get all the parents let check = N.Step(inNodeMinor, inKey, false); if(!check){ return; } // go up to each parent ... check.forEach( inNodeMajor => { let connections = inNodeMajor.Link[inKey]; remove( connections.Set, inNodeMinor); // ... and remove any reference to the child // if after the remove operation, this parent has no connections on inKey, scrub the key if( !connections.Set.length && !connections.Get.length ) { delete inNodeMajor.Link[inKey]; } }); // we just wiped out all incoming connections to the child, if outgoing connections are empty too we can purge the key there as well if(inNodeMinor.Link[inKey].Set.length == 0) { delete inNodeMinor.Link[inKey]; } return; } // if a specific parent and child were passed 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 === true) { inNode.Link[inKey] = connectionGroup = {Get:[], Set:[]}; } else { return false; } } return (inForward === undefined || inForward === true) ? connectionGroup.Set : connectionGroup.Get; }, Walk(inIterator, inNode, inKey, inForward, inTerminal) { let array = N.Step(inNode, inKey, inForward); if(!array.length && inTerminal) { return inTerminal(inNode); } 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, inTerminal); } } } }, 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> var Pivot = { Leaves:{}, Root:N.Create({Label:"All Pivots"}), Schema:N.Create({Label:"Column Details"}), Proto:N.Create({Label:"User Form"}), Init(inColumnNames, inColumnTypes, inRows) { for(let i=0; i<inColumnNames.length; i++) { N.Connect(Pivot.Schema, N.Create({Label:inColumnNames[i], Index:i}), inColumnTypes[i]); } Pivot.Leaves = inRows.map(r => N.Create({Row:r})); Pivot.Init = ()=>{}; }, Pivot(inRoot, inParent, inPivotIndicies, 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[inPivotIndicies[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 let clone = row.map(r=>null); inSumIndicies.forEach((inSumIndex, inIndex, inArray)=> { clone[inSumIndex] = row[inSumIndex] }); match = uniques[value] = { Label:value, Row:clone, 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((inSumIndex) => match.Row[inSumIndex] += row[inSumIndex]); } // move the leaves into the child match.Leaves.push(inLeaf); }); delete inParent.Meta.Leaves; var iterator; if(depth >= inPivotIndicies.length-1) { iterator = inLastBranch => { N.Connect(inRoot, inLastBranch, "Terminal"); inLastBranch.Meta.Leaves.forEach( inLeaf => { // collect modifiers effecting leaves let modifiers = []; let check = N.Step(inLeaf, "ModifyAt", false); if(check) { modifiers = modifiers.concat(check); } check = N.Step(inLeaf, "ModifyDown", false); if(check) { modifiers = modifiers.concat(check); } if(modifiers.length) { // apply them to the branch inLastBranch.ID.Walk = N.ID.Walk; modifiers.forEach( inModifier => N.Connect(inModifier, inLastBranch, "ModifyUp") ) // also walk them up and connect, but with "check unique" enabled N.Walk( inNode=> { modifiers.forEach( inModifier => N.Connect(inModifier, inNode, "ModifyUp", true) ) } , inLastBranch, "Hierarchy", false); } // lastly connect the leaf to the branch N.Connect(inLastBranch, inLeaf, "Hierarchy"); }); delete inLastBranch.Meta.Leaves; } } else { iterator = child => Pivot.Pivot(inRoot, child, inPivotIndicies, inSumIndicies, depth+1); } N.Step(inParent, "Hierarchy").forEach(iterator); return inParent; }, Create(inPivotIndicies, inSumIndicies) { N.ID.Walk++; let pivotRoot = N.Create({Label:"Pivot Root", Leaves:Pivot.Leaves}); N.Connect(Pivot.Root, pivotRoot, "Pivot"); return Pivot.Pivot(pivotRoot, pivotRoot, inPivotIndicies, inSumIndicies); }, Delete(inRoot) { // disconnect modifiers let check = N.Step(inRoot, "Modifier"); if(check) { while(check.length>0) { Pivot.Unmodify(check[0]); } } // disconnect terminal branches N.Walk(()=>{}, inRoot, "Terminal", inNode=>{ N.Disconnect(inNode, null, "Hierarchy"); }); // disconnect from app N.Disconnect(null, inRoot, "Pivot") }, Modify(inNode) { let modified = N.Create({Label:"Modifier"}); N.ID.Walk++; if(N.Step(inNode, "Hierarchy").length) { N.Walk(()=>{}, inNode, "Hierarchy", false, (inRoot)=> { N.Connect(inRoot, modified, "Modifier"); }); } else { N.Connect(Pivot.Root, modified, "Modifier"); } let leaves = []; let gatherUp = n => N.Connect(modified, n, "ModifyUp"); let gatherDown = n => { N.Connect(modified, n, "ModifyDown"); N.Step(n, "Hierarchy").length == 0 ? leaves.push(n) : null; }; let gatherOut = n => N.Connect(modified, n, "ModifyOut"); N.ID.Walk++; inNode.ID.Walk = N.ID.Walk; N.Connect(modified, inNode, "ModifyAt"); N.Walk(gatherUp, inNode, "Hierarchy", false); N.Walk(gatherDown, inNode, "Hierarchy"); leaves.forEach(leaf=>N.Walk(gatherOut, leaf, "Hierarchy", false)); return modified; }, Unmodify(inModifier) { N.Disconnect(inModifier, null, "ModifyUp"); N.Disconnect(inModifier, null, "ModifyDown"); N.Disconnect(inModifier, null, "ModifyOut"); N.Disconnect(inModifier, null, "ModifyAt"); N.Disconnect(null, inModifier, "Modifier"); } }; </script> <script type="module"> import { h, render, createContext, Fragment } 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( ["number", "type-a", "type-b", "count"], ["label", "label", "label", "sum"], [ ["#1", "a", "long", 1, 4], ["#2", "b", "long", 2, 4], ["#3", "b", "short", 2, 4], ["#4", "a", "long", 3, 4], ["#5", "b", "short", 1, 4], ["#6", "a", "short", 0, 4], ["#7", "b", "short", 7, 4] ] ); let ElForm = props => { let labelColumns = N.Step(Pivot.Schema, "label")||[]; let used = N.Step(Pivot.Proto, "used")||[]; let unused = N.Step(Pivot.Schema, "label").filter( columnLabel => !N.Step(columnLabel, "used", false) ); let indicies = used.map(node=>node.Meta.Index); var action; if(indicies.length) { action = h("div", null, [ h("span", null, "Build"), h("button", {onClick:e=> { N.Disconnect(Pivot.Proto, null, "used"); Pivot.Create(indicies, [3, 4]); Render(); } }, indicies) ]); } else { action = h("div", null, "(select columns)") } return h("div", {}, [ h("div", null, [ h("span", null, "available"), ...unused.map( node=> { return h("button", {onClick:e=> { N.Connect(Pivot.Proto, node, "used"); Render(); } }, node.Meta.Label); } ) ]), h("div", null, [ h("span", null, "taken"), ...used.map( node=> { return h("button", {onClick:e=> { N.Disconnect(Pivot.Proto, node, "used"); Render(); } }, node.Meta.Label); } ) ]), action ]) }; let ElNode = ({node, depth}) => { let nodeBase = css` position:relative; padding:0; margin:0; border-top:1px solid lightgrey; .Table { display:inline-block; text-align:right; } .Cell { width:50px; display:inline-block; padding:10px; } `; let label = css` display:inline-block; width:200px; &::before { content:" "; display:inline-block; width:${depth*15}px; } .Modify { float:right; } `; let icon = cx( { [css` display:inline-block; width:0px; height:0px; border:7px solid white;` ]: true, [css`border-bottom-color:lightblue;`]: N.Step(node, "ModifyUp" ), [css`border-top-color:orange;` ]: N.Step(node, "ModifyDown"), [css`border-left-color:grey;` ]: N.Step(node, "ModifyOut" ), [css`border-right-color:red;` ]: N.Step(node, "ModifyAt" ) }, ); let buttonModify = h("span", { className:"Icon Modify Add", onClick:e=> { Pivot.Modify(node); Render(); } }, "Modify"); let buttonUnmodify = h("span", { className:"Icon Modify remove", onClick:e=> { Pivot.Unmodify(N.Step(node, "ModifyAt", false)[0]); Render(); } }, "Unmodify"); return h("div", {className:"Node"}, [ h("div", {className:cx(nodeBase, "Upper")}, [ h("div", {className:label}, [ h("span", {className:"Icon Expand"}, "+"), h("span", {className:"Name"}, (node.Meta.Label || "a node")+" "+node.ID.Walk), h("span", {className: icon}), N.Step(node, "ModifyAt") ? buttonUnmodify : buttonModify ] ), h("div", {className:"Table"}, (node.Meta.Row || []).map( cell => h("div", {className:"Cell"}, cell)) ) ]), h("div", {className:"Nodes"}, N.Step(node, "Hierarchy").map(child=>h(ElNode, {node:child, depth:depth+1})) ) ]); }; let ElPivot = ({pivot}) => { return h("div", {style:{display:"inline-block", width:"800px"}}, [ h("button", {onClick:e=>{Pivot.Delete(pivot);Render();}}, "delete?"), h(ElModifiers, {node:pivot}), h(ElNode, {node:pivot, depth:0}) ]); }; let ElModifiers = ({node}) => { let modifiers = N.Step(node, "Modifier") || []; return h("div", null, [ h("strong", null, "modifiers"), ...modifiers.map( m => h("span", {onClick:e=> { Pivot.Unmodify(m); Render(); } }, "modifier")) ]); }; let ElRoot = props => { let pivots = N.Step(Pivot.Root, "Pivot")||[]; return h("div", null, [ h("h3", null, "tree view"), h(ElForm), h(ElModifiers, {node:Pivot.Root}), pivots.map(pivot=>h(ElPivot, {pivot})) ]) }; let Render = () => render(h(ElRoot), document.querySelector("#app")); Render(); </script>