@checkstack/healthcheck-frontend 0.17.0 → 0.18.0

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.
@@ -10,7 +10,14 @@ import { extractChartFields, getFieldValue } from "./schema-parser";
10
10
  import { useStrategySchemas } from "./useStrategySchemas";
11
11
  import type { HealthCheckDiagramSlotContext } from "../slots";
12
12
  import { SparklineTooltip } from "../components/SparklineTooltip";
13
- import { Card, CardContent, CardHeader, CardTitle } from "@checkstack/ui";
13
+ import {
14
+ Badge,
15
+ Card,
16
+ CardContent,
17
+ CardHeader,
18
+ CardTitle,
19
+ usePerformance,
20
+ } from "@checkstack/ui";
14
21
  import {
15
22
  PieChart,
16
23
  Pie,
@@ -83,10 +90,10 @@ export function AutoChartGrid({ context }: AutoChartGridProps) {
83
90
  const collectorGroups = buildCollectorGroups(schemaFields, instanceMap);
84
91
 
85
92
  return (
86
- <div className="space-y-6 mt-4">
93
+ <div className="mt-4 space-y-6">
87
94
  {/* Strategy-level fields */}
88
95
  {strategyFields.length > 0 && (
89
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
96
+ <div className="space-y-4">
90
97
  {strategyFields.map((field) => (
91
98
  <AutoChartCard
92
99
  key={field.name}
@@ -118,6 +125,7 @@ interface CollectorGroupData {
118
125
  instanceKey: string;
119
126
  collectorId: string;
120
127
  displayName: string;
128
+ instanceLabel?: string;
121
129
  fields: ExpandedChartField[];
122
130
  }
123
131
 
@@ -140,15 +148,15 @@ function buildCollectorGroups(
140
148
 
141
149
  // Create a group for each instance
142
150
  for (const [index, instanceKey] of instanceKeys.entries()) {
143
- const displayName =
144
- instanceKeys.length === 1
145
- ? collectorId.split(".").pop() || collectorId
146
- : `${collectorId.split(".").pop() || collectorId} #${index + 1}`;
151
+ const displayName = collectorId.split(".").pop() || collectorId;
152
+ const instanceLabel =
153
+ instanceKeys.length > 1 ? `#${index + 1}` : undefined;
147
154
 
148
155
  groups.push({
149
156
  instanceKey,
150
157
  collectorId,
151
158
  displayName,
159
+ instanceLabel,
152
160
  fields: collectorFields.map((field) => ({
153
161
  ...field,
154
162
  instanceKey,
@@ -174,7 +182,8 @@ function CollectorGroup({
174
182
  context: HealthCheckDiagramSlotContext;
175
183
  baselines: AnomalyBaselineDto[];
176
184
  }) {
177
- // Separate fields into narrow (grid) and wide (full-width) categories
185
+ // Order: narrow (summary) cards first, then wide timeline cards.
186
+ // Layout is now fully stacked at 100% width.
178
187
  const narrowFields = group.fields.filter(
179
188
  (f) => !WIDE_CHART_TYPES.has(f.chartType),
180
189
  );
@@ -184,26 +193,30 @@ function CollectorGroup({
184
193
 
185
194
  return (
186
195
  <div className="space-y-4">
187
- <h4 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
188
- {group.displayName}
189
- </h4>
190
-
191
- {/* Narrow cards grid - these pack together nicely */}
192
- {narrowFields.length > 0 && (
193
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
194
- {narrowFields.map((field) => (
195
- <AutoChartCard
196
- key={`${field.instanceKey}-${field.name}`}
197
- field={field}
198
- context={context}
199
- baselines={baselines}
200
- />
201
- ))}
202
- </div>
203
- )}
196
+ <div className="flex flex-wrap items-center gap-2 pb-2 border-b">
197
+ <h3 className="text-lg font-semibold capitalize">
198
+ {group.displayName}
199
+ </h3>
200
+ {group.instanceLabel && (
201
+ <span className="text-sm font-medium text-muted-foreground">
202
+ {group.instanceLabel}
203
+ </span>
204
+ )}
205
+ <Badge variant="outline" className="font-mono">
206
+ {group.collectorId}
207
+ </Badge>
208
+ </div>
204
209
 
205
- {/* Wide timeline cards - assertion plus timeline fields */}
206
210
  <div className="space-y-4">
211
+ {narrowFields.map((field) => (
212
+ <AutoChartCard
213
+ key={`${field.instanceKey}-${field.name}`}
214
+ field={field}
215
+ context={context}
216
+ baselines={baselines}
217
+ />
218
+ ))}
219
+
207
220
  <AssertionStatusCard
208
221
  context={context}
209
222
  instanceKey={group.instanceKey}
@@ -227,20 +240,28 @@ function CollectorGroup({
227
240
  * Returns array of results with timestamps/time spans in chronological order.
228
241
  * Uses bucket counts with time span from aggregated data.
229
242
  */
243
+ interface AssertionResult {
244
+ passedCount: number;
245
+ failedCount: number;
246
+ errorMessage?: string;
247
+ timeLabel: string;
248
+ bucketStart: number;
249
+ bucketIntervalSeconds: number;
250
+ }
251
+
230
252
  function getAllAssertionResults(
231
253
  context: HealthCheckDiagramSlotContext,
232
254
  _instanceKey: string,
233
- ): { passed: boolean; errorMessage?: string; timeLabel?: string }[] {
255
+ ): AssertionResult[] {
234
256
  return context.buckets.map((bucket) => {
235
257
  const failedCount = bucket.degradedCount + bucket.unhealthyCount;
236
- const passed = failedCount === 0;
258
+ const passedCount = bucket.healthyCount;
237
259
  const bucketStart = new Date(bucket.bucketStart);
238
260
  const bucketEnd = new Date(bucket.bucketEnd);
239
261
  const timeSpan = `${format(bucketStart, "MMM d, HH:mm")} - ${format(bucketEnd, "HH:mm")}`;
240
262
 
241
- // Build detailed error message showing breakdown by type
242
263
  let errorMessage: string | undefined;
243
- if (!passed) {
264
+ if (failedCount > 0) {
244
265
  const parts: string[] = [];
245
266
  if (bucket.unhealthyCount > 0) {
246
267
  parts.push(`${bucket.unhealthyCount} unhealthy`);
@@ -252,13 +273,183 @@ function getAllAssertionResults(
252
273
  }
253
274
 
254
275
  return {
255
- passed,
276
+ passedCount,
277
+ failedCount,
256
278
  errorMessage,
257
279
  timeLabel: timeSpan,
280
+ bucketStart: bucketStart.getTime(),
281
+ bucketIntervalSeconds: bucket.bucketIntervalSeconds,
258
282
  };
259
283
  });
260
284
  }
261
285
 
286
+ interface AssertionDatum {
287
+ bucketStart: number;
288
+ passed: number;
289
+ failed: number;
290
+ total: number;
291
+ timeLabel: string;
292
+ errorMessage?: string;
293
+ }
294
+
295
+ const MAX_ASSERTION_BARS = 50;
296
+
297
+ function downsampleAssertion(results: AssertionResult[]): AssertionDatum[] {
298
+ if (results.length === 0) return [];
299
+
300
+ const groupSize = Math.max(1, Math.ceil(results.length / MAX_ASSERTION_BARS));
301
+ const out: AssertionDatum[] = [];
302
+
303
+ for (let i = 0; i < results.length; i += groupSize) {
304
+ const slice = results.slice(i, i + groupSize);
305
+ const first = slice[0];
306
+ let passed = 0;
307
+ let failed = 0;
308
+ const failureMessages: string[] = [];
309
+ for (const r of slice) {
310
+ passed += r.passedCount;
311
+ failed += r.failedCount;
312
+ if (r.errorMessage) failureMessages.push(r.errorMessage);
313
+ }
314
+ const startLabel = first.timeLabel.split(" - ")[0];
315
+ const endLabel = slice.at(-1)?.timeLabel.split(" - ").pop() ?? startLabel;
316
+ const timeLabel =
317
+ slice.length === 1 ? first.timeLabel : `${startLabel} – ${endLabel}`;
318
+
319
+ out.push({
320
+ bucketStart: first.bucketStart,
321
+ passed,
322
+ failed,
323
+ total: passed + failed,
324
+ timeLabel,
325
+ errorMessage:
326
+ failed > 0
327
+ ? failureMessages.length === 1
328
+ ? failureMessages[0]
329
+ : `${failed} of ${passed + failed} runs failed`
330
+ : undefined,
331
+ });
332
+ }
333
+
334
+ return out;
335
+ }
336
+
337
+ function AssertionSparkline({
338
+ results,
339
+ isLowPower,
340
+ }: {
341
+ results: AssertionResult[];
342
+ isLowPower: boolean;
343
+ }) {
344
+ const data = downsampleAssertion(results);
345
+ if (data.length === 0) return <></>;
346
+
347
+ const intervalSeconds = results[0]?.bucketIntervalSeconds ?? 3600;
348
+ const effectiveInterval =
349
+ intervalSeconds *
350
+ Math.max(1, Math.ceil(results.length / MAX_ASSERTION_BARS));
351
+ const tickFmt =
352
+ effectiveInterval >= 86_400
353
+ ? "MMM d"
354
+ : effectiveInterval >= 3600
355
+ ? "MMM d HH:mm"
356
+ : "HH:mm";
357
+
358
+ return (
359
+ <div className="w-full h-12">
360
+ <ResponsiveContainer width="100%" height="100%">
361
+ <BarChart
362
+ data={data}
363
+ margin={{ top: 2, right: 2, left: 2, bottom: 0 }}
364
+ barCategoryGap={2}
365
+ >
366
+ <XAxis
367
+ dataKey="bucketStart"
368
+ tickFormatter={(value: number) => format(new Date(value), tickFmt)}
369
+ stroke="hsl(var(--muted-foreground))"
370
+ fontSize={10}
371
+ minTickGap={48}
372
+ tickLine={false}
373
+ axisLine={false}
374
+ interval="preserveStartEnd"
375
+ />
376
+ <YAxis hide domain={[0, "dataMax"]} />
377
+ <Tooltip
378
+ cursor={{ fill: "hsl(var(--muted))", fillOpacity: 0.3 }}
379
+ content={(props) => (
380
+ <AssertionTooltip
381
+ active={props.active}
382
+ payload={
383
+ props.payload as unknown as
384
+ | { payload: AssertionDatum }[]
385
+ | undefined
386
+ }
387
+ />
388
+ )}
389
+ />
390
+ <Bar
391
+ dataKey="passed"
392
+ stackId="assertion"
393
+ fill="hsl(var(--success))"
394
+ isAnimationActive={!isLowPower}
395
+ />
396
+ <Bar
397
+ dataKey="failed"
398
+ stackId="assertion"
399
+ fill="hsl(var(--destructive))"
400
+ isAnimationActive={!isLowPower}
401
+ />
402
+ </BarChart>
403
+ </ResponsiveContainer>
404
+ </div>
405
+ );
406
+ }
407
+
408
+ function AssertionTooltip({
409
+ active,
410
+ payload,
411
+ }: {
412
+ active?: boolean;
413
+ payload?: { payload: AssertionDatum }[];
414
+ }) {
415
+ if (!active || !payload?.length) return <></>;
416
+ const datum = payload[0].payload;
417
+ const passRate =
418
+ datum.total > 0 ? Math.round((datum.passed / datum.total) * 100) : 0;
419
+ return (
420
+ <div
421
+ className="p-2 text-xs rounded-md shadow-md"
422
+ style={{
423
+ backgroundColor: "hsl(var(--popover))",
424
+ border: "1px solid hsl(var(--border))",
425
+ color: "hsl(var(--popover-foreground))",
426
+ }}
427
+ >
428
+ <p className="mb-1 text-muted-foreground">{datum.timeLabel}</p>
429
+ <div className="space-y-0.5">
430
+ <p>
431
+ <span style={{ color: "hsl(var(--success))" }}>●</span> Passed:{" "}
432
+ {datum.passed}
433
+ </p>
434
+ {datum.failed > 0 && (
435
+ <p>
436
+ <span style={{ color: "hsl(var(--destructive))" }}>●</span> Failed:{" "}
437
+ {datum.failed}
438
+ </p>
439
+ )}
440
+ {datum.total > 0 && (
441
+ <p className="pt-0.5 text-muted-foreground">{passRate}% passed</p>
442
+ )}
443
+ {datum.errorMessage && (
444
+ <p className="italic text-muted-foreground max-w-[240px] truncate">
445
+ {datum.errorMessage}
446
+ </p>
447
+ )}
448
+ </div>
449
+ </div>
450
+ );
451
+ }
452
+
262
453
  /**
263
454
  * Card showing assertion pass/fail status with historical sparkline.
264
455
  */
@@ -269,15 +460,18 @@ function AssertionStatusCard({
269
460
  context: HealthCheckDiagramSlotContext;
270
461
  instanceKey: string;
271
462
  }) {
463
+ const { isLowPower } = usePerformance();
272
464
  const results = getAllAssertionResults(context, instanceKey);
273
465
 
274
466
  if (results.length === 0) {
275
467
  return (
276
468
  <Card>
277
469
  <CardHeader className="pb-2">
278
- <CardTitle className="text-sm font-medium">Assertion</CardTitle>
470
+ <CardTitle className="text-sm font-medium text-center">
471
+ Assertion
472
+ </CardTitle>
279
473
  </CardHeader>
280
- <CardContent>
474
+ <CardContent className="text-center">
281
475
  <div className="text-sm text-muted-foreground">No data</div>
282
476
  </CardContent>
283
477
  </Card>
@@ -285,36 +479,40 @@ function AssertionStatusCard({
285
479
  }
286
480
 
287
481
  const latestResult = results.at(-1)!;
288
- const passCount = results.filter((r) => r.passed).length;
289
- const passRate = Math.round((passCount / results.length) * 100);
290
- const allPassed = results.every((r) => r.passed);
291
- const allFailed = results.every((r) => !r.passed);
482
+ const latestPassed = latestResult.failedCount === 0;
483
+ let totalPassed = 0;
484
+ let totalFailed = 0;
485
+ for (const r of results) {
486
+ totalPassed += r.passedCount;
487
+ totalFailed += r.failedCount;
488
+ }
489
+ const totalRuns = totalPassed + totalFailed;
490
+ const passRate =
491
+ totalRuns > 0 ? Math.round((totalPassed / totalRuns) * 100) : 100;
492
+ const allPassed = totalFailed === 0;
493
+ const allFailed = totalPassed === 0 && totalFailed > 0;
292
494
 
293
495
  return (
294
496
  <Card
295
- className={
296
- latestResult.passed ? "" : "border-red-200 dark:border-red-900"
297
- }
497
+ className={latestPassed ? "" : "border-red-200 dark:border-red-900"}
298
498
  >
299
499
  <CardHeader className="pb-2">
300
500
  <CardTitle
301
- className={`text-sm font-medium ${latestResult.passed ? "" : "text-red-600"}`}
501
+ className={`text-sm font-medium text-center ${latestPassed ? "" : "text-red-600"}`}
302
502
  >
303
- {latestResult.passed ? "Assertion" : "Assertion Failed"}
503
+ {latestPassed ? "Assertion" : "Assertion Failed"}
304
504
  </CardTitle>
305
505
  </CardHeader>
306
- <CardContent className="space-y-2">
506
+ <CardContent className="space-y-2 text-center">
307
507
  {/* Current status with rate */}
308
- <div className="flex items-center gap-2">
508
+ <div className="flex items-center justify-center gap-2">
309
509
  <div
310
510
  className={`w-3 h-3 rounded-full ${
311
- latestResult.passed ? "bg-green-500" : "bg-red-500"
511
+ latestPassed ? "bg-green-500" : "bg-red-500"
312
512
  }`}
313
513
  />
314
- <span
315
- className={latestResult.passed ? "text-green-600" : "text-red-600"}
316
- >
317
- {latestResult.passed ? "Passed" : "Failed"}
514
+ <span className={latestPassed ? "text-green-600" : "text-red-600"}>
515
+ {latestPassed ? "Passed" : "Failed"}
318
516
  </span>
319
517
  {!allPassed && !allFailed && (
320
518
  <span className="text-xs text-muted-foreground">
@@ -324,29 +522,14 @@ function AssertionStatusCard({
324
522
  </div>
325
523
 
326
524
  {/* Error message if failed */}
327
- {!latestResult.passed && latestResult.errorMessage && (
328
- <div className="text-sm text-red-600 bg-red-50 dark:bg-red-950 px-2 py-1 rounded truncate">
525
+ {!latestPassed && latestResult.errorMessage && (
526
+ <div className="px-2 py-1 text-sm text-red-600 truncate rounded bg-red-50 dark:bg-red-950">
329
527
  {latestResult.errorMessage}
330
528
  </div>
331
529
  )}
332
530
 
333
- {/* Sparkline timeline - render each bucket as a bar */}
334
- <div className="flex h-2 gap-px rounded">
335
- {results.map((result, index) => {
336
- const tooltip = result.timeLabel
337
- ? `${result.timeLabel}\n${result.passed ? "Passed" : result.errorMessage || "Failed"}`
338
- : result.passed
339
- ? "Passed"
340
- : "Failed";
341
- return (
342
- <SparklineTooltip key={index} content={tooltip}>
343
- <div
344
- className={`flex-1 h-full ${result.passed ? "bg-green-500" : "bg-red-500"} hover:opacity-80`}
345
- />
346
- </SparklineTooltip>
347
- );
348
- })}
349
- </div>
531
+ {/* Sparkline timeline - one bar per bucket, with cursor-tracking tooltip */}
532
+ <AssertionSparkline results={results} isLowPower={isLowPower} />
350
533
  </CardContent>
351
534
  </Card>
352
535
  );
@@ -449,9 +632,11 @@ function AutoChartCard({ field, context, baselines }: AutoChartCardProps) {
449
632
  return (
450
633
  <Card>
451
634
  <CardHeader className="pb-2">
452
- <CardTitle className="text-sm font-medium">{field.label}</CardTitle>
635
+ <CardTitle className="text-sm font-medium text-center">
636
+ {field.label}
637
+ </CardTitle>
453
638
  </CardHeader>
454
- <CardContent>
639
+ <CardContent className="flex flex-col items-center text-center [&>*]:w-full">
455
640
  <ChartRenderer field={field} context={context} baseline={baseline} />
456
641
  </CardContent>
457
642
  </Card>
@@ -546,7 +731,7 @@ function CounterRenderer({ field, context }: ChartRendererProps) {
546
731
  <div className="text-2xl font-bold">
547
732
  {value}
548
733
  {count > 1 && (
549
- <span className="text-sm font-normal text-muted-foreground ml-2">
734
+ <span className="ml-2 text-sm font-normal text-muted-foreground">
550
735
  ({count}×)
551
736
  </span>
552
737
  )}
@@ -560,7 +745,7 @@ function CounterRenderer({ field, context }: ChartRendererProps) {
560
745
  {entries.slice(0, 5).map(([value, count]) => (
561
746
  <div key={value} className="flex items-center justify-between">
562
747
  <span className="font-mono text-sm">{value}</span>
563
- <span className="text-muted-foreground text-sm">{count}×</span>
748
+ <span className="text-sm text-muted-foreground">{count}×</span>
564
749
  </div>
565
750
  ))}
566
751
  {entries.length > 5 && (
@@ -592,8 +777,8 @@ function GaugeRenderer({ field, context, baseline }: ChartRendererProps) {
592
777
  const data = [{ name: field.label, value: numValue, fill: fillColor }];
593
778
 
594
779
  return (
595
- <div className="flex flex-col gap-2">
596
- <div className="flex items-center gap-3">
780
+ <div className="flex flex-col items-center gap-2">
781
+ <div className="flex items-center justify-center gap-3">
597
782
  <ResponsiveContainer width={80} height={80}>
598
783
  <RadialBarChart
599
784
  cx="50%"
@@ -624,13 +809,21 @@ function GaugeRenderer({ field, context, baseline }: ChartRendererProps) {
624
809
  baseline.dominantValue !== null
625
810
  ) {
626
811
  let expectedNum = Number(baseline.dominantValue);
627
- if (baseline.dominantValue === "true" || baseline.dominantValue === "false") {
628
- const ratio = baseline.dominantRatio ?? (baseline.dominantValue === "true" ? 1 : 0);
629
- expectedNum = baseline.dominantValue === "true" ? (ratio * 100) : ((1 - ratio) * 100);
812
+ if (
813
+ baseline.dominantValue === "true" ||
814
+ baseline.dominantValue === "false"
815
+ ) {
816
+ const ratio =
817
+ baseline.dominantRatio ??
818
+ (baseline.dominantValue === "true" ? 1 : 0);
819
+ expectedNum =
820
+ baseline.dominantValue === "true"
821
+ ? ratio * 100
822
+ : (1 - ratio) * 100;
630
823
  }
631
824
  if (!Number.isNaN(expectedNum)) {
632
825
  return (
633
- <div className="text-xs text-muted-foreground mt-1">
826
+ <div className="mt-1 text-xs text-muted-foreground">
634
827
  Expected: {expectedNum.toFixed(1)}
635
828
  {unit}
636
829
  </div>
@@ -642,7 +835,7 @@ function GaugeRenderer({ field, context, baseline }: ChartRendererProps) {
642
835
  const min = Math.max(0, baseline.mean - baseline.stdDev * 3);
643
836
  const max = baseline.mean + baseline.stdDev * 3;
644
837
  return (
645
- <div className="text-xs text-muted-foreground mt-1">
838
+ <div className="mt-1 text-xs text-muted-foreground">
646
839
  Expected: {baseline.mean.toFixed(1)}
647
840
  {unit} (±{(baseline.stdDev * 3).toFixed(1)}) [{min.toFixed(1)} -{" "}
648
841
  {max.toFixed(1)}]
@@ -681,7 +874,7 @@ function BooleanRenderer({ field, context, baseline }: ChartRendererProps) {
681
874
  return (
682
875
  <div className="space-y-2">
683
876
  {/* Current status with rate */}
684
- <div className="flex items-center gap-2">
877
+ <div className="flex items-center justify-center gap-2">
685
878
  <div
686
879
  className={`w-3 h-3 rounded-full ${
687
880
  latestValue ? "bg-green-500" : "bg-red-500"
@@ -753,8 +946,8 @@ function TextRenderer({ field, context, baseline }: ChartRendererProps) {
753
946
  return (
754
947
  <div className="space-y-2">
755
948
  {/* Current value with count */}
756
- <div className="flex items-center gap-2">
757
- <span className="text-sm font-mono">{latestValue || "—"}</span>
949
+ <div className="flex items-center justify-center gap-2">
950
+ <span className="font-mono text-sm">{latestValue || "—"}</span>
758
951
  {!allSame && (
759
952
  <span className="text-xs text-muted-foreground">
760
953
  ({latestCount}/{valuesWithTime.length}×)
@@ -856,7 +1049,7 @@ function StatusRenderer({ field, context }: ChartRendererProps) {
856
1049
  }
857
1050
 
858
1051
  return (
859
- <div className="text-sm text-red-600 bg-red-50 dark:bg-red-950 px-2 py-1 rounded truncate">
1052
+ <div className="px-2 py-1 text-sm text-red-600 truncate rounded bg-red-50 dark:bg-red-950">
860
1053
  {String(value)}
861
1054
  </div>
862
1055
  );
@@ -897,11 +1090,14 @@ function LineChartRenderer({ field, context, baseline }: ChartRendererProps) {
897
1090
  // — the same scalar the drift evaluator uses. Surfaced as a header chip rather
898
1091
  // than a diagonal line because it's a rate, not an absolute value, and shares
899
1092
  // no natural axis with the data series.
900
- const projectedChange = baseline ? baseline.trendSlope * baseline.sampleCount : 0;
901
- const showTrend = !!baseline && Math.abs(projectedChange) > 0.01;
902
- const driftSigmas = baseline && baseline.stdDev > 0
903
- ? Math.abs(projectedChange) / baseline.stdDev
1093
+ const projectedChange = baseline
1094
+ ? baseline.trendSlope * baseline.sampleCount
904
1095
  : 0;
1096
+ const showTrend = !!baseline && Math.abs(projectedChange) > 0.01;
1097
+ const driftSigmas =
1098
+ baseline && baseline.stdDev > 0
1099
+ ? Math.abs(projectedChange) / baseline.stdDev
1100
+ : 0;
905
1101
  const isDrifting = driftSigmas >= 2;
906
1102
 
907
1103
  const chartData = valuesWithTime.map((item, index) => ({
@@ -913,25 +1109,37 @@ function LineChartRenderer({ field, context, baseline }: ChartRendererProps) {
913
1109
  return (
914
1110
  <div className="space-y-2">
915
1111
  {baseline ? (
916
- <div className="flex items-center justify-between text-xs px-1 gap-3">
917
- <span className="text-warning font-medium">
918
- Expected: {baseline.mean.toFixed(1)}{unit} (±{((baseline.stdDev / Math.sqrt(avgRunCount)) * 3).toFixed(1)})
1112
+ <div className="flex items-center justify-between gap-3 px-1 text-xs">
1113
+ <span className="font-medium text-warning">
1114
+ Expected: {baseline.mean.toFixed(1)}
1115
+ {unit} (±
1116
+ {((baseline.stdDev / Math.sqrt(avgRunCount)) * 3).toFixed(1)})
919
1117
  </span>
920
1118
  <div className="flex items-center gap-3">
921
1119
  {showTrend && (
922
- <span className={isDrifting ? "text-warning font-medium" : "text-muted-foreground"}>
923
- Trend: {projectedChange >= 0 ? "↑ +" : "↓ "}{projectedChange.toFixed(1)}{unit}
1120
+ <span
1121
+ className={
1122
+ isDrifting
1123
+ ? "text-warning font-medium"
1124
+ : "text-muted-foreground"
1125
+ }
1126
+ >
1127
+ Trend: {projectedChange >= 0 ? "↑ +" : "↓ "}
1128
+ {projectedChange.toFixed(1)}
1129
+ {unit}
924
1130
  </span>
925
1131
  )}
926
1132
  <span className="text-muted-foreground">
927
- Avg: {avg.toFixed(1)}{unit}
1133
+ Avg: {avg.toFixed(1)}
1134
+ {unit}
928
1135
  </span>
929
1136
  </div>
930
1137
  </div>
931
1138
  ) : (
932
- <div className="flex items-center justify-end text-xs px-1">
1139
+ <div className="flex items-center justify-end px-1 text-xs">
933
1140
  <span className="text-muted-foreground">
934
- Avg: {avg.toFixed(1)}{unit}
1141
+ Avg: {avg.toFixed(1)}
1142
+ {unit}
935
1143
  </span>
936
1144
  </div>
937
1145
  )}
@@ -982,8 +1190,7 @@ function LineChartRenderer({ field, context, baseline }: ChartRendererProps) {
982
1190
  <ReferenceArea
983
1191
  y1={Math.max(
984
1192
  0,
985
- baseline.mean -
986
- (baseline.stdDev / Math.sqrt(avgRunCount)) * 3,
1193
+ baseline.mean - (baseline.stdDev / Math.sqrt(avgRunCount)) * 3,
987
1194
  )}
988
1195
  y2={
989
1196
  baseline.mean + (baseline.stdDev / Math.sqrt(avgRunCount)) * 3
@@ -1011,13 +1218,13 @@ function LineChartRenderer({ field, context, baseline }: ChartRendererProps) {
1011
1218
  };
1012
1219
  return (
1013
1220
  <div
1014
- className="rounded-md border bg-popover p-2 text-sm shadow-md"
1221
+ className="p-2 text-sm border rounded-md shadow-md bg-popover"
1015
1222
  style={{
1016
1223
  backgroundColor: "hsl(var(--popover))",
1017
1224
  border: "1px solid hsl(var(--border))",
1018
1225
  }}
1019
1226
  >
1020
- <p className="text-xs text-muted-foreground mb-1">
1227
+ <p className="mb-1 text-xs text-muted-foreground">
1021
1228
  {data.timeLabel}
1022
1229
  </p>
1023
1230
  <p className="font-medium">
@@ -1072,7 +1279,7 @@ function BarChartRenderer({ field, context }: ChartRendererProps) {
1072
1279
  const data = payload[0].payload as { name: string; value: number };
1073
1280
  return (
1074
1281
  <div
1075
- className="rounded-md border bg-popover p-2 text-sm shadow-md"
1282
+ className="p-2 text-sm border rounded-md shadow-md bg-popover"
1076
1283
  style={{
1077
1284
  backgroundColor: "hsl(var(--popover))",
1078
1285
  border: "1px solid hsl(var(--border))",
@@ -1162,7 +1369,7 @@ function PieChartRenderer({ field, context }: ChartRendererProps) {
1162
1369
  };
1163
1370
  return (
1164
1371
  <div
1165
- className="rounded-md border bg-popover p-2 text-sm shadow-md"
1372
+ className="p-2 text-sm border rounded-md shadow-md bg-popover"
1166
1373
  style={{
1167
1374
  backgroundColor: "hsl(var(--popover))",
1168
1375
  border: "1px solid hsl(var(--border))",