Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into 582-bubble-filter-sen…
Browse files Browse the repository at this point in the history
…sitivity
  • Loading branch information
bobular committed Nov 10, 2023
2 parents a40d98a + 139dc2e commit 7af2402
Show file tree
Hide file tree
Showing 35 changed files with 748 additions and 877 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ dist
# VSCode config files
.vscode

# IntelliJ config files
.idea

.editorconfig

.pnp.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@ export interface PlotLegendBubbleProps {
valueToDiameterMapper: ((value: number) => number) | undefined;
}

// legend ellipsis function for legend title and legend items (from custom legend work)
// const legendEllipsis = (label: string, ellipsisLength: number) => {
// return (label || '').length > ellipsisLength
// ? (label || '').substring(0, ellipsisLength) + '...'
// : label;
// };

export default function PlotBubbleLegend({
legendMax,
valueToDiameterMapper,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { range } from 'd3';
import { truncateWithEllipsis } from '../../utils/axis-tick-label-ellipsis';

// set props for custom legend function
export interface PlotLegendGradientProps {
Expand All @@ -10,13 +10,6 @@ export interface PlotLegendGradientProps {
showMissingness?: boolean;
}

// legend ellipsis function for legend title and legend items (from custom legend work)
const legendEllipsis = (label: string, ellipsisLength: number) => {
return (label || '').length > ellipsisLength
? (label || '').substring(0, ellipsisLength) + '...'
: label;
};

// make gradient colorscale legend into a component so it can be more easily incorporated into DK's custom legend if we need
export default function PlotGradientLegend({
legendMax,
Expand Down Expand Up @@ -119,15 +112,15 @@ export default function PlotGradientLegend({
<label
title={'No data'}
style={{
cursor: 'pointer',
cursor: 'default',
display: 'flex',
alignItems: 'center',
fontSize: legendTextSize,
color: '#999',
margin: 0,
}}
>
<i>{legendEllipsis('No data', 20)}</i>
<i>{truncateWithEllipsis('No data', 20)}</i>
</label>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ export default function PlotLegend({
width: 400,
overflowX: 'hidden',
overflowY: 'auto',
cursor: 'default',
...containerStyles,
}}
>
<div
title={legendTitle}
// style={{ cursor: 'pointer', fontSize: legendTextSize, fontWeight: 'bold', margin: '0 0 0 0.15em' }}
style={{
cursor: 'pointer',
cursor: 'default',
fontSize: legendTextSize,
fontWeight: 'bold',
marginLeft: '0.15em',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ export default function PlotListLegend({
key={item.label}
title={item.label}
style={{
cursor: 'pointer',
// only those items with checkboxes that actually have data should look clickable
cursor:
showCheckbox && item.hasData ? 'pointer' : 'default',
display: 'flex',
alignItems: 'center',
fontSize: legendTextSize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,33 @@ import { millisecondTodate } from '../../utils/date-format-change';
import { Bar } from '@visx/shape';
import { debounce } from 'lodash';

export type EZTimeFilterDataProp = {
export type TimeSliderDataProp = {
x: string;
y: number;
};

export type EzTimeFilterProps = {
export type TimeSliderProps = {
/** Ez time filter data */
data: EZTimeFilterDataProp[];
data: TimeSliderDataProp[];
/** current state of selectedRange */
selectedRange: { start: string; end: string } | undefined;
/** update function selectedRange */
setSelectedRange: (
selectedRange: { start: string; end: string } | undefined
) => void;
/** optional xAxisRange - will limit the selection to be within this */
xAxisRange?: { start: string; end: string };
/** width */
width?: number;
/** height */
height?: number;
/** color of the selected range */
/** color of the 'has data' bars - default is black */
barColor?: string;
/** color of the selected range - default is lightblue */
brushColor?: string;
/** axis tick and tick label color */
/** axis tick and tick label color - default is black */
axisColor?: string;
/** opacity of selected brush */
/** opacity of selected brush - default is 0.4 */
brushOpacity?: number;
/** debounce rate in millisecond */
debounceRateMs?: number;
Expand All @@ -43,20 +47,22 @@ export type EzTimeFilterProps = {
};

// using forwardRef
function EzTimeFilter(props: EzTimeFilterProps) {
function TimeSlider(props: TimeSliderProps) {
const {
data,
// set default width and height
width = 720,
height = 100,
brushColor = 'lightblue',
barColor = '#333',
axisColor = '#000',
brushOpacity = 0.4,
selectedRange,
setSelectedRange,
// set a default debounce time in milliseconds
debounceRateMs = 500,
disabled = false,
xAxisRange,
} = props;

const resizeTriggerAreas: ResizeTriggerAreas[] = disabled
Expand All @@ -82,25 +88,32 @@ function EzTimeFilter(props: EzTimeFilterProps) {
};

// accessors for data
const getXData = (d: EZTimeFilterDataProp) => new Date(d.x);
const getYData = (d: EZTimeFilterDataProp) => d.y;
const getXData = (d: TimeSliderDataProp) => new Date(d.x);
const getYData = (d: TimeSliderDataProp) => d.y;

const onBrushChange = useMemo(
() =>
debounce((domain: Bounds | null) => {
if (!domain) return;

const { x0, x1 } = domain;

const selectedDomain = {
// x0 and x1 are millisecond value
start: millisecondTodate(x0),
end: millisecondTodate(x1),
};

setSelectedRange(selectedDomain);
// x0 and x1 are millisecond value
const startDate = millisecondTodate(x0);
const endDate = millisecondTodate(x1);
setSelectedRange({
// don't let range go outside the xAxisRange, if provided
start: xAxisRange
? startDate < xAxisRange.start
? xAxisRange.start
: startDate
: startDate,
end: xAxisRange
? endDate > xAxisRange.end
? xAxisRange.end
: endDate
: endDate,
});
}, debounceRateMs),
[setSelectedRange]
[setSelectedRange, xAxisRange]
);

// Cancel any pending onBrushChange requests when this component is unmounted
Expand All @@ -112,8 +125,8 @@ function EzTimeFilter(props: EzTimeFilterProps) {

// bounds
const xBrushMax = Math.max(width - margin.left - margin.right, 0);
// take 80 % of given height considering axis tick/tick labels at the bottom
const yBrushMax = Math.max(0.8 * height - margin.top - margin.bottom, 0);
// take 70 % of given height considering axis tick/tick labels at the bottom
const yBrushMax = Math.max(0.7 * height - margin.top - margin.bottom, 0);

// scaling
const xBrushScale = useMemo(
Expand Down Expand Up @@ -143,24 +156,22 @@ function EzTimeFilter(props: EzTimeFilterProps) {
() =>
selectedRange != null
? {
// If we reenable the fake controlled behaviour of the <Brush> component using the key prop
// then we'll need to figure out why both brush handles drift every time you adjust one of them.
// The issue is something to do with the round-trip conversion of pixel/date/millisecond positions.
// A good place to start looking is here.
start: { x: xBrushScale(new Date(selectedRange.start)) },
end: { x: xBrushScale(new Date(selectedRange.end)) },
}
: undefined,
[selectedRange, xBrushScale]
);

// compute bar width manually as scaleTime is used for Bar chart
const barWidth = xBrushMax / data.length;

// data bar color
const defaultColor = '#333';

// this makes/fakes the brush as a controlled component
const brushKey =
initialBrushPosition != null
? initialBrushPosition.start + ':' + initialBrushPosition.end
: 'no_brush';
// `brushKey` makes/fakes the brush as a controlled component,
const brushKey = 'not_fake_controlled';
// selectedRange != null
// ? selectedRange.start + ':' + selectedRange.end
// : 'no_brush';

return (
<div
Expand All @@ -175,11 +186,18 @@ function EzTimeFilter(props: EzTimeFilterProps) {
{/* use Bar chart */}
{data.map((d, i) => {
const barHeight = yBrushMax - yBrushScale(getYData(d));
const x = xBrushScale(getXData(d));
// calculate the width using the next bin's date:
// subtract a constant to create separate bars for each bin
const barWidth =
i + 1 >= data.length
? 0
: xBrushScale(getXData(data[i + 1])) - x - 1;
return (
<React.Fragment key={i}>
<Bar
key={`bar-${i.toString()}`}
x={xBrushScale(getXData(d))}
x={x}
// In SVG bar chart, y-coordinate increases downward, i.e.,
// y-coordinates of the top and bottom of the bars are 0 and yBrushMax, respectively
// Also, under current yBrushScale, dataY = 0 -> barHeight = 0; dataY = 1 -> barHeight = 60
Expand All @@ -190,8 +208,8 @@ function EzTimeFilter(props: EzTimeFilterProps) {
y={barHeight === 0 ? yBrushMax : (1 / 4) * yBrushMax}
height={(1 / 2) * barHeight}
// set the last data's barWidth to be 0 so that it does not overflow to dragging area
width={i === data.length - 1 ? 0 : barWidth}
fill={defaultColor}
width={barWidth}
fill={barColor}
/>
</React.Fragment>
);
Expand All @@ -210,9 +228,8 @@ function EzTimeFilter(props: EzTimeFilterProps) {
yScale={yBrushScale}
width={xBrushMax}
height={yBrushMax}
margin={margin}
handleSize={8}
// resize
margin={margin /* prevents brushing offset */}
resizeTriggerAreas={resizeTriggerAreas}
brushDirection="horizontal"
initialBrushPosition={initialBrushPosition}
Expand Down Expand Up @@ -249,4 +266,4 @@ function BrushHandle({ x, height, isBrushActive }: BrushHandleRenderProps) {
);
}

export default EzTimeFilter;
export default TimeSlider;
10 changes: 6 additions & 4 deletions packages/libs/components/src/plots/VolcanoPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { ToImgopts } from 'plotly.js';
import { DEFAULT_CONTAINER_HEIGHT } from './PlotlyPlot';
import domToImage from 'dom-to-image';
import './VolcanoPlot.css';
import { truncateWithEllipsis } from '../utils/axis-tick-label-ellipsis';

export interface RawDataMinMaxValues {
x: NumberRange;
Expand Down Expand Up @@ -293,9 +294,9 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref<HTMLDivElement>) {
findNearestDatumOverride={findNearestDatumXY}
margin={{
top: MARGIN_DEFAULT,
right: showFlooredDataAnnotation ? 150 : MARGIN_DEFAULT,
left: MARGIN_DEFAULT,
bottom: MARGIN_DEFAULT,
right: showFlooredDataAnnotation ? 150 : MARGIN_DEFAULT + 10,
left: MARGIN_DEFAULT + 10, // Bottom annotatiions get wide (for right margin, too)
bottom: MARGIN_DEFAULT + 20, // Bottom annotations can get long
}}
>
{/* Set up the axes and grid lines. XYChart magically lays them out correctly */}
Expand All @@ -317,11 +318,12 @@ function VolcanoPlot(props: VolcanoPlotProps, ref: Ref<HTMLDivElement>) {
{...xyAccessors}
>
<AnnotationLabel
subtitle={label}
subtitle={truncateWithEllipsis(label, 30)}
horizontalAnchor="middle"
verticalAnchor="start"
showAnchorLine={false}
showBackground={false}
maxWidth={100}
/>
</Annotation>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React, { useState } from 'react';
import { useState } from 'react';
import { Story, Meta } from '@storybook/react/types-6-0';
import { LinePlotProps } from '../../plots/LinePlot';
import EzTimeFilter, {
EZTimeFilterDataProp,
} from '../../components/plotControls/EzTimeFilter';
import TimeSlider, {
TimeSliderDataProp,
} from '../../components/plotControls/TimeSlider';
import { DraggablePanel } from '@veupathdb/coreui/lib/components/containers';

export default {
title: 'Plot Controls/EzTimeFilter',
component: EzTimeFilter,
title: 'Plot Controls/TimeSlider',
component: TimeSlider,
} as Meta;

// GEMS1 Case Control; x: Enrollment date; y: Weight
Expand Down Expand Up @@ -246,7 +246,7 @@ const LineplotData = {

export const TimeFilter: Story<LinePlotProps> = (args: any) => {
// converting lineplot data to visx format
const timeFilterData: EZTimeFilterDataProp[] = LineplotData.series[0].x.map(
const timeFilterData: TimeSliderDataProp[] = LineplotData.series[0].x.map(
(value, index) => {
// return { x: value, y: LineplotData.series[0].y[index] };
return { x: value, y: LineplotData.series[0].y[index] >= 9 ? 1 : 0 };
Expand Down Expand Up @@ -334,7 +334,7 @@ export const TimeFilter: Story<LinePlotProps> = (args: any) => {
{selectedRange?.start} ~ {selectedRange?.end}
</div>
</div>
<EzTimeFilter
<TimeSlider
data={timeFilterData}
selectedRange={selectedRange}
setSelectedRange={setSelectedRange}
Expand Down
Loading

0 comments on commit 7af2402

Please sign in to comment.