@genspectrum/dashboard-components 1.14.2 → 1.15.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.
package/dist/util.d.ts CHANGED
@@ -753,19 +753,32 @@ declare const queriesOverTimeSchema: default_2.ZodObject<{
753
753
  nucleotideInsertions?: string[] | undefined;
754
754
  aminoAcidInsertions?: string[] | undefined;
755
755
  }>>;
756
- queries: default_2.ZodArray<default_2.ZodObject<{
756
+ queries: default_2.ZodEffects<default_2.ZodArray<default_2.ZodObject<{
757
757
  displayLabel: default_2.ZodString;
758
+ description: default_2.ZodOptional<default_2.ZodString>;
758
759
  countQuery: default_2.ZodString;
759
760
  coverageQuery: default_2.ZodString;
760
761
  }, "strip", default_2.ZodTypeAny, {
761
762
  displayLabel: string;
762
763
  countQuery: string;
763
764
  coverageQuery: string;
765
+ description?: string | undefined;
764
766
  }, {
765
767
  displayLabel: string;
766
768
  countQuery: string;
767
769
  coverageQuery: string;
768
- }>, "many">;
770
+ description?: string | undefined;
771
+ }>, "many">, {
772
+ displayLabel: string;
773
+ countQuery: string;
774
+ coverageQuery: string;
775
+ description?: string | undefined;
776
+ }[], {
777
+ displayLabel: string;
778
+ countQuery: string;
779
+ coverageQuery: string;
780
+ description?: string | undefined;
781
+ }[]>;
769
782
  views: default_2.ZodArray<default_2.ZodLiteral<"grid">, "many">;
770
783
  granularity: default_2.ZodUnion<[default_2.ZodLiteral<"day">, default_2.ZodLiteral<"week">, default_2.ZodLiteral<"month">, default_2.ZodLiteral<"year">]>;
771
784
  lapisDateField: default_2.ZodString;
@@ -804,6 +817,7 @@ declare const queriesOverTimeSchema: default_2.ZodObject<{
804
817
  displayLabel: string;
805
818
  countQuery: string;
806
819
  coverageQuery: string;
820
+ description?: string | undefined;
807
821
  }[];
808
822
  width: string;
809
823
  views: "grid"[];
@@ -831,6 +845,7 @@ declare const queriesOverTimeSchema: default_2.ZodObject<{
831
845
  displayLabel: string;
832
846
  countQuery: string;
833
847
  coverageQuery: string;
848
+ description?: string | undefined;
834
849
  }[];
835
850
  width: string;
836
851
  views: "grid"[];
@@ -856,17 +871,17 @@ declare const queriesOverTimeViewSchema: default_2.ZodLiteral<"grid">;
856
871
  export declare type QueryDefinition = default_2.infer<typeof queryDefinition>;
857
872
 
858
873
  declare const queryDefinition: default_2.ZodObject<{
859
- displayLabel: default_2.ZodString;
874
+ displayLabel: default_2.ZodOptional<default_2.ZodString>;
860
875
  countQuery: default_2.ZodString;
861
876
  coverageQuery: default_2.ZodString;
862
877
  }, "strip", default_2.ZodTypeAny, {
863
- displayLabel: string;
864
878
  countQuery: string;
865
879
  coverageQuery: string;
880
+ displayLabel?: string | undefined;
866
881
  }, {
867
- displayLabel: string;
868
882
  countQuery: string;
869
883
  coverageQuery: string;
884
+ displayLabel?: string | undefined;
870
885
  }>;
871
886
 
872
887
  export declare type RelativeGrowthAdvantageProps = default_2.infer<typeof relativeGrowthAdvantagePropsSchema>;
