UNPKG

create-gojs-kit

Version:

A CLI for downloading GoJS samples, extensions, and docs

825 lines (758 loc) 34 kB
<!DOCTYPE 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." /> <meta itemprop="description" content="A Gantt chart that supports zooming into the timeline." /> <meta property="og:description" content="A Gantt chart that supports zooming into the timeline." /> <meta name="twitter:description" content="A Gantt chart that supports zooming into the timeline." /> <link rel="preconnect" href="https://rsms.me/"> <link rel="stylesheet" href="../assets/css/style.css"> <!-- Copyright 1998-2025 by Northwoods Software Corporation. --> <meta itemprop="name" content="Interactive Gantt Chart With Collapsible Tree of Tasks, Scrolling, and Highlighting" /> <meta property="og:title" content="Interactive Gantt Chart With Collapsible Tree of Tasks, Scrolling, and Highlighting" /> <meta name="twitter:title" content="Interactive Gantt Chart With Collapsible Tree of Tasks, Scrolling, and Highlighting" /> <meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/gantt.png" /> <meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/gantt.png" /> <meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/gantt.png" /> <meta property="og:url" content="https://gojs.net/latest/samples/gantt.html" /> <meta property="twitter:url" content="https://gojs.net/latest/samples/gantt.html" /> <meta name="twitter:card" content="summary_large_image" /> <meta property="og:type" content="website" /> <meta property="twitter:domain" content="gojs.net" /> <title> Interactive Gantt Chart With Collapsible Tree of Tasks, Scrolling, and Highlighting | GoJS Diagramming Library </title> </head> <body> <!-- This top nav is not part of the sample code --> <nav id="navTop" class=" w-full h-[var(--topnav-h)] z-30 bg-white border-b border-b-gray-200"> <div class="max-w-screen-xl mx-auto flex flex-wrap items-start justify-between px-4"> <a class="text-white bg-nwoods-primary font-bold !leading-[calc(var(--topnav-h)_-_1px)] my-0 px-2 text-4xl lg:text-5xl logo" href="../"> GoJS </a> <div class="relative"> <button id="topnavButton" class="h-[calc(var(--topnav-h)_-_1px)] px-2 m-0 text-gray-900 bg-inherit shadow-none md:hidden hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation"> <svg class="h-7 w-7 block" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"> <path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> </button> <div id="topnavList" class="hidden md:block"> <div class="absolute right-0 z-30 flex flex-col items-end rounded border border-gray-200 p-4 pl-12 shadow bg-white text-gray-900 font-semibold md:flex-row md:space-x-4 md:items-start md:border-0 md:p-0 md:shadow-none md:bg-inherit"> <a href="../learn/">Learn</a> <a href="../samples/">Samples</a> <a href="../intro/">Intro</a> <a href="../api/">API</a> <a href="../download.html">Download</a> <a href="https://forum.nwoods.com/c/gojs/11" target="_blank" rel="noopener">Forum</a> <a id="tc" href="https://nwoods.com/contact.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a> <a id="tb" href="https://nwoods.com/sales/index.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a> </div> </div> </div> </div> </nav> <script> window.addEventListener("DOMContentLoaded", function () { // topnav var topButton = document.getElementById("topnavButton"); var topnavList = document.getElementById("topnavList"); if (topButton && topnavList) { topButton.addEventListener("click", function (e) { topnavList .classList .toggle("hidden"); e.stopPropagation(); }); document.addEventListener("click", function (e) { // if the clicked element isn't the list, close the list if (!topnavList.classList.contains("hidden") && !e.target.closest("#topnavList")) { topButton.click(); } }); // set active <a> element var url = window .location .href .toLowerCase(); var aTags = topnavList.getElementsByTagName('a'); for (var i = 0; i < aTags.length; i++) { var lowerhref = aTags[i] .href .toLowerCase(); if (lowerhref.endsWith('.html')) lowerhref = lowerhref.slice(0, -5); if (url.startsWith(lowerhref)) { aTags[i] .classList .add('active'); break; } } } }); </script> <div class="flex flex-col prose"> <div class="w-full max-w-screen-xl mx-auto"> <!-- * * * * * * * * * * * * * --> <!-- Start of GoJS sample code --> <script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></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: 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" 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, automatically updated as the diagram is modified:</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(init) { super(); this.cellHeight = GridCellHeight; if (init) Object.assign(this, init); } doLayout(coll) { coll = this.collectParts(coll); const diagram = this.diagram; diagram.startTransaction('Gantt Layout'); const bars = []; this.assignTimes(diagram, bars); this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin); let y = this.arrangementOrigin.y; bars.forEach(node => { const tasknode = myTasks.findNodeForData(node.data); 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, bars) { const roots = diagram.findTreeRoots(); roots.each(root => this.walkTree(root, 0, bars)); } walkTree(node, start, bars) { bars.push(node); 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 const GridCellHeight = 20; // document units; cannot be changed dynamically let GridCellWidth = 12; // document units per day; this can be modified -- see rescale() const 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) { return n; } function convertUnitsToDays(n) { return n; } function convertStartToX(start) { return convertUnitsToDays(start) * GridCellWidth; } function convertXToStart(x, node) { return convertDaysToUnits(x / GridCellWidth); } // these four functions are used in TwoWay Bindings on the task/node template function convertDurationToW(duration) { return convertUnitsToDays(duration) * GridCellWidth; } function convertWToDuration(w) { return convertDaysToUnits(w / GridCellWidth); } function convertStartToPosition(start, node) { return new go.Point(convertStartToX(start), node.position.y || 0); } function convertPositionToStart(pos) { return convertXToStart(pos.x); } let StartDate = new Date(); // set from Model.modelData.origin function valueToText(n) { // 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) { // 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 standardContextMenu(obj) { obj.contextMenu = go.GraphObject.build('ContextMenu') .add( go.GraphObject.build('ContextMenuButton', { click: (e, button) => { const task = button.part.adornedPart; // not yet implemented... } }) .add(new go.TextBlock('Details...')), go.GraphObject.build('ContextMenuButton', { click: (e, button) => { const task = button.part.adornedPart; e.diagram.model.commit(m => { const newdata = { key: undefined, text: 'New Task', color: task.data.color, duration: convertDaysToUnits(5) }; m.addNodeData(newdata); m.addLinkData({ from: task.key, to: newdata.key }); e.diagram.select(e.diagram.findNodeForData(newdata)); }); } }) .add(new go.TextBlock('New Task')) ) } // 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 go.TreeLayout({ 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), // 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 commitNodes: function () { // method override must be function, not => go.TreeLayout.prototype.commitNodes.call(this); updateNodeWidths(400); } }), mouseLeave: (e, node) => (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 = []; e.diagram.selection.each(part => { if (part instanceof go.Node) tasks.push(myGantt.findNodeForData(part.data)); }); 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.isSelected ? 'dodgerblue' : 'transparent'; myHighlightTask.visible = false; }, doubleClick: (e, node) => { // scroll myGantt so the corresponding bar is visible const bar = myGantt.findNodeForData(node.data); if (bar) myGantt.commandHandler.scrollToPart(bar); } }) .apply(standardContextMenu) .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) { 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(1) .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 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); myTimeline.findObject('MAIN').width = myTimeline.graduatedMax; myTimeline.findObject('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 = []; e.diagram.selection.each(part => { if (part instanceof go.Node) bars.push(myTasks.findNodeForData(part.data)); }); 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.graduatedMax - 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: 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) }) .apply(standardContextMenu) .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), toShortLength: 3 }) .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; }); // change horizontal scale function rescale() { const val = parseFloat(document.getElementById('widthSlider').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.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) { // show the model data in the page's TextArea document.getElementById('mySavedModel').textContent = e.model.toJson(); } }); </script> </div> <!-- * * * * * * * * * * * * * --> <!-- End of GoJS sample code --> </div> <div id="allTagDescriptions" class="p-4 w-full max-w-screen-xl mx-auto"> <hr/> <h3 class="text-xl">GoJS Features in this sample</h3> <!-- blacklist tags that do not correspond to a specific GoJS feature --> <h4>Grid Patterns</h4> <p> <b>GoJS</b> provides functionality to display a grid of lines drawn at regular intervals. Grid Panels can also force dragged parts to be aligned on grid points, and resize parts to be multiples of the grid cell size. </p> <p> Grids are implemented using a type of <a href="../api/symbols/Panel.html" target="api">Panel</a>, <a href="../api/symbols/PanelLayout.html#Grid" target="api">Panel.Grid</a>. Grid Panels, like most other types of Panels, can be used within <a href="../api/symbols/Node.html" target="api">Node</a>s or any other kind of <a href="../api/symbols/Part.html" target="api">Part</a>. However when they are used as the <a href="../api/symbols/Diagram.html#grid" target="api">Diagram.grid</a>, they are effectively infinite in extent. </p> <p> More information can be found in the <a href="../intro/grids.html">GoJS Intro</a>. </p> <p> <a href="../samples/index.html#grid">Related samples</a> </p> <hr> </div> </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>