forked from jmuyskens's block: Interactive drag and drop weekly course schedule.
forked from slattery's block: Interactive drag and drop weekly course schedule.
| license: mit |
forked from jmuyskens's block: Interactive drag and drop weekly course schedule.
forked from slattery's block: Interactive drag and drop weekly course schedule.
| /** | |
| * Created by jmuyskens on 2/9/14. | |
| */ | |
| var margin = {top:10, right: 10, bottom: 10, left: 50}, | |
| height = 500 - margin.top - margin.bottom, | |
| height = 500 - margin.top - margin.bottom, | |
| width = 500 - margin.left - margin.right; | |
| var days = ['mon','tue','wed','thu','fri']; | |
| var hours = ['08:00 AM','08:30 AM','09:00 AM','09:30 AM','10:00 AM','10:30 AM','11:00 AM','11:30 AM','12:00 PM','12:30 PM', | |
| '01:00 PM','01:30 PM','02:00 PM','02:30 PM','03:00 PM','03:30 PM','04:00 PM','04:30 PM','05:00 PM','05:30 PM','06:00 PM', | |
| '06:30 PM','07:00 PM','07:30 PM','08:00 PM','08:30 PM','09:00 PM','09:30 PM']; | |
| var hourScaleRangeBand = height / hours.length; | |
| function isColliding(course, trans, otherCourses) { | |
| return _.chain(otherCourses).map(function(otherCourse) { | |
| if (course.id != otherCourse.id) { | |
| if (_.intersection(course.day, otherCourse.day).length > 0){ | |
| var otherStart = hourScale(format.parse(otherCourse.time.start)); | |
| var otherEnd = hourScale(format.parse(otherCourse.time.end)); | |
| var thisStart = trans[1]; | |
| var thisEnd = trans[1] + (hourScale(format.parse(course.time.end)) - hourScale(format.parse(course.time.start))); | |
| //console.log(otherStart,otherEnd,thisStart,thisEnd); | |
| return ((thisStart < otherStart && thisEnd > otherStart) || (thisStart > otherStart && thisStart < otherEnd)); | |
| } else { | |
| return false; | |
| } | |
| } else { | |
| return false; | |
| } | |
| }).contains(true).value(); | |
| } | |
| courses = [ | |
| { | |
| id: 0, | |
| abbr: 'MATH243', | |
| time: { | |
| start: '09:00 AM', | |
| end: '09:50 AM' | |
| }, | |
| day: ['mon', 'tue', 'thu', 'fri'] | |
| }, | |
| { | |
| id: 1, | |
| abbr: 'CS332', | |
| time: { | |
| start: '10:30 AM', | |
| end: '11:20 AM' | |
| }, | |
| day: ['mon', 'wed', 'fri'] | |
| }, | |
| { | |
| id: 2, | |
| abbr: 'CS300', | |
| time: { | |
| start: '11:30 AM', | |
| end: '12:20 PM' | |
| }, | |
| day: ['mon','wed','fri'] | |
| }, | |
| { | |
| id: 3, | |
| abbr: 'PER181', | |
| time: { | |
| start: '10:30 AM', | |
| end: '11:50 AM' | |
| }, | |
| day: ['tue','thu'] | |
| }, | |
| { | |
| id: 4, | |
| abbr: 'CS384', | |
| time: { | |
| start: '06:30 PM', | |
| end: '9:30 PM' | |
| }, | |
| day: ['thu'] | |
| } | |
| ]; | |
| var week = []; | |
| var format = d3.time.format("%I:%M %p"); | |
| var drag = d3.behavior.drag() | |
| .origin(Object) | |
| .on('drag', function (d, i) { | |
| var t = d3.select(this); | |
| var trans = d3.transform(t.attr('transform')).translate; | |
| if (isColliding(d, trans, courses)) { | |
| t.style('fill','red'); // warn the user about the impending collision | |
| } else { | |
| t.style('fill','black'); | |
| } | |
| // update the position of the course group | |
| // TODO: there is no t.attr('height') b/c its just a group, so get that height not critical | |
| t.attr('transform', 'translate(0,' + Math.min(height - t.attr('height'), Math.max(0, +d3.event.dy + trans[1])) + ')'); | |
| }) | |
| .on('dragend', function (d, i) { | |
| var t = d3.select(this); | |
| var trans = d3.transform(t.attr('transform')).translate; | |
| // round to the nearest half hour, TODO: do this with d3.time.minute.ceil or something | |
| var hrs = hours[Math.floor(trans[1] / hourScaleRangeBand)]; | |
| if (isColliding(d, trans, courses)) { | |
| // user lets go in an invalid place -> return the course group to original location | |
| t.attr('transform', function(d) { console.log(d.time.start); return 'translate(0,' + hourScale(format.parse(d.time.start)) + ')';}); | |
| t.style('fill','black'); // I see a red course and I want it painted black | |
| } else { | |
| // user selected a valid time for the course -> snap to nearest half hour slot | |
| // TODO: is there a possibility with a weird length course that it could snap to a conflicting time? | |
| t.attr('transform', function() { return 'translate(0,' + hourScale(format.parse(hrs)) + ')';}); | |
| // calculate the duration of the course section | |
| var sectionDuration = (format.parse(courses[d.id].time.end) - format.parse(courses[d.id].time.start)) / 60000; | |
| courses[d.id].time.start = hrs; | |
| // find the section end time by add the duration of the section to the start time | |
| courses[d.id].time.end = format(d3.time.minute.offset(format.parse(hrs), sectionDuration)); | |
| // redraw | |
| updateCourseGroups(); | |
| }; | |
| }); | |
| var d3datehours = hours.map(format.parse); | |
| days.forEach(function(day){ | |
| var hourObj = {}; | |
| hours.forEach(function(hour){ | |
| hourObj[hour] = { | |
| occupied: false, | |
| course: '' | |
| }; | |
| }); | |
| week.push(hourObj.day = day); | |
| }); | |
| var dayScale = d3.scale.ordinal() | |
| .domain(days) | |
| .rangeRoundBands([0, width], 0.01); | |
| var hourScale = d3.time.scale() | |
| .domain([d3datehours[0],d3datehours[27]]) | |
| .rangeRound([0, height]); | |
| svg = d3.select('body').append('svg') | |
| .attr('height', height + margin.top + margin.bottom) | |
| .attr('width', width + margin.right + margin.left) | |
| .append('g') | |
| .attr('transform','translate(' + margin.left + ',' + margin.top + ')'); | |
| function makeHourAxis() { | |
| return d3.svg.axis() | |
| .scale(hourScale) | |
| .orient('left') | |
| } | |
| function makeDayAxis() { | |
| return d3.svg.axis() | |
| .scale(dayScale) | |
| .orient('top') | |
| } | |
| svg.append('g') | |
| .attr('class', 'hour axis') | |
| .attr('transform','translate(' + width + ',0)') | |
| .attr('transform','translate(' + width + ',0)') | |
| .call(makeHourAxis().ticks(hours.length).tickSize(width, 0, 0).tickFormat("")); | |
| svg.append('g') | |
| .attr('class', 'hour axis') | |
| .call(makeHourAxis()); | |
| svg.append('g') | |
| .attr('class', 'day axis') | |
| .attr('transform', 'translate(' + (dayScale.rangeBand() / 2) + ',' + height + ')') | |
| .call(makeDayAxis().tickSize(height, 0, 0).tickFormat("")); | |
| svg.append('g') | |
| .attr('class', 'day axis') | |
| .call(makeDayAxis().tickSize(0)); | |
| var courseGroups = svg.selectAll('g.course') | |
| .data(courses) | |
| .enter().append('g') | |
| .attr('class', 'course') | |
| .attr('transform', function(d) { | |
| return 'translate(' + 0 + ',' + hourScale(format.parse(d.time.start)) + ')'; | |
| }) | |
| .call(drag); | |
| var sections = courseGroups.selectAll('g.section') | |
| .data(function(d) { | |
| return d.day.map(function(e){ | |
| return {day: e, time: d.time, abbr: d.abbr}; | |
| }); | |
| }) | |
| .enter().append('g') | |
| .attr('class','section'); | |
| sections.append('rect') | |
| .attr('class','section') | |
| .attr('y', 0) | |
| .attr('x', function(d){ return dayScale(d.day); }) | |
| .attr('width', dayScale.rangeBand()) | |
| .attr('height', function(d) { | |
| return hourScale(format.parse(d.time.end)) - hourScale(format.parse(d.time.start)); | |
| }); | |
| sections.append('text') | |
| .style('fill', 'white') | |
| .attr('y', 10) | |
| .attr('x', function(d){ return dayScale(d.day); }) | |
| .text(function(d){ return d.abbr; }); | |
| function updateCourseGroups() { | |
| svg.selectAll('g.course') | |
| .data(courses) | |
| .attr('transform', function(d) { | |
| return 'translate(' + 0 + ',' + hourScale(format.parse(d.time.start)) + ')'; | |
| }); | |
| } |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>drag 'n drop test</title> | |
| <!--<script src="../bower_components/d3/d3.js" charset="utf-8"></script>--> | |
| <script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script> | |
| <script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.2/underscore-min.js"></script> | |
| <style> | |
| svg { | |
| font: 10px sans-serif; | |
| background: white; | |
| display:block; | |
| margin:0 auto; | |
| } | |
| /*button { | |
| clear:right; | |
| float:right; | |
| }*/ | |
| .chart rect { | |
| stroke: white; | |
| } | |
| rect.section { | |
| cursor: ns-resize; | |
| } | |
| .line { | |
| fill:none; | |
| stroke:#000; | |
| stroke-width:1.5px; | |
| } | |
| .axis path, .axis line { | |
| fill:none; | |
| stroke:silver; | |
| shape-rendering: crispEdges; | |
| } | |
| .axis path { | |
| display: none; | |
| } | |
| .grid { | |
| fill:none; | |
| stroke-width:0.5px; | |
| stroke:#000; | |
| opacity:0.2; | |
| } | |
| .ratio { | |
| fill:#f2c180; | |
| color:#f2c180; | |
| } | |
| .fsratio { | |
| fill:#f3a53d; | |
| color:#f3a53d; | |
| } | |
| .students { | |
| fill:#5cadf0; | |
| color:#5cadf0; | |
| } | |
| .faculty { | |
| fill:steelblue; | |
| color:steelblue; | |
| } | |
| #wrap { | |
| width:910px; | |
| margin:0 auto; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <script src="drag.js"></script> | |
| </body> | |
| </html> |