@genspectrum/dashboard-components 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/custom-elements.json +36 -4
  2. package/dist/dashboard-components.js +181 -55
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +13 -7
  5. package/dist/style.css +53 -2
  6. package/package.json +1 -1
  7. package/src/constants.ts +1 -0
  8. package/src/preact/components/tooltip.stories.tsx +12 -2
  9. package/src/preact/components/tooltip.tsx +37 -13
  10. package/src/preact/mutationsOverTime/__mockData__/aggregated_byDay.json +38 -0
  11. package/src/preact/mutationsOverTime/__mockData__/aggregated_byWeek.json +122 -0
  12. package/src/preact/mutationsOverTime/__mockData__/aggregated_tooManyMutations.json +1470 -0
  13. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_20_01_2024.json +6778 -0
  14. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_21_01_2024.json +7129 -0
  15. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_22_01_2024.json +4681 -0
  16. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_23_01_2024.json +10738 -0
  17. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_24_01_2024.json +11710 -0
  18. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_25_01_2024.json +11557 -0
  19. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_26_01_2024.json +8596 -0
  20. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_tooManyMutations.json +16453 -0
  21. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_week3_2024.json +8812 -0
  22. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_week4_2024.json +9730 -0
  23. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_week5_2024.json +9865 -0
  24. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_week6_2024.json +11314 -0
  25. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +62 -43
  26. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +62 -8
  27. package/src/preact/mutationsOverTime/mutations-over-time.tsx +32 -8
  28. package/src/utils/temporal.spec.ts +3 -4
  29. package/src/utils/temporal.ts +9 -4
  30. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +254 -2
  31. package/src/web-components/visualization/gs-mutations-over-time.tsx +11 -5
@@ -6,8 +6,7 @@ import {
6
6
  } from '../../query/queryMutationsOverTime';
7
7
  import { type Deletion, type Substitution } from '../../utils/mutations';
8
8
  import { compareTemporal, type Temporal, YearMonthDay } from '../../utils/temporal';
9
- import { UserFacingError } from '../components/error-display';
10
- import Tooltip from '../components/tooltip';
9
+ import Tooltip, { type TooltipPosition } from '../components/tooltip';
11
10
  import { singleGraphColorRGBByName } from '../shared/charts/colors';
12
11
  import { formatProportion } from '../shared/table/formatProportion';
13
12
 
