Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into map-collection-variab…
Browse files Browse the repository at this point in the history
…le-map-type
  • Loading branch information
dmfalke committed Sep 19, 2023
2 parents eb7f285 + 1d88dbc commit 41ea287
Show file tree
Hide file tree
Showing 81 changed files with 2,255 additions and 561 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
with:
node-version: '14'
cache: 'yarn'
- uses: nrwl/nx-set-shas@v3
- uses: nrwl/nx-set-shas@v3.0.0
- run: yarn
- run: yarn nx affected --target=build-npm-modules --parallel=3
- run: yarn nx affected --target=compile:check
7 changes: 2 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,9 @@ COPY packages packages
ARG NODE_OPTIONS=--max-old-space-size=4096

# Build the client bundles
RUN echo "Building with NODE_OPTIONS=$NODE_OPTIONS"
# RUN echo "Building with NODE_OPTIONS=$NODE_OPTIONS"
RUN yarn \
&& yarn nx bundle:npm @veupathdb/clinepi-site \
&& yarn nx bundle:npm @veupathdb/genomics-site \
&& yarn nx bundle:npm @veupathdb/mbio-site \
&& yarn nx bundle:npm @veupathdb/ortho-site
&& yarn nx run-many --target=bundle:npm --parallel=1


# # # # # # # # # # # # # # # # #
Expand Down
5 changes: 5 additions & 0 deletions nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@
"^build-npm-modules"
]
},
"compile:check": {
"dependsOn": [
"^build-npm-modules"
]
},
"bundle:dev": {
"dependsOn": [
"^build-npm-modules"
Expand Down
1 change: 1 addition & 0 deletions packages/libs/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@typescript-eslint/parser": "^5.46.0",
"@veupathdb/coreui": "workspace:^",
"@visx/axis": "^3.1.0",
"@visx/brush": "^3.0.1",
"@visx/gradient": "^1.0.0",
"@visx/group": "^1.0.0",
"@visx/hierarchy": "^1.0.0",
Expand Down
252 changes: 252 additions & 0 deletions packages/libs/components/src/components/plotControls/EzTimeFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
import React, { useEffect, useMemo } from 'react';
import { scaleTime, scaleLinear } from '@visx/scale';
import { Brush } from '@visx/brush';
// add ResizeTriggerAreas type
import { Bounds, ResizeTriggerAreas } from '@visx/brush/lib/types';
import { Group } from '@visx/group';
import { max, extent } from 'd3-array';
import { BrushHandleRenderProps } from '@visx/brush/lib/BrushHandle';
import { AxisBottom } from '@visx/axis';
import { millisecondTodate } from '../../utils/date-format-change';
import { Bar } from '@visx/shape';
import { debounce } from 'lodash';

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

export type EzTimeFilterProps = {
/** Ez time filter data */
data: EZTimeFilterDataProp[];
/** current state of selectedRange */
selectedRange: { start: string; end: string } | undefined;
/** update function selectedRange */
setSelectedRange: (
selectedRange: { start: string; end: string } | undefined
) => void;
/** width */
width?: number;
/** height */
height?: number;
/** color of the selected range */
brushColor?: string;
/** axis tick and tick label color */
axisColor?: string;
/** opacity of selected brush */
brushOpacity?: number;
/** debounce rate in millisecond */
debounceRateMs?: number;
/** all user-interaction disabled */
disabled?: boolean;
};

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

const resizeTriggerAreas: ResizeTriggerAreas[] = disabled
? []
: ['left', 'right'];

// define default values
const margin = { top: 0, bottom: 10, left: 10, right: 10 };
const selectedBrushStyle = {
fill: disabled ? 'lightgray' : brushColor,
stroke: disabled ? 'lightgray' : brushColor,
fillOpacity: brushOpacity,
// need to set this to be 1?
strokeOpacity: 1,
};

// axis props
const axisBottomTickLabelProps = {
textAnchor: 'middle' as const,
fontFamily: 'Arial',
fontSize: 10,
fill: axisColor,
};

// accessors for data
const getXData = (d: EZTimeFilterDataProp) => new Date(d.x);
const getYData = (d: EZTimeFilterDataProp) => 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);
}, debounceRateMs),
[setSelectedRange]
);

// Cancel any pending onBrushChange requests when this component is unmounted
useEffect(() => {
return () => {
onBrushChange.cancel();
};
}, []);

// 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);

// scaling
const xBrushScale = useMemo(
() =>
scaleTime<number>({
range: [0, xBrushMax],
domain:
data != null ? (extent(data, getXData) as [Date, Date]) : undefined,
}),
[data, xBrushMax]
);

const yBrushScale = useMemo(
() =>
scaleLinear({
range: [yBrushMax, 0],
domain: [0, max(data, getYData) || 1],
// set zero: false so that it does not include zero line in the middle of y-axis
// this is useful when all data have zeros
zero: false,
}),
[data, yBrushMax]
);

