A project plan on a winding road, so four months fit on one screen. Each module has its own lane and color, and lettered badges mark the milestones.
A long timeline on one straight line gets either tiny or so wide that it scrolls. Folding it into rows keeps every date big enough to read, and the turns show where one stretch of time carries on into the next. It reads best with a few lanes over a few months.
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, {
// the timeline winds back and forth in three rows
levelCount: 3,
// the first row starts 20% of the way in, leaving room for the lane names at its left
startLocation: 0.2,
endLocation: 1, // the last row runs to its end
// How much of each row the lanes take
yAxisRadius: am5.percent(80),
wheelY: "zoomX" // the mouse wheel zooms in on the dates
}));
chart.set("scrollbarX", am5.Scrollbar.new(root, {
orientation: "horizontal" // above the chart; zooms the dates
}));
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
});
yRenderer.labels.template.setAll({
centerY: am5.p50, // lane names centered on their lane...
centerX: am5.p100, // ...and ending where the lanes start
fontSize: 11 // small text
});
yRenderer.grid.template.set("forceHidden", true); // no lines between the lanes
// Create axes and their renderers
var xRenderer = am5timeline.AxisRendererCurveX.new(root, {
yRenderer: yRenderer, // required: the date axis follows the lanes' curve
strokeDasharray: [2, 3], // a dashed line: 2px dashes, 3px gaps...
strokeOpacity: 0.5, // ...half see-through...
stroke: root.interfaceColors.get("alternativeBackground") // ...in the theme's text color
});
xRenderer.labels.template.setAll({
centerY: am5.p50, // date labels centered on the line
fontSize: 11, // small text
minPosition: 0.01 // hide a label at the very start, where it would be cut
});
// 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); // drawn on layer 30, above everything without a layer
target.set("background", am5.Rectangle.new(root, {
fill: root.interfaceColors.get("background"),
fillOpacity: 1 // solid, hiding the dashed line under the text
}));
}
var yAxis = chart.yAxes.push(am5xy.CategoryAxis.new(root, {
maxDeviation: 0, // no panning past the first and last lane
categoryField: "category",
renderer: yRenderer
}));
var xAxis = chart.xAxes.push(am5xy.DateAxis.new(root, {
baseInterval: { timeUnit: "day", count: 1 }, // dates by the day
renderer: xRenderer,
tooltip: am5.Tooltip.new(root, {}) // shows the cursor's date on the axis
}));
// 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(3) };
var module3 = { fill: colorSet.getIndex(5) };
var module4 = { fill: colorSet.getIndex(8) };
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 lanes are the base: bars stretch along the dates
valueXField: "end", // each bar ends on its end date...
openValueXField: "start", // ...and starts on its start date
categoryYField: "category",
layer: 30 // drawn on layer 30, above the axes
}));
// 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), // a bar takes a quarter of its lane's height
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, // a 4px dot
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"
}));
// only the badges show, no line joining them
lineSeries.strokes.template.set("forceHidden", true);
lineSeries.bullets.push(function(root, series, dataItem) {
return am5.Bullet.new(root, {
sprite: am5.Label.new(root, {
text: "{letter}",
populateText: true, // fill in {letter} from the data
fontSize: 10, // small text
fontWeight: "bold",
centerX: am5.p50, // centered on its date...
centerY: am5.p50, // ...and in the middle of its lane
paddingTop: 2, paddingBottom: 2, paddingLeft: 5, paddingRight: 5, // space around the letter
layer: 30, // on layer 30 with the bars, above the axes
background: am5.Rectangle.new(root, {
fill: root.interfaceColors.get("background"), // a badge in the background color...
stroke: root.interfaceColors.get("alternativeBackground") // ...with an outline in the text color
})
})
});
});
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", // a drag along the timeline zooms in on those dates
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": {
"levelCount": 3,
"startLocation": 0.2,
"endLocation": 1,
"yAxisRadius": {
"type": "Percent",
"value": 80
},
"wheelY": "zoomX",
"scrollbarX": {
"type": "Scrollbar",
"settings": {
"orientation": "horizontal"
}
},
"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": "#9252a0"
},
"__parse": true
},
"task": "Gathering requirements",
"__parse": true
},
{
"category": "Module #2",
"start": 1768608000000,
"end": 1768867200000,
"settings": {
"fill": {
"type": "Color",
"value": "#9252a0"
},
"__parse": true
},
"task": "Producing specifications",
"__parse": true
},
{
"category": "Module #2",
"start": 1768953600000,
"end": 1770681600000,
"settings": {
"fill": {
"type": "Color",
"value": "#9252a0"
},
"__parse": true
},
"task": "Development",
"__parse": true
},
{
"category": "Module #2",
"start": 1771113600000,
"end": 1771804800000,
"settings": {
"fill": {
"type": "Color",
"value": "#9252a0"
},
"__parse": true
},
"task": "Testing and QA",
"__parse": true
},
{
"category": "Module #3",
"start": 1767657600000,
"end": 1769212800000,
"settings": {
"fill": {
"type": "Color",
"value": "#bd5989"
},
"__parse": true
},
"task": "Gathering requirements",
"__parse": true
},
{
"category": "Module #3",
"start": 1770336000000,
"end": 1771113600000,
"settings": {
"fill": {
"type": "Color",
"value": "#bd5989"
},
"__parse": true
},
"task": "Producing specifications",
"__parse": true
},
{
"category": "Module #3",
"start": 1773532800000,
"end": 1776643200000,
"settings": {
"fill": {
"type": "Color",
"value": "#bd5989"
},
"__parse": true
},
"task": "Development",
"__parse": true
},
{
"category": "Module #3",
"start": 1777075200000,
"end": 1777939200000,
"settings": {
"fill": {
"type": "Color",
"value": "#bd5989"
},
"__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
},
"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')",
"strokeDasharray": [2, 3],
"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')"
}
}
}
}
}
}
]
}
}
}
]
}