Skip to content

Commit 38abaa2

Browse files
committed
For time-based graphs show the full time range from the filter
1 parent b0f2d6a commit 38abaa2

5 files changed

Lines changed: 63 additions & 25 deletions

File tree

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

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ interface QueryResultsChartProps {
5050
rows: Record<string, unknown>[];
5151
columns: OutputColumnMetadata[];
5252
config: ChartConfiguration;
53+
/** The effective time range from the query filter (used to show the full x-axis period) */
54+
timeRange?: { from: string; to: string };
5355
fullLegend?: boolean;
5456
/** Callback when "View all" legend button is clicked */
5557
onViewAllLegendItems?: () => void;
@@ -159,7 +161,7 @@ function formatDateByGranularity(date: Date, granularity: TimeGranularity): stri
159161
* This helps us understand the natural granularity of the data
160162
*/
161163
function detectDataInterval(timestamps: number[]): number {
162-
if (timestamps.length < 2) return 60 * 1000; // Default to 1 minute
164+
if (timestamps.length < 2) return 24 * 60 * 60 * 1000; // Default to 1 day
163165

164166
const sorted = [...timestamps].sort((a, b) => a - b);
165167
const gaps: number[] = [];
@@ -464,7 +466,8 @@ function tryParseDate(value: unknown): Date | null {
464466
*/
465467
function transformDataForChart(
466468
rows: Record<string, unknown>[],
467-
config: ChartConfiguration
469+
config: ChartConfiguration,
470+
timeRange?: { from: string; to: string }
468471
): TransformedData {
469472
const { xAxisColumn, yAxisColumns, groupByColumn, aggregation } = config;
470473

@@ -490,24 +493,37 @@ function transformDataForChart(
490493
}
491494

492495
// Determine if X-axis is date-based (most values should be parseable as dates)
493-
const isDateBased = dateValues.length >= rows.length * 0.8; // At least 80% are dates
494-
const granularity = isDateBased ? detectTimeGranularity(dateValues) : "days";
496+
// When there are no results but a timeRange is provided, treat as date-based
497+
const isDateBased =
498+
rows.length === 0 && timeRange ? true : dateValues.length >= rows.length * 0.8; // At least 80% are dates
499+
500+
// Detect granularity from the full time range when available, otherwise from data
501+
const granularity = isDateBased
502+
? timeRange
503+
? detectTimeGranularity([new Date(timeRange.from), new Date(timeRange.to)])
504+
: detectTimeGranularity(dateValues)
505+
: "days";
495506

496507
// For date-based axes, use a special key for the timestamp
497508
const xDataKey = isDateBased ? "__timestamp" : xAxisColumn;
498509

499510
// Calculate time domain and ticks for date-based axes
511+
// When a timeRange is provided (from the query filter), use it so the chart
512+
// shows the full requested period rather than just the range of returned data.
500513
let timeDomain: [number, number] | null = null;
501514
let timeTicks: number[] | null = null;
502-
if (isDateBased && dateValues.length > 0) {
503-
const timestamps = dateValues.map((d) => d.getTime());
504-
const minTime = Math.min(...timestamps);
505-
const maxTime = Math.max(...timestamps);
515+
// Raw min/max used for gap filling (without padding)
516+
let rawMinTime = 0;
517+
let rawMaxTime = 0;
518+
if (isDateBased && (dateValues.length > 0 || timeRange)) {
519+
const dataTimestamps = dateValues.map((d) => d.getTime());
520+
rawMinTime = timeRange ? new Date(timeRange.from).getTime() : Math.min(...dataTimestamps);
521+
rawMaxTime = timeRange ? new Date(timeRange.to).getTime() : Math.max(...dataTimestamps);
506522
// Add a small padding (2% on each side) so points aren't at the very edge
507-
const padding = (maxTime - minTime) * 0.02;
508-
timeDomain = [minTime - padding, maxTime + padding];
523+
const padding = (rawMaxTime - rawMinTime) * 0.02;
524+
timeDomain = [rawMinTime - padding, rawMaxTime + padding];
509525
// Generate evenly-spaced ticks across the entire range using nice intervals
510-
timeTicks = generateTimeTicks(minTime, maxTime);
526+
timeTicks = generateTimeTicks(rawMinTime, rawMaxTime);
511527
}
512528

513529
// Helper to format X value for categorical axes (non-date)
@@ -569,8 +585,8 @@ function transformDataForChart(
569585
data,
570586
xDataKey,
571587
yAxisColumns,
572-
timeDomain[0],
573-
timeDomain[1],
588+
rawMinTime,
589+
rawMaxTime,
574590
dataInterval,
575591
granularity,
576592
aggregation
@@ -638,8 +654,8 @@ function transformDataForChart(
638654
data,
639655
xDataKey,
640656
series,
641-
timeDomain[0],
642-
timeDomain[1],
657+
rawMinTime,
658+
rawMaxTime,
643659
dataInterval,
644660
granularity,
645661
aggregation
@@ -726,6 +742,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
726742
rows,
727743
columns,
728744
config,
745+
timeRange,
729746
fullLegend = false,
730747
onViewAllLegendItems,
731748
isLoading = false,
@@ -750,7 +767,7 @@ export const QueryResultsChart = memo(function QueryResultsChart({
750767
xDataKey,
751768
timeDomain,
752769
timeTicks,
753-
} = useMemo(() => transformDataForChart(rows, config), [rows, config]);
770+
} = useMemo(() => transformDataForChart(rows, config, timeRange), [rows, config, timeRange]);
754771

755772
// Apply sorting (for date-based, sort by timestamp to ensure correct order)
756773
const data = useMemo(() => {

apps/webapp/app/components/metrics/QueryWidget.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ export type QueryWidgetProps = {
137137
error?: string;
138138
data: QueryWidgetData;
139139
config: QueryWidgetConfig;
140+
/** The effective time range for the query (used to show full x-axis on time-based charts) */
141+
timeRange?: { from: string; to: string };
140142
accessory?: ReactNode;
141143
isResizing?: boolean;
142144
isDraggable?: boolean;
@@ -322,6 +324,7 @@ type QueryWidgetBodyProps = {
322324
title: ReactNode;
323325
data: QueryWidgetData;
324326
config: QueryWidgetConfig;
327+
timeRange?: { from: string; to: string };
325328
isFullscreen: boolean;
326329
setIsFullscreen: (open: boolean) => void;
327330
isLoading: boolean;
@@ -331,6 +334,7 @@ function QueryWidgetBody({
331334
title,
332335
data,
333336
config,
337+
timeRange,
334338
isFullscreen,
335339
setIsFullscreen,
336340
isLoading,
@@ -376,6 +380,7 @@ function QueryWidgetBody({
376380
rows={data.rows}
377381
columns={data.columns}
378382
config={config}
383+
timeRange={timeRange}
379384
onViewAllLegendItems={() => setIsFullscreen(true)}
380385
/>
381386
<Dialog open={isFullscreen} onOpenChange={setIsFullscreen}>
@@ -386,6 +391,7 @@ function QueryWidgetBody({
386391
rows={data.rows}
387392
columns={data.columns}
388393
config={config}
394+
timeRange={timeRange}
389395
onViewAllLegendItems={() => setIsFullscreen(true)}
390396
isLoading={showLoading}
391397
/>

apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,7 @@ export function ChartLegendCompound({
132132

133133
return (
134134
<div
135-
className={cn(
136-
"flex flex-col pt-4 text-sm",
137-
scrollable && "max-h-[50%] min-h-0",
138-
className
139-
)}
135+
className={cn("flex flex-col pt-4 text-sm", scrollable && "max-h-[50%] min-h-0", className)}
140136
>
141137
{/* Total row */}
142138
<div
@@ -155,7 +151,13 @@ export function ChartLegendCompound({
155151
<div className="mx-2 my-1 shrink-0 border-t border-charcoal-750" />
156152

157153
{/* Legend items - scrollable when scrollable prop is true */}
158-
<div className={cn("flex flex-col", scrollable && "min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600")}>
154+
<div
155+
className={cn(
156+
"flex flex-col",
157+
scrollable &&
158+
"min-h-0 flex-1 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
159+
)}
160+
>
159161
{legendItems.visible.map((item) => {
160162
const total = currentData[item.dataKey] ?? 0;
161163
const isActive = highlight.activeBarKey === item.dataKey;
@@ -211,7 +213,10 @@ export function ChartLegendCompound({
211213
remainingCount={legendItems.remaining - 1}
212214
/>
213215
) : (
214-
<ViewAllDataRow remainingCount={legendItems.remaining} onViewAll={onViewAllLegendItems} />
216+
<ViewAllDataRow
217+
remainingCount={legendItems.remaining}
218+
onViewAll={onViewAllLegendItems}
219+
/>
215220
))}
216221
</div>
217222
</div>
@@ -234,11 +239,11 @@ function ViewAllDataRow({ remainingCount, onViewAll }: ViewAllDataRowProps) {
234239
>
235240
<div className="flex items-center gap-1.5 text-text-dimmed">
236241
<div className="h-3 w-1 rounded-[2px] border border-charcoal-600" />
237-
<Paragraph variant="extra-small" className="tabular-nums">
242+
<Paragraph variant="small" className="tabular-nums">
238243
{remainingCount} more…
239244
</Paragraph>
240245
</div>
241-
<Paragraph variant="extra-small" className="text-indigo-500">
246+
<Paragraph variant="small" className="text-indigo-500">
242247
View all
243248
</Paragraph>
244249
</Button>

apps/webapp/app/routes/resources.metric.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type MetricWidgetActionResponse =
2727
reachedMaxRows: boolean;
2828
periodClipped: number | null;
2929
maxQueryPeriod: number | undefined;
30+
timeRange: { from: string; to: string };
3031
};
3132
};
3233

@@ -128,6 +129,10 @@ export const action = async ({ request }: ActionFunctionArgs) => {
128129
reachedMaxRows: queryResult.result.reachedMaxRows,
129130
periodClipped: queryResult.periodClipped,
130131
maxQueryPeriod: queryResult.maxQueryPeriod,
132+
timeRange: {
133+
from: queryResult.timeRange.from.toISOString(),
134+
to: queryResult.timeRange.to.toISOString(),
135+
},
131136
},
132137
});
133138
};
@@ -230,13 +235,16 @@ export function MetricWidget({
230235
? { rows: response.data.rows, columns: response.data.columns }
231236
: { rows: [], columns: [] };
232237

238+
const timeRange = response?.success ? response.data.timeRange : undefined;
239+
233240
return (
234241
<QueryWidget
235242
title={title}
236243
titleString={title}
237244
config={config}
238245
isLoading={isLoading}
239246
data={data}
247+
timeRange={timeRange}
240248
error={response?.success === false ? response.error : undefined}
241249
isResizing={isResizing}
242250
isDraggable={isDraggable}

apps/webapp/app/services/queryService.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export type ExecuteQueryResult<T> =
9797
queryId: string | null;
9898
periodClipped: number | null;
9999
maxQueryPeriod: number;
100+
timeRange: { from: Date; to: Date };
100101
}
101102
| { success: false; error: Error };
102103

@@ -323,6 +324,7 @@ export async function executeQuery<TOut extends z.ZodSchema>(
323324
queryId,
324325
periodClipped: periodClipped ? maxQueryPeriod : null,
325326
maxQueryPeriod,
327+
timeRange,
326328
};
327329
} finally {
328330
// Always release the concurrency slot

0 commit comments

Comments
 (0)