I'm using JQuery, ChartJS, Moment.js gathering data in JSON format for multiple charts on the same page, but from the same JSON source. In the JSON the height objects are one graph and the lengths another one.
This is an example of how the JSON looks
"Series": {
"heights": [
{
"Date": "2014-10-01",
"Value": 22
},
{
"Date": "2014-10-01",
"Value": 53
},
{
"Date": "2014-10-01",
"Value": 57
},
],
"lengths": [
{
"Date": "2014-10-01",
"Value": 54
},
{
"Date": "2014-10-01",
"Value": 33
}
]
}
I've managed to loop through the JSON to display each graph but I'm not really able to do it using the "DRY - Don't repeat yourself" way. Which now I have large chunks of code that is hard to update/read.
$.getJSON("data.json", function(data) {
var dateArray = [];
var valueArray = [];
for ( var i = 0; i < data.Series["heights"].length; i++) {
var obj = data.Series.heights[i];
var date = obj["Date"].toString();
var Value = obj["Value"];
for ( var key in obj) {
//console.log(obj["Value"]);
//date = obj["Date"];
Value = obj[key].toString();
}
valueArray.push(Value);
dateArray.push(moment(date).format("MMMM Mo"));
var dataArray = {
labels: dateArray,
datasets: [
{
label: "Lengths",
strokeColor: "rgb(26, 188, 156)",
pointColor: "rgba(220,220,220,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(220,220,220,1)",
data: valueArray
}
]
};
}
var ctx = document.getElementById("lengthsChart").getContext("2d");
var myLineChart = new Chart(ctx).Line(dataArray, {
scaleShowGridLines : true,
bezierCurve : true,
bezierCurveTension : 0.4,
datasetStroke : false,
fillColor: "rgba(0,0,0,0)",
datasetFill : false,
responsive: true,
showTooltips: true,
animation: false
});
});
Right now I have this code in a switch statement for "heights", "lengths" etc... Which I guess is a horrible way to do it. But I've been unable to make a loop for the individual charts.
I've tried things like this:
for(var x in measurement) {
console.log(measurement[x]);
for ( var i = 0; i < data.Series.hasOwnProperty(measurement).length; i++) {
var obj = data.Series.hasOwnProperty(measurement)[i];
var date = obj["Date"].toString();
var Value = obj["Value"];
console.log(date, Value);
}
But I'm unable to get it to work, to loop through the data.Series. /heights/lengths../ [i]
I'm very thankful for tips how to accomplish this.
Thanks!
If you replace measurement with data.Series and get rid of the hasOwnProperty(measurement) thing, you are almost there. The only thing you need is a way to keep the transformation from a list of {Date, Value} objects to a pair of list of dates and value for each serie.
var series = {};
// This loop is looping across all the series.
// x will have all the series names (heights, lengths, etc.).
for (var x in data.Series) {
var dates = [];
var values = [];
// Loop across all the measurements for every serie.
for (var i = 0; i < data.Series[x].length; i++) {
var obj = data.Series[x][i];
// Assuming that all the different series (heights, lengths, etc.) have the same two Date, Value attributes.
dates.push(obj.Date);
values.push(obj.Value);
}
// Keep the list of dates and values by serie name.
series[x] = {
dates: dates,
values: values
};
}
series will contain this:
{
heights: {
dates: [
'2014-10-01',
'2014-10-01',
'2014-10-01'
],
values: [
22,
53,
57
]
},
lengths: {
dates: [
'2014-10-01',
'2014-10-01'
],
values: [
54,
33
]
}
}
So you can use them like this:
console.log(series);
console.log(series.heights);
console.log(series.heights.dates);
console.log(series.heights.values);
console.log(series.lengths);
console.log(series.lengths.dates);
console.log(series.lengths.values);
Related
I am really struggling with the Object transformation. I have an array of Object and want to transform it into Highcharts multi line chart input. I want to get the unique dates first sorted from low to high, which will go into x axis and then transform the Original based on ID and date. The length for each ID.data is going to remain same (for whatever date count is not available for that date it will come as null)
Original:
[
{
"date": "1997-09-29",
"Count": 100,
"ID": "AB12-R"
},
{
"date": "1997-12-30",
"Count": 104.7,
"ID": "AB13-R"
},
{
"date": "1998-03-30",
"Count": 981,
"ID": "BA12-R"
},
{
"date": "1998-06-01",
"Count": 341,
"ID": "BA12-R"
}
]
Transformed:
[{
Identiy : 'AB12-R',
data : [100,null,null,null]
},
{
Identiy : 'AB13-R',
data : [null,104.7,null,null]
},{
Identiy : 'BA12-R',
data : [null,null,981,341]
}]
I have tried with reduce but nothing is working it seems. I am able to group it by ID but not able to handle the null and missing count, Can someone please help me here ?
This is what i have tried:
const result = Original.reduce(function (r, a) {
r[a.ID] = r[a.ID] || [];
r[a.ID].push(a);
return r;
}, Object.create(null));
console.log({'Transformed':result})
Take a look at this solution:
const hcData = [];
data.forEach((d, i) => {
const checkIfExist = hcData.find(data => data.id === d["ID"])
if (checkIfExist) {
checkIfExist.data[i] = d["Count"];
} else {
const initialData = [...Array(data.length)]
initialData.fill(null, 0, data.length)
initialData[i] = d["Count"];
hcData.push({
data: initialData,
id: d["ID"]
})
}
})
Demo: https://jsfiddle.net/BlackLabel/k2dg1wns/
Im working with chartJS to populate graphs using a large recordset.
Im creating an ArrayOfArrays like follows:
var arrayOfArrays =
JSON.parse('#Html.Raw(Json.Encode(Model.organisationData))');
I am now trying to create multiple arrays to store the data to populate the graph. To do so I am doing the following:
var array0=[]
var array1=[]
var array2=[]
var array3=[]
...etc...up to var array17=[]
I populate each array like follows:
for (var i = 0; i < 5; i++) {
array1.push(arrayOfArrays[i]['dataRows'][1]['dataCount']);
}
array1.push(arrayOfArrays[1]['dataRows'][1]['dataLabel']); //Item 5 - Label
for (var i = 0; i < 5; i++) {
array2.push(arrayOfArrays[i]['dataRows'][2]['dataCount']);
}
array2.push(arrayOfArrays[1]['dataRows'][2]['dataLabel']);//Item 5 - Label
etc..etc...repeated 17 times....
Then to populate the chart I'm doing..
var options = {
type: 'bar',
data: {
datasets: [
{
label: #array0[5]",
data: #array0
},
{
label: #array1[5]",
data: #array1
},
{
label: #array2[5]",
data: #array2
}
.....etc....etc.....17 times...
]
},
options: {
scales: {
yAxes: [{
ticks: {
reverse: false
}
}]
}
}
}
Is there a more efficient way of doing this? Instead of repeating everything 17 times can i create a loop that generates each of the arrays. Then populate those arrays and use them in chartJS.
I have a Chart that shows multiple time series. The different time series does not sample at the same time. Is there a way I can show all series in the tooltip? In the example, you can see that all series are included in the tooltip for the 2 first points as they are sampled at the same time. For the rest of the points, only 1 series is included.
var myChart = echarts.init(document.getElementById('main'));
var series = [{
"name": "sensor 1",
"data": [{
"value": [
"2019-02-20T11:47:44.000Z",
22.2
]
},
{
"value": [
"2019-02-20T12:03:02.000Z",
22.1
]
},
{
"value": [
"2019-02-20T12:18:19.000Z",
22.15
]
},
{
"value": [
"2019-02-20T12:33:36.000Z",
22.2
]
},
{
"value": [
"2019-02-20T12:48:53.000Z",
22.15
]
}
],
"type": "line"
},
{
"name": "sensor 2",
"data": [{
"value": [
"2019-02-20T11:47:44.000Z",
23.2
]
},
{
"value": [
"2019-02-20T12:03:02.000Z",
23.1
]
},
{
"value": [
"2019-02-20T12:22:19.000Z",
24.15
]
},
{
"value": [
"2019-02-20T12:39:36.000Z",
21.2
]
},
{
"value": [
"2019-02-20T12:52:53.000Z",
20.15
]
}
],
"type": "line"
}
]
var option = {
legend: {},
tooltip: {
trigger: 'axis',
},
xAxis: {
type: 'time'
},
yAxis: {
scale: true
},
series: series,
};
myChart.setOption(option);
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/4.0.4/echarts.min.js"></script>
<div id="main" style="width: 500px;height:400px;"></div>
Solution explanation
As stated in this feature request on echarts' github, they plan to add what you're looking for in the future. But for now, it is still not supported.
So I found a workaround to have the tooltip display all series even if they don't have a value at the exact x where the axisPointer is. To do so, I used the tooltip formatter that can be defined as a callback function that is called every time the tooltip has to be changed (i.e. every time the axisPointer moves on a new value) and where you can specify your own tooltip format.
Inside this function, you have access to every piece of information about the data at the axisPointer (especially its xAxis value in our case). Given the xAxis value of the axisPointer, we can go through our series and find the closest value from that xAxis value.
formatter : (params) => {
//The datetime where the axisPointer is
var xTime = new Date(params[0].axisValue)
//Create our custom tooltip and add to its top the dateTime where the axisPointer is
let tooltip = `<p>${xTime.toLocaleString()}</p> `;
//Go through each serie
series.forEach((serie, index) => {
//Find the closest value
value = serie.data.reduce((prev, curr) => Math.abs(new Date(curr.value[0]).valueOf() - xTime.valueOf()) < Math.abs(new Date(prev.value[0]).valueOf() - xTime.valueOf()) ? curr : prev).value[1]
/* Add a line in our custom tooltip */
// Add the colored circle at the begining of the line
tooltip += `<p><span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color: ${myChart.getVisual({ seriesIndex: index }, 'color')}"></span>`
// Add the serie's name and its value
tooltip += `${serie.name} <b>${value}</b></p>`;
});
return tooltip;
}
Full code
Here is the full code, using your example :
var myChart = echarts.init(document.getElementById('main'));
var series = [{
"name": "sensor 1",
//step: "end",
"data": [{
"value": [
"2019-02-20T11:47:44.000Z",
22.2
]
},
{
"value": [
"2019-02-20T12:03:02.000Z",
22.1
]
},
{
"value": [
"2019-02-20T12:18:19.000Z",
22.15
]
},
{
"value": [
"2019-02-20T12:33:36.000Z",
22.2
]
},
{
"value": [
"2019-02-20T12:48:53.000Z",
22.15
]
}
],
"type": "line"
},
{
"name": "sensor 2",
//step: 'end',
"data": [{
"value": [
"2019-02-20T11:47:44.000Z",
23.2
]
},
{
"value": [
"2019-02-20T12:03:02.000Z",
23.1
]
},
{
"value": [
"2019-02-20T12:22:19.000Z",
24.15
]
},
{
"value": [
"2019-02-20T12:39:36.000Z",
21.2
]
},
{
"value": [
"2019-02-20T12:52:53.000Z",
20.15
]
}
],
"type": "line"
}
]
option = {
legend: {},
tooltip: {
trigger: 'axis',
formatter : (params) => {
//The datetime where the axisPointer is
var xTime = new Date(params[0].axisValue)
//Create our custom tooltip and add to its top the dateTime where the axisPointer is
let tooltip = `<p>${xTime.toLocaleString()}</p> `;
//Go through each serie
series.forEach((serie, index) => {
//Find the closest value
value = serie.data.reduce((prev, curr) => Math.abs(new Date(curr.value[0]).valueOf() - xTime.valueOf()) < Math.abs(new Date(prev.value[0]).valueOf() - xTime.valueOf()) ? curr : prev).value[1]
/* Add a line in our custom tooltip */
// Add the colored circle at the begining of the line
tooltip += `<p><span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color: ${myChart.getVisual({ seriesIndex: index }, 'color')}"></span>`
// Add the serie's name and its value
tooltip += `${serie.name} <b>${value}</b></p>`;
});
return tooltip;
}
},
xAxis: {
type: 'time'
},
yAxis: {
scale: true
},
series: series,
};
myChart .setOption(option)
<html>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.3.2/echarts.min.js"></script>
<div id="main" style="width: 600px; height:400px;"></div>
</body>
</html>
Further thoughts
Linear interpolation
Instead of displaying the value of the closest real point, we could also calculate the value with a simple linear interpolation.
Here is the formatter function with linear interpolation (not the most preformant, but working)
formatter : (params) => {
var xTime = new Date(params[0].axisValue)
let tooltip = `<p>${xTime.toLocaleString()}</p> `;
series.forEach((serie, index) => {
//Only works if series is chronologically sorted
prev_point = serie.data.reduce((prev, curr) => new Date(curr.value[0]).valueOf() <= xTime.valueOf() ? curr : prev)
next_point = serie.data.slice(0).reduce((prev, curr, i, arr) => {
if(new Date(curr.value[0]).valueOf() >= xTime.valueOf()) {
arr.splice(1);
}
return curr
})
var value = 0
if(next_point.value[1] == prev_point.value[1]){
value = next_point.value[1]
}
else {
//Linear interpolation
value = Math.round((prev_point.value[1] + (xTime.valueOf()/1000 - new Date(prev_point.value[0]).valueOf()/1000) * ((next_point.value[1] - prev_point.value[1]) / (new Date(next_point.value[0]).valueOf()/1000 - new Date(prev_point.value[0]).valueOf()/1000)))*10)/10
}
tooltip += `<p><span style="display:inline-block;margin-right:5px;border-radius:10px;width:9px;height:9px;background-color: ${myChart.getVisual({ seriesIndex: index }, 'color')}"></span> ${serie.name} <b>${value}</b></p>`;
});
return tooltip;
}
Display the series as steps
To make it visually more accurate, you can display the series as
step: 'end' and get the closest previous value instead of just the
closest value, using:
value = serie.data.reduce((prev, curr) => new Date(curr.value[0]).valueOf() <= xTime.valueOf() ? curr : prev).value[1]
Doing so, the value displayed in the tooltip will be exactly what you see on the graph.
Performance
The reduce() methods are slow on large datasets as they go through the whole series. For example, replacing it with binary search (dichotomic search) will drastically improve the performance.
I'd be interested if anyone has an idea to make it even more performant.
Excellent !
One option that may interest you: you can get the color from the graph:
${myChart.getVisual({ seriesIndex: index }, 'color')}
This way you don't need to use the color array.
I am migrating google charts to amCharts. I am using a data array like this:
[
[CITY, SUM],
[A, 1500],
[B, 1470],
[C, 1920]
]
I can use this in google charts. So this solution is very flexible and dynamic. And I do not set any value field ot category field like amCharts.
But I see that amCharts data should be json object array.
[
{CITY: A, SUM: 1500},
{CITY: B, SUM: 1470},
{CITY: C, SUM: 1920}
]
So I need to know value ad category propery for every dataset.
var chart = AmCharts.makeChart("chartdiv", {
"categoryField": "CITY",
"graphs": [{
"type": "column",
"valueField": "SUM"
}]
}
SO this is not very flexible.
Is there any solution to get;
first item of json object is categoryField
second item of solution is valueField
Or using google datatable data in amCharts.
This functionality is not available out of the box as AmCharts requires this information to be defined upfront.
You can certainly write a pre-processing method or a plugin through AmCharts' addInitHandler method to convert your data and create graphs for you. Here's a basic example which defines a custom dataTable property containing the settings needed to make a custom plugin work:
//mini plugin to handle google datatable array of arrays format
AmCharts.addInitHandler(function(chart) {
if (!chart.dataTable && !chart.dataTable.data && !chart.dataTable.graph) {
return;
}
var dataProvider;
var graphs = [];
var graphTemplate = chart.dataTable.graph;
var fields = chart.dataTable.data[0];
var data = chart.dataTable.data.slice(1);
fields.slice(1).forEach(function(valueField) {
graphs.push({
type: graphTemplate.type || "line",
fillAlphas: graphTemplate.fillAlphas || 0,
lineAlpha: graphTemplate.lineAlpha || 1,
valueField: valueField
});
});
dataProvider = data.map(function(arr) {
var dataObj = {};
arr.forEach(function(value, idx) {
dataObj[fields[idx]] = value;
})
return dataObj;
});
chart.categoryField = fields[0];
chart.graphs = graphs;
chart.dataProvider = dataProvider;
});
var chart = AmCharts.makeChart("chartdiv", {
"type": "serial",
"theme": "light",
//custom dataTable property used by the chart to accept dataTable format
"dataTable": {
"data": dataTable,
"graph": { //graph template for all value fields
"type": "column",
"fillAlphas": .8,
"lineAlpha": 1
}
}
});
You can extend this as much as you need.
Here's a demo using your data and an additional column of dummy data:
var dataTable = [
["CITY", "SUM", "AVG"],
["A", 1500, 500],
["B", 1470, 490],
["C", 1920, 640]
];
//mini plugin to handle google datatable array of arrays format
AmCharts.addInitHandler(function(chart) {
//check if the required properties for the plugin are defined before proceeding
if (!chart.dataTable && !chart.dataTable.data && !chart.dataTable.graph) {
return;
}
var dataProvider;
var graphs = [];
var graphTemplate = chart.dataTable.graph;
var fields = chart.dataTable.data[0];
var data = chart.dataTable.data.slice(1);
//create the graph objects using the graph template from the custom dataTable property
fields.slice(1).forEach(function(valueField) {
graphs.push({
type: graphTemplate.type || "line",
fillAlphas: graphTemplate.fillAlphas || 0,
lineAlpha: graphTemplate.lineAlpha || 1,
valueField: valueField
});
});
//construct the dataProvider array from the datatable data
dataProvider = data.map(function(arr) {
var dataObj = {};
arr.forEach(function(value, idx) {
dataObj[fields[idx]] = value;
})
return dataObj;
});
//update the chart properties
chart.categoryField = fields[0];
chart.graphs = graphs;
chart.dataProvider = dataProvider;
});
var chart = AmCharts.makeChart("chartdiv", {
"type": "serial",
"theme": "light",
//custom dataTable property used by the chart to accept dataTable format
"dataTable": {
"data": dataTable,
"graph": { //graph template for all value fields
"type": "column",
"fillAlphas": .8,
"lineAlpha": 1
}
}
});
html,
body {
width: 100%;
height: 100%;
margin: 0px;
}
#chartdiv {
width: 100%;
height: 100%;
}
<script src="//www.amcharts.com/lib/3/amcharts.js"></script>
<script src="//www.amcharts.com/lib/3/serial.js"></script>
<script src="//www.amcharts.com/lib/3/themes/light.js"></script>
<div id="chartdiv"></div>
I'm new in AmCharts.js. I want to create a chart with multiple value axis which represents occurences of prices on different websites for one product according to datetime (up to hours or better minutes) (not date).
So I need to draw chart with multiple lines which doesn't depends on each other. So when I one is null value, the value of second line is still drawn.
Every product can have different number of occurences so I can't hardcode colors and another properties of datasets.
One of the best approaches I found is AmStockChart because there can be drawn multiple lines. But there are multiple problems. One of them is that it needs to "compare" one line to another lines so if there is no value for datetime xxx, the value of line2 is not shown for this datetime.
The datetimes can differ (for one line is it 12.01 13:00, for another is it 14:00 etc).
This is my solution which doesn't work correctly since it has to be compared.
The JSON is: {'web_name':[[[year,month,day,hour...],price],[[[year,month....}
<script>
var lines = [];
var dataSets = [];
generateChartData();
function generateChartData() {
var google_chart_json = JSON;
var loopcounter = -1;
$.each(google_chart_json, function (key, val) {
var line = [];
loopcounter = loopcounter + 1;
$.each(val, function (_, scan) {
var year = scan[0][0];
var month = scan[0][1];
var day = scan[0][2];
var hour = scan[0][3];
var minute = scan[0][4];
var price = scan[1];
var data = {
'date': new Date(year, month - 1, day, hour, minute),
'value': price
};
line.push(data);
});
line.sort(function (lhs, rhs) {
return lhs.date.getTime() - rhs.date.getTime();
});
lines.push([key, line]);
});
console.log('LINES');
console.log(lines);
$.each(lines, function (_, name_line) {
var dict = {
'title': name_line[0],
"fieldMappings": [{
"fromField": "value",
"toField": "value"
}],
"dataProvider": name_line[1],
"categoryField": "date"
};
dataSets.push(dict);
});
}
console.log(dataSets)
var chart = AmCharts.makeChart("chartdiv", {
"allLabels": [
{
"text": "Free label",
"bold": true,
"x": 20,
"y": 20
}
],
categoryAxesSettings: {
minPeriod: "hh",//(at least that is not grouped)
groupToPeriods: ["DD", "WW", "MM"]//(Data will be grouped by day,week and month)
},
"type": "stock",
"theme": "light",
"dataSets": dataSets,
"panels": [{
"showCategoryAxis": false,
"title": "Value",
"percentHeight": 70,
"stockGraphs": [{
"id": "g1",
"valueField": "value",
"comparable": true,
"compareField": "value",
"balloonText": "[[date]][[title]]:<b>[[value]]</b>",
"compareGraphBalloonText": "[[title]]:<b>[[value]]</b>"
}],
"stockLegend": {
"periodValueTextComparing": "[[percents.value.close]]%",
"periodValueTextRegular": "[[value.close]]"
}
}],
{#https://docs.amcharts.com/javascriptcharts/ChartScrollbar#}
"chartScrollbarSettings": {
"graph": "g1",
"color": "#333333"
},
"chartCursorSettings": {
"valueBalloonsEnabled": true,
"fullWidth": true,
"cursorAlpha": 0.1,
"valueLineBalloonEnabled": true,
"valueLineEnabled": true,
"valueLineAlpha": 0.5
},
"periodSelector": {
"position": "left",
"periods": [{
"period": "MM",
"selected": true,
"count": 1,
"label": "1 month"
}, {
"period": "YYYY",
"count": 1,
"label": "1 year"
}, {
"period": "YTD",
"label": "YTD"
}, {
"period": "MAX",
"label": "MAX"
}]
},
"dataSetSelector": {
"position": "left",
},
"export": {
"enabled": true
}
});
chart.panelsSettings.recalculateToPercents = "never";
</script>
When I put the same datetimes for the values, it shows lines. But when each value has different datetime, it shows nothing except the first line:
Another solution (Line chart FiddleJS) has hardcoded lines which I can't do because there are different numbers of them. But the main problem is that they have own value axises.
Could you tell me what what to do in my code to achieve not compared multiple line chart with allowed different datetimes for different values and lines? Or if you know - recommend some type of amchart which can do this all?
The comparison requires that every date/time has to match or it won't show every point, as you noticed. In the AmCharts knowledge base, there's a demo that implements a mini-plugin that syncs the timestamps in your data prior to initializing the chart:
/**
* amCharts plugin: sync timestamps of the data sets
* ---------------
* Will work only if syncDataTimestamps is set to true in chart config
*/
AmCharts.addInitHandler(function(chart) {
// check if plugin is enabled
if (chart.syncDataTimestamps !== true)
return;
// go thorugh all data sets and collect all the different timestamps
var dates = {};
for (var i = 0; i < chart.dataSets.length; i++) {
var ds = chart.dataSets[i];
for (var x = 0; x < ds.dataProvider.length; x++) {
var date = ds.dataProvider[x][ds.categoryField];
if (dates[date.getTime()] === undefined)
dates[date.getTime()] = {};
dates[date.getTime()][i] = ds.dataProvider[x];
}
}
// iterate through data sets again and fill in the blanks
for (var i = 0; i < chart.dataSets.length; i++) {
var ds = chart.dataSets[i];
var dp = [];
for (var ts in dates) {
if (!dates.hasOwnProperty(ts))
continue;
var row = dates[ts];
if (row[i] === undefined) {
row[i] = {};
var d = new Date();
d.setTime(ts);
row[i][ds.categoryField] = d;
}
dp.push(row[i]);
}
dp.sort(function(a,b){
return new Date(a[ds.categoryField]) - new Date(b[ds.categoryField]);
});
ds.dataProvider = dp;
}
}, ["stock"]);
Just add this before your chart code and set the custom syncDataTimestamps property to true in the top level of your chart config and it will run upon initialization.