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

272 lines
10 KiB
JavaScript

// ------VARIABLE DECLARATIONS------
const xAxisSelect = document.querySelector(".x-axis-value")
const yAxisSelect = document.querySelector(".y-axis-value")
const yAxisDetails = document.querySelector(".y-axis-details")
const yAxisAggDom = 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")
// Colours for our graph
const fill = "steelblue"
const hoverFill = "#2D4053"
// Get the graph data
const data = graphData["chart_data"]
// Get the DOM element that will hold the SVG
const graphDOM = document.getElementById('graph')
// Get the width and height of the SVG on the client screen
let width = graphDOM.clientWidth;
let height = graphDOM.clientHeight;
// Re-set the width and height on window resize
window.onresize = function(){
width = graphDOM.clientWidth;
height = graphDOM.clientHeight;
svg.attr('width', width).attr('height', height);
}
// Set margins around graph for axis and labels
const margin = { top: 20, right: 20, bottom: 60, left: 80 };
// Set the graph width and height to account for margins
const gWidth = width - margin.left - margin.right;
const gHeight = height - margin.top - margin.bottom;
// Create SVG ready for graph
const svg = d3.select('#graph')
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", "0 0 " + width + " " + height)
.attr("preserveAspectRatio", "none")
// Add the graph area to the SVG, factoring in the margin dimensions
let graph = svg.append('g').attr("transform", `translate(${margin.left}, ${margin.top})`)
// ------END OF VARIABLE DECLARATIONS------
// ------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 value is not equal to an empty string on page load (i.e. user is editing graph)
if(xAxisSelect.options[xAxisSelect.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"});
})
// ------FUNCTIONS------
function axisChange (){
// Get the selected values
let xAxisValue = xAxisSelect.options[xAxisSelect.selectedIndex].value;
let yAxisValue = yAxisSelect.options[yAxisSelect.selectedIndex].value;
let yAxisAgg = yAxisAggDom.options[yAxisAggDom.selectedIndex].value;
// Hide the overlay if it is still present
emptyGraph.classList.remove("visible");
emptyGraph.classList.add("invisible");
// Remove the ' -- select an option -- ' option
xAxisSelect.firstChild.hidden = true;
// Reveal the y-axis variable for the user to select
yAxisDetails.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(yAxisValue != 'Amount'){
aggregate.classList.remove('hidden-down')
aggregate.classList.add('visible')
} else{
aggregate.classList.remove('visible')
aggregate.classList.add('hidden-down')
}
// Get the grouped data based on the chose variables
let groupedData = groupData(xAxisValue, yAxisValue);
// draw the graph with the chosen variables
render(groupedData, xAxisValue, yAxisValue, yAxisAgg);
}
// Data grouping function. Called whenever an axis setting changes
function groupData(xAxisValue, yAxisValue){
// 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[xAxisValue]; })
// 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(yAxisValue == "Amount"){
return nestedData
.rollup(function(v) { return v.length; })
.entries(data)
}
// Else, we need to see which y-axis aggregation was chosen
let yAxisAgg = yAxisAggDom.options[yAxisAggDom.selectedIndex].value;
if(yAxisAgg == "Average"){
return nestedData
.rollup(function(v) { return d3.mean(v, function(d) { return d[yAxisValue]; }); })
.entries(data)
}
if(yAxisAgg == "Highest"){
return nestedData
.rollup(function(v) { return d3.max(v, function(d) { return d[yAxisValue]; }); })
.entries(data)
}
if(yAxisAgg == "Lowest"){
return nestedData
.rollup(function(v) { return d3.min(v, function(d) { return d[yAxisValue]; }); })
.entries(data)
}
if(yAxisAgg == "Sum"){
return nestedData
.rollup(function(v) { return d3.sum(v, function(d) { return d[yAxisValue]; }); })
.entries(data)
}
}
// Function that draws the graph
function render(groupedData, xAxisValue, yAxisValue, yAxisAgg){
// Specify the x-axis values and the y-axis valus
const xValues = d => d.key;
const yValues = d => d.value;
// Remove old axes labels (if they exist)
d3.selectAll('.label').remove();
// sort the grouped data keys in ascending order (so the x-axis is in numerical order)
groupedData.sort(function(a, b) { return d3.ascending(parseInt(a.key), parseInt(b.key))});
// Set the scale for the x-axis (domain is the range of our data, range is the physical width of the graph)
const xScale = d3.scaleBand()
.domain(groupedData.map(xValues))
.range([0, gWidth])
.paddingInner(0.1)
// Set the scale for the y-axis
const yScale = d3.scaleLinear()
.domain([0, d3.max(groupedData, yValues)]).nice()
.range([gHeight, 0])
// Select the axes (if they exist)
let yAxis = d3.selectAll(".yAxis")
let xAxis = d3.selectAll(".xAxis")
// If they dont exist, we create them. If they do, we update them
if (yAxis.empty() && xAxis.empty()){
// For the x-axis we create an axisBottom and 'translate' it so it appears on the bottom of the graph
graph.append('g').attr("class", "xAxis").call(d3.axisBottom(xScale))
.attr("transform", `translate(0, ${gHeight})`)
// For y axis we do not need to translate it, as the default is on the left
graph.append('g').attr("class", "yAxis").call(d3.axisLeft(yScale))
} else {
// Adjust the x-axis according the x-axis variable data
xAxis.transition().duration(1000).call(d3.axisBottom(xScale))
// Adjust the y-axis according the y-axis variable data
yAxis.transition().duration(1000).call(d3.axisLeft(yScale))
}
// If the yAxis is 'Amount' we can leave the label as it is, otherwise we need to add the type of aggregation
let yAxisLabel = yAxisValue == 'Amount' ? 'Amount' : `${yAxisAgg}: ${yAxisValue}`
// Add y axis label
svg.append("text")
.attr("transform", "rotate(-90)")
.attr("class", "label")
.attr("y", 0)
.attr("x",0 - (gHeight / 2))
.attr("dy", "1em")
.style("text-anchor", "middle")
.text(yAxisLabel);
// Add x axis label
svg.append("text")
.attr("transform",`translate(${width/2}, ${gHeight + margin.top + 55})`)
.attr("class", "label")
.style("text-anchor", "middle")
.text(xAxisValue);
// Select all 'rect' DOM elements (if they exist)
let rect = graph.selectAll('rect').data(groupedData)
// D3 'exit()' is what happens to DOM elements that no longer have data bound to them
// Given a transition that shrinks them down to the x-axis before removing
rect.exit().transition()
.duration(1000)
.attr("y", yScale(0))
.attr('height', 0)
.remove()
// D3 'enter()' is the creation of DOM elements bound to the data
// At this stage, the bars are all flat along the x-axis
let bar = rect.enter()
.append('rect')
.attr("y", yScale(0))
.attr('x', d => xScale(xValues(d)))
.attr('width', xScale.bandwidth()) // band width is width of a single bar
.style('fill', fill)
// Tooltip needs to be set before merging?????
setTooltip(bar, xValues, yValues)
// Finally, we merge the newly entered bars with the existing ones.
// (i.e. merges the 'enter and 'update' groups)
bar.merge(rect) // 'merge' merges the 'enter' and 'update' groups
.transition()
.delay(d => xScale(xValues(d))/2 )
.duration(1000)
.attr('height', d => gHeight - yScale(yValues(d)))
.attr('y', d=> yScale(yValues(d)))
.attr('x', d => xScale(xValues(d)))
.attr('width', xScale.bandwidth()) // band width is width of a single bar
}
// Function that sets tooltip over each bar when hovered over
function setTooltip(bar, xValues, yValues){
bar.on('mouseenter', function(d) {
d3.select(this)
.transition()
.duration(100)
.style('fill', hoverFill)
let tooltip = d3.select(".graph-tooltip")
// 80 chosen to position the tooltip above bars
let tooltipOffset = (d3.select(this).attr("width") - 80)/2;
// To position the tool tip when the user hovers. Use the window and calculate the offset
let position = this.getScreenCTM()
.translate(+ this.getAttribute("x"), + this.getAttribute("y"));
// Now give the tooltip the data it needs to show and the position it should be.
tooltip.html(xValues(d)+": " + yValues(d))
.style("left", (window.pageXOffset + position.e + tooltipOffset) + "px") // Center it horizontally over the bar
.style("top", (window.pageYOffset + position.f - 50) + "px"); // Shift it 50 px above the bar
// Finally remove the 'hidden' class
tooltip.classed("tooltip-hidden", false)
}).on('mouseout', function() {
d3.select(this)
.transition()
.duration(100)
.style('fill', fill)
// When the mouse is removed from the bar we can add the hidden class to the tooltip again
d3.select(".graph-tooltip").classed("tooltip-hidden", true);
})
}
// 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)
});