/* 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; };