A timeline that winds up and down in six columns across the page, for a wide, short space. Four project modules keep their lanes and colors at every turn.
A serpentine timeline can fold into rows or into columns. Rows suit a tall space and read like text; columns, as here, suit a wide one, like a banner, a slide or a dashboard strip. The date labels turn with the path, so each one stays in its own lane.
Built with amCharts 5, a JavaScript library. The JavaScript includes your theme and renderer (canvas or SVG); the JSON also follows the changes you make with the controls.
npm install @amcharts/amcharts5. See Getting started.
The same chart as a JSON config. Chart Maker reads and writes this format, the library's JSON parser turns it into a chart, and the <am5-chart> element puts it on a page with no code."type" is the class to create and "settings" what you would pass to its new() in code; "properties" fill in its parts, lists such as series and axes or templates such as columns.template; "refs" holds objects used in more than one place ("#series-0" stands for the one named series-0); a string starting with "@" takes a value from another object, such as "@series.get('fill')".
A complete page. Save it as an .html file and open it in a browser.
Paste this into an AI coding assistant to build the chart from scratch. More in amCharts & AI.
var root = am5.Root.new("chartdiv");
// Set themes
// https://www.amcharts.com/docs/v5/concepts/themes/
root.setThemes([
am5themes_Animated.new(root),
am5themes_Responsive.new(root)
]);
// Create chart
var chart = root.container.children.push(am5timeline.SerpentineChart.new(root, {
orientation: "horizontal", // the runs stand side by side, across the chart
// time runs along a path that doubles back on itself to make 6 runs
levelCount: 6,
startLocation: 0.2, // the path starts 20% of the way along the first run...
endLocation: 1, // ...and goes to the very end of the last
// How much of each row the lanes take
yAxisRadius: am5.percent(70),
wheelY: "zoomX" // the mouse wheel zooms in on the dates
}));
chart.set("scrollbarX", am5.Scrollbar.new(root, {
orientation: "horizontal", // a scrollbar above the chart, to zoom and pan the dates
// The turns at the top reach past the plot area: keep them clear of the scrollbar
marginBottom: 50
}));
var yRenderer = am5timeline.AxisRendererCurveY.new(root, {
// The lanes are close together: let every name show
minGridDistance: 10,
// The dashed line with the dates runs along the middle of the third lane of six (the empty one)
axisLocation: 2.5 / 6,
// The rows run up and down, so the lane names turn with them
rotateLabels: true
});
yRenderer.labels.template.setAll({
centerY: am5.p50,
centerX: am5.p100,
fontSize: 11 // small lane names
});
yRenderer.grid.template.set("forceHidden", true); // no lane grid lines
// Create axes and their renderers
var xRenderer = am5timeline.AxisRendererCurveX.new(root, {
yRenderer: yRenderer, // the date axis follows the same curve as the lanes
// Date labels follow the path, so they stay in their own lane
rotateLabels: true,
strokeDasharray: [2, 2], // dashed: 2px dashes, 2px gaps...
strokeOpacity: 0.5, // ...half transparent...
stroke: root.interfaceColors.get("alternativeBackground") // ...dark on light backgrounds, light on dark ones
});
xRenderer.labels.template.setAll({
centerY: am5.p50, // centered on the dashed line
fontSize: 11, // small dates
minPosition: 0.01 // hide a label that would sit at the very start
});
// Date labels sit on the dashed line, with the chart's background color behind them
xRenderer.labels.template.setup = function(target) {
target.set("layer", 30); // above the bars
target.set("background", am5.Rectangle.new(root, {
fill: root.interfaceColors.get("background"),
fillOpacity: 1
}));
}
var yAxis = chart.yAxes.push(am5xy.CategoryAxis.new(root, {
maxDeviation: 0,
categoryField: "category",
renderer: yRenderer
}));
var xAxis = chart.xAxes.push(am5xy.DateAxis.new(root, {
baseInterval: { timeUnit: "day", count: 1 }, // one day per unit
renderer: xRenderer,
tooltip: am5.Tooltip.new(root, {}) // shows the date at the cursor
}));
// Data: each module has its own color, used by its bars and their end dots
var colorSet = chart.get("colors");
var module1 = { fill: colorSet.getIndex(0) };
var module2 = { fill: colorSet.getIndex(5) };
var module3 = { fill: colorSet.getIndex(9) };
var module4 = { fill: colorSet.getIndex(15) };
var data = [{
"category": "Module #1",
"start": new Date("2026-01-15").getTime(),
"end": new Date("2026-01-18").getTime(),
"settings": module1,
"task": "Gathering requirements"
}, {
"category": "Module #1",
"start": new Date("2026-02-10").getTime(),
"end": new Date("2026-04-23").getTime(),
"settings": module1,
"task": "Development"
}, {
"category": "Module #2",
"start": new Date("2026-01-13").getTime(),
"end": new Date("2026-01-15").getTime(),
"settings": module2,
"task": "Gathering requirements"
}, {
"category": "Module #2",
"start": new Date("2026-01-17").getTime(),
"end": new Date("2026-01-20").getTime(),
"settings": module2,
"task": "Producing specifications"
}, {
"category": "Module #2",
"start": new Date("2026-01-21").getTime(),
"end": new Date("2026-02-10").getTime(),
"settings": module2,
"task": "Development"
}, {
"category": "Module #2",
"start": new Date("2026-02-15").getTime(),
"end": new Date("2026-02-23").getTime(),
"settings": module2,
"task": "Testing and QA"
}, {
"category": "Module #3",
"start": new Date("2026-01-06").getTime(),
"end": new Date("2026-01-24").getTime(),
"settings": module3,
"task": "Gathering requirements"
}, {
"category": "Module #3",
"start": new Date("2026-02-06").getTime(),
"end": new Date("2026-02-15").getTime(),
"settings": module3,
"task": "Producing specifications"
}, {
"category": "Module #3",
"start": new Date("2026-03-15").getTime(),
"end": new Date("2026-04-20").getTime(),
"settings": module3,
"task": "Development"
}, {
"category": "Module #3",
"start": new Date("2026-04-25").getTime(),
"end": new Date("2026-05-05").getTime(),
"settings": module3,
"task": "Testing and QA"
}, {
"category": "Module #4",
"start": new Date("2026-01-20").getTime(),
"end": new Date("2026-02-17").getTime(),
"settings": module4,
"task": "Gathering requirements"
}, {
"category": "Module #4",
"start": new Date("2026-03-02").getTime(),
"end": new Date("2026-03-15").getTime(),
"settings": module4,
"task": "Development"
}, {
"category": "Module #4",
"start": new Date("2026-03-28").getTime(),
"end": new Date("2026-05-04").getTime(),
"settings": module4,
"task": "Testing and QA"
}];
// Add series
// https://www.amcharts.com/docs/v5/charts/xy-chart/series/
var series = chart.series.push(am5timeline.CurveColumnSeries.new(root, {
xAxis: xAxis,
yAxis: yAxis,
baseAxis: yAxis, // the bars run along the time path
valueXField: "end",
openValueXField: "start", // each bar starts at its start date
categoryYField: "category",
layer: 30
}));
// Hovering a bar or one of its end dots shows the task and its dates
var tooltipText = "{category}: {task}\n{openValueX.formatDate('MMM d')} to {valueX.formatDate('MMM d')}";
series.columns.template.setAll({
height: am5.percent(25), // bars a quarter as thick as their lane
strokeOpacity: 0, // no outline
// The module's color comes from the data
templateField: "settings",
tooltipText: tooltipText
});
// A dot at the start and at the end of each bar, in the bar's color
series.bullets.push(function(root, series, dataItem) {
return am5.Bullet.new(root, {
locationX: 0, // at the bar's start
sprite: am5.Circle.new(root, {
radius: 4, // 4px radius
templateField: "settings",
tooltipText: tooltipText,
layer: 30
})
});
});
series.bullets.push(function(root, series, dataItem) {
return am5.Bullet.new(root, {
locationX: 1, // at the bar's end
sprite: am5.Circle.new(root, {
radius: 4,
templateField: "settings",
tooltipText: tooltipText,
layer: 30
})
});
});
// Milestones: a lettered badge on its date, in a lane of their own
var lineSeries = chart.series.push(am5timeline.CurveLineSeries.new(root, {
xAxis: xAxis, yAxis: yAxis, categoryYField: "category", valueXField: "date"
}));
lineSeries.strokes.template.set("forceHidden", true); // only the badges, no line between them
lineSeries.bullets.push(function(root, series, dataItem) {
return am5.Bullet.new(root, {
sprite: am5.Label.new(root, {
text: "{letter}",
populateText: true, // fills in {letter} from the data
fontSize: 10, // small...
fontWeight: "bold", // ...bold letters...
centerX: am5.p50, // ...centered on the date
centerY: am5.p50,
paddingTop: 2, paddingBottom: 2, paddingLeft: 5, paddingRight: 5, // a little room around the letter
layer: 30,
background: am5.Rectangle.new(root, {
fill: root.interfaceColors.get("background"), // a box in the background color...
stroke: root.interfaceColors.get("alternativeBackground") // ...with a contrasting outline
})
})
});
});
lineSeries.data.setAll([
{ category: "Milestones", date: new Date("2026-01-20").getTime(), letter: "A" },
{ category: "Milestones", date: new Date("2026-01-28").getTime(), letter: "B" },
{ category: "Milestones", date: new Date("2026-02-15").getTime(), letter: "C" },
{ category: "Milestones", date: new Date("2026-03-05").getTime(), letter: "D" },
{ category: "Milestones", date: new Date("2026-03-11").getTime(), letter: "E" },
{ category: "Milestones", date: new Date("2026-03-17").getTime(), letter: "F" },
{ category: "Milestones", date: new Date("2026-03-25").getTime(), letter: "G" }
]);
var cursor = chart.set("cursor", am5timeline.CurveCursor.new(root, {
behavior: "zoomX", // drag along the timeline to zoom in
xAxis: xAxis,
yAxis: yAxis
}));
series.data.setAll(data);
// Six lanes: the empty one keeps room for the date labels
yAxis.data.setAll([
{ category: "Module #1" },
{ category: "Module #2" },
{ category: "" },
{ category: "Milestones" },
{ category: "Module #3" },
{ category: "Module #4" }
]);
// Animate chart and series in
// https://www.amcharts.com/docs/v5/concepts/animations/#Initial_animation
series.appear(1000);
chart.appear(1000, 100);
{
"type": "SerpentineChart",
"settings": {
"orientation": "horizontal",
"levelCount": 6,
"startLocation": 0.2,
"endLocation": 1,
"yAxisRadius": {
"type": "Percent",
"value": 70
},
"wheelY": "zoomX",
"scrollbarX": {
"type": "Scrollbar",
"settings": {
"orientation": "horizontal",
"marginBottom": 50
}
},
"cursor": {
"type": "CurveCursor",
"settings": {
"behavior": "zoomX",
"xAxis": "#axis-0",
"yAxis": "#axis-1"
}
}
},
"properties": {
"series": [
"#series-0",
"#series-1"
],
"xAxes": [
"#axis-0"
],
"yAxes": [
"#axis-1"
]
},
"refs": [
{
"data-0": [
{
"category": "Module #1",
"start": 1768435200000,
"end": 1768694400000,
"settings": {
"fill": {
"type": "Color",
"value": "#4150a0"
},
"__parse": true
},
"task": "Gathering requirements",
"__parse": true
},
{
"category": "Module #1",
"start": 1770681600000,
"end": 1776902400000,
"settings": {
"fill": {
"type": "Color",
"value": "#4150a0"
},
"__parse": true
},
"task": "Development",
"__parse": true
},
{
"category": "Module #2",
"start": 1768262400000,
"end": 1768435200000,
"settings": {
"fill": {
"type": "Color",
"value": "#8a52a3"
},
"__parse": true
},
"task": "Gathering requirements",
"__parse": true
},
{
"category": "Module #2",
"start": 1768608000000,
"end": 1768867200000,
"settings": {
"fill": {
"type": "Color",
"value": "#8a52a3"
},
"__parse": true
},
"task": "Producing specifications",
"__parse": true
},
{
"category": "Module #2",
"start": 1768953600000,
"end": 1770681600000,
"settings": {
"fill": {
"type": "Color",
"value": "#8a52a3"
},
"__parse": true
},
"task": "Development",
"__parse": true
},
{
"category": "Module #2",
"start": 1771113600000,
"end": 1771804800000,
"settings": {
"fill": {
"type": "Color",
"value": "#8a52a3"
},
"__parse": true
},
"task": "Testing and QA",
"__parse": true
},
{
"category": "Module #3",
"start": 1767657600000,
"end": 1769212800000,
"settings": {
"fill": {
"type": "Color",
"value": "#b9588c"
},
"__parse": true
},
"task": "Gathering requirements",
"__parse": true
},
{
"category": "Module #3",
"start": 1770336000000,
"end": 1771113600000,
"settings": {
"fill": {
"type": "Color",
"value": "#b9588c"
},
"__parse": true
},
"task": "Producing specifications",
"__parse": true
},
{
"category": "Module #3",
"start": 1773532800000,
"end": 1776643200000,
"settings": {
"fill": {
"type": "Color",
"value": "#b9588c"
},
"__parse": true
},
"task": "Development",
"__parse": true
},
{
"category": "Module #3",
"start": 1777075200000,
"end": 1777939200000,
"settings": {
"fill": {
"type": "Color",
"value": "#b9588c"
},
"__parse": true
},
"task": "Testing and QA",
"__parse": true
},
{
"category": "Module #4",
"start": 1768867200000,
"end": 1771286400000,
"settings": {
"fill": {
"type": "Color",
"value": "#e3764f"
},
"__parse": true
},
"task": "Gathering requirements",
"__parse": true
},
{
"category": "Module #4",
"start": 1772409600000,
"end": 1773532800000,
"settings": {
"fill": {
"type": "Color",
"value": "#e3764f"
},
"__parse": true
},
"task": "Development",
"__parse": true
},
{
"category": "Module #4",
"start": 1774656000000,
"end": 1777852800000,
"settings": {
"fill": {
"type": "Color",
"value": "#e3764f"
},
"__parse": true
},
"task": "Testing and QA",
"__parse": true
}
]
},
{
"data-1": [
{
"category": "Milestones",
"date": 1768867200000,
"letter": "A"
},
{
"category": "Milestones",
"date": 1769558400000,
"letter": "B"
},
{
"category": "Milestones",
"date": 1771113600000,
"letter": "C"
},
{
"category": "Milestones",
"date": 1772668800000,
"letter": "D"
},
{
"category": "Milestones",
"date": 1773187200000,
"letter": "E"
},
{
"category": "Milestones",
"date": 1773705600000,
"letter": "F"
},
{
"category": "Milestones",
"date": 1774396800000,
"letter": "G"
}
]
},
{
"data-2": [
{
"category": "Module #1"
},
{
"category": "Module #2"
},
{
"category": ""
},
{
"category": "Milestones"
},
{
"category": "Module #3"
},
{
"category": "Module #4"
}
]
},
{
"axis-1": {
"type": "CategoryAxis",
"settings": {
"maxDeviation": 0,
"categoryField": "category",
"renderer": {
"type": "AxisRendererCurveY",
"settings": {
"minGridDistance": 10,
"axisLocation": 0.4166666666666667,
"rotateLabels": true
},
"properties": {
"labels": {
"properties": {
"template": {
"settings": {
"centerY": {
"type": "Percent",
"value": 50
},
"centerX": {
"type": "Percent",
"value": 100
},
"fontSize": 11
}
}
}
},
"grid": {
"properties": {
"template": {
"settings": {
"forceHidden": true
}
}
}
}
}
}
},
"properties": {
"data": "#data-2"
}
}
},
{
"axis-0": {
"type": "DateAxis",
"settings": {
"baseInterval": {
"timeUnit": "day",
"count": 1
},
"renderer": {
"type": "AxisRendererCurveX",
"settings": {
"yRenderer": "#axis-1.get('renderer')",
"rotateLabels": true,
"strokeDasharray": [2, 2],
"strokeOpacity": 0.5,
"stroke": "@root.interfaceColors.get('alternativeBackground')"
},
"properties": {
"labels": {
"properties": {
"template": {
"settings": {
"centerY": {
"type": "Percent",
"value": 50
},
"fontSize": 11,
"minPosition": 0.01
}
}
}
}
}
},
"tooltip": {
"type": "Tooltip"
}
}
}
},
{
"series-0": {
"type": "CurveColumnSeries",
"settings": {
"xAxis": "#axis-0",
"yAxis": "#axis-1",
"baseAxis": "#axis-1",
"valueXField": "end",
"openValueXField": "start",
"categoryYField": "category",
"layer": 30
},
"properties": {
"data": "#data-0",
"columns": {
"properties": {
"template": {
"settings": {
"height": {
"type": "Percent",
"value": 25
},
"strokeOpacity": 0,
"templateField": "settings",
"tooltipText": "{category}: {task}\n{openValueX.formatDate('MMM d')} to {valueX.formatDate('MMM d')}"
}
}
}
},
"bullets": [
{
"type": "Bullet",
"settings": {
"locationX": 0,
"sprite": {
"type": "Circle",
"settings": {
"radius": 4,
"templateField": "settings",
"tooltipText": "{category}: {task}\n{openValueX.formatDate('MMM d')} to {valueX.formatDate('MMM d')}",
"layer": 30
}
}
}
},
{
"type": "Bullet",
"settings": {
"locationX": 1,
"sprite": {
"type": "Circle",
"settings": {
"radius": 4,
"templateField": "settings",
"tooltipText": "{category}: {task}\n{openValueX.formatDate('MMM d')} to {valueX.formatDate('MMM d')}",
"layer": 30
}
}
}
}
]
}
}
},
{
"series-1": {
"type": "CurveLineSeries",
"settings": {
"xAxis": "#axis-0",
"yAxis": "#axis-1",
"categoryYField": "category",
"valueXField": "date"
},
"properties": {
"data": "#data-1",
"strokes": {
"properties": {
"template": {
"settings": {
"forceHidden": true
}
}
}
},
"bullets": [
{
"type": "Bullet",
"settings": {
"sprite": {
"type": "Label",
"settings": {
"text": "{letter}",
"populateText": true,
"fontSize": 10,
"fontWeight": "bold",
"centerX": {
"type": "Percent",
"value": 50
},
"centerY": {
"type": "Percent",
"value": 50
},
"paddingTop": 2,
"paddingBottom": 2,
"paddingLeft": 5,
"paddingRight": 5,
"layer": 30,
"background": {
"type": "Rectangle",
"settings": {
"fill": "@root.interfaceColors.get('background')",
"stroke": "@root.interfaceColors.get('alternativeBackground')"
}
}
}
}
}
}
]
}
}
}
]
}