@genspectrum/dashboard-components 0.1.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 (186) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +109 -0
  3. package/custom-elements.json +1587 -0
  4. package/dist/dashboard-components.js +7322 -0
  5. package/dist/dashboard-components.js.map +1 -0
  6. package/dist/genspectrum-components.d.ts +298 -0
  7. package/dist/style.css +2930 -0
  8. package/package.json +109 -0
  9. package/src/constants.ts +6 -0
  10. package/src/index.ts +1 -0
  11. package/src/lapisApi/ReferenceGenome.ts +30 -0
  12. package/src/lapisApi/__mockData__/referenceGenome.json +58 -0
  13. package/src/lapisApi/lapisApi.ts +99 -0
  14. package/src/lapisApi/lapisTypes.ts +51 -0
  15. package/src/operator/Dataset.ts +3 -0
  16. package/src/operator/DivisionOperator.spec.ts +27 -0
  17. package/src/operator/DivisionOperator.ts +60 -0
  18. package/src/operator/FetchAggregatedOperator.ts +44 -0
  19. package/src/operator/FetchInsertionsOperator.ts +24 -0
  20. package/src/operator/FetchSubstitutionsOrDeletionsOperator.ts +49 -0
  21. package/src/operator/FillMissingOperator.spec.ts +26 -0
  22. package/src/operator/FillMissingOperator.ts +30 -0
  23. package/src/operator/GroupByAndSumOperator.spec.ts +26 -0
  24. package/src/operator/GroupByAndSumOperator.ts +26 -0
  25. package/src/operator/GroupByOperator.spec.ts +43 -0
  26. package/src/operator/GroupByOperator.ts +32 -0
  27. package/src/operator/MapOperator.spec.ts +13 -0
  28. package/src/operator/MapOperator.ts +16 -0
  29. package/src/operator/MockOperator.spec.ts +11 -0
  30. package/src/operator/MockOperator.ts +12 -0
  31. package/src/operator/Operator.ts +5 -0
  32. package/src/operator/SlidingOperator.spec.ts +52 -0
  33. package/src/operator/SlidingOperator.ts +23 -0
  34. package/src/operator/SortOperator.spec.ts +13 -0
  35. package/src/operator/SortOperator.ts +16 -0
  36. package/src/preact/LapisUrlContext.ts +3 -0
  37. package/src/preact/ReferenceGenomeContext.ts +5 -0
  38. package/src/preact/components/SegmentSelector.tsx +62 -0
  39. package/src/preact/components/chart.stories.tsx +42 -0
  40. package/src/preact/components/chart.tsx +32 -0
  41. package/src/preact/components/checkbox-selector.stories.tsx +56 -0
  42. package/src/preact/components/checkbox-selector.tsx +46 -0
  43. package/src/preact/components/confidence-interval-selector.tsx +45 -0
  44. package/src/preact/components/csv-download-button.stories.tsx +25 -0
  45. package/src/preact/components/csv-download-button.tsx +51 -0
  46. package/src/preact/components/error-display.stories.tsx +22 -0
  47. package/src/preact/components/error-display.tsx +5 -0
  48. package/src/preact/components/headline.stories.tsx +29 -0
  49. package/src/preact/components/headline.tsx +16 -0
  50. package/src/preact/components/info.stories.tsx +22 -0
  51. package/src/preact/components/info.tsx +16 -0
  52. package/src/preact/components/loading-display.stories.tsx +20 -0
  53. package/src/preact/components/loading-display.tsx +5 -0
  54. package/src/preact/components/min-max-percent-slider.css +40 -0
  55. package/src/preact/components/min-max-range-slider.tsx +95 -0
  56. package/src/preact/components/mutation-type-selector.tsx +30 -0
  57. package/src/preact/components/no-data-display.stories.tsx +20 -0
  58. package/src/preact/components/no-data-display.tsx +5 -0
  59. package/src/preact/components/percent-intput.tsx +49 -0
  60. package/src/preact/components/proportion-selector-dropdown.stories.tsx +66 -0
  61. package/src/preact/components/proportion-selector-dropdown.tsx +33 -0
  62. package/src/preact/components/proportion-selector.stories.tsx +81 -0
  63. package/src/preact/components/proportion-selector.tsx +43 -0
  64. package/src/preact/components/scaling-selector.stories.tsx +25 -0
  65. package/src/preact/components/scaling-selector.tsx +36 -0
  66. package/src/preact/components/select.stories.tsx +42 -0
  67. package/src/preact/components/select.tsx +21 -0
  68. package/src/preact/components/table.stories.tsx +24 -0
  69. package/src/preact/components/table.tsx +51 -0
  70. package/src/preact/components/tabs.stories.tsx +60 -0
  71. package/src/preact/components/tabs.tsx +49 -0
  72. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +32 -0
  73. package/src/preact/dateRangeSelector/date-range-selector.tsx +228 -0
  74. package/src/preact/dateRangeSelector/dateConversion.ts +8 -0
  75. package/src/preact/locationFilter/__mockData__/aggregated.json +775 -0
  76. package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +36 -0
  77. package/src/preact/locationFilter/fetchAutocompletionList.ts +43 -0
  78. package/src/preact/locationFilter/location-filter.stories.tsx +50 -0
  79. package/src/preact/locationFilter/location-filter.tsx +112 -0
  80. package/src/preact/mutationComparison/__mockData__/nucleotideMutationsOtherVariant.json +295 -0
  81. package/src/preact/mutationComparison/__mockData__/nucleotideMutationsSomeVariant.json +304 -0
  82. package/src/preact/mutationComparison/fetchMutationData.spec.ts +118 -0
  83. package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +125 -0
  84. package/src/preact/mutationComparison/getMutationComparisonTableData.ts +40 -0
  85. package/src/preact/mutationComparison/mutation-comparison-table.tsx +43 -0
  86. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +122 -0
  87. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +152 -0
  88. package/src/preact/mutationComparison/mutation-comparison.tsx +179 -0
  89. package/src/preact/mutationComparison/queryMutationData.ts +53 -0
  90. package/src/preact/mutationFilter/mutation-filter.stories.tsx +164 -0
  91. package/src/preact/mutationFilter/mutation-filter.tsx +268 -0
  92. package/src/preact/mutationFilter/parseAndValidateMutation.ts +54 -0
  93. package/src/preact/mutationFilter/parseMutation.spec.ts +150 -0
  94. package/src/preact/mutationFilter/sequenceTypeFromSegment.spec.ts +66 -0
  95. package/src/preact/mutationFilter/sequenceTypeFromSegment.ts +20 -0
  96. package/src/preact/mutations/__mockData__/nucleotideInsertions.json +252 -0
  97. package/src/preact/mutations/__mockData__/nucleotideMutations.json +880 -0
  98. package/src/preact/mutations/getInsertionsTableData.spec.ts +36 -0
  99. package/src/preact/mutations/getInsertionsTableData.ts +10 -0
  100. package/src/preact/mutations/getMutationsGridData.spec.ts +135 -0
  101. package/src/preact/mutations/getMutationsGridData.ts +92 -0
  102. package/src/preact/mutations/getMutationsTableData.spec.ts +94 -0
  103. package/src/preact/mutations/getMutationsTableData.ts +17 -0
  104. package/src/preact/mutations/mutations-grid.tsx +84 -0
  105. package/src/preact/mutations/mutations-insertions-table.tsx +33 -0
  106. package/src/preact/mutations/mutations-table.tsx +47 -0
  107. package/src/preact/mutations/mutations.stories.tsx +95 -0
  108. package/src/preact/mutations/mutations.tsx +192 -0
  109. package/src/preact/mutations/queryMutations.ts +55 -0
  110. package/src/preact/prevalenceOverTime/__mockData__/denominator.json +1700 -0
  111. package/src/preact/prevalenceOverTime/__mockData__/denominatorOneVariant.json +608 -0
  112. package/src/preact/prevalenceOverTime/__mockData__/numeratorEG.json +1560 -0
  113. package/src/preact/prevalenceOverTime/__mockData__/numeratorJN1.json +592 -0
  114. package/src/preact/prevalenceOverTime/__mockData__/numeratorOneVariant.json +604 -0
  115. package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.spec.ts +67 -0
  116. package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.ts +18 -0
  117. package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +105 -0
  118. package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +86 -0
  119. package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +141 -0
  120. package/src/preact/prevalenceOverTime/prevalence-over-time-table.tsx +46 -0
  121. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +165 -0
  122. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +202 -0
  123. package/src/preact/relativeGrowthAdvantage/__mockData__/denominator.json +376 -0
  124. package/src/preact/relativeGrowthAdvantage/__mockData__/numerator.json +332 -0
  125. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +138 -0
  126. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +71 -0
  127. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +136 -0
  128. package/src/preact/shared/charts/LogitScale.ts +48 -0
  129. package/src/preact/shared/charts/colors.ts +26 -0
  130. package/src/preact/shared/charts/confideceInterval.ts +29 -0
  131. package/src/preact/shared/charts/getYAxisScale.ts +16 -0
  132. package/src/preact/shared/charts/scales.ts +16 -0
  133. package/src/preact/shared/icons/DeleteIcon.tsx +17 -0
  134. package/src/preact/shared/sort/sortInsertions.spec.ts +47 -0
  135. package/src/preact/shared/sort/sortInsertions.ts +21 -0
  136. package/src/preact/shared/sort/sortMutationPositions.spec.ts +31 -0
  137. package/src/preact/shared/sort/sortMutationPositions.ts +14 -0
  138. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +47 -0
  139. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +17 -0
  140. package/src/preact/shared/table/formatProportion.ts +3 -0
  141. package/src/preact/textInput/__mockData__/aggregated_hosts.json +24 -0
  142. package/src/preact/textInput/fetchAutocompleteList.ts +9 -0
  143. package/src/preact/textInput/text-input.stories.tsx +49 -0
  144. package/src/preact/textInput/text-input.tsx +73 -0
  145. package/src/preact/useQuery.ts +27 -0
  146. package/src/query/queryInsertions.ts +14 -0
  147. package/src/query/queryPrevalenceOverTime.ts +126 -0
  148. package/src/query/queryRelativeGrowthAdvantage.ts +131 -0
  149. package/src/query/querySubstitutionsOrDeletions.ts +19 -0
  150. package/src/styles/tailwind.css +3 -0
  151. package/src/styles/tailwind.d.ts +3 -0
  152. package/src/types.ts +23 -0
  153. package/src/utils/mutations.spec.ts +64 -0
  154. package/src/utils/mutations.ts +165 -0
  155. package/src/utils/temporal.spec.ts +97 -0
  156. package/src/utils/temporal.ts +348 -0
  157. package/src/utils/test-utils.ts +5 -0
  158. package/src/utils/type-utils.ts +15 -0
  159. package/src/utils/utils.spec.ts +16 -0
  160. package/src/utils/utils.ts +38 -0
  161. package/src/web-components/PreactLitAdapter.tsx +62 -0
  162. package/src/web-components/PreactLitAdapterWithGridJsStyles.tsx +12 -0
  163. package/src/web-components/app.ts +51 -0
  164. package/src/web-components/display/index.ts +4 -0
  165. package/src/web-components/display/mutation-comparison-component.stories.ts +138 -0
  166. package/src/web-components/display/mutation-comparison-component.tsx +31 -0
  167. package/src/web-components/display/mutations-component.stories.ts +107 -0
  168. package/src/web-components/display/mutations-component.tsx +27 -0
  169. package/src/web-components/display/prevalence-over-time-component.stories.ts +205 -0
  170. package/src/web-components/display/prevalence-over-time-component.tsx +46 -0
  171. package/src/web-components/display/relative-growth-advantage-component.stories.ts +89 -0
  172. package/src/web-components/display/relative-growth-advantage-component.tsx +37 -0
  173. package/src/web-components/index.ts +3 -0
  174. package/src/web-components/input/date-range-selector-component.stories.ts +53 -0
  175. package/src/web-components/input/date-range-selector-component.tsx +33 -0
  176. package/src/web-components/input/index.ts +4 -0
  177. package/src/web-components/input/location-filter-component.stories.ts +184 -0
  178. package/src/web-components/input/location-filter-component.tsx +68 -0
  179. package/src/web-components/input/location-filter.mdx +25 -0
  180. package/src/web-components/input/mutation-filter-component.stories.ts +97 -0
  181. package/src/web-components/input/mutation-filter-component.tsx +27 -0
  182. package/src/web-components/input/text-input-component.stories.ts +92 -0
  183. package/src/web-components/input/text-input-component.tsx +30 -0
  184. package/src/web-components/lapis-context.ts +3 -0
  185. package/src/web-components/reference-genome-context.ts +5 -0
  186. package/src/web-components/withinShadowRoot.story.ts +34 -0
