HTML table not updating when data changed - d3.js

I'm building a chart with an accompanying table. The data for both comes from html input elements with data provided by the user. For now I'm just working on getting the table to update when the user clicks the submit button.
The first time the user clicks the submit button, the table displays correctly, however, on subsequent clicks the table is not updated, even though the underlying data is correctly updated (the global variable cashflows contains the user-entered data).
The code is shown below, but I also have a jsfiddle: http://jsfiddle.net/cyclical/xcGSu/5/
(selecting the Load Sample Data button will populate some sample data).
If the table updates correctly on the first click, doesn't that indicate that the data is bound correctly to the DOM?
Thanks,
Neil
var dollars = d3.format(",.2f");
function sampleData() {
d3.select("#cf1")[0][0].value=50000;
d3.select("#dt1")[0][0].value = "2007-05-10";
d3.select("#cf2")[0][0].value = 20000;
d3.select("#dt2")[0][0].value = "2011-01-11";
d3.select("#cf3")[0][0].value = 50000;
d3.select("#dt3")[0][0].value = "2012-07-19";
d3.select("#cf4")[0][0].value = 40000;
d3.select("#dt4")[0][0].value = "2012-08-03";
d3.select("#endMV")[0][0].value = 190551.29 ;
d3.select("#endDate")[0][0].value = "2013-09-30";
}
d3.select("#sample")
.on("click", sampleData);
var cashflows = [];
var total = 0;
var irr = 0;
// bind the cashflows array to a table for display
var column_titles = ['Date','Cashflow','Days','IRR Cashflow'];
var columns = ['date','cf','days','irr_cashflow'];
d3.select("#results").selectAll('table').data([0]).enter().append('table');
var table = d3.select('table');
table.selectAll('thead').data([0]).enter().append('thead');
var thead = table.select('thead');
table.selectAll('tbody').data([0]).enter().append('tbody');
var tbody = table.select('tbody');
table.selectAll('tfoot').data([0]).enter().append('tfoot');
var tfoot = table.select('tbody');
// append the header row
thead.append("tr")
.selectAll("th")
.data(column_titles)
.enter()
.append("th")
.attr("align", function(d) {if (d=='Date') { return "left"} else { return "right"}})
.text(function(column) { return column; });
function calculateIRR(){
cashflows = [];
var cfvalues = [];
var cfdates = [];
// get cashflows
d3.selectAll("input.cashflow")[0]
.forEach(function(d,i) {
if (d.value) {cfvalues.push( 1 * d.value )};
}
)
// get dates
d3.selectAll("input.cfdate")[0]
.forEach(function(d,i) {
if (d.value) {cfdates.push( d.value)};
}
)
// get ending MV and associated date; MV is multiplied by -1
cfvalues.push(-1 * d3.select("#endMV")[0][0].value);
cfdates.push(d3.select("#endDate")[0][0].value);
// convert date strings to date objects
var dates = cfdates.map(function(d) { return new Date(d.replace(/-/g, "/"))});
// calculate the IRR; use 5% as starting value
var rate = XIRR(cfvalues ,dates, .05);
irr = rate;
var r = d3.select("#ratedisplay").selectAll("div")
.data([rate])
.enter()
.append("div")
.attr("class","rate")
.text(function(d,i) {return "IRR: " + d}); //{return "IRR:" + dollars(d) + ""});
var len = cfvalues.length;
var last_day = dates[len -1];
// construct final cashflow array for binding
for (var i = 0; i < len; i++) {
var cf_days = moment(last_day).diff(moment(dates[i]), 'days');
var irr_cashflow = FV(rate, cf_days/365 , 0, cfvalues[i],0);
total += irr_cashflow;
cashflows.push(
{'cf': cfvalues[i], 'date': dates[i], 'days' : cf_days, 'irr_cashflow': irr_cashflow}
);
};
var rows = tbody.selectAll("tr")
.data(function(d) {return cashflows} )
.enter()
.append("tr");
var cells = rows.selectAll("td")
.data(function(row) {
return columns.map(function(column) {
return {'name': column, 'value': row[column]};
});
})
.enter()
.append("td")
.attr("align",function(d) {
if (d.name == 'date') { return "left" } else {return "right"}
})
.html(function(d) {
if (d.name == 'date') {
return d3.time.format("%Y-%m-%d")(d.value);
} else {
return dollars(d.value);
}
});
rows.exit().remove();
cells.exit().remove();
}
d3.select("#submit")
.on("click", calculateIRR);

