likes
comments
collection
share

日历时间轴事件展示

作者站长头像
站长
· 阅读数 24

实现逻辑

  1. 对事件根据时间进行排序
  2. 根据事件是否有重叠时间进行分组(同一个组内事件展示在一行)
  3. 确定组内事件的展示位置

一、事件排序

使用第三方库对事件按照开始时间排序,若开始时间一致,以结束时间进行排序。以 dayjs为例:

import dayjs from 'dayjs'

interface Event {
  start: string
  end: string
  name: string
}

export function sort(events: Event[]): Evnet[] {
  return events.sort((a, b) => {
    if (a.start === b.start)
      return dayjs(a.end).isBefore(dayjs(b.end)) ? -1 : 1
    return dayjs(a.start).isBefore(dayjs(b.start)) ? -1 : 1
  })
}

二、事件分组

事件分组根据事件时间是否有重叠,判定两个已排序事件是否有重叠的条件是后一个事件开始时间是否早于上一个事件的结束时间。进一步,在两个有重叠时间的事件基础上,判断第三件事件是否重叠,那就需要知道前两个事件的哪一个的结束时间更晚,前两个重叠时间的组可以看作为是一个新的事件,这个新的事件结束时间添加在新增事件时可能需要更新。

import dayjs from 'dayjs'
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
import { sort } from './sort'

dayjs.extend(isSameOrAfter)

interface Group {
  start: string
  end: string
  events: Event[]
}

export function group(events: Event[]): Group[] {
  events = sort(events)

  const result: Group[] = []
  let currentGroup: Group | null = null

  events.forEach((event) => {
    if (!currentGroup || dayjs(event.start).isSameOrAfter(currentGroup.end)) {
      currentGroup = {
        start: event.start,
        end: event.end,
        events: [event],
      }
      result.push(currentGroup)
    }
    else if (dayjs(event.start).isBefore(currentGroup.end)) {
      if (dayjs(event.end).isAfter(currentGroup.end))
        currentGroup.end = event.end
      currentGroup.events.push(event)
    }
  })

  return result
}

测试用例:

import { group } from './group'

describe('group', () => {
  it('should return one group when just one event', () => {
    const events = [{
      start: '20210101080000',
      end: '20210101090000',
      name: 'Meeting 1',
    }]
    const result = group(events)
    expect(result).toMatchInlineSnapshot(`
      [
        {
          "end": "20210101090000",
          "events": [
            {
              "end": "20210101090000",
              "name": "Meeting 1",
              "start": "20210101080000",
            },
          ],
          "start": "20210101080000",
        },
      ]
    `)
  })

  it('should return two group when two events have not overlap', () => {
    const events = [{
      start: '20210101080000',
      end: '20210101090000',
      name: 'Meeting 1',
    }, {
      start: '20210101090000',
      end: '20210101010000',
      name: 'Meeting 2',
    }]
    const result = group(events)
    expect(result).toMatchInlineSnapshot(`
      [
        {
          "end": "20210101090000",
          "events": [
            {
              "end": "20210101090000",
              "name": "Meeting 1",
              "start": "20210101080000",
            },
          ],
          "start": "20210101080000",
        },
        {
          "end": "20210101010000",
          "events": [
            {
              "end": "20210101010000",
              "name": "Meeting 2",
              "start": "20210101090000",
            },
          ],
          "start": "20210101090000",
        },
      ]
    `)
  })

  it('should return one group when two events have overlap', () => {
    const events = [{
      start: '20210101080000',
      end: '20210101090000',
      name: 'Meeting 1',
    }, {
      start: '20210101083000',
      end: '20210101093000',
      name: 'Meeting 2',
    }]
    const result = group(events)
    expect(result).toMatchInlineSnapshot(`
      [
        {
          "end": "20210101093000",
          "events": [
            {
              "end": "20210101090000",
              "name": "Meeting 1",
              "start": "20210101080000",
            },
            {
              "end": "20210101093000",
              "name": "Meeting 2",
              "start": "20210101083000",
            },
          ],
          "start": "20210101080000",
        },
      ]
    `)
  })

  it('should return one group when three events have overlap', () => {
    const events = [{
      start: '20210101080000',
      end: '20210101090000',
      name: 'Meeting 1',
    }, {
      start: '20210101083000',
      end: '20210101093000',
      name: 'Meeting 2',
    }, {
      start: '20210101090000',
      end: '20210101100000',
      name: 'Meeting 3',
    }]
    const result = group(events)
    expect(result).toMatchInlineSnapshot(`
      [
        {
          "end": "20210101100000",
          "events": [
            {
              "end": "20210101090000",
              "name": "Meeting 1",
              "start": "20210101080000",
            },
            {
              "end": "20210101093000",
              "name": "Meeting 2",
              "start": "20210101083000",
            },
            {
              "end": "20210101100000",
              "name": "Meeting 3",
              "start": "20210101090000",
            },
          ],
          "start": "20210101080000",
        },
      ]
    `)
  })
})

三、按组排位

