@genspectrum/dashboard-components 1.10.3 → 1.11.1

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 (23) hide show
  1. package/custom-elements.json +38 -3
  2. package/dist/assets/{mutationOverTimeWorker-dhufsWQ2.js.map → mutationOverTimeWorker-CQQFRoK4.js.map} +1 -1
  3. package/dist/components.d.ts +40 -32
  4. package/dist/components.js +144 -66
  5. package/dist/components.js.map +1 -1
  6. package/dist/util.d.ts +45 -32
  7. package/package.json +1 -1
  8. package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +1 -1
  9. package/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.stories.tsx +62 -10
  10. package/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.tsx +93 -51
  11. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +36 -8
  12. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +27 -0
  13. package/src/preact/mutationsOverTime/mutations-over-time.tsx +5 -3
  14. package/src/query/queryMutationsOverTime.ts +21 -2
  15. package/src/query/queryMutationsOverTimeNewEndpoint.spec.ts +23 -23
  16. package/src/utilEntrypoint.ts +1 -0
  17. package/src/web-components/input/gs-lineage-filter.stories.ts +9 -1
  18. package/src/web-components/visualization/gs-mutations-over-time.spec-d.ts +3 -0
  19. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +21 -0
  20. package/src/web-components/visualization/gs-mutations-over-time.tsx +8 -0
  21. package/standalone-bundle/assets/{mutationOverTimeWorker-CGqPKySO.js.map → mutationOverTimeWorker-DIpJukJC.js.map} +1 -1
  22. package/standalone-bundle/dashboard-components.js +2953 -2880
  23. package/standalone-bundle/dashboard-components.js.map +1 -1
package/dist/util.d.ts CHANGED
@@ -71,6 +71,19 @@ export declare type ConfidenceIntervalMethod = default_2.infer<typeof confidence
71
71
 
72
72
  declare const confidenceIntervalMethodSchema: default_2.ZodUnion<[default_2.ZodLiteral<"wilson">, default_2.ZodLiteral<"none">]>;
73
73
 
74
+ export declare type CustomColumn = default_2.infer<typeof customColumnSchema>;
75
+
76
+ declare const customColumnSchema: default_2.ZodObject<{
77
+ header: default_2.ZodString;
78
+ values: default_2.ZodRecord<default_2.ZodString, default_2.ZodUnion<[default_2.ZodString, default_2.ZodNumber]>>;
79
+ }, "strip", default_2.ZodTypeAny, {
80
+ values: Record<string, string | number>;
81
+ header: string;
82
+ }, {
83
+ values: Record<string, string | number>;
84
+ header: string;
85
+ }>;
86
+
74
87
  export declare type DateRangeOption = default_2.infer<typeof dateRangeOptionSchema>;
75
88
 
