Skip to content

Commit

Permalink
feat: EFP masking (#63)
Browse files Browse the repository at this point in the history
* feat: Added sample masking

* Updated modal css

* Removed unnecessary props

* Updated type annotation
  • Loading branch information
Yukthiw authored Jan 3, 2024
1 parent d927c05 commit 1f85b5a
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 28 deletions.
5 changes: 4 additions & 1 deletion Eplant/views/eFP/EFPPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ export type EFPPreviewProps = {
selected: boolean
colorMode: 'absolute' | 'relative'
data: EFPData
maskThreshold: number
} & BoxProps
export default function EFPPreview({
gene,
view,
selected,
colorMode,
data,
maskThreshold,
...boxProps
}: EFPPreviewProps) {
const colorModeDeferred = React.useDeferredValue(colorMode)
Expand All @@ -54,9 +56,10 @@ export default function EFPPreview({
state={{
renderAsThumbnail: true,
colorMode: colorModeDeferred,
maskThreshold: maskThreshold
}}
geneticElement={gene}
dispatch={() => {}}
dispatch={() => { }}
/>
<div
style={{
Expand Down
68 changes: 68 additions & 0 deletions Eplant/views/eFP/Viewer/MaskModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Import necessary dependencies from Material-UI
import React, { useState } from 'react';
import { Modal, Slider, Typography, Button, useTheme } from '@mui/material';
import { EFPViewerState } from './types';

// Modal component with a slider
interface MaskModalProps {
state: EFPViewerState
onClose: () => void
onSubmit: (threshhold: number) => void
}

const MaskModal = ({ state, onClose, onSubmit }: MaskModalProps) => {
const [sliderValue, setSliderValue] = useState<number>(state.maskThreshold);
const theme = useTheme()
const handleSliderChange = (event: Event, newValue: number | number[]) => {
setSliderValue(newValue as number);
};

const handleClose = () => {
setSliderValue(state.maskThreshold)
onClose();
};

const handleSubmit = () => {
onSubmit(sliderValue);
onClose();
};

return (
<Modal open={state.maskModalVisible} onClose={handleClose}>
<div style={{
width: 350,
height: 200,
padding: 20,
background: theme.palette.background.paperOverlay,
margin: 'auto',
marginTop: 100,
borderRadius: '8px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
}}>
<Typography variant="body2" gutterBottom>
Mask samples if the expression level is within a given range of their standard deviation.
</Typography>
<Slider
value={sliderValue}
onChange={handleSliderChange}
valueLabelDisplay="on"
valueLabelFormat={(value) => `${value}%`}
min={0}
max={100}
/>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={handleClose} style={{ marginRight: 8 }}>
Cancel
</Button>
<Button variant="contained" color="inherit" onClick={handleSubmit}>
Mask Thresholds
</Button>
</div>
</div>
</Modal>
);
};

export default MaskModal;
35 changes: 32 additions & 3 deletions Eplant/views/eFP/Viewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Legend from './legend'
import NotSupported from '@eplant/UI/Layout/ViewNotSupported'
import Dropdown from '@eplant/UI/Dropdown'
import GeneDistributionChart from './GeneDistributionChart'
import MaskModal from './MaskModal'

type EFPListProps = {
geneticElement: GeneticElement
Expand All @@ -31,6 +32,7 @@ type EFPListProps = {
>['dispatch']
height: number
colorMode: 'absolute' | 'relative'
maskThreshold: number
}

const EFPListItem = React.memo(
Expand All @@ -50,6 +52,7 @@ const EFPListItem = React.memo(
gene={data.geneticElement}
selected={data.views[i].id == data.activeView.id}
view={data.views[i]}
maskThreshold={data.maskThreshold}
onClick={() => {
startTransition(() => {
data.dispatch({ type: 'set-view', id: data.views[i].id })
Expand Down Expand Up @@ -116,6 +119,8 @@ export default class EFPViewer
zoom: 1,
},
sortBy: 'name',
maskThreshold: 100,
maskModalVisible: false,
}
}
constructor(
Expand All @@ -126,7 +131,7 @@ export default class EFPViewer
public icon: () => JSX.Element,
public description?: string,
public thumbnail?: string,
) {}
) { }
getInitialData = async (
gene: GeneticElement | null,
loadEvent: (progress: number) => void,
Expand Down Expand Up @@ -157,6 +162,7 @@ export default class EFPViewer
viewData: viewData,
efps: this.efps,
colorMode: 'absolute' as const,

}
}
reducer = (state: EFPViewerState, action: EFPViewerAction) => {
Expand Down Expand Up @@ -192,6 +198,16 @@ export default class EFPViewer
? ('relative' as const)
: ('absolute' as const),
}
case 'toggle-mask-modal':
return {
...state,
maskModalVisible: state.maskModalVisible ? false : true
}
case 'set-mask-threshold':
return {
...state,
maskThreshold: action.threshold
}
default:
return state
}
Expand Down Expand Up @@ -238,9 +254,10 @@ export default class EFPViewer
state={{
colorMode: props.state.colorMode,
renderAsThumbnail: false,
maskThreshold: props.state.maskThreshold
}}
geneticElement={props.geneticElement}
dispatch={() => {}}
dispatch={() => { }}
/>
)
}, [
Expand All @@ -249,6 +266,7 @@ export default class EFPViewer
props.dispatch,
sortedViewData[activeViewIndex],
props.state.colorMode,
props.state.maskThreshold
])
const ref = React.useRef<HTMLDivElement>(null)
const dimensions = useDimensions(ref)
Expand Down Expand Up @@ -289,7 +307,7 @@ export default class EFPViewer
}}
>
{/* Dropdown menus for selecting a view and sort options
//TODO: Make the dropdown menus appear closer to the button, left aligned and with a max height */}
<Box sx={{ marginBottom: 1 }}>
<Dropdown
Expand Down Expand Up @@ -350,6 +368,7 @@ export default class EFPViewer
geneticElement={props.geneticElement}
views={sortedEfps}
colorMode={props.state.colorMode}
maskThreshold={props.state.maskThreshold}
/>
</Box>
<Box
Expand All @@ -365,6 +384,11 @@ export default class EFPViewer
data={{ ...props.activeData.viewData[activeViewIndex] }}
/>
)}
<MaskModal
state={props.state}
onClose={() => props.dispatch({ type: 'toggle-mask-modal' })}
onSubmit={(threshold) => props.dispatch({ type: 'set-mask-threshold', threshold: threshold })}
/>
<Legend
sx={(theme) => ({
position: 'absolute',
Expand All @@ -378,6 +402,7 @@ export default class EFPViewer
state={{
colorMode: props.state.colorMode,
renderAsThumbnail: false,
maskThreshold: props.state.maskThreshold
}}
/>
<PanZoom
Expand Down Expand Up @@ -428,6 +453,10 @@ export default class EFPViewer
action: { type: 'toggle-color-mode' },
render: (props) => <>Toggle data mode: {props.state.colorMode}</>,
},
{
action: { type: 'toggle-mask-modal' },
render: () => <>Mask data</>
}
]
header: View<EFPViewerData, EFPViewerState, EFPViewerAction>['header'] = (
props,
Expand Down
4 changes: 4 additions & 0 deletions Eplant/views/eFP/Viewer/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type EFPViewerState = {
transform: Transform
colorMode: ColorMode
sortBy: EFPViewerSortTypes
maskModalVisible: boolean
maskThreshold: number
}

export type EFPViewerAction =
Expand All @@ -26,3 +28,5 @@ export type EFPViewerAction =
| { type: 'set-transform'; transform: Transform }
| { type: 'toggle-color-mode' }
| { type: 'sort-by'; by: EFPViewerSortTypes }
| { type: 'toggle-mask-modal' }
| { type: 'set-mask-threshold'; threshold: number }
29 changes: 14 additions & 15 deletions Eplant/views/eFP/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import GeneticElement from '@eplant/GeneticElement'
import { CircularProgress, Typography } from '@mui/material'
import React from 'react'
import { View, ViewProps } from '@eplant/View'
import { useEFPSVG, useStyles } from './svg'
import {
Expand All @@ -15,19 +14,21 @@ import {
import _ from 'lodash'
import { ViewDataError } from '@eplant/View/viewData'
import SVGTooltip from './Tooltips/EFPTooltip'
import React, { useState, useMemo, useEffect, useLayoutEffect } from 'react'

export default class EFP implements View<EFPData, EFPState, EFPAction> {
getInitialState: () => EFPState = () => ({
colorMode: 'absolute',
renderAsThumbnail: false,
maskThreshold: 100
})
tooltipComponent: (props: {
el: SVGElement | null
group: EFPGroup
tissue: EFPTissue
data: EFPData
state: EFPState
}) => React.JSX.Element
}) => JSX.Element
constructor(
public name: string,
public id: EFPId,
Expand All @@ -51,9 +52,8 @@ export default class EFP implements View<EFPData, EFPState, EFPAction> {
const database = xml.getElementsByTagName('view')[0]?.getAttribute('db')
let webservice = xml.getElementsByTagName('webservice')[0]?.textContent
if (!webservice)
webservice = `https://bar.utoronto.ca/eplant/cgi-bin/plantefp.cgi?datasource=${
database ?? 'atgenexp_plus'
}&`
webservice = `https://bar.utoronto.ca/eplant/cgi-bin/plantefp.cgi?datasource=${database ?? 'atgenexp_plus'
}&`

// Get a list of groups and samples
const sampleNames: string[] = []
Expand Down Expand Up @@ -95,9 +95,9 @@ export default class EFP implements View<EFPData, EFPState, EFPAction> {
chunks.map((names) =>
fetch(
webservice +
`id=${gene.id}&samples=${encodeURIComponent(
JSON.stringify(names),
)}`,
`id=${gene.id}&samples=${encodeURIComponent(
JSON.stringify(names),
)}`,
)
.then((res) => res.json())
.then(
Expand Down Expand Up @@ -176,17 +176,16 @@ export default class EFP implements View<EFPData, EFPState, EFPAction> {
showText: !props.state.renderAsThumbnail,
},
)

const { svg } = view ?? {}
const id =
'svg-container-' +
this.id +
'-' +
(props.geneticElement?.id ?? 'no-gene') +
'-' +
React.useMemo(() => Math.random().toString(16).slice(3), [])
const styles = useStyles(id, props.activeData, props.state.colorMode)
React.useEffect(() => {
useMemo(() => Math.random().toString(16).slice(3), [])
const styles = useStyles(id, props.activeData, props.state.colorMode, props.state.maskThreshold)
useEffect(() => {
const el = document.createElement('style')
el.innerHTML = styles
document.head.appendChild(el)
Expand All @@ -196,15 +195,15 @@ export default class EFP implements View<EFPData, EFPState, EFPAction> {
}, [props.activeData.groups, styles])

// Add tooltips to svg
const [svgElements, setSvgElements] = React.useState<
const [svgElements, setSvgElements] = useState<
{
el: SVGElement
group: EFPGroup
tissue: EFPTissue
}[]
>([])

const svgDiv = React.useMemo(() => {
const svgDiv = useMemo(() => {
return (
<div
style={{
Expand All @@ -221,7 +220,7 @@ export default class EFP implements View<EFPData, EFPState, EFPAction> {
)
}, [svg, id])

React.useLayoutEffect(() => {
useLayoutEffect(() => {
const elements = Array.from(
props.activeData.groups.flatMap((group) =>
group.tissues.map((t) => ({
Expand Down
Loading

0 comments on commit 1f85b5a

Please sign in to comment.