Skip to content

Commit

Permalink
Update RAMPAGE tab (#491)
Browse files Browse the repository at this point in the history
* Replace Rampage plot with new bar plot

* DL, Search, Multiselect improvements

* Dynamically figure out spaceForLabel

* Small tweaks

* Update BarPlot.tsx

* Cleanup

* Working resizing
  • Loading branch information
jpfisher72 authored Nov 12, 2024
1 parent 2096359 commit c3e9799
Show file tree
Hide file tree
Showing 9 changed files with 530 additions and 510 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React, { useCallback, useMemo, useRef } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Bar } from '@visx/shape';
import { scaleBand, scaleLinear } from '@visx/scale';
import { AxisTop } from '@visx/axis';
import { Group } from '@visx/group';
import { Text } from '@visx/text';
import { useParentSize } from '@visx/responsive';
import { defaultStyles as defaultTooltipStyles, useTooltip, TooltipWithBounds, Portal } from '@visx/tooltip';
import { CircularProgress } from '@mui/material';

const fontFamily = "Roboto,Helvetica,Arial,sans-serif"

export interface BarData<T> {
category: string;
Expand All @@ -24,13 +27,14 @@ export interface BarPlotProps<T> {
}

const VerticalBarPlot = <T,>({
data = [],
data,
SVGref,
topAxisLabel,
onBarClicked,
TooltipContents
}: BarPlotProps<T>) => {
const { parentRef, width: ParentWidth } = useParentSize({ debounceTime: 150 });
const [spaceForLabel, setSpaceForLabel] = useState(200) //this needs to be initialized with zero. Will break useEffect if changed
const [labelSpaceDecided, setLabelSpaceDecided] = useState(false)
const { tooltipOpen, tooltipLeft, tooltipTop, tooltipData, hideTooltip, showTooltip } = useTooltip<BarData<T>>({});
const requestRef = useRef<number | null>(null);
const tooltipDataRef = useRef<{ top: number; left: number; data: BarData<T> } | null>(null);
Expand All @@ -55,66 +59,108 @@ const VerticalBarPlot = <T,>({
}
}, [showTooltip]);

const width = useMemo(() => Math.max(500, ParentWidth), [ParentWidth])

/**
* @todo it'd be nice to somehow extract these from what is passed to the component. This is hardcoded to fit the gene expression data
*/
const { parentRef, width: ParentWidth } = useParentSize({ debounceTime: 150 });
const width = useMemo(() => Math.max(750, ParentWidth), [ParentWidth])
const spaceForTopAxis = 50
const spaceOnBottom = 20
const spaceForCategory = 120
const spaceForLabel = 280

const height = data.length * 20 + spaceForTopAxis + spaceOnBottom

// Dimensions
const xMax = width - spaceForCategory - spaceForLabel;
const yMax = height - spaceForTopAxis - spaceOnBottom;
const gapBetweenTextAndBar = 10
const dataHeight = data.length * 20
const totalHeight = dataHeight + spaceForTopAxis + spaceOnBottom

// Scales
const yScale = useMemo(() =>
scaleBand<string>({
domain: data.map((d) => d.label),
range: [0, yMax],
range: [0, dataHeight],
padding: 0.2,
}), [data, yMax])
}), [data, dataHeight])

const xScale = useMemo(() =>
scaleLinear<number>({
domain: [0, Math.max(...data.map((d) => d.value))],
range: [0, Math.max(xMax, 0)],
}), [data, xMax])
range: [0, Math.max(width - spaceForCategory - spaceForLabel, 0)],
}), [data, spaceForLabel, width])

//This feels really dumb but I couldn't figure out a better way to have the labels not overflow sometimes - JF 11/8/24
//Whenever xScale is adjusted, it checks to see if any of the labels overflow the container, and if so
//it sets the spaceForLabel to be the amount overflowed.
useEffect(() => {
const containerWidth = document.getElementById('outerSVG')?.clientWidth
if (!containerWidth) { return }

let maxOverflow = 0
let minUnderflow: number = null
// let maxOverflowingPoint: [BarData<T>, { textWidth: number, barWidth: number, totalWidth: number, overflow: number }]

data.forEach((d, i) => {
const textElement = document.getElementById(`label-${i}`) as unknown as SVGSVGElement;

if (textElement) {
const textWidth = textElement.getBBox().width;
const barWidth = xScale(d.value);

const totalWidth = spaceForCategory + barWidth + gapBetweenTextAndBar + textWidth
const overflow = totalWidth - containerWidth

maxOverflow = Math.max(overflow, maxOverflow)
if (overflow < 0) {
if (minUnderflow === null) {
minUnderflow = Math.abs(overflow)
} else {
minUnderflow = Math.min(Math.abs(overflow), minUnderflow)
}
}
}
});

if (maxOverflow > 0) { //ensure nothing is cut off
setLabelSpaceDecided(false)
setSpaceForLabel((prev) => {
return prev + 25
})
} else if (minUnderflow > 30) { //ensure not too much space is left empty
setLabelSpaceDecided(false)
setSpaceForLabel((prev) => {
return prev - 25
})
} else { //If there is no overflow or underflow to handle
setLabelSpaceDecided(true)
}

}, [data, xScale]);

