The VisualScript Cookbook

17/18 Page

Creating a Timeline with VisualScript

You can add a Timeline to any shape using VisualScript. A Timeline is a specialized object that shows a period of time as a special kind of Table and then positions an array of Events on it.

{
var myDocument=new VS.Document();
var rootShape=myDocument.GetTheShape();
var myTimeline=rootShape.AddTimeline();
var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

A Shape that contains a Timeline cannot also contain a Table.

There are two types of Timeline: Row and Grid.

Row timelines show a date range with "bubble" type event shapes attached to it for each event, like this:

Bubble timeline

Grid timelines show a date range with additional rows below it. The event shapes are horizontal and lie on the rows, like this:

Grid timeline

Grid timeline events can show the duration of an event based on their length. Bubble events show only the "start" of the event.

Creating a Row Timeline

You create a Row timeline by adding a timeline to a shape with the Row1 arrangement:

{
myTimeline=rootShape.AddTimeline(VS.Timeline_Arrangements.Row1);
}

This set up: VS.Timeline_Arrangements.Row1 is also the default if no arrangement is passed to AddTimeline()

In addition to setting the layout of the timeline, Row1 also sets up a default color scheme and design for the events.

Adding a timeline to a shape resizes it to 12 inches wide and tall enough to accommodate the timeline.

Once the timeline is added to a shape, the next step is to add the events to the timeline using the Timeline's AddEvent() method:

{
myEvent.=myTimeline.AddEvent(datestring);
}

The datestring is the date that the event happened (or began). It has a "YYYY-MM-DD" format. If the datestring is null, today becomes the default date for the event.

Creating and populating a timeline follows this pattern:

{
var myDocument=new VS.Document();
var rootShape=myDocument.GetTheShape();
var myTimeline=rootShape.AddTimeline();
myTimeline.AddEvent("2019-3-4").SetLabel("One");
myTimeline.AddEvent("2019-3-7").SetLabel("Two");
myTimeline.AddEvent("2019-3-10").SetLabel("Two-A");
myTimeline.AddEvent("2019-8-10").SetLabel("Three");
myTimeline.AddEvent("2019-11-4").SetLabel("Four");
myTimeline.AddEvent("2019-11-14").SetLabel("Four-A");
myTimeline.AddEvent("2019-12-1").SetLabel("Five");
var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

Add events to timeline

Note that the events are set to alternate above and below the timeline by default. Also notice other defaults:

  • The events are bubble type
  • They have a circular shape as the bubble.
  • They touch the timeline along its center line.

You can override any of these defaults.

Setting the Type of Event

The SetEventType(eventtype) method of a timeline allows you to set the event type to be any of the following:

VS.Timeline_EventTypes =
{
Bubble: "Bubble",
BubbleVertical: "Bubble-Vertical",
BubbleTextOnly: "TextOnly",
GridBullet: "Grid-Bullet",
GridBar: "Grid-Bar",
};

This illustrates the three types of bubble event:

Set event type

Setting How the Event Touches the Timeline

The SetEventPosition(position) method of a timeline allows you to set where the line part of the bubble event touches the timeline to be any of the following:

VS.Timeline_BubbleEventPositions =
{
Above: "above",
AboveCenter: "above-center",
Below: "below",
BelowCenter: "below-center",
Alternate: "alternate",
AlternateCenter: "alternate-center",
};

Above and Below place the events so they connect at the top or bottom edge of the Timeline. AboveCenter and BelowCenter both connect to the center position of the Timeline. Set event position

Changing the Shape and Appearance of the Bubble

Timelines have a DefaultShape. It determines the settings for shape at the end of the bubble event.

This code changes the events to use a rectangle as the bubble shape, sets its fill color to yellow and sets its border to a 2/100" black line.

{
var myDocument=new VS.Document();
var rootShape=myDocument.GetTheShape();
var myTimeline=rootShape.AddTimeline();
var eventDefaultShape=myTimeline.AddDefaultShape();
eventDefaultShape.SetShapeType(VS.ShapeTypes.Rectangle).SetFillColor("#FFFF00").SetBorderThickness(2);
myTimeline.AddEvent("2019-3-4").SetLabel("One");
myTimeline.AddEvent("2019-3-7").SetLabel("Two");
myTimeline.AddEvent("2019-3-10").SetLabel("Two-A");
myTimeline.AddEvent("2019-8-10").SetLabel("Three");
myTimeline.AddEvent("2019-11-4").SetLabel("Four");
myTimeline.AddEvent("2019-11-14").SetLabel("Four-A");
myTimeline.AddEvent("2019-12-1").SetLabel("Five");
var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

Change appearance of bubble events

You can override the defaultshape and other timeline settings for individual events:

{
var myDocument=new VS.Document();
var rootShape=myDocument.GetTheShape();
var myTimeline=rootShape.AddTimeline();
var eventDefaultShape=myTimeline.AddDefaultShape();
eventDefaultShape.SetShapeType(VS.ShapeTypes.Rectangle).SetFillColor("#FFFF00").SetBorderThickness(2);
myTimeline.AddEvent("2019-3-4").SetLabel("One").SetEventType(VS.Timeline_EventTypes.BubbleVertical);
myTimeline.AddEvent("2019-3-7").SetLabel("Two");
myTimeline.AddEvent("2019-3-10").SetLabel("Two-A").SetEventPosition(VS.Timeline_BubbleEventPositions.Below);
myTimeline.AddEvent("2019-8-10").SetLabel("Three").SetShapeType(VS.ShapeTypes.Oval).SetFillColor("#00FF00").SetLineColor("#0000FF");
myTimeline.AddEvent("2019-11-4").SetLabel("Four");
myTimeline.AddEvent("2019-11-14").SetLabel("Four-A");
myTimeline.AddEvent("2019-12-1").SetLabel("Five");
var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

Change appearance of bubble events

You can even add a table to the shape on the end of the bubble and show multiple values and hyperlinks.

{
var myDocument=new VS.Document();
var rootShape=myDocument.GetTheShape();
var myTimeline=rootShape.AddTimeline();
var eventDefaultShape=myTimeline.AddDefaultShape();
var defaultTable=eventDefaultShape.AddTable(3,2);
eventDefaultShape.SetShapeType(VS.ShapeTypes.Rectangle);
var myEvent=myTimeline.AddEvent("2019-3-4").SetLabel("One");
myTable=myEvent.AddTable();  
myTable.AddCell(1,2).SetLabel("More Data"); //add another label to the first event
myTimeline.AddEvent("2019-3-7").SetLabel("Two");
myTimeline.AddEvent("2019-3-10").SetLabel("Two-A");
myTimeline.AddEvent("2019-8-10").SetLabel("Three");
myTimeline.AddEvent("2019-11-4").SetLabel("Four");
myTimeline.AddEvent("2019-11-14").SetLabel("Four-A");
myTimeline.AddEvent("2019-12-1").SetLabel("Five");
var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

Add table to events

Intelligent Formatting

Like everything else in VisualScript, timelines use intelligent formatting. You don't need to set the date range of the timeline, specify the exact placing for events, or worry about preventing events from overlapping. VisualScript's intelligent formatting engine takes care of this.

The scale of the timeline is calculated based on the date range of events and the length of the shape that contains the timeline. VisualScript tries to show the smallest division of time it can that still fit all of the events. The columns are labeled automatically. If we take the timeline above and shrink it to about 4 inches the scale changes to quarters:

Intelligent formatting

VisualScript places events on the timeline exactly where they are supposed to be and adjusts the length of the lines connecting the bubble shape to the timeline to avoid overlapping shapes that are close together.

Changing the Scale and Appearance of a Timeline

Setting a Fixed Scale

You can turn off the automatic selection of a range and scale using the SetAuto() method for a timeline.

{
myTimeline.SetAuto(false);
}

If you do this, you can use additional Timeline methods to set the starting and ending date of the Timelinee and its units (days, months, years etc).

If the starting date and ending dates conflict with the units, the units are adjusted. For example, if the starting and ending dates differ by one year and the units are set to days, unless the Timeline shape is wide enough to accommodate 365 columns, the units will be adjusted to the smallest division that will fit.

The following methods are used to set the starting date etc for a Timeline with no events, or one with Auto=false.

  • SetStart() - Sets the starting date for the Timeline
  • SetDuration() - Sets the length in days between the starting dates and ending dates.
  • SetUnits() - Sets the units for the Timeline. This is one of the values from VS.TimelineUnits

Events that occur on dates outside the range of a fixed scale Timeline do not appear.

{
var myDocument=new VS.Document();
var rootShape=myDocument.GetTheShape();
var myTimeline=rootShape.AddTimeline();
myTimeline.SetAuto(false).SetStart("2019-1-1").SetDuration(200).SetUnits(VS.TimelineUnits.Week);
myTimeline.AddEvent("2019-3-4").SetLabel("One");
myTimeline.AddEvent("2019-3-7").SetLabel("Two");
myTimeline.AddEvent("2019-3-10").SetLabel("Two-A");
myTimeline.AddEvent("2019-8-10").SetLabel("Three");
myTimeline.AddEvent("2019-11-4").SetLabel("Four");
myTimeline.AddEvent("2019-11-14").SetLabel("Four-A");
myTimeline.AddEvent("2019-12-1").SetLabel("Five");
var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

Changing scale

Note that the range of 200 days was too long to accommodate the desired scale of "weeks". Note also that the events after July are skipped because they don't fit in the range of the timeline.

Setting a fixed scale is most useful when you know the expected range of your events and want a specific scale that will fit. For example switching the scale to "Years" and the duration to 365 gives this result.

Set the scale to fixed

Changing the Width

The width of a shape that contains a timeline is 12 inches by default. You can override this by setting the MinWidth of the shape containing the timeline.

{
myShape.SetMinWidth(600);
}

The minimum width for a timeline is 4 inches.

Changing the Height

The height of a timeline is determined by its row height, which is determined by the font and size of the text used to show the labels. This is Arial 11 point by default. You can override these defaults using the methods that set these properties for the parent shape of the timeline.

{
myShape.SetTextSize(16);
}

Creating Timelines That Use Time of Day

By default, an event starts at 00:00 on its start date. You can set a specific time using the SetStartTime(timestring) method for an event. The timestring is a string in HH:MM:SS format.

{
var myDocument=new VS.Document();
var rootShape=myDocument.GetTheShape();
var myTimeline=rootShape.AddTimeline();
myTimeline.AddDefaultShape().SetMinHeight(100);
myTimeline.AddEvent("2019-3-4").SetLabel("System Failure").SetStartTime("04:00:00");
myTimeline.AddEvent("2019-3-4").SetLabel("Tech notified").SetStartTime("04:30:00");
myTimeline.AddEvent("2019-3-4").SetLabel("Tech arrived").SetStartTime("05:45:00");
myTimeline.AddEvent("2019-3-4").SetLabel("Fail over").SetStartTime("06:08:00");
myTimeline.AddEvent("2019-3-4").SetLabel("Issue Resolved").SetStartTime("07:30:00");
myTimeline.AddEvent("2019-3-4").SetLabel("Server restored to cluster").SetStartTime("08:12:00");
var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

Time of day timeline

Creating a Grid Timeline

You create a Grid timeline by passing VS.Timeline_Arrangements.Grid1 to the AddTimeline() method.

{
var myDocument=new VS.Document();
var rootShape=myDocument.GetTheShape();
var myTimeline=rootShape.AddTimeline(VS.Timeline_Arrangements.Grid1);
var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

By default Grid timelines use bars for events. They can also use bullets. The events lie on one of the grid's rows.

You can add multiple rows to a Grid timeline using the AddGridRow() method. AddGridRow takes a label for the row as an argument. The label is shown in the left column of the grid.

{
var myDocument=new VS.Document();
var rootShape=myDocument.GetTheShape();
var myTimeline=rootShape.AddTimeline(VS.Timeline_Arrangements.Grid1);
myTimeline.AddGridRow("IT Department");
myTimeline.AddGridRow("Software");
var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

Grid timeline

Adding events creates bars where the length indicates the duration of the event.

{
var myDocument=new VS.Document();
var rootShape=myDocument.GetTheShape();
var myTimeline=rootShape.AddTimeline(VS.Timeline_Arrangements.Grid1);
myTimeline.AddGridRow("IT Department");
myTimeline.AsddGridRow("Software");
myTimeline.AddEvent("2019-3-4").SetLabel("One").SetDuration(20);
myTimeline.AddEvent("2019-3-7").SetLabel("Two").SetDuration(50);
myTimeline.AddEvent("2019-8-10").SetLabel("Three").SetDuration(30);
myTimeline.AddEvent("2019-11-4").SetLabel("Four").SetDuration(10);
myTimeline.AddEvent("2019-12-1").SetLabel("Five").SetDuration(20);
var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

Grid timeline events

Note that the events are placed in the first row by default and that intelligent formatting keeps the bars from overlapping by growing the height of a row and placing the two bars at different vertical positions.

We can assign the events to specific rows using the SetEventPosition() method with the row number as the argument:

{
var myDocument=new VS.Document();
var rootShape=myDocument.GetTheShape();
var myTimeline=rootShape.AddTimeline(VS.Timeline_Arrangements.Grid1);
myTimeline.AddGridRow("IT Department");
myTimeline.AddGridRow("Software");
myTimeline.AddEvent("2019-3-4").SetLabel("One").SetDuration(20).SetEventPosition(1);
myTimeline.AddEvent("2019-3-7").SetLabel("Two").SetDuration(50).SetEventPosition(2);
myTimeline.AddEvent("2019-8-10").SetLabel("Three").SetDuration(30).SetEventPosition(1);
myTimeline.AddEvent("2019-11-4").SetLabel("Four").SetDuration(10).SetEventPosition(1);
myTimeline.AddEvent("2019-12-1").SetLabel("Five").SetDuration(20).SetEventPosition(2);
var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

Set position for grid timeline events

You can replace the bar events with bullets by setting the EventType of the timeline to bullets:

{
var myDocument=new VS.Document();
var rootShape=myDocument.GetTheShape();
var myTimeline=rootShape.AddTimeline(VS.Timeline_Arrangements.Grid1);
myTimeline.AddGridRow("IT Department");
myTimeline.AddGridRow("Software");
myTimeline.SetEventType(VS.Timeline_EventTypes.GridBullet);
myTimeline.AddEvent("2019-3-4").SetLabel("One").SetDuration(20).SetEventPosition(1);
myTimeline.AddEvent("2019-3-7").SetLabel("Two").SetDuration(50).SetEventPosition(2);
myTimeline.AddEvent("2019-8-10").SetLabel("Three").SetDuration(30).SetEventPosition(1);
myTimeline.AddEvent("2019-11-4").SetLabel("Four").SetDuration(10).SetEventPosition(1);
myTimeline.AddEvent("2019-12-1").SetLabel("Five").SetDuration(20).SetEventPosition(2);
var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

Bullet type events

You can set the type at the timeline level as the default, like above, or set the EventType of individual events to override the default.

Like bubble events you can change the appearance of the bullets or bars by using the DefaultShape of the timeline, or by setting properties like colors and thicknesses on the event object itself.

Grid timelines can also show hours and minutes just like Row timelines.

Block Timelines

The Block Timeline is a variant of a Grid Timeline that uses Block Events by default. You can create a Block Timeline by using VS.Timeline_Arrangements.GridBlock1 as the timeline type in the AddTimeline(); method.

{
var myDocument = new VS.Document(); //Create a new VisualScript document

var theShape = myDocument.GetTheShape();
var myTimeline=theShape.AddTimeline(VS.Timeline_Arrangements.GridBlock1);
myTimeline.AddGridRow("Team 1");
var event1=myTimeline.AddEvent();
event1.SetStart("2019-07-1").SetLabel("Sprint-1").SetDuration(28);

var event2=myTimeline.AddEvent();
event2.SetStart("2019-08-1").SetLabel("Sprint-2").SetDuration(28);

var event3=myTimeline.AddEvent();
event3.SetStart("2019-09-1").SetLabel("Sprint-3").SetDuration(28);

var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}


Block timeline

The events on a Block Timeline have a table format by default. You can set the label of the event such as the Sprint name and then add a label that names the issue in the second row:

{
var myTable=event1.AddTable();
myTable.AddCell(2,1).SetLabel("Issues 2011-2095");
}

Here's the full script:

{
var myDocument = new VS.Document(); //Create a new VisualScript document

var theShape = myDocument.GetTheShape();
var myTimeline=theShape.AddTimeline(VS.Timeline_Arrangements.GridBlock1);
myTimeline.AddGridRow("Team 1");
var event1=myTimeline.AddEvent();
event1.SetStart("2019-07-1").SetLabel("Sprint-1").SetDuration(28);
var myTable=event1.AddTable();
myTable.AddCell(2,1).SetLabel("Issues 2011-2095");

var event2=myTimeline.AddEvent();
event2.SetStart("2019-08-1").SetLabel("Sprint-2").SetDuration(28);
myTable=event2.AddTable();
myTable.AddCell(2,1).SetLabel("Issues 2100-2200");

var event3=myTimeline.AddEvent();
event3.SetStart("2019-09-1").SetLabel("Sprint-3").SetDuration(28);
myTable=event3.AddTable();
myTable.AddCell(2,1).SetLabel("Issues 2200-2300");

var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

Block timeline events with labels

Swimlane Timelines

The Swimlane Timeline is a variant of a Grid Timeline that uses Swimlane Events by default. You can create a Swimlane Timeline by using VS.Timeline_Arrangements.GridSwimlane1 as the timeline type in the AddTimeline(); method.

Swimlane events are meant to serve as a backdrop for other (usually block) events. They are placed behind a transparent timeline and do not try to avoid overlapping each other or other events. They are an ideal way to represent the program increments in a product roadmap. Swimlane events also automatically extend to the bottom of the timeline.

Adding a block event to a swimlane timeline ignores the DefaultShape that controls the design of the swimlanes and uses the default design of events for the Block Timeline. You can override this by defining your own table. You can remove the table by adding a table with 0 rows.

{
var myDocument = new VS.Document(); //Create a new VisualScript document

var theShape = myDocument.GetTheShape();
var myTimeline=theShape.AddTimeline(VS.Timeline_Arrangements.GridSwimlane1);

//set a fixed scale and add the rows
myTimeline.SetAuto(false).SetUnits(VS.TimelineUnits.Quarter).SetStart("2019-07-01").SetDuration(365);

myTimeline.AddGridRow();
myTimeline.AddRowProperties(2).SetHeight(100);
myTimeline.AddGridRow("Team 1",VS.Timeline_RowTypes.LabelRow);
myTimeline.AddGridRow();
myTimeline.AddRowProperties(4).SetHeight(100);
myTimeline.AddGridRow("Team 2",VS.Timeline_RowTypes.LabelRow);
myTimeline.AddGridRow();
myTimeline.AddRowProperties(6).SetHeight(100);
myTimeline.SetGridLabelColumn(false);

//add the swimlanes
var swimlane1=myTimeline.AddEvent();
swimlane1.SetStart("2019-07-1").SetLabel("PI-1").SetDuration(90);

var swimlane2=myTimeline.AddEvent();
swimlane2.SetStart("2019-10-1").SetLabel("PI-2").SetDuration(90);

var swimlane3=myTimeline.AddEvent();
swimlane3.SetStart("2020-01-1").SetLabel("PI-3").SetDuration(90);

var swimlane4=myTimeline.AddEvent();
swimlane4.SetStart("2020-04-1").SetLabel("PI-4").SetDuration(90);

//add the block events
var event1=myTimeline.AddEvent().SetEventType(VS.Timeline_EventTypes.GridBlock);
event1.SetStart("2019-07-1").SetLabel("Sprint-1").SetDuration(28).SetEventPosition(2);
var myTable=event1.AddTable(); //add a null table to use the table created by default for these events
myTable.AddCell(2,1).SetLabel("Issues 2011-2095"); //Set the second cell label

var event2=myTimeline.AddEvent().SetEventType(VS.Timeline_EventTypes.GridBlock);
event2.SetStart("2019-08-1").SetLabel("Sprint-2").SetDuration(28).SetEventPosition(2);
myTable=event2.AddTable();
myTable.AddCell(2,1).SetLabel("Issues 2100-2200");

var event3=myTimeline.AddEvent().SetEventType(VS.Timeline_EventTypes.GridBlock);
event3.SetStart("2019-09-1").SetLabel("Sprint-3").SetDuration(28).SetEventPosition(2);
myTable=event3.AddTable();
myTable.AddCell(2,1).SetLabel("Issues 2200-2300");

var vsJSON = myDocument.toJSON(); //turn the VisualScript object constructed using the API into a JSON string
vsCompleteCallback(vsJSON); //pass the JSON string into the callback to trigger the creation of a document.
}

Swimlane timeline

Title Rows

In the Swimlane Timeline example above instead of a title column at the left, the event rows have titles above them. This is achieved using two features of the SDK:

  • myTimeline.SetGridLabelColumn(false) - This hides the row label column when false.
  • myTimeline.AddGridRow(Label,VS.Timeline_RowTypes.LabelRow) - This adds a row with a label that spans the whole timeline. The row type VS.Timeline_RowTypes.LabelRow sets the title for the entire row. Normal rows are added with no type.

Creating Stacked Timelines on the Same Page

You can create a stack of timelines on the same page using a ShapeContainer:

{
myContainer=myDocument.AddShapeContainer(VS.ShapeContainerArrangement.Column);
myShape1=myContainer.AddShape();
myShape2=myContainer.AddShape();
myShape3=myContainer.AddShape();
}
Shapes container

Now add a timeline to each shape:

{
myTimeline1=myShape1.AddTimeline(VS.Timeline_Arrangements.Grid1);
myEvent1a=myTimeline1.AddEvent("2018-03-01");
myEvent1b=myTimeline1.AddEvent("2018-06-10");

myTimeline2=myShape2.AddTimeline(VS.Timeline_Arrangements.Grid1);
myEvent2a=myTimeline2.AddEvent("2018-8-10");
myEvent2b=myTimeline2.AddEvent("2018-11-10");

myTimeline3=myShape3.AddTimeline(VS.Timeline_Arrangements.Grid1);
myEvent3a=myTimeline2.AddEvent("2018-12-10");
myEvent3b=myTimeline2.AddEvent("2019-4-10");
}

We now get the same three shapes as Timelines. This also allows you to "wrap" a long timeline into multiple rows for easier display on a page or report.

Multiple timelines
17/18 Page