Finalized area chart, added custom tick

This commit is contained in:
Daniel Cojocea
2024-09-11 12:46:41 -04:00
parent db3c7e6b52
commit 23dc8fed36
10 changed files with 245 additions and 190 deletions

View File

@@ -6,10 +6,12 @@ import {
Tooltip,
CartesianGrid,
ResponsiveContainer,
Text,
} from "recharts";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useMemo } from "react";
import { formatDate } from "../../../Utils/timeUtils";
import "./index.css";
const CustomToolTip = ({ active, payload, label }) => {
@@ -87,20 +89,34 @@ const CustomToolTip = ({ active, payload, label }) => {
return null;
};
const MonitorDetailsAreaChart = ({ checks }) => {
const formatDate = (timestamp) => {
const date = new Date(timestamp);
return date.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
hour12: true,
});
};
const memoizedChecks = useMemo(() => checks, [checks[0]]);
const CustomTick = ({ x, y, payload, index }) => {
const theme = useTheme();
// Render nothing for the first tick
if (index === 0) return null;
return (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.text.tertiary}
fontSize={11}
fontWeight={400}
>
{formatDate(new Date(payload.value), {
year: undefined,
month: undefined,
day: undefined,
})}
</Text>
);
};
const MonitorDetailsAreaChart = ({ checks }) => {
const theme = useTheme();
const memoizedChecks = useMemo(() => checks, [checks[0]]);
return (
<ResponsiveContainer width="100%" minWidth={25} height={220}>
<AreaChart
@@ -138,10 +154,12 @@ const MonitorDetailsAreaChart = ({ checks }) => {
<XAxis
stroke={theme.palette.border.dark}
dataKey="createdAt"
tickFormatter={formatDate}
tick={{ fontSize: "13px" }}
tick={<CustomTick />}
minTickGap={0}
axisLine={false}
tickLine={false}
height={18}
height={20}
interval="equidistantPreserveStart"
/>
<Tooltip
cursor={{ stroke: theme.palette.border.light }}

View File

@@ -1,71 +0,0 @@
import { LineChart } from "@mui/x-charts/LineChart";
import PropTypes from "prop-types";
const PageSpeedLineChart = ({ pageSpeedChecks = [] }) => {
const keyToLabel = {
performance: "Performance",
seo: "SEO",
bestPractices: "Best practices",
accessibility: "Accessibility",
};
const colors = {
performance: "#2aa02b",
seo: "#9467bd",
bestPractices: "#ff7f0e",
accessibility: "#1f76b3",
};
const customize = {
legend: { position: { vertical: "bottom", horizontal: "middle" } },
margin: { bottom: 75 },
};
const xLabels = pageSpeedChecks.map((check) => {
return check.createdAt;
});
return (
<LineChart
series={Object.keys(keyToLabel).map((key) => ({
dataKey: key,
label: keyToLabel[key],
color: colors[key],
showMark: false,
}))}
yAxis={[{ min: 0, max: 100 }]}
xAxis={[
{
scaleType: "point",
data: xLabels,
valueFormatter: (val) => new Date(val).toLocaleDateString(),
},
]}
dataset={pageSpeedChecks}
{...customize}
grid={{ vertical: true, horizontal: true }}
tooltip={{ trigger: "none" }}
slotProps={{
legend: {
direction: "row",
position: { vertical: "bottom", horizontal: "middle" },
padding: 2,
itemMarkWidth: 8,
itemMarkHeight: 8,
markGap: 5,
itemGap: 15,
labelStyle: {
fontSize: 13,
color: "#344054"
}
},
}}
/>
);
};
PageSpeedLineChart.propTypes = {
pageSpeedChecks: PropTypes.array,
};
export default PageSpeedLineChart;

View File

@@ -445,14 +445,8 @@ const DetailsPage = ({ isAdmin }) => {
data={[{ response: monitor?.periodAvgResponseTime }]}
/>
</ChartBox>
<ChartBox
sx={{
"& tspan": {
fontSize: 11,
},
}}
>
<Stack>
<ChartBox sx={{ padding: 0 }}>
<Stack pt={theme.spacing(8)} pl={theme.spacing(8)}>
<IconBox>
<ResponseTimeIcon />
</IconBox>

View File

@@ -76,7 +76,7 @@ export const StatBox = styled(Box)(({ theme }) => ({
borderColor: theme.palette.border.light,
borderRadius: 4,
backgroundColor: theme.palette.background.main,
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 35%)`,
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
"& h2": {
fontSize: 13,
fontWeight: 500,

View File

@@ -1,3 +1,4 @@
import PropTypes from "prop-types";
import {
AreaChart,
Area,
@@ -5,6 +6,7 @@ import {
Tooltip,
CartesianGrid,
ResponsiveContainer,
Text,
} from "recharts";
import { useTheme } from "@emotion/react";
import { useMemo } from "react";
@@ -12,28 +14,37 @@ import { Box, Stack, Typography } from "@mui/material";
import { formatDate } from "../../../../Utils/timeUtils";
const config = {
accessibility: {
id: "accessibility",
text: "accessibility",
color: "primary",
},
bestPractices: {
id: "bestPractices",
text: "best practices",
color: "warning",
seo: {
id: "seo",
text: "SEO",
color: "unresolved",
},
performance: {
id: "performance",
text: "performance",
color: "success",
},
seo: {
id: "seo",
text: "SEO",
color: "unresolved",
bestPractices: {
id: "bestPractices",
text: "best practices",
color: "warning",
},
accessibility: {
id: "accessibility",
text: "accessibility",
color: "primary",
},
};
/**
* Custom tooltip for the area chart.
* @param {Object} props
* @param {boolean} props.active - Whether the tooltip is active.
* @param {Array} props.payload - The payload data for the tooltip.
* @param {string} props.label - The label for the tooltip.
* @returns {JSX.Element|null} The tooltip element or null if not active.
*/
const CustomToolTip = ({ active, payload, label }) => {
const theme = useTheme();
@@ -58,50 +69,64 @@ const CustomToolTip = ({ active, payload, label }) => {
>
{formatDate(new Date(label))}
</Typography>
{Object.keys(config).map((key) => {
const { color } = config[key];
const dotColor = theme.palette[color].main;
{Object.keys(config)
.reverse()
.map((key) => {
const { color } = config[key];
const dotColor = theme.palette[color].main;
return (
<Stack
key={`${key}-tooltip`}
direction="row"
alignItems="center"
gap={theme.spacing(3)}
mt={theme.spacing(1)}
sx={{
"& span": {
color: theme.palette.text.tertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
<Box
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={dotColor}
sx={{ borderRadius: "50%" }}
/>
<Typography
component="span"
textTransform="capitalize"
sx={{ opacity: 0.8 }}
return (
<Stack
key={`${key}-tooltip`}
direction="row"
alignItems="center"
gap={theme.spacing(3)}
mt={theme.spacing(1)}
sx={{
"& span": {
color: theme.palette.text.tertiary,
fontSize: 11,
fontWeight: 500,
},
}}
>
{config[key].text}
</Typography>{" "}
<Typography component="span">
{payload[0].payload[key]}
</Typography>
</Stack>
);
})}
<Box
width={theme.spacing(4)}
height={theme.spacing(4)}
backgroundColor={dotColor}
sx={{ borderRadius: "50%" }}
/>
<Typography
component="span"
textTransform="capitalize"
sx={{ opacity: 0.8 }}
>
{config[key].text}
</Typography>{" "}
<Typography component="span">
{payload[0].payload[key]}
</Typography>
</Stack>
);
})}
</Box>
);
}
return null;
};
CustomToolTip.propTypes = {
active: PropTypes.bool,
payload: PropTypes.array,
label: PropTypes.string,
};
/**
* Processes data to insert gaps with null values based on the interval.
* @param {Array} data
* @param {number} interval - The interval in milliseconds for gaps.
* @returns {Array} The formatted data with gaps.
*/
const processDataWithGaps = (data, interval) => {
if (data.length === 0) return [];
let formattedData = [];
@@ -137,6 +162,57 @@ const processDataWithGaps = (data, interval) => {
return formattedData;
};
/**
* Custom tick component to render ticks on the XAxis.
*
* @param {Object} props
* @param {number} props.x - The x coordinate for the tick.
* @param {number} props.y - The y coordinate for the tick.
* @param {Object} props.payload - The data object containing the tick value.
* @param {number} props.index - The index of the tick in the array of ticks.
*
* @returns {JSX.Element|null} The tick element or null if the tick should be hidden.
*/
const CustomTick = ({ x, y, payload, index }) => {
const theme = useTheme();
// Render nothing for the first tick
if (index === 0) return null;
return (
<Text
x={x}
y={y + 8}
textAnchor="middle"
fill={theme.palette.text.tertiary}
fontSize={11}
fontWeight={400}
>
{formatDate(new Date(payload.value), {
year: undefined,
month: undefined,
day: undefined,
})}
</Text>
);
};
CustomTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.shape({
value: PropTypes.string.isRequired,
}),
index: PropTypes.number,
};
/**
* A chart displaying pagespeed details over time.
* @param {Object} props
* @param {Array} props.data - The data to display in the chart.
* @param {number} props.interval - The interval in milliseconds for processing gaps.
* @returns {JSX.Element} The area chart component.
*/
const PagespeedDetailsAreaChart = ({ data, interval }) => {
const theme = useTheme();
const memoizedData = useMemo(
@@ -162,23 +238,12 @@ const PagespeedDetailsAreaChart = ({ data, interval }) => {
<XAxis
stroke={theme.palette.border.dark}
dataKey="createdAt"
tickFormatter={(timestamp) =>
formatDate(new Date(timestamp), {
year: undefined,
month: undefined,
day: undefined,
})
}
tick={{
fontSize: 11,
fontWeight: 100,
opacity: 0.8,
stroke: theme.palette.text.tertiary,
}}
tick={<CustomTick />}
axisLine={false}
tickLine={false}
minTickGap={20}
height={18}
interval="preserveEnd"
minTickGap={0}
interval="equidistantPreserveStart"
/>
<Tooltip
cursor={{ stroke: theme.palette.border.light }}
@@ -220,4 +285,20 @@ const PagespeedDetailsAreaChart = ({ data, interval }) => {
);
};
PagespeedDetailsAreaChart.propTypes = {
data: PropTypes.arrayOf(
PropTypes.shape({
createdAt: PropTypes.string.isRequired,
accessibility: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
bestPractices: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
performance: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
seo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
})
).isRequired,
interval: PropTypes.number.isRequired,
};
export default PagespeedDetailsAreaChart;

View File

@@ -15,9 +15,9 @@ import { logger } from "../../../Utils/Logger";
import { networkService } from "../../../main";
import SkeletonLayout from "./skeleton";
import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
import MetricsIcon from "../../../assets/icons/ruler-icon.svg?react";
import ScoreIcon from "../../../assets/icons/monitor-graph-line.svg?react";
import PerformanceIcon from "../../../assets/icons/performance-report.svg?react";
import PageSpeedLineChart from "../../../Components/Charts/PagespeedLineChart";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import PagespeedDetailsAreaChart from "./Charts/AreaChart";
@@ -402,23 +402,44 @@ const PageSpeedDetails = () => {
</StatBox>
</Stack>
<Box>
<Typography fontSize={12} color={theme.palette.text.tertiary}>
<Typography
fontSize={12}
color={theme.palette.text.tertiary}
my={theme.spacing(8)}
>
Showing statistics for past 24 hours.
</Typography>
<ChartBox sx={{ gridTemplateColumns: "75% 25%" }}>
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
<IconBox>
<ScoreIcon />
</IconBox>
<Typography component="h2">Score history</Typography>
</Stack>
<Box>
<PagespeedDetailsAreaChart
data={data}
interval={monitor?.interval}
/>
</Box>
<Box>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>
<MetricsIcon />
</IconBox>
<Typography component="h2">Metrics</Typography>
</Stack>
</Box>
</ChartBox>
</Box>
<ChartBox>
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
<IconBox>
<ScoreIcon />
</IconBox>
<Typography component="h2">Score history</Typography>
</Stack>
<PagespeedDetailsAreaChart
data={data}
interval={monitor?.interval}
/>
</ChartBox>
<ChartBox flex={1}>
<ChartBox
flex={1}
sx={{ gridTemplateColumns: "40% 60%", gridTemplateRows: "15% 85%" }}
>
<Stack direction="row" alignItems="center" gap={theme.spacing(6)}>
<IconBox>
<PerformanceIcon />
@@ -536,16 +557,7 @@ const PageSpeedDetails = () => {
</Typography>
</Typography>
</Stack>
<Box
px={theme.spacing(20)}
py={theme.spacing(8)}
height="100%"
flex={1}
sx={{
borderLeft: 1,
borderLeftColor: theme.palette.border.light,
}}
>
<Box px={theme.spacing(20)} py={theme.spacing(8)} height="100%">
<Typography
mb={theme.spacing(6)}
pb={theme.spacing(8)}

View File

@@ -1,22 +1,40 @@
import { Box, Stack, styled } from "@mui/material";
export const ChartBox = styled(Stack)(({ theme }) => ({
gap: theme.spacing(8),
display: "grid",
height: 300,
minWidth: 250,
padding: theme.spacing(8),
border: 1,
borderStyle: "solid",
borderColor: theme.palette.border.light,
borderRadius: 4,
borderTopRightRadius: 16,
borderBottomRightRadius: 16,
backgroundColor: theme.palette.background.main,
"& h2": {
color: theme.palette.text.secondary,
fontSize: 15,
fontWeight: 500,
},
"& p": {
color: theme.palette.text.secondary,
"& p": { color: theme.palette.text.secondary },
"& > :nth-of-type(1)": {
gridColumn: 1,
gridRow: 1,
height: "fit-content",
paddingTop: theme.spacing(8),
paddingLeft: theme.spacing(8),
},
"& > :nth-of-type(2)": { gridColumn: 1, gridRow: 2 },
"& > :nth-of-type(3)": {
gridColumn: 2,
gridRow: "span 2",
padding: theme.spacing(8),
borderLeft: 1,
borderLeftStyle: "solid",
borderLeftColor: theme.palette.border.light,
borderRadius: 16,
backgroundColor: theme.palette.background.main,
background: `linear-gradient(325deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
},
}));
@@ -52,7 +70,7 @@ export const StatBox = styled(Box)(({ theme }) => ({
borderColor: theme.palette.border.light,
borderRadius: 4,
backgroundColor: theme.palette.background.main,
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 35%)`,
background: `linear-gradient(340deg, ${theme.palette.background.accent} 20%, ${theme.palette.background.main} 45%)`,
"& h2": {
fontSize: 13,
fontWeight: 500,

View File

@@ -63,7 +63,7 @@ const darkTheme = createTheme({
uptimeGood: "#ffd600",
uptimeExcellent: "#079455",
},
unresolved: { main: "#4e5ba6", light: "#e2eaf7", bg: "#f2f4f7" },
unresolved: { main: "#664eff", light: "#3a1bff", bg: "#f2f4f7" },
divider: border.light,
other: {
icon: "#e6e6e6",

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5 5.50001L16 7.00001M11.5 8.50001L13 10M8.49996 11.5L9.99996 13M5.49996 14.5L6.99996 16M2.56561 17.5657L6.43424 21.4344C6.63225 21.6324 6.73125 21.7314 6.84542 21.7685C6.94584 21.8011 7.05401 21.8011 7.15443 21.7685C7.2686 21.7314 7.3676 21.6324 7.56561 21.4344L21.4342 7.56573C21.6322 7.36772 21.7313 7.26872 21.7683 7.15455C21.801 7.05413 21.801 6.94596 21.7683 6.84554C21.7313 6.73137 21.6322 6.63237 21.4342 6.43436L17.5656 2.56573C17.3676 2.36772 17.2686 2.26872 17.1544 2.23163C17.054 2.199 16.9458 2.199 16.8454 2.23163C16.7313 2.26872 16.6322 2.36772 16.4342 2.56573L2.56561 16.4344C2.3676 16.6324 2.2686 16.7314 2.2315 16.8455C2.19887 16.946 2.19887 17.0541 2.2315 17.1546C2.2686 17.2687 2.3676 17.3677 2.56561 17.5657Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 929 B