D3 exit().remove() on stack area chart - d3.js

I'm having some trouble with the exit().remove() function in a stacked area chart I am creating.
JSFiddle here: Link
I have functionality where the user can enable/disable the data in the chart by clicking on the legend rectangle/color. I know that the data is being updated based on console messages and the Y axis changing scale, but the data in the chart does not change. For instance if the user deselects the Failed category the orange layer should disappear and the Failed and Passed layers should re-adjust.
The issue appears to be in lines 214 to 234 in the fiddle, specifically where I am calling exit().remove():
// filter the data
var updatedData = dataSeries.filter(function(d) {
if(d.vis === "1"){
return d;
}
else {
return null;
}
});
stack.values(function(d) { return d.values; });
layer = stack(updatedData);
main_layer.selectAll(".layer")
.data(layer);
main_layer
.attr("d", function(d) { return main_area(d.values); });
main_layer.exit().remove();
The error I am getting is Object [object Array] has no method exit I have tried changing the selectAll to just a select, but that also produces the same error. Thanks in advance.

I finally got this working. The code below updates the layers correctly:
// filter the data
var updatedData = dataSeries.filter(function(d) {
if(d.vis === "1"){
return d;
}
else {
return null;
}
});
stack(updatedData);
var sel = main_layer.select(".layer");
sel
.attr("class", function(d) { return d.key + " layer"; })
.style("fill", function(d, i) {
if(d.vis === "1") {
return z(i);
}
else return null;
})
.attr("d", function(d) {
if(d.vis === "1") {
return main_area(d.values);
}
else return null;
});

Related

dc.js geoChoroplethChart doesn't display legend

I'm trying to plot a dc choropleth , but somehow the legend is not showing up.
Here is the sample fiddle :
http://jsfiddle.net/susram/9VJHe/56/
usChart
.width(1200)
.height(500)
.dimension(state_dim)
.group(latest_mean_sqft_per_state)
//.colors(d3.scale.quantize().range(["#E2F2FF", "#C4E4FF", "#9ED2FF", "#81C5FF", "#6BBAFF", "#51AEFF", "#36A2FF", "#1E96FF", "#0089FF", "#0061B5"]))
.colors(d3.scale.quantize().range(["#fff7fb","#ece2f0","#d0d1e6","#a6bddb","#67a9cf","#3690c0","#02818a","#016c59","#014636"]))
//.colors(d3.scale.quantize().range(d3.schemeBlues()(9)))
.colorDomain([0, 500])
//.colorAccessor(function (d) { /*console.log(d);*/ return d? usChart.colors(d.avg_psft) : '#ccc'; })
.colorAccessor(function (d) { /*console.log(d);*/ return d.avg_psft; })
.overlayGeoJson(statesJson.features, "state", function (d) {
return d.properties.name;
})
.valueAccessor(function(kv) {
console.log(kv);
return kv.value;
})
.title(function (d) {
return "State: " + d.key + "\nAverage Price per SqFt: " + numberFormat(d.value.avg_psft ? d.value.avg_psft : 0) + "M";
})
.legend(dc.legend().x(1300).y(80).itemHeight(13).gap(5));
Why is the legend showing up as 0x0 ?
I've been trying to get the legend to work with geoChoroplethCharts as well and unfortunately legend support appears to not have been implemented yet in dc. There are a few functions (legendables, legendHighlight, legendReset, legendToggle, ect...) that were defined in the dc base-mixin and would need to be extended before legend support would work.
For an example take a look at the source for pieChart:
https://github.com/dc-js/dc.js/blob/develop/src/pie-chart.js
Versus the soruce for geoChoroplethChart:
https://github.com/dc-js/dc.js/blob/develop/src/geo-choropleth-chart.js
You'll notice at the bottom of the pieChart source that the related legend functions were extended. I belive something similar would need to be done for the geoChoroplethChart source code.
EDIT:
I worked off your jsfiddle and was able to get a bare bones label to display on the geoChoroplethChart: http://jsfiddle.net/Lx3x929v/2/
usChart.legendables = function () {
return usChart.group().all().map(function (d, i) {
var legendable = {name: d.key, data: d.value, others: d.others,
chart: usChart};
legendable.color = usChart.colorCalculator()(d.value);
return legendable;
});
};
Here is my modification —for a continuous map— from #MarcTifrea 's solution and comment.
chart.legendables = function () {
var domain = chart.colorDomain();
return domain.map(function (d, i) {
var legendable = {name: parseFloat((Math.round(domain[i] * 100000) /100000).toPrecision(2)) , chart: chart};
if (i==1) legendable.name += ' unit'; // add the unit only in second(last) legend item
legendable.color = chart.colorCalculator()(domain[i]);
return legendable;
});
};
chart.legend(
dc.legend()
.x(width/4)
.y(height*4/5)
.itemHeight(height/30)
// .itemWidth(width/25)
.gap(5)
// .horizontal(1)
// .autoItemWidth(1)
);