@@ -18,57 +17,77 @@ export interface MutationsOverTimeGridProps {
18
17
  const MAX_NUMBER_OF_GRID_ROWS = 100;
19
18
 
20
19
  const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({ data }) => {
21
- const mutations = data.getFirstAxisKeys();
22
- if (mutations.length > MAX_NUMBER_OF_GRID_ROWS) {
23
- throw new UserFacingError(
24
- 'Too many mutations',
25
- `The dataset contains ${mutations.length} mutations. ` +
26
- `Please adapt the filters to reduce the number to below ${MAX_NUMBER_OF_GRID_ROWS}.`,
27
- );
28
- }
20
+ const allMutations = data.getFirstAxisKeys();
21
+ const shownMutations = allMutations.slice(0, MAX_NUMBER_OF_GRID_ROWS);
29
22
 
30
23
  const dates = data.getSecondAxisKeys().sort((a, b) => compareTemporal(a, b));
31
24
 
32
25
  return (
33
- <div
34
- style={{
35
- display: 'grid',
36
- gridTemplateRows: `repeat(${mutations.length}, 24px)`,
37
- gridTemplateColumns: `8rem repeat(${dates.length}, minmax(1.5rem, 1fr))`,
38
- }}
39
- >
40
- {mutations.map((mutation, i) => {
41
- return (
42
- <Fragment key={`fragment-${mutation.toString()}`}>
43
- <div
44
- key={`mutation-${mutation.toString()}`}
45
- style={{ gridRowStart: i + 1, gridColumnStart: 1 }}
46
- >
47
- <MutationCell mutation={mutation} />
48
- </div>
49
- {dates.map((date, j) => {
50
- const value = data.get(mutation, date) ?? { proportion: 0, count: 0 };
51
- return (
52
- <div
53
- style={{ gridRowStart: i + 1, gridColumnStart: j + 2 }}
54
- key={`${mutation.toString()}-${date.toString()}`}
55
- >
56
- <ProportionCell value={value} date={date} mutation={mutation} />
57
- </div>
58
- );
59
- })}
60
- </Fragment>
61
- );
62
- })}
63
- </div>
26
+ <>
27
+ {allMutations.length > MAX_NUMBER_OF_GRID_ROWS && (
28
+ <div className='pl-2'>
29
+ Showing {MAX_NUMBER_OF_GRID_ROWS} of {allMutations.length} mutations. You can narrow the filter to
30
+ reduce the number of mutations.
31
+ </div>
32
+ )}
33
+ <div
34
+ style={{
35
+ display: 'grid',
36
+ gridTemplateRows: `repeat(${shownMutations.length}, 24px)`,
37
+ gridTemplateColumns: `8rem repeat(${dates.length}, minmax(1.5rem, 1fr))`,
38
+ }}
39
+ >
40
+ {shownMutations.map((mutation, rowIndex) => {
41
+ return (
42
+ <Fragment key={`fragment-${mutation.toString()}`}>
43
+ <div
44
+ key={`mutation-${mutation.toString()}`}
45
+ style={{ gridRowStart: rowIndex + 1, gridColumnStart: 1 }}
46
+ >
47
+ <MutationCell mutation={mutation} />
48
+ </div>
49
+ {dates.map((date, columnIndex) => {
50
+ const value = data.get(mutation, date) ?? { proportion: 0, count: 0 };
51
+ const tooltipPosition = getTooltipPosition(
52
+ rowIndex,
53
+ shownMutations.length,
54
+ columnIndex,
55
+ dates.length,
56
+ );
57
+ return (
58
+ <div
59
+ style={{ gridRowStart: rowIndex + 1, gridColumnStart: columnIndex + 2 }}
60
+ key={`${mutation.toString()}-${date.toString()}`}
61
+ >
62
+ <ProportionCell
63
+ value={value}
64
+ date={date}
65
+ mutation={mutation}
66
+ tooltipPosition={tooltipPosition}
67
+ />
68
+ </div>
69
+ );
70
+ })}
71
+ </Fragment>
72
+ );
73
+ })}
74
+ </div>
75
+ </>
64
76
  );
65
77
  };
66
78
 
79
+ function getTooltipPosition(rowIndex: number, rows: number, columnIndex: number, columns: number) {
80
+ const tooltipX = rowIndex < rows / 2 ? 'bottom' : 'top';
81
+ const tooltipY = columnIndex < columns / 2 ? 'start' : 'end';
82
+ return `${tooltipX}-${tooltipY}` as const;
83
+ }
84
+
67
85
  const ProportionCell: FunctionComponent<{
68
86
  value: MutationOverTimeMutationValue;
69
87
  date: Temporal;
70
88
  mutation: Substitution | Deletion;
71
- }> = ({ value, mutation, date }) => {
89
+ tooltipPosition: TooltipPosition;
90
+ }> = ({ value, mutation, date, tooltipPosition }) => {
72
91
  const tooltipContent = (
73
92
  <div>
74
93
  <p>
@@ -83,7 +102,7 @@ const ProportionCell: FunctionComponent<{
83
102
  return (
84
103
  <>
85
104
  <div className={'py-1'}>
86
- <Tooltip content={tooltipContent}>
105
+ <Tooltip content={tooltipContent} position={tooltipPosition}>
87
106
  <div
88
107
  style={{
89
108
  backgroundColor: backgroundColor(value.proportion),
@@ -1,6 +1,8 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, waitFor } from '@storybook/test';
2
3
 
3
4
  import aggregated_date from './__mockData__/aggregated_date.json';
5
+ import aggregated_tooManyMutations from './__mockData__/aggregated_tooManyMutations.json';
4
6
  import nucleotideMutation_01 from './__mockData__/nucleotideMutations_2024_01.json';
5
7
  import nucleotideMutation_02 from './__mockData__/nucleotideMutations_2024_02.json';
6
8
  import nucleotideMutation_03 from './__mockData__/nucleotideMutations_2024_03.json';
@@ -8,6 +10,7 @@ import nucleotideMutation_04 from './__mockData__/nucleotideMutations_2024_04.js
8
10
  import nucleotideMutation_05 from './__mockData__/nucleotideMutations_2024_05.json';
9
11
  import nucleotideMutation_06 from './__mockData__/nucleotideMutations_2024_06.json';
10
12
  import nucleotideMutation_07 from './__mockData__/nucleotideMutations_2024_07.json';
13
+ import nucleotideMutation_tooManyMutations from './__mockData__/nucleotideMutations_tooManyMutations.json';
11
14
  import { MutationsOverTime, type MutationsOverTimeProps } from './mutations-over-time';
12
15
  import { AGGREGATED_ENDPOINT, LAPIS_URL, NUCLEOTIDE_MUTATIONS_ENDPOINT } from '../../constants';
13
16
  import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
@@ -95,7 +98,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
95
98
  pangoLineage: 'JN.1*',
96
99
  dateFrom: '2024-01-01',
97
100
  dateTo: '2024-01-31',
98
- minProportion: 0,
101
+ minProportion: 0.001,
99
102
  },
100
103
  },
101
104
  response: {
@@ -111,7 +114,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
111
114
  pangoLineage: 'JN.1*',
112
115
  dateFrom: '2024-02-01',
113
116
  dateTo: '2024-02-29',
114
- minProportion: 0,
117
+ minProportion: 0.001,
115
118
  },
116
119
  },
117
120
  response: {
@@ -127,7 +130,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
127
130
  pangoLineage: 'JN.1*',
128
131
  dateFrom: '2024-03-01',
129
132
  dateTo: '2024-03-31',
130
- minProportion: 0,
133
+ minProportion: 0.001,
131
134
  },
132
135
  response: {
133
136
  status: 200,
@@ -143,7 +146,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
143
146
  pangoLineage: 'JN.1*',
144
147
  dateFrom: '2024-04-01',
145
148
  dateTo: '2024-04-30',
146
- minProportion: 0,
149
+ minProportion: 0.001,
147
150
  },
148
151
  response: {
149
152
  status: 200,
@@ -159,7 +162,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
159
162
  pangoLineage: 'JN.1*',
160
163
  dateFrom: '2024-05-01',
161
164
  dateTo: '2024-05-31',
162
- minProportion: 0,
165
+ minProportion: 0.001,
163
166
  },
164
167
  response: {
165
168
  status: 200,
@@ -175,7 +178,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
175
178
  pangoLineage: 'JN.1*',
176
179
  dateFrom: '2024-06-01',
177
180
  dateTo: '2024-06-30',
178
- minProportion: 0,
181
+ minProportion: 0.001,
179
182
  },
180
183
  response: {
181
184
  status: 200,
@@ -183,7 +186,6 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
183
186
  },
184
187
  },
185
188
  },
186
-
187
189
  {
188
190
  matcher: {
189
191
  name: 'nucleotideMutations_07',
@@ -192,7 +194,7 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
192
194
  pangoLineage: 'JN.1*',
193
195
  dateFrom: '2024-07-01',
194
196
  dateTo: '2024-07-31',
195
- minProportion: 0,
197
+ minProportion: 0.001,
196
198
  },
197
199
  response: {
198
200
  status: 200,
@@ -204,3 +206,55 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
204
206
  },
205
207
  },
206
208
  };
