Started bs-gantt
This commit is contained in:
+183
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* _ ___ 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)
|
||||
Reference in New Issue
Block a user