When creating a combo chart with 2 different sets of data(national and regional).
I get a different result.
The regional dataset works but the national dataset makes a new line for every bar.
I can't personally find where I went wrong and it might be something obvious im missing.
This is part of the data I am using (full data in jsfiddle)
SupplyRaw = {
"regional": [
{
"category": 1,
"min": 75,
"max": 150,
"avarage": 113,
"standardDeviation": 32.036437588054845
}
],
"national": [
{
"category": 3,
"min": 20,
"max": 464,
"avarage": 104,
"standardDeviation": 55.76672091291433
}
]
}
and this is how I convert the data for the combo chart
var header = [['Category', 'Min',
'Min Deviation',
'Max Divation', 'Max',
{ type: 'string', role: 'style' },
'Avarage']];
var dataRegional = google.visualization.arrayToDataTable(
header.concat(SupplyRaw.regional.map(function (x) {
return [x.category, x.min,
x.avarage - x.standardDeviation,
x.avarage + x.standardDeviation,
x.max, x.category == selected ?
'color: #244c8e' : 'color: #4285f4', x.avarage];
})), false);
var dataNational = google.visualization.arrayToDataTable(
header.concat(SupplyRaw.national.map(function (x) {
return [x.category, x.min,
x.avarage - x.standardDeviation,
x.avarage + x.standardDeviation,
x.max, x.category == selected ?
'color: #244c8e' : 'color: #4285f4', x.avarage];
})), false);
var options = {
legend: 'none',
hAxis: {
title: 'Metrage', gridlines: { count: 7 },
ticks: new Array(7).fill().map(function (val, i) {
return { v: i, f: getMetrageCategoryString(i) };
})
},
seriesType: "candlesticks",
series: { 1: { type: "line" } },
animation: {
duration: 1000,
easing: 'out'
},
};
var chart =
new google.visualization.ComboChart(document.getElementById("chart_div"));
chart.draw(dataRegional, options);
I've put my code in this jsFiddle : https://jsfiddle.net/znrsrhzc/1/
I hope someone can spot the issue here
since the data is not loaded in order of the x-axis,
need to sort the data table before drawing.
dataNational.sort([{column: 0}]);
see following working snippet...
SupplyRaw = {
"regional": [
{
"category": 1,
"min": 75,
"max": 150,
"avarage": 113,
"standardDeviation": 32.036437588054845
},
{
"category": 2,
"min": 89,
"max": 162,
"avarage": 117,
"standardDeviation": 26.979004182264877
},
{
"category": 3,
"min": 31,
"max": 50,
"avarage": 42,
"standardDeviation": 10.214368964029715
}
],
"national": [
{
"category": 3,
"min": 20,
"max": 464,
"avarage": 104,
"standardDeviation": 55.76672091291433
},
{
"category": 6,
"min": 20,
"max": 115,
"avarage": 65,
"standardDeviation": 28.04067083325969
},
{
"category": 1,
"min": 23,
"max": 500,
"avarage": 192,
"standardDeviation": 89.87525674143646
},
{
"category": 7,
"min": 25,
"max": 100,
"avarage": 49,
"standardDeviation": 23.556315501368204
},
{
"category": 4,
"min": 20,
"max": 300,
"avarage": 88,
"standardDeviation": 48.83288977327806
},
{
"category": 5,
"min": 20,
"max": 210,
"avarage": 72,
"standardDeviation": 38.35082894261975
},
{
"category": 2,
"min": 20,
"max": 500,
"avarage": 137,
"standardDeviation": 72.39801425371081
}
]
}
function getMetrageCategoryString(id) {
if (id === 1)
return '< 250';
else if (id === 2)
return '250 - 500';
else if (id === 3)
return '500 - 1k';
else if (id === 4)
return '1k - 2.5k';
else if (id === 5)
return '2.5k - 5k';
else if (id === 6)
return '5k - 10k';
else if (id === 7)
return '10k >';
else
return "";
}
var selected = 2;
google.charts.load('current', {
callback: function () {
var header = [['Category', 'Min',
'Min Deviation',
'Max Divation', 'Max',
{ type: 'string', role: 'style' },
'Avarage']];
var dataRegional = google.visualization.arrayToDataTable(
header.concat(SupplyRaw.regional.map(function (x) {
return [x.category, x.min,
x.avarage - x.standardDeviation,
x.avarage + x.standardDeviation,
x.max, x.category == selected ?
'color: #244c8e' : 'color: #4285f4', x.avarage];
})), false);
var dataNational = google.visualization.arrayToDataTable(
header.concat(SupplyRaw.national.map(function (x) {
return [x.category, x.min,
x.avarage - x.standardDeviation,
x.avarage + x.standardDeviation,
x.max, x.category == selected ?
'color: #244c8e' : 'color: #4285f4', x.avarage];
})), false);
var options = {
legend: 'none',
hAxis: {
title: 'Metrage', gridlines: { count: 7 },
ticks: new Array(7).fill().map(function (val, i) {
return { v: i, f: getMetrageCategoryString(i) };
})
},
seriesType: "candlesticks",
series: { 1: { type: "line" } },
animation: {
duration: 1000,
easing: 'out'
},
};
var chart =
new google.visualization.ComboChart(document.getElementById("chart_div"));
chart.draw(dataRegional, options);
$("#Regional").click(function () {
dataRegional.sort([{column: 0}]);
chart.draw(dataRegional, options);
});
$("#National").click(function () {
dataNational.sort([{column: 0}]);
chart.draw(dataNational, options);
});
},
packages: ['corechart', 'table']
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://www.gstatic.com/charts/loader.js"></script>
<div id="chart_div"></div>
<button id="Regional" type="button">Regional</button>
<button id="National" type="button">National</button>
Related
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>
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'
https://jsfiddle.net/dcfmL4br/
series: [{
name:'İstanbul',
data: [
{ x: 1246665600000, y: 100, z: 20.8, country: 'İstanbul' },
{ x: 1246752000000, y: 95, z: 23.8, country: 'İstanbul' },
],
color:Highcharts.getOptions().colors[2]
},
{
name:'Ankara',
data: [
{ x: 1246665600000, y: 96, z: 10.8, country: 'Ankara' },
{ x: 1246752000000, y: 97, z: 13.8, country: 'Ankara' },
],
color:Highcharts.getOptions().colors[3]
}
]
I have a series like that for fourth of july and fifth of july. But between these points, hours are shown. How do I hide xaxis points when there is no corresponding y axis value for them?
In labels.formatter function you can loop through series points to find out whether the current value is used:
xAxis: {
...,
labels: {
formatter: function() {
var series = this.chart.series,
s,
p,
usedValue;
for (s of series) {
if (usedValue) {
break;
}
for (p of s.xData) {
if (p === this.pos) {
usedValue = true;
break;
}
}
}
if (usedValue) {
return Highcharts.dateFormat(this.dateTimeLabelFormat, this.pos)
}
}
}
}
Live demo: https://jsfiddle.net/BlackLabel/db8pqLty/
API Reference:
https://api.highcharts.com/class-reference/Highcharts#.dateFormat
https://api.highcharts.com/highcharts/xAxis.labels.formatter
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/
You can change the width of the pointer by setting the cap:size value, but I need to change the length of the pointers so that they are in a tiered fashion representing 5 different points in time. How can this be done?
function createGauge() {
$("#gauge").kendoRadialGauge({
pointer: [{
value: 10,
color: "#c20000",
cap: {
size: 0.19
}
}, {
value: 70,
color: "#ff7a00",
cap: {
size: 0.15
}
}, {
value: 140,
color: "#ffc700",
cap: {
size: 0.11
}
}, {
value: 350,
color: "#ffe700",
cap: {
size: 0.07
}
}, {
value: 313,
color: "#fff700",
cap: {
size: 0.03
}
}],
scale: {
minorUnit: 5,
startAngle: 90,
endAngle: 450,
max: 360
}
});
}
$(document).ready(function() {
createGauge();
$("#example .slider").each(function() {
$(this).kendoSlider({
min: 0,
max: 360,
showButtons: true,
change: function() {
var id = this.element.attr("id");
var pointerIndex = id.substr(id.length - 1);
var gauge = $("#gauge").data("kendoRadialGauge");
gauge.pointers[pointerIndex].value(this.value());
}
});
});
$("#getValues").click(function() {
alert("All values: " + $("#gauge").data("kendoRadialGauge").allValues().join(", "));
});
$("#setValues").click(function() {
var values = $("#newValues").val().split(",");
values = $.map(values, function(val) {
return parseInt(val);
});
$("#gauge").data("kendoRadialGauge").allValues(values);
});
$(document).bind("kendo:skinChange", function(e) {
createGauge();
});
});
#gauge {
width: 33em;
height: 33em;
//margin: 0 auto 0;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="http://cdn.kendostatic.com/2014.3.1411/js/kendo.all.min.js"></script>
<div id="gauge-container">
<div id="gauge"></div>
</div>
According to Telerik you can't. At least for the current version.
http://www.telerik.com/forums/change-length-of-radial-gauge-pointer
I used in ANGULARJS below code is used to reduce the length of the pointer.
gauge.options.scale.labels = {
border: { width:13 }
};
I used label position as 'outside'.
Yes, you can change the pointer length now. pointer.length:
The pointer length (in percent) that is based on the distance to the scale. The default length of 1 indicates that the pointer exactly reaches the scale. Accepts values between 0.1 and 1.5.
function createGauge() {
$("#gauge").kendoRadialGauge({
pointer: [{
value: 10,
color: "#c20000",
cap: {
size: 0.19
},
length: 1,
}, {
value: 70,
color: "#ff7a00",
cap: {
size: 0.15
},
length: 0.8,
}, {
value: 140,
color: "#ffc700",
cap: {
size: 0.11
},
length: 0.6,
}, {
value: 350,
color: "#ffe700",
cap: {
size: 0.07
},
length: 0.5,
}, {
value: 313,
color: "#fff700",
cap: {
size: 0.03
},
length: 0.4,
}],
scale: {
minorUnit: 5,
startAngle: 90,
endAngle: 450,
max: 360
}
});
}
$(document).ready(function() {
createGauge();
$("#example .slider").each(function() {
$(this).kendoSlider({
min: 0,
max: 360,
showButtons: true,
change: function() {
var id = this.element.attr("id");
var pointerIndex = id.substr(id.length - 1);
var gauge = $("#gauge").data("kendoRadialGauge");
gauge.pointers[pointerIndex].value(this.value());
}
});
});
$("#getValues").click(function() {
alert("All values: " + $("#gauge").data("kendoRadialGauge").allValues().join(", "));
});
$("#setValues").click(function() {
var values = $("#newValues").val().split(",");
values = $.map(values, function(val) {
return parseInt(val);
});
$("#gauge").data("kendoRadialGauge").allValues(values);
});
$(document).bind("kendo:skinChange", function(e) {
createGauge();
});
});
#gauge {
width: 33em;
height: 33em;
//margin: 0 auto 0;
}
<script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
<script src="https://kendo.cdn.telerik.com/2021.2.511/js/kendo.all.min.js"></script>
<div id="gauge-container">
<div id="gauge"></div>
</div>