Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/web/src/components/widget-lab/scenarios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,21 @@ export const stressScenarios: ChartScenario[] = [
chartPresentation: { legend: "right", seriesStats: false },
},
},
{
// Line keeps every series (no "Other" bucketing), so the right legend is
// genuinely tall — confirms it scrolls within the card instead of overflowing.
label: "Line — 50 series (right legend, scrolls)",
chartId: "query-builder-line",
chartName: "Query Builder Line",
category: "line",
dataState: ready(makeManySeries(50)),
display: {
title: "Latency by service (50, right legend)",
chartId: "query-builder-line",
unit: "duration_ms",
chartPresentation: { legend: "right", seriesStats: true },
},
},
{
label: "Pie — 20 slices → Other",
chartId: "query-builder-pie",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ interface QueryBuilderLegendProps {
* per-series Min/Max/Mean/Last columns.
*/
variant?: "compact" | "stats"
/**
* Upper bound (px) on the legend's own height. A right-aligned legend isn't
* height-constrained by Recharts, so a long series list would overflow the
* card; capping it here lets the `overflow-auto` body scroll instead.
*/
maxHeight?: number
}

/** Vertical space (px) a bottom-aligned legend block needs. */
Expand Down Expand Up @@ -120,12 +126,16 @@ export function QueryBuilderLegend({
unit,
layout = "bottom",
variant = "stats",
maxHeight,
}: QueryBuilderLegendProps) {
if (series.length === 0) return null

const maxHeightStyle = maxHeight != null ? { maxHeight } : undefined

if (variant === "compact") {
return (
<div
style={maxHeightStyle}
className={cn(
"h-full overflow-auto text-xs",
layout === "right"
Expand Down Expand Up @@ -158,7 +168,10 @@ export function QueryBuilderLegend({
}

return (
<div className={cn("h-full overflow-auto text-xs", layout === "right" ? "pl-3" : "pt-2")}>
<div
style={maxHeightStyle}
className={cn("h-full overflow-auto text-xs", layout === "right" ? "pl-3" : "pt-2")}
>
<table className="w-full border-collapse">
<thead>
<tr className="text-muted-foreground">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,11 @@ export function QueryBuilderAreaChart({

return (
<div ref={containerRef} className={cn("h-full w-full", className)}>
<ChartContainer config={chartConfig} className="h-full w-full aspect-auto">
<ChartContainer
config={chartConfig}
className="h-full w-full aspect-auto"
hoistLegend={!showLegendBlock}
>
<AreaChart data={processedData} accessibilityLayer syncId={syncId} syncMethod="value">
<defs>
{seriesDefinitions.map((definition) => (
Expand Down Expand Up @@ -360,6 +364,7 @@ export function QueryBuilderAreaChart({
unit={unit}
layout="right"
variant={variant}
maxHeight={containerHeight}
/>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,11 @@ export function QueryBuilderBarChart({

return (
<div ref={containerRef} className={cn("h-full w-full", className)}>
<ChartContainer config={chartConfig} className="h-full w-full aspect-auto">
<ChartContainer
config={chartConfig}
className="h-full w-full aspect-auto"
hoistLegend={!showLegendBlock}
>
<BarChart data={displayData} accessibilityLayer syncId={syncId} syncMethod="value">
<CartesianGrid vertical={false} />
<XAxis
Expand Down Expand Up @@ -271,6 +275,7 @@ export function QueryBuilderBarChart({
unit={unit}
layout="right"
variant={variant}
maxHeight={containerHeight}
/>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,11 @@ export function QueryBuilderLineChart({

return (
<div ref={containerRef} className={cn("h-full w-full", className)}>
<ChartContainer config={chartConfig} className="h-full w-full aspect-auto">
<ChartContainer
config={chartConfig}
className="h-full w-full aspect-auto"
hoistLegend={!showLegendBlock}
>
<LineChart data={processedData} accessibilityLayer syncId={syncId} syncMethod="value">
<CartesianGrid vertical={false} />
<XAxis
Expand Down Expand Up @@ -321,6 +325,7 @@ export function QueryBuilderLineChart({
unit={unit}
layout="right"
variant={variant}
maxHeight={containerHeight}
/>
}
/>
Expand Down
16 changes: 14 additions & 2 deletions packages/ui/src/components/ui/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ function useChart() {

export type ChartLegendItem = { key: string; label: React.ReactNode; color?: string }

/** Stable empty reference so the legend-slot publish effect doesn't churn. */
const EMPTY_LEGEND_ITEMS: ChartLegendItem[] = []

/**
* Optional slot for hoisting a chart's legend out of the plot area and into an
* ancestor (e.g. a card header). When a provider is present, `ChartContainer`
Expand All @@ -52,10 +55,18 @@ function ChartContainer({
className,
children,
config,
hoistLegend = true,
...props
}: React.ComponentProps<"div"> & {
config: ChartConfig
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"]
/**
* When `true` (default) and a {@link ChartLegendSlotContext} ancestor is
* present, the chart's series are published into that slot (e.g. a widget
* header strip). Charts that render their own in-plot legend should pass
* `false` so the header doesn't duplicate it.
*/
hoistLegend?: boolean
}) {
const uniqueId = React.useId()
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
Expand All @@ -73,11 +84,12 @@ function ChartContainer({
})),
[config],
)
const publishedItems = hoistLegend ? legendItems : EMPTY_LEGEND_ITEMS
React.useEffect(() => {
if (!legendSlot) return
legendSlot.setItems(legendItems)
legendSlot.setItems(publishedItems)
return () => legendSlot.setItems([])
}, [legendSlot, legendItems])
}, [legendSlot, publishedItems])

return (
<ChartContext.Provider value={{ config, containerRef, chartId }}>
Expand Down
Loading