gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
320 lines (287 loc) • 12.2 kB
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>