Format for D3's data() binding - d3.js

What is the required format for things passed to d3's .data()?
In this jsfiddle, I try to create several <div> elements for each metric. Unfortunately, nothing happens. I'm assuming this is related to an incorrect data structure?
http://jsfiddle.net/GppWz/

The main issue here is that you are trying to use a hash as a data source, while d3 wants your data in array format.
If you can, modify your data source so that you are receiving data in array format. If this is not possible, you can use the d3.entries function to convert the object into an array:
var listContainers = d3.select('#lists').selectAll('div')
.data(d3.entries(data))
.enter().append('div')
.attr('class', 'listContainer');
listContainers.append('h5')
.text(function(d) {
return d.key;
});
var item = listContainers.selectAll('.item').data(function(d) {
return d.value;
}).enter()
.append('div')
.attr('class', 'item')
.text(function(d) {
return 'average_dif = ' + d.average_dif;
});
// ...

Related

Replace text value upon dataset change within an existing div using d3

I am trying to replace text inside existing divs using d3.select when a new dataset is selected, but what I've written adds new divs every time a new dataset is selected. The use case is a leaderboard that picks up the top people groups (d.Culture) that have created objects out of a certain material (Gold, Silver, Bronze, etc.).
Page in question: https://3milychu.github.io/whatmakesart/
It takes a full minute to load (if you see the bar chart, page is loaded)
I've also tried adding .remove() and .exit() unsuccessfully per other suggestions I've viewed.
For some reason, even though the targeted divs are classes, I have to use ".myDiv" instead of "#myDiv" to see the text.
function origins(dataset) {
var totalRows = dataset.length;
console.log(totalRows);
var format = d3.format(".0%");
var origins = d3.nest()
.key(function(d) { return d.Culture; })
.rollup(function(v) { return v.length; })
.entries(dataset)
.sort(function(a,b) {return d3.descending(a.value,b.value);});
console.log(origins);
var culture1 = d3.select(".culture").selectAll(".culture1")
.data(origins)
.enter()
.append("div")
.attr("id", "culture1")
.filter(function (d, i) { return i === 0;})
.text(function(d) { return d.key + " " + format(d.value/totalRows); })
<--repeat for "culture2", "culture3", etc. -->
};
The html divs I am trying to target:
<!-- Origins -->
<div id ="origins">
<h1>Origins</h1>
<div class="culture" id="culture1"></div>
<div class="culture" id="culture2"></div>
<div class="culture" id="culture3"></div>
<div class="culture" id="culture4"></div>
<div class="culture" id="culture5"></div>
<div class="culture" id="culture6"></div>
<div class="culture" id="culture7"></div>
</div>
There are a couple of things going on here that are problematic.
First, you're appending lots of divs with the same ID when you do .attr("id", "culture1")
. This can cause all sorts of unexpected problems.
The filter function doesn't work quite as you would expect. When you do .filter(function (d, i) { return i === 0;}), you're still creating empty divs for all of the elements that do not match the filter. Use the DOM inspector in chrome and you'll see you have tons of empty divs. If you're only trying to get the first element from the array, I suggest passing only that data into the data function. If origins is going to be a different size each time you pass it into the data function, then you will need to implement removing the element on exit. But if the origins array is the same size, the existing data should just update.
That should help fix up the behavior you're seeing. It's possible there are other issues, but these are definite issues that should be fixed first.
I got the following to replace the ranks upon selection change:
function origins(dataset) {
var totalRows = dataset.length;
console.log(totalRows);
var format = d3.format(".0%");
var origins = d3.nest()
.key(function(d) { return d.Culture; })
.rollup(function(v) { return v.length; })
.entries(dataset)
.sort(function(a,b) {return d3.descending(a.value,b.value);});
// console.log(origins);
d3.select(".culture").selectAll("text").remove()
var culture1 = d3.select(".culture").selectAll("#ranks")
.data(origins.filter(function (d, i) { return i === 0;}))
.enter()
.append("text")
.attr("id", "culture1")
.text(function(d) { return d.key + " " + format(d.value/totalRows); })
.exit();
<--repeat for all ranks -->
};
The html was changed to:
<div id ="origins">
<h1>Origins</h1>
<div class="culture" id="ranks"></div>
</div>
The key was to select the element types within the div, and .remove() them prior to declaring the variable and appending. Also per Jeff's suggestion, passing through the filter at .data() prevented creating extra elements.

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)

How to use CSV data in d3

What I want to do
My CSV looks like this
,,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,,,
How should I import my CSV so that I will be able to access data such as
.text(function(d) {
return d['starting point'];
});
What I've tried
I have tried nesting the data
d3.csv("data.csv", function(csv_data) {
var data = d3.nest()
.key(function(d) {
return d.Name;
})
.entries(csv_data)
console.log(data);
which in my console looks something like this:
0: Object
key: " DICKINSON"
values: Array[2]
0: Object
Assist: "n"
First names: "Conrad"
Name: " DICKINSON"
Nat: "UK"
starting point: "Hercules Inlet"
1:Object
2:Object
....
However I am only able to return d.key
.text(function(d) {
return d.key;
});
I have asked this question before with code examples but I think my question was too confusing. If anyone could help me get to my data I would be really grateful!
Don't nest it.
d3.csv("data.csv", function(csv_data) {
select("body")
.append("div")
.selectAll('p')
.data(csv_data)
.enter()
.append('p')
.text(function(d) { d.Name + ', ' + d.location...})
});

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>

D3 - dealing with JSON nested data structures

My post is somehow a follow-up to this question : D3 - how to deal with JSON data structures?
In short, I'm in trouble with handling complex nested JSON structures.
Let me give a simple sample (that mirrors the above link) to illustrate:
var regularData = [[40,20,30,24,18,40],
[24,20,30,41,12,34]];
var myTable = d3.select("body").append("table")
.selectAll("tr")
.data(regularData, function(d) {
return d;
})
.enter()
.append("tr")
.selectAll("td")
.data(function(d) {
return d;
})
.enter()
.append("td")
.text(function(d) {
return d;
});
As already shown through D3's doc, this produces two lines of numbers, as expected.
But if I replace regularData by this:
var newData = [{"user":"jim","scores":[40,20,30,24,18,40]},
{"user":"ray","scores":[24,20,30,41,12,34]}];
and adapt the myTable's construction lines 3 to 5 with this:
.data(newData, function(d) {
return d.scores;
})
I would have expected to loop in the scores for each user. Instead, the second data clause is still bound to top-level objects (i.e. with properties "user" and "scores").
A brute-force approach would be to cast my data in a plain "array of arrays" adapted to each purpose. But maybe I missed something, and a more elegant way is possible ?
Thanks by advance for your help,
P.
You have slightly misunderstood what the second argument to d3.data is meant for.
That argument is used for object constancy by providing an id for the data and not as an accessor function, as in most other functions in the d3 API.
However, when a function is passed as the first argument, as is here:
.data(function(d) {
return d;
})
then it does behave as an accessor function.
In your case, what you want is something along these lines:
var newData = [{"user":"jim","scores":[40,20,30,24,18,40]},
{"user":"ray","scores":[24,20,30,41,12,34]}];
var myTable = d3.select("body").append("table")
.selectAll("tr")
.data(regularData, function(d) {
return d.user;
})
.enter()
.append("tr")
.selectAll("td")
.data(function(d) {
return d.scores;
})
.enter()
.append("td")
.text(function(d) {
return d;
});
I have used the user field as the id of the data and have returned the scores in the second call to data to create the td elements.

Resources