javascript - 如何正确显示盒子? (算法、用户界面)

标签 javascript reactjs typescript algorithm frontend

我目前正在尝试使用 React Typescript 生成一个 View ,在其中可以显示一天的所有约会(类似于 Outlook 日历)。但我面临一个问题。当约会重叠时,我如何确定时间和位置?所以我想我可能需要一个算法来完成这个问题。

这里是一个如何构建它的示例: Example Calendar

每个方框代表一次约会

对象的类如下所示:

class AppointmentStructured {
  appointment: Appointment
  width?: number
  left?: number
}

class Appointment {
start: Date
end: Date
subject:string
duration: number
}

我正在寻找一种解决方案,可以确定特定约会的宽度(最大 1 = 100%),此外我还需要该位置。最简单的是距离左侧有多远(最大 1)。当同时开始的预约超过 2 个时,按照持续时间最长的预约进行排序。

示例图像中的第三个框是:

  • 宽度:0.333
  • 左:0.666

根据这些数据,我可以使用 CSS 来设计它们。 生成的 CSS 看起来或多或少像这样:

 position: absolute;
 width: calc(100% * 0.33333);
 top: 20rem; //doesn't matter for the algorithm
 left: calc(100% * 0.66666)

--编辑 这就是我走了多远。它以 generateAppointmentTile([])

方法开始
export interface AppointmentStructured {
    appointment: Appointment
    width: string | number;
    left: string | number
}

export const generateAppointmentTile = (appointments: Appointment[]): AppointmentStructured[] => {
    var appointmentsStructured = initAppointmentStructured(appointments);
    var response: AppointmentStructured[] = [];

    for (var i = 0; i < appointmentsStructured.length; ++i) {
        var width = 1;

        var previous = getPreviousOverlapping(appointmentsStructured, i);
        var previousWidth = previous
            .map((item: AppointmentStructured): number => item.width as number)
            .reduce((a: number, b: number): number => a + b, 0)

        var forward = getForwardOverlapping(appointmentsStructured, i);
        var forwardOverlays = appointmentOverlays(forward);

        var previousHasOverlayWithForward = false;
        previous.forEach((structured: AppointmentStructured) => checkAppointmentOverlaySingle(structured, forward) !== 0 ? previousHasOverlayWithForward = true :  null);


        width = (width - previousWidth) / (!previousHasOverlayWithForward && forwardOverlays !== 0 ? forwardOverlays : 1);
        appointmentsStructured[i].width = width;
        response.push(appointmentsStructured[i]);
    }

    response.forEach((value: AppointmentStructured): void => {
        value.width = `calc((100% - 8rem) * ${value.width})`;
    });

    return response
}
const appointmentOverlays = (reading: AppointmentStructured[]): number => {
    var highestNumber = 0;

    reading.forEach((structured: AppointmentStructured): void => {
        var start = checkAppointmentOverlaySingle(structured, reading) + 1;
        highestNumber = start > highestNumber ? start : highestNumber;
    });

    return highestNumber;
}

const checkAppointmentOverlaySingle = (structured: AppointmentStructured, reading: AppointmentStructured[]): number => {
    var start = 0;

    reading.forEach((item: AppointmentStructured): void => {
        if (item.appointment.id !== structured.appointment.id) {
            if ((structured.appointment.start <= item.appointment.start && structured.appointment.end >= item.appointment.start)
                || (structured.appointment.start >= item.appointment.start && structured.appointment.start <= item.appointment.end)) {
                start += 1;
            }
        }
    });

    return start;
}

const getPreviousOverlapping = (appointmentsStructured: AppointmentStructured[], index: number): AppointmentStructured[] => {
    var response: AppointmentStructured[] = [];

    for (var i = index - 1; i >= 0; --i) {
        if (appointmentsStructured[index].appointment.start >= appointmentsStructured[i].appointment.start
            && appointmentsStructured[index].appointment.start <= appointmentsStructured[i].appointment.end) {
            response.push(appointmentsStructured[i]);
        }
    }
    return response;
}

