10

我正在开发一个事件系统,它基本上是一个高度为 720 像素的容器,每个像素代表从上午 9 点到晚上 9 点的一分钟,宽度为 620 像素(左右填充 10 像素)

日历系统的自然要求是:

  • 对象的布局应使它们在视觉上不会重叠。
  • 如果一个时间段有一个事件,它的宽度将是 600px
  • 每个碰撞事件的宽度必须与它碰撞的所有其他事件的宽度相同。
  • 事件应使用可能的最大宽度,同时仍遵守第一个约束。

在此处输入图像描述

输入将是一个数组,例如:

[
 {id : 1, start : 30, end : 150},  // an event from 9:30am to 11:30am
 {id : 2, start : 540, end : 600}, // an event from 6pm to 7pm
 {id : 3, start : 560, end : 620}, // an event from 6:20pm to 7:20pm
 {id : 4, start : 610, end : 670} // an event from 7:10pm to 8:10pm
]

我已经创建了所需的布局,但我被 JavaScript 部分卡住了:(这是我到目前为止所拥有的:

var Calendar = function() {

   var layOutDay = function(events) {
     var eventsLength = events.length;

     if (! eventsLength) return false;

     // sort events
     events.sort(function(a, b){return a.start - b.start;});

     for (var i = 0; i < eventsLength; i++) {
         // not sure what is next
     }         

   };

   return {
       layOutDay : layOutDay,
   }

}();

需要根据上述要求创建 div 并定位它们。

请参阅 JSBin 演示。

任何帮助将不胜感激。

4

6 回答 6

7

这是一个可行的解决方案:http: //jsbin.com/igujil/13/edit#preview

如您所见,这不是一个容易解决的问题。让我带你了解一下我是如何做到的。

第一步,标记为Step 0,是确保事件按 id 排序。当我们开始使用数据时,这将使我们的生活更轻松。

第 1 步是初始化一个二维时隙数组。对于日历中的每一分钟,我们将创建一个数组,其中包含在那一分钟内发生的事件。我们这样做...

第2步!您会注意到我添加了一项检查以确保活动在结束之前开始。有点防御,但我的算法会在坏数据上陷入无限循环,所以我想确保这些事件有意义。

在这个循环结束时,我们的时隙数组将如下所示:

0:[]
1:[]
...
30:[1]
31:[1]
...
(跳到一些有趣的数字)
540:[2]
560:[2,3]
610:[3,4 ]

如果您感到困惑/好奇,我鼓励您console.log(timeslots)在第 3 步之前添加。这是解决方案中非常重要的一部分,下一步要解释起来要困难得多。

第 3 步是我们解决调度冲突的地方。每个事件需要知道两件事:

  1. 冲突的最大次数。
  2. 它的水平顺序(这样冲突就不会重叠)。

(1) 很容易,因为我们的数据是如何存储的;每个时隙数组的宽度是事件的数量。例如,时间段 30 只有 1 个事件,因为事件 #1 是当时唯一的一个。然而,在时隙 560 中,我们有两个事件,因此每个事件(#2 和 #3)计数为 2。(如果有一排有三个事件,他们都会得到三个,等等)

(2) 更微妙一些。事件#1 很明显,因为它可以跨越日历的整个宽度。事件 #2 将不得不缩小其宽度,但它仍然可以从左边缘开始。事件 #3 不能。

我们使用我称为的每个时隙变量来解决这个问题next_hindex。它从 0 开始,因为默认情况下我们希望沿左边缘定位,但每次发现冲突时它都会增加。这样,下一个事件(我们的下一个冲突部分)将从下一个水平位置开始。

第 4 步要简单得多。宽度计算使用步骤 3 中的最大冲突计数。例如,如果我们知道在 5:50 有 2 个事件,我们知道每个事件必须是日历宽度的 1/2。(如果我们有 3 个事件,每个事件将是 1/3,等等。) x 位置的计算类似;我们乘以 hindex 因为我们想要抵消(冲突数量)事件的宽度。

最后,我们只需创建一个小 DOM,定位我们的事件 div,并设置随机颜色,以便于区分它们。结果是(我认为)你正在寻找的东西。

如果您有任何问题,我很乐意回答。我知道这可能比您预期的代码更多(并且更复杂),但这是一个令人惊讶的复杂问题要解决:)

于 2012-05-22T09:14:53.360 回答
3

