@checkstack/slo-frontend 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @checkstack/slo-frontend
2
2
 
3
+ ## 0.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - edc9ee0: Refactored SloTrendChart to use Recharts, fixing responsive layout issues and preventing visual distortion. The new implementation correctly scales and preserves SVG aspect ratios.
8
+ - @checkstack/dashboard-frontend@0.4.3
9
+ - @checkstack/catalog-common@1.4.1
10
+
11
+ ## 0.3.2
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [3da7582]
16
+ - @checkstack/ui@1.5.0
17
+ - @checkstack/dashboard-frontend@0.4.2
18
+
3
19
  ## 0.3.1
4
20
 
5
21
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/slo-frontend",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "checkstack": {
@@ -12,19 +12,20 @@
12
12
  "lint:code": "eslint . --max-warnings 0"
13
13
  },
14
14
  "dependencies": {
15
- "@checkstack/catalog-common": "1.3.1",
15
+ "@checkstack/catalog-common": "1.4.0",
16
16
  "@checkstack/common": "0.6.5",
17
- "@checkstack/dashboard-frontend": "0.3.34",
18
- "@checkstack/frontend-api": "0.3.9",
19
- "@checkstack/signal-frontend": "0.0.15",
17
+ "@checkstack/dashboard-frontend": "0.4.2",
20
18
  "@checkstack/dependency-common": "0.2.1",
19
+ "@checkstack/frontend-api": "0.3.9",
21
20
  "@checkstack/healthcheck-common": "0.11.0",
21
+ "@checkstack/signal-frontend": "0.0.15",
22
22
  "@checkstack/slo-common": "0.2.0",
23
- "@checkstack/ui": "1.3.6",
23
+ "@checkstack/ui": "1.5.0",
24
24
  "date-fns": "^4.1.0",
25
25
  "lucide-react": "^0.344.0",
26
26
  "react": "^18.2.0",
27
- "react-router-dom": "^6.20.0"
27
+ "react-router-dom": "^6.20.0",
28
+ "recharts": "^3.8.1"
28
29
  },
