Skip to content

Commit 3964385

Browse files
committed
feat: drag to zoom in errors chart
1 parent 9c3bb3b commit 3964385

File tree

4 files changed

+207
-76
lines changed

4 files changed

+207
-76
lines changed

apps/dashboard/app/(main)/websites/[id]/errors/_components/error-trends-chart.tsx

Lines changed: 113 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,15 @@
33
import { ArrowCounterClockwiseIcon, BugIcon } from "@phosphor-icons/react";
44
import dynamic from "next/dynamic";
55
import { useCallback, useState } from "react";
6-
import { Area, CartesianGrid, Legend, Tooltip, XAxis, YAxis } from "recharts";
6+
import {
7+
Area,
8+
CartesianGrid,
9+
Legend,
10+
ReferenceArea,
11+
Tooltip,
12+
XAxis,
13+
YAxis,
14+
} from "recharts";
715
import { METRIC_COLORS, METRICS } from "@/components/charts/metrics-constants";
816
import { Button } from "@/components/ui/button";
917
import { ErrorChartTooltip } from "./error-chart-tooltip";
@@ -17,46 +25,80 @@ const AreaChart = dynamic(
1725
() => import("recharts").then((mod) => mod.AreaChart),
1826
{ ssr: false }
1927
);
20-
const Brush = dynamic(() => import("recharts").then((mod) => mod.Brush), {
21-
ssr: false,
22-
});
2328

24-
interface ErrorTrendsChartProps {
29+
type ErrorTrendsChartProps = {
2530
errorChartData: Array<{
2631
date: string;
2732
"Total Errors": number;
2833
"Affected Users": number;
2934
}>;
30-
}
35+
};
3136

