@genspectrum/dashboard-components 0.15.0 → 0.16.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.
Files changed (80) hide show
  1. package/custom-elements.json +200 -56
  2. package/dist/components.d.ts +35 -35
  3. package/dist/components.js +311 -192
  4. package/dist/components.js.map +1 -1
  5. package/dist/style.css +9 -0
  6. package/dist/util.d.ts +46 -46
  7. package/package.json +1 -1
  8. package/src/preact/ReferenceGenomeContext.ts +14 -1
  9. package/src/preact/aggregatedData/aggregate-bar-chart.tsx +26 -5
  10. package/src/preact/aggregatedData/aggregate.stories.tsx +0 -1
  11. package/src/preact/aggregatedData/aggregate.tsx +5 -1
  12. package/src/preact/components/ReferenceGenomesAwaiter.tsx +1 -6
  13. package/src/preact/components/resize-container.tsx +1 -1
  14. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +4 -2
  15. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +0 -1
  16. package/src/preact/mutationComparison/mutation-comparison.tsx +5 -1
  17. package/src/preact/mutationFilter/mutation-filter.stories.tsx +17 -1
  18. package/src/preact/mutationFilter/mutation-filter.tsx +8 -0
  19. package/src/preact/mutations/mutations.stories.tsx +0 -1
  20. package/src/preact/mutations/mutations.tsx +1 -1
  21. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +0 -2
  22. package/src/preact/mutationsOverTime/mutations-over-time.tsx +1 -1
  23. package/src/preact/numberSequencesOverTime/number-sequences-over-time-bar-chart.tsx +8 -3
  24. package/src/preact/numberSequencesOverTime/number-sequences-over-time-line-chart.tsx +8 -3
  25. package/src/preact/numberSequencesOverTime/number-sequences-over-time.stories.tsx +3 -1
  26. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +18 -3
  27. package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +48 -35
  28. package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +83 -70
  29. package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +48 -37
  30. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +0 -3
  31. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +6 -1
  32. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +31 -23
  33. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +0 -1
  34. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +5 -1
  35. package/src/preact/sequencesByLocation/__mockData__/worldAtlas.json +1 -0
  36. package/src/preact/{map → sequencesByLocation}/sequences-by-location-map.tsx +6 -3
  37. package/src/preact/{map → sequencesByLocation}/sequences-by-location-table.tsx +1 -1
  38. package/src/preact/{map → sequencesByLocation}/sequences-by-location.stories.tsx +58 -1
  39. package/src/preact/{map → sequencesByLocation}/sequences-by-location.tsx +10 -1
  40. package/src/preact/shared/aspectRatio/AspectRatio.tsx +13 -0
  41. package/src/preact/shared/charts/getMaintainAspectRatio.ts +3 -0
  42. package/src/preact/statistic/statistics.stories.tsx +0 -1
  43. package/src/preact/statistic/statistics.tsx +4 -4
  44. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +0 -1
  45. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +1 -1
  46. package/src/query/computeMapLocationData.spec.ts +1 -1
  47. package/src/query/computeMapLocationData.ts +1 -1
  48. package/src/query/querySequencesByLocationData.ts +1 -1
  49. package/src/utilEntrypoint.ts +1 -1
  50. package/src/web-components/ResizeContainer.mdx +4 -1
  51. package/src/web-components/visualization/gs-aggregate.stories.ts +13 -6
  52. package/src/web-components/visualization/gs-aggregate.tsx +1 -1
  53. package/src/web-components/visualization/gs-mutation-comparison.stories.ts +8 -1
  54. package/src/web-components/visualization/gs-mutation-comparison.tsx +1 -1
  55. package/src/web-components/visualization/gs-mutations-over-time.stories.ts +9 -1
  56. package/src/web-components/visualization/gs-mutations-over-time.tsx +1 -1
  57. package/src/web-components/visualization/gs-mutations.stories.ts +8 -1
  58. package/src/web-components/visualization/gs-mutations.tsx +1 -1
  59. package/src/web-components/visualization/gs-number-sequences-over-time.stories.ts +11 -1
  60. package/src/web-components/visualization/gs-number-sequences-over-time.tsx +1 -1
  61. package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +8 -2
  62. package/src/web-components/visualization/gs-prevalence-over-time.tsx +1 -1
  63. package/src/web-components/visualization/gs-relative-growth-advantage.stories.ts +8 -1
  64. package/src/web-components/visualization/gs-relative-growth-advantage.tsx +1 -1
  65. package/src/web-components/visualization/gs-sequences-by-location.stories.ts +13 -7
  66. package/src/web-components/visualization/gs-sequences-by-location.tsx +6 -3
  67. package/src/web-components/visualization/gs-statistics.stories.ts +0 -1
  68. package/src/web-components/visualization/gs-statistics.tsx +1 -1
  69. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +9 -1
  70. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +1 -1
  71. package/standalone-bundle/dashboard-components.js +4965 -4876
  72. package/standalone-bundle/dashboard-components.js.map +1 -1
  73. package/standalone-bundle/style.css +1 -1
  74. package/src/preact/map/__mockData__/worldAtlas.json +0 -497127
  75. /package/src/preact/{map → sequencesByLocation}/__mockData__/aggregatedGermany.json +0 -0
  76. /package/src/preact/{map → sequencesByLocation}/__mockData__/aggregatedWorld.json +0 -0
  77. /package/src/preact/{map → sequencesByLocation}/__mockData__/germanyMap.json +0 -0
  78. /package/src/preact/{map → sequencesByLocation}/__mockData__/howToGenerateWorldMap.md +0 -0
  79. /package/src/preact/{map → sequencesByLocation}/leafletStyleModifications.css +0 -0
  80. /package/src/preact/{map → sequencesByLocation}/loadMapSource.tsx +0 -0
