Skip to content

Commit 9030e94

Browse files
authored
Metrics improvements (#3046)
Summary - Remove LIMIT from built-in dashboard queries - Make concurrency configurable per project via environment variables - Fix widget fallback period to Metrics default (1d) instead of 7d - Handle concurrency at the project level - Sort series for graphs so largest is displayed at the bottom (legend shows largest at top) - Use average aggregation for some built-in charts - Improve aggregation handling for the legend - Only render chart points when there is data; render dots on line charts - Truncate legend items and show tooltip on hover - Better preserve chart configuration when the underlying query changes
1 parent 2462c80 commit 9030e94

File tree

13 files changed

+307
-119
lines changed

13 files changed

+307
-119
lines changed

apps/webapp/app/components/code/ChartConfigPanel.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,11 @@ export function ChartConfigPanel({ columns, config, onChange, className }: Chart
147147
if (needsUpdate) {
148148
onChangeRef.current({ ...currentConfig, ...updates });
149149
}
150-
// Only re-run when the actual column structure changes, not on every config change
151-
}, [columnsKey, columns, dateTimeColumns, categoricalColumns, numericColumns]);
150+
// Only re-run when the actual column structure changes, not on every config change.
151+
// columnsKey (a string) is stable when columns match, so this won't re-fire
152+
// unnecessarily when the same query is re-run with identical columns.
153+
// eslint-disable-next-line react-hooks/exhaustive-deps
154+
}, [columnsKey]);
152155

