diff --git a/public/app.js b/public/app.js index ac2b86c..92eb339 100644 --- a/public/app.js +++ b/public/app.js @@ -1,3 +1,4 @@ +// TODO - zoom/pan on graph const WIDTH = 800; const HEIGHT = 600; const parseTime = d3.timeParse("%B %e, %Y"); diff --git a/public/app.js.orig b/public/app.js.orig new file mode 100644 index 0000000..1a1938d --- /dev/null +++ b/public/app.js.orig @@ -0,0 +1,278 @@ +<<<<<<< HEAD +======= +// TODO - zoom/pan on graph +>>>>>>> 01a00149b25d7d1ed5b3d04ac16a1a1956d8dd94 +const WIDTH = 800; +const HEIGHT = 600; +const parseTime = d3.timeParse("%B %e, %Y"); + +let instances; +let xScale, yScale; +let highlighted = [] +let courses = [] +let metros = [] +let yAxis = 'outcomes' +let bottomAxis; +let leftAxis; + +const randomColor = ()=>{ + const red = Math.floor(Math.random()*128) + 64; + const green = Math.floor(Math.random()*128) + 64; + const blue = Math.floor(Math.random()*128) + 64; + return `rgb(${red}, ${green}, ${blue})` +} + +const renderTable = () => { + let trs = d3.select('tbody') + .selectAll('tr') + .data(highlighted, d => d.instance_id) + + trs.exit().remove() + + trs = trs + .enter() + .append('tr') + .style('background-color', datum => datum.color) + + trs.selectAll('td') + .data(d => [ + d.instance_id, + d.course, + d.graduation_date, + d.total_students, + `${d.dropped} (${Math.floor(d.dropped/d.total_students*100)}%)`, + d.graduates, + `${d.ninety_day_outcomes} (${Math.floor(d.ninety_day_outcomes/d.graduates*100)}%)` + ], d => d.instance_id) + .enter() + .append('td') + .text(value => value) + + trs.append('td') + .append('button') + .text('Deselect') + .on('click', (event, datum) =>{ + event.preventDefault(); + delete datum.color + highlighted = highlighted.filter(i => i.instance_id != datum.instance_id) + renderPoints() + renderTable() + }) + +} + +const renderPoints = () => { + d3.select('#points') + .selectAll('circle') + .data(instances, d => d.id) + .enter() + .append('circle'); + + d3.selectAll('circle') + .attr('cy', (datum, index) => { + if(yAxis === 'outcomes'){ + return yScale(datum.ninety_day_outcomes/datum.graduates*100) + } else { + return yScale(datum.dropped/datum.total_students*100) + } + }) + .attr('cx', (datum, index) => xScale(parseTime(datum.graduation_date)) ) + .attr('fill', datum => datum.color? datum.color : 'black') + .attr('r', datum => datum.color? 10 : 5) + .style('display', (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 'block' + } else { + return 'none' + } + }) + .on('click', (event, datum) => { + const found = highlighted.find(i => i.instance_id === datum.instance_id) + + if(found === undefined){ + datum.color = randomColor() + highlighted.push(datum) + } else { + delete datum.color + highlighted = highlighted.filter(i => i.instance_id != datum.instance_id) + } + renderPoints() + renderTable() + }); + +} + +const setupGraph = ()=>{ + d3.select('svg'); + d3.select('svg') + .style('width', WIDTH) + .style('height', HEIGHT); + + xScale = d3.scaleTime(); + xScale.range([0,WIDTH]); + const xDomain = d3.extent(instances, (datum, index) => { + return parseTime(datum.graduation_date); + }); + xScale.domain(xDomain); + + yScale = d3.scaleLinear(); + yScale.range([HEIGHT, 0]); + const yDomain = d3.extent(instances, (datum, index) => { + if(yAxis === 'outcomes'){ + return datum.ninety_day_outcomes/datum.graduates*100; + } else { + return datum.dropped/datum.total_students*100; + } + }) + yScale.domain(yDomain); +} + +const createAxes = () => { + bottomAxis = d3.axisBottom(xScale); + d3.select('#container') + .append('g') + .attr('id', 'x-axis') + .call(bottomAxis) + .attr('transform', 'translate(0,'+HEIGHT+')'); + + leftAxis = d3.axisLeft(yScale); + d3.select('#container') + .append('g') + .attr('id', 'y-axis') + .call(leftAxis); +} + +const createFormSubmissionHandler = () => { + d3.select('form').on('submit', (event)=>{ + event.preventDefault(); + const instanceID = parseInt(d3.select('input[type="text"]').property('value')) + const found = instances.find(i => i.instance_id === instanceID) + if(found !== undefined && highlighted.find(instance => instance.instance_id === instanceID) === undefined){ + found.color = randomColor() + highlighted.push(found); + } + renderTable(); + renderPoints(); + }); +} + +const populateMetrosCoursesCheckboxes = ()=>{ + + d3.select('#courses ul') + .selectAll('li') + .data(courses) + .enter() + .append('li') + .text(d => d.course) + .append('input') + + d3.selectAll('#courses ul li input') + .attr('type', 'checkbox') + .property('checked', d => d.checked) + .on('click', (event, datum)=>{ + datum.checked = !datum.checked + renderPoints() + }) + + d3.select('#metros ul') + .selectAll('li') + .data(metros) + .enter() + .append('li') + .text(d => d.metro) + .append('input') + + d3.selectAll('#metros ul li input') + .attr('type', 'checkbox') + .property('checked', d => d.checked) + .on('click', (event, datum)=>{ + datum.checked = !datum.checked + renderPoints() + }) + + d3.select('#courses button:nth-child(2)') + .on('click', ()=>{ + for(course of courses){ + course.checked = true + } + renderPoints() + populateMetrosCoursesCheckboxes() + }) + d3.select('#courses button:nth-child(3)') + .on('click', ()=>{ + for(course of courses){ + course.checked = false + } + renderPoints() + populateMetrosCoursesCheckboxes() + }) + + d3.select('#metros button:nth-child(2)') + .on('click', ()=>{ + for(metro of metros){ + metro.checked = true + } + renderPoints() + populateMetrosCoursesCheckboxes() + }) + d3.select('#metros button:nth-child(3)') + .on('click', ()=>{ + for(metro of metros){ + metro.checked = false + } + renderPoints() + populateMetrosCoursesCheckboxes() + }) +} + +const createRadioButtonHanlders = ()=>{ + d3.selectAll('input[type="radio"]') + .on('click', (event)=>{ + yAxis = event.target.value + d3.select('#y-axis').remove() + d3.select('#x-axis').remove() + d3.selectAll('#points circle').remove(); + setupGraph() + createAxes() + renderPoints() + }) +} + +window.onload = async ()=>{ + instances = await d3.json('/instances'); + for(let instance of instances){ + const segments = instance.course.split('-') + + if(metros.find(m => m.metro === segments[0]) === undefined){ + metros.push({ checked:true, metro: segments[0] }) + } + + if(courses.find(c => c.course === segments[1]) === undefined){ + courses.push({ checked:true, course: segments[1] }) + } + + } + populateMetrosCoursesCheckboxes() + setupGraph(); + createAxes(); + 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))); + } + + const zoom = d3.zoom() + .on('zoom', zoomCallback); + d3.select('#container').call(zoom); +}