You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

日历项布局算法需求:最小化垂直空间的彩色条y/top计算

日历事件垂直空间优化方案

这是个非常典型的日历布局问题,核心就是让互不重叠的事件尽可能共享同一垂直行,从而最小化整体的垂直空间占用。我给你分享两种实用的实现思路,用JavaScript就能轻松搞定:

一、核心前提:先排序事件

不管用哪种算法,第一步必须先把所有事件按开始时间从小到大排序(如果开始时间相同,建议按结束时间从小到大排,这样短事件优先安排,能更高效利用空间)。这是后续判断重叠的基础:

const sortedEvents = [...events].sort((a, b) => {
  if (a.start !== b.start) {
    return a.start - b.start;
  }
  // 开始时间相同时,短事件优先
  return a.end - b.end;
});

二、基础实现:暴力遍历法(适合事件量少的场景)

这个方法逻辑简单易懂,适合中小规模的事件列表(比如单日日历):

  1. 维护一个数组rowEnds,用来记录每一行最后一个事件的结束时间
  2. 遍历每个已排序的事件,找到第一个“结束时间 ≤ 当前事件开始时间”的行,把事件放到这一行
  3. 如果找不到这样的行,就新增一行
  4. 最后根据事件所在的行号,计算top值(行号 × 固定行高)

示例代码

/**
 * 计算日历事件的top值,实现垂直空间最小化
 * @param {Array} events - 事件列表,每个事件需包含start(开始时间戳)、end(结束时间戳)
 * @param {number} rowHeight - 每行的固定高度
 * @returns {Array} 带top值的事件列表
 */
const calculateEventPositions = (events, rowHeight = 60) => {
  const sortedEvents = [...events].sort((a, b) => {
    if (a.start !== b.start) return a.start - b.start;
    return a.end - b.end;
  });

  const rowEnds = []; // 记录每行的最后一个事件的结束时间

  return sortedEvents.map(event => {
    // 找到第一个可以容纳当前事件的行
    const targetRowIndex = rowEnds.findIndex(endTime => endTime <= event.start);

    if (targetRowIndex !== -1) {
      // 复用已有行,更新该行的结束时间为当前事件的结束时间
      rowEnds[targetRowIndex] = event.end;
      return { ...event, top: targetRowIndex * rowHeight };
    } else {
      // 新增一行,加入当前事件的结束时间
      rowEnds.push(event.end);
      return { ...event, top: (rowEnds.length - 1) * rowHeight };
    }
  });
};

三、进阶优化:优先队列(最小堆)法(适合大量事件)

如果你的日历需要处理成百上千个事件,暴力遍历的findIndex会导致O(n²)的时间复杂度,这时候可以用**最小堆(优先队列)**来优化,把时间复杂度降到O(n log n):

  1. 用最小堆来维护当前所有行的结束时间,堆顶始终是最早结束的行
  2. 遍历每个事件时,取出堆顶的结束时间:
    • 如果堆顶结束时间 ≤ 当前事件开始时间,就把当前事件放到这一行,然后将堆顶更新为当前事件的结束时间
    • 如果堆顶结束时间 > 当前事件开始时间,说明所有行都在忙碌,新增一行,把当前事件的结束时间加入堆
  3. 同样根据行号计算top值(可以在堆中同时存储行号和结束时间)

伪代码思路

// 先实现一个简单的最小堆(或者用第三方库如heap-js)
class MinHeap {
  constructor() {
    this.heap = [];
  }
  // 堆的基础方法:push, pop, peek等...
}

const calculateEventPositionsAdvanced = (events, rowHeight = 60) => {
  const sortedEvents = [...events].sort((a, b) => a.start - b.start);
  const heap = new MinHeap();
  const eventPositions = [];

  for (const event of sortedEvents) {
    if (heap.size() > 0 && heap.peek().end <= event.start) {
      // 复用最早结束的行
      const row = heap.pop();
      eventPositions.push({ ...event, top: row.index * rowHeight });
      heap.push({ end: event.end, index: row.index });
    } else {
      // 新增一行
      const newRowIndex = heap.size();
      eventPositions.push({ ...event, top: newRowIndex * rowHeight });
      heap.push({ end: event.end, index: newRowIndex });
    }
  }

  return eventPositions;
};

四、额外注意点

  • 事件的start和end建议统一用时间戳(毫秒数),方便比较
  • 如果你的日历是按天展示,也可以把时间转换成当天的分钟数(比如0点是0,23:59是1439),计算逻辑完全一致
  • 要是需要支持事件的拖拽调整,调整后重新调用这个方法即可更新top值

内容的提问来源于stack exchange,提问作者L G

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 09:19:09