153156
const updateConfig = useCallback(
154157
(updates: Partial<ChartConfiguration>) => {

apps/webapp/app/components/code/QueryResultsChart.tsx

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { memo, useMemo } from "react";
33
import type { ChartConfig } from "~/components/primitives/charts/Chart";
44
import { Chart } from "~/components/primitives/charts/ChartCompound";
55
import { Paragraph } from "../primitives/Paragraph";
6-
import { AggregationType, ChartConfiguration } from "../metrics/QueryWidget";
6+
import type { AggregationType, ChartConfiguration } from "../metrics/QueryWidget";
7+
import { aggregateValues } from "../primitives/charts/aggregation";
78
import { getRunStatusHexColor } from "~/components/runs/v3/TaskRunStatus";
89
import { getSeriesColor } from "./chartColors";
910

@@ -243,17 +244,18 @@ function fillTimeGaps(
243244
}
244245
filledData.push(point);
245246
} else {
246-
// Create a zero-filled data point
247-
const zeroPoint: Record<string, unknown> = {
247+
// Create a null-filled data point so gaps appear in line/bar charts
248+
// and legend aggregations (avg/min/max) skip these slots
249+
const gapPoint: Record<string, unknown> = {
248250
[xDataKey]: t,
249251
__rawDate: new Date(t),
250252
__granularity: granularity,
251253
__originalX: new Date(t).toISOString(),
252254
};
253255
for (const s of series) {
254-
zeroPoint[s] = 0;
256+
gapPoint[s] = null;
255257
}
256-
filledData.push(zeroPoint);
258+
filledData.push(gapPoint);
257259
}
258260
}
259261

@@ -671,25 +673,6 @@ function toNumber(value: unknown): number {
671673
return 0;
672674
}
673675

674-
/**
675-
* Aggregate an array of numbers using the specified aggregation function
676-
*/
677-
function aggregateValues(values: number[], aggregation: AggregationType): number {
678-
if (values.length === 0) return 0;
679-
switch (aggregation) {
680-
case "sum":
681-
return values.reduce((a, b) => a + b, 0);
682-
case "avg":
683-
return values.reduce((a, b) => a + b, 0) / values.length;
684-
case "count":
685-
return values.length;
686-
case "min":
687-
return Math.min(...values);
688-
case "max":
689-
return Math.max(...values);
690-
}
691-
}
692-
693676
/**
694677
* Sort data array by a specified column
695678
*/
@@ -775,6 +758,24 @@ export const QueryResultsChart = memo(function QueryResultsChart({
775758
return sortData(unsortedData, sortByColumn, sortDirection, xDataKey);
776759
}, [unsortedData, sortByColumn, sortDirection, isDateBased, xDataKey]);
777760

761+
// Sort series by descending total sum so largest appears at bottom of
762+
// stacked charts and first in the legend
763+
const sortedSeries = useMemo(() => {
764+
if (series.length <= 1) return series;
765+
const totals = new Map<string, number>();
766+
for (const s of series) {
767+
let total = 0;
768+
for (const point of data) {
769+
const val = point[s];
770+
if (typeof val === "number" && isFinite(val)) {
771+
total += Math.abs(val);
772+
}
773+
}
774+
totals.set(s, total);
775+
}
776+
return [...series].sort((a, b) => (totals.get(b) ?? 0) - (totals.get(a) ?? 0));
777+
}, [series, data]);
778+
778779
// Detect time granularity — use the full time range when available so tick
779780
// labels are appropriate for the period (e.g. "Jan 5" for a 7-day range
780781
// instead of just "16:00:00" when data is sparse)
@@ -809,15 +810,15 @@ export const QueryResultsChart = memo(function QueryResultsChart({
809810
// Build chart config for colors/labels
810811
const chartConfig = useMemo(() => {
811812
const cfg: ChartConfig = {};
812-
series.forEach((s, i) => {
813+
sortedSeries.forEach((s, i) => {
813814
const statusColor = groupByIsRunStatus ? getRunStatusHexColor(s) : undefined;
814815
cfg[s] = {
815816
label: s,
816817
color: statusColor ?? config.seriesColors?.[s] ?? getSeriesColor(i),
817818
};
818819
});
819820
return cfg;
820-
}, [series, groupByIsRunStatus, config.seriesColors]);
821+
}, [sortedSeries, groupByIsRunStatus, config.seriesColors]);
821822

822823
// Custom tooltip label formatter for better date display
823824
const tooltipLabelFormatter = useMemo(() => {
@@ -1002,18 +1003,19 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10021003
domain: yAxisDomain,
10031004
};
10041005

1005-
const showLegend = series.length > 0;
1006+
const showLegend = sortedSeries.length > 0;
10061007

10071008
if (chartType === "bar") {
10081009
return (
10091010
<Chart.Root
10101011
config={chartConfig}
10111012
data={data}
10121013
dataKey={xDataKey}
1013-
series={series}
1014+
series={sortedSeries}
10141015
labelFormatter={legendLabelFormatter}
10151016
showLegend={showLegend}
10161017
maxLegendItems={fullLegend ? Infinity : 5}
1018+
legendAggregation={config.aggregation}
10171019
minHeight="300px"
10181020
fillContainer
10191021
onViewAllLegendItems={onViewAllLegendItems}
@@ -1036,10 +1038,11 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10361038
config={chartConfig}
10371039
data={data}
10381040
dataKey={xDataKey}
1039-
series={series}
1041+
series={sortedSeries}
10401042
labelFormatter={legendLabelFormatter}
10411043
showLegend={showLegend}
10421044
maxLegendItems={fullLegend ? Infinity : 5}
1045+
legendAggregation={config.aggregation}
10431046
minHeight="300px"
10441047
fillContainer
10451048
onViewAllLegendItems={onViewAllLegendItems}
@@ -1049,7 +1052,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
10491052
<Chart.Line
10501053
xAxisProps={xAxisPropsForLine}
10511054
yAxisProps={yAxisProps}
1052-
stacked={stacked && series.length > 1}
1055+
stacked={stacked && sortedSeries.length > 1}
10531056
tooltipLabelFormatter={tooltipLabelFormatter}
10541057
lineType="linear"
10551058
/>

0 commit comments

Comments
 (0)