aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/TimeLine.vue175
1 files changed, 147 insertions, 28 deletions
diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue
index a175681..91743f6 100644
--- a/src/components/TimeLine.vue
+++ b/src/components/TimeLine.vue
@@ -1,5 +1,5 @@
<template>
-<div class="touch-none relative"
+<div class="touch-none relative overflow-hidden"
@pointerdown.prevent="onPointerDown" @pointermove.prevent="onPointerMove" @pointerup.prevent="onPointerUp"
@pointercancel.prevent="onPointerUp" @pointerout.prevent="onPointerUp" @pointerleave.prevent="onPointerUp"
@wheel.exact.prevent="onWheel" @wheel.shift.exact.prevent="onShiftWheel"
@@ -125,7 +125,7 @@ const eventMinorSz = computed(() => props.vert ? eventWidth.value : eventHeight.
const SPACING = 10;
const sideMainline = computed( // True if unable to fit mainline in middle with events on both sides
() => availBreadth.value < MAINLINE_WIDTH + (eventMinorSz.value + SPACING * 2) * 2);
-const mainlineOffset = computed(() => { // Distance from side of display area
+const mainlineOffset = computed(() => { // Distance mainline-area line to side of display area
if (!sideMainline.value){
return availBreadth.value / 2 - MAINLINE_WIDTH /2 + LARGE_TICK_LEN;
} else {
@@ -312,43 +312,162 @@ const idToPos = computed(() => {
return new Map();
}
let map: Map<number, [number, number, number, number]> = new Map(); // Maps visible event IDs to x/y/w/h
- // Do basic grid-like layout
- let minorOffset = SPACING, majorOffset = SPACING; // Holds x and y for event element (or y and x if !props.vert)
- let full = false;
- for (let event of idToEvent.value.values()){
- // Check if at end of column (or row if !props.vert)
- if (majorOffset + eventMajorSz.value + SPACING > availLen.value){
- majorOffset = SPACING;
- minorOffset += eventMinorSz.value + SPACING;
- // If finished last row
- if (minorOffset + eventMinorSz.value + SPACING > availBreadth.value){
- full = true;
+ // Determine columns to place event elements in (or rows if !props.vert)
+ let cols: [number, number][][] = []; // For each column, for each element, stores an ID and pixel offset
+ let colOffsets: number[] = []; // Stores the pixel offset of each column
+ let afterMainlineIdx: number | null = null; // Index of first column after the mainline, if there is one
+ if (!sideMainline.value){
+ // Get columns before mainline area
+ let columnOffset = availBreadth.value / 2 - MAINLINE_WIDTH / 2 - SPACING - eventMinorSz.value;
+ while (columnOffset >= SPACING){
+ cols.push([]);
+ colOffsets.push(columnOffset);
+ columnOffset -= eventMinorSz.value + SPACING;
+ }
+ colOffsets.reverse();
+ afterMainlineIdx = cols.length;
+ // Get columns after mainline area
+ columnOffset = availBreadth.value / 2 + MAINLINE_WIDTH / 2 + SPACING;
+ while (columnOffset + eventMinorSz.value + SPACING < availBreadth.value){
+ cols.push([]);
+ colOffsets.push(columnOffset);
+ columnOffset += eventMinorSz.value + SPACING;
+ }
+ } else {
+ // Get columns before mainline area
+ let columnOffset = mainlineOffset.value - SPACING - eventMinorSz.value - SPACING;
+ while (columnOffset >= SPACING){
+ cols.push([]);
+ colOffsets.push(columnOffset);
+ columnOffset -= eventMinorSz.value + SPACING;
+ }
+ colOffsets.reverse();
+ }
+ if (cols.length == 0){
+ console.log('WARNING: No space for events');
+ return map;
+ }
+ // Place events in columns, trying to minimise distance to points on mainline
+ // Note: Placing popular events first so the layout is more stable between event requests
+ let orderedEvents = [...idToEvent.value.values()];
+ orderedEvents.sort((x, y) => y.pop - x.pop);
+ let numUnits = getNumVisibleUnits();
+ for (let event of orderedEvents){
+ // Get preferred pixel offset in column
+ let unitOffset = getUnitDiff(event.start, startDate.value, scale.value) + startOffset.value;
+ let targetOffset = unitOffset / numUnits * availLen.value - eventMajorSz.value / 2;
+ // Find potential positions
+ let positions: [number, number, number][] = [];
+ // For each position, holds a column index, a within-column index to insert at, and an offset value
+ let colIdx = afterMainlineIdx == null ? cols.length - 1 : afterMainlineIdx - 1;
+ // Column index, used to iterate from columns closest to mainline outward
+ columnLoop:
+ while (colIdx >= 0){ // For each column
+ let bestOffset: number | null = null; // Best offset found so far
+ let bestIdx: number | null = null; // Index of insertion for bestOffset
+ // Check for empty column
+ if (cols[colIdx].length == 0){
+ let offset = Math.min(Math.max(SPACING, targetOffset), availLen.value - eventMajorSz.value - SPACING);
+ positions.push([colIdx, 0, offset]);
break;
}
+ // Check placement before first event in column
+ let offset = cols[colIdx][0][1] - eventMajorSz.value - SPACING;
+ if (offset >= SPACING){
+ if (offset >= targetOffset){
+ positions.push([colIdx, 0, Math.max(SPACING, targetOffset)]);
+ break;
+ } else {
+ bestOffset = offset;
+ bestIdx = 0;
+ }
+ }
+ // Check placement after each event element in column
+ for (let elIdx = 0; elIdx < cols[colIdx].length; elIdx++){
+ offset = cols[colIdx][elIdx][1] + eventMajorSz.value + SPACING;
+ if (elIdx == cols[colIdx].length - 1){ // If last element in column
+ if (offset < availLen.value - eventMajorSz.value - SPACING){
+ // Check for better offset
+ if (bestOffset == null
+ || Math.abs(targetOffset - offset) < Math.abs(targetOffset - bestOffset)){
+ if (offset <= targetOffset){
+ offset = Math.min(targetOffset, availLen.value - eventMajorSz.value - SPACING);
+ positions.push([colIdx, elIdx + 1, offset]);
+ break columnLoop;
+ } else {
+ bestOffset = offset;
+ bestIdx = elIdx + 1;
+ }
+ }
+ }
+ } else { // If not last event in column
+ // Check for space between this and next element
+ let nextOffset = cols[colIdx][elIdx + 1][1];
+ if (nextOffset - offset < eventMajorSz.value + SPACING){
+ continue;
+ }
+ // Check for better offset
+ if (bestOffset == null || Math.abs(targetOffset - offset) < Math.abs(targetOffset - bestOffset)){
+ if (offset <= targetOffset && targetOffset <= nextOffset - eventMajorSz.value - SPACING){
+ positions.push([colIdx, elIdx + 1, targetOffset]);
+ break columnLoop;
+ } else {
+ if (offset > targetOffset){
+ bestOffset = offset;
+ } else {
+ bestOffset = nextOffset - eventMajorSz.value - SPACING;
+ }
+ bestIdx = elIdx + 1;
+ }
+ } else {
+ break;
+ }
+ }
+ }
+ // Add potential position
+ if (bestOffset != null){
+ positions.push([colIdx, bestIdx!, bestOffset]);
+ }
+ // Update colIdx
+ if (afterMainlineIdx == null){
+ colIdx -= 1;
+ } else {
+ if (colIdx < afterMainlineIdx){ // Swap to 'right' of mainline
+ colIdx = afterMainlineIdx + (afterMainlineIdx - 1 - colIdx);
+ } else { // Swap to 'left' of mainline, and move 'outward'
+ colIdx = afterMainlineIdx - 1 - (colIdx - afterMainlineIdx) - 1;
+ }
+ }
}
- // Avoid collision with timeline
- if (!sideMainline.value){
- if (minorOffset <= availBreadth.value / 2 + MAINLINE_WIDTH / 2 + SPACING &&
- minorOffset + eventMinorSz.value >= availBreadth.value / 2 - MAINLINE_WIDTH / 2 - SPACING){
- minorOffset = availBreadth.value / 2 + MAINLINE_WIDTH / 2 + SPACING;
+ // Choose position with minimal distance
+ if (positions.length > 0){
+ let bestPos = positions[0]!;
+ for (let i = 1; i < positions.length; i++){
+ if (Math.abs(targetOffset - positions[i][2]) < Math.abs(targetOffset - bestPos[2])){
+ bestPos = positions[i];
+ }
}
- } else if (minorOffset + eventMinorSz.value + SPACING > mainlineOffset.value){
- break;
+ cols[bestPos[0]].splice(bestPos[1], 0, [event.id, bestPos[2]]);
}
- // Add coords
- if (props.vert){
- map.set(event.id, [minorOffset, majorOffset, eventWidth.value, eventHeight.value]);
- } else {
- map.set(event.id, [majorOffset, minorOffset, eventWidth.value, eventHeight.value]);
+ }
+ // Add events to map
+ for (let colIdx = 0; colIdx < cols.length; colIdx++){
+ let minorOffset = colOffsets[colIdx];
+ for (let [eventId, majorOffset] of cols[colIdx]){
+ if (props.vert){
+ map.set(eventId, [minorOffset, majorOffset, eventWidth.value, eventHeight.value]);
+ } else {
+ map.set(eventId, [majorOffset, minorOffset, eventWidth.value, eventHeight.value]);
+ }
}
- // Update to next position
- majorOffset += eventMajorSz.value + SPACING;
}
// If more events could be displayed, notify parent
+ let colFillThreshold = (availLen.value - SPACING) / (eventMajorSz.value + SPACING) * 2/3;
+ let full = cols.every(col => col.length >= colFillThreshold);
if (!full){
emit('event-req', startDate.value, endDate.value);
} else { // Send displayed event IDs to parent
- emit('event-display', [...idToEvent.value.keys()], ID);
+ emit('event-display', [...map.keys()], ID);
}
//
return map;