diff options
| author | Terry Truong <terry06890@gmail.com> | 2023-01-21 15:23:51 +1100 |
|---|---|---|
| committer | Terry Truong <terry06890@gmail.com> | 2023-01-21 16:17:31 +1100 |
| commit | c318c4cedf3f50c21c403649945c2abbbc30a89e (patch) | |
| tree | c74f967755c1b653a450973712a99bec65724f6a /src | |
| parent | d581e5b61a771ef8619a5bfbc84a6e337c7ca13f (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.vue | 64 | ||||
| -rw-r--r-- | src/components/BaseLine.vue | 4 | ||||
| -rw-r--r-- | src/components/SearchModal.vue | 8 | ||||
| -rw-r--r-- | src/components/SettingsModal.vue | 2 | ||||
| -rw-r--r-- | src/components/TimeLine.vue | 71 | ||||
| -rw-r--r-- | src/lib.ts | 136 | ||||
| -rw-r--r-- | src/util.ts | 2 |
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(); @@ -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 |
