Skip to content
New issue

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

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

Already on GitHub? # to your account

CRDCDH-538 Data Submission QC Statistics (v2) #255

Merged
merged 8 commits into from
Jan 8, 2024
21 changes: 16 additions & 5 deletions src/components/DataSubmissions/LegendItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import { Box, Stack, Typography, styled } from '@mui/material';
type Props = {
color: string;
label: string;
disabled?: boolean;
onClick?: () => void;
};

const StyledStack = styled(Stack)({
marginRight: "20px",
const StyledStack = styled(Stack, { shouldForwardProp: (p) => p !== "disabled" })<{ disabled: boolean }>(({ disabled }) => ({
opacity: disabled ? 0.4 : 1,
textDecoration: disabled ? "line-through" : "none",
marginRight: "35px",
"&:last-child": {
marginRight: "0",
},
});
cursor: "pointer",
userSelect: "none",
}));

const StyledLabel = styled(Typography)({
color: "#383838",
Expand All @@ -36,8 +42,13 @@ const StyledColorBox = styled(Box, { shouldForwardProp: (p) => p !== "color" })<
* @param {Props} props
* @returns {React.FC<Props>}
*/
const LegendItem: FC<Props> = ({ color, label }: Props) => (
<StyledStack direction="row" alignItems="center">
const LegendItem: FC<Props> = ({ color, label, disabled, onClick }: Props) => (
<StyledStack
direction="row"
alignItems="center"
disabled={disabled}
onClick={() => onClick?.()}
>
<StyledColorBox color={color} />
<StyledLabel>{label}</StyledLabel>
</StyledStack>
Expand Down
48 changes: 25 additions & 23 deletions src/components/DataSubmissions/StatisticLegend.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,43 @@
import { FC } from 'react';
import { Box, Stack, Typography, styled } from '@mui/material';
import { Stack, styled } from '@mui/material';
import LegendItem from './LegendItem';

const StyledContainer = styled(Box)({
export type Props = {
filters: LegendFilter[];
onClick?: (filter: LegendFilter) => void;
};

export type LegendFilter = {
label: string;
color: string;
disabled?: boolean;
};

const StyledContainer = styled(Stack)({
borderRadius: "8px",
background: "#F5F8F9",
border: "1px solid #939393",
width: "600px",
paddingLeft: "18px",
paddingRight: "18px",
paddingTop: "3px",
paddingBottom: "7px",
padding: "8px 0",
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>
const StatisticLegend: FC<Props> = ({ filters, onClick }) => (
<StyledContainer direction="row" justifyContent="center" alignItems="center">
{filters.map((filter) => (
<LegendItem
key={filter.label}
color={filter.color}
label={filter.label}
disabled={filter.disabled}
onClick={() => onClick?.(filter)}
/>
))}
</StyledContainer>
);

Expand Down
1 change: 0 additions & 1 deletion src/components/DataSubmissions/ValidationControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ const StyledFileValidationSection = styled("div")({
borderRadius: 0,
minHeight: "147px",
padding: "21px 40px 0",
borderTop: "solid 1.5px #6CACDA",
background: "#F0FBFD",
gridAutoFlow: "row",
gridTemplateColumns: "2.5fr 0.5fr",
Expand Down
49 changes: 39 additions & 10 deletions src/components/DataSubmissions/ValidationStatistics.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { FC } from 'react';
import React, { FC, useState } from 'react';
import { isEqual } from 'lodash';
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';
import { buildMiniChartSeries, buildPrimaryChartSeries, compareNodeStats } from '../../utils/statisticUtils';
import StatisticLegend, { LegendFilter } from './StatisticLegend';

type Props = {
dataSubmission: Submission;
Expand Down Expand Up @@ -98,13 +99,37 @@ const StyledSecondaryTitle = styled(Typography)({
color: "#1D91AB",
});

const defaultFilters: LegendFilter[] = [
{ label: "New", color: "#4D90D3", disabled: false },
{ label: "Passed", color: "#32E69A", disabled: false },
{ label: "Error", color: "#D65219", disabled: false },
{ label: "Warning", color: "#FFD700", disabled: false },
];

/**
* The primary chart container with secondary detail charts
*
* @param {Props} props
* @returns {React.FC<Props>}
*/
const DataSubmissionStatistics: FC<Props> = ({ dataSubmission, statistics }: Props) => {
const [filters, setFilters] = useState<LegendFilter[]>(defaultFilters);
const disabledSeries: string[] = filters.filter((f) => f.disabled).map((f) => f.label);

const handleFilterChange = (filter: LegendFilter) => {
const newFilters = filters.map((f) => {
if (f.label === filter.label) { return { ...f, disabled: !f.disabled }; }
return f;
});

// If all filters are disabled, do not allow disabling the last one
if (newFilters.every((f) => f.disabled)) {
return;
}

setFilters(newFilters);
};

// If there is no data submission or no items uploaded, do not render
if (!dataSubmission || !statistics?.some((s) => s.total > 0)) {
return null;
Expand All @@ -113,25 +138,29 @@ const DataSubmissionStatistics: FC<Props> = ({ dataSubmission, statistics }: Pro
return (
<StyledChartArea direction="row">
<StyledPrimaryChart>
<StyledPrimaryTitle variant="h6">Node Totals</StyledPrimaryTitle>
<NodeTotalChart data={buildPrimaryChartSeries(statistics)} />
<StyledPrimaryTitle variant="h6">Summary</StyledPrimaryTitle>
<NodeTotalChart data={buildPrimaryChartSeries(statistics, disabledSeries)} />
</StyledPrimaryChart>
<StyledSecondaryStack direction="column" alignItems="center" flex={1}>
<StyledSecondaryTitle variant="h6">Node Count Breakdown</StyledSecondaryTitle>
<StyledSecondaryTitle variant="h6">
Individual Node Types
{" "}
{`(${statistics.length})`}
</StyledSecondaryTitle>
<ContentCarousel focusOnSelect={statistics.length > 2}>
{statistics.map((stat) => (
{statistics?.sort(compareNodeStats).map((stat) => (
<MiniPieChart
key={stat.nodeName}
label={stat.nodeName}
centerCount={stat.total}
data={buildMiniChartSeries(stat)}
data={buildMiniChartSeries(stat, disabledSeries)}
/>
))}
</ContentCarousel>
<StatisticLegend />
<StatisticLegend filters={filters} onClick={handleFilterChange} />
</StyledSecondaryStack>
</StyledChartArea>
);
};

export default DataSubmissionStatistics;
export default React.memo<Props>(DataSubmissionStatistics, (prevProps, nextProps) => isEqual(prevProps, nextProps));
6 changes: 3 additions & 3 deletions src/components/NodeChart/PieChartCenter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ type PieChartCenterProps = {
/**
* The count to display in the center of the chart
*/
count: number;
value: number;
viewBox?: {
cx: number;
cy: number;
Expand All @@ -48,7 +48,7 @@ type PieChartCenterProps = {
* @param {Props} props
* @returns {React.FC<PieChartCenterProps>}
*/
const PieChartCenter: FC<PieChartCenterProps> = ({ title, count, viewBox }: PieChartCenterProps) => {
const PieChartCenter: FC<PieChartCenterProps> = ({ title, value, viewBox }: PieChartCenterProps) => {
const { cx, cy } = viewBox;

return (
Expand All @@ -57,7 +57,7 @@ const PieChartCenter: FC<PieChartCenterProps> = ({ title, count, viewBox }: PieC
<StyledTextContainer x={cx} y={cy - 10}>
<StyledCenterTitle>{title}</StyledCenterTitle>
<StyledCenterCount x={cx} dy={20}>
{count}
{value}
</StyledCenterCount>
</StyledTextContainer>
</g>
Expand Down
74 changes: 50 additions & 24 deletions src/components/NodeChart/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FC } from 'react';
import React, { FC, useCallback, useMemo, useState } from 'react';
import { Box, Typography, styled } from "@mui/material";
import { PieChart, Pie, Label, Cell } from 'recharts';
import { isEqual } from 'lodash';
import PieChartCenter from './PieChartCenter';

export type PieSectorDataItem = {
Expand Down Expand Up @@ -53,27 +54,52 @@ const StyledChartContainer = styled(Box)({
* @param {number} centerCount Node count to display in the center of the chart
* @returns {React.FC<Props>}
*/
const NodeChart: FC<Props> = ({ label, centerCount, data }: Props) => (
<StyledChartContainer>
{label && <StyledPieChartLabel>{label}</StyledPieChartLabel>}
<PieChart width={150} height={150}>
<Pie
data={data}
dataKey="value"
cx="50%"
cy="50%"
labelLine={false}
outerRadius={75}
innerRadius={40}
>
{data.map(({ label, color }) => (<Cell key={label} fill={color} />))}
<Label
position="center"
content={(<PieChartCenter title="Total" count={centerCount} />)}
/>
</Pie>
</PieChart>
</StyledChartContainer>
);
const NodeChart: FC<Props> = ({ label, centerCount, data }: Props) => {
const [hoveredSlice, setHoveredSlice] = useState<PieSectorDataItem>(null);

export default NodeChart;
const dataset: PieSectorDataItem[] = useMemo(() => data.filter(({ value }) => value > 0), [data]);
const onMouseOver = useCallback((data) => setHoveredSlice(data), []);
const onMouseLeave = useCallback(() => setHoveredSlice(null), []);

return (
<StyledChartContainer>
{label && <StyledPieChartLabel>{label}</StyledPieChartLabel>}
<PieChart width={150} height={150}>
<Pie
data={[{ value: 100 }]}
dataKey="value"
outerRadius={75}
innerRadius={40}
fill="#f2f2f2"
isAnimationActive={false}
>
{(dataset.length === 0 && hoveredSlice === null) && <Label position="center" content={(<PieChartCenter title="Total" value={0} />)} />}
</Pie>
<Pie
data={dataset}
dataKey="value"
cx="50%"
cy="50%"
labelLine={false}
outerRadius={75}
innerRadius={40}
onMouseOver={onMouseOver}
onMouseLeave={onMouseLeave}
>
{dataset.map(({ label, color }) => (<Cell key={label} fill={color} />))}
<Label
position="center"
content={(
<PieChartCenter
title={hoveredSlice ? hoveredSlice.label : "Total"}
value={hoveredSlice ? hoveredSlice.value : centerCount}
/>
)}
/>
</Pie>
</PieChart>
</StyledChartContainer>
);
};

export default React.memo<Props>(NodeChart, (prevProps, nextProps) => isEqual(prevProps, nextProps));
38 changes: 10 additions & 28 deletions src/components/NodeTotalChart/PieChartCenter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ const StyledTextContainer = styled('text')({

const StyledCenterTitle = styled('tspan')({
fontSize: 26,
});

const StyledCenterSubtitle = styled('tspan')({
fontSize: 26,
fontWeight: 600,
});

Expand All @@ -28,38 +24,24 @@ type Props = {
cy: number;
};
title: string | number;
subtitle: string | number;
value: string | number;
};

/**
* Builds the center of the pie chart with the title, subtitle, and value.
*
* Will not render if the subtitle or value are not provided.
*
* @param {Props} props
* @returns {React.FC<Props>}
*/
const PieChartCenter: FC<Props> = ({ viewBox, title, subtitle, value }) => {
const { cx, cy } = viewBox;

if (!subtitle || !value) {
return null;
}

return (
<g>
<StyledTextContainer x={cx} y={cy - 40}>
<StyledCenterTitle>{title}</StyledCenterTitle>
<StyledCenterSubtitle x={cx} dy={35}>
{subtitle}
</StyledCenterSubtitle>
<StyledCenterValue x={cx} dy={35}>
{value}
</StyledCenterValue>
</StyledTextContainer>
</g>
);
};
const PieChartCenter: FC<Props> = ({ viewBox: { cx, cy }, title, value }) => (
<g>
<StyledTextContainer x={cx} y={cy - 25}>
<StyledCenterTitle>{title}</StyledCenterTitle>
<StyledCenterValue x={cx} dy={35}>
{value}
</StyledCenterValue>
</StyledTextContainer>
</g>
);

export default PieChartCenter;
Loading