Timeline chart with highcharts using x-range with multiple stacks - javascript

I am using Highcharts to create a timeline chart which shows the flow of different "states" over time. The current implementation is at http://jsfiddle.net/hq1kdpmo/8/ and it looks like .
The current code is as follows:
Highcharts.chart('container', {
"chart": {
"type": "xrange"
},
"title": {
"text": "State Periods"
},
"xAxis": {
"type": "datetime"
},
"yAxis": [{
"title": {
"text": "Factions"
},
"categories": ["A", "B", "C", "D"],
"reversed": true
}],
"plotOptions": {
"xrange": {
"borderRadius": 0,
"borderWidth": 0,
"grouping": false,
"dataLabels": {
"align": "center",
"enabled": true,
"format": "{point.name}"
},
"colorByPoint": false
}
},
"tooltip": {
"headerFormat": "<span style=\"font-size: 0.85em\">{point.x} - {point.x2}</span><br/>",
"pointFormat": "<span style=\"color:{series.color}\">●</span> {series.name}: <b>{point.yCategory}</b><br/>"
},
"series": [{
"name": "State A",
"pointWidth": 20,
"data": [{
"x": 1540430613000,
"x2": 1540633768100,
"y": 0
}, {
"x": 1540191009000,
"x2": 1540633768100,
"y": 1
}, {
"x": 1540191009000,
"x2": 1540530613000,
"y": 2
}, {
"x": 1540530613000,
"x2": 1540633768100,
"y": 3
}]
}, {
"name": "State B",
"pointWidth": 20,
"data": [{
"x": 1540191009000,
"x2": 1540430613000,
"y": 0
}, {
"x": 1540530613000,
"x2": 1540633768100,
"y": 2
}, {
"x": 1540191009000,
"x2": 1540330613000,
"y": 3
}]
}, {
"name": "State C",
"pointWidth": 20,
"data": [{
"x": 1540330613000,
"x2": 1540530613000,
"y": 3
}]
}],
"exporting": {
"enabled": true,
"sourceWidth": 1200
}
});
Now what I am looking forward to is create something of this sort (pardon my paint skills).
Here the categories A to D are the only axis on y. But I would like to group a variable number of parallel ranges. The use case is that there can be multiple states at any point of time and the number of states at any point of time is variable. How do I go on about doing this?

To create such a chart you will have to add 12 yAxis (0-11) and set proper ticks and labels so that only A-D categories will be plotted. Additionally, adjust plotOptions.pointPadding and plotOptions.groupPadding properties to set points width automatically (series.pointWidth should be undefined then).
yAxis options:
yAxis: [{
title: {
text: "Factions"
},
categories: ["A", "B", "C", "D"],
tickPositions: [-1, 2, 5, 8, 11],
lineWidth: 0,
labels: {
y: -20,
formatter: function() {
var chart = this.chart,
axis = this.axis,
label;
if (!chart.yaxisLabelIndex) {
chart.yaxisLabelIndex = 0;
}
if (this.value !== -1) {
label = axis.categories[chart.yaxisLabelIndex];
chart.yaxisLabelIndex++;
if (chart.yaxisLabelIndex === 4) {
chart.yaxisLabelIndex = 0;
}
return label;
}
},
},
reversed: true
}]
Demo:
https://jsfiddle.net/wchmiel/s9qefg7t/1/