const getForwardOverlapping = (appointmentsStructured: AppointmentStructured[], index: number): AppointmentStructured[] => {
    var response: AppointmentStructured[] = [];

    for (var i = index; i < appointmentsStructured.length; ++i) {
        if (appointmentsStructured[index].appointment.start >= appointmentsStructured[i].appointment.start
            && appointmentsStructured[index].appointment.start <= appointmentsStructured[i].appointment.end) {
            response.push(appointmentsStructured[i]);
        }
    }
    return response;
}

const initAppointmentStructured = (appointments: Appointment[]): AppointmentStructured[] => {
    var appointmentsStructured: AppointmentStructured[] = appointments
        .sort((a: Appointment, b: Appointment): number => a.start.getTime() - b.start.getTime())
        .map((appointment: Appointment): AppointmentStructured => ({ appointment, width: 100, left: 0 }));
    var response: AppointmentStructured[] = [];

    // sort in a intelligent way
    for (var i = 0; i < appointmentsStructured.length; ++i) {
        var duration = appointmentsStructured[i].appointment.end.getTime() - appointmentsStructured[i].appointment.start.getTime();
        var sameStartAppointments = findAppointmentWithSameStart(appointmentsStructured[i], appointmentsStructured);
        var hasLongerAppointment: boolean = false;
        sameStartAppointments.forEach((structured: AppointmentStructured) => (structured.appointment.end.getTime() - structured.appointment.start.getTime()) > duration ? hasLongerAppointment = true : null);

        if (!hasLongerAppointment) {
            response.push(appointmentsStructured[i]);
            appointmentsStructured.splice(i, 1);
            i = -1;
        }
    }

    return response.sort((a: AppointmentStructured, b: AppointmentStructured): number => a.appointment.start.getTime() - b.appointment.start.getTime());
}

const findAppointmentWithSameStart = (structured: AppointmentStructured, all: AppointmentStructured[]): AppointmentStructured[] => {
    var response: AppointmentStructured[] = [];
    all.forEach((appointmentStructured: AppointmentStructured) => appointmentStructured.appointment.start === structured.appointment.start
        && appointmentStructured.appointment.id !== structured.appointment.id ? response.push(appointmentStructured) : null)
    return response;
}

即使是一些伪代码也会对我有很大帮助。

最佳答案

该解决方案基于以下答案:Visualization of calendar events. Algorithm to layout events with maximum width

我刚刚将脚本从 C# 移植到 JS 并提取了一些步骤。

  1. 将所有间隔/约会分为不同的组,其中一组不会影响另一组。
  2. 对于每个组,从左到右填充列(每个约会的宽度相同)。只需根据需要使用尽可能多的列即可。
  3. 最大限度地延长每次约会。
  4. 从中制作实际的盒子并渲染它们。

这个过程并不完美。有时,左边的盒子很薄,而右边的盒子一直被拉伸(stretch)。这种情况主要发生在左框到底部有很长的“重叠链”时,但实际上并不会发生。

/*
interface Interval {
  start: number;
  end: number;
}
*/

// Just for demonstration purposes.
function generateIntervals({ count, maxStart, maxEnd, minLength, maxLength, segments }) {
  return Array.from(Array(count), () => randomInterval())

  function randomInterval() {
    const start = randomInt(0, maxStart * segments) / segments
    const length = randomInt(minLength * segments, maxLength * segments) / segments
    return {
      start,
      end: Math.min(start + length, maxEnd),
      text: "Here could be your advertising :)"
    }
  }

  function randomInt(start, end) {
    return Math.floor(Math.random() * (end - start) + start)
  }
}

function isOverlapping(interval1, interval2) {
  const start = Math.max(interval1.start, interval2.start)
  const end = Math.min(interval1.end, interval2.end)
  return start < end
}

// Sorts by start ascending.
function sortIntervalsByStart(intervals) {
  return intervals
    .slice()
    .sort(({ start: s1 }, { start: s2 }) => s1 - s2)
}

