Adapting go-js Gantt chart sample to angular

I’d like to re-use the Gantt chart sample : Interactive Gantt Chart With Collapsible Tree of Tasks, Scrolling, and Highlighting | GoJS Diagramming Library

and adapt it to make it work with gojs-angular. It seems there are a ton of changes to make and I’m not sure where to start. Do you have by any chance a similar sample with angular ? or at least some guidance on what to change ?

Just pasting the sample code to a new angular app, I get plenty of compilation errors like :

Property ‘adornedPart’ does not exist on type ‘Part’
Property ‘add’ does not exist on type ‘GraphObject’.
Property ‘addLinkData’ does not exist on type ‘Model’.
Cannot find name ‘myTasks’.

I have already done a process component with go-js but it was very much similar to the provided go-js-angular-sample, with a diagram and a palette.

Those are TypeScript errors that are easy to fix:

  • cast it to Adornment
  • cast it to Panel
  • cast it to GraphLinksModel

myTasks is a Diagram that holds the tree on the left-hand side of the sample, including the “Name”/“Start”/“Dur.” column headers. myGantt is a Diagram holding the Gantt chart on the right-hand side of the sample. Presumably your Angular component would include both Diagrams.

If you are using an immutable data architecture, I suggest that you use the gojs-angular components that are in the GitHub repository GitHub - NorthwoodsSoftware/gojs-angular: A set of Angular components to manage GoJS Diagrams, Palettes, and Overviews.

If you are not using immutable data, then you don’t need to use the gojs-angular package, but you might find bits of its implementation useful in defining your own Gantt component, which would have those two Diagrams in them.

