Skip to content

Faceting #259 #272

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
131e7d1
Add faceting story to each plot
chowington Nov 22, 2021
22f52fd
Create FacetedBarplot component
chowington Nov 24, 2021
b73ea05
Merge branch 'individual-facetedplot-components' into faceting-#259
chowington Nov 30, 2021
9f8d027
Nonfunctional FacetedBarplot using FacetedPlot
chowington Nov 30, 2021
5d00b2a
Add basic functionality to expand faceted plot to modal
chowington Dec 1, 2021
46f6fcd
Faceted plot modal cleanup
chowington Dec 1, 2021
2de15e7
Uncomment FacetedBarplot which now mysteriously works
chowington Dec 1, 2021
6b545d0
Improve faceting modal close button
chowington Dec 2, 2021
d2291de
Better modal plot style in story
chowington Dec 2, 2021
280d244
Change order of props
chowington Dec 2, 2021
74cea42
Add unique FacetedXPlot component for each plot
chowington Dec 2, 2021
672b2cc
Fix faceting plot titles bug
chowington Dec 3, 2021
3d092eb
Fix pointer cursor bug
chowington Dec 3, 2021
3575d2b
Fix faceted pie plot click bug
chowington Dec 6, 2021
48483ba
Move pie plot onPlotlyRender call to FacetedPiePlot
chowington Dec 6, 2021
282e695
Add comment
chowington Dec 7, 2021
51185dd
Change a couple of default faceted plot styles
chowington Dec 7, 2021
fc75038
Export default faceted plot styles
chowington Dec 7, 2021
ac3926d
Make FacetedPlotPropsWithRef
chowington Dec 8, 2021
fa38b76
Add button title
chowington Dec 8, 2021
b1a7774
Increase pixels per character in mosaic plot
chowington Dec 8, 2021
eca0bbc
Reorder component props
chowington Dec 8, 2021
795102b
Update faceted plot default styles
chowington Dec 8, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
"url": "https://github.com/veupathdb/web-components.git"
},
"dependencies": {
"@emotion/react": "^11.7.0",
"@material-ui/core": "^4.11.2",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57",
"@types/d3": "^7.1.0",
"@types/date-arithmetic": "^4.1.1",
"@veupathdb/core-components": "^0.2.46",
"@visx/gradient": "^1.0.0",
"@visx/group": "^1.0.0",
"@visx/hierarchy": "^1.0.0",
Expand Down
4 changes: 2 additions & 2 deletions src/components/ContingencyTable.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { CSSProperties } from 'react';
import { MosaicData } from '../types/plots/mosaic';
import { MosaicPlotData } from '../types/plots/mosaicPlot';
import _ from 'lodash';
import { FacetedData } from '../types/plots';
import { isFaceted } from '../types/guards';
import Spinner from '../components/Spinner';

interface ContingencyTableProps {
data?: MosaicData | FacetedData<MosaicData>;
data?: MosaicPlotData | FacetedData<MosaicPlotData>;
independentVariable: string;
dependentVariable: string;
facetVariable?: string;
Expand Down
111 changes: 88 additions & 23 deletions src/plots/FacetedPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,51 @@ import React, {
forwardRef,
useImperativeHandle,
useRef,
useState,
} from 'react';

import { memoize } from 'lodash';

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

import { FullScreenModal } from '@veupathdb/core-components';

type ComponentWithPlotRef<P> = ComponentType<
PropsWithoutRef<P> & RefAttributes<PlotRef>
>;

export interface FacetedPlotProps<D, P extends PlotProps<D>> {
data?: FacetedData<D>;
component: ComponentWithPlotRef<P>;
props: P;
componentProps: P;
/** Provide modalComponentProps to activate click-to-expand
* These are the props the expanded plot inside the modal will receive
*/
modalComponentProps?: P;
// custom legend prop
checkedLegendItems?: string[];
}

export interface FacetedPlotPropsWithRef<D, P extends PlotProps<D>>
extends FacetedPlotProps<D, P> {
facetedPlotRef?: Ref<FacetedPlotRef>;
}
Comment on lines +35 to +38
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


function renderFacetedPlot<D, P extends PlotProps<D>>(
props: FacetedPlotProps<D, P>,
ref: Ref<FacetedPlotRef>
) {
const {
data,
component: Component,
props: componentProps,
checkedLegendItems: checkedLegendItems,
componentProps,
modalComponentProps,
checkedLegendItems,
} = props;
const plotRefs = useRef<FacetedPlotRef>([]);
const [modalIsOpen, setModalIsOpen] = useState(false);
const [modalPlot, setModalPlot] = useState<React.ReactNode | null>(null);

useImperativeHandle<FacetedPlotRef, FacetedPlotRef>(
ref,
Expand All @@ -60,27 +75,77 @@ function renderFacetedPlot<D, P extends PlotProps<D>>(
overflow: 'auto',
}}
>
{data?.facets.map(({ data, label }, index) => (
<Component
{...componentProps}
ref={(plotInstance) => {
if (plotInstance == null) {
delete plotRefs.current[index];
} else {
plotRefs.current[index] = plotInstance;
}
}}
key={index}
data={data}
title={label}
displayLegend={false}
interactive={false}
{data?.facets.map(({ data, label }, index) => {
const sharedProps = {
data: data,
// pass checkedLegendItems to PlotlyPlot
checkedLegendItems={checkedLegendItems}
showNoDataOverlay={data == null}
/>
))}
checkedLegendItems: checkedLegendItems,
showNoDataOverlay: data == null,
};

const divModalProps = modalComponentProps && {
onClick: () => {
setModalPlot(
<Component
{...sharedProps}
displayLegend={true}
interactive={true}
{...modalComponentProps}
title={label}
/>
);
setModalIsOpen(true);
},
title: 'Click to expand',
};

return (
<div
{...divModalProps}
key={index}
style={{
marginRight: 15,
cursor: modalComponentProps && 'pointer',
}}
>
<Component
{...sharedProps}
ref={(plotInstance) => {
if (plotInstance == null) {
delete plotRefs.current[index];
} else {
plotRefs.current[index] = plotInstance;
}
}}
displayLegend={false}
interactive={false}
{...componentProps}
title={label}
/>
</div>
);
})}
</div>
{modalComponentProps && (
<FullScreenModal visible={modalIsOpen}>
<button
onClick={() => setModalIsOpen(false)}
style={{
position: 'absolute',
top: 30,
right: 30,
backgroundColor: 'white',
cursor: 'pointer',
border: 'none',
zIndex: 2000,
}}
title="Close expanded plot"
>
<i className="fas fa-times fa-lg"></i>
</button>
{modalPlot}
</FullScreenModal>
)}
</>
);
}
Expand All @@ -101,7 +166,7 @@ const makeFacetedPlotComponent = memoize(function <D, P extends PlotProps<D>>(
});

export default function FacetedPlot<D, P extends PlotProps<D>>(
props: FacetedPlotProps<D, P> & { facetedPlotRef?: Ref<FacetedPlotRef> }
props: FacetedPlotPropsWithRef<D, P>
) {
const FacetedPlotComponent = makeFacetedPlotComponent<D, P>(props.component);

Expand Down
11 changes: 6 additions & 5 deletions src/plots/MosaicPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
makePlotlyPlotComponent,
PlotProps,
} from './PlotlyPlot';
import { MosaicData } from '../types/plots';
import { MosaicPlotData } from '../types/plots';
import { PlotParams } from 'react-plotly.js';
import _ from 'lodash';
// util functions for handling long tick labels with ellipsis
Expand All @@ -14,7 +14,7 @@ import { makeStyles } from '@material-ui/core/styles';
import { PlotSpacingDefault } from '../types/plots/addOns';
import { Layout } from 'plotly.js';

export interface MosaicPlotProps extends PlotProps<MosaicData> {
export interface MosaicPlotProps extends PlotProps<MosaicPlotData> {
/** label for independent axis */
independentAxisLabel?: string;
/** label for dependent axis */
Expand All @@ -24,7 +24,7 @@ export interface MosaicPlotProps extends PlotProps<MosaicData> {
showColumnLabels?: boolean;
}

export const EmptyMosaicData: MosaicData = {
export const EmptyMosaicData: MosaicPlotData = {
values: [[]],
independentLabels: [],
dependentLabels: [],
Expand Down Expand Up @@ -127,11 +127,12 @@ const MosaicPlot = makePlotlyPlotComponent(
maxIndependentTickLabelLength
);
// Subtraction at end is due to x-axis automargin shrinking the plot
const plotHeight =
let plotHeight =
containerHeight -
marginTop -
marginBottom -
5 * longestIndependentTickLabelLength;
8 * longestIndependentTickLabelLength;
if (!independentAxisLabel) plotHeight -= 20;
// Calculate the legend trace group gap accordingly
legendTraceGroupGap =
((plotHeight - defaultLegendItemHeight * data.dependentLabels.length) *
Expand Down
21 changes: 17 additions & 4 deletions src/plots/PlotlyPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export interface PlotProps<T> extends ColorPaletteAddon {
storedIndependentAxisTickLabel?: string[];
/** list of checked legend items via checkbox input */
checkedLegendItems?: string[];
/** A function to call each time after plotly renders the plot */
onPlotlyRender?: PlotParams['onUpdate'];
}

const Plot = lazy(() => import('react-plotly.js'));
Expand Down Expand Up @@ -99,6 +101,7 @@ function PlotlyPlot<T>(
storedIndependentAxisTickLabel,
checkedLegendItems,
colorPalette = ColorPaletteDefault,
onPlotlyRender,
...plotlyProps
} = props;

Expand Down Expand Up @@ -202,8 +205,9 @@ function PlotlyPlot<T>(
);

// ellipsis with tooltip for legend, legend title, and independent axis tick labels
const onUpdate = useCallback(
(_, graphDiv: Readonly<HTMLElement>) => {
const onRender = useCallback(
(figure, graphDiv: Readonly<HTMLElement>) => {
onPlotlyRender && onPlotlyRender(figure, graphDiv);
// legend tooltip
// remove pre-existing title to avoid duplicates
select(graphDiv)
Expand Down Expand Up @@ -291,6 +295,7 @@ function PlotlyPlot<T>(
}
},
[
onPlotlyRender,
storedLegendList,
legendTitle,
maxLegendTitleTextLength,
Expand All @@ -299,6 +304,14 @@ function PlotlyPlot<T>(
]
);

const onInitialized = useCallback(
(figure, graphDiv: Readonly<HTMLElement>) => {
onRender(figure, graphDiv);
sharedPlotCreation.run();
},
[onRender, sharedPlotCreation.run]
);

const finalData = useMemo(() => {
return data.map((d) => ({
...d,
Expand Down Expand Up @@ -352,8 +365,8 @@ function PlotlyPlot<T>(
style={{ width: '100%', height: '100%' }}
config={finalConfig}
// use onUpdate event handler for legend tooltip
onUpdate={onUpdate}
onInitialized={sharedPlotCreation.run}
onUpdate={onRender}
onInitialized={onInitialized}
/>
{showNoDataOverlay && (
<div
Expand Down
42 changes: 42 additions & 0 deletions src/plots/facetedPlots/FacetedBarplot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Barplot, { BarplotProps } from '../Barplot';
import FacetedPlot, { FacetedPlotPropsWithRef } from '../FacetedPlot';
import { BarplotData } from '../../types/plots';

export const defaultContainerStyles: BarplotProps['containerStyles'] = {
height: 300,
width: 375,
marginLeft: '0.75rem',
border: '1px solid #dedede',
boxShadow: '1px 1px 4px #00000066',
};

export const defaultSpacingOptions: BarplotProps['spacingOptions'] = {
marginRight: 10,
marginLeft: 10,
marginBottom: 10,
marginTop: 50,
};

type FacetedBarplotProps = Omit<
FacetedPlotPropsWithRef<BarplotData, BarplotProps>,
'component'
>;

const FacetedBarplot = (facetedBarplotProps: FacetedBarplotProps) => {
const { componentProps } = facetedBarplotProps;

return (
<FacetedPlot
component={Barplot}
{...facetedBarplotProps}
componentProps={{
...componentProps,
containerStyles:
componentProps.containerStyles ?? defaultContainerStyles,
spacingOptions: componentProps.spacingOptions ?? defaultSpacingOptions,
}}
/>
);
};

export default FacetedBarplot;
43 changes: 43 additions & 0 deletions src/plots/facetedPlots/FacetedBoxplot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import Boxplot, { BoxplotProps } from '../Boxplot';
import FacetedPlot, { FacetedPlotPropsWithRef } from '../FacetedPlot';
import { BoxplotData } from '../../types/plots';

export const defaultContainerStyles: BoxplotProps['containerStyles'] = {
height: 300,
width: 375,
marginLeft: '0.75rem',
marginBottom: '0.25rem',
border: '1px solid #dedede',
boxShadow: '1px 1px 4px #00000066',
};

export const defaultSpacingOptions: BoxplotProps['spacingOptions'] = {
marginRight: 15,
marginLeft: 15,
marginBottom: 10,
marginTop: 50,
};

type FacetedBoxplotProps = Omit<
FacetedPlotPropsWithRef<BoxplotData, BoxplotProps>,
'component'
>;

const FacetedBoxplot = (facetedBoxplotProps: FacetedBoxplotProps) => {
const { componentProps } = facetedBoxplotProps;

return (
<FacetedPlot
component={Boxplot}
{...facetedBoxplotProps}
componentProps={{
...componentProps,
containerStyles:
componentProps.containerStyles ?? defaultContainerStyles,
spacingOptions: componentProps.spacingOptions ?? defaultSpacingOptions,
}}
/>
);
};

export default FacetedBoxplot;
Loading