Skip to content

Commit

Permalink
[OPIK-437]: add fe cost chart (#778)
Browse files Browse the repository at this point in the history
* [OPIK-437]: add cost chart;

* [OPIK-437]: work on precision;

* [OPIK-437]: work on precision;

* [OPIK-437]: fix eslint issue;

* [OPIK-437]: fix the unhandled cases of cost displaying in small containers;

* [OPIK-437]: fix the tailwind order issue

* [OPIK-437]: fix the min width;

* [OPIK-437]: fix scss eslint issues;

* [OPIK-437]: improve the behavior for ticks;

* [OPIK-437]: eslint issues;

* [OPIK-437]: unite chart behaviors;

* [OPIK-437]: add toFixed;

* [OPIK-437]: handle a case with ticks with no decimals;

* [OPIK-437]: update the min width;

* [OPIK-437]: update the import from lodash;

---------

Co-authored-by: Sasha <[email protected]>
  • Loading branch information
aadereiko and Sasha authored Dec 2, 2024
1 parent b9705b0 commit fc1b7a3
Show file tree
Hide file tree
Showing 10 changed files with 284 additions and 79 deletions.
1 change: 1 addition & 0 deletions apps/opik-frontend/src/api/projects/useProjectMetric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export enum METRIC_NAME_TYPE {
TRACE_COUNT = "TRACE_COUNT",
DURATION = "DURATION",
TOKEN_USAGE = "TOKEN_USAGE",
COST = "COST",
}

export enum INTERVAL_TYPE {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@ import { useObserveResizeNode } from "@/hooks/useObserveResizeNode";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { DEFAULT_CHART_TICK } from "@/constants/chart";
import {
getDefaultChartYTickWidth,
getDefaultHashedColorsChartConfig,
} from "@/lib/charts";
import { getDefaultHashedColorsChartConfig } from "@/lib/charts";
import ChartTooltipContent, {
ChartTooltipRenderHeaderArguments,
} from "@/components/shared/ChartTooltipContent/ChartTooltipContent";
import useChartTickDefaultConfig from "@/hooks/charts/useChartTickDefaultConfig";

const MIN_LEGEND_WIDTH = 140;
const MAX_LEGEND_WIDTH = 300;
Expand Down Expand Up @@ -58,14 +56,23 @@ const ExperimentChartContainer: React.FC<ExperimentChartContainerProps> = ({
return chartData.data.every((record) => isEmpty(record.scores));
}, [chartData.data]);

const tickWidth = useMemo(() => {
const values = chartData.data.reduce<number[]>((acc, data) => {
const values = useMemo(() => {
return chartData.data.reduce<number[]>((acc, data) => {
return [...acc, ...Object.values(data.scores)];
}, []);

return getDefaultChartYTickWidth({ values });
}, [chartData.data]);

const {
width: tickWidth,
ticks,
domain,
tickFormatter,
interval: tickInterval,
} = useChartTickDefaultConfig(values, {
tickPrecision: 2,
numberOfTicks: 3,
});

const [width, setWidth] = useState<number>(0);
const { ref } = useObserveResizeNode<HTMLDivElement>((node) =>
setWidth(node.clientWidth),
Expand Down Expand Up @@ -121,7 +128,10 @@ const ExperimentChartContainer: React.FC<ExperimentChartContainerProps> = ({
axisLine={false}
tickLine={false}
tick={DEFAULT_CHART_TICK}
interval="preserveStartEnd"
interval={tickInterval}
ticks={ticks}
tickFormatter={tickFormatter}
domain={domain}
/>
<ChartTooltip
cursor={false}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ import { ProjectMetricValue } from "@/types/projects";
import { ChartContainer, ChartTooltip } from "@/components/ui/chart";
import dayjs from "dayjs";
import { DEFAULT_CHART_TICK } from "@/constants/chart";
import {
getDefaultChartYTickWidth,
getDefaultHashedColorsChartConfig,
} from "@/lib/charts";
import { getDefaultHashedColorsChartConfig } from "@/lib/charts";
import { Spinner } from "@/components/ui/spinner";
import useProjectMetric, {
INTERVAL_TYPE,
METRIC_NAME_TYPE,
} from "@/api/projects/useProjectMetric";
import ChartTooltipContent, {
ChartTooltipRenderHeaderArguments,
ChartTooltipRenderValueArguments,
} from "@/components/shared/ChartTooltipContent/ChartTooltipContent";
import { formatDate } from "@/lib/date";
import { formatCost } from "@/lib/money";
import useChartTickDefaultConfig from "@/hooks/charts/useChartTickDefaultConfig";

interface MetricChartProps {
name: string;
Expand Down Expand Up @@ -90,11 +90,13 @@ const MetricChart = ({
return getDefaultHashedColorsChartConfig(lines);
}, [lines]);

const yTickWidth = useMemo(() => {
return getDefaultChartYTickWidth({
values,
});
}, [values]);
const {
width: yTickWidth,
ticks,
tickFormatter: yTickFormatter,
domain,
interval: yTickInterval,
} = useChartTickDefaultConfig(values);

const renderChartTooltipHeader = useCallback(
({ payload }: ChartTooltipRenderHeaderArguments) => {
Expand All @@ -107,7 +109,18 @@ const MetricChart = ({
[],
);

const tickFormatter = useCallback(
const renderTooltipValue = useCallback(
({ value }: ChartTooltipRenderValueArguments) => {
if (metricName === METRIC_NAME_TYPE.COST) {
return formatCost(value as number);
}

return value;
},
[metricName],
);

const xTickFormatter = useCallback(
(val: string) => {
if (interval === INTERVAL_TYPE.HOURLY) {
return dayjs(val).utc().format("MM/DD hh:mm A");
Expand Down Expand Up @@ -137,7 +150,7 @@ const MetricChart = ({
margin={{
top: 5,
right: 10,
left: 0,
left: 5,
bottom: 5,
}}
>
Expand All @@ -147,19 +160,26 @@ const MetricChart = ({
axisLine={false}
tickLine={false}
tick={DEFAULT_CHART_TICK}
tickFormatter={tickFormatter}
tickFormatter={xTickFormatter}
/>
<YAxis
tick={DEFAULT_CHART_TICK}
axisLine={false}
width={yTickWidth}
tickLine={false}
tickFormatter={yTickFormatter}
ticks={ticks}
domain={domain}
interval={yTickInterval}
/>
<ChartTooltip
cursor={false}
isAnimationActive={false}
content={
<ChartTooltipContent renderHeader={renderChartTooltipHeader} />
<ChartTooltipContent
renderHeader={renderChartTooltipHeader}
renderValue={renderTooltipValue}
/>
}
/>
<Tooltip />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ const MetricsTab = ({ projectId }: MetricsTabProps) => {
disableLoadingData={!isValidDays}
/>
</div>

<div className="flex-1">
<MetricChart
name="Estimated cost"
metricName={METRIC_NAME_TYPE.COST}
interval={interval}
intervalStart={intervalStart}
intervalEnd={intervalEnd}
projectId={projectId}
disableLoadingData={!isValidDays}
/>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,77 +18,102 @@ export type ChartTooltipRenderHeaderArguments = {
payload: Payload<ValueType, NameType>[];
};

export type ChartTooltipRenderValueArguments = {
value: ValueType;
};

type ChartTooltipContentProps = {
renderHeader?: ({
payload,
}: ChartTooltipRenderHeaderArguments) => React.ReactNode;

renderValue?: ({
value,
}: ChartTooltipRenderValueArguments) => React.ReactNode;
} & React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
React.ComponentProps<"div">;

const ChartTooltipContent = React.forwardRef<
HTMLDivElement,
ChartTooltipContentProps
>(({ active, payload, color, renderHeader }, ref) => {
const { config } = useChart();
>(
(
{ active, payload, color, renderHeader, renderValue: parentRenderValue },
ref,
) => {
const { config } = useChart();

if (!active || !payload?.length) {
return null;
}
if (!active || !payload?.length) {
return null;
}

return (
<Popover open>
<PopoverAnchor asChild>
<div className="size-0.5 bg-transparent"></div>
</PopoverAnchor>
<PopoverContent className="min-w-32 max-w-72 px-1 py-1.5">
<div ref={ref} className="grid items-start gap-1.5 bg-background">
{isFunction(renderHeader) && (
<div className="mb-1 max-w-full overflow-hidden border-b px-2 pt-0.5">
{renderHeader({ payload })}
</div>
)}
const renderValue = (value: ValueType) => {
if (isFunction(parentRenderValue)) {
return parentRenderValue({ value });
}

<div className="grid gap-1.5">
{payload.map((item) => {
const key = `${item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const indicatorColor = color || item.payload.fill || item.color;
return value.toLocaleString();
};

return (
<div
key={key}
className="flex h-6 w-full flex-wrap items-center gap-1.5 px-2"
>
return (
<Popover open>
<PopoverAnchor asChild>
<div className="size-0.5 bg-transparent"></div>
</PopoverAnchor>
<PopoverContent className="min-w-32 max-w-72 px-1 py-1.5">
<div ref={ref} className="grid items-start gap-1.5 bg-background">
{isFunction(renderHeader) && (
<div className="mb-1 max-w-full overflow-hidden border-b px-2 pt-0.5">
{renderHeader({ payload })}
</div>
)}

<div className="grid gap-1.5">
{payload.map((item) => {
const key = `${item.name || item.dataKey || "value"}`;
const itemConfig = getPayloadConfigFromPayload(
config,
item,
key,
);
const indicatorColor = color || item.payload.fill || item.color;

return (
<div
className="size-2 shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]"
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
<div className="flex flex-1 items-center justify-between gap-2 leading-none">
<div className="grid gap-1.5">
<span className="comet-body-xs truncate text-muted-slate">
{itemConfig?.label || item.name}
</span>
key={key}
className="flex h-6 w-full flex-wrap items-center gap-1.5 px-2"
>
<div
className="size-2 shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]"
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
<div className="flex flex-1 items-center justify-between gap-2 leading-none">
<div className="grid gap-1.5">
<span className="comet-body-xs truncate text-muted-slate">
{itemConfig?.label || item.name}
</span>
</div>
{!isUndefined(item.value) && (
<span className="comet-body-xs-accented">
{renderValue(item.value)}
</span>
)}
</div>
{!isUndefined(item.value) && (
<span className="comet-body-xs-accented">
{item.value.toLocaleString()}
</span>
)}
</div>
</div>
);
})}
);
})}
</div>
</div>
</div>
</PopoverContent>
</Popover>
);
});
</PopoverContent>
</Popover>
);
},
);
ChartTooltipContent.displayName = "ChartTooltipContent";

export default ChartTooltipContent;
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ const TraceDataViewer: React.FunctionComponent<TraceDataViewerProps> = ({
<TooltipWrapper content="Estimated cost">
<div
data-testid="data-viewer-scores"
className="flex items-center gap-2 px-1"
className="flex items-center gap-2 break-all px-1"
>
<Coins className="size-4 shrink-0" />
{formatCost(data.total_estimated_cost)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
}

.chainSpanOuterContainer {
min-width: 150px;
min-width: 200px;
display: flex;
flex-direction: column;
align-items: start;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ export const treeRenderers: TreeRenderProps = {
</TooltipWrapper>
)}
{!isUndefined(estimatedCost) && (
<TooltipWrapper content="Estimated cost">
<TooltipWrapper
content={`Estimated cost ${formatCost(estimatedCost)}`}
>
<div className={styles.chainSpanDetailsItem}>
<Coins /> {formatCost(estimatedCost, true)}
</div>
Expand Down
Loading

0 comments on commit fc1b7a3

Please sign in to comment.