@genspectrum/dashboard-components 1.8.1 → 1.9.0

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 (26) hide show
  1. package/custom-elements.json +2 -2
  2. package/dist/assets/{mutationOverTimeWorker-BRPqAM5Z.js.map → mutationOverTimeWorker-dhufsWQ2.js.map} +1 -1
  3. package/dist/components.d.ts +20 -20
  4. package/dist/components.js +118 -31
  5. package/dist/components.js.map +1 -1
  6. package/dist/util.d.ts +20 -20
  7. package/package.json +1 -1
  8. package/src/preact/components/downshift-combobox.tsx +2 -1
  9. package/src/preact/components/portal-tooltip.tsx +129 -0
  10. package/src/preact/components/tooltip.tsx +32 -16
  11. package/src/preact/lineageFilter/lineage-filter.stories.tsx +57 -1
  12. package/src/preact/lineageFilter/lineage-filter.tsx +1 -1
  13. package/src/preact/locationFilter/location-filter.tsx +3 -1
  14. package/src/preact/mutationsOverTime/__mockData__/withGaps.ts +0 -54
  15. package/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.tsx +1 -1
  16. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +19 -9
  17. package/src/preact/mutationsOverTime/mutations-over-time.tsx +13 -4
  18. package/src/preact/textFilter/text-filter.tsx +3 -1
  19. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +8 -4
  20. package/src/query/queryMutationsOverTime.ts +28 -11
  21. package/src/query/queryMutationsOverTimeNewEndpoint.spec.ts +6 -6
  22. package/src/web-components/input/gs-lineage-filter.stories.ts +1 -1
  23. package/src/web-components/input/gs-text-filter.stories.ts +1 -1
  24. package/standalone-bundle/assets/{mutationOverTimeWorker-DtFX4Ihx.js.map → mutationOverTimeWorker-CGqPKySO.js.map} +1 -1
  25. package/standalone-bundle/dashboard-components.js +4663 -4594
  26. package/standalone-bundle/dashboard-components.js.map +1 -1
