Просмотр исходного кода

new algorithm for laying out agenda events. slotEventOverlap option

Adam Shaw 12 лет назад
Родитель
Сommit
038593d5f6

+ 244 - 94
src/agenda/AgendaEventRenderer.js

@@ -28,7 +28,6 @@ function AgendaEventRenderer() {
 	var colContentLeft = t.colContentLeft;
 	var colContentRight = t.colContentRight;
 	var cellToDate = t.cellToDate;
-	var segmentCompare = t.segmentCompare;
 	var getColCnt = t.getColCnt;
 	var getColWidth = t.getColWidth;
 	var getSnapHeight = t.getSnapHeight;
@@ -89,35 +88,32 @@ function AgendaEventRenderer() {
 			maxMinute = getMaxMinute(),
 			d,
 			visEventEnds = $.map(events, slotEventEnd),
-			i, col,
-			j, level,
-			k, seg,
+			i,
+			j, seg,
+			colSegs,
 			segs = [];
+
 		for (i=0; i<colCnt; i++) {
 
 			d = cellToDate(0, i);
 			addMinutes(d, minMinute);
 
-			col = stackAgendaSegs(
-				sliceSegs(
-					events,
-					visEventEnds,
-					d,
-					addMinutes(cloneDate(d), maxMinute-minMinute)
-				)
+			colSegs = sliceSegs(
+				events,
+				visEventEnds,
+				d,
+				addMinutes(cloneDate(d), maxMinute-minMinute)
 			);
-			countForwardSegs(col);
-
-			for (j=0; j<col.length; j++) {
-				level = col[j];
-				for (k=0; k<level.length; k++) {
-					seg = level[k];
-					seg.col = i;
-					seg.level = j;
-					segs.push(seg);
-				}
+
+			colSegs = placeSlotSegs(colSegs); // returns a new order
+
+			for (j=0; j<colSegs.length; j++) {
+				seg = colSegs[j];
+				seg.col = i;
+				segs.push(seg);
 			}
 		}
+
 		return segs;
 	}
 
@@ -152,12 +148,11 @@ function AgendaEventRenderer() {
 					start: segStart,
 					end: segEnd,
 					isStart: isStart,
-					isEnd: isEnd,
-					msLength: segEnd - segStart
+					isEnd: isEnd
 				});
 			}
 		}
-		return segs.sort(segmentCompare);
+		return segs.sort(compareSlotSegs);
 	}
 
 