I have managed to solve this thanks to support from Highcharts themselves. The idea is to set the tick position on the load event and use the labels.formatter for formatting each individual label.
events: {
load() {
let labelGroup = document.querySelectorAll('.highcharts-yaxis-labels');
// nodeValue is distance from top
let ticks = document.querySelectorAll('.highcharts-yaxis-grid');
let tickPositions = Array.from(ticks[0].childNodes).map(
function(node){
return +node.attributes.d.nodeValue.split(" ")[2];
}
);
let labelPositions = [];
for(let i =1 ;i<tickPositions.length;i++){
labelPositions.push((tickPositions[i] + tickPositions[i-1])/2);
}
labelGroup[0].childNodes[0].attributes.y.nodeValue = labelPositions[0] + parseFloat(labelGroup[0].childNodes[0].style["font-size"], 10) / 2;
labelGroup[0].childNodes[1].attributes.y.nodeValue = labelPositions[1] + parseFloat(labelGroup[0].childNodes[1].style["font-size"], 10) / 2;
labelGroup[0].childNodes[2].attributes.y.nodeValue = labelPositions[2] + parseFloat(labelGroup[0].childNodes[2].style["font-size"], 10) / 2;
labelGroup[0].childNodes[3].attributes.y.nodeValue = labelPositions[3] + parseFloat(labelGroup[0].childNodes[3].style["font-size"], 10) / 2;
labelGroup[0].childNodes[4].attributes.y.nodeValue = labelPositions[4] + parseFloat(labelGroup[0].childNodes[4].style["font-size"], 10) / 2;
}
}
And the labels are formatted as:
labels: {
formatter: function() {
var chart = this.chart,
axis = this.axis,
label;
if (!chart.yaxisLabelIndex) {
chart.yaxisLabelIndex = 0;
}
if (this.value !== -1) {
label = axis.categories[chart.yaxisLabelIndex];
chart.yaxisLabelIndex++;
if (chart.yaxisLabelIndex === groups.length) {
chart.yaxisLabelIndex = 0;
}
return label;
}
},
}
Fiddle at https://jsfiddle.net/yvnp4su0/42/

As I suggested in the comment above it is a better idea to use Highcharts renderer and add custom labels than manipulate Dom elements as you did in the previous answer, because it is a much cleaner solution.
Disable default labels:
yAxis: [{
title: {
text: "Factions",
margin: 35
},
categories: ["A", "B", "C", "D", "E"],
tickPositions: tickPositions,
lineWidth: 0,
labels: {
enabled: false
},
reversed: true
}]
Add custom labels in proper positions using renderer:
chart: {
type: 'xrange',
height: 500,
marginLeft: 60,
events: {
load: function() {
this.customLabels = [];
},
render: function() {
var chart = this,
yAxis = chart.yAxis[0],
categories = yAxis.categories,
xOffset = 15,
yOffset = 20,
xPos = yAxis.left - xOffset,
tickPositions = yAxis.tickPositions,
text,
label,
yPos,
tick1Y,
tick2Y,
i;
for (i = 0; i < tickPositions.length - 1; i++) {
if (chart.customLabels[i]) {
chart.customLabels[i].destroy();
}
tick1Y = yAxis.toPixels(tickPositions[i]);
tick2Y = yAxis.toPixels(tickPositions[i + 1]);
yPos = (tick1Y + tick2Y) / 2 + yOffset;
text = categories[i];
label = chart.renderer.text(text, xPos, yPos)
.css({
color: '#ccc',
fontSize: '14px'
})
.add();
chart.customLabels[i] = label;
}
}
}
}
Demo: https://jsfiddle.net/wchmiel/vkz7o1hw/

Related

Stacked Bar Chart (AmCharts 4) with one series and repeating category

