Problem creating multiple charts in the same page - javascript

I have two functions, each supposed to draw a different chart from the same dataset to two SVGs with id attributes of one and two respectively.
I have created different variables to append the chart to its corresponding SVG:
var svg1 = d3.select("#one")
var svg2 = d3.select("#two")
The problem is the function that draws chart for the second SVG overwrites both charts.
You can see the charts return correctly if you run each function separately. (Comment out each function call to see the data from the other chart that is being overwritten)
JS Fiddle
<html>
<body>
<svg id="one"></svg>
<svg id="two"></svg>
<script>
// variables we'll be filtering by
var quantile;
var amount;
var type;
var risk;
w = window.innerWidth;
h = window.innerHeight;
// <-- Make Selection -->
// Possible values --
// quantile: [1 2 3 4]
// variable: ['Income' 'Payments' 'Consumption' 'Utility']
// amount: [ 5000 30000 70000]
// type: ['Loan' 'ISA']
// risk: [1 2 3 4]
quantile = 1;
amount=5000;
type = 'Loan';
risk = 2;
getFirst();
getSecond();
function getFirst() {
variable= 'Income';
// chart stuff
margin = {top: h/4, right: w/4, bottom: h/4, left: w/4},
width = window.innerWidth - margin.left - margin.right,
height = window.innerHeight - margin.top - margin.bottom;
x = d3.scaleLinear()
.rangeRound([0, width]);
y = d3.scaleLinear()
.rangeRound([height,0]);
valueline = d3.line()
.x(function(d) { return x(d.key); })
.y(function(d) { return y(d.value); });
var svg1 = d3.select("#one")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// data
d3.csv("https://raw.githubusercontent.com/JainFamilyInstitute/isa-app/master/data/data_vis.csv?token=AXiiVXcAwXZjLK4-3tiyxKwj8yaVMVDmks5b6b8NwA%3D%3D", function(error, data) {
if (error) throw error;
// filter by selection
data = data.filter(function(d) {
return (d.quantile == quantile) &
(d.variable == variable) &
(d.amount == amount) &
(d.type == type) &
(d.risk == risk) });
// create visualizable array with only ages and amounts for selected series
data_filtered = data.map(({ quantile,amount,risk,type,variable, ...item }) => item);
data_vis = data_filtered[0];
console.log(data_vis);
result = [];
for(i=22;i<101;i++){
key = i;
value =parseFloat(data_vis[i]);
result.push({
key: key,
value: value
});
}
// console.log(data_vis);
console.log(result);
// Scale the range of the data
x.domain([d3.min(result, function(d) { return d.key; }), d3.max(result, function(d) { return d.key; })]);
y.domain([0, d3.max(result, function(d) { return d.value; })]);
// Add the valueline path.
svg1.append("path")
.data([result])
.attr("class", "line")
.attr("d", valueline);
// Add the X Axis
svg1.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
function y_grid_lines() {
return d3.axisLeft(y)
}
svg1.append("g")
.attr("class", "grid")
.call(y_grid_lines()
.tickSize(-width)
);
});
}
function getSecond() {
variable= 'Payments';
// chart stuff
margin = {top: h/4, right: w/4, bottom: h/4, left: w/4},
width = window.innerWidth - margin.left - margin.right,
height = window.innerHeight - margin.top - margin.bottom;
x = d3.scaleLinear()
.rangeRound([0, width]);
y = d3.scaleLinear()
.rangeRound([height,0]);
valueline = d3.line()
.x(function(d) { return x(d.key); })
.y(function(d) { return y(d.value); });
var svg2 = d3.select("#two")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// data
d3.csv("https://raw.githubusercontent.com/JainFamilyInstitute/isa-app/master/data/data_vis.csv?token=AXiiVXcAwXZjLK4-3tiyxKwj8yaVMVDmks5b6b8NwA%3D%3D", function(error, data) {
if (error) throw error;
// filter by selection
data = data.filter(function(d) {
return (d.quantile == quantile) &
(d.variable == variable) &
(d.amount == amount) &
(d.type == type) &
(d.risk == risk) });
// create visualizable array with only ages and amounts for selected series
data_filtered = data.map(({ quantile,amount,risk,type,variable, ...item }) => item);
data_vis = data_filtered[0];
console.log(data_vis);
result = [];
for(i=22;i<101;i++){
key = i;
value =parseFloat(data_vis[i]);
result.push({
key: key,
value: value
});
}
// console.log(data_vis);
console.log(result);
// Scale the range of the data
x.domain([d3.min(result, function(d) { return d.key; }), d3.max(result, function(d) { return d.key; })]);
y.domain([0, d3.max(result, function(d) { return d.value; })]);
// Add the valueline path.
svg2.append("path")
.data([result])
.attr("class", "line")
.attr("d", valueline);
// Add the X Axis
svg2.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
svg2.append("g")
.call(d3.axisLeft(y));
});
}
</script>
</html>

