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
Related
I am reading learnjsdata.com and came across this unfamiliar JavaScript syntax. The syntax is as follows:
lookupIndex[row[lookupKey]] = row;
Anyone know what's happening here? I haven't seen syntax like this. Used in context:
Data
var articles = [{
"id": 1,
"name": "vacuum cleaner",
"weight": 9.9,
"price": 89.9,
"brand_id": 2
}, {
"id": 2,
"name": "washing machine",
"weight": 540,
"price": 230,
"brand_id": 1
}, {
"id": 3,
"name": "hair dryer",
"weight": 1.2,
"price": 24.99,
"brand_id": 2
}, {
"id": 4,
"name": "super fast laptop",
"weight": 400,
"price": 899.9,
"brand_id": 3
}];
var brands = [{
"id": 1,
"name": "SuperKitchen"
}, {
"id": 2,
"name": "HomeSweetHome"
}];
Function & Invocation
function join(lookupTable, mainTable, lookupKey, mainKey, select) {
var l = lookupTable.length,
m = mainTable.length,
lookupIndex = [],
output = [];
for (var i = 0; i < l; i++) { // loop through l items
var row = lookupTable[i];
lookupIndex[row[lookupKey]] = row; // create an index for lookup table
}
for (var j = 0; j < m; j++) { // loop through m items
var y = mainTable[j];
var x = lookupIndex[y[mainKey]]; // get corresponding row from lookupTable
output.push(select(y, x)); // select only the columns you need
}
return output;
};
var result = join(brands, articles, "id", "brand_id", function(article, brand) {
return {
id: article.id,
name: article.name,
weight: article.weight,
price: article.price,
brand: (brand !== undefined) ? brand.name : null
};
});
console.log(result);
Appreciate any answers or pointers, thanks!
Think of it as two separate function calls:
var rowLookup = row[lookupKey];
lookupIndex[rowLookup] = row;
It's the same as doing it all in the same line:
lookupIndex[row[lookupKey]] = row;
I'm using http://d3pie.org/ to generate a pie chart for my web application. Now, I want to manipulate the value appearing on my chart. Currently, the values appearing have more than 4 decimals. For example, 10.35234234234 . I just want 10.3523 to appear.
Here's my current code:
function drawPie(id,from,to,C,formula){
var subArray = getSubArray(C.table,from,to);
if(Object.keys(subArray).length){
var partials = createPartials(subArray, C.attributes);
//console.log("partials -> ",partials);
formula = formula.slice(1,-1);
//console.log("formula ->" , formula);
formula = formula.split(",");
//console.log("formula ->" , formula);
var cloneFormula = formula.slice();
for( var i in partials){
if(i.substr(0,1) == "$"){
for(var j = 0; j< formula.length; j++){
formula[j] = replaceFunction(i,formula[j],partials[i]);
}
}
}
//console.log("replaced formula ->" , formula);
var data = [];
var isEmpty = true;
for(var j = 0; j< formula.length; j++){
var label = cloneFormula[j].split(/(?=[-+*\/])/)[0].split(".")[1];
var temp = {};
try{
temp.value = eval(formula[j]);
}catch(err){
temp.value = 0;
}
temp.label = partials[label + ".name"];
temp.color = partials[label + ".color"];
data.push(temp);
if(temp.value != 0){
isEmpty = false;
}
}
if(isEmpty){
return true;
}
//console.log("data ->" , data);
var pie = new d3pie(id, {
"size": {
"canvasHeight": 400,
"canvasWidth": 800, //see defect 1418
"pieOuterRadius": "88%"
},
"data": {
"content": data
},
"labels": {
"outer": {
"pieDistance": 100
},
"inner": {
"format": "value"
},
"mainLabel": {
"font": "verdana"
},
"percentage": {
"color": "#e1e1e1",
"font": "verdana",
"decimalPlaces": 0
},
"value": {
"color": "#e1e1e1",
"font": "verdana"
},
"lines": {
"enabled": true,
"color": "#cccccc"
},
"truncation": {
"enabled": true
}
},
"effects": {
"pullOutSegmentOnClick": {
"effect": "linear",
"speed": 400,
"size": 8
}
}
});
return false;
}
return true;
}
Can you help me try to modify the texts appearing inside my pie chart?
use the following to strip the number and then round it.
function strip(number) {
return (parseFloat(number).toPrecision(4));
}
example
var number = 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679
function strip(number) {
return (parseFloat(number).toPrecision(6));
}
window.alert(strip(number));
For the generator you are using, go to the labels section, and under the label styles / Settings you will find the option for decimal places for percentage values. Enter the number of decimal places you would like to limit the labels to.
I am using a method as found here with chartJS to input dates that are missing from a JSON response.
jsonObj["result"]["data"] is output from the initial JSON response:
{
"action": "data_link_day",
"result": {
"url_ending": "0",
"data": [{
"x": "2018-03-12",
"y": 3
}, {
"x": "2018-03-16",
"y": 5
}]
}
}
Inside the drawChart function, I need to separate the x/y values as graphData = y (number) and labels = x (date) by targeting them individually. I tried to do jsonObj["result"]["data"].x but obviously not correct.
function drawChart(jsonObj) {
var graphData = jsonObj["result"]["data"],
labels = jsonObj["result"]["data"];
for (var i = 0; i < labels.length; i++) {
//make sure we are not checking the last date in the labels array
if (i + 1 < labels.length) {
var date1 = moment(labels[i], "YYYY-MM-DD");
var date2 = moment(labels[i + 1], "YYYY-MM-DD");
//if the current date +1 is not the same as it's next neighbor we have to add in a new one
if (!date1.add(1, "days").isSame(date2)) {
//add the label
labels.splice(i + 1, 0, date1.format("YYYY-MM-DD"));
//add the data
graphData.splice(i + 1, 0, 0);
}
}
}
...
}
You can use the following code to separate your data into X and Y arrays:
let data = jsonObj.result.data;
let dataX = data.map(data => data.x);
let dataY = data.map(data => data.y);
Demo:
const json = `
{
"action": "data_link_day",
"result": {
"url_ending": "0",
"data": [{
"x": "2018-03-12",
"y": 3
}, {
"x": "2018-03-16",
"y": 5
}]
}
}`;
const input = JSON.parse(json);
let data = input.result.data;
let dataX = data.map(data => data.x);
let dataY = data.map(data => data.y);
console.log(dataX);
console.log(dataY);
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/
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.