I’m making slow progress. In fact there are many other errors and i’m trying to put a sample together.
Currently I’m stuck on this :


  const $ = go.GraphObject.make;

  myGantt.nodeTemplate = $(go.Node, 'Spot', {
    selectionAdorned: false,
    selectionChanged: (node) => {
      node.diagram.commit((diag) => {
        node.findObject('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({ row: 1, column: 0 }, 'start:'),
            new go.TextBlock({ row: 1, column: 1 })
              .bind('text', 'start', (d) => 'day ' + convertUnitsToDays(d).toFixed(0)),
            new go.TextBlock({ row: 2, column: 0 }, 'length:'),
            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, node) => myGantt.mouseOver(e),
    ...standardContextMenus()
  })

which gives the error:

No overload matches this call.
  The last overload gave the following error.
    Argument of type '{ contextMenu: Panel; selectionAdorned: false; selectionChanged: (node: Part) => void; minLocation: Point; maxLocation: Point; toolTip: any; resizable: true; resizeObjectName: string; resizeAdornmentTemplate: Adornment; mouseOver: (e: InputEvent, node: GraphObject) => void; }' is not assignable to parameter of type 'string | number | GraphObject | AnimationTrigger | Binding | RowColumnDefinition | PanelLayout | HTMLDivElement | (Partial<...> & { ...; }) | (string | ... 6 more ... | (Partial<...> & { ...; }))[]'.
      Type '{ contextMenu: Panel; selectionAdorned: false; selectionChanged: (node: Part) => void; minLocation: Point; maxLocation: Point; toolTip: any; resizable: true; resizeObjectName: string; resizeAdornmentTemplate: Adornment; mouseOver: (e: InputEvent, node: GraphObject) => void; }' is not assignable to type 'string'.

I tried to use new Node instead of $, but makes no difference

Ah, I have identified the problem comes from this line:

...standardContextMenus()

If I write this instead, it compiles (no idea if it will work though):
standardContextMenus

However I also get the following errors :

Property 'fill' does not exist on type 'GraphObject'.
Property 'add' does not exist on type 'GraphObject'.
Argument of type '{ row: number; column: number; }' is not assignable to parameter of type 'string'.  (that's on the TextBlock lines)


The first error, I fixed by casting to go.Shape :

(node.findObject('SHAPE') as go.Shape).fill 

The third error I fixed by inverting the string and the object in the constructor:

 new go.TextBlock( 'start:', { row: 1, column: 0 }),

But the second error, I don’t know how to fix

Did you not start with the latest version of samples/gantt.html? All of the samples in v3.0 have been rewritten not to use GraphObject.make (or $) for better support of edit-time and compile-time tools.

Yes, when converting to TypeScript, all of the casts should be narrowing. The name or signature of the methods/properties should tell you what type to expect.

I started with the version that can be found here : GoJS/samples/gantt.html at master · NorthwoodsSoftware/GoJS · GitHub
and here:
Interactive Gantt Chart With Collapsible Tree of Tasks, Scrolling, and Highlighting | GoJS Diagramming Library

I didn’t see any other versions…
EDIT
ah yes sorry my bad, the GraphObject.make was introduced by me because I thought the compilation errors were related to the new. But it makes no difference using new or $ or make

I created a sample and went as far as I could. These are the remaining errors. I have attached the sample. I’d appreciate if the team could take a look.

The first error is going to be tricky. ‘myTasks’ (which you will see I renamed to _myTasksDiagram) is inside the component. So how toi access it from the GanttLayout class ?

Also I’m not sure how to handle model-related code using angular. Should I create a state as shown in the gojs-angular sample ? But if so, what part of that code should go in the state exactly ?

 TS2304: Cannot find name 'myTasks'. [plugin angular-compiler]

    src/app/gantt-chart/gantt-chart.component.ts:26:23:
      26 │       const tasknode = myTasks.findNodeForData(node.data);
         ╵                        ~~~~~~~


X [ERROR] TS1005: '{' expected. [plugin angular-compiler]

    src/app/gantt-chart/gantt-chart.component.ts:416:25:
      416 │     standardContextMenus()
          ╵                          ^


X [ERROR] TS2353: Object literal may only specify known properties, and 'commitNodes' does not exist in type 'Partial<TreeLayout>'. [plugin angular-compiler]

    src/app/gantt-chart/gantt-chart.component.ts:486:6:
      486 │       commitNodes: function () {
          ╵       ~~~~~~~~~~~


X [ERROR] TS2445: Property 'commitNodes' is protected and only accessible within class 'TreeLayout' and its subclasses. [plugin angular-compiler]

    src/app/gantt-chart/gantt-chart.component.ts:488:32:
      488 │         go.TreeLayout.prototype.commitNodes.call(this);
          ╵                                 ~~~~~~~~~~~


X [ERROR] TS2740: Type 'Panel' is missing the following properties from type 'Part': updateRelationshipsFromData, key, adornments, findAdornment, and 74 more. [plugin angular-compiler]

    src/app/gantt-chart/gantt-chart.component.ts:510:1:
      510 │  this._myTasksDiagram.nodeTemplate = $(go.Node, 'Table', {
          ╵  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Can you change the GanttLayout constructor to take a reference to the tasks Diagram? Or to the tasks component, from which you can get its Diagram?

Yes I can pass the tasks component and get its diagram.

Did you take a look at the zip I provided ? There are a few compilation errors left that I don’t know how to fix, and most importantly the angular part that I have no idea how to handle give the model of the gantt sample.

Yes, typically the diagram model state is part of the app’s state, and is passed in to the diagram component, via your “Gantt” component. Presumably you have also seen the code that needs to update that state when the component/diagrams have modified their model state.

Yes, I saw it, although its confusing what to add the state for the gantt sample. In any case I 'm stuck on compilation errors for now:

X [ERROR] TS2353: Object literal may only specify known properties, and 'standardContextMenus' does not exist in type 'Partial<Node>'. [plugin angular-compiler]

    src/app/gantt-chart/gantt-chart.component.ts:417:4:
      417 │     standardContextMenus()
          ╵     ~~~~~~~~~~~~~~~~~~~~


X [ERROR] TS1005: '{' expected. [plugin angular-compiler]

    src/app/gantt-chart/gantt-chart.component.ts:417:25:
      417 │     standardContextMenus()
          ╵                          ^


X [ERROR] TS2353: Object literal may only specify known properties, and 'commitNodes' does not exist in type 'Partial<TreeLayout>'. [plugin angular-compiler]

    src/app/gantt-chart/gantt-chart.component.ts:486:6:
      486 │       commitNodes: function () {
          ╵       ~~~~~~~~~~~


X [ERROR] TS2445: Property 'commitNodes' is protected and only accessible within class 'TreeLayout' and its subclasses. [plugin angular-compiler]  

    src/app/gantt-chart/gantt-chart.component.ts:488:32:
      488 │         go.TreeLayout.prototype.commitNodes.call(this);
          ╵                                 ~~~~~~~~~~~


X [ERROR] TS2740: Type 'Panel' is missing the following properties from type 'Part': updateRelationshipsFromData, key, adornments, findAdornment, and 74 more. [plugin angular-compiler]

    src/app/gantt-chart/gantt-chart.component.ts:510:1:
      510 │  this._myTasksDiagram.nodeTemplate = new go.Node('Table', {
          ╵  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~


X [ERROR] TS2353: Object literal may only specify known properties, and 'standardContextMenus' does not exist in type 'Partial<Node>'. [plugin angular-compiler]

    src/app/gantt-chart/gantt-chart.component.ts:529:2:
      529 │   standardContextMenus
          ╵   ~~~~~~~~~~~~~~~~~~~~

how do i fix those ?

I haven’t tried any of these, but I suggest:

  • setting contextMenu: standardContextMenus(), and change the definition of that function to just return the value it is trying to assign to GraphObject.contextMenu
  • (and the same for the later use of standardContextMenus without the following parentheses, to make it into a function call)
  • instead of overriding the TreeLayout.commitNodes method by trying to set it on an instance of TreeLayout, JavaScript-style, actually define a subclass of TreeLayout and override the method, and create an instance of your subclass
  • I can’t explain the complaint about Node not having the same interface as Part. Maybe the problem will go away once you’ve fixed the other errors.
  • contextMenu

calling like this :

  contextMenu: standardContextMenus

and changed function to this :


function standardContextMenus() {
  return (go.GraphObject.build('ContextMenu') as go.Panel).add(
      (
        go.GraphObject.build('ContextMenuButton', {
          click: (e, button) => {
            const task = (button.part as go.Adornment).adornedPart;
          }
        }) as go.Panel
      ).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));
            });
          }
        }) as go.Panel
      ).add(new go.TextBlock('New Task'))
    )
}
+

just gives me another error:

Type ‘() => Panel’ is not assignable to type ‘Adornment | HTMLInfo’.

I don’t understand your second bullet point.

Please take a look at the sample.

I have to say it is frustrating that gojs advertise fancy charts on the sample page and none of them are actually available in typescript (the homepage of your website says Build diagrams for the web in JavaScript and TypeScript).
From managers point-of-view it should be a simple copy-paste to make it work in our application, when oviously it is not. And I haven’t even started the angular part yet. Please let us know how to fix the compilation errors at least to make it compile with typescript.
sample: gojs-gantt.zip

The library is implemented in TypeScript, and the extensions are all implemented in TypeScript, as you can see in the extensionsJSM directory.

We used to have a few samples implemented in TypeScript, in the samplesTS directory, but it was incomplete and appeared to get very little use. We can work on a translation of the Gantt sample for you.

yes I know the library is implemented in typescript, but the samples are not, that’s the problem.

I would very much appreciate if the sample could be translated to typescript. However did you look at my sample? it’s almost done, it’s just a matter of fixing 4 or 5 errors left.
but yes, if you can provide it , please.

also, I think the gojs-angular lib deserves more samples too. it only shows one use case with the process.

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.