// VARiABLE DECLARATIONS // 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") const xAxisSelect = document.querySelector(".x-axis-value") const yAxisSelect = document.querySelector(".y-axis-value") const yAxisDetails = document.querySelector(".y-axis-details") const emptyGraph = document.querySelector(".empty-graph") const exportButton = document.querySelector(".export") // Connecting line (if user wants to make a line graph) const addLine = document.querySelector(".add-line") const axesRange = document.querySelectorAll(".axis-range") // x and y axis ranges const xFrom = document.querySelector(".x-from") const yFrom = document.querySelector(".y-from") const xTo = document.querySelector(".x-to") const yTo = document.querySelector(".y-to") // Colours for our graph const fill = "steelblue" const hoverFill = "#2D4053" const stroke = "steelblue" // Get the graph data const data = graphData["chart_data"] // Set graph dimensions var width = document.getElementById('graph').clientWidth; var height = document.getElementById('graph').clientHeight; // Re set graph size when window changes size window.onresize = function(){ width = document.getElementById('graph').clientWidth; height = document.getElementById('graph').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 axes 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 var graph = svg.append('g').attr("transform", `translate(${margin.left}, ${margin.top})`) // EVENT LISTENERS // Function required to activate the 'help' tooltip on the axis $(function () { $("[data-toggle='help']").tooltip(); }); // If the x-axis is not empty (i.e. if user is editing graph) then call function immediately 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"}); }) // When the axes are altered, we need to re-group the data depending on the variables set axesSettings.forEach(setting => { setting.onchange = function(){ axisChange() } }) // Whenever the range changes we want to draw the graph (without having to re-get the data) axesRange.forEach(input => { input.onchange = function() { render(data) } }) // Event listener for adding a connecting line addLine.addEventListener("change", function(){ if(this.checked){ d3.selectAll('.graph-line').transition().duration(1000).style("visibility", "visible"); } else { d3.selectAll('.graph-line').transition().duration(1000).style("visibility", "hidden"); } render(data) }) // FUNCTIONS FOR RENDERING GRAPH function axisChange (){ let xAxisValue = xAxisSelect.options[xAxisSelect.selectedIndex].value; let yAxisValue = yAxisSelect.options[yAxisSelect.selectedIndex].value; // Remove the ' -- select an option -- ' option xAxisSelect.firstChild.hidden = true; yAxisSelect.firstChild.hidden = true; // Reveal the y-axis variable for the user to select yAxisDetails.classList.remove('hidden-down') // If the user has selected variables for both the x and the y axes if (xAxisValue != "" && yAxisValue != ""){ // Make the overlay hidden emptyGraph.classList.remove("visible"); emptyGraph.classList.add("invisible"); addLine.disabled = false; // re-draw the graph with the chosen variables render(data); } } function render(data){ let xAxisValue = xAxisSelect.options[xAxisSelect.selectedIndex].value; let yAxisValue = yAxisSelect.options[yAxisSelect.selectedIndex].value; // Specify the x-axis values and the y-axis values const xValues = d => d[xAxisValue]; const yValues = d => d[yAxisValue]; // Remove old axes labels (if they exist) d3.selectAll('.label').remove(); // sort the grouped data keys in ascending order (i.e. so the x-axis is in numerical order) // data.sort(function(a, b) { return d3.ascending(parseInt(a.key), parseInt(b.key))}); data.sort(function(a, b) { return d3.ascending(parseInt(a[xAxisValue]), parseInt(b[xAxisValue])) }); // set the input fields for the domain (i.e. range of values) if not yet set if(xFrom.value == "") xFrom.value = d3.min(data, xValues) if(xTo.value == "") xTo.value = d3.max(data, xValues) if(yFrom.value == "") yFrom.value = d3.min(data, yValues) if(yTo.value == "") yTo.value = d3.max(data, yValues) // Now extract the range from the values (if they are specifed by user) // If the values specified by the user are outside the range of the data, increase the range // else use the range of the data as default. xFromValue = xFrom.value = Math.min(d3.min(data, xValues), xFrom.value) xToValue = xTo.value = Math.max(d3.max(data, xValues), xTo.value) yFromValue = yFrom.value = Math.min(d3.min(data, yValues), yFrom.value) yToValue = yTo.value = Math.max(d3.max(data, yValues), yTo.value) // Reveal the 'add-line' select option document.querySelector(".form-add-line").classList.remove("invisible") // Set the scale for the x-axis const xScale = d3.scaleLinear() .domain([xFromValue, xToValue]).nice() .range([0, gWidth]) // Set the scale for the y-axis const yScale = d3.scaleLinear() .domain([yFromValue, yToValue]) .range([gHeight, 0]) // Select the axes (if they exist) var yAxis = d3.selectAll(".yAxis") var xAxis = d3.selectAll(".xAxis") // Get the position of the axes. Either set to 0 or set to the far left/bottom xPosition = (0 > yFromValue && 0 < yToValue) ? yScale(0) : gHeight yPosition = (0 > xFromValue && 0 < xToValue) ? xScale(0) : 0 // 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, ${xPosition})`) // For why 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)) .attr("transform", `translate(${yPosition}, 0)`) } else { // Adjust the x-axis according the x-axis variable data xAxis.transition() .duration(1000) .call(d3.axisBottom(xScale)) .attr("transform", `translate(0, ${xPosition})`) // Adjust the y-axis according the y-axis variable data yAxis.transition() .duration(1000) .call(d3.axisLeft(yScale)) .attr("transform", `translate(${yPosition}, 0)`) } // 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(yAxisValue); // 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); var line = d3.line() .x(d => xScale(xValues(d))) .y(d => yScale(yValues(d))) // .curve(d3.curveCatmullRom.alpha(0.5)); var startingLine = d3.line() .x(d => xScale(xValues(d))) .y(d => yScale(0)) var path = graph.selectAll('.graph-line').data(data) if(addLine.checked == true){ path .enter() .append("path") .attr("class","graph-line") .merge(path) .transition() .duration(1000) .attr("d", line(data)) .attr("fill", "none") .attr("stroke", stroke) .attr("stroke-width", 2.5) } // Select all 'circle' DOM elements (if they exist) var circle = graph.selectAll('circle').data(data) // 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 circle.exit().transition() .duration(1000) .attr("cy", yScale(0)) .remove() // D3 'enter()' is the creation of DOM elements bound to the data var plot = circle.enter() .append('circle') .attr("cy", yScale(0)) .attr('cx', d => xScale(xValues(d))) .attr("r", 3) .style('fill', fill) setTooltip(plot, yValues) plot.merge(circle) // 'merge' merges the 'enter' and 'update' groups .transition() .delay(d => xScale(xValues(d))/2 ) .duration(1000) .attr('cy', d=> yScale(yValues(d))) .attr('cx', d => xScale(xValues(d))) } function setTooltip(plot, yValues){ plot.on('mouseenter', function(d) { d3.select(this) .transition() .duration(100) .style('fill', hoverFill) var tooltipOffset = (d3.select(this).attr("width") - 80)/2; var tooltip = d3.select(".graph-tooltip") // To position the tool tip when the user hovers. Use the window and calculate the offset var position = this.getScreenCTM() .translate(+ this.getAttribute("cx"), + this.getAttribute("cy")); // Now give the tooltip the data it needs to show and the position it should be. tooltip.html(yValues(d)) .style("left", (window.pageXOffset + position.e + tooltipOffset) + "px") // Center it horizontally over the plot .style("top", (window.pageYOffset + position.f - 80) + "px"); // Shift it 40 px above the plot tooltip.classed("tooltip-hidden", false) }).on('mouseout', function() { d3.select(this) .transition() .duration(100) .style('fill', fill) d3.select(".graph-tooltip").classed("tooltip-hidden", true); }) } // JQUERY functions // Function that will confirm user input when they press enter, without submitting the form $('body').on('keydown', 'input, select', function(e) { if (e.key === "Enter") { $(this).blur() } }); // 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) });