Skip to content

Commit

Permalink
feat: improved health chart tooltip (#6359)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tymek committed Feb 28, 2024
1 parent b82a650 commit 96c86b2
Show file tree
Hide file tree
Showing 15 changed files with 489 additions and 354 deletions.
Expand Up @@ -178,7 +178,7 @@ export const ExecutiveDashboard: VFC = () => {
value={80}
healthy={4}
stale={1}
potenciallyStale={0}
potentiallyStale={0}
/>
</Widget>
<Widget title='Health per project' order={7} span={chartSpan}>
Expand Down
Expand Up @@ -6,14 +6,14 @@ interface IHealthStatsProps {
value: number;
healthy: number;
stale: number;
potenciallyStale: number;
potentiallyStale: number;
}

export const HealthStats: VFC<IHealthStatsProps> = ({
value,
healthy,
stale,
potenciallyStale,
potentiallyStale,
}) => {
const { themeMode } = useThemeMode();
const isDark = themeMode === 'dark';
Expand Down Expand Up @@ -116,12 +116,12 @@ export const HealthStats: VFC<IHealthStatsProps> = ({
<text
x={144}
y={216}
fill={theme.palette.charts.health.potenciallyStale}
fill={theme.palette.charts.health.potentiallyStale}
fontWeight={700}
fontSize={20}
textAnchor='middle'
>
{potenciallyStale || 0}
{potentiallyStale || 0}
</text>
<text
x={144}
Expand Down Expand Up @@ -302,7 +302,7 @@ export const HealthStats: VFC<IHealthStatsProps> = ({
<stop
offset='1'
stopColor={
theme.palette.charts.health.gradientPotenciallyStale
theme.palette.charts.health.gradientPotentiallyStale
}
/>
</linearGradient>
Expand Down
@@ -0,0 +1,41 @@
import { VFC } from 'react';
import { Box, styled } from '@mui/material';

type DistributionLineTypes = 'default' | 'success' | 'warning' | 'error';

const StyledDistributionLine = styled(Box)<{
type: DistributionLineTypes;
size?: 'large' | 'small';
}>(({ theme, type, size = 'large' }) => {
const color: Record<DistributionLineTypes, string | undefined> = {
default: theme.palette.secondary.border,
success: theme.palette.success.border,
warning: theme.palette.warning.border,
error: theme.palette.error.border,
};

return {
borderRadius: theme.shape.borderRadius,
height: size === 'large' ? theme.spacing(2) : theme.spacing(1),
backgroundColor: color[type],
marginBottom: theme.spacing(0.5),
};
});

export const HorizontalDistributionChart: VFC<{
sections: Array<{ type: DistributionLineTypes; value: number }>;
size?: 'large' | 'small';
}> = ({ sections, size }) => (
<Box sx={(theme) => ({ display: 'flex', gap: theme.spacing(0.5) })}>
{sections.map((section, index) =>
section.value ? (
<StyledDistributionLine
type={section.type}
sx={{ width: `${section.value}%` }}
key={`${section.type}-${index}`}
size={size}
/>
) : null,
)}
</Box>
);
Expand Up @@ -7,7 +7,7 @@ export type TooltipState = {
caretX: number;
caretY: number;
title: string;
align: 'left' | 'right';
align: 'left' | 'right' | 'center';
body: {
title: string;
color: string;
Expand Down Expand Up @@ -40,22 +40,47 @@ const StyledLabelIcon = styled('span')(({ theme }) => ({
marginRight: theme.spacing(1),
}));

const offset = 16;

const getAlign = (align?: 'left' | 'right' | 'center') => {
if (align === 'left') {
return 'flex-start';
}

if (align === 'right') {
return 'flex-end';
}

return 'center';
};

const getLeftOffset = (caretX = 0, align?: 'left' | 'right' | 'center') => {
if (align === 'left') {
return caretX + offset;
}

if (align === 'right') {
return caretX - offset;
}

return caretX;
};

export const ChartTooltipContainer: FC<IChartTooltipProps> = ({
tooltip,
children,
}) => (
<Box
sx={(theme) => ({
top: tooltip?.caretY,
left: tooltip?.align === 'left' ? tooltip?.caretX + 20 : 0,
right:
tooltip?.align === 'right' ? tooltip?.caretX + 20 : undefined,
top: (tooltip?.caretY || 0) + offset,
left: getLeftOffset(tooltip?.caretX, tooltip?.align),
width: '1px',
position: 'absolute',
display: tooltip ? 'flex' : 'none',
pointerEvents: 'none',
zIndex: theme.zIndex.tooltip,
flexDirection: 'column',
alignItems: tooltip?.align === 'left' ? 'flex-start' : 'flex-end',
alignItems: getAlign(tooltip?.align),
})}
>
{children}
Expand Down
Expand Up @@ -10,161 +10,20 @@ import {
Chart,
Filler,
type ChartData,
TooltipModel,
ChartOptions,
type ChartOptions,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
import 'chartjs-adapter-date-fns';
import { Theme, useTheme } from '@mui/material';
import {
useLocationSettings,
type ILocationSettings,
} from 'hooks/useLocationSettings';
import { useTheme } from '@mui/material';
import { useLocationSettings } from 'hooks/useLocationSettings';
import {
ChartTooltip,
ChartTooltipContainer,
TooltipState,
} from './ChartTooltip/ChartTooltip';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { styled } from '@mui/material';

const createTooltip =
(setTooltip: React.Dispatch<React.SetStateAction<TooltipState | null>>) =>
(context: {
chart: Chart;
tooltip: TooltipModel<any>;
}) => {
const tooltip = context.tooltip;
if (tooltip.opacity === 0) {
setTooltip(null);
return;
}

setTooltip({
caretX:
tooltip?.xAlign === 'right'
? context.chart.width - tooltip?.caretX
: tooltip?.caretX,
caretY: tooltip?.caretY,
title: tooltip?.title?.join(' ') || '',
align: tooltip?.xAlign === 'right' ? 'right' : 'left',
body:
tooltip?.body?.map((item: any, index: number) => ({
title: item?.lines?.join(' '),
color: tooltip?.labelColors?.[index]?.borderColor as string,
value: '',
})) || [],
dataPoints: tooltip?.dataPoints || [],
});
};

const createOptions = (
theme: Theme,
locationSettings: ILocationSettings,
setTooltip: React.Dispatch<React.SetStateAction<TooltipState | null>>,
isPlaceholder?: boolean,
localTooltip?: boolean,
) =>
({
responsive: true,
...(isPlaceholder
? {
animation: {
duration: 0,
},
}
: {}),
plugins: {
legend: {
display: !isPlaceholder,
position: 'bottom',
labels: {
boxWidth: 12,
padding: 30,
generateLabels: (chart: Chart) => {
const datasets = chart.data.datasets;
const {
labels: {
usePointStyle,
pointStyle,
textAlign,
color,
},
} = chart?.legend?.options || {
labels: {},
};
return (chart as any)
._getSortedDatasetMetas()
.map((meta: any) => {
const style = meta.controller.getStyle(
usePointStyle ? 0 : undefined,
);
return {
text: datasets[meta.index].label,
fillStyle: style.backgroundColor,
fontColor: color,
hidden: !meta.visible,
lineWidth: 0,
borderRadius: 6,
strokeStyle: style.borderColor,
pointStyle: pointStyle || style.pointStyle,
textAlign: textAlign || style.textAlign,
datasetIndex: meta.index,
};
});
},
},
},
tooltip: {
enabled: false,
external: createTooltip(setTooltip),
},
},
locale: locationSettings.locale,
interaction: {
intersect: localTooltip || false,
axis: 'x',
},
elements: {
point: {
radius: 0,
hitRadius: 15,
},
},
// cubicInterpolationMode: 'monotone',
tension: 0.1,
color: theme.palette.text.secondary,
scales: {
y: {
beginAtZero: true,
type: 'linear',
grid: {
color: theme.palette.divider,
borderColor: theme.palette.divider,
},
ticks: {
color: theme.palette.text.secondary,
display: !isPlaceholder,
precision: 0,
},
},
x: {
type: 'time',
time: {
unit: 'day',
tooltipFormat: 'PPP',
},
grid: {
color: 'transparent',
borderColor: 'transparent',
},
ticks: {
color: theme.palette.text.secondary,
display: !isPlaceholder,
},
},
},
}) as const;
import { createOptions } from './createChartOptions';

const StyledContainer = styled('div')(({ theme }) => ({
position: 'relative',
Expand Down
@@ -0,0 +1,77 @@
import { Theme } from '@mui/material';
import { ILocationSettings } from 'hooks/useLocationSettings';
import { TooltipState } from './ChartTooltip/ChartTooltip';
import { createTooltip } from './createTooltip';
import { legendOptions } from './legendOptions';

export const createOptions = (
theme: Theme,
locationSettings: ILocationSettings,
setTooltip: React.Dispatch<React.SetStateAction<TooltipState | null>>,
isPlaceholder?: boolean,
localTooltip?: boolean,
) =>
({
responsive: true,
...(isPlaceholder
? {
animation: {
duration: 0,
},
}
: {}),
plugins: {
legend: {
...legendOptions,
display: !isPlaceholder,
},
tooltip: {
enabled: false,
external: createTooltip(setTooltip),
},
},
locale: locationSettings.locale,
interaction: {
intersect: localTooltip || false,
axis: 'x',
},
elements: {
point: {
radius: 0,
hitRadius: 15,
},
},
// cubicInterpolationMode: 'monotone',
tension: 0.1,
color: theme.palette.text.secondary,
scales: {
y: {
beginAtZero: true,
type: 'linear',
grid: {
color: theme.palette.divider,
borderColor: theme.palette.divider,
},
ticks: {
color: theme.palette.text.secondary,
display: !isPlaceholder,
precision: 0,
},
},
x: {
type: 'time',
time: {
unit: 'day',
tooltipFormat: 'PPP',
},
grid: {
color: 'transparent',
borderColor: 'transparent',
},
ticks: {
color: theme.palette.text.secondary,
display: !isPlaceholder,
},
},
},
}) as const;

0 comments on commit 96c86b2

Please sign in to comment.