// Split intervals into groups, which are independent of another.
function groupIntervals(intervals) {
  const groups = []
  let latestIntervalEnd = -Infinity
  for (const interval of sortIntervalsByStart(intervals)) {
    const { start, end } = interval
    // There is no overlap to previous intervals. Create a new group.
    if (start >= latestIntervalEnd) {
      groups.push([])
    }
    groups[groups.length - 1].push(interval)
    latestIntervalEnd = Math.max(latestIntervalEnd, end)
  }
  return groups
}

// Fill columns with equal width from left to right.
function putIntervalsIntoColumns(intervals) {
  const columns = []
  for (const interval of intervals) {
    let columnIndex = findFreeColumn(interval)
    columns[columnIndex] = (columns[columnIndex] || []).concat([interval])
  }
  return columns

  function findFreeColumn(interval) {
    let columnIndex = 0
    while (
      columns?.[columnIndex]
        ?.some(otherInterval => isOverlapping(interval, otherInterval))
    ) {
      columnIndex++
    }
    return columnIndex
  }
}

// Expand columns maximally.
function makeBoxes(columns, containingInterval) {
  const boxes = []
  for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
    for (const interval of columns[columnIndex]) {
      const columnSpan = findColumnSpan(columnIndex, interval)
      const box = {
        ...interval,
        top: (interval.start - containingInterval.start) / (containingInterval.end - containingInterval.start),
        height: (interval.end - interval.start) / (containingInterval.end - containingInterval.start),
        left: columnIndex / columns.length,
        width: columnSpan / columns.length
      }
      boxes.push(box)
    }
  }
  return boxes

  function findColumnSpan(columnIndex, interval) {
    let columnSpan = 1
    while (
      columns
        ?.[columnIndex + columnSpan]
        ?.every(otherInterval => !isOverlapping(interval, otherInterval))
    ) {
      columnSpan++
    }
    return columnSpan
  }
}

function computeBoxes(intervals, containingInterval) {
  return groupIntervals(intervals)
    .map(intervals => putIntervalsIntoColumns(intervals))
    .flatMap(columns => makeBoxes(columns, containingInterval))
}

function renderBoxes(boxes) {
  const calendar = document.querySelector("#calendar")

  for (const { top, left, width, height, text = "" } of boxes) {
    const el = document.createElement("div")
    el.style.position = "absolute"
    el.style.top = `${top * 100}%`
    el.style.left = `${left * 100}%`
    el.style.width = `${width * 100}%`
    el.style.height = `${height * 100}%`
    el.textContent = text
    calendar.appendChild(el)
  }
}

const hours = 24
const intervals = generateIntervals({
  count: 20,
  maxStart: 22,
  maxEnd: hours,
  maxLength: 4,
  minLength: 1,
  segments: 2,
})
const boxes = computeBoxes(intervals, { start: 0, end: hours })
renderBoxes(boxes)
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}

#calendar {
  height: 100vh;
  width: 100vw;
  min-height: 800px;
  position: relative;
}

#calendar > * {
  border: 1px solid black;
  background-color: #b89d9d;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  overflow-y: auto;
}
<div id="calendar"></div>
<script src="./index.js"></script>

关于javascript - 如何正确显示盒子? (算法、用户界面),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/65736299/

相关文章:

javascript - 如何使用 id mongodb 在嵌入式文档数组中查找数据?

javascript - jQuery - 提交表单...但也传递提交输入名称和 ID

javascript - 如何从 Material-UI 将状态添加到函数

html - 选择框默认选定值未显示在 Angular 2 的选择框中

javascript - 在 TypeScript 中使用符号作为对象键类型

Angular 4 : Disabling nested form validators

javascript - 加载此标签时如何更改 svg 文本标签 X Y?

javascript - Angular Js 拖放 - 将 $index 传递给指令

javascript - Redux-Saga 为单个操作运行两次

javascript - 尝试使用 Next.js/React.js 时出现未处理的运行时错误,