@genspectrum/dashboard-components 0.6.5 → 0.6.7
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 +51 -3
- package/dist/dashboard-components.js +359 -159
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +3 -3
- package/dist/style.css +92 -8
- package/package.json +1 -1
- package/src/constants.ts +1 -0
- package/src/preact/aggregatedData/aggregate.tsx +1 -1
- package/src/preact/components/color-scale-selector-dropdown.stories.tsx +27 -0
- package/src/preact/components/color-scale-selector-dropdown.tsx +17 -0
- package/src/preact/components/color-scale-selector.stories.tsx +61 -0
- package/src/preact/components/color-scale-selector.tsx +79 -0
- package/src/preact/components/info.stories.tsx +37 -10
- package/src/preact/components/info.tsx +22 -43
- package/src/preact/components/min-max-range-slider.tsx +1 -1
- package/src/preact/components/tooltip.stories.tsx +12 -2
- package/src/preact/components/tooltip.tsx +38 -14
- package/src/preact/mutationComparison/mutation-comparison.tsx +1 -1
- package/src/preact/mutationFilter/mutation-filter-info.tsx +1 -1
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +1 -1
- package/src/preact/mutationFilter/mutation-filter.tsx +2 -3
- package/src/preact/mutations/mutations.tsx +1 -1
- package/src/preact/mutationsOverTime/__mockData__/aggregated_byDay.json +38 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_byWeek.json +122 -0
- package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_20_01_2024.json +6778 -0
- package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_21_01_2024.json +7129 -0
- package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_22_01_2024.json +4681 -0
- package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_23_01_2024.json +10738 -0
- package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_24_01_2024.json +11710 -0
- package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_25_01_2024.json +11557 -0
- package/src/preact/mutationsOverTime/__mockData__/aminoAcidMutations_26_01_2024.json +8596 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_week3_2024.json +8812 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_week4_2024.json +9730 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_week5_2024.json +9865 -0
- package/src/preact/mutationsOverTime/__mockData__/nucleotideMutations_week6_2024.json +11314 -0
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +83 -40
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +50 -11
- package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +1 -1
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +1 -1
- package/src/preact/shared/charts/colors.ts +1 -1
- package/src/utils/temporal.spec.ts +3 -4
- package/src/utils/temporal.ts +9 -4
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +262 -2
- package/src/preact/shared/icons/DeleteIcon.tsx +0 -17
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Fragment, type FunctionComponent } from 'preact';
|
|
1
|
+
import { Fragment, type FunctionComponent, type RefObject } from 'preact';
|
|
2
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
type MutationOverTimeDataGroupedByMutation,
|
|
@@ -6,22 +7,28 @@ import {
|
|
|
6
7
|
} from '../../query/queryMutationsOverTime';
|
|
7
8
|
import { type Deletion, type Substitution } from '../../utils/mutations';
|
|
8
9
|
import { compareTemporal, type Temporal, YearMonthDay } from '../../utils/temporal';
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
10
|
+
import { type ColorScale, getColorWithingScale, getTextColorForScale } from '../components/color-scale-selector';
|
|
11
|
+
import Tooltip, { type TooltipPosition } from '../components/tooltip';
|
|
11
12
|
import { formatProportion } from '../shared/table/formatProportion';
|
|
12
13
|
|
|
13
14
|
export interface MutationsOverTimeGridProps {
|
|
14
15
|
data: MutationOverTimeDataGroupedByMutation;
|
|
16
|
+
colorScale: ColorScale;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
const MAX_NUMBER_OF_GRID_ROWS = 100;
|
|
20
|
+
const MUTATION_CELL_WIDTH_REM = 8;
|
|
18
21
|
|
|
19
|
-
const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({ data }) => {
|
|
22
|
+
const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({ data, colorScale }) => {
|
|
20
23
|
const allMutations = data.getFirstAxisKeys();
|
|
21
24
|
const shownMutations = allMutations.slice(0, MAX_NUMBER_OF_GRID_ROWS);
|
|
22
25
|
|
|
23
26
|
const dates = data.getSecondAxisKeys().sort((a, b) => compareTemporal(a, b));
|
|
24
27
|
|
|
28
|
+
const [showProportionText, setShowProportionText] = useState(false);
|
|
29
|
+
const gridRef = useRef<HTMLDivElement>(null);
|
|
30
|
+
useShowProportion(gridRef, dates.length, setShowProportionText);
|
|
31
|
+
|
|
25
32
|
return (
|
|
26
33
|
<>
|
|
27
34
|
{allMutations.length > MAX_NUMBER_OF_GRID_ROWS && (
|
|
@@ -31,29 +38,43 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
|
|
|
31
38
|
</div>
|
|
32
39
|
)}
|
|
33
40
|
<div
|
|
41
|
+
ref={gridRef}
|
|
34
42
|
style={{
|
|
35
43
|
display: 'grid',
|
|
36
44
|
gridTemplateRows: `repeat(${shownMutations.length}, 24px)`,
|
|
37
|
-
gridTemplateColumns:
|
|
45
|
+
gridTemplateColumns: `${MUTATION_CELL_WIDTH_REM}rem repeat(${dates.length}, minmax(0.05rem, 1fr))`,
|
|
38
46
|
}}
|
|
39
47
|
>
|
|
40
|
-
{shownMutations.map((mutation,
|
|
48
|
+
{shownMutations.map((mutation, rowIndex) => {
|
|
41
49
|
return (
|
|
42
50
|
<Fragment key={`fragment-${mutation.toString()}`}>
|
|
43
51
|
<div
|
|
44
52
|
key={`mutation-${mutation.toString()}`}
|
|
45
|
-
style={{ gridRowStart:
|
|
53
|
+
style={{ gridRowStart: rowIndex + 1, gridColumnStart: 1 }}
|
|
46
54
|
>
|
|
47
55
|
<MutationCell mutation={mutation} />
|
|
48
56
|
</div>
|
|
49
|
-
{dates.map((date,
|
|
57
|
+
{dates.map((date, columnIndex) => {
|
|
50
58
|
const value = data.get(mutation, date) ?? { proportion: 0, count: 0 };
|
|
59
|
+
const tooltipPosition = getTooltipPosition(
|
|
60
|
+
rowIndex,
|
|
61
|
+
shownMutations.length,
|
|
62
|
+
columnIndex,
|
|
63
|
+
dates.length,
|
|
64
|
+
);
|
|
51
65
|
return (
|
|
52
66
|
<div
|
|
53
|
-
style={{ gridRowStart:
|
|
67
|
+
style={{ gridRowStart: rowIndex + 1, gridColumnStart: columnIndex + 2 }}
|
|
54
68
|
key={`${mutation.toString()}-${date.toString()}`}
|
|
55
69
|
>
|
|
56
|
-
<ProportionCell
|
|
70
|
+
<ProportionCell
|
|
71
|
+
value={value}
|
|
72
|
+
date={date}
|
|
73
|
+
mutation={mutation}
|
|
74
|
+
tooltipPosition={tooltipPosition}
|
|
75
|
+
showProportionText={showProportionText}
|
|
76
|
+
colorScale={colorScale}
|
|
77
|
+
/>
|
|
57
78
|
</div>
|
|
58
79
|
);
|
|
59
80
|
})}
|
|
@@ -65,16 +86,53 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
|
|
|
65
86
|
);
|
|
66
87
|
};
|
|
67
88
|
|
|
89
|
+
function useShowProportion(
|
|
90
|
+
gridRef: RefObject<HTMLDivElement>,
|
|
91
|
+
girdColumns: number,
|
|
92
|
+
setShowProportionText: (value: ((prevState: boolean) => boolean) | boolean) => void,
|
|
93
|
+
) {
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const checkWidth = () => {
|
|
96
|
+
if (gridRef.current) {
|
|
97
|
+
const width = gridRef.current.getBoundingClientRect().width;
|
|
98
|
+
const widthPerDate = (width - remToPx(MUTATION_CELL_WIDTH_REM)) / girdColumns;
|
|
99
|
+
const maxWidthProportionText = 28;
|
|
100
|
+
|
|
101
|
+
setShowProportionText(widthPerDate > maxWidthProportionText);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
checkWidth();
|
|
106
|
+
window.addEventListener('resize', checkWidth);
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
window.removeEventListener('resize', checkWidth);
|
|
110
|
+
};
|
|
111
|
+
}, [girdColumns, gridRef, setShowProportionText]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const remToPx = (rem: number) => rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
115
|
+
|
|
116
|
+
function getTooltipPosition(rowIndex: number, rows: number, columnIndex: number, columns: number) {
|
|
117
|
+
const tooltipX = rowIndex < rows / 2 ? 'bottom' : 'top';
|
|
118
|
+
const tooltipY = columnIndex < columns / 2 ? 'start' : 'end';
|
|
119
|
+
return `${tooltipX}-${tooltipY}` as const;
|
|
120
|
+
}
|
|
121
|
+
|
|
68
122
|
const ProportionCell: FunctionComponent<{
|
|
69
123
|
value: MutationOverTimeMutationValue;
|
|
70
124
|
date: Temporal;
|
|
71
125
|
mutation: Substitution | Deletion;
|
|
72
|
-
|
|
126
|
+
tooltipPosition: TooltipPosition;
|
|
127
|
+
showProportionText: boolean;
|
|
128
|
+
colorScale: ColorScale;
|
|
129
|
+
}> = ({ value, mutation, date, tooltipPosition, showProportionText, colorScale }) => {
|
|
73
130
|
const tooltipContent = (
|
|
74
131
|
<div>
|
|
75
132
|
<p>
|
|
76
|
-
<span className='font-bold'>{date.englishName()}</span>
|
|
133
|
+
<span className='font-bold'>{date.englishName()}</span>
|
|
77
134
|
</p>
|
|
135
|
+
<p>({timeIntervalDisplay(date)})</p>
|
|
78
136
|
<p>{mutation.code}</p>
|
|
79
137
|
<p>Proportion: {formatProportion(value.proportion)}</p>
|
|
80
138
|
<p>Count: {value.count}</p>
|
|
@@ -82,21 +140,19 @@ const ProportionCell: FunctionComponent<{
|
|
|
82
140
|
);
|
|
83
141
|
|
|
84
142
|
return (
|
|
85
|
-
|
|
86
|
-
<
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
</div>
|
|
99
|
-
</>
|
|
143
|
+
<div className={'py-1 w-full h-full'}>
|
|
144
|
+
<Tooltip content={tooltipContent} position={tooltipPosition}>
|
|
145
|
+
<div
|
|
146
|
+
style={{
|
|
147
|
+
backgroundColor: getColorWithingScale(value.proportion, colorScale),
|
|
148
|
+
color: getTextColorForScale(value.proportion, colorScale),
|
|
149
|
+
}}
|
|
150
|
+
className={`w-full h-full text-center hover:font-bold text-xs group`}
|
|
151
|
+
>
|
|
152
|
+
{showProportionText ? formatProportion(value.proportion, 0) : undefined}
|
|
153
|
+
</div>
|
|
154
|
+
</Tooltip>
|
|
155
|
+
</div>
|
|
100
156
|
);
|
|
101
157
|
};
|
|
102
158
|
|
|
@@ -108,19 +164,6 @@ const timeIntervalDisplay = (date: Temporal) => {
|
|
|
108
164
|
return `${date.firstDay.toString()} - ${date.lastDay.toString()}`;
|
|
109
165
|
};
|
|
110
166
|
|
|
111
|
-
const backgroundColor = (proportion: number) => {
|
|
112
|
-
// TODO(#353): Make minAlpha and maxAlpha configurable
|
|
113
|
-
const minAlpha = 0.0;
|
|
114
|
-
const maxAlpha = 1;
|
|
115
|
-
|
|
116
|
-
const alpha = minAlpha + (maxAlpha - minAlpha) * proportion;
|
|
117
|
-
return singleGraphColorRGBByName('indigo', alpha);
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const textColor = (proportion: number) => {
|
|
121
|
-
return proportion > 0.5 ? 'white' : 'black';
|
|
122
|
-
};
|
|
123
|
-
|
|
124
167
|
const MutationCell: FunctionComponent<{ mutation: Substitution | Deletion }> = ({ mutation }) => {
|
|
125
168
|
return <div className='text-center'>{mutation.toString()}</div>;
|
|
126
169
|
};
|
|
@@ -8,8 +8,12 @@ import {
|
|
|
8
8
|
queryMutationsOverTimeData,
|
|
9
9
|
} from '../../query/queryMutationsOverTime';
|
|
10
10
|
import { type LapisFilter, type SequenceType, type TemporalGranularity } from '../../types';
|
|
11
|
+
import { compareTemporal } from '../../utils/temporal';
|
|
11
12
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
12
13
|
import { type DisplayedSegment, SegmentSelector, useDisplayedSegments } from '../components/SegmentSelector';
|
|
14
|
+
import { type ColorScale } from '../components/color-scale-selector';
|
|
15
|
+
import { ColorScaleSelectorDropdown } from '../components/color-scale-selector-dropdown';
|
|
16
|
+
import { CsvDownloadButton } from '../components/csv-download-button';
|
|
13
17
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
14
18
|
import { ErrorDisplay } from '../components/error-display';
|
|
15
19
|
import Info from '../components/info';
|
|
@@ -88,6 +92,7 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
|
88
92
|
views,
|
|
89
93
|
}) => {
|
|
90
94
|
const [proportionInterval, setProportionInterval] = useState({ min: 0.05, max: 0.9 });
|
|
95
|
+
const [colorScale, setColorScale] = useState<ColorScale>({ min: 0, max: 1, color: 'indigo' });
|
|
91
96
|
|
|
92
97
|
const [displayedSegments, setDisplayedSegments] = useDisplayedSegments(sequenceType);
|
|
93
98
|
const [displayedMutationTypes, setDisplayedMutationTypes] = useState<DisplayedMutationType[]>([
|
|
@@ -111,21 +116,25 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
|
111
116
|
case 'grid':
|
|
112
117
|
return {
|
|
113
118
|
title: 'Grid',
|
|
114
|
-
content: <MutationsOverTimeGrid data={filteredData} />,
|
|
119
|
+
content: <MutationsOverTimeGrid data={filteredData} colorScale={colorScale} />,
|
|
115
120
|
};
|
|
116
121
|
}
|
|
117
122
|
};
|
|
118
123
|
|
|
119
124
|
const tabs = views.map((view) => getTab(view));
|
|
120
125
|
|
|
121
|
-
const toolbar = () => (
|
|
126
|
+
const toolbar = (activeTab: string) => (
|
|
122
127
|
<Toolbar
|
|
128
|
+
activeTab={activeTab}
|
|
123
129
|
displayedSegments={displayedSegments}
|
|
124
130
|
setDisplayedSegments={setDisplayedSegments}
|
|
125
131
|
displayedMutationTypes={displayedMutationTypes}
|
|
126
132
|
setDisplayedMutationTypes={setDisplayedMutationTypes}
|
|
127
133
|
proportionInterval={proportionInterval}
|
|
128
134
|
setProportionInterval={setProportionInterval}
|
|
135
|
+
filteredData={filteredData}
|
|
136
|
+
colorScale={colorScale}
|
|
137
|
+
setColorScale={setColorScale}
|
|
129
138
|
/>
|
|
130
139
|
);
|
|
131
140
|
|
|
@@ -133,38 +142,68 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
|
133
142
|
};
|
|
134
143
|
|
|
135
144
|
type ToolbarProps = {
|
|
145
|
+
activeTab: string;
|
|
136
146
|
displayedSegments: DisplayedSegment[];
|
|
137
147
|
setDisplayedSegments: (segments: DisplayedSegment[]) => void;
|
|
138
148
|
displayedMutationTypes: DisplayedMutationType[];
|
|
139
149
|
setDisplayedMutationTypes: (types: DisplayedMutationType[]) => void;
|
|
140
150
|
proportionInterval: ProportionInterval;
|
|
141
151
|
setProportionInterval: Dispatch<StateUpdater<ProportionInterval>>;
|
|
152
|
+
filteredData: MutationOverTimeDataGroupedByMutation;
|
|
153
|
+
colorScale: ColorScale;
|
|
154
|
+
setColorScale: Dispatch<StateUpdater<ColorScale>>;
|
|
142
155
|
};
|
|
143
156
|
|
|
144
157
|
const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
158
|
+
activeTab,
|
|
145
159
|
displayedSegments,
|
|
146
160
|
setDisplayedSegments,
|
|
147
161
|
displayedMutationTypes,
|
|
148
162
|
setDisplayedMutationTypes,
|
|
149
163
|
proportionInterval,
|
|
150
164
|
setProportionInterval,
|
|
165
|
+
filteredData,
|
|
166
|
+
colorScale,
|
|
167
|
+
setColorScale,
|
|
151
168
|
}) => {
|
|
152
169
|
return (
|
|
153
170
|
<>
|
|
171
|
+
{activeTab === 'Grid' && (
|
|
172
|
+
<ColorScaleSelectorDropdown colorScale={colorScale} setColorScale={setColorScale} />
|
|
173
|
+
)}
|
|
154
174
|
<SegmentSelector displayedSegments={displayedSegments} setDisplayedSegments={setDisplayedSegments} />
|
|
155
175
|
<MutationTypeSelector
|
|
156
176
|
setDisplayedMutationTypes={setDisplayedMutationTypes}
|
|
157
177
|
displayedMutationTypes={displayedMutationTypes}
|
|
158
178
|
/>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
179
|
+
<ProportionSelectorDropdown
|
|
180
|
+
proportionInterval={proportionInterval}
|
|
181
|
+
setMinProportion={(min) => setProportionInterval((prev) => ({ ...prev, min }))}
|
|
182
|
+
setMaxProportion={(max) => setProportionInterval((prev) => ({ ...prev, max }))}
|
|
183
|
+
/>
|
|
184
|
+
<CsvDownloadButton
|
|
185
|
+
className='mx-1 btn btn-xs'
|
|
186
|
+
getData={() => getDownloadData(filteredData)}
|
|
187
|
+
filename='mutations_over_time.csv'
|
|
188
|
+
/>
|
|
189
|
+
<Info>Info for mutations over time</Info>
|
|
168
190
|
</>
|
|
169
191
|
);
|
|
170
192
|
};
|
|
193
|
+
|
|
194
|
+
function getDownloadData(filteredData: MutationOverTimeDataGroupedByMutation) {
|
|
195
|
+
const dates = filteredData.getSecondAxisKeys().sort((a, b) => compareTemporal(a, b));
|
|
196
|
+
|
|
197
|
+
return filteredData.getFirstAxisKeys().map((mutation) => {
|
|
198
|
+
return dates.reduce(
|
|
199
|
+
(accumulated, date) => {
|
|
200
|
+
const proportion = filteredData.get(mutation, date)?.proportion ?? 0;
|
|
201
|
+
return {
|
|
202
|
+
...accumulated,
|
|
203
|
+
[date.toString()]: proportion,
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
{ mutation: mutation.toString() },
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -156,7 +156,7 @@ const Toolbar = ({ activeTab, data, granularity, yAxisScaleType, setYAxisScaleTy
|
|
|
156
156
|
};
|
|
157
157
|
|
|
158
158
|
const NumberSequencesOverTimeInfo = () => (
|
|
159
|
-
<Info
|
|
159
|
+
<Info>
|
|
160
160
|
<InfoHeadline1>Number of sequences over time</InfoHeadline1>
|
|
161
161
|
<InfoParagraph>
|
|
162
162
|
<a href='https://github.com/GenSpectrum/dashboard-components/issues/315'>TODO</a>
|
|
@@ -236,7 +236,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
236
236
|
|
|
237
237
|
const PrevalenceOverTimeInfo: FunctionComponent = () => {
|
|
238
238
|
return (
|
|
239
|
-
<Info
|
|
239
|
+
<Info>
|
|
240
240
|
<InfoHeadline1>Prevalence over time</InfoHeadline1>
|
|
241
241
|
<InfoParagraph>Prevalence over time info.</InfoParagraph>
|
|
242
242
|
</Info>
|
|
@@ -12,7 +12,7 @@ const ColorsRGB = {
|
|
|
12
12
|
purple: [170, 68, 153],
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
type GraphColor = keyof typeof ColorsRGB;
|
|
15
|
+
export type GraphColor = keyof typeof ColorsRGB;
|
|
16
16
|
|
|
17
17
|
export const singleGraphColorRGBAById = (id: number, alpha = 1) => {
|
|
18
18
|
const keys = Object.keys(ColorsRGB) as GraphColor[];
|
|
@@ -57,8 +57,7 @@ describe('YearMonthDay', () => {
|
|
|
57
57
|
expect(underTest.yearNumber).equal(2020);
|
|
58
58
|
expect(underTest.monthNumber).equal(1);
|
|
59
59
|
expect(underTest.dayNumber).equal(1);
|
|
60
|
-
|
|
61
|
-
expect(underTest.week.text).equal('2019-01');
|
|
60
|
+
expect(underTest.week.text).equal('2020-W01');
|
|
62
61
|
expect(underTest.text).equal('2020-01-01');
|
|
63
62
|
expect(underTest.firstDay.text).equal('2020-01-01');
|
|
64
63
|
expect(underTest.lastDay.text).equal('2020-01-01');
|
|
@@ -67,12 +66,12 @@ describe('YearMonthDay', () => {
|
|
|
67
66
|
|
|
68
67
|
describe('YearWeek', () => {
|
|
69
68
|
it('should parse from string', () => {
|
|
70
|
-
const underTest = YearWeek.parse('2020-
|
|
69
|
+
const underTest = YearWeek.parse('2020-W02', cache);
|
|
71
70
|
|
|
72
71
|
expect(underTest.isoYearNumber).equal(2020);
|
|
73
72
|
expect(underTest.isoWeekNumber).equal(2);
|
|
74
73
|
expect(underTest.firstDay.text).equal('2020-01-06');
|
|
75
|
-
expect(underTest.text).equal('2020-
|
|
74
|
+
expect(underTest.text).equal('2020-W02');
|
|
76
75
|
expect(underTest.lastDay.text).equal('2020-01-12');
|
|
77
76
|
});
|
|
78
77
|
});
|
package/src/utils/temporal.ts
CHANGED
|
@@ -7,6 +7,11 @@ import type { TemporalGranularity } from '../types';
|
|
|
7
7
|
dayjs.extend(isoWeek);
|
|
8
8
|
dayjs.extend(advancedFormat);
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* https://day.js.org/docs/en/plugin/advanced-format
|
|
12
|
+
*/
|
|
13
|
+
const FORMAT_ISO_WEEK_YEAR_WEEK = 'GGGG-[W]WW';
|
|
14
|
+
|
|
10
15
|
export class TemporalCache {
|
|
11
16
|
private yearMonthDayCache = new Map<string, YearMonthDay>();
|
|
12
17
|
private yearWeekCache = new Map<string, YearWeek>();
|
|
@@ -93,7 +98,7 @@ export class YearMonthDay {
|
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
get week(): YearWeek {
|
|
96
|
-
return this.cache.getYearWeek(this.dayjs.format(
|
|
101
|
+
return this.cache.getYearWeek(this.dayjs.format(FORMAT_ISO_WEEK_YEAR_WEEK));
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
addDays(days: number): YearMonthDay {
|
|
@@ -120,7 +125,7 @@ export class YearWeek {
|
|
|
120
125
|
) {}
|
|
121
126
|
|
|
122
127
|
get text(): string {
|
|
123
|
-
return this.firstDay.dayjs.format(
|
|
128
|
+
return this.firstDay.dayjs.format(FORMAT_ISO_WEEK_YEAR_WEEK);
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
toString(): string {
|
|
@@ -160,7 +165,7 @@ export class YearWeek {
|
|
|
160
165
|
|
|
161
166
|
addWeeks(weeks: number): YearWeek {
|
|
162
167
|
const date = this.firstDay.dayjs.add(weeks, 'week');
|
|
163
|
-
const s = date.format(
|
|
168
|
+
const s = date.format(FORMAT_ISO_WEEK_YEAR_WEEK);
|
|
164
169
|
return this.cache.getYearWeek(s);
|
|
165
170
|
}
|
|
166
171
|
|
|
@@ -169,7 +174,7 @@ export class YearWeek {
|
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
static parse(s: string, cache: TemporalCache): YearWeek {
|
|
172
|
-
const [year, week] = s.split('-').map((s) => parseInt(s, 10));
|
|
177
|
+
const [year, week] = s.split('-W').map((s) => parseInt(s, 10));
|
|
173
178
|
return new YearWeek(year, week, cache);
|
|
174
179
|
}
|
|
175
180
|
}
|