@genspectrum/dashboard-components 0.6.5 → 0.6.7

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 (44) hide show
  1. package/custom-elements.json +51 -3
  2. package/dist/dashboard-components.js +359 -159
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +3 -3
  5. package/dist/style.css +92 -8
  6. package/package.json +1 -1
  7. package/src/constants.ts +1 -0
  8. package/src/preact/aggregatedData/aggregate.tsx +1 -1
  9. package/src/preact/components/color-scale-selector-dropdown.stories.tsx +27 -0
  10. package/src/preact/components/color-scale-selector-dropdown.tsx +17 -0
  11. package/src/preact/components/color-scale-selector.stories.tsx +61 -0
  12. package/src/preact/components/color-scale-selector.tsx +79 -0
  13. package/src/preact/components/info.stories.tsx +37 -10
  14. package/src/preact/components/info.tsx +22 -43
  15. package/src/preact/components/min-max-range-slider.tsx +1 -1
  16. package/src/preact/components/tooltip.stories.tsx +12 -2
  17. package/src/preact/components/tooltip.tsx +38 -14
  18. package/src/preact/mutationComparison/mutation-comparison.tsx +1 -1
  19. package/src/preact/mutationFilter/mutation-filter-info.tsx +1 -1
  20. package/src/preact/mutationFilter/mutation-filter.stories.tsx +1 -1
  21. package/src/preact/mutationFilter/mutation-filter.tsx +2 -3
  22. package/src/preact/mutations/mutations.tsx +1 -1
  23. package/src/preact/mutationsOverTime/__mockData__/aggregated_byDay.json +38 -0
  24. package/src/preact/mutationsOverTime/__mockData__/aggregated_byWeek.json +122 -0
  25. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_20_01_2024.json +6778 -0
  26. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_21_01_2024.json +7129 -0
  27. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_22_01_2024.json +4681 -0
  28. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_23_01_2024.json +10738 -0
  29. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_24_01_2024.json +11710 -0
  30. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_25_01_2024.json +11557 -0
  31. package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_26_01_2024.json +8596 -0
  32. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_week3_2024.json +8812 -0
  33. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_week4_2024.json +9730 -0
  34. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_week5_2024.json +9865 -0
  35. package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_week6_2024.json +11314 -0
  36. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +83 -40
  37. package/src/preact/mutationsOverTime/mutations-over-time.tsx +50 -11
  38. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +1 -1
  39. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +1 -1
  40. package/src/preact/shared/charts/colors.ts +1 -1
  41. package/src/utils/temporal.spec.ts +3 -4
  42. package/src/utils/temporal.ts +9 -4
  43. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +262 -2
  44. package/src/preact/shared/icons/DeleteIcon.tsx +0 -17
@@ -1,4 +1,5 @@
1
- import { Fragment, type FunctionComponent } from 'preact';
1
+ import { Fragment, type FunctionComponent, type RefObject } from 'preact';
2
+ import { useEffect, useRef, useState } from 'preact/hooks';
2
3
 
