Updating nested Area chart in D3.js - javascript

In my application I have multiple checkboxes which contain the name of the product. User can select one or multiple products. When the user selects a particular product an area graph is drawn based on the number of products sold on certain days.
If multiple products are selected a stacked area graph is drawn.
If no product is selected no graph is shown just a message to select atleast one product is shown.
Code:
This is called once when the graph is initialized.
function initialiseChart(element){
var bRect = element.getBoundingClientRect();
var margin = {top: 0, right: 0, bottom: 20, left: 20};
var height = 250 - margin.top - margin.bottom;
var width = bRect.width - margin.left - margin.right;
var localChart = d3.select(element).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 + ")");
}
This is called when a user selects or de-selects a checkbox, selectedIds contain all the product ids which are selected. If all are deselected then selectedIds array is an empty array.
function updateChart(selectedIds,localChart,height,width){
var z = d3.scale.category20c();
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear() .range([height, 0]);
var xAxis = d3.svg.axis().scale(x).orient("bottom").ticks(12);
var yAxis = d3.svg.axis().scale(y).orient("left");
var stack = d3.layout.stack()
.offset("zero")
.values(function(d) {
return d.values;
})
.x(function(d,i) { return d.date; })
.y(function(d,i) { return d.counts; });
var nest = d3.nest()
.key(function(d) { return d.pid; });
var area = d3.svg.area()
.interpolate("cardinal")
.x(function(d) {
return x(d.date);
})
.y0(function(d) {
return y(d.y0);
})
.y1(function(d) {
return y(d.y0 + d.y);
});
var data = assignDefaultValues(selectedIds); //returns the data for selectedIds
var layers = stack(nest.entries(data));
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, d3.max(data, function(d) { return d.y0 + d.y; })]);
localChart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
localChart.append("g")
.attr("class", "y axis")
.call(yAxis);
localChart.selectAll(".layer")
.data(layers)
.exit().remove();
localChart.selectAll(".layer")
.data(layers)
.enter().append("path")
.attr("class", "layers")
.attr("d", function(d) { return area(d.values); })
.style("fill", function(d, i) { return z(i); });
}
Problem:
Whenever I select or deselect the checkbox the selectedIds array has content as expected, as in empty if all are selected or ids of one or more products which are selected.
But the axes are redrawn, also the first graph that is plotted doesnt go away even if its deselected.
What is it that I am doing wrong.

Related

D3 area chart not showing first index object in data array

First time using the d3 library, I am pulling my data from a local object structured below. Attempting to show the data but the 0 index data is not being shown (see in screenshot) despite the data array having content. I have tried many things including reading other stack questions for similar issues (such as D3 bar chart not showing first element in array), but their solutions are not working for me since my d3 is structured differently. I have been stuck on this for over 3 hours now and any help is appreciated.
Data array
roundDataArr = [...roundDataArr, {'round': roundNumber, 'data': finalRoundTime}]
1st object not displayed on graph:
Relevant code:
function displayGraph() {
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 575 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
var x = d3.scaleLinear()
.domain([0, d3.max(roundDataArr, function(d) { return d.round; })])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, d3.max(roundDataArr, function(d) { return d.data; })])
.range([height, 0]);
var xAxis = d3.axisBottom()
.scale(x);
var yAxis = d3.axisLeft()
.scale(y);
var area = d3.area()
.x(function(d) { return x(d.round); })
.y0(height)
.y1(function(d) { return y(d.data); });
var valueline = d3.line()
.x(function(d) { return x(d.round); })
.y(function(d) { return y(d.data); });
var svg = d3.select(".tfny-graphWrapper")
.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 + ")");
svg.append("path")
.datum(roundDataArr)
.attr("class", "area")
.attr("d", area);
svg.append("path")
.datum(roundDataArr)
.attr("class", "line")
.attr("d", valueline);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
}
And the wrapper div is appended to the DOM through Javascript with this code:
let graphWrapper = document.createElement("div")
graphWrapper.classList.add("tfny-graphWrapper");
resultPage.appendChild(graphWrapper);

How to rescale displayed data in d3.js?