return (
<div ref={parentRef}>
<div ref={parentRef} style={{position: "relative"}}>
{data.length === 0 ?
<p>No Data To Display</p>
:
<svg width={width} height={height} ref={SVGref}>
<Group left={spaceForCategory} top={spaceForTopAxis}>
:
<svg ref={SVGref} width={width} height={totalHeight} opacity={(labelSpaceDecided && ParentWidth > 0) ? 1 : 0.3} id={'outerSVG'}>
<Group left={spaceForCategory} top={spaceForTopAxis} >
{/* Top Axis with Label */}
<AxisTop scale={xScale} top={0} label={topAxisLabel} labelProps={{dy: -5, fontSize: 16}} numTicks={width < 600 ? 4 : undefined} />
{data.map((d) => {
<AxisTop scale={xScale} top={0} label={topAxisLabel} labelProps={{ dy: -5, fontSize: 16, fontFamily: fontFamily }} numTicks={width < 600 ? 4 : undefined} />
{data.map((d, i) => {
const barHeight = yScale.bandwidth();
const barWidth = xScale(d.value) ?? 0;
const barY = yScale(d.label);
const barX = 0;
return (
<Group
key={d.label}
key={i}
onClick={() => onBarClicked && onBarClicked(d)}
style={onBarClicked && { cursor: 'pointer' }}
onMouseMove={(event) => handleMouseMove(event, d)}
onMouseLeave={() => hideTooltip()}
fontFamily={fontFamily}
>
{/* Category label to the left of each bar */}
<Text
x={-10} // Positioning slightly to the left of the bar
x={-gapBetweenTextAndBar} // Positioning slightly to the left of the bar
y={(barY ?? 0) + barHeight / 2}
dy=".35em"
fontSize={12}
textAnchor="end"
fill="black"
fontSize={12}
>
{d.category}
</Text>
Expand All @@ -130,11 +176,12 @@ const VerticalBarPlot = <T,>({
/>
{/* Value label next to the bar */}
<Text
x={barX + barWidth + 5} // Position label slightly after the end of the bar
id={`label-${i}`}
x={barX + barWidth + gapBetweenTextAndBar} // Position label slightly after the end of the bar
y={(barY ?? 0) + barHeight / 2}
dy=".35em" // Vertically align to the middle of the bar
fontSize={12}
fill="black"
fontSize={12}
>
{d.label}
</Text>
Expand All @@ -144,7 +191,12 @@ const VerticalBarPlot = <T,>({
})}
</Group>
</svg>

}
{/* Loading Wheel for resizing */}
{!labelSpaceDecided &&
<div style={{display: "flex", position: "absolute", inset: 0, justifyContent: "center"}}>
<CircularProgress sx={{mt: 10}}/>
</div>
}
{/* Maybe should provide a default tooltip */}
{TooltipContents && tooltipOpen && (
Expand Down
11 changes: 5 additions & 6 deletions screen2.0/src/app/applets/gene-expression/MultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import Autocomplete, { AutocompleteChangeDetails, AutocompleteChangeReason } fro
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
import { Box, Divider, FormControlLabel, Paper } from '@mui/material';

function capitalizeWords(input: string): string {
return input.replace(/\b\w/g, char => char.toUpperCase());
}
import { capitalizeWords } from '../../search/_ccredetails/utils';

const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
Expand All @@ -20,14 +17,16 @@ export interface MultiSelectProps {
options: string[],
placeholder: string
limitTags?: number
size?: "small" | "medium"
//todo add width here
}

const MultiSelect = ({
onChange,
options,
placeholder,
limitTags
limitTags,
size = "small"
}: MultiSelectProps) => {
const [value, setValue] = React.useState<string[] | null>(options);
const scrollRef = React.useRef<number>(0) //needed to preserve the scroll position of the ListBox. Overriding PaperComponent changes the way that the Listbox renders and resets scroll on interaction
Expand Down Expand Up @@ -93,7 +92,7 @@ const MultiSelect = ({
<Autocomplete
multiple
limitTags={limitTags}
size='small'
size={size}
value={value}
onChange={handleChange}
id="checkboxes-tags-demo"
Expand Down
51 changes: 0 additions & 51 deletions screen2.0/src/app/applets/gene-expression/const.ts

This file was deleted.

Loading

0 comments on commit c3e9799

Please sign in to comment.