Skip to content

Commit

Permalink
Merge pull request #660 from ProteinsWebTeam/ptms-data
Browse files Browse the repository at this point in the history
PTMs track
  • Loading branch information
apolignano authored Dec 10, 2024
2 parents db607d8 + a2f3a5c commit 77d0f44
Show file tree
Hide file tree
Showing 9 changed files with 245 additions and 57 deletions.
5 changes: 5 additions & 0 deletions src/components/ExtLink/patternLinkWrapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ UniProtLink.displayName = 'UniProtLink';
export const DOILink = patternLinkWrapper('{id}');
DOILink.displayName = 'DOILink';

export const PTMLink = patternLinkWrapper(
'https://www.uniprot.org/uniprotkb/{id}/entry#ptm_processing',
);
PTMLink.displayName = 'PTMLink';

export const AlphafoldLink = patternLinkWrapper(
'https://alphafold.ebi.ac.uk/entry/{id}',
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Protein/Summary/__snapshots__/test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ exports[`<SummaryProtein /> should render 1`] = `
>
<Memo(Connect(loadData(Viewer))) />
<section>
<Memo(Connect(loadData(Connect(loadData(loadExternalSources(Connect(loadData(Connect(loadData(Connect(loadData(Connect(loadData(Connect(loadData(Connect(loadData(DomainOnProteinWithoutData))))))))))))))))))
<Memo(Connect(loadData(Connect(loadData(loadExternalSources(Connect(loadData(Connect(loadData(Connect(loadData(Connect(loadData(Connect(loadData(Connect(loadData(Connect(loadData(DomainOnProteinWithoutData))))))))))))))))))))
mainData={
{
"metadata": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
import { NOT_MEMBER_DBS } from 'menuConfig';
import Link from 'components/generic/Link';

import { AlphafoldLink } from 'components/ExtLink/patternLinkWrapper';
import { AlphafoldLink, PTMLink } from 'components/ExtLink/patternLinkWrapper';
import Tooltip from 'components/SimpleCommonComponents/Tooltip';

import { FunFamLink } from 'subPages/Subfamilies';
Expand All @@ -29,12 +29,15 @@ const EXCEPTIONAL_TYPES = [
'chain',
'secondary_structure',
'variation',
'ptm',
];
const EXCEPTIONAL_PREFIXES = ['G3D:', 'REPEAT:', 'DISPROT:'];

export const isAnExceptionalLabel = (entry: ExtendedFeature): boolean => {
return (
EXCEPTIONAL_TYPES.includes(entry.type || '') ||
// Exceptional types coming from InterPro (e.g PTMs), should not result in an ExceptionalLabel.
(EXCEPTIONAL_TYPES.includes(entry.type || '') &&
entry.source_database !== 'interpro') ||
NOT_MEMBER_DBS.has(entry.source_database || '') ||
EXCEPTIONAL_PREFIXES.some((prefix) => entry.accession.startsWith(prefix))
);
Expand Down Expand Up @@ -96,6 +99,17 @@ const ExceptionalLabels = ({ entry, isPrinting, databases }: PropsEL) => {
</AlphafoldLink>
);
}

if (entry.source_database === 'ptm') {
return isPrinting ? (
<span>UniProt</span>
) : (
<PTMLink id={entry.accession || ''} className={css('ext')}>
{entry.name}
</PTMLink>
);
}

if (entry.source_database === 'elm')
return isPrinting ? (
<span>{label}</span>
Expand Down
64 changes: 64 additions & 0 deletions src/components/ProteinViewer/Popup/PTM/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import type { PTMFragment } from 'src/components/ProteinViewer';

export type PTMDetail = {
target?: HTMLElement;
highlight: string;
feature?: {
name?: string;
accession?: string;
type?: string;
sources?: string;
locations: {
accession: string;
description: string;
fragments: PTMFragment[];
}[];
};
};
type Props = {
detail: PTMDetail;
};

const ProtVistaPTMPopup = ({ detail }: Props) => {
const highlightedPosition = parseInt(detail.highlight.split(':')[0]);
const ptmData: PTMFragment | undefined =
detail.feature?.locations[0].fragments.filter(
(f) => f.start == highlightedPosition,
)[0];

if (ptmData) {
const ptmPeptide: string = ptmData.peptide as string;
const ptmPos: number = ptmData.relative_pos as number;
const peptideStart: number = ptmData.peptide_start as number;

return (
<section>
<div>
<span> Peptide: </span>

{/* Show peptide sequence and highlight PTM */}
<span>{ptmPeptide.slice(0, ptmPos)}</span>
<span>
<b>{ptmPeptide[ptmPos]}</b>
</span>
<span>{ptmPeptide.slice(ptmPos + 1)}</span>

{/* Show peptide sequence and highlight PTM */}
<span>
&nbsp;({peptideStart} - {ptmData.peptide_end as string}){' '}
</span>
</div>
<div>
{ptmData.ptm_type as string} on {ptmPeptide[ptmPos]} (
{peptideStart + ptmPos})
</div>
<div>Source: {ptmData.source as string[]}</div>
</section>
);
}

return <></>;
};

export default ProtVistaPTMPopup;
9 changes: 9 additions & 0 deletions src/components/ProteinViewer/Popup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ProtVistaVariationPopup, { VariationDetail } from './Variation';
import ProtVistaEntryPopup, { EntryDetail } from './Entry';
import ProtVistaConservationPopup, { ConservationDetail } from './Conservation';
import RepeatsDBPopup, { RepeatsDBDetail } from './RepeatsDB';
import ProtVistaPTMPopup, { PTMDetail } from './PTM';
import DisProtPopup, { DisProtDetail } from './DisProt';
import { ExtendedFeature } from '..';

Expand All @@ -25,6 +26,14 @@ type Props = {
};

const ProtVistaPopup = ({ detail, sourceDatabase, currentLocation }: Props) => {
// comes from PTMTrack
if (
(detail as PTMDetail)?.feature?.type == 'ptm' &&
!(detail as PTMDetail).feature?.accession?.startsWith('IPR')
) {
return <ProtVistaPTMPopup detail={detail as PTMDetail} />;
}

// comes from the conservation track
if (detail.type === 'conservation') {
return <ProtVistaConservationPopup detail={detail as ConservationDetail} />;
Expand Down
26 changes: 6 additions & 20 deletions src/components/ProteinViewer/TracksInCategory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,11 @@ const TracksInCategory = forwardRef<ExpandedHandle, Props>(
hideCategory,
})}
>
{OTHER_TRACK_TYPES.includes(type) || isExternalSource ? (
{OTHER_TRACK_TYPES.includes(type) ||
isExternalSource ||
// Handle PTM exceptional case.
// Requires different tracks depending on where the info comes from (proteinsAPI or InterPro)
(entry.type === 'ptm' && entry.source_database === 'ptm') ? (
<div className={css('track', type.replace('_', '-'))}>
{entry.type === 'sequence_conservation' &&
(entry.warnings || []).length > 0 && (
Expand Down Expand Up @@ -348,24 +352,6 @@ const TracksInCategory = forwardRef<ExpandedHandle, Props>(
use-ctrl-to-zoom
/>
)}
{entry.type === 'ptm' && (
<></>
/*<NightingaleColoredSequence
id={getTrackAccession(entry.accession)}
data={entry.data as string}
length={sequence.length}
scale="H:90,M:70,L:50,D:0"
height={12}
color-range="#ff7d45:0,#ffdb13:50,#65cbf3:70,#0053d6:90,#0053d6:100"
margin-right={10}
margin-left={20}
margin-color="#fafafa"
highlight-event="onmouseover"
highlight-color={highlightColor}
className="confidence"
use-ctrl-to-zoom
/>*/
)}
{entry.type === 'variation' && (
<NightingaleVariation
id={getTrackAccession(entry.accession)}
Expand All @@ -383,7 +369,7 @@ const TracksInCategory = forwardRef<ExpandedHandle, Props>(
protein-api
/>
)}
{(['secondary_structure', 'residue'].includes(
{(['secondary_structure', 'residue', 'ptm'].includes(
entry.type || '',
) ||
isExternalSource) && (
Expand Down
118 changes: 118 additions & 0 deletions src/components/ProteinViewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,30 @@ export type ExtendedFeature = Feature & {
warnings?: Array<string>;
};

type PTM = {
position: number;
name: string;
sources: string[];
};

type PTMFeature = {
begin: string;
end: string;
peptide: string;
ptms: PTM[];
};

type PTMData = {
accession: string;
features: PTMFeature[];
};

export type PTMFragment = {
[annotation: string]: unknown;
start: number;
end: number;
};

type Zoomable = { zoomIn: () => void; zoomOut: () => void };

type Props = PropsWithChildren<{
Expand Down Expand Up @@ -238,6 +262,42 @@ export const ProteinViewer = ({
}
};

const residuesToLocations = (
residues: Residue[] | undefined,
): ExtendedFeatureLocation[] => {
const newLocations: ExtendedFeatureLocation[] = [];
if (residues) {
residues.map((residue) => {
residue.locations.map((location) => {
newLocations.push(location);
});
});
}
return newLocations;
};

const ptmFeaturesFragments = (features: PTMFeature[]): PTMFragment[] => {
const ptmFragments: PTMFragment[] = [];

features.map((feature) => {
feature.ptms.map((ptm) => {
const ptmFragment: PTMFragment = {
start: parseInt(feature.begin) + ptm.position - 1, // Absolute modification pos
end: parseInt(feature.begin) + ptm.position - 1, // Absolute modification pos
relative_pos: ptm.position - 1,
ptm_type: ptm.name,
peptide: feature.peptide,
peptide_start: parseInt(feature.begin),
peptide_end: parseInt(feature.end),
source: ptm.sources.join(', '),
};

ptmFragments.push(ptmFragment);
});
});
return ptmFragments;
};

return (
<div ref={mainRef} className={css('fullscreenable', 'margin-bottom-large')}>
{/* Tooltip */}
Expand Down Expand Up @@ -335,6 +395,64 @@ export const ProteinViewer = ({
hideDiv = 'none';
}

// Transform PTM data to track-like data
if (type == 'ptm') {
const ptmFragmentsGroupedByModification: {
[type: string]: PTMFragment[];
} = {};

// PTMs coming from APIs
entries
.filter(
(entry) => entry.source_database === 'proteinsAPI',
)
.map((entry) => {
const fragments = ptmFeaturesFragments(
(entry.data as PTMData).features,
);
fragments.map((fragment) => {
if (
ptmFragmentsGroupedByModification[
fragment.ptm_type as string
]
) {
ptmFragmentsGroupedByModification[
fragment.ptm_type as string
].push(fragment);
} else {
ptmFragmentsGroupedByModification[
fragment.ptm_type as string
] = [fragment];
}
});
});

const ptmsEntriesGroupedByModification: ExtendedFeature[] =
[];
Object.entries(ptmFragmentsGroupedByModification).map(
(ptmData) => {
const modificationType: string = ptmData[0]; // Key
const fragments: PTMFragment[] = ptmData[1]; // Key
const newFeature: ExtendedFeature = {
accession: protein.accession,
name: modificationType,
type: 'ptm',
source_database: 'ptm',
locations: [{ fragments: fragments }],
};

ptmsEntriesGroupedByModification.push(newFeature);
},
);

// PTMs coming from InterPro and external API should be in the same section but require different processing due to different structure (see above)
entries = ptmsEntriesGroupedByModification.concat(
entries.filter(
(entry) => entry.source_database === 'interpro',
),
);
}

return (
<div
key={type}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,7 @@ export const flattenTracksObject = (
Object.entries(tracksObject)
.sort(byEntryType)
// “Binding_site” -> “Binding site”
.map(([key, value]) => [
key === 'ptm' ? 'PTM' : key.replace(UNDERSCORE, ' '),
value,
])
.map(([key, value]) => [key.replace(UNDERSCORE, ' '), value])
);
};

Expand All @@ -250,22 +247,19 @@ export const addVariationTrack = (
export const addPTMTrack = (
proteomicsPayload: ProteinsAPIProteomics,
protein: string,
tracks: ProteinViewerData,
tracks: ProteinViewerDataObject,
) => {
if (proteomicsPayload?.features?.length) {
const proteomicsTrack: [string, Array<unknown>] = [
'PTM Data',
[
{
accession: `ptm_${protein}`,
data: proteomicsPayload,
type: 'ptm',
protein,
source_database: 'proteinsAPI',
},
],
];
tracks.push(proteomicsTrack);
if (!tracks['ptm']) {
tracks['ptm'] = [];
}
tracks['ptm'].push({
accession: `ptm_${protein}`,
data: proteomicsPayload,
type: 'ptm',
protein,
source_database: 'proteinsAPI',
});
}
};

Expand Down Expand Up @@ -326,7 +320,7 @@ const DomainsOnProteinLoaded = ({

if (dataProteomics?.ok && dataProteomics.payload) {
if (dataProteomics.payload.features.length > 0) {
/*addPTMTrack(dataProteomics.payload, protein.accession, sortedData);*/
addPTMTrack(dataProteomics.payload, protein.accession, dataMerged);
}
}

Expand Down
Loading

0 comments on commit 77d0f44

Please sign in to comment.