Related

Summing a column and displaying percentage in a number chart

I would like to sum the column elecVotes and then divide it by the elecVote of a state that has been clicked on so that I can show the percentage of the electoral vote that state is worth and display it in a dc.numberDisplay.
This is my data structure:
//updated
ElecVotes.csv:
state,elecVotes,
Alabama,9
Alaska,3
Arkansas,6
Arizona,11
Florida,29
Georgia,16
Iowa,6
Idaho,4
Indiana,11
Kansas,6
Kentucky,8
data.csv:
state,party,votes,winner,elecVote
Alabama,Democratic,725704,1,9
Alabama,Republican,1314431,1,9
Alabama,Libertarian,44211,1,9
Alabama,Green,20276,1,9
Alabama,Other,0,1,9
Alabama,Constitution Party,9341,1,9
Alaska,Democratic,116454,1,3
Alaska,Republican,163387,1,3
Alaska,Libertarian,18725,1,3
Alaska,Green,5735,1,3
Alaska,Constitution Party,3866,1,3
Alaska,Other,10441,1,3
Code:
d3.csv("data.csv", function (data) {
d3.json("us.json", function (json){
data.forEach(function(r) {
r.votes = +r.votes;
r.elecVote = +r.elecVote;
});
var elecVotes = d3.csv.parse("elecVotes")
var elecVotesMap = d3.map()
elecVotes.forEach(function(r){
elecVotesMap.set(r.state, +r.elecVotes)
});
// set up crossfilter on the data.
var ndx = crossfilter(data);
// set up the dimensions
var stateDim = ndx.dimension(function(d) { return d.state; });
var stateDim2 = ndx.dimension(function(d) { return d.state; });
var stateDim3 = ndx.dimension(function(d) { return d.state; });
var partyDim = ndx.dimension(function(d) { return d.party; });
var winnerDim = ndx.dimension(function(d) { return d.winner; });
var elecVotesDim = ndx.dimension(function(d) { return d.elecVote;});
var stateDim4 = ndx.dimension(function(d) { return d.state; }),
group = stateDim4.group().reduceSum(function(d) {return d.elecVote
}),
count = group.top(51);
count[0].key;
count[0].value;
// set up the groups/values
var state = stateDim.group();
var party = partyDim.group().reduceSum(function(d) { return d.votes;});
var party2 = partyDim.group().reduceSum(function(d) { return d.votes;});
var winner = stateDim2.group().reduceSum(function(d) { return d.winner; });
var elecVotes = stateDim3.group().reduceSum(function(d) { return d.elecVote; });
var group = stateDim4.group().reduceSum(function(d) { return d.votes; } )
// the 4 different charts - options are set below for each one.
var pie = dc.pieChart('#chart-pie');
var usmap = dc.geoChoroplethChart("#usmap");
var selectMenu = dc.selectMenu('#select-container');
var percentElec = dc.numberDisplay("#percentage-elec-vote");
var colorScale = d3.scale.ordinal().domain(["Democratic","Republican","Libertarian","Green","Constitution Party","Other"]) //set colour based on party
.range(["#4682B4","#B22222","#DAA520","#228B22","#80f2ee","#D3D3D3"]);
var stateColor = d3.scale.ordinal().domain(["1","2",""]).range(["#B22222","#4682B4","#B2B7B2"]); //set colour based on which party won
selectMenu
.dimension(stateDim3)
.group(state)
.onClick = function() {};
selectMenu.title(function (d){
return d.key;
})
//create pie from to show popular vote for each state
pie
.width(300)
.height(180)
.radius(80)
.dimension(stateDim2)
.group(party)
.legend(dc.legend())
.colors(colorScale)
.innerRadius(10)
.transitionDuration(500)
.filter = function() {};
//number chart to show percentage of electoral vote for each state
percentElec
.group(group)
.formatNumber(d3.format("d"))
.valueAccessor(function(d){ return elecVotesMap.get(d.key); })
//display US map
usmap
.width(900)
.height(500)
.dimension(stateDim)
.group(winner)
.colors(stateColor)
.overlayGeoJson(json.features, "name", function (d) { return d.properties.name; })
// at the end this needs to be called to actually go through and generate all the graphs on the page.
dc.renderAll();
});
});
};
So, to expand on the comments above a bit, here is a fiddle that pretty much shows how I would approach this: https://jsfiddle.net/esjewett/u9dq33v2/1/
This just loads the data from inline in the page.
var elecVotes = d3.csv.parse(document.getElementById("ElecVotes.csv").innerHTML)
var data = d3.csv.parse(document.getElementById("data.csv").innerHTML)
Set up your charts. In this example I am using a rowChart as well as the numberDisplay so that you can see how you need to approach dimension/group design for filtering.
var percentElec = dc.numberDisplay("#percentage-elec-vote");
var states = dc.rowChart("#state-votes")
Set up the Map that you're going to do the lookup of your electoral votes based on the state.
var elecVotesMap = d3.map()
elecVotes.forEach(function(r){
elecVotesMap.set(r.state, +r.elecVotes)
});
Set up your Crossfilter and dimension. Note that we are setting up 2 sets of identical dimensions and groups. Groups do not respect filters on their own dimension, so if you use the same dimension (or groups based on the same dimension) in both charts, it will not filter.
var cf = crossfilter(data)
var dim1 = cf.dimension(function(d) { return d.state; })
var grp1 = dim1.group().reduceSum(function(d) { return +d.votes })
var dim2 = cf.dimension(function(d) { return d.state; })
var grp2 = dim2.group().reduceSum(function(d) { return +d.votes })
Set up the state votes rowChart. You can click on this to restrict to just Alabama or Alaska.
states
.dimension(dim1)
.group(grp1)
Set up the numberDisplay. Note that the numberDisplay will display whatever grp2.top(1) returns. So if you select more than one state in the rowChart, it will display the state with the most votes (or whatever you have set your grp2.order() as). If you want to total everything up, wrap your group with an object with a top method that will return what you want to show, and pass the wrapper to the numberDisplay.
In numberDisplay.valueAccessor, you have access to both the key and the value of the group. Use the key (the name of the state) to look up the electoral votes for that state. That's what will display.
percentElec
.group(grp2)
.formatNumber(d3.format("d"))
.valueAccessor(function(d){ return elecVotesMap.get(d.key); })
dc.renderAll()

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

