@genspectrum/dashboard-components 0.1.2 → 0.1.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/README.md +1 -1
- package/custom-elements.json +453 -67
- package/dist/dashboard-components.js +778 -488
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +277 -50
- package/dist/style.css +132 -139
- package/package.json +9 -5
- package/src/lapisApi/lapisApi.ts +1 -1
- package/src/preact/aggregatedData/__mockData__/aggregated.json +585 -0
- package/src/preact/aggregatedData/aggregate-table.tsx +32 -0
- package/src/preact/aggregatedData/aggregate.stories.tsx +53 -0
- package/src/preact/aggregatedData/aggregate.tsx +102 -0
- package/src/preact/components/ReferenceGenomesAwaiter.tsx +25 -0
- package/src/preact/components/csv-download-button.tsx +8 -2
- package/src/preact/components/headline.tsx +16 -4
- package/src/preact/components/min-max-range-slider.tsx +4 -4
- package/src/preact/components/percent-intput.tsx +2 -3
- package/src/preact/components/resize-container.tsx +23 -0
- package/src/preact/components/table.tsx +1 -0
- package/src/preact/components/tabs.stories.tsx +2 -2
- package/src/preact/components/tabs.tsx +47 -24
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +36 -4
- package/src/preact/dateRangeSelector/date-range-selector.tsx +57 -43
- package/src/preact/locationFilter/location-filter.tsx +2 -2
- package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +5 -5
- package/src/preact/mutationComparison/getMutationComparisonTableData.ts +45 -10
- package/src/preact/mutationComparison/mutation-comparison-table.tsx +20 -22
- package/src/preact/mutationComparison/mutation-comparison-venn.tsx +6 -3
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +8 -1
- package/src/preact/mutationComparison/mutation-comparison.tsx +13 -4
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +70 -31
- package/src/preact/mutationFilter/mutation-filter.tsx +62 -14
- package/src/preact/mutations/getInsertionsTableData.spec.ts +6 -4
- package/src/preact/mutations/getInsertionsTableData.ts +1 -1
- package/src/preact/mutations/getMutationsTableData.spec.ts +9 -19
- package/src/preact/mutations/getMutationsTableData.ts +1 -1
- package/src/preact/mutations/mutations-insertions-table.tsx +3 -1
- package/src/preact/mutations/mutations-table.tsx +3 -1
- package/src/preact/mutations/mutations.stories.tsx +8 -1
- package/src/preact/mutations/mutations.tsx +16 -5
- package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +4 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +17 -9
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +8 -5
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +12 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +13 -8
- package/src/preact/shared/sort/sortInsertions.spec.ts +11 -10
- package/src/preact/shared/sort/sortInsertions.ts +10 -17
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +19 -10
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +45 -12
- package/src/preact/textInput/text-input.stories.tsx +22 -1
- package/src/preact/textInput/text-input.tsx +3 -1
- package/src/query/queryAggregateData.spec.ts +32 -0
- package/src/query/queryAggregateData.ts +25 -0
- package/src/utils/typeAssertions.spec.ts +31 -0
- package/src/utils/typeAssertions.ts +16 -0
- package/src/web-components/PreactLitAdapter.tsx +0 -1
- package/src/web-components/app.stories.ts +129 -0
- package/src/web-components/app.ts +27 -6
- package/src/web-components/display/aggregate-component.stories.ts +73 -0
- package/src/web-components/display/aggregate-component.tsx +58 -0
- package/src/web-components/display/index.ts +1 -0
- package/src/web-components/display/mutation-comparison-component.stories.ts +29 -11
- package/src/web-components/display/mutation-comparison-component.tsx +72 -4
- package/src/web-components/display/mutations-component.stories.ts +14 -13
- package/src/web-components/display/mutations-component.tsx +14 -1
- package/src/web-components/display/prevalence-over-time-component.stories.ts +20 -18
- package/src/web-components/display/prevalence-over-time-component.tsx +12 -0
- package/src/web-components/display/relative-growth-advantage-component.stories.ts +11 -10
- package/src/web-components/display/relative-growth-advantage-component.tsx +12 -0
- package/src/web-components/input/date-range-selector-component.stories.ts +35 -8
- package/src/web-components/input/date-range-selector-component.tsx +18 -5
- package/src/web-components/input/location-filter-component.stories.ts +17 -8
- package/src/web-components/input/location-filter-component.tsx +2 -6
- package/src/web-components/input/mutation-filter-component.stories.ts +20 -9
- package/src/web-components/input/mutation-filter-component.tsx +10 -2
- package/src/web-components/input/text-input-component.stories.ts +13 -4
- package/src/web-components/input/text-input-component.tsx +11 -2
- package/src/web-components/input/location-filter.mdx +0 -25
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { FetchAggregatedOperator } from '../operator/FetchAggregatedOperator';
|
|
2
|
+
import { type LapisFilter } from '../types';
|
|
3
|
+
|
|
4
|
+
export type AggregateData = (Record<string, string | null | number | boolean> & {
|
|
5
|
+
count: number;
|
|
6
|
+
proportion: number;
|
|
7
|
+
})[];
|
|
8
|
+
|
|
9
|
+
export async function queryAggregateData(variant: LapisFilter, fields: string[], lapis: string, signal?: AbortSignal) {
|
|
10
|
+
const fetchData = new FetchAggregatedOperator<Record<string, string | null | number>>(variant, fields);
|
|
11
|
+
const data = (await fetchData.evaluate(lapis, signal)).content;
|
|
12
|
+
|
|
13
|
+
const total = data.reduce((acc, row) => acc + row.count, 0);
|
|
14
|
+
|
|
15
|
+
return data.map(
|
|
16
|
+
(row) =>
|
|
17
|
+
({
|
|
18
|
+
...row,
|
|
19
|
+
proportion: row.count / total,
|
|
20
|
+
}) as Record<string, string | null | number | boolean> & {
|
|
21
|
+
count: number;
|
|
22
|
+
proportion: number;
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { type Equals, type Expect } from './typeAssertions';
|
|
4
|
+
|
|
5
|
+
/* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars */
|
|
6
|
+
describe('Expect', () => {
|
|
7
|
+
it('should only accept true', () => {
|
|
8
|
+
type ShouldBeTrue = Expect<true>;
|
|
9
|
+
// @ts-expect-error Expect should only accept true
|
|
10
|
+
type ShouldFail = Expect<false>;
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('Equals', () => {
|
|
15
|
+
it('should accept equal types', () => {
|
|
16
|
+
type Equals1 = Expect<Equals<1, 1>>;
|
|
17
|
+
type Equals1Or2 = Expect<Equals<1 | 2, 1 | 2>>;
|
|
18
|
+
type EqualsWithObject = Expect<Equals<{ key: string }, { key: string }>>;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should reject unequal types', () => {
|
|
22
|
+
// @ts-expect-error 1 is not 2
|
|
23
|
+
type Equals1Vs2 = Expect<Equals<1, 2>>;
|
|
24
|
+
// @ts-expect-error disjoint union types
|
|
25
|
+
type EqualsDisjointUnion = Expect<Equals<1 | 2, 1 | 999>>;
|
|
26
|
+
// @ts-expect-error object with different keys
|
|
27
|
+
type EqualsObjectWithDifferentKey = Expect<Equals<{ key: string }, { otherKey: string }>>;
|
|
28
|
+
// @ts-expect-error object with different value types
|
|
29
|
+
type EqualsObjectWithDifferentValue = Expect<Equals<{ key: string }, { key: number }>>;
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Those are helpers to make sure that types are equal.
|
|
3
|
+
*
|
|
4
|
+
* #### Why do we need this?
|
|
5
|
+
*
|
|
6
|
+
* The custom element manifest does not fully resolve the types of properties,
|
|
7
|
+
* so Storybook will only show non-resolved types (such as `View[]`).
|
|
8
|
+
* To give users full type information, we have to inline the types in the web component definitions.
|
|
9
|
+
* These assertions help us to make sure that the inlined type is equal to the type
|
|
10
|
+
* that is defined in the Preact components.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type Expect<T extends true> = T;
|
|
14
|
+
export type IsAssignable<X, Y> = X extends Y ? true : false;
|
|
15
|
+
export type And<X, Y> = X extends true ? (Y extends true ? true : false) : false;
|
|
16
|
+
export type Equals<X, Y> = And<IsAssignable<X, Y>, IsAssignable<Y, X>>;
|
|
@@ -46,7 +46,6 @@ export abstract class PreactLitAdapter extends ReactiveElement {
|
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
override update(changedProperties: PropertyValues) {
|
|
49
|
-
console.log('this.lapis', this.lapis);
|
|
50
49
|
const vdom = (
|
|
51
50
|
<LapisUrlContext.Provider value={this.lapis}>
|
|
52
51
|
<ReferenceGenomeContext.Provider value={this.referenceGenome}>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { consume } from '@lit/context';
|
|
2
|
+
import { withActions } from '@storybook/addon-actions/decorator';
|
|
3
|
+
import { expect, waitFor, within } from '@storybook/test';
|
|
4
|
+
import type { Meta, StoryObj } from '@storybook/web-components';
|
|
5
|
+
import { html, LitElement } from 'lit';
|
|
6
|
+
import { customElement } from 'lit/decorators.js';
|
|
7
|
+
|
|
8
|
+
import './app';
|
|
9
|
+
|
|
10
|
+
import { lapisContext } from './lapis-context';
|
|
11
|
+
import { referenceGenomeContext } from './reference-genome-context';
|
|
12
|
+
import { withComponentDocs } from '../../.storybook/ComponentDocsBlock';
|
|
13
|
+
import { LAPIS_URL, REFERENCE_GENOME_ENDPOINT } from '../constants';
|
|
14
|
+
import type { ReferenceGenome } from '../lapisApi/ReferenceGenome';
|
|
15
|
+
import referenceGenome from '../lapisApi/__mockData__/referenceGenome.json';
|
|
16
|
+
|
|
17
|
+
const codeExample = String.raw`
|
|
18
|
+
<gs-app lapis="https://url.to.lapis">
|
|
19
|
+
<p>Your application code goes here.</p>
|
|
20
|
+
</gs-app>`;
|
|
21
|
+
|
|
22
|
+
const meta: Meta = {
|
|
23
|
+
title: 'Wrapper/App',
|
|
24
|
+
component: 'gs-app',
|
|
25
|
+
parameters: withComponentDocs({
|
|
26
|
+
fetchMock: {},
|
|
27
|
+
componentDocs: {
|
|
28
|
+
tag: 'gs-app',
|
|
29
|
+
opensShadowDom: false,
|
|
30
|
+
expectsChildren: true,
|
|
31
|
+
codeExample,
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
decorators: [withActions],
|
|
35
|
+
tags: ['autodocs'],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default meta;
|
|
39
|
+
|
|
40
|
+
const Template: StoryObj<{ lapis: string }> = {
|
|
41
|
+
render: (args) => {
|
|
42
|
+
return html` <gs-app lapis="${args.lapis}">
|
|
43
|
+
<gs-app-display></gs-app-display>
|
|
44
|
+
</gs-app>`;
|
|
45
|
+
},
|
|
46
|
+
args: {
|
|
47
|
+
lapis: LAPIS_URL,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const Default: StoryObj<{ lapis: string }> = {
|
|
52
|
+
...Template,
|
|
53
|
+
play: async ({ canvasElement }) => {
|
|
54
|
+
const canvas = within(canvasElement);
|
|
55
|
+
|
|
56
|
+
await waitFor(() => {
|
|
57
|
+
expect(canvas.getByText(LAPIS_URL)).toBeVisible();
|
|
58
|
+
expect(canvas.getByText('"name": "ORF1a",', { exact: false })).toBeVisible();
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const DelayFetchingReferenceGenome: StoryObj<{ lapis: string }> = {
|
|
64
|
+
...Template,
|
|
65
|
+
parameters: {
|
|
66
|
+
fetchMock: {
|
|
67
|
+
mocks: [
|
|
68
|
+
{
|
|
69
|
+
matcher: {
|
|
70
|
+
name: 'referenceGenome',
|
|
71
|
+
url: REFERENCE_GENOME_ENDPOINT,
|
|
72
|
+
},
|
|
73
|
+
response: {
|
|
74
|
+
status: 200,
|
|
75
|
+
body: referenceGenome,
|
|
76
|
+
},
|
|
77
|
+
options: {
|
|
78
|
+
delay: 5000,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const FailsToFetchReferenceGenome: StoryObj<{ lapis: string }> = {
|
|
87
|
+
...Template,
|
|
88
|
+
args: {
|
|
89
|
+
lapis: 'definitely-not-a-valid-url',
|
|
90
|
+
},
|
|
91
|
+
play: async ({ canvasElement }) => {
|
|
92
|
+
const canvas = within(canvasElement);
|
|
93
|
+
|
|
94
|
+
await waitFor(() => {
|
|
95
|
+
expect(canvas.getByText('Error')).toBeVisible();
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
@customElement('gs-app-display')
|
|
101
|
+
// eslint-disable-next-line no-unused-vars,@typescript-eslint/no-unused-vars -- it is used in the story above
|
|
102
|
+
class AppDisplay extends LitElement {
|
|
103
|
+
@consume({ context: lapisContext })
|
|
104
|
+
lapis: string = '';
|
|
105
|
+
|
|
106
|
+
@consume({ context: referenceGenomeContext, subscribe: true })
|
|
107
|
+
referenceGenome: ReferenceGenome = {
|
|
108
|
+
nucleotideSequences: [],
|
|
109
|
+
genes: [],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
override render() {
|
|
113
|
+
return html`
|
|
114
|
+
<h1 class="text-xl font-bold">Dummy component</h1>
|
|
115
|
+
<p>
|
|
116
|
+
What you can see here is a dummy component that displays the values of the wrapping "gs-app". Actually
|
|
117
|
+
"gs-app" doesn't display anything.
|
|
118
|
+
</p>
|
|
119
|
+
<h2 class="text-lg font-bold">LAPIS URL</h2>
|
|
120
|
+
<p>${this.lapis}</p>
|
|
121
|
+
<h2 class="text-lg font-bold">Reference genomes</h2>
|
|
122
|
+
<pre><code>${JSON.stringify(this.referenceGenome, null, 2)}</code></pre>
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
override createRenderRoot() {
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -8,18 +8,43 @@ import { referenceGenomeContext } from './reference-genome-context';
|
|
|
8
8
|
import { type ReferenceGenome } from '../lapisApi/ReferenceGenome';
|
|
9
9
|
import { fetchReferenceGenome } from '../lapisApi/lapisApi';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* ## Context
|
|
13
|
+
*
|
|
14
|
+
* This component provides the main application context.
|
|
15
|
+
* All other `gs-*` components must be (possibly nested) children of this component.
|
|
16
|
+
* It makes use of the [Lit Context](https://lit.dev/docs/data/context/) to
|
|
17
|
+
* - provide the URL to the LAPIS instance to all its children
|
|
18
|
+
* - fetch the reference genomes from LAPIS and provide it to all its children
|
|
19
|
+
*
|
|
20
|
+
* This will show an error message if the reference genome cannot be fetched
|
|
21
|
+
* (e.g., due to an invalid LAPIS URL).
|
|
22
|
+
*
|
|
23
|
+
* ## Shadow DOM
|
|
24
|
+
*
|
|
25
|
+
* This component does __not__ use a shadow DOM. Children of this component will be rendered directly in the light DOM.
|
|
26
|
+
*/
|
|
11
27
|
@customElement('gs-app')
|
|
12
28
|
export class App extends LitElement {
|
|
29
|
+
/**
|
|
30
|
+
* The URL of the LAPIS instance that all children of this component will use.
|
|
31
|
+
*/
|
|
13
32
|
@provide({ context: lapisContext })
|
|
14
33
|
@property()
|
|
15
34
|
lapis: string = '';
|
|
16
35
|
|
|
36
|
+
/**
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
17
39
|
@provide({ context: referenceGenomeContext })
|
|
18
40
|
referenceGenome: ReferenceGenome = {
|
|
19
41
|
nucleotideSequences: [],
|
|
20
42
|
genes: [],
|
|
21
43
|
};
|
|
22
44
|
|
|
45
|
+
/**
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
23
48
|
private updateReferenceGenome = new Task(this, {
|
|
24
49
|
task: async () => {
|
|
25
50
|
this.referenceGenome = await fetchReferenceGenome(this.lapis);
|
|
@@ -29,13 +54,9 @@ export class App extends LitElement {
|
|
|
29
54
|
|
|
30
55
|
override render() {
|
|
31
56
|
return this.updateReferenceGenome.render({
|
|
32
|
-
complete: () =>
|
|
33
|
-
return html` <slot></slot>`;
|
|
34
|
-
},
|
|
57
|
+
complete: () => html` <slot></slot>`,
|
|
35
58
|
error: () => html`<p>Error</p>`, // TODO(#143): Add more advanced error handling
|
|
36
|
-
pending: () =>
|
|
37
|
-
return html`<p>Loading...</p>`;
|
|
38
|
-
},
|
|
59
|
+
pending: () => html` <p>Loading reference genomes...</p> `,
|
|
39
60
|
});
|
|
40
61
|
}
|
|
41
62
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type Meta, type StoryObj } from '@storybook/web-components';
|
|
2
|
+
import { html } from 'lit';
|
|
3
|
+
|
|
4
|
+
import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock';
|
|
5
|
+
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
6
|
+
import aggregatedData from '../../preact/aggregatedData/__mockData__/aggregated.json';
|
|
7
|
+
import type { AggregateProps } from '../../preact/aggregatedData/aggregate';
|
|
8
|
+
|
|
9
|
+
import './aggregate-component';
|
|
10
|
+
import '../app';
|
|
11
|
+
|
|
12
|
+
const meta: Meta<AggregateProps> = {
|
|
13
|
+
title: 'Visualization/Aggregate',
|
|
14
|
+
component: 'gs-aggregate-component',
|
|
15
|
+
argTypes: {
|
|
16
|
+
fields: [{ control: 'object' }],
|
|
17
|
+
views: {
|
|
18
|
+
options: ['table'],
|
|
19
|
+
control: { type: 'check' },
|
|
20
|
+
},
|
|
21
|
+
size: [{ control: 'object' }],
|
|
22
|
+
},
|
|
23
|
+
parameters: withComponentDocs({
|
|
24
|
+
fetchMock: {
|
|
25
|
+
mocks: [
|
|
26
|
+
{
|
|
27
|
+
matcher: {
|
|
28
|
+
name: 'aggregatedData',
|
|
29
|
+
url: AGGREGATED_ENDPOINT,
|
|
30
|
+
body: {
|
|
31
|
+
fields: ['division', 'host'],
|
|
32
|
+
country: 'USA',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
response: {
|
|
36
|
+
status: 200,
|
|
37
|
+
body: aggregatedData,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
componentDocs: {
|
|
43
|
+
tag: 'gs-aggregate-component',
|
|
44
|
+
opensShadowDom: true,
|
|
45
|
+
expectsChildren: false,
|
|
46
|
+
codeExample: `<gs-aggregate-component fields='["division", "host"]' filter='{"country": "USA"}' views='["table"]'></gs-aggregate-component>`,
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
tags: ['autodocs'],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default meta;
|
|
53
|
+
|
|
54
|
+
export const Table: StoryObj<AggregateProps> = {
|
|
55
|
+
render: (args) => html`
|
|
56
|
+
<gs-app lapis="${LAPIS_URL}">
|
|
57
|
+
<gs-aggregate-component
|
|
58
|
+
.fields=${args.fields}
|
|
59
|
+
.filter=${args.filter}
|
|
60
|
+
.views=${args.views}
|
|
61
|
+
.size=${args.size}
|
|
62
|
+
></gs-aggregate-component>
|
|
63
|
+
</gs-app>
|
|
64
|
+
`,
|
|
65
|
+
args: {
|
|
66
|
+
fields: ['division', 'host'],
|
|
67
|
+
views: ['table'],
|
|
68
|
+
filter: {
|
|
69
|
+
country: 'USA',
|
|
70
|
+
},
|
|
71
|
+
size: { width: '100%', height: '700px' },
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
2
|
+
|
|
3
|
+
import { Aggregate, type View } from '../../preact/aggregatedData/aggregate';
|
|
4
|
+
import { type LapisFilter } from '../../types';
|
|
5
|
+
import { PreactLitAdapterWithGridJsStyles } from '../PreactLitAdapterWithGridJsStyles';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ## Context
|
|
9
|
+
*
|
|
10
|
+
* This component displays aggregated data, which can provide an overview of the underlying data.
|
|
11
|
+
*
|
|
12
|
+
* It expects a list of fields to aggregate by and a filter to apply to the data.
|
|
13
|
+
*/
|
|
14
|
+
@customElement('gs-aggregate-component')
|
|
15
|
+
export class AggregateComponent extends PreactLitAdapterWithGridJsStyles {
|
|
16
|
+
/**
|
|
17
|
+
* The fields to aggregate by.
|
|
18
|
+
*/
|
|
19
|
+
@property({ type: Array })
|
|
20
|
+
fields: string[] = [];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The views are used to display the aggregated data.
|
|
24
|
+
* In the table view, the data is presented in a table format where each field is a column,
|
|
25
|
+
* along with the aggregated value and its proportion.
|
|
26
|
+
* The proportion represents the ratio of the aggregated value to the total count of the data
|
|
27
|
+
* (considering the applied filter).
|
|
28
|
+
*/
|
|
29
|
+
@property({ type: Array })
|
|
30
|
+
views: View[] = ['table'];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The filter to apply to the data.
|
|
34
|
+
*/
|
|
35
|
+
@property({ type: Object })
|
|
36
|
+
filter: LapisFilter = {};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The size of the component.
|
|
40
|
+
*
|
|
41
|
+
* If not set, the component will take the full width of its container with height 700px.
|
|
42
|
+
*
|
|
43
|
+
* The width and height should be a string with a unit in css style, e.g. '100%', '500px' or '50vh'.
|
|
44
|
+
* If the unit is %, the size will be relative to the container of the component.
|
|
45
|
+
*/
|
|
46
|
+
@property({ type: Object })
|
|
47
|
+
size: { width?: string; height?: string } | undefined = undefined;
|
|
48
|
+
|
|
49
|
+
override render() {
|
|
50
|
+
return <Aggregate fields={this.fields} views={this.views} filter={this.filter} size={this.size} />;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare global {
|
|
55
|
+
interface HTMLElementTagNameMap {
|
|
56
|
+
'gs-aggregate-component': AggregateComponent;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -2,3 +2,4 @@ export { MutationComparisonComponent } from './mutation-comparison-component';
|
|
|
2
2
|
export { MutationsComponent } from './mutations-component';
|
|
3
3
|
export { PrevalenceOverTimeComponent } from './prevalence-over-time-component';
|
|
4
4
|
export { RelativeGrowthAdvantageComponent } from './relative-growth-advantage-component';
|
|
5
|
+
export { AggregateComponent } from './aggregate-component';
|
|
@@ -4,12 +4,20 @@ import { html } from 'lit';
|
|
|
4
4
|
|
|
5
5
|
import './mutation-comparison-component';
|
|
6
6
|
import '../app';
|
|
7
|
+
import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock';
|
|
7
8
|
import { LAPIS_URL, NUCLEOTIDE_MUTATIONS_ENDPOINT } from '../../constants';
|
|
8
9
|
import nucleotideMutationsOtherVariant from '../../preact/mutationComparison/__mockData__/nucleotideMutationsOtherVariant.json';
|
|
9
10
|
import nucleotideMutationsSomeVariant from '../../preact/mutationComparison/__mockData__/nucleotideMutationsSomeVariant.json';
|
|
10
11
|
import { type MutationComparisonProps } from '../../preact/mutationComparison/mutation-comparison';
|
|
11
12
|
import { withinShadowRoot } from '../withinShadowRoot.story';
|
|
12
13
|
|
|
14
|
+
const codeExample = String.raw`
|
|
15
|
+
<gs-mutation-comparison-component
|
|
16
|
+
variants='[{ "displayName": "variant1", "lapisFilter": { "country": "Switzerland" }}, { "displayName": "variant2", "lapisFilter": { "country": "Germany" }}]'
|
|
17
|
+
sequenceType="nucleotide"
|
|
18
|
+
views='["table", "venn"]'
|
|
19
|
+
></gs-mutation-comparison-component>`;
|
|
20
|
+
|
|
13
21
|
const meta: Meta<MutationComparisonProps> = {
|
|
14
22
|
title: 'Visualization/Mutation comparison',
|
|
15
23
|
component: 'gs-mutation-comparison-component',
|
|
@@ -23,22 +31,31 @@ const meta: Meta<MutationComparisonProps> = {
|
|
|
23
31
|
options: ['table', 'venn'],
|
|
24
32
|
control: { type: 'check' },
|
|
25
33
|
},
|
|
34
|
+
size: { control: 'object' },
|
|
26
35
|
},
|
|
36
|
+
parameters: withComponentDocs({
|
|
37
|
+
componentDocs: {
|
|
38
|
+
tag: 'gs-mutation-comparison-component',
|
|
39
|
+
opensShadowDom: true,
|
|
40
|
+
expectsChildren: false,
|
|
41
|
+
codeExample,
|
|
42
|
+
},
|
|
43
|
+
}),
|
|
44
|
+
tags: ['autodocs'],
|
|
27
45
|
};
|
|
28
46
|
|
|
29
47
|
export default meta;
|
|
30
48
|
|
|
31
49
|
const Template: StoryObj<MutationComparisonProps> = {
|
|
32
50
|
render: (args) => html`
|
|
33
|
-
<
|
|
34
|
-
<gs-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
</div>
|
|
51
|
+
<gs-app lapis="${LAPIS_URL}">
|
|
52
|
+
<gs-mutation-comparison-component
|
|
53
|
+
.variants=${args.variants}
|
|
54
|
+
.sequenceType=${args.sequenceType}
|
|
55
|
+
.views=${args.views}
|
|
56
|
+
.size=${args.size}
|
|
57
|
+
></gs-mutation-comparison-component>
|
|
58
|
+
</gs-app>
|
|
42
59
|
`,
|
|
43
60
|
};
|
|
44
61
|
|
|
@@ -65,6 +82,7 @@ export const Default: StoryObj<MutationComparisonProps> = {
|
|
|
65
82
|
],
|
|
66
83
|
sequenceType: 'nucleotide',
|
|
67
84
|
views: ['table', 'venn'],
|
|
85
|
+
size: { width: '100%', height: '700px' },
|
|
68
86
|
},
|
|
69
87
|
parameters: {
|
|
70
88
|
fetchMock: {
|
|
@@ -124,9 +142,9 @@ export const VennDiagram: StoryObj<MutationComparisonProps> = {
|
|
|
124
142
|
const canvas = await withinShadowRoot(canvasElement, 'gs-mutation-comparison-component');
|
|
125
143
|
|
|
126
144
|
await step('Switch to Venn diagram view', async () => {
|
|
127
|
-
await waitFor(() => expect(canvas.
|
|
145
|
+
await waitFor(() => expect(canvas.getByRole('button', { name: 'Venn' })).toBeInTheDocument());
|
|
128
146
|
|
|
129
|
-
await fireEvent.click(canvas.
|
|
147
|
+
await fireEvent.click(canvas.getByRole('button', { name: 'Venn' }));
|
|
130
148
|
|
|
131
149
|
await waitFor(() =>
|
|
132
150
|
expect(
|
|
@@ -6,21 +6,81 @@ import {
|
|
|
6
6
|
type View,
|
|
7
7
|
} from '../../preact/mutationComparison/mutation-comparison';
|
|
8
8
|
import { type SequenceType } from '../../types';
|
|
9
|
+
import { type Equals, type Expect } from '../../utils/typeAssertions';
|
|
9
10
|
import { PreactLitAdapterWithGridJsStyles } from '../PreactLitAdapterWithGridJsStyles';
|
|
10
11
|
|
|
12
|
+
/**
|
|
13
|
+
* This component allows to compare mutations between different variants.
|
|
14
|
+
* A variant is defined by its LAPIS filter.
|
|
15
|
+
*
|
|
16
|
+
* It only shows substitutions and deletions, it does not show insertions.
|
|
17
|
+
*
|
|
18
|
+
* ## Views
|
|
19
|
+
*
|
|
20
|
+
* ### Table View
|
|
21
|
+
*
|
|
22
|
+
* The table view shows mutations
|
|
23
|
+
* and the proportions with which the mutation occurs in the respective variant.
|
|
24
|
+
* It only shows mutations that are present in at least one of the variants
|
|
25
|
+
* and where the proportion is within the selected proportion interval for at least one variant.
|
|
26
|
+
*
|
|
27
|
+
* ### Venn View
|
|
28
|
+
*
|
|
29
|
+
* The Venn view shows the overlap of mutations between the variants in a Venn diagram.
|
|
30
|
+
* A variant is considered to have a certain mutation,
|
|
31
|
+
* if the proportion of the mutation in the variant is within the selected proportion interval.
|
|
32
|
+
* Thus, changing the proportion interval may change a mutations from being "common" between variant
|
|
33
|
+
* to being "for one variant only".
|
|
34
|
+
*/
|
|
11
35
|
@customElement('gs-mutation-comparison-component')
|
|
12
36
|
export class MutationComparisonComponent extends PreactLitAdapterWithGridJsStyles {
|
|
37
|
+
/**
|
|
38
|
+
* An array of variants to compare.
|
|
39
|
+
*
|
|
40
|
+
* The `lapisFilter` will be sent as is to LAPIS to filter the mutation data.
|
|
41
|
+
* It must be a valid LAPIS filter object.
|
|
42
|
+
*
|
|
43
|
+
* The `displayName` will be used as the label for the variant in the views.
|
|
44
|
+
* It should be human-readable.
|
|
45
|
+
*/
|
|
13
46
|
@property({ type: Array })
|
|
14
|
-
variants:
|
|
47
|
+
variants: {
|
|
48
|
+
lapisFilter: Record<string, string | number | null | boolean>;
|
|
49
|
+
displayName: string;
|
|
50
|
+
}[] = [];
|
|
15
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The type of the sequence for which the mutations should be shown.
|
|
54
|
+
*/
|
|
16
55
|
@property({ type: String })
|
|
17
|
-
sequenceType:
|
|
56
|
+
sequenceType: 'nucleotide' | 'amino acid' = 'nucleotide';
|
|
18
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A list of tabs with views that this component should provide.
|
|
60
|
+
*/
|
|
19
61
|
@property({ type: Array })
|
|
20
|
-
views:
|
|
62
|
+
views: ('table' | 'venn')[] = ['table'];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The size of the component.
|
|
66
|
+
*
|
|
67
|
+
* If not set, the component will take the full width of its container with height 700px.
|
|
68
|
+
*
|
|
69
|
+
* The width and height should be a string with a unit in css style, e.g. '100%', '500px' or '50vh'.
|
|
70
|
+
* If the unit is %, the size will be relative to the container of the component.
|
|
71
|
+
*/
|
|
72
|
+
@property({ type: Object })
|
|
73
|
+
size: { width?: string; height?: string } | undefined = undefined;
|
|
21
74
|
|
|
22
75
|
override render() {
|
|
23
|
-
return
|
|
76
|
+
return (
|
|
77
|
+
<MutationComparison
|
|
78
|
+
variants={this.variants}
|
|
79
|
+
sequenceType={this.sequenceType}
|
|
80
|
+
views={this.views}
|
|
81
|
+
size={this.size}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
24
84
|
}
|
|
25
85
|
}
|
|
26
86
|
|
|
@@ -29,3 +89,11 @@ declare global {
|
|
|
29
89
|
'gs-mutation-comparison-component': MutationComparisonComponent;
|
|
30
90
|
}
|
|
31
91
|
}
|
|
92
|
+
|
|
93
|
+
/* eslint-disable @typescript-eslint/no-unused-vars, no-unused-vars */
|
|
94
|
+
type VariantsMatches = Expect<
|
|
95
|
+
Equals<typeof MutationComparisonComponent.prototype.variants, MutationComparisonVariant[]>
|
|
96
|
+
>;
|
|
97
|
+
type SequenceTypeMatches = Expect<Equals<typeof MutationComparisonComponent.prototype.sequenceType, SequenceType>>;
|
|
98
|
+
type ViewsMatches = Expect<Equals<typeof MutationComparisonComponent.prototype.views, View[]>>;
|
|
99
|
+
/* eslint-enable @typescript-eslint/no-unused-vars, no-unused-vars */
|
|
@@ -23,6 +23,7 @@ const meta: Meta<MutationsProps> = {
|
|
|
23
23
|
options: ['table', 'grid', 'insertions'],
|
|
24
24
|
control: { type: 'check' },
|
|
25
25
|
},
|
|
26
|
+
size: { control: 'object' },
|
|
26
27
|
},
|
|
27
28
|
};
|
|
28
29
|
|
|
@@ -30,15 +31,14 @@ export default meta;
|
|
|
30
31
|
|
|
31
32
|
const Template: StoryObj<MutationsProps> = {
|
|
32
33
|
render: (args) => html`
|
|
33
|
-
<
|
|
34
|
-
<gs-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
</div>
|
|
34
|
+
<gs-app lapis="${LAPIS_URL}">
|
|
35
|
+
<gs-mutations-component
|
|
36
|
+
.variant=${args.variant}
|
|
37
|
+
.sequenceType=${args.sequenceType}
|
|
38
|
+
.views=${args.views}
|
|
39
|
+
.size=${args.size}
|
|
40
|
+
></gs-mutations-component>
|
|
41
|
+
</gs-app>
|
|
42
42
|
`,
|
|
43
43
|
};
|
|
44
44
|
|
|
@@ -48,6 +48,7 @@ export const Default: StoryObj<MutationsProps> = {
|
|
|
48
48
|
variant: { country: 'Switzerland', pangoLineage: 'B.1.1.7', dateTo: '2022-01-01' },
|
|
49
49
|
sequenceType: 'nucleotide',
|
|
50
50
|
views: ['grid', 'table', 'insertions'],
|
|
51
|
+
size: { width: '100%', height: '700px' },
|
|
51
52
|
},
|
|
52
53
|
parameters: {
|
|
53
54
|
fetchMock: {
|
|
@@ -89,9 +90,9 @@ export const OnTableTab: StoryObj<MutationsProps> = {
|
|
|
89
90
|
play: async ({ canvasElement }) => {
|
|
90
91
|
const canvas = await withinShadowRoot(canvasElement, 'gs-mutations-component');
|
|
91
92
|
|
|
92
|
-
await waitFor(() => expect(canvas.
|
|
93
|
+
await waitFor(() => expect(canvas.getByRole('button', { name: 'Table' })).toBeInTheDocument());
|
|
93
94
|
|
|
94
|
-
await fireEvent.click(canvas.
|
|
95
|
+
await fireEvent.click(canvas.getByRole('button', { name: 'Table' }));
|
|
95
96
|
},
|
|
96
97
|
};
|
|
97
98
|
|
|
@@ -100,8 +101,8 @@ export const OnInsertionsTab: StoryObj<MutationsProps> = {
|
|
|
100
101
|
play: async ({ canvasElement }) => {
|
|
101
102
|
const canvas = await withinShadowRoot(canvasElement, 'gs-mutations-component');
|
|
102
103
|
|
|
103
|
-
await waitFor(() => expect(canvas.
|
|
104
|
+
await waitFor(() => expect(canvas.getByRole('button', { name: 'Insertions' })).toBeInTheDocument());
|
|
104
105
|
|
|
105
|
-
await fireEvent.click(canvas.
|
|
106
|
+
await fireEvent.click(canvas.getByRole('button', { name: 'Insertions' }));
|
|
106
107
|
},
|
|
107
108
|
};
|