// initial selectedRange position
const initialBrushPosition = useMemo(
() =>
selectedRange != null
? {
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';

return (
<div
style={{
// centering time filter
textAlign: 'center',
pointerEvents: disabled ? 'none' : 'all',
}}
>
<svg width={width} height={height}>
<Group left={margin.left} top={margin.top}>
{/* use Bar chart */}
{data.map((d, i) => {
const barHeight = yBrushMax - yBrushScale(getYData(d));
return (
<React.Fragment key={i}>
<Bar
key={`bar-${i.toString()}`}
x={xBrushScale(getXData(d))}
// 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
// Thus, to mimick the bar shape of the ez time filter mockup,
// starting y-coordinate for dataY = 1 sets to be 1/4*yBrushMax.
// And, the height prop is set to be 1/2*yBrushMax so that
// the bottom side of a bar has 1/4*yBrushMax space with respect to the x-axis line
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}
/>
</React.Fragment>
);
})}
<AxisBottom
top={yBrushMax}
scale={xBrushScale}
numTicks={width > 520 ? 10 : 5}
stroke={axisColor}
tickStroke={axisColor}
tickLabelProps={axisBottomTickLabelProps}
/>
<Brush
key={brushKey}
xScale={xBrushScale}
yScale={yBrushScale}
width={xBrushMax}
height={yBrushMax}
margin={margin}
handleSize={8}
// resize
resizeTriggerAreas={resizeTriggerAreas}
brushDirection="horizontal"
initialBrushPosition={initialBrushPosition}
onChange={onBrushChange}
onClick={disabled ? () => {} : () => setSelectedRange(undefined)}
selectedBoxStyle={selectedBrushStyle}
useWindowMoveEvents
disableDraggingSelection={disabled}
renderBrushHandle={(props) => <BrushHandle {...props} />}
/>
</Group>
</svg>
</div>
);
}

// define brush handle shape and position
function BrushHandle({ x, height, isBrushActive }: BrushHandleRenderProps) {
const pathWidth = 8;
const pathHeight = 15;
if (!isBrushActive) {
return null;
}
return (
<Group left={x + pathWidth / 2} top={(height - pathHeight) / 2}>
<path
fill="#f2f2f2"
d="M -4.5 0.5 L 3.5 0.5 L 3.5 15.5 L -4.5 15.5 L -4.5 0.5 M -1.5 4 L -1.5 12 M 0.5 4 L 0.5 12"
stroke="#999999"
strokeWidth="1"
style={{ cursor: 'ew-resize' }}
/>
</Group>
);
}

export default EzTimeFilter;
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ export default function PlotBubbleLegend({
// The largest circle's value will be the first number that's larger than
// legendMax and has only one significant digit. Each smaller circle will
// be half the size of the last (rounded and >= 1)
const legendMaxLog10 = Math.floor(Math.log10(legendMax));
const roundedOneSigFig = Number(legendMax.toPrecision(1));
const largestCircleValue =
legendMax <= 10
? legendMax
: (Number(legendMax.toPrecision(1)[0]) + 1) * 10 ** legendMaxLog10;
: roundedOneSigFig < legendMax
? roundedOneSigFig + 10 ** Math.floor(Math.log10(legendMax)) // effectively rounding up
: roundedOneSigFig; // no need to adjust - already rounded up
const circleValues = _.uniq(
range(numCircles)
.map((i) => Math.round(largestCircleValue / 2 ** i))
Expand Down
24 changes: 21 additions & 3 deletions packages/libs/components/src/components/widgets/LabelledGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export interface LabelledGroupProps {
label: ReactNode;
/** Additional styles to apply to the widget container. */
containerStyles?: React.CSSProperties;
/** Aligns children horizontally beneath the label; defaults to false */
alignChildrenHorizontally?: boolean;
}

/**
Expand All @@ -18,7 +20,12 @@ export interface LabelledGroupProps {
* But renders nothing if no children are contained within it.
*/
export default function LabelledGroup(props: LabelledGroupProps) {
const { children, label, containerStyles } = props;
const {
children,
label,
containerStyles,
alignChildrenHorizontally = false,
} = props;

// don't render anything if all the children (or no children) are null
if (every(React.Children.toArray(children), (child) => child == null))
Expand All @@ -40,11 +47,22 @@ export default function LabelledGroup(props: LabelledGroupProps) {
}}
>
{/* wrapper div to prevent from inline-flex */}
<div>
<div
style={
alignChildrenHorizontally
? { display: 'flex', width: '100%', flexWrap: 'wrap' }
: undefined
}
>
{label && (
<Typography
variant="button"
style={{ color: DARKEST_GRAY, fontWeight: 500, fontSize: '1.2em' }}
style={{
color: DARKEST_GRAY,
fontWeight: 500,
fontSize: '1.2em',
...(alignChildrenHorizontally ? { width: '100%' } : {}),
}}
>
{label}
</Typography>
Expand Down
Loading

0 comments on commit 41ea287

Please sign in to comment.