pivot-editor/index.html
2021-05-29 16:04:38 -04:00

854 lines
25 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<div id="app"></div>
<script src="papaparse.min.js"></script>
<!-- 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;
//console.log("processing", next.Meta)
let results = inIterator(next);
if(results !== false)
{
N.Walk(inIterator, next, inKey, inForward, inTerminal);
}
else
{
//console.log("routine exited");
}
}
else
{
//console.log("id collision");
}
}
},
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(inColumnTypes, inColumnNames, 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");
}
let numeric = (N.Step(Pivot.Schema, "sum")||[]).map(column=>column.Meta.Index);
console.log(numeric);
Pivot.Leaves = inRows.map(r =>
{
numeric.forEach(index => r[index] = parseFloat(r[index])||0);
return 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:[],
Depth:depth
};
// 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);
});
// get the leaves out of the parent, at this point they have been re-distributed to the children
delete inParent.Meta.Leaves;
let iterator = () => {};
if(depth >= inPivotIndicies.length-1)
{
iterator = inLastBranch =>
{
inLastBranch.Meta.Leaves.forEach( inLeaf =>
{
let modifiers = [];
let collectModifier = n => modifiers.push(n);
let connectModifiers = n => modifiers.forEach(inModifier => N.Connect(inModifier, n, "ModifyOut", true));
// collect modifiers effecting leaves
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 to parents, but with "check unique" enabled
console.log("walking modifiers up from", inLastBranch.Meta.Label);
N.ID.Walk++;
N.Walk(connectModifiers, inLastBranch, "Hierarchy", false);
}
// lastly connect the leaf to the branch
N.Connect(inLastBranch, inLeaf, "Leaf");
});
delete inLastBranch.Meta.Leaves;
return false;
}
}
else
{
iterator = child => {
Pivot.Pivot(inRoot, child, inPivotIndicies, inSumIndicies, depth+1);
return false;
};
}
N.Walk(iterator, inParent, "Hierarchy");
return inParent;
},
Create(inLabel, inPivotIndicies, inSumIndicies)
{
N.ID.Walk++;
/*
let sumColumns = (N.Step(Pivot.Schema, "sum")||[]).map(column=>column.Meta.Index);
let labelColumns = (N.Step(Pivot.Schema, "label")||[]).map(column=>column.Meta.Index);
*/
let pivotRoot = N.Create({Label:inLabel, 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]
]
);
*/
Papa.parse("./sample.csv",
{
download:"true",
complete: function(results)
{
let columnNames = results.data.shift();
let columnTypes = ([...columnNames]).fill("hidden");
columnTypes[29] = "sum";
columnTypes[30] = "sum";
columnTypes[31] = "sum";
columnTypes[32] = "sum";
columnTypes[33] = "sum";
columnTypes[7 ] = "label";
columnTypes[8 ] = "label";
columnTypes[9 ] = "label";
columnTypes[10] = "label";
columnTypes[11] = "label";
Pivot.Init(
columnTypes,
columnNames,
results.data
);
Render();
}
});
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(pivotColumnsUsed.map(column=>column.Meta.Label).join("|"), 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 ModificationsIcon = ({node}) =>
{
let modsUp = N.Step(node, "ModifyUp", false)||[];
let modsDown = N.Step(node, "ModifyDown", false)||[];
let modsAt = N.Step(node, "ModifyAt", false)||[];
let modsOut = N.Step(node, "ModifyOut", false)||[];
var button = null;
if(modsAt.length)
{
button = html`<button onClick=${e=>{Pivot.Unmodify(modsAt[0]); Render();}}>-</button>`;
}
else
{
button = html`<button onClick=${e=>{Pivot.Modify(node); Render();}}>+</button>`;
}
let padding = 7;
let icon = 0;
let styles = css`
position:relative;
display:inline-block;
vertical-align:middle;
width:${padding*2 + icon}px;
height:${padding*2 + icon}px;
margin:${padding*2};
.Icon
{
position:absolute;
display:inline-block;
width:${padding*2 + icon}px;
height:${padding*2 + icon}px;
text-align:center;
font-size:9px;
font-family:sans-serif;
font-weight:900;
line-height:${padding*2 + icon}px;
&::after
{
content:" ";
display:block;
position:absolute;
width:${icon}px;
height:${icon}px;
border:${padding}px solid transparent;
}
&.Down
{
left:0;
bottom:100%;
&::after
{
top:100%;
border-top-color:green;
border-bottom:0px solid transparent;
}
}
&.At
{
top:0;
left:100%;
&::after
{
top:0;
right:100%;
border-right-color:red;
border-left:0px solid transparent;
}
}
&.Up
{
left:0;
top:100%;
&::after
{
bottom:100%;
border-bottom-color:orange;
border-top:0px solid transparent;
}
}
&.Out
{
top:0;
right:100%;
&::after
{
top:0;
left:100%;
border-left-color:grey;
border-right:0px solid transparent;
}
}
}
`;
return html`
<div class=${styles}>
${modsDown.length ? html`<div class="Icon Down">${modsDown.length}</div>` : null}
${modsAt.length ? html`<span class="Icon At">${modsAt.length}</span>` : null}
${modsUp.length ? html`<span class="Icon Up">${modsUp.length}</span>` : null}
${modsOut.length ? html`<span class="Icon Out">${modsOut.length}</span>` : null}
</div>
${button}
`;
};
let PivotBranch = props =>
{
let row = props.node.Meta.Row;
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>`
}
});
let displayCells = (node, visible) =>
{
let output = [];
node.Meta.Row.forEach((column, i)=>
{
if(visible[i])
{
output.push( h("td", null, column) );
}
}
);
return output;
}
let stylesLeaf = css`
background:#ddd;
color:#333;
`;
return html`
<tbody>
<tr>
<td>
<strong class=${css`margin-left:${props.node.Meta.Depth*10}px;`}>${props.node.Meta.Label}</strong>
</td>
<td>
<${ModificationsIcon} node=${props.node}><//>
</td>
${displayCells(props.node, props.visible)}
</tr>
${(N.Step(props.node, "Leaf")||[]).map(leaf =>
{
return html`
<tr class=${stylesLeaf}>
<td></td>
<td><${ModificationsIcon} node=${leaf}><//></td>
${displayCells(leaf, props.visible)}
</tr>
`;
}
)}
</tbody>
`;
};
let PivotRoot = ({pivot}) =>
{
let labels = (N.Step(Pivot.Schema, "all")||[]).map(column => column.Meta.Label);
let [visibleGet, visibleSet] = useState(labels.map(column=>true));
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, visible:visibleGet}, null)), pivot, "Hierarchy");
return html`
<div class=${stylesRoot}>
<div key="heading" class=${stylesHeading}>${pivot.Meta.Label}</div>
<${Section} key="settings" label=${`Settings`}>
<h3>pivot</h3>
<button onClick=${handleDelete}>Destroy Pivot</button>
<h3>columns</h3>
<div>
<button onClick=${e =>
{
visibleSet(visibleGet.map(v=>true))
}
}>Show All</button>
<button onClick=${e =>
{
visibleSet(visibleGet.map(v=>false))
}
}>Hide All</button>
</div>
<div>
${visibleGet.map((v, i) => html`<button onClick=${e=>
{
let clone = [...visibleGet];
clone[i] = !v;
visibleSet(clone);
}
}>${labels[i]} ${v ? `visible`:`hidden`}</button>`)}
</div>
<//>
<${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>