@@ -178,27 +173,22 @@ function AgendaEventRenderer() {
 	
 		var i, segCnt=segs.length, seg,
 			event,
-			classes,
-			top, bottom,
-			colI, levelI, forward,
-			leftmost,
-			availWidth,
-			outerWidth,
+			top,
+			bottom,
+			columnLeft,
+			columnRight,
+			columnWidth,
+			width,
 			left,
-			html='',
+			right,
+			html = '',
 			eventElements,
 			eventElement,
 			triggerRes,
 			titleElement,
 			height,
 			slotSegmentContainer = getSlotSegmentContainer(),
-			rtl, dis;
-			
-		if (rtl = opt('isRTL')) {
-			dis = -1;
-		}else{
-			dis = 1;
-		}
+			isRTL = opt('isRTL');
 			
 		// calculate position/dimensions, create html
 		for (i=0; i<segCnt; i++) {
@@ -206,33 +196,48 @@ function AgendaEventRenderer() {
 			event = seg.event;
 			top = timePosition(seg.start, seg.start);
 			bottom = timePosition(seg.start, seg.end);
-			colI = seg.col;
-			levelI = seg.level;
-			forward = seg.forward || 0;
-			leftmost = colContentLeft(colI);
-			availWidth = colContentRight(colI) - leftmost;
-			availWidth = Math.min(availWidth-6, availWidth*.95); // TODO: move this to CSS
-			if (levelI) {
-				// indented and thin
-				outerWidth = availWidth / (levelI + forward + 1);
-			}else{
-				if (forward) {
-					// moderately wide, aligned left still
-					outerWidth = ((availWidth / (forward + 1)) - (12/2)) * 2; // 12 is the predicted width of resizer =
-				}else{
-					// can be entire width, aligned left
-					outerWidth = availWidth;
-				}
+			columnLeft = colContentLeft(seg.col);
+			columnRight = colContentRight(seg.col);
+			columnWidth = columnRight - columnLeft;
+
+			// shave off space on right near scrollbars (2.5%)
+			// TODO: move this to CSS somehow
+			columnRight -= columnWidth * .025;
+			columnWidth = columnRight - columnLeft;
+
+			width = columnWidth * (seg.forwardCoord - seg.backwardCoord);
+
+			if (opt('slotEventOverlap')) {
+				// double the width while making sure resize handle is visible
+				// (assumed to be 20px wide)
+				width = Math.max(
+					(width - (20/2)) * 2,
+					width // narrow columns will want to make the segment smaller than
+						// the natural width. don't allow it
+				);
 			}
-			left = leftmost +                                  // leftmost possible
-				(availWidth / (levelI + forward + 1) * levelI) // indentation
-				* dis + (rtl ? availWidth - outerWidth : 0);   // rtl
+
+			if (isRTL) {
+				right = columnRight - seg.backwardCoord * columnWidth;
+				left = right - width;
+			}
+			else {
+				left = columnLeft + seg.backwardCoord * columnWidth;
+				right = left + width;
+			}
+
+			// make sure horizontal coordinates are in bounds
+			left = Math.max(left, columnLeft);
+			right = Math.min(right, columnRight);
+			width = right - left;
+
 			seg.top = top;
 			seg.left = left;
-			seg.outerWidth = outerWidth;
+			seg.outerWidth = width;
 			seg.outerHeight = bottom - top;
 			html += slotSegHtml(event, seg);
 		}
+
 		slotSegmentContainer[0].innerHTML = html; // faster than html()
 		eventElements = slotSegmentContainer.children();
 		
@@ -678,60 +683,205 @@ function AgendaEventRenderer() {
 
 /* Agenda Event Segment Utilities
 -----------------------------------------------------------------------------*/
-// TODO: maybe somehow consolidate this with DayEventRenderer's segment system
 
 
-function stackAgendaSegs(segs) {
-	var levels = [],
-		i, len = segs.length, seg,
-		j, collide, k;
-	for (i=0; i<len; i++) {
+// Sets the seg.backwardCoord and seg.forwardCoord on each segment and returns a new
+// list in the order they should be placed into the DOM (an implicit z-index).
+function placeSlotSegs(segs) {
+	var levels = buildSlotSegLevels(segs);
+	var level0 = levels[0];
+	var i;
+
+	computeForwardSlotSegs(levels);
+
+	if (level0) {
+
+		for (i=0; i<level0.length; i++) {
+			computeSlotSegPressures(level0[i]);
+		}
+
+		for (i=0; i<level0.length; i++) {
+			computeSlotSegCoords(level0[i], 0, 0);
+		}
+	}
+
+	return flattenSlotSegLevels(levels);
+}
+
+
+// Builds an array of segments "levels". The first level will be the leftmost tier of segments
+// if the calendar is left-to-right, or the rightmost if the calendar is right-to-left.
+function buildSlotSegLevels(segs) {
+	var levels = [];
+	var i, seg;
+	var j;
+
+	for (i=0; i<segs.length; i++) {
 		seg = segs[i];
-		j = 0; // the level index where seg should belong
-		while (true) {
-			collide = false;
-			if (levels[j]) {
-				for (k=0; k<levels[j].length; k++) {
-					if (agendaSegsCollide(levels[j][k], seg)) {
-						collide = true;
-						break;
-					}
-				}
-			}
-			if (collide) {
-				j++;
-			}else{
+
+		// go through all the levels and stop on the first level where there are no collisions
+		for (j=0; j<levels.length; j++) {
+			if (!computeSlotSegCollisions(seg, levels[j]).length) {
 				break;
 			}
 		}
-		if (levels[j]) {
-			levels[j].push(seg);
-		}else{
-			levels[j] = [seg];
-		}
+
+		(levels[j] || (levels[j] = [])).push(seg);
 	}
+
 	return levels;
 }
 
 
-function countForwardSegs(levels) {
-	var i, j, k, level, segForward, segBack;
-	for (i=levels.length-1; i>0; i--) {
+// For every segment, figure out the other segments that are in subsequent
+// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
+function computeForwardSlotSegs(levels) {
+	var i, level;
+	var j, seg;
+	var k;
+
+	for (i=0; i<levels.length; i++) {
 		level = levels[i];
+
 		for (j=0; j<level.length; j++) {
-			segForward = level[j];
-			for (k=0; k<levels[i-1].length; k++) {
-				segBack = levels[i-1][k];
-				if (agendaSegsCollide(segForward, segBack)) {
-					segBack.forward = Math.max(segBack.forward||0, (segForward.forward||0)+1);
-				}
+			seg = level[j];
+
+			seg.forwardSegs = [];
+			for (k=i+1; k<levels.length; k++) {
+				computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
 			}
 		}
 	}
 }
 
 
-function agendaSegsCollide(seg1, seg2) {
+// Figure out which path forward (via seg.forwardSegs) results in the longest path until
+// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
+function computeSlotSegPressures(seg) {
+	var forwardSegs = seg.forwardSegs;
+	var forwardPressure = 0;
+	var i, forwardSeg;
+
+	if (seg.forwardPressure === undefined) { // not already computed
+
+		for (i=0; i<forwardSegs.length; i++) {
+			forwardSeg = forwardSegs[i];
+
+			// figure out the child's maximum forward path
+			computeSlotSegPressures(forwardSeg);
+
+			// either use the existing maximum, or use the child's forward pressure
+			// plus one (for the forwardSeg itself)
+			forwardPressure = Math.max(
+				forwardPressure,
+				1 + forwardSeg.forwardPressure
+			);
+		}
+
+		seg.forwardPressure = forwardPressure;
+	}
+}
+
+
+// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
+// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
+// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
+//
+// The segment might be part of a "series", which means consecutive segments with the same pressure
+// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
+// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
+// coordinate of the first segment in the series.
+function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) {
+	var forwardSegs = seg.forwardSegs;
+	var i;
+
+	if (seg.forwardCoord === undefined) { // not already computed
+
+		if (!forwardSegs.length) {
+
+			// if there are no forward segments, this segment should butt up against the edge
+			seg.forwardCoord = 1;
+		}
+		else {
+
+			// sort highest pressure first
+			forwardSegs.sort(compareForwardSlotSegs);
+
+			// this segment's forwardCoord will be calculated from the backwardCoord of the
+			// highest-pressure forward segment.
+			computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
+			seg.forwardCoord = forwardSegs[0].backwardCoord;
+		}
+
+		// calculate the backwardCoord from the forwardCoord. consider the series
+		seg.backwardCoord = seg.forwardCoord -
+			(seg.forwardCoord - seriesBackwardCoord) / // available width for series
+			(seriesBackwardPressure + 1); // # of segments in the series
+
+		// use this segment's coordinates to computed the coordinates of the less-pressurized
+		// forward segments
+		for (i=0; i<forwardSegs.length; i++) {
+			computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);
+		}
+	}
+}
+
+
+// Outputs a flat array of segments, from lowest to highest level
+function flattenSlotSegLevels(levels) {
+	var segs = [];
+	var i, level;
+	var j;
+
+	for (i=0; i<levels.length; i++) {
+		level = levels[i];
+
+		for (j=0; j<level.length; j++) {
+			segs.push(level[j]);
+		}
+	}
+
+	return segs;
+}
+
+
+// Find all the segments in `otherSegs` that vertically collide with `seg`.
+// Append into an optionally-supplied `results` array and return.
+function computeSlotSegCollisions(seg, otherSegs, results) {
+	results = results || [];
+
+	for (var i=0; i<otherSegs.length; i++) {
+		if (isSlotSegCollision(seg, otherSegs[i])) {
+			results.push(otherSegs[i]);
+		}
+	}
+
+	return results;
+}
+
+
+// Do these segments occupy the same vertical space?
+function isSlotSegCollision(seg1, seg2) {
 	return seg1.end > seg2.start && seg1.start < seg2.end;
 }
 
+
+// A cmp function for determining which forward segment to rely on more when computing coordinates.
+function compareForwardSlotSegs(seg1, seg2) {
+	// put higher-pressure first
+	return seg2.forwardPressure - seg1.forwardPressure ||
+		// put segments that are closer to initial edge first (and favor ones with no coords yet)
+		(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
+		// do normal sorting...
+		compareSlotSegs(seg1, seg2);
+}
+
+
+// A cmp function for determining which segment should be closer to the initial edge
+// (the left edge on a left-to-right calendar).
+function compareSlotSegs(seg1, seg2) {
+	return seg1.start - seg2.start || // earlier start time goes first
+		(seg2.end - seg2.start) - (seg1.end - seg1.start) || // tie? longer-duration goes first
+		(seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
+}
+

+ 2 - 1
src/agenda/AgendaView.js

@@ -13,7 +13,8 @@ setDefaults({
 		agenda: .5
 	},
 	minTime: 0,
-	maxTime: 24
+	maxTime: 24,
+	slotEventOverlap: true
 });
 
 

+ 11 - 2
src/common/DayEventRenderer.js

@@ -37,7 +37,6 @@ function DayEventRenderer() {
 	var clearSelection = t.clearSelection;
 	var getHoverListener = t.getHoverListener;
 	var rangeToSegments = t.rangeToSegments;
-	var segmentCompare = t.segmentCompare;
 	var cellToDate = t.cellToDate;
 	var cellToCellOffset = t.cellToCellOffset;
 	var cellOffsetToDayOffset = t.cellOffsetToDayOffset;
@@ -485,7 +484,7 @@ function DayEventRenderer() {
 
 		// Give preference to elements with certain criteria, so they have
 		// a chance to be closer to the top.
-		segments.sort(segmentCompare);
+		segments.sort(compareDaySegments);
 
 		var subrows = [];
 		for (var i=0; i<segments.length; i++) {
@@ -740,3 +739,13 @@ function segmentElementEach(segments, callback) { // TODO: use in AgendaView?
 		}
 	}
 }
+
+
+// A cmp function for determining which segments should appear higher up
+function compareDaySegments(a, b) {
+	return (b.rightCol - b.leftCol) - (a.rightCol - a.leftCol) || // put wider events first
+		b.event.allDay - a.event.allDay || // if tie, put all-day events first (booleans cast to 0/1)
+		a.event.start - b.event.start || // if a tie, sort by event start date
+		(a.event.title || '').localeCompare(b.event.title) // if a tie, sort by event title
+}
+

+ 0 - 25
src/common/View.js

@@ -307,7 +307,6 @@ function View(element, calendar, viewName) {
 	t.cellOffsetToDayOffset = cellOffsetToDayOffset;
 	t.dayOffsetToDate = dayOffsetToDate;
 	t.rangeToSegments = rangeToSegments;
-	t.segmentCompare = segmentCompare;
 
 
 	// internals
@@ -536,30 +535,6 @@ function View(element, calendar, viewName) {
 
 		return segments;
 	}
-
-
-	// Compare two event segments and determine which one takes priority (ex: rendered topmost/leftmost)
-	// NOTE: only works with segments that have `event` properties!
-	//
-	// Returns a negative value if `a` should be first.
-	// Returns a positive value of `b` should be first.
-	function segmentCompare(a, b) {
-		return _segmentCompare(a, b) // sort by dimension
-			|| (a.event.start - b.event.start) // if a tie, sort by event start date
-			|| (a.event.title || "").localeCompare(b.event.title) // if a tie, sort by event title
-	}
-
-	// compare dimensions
-	// NOTE: this is not modular! depends on subclass-specific segment schemas
-	function _segmentCompare(a, b) {
-		if ('msLength' in a) {
-			// segment generated by AgendaEventRenderer
-			return b.msLength - a.msLength; // put taller events first
-		}
-		// segment generated by DayEventRenderer
-		return (b.rightCol - b.leftCol) - (a.rightCol - a.leftCol) // put wider events first
-			|| b.event.allDay - a.event.allDay; // if tie, put all-day events first (booleans cast to 0/1)
-	}
 	
 
 }

+ 193 - 0
tests/slot_event_overlap.html

@@ -0,0 +1,193 @@
+<!DOCTYPE html>
+<html>
+<head>
+<link href='../build/out/fullcalendar.css' rel='stylesheet' />
+<link href='../build/out/fullcalendar.print.css' rel='stylesheet' media='print' />
+<script src='../build/out/jquery.js'></script>
+<script src='../build/out/jquery-ui.js'></script>
+<script src='../build/out/fullcalendar.js'></script>
+<script>
+
+	$(document).ready(function() {
+		
+		$('#calendar').fullCalendar({
+			header: {
+				left: 'prev,next today',
+				center: 'title',
+				right: 'month,agendaWeek,basicWeek,agendaDay,basicDay'
+			},
+			editable: true,
+			defaultView: 'agendaDay',
+			firstHour: 0,
+			year: 2013,
+			month: 6, // July
+			date: 31,
+			events: 'slot_event_overlap.json',
+			eventAfterAllRender: function() {
+				testOverlap($('#calendar'), true, false);
+			}
+		});
+
+		$('#calendar-nooverlap').fullCalendar({
+			header: {
+				left: 'prev,next today',
+				center: 'title',
+				right: 'month,agendaWeek,basicWeek,agendaDay,basicDay'
+			},
+			editable: true,
+			defaultView: 'agendaDay',
+			firstHour: 0,
+			year: 2013,
+			month: 6, // July
+			date: 31,
+			events: 'slot_event_overlap.json',
+			slotEventOverlap: false,
+			eventAfterAllRender: function() {
+				testOverlap($('#calendar-nooverlap'), false, false);
+			}
+		});
+
+		$('#calendar-rtl').fullCalendar({
+			header: {
+				left: 'prev,next today',
+				center: 'title',
+				right: 'month,agendaWeek,basicWeek,agendaDay,basicDay'
+			},
+			editable: true,
+			defaultView: 'agendaDay',
+			firstHour: 0,
+			year: 2013,
+			month: 6, // July
+			date: 31,
+			events: 'slot_event_overlap.json',
+			isRTL: true,
+			eventAfterAllRender: function() {
+				testOverlap($('#calendar-rtl'), true, true);
+			}
+		});
+
+		$('#calendar-rtl-nooverlap').fullCalendar({
+			header: {
+				left: 'prev,next today',
+				center: 'title',
+				right: 'month,agendaWeek,basicWeek,agendaDay,basicDay'
+			},
+			editable: true,
+			defaultView: 'agendaDay',
+			firstHour: 0,
+			year: 2013,
+			month: 6, // July
+			date: 31,
+			events: 'slot_event_overlap.json',
+			isRTL: true,
+			slotEventOverlap: false,
+			eventAfterAllRender: function() {
+				testOverlap($('#calendar-rtl-nooverlap'), false, true);
+			}
+		});
+		
+	});
+
+	function testOverlap(el, allowOverlap, isRTL) {
+		if (_testOverlap(el, allowOverlap, isRTL)) {
+			el.prev('h2').find('span').css('color', 'green').text('passed');
+		}
+		else {
+			el.prev('h2').find('span').css('color', 'red').text('failed');
+		}
+	}
+
+	function _testOverlap(el, allowOverlap, isRTL) {
+		var events = el.find('.fc-event');
+
+		for (var i=0; i<events.length; i++) {
+
+			var event = $(events[i]);
+			var offset = event.offset();
+			var top = Math.ceil(offset.top);
+			var left = Math.ceil(offset.left);
+			var width = Math.floor(event.outerWidth());
+			var height = Math.floor(event.outerHeight());
+
+			if (allowOverlap) {
+				width /= 2; // make sure nothing overlaps the first half of the event
+				if (isRTL) {
+					left += width;
+				}
+			}
+
+			for (var j=i+1; j<events.length; j++) {
+				// only test again events that are ahead in the DOM, meaning they have a higher
+				// implicit z-index and an top of the current event
+
+				var otherEvent = $(events[j]);
+				var otherOffset = otherEvent.offset();
+				var otherTop = Math.ceil(otherOffset.top);
+				var otherLeft = Math.ceil(otherOffset.left);
+				var otherWidth = Math.floor(otherEvent.outerWidth());
+				var otherHeight = Math.floor(otherEvent.outerHeight());
+
+				if (
+					top < otherTop + otherHeight &&
+					top + height > otherTop &&
+					left < otherLeft + otherWidth &&
+					left + width > otherLeft
+				) {
+					console.log('failed on', event[0], otherEvent[0]);
+					return false; // a collision
+				}
+			}
+		}
+
+		if (!allowOverlap) {
+			// we know of certain events that should have the same width
+			// because they share a liquid area...
+
+			var equalwidth1 = events.filter('.equalwidth1');
+			if (equalwidth1.eq(0).outerWidth() - equalwidth1.eq(1).outerWidth() > 1) {
+				console.log('not equal width', equalwidth1.toArray());
+				return false;
+			}
+
+			var equalwidth2 = events.filter('.equalwidth2');
+			if (equalwidth2.eq(0).outerWidth() - equalwidth2.eq(1).outerWidth() > 1) {
+				console.log('not equal width', equalwidth2.toArray());
+				return false;
+			}
+
+		}
+
+		return true;
+	}
+
+</script>
+<style>
+
+	body {
+		font-size: 13px;
+		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
+	}
+
+	.calendar {
+		width: 900px;
+		margin: 50px auto;
+	}
+
+</style>
+</head>
+<body>
+
+<h2>Default: <span></span></h2>
+<div id='calendar' class='calendar'></div>
+
+<h2>No overlap: <span></span></h2>
+<div id='calendar-nooverlap' class='calendar'></div>
+
+<h2>RTL: <span></span></h2>
+<div id='calendar-rtl' class='calendar'></div>
+
+<h2>RTL, no overlap: <span></span></h2>
+<div id='calendar-rtl-nooverlap' class='calendar'></div>
+
+</body>
+</html>

+ 102 - 0
tests/slot_event_overlap.json

@@ -0,0 +1,102 @@
+[
+  {
+    "title": "1",
+    "start": "2013-07-31T00:30",
+    "end": "2013-07-31T02:30",
+    "allDay": false
+  },
+  {
+    "title": "2",
+    "start": "2013-07-31T00:30",
+    "end": "2013-07-31T02:30",
+    "allDay": false
+  },
+  {
+    "title": "3",
+    "start": "2013-07-31T02:30",
+    "end": "2013-07-31T04:30",
+    "allDay": false
+  },
+  {
+    "title": "4",
+    "start": "2013-07-31T02:30",
+    "end": "2013-07-31T04:30",
+    "allDay": false
+  },
+  {
+    "title": "5",
+    "start": "2013-07-31T02:30",
+    "end": "2013-07-31T04:30",
+    "allDay": false
+  },
+  {
+    "title": "6",
+    "start": "2013-07-31T01:00",
+    "end": "2013-07-31T03:00",
+    "allDay": false
+  },
+  {
+    "title": "7",
+    "start": "2013-07-31T01:30",
+    "end": "2013-07-31T03:30",
+    "allDay": false
+  },
+  {
+    "title": "8",
+    "start": "2013-07-31T02:50",
+    "end": "2013-07-31T04:50",
+    "allDay": false
+  },
+  {
+    "title": "9",
+    "start": "2013-07-31T04:30",
+    "end": "2013-07-31T09:00",
+    "allDay": false
+  },
+  {
+    "title": "10",
+    "start": "2013-07-31T02:00",
+    "end": "2013-07-31T04:05",
+    "allDay": false
+  },    
+  {
+    "title": "11",
+    "start": "2013-07-31T02:50",
+    "end": "2013-07-31T04:50",
+    "allDay": false
+  },    
+  {
+    "title": "12",
+    "start": "2013-07-31T02:50",
+    "end": "2013-07-31T04:50",
+    "allDay": false
+  },    
+  {
+    "title": "13",
+    "start": "2013-07-31T04:00",
+    "end": "2013-07-31T06:00",
+    "allDay": false,
+    "className": "equalwidth1"
+  },
+  {
+    "title": "14",
+    "start": "2013-07-31T04:10",
+    "end": "2013-07-31T06:09",
+    "allDay": false,
+    "className": "equalwidth1"
+  },
+  {
+    "title": "15",
+    "start": "2013-07-31T06:30",
+    "end": "2013-07-31T08:30",
+    "allDay": false,
+    "className": "equalwidth2"
+  },
+  {
+    "title": "16",
+    "start": "2013-07-31T06:30",
+    "end": "2013-07-31T08:30",
+    "allDay": false,
+    "className": "equalwidth2"
+  }
+]