3237
export const ErrorTrendsChart = ({ errorChartData }: ErrorTrendsChartProps) => {
33-
const [zoomDomain, setZoomDomain] = useState<{
34-
startIndex?: number;
35-
endIndex?: number;
36-
}>({});
37-
const [isZoomed, setIsZoomed] = useState(false);
38+
const [refAreaLeft, setRefAreaLeft] = useState<string | null>(null);
39+
const [refAreaRight, setRefAreaRight] = useState<string | null>(null);
40+
const [zoomedData, setZoomedData] = useState<Array<{
41+
date: string;
42+
"Total Errors": number;
43+
"Affected Users": number;
44+
}> | null>(null);
45+
46+
const isZoomed = zoomedData !== null;
47+
48+
const displayData = zoomedData || errorChartData;
3849

3950
const resetZoom = useCallback(() => {
40-
setZoomDomain({});
41-
setIsZoomed(false);
51+
setRefAreaLeft(null);
52+
setRefAreaRight(null);
53+
setZoomedData(null);
4254
}, []);
4355

44-
const handleBrushChange = useCallback(
45-
(brushData: { startIndex?: number; endIndex?: number }) => {
46-
if (
47-
brushData &&
48-
brushData.startIndex !== undefined &&
49-
brushData.endIndex !== undefined
50-
) {
51-
setZoomDomain({
52-
startIndex: brushData.startIndex,
53-
endIndex: brushData.endIndex,
54-
});
55-
setIsZoomed(true);
56-
}
57-
},
58-
[]
59-
);
56+
const handleMouseDown = (e: any) => {
57+
if (!e?.activeLabel) {
58+
return;
59+
}
60+
setRefAreaLeft(e.activeLabel);
61+
setRefAreaRight(null);
62+
};
63+
64+
const handleMouseMove = (e: any) => {
65+
if (!(refAreaLeft && e?.activeLabel)) {
66+
return;
67+
}
68+
setRefAreaRight(e.activeLabel);
69+
};
70+
71+
const handleMouseUp = () => {
72+
if (!refAreaLeft) {
73+
setRefAreaLeft(null);
74+
setRefAreaRight(null);
75+
return;
76+
}
77+
78+
const rightBoundary = refAreaRight || refAreaLeft;
79+
80+
const leftIndex = errorChartData.findIndex((d) => d.date === refAreaLeft);
81+
const rightIndex = errorChartData.findIndex(
82+
(d) => d.date === rightBoundary
83+
);
84+
85+
if (leftIndex === -1 || rightIndex === -1) {
86+
setRefAreaLeft(null);
87+
setRefAreaRight(null);
88+
return;
89+
}
90+
91+
const [startIndex, endIndex] =
92+
leftIndex < rightIndex
93+
? [leftIndex, rightIndex]
94+
: [rightIndex, leftIndex];
95+
96+
const zoomed = errorChartData.slice(startIndex, endIndex + 1);
97+
setZoomedData(zoomed);
98+
99+
setRefAreaLeft(null);
100+
setRefAreaRight(null);
101+
};
60102

61103
if (!errorChartData.length) {
62104
return (
@@ -94,38 +136,47 @@ export const ErrorTrendsChart = ({ errorChartData }: ErrorTrendsChartProps) => {
94136
Error occurrences and impact over time
95137
</p>
96138
</div>
97-
{errorChartData.length > 5 && (
98-
<div className="flex items-center gap-2">
99-
{isZoomed && (
100-
<Button
101-
className="h-7 px-2 text-xs"
102-
onClick={resetZoom}
103-
size="sm"
104-
variant="outline"
105-
>
106-
<ArrowCounterClockwiseIcon
107-
className="mr-1 h-3 w-3"
108-
size={16}
109-
weight="duotone"
110-
/>
111-
Reset Zoom
112-
</Button>
113-
)}
114-
<div className="text-muted-foreground text-xs">Drag to zoom</div>
115-
</div>
116-
)}
139+
<div className="flex items-center gap-2">
140+
{isZoomed && (
141+
<Button
142+
className="h-7 px-2 text-xs"
143+
onClick={resetZoom}
144+
size="sm"
145+
variant="outline"
146+
>
147+
<ArrowCounterClockwiseIcon
148+
className="mr-1 h-3 w-3"
149+
size={16}
150+
weight="duotone"
151+
/>
152+
Reset Zoom
153+
</Button>
154+
)}
155+
<div className="text-muted-foreground text-xs">Drag to zoom</div>
156+
</div>
117157
</div>
118158
<div className="flex-1 p-2">
119-
<div style={{ width: "100%", height: 300 }}>
159+
<div
160+
className="relative select-none"
161+
style={{
162+
width: "100%",
163+
height: 300,
164+
userSelect: refAreaLeft ? "none" : "auto",
165+
WebkitUserSelect: refAreaLeft ? "none" : "auto",
166+
}}
167+
>
120168
<ResponsiveContainer height="100%" width="100%">
121169
<AreaChart
122-
data={errorChartData}
170+
data={displayData}
123171
margin={{
124172
top: 10,
125173
right: 10,
126174
left: 0,
127-
bottom: errorChartData.length > 5 ? 35 : 5,
175+
bottom: displayData.length > 5 ? 35 : 5,
128176
}}
177+
onMouseDown={handleMouseDown}
178+
onMouseMove={handleMouseMove}
179+
onMouseUp={handleMouseUp}
129180
>
130181
<defs>
131182
<linearGradient
@@ -203,9 +254,18 @@ export const ErrorTrendsChart = ({ errorChartData }: ErrorTrendsChartProps) => {
203254
wrapperStyle={{
204255
fontSize: "10px",
205256
paddingTop: "5px",
206-
bottom: errorChartData.length > 5 ? 20 : 0,
257+
bottom: displayData.length > 5 ? 20 : 0,
207258
}}
208259
/>
260+
{refAreaLeft && refAreaRight && (
261+
<ReferenceArea
262+
fill="var(--sidebar-ring)"
263+
fillOpacity={0.15}
264+
strokeOpacity={0.3}
265+
x1={refAreaLeft}
266+
x2={refAreaRight}
267+
/>
268+
)}
209269
<Area
210270
dataKey="Total Errors"
211271
fill="url(#colorTotalErrors)"
@@ -224,19 +284,6 @@ export const ErrorTrendsChart = ({ errorChartData }: ErrorTrendsChartProps) => {
224284
strokeWidth={2}
225285
type="monotone"
226286
/>
227-
{errorChartData.length > 5 && (
228-
<Brush
229-
dataKey="date"
230-
endIndex={zoomDomain.endIndex}
231-
fill="var(--muted)"
232-
fillOpacity={0.1}
233-
height={25}
234-
onChange={handleBrushChange}
235-
padding={{ top: 5, bottom: 5 }}
236-
startIndex={zoomDomain.startIndex}
237-
stroke="var(--border)"
238-
/>
239-
)}
240287
</AreaChart>
241288
</ResponsiveContainer>
242289
</div>

apps/dashboard/components/layout/navigation/navigation-config.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export const websiteNavigation: NavigationSection[] = [
199199
createNavItem("Performance", ActivityIcon, "/performance", {
200200
rootLevel: false,
201201
}),
202-
createNavItem("Geographic Data", MapPinIcon, "/map", { rootLevel: false }),
202+
createNavItem("Geographic", MapPinIcon, "/map", { rootLevel: false }),
203203
createNavItem("Error Tracking", BugIcon, "/errors", { rootLevel: false }),
204204
]),
205205
createNavSection("Product Analytics", TrendUpIcon, [

apps/dashboard/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"react-hook-form": "^7.65.0",
7272
"react-image-crop": "^11.0.10",
7373
"react-leaflet": "^5.0.0",
74+
"react-simple-maps": "^3.0.0",
7475
"recharts": "^2.15.4",
7576
"shiki": "^3.13.0",
7677
"sonner": "catalog:",
@@ -92,6 +93,7 @@
9293
"@types/node": "^22.18.9",
9394
"@types/react": "catalog:",
9495
"@types/react-dom": "catalog:",
96+
"@types/react-simple-maps": "^3.0.6",
9597
"@types/topojson-client": "^3.1.5",
9698
"husky": "^9.1.7",
9799
"lint-staged": "^16.2.4",

0 commit comments

Comments
 (0)