block by jmuyskens 8993967

Interactive drag and drop weekly course schedule.

Full Screen

index.html

<!DOCTYPE html>
<html>
<head>
    <title>drag 'n drop test</title>
    <!--<script src="../bower_components/d3/d3.js" charset="utf-8"></script>-->
    <script src="//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>

drag.js

/**
 * 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)) + ')';
        });
}