SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
2.9 KB · 59 lines tsx
Raw Blame History
1"use client";2import * as React from "react";3import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";4import { formatCompact, formatNumber } from "@/lib/format";5import { AXIS_TICK, CHART_COLORS, CHART_MARGIN, formatBucketLabel, formatTick } from "./chart-theme";6import { ChartTooltip } from "./chart-tooltip";7import { ChartLegend } from "./chart-frame";89export interface RequestsBarPoint {10  t: string;11  success: number;12  failed: number;13}1415/** Stacked success/failed bars per bucket. Legend + 1px surface gap between segments keep the two readable without color. */16export function RequestsBarChart({ data, bucket, height = 240 }: { data: RequestsBarPoint[]; bucket: "hour" | "day"; height?: number }) {17  const totalSuccess = data.reduce((a, d) => a + d.success, 0);18  const totalFailed = data.reduce((a, d) => a + d.failed, 0);19  const tickEvery = Math.max(1, Math.ceil(data.length / (bucket === "hour" ? 8 : 10)));20  return (21    <div>22      <ChartLegend23        className="mb-3"24        items={[25          { label: "Successful", color: CHART_COLORS.success, value: formatNumber(totalSuccess) },26          { label: "Failed", color: CHART_COLORS.danger, value: formatNumber(totalFailed) },27        ]}28      />29      <ResponsiveContainer width="100%" height={height}>30        <BarChart data={data} margin={CHART_MARGIN} barCategoryGap="30%">31          <CartesianGrid vertical={false} stroke={CHART_COLORS.grid} />32          <XAxis dataKey="t" tickFormatter={(v: string) => formatTick(v, bucket)} tick={AXIS_TICK} axisLine={false} tickLine={false} interval={tickEvery - 1} minTickGap={16} />33          <YAxis tick={AXIS_TICK} axisLine={false} tickLine={false} width={40} allowDecimals={false} tickFormatter={(v: number) => formatCompact(v)} />34          <Tooltip35            cursor={{ fill: CHART_COLORS.cursor, opacity: 0.6 }}36            content={37              <ChartTooltip38                labelFormatter={(l) => formatBucketLabel(String(l), bucket)}39                names={{ success: "Successful", failed: "Failed" }}40                footer={(p) => {41                  const total = p.reduce((a, e) => a + Number(e.value ?? 0), 0);42                  return (43                    <span className="flex justify-between">44                      <span>Total</span>45                      <span className="font-mono tabular text-fg">{formatNumber(total)}</span>46                    </span>47                  );48                }}49              />50            }51          />52          <Bar dataKey="success" stackId="r" fill={CHART_COLORS.success} stroke={CHART_COLORS.surface} strokeWidth={1} maxBarSize={28} isAnimationActive={false} />53          <Bar dataKey="failed" stackId="r" fill={CHART_COLORS.danger} stroke={CHART_COLORS.surface} strokeWidth={1} radius={[3, 3, 0, 0]} maxBarSize={28} isAnimationActive={false} />54        </BarChart>55      </ResponsiveContainer>56    </div>57  );58}59