GeneExternalReferences.tsx 9.24 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/**
 * See the NOTICE file distributed with this work for additional information
 * regarding copyright ownership.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

17
import React from 'react';
Andrey Azov's avatar
Andrey Azov committed
18
import { useQuery, gql } from '@apollo/client';
19
import { useParams } from 'react-router';
20
import sortBy from 'lodash/sortBy';
21
import { Pick2 } from 'ts-multipick';
22

23 24 25
import { parseEnsObjectIdFromUrl } from 'src/shared/state/ens-object/ensObjectHelpers';
import { defaultSort } from 'src/content/app/entity-viewer/shared/helpers/transcripts-sorter';

26 27 28 29 30 31 32 33 34
import {
  Accordion,
  AccordionItem,
  AccordionItemHeading,
  AccordionItemPanel,
  AccordionItemButton
} from 'src/shared/components/accordion';
import ExternalReference from 'src/shared/components/external-reference/ExternalReference';

35
import {
36 37
  ExternalReference as ExternalReferenceType,
  ExternalReferencesGroup
38
} from 'src/shared/types/thoas/externalReference';
39
import { EntityViewerParams } from 'src/content/app/entity-viewer/EntityViewer';
40 41
import { Slice } from 'src/shared/types/thoas/slice';
import { FullProductGeneratingContext } from 'src/shared/types/thoas/productGeneratingContext';
42
import { TranscriptMetadata } from 'ensemblRoot/src/shared/types/thoas/metadata';
43 44 45

import styles from './GeneExternalReferences.scss';

46 47 48 49
const QUERY = gql`
  query Gene($stable_id: String!, $genome_id: String!) {
    gene(byId: { stable_id: $stable_id, genome_id: $genome_id }) {
      stable_id
50
      symbol
51 52
      external_references {
        accession_id
53 54 55 56 57 58 59 60 61 62
        name
        description
        url
        source {
          id
          name
        }
      }
      transcripts {
        stable_id
63 64 65 66 67
        slice {
          location {
            length
          }
        }
68 69
        external_references {
          accession_id
70 71 72 73 74 75
          name
          description
          url
          source {
            id
            name
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
          }
        }
        product_generating_contexts {
          product_type
          product {
            external_references {
              accession_id
              name
              description
              url
              source {
                id
                name
              }
            }
91 92
          }
        }
93 94 95 96 97 98 99 100
        metadata {
          canonical {
            value
          }
          mane {
            value
          }
        }
101 102 103 104 105 106 107
      }
    }
  }
`;

type Transcript = {
  stable_id: string;
108 109 110
  slice: Pick2<Slice, 'location', 'length'>;
  product_generating_contexts: Array<
    Pick<FullProductGeneratingContext, 'product_type'> & {
111
      product: { external_references: ExternalReferenceType[] } | null;
112 113
    }
  >;
114
  external_references: ExternalReferenceType[];
115
  metadata: Pick<TranscriptMetadata, 'canonical' | 'mane'>;
116 117
};

118
type Gene = {
119
  symbol: string;
120 121
  stable_id: string;
  transcripts: Transcript[];
122
  external_references: ExternalReferenceType[];
123 124
};

125 126 127 128 129 130
const buildExternalReferencesGroups = (
  externalReferences: ExternalReferenceType[]
) => {
  const externalReferencesGroups: {
    [key: string]: ExternalReferencesGroup;
  } = {};
131

132 133 134 135 136 137
  const sortedExternalReferences = sortBy(
    externalReferences,
    (reference) => reference.source.name
  );

  sortedExternalReferences.forEach((externalReference) => {
138
    const sourceId = externalReference.source.id;
139

140 141 142
    if (!externalReferencesGroups[sourceId]) {
      externalReferencesGroups[sourceId] = {
        source: externalReference.source,
143 144 145 146
        references: []
      };
    }

147 148 149 150 151
    externalReferencesGroups[sourceId].references.push({
      accession_id: externalReference.accession_id,
      url: externalReference.url,
      name: externalReference.name,
      description: externalReference.description
152 153 154
    });
  });

155 156 157 158 159 160 161 162
  // Sort the xrefs within each group based on description (or accession_id when description is empty)
  Object.keys(externalReferencesGroups).forEach((sourceId) => {
    externalReferencesGroups[sourceId].references = sortBy(
      externalReferencesGroups[sourceId].references,
      (reference) => reference.description || reference.accession_id
    );
  });

163
  return externalReferencesGroups;
164 165 166 167 168
};

const GeneExternalReferences = () => {
  const params: EntityViewerParams = useParams();

169 170 171
  const { entityId, genomeId } = params;

  const stableId = entityId ? parseEnsObjectIdFromUrl(entityId).objectId : null;
172 173 174

  const { data, loading } = useQuery<{ gene: Gene }>(QUERY, {
    variables: {
175 176
      stable_id: stableId,
      genome_id: genomeId
177
    },
178
    skip: !stableId
179 180 181 182 183 184 185
  });

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!data || !data.gene) {
186 187
    return <div>No data to display</div>;
  }
188

189 190
  const externalReferencesGroups = buildExternalReferencesGroups(
    data.gene.external_references
191 192
  );
  const { transcripts } = data.gene;
193
  const sortedTranscripts = defaultSort(transcripts);
194

195 196 197
  return (
    <div className={styles.xrefsContainer}>
      <div className={styles.geneDetails}>
198 199
        <span className={styles.geneSymbol}>{data.gene.symbol}</span>
        <span>{data.gene.stable_id}</span>
200 201
      </div>
      <div className={styles.sectionHead}>Gene</div>
202
      {data.gene.external_references && renderXrefs(externalReferencesGroups)}
203
      {sortedTranscripts.length && (
204 205
        <div>
          <div className={styles.sectionHead}>Transcripts</div>
206
          {sortedTranscripts.map((transcript, key) => {
207
            return (
208
              <div key={key}>
209
                <TranscriptXrefs transcript={transcript} />
210 211 212 213 214 215 216 217 218
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
};

219
const TranscriptXrefs = (props: { transcript: Transcript }) => {
220
  const { transcript } = props;
221 222 223 224 225
  const unsortedXrefs = [...transcript.external_references];

  // Add protein level xrefs
  transcript.product_generating_contexts.forEach(
    (product_generating_context) => {
226 227 228 229
      product_generating_context.product &&
        unsortedXrefs.push(
          ...product_generating_context.product.external_references
        );
230
    }
231
  );
232 233 234

  const xrefGroups = buildExternalReferencesGroups(unsortedXrefs);

235
  return (
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
    <Accordion className={styles.xrefAccordion}>
      <AccordionItem className={styles.xrefAccordionItem}>
        <AccordionItemHeading className={styles.xrefAccordionHeader}>
          <AccordionItemButton className={styles.xrefAccordionButton}>
            <div className={styles.transcriptId}>{transcript.stable_id}</div>
          </AccordionItemButton>
        </AccordionItemHeading>
        <AccordionItemPanel className={styles.xrefAccordionItemContent}>
          <div>
            {transcript.external_references && (
              <div className={styles.transcriptXrefs}>
                {renderXrefs(xrefGroups)}
              </div>
            )}
          </div>
        </AccordionItemPanel>
      </AccordionItem>
    </Accordion>
254 255 256
  );
};

257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
const renderXrefs = (xrefGroups: {
  [key: string]: ExternalReferencesGroup;
}) => {
  return Object.values(xrefGroups).map((externalReferencesGroup, key) => {
    if (externalReferencesGroup.references.length === 1) {
      return (
        <div key={key}>
          <ExternalReference
            label={externalReferencesGroup.source.name}
            to={externalReferencesGroup.references[0].url}
            linkText={externalReferencesGroup.references[0].accession_id}
            classNames={{
              container: styles.externalReferenceContainer
            }}
          />
        </div>
      );
    } else {
      return renderXrefGroup(externalReferencesGroup, key);
    }
  });
};

280
const renderXrefGroup = (
281
  externalReferencesGroup: ExternalReferencesGroup,
282 283 284 285 286 287 288 289
  key: number
) => {
  return (
    <div className={styles.accordionContainer} key={key}>
      <Accordion className={styles.xrefAccordion}>
        <AccordionItem className={styles.xrefAccordionItem}>
          <AccordionItemHeading className={styles.xrefAccordionHeader}>
            <AccordionItemButton className={styles.xrefAccordionButton}>
290
              {externalReferencesGroup.source.name}
291 292 293 294
            </AccordionItemButton>
          </AccordionItemHeading>
          <AccordionItemPanel className={styles.xrefAccordionItemContent}>
            <div>
295
              {externalReferencesGroup.references.map((entry, key) => (
296
                <ExternalReference
297 298 299 300 301 302
                  label={
                    entry.description === entry.accession_id ||
                    externalReferencesGroup.source.name === entry.description
                      ? ''
                      : entry.description
                  }
303
                  to={entry.url}
304
                  linkText={entry.accession_id}
305
                  key={key}
306 307 308
                  classNames={{
                    container: styles.externalReferenceContainer
                  }}
309 310 311 312 313 314 315 316 317 318
                />
              ))}
            </div>
          </AccordionItemPanel>
        </AccordionItem>
      </Accordion>
    </div>
  );
};

319
export default GeneExternalReferences;