@genspectrum/dashboard-components 1.13.0 → 1.14.1
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 +170 -53
- package/dist/components.js +702 -164
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +190 -55
- package/package.json +1 -1
- package/src/lapisApi/lapisTypes.ts +1 -1
- package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +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 +8510 -8069
- package/standalone-bundle/dashboard-components.js.map +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"data": {
|
|
3
|
+
"queries": ["S:F456L", "S:R346T", "S:Q493E"],
|
|
4
|
+
"dateRanges": [
|
|
5
|
+
{ "dateFrom": "2024-01-01", "dateTo": "2024-01-31" },
|
|
6
|
+
{ "dateFrom": "2024-02-01", "dateTo": "2024-02-29" },
|
|
7
|
+
{ "dateFrom": "2024-03-01", "dateTo": "2024-03-31" },
|
|
8
|
+
{ "dateFrom": "2024-04-01", "dateTo": "2024-04-30" },
|
|
9
|
+
{ "dateFrom": "2024-05-01", "dateTo": "2024-05-31" },
|
|
10
|
+
{ "dateFrom": "2024-06-01", "dateTo": "2024-06-30" },
|
|
11
|
+
{ "dateFrom": "2024-07-01", "dateTo": "2024-07-31" }
|
|
12
|
+
],
|
|
13
|
+
"data": [
|
|
14
|
+
[
|
|
15
|
+
{ "count": 100, "coverage": 1000 },
|
|
16
|
+
{ "count": 150, "coverage": 1050 },
|
|
17
|
+
{ "count": 200, "coverage": 1100 },
|
|
18
|
+
{ "count": 0, "coverage": 0 },
|
|
19
|
+
{ "count": 250, "coverage": 1200 },
|
|
20
|
+
{ "count": 300, "coverage": 1250 },
|
|
21
|
+
{ "count": 350, "coverage": 1300 }
|
|
22
|
+
],
|
|
23
|
+
[
|
|
24
|
+
{ "count": 200, "coverage": 1000 },
|
|
25
|
+
{ "count": 250, "coverage": 1050 },
|
|
26
|
+
{ "count": 300, "coverage": 1100 },
|
|
27
|
+
{ "count": 0, "coverage": 0 },
|
|
28
|
+
{ "count": 350, "coverage": 1200 },
|
|
29
|
+
{ "count": 400, "coverage": 1250 },
|
|
30
|
+
{ "count": 450, "coverage": 1300 }
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
{ "count": 50, "coverage": 1000 },
|
|
34
|
+
{ "count": 75, "coverage": 1050 },
|
|
35
|
+
{ "count": 100, "coverage": 1100 },
|
|
36
|
+
{ "count": 0, "coverage": 0 },
|
|
37
|
+
{ "count": 125, "coverage": 1200 },
|
|
38
|
+
{ "count": 150, "coverage": 1250 },
|
|
39
|
+
{ "count": 175, "coverage": 1300 }
|
|
40
|
+
]
|
|
41
|
+
],
|
|
42
|
+
"totalCountsByDateRange": [1000, 1050, 1100, 0, 1200, 1250, 1300]
|
|
43
|
+
},
|
|
44
|
+
"info": {
|
|
45
|
+
"dataVersion": "test",
|
|
46
|
+
"requestId": "test-request-gaps",
|
|
47
|
+
"requestInfo": "Mock data with gaps for testing hide gaps functionality",
|
|
48
|
+
"reportTo": "https://github.com/GenSpectrum/LAPIS/issues",
|
|
49
|
+
"lapisVersion": "test",
|
|
50
|
+
"siloVersion": "test"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type ProportionValue } from '../../query/queryMutationsOverTime';
|
|
2
|
+
import { serializeQuery, serializeTemporal } from '../../query/queryQueriesOverTime';
|
|
3
|
+
import { Map2dBase, Map2dView, type Map2DContents } from '../../utils/map2d';
|
|
4
|
+
import { type Temporal } from '../../utils/temporalClass';
|
|
5
|
+
|
|
6
|
+
export type QueryFilter = {
|
|
7
|
+
textFilter: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type GetFilteredQueryOverTimeDataArgs = {
|
|
11
|
+
data: Map2DContents<string, Temporal, ProportionValue>;
|
|
12
|
+
proportionInterval: { min: number; max: number };
|
|
13
|
+
hideGaps: boolean;
|
|
14
|
+
queryFilterValue: QueryFilter;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a Map2d wrapper for query over time data.
|
|
19
|
+
* Uses displayLabel strings as the first axis (queries) and Temporal objects as the second axis (dates).
|
|
20
|
+
*/
|
|
21
|
+
export class QueryOverTimeDataMap extends Map2dBase<string, Temporal, ProportionValue> {
|
|
22
|
+
constructor(initialContent: Map2DContents<string, Temporal, ProportionValue>) {
|
|
23
|
+
super(serializeQuery, serializeTemporal, initialContent);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getFilteredQueryOverTimeData({
|
|
28
|
+
data,
|
|
29
|
+
proportionInterval,
|
|
30
|
+
hideGaps,
|
|
31
|
+
queryFilterValue,
|
|
32
|
+
}: GetFilteredQueryOverTimeDataArgs) {
|
|
33
|
+
const dataMap = new QueryOverTimeDataMap(data);
|
|
34
|
+
const filteredData = new Map2dView(dataMap);
|
|
35
|
+
|
|
36
|
+
const queries = filteredData.getFirstAxisKeys();
|
|
37
|
+
const dates = filteredData.getSecondAxisKeys();
|
|
38
|
+
|
|
39
|
+
const queriesToFilterOut = queries.filter((query) => {
|
|
40
|
+
// Calculate overall proportion for this query
|
|
41
|
+
let totalCount = 0;
|
|
42
|
+
let totalCoverage = 0;
|
|
43
|
+
|
|
44
|
+
dates.forEach((date) => {
|
|
45
|
+
const value = filteredData.get(query, date);
|
|
46
|
+
if (value?.type === 'valueWithCoverage') {
|
|
47
|
+
totalCount += value.count;
|
|
48
|
+
totalCoverage += value.coverage;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const overallProportion = totalCoverage > 0 ? totalCount / totalCoverage : 0;
|
|
53
|
+
|
|
54
|
+
// Filter by proportion interval
|
|
55
|
+
if (overallProportion < proportionInterval.min || overallProportion > proportionInterval.max) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Filter by text (case-insensitive search in displayLabel)
|
|
60
|
+
if (
|
|
61
|
+
queryFilterValue.textFilter !== '' &&
|
|
62
|
+
!query.toLowerCase().includes(queryFilterValue.textFilter.toLowerCase())
|
|
63
|
+
) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return false;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Remove filtered queries from the data view
|
|
71
|
+
queriesToFilterOut.forEach((query) => {
|
|
72
|
+
filteredData.deleteRow(query);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Hide gaps (columns with no data)
|
|
76
|
+
if (hideGaps) {
|
|
77
|
+
const dateRangesToFilterOut = filteredData.getSecondAxisKeys().filter((dateRange) => {
|
|
78
|
+
const vals = filteredData.getColumn(dateRange);
|
|
79
|
+
return !vals.some((v) => (v?.type === 'value' || v?.type === 'valueWithCoverage') && v.totalCount > 0);
|
|
80
|
+
});
|
|
81
|
+
dateRangesToFilterOut.forEach((dateRange) => filteredData.deleteColumn(dateRange));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return filteredData;
|
|
85
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { type Dispatch, type StateUpdater } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { type QueryFilter } from './getFilteredQueriesOverTimeData';
|
|
5
|
+
|
|
6
|
+
type QueriesOverTimeFilterProps = {
|
|
7
|
+
value: QueryFilter;
|
|
8
|
+
setFilterValue: Dispatch<StateUpdater<QueryFilter>>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const QueriesOverTimeFilter: FunctionComponent<QueriesOverTimeFilterProps> = ({ value, setFilterValue }) => {
|
|
12
|
+
return (
|
|
13
|
+
<input
|
|
14
|
+
type='text'
|
|
15
|
+
placeholder='Filter queries...'
|
|
16
|
+
className='input input-xs input-bordered w-40'
|
|
17
|
+
value={value.textFilter}
|
|
18
|
+
onInput={(e) =>
|
|
19
|
+
setFilterValue({
|
|
20
|
+
textFilter: (e.target as HTMLInputElement).value,
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
@@ -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
|
+
};
|