Columns grouped by provider and sorted from smallest to largest within each group, with the provider names on a second row of labels. A line marks each provider’s quantity, centered over its group.
When categories belong to groups, like products to suppliers, a second row of labels keeps each group together, and sorting within it puts each group’s largest item at the end. The line adds one figure per group on an axis of its own, so one chart answers two questions.
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: false, // no dragging the plot
panY: false,
wheelX: "none", // the mouse wheel scrolls the page, not the chart
wheelY: "none",
paddingLeft: 0 // the value labels sit at the chart's left edge
})
);
// Add cursor
// https://www.amcharts.com/docs/v5/charts/xy-chart/cursor/
var cursor = chart.set("cursor", am5xy.XYCursor.new(root, {}));
cursor.lineY.set("visible", false); // no horizontal cursor line
// Create axes
// https://www.amcharts.com/docs/v5/charts/xy-chart/axes/
var xRenderer = am5xy.AxisRendererX.new(root, {
minGridDistance: 30, // at least 30px between labels; on narrow screens some are skipped
minorGridEnabled:true // a skipped item still gets a faint grid line
});
// labels show the item name; the category itself is "provider_item", unique for each column
xRenderer.labels.template.setAll({ text: "{realName}" });
var xAxis = chart.xAxes.push(
am5xy.CategoryAxis.new(root, {
maxDeviation: 0, // no panning past the first or last column
categoryField: "category",
renderer: xRenderer,
tooltip: am5.Tooltip.new(root, {
labelText: "{realName}" // the item's name, not the unique category
})
})
);
// Both value axes start at zero: the quantity line on the left, the columns on the right
var yAxis = chart.yAxes.push(
am5xy.ValueAxis.new(root, {
maxDeviation: 0.3,
min: 0,
renderer: am5xy.AxisRendererY.new(root, {})
})
);
var yAxis2 = chart.yAxes.push(
am5xy.ValueAxis.new(root, {
maxDeviation: 0.3,
min: 0,
// picks its steps so its grid lines fall on the left axis's grid lines
syncWithAxis: yAxis,
renderer: am5xy.AxisRendererY.new(root, { opposite: true }) // on the right side
})
);
// Create series
// https://www.amcharts.com/docs/v5/charts/xy-chart/series/
var series = chart.series.push(
am5xy.ColumnSeries.new(root, {
name: "Series 1",
xAxis: xAxis,
yAxis: yAxis2,
valueYField: "value",
sequencedInterpolation: true, // on load, the columns grow one after another
categoryXField: "category",
tooltip: am5.Tooltip.new(root, {
labelText: "{provider} {realName}: {valueY}" // as "Provider 1 item 2: 35"
}),
// Each column takes its own color from the series palette
colorByDataItem: true
})
);
series.columns.template.setAll({
fillOpacity: 0.9, // slightly see-through
strokeOpacity: 0 // no outline
});
var lineSeries = chart.series.push(
am5xy.LineSeries.new(root, {
name: "Series 2",
xAxis: xAxis,
yAxis: yAxis,
valueYField: "quantity",
sequencedInterpolation: true, // on load, the line's points rise one after another
stroke: chart.get("colors").getIndex(13), // a palette color away from the columns' colors...
fill: chart.get("colors").getIndex(13), // ...for the dots too
categoryXField: "category",
tooltip: am5.Tooltip.new(root, {
labelText: "{provider} quantity: {valueY}" // as "Provider 1 quantity: 430"
})
})
);
lineSeries.strokes.template.set("strokeWidth", 2); // a 2px line
lineSeries.bullets.push(function () {
return am5.Bullet.new(root, {
locationY: 1,
locationX: undefined, // no fixed spot: each point's own locationX, set below
sprite: am5.Circle.new(root, {
radius: 5, // 5px radius
fill: lineSeries.get("fill") // in the line's color
})
});
});
// when data validated, adjust location of data item based on count
lineSeries.events.on("datavalidated", function () {
am5.array.each(lineSeries.dataItems, function (dataItem) {
// if count divides by two, location is 0 (on the grid)
if (
dataItem.dataContext.count / 2 ==
Math.round(dataItem.dataContext.count / 2)
) {
dataItem.set("locationX", 0);
}
// otherwise location is 0.5 (middle)
else {
dataItem.set("locationX", 0.5);
}
});
});
var chartData = [];
// Set data
var data = {
"Provider 1": {
"item 1": 10,
"item 2": 35,
"item 3": 5,
"item 4": 20,
quantity: 430
},
"Provider 2": {
"item 1": 15,
"item 3": 21,
quantity: 210
},
"Provider 3": {
"item 2": 25,
"item 3": 11,
"item 4": 17,
quantity: 265
},
"Provider 4": {
"item 3": 12,
"item 4": 15,
quantity: 98
}
};
// process data and prepare it for the chart
for (var providerName in data) {
var providerData = data[providerName];
// add data of one provider to temp array
var tempArray = [];
var count = 0;
// add items
for (var itemName in providerData) {
if (itemName != "quantity") {
count++;
// we generate unique category for each column (providerName + "_" + itemName) and store realName
tempArray.push({
category: providerName + "_" + itemName,
realName: itemName,
value: providerData[itemName],
provider: providerName
});
}
}
// sort temp array
tempArray.sort(function (a, b) {
if (a.value > b.value) {
return 1;
} else if (a.value < b.value) {
return -1;
} else {
return 0;
}
});
// add quantity and count to middle data item (line series uses it)
var lineSeriesDataIndex = Math.floor(count / 2);
tempArray[lineSeriesDataIndex].quantity = providerData.quantity;
tempArray[lineSeriesDataIndex].count = count;
// push to the final data
am5.array.each(tempArray, function (item) {
chartData.push(item);
});
// create range (the additional label at the bottom)
var range = xAxis.makeDataItem({});
xAxis.createAxisRange(range);
range.set("category", tempArray[0].category);
range.set("endCategory", tempArray[tempArray.length - 1].category);
var label = range.get("label");
label.setAll({
text: tempArray[0].provider,
dy: 30, // 30px below the item names
fontWeight: "bold",
tooltipText: tempArray[0].provider // the provider's name on hover
});
// a 50px tick and a solid grid line at the start of each group divide the groups
var tick = range.get("tick");
tick.setAll({ visible: true, strokeOpacity: 1, length: 50, location: 0 });
var grid = range.get("grid");
grid.setAll({ strokeOpacity: 1 });
}
// add range for the last grid
var range = xAxis.makeDataItem({});
xAxis.createAxisRange(range);
range.set("category", chartData[chartData.length - 1].category);
var tick = range.get("tick");
tick.setAll({ visible: true, strokeOpacity: 1, length: 50, location: 1 }); // a tick at the very end...
var grid = range.get("grid");
grid.setAll({ strokeOpacity: 1, location: 1 }); // ...with a solid grid line
xAxis.data.setAll(chartData);
series.data.setAll(chartData);
lineSeries.data.setAll(chartData);
// Make stuff animate on load
// https://www.amcharts.com/docs/v5/concepts/animations/
series.appear(1000);
chart.appear(1000, 100);
{
"type": "XYChart",
"settings": {
"panX": false,
"panY": false,
"wheelX": "none",
"wheelY": "none",
"paddingLeft": 0,
"cursor": {
"type": "XYCursor",
"properties": {
"lineY": {
"settings": {
"visible": false
}
}
}
}
},
"properties": {
"series": [
"#series-0",
"#series-1"
],
"xAxes": [
"#axis-0"
],
"yAxes": [
"#axis-1",
"#axis-2"
]
},
"refs": [
{
"data-0": [
{
"category": "Provider 1_item 3",
"realName": "item 3",
"value": 5,
"provider": "Provider 1"
},
{
"category": "Provider 1_item 1",
"realName": "item 1",
"value": 10,
"provider": "Provider 1"
},
{
"category": "Provider 1_item 4",
"realName": "item 4",
"value": 20,
"provider": "Provider 1",
"quantity": 430,
"count": 4
},
{
"category": "Provider 1_item 2",
"realName": "item 2",
"value": 35,
"provider": "Provider 1"
},
{
"category": "Provider 2_item 1",
"realName": "item 1",
"value": 15,
"provider": "Provider 2"
},
{
"category": "Provider 2_item 3",
"realName": "item 3",
"value": 21,
"provider": "Provider 2",
"quantity": 210,
"count": 2
},
{
"category": "Provider 3_item 3",
"realName": "item 3",
"value": 11,
"provider": "Provider 3"
},
{
"category": "Provider 3_item 4",
"realName": "item 4",
"value": 17,
"provider": "Provider 3",
"quantity": 265,
"count": 3
},
{
"category": "Provider 3_item 2",
"realName": "item 2",
"value": 25,
"provider": "Provider 3"
},
{
"category": "Provider 4_item 3",
"realName": "item 3",
"value": 12,
"provider": "Provider 4"
},
{
"category": "Provider 4_item 4",
"realName": "item 4",
"value": 15,
"provider": "Provider 4",
"quantity": 98,
"count": 2
}
]
},
{
"axis-0": {
"type": "CategoryAxis",
"settings": {
"maxDeviation": 0,
"categoryField": "category",
"renderer": {
"type": "AxisRendererX",
"settings": {
"minGridDistance": 30,
"minorGridEnabled": true
},
"properties": {
"labels": {
"properties": {
"template": {
"settings": {
"text": "{realName}"
}
}
}
}
}
},
"tooltip": {
"type": "Tooltip",
"settings": {
"labelText": "{realName}"
}
}
},
"properties": {
"data": "#data-0"
}
}
},
{
"axis-1": {
"type": "ValueAxis",
"settings": {
"maxDeviation": 0.3,
"min": 0,
"renderer": {
"type": "AxisRendererY"
}
}
}
},
{
"axis-2": {
"type": "ValueAxis",
"settings": {
"maxDeviation": 0.3,
"min": 0,
"syncWithAxis": "#axis-1",
"renderer": {
"type": "AxisRendererY",
"settings": {
"opposite": true
}
}
}
}
},
{
"series-0": {
"type": "ColumnSeries",
"settings": {
"name": "Series 1",
"xAxis": "#axis-0",
"yAxis": "#axis-2",
"valueYField": "value",
"sequencedInterpolation": true,
"categoryXField": "category",
"tooltip": {
"type": "Tooltip",
"settings": {
"labelText": "{provider} {realName}: {valueY}"
}
},
"colorByDataItem": true
},
"properties": {
"data": "#data-0",
"columns": {
"properties": {
"template": {
"settings": {
"fillOpacity": 0.9,
"strokeOpacity": 0
}
}
}
},
"bullets": [
{
"type": "Bullet",
"settings": {
"locationY": 1,
"sprite": {
"type": "Label",
"settings": {
"text": "{valueY}",
"populateText": true,
"centerX": {
"type": "Percent",
"value": 50
},
"centerY": {
"type": "Percent",
"value": 100
},
"dy": -6,
"fontSize": 12
}
}
}
}
]
}
}
},
{
"series-1": {
"type": "LineSeries",
"settings": {
"name": "Series 2",
"xAxis": "#axis-0",
"yAxis": "#axis-1",
"valueYField": "quantity",
"sequencedInterpolation": true,
"stroke": {
"type": "Color",
"value": "#c3dc67"
},
"fill": {
"type": "Color",
"value": "#c3dc67"
},
"categoryXField": "category",
"tooltip": {
"type": "Tooltip",
"settings": {
"labelText": "{provider} quantity: {valueY}"
}
}
},
"properties": {
"data": "#data-0",
"strokes": {
"properties": {
"template": {
"settings": {
"strokeWidth": 2
}
}
}
},
"bullets": [
{
"type": "Bullet",
"settings": {
"locationY": 1,
"sprite": {
"type": "Circle",
"settings": {
"radius": 5,
"fill": "@series.get('fill')"
}
}
}
}
]
}
}
},
{
"axisRange-0": {
"axis": "#axis-0",
"settings": {
"label": {
"type": "AxisLabel",
"settings": {
"text": "Provider 1",
"dy": 30,
"fontWeight": "bold",
"tooltipText": "Provider 1"
}
},
"grid": {
"type": "Grid",
"settings": {
"strokeOpacity": 1
}
},
"tick": {
"type": "AxisTick",
"settings": {
"location": 0,
"strokeOpacity": 1,
"visible": true,
"length": 50
}
},
"category": "Provider 1_item 3",
"endCategory": "Provider 1_item 2"
},
"__parseLogic": "axisRange"
}
},
{
"axisRange-1": {
"axis": "#axis-0",
"settings": {
"label": {
"type": "AxisLabel",
"settings": {
"text": "Provider 2",
"dy": 30,
"fontWeight": "bold",
"tooltipText": "Provider 2"
}
},
"grid": {
"type": "Grid",
"settings": {
"strokeOpacity": 1
}
},
"tick": {
"type": "AxisTick",
"settings": {
"location": 0,
"strokeOpacity": 1,
"visible": true,
"length": 50
}
},
"category": "Provider 2_item 1",
"endCategory": "Provider 2_item 3"
},
"__parseLogic": "axisRange"
}
},
{
"axisRange-2": {
"axis": "#axis-0",
"settings": {
"label": {
"type": "AxisLabel",
"settings": {
"text": "Provider 3",
"dy": 30,
"fontWeight": "bold",
"tooltipText": "Provider 3"
}
},
"grid": {
"type": "Grid",
"settings": {
"strokeOpacity": 1
}
},
"tick": {
"type": "AxisTick",
"settings": {
"location": 0,
"strokeOpacity": 1,
"visible": true,
"length": 50
}
},
"category": "Provider 3_item 3",
"endCategory": "Provider 3_item 2"
},
"__parseLogic": "axisRange"
}
},
{
"axisRange-3": {
"axis": "#axis-0",
"settings": {
"label": {
"type": "AxisLabel",
"settings": {
"text": "Provider 4",
"dy": 30,
"fontWeight": "bold",
"tooltipText": "Provider 4"
}
},
"grid": {
"type": "Grid",
"settings": {
"strokeOpacity": 1
}
},
"tick": {
"type": "AxisTick",
"settings": {
"location": 0,
"strokeOpacity": 1,
"visible": true,
"length": 50
}
},
"category": "Provider 4_item 3",
"endCategory": "Provider 4_item 4"
},
"__parseLogic": "axisRange"
}
},
{
"axisRange-4": {
"axis": "#axis-0",
"settings": {
"grid": {
"type": "Grid",
"settings": {
"location": 1,
"strokeOpacity": 1
}
},
"tick": {
"type": "AxisTick",
"settings": {
"location": 1,
"strokeOpacity": 1,
"visible": true,
"length": 50
}
},
"category": "Provider 4_item 4"
},
"__parseLogic": "axisRange"
}
}
]
}