diff --git a/public/app.js b/public/app.js index fa806ce..5fe9fd5 100644 --- a/public/app.js +++ b/public/app.js @@ -1,4 +1,3 @@ -// TODO - styling const WIDTH = 800; const HEIGHT = 600; const parseTime = d3.timeParse("%B %e, %Y"); @@ -12,6 +11,11 @@ let yAxis = 'outcomes' let bottomAxis; let leftAxis; let zoomScale = 1 +let averageOutcomesArray +let sortedInstances; +let displayAverage = false; +let displayStandardDeviation = false; +let displayInstances = true; const randomColor = ()=>{ const red = Math.floor(Math.random()*128) + 64; @@ -84,7 +88,7 @@ const renderPoints = () => { const metro = metros.find(m => m.metro === instanceMetro) const course = courses.find(c => c.course === instanceCourse) - if(metro.checked && course.checked){ + if(displayInstances && metro.checked && course.checked){ return 'block' } else { return 'none' @@ -107,10 +111,9 @@ const renderPoints = () => { } const setupGraph = ()=>{ - d3.select('svg'); - d3.select('svg') - .style('width', WIDTH) - .style('height', HEIGHT); + d3.select('#container') + .attr('width', WIDTH) + .attr('height', HEIGHT); xScale = d3.scaleTime(); xScale.range([0,WIDTH]); @@ -176,6 +179,7 @@ const populateMetrosCoursesCheckboxes = ()=>{ .on('click', (event, datum)=>{ datum.checked = !datum.checked renderPoints() + displayMeanStandardDeviation() }) d3.select('#metros ul') @@ -192,6 +196,7 @@ const populateMetrosCoursesCheckboxes = ()=>{ .on('click', (event, datum)=>{ datum.checked = !datum.checked renderPoints() + displayMeanStandardDeviation() }) d3.select('#courses button:nth-child(2)') @@ -200,6 +205,7 @@ const populateMetrosCoursesCheckboxes = ()=>{ course.checked = true } renderPoints() + displayMeanStandardDeviation() populateMetrosCoursesCheckboxes() }) d3.select('#courses button:nth-child(3)') @@ -208,6 +214,7 @@ const populateMetrosCoursesCheckboxes = ()=>{ course.checked = false } renderPoints() + displayMeanStandardDeviation() populateMetrosCoursesCheckboxes() }) @@ -217,6 +224,7 @@ const populateMetrosCoursesCheckboxes = ()=>{ metro.checked = true } renderPoints() + displayMeanStandardDeviation() populateMetrosCoursesCheckboxes() }) d3.select('#metros button:nth-child(3)') @@ -225,6 +233,7 @@ const populateMetrosCoursesCheckboxes = ()=>{ metro.checked = false } renderPoints() + displayMeanStandardDeviation() populateMetrosCoursesCheckboxes() }) } @@ -239,6 +248,135 @@ const createRadioButtonHanlders = ()=>{ setupGraph() createAxes() renderPoints() + displayMeanStandardDeviation(); + }) +} + +const setUpZoomPan = ()=>{ + const zoomCallback = (event) => { + d3.select('#points').attr("transform", event.transform); + d3.select('#x-axis') + .call(bottomAxis.scale(event.transform.rescaleX(xScale))); + d3.select('#y-axis') + .call(leftAxis.scale(event.transform.rescaleY(yScale))); + + if(event.transform.k !== zoomScale){ + zoomScale = event.transform.k + d3.selectAll('circle') + .attr('r', datum => (datum.color ? 10 : 5) / zoomScale); + } + } + + const zoom = d3.zoom() + .on('zoom', zoomCallback); + d3.select('#container').call(zoom); +} + +const getAverage = (start, end) => { + let sumOutcomes = 0 + let sumDropped = 0 + for(let i = start; i < end; i++){ + sumOutcomes += sortedInstances[i].ninety_day_outcomes/sortedInstances[i].graduates*100 + sumDropped += sortedInstances[i].dropped/sortedInstances[i].total_students*100 + } + const averageOutcomes = sumOutcomes/(end-start) + const averageDropped = sumDropped/(end-start) + + let sumOutcomesDifferences = 0 + let sumDroppedDifferences = 0 + for(let i = start; i < end; i++){ + sumOutcomesDifferences += Math.pow(sortedInstances[i].ninety_day_outcomes/sortedInstances[i].graduates*100 - averageOutcomes,2) + sumDroppedDifferences += Math.pow(sortedInstances[i].dropped/sortedInstances[i].total_students*100 - averageDropped,2) + } + const stdDevOutcomes = Math.sqrt(sumOutcomesDifferences/(end-start)) + const stdDevDropped = Math.sqrt(sumDroppedDifferences/(end-start)) + + averageOutcomesArray.push({ + averageOutcomes, + averageDropped, + stdDevOutcomes, + stdDevDropped, + initialGraduationDate:sortedInstances[start].graduation_date + }) +} + +const displayMeanStandardDeviation = () => { + sortedInstances = instances.sort((a,b) => Date.parse(a.graduation_date) - Date.parse(b.graduation_date)) + sortedInstances = sortedInstances.filter(datum=> { + const instanceMetro = datum.course.split('-')[0] + const instanceCourse = datum.course.split('-')[1] + const metro = metros.find(m => m.metro === instanceMetro) + const course = courses.find(c => c.course === instanceCourse) + if(metro.checked && course.checked){ + return true + } else { + return false + } + }) + + averageOutcomesArray = [] + for(let i = 0; i < sortedInstances.length-10; i += 10){ + getAverage(i,i+10) + } + + d3.selectAll('#points path').remove(); + + if(displayAverage){ + + d3.select('#points') + .append('path') + .datum(averageOutcomesArray) + .attr('fill', 'none') + .attr('stroke', 'steelblue') + .attr('stroke-width', 2) + .attr('opacity', 0.7) + .attr('d', d3.line() + .x(d => xScale(parseTime(d.initialGraduationDate))) + .y(d => (yAxis === 'outcomes') ? yScale(d.averageOutcomes) : yScale(d.averageDropped)) + ) + } + if(displayStandardDeviation){ + d3.select('#points') + .append('path') + .datum(averageOutcomesArray) + .attr('fill', 'none') + .attr('stroke', 'red') + .attr('stroke-width', 2) + .attr('opacity', 0.7) + .attr('d', d3.line() + .x(d => xScale(parseTime(d.initialGraduationDate))) + .y(d => (yAxis === 'outcomes') ? yScale(d.averageOutcomes+d.stdDevOutcomes) : yScale(d.averageDropped+d.stdDevDropped)) + ) + + d3.select('#points') + .append('path') + .datum(averageOutcomesArray) + .attr('fill', 'none') + .attr('stroke', 'red') + .attr('stroke-width', 2) + .attr('opacity', 0.7) + .attr('d', d3.line() + .x(d => xScale(parseTime(d.initialGraduationDate))) + .y(d => (yAxis === 'outcomes') ? yScale(d.averageOutcomes-d.stdDevOutcomes) : yScale(d.averageDropped-d.stdDevDropped)) + ) + } +} + +const setUpDisplayAverageHandler = ()=>{ + d3.select('#average input:nth-child(2)') + .on('click', (event)=>{ + displayAverage = event.target.checked + displayMeanStandardDeviation() + }) + d3.select('#average input:nth-child(3)') + .on('click', (event)=>{ + displayStandardDeviation = event.target.checked + displayMeanStandardDeviation() + }) + d3.select('#average input:nth-child(4)') + .on('click', (event)=>{ + displayInstances = event.target.checked + renderPoints() }) } @@ -262,21 +400,8 @@ window.onload = async ()=>{ renderPoints(); createFormSubmissionHandler(); createRadioButtonHanlders(); - const zoomCallback = (event) => { - d3.select('#points').attr("transform", event.transform); - d3.select('#x-axis') - .call(bottomAxis.scale(event.transform.rescaleX(xScale))); - d3.select('#y-axis') - .call(leftAxis.scale(event.transform.rescaleY(yScale))); + setUpZoomPan(); - if(event.transform.k !== zoomScale){ - zoomScale = event.transform.k - d3.selectAll('circle') - .attr('r', datum => (datum.color ? 10 : 5) / zoomScale); - } - } - - const zoom = d3.zoom() - .on('zoom', zoomCallback); - d3.select('#container').call(zoom); + displayMeanStandardDeviation(); + setUpDisplayAverageHandler() } diff --git a/public/index.html b/public/index.html index 81be8c0..53427e0 100644 --- a/public/index.html +++ b/public/index.html @@ -23,6 +23,15 @@ 90 Day Outcomes % Dropped % +
+

Display Average?

+ + Display Average? + + Display Standard Deviation? + + Display Instances? +

Choose Metros To Display