Skip to content

Commit

Permalink
Refactor timeline-view type fixes
Browse files Browse the repository at this point in the history
Use a helper method to convert the DateType objects in a more type safe
way. While we are getting them as numbers in the actual application,
it's better to prepare for them being Date or String objects as well.
Write a helper method to deal with all 3 types that DateType can be.
  • Loading branch information
meisekimiu committed Jan 23, 2024
1 parent 085a408 commit 1ad06de
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 39 deletions.
22 changes: 22 additions & 0 deletions src/app/views/components/timeline-view/timeline-util.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as Util from './timeline-util';

describe('Timeline Utilities', () => {
describe('DateType To Number', () => {
it('exists', () => {
expect(Util.dateTypeToNumber).not.toBeUndefined();
});
it('will pass number values through', () => {
expect(Util.dateTypeToNumber(0)).toBe(0);
expect(Util.dateTypeToNumber(Math.PI)).toBe(Math.PI);
expect(Util.dateTypeToNumber(NaN)).toBeNaN();
});
it('will convert dates to number values', () => {
expect(Util.dateTypeToNumber(new Date(0))).toBe(0);
expect(Util.dateTypeToNumber(new Date(123456789))).toBe(123456789);
});
it('will convert strings to number values', () => {
expect(Util.dateTypeToNumber('0')).toBe(0);
expect(Util.dateTypeToNumber(Math.PI.toString())).toBe(Math.PI);
});
});
});
128 changes: 96 additions & 32 deletions src/app/views/components/timeline-view/timeline-util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DataItem, moment } from 'vis-timeline/standalone';
import { DataItem, DateType, moment } from 'vis-timeline/standalone';
import { RecordVO, FolderVO, ItemVO, TimezoneVOData } from '@models';
import { groupBy, minBy, maxBy, meanBy } from 'lodash';
import { isMobileWidth } from '@shared/services/device/device.service';
Expand All @@ -18,7 +18,7 @@ export enum TimelineGroupTimespan {
Month,
Day,
Hour,
Item
Item,
}