这是我的解决方案: # one-day A 一日日历

要求

第一部分:编写一个函数,在日历上安排一天的一系列事件。

事件将被放置在一个容器中。容器的顶部代表上午 9 点,底部代表晚上 9 点。
容器的宽度为 620 像素(左右填充 10 像素),高度为 720 像素(上午 9 点到晚上 9 点之间每分钟 1 个像素)。对象的布局应使它们在视觉上不会重叠。如果给定时间段只有一个事件,则其宽度应为 600 像素。

有两个主要限制: 1. 每个碰撞事件的宽度必须与它碰撞的所有其他事件的宽度相同。2. 一个事件应该使用可能的最大宽度,同时仍然遵守第一个约束。

请参见下图作为示例。 图 1

该函数的输入将是一个包含事件开始和结束时间的事件对象数组。示例(JS):

[

     {id : 1, start : 60, end : 120},  // an event from 10am to 11am
     {id : 2, start : 100, end : 240}, // an event from 10:40am to 1pm
     {id : 3, start : 700, end : 720} // an event from 8:40pm to 9pm 
]

除了 id、开始和结束时间之外,该函数还应返回一个事件对象数组,这些对象对象设置了 left 和 top 位置(相对于容器的左上角)。

第 II 部分:使用第 I 部分中的函数创建一个样式类似于下面示例图像的网页。
图2 带有以下日历事件:

  1. 上午 9:30 开始,上午 11:30 结束的活动
  2. 下午 6:00 开始,晚上 7:00 结束的活动
  3. 下午 6:20 开始,晚上 7:20 结束的活动
  4. 晚上 7:10 开始,晚上 8:10 结束的活动

代码

安装和运行

  1. 克隆这个 repo
  2. 在您喜欢的浏览器中打开 index.html 文件。

注意:在启动时有一组默认事件(第二部分需要)。
为了测试,在默认数组的正下方(第 14 行),您可以找到generateEvents生成随机事件数组的函数。数组大小将由 arrayLength 属性确定。

依赖项

没有任何!

解决方案

您可以在下面找到根据要求解决问题的算法。

介绍

我将尝试用图表的方式来解决这个任务,所以应该给出很少的术语。

条款

节点:代表一个事件 - $n$, $n \in N, N$ - 所有节点的组。
边:表示碰撞事件 - $e$, $e \in E, E$ - 所有边的组。例如,如果节点 $u$ 和 $v$ 发生碰撞,那么将有一条边 $e_{u,v}$ 连接它们。
图:节点和边 $G, G\in(N,E)$ 的集合。
Cluster:表示一组连接的节点(图的子组) - $c$, $c \subseteq G$。例如,如果我们有以下节点:$u, v, w$ 和边 $e_{u,v}$。然后会有 2 个簇,第一个包含 $u,v$,第二个只包含 $w$。
集团:表示集群中节点的子组,该组中的每一对节点都有一条连接边 - $cq$, $cq \subseteq c$。注意,一个 clique 代表一组碰撞事件。

Board:存放所有事件的日容器。

术语示例

对于以下输入:

[
     {id : 1, start : 0, end : 120}, 
     {id : 2, start : 60, end : 120},
     {id : 3, start : 60, end : 180},
     {id : 4, start : 150, end : 240},
     {id : 5, start : 200, end : 240},
     {id : 6, start : 300, end : 420},
     {id : 7, start : 360, end : 420},
     {id : 8, start : 300, end : 720}
]

该图将是:

图形图像

黑色循环 - 节点 - 事件
绿色椭圆 - 集团 - 碰撞事件组
红色椭圆 - 集群 - 连接节点组
蓝色线 - 边缘 - 碰撞事件之间的连接器
注意:左上角的绿色椭圆是左侧集群中最大的集团。
董事会将是: 木板

红色矩形 - 簇
彩色圆点 - 派系(每种颜色都是不同的派系)。

约束释义

  1. 同一集群中的每个节点必须在板上具有相同的宽度,以满足第一个约束。
  2. 节点不得在板上相互重叠,同时淀粉至最大宽度并仍遵守第一个约束。
  3. 集群中节点的宽度将由集群中最大的集团设置。这是真的,因为同一集团中的节点将在棋盘上共享至少一分钟,这意味着它们必须具有相同的宽度(因为它们正在碰撞)。因此集群中的其他节点将具有与最小节点相同的宽度。
  4. clique 中的每个节点都将获得其相对于其邻居的 X 轴位置。

