Accessing nested data in D3 - d3.js

I'm having trouble understanding when and how to use nested data.
In this example I have a CSV with names ('Name') and locations ('starting point'). By assigning keys to the locations I am able to make a dropdown containing them all, and I would like to use this to filter the names associated with each location.
However I am unable to find the data's values, in this case 'd.Name'
Here inside the update function I have tried to access the 'values' on the data join.
var adventurer = canvas
.selectAll(".adventurer")
.data(function(d) {
return d.values;
})
Ive also tried creating an extra data variable but thats not working for me either.
Sorry I can't make a jsfiddle but here is a plunk
DATA
,,Name,First names,s,r,Nat,born,starting point,starting date,arrival date,days,km,Assist,Support,Style,note,arrival date 2
1,1,KAGGE,Erling,,,Nor,1/15/1963,Berkner Island,11/18/1992,1/7/1993,50,appr. 1300,n,n,solo,first solo unassisted,
2,2,ARNESEN,Liv,f,,Nor,6/1/1953,Hercules Inlet,11/4/1994,12/24/1994,50,1130,n,n,solo,first woman unassisted,
3,3,HAUGE,Odd Harald,,,Nor,1956,Berkner Island,11/4/1994,12/27/1994,54,appr. 1300,n,n,,,
HTML
<div id="menu"></div>
<div id="chart"></div>
SCRIPT
d3.csv("data.csv", function(csv_data) {
var data = d3.nest()
.key(function(d) {
return d['starting point'];})
.sortKeys(d3.ascending)
.entries(csv_data)
console.log(data);
//create dropdown select
var list = d3.select("#menu").append("select")
list.selectAll("option")
.data(data)
.enter().append("option")
.attr("value", function(d) {
return d.key;
})
.text(function(d) {
return d.key;
});
//chart config
var w = 375,
h = 1000;
var canvas = d3.select('#chart')
.append('svg')
.attr('width', w)
.attr('height', h)
.append('g')
.attr('transform', 'translate (0,50)');
//function (bind, add, remove, update)
function updateLegend(data) {
var adventurer = canvas
.selectAll(".adventurer")
.data(function(d) {
return d.values;
})
var adventurerEnter = adventurer
.enter().append("g")
.attr('class', 'adventurer');
adventurerEnter
.append("text")
.attr('class', 'name')
.attr('x', 0);
adventurer.select('.name')
.text(function(d, i) {
return d.Name;
})
.attr('y', function(d, i) {
return i * 30;
});
// remove old elements
adventurer.exit().remove();
};
// generate initial legend
updateLegend(data);
});
// handle on click event
d3.select('#menu')
.on('change', function() {
var data = eval(d3.select(this).property('value'));
console.log(data)
updateLegend(data);
});

You need to display both the locations and the names. You have a nest (in the plunk but not in your question) of location/name, but you also need a distinct list of names, or possibly a list of name/location:
var locations = d3.nest()
.key(function(d) {return d['starting point'];})
.sortKeys(function(a,b){ return a > b && 1 || b > a && -1 || 0})
.key(function(d) {return d.Name;})
.entries(csv_data)
var names = d3.nest()
.key(function(d) {return d.Name;})
.sortKeys(function(a,b){ return a > b && 1 || b > a && -1 || 0})
.key(function(d) {return d['starting point'];})
.entries(csv_data)
Then you have to display your names however you want. Then you need an .on('change', function()...) handler (or on click or whatever fits your needs) that actually filters the names wherever those are displayed.
I also fixed your sorting. d3.ascending is for numbers, not strings.

Related

How can i get the key of an element in stack-layout in d3?