I'm currently creating a chart whose data is dynamically fetch from a database. Based on this data, I want to create a Horizontal Stacked Bar Chart that has only one series. Would that be possible in AmCharts? Or do I need a different approach?
Here's what I'd like as final result:
Here's a sample of what I'm currently doing: Chart Sample
I'm also aware that using series.stacked = true; would make a column chart stacked but I think this needs to have multiple series which I want to avoid for now.
<html>
<head></head>
<style></style>
<!-- Resources -->
<script src="https://cdn.amcharts.com/lib/5/index.js"></script>
<script src="https://cdn.amcharts.com/lib/5/xy.js"></script>
<script src="https://cdn.amcharts.com/lib/5/themes/Animated.js"></script>
<body>
<div id="chartdiv" style="width: 600px;height: 200px;background:#c7cacb;margin: 0 auto;"></div>
<script>
am5.ready(function () {
var root = am5.Root.new("chartdiv");
root._logo.dispose();
root.setThemes([
am5themes_Animated.new(root)
]);
var chart = root.container.children.push(am5xy.XYChart.new(root, {
panX: false,
panY: false,
wheelX: false,
wheelY: false,
layout: root.verticalLayout
}));
var data = [{
"year": "Payments",
"europe": 50,
"namerica": 0,
"asia": 0,
}, {
"year": "Invoiced",
"europe": 30,
"namerica": 0,
"asia": 0,
}, {
"year": "Other Adjustment Sum",
"europe": 40,
"namerica": 20,
"asia": 39,
}]
var yAxis = chart.yAxes.push(am5xy.CategoryAxis.new(root, {
categoryField: "year",
renderer: am5xy.AxisRendererY.new(root, {}),
tooltip: am5.Tooltip.new(root, {})
}));
yAxis.data.setAll(data);
var xAxis = chart.xAxes.push(am5xy.ValueAxis.new(root, {
min: 0,
renderer: am5xy.AxisRendererX.new(root, {})
}));
xAxis = chart.xAxes.push(
am5xy.ValueAxis.new(root, {
min: 0,
numberFormat: "''",
renderer: am5xy.AxisRendererX.new(root, {
strokeOpacity: 1,
strokeWidth: 1,
minGridDistance: 60
}),
})
);
let myRange = [
{
x: 20,
},
{
x: 40,
},
{
x: 60,
},
{
x: 80,
},
{
x: 100,
},
];
for (var i = 0; i < data.length + 2; i++) {
let value = myRange[i].x;
let rangeDataItem = xAxis.makeDataItem({
value: value,
});
let range = xAxis.createAxisRange(rangeDataItem);
rangeDataItem.get('label').setAll({
forceHidden: false,
text: value,
});
}
var legend = chart.children.push(am5.Legend.new(root, {
centerX: am5.p50,
x: am5.p50
}));
function makeSeries(name, fieldName, color) {
var series = chart.series.push(am5xy.ColumnSeries.new(root, {
name: name,
stacked: true,
xAxis: xAxis,
yAxis: yAxis,
baseAxis: yAxis,
fill: color,
valueXField: fieldName,
categoryYField: "year"
}));
series.columns.template.setAll({
tooltipText: "{name}, {categoryY}: {valueX}",
tooltipY: am5.percent(90)
});
series.data.setAll(data);
series.appear();
}
makeSeries("Europe", "europe", "#83cdf4");
makeSeries("North America", "namerica","#caa3ed");
makeSeries("Asia", "asia","#eec48b");
chart.appear(1000, 100);
});
</script>
</body>
</html>

jsChart variable dataset

I'm trying to convert the below JSON array into a variable dataset for jsChart. The datasent count is currently 3, however can be more. I can't seem to figure out how to update a multidimensional array and push new data into existing keys, and the data that doesn't have the proper keys.
dataset_array: {
"2022-09-22": {
"A": {
"qty": 11,
"turnover": 405,
},
"B": {
"qty": 59,
"turnover": 3234,
},
"C": {
"qty": 11,
"turnover": 794,
}
},
"2022-09-23": {
"A": {
"qty": 10,
"turnover": 790,
},
"B": {
"qty": 91,
"turnover": 5582,
},
"C": {
"qty": 7,
"turnover": 902,
}
}}
JSChart:
dataset_main = [];
dataset_length = 0;
$.each(dataset_array, function(date, entity){
header.push(date);
$.each(entity, function(name, values){
if(dataset_main.includes(name) == false) {
var valueToPush = { };
valueToPush["type"] = "bar";
valueToPush["backgroundColor"] = "green";
valueToPush["label"] = name;
valueToPush["yAxisID"] = "y";
valueToPush["data"] = [values.turnover];
/* Create Multi-dimensional Array */
// Array(A => array(turnover1, turnover2, turnover3), B => array(turnover1, 0, 0));
dataset_main.push( name, [valueToPush]);
dataset_length++;
} else {
/* Add values.turnover tot dataset_main[name].data */
}
});
});
//console.log('dataset: ' + JSON.stringify(dataset_main, null, '\t'));
//dataset_main.sort();
//dataset_main.length = dataset_length;
var ctx = document.getElementById('canvas_id').getContext('2d');
var chart = new Chart(ctx, {
type: "bar",
data: {
labels: header,
datasets: dataset_main
},
options: {
scales: {
yAxes: [{
id: 'y',
type: 'linear',
position: 'left',
ticks: {
beginAtZero: true
},
gridLines: {
display:false
}
}]
}
}
});
Result:
Uncaught TypeError: Cannot create property '$colorschemes' on string 'A'

