A line chart that writes the value above every point, so the numbers are there without hovering. Here, sixteen readings half an hour apart through one night.
Labels put the exact figure on the chart itself, so it survives a screenshot, a printout or a slide, where nobody can hover. Readers get the numbers and the trend from one picture.
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.
// Create root element
// https://www.amcharts.com/docs/v5/getting-started/#Root_element
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
// https://www.amcharts.com/docs/v5/charts/xy-chart/
var chart = root.container.children.push(am5xy.XYChart.new(root, {
panX: true, // drag the plot sideways to pan through the times
panY: true, // and up and down
wheelX: "panX", // a horizontal wheel or trackpad swipe pans...
wheelY: "zoomX", // ...and the vertical wheel zooms in on the times
pinchZoomX: true // pinch with two fingers to zoom on a touch screen
}));
// a scrollbar above the plot to zoom and scroll through the times
chart.set("scrollbarX", am5.Scrollbar.new(root, {
orientation: "horizontal"
}));
// Create axes
// https://www.amcharts.com/docs/v5/charts/xy-chart/axes/
var xRenderer = am5xy.AxisRendererX.new(root, {
minGridDistance: 60, // at least 60px between labels; on narrow screens some are skipped
minorGridEnabled: true // fainter grid lines between the labeled ones
});
var xAxis = chart.xAxes.push(am5xy.DateAxis.new(root, {
baseInterval: { timeUnit: "minute", count: 30 }, // one data point every 30 minutes
renderer: xRenderer,
tooltip: am5.Tooltip.new(root, {}), // a time label follows the cursor along the axis
// 1% extra room before the first point and after the last one
extraMin: 0.01,
extraMax: 0.01,
tooltipLocation: 0 // the axis tooltip shows the start of the interval, where the point is
}));
var yRenderer = am5xy.AxisRendererY.new(root, {});
var yAxis = chart.yAxes.push(am5xy.ValueAxis.new(root, {
renderer: yRenderer
}));
// Add cursor
// https://www.amcharts.com/docs/v5/charts/xy-chart/cursor/
var cursor = chart.set("cursor", am5xy.XYCursor.new(root, {
behavior: "none", // dragging the plot pans instead of selecting a range to zoom
xAxis: xAxis // the cursor snaps to this axis' 30-minute intervals
}));
cursor.lineY.set("visible", false); // only the vertical cursor line, no horizontal one
// Add series
// https://www.amcharts.com/docs/v5/charts/xy-chart/series/
var series = chart.series.push(am5xy.LineSeries.new(root, {
name: "Series",
xAxis: xAxis,
yAxis: yAxis,
valueYField: "value",
valueXField: "timestamp",
// each point sits at the start of its 30-minute interval, on its exact time
locationX: 0,
// the tooltip takes its color from the first bullet (the circle) instead of the series
seriesTooltipTarget: "bullet",
tooltip: am5.Tooltip.new(root, {
labelText: "{valueY}" // the tooltip shows the point's value
})
}));
// All bullets share one template, so setting its radius resizes them at once
var circleTemplate = am5.Template.new({
radius: 6, // 6px radius
templateField: "bulletSettings", // a data item's bulletSettings can override these settings
fill: series.get("fill"), // filled in the line's color...
strokeWidth: 2, // ...with a 2px ring...
stroke: root.interfaceColors.get("background") // ...in the background color around it
});
// a dot on each point
series.bullets.push(function () {
var circle = am5.Circle.new(root, {}, circleTemplate);
return am5.Bullet.new(root, {
sprite: circle,
locationX: 0 // at the start of the interval, where the point is
});
});
// a value label above each point
series.bullets.push(function () {
var label = am5.Label.new(root, {
populateText: true, // fill in {valueY} from the data item
text: "{valueY}", // the point's value
centerX: am5.p50, // centered over the point
// anchored 30px below its top, which lifts the label above the point
centerY: 30,
fontSize: 10, // small...
fontWeight: "bold", // ...bold text
paddingBottom: 4, // room around the text inside its box
paddingTop: 4,
paddingLeft: 6,
paddingRight: 6,
fill: am5.Color.brighten(series.get("fill"), -0.1), // a little darker than the line
});
// The box takes the chart's background color, so it works in light and dark mode
label.set("background", am5.RoundedRectangle.new(root, {
fill: root.interfaceColors.get("background"),
fillOpacity: 0.75, // slightly see-through
cornerRadiusBL: 3, // rounded corners
cornerRadiusBR: 3,
cornerRadiusTL: 3,
cornerRadiusTR: 3,
stroke: series.get("fill"), // outlined in the line's color
}));
return am5.Bullet.new(root, {
sprite: label,
locationX: 0 // at the start of the interval, where the point is
});
});
var data = [{
"timestamp": new Date(2026, 0, 1, 22, 30).getTime(),
"value": 99.71
}, {
"timestamp": new Date(2026, 0, 1, 23, 0).getTime(),
"value": 99.13
}, {
"timestamp": new Date(2026, 0, 1, 23, 30).getTime(),
"value": 100.5
}, {
"timestamp": new Date(2026, 0, 2, 0, 0).getTime(),
"value": 101
}, {
"timestamp": new Date(2026, 0, 2, 0, 30).getTime(),
"value": 99.95
}, {
"timestamp": new Date(2026, 0, 2, 1, 0).getTime(),
"value": 100.9
}, {
"timestamp": new Date(2026, 0, 2, 1, 30).getTime(),
"value": 100.39
}, {
"timestamp": new Date(2026, 0, 2, 2, 0).getTime(),
"value": 101.1
}, {
"timestamp": new Date(2026, 0, 2, 2, 30).getTime(),
"value": 101.45
}, {
"timestamp": new Date(2026, 0, 2, 3, 0).getTime(),
"value": 101.15
}, {
"timestamp": new Date(2026, 0, 2, 3, 30).getTime(),
"value": 100.5
}, {
"timestamp": new Date(2026, 0, 2, 4, 0).getTime(),
"value": 101.55
}, {
"timestamp": new Date(2026, 0, 2, 4, 30).getTime(),
"value": 101.7
}, {
"timestamp": new Date(2026, 0, 2, 5, 0).getTime(),
"value": 100.5
}, {
"timestamp": new Date(2026, 0, 2, 5, 30).getTime(),
"value": 100.92
}, {
"timestamp": new Date(2026, 0, 2, 6, 0).getTime(),
"value": 102.2
}];
series.data.setAll(data);
// Make stuff animate on load
// https://www.amcharts.com/docs/v5/concepts/animations/
series.appear(1000);
chart.appear(1000, 100);
{
"type": "XYChart",
"settings": {
"panX": true,
"panY": true,
"wheelX": "panX",
"wheelY": "zoomX",
"pinchZoomX": true,
"scrollbarX": {
"type": "Scrollbar",
"settings": {
"orientation": "horizontal"
}
},
"cursor": {
"type": "XYCursor",
"settings": {
"behavior": "none",
"xAxis": "#axis-0"
},
"properties": {
"lineY": {
"settings": {
"visible": false
}
}
}
}
},
"properties": {
"series": [
"#series-0"
],
"xAxes": [
"#axis-0"
],
"yAxes": [
"#axis-1"
]
},
"refs": [
{
"data-0": [
{
"timestamp": 1767299400000,
"value": 99.71
},
{
"timestamp": 1767301200000,
"value": 99.13
},
{
"timestamp": 1767303000000,
"value": 100.5
},
{
"timestamp": 1767304800000,
"value": 101
},
{
"timestamp": 1767306600000,
"value": 99.95
},
{
"timestamp": 1767308400000,
"value": 100.9
},
{
"timestamp": 1767310200000,
"value": 100.39
},
{
"timestamp": 1767312000000,
"value": 101.1
},
{
"timestamp": 1767313800000,
"value": 101.45
},
{
"timestamp": 1767315600000,
"value": 101.15
},
{
"timestamp": 1767317400000,
"value": 100.5
},
{
"timestamp": 1767319200000,
"value": 101.55
},
{
"timestamp": 1767321000000,
"value": 101.7
},
{
"timestamp": 1767322800000,
"value": 100.5
},
{
"timestamp": 1767324600000,
"value": 100.92
},
{
"timestamp": 1767326400000,
"value": 102.2
}
]
},
{
"template-0": {
"type": "Template",
"settings": {
"radius": 6,
"templateField": "bulletSettings",
"fill": {
"type": "Color",
"value": "#67b7dc"
},
"strokeWidth": 2,
"stroke": "@root.interfaceColors.get('background')"
}
}
},
{
"axis-0": {
"type": "DateAxis",
"settings": {
"baseInterval": {
"timeUnit": "minute",
"count": 30
},
"renderer": {
"type": "AxisRendererX",
"settings": {
"minGridDistance": 60,
"minorGridEnabled": true
}
},
"tooltip": {
"type": "Tooltip"
},
"extraMin": 0.01,
"extraMax": 0.01,
"tooltipLocation": 0
}
}
},
{
"axis-1": {
"type": "ValueAxis",
"settings": {
"renderer": {
"type": "AxisRendererY"
}
}
}
},
{
"series-0": {
"type": "LineSeries",
"settings": {
"name": "Series",
"xAxis": "#axis-0",
"yAxis": "#axis-1",
"valueYField": "value",
"valueXField": "timestamp",
"locationX": 0,
"seriesTooltipTarget": "bullet",
"tooltip": {
"type": "Tooltip",
"settings": {
"labelText": "{valueY}"
}
}
},
"properties": {
"data": "#data-0",
"bullets": [
{
"type": "Bullet",
"settings": {
"sprite": {
"type": "Circle",
"properties": {
"template": "#template-0"
}
},
"locationX": 0
}
},
{
"type": "Bullet",
"settings": {
"sprite": {
"type": "Label",
"settings": {
"populateText": true,
"text": "{valueY}",
"centerX": {
"type": "Percent",
"value": 50
},
"centerY": 30,
"fontSize": 10,
"fontWeight": "bold",
"paddingBottom": 4,
"paddingTop": 4,
"paddingLeft": 6,
"paddingRight": 6,
"fill": {
"type": "Color",
"value": "#51a1c6"
},
"background": {
"type": "RoundedRectangle",
"settings": {
"fill": "@root.interfaceColors.get('background')",
"fillOpacity": 0.75,
"cornerRadiusBL": 3,
"cornerRadiusBR": 3,
"cornerRadiusTL": 3,
"cornerRadiusTR": 3,
"stroke": {
"type": "Color",
"value": "#67b7dc"
}
}
}
}
},
"locationX": 0
}
}
]
}
}
}
]
}