@@ -1154,7 +1169,7 @@ declare global {
1154
1169
 
1155
1170
  declare global {
1156
1171
  interface HTMLElementTagNameMap {
1157
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
1172
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
1158
1173
  }
1159
1174
  }
1160
1175
 
@@ -1162,7 +1177,7 @@ declare global {
1162
1177
  declare global {
1163
1178
  namespace JSX {
1164
1179
  interface IntrinsicElements {
1165
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1180
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1166
1181
  }
1167
1182
  }
1168
1183
  }
@@ -1170,7 +1185,7 @@ declare global {
1170
1185
 
1171
1186
  declare global {
1172
1187
  interface HTMLElementTagNameMap {
1173
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
1188
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
1174
1189
  }
1175
1190
  }
1176
1191
 
@@ -1178,7 +1193,7 @@ declare global {
1178
1193
  declare global {
1179
1194
  namespace JSX {
1180
1195
  interface IntrinsicElements {
1181
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1196
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1182
1197
  }
1183
1198
  }
1184
1199
  }
@@ -1218,7 +1233,7 @@ declare global {
1218
1233
 
1219
1234
  declare global {
1220
1235
  interface HTMLElementTagNameMap {
1221
- 'gs-mutations-over-time': MutationsOverTimeComponent;
1236
+ 'gs-queries-over-time': QueriesOverTimeComponent;
1222
1237
  }
1223
1238
  }
1224
1239
 
@@ -1226,7 +1241,7 @@ declare global {
1226
1241
  declare global {
1227
1242
  namespace JSX {
1228
1243
  interface IntrinsicElements {
1229
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1244
+ 'gs-queries-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1230
1245
  }
1231
1246
  }
1232
1247
  }
@@ -1234,7 +1249,7 @@ declare global {
1234
1249
 
1235
1250
  declare global {
1236
1251
  interface HTMLElementTagNameMap {
1237
- 'gs-queries-over-time': QueriesOverTimeComponent;
1252
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
1238
1253
  }
1239
1254
  }
1240
1255
 
@@ -1242,7 +1257,7 @@ declare global {
1242
1257
  declare global {
1243
1258
  namespace JSX {
1244
1259
  interface IntrinsicElements {
1245
- 'gs-queries-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1260
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1246
1261
  }
1247
1262
  }
1248
1263
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "1.14.2",
3
+ "version": "1.15.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -68,7 +68,7 @@ export const mutationsOverTimeResponse = makeLapisResponse(
68
68
  export type MutationsOverTimeResponse = z.infer<typeof mutationsOverTimeResponse>;
69
69
 
70
70
  const queryDefinition = z.object({
71
- displayLabel: z.string(),
71
+ displayLabel: z.string().optional(),
72
72
  countQuery: z.string(),
73
73
  coverageQuery: z.string(),
74
74
  });
@@ -0,0 +1,60 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, within } from '@storybook/test';
3
+
4
+ import {
5
+ QueriesOverTimeRowLabelTooltip,
6
+ type QueriesOverTimeRowLabelTooltipProps,
7
+ } from './queries-over-time-row-label-tooltip';
8
+
9
+ const meta: Meta<QueriesOverTimeRowLabelTooltipProps> = {
10
+ title: 'Component/Queries over time row label tooltip',
11
+ component: QueriesOverTimeRowLabelTooltip,
12
+ argTypes: {
13
+ query: { control: 'object' },
14
+ },
15
+ parameters: {
16
+ fetchMock: {},
17
+ },
18
+ };
19
+
20
+ export default meta;
21
+
22
+ export const Default: StoryObj<QueriesOverTimeRowLabelTooltipProps> = {
23
+ render: (args) => <QueriesOverTimeRowLabelTooltip {...args} />,
24
+ args: {
25
+ query: {
26
+ displayLabel: 'S:F456L (single mutation)',
27
+ description: 'This mutation is associated with increased transmissibility.',
28
+ countQuery: 'S:456L',
29
+ coverageQuery: '!S:456N',
30
+ },
31
+ },
32
+ play: async ({ canvasElement }) => {
33
+ const canvas = within(canvasElement);
34
+ await expect(canvas.getByText('S:F456L (single mutation)', { exact: true })).toBeVisible();
35
+ await expect(canvas.getByText('This mutation is associated with increased transmissibility.')).toBeVisible();
36
+ await expect(canvas.getByText('Count query:')).toBeVisible();
37
+ await expect(canvas.getByText('S:456L')).toBeVisible();
38
+ await expect(canvas.getByText('Coverage query:')).toBeVisible();
39
+ await expect(canvas.getByText('!S:456N')).toBeVisible();
40
+ },
41
+ };
42
+
43
+ export const WithoutDescription: StoryObj<QueriesOverTimeRowLabelTooltipProps> = {
44
+ render: (args) => <QueriesOverTimeRowLabelTooltip {...args} />,
45
+ args: {
46
+ query: {
47
+ displayLabel: 'S:R346T',
48
+ countQuery: 'S:346T',
49
+ coverageQuery: '!S:346N',
50
+ },
51
+ },
52
+ play: async ({ canvasElement }) => {
53
+ const canvas = within(canvasElement);
54
+ await expect(canvas.getByText('S:R346T', { exact: true })).toBeVisible();
55
+ await expect(canvas.getByText('Count query:')).toBeVisible();
56
+ await expect(canvas.getByText('S:346T')).toBeVisible();
57
+ await expect(canvas.getByText('Coverage query:')).toBeVisible();
58
+ await expect(canvas.getByText('!S:346N')).toBeVisible();
59
+ },
60
+ };
@@ -0,0 +1,34 @@
1
+ import type { FunctionComponent } from 'preact';
2
+
3
+ import type { CountCoverageQuery } from './queries-over-time';
4
+
5
+ export type QueriesOverTimeRowLabelTooltipProps = {
6
+ query: CountCoverageQuery;
7
+ };
8
+
9
+ export const QueriesOverTimeRowLabelTooltip: FunctionComponent<QueriesOverTimeRowLabelTooltipProps> = ({ query }) => {
10
+ return (
11
+ <div className='flex flex-col gap-2'>
12
+ <div className='font-bold'>{query.displayLabel}</div>
13
+ {query.description && <div className='text-sm text-gray-700'>{query.description}</div>}
14
+ <div className='flex flex-col gap-1'>
15
+ <div className='text-sm'>
16
+ <span className='text-gray-600'>Count query:</span>
17
+ <div className='p-2 border border-gray-200 rounded bg-gray-50 overflow-x-auto'>
18
+ <pre className='text-xs'>
19
+ <code>{query.countQuery}</code>
20
+ </pre>
21
+ </div>
22
+ </div>
23
+ <div className='text-sm'>
24
+ <span className='text-gray-600'>Coverage query:</span>
25
+ <div className='p-2 border border-gray-200 rounded bg-gray-50 overflow-x-auto'>
26
+ <pre className='text-xs'>
27
+ <code>{query.coverageQuery}</code>
28
+ </pre>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ );
34
+ };
@@ -466,6 +466,30 @@ export const WithNoLapisDateFieldField: StoryObj<QueriesOverTimeProps> = {
466
466
  },
467
467
  };
468
468
 
469
+ export const WithDuplicateDisplayLabels: StoryObj<QueriesOverTimeProps> = {
470
+ ...Default,
471
+ args: {
472
+ ...Default.args,
473
+ queries: [
474
+ {
475
+ displayLabel: 'S:F456L (single mutation)',
476
+ countQuery: 'S:456L',
477
+ coverageQuery: '!S:456N',
478
+ },
479
+ {
480
+ displayLabel: 'S:F456L (single mutation)',
481
+ countQuery: 'S:346T & S:456L',
482
+ coverageQuery: '!S:346N & !S:456N',
483
+ },
484
+ ],
485
+ },
486
+ play: async ({ canvasElement, step }) => {
487
+ await step('expect error message', async () => {
488
+ await expectInvalidAttributesErrorMessage(canvasElement, 'Display labels must be unique');
489
+ });
490
+ },
491
+ };
492
+
469
493
  async function expectQueryOnPage(canvas: Canvas, query: string) {
470
494
  await waitFor(async () => {
471
495
  const queryOnPage = canvas.getAllByText(query)[0];
@@ -5,6 +5,7 @@ import z from 'zod';
5
5
  import { getFilteredQueryOverTimeData, type QueryFilter } from './getFilteredQueriesOverTimeData';
6
6
  import { QueriesOverTimeFilter } from './queries-over-time-filter';
7
7
  import { QueriesOverTimeGridTooltip } from './queries-over-time-grid-tooltip';
8
+ import { QueriesOverTimeRowLabelTooltip } from './queries-over-time-row-label-tooltip';
8
9
  import { type ProportionValue, getProportion } from '../../query/queryMutationsOverTime';
9
10
  import { queryQueriesOverTimeData } from '../../query/queryQueriesOverTime';
10
11
  import { lapisFilterSchema, temporalGranularitySchema, views } from '../../types';
@@ -20,6 +21,7 @@ import { Fullscreen } from '../components/fullscreen';
20
21
  import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../components/info';
21
22
  import { LoadingDisplay } from '../components/loading-display';
22
23
  import { NoDataDisplay } from '../components/no-data-display';
24
+ import PortalTooltip from '../components/portal-tooltip';
23
25
  import type { ProportionInterval } from '../components/proportion-selector';
24
26
  import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown';
25
27
  import { ResizeContainer } from '../components/resize-container';
@@ -37,17 +39,28 @@ const meanProportionIntervalSchema = z.object({
37
39
  });
38
40
  export type MeanProportionInterval = z.infer<typeof meanProportionIntervalSchema>;
39
41
 
42
+ const countCoverageQuerySchema = z.object({
43
+ displayLabel: z.string(),
44
+ description: z.string().optional(),
45
+ countQuery: z.string(),
46
+ coverageQuery: z.string(),
47
+ });
48
+ export type CountCoverageQuery = z.infer<typeof countCoverageQuerySchema>;
49
+
40
50
  const queriesOverTimeSchema = z.object({
41
51
  lapisFilter: lapisFilterSchema,
42
52
  queries: z
43
- .array(
44
- z.object({
45
- displayLabel: z.string(),
46
- countQuery: z.string(),
47
- coverageQuery: z.string(),
48
- }),
49
- )
50
- .min(1),
53
+ .array(countCoverageQuerySchema)
54
+ .min(1)
55
+ .superRefine((queries, ctx) => {
56
+ const duplicateDisplayLabels = findDuplicateStrings(queries.map((v) => v.displayLabel));
57
+ if (duplicateDisplayLabels.length > 0) {
58
+ ctx.addIssue({
59
+ code: z.ZodIssueCode.custom,
60
+ message: `Display labels must be unique. Duplicates: ${duplicateDisplayLabels.join(', ')}`,
61
+ });
62
+ }
63
+ }),
51
64
  views: z.array(queriesOverTimeViewSchema),
52
65
  granularity: temporalGranularitySchema,
53
66
  lapisDateField: z.string().min(1),
@@ -136,19 +149,45 @@ const QueriesOverTimeTabs: FunctionComponent<QueriesOverTimeTabsProps> = ({
136
149
  });
137
150
  }, [queryOverTimeData, proportionInterval, hideGaps, queryFilterValue]);
138
151
 
152
+ const queryLookupMap = useMemo(
153
+ () => new Map(originalComponentProps.queries.map((query) => [query.displayLabel, query])),
154
+ [originalComponentProps.queries],
155
+ );
156
+
139
157
  const queryRenderer = useMemo<FeatureRenderer<string>>(
140
158
  () => ({
141
159
  asString: (value: string) => value,
142
- renderRowLabel: (value: string) => (
143
- <div className='text-center'>
144
- <span>{value}</span>
145
- </div>
146
- ),
160
+ renderRowLabel: (value: string) => {
161
+ const queryObject = queryLookupMap.get(value);
162
+
163
+ return (
164
+ <PortalTooltip
165
+ content={
166
+ <QueriesOverTimeRowLabelTooltip
167
+ query={
168
+ queryObject ?? {
169
+ displayLabel: value,
170
+ description: undefined,
171
+ countQuery: '',
172
+ coverageQuery: '',
173
+ }
174
+ }
175
+ />
176
+ }
177
+ position='right'
178
+ portalTarget={tooltipPortalTarget}
179
+ >
180
+ <div className='text-center'>
181
+ <span>{value}</span>
182
+ </div>
183
+ </PortalTooltip>
184
+ );
185
+ },
147
186
  renderTooltip: (value: string, temporal: Temporal, proportionValue: ProportionValue) => (
148
187
  <QueriesOverTimeGridTooltip query={value} date={temporal} value={proportionValue} />
149
188
  ),
150
189
  }),
151
- [],
190
+ [tooltipPortalTarget, queryLookupMap],
152
191
  );
153
192
 
154
193
  const getTab = (view: QueriesOverTimeView) => {
@@ -302,3 +341,13 @@ function getDownloadData(filteredData: ReturnType<typeof getFilteredQueryOverTim
302
341
  );
303
342
  });
304
343
  }
344
+
345
+ function findDuplicateStrings(items: string[]): string[] {
346
+ const counts = new Map<string, number>();
347
+
348
+ for (const item of items) {
349
+ counts.set(item, (counts.get(item) ?? 0) + 1);
350
+ }
351
+
352
+ return [...counts.entries()].filter(([, count]) => count > 1).map(([key]) => key);
353
+ }
@@ -15,11 +15,13 @@ const codeExample = String.raw`
15
15
  queries='[
16
16
  {
17
17
  "displayLabel": "S:F456L (single mutation)",
18
+ "description": "This mutation is associated with increased transmissibility.",
18
19
  "countQuery": "S:456L",
19
20
  "coverageQuery": "!S:456N"
20
21
  },
21
22
  {
22
23
  "displayLabel": "R346T + F456L (combination)",
24
+ "description": "Common mutation combination found in the JN.1 lineage.",
23
25
  "countQuery": "S:346T & S:456L",
24
26
  "coverageQuery": "!S:346N & !S:456N"
25
27
  }
@@ -59,11 +61,13 @@ const meta: Meta<Required<QueriesOverTimeProps>> = {
59
61
  queries: [
60
62
  {
61
63
  displayLabel: 'S:F456L (single mutation)',
64
+ description: 'This mutation is associated with increased transmissibility.',
62
65
  countQuery: 'S:456L',
63
66
  coverageQuery: '!S:456N',
64
67
  },
65
68
  {
66
69
  displayLabel: 'R346T + F456L (combination)',
70
+ description: 'Common mutation combination found in the JN.1 lineage.',
67
71
  countQuery: 'S:346T & S:456L',
68
72
  coverageQuery: '!S:346N & !S:456N',
69
73
  },
@@ -42,7 +42,8 @@ export class QueriesOverTimeComponent extends PreactLitAdapterWithGridJsStyles {
42
42
  * Required.
43
43
  *
44
44
  * Array of queries to display. Each query has:
45
- * - displayLabel: string - The name to show in the grid row label
45
+ * - displayLabel: string - The name to show in the grid row label. Must be unique.
46
+ * - description: string (optional) - Optional description shown in tooltip
46
47
  * - countQuery: string - Query string to count matches
47
48
  * - coverageQuery: string - Query string to determine coverage/denominator
48
49
  *
@@ -51,6 +52,7 @@ export class QueriesOverTimeComponent extends PreactLitAdapterWithGridJsStyles {
51
52
  @property({ type: Array })
52
53
  queries: {
53
54
  displayLabel: string;
55
+ description?: string;
54
56
  countQuery: string;
55
57
  coverageQuery: string;
56
58
  }[] = [];