diff --git a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx index c824c7705e..8d88dcb324 100644 --- a/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx +++ b/frontend/src/component/executiveDashboard/ExecutiveDashboard.tsx @@ -9,7 +9,8 @@ import { FlagStats } from './FlagStats/FlagStats'; const StyledGrid = styled(Box)(({ theme }) => ({ display: 'grid', - gridTemplateColumns: `repeat(auto-fill, minmax(600px, 1fr))`, + gridTemplateColumns: `300px 1fr`, + // TODO: responsive grid size gridAutoRows: '1fr', gap: theme.spacing(2), })); diff --git a/frontend/src/component/executiveDashboard/UsersChart/ChartTooltip/ChartTooltip.tsx b/frontend/src/component/executiveDashboard/UsersChart/ChartTooltip/ChartTooltip.tsx new file mode 100644 index 0000000000..3a3a889029 --- /dev/null +++ b/frontend/src/component/executiveDashboard/UsersChart/ChartTooltip/ChartTooltip.tsx @@ -0,0 +1,89 @@ +import { Paper, styled, Typography } from '@mui/material'; +import { VFC } from 'react'; + +export type TooltipState = { + caretX: number; + caretY: number; + title: string; + align: 'left' | 'right'; + body: { + title: string; + color: string; + value: string; + }[]; +}; + +interface IChartTooltipProps { + tooltip: TooltipState | null; +} + +const StyledList = styled('ul')(({ theme }) => ({ + listStyle: 'none', + margin: 0, + padding: 0, +})); + +const StyledItem = styled('li')(({ theme }) => ({ + marginBottom: theme.spacing(0.5), + display: 'flex', + alignItems: 'center', +})); + +const StyledLabelIcon = styled('span')(({ theme }) => ({ + display: 'inline-block', + width: 8, + height: 8, + borderRadius: '50%', + marginRight: theme.spacing(1), +})); + +export const ChartTooltip: VFC = ({ tooltip }) => ( + ({ + top: tooltip?.caretY, + left: + tooltip?.align === 'left' + ? tooltip?.caretX + 40 + : (tooltip?.caretX || 0) - 220, + position: 'absolute', + display: tooltip ? 'block' : 'none', + width: 220, + padding: theme.spacing(1.5, 2), + pointerEvents: 'none', + })} + > + { + ({ + marginBottom: theme.spacing(1), + color: theme.palette.text.secondary, + })} + > + {tooltip?.title} + + } + + {tooltip?.body.map((item) => ( + + + {' '} + + + {item.title} + + + ))} + + +); diff --git a/frontend/src/component/executiveDashboard/UsersChart/UsersChartComponent.tsx b/frontend/src/component/executiveDashboard/UsersChart/UsersChartComponent.tsx index b2493621ad..f5a7ae1e27 100644 --- a/frontend/src/component/executiveDashboard/UsersChart/UsersChartComponent.tsx +++ b/frontend/src/component/executiveDashboard/UsersChart/UsersChartComponent.tsx @@ -1,4 +1,4 @@ -import { useMemo, type VFC } from 'react'; +import { useMemo, useState, type VFC } from 'react'; import { Chart as ChartJS, CategoryScale, @@ -9,34 +9,88 @@ import { Tooltip, Legend, TimeScale, + Chart, } from 'chart.js'; import { Line } from 'react-chartjs-2'; import 'chartjs-adapter-date-fns'; -import { Paper, Theme, useTheme } from '@mui/material'; +import { Paper, Theme, Typography, useTheme } from '@mui/material'; import { useLocationSettings, type ILocationSettings, } from 'hooks/useLocationSettings'; -import { formatDateYMD } from 'utils/formatDate'; import { ExecutiveSummarySchema } from 'openapi'; +import { ChartTooltip, TooltipState } from './ChartTooltip/ChartTooltip'; -const createOptions = (theme: Theme, locationSettings: ILocationSettings) => +const createOptions = ( + theme: Theme, + locationSettings: ILocationSettings, + setTooltip: React.Dispatch>, +) => ({ responsive: true, plugins: { legend: { position: 'bottom', + labels: { + boxWidth: 12, + padding: 30, + // usePointStyle: true, + 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: { - callbacks: { - title: (tooltipItems: any) => { - const item = tooltipItems?.[0]; - const date = - item?.chart?.data?.labels?.[item.dataIndex]; - return date - ? formatDateYMD(date, locationSettings.locale) - : ''; - }, + enabled: false, + external: (context: any) => { + const tooltipModel = context.tooltip; + if (tooltipModel.opacity === 0) { + setTooltip(null); + return; + } + + const tooltip = context.tooltip; + setTooltip({ + caretX: tooltip?.caretX, + caretY: tooltip?.caretY, + title: tooltip?.title?.join(' ') || '', + align: tooltip?.xAlign || 'left', + body: + tooltip?.body?.map((item: any, index: number) => ({ + title: item?.lines?.join(' '), + color: tooltip?.labelColors?.[index] + ?.borderColor, + })) || [], + }); }, }, }, @@ -45,9 +99,16 @@ const createOptions = (theme: Theme, locationSettings: ILocationSettings) => intersect: false, axis: 'x', }, + elements: { + point: { + radius: 0, + }, + }, + // cubicInterpolationMode: 'monotone', color: theme.palette.text.secondary, scales: { y: { + beginAtZero: true, type: 'linear', grid: { color: theme.palette.divider, @@ -84,23 +145,21 @@ const createData = ( { label: 'Total users', data: userTrends.map((item) => item.total), - borderColor: theme.palette.primary.main, - backgroundColor: theme.palette.primary.main, - fill: true, - }, - { - label: 'Inactive users', - data: userTrends.map((item) => item.inactive), - borderColor: theme.palette.error.main, - backgroundColor: theme.palette.error.main, + borderColor: theme.palette.primary.light, + backgroundColor: theme.palette.primary.light, fill: true, }, { label: 'Active users', data: userTrends.map((item) => item.active), - borderColor: theme.palette.success.main, - backgroundColor: theme.palette.success.main, - fill: true, + borderColor: theme.palette.success.border, + backgroundColor: theme.palette.success.border, + }, + { + label: 'Inactive users', + data: userTrends.map((item) => item.inactive), + borderColor: theme.palette.warning.border, + backgroundColor: theme.palette.warning.border, }, ], }); @@ -115,11 +174,29 @@ const UsersChartComponent: VFC = ({ [theme, userTrends], ); - const options = createOptions(theme, locationSettings); + const [tooltip, setTooltip] = useState(null); + const options = useMemo( + () => createOptions(theme, locationSettings, setTooltip), + [theme, locationSettings], + ); return ( - ({ padding: theme.spacing(4) })}> + ({ + padding: theme.spacing(3), + position: 'relative', + })} + > + ({ marginBottom: theme.spacing(3) })} + > + Users + + + ); };