@genspectrum/dashboard-components 0.6.3 → 0.6.4
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 +1 -1
- package/dist/dashboard-components.js +84 -11
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +7 -2
- package/dist/style.css +3 -0
- package/package.json +1 -1
- package/src/preact/components/tooltip.stories.tsx +54 -0
- package/src/preact/components/tooltip.tsx +31 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +23 -11
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +6 -2
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +45 -10
- package/src/query/queryMutationsOverTime.spec.ts +50 -24
- package/src/query/queryMutationsOverTime.ts +20 -5
- package/src/utils/temporal.spec.ts +5 -0
- package/src/utils/temporal.ts +29 -5
- package/src/web-components/visualization/gs-mutations-over-time.tsx +5 -0
|
@@ -501,6 +501,11 @@ export declare class MutationsComponent extends PreactLitAdapterWithGridJsStyles
|
|
|
501
501
|
*
|
|
502
502
|
* The grid view shows the proportion for each mutation over date ranges.
|
|
503
503
|
*
|
|
504
|
+
* The grid will show at max 100 rows and 200 columns for browser performance reasons.
|
|
505
|
+
* More data might make the browser unresponsive.
|
|
506
|
+
* If the numbers are exceeded, an error message will be shown.
|
|
507
|
+
* In both cases, the `lapisFilter` should be narrowed down to reduce the number of mutations or date ranges.
|
|
508
|
+
* The number of date ranges can also be reduced by selecting a larger granularity (months instead of weeks).
|
|
504
509
|
*/
|
|
505
510
|
export declare class MutationsOverTimeComponent extends PreactLitAdapterWithGridJsStyles {
|
|
506
511
|
/**
|
|
@@ -958,14 +963,14 @@ declare global {
|
|
|
958
963
|
|
|
959
964
|
declare global {
|
|
960
965
|
interface HTMLElementTagNameMap {
|
|
961
|
-
'gs-
|
|
966
|
+
'gs-prevalence-over-time': PrevalenceOverTimeComponent;
|
|
962
967
|
}
|
|
963
968
|
}
|
|
964
969
|
|
|
965
970
|
|
|
966
971
|
declare global {
|
|
967
972
|
interface HTMLElementTagNameMap {
|
|
968
|
-
'gs-
|
|
973
|
+
'gs-mutations-component': MutationsComponent;
|
|
969
974
|
}
|
|
970
975
|
}
|
|
971
976
|
|
package/dist/style.css
CHANGED
|
@@ -3150,6 +3150,9 @@ input.tab:checked + .tab-content,
|
|
|
3150
3150
|
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
|
3151
3151
|
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
|
3152
3152
|
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
|
3153
|
+
}
|
|
3154
|
+
.peer:hover ~ .peer-hover\:block {
|
|
3155
|
+
display: block;
|
|
3153
3156
|
}.flatpickr-calendar{background:transparent;opacity:0;display:none;text-align:center;visibility:hidden;padding:0;-webkit-animation:none;animation:none;direction:ltr;border:0;font-size:14px;line-height:24px;border-radius:5px;position:absolute;width:307.875px;-webkit-box-sizing:border-box;box-sizing:border-box;-ms-touch-action:manipulation;touch-action:manipulation;background:#fff;-webkit-box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08);box-shadow:1px 0 0 #e6e6e6,-1px 0 0 #e6e6e6,0 1px 0 #e6e6e6,0 -1px 0 #e6e6e6,0 3px 13px rgba(0,0,0,0.08)}.flatpickr-calendar.open,.flatpickr-calendar.inline{opacity:1;max-height:640px;visibility:visible}.flatpickr-calendar.open{display:inline-block;z-index:99999}.flatpickr-calendar.animate.open{-webkit-animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1);animation:fpFadeInDown 300ms cubic-bezier(.23,1,.32,1)}.flatpickr-calendar.inline{display:block;position:relative;top:2px}.flatpickr-calendar.static{position:absolute;top:calc(100% + 2px)}.flatpickr-calendar.static.open{z-index:999;display:block}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+1) .flatpickr-day.inRange:nth-child(7n+7){-webkit-box-shadow:none !important;box-shadow:none !important}.flatpickr-calendar.multiMonth .flatpickr-days .dayContainer:nth-child(n+2) .flatpickr-day.inRange:nth-child(7n+1){-webkit-box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6;box-shadow:-2px 0 0 #e6e6e6,5px 0 0 #e6e6e6}.flatpickr-calendar .hasWeeks .dayContainer,.flatpickr-calendar .hasTime .dayContainer{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.flatpickr-calendar .hasWeeks .dayContainer{border-left:0}.flatpickr-calendar.hasTime .flatpickr-time{height:40px;border-top:1px solid #e6e6e6}.flatpickr-calendar.noCalendar.hasTime .flatpickr-time{height:auto}.flatpickr-calendar:before,.flatpickr-calendar:after{position:absolute;display:block;pointer-events:none;border:solid transparent;content:'';height:0;width:0;left:22px}.flatpickr-calendar.rightMost:before,.flatpickr-calendar.arrowRight:before,.flatpickr-calendar.rightMost:after,.flatpickr-calendar.arrowRight:after{left:auto;right:22px}.flatpickr-calendar.arrowCenter:before,.flatpickr-calendar.arrowCenter:after{left:50%;right:50%}.flatpickr-calendar:before{border-width:5px;margin:0 -5px}.flatpickr-calendar:after{border-width:4px;margin:0 -4px}.flatpickr-calendar.arrowTop:before,.flatpickr-calendar.arrowTop:after{bottom:100%}.flatpickr-calendar.arrowTop:before{border-bottom-color:#e6e6e6}.flatpickr-calendar.arrowTop:after{border-bottom-color:#fff}.flatpickr-calendar.arrowBottom:before,.flatpickr-calendar.arrowBottom:after{top:100%}.flatpickr-calendar.arrowBottom:before{border-top-color:#e6e6e6}.flatpickr-calendar.arrowBottom:after{border-top-color:#fff}.flatpickr-calendar:focus{outline:0}.flatpickr-wrapper{position:relative;display:inline-block}.flatpickr-months{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.flatpickr-months .flatpickr-month{background:transparent;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9);height:34px;line-height:1;text-align:center;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:hidden;-webkit-box-flex:1;-webkit-flex:1;-ms-flex:1;flex:1}.flatpickr-months .flatpickr-prev-month,.flatpickr-months .flatpickr-next-month{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-decoration:none;cursor:pointer;position:absolute;top:0;height:34px;padding:10px;z-index:3;color:rgba(0,0,0,0.9);fill:rgba(0,0,0,0.9)}.flatpickr-months .flatpickr-prev-month.flatpickr-disabled,.flatpickr-months .flatpickr-next-month.flatpickr-disabled{display:none}.flatpickr-months .flatpickr-prev-month i,.flatpickr-months .flatpickr-next-month i{position:relative}.flatpickr-months .flatpickr-prev-month.flatpickr-prev-month,.flatpickr-months .flatpickr-next-month.flatpickr-prev-month{/*
|
|
3154
3157
|
/*rtl:begin:ignore*/left:0/*
|
|
3155
3158
|
/*rtl:end:ignore*/}/*
|
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect, waitFor, within } from '@storybook/test';
|
|
3
|
+
|
|
4
|
+
import Tooltip, { type TooltipProps } from './tooltip';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<TooltipProps> = {
|
|
7
|
+
title: 'Component/Tooltip',
|
|
8
|
+
component: Tooltip,
|
|
9
|
+
parameters: { fetchMock: {} },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
|
|
14
|
+
const tooltipContent = 'This is some content.';
|
|
15
|
+
|
|
16
|
+
export const TooltipStory: StoryObj<TooltipProps> = {
|
|
17
|
+
render: (args) => (
|
|
18
|
+
<div class='flex justify-center px-4 py-16'>
|
|
19
|
+
<Tooltip {...args}>
|
|
20
|
+
<div>Hover me</div>
|
|
21
|
+
</Tooltip>
|
|
22
|
+
</div>
|
|
23
|
+
),
|
|
24
|
+
args: {
|
|
25
|
+
content: tooltipContent,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const RendersStringContent: StoryObj<TooltipProps> = {
|
|
30
|
+
...TooltipStory,
|
|
31
|
+
play: async ({ canvasElement }) => {
|
|
32
|
+
const canvas = within(canvasElement);
|
|
33
|
+
const tooltipBase = canvas.getByText('Hover me');
|
|
34
|
+
|
|
35
|
+
await waitFor(() => expect(tooltipBase).toBeInTheDocument());
|
|
36
|
+
|
|
37
|
+
await waitFor(() => expect(canvas.queryByText(tooltipContent, { exact: false })).toBeInTheDocument());
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const RendersComponentConent: StoryObj<TooltipProps> = {
|
|
42
|
+
...TooltipStory,
|
|
43
|
+
args: {
|
|
44
|
+
content: <div>{tooltipContent}</div>,
|
|
45
|
+
},
|
|
46
|
+
play: async ({ canvasElement }) => {
|
|
47
|
+
const canvas = within(canvasElement);
|
|
48
|
+
const tooltipBase = canvas.getByText('Hover me');
|
|
49
|
+
|
|
50
|
+
await waitFor(() => expect(tooltipBase).toBeInTheDocument());
|
|
51
|
+
|
|
52
|
+
await waitFor(() => expect(canvas.queryByText(tooltipContent, { exact: false })).toBeInTheDocument());
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { flip, offset, shift } from '@floating-ui/dom';
|
|
2
|
+
import { type FunctionComponent } from 'preact';
|
|
3
|
+
import { useRef } from 'preact/hooks';
|
|
4
|
+
import { type JSXInternal } from 'preact/src/jsx';
|
|
5
|
+
|
|
6
|
+
import { dropdownClass } from './dropdown';
|
|
7
|
+
import { useFloatingUi } from '../shared/floating-ui/hooks';
|
|
8
|
+
|
|
9
|
+
export type TooltipProps = {
|
|
10
|
+
content: string | JSXInternal.Element;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const Tooltip: FunctionComponent<TooltipProps> = ({ children, content }) => {
|
|
14
|
+
const referenceRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
const floatingRef = useRef<HTMLDivElement>(null);
|
|
16
|
+
|
|
17
|
+
useFloatingUi(referenceRef, floatingRef, [offset(5), shift(), flip()]);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className='relative'>
|
|
21
|
+
<div className='peer' ref={referenceRef}>
|
|
22
|
+
{children}
|
|
23
|
+
</div>
|
|
24
|
+
<div ref={floatingRef} className={`${dropdownClass} hidden peer-hover:block`}>
|
|
25
|
+
{content}
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default Tooltip;
|
|
@@ -12,8 +12,14 @@ describe('getFilteredMutationOverTimeData', () => {
|
|
|
12
12
|
it('should filter by displayed segments', () => {
|
|
13
13
|
const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
|
|
14
14
|
|
|
15
|
-
data.set(new Substitution('someSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'),
|
|
16
|
-
|
|
15
|
+
data.set(new Substitution('someSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'), {
|
|
16
|
+
count: 1,
|
|
17
|
+
proportion: 0.1,
|
|
18
|
+
});
|
|
19
|
+
data.set(new Substitution('someOtherSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'), {
|
|
20
|
+
count: 2,
|
|
21
|
+
proportion: 0.2,
|
|
22
|
+
});
|
|
17
23
|
|
|
18
24
|
filterDisplayedSegments(
|
|
19
25
|
[
|
|
@@ -32,8 +38,14 @@ describe('getFilteredMutationOverTimeData', () => {
|
|
|
32
38
|
it('should filter by mutation types', () => {
|
|
33
39
|
const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
|
|
34
40
|
|
|
35
|
-
data.set(new Substitution('someSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'),
|
|
36
|
-
|
|
41
|
+
data.set(new Substitution('someSegment', 'A', 'T', 123), yearMonthDay('2021-01-01'), {
|
|
42
|
+
count: 1,
|
|
43
|
+
proportion: 0.1,
|
|
44
|
+
});
|
|
45
|
+
data.set(new Deletion('someOtherSegment', 'A', 123), yearMonthDay('2021-01-01'), {
|
|
46
|
+
count: 2,
|
|
47
|
+
proportion: 0.2,
|
|
48
|
+
});
|
|
37
49
|
|
|
38
50
|
filterMutationTypes(
|
|
39
51
|
[
|
|
@@ -52,8 +64,8 @@ describe('getFilteredMutationOverTimeData', () => {
|
|
|
52
64
|
it('should filter by proportion', () => {
|
|
53
65
|
const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
|
|
54
66
|
|
|
55
|
-
const belowFilter = 0.1;
|
|
56
|
-
const aboveFilter = 0.99;
|
|
67
|
+
const belowFilter = { count: 1, proportion: 0.1 };
|
|
68
|
+
const aboveFilter = { count: 99, proportion: 0.99 };
|
|
57
69
|
const proportionInterval = { min: 0.2, max: 0.9 };
|
|
58
70
|
|
|
59
71
|
const someSubstitution = new Substitution('someSegment', 'A', 'T', 123);
|
|
@@ -62,15 +74,15 @@ describe('getFilteredMutationOverTimeData', () => {
|
|
|
62
74
|
|
|
63
75
|
filterProportion(data, proportionInterval);
|
|
64
76
|
|
|
65
|
-
expect(data.getAsArray(0).length).to.equal(0);
|
|
77
|
+
expect(data.getAsArray({ count: 0, proportion: 0 }).length).to.equal(0);
|
|
66
78
|
});
|
|
67
79
|
|
|
68
80
|
it('should not filter if one proportion is within the interval', () => {
|
|
69
81
|
const data = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>();
|
|
70
82
|
|
|
71
|
-
const belowFilter = 0.1;
|
|
72
|
-
const aboveFilter = 0.99;
|
|
73
|
-
const inFilter = 0.5;
|
|
83
|
+
const belowFilter = { count: 1, proportion: 0.1 };
|
|
84
|
+
const aboveFilter = { count: 99, proportion: 0.99 };
|
|
85
|
+
const inFilter = { count: 5, proportion: 0.5 };
|
|
74
86
|
const proportionInterval = { min: 0.2, max: 0.9 };
|
|
75
87
|
|
|
76
88
|
const someSubstitution = new Substitution('someSegment', 'A', 'T', 123);
|
|
@@ -80,7 +92,7 @@ describe('getFilteredMutationOverTimeData', () => {
|
|
|
80
92
|
|
|
81
93
|
filterProportion(data, proportionInterval);
|
|
82
94
|
|
|
83
|
-
expect(data.getRow(someSubstitution, 0).length).to.equal(3);
|
|
95
|
+
expect(data.getRow(someSubstitution, { count: 0, proportion: 0 }).length).to.equal(3);
|
|
84
96
|
});
|
|
85
97
|
});
|
|
86
98
|
});
|
|
@@ -54,8 +54,12 @@ export function filterProportion(
|
|
|
54
54
|
},
|
|
55
55
|
) {
|
|
56
56
|
data.getFirstAxisKeys().forEach((mutation) => {
|
|
57
|
-
const row = data.getRow(mutation, 0);
|
|
58
|
-
if (
|
|
57
|
+
const row = data.getRow(mutation, { count: 0, proportion: 0 });
|
|
58
|
+
if (
|
|
59
|
+
!row.some(
|
|
60
|
+
(value) => value.proportion >= proportionInterval.min && value.proportion <= proportionInterval.max,
|
|
61
|
+
)
|
|
62
|
+
) {
|
|
59
63
|
data.deleteRow(mutation);
|
|
60
64
|
}
|
|
61
65
|
});
|
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
type MutationOverTimeMutationValue,
|
|
6
6
|
} from '../../query/queryMutationsOverTime';
|
|
7
7
|
import { type Deletion, type Substitution } from '../../utils/mutations';
|
|
8
|
-
import { compareTemporal, type Temporal } from '../../utils/temporal';
|
|
8
|
+
import { compareTemporal, type Temporal, YearMonthDay } from '../../utils/temporal';
|
|
9
|
+
import { UserFacingError } from '../components/error-display';
|
|
10
|
+
import Tooltip from '../components/tooltip';
|
|
9
11
|
import { singleGraphColorRGBByName } from '../shared/charts/colors';
|
|
10
12
|
import { formatProportion } from '../shared/table/formatProportion';
|
|
11
13
|
|
|
@@ -13,8 +15,18 @@ export interface MutationsOverTimeGridProps {
|
|
|
13
15
|
data: MutationOverTimeDataGroupedByMutation;
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
const MAX_NUMBER_OF_GRID_ROWS = 100;
|
|
19
|
+
|
|
16
20
|
const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({ data }) => {
|
|
17
21
|
const mutations = data.getFirstAxisKeys();
|
|
22
|
+
if (mutations.length > MAX_NUMBER_OF_GRID_ROWS) {
|
|
23
|
+
throw new UserFacingError(
|
|
24
|
+
'Too many mutations',
|
|
25
|
+
`The dataset contains ${mutations.length} mutations. ` +
|
|
26
|
+
`Please adapt the filters to reduce the number to below ${MAX_NUMBER_OF_GRID_ROWS}.`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
const dates = data.getSecondAxisKeys().sort((a, b) => compareTemporal(a, b));
|
|
19
31
|
|
|
20
32
|
return (
|
|
@@ -35,7 +47,7 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
|
|
|
35
47
|
<MutationCell mutation={mutation} />
|
|
36
48
|
</div>
|
|
37
49
|
{dates.map((date, j) => {
|
|
38
|
-
const value = data.get(mutation, date) ?? 0;
|
|
50
|
+
const value = data.get(mutation, date) ?? { proportion: 0, count: 0 };
|
|
39
51
|
return (
|
|
40
52
|
<div
|
|
41
53
|
style={{ gridRowStart: i + 1, gridColumnStart: j + 2 }}
|
|
@@ -56,22 +68,45 @@ const ProportionCell: FunctionComponent<{
|
|
|
56
68
|
value: MutationOverTimeMutationValue;
|
|
57
69
|
date: Temporal;
|
|
58
70
|
mutation: Substitution | Deletion;
|
|
59
|
-
}> = ({ value }) => {
|
|
60
|
-
|
|
71
|
+
}> = ({ value, mutation, date }) => {
|
|
72
|
+
const tooltipContent = (
|
|
73
|
+
<div>
|
|
74
|
+
<p>
|
|
75
|
+
<span className='font-bold'>{date.englishName()}</span> ({timeIntervalDisplay(date)})
|
|
76
|
+
</p>
|
|
77
|
+
<p>{mutation.code}</p>
|
|
78
|
+
<p>Proportion: {formatProportion(value.proportion)}</p>
|
|
79
|
+
<p>Count: {value.count}</p>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
|
|
61
83
|
return (
|
|
62
84
|
<>
|
|
63
85
|
<div className={'py-1'}>
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
<Tooltip content={tooltipContent}>
|
|
87
|
+
<div
|
|
88
|
+
style={{
|
|
89
|
+
backgroundColor: backgroundColor(value.proportion),
|
|
90
|
+
color: textColor(value.proportion),
|
|
91
|
+
}}
|
|
92
|
+
className='text-center hover:font-bold text-xs'
|
|
93
|
+
>
|
|
94
|
+
{formatProportion(value.proportion, 0)}
|
|
95
|
+
</div>
|
|
96
|
+
</Tooltip>
|
|
70
97
|
</div>
|
|
71
98
|
</>
|
|
72
99
|
);
|
|
73
100
|
};
|
|
74
101
|
|
|
102
|
+
const timeIntervalDisplay = (date: Temporal) => {
|
|
103
|
+
if (date instanceof YearMonthDay) {
|
|
104
|
+
return date.toString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return `${date.firstDay.toString()} - ${date.lastDay.toString()}`;
|
|
108
|
+
};
|
|
109
|
+
|
|
75
110
|
const backgroundColor = (proportion: number) => {
|
|
76
111
|
// TODO(#353): Make minAlpha and maxAlpha configurable
|
|
77
112
|
const minAlpha = 0.0;
|
|
@@ -27,7 +27,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
27
27
|
dateFieldTo: '2023-01-01',
|
|
28
28
|
minProportion: 0.001,
|
|
29
29
|
},
|
|
30
|
-
response: { data: [getSomeTestMutation(0.1), getSomeOtherTestMutation(0.4)] },
|
|
30
|
+
response: { data: [getSomeTestMutation(0.1, 1), getSomeOtherTestMutation(0.4, 4)] },
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
body: {
|
|
@@ -36,7 +36,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
36
36
|
dateFieldTo: '2023-01-02',
|
|
37
37
|
minProportion: 0.001,
|
|
38
38
|
},
|
|
39
|
-
response: { data: [getSomeTestMutation(0.2)] },
|
|
39
|
+
response: { data: [getSomeTestMutation(0.2, 2)] },
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
body: {
|
|
@@ -45,7 +45,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
45
45
|
dateFieldTo: '2023-01-03',
|
|
46
46
|
minProportion: 0.001,
|
|
47
47
|
},
|
|
48
|
-
response: { data: [getSomeTestMutation(0.3)] },
|
|
48
|
+
response: { data: [getSomeTestMutation(0.3, 3)] },
|
|
49
49
|
},
|
|
50
50
|
],
|
|
51
51
|
'nucleotide',
|
|
@@ -53,9 +53,17 @@ describe('queryMutationsOverTime', () => {
|
|
|
53
53
|
|
|
54
54
|
const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
|
|
55
55
|
|
|
56
|
-
expect(result.getAsArray(0)).to.deep.equal([
|
|
57
|
-
[
|
|
58
|
-
|
|
56
|
+
expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([
|
|
57
|
+
[
|
|
58
|
+
{ proportion: 0.1, count: 1 },
|
|
59
|
+
{ proportion: 0.2, count: 2 },
|
|
60
|
+
{ proportion: 0.3, count: 3 },
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
{ proportion: 0.4, count: 4 },
|
|
64
|
+
{ proportion: 0, count: 0 },
|
|
65
|
+
{ proportion: 0, count: 0 },
|
|
66
|
+
],
|
|
59
67
|
]);
|
|
60
68
|
|
|
61
69
|
const sequences = result.getFirstAxisKeys();
|
|
@@ -91,7 +99,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
91
99
|
dateFieldTo: '2023-01-01',
|
|
92
100
|
minProportion: 0.001,
|
|
93
101
|
},
|
|
94
|
-
response: { data: [getSomeTestMutation(0.1), getSomeOtherTestMutation(0.4)] },
|
|
102
|
+
response: { data: [getSomeTestMutation(0.1, 1), getSomeOtherTestMutation(0.4, 4)] },
|
|
95
103
|
},
|
|
96
104
|
{
|
|
97
105
|
body: {
|
|
@@ -109,7 +117,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
109
117
|
dateFieldTo: '2023-01-03',
|
|
110
118
|
minProportion: 0.001,
|
|
111
119
|
},
|
|
112
|
-
response: { data: [getSomeTestMutation(0.3)] },
|
|
120
|
+
response: { data: [getSomeTestMutation(0.3, 3)] },
|
|
113
121
|
},
|
|
114
122
|
],
|
|
115
123
|
'nucleotide',
|
|
@@ -117,9 +125,17 @@ describe('queryMutationsOverTime', () => {
|
|
|
117
125
|
|
|
118
126
|
const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
|
|
119
127
|
|
|
120
|
-
expect(result.getAsArray(0)).to.deep.equal([
|
|
121
|
-
[
|
|
122
|
-
|
|
128
|
+
expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([
|
|
129
|
+
[
|
|
130
|
+
{ proportion: 0.1, count: 1 },
|
|
131
|
+
{ proportion: 0.3, count: 3 },
|
|
132
|
+
{ proportion: 0, count: 0 },
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
{ proportion: 0.4, count: 4 },
|
|
136
|
+
{ proportion: 0, count: 0 },
|
|
137
|
+
{ proportion: 0, count: 0 },
|
|
138
|
+
],
|
|
123
139
|
]);
|
|
124
140
|
|
|
125
141
|
const sequences = result.getFirstAxisKeys();
|
|
@@ -181,7 +197,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
181
197
|
|
|
182
198
|
const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
|
|
183
199
|
|
|
184
|
-
expect(result.getAsArray(0)).to.deep.equal([]);
|
|
200
|
+
expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([]);
|
|
185
201
|
expect(result.getFirstAxisKeys()).to.deep.equal([]);
|
|
186
202
|
expect(result.getSecondAxisKeys()).to.deep.equal([]);
|
|
187
203
|
});
|
|
@@ -209,7 +225,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
209
225
|
dateFieldTo: '2023-01-02',
|
|
210
226
|
minProportion: 0.001,
|
|
211
227
|
},
|
|
212
|
-
response: { data: [getSomeTestMutation(0.2)] },
|
|
228
|
+
response: { data: [getSomeTestMutation(0.2, 2)] },
|
|
213
229
|
},
|
|
214
230
|
{
|
|
215
231
|
body: {
|
|
@@ -218,7 +234,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
218
234
|
dateFieldTo: '2023-01-03',
|
|
219
235
|
minProportion: 0.001,
|
|
220
236
|
},
|
|
221
|
-
response: { data: [getSomeTestMutation(0.3)] },
|
|
237
|
+
response: { data: [getSomeTestMutation(0.3, 3)] },
|
|
222
238
|
},
|
|
223
239
|
],
|
|
224
240
|
'nucleotide',
|
|
@@ -226,7 +242,12 @@ describe('queryMutationsOverTime', () => {
|
|
|
226
242
|
|
|
227
243
|
const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
|
|
228
244
|
|
|
229
|
-
expect(result.getAsArray(0)).to.deep.equal([
|
|
245
|
+
expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([
|
|
246
|
+
[
|
|
247
|
+
{ proportion: 0.2, count: 2 },
|
|
248
|
+
{ proportion: 0.3, count: 3 },
|
|
249
|
+
],
|
|
250
|
+
]);
|
|
230
251
|
|
|
231
252
|
const sequences = result.getFirstAxisKeys();
|
|
232
253
|
expect(sequences[0].code).toBe('sequenceName:A123T');
|
|
@@ -259,7 +280,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
259
280
|
dateFieldTo: '2023-01-01',
|
|
260
281
|
minProportion: 0.001,
|
|
261
282
|
},
|
|
262
|
-
response: { data: [getSomeTestMutation(0.1)] },
|
|
283
|
+
response: { data: [getSomeTestMutation(0.1, 1)] },
|
|
263
284
|
},
|
|
264
285
|
{
|
|
265
286
|
body: {
|
|
@@ -268,7 +289,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
268
289
|
dateFieldTo: '2023-01-02',
|
|
269
290
|
minProportion: 0.001,
|
|
270
291
|
},
|
|
271
|
-
response: { data: [getSomeTestMutation(0.2)] },
|
|
292
|
+
response: { data: [getSomeTestMutation(0.2, 2)] },
|
|
272
293
|
},
|
|
273
294
|
],
|
|
274
295
|
'nucleotide',
|
|
@@ -276,7 +297,12 @@ describe('queryMutationsOverTime', () => {
|
|
|
276
297
|
|
|
277
298
|
const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
|
|
278
299
|
|
|
279
|
-
expect(result.getAsArray(0)).to.deep.equal([
|
|
300
|
+
expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([
|
|
301
|
+
[
|
|
302
|
+
{ proportion: 0.1, count: 1 },
|
|
303
|
+
{ proportion: 0.2, count: 2 },
|
|
304
|
+
],
|
|
305
|
+
]);
|
|
280
306
|
|
|
281
307
|
const sequences = result.getFirstAxisKeys();
|
|
282
308
|
expect(sequences[0].code).toBe('sequenceName:A123T');
|
|
@@ -309,7 +335,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
309
335
|
dateFieldTo: '2023-01-02',
|
|
310
336
|
minProportion: 0.001,
|
|
311
337
|
},
|
|
312
|
-
response: { data: [getSomeTestMutation(0.2)] },
|
|
338
|
+
response: { data: [getSomeTestMutation(0.2, 2)] },
|
|
313
339
|
},
|
|
314
340
|
],
|
|
315
341
|
'nucleotide',
|
|
@@ -317,7 +343,7 @@ describe('queryMutationsOverTime', () => {
|
|
|
317
343
|
|
|
318
344
|
const result = await queryMutationsOverTimeData(lapisFilter, 'nucleotide', DUMMY_LAPIS_URL, dateField, 'day');
|
|
319
345
|
|
|
320
|
-
expect(result.getAsArray(0)).to.deep.equal([[0.2]]);
|
|
346
|
+
expect(result.getAsArray({ count: 0, proportion: 0 })).to.deep.equal([[{ proportion: 0.2, count: 2 }]]);
|
|
321
347
|
|
|
322
348
|
const sequences = result.getFirstAxisKeys();
|
|
323
349
|
expect(sequences[0].code).toBe('sequenceName:A123T');
|
|
@@ -326,11 +352,11 @@ describe('queryMutationsOverTime', () => {
|
|
|
326
352
|
expect(dates[0].toString()).toBe('2023-01-02');
|
|
327
353
|
});
|
|
328
354
|
|
|
329
|
-
function getSomeTestMutation(proportion: number) {
|
|
355
|
+
function getSomeTestMutation(proportion: number, count: number) {
|
|
330
356
|
return {
|
|
331
357
|
mutation: 'sequenceName:A123T',
|
|
332
358
|
proportion,
|
|
333
|
-
count
|
|
359
|
+
count,
|
|
334
360
|
sequenceName: 'sequenceName',
|
|
335
361
|
mutationFrom: 'A',
|
|
336
362
|
mutationTo: 'T',
|
|
@@ -338,11 +364,11 @@ describe('queryMutationsOverTime', () => {
|
|
|
338
364
|
};
|
|
339
365
|
}
|
|
340
366
|
|
|
341
|
-
function getSomeOtherTestMutation(proportion: number) {
|
|
367
|
+
function getSomeOtherTestMutation(proportion: number, count: number) {
|
|
342
368
|
return {
|
|
343
369
|
mutation: 'otherSequenceName:A123T',
|
|
344
370
|
proportion,
|
|
345
|
-
count
|
|
371
|
+
count,
|
|
346
372
|
sequenceName: 'otherSequenceName',
|
|
347
373
|
mutationFrom: 'G',
|
|
348
374
|
mutationTo: 'C',
|
|
@@ -5,6 +5,7 @@ import { GroupByAndSumOperator } from '../operator/GroupByAndSumOperator';
|
|
|
5
5
|
import { MapOperator } from '../operator/MapOperator';
|
|
6
6
|
import { RenameFieldOperator } from '../operator/RenameFieldOperator';
|
|
7
7
|
import { SortOperator } from '../operator/SortOperator';
|
|
8
|
+
import { UserFacingError } from '../preact/components/error-display';
|
|
8
9
|
import {
|
|
9
10
|
type LapisFilter,
|
|
10
11
|
type SequenceType,
|
|
@@ -26,13 +27,15 @@ export type MutationOverTimeData = {
|
|
|
26
27
|
mutations: SubstitutionOrDeletionEntry[];
|
|
27
28
|
};
|
|
28
29
|
|
|
29
|
-
export type MutationOverTimeMutationValue = number;
|
|
30
|
+
export type MutationOverTimeMutationValue = { proportion: number; count: number };
|
|
30
31
|
export type MutationOverTimeDataGroupedByMutation = Map2d<
|
|
31
32
|
Substitution | Deletion,
|
|
32
33
|
Temporal,
|
|
33
34
|
MutationOverTimeMutationValue
|
|
34
35
|
>;
|
|
35
36
|
|
|
37
|
+
const MAX_NUMBER_OF_GRID_COLUMNS = 200;
|
|
38
|
+
|
|
36
39
|
export async function queryMutationsOverTimeData(
|
|
37
40
|
lapisFilter: LapisFilter,
|
|
38
41
|
sequenceType: 'nucleotide' | 'amino acid',
|
|
@@ -43,6 +46,15 @@ export async function queryMutationsOverTimeData(
|
|
|
43
46
|
) {
|
|
44
47
|
const allDates = await getDatesInDataset(lapisFilter, lapis, granularity, lapisDateField, signal);
|
|
45
48
|
|
|
49
|
+
if (allDates.length > MAX_NUMBER_OF_GRID_COLUMNS) {
|
|
50
|
+
throw new UserFacingError(
|
|
51
|
+
'Too many dates',
|
|
52
|
+
`The dataset would contain ${allDates.length} date intervals. ` +
|
|
53
|
+
`Please reduce the number to below ${MAX_NUMBER_OF_GRID_COLUMNS} to display the data. ` +
|
|
54
|
+
'You can achieve this by either narrowing the date range in the provided LAPIS filter or by selecting a larger granularity.',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
const subQueries = allDates.map(async (date) => {
|
|
47
59
|
const dateFrom = date.firstDay.toString();
|
|
48
60
|
const dateTo = date.lastDay.toString();
|
|
@@ -133,14 +145,17 @@ function fetchAndPrepareSubstitutionsOrDeletions(filter: LapisFilter, sequenceTy
|
|
|
133
145
|
}
|
|
134
146
|
|
|
135
147
|
export function groupByMutation(data: MutationOverTimeData[]) {
|
|
136
|
-
const dataArray = new Map2d<Substitution | Deletion, Temporal,
|
|
148
|
+
const dataArray = new Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>(
|
|
137
149
|
(mutation) => mutation.code,
|
|
138
150
|
(date) => date.toString(),
|
|
139
151
|
);
|
|
140
152
|
|
|
141
153
|
data.forEach((mutationData) => {
|
|
142
154
|
mutationData.mutations.forEach((mutationEntry) => {
|
|
143
|
-
dataArray.set(mutationEntry.mutation, mutationData.date,
|
|
155
|
+
dataArray.set(mutationEntry.mutation, mutationData.date, {
|
|
156
|
+
count: mutationEntry.count,
|
|
157
|
+
proportion: mutationEntry.proportion,
|
|
158
|
+
});
|
|
144
159
|
});
|
|
145
160
|
});
|
|
146
161
|
|
|
@@ -150,14 +165,14 @@ export function groupByMutation(data: MutationOverTimeData[]) {
|
|
|
150
165
|
}
|
|
151
166
|
|
|
152
167
|
function addZeroValuesForDatesWithNoMutationData(
|
|
153
|
-
dataArray: Map2d<Substitution | Deletion, Temporal,
|
|
168
|
+
dataArray: Map2d<Substitution | Deletion, Temporal, MutationOverTimeMutationValue>,
|
|
154
169
|
data: MutationOverTimeData[],
|
|
155
170
|
) {
|
|
156
171
|
if (dataArray.getFirstAxisKeys().length !== 0) {
|
|
157
172
|
const someMutation = dataArray.getFirstAxisKeys()[0];
|
|
158
173
|
data.forEach((mutationData) => {
|
|
159
174
|
if (mutationData.mutations.length === 0) {
|
|
160
|
-
dataArray.set(someMutation, mutationData.date, 0);
|
|
175
|
+
dataArray.set(someMutation, mutationData.date, { count: 0, proportion: 0 });
|
|
161
176
|
}
|
|
162
177
|
});
|
|
163
178
|
}
|
|
@@ -60,6 +60,8 @@ describe('YearMonthDay', () => {
|
|
|
60
60
|
// seems to be a bug in dayjs: https://github.com/iamkun/dayjs/issues/2620
|
|
61
61
|
expect(underTest.week.text).equal('2019-01');
|
|
62
62
|
expect(underTest.text).equal('2020-01-01');
|
|
63
|
+
expect(underTest.firstDay.text).equal('2020-01-01');
|
|
64
|
+
expect(underTest.lastDay.text).equal('2020-01-01');
|
|
63
65
|
});
|
|
64
66
|
});
|
|
65
67
|
|
|
@@ -71,6 +73,7 @@ describe('YearWeek', () => {
|
|
|
71
73
|
expect(underTest.isoWeekNumber).equal(2);
|
|
72
74
|
expect(underTest.firstDay.text).equal('2020-01-06');
|
|
73
75
|
expect(underTest.text).equal('2020-02');
|
|
76
|
+
expect(underTest.lastDay.text).equal('2020-01-12');
|
|
74
77
|
});
|
|
75
78
|
});
|
|
76
79
|
|
|
@@ -82,6 +85,7 @@ describe('YearMonth', () => {
|
|
|
82
85
|
expect(underTest.monthNumber).equal(1);
|
|
83
86
|
expect(underTest.text).equal('2020-01');
|
|
84
87
|
expect(underTest.firstDay.text).equal('2020-01-01');
|
|
88
|
+
expect(underTest.lastDay.text).equal('2020-01-31');
|
|
85
89
|
});
|
|
86
90
|
});
|
|
87
91
|
|
|
@@ -93,5 +97,6 @@ describe('Year', () => {
|
|
|
93
97
|
expect(underTest.text).equal('2020');
|
|
94
98
|
expect(underTest.firstDay.text).equal('2020-01-01');
|
|
95
99
|
expect(underTest.firstMonth.text).equal('2020-01');
|
|
100
|
+
expect(underTest.lastDay.text).equal('2020-12-31');
|
|
96
101
|
});
|
|
97
102
|
});
|