Skip to content

Commit

Permalink
Merge pull request #228 from CBIIT/amattu2/CRDCDH-538
Browse files Browse the repository at this point in the history
CRDCDH-538 Data Submission Statistics
  • Loading branch information
Alejandro-Vega authored Dec 11, 2023
2 parents f444d74 + 51e7c59 commit 1bb62a8
Show file tree
Hide file tree
Showing 17 changed files with 1,135 additions and 596 deletions.
1,012 changes: 460 additions & 552 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"@mui/icons-material": "^5.11.16",
"@mui/lab": "^5.0.0-alpha.130",
"@mui/material": "^5.13.1",
"@mui/x-charts": "^6.0.0-alpha.12",
"@mui/x-date-pickers": "^6.8.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
Expand All @@ -23,8 +22,10 @@
"react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0",
"react-hook-form": "^7.45.4",
"react-multi-carousel": "^2.8.4",
"react-router-dom": "^6.11.2",
"react-scripts": "5.0.1",
"recharts": "^2.10.3",
"redux": "^4.2.1",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.4.2",
Expand Down Expand Up @@ -70,6 +71,7 @@
"@types/node": "^20.4.0",
"@types/react": "^18.2.42",
"@types/react-dom": "^18.2.17",
"@types/recharts": "^1.8.29",
"@types/redux": "^3.6.0",
"@types/redux-logger": "^3.0.10",
"@types/redux-thunk": "^2.1.0",
Expand Down
78 changes: 78 additions & 0 deletions src/components/Carousel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { FC } from 'react';
import { styled } from '@mui/material';
import Carousel, { CarouselProps } from 'react-multi-carousel';
import 'react-multi-carousel/lib/styles.css';

type Props = {
children: React.ReactNode;
} & Partial<CarouselProps>;

const sizing = {
desktop: {
breakpoint: { max: 5000, min: 0 },
items: 3,
partialVisibilityGutter: 40
},
};

const StyledWrapper = styled('div')({
maxWidth: "700px",
minWidth: "464px", // NOTE: Without a min-width, the carousel collapses to 0px wide
width: "100%",
margin: "0 auto",
"& .react-multi-carousel-list": {
height: "206px",
},
"& .react-multi-carousel-track": {
margin: "0 auto", // NOTE: This centers the carousel when there are fewer than 2 items
},
"& .react-multi-carousel-item": {
width: "200px !important",
},
"& .react-multi-carousel-list::after": {
content: "''",
position: "absolute",
left: "calc(100% - 162px)",
top: "0",
bottom: "0",
width: "162px",
background: "linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 50%)",
zIndex: 9,
},
"& .react-multi-carousel-list::before": {
content: "''",
position: "absolute",
right: "calc(100% - 162px)",
top: "0",
bottom: "0",
width: "162px",
background: "linear-gradient(to left, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 50%)",
zIndex: 9,
},
});

/**
* Provides a general carousel component for displaying multiple items
* at the same time.
*
* @param Props
* @returns {JSX.Element}
*/
const ContentCarousel: FC<Props> = ({ children, ...props }: Props) => (
<StyledWrapper>
<Carousel
responsive={sizing}
swipeable
draggable
infinite
centerMode
arrows
focusOnSelect
{...props}
>
{children}
</Carousel>
</StyledWrapper>
);

export default ContentCarousel;
46 changes: 46 additions & 0 deletions src/components/DataSubmissions/LegendItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FC } from 'react';
import { Box, Stack, Typography, styled } from '@mui/material';

type Props = {
color: string;
label: string;
};

const StyledStack = styled(Stack)({
marginRight: "20px",
"&:last-child": {
marginRight: "0",
},
});

const StyledLabel = styled(Typography)({
color: "#383838",
fontSize: "13px",
fontWeight: 300,
fontFamily: "'Nunito Sans', 'Rubik', sans-serif",
});

const StyledColorBox = styled(Box, { shouldForwardProp: (p) => p !== "color" })<{ color: string }>(({ color }) => ({
width: "22px",
height: "9px",
background: color,
marginRight: "11px",
borderRadius: "1.5px",
}));

/**
* Represents an item in the legend of a chart
*
* e.g. [color box] [label]
*
* @param {Props} props
* @returns {React.FC<Props>}
*/
const LegendItem: FC<Props> = ({ color, label }: Props) => (
<StyledStack direction="row" alignItems="center">
<StyledColorBox color={color} />
<StyledLabel>{label}</StyledLabel>
</StyledStack>
);

export default LegendItem;
38 changes: 0 additions & 38 deletions src/components/DataSubmissions/PieChart.tsx

This file was deleted.

42 changes: 42 additions & 0 deletions src/components/DataSubmissions/StatisticLegend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { FC } from 'react';
import { Box, Stack, Typography, styled } from '@mui/material';
import LegendItem from './LegendItem';

