@genspectrum/dashboard-components 0.10.1 → 0.10.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/README.md +19 -19
- package/custom-elements.json +72 -56
- package/dist/assets/{mutationOverTimeWorker-CvZg52rf.js.map → mutationOverTimeWorker-CNg_ztNp.js.map} +1 -1
- package/dist/components.d.ts +19 -28
- package/dist/components.js +343 -96
- package/dist/components.js.map +1 -1
- package/dist/{utilEntrypoint-g4DsyhU7.js → dateRangeOption-DjtcAEWq.js} +46 -2
- package/dist/dateRangeOption-DjtcAEWq.js.map +1 -0
- package/dist/style.css +11 -5
- package/dist/util.d.ts +112 -21
- package/dist/util.js +3 -2
- package/package.json +2 -2
- package/src/preact/aggregatedData/aggregate.stories.tsx +14 -0
- package/src/preact/aggregatedData/aggregate.tsx +17 -15
- package/src/preact/components/color-scale-selector.tsx +7 -3
- package/src/preact/components/error-boundary.stories.tsx +24 -3
- package/src/preact/components/error-boundary.tsx +38 -5
- package/src/preact/components/error-display.tsx +62 -6
- package/src/preact/components/tabs.tsx +2 -2
- package/src/preact/dateRangeSelector/computeInitialValues.spec.ts +8 -2
- package/src/preact/dateRangeSelector/computeInitialValues.ts +6 -0
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +16 -2
- package/src/preact/dateRangeSelector/date-range-selector.tsx +20 -15
- package/src/preact/dateRangeSelector/dateRangeOption.ts +10 -5
- package/src/preact/lineageFilter/lineage-filter.stories.tsx +18 -4
- package/src/preact/lineageFilter/lineage-filter.tsx +15 -10
- package/src/preact/locationFilter/location-filter.stories.tsx +14 -0
- package/src/preact/locationFilter/location-filter.tsx +15 -10
- package/src/preact/mutationComparison/mutation-comparison-venn.tsx +17 -18
- package/src/preact/mutationComparison/mutation-comparison.tsx +18 -12
- package/src/preact/mutationFilter/mutation-filter.tsx +26 -13
- package/src/preact/mutations/mutations.tsx +16 -12
- package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutationsByDay.ts +1326 -9341
- package/src/preact/mutationsOverTime/__mockData__/byWeek.ts +615 -4920
- package/src/preact/mutationsOverTime/__mockData__/defaultMockData.ts +2203 -17624
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +16 -8
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +14 -0
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +19 -17
- package/src/preact/numberSequencesOverTime/number-sequences-over-time.stories.tsx +14 -0
- package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +22 -14
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +14 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +28 -19
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +14 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +18 -15
- package/src/preact/shared/charts/confideceInterval.ts +10 -8
- package/src/preact/shared/charts/getYAxisMax.ts +10 -5
- package/src/preact/shared/stories/expectInvalidAttributesErrorMessage.ts +13 -0
- package/src/preact/statistic/statistics.tsx +10 -8
- package/src/preact/textInput/text-input.stories.tsx +14 -0
- package/src/preact/textInput/text-input.tsx +16 -11
- package/src/preact/webWorkers/useWebWorker.ts +8 -4
- package/src/query/queryAggregateData.ts +2 -1
- package/src/query/queryMutationsOverTime.spec.ts +12 -27
- package/src/query/queryMutationsOverTime.ts +2 -6
- package/src/types.ts +31 -7
- package/src/utilEntrypoint.ts +15 -0
- package/src/utils/map2d.spec.ts +10 -10
- package/src/utils/map2d.ts +10 -10
- package/src/web-components/app.stories.ts +17 -2
- package/src/web-components/app.ts +17 -5
- package/src/web-components/input/gs-date-range-selector.stories.ts +2 -2
- package/src/web-components/input/gs-date-range-selector.tsx +3 -3
- package/src/web-components/input/gs-lineage-filter.tsx +1 -1
- package/src/web-components/input/gs-location-filter.tsx +2 -2
- package/src/web-components/input/gs-mutation-filter.stories.ts +2 -0
- package/src/web-components/input/gs-text-input.tsx +2 -2
- package/src/web-components/introduction.mdx +4 -4
- package/src/web-components/visualization/data_visualization_statistical_analysis.mdx +3 -3
- package/src/web-components/visualization/gs-aggregate.tsx +2 -2
- package/src/web-components/visualization/gs-mutations-over-time.tsx +1 -3
- package/src/web-components/visualization/gs-mutations.tsx +1 -3
- package/src/web-components/visualization/gs-number-sequences-over-time.tsx +1 -3
- package/src/web-components/visualization/gs-prevalence-over-time.tsx +3 -6
- package/src/web-components/visualization/gs-relative-growth-advantage.tsx +1 -5
- package/standalone-bundle/assets/{mutationOverTimeWorker-CypX_PYM.js.map → mutationOverTimeWorker-cIyshfj_.js.map} +1 -1
- package/standalone-bundle/dashboard-components.js +8800 -8577
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
- package/dist/utilEntrypoint-g4DsyhU7.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
import { useEffect, useRef } from 'preact/hooks';
|
|
3
|
+
import { type ZodError } from 'zod';
|
|
3
4
|
|
|
4
5
|
import { LapisError, UnknownLapisError } from '../../lapisApi/lapisApi';
|
|
5
6
|
|
|
@@ -24,9 +25,19 @@ export class UserFacingError extends Error {
|
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
export class InvalidPropsError extends Error {
|
|
29
|
+
constructor(
|
|
30
|
+
public readonly zodError: ZodError,
|
|
31
|
+
public readonly componentProps: Record<string, unknown>,
|
|
32
|
+
) {
|
|
33
|
+
super(zodError.message);
|
|
34
|
+
this.name = 'InvalidPropsError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
export type ErrorDisplayProps = {
|
|
28
39
|
error: Error;
|
|
29
|
-
resetError
|
|
40
|
+
resetError?: () => void;
|
|
30
41
|
layout?: 'horizontal' | 'vertical';
|
|
31
42
|
};
|
|
32
43
|
|
|
@@ -66,7 +77,7 @@ export const ErrorDisplay: FunctionComponent<ErrorDisplayProps> = ({ error, rese
|
|
|
66
77
|
</button>
|
|
67
78
|
</form>
|
|
68
79
|
<h1 class='text-lg'>{details.headline}</h1>
|
|
69
|
-
<
|
|
80
|
+
<div class='py-4'>{details.message}</div>
|
|
70
81
|
</div>
|
|
71
82
|
<form method='dialog' class='modal-backdrop'>
|
|
72
83
|
<button>close</button>
|
|
@@ -76,10 +87,12 @@ export const ErrorDisplay: FunctionComponent<ErrorDisplayProps> = ({ error, rese
|
|
|
76
87
|
)}
|
|
77
88
|
</div>
|
|
78
89
|
</div>
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
{resetError !== undefined && (
|
|
91
|
+
<button onClick={resetError} className='btn btn-sm flex items-center m-4'>
|
|
92
|
+
<span className='iconify mdi--reload text-lg' />
|
|
93
|
+
Try again
|
|
94
|
+
</button>
|
|
95
|
+
)}
|
|
83
96
|
</div>
|
|
84
97
|
);
|
|
85
98
|
};
|
|
@@ -115,5 +128,48 @@ function getDisplayedErrorMessage(error: Error) {
|
|
|
115
128
|
};
|
|
116
129
|
}
|
|
117
130
|
|
|
131
|
+
if (error instanceof InvalidPropsError) {
|
|
132
|
+
return {
|
|
133
|
+
headline: 'Error - Invalid component attributes',
|
|
134
|
+
details: { headline: 'Invalid component attributes', message: <ZodErrorDetails error={error} /> },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
118
138
|
return { headline: 'Error', details: undefined };
|
|
119
139
|
}
|
|
140
|
+
|
|
141
|
+
function ZodErrorDetails({ error }: { error: InvalidPropsError }) {
|
|
142
|
+
const firstError = error.zodError.errors[0];
|
|
143
|
+
return (
|
|
144
|
+
<>
|
|
145
|
+
<p>
|
|
146
|
+
<span className='font-bold'>You are a regular user?</span> Unfortunately, there is nothing you can do at
|
|
147
|
+
the moment. This component is misconfigured. Please contact the administrator of this page.
|
|
148
|
+
</p>
|
|
149
|
+
<p>
|
|
150
|
+
<span className='font-bold'>You are the administrator of this page?</span> You supplied invalid
|
|
151
|
+
attributes to this component. Please check the browser console for more detailed error messages.
|
|
152
|
+
</p>
|
|
153
|
+
{firstError.code === 'invalid_type' && firstError.received === 'null' && (
|
|
154
|
+
<p>
|
|
155
|
+
Is the "{firstError.path[0]}" attribute in the HTML of the correct type? The attribute is expected
|
|
156
|
+
to be of type "{firstError.expected}".
|
|
157
|
+
</p>
|
|
158
|
+
)}
|
|
159
|
+
<p>This is a summary of the unexpected attribute values:</p>
|
|
160
|
+
<ul class='m-4 list-outside list-disc '>
|
|
161
|
+
{error.zodError.issues.map((issue, index) => {
|
|
162
|
+
const actual =
|
|
163
|
+
issue.path[0] in error.componentProps
|
|
164
|
+
? `'${JSON.stringify(error.componentProps[issue.path[0]])}'`
|
|
165
|
+
: '';
|
|
166
|
+
return (
|
|
167
|
+
<li key={index}>
|
|
168
|
+
Unexpected value {actual} for "{issue.path.join('.')}": {issue.message}
|
|
169
|
+
</li>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
</ul>
|
|
173
|
+
</>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -13,7 +13,7 @@ interface ComponentTabsProps {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
|
|
16
|
-
const [activeTab, setActiveTab] = useState(tabs[0]
|
|
16
|
+
const [activeTab, setActiveTab] = useState(tabs[0]?.title);
|
|
17
17
|
const [heightOfTabs, setHeightOfTabs] = useState('3rem');
|
|
18
18
|
const tabRef = useRef<HTMLDivElement>(null);
|
|
19
19
|
|
|
@@ -65,7 +65,7 @@ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
|
|
|
65
65
|
{toolbar && <div className='py-2 flex flex-wrap gap-y-1'>{toolbarElement}</div>}
|
|
66
66
|
</div>
|
|
67
67
|
<div
|
|
68
|
-
className={`p-2 border-2 border-gray-100 rounded-b-md rounded-tr-md ${activeTab === tabs[0]
|
|
68
|
+
className={`p-2 border-2 border-gray-100 rounded-b-md rounded-tr-md ${activeTab === tabs[0]?.title ? '' : 'rounded-tl-md'}`}
|
|
69
69
|
style={{ height: `calc(100% - ${heightOfTabs})` }}
|
|
70
70
|
>
|
|
71
71
|
{tabs.map((tab) => (
|
|
@@ -50,9 +50,15 @@ describe('computeInitialValues', () => {
|
|
|
50
50
|
expectDateMatches(result.initialSelectedDateTo, today);
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
-
it('should
|
|
53
|
+
it('should throw when initial value is unknown', () => {
|
|
54
|
+
expect(() =>
|
|
55
|
+
computeInitialValues('not a known value', undefined, undefined, earliestDate, dateRangeOptions),
|
|
56
|
+
).toThrowError(/Invalid initialValue "not a known value", It must be one of/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should throw when initial value is set but no options are provided', () => {
|
|
54
60
|
expect(() => computeInitialValues('not a known value', undefined, undefined, earliestDate, [])).toThrowError(
|
|
55
|
-
/
|
|
61
|
+
/There are no selectable options/,
|
|
56
62
|
);
|
|
57
63
|
});
|
|
58
64
|
|
|
@@ -18,6 +18,12 @@ export function computeInitialValues(
|
|
|
18
18
|
const initialSelectedDateRange = selectableOptions.find((option) => option.value === initialValue)?.value;
|
|
19
19
|
|
|
20
20
|
if (initialValue !== undefined && initialSelectedDateRange === undefined) {
|
|
21
|
+
if (selectableOptions.length === 0) {
|
|
22
|
+
throw new UserFacingError(
|
|
23
|
+
'Invalid initialValue',
|
|
24
|
+
'There are no selectable options, but initialValue is set.',
|
|
25
|
+
);
|
|
26
|
+
}
|
|
21
27
|
throw new UserFacingError(
|
|
22
28
|
'Invalid initialValue',
|
|
23
29
|
`Invalid initialValue "${initialValue}", It must be one of ${selectableOptions.map((option) => `'${option.value}'`).join(', ')}`,
|
|
@@ -8,6 +8,7 @@ import { previewHandles } from '../../../.storybook/preview';
|
|
|
8
8
|
import { LAPIS_URL } from '../../constants';
|
|
9
9
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
10
10
|
import { dateRangeOptionPresets } from './dateRangeOption';
|
|
11
|
+
import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectInvalidAttributesErrorMessage';
|
|
11
12
|
|
|
12
13
|
const earliestDate = '1970-01-01';
|
|
13
14
|
|
|
@@ -55,8 +56,8 @@ const meta: Meta<DateRangeSelectorProps> = {
|
|
|
55
56
|
initialValue: dateRangeOptionPresets.lastMonth.label,
|
|
56
57
|
dateColumn: 'aDateColumn',
|
|
57
58
|
width: '100%',
|
|
58
|
-
initialDateFrom:
|
|
59
|
-
initialDateTo:
|
|
59
|
+
initialDateFrom: undefined,
|
|
60
|
+
initialDateTo: undefined,
|
|
60
61
|
},
|
|
61
62
|
};
|
|
62
63
|
|
|
@@ -221,6 +222,19 @@ export const HandlesInvalidInitialDateFrom: StoryObj<DateRangeSelectorProps> = {
|
|
|
221
222
|
},
|
|
222
223
|
};
|
|
223
224
|
|
|
225
|
+
export const WithNoDateColumn: StoryObj<DateRangeSelectorProps> = {
|
|
226
|
+
...Primary,
|
|
227
|
+
args: {
|
|
228
|
+
...Primary.args,
|
|
229
|
+
dateColumn: '',
|
|
230
|
+
},
|
|
231
|
+
play: async ({ canvasElement, step }) => {
|
|
232
|
+
step('expect error message', async () => {
|
|
233
|
+
await expectInvalidAttributesErrorMessage(canvasElement, 'String must contain at least 1 character(s)');
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
224
238
|
async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRenderer, unknown>) {
|
|
225
239
|
const canvas = within(canvasElement);
|
|
226
240
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import flatpickr from 'flatpickr';
|
|
2
2
|
import 'flatpickr/dist/flatpickr.min.css';
|
|
3
3
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
4
|
+
import z from 'zod';
|
|
4
5
|
|
|
5
6
|
import { computeInitialValues } from './computeInitialValues';
|
|
6
7
|
import { toYYYYMMDD } from './dateConversion';
|
|
7
|
-
import {
|
|
8
|
+
import { DateRangeOptionChangedEvent, dateRangeOptionSchema, type DateRangeSelectOption } from './dateRangeOption';
|
|
8
9
|
import { getDatesForSelectorValue, getSelectableOptions } from './selectableOptions';
|
|
9
10
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
10
11
|
import { Select } from '../components/select';
|
|
@@ -12,24 +13,28 @@ import type { ScaleType } from '../shared/charts/getYAxisScale';
|
|
|
12
13
|
|
|
13
14
|
const customOption = 'Custom';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const dateRangeSelectorInnerPropsSchema = z.object({
|
|
17
|
+
dateRangeOptions: z.array(dateRangeOptionSchema),
|
|
18
|
+
earliestDate: z.string().date(),
|
|
19
|
+
initialValue: z.string().optional(),
|
|
20
|
+
initialDateFrom: z.string().date().optional(),
|
|
21
|
+
initialDateTo: z.string().date().optional(),
|
|
22
|
+
dateColumn: z.string().min(1),
|
|
23
|
+
});
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
initialValue: string | undefined;
|
|
23
|
-
initialDateFrom: string;
|
|
24
|
-
initialDateTo: string;
|
|
25
|
-
dateColumn: string;
|
|
26
|
-
}
|
|
25
|
+
const dateRangeSelectorPropsSchema = dateRangeSelectorInnerPropsSchema.extend({
|
|
26
|
+
width: z.string(),
|
|
27
|
+
});
|
|
27
28
|
|
|
28
|
-
export
|
|
29
|
+
export type DateRangeSelectorProps = z.infer<typeof dateRangeSelectorPropsSchema>;
|
|
30
|
+
export type DateRangeSelectorInnerProps = z.infer<typeof dateRangeSelectorInnerPropsSchema>;
|
|
31
|
+
|
|
32
|
+
export const DateRangeSelector = (props: DateRangeSelectorProps) => {
|
|
33
|
+
const { width, ...innerProps } = props;
|
|
29
34
|
const size = { width, height: '3rem' };
|
|
30
35
|
|
|
31
36
|
return (
|
|
32
|
-
<ErrorBoundary size={size} layout='horizontal'>
|
|
37
|
+
<ErrorBoundary size={size} layout='horizontal' componentProps={props} schema={dateRangeSelectorPropsSchema}>
|
|
33
38
|
<div style={{ width }}>
|
|
34
39
|
<DateRangeSelectorInner {...innerProps} />
|
|
35
40
|
</div>
|
|
@@ -44,7 +49,7 @@ export const DateRangeSelectorInner = ({
|
|
|
44
49
|
dateColumn,
|
|
45
50
|
initialDateFrom,
|
|
46
51
|
initialDateTo,
|
|
47
|
-
}:
|
|
52
|
+
}: DateRangeSelectorInnerProps) => {
|
|
48
53
|
const initialValues = computeInitialValues(
|
|
49
54
|
initialValue,
|
|
50
55
|
initialDateFrom,
|
|
@@ -1,24 +1,29 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
|
|
1
3
|
import { toYYYYMMDD } from './dateConversion';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* A date range option that can be used in the `gs-date-range-selector` component.
|
|
5
7
|
*/
|
|
6
|
-
export
|
|
8
|
+
export const dateRangeOptionSchema = z.object({
|
|
7
9
|
/** The label of the date range option that will be shown to the user */
|
|
8
|
-
label: string
|
|
10
|
+
label: z.string(),
|
|
9
11
|
/**
|
|
10
12
|
* The start date of the date range in the format `YYYY-MM-DD`.
|
|
11
13
|
* If not set, the date range selector will default to the `earliestDate` property.
|
|
12
14
|
*/
|
|
13
|
-
dateFrom
|
|
15
|
+
dateFrom: z.string().date().optional(),
|
|
14
16
|
/**
|
|
15
17
|
* The end date of the date range in the format `YYYY-MM-DD`.
|
|
16
18
|
* If not set, the date range selector will default to the current date.
|
|
17
19
|
*/
|
|
18
|
-
dateTo
|
|
19
|
-
};
|
|
20
|
+
dateTo: z.string().date().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type DateRangeOption = z.infer<typeof dateRangeOptionSchema>;
|
|
20
24
|
|
|
21
25
|
export type DateRangeSelectOption = string | { dateFrom: string; dateTo: string };
|
|
26
|
+
|
|
22
27
|
export class DateRangeOptionChangedEvent extends CustomEvent<DateRangeSelectOption> {
|
|
23
28
|
constructor(detail: DateRangeSelectOption) {
|
|
24
29
|
super('gs-date-range-option-changed', {
|
|
@@ -5,6 +5,7 @@ import { previewHandles } from '../../../.storybook/preview';
|
|
|
5
5
|
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
6
6
|
import aggregatedData from '../../preact/lineageFilter/__mockData__/aggregated.json';
|
|
7
7
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
8
|
+
import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectInvalidAttributesErrorMessage';
|
|
8
9
|
|
|
9
10
|
const meta: Meta = {
|
|
10
11
|
title: 'Input/LineageFilter',
|
|
@@ -31,6 +32,12 @@ const meta: Meta = {
|
|
|
31
32
|
],
|
|
32
33
|
},
|
|
33
34
|
},
|
|
35
|
+
args: {
|
|
36
|
+
lapisField: 'pangoLineage',
|
|
37
|
+
placeholderText: 'Enter lineage',
|
|
38
|
+
initialValue: '',
|
|
39
|
+
width: '100%',
|
|
40
|
+
},
|
|
34
41
|
};
|
|
35
42
|
|
|
36
43
|
export default meta;
|
|
@@ -46,10 +53,17 @@ export const Default: StoryObj<LineageFilterProps> = {
|
|
|
46
53
|
/>
|
|
47
54
|
</LapisUrlContext.Provider>
|
|
48
55
|
),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const WithNoLapisField: StoryObj<LineageFilterProps> = {
|
|
59
|
+
...Default,
|
|
49
60
|
args: {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
...Default.args,
|
|
62
|
+
lapisField: '',
|
|
63
|
+
},
|
|
64
|
+
play: async ({ canvasElement, step }) => {
|
|
65
|
+
step('expect error message', async () => {
|
|
66
|
+
await expectInvalidAttributesErrorMessage(canvasElement, 'String must contain at least 1 character(s)');
|
|
67
|
+
});
|
|
54
68
|
},
|
|
55
69
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
import { useContext, useRef } from 'preact/hooks';
|
|
3
|
+
import z from 'zod';
|
|
3
4
|
|
|
4
5
|
import { fetchLineageAutocompleteList } from './fetchLineageAutocompleteList';
|
|
5
6
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
@@ -9,21 +10,25 @@ import { NoDataDisplay } from '../components/no-data-display';
|
|
|
9
10
|
import { ResizeContainer } from '../components/resize-container';
|
|
10
11
|
import { useQuery } from '../useQuery';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
lapisField: string
|
|
14
|
-
placeholderText: string
|
|
15
|
-
initialValue: string
|
|
16
|
-
}
|
|
13
|
+
const lineageFilterInnerPropsSchema = z.object({
|
|
14
|
+
lapisField: z.string().min(1),
|
|
15
|
+
placeholderText: z.string().optional(),
|
|
16
|
+
initialValue: z.string(),
|
|
17
|
+
});
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
width: string
|
|
20
|
-
}
|
|
19
|
+
const lineageFilterPropsSchema = lineageFilterInnerPropsSchema.extend({
|
|
20
|
+
width: z.string(),
|
|
21
|
+
});
|
|
21
22
|
|
|
22
|
-
export
|
|
23
|
+
export type LineageFilterInnerProps = z.infer<typeof lineageFilterInnerPropsSchema>;
|
|
24
|
+
export type LineageFilterProps = z.infer<typeof lineageFilterPropsSchema>;
|
|
25
|
+
|
|
26
|
+
export const LineageFilter: FunctionComponent<LineageFilterProps> = (props) => {
|
|
27
|
+
const { width, ...innerProps } = props;
|
|
23
28
|
const size = { width, height: '3rem' };
|
|
24
29
|
|
|
25
30
|
return (
|
|
26
|
-
<ErrorBoundary size={size} layout='horizontal'>
|
|
31
|
+
<ErrorBoundary size={size} layout='horizontal' componentProps={props} schema={lineageFilterPropsSchema}>
|
|
27
32
|
<ResizeContainer size={size}>
|
|
28
33
|
<LineageFilterInner {...innerProps} />
|
|
29
34
|
</ResizeContainer>
|
|
@@ -5,6 +5,7 @@ import { LocationFilter, type LocationFilterProps } from './location-filter';
|
|
|
5
5
|
import { previewHandles } from '../../../.storybook/preview';
|
|
6
6
|
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
7
7
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
8
|
+
import { expectInvalidAttributesErrorMessage } from '../shared/stories/expectInvalidAttributesErrorMessage';
|
|
8
9
|
|
|
9
10
|
const meta: Meta<LocationFilterProps> = {
|
|
10
11
|
title: 'Input/LocationFilter',
|
|
@@ -75,3 +76,16 @@ export const Primary: StoryObj<LocationFilterProps> = {
|
|
|
75
76
|
</LapisUrlContext.Provider>
|
|
76
77
|
),
|
|
77
78
|
};
|
|
79
|
+
|
|
80
|
+
export const WithNoFields: StoryObj<LocationFilterProps> = {
|
|
81
|
+
...Primary,
|
|
82
|
+
args: {
|
|
83
|
+
...Primary.args,
|
|
84
|
+
fields: [],
|
|
85
|
+
},
|
|
86
|
+
play: async ({ canvasElement, step }) => {
|
|
87
|
+
step('expect error message', async () => {
|
|
88
|
+
await expectInvalidAttributesErrorMessage(canvasElement, 'Array must contain at least 1 element(s)');
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
import { useContext, useRef, useState } from 'preact/hooks';
|
|
3
3
|
import { type JSXInternal } from 'preact/src/jsx';
|
|
4
|
+
import z from 'zod';
|
|
4
5
|
|
|
5
6
|
import { fetchAutocompletionList } from './fetchAutocompletionList';
|
|
6
7
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
@@ -9,21 +10,25 @@ import { LoadingDisplay } from '../components/loading-display';
|
|
|
9
10
|
import { ResizeContainer } from '../components/resize-container';
|
|
10
11
|
import { useQuery } from '../useQuery';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
initialValue: string
|
|
14
|
-
placeholderText: string
|
|
15
|
-
fields: string
|
|
16
|
-
}
|
|
13
|
+
const lineageFilterInnerPropsSchema = z.object({
|
|
14
|
+
initialValue: z.string().optional(),
|
|
15
|
+
placeholderText: z.string().optional(),
|
|
16
|
+
fields: z.array(z.string()).min(1),
|
|
17
|
+
});
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
width: string
|
|
20
|
-
}
|
|
19
|
+
const lineageFilterPropsSchema = lineageFilterInnerPropsSchema.extend({
|
|
20
|
+
width: z.string(),
|
|
21
|
+
});
|
|
21
22
|
|
|
22
|
-
export
|
|
23
|
+
export type LocationFilterInnerProps = z.infer<typeof lineageFilterInnerPropsSchema>;
|
|
24
|
+
export type LocationFilterProps = z.infer<typeof lineageFilterPropsSchema>;
|
|
25
|
+
|
|
26
|
+
export const LocationFilter: FunctionComponent<LocationFilterProps> = (props) => {
|
|
27
|
+
const { width, ...innerProps } = props;
|
|
23
28
|
const size = { width, height: '3rem' };
|
|
24
29
|
|
|
25
30
|
return (
|
|
26
|
-
<ErrorBoundary size={size} layout='horizontal'>
|
|
31
|
+
<ErrorBoundary size={size} layout='horizontal' componentProps={props} schema={lineageFilterPropsSchema}>
|
|
27
32
|
<ResizeContainer size={size}>
|
|
28
33
|
<LocationFilterInner {...innerProps} />
|
|
29
34
|
</ResizeContainer>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ActiveElement, Chart, type ChartConfiguration, type ChartEvent, registerables } from 'chart.js';
|
|
2
2
|
import { ArcSlice, extractSets, VennDiagramController } from 'chartjs-chart-venn';
|
|
3
3
|
import { type FunctionComponent } from 'preact';
|
|
4
|
-
import {
|
|
4
|
+
import { useMemo, useState } from 'preact/hooks';
|
|
5
5
|
|
|
6
6
|
import { type MutationData } from './queryMutationData';
|
|
7
7
|
import { type Dataset } from '../../operator/Dataset';
|
|
@@ -19,8 +19,6 @@ export const MutationComparisonVenn: FunctionComponent<MutationComparisonVennPro
|
|
|
19
19
|
data,
|
|
20
20
|
proportionInterval,
|
|
21
21
|
}) => {
|
|
22
|
-
const divRef = useRef<HTMLDivElement>(null);
|
|
23
|
-
const noElementSelectedMessage = 'You have no elements selected. Click in the venn diagram to select.';
|
|
24
22
|
const [selectedDatasetIndex, setSelectedDatasetIndex] = useState<null | number>(null);
|
|
25
23
|
|
|
26
24
|
const sets = useMemo(
|
|
@@ -45,20 +43,6 @@ export const MutationComparisonVenn: FunctionComponent<MutationComparisonVennPro
|
|
|
45
43
|
[data, proportionInterval],
|
|
46
44
|
);
|
|
47
45
|
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
if (divRef.current === null) {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (selectedDatasetIndex === null) {
|
|
53
|
-
divRef.current.innerText = noElementSelectedMessage;
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const values = sets.datasets[0].data[selectedDatasetIndex].values;
|
|
58
|
-
const label = sets.datasets[0].data[selectedDatasetIndex].label;
|
|
59
|
-
divRef.current!.innerText = `${label}: ${values.join(', ')}` || '';
|
|
60
|
-
}, [divRef, selectedDatasetIndex, sets]);
|
|
61
|
-
|
|
62
46
|
const config: ChartConfiguration = useMemo(
|
|
63
47
|
() => ({
|
|
64
48
|
type: 'venn',
|
|
@@ -119,7 +103,22 @@ export const MutationComparisonVenn: FunctionComponent<MutationComparisonVennPro
|
|
|
119
103
|
<div className='flex-1'>
|
|
120
104
|
<GsChart configuration={config} />
|
|
121
105
|
</div>
|
|
122
|
-
<
|
|
106
|
+
<p class='flex flex-wrap break-words m-2'>{getSelectedMutationsDescription(selectedDatasetIndex, sets)}</p>
|
|
123
107
|
</div>
|
|
124
108
|
);
|
|
125
109
|
};
|
|
110
|
+
|
|
111
|
+
const noElementSelectedMessage = 'You have no elements selected. Click in the venn diagram to select.';
|
|
112
|
+
|
|
113
|
+
function getSelectedMutationsDescription(
|
|
114
|
+
selectedDatasetIndex: number | null,
|
|
115
|
+
sets: ReturnType<typeof extractSets<string>>,
|
|
116
|
+
) {
|
|
117
|
+
if (selectedDatasetIndex === null) {
|
|
118
|
+
return noElementSelectedMessage;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const values = sets.datasets[0].data[selectedDatasetIndex].values;
|
|
122
|
+
const label = sets.datasets[0].data[selectedDatasetIndex].label;
|
|
123
|
+
return `${label}: ${values.join(', ')}` || '';
|
|
124
|
+
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
import { type Dispatch, type StateUpdater, useContext, useMemo, useState } from 'preact/hooks';
|
|
3
|
+
import z from 'zod';
|
|
3
4
|
|
|
4
5
|
import { getMutationComparisonTableData } from './getMutationComparisonTableData';
|
|
5
6
|
import { MutationComparisonTable } from './mutation-comparison-table';
|
|
6
7
|
import { MutationComparisonVenn } from './mutation-comparison-venn';
|
|
7
8
|
import { filterMutationData, type MutationData, queryMutationData } from './queryMutationData';
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
type MutationComparisonView,
|
|
11
|
+
mutationComparisonViewSchema,
|
|
12
|
+
namedLapisFilterSchema,
|
|
13
|
+
sequenceTypeSchema,
|
|
14
|
+
} from '../../types';
|
|
9
15
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
10
16
|
import { CsvDownloadButton } from '../components/csv-download-button';
|
|
11
17
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
@@ -21,23 +27,23 @@ import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '..
|
|
|
21
27
|
import Tabs from '../components/tabs';
|
|
22
28
|
import { useQuery } from '../useQuery';
|
|
23
29
|
|
|
24
|
-
|
|
30
|
+
const mutationComparisonPropsSchema = z.object({
|
|
31
|
+
width: z.string(),
|
|
32
|
+
height: z.string(),
|
|
33
|
+
lapisFilters: z.array(namedLapisFilterSchema).min(1),
|
|
34
|
+
sequenceType: sequenceTypeSchema,
|
|
35
|
+
views: z.array(mutationComparisonViewSchema),
|
|
36
|
+
pageSize: z.union([z.boolean(), z.number()]),
|
|
37
|
+
});
|
|
25
38
|
|
|
26
|
-
export
|
|
27
|
-
width: string;
|
|
28
|
-
height: string;
|
|
29
|
-
lapisFilters: NamedLapisFilter[];
|
|
30
|
-
sequenceType: SequenceType;
|
|
31
|
-
views: View[];
|
|
32
|
-
pageSize: boolean | number;
|
|
33
|
-
}
|
|
39
|
+
export type MutationComparisonProps = z.infer<typeof mutationComparisonPropsSchema>;
|
|
34
40
|
|
|
35
41
|
export const MutationComparison: FunctionComponent<MutationComparisonProps> = (componentProps) => {
|
|
36
42
|
const { width, height } = componentProps;
|
|
37
43
|
const size = { height, width };
|
|
38
44
|
|
|
39
45
|
return (
|
|
40
|
-
<ErrorBoundary size={size}>
|
|
46
|
+
<ErrorBoundary size={size} componentProps={componentProps} schema={mutationComparisonPropsSchema}>
|
|
41
47
|
<ResizeContainer size={size}>
|
|
42
48
|
<MutationComparisonInner {...componentProps} />
|
|
43
49
|
</ResizeContainer>
|
|
@@ -86,7 +92,7 @@ const MutationComparisonTabs: FunctionComponent<MutationComparisonTabsProps> = (
|
|
|
86
92
|
[data, displayedSegments, displayedMutationTypes],
|
|
87
93
|
);
|
|
88
94
|
|
|
89
|
-
const getTab = (view:
|
|
95
|
+
const getTab = (view: MutationComparisonView) => {
|
|
90
96
|
switch (view) {
|
|
91
97
|
case 'table':
|
|
92
98
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
-
import { useContext,
|
|
2
|
+
import { useContext, useRef, useState } from 'preact/hooks';
|
|
3
|
+
import z from 'zod';
|
|
3
4
|
|
|
4
5
|
import { MutationFilterInfo } from './mutation-filter-info';
|
|
5
6
|
import { parseAndValidateMutation, type ParsedMutationFilter } from './parseAndValidateMutation';
|
|
@@ -9,13 +10,23 @@ import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
|
9
10
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
10
11
|
import { singleGraphColorRGBByName } from '../shared/charts/colors';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
const selectedMutationFilterStringsSchema = z.object({
|
|
14
|
+
nucleotideMutations: z.array(z.string()),
|
|
15
|
+
aminoAcidMutations: z.array(z.string()),
|
|
16
|
+
nucleotideInsertions: z.array(z.string()),
|
|
17
|
+
aminoAcidInsertions: z.array(z.string()),
|
|
18
|
+
});
|
|
19
|
+
export type SelectedMutationFilterStrings = z.infer<typeof selectedMutationFilterStringsSchema>;
|
|
20
|
+
const mutationFilterInnerPropsSchema = z.object({
|
|
21
|
+
initialValue: z.union([selectedMutationFilterStringsSchema.optional(), z.array(z.string()), z.undefined()]),
|
|
22
|
+
});
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
width: string
|
|
18
|
-
}
|
|
24
|
+
const mutationFilterPropsSchema = mutationFilterInnerPropsSchema.extend({
|
|
25
|
+
width: z.string(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type MutationFilterInnerProps = z.infer<typeof mutationFilterInnerPropsSchema>;
|
|
29
|
+
export type MutationFilterProps = z.infer<typeof mutationFilterPropsSchema>;
|
|
19
30
|
|
|
20
31
|
export type SelectedFilters = {
|
|
21
32
|
nucleotideMutations: (SubstitutionClass | DeletionClass)[];
|
|
@@ -24,13 +35,15 @@ export type SelectedFilters = {
|
|
|
24
35
|
aminoAcidInsertions: InsertionClass[];
|
|
25
36
|
};
|
|
26
37
|
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export const MutationFilter: FunctionComponent<MutationFilterProps> = ({ initialValue, width }) => {
|
|
38
|
+
export const MutationFilter: FunctionComponent<MutationFilterProps> = (props) => {
|
|
39
|
+
const { width, initialValue } = props;
|
|
32
40
|
return (
|
|
33
|
-
<ErrorBoundary
|
|
41
|
+
<ErrorBoundary
|
|
42
|
+
size={{ height: '3.375rem', width }}
|
|
43
|
+
layout='horizontal'
|
|
44
|
+
schema={mutationFilterPropsSchema}
|
|
45
|
+
componentProps={props}
|
|
46
|
+
>
|
|
34
47
|
<div style={width}>
|
|
35
48
|
<MutationFilterInner initialValue={initialValue} />
|
|
36
49
|
</div>
|