aspx page on Sharepoint error: Function Expected only when rendering datatable - dc.js

I have been building a reporting page in sharepoint with dc and crossfilter.
Right now on my page, I have 5 pie charts that render with no problem. However, when I tried to add a dc datatable to the page to show results of the charts as they are filtered, I get a javascript error on "resultsChart.render();"
Because no errors are given when I render each of the pie charts, I assume this to mean that something is off with the datatable object, or that I cannot call render() on that object (whatever it thinks it is).
Here are the relevant pieces of my code:
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.5.0/d3.min.js" type="text/javascript">
<script src="https://cdnjs.cloudflare.com/ajax/libs/datatables/1.10.19/js/jquery.dataTables.min.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js" type="text/javascript">
<script src="https://cdnjs.cloudflare.com/ajax/libs/dc/3.0.6/dc.min.js" type="text/javascript">
//connect to sharepoint site (change this URL to redirect)
var siteUrl = 'path';
var masterData = [];
//retrieve list data from above sharepoint site based on List Name
function retrieveListItems() {
var clientContext = new SP.ClientContext(siteUrl);
var oList = clientContext.get_web().get_lists().getByTitle('Upcoming');
var camlQuery = new SP.CamlQuery();
camlQuery.set_viewXml("<View><Query></Query></View>");
this.collListItem = oList.getItems(camlQuery);
clientContext.load(collListItem);
clientContext.executeQueryAsync(
Function.createDelegate(this, this.onQuerySucceeded),
Function.createDelegate(this, this.onQueryFailed)
);
}
//on query success
function onQuerySucceeded(sender, args) {
var listItemEnumerator = collListItem.getEnumerator();
while (listItemEnumerator.moveNext()) {
var data = {};
var oListItem = listItemEnumerator.get_current();
//set field keys on array objects
data.project = oListItem.get_item('Project_x0020_Type');
data.stoplight = oListItem.get_item('Stoplight');
data.appmgr = oListItem.get_item('AIT_x0020_App_x0020_Manager');
data.compdate = oListItem.get_item('Completion_x0020_Due_x0020_Date');
data.ait = oListItem.get_item('AIT_x0020_Number');
data.lob = oListItem.get_item('Business_x0020_Area');
data.sublob = oListItem.get_item('Business_x0020_Sub_x0020_Area');
masterData.push(data);
}//end while
var projectChart = dc.pieChart("#project", "project");
var stoplightChart = dc.pieChart("#stoplight", "stoplight");
var appmgrChart = dc.pieChart("#appmgr", "appmgr");
var lobChart = dc.pieChart("#lob", "lob");
var sublobChart = dc.pieChart("#sublob", "sublob");
var resultChart = dc.dataTable("#result_table", "result");
var ndx = crossfilter(masterData),
projectType = ndx.dimension(function(d) { return d.project;}),
stoplight = ndx.dimension(function(d) { return d.stoplight;}),
appMgr = ndx.dimension(function(d) { return d.appmgr;}),
compdate = ndx.dimension(function(d) { return d.compdate;}),
lob = ndx.dimension(function(d) { return d.lob;}),
sublob = ndx.dimension(function(d) { return d.sublob;})
projectTypeGroup = projectType.group();
stoplightGroup = stoplight.group(),
appMgrGroup = appMgr.group(),
compDateGroup = compdate.group(),
lobGroup = lob.group(),
sublobGroup = sublob.group();
projectChart
.dimension(projectType)
.group(projectTypeGroup)
.width(200)
.height(200)
.innerRadius(75)
stoplightChart
.dimension(stoplight)
.group(stoplightGroup)
.width(200)
.height(200)
.innerRadius(75)
appmgrChart
.dimension(appMgr)
.group(appMgrGroup)
.width(200)
.height(200)
.innerRadius(75)
lobChart
.dimension(lob)
.group(lobGroup)
.width(300)
.height(300)
.innerRadius(117)
sublobChart
.dimension(sublob)
.group(sublobGroup)
.width(200)
.height(200)
.innerRadius(75)
resultChart
.dimension(compdate)
.group(compDateGroup)
.columns([
function(d) { return d.ait},
function(d) { return d.project},
function(d) { return d.stoplight},
function(d) { return d.compdate}
])
.size(100);
projectChart.render();
stoplightChart.render();
appmgrChart.render();
lobChart.render();
sublobChart.render();
resultChart.render();
}
function onQueryFailed(sender, args) {
alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
}
SP.SOD.executeOrDelayUntilScriptLoaded(retrieveListItems, 'sp.js');
</script>
Any and all help is extremely appreciated!