Dynamic filtering with D3

I'm quite new to D3 and coding in general. I'm trying to set up a bar chart which includes/excludes data depending on a checkbox. I have a set of product groups and countries which I want to toggle in/out of the total represented by the bar. The output should be one bar per product.
My full data set has many more products, product groups and countries so it is not viable to create a key-value pair for each potential combination of checkboxes. Instead I would like to create a function that re-evaluates the checkboxes and re-filters the data and updates the rollup when a checkbox is changed.
I'm not sure where this function should sit in my code or what it should look like... This is what I'm working with at the moment:
var data = data.filter(function(d) {
if (document.getElementById("nz_button").checked) {
return d.country == 'NZ'
}
if (document.getElementById("au_button").checked) {
return d.country == 'AU'
}
if (document.getElementById("us_button").checked) {
return d.country == 'US'
}
})
// to see how many distinct groups there are and sum volume
var products = d3.nest()
.key(function(d) {
return d.product
})
.rollup(function(leaves) {
var sum = 0;
leaves.forEach(function(d) {
sum += d.volume;
})
return sum
})
.entries(data);
Full code: http://plnkr.co/edit/qezdwMLt48RPc8KH17hS?p=preview
Maybe I should be working with selections and re-running the nest/rollup when required?
Any help appreciated. Thanks :)
You can move the full code which makes the graph in a new function like this:
function makeDataGraph(data) {//function to make the graph.
//
// FILTER
//
var data = data.filter(function(d) {
if (document.getElementById("au_button").checked) {
return d.country == 'AU'
}
if (document.getElementById("us_button").checked) {
return d.country == 'US'
}
if (document.getElementById("nz_button").checked) {
return d.country == 'NZ'
}
})
// to see how many distinct groups there are and sum volume
var products = d3.nest()
.key(function(d) {
return d.product
})
.rollup(function(leaves) {
var sum = 0;
leaves.forEach(function(d) {
sum += d.volume;
})
return sum
})
.entries(data);
// sorting on descending total
console.log(products);
products.sort(function(a, b) {
return b.values - a.values
})
var max = d3.max(products, function(d) {
return d.values;
});
var xscale = d3.scale.linear()
.domain([0, max])
.range([0, 600])
var svg = d3.select("svg");
//
// Still needs to be cleaned up \/ \/
//
var rects = svg.selectAll("rect.product")
.data(products)
rects.exit().remove();
rects.enter().append("rect").classed("product", true)
rects.attr({
x: 200,
y: function(d, i) {
return 100 + i * 50
},
width: function(d, i) {
return xscale(d.values)
},
height: 50
}).on("click", function(d, i) {
console.log(i, d);
})
var labels = svg.selectAll("text.label")
.data(products)
labels.exit().remove();
labels.enter().append("text").classed("label", true)
labels.attr({
x: 195,
y: function(d, i) {
return 128 + i * 50
},
"text-anchor": "end",
"alignment-baseline": "middle"
}).text(function(d) {
return d.key || "N/A"
})
var volume = svg.selectAll("text.volume")
.data(products);
volume.exit().remove();
volume.enter().append("text").classed("volume", true)
volume.attr({
x: function(d, i) {
return 205 + xscale(d.values)
},
y: function(d, i) {
return 128 + i * 50
},
"text-anchor": "start",
"alignment-baseline": "middle"
}).text(function(d) {
return d.values || "N/A"
})
}
Remember to do rects.exit().remove(); so that when the data is changed on click of the checkbox, rectangles related to old data is removed.
Now you can call this function from the click event and also afterloading the tsv like this:
d3.tsv("data.tsv", function(err, udata) {
var udata = udata.map(process);
console.log("udata", udata);
var data = udata // making new var to preserve unfiltered data
makeDataGraph(data);//call the function to make graph
function handleClick() { // event handler...
makeDataGraph(data)
}
//add listener to all check boxes.
d3.selectAll(".filter_button").on("click", handleClick);
});
working code here

