585 lines
20 KiB
JavaScript
Executable File
585 lines
20 KiB
JavaScript
Executable File
/*
|
|
* 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,
|
|
};
|
|
|
|
};
|