算法

对于给定的事件数组arrayOfEvents(来自需求示例):

[

     {id : 1, start : 60, end : 120},  // an event from 10am to 11am
     {id : 2, start : 100, end : 240}, // an event from 10:40am to 1pm
     {id : 3, start : 700, end : 720} // an event from 8:40pm to 9pm 
]

第一步:创建事件直方图。
将创建一个数组数组,让我们将此数组称为histogram. histogram长度为 720,每个索引将histogram代表棋盘上的一分钟(天)。histogram让我们调用a 的每个索引minute。每个minute, 本身就是一个数组。数组的每个索引minute代表在这一分钟发生的一个事件。

伪代码:

histogram = new Array(720); 

forEach minute in histogram:
    minute = new Array(); 

forEach event in arrayOfEvents:
    forEach minute inBetween event.start and endMinute:
        histogram[minute].push(event.id);

histogram在此步骤之后数组将如下所示(对于给定的示例):

[
    1: [],
    2: [],
    .
    .
    .
    59: [],
    60: [1],
    61: [1],
    .
    .
    .
    99: [1],
    100: [1,2],
    101: [1,2],
    .
    .
    .
    120: [1,2],
    121: [2],
    122: [2],
    .
    .
    .
    240: [2],
    241: [],
    242: [],
    .
    .
    .
    699: [],
    700: [3],
    701: [3],
    .
    .
    .
    720: [3]
]

第二步:创建图
在这一步中,将创建图,包括节点、节点邻居和集群,还将确定集群的最大集团。
请注意,不会有边缘实体,每个节点都将保存一个节点映射(键:节点 ID,值:节点),它与(其邻居)发生冲突。这张地图将被称为邻居。此外,maxCliqueSize将向每个节点添加一个属性。maxCliqueSize是该节点所属的最大集团。

伪代码:

nodesMap := Map<nodeId, node>;
graph := Object<clusters, nodesMap>;
Node := Object<nodeId, start, end, neighbours, cluster, position, biggestCliqueSize>;
Cluster := Object<mapOfNodesInCluster, width>

//creating the nodes
forEach event in arrayOfEvents {
    node = new Node(event.id, event.start, event.end, new Map<nodeId, node>, null)
    nodeMap[node.nodeId] = node;
 }

//creating the clusters
cluster = null;
forEach minute in histogram {
    if(minute.length > 0) {
        cluster = cluster || new Cluster(new Array(), 0);
        forEach eventId in minute {
            if(eventId not in cluster.nodes) {
                cluster.nodes[eventId] = nodeMap[eventId];
                nodeMap[eventId].cluster = cluster;
            }
        } 
    } else { 
        if(cluster != null) {
            graph.clusters.push(cluster);
        }

        cluster = null;
    }
}

//adding edges to nodes and finding biggest clique for each node
forEach minute in histogram {
    forEach sourceEventId in minute {
        sourceNode = eventsMap[sourceEventId];
        sourceNode.biggestCliqueSize = Math.max(sourceNode.biggestCliqueSize, minute.length);
        forEach targetEventId in minute {
            if(sourceEventId != targetEventId) {
                sourceNode.neighbours[targetEventId] = eventsMap[targetEventId];
            }
        }
    }
}

第三步:计算每个簇的宽度。
如上所述,集群中所有节点的宽度将由集群中最大集团的大小决定。
集群 $c$ 中每个节点 $n$ 的宽度将遵循以下等式:
$$n_{width} = \frac{Board_{width}}{Max\left (n_{1}.biggestCliqueSize, n_{2}。最大CliqueSize, ..., n_{n}.biggestCliqueSize\right )}$$

每个节点宽度都将在与其相关的集群中设置。因此将在集群实体上设置宽度属性。

伪代码:

