I want to dynamically adjust the radius of circle-based extrusions with Mapbox based on the zoom level.
I have used for a toy dataset the solution provided by #stdob-- here
and for which the JS Fiddle is available here.
The problem with that solution is that it is computationally very expensive and with my real dataset (more than a million point) this is not a viable solution. I therefore thought about using queryRenderedFeatures() as suggested in the comments of the previous SO posts. However even that is not giving me a good enough interactive visualization.
Instead, I therefore wanted to initially load all of my dataset and layers (including the 3D extrusions) and then on map-zoom events only recompute the radius that is going to be used for the 3D extrusions.
Here is the code I used:
Here is simple geojson file to reproduce the error with
{"type": "FeatureCollection", "features": [{"id": 1, "type": "Feature", "properties": {"x": 1.0, "group": 1, "my_property": 217}, "geometry": {"type": "Point", "coordinates": [8.539961, 47.37347]}}, {"id": 2, "type": "Feature", "properties": {"x": 2.0, "group": 1, "my_property": 520}, "geometry": {"type": "Point", "coordinates": [8.517961, 47.37520]}}]}
the following code:
HTML:
<html>
<head>
<meta charset='utf-8' />
<title>Display buildings in 3D</title>
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.48.0/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.48.0/mapbox-gl.css' rel='stylesheet' />
<script src='https://npmcdn.com/#turf/turf/turf.min.js'></script>
<script src="https://unpkg.com/supercluster#4.1.1/dist/supercluster.min.js"></script>
</head>
<body>
<div id='map'></div>
<script>
</script>
</body>
</html>
CSS:
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
JS:
mapboxgl.accessToken = 'pk.eyJ1IjoibG9ubmliZXNhbmNvbiIsImEiOiJjamxjaWNpOHQwMHV0M3FwaHhneGhvY2l2In0.7GxI8W_dnTKITNF4hEvZeQ';
var map = new mapboxgl.Map({
style: 'mapbox://styles/mapbox/light-v9',
center:[8.538961, 47.37247],
zoom: 10,
pitch: 20,
bearing: 0,
container: 'map'
});
var url = "REPLACE WITH GEOJSON LOCATION"
//
var zoom_level_3D_bars = 14
var radius_zoom_d = 10
var map_zoom = 10
map.on('load', function() {
// Insert the layer beneath any symbol layer.
var layers = map.getStyle().layers;
var labelLayerId;
for (var i = 0; i < layers.length; i++) {
if (layers[i].type === 'symbol' && layers[i].layout['text-field']) {
labelLayerId = layers[i].id;
break;
}
}
map.addSource("data", {
type: "geojson",
data: url,
});
map.addLayer({
'id': 'extrusion',
'type': 'fill-extrusion',
'minzoom': zoom_level_3D_bars,
"source": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": []
}
},
'source': 'data',
'paint': {
'fill-extrusion-height': ['/', ['number', ['get', 'my_property'],0], 10],
'fill-extrusion-base': 0,
'fill-extrusion-opacity': 0.5
}
});
map.addLayer({
'id': 'population',
'type': 'circle',
'source': 'data',
'paint': {
'circle-color': {
'property': 'group',
'type': 'categorical',
stops: [
[1, 'rgba(252,141,98,1)'],
[2, 'rgba(102,194,165,1)'],
[3, 'rgba(102,194,165,1)'],
[4, 'rgba(102,194,165,1)'],
[5, 'rgba(102,194,165,1)'],
[6, 'rgba(102,194,165,1)'],
//'4', '#3bb2d0',
/* other 'rgba(102,194,165,0.1)'*/
]
},
}
});
map.on('data', function() {
//if (!firstTower) updateTower();
//});
//console.log("Initialize")
//initializeTower();
})
map.on('zoom', function() {
map_zoom = map.getZoom();
if(map.isSourceLoaded('data') == false){
return
}
if(map_zoom < zoom_level_3D_bars){
map.setPaintProperty('population', 'circle-radius', radius_zoom_d);
if(map.getPaintProperty('population','circle-opacity') != 1){
map.setPaintProperty('population', 'circle-opacity', 1)
}
}
radius_zoom_d = 10 - (map_zoom/2)
if(map_zoom >= zoom_level_3D_bars){
opacity_point = 0
console.log("Update tower bc zoom = "+map_zoom)
if(map.getPaintProperty('population','circle-opacity') != 0){
map.setPaintProperty('population', 'circle-opacity', 0)
}
updateTower();
}
})
function updateTower() {
var radiusPX = false;
var layer = map.getLayer('population')
if (layer.paint) radiusPX = map.getLayer('population').paint.get('circle-radius').evaluate();
if (radiusPX === false) return;
var data = {
"type": "FeatureCollection",
"features": []
}
//HERE IS THE PART where I would like to change the radius without having to take
// all the querySourceFeatures or queryRenderedFeatures for performance issues
//But I don't know how to just go through the dataset of the layer extrusion
}
map.on('data', function(e) {
// if (e.sourceId !== 'total') return
if (e.sourceId !== 'data') return
if (e.isSourceLoaded !== true) return
initializeTower()
})
//map.on('sourcedata', sourceCallback);
function initializeTower(){
if (layer.paint) radiusPX = map.getLayer('population').paint.get('circle-radius').evaluate();
if (radiusPX === false) return;
var nb_of_objects = 0
var data = {
"type": "FeatureCollection",
"features": []
}
map.querySourceFeatures('data').forEach(function(f) {
var object = turf.centerOfMass(f)
var center = object.geometry.coordinates
var xy = map.project(center)
xy.x += radiusPX;
var LL = map.unproject(xy)
LL = turf.point([LL.lng, LL.lat])
//var radius = turf.distance(center, LL, {
// units: 'meters'
//}) + 0.00000001
var radius = radius_zoom_d ;
var options = {
steps: 16,
units: 'meters',
properties: object.properties
};
data.features.push(turf.circle(center, radius, options))
nb_of_objects +=1
})
console.log("Finished preparing data for "+nb_of_objects+" objects")
map.getSource('extrusion').setData(data);
}
});
The first issue I have is that it triggers a ReferenceError: layer is not defined on the line if (layer.paint) radiusPX = map.getLayer('my_initial_2D_layer').paint.get('circle-radius').evaluate();. This is probably due to the layer's style not being rendered yet, but it seems from the documentation and few Mapbox Questions on SO and on their GitHub that there is no way to check for that.
If I comment this line, this triggers later on in the code a Cannot read property 'setData' of undefined on the line map.getSource('extrusion').setData(data); and also that it prints it processed 0 objects which is quite problematic. I get the output from my console.log().
Finished preparing data for 0 objects
The second issue that I have is that I don't know how I could later modify the data of this extrusion layer. It seems that there is no function to get the data for my extrusion layer in order to just change its radius (as it seems that this cannot be done dynamically in the layer style).
Would anyone know how to proceed?
Related
I have an Array of Geography-Location-Objects of this kind:
let markers = [
{
"markerOffset": 10,
"name": "Plantation",
"coordinates": ["9.85804","53.5233"]
},
{
"markerOffset": 10,
"name": "Roasting",
"coordinates": ["29.85804","50.5233"]
},
{
"markerOffset": 10,
"name": "Packaging",
"coordinates": ["29.85804","50.5233"]
},
{
"markerOffset": 10,
"name": "Harbour",
"coordinates": ["21.85804","51.5213"]
},
]
How can I filter the array to remove duplicate objects with the same coordinates (such as element 2 and 3) and only leave one entry with the coordinates.
A Set wont work propably because of the differing name: attribute
You'll need to loop through the markers array add unique markers (by their coordinates) to a new array. This second array will be used to compare the next marker in the loop.
const markers = [
{
markerOffset: 10,
name: "Plantation",
coordinates: ["9.85804", "53.5233"],
},
{
markerOffset: 10,
name: "Roasting",
coordinates: ["29.85804", "50.5233"],
},
{
markerOffset: 10,
name: "Packaging",
coordinates: ["29.85804", "50.5233"],
},
{
markerOffset: 10,
name: "Harbour",
coordinates: ["21.85804", "51.5213"],
},
];
const filteredMarkers = [];
for (let i = 0, length = markers.length; i < length; i++) {
let duplicate = false;
for (let j = 0, length = filteredMarkers.length; j < length; j++) {
const [lat1, lon1] = markers[i].coordinates;
const [lat2, lon2] = filteredMarkers[j].coordinates;
if (lat1 === lat2 && lon1 === lon2) {
duplicate = true;
break;
}
}
if (!duplicate) {
filteredMarkers.push(markers[i]);
}
}
console.log(filteredMarkers);
Simple compare and build a new array. This doesn't care about the name attribute and just uses the first one that comes along
let markers = [
{ "markerOffset": 10, "name": "Plantation", "coordinates": ["9.85804","53.5233"] },
{ "markerOffset": 10, "name": "Roasting", "coordinates": ["29.85804","50.5233"] },
{ "markerOffset": 10, "name": "Packaging", "coordinates": ["29.85804","50.5233"] },
{ "markerOffset": 10, "name": "Harbour", "coordinates": ["21.85804","51.5213"] },
]
let results = [];
markers.forEach(marker => {
if(!results.find(m => m.coordinates[0] === marker.coordinates[0] && m.coordinates[1] === marker.coordinates[1] )) results.push(marker);
})
console.log(results);
A nice simple way is to use findIndex using the coordinates, you can just toString these to make the find even easier, use this in a filter, and if the index is the same it's the first of a duplicate or not a duplicate.
eg.
const markers = [
{
markerOffset: 10,
name: "Plantation",
coordinates: ["9.85804", "53.5233"],
},
{
markerOffset: 10,
name: "Roasting",
coordinates: ["29.85804", "50.5233"],
},
{
markerOffset: 10,
name: "Packaging",
coordinates: ["29.85804", "50.5233"],
},
{
markerOffset: 10,
name: "Harbour",
coordinates: ["21.85804", "51.5213"],
},
];
console.log(
markers.filter(
(m,ix) => markers.findIndex(c =>
c.coordinates.toString() ===
m.coordinates.toString()) === ix)
);
Bonus, if you wanted the last of a duplicate, you can just change findIndex to findLastIndex
Another way to make an array unique by a certain property:
const markers=[{markerOffset:10,name:"Plantation",coordinates:["9.85804","53.5233"]},{markerOffset:10,name:"Roasting",coordinates:["29.85804","50.5233"]},{markerOffset:10,name:"Packaging",coordinates:["29.85804","50.5233"]},{markerOffset:10,name:"Harbour",coordinates:["21.85804","51.5213"]}];
const uniqByCoords = Object.values(markers.reduce((acc, market) => {
acc[String(market.coordinates)] ??= market; //return first market item in array as uniq.
// If you need the last one replace ??= to =
return acc;
}, {}));
console.log(uniqByCoords);
.as-console-wrapper { max-height: 100% !important; top: 0; }
With static data the word cloud works fine.
When the data change and I update html, the cloud doesn't update.
This is my code in HTML
<div id="chartdiv"></div>
<script type="text/javascript" src="../wordCloud.js"></script>
<script type="text/javascript">wordcloud(myData)</script>
This is How I initialize the chart
function wordcloud(myData) {
am4core.ready(function () {
// Themes begin
am4core.useTheme(am4themes_animated);
am4core.useTheme(am4themes_kelly);
// Themes end
var chart = am4core.create("chartdiv", am4plugins_wordCloud.WordCloud);
chart.fontFamily = "Courier New";
var series = chart.series.push(new am4plugins_wordCloud.WordCloudSeries());
series.randomness = 0.1;
series.rotationThreshold = 0.5;
series.angles = [0];
series.data = myData;
series.dataFields.word = "tag";
series.dataFields.value = "count";
series.heatRules.push({
"target": series.labels.template,
"property": "fill",
"min": am4core.color("#0000CC"),
"max": am4core.color("#CC00CC"),
"dataField": "value"
});
series.labels.template.tooltipText = "{word}: {value}";
var hoverState = series.labels.template.states.create("hover");
hoverState.properties.fill = am4core.color("#FF0000");
var title = chart.titles.create();
title.text = "Most frequent words in corpus";
title.fontSize = 20;
title.fontWeight = "800";
});
}
Users can use a button to have more or less words displayed in the tag cloud.
The new data is then calculated in the back end. But how can I update the cloud?
Thanks for any help.
Edit:
I read the documentation and also read this thread. But this is not helping me because the difference btw. word cloud and chart is that the data are added via series variable and not the chart variable.
The same update rules mentioned in the linked thread apply to series-level data - replacing the array, calling addData or updating in place with invalidateRawData (both of which have series-level methods) will enable you to update the WordCloud. Your code seems to have the same limitation as the previous thread's code where you don't have access to the chart variable outside of your method, so I'm not seeing how you would exactly update that instance without making similar changes.
Basic demo below using a button that sets a new data array on the series:
var myData = [{
'tag': 'Yes',
'count': 50
}, {
'tag': 'No',
'count': 50
}, {
'tag': 'Maybe',
'count': 50
}]
am4core.useTheme(am4themes_animated);
// Themes end
var chart = am4core.create("chartdiv", am4plugins_wordCloud.WordCloud);
chart.fontFamily = "Courier New";
var series = chart.series.push(new am4plugins_wordCloud.WordCloudSeries());
series.randomness = 0.1;
series.rotationThreshold = 0.5;
series.angles = [0];
series.data = myData;
series.dataFields.word = "tag";
series.dataFields.value = "count";
series.heatRules.push({
"target": series.labels.template,
"property": "fill",
"min": am4core.color("#0000CC"),
"max": am4core.color("#CC00CC"),
"dataField": "value"
});
series.labels.template.tooltipText = "{word}: {value}";
var hoverState = series.labels.template.states.create("hover");
hoverState.properties.fill = am4core.color("#FF0000");
var title = chart.titles.create();
title.text = "Most frequent words in corpus";
title.fontSize = 20;
title.fontWeight = "800";
document.getElementById('change').addEventListener('click', function() {
series.data = [{
'tag': 'Yes',
'count': 50
}, {
'tag': 'No',
'count': 50
}, {
'tag': 'Maybe',
'count': 50
}, {
'tag': 'Sorta',
'count': 50
}, {
'tag': 'Kinda',
'count': 50
}]
})
#chartdiv {
width: 100%;
height: 350px;
}
<script src="//www.amcharts.com/lib/4/core.js"></script>
<script src="//www.amcharts.com/lib/4/charts.js"></script>
<script src="//www.amcharts.com/lib/4/plugins/wordCloud.js"></script>
<script src="//www.amcharts.com/lib/4/themes/animated.js"></script>
<div id="chartdiv"></div>
<button id='change'>Change data</button>
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.
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);