Update domain of color scale only once in d3 reusable charts

I am building a reusable chart following this tutorial: https://bost.ocks.org/mike/chart/. The full code is at the end of the question. I have the following problem:
As you can see the 'click' event on a specific component triggers a query that updates the whole chart retrieving new data. I am referring to this line:
selection.datum(relatedConcepts).call(chart); // Update this vis
Now this update works great, but of course given that in the function "chart" I also have
color.domain(data.map(function(d){ return d[0]}));
the domain of the color scale will be also updated and I don't want that.
So the question is: how do I set the color scale domain ONLY the first time the chart gets created?
d3.custom = d3.custom || {};
d3.custom.conceptsVis = function () {
var color = d3.scale.category20();
// To get events out of the module we use d3.dispatch, declaring a "conceptClicked" event
var dispatch = d3.dispatch('conceptClicked');
function chart(selection) {
selection.each(function (data) {
//TODO: This should be executed only the first time
color.domain(data.map(function(d){ return d[0]}));
// Data binding
var concepts = selection.selectAll(".progress").data(data, function (d) {return d[0]});
// Enter
concepts.enter()
.append("div")
.classed("progress", true)
.append("div")
.classed("progress-bar", true)
.classed("progress-bar-success", true)
.style("background-color", function (d) {
return color(d[0])
})
.attr("role", "progressbar")
.attr("aria-valuenow", "40")
.attr("aria-valuemin", "0")
.attr("aria-valuemax", "100")
.append("span") // (http://stackoverflow.com/questions/12937470/twitter-bootstrap-center-text-on-progress-bar)
.text(function (d) {
return d[0]
})
.on("click", function (d) {
// Update the concepts vis
d3.json("api/concepts" + "?concept=" + d[0], function (error, relatedConcepts) {
if (error) throw error;
selection.datum(relatedConcepts).call(chart); // Update this vis
dispatch.conceptClicked(relatedConcepts, color); // Push the event outside
});
});
// Enter + Update
concepts.select(".progress-bar").transition().duration(500)
.style("width", function (d) {
return (d[1] * 100) + "%"
});
// Exit
concepts.exit().select(".progress-bar").transition().duration(500)
.style("width", "0%");
});
}
d3.rebind(chart, dispatch, "on");
return chart;
};
ANSWER
I ended up doing what meetamit suggested and I added this:
// Getter/setter
chart.colorDomain = function(_) {
if (!arguments.length) return color.domain();
color.domain(_);
return chart;
};
to my conceptsVis function, so that from the outside I can do:
.... = d3.custom.conceptsVis().colorDomain(concepts);
Of course I deleted the line:
color.domain(data.map(function(d){ return d[0]}));
You can check if the domain is an empty array and only populate it if it is:
if(color.domain().length == 0) {
color.domain(data.map(function(d){ return d[0]}));
}
That being said, this behavior seems fundamentally wrong, or at least bug-prone. It means that the populating of the domain is a side-effect of the first render. But what is it about that first render that makes it different than subsequent calls and therefore worthy of setting the domain? What happens if later, as your app evolves, you decide to render a different dataset first and afterwards render what is currently the first dataset? Then you might end up with a different domain. It seems more sane to compute the domain explicitly, outside of the chart's code, and then pass the domain into the chart via a setter. Something like:
chart.colorDomain(someArrayOfValuesThatYouPreComputeOrHardCode)