Is there a way to make a key or legend for a heat map with dc.js?

I made a heat map with dc.js and I was wondering if there is a key or a legend function for heat maps in dc.js. I have searched the internet and can't seem to find a built in way of doing it, so has anyone else tackled this problem?
I faced this same task and what I did is I made a second heat map that is one row long from the min value to the max value.
var range = maxValue - minValue;
var heatArr = [];
for (var i = 0; i < 24; i++) {
heatArr.push({
val: minValue + i / 23 * rangeValue,
index: i
});
}
var ndx = crossfilter(heatArr);
var keyHeatmap = ndx.dimension(function(d) {
return [d.index, 1];
});
var keyHeatmapGroup = keyHeatmap.group().reduceSum(function(d) {
return d.val;
});
var heatmapChart = dc.heatMap("#heatmapKey");
var heatColorMapping = function(d) {
return d3.scale.linear().domain([minValue, maxValue]).range(["blue", "red"])(d);
};
heatColorMapping.domain = function() {
return [minValue, maxValue];
};
heatmapChart.width(400)
.height(80)
.dimension(keyHeatmap)
.group(keyHeatmapGroup)
.colorAccessor(function(d) {
return d.value;
})
.keyAccessor(function(d) { return d.key[0]; })
.valueAccessor(function(d) { return d.key[1]; })
.colsLabel(function(d){
return heatArr[d].val.toFixed(0);
})
.rowsLabel(function(d) {
return "Key";
})
.transitionDuration(0)
.colors(heatColorMapping1)
.calculateColorDomain();
heatmapChart.xBorderRadius(0);
heatmapChart.yBorderRadius(0);
minValue and maxValue were found in my original heat map. My final result looked like this:

dc.js/crossfilter.js brushon filter - selected filtering not updating in other charts

