242 lines
7.0 KiB
JavaScript
242 lines
7.0 KiB
JavaScript
import { getOverlapGroupHandler, getVisuals, hasOverlap, getNormalizedRange } from './common';
|
|
import { getTimestampIdentifier } from '../util/timestamp';
|
|
const FULL_WIDTH = 100;
|
|
const DEFAULT_OFFSET = 5;
|
|
const WIDTH_MULTIPLIER = 1.7;
|
|
/**
|
|
* Variation of column mode where events can be stacked. The priority of this
|
|
* mode is to stack events together taking up the least amount of space while
|
|
* trying to ensure the content of the event is always visible as well as its
|
|
* start and end. A sibling column has intersecting event content and must be
|
|
* placed beside each other. Non-sibling columns are offset by 5% from the
|
|
* previous column. The width is scaled by 1.7 so the events overlap and
|
|
* whitespace is reduced. If there is a hole in columns the event width is
|
|
* scaled up so it intersects with the next column. The columns have equal
|
|
* width in the space they are given. If the event doesn't have any to the
|
|
* right of it that intersect with it's content it's right side is extended
|
|
* to the right side.
|
|
*/
|
|
|
|
export const stack = (events, firstWeekday, overlapThreshold) => {
|
|
const handler = getOverlapGroupHandler(firstWeekday); // eslint-disable-next-line max-statements
|
|
|
|
return (day, dayEvents, timed, reset) => {
|
|
if (!timed) {
|
|
return handler.getVisuals(day, dayEvents, timed, reset);
|
|
}
|
|
|
|
const dayStart = getTimestampIdentifier(day);
|
|
const visuals = getVisuals(dayEvents, dayStart);
|
|
const groups = getGroups(visuals, dayStart);
|
|
|
|
for (const group of groups) {
|
|
const nodes = [];
|
|
|
|
for (const visual of group.visuals) {
|
|
const child = getNode(visual, dayStart);
|
|
const index = getNextIndex(child, nodes);
|
|
|
|
if (index === false) {
|
|
const parent = getParent(child, nodes);
|
|
|
|
if (parent) {
|
|
child.parent = parent;
|
|
child.sibling = hasOverlap(child.start, child.end, parent.start, addTime(parent.start, overlapThreshold));
|
|
child.index = parent.index + 1;
|
|
parent.children.push(child);
|
|
}
|
|
} else {
|
|
const [parent] = getOverlappingRange(child, nodes, index - 1, index - 1);
|
|
const children = getOverlappingRange(child, nodes, index + 1, index + nodes.length, true);
|
|
child.children = children;
|
|
child.index = index;
|
|
|
|
if (parent) {
|
|
child.parent = parent;
|
|
child.sibling = hasOverlap(child.start, child.end, parent.start, addTime(parent.start, overlapThreshold));
|
|
parent.children.push(child);
|
|
}
|
|
|
|
for (const grand of children) {
|
|
if (grand.parent === parent) {
|
|
grand.parent = child;
|
|
}
|
|
|
|
const grandNext = grand.index - child.index <= 1;
|
|
|
|
if (grandNext && child.sibling && hasOverlap(child.start, addTime(child.start, overlapThreshold), grand.start, grand.end)) {
|
|
grand.sibling = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
nodes.push(child);
|
|
}
|
|
|
|
calculateBounds(nodes, overlapThreshold);
|
|
}
|
|
|
|
visuals.sort((a, b) => a.left - b.left || a.event.startTimestampIdentifier - b.event.startTimestampIdentifier);
|
|
return visuals;
|
|
};
|
|
};
|
|
|
|
function calculateBounds(nodes, overlapThreshold) {
|
|
for (const node of nodes) {
|
|
const {
|
|
visual,
|
|
parent
|
|
} = node;
|
|
const columns = getMaxChildIndex(node) + 1;
|
|
const spaceLeft = parent ? parent.visual.left : 0;
|
|
const spaceWidth = FULL_WIDTH - spaceLeft;
|
|
const offset = Math.min(DEFAULT_OFFSET, FULL_WIDTH / columns);
|
|
const columnWidthMultiplier = getColumnWidthMultiplier(node, nodes);
|
|
const columnOffset = spaceWidth / (columns - node.index + 1);
|
|
const columnWidth = spaceWidth / (columns - node.index + (node.sibling ? 1 : 0)) * columnWidthMultiplier;
|
|
|
|
if (parent) {
|
|
visual.left = node.sibling ? spaceLeft + columnOffset : spaceLeft + offset;
|
|
}
|
|
|
|
visual.width = hasFullWidth(node, nodes, overlapThreshold) ? FULL_WIDTH - visual.left : Math.min(FULL_WIDTH - visual.left, columnWidth * WIDTH_MULTIPLIER);
|
|
}
|
|
}
|
|
|
|
function getColumnWidthMultiplier(node, nodes) {
|
|
if (!node.children.length) {
|
|
return 1;
|
|
}
|
|
|
|
const maxColumn = node.index + nodes.length;
|
|
const minColumn = node.children.reduce((min, c) => Math.min(min, c.index), maxColumn);
|
|
return minColumn - node.index;
|
|
}
|
|
|
|
function getOverlappingIndices(node, nodes) {
|
|
const indices = [];
|
|
|
|
for (const other of nodes) {
|
|
if (hasOverlap(node.start, node.end, other.start, other.end)) {
|
|
indices.push(other.index);
|
|
}
|
|
}
|
|
|
|
return indices;
|
|
}
|
|
|
|
function getNextIndex(node, nodes) {
|
|
const indices = getOverlappingIndices(node, nodes);
|
|
indices.sort();
|
|
|
|
for (let i = 0; i < indices.length; i++) {
|
|
if (i < indices[i]) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function getOverlappingRange(node, nodes, indexMin, indexMax, returnFirstColumn = false) {
|
|
const overlapping = [];
|
|
|
|
for (const other of nodes) {
|
|
if (other.index >= indexMin && other.index <= indexMax && hasOverlap(node.start, node.end, other.start, other.end)) {
|
|
overlapping.push(other);
|
|
}
|
|
}
|
|
|
|
if (returnFirstColumn && overlapping.length > 0) {
|
|
const first = overlapping.reduce((min, n) => Math.min(min, n.index), overlapping[0].index);
|
|
return overlapping.filter(n => n.index === first);
|
|
}
|
|
|
|
return overlapping;
|
|
}
|
|
|
|
function getParent(node, nodes) {
|
|
let parent = null;
|
|
|
|
for (const other of nodes) {
|
|
if (hasOverlap(node.start, node.end, other.start, other.end) && (parent === null || other.index > parent.index)) {
|
|
parent = other;
|
|
}
|
|
}
|
|
|
|
return parent;
|
|
}
|
|
|
|
function hasFullWidth(node, nodes, overlapThreshold) {
|
|
for (const other of nodes) {
|
|
if (other !== node && other.index > node.index && hasOverlap(node.start, addTime(node.start, overlapThreshold), other.start, other.end)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function getGroups(visuals, dayStart) {
|
|
const groups = [];
|
|
|
|
for (const visual of visuals) {
|
|
const [start, end] = getNormalizedRange(visual.event, dayStart);
|
|
let added = false;
|
|
|
|
for (const group of groups) {
|
|
if (hasOverlap(start, end, group.start, group.end)) {
|
|
group.visuals.push(visual);
|
|
group.end = Math.max(group.end, end);
|
|
added = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!added) {
|
|
groups.push({
|
|
start,
|
|
end,
|
|
visuals: [visual]
|
|
});
|
|
}
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
function getNode(visual, dayStart) {
|
|
const [start, end] = getNormalizedRange(visual.event, dayStart);
|
|
return {
|
|
parent: null,
|
|
sibling: true,
|
|
index: 0,
|
|
visual,
|
|
start,
|
|
end,
|
|
children: []
|
|
};
|
|
}
|
|
|
|
function getMaxChildIndex(node) {
|
|
let max = node.index;
|
|
|
|
for (const child of node.children) {
|
|
const childMax = getMaxChildIndex(child);
|
|
|
|
if (childMax > max) {
|
|
max = childMax;
|
|
}
|
|
}
|
|
|
|
return max;
|
|
}
|
|
|
|
function addTime(identifier, minutes) {
|
|
const removeMinutes = identifier % 100;
|
|
const totalMinutes = removeMinutes + minutes;
|
|
const addHours = Math.floor(totalMinutes / 60);
|
|
const addMinutes = totalMinutes % 60;
|
|
return identifier - removeMinutes + addHours * 100 + addMinutes;
|
|
}
|
|
//# sourceMappingURL=stack.js.map
|