@@ -0,0 +1,26 @@
1
+ import { describe, it } from 'vitest';
2
+
3
+ import { FillMissingOperator } from './FillMissingOperator';
4
+ import { MockOperator } from './MockOperator';
5
+ import { expectEqualAfterSorting } from '../utils/test-utils';
6
+
7
+ describe('FillMissingOperator', () => {
8
+ it('should fill in missing values', async () => {
9
+ const child = new MockOperator([{ id: 1 }, { id: 3 }]);
10
+ const query = new FillMissingOperator(
11
+ child,
12
+ 'id',
13
+ (ids) => [Math.min(...ids), Math.max(...ids)],
14
+ (min, max) => {
15
+ const result = [];
16
+ for (let i = min; i <= max; i++) {
17
+ result.push(i);
18
+ }
19
+ return result;
20
+ },
21
+ (id) => ({ id }),
22
+ );
23
+ const result = await query.evaluate('lapis');
24
+ await expectEqualAfterSorting(result.content, [{ id: 1 }, { id: 2 }, { id: 3 }], (a, b) => a.id - b.id);
25
+ });
26
+ });
@@ -0,0 +1,30 @@
1
+ import { type Dataset } from './Dataset';
2
+ import { type Operator } from './Operator';
3
+
4
+ export class FillMissingOperator<Data, KeyToFill extends keyof Data> implements Operator<Data> {
5
+ constructor(
6
+ private child: Operator<Data>,
7
+ private keyField: KeyToFill,
8
+ private getMinMaxFn: (values: Iterable<Data[KeyToFill]>) => [Data[KeyToFill], Data[KeyToFill]] | null,
9
+ private getAllRequiredKeysFn: (min: Data[KeyToFill], max: Data[KeyToFill]) => Data[KeyToFill][],
10
+ private defaultValueFn: (key: Data[KeyToFill]) => Data,
11
+ ) {}
12
+
13
+ async evaluate(lapis: string, signal?: AbortSignal): Promise<Dataset<Data>> {
14
+ const childEvaluated = await this.child.evaluate(lapis, signal);
15
+ const existingKeys = new Set(childEvaluated.content.map((row) => row[this.keyField]));
16
+ const minMax = this.getMinMaxFn(existingKeys);
17
+ if (minMax === null) {
18
+ return childEvaluated;
19
+ }
20
+ const [min, max] = minMax;
21
+ const requiredKeys = this.getAllRequiredKeysFn(min, max);
22
+ const content = childEvaluated.content;
23
+ for (const key of requiredKeys) {
24
+ if (!existingKeys.has(key)) {
25
+ content.push(this.defaultValueFn(key));
26
+ }
27
+ }
28
+ return { content };
29
+ }
30
+ }
@@ -0,0 +1,26 @@
1
+ import { describe, it } from 'vitest';
2
+
3
+ import { GroupByAndSumOperator } from './GroupByAndSumOperator';
4
+ import { MockOperator } from './MockOperator';
5
+ import { expectEqualAfterSorting } from '../utils/test-utils';
6
+
7
+ describe('GroupByAndSumOperator', () => {
8
+ it('should group the content of the child operator and calculate the sum if a field', async () => {
9
+ const child = new MockOperator([
10
+ { lineage: 'A', n: 1 },
11
+ { lineage: 'A', n: 2 },
12
+ { lineage: 'B', n: 3 },
13
+ ]);
14
+
15
+ const query = new GroupByAndSumOperator(child, 'lineage', 'n');
16
+ const result = await query.evaluate('lapis');
17
+ await expectEqualAfterSorting(
18
+ result.content,
19
+ [
20
+ { lineage: 'A', n: 3 },
21
+ { lineage: 'B', n: 3 },
22
+ ],
23
+ (a, b) => a.lineage.localeCompare(b.lineage),
24
+ );
25
+ });
26
+ });
@@ -0,0 +1,26 @@
1
+ import { GroupByOperator } from './GroupByOperator';
2
+ import { type Operator } from './Operator';
3
+ import { type NumberFields } from '../utils/type-utils';
4
+
5
+ type Result<Data, KeyToGroupBy extends keyof Data, KeyToSumBy extends keyof Data> = {
6
+ [P in KeyToGroupBy | KeyToSumBy]: P extends KeyToGroupBy ? Data[KeyToGroupBy] : number;
7
+ };
8
+
9
+ export class GroupByAndSumOperator<
10
+ Data,
11
+ KeyToGroupBy extends keyof Data,
12
+ KeySoSumBy extends NumberFields<Data>,
13
+ > extends GroupByOperator<Data, Result<Data, KeyToGroupBy, KeySoSumBy>, KeyToGroupBy> {
14
+ constructor(child: Operator<Data>, groupByField: KeyToGroupBy, sumField: KeySoSumBy) {
15
+ super(child, groupByField, (values: Data[]) => {
16
+ let n = 0;
17
+ for (const value of values) {
18
+ n += value[sumField] as number;
19
+ }
20
+ return {
21
+ [groupByField]: values[0][groupByField],
22
+ [sumField]: n,
23
+ } as Result<Data, KeyToGroupBy, KeySoSumBy>;
24
+ });
25
+ }
26
+ }
@@ -0,0 +1,43 @@
1
+ import { describe, it } from 'vitest';
2
+
3
+ import { GroupByOperator } from './GroupByOperator';
4
+ import { MockOperator } from './MockOperator';
5
+ import { expectEqualAfterSorting } from '../utils/test-utils';
6
+
7
+ describe('GroupByOperator', () => {
8
+ it('should group the content of the child operator', async () => {
9
+ const child = new MockOperator([
10
+ { lineage: 'A', n: 1 },
11
+ { lineage: 'A', n: 2 },
12
+ { lineage: 'B', n: 3 },
13
+ ]);
14
+
15
+ const queryCount = new GroupByOperator(child, 'lineage', (values) => ({
16
+ lineage: values[0].lineage,
17
+ n: values.length,
18
+ }));
19
+ const resultCount = await queryCount.evaluate('lapis');
20
+ await expectEqualAfterSorting(
21
+ resultCount.content,
22
+ [
23
+ { lineage: 'A', n: 2 },
24
+ { lineage: 'B', n: 1 },
25
+ ],
26
+ (a, b) => a.lineage.localeCompare(b.lineage),
27
+ );
28
+
29
+ const querySum = new GroupByOperator(child, 'lineage', (values) => ({
30
+ lineage: values[0].lineage,
31
+ n: values.reduce((a, b) => a + b.n, 0),
32
+ }));
33
+ const resultSum = await querySum.evaluate('lapis');
34
+ await expectEqualAfterSorting(
35
+ resultSum.content,
36
+ [
37
+ { lineage: 'A', n: 3 },
38
+ { lineage: 'B', n: 3 },
39
+ ],
40
+ (a, b) => a.lineage.localeCompare(b.lineage),
41
+ );
42
+ });
43
+ });
@@ -0,0 +1,32 @@
1
+ import { type Dataset } from './Dataset';
2
+ import { type Operator } from './Operator';
3
+
4
+ export class GroupByOperator<Data, AggregationResult, KeyToGroupBy extends keyof Data>
5
+ implements Operator<AggregationResult>
6
+ {
7
+ constructor(
8
+ private child: Operator<Data>,
9
+ private field: KeyToGroupBy,
10
+ private aggregate: (values: Data[]) => AggregationResult,
11
+ ) {}
12
+
13
+ async evaluate(lapis: string, signal?: AbortSignal): Promise<Dataset<AggregationResult>> {
14
+ const childEvaluated = await this.child.evaluate(lapis, signal);
15
+ const grouped = new Map<Data[KeyToGroupBy], Data[]>();
16
+ for (const row of childEvaluated.content) {
17
+ const key = row[this.field];
18
+ if (!grouped.has(key)) {
19
+ grouped.set(key, []);
20
+ }
21
+ grouped.get(key)!.push(row);
22
+ }
23
+ const result = new Array<AggregationResult>();
24
+ for (const [, values] of grouped) {
25
+ result.push(this.aggregate(values));
26
+ }
27
+
28
+ return {
29
+ content: result,
30
+ };
31
+ }
32
+ }
@@ -0,0 +1,13 @@
1
+ import { expect, describe, it } from 'vitest';
2
+
3
+ import { MapOperator } from './MapOperator';
4
+ import { MockOperator } from './MockOperator';
5
+
6
+ describe('MapOperator', () => {
7
+ it('should map the content of the child operator', async () => {
8
+ const child = new MockOperator([1, 2, 3]);
9
+ const query = new MapOperator(child, (x) => x * 2);
10
+ const result = await query.evaluate('lapis');
11
+ await expect(result.content).deep.equal([2, 4, 6]);
12
+ });
13
+ });
@@ -0,0 +1,16 @@
1
+ import { type Dataset } from './Dataset';
2
+ import { type Operator } from './Operator';
3
+
4
+ export class MapOperator<S, T> implements Operator<T> {
5
+ constructor(
6
+ private child: Operator<S>,
7
+ private func: (value: S) => T,
8
+ ) {}
9
+
10
+ async evaluate(lapis: string, signal?: AbortSignal): Promise<Dataset<T>> {
11
+ const childEvaluated = await this.child.evaluate(lapis, signal);
12
+ return {
13
+ content: childEvaluated.content.map(this.func),
14
+ };
15
+ }
16
+ }
@@ -0,0 +1,11 @@
1
+ import { expect, describe, it } from 'vitest';
2
+
3
+ import { MockOperator } from './MockOperator';
4
+
5
+ describe('MockOperator', () => {
6
+ it('should be a mock', async () => {
7
+ const query = new MockOperator([1, 2, 3]);
8
+ const result = await query.evaluate();
9
+ await expect(result.content).to.deep.equal([1, 2, 3]);
10
+ });
11
+ });
@@ -0,0 +1,12 @@
1
+ import { type Dataset } from './Dataset';
2
+ import { type Operator } from './Operator';
3
+
4
+ export class MockOperator<T> implements Operator<T> {
5
+ constructor(private content: T[]) {}
6
+
7
+ async evaluate(): Promise<Dataset<T>> {
8
+ return {
9
+ content: this.content,
10
+ };
11
+ }
12
+ }
@@ -0,0 +1,5 @@
1
+ import { type Dataset } from './Dataset';
2
+
3
+ export interface Operator<T> {
4
+ evaluate(lapis: string, signal?: AbortSignal): Promise<Dataset<T>>;
5
+ }
@@ -0,0 +1,52 @@
1
+ import { describe, it } from 'vitest';
2
+
3
+ import { MockOperator } from './MockOperator';
4
+ import { SlidingOperator } from './SlidingOperator';
5
+ import { expectEqualAfterSorting } from '../utils/test-utils';
6
+
7
+ const mockOperator = new MockOperator([
8
+ { id: 1, value: 1 },
9
+ { id: 2, value: 2 },
10
+ { id: 3, value: 3 },
11
+ { id: 4, value: 4 },
12
+ { id: 5, value: 5 },
13
+ ]);
14
+ describe('SlidingOperator', () => {
15
+ it('should slide the values', async () => {
16
+ const underTest = getSlidingOperatorWithWindowSize(3);
17
+
18
+ const result = await underTest.evaluate('lapis');
19
+
20
+ expectEqualAfterSorting(
21
+ result.content,
22
+ [
23
+ { id: 2, sum: 6 },
24
+ { id: 3, sum: 9 },
25
+ { id: 4, sum: 12 },
26
+ ],
27
+ sortById,
28
+ );
29
+ });
30
+
31
+ it('should return single value when window size is greater than number of entries', async () => {
32
+ const underTest = getSlidingOperatorWithWindowSize(999);
33
+
34
+ const result = await underTest.evaluate('lapis');
35
+
36
+ expectEqualAfterSorting(result.content, [{ id: 2, sum: 15 }], sortById);
37
+ });
38
+
39
+ function getSlidingOperatorWithWindowSize(windowSize: number) {
40
+ return new SlidingOperator(mockOperator, windowSize, (values) => {
41
+ let sum = 0;
42
+ for (const { value } of values) {
43
+ sum += value;
44
+ }
45
+ return { id: values[1].id, sum };
46
+ });
47
+ }
48
+
49
+ function sortById(a: { id: number }, b: { id: number }) {
50
+ return a.id - b.id;
51
+ }
52
+ });
@@ -0,0 +1,23 @@
1
+ import { type Operator } from './Operator';
2
+
3
+ export class SlidingOperator<Data, AggregationResult> implements Operator<AggregationResult> {
4
+ constructor(
5
+ private child: Operator<Data>,
6
+ private windowSize: number,
7
+ private aggregate: (values: Data[]) => AggregationResult,
8
+ ) {
9
+ if (windowSize < 1) {
10
+ throw new Error('Window size must be at least 1');
11
+ }
12
+ }
13
+
14
+ async evaluate(lapis: string, signal?: AbortSignal) {
15
+ const childEvaluated = await this.child.evaluate(lapis, signal);
16
+ const content = new Array<AggregationResult>();
17
+ const numberOfWindows = Math.max(childEvaluated.content.length - this.windowSize, 0) + 1;
18
+ for (let i = 0; i < numberOfWindows; i++) {
19
+ content.push(this.aggregate(childEvaluated.content.slice(i, i + this.windowSize)));
20
+ }
21
+ return { content };
22
+ }
23
+ }
@@ -0,0 +1,13 @@
1
+ import { expect, describe, it } from 'vitest';
2
+
3
+ import { MockOperator } from './MockOperator';
4
+ import { SortOperator } from './SortOperator';
5
+
6
+ describe('SortOperator', () => {
7
+ it('should sort the content of the child operator', async () => {
8
+ const child = new MockOperator([3, 1, 2]);
9
+ const query = new SortOperator(child, (a, b) => a - b);
10
+ const result = await query.evaluate('lapis');
11
+ await expect(result.content).deep.equal([1, 2, 3]);
12
+ });
13
+ });
@@ -0,0 +1,16 @@
1
+ import { type Dataset } from './Dataset';
2
+ import { type Operator } from './Operator';
3
+
4
+ export class SortOperator<S> implements Operator<S> {
5
+ constructor(
6
+ private child: Operator<S>,
7
+ private compareFn: (a: S, b: S) => number,
8
+ ) {}
9
+
10
+ async evaluate(lapis: string, signal?: AbortSignal): Promise<Dataset<S>> {
11
+ const childEvaluated = await this.child.evaluate(lapis, signal);
12
+ return {
13
+ content: childEvaluated.content.sort(this.compareFn),
14
+ };
15
+ }
16
+ }
@@ -0,0 +1,3 @@
1
+ import { createContext } from 'preact';
2
+
3
+ export const LapisUrlContext = createContext('');
@@ -0,0 +1,5 @@
1
+ import { createContext } from 'preact';
2
+
3
+ import { type ReferenceGenome } from '../lapisApi/ReferenceGenome';
4
+
5
+ export const ReferenceGenomeContext = createContext<ReferenceGenome>({ nucleotideSequences: [], genes: [] });
@@ -0,0 +1,62 @@
1
+ import { type FunctionComponent } from 'preact';
2
+ import { useContext, useState } from 'preact/hooks';
3
+
4
+ import { type CheckboxItem, CheckboxSelector } from './checkbox-selector';
5
+ import { getSegmentNames } from '../../lapisApi/ReferenceGenome';
6
+ import { type SequenceType } from '../../types';
7
+ import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
8
+
9
+ export type DisplayedSegment = CheckboxItem & {
10
+ segment: string;
11
+ };
12
+
13
+ export type SegmentSelectorProps = {
14
+ displayedSegments: DisplayedSegment[];
15
+ setDisplayedSegments: (items: DisplayedSegment[]) => void;
16
+ prefix?: string;
17
+ };
18
+
19
+ const getSegmentSelectorLabel = (displayedSegments: DisplayedSegment[], prefix: string) => {
20
+ const allSelectedSelected = displayedSegments
21
+ .filter((segment) => segment.checked)
22
+ .map((segment) => segment.segment);
23
+
24
+ if (allSelectedSelected.length === 0) {
25
+ return `${prefix}none`;
26
+ }
27
+ if (displayedSegments.length === allSelectedSelected.length) {
28
+ return `${prefix}all`;
29
+ }
30
+ return prefix + allSelectedSelected.join(', ');
31
+ };
32
+
33
+ export const SegmentSelector: FunctionComponent<SegmentSelectorProps> = ({
34
+ displayedSegments,
35
+ setDisplayedSegments,
36
+ prefix,
37
+ }) => {
38
+ if (displayedSegments.length <= 1) {
39
+ return null;
40
+ }
41
+
42
+ return (
43
+ <CheckboxSelector
44
+ className='mx-1'
45
+ items={displayedSegments}
46
+ label={getSegmentSelectorLabel(displayedSegments, prefix || 'Segments: ')}
47
+ setItems={(items) => setDisplayedSegments(items)}
48
+ />
49
+ );
50
+ };
51
+
52
+ export function useDisplayedSegments(sequenceType: SequenceType) {
53
+ const referenceGenome = useContext(ReferenceGenomeContext);
54
+
55
+ const displayedSegments = getSegmentNames(referenceGenome, sequenceType).map((segment) => ({
56
+ segment,
57
+ label: segment,
58
+ checked: true,
59
+ }));
60
+
61
+ return useState<DisplayedSegment[]>(displayedSegments);
62
+ }
@@ -0,0 +1,42 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { Chart, registerables } from 'chart.js';
3
+
4
+ import GsChart, { type GsChartProps } from './chart';
5
+ import { LogitScale } from '../shared/charts/LogitScale';
6
+ import { getYAxisScale } from '../shared/charts/getYAxisScale';
7
+
8
+ const meta: Meta<GsChartProps> = {
9
+ title: 'Component/Chart',
10
+ component: GsChart,
11
+ parameters: { fetchMock: {} },
12
+ };
13
+
14
+ export default meta;
15
+
16
+ Chart.register(...registerables, LogitScale);
17
+
18
+ export const ChartStory: StoryObj<GsChartProps> = {
19
+ render: (args) => {
20
+ return <GsChart configuration={args.configuration} />;
21
+ },
22
+ args: {
23
+ configuration: {
24
+ type: 'line',
25
+ data: {
26
+ datasets: [
27
+ {
28
+ label: 'Dataset 1',
29
+ data: [1, 2, 3, 4, 5],
30
+ },
31
+ ],
32
+ labels: ['A', 'B', 'C', 'D', 'E'],
33
+ },
34
+ options: {
35
+ animation: false,
36
+ scales: {
37
+ y: getYAxisScale('logarithmic'),
38
+ },
39
+ },
40
+ },
41
+ },
42
+ };
@@ -0,0 +1,32 @@
1
+ import { Chart, type ChartConfiguration } from 'chart.js';
2
+ import { useEffect, useRef } from 'preact/hooks';
3
+
4
+ export interface GsChartProps {
5
+ configuration: ChartConfiguration;
6
+ }
7
+
8
+ const GsChart = ({ configuration }: GsChartProps) => {
9
+ const canvasRef = useRef<HTMLCanvasElement>(null);
10
+ const chartRef = useRef<Chart | null>(null);
11
+
12
+ useEffect(() => {
13
+ if (canvasRef.current === null) {
14
+ return;
15
+ }
16
+
17
+ const ctx = canvasRef.current.getContext('2d');
18
+ if (ctx === null) {
19
+ return;
20
+ }
21
+
22
+ chartRef.current = new Chart(ctx, configuration);
23
+
24
+ return () => {
25
+ chartRef.current?.destroy();
26
+ };
27
+ }, [canvasRef, configuration]);
28
+
29
+ return <canvas ref={canvasRef} />;
30
+ };
31
+
32
+ export default GsChart;
@@ -0,0 +1,56 @@
1
+ import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, fn, waitFor, within } from '@storybook/test';
3
+
4
+ import { type CheckboxItem, CheckboxSelector, type CheckboxSelectorProps } from './checkbox-selector';
5
+
6
+ const meta: Meta<CheckboxSelectorProps> = {
7
+ title: 'Component/Checkbox Selector',
8
+ component: CheckboxSelector,
9
+ argTypes: {
10
+ setItems: { action: true },
11
+ },
12
+ parameters: { fetchMock: {} },
13
+ };
14
+
15
+ export default meta;
16
+
17
+ export const CheckboxSelectorStory: StoryObj<CheckboxSelectorProps> = {
18
+ render: (args) => {
19
+ let wrapperStateItems = args.items;
20
+
21
+ return (
22
+ <CheckboxSelector
23
+ items={wrapperStateItems}
24
+ label={args.label}
25
+ setItems={(items: CheckboxItem[]) => {
26
+ args.setItems(items);
27
+ wrapperStateItems = items;
28
+ }}
29
+ />
30
+ );
31
+ },
32
+ args: {
33
+ items: [
34
+ { checked: false, label: 'item1' },
35
+ { checked: false, label: 'item2' },
36
+ ],
37
+ label: 'Some label',
38
+ setItems: fn(),
39
+ },
40
+ play: async ({ canvasElement, args }) => {
41
+ const canvas = within(canvasElement);
42
+
43
+ const open = () => canvas.getByText('Some label', { exact: false });
44
+ open().click();
45
+
46
+ const item1 = canvas.getByLabelText('item1', { exact: false });
47
+ item1.click();
48
+
49
+ await waitFor(() =>
50
+ expect(args.setItems).toHaveBeenCalledWith([
51
+ { checked: true, label: 'item1' },
52
+ { checked: false, label: 'item2' },
53
+ ]),
54
+ );
55
+ },
56
+ };
@@ -0,0 +1,46 @@
1
+ export type CheckboxItem = {
2
+ label: string;
3
+ checked: boolean;
4
+ };
5
+
6
+ export interface CheckboxSelectorProps<Item extends CheckboxItem = CheckboxItem> {
7
+ className?: string;
8
+ items: Item[];
9
+ label: string;
10
+ setItems: (items: Item[]) => void;
11
+ }
12
+
13
+ export const CheckboxSelector = <Item extends CheckboxItem>({
14
+ className,
15
+ items,
16
+ label,
17
+ setItems,
18
+ }: CheckboxSelectorProps<Item>) => {
19
+ return (
20
+ <div class={`dropdown ${className}`}>
21
+ <div tabIndex={0} role='button' class='btn btn-xs text-nowrap'>
22
+ {label}
23
+ </div>
24
+ <ul tabIndex={0} class='p-2 shadow menu dropdown-content z-[1] bg-base-100 rounded-box'>
25
+ {items.map((item, index) => (
26
+ <li class='flex flex-row items-center' key={item.label}>
27
+ <label>
28
+ <input
29
+ type='checkbox'
30
+ id={`item-${index}`}
31
+ checked={item.checked}
32
+ onChange={() => {
33
+ const newItems = items.map((item, i) =>
34
+ i === index ? { ...item, checked: !item.checked } : item,
35
+ );
36
+ setItems(newItems);
37
+ }}
38
+ />
39
+ {item.label}
40
+ </label>
41
+ </li>
42
+ ))}
43
+ </ul>
44
+ </div>
45
+ );
46
+ };
@@ -0,0 +1,45 @@
1
+ import { type FunctionComponent } from 'preact';
2
+
3
+ import { Select } from './select';
4
+ import type { ConfidenceIntervalMethod } from '../shared/charts/confideceInterval';
5
+
6
+ type ConfidenceIntervalSelectorProps = {
7
+ confidenceIntervalMethod: ConfidenceIntervalMethod;
8
+ setConfidenceIntervalMethod: (method: ConfidenceIntervalMethod) => void;
9
+ confidenceIntervalMethods: ConfidenceIntervalMethod[];
10
+ };
11
+
12
+ export const ConfidenceIntervalSelector: FunctionComponent<ConfidenceIntervalSelectorProps> = ({
13
+ confidenceIntervalMethod,
14
+ setConfidenceIntervalMethod,
15
+ confidenceIntervalMethods,
16
+ }) => {
17
+ if (confidenceIntervalMethods.length === 0) {
18
+ return null;
19
+ }
20
+
21
+ const items = [
22
+ { label: 'Confidence interval method', value: 'none', disabled: true },
23
+ ...confidenceIntervalMethods.concat('none').map((method) => {
24
+ switch (method) {
25
+ case 'wilson':
26
+ return { label: 'Wilson, 95% CI', value: 'wilson' };
27
+ case 'none':
28
+ return { label: 'None', value: 'none' };
29
+ }
30
+ }),
31
+ ];
32
+
33
+ return (
34
+ <Select
35
+ items={items}
36
+ selected={confidenceIntervalMethod === undefined ? 'none' : confidenceIntervalMethod}
37
+ onChange={(event: Event) => {
38
+ const select = event.target as HTMLSelectElement;
39
+ const value = select.value as ConfidenceIntervalMethod;
40
+ setConfidenceIntervalMethod(value);
41
+ }}
42
+ selectStyle={'select-xs select-bordered'}
43
+ />
44
+ );
45
+ };