@genspectrum/dashboard-components 0.6.6 → 0.6.8

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 (28) hide show
  1. package/custom-elements.json +16 -0
  2. package/dist/dashboard-components.js +226 -145
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/style.css +42 -6
  5. package/package.json +1 -1
  6. package/src/preact/aggregatedData/aggregate.tsx +1 -1
  7. package/src/preact/components/color-scale-selector-dropdown.stories.tsx +27 -0
  8. package/src/preact/components/color-scale-selector-dropdown.tsx +17 -0
  9. package/src/preact/components/color-scale-selector.stories.tsx +61 -0
  10. package/src/preact/components/color-scale-selector.tsx +79 -0
  11. package/src/preact/components/info.stories.tsx +37 -10
  12. package/src/preact/components/info.tsx +22 -43
  13. package/src/preact/components/min-max-range-slider.tsx +1 -1
  14. package/src/preact/components/tooltip.tsx +2 -2
  15. package/src/preact/mutationComparison/mutation-comparison.tsx +1 -1
  16. package/src/preact/mutationFilter/mutation-filter-info.tsx +1 -1
  17. package/src/preact/mutationFilter/mutation-filter.stories.tsx +1 -1
  18. package/src/preact/mutationFilter/mutation-filter.tsx +2 -3
  19. package/src/preact/mutations/mutations.tsx +1 -1
  20. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +61 -35
  21. package/src/preact/mutationsOverTime/mutations-over-time.tsx +34 -15
  22. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +1 -1
  23. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +1 -1
  24. package/src/preact/shared/charts/colors.ts +1 -1
  25. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +1 -1
  26. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +1 -1
  27. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +8 -0
  28. 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,29 @@ 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';
10
+ import { type ColorScale, getColorWithingScale, getTextColorForScale } from '../components/color-scale-selector';
9
11
  import Tooltip, { type TooltipPosition } from '../components/tooltip';
10
- import { singleGraphColorRGBByName } from '../shared/charts/colors';
12
+ import { sortSubstitutionsAndDeletions } from '../shared/sort/sortSubstitutionsAndDeletions';
11
13
  import { formatProportion } from '../shared/table/formatProportion';
12
14
 
13
15
  export interface MutationsOverTimeGridProps {
14
16
  data: MutationOverTimeDataGroupedByMutation;
17
+ colorScale: ColorScale;
15
18
  }
16
19
 
17
20
  const MAX_NUMBER_OF_GRID_ROWS = 100;
21
+ const MUTATION_CELL_WIDTH_REM = 8;
18
22
 
