Skip to content

Commit 72f7932

Browse files
committed
Improves timeline performance and interactions
- Adds new loading and empty states - Refines selecting and opening data points
1 parent bd9d882 commit 72f7932

File tree

8 files changed

+454
-231
lines changed

8 files changed

+454
-231
lines changed

src/webviews/apps/plus/timeline/components/chart.css.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const timelineChartStyles = css`
66
flex-direction: column;
77
width: 100%;
88
height: 100%;
9+
position: relative;
910
1011
--scroller-track-top: unset;
1112
--scroller-track-left: 0;
@@ -357,12 +358,45 @@ export const timelineChartStyles = css`
357358
cursor: pointer;
358359
}
359360
360-
.empty {
361-
padding: 0.4rem 2rem 1.3rem 2rem;
361+
@keyframes fadeIn {
362+
from {
363+
opacity: 0;
364+
}
365+
to {
366+
opacity: 1;
367+
}
368+
}
369+
370+
.notice {
371+
position: absolute;
372+
top: 0;
373+
left: 0;
374+
bottom: 0;
375+
right: 0;
376+
display: flex;
377+
flex-direction: column;
378+
align-items: center;
379+
justify-content: center;
380+
padding: 10% 2rem 30% 2rem;
362381
font-size: var(--font-size);
382+
383+
z-index: 1;
384+
}
385+
386+
.notice--blur {
387+
backdrop-filter: blur(15px);
388+
-webkit-backdrop-filter: blur(15px);
389+
390+
animation: fadeIn 0.3s ease-in;
391+
animation-fill-mode: forwards;
392+
opacity: 0;
393+
}
394+
395+
:host-context(:host[placement='view']) .notice--blur {
396+
animation-delay: 0.5s;
363397
}
364398
365-
.empty p {
399+
.notice p {
366400
margin-top: 0;
367401
}
368402

src/webviews/apps/plus/timeline/components/chart.ts

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { log } from '../../../../../system/decorators/log';
88
import { debounce } from '../../../../../system/function/debounce';
99
import { defer } from '../../../../../system/promise';
1010
import { pluralize, truncateMiddle } from '../../../../../system/string';
11-
import type { Commit, State, TimelineSliceBy } from '../../../../plus/timeline/protocol';
11+
import type { State, TimelineDatum, TimelineSliceBy } from '../../../../plus/timeline/protocol';
1212
import { renderCommitSha } from '../../../shared/components/commit-sha';
1313
import { GlElement } from '../../../shared/components/element';
1414
import { createFromDateDelta, formatDate, fromNow } from '../../../shared/date';
@@ -17,6 +17,7 @@ import type { SliderChangeEventDetail } from './slider';
1717
import { GlChartSlider } from './slider';
1818
import '@shoelace-style/shoelace/dist/components/resize-observer/resize-observer.js';
1919
import './scroller';
20+
import '../../../shared/components/indicators/watermark-loader';
2021

2122
export const tagName = 'gl-timeline-chart';
2223

@@ -51,8 +52,9 @@ export class GlTimelineChart extends GlElement {
5152
}
5253
>();
5354
private readonly _slicesByIndex = new Map<number, string>();
54-
private readonly _commitsByTimestamp = new Map<number, Commit>();
55+
private readonly _commitsByTimestamp = new Map<number, TimelineDatum>();
5556

57+
@state()
5658
private _loading?: ReturnType<typeof defer<void>>;
5759

5860
private get compact(): boolean {
@@ -166,24 +168,29 @@ export class GlTimelineChart extends GlElement {
166168
}
167169

168170
protected override render(): unknown {
169-
// Don't render anything if the data is still loading
170-
if (this.data === null) return nothing;
171-
if (!this.data?.length) {
172-
return html`<div class="empty"><p>No commits found for the specified time period.</p></div>`;
173-
}
174-
175-
return html`<gl-chart-scroller
176-
.range=${this._rangeScrollable}
177-
.visibleRange=${this._zoomedRangeScrollable}
178-
@gl-scroll=${this.onScroll}
179-
@gl-scroll-start=${this.onScrollStart}
180-
@gl-scroll-end=${this.onScrollEnd}
181-
>
182-
<sl-resize-observer @sl-resize=${this.onResize}>
183-
<div id="chart" tabindex="-1"></div>
184-
</sl-resize-observer>
185-
${this.renderFooter()}
186-
</gl-chart-scroller>`;
171+
return html`${this._loading?.pending
172+
? html`<div class="notice notice--blur">
173+
<gl-watermark-loader pulse><p>Loading...</p></gl-watermark-loader>
174+
</div>`
175+
: !this.data?.length
176+
? html`<div class="notice">
177+
<gl-watermark-loader
178+
><p>No commits found for the specified time period</p></gl-watermark-loader
179+
>
180+
</div>`
181+
: nothing}
182+
<gl-chart-scroller
183+
.range=${this._rangeScrollable}
184+
.visibleRange=${this._zoomedRangeScrollable}
185+
@gl-scroll=${this.onScroll}
186+
@gl-scroll-start=${this.onScrollStart}
187+
@gl-scroll-end=${this.onScrollEnd}
188+
>
189+
<sl-resize-observer @sl-resize=${this.onResize}>
190+
<div id="chart" tabindex="-1"></div>
191+
</sl-resize-observer>
192+
${this.data?.length ? this.renderFooter() : nothing}
193+
</gl-chart-scroller>`;
187194
}
188195

189196
private renderFooter() {
@@ -442,7 +449,7 @@ export class GlTimelineChart extends GlElement {
442449
return result;
443450
}
444451

445-
private calculateChangeMetrics(dataset: Commit[]): { q1: number; q3: number; maxChanges: number } {
452+
private calculateChangeMetrics(dataset: TimelineDatum[]): { q1: number; q3: number; maxChanges: number } {
446453
const sortedChanges = dataset.map(c => (c.additions ?? 0) + (c.deletions ?? 0)).sort((a, b) => a - b);
447454
return {
448455
maxChanges: sortedChanges[sortedChanges.length - 1],
@@ -511,7 +518,7 @@ export class GlTimelineChart extends GlElement {
511518

512519
@log<GlTimelineChart['prepareChartData']>({ args: { 0: d => d?.length } })
513520
private prepareChartData(
514-
dataset: Commit[],
521+
dataset: TimelineDatum[],
515522
metrics: { minRadius: number; maxRadius: number; q1: number; q3: number; maxChanges: number },
516523
): PreparedChartData {
517524
const commits = dataset.length + 1;
@@ -601,24 +608,18 @@ export class GlTimelineChart extends GlElement {
601608
const abortController = this._abortController;
602609

603610
const loading = defer<void>();
611+
void loading.promise.finally(() => (this._loading = undefined));
612+
604613
this._loading = loading;
605614
this.emit('gl-loading', loading.promise);
606615

607-
const data = await dataPromise;
616+
const data = (await dataPromise) ?? [];
608617

609618
if (abortController?.signal.aborted) {
610619
loading?.cancel();
611620
return;
612621
}
613622

614-
if (!data?.length) {
615-
this._chart?.destroy();
616-
this._chart = undefined;
617-
618-
loading?.fulfill();
619-
return;
620-
}
621-
622623
// Clear previous state
623624
this._slices.clear();
624625
this._slicesByIndex.clear();
@@ -639,7 +640,9 @@ export class GlTimelineChart extends GlElement {
639640
return;
640641
}
641642

642-
this.range = [new Date(data[data.length - 1].date), new Date(data[0].date)];
643+
this.range = data.length
644+
? [new Date(data[data.length - 1].date), new Date(data[0].date)]
645+
: [new Date(), new Date()];
643646

644647
// Initialize plugins
645648
bar();
@@ -657,7 +660,7 @@ export class GlTimelineChart extends GlElement {
657660

658661
onafterinit: () => {
659662
this.updateChartSize();
660-
setTimeout(() => loading?.fulfill(), 250);
663+
setTimeout(() => loading?.fulfill(), 0);
661664
},
662665
onrendered: this.getOnRenderedCallback(this),
663666
// Restore the zoomed domain when the chart is resized, because it gets lost
@@ -830,9 +833,11 @@ export class GlTimelineChart extends GlElement {
830833

831834
const commit = data[0];
832835
this._shaHovered = undefined;
833-
this._shaSelected = commit.sha;
836+
this._shaSelected = commit?.sha;
834837

835-
this.selectDataPoint(new Date(commit.date), { select: true });
838+
if (commit != null) {
839+
this.selectDataPoint(new Date(commit.date), { select: true });
840+
}
836841
} else {
837842
this._chart.config('axis.y.tick.values', yTickValues, false);
838843
this._chart.config('axis.y.min', minY, false);
@@ -849,11 +854,14 @@ export class GlTimelineChart extends GlElement {
849854
if (commit == null) {
850855
commit = data[0];
851856
this._shaHovered = undefined;
852-
this._shaSelected = commit.sha;
857+
this._shaSelected = commit?.sha;
858+
}
859+
860+
if (commit != null) {
861+
this.selectDataPoint(new Date(commit.date), { select: true });
853862
}
854-
this.selectDataPoint(new Date(commit.date), { select: true });
855863

856-
setTimeout(() => loading?.fulfill(), 250);
864+
setTimeout(() => loading?.fulfill(), 0);
857865
},
858866
});
859867
}

src/webviews/apps/plus/timeline/components/slider.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { PropertyValues } from 'lit';
33
import { css, html } from 'lit';
44
import { customElement, property, query, state } from 'lit/decorators.js';
55
import { isUncommitted, isUncommittedStaged, shortenRevision } from '../../../../../git/utils/revision.utils';
6-
import type { Commit } from '../../../../plus/timeline/protocol';
6+
import type { TimelineDatum } from '../../../../plus/timeline/protocol';
77
import { GlElement } from '../../../shared/components/element';
88
import '@shoelace-style/shoelace/dist/components/range/range.js';
99

@@ -40,12 +40,12 @@ export class GlChartSlider extends GlElement {
4040
private _max: number = 0;
4141
private _min: number = 0;
4242

43-
private _data: Commit[] | undefined;
43+
private _data: TimelineDatum[] | undefined;
4444
get data() {
4545
return this._data;
4646
}
4747
@property({ type: Array })
48-
set data(value: Commit[] | undefined) {
48+
set data(value: TimelineDatum[] | undefined) {
4949
if (this._data === value) return;
5050

5151
this._data = value;

src/webviews/apps/plus/timeline/timeline.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -393,10 +393,11 @@ export class GlTimelineApp extends GlApp<State> {
393393

394394
private _fireSelectDataPointDebounced: Deferrable<(e: CommitEventDetail) => void> | undefined;
395395
private fireSelectDataPoint(e: CommitEventDetail) {
396+
const { itemType } = this;
396397
this._fireSelectDataPointDebounced ??= debounce(
397-
(e: CommitEventDetail) => this._ipc.sendCommand(SelectDataPointCommand, e),
398-
150,
399-
{ maxWait: 250 },
398+
(e: CommitEventDetail) => this._ipc.sendCommand(SelectDataPointCommand, { itemType: itemType, ...e }),
399+
250,
400+
{ maxWait: itemType === 'file' ? 500 : undefined },
400401
);
401402
this._fireSelectDataPointDebounced(e);
402403
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { css } from 'lit';
2+
3+
export const baseStyles = css`
4+
.container {
5+
display: flex;
6+
flex-direction: column;
7+
align-items: center;
8+
justify-content: center;
9+
margin: auto;
10+
position: absolute;
11+
top: 5%;
12+
bottom: 45%;
13+
left: 0;
14+
right: 0;
15+
}
16+
17+
::slotted(p) {
18+
padding-top: 1rem;
19+
color: var(--color-foreground--75);
20+
font-size: 1.4rem;
21+
}
22+
23+
.watermark {
24+
width: 12rem;
25+
height: 12rem;
26+
fill: color-mix(in srgb, var(--color-foreground) 15%, var(--color-background));
27+
transform-origin: center;
28+
}
29+
`;
30+
31+
export const pulseStyles = css`
32+
@keyframes pulse {
33+
0% {
34+
transform: scale(0.9);
35+
}
36+
50% {
37+
transform: scale(1.05);
38+
}
39+
100% {
40+
transform: scale(0.9);
41+
}
42+
}
43+
44+
.watermark--pulse .watermark-path {
45+
transform: scale(0.9);
46+
animation: pulse 1.8s ease-in-out infinite;
47+
transform-origin: center;
48+
}
49+
50+
/* Stagger the pulse animation for a wave effect on all paths */
51+
/* Targeting all paths using their order within the SVG */
52+
.watermark-path:nth-of-type(1) {
53+
/* Target the outer circle path */
54+
animation-delay: 0.2s;
55+
}
56+
57+
.watermark-path:nth-of-type(2) {
58+
/* Target the connection path */
59+
animation-delay: 0.4s;
60+
}
61+
62+
.watermark-path:nth-of-type(3) {
63+
/* Target the first dot path */
64+
animation-delay: 0.1s;
65+
}
66+
67+
.watermark-path:nth-of-type(4) {
68+
/* Target the second dot path */
69+
animation-delay: 0.1s;
70+
}
71+
72+
.watermark-path:nth-of-type(5) {
73+
/* Target the third dot path */
74+
animation-delay: 0.1s;
75+
}
76+
`;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { html, LitElement } from 'lit';
2+
import { customElement, property } from 'lit/decorators.js';
3+
import { baseStyles, pulseStyles } from './watermark-loader.css';
4+
5+
export const tagName = 'gl-watermark-loader';
6+
7+
@customElement(tagName)
8+
export class GlWatermarkLoader extends LitElement {
9+
static override styles = [baseStyles, pulseStyles];
10+
11+
@property({ type: Boolean })
12+
pulse = false;
13+
14+
override render(): unknown {
15+
return html`<div class="container">
16+
<svg
17+
class="watermark${this.pulse ? ' watermark--pulse' : ''}"
18+
viewBox="0 0 28 28"
19+
xmlns="http://www.w3.org/2000/svg"
20+
>
21+
<path
22+
class="watermark-path"
23+
d="M14 3.25C12.5883 3.25 11.1904 3.52806 9.88615 4.0683C8.5819 4.60853 7.39683 5.40037 6.3986 6.3986C5.40037 7.39683 4.60853 8.5819 4.06829 9.88615C3.52806 11.1904 3.25 12.5883 3.25 14C3.25 15.4117 3.52806 16.8096 4.06829 18.1138C4.60853 19.4181 5.40037 20.6032 6.3986 21.6014C7.39683 22.5996 8.5819 23.3915 9.88615 23.9317C11.1904 24.4719 12.5883 24.75 14 24.75C16.8511 24.75 19.5854 23.6174 21.6014 21.6014C23.6174 19.5854 24.75 16.8511 24.75 14C24.75 11.1489 23.6174 8.41462 21.6014 6.3986C19.5854 4.38259 16.8511 3.25 14 3.25ZM2 14C2 10.8174 3.26428 7.76516 5.51472 5.51472C7.76516 3.26428 10.8174 2 14 2C17.1826 2 20.2348 3.26428 22.4853 5.51472C24.7357 7.76516 26 10.8174 26 14C26 17.1826 24.7357 20.2348 22.4853 22.4853C20.2348 24.7357 17.1826 26 14 26C10.8174 26 7.76516 24.7357 5.51472 22.4853C3.26428 20.2348 2 17.1826 2 14Z"
24+
/>
25+
<path class="watermark-path" d="M18 15L11.5 8.5L12.5 7.5L19 14L18 15ZM11.5 20V8H13V20H11.5Z" />
26+
<path
27+
class="watermark-path"
28+
d="M12.25 10.5C12.8467 10.5 13.419 10.2629 13.841 9.84099C14.2629 9.41903 14.5 8.84674 14.5 8.25C14.5 7.65326 14.2629 7.08097 13.841 6.65901C13.419 6.23705 12.8467 6 12.25 6C11.6533 6 11.081 6.23705 10.659 6.65901C10.2371 7.08097 10 7.65326 10 8.25C10 8.84674 10.2371 9.41903 10.659 9.84099C11.081 10.2629 11.6533 10.5 12.25 10.5Z"
29+
/>
30+
<path
31+
class="watermark-path"
32+
d="M18.25 16.5C18.5455 16.5 18.8381 16.4418 19.111 16.3287C19.384 16.2157 19.6321 16.0499 19.841 15.841C20.0499 15.6321 20.2157 15.384 20.3287 15.111C20.4418 14.8381 20.5 14.5455 20.5 14.25C20.5 13.9545 20.4418 13.6619 20.3287 13.389C20.2157 13.116 20.0499 12.8679 19.841 12.659C19.6321 12.4501 19.384 12.2843 19.111 12.1713C18.8381 12.0582 18.5455 12 18.25 12C17.6533 12 17.081 12.2371 16.659 12.659C16.2371 13.081 16 13.6533 16 14.25C16 14.8467 16.2371 15.419 16.659 15.841C17.081 16.2629 17.6533 16.5 18.25 16.5Z"
33+
/>
34+
<path
35+
class="watermark-path"
36+
d="M12.25 22C12.8467 22 13.419 21.7629 13.841 21.341C14.2629 20.919 14.5 20.3467 14.5 19.75C14.5 19.1533 14.2629 18.581 13.841 18.159C13.419 17.7371 12.8467 17.5 12.25 17.5C11.6533 17.5 11.081 17.7371 10.659 18.159C10.2371 18.581 10 19.1533 10 19.75C10 20.3467 10.2371 20.919 10.659 21.341C11.081 21.7629 11.6533 22 12.25 22Z"
37+
/>
38+
</svg>
39+
<slot></slot>
40+
</div>`;
41+
}
42+
}

0 commit comments

Comments
 (0)