I am working on a scatter plot chart which is able to filter my data. My problem is that when I filter and remove some data, the displayed data no longer fills up the container. I need to update the domain to the new extent (which excludes the removed data points).
Here is my code:
var parseTime = d3.timeParse("%Y-%m-%d");
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var svg = d3.select("body").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 + ")");
function draw(data, instrument) {
var data = data[instrument];
data.forEach(function(d) {
d.date = parseTime(d.date);
d.close = +d.close;
d.year = +d.dyear;
d.month = +d.month;
d.day = +d.day;
});
x.domain(d3.extent(data, function(d) { return d.date; }));
y.domain([0, 1.2*d3.max(data, function(d) { return d.close; })]);
svg.selectAll("dot")
.data(data)
.enter()
.append("circle")
.style("fill", "red")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.date); })
.attr("cy", function(d) { return y(d.close); })
.filter(function(d) { return d.day > 12; })
.remove();
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.ticks(10));
svg.append("g")
.attr("class", "axis")
.call(d3.axisLeft(y)
.ticks(8));
}
var dataset = d3.json("test.json", function(error, data) {
if (error) throw error;
draw(data, "test_instrument");
});
You should filter the data, then reset the scale'domain based on the filtered dataset. You may not need to remove circles that don't meet the criteria, but instead set the 'visibility' or 'opacity' instead (depending how many you want to hide)
let threshold = 12;
let filteredData = data.filter(function(d){ return d.day > threshold ; })
...
x.domain(d3.extent(filteredData , function(d) { return d.date; }));
...
svg.selectAll("dot")
.data(data)
.enter()
.append("circle")
...
.style("visibility", function(d) { return d.day > threshold ? "visible": "hidden"; })

d3js chart dots and area not updating

I have a function I call to render a d3js chart:
var tooltip = tooltipd3();
var svg = d3.select("svg#svg-day"),
margin = {
top: 20,
right: 30,
bottom: 30,
left: 25,
padding: 15
},
width = 700 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// parse the periodo / time
var parseTime = d3.timeParse("%Y-%m-%d");
// set the ranges
var x = d3.scaleTime().range([0, width - margin.padding]);
var y = d3.scaleLinear().range([height, 0]);
// define the area
var area = d3.area()
.x(function(d) {
return x(d.periodo) + (margin.left + margin.padding);
})
.y0(height)
.y1(function(d) {
return y(d.guadagno);
});
// define the line
var valueline = d3.line()
.x(function(d) {
return x(d.periodo) + (margin.left + margin.padding);
})
.y(function(d) {
return y(d.guadagno);
});
var div = d3.select("svg#svg-day")
.append("div") // declare the tooltip div
.attr("class", "tooltip") // apply the 'tooltip' class
.style("opacity", 0);
// get the data
d3.csv(base_url() + 'graph/getStatementsDaily/', function(error, data) {
if (error) throw error;
$('.graph-loading').hide();
// format the data
data.forEach(function(d) {
d.periodo = parseTime(d.periodo)
d.guadagno = +d.guadagno;
});
// scale the range of the data
x.domain(d3.extent(data, function(d) {
return d.periodo;
}));
y.domain([0, d3.max(data, function(d) {
return d.guadagno + ((d.guadagno / 100) * 10); // 10% in più sulla scala numerica
})]);
// add the area
svg.append("path")
.data([data])
.attr("class", "area")
.attr("d", area);
// add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the scatterplot
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 3)
.attr("cx", function(d) {
return x(d.periodo) + (margin.left + margin.padding);
})
.attr("cy", function(d) {
return y(d.guadagno);
})
.on('mouseover', function(d) {
var html = '<h5>' + d.guadagno + ' €</h5>';
tooltip.mouseover(html); // pass html content
})
.on('mousemove', tooltip.mousemove)
.on('mouseout', tooltip.mouseout);
// add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + (margin.left + margin.padding) + "," + (height) + ")")
.call(d3.axisBottom(x).tickFormat(d3.timeFormat("%d/%m")))
// add the Y Axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate (" + (margin.left + margin.padding) + " 0)")
.call(d3.axisLeft(y));
});
This is the result:
The sides button you see are to change the csv url so that the chart updates on click, I do this with this:
$('.input-number__increase, .input-number__decrease').on('click', function() {
var where_at = $('#scroll-statement-day').val();
$('.graph-loading').show();
$('#svg').css({ 'opacity': 0.4 });
var display_where_at = (where_at - 7) + '-' + where_at;
if (parseInt(where_at) === 7) {
display_where_at = where_at;
}
$('#data-days').html(display_where_at);
var tooltip = tooltipd3();
var svg = d3.select("svg#svg-day"),
margin = {
top: 20,
right: 30,
bottom: 30,
left: 25,
padding: 15
},
width = 700 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
// parse the periodo / time
var parseTime = d3.timeParse("%Y-%m-%d");
// set the ranges
var x = d3.scaleTime().range([0, width - margin.padding]);
var y = d3.scaleLinear().range([height, 0]);
// define the area
var area = d3.area()
.x(function(d) {
return x(d.periodo) + (margin.left + margin.padding);
})
.y0(height)
.y1(function(d) {
return y(d.guadagno);
});
// define the line
var valueline = d3.line()
.x(function(d) {
return x(d.periodo) + (margin.left + margin.padding);
})
.y(function(d) {
return y(d.guadagno);
});
var div = d3.select("svg#svg-day")
.append("div") // declare the tooltip div
.attr("class", "tooltip") // apply the 'tooltip' class
.style("opacity", 0);
var speed = 750;
d3.csv(base_url() + 'graph/getStatementsDaily/' + where_at, function(error, data) {
if (error) throw error;
$('.graph-loading').hide();
$('#svg').css({ 'opacity': 1 });
// format the data
data.forEach(function(d) {
d.periodo = parseTime(d.periodo)
d.guadagno = +d.guadagno;
});
// Scale the range of the data again
x.domain(d3.extent(data, function(d) {
return d.periodo;
}));
y.domain([0, d3.max(data, function(d) {
return d.guadagno + ((d.guadagno / 100) * 10); // 10% in più sulla scala numerica
})]);
// Select the section we want to apply our changes to
var svg = d3.select("body").transition();
// Make the changes
svg.select(".line") // change the line
.duration(speed)
.attr("d", valueline(data));
svg.selectAll("g.x.axis") // change the x axis
.duration(speed)
.call(d3.axisBottom(x).tickFormat(d3.timeFormat("%d/%m")));
svg.selectAll("g.y.axis") // change the y axis
.duration(speed)
.call(d3.axisLeft(y));
svg.select("path")
.duration(speed)
.attr("d", area);
svg.select("circle")
.duration(speed)
.attr("r", 3)
.attr("cx", function(d) {
return x(d.periodo) + (margin.left + margin.padding);
})
.attr("cy", function(d) {
return y(d.guadagno);
})
});
});
This works only partially, as I get this result:
I tried to figure out why but I cannot get it.. Any ideas?
When you do this:
svg.select("circle")
You are selecting only the first circle (if any) in the page. According to the API, select...
Selects the first element that matches the specified selector string. (emphasis mine)
That being said, you need selectAll here. But that alone will not fix the problem: you have to rebind the data. Since I don't know your data structure, the default method binds by index.
All together, it should be:
svg.selectAll("circle")
.data(data)
//etc...
As those circles have a class named dot, you can avoid selecting other circles using:
svg.selectAll(".dot")
.data(data)
//etc...
Regarding the line and the area, do the same: bind the data first and then change their d attribute:
svg.select(".area")
.data([data])
.attr("d", area);
svg.select(".line")
.data([data])
.attr("d", valueline);
Also, since you are rebinding the data, you'll have to change this:
var svg = d3.select("body").transition();
Because svg.selectAll will be a transition selection.That being said, set the transition to each individual selection after rebinding the data, removing it from the svg selection.

