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