184 lines
6.2 KiB
JavaScript
184 lines
6.2 KiB
JavaScript
/**
|
|
* _ ___ Another
|
|
* / |/ (_)______ __ _____
|
|
* / / / __(_-</ // (_-<
|
|
* /_/|_/_/\__/___/\_, /___/
|
|
* /___/
|
|
* production !
|
|
*
|
|
* Licensed under the MIT License:
|
|
* This code is free to use and modify,
|
|
* as long as the copyright notice and license are kept.
|
|
*/
|
|
class BZgantt extends Buildoz {
|
|
|
|
constructor(){
|
|
super()
|
|
this.defaultAttrs = { }
|
|
}
|
|
|
|
async connectedCallback(){
|
|
await customElements.whenDefined('bz-graflow')
|
|
super.connectedCallback()
|
|
|
|
this.ul = document.createElement('ul')
|
|
this.ul.classList.add('tasks')
|
|
this.append(this.ul)
|
|
this.graflow = document.createElement('bz-graflow')
|
|
this._syncAttributesToGraflow()
|
|
this.append(this.graflow)
|
|
|
|
this.fireEvent('domConnected', { gantt: this })
|
|
this.dayWidth = this.getBZAttribute('daywidth') || 20
|
|
if(this.getBZAttribute('nodesfile')) this.nodesfile = this.getBZAttribute('nodesfile')
|
|
else this.initGantt()
|
|
const dataSrc = this.getBZAttribute('src')
|
|
if(dataSrc) this.loadData(dataSrc)
|
|
else this.gantt = { "tasks": [] }
|
|
|
|
}
|
|
|
|
_syncAttributesToGraflow(){
|
|
for(const attr of this.attributes){
|
|
if(attr.name === 'flow') continue
|
|
this.graflow.setAttribute(attr.name, attr.value)
|
|
}
|
|
}
|
|
|
|
async loadData(dataSrc){
|
|
let buf, ganttObj
|
|
if(dataSrc instanceof Blob){
|
|
buf = await dataSrc.text()
|
|
try{
|
|
ganttObj = JSON.parse(buf)
|
|
} catch(err){
|
|
this.error('Could not parse gantt data JSON!?', err)
|
|
return
|
|
}
|
|
} else if(typeof dataSrc == 'string') {
|
|
const url = dataSrc
|
|
const fetchUrl = (typeof url === 'string' && !url.startsWith('blob:') && !url.startsWith('data:'))
|
|
? (url + '?' + crypto.randomUUID())
|
|
: url
|
|
const res = await fetch(fetchUrl)
|
|
buf = await res.text()
|
|
try{
|
|
ganttObj = JSON.parse(buf)
|
|
} catch(err){
|
|
this.error('Could not parse gantt data JSON!?', err)
|
|
return
|
|
}
|
|
} else if(typeof dataSrc == 'object') {
|
|
ganttObj = structuredClone(dataSrc)
|
|
}
|
|
|
|
if(!ganttObj.nodesFile){
|
|
if(this.getBZAttribute('nodesfile')) ganttObj.nodesFile = this.getBZAttribute('nodesfile')
|
|
else {
|
|
this.error('No nodesFile in JSON!?')
|
|
return
|
|
}
|
|
}
|
|
this.tasks = ganttObj.tasks
|
|
this.reorder_tasks()
|
|
await this.graflow.loadNodes(ganttObj.nodesFile)
|
|
this.refresh()
|
|
this.fireEvent('ganttDataLoaded', {
|
|
component: this,
|
|
})
|
|
}
|
|
|
|
reorder_tasks(){
|
|
const byId = new Map(this.tasks.map(t => [t.id, t]))
|
|
const children = new Map(this.tasks.map(t => [t.id, []]))
|
|
for(const task of this.tasks){
|
|
for(const dep of task.dependsOn ?? []){
|
|
if(children.has(dep)) children.get(dep).push(task)
|
|
}
|
|
}
|
|
|
|
// Subtree weight: number of (unique) descendants of a task
|
|
const descendants = new Map()
|
|
const countDescendants = (task, seen) => {
|
|
for(const child of children.get(task.id)){
|
|
if(seen.has(child.id)) continue
|
|
seen.add(child.id)
|
|
countDescendants(child, seen)
|
|
}
|
|
return(seen.size)
|
|
}
|
|
for(const task of this.tasks) descendants.set(task.id, countDescendants(task, new Set()))
|
|
const byWeight = (a, b) => descendants.get(a.id) - descendants.get(b.id)
|
|
|
|
// Depth-first walk, a task is placed once all its dependencies are placed
|
|
const ordered = []
|
|
const placed = new Set()
|
|
const ready = task => (task.dependsOn ?? []).every(dep => placed.has(dep) || !byId.has(dep))
|
|
const visit = task => {
|
|
if(placed.has(task.id) || !ready(task)) return
|
|
placed.add(task.id)
|
|
ordered.push(task)
|
|
for(const child of [...children.get(task.id)].sort(byWeight)) visit(child)
|
|
}
|
|
const roots = this.tasks.filter(t => !(t.dependsOn?.length)).sort(byWeight)
|
|
for(const root of roots) visit(root)
|
|
|
|
// Leftovers (circular dependencies) keep their original order
|
|
for(const task of this.tasks) if(!placed.has(task.id)) ordered.push(task)
|
|
this.tasks = ordered
|
|
}
|
|
|
|
getDayIndex(firstDate, date){
|
|
return Math.ceil((date-firstDate)/86400000)
|
|
}
|
|
|
|
buildFlow(){
|
|
const firstDate = new Date(this.tasks[0].start)
|
|
for(const task of this.tasks){
|
|
const startDate = new Date(task.start)
|
|
if(startDate < firstDate) firstDate = startDate
|
|
}
|
|
|
|
const flow = { "nodes": [], "links": [] }
|
|
let curY = 0
|
|
for(const task of this.tasks){
|
|
const startIdx = this.getDayIndex(firstDate, new Date(task.start))
|
|
const endIdx = this.getDayIndex(firstDate, new Date(task.end))
|
|
flow.nodes.push({
|
|
"nodeType": "bar",
|
|
"id": task.id,
|
|
"coords": {
|
|
"x": startIdx * this.dayWidth,
|
|
"y": curY,
|
|
},
|
|
"markup": {
|
|
"task": task.name,
|
|
"color": task.color || '#CCC',
|
|
"width": (startIdx<endIdx) ? (endIdx-startIdx) * this.dayWidth : this.dayWidth,
|
|
}
|
|
})
|
|
for(const dep of task.dependsOn ?? []){
|
|
flow.links.push({ "from": [dep, "out1"], "to": [task.id, "inp1"] })
|
|
}
|
|
curY += 30 // row height
|
|
}
|
|
return flow
|
|
}
|
|
|
|
refresh(){
|
|
this.ul.innerHTML = ''
|
|
for(const task of this.tasks){
|
|
const li = document.createElement('li')
|
|
li.textContent = task.name
|
|
this.ul.appendChild(li)
|
|
}
|
|
const flow = this.buildFlow()
|
|
this.graflow.loadFlow({
|
|
"nodesFile": this.nodesfile,
|
|
"flow": flow
|
|
})
|
|
this.graflow.refresh()
|
|
}
|
|
}
|
|
Buildoz.define('gantt', BZgantt)
|