diff --git a/components/src/preact/aggregatedData/__mockData__/aggregatedWith1Field.json b/components/src/preact/aggregatedData/__mockData__/aggregatedWith1Field.json new file mode 100644 index 00000000..2c1e89c5 --- /dev/null +++ b/components/src/preact/aggregatedData/__mockData__/aggregatedWith1Field.json @@ -0,0 +1,399 @@ +{ + "data": [ + { + "count": 1793, + "division": "Virgin Islands" + }, + { + "count": 80581, + "division": "Colorado" + }, + { + "count": 5, + "division": "American Samoa" + }, + { + "count": 3462, + "division": "Northern Mariana Islands" + }, + { + "count": 29845, + "division": "Oklahoma" + }, + { + "count": 2582, + "division": "Wyoming" + }, + { + "count": 18733, + "division": "Kentucky" + }, + { + "count": 88, + "division": "New York City" + }, + { + "count": 30814, + "division": "Arkansas" + }, + { + "count": 8932, + "division": "Maine" + }, + { + "count": 10710, + "division": "Puerto Rico" + }, + { + "count": 11354, + "division": "Kansas" + }, + { + "count": 15336, + "division": "Alaska" + }, + { + "count": 1, + "division": "Sp" + }, + { + "count": 12017, + "division": "Washington DC" + }, + { + "count": 27043, + "division": "Vermont" + }, + { + "count": 30166, + "division": "Nevada" + }, + { + "count": 22443, + "division": "Utah" + }, + { + "count": 85817, + "division": "Pennsylvania" + }, + { + "count": 892, + "division": "Guam" + }, + { + "count": 2, + "division": "Virginia Beach" + }, + { + "count": 13808, + "division": "New Hampshire" + }, + { + "count": 9, + "division": "Fairfax" + }, + { + "count": 22914, + "division": "Rhode Island" + }, + { + "count": 78889, + "division": "Illinois" + }, + { + "count": 23674, + "division": "Nebraska" + }, + { + "count": 1, + "division": "Beltsville" + }, + { + "count": 141180, + "division": "Minnesota" + }, + { + "count": 35292, + "division": "Indiana" + }, + { + "count": 8949, + "division": "Montana" + }, + { + "count": 88026, + "division": "Virginia" + }, + { + "count": 199883, + "division": "Florida" + }, + { + "count": 9733, + "division": "Delaware" + }, + { + "count": 1, + "division": "Fishersville" + }, + { + "count": 22250, + "division": "Louisiana" + }, + { + "count": 6, + "division": "Springfield" + }, + { + "count": 2, + "division": "Afton" + }, + { + "count": 1, + "division": "Mt Solon" + }, + { + "count": 27784, + "division": "Iowa" + }, + { + "count": 30026, + "division": "Connecticut" + }, + { + "count": 9255, + "division": "South Dakota" + }, + { + "count": 14, + "division": "Annandale" + }, + { + "count": 2, + "division": "Burke" + }, + { + "count": 124361, + "division": "New York" + }, + { + "count": 36230, + "division": "West Virginia" + }, + { + "count": 11, + "division": "Falls Church" + }, + { + "count": 3, + "division": "Arlington" + }, + { + "count": 3, + "division": "Louisana" + }, + { + "count": 13, + "division": "Temple" + }, + { + "count": 10, + "division": "Louisiana/Caddo Parish" + }, + { + "count": 6, + "division": "Alexandria" + }, + { + "count": 1, + "division": "Louisiana/Bossier Parish" + }, + { + "count": 45448, + "division": "Wisconsin" + }, + { + "count": 2, + "division": "Un" + }, + { + "count": 3, + "division": "Mclean" + }, + { + "count": 6192, + "division": "Hawaii" + }, + { + "count": 142321, + "division": "Washington" + }, + { + "count": 1, + "division": "Chantilly" + }, + { + "count": 2, + "division": "Crozet" + }, + { + "count": 51468, + "division": "Tennessee" + }, + { + "count": 1, + "division": "Chilhowie" + }, + { + "count": 10, + "division": "Saipan" + }, + { + "count": 176243, + "division": "Massachusetts" + }, + { + "count": 1, + "division": "Fairfax Station" + }, + { + "count": 1, + "division": "Paeonian Springs" + }, + { + "count": 7, + "division": "Waynesboro" + }, + { + "count": 765924, + "division": "USA" + }, + { + "count": 5, + "division": "Louisiana/Webster Parish" + }, + { + "count": 1, + "division": "Harrisonburg" + }, + { + "count": 1, + "division": "Stafford" + }, + { + "count": 1, + "division": "Grottoes" + }, + { + "count": 144151, + "division": "Texas" + }, + { + "count": 92995, + "division": "North Carolina" + }, + { + "count": 1, + "division": "Severn" + }, + { + "count": 1, + "division": "Great Falls" + }, + { + "count": 1, + "division": "Temple Hills" + }, + { + "count": 98998, + "division": "Michigan" + }, + { + "count": 1, + "division": "Chesapeake" + }, + { + "count": 386575, + "division": "California" + }, + { + "count": 1, + "division": "Yap" + }, + { + "count": 12380, + "division": "Mississippi" + }, + { + "count": 17575, + "division": "Alabama" + }, + { + "count": 48541, + "division": "Maryland" + }, + { + "count": 1, + "division": "North America" + }, + { + "count": 16034, + "division": "Idaho" + }, + { + "count": 50896, + "division": "Ohio" + }, + { + "count": 75921, + "division": "Arizona" + }, + { + "count": 2394, + "division": "North Dakota" + }, + { + "count": 2, + "division": "Deleware" + }, + { + "count": 47723, + "division": "New Mexico" + }, + { + "count": 35425, + "division": "South Carolina" + }, + { + "count": 26192, + "division": "Missouri" + }, + { + "count": 5, + "division": "Vienna" + }, + { + "count": 1, + "division": "Woodbridge" + }, + { + "count": 104569, + "division": "New Jersey" + }, + { + "count": 70288, + "division": "Georgia" + }, + { + "count": 27269, + "division": "Oregon" + } + ], + "info": { + "dataVersion": "1736095391", + "requestId": "dd98582e-c486-4a0f-9341-d9c4ce78bc9d", + "requestInfo": "sars_cov-2_nextstrain_open on lapis.cov-spectrum.org at 2025-01-08T10:56:47.562437009", + "reportTo": "Please report to https://github.com/GenSpectrum/LAPIS/issues in case you encounter any unexpected issues. Please include the request ID and the requestInfo in your report.", + "lapisVersion": "0.3.10" + } +} diff --git a/components/src/preact/aggregatedData/__mockData__/aggregatedWith2Fields.json b/components/src/preact/aggregatedData/__mockData__/aggregatedWith2Fields.json new file mode 100644 index 00000000..95210c50 --- /dev/null +++ b/components/src/preact/aggregatedData/__mockData__/aggregatedWith2Fields.json @@ -0,0 +1,1771 @@ +{ + "data": [ + { + "count": 549, + "division": "Berlin", + "nextstrainClade": "21I" + }, + { + "count": 460, + "division": null, + "nextstrainClade": "21D" + }, + { + "count": 4, + "division": "Saxony-Anhalt", + "nextstrainClade": "21B" + }, + { + "count": 617, + "division": "Saxony", + "nextstrainClade": "21I" + }, + { + "count": 8, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "21F" + }, + { + "count": 2, + "division": "Bavaria", + "nextstrainClade": "21G" + }, + { + "count": 8361, + "division": "Hesse", + "nextstrainClade": "21J" + }, + { + "count": 176, + "division": "Saxony", + "nextstrainClade": "21A" + }, + { + "count": 16, + "division": "Thuringia", + "nextstrainClade": "21A" + }, + { + "count": 10, + "division": "North Rhine Westphalia", + "nextstrainClade": "21B" + }, + { + "count": 2, + "division": "Saarland", + "nextstrainClade": "21D" + }, + { + "count": 7, + "division": "Berlin", + "nextstrainClade": "21G" + }, + { + "count": 3, + "division": "Hesse", + "nextstrainClade": "21B" + }, + { + "count": 721, + "division": "Germany", + "nextstrainClade": "21J" + }, + { + "count": 320, + "division": "Schleswig-Holstein", + "nextstrainClade": "20I" + }, + { + "count": 44, + "division": "Germany", + "nextstrainClade": "21I" + }, + { + "count": 60, + "division": "Rheinland-Pfalz", + "nextstrainClade": "21D" + }, + { + "count": 42566, + "division": null, + "nextstrainClade": "21K" + }, + { + "count": 14, + "division": "Rheinland-Pfalz", + "nextstrainClade": "21M" + }, + { + "count": 2, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "19B" + }, + { + "count": 19, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "21A" + }, + { + "count": 40951, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "21J" + }, + { + "count": 16, + "division": "Saarland", + "nextstrainClade": "21L" + }, + { + "count": 398, + "division": "Hesse", + "nextstrainClade": "21I" + }, + { + "count": 4, + "division": "Schleswig-Holstein", + "nextstrainClade": "20H" + }, + { + "count": 5, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "21D" + }, + { + "count": 5042, + "division": null, + "nextstrainClade": "20E" + }, + { + "count": 422, + "division": "Hamburg", + "nextstrainClade": "20E" + }, + { + "count": 11, + "division": "Bremen", + "nextstrainClade": "21B" + }, + { + "count": 4, + "division": "Germany", + "nextstrainClade": "21F" + }, + { + "count": 27, + "division": "Germany", + "nextstrainClade": "21A" + }, + { + "count": 41, + "division": "North Rhine Westphalia", + "nextstrainClade": "20C" + }, + { + "count": 5, + "division": "Hesse", + "nextstrainClade": "21G" + }, + { + "count": 33, + "division": "Hesse", + "nextstrainClade": "20J" + }, + { + "count": 136, + "division": "Hesse", + "nextstrainClade": "20E" + }, + { + "count": 406, + "division": "Bavaria", + "nextstrainClade": "20E" + }, + { + "count": 22, + "division": "Saxony-Anhalt", + "nextstrainClade": "20H" + }, + { + "count": 1, + "division": "Saxony", + "nextstrainClade": "21F" + }, + { + "count": 13, + "division": "Bavaria", + "nextstrainClade": "21B" + }, + { + "count": 99, + "division": "Rheinland-Pfalz", + "nextstrainClade": "20J" + }, + { + "count": 97, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "20A" + }, + { + "count": 1, + "division": "Germany", + "nextstrainClade": "19A" + }, + { + "count": 224, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "20H" + }, + { + "count": 55, + "division": "Hamburg", + "nextstrainClade": "21A" + }, + { + "count": 1, + "division": "Rheinland-Pfalz", + "nextstrainClade": "19A" + }, + { + "count": 7542, + "division": null, + "nextstrainClade": "21I" + }, + { + "count": 3, + "division": "North Rhine Westphalia", + "nextstrainClade": "21F" + }, + { + "count": 1, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "20G" + }, + { + "count": 5, + "division": "Germany", + "nextstrainClade": "21E" + }, + { + "count": 1, + "division": "Hamburg", + "nextstrainClade": "21G" + }, + { + "count": 13, + "division": "Germany", + "nextstrainClade": "21B" + }, + { + "count": 4, + "division": "Germany", + "nextstrainClade": "19B" + }, + { + "count": 120, + "division": "Germany", + "nextstrainClade": "21D" + }, + { + "count": 3, + "division": "Hamburg", + "nextstrainClade": "21B" + }, + { + "count": 2, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "recombinant" + }, + { + "count": 2, + "division": "Hamburg", + "nextstrainClade": "20G" + }, + { + "count": 15, + "division": "Hamburg", + "nextstrainClade": "20D" + }, + { + "count": 56, + "division": null, + "nextstrainClade": "21B" + }, + { + "count": 133, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "20J" + }, + { + "count": 91, + "division": "Schleswig-Holstein", + "nextstrainClade": "20E" + }, + { + "count": 7167, + "division": "Berlin", + "nextstrainClade": "21J" + }, + { + "count": 56, + "division": "Germany", + "nextstrainClade": "20J" + }, + { + "count": 18, + "division": "Bremen", + "nextstrainClade": "20H" + }, + { + "count": 7, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "20D" + }, + { + "count": 5, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "21B" + }, + { + "count": 7, + "division": null, + "nextstrainClade": "21F" + }, + { + "count": 1338, + "division": "Bavaria", + "nextstrainClade": "21I" + }, + { + "count": 3, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "21C" + }, + { + "count": 224, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "20J" + }, + { + "count": 36, + "division": "Niedersachsen", + "nextstrainClade": "21A" + }, + { + "count": 8002, + "division": "Saxony", + "nextstrainClade": "21K" + }, + { + "count": 51, + "division": "Saxony", + "nextstrainClade": "20J" + }, + { + "count": 11, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "20C" + }, + { + "count": 12, + "division": "Saxony", + "nextstrainClade": "20C" + }, + { + "count": 64, + "division": "Rheinland-Pfalz", + "nextstrainClade": "20B" + }, + { + "count": 4, + "division": "Schleswig-Holstein", + "nextstrainClade": "21M" + }, + { + "count": 3, + "division": "Saxony", + "nextstrainClade": "21B" + }, + { + "count": 13, + "division": "Saxony", + "nextstrainClade": "21G" + }, + { + "count": 118, + "division": "Berlin", + "nextstrainClade": "21D" + }, + { + "count": 3, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "21E" + }, + { + "count": 44, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "21D" + }, + { + "count": 499, + "division": null, + "nextstrainClade": "20D" + }, + { + "count": 57, + "division": "North Rhine Westphalia", + "nextstrainClade": "21G" + }, + { + "count": 1, + "division": "Thuringia", + "nextstrainClade": "21F" + }, + { + "count": 69, + "division": "Bavaria", + "nextstrainClade": "21D" + }, + { + "count": 92, + "division": "North Rhine Westphalia", + "nextstrainClade": "20J" + }, + { + "count": 18, + "division": "Brandenburg", + "nextstrainClade": "21D" + }, + { + "count": 3, + "division": "Hamburg", + "nextstrainClade": "19B" + }, + { + "count": 565, + "division": "Saxony", + "nextstrainClade": "20B" + }, + { + "count": 1211, + "division": "Bavaria", + "nextstrainClade": "20A" + }, + { + "count": 66, + "division": "Niedersachsen", + "nextstrainClade": "21D" + }, + { + "count": 3034, + "division": "North Rhine Westphalia", + "nextstrainClade": "21I" + }, + { + "count": 814, + "division": "Germany", + "nextstrainClade": "20A" + }, + { + "count": 1, + "division": "Rheinland-Pfalz", + "nextstrainClade": "19B" + }, + { + "count": 7, + "division": "Niedersachsen", + "nextstrainClade": "20J" + }, + { + "count": 164, + "division": "Rheinland-Pfalz", + "nextstrainClade": "20H" + }, + { + "count": 1191, + "division": "Saxony", + "nextstrainClade": "20E" + }, + { + "count": 7406, + "division": "Bavaria", + "nextstrainClade": "20I" + }, + { + "count": 10, + "division": "Saarland", + "nextstrainClade": "21A" + }, + { + "count": 2607, + "division": "Niedersachsen", + "nextstrainClade": "20I" + }, + { + "count": 6, + "division": "Bremen", + "nextstrainClade": "19B" + }, + { + "count": 50, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "20H" + }, + { + "count": 2, + "division": "Rheinland-Pfalz", + "nextstrainClade": "21B" + }, + { + "count": 503, + "division": "Germany", + "nextstrainClade": "20E" + }, + { + "count": 2, + "division": "Niedersachsen", + "nextstrainClade": "19A" + }, + { + "count": 21854, + "division": "Bavaria", + "nextstrainClade": "21J" + }, + { + "count": 2, + "division": "Germany", + "nextstrainClade": "21G" + }, + { + "count": 157, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "20D" + }, + { + "count": 122494, + "division": null, + "nextstrainClade": "21J" + }, + { + "count": 8, + "division": "Hesse", + "nextstrainClade": "21D" + }, + { + "count": 8, + "division": null, + "nextstrainClade": "21C" + }, + { + "count": 10, + "division": "Brandenburg", + "nextstrainClade": "21A" + }, + { + "count": 12, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "20G" + }, + { + "count": 187, + "division": "North Rhine Westphalia", + "nextstrainClade": "20D" + }, + { + "count": 72, + "division": null, + "nextstrainClade": "19A" + }, + { + "count": 3188, + "division": "Berlin", + "nextstrainClade": "20I" + }, + { + "count": 32, + "division": "Saarland", + "nextstrainClade": "20E" + }, + { + "count": 180, + "division": "Bremen", + "nextstrainClade": "20A" + }, + { + "count": 26, + "division": "Berlin", + "nextstrainClade": "20D" + }, + { + "count": 19, + "division": "Lower Saxony", + "nextstrainClade": "20A" + }, + { + "count": 212, + "division": "Saxony-Anhalt", + "nextstrainClade": "20E" + }, + { + "count": 8, + "division": "Hamburg", + "nextstrainClade": "20J" + }, + { + "count": 2326, + "division": "Hamburg", + "nextstrainClade": "20I" + }, + { + "count": 24, + "division": "Bavaria", + "nextstrainClade": "20C" + }, + { + "count": 158, + "division": "North Rhine Westphalia", + "nextstrainClade": "21D" + }, + { + "count": 1756, + "division": "Bremen", + "nextstrainClade": "20I" + }, + { + "count": 130, + "division": "Brandenburg", + "nextstrainClade": "21L" + }, + { + "count": 1, + "division": "Saxony", + "nextstrainClade": "19B" + }, + { + "count": 32, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "19A" + }, + { + "count": 6, + "division": "Rheinland-Pfalz", + "nextstrainClade": "20C" + }, + { + "count": 173, + "division": "Rheinland-Pfalz", + "nextstrainClade": "20A" + }, + { + "count": 2, + "division": "Hesse", + "nextstrainClade": "19A" + }, + { + "count": 13, + "division": "Saarland", + "nextstrainClade": "20B" + }, + { + "count": 5, + "division": "Thuringia", + "nextstrainClade": "20G" + }, + { + "count": 4, + "division": "Hamburg", + "nextstrainClade": "21M" + }, + { + "count": 49, + "division": "Niedersachsen", + "nextstrainClade": "20H" + }, + { + "count": 2037, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "21I" + }, + { + "count": 440, + "division": "Schleswig-Holstein", + "nextstrainClade": "21L" + }, + { + "count": 617, + "division": "Saxony", + "nextstrainClade": "21L" + }, + { + "count": 20, + "division": "Thuringia", + "nextstrainClade": "20H" + }, + { + "count": 2, + "division": "Saxony-Anhalt", + "nextstrainClade": "20J" + }, + { + "count": 286, + "division": "Niedersachsen", + "nextstrainClade": "20A" + }, + { + "count": 7, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "21M" + }, + { + "count": 4058, + "division": null, + "nextstrainClade": "20B" + }, + { + "count": 118, + "division": "Saarland", + "nextstrainClade": "20A" + }, + { + "count": 7, + "division": "Bavaria", + "nextstrainClade": "recombinant" + }, + { + "count": 48, + "division": "Bremen", + "nextstrainClade": "21I" + }, + { + "count": 6, + "division": "Niedersachsen", + "nextstrainClade": "21G" + }, + { + "count": 2, + "division": "Hesse", + "nextstrainClade": "recombinant" + }, + { + "count": 1056, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "21L" + }, + { + "count": 419, + "division": "Berlin", + "nextstrainClade": "21L" + }, + { + "count": 2, + "division": "Hamburg", + "nextstrainClade": "21F" + }, + { + "count": 10824, + "division": "Schleswig-Holstein", + "nextstrainClade": "21J" + }, + { + "count": 164, + "division": "Hesse", + "nextstrainClade": "21L" + }, + { + "count": 732, + "division": "Bavaria", + "nextstrainClade": "21L" + }, + { + "count": 129, + "division": "Saxony-Anhalt", + "nextstrainClade": "20A" + }, + { + "count": 11, + "division": "Saarland", + "nextstrainClade": "20J" + }, + { + "count": 216, + "division": "Hamburg", + "nextstrainClade": "21L" + }, + { + "count": 14, + "division": "Bavaria", + "nextstrainClade": "21M" + }, + { + "count": 1, + "division": "Saxony-Anhalt", + "nextstrainClade": "21M" + }, + { + "count": 219, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "21I" + }, + { + "count": 14, + "division": "Berlin", + "nextstrainClade": "21B" + }, + { + "count": 1297, + "division": "Brandenburg", + "nextstrainClade": "21K" + }, + { + "count": 103, + "division": null, + "nextstrainClade": "21M" + }, + { + "count": 1, + "division": "Schleswig-Holstein", + "nextstrainClade": "recombinant" + }, + { + "count": 720, + "division": "Saarland", + "nextstrainClade": "21K" + }, + { + "count": 1, + "division": "Hamburg", + "nextstrainClade": "20C" + }, + { + "count": 58, + "division": "Saxony-Anhalt", + "nextstrainClade": "20B" + }, + { + "count": 74, + "division": "Schleswig-Holstein", + "nextstrainClade": "21A" + }, + { + "count": 2, + "division": "Germany", + "nextstrainClade": "21K" + }, + { + "count": 551, + "division": "North Rhine Westphalia", + "nextstrainClade": "21L" + }, + { + "count": 179, + "division": "Niedersachsen", + "nextstrainClade": "21L" + }, + { + "count": 734, + "division": "Rheinland-Pfalz", + "nextstrainClade": "21L" + }, + { + "count": 85, + "division": "Hesse", + "nextstrainClade": "21A" + }, + { + "count": 64, + "division": "Saxony-Anhalt", + "nextstrainClade": "21L" + }, + { + "count": 11, + "division": "Saxony", + "nextstrainClade": "recombinant" + }, + { + "count": 303, + "division": "Niedersachsen", + "nextstrainClade": "21I" + }, + { + "count": 1103, + "division": "Saxony-Anhalt", + "nextstrainClade": "21K" + }, + { + "count": 1, + "division": "Berlin", + "nextstrainClade": "21M" + }, + { + "count": 42, + "division": "Hesse", + "nextstrainClade": "20H" + }, + { + "count": 1, + "division": "Hesse", + "nextstrainClade": "20G" + }, + { + "count": 2, + "division": "Hesse", + "nextstrainClade": "21C" + }, + { + "count": 335, + "division": "Germany", + "nextstrainClade": "20H" + }, + { + "count": 1, + "division": "Hesse", + "nextstrainClade": "21E" + }, + { + "count": 2, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "recombinant" + }, + { + "count": 9, + "division": "Brandenburg", + "nextstrainClade": "20D" + }, + { + "count": 4, + "division": "North Rhine Westphalia", + "nextstrainClade": "recombinant" + }, + { + "count": 1, + "division": "Bremen", + "nextstrainClade": "21L" + }, + { + "count": 1, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "21M" + }, + { + "count": 40, + "division": "Thuringia", + "nextstrainClade": "21L" + }, + { + "count": 1, + "division": "Saxony-Anhalt", + "nextstrainClade": "20G" + }, + { + "count": 14, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "19B" + }, + { + "count": 2, + "division": "Germany", + "nextstrainClade": "21H" + }, + { + "count": 26, + "division": "Saxony", + "nextstrainClade": "20G" + }, + { + "count": 6217, + "division": "Bavaria", + "nextstrainClade": "21K" + }, + { + "count": 412, + "division": "Germany", + "nextstrainClade": "20B" + }, + { + "count": 2782, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "21J" + }, + { + "count": 1, + "division": "Schleswig-Holstein", + "nextstrainClade": "21F" + }, + { + "count": 109, + "division": null, + "nextstrainClade": "20G" + }, + { + "count": 2, + "division": "Thuringia", + "nextstrainClade": "21M" + }, + { + "count": 92, + "division": "Saxony-Anhalt", + "nextstrainClade": "21I" + }, + { + "count": 435, + "division": "Bremen", + "nextstrainClade": "21K" + }, + { + "count": 270, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "20E" + }, + { + "count": 10, + "division": "Berlin", + "nextstrainClade": "recombinant" + }, + { + "count": 23, + "division": "Niedersachsen", + "nextstrainClade": "20D" + }, + { + "count": 72, + "division": "Germany", + "nextstrainClade": "20D" + }, + { + "count": 1, + "division": "Schleswig-Holstein", + "nextstrainClade": "21D" + }, + { + "count": 2, + "division": "Saxony-Anhalt", + "nextstrainClade": "21D" + }, + { + "count": 15, + "division": "Schleswig-Holstein", + "nextstrainClade": "20J" + }, + { + "count": 87, + "division": "Berlin", + "nextstrainClade": "20B" + }, + { + "count": 173, + "division": "Thuringia", + "nextstrainClade": "21I" + }, + { + "count": 40, + "division": "Saxony", + "nextstrainClade": "21M" + }, + { + "count": 1, + "division": "Bavaria", + "nextstrainClade": "21C" + }, + { + "count": 1, + "division": "Thuringia", + "nextstrainClade": "20C" + }, + { + "count": 1, + "division": "Niedersachsen", + "nextstrainClade": null + }, + { + "count": 196, + "division": null, + "nextstrainClade": "21H" + }, + { + "count": 699, + "division": "Thuringia", + "nextstrainClade": "21K" + }, + { + "count": 79, + "division": "Rheinland-Pfalz", + "nextstrainClade": "21A" + }, + { + "count": 70, + "division": "Hesse", + "nextstrainClade": "20B" + }, + { + "count": 742, + "division": "Saarland", + "nextstrainClade": "20I" + }, + { + "count": 2709, + "division": "Brandenburg", + "nextstrainClade": "21J" + }, + { + "count": 3194, + "division": "Schleswig-Holstein", + "nextstrainClade": "21K" + }, + { + "count": 171, + "division": "Berlin", + "nextstrainClade": "20A" + }, + { + "count": 3891, + "division": "Rheinland-Pfalz", + "nextstrainClade": "21K" + }, + { + "count": 1927, + "division": "Niedersachsen", + "nextstrainClade": "21K" + }, + { + "count": 298, + "division": "Hamburg", + "nextstrainClade": "21I" + }, + { + "count": 51, + "division": "Bremen", + "nextstrainClade": "20G" + }, + { + "count": 2, + "division": "Thuringia", + "nextstrainClade": "20J" + }, + { + "count": 3836, + "division": "Thuringia", + "nextstrainClade": "21J" + }, + { + "count": 100, + "division": "Saxony", + "nextstrainClade": "20H" + }, + { + "count": 45, + "division": "Rheinland-Pfalz", + "nextstrainClade": "20D" + }, + { + "count": 50, + "division": "Brandenburg", + "nextstrainClade": "21I" + }, + { + "count": 531, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "21K" + }, + { + "count": 4189, + "division": "Saxony-Anhalt", + "nextstrainClade": "21J" + }, + { + "count": 4761, + "division": "Rheinland-Pfalz", + "nextstrainClade": "21J" + }, + { + "count": 115, + "division": "Saarland", + "nextstrainClade": "21I" + }, + { + "count": 8, + "division": "Saxony-Anhalt", + "nextstrainClade": "21A" + }, + { + "count": 25767, + "division": "North Rhine Westphalia", + "nextstrainClade": "20I" + }, + { + "count": 10295, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "20I" + }, + { + "count": 11, + "division": "Niedersachsen", + "nextstrainClade": "20G" + }, + { + "count": 3, + "division": "Bremen", + "nextstrainClade": "20J" + }, + { + "count": 17705, + "division": "Germany", + "nextstrainClade": "20I" + }, + { + "count": 90, + "division": "Brandenburg", + "nextstrainClade": "20B" + }, + { + "count": 5506, + "division": "Rheinland-Pfalz", + "nextstrainClade": "20I" + }, + { + "count": 1, + "division": "Hamburg", + "nextstrainClade": "21C" + }, + { + "count": 731, + "division": "Schleswig-Holstein", + "nextstrainClade": "21I" + }, + { + "count": 2573, + "division": "Hesse", + "nextstrainClade": "21K" + }, + { + "count": 57, + "division": "Schleswig-Holstein", + "nextstrainClade": "20A" + }, + { + "count": 83, + "division": "Berlin", + "nextstrainClade": "21A" + }, + { + "count": 73, + "division": null, + "nextstrainClade": "21G" + }, + { + "count": 12, + "division": "North Rhine Westphalia", + "nextstrainClade": "19B" + }, + { + "count": 140, + "division": "Hesse", + "nextstrainClade": "20A" + }, + { + "count": 2, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "20C" + }, + { + "count": 484, + "division": "Berlin", + "nextstrainClade": "20E" + }, + { + "count": 2, + "division": "Rheinland-Pfalz", + "nextstrainClade": "recombinant" + }, + { + "count": 162, + "division": "Brandenburg", + "nextstrainClade": "20A" + }, + { + "count": 27, + "division": "Brandenburg", + "nextstrainClade": "20H" + }, + { + "count": 430, + "division": "North Rhine Westphalia", + "nextstrainClade": "21A" + }, + { + "count": 5790, + "division": "Niedersachsen", + "nextstrainClade": "21J" + }, + { + "count": 4231, + "division": "Hamburg", + "nextstrainClade": "21K" + }, + { + "count": 146, + "division": null, + "nextstrainClade": "20C" + }, + { + "count": 14148, + "division": "North Rhine Westphalia", + "nextstrainClade": "21K" + }, + { + "count": 1583, + "division": "North Rhine Westphalia", + "nextstrainClade": "20E" + }, + { + "count": 1323, + "division": "Bremen", + "nextstrainClade": "21J" + }, + { + "count": 71, + "division": "Saxony", + "nextstrainClade": "20D" + }, + { + "count": 3141, + "division": "Thuringia", + "nextstrainClade": "20I" + }, + { + "count": 1209, + "division": null, + "nextstrainClade": "21A" + }, + { + "count": 420, + "division": "Niedersachsen", + "nextstrainClade": "20E" + }, + { + "count": 2819, + "division": null, + "nextstrainClade": "21L" + }, + { + "count": 1, + "division": "Hamburg", + "nextstrainClade": "21E" + }, + { + "count": 9080, + "division": "Saxony", + "nextstrainClade": "20I" + }, + { + "count": 10, + "division": "Schleswig-Holstein", + "nextstrainClade": "20D" + }, + { + "count": 409, + "division": "Thuringia", + "nextstrainClade": "20A" + }, + { + "count": 16883, + "division": "Saxony", + "nextstrainClade": "21J" + }, + { + "count": 12, + "division": "Bavaria", + "nextstrainClade": "21F" + }, + { + "count": 29, + "division": "Bremen", + "nextstrainClade": "21A" + }, + { + "count": 1, + "division": "Bavaria", + "nextstrainClade": "21E" + }, + { + "count": 17, + "division": "Germany", + "nextstrainClade": "20C" + }, + { + "count": 1415, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "20A" + }, + { + "count": 141, + "division": "Niedersachsen", + "nextstrainClade": "20B" + }, + { + "count": 471, + "division": null, + "nextstrainClade": "20J" + }, + { + "count": 984, + "division": null, + "nextstrainClade": "19B" + }, + { + "count": 1, + "division": "Saarland", + "nextstrainClade": "20C" + }, + { + "count": 98, + "division": "Hesse", + "nextstrainClade": "20D" + }, + { + "count": 338, + "division": "Bavaria", + "nextstrainClade": "21A" + }, + { + "count": 44, + "division": "Schleswig-Holstein", + "nextstrainClade": "20B" + }, + { + "count": 67, + "division": "Bavaria", + "nextstrainClade": "20D" + }, + { + "count": 91, + "division": "Hamburg", + "nextstrainClade": "20B" + }, + { + "count": 5, + "division": "Saxony-Anhalt", + "nextstrainClade": "20D" + }, + { + "count": 20, + "division": "Berlin", + "nextstrainClade": "20J" + }, + { + "count": 4, + "division": "Bremen", + "nextstrainClade": "20D" + }, + { + "count": 80, + "division": "Berlin", + "nextstrainClade": "20H" + }, + { + "count": 5, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "21G" + }, + { + "count": 92, + "division": "Bremen", + "nextstrainClade": "20B" + }, + { + "count": 807, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "20B" + }, + { + "count": 51, + "division": "North Rhine Westphalia", + "nextstrainClade": "21M" + }, + { + "count": 54816, + "division": null, + "nextstrainClade": "20I" + }, + { + "count": 1295, + "division": "Brandenburg", + "nextstrainClade": "20I" + }, + { + "count": 721, + "division": "Saxony-Anhalt", + "nextstrainClade": "20I" + }, + { + "count": 1761, + "division": "North Rhine Westphalia", + "nextstrainClade": "20A" + }, + { + "count": 74, + "division": "Bavaria", + "nextstrainClade": "19A" + }, + { + "count": 2, + "division": "Thuringia", + "nextstrainClade": "21D" + }, + { + "count": 762, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "20E" + }, + { + "count": 48579, + "division": "North Rhine Westphalia", + "nextstrainClade": "21J" + }, + { + "count": 943, + "division": "Bavaria", + "nextstrainClade": "20B" + }, + { + "count": 141, + "division": "Rheinland-Pfalz", + "nextstrainClade": "20E" + }, + { + "count": 28, + "division": "Hamburg", + "nextstrainClade": "20H" + }, + { + "count": 2, + "division": "Hesse", + "nextstrainClade": "19B" + }, + { + "count": 26, + "division": null, + "nextstrainClade": null + }, + { + "count": 3, + "division": "Hesse", + "nextstrainClade": "21M" + }, + { + "count": 22, + "division": "North Rhine Westphalia", + "nextstrainClade": "19A" + }, + { + "count": 5336, + "division": null, + "nextstrainClade": "20A" + }, + { + "count": 469, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "21A" + }, + { + "count": 14, + "division": "North Rhine Westphalia", + "nextstrainClade": "20G" + }, + { + "count": 56, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "20B" + }, + { + "count": 1309, + "division": "Berlin", + "nextstrainClade": "21K" + }, + { + "count": 1879, + "division": "Saarland", + "nextstrainClade": "21J" + }, + { + "count": 1, + "division": "Thuringia", + "nextstrainClade": "21C" + }, + { + "count": 2, + "division": "Niedersachsen", + "nextstrainClade": "19B" + }, + { + "count": 8, + "division": "North Rhine Westphalia", + "nextstrainClade": "21H" + }, + { + "count": 337, + "division": "North Rhine Westphalia", + "nextstrainClade": "20H" + }, + { + "count": 1, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "21H" + }, + { + "count": 1008, + "division": "North Rhine Westphalia", + "nextstrainClade": "20B" + }, + { + "count": 214, + "division": "Thuringia", + "nextstrainClade": "20B" + }, + { + "count": 298, + "division": "Brandenburg", + "nextstrainClade": "20E" + }, + { + "count": 5224, + "division": "Hamburg", + "nextstrainClade": "21J" + }, + { + "count": 1576, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "20I" + }, + { + "count": 1411, + "division": "Saxony", + "nextstrainClade": "20A" + }, + { + "count": 593, + "division": "Thuringia", + "nextstrainClade": "20E" + }, + { + "count": 21, + "division": null, + "nextstrainClade": "recombinant" + }, + { + "count": 1, + "division": "Rheinland-Pfalz", + "nextstrainClade": "20G" + }, + { + "count": 12859, + "division": "Baden-Wuerttemberg", + "nextstrainClade": "21K" + }, + { + "count": 341, + "division": "Bremen", + "nextstrainClade": "20E" + }, + { + "count": 10, + "division": "Hesse", + "nextstrainClade": "20C" + }, + { + "count": 1370, + "division": null, + "nextstrainClade": "20H" + }, + { + "count": 18, + "division": null, + "nextstrainClade": "21E" + }, + { + "count": 19, + "division": "Thuringia", + "nextstrainClade": "20D" + }, + { + "count": 5, + "division": "Rheinland-Pfalz", + "nextstrainClade": "21F" + }, + { + "count": 70, + "division": "Mecklenburg-Vorpommern", + "nextstrainClade": "21L" + }, + { + "count": 186, + "division": "Hamburg", + "nextstrainClade": "20A" + }, + { + "count": 71, + "division": "Saarland", + "nextstrainClade": "20H" + }, + { + "count": 163, + "division": "Bavaria", + "nextstrainClade": "20J" + }, + { + "count": 16, + "division": "Hamburg", + "nextstrainClade": "21D" + }, + { + "count": 1, + "division": "Brandenburg", + "nextstrainClade": "20C" + }, + { + "count": 4, + "division": "Bavaria", + "nextstrainClade": "20G" + }, + { + "count": 2902, + "division": "Hesse", + "nextstrainClade": "20I" + }, + { + "count": 4, + "division": "Bremen", + "nextstrainClade": "21D" + }, + { + "count": 26, + "division": "Saxony", + "nextstrainClade": "21D" + }, + { + "count": 3, + "division": "Berlin", + "nextstrainClade": "20C" + }, + { + "count": 3, + "division": "Bavaria", + "nextstrainClade": "19B" + }, + { + "count": 388, + "division": "Rheinland-Pfalz", + "nextstrainClade": "21I" + }, + { + "count": 516, + "division": "Bavaria", + "nextstrainClade": "20H" + } + ], + "info": { + "dataVersion": "1736095391", + "requestId": "e56e0f4b-b981-49b6-9779-7b5bd9b74dd4", + "requestInfo": "sars_cov-2_nextstrain_open on lapis.cov-spectrum.org at 2025-01-08T10:54:50.257111317", + "reportTo": "Please report to https://github.com/GenSpectrum/LAPIS/issues in case you encounter any unexpected issues. Please include the request ID and the requestInfo in your report.", + "lapisVersion": "0.3.10" + } +} diff --git a/components/src/preact/aggregatedData/aggregate-bar-chart.tsx b/components/src/preact/aggregatedData/aggregate-bar-chart.tsx new file mode 100644 index 00000000..6bb9b471 --- /dev/null +++ b/components/src/preact/aggregatedData/aggregate-bar-chart.tsx @@ -0,0 +1,166 @@ +import { BarController, Chart, type ChartConfiguration, type ChartDataset, registerables } from 'chart.js'; +import { type FunctionComponent } from 'preact'; +import { useMemo } from 'preact/hooks'; + +import type { AggregateData } from '../../query/queryAggregateData'; +import GsChart from '../components/chart'; +import { NoDataDisplay } from '../components/no-data-display'; +import { singleGraphColorRGBAById } from '../shared/charts/colors'; +import { formatProportion } from '../shared/table/formatProportion'; + +interface AggregateBarChartProps { + data: AggregateData; + fields: string[]; + maxNumberOfBars: number; +} + +Chart.register(...registerables, BarController); + +type DataPoint = { + y: string; + x: number; + proportion: number; +}; + +export const AggregateBarChart: FunctionComponent = ({ data, fields, maxNumberOfBars }) => { + if (data.length === 0) { + return ; + } + + if (fields.length === 0) { + return ( + + ); + } + + if (fields.length > 2) { + return ( + + ); + } + + return ; +}; + +const AggregateBarChartInner: FunctionComponent = ({ data, fields, maxNumberOfBars }) => { + const config = useMemo( + (): ChartConfiguration<'bar', DataPoint[]> => ({ + type: 'bar', + data: { + datasets: getDatasets(fields, maxNumberOfBars, data), + }, + options: { + maintainAspectRatio: false, + animation: false, + indexAxis: 'y', + scales: { + x: { + stacked: true, + }, + y: { + stacked: true, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + mode: 'y', + callbacks: { + label: (context) => { + const { x, proportion } = context.dataset.data[ + context.dataIndex + ] as unknown as DataPoint; + return fields.length === 1 + ? `${x} (${formatProportion(proportion)}})` + : `${context.dataset.label}: ${x} (${formatProportion(proportion)}})`; + }, + }, + }, + }, + }, + }), + [data, fields, maxNumberOfBars], + ); + + return ; +}; + +function getDatasets( + fields: string[], + maxNumberOfBars: number, + data: (Record & { + count: number; + proportion: number; + })[], +): ChartDataset<'bar', DataPoint[]>[] { + const sortedData = data.sort((a, b) => b.count - a.count); + + if (fields.length === 1) { + return [ + { + borderWidth: 1, + backgroundColor: singleGraphColorRGBAById(0, 0.3), + borderColor: singleGraphColorRGBAById(0), + data: sortedData.slice(0, maxNumberOfBars).map((row) => ({ + y: row[fields[0]] as string, + x: row.count, + proportion: row.proportion, + })), + }, + ]; + } + + const map = new Map(); + const countsOfEachBar = new Map(); + + for (const row of sortedData) { + const yValue = row[fields[0]]; + const secondaryValue = row[fields[1]]; + if (yValue === null || secondaryValue === null) { + continue; + } + const yAxisKey = String(yValue); + const secondaryKey = String(secondaryValue); + + if (!map.has(secondaryKey)) { + map.set(secondaryKey, []); + } + map.get(secondaryKey)?.push({ + y: yAxisKey.toString(), + x: row.count, + proportion: row.proportion, + }); + countsOfEachBar.set(yAxisKey, (countsOfEachBar.get(yAxisKey) ?? 0) + row.count); + } + + return Array.from(map.entries()) + .map(sortAndTruncateYAxisKeys(countsOfEachBar, maxNumberOfBars)) + .map(([key, value], index) => ({ + borderWidth: 1, + backgroundColor: singleGraphColorRGBAById(index, 0.3), + borderColor: singleGraphColorRGBAById(index), + label: key, + data: value, + })); +} + +function sortAndTruncateYAxisKeys(countsOfEachBar: Map, maxNumberOfBars: number) { + const yAxisKeysToConsider = new Set( + Array.from(countsOfEachBar.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, maxNumberOfBars) + .map(([key]) => key), + ); + + return ([key, value]: [string, DataPoint[]]): [string, DataPoint[]] => { + const sortedValues = value.sort((a, b) => (countsOfEachBar.get(b.y) ?? 0) - (countsOfEachBar.get(a.y) ?? 0)); + const valuesWithLargestBars = sortedValues + .slice(0, maxNumberOfBars) + .filter((v) => yAxisKeysToConsider.has(v.y)); + return [key, valuesWithLargestBars]; + }; +} diff --git a/components/src/preact/aggregatedData/aggregate-table.tsx b/components/src/preact/aggregatedData/aggregate-table.tsx index 06743704..55624138 100644 --- a/components/src/preact/aggregatedData/aggregate-table.tsx +++ b/components/src/preact/aggregatedData/aggregate-table.tsx @@ -1,4 +1,5 @@ import { type FunctionComponent } from 'preact'; +import { useMemo } from 'preact/hooks'; import { type AggregateData, compareAscending } from '../../query/queryAggregateData'; import { Table } from '../components/table'; @@ -8,9 +9,17 @@ type AggregateTableProps = { fields: string[]; data: AggregateData; pageSize: boolean | number; + initialSortField: string; + initialSortDirection: 'ascending' | 'descending'; }; -export const AggregateTable: FunctionComponent = ({ data, fields, pageSize }) => { +export const AggregateTable: FunctionComponent = ({ + data, + fields, + pageSize, + initialSortField, + initialSortDirection, +}) => { const headers = [ ...fields.map((field) => { return { @@ -31,5 +40,18 @@ export const AggregateTable: FunctionComponent = ({ data, f }, ]; - return ; + const sortedData = useMemo(() => { + const validSortFields = ['count', 'proportion', ...fields]; + if (!validSortFields.includes(initialSortField)) { + throw new Error(`InitialSort field not in fields. Valid fields are: ${validSortFields.join(', ')}`); + } + + return data.sort((a, b) => + initialSortDirection === 'ascending' + ? compareAscending(a[initialSortField], b[initialSortField]) + : compareAscending(b[initialSortField], a[initialSortField]), + ); + }, [data, initialSortField, initialSortDirection, fields]); + + return
; }; diff --git a/components/src/preact/aggregatedData/aggregate.stories.tsx b/components/src/preact/aggregatedData/aggregate.stories.tsx index db675950..e0046705 100644 --- a/components/src/preact/aggregatedData/aggregate.stories.tsx +++ b/components/src/preact/aggregatedData/aggregate.stories.tsx @@ -59,6 +59,7 @@ export const Default: StoryObj = { initialSortField: 'count', initialSortDirection: 'descending', pageSize: 10, + maxNumberOfBars: 20, }, }; @@ -108,3 +109,75 @@ export const WithEmptyFieldString: StoryObj = { }); }, }; + +export const BarChartWithNoFields: StoryObj = { + ...Default, + args: { + ...Default.args, + views: ['bar', 'table'], + fields: [], + }, + parameters: { + fetchMock: { + mocks: [ + { + matcher: { + name: 'aggregatedData', + url: AGGREGATED_ENDPOINT, + }, + response: { + status: 200, + body: aggregatedData, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await waitFor(async () => { + await expect( + canvas.getByText('Cannot display a bar chart when there are no fields given', { + exact: false, + }), + ).toBeVisible(); + }); + }, +}; + +export const BarChartWithMoreThan2Fields: StoryObj = { + ...Default, + args: { + ...Default.args, + views: ['bar', 'table'], + fields: ['division', 'host', 'country'], + }, + parameters: { + fetchMock: { + mocks: [ + { + matcher: { + name: 'aggregatedData', + url: AGGREGATED_ENDPOINT, + }, + response: { + status: 200, + body: aggregatedData, + }, + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await waitFor(async () => { + await expect( + canvas.getByText('Cannot display a bar chart when there are more than two fields given', { + exact: false, + }), + ).toBeVisible(); + }); + }, +}; diff --git a/components/src/preact/aggregatedData/aggregate.tsx b/components/src/preact/aggregatedData/aggregate.tsx index c9d5c534..f9a788ca 100644 --- a/components/src/preact/aggregatedData/aggregate.tsx +++ b/components/src/preact/aggregatedData/aggregate.tsx @@ -15,8 +15,9 @@ import { NoDataDisplay } from '../components/no-data-display'; import { ResizeContainer } from '../components/resize-container'; import Tabs from '../components/tabs'; import { useQuery } from '../useQuery'; +import { AggregateBarChart } from './aggregate-bar-chart'; -const aggregateViewSchema = z.literal(views.table); +const aggregateViewSchema = z.union([z.literal(views.table), z.literal(views.bar)]); export type AggregateView = z.infer; const aggregatePropsSchema = z.object({ @@ -28,6 +29,7 @@ const aggregatePropsSchema = z.object({ pageSize: z.union([z.boolean(), z.number()]), width: z.string(), height: z.string(), + maxNumberOfBars: z.number(), }); export type AggregateProps = z.infer; @@ -49,10 +51,7 @@ export const AggregateInner: FunctionComponent = (componentProps const lapis = useContext(LapisUrlContext); const { data, error, isLoading } = useQuery(async () => { - return queryAggregateData(lapisFilter, fields, lapis, { - field: initialSortField, - direction: initialSortDirection, - }); + return queryAggregateData(lapisFilter, fields, lapis); }, [lapisFilter, fields, lapis, initialSortField, initialSortDirection]); if (isLoading) { @@ -78,7 +77,7 @@ type AggregatedDataTabsProps = { const AggregatedDataTabs: FunctionComponent = ({ data, originalComponentProps }) => { const getTab = (view: AggregateView) => { switch (view) { - case 'table': + case views.table: return { title: 'Table', content: ( @@ -86,6 +85,19 @@ const AggregatedDataTabs: FunctionComponent = ({ data, data={data} fields={originalComponentProps.fields} pageSize={originalComponentProps.pageSize} + initialSortField={originalComponentProps.initialSortField} + initialSortDirection={originalComponentProps.initialSortDirection} + /> + ), + }; + case views.bar: + return { + title: 'Bar', + content: ( + ), }; diff --git a/components/src/preact/shared/charts/colors.ts b/components/src/preact/shared/charts/colors.ts index 8b2e836b..feaf19da 100644 --- a/components/src/preact/shared/charts/colors.ts +++ b/components/src/preact/shared/charts/colors.ts @@ -18,7 +18,7 @@ export const singleGraphColorRGBAById = (id: number, alpha = 1) => { const keys = Object.keys(ColorsRGB) as GraphColor[]; const key = keys[id % keys.length]; - return `rgba(${ColorsRGB[key].join(',')},${alpha})`; + return singleGraphColorRGBByName(key, alpha); }; export const singleGraphColorRGBByName = (name: GraphColor, alpha = 1) => { diff --git a/components/src/query/queryAggregateData.spec.ts b/components/src/query/queryAggregateData.spec.ts index 50345b2f..83becd37 100644 --- a/components/src/query/queryAggregateData.spec.ts +++ b/components/src/query/queryAggregateData.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'vitest'; -import { queryAggregateData } from './queryAggregateData'; +import { compareAscending, queryAggregateData } from './queryAggregateData'; import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../vitest.setup'; describe('queryAggregateData', () => { @@ -29,118 +29,25 @@ describe('queryAggregateData', () => { { proportion: 0.125, count: 4, region: 'region1', host: 'host2' }, ]); }); +}); - test('should sort by initialSort field ascending', async () => { - const fields = ['division', 'host']; - const filter = { country: 'USA' }; - const initialSortField = 'host'; - const initialSortDirection = 'ascending'; - - lapisRequestMocks.aggregated( - { fields, ...filter }, - { - data: [ - { count: 4, region: 'region1', host: 'A_host' }, - { count: 4, region: 'region1', host: 'B_host' }, - { count: 8, region: 'region2', host: 'A_host1' }, - { count: 16, region: 'region2', host: 'C_host' }, - ], - }, - ); - - const result = await queryAggregateData(filter, fields, DUMMY_LAPIS_URL, { - field: initialSortField, - direction: initialSortDirection, - }); - - expect(result).to.deep.equal([ - { proportion: 0.125, count: 4, region: 'region1', host: 'A_host' }, - { proportion: 0.25, count: 8, region: 'region2', host: 'A_host1' }, - { proportion: 0.125, count: 4, region: 'region1', host: 'B_host' }, - { proportion: 0.5, count: 16, region: 'region2', host: 'C_host' }, - ]); - }); - - test('should sort by initialSort field descending', async () => { - const fields = ['division', 'host']; - const filter = { country: 'USA' }; - const initialSortField = 'host'; - const initialSortDirection = 'descending'; - - lapisRequestMocks.aggregated( - { fields, ...filter }, - { - data: [ - { count: 4, region: 'region1', host: 'A_host' }, - { count: 4, region: 'region1', host: 'B_host' }, - { count: 8, region: 'region2', host: 'A_host1' }, - { count: 16, region: 'region2', host: 'C_host' }, - ], - }, - ); - - const result = await queryAggregateData(filter, fields, DUMMY_LAPIS_URL, { - field: initialSortField, - direction: initialSortDirection, - }); - - expect(result).to.deep.equal([ - { proportion: 0.5, count: 16, region: 'region2', host: 'C_host' }, - { proportion: 0.125, count: 4, region: 'region1', host: 'B_host' }, - { proportion: 0.25, count: 8, region: 'region2', host: 'A_host1' }, - { proportion: 0.125, count: 4, region: 'region1', host: 'A_host' }, - ]); +describe('compareAscending', () => { + test('should compare numbers', () => { + expect(compareAscending(1, 2)).to.equal(-1); + expect(compareAscending(2, 1)).to.equal(1); + expect(compareAscending(2, 2)).to.equal(0); }); - test('should sort by initialSort number field', async () => { - const fields = ['division', 'host']; - const filter = { country: 'USA' }; - const initialSortField = 'proportion'; - const initialSortDirection = 'descending'; - - lapisRequestMocks.aggregated( - { fields, ...filter }, - { - data: [ - { count: 4, region: 'region1', host: 'A_host' }, - { count: 4, region: 'region1', host: 'B_host' }, - { count: 8, region: 'region2', host: 'A_host1' }, - { count: 16, region: 'region2', host: 'C_host' }, - ], - }, - ); - - const result = await queryAggregateData(filter, fields, DUMMY_LAPIS_URL, { - field: initialSortField, - direction: initialSortDirection, - }); - - expect(result).to.deep.equal([ - { proportion: 0.125, count: 4, region: 'region1', host: 'A_host' }, - { proportion: 0.125, count: 4, region: 'region1', host: 'B_host' }, - { proportion: 0.25, count: 8, region: 'region2', host: 'A_host1' }, - { proportion: 0.5, count: 16, region: 'region2', host: 'C_host' }, - ]); + test('should compare strings', () => { + expect(compareAscending('a', 'b')).to.equal(-1); + expect(compareAscending('b', 'a')).to.equal(1); + expect(compareAscending('a', 'a')).to.equal(0); }); - test('should throw if initialSortField is not in fields', async () => { - const fields = ['division', 'host']; - const filter = { country: 'USA' }; - const initialSortField = 'not_in_fields'; - const initialSortDirection = 'descending'; - - lapisRequestMocks.aggregated( - { fields, ...filter }, - { - data: [{ count: 4, region: 'region1', host: 'A_host' }], - }, - ); - - await expect( - queryAggregateData(filter, fields, DUMMY_LAPIS_URL, { - field: initialSortField, - direction: initialSortDirection, - }), - ).rejects.toThrowError('InitialSort field not in fields. Valid fields are: count, proportion, division, host'); + test('should compare boolean', () => { + expect(compareAscending(true, false)).to.equal(1); + expect(compareAscending(false, true)).to.equal(-1); + expect(compareAscending(true, true)).to.equal(0); + expect(compareAscending(false, false)).to.equal(0); }); }); diff --git a/components/src/query/queryAggregateData.ts b/components/src/query/queryAggregateData.ts index c0ff86bc..f2f01912 100644 --- a/components/src/query/queryAggregateData.ts +++ b/components/src/query/queryAggregateData.ts @@ -9,7 +9,7 @@ export type AggregateData = (Record & proportion: number; })[]; -export const compareAscending = (a: string | null | number, b: string | null | number) => { +export const compareAscending = (a: string | null | number | boolean, b: string | null | number | boolean) => { if (typeof a === 'number' && typeof b === 'number') { return a - b; } @@ -24,20 +24,10 @@ export async function queryAggregateData( lapisFilter: LapisFilter, fields: string[], lapis: string, - initialSort: InitialSort = { field: 'count', direction: 'descending' }, signal?: AbortSignal, ) { - const validSortFields = ['count', 'proportion', ...fields]; - if (!validSortFields.includes(initialSort.field)) { - throw new Error(`InitialSort field not in fields. Valid fields are: ${validSortFields.join(', ')}`); - } - const fetchData = new FetchAggregatedOperator>(lapisFilter, fields); - const sortData = new SortOperator(fetchData, (a, b) => { - return initialSort.direction === 'ascending' - ? compareAscending(a[initialSort.field], b[initialSort.field]) - : compareAscending(b[initialSort.field], a[initialSort.field]); - }); + const sortData = new SortOperator(fetchData, (a, b) => compareAscending(b.count, a.count)); const data = (await sortData.evaluate(lapis, signal)).content; const total = data.reduce((acc, row) => acc + row.count, 0); diff --git a/components/src/query/queryGeneralStatistics.ts b/components/src/query/queryGeneralStatistics.ts index 0fa62424..e8fbb9e4 100644 --- a/components/src/query/queryGeneralStatistics.ts +++ b/components/src/query/queryGeneralStatistics.ts @@ -7,8 +7,8 @@ export async function queryGeneralStatistics( lapis: string, signal?: AbortSignal, ) { - const numeratorCount = await queryAggregateData(numeratorFilter, [], lapis, undefined, signal); - const denominatorCount = await queryAggregateData(denominatorFilter, [], lapis, undefined, signal); + const numeratorCount = await queryAggregateData(numeratorFilter, [], lapis, signal); + const denominatorCount = await queryAggregateData(denominatorFilter, [], lapis, signal); if (numeratorCount.length === 0 || denominatorCount.length === 0) { throw new Error('No data found for the given filters'); diff --git a/components/src/web-components/visualization/gs-aggregate.stories.ts b/components/src/web-components/visualization/gs-aggregate.stories.ts index 298be022..fadd0a56 100644 --- a/components/src/web-components/visualization/gs-aggregate.stories.ts +++ b/components/src/web-components/visualization/gs-aggregate.stories.ts @@ -4,6 +4,8 @@ import { html } from 'lit'; import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock'; import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants'; import aggregatedData from '../../preact/aggregatedData/__mockData__/aggregated.json'; +import aggregatedDataWith1Field from '../../preact/aggregatedData/__mockData__/aggregatedWith1Field.json'; +import aggregatedDataWith2Fields from '../../preact/aggregatedData/__mockData__/aggregatedWith2Fields.json'; import type { AggregateProps } from '../../preact/aggregatedData/aggregate'; import './gs-aggregate'; @@ -19,6 +21,7 @@ const codeExample = ` initialSortField="count" initialSortDirection="descending" pageSize="10" + maxNumberOfBars="50" >`; const meta: Meta> = { @@ -27,7 +30,7 @@ const meta: Meta> = { argTypes: { fields: [{ control: 'object' }], views: { - options: ['table'], + options: ['table', 'bar'], control: { type: 'check' }, }, width: { control: 'text' }, @@ -40,24 +43,6 @@ const meta: Meta> = { }, }, parameters: withComponentDocs({ - fetchMock: { - mocks: [ - { - matcher: { - name: 'aggregatedData', - url: AGGREGATED_ENDPOINT, - body: { - fields: ['division', 'host'], - country: 'USA', - }, - }, - response: { - status: 200, - body: aggregatedData, - }, - }, - ], - }, componentDocs: { opensShadowDom: true, expectsChildren: false, @@ -81,12 +66,33 @@ export const Table: StoryObj> = { .initialSortField=${args.initialSortField} .initialSortDirection=${args.initialSortDirection} .pageSize=${args.pageSize} + .maxNumberOfBars=${args.maxNumberOfBars} > `, + parameters: { + fetchMock: { + mocks: [ + { + matcher: { + name: 'aggregatedData', + url: AGGREGATED_ENDPOINT, + body: { + fields: ['division', 'host'], + country: 'USA', + }, + }, + response: { + status: 200, + body: aggregatedData, + }, + }, + ], + }, + }, args: { fields: ['division', 'host'], - views: ['table'], + views: ['table', 'bar'], lapisFilter: { country: 'USA', }, @@ -95,5 +101,69 @@ export const Table: StoryObj> = { initialSortField: 'count', initialSortDirection: 'descending', pageSize: 10, + maxNumberOfBars: 10, + }, +}; + +export const BarChartWithOneField: StoryObj> = { + ...Table, + args: { + ...Table.args, + fields: ['division'], + views: ['bar', 'table'], + }, + parameters: { + fetchMock: { + mocks: [ + { + matcher: { + name: 'aggregatedData', + url: AGGREGATED_ENDPOINT, + body: { + fields: ['division'], + country: 'USA', + }, + }, + response: { + status: 200, + body: aggregatedDataWith1Field, + }, + }, + ], + }, + }, +}; + +export const BarChartWithTwoFields: StoryObj> = { + ...Table, + args: { + ...Table.args, + fields: ['division', 'nextstrainClade'], + lapisFilter: { + country: 'Germany', + dateTo: '2022-02-01', + }, + views: ['bar', 'table'], + }, + parameters: { + fetchMock: { + mocks: [ + { + matcher: { + name: 'aggregatedData', + url: AGGREGATED_ENDPOINT, + body: { + fields: ['division', 'nextstrainClade'], + country: 'Germany', + dateTo: '2022-02-01', + }, + }, + response: { + status: 200, + body: aggregatedDataWith2Fields, + }, + }, + ], + }, }, }; diff --git a/components/src/web-components/visualization/gs-aggregate.tsx b/components/src/web-components/visualization/gs-aggregate.tsx index 400bff9d..77ef751d 100644 --- a/components/src/web-components/visualization/gs-aggregate.tsx +++ b/components/src/web-components/visualization/gs-aggregate.tsx @@ -20,6 +20,16 @@ import { PreactLitAdapterWithGridJsStyles } from '../PreactLitAdapterWithGridJsS * along with the aggregated value and its proportion. * The proportion represents the ratio of the aggregated value to the total count of the data * (considering the applied filter). + * + * ### Bar Chart View + * + * In the bar chart view, the data is presented in vertical bars. + * The bar chart is supported when `fields` contains one or two entries. + * The first field will be used as the y-axis. + * If a second field is provided, it's values will be stacked along the x-axis for each key on the y-axis. + * + * The chart shows the bars with the highest aggregated `count`. + * The number of bars can be adjusted with the `maxNumberOfBars` property. */ @customElement('gs-aggregate') export class AggregateComponent extends PreactLitAdapterWithGridJsStyles { @@ -87,6 +97,12 @@ export class AggregateComponent extends PreactLitAdapterWithGridJsStyles { @property({ type: Object }) pageSize: boolean | number = false; + /** + * The maximum number of bars to display in the bar chart view. + */ + @property({ type: Object }) + maxNumberOfBars: number = 20; + override render() { return ( ); } @@ -131,4 +148,7 @@ type InitialSortDirectionMatches = Expect< Equals >; type PageSizeMatches = Expect>; +type MaxNumberOfBarsMatches = Expect< + Equals +>; /* eslint-enable @typescript-eslint/no-unused-vars, no-unused-vars */ diff --git a/components/tests/snapshots.spec.ts-snapshots/Aggregate-Story-visualization-aggregate--bar-chart-with-one-field-should-match-screenshot-1-chromium-linux.png b/components/tests/snapshots.spec.ts-snapshots/Aggregate-Story-visualization-aggregate--bar-chart-with-one-field-should-match-screenshot-1-chromium-linux.png new file mode 100644 index 00000000..8d698d5a Binary files /dev/null and b/components/tests/snapshots.spec.ts-snapshots/Aggregate-Story-visualization-aggregate--bar-chart-with-one-field-should-match-screenshot-1-chromium-linux.png differ diff --git a/components/tests/snapshots.spec.ts-snapshots/Aggregate-Story-visualization-aggregate--bar-chart-with-one-field-should-match-screenshot-1-firefox-linux.png b/components/tests/snapshots.spec.ts-snapshots/Aggregate-Story-visualization-aggregate--bar-chart-with-one-field-should-match-screenshot-1-firefox-linux.png new file mode 100644 index 00000000..4cb3ae59 Binary files /dev/null and b/components/tests/snapshots.spec.ts-snapshots/Aggregate-Story-visualization-aggregate--bar-chart-with-one-field-should-match-screenshot-1-firefox-linux.png differ diff --git a/components/tests/snapshots.spec.ts-snapshots/Aggregate-Story-visualization-aggregate--bar-chart-with-two-fields-should-match-screenshot-1-chromium-linux.png b/components/tests/snapshots.spec.ts-snapshots/Aggregate-Story-visualization-aggregate--bar-chart-with-two-fields-should-match-screenshot-1-chromium-linux.png new file mode 100644 index 00000000..5ea82cc6 Binary files /dev/null and b/components/tests/snapshots.spec.ts-snapshots/Aggregate-Story-visualization-aggregate--bar-chart-with-two-fields-should-match-screenshot-1-chromium-linux.png differ diff --git a/components/tests/snapshots.spec.ts-snapshots/Aggregate-Story-visualization-aggregate--bar-chart-with-two-fields-should-match-screenshot-1-firefox-linux.png b/components/tests/snapshots.spec.ts-snapshots/Aggregate-Story-visualization-aggregate--bar-chart-with-two-fields-should-match-screenshot-1-firefox-linux.png new file mode 100644 index 00000000..b9245e75 Binary files /dev/null and b/components/tests/snapshots.spec.ts-snapshots/Aggregate-Story-visualization-aggregate--bar-chart-with-two-fields-should-match-screenshot-1-firefox-linux.png differ diff --git a/components/tests/visualizationStories.ts b/components/tests/visualizationStories.ts index 47b1c19d..2682da99 100644 --- a/components/tests/visualizationStories.ts +++ b/components/tests/visualizationStories.ts @@ -64,6 +64,16 @@ export const visualizationStories = [ testDownloadWithFilename: 'aggregate.csv', loadingIsDoneIndicator: 'Table', }, + { + id: 'visualization-aggregate--bar-chart-with-one-field', + title: 'Aggregate', + loadingIsDoneIndicator: 'Bar', + }, + { + id: 'visualization-aggregate--bar-chart-with-two-fields', + title: 'Aggregate', + loadingIsDoneIndicator: 'Bar', + }, { id: 'visualization-number-sequences-over-time--one-dataset-bar-chart', title: 'Number of sequences over time',