Skip to content

Commit

Permalink
Consequences (#3073)
Browse files Browse the repository at this point in the history
* sort consequences correctly + show pagination count + make workbench visible to all (but disabled for no kf-investigator)

* sort consequences + move file

* make sure func name is not misleading (filterThanSort.. vs sort)
  • Loading branch information
evans-g-crsj authored Apr 12, 2021
1 parent 82bb73a commit c8d8716
Show file tree
Hide file tree
Showing 12 changed files with 331 additions and 505 deletions.
210 changes: 210 additions & 0 deletions src/components/Variants/Test/consequences.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { generateConsequencesDataLines, filterThanSortConsequencesByImpact } from '../consequences';

describe('Consequences', () => {
describe('sortConsequences', () => {});
it('should sort correctly (desc impact and canonical comes first)', () => {
const generalCase = [
{
node: { biotype: 'protein_coding', impact_score: 4, canonical: true },
},
{
node: { biotype: 'retained_intron', impact_score: 4, canonical: false },
},
{
node: { biotype: 'protein_coding', impact_score: 2, canonical: true },
},
{
node: { biotype: 'protein_coding', impact_score: 2, canonical: false },
},
{
node: { biotype: 'protein_coding', impact_score: 1, canonical: false },
},
];
// @ts-ignore only needed data.
expect(filterThanSortConsequencesByImpact(generalCase)).toEqual([
{
node: {
biotype: 'protein_coding',
canonical: true,
impact_score: 4,
},
},
{
node: {
biotype: 'retained_intron',
canonical: false,
impact_score: 4,
},
},
{
node: {
biotype: 'protein_coding',
canonical: true,
impact_score: 2,
},
},
{
node: {
biotype: 'protein_coding',
canonical: false,
impact_score: 2,
},
},
{
node: {
biotype: 'protein_coding',
canonical: false,
impact_score: 1,
},
},
]);
});

describe('generateConsequencesDataLines', () => {
it('should handle trivial cases ', () => {
const degenerateCase = null;
expect(generateConsequencesDataLines(degenerateCase)).toEqual([]);
const trivialCase = [
{
node: {
impact_score: 1,
canonical: false,
},
},
];

// @ts-ignore only needed data.
expect(generateConsequencesDataLines(trivialCase)).toEqual([
{
node: {
impact_score: 1,
canonical: false,
},
},
]);
});

it(
'should generate one line of data for each symbol' +
'with priority for canonical when scores are equal',
() => {
const case1Canonical = [
{
node: { symbol: 'PALB2', impact_score: 1, canonical: true },
},
{
node: { symbol: 'PALB2', impact_score: 1, canonical: false },
},
{
node: { symbol: 'PALB2', impact_score: 1, canonical: false },
},
{
node: { symbol: 'AC008870.4', impact_score: 1, canonical: true },
},
];

// @ts-ignore only needed data.
expect(generateConsequencesDataLines(case1Canonical)).toEqual([
{
node: {
canonical: true,
impact_score: 1,
symbol: 'PALB2',
},
},
{
node: {
canonical: true,
impact_score: 1,
symbol: 'AC008870.4',
},
},
]);
},
);
it(
'should generate one line of data for each symbol' +
'with priority for highest score over canonical',
() => {
const case2NoCanonical = [
{
node: { symbol: 'PALB2', impact_score: 5, canonical: false },
},
{
node: { symbol: 'PALB2', impact_score: 1, canonical: false },
},
{
node: { symbol: 'PALB2', impact_score: 1, canonical: false },
},
{
node: { symbol: 'AC008870.4', impact_score: 1, canonical: false },
},
];

// @ts-ignore only needed data.
expect(generateConsequencesDataLines(case2NoCanonical)).toEqual([
{
node: {
canonical: false,
impact_score: 5,
symbol: 'PALB2',
},
},
{
node: {
canonical: false,
impact_score: 1,
symbol: 'AC008870.4',
},
},
]);
},
);

it('should generate pick first consequence when all is equal', () => {
const case2NoCanonical = [
{
node: {
symbol: 'PALB2',
impact_score: 1,
canonical: false,
biotype: 'nonsense_mediated_decay',
},
},
{
node: {
symbol: 'PALB2',
impact_score: 1,
canonical: false,
biotype: 'retained_intron',
},
},
{
node: { symbol: 'PALB2', impact_score: 1, canonical: false },
},
{
node: { symbol: 'AC008870.4', impact_score: 1, canonical: false },
},
];

// @ts-ignore only needed data.
expect(generateConsequencesDataLines(case2NoCanonical)).toEqual([
{
node: {
canonical: false,
impact_score: 1,
symbol: 'PALB2',
biotype: 'nonsense_mediated_decay',
},
},
{
node: {
canonical: false,
impact_score: 1,
symbol: 'AC008870.4',
},
},
]);
});
});
});
63 changes: 63 additions & 0 deletions src/components/Variants/consequences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Consequence } from 'store/graphql/variants/models';

