diff options
| author | Terry Truong <terry06890@gmail.com> | 2022-12-29 16:17:39 +1100 |
|---|---|---|
| committer | Terry Truong <terry06890@gmail.com> | 2022-12-29 16:20:26 +1100 |
| commit | 20d69469a4c80a196de23625d0420487b0ed04a6 (patch) | |
| tree | e3a7aad153e72322c0810c66bda3b70daa8d815a /src | |
| parent | 4ad7206443660587a15a7b47384b927188155da8 (diff) | |
Show event-count data on timeline
Backend:
Send event-count data to client in EventResponse instance
Fix certain usages of gregorian calendar instead of julian
Move HistDate, SCALES, etc, into cal.py
Frontend:
Make App update a unitCountMaps object using event-count data from server
Make TimeLine show visual indication of unit counts
Add showEventCounts option to store
Update unit tests
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.vue | 41 | ||||
| -rw-r--r-- | src/components/TimeLine.vue | 85 | ||||
| -rw-r--r-- | src/lib.ts | 21 | ||||
| -rw-r--r-- | src/store.ts | 1 |
4 files changed, 121 insertions, 27 deletions
diff --git a/src/App.vue b/src/App.vue index feba10e..ddc434f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -19,7 +19,8 @@ <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" :closeable="timelines.length > 1" :eventTree="eventTree" + :vert="vert" :initialState="state" :closeable="timelines.length > 1" + :eventTree="eventTree" :unitCountMaps="unitCountMaps" class="grow basis-full min-h-0 outline outline-1" @remove="onTimelineRemove(idx)" @state-chg="onTimelineChg($event, idx)" @event-display="onEventDisplay"/> <base-line :vert="vert" :timelines="timelines" class='m-1 sm:m-2'/> @@ -38,8 +39,8 @@ import PlusIcon from './components/icon/PlusIcon.vue'; import SettingsIcon from './components/icon/SettingsIcon.vue'; import HelpIcon from './components/icon/HelpIcon.vue'; // Other -import {timeout, HistDate, HistEvent, queryServer, HistEventJson, jsonToHistEvent, - SCALES, TimelineState, cmpHistEvent, DateRangeTree} from './lib'; +import {timeout, HistDate, HistEvent, queryServer, EventResponseJson, jsonToHistEvent, + SCALES, TimelineState, cmpHistEvent, dateToUnit, DateRangeTree} from './lib'; import {useStore} from './store'; import {RBTree, rbtree_shallow_copy} from './rbtree'; @@ -101,6 +102,7 @@ function onTimelineRemove(idx: number){ // For storing and looking up events const eventTree: ShallowRef<RBTree<HistEvent>> = shallowRef(new RBTree(cmpHistEvent)); let idToEvent: Map<number, HistEvent> = new Map(); +const unitCountMaps: Ref<Map<number, number>[]> = ref(SCALES.map(() => new Map())); // For each scale, maps units to event counts // For keeping event data under a memory limit const EXCESS_EVENTS_THRESHOLD = 10000; let displayedEvents: Map<number, number[]> = new Map(); // Maps TimeLine IDs to IDs of displayed events @@ -117,8 +119,25 @@ function reduceEvents(){ for (let [, event] of eventsToKeep){ newTree.insert(event); } + // Create new unit-count maps + let newMaps: Map<number, number>[] = SCALES.map(() => new Map()); + for (let timeline of timelines.value){ + if (timeline.scaleIdx == null){ + continue; + } + // Look for units to keep + let scaleIdx: number = timeline.scaleIdx; + let startUnit = dateToUnit(timeline.startDate, SCALES[scaleIdx]); + let endUnit = dateToUnit(timeline.endDate, SCALES[scaleIdx]); + for (let [unit, count] of unitCountMaps.value[scaleIdx]){ + if (unit >= startUnit && unit <= endUnit){ + newMaps[scaleIdx].set(unit, count); + } + } + } // Replace old data eventTree.value = newTree; + unitCountMaps.value = newMaps; idToEvent = eventsToKeep; } // For getting events from server @@ -144,15 +163,15 @@ async function onEventDisplay( scale: String(SCALES[scaleIdx]), limit: String(EVENT_REQ_LIMIT), }); - let responseObj: HistEventJson[] = await queryServer(urlParams); + let responseObj: EventResponseJson = await queryServer(urlParams); if (responseObj == null){ pendingReq = false; return; } queriedRanges[scaleIdx].add([firstDate, lastDate]); - // Add to map + // Collect events let added = false; - for (let eventObj of responseObj){ + for (let eventObj of responseObj.events){ let event = jsonToHistEvent(eventObj); let success = eventTree.value.insert(event); if (success){ @@ -160,6 +179,16 @@ async function onEventDisplay( idToEvent.set(event.id, event); } } + // Collect unit counts + const unitCounts = responseObj.unitCounts; + for (let [unitStr, count] of Object.entries(unitCounts)){ + let unit = parseInt(unitStr) + if (isNaN(unit)){ + console.log('ERROR: Invalid non-integer unit value in server response'); + break; + } + unitCountMaps.value[scaleIdx].set(unit, count) + } // Notify components if new events were added if (added){ eventTree.value = rbtree_shallow_copy(eventTree.value); // Note: triggerRef(eventTree) does not work here diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index b1607ef..1050c62 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -4,7 +4,11 @@ @pointercancel.prevent="onPointerUp" @pointerout.prevent="onPointerUp" @pointerleave.prevent="onPointerUp" @wheel.exact.prevent="onWheel" @wheel.shift.exact.prevent="onShiftWheel" ref="rootRef"> - <svg :viewBox="`0 0 ${width} ${height}`"> + <template v-if="store.showEventCounts"> + <div v-for="[tickIdx, count] in tickToCount.entries()" :key="ticks[tickIdx].date.toInt()" + :style="countDivStyles(tickIdx, count)" class="absolute bg-yellow-300/30 animate-fadein"></div> + </template> + <svg :viewBox="`0 0 ${width} ${height}`" class="relative z-10"> <defs> <linearGradient id="eventLineGradient"> <stop offset="5%" stop-color="#a3691e"/> @@ -42,7 +46,7 @@ <!-- Note: Can't use :x2="1" with scaling in :style="", as it makes dashed-lines non-uniform --> </svg> <!-- Events --> - <div v-for="id in idToPos.keys()" :key="id" class="absolute animate-fadein" :style="eventStyles(id)"> + <div v-for="id in idToPos.keys()" :key="id" class="absolute animate-fadein z-20" :style="eventStyles(id)"> <!-- Image --> <div class="rounded-full border border-yellow-500" :style="eventImgStyles(id)"></div> <!-- Label --> @@ -51,7 +55,7 @@ </div> </div> <!-- Buttons --> - <icon-button v-if="closeable" :size="30" class="absolute top-2 right-2" + <icon-button v-if="closeable" :size="30" class="absolute top-2 right-2 z-20" :style="{color: store.color.text, backgroundColor: store.color.altDark2}" @click="emit('remove')" title="Remove timeline"> <close-icon/> @@ -67,7 +71,8 @@ import IconButton from './IconButton.vue'; import CloseIcon from './icon/CloseIcon.vue'; // Other import {WRITING_MODE_HORZ, MIN_DATE, MAX_DATE, MONTH_SCALE, DAY_SCALE, SCALES, MIN_CAL_YEAR, - getDaysInMonth, HistDate, CalDate, stepDate, getScaleRatio, getNumSubUnits, getUnitDiff, getEventPrecision, + getDaysInMonth, HistDate, CalDate, stepDate, getScaleRatio, getNumSubUnits, getUnitDiff, + getEventPrecision, dateToUnit, moduloPositive, TimelineState, HistEvent, getImagePath} from '../lib'; import {useStore} from '../store'; import {RBTree} from '../rbtree'; @@ -84,6 +89,7 @@ const props = defineProps({ closeable: {type: Boolean, default: true}, initialState: {type: Object as PropType<TimelineState>, required: true}, eventTree: {type: Object as PropType<RBTree<HistEvent>>, required: true}, + unitCountMaps: {type: Object as PropType<Map<number, number>[]>, required: true}, }); const emit = defineEmits(['remove', 'state-chg', 'event-req', 'event-display']); @@ -349,30 +355,24 @@ const ticks = computed((): Tick[] => { ticks = [...ticksBefore, ...ticks, ...ticksAfter]; return ticks; }); -const firstDate = computed((): HistDate => { // Date of first visible tick - if (ticks.value.length == 0){ - return startDate.value; - } - return ticks.value.find((tick: Tick) => tick.offset > 0)!.date; -}); -const firstOffset = computed((): number => { // Offset of first visible tick - if (ticks.value.length == 0){ - return startOffset.value; - } - return ticks.value.find((tick: Tick) => tick.offset > 0)!.offset; +const firstIdx = computed((): number => { // Index of first visible tick + return ticks.value.findIndex((tick: Tick) => tick.offset >= 0); }); -const lastDate = computed((): HistDate => { +const firstDate = computed(() => // Date of first visible tick + firstIdx.value < 0 ? startDate.value : ticks.value[firstIdx.value]!.date); +const firstOffset = computed(() => // Offset of first visible tick + firstIdx.value < 0 ? startOffset.value : ticks.value[firstIdx.value]!.offset); +const lastIdx = computed((): number => { let numUnits = getNumDisplayUnits(); - let date = endDate.value; for (let i = ticks.value.length - 1; i >= 0; i--){ let tick = ticks.value[i]; - if (tick.offset < numUnits){ - date = tick.date; - break; + if (tick.offset <= numUnits){ + return i; } } - return date; + return -1; }); +const lastDate = computed(() => lastIdx.value < 0 ? endDate.value : ticks.value[lastIdx.value]!.date); // For displayed events function dateToOffset(date: HistDate){ @@ -614,6 +614,31 @@ watchEffect(() => { // Used instead of computed() in order to access old values eventLines.value = newEventLines; }); +// For event-count indicators +const tickToCount = computed((): Map<number, number> => { + let tickToCount: Map<number, number> = new Map(); // Maps tick index to event count + let unitToTickIdx: [number, number][] = []; // Holds tick units with their tick indexes in tickToCount + for (let tickIdx = firstIdx.value; tickIdx < lastIdx.value; tickIdx++){ + tickToCount.set(tickIdx, 0); + let unit = dateToUnit(ticks.value[tickIdx].date, minorScale.value); + unitToTickIdx.push([unit, tickIdx]); + } + // Accumulate counts for ticks + const firstUnit = dateToUnit(firstDate.value, minorScale.value); + const lastUnit = dateToUnit(lastDate.value, minorScale.value); + for (let [unit, count] of props.unitCountMaps[minorScaleIdx.value].entries()){ + if (unit >= firstUnit && unit < lastUnit){ + let i = 0; + while (i + 1 < unitToTickIdx.length && unitToTickIdx[i + 1][0] <= unit){ + i += 1; + } + const tickIdx = unitToTickIdx[i][1]; + tickToCount.set(tickIdx, tickToCount.get(tickIdx)! + count); + } + } + return tickToCount; +}); + // For panning/zooming function panTimeline(scrollRatio: number){ let numUnits = getNumDisplayUnits(); @@ -1088,4 +1113,22 @@ function eventLineStyles(eventId: number){ transitionTimingFunction: 'ease-out', }; } +function countDivStyles(tickIdx: number, count: number): Record<string,string> { + let tick = ticks.value[tickIdx]; + let numMajorUnits = getNumDisplayUnits(); + let pxOffset = tick.offset / numMajorUnits * availLen.value; + let nextPxOffset = ticks.value[tickIdx + 1].offset / numMajorUnits * availLen.value; + let len = nextPxOffset - pxOffset; + let countLevel = Math.min(Math.ceil(Math.log10(count+1)), 4); + let breadth = countLevel * 4 + 4; + return { + top: props.vert ? pxOffset + 'px' : (mainlineOffset.value - breadth / 2) + 'px', + left: props.vert ? (mainlineOffset.value - breadth / 2) + 'px' : pxOffset + 'px', + width: props.vert ? breadth + 'px' : len + 'px', + height: props.vert ? len + 'px' : breadth + 'px', + transitionProperty: 'top, left, width, height', + transitionDuration: store.transitionDuration + 'ms', + transitionTimingFunction: 'linear', + } +} </script> @@ -305,6 +305,10 @@ export type HistEventJson = { imgId: number, pop: number, } +export type EventResponseJson = { + events: HistEventJson[], + unitCounts: {[x: number]: number}, +} export function jsonToHistDate(json: HistDateJson){ if (json.gcal == null){ return new YearDate(json.year); @@ -493,6 +497,23 @@ export function getEventPrecision(event: HistEvent): number { } return Number.POSITIVE_INFINITY; } +export function dateToUnit(date: HistDate, scale: number): number { + if (scale >= 1){ + return Math.floor(date.year / scale); + } else if (scale == MONTH_SCALE){ + if (!date.gcal){ + return julianToJdn(date.year, date.month, 1); + } else { + return gregorianToJdn(date.year, date.month, 1); + } + } else { // scale == DAY_SCALE + if (!date.gcal){ + return julianToJdn(date.year, date.month, date.day); + } else { + return gregorianToJdn(date.year, date.month, date.day); + } + } +} // For sending timeline-bound data to BaseLine export class TimelineState { diff --git a/src/store.ts b/src/store.ts index 5f30bc6..d3ece49 100644 --- a/src/store.ts +++ b/src/store.ts @@ -40,6 +40,7 @@ export const useStore = defineStore('store', { initialStartDate: new CalDate(1900, 1, 1), initialEndDate: new CalDate(2000, 1, 1), color, + showEventCounts: true, transitionDuration: 300, }; }, |
