aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorTerry Truong <terry06890@gmail.com>2023-01-21 15:23:51 +1100
committerTerry Truong <terry06890@gmail.com>2023-01-21 16:17:31 +1100
commitc318c4cedf3f50c21c403649945c2abbbc30a89e (patch)
treec74f967755c1b653a450973712a99bec65724f6a /src
parentd581e5b61a771ef8619a5bfbc84a6e337c7ca13f (diff)
Do more minor refactoring
Document some variables coupled between client and server. Add more term consistency ('unit', 'event density'). Make console messages more consistent.
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