TraceCountTimeSeries.tsx•4.96 kB
import { useMemo } from "react";
import { graphql, useLazyLoadQuery } from "react-relay";
import {
Bar,
BarChart,
CartesianGrid,
Legend,
ResponsiveContainer,
Tooltip,
TooltipContentProps,
XAxis,
YAxis,
} from "recharts";
import { Text } from "@phoenix/components";
import {
ChartTooltip,
ChartTooltipItem,
defaultCartesianGridProps,
defaultLegendProps,
defaultXAxisProps,
defaultYAxisProps,
useBinTimeTickFormatter,
useSemanticChartColors,
useSequentialChartColors,
} from "@phoenix/components/chart";
import { useBinInterval } from "@phoenix/components/chart/useBinInterval";
import { useTimeBinScale } from "@phoenix/hooks/useTimeBin";
import { useUTCOffsetMinutes } from "@phoenix/hooks/useUTCOffsetMinutes";
import { ProjectMetricViewProps } from "@phoenix/pages/project/metrics/types";
import { intFormatter } from "@phoenix/utils/numberFormatUtils";
import { fullTimeFormatter } from "@phoenix/utils/timeFormatUtils";
import type { TraceCountTimeSeriesQuery } from "./__generated__/TraceCountTimeSeriesQuery.graphql";
function TooltipContent({
active,
payload,
label,
}: TooltipContentProps<number, string>) {
if (active && payload && payload.length) {
// For stacked bar charts, payload[0] is the first bar (error), payload[1] is the second bar (ok)
const errorValue = payload[0]?.value ?? null;
const errorColor = payload[0]?.color ?? null;
const okValue = payload[1]?.value ?? null;
const okColor = payload[1]?.color ?? null;
const okString = intFormatter(okValue);
const errorString = intFormatter(errorValue);
return (
<ChartTooltip>
{label && (
<Text weight="heavy" size="S">{`${fullTimeFormatter(
new Date(label)
)}`}</Text>
)}
<ChartTooltipItem
color={errorColor}
shape="circle"
name="error"
value={errorString}
/>
<ChartTooltipItem
color={okColor}
shape="circle"
name="ok"
value={okString}
/>
</ChartTooltip>
);
}
return null;
}
export function TraceCountTimeSeries({
projectId,
timeRange,
}: ProjectMetricViewProps) {
const scale = useTimeBinScale({ timeRange });
const utcOffsetMinutes = useUTCOffsetMinutes();
const data = useLazyLoadQuery<TraceCountTimeSeriesQuery>(
graphql`
query TraceCountTimeSeriesQuery(
$projectId: ID!
$timeRange: TimeRange!
$timeBinConfig: TimeBinConfig!
) {
project: node(id: $projectId) {
... on Project {
traceCountByStatusTimeSeries(
timeRange: $timeRange
timeBinConfig: $timeBinConfig
) {
data {
timestamp
okCount
errorCount
}
}
}
}
}
`,
{
projectId,
timeRange: {
start: timeRange.start?.toISOString(),
end: timeRange.end?.toISOString(),
},
timeBinConfig: {
scale,
utcOffsetMinutes,
},
}
);
const chartData = useMemo(
() =>
(data.project.traceCountByStatusTimeSeries?.data ?? []).map((datum) => ({
timestamp: new Date(datum.timestamp),
ok: datum.okCount,
error: datum.errorCount,
})),
[data.project.traceCountByStatusTimeSeries?.data]
);
const timeTickFormatter = useBinTimeTickFormatter({ scale });
const interval = useBinInterval({ scale });
const colors = useSequentialChartColors();
const SemanticChartColors = useSemanticChartColors();
return (
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={chartData}
margin={{ top: 0, right: 18, left: 0, bottom: 0 }}
barSize={10}
syncId={"projectMetrics"}
>
<XAxis
{...defaultXAxisProps}
dataKey="timestamp"
interval={interval}
tickFormatter={(x) => timeTickFormatter(new Date(x))}
/>
<YAxis
{...defaultYAxisProps}
width={55}
tickFormatter={(x) => intFormatter(x)}
label={{
value: "Count",
angle: -90,
dx: -20,
style: {
textAnchor: "middle",
fill: "var(--chart-axis-label-color)",
},
}}
/>
<CartesianGrid {...defaultCartesianGridProps} vertical={false} />
<Tooltip
content={TooltipContent}
// TODO formalize this
cursor={{ fill: "var(--chart-tooltip-cursor-fill-color)" }}
/>
<Bar dataKey="error" stackId="a" fill={SemanticChartColors.danger} />
<Bar
dataKey="ok"
stackId="a"
fill={colors.grey300}
radius={[2, 2, 0, 0]}
/>
<Legend iconType="circle" iconSize={8} {...defaultLegendProps} />
</BarChart>
</ResponsiveContainer>
);
}