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

486 lines
18 KiB
JavaScript
Executable File

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