My Javascript skills are fairly basic but advanced enough to get me to a position where I am able to design some relatively complex charts using the d3 library. I've now got to the point where I am looking to make some of my code reusable and this is where I've come across the first hurdle that the excellent api documentation can't get me past.
As an example, in the majority of my charts I want to add a legend. I have some basic code that adds a svg group for the legend which looks like this
var legend = chartContainer.selectAll(".legend")
.data(d3.map(data, function(d){return d.FIELDNAME;}).keys())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + (((height/3)) + (i*20)) + ")"; });
what I'd like to be able to do is add a parameter to a function which allows me to set what field from the dataset is used in the legend. Something like the below... but that actually works :)
function addLegend(legendKey) {
var legend = chartContainer.selectAll(".legend")
.data(d3.map(data, function(d){return d.legendKey;}).keys())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + (((height/3)) + (i*20)) + ")"; });
// rest of the legend code
}
called by
addLegend("FIELDNAME");
Lastly, understandably (perhaps) this didn't work either
function helper(legendKey){
return legendKey;
}
function addLegend(helper) {
var legend = chartContainer.selectAll(".legend")
.data(d3.map(data, function test (d, addLegend){return d.addLegend;}).keys())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + (((height/3)) + (i*20)) + ")"; });
// rest of the legend code
}
addLegend(helper("agent"))
Any help would be greatly appreciated.
Related
I'm trying to implement the general update pattern in D3, to allow me to update existing elements before entering new ones.
This code works and generates the elements I want:
var nodes = nodeGroup.selectAll(".node")
.data(root.descendants())
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y +")";
})
Now I want to change the code to store the update selection and modify some attributes before retrieving the enter selection.
But when I make this simple change, nothing gets appended any more:
var nodes = nodeGroup.selectAll(".node")
.data(root.descendants())
nodes.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y +")";
})
Why don't these two pieces of code behave identically?
I'm building a dashboard with a couple of d3 visualizations, which looks like:
The way I built this is by making a few div's on the page, and call the .js script inside these divs. This worked perfectly fine so far. Unfortunately I run into a problem when calling my .js file with a line graph on the right top div on my page. When I call the graph.js file, this happens:
I'm not entirely sure what's happening, but I think both the visualizations are using "path" elements, and therefore they interfere with eachother. This is the main code for the map of Europe:
//Load in GeoJSON data
d3.json("ne_50m_admin_0_countries_simplified.json", function(json) {
//Bind data and create one path per GeoJSON feature
svg.selectAll("path")
.data(json.features)
.enter()
.filter(function(d) { return d.properties.continent == 'Europe'}) //we only want Europe to be shown ánd loaded.
.append("path")
.attr("class", "border")
.attr("d", path)
.attr("stroke", "black")
.attr("fill", "#ADD8E6")
.attr("countryName", function(d){return d.properties.sovereignt})
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div .html(d.properties.sovereignt + ', ' + getCountryTempByYear(dataset, d.properties.sovereignt, document.getElementById("selectYear").value))
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
})
.on("click", function(d){
//add/delete country to/from selected countries
updateSelectedCountries(d.properties.sovereignt);
//draw the temperature visualizations again
redrawTemp(getCountryTempByYear(dataset, selectedCountries[0], document.getElementById("selectYear").value), getCountryTempByYear(dataset, selectedCountries[1], document.getElementById("selectYear").value));
//update header from the webpage
d3.select("#header").text("Climate Change: 1840-2013. Currently viewing: " + selectedCountries + ", " + document.getElementById("selectYear").value + '.');
console.log(selectedCountries);
});
});
And this is the code for the line graph:
// set the dimensions and margins of the grap
var array = [[1850, 11.1], [1851, 11.7], [1852, 12.2], [1853, 11.1], [1854, 11.7], [1855, 12.2], [1856, 13.4]]
var array2 = [[1850, 14.1], [1851, 17.7], [1852, 22.2], [1853, 13.1], [1854, 24.7], [1855, 19.2], [1856, 13.4]]
// set the ranges
var x = d3.scaleLinear().range([0, 200]);
var y = d3.scaleLinear().range([200, 0]);
// define the line
var valueline = d3.line()
.x(function(d, i) { return x(array[i][0]); })
.y(function(d, i) { return y(array[i][1]); })
.curve(d3.curveMonotoneX); //smooth line
var valueline2 = d3.line()
.x(function(d, i) { return x(array2[i][0]); })
.y(function(d, i) { return y(array2[i][1]); })
.curve(d3.curveMonotoneX); //smooth line
var svg = d3.select("#div2")
.append("svg")
.attr("width", 200)
.attr("height", 200)
.attr("id", "graph");
x.domain([1850, 1856]);
y.domain([10, 25]);
array.forEach(function(data, i){
svg.append("path")
.data([array])
.attr("class", "line")
.attr("d", valueline);
})
array2.forEach(function(data, i){
svg.append("path")
.data([array2])
.attr("class", "line")
.attr("d", valueline2);
})
// Add the X Axis
svg.append("g")
.attr("transform", "translate(0," + 200 + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
svg.append("g")
.call(d3.axisLeft(y));
As I said, I'm not entirely sure it's because of the graphs both using the "path" element, it could be something different too. Now what I'm wondering is: what is interfering with eachother? And how can I prevent this?
I can imagine something like a filter needs to be applied, but I'm not sure how to apply this to the right "path" elements.
Any help is highly appreciated!
When you create a new chart you should create a new variable to associate with it.
In your case, this might be happening due to the reuse of the variable var svg =....
Create one variable for each chart should be enough.
I want to shift the legends on top of the chart
I tried using CSS position relative but seems that it does not work on SVG
please have a look at the codepen
http://codepen.io/7deepakpatil/pen/LkaKoy
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
above is the code where the legend are being generated.
is it possible using css or please provide any d3 or JS way to fix this.
Use SVG translate. Similar to what you have but you need to apply it to the whole legend. So I appended a 'g' element, gave it an id for easy selection later and applied a translate before you appended anything to it, like so :
var legend = svg.append('g').attr('id','legendContainer')
.attr("transform", function(d) { return "translate(-200,100)"; }) //this is the translate for the legend
.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
Updated codepen : http://codepen.io/anon/pen/GqLXRx
I have some code on this jsFiddle here that generates a histogram for a data array called "values". That's all well and good.
When I want to update this histogram with a new data array, called "newData", things go wrong. I am trying to adhere to the enter(), update(), exit() D3 strategy (which I am obviously extremely new with). An animation does indeed occur, but as you can see by the fiddle, it just squishes everything into the upper right hand corner. Can someone point out what I am doing wrong in this segment of the code (the update)?
//Animations
d3.select('#new')
.on('click', function(d,i) {
var newHist = d3.layout.histogram().bins(x.ticks(bins))(newData);
var rect = svg.selectAll(".bar")
.data(values, function(d) { return d; });
// enter
rect.enter().insert("g", "g")
.attr("class", "bar")
.attr("transform", function(d) { return "translate(" + x(d) + "," + y(d) + ")"; });
rect.enter().append("rect")
.attr("x", 1)
.attr("width", w)
.attr("height", function(d) { return y(d); });
rect.enter().append("text")
.attr("dy", ".75em")
.attr("y", 6)
.attr("x", x(histogram[0].dx) / 2)
.attr("text-anchor", "middle")
.text(function(d) { return formatCount(d); });
// update
svg.selectAll('.bar')
.data(newHist)
.transition()
.duration(3000)
.attr("transform", function(d) { return "translate(" + x(d.x) + "," + y(d.y) + ")"; });
svg.selectAll("rect")
.data(newHist)
.transition()
.duration(3000)
.attr("height", function(d) { return height - y(d.y); });
svg.selectAll("text")
.data(newHist)
.transition()
.duration(3000)
.text(function(d) { return formatCount(d.y); });
// exit
rect.exit()
.remove();
});
The entirety of the code is on the JSFiddle linked above. Thanks!
Looking at the code above and the fiddle, a few things jump out at me:
(Line 85) You are still binding the original data
(Lines 105, 115) You are binding the data multiple times
(Line 99) You are still referencing the original histogram variable without updating it with the new data
You are declaring multiple bind/add/update/remove patterns for a single set of (changing) data
You're on the right track, but you need to differentiate between things that need to be updated when the data changes, and things that should not be updated/declared when the data changes. You should only have to declare the d3 pattern (bind, add, update, remove) once... it will work for updated datasets.
So, declare as much as you can outside the makeHist(values) function, and only have code that needs the changed data inside the function (this includes modifying a previously declared scale's domain and range). Then, the on click function can simply call the makeHist function again with the new data.
Here's a rough outline:
// generate data
// declare everything that can be static
// (add svg to dom, declare axes, etc)
// function that handles everything that new data should modify
function makeHist(values) {
// modify domains of axes, create histogram
// bind data
var rect = svg.selectAll('rect')
.data(histogram);
// add new elements
rect.enter().append('rect');
// update existing elements
rect.transition()
.duration(3000)
.attr('transform', '...');
// remove old elements
rect.exit().remove();
}
// generate initial histogram
makeHist(initialValues);
// handle on click event
d3.select('#new')
.on('click', function() {
makeHist(newData);
});
Here's a mostly working updated fiddle, it needs a little bit of cleanup, though:
http://jsfiddle.net/spanndemic/rf4cw/
Spoiler Alert: the two datasets aren't all that different
I am using a large amount of JSON data from an API for D3 bar charts. I would like to show only 10-20 bars at a time. Is there a way to paginate using D3 or do I need to do this another way (php)? Any best practices or suggestions are welcome.
I know this is a late question, but maybe this can still help you out.
I would create pagination in d3 by creating a second array that only contains the data you want shown at a particular time. This sliced array would come from your primary data array. By controlling where the array is sliced, you control the pagination.
I've created a simple example here with a long array divided into five-bar 'pages'.
http://jsfiddle.net/zNxgn/2/
Please go thorugh this piece of code but it makes sense if you go through my block. I have only put the essential part of the code. Link: http://bl.ocks.org/pragyandas
var legendCount = data.series.length;
var legendWidth=10; var legendSpacing=6;
var netLegendHeight=(legendWidth+legendSpacing)*legendCount;
var legendPerPage,totalPages,pageNo;
if(netLegendHeight/height > 1){
legendPerPage=Math.floor(height/(legendWidth+legendSpacing));
totalPages=Math.ceil(legendCount/legendPerPage);
pageNo=1;
var startIndex=(pageNo-1)*legendPerPage;
var endIndex=startIndex+legendPerPage;
var seriesSubset=[],colorSubset=[];
for(var i=0;i<data.series.length;i++){
if(i>=startIndex && i<endIndex){
seriesSubset.push(data.series[i]);
colorSubset.push(colors[i]);
}
}
DrawLegendSubset(seriesSubset,colorSubset,legendPerPage,pageNo,totalPages);
}
function DrawLegendSubset(seriesSubset,colorSubset,legendPerPage,pageNo,totalPages){
var legend = svg.selectAll("g.legendg")
.data(seriesSubset)
.enter().append("g")
.attr('class','legendg')
.attr("transform", function (d, i) { return "translate(" + (width-40) + ","+ i*(legendWidth+legendSpacing) +")"; });
legend.append("rect")
.attr("x", 45)
.attr("width", legendWidth)
.attr("height", legendWidth)
.attr("class", "legend")
.style('fill',function(d,i){return colorSubset[i];});
legend.append("text")
.attr("x", 60)
.attr("y", 6)
.attr("dy", ".35em")
.style("text-anchor", "start")
.text(function (d) { return d.name; });
var pageText = svg.append("g")
.attr('class','pageNo')
.attr("transform", "translate(" + (width+7.5) + ","+ (legendPerPage+1)*(legendWidth+legendSpacing) +")");
pageText.append('text').text(pageNo+'/'+totalPages)
.attr('dx','.25em');
var prevtriangle = svg.append("g")
.attr('class','prev')
.attr("transform", "translate(" + (width+5) + ","+ (legendPerPage+1.5)*(legendWidth+legendSpacing) +")")
.on('click',prevLegend)
.style('cursor','pointer');
var nexttriangle = svg.append("g")
.attr('class','next')
.attr("transform", "translate(" + (width+20) + ","+ (legendPerPage+1.5)*(legendWidth+legendSpacing) +")")
.on('click',nextLegend)
.style('cursor','pointer');
nexttriangle.append('polygon')
.style('stroke','#000')
.style('fill','#000')
.attr('points','0,0, 10,0, 5,5');
prevtriangle.append('polygon')
.style('stroke','#000')
.style('fill','#000')
.attr('points','0,5, 10,5, 5,0');
if(pageNo==totalPages){
nexttriangle.style('opacity','0.5')
nexttriangle.on('click','')
.style('cursor','');
}
else if(pageNo==1){
prevtriangle.style('opacity','0.5')
prevtriangle.on('click','')
.style('cursor','');
}
}
function prevLegend(){
pageNo--;
svg.selectAll("g.legendg").remove();
svg.select('.pageNo').remove();
svg.select('.prev').remove();
svg.select('.next').remove();
var startIndex=(pageNo-1)*legendPerPage;
var endIndex=startIndex+legendPerPage;
var seriesSubset=[],colorSubset=[];
for(var i=0;i<data.series.length;i++){
if(i>=startIndex && i<endIndex){
seriesSubset.push(data.series[i]);
colorSubset.push(colors[i]);
}
}
DrawLegendSubset(seriesSubset,colorSubset,legendPerPage,pageNo,totalPages);
}
function nextLegend(){
pageNo++;
svg.selectAll("g.legendg").remove();
svg.select('.pageNo').remove();
svg.select('.prev').remove();
svg.select('.next').remove();
var startIndex=(pageNo-1)*legendPerPage;
var endIndex=startIndex+legendPerPage;
var seriesSubset=[],colorSubset=[];
for(var i=0;i<data.series.length;i++){
if(i>=startIndex && i<endIndex){
seriesSubset.push(data.series[i]);
colorSubset.push(colors[i]);
}
}
DrawLegendSubset(seriesSubset,colorSubset,legendPerPage,pageNo,totalPages);
}