UNPKG

gojs

Version:

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

849 lines (848 loc) 36.9 kB
<html> <head> <script async src="https://www.googletagmanager.com/gtag/js?id=UA-1506307-6"></script> <script> window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'UA-1506307-6'); </script> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>GoTimeline</title> <meta name="description" content="" /> <meta charset="UTF-8"> <script src="./go.js"></script> <script src="./goeditor-data-interaction.js"></script> <script src="./goeditor-setup.js"></script> <script src="./storage/gcs.js"></script> <script src="https://apis.google.com/js/api.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/dropbox.js/2.5.7/Dropbox-sdk.min.js"></script> <script src="https://js.live.net/v7.2/OneDrive.js"></script> <script type="text/javascript" src="https://www.dropbox.com/static/api/2/dropins.js" id="dropboxjs" data-app-key="3sm2ko6q7u1gbix"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/jquery-ui.min.js"></script> <script src="./DataInspector.js"></script> <link rel="stylesheet" type="text/css" href="./DataInspector.css" /> <link rel="stylesheet" type="text/css" href="./storage/GoCloudStorageUI.css" /> <link rel="stylesheet" type="text/css" href="./goeditor.css" /> <script> function init() { setupEditorApplication(); nodeGenerationMap = new go.Map(); categories = [""]; var diagramContextMenu = $(go.Adornment, "Vertical", $("ContextMenuButton", { click: function (e, obj) { createNode("Event") } }, $(go.TextBlock, "Add Event") ), $("ContextMenuButton", { click: function (e, obj) { createNode("Range") } }, $(go.TextBlock, "Add Range") ) ); myDiagram.layout = $(TimelineLayout); myDiagram.contextMenu = diagramContextMenu; myDiagram.isTreePathToChildren = false; myDiagram.initialContentAlignment = go.Spot.Center; myDiagram.animationManager.isEnabled = false; // when a drag begins, if an event node is being dragged, disable the diagram layout until the drag ends myDiagram.toolManager.draggingTool.doActivate = function () { var tool = this; go.DraggingTool.prototype.doActivate.call(tool); var draggedPartsIncludeEvent = false; tool.draggedParts.iterator.each(function (kvp){ var part = kvp.key; if (part instanceof go.Node && part.category == "Event" || part.category == "Range") { draggedPartsIncludeEvent = true; } }); if (draggedPartsIncludeEvent) { tool.diagram.layout.isOngoing = false; } } // when a drag is completed, if an event node was dragged, relayout diagram myDiagram.toolManager.draggingTool.doDeactivate = function () { var tool = this; var draggedPartsIncludeEvent = false; tool.draggedParts.iterator.each(function(kvp){ var part = kvp.key; if (part instanceof go.Node && part.category == "Event" || part.category == "Range") { draggedPartsIncludeEvent = true; var timeline = myDiagram.findNodeForKey("timeline"); if (part.location.y > timeline.location.y) { tool.diagram.model.set(part.data, "isPositionedBelow", true); } else { tool.diagram.model.set(part.data, "isPositionedBelow", false); } } }); if (draggedPartsIncludeEvent) { tool.diagram.layout.isOngoing = true; tool.diagram.layout.doLayout(tool.diagram.nodes); } go.DraggingTool.prototype.doDeactivate.call(tool); } // context menu for event nodes var eventNodeContextMenu = $(go.Adornment, "Vertical", $("ContextMenuButton", { click: function(e, obj) { var part = obj.part.adornedPart; if (part.data.isPositionedBelow == undefined) part.data.isPositionedBelow = true; else part.data.isPositionedBelow = !part.data.isPositionedBelow; part.updateTargetBindings(); myDiagram.layout.doLayout(myDiagram.nodes); } }, $(go.TextBlock, "Position Below", new go.Binding("text", "", function(obj) { var node = obj.part.adornedPart; return node.data.isPositionedBelow ? "Position Above" : "Position Below"; }).ofObject() ), ), $("ContextMenuButton", { click: function(e, obj) { var part = obj.part.adornedPart; myDiagram.remove(part); } }, $(go.TextBlock, "Delete")) ); // Event nodes -- default node template myDiagram.nodeTemplateMap.add("Event", $(go.Node, "Table", { locationSpot: go.Spot.Center, movable: true, contextMenu: eventNodeContextMenu, dragComputation: function(part,pt,gridpt) { if (pt.x === part.location.x) { return pt; } // figure out how far along the line we are now var link = part.findLinksOutOf().first(); var ptOnLine = null; link.points.iterator.each(function(p) { if (p.y === 0) ptOnLine = p; }); var timeline = myDiagram.findNodeForKey("timeline"); var line = timeline.findObject("MAIN"); var totalLineWidth = line.actualBounds.width; var x1 = line.actualBounds.x; var x2 = line.actualBounds.x + line.actualBounds.width; // how far along the line is ptOnLine? var ptDistAlong = ptOnLine.x - x1; var ptFracAlong = ptDistAlong / totalLineWidth; var dfd = getDateForDist(ptDistAlong); var newDate = makeLocalDate(dfd); newDate.setHours(0,0,0); myDiagram.model.set(part.data, "date", newDate); return pt; }, toolTip: // define a tooltip for each node that displays the color as text $(go.Adornment, "Auto", $(go.Shape, { fill: "#FFFFCC" }), $(go.TextBlock, { margin: 4 }, new go.Binding("text", "description")) ) // end of Adornment }, new go.Binding("location", "loc").makeTwoWay(), $(go.Panel, "Auto", //{ maxSize: new go.Size(100,NaN) }, $(go.Shape, "RoundedRectangle", { fill: "#252526", stroke: "#519ABA", strokeWidth: 2 } ), $(go.Panel, "Table", $(go.TextBlock, { maxSize: new go.Size(100,100), row: 0, stroke: "#CCCCCC", font: "bold 10pt sans-serif", textAlign: "center", margin: 2 }, new go.Binding("text", "event") ), $(go.TextBlock, { maxSize: new go.Size(100,100), row: 1, stroke: "#A074C4", textAlign: "center", margin: 2 }, new go.Binding("text", "date", function(d) { return d.toLocaleDateString(); }) ) ) ) )); myDiagram.toolManager.resizingTool.resize = function(newr) { var side = this.handle.name; var node = this.adornedObject.part; if (node.category == "Range") { var pt = this.diagram.lastInput.documentPoint; var timeline = this.diagram.findNodeForKey("timeline").findObject("MAIN"); if (side === "L" || side === "R") { // how far along the timeline (in document units) is the dragging handle? var x1 = timeline.actualBounds.x; var y1 = timeline.actualBounds.y; var x2 = timeline.actualBounds.x+timeline.actualBounds.width; var y2 = y1; pt.projectOntoLineSegment(x1, y1, x2, y2); var daysDist = convertDocumentUnitsToDays(pt.x); var timelineNode = this.diagram.findNodeForKey("timeline"); var tlStart = new Date(timelineNode.data.start); var newDate = tlStart; newDate.setDate(newDate.getDate() + daysDist); newDate = new Date(newDate); if (side == "R" && newDate < new Date(node.data.start)) { newDate = new Date(node.data.start); } else if (side == "L" && newDate > new Date(node.data.end)) { newDate = new Date(node.data.end); } var prop = (side == "R") ? "end" : "start"; this.diagram.model.set(node.data, prop, newDate); this.diagram.layout.doLayout(this.diagram.nodes); } // adjusting range height else { if (pt.y > timeline.actualBounds.y && !node.data.isPositionedBelow) return; if (pt.y < timeline.actualBounds.y && node.data.isPositionedBelow) return; var yDistFromLine = Math.abs(pt.y-timeline.actualBounds.y); var rh = yDistFromLine < 30 ? 30 : yDistFromLine; this.diagram.model.set(node.data, "rangeHeight", rh); } } else { go.ResizingTool.prototype.resize.call(this, newr); } } // Event range nodes lastDraggedPoint = new go.Point(0,0); myDiagram.nodeTemplateMap.add("Range", $(go.Node, "Vertical", { locationSpot: go.Spot.BottomCenter, locationObjectName: "RANGELINEABOVE", contextMenu: eventNodeContextMenu, movable: true, resizable: true, resizeObjectName: "RANGELINEABOVE", selectionAdorned: false, resizeAdornmentTemplate: $(go.Adornment, "Spot", $(go.Placeholder), // takes size and position of adorned object $(go.Shape, "TriangleLeft", // left resize handle { alignment: go.Spot.Left, cursor: "col-resize", name: "L", desiredSize: new go.Size(8, 8), fill: "lightblue", stroke: "black" }, new go.Binding("fill", "color") ), $(go.Shape, "TriangleRight", // right resize handle { alignment: go.Spot.Right, cursor: "col-resize", name: "R", desiredSize: new go.Size(8, 8), fill: "lightblue", stroke: "black" }, new go.Binding("fill", "color") ), $(go.Shape, "Diamond", // up resize handle { alignment: go.Spot.BottomCenter, cursor: "ns-resize", name: "U", desiredSize: new go.Size(8, 8), fill: "lightblue", stroke: "black" }, new go.Binding("fill", "color") ) ), dragComputation: function (part, pt, gridpt) { if (myDiagram.lastInput.documentPoint.x == lastDraggedPoint.x) { return new go.Point(pt.x, part.location.y); } lastDraggedPoint = myDiagram.lastInput.documentPoint; var oldLoc = part.location; var rangeWidth = part.data.rangeWidth; var l = part.data.isPositionedBelow ? part.findObject("RANGELINEBELOW") : part.findObject("RANGELINEABOVE"); var sPtx = l.getDocumentPoint(go.Spot.Left).x; var ePtX = l.getDocumentPoint(go.Spot.Right).x; //var startX = part.actualBounds.x; startX = sPtx; var xOff = pt.x-oldLoc.x; var xVal = startX + (.5*rangeWidth) + xOff; var newLoc = new go.Point(xVal, oldLoc.y); part.location = newLoc; // gets the date at each end of the range node function getDateForEndpoint(pt) { var line = myDiagram.findNodeForKey("timeline").findObject("MAIN"); var lineStart = new go.Point(line.actualBounds.x, line.actualBounds.y); var lineEnd = new go.Point(line.actualBounds.x + line.actualBounds.width, line.actualBounds.y); var totalLineWidth = lineEnd.x - lineStart.x; // how far along the line is ptOnLine? var ptOnLine = pt.projectOntoLineSegmentPoint(lineStart, lineEnd); var ptDistAlong = ptOnLine.x - lineStart.x; var ptFracAlong = ptDistAlong / totalLineWidth; var d = new Date (getDateForDist(ptDistAlong)); return d; } // just update start / end dates based on the bounds of the part, given by the newLoc var sPt = new go.Point(sPtx, part.location.y); var ePt = new go.Point(ePtX, part.location.y); var newStartDate = getDateForEndpoint(sPt); var newEndDate = getDateForEndpoint(ePt); myDiagram.model.set(part.data, "start", newStartDate); myDiagram.model.set(part.data, "end", newEndDate); return newLoc; }, toolTip: // define a tooltip for each node that displays the color as text $(go.Adornment, "Auto", $(go.Shape, { fill: "#FFFFCC" }), $(go.TextBlock, { margin: 4 }, new go.Binding("text", "description")) ) // end of Adornment }, new go.Binding("location", "loc").makeTwoWay(), new go.Binding("locationSpot", "isPositionedBelow", function(ipb){ return ipb ? go.Spot.TopCenter : go.Spot.BottomCenter; }), new go.Binding("locationObjectName", "isPositionedBelow", function(ipb){ return ipb ? "RANGELINEBELOW" : "RANGELINEABOVE"; }), new go.Binding("resizeObjectName", "isPositionedBelow", function(ipb){ return ipb ? "RANGELINEBELOW" : "RANGELINEABOVE"; }), // dates + name, above $(go.Panel, "Vertical", { margin: 0, background: "black", cursor: "move" }, $(go.TextBlock, "Event Range Name", { stroke: "white", name: "RANGETEXTABOVE" }, new go.Binding("visible", "isPositionedBelow", function (ipb){ return !ipb; }), new go.Binding("text", "event") ), $(go.TextBlock, "dates range", { stroke: "white", name: "RANGEDATESABOVE" }, new go.Binding("text", "", function(obj){ var part = obj.part; var sd = new Date(obj.part.data.start); var ed = new Date(obj.part.data.end); return sd.toLocaleDateString() + " - " + ed.toLocaleDateString(); }).ofObject(), new go.Binding("visible", "isPositionedBelow", function (ipb){ return !ipb; }) ), // the range shape -- leftmost point must be at start date, rightmost point at end date $(go.Shape, "LineH", { stroke: "red", strokeWidth: 5, height: 0, alignment: go.Spot.Center, name: "RANGELINEABOVE", /*geometryString: "M0 480 C 148 320 183 450 285 419 S 335 274 392 391 S 583 320 730 480 "*/ }, new go.Binding("width", "rangeWidth").makeTwoWay(), new go.Binding("stroke", "color"), new go.Binding("visible", "isPositionedBelow", function (ipb){ return !ipb; }) ) ), // end dates + name, above // the range "rectangle" -- visible iff range is positioned above the timeline $(go.Shape, "Rectangle", { opacity: .05, stroke: null, name: "RANGESHAPEABOVE", pickable: false }, new go.Binding("width", "rangeWidth"), new go.Binding("height", "rangeHeight").makeTwoWay(), new go.Binding("fill", "color"), new go.Binding("visible", "isPositionedBelow", function (ipb){ return !ipb; }) ), // the range "rectangle" -- visible iff range is positioned below the timeline $(go.Shape, "Rectangle", { opacity: .2, name: "RANGESHAPEBELOW" }, new go.Binding("width", "rangeWidth").makeTwoWay(), new go.Binding("height", "rangeHeight").makeTwoWay(), new go.Binding("fill", "color"), new go.Binding("visible", "isPositionedBelow", function (ipb){ return ipb; }) ), $(go.Panel, "Vertical", { name: "RANGEDETAILBELOW", background: "black", cursor: "move" }, $(go.Shape, "LineH", { stroke: "red", strokeWidth: 5, alignment: go.Spot.Center, height: 0, name: "RANGELINEBELOW" }, new go.Binding("width", "rangeWidth").makeTwoWay(), new go.Binding("stroke", "color"), new go.Binding("visible", "isPositionedBelow", function (ipb){ return ipb; }) ), $(go.TextBlock, "Event Range Name", { stroke: "white", name: "RANGETEXTBELOW" }, new go.Binding("visible", "isPositionedBelow", function (ipb){ return ipb; }), new go.Binding("text", "event") ), $(go.TextBlock, "dates range", { stroke: "white", name: "RANGEDATESBELOW" }, new go.Binding("text", "", function(obj){ var part = obj.part; var sd = new Date(obj.part.data.start); var ed = new Date(obj.part.data.end); return sd.toLocaleDateString() + " - " + ed.toLocaleDateString(); }).ofObject(), new go.Binding("visible", "isPositionedBelow", function (ipb){ return ipb; }) ) ) ) ); var timelineContextMenu = $(go.Adornment, "Vertical", $("ContextMenuButton", { click: function (e, obj) { createEventNode(); } }, $(go.TextBlock, "Add Event") ) ); // timeline node myDiagram.nodeTemplateMap.add("Line", $(go.Node, "Graduated", { movable: false, copyable: false, deletable: false, resizable: true, resizeObjectName: "MAIN", background: "transparent", graduatedMin: 0, graduatedMax: 365, graduatedTickUnit: 1, resizeAdornmentTemplate: // only resizing at right end $(go.Adornment, "Spot", $(go.Placeholder), $(go.Shape, { alignment: go.Spot.Right, cursor: "e-resize", desiredSize: new go.Size(4, 16), fill: "lightblue", stroke: "deepskyblue" }) ), contextMenu: diagramContextMenu }, new go.Binding("graduatedMax", "", timelineDays), $(go.Shape, "LineH", { name: "MAIN", stroke: "#519ABA", height: 1, strokeWidth: 3, portId: "" }, new go.Binding("width", "length").makeTwoWay() ), $(go.Shape, { geometryString: "M0 0 V10", interval: 7, stroke: "#519ABA", strokeWidth: 2 }, new go.Binding("interval", "length", calculateLabelInterval) ), $(go.TextBlock, { font: "10pt sans-serif", stroke: "#CCCCCC", interval: 14, alignmentFocus: go.Spot.MiddleRight, segmentOrientation: go.Link.OrientMinus90, segmentOffset: new go.Point(0, 12), graduatedFunction: valueToDate, name: "LABEL" }, new go.Binding("interval", "length", calculateLabelInterval) ) ) ); myDiagram.addDiagramListener("ViewportBoundsChanged", function(e) { var diagram = e.diagram; var timeline = diagram.findNodeForKey("timeline"); timeline.updateTargetBindings("length"); }); function calculateLabelInterval(len) { var timeline = myDiagram.findNodeForKey("timeline"); // how many days does len represent? var daysInTimeline = Math.round((timeline.data.end-timeline.data.start)/(1000*60*60*24)); var daysPerPixel = daysInTimeline / len; var daysLen = len*daysPerPixel; var numIntervals = len/125; var daysPerInterval = daysLen/(numIntervals*myDiagram.scale); if (daysPerInterval < 1) daysPerInterval = 1; return daysPerInterval; } // The template for the link connecting the event node with the timeline bar node: myDiagram.linkTemplate = $(BarLink, // defined below { toShortLength: 2 , layerName: "Background" }, $(go.Shape, { stroke: "#E37933", strokeWidth: 2 }) ); // Setup the model data -- an object describing the timeline bar node // and an object for each event node: var data = [ { // this defines the actual time "Line" bar key: "timeline", category: "Line", lineSpacing: 30, // distance between timeline and event nodes length: 2500, // the width of the timeline start: new Date("1 May 2010"), end: new Date("31 Jul 2010") } ]; // prepare the model by adding links to the Line for (var i = 0; i < data.length; i++) { var d = data[i]; if (d.category == "Event") d.parent = "timeline"; } myDiagram.model = $(go.TreeModel, { nodeDataArray: data }); // Define GoEditor framework structures for node generation // Event nodes // event node properties var eDataProps = [ { name: "event", type: "string" }, { name: "date", type: "date" }, { name: "description", type: "description" } ]; // event node predicates, TODO? var ePredicates = [ function (nodeData) { if (nodeData.date instanceof Date && nodeData.date != "Invalid Date") return true; else return new Date(nodeData.date) != "Invalid Date"; }, // this is sort of sneaky -- I'm using this as a way to ensure there is always a Timeline Node, not to check if an Event Node can be created function (nodeData) { var timeline = myDiagram.findNodeForKey("timeline"); if (!timeline) { var timelineData = { // this defines the actual time "Line" bar key: "timeline", category: "Line", lineSpacing: 30, // distance between timeline and event nodes length: 2500, // the width of the timeline start: new Date("1 May 2010"), end: new Date("31 Jul 2010") }; myDiagram.model.addNodeData(timelineData); } return true; } ]; // event node postCreateFunction var ePostCreate = function (node) { var timeline = myDiagram.findNodeForKey("timeline"); var nodeData = node.data; // then, update that timeline's boundary dates if the event node is outside them var ed = nodeData.date; var tls = timeline.data.start; var tle = timeline.data.end; ed = new Date(ed); myDiagram.startTransaction(); if (ed > tle) { myDiagram.model.set(timeline.data, "end", ed); } else if (ed < tls) { myDiagram.model.set(timeline.data, "start", ed); } // then, add set the event node's "parent" data property to the timeline myDiagram.model.setParentKeyForNodeData(nodeData, "timeline"); myDiagram.commitTransaction(); }; // event node default data var eDefaultData = { isPositionedBelow: false, event: "Event", date: "Invalid Date", description: "Default description" }; nodeGenerationMap.add("Event", { dataProps: eDataProps, predicates: ePredicates, postCreateFunction: ePostCreate, defaultNodeData: eDefaultData }); // Range nodes // range node data props var rDataProps = [ { name: "event", type: "string" }, { name: "start", type: "date" }, { name: "end", type: "date" }, { name: "description", type: "string" }, { name: "color", type: "color" } ]; var rPredicates = [ function (nodeData) { var sd = true; var ed = true; if (nodeData.start instanceof Date && nodeData.start == "Invalid Date") sd = false; else if (new Date(nodeData.start) == "Invalid Date") sd = false; if (nodeData.end instanceof Date && nodeData.end == "Invalid Date") ed = false; else if (new Date(nodeData.end) == "Invalid Date") ed = false; return (sd && ed); }, // this is sort of sneaky -- I'm using this as a way to ensure there is always a Timeline Node, not to check if an Event Node can be created function (nodeData) { var timeline = myDiagram.findNodeForKey("timeline"); if (!timeline) { var timelineData = { // this defines the actual time "Line" bar key: "timeline", category: "Line", lineSpacing: 30, // distance between timeline and event nodes length: 2500, // the width of the timeline start: new Date("1 May 2010"), end: new Date("31 Jul 2010") }; myDiagram.model.addNodeData(timelineData); } return true; } ]; // if range node start / end dates outside timeline bounds, ajust timeline bounds var rPostCreate = function (node) { var timeline = myDiagram.findNodeForKey("timeline"); var dates = [node.data.start, node.data.end]; myDiagram.startTransaction(); for (let i in dates) { var d = dates[i]; var tls = timeline.data.start; var tle = timeline.data.end; if (d > tle) { myDiagram.model.set(timeline.data, "end", d); } else if (d < tls) { myDiagram.model.set(timeline.data, "start", d); } } myDiagram.commitTransaction(); }; var rDefaultData = { isPositionedBelow: false }; nodeGenerationMap.add("Range", { dataProps: rDataProps, predicates: rPredicates, postCreateFunction: rPostCreate, defaultNodeData: rDefaultData }); // define the categories node data can be imported for categories = ["Event", "Range"]; /*12DY@9dkd)dk*/ } // end init </script> </head> <body onload="init();"> <div> <nav> <span id="currentStorageSpan"></span> <ul id="fileMenus"> <li> <a href="#">File</a> <ul> <li><a href="#" onclick="handlePromise('New')">New <p class="shortcut">(Ctrl + D)</p></a></li> <li><a href="#" onclick="handlePromise('Load')">Open... <p class="shortcut">(Ctrl + O)</p></a></li> <li><a href="#" onclick="handlePromise('Save')">Save <p class="shortcut">(Ctrl + S)</p></a></li> <li><a href="#" onclick="handlePromise('SaveAs')">Save As...</a></li> <li><a href="#" onclick="handlePromise('Delete')">Remove... <p class="shortcut">(Ctrl + R)</p></a></li> <li><a href="#">Import Data From >>></a> <ul> <li><a href="#" onclick="authorizeGoogleUserForDataImport()">Google Sheet</a></li> <li><a href="#" onclick="document.getElementById('file-input').click()">Local CSV File</a> </ul> </li> <li><a href="#" onclick="makeDiagramImage()">Export PNG</a></li> <li><a href="#" onclick="updateCurrentStorageSpan()">Change Storage Service</a></li> </ul> </li> </ul> <p id="isAutoSavingP"><input type="checkbox" id="isAutoSavingCheckbox" unchecked /> <label for="isAutoSavingCheckbox">Autosave Enabled</label></p> <p id="ge-header">GoTimeline v1.0</p> <div id="ge-filename">(Unsaved file)</div> </nav> <input type="file" id="file-input" style="display: none;" /> <div id="container" style="display: flex;"> <div id="myDiagramDiv" style="background: #252526; width: 100%; height:800px; float: left;"></div> <div id="ge-inspector-div" style="background: #212121; flex-grow: 1; height: 800px; float: right;"> <div id="myInspectorDiv" class="inspector"> </div> </div> </div> <div id="ge-footer"> <span>Built with the <a href="https://gojs.net">GoJS Diagramming Library</a>, by <a href="https://nwoods.com"> Northwoods Software</a>.</span> </div> <p> <p> This editor allows one to build interactive timelines. Right click anywhere in the diagram to add a new Event or Range node. Select an element to edit its properties. </p> <p> Data formatted properly in rows and columns in either <code>.csv</code> or Google Sheet format can be imported using the File menu. A <code>png</code> of the entire timeline may be exported via the File menu. </p> <p> Timeline data can be saved or loaded to or from various cloud storage services via the Open, Remove, Save, and Save As buttons in the File menu. </p> </p> <script> // returns number of days in timeline function timelineDays() { var timeline = myDiagram.model.findNodeDataForKey("timeline"); var startDate = timeline.start; var endDate = timeline.end; function treatAsUTC(date) { var result = new Date(date); result.setMinutes(result.getMinutes() - result.getTimezoneOffset()); return result; } function daysBetween(startDate, endDate) { var millisecondsPerDay = 24 * 60 * 60 * 1000; return (treatAsUTC(endDate) - treatAsUTC(startDate)) / millisecondsPerDay; } return daysBetween(startDate, endDate); } // Useful little functions // convert "n" num of document units into days function convertDocumentUnitsToDays (n) { var timeline = myDiagram.findNodeForKey("timeline"); var daysInTimeline = Math.round((timeline.data.end-timeline.data.start)/(1000*60*60*24)); var daysPerPixel = daysInTimeline / timeline.data.length; return n*daysPerPixel; } function getDaysBetween(first, second) { // Take the difference between the dates and divide by milliseconds per day. // Round to nearest whole number to deal with DST. return Math.round((second-first)/(1000*60*60*24)); } // returns the date represented by a point "n" pixels along the timeline (from the left) function getDateForDist(n) { var timeline = myDiagram.model.findNodeDataForKey("timeline"); var startDate = timeline.start; var startDateMs = startDate.getTime() + startDate.getTimezoneOffset() * 60000; var daysInTimeline = Math.round((timeline.end-timeline.start)/(1000*60*60*24)); var daysPerPixel = daysInTimeline / myDiagram.findNodeForKey("timeline").findObject("MAIN").actualBounds.width; var msPerDay = 24 * 60 * 60 * 1000; var date = new Date(startDateMs + n * msPerDay * daysPerPixel); return date.toLocaleDateString(); } // return a Date with the correct timezone offset. accepts a formatted string function makeLocalDate(dateStr) { d = new Date(dateStr); d.setMinutes(d.getMinutes() + d.getTimezoneOffset()); return d; } function refreshDraggableWindows () { jQuery(".ge-menu").draggable({ handle: ".ge-handle", stack: ".ge-menu", containment: 'window', scroll: false }); } // This custom Layout locates the timeline bar at (0,0) // and alternates the event Nodes above and below the bar at // the X-coordinate locations determined by their data.date values. function TimelineLayout() { go.Layout.call(this); }; go.Diagram.inherit(TimelineLayout, go.Layout); TimelineLayout.prototype.doLayout = function(coll) { var diagram = this.diagram; if (diagram === null) return; coll = this.collectParts(coll); diagram.startTransaction("TimelineLayout"); var line = null; var parts = []; var it = coll.iterator; while (it.next()) { var part = it.value; if (part instanceof go.Link) continue; if (part.category === "Line") { line = part; continue; } if (part.data == null || part.category === "Title") continue; parts.push(part); if (part.category === "Range") continue; var x = part.data.date; if (x === undefined) { x = new Date(); part.data.date = x; } } if (!line) throw Error("No node of category 'Line' for TimelineLayout"); line.location = new go.Point(0, 0); // sort parts so we lay them out based on their vertical distance to the timeline parts.sort(function(a,b){ var distA = Math.abs(a.location.y-line.location.y); var distB = Math.abs(b.location.y-line.location.y); return distA > distB; }); // lay out the events and event ranges above or below the timeline if (parts.length > 0) { // determine the offset from the main shape to the timeline's boundaries var main = line.findMainElement(); var sw = main.strokeWidth; var mainOffX = main.actualBounds.x; var mainOffY = main.actualBounds.y; // spacing is between the Line and the closest Nodes, defaults to 30 var spacing = line.data.lineSpacing; if (!spacing) spacing = 30; for (var i = 0; i < parts.length; i++) { var part = parts[i]; var bnds = part.actualBounds; var pt = null; if (part.category == "Event") { var dt = part.data.date; var val = dateToValue(dt); pt = line.graduatedPointForValue(val); } else if (part.category == "Range") { var sd = part.data.start; var ed = part.data.end; sd = new Date(sd); ed = new Date(ed); // give the proper "rangeWidth" px value (for space between start / end dates) var val1 = dateToValue(sd); var val2 = dateToValue(ed); // val1, val2, in days var daysPerDocUnit = getDaysPerDocumentUnit(); val1 /= daysPerDocUnit; val2 /= daysPerDocUnit; // val1, val2, in doc units var w = val2 - val1; diagram.model.set(part.data, "rangeWidth", w); var midX = val1 + (.5*w); pt = new go.Point(midX,0); } var tempLoc = new go.Point(pt.x, pt.y - bnds.height / 2 - spacing); if (part.data.isPositionedBelow) { var additionalOffset = (part.category == "Event") ? 50 : 0; tempLoc = new go.Point(pt.x, pt.y + bnds.height / 2 + spacing + additionalOffset); } // check if this node will overlap with previously placed events, and offset if needed -- Event Nodes only for (var j = 0; j < i; j++) { if (part.data.category == "Range") continue; if (part.data.key == parts[j].data.key) continue; var partRect = null; var bnds = part.actualBounds; partRect = new go.Rect(tempLoc.x-(.5*bnds.width), tempLoc.y-(.5*bnds.height), bnds.width, bnds.height); var otherLoc = parts[j].location; var otherBnds = parts[j].actualBounds; var otherRect = null; // range nodes have a different locationSpot than event nodes, so we need to account for that when checking overlap if (parts[j].category != "Range") { otherRect = new go.Rect(otherLoc.x-(.5*otherBnds.width), otherLoc.y-(.5*otherBnds.height), otherBnds.width, otherBnds.height); } else { otherRect = new go.Rect(parts[j].actualBounds.x, parts[j].actualBounds.y, otherBnds.width, otherBnds.height); } if (partRect.intersectsRect(otherRect) && part.category == "Event") { if (part.data.isPositionedBelow) { tempLoc.offset(0, 1); } else { tempLoc.offset(0, -1); } j = -1; // now that we have a new location, we need to recheck in case we overlap with an event we didn't overlap before } } // final padding if (part.data.isPositionedBelow) { tempLoc.offset(0, 10); } else { tempLoc.offset(0, -10); } part.location = tempLoc; // re-position range nodes to factor out their "range rectangles" s.t. these ranges touch the timeline if (part.category == "Range") { var lineShape = line.findObject("MAIN"); var yDistFromLine = Math.abs(part.location.y-lineShape.actualBounds.y); var rh = part.data.rangeHeight != undefined ? part.data.rangeHeight : yDistFromLine; diagram.model.set(part.data, "rangeHeight", rh); part.location = part.data.isPositionedBelow ? new go.Point(part.location.x, rh) : new go.Point(part.location.x, -rh); } } } if (diagram.selection.first()) { var n = diagram.selection.first(); if (n.category == "Event" && !(n.actualBounds.intersectsRect(myDiagram.viewportBounds))) { diagram.centerRect(new go.Rect(n.location.x, n.location.y, 1, 1)); } } diagram.commitTransaction("TimelineLayout"); }; // end TimelineLayout class // This custom Link class was adapted from several of the samples function BarLink() { go.Link.call(this); } go.Diagram.inherit(BarLink, go.Link); BarLink.prototype.getLinkPoint = function(node, port, spot, from, ortho, othernode, otherport) { var r = new go.Rect(port.getDocumentPoint(go.Spot.TopLeft), port.getDocumentPoint(go.Spot.BottomRight)); var op = otherport.getDocumentPoint(go.Spot.Center); var main = node.category === "Line" ? node.findMainElement(): othernode.findMainElement(); var mainOffY = main.actualBounds.y; var y = r.top; if (node.category === "Line") { y += mainOffY; if (op.x < r.left) return new go.Point(r.left, y); if (op.x > r.right) return new go.Point(r.right, y); return new go.Point(op.x, y); } else { return new go.Point(r.centerX, r.bottom); } }; BarLink.prototype.getLinkDirection = function(node, port, linkpoint, spot, from, ortho, othernode, otherport) { return 270; }; // end BarLink class // "value" is an interval value for the graduated panel that represents the timeline function valueToDate(n) { var timeline = myDiagram.model.findNodeDataForKey("timeline"); var startDate = timeline.start; var startDateMs = startDate.getTime() + startDate.getTimezoneOffset() * 60000; var msPerDay = 24 * 60 * 60 * 1000; var date = new Date(startDateMs + n * msPerDay); return date.toLocaleDateString(); } // "value" is an interval value for the graduated panel that represents the timeline function dateToValue(d) { d = new Date(d); var timeline = myDiagram.model.findNodeDataForKey("timeline"); var startDate = timeline.start; var startDateMs = startDate.getTime() + startDate.getTimezoneOffset() * 60000; var dateInMs = d.getTime() + d.getTimezoneOffset() * 60000; var msSinceStart = dateInMs - startDateMs; var msPerDay = 24 * 60 * 60 * 1000; return msSinceStart / msPerDay; } // how many days are in 1 document unit? function getDaysPerDocumentUnit() { var line = myDiagram.findNodeForKey("timeline"); var daysInTimeline = Math.round((line.data.end-line.data.start)/(1000*60*60*24)); var daysPerPixel = daysInTimeline / myDiagram.findNodeForKey("timeline").findObject("MAIN").actualBounds.width; return daysPerPixel; } /* Post-Init files go here ijiwd8up@90n */ </script> </div> </body> </html>