Filter/Brush ranges are not working for some values in one of my charts.
The range of this chart starts at 0 and ends at 300.
brush selection for Ranges > 100 are not working and the other charts are not filtering and displaying relevant data.
It would be helpful if someone can point out possible issue.
Sample Data - Begin
ioi,analysis_date,closing_minutes,trade_time,window,price_channel,trade_quantity
No,02/28/2011,No,12:36.0,12:38:00,0.73,15,
No,02/28/2011,No,12:39.0,12:40:00,0.73,23,
No,02/28/2011,No,12:57.0,12:58:00,0.73,58,
No,02/25/2011,No,09:21.0,09:22:00,0.64,10,
No,02/25/2011,No,09:31.0,09:32:00,0.64,85,
Yes,11/30/2010,Yes,12:58.0,13:00:00,0.95,300,Long,
Yes,11/30/2010,Yes,12:58.0,13:00:00,0.95,200,Long,
END
NO or YES is start of new line
CODE BEGIN
var analysis_date_dimension;
var dimension_trade_qty;
var dim_time_of_day;
d3.csv("formatted_client_data.csv", function (error, response) {
var min_trade_quantity = 0
var max_trade_quantity = 310;
response.forEach(function (d, i) {
d.index = i;
d.analysis_date = d3.time.format("%m/%d/%Y").parse(d.analysis_date);
d.trade_time = d3.time.format("%I:%M").parse(d.trade_time.split('.')[0]);
d.date_time = getDateTime(d.analysis_date, d.trade_time);
});
function getDateTime(date, time) {
var dd = date.getDate();
var mm = date.getMonth() + 1;
var yy = date.getFullYear();
var hh = time.getHours();
var ms = time.getMinutes();
var x = yy + ',' + mm + ',' + dd + ' ' + hh + ':' + ms;
return new Date(x);
}
var responseData = crossfilter(response);
//Main Chart
analysis_date_dimension = responseData.dimension(
function (d) { return d.analysis_date; });
var day_total = analysis_date_dimension.group().reduceSum(
function (d) { return d.trade_quantity; });
//Trade Quantity Chart
dimension_trade_qty = responseData.dimension(
function (d) { return d.trade_quantity; });
var group_trade_qty = dimension_trade_qty.group().reduceCount(
function (d) { return d.trade_quantity; });
var day_chart = dc.barChart("#charts");
var trad_qty_chart = dc.barChart("#chart_trade_qty");
//Days chart
day_chart
.width(1024).height(340)
.dimension(analysis_date_dimension)
.group(day_total)
.brushOn(true)
.x(d3.time.scale().domain(d3.extent(response, function (d) { return d.analysis_date; })))
.yAxis().tickFormat(d3.format("d"));
//Trade Quantity Chart
trad_qty_chart
.width(600).height(200)
.dimension(dimension_trade_qty)
.group(group_trade_qty)
.brushOn(true)
.x(d3.scale.linear().domain([min_trade_quantity, max_trade_quantity + 10]))
.yAxis().ticks()
;
dc.renderAll();
});
CODE END
The data i had, had to be reformatted to number. d.trade_quantity = +d.trade_quantity; The brush filters are working as expected now.

Heatmap for multivalue variables

