<div id="app"></div> <!-- N --> <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) !== -1) { 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> <!-- Pivot --> <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++) { let columnNode = N.Create({Label:inColumnNames[i], Index:i}); N.Connect(Pivot.Schema, columnNode, inColumnTypes[i]); N.Connect(Pivot.Schema, columnNode, "all"); } 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 = {}; let indexPivot = inPivotIndicies[depth] 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[indexPivot]; // 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, IndexPivot:indexPivot, IndexSum:inSumIndicies, 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 => { inLastBranch.Meta.Leaves.forEach( inLeaf => { // collect modifiers effecting leaves let modifiers = []; let collectModifier = n => modifiers.push(n); let connectModifiers = n => modifiers.forEach(inModifier => N.Connect(inModifier, n, "ModifyOut", true)); N.Walk(collectModifier, inLeaf, "ModifyAt", false); N.Walk(collectModifier, inLeaf, "ModifyDown", false); if(modifiers.length) { // apply them to the branch inLastBranch.ID.Walk = N.ID.Walk; connectModifiers(inLastBranch); // also walk them up and connect, but with "check unique" enabled N.Walk(connectModifiers, inLastBranch, "Hierarchy", false); } // lastly connect the leaf to the branch N.Connect(inLastBranch, inLeaf, "Leaf"); }); delete inLastBranch.Meta.Leaves; } } else { iterator = child => Pivot.Pivot(inRoot, child, inPivotIndicies, inSumIndicies, depth+1); } N.Walk(iterator, inParent, "Hierarchy"); return inParent; }, Create(inPivotIndicies, inSumIndicies) { N.ID.Walk++; let columns = N.Step(Pivot.Schema, "all"); let label = inPivotIndicies.map( inPivotIndex => { return columns[inPivotIndex].Meta.Label; }); let pivotRoot = N.Create({Label:label.join("|"), 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, "ModifyUp", false); if(check) { while(check.length>0) { Pivot.Unmodify(check[0]); } } // disconnect leaves from terminal branches N.Walk(()=>{}, inRoot, "Hierarchy", true, terminal=>{ N.Disconnect(terminal, null, "Leaf"); }); // disconnect from app N.Disconnect(null, inRoot, "Pivot"); }, Modify(inNode) { let modified = N.Create({Label:"Modifier"}); // traverse let gatherUp = n => N.Connect(modified, n, "ModifyUp"); let gatherDown = n => N.Connect(modified, n, "ModifyDown"); let gatherOut = n => N.Connect(modified, n, "ModifyOut"); N.ID.Walk++; inNode.ID.Walk = N.ID.Walk; // at N.Connect(modified, inNode, "ModifyAt"); // up N.Walk(gatherUp, inNode, "Hierarchy", false); // down 1 N.Walk(gatherDown, inNode, "Hierarchy", true, terminal=> { // down 2 // for each terminal node, step down into its leaves and gather down N.Walk(gatherDown, terminal, "Leaf", true, leaf=> { // out 1 // walk back up on the leaf connections on other trees N.Walk(gatherOut, leaf, "Leaf", false, terminal=> { // out 2 // and continueup the hierarchy N.Walk(gatherOut, terminal, "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> <!-- rendering --> <script type="module"> Pivot.Init( ["id", "type-a", "type-b", "count", "extra"], ["label", "label", "label", "sum", "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] ] ); import { h, render, createContext, Fragment } from 'https://cdn.skypack.dev/preact'; import { useReducer, useState } from 'https://cdn.skypack.dev/preact/hooks'; import { css, cx } from 'https://cdn.skypack.dev/@emotion/css'; import htm from 'https://unpkg.com/htm?module'; const html = htm.bind(h); let PivotForm = props => { let styles = css` position:realtive; box-sizing: border-box; padding: 10px; color:black; font-family:sans-serif; .Title { font-size:24px; font-weight:100; } .Section { padding:10px 0 10px 0; .Heading { display:inline-block; color:#666; font-family:sans-serif; font-size:12px; font-weight:900; text-transform:uppercase; } .Group { display:inline-block; padding:5px; border-radius:5px; margin:3px; background:rgba(0, 0, 0, 0.3) } } `; let pivotColumns = N.Step(Pivot.Schema, "label")||[]; let pivotColumnsUsed = N.Step(Pivot.Proto, "used-pivot")||[]; let sumColumns = N.Step(Pivot.Schema, "sum")||[]; //let sumColumnsUsed = N.Step(Pivot.Proto, "used-sum")||[]; let indiciesPivot = pivotColumnsUsed.map(node=>node.Meta.Index); let indiciesSum = sumColumns.map(node=>node.Meta.Index); //let indiciesSum = sumColumnsUsed.map(node=>node.Meta.Index); let displayPivotsAll = html` <div class="Section"> <div class="Heading">Available Columns</div> <div class="Group"> ${pivotColumns.map( columnPivot => { let attributes = {}; if(N.Step(columnPivot, "used-pivot", false)) { attributes.disabled = true; } else { attributes.onClick = e=> { N.Connect(Pivot.Proto, columnPivot, "used-pivot"); Render(); } } return html`<button ...${attributes}>${columnPivot.Meta.Label}</button>`; })} </div> </div> `; let displayPivotsPending = null; if(pivotColumnsUsed.length) { displayPivotsPending = html` <div class="Section"> <div class="Heading">Pending Pivot</div> <div class="Group"> ${pivotColumnsUsed.map(columnPivot=>html` <button onClick=${e=>{N.Disconnect(Pivot.Proto, columnPivot, "used-pivot");Render();}}> ${columnPivot.Meta.Label} </button> `)} </div> <button onClick=${e=>{ N.Disconnect(Pivot.Proto, null, "used-pivot"); N.Disconnect(Pivot.Proto, null, "used-sum"); Pivot.Create(indiciesPivot, indiciesSum); Render(); }}>Create</button> </div> `; } return html` <div class=${styles}> <div class="Title">Create New Pivot</div> ${displayPivotsAll} ${displayPivotsPending} </div> `; } let Section = props => { let styles = css` .Heading { padding:6px 0 6px 0; color:#666; font-weight:900; font-size:12px; text-transform:uppercase; cursor:pointer; span { display:inline-block; width:20px; height:20px; margin-right:10px; border-radius:20px; background:black; color:white; text-align:center; } } .Heading:hover { color:black; } .Body { position:relative; padding:10px 0 20px 30px; &::before { content: " "; display:block; position:absolute; top:-8px; left:8px; width:3px; height:100%; background:black; } } `; let [openGet, openSet] = useState(false); return html` <div class=${styles}> <div class="Heading" onClick=${e=>openSet(!openGet)}> <span>${openGet ? `−` : `+`}</span> ${props.label} </div> ${ openGet ? html`<div class="Body">${props.children}</div>` : null } </div> `; } let Modifier = props => { let refNode = N.Step(props.modifier, "ModifyAt")[0]; let handleDelete = () => { Pivot.Unmodify(props.modifier); Render(); }; let displayFields = []; N.Walk(node=>{ displayFields.push(html`<input type='number'/>`) }, Pivot.Schema, "sum"); return html` <div> <div>${refNode.Meta.Label}</div> <button onClick=${handleDelete}>Delete</button> </div> `; } let PivotBranch = props => { let row = props.node.Meta.Row; let displayCells = row.map(column=>h("td", null, column)); let displayCellsModify = row.map(column=>false); props.node.Meta.IndexSum.forEach(i=> { displayCellsModify[i] = html`<td><input type="number" value=${row[i]}/></td>`; }); displayCellsModify.forEach((cell, i)=> { if(!cell) { displayCellsModify[i] = html`<td>${row[i]}</td>` } }); return html` <tbody> <tr> <td colspan=${displayCells.length+1}><hr/></td> </tr> <tr> <td> <strong>${props.node.Meta.Label}</strong> </td> ${displayCells} </tr> <tr> <td colspan=${displayCells.length+1}>Modifications</td> </tr> <tr> <td>At</td> ${displayCellsModify} </tr> <tr> <td>Below</td> ${displayCells} </tr> <tr> <td>Above</td> ${displayCells} </tr> <tr> <td>Outside</td> ${displayCells} </tr> <tr> <td>Computed</td> ${displayCells} </tr> <tr> <td colspan=${displayCells.length+1}>Goals</td> </tr> <tr> <td>Max</td> ${displayCells} </tr> <tr> <td>Min</td> ${displayCells} </tr> </tbody> `; }; let PivotRoot = ({pivot}) => { let stylesRoot = css` display:block; box-sizing:border-box; padding:15px; font-family:sans-serif; `; let stylesHeading = css` margin: 0 0 15px 0; font-weight:0; font-size:24px; `; let modifiers = N.Step(pivot, "ModifyUp", false) || []; let handleDelete = ()=> { Pivot.Delete(pivot); Render(); }; let rows = []; N.ID.Walk++; N.Walk(n=>rows.push(h(PivotBranch, {node:n}, null)), pivot, "Hierarchy"); return html` <div class=${stylesRoot}> <div key="heading" class=${stylesHeading}>${pivot.Meta.Label}</div> <${Section} key="settings" label=${`Settings`}> <button onClick=${handleDelete}>Destroy Pivot</button> <//> <${Section} key="modifiers" label=${`Modifiers (${modifiers.length})`}> ${ !modifiers.length ? html`<em>No modifiers</em>` : modifiers.map( m => html`<${Modifier} modifier=${m}/>`)} <//> <${Section} key="tree" label=${"Tree"}> <table> ${rows} </table> <//> </div> `; }; let ElRoot = props => { let pivots = N.Step(Pivot.Root, "Pivot")||[]; return h("div", null, [ h(PivotForm), pivots.map(pivot=>h(PivotRoot, {key:pivot.Meta.Label, pivot})) ]) }; const Render = () => render(h(ElRoot), document.querySelector("#app")); Render(); </script> <!-- testing area --> <script> let ModificationPull = (inDAG) => { let collect = inArray => n => { n.Meta.Tweak.forEach((t, i)=>{ inArray[n.Meta.Indicies[i]] += t; }); }; let totalScale = [0, 0, 0, 0, 0, 0, 0, 0]; let totalAdd = [0, 0, 0, 0, 0, 0, 0, 0]; N.ID.Walk++; N.Walk(collect(totalScale), inDAG, "ModifyAt", false); N.Walk(collect(totalScale), inDAG, "ModifyDown", false); N.Walk(collect(totalAdd), inDAG, "ModifyUp", false); N.Walk(collect(totalAdd), inDAG, "ModifyOut", false); return [totalScale, totalAdd]; }; let dag1 = N.Create({Label:"dag-1"}); let m0 = N.Create({Label:"m-0", Tweak:[-1, 2, 0.7], Indicies:[0, 1, 2]}); let m1 = N.Create({Label:"m-1", Tweak:[0.1, 0.1, 0.1], Indicies:[1, 2, 3]}); N.Connect(m0, dag1, "ModifyAt"); N.Connect(m1, dag1, "ModifyDown"); let results = ModificationPull(dag1); console.log(results); </script>