|
|
|
|
@ -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()
|
|
|
|
|
}
|
|
|
|
|
|