Files
edx-platform/lms/templates/class_dashboard/d3_stacked_bar_graph.js
2014-06-17 13:25:46 -04:00

446 lines
15 KiB
JavaScript

/*
There are three parameters:
(1) Parameter is of type object. Inside can include (* marks required):
data* - Array of objects with key, value pairs that represent a single stack of bars:
xValue - Corresponding value for the x-axis
stackData - Array of objects with key, value pairs that represent a bar:
color - Defines what "color" the bar will map to
value - Maps to the height of the bar, along the y-axis
tooltip - (Optional) Text to display on mouse hover
height - Height of the SVG the graph will be displayed in (default: 500)
width - Width of the SVG the graph will be displayed in (default: 500)
margin - Object with key, value pairs for the graph's margins within the SVG (default for all: 10)
top - Top margin
bottom - Bottom margin
right - Right margin
left - Left margin
yRange - Array of two values, representing the min and max respectively. (default: [0, <calculated max>])
xRange - Array of either the min and max or ordered ordinals (default: calculated min and max or ordered ordinals given in data)
colorRange - Array of either the min and max or ordered ordinals (default: calculated min and max or ordered ordinals given in data)
bVerticalXAxisLabel - Boolean whether to make the labels in the x-axis veritcal (default: false)
bLegend - Boolean if false does not create the graph with a legend (default: true)
(2) Parameter is a d3 pointer to the SVG the graph will draw itself in.
(3) Parameter is a d3 pointer to a div that will be used for the graph's tooltip.
****Does not actually draw graph.**** Returns an object that includes a function
drawGraph, for when ready to draw graph. Reason for this is, because of all
the defaults, some changes may be needed before drawing the graph
returns an object with the following:
state - All information that can be put in parameters and adding:
margin.axisX - margin to accomodate the x-axis
margin.axisY - margin to acommodate the y-axis
drawGraph - function to call when ready to draw graph
scale - Object containing three d3 scales
x - d3 scale for the x-axis
y - d3 scale for the y-axis
stackColor - d3 scale for the stack color
axis - Object containg the graph's two d3 axis
x - d3 axis for the x-axis
y - d3 axis for the y-axis
svg - d3 pointer to the svg holding the graph
svgGroup - object holding the svg groups
main - svg group holding all other groups
xAxis - svg group holding the x-axis
yAxis - svg group holding the x-axis
bars - svg groups holding the bars
yAxisLabel - d3 pointer to the text component that holds the y axis label
divTooltip - d3 pointer to the div that is used as the tooltip for the graph
rects - d3 collection of the rects used in the bars
legend - object containing information for the legend
height - height of the legend
width - width of the legend (if change, need to update state.margin.axisY also)
range - array of values that appears in the legend
barHeight - height of a bar in the legend, based on height and length of range
*/
edx_d3CreateStackedBarGraph = function(parameters, svg, divTooltip) {
var graph = {
svg : svg,
state : {
data : undefined,
height : 500,
width : 500,
margin: {top: 10, bottom: 10, right: 10, left: 10},
yRange: [0],
xRange : undefined,
colorRange : undefined,
tag : "",
bVerticalXAxisLabel : false,
bLegend : true,
},
divTooltip : divTooltip,
};
var state = graph.state;
// Handle parameters
state.data = parameters.data;
if (parameters.margin != undefined) {
for (var key in state.margin) {
if ((state.margin.hasOwnProperty(key) &&
(parameters.margin[key] != undefined))) {
state.margin[key] = parameters.margin[key];
}
}
}
for (var key in state) {
if ((key != "data") && (key != "margin")) {
if (state.hasOwnProperty(key) && (parameters[key] != undefined)) {
state[key] = parameters[key];
}
}
}
if (state.tag != "")
state.tag = state.tag+"-";
if ((state.xRange == undefined) || (state.yRange.length < 2 ||
state.colorRange == undefined)) {
var aryXRange = [];
var bXIsOrdinal = false;
var maxYRange = 0;
var aryColorRange = [];
var bColorIsOrdinal = false;
for (var stackKey in state.data) {
var stack = state.data[stackKey];
aryXRange.push(stack.xValue);
if (isNaN(stack.xValue))
bXIsOrdinal = true;
var valueTotal = 0;
for (var barKey in stack.stackData) {
var bar = stack.stackData[barKey];
valueTotal += bar.value;
if (isNaN(bar.color))
bColorIsOrdinal = true;
if (aryColorRange.indexOf(bar.color) < 0)
aryColorRange.push(bar.color);
}
if (maxYRange < valueTotal)
maxYRange = valueTotal;
}
if (state.xRange == undefined){
if (bXIsOrdinal)
state.xRange = aryXRange;
else
state.xRange = [
Math.min.apply(null,aryXRange),
Math.max.apply(null,aryXRange)
];
}
if (state.yRange.length < 2)
state.yRange[1] = maxYRange;
if (state.colorRange == undefined){
if (bColorIsOrdinal)
state.colorRange = aryColorRange;
else
state.colorRange = [
Math.min.apply(null,aryColorRange),
Math.max.apply(null,aryColorRange)
];
}
}
// Find needed spacing for axes
var tmpEl = graph.svg.append("text").text(state.yRange[1]+"1234")
.attr("id",state.tag+"stacked-bar-graph-long-str");
state.margin.axisY = document.getElementById(state.tag+"stacked-bar-graph-long-str")
.getComputedTextLength()+state.margin.left;
var longestXAxisStr = "";
if (isNaN(state.xRange[0])) {
for (var i in state.xRange) {
if (longestXAxisStr.length < state.xRange[i].length)
longestXAxisStr = state.xRange[i]+"1234";
}
} else {
longestXAxisStr = state.xRange[1]+"1234";
}
tmpEl.text(longestXAxisStr);
if (state.bVerticalXAxisLabel) {
state.margin.axisX = document.getElementById(state.tag+"stacked-bar-graph-long-str")
.getComputedTextLength()+state.margin.bottom;
} else {
state.margin.axisX = document.getElementById(state.tag+"stacked-bar-graph-long-str")
.clientHeight+state.margin.bottom;
}
tmpEl.remove();
// Add y0 and y1 of the y-axis based on the count and order of the colorRange.
// First, case if color is a number range
if ((state.colorRange.length == 2) && !(isNaN(state.colorRange[0])) &&
!(isNaN(state.colorRange[1]))) {
for (var stackKey in state.data) {
var stack = state.data[stackKey];
stack.stackData.sort(function(a,b) { return a.color - b.color; });
var currTotal = 0;
for (var barKey in stack.stackData) {
var bar = stack.stackData[barKey];
bar.y0 = currTotal;
currTotal += bar.value;
bar.y1 = currTotal;
}
}
} else {
for (var stackKey in state.data) {
var stack = state.data[stackKey];
var tmpStackData = [];
for (var barKey in stack.stackData) {
var bar = stack.stackData[barKey];
tmpStackData[state.colorRange.indexOf(bar.color)] = bar;
}
stack.stackData = tmpStackData;
var currTotal = 0;
for (var barKey in stack.stackData) {
var bar = stack.stackData[barKey];
bar.y0 = currTotal;
currTotal += bar.value;
bar.y1 = currTotal;
}
}
}
// Add information to create legend
if (state.bLegend) {
graph.legend = {
height : (state.height-state.margin.top-state.margin.axisX),
width : 30,
range : state.colorRange,
};
if ((state.colorRange.length == 2) && !(isNaN(state.colorRange[0])) &&
!(isNaN(state.colorRange[1]))) {
graph.legend.range = [];
var i = 0;
var min = state.colorRange[0];
var max = state.colorRange[1];
while (i <= 10) {
graph.legend.range[i] = min+((max-min)/10)*i;
i += 1;
}
}
graph.legend.barHeight = graph.legend.height/graph.legend.range.length;
// Shifting the axis over to make room
graph.state.margin.axisY += graph.legend.width;
}
// Make the scales
graph.scale = {
x: d3.scale.ordinal()
.domain(graph.state.xRange)
.rangeRoundBands([
(graph.state.margin.axisY),
(graph.state.width-graph.state.margin.right)],
.3),
y: d3.scale.linear()
.domain(graph.state.yRange) // yRange is the range of the y-axis values
.range([
(graph.state.height-graph.state.margin.axisX),
graph.state.margin.top
]),
stackColor: d3.scale.ordinal()
.domain(graph.state.colorRange)
.range(["#ffeeee","#ffebeb","#ffd8d8","#ffc4c4","#ffb1b1","#ff9d9d","#ff8989","#ff7676","#ff6262","#ff4e4e","#ff3b3b"])
};
if ((state.colorRange.length == 2) && !(isNaN(state.colorRange[0])) &&
!(isNaN(state.colorRange[1]))) {
graph.scale.stackColor = d3.scale.linear()
.domain(state.colorRange)
.range(["#e13f29","#17a74d"]);
}
// Setup axes
graph.axis = {
x: d3.svg.axis()
.scale(graph.scale.x),
y: d3.svg.axis()
.scale(graph.scale.y),
}
graph.axis.x.orient("bottom");
graph.axis.y.orient("left");
// Draw graph function, to call when ready.
graph.drawGraph = function() {
var graph = this;
// Steup SVG
graph.svg.attr("id", graph.state.tag+"stacked-bar-graph")
.attr("class", "stacked-bar-graph")
.attr("width", graph.state.width)
.attr("height", graph.state.height);
graph.svgGroup = {};
graph.svgGroup.main = graph.svg.append("g");
// Draw Bars
graph.svgGroup.bars = graph.svgGroup.main.selectAll(".stacked-bar")
.data(graph.state.data)
.enter().append("g")
.attr("class", "stacked-bar")
.attr("transform", function(d) {
return "translate("+graph.scale.x(d.xValue)+",0)";
});
graph.rects = graph.svgGroup.bars.selectAll("rect")
.data(function(d) { return d.stackData; })
.enter().append("rect")
.attr("width", function(d) {
return graph.scale.x.rangeBand()
})
.attr("y", function(d) { return graph.scale.y(d.y1); })
.attr("height", function(d) {
return graph.scale.y(d.y0) - graph.scale.y(d.y1);
})
.attr("id", function(d) { return d.module_url })
.style("fill", function(d) { return graph.scale.stackColor(d.color); })
.style("stroke", "white")
.style("stroke-width", "0.5px");
// Setup tooltip
if (graph.divTooltip != undefined) {
graph.divTooltip
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden");
}
graph.rects
.on("mouseover", function(d) {
var pos = d3.mouse(graph.divTooltip.node().parentNode);
var left = pos[0]+10;
var top = pos[1]-10;
var width = $('#'+graph.divTooltip.attr("id")).width();
// Construct the tooltip
if (d.tooltip['type'] == 'subsection') {
stud_str = ngettext('%(num_students)s student opened Subsection', '%(num_students)s students opened Subsection', d.tooltip['num_students']);
stud_str = interpolate(stud_str, {'num_students': d.tooltip['num_students']}, true);
tooltip_str = stud_str + ' ' + d.tooltip['subsection_num'] + ': ' + d.tooltip['subsection_name'];
}else if (d.tooltip['type'] == 'problem') {
stud_str = ngettext('%(num_students)s student', '%(num_students)s students', d.tooltip['count_grade']);
stud_str = interpolate(stud_str, {'num_students': d.tooltip['count_grade']}, true);
q_str = ngettext('%(num_questions)s question', '%(num_questions)s questions', d.tooltip['max_grade']);
q_str = interpolate(q_str, {'num_questions': d.tooltip['max_grade']}, true);
tooltip_str = d.tooltip['label'] + ' ' + d.tooltip['problem_name'] + ' - ' \
+ stud_str + ' (' + d.tooltip['student_count_percent'] + '%) (' \
+ d.tooltip['percent'] + '%: ' + d.tooltip['grade'] +'/' \
+ q_str + ')';
}
graph.divTooltip.style("visibility", "visible")
.text(tooltip_str);
if ((left+width+30) > $("#"+graph.divTooltip.node().parentNode.id).width())
left -= (width+30);
graph.divTooltip.style("top", top+"px")
.style("left", left+"px");
})
.on("mouseout", function(d){
graph.divTooltip.style("visibility", "hidden")
});
// Add legend
if (graph.state.bLegend) {
graph.svgGroup.legendG = graph.svgGroup.main.append("g")
.attr("class","stacked-bar-graph-legend")
.attr("transform","translate("+graph.state.margin.left+","+
graph.state.margin.top+")");
graph.svgGroup.legendGs = graph.svgGroup.legendG.selectAll(".stacked-bar-graph-legend-g")
.data(graph.legend.range)
.enter().append("g")
.attr("class","stacked-bar-graph-legend-g")
.attr("id",function(d,i) { return graph.state.tag+"legend-"+i; })
.attr("transform", function(d,i) {
return "translate(0,"+
(graph.state.height-graph.state.margin.axisX-((i+1)*(graph.legend.barHeight))) + ")";
});
graph.svgGroup.legendGs.append("rect")
.attr("class","stacked-bar-graph-legend-rect")
.attr("height", graph.legend.barHeight)
.attr("width", graph.legend.width)
.style("fill", graph.scale.stackColor)
.style("stroke", "white");
graph.svgGroup.legendGs.append("text")
.attr("class","axis-label")
.attr("transform", function(d) {
var str = "translate("+(graph.legend.width/2)+","+
(graph.legend.barHeight/2)+")";
return str;
})
.attr("dy", ".35em")
.attr("dx", "-1px")
.style("text-anchor", "middle")
.text(function(d,i) { return d; });
}
// Draw Axes
graph.svgGroup.xAxis = graph.svgGroup.main.append("g")
.attr("class","stacked-bar-graph-axis")
.attr("id",graph.state.tag+"x-axis");
var tmpS = "translate(0,"+(graph.state.height-graph.state.margin.axisX)+")";
if (graph.state.bVerticalXAxisLabel) {
graph.axis.x.orient("left");
tmpS = "rotate(270), translate(-"+(graph.state.height-graph.state.margin.axisX)+",0)";
}
graph.svgGroup.xAxis.attr("transform", tmpS)
.call(graph.axis.x);
graph.svgGroup.yAxis = graph.svgGroup.main.append("g")
.attr("class","stacked-bar-graph-axis")
.attr("id",graph.state.tag+"y-axis")
.attr("transform","translate("+
(graph.state.margin.axisY)+",0)")
.call(graph.axis.y);
graph.yAxisLabel = graph.svgGroup.yAxis.append("text")
.attr("dy","1em")
.attr("transform","rotate(-90)")
.style("text-anchor","end")
.text(gettext("Number of Students"));
};
return graph;
};