Error: <rect> attribute width: Expected length, "NaN". Uncaught TypeError: Cannot read property 'length' of undefined

I've been fiddling with this for a few days now and I either get the error in the title, or I get "key.call is not a function", and I don't know why... basically the point of the script is to load different csv files based on the selection made in the dropdown. Without all the dropdown stuff, the script works fine, but once I introduce the dropdown script it starts bugging out and I can't figure out why. Below is the script, and I'll attach a sample of the kind of csv I'm working with if that helps at all.
Thanks a ton in advance!
CSV example: https://www.dropbox.com/s/24zopp43r3y1ft6/PCC-edit.csv?dl=0 (QC = QuantityCharged in this example)
<svg class="chart"></svg>
<select onchange="loadData()" id="metric">
<option>A-codes</option>
<option>Anesthesia</option>
<option>C-codes</option>
<option>Clinical Components (NRV)</option>
<option>E-Codes</option>
<option>Emerging Technology</option>
<option>Evaluation & Management</option>
<option>G-codes</option>
<option>J-codes</option>
<option>L-codes</option>
<option>Medicine</option>
<option>Pathology & Laboratory</option>
<option>P-codes</option>
<option>Q-codes</option>
<option>Radiology</option>
<option>Surgery</option>
<option>V-codes</option>
</select>
<script src="http://d3js.org/d3.v3.js" charset="utf-8"></script>
<script>
var svg = d3.select("svg"),
margin = {top:10, right: 720, bottom: 5, left: 720},
width = +svg.attr("width") + margin.left + margin.right,
height = +svg.attr("height") + margin.top + margin.bottom;
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var x = d3.scale.linear()
.range([3, width]);
var y = d3.scale.ordinal()
.range([height, 1]);
var chart = d3.select(".chart")
.attr("width", width);
var parseRow = function(row) {
return {
subcategory: row.Subcategory,
quantityCharged: parseFloat(row.QuantityCharged)
}
};
//d3
var loadData = function(data) {
var metric = document.getElementById('metric').selectedOptions[0].text;
var dataFile = 'data/' + metric + '.csv'
d3.csv(dataFile, parseRow, function(error, data) {
x.domain([1, d3.max(data, function(d) { return d.QuantityCharged; })]);
y.domain([1, d3.max(data, function(d) { return d.Charge; })])
});
chart.attr("height", height);
var bar = chart.selectAll("g")
.data("width", data)
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * height + ")"; });
bar.append("rect")
.attr("width", function(d) { return x(d.QuantityCharged); })
.attr("height", height - 1);
bar.append("text")
.attr("x", function(d) { return x(d.QuantityCharged) + 10; })
.attr("y", height / 2)
.attr("dy", ".35em")
.text(function(d) { return d.QuantityCharged + " -- " + d.Charge; });
var xAxis = d3.svg.axis()
.scale(x)
.ticks(20)
.orient("top")
var yAxis = d3.svg.axis()
.scale(y)
.ticks(24)
.orient("left");
var line = d3.svg.line()
.x(function(d) { return x(d.QuantityCharged); })
.y(function(d) { return y(d.QuantityCharged); });
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("path")
.attr("class", "line")
.attr("data", line);
function type(d) {
d.QuantityCharged = +d.QuantityCharged; // coerce to number
d.Subcategory = d.Subcategory;
return d;
}};
function Subcategory(csv) {
d3.text(dataFile, parseRow, function(text) {
var Subcategory = d3.csv.parseRows(text),
Charge = d3.csv.parseRows(text);
})}
loadData();
</script>

