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