Files
gtav-src/tools_ng/web/cert/shared/js/LinesGraph.js
T
2025-09-29 00:52:08 +02:00

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,
};
};