209
+
210
+ export const ShowsMessageWhenTooManyMutations: StoryObj<MutationsOverTimeProps> = {
211
+ ...Template,
212
+ args: {
213
+ lapisFilter: { dateFrom: '2023-01-01', dateTo: '2023-12-31' },
214
+ sequenceType: 'nucleotide',
215
+ views: ['grid'],
216
+ width: '100%',
217
+ height: '700px',
218
+ granularity: 'year',
219
+ lapisDateField: 'date',
220
+ },
221
+ parameters: {
222
+ fetchMock: {
223
+ mocks: [
224
+ {
225
+ matcher: {
226
+ name: 'aggregated',
227
+ url: AGGREGATED_ENDPOINT,
228
+ body: {
229
+ dateFrom: '2023-01-01',
230
+ dateTo: '2023-12-31',
231
+ fields: ['date'],
232
+ },
233
+ },
234
+ response: {
235
+ status: 200,
236
+ body: aggregated_tooManyMutations,
237
+ },
238
+ },
239
+ {
240
+ matcher: {
241
+ name: 'nucleotideMutations',
242
+ url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
243
+ body: {
244
+ dateFrom: '2023-01-01',
245
+ dateTo: '2023-12-31',
246
+ minProportion: 0.001,
247
+ },
248
+ response: {
249
+ status: 200,
250
+ body: nucleotideMutation_tooManyMutations,
251
+ },
252
+ },
253
+ },
254
+ ],
255
+ },
256
+ },
257
+ play: async ({ canvas }) => {
258
+ await waitFor(() => expect(canvas.getByText('Showing 100 of 137 mutations.', { exact: false })).toBeVisible());
259
+ },
260
+ };
@@ -8,8 +8,10 @@ import {
8
8
  queryMutationsOverTimeData,
9
9
  } from '../../query/queryMutationsOverTime';