forEach cluster in graph.clusters {
    maxCliqueSize = 1;
    forEach node in cluster.nodes {
        maxCliqueSize = Max(node.biggestCliqueSize, sizeOf(node.clique);
    }
    cluster.width = BOARD_WIDTH / maxCliqueSize; 
    cluster.biggestCliqueSize = biggestCliqueSize;
}

第四步:计算其团内的节点位置。
如前所述,节点必须与其邻居共享 X 轴(“不动产”)。在这一步中,将根据其邻居为每个节点给出 X 轴位置。集群中最大的集团将决定可用名额的数量。

伪代码:

forEach node in nodesMap {
    positionArray = new Array(node.cluster.biggestCliqueSize);
    forEach cliqueNode in node.clique {
        if(cliqueNode.position != null) {
            //marking occupied indexes
            positionArray[cliqueNode.position] = true;
        }
    }

    forEach index in positionArray {
        if(!positionArray[index]) {
            node.position = index;
            break;
        }
    }
}

第五步:将节点放在板上。在这一步中,我们已经拥有了将事件(节点)放置在板上的位置所需的所有信息。每个节点的位置和大小将由以下因素决定:

  1. 高度:node.end - node.start
  2. 宽度:node.cluster.width
  3. 顶部偏移:node.start
  4. 左偏移:node.cluster.width * node.position + 左填充
算法复杂度

该算法的时间复杂度为$O\left(n^{2}\right)$。
该算法的空间复杂度为$O\left (n\right)$。

Github 仓库:https ://github.com/vlio20/one-day

于 2015-08-28T10:40:04.193 回答
2

如果您想自己动手,请使用以下代码:
DEMO:http: //jsfiddle.net/CBnJY/11/

var Calendar = function() {
var layOutDay = function(events) {
    var eventsLength = events.length;

    if (!eventsLength) return false;

    // sort events
    events.sort(function(a, b) {
        return a.start - b.start;
    });

    $(".timeSlot").each(function(index, val) {
        var CurSlot = $(this);
        var SlotID = CurSlot.prop("SlotID");
        var EventHeight = CurSlot.height() - 1;
        //alert(SlotID);
        //get events and add to calendar
        var CurrEvent = [];
        for (var i = 0; i < eventsLength; i++) {
            // not sure what is next
            if ((events[i].start <= SlotID) && (SlotID < events[i].end)) {
                CurrEvent.push(events[i]);
            }
        }

        var EventTable = $('<table style="border:1px dashed purple;width:100%"><tr></tr></table');
        for (var x = 0; x < CurrEvent.length; x++) {
            var newEvt = $('<td></td>');
            newEvt.html(CurrEvent[x].start+"-"+CurrEvent[x].end);
            newEvt.addClass("timeEvent");
            newEvt.css("width", (100/CurrEvent.length)+"%");
            newEvt.css("height", EventHeight);
            newEvt.prop("id", CurrEvent[x].id);
            newEvt.appendTo(EventTable.find("tr"));
        }
        EventTable.appendTo(CurSlot);
    });

};

return {
    layOutDay: layOutDay
}
}();

var events = [
{
id: 1,
start: 30,
end: 150},
{
id: 2,
start: 180,
end: 240},
{
id: 3,
start: 180,
end: 240}];

$(document).ready(function() {
var SlotId = 0;
$(".slot").each(function(index, val) {
    var newDiv = $('<div></div>');
    newDiv.prop("SlotID", SlotId)
    //newDiv.html(SlotId);
    newDiv.height($(this).height()+2);
    newDiv.addClass("timeSlot");
    newDiv.appendTo($("#calander"));
    SlotId = SlotId + 30;
});

// call now
Calendar.layOutDay(events);
});

我强烈建议使用http://arshaw.com/fullcalendar/
演示: http: //jsfiddle.net/jGG34/2/
无论你想要实现什么都已经实现,只需启用白天模式并做一些 css黑客..就是这样!

于 2012-05-18T12:48:05.920 回答
1

如果我理解正确,输入是包含开始时间和结束时间的事件列表,对于每个事件,输出是该事件的列号和该事件期间的总列数。您基本上需要为区间图着色;这是一些伪代码。

  1. 对于每个事件e,使两个“瞬间”(开始,e)和(结束,e)指向e

  2. 按时间对这些瞬间进行排序,结束瞬间出现同时的开始瞬间之前。

  3. 初始化一个空列表component、一个空列表column_stack、一个数字num_columns = 0和一个数字num_active = 0component包含将分配相同数量的列的所有事件。column_stack记住哪些列是空闲的。

  4. 按顺序扫描瞬间。如果它是事件的开始时刻e,那么我们需要分配e一列。column_stack如果该列非空,则通过弹出来获取该列;否则,分配一个新列 (number num_columns) 并递增num_columns(基于 1 的索引的其他顺序而不是基于 0 的索引)。附加ecomponent. 增量num_active。如果是结束瞬间,则将e的指定列推到column_stack。减量num_active。如果num_active现在为 0,那么我们通过弹出所有事件component并将它们的总列数设置为开始一个新的连接组件num_columns,然后清除column_stack并重置num_columns为 0。

于 2012-05-16T12:05:20.773 回答
0

我将按如下方式解决问题。

分隔线是一天中没有事件跨越的任何时刻。因此,如果您在上午 9 点到上午 11 点有一个活动,从上午 11 点到下午 1 点有另一个活动,并且没有其他活动,那么在上午 11 点、下午 1 点或之后的任何时间以及 9 点的任何时间都有一个分隔线上午或更早。

我会将每一天分成一组“多事的时间跨度”,它们是不包含分隔符的最大时间跨度。对于每个多事的时间跨度,我将计算同时重叠事件的最大数量,并将其用作该事件跨度的“列号”。然后,我会在计算出的列数上贪婪地布局每个事件的时间跨度,以便每个事件按照事件开始时间的顺序尽可能左布置。

因此,例如以下时间表:

A  9 am - 11 am
B 10 am - 12 pm
C 10 am -  1 pm
D  1 pm  - 2 pm
E  2 pm  - 5 pm
F  3 pm  - 4 pm

将按如下方式处理。多事的时间跨度是上午 9 点 - 下午 1 点、下午 1 点 - 下午 2 点和下午 2 点 - 下午 5 点,因为在下午 1 点和下午 2 点有分隔线(没有事件跨越这些时间)。

在第一个跨度上,最多有三个重叠事件,第二个只有一个,第三个是两个。

列分配如下:

 9 am - 10 am  |   |   |   |
10 am - 11 am  |   |   |   |
11 am - 12 pm  |   |   |   |
12 pm -  1 pm  |   |   |   |___ end of first e.t.s.
 1 pm -  2 pm  |           |___ end of second e.t.s.
 2 pm -  3 pm  |     |     |
 3 pm -  4 pm  |     |     |
 4 pm -  5 pm  |     |     |

之后,按照时间顺序贪婪地填写事件:

 9 am - 10 am  | A |###|###|
10 am - 11 am  |_A_| B | C |
11 am - 12 pm  |###|_B_| C |
12 pm -  1 pm  |###|###|_C_|
 1 pm -  2 pm  |_____D_____|
 2 pm -  3 pm  |  E  |#####|
 3 pm -  4 pm  |  E  |__F__|
 4 pm -  5 pm  |__E__|#####|

这看起来很合理。# 表示空闲空间

于 2012-05-12T18:54:22.057 回答
0

关键点是计算约会中的所有冲突。有一个非常简单的算法可以做到这一点:

  • appointments根据开始日期和结束日期对约会数组进行排序。
  • 维护一个包含所有活动约会的数组active,一开始是空的
  • 为每个约会维护一个值collision(因为您已经有对象,您可以将其存储为另一个属性)[{id : 1, start : 30, end : 150, collisions : 0},...]

appointments通过以下步骤迭代:

  1. 将第一项 ( i) 从appointments
  2. 比较中所有项目的开始日期i和结束日期- 删除所有项目,j其中<activej.enddatei.startdate
  3. 更新所有剩余的碰撞j(每个+1)
  4. i( i.collision = active.length)的更新碰撞
  5. 弹出i数组active

对 的所有项目重复这些步骤appointments

例子:

(当心,伪代码):

var unsorted = [7,9],[2,8],[1,3],[2,5],[10,12]
// var appointments = sort(unsorted);
var appointments = [1,3],[2,5],[2,8],[7,9],[10,12]

// now for all items of appoitments:
for (var x = 0; x<appointments.length;x++){
    var i = appointments[x];                 // step 1
    for (var j=0; j<active.length;j++){      
     // remove j if j.enddate < j.startdate  // step 2
     // else j.collision += 1;               // step 3
    }
    i.collision = active.length;             // step 4
    active.pop(i);                           // step 5
}

如果您收集从活动中删除的项目,您会得到一个数组,按开始日期之前的结束日期排序,您可以使用它来显示 div。

现在,试试您是否可以获得代码以使其工作,如果您需要进一步的帮助,请写评论。

于 2012-05-16T12:26:19.270 回答