Adding dynamic tooltips to a streamgraph d3.js

Im working on a streamgraph at the moment, I want to add tooltips to each layer similar to this http://archive.stamen.com/mtvmovies-streamgraph/chart.html
The tooltips I have now dont really work at all. All I get is 'NaN' displayed in the tooltip box.
Any suggestions?? My code is below.
Thanks in advance.
var customPalette = [
"#ff7f0e", "#2ca02c", "#00FFFF", "#d62728", "#9467bd",
"#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"
];
var format = d3.time.format("%y");
//creating margins around the graph
var margin = {top: 20, right: 30, bottom: 30, left: 200},
width = 1200 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
//OUTPUT RANGE
var x = d3.time.scale()
.range([0, width]);
//OUTPUT RANGE
var y = d3.scale.linear()
.range([height, 0]);
//assining custom colors to layers
var colours = d3.scale.ordinal().range(customPalette);
var xAxis = d3.svg.axis()
.scale(x)
.orient("top")
.ticks(d3.time.years);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
//ctreate stack layout
var stack = d3.layout.stack()
.offset("wiggle")
.order("reverse")
.values(function(d) { return d.values; })
.x(function(d) { return d.date; })
.y(function(d) { return d.amount; });
//creates array of datya elements for stacked bar graph
var nest = d3.nest()
.key(function(d) { return d.age; });
//create area
var area = d3.svg.area()
//adds curviture
.interpolate("monotone")
.x(function(d) { return x(d.date); })
.y0(function(d) { return y(d.y0); })
.y1(function(d) { return y(d.y0 + d.y); });
var svg = d3.select("body").append("svg")
//defines length of x-axis
.attr("width", width + margin.left + margin.right)
//defines height of y-axis
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("data6.csv", function(data) {
data.forEach(function(d) {
// Convert strings to numbers
d.date = format.parse(d.date);
d.amount = +d.amount;
});
//returns an array of objects with a key feild (0-20yrs....)
//and a value array which contains associated records
var layers = stack(nest.entries(data));
//.extent() returns min and max values of argument
x.domain(d3.extent(data, function(d) { return d.date; }));
//
y.domain([0, d3.max(data, function(d) { return d.y0 + d.y; })]);
svg.selectAll(".layer")
.data(layers)
.enter().append("path")
.attr("class", "layer")
.attr("d", function(d) { return area(d.values); })
.style("fill", function(d, i) { return colours(i); });
//CURRENT TOOLTIP CODE
var toolTip = svg.selectAll("path")
.append("svg:title")
.text(function(d) { return (d.date) + (d.amount) });;
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + 0 + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
Are you hoping to concatenate the values of d.date and d.close, or are you interested in adding the values and returning the result?
If the latter:
I would bet that the types of d.date and d.close are not what you expect. I'd recommend, if you haven't already, putting some debug code in to check the types of those variables. Example:
//CURRENT TOOLTIP CODE
var toolTip = svg.selectAll("path")
.append("svg:title")
.text(function(d) {
console.log('d.date type:' + typeof d.date + 'd.close type:' + typeof d.close);
return (d.date) + (d.close);
}
);
Also, you have an extra semicolon at the end of that statement in your code snippet.
If the former:
One or both are of type number and Javascript will try to add them when you use the + operator instead of concatenating them. To return the strings:
.text(function(d) {
return d.date.toString() + ' ' + d.close.toString();
});

Categories

Resources