29
30
  "devDependencies": {
30
31
  "typescript": "^5.0.0",
@@ -1,4 +1,13 @@
1
1
  import React, { useMemo } from "react";
2
+ import {
3
+ ResponsiveContainer,
4
+ AreaChart,
5
+ Area,
6
+ XAxis,
7
+ YAxis,
8
+ Tooltip,
9
+ ReferenceLine,
10
+ } from "recharts";
2
11
 
3
12
  interface Snapshot {
4
13
  date: Date;
@@ -12,11 +21,9 @@ interface SloTrendChartProps {
12
21
  }
13
22
 
14
23
  const CHART_HEIGHT = 160;
15
- const CHART_PADDING = { top: 8, right: 12, bottom: 24, left: 48 };
16
24
 
17
25
  /**
18
- * Pure SVG line chart showing SLO availability trend from daily snapshots.
19
- * Draws a target line, area fill, and data line with responsive sizing.
26
+ * Recharts area chart showing SLO availability trend from daily snapshots.
20
27
  */
21
28
  export const SloTrendChart: React.FC<SloTrendChartProps> = ({
22
29
  snapshots,
@@ -29,155 +36,106 @@ export const SloTrendChart: React.FC<SloTrendChartProps> = ({
29
36
  (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
30
37
  );
31
38
 
39
+ // Format data for Recharts
40
+ const data = sorted.map((s) => ({
41
+ date: new Date(s.date).toLocaleDateString("en-US", {
42
+ month: "short",
43
+ day: "numeric",
44
+ }),
45
+ availabilityPercent: s.availabilityPercent,
46
+ budgetRemainingPercent: s.budgetRemainingPercent,
47
+ fullDate: s.date,
48
+ }));
49
+
32
50
  // Y-axis range: ensure target and all data points are visible
33
51
  const allValues = sorted.map((s) => s.availabilityPercent);
34
52
  const minVal = Math.min(...allValues, target);
35
53
  const maxVal = Math.max(...allValues, 100);
36
54
  const yMin = Math.max(Math.floor(minVal - 0.5), 0);
37
55
  const yMax = Math.min(Math.ceil(maxVal + 0.5), 100);
38
- const yRange = yMax - yMin || 1;
39
56
 
40
- return { sorted, yMin, yMax, yRange };
57
+ return { data, yMin, yMax };
41
58
  }, [snapshots, target]);
42
59
 
43
- if (!chartData || chartData.sorted.length < 2) {
60
+ if (!chartData || chartData.data.length < 2) {
44
61
  return (
45
62
  <div className="text-sm text-muted-foreground text-center py-8">
46
- {chartData && chartData.sorted.length === 1
63
+ {chartData && chartData.data.length === 1
47
64
  ? "Need at least 2 daily snapshots to render a trend chart"
48
65
  : "No daily snapshot data available yet"}
49
66
  </div>
50
67
  );
51
68
  }
52
69
 
53
- const { sorted, yMin, yMax, yRange } = chartData;
54
- const drawWidth = 100 - CHART_PADDING.left - CHART_PADDING.right;
55
-
56
- // Compute scaled positions (in viewBox %)
57
- const scaleX = (index: number) =>
58
- CHART_PADDING.left + (index / (sorted.length - 1)) * drawWidth;
59
-
60
- const drawHeight = CHART_HEIGHT - CHART_PADDING.top - CHART_PADDING.bottom;
61
- const scaleY = (value: number) =>
62
- CHART_PADDING.top + ((yMax - value) / yRange) * drawHeight;
63
-
64
- // Build SVG path for the line
65
- const linePath = sorted
66
- .map(
67
- (s, i) =>
68
- `${i === 0 ? "M" : "L"} ${scaleX(i).toFixed(2)} ${scaleY(s.availabilityPercent).toFixed(2)}`,
69
- )
70
- .join(" ");
71
-
72
- // Build SVG path for the area fill
73
- const areaPath = `${linePath} L ${scaleX(sorted.length - 1).toFixed(2)} ${scaleY(yMin).toFixed(2)} L ${scaleX(0).toFixed(2)} ${scaleY(yMin).toFixed(2)} Z`;
74
-
75
- // Target line Y position
76
- const targetY = scaleY(target);
77
-
78
- // Y-axis tick values
79
- const yTicks = [yMin, target, yMax].filter(
80
- (v, i, arr) => arr.indexOf(v) === i,
81
- );
82
-
83
- // X-axis labels (first, middle, last)
84
- const xLabels = [0, Math.floor(sorted.length / 2), sorted.length - 1]
85
- .filter((v, i, arr) => arr.indexOf(v) === i)
86
- .map((i) => ({
87
- x: scaleX(i),
88
- label: new Date(sorted[i].date).toLocaleDateString("en-US", {
89
- month: "short",
90
- day: "numeric",
91
- }),
92
- }));
70
+ const { data, yMin, yMax } = chartData;
93
71
 
94
72
  return (
95
- <svg
96
- viewBox={`0 0 100 ${CHART_HEIGHT}`}
97
- className="w-full"
98
- preserveAspectRatio="none"
99
- style={{ height: CHART_HEIGHT }}
100
- >
101
- {/* Area fill */}
102
- <path d={areaPath} fill="url(#slo-gradient)" opacity={0.3} />
103
-
104
- {/* Gradient definition */}
105
- <defs>
106
- <linearGradient id="slo-gradient" x1="0" y1="0" x2="0" y2="1">
107
- <stop offset="0%" stopColor="hsl(var(--primary))" stopOpacity={0.4} />
108
- <stop offset="100%" stopColor="hsl(var(--primary))" stopOpacity={0} />
109
- </linearGradient>
110
- </defs>
111
-
112
- {/* Target line */}
113
- <line
114
- x1={CHART_PADDING.left}
115
- y1={targetY}
116
- x2={100 - CHART_PADDING.right}
117
- y2={targetY}
118
- stroke="hsl(var(--destructive))"
119
- strokeWidth={0.3}
120
- strokeDasharray="1 1"
121
- opacity={0.6}
122
- />
123
-
124
- {/* Data line */}
125
- <path
126
- d={linePath}
127
- fill="none"
128
- stroke="hsl(var(--primary))"
129
- strokeWidth={0.5}
130
- strokeLinejoin="round"
131
- strokeLinecap="round"
132
- />
133
-
134
- {/* Data points */}
135
- {sorted.map((s, i) => (
136
- <circle
137
- key={i}
138
- cx={scaleX(i)}
139
- cy={scaleY(s.availabilityPercent)}
140
- r={0.8}
141
- fill="hsl(var(--primary))"
142
- >
143
- <title>
144
- {new Date(s.date).toLocaleDateString("en-US", {
145
- month: "short",
146
- day: "numeric",
147
- })}
148
- : {s.availabilityPercent.toFixed(3)}%
149
- </title>
150
- </circle>
151
- ))}
152
-
153
- {/* Y-axis labels */}
154
- {yTicks.map((tick) => (
155
- <text
156
- key={tick}
157
- x={CHART_PADDING.left - 2}
158
- y={scaleY(tick)}
159
- textAnchor="end"
160
- dominantBaseline="middle"
161
- className="fill-muted-foreground"
162
- fontSize={3.5}
163
- >
164
- {tick}%
165
- </text>
166
- ))}
167
-
168
- {/* X-axis labels */}
169
- {xLabels.map((item) => (
170
- <text
171
- key={item.x}
172
- x={item.x}
173
- y={CHART_HEIGHT - 4}
174
- textAnchor="middle"
175
- className="fill-muted-foreground"
176
- fontSize={3}
73
+ <div className="w-full" style={{ height: CHART_HEIGHT }}>
74
+ <ResponsiveContainer width="100%" height="100%">
75
+ <AreaChart
76
+ data={data}
77
+ margin={{ top: 8, right: 12, bottom: 0, left: -20 }}
177
78
  >
178
- {item.label}
179
- </text>
180
- ))}
181
- </svg>
79
+ <defs>
80
+ <linearGradient id="slo-gradient" x1="0" y1="0" x2="0" y2="1">
81
+ <stop offset="0%" stopColor="hsl(var(--primary))" stopOpacity={0.4} />
82
+ <stop offset="100%" stopColor="hsl(var(--primary))" stopOpacity={0} />
83
+ </linearGradient>
84
+ </defs>
85
+ <XAxis
86
+ dataKey="date"
87
+ axisLine={false}
88
+ tickLine={false}
89
+ tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 11 }}
90
+ dy={8}
91
+ minTickGap={30}
92
+ />
93
+ <YAxis
94
+ domain={[yMin, yMax]}
95
+ axisLine={false}
96
+ tickLine={false}
97
+ tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 12 }}
98
+ tickCount={3}
99
+ tickFormatter={(value) => `${value}%`}
100
+ />
101
+ <Tooltip
102
+ content={({ active, payload }) => {
103
+ // eslint-disable-next-line unicorn/no-null
104
+ if (!active || !payload?.length) return null;
105
+ const payloadData = payload[0].payload as (typeof data)[number];
106
+ return (
107
+ <div
108
+ className="rounded-md border bg-popover p-2 text-sm shadow-md"
109
+ style={{
110
+ backgroundColor: "hsl(var(--popover))",
111
+ border: "1px solid hsl(var(--border))",
112
+ }}
113
+ >
114
+ <p className="font-medium text-foreground mb-1">{payloadData.date}</p>
115
+ <p className="text-primary font-bold">
116
+ {payloadData.availabilityPercent.toFixed(3)}% Availability
117
+ </p>
118
+ </div>
119
+ );
120
+ }}
121
+ />
122
+ <ReferenceLine
123
+ y={target}
124
+ stroke="hsl(var(--destructive))"
125
+ strokeDasharray="4 4"
126
+ opacity={0.6}
127
+ />
128
+ <Area
129
+ type="monotone"
130
+ dataKey="availabilityPercent"
131
+ stroke="hsl(var(--primary))"
132
+ strokeWidth={2}
133
+ fill="url(#slo-gradient)"
134
+ activeDot={{ r: 4, fill: "hsl(var(--primary))" }}
135
+ isAnimationActive={false}
136
+ />
137
+ </AreaChart>
138
+ </ResponsiveContainer>
139
+ </div>
182
140
  );
183
141
  };