324 lines
10 KiB
JavaScript
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)
|
|
});
|