同一个组内的时间有重叠,在界面展示时在同一行,分为个列进行展示,如下图。 日历时间轴事件展示

目标:确定组内重叠的事件总共需要放几列和每个事件在第几列。以上面五个事件时间作为示例:

  1. 会议 1 (8:30-10:30),计算在第一个位置,记录起始下标为 0,当前组有 1 列;
  2. 会议 2 (9:00-10:00),计算在第二个位置,记录下标为 1,当前组有 2 列;
  3. 会议 3 (9:30-10:30),计算在第三个位置,记录下标为 2,当前组有 3 列;
  4. 会议 4 (10:20-11:20),计算在第二个位置,放置在会议 2 的下面,下标也同为 1,当前组有 3 列;
  5. 会议 5 (10:40-11:40),计算在第一个位置,放置在会议 1 的下面,下标也同为 0,当前组有 3 列;
import dayjs from 'dayjs'
import isSameOrAfter from 'dayjs/plugin/isSameOrAfter'
import { group } from './group'

dayjs.extend(isSameOrAfter)

interface Event {
  start: string
  end: string
  name: string
  left?: number
  isPlacement?: boolean
  isStack?: boolean
}

interface Group {
  start: string
  end: string
  events: Event[]
  columnCount?: number
}

export function layout(events: Event[]): Map<string, number[]> {
  const result = new Map<string, number[]>()
  const groups = group(events)

  for (const group of groups) {
    const { events } = group
    let columnCount = 0
    let left = 0
    events.forEach((event) => {
      const placementEvents = events.filter(e => e.isPlacement && !e.isStack).sort((a, b) => dayjs(a.end).isBefore(dayjs(b.end)) ? -1 : 1)
      const stackEvent = placementEvents.find(e => dayjs(event.start).isSameOrAfter(dayjs(e.end)))
      if (stackEvent) {
        event.left = stackEvent.left
        event.isPlacement = true
        stackEvent.isStack = true
      }
      else {
        event.left = left
        event.isPlacement = true
        left++
        columnCount++
      }
    })
    group.columnCount = columnCount
  }

  for (const group of groups) {
    const { events } = group
    events.forEach((event) => {
      result.set(event.name, [event.left ?? -1, group.columnCount || 0])
    })
  }

  return result
}

注意点:

  1. 计算事件位置时需要判断是否可以放置在已排好位置的事件下面,开始时间晚于已排好序事件的结束时间,如果有就无需更新 left 下标位置和columnCount 列的值,同时需要标记被堆叠的事件,该两个事件可以看作是一个事件,
  2. 为了使事件排列更紧凑,需要将已排好位置的事件按照结束时间再次进行排序来寻找结束时间早于事件开始时间的位置。

测试用例:

import { layout } from './layout'