One possible fix is to fetch your data once and the filter twice.
Here is a fiddle I create using d3v5 which uses fetch API making it easier to avoid this kind of issues. In general, be wary of callbacks when you write Javascript code.
The main change in this code is the following:
d3.csv("<remote csv>").then(function(data) {
getIncome(data);
getPayments(data);
});
The rest is quite similar to your code.
EDIT: Gerardo Furtado's answer also addresses an important issue regarding the cause of the issue. You should also follow the suggestions in that answer. My solution leaves the issue addressed in that answer unresolved. Instead, my answer focuses on ensuring the intended order of execution.

A word of caution about Javascript: when you do this...
variable = 'Income';
... instead of:
var variable = 'Income';
... or even using const or let, your variable is a global variable, that is, it's not scoped inside getIncome() or getPayments().
Together with the fact that the callback of d3.csv doesn't run immediately, this is what happens:
You call both functions, one after the other:
getIncome();
getPayments();
Inside getIncome(), you have:
function getIncome() {
variable= 'Income';
//etc...
which makes variable a global with "Income" as value.
Then, when it comes to getPayments(), you have:
function getPayments() {
variable= 'Payments';
//etc...
now variable has "Payments" as value, but the callback from the first function (getIncome()) was not executed yet!
Therefore, when the first function runs, the value of variable is "Payments", and both charts have the same path.
Solution: don't create globals, do not assign a value to an undeclared variable.
Here is your same Fiddle, just adding var in front of each variable: https://jsfiddle.net/qrj803za/3/
Also, take care of other globals you're creating here (like result, data_vis, etc...)

Related

apply sum function to parse sum of a column from csv into d3.js-chart

I have the following dataset in csv format:
Month,S40201,S40202,S40203
JAN,79,0,70
FEB,58,26,70
MAR,48,47,46
APR,64,98,77
MAY,79,71,64
JUN,86,103,116
JUL,95,75,95
AUG,0,40,3,5
SEP,60,82,79
OCT,98,101,79
NOV,60,81,75
DEC,7,30,46
The D3.js bar chart should display the sum of each column "S40201", "S40202", "S40203" as bar with the corresponding label on the X-axis. Label should be the column name (first row).
<script>
// Set the margins
var margin = {top: 60, right: 100, bottom: 20, left: 80},
width = 850 - margin.left - margin.right,
height = 370 - margin.top - margin.bottom;
// Parse the month variable
var parseMonth = d3.timeParse("%b");
var formatMonth = d3.timeFormat("%b");
// Set the ranges
var x = d3.scaleBand().rangeRound([0, width]).padding(0.1)
var y = d3.scaleLinear().range([height, 0]);
// Create the svg canvas in the "graph" div
var svg = d3.select("#graph")
.append("svg")
.style("width", width + margin.left + margin.right + "px")
.style("height", height + margin.top + margin.bottom + "px")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform","translate(" + margin.left + "," + margin.top + ")")
.attr("class", "svg");
// Import the CSV data
d3.csv("data.csv", function(error, data) {
if (error) throw error;
// Format the data
data.forEach(function(d) {
d.Month = parseMonth(d.Month);
d.S40201 = +d.S40201;
d.S40202 = +d.S40202;
d.S40203 = +d.S40203;
});
var nest = d3.nest()
.key(function(d){
return d.S40201,d.S40202,d.S40203;
})
.sortKeys(d3.ascending)
.rollup(function(leaves){
return d3.sum(leaves, function(d) {return (d.S40201,d.S40203,d.S40203)});
})
.entries(data)
console.log(nest)
// Scale the range of the data
x.domain(nest.map(function(d) { return d.key; }));
y.domain([0, d3.max(nest, function(d) { return d.value; })]);
// Set up the x axis
var xaxis = svg.append("g")
.attr("transform", "translate(0," + height + ")")
.attr("class", "x axis")
.call(d3.axisBottom(x)
//.ticks(d3.timeMonth)
.tickSize(0, 0)
//.tickFormat(d3.timeFormat("%B"))
.tickSizeInner(0)
.tickPadding(10));
// Add the Y Axis
var yaxis = svg.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(y)
.ticks(5)
.tickSizeInner(0)
.tickPadding(6)
.tickSize(0, 0));
// yaxis.select(".domain").style("display","none")
// Add a label to the y axis
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 0 - 60)
.attr("x", 0 - (height / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text("Annual Sales")
.attr("class", "y axis label");
// Draw the bars
svg.selectAll(".rect")
.data(nest)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("width", x.bandwidth())
.attr("height", function(d) { return height - y(d.value); });
})
</script>
With just one column it works fine, but If I want to add more than one column it doesn´t work correctly.
Welcome to StackOverflow. The problem you face is your nested data is not the way you like. If you console log your nested data the way you have it, the key and the value are both the same and not the headers.
Instead if you manually summarize the data the way you like it would be easier. For example:
var nest = [];//create empty array
var keys = Object.keys(data[0]); //get the headers for your data
//for each header push the sum as an object
keys.forEach(function (d, i) {
if (i == 0) return;//ignore first column as that is month
//get the sumfrom your data for all the values of this key i.e. d
var sum = d3.sum (data, function(e){ return e[d] });
//create an object with this key value pair
var obj = {
key: d, //column name
value: sum //sum for the column
}
nest.push(obj); //push this as an object in the nest array
})
Here is a block with the correct code showing the headers as the labels on the x-axis and the sum values.
https://bl.ocks.org/akulmehta/724d63f0108304ede84a14fc145aad28
Please feel free to comment if you need more explanation /guidance and please remember to mark it as an answer if this answers your question.
Update 1- For selected columns only
Based on the comments below, if you want selected columns only simply replace the var keys = Object.keys(data[0]); with an array of headers like var keys = ['S40201','S40202','S40203'] and also remove the line if (i == 0) return;//ignore first column as that is month as we do not have the month's column anymore. Final code for this part should look like this:
var nest = [];//create empty array
var keys = ['S40201','S40202','S40203']; //the headers for your data
//for each header push the sum as an object
keys.forEach(function (d, i) {
//get the sumfrom your data for all the values of this key i.e. d
var sum = d3.sum (data, function(e){ return e[d] });
//create an object with this key value pair
var obj = {
key: d, //column name
value: sum //sum for the column
}
nest.push(obj); //push this as an object in the nest array
})
And here is the block to demonstrate it works. Notice the CSV file has an additional column but it the graph ignores this. https://bl.ocks.org/akulmehta/724d63f0108304ede84a14fc145aad28

How do I get data to show up in my graph?

I'm trying to get my data to show up in my graph, however I get an error that says that my data is "NaN" after I converted the Year and Miles column from strings to integers.
I'm guessing that it's something with my x_scale & y_scale...?
<!DOCTYPE html>
<html lang="en">
<head>
<description>
<!--charts - avg vehicle trip per mile, source: http://nhts.ornl.gov/2009/pub/stt.pdf-->
</description>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<link rel="stylesheet" href="/bootstrap-3.3.7-dist/css/bootstrap.min.css">
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script type="text/javascript">
// global variables
var dataset;
d3.csv("avgVehicleTripLengthMiles.csv", function (error, data) {
if (error) {
console.log(error);
} else {
console.log(data);
}
// once loaded, data is copied to dataset because js is asynchronous
dataset = data;
createScatterplot();
});
/*
function typeConv() {
// type conversion from string to integer
var typeConv = dataset.forEach(function (d) {
d["Year"] = +d["Year"];
d["Miles"] = +d["Miles"];
return d;
});
}
*/
function createScatterplot() {
// TEST
var typeConv = dataset.forEach(function (d) {
d["Year"] = +d["Year"];
d["Miles"] = +d["Miles"];
return d;
});
var title = d3.select("body")
.append("h4")
.text("Avg. Vehicle Trip Length per Mile");
// dimensions of canvas
var padding = 30;
var margin = {
top: 20,
right: 40,
bottom: 20,
left: 40
},
w = 800 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
// create svg canvas
var svg_canvas = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom);
// create scale for axis
var x_scale = d3.scaleLinear().domain([1969, 2009]).range([padding, w - padding * 2]);
var y_scale =
d3.scaleLinear().domain([0, 20]).range([h - padding, padding]);
// r_scale created specifically for circles' radius to be mapped unto axes
var r_scale =
d3.scaleLinear().domain([0, d3.max(dataset, function (d) {
return d[1];
})]).range([0, 20]);
// define axis & ticks // .ticks(5) to x_axis and .ticks(1) to y_axis
var x_axis = d3.axisBottom().scale(x_scale);
var y_axis = d3.axisLeft().scale(y_scale);
// create group, "g" element, to create x_axis and y_axis
var x_group = svg_canvas.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(x_axis);
var y_group = svg_canvas.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(y_axis);
// create circles
svg_canvas.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) {
return x_scale(d[0]);
})
.attr("cy", function (d) {
console.log(d); // TEST
return y_scale(d[1]);
})
.attr("cr", function (d) {
return r_scale(d[1]);
});
}
</script>
</body>
</html>
EDIT (new answer):
There are several issues, and I'll try to step through them one by one.
In order to test, I had to make up my own data. My test CSV file looked like this (so your final answer might change slightly if your file is different)
Year,Miles
2006,5.0
2007,7.2
2008,19.3
As was pointed out by #altocumulus in the comments above, your .attr() calls are referencing non-existant indexes, which might be part of the trouble.
The radius attribute for circles is r, not cr
I simplified the code by not calling a function for r, but rather doing a static value. You may want to play further with this.
The significantly changed portion of code is
svg_canvas.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) {
return x_scale(d["Year"]);
})
.attr("cy", function (d) {
return y_scale(d["Miles"]);
})
.attr("r", function (d) {
return 5;
//return r_scale(d[1]);
});
You still have an issue with the x-axis acting like numbers, and not dates, making 2006 look like 2,006, for example. I haven't solved that issue here.
Lastly, I feel like you're complicating your code for no reason by trying to handle margin AND padding via the D3 code, when these end up meaning similar things in the Javascript context. I suggest accomplishing most of the margin/padding via CSS, which would simplify your code. Another example of an unnecessary complication is in the previous answer, below.
FIDDLE
OLD (incomplete, incorrect) ANSWER:
The return value of Array.forEach() is undefined, so it can't be assigned.
dataset.forEach(function (d) {
//d["Year"] = +d["Year"];
d["Miles"] = +d["Miles"];
// NOT NEEDED: return d;
});
If you need to keep your converted array separate, use Array.map().
// global variables
var dataset;
d3.csv("avgVehicleTripLengthMiles.csv", function (error, data) {
if (error) {
console.log(error);
} else {
console.log(data);
}
// once loaded, data is copied to dataset because js is asynchronous
dataset = data;
createScatterplot();
});
function createScatterplot() {
// TEST
var typeConv = dataset.forEach(function (d) {
// d["Year"] = +d["Year"];
d["Miles"] = +d["Miles"];
// return d;
});
var title = d3.select("body")
.append("h4")
.text("Avg. Vehicle Trip Length per Mile");
// dimensions of canvas
var padding = 30;
var margin = {
top: 20,
right: 40,
bottom: 20,
left: 40
},
w = 800 - margin.left - margin.right,
h = 400 - margin.top - margin.bottom;
// create svg canvas
var svg_canvas = d3.select("body")
.append("svg")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom);
// create scale for axis
var x_scale = d3.scaleLinear().domain([1965, 2009]).range([padding, w - padding * 2]);
var y_scale =
d3.scaleLinear().domain([0, 20]).range([h - padding, padding]);
// r_scale created specifically for circles' radius to be mapped unto axes
var r_scale =
d3.scaleLinear().domain([0, d3.max(dataset, function (d) {
return d[1];
})]).range([0, 20]);
// define axis & ticks // .ticks(5) to x_axis and .ticks(1) to y_axis
var x_axis = d3.axisBottom().scale(x_scale);
var y_axis = d3.axisLeft().scale(y_scale);
// create group, "g" element, to create x_axis and y_axis
var x_group = svg_canvas.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(x_axis);
var y_group = svg_canvas.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(y_axis);
// create & color circles
svg_canvas.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function (d) {
return x_scale(d["Year"]);
})
.attr("cy", function (d) {
return y_scale(d["Miles"]);
})
.attr("r", function (d) {
return 5;
})
.style("fill", function (d) {
if (d["Trip Purpose"] === "All Purposes") {
return "pink";
} else if (d["Trip Purpose"] === "To or From Work") {
return "red";
} else if (d["Trip Purpose"] === "Shopping") {
return "blue";
} else if (d["Trip Purpose"] === "Other Family/Personal Errands") {
return "green";
} else if (d["Trip Purpose"] === "Social and Recreational") {
return "gray";
};
});
// create text label for x-axis
svg_canvas.append("text")
.attr("x", w / 2)
.attr("y", h + margin.top + 20)
.style("text-anchor", "middle")
.text("Year");
// create text label for y-axis
svg_canvas.append("text")
.attr("transform", "rotate(-90)")
.attr("x", (0 - margin.left / 2))
.attr("y", (h/2))
.style("text-anchor", "middle")
.text("Miles");

