// TODO - change y axis to dropped const WIDTH = 800; const HEIGHT = 600; const parseTime = d3.timeParse("%B %e, %Y"); let instances; let xScale, yScale; let highlighted = [] let courses = [] let metros = [] 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) => yScale(datum.ninety_day_outcomes/datum.graduates*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) => { return datum.ninety_day_outcomes/datum.graduates*100; }) yScale.domain(yDomain); } const createAxes = () => { const bottomAxis = d3.axisBottom(xScale); d3.select('#container') .append('g') .attr('id', 'x-axis') .call(bottomAxis) .attr('transform', 'translate(0,'+HEIGHT+')'); const 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() }) } 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(); }