Skip to content

Commit

Permalink
Data placeholders on UsersChart and FlagsChart components
Browse files Browse the repository at this point in the history
  • Loading branch information
Tymek committed Feb 8, 2024
1 parent ec5aabc commit 8436937
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ const PremiumFeatures = {
url: 'https://docs.getunleash.io/reference/actions',
label: 'Actions',
},
dashboard: {
plan: FeaturePlan.ENTERPRISE,
url: '', // FIXME: url
label: 'Dashboard',
},
};

type PremiumFeatureType = keyof typeof PremiumFeatures;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export const ExecutiveDashboard: VFC = () => {
<Widget title='Users' order={userTrendsOrder} span={chartSpan}>
<UsersChart
userTrends={executiveDashboardData.userTrends}
isLoading={loading}
/>
</Widget>
<Widget
Expand All @@ -115,6 +116,7 @@ export const ExecutiveDashboard: VFC = () => {
<Widget title='Number of flags' order={4} span={chartSpan}>
<FlagsChart
flagTrends={executiveDashboardData.flagTrends}
isLoading={loading}
/>
</Widget>
<Widget
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,45 @@ import { useMemo, type VFC } from 'react';
import 'chartjs-adapter-date-fns';
import { useTheme } from '@mui/material';
import { ExecutiveSummarySchema } from 'openapi';
import { LineChart } from '../LineChart/LineChart';
import { LineChart, NotEnoughData } from '../LineChart/LineChart';

interface IFlagsChartProps {
flagTrends: ExecutiveSummarySchema['flagTrends'];
isLoading?: boolean;
}

export const FlagsChart: VFC<IFlagsChartProps> = ({ flagTrends }) => {
export const FlagsChart: VFC<IFlagsChartProps> = ({
flagTrends,
isLoading,
}) => {
const theme = useTheme();
const notEnoughData = flagTrends.length < 2;
const placeholderData = useMemo(
() => ({
labels: Array.from({ length: 15 }, (_, i) => i + 1).map(
(i) =>
new Date(Date.now() - (15 - i) * 7 * 24 * 60 * 60 * 1000),
),
datasets: [
{
label: 'Total flags',
data: [
43, 66, 55, 65, 62, 72, 75, 73, 80, 65, 62, 61, 69, 70, 77
],
borderColor: theme.palette.primary.light,
backgroundColor: theme.palette.primary.light,
},
{
label: 'Stale',
data: [3, 5, 4, 6, 2, 7, 5, 3, 8, 3, 5, 11, 8, 4, 3],
borderColor: theme.palette.warning.border,
backgroundColor: theme.palette.warning.border,
},
],
}),
[theme],
);

const data = useMemo(
() => ({
labels: flagTrends.map((item) => item.date),
Expand All @@ -31,5 +62,10 @@ export const FlagsChart: VFC<IFlagsChartProps> = ({ flagTrends }) => {
[theme, flagTrends],
);

return <LineChart data={data} />;
return (
<LineChart
data={notEnoughData || isLoading ? placeholderData : data}
cover={notEnoughData ? <NotEnoughData /> : isLoading}
/>
);
};
42 changes: 42 additions & 0 deletions frontend/src/component/executiveDashboard/LineChart/LineChart.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,45 @@
import { lazy } from 'react';
import { type ScriptableContext } from 'chart.js';
import { Typography } from '@mui/material';

export const LineChart = lazy(() => import('./LineChartComponent'));

export const fillGradient =
(a: string, b: string) => (context: ScriptableContext<'line'>) => {
const chart = context.chart;
const { ctx, chartArea } = chart;
if (!chartArea) {
return;
}
const gradient = ctx.createLinearGradient(
0,
chartArea.bottom,
0,
chartArea.top,
);
gradient.addColorStop(0, a);
gradient.addColorStop(1, b);
return gradient;
};

export const fillGradientPrimary = fillGradient(
'rgba(129, 122, 254, 0)',
'rgba(129, 122, 254, 0.12)',
);

export const NotEnoughData = () => (
<>
<Typography
variant='body1'
component='h4'
sx={(theme) => ({
paddingBottom: theme.spacing(1),
})}
>
Not enough data
</Typography>
<Typography variant='body2'>
Two or more weeks of data are needed to show a chart.
</Typography>
</>
);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useState, type VFC } from 'react';
import { type ReactNode, useMemo, useState, type VFC } from 'react';
import {
CategoryScale,
LinearScale,
Expand All @@ -20,16 +20,27 @@ import {
type ILocationSettings,
} from 'hooks/useLocationSettings';
import { ChartTooltip, TooltipState } from './ChartTooltip/ChartTooltip';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { styled } from '@mui/material';

const createOptions = (
theme: Theme,
locationSettings: ILocationSettings,
setTooltip: React.Dispatch<React.SetStateAction<TooltipState | null>>,
isPlaceholder?: boolean,
) =>
({
responsive: true,
...(isPlaceholder
? {
animation: {
duration: 0,
},
}
: {}),
plugins: {
legend: {
display: !isPlaceholder,
position: 'bottom',
labels: {
boxWidth: 12,
Expand Down Expand Up @@ -113,7 +124,10 @@ const createOptions = (
color: theme.palette.divider,
borderColor: theme.palette.divider,
},
ticks: { color: theme.palette.text.secondary },
ticks: {
color: theme.palette.text.secondary,
display: !isPlaceholder,
},
},
x: {
type: 'time',
Expand All @@ -126,11 +140,38 @@ const createOptions = (
},
ticks: {
color: theme.palette.text.secondary,
display: !isPlaceholder,
},
},
},
}) as const;

const StyledContainer = styled('div')(({ theme }) => ({
position: 'relative',
}));

const StyledCover = styled('div')(({ theme }) => ({
position: 'absolute',
inset: 0,
display: 'flex',
zIndex: theme.zIndex.appBar,
'&::before': {
zIndex: theme.zIndex.fab,
content: '""',
position: 'absolute',
inset: 0,
backgroundColor: theme.palette.background.paper,
opacity: 0.8,
},
}));

const StyledCoverContent = styled('div')(({ theme }) => ({
zIndex: theme.zIndex.modal,
margin: 'auto',
color: theme.palette.text.secondary,
textAlign: 'center',
}));

// Vertical line on the hovered chart, filled with gradient. Highlights a section of a chart when you hover over datapoints
const customHighlightPlugin = {
id: 'customLine',
Expand Down Expand Up @@ -164,27 +205,39 @@ const customHighlightPlugin = {
const LineChartComponent: VFC<{
data: ChartData<'line', (number | ScatterDataPoint | null)[], unknown>;
aspectRatio?: number;
}> = ({ data, aspectRatio }) => {
cover?: ReactNode;
}> = ({ data, aspectRatio, cover }) => {
const theme = useTheme();
const { locationSettings } = useLocationSettings();

const [tooltip, setTooltip] = useState<null | TooltipState>(null);
const options = useMemo(
() => createOptions(theme, locationSettings, setTooltip),
() =>
createOptions(theme, locationSettings, setTooltip, Boolean(cover)),
[theme, locationSettings],
);

return (
<>
<StyledContainer>
<Line
options={options}
data={data}
plugins={[customHighlightPlugin]}
height={aspectRatio ? 100 : undefined}
width={aspectRatio ? 100 * aspectRatio : undefined}
/>
<ChartTooltip tooltip={tooltip} />
</>
<ConditionallyRender
condition={!cover}
show={<ChartTooltip tooltip={tooltip} />}
elseShow={
<StyledCover>
<StyledCoverContent>
{cover !== true ? cover : ' '}
</StyledCoverContent>
</StyledCover>
}
/>
</StyledContainer>
);
};

Expand Down
60 changes: 40 additions & 20 deletions frontend/src/component/executiveDashboard/UsersChart/UsersChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,45 @@ import { useMemo, type VFC } from 'react';
import 'chartjs-adapter-date-fns';
import { useTheme } from '@mui/material';
import { ExecutiveSummarySchema } from 'openapi';
import { LineChart } from '../LineChart/LineChart';
import { type ScriptableContext } from 'chart.js';
import {
fillGradientPrimary,
LineChart,
NotEnoughData,
} from '../LineChart/LineChart';

interface IUsersChartProps {
userTrends: ExecutiveSummarySchema['userTrends'];
isLoading?: boolean;
}

export const UsersChart: VFC<IUsersChartProps> = ({ userTrends }) => {
export const UsersChart: VFC<IUsersChartProps> = ({
userTrends,
isLoading,
}) => {
const theme = useTheme();
const notEnoughData = userTrends.length < 2;
const placeholderData = useMemo(
() => ({
labels: Array.from({ length: 15 }, (_, i) => i + 1).map(
(i) =>
new Date(Date.now() - (15 - i) * 7 * 24 * 60 * 60 * 1000),
),
datasets: [
{
label: 'Total users',
data: [
3, 5, 15, 17, 25, 40, 47, 48, 55, 65, 62, 72, 75, 73,
80,
],
borderColor: theme.palette.primary.light,
backgroundColor: fillGradientPrimary,
fill: true,
order: 3,
},
],
}),
[theme],
);
const data = useMemo(
() => ({
labels: userTrends.map((item) => item.date),
Expand All @@ -19,22 +49,7 @@ export const UsersChart: VFC<IUsersChartProps> = ({ userTrends }) => {
label: 'Total users',
data: userTrends.map((item) => item.total),
borderColor: theme.palette.primary.light,
backgroundColor: (context: ScriptableContext<'line'>) => {
const chart = context.chart;
const { ctx, chartArea } = chart;
if (!chartArea) {
return;
}
const gradient = ctx.createLinearGradient(
0,
chartArea.bottom,
0,
chartArea.top,
);
gradient.addColorStop(0, 'rgba(129, 122, 254, 0)');
gradient.addColorStop(1, 'rgba(129, 122, 254, 0.12)');
return gradient;
},
backgroundColor: fillGradientPrimary,
fill: true,
order: 3,
},
Expand All @@ -57,5 +72,10 @@ export const UsersChart: VFC<IUsersChartProps> = ({ userTrends }) => {
[theme, userTrends],
);

return <LineChart data={data} />;
return (
<LineChart
data={notEnoughData || isLoading ? placeholderData : data}
cover={notEnoughData ? <NotEnoughData /> : isLoading}
/>
);
};

0 comments on commit 8436937

Please sign in to comment.