UNPKG

gojs

Version:

Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams

696 lines (645 loc) 30 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."/> <link rel="stylesheet" href="../assets/css/style.css"/> <!-- Copyright 1998-2023 by Northwoods Software Corporation. --> <title>Gantt chart</title> </head> <body> <!-- This top nav is not part of the sample code --> <nav id="navTop" class="w-full z-30 top-0 text-white bg-nwoods-primary"> <div class="w-full container max-w-screen-lg mx-auto flex flex-wrap sm:flex-nowrap items-center justify-between mt-0 py-2"> <div class="md:pl-4"> <a class="text-white hover:text-white no-underline hover:no-underline font-bold text-2xl lg:text-4xl rounded-lg hover:bg-nwoods-secondary " href="../"> <h1 class="my-0 p-1 ">GoJS</h1> </a> </div> <button id="topnavButton" class="rounded-lg sm:hidden focus:outline-none focus:ring" aria-label="Navigation"> <svg fill="currentColor" viewBox="0 0 20 20" class="w-6 h-6"> <path id="topnavOpen" fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM9 15a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1z" clip-rule="evenodd"></path> <path id="topnavClosed" class="hidden" fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path> </svg> </button> <div id="topnavList" class="hidden sm:block items-center w-auto mt-0 text-white p-0 z-20"> <ul class="list-reset list-none font-semibold flex justify-end flex-wrap sm:flex-nowrap items-center px-0 pb-0"> <li class="p-1 sm:p-0"><a class="topnav-link" href="../learn/">Learn</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../samples/">Samples</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../intro/">Intro</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../api/">API</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/products/register.html">Register</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="../download.html">Download</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://forum.nwoods.com/c/gojs/11">Forum</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/contact.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/contact.html', 'contact');">Contact</a></li> <li class="p-1 sm:p-0"><a class="topnav-link" href="https://www.nwoods.com/sales/index.html" target="_blank" rel="noopener" onclick="getOutboundLink('https://www.nwoods.com/sales/index.html', 'buy');">Buy</a></li> </ul> </div> </div> <hr class="border-b border-gray-600 opacity-50 my-0 py-0" /> </nav> <div class="md:flex flex-col md:flex-row md:min-h-screen w-full max-w-screen-xl mx-auto"> <div id="navSide" class="flex flex-col w-full md:w-48 text-gray-700 bg-white flex-shrink-0"></div> <!-- * * * * * * * * * * * * * --> <!-- Start of GoJS sample code --> <script src="../release/go.js"></script> <div id="allSampleContent" class="p-4 w-full"> <div id="sample"> <div style="width: 100%; display: flex; justify-content: space-between; border: solid 1px black"> <div id="myTasksDiv" style="width: 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() { super(); this.cellHeight = GridCellHeight; } 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 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) { 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); } var 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.PathSegment.Line, w, 0)) .add(new go.PathSegment(go.PathSegment.Line, w, h)) .add(new go.PathSegment(go.PathSegment.Line, w-b, h-d)) .add(new go.PathSegment(go.PathSegment.Line, b, h-d)) .add(new go.PathSegment(go.PathSegment.Line, 0, h).close())); }); const $ = go.GraphObject.make; function standardContextMenus() { return { contextMenu: $("ContextMenu", $("ContextMenuButton", $(go.TextBlock, "Details..."), { click: (e, button) => { const task = button.part.adornedPart; console.log("show HTML panel with details about the task " + task.text); } } ), $("ContextMenuButton", $(go.TextBlock, "New Task"), { 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)); }); } } ) ) }; } // 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: $(go.TreeLayout, { alignment: go.TreeLayout.AlignmentStart, compaction: go.TreeLayout.CompactionNone, layerSpacing: 16, layerSpacingParentOverlap: 1, nodeIndentPastParent: 1, nodeSpacing: 0, portSpot: go.Spot.Bottom, childPortSpot: go.Spot.Left, 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 = $(go.Node, "Table", { columnSizing: go.RowColumnDefinition.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); } }, new go.Binding("background", "isSelected", s => s ? "dodgerblue" : "transparent").ofObject(), new go.Binding("isTreeExpanded").makeTwoWay(), $(go.RowColumnDefinition, { column: 0, width: 14 }), $("TreeExpanderButton", { column: 0, portId: "", scale: 0.85 }), $(go.RowColumnDefinition, { column: 1, alignment: go.Spot.Left }), $(go.TextBlock, { column: 1, editable: true }, new go.Binding("text").makeTwoWay()), // additional columns $(go.RowColumnDefinition, { column: 2, width: 40, alignment: go.Spot.Right, separatorPadding: new go.Margin(0, 4), separatorStroke: "gray" }), $(go.TextBlock, { column: 2 }, new go.Binding("text", "start", s => s.toFixed(2))), $(go.RowColumnDefinition, { column: 3, width: 40, alignment: go.Spot.Right, separatorPadding: new go.Margin(0, 4), separatorStroke: "gray" }), $(go.TextBlock, { column: 3 }, new go.Binding("text", "duration", d => d.toFixed(2))), standardContextMenus() ); 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 = // the timeline at the top of the myTasks viewport $(go.Part, "Table", { layerName: "Adornment", pickable: false, position: new go.Point(-26, 0), // position will be set in "ViewportBoundsChanged" listener columnSizing: go.RowColumnDefinition.None, selectionAdorned: false, height: GridCellHeight, background: "lightgray" }, $(go.RowColumnDefinition, { column: 0, width: 14 }), $(go.RowColumnDefinition, { column: 1 }), $(go.TextBlock, "Name", { column: 1 }), // additional columns $(go.RowColumnDefinition, { column: 2, width: 40, alignment: go.Spot.Right, separatorPadding: new go.Margin(0, 4), separatorStroke: "gray" }), $(go.TextBlock, "Start", { column: 2 }), $(go.RowColumnDefinition, { column: 3, width: 40, alignment: go.Spot.Right, separatorPadding: new go.Margin(0, 4), separatorStroke: "gray" }), $(go.TextBlock, "Dur.", { column: 3 }), ); myTasks.add(myTasksHeader); myTasks.linkTemplate = $(go.Link, { selectable: false, routing: go.Link.Orthogonal, fromEndSegmentLength: 1, toEndSegmentLength: 1 }, $(go.Shape) ); myTasks.linkTemplateMap.add("Dep", $(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: $(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 = // the timeline at the top of the myGantt viewport $(go.Part, "Graduated", { 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 }, $(go.Shape, "LineH", { name: "MAIN", strokeWidth: 0, // don't draw the actual line height: TimelineHeight, // width will be set in "DocumentBoundsChanged" listener background: "lightgray" }), $(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 }), $(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 = // the grid of horizontal lines $(go.Part, "Grid", { layerName: "Grid", pickable: false, position: new go.Point(0, 0), gridCellSize: new go.Size(3000, GridCellHeight) }, $(go.Shape, "LineH", { strokeWidth: 0.5 }) ); myGantt.add(myGrid); const myHighlightDay = // the vertical highlighter covering the day where the mouse is $(go.Part, { 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 = // the horizontal highlighter covering the current task $(go.Part, { 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 = $(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: $("ToolTip", $(go.Panel, "Table", { defaultAlignment: go.Spot.Left }, $(go.RowColumnDefinition, { column: 1, separatorPadding: 3 }), $(go.TextBlock, { row: 0, column: 0, columnSpan: 9, font: "bold 12pt sans-serif" }, new go.Binding("text")), $(go.TextBlock, { row: 1, column: 0 }, "start:"), $(go.TextBlock, { row: 1, column: 1 }, new go.Binding("text", "start", d => "day " + convertUnitsToDays(d).toFixed(0))), $(go.TextBlock, { row: 2, column: 0 }, "length:"), $(go.TextBlock, { row: 2, column: 1 }, new go.Binding("text", "duration", d => convertUnitsToDays(d).toFixed(0) + " days")), ) ), resizable: true, resizeObjectName: "SHAPE", resizeAdornmentTemplate: $(go.Adornment, "Spot", $(go.Placeholder), $(go.Shape, "Diamond", { alignment: go.Spot.Right, width: 8, height: 8, strokeWidth: 0, fill: "fuchsia", cursor: 'e-resize' } ) ), mouseOver: (e, node) => myGantt.mouseOver(e) }, standardContextMenus(), new go.Binding("position", "start", convertStartToPosition).makeTwoWay(convertPositionToStart), new go.Binding("resizable", "isTreeLeaf").ofObject(), new go.Binding("isTreeExpanded").makeTwoWay(), $(go.Shape, { name: "SHAPE", height: 18, margin: new go.Margin(1, 0), strokeWidth: 0, fill: "gray" }, new go.Binding("fill", "color"), new go.Binding("width", "duration", convertDurationToW).makeTwoWay(convertWToDuration), new go.Binding("figure", "isTreeLeaf", leaf => leaf ? "Rectangle" : "RangeBar").ofObject()), // "RangeBar" is defined above as a custom figure $(go.TextBlock, { font: "8pt sans-serif", alignment: go.Spot.TopLeft, alignmentFocus: new go.Spot(0, 0, 0, -2) }, new go.Binding("text"), new go.Binding("stroke", "color", c => go.Brush.isDark(c) ? "#DDDDDD" : "#333333")) ); myGantt.linkTemplate = $(go.Link, { visible: false }); myGantt.linkTemplateMap.add("Dep", $(go.Link, { routing: go.Link.Orthogonal, isTreeLink: false, isLayoutPositioned: false, fromSpot: new go.Spot(0.999999, 1), toSpot: new go.Spot(0.000001, 0) }, $(go.Shape, { stroke: "brown", strokeWidth: 3 }), $(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> </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>