From 47c3785341fb4c90ad98cbdc4c212f8d846143e9 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Tue, 15 Aug 2023 05:49:20 +0200 Subject: [PATCH 01/11] feat: add defining mutations --- web/data/definingMutations/BQ.1.json | 1165 +++++++++++ web/data/definingMutations/XBC.1.json | 1804 +++++++++++++++++ web/data/definingMutationsIndex.json | 12 + .../components/Common/ButtonTransparent.tsx | 42 + web/src/components/Common/MutationBadge.tsx | 93 +- web/src/components/Common/NameTable.tsx | 9 +- web/src/components/Common/SearchBox.tsx | 96 + web/src/components/Common/parsePosition.ts | 8 + .../DefiningMutations/DefMutLineageTitle.tsx | 45 + .../DefiningMutationsClusterIndexTable.tsx | 186 ++ .../DefiningMutationsIndexPage.tsx | 29 + .../DefiningMutationsPage.tsx | 402 ++++ web/src/components/Layout/NavigationBar.tsx | 1 + web/src/components/Loading/Loading.tsx | 2 + .../en/DefiningMutationsIndexIntro.mdx | 5 + .../en/DefiningMutationsVariantIntro.mdx | 3 + web/src/helpers/join.tsx | 19 + web/src/helpers/search.ts | 20 + web/src/helpers/useToggle.ts | 18 + web/src/io/getDefiningMutationsClusters.ts | 99 + .../defining-mutations/[clusterName].tsx | 32 + web/src/pages/defining-mutations/index.tsx | 5 + web/src/theme.ts | 2 + 23 files changed, 4048 insertions(+), 49 deletions(-) create mode 100644 web/data/definingMutations/BQ.1.json create mode 100644 web/data/definingMutations/XBC.1.json create mode 100644 web/data/definingMutationsIndex.json create mode 100644 web/src/components/Common/ButtonTransparent.tsx create mode 100644 web/src/components/Common/SearchBox.tsx create mode 100644 web/src/components/DefiningMutations/DefMutLineageTitle.tsx create mode 100644 web/src/components/DefiningMutations/DefiningMutationsClusterIndexTable.tsx create mode 100644 web/src/components/DefiningMutations/DefiningMutationsIndexPage.tsx create mode 100644 web/src/components/DefiningMutations/DefiningMutationsPage.tsx create mode 100644 web/src/content/en/DefiningMutationsIndexIntro.mdx create mode 100644 web/src/content/en/DefiningMutationsVariantIntro.mdx create mode 100644 web/src/helpers/join.tsx create mode 100644 web/src/helpers/search.ts create mode 100644 web/src/helpers/useToggle.ts create mode 100644 web/src/io/getDefiningMutationsClusters.ts create mode 100644 web/src/pages/defining-mutations/[clusterName].tsx create mode 100644 web/src/pages/defining-mutations/index.tsx diff --git a/web/data/definingMutations/BQ.1.json b/web/data/definingMutations/BQ.1.json new file mode 100644 index 0000000000..9163173aac --- /dev/null +++ b/web/data/definingMutations/BQ.1.json @@ -0,0 +1,1165 @@ +{ + "lineage": "BQ.1", + "unaliased": "B.1.1.529.5.3.1.1.1.1.1", + "parent": "BE.1.1.1", + "children": [ + "BQ.1.1", + "BQ.1.2", + "BQ.1.3", + "BQ.1.4", + "BQ.1.5", + "BQ.1.6", + "BQ.1.7", + "BQ.1.8", + "BQ.1.9", + "BQ.1.10", + "BQ.1.11", + "BQ.1.12", + "BQ.1.13", + "BQ.1.14", + "BQ.1.15", + "BQ.1.16", + "BQ.1.17", + "BQ.1.18", + "BQ.1.19", + "BQ.1.20", + "BQ.1.21", + "BQ.1.22", + "BQ.1.23", + "BQ.1.24", + "BQ.1.25", + "BQ.1.26", + "BQ.1.27", + "BQ.1.28", + "BQ.1.29", + "BQ.1.30", + "BQ.1.31", + "BQ.1.32", + "BQ.1.33" + ], + "nextstrainClade": "22E", + "frameShifts": [], + "designationDate": "2022-09-02", + "mutations": { + "wuhan": { + "nuc": { + "241": { + "ref": "C", + "alt": "T" + }, + "670": { + "ref": "T", + "alt": "G" + }, + "1931": { + "ref": "C", + "alt": "A" + }, + "2790": { + "ref": "C", + "alt": "T" + }, + "2954": { + "ref": "T", + "alt": "C" + }, + "3037": { + "ref": "C", + "alt": "T" + }, + "4184": { + "ref": "G", + "alt": "A" + }, + "4321": { + "ref": "C", + "alt": "T" + }, + "9344": { + "ref": "C", + "alt": "T" + }, + "9424": { + "ref": "A", + "alt": "G" + }, + "9534": { + "ref": "C", + "alt": "T" + }, + "10029": { + "ref": "C", + "alt": "T" + }, + "10198": { + "ref": "C", + "alt": "T" + }, + "10447": { + "ref": "G", + "alt": "A" + }, + "10449": { + "ref": "C", + "alt": "A" + }, + "11750": { + "ref": "C", + "alt": "T" + }, + "12160": { + "ref": "G", + "alt": "A" + }, + "12880": { + "ref": "C", + "alt": "T" + }, + "14257": { + "ref": "T", + "alt": "C" + }, + "14408": { + "ref": "C", + "alt": "T" + }, + "15714": { + "ref": "C", + "alt": "T" + }, + "16935": { + "ref": "G", + "alt": "A" + }, + "17410": { + "ref": "C", + "alt": "T" + }, + "18163": { + "ref": "A", + "alt": "G" + }, + "19955": { + "ref": "C", + "alt": "T" + }, + "20055": { + "ref": "A", + "alt": "G" + }, + "21618": { + "ref": "C", + "alt": "T" + }, + "21987": { + "ref": "G", + "alt": "A" + }, + "22200": { + "ref": "T", + "alt": "G" + }, + "22578": { + "ref": "G", + "alt": "A" + }, + "22674": { + "ref": "C", + "alt": "T" + }, + "22679": { + "ref": "T", + "alt": "C" + }, + "22686": { + "ref": "C", + "alt": "T" + }, + "22688": { + "ref": "A", + "alt": "G" + }, + "22775": { + "ref": "G", + "alt": "A" + }, + "22786": { + "ref": "A", + "alt": "C" + }, + "22813": { + "ref": "G", + "alt": "T" + }, + "22882": { + "ref": "T", + "alt": "G" + }, + "22893": { + "ref": "A", + "alt": "C" + }, + "22917": { + "ref": "T", + "alt": "G" + }, + "22942": { + "ref": "T", + "alt": "A" + }, + "22992": { + "ref": "G", + "alt": "A" + }, + "22995": { + "ref": "C", + "alt": "A" + }, + "23013": { + "ref": "A", + "alt": "C" + }, + "23018": { + "ref": "T", + "alt": "G" + }, + "23055": { + "ref": "A", + "alt": "G" + }, + "23063": { + "ref": "A", + "alt": "T" + }, + "23075": { + "ref": "T", + "alt": "C" + }, + "23403": { + "ref": "A", + "alt": "G" + }, + "23525": { + "ref": "C", + "alt": "T" + }, + "23599": { + "ref": "T", + "alt": "G" + }, + "23604": { + "ref": "C", + "alt": "A" + }, + "23854": { + "ref": "C", + "alt": "A" + }, + "23948": { + "ref": "G", + "alt": "T" + }, + "24424": { + "ref": "A", + "alt": "T" + }, + "24469": { + "ref": "T", + "alt": "A" + }, + "25000": { + "ref": "C", + "alt": "T" + }, + "25584": { + "ref": "C", + "alt": "T" + }, + "26060": { + "ref": "C", + "alt": "T" + }, + "26270": { + "ref": "C", + "alt": "T" + }, + "26529": { + "ref": "G", + "alt": "A" + }, + "26577": { + "ref": "C", + "alt": "G" + }, + "26709": { + "ref": "G", + "alt": "A" + }, + "27807": { + "ref": "C", + "alt": "T" + }, + "27889": { + "ref": "C", + "alt": "T" + }, + "28271": { + "ref": "A", + "alt": "T" + }, + "28311": { + "ref": "C", + "alt": "T", + "annotation": "This mutation causes ORG9b:P10S, but the adjacent mutation changes to P10F" + }, + "28312": { + "ref": "C", + "alt": "T", + "annotation": "This mutation on top of 28311, change ORf9b:P10S to P10F" + }, + "28681": { + "ref": "G", + "alt": "T" + }, + "28881": { + "ref": "G", + "alt": "A" + }, + "28882": { + "ref": "G", + "alt": "A" + }, + "28883": { + "ref": "G", + "alt": "C" + }, + "29510": { + "ref": "A", + "alt": "C" + }, + "29868": { + "ref": "G", + "alt": "A" + }, + "11288": { + "ref": "T", + "alt": "-" + }, + "11289": { + "ref": "C", + "alt": "-" + }, + "11290": { + "ref": "T", + "alt": "-" + }, + "11291": { + "ref": "G", + "alt": "-" + }, + "11292": { + "ref": "G", + "alt": "-" + }, + "11293": { + "ref": "T", + "alt": "-" + }, + "11294": { + "ref": "T", + "alt": "-" + }, + "11295": { + "ref": "T", + "alt": "-" + }, + "11296": { + "ref": "T", + "alt": "-" + }, + "21633": { + "ref": "T", + "alt": "-" + }, + "21634": { + "ref": "A", + "alt": "-" + }, + "21635": { + "ref": "C", + "alt": "-" + }, + "21636": { + "ref": "C", + "alt": "-" + }, + "21637": { + "ref": "C", + "alt": "-" + }, + "21638": { + "ref": "C", + "alt": "-" + }, + "21639": { + "ref": "C", + "alt": "-" + }, + "21640": { + "ref": "T", + "alt": "-" + }, + "21641": { + "ref": "G", + "alt": "-" + }, + "21765": { + "ref": "T", + "alt": "-" + }, + "21766": { + "ref": "A", + "alt": "-" + }, + "21767": { + "ref": "C", + "alt": "-" + }, + "21768": { + "ref": "A", + "alt": "-" + }, + "21769": { + "ref": "T", + "alt": "-" + }, + "21770": { + "ref": "G", + "alt": "-" + }, + "28362": { + "ref": "G", + "alt": "-" + }, + "28363": { + "ref": "A", + "alt": "-" + }, + "28364": { + "ref": "G", + "alt": "-" + }, + "28365": { + "ref": "A", + "alt": "-" + }, + "28366": { + "ref": "A", + "alt": "-" + }, + "28367": { + "ref": "C", + "alt": "-" + }, + "28368": { + "ref": "G", + "alt": "-" + }, + "28369": { + "ref": "C", + "alt": "-" + }, + "28370": { + "ref": "A", + "alt": "-" + }, + "29734": { + "ref": "G", + "alt": "-" + }, + "29735": { + "ref": "A", + "alt": "-" + }, + "29736": { + "ref": "G", + "alt": "-" + }, + "29737": { + "ref": "G", + "alt": "-" + }, + "29738": { + "ref": "C", + "alt": "-" + }, + "29739": { + "ref": "C", + "alt": "-" + }, + "29740": { + "ref": "A", + "alt": "-" + }, + "29741": { + "ref": "C", + "alt": "-" + }, + "29742": { + "ref": "G", + "alt": "-" + }, + "29743": { + "ref": "C", + "alt": "-" + }, + "29744": { + "ref": "G", + "alt": "-" + }, + "29745": { + "ref": "G", + "alt": "-" + }, + "29746": { + "ref": "A", + "alt": "-" + }, + "29747": { + "ref": "G", + "alt": "-" + }, + "29748": { + "ref": "T", + "alt": "-" + }, + "29749": { + "ref": "A", + "alt": "-" + }, + "29750": { + "ref": "C", + "alt": "-" + }, + "29751": { + "ref": "G", + "alt": "-" + }, + "29752": { + "ref": "A", + "alt": "-" + }, + "29753": { + "ref": "T", + "alt": "-" + }, + "29754": { + "ref": "C", + "alt": "-" + }, + "29755": { + "ref": "G", + "alt": "-" + }, + "29756": { + "ref": "A", + "alt": "-" + }, + "29757": { + "ref": "G", + "alt": "-" + }, + "29758": { + "ref": "T", + "alt": "-" + }, + "29759": { + "ref": "G", + "alt": "-" + } + }, + "aa": { + "E": { + "9": { + "ref": "T", + "alt": "I", + "nucPos": [ + 26270 + ] + } + }, + "M": { + "3": { + "ref": "D", + "alt": "N", + "nucPos": [ + 26529 + ] + }, + "19": { + "ref": "Q", + "alt": "E", + "nucPos": [ + 26577 + ] + }, + "63": { + "ref": "A", + "alt": "T", + "nucPos": [ + 26709 + ] + } + }, + "N": { + "13": { + "ref": "P", + "alt": "L", + "nucPos": [ + 28312, + 28311 + ] + }, + "136": { + "ref": "E", + "alt": "D", + "nucPos": [ + 28681 + ] + }, + "203": { + "ref": "R", + "alt": "K", + "nucPos": [ + 28881, + 28882 + ] + }, + "204": { + "ref": "G", + "alt": "R", + "nucPos": [ + 28883 + ] + }, + "413": { + "ref": "S", + "alt": "R", + "nucPos": [ + 29510 + ] + }, + "31": { + "ref": "E", + "alt": "-", + "nucPos": [ + 28364, + 28365, + 28366 + ], + "annotation": "Deletion in-frame for ORF9b; in N, deletes last 2 bases of AA 30, entirety of 31/32, first base of 33" + }, + "32": { + "ref": "R", + "alt": "-", + "nucPos": [ + 28368, + 28369, + 28367 + ], + "annotation": "Deletion in-frame for ORF9b; in N, deletes last 2 bases of AA 30, entirety of 31/32, first base of 33" + }, + "33": { + "ref": "S", + "alt": "-", + "nucPos": [ + 28370 + ], + "annotation": "Deletion in-frame for ORF9b; in N, deletes last 2 bases of AA 30, entirety of 31/32, first base of 33" + } + }, + "ORF1a": { + "135": { + "ref": "S", + "alt": "R", + "nucPos": [ + 670 + ] + }, + "556": { + "ref": "Q", + "alt": "K", + "nucPos": [ + 1931 + ] + }, + "842": { + "ref": "T", + "alt": "I", + "nucPos": [ + 2790 + ] + }, + "1307": { + "ref": "G", + "alt": "S", + "nucPos": [ + 4184 + ] + }, + "3027": { + "ref": "L", + "alt": "F", + "nucPos": [ + 9344 + ] + }, + "3090": { + "ref": "T", + "alt": "I", + "nucPos": [ + 9534 + ] + }, + "3255": { + "ref": "T", + "alt": "I", + "nucPos": [ + 10029 + ] + }, + "3395": { + "ref": "P", + "alt": "H", + "nucPos": [ + 10449 + ] + }, + "3829": { + "ref": "L", + "alt": "F", + "nucPos": [ + 11750 + ] + }, + "3675": { + "ref": "S", + "alt": "-", + "nucPos": [ + 11288, + 11289, + 11290 + ] + }, + "3676": { + "ref": "G", + "alt": "-", + "nucPos": [ + 11291, + 11292, + 11293 + ] + }, + "3677": { + "ref": "F", + "alt": "-", + "nucPos": [ + 11296, + 11294, + 11295 + ] + } + }, + "ORF1b": { + "264": { + "ref": "Y", + "alt": "H", + "nucPos": [ + 14257 + ] + }, + "314": { + "ref": "P", + "alt": "L", + "nucPos": [ + 14408 + ] + }, + "1156": { + "ref": "M", + "alt": "I", + "nucPos": [ + 16935 + ] + }, + "1315": { + "ref": "R", + "alt": "C", + "nucPos": [ + 17410 + ] + }, + "1566": { + "ref": "I", + "alt": "V", + "nucPos": [ + 18163 + ] + }, + "2163": { + "ref": "T", + "alt": "I", + "nucPos": [ + 19955 + ] + } + }, + "ORF3a": { + "223": { + "ref": "T", + "alt": "I", + "nucPos": [ + 26060 + ] + } + }, + "ORF9b": { + "10": { + "ref": "P", + "alt": "F", + "nucPos": [ + 28312, + 28311 + ] + }, + "27": { + "ref": "E", + "alt": "-", + "nucPos": [ + 28362, + 28363, + 28364 + ] + }, + "28": { + "ref": "N", + "alt": "-", + "nucPos": [ + 28365, + 28366, + 28367 + ] + }, + "29": { + "ref": "A", + "alt": "-", + "nucPos": [ + 28368, + 28369, + 28370 + ] + } + }, + "S": { + "19": { + "ref": "T", + "alt": "I", + "nucPos": [ + 21618 + ] + }, + "27": { + "ref": "A", + "alt": "S", + "nucPos": [ + 21641 + ], + "annotation": "This change is a consequence of the prior deletion" + }, + "142": { + "ref": "G", + "alt": "D", + "nucPos": [ + 21987 + ] + }, + "213": { + "ref": "V", + "alt": "G", + "nucPos": [ + 22200 + ] + }, + "339": { + "ref": "G", + "alt": "D", + "nucPos": [ + 22578 + ] + }, + "371": { + "ref": "S", + "alt": "F", + "nucPos": [ + 22674 + ] + }, + "373": { + "ref": "S", + "alt": "P", + "nucPos": [ + 22679 + ] + }, + "375": { + "ref": "S", + "alt": "F", + "nucPos": [ + 22686 + ] + }, + "376": { + "ref": "T", + "alt": "A", + "nucPos": [ + 22688 + ] + }, + "405": { + "ref": "D", + "alt": "N", + "nucPos": [ + 22775 + ] + }, + "408": { + "ref": "R", + "alt": "S", + "nucPos": [ + 22786 + ] + }, + "417": { + "ref": "K", + "alt": "N", + "nucPos": [ + 22813 + ] + }, + "440": { + "ref": "N", + "alt": "K", + "nucPos": [ + 22882 + ] + }, + "444": { + "ref": "K", + "alt": "T", + "nucPos": [ + 22893 + ] + }, + "452": { + "ref": "L", + "alt": "R", + "nucPos": [ + 22917 + ] + }, + "460": { + "ref": "N", + "alt": "K", + "nucPos": [ + 22942 + ] + }, + "477": { + "ref": "S", + "alt": "N", + "nucPos": [ + 22992 + ] + }, + "478": { + "ref": "T", + "alt": "K", + "nucPos": [ + 22995 + ] + }, + "484": { + "ref": "E", + "alt": "A", + "nucPos": [ + 23013 + ] + }, + "486": { + "ref": "F", + "alt": "V", + "nucPos": [ + 23018 + ] + }, + "498": { + "ref": "Q", + "alt": "R", + "nucPos": [ + 23055 + ] + }, + "501": { + "ref": "N", + "alt": "Y", + "nucPos": [ + 23063 + ] + }, + "505": { + "ref": "Y", + "alt": "H", + "nucPos": [ + 23075 + ] + }, + "614": { + "ref": "D", + "alt": "G", + "nucPos": [ + 23403 + ] + }, + "655": { + "ref": "H", + "alt": "Y", + "nucPos": [ + 23525 + ] + }, + "679": { + "ref": "N", + "alt": "K", + "nucPos": [ + 23599 + ] + }, + "681": { + "ref": "P", + "alt": "H", + "nucPos": [ + 23604 + ] + }, + "764": { + "ref": "N", + "alt": "K", + "nucPos": [ + 23854 + ] + }, + "796": { + "ref": "D", + "alt": "Y", + "nucPos": [ + 23948 + ] + }, + "954": { + "ref": "Q", + "alt": "H", + "nucPos": [ + 24424 + ] + }, + "969": { + "ref": "N", + "alt": "K", + "nucPos": [ + 24469 + ] + }, + "24": { + "ref": "L", + "alt": "-", + "nucPos": [ + 21633, + 21634 + ], + "annotation": "Deletion removes last 2 bases from AA 24, entirety of 25/26, first base of 27" + }, + "25": { + "ref": "P", + "alt": "-", + "nucPos": [ + 21635, + 21636, + 21637 + ], + "annotation": "Deletion removes last 2 bases from AA 24, entirety of 25/26, first base of 27" + }, + "26": { + "ref": "P", + "alt": "-", + "nucPos": [ + 21640, + 21638, + 21639 + ], + "annotation": "Deletion removes last 2 bases from AA 24, entirety of 25/26, first base of 27" + }, + "69": { + "ref": "H", + "alt": "-", + "nucPos": [ + 21768, + 21769, + 21767 + ] + }, + "70": { + "ref": "V", + "alt": "-", + "nucPos": [ + 21770 + ] + } + } + }, + "frameshifts": {} + }, + "pangoParent": { + "nuc": { + "14257": { + "ref": "T", + "alt": "C" + }, + "22942": { + "ref": "T", + "alt": "A" + } + }, + "aa": { + "ORF1b": { + "264": { + "ref": "Y", + "alt": "H", + "nucPos": [ + 14257 + ] + } + }, + "S": { + "460": { + "ref": "N", + "alt": "K", + "nucPos": [ + 22942 + ] + } + } + }, + "frameshifts": {} + } + } +} diff --git a/web/data/definingMutations/XBC.1.json b/web/data/definingMutations/XBC.1.json new file mode 100644 index 0000000000..ed781e73b9 --- /dev/null +++ b/web/data/definingMutations/XBC.1.json @@ -0,0 +1,1804 @@ +{ + "lineage": "XBC.1", + "unaliased": "XBC.1", + "parent": "", + "children": [ + "XBC.1.1", + "XBC.1.2", + "XBC.1.3", + "XBC.1.5", + "XBC.1.6" + ], + "nextstrainClade": "recombinant", + "frameShifts": [ + "ORF7b:14-44", + "ORF8:35-122" + ], + "designationDate": "2022-10-03", + "mutations": { + "wuhan": { + "nuc": { + "241": { + "ref": "C", + "alt": "T" + }, + "670": { + "ref": "T", + "alt": "G" + }, + "1437": { + "ref": "C", + "alt": "T" + }, + "2790": { + "ref": "C", + "alt": "T" + }, + "3037": { + "ref": "C", + "alt": "T" + }, + "4893": { + "ref": "C", + "alt": "T" + }, + "5184": { + "ref": "C", + "alt": "T" + }, + "5584": { + "ref": "A", + "alt": "G" + }, + "6196": { + "ref": "C", + "alt": "T" + }, + "6576": { + "ref": "G", + "alt": "T" + }, + "7091": { + "ref": "G", + "alt": "A" + }, + "9073": { + "ref": "C", + "alt": "T" + }, + "9891": { + "ref": "C", + "alt": "T" + }, + "11418": { + "ref": "T", + "alt": "C" + }, + "11514": { + "ref": "C", + "alt": "T" + }, + "13019": { + "ref": "C", + "alt": "T" + }, + "14408": { + "ref": "C", + "alt": "T" + }, + "15030": { + "ref": "T", + "alt": "C" + }, + "15237": { + "ref": "C", + "alt": "T" + }, + "15451": { + "ref": "G", + "alt": "A" + }, + "16466": { + "ref": "C", + "alt": "T" + }, + "16616": { + "ref": "C", + "alt": "T" + }, + "21618": { + "ref": "C", + "alt": "T" + }, + "21635": { + "ref": "C", + "alt": "T" + }, + "21987": { + "ref": "G", + "alt": "A" + }, + "22188": { + "ref": "C", + "alt": "T" + }, + "22197": { + "ref": "T", + "alt": "C" + }, + "22205": { + "ref": "G", + "alt": "C" + }, + "22227": { + "ref": "C", + "alt": "T" + }, + "22329": { + "ref": "C", + "alt": "T" + }, + "22674": { + "ref": "C", + "alt": "T" + }, + "22679": { + "ref": "T", + "alt": "C" + }, + "22686": { + "ref": "C", + "alt": "T" + }, + "22688": { + "ref": "A", + "alt": "G" + }, + "22775": { + "ref": "G", + "alt": "A" + }, + "22786": { + "ref": "A", + "alt": "C" + }, + "22813": { + "ref": "G", + "alt": "T" + }, + "22882": { + "ref": "T", + "alt": "G" + }, + "22898": { + "ref": "G", + "alt": "A" + }, + "22916": { + "ref": "C", + "alt": "A" + }, + "22992": { + "ref": "G", + "alt": "A" + }, + "22995": { + "ref": "C", + "alt": "A" + }, + "23013": { + "ref": "A", + "alt": "C" + }, + "23018": { + "ref": "T", + "alt": "C" + }, + "23019": { + "ref": "T", + "alt": "C" + }, + "23055": { + "ref": "A", + "alt": "G" + }, + "23063": { + "ref": "A", + "alt": "T" + }, + "23075": { + "ref": "T", + "alt": "C" + }, + "23403": { + "ref": "A", + "alt": "G" + }, + "23525": { + "ref": "C", + "alt": "T" + }, + "23599": { + "ref": "T", + "alt": "G" + }, + "23604": { + "ref": "C", + "alt": "A" + }, + "23670": { + "ref": "A", + "alt": "T" + }, + "23854": { + "ref": "C", + "alt": "A" + }, + "23948": { + "ref": "G", + "alt": "T" + }, + "24424": { + "ref": "A", + "alt": "T" + }, + "24469": { + "ref": "T", + "alt": "A" + }, + "25000": { + "ref": "C", + "alt": "T" + }, + "25854": { + "ref": "C", + "alt": "T" + }, + "26767": { + "ref": "T", + "alt": "C" + }, + "26819": { + "ref": "T", + "alt": "C" + }, + "27718": { + "ref": "T", + "alt": "C" + }, + "27752": { + "ref": "C", + "alt": "T" + }, + "27890": { + "ref": "G", + "alt": "T" + }, + "27898": { + "ref": "A", + "alt": "C" + }, + "28461": { + "ref": "A", + "alt": "G" + }, + "28881": { + "ref": "G", + "alt": "T" + }, + "29402": { + "ref": "G", + "alt": "T" + }, + "29742": { + "ref": "G", + "alt": "T" + }, + "21992": { + "ref": "T", + "alt": "-" + }, + "21993": { + "ref": "A", + "alt": "-" + }, + "21994": { + "ref": "T", + "alt": "-" + }, + "22029": { + "ref": "A", + "alt": "-" + }, + "22030": { + "ref": "G", + "alt": "-" + }, + "22031": { + "ref": "T", + "alt": "-" + }, + "22032": { + "ref": "T", + "alt": "-" + }, + "22033": { + "ref": "C", + "alt": "-" + }, + "22034": { + "ref": "A", + "alt": "-" + }, + "22289": { + "ref": "G", + "alt": "-" + }, + "22290": { + "ref": "C", + "alt": "-" + }, + "22291": { + "ref": "T", + "alt": "-" + }, + "22292": { + "ref": "T", + "alt": "-" + }, + "22293": { + "ref": "T", + "alt": "-" + }, + "22294": { + "ref": "A", + "alt": "-" + }, + "27792": { + "ref": "T", + "alt": "-" + }, + "27793": { + "ref": "T", + "alt": "-" + }, + "27992": { + "ref": "T", + "alt": "-" + }, + "27993": { + "ref": "G", + "alt": "-" + }, + "28248": { + "ref": "G", + "alt": "-" + }, + "28249": { + "ref": "A", + "alt": "-" + }, + "28250": { + "ref": "T", + "alt": "-" + }, + "28251": { + "ref": "T", + "alt": "-" + }, + "28252": { + "ref": "T", + "alt": "-" + }, + "28253": { + "ref": "C", + "alt": "-" + }, + "28271": { + "ref": "A", + "alt": "-" + } + }, + "aa": { + "M": { + "82": { + "ref": "I", + "alt": "T", + "nucPos": [ + 26767 + ] + } + }, + "N": { + "63": { + "ref": "D", + "alt": "G", + "nucPos": [ + 28461 + ] + }, + "203": { + "ref": "R", + "alt": "M", + "nucPos": [ + 28881 + ] + }, + "377": { + "ref": "D", + "alt": "Y", + "nucPos": [ + 29402 + ] + } + }, + "ORF1a": { + "135": { + "ref": "S", + "alt": "R", + "nucPos": [ + 670 + ] + }, + "391": { + "ref": "S", + "alt": "F", + "nucPos": [ + 1437 + ] + }, + "842": { + "ref": "T", + "alt": "I", + "nucPos": [ + 2790 + ] + }, + "1543": { + "ref": "T", + "alt": "I", + "nucPos": [ + 4893 + ] + }, + "1640": { + "ref": "P", + "alt": "L", + "nucPos": [ + 5184 + ] + }, + "2104": { + "ref": "S", + "alt": "I", + "nucPos": [ + 6576 + ] + }, + "2276": { + "ref": "V", + "alt": "I", + "nucPos": [ + 7091 + ] + }, + "3209": { + "ref": "A", + "alt": "V", + "nucPos": [ + 9891 + ] + }, + "3718": { + "ref": "V", + "alt": "A", + "nucPos": [ + 11418 + ] + }, + "3750": { + "ref": "T", + "alt": "I", + "nucPos": [ + 11514 + ] + } + }, + "ORF1b": { + "314": { + "ref": "P", + "alt": "L", + "nucPos": [ + 14408 + ] + }, + "662": { + "ref": "G", + "alt": "S", + "nucPos": [ + 15451 + ] + }, + "1000": { + "ref": "P", + "alt": "L", + "nucPos": [ + 16466 + ] + }, + "1050": { + "ref": "T", + "alt": "I", + "nucPos": [ + 16616 + ] + } + }, + "ORF7a": { + "109": { + "ref": "F", + "alt": "L", + "nucPos": [ + 27718 + ] + }, + "120": { + "ref": "T", + "alt": "I", + "nucPos": [ + 27752 + ] + } + }, + "ORF8": { + "2": { + "ref": "K", + "alt": "T", + "nucPos": [ + 27898 + ] + }, + "33": { + "ref": "V", + "alt": "-", + "nucPos": [ + 27992 + ] + }, + "34": { + "ref": "D", + "alt": "-", + "nucPos": [ + 27993 + ] + } + }, + "ORF9b": { + "60": { + "ref": "T", + "alt": "A", + "nucPos": [ + 28461 + ] + } + }, + "S": { + "19": { + "ref": "T", + "alt": "I", + "nucPos": [ + 21618 + ] + }, + "25": { + "ref": "P", + "alt": "S", + "nucPos": [ + 21635 + ] + }, + "142": { + "ref": "G", + "alt": "D", + "nucPos": [ + 21987 + ] + }, + "158": { + "ref": "R", + "alt": "G", + "nucPos": [ + 22034 + ] + }, + "209": { + "ref": "P", + "alt": "L", + "nucPos": [ + 22188 + ] + }, + "212": { + "ref": "L", + "alt": "S", + "nucPos": [ + 22197 + ] + }, + "215": { + "ref": "D", + "alt": "H", + "nucPos": [ + 22205 + ] + }, + "222": { + "ref": "A", + "alt": "V", + "nucPos": [ + 22227 + ] + }, + "256": { + "ref": "S", + "alt": "L", + "nucPos": [ + 22329 + ] + }, + "371": { + "ref": "S", + "alt": "F", + "nucPos": [ + 22674 + ] + }, + "373": { + "ref": "S", + "alt": "P", + "nucPos": [ + 22679 + ] + }, + "375": { + "ref": "S", + "alt": "F", + "nucPos": [ + 22686 + ] + }, + "376": { + "ref": "T", + "alt": "A", + "nucPos": [ + 22688 + ] + }, + "405": { + "ref": "D", + "alt": "N", + "nucPos": [ + 22775 + ] + }, + "408": { + "ref": "R", + "alt": "S", + "nucPos": [ + 22786 + ] + }, + "417": { + "ref": "K", + "alt": "N", + "nucPos": [ + 22813 + ] + }, + "440": { + "ref": "N", + "alt": "K", + "nucPos": [ + 22882 + ] + }, + "446": { + "ref": "G", + "alt": "S", + "nucPos": [ + 22898 + ] + }, + "452": { + "ref": "L", + "alt": "M", + "nucPos": [ + 22916 + ] + }, + "477": { + "ref": "S", + "alt": "N", + "nucPos": [ + 22992 + ] + }, + "478": { + "ref": "T", + "alt": "K", + "nucPos": [ + 22995 + ] + }, + "484": { + "ref": "E", + "alt": "A", + "nucPos": [ + 23013 + ] + }, + "486": { + "ref": "F", + "alt": "P", + "nucPos": [ + 23018, + 23019 + ] + }, + "498": { + "ref": "Q", + "alt": "R", + "nucPos": [ + 23055 + ] + }, + "501": { + "ref": "N", + "alt": "Y", + "nucPos": [ + 23063 + ] + }, + "505": { + "ref": "Y", + "alt": "H", + "nucPos": [ + 23075 + ] + }, + "614": { + "ref": "D", + "alt": "G", + "nucPos": [ + 23403 + ] + }, + "655": { + "ref": "H", + "alt": "Y", + "nucPos": [ + 23525 + ] + }, + "679": { + "ref": "N", + "alt": "K", + "nucPos": [ + 23599 + ] + }, + "681": { + "ref": "P", + "alt": "H", + "nucPos": [ + 23604 + ] + }, + "703": { + "ref": "N", + "alt": "I", + "nucPos": [ + 23670 + ] + }, + "764": { + "ref": "N", + "alt": "K", + "nucPos": [ + 23854 + ] + }, + "796": { + "ref": "D", + "alt": "Y", + "nucPos": [ + 23948 + ] + }, + "954": { + "ref": "Q", + "alt": "H", + "nucPos": [ + 24424 + ] + }, + "969": { + "ref": "N", + "alt": "K", + "nucPos": [ + 24469 + ] + }, + "144": { + "ref": "Y", + "alt": "-", + "nucPos": [ + 21992, + 21993, + 21994 + ] + }, + "156": { + "ref": "E", + "alt": "-", + "nucPos": [ + 22029, + 22030 + ] + }, + "157": { + "ref": "F", + "alt": "-", + "nucPos": [ + 22032, + 22033, + 22031 + ] + }, + "242": { + "ref": "L", + "alt": "-", + "nucPos": [] + }, + "243": { + "ref": "A", + "alt": "-", + "nucPos": [ + 22289, + 22290, + 22291 + ] + } + }, + "ORF7b": { + "13": { + "ref": "F", + "alt": "-", + "nucPos": [ + 27792, + 27793 + ] + } + } + }, + "frameshifts": { + "ORF7b": { + "14": { + "ref": "", + "alt": "frameShiftStart", + "nucPos": [] + }, + "44": { + "ref": "", + "alt": "frameShiftEnd", + "nucPos": [] + } + }, + "ORF8": { + "35": { + "ref": "", + "alt": "frameShiftStart", + "nucPos": [] + }, + "122": { + "ref": "", + "alt": "frameShiftEnd", + "nucPos": [] + } + } + } + }, + "pangoParent": { + "nuc": { + "241": { + "ref": "C", + "alt": "T" + }, + "670": { + "ref": "T", + "alt": "G" + }, + "1437": { + "ref": "C", + "alt": "T" + }, + "2790": { + "ref": "C", + "alt": "T" + }, + "3037": { + "ref": "C", + "alt": "T" + }, + "4893": { + "ref": "C", + "alt": "T" + }, + "5184": { + "ref": "C", + "alt": "T" + }, + "5584": { + "ref": "A", + "alt": "G" + }, + "6196": { + "ref": "C", + "alt": "T" + }, + "6576": { + "ref": "G", + "alt": "T" + }, + "7091": { + "ref": "G", + "alt": "A" + }, + "9073": { + "ref": "C", + "alt": "T" + }, + "9891": { + "ref": "C", + "alt": "T" + }, + "11418": { + "ref": "T", + "alt": "C" + }, + "11514": { + "ref": "C", + "alt": "T" + }, + "13019": { + "ref": "C", + "alt": "T" + }, + "14408": { + "ref": "C", + "alt": "T" + }, + "15030": { + "ref": "T", + "alt": "C" + }, + "15237": { + "ref": "C", + "alt": "T" + }, + "15451": { + "ref": "G", + "alt": "A" + }, + "16466": { + "ref": "C", + "alt": "T" + }, + "16616": { + "ref": "C", + "alt": "T" + }, + "21618": { + "ref": "C", + "alt": "T" + }, + "21635": { + "ref": "C", + "alt": "T" + }, + "21987": { + "ref": "G", + "alt": "A" + }, + "22188": { + "ref": "C", + "alt": "T" + }, + "22197": { + "ref": "T", + "alt": "C" + }, + "22205": { + "ref": "G", + "alt": "C" + }, + "22227": { + "ref": "C", + "alt": "T" + }, + "22329": { + "ref": "C", + "alt": "T" + }, + "22674": { + "ref": "C", + "alt": "T" + }, + "22679": { + "ref": "T", + "alt": "C" + }, + "22686": { + "ref": "C", + "alt": "T" + }, + "22688": { + "ref": "A", + "alt": "G" + }, + "22775": { + "ref": "G", + "alt": "A" + }, + "22786": { + "ref": "A", + "alt": "C" + }, + "22813": { + "ref": "G", + "alt": "T" + }, + "22882": { + "ref": "T", + "alt": "G" + }, + "22898": { + "ref": "G", + "alt": "A" + }, + "22916": { + "ref": "C", + "alt": "A" + }, + "22992": { + "ref": "G", + "alt": "A" + }, + "22995": { + "ref": "C", + "alt": "A" + }, + "23013": { + "ref": "A", + "alt": "C" + }, + "23018": { + "ref": "T", + "alt": "C" + }, + "23019": { + "ref": "T", + "alt": "C" + }, + "23055": { + "ref": "A", + "alt": "G" + }, + "23063": { + "ref": "A", + "alt": "T" + }, + "23075": { + "ref": "T", + "alt": "C" + }, + "23403": { + "ref": "A", + "alt": "G" + }, + "23525": { + "ref": "C", + "alt": "T" + }, + "23599": { + "ref": "T", + "alt": "G" + }, + "23604": { + "ref": "C", + "alt": "A" + }, + "23670": { + "ref": "A", + "alt": "T" + }, + "23854": { + "ref": "C", + "alt": "A" + }, + "23948": { + "ref": "G", + "alt": "T" + }, + "24424": { + "ref": "A", + "alt": "T" + }, + "24469": { + "ref": "T", + "alt": "A" + }, + "25000": { + "ref": "C", + "alt": "T" + }, + "25854": { + "ref": "C", + "alt": "T" + }, + "26767": { + "ref": "T", + "alt": "C" + }, + "26819": { + "ref": "T", + "alt": "C" + }, + "27718": { + "ref": "T", + "alt": "C" + }, + "27752": { + "ref": "C", + "alt": "T" + }, + "27890": { + "ref": "G", + "alt": "T" + }, + "27898": { + "ref": "A", + "alt": "C" + }, + "28461": { + "ref": "A", + "alt": "G" + }, + "28881": { + "ref": "G", + "alt": "T" + }, + "29402": { + "ref": "G", + "alt": "T" + }, + "29742": { + "ref": "G", + "alt": "T" + }, + "21992": { + "ref": "T", + "alt": "-" + }, + "21993": { + "ref": "A", + "alt": "-" + }, + "21994": { + "ref": "T", + "alt": "-" + }, + "22029": { + "ref": "A", + "alt": "-" + }, + "22030": { + "ref": "G", + "alt": "-" + }, + "22031": { + "ref": "T", + "alt": "-" + }, + "22032": { + "ref": "T", + "alt": "-" + }, + "22033": { + "ref": "C", + "alt": "-" + }, + "22034": { + "ref": "A", + "alt": "-" + }, + "22289": { + "ref": "G", + "alt": "-" + }, + "22290": { + "ref": "C", + "alt": "-" + }, + "22291": { + "ref": "T", + "alt": "-" + }, + "22292": { + "ref": "T", + "alt": "-" + }, + "22293": { + "ref": "T", + "alt": "-" + }, + "22294": { + "ref": "A", + "alt": "-" + }, + "27792": { + "ref": "T", + "alt": "-" + }, + "27793": { + "ref": "T", + "alt": "-" + }, + "27992": { + "ref": "T", + "alt": "-" + }, + "27993": { + "ref": "G", + "alt": "-" + }, + "28248": { + "ref": "G", + "alt": "-" + }, + "28249": { + "ref": "A", + "alt": "-" + }, + "28250": { + "ref": "T", + "alt": "-" + }, + "28251": { + "ref": "T", + "alt": "-" + }, + "28252": { + "ref": "T", + "alt": "-" + }, + "28253": { + "ref": "C", + "alt": "-" + }, + "28271": { + "ref": "A", + "alt": "-" + } + }, + "aa": { + "M": { + "82": { + "ref": "I", + "alt": "T", + "nucPos": [ + 26767 + ] + } + }, + "N": { + "63": { + "ref": "D", + "alt": "G", + "nucPos": [ + 28461 + ] + }, + "203": { + "ref": "R", + "alt": "M", + "nucPos": [ + 28881 + ] + }, + "377": { + "ref": "D", + "alt": "Y", + "nucPos": [ + 29402 + ] + } + }, + "ORF1a": { + "135": { + "ref": "S", + "alt": "R", + "nucPos": [ + 670 + ] + }, + "391": { + "ref": "S", + "alt": "F", + "nucPos": [ + 1437 + ] + }, + "842": { + "ref": "T", + "alt": "I", + "nucPos": [ + 2790 + ] + }, + "1543": { + "ref": "T", + "alt": "I", + "nucPos": [ + 4893 + ] + }, + "1640": { + "ref": "P", + "alt": "L", + "nucPos": [ + 5184 + ] + }, + "2104": { + "ref": "S", + "alt": "I", + "nucPos": [ + 6576 + ] + }, + "2276": { + "ref": "V", + "alt": "I", + "nucPos": [ + 7091 + ] + }, + "3209": { + "ref": "A", + "alt": "V", + "nucPos": [ + 9891 + ] + }, + "3718": { + "ref": "V", + "alt": "A", + "nucPos": [ + 11418 + ] + }, + "3750": { + "ref": "T", + "alt": "I", + "nucPos": [ + 11514 + ] + } + }, + "ORF1b": { + "314": { + "ref": "P", + "alt": "L", + "nucPos": [ + 14408 + ] + }, + "662": { + "ref": "G", + "alt": "S", + "nucPos": [ + 15451 + ] + }, + "1000": { + "ref": "P", + "alt": "L", + "nucPos": [ + 16466 + ] + }, + "1050": { + "ref": "T", + "alt": "I", + "nucPos": [ + 16616 + ] + } + }, + "ORF7a": { + "109": { + "ref": "F", + "alt": "L", + "nucPos": [ + 27718 + ] + }, + "120": { + "ref": "T", + "alt": "I", + "nucPos": [ + 27752 + ] + } + }, + "ORF8": { + "2": { + "ref": "K", + "alt": "T", + "nucPos": [ + 27898 + ] + }, + "33": { + "ref": "V", + "alt": "-", + "nucPos": [ + 27992 + ] + }, + "34": { + "ref": "D", + "alt": "-", + "nucPos": [ + 27993 + ] + } + }, + "ORF9b": { + "60": { + "ref": "T", + "alt": "A", + "nucPos": [ + 28461 + ] + } + }, + "S": { + "19": { + "ref": "T", + "alt": "I", + "nucPos": [ + 21618 + ] + }, + "25": { + "ref": "P", + "alt": "S", + "nucPos": [ + 21635 + ] + }, + "142": { + "ref": "G", + "alt": "D", + "nucPos": [ + 21987 + ] + }, + "158": { + "ref": "R", + "alt": "G", + "nucPos": [ + 22034 + ] + }, + "209": { + "ref": "P", + "alt": "L", + "nucPos": [ + 22188 + ] + }, + "212": { + "ref": "L", + "alt": "S", + "nucPos": [ + 22197 + ] + }, + "215": { + "ref": "D", + "alt": "H", + "nucPos": [ + 22205 + ] + }, + "222": { + "ref": "A", + "alt": "V", + "nucPos": [ + 22227 + ] + }, + "256": { + "ref": "S", + "alt": "L", + "nucPos": [ + 22329 + ] + }, + "371": { + "ref": "S", + "alt": "F", + "nucPos": [ + 22674 + ] + }, + "373": { + "ref": "S", + "alt": "P", + "nucPos": [ + 22679 + ] + }, + "375": { + "ref": "S", + "alt": "F", + "nucPos": [ + 22686 + ] + }, + "376": { + "ref": "T", + "alt": "A", + "nucPos": [ + 22688 + ] + }, + "405": { + "ref": "D", + "alt": "N", + "nucPos": [ + 22775 + ] + }, + "408": { + "ref": "R", + "alt": "S", + "nucPos": [ + 22786 + ] + }, + "417": { + "ref": "K", + "alt": "N", + "nucPos": [ + 22813 + ] + }, + "440": { + "ref": "N", + "alt": "K", + "nucPos": [ + 22882 + ] + }, + "446": { + "ref": "G", + "alt": "S", + "nucPos": [ + 22898 + ] + }, + "452": { + "ref": "L", + "alt": "M", + "nucPos": [ + 22916 + ] + }, + "477": { + "ref": "S", + "alt": "N", + "nucPos": [ + 22992 + ] + }, + "478": { + "ref": "T", + "alt": "K", + "nucPos": [ + 22995 + ] + }, + "484": { + "ref": "E", + "alt": "A", + "nucPos": [ + 23013 + ] + }, + "486": { + "ref": "F", + "alt": "P", + "nucPos": [ + 23018, + 23019 + ] + }, + "498": { + "ref": "Q", + "alt": "R", + "nucPos": [ + 23055 + ] + }, + "501": { + "ref": "N", + "alt": "Y", + "nucPos": [ + 23063 + ] + }, + "505": { + "ref": "Y", + "alt": "H", + "nucPos": [ + 23075 + ] + }, + "614": { + "ref": "D", + "alt": "G", + "nucPos": [ + 23403 + ] + }, + "655": { + "ref": "H", + "alt": "Y", + "nucPos": [ + 23525 + ] + }, + "679": { + "ref": "N", + "alt": "K", + "nucPos": [ + 23599 + ] + }, + "681": { + "ref": "P", + "alt": "H", + "nucPos": [ + 23604 + ] + }, + "703": { + "ref": "N", + "alt": "I", + "nucPos": [ + 23670 + ] + }, + "764": { + "ref": "N", + "alt": "K", + "nucPos": [ + 23854 + ] + }, + "796": { + "ref": "D", + "alt": "Y", + "nucPos": [ + 23948 + ] + }, + "954": { + "ref": "Q", + "alt": "H", + "nucPos": [ + 24424 + ] + }, + "969": { + "ref": "N", + "alt": "K", + "nucPos": [ + 24469 + ] + }, + "144": { + "ref": "Y", + "alt": "-", + "nucPos": [ + 21992, + 21993, + 21994 + ] + }, + "156": { + "ref": "E", + "alt": "-", + "nucPos": [ + 22029, + 22030 + ] + }, + "157": { + "ref": "F", + "alt": "-", + "nucPos": [ + 22032, + 22033, + 22031 + ] + }, + "242": { + "ref": "L", + "alt": "-", + "nucPos": [] + }, + "243": { + "ref": "A", + "alt": "-", + "nucPos": [ + 22289, + 22290, + 22291 + ] + } + }, + "ORF7b": { + "13": { + "ref": "F", + "alt": "-", + "nucPos": [ + 27792, + 27793 + ] + } + } + }, + "frameshifts": { + "ORF7b": { + "14": { + "ref": "", + "alt": "frameShiftStart", + "nucPos": [] + }, + "44": { + "ref": "", + "alt": "frameShiftEnd", + "nucPos": [] + } + }, + "ORF8": { + "35": { + "ref": "", + "alt": "frameShiftStart", + "nucPos": [] + }, + "122": { + "ref": "", + "alt": "frameShiftEnd", + "nucPos": [] + } + } + } + } + } +} diff --git a/web/data/definingMutationsIndex.json b/web/data/definingMutationsIndex.json new file mode 100644 index 0000000000..3e875f5f52 --- /dev/null +++ b/web/data/definingMutationsIndex.json @@ -0,0 +1,12 @@ +{ + "clusters": [ + { + "lineage": "BQ.1", + "nextstrainClade": "22E" + }, + { + "lineage": "XBC.1", + "nextstrainClade": "recombinant" + } + ] +} diff --git a/web/src/components/Common/ButtonTransparent.tsx b/web/src/components/Common/ButtonTransparent.tsx new file mode 100644 index 0000000000..2e34df1d09 --- /dev/null +++ b/web/src/components/Common/ButtonTransparent.tsx @@ -0,0 +1,42 @@ +import { Button, ButtonProps } from 'reactstrap' +import styled from 'styled-components' + +export interface ButtonTransparentProps extends ButtonProps { + height?: string + width?: string + fontSize?: string +} + +export const ButtonTransparent = styled(Button)` + width: ${(props) => props.width ?? props.height}; + height: ${(props) => props.height}; + line-height: ${(props) => props.height}; + font-size: ${(props) => props.fontSize}; + padding: 0; + margin: 4px 0; + background-color: transparent; + background-image: none; + color: ${(props) => props.theme.bodyColor}; + border: none; + border-radius: 0; + box-shadow: none; + border-image: none; + text-decoration: none; + -webkit-tap-highlight-color: #ccc; + + &.show > .btn-secondary.dropdown-toggle, + &.active, + &:active, + &:hover, + &:focus, + &:focus-within { + background-color: transparent; + background-image: none; + color: ${(props) => props.theme.bodyColor}; + border: none; + border-radius: 0; + box-shadow: none; + border-image: none; + text-decoration: none; + } +` diff --git a/web/src/components/Common/MutationBadge.tsx b/web/src/components/Common/MutationBadge.tsx index 8b433f4b1c..690106d94f 100644 --- a/web/src/components/Common/MutationBadge.tsx +++ b/web/src/components/Common/MutationBadge.tsx @@ -286,32 +286,31 @@ export interface VariantLinkBadgeProps { } export function VariantLinkBadge({ name, href, prefix }: VariantLinkBadgeProps) { - const { mutationObj, mutationStr } = useMemo(() => variantToObjectAndString(name), [name]) + const { mutationStr } = useMemo(() => variantToObjectAndString(name), [name]) const url = useMemo(() => href ?? formatVariantUrl(mutationStr), [href, mutationStr]) - - if (!mutationObj) { - return {`VariantLinkBadge: Invalid mutation: ${JSON.stringify(name)}`} - } - - if (!url) { - return ( - - { - // prettier-ignore - `VariantLinkBadge: Variant not recognized: ${JSON.stringify(name)}.` + - `Known variants: ${clusterNames.join(", ")}` - } - - ) - } - return ( - + ) } +export interface VariantBadgeProps { + name: string | Mutation + prefix?: string +} + +export function VariantBadge({ name, prefix }: VariantBadgeProps) { + const mutationObj = useMemo(() => { + const { mutationObj } = variantToObjectAndString(name) + if (!mutationObj) { + return { parent: mutationObj } + } + return mutationObj + }, [name]) + return +} + export interface LineageLinkBadgeProps { name: string href?: string @@ -320,38 +319,50 @@ export interface LineageLinkBadgeProps { } export function LineageLinkBadge({ name, href, prefix, report }: LineageLinkBadgeProps) { - const { t } = useTranslationSafe() - const url = useMemo( // prettier-ignore - () => (href ?? (report ? `https://cov-lineages.org/global_report_${name}.html` : "")), + () => (href ?? (report ? `https://cov-lineages.org/global_report_${name}.html` : '')), [href, report, name], ) + + return ( + + + + ) +} + +export interface LineageBadgeProps { + name: string + prefix?: string +} + +export function LineageBadge({ name, prefix }: LineageBadgeProps) { + const { t } = useTranslationSafe() + const tooltip = useMemo(() => { const text = t('Pango Lineage') return `${text} '${name}'` }, [name, t]) return ( - - - - {prefix && {prefix}} - - {name} - - - - + + + {prefix && {prefix}} + + {name} + + + ) } diff --git a/web/src/components/Common/NameTable.tsx b/web/src/components/Common/NameTable.tsx index b8537b0a20..709d0178e5 100644 --- a/web/src/components/Common/NameTable.tsx +++ b/web/src/components/Common/NameTable.tsx @@ -3,6 +3,7 @@ import React, { ReactNode, useMemo } from 'react' import { Table as TableBase, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table' import styled from 'styled-components' +import { joinWithCommas } from 'src/helpers/join' import { useTranslationSafe } from 'src/helpers/useTranslationSafe' import { LinkExternal } from 'src/components/Link/LinkExternal' import type { NameTableDatum, NameTableEntry } from 'src/io/getNameTable' @@ -47,14 +48,6 @@ const Table = styled(TableBase)` } ` -export function joinWithCommas(elems: ReactNode[]): ReactNode { - if (elems.length === 0) { - return ' ' - } - - return elems.reduce((prev, curr) => [prev, ', ', curr]) -} - export interface NameTableEntryProps { entry: NameTableEntry } diff --git a/web/src/components/Common/SearchBox.tsx b/web/src/components/Common/SearchBox.tsx new file mode 100644 index 0000000000..e7fec01d5b --- /dev/null +++ b/web/src/components/Common/SearchBox.tsx @@ -0,0 +1,96 @@ +import React, { ChangeEvent, useCallback, useMemo, HTMLProps } from 'react' +import styled from 'styled-components' +import { Form, Input as InputBase } from 'reactstrap' +import { MdSearch as IconSearchBase, MdClear as IconClearBase } from 'react-icons/md' +import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import { ButtonTransparent } from 'src/components/Common/ButtonTransparent' + +const SearchForm = styled(Form)` + display: inline; + position: relative; +` + +const IconSearchWrapper = styled.span` + display: inline; + position: absolute; + padding: 5px 7px; +` + +const IconSearch = styled(IconSearchBase)` + * { + color: ${(props) => props.theme.gray500}; + } +` + +const ButtonClear = styled(ButtonTransparent)` + display: inline; + position: absolute; + right: 0; + padding: 0 7px; +` + +const IconClear = styled(IconClearBase)` + * { + color: ${(props) => props.theme.gray500}; + } +` + +const Input = styled(InputBase)` + display: inline !important; + padding-left: 35px; + padding-right: 30px; + height: 2.2em; +` + +export interface TableSearchBoxProps extends Omit, 'as'> { + searchTitle?: string + searchTerm: string + onSearchTermChange(term: string): void +} + +export function SearchBox({ searchTitle, searchTerm, onSearchTermChange, ...restProps }: TableSearchBoxProps) { + const { t } = useTranslationSafe() + + const onChange = useCallback( + (event: ChangeEvent) => { + onSearchTermChange(event.target.value) + }, + [onSearchTermChange], + ) + + const onClear = useCallback(() => { + onSearchTermChange('') + }, [onSearchTermChange]) + + const buttonClear = useMemo(() => { + if (searchTerm.length === 0) { + return null + } + return ( + + + + ) + }, [onClear, searchTerm.length, t]) + + return ( + + + + + + {buttonClear} + + ) +} diff --git a/web/src/components/Common/parsePosition.ts b/web/src/components/Common/parsePosition.ts index bc27105b36..8f688c7669 100644 --- a/web/src/components/Common/parsePosition.ts +++ b/web/src/components/Common/parsePosition.ts @@ -11,3 +11,11 @@ export function parsePosition(raw: string | undefined | null) { return num } + +export function parsePositionOrThrow(raw: string | undefined | null) { + const pos = parsePosition(raw) + if (!pos) { + throw new Error(`Unable to parse mutation posiiton: '${JSON.stringify(raw)}'`) + } + return pos +} diff --git a/web/src/components/DefiningMutations/DefMutLineageTitle.tsx b/web/src/components/DefiningMutations/DefMutLineageTitle.tsx new file mode 100644 index 0000000000..77caff2591 --- /dev/null +++ b/web/src/components/DefiningMutations/DefMutLineageTitle.tsx @@ -0,0 +1,45 @@ +import React, { useMemo } from 'react' +import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import type { DefMutClusterDatum } from 'src/io/getDefiningMutationsClusters' + +import styled from 'styled-components' + +const VariantTitleWrapper = styled.header` + text-align: center; + min-height: 90px; +` + +const ClusterNameTitle = styled.h1`` + +const ClusterNameSubtitle = styled.p` + margin-bottom: 0; + text-align: center; +` + +export interface DefMutLineageTitleProps { + cluster: DefMutClusterDatum +} + +export function DefMutLineageTitle({ cluster: { lineage, nextstrainClade } }: DefMutLineageTitleProps) { + const { t } = useTranslationSafe() + + const subtitle = useMemo(() => { + if (nextstrainClade === 'recombinant') { + return null + } + + return ( + + {t(`also known as clade `)} + {nextstrainClade} + + ) + }, [nextstrainClade, t]) + + return ( + + {`Defining mutations for ${lineage}`} + {subtitle} + + ) +} diff --git a/web/src/components/DefiningMutations/DefiningMutationsClusterIndexTable.tsx b/web/src/components/DefiningMutations/DefiningMutationsClusterIndexTable.tsx new file mode 100644 index 0000000000..cdbe9a710a --- /dev/null +++ b/web/src/components/DefiningMutations/DefiningMutationsClusterIndexTable.tsx @@ -0,0 +1,186 @@ +import { isEmpty } from 'lodash' +import { transparentize } from 'polished' +import React, { useMemo, useState } from 'react' +import { Col, Row } from 'reactstrap' +import styled from 'styled-components' +import { AMINOACID_COLORS } from 'src/colors' +import { search } from 'src/helpers/search' +import { Table as TableBase, Thead, Tbody, Tr, Th, Td } from 'react-super-responsive-table' +import { + variantToObjectAndString, + MutationBadge, + LinkUnstyled, + LineageBadge, +} from 'src/components/Common/MutationBadge' +import { DefMutClusterIndexDatum } from 'src/io/getDefiningMutationsClusters' +import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import { SearchBox } from 'src/components/Common/SearchBox' + +export const Table = styled(TableBase)` + margin-left: auto; + margin-right: auto; + border-collapse: collapse; + + & > thead > tr, + & > tbody > tr, + & > tbody > td { + border: #aaa solid 1px; + border-collapse: collapse; + } + + & > tbody > tr:nth-child(even) { + background-color: white; + } + + & > tbody > tr:nth-child(odd) { + background-color: #f5f5f5; + } + + & > thead > tr > th { + font-size: 0.9rem; + text-align: center; + height: 3rem; + border: #aaa solid 1px; + } + + & > tbody > tr:hover { + background-color: ${(props) => transparentize(0.8)(props.theme.green)}; + } + + & > tbody > tr > td { + font-family: ${(props) => props.theme.font.monospace}; + font-size: 0.8rem; + text-align: left; + border: #aaa solid 1px; + min-width: 100px; + padding: 2px; + } +` + +export interface DefiningMutationsClusterIndexTableRowProps { + cluster: DefMutClusterIndexDatum +} + +export function DefiningMutationsClusterIndexTableRow({ cluster }: DefiningMutationsClusterIndexTableRowProps) { + const { lineage, nextstrainClade } = cluster + + const variant = useMemo(() => { + const { mutationObj: variant } = variantToObjectAndString(nextstrainClade) + if (!variant) { + return { parent: nextstrainClade } + } + return variant + }, [nextstrainClade]) + + return ( + + + + + + + + + + + + + + + + + ) +} + +export function DefiningMutationsClusterIndexTable({ clusters }: { clusters: DefMutClusterIndexDatum[] }) { + const { t } = useTranslationSafe() + + const rows = useMemo(() => { + if (isEmpty(clusters)) { + return ( + + +
+

{t('Nothing found')}

+
+ + + ) + } + return clusters.map((cluster) => ) + }, [clusters, t]) + + return ( + + + + + + + + {rows} +
{t('{{nextstrain}} Clade', { nextstrain: 'Nextstrain' })}{t('Pango Lineage')}
+ ) +} + +export function DefiningMutationsClusterIndexTableWithSearch({ clusters }: { clusters: DefMutClusterIndexDatum[] }) { + const { t } = useTranslationSafe() + const [searchTerm, setSearchTerm] = useState('') + + const clustersFiltered = useMemo(() => { + if (searchTerm.trim().length === 0) { + return clusters + } + + const { itemsStartWith, itemsInclude } = search(clusters, searchTerm, (cluster) => [ + cluster.lineage, + cluster.nextstrainClade, + ]) + + return [...itemsStartWith, ...itemsInclude] + }, [clusters, searchTerm]) + + return ( + + + + + + + + + + + + + + + + +

+ {t('Showing {{num}}/{{total}} rows', { + num: clustersFiltered.length, + total: clusters.length, + })} +

+ +
+
+
+ ) +} + +const WrapperOuter = styled.div` + display: flex; +` + +const WrapperInner = styled.div` + display: flex; + flex-direction: column; + margin: 0 auto; + width: 500px; +` diff --git a/web/src/components/DefiningMutations/DefiningMutationsIndexPage.tsx b/web/src/components/DefiningMutations/DefiningMutationsIndexPage.tsx new file mode 100644 index 0000000000..15cf31293d --- /dev/null +++ b/web/src/components/DefiningMutations/DefiningMutationsIndexPage.tsx @@ -0,0 +1,29 @@ +import React from 'react' +import { Col, Row } from 'reactstrap' +import { NarrowPageContainer } from 'src/components/Common/ClusterSidebarLayout' +import { DefiningMutationsClusterIndexTableWithSearch } from 'src/components/DefiningMutations/DefiningMutationsClusterIndexTable' +import { Layout } from 'src/components/Layout/Layout' +import { MdxContent } from 'src/i18n/getMdxContent' +import { getDefMutClusters } from 'src/io/getDefiningMutationsClusters' + +const clusters = getDefMutClusters() + +export default function DefiningMutationsIndexPage() { + return ( + + + + + + + + + + + + + + + + ) +} diff --git a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx new file mode 100644 index 0000000000..8c953e2769 --- /dev/null +++ b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx @@ -0,0 +1,402 @@ +import unique from 'fork-ts-checker-webpack-plugin/lib/utils/array/unique' +import React, { ChangeEvent, useCallback, useMemo, useState } from 'react' +import { get } from 'lodash' +import { Col, Row, Input, Label, Table, Form as FormBase, FormGroup as FormGroupBase } from 'reactstrap' +import { AaMut, LineageBadge, NucMut, VariantBadge } from 'src/components/Common/MutationBadge' +import { parsePositionOrThrow } from 'src/components/Common/parsePosition' +import { LinkSmart } from 'src/components/Link/LinkSmart' +import { joinWithCommas } from 'src/helpers/join' +import styled from 'styled-components' +import { TableSlimWithBorders } from 'src/components/Common/TableSlim' +import { DefMutLineageTitle } from 'src/components/DefiningMutations/DefMutLineageTitle' +import { useRouter } from 'next/router' +import { useTranslationSafe } from 'src/helpers/useTranslationSafe' +import { MdxContent } from 'src/i18n/getMdxContent' +import { + useDefMutCluster, + getDefMutClusterRedirects, + DefMutClusterDatum, + DefMutAa, + DefMutNuc, +} from 'src/io/getDefiningMutationsClusters' +import { Layout } from 'src/components/Layout/Layout' +import { NarrowPageContainer } from 'src/components/Common/ClusterSidebarLayout' + +const clusterRedirects = getDefMutClusterRedirects() + +export function useCurrentClusterName(clusterName?: string) { + const router = useRouter() + + if (clusterName) { + const clusterNewName = get(clusterRedirects, clusterName) + if (clusterNewName) { + void router.replace(`/defining-mutations/${clusterNewName}`) // eslint-disable-line no-void + return clusterNewName + } + } + + if (!clusterName) { + throw new Error(`Clade or lineage not found`) + } + + return clusterName +} + +export interface DefiningMutationsPageProps { + clusterName?: string +} + +export default function DefiningMutationsPage({ clusterName: clusterNameUnsafe }: DefiningMutationsPageProps) { + const clusterName = useCurrentClusterName(clusterNameUnsafe) + const currentCluster = useDefMutCluster(clusterName) + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export const InfoTable = styled(Table)` + max-width: 1000px; + + & td { + padding: 0.25rem 0.5rem; + min-width: 200px; + } + + * { + border: none !important; + } +` + +export interface DefiningMutationsInfoProps { + currentCluster: DefMutClusterDatum +} + +export function DefiningMutationsInfo({ currentCluster }: DefiningMutationsInfoProps) { + const { t } = useTranslationSafe() + + const cladeSafe = encodeURI(currentCluster.nextstrainClade) + const lineageSafe = encodeURI(currentCluster.lineage) + + const urlPerCountry = `/per-country?variant=${cladeSafe}` + const urlPerVariant = `/per-variant?variant=${cladeSafe}` + const urlNextstrainClade = `https://nextstrain.org/ncov/gisaid/global/6m?f_clade_membership=${cladeSafe}` + const urlCovSpectrumLineage = `https://cov-spectrum.org/explore/World/AllSamples/Past6M/variants?nextcladePangoLineage=${lineageSafe}` + const urlCovSpectrumClade = `https://cov-spectrum.org/explore/World/AllSamples/Past6M/variants?nextstrainClade=${cladeSafe}` + + return ( + + + + {t('Nextstrain clade')} + {currentCluster.nextstrainClade ? : 'none'} + + + {t('Unaliased lineage')} + {currentCluster.unaliased ? : 'none'} + + + {t('Parent lineage')} + {currentCluster.parent ? : 'none'} + + + {t('Child lineages')} + + {joinWithCommas( + (currentCluster.children ?? ['none']).map((child) => ), + )} + + + + {t('Designation date')} + {currentCluster.designationDate} + + + {t('Designation issue')} + {currentCluster.designationIssue} + + + {t('Links')} + +
    +
  • + + {t('CoVariants - Per country - Clade {{name}}', { name: currentCluster.nextstrainClade })} + +
  • +
  • + + {t('CoVariants - Per variant - Clade {{name}}', { name: currentCluster.nextstrainClade })} + +
  • +
  • + + {t('Nextstrain - Clade {{name}}', { name: currentCluster.nextstrainClade })} + +
  • +
  • + + {t('CoV Spectrum - Clade {{name}}', { name: currentCluster.nextstrainClade })} + +
  • +
  • + + {t('CoV Spectrum - Lineage {{name}}', { name: currentCluster.lineage })} + +
  • +
+ + + +
+ ) +} + +const Ul = styled.ul` + list-style: none; + padding: 0; + margin-top: 0.5rem; +` + +const Li = styled.li` + margin: 0; +` + +export interface DefiningMutationsTableWithTargetsProps { + currentCluster: DefMutClusterDatum +} + +export function DefiningMutationsTableWithTargets({ currentCluster }: DefiningMutationsTableWithTargetsProps) { + const targetIds = Object.keys(currentCluster.mutations) + const [currentTargetId, setCurrentTargetId] = useState(targetIds[0]) + + if (!currentTargetId) { + return null + } + + return ( + + + + + + + + + + + + + + + + ) +} + +export interface DropdownComparisonTargetProps { + targetIds: string[] + currentTargetId: string + setCurrentTargetId(currentTargetId: string): void +} + +export function ComparisonTargetDropdown({ + targetIds, + currentTargetId, + setCurrentTargetId, +}: DropdownComparisonTargetProps) { + const { t } = useTranslationSafe() + + const options = useMemo( + () => + targetIds.map((id) => ( + + )), + [targetIds], + ) + + const onChange = useCallback( + (event: ChangeEvent) => { + setCurrentTargetId(event.target.value) + }, + [setCurrentTargetId], + ) + + return ( +
+ + + +
+ ) +} + +export const FormGroup = styled(FormGroupBase)`` + +export const Form = styled(FormBase)`` + +export interface DefiningMutationsTableProps { + currentCluster: DefMutClusterDatum + comparisonTargetName: string +} + +export function DefiningMutationsTable({ currentCluster, comparisonTargetName }: DefiningMutationsTableProps) { + const { t } = useTranslationSafe() + + const rows = useMemo(() => { + const mutations = currentCluster.mutations[comparisonTargetName] + if (!mutations) { + return [] + } + + const allNucMuts: DefMutNuc[] = Object.entries(mutations.nuc).map(([posStr, nucMut]) => { + const pos = parsePositionOrThrow(posStr) + return { pos, ...nucMut } + }) + + const aaMuts: DefMutAa[] = Object.entries(mutations.aa).flatMap(([gene, aaMuts]) => + Object.entries(aaMuts).map(([posStr, aaMut]) => { + const pos = parsePositionOrThrow(posStr) + const nucMuts = allNucMuts.filter((nucMut) => aaMut.nucPos?.includes(nucMut.pos)) + return { gene, pos, ...aaMut, nucMuts } + }), + ) + + const codingPositions = unique(aaMuts.flatMap((aaMut) => aaMut.nucPos)) + const silentNucMuts = allNucMuts.filter((nucMut) => !codingPositions.includes(nucMut.pos)) + + const silentRows = silentNucMuts.map((nucMut) => ( + + )) + + const codingRows = aaMuts.map((aaMut) => ( + + )) + + return [...codingRows, ...silentRows] + }, [comparisonTargetName, currentCluster.mutations]) + + if (!rows) { + return null + } + + return ( + + + + {t('AA Notes')} + {t('AA Mut')} + {t('Nuc Mut')} + {t('Nuc Notes')} + + + {rows} + + ) +} + +export interface DefiningMutationsTableRowSilentProps { + nucMut: DefMutNuc +} + +export function DefiningMutationsTableRowSilent({ nucMut }: DefiningMutationsTableRowSilentProps) { + const mstr = nucMutTostring(nucMut) + return ( + + + + + + + {nucMut.annotation} + + ) +} + +export interface DefiningMutationsTableRowCodingProps { + aaMut: DefMutAa +} + +export function DefiningMutationsTableRowCoding({ aaMut }: DefiningMutationsTableRowCodingProps) { + const numNucMuts = aaMut.nucMuts.length + + const components = useMemo( + () => + (aaMut.nucMuts ?? []).map((m, i) => { + const mstr = nucMutTostring(m) + return ( + + {i === 0 && ( + <> + {aaMut.annotation} + + + + + )} + + + + + {m.annotation} + + ) + }), + [aaMut, numNucMuts], + ) + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{components} +} + +const DefiningMutationsTableTdNarrow = styled.td` + width: 10%; +` + +const DefiningMutationsTableTd = styled.td` + width: 45%; +` + +function nucMutTostring({ ref, pos, alt }: DefMutNuc) { + return `${ref}${pos}${alt}` +} + +function aaMutToString({ gene, ref, pos, alt }: DefMutAa) { + return `${gene}:${ref}${pos}${alt}` +} diff --git a/web/src/components/Layout/NavigationBar.tsx b/web/src/components/Layout/NavigationBar.tsx index 1483324272..d786ab93b9 100644 --- a/web/src/components/Layout/NavigationBar.tsx +++ b/web/src/components/Layout/NavigationBar.tsx @@ -126,6 +126,7 @@ export function NavigationBar() { '/per-variant': t('Per variant'), '/cases': t('Cases'), '/shared-mutations': t('Shared Mutations'), + '/defining-mutations': t('Defining Mutations'), '/acknowledgements': t('Acknowledgements'), } if (process.env.NODE_ENV === 'development' || process.env.DOMAIN?.includes('vercel')) { diff --git a/web/src/components/Loading/Loading.tsx b/web/src/components/Loading/Loading.tsx index a59824d311..73bdb5022a 100644 --- a/web/src/components/Loading/Loading.tsx +++ b/web/src/components/Loading/Loading.tsx @@ -45,3 +45,5 @@ function Loading() { } export default Loading + +export const LOADING = diff --git a/web/src/content/en/DefiningMutationsIndexIntro.mdx b/web/src/content/en/DefiningMutationsIndexIntro.mdx new file mode 100644 index 0000000000..ab32d14fb2 --- /dev/null +++ b/web/src/content/en/DefiningMutationsIndexIntro.mdx @@ -0,0 +1,5 @@ +import { LinkExternal } from 'src/components/Link/LinkExternal' + +Here you can view the defining mutations of all designated Pango lineages, as well as download machine-readable files of the same mutations. + +Please note most of these are computationally generated, please check carefully and let us know if there's a mistake, so we can correct it! diff --git a/web/src/content/en/DefiningMutationsVariantIntro.mdx b/web/src/content/en/DefiningMutationsVariantIntro.mdx new file mode 100644 index 0000000000..4972aed3a3 --- /dev/null +++ b/web/src/content/en/DefiningMutationsVariantIntro.mdx @@ -0,0 +1,3 @@ +Disclaimer text for the data + +Note Nextclade parent may change as new Nextstrain clades are designated. diff --git a/web/src/helpers/join.tsx b/web/src/helpers/join.tsx new file mode 100644 index 0000000000..de2d6ada63 --- /dev/null +++ b/web/src/helpers/join.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import type { ReactNode } from 'react' + +export function joinWithCommas(elems: ReactNode[]): ReactNode { + if (elems.length === 0) { + return ' ' + } + + return elems.reduce((prev, curr) => [prev, ', ', curr]) +} + +export function joinWithNewlines(elems: ReactNode[]): ReactNode { + if (elems.length === 0) { + return ' ' + } + + // eslint-disable-next-line react/no-array-index-key + return elems.reduce((prev, curr, i) => [prev,
, curr]) +} diff --git a/web/src/helpers/search.ts b/web/src/helpers/search.ts new file mode 100644 index 0000000000..cc5647d23b --- /dev/null +++ b/web/src/helpers/search.ts @@ -0,0 +1,20 @@ +import { partition } from 'lodash' + +export function includesLowerCase(candidate: string, searchTerm: string): boolean { + return candidate.toLowerCase().includes(searchTerm.toLowerCase()) +} + +export function startsWithLowerCase(candidate: string, searchTerm: string): boolean { + return candidate.toLowerCase().startsWith(searchTerm.toLowerCase()) +} + +/** Parition array in 3 parts: items starting with term, items including term and items not including term */ +export function search(items: T[], term: string, getter: (item: T) => string[]) { + const [itemsStartWith, itemsNotStartWith] = partition(items, (item) => + getter(item).some((candidate) => startsWithLowerCase(candidate, term)), + ) + const [itemsInclude, itemsNotInclude] = partition(itemsNotStartWith, (item) => + getter(item).some((candidate) => includesLowerCase(candidate, term)), + ) + return { itemsStartWith, itemsInclude, itemsNotInclude } +} diff --git a/web/src/helpers/useToggle.ts b/web/src/helpers/useToggle.ts new file mode 100644 index 0000000000..87eabe9fb1 --- /dev/null +++ b/web/src/helpers/useToggle.ts @@ -0,0 +1,18 @@ +import { useCallback, useState } from 'react' +import { RecoilState, useRecoilState } from 'recoil' + +export function useToggle(initialState = false) { + const [state, setState] = useState(initialState) + const toggle = useCallback(() => setState((state) => !state), []) + const enable = useCallback(() => setState(true), []) + const disable = useCallback(() => setState(false), []) + return { state, setState, toggle, enable, disable } +} + +export function useRecoilToggle(recoilState: RecoilState) { + const [state, setState] = useRecoilState(recoilState) + const toggle = useCallback(() => setState((state) => !state), [setState]) + const enable = useCallback(() => setState(true), [setState]) + const disable = useCallback(() => setState(false), [setState]) + return { state, setState, toggle, enable, disable } +} diff --git a/web/src/io/getDefiningMutationsClusters.ts b/web/src/io/getDefiningMutationsClusters.ts new file mode 100644 index 0000000000..bed3b5d05a --- /dev/null +++ b/web/src/io/getDefiningMutationsClusters.ts @@ -0,0 +1,99 @@ +import { useMemo } from 'react' +import { useQuery } from 'react-query' +import clustersJson from 'src/../data/definingMutationsIndex.json' + +export interface DefMutClusterIndexDatum { + lineage: string + nextstrainClade: string +} + +export interface DefMutNucRaw { + ref: string + alt: string + annotation: string +} + +export interface DefMutNuc { + ref: string + pos: number + alt: string + annotation: string +} + +export interface DefMutAaRaw { + ref: string + alt: string + nucPos: number[] + annotation: string +} + +export interface DefMutAa { + gene: string + ref: string + pos: number + alt: string + nucPos: number[] + nucMuts: DefMutNuc[] + annotation: string +} + +export interface DefiningMutations { + nuc: Record + aa: Record> +} + +export interface DefMutClusterDatum { + lineage: string + unaliased?: string + parent?: string + children?: string[] + nextstrainClade: string + frameShifts?: [] + designationDate: string + designationIssue?: string + mutations: Record +} + +export function getDefMutClusters(): DefMutClusterIndexDatum[] { + return clustersJson.clusters as DefMutClusterIndexDatum[] +} + +export function useDefMutCluster(clusterName: string): DefMutClusterDatum { + const res = useQuery( + ['definingMutations', clusterName], + async () => import(`src/../data/definingMutations/${clusterName}.json`), + { + staleTime: Number.POSITIVE_INFINITY, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: true, + refetchInterval: Number.POSITIVE_INFINITY, + suspense: true, + retry: 1, + }, + ) + return useMemo(() => { + if (!res.data) { + throw new Error(`Data not found for clade or lineage '${clusterName}'`) + } + return res.data + }, [clusterName, res.data]) +} + +export function getDefMutClusterRedirects(): Record { + return Object.fromEntries( + getDefMutClusters() + .filter((cluster) => cluster.nextstrainClade !== 'recombinant') + .map((cluster) => [cluster.nextstrainClade, cluster.lineage]), + ) +} + +export function getDefMutClusterLineages() { + return getDefMutClusters().map((cluster) => cluster.lineage) +} + +export function getDefMutClusterClades() { + return getDefMutClusters() + .filter((cluster) => cluster.nextstrainClade !== 'recombinant') + .map((cluster) => cluster.nextstrainClade) +} diff --git a/web/src/pages/defining-mutations/[clusterName].tsx b/web/src/pages/defining-mutations/[clusterName].tsx new file mode 100644 index 0000000000..95cdcdfb3c --- /dev/null +++ b/web/src/pages/defining-mutations/[clusterName].tsx @@ -0,0 +1,32 @@ +import type { GetStaticPathsContext, GetStaticPropsContext, GetStaticPathsResult, GetStaticPropsResult } from 'next' +import { get } from 'lodash' +import dynamic from 'next/dynamic' +import type { DefiningMutationsPageProps } from 'src/components/DefiningMutations/DefiningMutationsPage' +import { takeFirstMaybe } from 'src/helpers/takeFirstMaybe' +import { getDefMutClusterClades, getDefMutClusterLineages } from 'src/io/getDefiningMutationsClusters' + +const lineages = getDefMutClusterLineages() +const clades = getDefMutClusterClades() + +export async function getStaticProps( + context: GetStaticPropsContext, +): Promise> { + const clusterName = takeFirstMaybe(get(context?.params, 'clusterName')) + + return { + props: { + clusterName, + }, + } +} + +export async function getStaticPaths(_0: GetStaticPathsContext): Promise { + return { + paths: [...lineages, ...clades].map((clusterName) => `/defining-mutations/${clusterName}`), + fallback: false, + } +} + +export default dynamic(async () => import('src/components/DefiningMutations/DefiningMutationsPage'), { + ssr: false, +}) diff --git a/web/src/pages/defining-mutations/index.tsx b/web/src/pages/defining-mutations/index.tsx new file mode 100644 index 0000000000..7b9b1218ed --- /dev/null +++ b/web/src/pages/defining-mutations/index.tsx @@ -0,0 +1,5 @@ +import dynamic from 'next/dynamic' + +export default dynamic(async () => import('src/components/DefiningMutations/DefiningMutationsIndexPage'), { + ssr: false, +}) diff --git a/web/src/theme.ts b/web/src/theme.ts index 939130493c..d4f1f8986d 100644 --- a/web/src/theme.ts +++ b/web/src/theme.ts @@ -172,6 +172,8 @@ export const clusters = { } export const theme = { + bodyColor: basicColors.gray700, + bodyBg: basicColors.white, ...basicColors, ...themeColors, ...gridBreakpoints, From 56a9f431ebb6e69e055c9ae3d931582317a09523 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Tue, 15 Aug 2023 07:04:02 +0200 Subject: [PATCH 02/11] refactor: lint --- .../components/DefiningMutations/DefiningMutationsPage.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx index 8c953e2769..d37f62b757 100644 --- a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx +++ b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx @@ -312,10 +312,6 @@ export function DefiningMutationsTable({ currentCluster, comparisonTargetName }: return [...codingRows, ...silentRows] }, [comparisonTargetName, currentCluster.mutations]) - if (!rows) { - return null - } - return ( From cdce59afccd14add8aaa11115617b8a81ac55712 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 9 Nov 2023 00:34:35 +0100 Subject: [PATCH 03/11] feat(web): display friendly nextstrain clades --- .../DefiningMutationsClusterIndexTable.tsx | 6 +++++- web/src/io/getClusters.ts | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/web/src/components/DefiningMutations/DefiningMutationsClusterIndexTable.tsx b/web/src/components/DefiningMutations/DefiningMutationsClusterIndexTable.tsx index cdbe9a710a..94af317691 100644 --- a/web/src/components/DefiningMutations/DefiningMutationsClusterIndexTable.tsx +++ b/web/src/components/DefiningMutations/DefiningMutationsClusterIndexTable.tsx @@ -2,6 +2,7 @@ import { isEmpty } from 'lodash' import { transparentize } from 'polished' import React, { useMemo, useState } from 'react' import { Col, Row } from 'reactstrap' +import { getClusters } from 'src/io/getClusters' import styled from 'styled-components' import { AMINOACID_COLORS } from 'src/colors' import { search } from 'src/helpers/search' @@ -16,6 +17,8 @@ import { DefMutClusterIndexDatum } from 'src/io/getDefiningMutationsClusters' import { useTranslationSafe } from 'src/helpers/useTranslationSafe' import { SearchBox } from 'src/components/Common/SearchBox' +const clusters = getClusters() + export const Table = styled(TableBase)` margin-left: auto; margin-right: auto; @@ -65,7 +68,8 @@ export function DefiningMutationsClusterIndexTableRow({ cluster }: DefiningMutat const { lineage, nextstrainClade } = cluster const variant = useMemo(() => { - const { mutationObj: variant } = variantToObjectAndString(nextstrainClade) + const cluster = clusters.find((cluster) => cluster.nextstrain_name === nextstrainClade) + const { mutationObj: variant } = variantToObjectAndString(cluster?.display_name ?? nextstrainClade) if (!variant) { return { parent: nextstrainClade } } diff --git a/web/src/io/getClusters.ts b/web/src/io/getClusters.ts index 052404233d..6c0fcdafc8 100644 --- a/web/src/io/getClusters.ts +++ b/web/src/io/getClusters.ts @@ -22,6 +22,7 @@ export type ClusterDatum = { col: string display_name: string alt_display_name?: string[] + nextstrain_name?: string snps: number[] mutations?: { nonsynonymous?: Mutation[] From 97e31fd10ec9d14ab6e54e2a89b1ed99c8b4d675 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 9 Nov 2023 00:35:30 +0100 Subject: [PATCH 04/11] feat(web): remove child lineages --- .../DefiningMutations/DefiningMutationsPage.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx index d37f62b757..e3693ca752 100644 --- a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx +++ b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx @@ -125,14 +125,6 @@ export function DefiningMutationsInfo({ currentCluster }: DefiningMutationsInfoP {t('Parent lineage')} {currentCluster.parent ? : 'none'} - - {t('Child lineages')} - - {joinWithCommas( - (currentCluster.children ?? ['none']).map((child) => ), - )} - - {t('Designation date')} {currentCluster.designationDate} From a996beb1434c6d0590b8d4307fd9bd53b95c973d Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 9 Nov 2023 00:38:33 +0100 Subject: [PATCH 05/11] fix(web): avoid var shadowing --- .../DefiningMutations/DefiningMutationsClusterIndexTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/components/DefiningMutations/DefiningMutationsClusterIndexTable.tsx b/web/src/components/DefiningMutations/DefiningMutationsClusterIndexTable.tsx index 94af317691..98a2710733 100644 --- a/web/src/components/DefiningMutations/DefiningMutationsClusterIndexTable.tsx +++ b/web/src/components/DefiningMutations/DefiningMutationsClusterIndexTable.tsx @@ -68,8 +68,8 @@ export function DefiningMutationsClusterIndexTableRow({ cluster }: DefiningMutat const { lineage, nextstrainClade } = cluster const variant = useMemo(() => { - const cluster = clusters.find((cluster) => cluster.nextstrain_name === nextstrainClade) - const { mutationObj: variant } = variantToObjectAndString(cluster?.display_name ?? nextstrainClade) + const cl = clusters.find((cl) => cl.nextstrain_name === nextstrainClade) + const { mutationObj: variant } = variantToObjectAndString(cl?.display_name ?? nextstrainClade) if (!variant) { return { parent: nextstrainClade } } From 4294fd8552dd25597f6c1571f3c8fa3cb05a27e9 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 9 Nov 2023 01:01:55 +0100 Subject: [PATCH 06/11] feat(web): link badges to cov pages, remove extra links --- .../DefiningMutationsPage.tsx | 69 +++++-------------- web/src/io/getDefiningMutationsClusters.ts | 21 ++++-- 2 files changed, 32 insertions(+), 58 deletions(-) diff --git a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx index e3693ca752..c282c929bf 100644 --- a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx +++ b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx @@ -2,11 +2,11 @@ import unique from 'fork-ts-checker-webpack-plugin/lib/utils/array/unique' import React, { ChangeEvent, useCallback, useMemo, useState } from 'react' import { get } from 'lodash' import { Col, Row, Input, Label, Table, Form as FormBase, FormGroup as FormGroupBase } from 'reactstrap' -import { AaMut, LineageBadge, NucMut, VariantBadge } from 'src/components/Common/MutationBadge' +import { AaMut, LineageBadge, NucMut, VariantLinkBadge } from 'src/components/Common/MutationBadge' import { parsePositionOrThrow } from 'src/components/Common/parsePosition' import { LinkSmart } from 'src/components/Link/LinkSmart' -import { joinWithCommas } from 'src/helpers/join' import styled from 'styled-components' +import { getClusters } from 'src/io/getClusters' import { TableSlimWithBorders } from 'src/components/Common/TableSlim' import { DefMutLineageTitle } from 'src/components/DefiningMutations/DefMutLineageTitle' import { useRouter } from 'next/router' @@ -23,6 +23,7 @@ import { Layout } from 'src/components/Layout/Layout' import { NarrowPageContainer } from 'src/components/Common/ClusterSidebarLayout' const clusterRedirects = getDefMutClusterRedirects() +const clusters = getClusters() export function useCurrentClusterName(clusterName?: string) { const router = useRouter() @@ -82,15 +83,11 @@ export default function DefiningMutationsPage({ clusterName: clusterNameUnsafe } } export const InfoTable = styled(Table)` - max-width: 1000px; + flex: 0; + max-width: 600px; & td { padding: 0.25rem 0.5rem; - min-width: 200px; - } - - * { - border: none !important; } ` @@ -101,21 +98,21 @@ export interface DefiningMutationsInfoProps { export function DefiningMutationsInfo({ currentCluster }: DefiningMutationsInfoProps) { const { t } = useTranslationSafe() - const cladeSafe = encodeURI(currentCluster.nextstrainClade) const lineageSafe = encodeURI(currentCluster.lineage) - - const urlPerCountry = `/per-country?variant=${cladeSafe}` - const urlPerVariant = `/per-variant?variant=${cladeSafe}` - const urlNextstrainClade = `https://nextstrain.org/ncov/gisaid/global/6m?f_clade_membership=${cladeSafe}` const urlCovSpectrumLineage = `https://cov-spectrum.org/explore/World/AllSamples/Past6M/variants?nextcladePangoLineage=${lineageSafe}` - const urlCovSpectrumClade = `https://cov-spectrum.org/explore/World/AllSamples/Past6M/variants?nextstrainClade=${cladeSafe}` return ( - + {t('Nextstrain clade')} - {currentCluster.nextstrainClade ? : 'none'} + + {currentCluster.cluster?.display_name ? ( + + ) : ( + 'none' + )} + {t('Unaliased lineage')} @@ -136,33 +133,9 @@ export function DefiningMutationsInfo({ currentCluster }: DefiningMutationsInfoP {t('Links')} -
    -
  • - - {t('CoVariants - Per country - Clade {{name}}', { name: currentCluster.nextstrainClade })} - -
  • -
  • - - {t('CoVariants - Per variant - Clade {{name}}', { name: currentCluster.nextstrainClade })} - -
  • -
  • - - {t('Nextstrain - Clade {{name}}', { name: currentCluster.nextstrainClade })} - -
  • -
  • - - {t('CoV Spectrum - Clade {{name}}', { name: currentCluster.nextstrainClade })} - -
  • -
  • - - {t('CoV Spectrum - Lineage {{name}}', { name: currentCluster.lineage })} - -
  • -
+ + {t('CoV Spectrum - Lineage {{name}}', { name: currentCluster.lineage })} + @@ -170,16 +143,6 @@ export function DefiningMutationsInfo({ currentCluster }: DefiningMutationsInfoP ) } -const Ul = styled.ul` - list-style: none; - padding: 0; - margin-top: 0.5rem; -` - -const Li = styled.li` - margin: 0; -` - export interface DefiningMutationsTableWithTargetsProps { currentCluster: DefMutClusterDatum } diff --git a/web/src/io/getDefiningMutationsClusters.ts b/web/src/io/getDefiningMutationsClusters.ts index bed3b5d05a..8ac8872e2f 100644 --- a/web/src/io/getDefiningMutationsClusters.ts +++ b/web/src/io/getDefiningMutationsClusters.ts @@ -1,6 +1,9 @@ +import { omit } from 'lodash' import { useMemo } from 'react' import { useQuery } from 'react-query' import clustersJson from 'src/../data/definingMutationsIndex.json' +import { ClusterDatum, getClusters } from 'src/io/getClusters' +import { Cluster } from 'src/state/Clusters' export interface DefMutClusterIndexDatum { lineage: string @@ -42,7 +45,11 @@ export interface DefiningMutations { aa: Record> } -export interface DefMutClusterDatum { +export function getDefMutClusters(): DefMutClusterIndexDatum[] { + return clustersJson.clusters as DefMutClusterIndexDatum[] +} + +export interface DefMutClusterDatumRaw { lineage: string unaliased?: string parent?: string @@ -54,12 +61,12 @@ export interface DefMutClusterDatum { mutations: Record } -export function getDefMutClusters(): DefMutClusterIndexDatum[] { - return clustersJson.clusters as DefMutClusterIndexDatum[] +export interface DefMutClusterDatum extends Omit { + cluster?: ClusterDatum } export function useDefMutCluster(clusterName: string): DefMutClusterDatum { - const res = useQuery( + const res = useQuery( ['definingMutations', clusterName], async () => import(`src/../data/definingMutations/${clusterName}.json`), { @@ -76,10 +83,14 @@ export function useDefMutCluster(clusterName: string): DefMutClusterDatum { if (!res.data) { throw new Error(`Data not found for clade or lineage '${clusterName}'`) } - return res.data + const defMutClusterRaw = res.data + const cluster = getClusters().find((cl) => cl.nextstrain_name === defMutClusterRaw.nextstrainClade) + return { ...omit(defMutClusterRaw, 'nextstrainClade'), cluster } }, [clusterName, res.data]) } +function addMoreClusterInfo() {} + export function getDefMutClusterRedirects(): Record { return Object.fromEntries( getDefMutClusters() From 4893c6370644a51f21417a0bfec75dcf6c62f864 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 9 Nov 2023 01:08:01 +0100 Subject: [PATCH 07/11] feat(web): make info table 2-column --- .../DefiningMutationsPage.tsx | 89 +++++++++++-------- 1 file changed, 50 insertions(+), 39 deletions(-) diff --git a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx index c282c929bf..cfa5f44884 100644 --- a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx +++ b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx @@ -83,8 +83,8 @@ export default function DefiningMutationsPage({ clusterName: clusterNameUnsafe } } export const InfoTable = styled(Table)` - flex: 0; max-width: 600px; + margin-bottom: 0; & td { padding: 0.25rem 0.5rem; @@ -102,44 +102,55 @@ export function DefiningMutationsInfo({ currentCluster }: DefiningMutationsInfoP const urlCovSpectrumLineage = `https://cov-spectrum.org/explore/World/AllSamples/Past6M/variants?nextcladePangoLineage=${lineageSafe}` return ( - - - - {t('Nextstrain clade')} - - {currentCluster.cluster?.display_name ? ( - - ) : ( - 'none' - )} - - - - {t('Unaliased lineage')} - {currentCluster.unaliased ? : 'none'} - - - {t('Parent lineage')} - {currentCluster.parent ? : 'none'} - - - {t('Designation date')} - {currentCluster.designationDate} - - - {t('Designation issue')} - {currentCluster.designationIssue} - - - {t('Links')} - - - {t('CoV Spectrum - Lineage {{name}}', { name: currentCluster.lineage })} - - - - - + + + + + + {t('Nextstrain clade')} + + {currentCluster.cluster?.display_name ? ( + + ) : ( + 'none' + )} + + + + {t('Unaliased lineage')} + {currentCluster.unaliased ? : 'none'} + + + {t('Parent lineage')} + {currentCluster.parent ? : 'none'} + + + + + + + + + + {t('Designation date')} + {currentCluster.designationDate} + + + {t('Designation issue')} + {currentCluster.designationIssue} + + + {t('Links')} + + + {t('CoV Spectrum - Lineage {{name}}', { name: currentCluster.lineage })} + + + + + + + ) } From 7ab3d3f29c46fca74ef690dba74d839174f8a589 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 9 Nov 2023 01:17:02 +0100 Subject: [PATCH 08/11] fix(web): adjust title --- .../DefiningMutations/DefMutLineageTitle.tsx | 10 +++------- .../DefiningMutations/DefiningMutationsPage.tsx | 2 -- web/src/io/getDefiningMutationsClusters.ts | 3 --- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/web/src/components/DefiningMutations/DefMutLineageTitle.tsx b/web/src/components/DefiningMutations/DefMutLineageTitle.tsx index 77caff2591..b613d9c895 100644 --- a/web/src/components/DefiningMutations/DefMutLineageTitle.tsx +++ b/web/src/components/DefiningMutations/DefMutLineageTitle.tsx @@ -20,21 +20,17 @@ export interface DefMutLineageTitleProps { cluster: DefMutClusterDatum } -export function DefMutLineageTitle({ cluster: { lineage, nextstrainClade } }: DefMutLineageTitleProps) { +export function DefMutLineageTitle({ cluster: { lineage, cluster } }: DefMutLineageTitleProps) { const { t } = useTranslationSafe() const subtitle = useMemo(() => { - if (nextstrainClade === 'recombinant') { - return null - } - return ( {t(`also known as clade `)} - {nextstrainClade} + {cluster?.display_name} ) - }, [nextstrainClade, t]) + }, [cluster?.display_name, t]) return ( diff --git a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx index cfa5f44884..3a34b6ee03 100644 --- a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx +++ b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx @@ -6,7 +6,6 @@ import { AaMut, LineageBadge, NucMut, VariantLinkBadge } from 'src/components/Co import { parsePositionOrThrow } from 'src/components/Common/parsePosition' import { LinkSmart } from 'src/components/Link/LinkSmart' import styled from 'styled-components' -import { getClusters } from 'src/io/getClusters' import { TableSlimWithBorders } from 'src/components/Common/TableSlim' import { DefMutLineageTitle } from 'src/components/DefiningMutations/DefMutLineageTitle' import { useRouter } from 'next/router' @@ -23,7 +22,6 @@ import { Layout } from 'src/components/Layout/Layout' import { NarrowPageContainer } from 'src/components/Common/ClusterSidebarLayout' const clusterRedirects = getDefMutClusterRedirects() -const clusters = getClusters() export function useCurrentClusterName(clusterName?: string) { const router = useRouter() diff --git a/web/src/io/getDefiningMutationsClusters.ts b/web/src/io/getDefiningMutationsClusters.ts index 8ac8872e2f..3a1d5152a3 100644 --- a/web/src/io/getDefiningMutationsClusters.ts +++ b/web/src/io/getDefiningMutationsClusters.ts @@ -3,7 +3,6 @@ import { useMemo } from 'react' import { useQuery } from 'react-query' import clustersJson from 'src/../data/definingMutationsIndex.json' import { ClusterDatum, getClusters } from 'src/io/getClusters' -import { Cluster } from 'src/state/Clusters' export interface DefMutClusterIndexDatum { lineage: string @@ -89,8 +88,6 @@ export function useDefMutCluster(clusterName: string): DefMutClusterDatum { }, [clusterName, res.data]) } -function addMoreClusterInfo() {} - export function getDefMutClusterRedirects(): Record { return Object.fromEntries( getDefMutClusters() From 5a439d2b52edda43e870c4be881cb7d04b2bab63 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 9 Nov 2023 01:57:13 +0100 Subject: [PATCH 09/11] feat(web): sort defining mutations by nuc pos --- .../DefiningMutationsPage.tsx | 48 ++++++++++++------- web/src/io/getDefiningMutationsClusters.ts | 5 ++ 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx index 3a34b6ee03..6a7c1f3d56 100644 --- a/web/src/components/DefiningMutations/DefiningMutationsPage.tsx +++ b/web/src/components/DefiningMutations/DefiningMutationsPage.tsx @@ -1,10 +1,11 @@ import unique from 'fork-ts-checker-webpack-plugin/lib/utils/array/unique' import React, { ChangeEvent, useCallback, useMemo, useState } from 'react' -import { get } from 'lodash' +import { get, sortBy } from 'lodash' import { Col, Row, Input, Label, Table, Form as FormBase, FormGroup as FormGroupBase } from 'reactstrap' import { AaMut, LineageBadge, NucMut, VariantLinkBadge } from 'src/components/Common/MutationBadge' import { parsePositionOrThrow } from 'src/components/Common/parsePosition' import { LinkSmart } from 'src/components/Link/LinkSmart' +import { notUndefinedOrNull } from 'src/helpers/notUndefined' import styled from 'styled-components' import { TableSlimWithBorders } from 'src/components/Common/TableSlim' import { DefMutLineageTitle } from 'src/components/DefiningMutations/DefMutLineageTitle' @@ -17,6 +18,7 @@ import { DefMutClusterDatum, DefMutAa, DefMutNuc, + SilentOrCodingMut, } from 'src/io/getDefiningMutationsClusters' import { Layout } from 'src/components/Layout/Layout' import { NarrowPageContainer } from 'src/components/Common/ClusterSidebarLayout' @@ -254,24 +256,38 @@ export function DefiningMutationsTable({ currentCluster, comparisonTargetName }: return { pos, ...nucMut } }) - const aaMuts: DefMutAa[] = Object.entries(mutations.aa).flatMap(([gene, aaMuts]) => - Object.entries(aaMuts).map(([posStr, aaMut]) => { - const pos = parsePositionOrThrow(posStr) - const nucMuts = allNucMuts.filter((nucMut) => aaMut.nucPos?.includes(nucMut.pos)) - return { gene, pos, ...aaMut, nucMuts } - }), + const aaMuts = Object.entries(mutations.aa) + .flatMap(([gene, aaMuts]) => + Object.entries(aaMuts).map(([posStr, aaMut]) => { + const pos = parsePositionOrThrow(posStr) + let nucMuts = allNucMuts.filter((nucMut) => aaMut.nucPos?.includes(nucMut.pos)) + nucMuts = sortBy(nucMuts, (m) => m.pos) + return { gene, pos, ...aaMut, nucMuts } + }), + ) + .map((aaMut: DefMutAa) => ({ aaMut })) + + const codingPositions = unique(aaMuts.flatMap(({ aaMut }) => aaMut.nucPos)) + const silentNucMuts = allNucMuts + .filter((nucMut) => !codingPositions.includes(nucMut.pos)) + .map((nucMut) => ({ nucMut })) + + const allMuts: SilentOrCodingMut[] = [...silentNucMuts, ...aaMuts] + + const allRows: SilentOrCodingMut[] = sortBy( + allMuts, + ({ nucMut, aaMut }: SilentOrCodingMut) => nucMut?.pos ?? aaMut?.nucPos ?? Number.POSITIVE_INFINITY, ) - const codingPositions = unique(aaMuts.flatMap((aaMut) => aaMut.nucPos)) - const silentNucMuts = allNucMuts.filter((nucMut) => !codingPositions.includes(nucMut.pos)) - - const silentRows = silentNucMuts.map((nucMut) => ( - - )) + const silentRows = allRows + .map(({ nucMut }) => nucMut) + .filter(notUndefinedOrNull) + .map((nucMut) => ) - const codingRows = aaMuts.map((aaMut) => ( - - )) + const codingRows = allRows + .map(({ aaMut }) => aaMut) + .filter(notUndefinedOrNull) + .map((aaMut) => ) return [...codingRows, ...silentRows] }, [comparisonTargetName, currentCluster.mutations]) diff --git a/web/src/io/getDefiningMutationsClusters.ts b/web/src/io/getDefiningMutationsClusters.ts index 3a1d5152a3..ffc12f67de 100644 --- a/web/src/io/getDefiningMutationsClusters.ts +++ b/web/src/io/getDefiningMutationsClusters.ts @@ -39,6 +39,11 @@ export interface DefMutAa { annotation: string } +export interface SilentOrCodingMut { + nucMut?: DefMutNuc + aaMut?: DefMutAa +} + export interface DefiningMutations { nuc: Record aa: Record> From 0617487c383bdd9c79255357e4644d89895d2233 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 9 Nov 2023 02:24:51 +0100 Subject: [PATCH 10/11] feat(web): add 'search lineages' dropdown --- .../DefiningMutations/DefMutLineageTitle.tsx | 10 ++-- .../DefiningMutationsDropdown.tsx | 50 +++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 web/src/components/DefiningMutations/DefiningMutationsDropdown.tsx diff --git a/web/src/components/DefiningMutations/DefMutLineageTitle.tsx b/web/src/components/DefiningMutations/DefMutLineageTitle.tsx index b613d9c895..d448b16654 100644 --- a/web/src/components/DefiningMutations/DefMutLineageTitle.tsx +++ b/web/src/components/DefiningMutations/DefMutLineageTitle.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from 'react' +import { DefiningMutationsDropdown } from 'src/components/DefiningMutations/DefiningMutationsDropdown' import { useTranslationSafe } from 'src/helpers/useTranslationSafe' import type { DefMutClusterDatum } from 'src/io/getDefiningMutationsClusters' @@ -20,22 +21,23 @@ export interface DefMutLineageTitleProps { cluster: DefMutClusterDatum } -export function DefMutLineageTitle({ cluster: { lineage, cluster } }: DefMutLineageTitleProps) { +export function DefMutLineageTitle({ cluster } : DefMutLineageTitleProps) { const { t } = useTranslationSafe() const subtitle = useMemo(() => { return ( {t(`also known as clade `)} - {cluster?.display_name} + {cluster.cluster?.display_name} ) - }, [cluster?.display_name, t]) + }, [cluster.cluster?.display_name, t]) return ( - {`Defining mutations for ${lineage}`} + {`Defining mutations for ${cluster.lineage}`} {subtitle} + ) } diff --git a/web/src/components/DefiningMutations/DefiningMutationsDropdown.tsx b/web/src/components/DefiningMutations/DefiningMutationsDropdown.tsx new file mode 100644 index 0000000000..5603fbb25f --- /dev/null +++ b/web/src/components/DefiningMutations/DefiningMutationsDropdown.tsx @@ -0,0 +1,50 @@ +import React, { useCallback, useMemo, useState } from 'react' +import { useRouter } from 'next/router' +import urljoin from 'url-join' +import Select, { OnChangeValue, StylesConfig } from 'react-select' +import { DefMutClusterDatum, getDefMutClusters } from 'src/io/getDefiningMutationsClusters' +import { DropdownOption } from 'src/components/Common/DropdownOption' + +const clusters = getDefMutClusters() + +export function DefiningMutationsDropdown({ cluster }: { cluster: DefMutClusterDatum }) { + const { push } = useRouter() + const [value, setValue] = useState(cluster.lineage) + + const options = useMemo(() => clusters.map(({ lineage }) => ({ label: lineage, value: lineage })), []) + const option = useMemo(() => ({ label: value, value }), [value]) + const onChange = useCallback( + (newValue: OnChangeValue, false>) => { + if (newValue) { + void push(urljoin('/defining-mutations', newValue.value)) // eslint-disable-line no-void + setValue(newValue.value) + } + }, + [push], + ) + + return ( +
+
+ {'Search lineages '} + + , false> + options={options} + value={option} + onChange={onChange} + isClearable={false} + isMulti={false} + isSearchable + menuPortalTarget={document.body} + styles={DROPDOWN_STYLES} + /> + +
+
+ ) +} + +const DROPDOWN_STYLES: StylesConfig<{ label: string; value: string }> = { + container: (base) => ({ ...base, width: '200px' }), + menuPortal: (base) => ({ ...base, zIndex: 9999 }), +} From 96a085ee05e421a4fbdfccc346455fc0607525ff Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Thu, 9 Nov 2023 02:34:44 +0100 Subject: [PATCH 11/11] refactor: lint --- web/src/components/DefiningMutations/DefMutLineageTitle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/DefiningMutations/DefMutLineageTitle.tsx b/web/src/components/DefiningMutations/DefMutLineageTitle.tsx index d448b16654..33496a552d 100644 --- a/web/src/components/DefiningMutations/DefMutLineageTitle.tsx +++ b/web/src/components/DefiningMutations/DefMutLineageTitle.tsx @@ -21,7 +21,7 @@ export interface DefMutLineageTitleProps { cluster: DefMutClusterDatum } -export function DefMutLineageTitle({ cluster } : DefMutLineageTitleProps) { +export function DefMutLineageTitle({ cluster }: DefMutLineageTitleProps) { const { t } = useTranslationSafe() const subtitle = useMemo(() => {