describe('layout', () => {
  it('should return one row when just one event', () => {
    const events = [{
      start: '20210101080000',
      end: '20210101090000',
      name: 'Meeting 1',
    }]
    const result = layout(events)
    expect(result).toMatchInlineSnapshot(`
      Map {
        "Meeting 1" => [
          0,
          1,
        ],
      }
    `)
  })

  it('should return two rows when two events have not overlap', () => {
    const events = [{
      start: '20210101080000',
      end: '20210101090000',
      name: 'Meeting 1',
    }, {
      start: '20210101090000',
      end: '20210101100000',
      name: 'Meeting 2',
    }]
    const result = layout(events)
    expect(result).toMatchInlineSnapshot(`
      Map {
        "Meeting 1" => [
          0,
          1,
        ],
        "Meeting 2" => [
          0,
          1,
        ],
      }
    `)
  })

  it('should return one row when two events have overlap', () => {
    const events = [{
      start: '20210101080000',
      end: '20210101090000',
      name: 'Meeting 1',
    }, {
      start: '20210101083000',
      end: '20210101093000',
      name: 'Meeting 2',
    }]
    const result = layout(events)
    expect(result).toMatchInlineSnapshot(`
      Map {
        "Meeting 1" => [
          0,
          2,
        ],
        "Meeting 2" => [
          1,
          2,
        ],
      }
    `)
  })

  it('should return one row when three events have right stack', () => {
    const events = [{
      start: '20210101080000',
      end: '20210101100000',
      name: 'Meeting 1',
    }, {
      start: '20210101083000',
      end: '20210101090000',
      name: 'Meeting 2',
    }, {
      start: '20210101090000',
      end: '20210101093000',
      name: 'Meeting 3',
    }]
    const result = layout(events)
    expect(result).toMatchInlineSnapshot(`
      Map {
        "Meeting 1" => [
          0,
          2,
        ],
        "Meeting 2" => [
          1,
          2,
        ],
        "Meeting 3" => [
          1,
          2,
        ],
      }
    `)
  })

  it('should return one row when three events have left stack', () => {
    const events = [{
      start: '20210101083000',
      end: '20210101090000',
      name: 'Meeting 1',
    }, {
      start: '20210101093000',
      end: '20210101100000',
      name: 'Meeting 2',
    }, {
      start: '20210101084500',
      end: '20210101094500',
      name: 'Meeting 3',
    }]
    const result = layout(events)
    expect(result).toMatchInlineSnapshot(`
      Map {
        "Meeting 1" => [
          0,
          2,
        ],
        "Meeting 3" => [
          1,
          2,
        ],
        "Meeting 2" => [
          0,
          2,
        ],
      }
    `)
  })

  it('should return one row when four events overlap', () => {
    const events = [{
      start: '20210101083000',
      end: '20210101100000',
      name: 'Meeting 1',
    }, {
      start: '20210101084500',
      end: '20210101094500',
      name: 'Meeting 2',
    }, {
      start: '20210101090000',
      end: '20210101100000',
      name: 'Meeting 3',
    }, {
      start: '20210101095000',
      end: '20210101105000',
      name: 'Meeting 4',
    }]
    const result = layout(events)
    expect(result).toMatchInlineSnapshot(`
      Map {
        "Meeting 1" => [
          0,
          3,
        ],
        "Meeting 2" => [
          1,
          3,
        ],
        "Meeting 3" => [
          2,
          3,
        ],
        "Meeting 4" => [
          1,
          3,
        ],
      }
    `)
  })

  it('should return one row when five events overlap', () => {
    const events = [{
      start: '20210101083000',
      end: '20210101100000',
      name: 'Meeting 1',
    }, {
      start: '20210101084500',
      end: '20210101094500',
      name: 'Meeting 2',
    }, {
      start: '20210101090000',
      end: '20210101100000',
      name: 'Meeting 3',
    }, {
      start: '20210101095000',
      end: '20210101105000',
      name: 'Meeting 4',
    }, {
      start: '20210101095500',
      end: '20210101105500',
      name: 'Meeting 5',
    }]
    const result = layout(events)
    expect(result).toMatchInlineSnapshot(`
      Map {
        "Meeting 1" => [
          0,
          4,
        ],
        "Meeting 2" => [
          1,
          4,
        ],
        "Meeting 3" => [
          2,
          4,
        ],
        "Meeting 4" => [
          1,
          4,
        ],
        "Meeting 5" => [
          3,
          4,
        ],
      }
    `)
  })

  it('should return one row when four events overlap and sort stack by endTime', () => {
    const events = [{
      start: '20210101083000',
      end: '2021010110000',
      name: 'Meeting 1',
    }, {
      start: '20210101084500',
      end: '20210101094500',
      name: 'Meeting 2',
    }, {
      start: '20210101090000',
      end: '20210101110000',
      name: 'Meeting 3',
    }, {
      start: '20210101100000',
      end: '20210101110000',
      name: 'Meeting 4',
    }]
    const result = layout(events)
    expect(result).toMatchInlineSnapshot(`
      Map {
        "Meeting 1" => [
          0,
          3,
        ],
        "Meeting 2" => [
          1,
          3,
        ],
        "Meeting 3" => [
          2,
          3,
        ],
        "Meeting 4" => [
          1,
          3,
        ],
      }
    `)
  })

  it.only('should return one row when five events overlap and sort stack by endTime', () => {
    const events = [{
      start: '20210101083000',
      end: '20210101100000',
      name: 'Meeting 1',
    }, {
      start: '20210101084500',
      end: '20210101094500',
      name: 'Meeting 2',
    }, {
      start: '20210101090000',
      end: '20210101110000',
      name: 'Meeting 3',
    }, {
      start: '20210101100000',
      end: '20210101110000',
      name: 'Meeting 4',
    }, {
      start: '20210101100000',
      end: '20210101110000',
      name: 'Meeting 5',
    }]
    const result = layout(events)
    expect(result).toMatchInlineSnapshot(`
      Map {
        "Meeting 1" => [
          0,
          3,
        ],
        "Meeting 2" => [
          1,
          3,
        ],
        "Meeting 3" => [
          2,
          3,
        ],
        "Meeting 4" => [
          1,
          3,
        ],
        "Meeting 5" => [
          0,
          3,
        ],
      }
    `)
  })

  it('should return two rows when four events which three overlaps', () => {
    const events = [{
      start: '20210101083000',
      end: '20210101093000',
      name: 'Meeting 1',
    }, {
      start: '20210101090000',
      end: '20210101100000',
      name: 'Meeting 2',
    }, {
      start: '20210101093000',
      end: '20210101103000',
      name: 'Meeting 3',
    }, {
      start: '20210101104500',
      end: '20210101113000',
      name: 'Meeting 4',
    }]
    const result = layout(events)
    expect(result).toMatchInlineSnapshot(`
      Map {
        "Meeting 1" => [
          0,
          2,
        ],
        "Meeting 2" => [
          1,
          2,
        ],
        "Meeting 3" => [
          0,
          2,
        ],
        "Meeting 4" => [
          0,
          1,
        ],
      }
    `)
  })
})
转载自:https://juejin.cn/post/7384265691162181666
评论
请登录