aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.vue64
-rw-r--r--src/components/BaseLine.vue4
-rw-r--r--src/components/SearchModal.vue8
-rw-r--r--src/components/SettingsModal.vue2
-rw-r--r--src/components/TimeLine.vue71
-rw-r--r--src/lib.ts136
-rw-r--r--src/util.ts2
7 files changed, 146 insertions, 141 deletions
diff --git a/src/App.vue b/src/App.vue
index 99ba03c..6369751 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -13,7 +13,7 @@
<settings-icon/>
</icon-button>
<icon-button :size="45" :disabled="maxTimelines" :style="buttonStyles"
- @click="onTimelineAdd" title="Add a timeline">
+ @click="addTimeline" title="Add a timeline">
<plus-icon/>
</icon-button>
<icon-button :size="45" :style="buttonStyles" @click="searchOpen = true" title="Search">
@@ -96,6 +96,7 @@ function updateAreaDims(){
contentWidth.value = contentAreaEl.offsetWidth;
contentHeight.value = contentAreaEl.offsetHeight;
}
+
onMounted(updateAreaDims);
// ========== Timeline data ==========
@@ -104,9 +105,18 @@ const timelines: Ref<TimelineState[]> = ref([]);
const currentTimelineIdx = ref(0);
let nextTimelineId = 1;
+const MIN_TIMELINE_BREADTH = store.mainlineBreadth + store.spacing * 2 + store.eventImgSz + store.eventLabelHeight;
+const maxTimelines = computed(() => {
+ return vert.value && contentWidth.value / (timelines.value.length + 1) < MIN_TIMELINE_BREADTH
+ || !vert.value && contentHeight.value / (timelines.value.length + 1) < MIN_TIMELINE_BREADTH
+});
+
function addTimeline(){
if (timelines.value.length == 0){
timelines.value.push(new TimelineState(nextTimelineId, store.initialStartDate, store.initialEndDate));
+ } else if (maxTimelines.value){
+ console.log('INFO: Ignored addition of timeline upon reaching max');
+ return;
} else {
let state = timelines.value[currentTimelineIdx.value];
timelines.value.splice(currentTimelineIdx.value, 0, new TimelineState(
@@ -117,35 +127,12 @@ function addTimeline(){
currentTimelineIdx.value += 1;
nextTimelineId += 1;
}
-onMounted(addTimeline);
-
-function onTimelineChg(state: TimelineState, idx: number){
- timelines.value[idx] = state;
- currentTimelineIdx.value = idx;
-}
-
-// ========== For timeline add/remove ==========
-
-const MIN_TIMELINE_BREADTH = store.mainlineBreadth + store.spacing * 2 + store.eventImgSz + store.eventLabelHeight;
-const maxTimelines = computed(() => {
- return vert.value && contentWidth.value / (timelines.value.length + 1) < MIN_TIMELINE_BREADTH
- || !vert.value && contentHeight.value / (timelines.value.length + 1) < MIN_TIMELINE_BREADTH
-});
-
-function onTimelineAdd(){
- if (maxTimelines.value){
- console.log('Ignored addition of timeline upon reaching max');
- return;
- }
- addTimeline();
-}
function onTimelineClose(idx: number){
if (timelines.value.length == 1){
- console.log('Ignored removal of last timeline')
+ console.log('INFO: Ignored removal of last timeline')
return;
}
-
timelines.value.splice(idx, 1);
searchTargets.value.splice(idx, 1);
resetFlags.value.splice(idx, 1);
@@ -154,6 +141,13 @@ function onTimelineClose(idx: number){
}
}
+function onTimelineChg(state: TimelineState, idx: number){
+ timelines.value[idx] = state;
+ currentTimelineIdx.value = idx;
+}
+
+onMounted(addTimeline);
+
// ========== For event data ==========
const eventTree: ShallowRef<RBTree<HistEvent>> = shallowRef(new RBTree(cmpHistEvent));
@@ -211,15 +205,16 @@ function reduceEvents(){
// ========== For getting events from server ==========
+const MAX_EVENTS_PER_UNIT = 4; // (Should equal MAX_DISPLAYED_PER_UNIT in backend/hist_data/gen_disp_data.py)
const eventReqLimit = computed(() => {
- // As a rough heuristic, is the number of events that could fit along the major axis,
+ // As a rough heuristic, computes the number of events that could fit along the major axis,
// multiplied by a rough number of time points per event-occupied region,
// multiplied by the max number of events per time point (four).
- return Math.ceil(Math.max(contentWidth.value, contentHeight.value) / store.eventImgSz * 32);
+ return Math.ceil(Math.max(contentWidth.value, contentHeight.value) / store.eventImgSz * 8 * MAX_EVENTS_PER_UNIT);
});
-const MAX_EVENTS_PER_UNIT = 4; // Should equal MAX_DISPLAYED_PER_UNIT in backend gen_disp_data.py
-let queriedRanges: DateRangeTree[] = // For each scale, holds date ranges for which data has already been queried
- SCALES.map(() => new DateRangeTree());
+
+let queriedRanges: DateRangeTree[] = SCALES.map(() => new DateRangeTree());
+ // For each scale, holds date ranges for which data has already been queried
let lastQueriedRange: [HistDate, HistDate] | null = null;
async function handleOnEventDisplay(
@@ -341,6 +336,7 @@ async function handleOnEventDisplay(
queriedRanges.forEach((t: DateRangeTree) => t.clear());
}
}
+
const onEventDisplay = makeThrottled(handleOnEventDisplay, 200);
// ========== For info modal ==========
@@ -377,7 +373,8 @@ function onSearch(event: HistEvent){
const settingsOpen = ref(false);
function onSettingChg(option: string){
- if (option == 'reqImgs' || option.startsWith('ctgs.')){ // Reset event data
+ if (option == 'reqImgs' || option.startsWith('ctgs.')){
+ // Reset event data
eventTree.value = new RBTree(cmpHistEvent); // Will trigger event re-query
unitCountMaps.value = SCALES.map(() => new Map());
idToEvent.clear();
@@ -421,7 +418,7 @@ async function loadFromServer(urlParams: URLSearchParams, delay?: number){
return responseObj;
}
-// For timeline reset
+// For resetting timeline bounds
const resetFlags: Ref<boolean[]> = ref([]);
function onReset(){
let oldFlag = resetFlags.value[currentTimelineIdx.value];
@@ -437,6 +434,7 @@ const modalOpen = computed(() =>
const onResize = makeThrottledSpaced(updateAreaDims, 200);
// Note: If delay is too small, touch-device detection when swapping to/from mobile-mode gets unreliable
+
onMounted(() => window.addEventListener('resize', onResize));
onUnmounted(() => window.removeEventListener('resize', onResize));
@@ -488,7 +486,7 @@ function onKeyDown(evt: KeyboardEvent){
}
}
} else if (evt.key == '+' && !modalOpen.value){
- onTimelineAdd();
+ addTimeline();
} else if (evt.key == 'Delete' && !modalOpen.value){
onTimelineClose(currentTimelineIdx.value);
}
diff --git a/src/components/BaseLine.vue b/src/components/BaseLine.vue
index 91f5b69..53ab6bd 100644
--- a/src/components/BaseLine.vue
+++ b/src/components/BaseLine.vue
@@ -7,7 +7,7 @@
<div v-if="props.vert" class="absolute bottom-0 w-full h-6"
style="background-image: linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,1))"></div>
</div>
- <!-- Timeline spans -->
+ <!-- Timeline 'spans' -->
<TransitionGroup name="fade" v-if="mounted">
<div v-for="(state, idx) in timelines" :key="state.id" class="absolute" :style="spanStyles(idx)"></div>
</TransitionGroup>
@@ -44,6 +44,7 @@ const periods: Ref<Period[]> = ref([
// ========== For skipping transitions on startup ==========
const skipTransition = ref(true);
+
onMounted(() => setTimeout(() => {skipTransition.value = false}, 100));
// ========== For size and mount-status tracking ==========
@@ -68,6 +69,7 @@ const resizeObserver = new ResizeObserver((entries) => {
}
}
});
+
onMounted(() => resizeObserver.observe(rootRef.value as HTMLElement));
// ========== For styles ==========
diff --git a/src/components/SearchModal.vue b/src/components/SearchModal.vue
index ec1b4cd..addb764 100644
--- a/src/components/SearchModal.vue
+++ b/src/components/SearchModal.vue
@@ -84,7 +84,7 @@ const focusedSuggIdx = ref(null as null | number); // Index of a suggestion sele
const lastReqTime = ref(0);
const pendingReqParams = ref(null as null | URLSearchParams); // Holds data for latest request to make
-const pendingReqInput = ref(''); // Holds the user input associated with pendingReqData
+const pendingReqInput = ref(''); // Holds the user input associated with pendingReqParams
const pendingDelayedSuggReq = ref(0); // Set via setTimeout() for making a request despite a previous one still waiting
async function onInput(){
@@ -213,6 +213,9 @@ async function resolveSearch(eventTitle: string){
// ========== More event handling ==========
+// Focus input on mount
+onMounted(() => inputRef.value!.focus())
+
function onClose(evt: Event){
if (evt.target == rootRef.value){
emit('close');
@@ -236,9 +239,6 @@ function onInfoIconClick(eventTitle: string){
emit('info-click', eventTitle);
}
-// Focus input on mount
-onMounted(() => inputRef.value!.focus())
-
// ========== For styles ==========
const styles = computed((): Record<string,string> => {
diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue
index bb1370e..2c4a0df 100644
--- a/src/components/SettingsModal.vue
+++ b/src/components/SettingsModal.vue
@@ -124,11 +124,13 @@ let changedCtg: string | null = null; // Used to defer signalling of a category
function onSettingChg(option: string){
store.save(option);
+
if (option.startsWith('ctgs.')){
changedCtg = option;
} else {
emit('change', option);
}
+
// Make 'Saved' indicator appear/animate
if (!saved.value){
saved.value = true;
diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue
index a040233..6c58ae3 100644
--- a/src/components/TimeLine.vue
+++ b/src/components/TimeLine.vue
@@ -4,7 +4,7 @@
@pointercancel="onPointerUp" @pointerout="onPointerUp" @pointerleave="onPointerUp"
@wheel.exact="onWheel" @wheel.shift.exact="onShiftWheel">
- <!-- Event count indicators -->
+ <!-- Event density indicators -->
<template v-if="store.showEventCounts">
<div v-for="[tickIdx, count] in tickToCount.entries()" :key="ticks[tickIdx].date.toInt()"
:style="countDivStyles(tickIdx, count)" class="absolute animate-fadein"></div>
@@ -30,7 +30,7 @@
<!-- Note: Can't use :x2="1" with scaling in :style="", as it makes dashed-lines non-uniform -->
</template>
- <!-- Main line (unit horizontal line that gets transformed, with extra length to avoid gaps when panning) -->
+ <!-- Main line (horizontal line that gets transformed, with extra length to avoid gaps when panning) -->
<line :stroke="store.color.alt" stroke-width="2px" x1="-1" y1="0" x2="2" y2="0" :style="mainlineStyles"/>
<!-- Tick markers -->
@@ -90,7 +90,7 @@ import CloseIcon from './icon/CloseIcon.vue';
import {WRITING_MODE_HORZ, moduloPositive, animateWithClass, getTextWidth} from '../util';
import {
- getDaysInMonth, MIN_CAL_DATE, MONTH_NAMES, HistDate, HistEvent, getImagePath,
+ getDaysInMonth, MIN_CAL_DATE, MONTH_NAMES, HistDate, HistEvent, getImagePath, dateToYearStr, dateToTickStr,
MIN_DATE, MAX_DATE, MONTH_SCALE, DAY_SCALE, SCALES,
stepDate, getScaleRatio, getNumSubUnits, getUnitDiff, getEventPrecision, dateToUnit, dateToScaleDate,
TimelineState,
@@ -107,14 +107,17 @@ const bgFailRef: Ref<HTMLElement | null> = ref(null);
const store = useStore();
const props = defineProps({
- vert: {type: Boolean, required: true},
+ vert: {type: Boolean, required: true}, // Display orientation
closeable: {type: Boolean, default: true},
+ current: {type: Boolean, required: 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},
- current: {type: Boolean, required: true},
+
searchTarget: {type: Object as PropType<[null | HistEvent, boolean]>, required: true},
- reset: {type: Boolean, required: true},
+ // For triggering a jump to a search result
+ reset: {type: Boolean, required: true}, // For triggering a bounds reset
});
const emit = defineEmits(['close', 'state-chg', 'event-display', 'info-click']);
@@ -147,6 +150,7 @@ const resizeObserver = new ResizeObserver((entries) => {
}
}
});
+
onMounted(() => resizeObserver.observe(rootRef.value as HTMLElement));
// ========== Computed values used for layout ==========
@@ -170,6 +174,9 @@ const mainlineOffset = computed(() => { // Distance from mainline-area line to l
// ========== Timeline data ==========
+// Note: The visible timeline is divided into 'units', representing time periods on a scale (eg: months, decades).
+ // If there is space, units of a smaller scale are displayed (called 'minor units', in contrast to 'major units').
+
const ID = props.initialState.id as number;
const startDate = ref(props.initialState.startDate); // Earliest date in scale to display
@@ -200,7 +207,7 @@ const hasMinorScale = computed(() => { // If true, display subset of ticks of ne
const minorScaleIdx = computed(() => scaleIdx.value + (hasMinorScale.value ? 1 : 0));
const minorScale = computed(() => SCALES[minorScaleIdx.value]);
-// Start/end date/offset initialisation
+// Initialise start/end date/offset
if (props.initialState.startOffset != null){
startOffset.value = props.initialState.startOffset as number;
}
@@ -336,6 +343,7 @@ function getMinorTicks(date: HistDate, scaleIdx: number, majorUnitSz: number, ma
return minorTicks;
}
+// Contains the ticks to render, computed from the start/end dates/offsets, the scale, and display area
const ticks = computed((): Tick[] => {
let ticks: Tick[] = [];
if (!mounted.value){
@@ -471,6 +479,7 @@ const lastIdx = computed((): number => {
const firstDate = computed(() => firstIdx.value < 0 ? startDate.value : ticks.value[firstIdx.value]!.date);
const lastDate = computed(() => lastIdx.value < 0 ? endDate.value : ticks.value[lastIdx.value]!.date);
+// True if the first visible tick is at startDate
const startIsFirstVisible = computed(() => {
if (ticks.value.length == 0){
return true;
@@ -490,7 +499,7 @@ const endIsLastVisible = computed(() => {
// ========== For displayed events ==========
-function dateToOffset(date: HistDate){ // Assumes 'date' is >=firstDate and <=lastDate
+function dateToUnitOffset(date: HistDate){ // Assumes 'date' is >=firstDate and <=lastDate
// Find containing major tick
let tickIdx = firstIdx.value;
for (let i = tickIdx + 1; i < lastIdx.value; i++){
@@ -539,11 +548,13 @@ function updateIdToEvent(){
}
idToEvent.value = map;
}
+
watch(() => props.eventTree, updateIdToEvent);
watch(ticks, updateIdToEvent);
const idToPos: Ref<Map<number, [number, number, number, number]>> = ref(new Map()); // Maps event IDs to x/y/w/h
const idsToSkipTransition: Ref<Set<number>> = ref(new Set()); // Used to prevent events moving across mainline
+
type LineCoords = [number, number, number, number]; // x, y, length, angle
const eventLines: Ref<Map<number, LineCoords>> = ref(new Map()); // Maps event ID to event line data
@@ -610,7 +621,7 @@ function getEventLayout(): Map<number, [number, number, number, number]> {
const maxOffset = availLen.value - eventMajorSz.value - store.spacing;
for (let event of orderedEvents){
// Get preferred offset in column
- let pxOffset = dateToOffset(event.start) / numUnits * availLen.value - eventMajorSz.value / 2;
+ let pxOffset = dateToUnitOffset(event.start) / numUnits * availLen.value - eventMajorSz.value / 2;
let targetOffset = Math.max(Math.min(pxOffset, maxOffset), minOffset);
// Find potential positions
@@ -775,7 +786,7 @@ function updateLayout(){
// Note: Drawing the line in the reverse direction causes 'detachment' from the mainline during transitions
let y2: number;
let event = idToEvent.value.get(id)!;
- let unitOffset = dateToOffset(event.start);
+ let unitOffset = dateToUnitOffset(event.start);
let posFrac = unitOffset / numUnits;
if (props.vert){
x = mainlineOffset.value;
@@ -810,11 +821,12 @@ function updateLayout(){
// Notify parent
emit('event-display', ID, [...map.keys()], firstDate.value, lastDate.value, minorScaleIdx.value);
}
+
watch(idToEvent, updateLayout);
watch(width, updateLayout);
watch(height, updateLayout);
-// ========== For event-count indicators ==========
+// ========== For event density indicators ==========
// Maps tick index to event count
const tickToCount = computed((): Map<number, number> => {
@@ -846,15 +858,15 @@ const timelinePosStr = computed((): string => {
const date2 = endIsLastVisible.value ? endDate.value : lastDate.value;
if (minorScale.value == DAY_SCALE){
const multiMonth = date1.month != date2.month;
- return `${date1.toYearString()} ${MONTH_NAMES[date1.month - 1]}${multiMonth ? ' >' : ''}`;
+ return `${dateToYearStr(date1)} ${MONTH_NAMES[date1.month - 1]}${multiMonth ? ' >' : ''}`;
} else if (minorScale.value == MONTH_SCALE){
const multiYear = date1.year != date2.year;
- return `${date1.toYearString()}${multiYear ? ' >' : ''}`;
+ return `${dateToYearStr(date1)}${multiYear ? ' >' : ''}`;
} else {
if (date1.year > 0){
- return `${date1.toYearString()} - ${date2.toYearString()}`;
+ return `${dateToYearStr(date1)} - ${dateToYearStr(date2)}`;
} else {
- return `${date1.toYearString()} >`;
+ return `${dateToYearStr(date1)} >`;
}
}
});
@@ -886,7 +898,7 @@ function panTimeline(scrollRatio: number){
} else {
// Pan up to an offset of store.defaultEndTickOffset
if (store.defaultEndTickOffset == endOffset.value){
- console.log('Reached maximum date limit');
+ console.log('INFO: Reached maximum date limit');
animateFailDiv('max');
newStartOffset = startOffset.value;
newEndOffset = endOffset.value;
@@ -923,7 +935,7 @@ function panTimeline(scrollRatio: number){
} else {
// Pan up to an offset of store.defaultEndTickOffset
if (store.defaultEndTickOffset == startOffset.value){
- console.log('Reached minimum date limit');
+ console.log('INFO: Reached minimum date limit');
animateFailDiv('min');
newStartOffset = startOffset.value;
newEndOffset = endOffset.value;
@@ -946,7 +958,7 @@ function panTimeline(scrollRatio: number){
}
if (newStart.isEarlier(MIN_CAL_DATE, scale.value) && (scale.value == MONTH_SCALE || scale.value == DAY_SCALE)){
- console.log('Unable to pan into dates where months/days are invalid');
+ console.log('INFO: Ignored pan into dates where months/days are invalid');
return;
}
if (!newStart.equals(startDate.value)){
@@ -963,7 +975,7 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){
if (zoomRatio > 1
&& startDate.value.equals(MIN_DATE, scale.value)
&& endDate.value.equals(MAX_DATE, scale.value)){
- console.log('Reached upper scale limit');
+ console.log('INFO: Reached upper scale limit');
animateFailDiv('both');
return;
}
@@ -1003,7 +1015,7 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){
newNumUnits = numUnits;
while (numStartSteps < 0){
if (newStart.equals(MIN_CAL_DATE, scale.value) && (scale.value == MONTH_SCALE || scale.value == DAY_SCALE)){
- console.log('Restricting new range to dates where month/day scale is usable');
+ console.log('INFO: Restricting new range to dates where month/day scale is usable');
newStartOffset = store.defaultEndTickOffset;
break;
}
@@ -1026,11 +1038,12 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){
}
newNumUnits += newStartOffset + newEndOffset;
}
+
// Possibly zoom in/out
let tickDiff = availLen.value / newNumUnits;
if (tickDiff < store.minTickSep){ // Zoom out into new scale
if (scaleIdx.value == 0){
- console.log('Reached zoom out limit');
+ console.log('INFO: Reached zoom out limit');
animateFailDiv('both');
return;
} else {
@@ -1096,7 +1109,7 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){
} else { // If trying to zoom in
if (scaleIdx.value == SCALES.length - 1){
if (newNumUnits < store.minLastTicks){
- console.log('Reached zoom in limit');
+ console.log('INFO: Reached zoom in limit');
animateFailDiv('bg');
return;
}
@@ -1130,7 +1143,7 @@ function zoomTimeline(zoomRatio: number, ignorePointer=false){
// Account for zooming into sub-year dates before MIN_CAL_DATE
if (newStart.isEarlier(MIN_CAL_DATE, newScale) && (newScale == MONTH_SCALE || newScale == DAY_SCALE)){
- console.log('Unable to zoom into range where month/day scale is invalid');
+ console.log('INFO: Ignored zooming into range where month/day scale is invalid');
animateFailDiv('bg');
return;
}
@@ -1291,9 +1304,12 @@ function onStateChg(){
ID, startDate.value, endDate.value, startOffset.value, endOffset.value, scaleIdx.value
));
}
+
watch(startDate, onStateChg);
+watch(endDate, onStateChg);
// ========== For jumping to search result ==========
+
const searchEvent = ref(null as null | HistEvent); // Holds most recent search result
let pendingSearch = false;
@@ -1303,7 +1319,7 @@ watch(() => props.searchTarget, () => {
return;
}
if (MAX_DATE.isEarlier(event.start)){
- console.log('Target is past maximum date');
+ console.log('INFO: Ignoring search target past maximum date');
animateFailDiv('max');
return;
}
@@ -1354,7 +1370,7 @@ watch(idToEvent, () => {
}
});
-// ========== For resets ==========
+// ========== For bound resets ==========
watch(() => props.reset, () => {
startDate.value = store.initialStartDate;
@@ -1392,9 +1408,11 @@ function onKeyDown(evt: KeyboardEvent){
}
}
}
+
onMounted(() => {
window.addEventListener('keydown', onKeyDown);
});
+
onUnmounted(() => {
window.removeEventListener('keydown', onKeyDown);
});
@@ -1434,7 +1452,8 @@ function tickStyles(tick: Tick){
const REF_LABEL = '9999 BC'; // Used as a reference for preventing tick label overlap
const refTickLabelWidth = getTextWidth(REF_LABEL, '14px Ubuntu') + 10;
-const tickLabelTexts = computed(() => ticks.value.map((tick: Tick) => tick.date.toTickString()));
+
+const tickLabelTexts = computed(() => ticks.value.map((tick: Tick) => dateToTickStr(tick.date)));
const tickLabelStyles = computed((): Record<string,string>[] => {
let numMajorUnits = getNumDisplayUnits();
diff --git a/src/lib.ts b/src/lib.ts
index e8ed448..51bd2a4 100644
--- a/src/lib.ts
+++ b/src/lib.ts
@@ -5,8 +5,6 @@
import {moduloPositive, intToOrdinal, getNumTrailingZeros} from './util';
import {RBTree} from './rbtree';
-export const DEBUG = true;
-
// ========== For calendar conversion (mostly copied from backend/hist_data/cal.py) ==========
export function gregorianToJdn(year: number, month: number, day: number): number {
@@ -77,6 +75,7 @@ export function getDaysInMonth(year: number, month: number){
export const MIN_CAL_YEAR = -4713; // Earliest year where months/day scales are usable
export const MONTH_NAMES = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+// (Same as in backend/hist_data/cal.py)
export class HistDate {
gcal: boolean | null;
year: number;
@@ -168,50 +167,6 @@ export class HistDate {
}
}
- toYearString(){
- if (this.year >= 1000){
- return String(this.year);
- } else if (this.year > 0){
- return String(this.year) + ' AD';
- } else if (this.year > -1e3){
- return String(-this.year) + ' BC';
- } else if (this.year > -1e6){
- if (this.year % 1e3 == 0){
- return String(Math.floor(-this.year / 1e3)) + 'k BC';
- } else if (this.year % 100 == 0){
- return String(Math.floor(-this.year / 100) / 10) + 'k BC';
- } else {
- return String(-this.year) + ' BC';
- }
- } else if (this.year > -1e9){
- if (this.year % 1e6 == 0){
- return String(Math.floor(-this.year / 1e6)) + ' mya';
- } else if (this.year % 1e3 == 0){
- return String(Math.floor(-this.year / 1e3) / 1e3) + ' mya';
- } else {
- return String(this.year.toLocaleString());
- }
- } else {
- if (this.year % 1e9 == 0){
- return String(Math.floor(-this.year / 1e9)) + ' bya';
- } else if (this.year % 1e6 == 0){
- return String(Math.floor(-this.year / 1e6) / 1e3) + ' bya';
- } else {
- return String(this.year.toLocaleString());
- }
- }
- }
-
- toTickString(){
- if (this.month == 1 && this.day == 1){
- return this.toYearString();
- } else if (this.day == 1){
- return MONTH_NAMES[this.month - 1];
- } else {
- return intToOrdinal(this.day);
- }
- }
-
toInt(){ // Used for v-for keys
return this.day + this.month * 50 + this.year * 1000;
}
@@ -224,9 +179,6 @@ export class YearDate extends HistDate {
declare day: 1;
constructor(year: number){
- // Note: Intentionally not enforcing year < MIN_CAL_YEAR here. This does mean a YearDate can be
- // interpreted as the same day as a CalDate, but it also avoids having HistEvents that
- // span across MIN_CAL_YEAR that have a mix of YearDates and CalDates.
super(null, year, 1, 1);
}
}
@@ -249,6 +201,7 @@ export const MIN_CAL_DATE = new CalDate(MIN_CAL_YEAR, 1, 1);
// ========== For event representation ==========
+// (Same as in backend/hist_data/cal.py)
export class HistEvent {
id: number;
title: string;
@@ -275,6 +228,7 @@ export class HistEvent {
}
}
+// (Same as in backend/hist_data/cal.py)
export class ImgInfo {
url: string;
license: string;
@@ -289,6 +243,7 @@ export class ImgInfo {
}
}
+// (Same as in backend/hist_data/cal.py)
export class EventInfo {
event: HistEvent;
desc: string | null;
@@ -310,6 +265,51 @@ export function cmpHistEvent(event: HistEvent, event2: HistEvent){
// ========== For [event] date display ==========
+export function dateToYearStr(date: HistDate){
+ const year = date.year;
+ if (year >= 1000){
+ return String(year);
+ } else if (year > 0){
+ return String(year) + ' AD';
+ } else if (year > -1e3){
+ return String(-year) + ' BC';
+ } else if (year > -1e6){
+ if (year % 1e3 == 0){
+ return String(Math.floor(-year / 1e3)) + 'k BC';
+ } else if (year % 100 == 0){
+ return String(Math.floor(-year / 100) / 10) + 'k BC';
+ } else {
+ return String(-year) + ' BC';
+ }
+ } else if (year > -1e9){
+ if (year % 1e6 == 0){
+ return String(Math.floor(-year / 1e6)) + ' mya';
+ } else if (year % 1e3 == 0){
+ return String(Math.floor(-year / 1e3) / 1e3) + ' mya';
+ } else {
+ return String(year.toLocaleString());
+ }
+ } else {
+ if (year % 1e9 == 0){
+ return String(Math.floor(-year / 1e9)) + ' bya';
+ } else if (year % 1e6 == 0){
+ return String(Math.floor(-year / 1e6) / 1e3) + ' bya';
+ } else {
+ return String(year.toLocaleString());
+ }
+ }
+}
+
+export function dateToTickStr(date: HistDate){
+ if (date.month == 1 && date.day == 1){
+ return dateToYearStr(date);
+ } else if (date.day == 1){
+ return MONTH_NAMES[date.month - 1];
+ } else {
+ return intToOrdinal(date.day);
+ }
+}
+
export function dateToDisplayStr(date: HistDate){
if (date.year <= -1e4){ // N.NNN billion/million/thousand years ago
if (date.year <= -1e9){
@@ -334,7 +334,7 @@ export function dateToDisplayStr(date: HistDate){
}
}
-// Converts a date with uncertain end bound to string for display
+// Converts a date with imprecise bounds into a string for display
export function boundedDateToStr(start: HistDate, end: HistDate | null) : string {
if (end == null){
return dateToDisplayStr(start);
@@ -344,6 +344,7 @@ export function boundedDateToStr(start: HistDate, end: HistDate | null) : string
if (startStr == endStr){
return startStr;
}
+
if (start.gcal == null && end.gcal == null){
if (startStr.endsWith(' years ago') && endStr.endsWith(' years ago')){
const dateRegex = /^(.*) (.*) years ago$/;
@@ -409,7 +410,7 @@ export async function queryServer(params: URLSearchParams, serverDataUrl=SERVER_
const response = await fetch(url.toString());
responseObj = await response.json();
} catch (error){
- console.log(`Error with querying ${url.toString()}: ${error}`);
+ console.log(`ERROR: Error with querying ${url.toString()}: ${error}`);
return null;
}
return responseObj;
@@ -494,35 +495,16 @@ export function jsonToImgInfo(json: ImgInfoJson | null): ImgInfo | null {
export const MIN_DATE = new YearDate(-13.8e9);
export const MAX_DATE = new CalDate(2030, 1, 1);
+
+// (Same as in /backend/hist_data/cal.py)
export const MONTH_SCALE = -1;
export const DAY_SCALE = -2;
export const SCALES = [1e9, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 100, 10, 1, MONTH_SCALE, DAY_SCALE];
- // The timeline will be divided into units of SCALES[0], then SCALES[1], etc
- // Positive ints represent numbers of years, -1 represents 1 month, -2 represents 1 day
-
-if (DEBUG){ // Validate SCALES
- if (SCALES[SCALES.length - 1] != DAY_SCALE
- || SCALES[SCALES.length - 2] != MONTH_SCALE
- || SCALES[SCALES.length - 3] != 1){
- throw new Error('SCALES must end with [1, MONTH_SCALE, DAY_SCALE]');
- }
- for (let i = 1; i < SCALES.length - 2; i++){
- if (SCALES[i] <= 0){
- throw new Error('SCALES must only have positive ints before MONTH_SCALE');
- }
- if (SCALES[i-1] <= SCALES[i]){
- throw new Error('SCALES must hold decreasing values');
- }
- if (SCALES[i-1] % SCALES[i] > 0){
- throw new Error('Each positive int in SCALES must divide the previous int');
- }
- }
-}
-export function stepDate( // Steps a date N units along a scale
- date: HistDate, scale: number, {forward=true, count=1, inplace=false} = {}): HistDate {
- // If stepping by month or years, leaves day value unchanged
- // Does not account for stepping a CalDate into before MIN_CAL_YEAR
+// Steps a date N units along a scale
+export function stepDate(date: HistDate, scale: number, {forward=true, count=1, inplace=false} = {}): HistDate {
+ // If stepping by month or years, leaves day value unchanged.
+ // Does not account for stepping a CalDate into before MIN_CAL_YEAR.
const newDate = inplace ? date : date.clone();
if (count < 0){
count = -count;
@@ -667,8 +649,8 @@ export function getEventPrecision(event: HistEvent): number {
return Number.POSITIVE_INFINITY;
}
-// For a YearDate and sub-yearly scale, uses the first day of the YearDate's year
export function dateToUnit(date: HistDate, scale: number): number {
+ // For a YearDate and sub-yearly scale, uses the first day of the YearDate's year
if (scale >= 1){
return Math.floor(date.year / scale);
} else if (scale == MONTH_SCALE){
diff --git a/src/util.ts b/src/util.ts
index bb0d162..ea9e76e 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -43,6 +43,7 @@ export function makeThrottled(hdlr: (...args: any[]) => void, delay: number){
timeout = window.setTimeout(async () => hdlr(...args), delay);
};
}
+
// Like makeThrottled(), but accepts an async function
export function makeThrottledAsync(hdlr: (...args: any[]) => Promise<void>, delay: number){
let timeout = 0;
@@ -51,6 +52,7 @@ export function makeThrottledAsync(hdlr: (...args: any[]) => Promise<void>, dela
timeout = window.setTimeout(async () => await hdlr(...args), delay);
};
}
+
// Like makeThrottled(), but, for runs of fast handler calls, calls it at spaced intervals, and at the start/end
export function makeThrottledSpaced(hdlr: (...args: any[]) => void, delay: number){
let lastHdlrTime = 0; // Used for throttling