@genspectrum/dashboard-components 0.6.2 → 0.6.3
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/custom-elements.json +220 -0
- package/dist/dashboard-components.js +602 -178
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +63 -0
- package/dist/style.css +7 -4
- package/package.json +3 -1
- package/src/constants.ts +1 -1
- package/src/lapisApi/lapisTypes.ts +1 -0
- package/src/operator/FillMissingOperator.spec.ts +3 -1
- package/src/operator/FillMissingOperator.ts +4 -2
- package/src/preact/mutationComparison/queryMutationData.ts +12 -4
- package/src/preact/mutationsOverTime/__mockData__/aggregated_date.json +642 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_01.json +1747 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_02.json +1774 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_03.json +1819 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_04.json +1864 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_05.json +1927 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_06.json +1864 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_2024_07.json +9 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +86 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +62 -0
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +92 -0
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +206 -0
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +170 -0
- package/src/preact/numberSequencesOverTime/getNumberOfSequencesOverTimeTableData.ts +1 -1
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +1 -0
- package/src/preact/shared/table/formatProportion.ts +2 -2
- package/src/query/queryAggregatedDataOverTime.ts +8 -33
- package/src/query/queryMutationsOverTime.spec.ts +352 -0
- package/src/query/queryMutationsOverTime.ts +164 -0
- package/src/query/queryNumberOfSequencesOverTime.ts +0 -1
- package/src/query/queryRelativeGrowthAdvantage.ts +3 -3
- package/src/utils/Map2d.ts +75 -0
- package/src/utils/map2d.spec.ts +94 -0
- package/src/utils/mutations.ts +5 -1
- package/src/utils/temporal.ts +64 -5
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +225 -0
- package/src/web-components/visualization/gs-mutations-over-time.tsx +107 -0
- package/src/web-components/visualization/index.ts +1 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"data": [],
|
|
3
|
+
"info": {
|
|
4
|
+
"dataVersion": "1720033519",
|
|
5
|
+
"requestId": "4701b007-bce8-49cf-b417-035785c0273b",
|
|
6
|
+
"requestInfo": "sars_cov-2_nextstrain_open on lapis.cov-spectrum.org at 2024-07-17T14:53:28.668667270",
|
|
7
|
+
"reportTo": "Please report to https://github.com/GenSpectrum/LAPIS/issues in case you encounter any unexpected issues. Please include the request ID and the requestInfo in your report."
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { filterDisplayedSegments, filterMutationTypes, filterProportion } from './getFilteredMutationsOverTimeData';
|
|
4
|
+
import { type MutationOverTimeMutationValue } from '../../query/queryMutationsOverTime';
|
|
5
|
+
import { Map2d } from '../../utils/Map2d';
|
|
6
|
+
import { Deletion, Substitution } from '../../utils/mutations';
|
|
7
|
+
import { type Temporal } from '../../utils/temporal';
|
|
8
|
+
import { yearMonthDay } from '../../utils/temporalTestHelpers';
|
|
9
|
+
|
|
10
|
+
describe('getFilteredMutationOverTimeData', () => {
|
|
11
|
+
describe('filterDisplayedSegments', () => {
|
|
12
|
+
it('should filter by displayed segments', () => {
|
|
13
|
+
const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
|
|
14
|
+
|
|
15
|
+
data.set(new Substitution('someSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'), 1);
|
|
16
|
+
data.set(new Substitution('someOtherSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'), 2);
|
|
17
|
+
|
|
18
|
+
filterDisplayedSegments(
|
|
19
|
+
[
|
|
20
|
+
{ segment: 'someSegment', checked: false, label: 'Some Segment' },
|
|
21
|
+
{ segment: 'someOtherSegment', checked: true, label: 'Some Other Segment' },
|
|
22
|
+
],
|
|
23
|
+
data,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(data.getFirstAxisKeys().length).to.equal(1);
|
|
27
|
+
expect(data.getFirstAxisKeys()[0].segment).to.equal('someOtherSegment');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('filterMutationTypes', () => {
|
|
32
|
+
it('should filter by mutation types', () => {
|
|
33
|
+
const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
|
|
34
|
+
|
|
35
|
+
data.set(new Substitution('someSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'), 1);
|
|
36
|
+
data.set(new Deletion('someOtherSegment', 'A', 123), yearMonthDay('2021-01-01'), 2);
|
|
37
|
+
|
|
38
|
+
filterMutationTypes(
|
|
39
|
+
[
|
|
40
|
+
{ type: 'substitution', checked: false, label: 'Substitution' },
|
|
41
|
+
{ type: 'deletion', checked: true, label: 'Deletion' },
|
|
42
|
+
],
|
|
43
|
+
data,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(data.getFirstAxisKeys().length).to.equal(1);
|
|
47
|
+
expect(data.getFirstAxisKeys()[0].type).to.equal('deletion');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('filterProportion', () => {
|
|
52
|
+
it('should filter by proportion', () => {
|
|
53
|
+
const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
|
|
54
|
+
|
|
55
|
+
const belowFilter = 0.1;
|
|
56
|
+
const aboveFilter = 0.99;
|
|
57
|
+
const proportionInterval = { min: 0.2, max: 0.9 };
|
|
58
|
+
|
|
59
|
+
const someSubstitution = new Substitution('someSegment', 'A', 'T', 123);
|
|
60
|
+
data.set(someSubstitution, yearMonthDay('2021-01-01'), belowFilter);
|
|
61
|
+
data.set(someSubstitution, yearMonthDay('2021-02-02'), aboveFilter);
|
|
62
|
+
|
|
63
|
+
filterProportion(data, proportionInterval);
|
|
64
|
+
|
|
65
|
+
expect(data.getAsArray(0).length).to.equal(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should not filter if one proportion is within the interval', () => {
|
|
69
|
+
const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
|
|
70
|
+
|
|
71
|
+
const belowFilter = 0.1;
|
|
72
|
+
const aboveFilter = 0.99;
|
|
73
|
+
const inFilter = 0.5;
|
|
74
|
+
const proportionInterval = { min: 0.2, max: 0.9 };
|
|
75
|
+
|
|
76
|
+
const someSubstitution = new Substitution('someSegment', 'A', 'T', 123);
|
|
77
|
+
data.set(someSubstitution, yearMonthDay('2021-01-01'), belowFilter);
|
|
78
|
+
data.set(someSubstitution, yearMonthDay('2021-02-02'), aboveFilter);
|
|
79
|
+
data.set(someSubstitution, yearMonthDay('2021-03-03'), inFilter);
|
|
80
|
+
|
|
81
|
+
filterProportion(data, proportionInterval);
|
|
82
|
+
|
|
83
|
+
expect(data.getRow(someSubstitution, 0).length).to.equal(3);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type MutationOverTimeDataGroupedByMutation } from '../../query/queryMutationsOverTime';
|
|
2
|
+
import type { DisplayedSegment } from '../components/SegmentSelector';
|
|
3
|
+
import type { DisplayedMutationType } from '../components/mutation-type-selector';
|
|
4
|
+
|
|
5
|
+
export function getFilteredMutationOverTimeData(
|
|
6
|
+
data: MutationOverTimeDataGroupedByMutation,
|
|
7
|
+
displayedSegments: DisplayedSegment[],
|
|
8
|
+
displayedMutationTypes: DisplayedMutationType[],
|
|
9
|
+
proportionInterval: { min: number; max: number },
|
|
10
|
+
) {
|
|
11
|
+
const filteredData = data.copy();
|
|
12
|
+
filterDisplayedSegments(displayedSegments, filteredData);
|
|
13
|
+
filterMutationTypes(displayedMutationTypes, filteredData);
|
|
14
|
+
filterProportion(filteredData, proportionInterval);
|
|
15
|
+
|
|
16
|
+
return filteredData;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function filterDisplayedSegments(
|
|
20
|
+
displayedSegments: DisplayedSegment[],
|
|
21
|
+
data: MutationOverTimeDataGroupedByMutation,
|
|
22
|
+
) {
|
|
23
|
+
displayedSegments.forEach((segment) => {
|
|
24
|
+
if (!segment.checked) {
|
|
25
|
+
data.getFirstAxisKeys().forEach((mutation) => {
|
|
26
|
+
if (mutation.segment === segment.segment) {
|
|
27
|
+
data.deleteRow(mutation);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function filterMutationTypes(
|
|
35
|
+
displayedMutationTypes: DisplayedMutationType[],
|
|
36
|
+
data: MutationOverTimeDataGroupedByMutation,
|
|
37
|
+
) {
|
|
38
|
+
displayedMutationTypes.forEach((mutationType) => {
|
|
39
|
+
if (!mutationType.checked) {
|
|
40
|
+
data.getFirstAxisKeys().forEach((mutation) => {
|
|
41
|
+
if (mutationType.type === mutation.type) {
|
|
42
|
+
data.deleteRow(mutation);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function filterProportion(
|
|
50
|
+
data: MutationOverTimeDataGroupedByMutation,
|
|
51
|
+
proportionInterval: {
|
|
52
|
+
min: number;
|
|
53
|
+
max: number;
|
|
54
|
+
},
|
|
55
|
+
) {
|
|
56
|
+
data.getFirstAxisKeys().forEach((mutation) => {
|
|
57
|
+
const row = data.getRow(mutation, 0);
|
|
58
|
+
if (!row.some((value) => value >= proportionInterval.min && value <= proportionInterval.max)) {
|
|
59
|
+
data.deleteRow(mutation);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Fragment, type FunctionComponent } from 'preact';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type MutationOverTimeDataGroupedByMutation,
|
|
5
|
+
type MutationOverTimeMutationValue,
|
|
6
|
+
} from '../../query/queryMutationsOverTime';
|
|
7
|
+
import { type Deletion, type Substitution } from '../../utils/mutations';
|
|
8
|
+
import { compareTemporal, type Temporal } from '../../utils/temporal';
|
|
9
|
+
import { singleGraphColorRGBByName } from '../shared/charts/colors';
|
|
10
|
+
import { formatProportion } from '../shared/table/formatProportion';
|
|
11
|
+
|
|
12
|
+
export interface MutationsOverTimeGridProps {
|
|
13
|
+
data: MutationOverTimeDataGroupedByMutation;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({ data }) => {
|
|
17
|
+
const mutations = data.getFirstAxisKeys();
|
|
18
|
+
const dates = data.getSecondAxisKeys().sort((a, b) => compareTemporal(a, b));
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
style={{
|
|
23
|
+
display: 'grid',
|
|
24
|
+
gridTemplateRows: `repeat(${mutations.length}, 24px)`,
|
|
25
|
+
gridTemplateColumns: `8rem repeat(${dates.length}, minmax(1.5rem, 1fr))`,
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
{mutations.map((mutation, i) => {
|
|
29
|
+
return (
|
|
30
|
+
<Fragment key={`fragment-${mutation.toString()}`}>
|
|
31
|
+
<div
|
|
32
|
+
key={`mutation-${mutation.toString()}`}
|
|
33
|
+
style={{ gridRowStart: i + 1, gridColumnStart: 1 }}
|
|
34
|
+
>
|
|
35
|
+
<MutationCell mutation={mutation} />
|
|
36
|
+
</div>
|
|
37
|
+
{dates.map((date, j) => {
|
|
38
|
+
const value = data.get(mutation, date) ?? 0;
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
style={{ gridRowStart: i + 1, gridColumnStart: j + 2 }}
|
|
42
|
+
key={`${mutation.toString()}-${date.toString()}`}
|
|
43
|
+
>
|
|
44
|
+
<ProportionCell value={value} date={date} mutation={mutation} />
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
})}
|
|
48
|
+
</Fragment>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const ProportionCell: FunctionComponent<{
|
|
56
|
+
value: MutationOverTimeMutationValue;
|
|
57
|
+
date: Temporal;
|
|
58
|
+
mutation: Substitution | Deletion;
|
|
59
|
+
}> = ({ value }) => {
|
|
60
|
+
// TODO(#353): Add tooltip with date, mutation and proportion
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<div className={'py-1'}>
|
|
64
|
+
<div
|
|
65
|
+
style={{ backgroundColor: backgroundColor(value), color: textColor(value) }}
|
|
66
|
+
className='text-center hover:font-bold text-xs'
|
|
67
|
+
>
|
|
68
|
+
{formatProportion(value, 0)}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const backgroundColor = (proportion: number) => {
|
|
76
|
+
// TODO(#353): Make minAlpha and maxAlpha configurable
|
|
77
|
+
const minAlpha = 0.0;
|
|
78
|
+
const maxAlpha = 1;
|
|
79
|
+
|
|
80
|
+
const alpha = minAlpha + (maxAlpha - minAlpha) * proportion;
|
|
81
|
+
return singleGraphColorRGBByName('indigo', alpha);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const textColor = (proportion: number) => {
|
|
85
|
+
return proportion > 0.5 ? 'white' : 'black';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const MutationCell: FunctionComponent<{ mutation: Substitution | Deletion }> = ({ mutation }) => {
|
|
89
|
+
return <div className='text-center'>{mutation.toString()}</div>;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default MutationsOverTimeGrid;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
|
|
3
|
+
import aggregated_date from './__mockData__/aggregated_date.json';
|
|
4
|
+
import nucleotideMutation_01 from './__mockData__/nucleotideMutations_2024_01.json';
|
|
5
|
+
import nucleotideMutation_02 from './__mockData__/nucleotideMutations_2024_02.json';
|
|
6
|
+
import nucleotideMutation_03 from './__mockData__/nucleotideMutations_2024_03.json';
|
|
7
|
+
import nucleotideMutation_04 from './__mockData__/nucleotideMutations_2024_04.json';
|
|
8
|
+
import nucleotideMutation_05 from './__mockData__/nucleotideMutations_2024_05.json';
|
|
9
|
+
import nucleotideMutation_06 from './__mockData__/nucleotideMutations_2024_06.json';
|
|
10
|
+
import nucleotideMutation_07 from './__mockData__/nucleotideMutations_2024_07.json';
|
|
11
|
+
import { MutationsOverTime, type MutationsOverTimeProps } from './mutations-over-time';
|
|
12
|
+
import { AGGREGATED_ENDPOINT, LAPIS_URL, NUCLEOTIDE_MUTATIONS_ENDPOINT } from '../../constants';
|
|
13
|
+
import referenceGenome from '../../lapisApi/__mockData__/referenceGenome.json';
|
|
14
|
+
import { LapisUrlContext } from '../LapisUrlContext';
|
|
15
|
+
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
16
|
+
|
|
17
|
+
const meta: Meta<MutationsOverTimeProps> = {
|
|
18
|
+
title: 'Visualization/Mutation over time',
|
|
19
|
+
component: MutationsOverTime,
|
|
20
|
+
argTypes: {
|
|
21
|
+
lapisFilter: { control: 'object' },
|
|
22
|
+
sequenceType: {
|
|
23
|
+
options: ['nucleotide', 'amino acid'],
|
|
24
|
+
control: { type: 'radio' },
|
|
25
|
+
},
|
|
26
|
+
views: {
|
|
27
|
+
options: ['grid'],
|
|
28
|
+
control: { type: 'check' },
|
|
29
|
+
},
|
|
30
|
+
width: { control: 'text' },
|
|
31
|
+
height: { control: 'text' },
|
|
32
|
+
granularity: {
|
|
33
|
+
options: ['day', 'week', 'month', 'year'],
|
|
34
|
+
control: { type: 'radio' },
|
|
35
|
+
},
|
|
36
|
+
lapisDateField: { control: 'text' },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
|
|
42
|
+
const Template = {
|
|
43
|
+
render: (args: MutationsOverTimeProps) => (
|
|
44
|
+
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
45
|
+
<ReferenceGenomeContext.Provider value={referenceGenome}>
|
|
46
|
+
<MutationsOverTime
|
|
47
|
+
lapisFilter={args.lapisFilter}
|
|
48
|
+
sequenceType={args.sequenceType}
|
|
49
|
+
views={args.views}
|
|
50
|
+
width={args.width}
|
|
51
|
+
height={args.height}
|
|
52
|
+
granularity={args.granularity}
|
|
53
|
+
lapisDateField={args.lapisDateField}
|
|
54
|
+
/>
|
|
55
|
+
</ReferenceGenomeContext.Provider>
|
|
56
|
+
</LapisUrlContext.Provider>
|
|
57
|
+
),
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const Default: StoryObj<MutationsOverTimeProps> = {
|
|
61
|
+
...Template,
|
|
62
|
+
args: {
|
|
63
|
+
lapisFilter: { pangoLineage: 'JN.1*', dateFrom: '2024-01-15', dateTo: '2024-07-10' },
|
|
64
|
+
sequenceType: 'nucleotide',
|
|
65
|
+
views: ['grid'],
|
|
66
|
+
width: '100%',
|
|
67
|
+
height: '700px',
|
|
68
|
+
granularity: 'month',
|
|
69
|
+
lapisDateField: 'date',
|
|
70
|
+
},
|
|
71
|
+
parameters: {
|
|
72
|
+
fetchMock: {
|
|
73
|
+
mocks: [
|
|
74
|
+
{
|
|
75
|
+
matcher: {
|
|
76
|
+
name: 'aggregated_dates',
|
|
77
|
+
url: AGGREGATED_ENDPOINT,
|
|
78
|
+
body: {
|
|
79
|
+
dateFrom: '2024-01-15',
|
|
80
|
+
dateTo: '2024-07-10',
|
|
81
|
+
fields: ['date'],
|
|
82
|
+
pangoLineage: 'JN.1*',
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
response: {
|
|
86
|
+
status: 200,
|
|
87
|
+
body: aggregated_date,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
matcher: {
|
|
92
|
+
name: 'nucleotideMutations_01',
|
|
93
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
94
|
+
body: {
|
|
95
|
+
pangoLineage: 'JN.1*',
|
|
96
|
+
dateFrom: '2024-01-01',
|
|
97
|
+
dateTo: '2024-01-31',
|
|
98
|
+
minProportion: 0,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
response: {
|
|
102
|
+
status: 200,
|
|
103
|
+
body: nucleotideMutation_01,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
matcher: {
|
|
108
|
+
name: 'nucleotideMutations_02',
|
|
109
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
110
|
+
body: {
|
|
111
|
+
pangoLineage: 'JN.1*',
|
|
112
|
+
dateFrom: '2024-02-01',
|
|
113
|
+
dateTo: '2024-02-29',
|
|
114
|
+
minProportion: 0,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
response: {
|
|
118
|
+
status: 200,
|
|
119
|
+
body: nucleotideMutation_02,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
matcher: {
|
|
124
|
+
name: 'nucleotideMutations_03',
|
|
125
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
126
|
+
body: {
|
|
127
|
+
pangoLineage: 'JN.1*',
|
|
128
|
+
dateFrom: '2024-03-01',
|
|
129
|
+
dateTo: '2024-03-31',
|
|
130
|
+
minProportion: 0,
|
|
131
|
+
},
|
|
132
|
+
response: {
|
|
133
|
+
status: 200,
|
|
134
|
+
body: nucleotideMutation_03,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
matcher: {
|
|
140
|
+
name: 'nucleotideMutations_04',
|
|
141
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
142
|
+
body: {
|
|
143
|
+
pangoLineage: 'JN.1*',
|
|
144
|
+
dateFrom: '2024-04-01',
|
|
145
|
+
dateTo: '2024-04-30',
|
|
146
|
+
minProportion: 0,
|
|
147
|
+
},
|
|
148
|
+
response: {
|
|
149
|
+
status: 200,
|
|
150
|
+
body: nucleotideMutation_04,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
matcher: {
|
|
156
|
+
name: 'nucleotideMutations_05',
|
|
157
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
158
|
+
body: {
|
|
159
|
+
pangoLineage: 'JN.1*',
|
|
160
|
+
dateFrom: '2024-05-01',
|
|
161
|
+
dateTo: '2024-05-31',
|
|
162
|
+
minProportion: 0,
|
|
163
|
+
},
|
|
164
|
+
response: {
|
|
165
|
+
status: 200,
|
|
166
|
+
body: nucleotideMutation_05,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
matcher: {
|
|
172
|
+
name: 'nucleotideMutations_06',
|
|
173
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
174
|
+
body: {
|
|
175
|
+
pangoLineage: 'JN.1*',
|
|
176
|
+
dateFrom: '2024-06-01',
|
|
177
|
+
dateTo: '2024-06-30',
|
|
178
|
+
minProportion: 0,
|
|
179
|
+
},
|
|
180
|
+
response: {
|
|
181
|
+
status: 200,
|
|
182
|
+
body: nucleotideMutation_06,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
{
|
|
188
|
+
matcher: {
|
|
189
|
+
name: 'nucleotideMutations_07',
|
|
190
|
+
url: NUCLEOTIDE_MUTATIONS_ENDPOINT,
|
|
191
|
+
body: {
|
|
192
|
+
pangoLineage: 'JN.1*',
|
|
193
|
+
dateFrom: '2024-07-01',
|
|
194
|
+
dateTo: '2024-07-31',
|
|
195
|
+
minProportion: 0,
|
|
196
|
+
},
|
|
197
|
+
response: {
|
|
198
|
+
status: 200,
|
|
199
|
+
body: nucleotideMutation_07,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { type Dispatch, type StateUpdater, useContext, useMemo, useState } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { getFilteredMutationOverTimeData } from './getFilteredMutationsOverTimeData';
|
|
5
|
+
import MutationsOverTimeGrid from './mutations-over-time-grid';
|
|
6
|
+
import {
|
|
7
|
+
type MutationOverTimeDataGroupedByMutation,
|
|
8
|
+
queryMutationsOverTimeData,
|
|
9
|
+
} from '../../query/queryMutationsOverTime';
|
|
10
|
+
import { type LapisFilter, type SequenceType, type TemporalGranularity } from '../../types';
|
|
11
|
+
import { LapisUrlContext } from '../LapisUrlContext';
|
|
12
|
+
import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/SegmentSelector';
|
|
13
|
+
import { ErrorBoundary } from '../components/error-boundary';
|
|
14
|
+
import { ErrorDisplay } from '../components/error-display';
|
|
15
|
+
import Info from '../components/info';
|
|
16
|
+
import { LoadingDisplay } from '../components/loading-display';
|
|
17
|
+
import { type DisplayedMutationType, MutationTypeSelector } from '../components/mutation-type-selector';
|
|
18
|
+
import { NoDataDisplay } from '../components/no-data-display';
|
|
19
|
+
import type { ProportionInterval } from '../components/proportion-selector';
|
|
20
|
+
import { ProportionSelectorDropdown } from '../components/proportion-selector-dropdown';
|
|
21
|
+
import { ResizeContainer } from '../components/resize-container';
|
|
22
|
+
import Tabs from '../components/tabs';
|
|
23
|
+
import { useQuery } from '../useQuery';
|
|
24
|
+
|
|
25
|
+
export type View = 'grid';
|
|
26
|
+
|
|
27
|
+
export interface MutationsOverTimeInnerProps {
|
|
28
|
+
lapisFilter: LapisFilter;
|
|
29
|
+
sequenceType: SequenceType;
|
|
30
|
+
views: View[];
|
|
31
|
+
granularity: TemporalGranularity;
|
|
32
|
+
lapisDateField: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MutationsOverTimeProps extends MutationsOverTimeInnerProps {
|
|
36
|
+
width: string;
|
|
37
|
+
height: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const MutationsOverTime: FunctionComponent<MutationsOverTimeProps> = ({ width, height, ...innerProps }) => {
|
|
41
|
+
const size = { height, width };
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<ErrorBoundary size={size}>
|
|
45
|
+
<ResizeContainer size={size}>
|
|
46
|
+
<MutationsOverTimeInner {...innerProps} />
|
|
47
|
+
</ResizeContainer>
|
|
48
|
+
</ErrorBoundary>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const MutationsOverTimeInner: FunctionComponent<MutationsOverTimeInnerProps> = ({
|
|
53
|
+
lapisFilter,
|
|
54
|
+
sequenceType,
|
|
55
|
+
views,
|
|
56
|
+
granularity,
|
|
57
|
+
lapisDateField,
|
|
58
|
+
}) => {
|
|
59
|
+
const lapis = useContext(LapisUrlContext);
|
|
60
|
+
const { data, error, isLoading } = useQuery(async () => {
|
|
61
|
+
return queryMutationsOverTimeData(lapisFilter, sequenceType, lapis, lapisDateField, granularity);
|
|
62
|
+
}, [lapisFilter, sequenceType, lapis, granularity, lapisDateField]);
|
|
63
|
+
|
|
64
|
+
if (isLoading) {
|
|
65
|
+
return <LoadingDisplay />;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (error !== null) {
|
|
69
|
+
return <ErrorDisplay error={error} />;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (data === null) {
|
|
73
|
+
return <NoDataDisplay />;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return <MutationsOverTimeTabs mutationOverTimeData={data} sequenceType={sequenceType} views={views} />;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type MutationOverTimeTabsProps = {
|
|
80
|
+
mutationOverTimeData: MutationOverTimeDataGroupedByMutation;
|
|
81
|
+
sequenceType: SequenceType;
|
|
82
|
+
views: View[];
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
86
|
+
mutationOverTimeData,
|
|
87
|
+
sequenceType,
|
|
88
|
+
views,
|
|
89
|
+
}) => {
|
|
90
|
+
const [proportionInterval, setProportionInterval] = useState({ min: 0.05, max: 0.9 });
|
|
91
|
+
|
|
92
|
+
const [displayedSegments, setDisplayedSegments] = useDisplayedSegments(sequenceType);
|
|
93
|
+
const [displayedMutationTypes, setDisplayedMutationTypes] = useState<DisplayedMutationType[]>([
|
|
94
|
+
{ label: 'Substitutions', checked: true, type: 'substitution' },
|
|
95
|
+
{ label: 'Deletions', checked: true, type: 'deletion' },
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
const filteredData = useMemo(
|
|
99
|
+
() =>
|
|
100
|
+
getFilteredMutationOverTimeData(
|
|
101
|
+
mutationOverTimeData,
|
|
102
|
+
displayedSegments,
|
|
103
|
+
displayedMutationTypes,
|
|
104
|
+
proportionInterval,
|
|
105
|
+
),
|
|
106
|
+
[mutationOverTimeData, displayedSegments, displayedMutationTypes, proportionInterval],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const getTab = (view: View) => {
|
|
110
|
+
switch (view) {
|
|
111
|
+
case 'grid':
|
|
112
|
+
return {
|
|
113
|
+
title: 'Grid',
|
|
114
|
+
content: <MutationsOverTimeGrid data={filteredData} />,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const tabs = views.map((view) => getTab(view));
|
|
120
|
+
|
|
121
|
+
const toolbar = () => (
|
|
122
|
+
<Toolbar
|
|
123
|
+
displayedSegments={displayedSegments}
|
|
124
|
+
setDisplayedSegments={setDisplayedSegments}
|
|
125
|
+
displayedMutationTypes={displayedMutationTypes}
|
|
126
|
+
setDisplayedMutationTypes={setDisplayedMutationTypes}
|
|
127
|
+
proportionInterval={proportionInterval}
|
|
128
|
+
setProportionInterval={setProportionInterval}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return <Tabs tabs={tabs} toolbar={toolbar} />;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
type ToolbarProps = {
|
|
136
|
+
displayedSegments: DisplayedSegment[];
|
|
137
|
+
setDisplayedSegments: (segments: DisplayedSegment[]) => void;
|
|
138
|
+
displayedMutationTypes: DisplayedMutationType[];
|
|
139
|
+
setDisplayedMutationTypes: (types: DisplayedMutationType[]) => void;
|
|
140
|
+
proportionInterval: ProportionInterval;
|
|
141
|
+
setProportionInterval: Dispatch<StateUpdater<ProportionInterval>>;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
145
|
+
displayedSegments,
|
|
146
|
+
setDisplayedSegments,
|
|
147
|
+
displayedMutationTypes,
|
|
148
|
+
setDisplayedMutationTypes,
|
|
149
|
+
proportionInterval,
|
|
150
|
+
setProportionInterval,
|
|
151
|
+
}) => {
|
|
152
|
+
return (
|
|
153
|
+
<>
|
|
154
|
+
<SegmentSelector displayedSegments={displayedSegments} setDisplayedSegments={setDisplayedSegments} />
|
|
155
|
+
<MutationTypeSelector
|
|
156
|
+
setDisplayedMutationTypes={setDisplayedMutationTypes}
|
|
157
|
+
displayedMutationTypes={displayedMutationTypes}
|
|
158
|
+
/>
|
|
159
|
+
<>
|
|
160
|
+
<ProportionSelectorDropdown
|
|
161
|
+
proportionInterval={proportionInterval}
|
|
162
|
+
setMinProportion={(min) => setProportionInterval((prev) => ({ ...prev, min }))}
|
|
163
|
+
setMaxProportion={(max) => setProportionInterval((prev) => ({ ...prev, max }))}
|
|
164
|
+
/>
|
|
165
|
+
{/* TODO(#362): Add download button */}
|
|
166
|
+
</>
|
|
167
|
+
<Info height={'100px'}>Info for mutations over time</Info>
|
|
168
|
+
</>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
@@ -21,7 +21,7 @@ export const getNumberOfSequencesOverTimeTableData = <DateRangeKey extends strin
|
|
|
21
21
|
return [];
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const allDateRanges: (Temporal | null)[] = generateAllInRange(
|
|
24
|
+
const allDateRanges: (Temporal | null)[] = generateAllInRange(minMax.min, minMax.max);
|
|
25
25
|
|
|
26
26
|
if (allDateRangesThatOccurInData.has(null)) {
|
|
27
27
|
allDateRanges.unshift(null);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export const formatProportion = (proportion: number) => {
|
|
2
|
-
return `${(proportion * 100).toFixed(
|
|
1
|
+
export const formatProportion = (proportion: number, digits: number = 2) => {
|
|
2
|
+
return `${(proportion * 100).toFixed(digits)}%`;
|
|
3
3
|
};
|