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.