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
7 changes: 5 additions & 2 deletions apps/webapp/app/components/code/ChartConfigPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,11 @@ export function ChartConfigPanel({ columns, config, onChange, className }: Chart
if (needsUpdate) {
onChangeRef.current({ ...currentConfig, ...updates });
}
// Only re-run when the actual column structure changes, not on every config change
}, [columnsKey, columns, dateTimeColumns, categoricalColumns, numericColumns]);
// Only re-run when the actual column structure changes, not on every config change.
// columnsKey (a string) is stable when columns match, so this won't re-fire
// unnecessarily when the same query is re-run with identical columns.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [columnsKey]);

const updateConfig = useCallback(
(updates: Partial<ChartConfiguration>) => {
Expand Down
63 changes: 33 additions & 30 deletions apps/webapp/app/components/code/QueryResultsChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { memo, useMemo } from "react";
import type { ChartConfig } from "~/components/primitives/charts/Chart";
import { Chart } from "~/components/primitives/charts/ChartCompound";
import { Paragraph } from "../primitives/Paragraph";
import { AggregationType, ChartConfiguration } from "../metrics/QueryWidget";
import type { AggregationType, ChartConfiguration } from "../metrics/QueryWidget";
import { aggregateValues } from "../primitives/charts/aggregation";
import { getRunStatusHexColor } from "~/components/runs/v3/TaskRunStatus";
import { getSeriesColor } from "./chartColors";

Expand Down Expand Up @@ -243,17 +244,18 @@ function fillTimeGaps(
}
filledData.push(point);
} else {
// Create a zero-filled data point
const zeroPoint: Record<string, unknown> = {
// Create a null-filled data point so gaps appear in line/bar charts
// and legend aggregations (avg/min/max) skip these slots
const gapPoint: Record<string, unknown> = {
[xDataKey]: t,
__rawDate: new Date(t),
__granularity: granularity,
__originalX: new Date(t).toISOString(),
};
for (const s of series) {
zeroPoint[s] = 0;
gapPoint[s] = null;
}
filledData.push(zeroPoint);
filledData.push(gapPoint);
}
}

Expand Down Expand Up @@ -671,25 +673,6 @@ function toNumber(value: unknown): number {
return 0;
}

/**
* Aggregate an array of numbers using the specified aggregation function
*/
function aggregateValues(values: number[], aggregation: AggregationType): number {
if (values.length === 0) return 0;
switch (aggregation) {
case "sum":
return values.reduce((a, b) => a + b, 0);
case "avg":
return values.reduce((a, b) => a + b, 0) / values.length;
case "count":
return values.length;
case "min":
return Math.min(...values);
case "max":
return Math.max(...values);
}
}

/**
* Sort data array by a specified column
*/
Expand Down Expand Up @@ -775,6 +758,24 @@ export const QueryResultsChart = memo(function QueryResultsChart({
return sortData(unsortedData, sortByColumn, sortDirection, xDataKey);
}, [unsortedData, sortByColumn, sortDirection, isDateBased, xDataKey]);

// Sort series by descending total sum so largest appears at bottom of
// stacked charts and first in the legend
const sortedSeries = useMemo(() => {
if (series.length <= 1) return series;
const totals = new Map<string, number>();
for (const s of series) {
let total = 0;
for (const point of data) {
const val = point[s];
if (typeof val === "number" && isFinite(val)) {
total += Math.abs(val);
}
}
totals.set(s, total);
}
return [...series].sort((a, b) => (totals.get(b) ?? 0) - (totals.get(a) ?? 0));
}, [series, data]);

// Detect time granularity — use the full time range when available so tick
// labels are appropriate for the period (e.g. "Jan 5" for a 7-day range
// instead of just "16:00:00" when data is sparse)
Expand Down Expand Up @@ -809,15 +810,15 @@ export const QueryResultsChart = memo(function QueryResultsChart({
// Build chart config for colors/labels
const chartConfig = useMemo(() => {
const cfg: ChartConfig = {};
series.forEach((s, i) => {
sortedSeries.forEach((s, i) => {
const statusColor = groupByIsRunStatus ? getRunStatusHexColor(s) : undefined;
cfg[s] = {
label: s,
color: statusColor ?? config.seriesColors?.[s] ?? getSeriesColor(i),
};
});
return cfg;
}, [series, groupByIsRunStatus, config.seriesColors]);
}, [sortedSeries, groupByIsRunStatus, config.seriesColors]);

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

const showLegend = series.length > 0;
const showLegend = sortedSeries.length > 0;

if (chartType === "bar") {
return (
<Chart.Root
config={chartConfig}
data={data}
dataKey={xDataKey}
series={series}
series={sortedSeries}
labelFormatter={legendLabelFormatter}
showLegend={showLegend}
maxLegendItems={fullLegend ? Infinity : 5}
legendAggregation={config.aggregation}
minHeight="300px"
fillContainer
onViewAllLegendItems={onViewAllLegendItems}
Expand All @@ -1036,10 +1038,11 @@ export const QueryResultsChart = memo(function QueryResultsChart({
config={chartConfig}
data={data}
dataKey={xDataKey}
series={series}
series={sortedSeries}
labelFormatter={legendLabelFormatter}
showLegend={showLegend}
maxLegendItems={fullLegend ? Infinity : 5}
legendAggregation={config.aggregation}
minHeight="300px"
fillContainer
onViewAllLegendItems={onViewAllLegendItems}
Expand All @@ -1049,7 +1052,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
<Chart.Line
xAxisProps={xAxisPropsForLine}
yAxisProps={yAxisProps}
stacked={stacked && series.length > 1}
stacked={stacked && sortedSeries.length > 1}
tooltipLabelFormatter={tooltipLabelFormatter}
lineType="linear"
/>
Expand Down
Loading