/* * Class LinesGraph * * Generates a graph with many line graphs. * * @element is the name of the DOM element where that graph is about to be generated * @contentElement is the name of the parent DOM element for calculating the available space */ var LinesGraph = function() { //var element = element; var PIXELS_PER_CHAR = 6.5; // Assign 6.5 pixels to each character var PIXELS_PER_VALUE_CHAR = 8; // Assign 8 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 var AXIS_LABELS_MARGIN = 18; // Extra margin for labels var lastYScale = null; var lastY1Scale = null; /* * 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 * @element - the DOM element name to host the new graph * @contentElement - the graph container element for calculating the available space (width/height) * @secondAxisKeys - the keys of the data dict entries where their values will be in a different axis scale (optional) * */ function draw(rowData, chartOptions, element, contentElement, lockedScales, secondAxisKeys) { cleanSvg(element); if (typeof secondAxisKeys === "undefined") secondAxisKeys = []; // Variable that will store the longest graph name for x-axis space allocation var maxNameChars = 0; // Variable that will store the longest graph value for y-axis space allocation var maxValueChars = 0; var maxValueChars1 = 0; // for the extra axis // Variable that will store all the min/max values for each data entry across all of them var valuesArray = []; // Variable that will store the total max value for the range of y-axis var totalMin = 0; var totalMax = 0; var totalMin1 = 0; // for the extra axis scale var totalMax1 = 0; $.each(rowData, function(key, data) { // max value for the current set of data var minValue = 0; var maxValue = 0; var inSecondAxis = (secondAxisKeys.indexOf(key) != -1) ? true : false; $.each(data, function(i, d) { var label = chartOptions.label(d) ? chartOptions.label(d) : ""; maxNameChars = (maxNameChars > label.toString().length) ? maxNameChars : label.toString().length; var value = chartOptions.value(d) ? chartOptions.value(d) : 0; maxValueChars = (maxValueChars > value.toFixed(2).length) ? maxValueChars : value.toFixed(2).length; minValue = (minValue < chartOptions.value(d)) ? minValue : chartOptions.value(d); maxValue = (maxValue > chartOptions.value(d)) ? maxValue : chartOptions.value(d); var truncatedName; if (chartOptions.truncatedNameChars && (chartOptions.label(d).length > chartOptions.truncatedNameChars)) { truncatedName = truncatedName.substring(0, chartOptions.truncatedNameChars-3) + "..."; } else truncatedName = chartOptions.label(d); // populate the values array if chartOptions.predefinedLabels is not set if (typeof valuesArray[i] === "undefined") { valuesArray[i] = { name: chartOptions.name(d), fullName: chartOptions.fullName(d), truncatedName: truncatedName, } } }); if (!inSecondAxis) { totalMin = (totalMin < minValue) ? totalMin : minValue; totalMax = (totalMax > maxValue) ? totalMax : maxValue; } else { totalMin1 = (totalMin1 < minValue) ? totalMin1 : minValue; totalMax1 = (totalMax1 > maxValue) ? totalMax1 : maxValue; } }); // Increase by 10% so the value is not at the end of the graph totalMin -= (Math.abs(totalMin) * 10)/100; totalMax += (totalMax * 10)/100; totalMin1 -= (Math.abs(totalMin1) * 10)/100; totalMax1 += (totalMax1 * 10)/100; if (lastYScale && lockedScales) { totalMin = lastYScale[0]; totalMax = lastYScale[1]; totalMin1 = lastY1Scale[0]; totalMax1 = lastY1Scale[1]; } else { // Use and save current for future lastYScale = [totalMin, totalMax]; lastY1Scale = [totalMin1, totalMax1]; } maxValueChars1 = maxValueChars; // 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) + AXIS_LABELS_MARGIN; var valueLength = maxValueChars * ((chartOptions.isOverlay) ? OVERLAY_PIXELS_PER_VALUE_CHAR : PIXELS_PER_VALUE_CHAR) + AXIS_LABELS_MARGIN; //console.log(valueLength); var valueLength1 = maxValueChars1 * ((chartOptions.isOverlay) ? OVERLAY_PIXELS_PER_VALUE_CHAR : PIXELS_PER_VALUE_CHAR) + AXIS_LABELS_MARGIN; //var defaultElementSpace = 20, var defaultElementSpace = 12, contentHeight = $("#" + contentElement).height() - 10, 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; //chartOptions.margin.left = chartOptions.margin.left+ valueLength; chartOptions.margin.left = valueLength; if (secondAxisKeys.length > 0) chartOptions.margin.right = valueLength1; // recalculate height height = contentHeight - chartOptions.margin.top - chartOptions.margin.bottom - chartOptions.legend.height; if ((valuesArray.length * defaultElementSpace) > width) { height -= 17; // there will be a horizontal scrollbar $("#" + element).css("overflow-y", "hidden"); width = valuesArray.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 = chartOptions.margin.left + nameLength; chartOptions.margin.left = nameLength; chartOptions.margin.bottom = valueLength; if (secondAxisKeys.length > 0) chartOptions.margin.top = valueLength1; // recalculate width width = contentWidth - chartOptions.margin.left - chartOptions.margin.right; if ((valuesArray.length * defaultElementSpace) > height) { height = valuesArray.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"); } } 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]); if (secondAxisKeys) var y1 = 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(valuesArray.length)) .tickFormat(function(d, i) { return valuesArray[i].truncatedName; }) .orient("bottom"); var yAxis = d3.svg.axis() .scale(y) .tickSize(-width) .orient("left"); if (secondAxisKeys) var yAxis1 = d3.svg.axis() .scale(y1) .tickSize(width) .orient("right"); // 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)); }); if (secondAxisKeys) var valueLine1 = d3.svg.line() .x(function(d, i) { return x(i); }) .y(function(d) { return y1(chartOptions.value(d)); }); // Domain range of the x,y functions, used for the coordinate ranges x.domain(d3.extent(valuesArray, function(d, i) { return i; })); //Extend finds the min, max elements of an array //console.log(totalMax); y.domain([totalMin, totalMax]); if (secondAxisKeys) y1.domain([totalMin1, totalMax1]); } 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]); if (secondAxisKeys) var y1 = 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(valuesArray.length)) .tickFormat(function(d, i) { return valuesArray[i].truncatedName; }); var yAxis = d3.svg.axis() .scale(y) .orient("bottom") .tickSize(-height); if (secondAxisKeys) var yAxis1 = d3.svg.axis() .scale(y1) .orient("top") .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)); }); if (secondAxisKeys) var valueLine1 = d3.svg.line() .x(function(d, i) { return x(i); }) .y(function(d) { return y1(chartOptions.value(d)); }); // Domain range of the x,y functions, used for the coordinate ranges (input values) x.domain(d3.extent(valuesArray, function(d, i) { return i; })); y.domain([totalMin, totalMax]); if (secondAxisKeys) y1.domain([totalMin1, totalMax1]); } 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 valuesArray[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"); 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 ""; }) .call(yAxis1) .selectAll("text") .attr("stroke", "none"); if (chartOptions.updateXLabel) chartOptions.xLabel = chartOptions.updateXLabel(rowData); if (chartOptions.xLabel) { svg.append("text") .attr("class", "x-label") .attr("text-anchor", "end") .attr("dy", "0.75em") .attr("x", width/2) .attr("y", height + chartOptions.margin.bottom + chartOptions.margin.top) .text(chartOptions.xLabel); } if (chartOptions.yLabel) { svg.append("text") .attr("class", "y-label") .attr("text-anchor", "end") .attr("transform", "rotate(-90)") .attr("dy", "-1.25em") .attr("x", -height/2) .attr("y", -chartOptions.margin.left/2) .text(chartOptions.yLabel); } if (chartOptions.y1Label) { svg.append("text") .attr("class", "y-label") .attr("text-anchor", "end") .attr("transform", "rotate(-90)") .attr("dy", "-0.5em") .attr("x", -height/2) .attr("y", width + chartOptions.margin.right) .text(chartOptions.y1Label); } // Colour generator for multiple lines var colourGen = d3.scale.category20(); var legendData = []; var csvColumns = (chartOptions.xLabel) ? [chartOptions.xLabel] : [" "]; var csvRows = []; $.each(rowData, function(key, data) { var inSecondAxis = (secondAxisKeys.indexOf(key) != -1) ? true : false; $.each(data, function(i, d) { if (!csvRows[i]) { csvRows[i] = []; csvRows[i][0] = chartOptions.name(d); } csvRows[i][csvColumns.length] = chartOptions.value(d); }); csvColumns.push(key); // 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.hasBrackets) ? (chartOptions.legendDataVar.label + " (" + key + ")") : (chartOptions.legendDataVar.label + key) }); valueTooltipContent = function(d) {return chartOptions.valueTooltipContent(d, key)}; } // Draw the line for the values and circles on the specific points var valueLineFunc; if (!inSecondAxis) valueLineFunc = valueLine; else valueLineFunc = valueLine1; svg.append("svg:path") .datum(data) .attr("class", "value-line") .attr("stroke", colour) .attr("d", valueLineFunc); 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") { if (!inSecondAxis) return y(chartOptions.value(d)); else return y1(chartOptions.value(d)); } }) .attr("cy", function(d, i) { if (chartOptions.orientation == "horizontal") { if (!inSecondAxis) return y(chartOptions.value(d)); else return y1(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)); } }); }); // end of each rowData if (!chartOptions.hideLegend) { // 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") .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 d.label; }); } if (reportOptions.enableCSVExport) { var csvFrameID = "linegraph-frame"; $("#" + csvFrameID).remove(); addCSVButton(reportOptions.enableCSVExport, csvFrameID, function() { exportToCSV(csvColumns, csvRows, csvFrameID, encodeURIComponent(chartOptions.graphTitle + ".csv")); }); $("#" + csvFrameID).addClass("csv-right"); } /* $(window).resize(function() { cleanSvg(element); draw(rowData, chartOptions, element, contentElement); }); */ $(window).resize(function() { clearTimeout(this.id); this.id = setTimeout(function() { cleanSvg(element); draw(rowData, chartOptions, element, contentElement, secondAxisKeys); }, config.transitionDuration); }); } // 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, }; };