|
1 | 1 | <script lang="ts">
|
2 | 2 | import '$lib/app.css';
|
3 | 3 | import { debounce } from '$lib/utils';
|
4 |
| - import { Theme, theme } from '@immich/ui'; |
| 4 | + import { LoadingSpinner, Theme, theme } from '@immich/ui'; |
5 | 5 | import { DateTime } from 'luxon';
|
6 |
| - import { onMount } from 'svelte'; |
| 6 | + import { onMount, untrack } from 'svelte'; |
7 | 7 | import uPlot, { type Axis } from 'uplot';
|
8 | 8 |
|
9 | 9 | type DataRecord = [DateTime, number];
|
|
12 | 12 | type Props = {
|
13 | 13 | id: string;
|
14 | 14 | label: string;
|
15 |
| - data: DataRecord[]; |
| 15 | + data?: DataRecord[]; |
16 | 16 | cursorOpts: uPlot.Cursor;
|
17 | 17 | color: Colors;
|
18 | 18 | };
|
|
28 | 28 | let tooltipValue = $state('');
|
29 | 29 | let mousePosition = $state<{ x: number; y: number }>({ x: 0, y: 0 });
|
30 | 30 |
|
31 |
| - const xAxis: number[] = []; |
32 |
| - const yAxis: number[] = []; |
| 31 | + const { xAxis, yAxis } = $derived.by(() => { |
| 32 | + const xAxis: number[] = []; |
| 33 | + const yAxis: number[] = []; |
33 | 34 |
|
34 |
| - for (const [date, value] of data) { |
35 |
| - xAxis.push(date.toMillis() / 1000); |
36 |
| - yAxis.push(value); |
37 |
| - } |
| 35 | + for (const [date, value] of data ?? []) { |
| 36 | + xAxis.push(date.toMillis() / 1000); |
| 37 | + yAxis.push(value); |
| 38 | + } |
| 39 | + return { xAxis, yAxis }; |
| 40 | + }); |
38 | 41 |
|
39 | 42 | let colors = {
|
40 | 43 | yellow: 'rgb(255, 197, 0)',
|
|
50 | 53 | blue: 'rgba(63, 106, 222, 0.1)',
|
51 | 54 | };
|
52 | 55 |
|
53 |
| - let plot: uPlot; |
54 |
| -
|
55 | 56 | let isDark = $derived(theme.value === Theme.Dark);
|
56 | 57 |
|
57 | 58 | const hideTooltipElement = () => {
|
|
68 | 69 | }
|
69 | 70 | };
|
70 | 71 |
|
71 |
| - onMount(() => { |
72 |
| - if (!chartElement) { |
73 |
| - return; |
74 |
| - } |
75 |
| -
|
76 |
| - const formatData: uPlot.AlignedData = [new Float64Array(xAxis), new Float64Array(yAxis)]; |
| 72 | + const axis: Axis = $derived({ |
| 73 | + stroke: () => (isDark ? '#ccc' : 'black'), |
| 74 | + ticks: { |
| 75 | + stroke: () => (isDark ? '#444' : '#ddd'), |
| 76 | + }, |
| 77 | + grid: { |
| 78 | + show: false, |
| 79 | + }, |
| 80 | + }); |
77 | 81 |
|
78 |
| - const axis: Axis = { |
79 |
| - stroke: () => (isDark ? '#ccc' : 'black'), |
80 |
| - ticks: { |
81 |
| - stroke: () => (isDark ? '#444' : '#ddd'), |
82 |
| - }, |
83 |
| - grid: { |
84 |
| - show: false, |
| 82 | + const opts: uPlot.Options = $derived({ |
| 83 | + id, |
| 84 | + padding: [16, 16, 16, 16], |
| 85 | + cursor: cursorOpts, |
| 86 | + width: chartWidth, |
| 87 | + height: chartHeight, |
| 88 | + scales: {}, |
| 89 | + legend: { |
| 90 | + show: true, |
| 91 | + }, |
| 92 | + series: [ |
| 93 | + { |
| 94 | + value: '{YYYY}-{MM}-{DD}', |
| 95 | + label: 'Date', |
85 | 96 | },
|
86 |
| - }; |
87 |
| -
|
88 |
| - const opts: uPlot.Options = { |
89 |
| - id, |
90 |
| - padding: [16, 16, 16, 16], |
91 |
| - cursor: cursorOpts, |
92 |
| - width: 500, |
93 |
| - height: 500, |
94 |
| - scales: {}, |
95 |
| - legend: { |
| 97 | + { |
96 | 98 | show: true,
|
| 99 | + spanGaps: false, |
| 100 | + stroke: colors[color], |
| 101 | + width: 2, |
| 102 | + fill: areaColor[color], |
| 103 | + label, |
97 | 104 | },
|
98 |
| - series: [ |
99 |
| - { |
100 |
| - value: '{YYYY}-{MM}-{DD}', |
101 |
| - label: 'Date', |
102 |
| - }, |
103 |
| - { |
104 |
| - show: true, |
105 |
| - spanGaps: false, |
106 |
| - stroke: colors[color], |
107 |
| - width: 2, |
108 |
| - fill: areaColor[color], |
109 |
| - label, |
110 |
| - }, |
111 |
| - ], |
| 105 | + ], |
112 | 106 |
|
113 |
| - axes: [axis, axis], |
| 107 | + axes: [axis, axis], |
114 | 108 |
|
115 |
| - hooks: { |
116 |
| - setCursor: [ |
117 |
| - (u) => { |
118 |
| - if (u.root.id != chartId || !tooltipElement) { |
119 |
| - return; |
120 |
| - } |
| 109 | + hooks: { |
| 110 | + setCursor: [ |
| 111 | + (u) => { |
| 112 | + if (u.root.id != chartId || !tooltipElement) { |
| 113 | + return; |
| 114 | + } |
121 | 115 |
|
122 |
| - const { idx } = u.cursor; |
| 116 | + const { idx } = u.cursor; |
123 | 117 |
|
124 |
| - if (idx == null) { |
125 |
| - hideTooltipElement(); |
126 |
| - return; |
127 |
| - } |
| 118 | + if (idx == null) { |
| 119 | + hideTooltipElement(); |
| 120 | + return; |
| 121 | + } |
128 | 122 |
|
129 |
| - const date = new Date(u.data[0][idx] * 1000).toLocaleDateString(); |
130 |
| - const value = u.data[1][idx]; |
| 123 | + const date = new Date(u.data[0][idx] * 1000).toLocaleDateString(); |
| 124 | + const value = u.data[1][idx]; |
131 | 125 |
|
132 |
| - tooltipDate = date; |
133 |
| - tooltipValue = value?.toLocaleString() || ''; |
| 126 | + tooltipDate = date; |
| 127 | + tooltipValue = value?.toLocaleString() || ''; |
134 | 128 |
|
135 |
| - showTooltipElement(); |
136 |
| - }, |
137 |
| - ], |
138 |
| - setSeries: [() => hideTooltipElement()], |
139 |
| - }, |
140 |
| - }; |
| 129 | + showTooltipElement(); |
| 130 | + }, |
| 131 | + ], |
| 132 | + setSeries: [() => hideTooltipElement()], |
| 133 | + }, |
| 134 | + }); |
| 135 | +
|
| 136 | + const formatData: uPlot.AlignedData = $derived([new Float64Array(xAxis), new Float64Array(yAxis)]); |
141 | 137 |
|
| 138 | + let plot: uPlot; |
| 139 | + onMount(() => { |
142 | 140 | plot = new uPlot(opts, formatData, chartElement);
|
143 |
| - plot.setSize({ width: chartWidth, height: chartHeight }); |
144 | 141 | });
|
145 | 142 |
|
146 | 143 | $effect(() => {
|
147 |
| - if (plot && theme.value) { |
148 |
| - plot.redraw(false); |
| 144 | + plot.setData(formatData); |
| 145 | + }); |
| 146 | +
|
| 147 | + $effect(() => { |
| 148 | + // really this is just to make this reactive when opts updates. |
| 149 | + if (!opts) { |
| 150 | + return; |
149 | 151 | }
|
| 152 | +
|
| 153 | + plot.destroy(); |
| 154 | + untrack(() => (plot = new uPlot(opts, formatData, chartElement))); |
150 | 155 | });
|
151 | 156 |
|
152 | 157 | const onResize = debounce(() => {
|
|
161 | 166 |
|
162 | 167 | <svelte:window onresize={onResize} onmousemove={onMouseMove} />
|
163 | 168 |
|
164 |
| -<!-- svelte-ignore a11y_mouse_events_have_key_events --> |
165 |
| -<!-- svelte-ignore a11y_no_static_element_interactions --> |
166 |
| -<div |
167 |
| - class="h-[275px] mb-10 w-full relative" |
168 |
| - bind:clientWidth={chartWidth} |
169 |
| - bind:clientHeight={chartHeight} |
170 |
| - bind:this={chartElement} |
171 |
| - onmouseover={() => { |
172 |
| - showTooltipElement(); |
173 |
| - chartId = id; |
174 |
| - }} |
175 |
| - onmouseleave={() => { |
176 |
| - hideTooltipElement(); |
177 |
| - chartId = ''; |
178 |
| - }} |
179 |
| -></div> |
180 |
| -<div |
181 |
| - bind:this={tooltipElement} |
182 |
| - style="top: {mousePosition.y - 32}px; left: {mousePosition.x - 112}px" |
183 |
| - class="absolute border shadow-md text-xs w-[100px] rounded-lg bg-light py-2 px-3 text-center font-mono {chartId && |
184 |
| - tooltipValue |
185 |
| - ? '' |
186 |
| - : 'hidden'}" |
187 |
| -> |
188 |
| - <p>{tooltipDate}</p> |
189 |
| - <p class="font-bold">{tooltipValue}</p> |
190 |
| -</div> |
| 169 | +{#if data === undefined} |
| 170 | + <div class="flex h-[275px] justify-center items-center"> |
| 171 | + <LoadingSpinner size="giant" /> |
| 172 | + </div> |
| 173 | +{:else} |
| 174 | + <!-- svelte-ignore a11y_mouse_events_have_key_events --> |
| 175 | + <!-- svelte-ignore a11y_no_static_element_interactions --> |
| 176 | + <div |
| 177 | + class="h-[275px] mb-10 w-full relative" |
| 178 | + bind:clientWidth={chartWidth} |
| 179 | + bind:clientHeight={chartHeight} |
| 180 | + bind:this={chartElement} |
| 181 | + onmouseover={() => { |
| 182 | + showTooltipElement(); |
| 183 | + chartId = id; |
| 184 | + }} |
| 185 | + onmouseleave={() => { |
| 186 | + hideTooltipElement(); |
| 187 | + chartId = ''; |
| 188 | + }} |
| 189 | + ></div> |
| 190 | + <div |
| 191 | + bind:this={tooltipElement} |
| 192 | + style="top: {mousePosition.y - 32}px; left: {mousePosition.x - 112}px" |
| 193 | + class="absolute border shadow-md text-xs w-[100px] rounded-lg bg-light py-2 px-3 text-center font-mono {chartId && |
| 194 | + tooltipValue |
| 195 | + ? '' |
| 196 | + : 'hidden'}" |
| 197 | + > |
| 198 | + <p>{tooltipDate}</p> |
| 199 | + <p class="font-bold">{tooltipValue}</p> |
| 200 | + </div> |
| 201 | +{/if} |
0 commit comments