10
10
  import { type LapisFilter, type SequenceType, type TemporalGranularity } from '../../types';
11
+ import { compareTemporal } from '../../utils/temporal';
11
12
  import { LapisUrlContext } from '../LapisUrlContext';
12
13
  import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/SegmentSelector';
14
+ import { CsvDownloadButton } from '../components/csv-download-button';
13
15
  import { ErrorBoundary } from '../components/error-boundary';
14
16
  import { ErrorDisplay } from '../components/error-display';
15
17
  import Info from '../components/info';
@@ -126,6 +128,7 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
126
128
  setDisplayedMutationTypes={setDisplayedMutationTypes}
127
129
  proportionInterval={proportionInterval}
128
130
  setProportionInterval={setProportionInterval}
131
+ filteredData={filteredData}
129
132
  />
130
133
  );
131
134
 
@@ -139,6 +142,7 @@ type ToolbarProps = {
139
142
  setDisplayedMutationTypes: (types: DisplayedMutationType[]) => void;
140
143
  proportionInterval: ProportionInterval;
141
144
  setProportionInterval: Dispatch<StateUpdater<ProportionInterval>>;
145
+ filteredData: MutationOverTimeDataGroupedByMutation;
142
146
  };
143
147
 
144
148
  const Toolbar: FunctionComponent<ToolbarProps> = ({
@@ -148,6 +152,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
148
152
  setDisplayedMutationTypes,
149
153
  proportionInterval,
150
154
  setProportionInterval,
155
+ filteredData,
151
156
  }) => {
152
157
  return (
153
158
  <>
@@ -156,15 +161,34 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
156
161
  setDisplayedMutationTypes={setDisplayedMutationTypes}
157
162
  displayedMutationTypes={displayedMutationTypes}
158
163
  />
159
- <>
160
- <ProportionSelectorDropdown
161
- proportionInterval={proportionInterval}
162
- setMinProportion={(min) => setProportionInterval((prev) => ({ ...prev, min }))}
163
- setMaxProportion={(max) => setProportionInterval((prev) => ({ ...prev, max }))}
164
- />
165
- {/* TODO(#362): Add download button */}
166
- </>
164
+ <ProportionSelectorDropdown
165
+ proportionInterval={proportionInterval}
166
+ setMinProportion={(min) => setProportionInterval((prev) => ({ ...prev, min }))}
167
+ setMaxProportion={(max) => setProportionInterval((prev) => ({ ...prev, max }))}
168
+ />
169
+ <CsvDownloadButton
170
+ className='mx-1 btn btn-xs'
171
+ getData={() => getDownloadData(filteredData)}
172
+ filename='mutations_over_time.csv'
173
+ />
167
174
  <Info height={'100px'}>Info for mutations over time</Info>
168
175
  </>
169
176
  );
170
177
  };