Updating data in D3 stacked bar graph

I have a new set of data that I want to have updated within the SVG but I'm not really sure how to correctly grab the elements I have replace the data while smoothly transitioning.
http://codepen.io/jacob_johnson/pen/jAkmPG
All relevant code is in the CodePen above; however, I will post some of it here.
// Array to supply graph data
var FOR_PROFIT = [10,80,10];
var NONPROFIT = [60,10,30];
var PUBLIC = [40,40,20];
var data = [
{
"key":"PUBLIC",
"pop1":PUBLIC[0],
"pop2":PUBLIC[1],
"pop3":PUBLIC[2]
},
{
"key":"NONPROFIT",
"pop1":NONPROFIT[0],
"pop2":NONPROFIT[1],
"pop3":NONPROFIT[2]
},
{
"key":"FORPROFIT",
"pop1":FOR_PROFIT[0],
"pop2":FOR_PROFIT[1],
"pop3":FOR_PROFIT[2]
}
];
I have two data arrays (one called data and another called data2 with modified information). I essentially want to transition the created graph into the new data. I understand enough to rewrite the graph over the old graph but I am not getting any transitions and am obviously just printing new data on top of the old instead of modifying what I have.
var n = 3, // Number of layers
m = data.length, // Number of samples per layer
stack = d3.layout.stack(),
labels = data.map(function(d) { return d.key; }),
layers = stack(d3.range(n).map(function(d)
{
var a = [];
for (var i = 0; i < m; ++i)
{
a[i] = { x: i, y: data[i]['pop' + (d+1)] };
}
return a;
})),
// The largest single layer
yGroupMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y; }); }),
// The largest stack
yStackMax = d3.max(layers, function(layer) { return d3.max(layer, function(d) { return d.y0 + d.y; }); });
var margin = {top: 40, right: 10, bottom: 20, left: 150},
width = 677 - margin.left - margin.right,
height = 212 - margin.top - margin.bottom;
var y = d3.scale.ordinal()
.domain(d3.range(m))
.rangeRoundBands([2, height], .08);
var x = d3.scale.linear()
.domain([0, yStackMax])
.range([0, width]);
var color = d3.scale.linear()
.domain([0, n - 1])
.range(["#aad", "#556"]);
var svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var layer = svg.selectAll(".layer")
.data(layers)
.enter().append("g")
.attr("class", "layer")
.style("fill", function(d, i) {
return color(i);
});
layer.selectAll("rect")
.data(function(d) {
return d;
})
.enter().append("rect")
.attr("y", function(d) {
return y(d.x);
})
.attr("x", function(d) {
return x(d.y0);
})
.attr("height", y.rangeBand())
.attr("width", function(d) {
return x(d.y);
});
This is what is handling the creation of my graph with its layers being the individual bars. Now I've seen this: https://bl.ocks.org/mbostock/1134768 but I still don't understand as its not very well commented.
Any guidance, links, or help in this would be amazing. Thanks.
Your Solution: http://codepen.io/typhon/pen/bZLoZW
Hope this helps!!
You can slow down the speed of transition by increasing parameter passed to duration(500) at line 200.(and vice versa)
Read update, enter and exit selections. They are handy almost all the time while working with D3. Take this for reference.

