From 51251c0a8f275551f21bb070a5454afbe95907e8 Mon Sep 17 00:00:00 2001 From: Daru13 Date: Mon, 26 Nov 2018 16:06:31 +0100 Subject: [PATCH] Add a new event sorting algorithm (using interval graph coloring). Note: this only sort the event nodes in a particular, "optimal" order; but it does not explicitly set the grid row of each node. Therefore, the result is likely to spread on many more rows than what could be expected. --- event/static/js/calendar.js | 65 +++++++++++++++++++- event/static/js/interval_coloration.js | 84 ++++++++++++++++++++++++++ event/templates/event/calendar.html | 1 + 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 event/static/js/interval_coloration.js diff --git a/event/static/js/calendar.js b/event/static/js/calendar.js index a2019c9..d5deb70 100644 --- a/event/static/js/calendar.js +++ b/event/static/js/calendar.js @@ -70,7 +70,8 @@ class Calendar { this.applyLocationStylesAsCSS(); this.updateEventLocationStyleID(); - this.sortEventNodesByEndTimeAndLocation(); + //this.sortEventNodesByEndTimeAndLocation(); + this.sortEventNodesByLocationAndIntervalGraphColoring(); this.updateCalendarNodeHeight(); this.initEventOverflowTooltips(); @@ -334,6 +335,68 @@ class Calendar { // Event node sorting + // The following method requires the IntervalColoration class, + // which provides the algorithm (see interval_coloring.js) + sortEventNodesByLocationAndIntervalGraphColoring () { + // Group events by location + let locationsToEvents = new Map(); + for (let event of this.events) { + let location = event.location; + + if (! locationsToEvents.has(location)) { + locationsToEvents.set(location, [event]); + } + else { + locationsToEvents + .get(location) + .push(event); + } + } + + // Assign a color to all events, + // by using the interval graph coloration algorithm + // on each subset of events with the same location + let allEventColors = []; + let nbAlreadyColoredEvents = 0; + + for (let eventsAtSameLocation of locationsToEvents.values()) { + // Build intervals for each event + let intervals = []; + for (let event of eventsAtSameLocation) { + intervals.push([ + event.startDate.getTime(), + event.endDate.getTime() + ]); + } + + // Get the graph coloring, and assign each color to each event + let intervalGraphColors = new IntervalColoration(intervals).colors; + let eventsToColors = new Map(); + for (let i = 0; i < eventsAtSameLocation; i++) { + eventsToColors.set(event, intervalGraphColors[i]); + } + + // Sort the events by color + eventsAtSameLocation.sort((event1, event2) => { + return eventsToColors.get(event2) - eventsToColors.get(event1); + }); + } + + // Finally sort all event nodes, + // (1) by their location and (2) by their color + + // Note: the container is detached from the DOM for better performances + this.eventContainerNode.detach(); + + for (let eventsAtSameLocation of locationsToEvents.values()) { + for (let event of eventsAtSameLocation) { + this.eventContainerNode.prepend(event.node); + } + } + + this.eventContainerNode.appendTo(this.containerNode); + } + sortEventNodesByEndTimeAndLocation () { this.events.sort((event1, event2) => { return event2.endDate.getTime() - event1.endDate.getTime(); diff --git a/event/static/js/interval_coloration.js b/event/static/js/interval_coloration.js new file mode 100644 index 0000000..082b9de --- /dev/null +++ b/event/static/js/interval_coloration.js @@ -0,0 +1,84 @@ +// Interval graph coloring algorithm, by Twal + +class IntervalColoration { + constructor (intervals) { + this.intervals = intervals; + this.n = this.intervals.length; + this.computeInterferenceGraph(); + this.computePEO(); + this.computeColoration(); + } + + computeInterferenceGraph() { + this.adj = new Array(this.n); + for (let i = 0; i < this.n; ++i) { + this.adj[i] = []; + } + for (let i = 0; i < this.n; ++i) { + for (let j = 0; j < i; ++j) { + let inti = this.intervals[i]; + let intj = this.intervals[j]; + if (inti[0] <= intj[1] && intj[0] <= inti[1]) { + this.adj[i].push(j); + this.adj[j].push(i); + } + } + } + } + + //Perfect elimination order using Maximum Cardinality Search + //Runs in O(n^2), could be optimized in O(n log n) + computePEO() { + let marked = new Array(this.n); + let nbMarkedNeighbor = new Array(this.n); + this.perm = new Array(this.n); + for (let i = 0; i < this.n; ++i) { + marked[i] = false; + nbMarkedNeighbor[i] = 0; + } + for (let k = this.n-1; k >= 0; --k) { + let maxi = -1; + for (let i = 0; i < this.n; ++i) { + if (!marked[i] && (maxi == -1 || nbMarkedNeighbor[i] >= nbMarkedNeighbor[maxi])) { + maxi = i; + } + } + for (let i = 0; i < this.adj[maxi].length; ++i) { + nbMarkedNeighbor[this.adj[maxi][i]] += 1; + } + this.perm[maxi] = k; + marked[maxi] = true; + } + // console.log(this.perm); + } + + computeColoration() { + this.colors = new Array(this.n); + let isColorUsed = new Array(this.n); + for (let i = 0; i < this.n; ++i) { + this.colors[i] = -1; + isColorUsed[i] = false; + } + for (let i = 0; i < this.n; ++i) { + let ind = this.perm[i]; + for (let j = 0; j < this.adj[ind].length; ++j) { + let neigh = this.adj[ind][j]; + if (this.colors[neigh] >= 0) { + isColorUsed[this.colors[neigh]] = true; + } + } + for (let j = 0; j < this.n; ++j) { + if (!isColorUsed[j]) { + this.colors[ind] = j; + break; + } + } + for (let j = 0; j < this.adj[ind].length; ++j) { + let neigh = this.adj[ind][j]; + if (this.colors[neigh] >= 0) { + isColorUsed[this.colors[neigh]] = false; + } + } + } + } +} diff --git a/event/templates/event/calendar.html b/event/templates/event/calendar.html index 2e4a1dc..7aa8196 100644 --- a/event/templates/event/calendar.html +++ b/event/templates/event/calendar.html @@ -26,6 +26,7 @@ {% block extra_js %} {{ block.super }} +