178
+
179
+ function getDownloadData(filteredData: MutationOverTimeDataGroupedByMutation) {
180
+ const dates = filteredData.getSecondAxisKeys().sort((a, b) => compareTemporal(a, b));
181
+
182
+ return filteredData.getFirstAxisKeys().map((mutation) => {
183
+ return dates.reduce(
184
+ (accumulated, date) => {
185
+ const proportion = filteredData.get(mutation, date)?.proportion ?? 0;
186
+ return {
187
+ ...accumulated,
188
+ [date.toString()]: proportion,
189
+ };
190
+ },
191
+ { mutation: mutation.toString() },
192
+ );
193
+ });
194
+ }
@@ -57,8 +57,7 @@ describe('YearMonthDay', () => {
57
57
  expect(underTest.yearNumber).equal(2020);
58
58
  expect(underTest.monthNumber).equal(1);
59
59
  expect(underTest.dayNumber).equal(1);
60
- // seems to be a bug in dayjs: https://github.com/iamkun/dayjs/issues/2620
61
- expect(underTest.week.text).equal('2019-01');
60
+ expect(underTest.week.text).equal('2020-W01');
62
61
  expect(underTest.text).equal('2020-01-01');
63
62
  expect(underTest.firstDay.text).equal('2020-01-01');
64
63
  expect(underTest.lastDay.text).equal('2020-01-01');
@@ -67,12 +66,12 @@ describe('YearMonthDay', () => {
67
66
 
68
67
  describe('YearWeek', () => {
69
68
  it('should parse from string', () => {
70
- const underTest = YearWeek.parse('2020-02', cache);
69
+ const underTest = YearWeek.parse('2020-W02', cache);
71
70
 
72
71
  expect(underTest.isoYearNumber).equal(2020);
73
72
  expect(underTest.isoWeekNumber).equal(2);
74
73
  expect(underTest.firstDay.text).equal('2020-01-06');
75
- expect(underTest.text).equal('2020-02');
74
+ expect(underTest.text).equal('2020-W02');
76
75
  expect(underTest.lastDay.text).equal('2020-01-12');
77
76
  });
78
77
  });
@@ -7,6 +7,11 @@ import type { TemporalGranularity } from '../types';
7
7
  dayjs.extend(isoWeek);
8
8
  dayjs.extend(advancedFormat);
9
9
 
10
+ /**
11
+ * https://day.js.org/docs/en/plugin/advanced-format
12
+ */
13
+ const FORMAT_ISO_WEEK_YEAR_WEEK = 'GGGG-[W]WW';
14
+
10
15
  export class TemporalCache {
11
16
  private yearMonthDayCache = new Map<string, YearMonthDay>();
12
17
  private yearWeekCache = new Map<string, YearWeek>();
@@ -93,7 +98,7 @@ export class YearMonthDay {
93
98
  }
94
99
 
95
100
  get week(): YearWeek {
96
- return this.cache.getYearWeek(this.dayjs.format('GGGG-WW'));
101
+ return this.cache.getYearWeek(this.dayjs.format(FORMAT_ISO_WEEK_YEAR_WEEK));
97
102
  }
98
103
 
99
104
  addDays(days: number): YearMonthDay {
@@ -120,7 +125,7 @@ export class YearWeek {
120
125
  ) {}
121
126
 
122
127
  get text(): string {
123
- return this.firstDay.dayjs.format('YYYY-WW');
128
+ return this.firstDay.dayjs.format(FORMAT_ISO_WEEK_YEAR_WEEK);
124
129
  }
125
130
 
126
131
  toString(): string {
@@ -160,7 +165,7 @@ export class YearWeek {
160
165
 
161
166
  addWeeks(weeks: number): YearWeek {
162
167
  const date = this.firstDay.dayjs.add(weeks, 'week');
163
- const s = date.format('YYYY-WW');
168
+ const s = date.format(FORMAT_ISO_WEEK_YEAR_WEEK);
164
169
  return this.cache.getYearWeek(s);
165
170
  }
166
171
 
@@ -169,7 +174,7 @@ export class YearWeek {
169
174
  }
170
175
 
171
176
  static parse(s: string, cache: TemporalCache): YearWeek {
172
- const [year, week] = s.split('-').map((s) => parseInt(s, 10));
177
+ const [year, week] = s.split('-W').map((s) => parseInt(s, 10));
173
178
  return new YearWeek(year, week, cache);
174
179
  }
175
180
  }