I'm trying to learn D3 by book and examples. One example I'm working through is a simple (multi) line chart located here http://bl.ocks.org/mbostock/3884955#index.html .
I can follow along for the most part but I can't make sense of this:
y.domain([
d3.min(cities, function(c) { return d3.min(c.values, function(v) { return v.temperature; }); }),
d3.max(cities, function(c) { return d3.max(c.values, function(v) { return v.temperature; }); })
]);
When I was trying to write the code on my own, using the example as a cheat sheet, I came up with this
y.domain([0, d3.max(data, function(d) { return d.temperature; })]);
because I wanted the y range to span from 0 to the max of all temperatures.
I believe I have two questions here:
1) is the nested mins and maxs because it's looking at the max of each array within the array?
2) am I correct thinking that 'cities' is the entire array and values is the array of temperatures within 'cities'?
Apologies if this question isn't very focused. I believe I want to figure out how to find the maximum of an array of arrays.
Is the nested mins and maxs because it's looking at the max of each array within the array?
Yes! you are right the cities json is an array which has another array in it with key values the idea here is t find the min temperature in this nested array
d3.min(cities, function(c) { return d3.min(c.values, function(v) { return v.temperature; }); }),
am I correct thinking that 'cities' is the entire array and values is the array of temperatures within 'cities'?
Yes you are correct again copy this json below in a json formatter you will be able to understand the JSON better:
cities = [
{
"name":"New York",
"values":[
{
"date":"2011-09-30T18:30:00.000Z",
"temperature":63.4
},
{
"date":"2011-10-01T18:30:00.000Z",
"temperature":58
},
{
"date":"2011-10-02T18:30:00.000Z",
"temperature":53.3
},
{
"date":"2011-10-03T18:30:00.000Z",
"temperature":55.7
},
{
"date":"2011-10-04T18:30:00.000Z",
"temperature":64.2
}
]
},
{
"name":"San Francisco",
"values":[
{
"date":"2011-09-30T18:30:00.000Z",
"temperature":62.7
},
{
"date":"2011-10-01T18:30:00.000Z",
"temperature":59.9
},
{
"date":"2011-10-02T18:30:00.000Z",
"temperature":59.1
},
{
"date":"2011-10-03T18:30:00.000Z",
"temperature":58.8
}
]
},
{
"name":"Austin",
"values":[
{
"date":"2011-09-30T18:30:00.000Z",
"temperature":72.2
},
{
"date":"2011-10-01T18:30:00.000Z",
"temperature":67.7
},
{
"date":"2011-10-02T18:30:00.000Z",
"temperature":69.4
}
]
}
]
Hope this helps!
Related
I want to convert the following nested object into hierarchical data structure
{
"AP":{
"districts":{
"Anantapur":{
"total":{
"confirmed":66593,
"deceased":587,
"recovered":65697
}
},
"Chittoor":{
...
}
}
},
"AR":{
"districts":{....}
}...so on
}
to
[
{
"name":"AP",
"children":[
{
"name":"Anantapur",
"children":[
{
"name":"confirmed",
"value":66593
},
{
"name":"deceased",
"value":587
},
{
"name":"recovered",
"value":65697
}
]
},
{
...
}
]
},...so on
]
How can this be done?...I tried using d3 nest but there is no common key value like
"state":"AP", "state":"AR".
Here "AP" and "AR" are keys themselves. Is there any other method of doing this?
You can use Object.keys(data).map(function(key) { to create an array with an item for every property in your objects. You can nest this approach to get to the desired outcome. Here's a manual solution for your data structure (you could use a recursive function for arbitrarily nested data with a slightly different output, but given the depth of the object and the structure of the output, I have not done that):
var data = { "State One":{
"districts":{
"Region A":{
"total":{
"confirmed":1,
"deceased":2,
"recovered":3
}
},
"Region B":{
"total":{
"confirmed":4,
"deceased":5,
"recovered":6
}
}
}
},
"State Two":{
"districts":{
"Region C":{
"total":{
"confirmed":7,
"deceased":8,
"recovered":9
}
}
}
}
}
var output = Object.keys(data).map(function(key) {
return {
name:key,
children: Object.keys(data[key]["districts"]).map(function(district) {
return {
name:district,
children: Object.keys(data[key]["districts"][district]["total"]).map(function(d) {
return { name: d, value:data[key]["districts"][district]["total"][d] }
})
}
})
}
})
console.log(output);
Let's say I have a 100 years worth of monthly data, total of 1200 data points, see bottom.
To plot a tiny overview line chart (e.g. just 100 data points) I have to do it manually by grouping. For instance, group the data by year, then get the average of 12 months value, iterate through every group, then finally reduced the data points to 100.
Instead of this approach, is there a convenient way using crossfilter or any other library?
[
{ date: 1900-01, value: 72000000000},
{ date: 1900-02, value: 58000000000},
{ date: 1900-03, value: 2900000000},
{ date: 1900-04, value: 31000000000},
{ date: 1900-05, value: 33000000000},
...
{ date: 1999-11, value: 30000000000},
{ date: 1999-12, value: 10000000000},
]
It's going to be the same algorithm no matter which library you use, just different ways of specifying it. In this case d3.nest is probably the easiest way to do this, but if you want quick filtering, the crossfilter way isn't too bad.
The difference between using d3.nest and crossfilter is that we're not constructing an array of values, just a single value. So we'll maintain both sum and count.
We'll also need to specify what happens when a row is removed from a bin.
var parse = d3.timeParse("%Y-%m");
data.forEach(function(d) {
// it's best to convert fields before passing to crossfilter
// because crossfilter will look at them many times
d.date = parse(d.key);
});
var cf = crossfilter(data);
var yearDim = cf.dimension(d => d3.timeYear(d.date));
var yearAvgGroup = yearDim.group().reduce(
function(p, v) { // add
p.sum += v.value;
++p.count;
p.avg = p.sum/p.count;
return p;
},
function(p, v) { // remove
p.sum -= v.value;
--p.count;
p.avg = p.count ? p.sum/p.count : 0;
return p;
},
function() { // init
return {sum: 0, count: 0, avg: 0};
}
);
Now yearAvgGroup.all() will return an array of key/value pairs, where the key is the year, and the value contains sum, count, and avg.
Crossfilter doesn't make this problem particularly convenient to solve, but reductio has a helper function for this:
var yearAvgGroup = yearDim.group();
reductio().avg(d => d.value);
Note: it doesn't matter unless you have ton of data, but it's more efficient to only compute sum and count in the group, and compute the average when it's needed.
If you're using dc.js, you can use valueAccessor for this:
// remove avg lines from the above, and
chart.dimension(yearDim)
.group(yearAvgGroup)
.valueAccessor(kv => kv.value.sum / kv.value.count);
Assuming your question is only concerned with producing the data, you can use d3-nest, without crossfilter, to average each year:
Parsing the date value, you can then format the date as a year to create a key. This groups values by key, then we rollup those values with a function to calculate the mean for a given year:
var parse = d3.timeParse("%Y-%m"); // takes: "1900-01"
var format = d3.timeFormat("%Y"); // gives: "1900"
var means = d3.nest()
.key(function(d) { return format(parse(d.date)); })
.rollup(function(values) { return d3.mean(values, function(d) {return d.value; }) })
.entries(data);
Which gives us the following structure:
[
{
"key": "1900",
"value": 39380000000
},
{
"key": "1999",
"value": 20000000000
}
]
var data = [
{ date: "1900-01", value: 72000000000},
{ date: "1900-02", value: 58000000000},
{ date: "1900-03", value: 2900000000},
{ date: "1900-04", value: 31000000000},
{ date: "1900-05", value: 33000000000},
{ date: "1999-11", value: 30000000000},
{ date: "1999-12", value: 10000000000},
];
var parse = d3.timeParse("%Y-%m");
var format = d3.timeFormat("%Y");
var means = d3.nest()
.key(function(d) { return format(parse(d.date)); })
.rollup(function(values) { return d3.mean(values, function(d) {return d.value; }) })
.entries(data);
console.log(means);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I have a group with custom reducer calculating various total and average values. The goal is to show them all on the same barChart. But I can only get the first bar to show. Here is the JSFiddler
https://jsfiddle.net/71k0guxe/15/
Is it possible to show all the value on the barChart?
Thanks in advance!
Data
ID,SurveySent,ResponseReceived
1,Yes,No
2,No,No
3,Yes,Yes
4,No,No
5,Yes,Yes
6,No,No
7,Yes,No
8,No,No
9,Yes,No
10,No,No
Code
var chart = dc.barChart("#test");
//d3.csv("morley.csv", function(error, experiments) {
var experiments = d3.csvParse(d3.select('pre#data').text());
var ndx = crossfilter(experiments),
dimStat = ndx.dimension(function(d) {return "Statistics";}),
groupStat = dimStat.group().reduce(reduceAdd, reduceRemove, reduceInitial);
function reduceAdd(p, v) {
++p.count;
if (v.SurveySent === "Yes") p.sent++;
if (v.ResponseReceived === "Yes") p.received++;
return p;
}
function reduceRemove(p, v) {
--p.count;
if (v.SurveySent === "Yes") p.sent--;
if (v.ResponseReceived === "Yes") p.received--;
return p;
}
function reduceInitial() {
return {count: 0, sent: 0, received: 0};
}
chart
.width(400)
.height(400)
.xUnits(dc.units.ordinal)
.label(function(d) { return d.data.value })
.elasticY(true)
.x(d3.scaleOrdinal().domain(["Total", "Sent", "Received"]))
.brushOn(false)
.yAxisLabel("This is the Y Axis!")
.dimension(dimStat)
.group(groupStat)
.valueAccessor(function (d) {
//Is it possible to return count sent and received all from here?
return d.value.count;
})
.on('renderlet', function(chart) {
chart.selectAll('rect').on("click", function(d) {
console.log("click!", d);
});
});
chart.render();
Just got some idea from the FAQ section of dc.js/wiki/FAQ
Fake Groups
"dc.js uses a very limited part of the crossfilter API - in fact, it really only uses dimension.filter() and group.all()."
I don't care about filtering, so i just need to mark up my own group.all. Basically transpose it from one row to multiple row. Works my purpose.
/* solution */
var groupStatTranposed = group_transpose(groupStat);
function group_transpose(source_group, f) {
return {
all:function () {
return [
{key: "Total", value: source_group.all()[0].value.count},
{key: "Sent", value: source_group.all()[0].value.sent},
{key: "Received", value: source_group.all()[0].value.received}
];
}
};
}
//use groupStatTranposed in the chart.
/** solution */
In the example below, I am trying to sum by unique occurence of Respond_Id. eg. in this case, it should be in total 3, "Respond_Id" being 258,261 and 345.
This is my data:
{"Respond_Id":258,"Gender":"Female","Age":"18-21","Answer":424},
{"Respond_Id":258,"Gender":"Female","Age":"18-21","Answer":428},
{"Respond_Id":261,"Gender":"Male","Age":"22-26", "Answer":427},
{"Respond_Id":261,"Gender":"Male","Age":"22-26", "Answer":432},
{"Respond_Id":345,"Gender":"Female","Age":"27-30","Answer":424},
{"Respond_Id":345,"Gender":"Female","Age":"27-30","Answer":425},
{"Respond_Id":345,"Gender":"Female","Age":"27-30","Answer":433},
I know I should use group reduce for this, so I tried (adapted from an example):
var ntotal = answerDim.group().reduce(
function(p, d) {
if(d.Respond_Id in p.Respond_Ids){
p.Respond_Ids[d.Respond_Id]++;
}
else {
p.Respond_Ids[d.Respond_Id] = 1;
p.RespondCount++;
}
return p;
},
function (p, d) {
p.Respond_Ids[d.Respond_Id]--;
if(p.Respond_Ids[d.Respond_Id] === 0){
delete p.Respond_Ids[d.Respond_Id];
p.RespondCount--;
}
return p;
},
function () {
return {
RespondCount: 0,
Respond_Ids: {}
};
}
);
Then:
numberDisplay
.group(ntotal)
.valueAccessor(function(d){ return d.value.RespondCount; });
dc.renderAll();
But seems not working. Does someone know how to make it work ? Thank you
Based on your JSFiddle, your setup is like this:
var RespondDim = ndx.dimension(function (d) { return d.Respond_Id;});
var ntotal = RespondDim.group().reduce(
function(p, d) {
if(d.Respond_Id in p.Respond_Ids){
p.Respond_Ids[d.Respond_Id]++;
}
else {
p.Respond_Ids[d.Respond_Id] = 1;
p.RespondCount++;
}
return p;
},
function (p, d) {
p.Respond_Ids[d.Respond_Id]--;
if(p.Respond_Ids[d.Respond_Id] === 0){
delete p.Respond_Ids[d.Respond_Id];
p.RespondCount--;
}
return p;
},
function () {
return {
RespondCount: 0,
Respond_Ids: {}
};
});
What is important to note here is that your group keys, by default, are the same as your dimension keys. So you will have one group per respondent ID. This isn't what you want.
You could switch to using dimension.groupAll, which is designed for this use case, but unfortunately the dimension.groupAll.reduce signature is slightly different. The easiest fix for you is going to be to just define your dimension to have a single value:
var RespondDim = ndx.dimension(function (d) { return true;});
Now you'll see that ntotal.all() will look like this:
{key: true, value: {RespondCount: 3, Respond_Ids: {258: 2, 261: 2, 345: 3}}}
Working fiddle: https://jsfiddle.net/v0rdoyrt/2/
I have the following document structure:
{
"_id": "car_1234",
"_rev": "1-9464f5d70547c255a423ff8dae653db1",
"Tags": [
"Audi",
"A4",
"black"
],
"Car Brand": "Audi",
"Model": "A4",
"Color": "black",
"CarDealerID": "5"
}
The Tags field stores the information of the document in a list. This structure needs to stay like this. Now the user has the opportunity to search for cars in a HTML text input field, where a , represents a separation between cars. Let's take the following example:
black Audi, pink Audi A4
Here the user wants to find a black Audi or a pink Audi A4. My approach of querying through the database is by splitting the entered words to the following structure [["black", "Audi"],["pink", "Audi", "A4"]] and to search inside the Tags field of each document in the db if all the words in a subarray (e.g. "black" and "Audi") are existent and to return the CarDealerID.
///Before this I return the word list as described
}).then(function (wordList) {
results = [];
for (var i = 0; i < userWords.length; i++) {
//Check if the object is a single word or an array of words
if (wordList[i].constructor === Array) {
//Recreate the words in the array as one string
wordString = ""
wordList[i].forEach(function (part) {
wordString += part + " "
})
wordString = wordString.trim()
//Search for the car
car_db.search({
query: wordString,
fields: ["Tags"],
include_docs: true
}).then(function(result) {
result.rows.forEach(function (row) {
results.push(row.doc.CarDealerID)
})
})
} else {
car_db.search({
query: userWords[i],
fields: ["Tags"],
include_docs: true
}).then(function(result) {
result.rows.forEach(function (row) {
results.push(row.doc.CarDealerID)
})
})
}
return results
}).then(function(results) {
console.log(results)
}).catch(function (err) {
console.log(err)
});
My Problem
My problem is now that the results are returned before the for loop finishes. This is probably because it is an async procedure and the result should wait to be returned until this async is finished. But I don't know how to achieve that. I hope someone can help me out.
Thanks to Nolan Lawson's Blog (Rookie Mistake #2) I could figure it out. Instead of the for loop, I use
return Promise.all(wordList.map(function (i) {
results = [];
//
//Same Code as before
//
//Return results inside the map function
return results;
}));