Apache echarts - Schedule style chart layout

Does anyone know of a chart layout that functions like a schedule/timeline?
I currently have a chart that shows the on/off status of 3 valves over time.
It's a stacked line/area chart, it's works ok, but it's not the easiest to follow.
See linked screenshot of what I'm looking to achieve: Chart actual vs desired (Top is current chart, bottom is what I want to achieve).
Is this possible with apache echarts?
Thanks to #pthomson for pointing me in the right direction.
Here is what I've come up with: Codepen chart example
I'm still having some issues with the min/max values for the xAxis.
The min/max seems to be calculated on values in index 1, which is the end timestamp of each schedule piece, so not ideal.
var dom = document.getElementById('chart-container');
var myChart = echarts.init(dom, null, {
renderer: 'canvas',
useDirtyRect: false
});
var app = {};
var option;
const valveColors = [
"#f59527",
"#00e200",
"#2da8f3",
]
var data = [
{
"name": "Valve 1",
"value": [
1655647200000,
1657980000000,
0
]
},
{
"name": "Valve 3",
"value": [
1657980000000,
1659448800000,
2
]
},
{
"name": "Valve 1",
"value": [
1659448800000,
1660526144467,
0
]
},
{
"name": "Valve 2",
"value": [
1655647200000,
1660526144467,
1
]
}
];
option = {
xAxis: {
type: "time",
//Min is getting values from index 1, not sure why
min: range => range.min - (7 * 24 * 60 * 60 * 1000), //Subtract 7 days
},
yAxis: {
type: "category",
data: [ "Valve 3", "Valve 2", "Valve 1" ]
},
series: [
{
type: "custom",
renderItem: (params, api) => {
var catIndex = api.value(2);
var timeSpan = [api.value(0), api.value(1)];
var start = api.coord([timeSpan[0], 2 - catIndex]);
var end = api.coord([timeSpan[1], 2 -catIndex]);
var size = api.size([0,1]);
var height = size[1] * 0.6;
var rect = echarts.graphic.clipRectByRect(
{ x: start[0], y: start[1] - height / 2, width: end[0] - start[0], height: height},
{ x: params.coordSys.x, y: params.coordSys.y, width: params.coordSys.width, height: params.coordSys.height}
);
return (
rect && {
type: "rect",
transition: ["shape"],
shape: rect,
style: {
fill: valveColors[catIndex],
},
}
);
},
encode: {
x: [0,1],
y: 0,
},
data: data,
}
],
tooltip: {
show: true,
trigger: "item",
formatter: params => {
return `${params.data.name}<br/> ${params.data.value[0]} - ${params.data.value[1]}` //Unix timestamps should be converted to readable dates
}
},
dataZoom: [
{
type: "slider",
filterMode: "none"
},
],
}
if (option && typeof option === 'object') {
myChart.setOption(option);
}
window.addEventListener('resize', myChart.resize);

How to Create synchronized High chart with different no of series per chart

