gojs
Version:
Interactive diagrams, charts, and graphs, such as trees, flowcharts, orgcharts, UML, BPMN, or business diagrams
360 lines (331 loc) • 16.8 kB
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 stretchable timeline."/>
<link rel="stylesheet" href="../assets/css/style.css"/>
<!-- Copyright 1998-2023 by Northwoods Software Corporation. -->
<title>Timeline</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">
<script id="code">
function init() {
// Since 2.2 you can also author concise templates with method chaining instead of GraphObject.make
// For details, see https://gojs.net/latest/intro/buildingObjects.html
const $ = go.GraphObject.make; // for conciseness in defining templates
myDiagram = new go.Diagram("myDiagramDiv", // create a Diagram for the DIV HTML element
{
"animationManager.isEnabled": false,
"commandHandler.decreaseZoom": function() { changeScale(1/1.05) }, // method override must be function, not =>
"commandHandler.increaseZoom": function() { changeScale(1.05) }, // method override must be function, not =>
"commandHandler.resetZoom": function() { setScale(1.0) }, // method override must be function, not =>
layout: $(TimelineLayout),
isTreePathToChildren: false // arrows from children (events) to the parent (timeline bar)
});
function changeScale(factor) {
const oldscale = myDiagram.model.modelData.scale || 1.0;
const newscale = factor ? (oldscale * factor) : 1.0;
setScale(newscale);
}
function setScale(scale) {
const docpt = myDiagram.lastInput.documentPoint.copy();
let line = null;
myDiagram.commit(diag => {
diag.model.set(diag.model.modelData, "scale", scale);
diag.nodes.each(n => {
if (n.category === "Line") {
line = n;
n.updateTargetBindings();
return;
}
});
}, null); // no UndoManager
if (line !== null && docpt.x > line.position.x) {
myDiagram.position = new go.Point(docpt.x - (docpt.x-line.position.x)/scale, myDiagram.position.y);
}
}
myDiagram.nodeTemplate =
$(go.Node, "Table",
{ locationSpot: go.Spot.Center, movable: false },
$(go.Panel, "Auto",
$(go.Shape, "RoundedRectangle",
{ fill: "#252526", stroke: "#519ABA", strokeWidth: 3 }
),
$(go.Panel, "Table",
$(go.TextBlock,
{
row: 0,
stroke: "#CCCCCC",
wrap: go.TextBlock.WrapFit,
font: "bold 12pt sans-serif",
textAlign: "center", margin: 4
},
new go.Binding("text", "event")
),
$(go.TextBlock,
{
row: 1,
stroke: "#A074C4",
textAlign: "center", margin: 4
},
new go.Binding("text", "date", d => d.toLocaleDateString())
)
)
)
);
myDiagram.nodeTemplateMap.add("Line",
$(go.Node, "Graduated",
{
movable: false, copyable: 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" })
)
},
new go.Binding("graduatedMax", "", timelineDays),
$(go.Shape, "LineH",
{ name: "MAIN", stroke: "#519ABA", height: 1, strokeWidth: 3 },
new go.Binding("width", "length", (l, shape) => l * (shape.diagram.model.modelData.scale || 1.0))
.makeTwoWay((w, data, model) => w/(model.modelData.scale || 1.0))
),
$(go.Shape, { geometryString: "M0 0 V10", interval: 7, stroke: "#519ABA", strokeWidth: 2 }),
$(go.TextBlock,
{
font: "10pt sans-serif",
stroke: "#CCCCCC",
interval: 14,
alignmentFocus: go.Spot.Right,
segmentOrientation: go.Link.OrientMinus90,
segmentOffset: new go.Point(0, 12),
graduatedFunction: valueToDate
},
new go.Binding("interval", "length", calculateLabelInterval)
)
)
);
function calculateLabelInterval(len) {
if (len >= 800) return 7;
else if (400 <= len && len < 800) return 14;
else if (200 <= len && len < 400) return 21;
else if (140 <= len && len < 200) return 28;
else if (110 <= len && len < 140) return 35;
else return 365;
}
// 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:
const data = [
{ // this defines the actual time "Line" bar
key: "timeline", category: "Line",
lineSpacing: 30, // distance between timeline and event nodes
length: 700, // the width of the timeline
start: new Date("1 Jan 2016"),
end: new Date("31 Dec 2016")
},
// 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
{ event: "New Year's Day", date: new Date("1 Jan 2016") },
{ event: "MLK Jr. Day", date: new Date("18 Jan 2016") },
{ event: "Presidents Day", date: new Date("15 Feb 2016") },
{ event: "Memorial Day", date: new Date("30 May 2016") },
{ event: "Independence Day", date: new Date("4 Jul 2016") },
{ event: "Labor Day", date: new Date("5 Sep 2016") },
{ event: "Columbus Day", date: new Date("10 Oct 2016") },
{ event: "Veterans Day", date: new Date("11 Nov 2016") },
{ event: "Thanksgiving", date: new Date("24 Nov 2016") },
{ event: "Christmas", date: new Date("25 Dec 2016") }
];
// prepare the model by adding links to the Line
for (let i = 0; i < data.length; i++) {
const d = data[i];
if (d.key !== "timeline") d.parent = "timeline";
}
myDiagram.model = new go.TreeModel( { nodeDataArray: data });
}
function timelineDays() {
const timeline = myDiagram.model.findNodeDataForKey("timeline");
const startDate = timeline.start;
const endDate = timeline.end;
function treatAsUTC(date) {
const result = new Date(date);
result.setMinutes(result.getMinutes() - result.getTimezoneOffset());
return result;
}
const millisecondsPerDay = 24 * 60 * 60 * 1000;
return (treatAsUTC(endDate) - treatAsUTC(startDate)) / millisecondsPerDay;
}
// 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.
class TimelineLayout extends go.Layout {
doLayout(coll) {
const diagram = this.diagram;
if (diagram === null) return;
coll = this.collectParts(coll);
diagram.startTransaction("TimelineLayout");
let line = null;
const parts = [];
const it = coll.iterator;
while (it.next()) {
const part = it.value;
if (part instanceof go.Link) continue;
if (part.category === "Line") { line = part; continue; }
parts.push(part);
let d = part.data.date;
if (d === undefined) { d = new Date(); part.data.date = d; }
}
if (!line) throw Error("No node of category 'Line' for TimelineLayout");
line.location = new go.Point(0, 0);
// lay out the events above the timeline
if (parts.length > 0) {
// determine the offset from the main shape to the timeline's boundaries
const main = line.findMainElement();
const sw = main.strokeWidth;
const mainOffX = main.actualBounds.x;
const mainOffY = main.actualBounds.y;
// spacing is between the Line and the closest Nodes, defaults to 30
let spacing = line.data.lineSpacing;
if (!spacing) spacing = 30;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const bnds = part.actualBounds;
const dt = part.data.date;
const val = dateToValue(dt);
const pt = line.graduatedPointForValue(val);
const tempLoc = new go.Point(pt.x, pt.y - bnds.height / 2 - spacing);
// check if this node will overlap with previously placed events, and offset if needed
for (let j = 0; j < i; j++) {
const partRect = new go.Rect(tempLoc.x, tempLoc.y, bnds.width, bnds.height);
const otherLoc = parts[j].location;
const otherBnds = parts[j].actualBounds;
const otherRect = new go.Rect(otherLoc.x, otherLoc.y, otherBnds.width, otherBnds.height);
if (partRect.intersectsRect(otherRect)) {
tempLoc.offset(0, -otherBnds.height - 10);
j = 0; // now that we have a new location, we need to recheck in case we overlap with an event we didn't overlap before
}
}
part.location = tempLoc;
}
}
diagram.commitTransaction("TimelineLayout");
}
}
// end TimelineLayout class
// This custom Link class was adapted from several of the samples
class BarLink extends go.Link {
getLinkPoint(node, port, spot, from, ortho, othernode, otherport) {
const r = port.getDocumentBounds();
const op = otherport.getDocumentPoint(go.Spot.Center);
const main = node.category === "Line" ? node.findMainElement() : othernode.findMainElement();
const mainOffY = main.actualBounds.y;
let 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);
}
}
}
// end BarLink class
function valueToDate(n) {
const timeline = myDiagram.model.findNodeDataForKey("timeline");
const startDate = timeline.start;
const startDateMs = startDate.getTime() + startDate.getTimezoneOffset() * 60000;
const msPerDay = 24 * 60 * 60 * 1000;
const date = new Date(startDateMs + n * msPerDay);
return date.toLocaleDateString();
}
function dateToValue(d) {
const timeline = myDiagram.model.findNodeDataForKey("timeline");
const startDate = timeline.start;
const startDateMs = startDate.getTime() + startDate.getTimezoneOffset() * 60000;
const dateInMs = d.getTime() + d.getTimezoneOffset() * 60000;
const msSinceStart = dateInMs - startDateMs;
const msPerDay = 24 * 60 * 60 * 1000;
return msSinceStart / msPerDay;
}
window.addEventListener('DOMContentLoaded', init);
</script>
<div id="sample">
<div id="myDiagramDiv" style="border: solid 1px black; background: #252526; width:100%; height:400px"></div>
<p>
This sample demonstrates an example usage of a <a href="../intro/graduatedPanels.html">Graduated Panel</a> to draw ticks and text labels along a timeline.
</p>
<p>
The Panel uses a <a>Panel.graduatedTickUnit</a> of 1 to represent one day, and ticks are drawn at <a>Shape.interval</a>s of 7 to represent weeks.
</p>
<p>
Labels are drawn at <a>TextBlock.interval</a>s of 14, or every two weeks. As the timeline is resized, the interval is updated to prevent overlaps.
Text strings are generated by setting the <a>TextBlock.graduatedFunction</a>to convert from values in the graduated range to date strings.
Also notice that labels use the <a>GraphObject.alignmentFocus</a>, <a>GraphObject.segmentOrientation</a>,
and <a>GraphObject.segmentOffset</a> properties to place text below the timeline bar.
</p>
<p>
Try resizing the timeline: select the timeline and drag the resize handle that is on the right side. Event nodes
will automatically be laid out relative to the timeline using the <code>TimelineLayout</code>. TimelineLayout converts
a date to a value, then uses <a>Panel.graduatedPointForValue</a> to help determine where event nodes will be placed.
</p>
</div>
</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>