dc.js dataTable - how to use a pre-defined group

I would like to load a dc dataTable with dimensional data that has been grouped and filtered. Is this possible? I am assuming it is but the examples I have seen so far have simple groups coded at the time of table construction and I can't get it to work any other way!
var facts = crossfilter(data);
var testTable = dc.dataTable('#testtable');
var providerDim = facts.dimension(function (d) {
if (d.report_by_3 !== null) {
return d.report_by_3;
}
});
var MH003Group = providerDim.group().reduceSum(function (d) {
if (d.indicator_code === 'MH003') {
return (d.observed / d.expected);
} else {
return null;
}
});
var MH003Group2 = {
all: function () {
return MH003Group.all().filter(function (d) {
return d.value !== null;
});
}
};
dataTable.width(960)
.height(8000)
.dimension(providerDim)
// .group(MH003Group2)
.group(function (d) {
return 'data';
})
.size(10)
.columns([function (d) {return d.indicator_code;},
function (d) {return d.report_by_1;},
function (d) {return d.report_by_3;},
function (d) {return (d.observed / d.expected);}
]);
.sortBy(function (d) {return d.report_by_1;})
.order(d3.ascending);
In the code above I would like to display filtered rows in the dimension using the pre-defined grouping rather than all the rows of data in the dimension which is what is currently happening.

D3 sort() with CSV data

I am trying all kinds of ways to make .sort() work on my csv dataset. No luck.
I'd just like to sort my data by a "value" column.
This is the function I'm running inside my d3.csv api call and before I select the dom and append my divs:
dataset = dataset.sort(function (a,b) {return d3.ascending(a.value, b.value); });
Before I get to the .sort, I clean the data:
dataset.forEach(function(d) {
d.funded_month = parseDate(d.funded_month);
d.value = +d.value;
});
};
Everything seems in order. When I console.log(d3.ascending(a.value, b.value)), I get the right outputs:
-1 d32.html:138
1 d32.html:138
-1 d32.html:138
1 d32.html:138
etc..
Yet the bars data doesn't sort.
It is not clear from the provided code but I will hazard a guess you are not handling async nature of d3.csv.
This plunkr shows your sort code working fine. Note where the data object is declared, populated, and used.
here is a partial listing. I have added buttons that re-order data. To achieve this we need to put the ordering logic inside render rather than inside the d3.csv callback.
<script type="text/javascript">
var data = [];
d3.csv("data.csv",
function(error, rows) {
rows.forEach(function(r) {
data.push({
expense: +r.expense,
category: r.category
})
});
render();
});
function render(d3Comparator) {
if(d3Comparator) data = data.sort(function(a, b) {
return d3[d3Comparator](a.expense, b.expense);
});
d3.select("body").selectAll("div.h-bar") // <-B
.data(data)
.enter().append("div")
.attr("class", "h-bar")
.append("span");
d3.select("body").selectAll("div.h-bar") // <-C
.data(data)
.exit().remove();
d3.select("body").selectAll("div.h-bar") // <-D
.style("width", function(d) {
return (d.expense * 5) + "px";
})
.select("span")
.text(function(d) {
return d.category;
});
}
</script>
<button onclick="render('ascending')">Sort ascending!</button>
<button onclick="render('descending')">Sort descending!</button>

Resources