Files
buildoz/bzGantt.js
T
2026-08-28 19:40:56 +00:00

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)