RHS assignment manipulated over LHS manipulation

sap.ui.core.Control.extend("control.linechart", {
/* the control API */
metadata : {
properties : {
"items" : { type: "any" },
"height" : {type: "int"},
"width" : {type: "int"},
"popup" : {type: "any"}
},
events: {
"select" : {},
"selectEnd": {}
}
},
// the part creating the HTML:
renderer : function(oRm, oControl) { // static function, so use the given "oControl" instance instead of "this" in the renderer function
oRm.write("<div");
oRm.writeControlData(oControl); // writes the Control ID and enables event handling - important!
oRm.addClass("lineChart"); // add a CSS class for styles common to all control instances
oRm.writeClasses(); // this call writes the above class plus enables support for my.Bubble.addStyleClass(...)
oRm.write(">");
oRm.write("</div>");
},
onAfterRendering: function() {
data = this.getItems();
//alert(JSON.stringify(this.getItems()));
var passedheight = this.getHeight();
//var containerWidth = jQuery.sap.byId(this.oParent.sId).width() || 800; // gets super parent width
var containerWidth = $("#"+this.getId()).parent().width(); // gets immediate parent width
var margin = {top: 15, right: 30, bottom: 20, left: 30},
width = containerWidth- margin.left - margin.right,
height = passedheight - margin.top - margin.bottom;
var parseDate = d3.time.format("%d-%b-%y %H:%M %p").parse;
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var color = d3.scale.category10();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left").ticks(4).tickSize(-width, 0, 0);
var line = d3.svg.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.tonneValue); });
var svg = d3.select("#"+this.getId()).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "date"; }));
data.forEach(function(d) {
d.date = parseDate(d.date);
});
var wsfs = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {date: d.date, tonneValue: +d[name]};
})
};
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([
d3.min(wsfs, function(c) { return d3.min(c.values, function(v) { return v.tonneValue; }); }),
d3.max(wsfs, function(c) { return d3.max(c.values, function(v) { return v.tonneValue; }); })
]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
var wsf = svg.selectAll(".wsf")
.data(wsfs)
.enter().append("g")
.attr("class", "wsf");
wsf.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return color(d.name); });
var legendNames = d3.keys(data[0]).filter(function(key) { return key !== "date" });
data.forEach(function(d) {
d.ages = legendNames.map(function(name) { return {name: name, value: +d[name]}; });
});
var legend = svg.selectAll(".legend")
.data(legendNames.slice())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 4)
.style("fill", function(d) {return color(d); });
legend.append("text")
.attr("x", width - 24)
.attr("y", 6)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
},
});
In this whole program why Right hand side value is changing i.e. when I do data = this.getItems(); I assume this.getItems is assigned to data. But data is manipulated in rest of the program but when oAfterRendering is invoked again I get the manipulated data thats there in variable data. How is this.getItems() i.e. items property is manipulated when data is the one that is manipulated.Due to use of Openui5 and custom controls there is no way to save the data in some temporary variable.
When you return an object from a function, JavaScript does not copy that object, but return a reference to it (similar a pointer in C).
This means that if it's modified, it will be reflected in all variables that point to it.
This is the same behaviour for Arrays and other objects.
Anything to avoid that.
You could return a clone of the object. Depending on how complex your object is, a deep clone could be necessary. Keep in mind that this shouldn't be done often, as it can likely affect performance.
An easy way of creating a shallow clone (really just copying properties, cloning any object is more nuanced in JavaScript) in ES6 is...
var cloned = Object.assign({}, objectToBeCloned);