const StyledContainer = styled(Box)({
borderRadius: "8px",
background: "#F5F8F9",
border: "1px solid #939393",
width: "600px",
paddingLeft: "18px",
paddingRight: "18px",
paddingTop: "3px",
paddingBottom: "7px",
marginTop: "20px",
});

const StyledLegendTitle = styled(Typography)({
color: "#005EA2",
fontFamily: "'Nunito Sans', 'Rubik', sans-serif",
fontSize: "14px",
fontWeight: 600,
lineHeight: "27px",
});

/**
* A color code legend for the Data Submissions statistics chart(s)
*
* @returns {React.FC}
*/
const StatisticLegend: FC = () => (
<StyledContainer>
<StyledLegendTitle>Color Key</StyledLegendTitle>
<Stack direction="row" justifyItems="center" alignItems="center">
<LegendItem color="#4D90D3" label="New Counts" />
<LegendItem color="#32E69A" label="Passed Counts" />
<LegendItem color="#D65219" label="Failed Counts" />
<LegendItem color="#FFD700" label="Warning Counts" />
</Stack>
</StyledContainer>
);

export default StatisticLegend;
137 changes: 137 additions & 0 deletions src/components/DataSubmissions/ValidationStatistics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { FC } from 'react';
import { Box, Stack, Typography, styled } from '@mui/material';
import ContentCarousel from '../Carousel';
import NodeTotalChart from '../NodeTotalChart';
import MiniPieChart from '../NodeChart';
import { buildMiniChartSeries, buildPrimaryChartSeries } from '../../utils/statisticUtils';
import StatisticLegend from './StatisticLegend';

type Props = {
dataSubmission: Submission;
statistics: SubmissionStatistic[];
};

const StyledChartArea = styled(Stack)({
height: "385px",
display: "flex",
justifyContent: "center",
alignItems: "center",
overflow: "visible",
background: "#FFFFFF",
width: "100%",
position: "relative",
"& > *": {
zIndex: 10
},
"&::after": {
position: "absolute",
content: '""',
height: "100%",
background: "#FFFFFF",
left: "-100%",
right: "-100%",
top: "0",
bottom: "0",
boxShadow: "0px 4px 20px 0px #00000059",
zIndex: 1,
},
});

const StyledPrimaryChart = styled(Box)({
margin: "0 90px",
display: "flex",
alignItems: "center",
justifyContent: "center",
position: "relative",
"& > *": {
zIndex: 10
},
// Add top curve to chart area
"&::before": {
position: "absolute",
content: '""',
width: "calc(100% + 20px)",
height: "calc(100% + 20px)",
background: "#FFFFFF",
zIndex: 1,
left: "-10px",
right: "-10px",
top: "-10px",
bottom: "-10px",
borderRadius: "50%",
boxShadow: "0px 4px 20px 0px #00000059",
},
// Remove overlapping box shadow from sides of chart area
"&::after": {
position: "absolute",
content: '""',
background: "#FFFFFF",
zIndex: 1,
left: "-30px",
right: "-30px",
top: "3px",
bottom: "3px",
},
});

const StyledPrimaryTitle = styled(Typography)({
position: "absolute",
top: "28px",
left: "-48px",
fontSize: "13px",
fontWeight: 600,
textTransform: "uppercase",
color: "#1D91AB",
});

const StyledSecondaryStack = styled(Stack)({
position: "relative",
height: "100%",
});

const StyledSecondaryTitle = styled(Typography)({
marginTop: "25px",
marginBottom: "23px",
fontSize: "13px",
fontWeight: 600,
textTransform: "uppercase",
color: "#1D91AB",
});

/**
* The primary chart container with secondary detail charts
*
* @param {Props} props
* @returns {React.FC<Props>}
*/
const DataSubmissionStatistics: FC<Props> = ({ dataSubmission, statistics }: Props) => {
// If there is no data submission or no items uploaded, do not render
if (!dataSubmission || !statistics?.some((s) => s.total > 0)) {
return null;
}

return (
<StyledChartArea direction="row">
<StyledPrimaryChart>
<StyledPrimaryTitle variant="h6">Node Totals</StyledPrimaryTitle>
<NodeTotalChart data={buildPrimaryChartSeries(statistics)} />
</StyledPrimaryChart>
<StyledSecondaryStack direction="column" alignItems="center" flex={1}>
<StyledSecondaryTitle variant="h6">Node Count Breakdown</StyledSecondaryTitle>
<ContentCarousel focusOnSelect={statistics.length > 2}>
{statistics.map((stat) => (
<MiniPieChart
key={stat.nodeName}
label={stat.nodeName}
centerCount={stat.total}
data={buildMiniChartSeries(stat)}
/>
))}
</ContentCarousel>
<StatisticLegend />
</StyledSecondaryStack>
</StyledChartArea>
);
};

export default DataSubmissionStatistics;
Loading

0 comments on commit 1bb62a8

Please # to comment.