UNPKG

gojs

Version:

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

320 lines (287 loc) 12.2 kB
<!DOCTYPE html> <html> <head> <title>Timeline Sample</title> <meta name="description" content="A stretchable timeline." /> <!-- Copyright 1998-2016 by Northwoods Software Corporation. --> <meta charset="UTF-8"> <script src="go.js"></script> <link href="../assets/css/goSamples.css" rel="stylesheet" type="text/css" /> <!-- you don't need to use this --> <script src="goSamples.js"></script> <!-- this is only for the GoJS Samples framework --> <script id="code"> function init() { if (window.goSamples) goSamples(); // init for these samples -- you don't need to call this var $ = go.GraphObject.make; myDiagram = $(go.Diagram, "myDiagramDiv", { isTreePathToChildren: false, // arrows from children (events) to the parent (timeline bar) initialContentAlignment: go.Spot.Center, layout: $(TimelineLayout), // defined below "PartResized": function(e) { e.diagram.layoutDiagram(true); } }); // The template for each "event": myDiagram.nodeTemplate = $(go.Node, "Auto", { locationSpot: go.Spot.Center }, $(go.Shape, { fill: "lightyellow", stroke: "gray" }), $(go.Panel, "Vertical", { defaultStretch: go.GraphObject.Horizontal }, $(go.TextBlock, { font: "bold 12pt sans-serif", textAlign: "center", background: "slateblue", stroke: "white" }, new go.Binding("text", "name")), $(go.TextBlock, { maxSize: new go.Size(100, NaN), textAlign: "center", margin: 4 }, new go.Binding("text", "time", function(d) { return d.toLocaleDateString(); })) ) ); // The template for the timeline bar: // convert a Date object to a string to be shown in the timeline bar function formatDate(d) { var s = d.toDateString(); return s.substr(s.indexOf(' ')); // don't need time of day } // these parameters define the size of the timeline bar var MINGRIDWIDTH = 200; var GRIDHEIGHT = 28; myDiagram.nodeTemplateMap.add("Line", $(go.Node, "Auto", { location: new go.Point(0, 0), locationObjectName: "BAR", locationSpot: go.Spot.Left, background: "lightgray", selectionAdorned: false, movable: false, copyable: false, resizable: true, resizeObjectName: "BAR", resizeAdornmentTemplate: // only resizing at right end $(go.Adornment, "Spot", $(go.Placeholder), //$(go.Shape, { alignment: go.Spot.Left, cursor: "w-resize", desiredSize: new go.Size(6, 16), fill: "lightblue", stroke: "deepskyblue" }), $(go.Shape, { alignment: go.Spot.Right, cursor: "e-resize", desiredSize: new go.Size(6, 16), fill: "lightblue", stroke: "deepskyblue" }) ) }, // this is the timeline bar $(go.Panel, "Grid", { name: "BAR", width: MINGRIDWIDTH, height: GRIDHEIGHT, minSize: new go.Size(MINGRIDWIDTH, GRIDHEIGHT), maxSize: new go.Size(9999, GRIDHEIGHT) }, new go.Binding("width", "length").makeTwoWay(), $(go.Shape, "LineV", { stroke: "white" }) ), // this holds all of the text $(go.Panel, "Table", { name: "TABLE", alignment: go.Spot.TopLeft, // itemArray is set in TimelineLayout itemTemplate: $(go.Panel, "TableColumn", $(go.TextBlock, { alignment: go.Spot.TopLeft, stretch: go.GraphObject.Horizontal, margin: new go.Margin(1, 0) }, new go.Binding("text", "", formatDate)) ) } ) )); // The template for the link connecting the event node with the timeline bar node: myDiagram.linkTemplate = $(BarLink, // defined below { routing: go.Link.Orthogonal, toShortLength: 2 }, $(go.Shape, { stroke: "gray" }), $(go.Shape, { toArrow: "Standard", fill: "gray", stroke: "gray" }) ); // 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: 0, category: "Line", length: 500, // length of bar lineSpacing: 30, // distance between bar and event nodes start: new Date("1 Dec 2013"), end: new Date("1 Apr 2014") }, // the rest are just "events" -- // you can add as much information as you want on each and extend the // default nodeTemplate to show as much information as you want { name: "Alpha", time: new Date("12 Feb 2014") }, { name: "Beta", time: new Date("17 Mar 2014") }, { name: "Gamma", time: new Date("3 Jan 2014") }, { name: "Delta", time: new Date("31 Jan 2014") } ]; // prepare the model by adding links to the Line for (var i = 0; i < data.length; i++) { var d = data[i]; if (d.key !== 0) d.parent = 0; } myDiagram.model = $(go.TreeModel, { nodeDataArray: data }); } // end init // 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.time 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; if (coll instanceof go.Diagram) { coll = coll.nodes; } else if (coll instanceof go.Group) { coll = coll.memberParts; } 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; } parts.push(part); var x = part.data.time; if (x === undefined) { x = new Date(); part.data.time = x; } } if (!line) throw Error("No node of category 'Line' for TimelineLayout"); line.location = new go.Point(0, 0); var linebnds = line.actualBounds; if (parts.length > 0) { parts.sort(function(a, b) { var ad = a.data; var bd = b.data; if (ad.time < bd.time) return -1; else if (ad.time > bd.time) return 1; else return 0; }); // compute the first and last dates var first = parts[0].data.time; var start = line.data.start; if (typeof start === "string") start = new Date(start); if (!start) start = first; // a start date might be earlier than the first date in the data start = Math.min(start, first); var last = parts[parts.length - 1].data.time; var end = line.data.end; if (typeof end === "string") end = new Date(end); if (!end) end = last; // an end date might be later than the last date in the data end = Math.max(end, last); // calculate how many days and weeks the data covers start = Math.min(start, end); end = Math.max(start, end); var numdays = (end-start) / (24 * 60 * 60 * 1000); var numweeks = numdays/7; var firstsunday = new Date(start); // before or on the start date var dayofweek = firstsunday.getDay(); firstsunday.setDate(firstsunday.getDate() - dayofweek); firstsunday.setHours(0); // set to midnight firstsunday.setMinutes(0); firstsunday.setSeconds(0); firstsunday.setMilliseconds(0); // given a Date, determine the X coordinate function convertDateToX(d) { if (end - start <= 0) return 0; // handle zero! var frac = (d - start) / (end - start); return frac * length; } // draw gridlines at the beginning of each week var bar = line.findObject("BAR"); var length = 100; var cellw = 100; if (bar && numweeks > 0) { length = bar.actualBounds.width; cellw = length / numweeks; // set the size of each cell bar.gridCellSize = new go.Size(cellw, bar.gridCellSize.height); // offset to account for starting on a non-first day of the week bar.gridOrigin = new go.Point(convertDateToX(firstsunday), bar.gridOrigin.y); } // show the date of the first day of each week var table = line.findObject("TABLE"); if (table) { // offset to account for starting on a non-first day of the week table.margin = new go.Margin(0, 0, 0, convertDateToX(firstsunday)); // build the itemArray of Date objects, if needed var periods = table.itemArray; if (!periods || periods.length !== Math.floor(numweeks+1)) { // create an Array of Dates to be shown in the timeline bar var periods = []; var date = new Date(firstsunday); periods.push(date); for (var i = 1; i < numweeks; i++) { date = new Date(date); date.setDate(date.getDate() + 7); periods.push(date); } // this creates as many itemTemplate copies as there are Dates in the Array table.itemArray = periods; } // make sure they all have the same width for (var i = 0; i < periods.length; i++) { table.getColumnDefinition(i).width = cellw; } } // This layout is not smart enough (yet) to have multiple rows of event nodes // when the nodes are too close to each other and start overlapping. var above = true; // 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 t = part.data.time; var x = convertDateToX(t); if (above) { part.location = new go.Point(x, -bnds.height/2 - spacing - linebnds.height/2); } else { part.location = new go.Point(x, bnds.height/2 + spacing + linebnds.height/2); } above = !above; } } 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 below = op.y > r.centerY; var y = below ? r.bottom : r.top; if (node.category === "Line") { 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, y); } }; BarLink.prototype.getLinkDirection = function(node, port, linkpoint, spot, from, ortho, othernode, otherport) { var p = port.getDocumentPoint(go.Spot.Center); var op = otherport.getDocumentPoint(go.Spot.Center); var below = op.y > p.y; return below ? 90 : 270; }; // end BarLink class </script> </head> <body onload="init()"> <div id="sample"> <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div> <p> Try resizing the timeline: select the timeline and drag the resize handle that is on the right side. </p> <p> This sample includes a <code>TimelineLayout</code> which is responsible for rendering the timeline bar and for positioning the event nodes. </p> </div> </body> </html>