Related
When I click on a dc.js stacked bar chart, my pie chart elsewhere on the same page doesn't show the correct groups.
I'm new to dc.js, so I've created a simple dataset to demo features I need: Alice and Bob write articles about fruit, and tag each article with a single tag. I've charted this data as follows:
Line chart showing number of articles per day
Pie chart showing total number of each tag used
Stacked bar chart showing number of tags used by author
The data set is as follows:
rawData = [
{"ID":"00000001","User":"Alice","Date":"20/02/2019","Tag":"apple"},
{"ID":"00000002","User":"Bob","Date":"17/02/2019","Tag":"dragonfruit"},
{"ID":"00000003","User":"Alice","Date":"21/02/2019","Tag":"banana"},
{"ID":"00000004","User":"Alice","Date":"22/02/2019","Tag":"cherry"},
{"ID":"00000005","User":"Bob","Date":"23/02/2019","Tag":"cherry"},
];
Illustrative JSFiddle here: https://jsfiddle.net/hv8sw6km/ and code snippet below:
/* Prepare data */
rawData = [
{"ID":"00000001","User":"Alice","Date":"20/02/2019","Tag":"apple"},
{"ID":"00000002","User":"Bob","Date":"17/02/2019","Tag":"dragonfruit"},
{"ID":"00000003","User":"Alice","Date":"21/02/2019","Tag":"banana"},
{"ID":"00000004","User":"Alice","Date":"22/02/2019","Tag":"cherry"},
{"ID":"00000005","User":"Bob","Date":"23/02/2019","Tag":"cherry"},
];
var data = [];
var parseDate = d3.timeParse("%d/%m/%Y");
rawData.forEach(function(d) {
d.Date = parseDate(d.Date);
data.push(d);
});
var ndx = crossfilter(data);
/* Set up dimensions, groups etc. */
var dateDim = ndx.dimension(function(d) {return d.Date;});
var dateGrp = dateDim.group();
var tagsDim = ndx.dimension(function(d) {return d.Tag;});
var tagsGrp = tagsDim.group();
var authorDim = ndx.dimension(function(d) { return d.User; });
/* Following stacked bar chart example at
https://dc-js.github.io/dc.js/examples/stacked-bar.html
adapted for context. */
var authorGrp = authorDim.group().reduce(
function reduceAdd(p,v) {
p[v.Tag] = (p[v.Tag] || 0) + 1;
p.total += 1;
return p;
},
function reduceRemove(p,v) {
p[v.Tag] = (p[v.Tag] || 0) - 1;
p.total -= 1;
return p;
},
function reduceInit() { return { total: 0 } }
);
var minDate = dateDim.bottom(1)[0].Date;
var maxDate = dateDim.top(1)[0].Date;
var fruitColors = d3
.scaleOrdinal()
.range(["#00CC00","#FFFF33","#CC0000","#CC00CC"])
.domain(["apple","banana","cherry","dragonfruit"]);
/* Create charts */
var articlesByDay = dc.lineChart("#chart-articlesperday");
articlesByDay
.width(500).height(200)
.dimension(dateDim)
.group(dateGrp)
.x(d3.scaleTime().domain([minDate,maxDate]));
var tagsPie = dc.pieChart("#chart-article-tags");
tagsPie
.width(150).height(150)
.dimension(tagsDim)
.group(tagsGrp)
.colors(fruitColors)
.ordering(function (d) { return d.key });
var reviewerOrdering = authorGrp
.all()
// .sort(function (a, b) { return a.value.total - b.value.total })
.map(function (d) { return d.key });
var tagsByAuthor = dc.barChart("#chart-tags-by-reviewer");
tagsByAuthor
.width(600).height(400)
.x(d3.scaleBand().domain(reviewerOrdering))
.xUnits(dc.units.ordinal)
.dimension(authorDim)
.colors(fruitColors)
.elasticY(true)
.title(function (d) { return d.key + ": " + this.layer + ": " + d.value[this.layer] });
function sel_stack(i) {
return function(d) {
return d.value[i];
};
}
var tags = tagsGrp
.all()
.sort(function(a,b) { return b.value - a.value})
.map(function (d) { return d.key });
tagsByAuthor.group(authorGrp, tags[0]);
tagsByAuthor.valueAccessor(sel_stack(tags[0]));
tags.shift(); // drop the first, as already added as .group()
tags.forEach(function (tag) {
tagsByAuthor.stack(authorGrp, tag, sel_stack(tag));
});
dc.renderAll();
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter2/1.4.7/crossfilter.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dc/3.1.1/dc.min.js"></script>
<div id="chart-articlesperday"></div>
<div id="chart-article-tags"></div>
<div id="chart-tags-by-reviewer"></div>
As you can see, Alice has made three articles, each tagged with "apple", "banana" and "cherry" respectively, and her stacked bar chart shows this.
However whenever her column of the bar chart is clicked, the pie chart instead shows her as having 1 "apple" and 2 "cherry".
It took me a very long time even to get to this point, so it may be that there's something fundamental I'm not getting about crossfilter groupings, so any insights, tips or comments are very welcome.
Indeed, this is very weird behavior, and I wouldn't know what to think except that I have faced it a few times before.
If you look at the documentation for group.all(), it warns:
This method is faster than top(Infinity) because the entire group array is returned as-is rather than selecting into a new array and sorting. Do not modify the returned array!
I guess otherwise it might start modifying the wrong bins when aggregating. (Just a guess, I haven't traced through the code.)
You have:
var tags = tagsGrp
.all()
.sort(function(a,b) { return b.value - a.value})
.map(function (d) { return d.key });
Adding .slice(), to copy the array, fixes it:
var tags = tagsGrp
.all().slice()
.sort(function(a,b) { return b.value - a.value})
.map(function (d) { return d.key });
working fork of your fiddle
We actually have an open bug where the library does this itself. Ugh! (Easy enough to fix, but a little work to produce a test case.)
*Updated with more relevant code.
*Updated again: removing chart groupings results in this error: "Unable to get property 'classed' of undefined or null reference dc.js (5575,9)". I am using dc 3.0.11.
I have an issue where my dc charts are not filtering each other upon clicking a selection on a chart. It does work if I call a function to explicitly do so, but I would like to avoid that.
This is my general approach:
I do have dc.css included (if that matters)
my crossfilter ndx and dimensions are correct
I am defining my charts WITHOUT .on renderlet (is this the reason?)
var masterData = [];
$(document).ready(function () { //UPDATED CODE START
var siteurl = 'site';
var ItemCount= GetItemCount(siteurl, 'list');
createRestUrl(siteurl,ItemCount,'list');
}); // UPDATED CODE END
function GetItemCount(siteurl, ListName){
var ItemCount='';
$.ajax({
url: siteurl+"/_api/web/lists/GetByTitle('"+ListName+"')/ItemCount",
method: "GET",
async: false,
headers: { "Accept": "application/json; odata=verbose" },
success: function (data) {
ItemCount = data.d.ItemCount;
},
error: function (data) {
console.log(data);
}
});
return ItemCount;
} // GetItemCount END
function createRestUrl(siteurl, ItemCount, ListName) {
var listServiceUrl = siteurl+ "/_api/web/lists/GetByTitle('" + ListName + "')/Items";
//Rest call to process each items of list
$.when(processList(listServiceUrl,ItemCount)).done(function () {
console.log("FINISHED");
console.log("--------");
console.log(masterData);
var ndx = crossfilter(masterData),
clientMgr = ndx.dimension(function(d) { return d.clientMgr}),
otherTeammates = ndx.dimension(function(d) { return d.otherTeammates}),
topic = ndx.dimension(function(d) { return d.topic}),
status = ndx.dimension(function(d) { return d.status}),
team = ndx.dimension(function(d) { return d.team}),
requester = ndx.dimension(function(d) { return d.requester}),
requesterBusiness = ndx.dimension(function(d) { return d.requesterBusiness}),
submitted = ndx.dimension(function(d) { return d.submitted}),
clientMgrGroup = clientMgr.group(),
otherTeammatesGroup = otherTeammates.group(),
topicGroup = topic.group(),
statusGroup = status.group(),
teamGroup = team.group(),
requesterGroup = requester.group(),
requesterBusinessGroup = requesterBusiness.group(),
submittedGroup = submitted.group();
var teamChart = dc.rowChart("#team_chart", "team");
var clientMgrChart = dc.pieChart("#mgr_chart", "mgr");
var statusChart = dc.pieChart("#status_chart", "status");
var requesterChart = dc.rowChart("#requester_chart", "request");
var requesterBusinessChart = dc.pieChart("#requesterBusiness_chart", "requestBusiness");
var timeSelect = dc.lineChart("#time_chart", "time");
var topicChart = dc.pieChart("#topic_chart", "topic");
var teamNum = dc.numberDisplay("#teamNum", "teamNum");
Date.prototype.addDays = function(days) {
var dat = new Date(this.valueOf());
dat.setDate(dat.getDate() + days);
return dat;
}
var thisDay = new Date();
var todayMinSix = thisDay.addDays(-30);
var todayPlusSix = thisDay.addDays(30);
teamChart
.dimension(team)
.group(teamGroup)
.width(800)
.height(200)
.transitionDuration(3000)
.ordering(function(d) { return -d.value })
.elasticX(true)
.x(d3.scaleLinear().domain([0, 100]))
.colors('#58D3F7')
//teamNum
//.valueAccessor(function(d) { return d})
//.formatNumber(d3.format())
//.group(teamGroup)
clientMgrChart
.dimension(clientMgr)
.group(clientMgrGroup)
.width(300)
.height(300)
.transitionDuration(3000)
statusChart
.dimension(status)
.group(statusGroup)
.height(200)
.width(500)
.innerRadius(95)
.transitionDuration(3000)
.colors(d3.scaleOrdinal().domain(["02 - Work-In-Progress", "01 - Pending Initial Review"])
.range(['#58D3F7', '#2E9AFE']))
requesterChart
.dimension(requester)
.group(requesterGroup)
.height(200)
.width(800)
.gap(10)
.transitionDuration(3000)
.ordering(function(d) { return -d.value })
.elasticX(true)
.colors('#F78181')
.x(d3.scaleLinear().domain([0, 100]));
requesterBusinessChart
.dimension(requesterBusiness)
.group(requesterBusinessGroup)
.height(300)
.width(300)
.innerRadius(117)
.transitionDuration(3000)
.colors('#F78181')
topicChart
.dimension(topic)
.group(topicGroup)
.height(200)
.width(500)
.innerRadius(95)
.transitionDuration(3000)
.colors(d3.scaleOrdinal().domain(["New Report / Interface", "General Support", "One-Time Data Set", "Recurring Data Set", "Modify Existing Report / Interface", "Production Issue"])
.range(['#F5A9A9', '#F78181', '#FA5858', '#F6CECE', '#F8E0E0', "#FBEFEF"]))
timeSelect
.width(1700)
.height(150)
.dimension(submitted)
.group(submittedGroup)
.transitionDuration(1000)
.elasticY(true)
.renderArea(true)
.x(d3.scaleTime().domain([todayMinSix, thisDay]))
.xUnits(d3.timeDays)
.mouseZoomable(false)
.xAxis();
teamChart.render();
statusChart.render();
requesterChart.render();
topicChart.render();
});
}
//Step 3: Rest call to process each items of list
function processList(nextUrl,ItemCount) {
var dfd = new $.Deferred();
if (nextUrl == undefined) {
dfd.resolve();
return;
}
getJSONDataFromUrl(nextUrl).done(function (listItems) {
TotalItemCount = TotalItemCount+listItems.d.results.length;
console.log("getJSON called");
var items = listItems.d.results;
var next = listItems.d.__next;
$.when(processList(next,ItemCount)).done(function (){
dfd.resolve();
});
$.each(items, function(index, obj){
items = {};
items.clientMgr = obj.ClientMgr; //Assigned To - might not be the right field
items.otherTeammates = obj.OtherTeammatesEngaged; //Assigned To - might not be the right field
items.topic = obj.Topic;
items.status = obj.Status;
items.team = obj.Team;
items.requester = obj.RequesterLOB;
items.submitted = obj.Submitted;
items.requesterBusiness = obj.RequesterBusinessGroup;
console.log("looping through each");
var convert = new Date(items.submitted);
items.submittedConvert = moment(convert).format('L');
items.submitted = convert;
var assignName = items.clientMgr;
items.clientMgr = assignName;
masterData.push(items);
console.log(items.requesterBusiness);
console.log(items.status);
}); //.each END
}); // getJSONDataFromUrl END
return dfd.promise();
} // processList END
I had this working long ago, but as the project became more complex, something broke along the way.
I'm going to guess that the error refers to the use of .classed() a few lines up from 5575. Here are the lines leading up to 5575:
if (d3.sum(_chart.data(), _chart.cappedValueAccessor)) {
pieData = pie(_chart.data());
_g.classed(_emptyCssClass, false);
} else {
// otherwise we'd be getting NaNs, so override
// note: abuse others for its ignoring the value accessor
pieData = pie([{key: _emptyTitle, value: 1, others: [_emptyTitle]}]);
_g.classed(_emptyCssClass, true);
}
if (_g) {
https://github.com/dc-js/dc.js/blob/3.0.11/dc.js#L5565-L5575
So (except for the line number being a little off) it's a good guess that _g is null.
As I noted in my comment above, this probably indicates that the chart was redrawn before it was rendered. This can happen if you create a chart but don't render it. Rendering initializes things like the scale and parent elements like the <g> that holds the chart.
When you create a chart, you either specify a chart group or the default chart group is selected for you. The chart is registered in that group, and when any chart in the group is filtered, all the charts are redrawn.
In your code above, you render specific charts (4 of them), but there are many more charts which you initialize but don't render (8). In particular, all of the pie charts are rendered except for clientMgrChart. When you later click on a chart, it's likely that chart crashes when it tries to redraw.
It would be nice if dc.js implemented a more helpful error for this case since you currently have to guess "hmm, null, guess something hasn't been set up right" and know that render must happen before redraw.
Note: it's more robust to initialize the charts and then call
dc.renderAll();
instead of rendering each one manually.
Note to others: With dc.js 3.1.2, my individual rowCharts would not animate themselves when clicked. The other charts in the crossfilter did animate.
Very confused, traced all the way through the dc.js call stack and learned a lot.
Ultimately, root cause for the failure was not having dc.css linked in my page's HTML.
Another good 'tell' that you don't have dc.css properly linked is that the rowChart vertical lines do not render.
Screenshots
rowChart With CSS - Clicking any of the bars DOES trigger animation.
rowChart Without CSS - Clicking any of the bars does not trigger animation.
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()
I'm trying to link a pie chart to a map so that when you click a state the pie chart updates with the popular vote for that state.
Currently my piechart is displaying empty.
Csv is formatted like so:
state, party, votes
Alabama,dem,725704
Alabama,rep,1314431
Alabama,lib,44211
Alabama,gre,20276
Alabama,con,9341
Alaska,dem,116454
Alaska,rep,163387
Alaska,lib,18725
Alaska,gre,5735
Alaska,con,3866
Alaska,other,10441
My code:
d3.csv("elecVotes.csv", function (data) {
d3.json("us.json", function (json){
// set up crossfilter on the data.
var ndx = crossfilter(data);
// set up the dimensions
var stateDim = ndx.dimension(function (d) { return d.state; });
var party = partyDim.group().reduceSum(function(d) { return d.votes;});
var votesDim = ndx.dimension(function(d) { return d.votes; });
// set up the groups/values
var state = stateDim.group();
var party = partyDim.group();
var votes = votesDim.group();
// the 4 different charts - options are set below for each one.
var pie = dc.pieChart('#chart-pie');
var usmap = dc.geoChoroplethChart("#usmap");
//create pie from to show popular vote for each state
pie
.width(180)
.height(180)
.radius(80)
.dimension(partyDim)
.group(votes)
.renderLabel(true)
.innerRadius(10)
.transitionDuration(500)
.colorAccessor(function (d, i) { return d.value; });
//display US map
usmap
.width(900)
.height(500)
.dimension(stateDim)
.group(state)
.colors(["rgb(20,202,255)","rgb(144,211,035)"])
.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();
});
});
I'm not sure what i'm doing wrong
I don't think you want a votesDim - that would group by the number of votes, so you would probably end up with a different bin for each count, since they are likely to be unique.
I'm guessing you probably want to count the number of votes for each party, so:
var partyGroup = partyDim.group().reduceSum(function(d) { return d.votes; });
Remember that a dimension specifies what you want to filter on, and a group is where data gets aggregated and read.
You also need to convert any numbers explicitly before you get started, since d3.csv will read every field as a string. So:
data.forEach(function(r) {
r.votes = +r.votes;
});
var ndx = crossfilter(data);
Not sure if this helps with your problem. Note that this really has nothing to do with the map; the pie chart should be able to draw itself independent of what the map is doing.
Edit
I bet the problem is those spaces in the column names. You could easily end up with fields named " party" and " votes" that way...
I want to show value in datatable for that i have put line of code
// Bar chart For pricing
var collectionRateValue = ndx.dimension(function (d) {
return d3.time.month(d.dd);
});
var collectionRateValueGroup_adj = collectionRateValue.group().reduceSum(function(d) {
return d.pay_amt;
});
var collectionRateValueGroup_payment = collectionRateValue.group().reduceSum(function(d) {
return d.ar_balance;
});
var collectionRateValueGroup_ar_bal = collectionRateValue.group().reduceSum(function(d) {
return d.charge_amt;
});
var collectionRateValueGroup_payment_line = collectionRateValue.group().reduceSum(function(d) {
return d.pay_amt;
});
This is code for showing data
dc.dataTable("#dc-data-table")
.dimension(collectionService)
.group(function (d) {
return d3.time.format(d.month_year_dos);
})
.size(10)
.columns([
function (d){
return d.month_year_dos;
},
function (d){
return d.ar_balance;
},
function (d){
return d.pay_amt;
},
function (d){
return "d.date";
},
])
This is how I want to show my data table.
Table header will come dynamically
I have find a lot but i didn't get how data will shown in row format?
It seems like you want to flip the rows of your table for columns. You will probably want to do that directly with D3.
The example at http://www.d3noob.org/2013/02/add-html-table-to-your-d3js-graph.html should show to use D3 to render a table. you will want to adapt this to render in a different order however...
Start simple and try to render one of the rows in your table - perhaps the adjustments row - by appending a td for each value in the crossfilter group.