create-gojs-kit
Version:
A CLI for downloading GoJS samples, extensions, and docs
566 lines (505 loc) • 23.7 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." />
<meta itemprop="description" content="A stretchable timeline." />
<meta property="og:description" content="A stretchable timeline." />
<meta name="twitter:description" content="A stretchable timeline." />
<link rel="preconnect" href="https://rsms.me/">
<link rel="stylesheet" href="../assets/css/style.css">
<!-- Copyright 1998-2025 by Northwoods Software Corporation. -->
<meta itemprop="name" content="Horizontal Timeline with Events Laid Out Avoiding Overlaps" />
<meta property="og:title" content="Horizontal Timeline with Events Laid Out Avoiding Overlaps" />
<meta name="twitter:title" content="Horizontal Timeline with Events Laid Out Avoiding Overlaps" />
<meta property="og:image" content="https://gojs.net/latest/assets/images/screenshots/timeline.png" />
<meta itemprop="image" content="https://gojs.net/latest/assets/images/screenshots/timeline.png" />
<meta name="twitter:image" content="https://gojs.net/latest/assets/images/screenshots/timeline.png" />
<meta property="og:url" content="https://gojs.net/latest/samples/timeline.html" />
<meta property="twitter:url" content="https://gojs.net/latest/samples/timeline.html" />
<meta name="twitter:card" content="summary_large_image" />
<meta property="og:type" content="website" />
<meta property="twitter:domain" content="gojs.net" />
<title>
Horizontal Timeline with Events Laid Out Avoiding Overlaps | GoJS Diagramming Library
</title>
</head>
<body>
<!-- This top nav is not part of the sample code -->
<nav id="navTop" class=" w-full h-[var(--topnav-h)] z-30 bg-white border-b border-b-gray-200">
<div class="max-w-screen-xl mx-auto flex flex-wrap items-start justify-between px-4">
<a class="text-white bg-nwoods-primary font-bold !leading-[calc(var(--topnav-h)_-_1px)] my-0 px-2 text-4xl lg:text-5xl logo"
href="../">
GoJS
</a>
<div class="relative">
<button id="topnavButton" class="h-[calc(var(--topnav-h)_-_1px)] px-2 m-0 text-gray-900 bg-inherit shadow-none md:hidden hover:!bg-inherit hover:!text-nwoods-accent hover:!shadow-none" aria-label="Navigation">
<svg class="h-7 w-7 block" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
<path d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div id="topnavList" class="hidden md:block">
<div class="absolute right-0 z-30 flex flex-col items-end rounded border border-gray-200 p-4 pl-12 shadow bg-white text-gray-900 font-semibold
md:flex-row md:space-x-4 md:items-start md:border-0 md:p-0 md:shadow-none md:bg-inherit">
<a href="../learn/">Learn</a>
<a href="../samples/">Samples</a>
<a href="../intro/">Intro</a>
<a href="../api/">API</a>
<a href="../download.html">Download</a>
<a href="https://forum.nwoods.com/c/gojs/11" target="_blank" rel="noopener">Forum</a>
<a id="tc" href="https://nwoods.com/contact.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/contact.html', 'contact');">Contact</a>
<a id="tb" href="https://nwoods.com/sales/index.html"
target="_blank" rel="noopener" onclick="getOutboundLink('https://nwoods.com/sales/index.html', 'buy');">Buy</a>
</div>
</div>
</div>
</div>
</nav>
<script>
window.addEventListener("DOMContentLoaded", function () {
// topnav
var topButton = document.getElementById("topnavButton");
var topnavList = document.getElementById("topnavList");
if (topButton && topnavList) {
topButton.addEventListener("click", function (e) {
topnavList
.classList
.toggle("hidden");
e.stopPropagation();
});
document.addEventListener("click", function (e) {
// if the clicked element isn't the list, close the list
if (!topnavList.classList.contains("hidden") && !e.target.closest("#topnavList")) {
topButton.click();
}
});
// set active <a> element
var url = window
.location
.href
.toLowerCase();
var aTags = topnavList.getElementsByTagName('a');
for (var i = 0; i < aTags.length; i++) {
var lowerhref = aTags[i]
.href
.toLowerCase();
if (lowerhref.endsWith('.html'))
lowerhref = lowerhref.slice(0, -5);
if (url.startsWith(lowerhref)) {
aTags[i]
.classList
.add('active');
break;
}
}
}
});
</script>
<div class="flex flex-col prose">
<div class="w-full max-w-screen-xl mx-auto">
<!-- * * * * * * * * * * * * * -->
<!-- Start of GoJS sample code -->
<script src="https://cdn.jsdelivr.net/npm/gojs@3.1.0"></script>
<div id="allSampleContent" class="p-4 w-full">
<script id="code">
// 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 {
constructor(init) {
super();
if (init) Object.assign(this, init);
}
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 {
constructor(init) {
super();
if (init) Object.assign(this, init);
}
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;
}
function init() {
myDiagram = new go.Diagram('myDiagramDiv', {
'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: new 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 =
new go.Node('Table', { locationSpot: go.Spot.Center, movable: false })
.add(
new go.Panel('Auto')
.add(
new go.Shape('RoundedRectangle', {
fill: '#252526',
stroke: '#519ABA',
strokeWidth: 3
}),
new go.Panel('Table')
.add(
new go.TextBlock({
row: 0,
stroke: '#CCCCCC',
wrap: go.Wrap.Fit,
font: 'bold 12pt sans-serif',
textAlign: 'center',
margin: 4
})
.bind('text', 'event'),
new go.TextBlock({
row: 1,
stroke: '#A074C4',
textAlign: 'center',
margin: 4
})
.bind('text', 'date', d => d.toLocaleDateString())
)
)
);
myDiagram.nodeTemplateMap.add('Line',
new go.Node('Graduated', {
movable: false,
copyable: false,
resizable: true,
resizeObjectName: 'MAIN',
background: 'transparent',
graduatedMin: 0,
graduatedMax: 365,
graduatedTickUnit: 1,
// only resizing at right end
resizeAdornmentTemplate:
new go.Adornment('Spot')
.add(
new go.Placeholder(),
new go.Shape({
alignment: go.Spot.Right,
cursor: 'e-resize',
desiredSize: new go.Size(4, 16),
fill: 'lightblue',
stroke: 'deepskyblue'
})
)
})
.bind('graduatedMax', '', timelineDays)
.add(
new go.Shape('LineH', {
name: 'MAIN',
stroke: '#519ABA',
height: 1,
strokeWidth: 3
})
.bindTwoWay('width', 'length',
(l, shape) => l * (shape.diagram.model.modelData.scale || 1.0),
(w, data, model) => w / (model.modelData.scale || 1.0)
),
new go.Shape({
geometryString: 'M0 0 V10',
interval: 7,
stroke: '#519ABA',
strokeWidth: 2
}),
new go.TextBlock({
font: '10pt sans-serif',
stroke: '#CCCCCC',
interval: 14,
alignmentFocus: go.Spot.Right,
segmentOrientation: go.Orientation.Minus90,
segmentOffset: new go.Point(0, 12),
graduatedFunction: valueToDate
})
.bind('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 =
new BarLink({ toShortLength: 2, layerName: 'Background' }) // defined below
.add(
new 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;
}
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>
<p>
We have many other timeline samples available demonstrating various features.
</p>
</div>
</div>
<!-- * * * * * * * * * * * * * -->
<!-- End of GoJS sample code -->
</div>
<div id="allTagDescriptions" class="p-4 w-full max-w-screen-xl mx-auto">
<hr/>
<h3 class="text-xl">GoJS Features in this sample</h3>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Table Panels</h4>
<p>
The "Table" Panel, <a href="../api/symbols/Panel.html#static-Table" target="api">Panel.Table</a>, arranges objects in rows and columns.
Each object in a Table Panel is put into the cell indexed by the value of <a href="../api/symbols/GraphObject.html#row" target="api">GraphObject.row</a> and <a href="../api/symbols/GraphObject.html#column" target="api">GraphObject.column</a>.
The panel will look at the rows and columns for all of the objects in the panel to determine how many rows and columns the table should have.
More information can be found in the <a href="../intro/tablePanels.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#tables">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Item Arrays</h4>
<p>
It is sometimes useful to display a variable number of elements in a node by data binding to a JavaScript Array.
In GoJS, this is simply achieved by binding (or setting) <a href="../api/symbols/Panel.html#itemArray" target="api">Panel.itemArray</a>.
The <a href="../api/symbols/Panel.html" target="api">Panel</a> will create an element in the panel for each value in the Array.
More information can be found in the <a href="../intro/itemArrays.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#itemarrays">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Links</h4>
<p>
The <a href="../api/symbols/Link.html" target="api">Link</a> class is used to implement a visual relationship between nodes.
Links are normally created by the presence of link data objects in the <a href="../api/symbols/GraphLinksModel.html#linkDataArray" target="api">GraphLinksModel.linkDataArray</a>
or by a parent key reference as the value of the <a href="../api/symbols/TreeModel.html#nodeParentKeyProperty" target="api">TreeModel.nodeParentKeyProperty</a> of a node data object
in a <a href="../api/symbols/TreeModel.html" target="api">TreeModel</a>.
More information can be found in the <a href="../intro/links.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#links">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Custom Layouts</h4>
<p>
GoJS allows for the creation of custom layouts to meet specific needs.
</p>
<p>
There are also many layouts that are extensions -- not predefined in the <code>go.js</code> or <code>go-debug.js</code> library,
but available as source code in one of the three extension directories, with some documentation and corresponding samples.
More information can be found in the <a href="../intro/layouts.html#CustomLayouts">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#customlayout">Related samples</a>
</p>
<hr>
<!-- blacklist tags that do not correspond to a specific GoJS feature -->
<h4>Commands</h4>
<p>
A <a href="../api/symbols/CommandHandler.html" target="api">CommandHandler</a> handles all default keyboard input events in a Diagram.
There are many predefined methods on <a>CommandHandler</a> that implement common commands to operate on the Diagram or the current <a>Diagram.selection></a>.
</p>
<p>
You can override <a>CommandHandler.doKeyDown</a> to handle additional keyboard shortcuts or to change which commands are invoked via the keyboard.
<p>
Your code can invoke a command by calling the appropriate method on the <a>Diagram.commandHandler</a>.
Each command method has a corresponding <b>can...</b> predicate that your code can use to enable or disable any buttons that invoke the command.
Your code can customize the behavior of a command by overriding the method on <a>CommandHandler</a>,
or by setting properties on the <a>CommandHandler</a> or <a>Diagram</a> or <a>Part</a>s --
see <a href="../intro/permissions.html">GoJS Permissions</a>.
</p>
<p>
There are several CommandHandler extensions that provide additional functionality.
<p>
More information can be found in the <a href="../intro/commands.html">GoJS Intro</a>.
</p>
<p>
<a href="../samples/index.html#commands">Related samples</a>
</p>
<hr>
</div>
</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>