3
4
  import {
4
5
  type MutationOverTimeDataGroupedByMutation,
@@ -6,22 +7,28 @@ import {
6
7
  } from '../../query/queryMutationsOverTime';
7
8
  import { type Deletion, type Substitution } from '../../utils/mutations';
8
9
  import { compareTemporal, type Temporal, YearMonthDay } from '../../utils/temporal';
9
- import Tooltip from '../components/tooltip';
10
- import { singleGraphColorRGBByName } from '../shared/charts/colors';
10
+ import { type ColorScale, getColorWithingScale, getTextColorForScale } from '../components/color-scale-selector';
11
+ import Tooltip, { type TooltipPosition } from '../components/tooltip';
11
12
  import { formatProportion } from '../shared/table/formatProportion';
12
13
 
13
14
  export interface MutationsOverTimeGridProps {
14
15
  data: MutationOverTimeDataGroupedByMutation;
16
+ colorScale: ColorScale;
15
17
  }
16
18
 
17
19
  const MAX_NUMBER_OF_GRID_ROWS = 100;
20
+ const MUTATION_CELL_WIDTH_REM = 8;
18
21
 
19
- const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({ data }) => {
22
+ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({ data, colorScale }) => {
20
23
  const allMutations = data.getFirstAxisKeys();
21
24
  const shownMutations = allMutations.slice(0, MAX_NUMBER_OF_GRID_ROWS);
22
25
 
23
26
  const dates = data.getSecondAxisKeys().sort((a, b) => compareTemporal(a, b));
24
27
 
28
+ const [showProportionText, setShowProportionText] = useState(false);
29
+ const gridRef = useRef<HTMLDivElement>(null);
30
+ useShowProportion(gridRef, dates.length, setShowProportionText);
31
+
25
32
  return (
26
33
  <>
27
34
  {allMutations.length > MAX_NUMBER_OF_GRID_ROWS && (
@@ -31,29 +38,43 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
31
38
  </div>
32
39
  )}
33
40
  <div
41
+ ref={gridRef}
34
42
  style={{
35
43
  display: 'grid',
36
44
  gridTemplateRows: `repeat(${shownMutations.length}, 24px)`,
37
- gridTemplateColumns: `8rem repeat(${dates.length}, minmax(1.5rem, 1fr))`,
45
+ gridTemplateColumns: `${MUTATION_CELL_WIDTH_REM}rem repeat(${dates.length}, minmax(0.05rem, 1fr))`,
38
46
  }}
39
47
  >
40
- {shownMutations.map((mutation, i) => {
48
+ {shownMutations.map((mutation, rowIndex) => {
41
49
  return (
42
50
  <Fragment key={`fragment-${mutation.toString()}`}>
43
51
  <div
44
52
  key={`mutation-${mutation.toString()}`}
45
- style={{ gridRowStart: i + 1, gridColumnStart: 1 }}
53
+ style={{ gridRowStart: rowIndex + 1, gridColumnStart: 1 }}
46
54
  >
47
55
  <MutationCell mutation={mutation} />
48
56
  </div>
49
- {dates.map((date, j) => {
57
+ {dates.map((date, columnIndex) => {
50
58
  const value = data.get(mutation, date) ?? { proportion: 0, count: 0 };
59
+ const tooltipPosition = getTooltipPosition(
60
+ rowIndex,
61
+ shownMutations.length,
62
+ columnIndex,
63
+ dates.length,
64
+ );
51
65
  return (
52
66
  <div
53
- style={{ gridRowStart: i + 1, gridColumnStart: j + 2 }}
67
+ style={{ gridRowStart: rowIndex + 1, gridColumnStart: columnIndex + 2 }}
54
68
  key={`${mutation.toString()}-${date.toString()}`}
55
69
  >
56
- <ProportionCell value={value} date={date} mutation={mutation} />
70
+ <ProportionCell
71
+ value={value}
72
+ date={date}
73
+ mutation={mutation}
74
+ tooltipPosition={tooltipPosition}
75
+ showProportionText={showProportionText}
76
+ colorScale={colorScale}
77
+ />
57
78
  </div>
58
79
  );
59
80
  })}
@@ -65,16 +86,53 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
65
86
  );
66
87
  };
67
88
 
89
+ function useShowProportion(
90
+ gridRef: RefObject<HTMLDivElement>,
91
+ girdColumns: number,
92
+ setShowProportionText: (value: ((prevState: boolean) => boolean) | boolean) => void,
93
+ ) {
94
+ useEffect(() => {
95
+ const checkWidth = () => {
96
+ if (gridRef.current) {
97
+ const width = gridRef.current.getBoundingClientRect().width;
98
+ const widthPerDate = (width - remToPx(MUTATION_CELL_WIDTH_REM)) / girdColumns;
99
+ const maxWidthProportionText = 28;
100
+
101
+ setShowProportionText(widthPerDate > maxWidthProportionText);
102
+ }
103
+ };
104
+
105
+ checkWidth();
106
+ window.addEventListener('resize', checkWidth);
107
+
108
+ return () => {
109
+ window.removeEventListener('resize', checkWidth);
110
+ };
111
+ }, [girdColumns, gridRef, setShowProportionText]);
112
+ }
113
+
114
+ const remToPx = (rem: number) => rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
115
+
116
+ function getTooltipPosition(rowIndex: number, rows: number, columnIndex: number, columns: number) {
117
+ const tooltipX = rowIndex < rows / 2 ? 'bottom' : 'top';
118
+ const tooltipY = columnIndex < columns / 2 ? 'start' : 'end';
119
+ return `${tooltipX}-${tooltipY}` as const;
120
+ }
121
+
68
122
  const ProportionCell: FunctionComponent<{
69
123
  value: MutationOverTimeMutationValue;
70
124
  date: Temporal;
71
125
  mutation: Substitution | Deletion;
72
- }> = ({ value, mutation, date }) => {
126
+ tooltipPosition: TooltipPosition;
127
+ showProportionText: boolean;
128
+ colorScale: ColorScale;
129
+ }> = ({ value, mutation, date, tooltipPosition, showProportionText, colorScale }) => {
73
130
  const tooltipContent = (
74
131
  <div>
75
132
  <p>
76
- <span className='font-bold'>{date.englishName()}</span> ({timeIntervalDisplay(date)})
133
+ <span className='font-bold'>{date.englishName()}</span>
77
134
  </p>
135
+ <p>({timeIntervalDisplay(date)})</p>
78
136
  <p>{mutation.code}</p>
79
137
  <p>Proportion: {formatProportion(value.proportion)}</p>
80
138
  <p>Count: {value.count}</p>
@@ -82,21 +140,19 @@ const ProportionCell: FunctionComponent<{
82
140
  );
83
141
 
84
142
  return (
85
- <>
86
- <div className={'py-1'}>
87
- <Tooltip content={tooltipContent}>
88
- <div
89
- style={{
90
- backgroundColor: backgroundColor(value.proportion),
91
- color: textColor(value.proportion),
92
- }}
93
- className='text-center hover:font-bold text-xs'
94
- >
95
- {formatProportion(value.proportion, 0)}
96
- </div>
97
- </Tooltip>
98
- </div>
99
- </>
143
+ <div className={'py-1 w-full h-full'}>
144
+ <Tooltip content={tooltipContent} position={tooltipPosition}>
145
+ <div
146
+ style={{
147
+ backgroundColor: getColorWithingScale(value.proportion, colorScale),
148
+ color: getTextColorForScale(value.proportion, colorScale),
149
+ }}
150
+ className={`w-full h-full text-center hover:font-bold text-xs group`}
151
+ >
152
+ {showProportionText ? formatProportion(value.proportion, 0) : undefined}
153
+ </div>
154
+ </Tooltip>
155
+ </div>
100
156
  );
101
157
  };
102
158
 
@@ -108,19 +164,6 @@ const timeIntervalDisplay = (date: Temporal) => {
108
164
  return `${date.firstDay.toString()} - ${date.lastDay.toString()}`;
109
165
  };
110
166
 
111
- const backgroundColor = (proportion: number) => {
112
- // TODO(#353): Make minAlpha and maxAlpha configurable
113
- const minAlpha = 0.0;
114
- const maxAlpha = 1;
115
-
116
- const alpha = minAlpha + (maxAlpha - minAlpha) * proportion;
117
- return singleGraphColorRGBByName('indigo', alpha);
118
- };
119
-
120
- const textColor = (proportion: number) => {
121
- return proportion > 0.5 ? 'white' : 'black';
122
- };
123
-
124
167
  const MutationCell: FunctionComponent<{ mutation: Substitution | Deletion }> = ({ mutation }) => {
125
168
  return <div className='text-center'>{mutation.toString()}</div>;
126
169
  };
@@ -8,8 +8,12 @@ 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 { type ColorScale } from '../components/color-scale-selector';
15
+ import { ColorScaleSelectorDropdown } from '../components/color-scale-selector-dropdown';
16
+ import { CsvDownloadButton } from '../components/csv-download-button';
13
17
  import { ErrorBoundary } from '../components/error-boundary';
14
18
  import { ErrorDisplay } from '../components/error-display';
15
19
  import Info from '../components/info';
@@ -88,6 +92,7 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
88
92
  views,
89
93
  }) => {
90
94
  const [proportionInterval, setProportionInterval] = useState({ min: 0.05, max: 0.9 });
95
+ const [colorScale, setColorScale] = useState<ColorScale>({ min: 0, max: 1, color: 'indigo' });
91
96
 
92
97
  const [displayedSegments, setDisplayedSegments] = useDisplayedSegments(sequenceType);
93
98
  const [displayedMutationTypes, setDisplayedMutationTypes] = useState<DisplayedMutationType[]>([
@@ -111,21 +116,25 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
111
116
  case 'grid':
112
117
  return {
113
118
  title: 'Grid',
114
- content: <MutationsOverTimeGrid data={filteredData} />,
119
+ content: <MutationsOverTimeGrid data={filteredData} colorScale={colorScale} />,
115
120
  };
116
121
  }
117
122
  };
118
123
 
119
124
  const tabs = views.map((view) => getTab(view));
120
125
 
121
- const toolbar = () => (
126
+ const toolbar = (activeTab: string) => (
122
127
  <Toolbar
128
+ activeTab={activeTab}
123
129
  displayedSegments={displayedSegments}
124
130
  setDisplayedSegments={setDisplayedSegments}
125
131
  displayedMutationTypes={displayedMutationTypes}
126
132
  setDisplayedMutationTypes={setDisplayedMutationTypes}
127
133
  proportionInterval={proportionInterval}
128
134
  setProportionInterval={setProportionInterval}
135
+ filteredData={filteredData}
136
+ colorScale={colorScale}
137
+ setColorScale={setColorScale}
129
138
  />
130
139
  );
131
140
 
@@ -133,38 +142,68 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
133
142
  };
134
143
 
135
144
  type ToolbarProps = {
145
+ activeTab: string;
136
146
  displayedSegments: DisplayedSegment[];
137
147
  setDisplayedSegments: (segments: DisplayedSegment[]) => void;
138
148
  displayedMutationTypes: DisplayedMutationType[];
139
149
  setDisplayedMutationTypes: (types: DisplayedMutationType[]) => void;
140
150
  proportionInterval: ProportionInterval;
141
151
  setProportionInterval: Dispatch<StateUpdater<ProportionInterval>>;
152
+ filteredData: MutationOverTimeDataGroupedByMutation;
153
+ colorScale: ColorScale;
154
+ setColorScale: Dispatch<StateUpdater<ColorScale>>;
142
155
  };
143
156
 
144
157
  const Toolbar: FunctionComponent<ToolbarProps> = ({
158
+ activeTab,
145
159
  displayedSegments,
146
160
  setDisplayedSegments,
147
161
  displayedMutationTypes,
148
162
  setDisplayedMutationTypes,
149
163
  proportionInterval,
150
164
  setProportionInterval,
165
+ filteredData,
166
+ colorScale,
167
+ setColorScale,
151
168
  }) => {
152
169
  return (
153
170
  <>
171
+ {activeTab === 'Grid' && (
172
+ <ColorScaleSelectorDropdown colorScale={colorScale} setColorScale={setColorScale} />
173
+ )}
154
174
  <SegmentSelector displayedSegments={displayedSegments} setDisplayedSegments={setDisplayedSegments} />
155
175
  <MutationTypeSelector
156
176
  setDisplayedMutationTypes={setDisplayedMutationTypes}
157
177
  displayedMutationTypes={displayedMutationTypes}
158
178
  />
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
- </>
167
- <Info height={'100px'}>Info for mutations over time</Info>
179
+ <ProportionSelectorDropdown
180
+ proportionInterval={proportionInterval}
181
+ setMinProportion={(min) => setProportionInterval((prev) => ({ ...prev, min }))}
182
+ setMaxProportion={(max) => setProportionInterval((prev) => ({ ...prev, max }))}
183
+ />
184
+ <CsvDownloadButton
185
+ className='mx-1 btn btn-xs'
186
+ getData={() => getDownloadData(filteredData)}
187
+ filename='mutations_over_time.csv'
188
+ />
189
+ <Info>Info for mutations over time</Info>
168
190
  </>
169
191
  );
170
192
  };
193
+
194
+ function getDownloadData(filteredData: MutationOverTimeDataGroupedByMutation) {
195
+ const dates = filteredData.getSecondAxisKeys().sort((a, b) => compareTemporal(a, b));
196
+
197
+ return filteredData.getFirstAxisKeys().map((mutation) => {
198
+ return dates.reduce(
199
+ (accumulated, date) => {
200
+ const proportion = filteredData.get(mutation, date)?.proportion ?? 0;
201
+ return {
202
+ ...accumulated,
203
+ [date.toString()]: proportion,
204
+ };
205
+ },
206
+ { mutation: mutation.toString() },
207
+ );
208
+ });
209
+ }
@@ -156,7 +156,7 @@ const Toolbar = ({ activeTab, data, granularity, yAxisScaleType, setYAxisScaleTy
156
156
  };
157
157
 
158
158
  const NumberSequencesOverTimeInfo = () => (
159
- <Info height='100px'>
159
+ <Info>
160
160
  <InfoHeadline1>Number of sequences over time</InfoHeadline1>
161
161
  <InfoParagraph>
162
162
  <a href='https://github.com/GenSpectrum/dashboard-components/issues/315'>TODO</a>
@@ -236,7 +236,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
236
236
 
237
237
  const PrevalenceOverTimeInfo: FunctionComponent = () => {
238
238
  return (
239
- <Info height={'100px'}>
239
+ <Info>
240
240
  <InfoHeadline1>Prevalence over time</InfoHeadline1>
241
241
  <InfoParagraph>Prevalence over time info.</InfoParagraph>
242
242
  </Info>
@@ -12,7 +12,7 @@ const ColorsRGB = {
12
12
  purple: [170, 68, 153],
13
13
  };
14
14
 
15
- type GraphColor = keyof typeof ColorsRGB;
15
+ export type GraphColor = keyof typeof ColorsRGB;
16
16
 
17
17
  export const singleGraphColorRGBAById = (id: number, alpha = 1) => {
18
18
  const keys = Object.keys(ColorsRGB) as GraphColor[];
@@ -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
  }