/* * Class LinesPlusBivariateAreaGraph * * Generates a graph with two line graphs (one for normal values and one for averages) * It also draws an area between min and max values. * * @element is the name of the DOM element where that graph is about to be generated */ var LinesPlusBivariateAreaGraph = function(element, contentElement) { var element = element; var PIXELS_PER_CHAR = 6; // Assign 6 pixels to each character var OVERLAY_PIXELS_PER_CHAR = 5.5; // Assign 5 pixels to each character when the graph is drawn on an overlay /* * Function draw, draws the graph in the specified DOM element * * @data - the array of the data in the form of difficulty tracking missions * * @chartOptions - the provided object of the chart options * Check the difficulty tracking missions for an example */ function draw(rowData, chartOptions) { // Variable that will store the longest graph name for x-axis space allocation var maxNameChars = 0; // Variable that will store all the min/max values for each data entry across all of them var minMaxValuesArray = []; // Variable that will store the total max value for the range of y-axis var totalMax = 0; $.each(rowData, function(key, data) { // max value and max area for the current set of data var maxArea = 0; var maxValue = 0; $.each(data, function(i, d) { maxNameChars = (maxNameChars > chartOptions.name(d).length) ? maxNameChars : chartOptions.name(d).length; maxValue = (maxValue > chartOptions.value(d)) ? maxValue : chartOptions.value(d); var truncatedName = chartOptions.name(d); if (chartOptions.truncatedNameChars && (truncatedName.length > chartOptions.truncatedNameChars)) { truncatedName = truncatedName.substring(0, chartOptions.truncatedNameChars-3) + "..."; //console.log(truncatedName); } if (typeof minMaxValuesArray[i] === "undefined") minMaxValuesArray[i] = { min: chartOptions.minValue(d), max: chartOptions.maxValue(d), name: chartOptions.name(d), fullName: chartOptions.fullName(d), truncatedName: truncatedName, }; minMaxValuesArray[i].min = (minMaxValuesArray[i].min < chartOptions.minValue(d)) ? minMaxValuesArray[i].min : chartOptions.minValue(d); minMaxValuesArray[i].max = (minMaxValuesArray[i].max > chartOptions.maxValue(d)) ? minMaxValuesArray[i].max : chartOptions.maxValue(d); maxArea = (maxArea > minMaxValuesArray[i].max) ? maxArea : minMaxValuesArray[i].max; }); totalMax = (totalMax > d3.max([maxValue, maxArea]) ? totalMax : d3.max([maxValue, maxArea])); }); // Increase by 10% so the value is not at the end of the graph totalMax += (totalMax * 10)/100; //console.log(namesDict); // if the graph text is going to be truncated, set maxNameChars not higher than that threshold if (chartOptions.truncatedNameChars && (maxNameChars > chartOptions.truncatedNameChars)) maxNameChars = chartOptions.truncatedNameChars; var nameLength = maxNameChars * ((chartOptions.isOverlay) ? OVERLAY_PIXELS_PER_CHAR : PIXELS_PER_CHAR); //var defaultElementSpace = 20, var defaultElementSpace = 12, contentHeight = $("#" + contentElement).height(), contentWidth = $("#" + contentElement).width(); // Calculate the chart width and height automatically var width = contentWidth - chartOptions.margin.left - chartOptions.margin.right, height = contentHeight - chartOptions.margin.top - chartOptions.margin.bottom - chartOptions.legend.height; if (chartOptions.orientation == "horizontal") { // overwrite with the calculated value chartOptions.margin.bottom = nameLength; // recalculate height height = contentHeight - chartOptions.margin.top - chartOptions.margin.bottom - chartOptions.legend.height; if ((minMaxValuesArray.length * defaultElementSpace) > width) { height -= 17; // there will be a horizontal scrollbar $("#" + element).css("overflow-y", "hidden"); width = minMaxValuesArray.length * defaultElementSpace; $("#" + element).css("width", width + chartOptions.margin.left + chartOptions.margin.right); } else { $("#" + element).css("width", contentWidth); width = contentWidth - chartOptions.margin.left - chartOptions.margin.right $("#" + element).css("overflow", "hidden"); } } else if (chartOptions.orientation == "vertical") { // overwrite with the calculated value chartOptions.margin.left = nameLength; // recalculate width width = contentWidth - chartOptions.margin.left - chartOptions.margin.right; if ((minMaxValuesArray.length * defaultElementSpace) > height) { height = minMaxValuesArray.length * defaultElementSpace; $("#" + element).css("height", height + chartOptions.margin.top + chartOptions.margin.bottom + chartOptions.legend.height); } else { $("#" + element).css("height", contentHeight); height = contentHeight - chartOptions.margin.top - chartOptions.margin.bottom - chartOptions.legend.height; $("#" + element).css("overflow", "hidden"); } } /* console.log("data length : " + data.length); console.log("height : " + height); */ var tooltip = new CustomTooltip(element + "-tooltip"); // This is usually the name overlay var onNameOverlay = new CustomOverlay(element + "-name-overlay", $(window).width()*(5/6), $(window).height()*(5/6)); if (chartOptions.orientation == "horizontal") { // x & y functions for mapping the values to svg cordinates var x = d3.scale.linear() .range([0, width]); var y = d3.scale.linear() .range([height, 0]); // Functions for drawing the axis var xAxis = d3.svg.axis() .scale(x) .tickSize(-height) //.ticks(data.length) // use specific values instead .tickValues(d3.range(minMaxValuesArray.length)) .tickFormat(function(d, i) { return minMaxValuesArray[i].truncatedName; }) .orient("bottom"); var yAxis = d3.svg.axis() .scale(y) .tickSize(-width) .orient("left"); // Functions for drawing the lines and the bivariate area var valueLine = d3.svg.line() .x(function(d, i) { return x(i); }) .y(function(d) { return y(chartOptions.value(d)); }); var minMaxArea = d3.svg.area() .x(function(d, i) { return x(i); }) .y0(function(d) { return y(d.min); }) .y1(function(d) { return y(d.max); }); // Domain range of the x,y functions, used for the coordinate ranges //x.domain(d3.extent(data, function(d, i) { return i; })); x.domain(d3.extent(minMaxValuesArray, function(d, i) { return i; })); //Extend finds the min, max elements of an array //console.log(totalMax); y.domain([0, totalMax]); //y.domain([0, d3.max(data, function(d) {return chartOptions.maxValue(d);})]); } else if (chartOptions.orientation == "vertical") { // x & y functions for mapping the values to svg coordinates - range is for output var x = d3.scale.linear() .range([0, height]); var y = d3.scale.linear() .range([0, width]); // Functions for drawing the axis var xAxis = d3.svg.axis() .scale(x) .orient("left") .tickSize(-width) // .ticks(data.length) // use specific values instead .tickValues(d3.range(minMaxValuesArray.length)) .tickFormat(function(d, i) { return minMaxValuesArray[i].truncatedName; }); var yAxis = d3.svg.axis() .scale(y) .orient("bottom") .tickSize(-height); // Functions for drawing the lines and the bivariate area var valueLine = d3.svg.line() .y(function(d, i) { return x(i); }) .x(function(d) { return y(chartOptions.value(d)); }); var minMaxArea = d3.svg.area() .y(function(d, i) { return x(i); }) .x0(function(d) { return y(d.min); }) .x1(function(d) { return y(d.max); }); // Domain range of the x,y functions, used for the coordinate ranges (input values) x.domain(d3.extent(minMaxValuesArray, function(d, i) { return i; })); y.domain([0, totalMax]); } else console.error(chartOptions.orientation + " orientation is not supported"); // select the initialised svg element and set width/height and add top svg group with general transformation var svg = d3.select("#" + element).select("svg") .attr("width", width + chartOptions.margin.left + chartOptions.margin.right) .attr("height", height + chartOptions.margin.top + chartOptions.margin.bottom) .style("background", chartOptions.backgroundColour) .append("svg:g") .attr("transform", "translate(" + chartOptions.margin.left + "," + chartOptions.margin.top + ")"); // var transition = svg.transition().duration(config.transitionDuration); // append svg groups for the axis elements svg.append("svg:g") .attr("class", "x-axis") .attr("stroke", chartOptions.gridColour) .attr("fill", chartOptions.textColour) .attr("transform", function() { if (chartOptions.orientation == "horizontal") return "translate(0," + height + ")"; else if (chartOptions.orientation == "vertical") return ""; }) .call(xAxis) .selectAll("text") .attr("stroke", "none") .attr("text-anchor", "end") .attr("transform", function() { if (chartOptions.orientation == "horizontal") return "rotate(-90)" + "translate(" + (-1/2) * parseInt($("svg").css("font-size")) + ", " + (-1/2) * parseInt($("svg").css("font-size")) + ")"; else if (chartOptions.orientation == "vertical") return ""; }) .style("cursor", (chartOptions.onGraphClickOverlayContent) ? "pointer" : "default") .on("click", function(d, i) { if (chartOptions.onGraphClickOverlayContent) { // hide if another instance exists onNameOverlay.hideOverlay(); onNameOverlay.showOverlay(chartOptions.onGraphClickOverlayContent(rowData, i)); } }) .append("svg:title") .text(function(d, i) { return minMaxValuesArray[i].name; }); svg.append("svg:g") .attr("class", "y-axis") .attr("stroke", chartOptions.gridColour) .attr("fill", chartOptions.textColour) .attr("transform", function() { if (chartOptions.orientation == "horizontal") return ""; else if (chartOptions.orientation == "vertical") return "translate(0," + height + ")"; }) .call(yAxis) .selectAll("text") .attr("stroke", "none"); // Draw the min max bivariate area svg.append("svg:path") .datum(minMaxValuesArray) .attr("fill", chartOptions.areaColour) .attr("stroke", chartOptions.areaColour) .attr("class", "min-max-area") .attr("d", minMaxArea); // Draw the circles for the maximum and minimum points of the bivariate area svg.append("svg:g") .selectAll(".max-line-circle") .data(minMaxValuesArray) .enter() .append("svg:circle") .attr("class", "max-line-circle") //.attr("fill", chartOptions.areaColour) .attr("stroke", chartOptions.areaCircleColour) .attr("cx", function(d, i) { if (chartOptions.orientation == "horizontal") return x(i); else if (chartOptions.orientation == "vertical") return y(d.max); }) .attr("cy", function(d, i) { if (chartOptions.orientation == "horizontal") return y(d.max); else if (chartOptions.orientation == "vertical") return x(i); }) .attr("r", "0.3em") .on("mouseover", function(d, i) { tooltip.showTooltip(chartOptions.areaTooltipContent(d), d3.event); highlightCircle(d3.select(this)); }) .on("mouseout", function(d, i) { tooltip.hideTooltip(); revertCircle(d3.select(this)); }) .style("cursor", (chartOptions.onGraphClickOverlayContent) ? "pointer" : "default") .on("click", function(d, i) { if (chartOptions.onGraphClickOverlayContent) { tooltip.hideTooltip(); // hide if another instance exists onNameOverlay.hideOverlay(); onNameOverlay.showOverlay(chartOptions.onGraphClickOverlayContent(rowData, i)); } }); //.append("svg:title") // .text(function(d) { return chartOptions.maxValue(d); }); svg.append("svg:g") .selectAll(".min-line-circle") .data(minMaxValuesArray) .enter() .append("svg:circle") .attr("class", "min-line-circle") //.attr("fill", chartOptions.areaColour) .attr("stroke", chartOptions.areaCircleColour) .attr("cx", function(d, i) { if (chartOptions.orientation == "horizontal") return x(i); else if (chartOptions.orientation == "vertical") return y(d.min); }) .attr("cy", function(d, i) { if (chartOptions.orientation == "horizontal") return y(d.min); else if (chartOptions.orientation == "vertical") return x(i); }) .attr("r", "0.3em") .on("mouseover", function(d, i) { tooltip.showTooltip(chartOptions.areaTooltipContent(d), d3.event); highlightCircle(d3.select(this)); }) .on("mouseout", function(d, i) { tooltip.hideTooltip(); revertCircle(d3.select(this)); }) .style("cursor", (chartOptions.onGraphClickOverlayContent) ? "pointer" : "default") .on("click", function(d, i) { if (chartOptions.onGraphClickOverlayContent) { tooltip.hideTooltip(); // hide if another instance exists onNameOverlay.hideOverlay(); onNameOverlay.showOverlay(chartOptions.onGraphClickOverlayContent(rowData, i)); } }); // Colour generator for multiple lines var colourGen = d3.scale.category20(); var legendData = []; $.each(rowData, function(key, data) { // Draw the line for the values and circles on the specific points var colour; var valueTooltipContent; if (Object.keys(rowData).length == 1) { colour = chartOptions.lineColour; legendData.push(chartOptions.legendDataVar); valueTooltipContent = function(d) {return chartOptions.valueTooltipContent(d)}; } else { colour = colourGen(Math.random()*20); legendData.push({ "colour": colour, "label": (chartOptions.legendDataVar.label + " (" + key + ")"), }); valueTooltipContent = function(d) {return chartOptions.valueTooltipContent(d, key)}; } // Draw the line for the values and circles on the specific points svg.append("svg:path") .datum(data) .attr("class", "value-line") .attr("stroke", colour) .attr("d", valueLine); svg.append("svg:g") .selectAll(".value-line-circle") .data(data) .enter() .append("svg:circle") .attr("class", "value-line-circle") .attr("stroke", colour) //.attr("fill", chartOptions.lineColour) .attr("cx", function(d, i) { if (chartOptions.orientation == "horizontal") return x(i); else if (chartOptions.orientation == "vertical") return y(chartOptions.value(d)); }) .attr("cy", function(d, i) { if (chartOptions.orientation == "horizontal") return y(chartOptions.value(d)); else if (chartOptions.orientation == "vertical") return x(i); }) .attr("r", "0.3em") .on("mouseover", function(d, i) { tooltip.showTooltip(valueTooltipContent(d), d3.event); highlightCircle(d3.select(this)); }) .on("mouseout", function(d, i) { tooltip.hideTooltip(); revertCircle(d3.select(this)); }) .style("cursor", (chartOptions.onGraphClickOverlayContent) ? "pointer" : "default") .on("click", function(d, i) { if (chartOptions.onGraphClickOverlayContent) { tooltip.hideTooltip(); // hide if another instance exists onNameOverlay.hideOverlay(); onNameOverlay.showOverlay(chartOptions.onGraphClickOverlayContent(rowData, i)); } }); }); // Svg group for the bottom legends var legendGroup = svg.selectAll(".legend") .data(chartOptions.legendDataConst.concat(legendData.reverse())) .enter().append("g") .attr("class", "legend") .attr("transform", function(d, i) { return "translate(0, " + (height + chartOptions.margin.top + chartOptions.margin.bottom) + ")"; }); legendGroup.append("rect") .attr("x", function(d, i) {return i*chartOptions.legend.width}) .attr("width", chartOptions.legend.rectWidth) .attr("height", chartOptions.legend.rectWidth) .attr("fill", function(d) { return d.colour; }); legendGroup.append("text") //.attr("stroke", chartOptions.textColour) .style("fill", chartOptions.textColour) .attr("x", function(d, i) {return i*chartOptions.legend.width + chartOptions.legend.rectWidth + 2}) .attr("y", chartOptions.legend.rectWidth/2) .attr("dy", ".35em") .style("text-anchor", "start") .text(function(d) { //return (Object.keys(rowData).length == 1) ? d.label + " [" + Object.keys(rowData)[0] + "]" : d.label; return d.label; }); } // end of draw() function highlightCircle(d3Element) { d3Element.transition() .attr("r", "0.6em") //.duration(config.transitionDuration); } function revertCircle(d3Element) { d3Element.transition() .attr("r", "0.3em") //.duration(config.transitionDuration); } /* Expose the draw function to the Class instances */ return { draw: draw }; };