I figured it out. According to the dc.dataTable documentation, you cannot use a crossfilter group as the .group attribute on a datatable. Instead, you must explicitly use a function there.
So it should be
resultChart
.dimension(compdate)
.group(function(d) { return d.compdate;})
.columns([
function(d) { return d.ait},
function(d) { return d.project},
function(d) { return d.stoplight},
function(d) { return d.compdate}
])
.size(100);
Instead of
resultChart
.dimension(compdate)
.group(compDateGroup)
.columns([
function(d) { return d.ait},
function(d) { return d.project},
function(d) { return d.stoplight},
function(d) { return d.compdate}
])
.size(100);

Related

Why is my pie chart showing incorrect groups when filtered with stacked bar chart in dc.js & crossfilter.js?

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.)

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()

D3-request retrieval of a google spreadsheet csv/json file fails

I have difficulties to make Google spreadsheet and D3js' D3-request / D3-request > header works together via xhr requests.
I use the following JS :
d3.request(url)
.header("X-Requested-With", "XMLHttpRequest")
.mimeType("text/csv")
.get(function(error, data) {
if (error) throw error;
console.log('request: '+ data);
});
I get the following error:
XMLHttpRequest cannot load https://docs.google.com/spreadsheets/d/e/2PACX-1vSZyV9olwK_hx0BRFgLtTz5hs_Z…mROYhax3VD9AFXTvmcataf8LuSIpxGT2/pub?gid=1023695213&single=true&output=csv. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://fiddle.jshell.net' is therefore not allowed access.
Jsfiddle here
Any idea to bypass it ?
Using D3 only
d3.json(url, function (error, result) {
var data = [];
for (i = 0; i < result.feed.entry.length; i += 1) {
data.push({
"animal": result.feed.entry[i].gsx$animal.$t,
"population": result.feed.entry[i].gsx$population.$t
});
}
pie_chart(data, "#chart1");
});
Using jQuery
$.get(url, function (result) {
var data = [];
$(result.feed.entry).each(function () {
data.push({"animal": this.gsx$animal.$t, "population": this.gsx$population.$t});
});
pie_chart(data, "#chart2");
});
Using tabletop.
Tabletop.init({
key: key,
callback: function (data, tabletop) {
pie_chart(data, "#chart3");
},
simpleSheet: true
});
Below is just a simple example to get data from a Google spreadsheet and turn it into a D3 pie chart.
//draws a pie chart with D3
function pie_chart(data, id) {
var w = 400;
var h = 400;
var r = h / 2;
var color = d3.scale.category20c();
var vis = d3.select(id).append("svg:svg").data([data]).attr("width", w).attr("height", h).append("svg:g").attr("transform", "translate(" + r + "," + r + ")");
var pie = d3.layout.pie().value(function (d) {
return d.population;
});
var arc = d3.svg.arc().outerRadius(r);
var arcs = vis.selectAll("g.slice").data(pie).enter().append("svg:g").attr("class", "slice");
arcs.append("svg:path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", function (d) {
return arc(d);
});
arcs.append("svg:text").attr("transform", function (d) {
d.innerRadius = 0;
d.outerRadius = r;
return "translate(" + arc.centroid(d) + ")";
}).attr("text-anchor", "middle").text(function (d, i) {
return data[i].animal;
});
}
//the key of google spreadsheet
var key = "1moczdbrfFwCp0L4Ube1a4GevuDcj2XQmCnpjArF_UEY";
//the url for jQuery and D3
var url = "https://spreadsheets.google.com/feeds/list/" + key + "/od6/public/values?alt=json";
var i = 0;
//D3 only
d3.json(url, function (error, result) {
var data = [];
for (i = 0; i < result.feed.entry.length; i += 1) {
data.push({
"animal": result.feed.entry[i].gsx$animal.$t,
"population": result.feed.entry[i].gsx$population.$t
});
}
pie_chart(data, "#chart1");
});
//Jquery
$.get(url, function (result) {
var data = [];
$(result.feed.entry).each(function () {
data.push({"animal": this.gsx$animal.$t, "population": this.gsx$population.$t});
});
pie_chart(data, "#chart2");
});
//tabletop
Tabletop.init({
key: key,
callback: function (data, tabletop) {
pie_chart(data, "#chart3");
},
simpleSheet: true
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tabletop.js/1.4.3/tabletop.js"></script>
<div id="chart1" style="width: 480px; height: 400px;"><span>D3 only</span></div>
<hr>
<div id="chart2" style="width: 480px; height: 400px;"><span>Jquery</span></div>
<hr>
<div id="chart3" style="width: 480px; height: 400px;"><span>Tabletop</span></div>

date sorting inside table using dc.js and d3

I'm trying to sort data based on date inside a table. However the dates are not sorted properly. Any guidance would be appreciated.
Pasted some parts of my code:
var dateFormat = d3.time.format("%Y-%m-%d");
var formatDate = d3.time.format('%d/%m/%Y');
jiraalertproject.forEach(function(d) {
d["date"] = dateFormat.parse(d["date"]);
var dataTable = dc.dataTable("#dc-table-graph");
dataTable
.width(1000)
.height(600)
.dimension(dateDim)
.group(function(d) { return "You will get the Ticket details with scroll option:"
})
.size(10)
.columns([
function(d) { return formatDate(d.date); },
function(d) { return d.ticket; },
function(d) { return d.component; },
function(d) { return d.summary; }
])
.sortBy(function(d){ return +formatDate(d.date); })
.order(d3.descending);

How to override a function in NVD3 library?

My goal is to override the function used by NVD3 for the content of a tooltip.
I found the function that provides this behaviour: contentGenerator.
Below here the code:
var contentGenerator = function(d) {
if (content != null) {
return content;
}
if (d == null) {
return '';
}
var table = d3.select(document.createElement("table"));
var theadEnter = table.selectAll("thead")
.data([d])
.enter().append("thead");
theadEnter.append("tr")
.append("td")
.attr("colspan",3)
.append("strong")
.classed("x-value",true)
.html(headerFormatter(d.value));
var tbodyEnter = table.selectAll("tbody")
.data([d])
.enter().append("tbody");
var trowEnter = tbodyEnter.selectAll("tr")
.data(function(p) { return p.series})
.enter()
.append("tr")
.classed("highlight", function(p) { return p.highlight});
trowEnter.append("td")
.classed("legend-color-guide",true)
.append("div")
.style("background-color", function(p) { return p.color});
trowEnter.append("td")
.classed("key",true)
.html(function(p) {return p.key});
trowEnter.append("td")
.classed("value",true)
.html(function(p,i) { return valueFormatter(p.value,i) });
trowEnter.selectAll("td").each(function(p) {
if (p.highlight) {
var opacityScale = d3.scale.linear().domain([0,1]).range(["#fff",p.color]);
var opacity = 0.6;
d3.select(this)
.style("border-bottom-color", opacityScale(opacity))
.style("border-top-color", opacityScale(opacity))
;
}
});
var html = table.node().outerHTML;
if (d.footer == undefined)
html += "<div class='footer'>" + d.footer + "</div>";
return html;
};
Now, I'd like to put inside my chart section the function to override this method.
var chart = nv.models.lineChart()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.useInteractiveGuideline(false)
.tooltipContent(function (key, x, y, e, graph) {
return contentGenerator
});
How Can I achieve this goal?
I wanted to customise:
1) In the header add a simple text
2) Add a footer

Resources