From dc3efd5ff108cf43a6bb7e240b8eae5355126a80 Mon Sep 17 00:00:00 2001 From: "Gustavo A. Salazar" Date: Thu, 16 Nov 2023 11:18:33 +0000 Subject: [PATCH 1/4] representative domains on structure pages --- .prettierrc | 5 +- src/components/ProteinViewer/utils.ts | 20 +++---- .../DomainEntriesOnStructure/index.tsx | 52 +++++++++++++++---- .../Related/DomainsOnProtein/index.tsx | 40 +++++--------- .../ProteinViewerForStructures/index.tsx | 11 ++-- 5 files changed, 78 insertions(+), 50 deletions(-) diff --git a/.prettierrc b/.prettierrc index dcdf9ba4e..1463efe1b 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,7 +4,10 @@ overrides: - files: "*.css" options: singleQuote: false -- files: "src/**/*.js" +- files: + - "src/**/*.js" + - "src/**/*.ts" + - "src/**/*.tsx" options: parser: typescript trailingComma: all diff --git a/src/components/ProteinViewer/utils.ts b/src/components/ProteinViewer/utils.ts index 2b5cb675c..45900e0a5 100644 --- a/src/components/ProteinViewer/utils.ts +++ b/src/components/ProteinViewer/utils.ts @@ -6,13 +6,15 @@ import { getTrackColor, EntryColorMode } from 'utils/entry-color'; const selectRepresentativeDomains = (domains: Record[]) => { const flatDomains = []; for (const domain of domains) { - const { accession, short_name, name, source_database, integrated } = domain; + const { accession, short_name, name, source_database, integrated, chain } = + domain; for (const location of domain.entry_protein_locations as Array) { for (const fragment of location.fragments) { const { start, end, representative } = fragment; if (representative) { flatDomains.push({ accession, + chain, short_name, name, source_database, @@ -31,7 +33,7 @@ const selectRepresentativeDomains = (domains: Record[]) => { }; export const useProcessData = ( results: EndpointWithMatchesPayload[] | undefined, - endpoint: Endpoint + endpoint: Endpoint, ) => useMemo(() => { return processData(results || [], endpoint); @@ -39,7 +41,7 @@ export const useProcessData = ( const processData = ( dataResults: EndpointWithMatchesPayload[], - endpoint: Endpoint + endpoint: Endpoint, ) => { const results: Record[] = []; for (const item of dataResults) { @@ -50,13 +52,13 @@ const processData = ( ...match, ...item.metadata, ...(item.extra_fields || {}), - })) + })), ); } const interpro = results.filter( (entry) => (entry as unknown as Metadata).source_database.toLowerCase() === - 'interpro' + 'interpro', ); const representativeDomains = selectRepresentativeDomains(results); @@ -64,15 +66,15 @@ const processData = ( interpro.map((ipro) => [ `${ipro.accession}-${ipro.chain}-${ipro.protein}`, ipro, - ]) + ]), ); const integrated = results.filter((entry) => entry.integrated); const unintegrated = results.filter( (entry) => interpro.concat(integrated).indexOf(entry) === -1 && !NOT_MEMBER_DBS.has( - (entry as unknown as Metadata).source_database.toLowerCase() - ) + (entry as unknown as Metadata).source_database.toLowerCase(), + ), ); integrated.forEach((entry) => { const ipro: Record & { @@ -84,7 +86,7 @@ const processData = ( if (ipro.children.indexOf(entry) === -1) ipro.children.push(entry); }); integrated.sort((a, b) => - a.chain ? (a.chain as string).localeCompare(b.chain as string) : -1 + a.chain ? (a.chain as string).localeCompare(b.chain as string) : -1, ); return { interpro, diff --git a/src/components/Related/DomainEntriesOnStructure/index.tsx b/src/components/Related/DomainEntriesOnStructure/index.tsx index 9c73e8a26..27c66c541 100644 --- a/src/components/Related/DomainEntriesOnStructure/index.tsx +++ b/src/components/Related/DomainEntriesOnStructure/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import Link from 'components/generic/Link'; import Tooltip from 'components/SimpleCommonComponents/Tooltip'; @@ -50,7 +50,7 @@ export type DataForProteinChain = { const mergeData = ( secondaryData: StructureLinkedObject[], - secondaryStructures?: SecondaryStructure[] + secondaryStructures?: SecondaryStructure[], ) => { const out: Record> = {}; for (const entry of secondaryData) { @@ -119,7 +119,7 @@ const mergeData = ( ({ entry_protein_locations, entry_structure_locations, ...child }) => ({ ...child, locations: entry_structure_locations, - }) + }), ), chain: entry.chain, protein: entry.protein, @@ -131,7 +131,7 @@ const mergeData = ( const chains = Object.keys(out).sort((a, b) => (a ? a.localeCompare(b) : -1)); for (const chain of chains) { const proteins = Object.keys(out[chain]).sort((a, b) => - a ? a.localeCompare(b) : -1 + a ? a.localeCompare(b) : -1, ); for (const protein of proteins) { entries.push(out[chain][protein]); @@ -151,22 +151,50 @@ const tagChimericStructures = (data: DataForProteinChain[]) => { } }; +const getRepresentativesPerChain = ( + representativeDomains?: Record[], +) => { + const representativesPerChain: Record> = {}; + if (representativeDomains?.length) { + representativeDomains.forEach((domain) => { + if (domain.chain) { + if (!representativesPerChain[domain.chain as string]) + representativesPerChain[domain.chain as string] = []; + representativesPerChain[domain.chain as string].push( + domain as MinimalFeature, + ); + } + }); + } + return representativesPerChain; +}; + type Props = { structure: string; entries: StructureLinkedObject[]; secondaryStructures?: SecondaryStructure[]; + representativeDomains?: Record[]; }; const EntriesOnStructure = ({ entries, secondaryStructures, structure, + representativeDomains, }: Props) => { - const merged = mergeData(entries, secondaryStructures); - tagChimericStructures(merged); + const merged = useMemo(() => { + const data = mergeData(entries, secondaryStructures); + tagChimericStructures(data); + return data; + }, [entries, secondaryStructures]); + const representativesPerChain = useMemo( + () => getRepresentativesPerChain(representativeDomains), + [representativeDomains], + ); + return ( <> -
+
{merged.map((e, i) => { const sequenceData = { accession: `${e.chain}-${structure}`, @@ -174,14 +202,20 @@ const EntriesOnStructure = ({ }; const tracks = Object.entries( - e.data as Record>> + e.data as Record>>, ).sort(([a], [b]) => { if (a && a.toLowerCase() === 'chain') return -1; if (b && b.toLowerCase() === 'chain') return 1; return b ? b.localeCompare(a) : -1; }); + if (representativesPerChain[e.chain]) { + tracks.splice(0, 0, [ + 'representative domains', + representativesPerChain[e.chain], + ]); + } return ( -
+

Chain {e.chain}{' '} {e.protein?.accession && ( diff --git a/src/components/Related/DomainsOnProtein/index.tsx b/src/components/Related/DomainsOnProtein/index.tsx index d22a3bcf1..2e3c2984f 100644 --- a/src/components/Related/DomainsOnProtein/index.tsx +++ b/src/components/Related/DomainsOnProtein/index.tsx @@ -33,14 +33,14 @@ const css = cssBinder(ipro); export const orderByAccession = ( a: { accession: string }, - b: { accession: string } + b: { accession: string }, ) => (a.accession > b.accession ? 1 : -1); export const groupByEntryType = ( interpro: Array<{ accession: string; type: string; - }> + }>, ) => { const groups: Record< string, @@ -60,7 +60,7 @@ export const groupByEntryType = ( type Props = PropsWithChildren<{ mainData: { metadata: ProteinMetadata }; onMatchesLoaded?: ( - results: EndpointWithMatchesPayload[] + results: EndpointWithMatchesPayload[], ) => void; onFamiliesFound?: (families: Record[]) => void; title?: string; @@ -111,7 +111,7 @@ const DomainOnProteinWithoutData = ({ EndpointWithMatchesPayload > )?.results, - 'protein' + 'protein', ); useEffect(() => { const payload = data?.payload as PayloadList< @@ -146,10 +146,10 @@ const DomainOnProteinWithoutData = ({ const { interpro, unintegrated, other, representativeDomains } = processedData; const groups = groupByEntryType( - interpro as Array<{ accession: string; type: string }> + interpro as Array<{ accession: string; type: string }>, ); (unintegrated as Array<{ accession: string; type: string }>).sort( - orderByAccession + orderByAccession, ); const mergedData: ProteinViewerDataObject = { ...groups, @@ -159,18 +159,6 @@ const DomainOnProteinWithoutData = ({ if (representativeDomains?.length) mergedData.representative_domains = representativeDomains as MinimalFeature[]; - // [ - // // { - // // accession: 'Representative Domains', - // // locations: representativeDomains.map((d) => ({ - // // fragments: [{ start: d.start as number, end: d.end as number }], - // // accession: d.accession as string, - // // short_name: d.short_name, - // // source_database: d.source_database, - // // })), - // // }, - // representativeDomains, - // ]; if (externalSourcesData.length) { mergedData.external_sources = externalSourcesData; @@ -276,7 +264,7 @@ const getRelatedEntriesURL = createSelector( extra_fields: 'hierarchy,short_name', }, }); - } + }, ); const getExtraURL = (query: string) => @@ -285,7 +273,7 @@ const getExtraURL = (query: string) => (state: GlobalState) => state.customLocation.description, ( { protocol, hostname, port, root }: ParsedURLServer, - description: InterProDescription + description: InterProDescription, ) => { const url = format({ protocol, @@ -297,7 +285,7 @@ const getExtraURL = (query: string) => }, }); return url; - } + }, ); export default loadExternalSources( @@ -317,9 +305,9 @@ export default loadExternalSources( getUrl: getExtraURL('residues'), propNamespace: 'Residues', } as Params)( - loadData(getRelatedEntriesURL as Params)(DomainOnProteinWithoutData) - ) - ) - ) - ) + loadData(getRelatedEntriesURL as Params)(DomainOnProteinWithoutData), + ), + ), + ), + ), ); diff --git a/src/components/Structure/ViewerAndEntries/ProteinViewerForStructures/index.tsx b/src/components/Structure/ViewerAndEntries/ProteinViewerForStructures/index.tsx index bb61ef98d..e8f972081 100644 --- a/src/components/Structure/ViewerAndEntries/ProteinViewerForStructures/index.tsx +++ b/src/components/Structure/ViewerAndEntries/ProteinViewerForStructures/index.tsx @@ -38,13 +38,14 @@ const ProteinViewerForStructure = ({ } } if (!data.payload || !processedData) return null; - const { interpro, unintegrated } = processedData; + const { interpro, unintegrated, representativeDomains } = processedData; return (
); @@ -67,7 +68,7 @@ export const getURLForMatches = createSelector( page_size: 200, extra_fields: 'short_name', }, - }) + }), ); const getSecondaryStructureURL = createSelector( (state) => state.settings.api, @@ -86,7 +87,7 @@ const getSecondaryStructureURL = createSelector( extra_fields: 'secondary_structures', }, }); - } + }, ); export default loadData({ @@ -94,6 +95,6 @@ export default loadData({ getUrl: getSecondaryStructureURL, } as Params)( loadData>>( - getURLForMatches - )(ProteinViewerForStructure) + getURLForMatches, + )(ProteinViewerForStructure), ); From 1f19b963a81e33b7adbe610e9bb790a9e9ea7355 Mon Sep 17 00:00:00 2001 From: "Gustavo A. Salazar" Date: Thu, 16 Nov 2023 11:26:43 +0000 Subject: [PATCH 2/4] representative domains on alphafold pages --- .../ProteinViewerForAlphafold/index.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/Structure/ViewerAndEntries/ProteinViewerForAlphafold/index.tsx b/src/components/Structure/ViewerAndEntries/ProteinViewerForAlphafold/index.tsx index ad445f50f..45f8c9402 100644 --- a/src/components/Structure/ViewerAndEntries/ProteinViewerForAlphafold/index.tsx +++ b/src/components/Structure/ViewerAndEntries/ProteinViewerForAlphafold/index.tsx @@ -25,7 +25,7 @@ const ProteinViewer = loadable({ export const addConfidenceTrack = ( dataConfidence: RequestedData, protein: string, - tracks: ProteinViewerData + tracks: ProteinViewerData, ) => { if (dataConfidence?.payload?.confidenceCategory?.length) { const confidenceTrack: [string, Array] = [ @@ -124,12 +124,15 @@ const ProteinViewerForAlphafold = ({ !processedData ) return ; - const { interpro, unintegrated } = processedData; + const { interpro, unintegrated, representativeDomains } = processedData; const tracks: ProteinViewerData = [ ['Entries', interpro.concat(unintegrated)], ]; if (dataConfidence) addConfidenceTrack(dataConfidence, protein, tracks); if (!dataProtein.payload?.metadata) return null; + if (representativeDomains?.length) { + tracks.splice(1, 0, ['representative domains', representativeDomains]); + } return (
state.settings.api, @@ -177,7 +180,7 @@ const getInterproRelatedEntriesURL = createSelector( extra_fields: 'short_name', }, }); - } + }, ); export default loadData({ @@ -193,8 +196,8 @@ export default loadData({ propNamespace: 'Protein', } as Params)( loadData(getInterproRelatedEntriesURL as Params)( - ProteinViewerForAlphafold - ) - ) - ) + ProteinViewerForAlphafold, + ), + ), + ), ); From 0e14c0a201b27fefad21aced412d6b3a522c79cf Mon Sep 17 00:00:00 2001 From: "Gustavo A. Salazar" Date: Thu, 16 Nov 2023 14:02:28 +0000 Subject: [PATCH 3/4] representative domains on isoforms --- .../Protein/Isoforms/Viewer/index.tsx | 29 ++++++++++++++----- src/components/ProteinViewer/utils.ts | 7 +++-- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/components/Protein/Isoforms/Viewer/index.tsx b/src/components/Protein/Isoforms/Viewer/index.tsx index 9e0b6140d..6f048d599 100644 --- a/src/components/Protein/Isoforms/Viewer/index.tsx +++ b/src/components/Protein/Isoforms/Viewer/index.tsx @@ -9,6 +9,7 @@ import descriptionToPath from 'utils/processDescription/descriptionToPath'; import NumberComponent from 'components/NumberComponent'; import { groupByEntryType } from 'components/Related/DomainsOnProtein'; import { byEntryType } from 'components/Related/DomainsOnProtein/DomainsOnProteinLoaded'; +import { selectRepresentativeDomains } from 'components/ProteinViewer/utils'; import Loading from 'components/SimpleCommonComponents/Loading'; import loadable from 'higherOrder/loadable'; @@ -58,17 +59,31 @@ const features2protvista = (features: FeatureMap) => { } const interpro = featArray.filter(({ accession }) => - accession.toLowerCase().startsWith('ipr') + accession.toLowerCase().startsWith('ipr'), ); const groups = groupByEntryType(interpro); const unintegrated = featArray.filter( - (f) => interpro.indexOf(f) === -1 && integrated.indexOf(f) === -1 + (f) => interpro.indexOf(f) === -1 && integrated.indexOf(f) === -1, ); const categories: Array<[string, unknown]> = [ ...(Object.entries(groups) as Array<[string, unknown]>), ['unintegrated', unintegrated], ]; - return categories.sort(byEntryType); + + const sortedCategories = categories.sort(byEntryType); + const representativeDomains = selectRepresentativeDomains( + featArray, + 'locations', + ); + + if (representativeDomains?.length) { + sortedCategories.splice(0, 0, [ + 'representative domains', + representativeDomains, + ]); + } + + return sortedCategories; }; type Props = { isoform?: string; @@ -113,7 +128,7 @@ const Viewer = ({ isoform, data }: LoadedProps) => { const mapStateToProps = createSelector( (state: GlobalState) => state.customLocation.search, - ({ isoform }) => ({ isoform }) + ({ isoform }) => ({ isoform }), ); const getIsoformURL = createSelector( @@ -123,7 +138,7 @@ const getIsoformURL = createSelector( ( { protocol, hostname, port, root }, { protein: { accession } }, - { isoform } + { isoform }, ) => { const description = { main: { key: 'protein' }, @@ -139,8 +154,8 @@ const getIsoformURL = createSelector( isoforms: isoform, }, }); - } + }, ); export default loadData({ getUrl: getIsoformURL, mapStateToProps } as Params)( - Viewer + Viewer, ); diff --git a/src/components/ProteinViewer/utils.ts b/src/components/ProteinViewer/utils.ts index 45900e0a5..d7102bc88 100644 --- a/src/components/ProteinViewer/utils.ts +++ b/src/components/ProteinViewer/utils.ts @@ -3,12 +3,15 @@ import { toPlural } from 'utils/pages/toPlural'; import { NOT_MEMBER_DBS } from 'menuConfig'; import { getTrackColor, EntryColorMode } from 'utils/entry-color'; -const selectRepresentativeDomains = (domains: Record[]) => { +export const selectRepresentativeDomains = ( + domains: Record[], + locationKey: string = 'entry_protein_locations', +) => { const flatDomains = []; for (const domain of domains) { const { accession, short_name, name, source_database, integrated, chain } = domain; - for (const location of domain.entry_protein_locations as Array) { + for (const location of domain[locationKey] as Array) { for (const fragment of location.fragments) { const { start, end, representative } = fragment; if (representative) { From 99b2c42d6f1403106125553fc96af4e3975bff73 Mon Sep 17 00:00:00 2001 From: "Gustavo A. Salazar" Date: Fri, 17 Nov 2023 10:29:42 +0000 Subject: [PATCH 4/4] snapshot update --- .../DomainEntriesOnStructure/__snapshots__/test.js.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Related/DomainEntriesOnStructure/__snapshots__/test.js.snap b/src/components/Related/DomainEntriesOnStructure/__snapshots__/test.js.snap index 68a7a5856..8b96578d4 100644 --- a/src/components/Related/DomainEntriesOnStructure/__snapshots__/test.js.snap +++ b/src/components/Related/DomainEntriesOnStructure/__snapshots__/test.js.snap @@ -3,10 +3,10 @@ exports[` should render 1`] = `