Skip to content

Commit

Permalink
Feat: add transformation dropdown to histogram (#1666)
Browse files Browse the repository at this point in the history
* feat: add transformation dropdown to histogram

...and adjust histogram endpoint call to include histogram concept id in variables list instead
of being part of the url, according to the backend changes implemented for this same endpoint.

* feat: add filters to histogram

* feat: do not filter the histogram data

...instead, we want the filter just to be recorded as part of the concept item
and be reflected in the histogram UI as a line at this point.

* feat: add min and max lines to histogram

* fix: empty data handling and move select and input boxes

* feat: improve comments in Histogram code; remove data-tour

* feat: define and use FILTERS as enumeration options

* feat: executed eslint-new on new histogram feature code

* fix: add missing onchange setters for state values in PhenotypeHistogram

* fix: min/max constraints on PhenotypeHistogram cutoff inputs

* fix: add aria labels and some css improvements

* feat(add_transformration_to_histogram): Added CSS styles and class names to components and storybooks to better match design

* feat(add_transformration_to_histogram): fixed typo in SVG aria-labelledby reference

---------

Co-authored-by: Jarvis Raymond <jarvisraymond@uchicago.edu>
Co-authored-by: Jarvis <113449836+jarvisraymond-uchicago@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 6, 2025
1 parent 68dc9f5 commit b630445
Show file tree
Hide file tree
Showing 14 changed files with 298 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ MockedSuccess.parameters = {
}
),
rest.post(
'http://:cohortmiddlewarepath/cohort-middleware/histogram/by-source-id/:sourceid/by-cohort-definition-id/:cohortdefinitionId/by-histogram-concept-id/:conceptId',
'http://:cohortmiddlewarepath/cohort-middleware/histogram/by-source-id/:sourceid/by-cohort-definition-id/:cohortdefinitionId',
(req, res, ctx) => {
return res(
ctx.delay(2000),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ const AttritionTableModal = ({ modalInfo, setModalInfo }) => {
&& modalInfo.rowObject.variable_type === 'concept' && (
<div data-testid='phenotype-histogram-diagram'>
<PhenotypeHistogram
useInlineErrorMessages
selectedStudyPopulationCohort={modalInfo.selectedCohort}
selectedCovariates={
modalInfo.currentCovariateAndCovariatesFromPrecedingRows
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ WithConceptOutcome.parameters = {
}
),
rest.post(
'http://:cohortmiddlewarepath/cohort-middleware/histogram/by-source-id/:sourceid/by-cohort-definition-id/:cohortdefinitionId/by-histogram-concept-id/:conceptId',
'http://:cohortmiddlewarepath/cohort-middleware/histogram/by-source-id/:sourceid/by-cohort-definition-id/:cohortdefinitionId',
(req, res, ctx) => {
return res(
ctx.delay(2000),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const BarChart = () => (
<svg
className='bar-chart-icon'
role='img'
aria-labelledby='barChartDesc'
aria-labelledby='barChartTitle'
xmlns='http://www.w3.org/2000/svg'
width='16px'
viewBox='0 0 64 64'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types';
import Covariates from './Covariates';
import PhenotypeHistogram from '../Diagrams/PhenotypeHistogram/PhenotypeHistogram';
import FILTERS from '../../../SharedUtils/FiltersEnumeration';
import BarChart from '../AttritionTableWrapper/ChartIcons/BarChart';
import './Covariates.css';

const ContinuousCovariates = ({
Expand All @@ -19,8 +21,57 @@ const ContinuousCovariates = ({
variable_type: 'concept',
concept_id: selected.concept_id,
concept_name: selected.concept_name,
transformation: selected.transformation,
filters: selected.filters,
});

const onChangeTransformation = (selectedTransformationType) => {
setSelected((prevSelected) => {
const transformation = selectedTransformationType.key;
return { ...prevSelected, transformation };
});
};

const onChangeMinOutlierCutoff = (minOutlierCutoff) => {
setSelected((prevSelected) => {
// Ensure filters array exists
const filters = prevSelected.filters ? [...prevSelected.filters] : [];

// Find the index of the ">=" filter
const minFilterIndex = filters.findIndex((filter) => filter.type === FILTERS.greaterThanOrEqualTo);

if (minFilterIndex !== -1) {
// Update the existing ">=" filter
filters[minFilterIndex] = { type: FILTERS.greaterThanOrEqualTo, value: minOutlierCutoff };
} else {
// Add a new ">=" filter
filters.push({ type: FILTERS.greaterThanOrEqualTo, value: minOutlierCutoff });
}

return { ...prevSelected, filters };
});
};

const onChangeMaxOutlierCutoff = (maxOutlierCutoff) => {
setSelected((prevSelected) => {
// Ensure filters array exists
const filters = prevSelected.filters ? [...prevSelected.filters] : [];

// Find the index of the "<=" filter
const maxFilterIndex = filters.findIndex((filter) => filter.type === FILTERS.lessThanOrEqualTo);

if (maxFilterIndex !== -1) {
// Update the existing "<=" filter
filters[maxFilterIndex] = { type: FILTERS.lessThanOrEqualTo, value: maxOutlierCutoff };
} else {
// Add a new "<=" filter
filters.push({ type: FILTERS.lessThanOrEqualTo, value: maxOutlierCutoff });
}

return { ...prevSelected, filters };
});
};

// when a user has selected a outcome phenotype that is a continuous covariate with a concept ID,
// that should not appear as a selectable option, and be included in the submitted covariates.
// If they selected a outcome phenotype that is dichotomous
Expand Down Expand Up @@ -63,18 +114,21 @@ const ContinuousCovariates = ({
</button>
</div>
{selected ? (
<div data-tour='phenotype-histogram'>
<div data-tour='phenotype-histogram' className='phenotype-histogram-component'>
<PhenotypeHistogram
dispatch={dispatch}
selectedStudyPopulationCohort={selectedStudyPopulationCohort}
selectedCovariates={selectedCovariates}
outcome={outcome}
selectedContinuousItem={selected}
handleChangeTransformation={onChangeTransformation}
handleChangeMinOutlierCutoff={onChangeMinOutlierCutoff}
handleChangeMaxOutlierCutoff={onChangeMaxOutlierCutoff}
/>
</div>
) : (
<div data-tour='phenotype-histogram' className='phenotype-histogram-directions'>
Select a concept to render its corresponding histogram
<BarChart />Select a concept to render its corresponding histogram
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,7 @@ SuccessCase.parameters = {
}
),
rest.post(
//histogram/by-source-id/${sourceId}/by-cohort-definition-id/${cohortId}/by-histogram-concept-id/${currentSelection.concept_id}`;
'http://:cohortmiddlewarepath/cohort-middleware/histogram/by-source-id/:sourceid/by-cohort-definition-id/:cohortdefinitionId/by-histogram-concept-id/:conceptId',
'http://:cohortmiddlewarepath/cohort-middleware/histogram/by-source-id/:sourceid/by-cohort-definition-id/:cohortdefinitionId',
(req, res, ctx) => {
const { cohortmiddlewarepath } = req.params;
const { cohortdefinitionId } = req.params;
Expand Down Expand Up @@ -152,8 +151,7 @@ EmptyDataCase.parameters = {
}
),
rest.post(
//histogram/by-source-id/${sourceId}/by-cohort-definition-id/${cohortId}/by-histogram-concept-id/${currentSelection.concept_id}`;
'http://:cohortmiddlewarepath/cohort-middleware/histogram/by-source-id/:sourceid/by-cohort-definition-id/:cohortdefinitionId/by-histogram-concept-id/:conceptId',
'http://:cohortmiddlewarepath/cohort-middleware/histogram/by-source-id/:sourceid/by-cohort-definition-id/:cohortdefinitionId',
(req, res, ctx) => {
const { cohortmiddlewarepath } = req.params;
const { cohortdefinitionId } = req.params;
Expand Down
14 changes: 13 additions & 1 deletion src/Analysis/GWASApp/Components/Covariates/Covariates.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,11 @@
display: block;
float: right;
height: 36px;
margin-right: 5%;
}

.GWASApp .phenotype-histogram .recharts-wrapper {
margin-top: 60px;
margin-top: 10px;
}

.GWASApp .phenotype-histogram .histrogram-loading {
Expand All @@ -111,6 +112,17 @@
width: 600px;
}

.GWASApp .phenotype-histogram-directions svg {
margin-right: 4px;
}

.GWASApp .phenotype-histogram-component {
float: left;
margin-top: 25px;
text-align: left;
width: 600px;
}

/* Dichotomous Covariate Selection */
.GWASApp .custom-dichotomous-covariates .GWASUI-flexRow label {
font-weight: bold;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.GWASUI-column.transformation-dropdown-label {
max-width: 25%;
}

.GWASUI-column.transformation-select {
max-width: 65%;
}

label[for='input-minOutlierCutoff'],
label[for='input-maxOutlierCutoff'] {
margin-top: 3px;
}

.GWASUI-row.outlier-inputs {
margin-top: -8px;
}

.recharts-text.xAxisLabel tspan:nth-child(1) {
font-weight: bold;
fill: black;
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useQuery } from 'react-query';
import { Spin } from 'antd';
import { Select, InputNumber, Spin } from 'antd';
import { fetchHistogramInfo } from '../../../Utils/cohortMiddlewareApi';
import queryConfig from '../../../../SharedUtils/QueryConfig';
import Histogram from '../../../../SharedUtils/DataViz/Histogram/Histogram';
import { useSourceContext } from '../../../Utils/Source';
import ACTIONS from '../../../Utils/StateManagement/Actions';
import { MESSAGES } from '../../../Utils/constants';
import FILTERS from '../../../../SharedUtils/FiltersEnumeration';
import './PhenotypeHistogram.css';

const PhenotypeHistogram = ({
dispatch,
selectedStudyPopulationCohort,
selectedCovariates,
outcome,
selectedContinuousItem,
useInlineErrorMessages,
useAnimation,
handleChangeTransformation,
handleChangeMinOutlierCutoff,
handleChangeMaxOutlierCutoff,
}) => {
const { source } = useSourceContext();
const [inlineErrorMessage, setInlineErrorMessage] = useState(null);
const [minOutlierCutoff, setMinOutlierCutoff] = useState(null);
const [maxOutlierCutoff, setMaxOutlierCutoff] = useState(null);
const [selectedTransformation, setSelectedTransformation] = useState(null);
const sourceId = source; // TODO - change name of source to sourceId for clarity
const { data, status } = useQuery(
[
Expand All @@ -29,13 +36,15 @@ const PhenotypeHistogram = ({
selectedCovariates,
outcome,
selectedContinuousItem.concept_id,
selectedContinuousItem.transformation,
],
() => fetchHistogramInfo(
sourceId,
selectedStudyPopulationCohort.cohort_definition_id,
selectedCovariates,
outcome,
selectedContinuousItem.concept_id,
selectedContinuousItem.transformation,
),
queryConfig,
);
Expand Down Expand Up @@ -82,31 +91,145 @@ const PhenotypeHistogram = ({
xAxisLegend: selectedContinuousItem.concept_name,
yAxisLegend: 'Persons',
useAnimation,
minCutoff:
selectedContinuousItem.filters?.find(
(filter) => filter.type === FILTERS.greaterThanOrEqualTo,
)?.value ?? undefined,
maxCutoff:
selectedContinuousItem.filters?.find(
(filter) => filter.type === FILTERS.lessThanOrEqualTo,
)?.value ?? undefined,
};
return (
<React.Fragment>
{useInlineErrorMessages && inlineErrorMessage}
<Histogram {...histogramArgs} />
{inlineErrorMessage}
{data.bins !== null && (
<div>
<div className='GWASUI-row'>
<div className='GWASUI-column transformation-dropdown-label'>
<label
id='transformation-dropdown-label'
htmlFor='transformation-select'
>
Select Transformation
</label>
</div>
<div className='GWASUI-column transformation-select'>
<Select
id='transformation-select'
showSearch={false}
labelInValue
value={selectedTransformation}
onChange={(value) => {
setSelectedTransformation(value);
handleChangeTransformation(value);
}}
placeholder='-optional transformation-'
fieldNames={{ label: 'description', value: 'type' }}
options={[
{ type: 'log', description: 'log transformation' },
{ type: 'z_score', description: 'z-score transformation' },
]}
aria-label='Transformation Selection'
aria-labelledby='transformation-dropdown-label'
/>
</div>
</div>
<Histogram {...histogramArgs} />
<div className='GWASUI-row outlier-inputs'>
<div className='GWASUI-column'>
<label htmlFor='input-minOutlierCutoff'>
Minimum outlier cutoff
</label>
</div>
<div className='GWASUI-column'>
<InputNumber
id='input-minOutlierCutoff'
value={minOutlierCutoff}
onChange={(value) => {
setMinOutlierCutoff(value);
handleChangeMinOutlierCutoff(value);
}}
min={data.bins[0]?.start || 0}
max={
maxOutlierCutoff
|| data.bins[data.bins.length - 1]?.end
|| 100
}
onKeyDown={(e) => {
const { key } = e;
// Allow only numeric keys, backspace, and delete, and one decimal point
if (
!/[\d]/.test(key)
&& key !== 'Backspace'
&& key !== 'Delete'
&& key !== 'ArrowLeft'
&& key !== 'ArrowRight'
&& (key !== '.' || e.target.value.includes('.'))
) {
e.preventDefault();
}
}}
/>
</div>
<div className='GWASUI-column'>
<label htmlFor='input-maxOutlierCutoff'>
Maximum outlier cutoff
</label>
</div>
<div className='GWASUI-column'>
<InputNumber
id='input-maxOutlierCutoff'
value={maxOutlierCutoff}
onChange={(value) => {
setMaxOutlierCutoff(value);
handleChangeMaxOutlierCutoff(value);
}}
min={minOutlierCutoff || data.bins[0]?.start || 0}
max={data.bins[data.bins.length - 1]?.end || 100}
onKeyDown={(e) => {
const { key } = e;
// Allow only numeric keys, backspace, and delete, and one decimal point
if (
!/[\d]/.test(key)
&& key !== 'Backspace'
&& key !== 'Delete'
&& key !== 'ArrowLeft'
&& key !== 'ArrowRight'
&& (key !== '.' || e.target.value.includes('.'))
) {
e.preventDefault();
}
}}
/>
</div>
</div>
</div>
)}
</React.Fragment>
);
};

PhenotypeHistogram.propTypes = {
useInlineErrorMessages: PropTypes.bool,
dispatch: PropTypes.func,
selectedStudyPopulationCohort: PropTypes.object.isRequired,
selectedCovariates: PropTypes.array,
outcome: PropTypes.object,
selectedContinuousItem: PropTypes.object.isRequired,
useAnimation: PropTypes.bool,
handleChangeTransformation: PropTypes.func,
handleChangeMinOutlierCutoff: PropTypes.func,
handleChangeMaxOutlierCutoff: PropTypes.func,
};

PhenotypeHistogram.defaultProps = {
useInlineErrorMessages: false,
dispatch: null,
selectedCovariates: [],
outcome: null,
useAnimation: true,
handleChangeTransformation: null,
handleChangeMinOutlierCutoff: null,
handleChangeMaxOutlierCutoff: null,
};

export default PhenotypeHistogram;
Loading

0 comments on commit b630445

Please sign in to comment.