I want to add a toolkit that show the type of the disaster, which is the key of the stack datum, how can i get it?
The format of .csv file is like this: (Forgive me can not take pictures)
AllNaturalDisasters,Drought,Earthquake,ExtremeTemperature,ExtremeWeather,Flood,Impact,Landslide,MassMovementDry,VolcanicActivity,Wildfire,Year
5,2,null,null,1,1,null,null,null,1,null,1900
2,null,2,null,null,null,null,null,null,null,null,1901
Here I create a stack
var stack = d3.stack()
.keys(["Drought", "Earthquake", "ExtremeTemperature", "ExtremeWeather", "Flood", "Impact", "Landslide", "MassMovementDry", "VolcanicActivity", "Wildfire"]);
and then I pass it my data:var series = stack(dataset);. dataset is the all data from the csv file. Then I create a chart using stack-layout, like this:
var groups = svg.selectAll("g")
.data(series)
.enter()
.append("g")
.style("fill", function(d, i) {
return colors(i);
});
var rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("height", function(d) {
return yScale(d[0]) - yScale(d[1]);
})
.attr("width", xScale.bandwidth())
.append("title")
.text(function (d) {
return d.data.Year;
});
The problem is right here:
.append("title")
.text(function (d) {
return d.data.Year;
});
I want to add a toolkit to show the type of the disaster, which is the key of this datum in series , how can I get it instead of the year?!
Each rectangle contains information on the column (year of disaster), but each g has information on the "row" (type of disaster).
The stack produces a nested array, the parent level (which we use to create the g elements) contains the key, or type of disaster
The child level represents the columns, which contains the year.
The grandchild level just contains individual rectangles.
So, we can get a key by selecting the parent g:
.append("title")
.text(function() {
var rect = this.parentNode; // the rectangle, parent of the title
var g = rect.parentNode; // the g, parent of the rect.
return d3.select(g).datum().key; // now we get the key.
})
Of course this could be simplified a bit, but I broke it out to comment it better.
This allows for more flexible sorting - rather than relying on fixed indexes.
Here it is using your data:
var csv = d3.csvParse(d3.select("pre").text());
var stack = d3.stack().keys(["Drought", "Earthquake", "ExtremeTemperature", "ExtremeWeather", "Flood", "Impact", "Landslide", "MassMovementDry", "VolcanicActivity", "Wildfire"]);
var series = stack(csv);
var colors = d3.scaleOrdinal()
.range(d3.schemeCategory10);
var xScale = d3.scaleBand()
.domain([0,1])
.range([0,300])
var yScale = d3.scaleLinear()
.domain([0,6])
.range([200,0]);
var svg = d3.select("svg");
var groups = svg.selectAll("g")
.data(series)
.enter()
.append("g")
.style("fill", function(d, i) {
return colors(i);
});
var rects = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("height", function(d) {
return yScale(d[0]) - yScale(d[1]);
})
.attr("width", xScale.bandwidth())
.append("title")
.text(function (d) {
var rect = this.parentNode;
var g = rect.parentNode;
return d3.select(g).datum().key;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="300"></svg>
<pre>AllNaturalDisasters,Drought,Earthquake,ExtremeTemperature,ExtremeWeather,Flood,Impact,Landslide,MassMovementDry,VolcanicActivity,Wildfire,Year
5,2,0,0,1,1,0,0,0,1,0,1900
2,0,2,0,0,0,0,0,0,0,0,1901</pre>
Well, I have fixed this problem by a very 'low' method. I have created a simple function:
function getKeys(d) {
return series[parseInt(groups.selectAll("rect").data().indexOf(d) / series[0].length)].key;
}
Well, it so simple and crude, and I still want to know a more efficient method!!!

Conditionally change background image of D3 circles

I am trying to conditionally change the background of some circles created with D3 but I am unable to get the if else logic to correctly pick out the correct picture. The code goes right to the final else statement for the default.gif. I don't see any errors in the console. All the images are in the same directory as the html file.
var diameter = 500, //max size of the bubbles
format = d3.format(",d"),
color = d3.scaleOrdinal(d3.schemeCategory20c); //color category
var bubble = d3.pack()
.size([diameter, diameter])
.padding(1.5);
var svg = d3.select("body")
.append("svg")
.attr("width", diameter)
.attr("height", diameter)
.attr("class", "bubble");
d3.csv("fruit.csv", function(error, data){
if (error) throw error;
//convert numerical values from strings to numbers
data = data.map(function(d){ d.value = +d["Amount"]; return d; });
//Sets up a hierarchy of data object
var root = d3.hierarchy({children:data})
.sum(function(d) { return d.value; })
.sort(function(a, b) { return b.value - a.value; });
//Once we have hierarchal data, run bubble generator
bubble(root);
//setup the chart
var bubbles = svg.selectAll(".bubble")
.data(root.children)
.enter();
//create the bubbles
bubbles.append("circle")
.attr("class", "circle")
.attr("r", function(d){ return d.r; })
.attr("cx", function(d){ return d.x; })
.attr("cy", function(d){ return d.y; })
.append("defs")
.append("pattern")
.append("image")
.attr("xlink:href", function(d) {
if ( d.Fruit == "Apple") {
return "apple.jpg";
}
else if (d.Fruit == "Pear") {
return "pear.jpg"
}
else if (d.Fruit == "Banana") {
return "banana.jpg";
}
else if (d.Fruit == "Strawberry") {
return "strawberry.jpg";
}
else if (d.Fruit == "Grapes") {
return "grapes.jpg";
}
else { return "default.gif"; }
});
//.style("fill", function(d) { return color(d.value); });
//format the text for each bubble
bubbles.append("text")
.attr("x", function(d){ return d.x; })
.attr("y", function(d){ return d.y + 5; })
.attr("text-anchor", "middle")
.text(function(d){ return d.data["Fruit"]; })
.style("fill","white")
.style("font-family", "Helvetica Neue, Helvetica, Arial, san-serif")
.style("font-size", "12px");
});
The csv file contains the following data:
Fruit,Amount
Apple,32
Pear,13
Banana,25
Grapes,29
Strawberry,36
As I said in my comment, I'm surprised that you're seeing anything as the circles' background, since your code for creating/referencing the patterns is not correct. For a quick explanation, check my answer here.
However, I'll address only your main question here, which is the if statement.
The problem is that you're using d3.hierarchy to create your data array. Because of that, all data are inside node.data. According the API:
The returned node and each descendant has the following properties:
node.data - the associated data, as specified to the constructor.
Therefore, for your conditionals, instead of:
d.Fruit == "Apple"
It should be:
d.data.Fruit == "Apple"
And the same for d.data.Fruit == "Pear", d.data.Fruit == "Banana" etc...

d3 treemap graphic don't recognize enter() and exit() data correctly

I'm trying to create dynamic treemap graphic with lovely d3.js library.
Here are sample of my code
element = d3.select(element[0]);
var margin = 10,
width = parseInt(element.style('width')) - margin * 2,
height = parseInt(element.style('height')) - margin * 2;
var color = d3.scale.category10();
var canvas = d3.select('.treemap').append('svg')
.attr('width', width)
.attr('height', height)
.attr('transform', 'translate(-.5,-.5)')
.style('margin', margin);
var treemap = d3.layout.treemap()
.size([width, height])
.value(function(d) { return d.value; })
.sticky(false);
function redraw(data) {
d3.selectAll('.cell').remove();
var treemapData = {};
treemapData.children = data.map(function(d) {
return {
name: d.name,
value: d.value
};
});
var leaves = treemap(treemapData);
var cells = canvas.selectAll("g")
.data(leaves);
var cellsEnter = cells.enter()
.append('rect')
.attr('class', 'cell')
.attr('x', function(d) { return d.x; })
.attr('y', function(d) { return d.y; })
.attr('width', function(d) { return d.dx; })
.attr('height', function(d) { return d.dy; })
.attr('fill', function(d) { return d.children ? null : color(d.name); })
.attr('stroke', "#fff")
.style('fill-opacity', 0);
console.log(cells.exit(), cells.enter());
}
And here I have stucked.
console.log() shows that whole new data are enter(), and none are exit() !
Input data presents like
[{value: 590, name:"A1"}, {...}, ...]
without root object field, so that's why I remapped data in treemapData object.
Š¢hanks that you at least spent your time for reading this post so far, hope you have any suggestions.
UDP. you can check working version of my code here: https://jsfiddle.net/qtbfm08k/
The following works:
remove d3.selectAll('.cell').remove();
use the code below
See the fiddle: https://jsfiddle.net/b6meLedn/4/
var cells = canvas.selectAll('.cell') //select all cells
.data(leaves); //map the data
cells.exit().remove(); //remove old extra elements
cells.enter()
.append('rect') //create new rectangles as necessary
.attr('class', 'cell')
cells //take all cells (old cells that have new data+extra new cells)
.attr('x', function(d) { return d.x; })
...

D3 How to filter menu based on nested data

I have the following plunk
https://plnkr.co/edit/gDbUE99nNNMtYp9IpR1d?p=info and am struggling to get my dropdown menu working with nested data. Ive generated a menu from a first level nest ('starting point'), which shows a list of nationalities that is generated from the second level nest ('Nat'). I want to use the menu to load the different 'Nat' assosicated with each 'starting point'.
Previously I created my menu like this, but it doesn't seem to be working with 'nested_data'. Is d3.filter() wrong for this?
list.on('change', function() {
var selected = d3.select(this).select("select").property("value")
var cd = nested_data.filter(function(d) {
return (selected == d.key)
});
updateNested_data(cd)
});
...
var filter = nested_data.filter(function(d) {
return ("Berkner Island" == d.key)
});
updateNested_data(filter)
I'm also having a problem with exit() in my update function, I think because I'm binding my data too early? But for this question I'd like to focus on how to get the menu working.
Full code
d3.csv("data.csv", function(CSV) {
var nested_data = d3.nest()
.key(function(d) {
return d['starting point'];
})
.key(function(d) {
return d.Nat;
})
.entries(CSV);
// CREATE DROPDOWN
var list = d3.select("#opts")
list.append("select").selectAll("option")
.data(nested_data)
.enter().append("option")
.attr("value", function(d) {
return d.key;
})
.text(function(d) {
return d.key;
});
// MENU
list.on('change', function() {
var selected = d3.select(this).select("select").property("value")
var cd = nested_data.filter(function(d) {
return (selected == d.key)
});
updateNested_data(cd)
});
// CONFIG
var canvas = d3.select('#explorers').data(nested_data)
// BIND, ENTER, REMOVE
function updateNested_data(nested_data) {
var country = canvas.selectAll(".country")
var countryEnter = country
.data(function(d) {
return d.values
})
.enter().append('div')
.attr('class', 'country')
countryEnter
.append("p")
.attr('class', 'label')
.style('font-weight', 'bold')
.text(function(d) {
return d.key;
})
country.exit().remove();
};
//FILTER
var filter = nested_data.filter(function(d) {
return ("Berkner Island" == d.key)
});
// UPDATE
updateNested_data(filter)
});
There is no problem anywhere except the way you entering and exiting:
function updateNested_data(cd) {
//set the data to the selection
var country = canvas.selectAll(".country").data(cd.values)
var countryEnter = country
.enter().append('div')
.attr('class', 'country')
countryEnter
.append("p")
.attr('class', 'label')
.style('font-weight', 'bold')
.text(function(d) {
return d.key;
})
//exit on selection
country.exit().remove();
};
working code here

Is it possible to add labeled points to a dc.js line chart?

I like dcjs, http://bl.ocks.org/d3noob/6584483 but the problem is I see no labels anywhere for the line chart (Events Per Hour). Is it possible to add a label that shows up just above the data point, or even better, within a circular dot at the tip of each data point?
I attempted to apply the concepts in the pull request and came up with:
function getLayers(chart){
var chartBody = chart.chartBodyG();
var layersList = chartBody.selectAll('g.label-list');
if (layersList.empty()) {
layersList = chartBody.append('g').attr('class', 'label-list');
}
var layers = layersList.data(chart.data());
return layers;
}
function addDataLabelToLineChart(chart){
var LABEL_FONTSIZE = 50;
var LABEL_PADDING = -19;
var layers = getLayers(chart);
layers.each(function (d, layerIndex) {
var layer = d3.select(this);
var labels = layer.selectAll('text.lineLabel')
.data(d.values, dc.pluck('x'));
labels.enter()
.append('text')
.attr('class', 'lineLabel')
.attr('text-anchor', 'middle')
.attr('x', function (d) {
return dc.utils.safeNumber(chart.x()(d.x));
})
.attr('y', function (d) {
var y = chart.y()(d.y + d.y0) - LABEL_PADDING;
return dc.utils.safeNumber(y);
})
.attr('fill', 'white')
.style('font-size', LABEL_FONTSIZE + "px")
.text(function (d) {
return chart.label()(d);
});
dc.transition(labels.exit(), chart.transitionDuration())
.attr('height', 0)
.remove();
});
}
I changed the "layers" to be a new group rather than using the existing "stack-list" group so that it would be added after the data points and therefore render on top of them.
Here is a fiddle of this hack: https://jsfiddle.net/bsx0vmok/

Resources