Last active
May 11, 2019 01:44
-
-
Save strangesast/6572414e6994576e661d37a3e7677ea5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<style> | |
html { | |
height: 100%; | |
box-sizing: border-box; | |
} | |
*, *:after, *:before { | |
box-sizing: inherit; | |
} | |
body { | |
margin: 0; | |
} | |
body svg { | |
height: 100%; | |
max-width: 100vh; | |
} | |
.links line { | |
stroke: #999; | |
stroke-opacity: 0.6; | |
} | |
.nodes circle { | |
stroke: #fff; | |
stroke-width: 1.5px; | |
} | |
</style> | |
</head> | |
<body> | |
<p>Distribute open work orders among owners. Also include unclaimed W/O's</p> | |
<svg viewBox="0 0 1000 1000"></svg> | |
<script src="https://d3js.org/d3.v5.js"></script> | |
<script src="https://d3js.org/d3-color.v1.min.js"></script> | |
<script src="https://d3js.org/d3-interpolate.v1.min.js"></script> | |
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script> | |
<script> | |
var data = (function() { | |
const partIds = Array.from(Array(20), (_, i) => `part-${('0'.repeat(3) + i).slice(-3)}`); | |
const parts = partIds.reduce((acc, id) => { | |
const name = id[0].toUpperCase() + id.slice(1).split('-').join(' '); | |
return {...acc, [id]: {id, name}}; | |
}, {}); | |
const priorityCount = 3; | |
const priorityColorScheme = d3.schemeSet1; | |
const priorityIds = Array.from(Array(3), (_, i) => i); | |
const priorities = priorityIds.reduce((acc, id, i) => ({...acc, [id]: {id, name: id, color: priorityColorScheme[i]}}), {}); | |
const workOrderIds = Array.from(Array(50)).reduce(acc => { | |
while (true) { | |
const id = `W${('0'.repeat(5) + Math.floor(Math.random() * 1e5)).slice(-5)}`; | |
if (acc.indexOf(id) < 0) { | |
return [...acc, id]; | |
} | |
} | |
}, []); | |
const workOrders = workOrderIds.reduce((acc, id) => { | |
const priority = Math.floor(Math.random() * priorityCount); | |
const completed = Math.random() > 0.8; | |
const part = partIds[Math.floor(partIds.length * Math.random())]; | |
return {...acc, [id]: {id, name: id, completed, part, priority }}; | |
}, {}); | |
const incompleteWorkOrderIds = []; | |
const completeWorkOrderIds = []; | |
for (const {id, completed} of Object.values(workOrders)) { | |
if (completed) { | |
completeWorkOrderIds.push(id); | |
} else { | |
incompleteWorkOrderIds.push(id); | |
} | |
} | |
const workerIds = Array.from(Array(10), (_, i) => ('0'.repeat(4) + Math.floor(Math.random() * 1e4)).slice(-4)); | |
const workers = workerIds.reduce((acc, id) => { | |
const workOrders = [...take(Math.floor(Math.random() * 3), completeWorkOrderIds), ...take(Math.ceil(Math.random() * 3), incompleteWorkOrderIds)]; | |
return {...acc, [id]: {id, name: `#${id}`, workOrders}}; | |
}, {'none': {'name': 'Unclaimed', workOrders: []}}); | |
return { | |
part: { | |
ids: partIds, | |
values: parts, | |
}, | |
priority: { | |
ids: priorityIds, | |
values: priorities, | |
}, | |
workOrder: { | |
ids: workOrderIds, | |
values: workOrders, | |
}, | |
completedState: { | |
ids: [false, true], | |
values: {'true': {name: 'Completed'}, 'false': {name: 'Incomplete'}}, | |
}, | |
worker: { | |
ids: ['none', ...workerIds], | |
values: workers, | |
}, | |
}; | |
})(); | |
function random(array) { | |
return array[Math.floor(Math.random() * array.length)]; | |
} | |
function take(n, array) { | |
const subset = []; | |
while (n > 0 && array.length) { | |
const [el] = array.splice(Math.floor(array.length * Math.random()), 1); | |
subset.push(el); | |
n--; | |
} | |
return subset; | |
} | |
</script> | |
<script> | |
const width = 1000; | |
const height = 1000; | |
var svg = d3.select('svg'); | |
// var color = d3.scaleOrdinal(d3.schemeCategory20); | |
// var nodes = d3.range(100).map(function() { | |
// var node = {}; | |
// node.groupA = Math.floor(Math.random()*5); | |
// node.groupB = Math.floor(Math.random()*2); | |
// return node; | |
// }) | |
// radially | |
const radius = 300; | |
function generatePts(arr) { | |
return arr.reduce((acc, id, i) => { | |
if (id === 'none') { | |
return {...acc, [id]: {x: 500, y: 500}}; | |
} | |
return { | |
...acc, | |
[id]: { | |
y: 500 + Math.sin(Math.PI * 2 * i / arr.length) * radius, | |
x: 500 + Math.cos(Math.PI * 2 * i / arr.length) * radius, | |
} | |
}; | |
}, {}); | |
} | |
// function generatePts(arr) { | |
// return arr.reduce((acc, id, i) => ({...acc, [id]: { | |
// y: height / 2, | |
// x: width / (arr.length + 1) * (1 + i), | |
// }}), {}); | |
// } | |
// priority | |
const foci = { | |
'priority': generatePts(data.priority.ids), | |
'worker': generatePts(['none', ...data.worker.ids]), | |
'completedState': generatePts([true, false]), | |
'part': generatePts(data.part.ids), | |
}; | |
var currentView = Object.keys(foci)[0]; | |
var simulation = d3.forceSimulation() | |
.force('link', d3.forceLink().id(d => d.id)) | |
.force('charge', d3.forceManyBody()); | |
// .force('center', d3.forceCenter(width / 2, height / 2)); | |
var nodes = Object.entries(data.workOrder.values).map(([id, wo]) => ({ | |
id, | |
foci: { | |
'priority': wo['priority'], | |
'worker': data.worker.ids.find(id => data.worker.values[id].workOrders.includes(wo.id)) || 'none', | |
'completedState': wo['completed'].toString(), | |
'part': wo['part'], | |
}, | |
x: 0, | |
y: 0, | |
})); | |
let node = svg.append('g') | |
.attr('class', 'nodes') | |
.selectAll('g.circle') | |
.data(nodes); | |
let nodeEnter = node.enter() | |
.append('g') | |
.classed('circle', true) | |
nodeEnter.call(d3.drag() | |
.on('start', dragstarted) | |
.on('drag', dragged) | |
.on('end', dragended)); | |
nodeEnter.append('circle') | |
.attr('r', 8) | |
.attr('fill', d => data.priority.values[data.workOrder.values[d.id].priority].color) | |
.append('title') | |
.text(d => data.workOrder.values[d.id].name); | |
node = nodeEnter.merge(node); | |
let titles = svg.append('g') | |
.classed('titles', true) | |
titles.selectAll('g.title') | |
.data(data[currentView].ids) | |
.enter().append('g').classed('title', true) | |
.attr('transform', d => `translate(${foci[currentView][d].x},${foci[currentView][d].y})`) | |
.append('text') | |
.attr('text-anchor', 'middle') | |
.text(t => data[currentView].values[t].name); | |
simulation | |
.nodes(nodes) | |
.on('tick', ticked); | |
function onChange() { | |
if (!d3.event.active) { | |
simulation.alpha(0.7); | |
simulation.restart(); | |
} | |
} | |
function changeView(nextView) { | |
if(nextView != currentView) { | |
currentView = nextView; | |
const sel = titles.selectAll('g.title').data(data[currentView].ids, v => typeof v === 'string' ? v : v.toString()); | |
sel.exit().remove(); | |
sel.enter().append('g').classed('title', true) | |
.attr('transform', d => `translate(${foci[currentView][d].x},${foci[currentView][d].y})`) | |
.append('text') | |
.attr('text-anchor', 'middle') | |
.text(t => data[currentView].values[t].name); | |
simulation.alpha(0.7); | |
simulation.restart(); | |
} | |
} | |
var buttons = svg.selectAll(null) | |
.data(Object.keys(foci)) | |
.enter() | |
.append('g') | |
.attr('transform', (_, i) => `translate(${i*120+50},${50})`) | |
.on('click', function(d) { | |
changeView(d); | |
}) | |
.style('cursor','pointer') | |
buttons.append('rect') | |
.attr('width',100) | |
.attr('height',50) | |
.attr('fill','lightgrey') | |
buttons.append('text') | |
.text(function(d) { return d; }) | |
.attr('dy', 30) | |
.attr('dx', 50) | |
.style('text-anchor','middle'); | |
function ticked() { | |
var k = this.alpha() * 0.3; | |
for (const n of nodes) { | |
const key = n['foci'][currentView]; | |
n.y += (foci[currentView][key].y - n.y) * k; | |
n.x += (foci[currentView][key].x - n.x) * k; | |
} | |
node.attr('transform', d => `translate(${d.x},${d.y})`); | |
} | |
function dragstarted(d) { | |
if (!d3.event.active) simulation.alphaTarget(0.3).restart(); | |
d.fx = d.x; | |
d.fy = d.y; | |
} | |
function dragged(d) { | |
d.fx = d3.event.x; | |
d.fy = d3.event.y; | |
} | |
function dragended(d) { | |
if (!d3.event.active) simulation.alphaTarget(0); | |
d.fx = null; | |
d.fy = null; | |
} | |
setInterval(() => { | |
const subset = take(5, data.workOrder.ids.slice()).map(id => data.workOrder.values[id]); | |
for (const each of subset) { | |
data.workOrder.values[each.id].completed = !each.completed; | |
data.workOrder.values[each.id].priority = random(data.priority.ids.filter(id => id != each.priority)); | |
} | |
node.filter(n => subset.includes(n.id)); | |
// foci: { | |
// 'priority': wo['priority'], | |
// 'worker': data.worker.ids.find(id => data.worker.values[id].workOrders.includes(wo.id)) || 'none', | |
// 'completedState': wo['completed'].toString(), | |
// 'part': wo['part'], | |
// }, | |
}, 1000); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment