mirror of
https://github.com/annimon/obsidian-timelive.git
synced 2026-07-22 05:45:09 +00:00
Use narrower month interval for calendar dates
This commit is contained in:
parent
f61ea43592
commit
d7259edb4d
4 changed files with 107 additions and 43 deletions
|
|
@ -1,9 +1,11 @@
|
|||
import type { Moment } from "moment";
|
||||
import { TimeliveSettings } from "./TimeliveSettings.ts";
|
||||
import { TimeUnit } from "./TimeUnit.ts";
|
||||
|
||||
export interface DateFormatter {
|
||||
formatSpan(fromDate: Moment, toDate: Moment): string;
|
||||
formatDate(date: Moment): string;
|
||||
formatCalendarDate(date: Moment, unit: TimeUnit): string;
|
||||
}
|
||||
|
||||
export class TimeliveDateFormatter implements DateFormatter {
|
||||
|
|
@ -20,4 +22,12 @@ export class TimeliveDateFormatter implements DateFormatter {
|
|||
public formatDate(date: Moment): string {
|
||||
return date.format(this.settings.previewTitleDateFormat);
|
||||
}
|
||||
|
||||
public formatCalendarDate(date: Moment, unit: TimeUnit): string {
|
||||
if (unit === TimeUnit.Month) {
|
||||
return date.format("YYYY-MM");
|
||||
} else {
|
||||
return date.format("YYYY");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
src/TimeUnit.ts
Normal file
16
src/TimeUnit.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export enum TimeUnit {
|
||||
Month = "month",
|
||||
Year = "year",
|
||||
}
|
||||
type TimeUnitData = {
|
||||
daysInUnit: number;
|
||||
};
|
||||
|
||||
export const TIME_UNITS: Record<TimeUnit, TimeUnitData> = {
|
||||
[TimeUnit.Month]: {
|
||||
daysInUnit: 31,
|
||||
},
|
||||
[TimeUnit.Year]: {
|
||||
daysInUnit: 365.2422,
|
||||
},
|
||||
};
|
||||
|
|
@ -1,39 +1,48 @@
|
|||
import type { Moment } from "moment";
|
||||
import { TIME_UNITS, TimeUnit } from "./TimeUnit.ts";
|
||||
|
||||
const DAYS_IN_YEAR = 365.2422;
|
||||
|
||||
export type TimeSpan = { min?: Moment; max?: Moment };
|
||||
export type YearRange = { fromYear: number; toYear: number };
|
||||
export type TimeSpan = { min?: Moment; max?: Moment; unit: TimeUnit };
|
||||
export type CalendarDates = {
|
||||
dates: Set<Moment>;
|
||||
unit: TimeUnit;
|
||||
};
|
||||
|
||||
export interface TimelineBuilder {
|
||||
addDate(date: Moment): void;
|
||||
|
||||
getActualTimeSpan(): TimeSpan;
|
||||
getYearRange(): YearRange;
|
||||
getCalendarTimeSpan(): TimeSpan;
|
||||
|
||||
buildCalendarDates(): CalendarDates;
|
||||
calculatePosition(date: Moment): number;
|
||||
}
|
||||
|
||||
export class DynamicTimelineBuilder implements TimelineBuilder {
|
||||
private actualSpan: TimeSpan = {};
|
||||
private startOfYear?: Moment;
|
||||
private deltaYears = 0.0;
|
||||
private actualSpan: TimeSpan = { unit: TimeUnit.Month };
|
||||
private startOfUnit?: Moment;
|
||||
private deltaInUnit = 0.0;
|
||||
|
||||
addDate(date: Moment): void {
|
||||
const time = date.valueOf();
|
||||
if (!this.actualSpan.min || !this.actualSpan.max) {
|
||||
const { min, max, unit } = this.actualSpan;
|
||||
if (!min || !max) {
|
||||
this.actualSpan.min = date;
|
||||
this.actualSpan.max = date;
|
||||
this.startOfYear = date.clone().startOf("year");
|
||||
this.startOfUnit = date.clone().startOf(unit);
|
||||
} else {
|
||||
const { min, max } = this.actualSpan;
|
||||
if (time < min.valueOf()) {
|
||||
this.actualSpan.min = date;
|
||||
this.startOfYear = date.clone().startOf("year");
|
||||
this.startOfUnit = date.clone().startOf(unit);
|
||||
} else if (time > max.valueOf()) {
|
||||
this.actualSpan.max = date;
|
||||
} else return; // no updates to min/max dates -> skip
|
||||
this.deltaYears = 1 + this.actualSpan.max.diff(this.startOfYear, "years");
|
||||
|
||||
const newUnit = this.determineUnit();
|
||||
if (newUnit !== unit) {
|
||||
this.actualSpan.unit = newUnit;
|
||||
this.startOfUnit = this.actualSpan.min!.clone().startOf(newUnit);
|
||||
}
|
||||
this.deltaInUnit = 1 + this.actualSpan.max!.diff(this.startOfUnit, this.actualSpan.unit);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,23 +50,67 @@ export class DynamicTimelineBuilder implements TimelineBuilder {
|
|||
return this.actualSpan;
|
||||
}
|
||||
|
||||
getYearRange(): YearRange {
|
||||
const fromYear = this.getFromYear();
|
||||
getCalendarTimeSpan(): TimeSpan {
|
||||
const { min, max, unit } = this.actualSpan;
|
||||
const start = (min?.clone() ?? this.now()).startOf(unit);
|
||||
return {
|
||||
fromYear: fromYear,
|
||||
toYear: 1 + (this.actualSpan.max?.year() ?? fromYear),
|
||||
min: start,
|
||||
max: (max ?? start).clone().add(1, unit),
|
||||
unit,
|
||||
};
|
||||
}
|
||||
|
||||
calculatePosition(date: Moment): number {
|
||||
const deltaDays = date.diff(this.startOfYear, "days");
|
||||
const totalDays = this.deltaYears * DAYS_IN_YEAR;
|
||||
const unit = this.actualSpan.unit;
|
||||
const deltaDays = date.diff(this.startOfUnit, "days");
|
||||
const totalDays = this.deltaInUnit * TIME_UNITS[unit].daysInUnit;
|
||||
const value = (deltaDays / totalDays) * 100;
|
||||
// Round to 3 decimal points
|
||||
return Math.round(value * 1000) / 1000;
|
||||
}
|
||||
|
||||
private getFromYear(): number {
|
||||
return this.startOfYear?.year() ?? (new Date().getFullYear());
|
||||
buildCalendarDates(): CalendarDates {
|
||||
const { min, max, unit } = this.getCalendarTimeSpan();
|
||||
const dates = this.splitDates(new Set<Moment>(), min!, max!, unit, 0);
|
||||
return { dates, unit };
|
||||
}
|
||||
|
||||
// Recursively split dates for building a calendar
|
||||
private splitDates(
|
||||
dates: Set<Moment>,
|
||||
a: Moment,
|
||||
b: Moment,
|
||||
unit: TimeUnit,
|
||||
level: number,
|
||||
): Set<Moment> {
|
||||
if (level < 3 && !this.datesMatchInUnit(a, b, unit)) {
|
||||
dates.add(a).add(b);
|
||||
const mid = a.clone().add(b.diff(a, unit, true) / 2, unit);
|
||||
this.splitDates(dates, a, mid, unit, level + 1);
|
||||
this.splitDates(dates, mid, b, unit, level + 1);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
private datesMatchInUnit(a: Moment, b: Moment, unit: TimeUnit): boolean {
|
||||
if (unit === TimeUnit.Year) {
|
||||
return a.year() === b.year();
|
||||
} else {
|
||||
return a.year() === b.year() && a.month() === b.month();
|
||||
}
|
||||
}
|
||||
|
||||
private determineUnit(): TimeUnit {
|
||||
const { min, max } = this.actualSpan;
|
||||
if (!min || !max) return TimeUnit.Month;
|
||||
const deltaMonths = max.diff(min, "months");
|
||||
if (deltaMonths <= 6) return TimeUnit.Month;
|
||||
return TimeUnit.Year;
|
||||
}
|
||||
|
||||
private now(): Moment {
|
||||
// @ts-ignore: deno lack of type
|
||||
const moment: MomentCallable = globalThis.moment;
|
||||
return moment();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,17 +59,18 @@ export class Timelive {
|
|||
public render() {
|
||||
this.root.empty();
|
||||
this.root.style.minWidth = "100%";
|
||||
this.renderYears();
|
||||
this.renderCalendar();
|
||||
this.renderLine();
|
||||
}
|
||||
|
||||
private renderYears() {
|
||||
private renderCalendar() {
|
||||
const yearsContainer = this.root.createDiv({ cls: "tlv-years" });
|
||||
const { fromYear, toYear } = this.timelineBuilder.getYearRange();
|
||||
const years = this.splitYears(new Set<number>(), fromYear, toYear, 0);
|
||||
[...years]
|
||||
const { dates, unit } = this.timelineBuilder.buildCalendarDates();
|
||||
const fmt = this.transformer.formatter;
|
||||
const formattedDates = [...dates].map((date) => fmt.formatCalendarDate(date, unit));
|
||||
[...new Set(formattedDates)]
|
||||
.sort()
|
||||
.forEach((year) => yearsContainer.createSpan({ text: `${year}` }));
|
||||
.forEach((date) => yearsContainer.createSpan({ text: `${date}` }));
|
||||
}
|
||||
|
||||
private renderLine() {
|
||||
|
|
@ -176,22 +177,6 @@ export class Timelive {
|
|||
parent.append(...children);
|
||||
}
|
||||
|
||||
// Recursively split years for building a years line
|
||||
private splitYears(
|
||||
years: Set<number>,
|
||||
a: number,
|
||||
b: number,
|
||||
level: number,
|
||||
): Set<number> {
|
||||
if (level < 3 && a != b) {
|
||||
years.add(a).add(b);
|
||||
const average = Math.floor(a / 2 + b / 2);
|
||||
this.splitYears(years, a, average, level + 1);
|
||||
this.splitYears(years, average, b, level + 1);
|
||||
}
|
||||
return years;
|
||||
}
|
||||
|
||||
private getPopoverPosition(marker: HTMLElement): { x: number; y: number } {
|
||||
const rect = marker.getBoundingClientRect();
|
||||
const x = rect.left + globalThis.scrollX;
|
||||
|
|
|
|||
Loading…
Reference in a new issue