19
- const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({ data }) => {
20
- const allMutations = data.getFirstAxisKeys();
23
+ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({ data, colorScale }) => {
24
+ const allMutations = data.getFirstAxisKeys().sort(sortSubstitutionsAndDeletions);
21
25
  const shownMutations = allMutations.slice(0, MAX_NUMBER_OF_GRID_ROWS);
22
26
 
23
27
  const dates = data.getSecondAxisKeys().sort((a, b) => compareTemporal(a, b));
24
28
 
29
+ const [showProportionText, setShowProportionText] = useState(false);
30
+ const gridRef = useRef<HTMLDivElement>(null);
31
+ useShowProportion(gridRef, dates.length, setShowProportionText);
32
+
25
33
  return (
26
34
  <>
27
35
  {allMutations.length > MAX_NUMBER_OF_GRID_ROWS && (
@@ -31,10 +39,11 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
31
39
  </div>
32
40
  )}
33
41
  <div
42
+ ref={gridRef}
34
43
  style={{
35
44
  display: 'grid',
36
45
  gridTemplateRows: `repeat(${shownMutations.length}, 24px)`,
37
- gridTemplateColumns: `8rem repeat(${dates.length}, minmax(1.5rem, 1fr))`,
46
+ gridTemplateColumns: `${MUTATION_CELL_WIDTH_REM}rem repeat(${dates.length}, minmax(0.05rem, 1fr))`,
38
47
  }}
39
48
  >
40
49
  {shownMutations.map((mutation, rowIndex) => {
@@ -64,6 +73,8 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
64
73
  date={date}
65
74
  mutation={mutation}
66
75
  tooltipPosition={tooltipPosition}
76
+ showProportionText={showProportionText}
77
+ colorScale={colorScale}
67
78
  />
68
79
  </div>
69
80
  );
@@ -76,6 +87,33 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
76
87
  );
77
88
  };
78
89
 
90
+ function useShowProportion(
91
+ gridRef: RefObject<HTMLDivElement>,
92
+ girdColumns: number,
93
+ setShowProportionText: (value: ((prevState: boolean) => boolean) | boolean) => void,
94
+ ) {
95
+ useEffect(() => {
96
+ const checkWidth = () => {
97
+ if (gridRef.current) {
98
+ const width = gridRef.current.getBoundingClientRect().width;
99
+ const widthPerDate = (width - remToPx(MUTATION_CELL_WIDTH_REM)) / girdColumns;
100
+ const maxWidthProportionText = 28;
101
+
102
+ setShowProportionText(widthPerDate > maxWidthProportionText);
103
+ }
104
+ };
105
+
106
+ checkWidth();
107
+ window.addEventListener('resize', checkWidth);
108
+
109
+ return () => {
110
+ window.removeEventListener('resize', checkWidth);
111
+ };
112
+ }, [girdColumns, gridRef, setShowProportionText]);
113
+ }
114
+
115
+ const remToPx = (rem: number) => rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
116
+
79
117
  function getTooltipPosition(rowIndex: number, rows: number, columnIndex: number, columns: number) {
80
118
  const tooltipX = rowIndex < rows / 2 ? 'bottom' : 'top';
81
119
  const tooltipY = columnIndex < columns / 2 ? 'start' : 'end';
@@ -87,12 +125,15 @@ const ProportionCell: FunctionComponent<{
87
125
  date: Temporal;
88
126
  mutation: Substitution | Deletion;
89
127
  tooltipPosition: TooltipPosition;
90
- }> = ({ value, mutation, date, tooltipPosition }) => {
128
+ showProportionText: boolean;
129
+ colorScale: ColorScale;
130
+ }> = ({ value, mutation, date, tooltipPosition, showProportionText, colorScale }) => {
91
131
  const tooltipContent = (
92
132
  <div>
93
133
  <p>
94
- <span className='font-bold'>{date.englishName()}</span> ({timeIntervalDisplay(date)})
134
+ <span className='font-bold'>{date.englishName()}</span>
95
135
  </p>
136
+ <p>({timeIntervalDisplay(date)})</p>
96
137
  <p>{mutation.code}</p>
97
138
  <p>Proportion: {formatProportion(value.proportion)}</p>
98
139
  <p>Count: {value.count}</p>
@@ -100,21 +141,19 @@ const ProportionCell: FunctionComponent<{
100
141
  );
101
142
 
102
143
  return (
103
- <>
104
- <div className={'py-1'}>
105
- <Tooltip content={tooltipContent} position={tooltipPosition}>
106
- <div
107
- style={{
108
- backgroundColor: backgroundColor(value.proportion),
109
- color: textColor(value.proportion),
110
- }}
111
- className='text-center hover:font-bold text-xs'
112
- >
113
- {formatProportion(value.proportion, 0)}
114
- </div>
115
- </Tooltip>
116
- </div>
117
- </>
144
+ <div className={'py-1 w-full h-full'}>
145
+ <Tooltip content={tooltipContent} position={tooltipPosition}>
146
+ <div
147
+ style={{
148
+ backgroundColor: getColorWithingScale(value.proportion, colorScale),
149
+ color: getTextColorForScale(value.proportion, colorScale),
150
+ }}
151
+ className={`w-full h-full text-center hover:font-bold text-xs group`}
152
+ >
153
+ {showProportionText ? formatProportion(value.proportion, 0) : undefined}
154
+ </div>
155
+ </Tooltip>
156
+ </div>
118
157
  );
119
158
  };
120
159
 
@@ -126,19 +165,6 @@ const timeIntervalDisplay = (date: Temporal) => {
126
165
  return `${date.firstDay.toString()} - ${date.lastDay.toString()}`;
127
166
  };
128
167
 
129
- const backgroundColor = (proportion: number) => {
130
- // TODO(#353): Make minAlpha and maxAlpha configurable
131
- const minAlpha = 0.0;
132
- const maxAlpha = 1;
133
-
134
- const alpha = minAlpha + (maxAlpha - minAlpha) * proportion;
135
- return singleGraphColorRGBByName('indigo', alpha);
136
- };
137
-
138
- const textColor = (proportion: number) => {
139
- return proportion > 0.5 ? 'white' : 'black';
140
- };
141
-
142
168
  const MutationCell: FunctionComponent<{ mutation: Substitution | Deletion }> = ({ mutation }) => {
143
169
  return <div className='text-center'>{mutation.toString()}</div>;
144
170
  };
@@ -11,6 +11,8 @@ import { type LapisFilter, type SequenceType, type TemporalGranularity } from '.
11
11
  import { compareTemporal } from '../../utils/temporal';
12
12
  import { LapisUrlContext } from '../LapisUrlContext';
13
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';
14
16
  import { CsvDownloadButton } from '../components/csv-download-button';
15
17
  import { ErrorBoundary } from '../components/error-boundary';
16
18
  import { ErrorDisplay } from '../components/error-display';
@@ -22,6 +24,7 @@ import type { ProportionInterval } from '../components/proportion-selector';
22
24
  import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown';
23
25
  import { ResizeContainer } from '../components/resize-container';
24
26
  import Tabs from '../components/tabs';
27
+ import { sortSubstitutionsAndDeletions } from '../shared/sort/sortSubstitutionsAndDeletions';
25
28
  import { useQuery } from '../useQuery';
26
29
 
27
30
  export type View = 'grid';
@@ -90,6 +93,7 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
90
93
  views,
91
94
  }) => {
92
95
  const [proportionInterval, setProportionInterval] = useState({ min: 0.05, max: 0.9 });
96
+ const [colorScale, setColorScale] = useState<ColorScale>({ min: 0, max: 1, color: 'indigo' });
93
97
 
94
98
  const [displayedSegments, setDisplayedSegments] = useDisplayedSegments(sequenceType);
95
99
  const [displayedMutationTypes, setDisplayedMutationTypes] = useState<DisplayedMutationType[]>([
@@ -113,15 +117,16 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
113
117
  case 'grid':
114
118
  return {
115
119
  title: 'Grid',
116
- content: <MutationsOverTimeGrid data={filteredData} />,
120
+ content: <MutationsOverTimeGrid data={filteredData} colorScale={colorScale} />,
117
121
  };
118
122
  }
119
123
  };
120
124
 
121
125
  const tabs = views.map((view) => getTab(view));
122
126
 
123
- const toolbar = () => (
127
+ const toolbar = (activeTab: string) => (
124
128
  <Toolbar
129
+ activeTab={activeTab}
125
130
  displayedSegments={displayedSegments}
126
131
  setDisplayedSegments={setDisplayedSegments}
127
132
  displayedMutationTypes={displayedMutationTypes}
@@ -129,6 +134,8 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
129
134
  proportionInterval={proportionInterval}
130
135
  setProportionInterval={setProportionInterval}
131
136
  filteredData={filteredData}
137
+ colorScale={colorScale}
138
+ setColorScale={setColorScale}
132
139
  />
133
140
  );
134
141
 
@@ -136,6 +143,7 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
136
143
  };
137
144
 
138
145
  type ToolbarProps = {
146
+ activeTab: string;
139
147
  displayedSegments: DisplayedSegment[];
140
148
  setDisplayedSegments: (segments: DisplayedSegment[]) => void;
141
149
  displayedMutationTypes: DisplayedMutationType[];
@@ -143,9 +151,12 @@ type ToolbarProps = {
143
151
  proportionInterval: ProportionInterval;
144
152
  setProportionInterval: Dispatch<StateUpdater<ProportionInterval>>;
145
153
  filteredData: MutationOverTimeDataGroupedByMutation;
154
+ colorScale: ColorScale;
155
+ setColorScale: Dispatch<StateUpdater<ColorScale>>;
146
156
  };
147
157
 
148
158
  const Toolbar: FunctionComponent<ToolbarProps> = ({
159
+ activeTab,
149
160
  displayedSegments,
150
161
  setDisplayedSegments,
151
162
  displayedMutationTypes,
@@ -153,9 +164,14 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
153
164
  proportionInterval,
154
165
  setProportionInterval,
155
166
  filteredData,
167
+ colorScale,
168
+ setColorScale,
156
169
  }) => {
157
170
  return (
158
171
  <>
172
+ {activeTab === 'Grid' && (
173
+ <ColorScaleSelectorDropdown colorScale={colorScale} setColorScale={setColorScale} />
174
+ )}
159
175
  <SegmentSelector displayedSegments={displayedSegments} setDisplayedSegments={setDisplayedSegments} />
160
176
  <MutationTypeSelector
161
177
  setDisplayedMutationTypes={setDisplayedMutationTypes}
@@ -171,7 +187,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
171
187
  getData={() => getDownloadData(filteredData)}
172
188
  filename='mutations_over_time.csv'
173
189
  />
174
- <Info height={'100px'}>Info for mutations over time</Info>
190
+ <Info>Info for mutations over time</Info>
175
191
  </>
176
192
  );
177
193
  };
@@ -179,16 +195,19 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
179
195
  function getDownloadData(filteredData: MutationOverTimeDataGroupedByMutation) {
180
196
  const dates = filteredData.getSecondAxisKeys().sort((a, b) => compareTemporal(a, b));
181
197
 
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
- });
198
+ return filteredData
199
+ .getFirstAxisKeys()
200
+ .sort(sortSubstitutionsAndDeletions)
201
+ .map((mutation) => {
202
+ return dates.reduce(
203
+ (accumulated, date) => {
204
+ const proportion = filteredData.get(mutation, date)?.proportion ?? 0;
205
+ return {
206
+ ...accumulated,
207
+ [date.toString()]: proportion,
208
+ };
209
+ },
210
+ { mutation: mutation.toString() },
211
+ );
212
+ });
194
213
  }
@@ -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[];
@@ -32,7 +32,7 @@ describe('sortSubstitutionsAndDeletions with no segments', () => {
32
32
  describe('sortSubstitutionsAndDeletions with segments', () => {
33
33
  test('should sort for segment first', () => {
34
34
  const a = new Substitution('AA1', 'A', 'B', 123);
35
- const b = new Substitution('BB1', 'A', 'B', 234);
35
+ const b = new Substitution('BB1', 'A', 'B', 123);
36
36
 
37
37
  expect(sortSubstitutionsAndDeletions(a, b)).toBeLessThan(0);
38
38
  expect(sortSubstitutionsAndDeletions(b, a)).toBeGreaterThan(0);
@@ -2,7 +2,7 @@ import { Deletion, type Substitution } from '../../../utils/mutations';
2
2
 
3
3
  export const sortSubstitutionsAndDeletions = (a: Substitution | Deletion, b: Substitution | Deletion) => {
4
4
  if (a.segment !== b.segment) {
5
- compareSegments(a.segment, b.segment);
5
+ return compareSegments(a.segment, b.segment);
6
6
  }
7
7
 
8
8
  if (a.position !== b.position) {
@@ -475,3 +475,11 @@ export const AminoAcidMutationsByDay: StoryObj<Required<MutationsOverTimeProps>>
475
475
  },
476
476
  },
477
477
  };
478
+
479
+ export const HideProportionOnSmallScreen: StoryObj<Required<MutationsOverTimeProps>> = {
480
+ ...ByMonth,
481
+ args: {
482
+ ...ByMonth.args,
483
+ width: '300px',
484
+ },
485
+ };
@@ -1,17 +0,0 @@
1
- import type { FunctionComponent } from 'preact';
2
-
3
- export const DeleteIcon: FunctionComponent = () => {
4
- return (
5
- <svg
6
- fill='currentColor'
7
- stroke-width='0'
8
- xmlns='http://www.w3.org/2000/svg'
9
- viewBox='0 0 16 16'
10
- style='overflow: visible; color: currentcolor;'
11
- height='1em'
12
- width='1em'
13
- >
14
- <path d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z' />
15
- </svg>
16
- );
17
- };