Stacked bar chart in D3js - bars are not on the correct places

I'm trying to build a stacked bar chart in D3js. I have problems to set properly y and y0 attributes and draw the bars on their right positions. Probably I have a calculation mistake but I cannot find it. This is the link to the example code FIDDLE
The scenario is:
I group the data first by "period" and the periods are shown on xAxis
Then I have grouping by "type" - MONTH and ENTRY which should be stacked bars in different colors.
The sum "amount" for each type per each period is shown on yAxis.
I use nest function with 2 keys to structure the data. The problem appears when I draw the bars in the actual stacked bar chart. I'm not sure whether the problem is in the way I access the data (key and values) or in the way I set the attributes "y" and "height".
selection.selectAll("rect")
.data(function (d) { return d.values; })
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("y", function (d) { return y(d.values); })
.attr("height", function (d) { return y(d.y0) + y(d.values); })
//.attr("height", function (d) { return y(d.y0) - y(d.values); })
.style("fill", function (d) { return color(d.key); })
The obvious errors are that one of the bars is hidden behind another one. And the second bar is under the xAxis.
I'm beginner in d3js and I cannot find the solution. Can somebody help me?
I can see a few things:
It looks like you're overcomplicating the nest. You should only need to nest a single level.
The max value that you're calculating will only ever be the maximum of a single element of the stack, when you actually want the maximum to be the total of the stack.
The group elements that you're creating (g), seem to be grouped the "wrong" way. You generally want to group the same "bit" of each stack. That is, you want the first rect of each stack to be in the same group as the other first rects. Then the second one in each stack will be grouped with the other second rects and so on. This is probably due to the nesting error in the first point.
You actually need to calculate the valueOffset, which you've got in your fiddle, but is commented out. This value is used to set the relative position when constructing the stack.
To help, I've put together what seems right based on what you've written. Check out the snippet below.
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 400 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var color = d3.scale.category10();
var data = [
{
"period":201409,
"type":"MONTH",
"amount":85.0
},
{
"period":201409,
"type":"ENTRY",
"amount":111.0
},
{
"period":201410,
"type":"MONTH",
"amount":85.0
},
{
"period":201410,
"type":"ENTRY",
"amount":55.0
}
];
var x = d3.scale.ordinal().rangeRoundBands([0, width], .1, 0);
var y = d3.scale.linear().range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left").ticks(10);
var svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
data.forEach(function(d) {
d["period"] = d["period"];
d["amount"] = +d["amount"];
d["type"] = d["type"];
});
var nest = d3.nest()
.key(function(d) { return d["type"];});
var dataByType = nest.entries(data);
//var max = d3.max(dataByGroup, function(d) { return d3.sum(d.values, function(e) { return e.values; }); })
//console.log("dataByGroup", dataByGroup);
var stack = d3.layout.stack()
.values(function(d) { return d.values; })
.x(function(d) { return d.period; })
.y(function(d) { return d.amount; })
.out(function(d, y0) {
d.valueOffset = y0;
});
//data: key: group element, values: values in each group
stack(dataByType);
var yMax = d3.max(dataByType, function(type) { return d3.max(type.values, function(d) { return d.amount + d.valueOffset; }); });
color.domain(dataByType[0].values.map(function(d) { return d.type; }));
x.domain(dataByType[0].values.map(function(d) { return d.period; }));
y.domain([0, yMax]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 3)
.attr("dy", ".71em")
.style("text-anchor", "end");
var selection = svg.selectAll(".group")
.data(dataByType)
.enter().append("g")
.attr("class", "group");
//.attr("transform", function(d) { return "translate(0," + y0(y0.domain()[0]) + ")"; });
selection.selectAll("rect")
.data(function (d) { return d.values; })
.enter().append("rect")
.attr("width", x.rangeBand())
.attr("x", function(d) { return x(d.period); })
.attr("y", function (d) { return y(d.amount + d.valueOffset); })
.attr("height", function (d) { return y(d.valueOffset) - y(d.valueOffset + d.amount); })
.style("fill", function (d) { return color(d.type); })
.style("stroke", "grey");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="chart"></div>
Some notes on the above snippet (that match my comments):
A much simpler nest:
var nest = d3.nest()
.key(function(d) { return d["type"];});
This is much simpler than your previous one, and there is no need to do the rollup function. Rollups are generally required when you want to aggregate your data, in this case you don't need to, which should be a giveaway that your nesting was too complex.
The calculation of the maximum value for the y axis:
var yMax = d3.max(dataByType, function(type) { return d3.max(type.values, function(d) { return d.amount + d.valueOffset; }); });
This will calculate the maximum value that your axis needs to take, making everything fit nicely.
If you look at the resulting SVG, you'll see what I mean about the grouping of the rects in each stack. I generally find that it's easier to group this way. I guess there's no "right" way, but this typically works best for me.
The calculation of the valueOffset in the stack:
d3.layout.stack()
.values(function(d) { return d.values; })
.x(function(d) { return d.period; })
.y(function(d) { return d.amount; })
.out(function(d, y0) {
d.valueOffset = y0;
});
The calculated valueOffset is used to "move" each rect in the stack into position relative to the other rects. You'll see it used a few times, calculating the max y value, the y attr of each rect, and the height of each rect.
I haven't explained every change that I've made, but hopefully with the above and the snippet you'll be able to work through the differences and apply it your exact use case.

Categories

Resources