我目前正在尝试使用 React Typescript 生成一个 View ,在其中可以显示一天的所有约会(类似于 Outlook 日历)。但我面临一个问题。当约会重叠时,我如何确定时间和位置?所以我想我可能需要一个算法来完成这个问题。
每个方框代表一次约会
对象的类如下所示:
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 并提取了一些步骤。
- 将所有间隔/约会分为不同的组,其中一组不会影响另一组。
- 对于每个组,从左到右填充列(每个约会的宽度相同)。只需根据需要使用尽可能多的列即可。
- 最大限度地延长每次约会。
- 从中制作实际的盒子并渲染它们。
这个过程并不完美。有时,左边的盒子很薄,而右边的盒子一直被拉伸(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/