export interface TimelineDataItem {
Expand All @@ -34,7 +34,9 @@ const TimelineItemClasses = [
let timelineItemClassCounter = 0;

function getAlternatingTimelineItemClass() {
return TimelineItemClasses[timelineItemClassCounter++ % TimelineItemClasses.length];
return TimelineItemClasses[
timelineItemClassCounter++ % TimelineItemClasses.length
];
}

function getEvenSpreadItems(items: any[], count = 4) {
Expand Down Expand Up @@ -87,20 +89,28 @@ export class TimelineItem implements DataItem, TimelineDataItem {
this.item = item;
this.content = item.displayName;
this.className = getAlternatingTimelineItemClass();
this.start = getTimezoneDateFromDisplayDate(item.displayDT, timezone).valueOf();
this.start = getTimezoneDateFromDisplayDate(
item.displayDT,
timezone
).valueOf();

if (item instanceof FolderVO) {
this.dataType = 'folder';
this.imageWidth = `${imageHeight}px`;
const end = getTimezoneDateFromDisplayDate(item.displayEndDT, timezone).valueOf();
const end = getTimezoneDateFromDisplayDate(
item.displayEndDT,
timezone
).valueOf();
if (end - this.start > 6 * Month) {
this.end = end;
}
} else {
this.dataType = 'record';
const height = isMobileWidth() ? imageHeight : recordImageHeight;
this.imageHeight = `${height}px`;
this.imageWidth = `${height * (item.imageRatio ? 1 / Number(item.imageRatio) : 1)}px`;
this.imageWidth = `${
height * (item.imageRatio ? 1 / Number(item.imageRatio) : 1)
}px`;
}
}
}
Expand All @@ -119,15 +129,24 @@ export class TimelineGroup implements DataItem, TimelineDataItem {
previewThumbs: string[] = [];
groupName: string;

constructor(items: RecordVO[], timespan: TimelineGroupTimespan, name: string, timezone: TimezoneVOData = null) {
constructor(
items: RecordVO[],
timespan: TimelineGroupTimespan,
name: string,
timezone: TimezoneVOData = null
) {
this.groupItems = items;
this.previewThumbs = getEvenSpreadItems(items.map(i => i.thumbURL200));
this.previewThumbs = getEvenSpreadItems(items.map((i) => i.thumbURL200));
this.groupTimespan = timespan;
this.groupName = name;
this.content = name;
this.className = getAlternatingTimelineItemClass();
this.groupStart = moment.utc(minBy(items, item => item.displayDT).displayDT).valueOf();
this.groupEnd = moment.utc(maxBy(items, item => item.displayDT).displayDT).valueOf();
this.groupStart = moment
.utc(minBy(items, (item) => item.displayDT).displayDT)
.valueOf();
this.groupEnd = moment
.utc(maxBy(items, (item) => item.displayDT).displayDT)
.valueOf();
const diff = this.groupEnd - this.groupStart;
let minDiffForRange = 20 * Minute;
let neverRange = true;
Expand All @@ -144,18 +163,28 @@ export class TimelineGroup implements DataItem, TimelineDataItem {
break;
}


if (diff >= minDiffForRange && !neverRange) {
this.start = getTimezoneDateFromDisplayDate(this.groupStart, timezone).valueOf();
this.end = getTimezoneDateFromDisplayDate(this.groupEnd, timezone).valueOf();

this.start = getTimezoneDateFromDisplayDate(
this.groupStart,
timezone
).valueOf();
this.end = getTimezoneDateFromDisplayDate(
this.groupEnd,
timezone
).valueOf();
} else {
this.start = getTimezoneDateFromDisplayDate(meanBy(this.groupItems, i => new Date(i.displayDT).valueOf()), timezone).valueOf();
this.start = getTimezoneDateFromDisplayDate(
meanBy(this.groupItems, (i) => new Date(i.displayDT).valueOf()),
timezone
).valueOf();
}
}
}

function getTimezoneDateFromDisplayDate(displayDate: string | number, timezone: TimezoneVOData = null) {
function getTimezoneDateFromDisplayDate(
displayDate: string | number,
timezone: TimezoneVOData = null
) {
const m = moment.utc(displayDate);
if (!timezone) {
return m;
Expand All @@ -173,7 +202,12 @@ function getTimezoneDateFromDisplayDate(displayDate: string | number, timezone:
return m.add(offsetMinutes, 'minutes');
}

export function GroupByTimespan(items: ItemVO[], timespan: TimelineGroupTimespan, bestFit = false, timezone: TimezoneVOData = null) {
export function GroupByTimespan(
items: ItemVO[],
timespan: TimelineGroupTimespan,
bestFit = false,
timezone: TimezoneVOData = null
) {
const timelineItems: (TimelineGroup | TimelineItem)[] = [];
const records: RecordVO[] = [];
const minimumGroupCount = 20;
Expand All @@ -192,14 +226,14 @@ export function GroupByTimespan(items: ItemVO[], timespan: TimelineGroupTimespan
}

if (timespan === TimelineGroupTimespan.Item) {
timelineItems.push(...records.map(r => new TimelineItem(r)));
timelineItems.push(...records.map((r) => new TimelineItem(r)));
} else {
const groups = groupBy(records, record => {
const groups = groupBy(records, (record) => {
const groupFormat = getDateGroupFormatFromTimespan(timespan);
const displayFormat = getDisplayDateFormatFromTimespan(timespan);
const date = getTimezoneDateFromDisplayDate(record.displayDT, timezone);
if (timespan >= TimelineGroupTimespan.Year) {
return date.format(`${groupFormat}[.]${displayFormat}`);
return date.format(`${groupFormat}[.]${displayFormat}`);
} else {
let group = date.format('YYYY');
switch (timespan) {
Expand All @@ -218,10 +252,14 @@ export function GroupByTimespan(items: ItemVO[], timespan: TimelineGroupTimespan
if (groups.hasOwnProperty(key)) {
const groupItems = groups[key];
if (groupItems.length < minimumGroupCount) {
timelineItems.push(...groupItems.map(i => new TimelineItem(i)));
timelineItems.push(...groupItems.map((i) => new TimelineItem(i)));
} else {
const parts = key.split('.');
const timelineGroup = new TimelineGroup(groupItems, timespan, parts[1]);
const timelineGroup = new TimelineGroup(
groupItems,
timespan,
parts[1]
);
timelineItems.push(timelineGroup);
}
}
Expand All @@ -230,7 +268,7 @@ export function GroupByTimespan(items: ItemVO[], timespan: TimelineGroupTimespan

return {
groupedItems: timelineItems,
timespan: timespan
timespan: timespan,
};
}

Expand Down Expand Up @@ -259,25 +297,35 @@ export function GetBreadcrumbsFromRange(start: number, end: number) {
const path = [];

if (range <= Year * 1.05) {
path.push(mid.format(getDisplayDateFormatFromTimespan(TimelineGroupTimespan.Year)));
path.push(
mid.format(getDisplayDateFormatFromTimespan(TimelineGroupTimespan.Year))
);
}

if (range <= Month + Day) {
path.push(mid.format(getDisplayDateFormatFromTimespan(TimelineGroupTimespan.Month)));
path.push(
mid.format(getDisplayDateFormatFromTimespan(TimelineGroupTimespan.Month))
);
}

if (range <= Day + Hour) {
path.push(mid.format(getDisplayDateFormatFromTimespan(TimelineGroupTimespan.Day)));
path.push(
mid.format(getDisplayDateFormatFromTimespan(TimelineGroupTimespan.Day))
);
}

if (range <= Hour + 5 * Minute) {
path.push(mid.format(getDisplayDateFormatFromTimespan(TimelineGroupTimespan.Hour)));
path.push(
mid.format(getDisplayDateFormatFromTimespan(TimelineGroupTimespan.Hour))
);
}

return path;
}

function getDateGroupFormatFromTimespan(timespan: TimelineGroupTimespan): string {
function getDateGroupFormatFromTimespan(
timespan: TimelineGroupTimespan
): string {
switch (timespan) {
case TimelineGroupTimespan.Year:
return 'YYYY';
Expand All @@ -290,7 +338,9 @@ function getDateGroupFormatFromTimespan(timespan: TimelineGroupTimespan): string
}
}

function getDisplayDateFormatFromTimespan(timespan: TimelineGroupTimespan): string {
function getDisplayDateFormatFromTimespan(
timespan: TimelineGroupTimespan
): string {
switch (timespan) {
case TimelineGroupTimespan.Year:
case TimelineGroupTimespan.Decade:
Expand All @@ -305,14 +355,28 @@ function getDisplayDateFormatFromTimespan(timespan: TimelineGroupTimespan): stri
}
}

export function getBestFitTimespanForItems(items: ItemVO[]): TimelineGroupTimespan {
export function getBestFitTimespanForItems(
items: ItemVO[]
): TimelineGroupTimespan {
if (!items || !items.length) {
return TimelineGroupTimespan.Year;
}

const start = moment.utc(minBy(items, item => item.displayDT).displayDT).valueOf();
const endItem = maxBy(items, item => item.displayEndDT || item.displayDT);
const start = moment
.utc(minBy(items, (item) => item.displayDT).displayDT)
.valueOf();
const endItem = maxBy(items, (item) => item.displayEndDT || item.displayDT);
const end = moment.utc(endItem.displayEndDT || endItem.displayDT).valueOf();

return GetTimespanFromRange(start, end);
}

export function dateTypeToNumber(date: DateType): number {
if (typeof date === 'number') {
return date;
} else if (typeof date === 'string') {
return parseFloat(date);
} else {
return date.getTime();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
GroupByTimespan,
GetTimespanFromRange,
getBestFitTimespanForItems,
dateTypeToNumber,
} from './timeline-util';

interface VoDataItem extends DataItem {
Expand Down Expand Up @@ -415,7 +416,7 @@ export class TimelineViewComponent implements OnInit, AfterViewInit, OnDestroy {

let firstItemBefore: DataItem;
this.timelineItems.forEach((i) => {
if ((i.start as number) < midpointWithMinDiff) {
if (dateTypeToNumber(i.start) < midpointWithMinDiff) {
if (!firstItemBefore) {
firstItemBefore = i;
} else if (firstItemBefore.start < i.start) {
Expand All @@ -425,7 +426,7 @@ export class TimelineViewComponent implements OnInit, AfterViewInit, OnDestroy {
});

if (firstItemBefore) {
const newMidpoint = (firstItemBefore.start as number) - 10;
const newMidpoint = dateTypeToNumber(firstItemBefore.start) - 10;
this.timeline.moveTo(newMidpoint);
} else {
this.hasPrev = false;
Expand All @@ -442,7 +443,7 @@ export class TimelineViewComponent implements OnInit, AfterViewInit, OnDestroy {

let firstItemAfter: DataItem;
this.timelineItems.forEach((i) => {
if ((i.start as number) > midpointWithMinDiff) {
if (dateTypeToNumber(i.start) > midpointWithMinDiff) {
if (!firstItemAfter) {
firstItemAfter = i;
} else if (firstItemAfter.start > i.start) {
Expand All @@ -452,7 +453,7 @@ export class TimelineViewComponent implements OnInit, AfterViewInit, OnDestroy {
});

if (firstItemAfter) {
const newMidpoint = (firstItemAfter.start as number) + 10;
const newMidpoint = dateTypeToNumber(firstItemAfter.start) + 10;
this.timeline.moveTo(newMidpoint);
} else {
this.hasPrev = false;
Expand Down Expand Up @@ -516,10 +517,11 @@ export class TimelineViewComponent implements OnInit, AfterViewInit, OnDestroy {
findItemsInRange(start: number, end: number) {
const itemIds = [];
this.timelineItems.forEach((item) => {
const itemStart = dateTypeToNumber(item.start);
if (
(item.start as number) >= start &&
(item.start as number) <= end &&
(!item.end || (item.end as number) <= end)
itemStart >= start &&
itemStart <= end &&
(!item.end || dateTypeToNumber(item.end) <= end)
) {
itemIds.push(item.id);
}
Expand Down

0 comments on commit 1ad06de

Please sign in to comment.