@@ -10,6 +10,7 @@ import { type MutationsFilter, mutationsFilterSchema } from '../../types';
10
10
  import { type DeletionClass, type InsertionClass, type SubstitutionClass } from '../../utils/mutations';
11
11
  import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
12
12
  import { ErrorBoundary } from '../components/error-boundary';
13
+ import { UserFacingError } from '../components/error-display';
13
14
  import { singleGraphColorRGBByName } from '../shared/charts/colors';
14
15
 
15
16
  const mutationFilterInnerPropsSchema = z.object({
@@ -54,6 +55,13 @@ export const MutationFilterInner: FunctionComponent<MutationFilterInnerProps> =
54
55
 
55
56
  const filterRef = useRef<HTMLDivElement>(null);
56
57
 
58
+ if (referenceGenome.nucleotideSequences.length === 0 && referenceGenome.genes.length === 0) {
59
+ throw new UserFacingError(
60
+ 'No reference sequences available',
61
+ 'This organism has neither nucleotide nor amino acid sequences configured in its reference genome. You cannot filter by mutations.',
62
+ );
63
+ }
64
+
57
65
  const handleRemoveValue = (option: ParsedMutationFilter) => {
58
66
  const newSelectedFilters = {
59
67
  ...selectedFilters,
@@ -55,7 +55,6 @@ export const Default: StoryObj<MutationsProps> = {
55
55
  sequenceType: 'nucleotide',
56
56
  views: ['grid', 'table', 'insertions'],
57
57
  width: '100%',
58
- height: '700px',
59
58
  pageSize: 10,
60
59
  },
61
60
  parameters: {
@@ -41,7 +41,7 @@ const mutationsPropsSchema = z.object({
41
41
  views: mutationsViewSchema.array(),
42
42
  pageSize: z.union([z.boolean(), z.number()]),
43
43
  width: z.string(),
44
- height: z.string(),
44
+ height: z.string().optional(),
45
45
  });
46
46
  export type MutationsProps = z.infer<typeof mutationsPropsSchema>;
47
47
 
@@ -62,7 +62,6 @@ export const Default: StoryObj<MutationsOverTimeProps> = {
62
62
  sequenceType: 'nucleotide',
63
63
  views: ['grid'],
64
64
  width: '100%',
65
- height: '700px',
66
65
  granularity: 'month',
67
66
  lapisDateField: 'date',
68
67
  },
@@ -76,7 +75,6 @@ export const ShowsMessageWhenTooManyMutations: StoryObj<MutationsOverTimeProps>
76
75
  sequenceType: 'nucleotide',
77
76
  views: ['grid'],
78
77
  width: '100%',
79
- height: '700px',
80
78
  granularity: 'year',
81
79
  lapisDateField: 'date',
82
80
  },
@@ -45,7 +45,7 @@ const mutationOverTimeSchema = z.object({
45
45
  granularity: temporalGranularitySchema,
46
46
  lapisDateField: z.string().min(1),
47
47
  width: z.string(),
48
- height: z.string(),
48
+ height: z.string().optional(),
49
49
  });
50
50
  export type MutationsOverTimeProps = z.infer<typeof mutationOverTimeSchema>;
51
51
 
@@ -10,11 +10,16 @@ import { getYAxisScale, type ScaleType } from '../shared/charts/getYAxisScale';
10
10
  interface NumberSequencesOverBarChartProps {
11
11
  data: NumberOfSequencesDatasets;
12
12
  yAxisScaleType: ScaleType;
13
+ maintainAspectRatio: boolean;
13
14
  }
14
15
 
15
16
  Chart.register(...registerables);
16
17
 
17
- export const NumberSequencesOverTimeBarChart = ({ data, yAxisScaleType }: NumberSequencesOverBarChartProps) => {
18
+ export const NumberSequencesOverTimeBarChart = ({
19
+ data,
20
+ yAxisScaleType,
21
+ maintainAspectRatio,
22
+ }: NumberSequencesOverBarChartProps) => {
18
23
  const config: ChartConfiguration = useMemo(
19
24
  () => ({
20
25
  type: 'bar',
@@ -22,7 +27,7 @@ export const NumberSequencesOverTimeBarChart = ({ data, yAxisScaleType }: Number
22
27
  datasets: getDatasets(data),
23
28
  },
24
29
  options: {
25
- maintainAspectRatio: false,
30
+ maintainAspectRatio,
26
31
  animation: false,
27
32
  scales: {
28
33
  y: {
@@ -40,7 +45,7 @@ export const NumberSequencesOverTimeBarChart = ({ data, yAxisScaleType }: Number
40
45
  },
41
46
  },
42
47
  }),
43
- [data, yAxisScaleType],
48
+ [data, maintainAspectRatio, yAxisScaleType],
44
49
  );
45
50
 
46
51
  return <GsChart configuration={config} />;
@@ -10,11 +10,16 @@ import { getYAxisScale, type ScaleType } from '../shared/charts/getYAxisScale';
10
10
  interface NumberSequencesOverBarChartProps {
11
11
  data: NumberOfSequencesDatasets;
12
12
  yAxisScaleType: ScaleType;
13
+ maintainAspectRatio: boolean;
13
14
  }
14
15
 
15
16
  Chart.register(...registerables);
16
17
 
17
- export const NumberSequencesOverTimeLineChart = ({ data, yAxisScaleType }: NumberSequencesOverBarChartProps) => {
18
+ export const NumberSequencesOverTimeLineChart = ({
19
+ data,
20
+ yAxisScaleType,
21
+ maintainAspectRatio,
22
+ }: NumberSequencesOverBarChartProps) => {
18
23
  const config: ChartConfiguration = useMemo(
19
24
  () => ({
20
25
  type: 'line',
@@ -22,7 +27,7 @@ export const NumberSequencesOverTimeLineChart = ({ data, yAxisScaleType }: Numbe
22
27
  datasets: getDatasets(data),
23
28
  },
24
29
  options: {
25
- maintainAspectRatio: false,
30
+ maintainAspectRatio,
26
31
  animation: false,
27
32
  scales: {
28
33
  y: {
@@ -40,7 +45,7 @@ export const NumberSequencesOverTimeLineChart = ({ data, yAxisScaleType }: Numbe
40
45
  },
41
46
  },
42
47
  }),
43
- [data, yAxisScaleType],
48
+ [data, maintainAspectRatio, yAxisScaleType],
44
49
  );
45
50
 
46
51
  return <GsChart configuration={config} />;
@@ -24,6 +24,9 @@ export default {
24
24
  control: { type: 'check' },
25
25
  },
26
26
  pageSize: { control: 'object' },
27
+ height: {
28
+ control: 'text',
29
+ },
27
30
  },
28
31
  };
29
32
 
@@ -40,7 +43,6 @@ const Template: StoryObj<NumberSequencesOverTimeProps> = {
40
43
  ],
41
44
  lapisDateField: 'date',
42
45
  width: '100%',
43
- height: '700px',
44
46
  smoothingWindow: 0,
45
47
  granularity: 'month',
46
48
  pageSize: 10,
@@ -21,6 +21,7 @@ import { NoDataDisplay } from '../components/no-data-display';
21
21
  import { ResizeContainer } from '../components/resize-container';
22
22
  import { ScalingSelector } from '../components/scaling-selector';
23
23
  import Tabs from '../components/tabs';
24
+ import { getMaintainAspectRatio } from '../shared/charts/getMaintainAspectRatio';
24
25
  import type { ScaleType } from '../shared/charts/getYAxisScale';
25
26
  import { useQuery } from '../useQuery';
26
27
 
@@ -33,7 +34,7 @@ export type NumberSequencesOverTimeView = z.infer<typeof numberSequencesOverTime
33
34
 
34
35
  const numberSequencesOverTimePropsSchema = z.object({
35
36
  width: z.string(),
36
- height: z.string(),
37
+ height: z.string().optional(),
37
38
  lapisFilters: z.array(namedLapisFilterSchema).min(1),
38
39
  lapisDateField: z.string().min(1),
39
40
  views: z.array(numberSequencesOverTimeViewSchema),
@@ -89,17 +90,31 @@ interface NumberSequencesOverTimeTabsProps {
89
90
  const NumberSequencesOverTimeTabs = ({ data, originalComponentProps }: NumberSequencesOverTimeTabsProps) => {
90
91
  const [yAxisScaleType, setYAxisScaleType] = useState<ScaleType>('linear');
91
92
 
93
+ const maintainAspectRatio = getMaintainAspectRatio(originalComponentProps.height);
94
+
92
95
  const getTab = (view: NumberSequencesOverTimeView) => {
93
96
  switch (view) {
94
97
  case 'bar':
95
98
  return {
96
99
  title: 'Bar',
97
- content: <NumberSequencesOverTimeBarChart data={data} yAxisScaleType={yAxisScaleType} />,
100
+ content: (
101
+ <NumberSequencesOverTimeBarChart
102
+ data={data}
103
+ yAxisScaleType={yAxisScaleType}
104
+ maintainAspectRatio={maintainAspectRatio}
105
+ />
106
+ ),
98
107
  };
99
108
  case 'line':
100
109
  return {
101
110
  title: 'Line',
102
- content: <NumberSequencesOverTimeLineChart data={data} yAxisScaleType={yAxisScaleType} />,
111
+ content: (
112
+ <NumberSequencesOverTimeLineChart
113
+ data={data}
114
+ yAxisScaleType={yAxisScaleType}
115
+ maintainAspectRatio={maintainAspectRatio}
116
+ />
117
+ ),
103
118
  };
104
119
  case 'table':
105
120
  return {
@@ -1,5 +1,6 @@
1
1
  import { Chart, type ChartConfiguration, type ChartDataset, registerables, type TooltipItem } from 'chart.js';
2
2
  import { BarWithErrorBar, BarWithErrorBarsController } from 'chartjs-chart-error-bars';
3
+ import { useMemo } from 'preact/hooks';
3
4
 
4
5
  import { maxInData } from './prevalence-over-time';
5
6
  import {
@@ -21,55 +22,67 @@ interface PrevalenceOverTimeBarChartProps {
21
22
  yAxisScaleType: ScaleType;
22
23
  confidenceIntervalMethod: ConfidenceIntervalMethod;
23
24
  yAxisMaxConfig: YAxisMaxConfig;
25
+ maintainAspectRatio: boolean;
24
26
  }
25
27
 
26
28
  Chart.register(...registerables, LogitScale, BarWithErrorBarsController, BarWithErrorBar);
27
29
 
30
+ const NO_DATA = 'noData';
31
+
28
32
  const PrevalenceOverTimeBarChart = ({
29
33
  data,
30
34
  yAxisScaleType,
31
35
  confidenceIntervalMethod,
32
36
  yAxisMaxConfig,
37
+ maintainAspectRatio,
33
38
  }: PrevalenceOverTimeBarChartProps) => {
34
- const nullFirstData = data
35
- .filter((prevalenceOverTimeData) => prevalenceOverTimeData.content.length > 0)
36
- .map((variantData) => {
37
- return {
38
- content: variantData.content.sort(sortNullToBeginningThenByDate),
39
- displayName: variantData.displayName,
40
- };
41
- });
39
+ const config = useMemo<ChartConfiguration | typeof NO_DATA>(() => {
40
+ const nullFirstData = data
41
+ .filter((prevalenceOverTimeData) => prevalenceOverTimeData.content.length > 0)
42
+ .map((variantData) => {
43
+ return {
44
+ content: variantData.content.sort(sortNullToBeginningThenByDate),
45
+ displayName: variantData.displayName,
46
+ };
47
+ });
48
+
49
+ if (nullFirstData.length === 0) {
50
+ return NO_DATA;
51
+ }
52
+
53
+ const datasets = nullFirstData.map((graphData, index) =>
54
+ getDataset(graphData, index, confidenceIntervalMethod),
55
+ );
56
+
57
+ const maxY =
58
+ yAxisScaleType !== 'logit'
59
+ ? getYAxisMax(maxInData(nullFirstData), yAxisMaxConfig?.[yAxisScaleType])
60
+ : undefined;
42
61
 
43
- if (nullFirstData.length === 0) {
44
- return <NoDataDisplay />;
45
- }
46
-
47
- const datasets = nullFirstData.map((graphData, index) => getDataset(graphData, index, confidenceIntervalMethod));
48
-
49
- const maxY =
50
- yAxisScaleType !== 'logit'
51
- ? getYAxisMax(maxInData(nullFirstData), yAxisMaxConfig?.[yAxisScaleType])
52
- : undefined;
53
-
54
- const config: ChartConfiguration = {
55
- type: BarWithErrorBarsController.id,
56
- data: {
57
- datasets,
58
- },
59
- options: {
60
- maintainAspectRatio: false,
61
- animation: false,
62
- scales: {
63
- y: { ...getYAxisScale(yAxisScaleType), max: maxY },
62
+ return {
63
+ type: BarWithErrorBarsController.id,
64
+ data: {
65
+ datasets,
64
66
  },
65
- plugins: {
66
- legend: {
67
- display: false,
67
+ options: {
68
+ maintainAspectRatio,
69
+ animation: false,
70
+ scales: {
71
+ y: { ...getYAxisScale(yAxisScaleType), max: maxY },
72
+ },
73
+ plugins: {
74
+ legend: {
75
+ display: false,
76
+ },
77
+ tooltip: tooltip(confidenceIntervalMethod),
68
78
  },
69
- tooltip: tooltip(confidenceIntervalMethod),
70
79
  },
71
- },
72
- };
80
+ };
81
+ }, [data, yAxisScaleType, confidenceIntervalMethod, yAxisMaxConfig, maintainAspectRatio]);
82
+
83
+ if (config === NO_DATA) {
84
+ return <NoDataDisplay />;
85
+ }
73
86
 
74
87
  return <GsChart configuration={config} />;
75
88
  };
@@ -1,4 +1,5 @@
1
1
  import { Chart, type ChartConfiguration, registerables } from 'chart.js';
2
+ import { useMemo } from 'preact/hooks';
2
3
 
3
4
  import { maxInData } from './prevalence-over-time';
4
5
  import { type PrevalenceOverTimeData } from '../../query/queryPrevalenceOverTime';
@@ -15,97 +16,109 @@ interface PrevalenceOverTimeBubbleChartProps {
15
16
  data: PrevalenceOverTimeData;
16
17
  yAxisScaleType: ScaleType;
17
18
  yAxisMaxConfig: YAxisMaxConfig;
19
+ maintainAspectRatio: boolean;
18
20
  }
19
21
 
20
22
  Chart.register(...registerables, LogitScale);
21
23
 
24
+ const NO_DATA = 'noData';
25
+
22
26
  const PrevalenceOverTimeBubbleChart = ({
23
27
  data,
24
28
  yAxisScaleType,
25
29
  yAxisMaxConfig,
30
+ maintainAspectRatio,
26
31
  }: PrevalenceOverTimeBubbleChartProps) => {
27
- const nonNullDateRangeData = data
28
- .filter((prevalenceOverTimeData) => prevalenceOverTimeData.content.length > 0)
29
- .map((variantData) => {
30
- return {
31
- content: variantData.content.filter((dataPoint) => dataPoint.dateRange !== null),
32
- displayName: variantData.displayName,
33
- };
34
- });
32
+ const config = useMemo<ChartConfiguration | typeof NO_DATA>(() => {
33
+ const nonNullDateRangeData = data
34
+ .filter((prevalenceOverTimeData) => prevalenceOverTimeData.content.length > 0)
35
+ .map((variantData) => {
36
+ return {
37
+ content: variantData.content.filter((dataPoint) => dataPoint.dateRange !== null),
38
+ displayName: variantData.displayName,
39
+ };
40
+ });
35
41
 
36
- if (nonNullDateRangeData.length === 0) {
37
- return <NoDataDisplay />;
38
- }
42
+ if (nonNullDateRangeData.length === 0) {
43
+ return NO_DATA;
44
+ }
39
45
 
40
- const firstDate = nonNullDateRangeData[0].content[0].dateRange!;
41
- const total = nonNullDateRangeData.map((graphData) => graphData.content.map((dataPoint) => dataPoint.total)).flat();
42
- const [minTotal, maxTotal] = getMinMaxNumber(total)!;
43
- const scaleBubble = (value: number) => {
44
- return ((value - minTotal) / (maxTotal - minTotal)) * 4.5 + 0.5;
45
- };
46
+ const firstDate = nonNullDateRangeData[0].content[0].dateRange!;
47
+ const total = nonNullDateRangeData
48
+ .map((graphData) => graphData.content.map((dataPoint) => dataPoint.total))
49
+ .flat();
50
+ const [minTotal, maxTotal] = getMinMaxNumber(total)!;
51
+ const scaleBubble = (value: number) => {
52
+ return ((value - minTotal) / (maxTotal - minTotal)) * 4.5 + 0.5;
53
+ };
46
54
 
47
- const maxY =
48
- yAxisScaleType !== 'logit'
49
- ? getYAxisMax(maxInData(nonNullDateRangeData), yAxisMaxConfig?.[yAxisScaleType])
50
- : undefined;
55
+ const maxY =
56
+ yAxisScaleType !== 'logit'
57
+ ? getYAxisMax(maxInData(nonNullDateRangeData), yAxisMaxConfig?.[yAxisScaleType])
58
+ : undefined;
51
59
 
52
- const config: ChartConfiguration = {
53
- type: 'bubble',
54
- data: {
55
- datasets: nonNullDateRangeData.map((graphData, index) => ({
56
- label: graphData.displayName,
57
- data: graphData.content
58
- .filter((dataPoint) => dataPoint.dateRange !== null)
59
- .map((dataPoint) => ({
60
- x: minusTemporal(dataPoint.dateRange!, firstDate),
61
- y: dataPoint.prevalence,
62
- r: scaleBubble(dataPoint.total),
63
- })),
64
- borderWidth: 1,
65
- pointRadius: 0,
66
- backgroundColor: singleGraphColorRGBAById(index, 0.3),
67
- borderColor: singleGraphColorRGBAById(index),
68
- })),
69
- },
70
- options: {
71
- animation: false,
72
- maintainAspectRatio: false,
73
- scales: {
74
- x: {
75
- ticks: {
76
- callback: (value) => addUnit(firstDate, value as number).toString(),
77
- },
78
- },
79
- y: { ...getYAxisScale(yAxisScaleType), max: maxY },
60
+ return {
61
+ type: 'bubble',
62
+ data: {
63
+ datasets: nonNullDateRangeData.map((graphData, index) => ({
64
+ label: graphData.displayName,
65
+ data: graphData.content
66
+ .filter((dataPoint) => dataPoint.dateRange !== null)
67
+ .map((dataPoint) => ({
68
+ x: minusTemporal(dataPoint.dateRange!, firstDate),
69
+ y: dataPoint.prevalence,
70
+ r: scaleBubble(dataPoint.total),
71
+ })),
72
+ borderWidth: 1,
73
+ pointRadius: 0,
74
+ backgroundColor: singleGraphColorRGBAById(index, 0.3),
75
+ borderColor: singleGraphColorRGBAById(index),
76
+ })),
80
77
  },
81
- plugins: {
82
- legend: {
83
- display: false,
84
- },
85
- tooltip: {
86
- mode: 'index',
87
- intersect: false,
88
- callbacks: {
89
- title: (context) => {
90
- const dataset = nonNullDateRangeData[context[0].datasetIndex];
91
- const dataPoint = dataset.content[context[0].dataIndex];
92
- return dataPoint.dateRange?.toString();
78
+ options: {
79
+ animation: false,
80
+ maintainAspectRatio,
81
+ scales: {
82
+ x: {
83
+ ticks: {
84
+ callback: (value) => addUnit(firstDate, value as number).toString(),
93
85
  },
94
- label: (context) => {
95
- const dataset = nonNullDateRangeData[context.datasetIndex];
96
- const dataPoint = dataset.content[context.dataIndex];
86
+ },
87
+ y: { ...getYAxisScale(yAxisScaleType), max: maxY },
88
+ },
89
+ plugins: {
90
+ legend: {
91
+ display: false,
92
+ },
93
+ tooltip: {
94
+ mode: 'index',
95
+ intersect: false,
96
+ callbacks: {
97
+ title: (context) => {
98
+ const dataset = nonNullDateRangeData[context[0].datasetIndex];
99
+ const dataPoint = dataset.content[context[0].dataIndex];
100
+ return dataPoint.dateRange?.toString();
101
+ },
102
+ label: (context) => {
103
+ const dataset = nonNullDateRangeData[context.datasetIndex];
104
+ const dataPoint = dataset.content[context.dataIndex];
97
105
 
98
- const percentage = (dataPoint.prevalence * 100).toFixed(2);
99
- const count = dataPoint.count.toFixed(0);
100
- const total = dataPoint.total.toFixed(0);
106
+ const percentage = (dataPoint.prevalence * 100).toFixed(2);
107
+ const count = dataPoint.count.toFixed(0);
108
+ const total = dataPoint.total.toFixed(0);
101
109
 
102
- return `${dataset.displayName}: ${percentage}%, ${count}/${total} samples`;
110
+ return `${dataset.displayName}: ${percentage}%, ${count}/${total} samples`;
111
+ },
103
112
  },
104
113
  },
105
114
  },
106
115
  },
107
- },
108
- };
116
+ } satisfies ChartConfiguration;
117
+ }, [data, maintainAspectRatio, yAxisMaxConfig, yAxisScaleType]);
118
+
119
+ if (config === NO_DATA) {
120
+ return <NoDataDisplay />;
121
+ }
109
122
 
110
123
  return <GsChart configuration={config} />;
111
124
  };
@@ -1,5 +1,6 @@
1
1
  import { Chart, type ChartConfiguration, registerables } from 'chart.js';
2
2
  import { type TooltipItem } from 'chart.js/dist/types';
3
+ import { useMemo } from 'preact/hooks';
3
4
 
4
5
  import { maxInData } from './prevalence-over-time';
5
6
  import { type PrevalenceOverTimeData, type PrevalenceOverTimeVariantData } from '../../query/queryPrevalenceOverTime';
@@ -20,57 +21,67 @@ interface PrevalenceOverTimeLineChartProps {
20
21
  yAxisScaleType: ScaleType;
21
22
  confidenceIntervalMethod: ConfidenceIntervalMethod;
22
23
  yAxisMaxConfig: YAxisMaxConfig;
24
+ maintainAspectRatio: boolean;
23
25
  }
24
26
 
25
27
  Chart.register(...registerables, LogitScale);
26
28
 
29
+ const NO_DATA = 'noData';
30
+
27
31
  const PrevalenceOverTimeLineChart = ({
28
32
  data,
29
33
  yAxisScaleType,
30
34
  confidenceIntervalMethod,
31
35
  yAxisMaxConfig,
36
+ maintainAspectRatio,
32
37
  }: PrevalenceOverTimeLineChartProps) => {
33
- const nonNullDateRangeData = data
34
- .filter((prevalenceOverTimeData) => prevalenceOverTimeData.content.length > 0)
35
- .map((variantData) => {
36
- return {
37
- content: variantData.content.filter((dataPoint) => dataPoint.dateRange !== null),
38
- displayName: variantData.displayName,
39
- };
40
- });
38
+ const config = useMemo<ChartConfiguration | typeof NO_DATA>(() => {
39
+ const nonNullDateRangeData = data
40
+ .filter((prevalenceOverTimeData) => prevalenceOverTimeData.content.length > 0)
41
+ .map((variantData) => {
42
+ return {
43
+ content: variantData.content.filter((dataPoint) => dataPoint.dateRange !== null),
44
+ displayName: variantData.displayName,
45
+ };
46
+ });
47
+
48
+ if (nonNullDateRangeData.length === 0) {
49
+ return NO_DATA;
50
+ }
51
+
52
+ const datasets = nonNullDateRangeData
53
+ .map((graphData, index) => getDataset(graphData, index, confidenceIntervalMethod))
54
+ .flat();
55
+
56
+ const maxY =
57
+ yAxisScaleType !== 'logit'
58
+ ? getYAxisMax(maxInData(nonNullDateRangeData), yAxisMaxConfig?.[yAxisScaleType])
59
+ : undefined;
41
60
 
42
- if (nonNullDateRangeData.length === 0) {
43
- return <NoDataDisplay />;
44
- }
45
-
46
- const datasets = nonNullDateRangeData
47
- .map((graphData, index) => getDataset(graphData, index, confidenceIntervalMethod))
48
- .flat();
49
-
50
- const maxY =
51
- yAxisScaleType !== 'logit'
52
- ? getYAxisMax(maxInData(nonNullDateRangeData), yAxisMaxConfig?.[yAxisScaleType])
53
- : undefined;
54
-
55
- const config: ChartConfiguration = {
56
- type: 'line',
57
- data: {
58
- datasets,
59
- },
60
- options: {
61
- animation: false,
62
- maintainAspectRatio: false,
63
- scales: {
64
- y: { ...getYAxisScale(yAxisScaleType), max: maxY },
61
+ return {
62
+ type: 'line',
63
+ data: {
64
+ datasets,
65
65
  },
66
- plugins: {
67
- legend: {
68
- display: false,
66
+ options: {
67
+ animation: false,
68
+ maintainAspectRatio,
69
+ scales: {
70
+ y: { ...getYAxisScale(yAxisScaleType), max: maxY },
71
+ },
72
+ plugins: {
73
+ legend: {
74
+ display: false,
75
+ },
76
+ tooltip: tooltip(confidenceIntervalMethod),
69
77
  },
70
- tooltip: tooltip(confidenceIntervalMethod),
71
78
  },
72
- },
73
- };
79
+ };
80
+ }, [data, yAxisScaleType, confidenceIntervalMethod, yAxisMaxConfig, maintainAspectRatio]);
81
+
82
+ if (config === NO_DATA) {
83
+ return <NoDataDisplay />;
84
+ }
74
85
 
75
86
  return <GsChart configuration={config} />;
76
87
  };
@@ -61,7 +61,6 @@ export const TwoVariants: StoryObj<PrevalenceOverTimeProps> = {
61
61
  views: ['bar', 'line', 'bubble', 'table'],
62
62
  confidenceIntervalMethods: ['none', 'wilson'],
63
63
  width: '100%',
64
- height: '700px',
65
64
  lapisDateField: 'date',
66
65
  pageSize: 10,
67
66
  yAxisMaxLinear: 1,
@@ -137,7 +136,6 @@ export const OneVariant: StoryObj<PrevalenceOverTimeProps> = {
137
136
  views: ['bar', 'line', 'bubble', 'table'],
138
137
  confidenceIntervalMethods: ['none', 'wilson'],
139
138
  width: '100%',
140
- height: '700px',
141
139
  lapisDateField: 'date',
142
140
  pageSize: 10,
143
141
  yAxisMaxLinear: 1,
@@ -197,7 +195,6 @@ export const ShowsNoDataBanner: StoryObj<PrevalenceOverTimeProps> = {
197
195
  views: ['bar', 'line', 'bubble', 'table'],
198
196
  confidenceIntervalMethods: ['none', 'wilson'],
199
197
  width: '100%',
200
- height: '700px',
201
198
  lapisDateField: 'date',
202
199
  pageSize: 10,
203
200
  yAxisMaxLinear: 1,