@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.
- package/LICENSE +661 -0
- package/README.md +109 -0
- package/custom-elements.json +1587 -0
- package/dist/dashboard-components.js +7322 -0
- package/dist/dashboard-components.js.map +1 -0
- package/dist/genspectrum-components.d.ts +298 -0
- package/dist/style.css +2930 -0
- package/package.json +109 -0
- package/src/constants.ts +6 -0
- package/src/index.ts +1 -0
- package/src/lapisApi/ReferenceGenome.ts +30 -0
- package/src/lapisApi/__mockData__/referenceGenome.json +58 -0
- package/src/lapisApi/lapisApi.ts +99 -0
- package/src/lapisApi/lapisTypes.ts +51 -0
- package/src/operator/Dataset.ts +3 -0
- package/src/operator/DivisionOperator.spec.ts +27 -0
- package/src/operator/DivisionOperator.ts +60 -0
- package/src/operator/FetchAggregatedOperator.ts +44 -0
- package/src/operator/FetchInsertionsOperator.ts +24 -0
- package/src/operator/FetchSubstitutionsOrDeletionsOperator.ts +49 -0
- package/src/operator/FillMissingOperator.spec.ts +26 -0
- package/src/operator/FillMissingOperator.ts +30 -0
- package/src/operator/GroupByAndSumOperator.spec.ts +26 -0
- package/src/operator/GroupByAndSumOperator.ts +26 -0
- package/src/operator/GroupByOperator.spec.ts +43 -0
- package/src/operator/GroupByOperator.ts +32 -0
- package/src/operator/MapOperator.spec.ts +13 -0
- package/src/operator/MapOperator.ts +16 -0
- package/src/operator/MockOperator.spec.ts +11 -0
- package/src/operator/MockOperator.ts +12 -0
- package/src/operator/Operator.ts +5 -0
- package/src/operator/SlidingOperator.spec.ts +52 -0
- package/src/operator/SlidingOperator.ts +23 -0
- package/src/operator/SortOperator.spec.ts +13 -0
- package/src/operator/SortOperator.ts +16 -0
- package/src/preact/LapisUrlContext.ts +3 -0
- package/src/preact/ReferenceGenomeContext.ts +5 -0
- package/src/preact/components/SegmentSelector.tsx +62 -0
- package/src/preact/components/chart.stories.tsx +42 -0
- package/src/preact/components/chart.tsx +32 -0
- package/src/preact/components/checkbox-selector.stories.tsx +56 -0
- package/src/preact/components/checkbox-selector.tsx +46 -0
- package/src/preact/components/confidence-interval-selector.tsx +45 -0
- package/src/preact/components/csv-download-button.stories.tsx +25 -0
- package/src/preact/components/csv-download-button.tsx +51 -0
- package/src/preact/components/error-display.stories.tsx +22 -0
- package/src/preact/components/error-display.tsx +5 -0
- package/src/preact/components/headline.stories.tsx +29 -0
- package/src/preact/components/headline.tsx +16 -0
- package/src/preact/components/info.stories.tsx +22 -0
- package/src/preact/components/info.tsx +16 -0
- package/src/preact/components/loading-display.stories.tsx +20 -0
- package/src/preact/components/loading-display.tsx +5 -0
- package/src/preact/components/min-max-percent-slider.css +40 -0
- package/src/preact/components/min-max-range-slider.tsx +95 -0
- package/src/preact/components/mutation-type-selector.tsx +30 -0
- package/src/preact/components/no-data-display.stories.tsx +20 -0
- package/src/preact/components/no-data-display.tsx +5 -0
- package/src/preact/components/percent-intput.tsx +49 -0
- package/src/preact/components/proportion-selector-dropdown.stories.tsx +66 -0
- package/src/preact/components/proportion-selector-dropdown.tsx +33 -0
- package/src/preact/components/proportion-selector.stories.tsx +81 -0
- package/src/preact/components/proportion-selector.tsx +43 -0
- package/src/preact/components/scaling-selector.stories.tsx +25 -0
- package/src/preact/components/scaling-selector.tsx +36 -0
- package/src/preact/components/select.stories.tsx +42 -0
- package/src/preact/components/select.tsx +21 -0
- package/src/preact/components/table.stories.tsx +24 -0
- package/src/preact/components/table.tsx +51 -0
- package/src/preact/components/tabs.stories.tsx +60 -0
- package/src/preact/components/tabs.tsx +49 -0
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +32 -0
- package/src/preact/dateRangeSelector/date-range-selector.tsx +228 -0
- package/src/preact/dateRangeSelector/dateConversion.ts +8 -0
- package/src/preact/locationFilter/__mockData__/aggregated.json +775 -0
- package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +36 -0
- package/src/preact/locationFilter/fetchAutocompletionList.ts +43 -0
- package/src/preact/locationFilter/location-filter.stories.tsx +50 -0
- package/src/preact/locationFilter/location-filter.tsx +112 -0
- package/src/preact/mutationComparison/__mockData__/nucleotideMutationsOtherVariant.json +295 -0
- package/src/preact/mutationComparison/__mockData__/nucleotideMutationsSomeVariant.json +304 -0
- package/src/preact/mutationComparison/fetchMutationData.spec.ts +118 -0
- package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +125 -0
- package/src/preact/mutationComparison/getMutationComparisonTableData.ts +40 -0
- package/src/preact/mutationComparison/mutation-comparison-table.tsx +43 -0
- package/src/preact/mutationComparison/mutation-comparison-venn.tsx +122 -0
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +152 -0
- package/src/preact/mutationComparison/mutation-comparison.tsx +179 -0
- package/src/preact/mutationComparison/queryMutationData.ts +53 -0
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +164 -0
- package/src/preact/mutationFilter/mutation-filter.tsx +268 -0
- package/src/preact/mutationFilter/parseAndValidateMutation.ts +54 -0
- package/src/preact/mutationFilter/parseMutation.spec.ts +150 -0
- package/src/preact/mutationFilter/sequenceTypeFromSegment.spec.ts +66 -0
- package/src/preact/mutationFilter/sequenceTypeFromSegment.ts +20 -0
- package/src/preact/mutations/__mockData__/nucleotideInsertions.json +252 -0
- package/src/preact/mutations/__mockData__/nucleotideMutations.json +880 -0
- package/src/preact/mutations/getInsertionsTableData.spec.ts +36 -0
- package/src/preact/mutations/getInsertionsTableData.ts +10 -0
- package/src/preact/mutations/getMutationsGridData.spec.ts +135 -0
- package/src/preact/mutations/getMutationsGridData.ts +92 -0
- package/src/preact/mutations/getMutationsTableData.spec.ts +94 -0
- package/src/preact/mutations/getMutationsTableData.ts +17 -0
- package/src/preact/mutations/mutations-grid.tsx +84 -0
- package/src/preact/mutations/mutations-insertions-table.tsx +33 -0
- package/src/preact/mutations/mutations-table.tsx +47 -0
- package/src/preact/mutations/mutations.stories.tsx +95 -0
- package/src/preact/mutations/mutations.tsx +192 -0
- package/src/preact/mutations/queryMutations.ts +55 -0
- package/src/preact/prevalenceOverTime/__mockData__/denominator.json +1700 -0
- package/src/preact/prevalenceOverTime/__mockData__/denominatorOneVariant.json +608 -0
- package/src/preact/prevalenceOverTime/__mockData__/numeratorEG.json +1560 -0
- package/src/preact/prevalenceOverTime/__mockData__/numeratorJN1.json +592 -0
- package/src/preact/prevalenceOverTime/__mockData__/numeratorOneVariant.json +604 -0
- package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.spec.ts +67 -0
- package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.ts +18 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +105 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +86 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +141 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-table.tsx +46 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +165 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +202 -0
- package/src/preact/relativeGrowthAdvantage/__mockData__/denominator.json +376 -0
- package/src/preact/relativeGrowthAdvantage/__mockData__/numerator.json +332 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +138 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +71 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +136 -0
- package/src/preact/shared/charts/LogitScale.ts +48 -0
- package/src/preact/shared/charts/colors.ts +26 -0
- package/src/preact/shared/charts/confideceInterval.ts +29 -0
- package/src/preact/shared/charts/getYAxisScale.ts +16 -0
- package/src/preact/shared/charts/scales.ts +16 -0
- package/src/preact/shared/icons/DeleteIcon.tsx +17 -0
- package/src/preact/shared/sort/sortInsertions.spec.ts +47 -0
- package/src/preact/shared/sort/sortInsertions.ts +21 -0
- package/src/preact/shared/sort/sortMutationPositions.spec.ts +31 -0
- package/src/preact/shared/sort/sortMutationPositions.ts +14 -0
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +47 -0
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +17 -0
- package/src/preact/shared/table/formatProportion.ts +3 -0
- package/src/preact/textInput/__mockData__/aggregated_hosts.json +24 -0
- package/src/preact/textInput/fetchAutocompleteList.ts +9 -0
- package/src/preact/textInput/text-input.stories.tsx +49 -0
- package/src/preact/textInput/text-input.tsx +73 -0
- package/src/preact/useQuery.ts +27 -0
- package/src/query/queryInsertions.ts +14 -0
- package/src/query/queryPrevalenceOverTime.ts +126 -0
- package/src/query/queryRelativeGrowthAdvantage.ts +131 -0
- package/src/query/querySubstitutionsOrDeletions.ts +19 -0
- package/src/styles/tailwind.css +3 -0
- package/src/styles/tailwind.d.ts +3 -0
- package/src/types.ts +23 -0
- package/src/utils/mutations.spec.ts +64 -0
- package/src/utils/mutations.ts +165 -0
- package/src/utils/temporal.spec.ts +97 -0
- package/src/utils/temporal.ts +348 -0
- package/src/utils/test-utils.ts +5 -0
- package/src/utils/type-utils.ts +15 -0
- package/src/utils/utils.spec.ts +16 -0
- package/src/utils/utils.ts +38 -0
- package/src/web-components/PreactLitAdapter.tsx +62 -0
- package/src/web-components/PreactLitAdapterWithGridJsStyles.tsx +12 -0
- package/src/web-components/app.ts +51 -0
- package/src/web-components/display/index.ts +4 -0
- package/src/web-components/display/mutation-comparison-component.stories.ts +138 -0
- package/src/web-components/display/mutation-comparison-component.tsx +31 -0
- package/src/web-components/display/mutations-component.stories.ts +107 -0
- package/src/web-components/display/mutations-component.tsx +27 -0
- package/src/web-components/display/prevalence-over-time-component.stories.ts +205 -0
- package/src/web-components/display/prevalence-over-time-component.tsx +46 -0
- package/src/web-components/display/relative-growth-advantage-component.stories.ts +89 -0
- package/src/web-components/display/relative-growth-advantage-component.tsx +37 -0
- package/src/web-components/index.ts +3 -0
- package/src/web-components/input/date-range-selector-component.stories.ts +53 -0
- package/src/web-components/input/date-range-selector-component.tsx +33 -0
- package/src/web-components/input/index.ts +4 -0
- package/src/web-components/input/location-filter-component.stories.ts +184 -0
- package/src/web-components/input/location-filter-component.tsx +68 -0
- package/src/web-components/input/location-filter.mdx +25 -0
- package/src/web-components/input/mutation-filter-component.stories.ts +97 -0
- package/src/web-components/input/mutation-filter-component.tsx +27 -0
- package/src/web-components/input/text-input-component.stories.ts +92 -0
- package/src/web-components/input/text-input-component.tsx +30 -0
- package/src/web-components/lapis-context.ts +3 -0
- package/src/web-components/reference-genome-context.ts +5 -0
- package/src/web-components/withinShadowRoot.story.ts +34 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type Chart, type CoreScaleOptions, Scale } from 'chart.js';
|
|
2
|
+
|
|
3
|
+
export class LogitScale extends Scale {
|
|
4
|
+
static id = 'logit';
|
|
5
|
+
|
|
6
|
+
constructor(cfg: { id: string; type: string; ctx: CanvasRenderingContext2D; chart: Chart }) {
|
|
7
|
+
super(cfg);
|
|
8
|
+
this.min = 0.001;
|
|
9
|
+
this.max = 0.999;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
override buildTicks() {
|
|
13
|
+
const tickValues = [this.min, 0.01, 0.1, 0.5, 0.9, 0.99, this.max];
|
|
14
|
+
|
|
15
|
+
this.ticks = tickValues.map((value) => ({ value }));
|
|
16
|
+
return this.ticks;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getLogit(value: number): number {
|
|
20
|
+
if (value <= 0) {
|
|
21
|
+
return -Infinity;
|
|
22
|
+
}
|
|
23
|
+
if (value >= 1) {
|
|
24
|
+
return Infinity;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return Math.log(value / (1 - value));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
override getPixelForValue(value: number): number {
|
|
31
|
+
const logitMin = this.getLogit(this.min);
|
|
32
|
+
const logitMax = this.getLogit(this.max);
|
|
33
|
+
|
|
34
|
+
const logitValue = this.getLogit(value);
|
|
35
|
+
|
|
36
|
+
const decimal = (logitValue - logitMin) / (logitMax - logitMin);
|
|
37
|
+
|
|
38
|
+
return this.getPixelForDecimal(decimal);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare module 'chart.js' {
|
|
43
|
+
export interface CartesianScaleTypeRegistry {
|
|
44
|
+
logit: {
|
|
45
|
+
options: CoreScaleOptions;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// colorblind friendly colors taken from https://personal.sron.nl/~pault/
|
|
2
|
+
|
|
3
|
+
const ColorsRGB = {
|
|
4
|
+
indigo: [51, 34, 136],
|
|
5
|
+
green: [17, 119, 51],
|
|
6
|
+
cyan: [136, 204, 238],
|
|
7
|
+
teal: [68, 170, 153],
|
|
8
|
+
olive: [153, 153, 51],
|
|
9
|
+
sand: [221, 204, 119],
|
|
10
|
+
rose: [204, 102, 119],
|
|
11
|
+
wine: [136, 34, 85],
|
|
12
|
+
purple: [170, 68, 153],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type GraphColor = keyof typeof ColorsRGB;
|
|
16
|
+
|
|
17
|
+
export const singleGraphColorRGBAById = (id: number, alpha = 1) => {
|
|
18
|
+
const keys = Object.keys(ColorsRGB) as GraphColor[];
|
|
19
|
+
const key = keys[id % keys.length];
|
|
20
|
+
|
|
21
|
+
return `rgba(${ColorsRGB[key].join(',')},${alpha})`;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const singleGraphColorRGBByName = (name: GraphColor, alpha = 1) => {
|
|
25
|
+
return `rgba(${ColorsRGB[name].join(',')},${alpha})`;
|
|
26
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// calculateWilsonInterval calculates the Wilson score interval for 95% confidence.
|
|
2
|
+
//
|
|
3
|
+
// This function is based on https://github.com/erikfox/wilson-interval, but without
|
|
4
|
+
// high precision math.
|
|
5
|
+
//
|
|
6
|
+
// observed - number of observed positive outcomes
|
|
7
|
+
// sample - number of experiments or size of the sample
|
|
8
|
+
export function wilson95PercentConfidenceInterval(observed: number, sample: number) {
|
|
9
|
+
const p = observed / sample;
|
|
10
|
+
const n = sample;
|
|
11
|
+
const z = 1.9599639715843482;
|
|
12
|
+
|
|
13
|
+
const numerator = p + (z * z) / (2 * n);
|
|
14
|
+
const w_plus_minus = z * Math.sqrt((p * (1 - p)) / n + (z * z) / (4 * (n * n)));
|
|
15
|
+
const denominator = 1 + (z * z) / n;
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
lowerLimit: (numerator - w_plus_minus) / denominator,
|
|
19
|
+
upperLimit: (numerator + w_plus_minus) / denominator,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const confidenceIntervalDataLabel = (value: number, lowerLimit: number, upperLimit: number, prefix?: string) => {
|
|
24
|
+
const label = prefix ? `${prefix}: ` : '';
|
|
25
|
+
|
|
26
|
+
return `${label}${value.toFixed(3)} (${lowerLimit.toFixed(3)} - ${upperLimit.toFixed(3)})`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ConfidenceIntervalMethod = 'wilson' | 'none';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ScaleType = 'linear' | 'logarithmic' | 'logit';
|
|
2
|
+
|
|
3
|
+
export function getYAxisScale(scaleType: ScaleType) {
|
|
4
|
+
switch (scaleType) {
|
|
5
|
+
case 'linear': {
|
|
6
|
+
return { beginAtZero: true, type: 'linear' as const, min: 0, max: 1 };
|
|
7
|
+
}
|
|
8
|
+
case 'logarithmic': {
|
|
9
|
+
return { type: 'logarithmic' as const };
|
|
10
|
+
}
|
|
11
|
+
case 'logit':
|
|
12
|
+
return { type: 'logit' as const };
|
|
13
|
+
default:
|
|
14
|
+
return { beginAtZero: true, type: 'linear' as const };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ScaleType = 'linear' | 'logarithmic' | 'logit';
|
|
2
|
+
|
|
3
|
+
export function getYAxisScale(scaleType: ScaleType) {
|
|
4
|
+
switch (scaleType) {
|
|
5
|
+
case 'linear': {
|
|
6
|
+
return { beginAtZero: true, type: 'linear' as const };
|
|
7
|
+
}
|
|
8
|
+
case 'logarithmic': {
|
|
9
|
+
return { type: 'logarithmic' as const };
|
|
10
|
+
}
|
|
11
|
+
case 'logit':
|
|
12
|
+
return { type: 'logit' as const };
|
|
13
|
+
default:
|
|
14
|
+
return { beginAtZero: true, type: 'linear' as const };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FunctionComponent } from 'preact';
|
|
2
|
+
|
|
3
|
+
export const DeleteIcon: FunctionComponent = () => {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
fill='currentColor'
|
|
7
|
+
stroke-width='0'
|
|
8
|
+
xmlns='http://www.w3.org/2000/svg'
|
|
9
|
+
viewBox='0 0 16 16'
|
|
10
|
+
style='overflow: visible; color: currentcolor;'
|
|
11
|
+
height='1em'
|
|
12
|
+
width='1em'
|
|
13
|
+
>
|
|
14
|
+
<path d='M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z' />
|
|
15
|
+
</svg>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { sortInsertions } from './sortInsertions';
|
|
4
|
+
|
|
5
|
+
describe('sortInsertions with no segments', () => {
|
|
6
|
+
test('should sort for positions first', () => {
|
|
7
|
+
const a = 'ins_1:A';
|
|
8
|
+
const b = 'ins_2:A';
|
|
9
|
+
|
|
10
|
+
expect(sortInsertions(a, b)).toBeLessThan(0);
|
|
11
|
+
expect(sortInsertions(b, a)).toBeGreaterThan(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('should sort for symbols second', () => {
|
|
15
|
+
const a = 'ins_1:A';
|
|
16
|
+
const b = 'ins_1:B';
|
|
17
|
+
|
|
18
|
+
expect(sortInsertions(a, b)).toBeLessThan(0);
|
|
19
|
+
expect(sortInsertions(b, a)).toBeGreaterThan(0);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('sortInsertions with segments', () => {
|
|
24
|
+
test('should sort for segments first', () => {
|
|
25
|
+
const a = 'ins_AA1:1:A';
|
|
26
|
+
const b = 'ins_BB2:1:A';
|
|
27
|
+
|
|
28
|
+
expect(sortInsertions(a, b)).toBeLessThan(0);
|
|
29
|
+
expect(sortInsertions(b, a)).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('should sort for positions second', () => {
|
|
33
|
+
const a = 'ins_AA1:1:A';
|
|
34
|
+
const b = 'ins_AA1:2:A';
|
|
35
|
+
|
|
36
|
+
expect(sortInsertions(a, b)).toBeLessThan(0);
|
|
37
|
+
expect(sortInsertions(b, a)).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should sort for symbols third', () => {
|
|
41
|
+
const a = 'ins_AA1:1:A';
|
|
42
|
+
const b = 'ins_AA1:1:B';
|
|
43
|
+
|
|
44
|
+
expect(sortInsertions(a, b)).toBeLessThan(0);
|
|
45
|
+
expect(sortInsertions(b, a)).toBeGreaterThan(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Insertion } from '../../../utils/mutations';
|
|
2
|
+
|
|
3
|
+
export const sortInsertions = (a: string, b: string) => {
|
|
4
|
+
const insertionA = Insertion.parse(a);
|
|
5
|
+
const insertionB = Insertion.parse(b);
|
|
6
|
+
|
|
7
|
+
if (insertionA && insertionB) {
|
|
8
|
+
const segmentA = insertionA.segment;
|
|
9
|
+
const segmentB = insertionB.segment;
|
|
10
|
+
if (segmentA !== undefined && segmentB !== undefined && segmentA !== segmentB) {
|
|
11
|
+
return segmentA.localeCompare(segmentB);
|
|
12
|
+
}
|
|
13
|
+
const positionA = insertionA.position;
|
|
14
|
+
const positionB = insertionB.position;
|
|
15
|
+
if (positionA !== positionB) {
|
|
16
|
+
return positionA - positionB;
|
|
17
|
+
}
|
|
18
|
+
return insertionA.insertedSymbols.localeCompare(insertionB.insertedSymbols);
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Invalid insertion: ${a} or ${b}`);
|
|
21
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { sortMutationPositions } from './sortMutationPositions';
|
|
4
|
+
|
|
5
|
+
describe('sortMutationPositions with no segments', () => {
|
|
6
|
+
test('should sort for positions', () => {
|
|
7
|
+
const a = '1';
|
|
8
|
+
const b = '2';
|
|
9
|
+
|
|
10
|
+
expect(sortMutationPositions(a, b)).toBeLessThan(0);
|
|
11
|
+
expect(sortMutationPositions(b, a)).toBeGreaterThan(0);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('sortMutationPositions with segments', () => {
|
|
16
|
+
test('should sort for segments first', () => {
|
|
17
|
+
const a = 'AA1:1';
|
|
18
|
+
const b = 'BB2:1';
|
|
19
|
+
|
|
20
|
+
expect(sortMutationPositions(a, b)).toBeLessThan(0);
|
|
21
|
+
expect(sortMutationPositions(b, a)).toBeGreaterThan(0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should sort for positions second', () => {
|
|
25
|
+
const a = 'ins_AA1:1';
|
|
26
|
+
const b = 'ins_AA1:2';
|
|
27
|
+
|
|
28
|
+
expect(sortMutationPositions(a, b)).toBeLessThan(0);
|
|
29
|
+
expect(sortMutationPositions(b, a)).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const pattern = /(?:([A-Za-z0-9]+):)?(\d+)$/;
|
|
2
|
+
|
|
3
|
+
export const sortMutationPositions = (a: string, b: string) => {
|
|
4
|
+
const aMatch = a.match(pattern);
|
|
5
|
+
const bMatch = b.match(pattern);
|
|
6
|
+
|
|
7
|
+
if (aMatch && bMatch) {
|
|
8
|
+
if (aMatch[1] !== bMatch[1]) {
|
|
9
|
+
return aMatch[1].localeCompare(bMatch[1]);
|
|
10
|
+
}
|
|
11
|
+
return parseInt(aMatch[2], 10) - parseInt(bMatch[2], 10);
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Invalid mutation position: ${a} or ${b}`);
|
|
14
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { sortSubstitutionsAndDeletions } from './sortSubstitutionsAndDeletions';
|
|
4
|
+
|
|
5
|
+
describe('sortSubstitutionsAndDeletions with no segments', () => {
|
|
6
|
+
test('should sort for positions first', () => {
|
|
7
|
+
const a = 'A123B';
|
|
8
|
+
const b = 'A234B';
|
|
9
|
+
|
|
10
|
+
expect(sortSubstitutionsAndDeletions(a, b)).toBeLessThan(0);
|
|
11
|
+
expect(sortSubstitutionsAndDeletions(b, a)).toBeGreaterThan(0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('should sort for substitutionValue second', () => {
|
|
15
|
+
const a = 'A123A';
|
|
16
|
+
const b = 'A123B';
|
|
17
|
+
|
|
18
|
+
expect(sortSubstitutionsAndDeletions(a, b)).toBeLessThan(0);
|
|
19
|
+
expect(sortSubstitutionsAndDeletions(b, a)).toBeGreaterThan(0);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('sortSubstitutionsAndDeletions with segments', () => {
|
|
24
|
+
test('should sort for segment first', () => {
|
|
25
|
+
const a = 'AA1:A123B';
|
|
26
|
+
const b = 'BB1:A234B';
|
|
27
|
+
|
|
28
|
+
expect(sortSubstitutionsAndDeletions(a, b)).toBeLessThan(0);
|
|
29
|
+
expect(sortSubstitutionsAndDeletions(b, a)).toBeGreaterThan(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('should sort for position second', () => {
|
|
33
|
+
const a = 'AA1:A123B';
|
|
34
|
+
const b = 'AA1:A234B';
|
|
35
|
+
|
|
36
|
+
expect(sortSubstitutionsAndDeletions(a, b)).toBeLessThan(0);
|
|
37
|
+
expect(sortSubstitutionsAndDeletions(b, a)).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should sort for substitutionValue third', () => {
|
|
41
|
+
const a = 'AA1:A123A';
|
|
42
|
+
const b = 'AA1:A123B';
|
|
43
|
+
|
|
44
|
+
expect(sortSubstitutionsAndDeletions(a, b)).toBeLessThan(0);
|
|
45
|
+
expect(sortSubstitutionsAndDeletions(b, a)).toBeGreaterThan(0);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const substitutionAndDeletionRegex = /(?:([A-Za-z0-9]+):)?([A-Za-z])(\d+)([A-Za-z]|-|\*)/;
|
|
2
|
+
|
|
3
|
+
export const sortSubstitutionsAndDeletions = (a: string, b: string) => {
|
|
4
|
+
const aMatch = a.match(substitutionAndDeletionRegex);
|
|
5
|
+
const bMatch = b.match(substitutionAndDeletionRegex);
|
|
6
|
+
|
|
7
|
+
if (aMatch && bMatch) {
|
|
8
|
+
if (aMatch[1] !== bMatch[1]) {
|
|
9
|
+
return aMatch[1].localeCompare(bMatch[1]);
|
|
10
|
+
}
|
|
11
|
+
if (aMatch[3] !== bMatch[3]) {
|
|
12
|
+
return parseInt(aMatch[3], 10) - parseInt(bMatch[3], 10);
|
|
13
|
+
}
|
|
14
|
+
return aMatch[4].localeCompare(bMatch[4]);
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`Invalid substitution or deletion: ${a} or ${b}`);
|
|
17
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"errors": [],
|
|
3
|
+
"info": {
|
|
4
|
+
"apiVersion": 1,
|
|
5
|
+
"dataVersion": 1709685650,
|
|
6
|
+
"deprecationDate": null,
|
|
7
|
+
"deprecationInfo": null,
|
|
8
|
+
"acknowledgement": null
|
|
9
|
+
},
|
|
10
|
+
"data": [
|
|
11
|
+
{
|
|
12
|
+
"host": "Homo",
|
|
13
|
+
"count": 123
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"host": "Homo sapiens",
|
|
17
|
+
"count": 234
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"host": "Ape",
|
|
21
|
+
"count": 345
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
|
|
2
|
+
|
|
3
|
+
export async function fetchAutocompleteList(lapis: string, field: string, signal?: AbortSignal) {
|
|
4
|
+
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>({}, [field]);
|
|
5
|
+
|
|
6
|
+
const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
|
|
7
|
+
|
|
8
|
+
return data.map((item) => item[field]);
|
|
9
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { withActions } from '@storybook/addon-actions/decorator';
|
|
2
|
+
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
3
|
+
|
|
4
|
+
import data from './__mockData__/aggregated_hosts.json';
|
|
5
|
+
import { TextInput, type TextInputProps } from './text-input';
|
|
6
|
+
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
7
|
+
import { LapisUrlContext } from '../LapisUrlContext';
|
|
8
|
+
|
|
9
|
+
const meta: Meta<TextInputProps> = {
|
|
10
|
+
title: 'Input/TextInput',
|
|
11
|
+
component: TextInput,
|
|
12
|
+
parameters: {
|
|
13
|
+
actions: {
|
|
14
|
+
handles: ['gs-text-input-changed'],
|
|
15
|
+
},
|
|
16
|
+
fetchMock: {
|
|
17
|
+
mocks: [
|
|
18
|
+
{
|
|
19
|
+
matcher: {
|
|
20
|
+
name: 'hosts',
|
|
21
|
+
url: AGGREGATED_ENDPOINT,
|
|
22
|
+
body: {
|
|
23
|
+
fields: ['host'],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
response: {
|
|
27
|
+
status: 200,
|
|
28
|
+
body: data,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
decorators: [withActions],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
|
|
39
|
+
export const Default: StoryObj<TextInputProps> = {
|
|
40
|
+
render: (args) => (
|
|
41
|
+
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
42
|
+
<TextInput lapisField={args.lapisField} placeholderText={args.placeholderText} />
|
|
43
|
+
</LapisUrlContext.Provider>
|
|
44
|
+
),
|
|
45
|
+
args: {
|
|
46
|
+
lapisField: 'host',
|
|
47
|
+
placeholderText: 'Enter a host name',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { useContext, useRef } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { fetchAutocompleteList } from './fetchAutocompleteList';
|
|
5
|
+
import { LapisUrlContext } from '../LapisUrlContext';
|
|
6
|
+
import { ErrorDisplay } from '../components/error-display';
|
|
7
|
+
import { LoadingDisplay } from '../components/loading-display';
|
|
8
|
+
import { NoDataDisplay } from '../components/no-data-display';
|
|
9
|
+
import { useQuery } from '../useQuery';
|
|
10
|
+
|
|
11
|
+
export interface TextInputProps {
|
|
12
|
+
lapisField: string;
|
|
13
|
+
placeholderText?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const TextInput: FunctionComponent<TextInputProps> = ({ lapisField, placeholderText }) => {
|
|
17
|
+
const lapis = useContext(LapisUrlContext);
|
|
18
|
+
|
|
19
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
20
|
+
|
|
21
|
+
const { data, error, isLoading } = useQuery(() => fetchAutocompleteList(lapis, lapisField), [lapisField, lapis]);
|
|
22
|
+
|
|
23
|
+
if (isLoading) {
|
|
24
|
+
return <LoadingDisplay />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (error !== null) {
|
|
28
|
+
return <ErrorDisplay error={error} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (data === null) {
|
|
32
|
+
return <NoDataDisplay />;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const onInput = () => {
|
|
36
|
+
const value = inputRef.current?.value;
|
|
37
|
+
|
|
38
|
+
if (isValidValue(value)) {
|
|
39
|
+
inputRef.current?.dispatchEvent(
|
|
40
|
+
new CustomEvent('gs-text-input-changed', {
|
|
41
|
+
detail: { [lapisField]: value },
|
|
42
|
+
bubbles: true,
|
|
43
|
+
composed: true,
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const isValidValue = (value: string | undefined) => {
|
|
50
|
+
if (value === undefined) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return data.includes(value);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<input
|
|
59
|
+
type='text'
|
|
60
|
+
class='input input-bordered'
|
|
61
|
+
placeholder={placeholderText !== undefined ? placeholderText : lapisField}
|
|
62
|
+
onInput={onInput}
|
|
63
|
+
ref={inputRef}
|
|
64
|
+
list={lapisField}
|
|
65
|
+
/>
|
|
66
|
+
<datalist id={lapisField}>
|
|
67
|
+
{data.map((item) => (
|
|
68
|
+
<option value={item} key={item} />
|
|
69
|
+
))}
|
|
70
|
+
</datalist>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
export function useQuery<Data>(fetchDataCallback: () => Promise<Data>, dependencies: unknown[] = []) {
|
|
4
|
+
const [data, setData] = useState<Data | null>(null);
|
|
5
|
+
const [error, setError] = useState<Error | null>(null);
|
|
6
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const fetchData = async () => {
|
|
10
|
+
setIsLoading(true);
|
|
11
|
+
try {
|
|
12
|
+
const result = await fetchDataCallback();
|
|
13
|
+
setData(result);
|
|
14
|
+
setError(null);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
setError(error as Error);
|
|
17
|
+
} finally {
|
|
18
|
+
setIsLoading(false);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
fetchData();
|
|
23
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
24
|
+
}, [JSON.stringify(dependencies)]);
|
|
25
|
+
|
|
26
|
+
return { data, error, isLoading };
|
|
27
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { FetchInsertionsOperator } from '../operator/FetchInsertionsOperator';
|
|
2
|
+
import { SortOperator } from '../operator/SortOperator';
|
|
3
|
+
import { type LapisFilter, type SequenceType } from '../types';
|
|
4
|
+
|
|
5
|
+
export function queryInsertions(variant: LapisFilter, sequenceType: SequenceType, lapis: string, signal?: AbortSignal) {
|
|
6
|
+
const fetchData = new FetchInsertionsOperator(variant, sequenceType);
|
|
7
|
+
const sortData = new SortOperator(fetchData, (a, b) => {
|
|
8
|
+
if (a.mutation.segment !== b.mutation.segment) {
|
|
9
|
+
return (a.mutation.segment ?? '').localeCompare(b.mutation.segment ?? '');
|
|
10
|
+
}
|
|
11
|
+
return a.mutation.position - b.mutation.position;
|
|
12
|
+
});
|
|
13
|
+
return sortData.evaluate(lapis, signal);
|
|
14
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { DivisionOperator } from '../operator/DivisionOperator';
|
|
2
|
+
import { FetchAggregatedOperator } from '../operator/FetchAggregatedOperator';
|
|
3
|
+
import { FillMissingOperator } from '../operator/FillMissingOperator';
|
|
4
|
+
import { GroupByAndSumOperator } from '../operator/GroupByAndSumOperator';
|
|
5
|
+
import { MapOperator } from '../operator/MapOperator';
|
|
6
|
+
import { SlidingOperator } from '../operator/SlidingOperator';
|
|
7
|
+
import { SortOperator } from '../operator/SortOperator';
|
|
8
|
+
import { type LapisFilter, type NamedLapisFilter, type TemporalGranularity } from '../types';
|
|
9
|
+
import {
|
|
10
|
+
compareTemporal,
|
|
11
|
+
generateAllInRange,
|
|
12
|
+
getMinMaxTemporal,
|
|
13
|
+
type Temporal,
|
|
14
|
+
TemporalCache,
|
|
15
|
+
} from '../utils/temporal';
|
|
16
|
+
|
|
17
|
+
export type PrevalenceOverTimeData = PrevalenceOverTimeVariantData[];
|
|
18
|
+
|
|
19
|
+
export type PrevalenceOverTimeVariantData = {
|
|
20
|
+
displayName: string;
|
|
21
|
+
content: { count: number; prevalence: number; total: number; dateRange: Temporal | null }[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function queryPrevalenceOverTime(
|
|
25
|
+
numeratorFilter: NamedLapisFilter | NamedLapisFilter[],
|
|
26
|
+
denominatorFilter: NamedLapisFilter,
|
|
27
|
+
granularity: TemporalGranularity,
|
|
28
|
+
smoothingWindow: number,
|
|
29
|
+
lapis: string,
|
|
30
|
+
signal?: AbortSignal,
|
|
31
|
+
): Promise<PrevalenceOverTimeData> {
|
|
32
|
+
const numeratorFilters = makeArray(numeratorFilter);
|
|
33
|
+
|
|
34
|
+
const denominatorData = fetchAndPrepare(getFilter(denominatorFilter), granularity, smoothingWindow);
|
|
35
|
+
const subQueries = numeratorFilters.map(async (namedLapisFilter) => {
|
|
36
|
+
const { displayName, ...filter } = namedLapisFilter;
|
|
37
|
+
const numeratorData = fetchAndPrepare(filter, granularity, smoothingWindow);
|
|
38
|
+
const divide = new DivisionOperator(
|
|
39
|
+
numeratorData,
|
|
40
|
+
denominatorData,
|
|
41
|
+
'dateRange',
|
|
42
|
+
'count',
|
|
43
|
+
'prevalence',
|
|
44
|
+
'count',
|
|
45
|
+
'total',
|
|
46
|
+
);
|
|
47
|
+
const dataset = await divide.evaluate(lapis, signal);
|
|
48
|
+
return {
|
|
49
|
+
displayName,
|
|
50
|
+
content: dataset.content,
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
return Promise.all(subQueries);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getFilter(filter: NamedLapisFilter) {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
58
|
+
const { displayName, ...filterWithoutDisplayName } = filter;
|
|
59
|
+
return filterWithoutDisplayName;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function makeArray<T>(arrayOrSingleItem: T | T[]) {
|
|
63
|
+
if (Array.isArray(arrayOrSingleItem)) {
|
|
64
|
+
return arrayOrSingleItem;
|
|
65
|
+
}
|
|
66
|
+
return [arrayOrSingleItem];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fetchAndPrepare(filter: LapisFilter, granularity: TemporalGranularity, smoothingWindow: number) {
|
|
70
|
+
const fetchData = new FetchAggregatedOperator<{
|
|
71
|
+
date: string | null;
|
|
72
|
+
}>(filter, ['date']);
|
|
73
|
+
const mapData = new MapOperator(fetchData, (d) => mapDateToGranularityRange(d, granularity));
|
|
74
|
+
const groupByData = new GroupByAndSumOperator(mapData, 'dateRange', 'count');
|
|
75
|
+
const fillData = new FillMissingOperator(
|
|
76
|
+
groupByData,
|
|
77
|
+
'dateRange',
|
|
78
|
+
getMinMaxTemporal,
|
|
79
|
+
generateAllInRange,
|
|
80
|
+
(key) => ({ dateRange: key, count: 0 }),
|
|
81
|
+
);
|
|
82
|
+
const sortData = new SortOperator(fillData, dateRangeCompare);
|
|
83
|
+
|
|
84
|
+
return smoothingWindow >= 1 ? new SlidingOperator(sortData, smoothingWindow, averageSmoothing) : sortData;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function mapDateToGranularityRange(d: { date: string | null; count: number }, granularity: TemporalGranularity) {
|
|
88
|
+
let dateRange: Temporal | null = null;
|
|
89
|
+
if (d.date !== null) {
|
|
90
|
+
const date = TemporalCache.getInstance().getYearMonthDay(d.date);
|
|
91
|
+
switch (granularity) {
|
|
92
|
+
case 'day':
|
|
93
|
+
dateRange = date;
|
|
94
|
+
break;
|
|
95
|
+
case 'week':
|
|
96
|
+
dateRange = date.week;
|
|
97
|
+
break;
|
|
98
|
+
case 'month':
|
|
99
|
+
dateRange = date.month;
|
|
100
|
+
break;
|
|
101
|
+
case 'year':
|
|
102
|
+
dateRange = date.year;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
dateRange,
|
|
108
|
+
count: d.count,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function dateRangeCompare(a: { dateRange: Temporal | null }, b: { dateRange: Temporal | null }) {
|
|
113
|
+
if (a.dateRange === null) {
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
if (b.dateRange === null) {
|
|
117
|
+
return -1;
|
|
118
|
+
}
|
|
119
|
+
return compareTemporal(a.dateRange, b.dateRange);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function averageSmoothing(slidingWindow: { dateRange: Temporal | null; count: number }[]) {
|
|
123
|
+
const average = slidingWindow.reduce((acc, curr) => acc + curr.count, 0) / slidingWindow.length;
|
|
124
|
+
const centerIndex = Math.floor(slidingWindow.length / 2);
|
|
125
|
+
return { dateRange: slidingWindow[centerIndex].dateRange, count: average };
|
|
126
|
+
}
|