gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
514 lines (473 loc) • 21.1 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, viewport-fit=cover"/>
<meta name="description" content="A Gantt chart that supports zooming into the timeline."/>
<link rel="stylesheet" href="../assets/css/style.css"/>
<!-- Copyright 1998-2022 by Northwoods Software Corporation. -->
<title>Gantt chart</title>
</head>
<body>
<!-- This top nav is not part of the sample code -->
<nav id="navTop" class="w-full z-30 top-0 text-white bg-nwoods-primary">
<div class="w-full container max-w-screen-lg mx-auto flex flex-wrap sm:flex-nowrap items-center justify-between mt-0 py-2">
<div class="md:pl-4">
<a class="text-white hover:text-white no-underline hover:no-underline
font-bold text-2xl lg:text-4xl rounded-lg hover:bg-nwoods-secondary " href="../">
<h1 class="mb-0 p-1 ">GoJS</h1>
</a>
</div>
<button id="topnavButton" class="rounded-lg sm:hidden focus:outline-none focus:ring" aria-label="Navigation">
<svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6">
<path id="topnavOpen" fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z" clip-rule="evenodd"></path>
<path id="topnavClosed" class="hidden" fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
<div id="topnavList" class="hidden sm:block items-center w-auto mt-0 text-white p-0 z-20">
<ul class="list-reset list-none font-semibold flex justify-end flex-wrap sm:flex-nowrap items-center px-0 pb-0">
<li class="p-1 sm:p-0"><a class="topnav-link" href="../learn/">Learn</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="../samples/">Samples</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="../intro/">Intro</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="../api/">API</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/products/register.html">Register</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="../download.html">Download</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="https://forum.nwoods.com/c/gojs/11">Forum</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/contact.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a></li>
<li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/sales/index.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a></li>
</ul>
</div>
</div>
<hr class="border-b border-gray-600 opacity-50 my-0 py-0" />
</nav>
<div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto">
<div id="navSide" class="flex flex-col w-full md:w-48 text-gray-700 bg-white flex-shrink-0"></div>
<!-- * * * * * * * * * * * * * -->
<!-- Start of GoJS sample code -->
<script src="../release/go.js"></script>
<div id="allSampleContent" class="p-4 w-full">
<div id="sample">
<div style="width: 100%; display: flex; justify-content: space-between; border: solid 1px black">
<div id="myTasksDiv" style="width: 150px; margin-right: 2px; border-right: solid 1px black"></div>
<div id="myGanttDiv" style="flex-grow: 1; height: 400px"></div>
</div>
<div id="slider">
<label>Spacing:</label>
<input id="widthSlider" type="range" min="8" max="24" value="12" oninput="rescale()"/>
</div>
<p>
This sample demonstrates a simple Gantt chart.
Gantt charts are used to illustrate project schedules, denoting the start and end dates for terminal and summary elements of the project.
</p>
<p>
You can zoom in on the diagram by changing the "Spacing" value,
which scales the diagram using a data binding function for nodes' widths and locations.
This is in place of changing the <a>Diagram.scale</a>.
</p>
<p>
The current model in JSON format:
</p>
<textarea id="mySavedModel" style="width:100%;height:250px"></textarea>
</div>
<script id="code">
// Custom Layout for myGantt Diagram
class GanttLayout extends go.Layout {
constructor() {
super();
this.cellHeight = GridCellHeight;
}
doLayout(coll) {
coll = this.collectParts(coll);
var diagram = this.diagram;
diagram.startTransaction("Gantt Layout");
this.assignTimes(diagram);
this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
var y = this.arrangementOrigin.y;
var it = coll.iterator;
while (it.next()) {
var node = it.value;
if (!(node instanceof go.Node)) continue; // skip Links
var tasknode = myTasks.findNodeForData(node.data);
node.visible = tasknode.isVisible();
if (node.visible) {
node.position = new go.Point(convertStartToX(node.data.start || 0), y);
y += this.cellHeight;
}
}
diagram.commitTransaction("Gantt Layout");
}
// Update node data, to make sure each node has a start and a duration
assignTimes(diagram) {
var roots = diagram.findTreeRoots();
roots.each(root => this.walkTree(root, 0));
}
walkTree(node, start) {
var model = node.diagram.model;
if (node.isTreeLeaf) {
var dur = node.data.duration;
if (dur === undefined || isNaN(dur)) {
dur = 100; // default task length?
model.set(node.data, "duration", dur);
}
var st = node.data.start;
if (st === undefined || isNaN(st)) {
st = start; // use given START
model.set(node.data, "start", st);
}
return st + dur;
} else {
// first recurse to fill in any missing data
node.findTreeChildrenNodes().each(n => {
start = this.walkTree(n, start);
});
// now can calculate this non-leaf node's data
var min = Infinity;
var max = -Infinity;
var colors = new go.Set();
node.findTreeChildrenNodes().each(n => {
min = Math.min(min, n.data.start);
max = Math.max(max, n.data.start + n.data.duration);
if (n.data.color) colors.add(n.data.color);
});
model.set(node.data, "start", min);
model.set(node.data, "duration", max - min);
return max;
}
}
}
// end of GanttLayout
var GridCellHeight = 20;
var GridCellWidth = 12; // document units per day
var TimelineHeight = 24;
function convertStartToX(start) {
return start * GridCellWidth;
}
function convertXToStart(x, node) {
return x / GridCellWidth;
}
function convertDurationToW(duration) {
return duration * GridCellWidth;
}
function convertWToDuration(w) {
return w / GridCellWidth;
}
function convertStartToPosition(start, node) {
return new go.Point(convertStartToX(start), node.position.y || 0);
}
function convertPositionToStart(pos) {
return convertXToStart(pos.x);
}
var StartDate = new Date(2018, 6, 14);
var MsPerDay = 24 * 60 * 60 * 1000;
function valueToDate(n) { // N is in days
var startDate = StartDate;
var startDateMs = startDate.getTime() + startDate.getTimezoneOffset() * 60000;
var date = new Date(startDateMs + (n * MsPerDay) / GridCellWidth);
return date.toLocaleDateString();
}
function dateToValue(d) {
var startDate = StartDate;
var startDateMs = startDate.getTime() + startDate.getTimezoneOffset() * 60000;
var dateInMs = d.getTime() + d.getTimezoneOffset() * 60000;
var msSinceStart = dateInMs - startDateMs;
return msSinceStart * GridCellWidth / MsPerDay;
}
// the custom figure used for task bars that have downward points at their ends
go.Shape.defineFigureGenerator("RangeBar", (shape, w, h) => {
var b = Math.min(5, w);
var d = Math.min(5, h);
return new go.Geometry()
.add(new go.PathFigure(0, 0, true)
.add(new go.PathSegment(go.PathSegment.Line, w, 0))
.add(new go.PathSegment(go.PathSegment.Line, w, h))
.add(new go.PathSegment(go.PathSegment.Line, w-b, h-d))
.add(new go.PathSegment(go.PathSegment.Line, b, h-d))
.add(new go.PathSegment(go.PathSegment.Line, 0, h).close()));
});
const $ = go.GraphObject.make;
// the left side of the whole diagram
myTasks =
new go.Diagram("myTasksDiv",
{
initialContentAlignment: go.Spot.Right,
padding: new go.Margin(TimelineHeight, 0, 0, 0),
hasVerticalScrollbar: false,
allowMove: false,
allowCopy: false,
allowDelete: false,
layout:
$(go.TreeLayout,
{
alignment: go.TreeLayout.AlignmentStart,
compaction: go.TreeLayout.CompactionNone,
layerSpacing: 16,
layerSpacingParentOverlap: 1,
nodeIndentPastParent: 1,
nodeSpacing: 0,
portSpot: go.Spot.Bottom,
childPortSpot: go.Spot.Left
}),
"animationManager.isInitial": false,
"TreeCollapsed": e => myGantt.layoutDiagram(true),
"TreeExpanded": e => myGantt.layoutDiagram(true),
// Showing a grid in the tasks tree diagram
//"DocumentBoundsChanged": e => {
// var b = e.diagram.documentBounds;
// var gridpart = e.diagram.parts.first();
// if (gridpart !== null && gridpart.type === go.Panel.Grid) {
// gridpart.desiredSize = new go.Size(b.right, b.bottom);
// }
//}
});
// Showing a grid in the tasks tree diagram
//myTasks.add($(go.Part, "Grid",
// { layerName: "Grid", position: new go.Point(0, 0), gridCellSize: new go.Size(3000, GridCellHeight) },
// $(go.Shape, "LineH", { strokeWidth: 0.5 })
//));
myTasks.nodeTemplate =
$(go.Node, "Horizontal",
{ height: 20 },
new go.Binding("isTreeExpanded").makeTwoWay(),
$("TreeExpanderButton", { portId: "", scale: 0.85 }),
$(go.TextBlock,
{ editable: true },
new go.Binding("text").makeTwoWay())
);
myTasks.linkTemplate =
$(go.Link,
{
routing: go.Link.Orthogonal,
fromEndSegmentLength: 1,
toEndSegmentLength: 1
},
$(go.Shape)
);
myTasks.linkTemplateMap.add("Dep",
$(go.Link, // ignore these links in the Tasks diagram
{ visible: false, isTreeLink: false }
));
// the right side of the whole diagram
myGantt =
new go.Diagram("myGanttDiv",
{
initialPosition: new go.Point(-7, -100), // show labels
padding: new go.Margin(TimelineHeight, 0, 0, 0),
scrollMargin: new go.Margin(0, GridCellWidth*7, 0, 0), // show a week beyond
allowCopy: false,
allowDelete: false,
"draggingTool.isGridSnapEnabled": true,
"draggingTool.gridSnapCellSize": new go.Size(GridCellWidth, GridCellHeight),
"draggingTool.dragsTree": true,
"resizingTool.isGridSnapEnabled": true,
"resizingTool.cellSize": new go.Size(GridCellWidth, GridCellHeight),
"resizingTool.minSize": new go.Size(GridCellWidth, GridCellHeight),
layout: $(GanttLayout),
"animationManager.isInitial": false,
"SelectionMoved": e => e.diagram.layoutDiagram(true),
"DocumentBoundsChanged": e => {
// the grid extends to only the area needed
var b = e.diagram.documentBounds;
var gridpart = e.diagram.parts.first();
if (gridpart !== null && gridpart.type === go.Panel.Grid) {
gridpart.desiredSize = new go.Size(b.right - gridpart.position.x, b.bottom);
}
// the timeline only covers the needed area
myTimeline.findObject("MAIN").width = b.right;
myTimeline.findObject("TICKS").height = e.diagram.viewportBounds.height;
myTimeline.graduatedMax = b.right;
}
});
myGantt.add($(go.Part, "Grid",
{ layerName: "Grid", position: new go.Point(-10, 0), gridCellSize: new go.Size(3000, GridCellHeight) },
$(go.Shape, "LineH", { strokeWidth: 0.5 })
));
myGantt.nodeTemplate =
$(go.Node, "Spot",
{
selectionAdorned: false,
selectionChanged: node => {
node.diagram.commit(diag => {
node.findObject("SHAPE").fill = node.isSelected ? "dodgerblue" : node.data.color || "gray";
}, null);
},
minLocation: new go.Point(0, NaN),
maxLocation: new go.Point(Infinity, NaN),
toolTip:
$("ToolTip",
$(go.Panel, "Table",
{ defaultAlignment: go.Spot.Left },
$(go.RowColumnDefinition, { column: 1, separatorPadding: 3 }),
$(go.TextBlock, { row: 0, column: 0, columnSpan: 9, font: "bold 12pt sans-serif" }, new go.Binding("text")),
$(go.TextBlock, { row: 1, column: 0 }, "start:"),
$(go.TextBlock, { row: 1, column: 1 }, new go.Binding("text", "start", d => "day " + d.toFixed(0))),
$(go.TextBlock, { row: 2, column: 0 }, "length:"),
$(go.TextBlock, { row: 2, column: 1 }, new go.Binding("text", "duration", d => d.toFixed(0) + " days")),
)
),
resizable: true,
resizeObjectName: "SHAPE",
resizeAdornmentTemplate: $(go.Adornment, "Spot",
$(go.Placeholder),
$(go.Shape, "Diamond",
{
alignment: go.Spot.Right,
width: 8, height: 8,
strokeWidth: 0,
fill: "fuchsia",
cursor: 'e-resize'
}
)
)
},
new go.Binding("position", "start", convertStartToPosition).makeTwoWay(convertPositionToStart),
new go.Binding("resizable", "isTreeLeaf").ofObject(),
new go.Binding("isTreeExpanded").makeTwoWay(),
$(go.Shape,
{ name: "SHAPE", height: 18, margin: new go.Margin(1, 0), strokeWidth: 0, fill: "gray" },
new go.Binding("fill", "color"),
new go.Binding("width", "duration", convertDurationToW).makeTwoWay(convertWToDuration),
new go.Binding("figure", "isTreeLeaf", leaf => leaf ? "Rectangle" : "RangeBar").ofObject()),
// "RangeBar" is defined above as a custom figure
$(go.TextBlock,
{ font: "8pt sans-serif", alignment: go.Spot.TopLeft, alignmentFocus: new go.Spot(0, 0, 0, -2) },
new go.Binding("text"),
new go.Binding("stroke", "color", c => go.Brush.isDark(c) ? "white" : "black"))
);
myGantt.linkTemplate =
$(go.Link, { visible: false });
myGantt.linkTemplateMap.add("Dep",
$(go.Link,
{
routing: go.Link.Orthogonal,
isTreeLink: false, isLayoutPositioned: false,
fromSpot: new go.Spot(0.999999, 1), toSpot: new go.Spot(0.000001, 0)
},
$(go.Shape, { stroke: "brown" }),
$(go.Shape, { toArrow: "Standard", fill: "brown", strokeWidth: 0, scale: 0.75 })
));
// the timeline
var myTimeline =
$(go.Part, "Graduated",
{
layerName: "Adornment",
location: new go.Point(0, 0),
locationSpot: go.Spot.Left,
locationObjectName: "MAIN",
graduatedTickUnit: GridCellWidth
},
$(go.Shape, "LineH", { name: "MAIN", strokeWidth: 0, height: TimelineHeight, background: "lightgray" }),
$(go.Shape, { name: "TICKS", geometryString: "M0 0 V1000", interval: 7, stroke: "lightgray", strokeWidth: 0.5 }),
$(go.TextBlock,
{
alignmentFocus: go.Spot.Left,
interval: 7, // once per week
graduatedFunction: valueToDate
}
)
);
myGantt.add(myTimeline);
// The Model that is shared by both Diagrams
var model = new go.GraphLinksModel(
[
{ key: 0, text: "Project X" },
{ key: 1, text: "Task 1", color: "darkgreen" },
{ key: 11, text: "Task 1.1", color: "green", duration: 10 },
{ key: 12, text: "Task 1.2", color: "green" },
{ key: 121, text: "Task 1.2.1", color: "lightgreen", duration: 3 },
{ key: 122, text: "Task 1.2.2", color: "lightgreen", duration: 5 },
{ key: 123, text: "Task 1.2.3", color: "lightgreen", duration: 4 },
{ key: 2, text: "Task 2", color: "darkblue" },
{ key: 21, text: "Task 2.1", color: "blue", duration: 15, start: 10 },
{ key: 22, text: "Task 2.2", color: "goldenrod" },
{ key: 221, text: "Task 2.2.1", color: "yellow", duration: 8 },
{ key: 222, text: "Task 2.2.2", color: "yellow", duration: 6 },
{ key: 23, text: "Task 2.3", color: "darkorange" },
{ key: 231, text: "Task 2.3.1", color: "orange", duration: 11 },
{ key: 3, text: "Task 3", color: "maroon" },
{ key: 31, text: "Task 3.1", color: "brown", duration: 10 },
{ key: 32, text: "Task 3.2", color: "brown" },
{ key: 321, text: "Task 3.2.1", color: "lightsalmon", duration: 8 },
{ key: 322, text: "Task 3.2.2", color: "lightsalmon", duration: 3 },
{ key: 323, text: "Task 3.2.3", color: "lightsalmon", duration: 7 },
{ key: 324, text: "Task 3.2.4", color: "lightsalmon", duration: 5, start: 71 },
{ key: 325, text: "Task 3.2.5", color: "lightsalmon", duration: 4 },
{ key: 326, text: "Task 3.2.6", color: "lightsalmon", duration: 5 },
],
[
{ from: 0, to: 1 },
{ from: 1, to: 11 },
{ from: 1, to: 12 },
{ from: 12, to: 121 },
{ from: 12, to: 122 },
{ from: 12, to: 123 },
{ from: 0, to: 2 },
{ from: 2, to: 21 },
{ from: 2, to: 22 },
{ from: 22, to: 221 },
{ from: 22, to: 222 },
{ from: 2, to: 23 },
{ from: 23, to: 231 },
{ from: 0, to: 3 },
{ from: 3, to: 31 },
{ from: 3, to: 32 },
{ from: 32, to: 321 },
{ from: 32, to: 322 },
{ from: 32, to: 323 },
{ from: 32, to: 324 },
{ from: 32, to: 325 },
{ from: 32, to: 326 },
{ from: 11, to: 2, category: "Dep" },
]);
// share model
myTasks.model = model;
myGantt.model = model;
model.addChangedListener(e => {
if (e.isTransactionFinished) { // show the model data in the page's TextArea
document.getElementById("mySavedModel").textContent = e.model.toJson();
}
})
model.undoManager.isEnabled = true;
// sync viewports
var changingView = false;
myTasks.addDiagramListener("ViewportBoundsChanged", e => {
if (changingView) return;
changingView = true;
myGantt.scale = myTasks.scale;
myGantt.position = new go.Point(myGantt.position.x, myTasks.position.y);
myTimeline.position = new go.Point(myTimeline.position.x, myTasks.viewportBounds.position.y);
changingView = false;
});
myGantt.addDiagramListener("ViewportBoundsChanged", e => {
if (changingView) return;
changingView = true;
myTasks.scale = myGantt.scale;
myTasks.position = new go.Point(myTasks.position.x, myGantt.position.y);
myTimeline.position = new go.Point(myTimeline.position.x, myGantt.viewportBounds.position.y);
changingView = false;
});
// change horizontal scale
function rescale() {
var val = parseFloat(document.getElementById("widthSlider").value);
console.log(val);
myGantt.commit(diag => {
GridCellWidth = val;
diag.scrollMargin = new go.Margin(0, GridCellWidth*7, 0, 0);
diag.toolManager.draggingTool.gridSnapCellSize = new go.Size(GridCellWidth, GridCellHeight);
diag.toolManager.resizingTool.cellSize = new go.Size(GridCellWidth, GridCellHeight);
diag.toolManager.resizingTool.minSize = new go.Size(GridCellWidth, GridCellHeight);
diag.updateAllTargetBindings();
diag.layoutDiagram(true);
myTimeline.graduatedTickUnit = GridCellWidth;
}, null); // skipsUndoManager
}
</script>
</div>
<!-- * * * * * * * * * * * * * -->
<!-- End of GoJS sample code -->
</div>
</body>
<!-- This script is part of the gojs.net website, and is not needed to run the sample -->
<script src="../assets/js/goSamples.js"></script>
</html>