I am creating a heatmap using dc.js master API. My example is based on this. But the major difference is that the rows and columns have multivalues.
Below is the dataset in CSV format:
id,city,visitor,animals,food
1,NYC,854,"Lion,Tiger,Rabbit,Ape,Zebra,Monkey,Elephant,Horse","Apple,Banana,Chicken,Egg,Fish,Grape,Ham,Ice,Juice"
2,LAX,123,"Cat,Tiger,Rabbit,Ape,Whale,Bear,Zebra,Donkey,Goat,Turtle","Apple,Banana,Cake,Fish,Sugar,Bamboo,Leaf,Ham,Ice,Water"
3,LON,584,"Lion,Tiger,Ape,Shark,Panda,Zebra,Deer,Turtle,Bear,","Apple,Coke,Cake,Fish,Bamboo,Water,Grape,Orange"
4,TOR,704,"Cat,Rabbit,Ape,Whale,Bear,Panda,Donkey,Turtle,Cheetah","Banana,Cake,Orange,Kiwi,Sugar,Bamboo,Leaf,Goat,Ice,Juice"
5,SFO,855,"Lion,Tiger,Ape,Zebra,Monkey,Elephant,Donkey,Goat,Turtle,Cheetah","Apple,Cake,Grape,Ham,Juice,Hay"
6,DAL,654,"Salmon,Penguin,Rabbit,Ape,Whale,Goat,Tortoise","Apple,Banana,Cake,Ice,Water,Earthworm"
Animals and food each has multiple values. The rows are based on food and the columns are based on animals. I am able to create the heatmap but there is no interaction with other charts. For example, if I can click on a box, there is no action in the bar chart. The error message found at console is "Uncaught TypeError: Cannot read property 'all' of undefined." Here is the code.
parsecsv = function (string) {
var rows = d3.csv.parse(string);
var records = [];
rows.forEach(function (d, i) {
d.animals= d.animals.split(/,/);
d.food = d.food.split(/,/);
records.push(d)
});
return records
};
var chartGroup = "chartGroup";
var heatmapChart = dc.heatMap("#heatmap", chartGroup);
var pieChart1 = dc.pieChart("#piechart1", chartGroup);
var pieChart2 = dc.pieChart("#piechart2", chartGroup);
var barChart = dc.barChart("#barchart", chartGroup);
var ndx = crossfilter(parsecsv(csvtext));
console.log(ndx);
var animals_food_dim = ndx.dimension(function(d) { console.log (d.animals);return [d.animals, d.food]; });
var animals_food_group = animals_food_dim.groupAll().reduce(reduceAdd, reduceRemove, reduceInitial).value();
function reduceAdd(p, v) {
// skip empty values
if (v.animals[0] === "" || v.food[0]==="") return p;
v.animals.forEach (function(val1, idx1) {
v.food.forEach (function(val2, idx2) {
var temp_array=[val1,val2];
p[temp_array] = (p[temp_array] || 0) + 1; //increment counts
//console.log(val1+":"+val2, p[temp_array],temp_array);
});
});
return p;
}
function reduceRemove(p, v) {
if (v.animals[0] === "") return p; // skip empty values
v.animals.forEach (function(val1, idx1) {
v.food.forEach (function(val2, idx2) {
var temp_array=[val1,val2];
p[temp_array] = (p[temp_array] || 0) - 1; //increment counts
//console.log(val1+":"+val2,p[temp_array]);
});
});
return p;
}
function reduceInitial() {
return {};
}
animals_food_group.all = function() {
var newObject = [];
for (var key in this) {
if (this.hasOwnProperty(key) && key != "all" && key != "top") {
var temp_array=[key.substring(0,key.indexOf(",")),key.substring(key.indexOf(",")+1)];
//console.log(temp_array,this[temp_array]);
newObject.push({
key: temp_array,
value: this[temp_array]
});
}
}
return newObject;
};
animals_food_group.top = function(count) {
var newObject = this.all();
newObject.sort(function(a, b){return b.value - a.value});
return newObject.slice(0, count);
};
heatmapChart
.width(12 * 80 + 80)
.height(27 * 10 + 40)
.dimension(animals_food_dim)
.group(animals_food_group)
.keyAccessor(function(d) {return d.key[0];})
.valueAccessor(function(d) {return d.key[1];})
.colorAccessor(function(d) {return +d.value;})
.linearColors(["#FFEAEA", "#FF0000"])
.title(function(d) {
return "Animals: " + d.key[0] + "\n" +
"Food: " + d.key[1] + "\n" +
"Count: " + ( d.value) + " ";})
.calculateColorDomain();
heatmapChart.render();
var city_dim = ndx.dimension(function(d) {return d.city; });
var city_group = city_dim.group().reduceSum(function(d) {return +d.visitor;});
barChart
.dimension(city_dim)
.group(city_group)
.width(12 * 80 + 80)
.height(480)
.elasticY(true)
.x(d3.scale.ordinal().domain(["NYC","LAX","LON","TOR","SFO","DAL"]))
.xUnits(dc.units.ordinal)
.elasticY(true)
.centerBar(false)
.xAxisPadding(50);
barChart.render();
I bet I must did something wrong in reduceAdd, reduceRemove and/or animals_food_group.all. Would someone help me how to fix the interaction problem?
The JSFiddle is http://jsfiddle.net/chitester11/u33yb8k5/.

Resources