diff options
| -rw-r--r-- | src/App.vue | 39 | ||||
| -rw-r--r-- | src/components/TimeLine.vue | 66 | ||||
| -rw-r--r-- | src/lib.ts | 14 |
3 files changed, 100 insertions, 19 deletions
diff --git a/src/App.vue b/src/App.vue index 1cd00f5..24da714 100644 --- a/src/App.vue +++ b/src/App.vue @@ -19,8 +19,9 @@ <div class="grow min-h-0 flex" :class="{'flex-col': !vert}" :style="{backgroundColor: store.color.bg}" ref="contentAreaRef"> <time-line v-for="(state, idx) in timelines" :key="state.id" - :vert="vert" :initialState="state" class="grow basis-full min-h-0 outline outline-1" - @remove="onTimelineRemove(idx)" @state-chg="onTimelineChg($event, idx)"/> + :vert="vert" :initialState="state" :eventMap="eventMap" + class="grow basis-full min-h-0 outline outline-1" + @remove="onTimelineRemove(idx)" @state-chg="onTimelineChg($event, idx)" @event-req="onEventReq"/> <base-line :vert="vert" :timelines="timelines"/> </div> </div> @@ -37,7 +38,7 @@ import PlusIcon from './components/icon/PlusIcon.vue'; import SettingsIcon from './components/icon/SettingsIcon.vue'; import HelpIcon from './components/icon/HelpIcon.vue'; // Other -import {HistDate, TimelineState} from './lib'; +import {HistDate, TimelineState, HistEvent, getUnitDiff, MONTH_SCALE, DAY_SCALE, stepDate} from './lib'; import {useStore} from './store'; // Refs @@ -97,6 +98,38 @@ function onTimelineRemove(idx: number){ timelines.value.splice(idx, 1); } +// Event data +const eventMap: Ref<Map<number, HistEvent>> = ref(new Map()); // Maps event IDs to HistEvents +let nextEventId = 0; // For generating placeholder events +function onEventReq(startDate: HistDate, endDate: HistDate){ + // Get number of existing events in range + let numExisting = 0; + for (let event of eventMap.value.values()){ + if (!event.start.isEarlier(startDate) && !endDate.isEarlier(event.start)){ + numExisting += 1; + } + } + // Possibly add new events + let tempScale = 1; + let numUnits = getUnitDiff(startDate, endDate, tempScale); + if (numUnits < 2){ + tempScale = MONTH_SCALE; + numUnits = getUnitDiff(startDate, endDate, tempScale); + if (numUnits < 2){ + tempScale = DAY_SCALE; + numUnits = getUnitDiff(startDate, endDate, tempScale); + } + } + for (let i = 0; i < 3 - numExisting; i++){ + let start = startDate.clone(); + let steps = Math.floor(Math.random() * (numUnits + 1)); + stepDate(start, tempScale, {count: steps, inplace: true}); + let event = {id: nextEventId, title: `Event ${nextEventId}`, start, startUpper: null, end: null, endUpper: null}; + eventMap.value.set(event.id, event); + nextEventId += 1; + } +} + // For resize handling let lastResizeHdlrTime = 0; // Used to throttle resize handling let afterResizeHdlr = 0; // Used to trigger handler after ending a run of resize events diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index add7ce8..d71176f 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -27,6 +27,12 @@ {{date}} </text> </svg> + <!-- Events --> + <div v-for="id in eventIdToPos.keys()" :key="id" + class="absolute bg-black text-white border border-white rounded animate-fadein" + :style="eventStyles(id)"> + {{eventMap.get(id)!.id}} + </div> <!-- Buttons --> <icon-button :size="30" class="absolute top-2 right-2" :style="{color: store.color.text, backgroundColor: store.color.altDark2}" @@ -37,18 +43,19 @@ </template> <script setup lang="ts"> -import {ref, onMounted, computed, watch, PropType} from 'vue'; +import {ref, onMounted, computed, watch, PropType, Ref, nextTick} from 'vue'; // Components import IconButton from './IconButton.vue'; // Icons import MinusIcon from './icon/MinusIcon.vue'; // Other import {WRITING_MODE_HORZ, MIN_DATE, MAX_DATE, MONTH_SCALE, DAY_SCALE, SCALES, MIN_CAL_DATE, - HistDate, stepDate, inDateScale, getScaleRatio, getDaysInMonth, moduloPositive, TimelineState} from '../lib'; + HistDate, stepDate, inDateScale, getScaleRatio, getUnitDiff, getDaysInMonth, moduloPositive, TimelineState, + HistEvent} from '../lib'; import {useStore} from '../store'; // Refs -const rootRef = ref(null as HTMLElement | null); +const rootRef: Ref<HTMLElement | null> = ref(null); // Global store const store = useStore(); @@ -57,8 +64,9 @@ const store = useStore(); const props = defineProps({ vert: {type: Boolean, required: true}, initialState: {type: Object as PropType<TimelineState>, required: true}, + eventMap: {type: Object as PropType<Map<number, HistEvent>>, required: true}, }); -const emit = defineEmits(['remove', 'state-chg']); +const emit = defineEmits(['remove', 'state-chg', 'event-req']); // For size tracking const width = ref(0); @@ -91,7 +99,7 @@ const resizeObserver = new ResizeObserver((entries) => { onMounted(() => resizeObserver.observe(rootRef.value as HTMLElement)); // Timeline data -const ID = props.initialState.id; +const ID = props.initialState.id as number; const startDate = ref(props.initialState.startDate); // Earliest date to display const endDate = ref(props.initialState.endDate); const INITIAL_EXTRA_OFFSET = 0.5; @@ -164,15 +172,8 @@ type Ticks = { endIdx: number, // Index of last visible tick }; function getNumVisibleUnits(): number { - let numUnits: number; - if (scale.value == DAY_SCALE){ - numUnits = startDate.value.getDayDiff(endDate.value); - } else if (scale.value == MONTH_SCALE){ - numUnits = startDate.value.getMonthDiff(endDate.value); - } else { - numUnits = startDate.value.getYearDiff(endDate.value) / scale.value; - } - return numUnits + startOffset.value + endOffset.value; + let unitDiff = getUnitDiff(startDate.value, endDate.value, scale.value); + return unitDiff + startOffset.value + endOffset.value; } const ticks = computed((): Ticks => { if (!mounted.value){ @@ -251,6 +252,31 @@ const ticks = computed((): Ticks => { return {dates, startIdx, endIdx}; }); +// For displayed events +const eventIdToPos = computed(() => { // Maps visible event IDs to x-pos, y-pos, width, and height + let idToPos: Map<number, [number, number, number, number]> = new Map(); + // Find events to display, and do basic layouting + let numUnits = getNumVisibleUnits(); + let minorAxisStep = 0; + for (let event of props.eventMap.values()){ + if (event.start.isEarlier(startDate.value) || endDate.value.isEarlier(event.start)){ + continue; + } + let unitOffset = getUnitDiff(event.start, startDate.value, scale.value) + startOffset.value; + let posFrac = unitOffset / numUnits; + let posX = props.vert ? minorAxisStep : availLen.value * posFrac; + let posY = props.vert ? availLen.value * posFrac : minorAxisStep; + idToPos.set(event.id, [posX, posY, 50, 50]); + minorAxisStep += 5; + } + // If more events could be displayed, notify parent + if (idToPos.size < 3){ + nextTick(() => emit('event-req', startDate.value, endDate.value)); + } + // + return idToPos; +}); + // For panning/zooming function panTimeline(scrollRatio: number){ let numUnits = getNumVisibleUnits(); @@ -698,4 +724,16 @@ function tickLabelStyles(idx: number){ opacity: (offset >= labelSz && offset <= availLen.value - labelSz) ? 1 : 0, } } +function eventStyles(eventId: number){ + const [x, y, w, h] = eventIdToPos.value.get(eventId)!; + return { + left: x + 'px', + top: y + 'px', + width: w + 'px', + height: h + 'px', + transitionProperty: skipTransition.value ? 'none' : 'all', + transitionDuration, + transitionTimingFunction, + }; +} </script> @@ -266,6 +266,15 @@ export function getScaleRatio(scale: number, scale2: number){ } return scale2 / scale; } +export function getUnitDiff(date: HistDate, date2: HistDate, scale: number): number { + if (scale == DAY_SCALE){ + return date.getDayDiff(date2); + } else if (scale == MONTH_SCALE){ + return date.getMonthDiff(date2); + } else { + return date.getYearDiff(date2) / scale; + } +} // For sending timeline-bound data to BaseLine export class TimelineState { @@ -284,12 +293,13 @@ export class TimelineState { this.endOffset = endOffset; this.scaleIdx = scaleIdx; } -}; +} export type HistEvent = { + id: number, title: string, start: HistDate, startUpper: HistDate | null, - end: HistDate, + end: HistDate | null, endUpper: HistDate | null, }; |
