aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.vue41
-rw-r--r--src/components/TimeLine.vue85
-rw-r--r--src/lib.ts21
-rw-r--r--src/store.ts1
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>
diff --git a/src/lib.ts b/src/lib.ts
index 353fa57..7797469 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -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,
};
},