I want to create a synchronized High chart with two charts and the 1st chart will have one series and 2nd chart will have 2 series.
this link contains the sync chart with two series each chart, but when I reduced data to my required condition crosshair did not work properly.
data for chart.
var activity = {
"xData": [1, 1056, 2161, 3215, 4267],
"datasets": [{
"name": "Chart 1 series 1",
"data": [0, 10, 20, 30, 20],
"unit": "ms",
"type": "line",
"valueDecimals": 1
}, {
"name": "Chart 1 series 2",
"data": [23, 84, 22, 5, 75],
"unit": "ms",
"type": "line",
"valueDecimals": 1
}, {
"name": "Chart 2 series 1",
"data": [0, 10, 20, 30, 20],
"unit": "%",
"type": "line",
"valueDecimals": 1
}]
You are going from 2 series on each chart to 2 series on the 1st chart and 1 series on the 2nd chart, so you will be getting an error while trying your updated activity data. You need to add a conditional check like this (comments in uppercase):
/*
The purpose of this demo is to demonstrate how multiple charts on the same page can be linked
through DOM and Highcharts events and API methods. It takes a standard Highcharts config with a
small variation for each data set, and a mouse/touch event handler to bind the charts together.
*/
$(function () {
/**
* In order to synchronize tooltips and crosshairs, override the
* built-in events with handlers defined on the parent element.
*/
$('#container').bind('mousemove touchmove', function (e) {
var chart,
points,
i;
for (i = 0; i < Highcharts.charts.length; i++) {
chart = Highcharts.charts[i];
e = chart.pointer.normalize(e); // Find coordinates within the chart
// CHECK IF WE HAVE 2:
if ( chart.series.length === 2 ){
points = [chart.series[0].searchPoint(e, true), chart.series[1].searchPoint(e, true)]; // Get the hovered point
if (points[0] && points[1]) {
points[0].onMouseOver(); // Show the hover marker
points[1].onMouseOver(); // Show the hover marker
chart.tooltip.refresh(points); // Show the tooltip
chart.xAxis[0].drawCrosshair(e, points[0]); // Show the crosshair
}
// CHECK IF WE HAVE 1 CHART:
} else {
points = [chart.series[0].searchPoint(e, true)]; // Get the hovered poi
if (points[0]) {
points[0].onMouseOver(); // Show the hover marker
chart.tooltip.refresh(points); // Show the tooltip
chart.xAxis[0].drawCrosshair(e, points[0]); // Show the crosshair
}
}
}
});
/**
* Override the reset function, we don't need to hide the tooltips and crosshairs.
*/
Highcharts.Pointer.prototype.reset = function () {};
/**
* Synchronize zooming through the setExtremes event handler.
*/
function syncExtremes(e) {
var thisChart = this.chart;
Highcharts.each(Highcharts.charts, function (chart) {
if (chart !== thisChart) {
if (chart.xAxis[0].setExtremes) { // It is null while updating
chart.xAxis[0].setExtremes(e.min, e.max);
}
}
});
}
// Get the data. The contents of the data file can be viewed at
// https://github.com/highslide-software/highcharts.com/blob/master/samples/data/activity.json
//$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=activity.json&callback=?', function (activity) {
var activity = {
"xData": [1, 1056, 2161, 3215, 4267],
"datasets": [{
"name": "Chart 1 series 1",
"data": [0, 10, 20, 30, 20],
"unit": "ms",
"type": "line",
"valueDecimals": 1
}, {
"name": "Chart 1 series 2",
"data": [23, 84, 22, 5, 75],
"unit": "ms",
"type": "line",
"valueDecimals": 1
}, {
"name": "Chart 2 series 1",
"data": [0, 10, 20, 30, 20],
"unit": "%",
"type": "line",
"valueDecimals": 1
}]
},
lastChart;
$.each(activity.datasets, function (i, dataset) {
// Add X values
dataset.data = Highcharts.map(dataset.data, function (val, i) {
return [activity.xData[i], val];
});
if(i%2 == 0) { //first series of chart
$('<div class="chart">')
.appendTo('#container')
.highcharts({
chart: {
marginLeft: 40, // Keep all charts left aligned
spacingTop: 20,
spacingBottom: 20,
// zoomType: 'x'
// pinchType: null // Disable zoom on touch devices
},
title: {
text: dataset.name.slice(0,7),
align: 'left',
margin: 0,
x: 30
},
credits: {
enabled: false
},
legend: {
enabled: false
},
xAxis: {
crosshair: true,
events: {
setExtremes: syncExtremes
},
labels: {
format: '{value} km'
}
},
yAxis: {
title: {
text: null
}
},
tooltip: {
shared: true,
headerFormat: '',
valueDecimals: dataset.valueDecimals
},
series: [{
data: dataset.data,
name: dataset.name,
type: dataset.type,
color: Highcharts.getOptions().colors[i],
fillOpacity: 0.3,
tooltip: {
visible:true
}
}]
});
} else { //second series of chart
lastChart = Highcharts.charts[Highcharts.charts.length-1];
lastChart.addSeries({
data: dataset.data,
name: dataset.name,
type: dataset.type,
color: Highcharts.getOptions().colors[i],
fillOpacity: 0.3,
tooltip: {
valueSuffix: ' ' + dataset.unit
}
});
}
});
//});
});
Working JSFiddle: http://jsfiddle.net/kostasx/kfwtv1zj/

Highcharts determine if text is outside of data point

I’m wondering if there is a way to distinguish the data point being inside or outside of a bar.
Basically, with the chart below
I want the text 9178 to be in a colour I generate (because it’s inside a bar)
I want the rest of the numbers to be black (because they are outside of bars)
I’m wondering if the API provides something that I can use programmatically to decide what colour to use.
e.g. If a data point has a method isOverflow() that I can call, then I can set the colour to black if it returns true.
created on jsfiddle: https://jsfiddle.net/Mingzilla/pjqhqn4u/6
var generateColor = function(item) {
var lightColor = "#DADADA";
var color = new Highcharts.Color(item.point.color).rgba;
var isLightBg = color[0] + color[1] + color[2] > 384;
return isLightBg ? '#000000' : lightColor;
};
$(function() {
$('#container').highcharts({
title: {
text: 'I want data point inside a bar to be my colour, and outside to be black'
},
chart: {
type: 'column'
},
xAxis: {
categories: ['Car Insurance',
'Life Insurance',
'Pet Insurance'
]
},
plotOptions: {
series: {
dataLabels: {
enabled: true,
overflow: 'justify',
style: {
fontWeight: 'normal',
textShadow: 'none'
},
formatter: function() {
var item = this;
return '<span style="fill:' +
generateColor(item) + '">' +
(item.point.formattedValue || item.point.y) +
'</span>';
}
}
}
},
series: [{
"color": "#666699",
"name": "North",
"data": [{
"color": "#666699",
"name": "Car Insurance",
"y": 9178
}, {
"color": "#666699",
"name": "Life Insurance",
"y": 4518
}, {
"color": "#666699",
"name": "Pet Insurance",
"y": 1450
}]
}, {
"color": "#663366",
"name": "South",
"data": [{
"color": "#663366",
"name": "Car Insurance",
"y": 2129
}, {
"color": "#663366",
"name": "Life Insurance",
"y": 1066
}, {
"color": "#663366",
"name": "Pet Insurance",
"y": 374
}]
}]
});
});
Defaulty the color is based on contrast or declared single color. But you can return a color (i.e black) for all datalabels. Then catch load / redraw events in the chart object to interate on all dataLabels. Inside each of them, there is information about position and verticalAlign, which can be used to check where label is. If this is top, apply your lightColor by css() method.
var lightColor = "#DADADA";
function datalabelColor() {
var chart = this,
series = chart.series,
each = Highcharts.each,
dL;
each(series, function(serie, i) {
each(serie.data, function(p, j) {
dL = p.dataLabel;
if(dL.alignOptions.verticalAlign === 'top') {
dL.css({
color: lightColor
});
}
});
});
}
Example:
- https://jsfiddle.net/mys5k9ty/
Let me know if you have any further questions.

Categories