Skip to content

Commit

Permalink
TopFeat/results view top loci (#1337)
Browse files Browse the repository at this point in the history
* feat(no-access): add no access message (#1301)

* feat(resultsViewTopLoci): Initial commit

* feat(resultsViewTopLoci): Fixed title for AF

* feat(resultsViewTopLoci): Got search to work on first column

* feat(resultsViewTopLoci): Got search to work on remaining columns

* feat(resultsViewTopLoci): Formatted code

* feat(resultsViewTopLoci): Seperated out logic for filtering into seperate file

* feat(resultsViewTopLoci): Ran linter

* feat(resultsViewTopLoci): Moved files into seperate folder

* feat(resultsViewTopLoci): Wrote unit tests

* feat(resultsViewTopLoci): Ran formatter

* feat(resultsViewTopLoci): Ran formatter and npm test

* feat(resultsViewTopLoci): Added comments back for ESLINT ignores

* feat(resultsViewTopLoci): Reverted unintentially changed file

* feat(resultsViewTopLoci): Updated search function to remove commas and added additional pageSizeOptions

* feat(resultsViewTopLoci): Wrote custom sorting function for variant

* feat(resultsViewTopLoci): Updated sort functions for AF and PVAL to parse Number before sort

* feat(resultsViewTopLoci): Simplified sort method for variant

---------

Co-authored-by: Thanh Dang Nguyen <thanhnd@uchicago.edu>
  • Loading branch information
2 people authored and pieterlukasse committed Aug 1, 2023
1 parent f8ddb3e commit 6308e59
Show file tree
Hide file tree
Showing 11 changed files with 448 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const TopLociTableData = [
{
chrom: '1',
pos: 1000,
ref: 'A',
alt: 'T',
rsids: 'rs123',
nearest_genes: 'GeneA',
af: 0.1,
pval: 0.05,
},
{
chrom: '1',
pos: 1001,
ref: 'A',
alt: 'T',
rsids: 'rs456',
nearest_genes: 'GeneB',
af: 0.1,
pval: 0.05,
},
];

export default TopLociTableData;
26 changes: 26 additions & 0 deletions src/Analysis/GWASResults/Views/Results/Results.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,29 @@ section.data-viz {
.data-viz img {
width: 100%;
}

.top-loci h2 {
text-align: left;
color: #2e77b8;
font-size: 18px;
}

.top-loci th.ant-table-cell.ant-table-column-has-sorters {
background-color: #e9eef2;
font-weight: bold;
}

.top-loci .table-header {
background-color: #cfdbe6;
min-height: 64px;
position: relative;
border-radius: 4px 4px 0px 0px;
}

.top-loci .table-header button {
position: absolute;
top: 50%;
transform: translateY(-50%);
margin-right: 10px;
right: 0;
}
29 changes: 13 additions & 16 deletions src/Analysis/GWASResults/Views/Results/Results.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import {
} from '../../Utils/gwasWorkflowApi';
import LoadingErrorMessage from '../../Components/LoadingErrorMessage/LoadingErrorMessage';
import './Results.css';
import ResultsPheWeb from './ResultsPheWeb';
import ResultsPng from './ResultsPng';
import ResultsPheWeb from './ResultsPheWeb/ResultsPheWeb';
import ResultsPng from './ResultsPng/ResultsPng';

/* eslint no-alert: 0 */ // --> OFF

const Results = () => {
const { selectedRowData } = useContext(SharedContext);
const { name, uid } = selectedRowData;
Expand Down Expand Up @@ -77,18 +76,18 @@ const Results = () => {

const displayManhattanPlot = () => {
// Try the pheweb option first:
let results = data.outputs.parameters.filter((entry) => entry.name === 'pheweb_json_index');
if (results.length !== 0) {
return (
<ResultsPheWeb />
);
let results = data?.outputs?.parameters?.filter(
(entry) => entry.name === 'pheweb_json_index',
);
if (results && results.length !== 0) {
return <ResultsPheWeb />;
}
// If no pheweb json file, try to see if there is a PNG Manhattan plot:
results = data.outputs.parameters.filter((entry) => entry.name === 'manhattan_plot_index');
if (results.length !== 0) {
return (
<ResultsPng />
);
results = data?.outputs?.parameters?.filter(
(entry) => entry.name === 'manhattan_plot_index',
);
if (results && results.length !== 0) {
return <ResultsPng />;
}
// If none of the above, show error:
return (
Expand All @@ -99,9 +98,7 @@ const Results = () => {
return (
<div className='results-view'>
{displayTopSection()}
<section className='data-viz'>
{displayManhattanPlot()}
</section>
<section className='data-viz'>{displayManhattanPlot()}</section>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import React, { useContext } from 'react';
import { useQuery } from 'react-query';
import { Spin, Button } from 'antd';
import * as d3 from 'd3-selection';
import SharedContext from '../../Utils/SharedContext';
import SharedContext from '../../../Utils/SharedContext';
import {
getDataForWorkflowArtifact,
queryConfig,
} from '../../Utils/gwasWorkflowApi';
import LoadingErrorMessage from '../../Components/LoadingErrorMessage/LoadingErrorMessage';
import './Results.css';
import ManhattanPlot from '../../Components/Diagrams/ManhattanPlot/ManhattanPlot';
} from '../../../Utils/gwasWorkflowApi';
import LoadingErrorMessage from '../../../Components/LoadingErrorMessage/LoadingErrorMessage';
import '../Results.css';
import ManhattanPlot from '../../../Components/Diagrams/ManhattanPlot/ManhattanPlot';
import TopLociTable from './TopLociTable/TopLociTable';

/* eslint func-names: 0 */ // --> OFF

const ResultsPheWeb = () => {
const { selectedRowData } = useContext(SharedContext);
const { name, uid } = selectedRowData;
Expand All @@ -24,14 +24,18 @@ const ResultsPheWeb = () => {
const manhattanPlotContainerId = 'manhattan_plot_container';

const downloadManhattanPlot = () => {
const svgAsInnerHTML = d3.select(`#${manhattanPlotContainerId}`).select('svg')
const svgAsInnerHTML = d3
.select(`#${manhattanPlotContainerId}`)
.select('svg')
.attr('version', 1.1)
.attr('xmlns', 'http://www.w3.org/2000/svg')
.attr('xmlns:xlink', 'http://www.w3.org/1999/xlink') // https://stackoverflow.com/questions/59138117/svg-namespace-prefix-xlink-for-href-on-image-is-not-defined - this deprecated xlink is still used by PheWeb
.node().parentNode.innerHTML;

const svgAsInnerHTMLAsUtf8Buffer = Buffer.from(svgAsInnerHTML);
const svgAsInnerHTMLAsBase64 = svgAsInnerHTMLAsUtf8Buffer.toString('base64');
const svgAsInnerHTMLAsBase64 = svgAsInnerHTMLAsUtf8Buffer.toString(
'base64',
);

const svgData = `data:image/svg+xml;base64,${svgAsInnerHTMLAsBase64}`;
const tmpImage = new Image();
Expand All @@ -41,7 +45,13 @@ const ResultsPheWeb = () => {
hiddenCanvas.width = document.body.clientWidth;
hiddenCanvas.height = document.body.clientHeight * 0.6;
const canvasContext = hiddenCanvas.getContext('2d');
canvasContext.drawImage(tmpImage, 0, 0, hiddenCanvas.width, hiddenCanvas.height);
canvasContext.drawImage(
tmpImage,
0,
0,
hiddenCanvas.width,
hiddenCanvas.height,
);
const canvasData = hiddenCanvas.toDataURL('image/png');

const a = document.createElement('a');
Expand Down Expand Up @@ -100,6 +110,7 @@ const ResultsPheWeb = () => {
manhattan_plot_container_id={manhattanPlotContainerId}
/>
</section>
<TopLociTable data={data.unbinned_variants} />
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import React, { useMemo, useState, useEffect } from 'react';
import { Table, Input, Button } from 'antd';
import PropTypes from 'prop-types';
import { SearchOutlined } from '@ant-design/icons';
import downloadTSVFromJson from './downloadTSVFromJSON';
import filterLociTableData from './filterLociTableData';

const TopLociTable = ({ data }) => {
// Adds a variant key value pair with the desired formatting
const tableData = useMemo(
() => data.map((obj) => ({
...obj,
variant: `${obj?.chrom}:${obj?.pos.toLocaleString('en-US')} ${obj?.ref}/
${obj?.alt} (${obj?.rsids})`,
})),
[data],
);

const [filteredData, setFilteredData] = useState(data);
const [lociTableState, setLociTableState] = useState({
variantSearchTerm: '',
nearestGenesSearchTerm: '',
pvalSearchTerm: '',
afSearchTerm: '',
currentPage: 1,
});

useEffect(() => {
setFilteredData(filterLociTableData(tableData, lociTableState));
}, [lociTableState, tableData]);

const handleSearchTermChange = (event, searchTermKey) => {
if (searchTermKey === 'variant') {
setLociTableState({
...lociTableState,
currentPage: 1,
variantSearchTerm: event.target.value,
});
}
if (searchTermKey === 'nearest_genes') {
setLociTableState({
...lociTableState,
currentPage: 1,
nearestGenesSearchTerm: event.target.value,
});
}
if (searchTermKey === 'pval') {
setLociTableState({
...lociTableState,
currentPage: 1,
pvalSearchTerm: event.target.value,
});
}
if (searchTermKey === 'af') {
setLociTableState({
...lociTableState,
currentPage: 1,
afSearchTerm: event.target.value,
});
}
return null;
};

const handleTableChange = (pagination) => {
if (pagination.current !== lociTableState.currentPage) {
// User changes page selection, set page to current pagination selection
return setLociTableState({
...lociTableState,
currentPage: pagination.current,
});
}
// When the user updates sorting set page to first page
return setLociTableState({
...lociTableState,
currentPage: 1,
});
};

const columns = [
{
title: 'Variant',
dataIndex: 'variant',
key: 'variant',
sorter: (a, b) => {
const chromA = Number(a.chrom);
const chromB = Number(b.chrom);
if (chromA === chromB) {
return Number(a.pos) - Number(b.pos);
}
return chromA - chromB;
},

children: [
{
title: (
<Input
placeholder='Search by Variant'
value={lociTableState.variantSearchTerm}
onChange={(event) => handleSearchTermChange(event, 'variant')}
suffix={<SearchOutlined />}
/>
),
dataIndex: 'variant',
},
],
},
{
title: 'Nearest Gene(s)',
dataIndex: 'nearest_genes',
key: 'nearest_genes',
sorter: (a, b) => a.nearest_genes.localeCompare(b.nearest_genes),
children: [
{
title: (
<Input
placeholder='Search by Nearest gene(s)'
suffix={<SearchOutlined />}
value={lociTableState.nearestGenesSearchTerm}
onChange={(event) => handleSearchTermChange(event, 'nearest_genes')}
/>
),
dataIndex: 'nearest_genes',
},
],
},
{
title: 'AF',
dataIndex: 'af',
key: 'af',
sorter: (a, b) => Number(a.af) - Number(b.af),

children: [
{
title: (
<Input
placeholder='Search by Af'
suffix={<SearchOutlined />}
value={lociTableState.afSearchTerm}
onChange={(event) => handleSearchTermChange(event, 'af')}
/>
),
dataIndex: 'af',
},
],
},
{
title: 'P-value',
dataIndex: 'pval',
key: 'pval',
sorter: (a, b) => Number(a.pval) - Number(b.pval),
children: [
{
title: (
<Input
placeholder='Search by P-value'
suffix={<SearchOutlined />}
value={lociTableState.pvalSearchTerm}
onChange={(event) => handleSearchTermChange(event, 'pval')}
/>
),
dataIndex: 'pval',
},
],
},
];

return (
<section className='top-loci'>
<h2>Top Loci</h2>
<div className='table-header'>
<Button onClick={() => downloadTSVFromJson('topLociTSV.tsv', data)}>
Download All Results
</Button>
</div>
<Table
dataSource={filteredData}
columns={columns}
rowKey={(record) => record.pos}
onChange={handleTableChange}
pagination={{
current: lociTableState.currentPage,
defaultPageSize: 10,
showSizeChanger: true,
pageSizeOptions: ['10', '50', '100', '500', '1000'],
}}
/>
</section>
);
};

TopLociTable.propTypes = {
data: PropTypes.array.isRequired,
};

export default TopLociTable;
Loading

0 comments on commit 6308e59

Please sign in to comment.