Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Publication column for Cases, Biospecimen, and Files tabs #738

Merged
merged 2 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions data/processSynapseJSON.log
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ ncc: Version 0.28.6
ncc: Compiling file index.js
ncc: Using [email protected] (local user-provided)
40kB sourcemap-register.js
10201kB index.js
1427kB index.js.map
10241kB [26259ms] - ncc 0.28.6
11086kB index.js
1428kB index.js.map
11126kB [33746ms] - ncc 0.28.6
Missing ParentBiospecimenID: {
Component: 'ScRNA-seqLevel3',
Filename: 'single_cell_RNAseq_level_3_ped_glioma/HTAN_pHGG_161_New_Reg1_snRNA/barcodes.tsv.gz',
Expand Down
184 changes: 146 additions & 38 deletions data/processSynapseJSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ function processSynapseJSON(
.mapValues((p) => new Set(getPublicationAssociatedParentDataFileIDs(p)))
.value();

// add publication id
// add publication ids
flatData.forEach((f) => {
_.forEach(
publicationParentDataFileIdsByUid,
Expand Down Expand Up @@ -612,6 +612,19 @@ function processSynapseJSON(
ancestryByParticipantID
);

// add publication ids for biospecimens
addBiopsecimenPublicationIds(files, filesById, biospecimenByBiospecimenID);

// add publication ids for participants
addParticipantPublicationIds(
biospecimenByBiospecimenID,
diagnosisByParticipantID
);
addParticipantPublicationIds(
biospecimenByBiospecimenID,
demographicsByParticipantID
);

const dbgapImgSynapseSet = new Set<string>(
getDbgapImgSynapseIds(entitiesById)
);
Expand Down Expand Up @@ -733,6 +746,71 @@ function addPrimaryParents(
});
}

function addParticipantPublicationIds(
biospecimenByBiospecimenID: {
[biospecimenID: string]: BaseSerializableEntity;
},
casesByParticipantID: {
[participantID: string]: BaseSerializableEntity;
}
) {
// for each biospecimen find related participants and add publication ids
_.forEach(biospecimenByBiospecimenID, (biospecimen) => {
const participant = getParticipantCaseData(
biospecimen,
biospecimenByBiospecimenID,
casesByParticipantID
);

if (participant && !_.isEmpty(biospecimen.publicationIds)) {
participant.publicationIds = participant.publicationIds || [];
participant.publicationIds.push(...biospecimen.publicationIds!);
}
});

// remove duplicate publication ids if any
_.forEach(casesByParticipantID, (participant) => {
if (!_.isEmpty(participant.publicationIds)) {
participant.publicationIds = _.uniq(participant.publicationIds);
}
});
}

function addBiopsecimenPublicationIds(
files: BaseSerializableEntity[],
filesByHTANId: { [DataFileID: string]: BaseSerializableEntity },
biospecimenByBiospecimenID: {
[biospecimenID: string]: BaseSerializableEntity;
}
) {
// for each file find related biospecimens and add publication ids
files.forEach((file) => {
const primaryParents =
file.primaryParents && file.primaryParents.length
? file.primaryParents
: [file.DataFileID];
const biospecimens = getUniqueParentBiospecimens(
primaryParents,
filesByHTANId,
biospecimenByBiospecimenID
);

if (!_.isEmpty(file.publicationIds)) {
biospecimens.forEach((biospecimen) => {
biospecimen.publicationIds = biospecimen.publicationIds || [];
biospecimen.publicationIds.push(...file.publicationIds!);
});
}
});

// remove duplicate publication ids if any
_.forEach(biospecimenByBiospecimenID, (biospecimen) => {
if (!_.isEmpty(biospecimen.publicationIds)) {
biospecimen.publicationIds = _.uniq(biospecimen.publicationIds);
}
});
}

function findAndAddPrimaryParents(
f: BaseSerializableEntity,
filesByFileId: { [DataFileID: string]: BaseSerializableEntity }
Expand Down Expand Up @@ -865,6 +943,23 @@ function getParentBiospecimens(
);
}

function getUniqueParentBiospecimens(
primaryParents: string[],
filesByHTANId: { [DataFileID: string]: BaseSerializableEntity },
biospecimenByBiospecimenID: {
[biospecimenID: string]: BaseSerializableEntity;
}
) {
return _(primaryParents)
.map((p) =>
getParentBiospecimens(filesByHTANId[p], biospecimenByBiospecimenID)
)
.flatten()
.compact()
.uniqBy((b) => b.BiospecimenID)
.value();
}

function getSampleAndPatientData(
file: BaseSerializableEntity,
filesByHTANId: { [DataFileID: string]: BaseSerializableEntity },
Expand Down Expand Up @@ -899,14 +994,11 @@ function getSampleAndPatientData(
}
}

const biospecimen = _(primaryParents)
.map((p) =>
getParentBiospecimens(filesByHTANId[p], biospecimenByBiospecimenID)
)
.flatten()
.compact()
.uniqBy((b) => b.BiospecimenID)
.value();
const biospecimen = getUniqueParentBiospecimens(
primaryParents,
filesByHTANId,
biospecimenByBiospecimenID
);

const diagnosis = _.uniqBy(
getCaseData(
Expand Down Expand Up @@ -938,6 +1030,44 @@ function getSampleAndPatientData(
return { biospecimen, diagnosis, demographics, therapy };
}

function getParticipantCaseData(
entity: BaseSerializableEntity,
biospecimenByBiospecimenID: {
[biospecimenID: string]: BaseSerializableEntity;
},
casesByParticipantID: {
[participantID: string]: BaseSerializableEntity;
}
) {
// parentID can be both participant or biospecimen, so keep
// going up the tree until participant is found.
let parentID = entity.ParentID;
const alreadyProcessed = new Set();

while (parentID in biospecimenByBiospecimenID) {
// this is to prevent infinite loop due to possible circular references
if (alreadyProcessed.has(parentID)) {
break;
} else {
alreadyProcessed.add(parentID);
}

const parentBioSpecimen = biospecimenByBiospecimenID[parentID];
if (parentBioSpecimen.ParentID) {
parentID = parentBioSpecimen.ParentID;
}
}

if (!(parentID in casesByParticipantID)) {
// console.error(
// `${s.BiospecimenID} does not have a parentID (${parentID}) with diagnosis/demographics information`
// );
return undefined;
} else {
return casesByParticipantID[parentID] as Entity;
}
}

function getCaseData(
biospecimen: BaseSerializableEntity[],
biospecimenByBiospecimenID: {
Expand All @@ -948,35 +1078,13 @@ function getCaseData(
}
) {
return biospecimen
.map((s) => {
// parentID can be both participant or biospecimen, so keep
// going up the tree until participant is found.
let parentID = s.ParentID;
const alreadyProcessed = new Set();

while (parentID in biospecimenByBiospecimenID) {
// this is to prevent infinite loop due to possible circular references
if (alreadyProcessed.has(parentID)) {
break;
} else {
alreadyProcessed.add(parentID);
}

const parentBioSpecimen = biospecimenByBiospecimenID[parentID];
if (parentBioSpecimen.ParentID) {
parentID = parentBioSpecimen.ParentID;
}
}

if (!(parentID in casesByParticipantID)) {
// console.error(
// `${s.BiospecimenID} does not have a parentID (${parentID}) with diagnosis/demographics information`
// );
return undefined;
} else {
return casesByParticipantID[parentID] as Entity;
}
})
.map((s) =>
getParticipantCaseData(
s,
biospecimenByBiospecimenID,
casesByParticipantID
)
)
.filter((f) => !!f) as BaseSerializableEntity[];
}

Expand Down
2 changes: 1 addition & 1 deletion lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export async function fetchData(): Promise<LoadDataResult> {
const processedSynURL =
process.env.NODE_ENV === 'development'
? '/processed_syn_data.json'
: `${getCloudBaseUrl()}/processed_syn_data_20241226_1531.json`;
: `${getCloudBaseUrl()}/processed_syn_data_20250103_1351.json`;
return fetchSynData(processedSynURL);
}

Expand Down
35 changes: 35 additions & 0 deletions packages/data-portal-commons/src/components/PublicationIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faBook } from '@fortawesome/free-solid-svg-icons';
import Tooltip from 'rc-tooltip';
import React from 'react';

import { PublicationManifest } from '../lib/entity';
import {
getCiteFromPublicationManifest,
getPublicationUid,
} from '../lib/publicationHelpers';

export const PublicationIcon: React.FunctionComponent<{
publicationManifest: PublicationManifest;
}> = (props) => {
const { publicationManifest } = props;

return (
<Tooltip
overlay={getCiteFromPublicationManifest(publicationManifest)}
key={getPublicationUid(publicationManifest)}
>
<a
href={`//${
window.location.host
}/publications/${getPublicationUid(publicationManifest)}`}
key={getPublicationUid(publicationManifest)}
style={{ paddingRight: 3 }}
>
<FontAwesomeIcon icon={faBook} />
</a>
</Tooltip>
);
};

export default PublicationIcon;
1 change: 1 addition & 0 deletions packages/data-portal-commons/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './lib/types';

export * from './components/AtlasDescription';
export * from './components/ExpandableText';
export * from './components/PublicationIcon';
export * from './components/ViewDetailsModal';

export { default as commonStyles } from './components/common.module.scss';
26 changes: 4 additions & 22 deletions packages/data-portal-explore/src/components/AtlasTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ import {
AtlasDescription,
AtlasMetaData,
Entity,
getCiteFromPublicationManifest,
getPublicationUid,
isManuscriptInReview,
PublicationIcon,
PublicationManifest,
} from '@htan/data-portal-commons';
import { ExploreTab } from '../lib/types';
Expand Down Expand Up @@ -270,26 +269,9 @@ export class AtlasTable extends React.Component<IAtlasTableProps> {
if (atlasTableData.publicationManifests.length > 0) {
return atlasTableData.publicationManifests.map(
(publicationManifest) => (
<Tooltip
overlay={getCiteFromPublicationManifest(
publicationManifest
)}
key={getPublicationUid(publicationManifest)}
>
<a
href={`//${
window.location.host
}/publications/${getPublicationUid(
publicationManifest
)}`}
key={getPublicationUid(
publicationManifest
)}
style={{ paddingRight: 3 }}
>
<FontAwesomeIcon icon={faBook} />
</a>
</Tooltip>
<PublicationIcon
publicationManifest={publicationManifest}
/>
)
);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import {
EnhancedDataTable,
getDefaultDataTableStyle,
} from '@htan/data-portal-table';
import { Atlas, Entity } from '@htan/data-portal-commons';
import { Atlas, Entity, PublicationManifest } from '@htan/data-portal-commons';
import { GenericAttributeNames } from '@htan/data-portal-utils';
import { DataSchemaData, SchemaDataId } from '@htan/data-portal-schema';

import {
generateColumnsForDataSchema,
getAtlasColumn,
getPublicationColumn,
sortByBiospecimenId,
sortByParentID,
} from '../lib/dataTableHelpers';
Expand All @@ -21,6 +22,7 @@ interface IBiospecimenTableProps {
synapseAtlases: Atlas[];
schemaDataById?: { [schemaDataId: string]: DataSchemaData };
genericAttributeMap?: { [attr: string]: GenericAttributeNames };
publicationsByUid?: { [uid: string]: PublicationManifest };
}

export const BiospecimenTable: React.FunctionComponent<IBiospecimenTableProps> = (
Expand All @@ -46,11 +48,12 @@ export const BiospecimenTable: React.FunctionComponent<IBiospecimenTableProps> =
columns,
(c) => c.selector === GenericAttributeNames.BiospecimenID
);
// insert Atlas Name right after Biospecimen ID
// insert Atlas Name and Publications right after Biospecimen ID
columns.splice(
indexOfBiospecimenId + 1,
0,
getAtlasColumn(props.synapseAtlases)
getAtlasColumn(props.synapseAtlases),
getPublicationColumn(props.publicationsByUid)
);

return (
Expand Down
Loading