multiple value axis with datetimes - javascript

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.

Related

How can I show all series in tooltip when series does not have same time values

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.

Amcharts Sort stackbyvalue with bullet chart

I am using stackByValue of amcharts to arranged a stack column chart . I would like to add a bullet point on each chart to check if they meet a certain target or not. Currently what happen is the bullet point is added to the stacked chart is there a way that I could do this without removing the stackByValue ?
Here is my JsFiddle: `http://jsfiddle.net/sky5rvdz/13/
$(document).ready(function() {
AmCharts.addInitHandler(function(chart) {
// Check if enabled
if (chart.valueAxes === undefined || chart.valueAxes.length === 0 || !chart.valueAxes[0].stackByValue)
return;
// Disable built-in stacking
chart.valueAxes[0].stackType = "none";
// Prepare all graphs
for (var i = 0; i < chart.graphs.length; i++) {
var graph = chart.graphs[i];
graph.originalValueField = graph.valueField;
graph.valueField = graph.originalValueField + "Close";
graph.openField = graph.originalValueField + "Open";
graph.clustered = false;
if (graph.labelText)
graph.labelText = graph.labelText.split("[[value]]").join("[[" + graph.originalValueField + "]]");
if (graph.balloonText)
graph.balloonText = graph.balloonText.split("[[value]]").join("[[" + graph.originalValueField + "]]");
}
// Go through each category and order values
for (var i = 0; i < chart.dataProvider.length; i++) {
// Assemble intermediate array of data point items
var dp = chart.dataProvider[i];
var items = [];
var sum = 0;
for (var x = 0; x < chart.graphs.length; x++) {
var graph = chart.graphs[x];
items.push({
"graph": graph,
"value": dp[graph.originalValueField]
});
}
var sortValue = 0;
// Order according to value
items.sort(function(a, b) {
if (sortValue == 0) {
return a.value - b.value;
} else {
return b.value - a.value;
}
});
// Calculate open and close fields
var offset = 0;
for (var x = 0; x < items.length; x++) {
var item = items[x];
dp[item.graph.openField] = offset;
dp[item.graph.valueField] = offset + dp[item.graph.originalValueField];
offset = dp[item.graph.valueField];
}
}
}, ["serial"]);
var response = [{
"name": "Jan",
"target": 2062186.74,
"USA": 0,
"MAN": 605873.95,
"PAN": 759763.5
}, {
"name": "Feb",
"target": 1492210.81,
"MAN": 499538.43,
"PAN": 559504.95,
"USA": 5850
}, {
"name": "Mar",
"target": 1455750,
"MAN": 403715.2,
"PAN": 694353.95,
"USA": 0
}, {
"name": "Apr",
"target": 2008623.96,
"USA": 0,
"MAN": 409993.3,
"PAN": 511030
}];
var graphs = Object.keys(response[0]).reduce(function(graphsArray, key) {
if (key !== "name" && key !== "target") {
graphsArray.push({
"balloonText": "<b>[[value]]</b>",
"balloonFunction": function(item, graph) {
var result = graph.balloonText;
for (var key in item.dataContext) {
if (item.dataContext.hasOwnProperty(key) && !isNaN(item.dataContext[key])) {
var formatted = AmCharts.formatNumber(item.dataContext[key], {
precision: chart.precision,
decimalSeparator: chart.decimalSeparator,
thousandsSeparator: chart.thousandsSeparator
}, 2);
result = result.replace("[[" + key + "]]", formatted);
}
}
return result;
},
"fillAlphas": 0.8,
"labelText": "[[title]]<br>",
"labelPosition": "middle",
"lineAlpha": 0.3,
"title": key,
"type": "column",
"color": "#000000",
//"showAllValueLabels": true,
"valueField": key
});
}
if (key === "target") {
graphsArray.push({
"balloonText": "<b>[[value]]</b>",
"balloonFunction": function(item, graph) {
var result = graph.balloonText;
for (var key in item.dataContext) {
if (item.dataContext.hasOwnProperty(key) && !isNaN(item.dataContext[key])) {
var formatted = AmCharts.formatNumber(item.dataContext[key], {
precision: chart.precision,
decimalSeparator: chart.decimalSeparator,
thousandsSeparator: chart.thousandsSeparator
}, 2);
result = result.replace("[[" + key + "]]", formatted);
}
}
return result;
},
"valueAxis": "v2",
"lineAlpha": 0,
"bullet": "round",
"bulletSize": 20,
"title": "target",
"type": "line",
"valueField": "target"
});
}
return graphsArray;
}, []);
var chart = AmCharts.makeChart("chartdiv", {
"type": "serial",
"theme": "light",
"legend": {
"horizontalGap": 10,
"maxColumns": 1,
"position": "right",
"useGraphSettings": true,
"markerSize": 10
},
"numberFormatter": {
"precision": 1,
"decimalSeparator": ".",
"thousandsSeparator": ","
},
"dataProvider": response,
"valueAxes": [{
"id": "v1",
"stackType": "regular",
/**
* A proprietary setting `stackByValue` which is not an
* official config option. It will be used by our custom
* plugin
*/
"stackByValue": true,
"axisAlpha": 0.3,
"gridAlpha": 0
}, , {
"id": "v2",
"axisAlpha": 0.3,
"gridAlpha": 0,
"position": "top",
"title": "Target"
}],
"gridAboveGraphs": true,
"startDuration": 0,
"graphs": graphs,
"categoryField": "name",
"categoryAxis": {
"gridPosition": "start",
"axisAlpha": 0,
"gridAlpha": 0,
"position": "left"
},
"export": {
"enabled": true
}
});
console.log(graphs);
console.log(response);
Object.keys(response[0]).forEach(key => {
console.log(key) // returns the keys in an object
// console.log(a[key]) // returns the appropriate value
})
});
The issue is that the sort by value plugin assumes that all graphs need to be sorted and modified to use the open/close fields to achieve this effect, which causes it to move your bullet to an incorrect location. Since you have multiple axes, you can modify the plugin to check if the graph belongs to the first axis and set a flag to be used to re-add the point correctly:
// Go through each category and order values
for (var i = 0; i < chart.dataProvider.length; i++) {
// ...
for (var x = 0; x < chart.graphs.length; x++) {
var graph = chart.graphs[x];
items.push({
"graph": graph,
// check if this graph's data points need to be omitted from the sorting process.
"ignoreSort": (graph.valueAxis && graph.valueAxis !== chart.valueAxes[0].id),
"value": dp[graph.originalValueField]
});
}
// ...
// Calculate open and close fields
var offset = 0;
for (var x = 0; x < items.length; x++) {
var item = items[x];
if (!item.ignoreSort) {
//process the pont as normal if it doesn't have the flag set with open/value fields
dp[item.graph.openField] = offset;
dp[item.graph.valueField] = offset + dp[item.graph.originalValueField];
offset = dp[item.graph.valueField];
} else {
//otherwise treat the point as a normal graph and use the value field
dp[item.graph.valueField] = dp[item.graph.originalValueField]
}
}
}
You'll also want to synchronize the axes' min/max values so that your target is correctly placed with respect to your stacked bars. You can achieve this using another custom plugin through addInitHandler:
//synchronizes axes' min/max values using a custom synchronizeValueAxes property
//(while synchronizeGrid exists, it doesn't work with this particular chart)
AmCharts.addInitHandler(function(chart) {
if (chart.synchronizeValueAxes) {
setTimeout(function() {
var max = chart.valueAxes.reduce(function(max, axis) {
if (!isNaN(axis.max)) {
return Math.max(max, axis.max);
} else {
return max;
}
}, Number.MIN_VALUE);
var min = chart.valueAxes.reduce(function(min, axis) {
if (!isNaN(axis.min)) {
return Math.min(min, axis.min);
} else {
return min;
}
}, Number.MAX_VALUE);
chart.valueAxes.forEach(function(axis) {
axis.maximum = max;
axis.minimum = min;
axis.strictMinMax = true;
});
chart.validateData();
}, 500);
}
}, ["serial"]);
Updated fiddle

Drawing multiple chart Amchart using different data providers

I'm trying to draw multiple linear chart, I am using a socket.io connection to receive multiple Data (MQTT messages) from different sensors. The problem is that I cannot find a way to reuse the code for every chart, this is what I have so far:
var socket = io();
var chartData = [];
chart_config = {
"type": "serial",
"theme": "light",
"fontFamily": "sans-serif",
"fontSize" : "12",
"valueAxes": [{
"id": "v1",
"position": "left"
}],
"mouseWheelZoomEnabled": true,
"graphs": [{
"id": "g1",
"bullet": "round",
"bulletBorderAlpha": 1,
"bulletColor": "#FFFFFF",
"useLineColorForBulletBorder": true,
"hideBulletsCount": 50,
"valueField": "value",
"balloonText": "[[value]]",
"balloon":{
"drop":true
}
}],
"chartCursor": {
"limitToGraph":"g1"
},
"categoryField": "date",
"categoryAxis": {
"equalSpacing": true,
"dashLength": 1,
"minorGridEnabled": true
},
"dataProvider": chartData,
"export": {
"enabled": true
}
};
var chart = AmCharts.makeChart("V1_chart", chart_config);
socket.on('panel1_V', function (data) {
var newDate = new Date();
chartData.push({
date: newDate,
value: data.message
});
console.log('mqtt message: ' + data.message);
console.log(JSON.stringify(chartData));
if (chartData.length > 20) {
chartData.splice(0, chartData.length - 20);
}
chart.validateData();
});
thanks for the help in advance.
You will need to do a deep clone of the configuration object for every chart, keep the original unused.
The code below is an example how to clone the object in case you don't have a helper to do it.
/**
* Define function for cloning objects
* Taken from: http://stackoverflow.com/questions/728360/most-elegant-way-to-clone-a-javascript-object
*/
function clone(obj) {
var copy;
// Handle the 3 simple types, and null or undefined
if (null == obj || "object" != typeof obj) return obj;
// Handle Date
if (obj instanceof Date) {
copy = new Date();
copy.setTime(obj.getTime());
return copy;
}
// Handle Array
if (obj instanceof Array) {
copy = [];
for (var i = 0, len = obj.length; i < len; i++) {
copy[i] = clone(obj[i]);
}
return copy;
}
// Handle Object
if (obj instanceof Object) {
copy = {};
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) copy[attr] = clone(obj[attr]);
}
return copy;
}
throw new Error("Unable to copy obj! Its type isn't supported.");
}
/**
* Create first chart
*/
// create a copy of the universal config
var chartConfig1 = clone(chartConfig);
Please check this example: https://www.amcharts.com/kbase/reusing-the-same-config-object-for-multiple-charts/

Number of features by day javascript

I have a json document that looks like this:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"time": 1438342780,
"title": "Iran's foreign minister calls for world's nuclear weapons states to disarm",
"author": "Julian Borger",
"web_id": "world/2015/jul/31/iran-nuclear-weapons-states-disarm-israel"
},
"geometry": {
"type": "Point",
"coordinates": [
-77.26526,
38.90122
]
}
},
{
"type": "Feature",
"properties": {
"time": 1438300867,
"title": "Big bangs over the white cliffs of Dover as unique 1915 artillery gun is fired again",
"author": "Maev Kennedy",
"web_id": "world/2015/jul/31/big-bangs-over-white-cliffs-dover-unique-1915-artillery-gun-fired-again"
},
"geometry": {
"type": "Point",
"coordinates": [
1.3,
51.13333
]
}
}
]
}
I would like to fetch the 'feature' array inside json and return the number of features for a given day. For example, for the data above I would expect something like:
{
"date": 7/31/2015,
"number": 2
}
Currently I have something that looks like this:
d3.json('path/to/json', function(json) {
data = json;
});
Fairly new to js and d3 so a bit stumped. Let me know if you need any more details. Thanks in advance!
This will work for you, it returns an array of object. each object is the object you asked.
var a = yourJSONObject, var map = {}, output = [];
for (var i = 0; i < a.features.length; i++) {
var ref = new Date(a.features[i].properties.time*1000).toDateString();
if (map[ref] == undefined) {
map[ref] = output.push({
date: ref,
number: 1
}) - 1;
} else
output[map[ref]].number++
}
console.log(output) //[ { date: 'Sat Jan 17 1970', number: 2 } ]
The critical piece here is that your time values are in Epoch time, which means you'll have to convert them to preset dates using this technique.
Then you can traverse the features array, and track the count for each date.
var features = yourJSONObject.features;
var featuresByDate = {};
for (var i = 0, len = features.length; i < len; i++) {
// find the current feature's date
var epochTime = features[0].properties.time;
var date = new Date(0);
date.setUTCSeconds(epochTime);
// find the date in 7/31/2015 format
var dateStr = (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear();
// count the date for the first time if it has not been counted yet
if ( ! featuresByDate.hasOwnProperty(dateStr) ) {
featuresByDate[dateStr] = 1;
}
// otherwise, increment its counter
else {
featuresByDate[dateStr]++;
}
}
Two functions - one to get the correct date based on the epoch time, the other to iterate through the features building a temporary object, then iterating through the object to give you an array of date/number objects.
function getDate(time) {
var d = new Date(0);
d.setUTCSeconds(time);
return [d.getMonth() + 1, d.getDate(), d.getFullYear()].join('/');
}
function getData(data) {
var obj = data.features.reduce(function(p, c) {
var date = getDate(c.properties.time);
p[date] = (p[date] + 1) || 1;
return p;
}, {});
return Object.keys(obj).map(function (el) {
return { date: el, number: obj[el] };
});
}
getData(data);
OUTPUT
[
{
"date": "7/31/2015",
"number": 2
}
]
DEMO
I don't know D3, but you can do this with straight JS:
var json = {
"features": [{
"type": "Feature",
"properties": {
"time": 1438342780,
"title": "Iran's foreign minister calls for world's nuclear weapons states to disarm"
}
}, {
"type": "Feature",
"properties": {
"time": 1438300867,
"title": "Big bangs over the white cliffs of Dover as unique 1915 artillery gun is fired again"
}
}, {
"type": "Feature same date",
"properties": {
"time": 1448300867,
"title": "Big bangs over the white cliffs of Dover as unique 1915 artillery gun is fired again"
}
}]
}
var counts = {}
function secondsToDate(seconds) {
var date = new Date(1970,0,1);
date.setSeconds(seconds);
return date.toDateString();
}
json.features.reduce((counts, feature) => {
var date = secondsToDate(feature.properties.time)
if (counts[date]) {
counts[date]++
} else {
counts[date] = 1
}
return counts
}, counts)
console.log(counts) // {'Fri Jul 31 2015': 2, 'Mon Nov 23 2015': 1}
The missing bit is parsing your timestamp into a date.
Now grouping on dates. Maybe now the downvoter can undo that!
I added an object with a replicated timestamp to highlight the count going up.

Looping through json array properties

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);

Categories