Started bs-gantt

This commit is contained in:
STEINNI
2026-08-28 19:40:56 +00:00
parent 7471a37220
commit 6e902b91fd
9 changed files with 454 additions and 5 deletions
+25
View File
@@ -371,3 +371,28 @@ bz-grafloweditor .inner-console section .cols-2 input{
max-height: 2em;
align-self: center;
}
bz-gantt{
position: relative;
--row: 30px;
}
bz-gantt ul.tasks{
list-style: none;
padding: 0;
margin: 0;
width: 100%;
}
bz-gantt ul.tasks li{
box-sizing: border-box;
text-wrap: nowrap;
height: var(--row);
line-height: calc(var(--row) - 1px);
border-bottom: 1px dotted #555;
}
bz-gantt.gantt bz-graflow{
position: absolute;
left: 8em;
top: 0.5em;
}
+183
View File
@@ -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)
-1
View File
@@ -14,7 +14,6 @@ class BZgrafloweditor extends Buildoz{
constructor(){
super()
this.defaultAttrs = { }
window.debugEditor = this
}
async connectedCallback() {
+3 -3
View File
@@ -230,8 +230,8 @@ class BZgraflow extends Buildoz{
}
if(typeof(node.data)=='object') Object.assign(this.stagedNodes[id].dataset, node.data)
const portEls = this.stagedNodes[id].querySelectorAll('.port')
this.stagedNodes[id].style.left = (node.coords && node.coords.x) ? `${node.coords.x}px` : '10%'
this.stagedNodes[id].style.top = (node.coords && node.coords.y) ? `${node.coords.y}px` : '10%'
this.stagedNodes[id].style.left = (node.coords && node.coords.x != null) ? `${node.coords.x}px` : '10%'
this.stagedNodes[id].style.top = (node.coords && node.coords.y != null) ? `${node.coords.y}px` : '10%'
this.stagedNodes[id].dataset.id = id
this.stagedNodes[id].ports = Object.fromEntries(Array.from(portEls).map(item => ([item.dataset.id, { ...item.dataset, el:item }])))
if(node.subflow) {
@@ -490,7 +490,7 @@ class BZgraflow extends Buildoz{
this.clear()
let forceAutoplace = false
for(const node of this.flow.nodes){
if((!node.coords) || (!node.coords.x) ||(!node.coords.y)) forceAutoplace=true
if((!node.coords) || (node.coords.x == null) || (node.coords.y == null)) forceAutoplace=true
this.addNode(node)
}
for(const link of this.flow.links){
+62
View File
@@ -0,0 +1,62 @@
{
"nodesFile": "./nodesLib/nodesGantt.html",
"flow": {
"nodes":[
{ "nodeType": "bar",
"id": "t1",
"coords": { "x": 20, "y": 0},
"markup": { "task": "Task 1", "color": "#0F0", "width": 160 }
},
{ "nodeType": "bar",
"id": "t2",
"coords": { "x": 510, "y": 180},
"markup": { "task": "Task 2", "color": "#CCF", "width": 60 }
},
{ "nodeType": "bar",
"id": "t3",
"coords": { "x": 570, "y": 240},
"markup": { "task": "Task 3", "color": "#FFC", "width": 100 }
},
{ "nodeType": "bar",
"id": "t4",
"coords": { "x": 20, "y": 60},
"markup": { "task": "Task 4", "color": "#FCF", "width": 260 }
},
{ "nodeType": "bar",
"id": "t5",
"coords": { "x": 280, "y": 90},
"markup": { "task": "Task 5", "color": "#CFF", "width": 30 }
},
{ "nodeType": "bar",
"id": "t6",
"coords": { "x": 310, "y": 120},
"markup": { "task": "Task 6", "color": "#FFC", "width": 40 }
},
{ "nodeType": "bar",
"id": "t7",
"coords": { "x": 180, "y": 30},
"markup": { "task": "Task 7", "color": "#FCF", "width": 80 }
},
{ "nodeType": "bar",
"id": "t8",
"coords": { "x": 350, "y": 150},
"markup": { "task": "Task 8", "color": "#CFF", "width": 160 }
},
{ "nodeType": "bar",
"id": "t9",
"coords": { "x": 510, "y": 210},
"markup": { "task": "Task 9", "color": "#FFC", "width": 60 }
}
],
"links": [
{ "from": ["t1", "out1"], "to": ["t7", "inp1"] },
{ "from": ["t7", "out1"], "to": ["t5", "inp1"] },
{ "from": ["t8", "out1"], "to": ["t2", "inp1"] },
{ "from": ["t9", "out1"], "to": ["t3", "inp1"] },
{ "from": ["t4", "out1"], "to": ["t5", "inp1"] },
{ "from": ["t5", "out1"], "to": ["t6", "inp1"] },
{ "from": ["t6", "out1"], "to": ["t8", "inp1"] },
{ "from": ["t8", "out1"], "to": ["t9", "inp1"] }
]
}
}
+37
View File
@@ -0,0 +1,37 @@
<style>
.bzgf-node {
font-family: Arial, Helvetica, sans-serif;
color: black;
text-align: center;
position: absolute;
}
.bzgf-node .body{
border-radius: 4px;
overflow: hidden;
height: 1.2em;
text-overflow: ellipsis;
white-space: nowrap;
font-size: .9em;
padding: 0 2px 0 2px;
}
.bzgf-node .port{
position: absolute;
height: 1px;
width: 1px;
}
.bzgf-node [data-direction="w"]{ left: -4px; top: 50%; }
.bzgf-node [data-direction="e"]{ right: -4px; top: 50%; }
.bzgf-wire{ stroke: #AAA; stroke-width: 1; }
</style>
<template>
<div class="bzgf-node" data-nodetype="bar">
<div class="port" data-type="in" data-id="inp1" data-direction="w"></div>
<div class="port" data-type="out" data-id="out1" data-direction="e"></div>
<div class="body" style="background-color:{color};width:{width}px;" title="{task}">{task}</div>
</div>
</template>
+12
View File
@@ -126,6 +126,18 @@
<td>X</td>
<td></td>
</tr>
<tr>
<td><a target="test6" href="./test7.html">test7</a></td>
<td>Gantt chart 1</td>
<td>YES</td>
<td></td>
<td></td>
<td>Bezier</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<br>
+76
View File
@@ -0,0 +1,76 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>graflow</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="../../eicui/eicui-2.0.css">
<link type="text/css" rel="stylesheet" href="../buildoz.css">
<script src="../buildoz.js"></script>
<script src="../bzGraflow.js"></script>
<style>
@font-face { /*FF does not indirectly load if inside a shawdow-dom */
font-family: 'glyphs';
src: url('/assets/styles/fonts/glyphs.eot');
src: url('/assets/styles/fonts/glyphs.eot') format('embedded-opentype'),
url('/assets/styles/fonts/glyphs.ttf') format('truetype'),
url('/assets/styles/fonts/glyphs.woff') format('woff'),
url('/assets/styles/fonts/glyphs.svg') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
body{
background:#888;
font-size: 16px;
}
.gantt-wrap{
position: relative;
--row: 30px;
}
ul.tasks{
list-style: none;
padding: 0;
margin: 0;
width: 100%;
}
ul.tasks li{
box-sizing: border-box;
text-wrap: nowrap;
height: var(--row);
line-height: calc(var(--row) - 1px);
border-bottom: 1px dotted #555;
}
bz-graflow.gantt{
position: absolute;
left: 10em;
top: 6px;
right: 0;
bottom: 0;
}
</style>
<script>
window.addEventListener('load',()=>{
let grflw1 = document.querySelector('bz-graflow.gantt')
})
</script>
</head>
<body>
<div class="gantt-wrap">
<ul class="tasks">
<li>Task 1</li>
<li>Task 7</li>
<li>Task 4</li>
<li>Task 5</li>
<li>Task 6</li>
<li>Task 8</li>
<li>Task 2</li>
<li>Task 9</li>
<li>Task 3</li>
</ul>
<bz-graflow class="gantt" flow="./flows/gantt.json" tension="60" isolated></bz-graflow>
</div>
</body>
</html>
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>graflow</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link type="text/css" rel="stylesheet" href="../../eicui/eicui-2.0.css">
<link type="text/css" rel="stylesheet" href="../buildoz.css">
<script src="../buildoz.js"></script>
<script src="../bzGraflow.js"></script>
<script src="../bzGantt.js"></script>
<style>
@font-face { /*FF does not indirectly load if inside a shawdow-dom */
font-family: 'glyphs';
src: url('/assets/styles/fonts/glyphs.eot');
src: url('/assets/styles/fonts/glyphs.eot') format('embedded-opentype'),
url('/assets/styles/fonts/glyphs.ttf') format('truetype'),
url('/assets/styles/fonts/glyphs.woff') format('woff'),
url('/assets/styles/fonts/glyphs.svg') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
body{
background:#888;
font-size: 16px;
}
</style>
<script>
const ganttData = {
"tasks": [
{ "id": "t1", "name": "Task 1", "start": "2026-09-01", "end":"2026-09-05" },
{ "id": "t2", "name": "Task 2", "start": "2026-09-15", "end":"2026-09-20", "dependsOn": ["t8"] },
{ "id": "t3", "name": "Task 3", "start": "2026-09-20", "end":"2026-09-25", "dependsOn": ["t9"] },
{ "id": "t4", "name": "Task 4", "start": "2026-09-01", "end":"2026-09-10" },
{ "id": "t5", "name": "Task 5", "start": "2026-09-05", "end":"2026-09-07", "dependsOn": ["t4", "t7"] },
{ "id": "t6", "name": "Task 6", "start": "2026-09-07", "end":"2026-09-08", "dependsOn": ["t5"] },
{ "id": "t7", "name": "Task 7", "start": "2026-09-05", "end":"2026-09-08", "dependsOn": ["t1"] },
{ "id": "t8", "name": "Task 8", "start": "2026-09-08", "end":"2026-09-15", "dependsOn": ["t6"] },
{ "id": "t9", "name": "Task 9", "start": "2026-09-15", "end":"2026-09-18", "dependsOn": ["t8"] },
]
}
window.addEventListener('load',()=>{
let gantt = document.querySelector('bz-gantt.gantt')
gantt.loadData(ganttData)
gantt.refresh()
})
</script>
</head>
<body>
<bz-gantt class="gantt" nodesfile="./nodesLib/nodesGantt.html" tension="60" isolated></bz-gantt>
</body>
</html>