Last active
August 29, 2015 13:56
-
-
Save jmuyskens/8993967 to your computer and use it in GitHub Desktop.
Interactive drag and drop weekly course schedule.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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)) + ')'; | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment