Skip to content

Commit 2c2496c

Browse files
authored
Merge pull request #272 from VEuPathDB/faceting-#259
Faceting #259
2 parents e764d23 + 795102b commit 2c2496c

22 files changed

+1161
-287
lines changed

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
"url": "https://github.com/veupathdb/web-components.git"
99
},
1010
"dependencies": {
11+
"@emotion/react": "^11.7.0",
1112
"@material-ui/core": "^4.11.2",
1213
"@material-ui/icons": "^4.11.2",
1314
"@material-ui/lab": "^4.0.0-alpha.57",
1415
"@types/d3": "^7.1.0",
1516
"@types/date-arithmetic": "^4.1.1",
17+
"@veupathdb/core-components": "^0.2.46",
1618
"@visx/gradient": "^1.0.0",
1719
"@visx/group": "^1.0.0",
1820
"@visx/hierarchy": "^1.0.0",

src/components/ContingencyTable.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { CSSProperties } from 'react';
2-
import { MosaicData } from '../types/plots/mosaic';
2+
import { MosaicPlotData } from '../types/plots/mosaicPlot';
33
import _ from 'lodash';
44
import { FacetedData } from '../types/plots';
55
import { isFaceted } from '../types/guards';
66
import Spinner from '../components/Spinner';
77

88
interface ContingencyTableProps {
9-
data?: MosaicData | FacetedData<MosaicData>;
9+
data?: MosaicPlotData | FacetedData<MosaicPlotData>;
1010
independentVariable: string;
1111
dependentVariable: string;
1212
facetVariable?: string;

src/plots/FacetedPlot.tsx

+88-23
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,51 @@ import React, {
66
forwardRef,
77
useImperativeHandle,
88
useRef,
9+
useState,
910
} from 'react';
1011

1112
import { memoize } from 'lodash';
1213

1314
import { FacetedData, FacetedPlotRef, PlotRef } from '../types/plots';
1415
import { PlotProps } from './PlotlyPlot';
1516

17+
import { FullScreenModal } from '@veupathdb/core-components';
18+
1619
type ComponentWithPlotRef<P> = ComponentType<
1720
PropsWithoutRef<P> & RefAttributes<PlotRef>
1821
>;
1922

2023
export interface FacetedPlotProps<D, P extends PlotProps<D>> {
2124
data?: FacetedData<D>;
2225
component: ComponentWithPlotRef<P>;
23-
props: P;
26+
componentProps: P;
27+
/** Provide modalComponentProps to activate click-to-expand
28+
* These are the props the expanded plot inside the modal will receive
29+
*/
30+
modalComponentProps?: P;
2431
// custom legend prop
2532
checkedLegendItems?: string[];
2633
}
2734

35+
export interface FacetedPlotPropsWithRef<D, P extends PlotProps<D>>
36+
extends FacetedPlotProps<D, P> {
37+
facetedPlotRef?: Ref<FacetedPlotRef>;
38+
}
39+
2840
function renderFacetedPlot<D, P extends PlotProps<D>>(
2941
props: FacetedPlotProps<D, P>,
3042
ref: Ref<FacetedPlotRef>
3143
) {
3244
const {
3345
data,
3446
component: Component,
35-
props: componentProps,
36-
checkedLegendItems: checkedLegendItems,
47+
componentProps,
48+
modalComponentProps,
49+
checkedLegendItems,
3750
} = props;
3851
const plotRefs = useRef<FacetedPlotRef>([]);
52+
const [modalIsOpen, setModalIsOpen] = useState(false);
53+
const [modalPlot, setModalPlot] = useState<React.ReactNode | null>(null);
3954

4055
useImperativeHandle<FacetedPlotRef, FacetedPlotRef>(
4156
ref,
@@ -60,27 +75,77 @@ function renderFacetedPlot<D, P extends PlotProps<D>>(
6075
overflow: 'auto',
6176
}}
6277
>
63-
{data?.facets.map(({ data, label }, index) => (
64-
<Component
65-
{...componentProps}
66-
ref={(plotInstance) => {
67-
if (plotInstance == null) {
68-
delete plotRefs.current[index];
69-
} else {
70-
plotRefs.current[index] = plotInstance;
71-
}
72-
}}
73-
key={index}
74-
data={data}
75-
title={label}
76-
displayLegend={false}
77-
interactive={false}
78+
{data?.facets.map(({ data, label }, index) => {
79+
const sharedProps = {
80+
data: data,
7881
// pass checkedLegendItems to PlotlyPlot
79-
checkedLegendItems={checkedLegendItems}
80-
showNoDataOverlay={data == null}
81-
/>
82-
))}
82+
checkedLegendItems: checkedLegendItems,
83+
showNoDataOverlay: data == null,
84+
};
85+
86+
const divModalProps = modalComponentProps && {
87+
onClick: () => {
88+
setModalPlot(
89+
<Component
90+
{...sharedProps}
91+
displayLegend={true}
92+
interactive={true}
93+
{...modalComponentProps}
94+
title={label}
95+
/>
96+
);
97+
setModalIsOpen(true);
98+
},
99+
title: 'Click to expand',
100+
};
101+
102+
return (
103+
<div
104+
{...divModalProps}
105+
key={index}
106+
style={{
107+
marginRight: 15,
108+
cursor: modalComponentProps && 'pointer',
109+
}}
110+
>
111+
<Component
112+
{...sharedProps}
113+
ref={(plotInstance) => {
114+
if (plotInstance == null) {
115+
delete plotRefs.current[index];
116+
} else {
117+
plotRefs.current[index] = plotInstance;
118+
}
119+
}}
120+
displayLegend={false}
121+
interactive={false}
122+
{...componentProps}
123+
title={label}
124+
/>
125+
</div>
126+
);
127+
})}
83128
</div>
129+
{modalComponentProps && (
130+
<FullScreenModal visible={modalIsOpen}>
131+
<button
132+
onClick={() => setModalIsOpen(false)}
133+
style={{
134+
position: 'absolute',
135+
top: 30,
136+
right: 30,
137+
backgroundColor: 'white',
138+
cursor: 'pointer',
139+
border: 'none',
140+
zIndex: 2000,
141+
}}
142+
title="Close expanded plot"
143+
>
144+
<i className="fas fa-times fa-lg"></i>
145+
</button>
146+
{modalPlot}
147+
</FullScreenModal>
148+
)}
84149
</>
85150
);
86151
}
@@ -101,7 +166,7 @@ const makeFacetedPlotComponent = memoize(function <D, P extends PlotProps<D>>(
101166
});
102167

103168
export default function FacetedPlot<D, P extends PlotProps<D>>(
104-
props: FacetedPlotProps<D, P> & { facetedPlotRef?: Ref<FacetedPlotRef> }
169+
props: FacetedPlotPropsWithRef<D, P>
105170
) {
106171
const FacetedPlotComponent = makeFacetedPlotComponent<D, P>(props.component);
107172

src/plots/MosaicPlot.tsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
makePlotlyPlotComponent,
66
PlotProps,
77
} from './PlotlyPlot';
8-
import { MosaicData } from '../types/plots';
8+
import { MosaicPlotData } from '../types/plots';
99
import { PlotParams } from 'react-plotly.js';
1010
import _ from 'lodash';
1111
// util functions for handling long tick labels with ellipsis
@@ -14,7 +14,7 @@ import { makeStyles } from '@material-ui/core/styles';
1414
import { PlotSpacingDefault } from '../types/plots/addOns';
1515
import { Layout } from 'plotly.js';
1616

17-
export interface MosaicPlotProps extends PlotProps<MosaicData> {
17+
export interface MosaicPlotProps extends PlotProps<MosaicPlotData> {
1818
/** label for independent axis */
1919
independentAxisLabel?: string;
2020
/** label for dependent axis */
@@ -24,7 +24,7 @@ export interface MosaicPlotProps extends PlotProps<MosaicData> {
2424
showColumnLabels?: boolean;
2525
}
2626

27-
export const EmptyMosaicData: MosaicData = {
27+
export const EmptyMosaicData: MosaicPlotData = {
2828
values: [[]],
2929
independentLabels: [],
3030
dependentLabels: [],
@@ -127,11 +127,12 @@ const MosaicPlot = makePlotlyPlotComponent(
127127
maxIndependentTickLabelLength
128128
);
129129
// Subtraction at end is due to x-axis automargin shrinking the plot
130-
const plotHeight =
130+
let plotHeight =
131131
containerHeight -
132132
marginTop -
133133
marginBottom -
134-
5 * longestIndependentTickLabelLength;
134+
8 * longestIndependentTickLabelLength;
135+
if (!independentAxisLabel) plotHeight -= 20;
135136
// Calculate the legend trace group gap accordingly
136137
legendTraceGroupGap =
137138
((plotHeight - defaultLegendItemHeight * data.dependentLabels.length) *

src/plots/PlotlyPlot.tsx

+17-4
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ export interface PlotProps<T> extends ColorPaletteAddon {
5959
storedIndependentAxisTickLabel?: string[];
6060
/** list of checked legend items via checkbox input */
6161
checkedLegendItems?: string[];
62+
/** A function to call each time after plotly renders the plot */
63+
onPlotlyRender?: PlotParams['onUpdate'];
6264
}
6365

6466
const Plot = lazy(() => import('react-plotly.js'));
@@ -99,6 +101,7 @@ function PlotlyPlot<T>(
99101
storedIndependentAxisTickLabel,
100102
checkedLegendItems,
101103
colorPalette = ColorPaletteDefault,
104+
onPlotlyRender,
102105
...plotlyProps
103106
} = props;
104107

@@ -202,8 +205,9 @@ function PlotlyPlot<T>(
202205
);
203206

204207
// ellipsis with tooltip for legend, legend title, and independent axis tick labels
205-
const onUpdate = useCallback(
206-
(_, graphDiv: Readonly<HTMLElement>) => {
208+
const onRender = useCallback(
209+
(figure, graphDiv: Readonly<HTMLElement>) => {
210+
onPlotlyRender && onPlotlyRender(figure, graphDiv);
207211
// legend tooltip
208212
// remove pre-existing title to avoid duplicates
209213
select(graphDiv)
@@ -291,6 +295,7 @@ function PlotlyPlot<T>(
291295
}
292296
},
293297
[
298+
onPlotlyRender,
294299
storedLegendList,
295300
legendTitle,
296301
maxLegendTitleTextLength,
@@ -299,6 +304,14 @@ function PlotlyPlot<T>(
299304
]
300305
);
301306

307+
const onInitialized = useCallback(
308+
(figure, graphDiv: Readonly<HTMLElement>) => {
309+
onRender(figure, graphDiv);
310+
sharedPlotCreation.run();
311+
},
312+
[onRender, sharedPlotCreation.run]
313+
);
314+
302315
const finalData = useMemo(() => {
303316
return data.map((d) => ({
304317
...d,
@@ -352,8 +365,8 @@ function PlotlyPlot<T>(
352365
style={{ width: '100%', height: '100%' }}
353366
config={finalConfig}
354367
// use onUpdate event handler for legend tooltip
355-
onUpdate={onUpdate}
356-
onInitialized={sharedPlotCreation.run}
368+
onUpdate={onRender}
369+
onInitialized={onInitialized}
357370
/>
358371
{showNoDataOverlay && (
359372
<div
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import Barplot, { BarplotProps } from '../Barplot';
2+
import FacetedPlot, { FacetedPlotPropsWithRef } from '../FacetedPlot';
3+
import { BarplotData } from '../../types/plots';
4+
5+
export const defaultContainerStyles: BarplotProps['containerStyles'] = {
6+
height: 300,
7+
width: 375,
8+
marginLeft: '0.75rem',
9+
border: '1px solid #dedede',
10+
boxShadow: '1px 1px 4px #00000066',
11+
};
12+
13+
export const defaultSpacingOptions: BarplotProps['spacingOptions'] = {
14+
marginRight: 10,
15+
marginLeft: 10,
16+
marginBottom: 10,
17+
marginTop: 50,
18+
};
19+
20+
type FacetedBarplotProps = Omit<
21+
FacetedPlotPropsWithRef<BarplotData, BarplotProps>,
22+
'component'
23+
>;
24+
25+
const FacetedBarplot = (facetedBarplotProps: FacetedBarplotProps) => {
26+
const { componentProps } = facetedBarplotProps;
27+
28+
return (
29+
<FacetedPlot
30+
component={Barplot}
31+
{...facetedBarplotProps}
32+
componentProps={{
33+
...componentProps,
34+
containerStyles:
35+
componentProps.containerStyles ?? defaultContainerStyles,
36+
spacingOptions: componentProps.spacingOptions ?? defaultSpacingOptions,
37+
}}
38+
/>
39+
);
40+
};
41+
42+
export default FacetedBarplot;
+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Boxplot, { BoxplotProps } from '../Boxplot';
2+
import FacetedPlot, { FacetedPlotPropsWithRef } from '../FacetedPlot';
3+
import { BoxplotData } from '../../types/plots';
4+
5+
export const defaultContainerStyles: BoxplotProps['containerStyles'] = {
6+
height: 300,
7+
width: 375,
8+
marginLeft: '0.75rem',
9+
marginBottom: '0.25rem',
10+
border: '1px solid #dedede',
11+
boxShadow: '1px 1px 4px #00000066',
12+
};
13+
14+
export const defaultSpacingOptions: BoxplotProps['spacingOptions'] = {
15+
marginRight: 15,
16+
marginLeft: 15,
17+
marginBottom: 10,
18+
marginTop: 50,
19+
};
20+
21+
type FacetedBoxplotProps = Omit<
22+
FacetedPlotPropsWithRef<BoxplotData, BoxplotProps>,
23+
'component'
24+
>;
25+
26+
const FacetedBoxplot = (facetedBoxplotProps: FacetedBoxplotProps) => {
27+
const { componentProps } = facetedBoxplotProps;
28+
29+
return (
30+
<FacetedPlot
31+
component={Boxplot}
32+
{...facetedBoxplotProps}
33+
componentProps={{
34+
...componentProps,
35+
containerStyles:
36+
componentProps.containerStyles ?? defaultContainerStyles,
37+
spacingOptions: componentProps.spacingOptions ?? defaultSpacingOptions,
38+
}}
39+
/>
40+
);
41+
};
42+
43+
export default FacetedBoxplot;

0 commit comments

Comments
 (0)