package/dist/util.d.ts CHANGED
@@ -1005,7 +1005,7 @@ declare global {
1005
1005
 
1006
1006
  declare global {
1007
1007
  interface HTMLElementTagNameMap {
1008
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
1008
+ 'gs-aggregate': AggregateComponent;
1009
1009
  }
1010
1010
  }
1011
1011
 
@@ -1013,7 +1013,7 @@ declare global {
1013
1013
  declare global {
1014
1014
  namespace JSX {
1015
1015
  interface IntrinsicElements {
1016
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1016
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1017
1017
  }
1018
1018
  }
1019
1019
  }
@@ -1021,7 +1021,7 @@ declare global {
1021
1021
 
1022
1022
  declare global {
1023
1023
  interface HTMLElementTagNameMap {
1024
- 'gs-aggregate': AggregateComponent;
1024
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1025
1025
  }
1026
1026
  }
1027
1027
 
@@ -1029,7 +1029,7 @@ declare global {
1029
1029
  declare global {
1030
1030
  namespace JSX {
1031
1031
  interface IntrinsicElements {
1032
- 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1032
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1033
1033
  }
1034
1034
  }
1035
1035
  }
@@ -1037,7 +1037,7 @@ declare global {
1037
1037
 
1038
1038
  declare global {
1039
1039
  interface HTMLElementTagNameMap {
1040
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1040
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
1041
1041
  }
1042
1042
  }
1043
1043
 
@@ -1045,7 +1045,7 @@ declare global {
1045
1045
  declare global {
1046
1046
  namespace JSX {
1047
1047
  interface IntrinsicElements {
1048
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1048
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1049
1049
  }
1050
1050
  }
1051
1051
  }
@@ -1053,7 +1053,7 @@ declare global {
1053
1053
 
1054
1054
  declare global {
1055
1055
  interface HTMLElementTagNameMap {
1056
- 'gs-mutations-over-time': MutationsOverTimeComponent;
1056
+ 'gs-sequences-by-location': SequencesByLocationComponent;
1057
1057
  }
1058
1058
  }
1059
1059
 
@@ -1061,7 +1061,7 @@ declare global {
1061
1061
  declare global {
1062
1062
  namespace JSX {
1063
1063
  interface IntrinsicElements {
1064
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1064
+ 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1065
1065
  }
1066
1066
  }
1067
1067
  }
@@ -1069,7 +1069,7 @@ declare global {
1069
1069
 
1070
1070
  declare global {
1071
1071
  interface HTMLElementTagNameMap {
1072
- 'gs-statistics': StatisticsComponent;
1072
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
1073
1073
  }
1074
1074
  }
1075
1075
 
@@ -1077,7 +1077,7 @@ declare global {
1077
1077
  declare global {
1078
1078
  namespace JSX {
1079
1079
  interface IntrinsicElements {
1080
- 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1080
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1081
1081
  }
1082
1082
  }
1083
1083
  }
@@ -1085,7 +1085,7 @@ declare global {
1085
1085
 
1086
1086
  declare global {
1087
1087
  interface HTMLElementTagNameMap {
1088
- 'gs-sequences-by-location': SequencesByLocationComponent;
1088
+ 'gs-statistics': StatisticsComponent;
1089
1089
  }
1090
1090
  }
1091
1091
 
@@ -1093,7 +1093,7 @@ declare global {
1093
1093
  declare global {
1094
1094
  namespace JSX {
1095
1095
  interface IntrinsicElements {
1096
- 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1096
+ 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1097
1097
  }
1098
1098
  }
1099
1099
  }
@@ -1101,7 +1101,11 @@ declare global {
1101
1101
 
1102
1102
  declare global {
1103
1103
  interface HTMLElementTagNameMap {
1104
- 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1104
+ 'gs-date-range-filter': DateRangeFilterComponent;
1105
+ }
1106
+ interface HTMLElementEventMap {
1107
+ [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
1108
+ [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
1105
1109
  }
1106
1110
  }
1107
1111
 
@@ -1109,7 +1113,7 @@ declare global {
1109
1113
  declare global {
1110
1114
  namespace JSX {
1111
1115
  interface IntrinsicElements {
1112
- 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1116
+ 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1113
1117
  }
1114
1118
  }
1115
1119
  }
@@ -1117,11 +1121,7 @@ declare global {
1117
1121
 
1118
1122
  declare global {
1119
1123
  interface HTMLElementTagNameMap {
1120
- 'gs-date-range-filter': DateRangeFilterComponent;
1121
- }
1122
- interface HTMLElementEventMap {
1123
- [gsEventNames.dateRangeFilterChanged]: CustomEvent<Record<string, string>>;
1124
- [gsEventNames.dateRangeOptionChanged]: DateRangeOptionChangedEvent;
1124
+ 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1125
1125
  }
1126
1126
  }
1127
1127
 
@@ -1129,7 +1129,7 @@ declare global {
1129
1129
  declare global {
1130
1130
  namespace JSX {
1131
1131
  interface IntrinsicElements {
1132
- 'gs-date-range-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1132
+ 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1133
1133
  }
1134
1134
  }
1135
1135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -64,6 +64,7 @@ export function DownshiftCombobox<Item>({
64
64
  getItemProps,
65
65
  inputValue,
66
66
  closeMenu,
67
+ reset,
67
68
  } = useCombobox({
68
69
  onInputValueChange({ inputValue }) {
69
70
  setInputIsInvalid(false);
@@ -97,7 +98,7 @@ export function DownshiftCombobox<Item>({
97
98
  };
98
99
 
99
100
  const clearInput = () => {
100
- selectItem(null);
101
+ reset();
101
102
  };
102
103
 
103
104
  const buttonRef = useRef(null);
@@ -0,0 +1,129 @@
1
+ import { type FunctionComponent } from 'preact';
2
+ import { type CSSProperties, createPortal } from 'preact/compat';
3
+ import { useState, useRef, useLayoutEffect } from 'preact/hooks';
4
+ import { type JSXInternal } from 'preact/src/jsx';
5
+
6
+ import { type TooltipPosition, TOOLTIP_BASE_STYLES } from './tooltip';
7
+
8
+ export type PortalTooltipProps = {
9
+ content: string | JSXInternal.Element;
10
+ position?: TooltipPosition;
11
+ tooltipStyle?: CSSProperties;
12
+ portalTarget: HTMLElement | null;
13
+ };
14
+
15
+ /**
16
+ * A portal-based tooltip component that renders content in a specified DOM element.
17
+ *
18
+ * Unlike the regular `Tooltip` component, this uses Preact portals to render the tooltip
19
+ * at a specific location in the DOM with fixed positioning. This is useful when:
20
+ * - The tooltip needs to escape overflow constraints from parent containers
21
+ * - You need precise control over the tooltip's rendering location
22
+ * - Parent containers have `overflow: hidden` or other clipping styles
23
+ *
24
+ * **Important:** The `portalTarget` element should still be within the same shadow DOM as the
25
+ * component to ensure proper styling and encapsulation. Typically, this is a container element
26
+ * at the root of your component. Do not use `document.body`.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * const portalTarget = document.getElementById('tooltip-root');
31
+ *
32
+ * <PortalTooltip
33
+ * content="This is a portal tooltip"
34
+ * position="top"
35
+ * portalTarget={portalTarget}
36
+ * >
37
+ * <button>Hover me</button>
38
+ * </PortalTooltip>
39
+ * ```
40
+ */
41
+ const PortalTooltip: FunctionComponent<PortalTooltipProps> = ({
42
+ children,
43
+ content,
44
+ position = 'bottom',
45
+ tooltipStyle,
46
+ portalTarget,
47
+ }) => {
48
+ const [isHovered, setIsHovered] = useState(false);
49
+ const [tooltipPosition, setTooltipPosition] = useState({ top: 0, left: 0 });
50
+ const triggerRef = useRef<HTMLDivElement>(null);
51
+ const tooltipRef = useRef<HTMLDivElement>(null);
52
+
53
+ useLayoutEffect(() => {
54
+ if (isHovered && triggerRef.current !== null && tooltipRef.current !== null) {
55
+ const triggerRect = triggerRef.current.getBoundingClientRect();
56
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
57
+ const newPosition = calculateTooltipPosition(triggerRect, tooltipRect, position);
58
+ setTooltipPosition(newPosition);
59
+ }
60
+ }, [isHovered, position]);
61
+
62
+ const tooltipContent = (
63
+ <div
64
+ ref={tooltipRef}
65
+ className={`fixed ${TOOLTIP_BASE_STYLES} ${isHovered ? 'visible' : 'invisible'}`}
66
+ style={Object.assign({}, tooltipStyle, { top: tooltipPosition.top, left: tooltipPosition.left })}
67
+ >
68
+ {content}
69
+ </div>
70
+ );
71
+
72
+ return (
73
+ <>
74
+ <div ref={triggerRef} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
75
+ {children}
76
+ </div>
77
+ {portalTarget !== null && createPortal(tooltipContent, portalTarget)}
78
+ </>
79
+ );
80
+ };
81
+
82
+ export default PortalTooltip;
83
+
84
+ function calculateTooltipPosition(
85
+ triggerRect: DOMRect,
86
+ tooltipRect: DOMRect,
87
+ position: TooltipPosition,
88
+ ): { top: number; left: number } {
89
+ const gap = 4;
90
+ let top;
91
+ let left;
92
+
93
+ switch (position) {
94
+ case 'top':
95
+ top = triggerRect.top - tooltipRect.height - gap;
96
+ left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
97
+ break;
98
+ case 'top-start':
99
+ top = triggerRect.top - tooltipRect.height - gap;
100
+ left = triggerRect.left;
101
+ break;
102
+ case 'top-end':
103
+ top = triggerRect.top - tooltipRect.height - gap;
104
+ left = triggerRect.right - tooltipRect.width;
105
+ break;
106
+ case 'bottom':
107
+ top = triggerRect.bottom + gap;
108
+ left = triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
109
+ break;
110
+ case 'bottom-start':
111
+ top = triggerRect.bottom + gap;
112
+ left = triggerRect.left;
113
+ break;
114
+ case 'bottom-end':
115
+ top = triggerRect.bottom + gap;
116
+ left = triggerRect.right - tooltipRect.width;
117
+ break;
118
+ case 'left':
119
+ top = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
120
+ left = triggerRect.left - tooltipRect.width - gap;
121
+ break;
122
+ case 'right':
123
+ top = triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
124
+ left = triggerRect.right + gap;
125
+ break;
126
+ }
127
+
128
+ return { top, left };
129
+ }
@@ -18,6 +18,38 @@ export type TooltipProps = {
18
18
  tooltipStyle?: CSSProperties;
19
19
  };
20
20
 
21
+ export const TOOLTIP_BASE_STYLES = 'z-10 w-max bg-white p-4 border border-gray-200 rounded-md';
22
+
23
+ /**
24
+ * A simple CSS-based tooltip component that displays content on hover.
25
+ *
26
+ * **Note:** If you need the tooltip to escape overflow constraints or render at a specific
27
+ * location in the DOM (e.g., to avoid clipping by parent containers with `overflow: hidden`),
28
+ * use `PortalTooltip` instead.
29
+ *
30
+ * @example
31
+ * ```tsx
32
+ * <Tooltip content="This is a tooltip" position="top">
33
+ * <button>Hover me</button>
34
+ * </Tooltip>
35
+ * ```
36
+ */
37
+ const Tooltip: FunctionComponent<TooltipProps> = ({ children, content, position = 'bottom', tooltipStyle }) => {
38
+ return (
39
+ <div className={`relative group`}>
40
+ <div>{children}</div>
41
+ <div
42
+ className={`absolute ${TOOLTIP_BASE_STYLES} invisible group-hover:visible ${getPositionCss(position)}`}
43
+ style={tooltipStyle}
44
+ >
45
+ {content}
46
+ </div>
47
+ </div>
48
+ );
49
+ };
50
+
51
+ export default Tooltip;
52
+
21
53
  function getPositionCss(position?: TooltipPosition) {
22
54
  switch (position) {
23
55
  case 'top':
@@ -40,19 +72,3 @@ function getPositionCss(position?: TooltipPosition) {
40
72
  return '';
41
73
  }
42
74
  }
43
-
44
- const Tooltip: FunctionComponent<TooltipProps> = ({ children, content, position = 'bottom', tooltipStyle }) => {
45
- return (
46
- <div className={`relative group`}>
47
- <div>{children}</div>
48
- <div
49
- className={`absolute z-10 w-max bg-white p-4 border border-gray-200 rounded-md invisible group-hover:visible ${getPositionCss(position)}`}
50
- style={tooltipStyle}
51
- >
52
- {content}
53
- </div>
54
- </div>
55
- );
56
- };
57
-
58
- export default Tooltip;
@@ -108,7 +108,7 @@ export const Default: StoryObj<LineageFilterProps> = {
108
108
  const input = await inputField(canvas);
109
109
  await userEvent.clear(input);
110
110
  await userEvent.type(input, 'B.1');
111
- await userEvent.click(canvas.getByRole('option', { name: 'B.1(53802)' }));
111
+ await userEvent.click(canvas.getByRole('option', { name: 'B.1(53,802)' }));
112
112
 
113
113
  await waitFor(() => {
114
114
  return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
@@ -187,6 +187,62 @@ export const WithHideCountsTrue: StoryObj<LineageFilterProps> = {
187
187
  },
188
188
  };
189
189
 
190
+ export const EnterAndClearMultipleTimes: StoryObj<LineageFilterProps> = {
191
+ ...Default,
192
+ play: async ({ canvasElement, step }) => {
193
+ const { canvas, lineageChangedListenerMock } = await prepare(canvasElement, step);
194
+ const input = await inputField(canvas);
195
+ const inputContainer = canvas.getByRole('combobox').parentElement;
196
+ const clearSelectionButton = await canvas.findByLabelText('clear selection');
197
+
198
+ await step('clear field initially', async () => {
199
+ await userEvent.clear(input);
200
+ });
201
+
202
+ await step('enter F in the input field', async () => {
203
+ await userEvent.type(input, 'F');
204
+ });
205
+
206
+ await step('clear selection using clear button', async () => {
207
+ await userEvent.click(clearSelectionButton);
208
+
209
+ await waitFor(() => {
210
+ return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
211
+ pangoLineage: undefined,
212
+ });
213
+ });
214
+ });
215
+
216
+ await step('verify input field is empty after clearing', async () => {
217
+ await expect(input).toHaveValue('');
218
+ });
219
+
220
+ await step('verify no red border after clearing', async () => {
221
+ await expect(inputContainer).not.toHaveClass('input-error');
222
+ });
223
+
224
+ // do it again
225
+
226
+ await step('enter F in the input field again', async () => {
227
+ await userEvent.type(input, 'F');
228
+ });
229
+
230
+ await step('clear selection using clear button again', async () => {
231
+ await userEvent.click(clearSelectionButton);
232
+
233
+ await waitFor(() => {
234
+ return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
235
+ pangoLineage: undefined,
236
+ });
237
+ });
238
+ });
239
+
240
+ await step('verify input field is empty after clearing again', async () => {
241
+ await expect(input).toHaveValue('');
242
+ });
243
+ },
244
+ };
245
+
190
246
  async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRenderer, unknown>) {
191
247
  const canvas = within(canvasElement);
192
248
 
@@ -99,7 +99,7 @@ const LineageSelector = ({
99
99
  formatItemInList={(item: LineageItem) => (
100
100
  <p>
101
101
  <span>{item.lineage}</span>
102
- {!hideCounts && <span className='ml-2 text-gray-500'>({item.count})</span>}
102
+ {!hideCounts && <span className='ml-2 text-gray-500'>({item.count.toLocaleString('en-US')})</span>}
103
103
  </p>
104
104
  )}
105
105
  />
@@ -112,7 +112,9 @@ const LocationSelector = ({
112
112
  <>
113
113
  <p>
114
114
  <span>{item.label}</span>
115
- {!hideCounts && <span className='ml-2 text-gray-500'>({item.count})</span>}
115
+ {!hideCounts && (
116
+ <span className='ml-2 text-gray-500'>({item.count.toLocaleString('en-US')})</span>
117
+ )}
116
118
  </p>
117
119
  <span className='text-sm text-gray-500'>{item.description}</span>
118
120
  </>
@@ -80,15 +80,6 @@ export const withGaps: MutationOverTimeMockData = {
80
80
  totalCount: 8236,
81
81
  },
82
82
  ],
83
- [
84
- '2024-04',
85
- {
86
- type: 'value',
87
- count: 0,
88
- proportion: NaN,
89
- totalCount: 0,
90
- },
91
- ],
92
83
  [
93
84
  '2024-05',
94
85
  {
@@ -98,15 +89,6 @@ export const withGaps: MutationOverTimeMockData = {
98
89
  totalCount: 8799,
99
90
  },
100
91
  ],
101
- [
102
- '2024-06',
103
- {
104
- type: 'value',
105
- count: 0,
106
- proportion: NaN,
107
- totalCount: 0,
108
- },
109
- ],
110
92
  [
111
93
  '2024-07',
112
94
  {
@@ -148,15 +130,6 @@ export const withGaps: MutationOverTimeMockData = {
148
130
  totalCount: 8236,
149
131
  },
150
132
  ],
151
- [
152
- '2024-04',
153
- {
154
- type: 'value',
155
- count: 0,
156
- proportion: NaN,
157
- totalCount: 0,
158
- },
159
- ],
160
133
  [
161
134
  '2024-05',
162
135
  {
@@ -166,15 +139,6 @@ export const withGaps: MutationOverTimeMockData = {
166
139
  totalCount: 8799,
167
140
  },
168
141
  ],
169
- [
170
- '2024-06',
171
- {
172
- type: 'value',
173
- count: 0,
174
- proportion: NaN,
175
- totalCount: 0,
176
- },
177
- ],
178
142
  [
179
143
  '2024-07',
180
144
  {
@@ -212,15 +176,6 @@ export const withGaps: MutationOverTimeMockData = {
212
176
  totalCount: 8236,
213
177
  },
214
178
  ],
215
- [
216
- '2024-04',
217
- {
218
- type: 'value',
219
- count: 0,
220
- proportion: NaN,
221
- totalCount: 0,
222
- },
223
- ],
224
179
  [
225
180
  '2024-05',
226
181
  {
@@ -230,15 +185,6 @@ export const withGaps: MutationOverTimeMockData = {
230
185
  totalCount: 8799,
231
186
  },
232
187
  ],
233
- [
234
- '2024-06',
235
- {
236
- type: 'value',
237
- count: 0,
238
- proportion: NaN,
239
- totalCount: 0,
240
- },
241
- ],
242
188
  [
243
189
  '2024-07',
244
190
  {
@@ -22,7 +22,7 @@ export const MutationsOverTimeGridTooltip: FunctionComponent<MutationsOverTimeGr
22
22
  const dateClass = toTemporalClass(date);
23
23
 
24
24
  return (
25
- <div>
25
+ <div className='text-center'>
26
26
  <p>
27
27
  <span className='font-bold'>{dateClass.englishName()}</span>
28
28
  </p>
@@ -9,7 +9,8 @@ import { type Deletion, type Substitution } from '../../utils/mutations';
9
9
  import { type Temporal } from '../../utils/temporalClass';
10
10
  import { AnnotatedMutation } from '../components/annotated-mutation';
11
11
  import { type ColorScale, getColorWithinScale, getTextColorForScale } from '../components/color-scale-selector';
12
- import Tooltip, { type TooltipPosition } from '../components/tooltip';
12
+ import PortalTooltip from '../components/portal-tooltip';
13
+ import { type TooltipPosition } from '../components/tooltip';
13
14
  import { formatProportion } from '../shared/table/formatProportion';
14
15
  import { type PageSizes, Pagination } from '../shared/tanstackTable/pagination';
15
16
  import { usePageSizeContext } from '../shared/tanstackTable/pagination-context';
@@ -21,11 +22,14 @@ import {
21
22
  usePreactTable,
22
23
  } from '../shared/tanstackTable/tanstackTable';
23
24
 
25
+ const NON_BREAKING_SPACE = '\u00A0';
26
+
24
27
  export interface MutationsOverTimeGridProps {
25
28
  data: MutationOverTimeDataMap;
26
29
  colorScale: ColorScale;
27
30
  sequenceType: SequenceType;
28
31
  pageSizes: PageSizes;
32
+ tooltipPortalTarget: HTMLElement | null;
29
33
  }
30
34
 
31
35
  type RowType = { mutation: Substitution | Deletion; values: (MutationOverTimeMutationValue | undefined)[] };
@@ -35,6 +39,7 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
35
39
  colorScale,
36
40
  sequenceType,
37
41
  pageSizes,
42
+ tooltipPortalTarget,
38
43
  }) => {
39
44
  const tableData = useMemo(() => {
40
45
  const allMutations = data.getFirstAxisKeys();
@@ -89,6 +94,7 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
89
94
  numberOfColumns,
90
95
  )}
91
96
  colorScale={colorScale}
97
+ tooltipPortalTarget={tooltipPortalTarget}
92
98
  />
93
99
  </div>
94
100
  );
@@ -97,7 +103,7 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
97
103
  });
98
104
 
99
105
  return [mutationHeader, ...dateHeaders];
100
- }, [colorScale, data, sequenceType]);
106
+ }, [colorScale, data, sequenceType, tooltipPortalTarget]);
101
107
 
102
108
  const { pageSize } = usePageSizeContext();
103
109
  const table = usePreactTable({
@@ -163,10 +169,10 @@ function styleGridHeader(columnIndex: number, numDateColumns: number) {
163
169
  return { className: 'invisible @[6rem]:visible' };
164
170
  }
165
171
 
166
- function getTooltipPosition(rowIndex: number, rows: number, columnIndex: number, columns: number) {
172
+ function getTooltipPosition(rowIndex: number, rows: number, columnIndex: number, columns: number): TooltipPosition {
167
173
  const tooltipX = rowIndex < rows / 2 || rowIndex < 6 ? 'bottom' : 'top';
168
174
  const tooltipY = columnIndex < columns / 2 ? 'start' : 'end';
169
- return `${tooltipX}-${tooltipY}` as const;
175
+ return `${tooltipX}-${tooltipY}`;
170
176
  }
171
177
 
172
178
  const ProportionCell: FunctionComponent<{
@@ -175,14 +181,16 @@ const ProportionCell: FunctionComponent<{
175
181
  mutation: Substitution | Deletion;
176
182
  tooltipPosition: TooltipPosition;
177
183
  colorScale: ColorScale;
178
- }> = ({ value, mutation, date, tooltipPosition, colorScale }) => {
179
- const proportion = value?.type === 'belowThreshold' ? 0 : value?.proportion;
184
+ tooltipPortalTarget: HTMLElement | null;
185
+ }> = ({ value, mutation, date, tooltipPosition, colorScale, tooltipPortalTarget }) => {
186
+ const proportion = value?.type === 'belowThreshold' ? undefined : value?.proportion;
180
187
 
181
188
  return (
182
189
  <div className={'py-1 w-full h-full'}>
183
- <Tooltip
190
+ <PortalTooltip
184
191
  content={<MutationsOverTimeGridTooltip mutation={mutation} date={date} value={value} />}
185
192
  position={tooltipPosition}
193
+ portalTarget={tooltipPortalTarget}
186
194
  >
187
195
  <div
188
196
  style={{
@@ -194,10 +202,12 @@ const ProportionCell: FunctionComponent<{
194
202
  {value === null ? (
195
203
  <span className='invisible'>No data</span>
196
204
  ) : (
197
- <span className='invisible @[2rem]:visible'>{formatProportion(proportion ?? 0, 0)}</span>
205
+ <span className='invisible @[2rem]:visible'>
206
+ {proportion !== undefined ? formatProportion(proportion, 0) : NON_BREAKING_SPACE}
207
+ </span>
198
208
  )}
199
209
  </div>
200
- </Tooltip>
210
+ </PortalTooltip>
201
211
  </div>
202
212
  );
203
213
  };