76
89
  export declare class DateRangeOptionChangedEvent extends CustomEvent<DateRangeValue> {
@@ -942,7 +955,7 @@ declare global {
942
955
 
943
956
  declare global {
944
957
  interface HTMLElementTagNameMap {
945
- 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
958
+ 'gs-genome-data-viewer': GenomeDataViewerComponent;
946
959
  }
947
960
  }
948
961
 
@@ -950,7 +963,7 @@ declare global {
950
963
  declare global {
951
964
  namespace JSX {
952
965
  interface IntrinsicElements {
953
- 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
966
+ 'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
954
967
  }
955
968
  }
956
969
  }
@@ -974,7 +987,7 @@ declare global {
974
987
 
975
988
  declare global {
976
989
  interface HTMLElementTagNameMap {
977
- 'gs-genome-data-viewer': GenomeDataViewerComponent;
990
+ 'gs-mutations': MutationsComponent;
978
991
  }
979
992
  }
980
993
 
@@ -982,7 +995,7 @@ declare global {
982
995
  declare global {
983
996
  namespace JSX {
984
997
  interface IntrinsicElements {
985
- 'gs-genome-data-viewer': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
998
+ 'gs-mutations': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
986
999
  }
987
1000
  }
988
1001
  }
@@ -990,7 +1003,7 @@ declare global {
990
1003
 
991
1004
  declare global {
992
1005
  interface HTMLElementTagNameMap {
993
- 'gs-mutations': MutationsComponent;
1006
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
994
1007
  }
995
1008
  }
996
1009
 
@@ -998,7 +1011,7 @@ declare global {
998
1011
  declare global {
999
1012
  namespace JSX {
1000
1013
  interface IntrinsicElements {
1001
- 'gs-mutations': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1014
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1002
1015
  }
1003
1016
  }
1004
1017
  }
@@ -1006,7 +1019,7 @@ declare global {
1006
1019
 
1007
1020
  declare global {
1008
1021
  interface HTMLElementTagNameMap {
1009
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
1022
+ 'gs-aggregate': AggregateComponent;
1010
1023
  }
1011
1024
  }
1012
1025
 
@@ -1014,7 +1027,7 @@ declare global {
1014
1027
  declare global {
1015
1028
  namespace JSX {
1016
1029
  interface IntrinsicElements {
1017
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1030
+ 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1018
1031
  }
1019
1032
  }
1020
1033
  }
@@ -1022,7 +1035,7 @@ declare global {
1022
1035
 
1023
1036
  declare global {
1024
1037
  interface HTMLElementTagNameMap {
1025
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
1038
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1026
1039
  }
1027
1040
  }
1028
1041
 
@@ -1030,7 +1043,7 @@ declare global {
1030
1043
  declare global {
1031
1044
  namespace JSX {
1032
1045
  interface IntrinsicElements {
1033
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1046
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1034
1047
  }
1035
1048
  }
1036
1049
  }
@@ -1038,7 +1051,7 @@ declare global {
1038
1051
 
1039
1052
  declare global {
1040
1053
  interface HTMLElementTagNameMap {
1041
- 'gs-aggregate': AggregateComponent;
1054
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
1042
1055
  }
1043
1056
  }
1044
1057
 
@@ -1046,7 +1059,7 @@ declare global {
1046
1059
  declare global {
1047
1060
  namespace JSX {
1048
1061
  interface IntrinsicElements {
1049
- 'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1062
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1050
1063
  }
1051
1064
  }
1052
1065
  }
@@ -1054,7 +1067,7 @@ declare global {
1054
1067
 
1055
1068
  declare global {
1056
1069
  interface HTMLElementTagNameMap {
1057
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1070
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
1058
1071
  }
1059
1072
  }
1060
1073
 
@@ -1062,7 +1075,7 @@ declare global {
1062
1075
  declare global {
1063
1076
  namespace JSX {
1064
1077
  interface IntrinsicElements {
1065
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1078
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1066
1079
  }
1067
1080
  }
1068
1081
  }
@@ -1070,7 +1083,7 @@ declare global {
1070
1083
 
1071
1084
  declare global {
1072
1085
  interface HTMLElementTagNameMap {
1073
- 'gs-mutations-over-time': MutationsOverTimeComponent;
1086
+ 'gs-sequences-by-location': SequencesByLocationComponent;
1074
1087
  }
1075
1088
  }
1076
1089
 
@@ -1078,7 +1091,7 @@ declare global {
1078
1091
  declare global {
1079
1092
  namespace JSX {
1080
1093
  interface IntrinsicElements {
1081
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1094
+ 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1082
1095
  }
1083
1096
  }
1084
1097
  }
@@ -1086,7 +1099,7 @@ declare global {
1086
1099
 
1087
1100
  declare global {
1088
1101
  interface HTMLElementTagNameMap {
1089
- 'gs-sequences-by-location': SequencesByLocationComponent;
1102
+ 'gs-statistics': StatisticsComponent;
1090
1103
  }
1091
1104
  }
1092
1105
 
@@ -1094,7 +1107,7 @@ declare global {
1094
1107
  declare global {
1095
1108
  namespace JSX {
1096
1109
  interface IntrinsicElements {
1097
- 'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1110
+ 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1098
1111
  }
1099
1112
  }
1100
1113
  }
@@ -1122,7 +1135,10 @@ declare global {
1122
1135
 
1123
1136
  declare global {
1124
1137
  interface HTMLElementTagNameMap {
1125
- 'gs-statistics': StatisticsComponent;
1138
+ 'gs-location-filter': LocationFilterComponent;
1139
+ }
1140
+ interface HTMLElementEventMap {
1141
+ [gsEventNames.locationChanged]: LocationChangedEvent;
1126
1142
  }
1127
1143
  }
1128
1144
 
@@ -1130,7 +1146,7 @@ declare global {
1130
1146
  declare global {
1131
1147
  namespace JSX {
1132
1148
  interface IntrinsicElements {
1133
- 'gs-statistics': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1149
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1134
1150
  }
1135
1151
  }
1136
1152
  }
@@ -1157,10 +1173,10 @@ declare global {
1157
1173
 
1158
1174
  declare global {
1159
1175
  interface HTMLElementTagNameMap {
1160
- 'gs-location-filter': LocationFilterComponent;
1176
+ 'gs-mutation-filter': MutationFilterComponent;
1161
1177
  }
1162
1178
  interface HTMLElementEventMap {
1163
- [gsEventNames.locationChanged]: LocationChangedEvent;
1179
+ [gsEventNames.mutationFilterChanged]: CustomEvent<MutationsFilter>;
1164
1180
  }
1165
1181
  }
1166
1182
 
@@ -1168,7 +1184,7 @@ declare global {
1168
1184
  declare global {
1169
1185
  namespace JSX {
1170
1186
  interface IntrinsicElements {
1171
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1187
+ 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1172
1188
  }
1173
1189
  }
1174
1190
  }
@@ -1176,10 +1192,11 @@ declare global {
1176
1192
 
1177
1193
  declare global {
1178
1194
  interface HTMLElementTagNameMap {
1179
- 'gs-mutation-filter': MutationFilterComponent;
1195
+ 'gs-lineage-filter': LineageFilterComponent;
1180
1196
  }
1181
1197
  interface HTMLElementEventMap {
1182
- [gsEventNames.mutationFilterChanged]: CustomEvent<MutationsFilter>;
1198
+ [gsEventNames.lineageFilterChanged]: LineageFilterChangedEvent;
1199
+ [gsEventNames.lineageFilterMultiChanged]: LineageMultiFilterChangedEvent;
1183
1200
  }
1184
1201
  }
1185
1202
 
@@ -1187,7 +1204,7 @@ declare global {
1187
1204
  declare global {
1188
1205
  namespace JSX {
1189
1206
  interface IntrinsicElements {
1190
- 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1207
+ 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1191
1208
  }
1192
1209
  }
1193
1210
  }
@@ -1215,11 +1232,7 @@ declare global {
1215
1232
 
1216
1233
  declare global {
1217
1234
  interface HTMLElementTagNameMap {
1218
- 'gs-lineage-filter': LineageFilterComponent;
1219
- }
1220
- interface HTMLElementEventMap {
1221
- [gsEventNames.lineageFilterChanged]: LineageFilterChangedEvent;
1222
- [gsEventNames.lineageFilterMultiChanged]: LineageMultiFilterChangedEvent;
1235
+ 'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
1223
1236
  }
1224
1237
  }
1225
1238
 
@@ -1227,7 +1240,7 @@ declare global {
1227
1240
  declare global {
1228
1241
  namespace JSX {
1229
1242
  interface IntrinsicElements {
1230
- 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1243
+ 'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1231
1244
  }
1232
1245
  }
1233
1246
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "1.10.3",
3
+ "version": "1.11.1",
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 function getFilteredMutationOverTimeData({
68
68
  if (hideGaps) {
69
69
  const dateRangesToFilterOut = filteredData.getSecondAxisKeys().filter((dateRange) => {
70
70
  const vals = filteredData.getColumn(dateRange);
71
- return !vals.some((v) => v?.type === 'value' && v.totalCount > 0);
71
+ return !vals.some((v) => (v?.type === 'value' || v?.type === 'valueWithCoverage') && v.totalCount > 0);
72
72
  });
73
73
  dateRangesToFilterOut.forEach((dateRange) => filteredData.deleteColumn(dateRange));
74
74
  }
@@ -45,7 +45,7 @@ export const NoValue: StoryObj<MutationsOverTimeGridTooltipProps> = {
45
45
  const canvas = within(canvasElement);
46
46
 
47
47
  await expect(canvas.getByText('2025', { exact: true })).toBeVisible();
48
- await expect(canvas.getByText('(2025-01-01 - 2025-12-31)')).toBeVisible();
48
+ await expect(canvas.getByText('2025-01-01 - 2025-12-31')).toBeVisible();
49
49
  await expect(canvas.getByText('A500-')).toBeVisible();
50
50
  await expect(canvas.getByText('No data')).toBeVisible();
51
51
  },
@@ -65,9 +65,60 @@ export const WithValue: StoryObj<MutationsOverTimeGridTooltipProps> = {
65
65
  play: async ({ canvasElement }) => {
66
66
  const canvas = within(canvasElement);
67
67
 
68
- await expect(canvas.getByText('Proportion: 50.00%')).toBeVisible();
69
- await expect(canvas.getByText('300 samples are in the timeframe')).toBeVisible();
70
- await expect(canvas.getByText('200 have coverage, of those 100 have the mutation')).toBeVisible();
68
+ await expect(canvas.getByText('50.00%')).toBeVisible();
69
+ await expect(canvas.getByText('100')).toBeVisible();
70
+ await expect(canvas.getByText('have the mutation A500-.')).toBeVisible();
71
+ await expect(canvas.getByText('200')).toBeVisible();
72
+ await expect(canvas.getByText('have coverage at position 500.')).toBeVisible();
73
+ await expect(canvas.getByText('300')).toBeVisible();
74
+ await expect(canvas.getByText('total in this date range.')).toBeVisible();
75
+ },
76
+ };
77
+
78
+ export const WithValueWithZero: StoryObj<MutationsOverTimeGridTooltipProps> = {
79
+ ...Template,
80
+ args: {
81
+ ...Template.args,
82
+ value: {
83
+ type: 'value',
84
+ proportion: 0,
85
+ count: 0,
86
+ totalCount: 300,
87
+ },
88
+ },
89
+ play: async ({ canvasElement }) => {
90
+ const canvas = within(canvasElement);
91
+
92
+ await expect(canvas.getByText('0.00%')).toBeVisible();
93
+ await expect(canvas.getByText('0')).toBeVisible();
94
+ await expect(canvas.getByText('have the mutation A500-.')).toBeVisible();
95
+ await expect(canvas.queryByText('with coverage at position 500.')).not.toBeInTheDocument();
96
+ await expect(canvas.getByText('300')).toBeVisible();
97
+ await expect(canvas.getByText('total in this date range.')).toBeVisible();
98
+ },
99
+ };
100
+
101
+ export const WithValueWithCoverage: StoryObj<MutationsOverTimeGridTooltipProps> = {
102
+ ...Template,
103
+ args: {
104
+ ...Template.args,
105
+ value: {
106
+ type: 'valueWithCoverage',
107
+ count: 100,
108
+ coverage: 200,
109
+ totalCount: 300,
110
+ },
111
+ },
112
+ play: async ({ canvasElement }) => {
113
+ const canvas = within(canvasElement);
114
+
115
+ await expect(canvas.getByText('50.00%')).toBeVisible();
116
+ await expect(canvas.getByText('100')).toBeVisible();
117
+ await expect(canvas.getByText('have the mutation A500- out of')).toBeVisible();
118
+ await expect(canvas.getByText('200')).toBeVisible();
119
+ await expect(canvas.getByText('with coverage at position 500.')).toBeVisible();
120
+ await expect(canvas.getByText('300')).toBeVisible();
121
+ await expect(canvas.getByText('total in this date range.')).toBeVisible();
71
122
  },
72
123
  };
73
124
 
@@ -83,9 +134,10 @@ export const WithValueBelowThreshold: StoryObj<MutationsOverTimeGridTooltipProps
83
134
  play: async ({ canvasElement }) => {
84
135
  const canvas = within(canvasElement);
85
136
 
86
- await expect(canvas.getByText('Proportion: <0.10%')).toBeVisible();
87
- await expect(canvas.getByText('300 samples are in the timeframe')).toBeVisible();
88
- await expect(canvas.getByText('none or less than 0.10% have the mutation')).toBeVisible();
137
+ await expect(canvas.getByText('<0.10%')).toBeVisible();
138
+ await expect(canvas.getByText('None or less than 0.10% have the mutation.')).toBeVisible();
139
+ await expect(canvas.getByText('300')).toBeVisible();
140
+ await expect(canvas.getByText('total in this date range.')).toBeVisible();
89
141
  },
90
142
  };
91
143
 
@@ -101,8 +153,8 @@ export const WithWastewaterValue: StoryObj<MutationsOverTimeGridTooltipProps> =
101
153
  play: async ({ canvasElement }) => {
102
154
  const canvas = within(canvasElement);
103
155
 
104
- await expect(canvas.getByText('Proportion: 50.00%')).toBeVisible();
105
- await expect(canvas.queryByText('samples are in the timeframe')).not.toBeInTheDocument();
106
- await expect(canvas.queryByText('have coverage')).not.toBeInTheDocument();
156
+ await expect(canvas.getByText('50.00%')).toBeVisible();
157
+ await expect(canvas.queryByText('total in this date range.')).not.toBeInTheDocument();
158
+ await expect(canvas.queryByText('coverage')).not.toBeInTheDocument();
107
159
  },
108
160
  };
@@ -21,68 +21,110 @@ export const MutationsOverTimeGridTooltip: FunctionComponent<MutationsOverTimeGr
21
21
  }: MutationsOverTimeGridTooltipProps) => {
22
22
  const dateClass = toTemporalClass(date);
23
23
 
24
- return (
25
- <div className='text-center'>
26
- <p>
27
- <span className='font-bold'>{dateClass.englishName()}</span>
28
- </p>
29
- <p>({timeIntervalDisplay(dateClass)})</p>
30
- <p>{mutation.code}</p>
31
- <TooltipValueDescription value={value} />
32
- </div>
33
- );
34
- };
24
+ let proportionText = 'No data';
35
25
 
36
- const TooltipValueDescription: FunctionComponent<{ value: MutationOverTimeMutationValue }> = ({ value }) => {
37
- if (value === null) {
38
- return <p>No data</p>;
26
+ if (value !== null) {
27
+ switch (value.type) {
28
+ case 'belowThreshold': {
29
+ proportionText = `<${formatProportion(MUTATIONS_OVER_TIME_MIN_PROPORTION)}`;
30
+ break;
31
+ }
32
+ case 'value':
33
+ case 'wastewaterValue': {
34
+ proportionText = formatProportion(value.proportion);
35
+ break;
36
+ }
37
+ case 'valueWithCoverage': {
38
+ // value.coverage will always be non-zero if we're in this case
39
+ proportionText = formatProportion(value.count / value.coverage);
40
+ break;
41
+ }
42
+ }
39
43
  }
40
44
 
41
- const proportion =
42
- value.type === 'belowThreshold'
43
- ? `<${formatProportion(MUTATIONS_OVER_TIME_MIN_PROPORTION)}`
44
- : formatProportion(value.proportion);
45
-
46
45
  return (
47
- <>
48
- <p>Proportion: {proportion}</p>
49
- <TooltipValueCountsDescription value={value} />
50
- </>
46
+ <div>
47
+ <div className='flex flex-row justify-between gap-4 items-baseline'>
48
+ <div className='flex flex-col text-left'>
49
+ <span className='font-bold'>{mutation.code}</span>
50
+ <span>{proportionText}</span>
51
+ </div>
52
+ <div className='flex flex-col text-right'>
53
+ <span className='font-bold'>{dateClass.englishName()}</span>
54
+ <span className='text-gray-600'>{timeIntervalDisplay(dateClass)}</span>
55
+ </div>
56
+ </div>
57
+ {value !== null && (
58
+ <TooltipValueCountsDescription
59
+ value={value}
60
+ mutationCode={mutation.code}
61
+ mutationPosition={mutation.position}
62
+ />
63
+ )}
64
+ </div>
51
65
  );
52
66
  };
53
67
 
54
68
  const TooltipValueCountsDescription: FunctionComponent<{
55
69
  value: NonNullable<MutationOverTimeMutationValue>;
56
- }> = ({ value }) => {
57
- switch (value.type) {
58
- case 'wastewaterValue':
59
- return;
60
- case 'belowThreshold':
61
- return (
62
- <>
63
- <p>{value.totalCount} samples are in the timeframe</p>
64
- <p>none or less than {formatProportion(MUTATIONS_OVER_TIME_MIN_PROPORTION)} have the mutation</p>
65
- </>
66
- );
67
- case 'value':
68
- return (
69
- <>
70
- <p>{value.totalCount} samples are in the timeframe</p>
71
- <p>
72
- {totalCountWithCoverage(value.count, value.proportion)} have coverage, of those {value.count}{' '}
73
- have the mutation
74
- </p>
75
- </>
76
- );
70
+ mutationCode: string;
71
+ mutationPosition: number;
72
+ }> = ({ value, mutationCode, mutationPosition }) => {
73
+ if (value.type === 'wastewaterValue') {
74
+ return;
77
75
  }
78
- };
76
+ return (
77
+ <div className='mt-2'>
78
+ {(() => {
79
+ switch (value.type) {
80
+ case 'belowThreshold':
81
+ return (
82
+ <p className='text-gray-600'>
83
+ None or less than {formatProportion(MUTATIONS_OVER_TIME_MIN_PROPORTION)} have the
84
+ mutation.
85
+ </p>
86
+ );
79
87
 
80
- function totalCountWithCoverage(count: number, proportion: number) {
81
- if (count === 0) {
82
- return 0;
83
- }
84
- return Math.round(count / proportion);
85
- }
88
+ case 'value':
89
+ return (
90
+ <>
91
+ <p>
92
+ {value.count}{' '}
93
+ <span className='text-gray-600'>have the mutation {mutationCode}.</span>
94
+ </p>
95
+ {value.proportion > 0 && (
96
+ <p>
97
+ {Math.round(value.count / value.proportion)}{' '}
98
+ <span className='text-gray-600'>
99
+ have coverage at position {mutationPosition}.
100
+ </span>
101
+ </p>
102
+ )}
103
+ </>
104
+ );
105
+
106
+ case 'valueWithCoverage':
107
+ return (
108
+ <>
109
+ <p>
110
+ {value.count}{' '}
111
+ <span className='text-gray-600'>have the mutation {mutationCode} out of</span>
112
+ </p>
113
+ <p>
114
+ {value.coverage}{' '}
115
+ <span className='text-gray-600'>with coverage at position {mutationPosition}.</span>
116
+ </p>
117
+ </>
118
+ );
119
+ }
120
+ })()}
121
+
122
+ <p>
123
+ {value.totalCount} <span className='text-gray-600'>total in this date range.</span>
124
+ </p>
125
+ </div>
126
+ );
127
+ };
86
128
 
87
129
  const timeIntervalDisplay = (date: TemporalClass) => {
88
130
  if (date instanceof YearMonthDayClass) {
@@ -1,9 +1,10 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
  import { useMemo } from 'preact/hooks';
3
+ import z from 'zod';
3
4
 
4
5
  import { type MutationOverTimeDataMap } from './MutationOverTimeData';
5
6
  import { MutationsOverTimeGridTooltip } from './mutations-over-time-grid-tooltip';
6
- import { type MutationOverTimeMutationValue } from '../../query/queryMutationsOverTime';
7
+ import { getProportion, type MutationOverTimeMutationValue } from '../../query/queryMutationsOverTime';
7
8
  import { type SequenceType } from '../../types';
8
9
  import { type Deletion, type Substitution } from '../../utils/mutations';
9
10
  import { type Temporal } from '../../utils/temporalClass';
@@ -24,29 +25,45 @@ import {
24
25
 
25
26
  const NON_BREAKING_SPACE = '\u00A0';
26
27
 
28
+ export const customColumnSchema = z.object({
29
+ header: z.string(),
30
+ values: z.record(z.string(), z.union([z.string(), z.number()])),
31
+ });
32
+ export type CustomColumn = z.infer<typeof customColumnSchema>;
33
+
27
34
  export interface MutationsOverTimeGridProps {
28
35
  data: MutationOverTimeDataMap;
29
36
  colorScale: ColorScale;
30
37
  sequenceType: SequenceType;
31
38
  pageSizes: PageSizes;
39
+ customColumns?: CustomColumn[];
32
40
  tooltipPortalTarget: HTMLElement | null;
33
41
  }
34
42
 
35
- type RowType = { mutation: Substitution | Deletion; values: (MutationOverTimeMutationValue | undefined)[] };
43
+ type RowType = {
44
+ mutation: Substitution | Deletion;
45
+ values: (MutationOverTimeMutationValue | undefined)[];
46
+ customValues: (string | number | undefined)[];
47
+ };
48
+
49
+ const EMPTY_COLUMNS: CustomColumn[] = [];
36
50
 
37
51
  const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
38
52
  data,
39
53
  colorScale,
40
54
  sequenceType,
41
55
  pageSizes,
56
+ customColumns = EMPTY_COLUMNS,
42
57
  tooltipPortalTarget,
43
58
  }) => {
44
59
  const tableData = useMemo(() => {
45
60
  const allMutations = data.getFirstAxisKeys();
46
- return data.getAsArray().map((row, index) => {
47
- return { mutation: allMutations[index], values: [...row] };
61
+ return data.getAsArray().map((row, index): RowType => {
62
+ const mutation = allMutations[index];
63
+ const customValues = customColumns.map((col) => col.values[mutation.code]);
64
+ return { mutation, values: [...row], customValues };
48
65
  });
49
- }, [data]);
66
+ }, [data, customColumns]);
50
67
 
51
68
  const columns = useMemo(() => {
52
69
  const columnHelper = createColumnHelper<RowType>();
@@ -65,6 +82,17 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
65
82
  },
66
83
  });
67
84
 
85
+ const customColumnHeaders = customColumns.map((customCol, index) => {
86
+ return columnHelper.accessor((row) => row.customValues[index], {
87
+ id: `custom-${index}`,
88
+ header: () => <span>{customCol.header}</span>,
89
+ cell: ({ getValue }) => {
90
+ const value = getValue();
91
+ return <div className={'text-center'}>{value ?? ''}</div>;
92
+ },
93
+ });
94
+ });
95
+
68
96
  const dateHeaders = dates.map((date, index) => {
69
97
  return columnHelper.accessor((row) => row.values[index], {
70
98
  id: `date-${index}`,
@@ -102,8 +130,8 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
102
130
  });
103
131
  });
104
132
 
105
- return [mutationHeader, ...dateHeaders];
106
- }, [colorScale, data, sequenceType, tooltipPortalTarget]);
133
+ return [mutationHeader, ...customColumnHeaders, ...dateHeaders];
134
+ }, [colorScale, data, sequenceType, customColumns, tooltipPortalTarget]);
107
135
 
108
136
  const { pageSize } = usePageSizeContext();
109
137
  const table = usePreactTable({
@@ -183,7 +211,7 @@ const ProportionCell: FunctionComponent<{
183
211
  colorScale: ColorScale;
184
212
  tooltipPortalTarget: HTMLElement | null;
185
213
  }> = ({ value, mutation, date, tooltipPosition, colorScale, tooltipPortalTarget }) => {
186
- const proportion = value?.type === 'belowThreshold' ? undefined : value?.proportion;
214
+ const proportion = getProportion(value);
187
215
 
188
216
  return (
189
217
  <div className={'py-1 w-full h-full'}>