Started bs-gantt
This commit is contained in:
+25
@@ -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
@@ -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)
|
||||
@@ -14,7 +14,6 @@ class BZgrafloweditor extends Buildoz{
|
||||
constructor(){
|
||||
super()
|
||||
this.defaultAttrs = { }
|
||||
window.debugEditor = this
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
|
||||
+3
-3
@@ -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){
|
||||
|
||||
@@ -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"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user