const keyNoSymbol = 'noSymbol_';

/*
* Algorithm:
* Input: consequences
* #=====#
* - IF consequence has NO gene (symbol) then keep it;
* - IF consequence has multiple symbols, then keep one consequence per symbol.
* - IF consequence has a symbol filter accordingly to these rules:
* for a given symbol,
* find consequence with highest score s
* IF multiple consequences with same score s then find the one that is canonical
* IF canonical does not exist then grab whatever consequence with score s.
* #=====#
* Output: filtered consequences.
* */
export const filterThanSortConsequencesByImpact = (consequences: Consequence[]) => {
if (!consequences || consequences.length === 0) {
return [];
}
return consequences
.filter((c) => c.node?.impact_score !== null)
.map((c) => ({ ...c }))
.sort((a, b) => {
const isSameScore = a.node.impact_score! === b.node.impact_score!;
const canonicalIsNotFirst = !a.node.canonical && b.node.canonical;
const canonicalNeedsToBeSwapped = isSameScore && canonicalIsNotFirst;
if (canonicalNeedsToBeSwapped) {
return 1;
}
return b.node.impact_score! - a.node.impact_score!;
});
};
type SymbolToConsequences = { [key: string]: Consequence[] };

export const generateConsequencesDataLines = (
rawConsequences: Consequence[] | null,
): Consequence[] => {
if (!rawConsequences || rawConsequences.length === 0) {
return [];
}

const symbolToConsequences: SymbolToConsequences = rawConsequences.reduce<SymbolToConsequences>(
(dict: SymbolToConsequences, consequence: Consequence) => {
const keyForCurrentConsequence = consequence.node?.symbol || keyNoSymbol;
const oldConsequences = dict[keyForCurrentConsequence] || [];
return { ...dict, [keyForCurrentConsequence]: [...oldConsequences, { ...consequence }] };
},
{},
);

return Object.entries(symbolToConsequences).reduce((acc: Consequence[], [key, consequences]) => {
// no gene then show
if (key === keyNoSymbol) {
return [...acc, ...consequences];
}

const highestRanked = filterThanSortConsequencesByImpact(consequences)[0] || {};
return [...acc, { ...highestRanked }];
}, []);
};
2 changes: 1 addition & 1 deletion src/pages/variantEntity/TabFrequencies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ const TabFrequencies = ({ variantId }: OwnProps) => {
<Spin spinning={loading}>
<StackLayout vertical fitContent>
<Space direction={'vertical'} size={'large'}>
<Card title="Internal Cohorts">
<Card title="Kids First Studies">
<Table
dataSource={makeInternalCohortsRows(studies)}
columns={internalColumns}
Expand Down
5 changes: 2 additions & 3 deletions src/pages/variantEntity/TabSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import style from 'style/themes/default/_colors.scss';
import ExpandableTable from 'components/ExpandableTable';

import styles from './tables.module.scss';
import { filterThanSortConsequencesByImpact } from 'components/Variants/consequences';

const { Text } = Typography;

Expand Down Expand Up @@ -63,8 +64,6 @@ const orderGenes = (mSymbolToConsequences: SymbolToConsequences) => {
return Object.entries(mSymbolToConsequences).map(([, values]) => ({ ...values }));
};

const orderConsequences = (consequences: Consequence[]) => [...consequences];

const orderConsequencesForTable = (tableGroups: TableGroup[]) => {
if (!tableGroups || tableGroups.length === 0) {
return [];
Expand All @@ -74,7 +73,7 @@ const orderConsequencesForTable = (tableGroups: TableGroup[]) => {
const consequences = tableGroup.consequences;
return {
...tableGroup,
consequences: orderConsequences(consequences),
consequences: filterThanSortConsequencesByImpact(consequences),
};
});
};
Expand Down
6 changes: 3 additions & 3 deletions src/pages/variantsSearchPage/ConsequencesCell.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { FC } from 'react';
import React from 'react';
import style from './ConsequencesCell.module.scss';
import StackLayout from '@ferlab/ui/core/layout/StackLayout';
import Symbol from './Symbol';
import { toKebabCase } from 'utils';
import { Consequence, Impact } from 'store/graphql/variants/models';
import { generateConsequencesDataLines } from './consequences';
import { generateConsequencesDataLines } from 'components/Variants/consequences';

type OwnProps = {
consequences: Consequence[];
Expand All @@ -23,7 +23,7 @@ const Bullet = ({ colorClassName = '' }) => (
<span className={`${style.bullet} ${colorClassName}`} />
);

const ConsequencesCell: FC<OwnProps> = ({ consequences }) => {
const ConsequencesCell = ({ consequences }: OwnProps) => {
const lines = generateConsequencesDataLines(consequences);
return (
<>
Expand Down
Loading

0 comments on commit c8d8716

Please sign in to comment.