@genspectrum/dashboard-components 1.13.0 → 1.14.0
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 +393 -2
- package/dist/components.d.ts +126 -9
- package/dist/components.js +701 -163
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +146 -11
- package/package.json +1 -1
- package/src/lapisApi/lapisTypes.ts +1 -1
- package/src/preact/queriesOverTime/__mockData__/defaultMockData/queriesOverTime.json +32 -0
- package/src/preact/queriesOverTime/__mockData__/manyQueries.json +128 -0
- package/src/preact/queriesOverTime/__mockData__/request1800s.json +16 -0
- package/src/preact/queriesOverTime/__mockData__/withGaps.json +52 -0
- package/src/preact/queriesOverTime/getFilteredQueriesOverTimeData.ts +85 -0
- package/src/preact/queriesOverTime/queries-over-time-filter.tsx +25 -0
- package/src/preact/queriesOverTime/queries-over-time-grid-tooltip.stories.tsx +134 -0
- package/src/preact/queriesOverTime/queries-over-time-grid-tooltip.tsx +123 -0
- package/src/preact/queriesOverTime/queries-over-time.stories.tsx +481 -0
- package/src/preact/queriesOverTime/queries-over-time.tsx +304 -0
- package/src/utilEntrypoint.ts +1 -0
- package/src/web-components/visualization/gs-mutations-over-time.spec-d.ts +3 -0
- package/src/web-components/visualization/gs-mutations-over-time.tsx +1 -1
- package/src/web-components/visualization/gs-queries-over-time.spec-d.ts +38 -0
- package/src/web-components/visualization/gs-queries-over-time.stories.ts +288 -0
- package/src/web-components/visualization/gs-queries-over-time.tsx +154 -0
- package/src/web-components/visualization/index.ts +1 -0
- package/standalone-bundle/dashboard-components.js +8509 -8068
- package/standalone-bundle/dashboard-components.js.map +1 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect, within } from '@storybook/test';
|
|
3
|
+
|
|
4
|
+
import { QueriesOverTimeGridTooltip, type QueriesOverTimeGridTooltipProps } from './queries-over-time-grid-tooltip';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<QueriesOverTimeGridTooltipProps> = {
|
|
7
|
+
title: 'Component/Queries over time grid tooltip',
|
|
8
|
+
component: QueriesOverTimeGridTooltip,
|
|
9
|
+
argTypes: {
|
|
10
|
+
query: { control: 'text' },
|
|
11
|
+
date: { control: 'object' },
|
|
12
|
+
value: { control: 'object' },
|
|
13
|
+
},
|
|
14
|
+
parameters: {
|
|
15
|
+
fetchMock: {},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
|
|
21
|
+
const Template: StoryObj<QueriesOverTimeGridTooltipProps> = {
|
|
22
|
+
render: (args: QueriesOverTimeGridTooltipProps) => <QueriesOverTimeGridTooltip {...args} />,
|
|
23
|
+
args: {
|
|
24
|
+
query: 'BA.1 Lineage',
|
|
25
|
+
date: {
|
|
26
|
+
type: 'Year',
|
|
27
|
+
year: 2025,
|
|
28
|
+
dateString: '2025',
|
|
29
|
+
},
|
|
30
|
+
value: null,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const NoValue: StoryObj<QueriesOverTimeGridTooltipProps> = {
|
|
35
|
+
...Template,
|
|
36
|
+
play: async ({ canvasElement }) => {
|
|
37
|
+
const canvas = within(canvasElement);
|
|
38
|
+
|
|
39
|
+
await expect(canvas.getByText('2025', { exact: true })).toBeVisible();
|
|
40
|
+
await expect(canvas.getByText('2025-01-01 - 2025-12-31')).toBeVisible();
|
|
41
|
+
await expect(canvas.getByText('BA.1 Lineage')).toBeVisible();
|
|
42
|
+
await expect(canvas.getByText('No data')).toBeVisible();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const WithValue: StoryObj<QueriesOverTimeGridTooltipProps> = {
|
|
47
|
+
...Template,
|
|
48
|
+
args: {
|
|
49
|
+
...Template.args,
|
|
50
|
+
value: {
|
|
51
|
+
type: 'value',
|
|
52
|
+
proportion: 0.5,
|
|
53
|
+
count: 100,
|
|
54
|
+
totalCount: 300,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
play: async ({ canvasElement }) => {
|
|
58
|
+
const canvas = within(canvasElement);
|
|
59
|
+
|
|
60
|
+
await expect(canvas.getByText('50.00%')).toBeVisible();
|
|
61
|
+
await expect(canvas.getByText('100')).toBeVisible();
|
|
62
|
+
await expect(canvas.getByText('match the query BA.1 Lineage.')).toBeVisible();
|
|
63
|
+
await expect(canvas.getByText('200')).toBeVisible();
|
|
64
|
+
await expect(canvas.getByText('total with coverage.')).toBeVisible();
|
|
65
|
+
await expect(canvas.getByText('300')).toBeVisible();
|
|
66
|
+
await expect(canvas.getByText('total in this date range.')).toBeVisible();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const WithValueWithZero: StoryObj<QueriesOverTimeGridTooltipProps> = {
|
|
71
|
+
...Template,
|
|
72
|
+
args: {
|
|
73
|
+
...Template.args,
|
|
74
|
+
value: {
|
|
75
|
+
type: 'value',
|
|
76
|
+
proportion: 0,
|
|
77
|
+
count: 0,
|
|
78
|
+
totalCount: 300,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
play: async ({ canvasElement }) => {
|
|
82
|
+
const canvas = within(canvasElement);
|
|
83
|
+
|
|
84
|
+
await expect(canvas.getByText('0.00%')).toBeVisible();
|
|
85
|
+
await expect(canvas.getByText('0')).toBeVisible();
|
|
86
|
+
await expect(canvas.getByText('match the query BA.1 Lineage.')).toBeVisible();
|
|
87
|
+
await expect(canvas.queryByText('with coverage')).not.toBeInTheDocument();
|
|
88
|
+
await expect(canvas.getByText('300')).toBeVisible();
|
|
89
|
+
await expect(canvas.getByText('total in this date range.')).toBeVisible();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const WithValueWithCoverage: StoryObj<QueriesOverTimeGridTooltipProps> = {
|
|
94
|
+
...Template,
|
|
95
|
+
args: {
|
|
96
|
+
...Template.args,
|
|
97
|
+
value: {
|
|
98
|
+
type: 'valueWithCoverage',
|
|
99
|
+
count: 100,
|
|
100
|
+
coverage: 200,
|
|
101
|
+
totalCount: 300,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
play: async ({ canvasElement }) => {
|
|
105
|
+
const canvas = within(canvasElement);
|
|
106
|
+
|
|
107
|
+
await expect(canvas.getByText('50.00%')).toBeVisible();
|
|
108
|
+
await expect(canvas.getByText('100')).toBeVisible();
|
|
109
|
+
await expect(canvas.getByText('match the query BA.1 Lineage out of')).toBeVisible();
|
|
110
|
+
await expect(canvas.getByText('200')).toBeVisible();
|
|
111
|
+
await expect(canvas.getByText('with coverage for this query.')).toBeVisible();
|
|
112
|
+
await expect(canvas.getByText('300')).toBeVisible();
|
|
113
|
+
await expect(canvas.getByText('total in this date range.')).toBeVisible();
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const WithValueBelowThreshold: StoryObj<QueriesOverTimeGridTooltipProps> = {
|
|
118
|
+
...Template,
|
|
119
|
+
args: {
|
|
120
|
+
...Template.args,
|
|
121
|
+
value: {
|
|
122
|
+
type: 'belowThreshold',
|
|
123
|
+
totalCount: 300,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
play: async ({ canvasElement }) => {
|
|
127
|
+
const canvas = within(canvasElement);
|
|
128
|
+
|
|
129
|
+
await expect(canvas.getByText('<0.10%')).toBeVisible();
|
|
130
|
+
await expect(canvas.getByText('None or less than 0.10% match the query.')).toBeVisible();
|
|
131
|
+
await expect(canvas.getByText('300')).toBeVisible();
|
|
132
|
+
await expect(canvas.getByText('total in this date range.')).toBeVisible();
|
|
133
|
+
},
|
|
134
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { FunctionComponent } from 'preact';
|
|
2
|
+
|
|
3
|
+
import { type ProportionValue } from '../../query/queryMutationsOverTime';
|
|
4
|
+
import { type Temporal, type TemporalClass, toTemporalClass, YearMonthDayClass } from '../../utils/temporalClass';
|
|
5
|
+
import { formatProportion } from '../shared/table/formatProportion';
|
|
6
|
+
|
|
7
|
+
// Use the same threshold as mutations for consistency
|
|
8
|
+
const QUERIES_OVER_TIME_MIN_PROPORTION = 0.001;
|
|
9
|
+
|
|
10
|
+
export type QueriesOverTimeGridTooltipProps = {
|
|
11
|
+
query: string; // displayLabel
|
|
12
|
+
date: Temporal;
|
|
13
|
+
value: ProportionValue;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const QueriesOverTimeGridTooltip: FunctionComponent<QueriesOverTimeGridTooltipProps> = ({
|
|
17
|
+
query,
|
|
18
|
+
date,
|
|
19
|
+
value,
|
|
20
|
+
}: QueriesOverTimeGridTooltipProps) => {
|
|
21
|
+
const dateClass = toTemporalClass(date);
|
|
22
|
+
|
|
23
|
+
let proportionText = 'No data';
|
|
24
|
+
|
|
25
|
+
if (value !== null) {
|
|
26
|
+
switch (value.type) {
|
|
27
|
+
case 'belowThreshold': {
|
|
28
|
+
proportionText = `<${formatProportion(QUERIES_OVER_TIME_MIN_PROPORTION)}`;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
case 'value':
|
|
32
|
+
case 'wastewaterValue': {
|
|
33
|
+
proportionText = formatProportion(value.proportion);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case 'valueWithCoverage': {
|
|
37
|
+
// value.coverage will always be non-zero if we're in this case
|
|
38
|
+
proportionText = formatProportion(value.count / value.coverage);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div>
|
|
46
|
+
<div className='flex flex-row justify-between gap-4 items-baseline'>
|
|
47
|
+
<div className='flex flex-col text-left'>
|
|
48
|
+
<span className='font-bold'>{query}</span>
|
|
49
|
+
<span>{proportionText}</span>
|
|
50
|
+
</div>
|
|
51
|
+
<div className='flex flex-col text-right'>
|
|
52
|
+
<span className='font-bold'>{dateClass.englishName()}</span>
|
|
53
|
+
<span className='text-gray-600'>{timeIntervalDisplay(dateClass)}</span>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
{value !== null && <TooltipValueCountsDescription value={value} queryLabel={query} />}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const TooltipValueCountsDescription: FunctionComponent<{
|
|
62
|
+
value: NonNullable<ProportionValue>;
|
|
63
|
+
queryLabel: string;
|
|
64
|
+
}> = ({ value, queryLabel }) => {
|
|
65
|
+
if (value.type === 'wastewaterValue') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
return (
|
|
69
|
+
<div className='mt-2'>
|
|
70
|
+
{(() => {
|
|
71
|
+
switch (value.type) {
|
|
72
|
+
case 'belowThreshold':
|
|
73
|
+
return (
|
|
74
|
+
<p className='text-gray-600'>
|
|
75
|
+
None or less than {formatProportion(QUERIES_OVER_TIME_MIN_PROPORTION)} match the query.
|
|
76
|
+
</p>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
case 'value':
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
<p>
|
|
83
|
+
{value.count} <span className='text-gray-600'>match the query {queryLabel}.</span>
|
|
84
|
+
</p>
|
|
85
|
+
{value.proportion > 0 && (
|
|
86
|
+
<p>
|
|
87
|
+
{Math.round(value.count / value.proportion)}{' '}
|
|
88
|
+
<span className='text-gray-600'>total with coverage.</span>
|
|
89
|
+
</p>
|
|
90
|
+
)}
|
|
91
|
+
</>
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
case 'valueWithCoverage':
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
<p>
|
|
98
|
+
{value.count}{' '}
|
|
99
|
+
<span className='text-gray-600'>match the query {queryLabel} out of</span>
|
|
100
|
+
</p>
|
|
101
|
+
<p>
|
|
102
|
+
{value.coverage}{' '}
|
|
103
|
+
<span className='text-gray-600'>with coverage for this query.</span>
|
|
104
|
+
</p>
|
|
105
|
+
</>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
})()}
|
|
109
|
+
|
|
110
|
+
<p>
|
|
111
|
+
{value.totalCount} <span className='text-gray-600'>total in this date range.</span>
|
|
112
|
+
</p>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const timeIntervalDisplay = (date: TemporalClass) => {
|
|
118
|
+
if (date instanceof YearMonthDayClass) {
|
|
119
|
+
return date.toString();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return `${date.firstDay.toString()} - ${date.lastDay.toString()}`;
|
|
123
|
+
};
|