Here’s an updated gantt.html
with the JavaScript removed:
<!DOCTYPE html>
<html lang="en">
<body>
<link href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<p>
This is a minimalist HTML and JavaScript skeleton of the GoJS Sample
<a href="https://gojs.net/latest/samples/gantt.html">gantt.html</a>. It was automatically generated from a button on the sample page,
and does not contain the full HTML. It is intended as a starting point to adapt for your own usage.
For many samples, you may need to inspect the
<a href="https://github.com/NorthwoodsSoftware/GoJS/blob/master/samples/gantt.html">full source on Github</a>
and copy other files or scripts.
</p>
<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: 280px; 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">
</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, automatically updated as the diagram is modified:</p>
<textarea id="mySavedModel" style="width: 100%; height: 250px"></textarea>
</div>
<script type="importmap">{"imports":{"gojs":"../latest/release/go-module.js"}}</script>
<script id="code" type="module" src="gantt.js">
</script>
</div>
</body>
</html>
And the corresponding gantt.ts
file:
/*
* Copyright 1998-2025 by Northwoods Software Corporation. All Rights Reserved.
*/
// This is a TypeScript version of the JavaScript code in the samples/gantt.html sample.
import * as go from "gojs";
// Custom Layout for myGantt Diagram
class GanttLayout extends go.Layout {
private _cellHeight: number;
constructor(init?: Partial<GanttLayout>) {
super();
this._cellHeight = GridCellHeight;
if (init) Object.assign(this, init);
}
get cellHeight(): number { return this._cellHeight; }
set cellHeight(h: number) {
if (h !== this._cellHeight) {
this._cellHeight = h;
this.invalidateLayout();
}
}
override doLayout(coll: go.Iterable<go.Part> | go.Diagram | go.Group) {
coll = this.collectParts(coll);
const diagram = this.diagram;
if (!diagram) return;
diagram.startTransaction('Gantt Layout');
const bars = [] as Array<go.Node>;
this.assignTimes(diagram, bars);
this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
let y = this.arrangementOrigin.y;
bars.forEach((node) => {
const tasknode = myTasks.findNodeForData(node.data);
if (!tasknode) return;
node.visible = tasknode.isVisible();
node.moveTo(convertStartToX(node.data.start), y);
if (node.visible) y += this.cellHeight;
});
diagram.commitTransaction('Gantt Layout');
}
// Update node data, to make sure each node has a start and a duration
assignTimes(diagram: go.Diagram, bars: Array<go.Node>) {
const roots = diagram.findTreeRoots();
roots.each((root) => this.walkTree(root, 0, bars));
}
walkTree(node: go.Node, start: number, bars: Array<go.Node>) {
bars.push(node);
if (!node.diagram) return start;
const model = node.diagram.model;
if (node.isTreeLeaf) {
let dur = node.data.duration;
if (dur === undefined || isNaN(dur)) {
dur = convertDaysToUnits(1); // default task length?
model.set(node.data, 'duration', dur);
}
let 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, bars);
});
// now can calculate this non-leaf node's data
let min = Infinity;
let max = -Infinity;
const 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; // document units; cannot be changed dynamically
var GridCellWidth = 12; // document units per day; this can be modified -- see rescale()
var TimelineHeight = 24; // document units; cannot be changed dynamically
const MsPerDay = 24 * 60 * 60 * 1000;
// By default the values for the data properties start and duration are in days,
// and the start value is relative to the StartDate.
// If you want the start and duration properties to be in a unit other than days,
// you only need to change the implementation of convertDaysToUnits and convertUnitsToDays.
function convertDaysToUnits(n: number): number {
return n;
}
function convertUnitsToDays(n: number): number {
return n;
}
function convertStartToX(start: number): number {
return convertUnitsToDays(start) * GridCellWidth;
}
function convertXToStart(x: number): number {
return convertDaysToUnits(x / GridCellWidth);
}
// these four functions are used in TwoWay Bindings on the task/node template
function convertDurationToW(duration: number): number {
return convertUnitsToDays(duration) * GridCellWidth;
}
function convertWToDuration(w: number): number {
return convertDaysToUnits(w / GridCellWidth);
}
function convertStartToPosition(start: number, node: go.Node): go.Point {
return new go.Point(convertStartToX(start), node.position.y || 0);
}
function convertPositionToStart(pos: go.Point): number {
return convertXToStart(pos.x);
}
var StartDate = new Date(); // set from Model.modelData.origin
function valueToText(n: number): string {
// N document units after StartDate
const startDate = StartDate;
const startDateMs = startDate.getTime() + startDate.getTimezoneOffset() * 60000;
const date = new Date(startDateMs + (n / GridCellWidth) * MsPerDay);
return date.toLocaleDateString();
}
function dateToValue(d: Date): number {
// D is a Date
const startDate = StartDate;
const startDateMs = startDate.getTime() + startDate.getTimezoneOffset() * 60000;
const dateInMs = d.getTime() + d.getTimezoneOffset() * 60000;
const msSinceStart = dateInMs - startDateMs;
return (msSinceStart / MsPerDay) * GridCellWidth;
}
// the custom figure used for task bars that have downward points at their ends
go.Shape.defineFigureGenerator('RangeBar', (shape, w, h) => {
const b = Math.min(5, w);
const d = Math.min(5, h);
return new go.Geometry().add(
new go.PathFigure(0, 0, true)
.add(new go.PathSegment(go.SegmentType.Line, w, 0))
.add(new go.PathSegment(go.SegmentType.Line, w, h))
.add(new go.PathSegment(go.SegmentType.Line, w - b, h - d))
.add(new go.PathSegment(go.SegmentType.Line, b, h - d))
.add(new go.PathSegment(go.SegmentType.Line, 0, h).close())
);
});
function standardContextMenus() {
return {
contextMenu: go.GraphObject.build('ContextMenu')
.add(
go.GraphObject.build('ContextMenuButton', {
click: (e, button) => {
const task = (button.part as go.Adornment).adornedPart;
}
})
.add(
new go.TextBlock('Details...')
),
go.GraphObject.build('ContextMenuButton', {
click: (e, button) => {
const task = (button.part as go.Adornment).adornedPart;
e.diagram.model.commit((m) => {
const newdata = { key: undefined, text: 'New Task', color: task?.data.color, duration: convertDaysToUnits(5) };
m.addNodeData(newdata);
(m as go.GraphLinksModel).addLinkData({ from: task?.key, to: newdata.key });
e.diagram.select(e.diagram.findNodeForData(newdata));
});
}
})
.add(
new go.TextBlock('New Task')
)
)
};
}
class CustomTreeLayout extends go.TreeLayout {
constructor(init: Partial<CustomTreeLayout>) {
super();
if (init) Object.assign(this, init);
}
// after the tree layout, change the width of each node so that all
// of the nodes have widths such that the collection has a given width
override commitNodes() {
super.commitNodes();
updateNodeWidths(400);
}
}
// the tree on the left side of the page
const myTasks = new go.Diagram('myTasksDiv', {
initialContentAlignment: go.Spot.Right,
// make room on top for myTimeline and a bit of spacing; on bottom for whole task row and a bit more
padding: new go.Margin(TimelineHeight + 4, 0, GridCellHeight, 0), // needs to be the same vertically as for myGantt
hasVerticalScrollbar: false,
allowMove: false,
allowCopy: false,
'commandHandler.deletesTree': true,
layout: new CustomTreeLayout({
alignment: go.TreeAlignment.Start,
compaction: go.TreeCompaction.None,
layerSpacing: 16,
layerSpacingParentOverlap: 1,
nodeIndentPastParent: 1,
nodeSpacing: 0,
portSpot: go.Spot.Bottom,
childPortSpot: go.Spot.Left,
arrangementSpacing: new go.Size(0, 0),
}),
mouseLeave: () => (myHighlightTask.visible = false),
'animationManager.isInitial': false,
TreeCollapsed: (e) => myGantt.layoutDiagram(true),
TreeExpanded: (e) => myGantt.layoutDiagram(true),
ChangedSelection: (e) => {
// selecting a bar also selects the corresponding task in myTasks
if (myChangingSelection) return;
myChangingSelection = true;
const tasks = [] as Array<go.Node>;
e.diagram.selection.each((part) => {
if (part instanceof go.Node) {
const node = myGantt.findNodeForData(part.data);
if (node) tasks.push(node);
}
});
myGantt.selectCollection(tasks);
myChangingSelection = false;
}
});
var myChangingSelection = false;
myTasks.nodeTemplate = new go.Node('Table', {
columnSizing: go.Sizing.None,
selectionAdorned: false,
height: GridCellHeight,
mouseEnter: (e, node) => {
node.background = 'rgba(0,0,255,0.2)';
myHighlightTask.position = new go.Point(myGrid.actualBounds.x, node.actualBounds.y);
myHighlightTask.width = myGrid.actualBounds.width;
myHighlightTask.visible = true;
},
mouseLeave: (e, node) => {
node.background = (node as go.Node).isSelected ? 'dodgerblue' : 'transparent';
myHighlightTask.visible = false;
},
doubleClick: (e, node) => {
// scroll myGantt so the corresponding bar is visible
const bar = myGantt.findNodeForData((node as go.Node).data);
if (bar) myGantt.commandHandler.scrollToPart(bar);
},
...standardContextMenus()
})
.bindObject('background', 'isSelected', (s) => (s ? 'dodgerblue' : 'transparent'))
.bindTwoWay('isTreeExpanded')
.addColumnDefinition(0, { width: 14 })
.addColumnDefinition(1, { alignment: go.Spot.Left })
.addColumnDefinition(2, {
width: 40,
alignment: go.Spot.Right,
separatorPadding: new go.Margin(0, 4),
separatorStroke: 'gray'
})
.addColumnDefinition(3, {
width: 40,
alignment: go.Spot.Right,
separatorPadding: new go.Margin(0, 4),
separatorStroke: 'gray'
})
.add(
go.GraphObject.build('TreeExpanderButton', { column: 0, portId: '', scale: 0.85 }),
new go.TextBlock({ column: 1, editable: true })
.bindTwoWay('text'),
// additional columns
new go.TextBlock({ column: 2 })
.bind('text', 'start', (s) => s.toFixed(2)),
new go.TextBlock({ column: 3 })
.bind('text', 'duration', (d) => d.toFixed(2))
);
var TREEWIDTH = 160; // document units, may be modified, used by updateNodeWidths
function updateNodeWidths(width: number) {
let minx = Infinity;
myTasks.nodes.each((n) => {
if (n instanceof go.Node) {
minx = Math.min(minx, n.actualBounds.x);
}
});
if (minx === Infinity) return;
const right = minx + width;
myTasks.nodes.each((n) => {
if (n instanceof go.Node) {
n.width = Math.max(0, right - n.actualBounds.x);
n.getColumnDefinition(1).width = TREEWIDTH - n.actualBounds.x;
}
});
myTasksHeader.getColumnDefinition(1).width = TREEWIDTH - myTasksHeader.actualBounds.x;
}
const myTasksHeader = new go.Part('Table', { // the timeline at the top of the myTasks viewport
layerName: 'Adornment',
pickable: false,
position: new go.Point(-26, 0), // position will be set in "ViewportBoundsChanged" listener
columnSizing: go.Sizing.None,
selectionAdorned: false,
height: GridCellHeight,
background: 'lightgray'
})
.addColumnDefinition(0, { width: 14 })
.addColumnDefinition(2, { width: 40, alignment: go.Spot.Right, separatorPadding: new go.Margin(0, 4), separatorStroke: 'gray' })
.addColumnDefinition(3, { width: 40, alignment: go.Spot.Right, separatorPadding: new go.Margin(0, 4), separatorStroke: 'gray' })
.add(
new go.TextBlock('Name', { column: 1 }),
// additional columns
new go.TextBlock('Start', { column: 2 }),
new go.TextBlock('Dur.', { column: 3 })
);
myTasks.add(myTasksHeader);
myTasks.linkTemplate = new go.Link({
selectable: false,
routing: go.Routing.Orthogonal,
fromEndSegmentLength: 1,
toEndSegmentLength: 1
})
.add(
new go.Shape()
);
myTasks.linkTemplateMap.add('Dep',
new go.Link({ // ignore these links in the Tasks diagram
selectable: false,
visible: false,
isTreeLink: false
})
);
// the right side of the page, holding both the timeline and all of the task bars
const myGantt = new go.Diagram('myGanttDiv', {
initialPosition: new go.Point(-10, -100), // show labels
// make room on top for myTimeline and a bit of spacing; on bottom for whole task row and a bit more
padding: new go.Margin(TimelineHeight + 4, GridCellWidth * 7, GridCellHeight, 0), // needs to be the same vertically as for myTasks
scrollMargin: new go.Margin(0, GridCellWidth * 7, 0, 0), // and allow scrolling to a week beyond that
allowCopy: false,
'commandHandler.deletesTree': true,
'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: new GanttLayout(),
mouseOver: (e) => {
if (!myGrid || !myHighlightDay) return;
const lp = myGrid.getLocalPoint(e.documentPoint);
const day = Math.floor(convertXToStart(lp.x)); // floor gets start of day
myHighlightDay.position = new go.Point(convertStartToX(day), myGrid.position.y);
myHighlightDay.width = GridCellWidth; // 1 day
myHighlightDay.height = myGrid.actualBounds.height;
myHighlightDay.visible = true;
},
mouseLeave: (e) => (myHighlightDay.visible = false),
'animationManager.isInitial': false,
SelectionMoved: (e) => e.diagram.layoutDiagram(true),
DocumentBoundsChanged: (e) => {
// the grid extends to only the area needed
const b = e.diagram.documentBounds;
myGrid.desiredSize = new go.Size(b.width + GridCellWidth * 7, b.bottom);
// the timeline, which is not in the documentBounds, only covers the needed area
// widen to cover whole weeks
myTimeline.graduatedMax = Math.ceil(b.width / (GridCellWidth * 7)) * (GridCellWidth * 7);
const main = myTimeline.findObject('MAIN');
if (main) main.width = myTimeline.graduatedMax;
const ticks = myTimeline.findObject('TICKS');
if (ticks) ticks.height = Math.max(e.diagram.documentBounds.height, e.diagram.viewportBounds.height);
},
ChangedSelection: (e) => {
// selecting a task also selects the corresponding bar in myGantt
if (myChangingSelection) return;
myChangingSelection = true;
const bars = [] as Array<go.Node>;
e.diagram.selection.each((part) => {
if (part instanceof go.Node) {
const n = myTasks.findNodeForData(part.data);
if (n) bars.push(n);
}
});
myTasks.selectCollection(bars);
myChangingSelection = false;
}
});
const myTimeline = new go.Part('Graduated', { // the timeline at the top of the myGantt viewport
layerName: 'Adornment',
pickable: false,
position: new go.Point(-26, 0), // position will be set in "ViewportBoundsChanged" listener
graduatedTickUnit: GridCellWidth // each tick is one day
// assume graduatedMax == length of line
})
.add(
new go.Shape('LineH', {
name: 'MAIN',
strokeWidth: 0, // don't draw the actual line
height: TimelineHeight, // width will be set in "DocumentBoundsChanged" listener
background: 'lightgray'
}),
new go.Shape('LineV', {
name: 'TICKS',
interval: 7, // once per week
alignmentFocus: new go.Spot(0.5, 0, 0, -TimelineHeight / 2), // tick marks cross over the timeline itself
stroke: 'lightgray',
strokeWidth: 0.5
}),
new go.TextBlock({
alignmentFocus: go.Spot.Left,
interval: 7, // once per week
graduatedFunction: valueToText,
graduatedSkip: (val, tb) => val > (tb.panel ? tb.panel.graduatedMax : 0) - GridCellWidth * 7 // don't show last label
})
);
myGantt.add(myTimeline);
const myGrid = new go.Part('Grid', { // the grid of horizontal lines
layerName: 'Grid',
pickable: false,
position: new go.Point(0, 0),
gridCellSize: new go.Size(3000, GridCellHeight)
})
.add(
new go.Shape('LineH', { strokeWidth: 0.5 })
);
myGantt.add(myGrid);
const myHighlightDay = new go.Part({ // the vertical highlighter covering the day where the mouse is
layerName: 'Grid',
visible: false,
pickable: false,
background: 'rgba(255,0,0,0.2)',
position: new go.Point(0, 0),
width: GridCellWidth,
height: GridCellHeight
});
myGantt.add(myHighlightDay);
const myHighlightTask = new go.Part({ // the horizontal highlighter covering the current task
layerName: 'Grid',
visible: false,
pickable: false,
background: 'rgba(0,0,255,0.2)',
position: new go.Point(0, 0),
width: GridCellWidth,
height: GridCellHeight
});
myGantt.add(myHighlightTask);
myGantt.nodeTemplate = new go.Node('Spot', {
selectionAdorned: false,
selectionChanged: (n) => {
if (!n) return;
const node = n as go.Node;
node.diagram?.commit((diag) => {
const shape = node.findObject('SHAPE') as go.Shape;
if (shape) shape.fill = node.isSelected ? 'dodgerblue' : (node.data && node.data.color) || 'gray';
}, null);
},
minLocation: new go.Point(0, NaN),
maxLocation: new go.Point(Infinity, NaN),
toolTip: go.GraphObject.build('ToolTip')
.add(
new go.Panel('Table', { defaultAlignment: go.Spot.Left })
.addColumnDefinition(1, { separatorPadding: 3 })
.add(
new go.TextBlock({ row: 0, column: 0, columnSpan: 9, font: 'bold 12pt sans-serif' })
.bind('text'),
new go.TextBlock('start:', { row: 1, column: 0 }),
new go.TextBlock({ row: 1, column: 1 })
.bind('text', 'start', (d) => 'day ' + convertUnitsToDays(d).toFixed(0)),
new go.TextBlock('length:', { row: 2, column: 0 }),
new go.TextBlock({ row: 2, column: 1 })
.bind('text', 'duration', (d) => convertUnitsToDays(d).toFixed(0) + ' days')
)
),
resizable: true,
resizeObjectName: 'SHAPE',
resizeAdornmentTemplate: new go.Adornment('Spot')
.add(
new go.Placeholder(),
new go.Shape('Diamond', {
alignment: go.Spot.Right,
width: 8,
height: 8,
strokeWidth: 0,
fill: 'fuchsia',
cursor: 'e-resize'
})
),
mouseOver: (e) => { if (myGantt.mouseOver) myGantt.mouseOver(e); },
...standardContextMenus()
})
.bindTwoWay('position', 'start', convertStartToPosition, convertPositionToStart)
.bindObject('resizable', 'isTreeLeaf')
.bindTwoWay('isTreeExpanded')
.add(
new go.Shape({
name: 'SHAPE',
height: 18,
margin: new go.Margin(1, 0),
strokeWidth: 0,
fill: 'gray'
})
.bind('fill', 'color')
.bindTwoWay('width', 'duration', convertDurationToW, convertWToDuration)
.bindObject('figure', 'isTreeLeaf', (leaf) => (leaf ? 'Rectangle' : 'RangeBar')),
// "RangeBar" is defined above as a custom figure
new go.TextBlock({
font: '8pt sans-serif',
alignment: go.Spot.TopLeft,
alignmentFocus: new go.Spot(0, 0, 0, -2)
})
.bind('text')
.bind('stroke', 'color', (c) => (go.Brush.isDark(c) ? '#DDDDDD' : '#333333'))
);
myGantt.linkTemplate = new go.Link({ visible: false });
myGantt.linkTemplateMap.add('Dep',
new go.Link({
routing: go.Routing.Orthogonal,
isTreeLink: false,
isLayoutPositioned: false,
fromSpot: new go.Spot(0.999999, 1),
toSpot: new go.Spot(0.000001, 0)
})
.add(
new go.Shape({ stroke: 'brown', strokeWidth: 3 }),
new go.Shape({ toArrow: 'Standard', fill: 'brown', strokeWidth: 0, scale: 0.75 })
)
);
// The Model that is shared by both Diagrams
const myModel = new go.GraphLinksModel({
modelData: {
origin: 1531540800000 // new Date(2018, 6, 14);
},
nodeDataArray: [
{ key: 0, text: 'Project X' },
{ key: 1, text: 'Task 1', color: 'darkgreen' },
{ key: 11, text: 'Task 1.1', color: 'green', duration: convertDaysToUnits(7) },
{ key: 12, text: 'Task 1.2', color: 'green' },
{ key: 121, text: 'Task 1.2.1', color: 'lightgreen', duration: convertDaysToUnits(3) },
{ key: 122, text: 'Task 1.2.2', color: 'lightgreen', duration: convertDaysToUnits(5) },
{ key: 123, text: 'Task 1.2.3', color: 'lightgreen', duration: convertDaysToUnits(4) },
{ key: 2, text: 'Task 2', color: 'darkblue' },
{ key: 21, text: 'Task 2.1', color: 'blue', duration: convertDaysToUnits(15), start: convertDaysToUnits(10) },
{ key: 22, text: 'Task 2.2', color: 'goldenrod' },
{ key: 221, text: 'Task 2.2.1', color: 'yellow', duration: convertDaysToUnits(8) },
{ key: 222, text: 'Task 2.2.2', color: 'yellow', duration: convertDaysToUnits(6) },
{ key: 23, text: 'Task 2.3', color: 'darkorange' },
{ key: 231, text: 'Task 2.3.1', color: 'orange', duration: convertDaysToUnits(11) },
{ key: 3, text: 'Task 3', color: 'maroon' },
{ key: 31, text: 'Task 3.1', color: 'brown', duration: convertDaysToUnits(10) },
{ key: 32, text: 'Task 3.2', color: 'brown' },
{ key: 321, text: 'Task 3.2.1', color: 'lightsalmon', duration: convertDaysToUnits(8) },
{ key: 322, text: 'Task 3.2.2', color: 'lightsalmon', duration: convertDaysToUnits(3) },
{ key: 323, text: 'Task 3.2.3', color: 'lightsalmon', duration: convertDaysToUnits(7) },
{ key: 324, text: 'Task 3.2.4', color: 'lightsalmon', duration: convertDaysToUnits(5), start: convertDaysToUnits(71) },
{ key: 325, text: 'Task 3.2.5', color: 'lightsalmon', duration: convertDaysToUnits(4) },
{ key: 326, text: 'Task 3.2.6', color: 'lightsalmon', duration: convertDaysToUnits(5) }
],
linkDataArray: [
{ 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' }
]
});
StartDate = new Date(myModel.modelData.origin);
// share model
myTasks.model = myModel;
myGantt.model = myModel;
myModel.undoManager.isEnabled = true;
// sync viewports
var changingView = false; // for preventing recursive updates
myTasks.addDiagramListener('ViewportBoundsChanged', (e) => {
if (changingView) return;
changingView = true;
myTasksHeader.position = new go.Point(myTasksHeader.position.x, myTasks.viewportBounds.position.y);
myGantt.scale = myTasks.scale;
myGantt.position = new go.Point(myGantt.position.x, myTasks.position.y);
myTimeline.position = new go.Point(myTimeline.position.x, myGantt.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);
myTasksHeader.position = new go.Point(myTasksHeader.position.x, myTasks.viewportBounds.position.y);
myGantt.position = new go.Point(myGantt.position.x, myTasks.position.y); // don't scroll more if myTasks can't scroll more
myTimeline.position = new go.Point(myTimeline.position.x, myGantt.viewportBounds.position.y);
changingView = false;
});
document.getElementById("widthSlider")?.addEventListener("input", rescale);
// change horizontal scale
function rescale() {
const slider = document.getElementById('widthSlider') as HTMLInputElement;
if (!slider) return;
const val = parseFloat(slider.value);
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.layout as GanttLayout).cellHeight = GridCellHeight;
diag.layoutDiagram(true);
myTimeline.graduatedTickUnit = GridCellWidth;
diag.padding = new go.Margin(TimelineHeight + 4, GridCellWidth * 7, GridCellHeight, 0);
myTasks.padding = new go.Margin(TimelineHeight + 4, 0, GridCellHeight, 0);
}, null); // skipsUndoManager
}
// just for debugging:
myModel.addChangedListener((e) => {
if (e.isTransactionFinished && e.model) {
// show the model data in the page's TextArea
const saved = document.getElementById('mySavedModel');
if (saved) saved.textContent = e.model.toJson();
}
});
FYI, here’s the tsconfig.json
file that I use:
{
"compilerOptions": {
"target": "ES2020",
"strict": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"paths": {
"gojs": [
"../latest/release/go-module.d.ts"
]
}
}
}
The two remaining compilation errors are due to a misdeclaration of the Panel.addRowDefinition and addColumnDefinition methods, which should return this
instead of Panel
. We’ll fix that in 3.0.19, which should come out next week. As usual, the compiled code runs correctly in either case.