datasaur/site/surveyapp/static/graphscripts/piechart.js
2026-01-25 15:56:01 +00:00

324 lines
10 KiB
JavaScript

// ------VARIABLE DECLARATIONS------
const variableSelect = document.querySelector(".x-axis-value")
const againstSelect = document.querySelector(".y-axis-value")
const againstSection = document.querySelector(".y-axis-details")
const againstAggDOM = document.querySelector(".y-axis-aggregation")
const aggregate = document.querySelector(".aggregate")
const emptyGraph = document.querySelector(".empty-graph")
const exportButton = document.querySelector(".export")
// Get the DOM elements for all of the axes, so that we can add event listeners for when they are changed
const axesSettings = document.querySelectorAll(".axis-setting")
// Get the graph data
const data = graphData["chart_data"]
// Set graph dimensions
let width = document.getElementById('graph').clientWidth;
let height = document.getElementById('graph').clientHeight;
// Re set the width and height when the window resizes
window.onresize = function(){
width = document.getElementById('graph').clientWidth;
height = document.getElementById('graph').clientHeight;
svg.attr('width', width).attr('height', height);
}
const svg = d3.select('#graph').append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "none")
// Add a 'graph' group to the svg
const graph = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Add 'labels' group to the graph
graph.append("g")
.attr("class", "labels")
// Define margins around graph for labels
const margin = { top: 20, right: 20, bottom: 20, left: 20 };
// Define the graph width and height to account for margin
const gWidth = width - margin.left - margin.right;
const gHeight = height - margin.top - margin.bottom;
// Define the radius of the pie_chart (half the graph)
const radius = Math.min(gWidth, gHeight) / 2
// Define the 'arc' (i.e. the curve/radius of the pie)
const arc = d3.arc()
.outerRadius(radius * 0.8)
.innerRadius(radius * 0.5);
// The 'labels' will be on a circle with a greater radius than the pie (i.e. just outside)
const labelArc = d3.arc()
.innerRadius(radius)
.outerRadius(radius);
// ------EVENT LISTENERS------
// When the axes are altered, we need to re-group the data depending on the variables set
axesSettings.forEach(setting => {
setting.onchange = function(){
axisChange()
}
})
// If the variable is not an empty string on page load (when user editing a graph)
if(variableSelect.options[variableSelect.selectedIndex].value != ''){
axisChange()
}
// Export button that allows user to export and download the SVG as a PNG image
exportButton.addEventListener("click", () => {
let title = document.querySelector(".title").value
let exportTitle = title == "" ? "plot.png": `${title}.png`
saveSvgAsPng(document.getElementsByTagName("svg")[0], exportTitle, {scale: 2, backgroundColor: "#FFFFFF"});
})
// Called whenever the axis variables change. Handles display of DOM elements before drawing graph
function axisChange (){
let variableValue = variableSelect.options[variableSelect.selectedIndex].value;
let againstValue = againstSelect.options[againstSelect.selectedIndex].value;
let againstAgg = againstAggDOM.options[againstAggDOM.selectedIndex].value;
// Hide the overlay
emptyGraph.classList.remove("visible");
emptyGraph.classList.add("invisible");
// Remove the ' -- select an option -- ' option
variableSelect.firstChild.hidden = true;
// Reveal the y-axis variable for the user to select
againstSection.classList.remove('hidden-down')
// If the chosen y variable is equal to 'Amount' then we don't want to give the user the option to perform data aggregations
if(againstValue != 'Amount'){
aggregate.classList.remove('hidden-down')
aggregate.classList.add('visible')
} else{
aggregate.classList.remove('visible')
aggregate.classList.add('hidden-down')
}
// A function that carries ou the grouping, based on the chosen settings
let groupedData = groupData(variableValue, againstValue);
// re-draw the graph with the chosen variables
render(groupedData, variableValue, againstValue, againstAgg);
}
// Data grouping function. Called when an axis variable is changed
function groupData(variableValue, againstValue){
// We can create a 'nested' D3 object, with the key as the chosen x-axis variable
let nestedData = d3.nest().key(function(d) { return d[variableValue]; })
// If the y axis is just to count the values, we can group and perform a roll up on the calculation of the length
if(againstValue == "Amount"){
return nestedData
.rollup(function(v) { return v.length; })
.entries(data)
}
// Else, we need to see which y-axis aggregation was chosen
let againstAgg = againstAggDOM.options[againstAggDOM.selectedIndex].value;
if(againstAgg == "Average"){
return nestedData
.rollup(function(v) { return d3.mean(v, function(d) { return d[againstValue]; }); })
.entries(data)
}
if(againstAgg == "Highest"){
return nestedData
.rollup(function(v) { return d3.max(v, function(d) { return d[againstValue]; }); })
.entries(data)
}
if(againstAgg == "Lowest"){
return nestedData
.rollup(function(v) { return d3.min(v, function(d) { return d[againstValue]; }); })
.entries(data)
}
if(againstAgg == "Sum"){
return nestedData
.rollup(function(v) { return d3.sum(v, function(d) { return d[againstValue]; }); })
.entries(data)
}
}
// Function that draws the pie chart
function render(groupedData, variableValue, againstValue, againstAgg) {
// Specify the values and keys to be used by the graph
const keys = d => d.data.data.key;
const values = d => d.value;
// set the colour scale, using D3 spectral scheme
let colour = d3.scaleOrdinal(d3.schemeSet3)
.domain(groupedData)
// Compute the position of each group on the pie:
let pie = d3.pie()
.value(values)
.sort(null);
let pieData = pie(groupedData)
// Add percentages to pieData
let total = d3.sum(pieData, values);
const percentage = d => Math.round((d.value / total) * 100) + "%";
// map the segments to the data
let segments = graph.selectAll("path")
.data(pieData)
// Initialise each segment
let initSegment = segments
.enter()
.append('path')
setTooltip(initSegment)
// Finally merge the segments
initSegment.merge(segments)
.transition()
.duration(750)
// Custom function for transitioning, needed for pie charts due to the 'sweep-flag' and 'large-arc-flag'
// See stackoverflow here https://stackoverflow.com/questions/21285385/d3-pie-chart-arc-is-invisible-in-transition-to-180
.attrTween("d", function(d) {
let transition = d3.interpolate(this.angle, d);
this.angle = transition(0);
return function(t) {
return arc(transition(t));
};
})
.attr('fill', function(d){ return(colour(d.data.key)) })
.attr("stroke", "white")
.style("stroke-width", "4px")
.style("opacity", 1)
// remove the groups that are not present anymore
segments
.exit()
.remove()
// Add the labels next to the piechart
let text = graph.select(".labels").selectAll("text")
.data(pie(pieData));
text.enter()
.append("text")
.style("font-size", "0.8rem")
.merge(text)
.transition()
.duration(750)
.text(percentage)
.attr("transform", function(d) {return "translate(" + labelArc.centroid(d) + ")"; })
.style("text-anchor", "middle")
text.exit()
.remove();
// And now add the legend title
let legendTitle = againstValue == 'Amount' ? variableValue : `${againstAgg} ${againstValue} of ${variableValue}`
// Add the legend with the specified title and associated colours
addLegend(legendTitle, colour, pieData, percentage)
}
function setTooltip(initSegment){
// Then add the tooltip on hover affect
initSegment.on("mouseenter", function(d){
d3.select(this)
.transition()
.duration(200)
.style('opacity', '0.5')
d3.select(".graph-tooltip")
.style("left", d3.event.pageX + 20 + "px")
.style("top", d3.event.pageY + "px")
.style("opacity", 1)
// .classed("tooltip-hidden", false)
.select(".tooltip-value")
.text(d.data.key + ": " + d.value)
})
.on('mouseout', function() {
d3.select(this)
.transition()
.duration(200)
.style('opacity', '1')
d3.select(".graph-tooltip")
.style("opacity", 0)
})
}
// Add the legend, corresponding to the pie chart
function addLegend(legendTitle, colour, pieData, percentage){
// Define size for the legend. These numbers fit nicely on the chart
let legendRectSize = 18;
let legendSpacing = 4;
let legendFontsize = "1rem";
// THE FIRST ELEMENT IN COLOUR.DOMAIN() IS AN UNWANTED OBJECT SO IT IS REMOVED
let legendData = colour.domain().slice(1)
// If there are lots of elements in the pie chart (22 is cutoff that can fit) then
// half the size of the legend elements
if(legendData.length > 22){
legendRectSize = legendRectSize/2
legendSpacing = legendSpacing/2
legendFontsize = "0.5rem"
}
// Remove the legend and title before redrawing it
svg.selectAll(".legend").remove()
svg.selectAll(".legend-title").remove()
let legend = graph.selectAll("#graph")
.data(legendData)
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
let height = legendRectSize + legendSpacing;
// The vertical position is distance from the center
// I also add 1 x 'height' onto the value to make space for legend title
let vert = height + (i * height - (gHeight/2));
return 'translate(' + (-gWidth/2) + ',' + vert + ')';
});
// Add the title for the legend
graph.append("text")
.attr("class", "legend-title")
.attr("x", -gWidth / 2)
.attr("y", -gHeight / 2)
.attr("text-anchor", "left")
.style("font-size", "1rem")
.style("text-decoration", "underline")
.text(legendTitle);
// Add the 'rect' for the coloured squares
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', colour)
.style('stroke', colour)
// Add the text for each variable in the legend
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.style("font-size", legendFontsize)
.text(d => d)
}
// When the form is submitted, we want to get a jpg image of the svg
$('form').submit(function (e) {
// prevent default form submission
e.preventDefault();
// call function to post form (separate js file)
postgraph(width, height)
});