Skip to content

Commit eb8e999

Browse files
committed
Surface time dimensions in time pickers (#9608)
* feat: surface time dimensions in time pickers - Visual metrics editor: time picker now lists defined time dimensions (with display name and description) alongside raw timestamp columns, grouped under section headers. - Explore/Canvas time picker: primary range tooltip shows the active time dimension's name and description when the time axis is a dimension (not a raw column). - Hide the time axis switcher when there is only one time dimension. * Show display name (not raw column) in time axis trigger Only render the time axis name when it resolves to a defined time dimension, using its display name; hide it for a raw column.
1 parent a4188ab commit eb8e999

7 files changed

Lines changed: 137 additions & 32 deletions

File tree

web-common/src/components/forms/Input.svelte

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,15 @@
4747
export let leadingIcon: ComponentType<SvelteComponent> | undefined =
4848
undefined;
4949
export let options:
50-
| { value: string; label: string; type?: string }[]
50+
| {
51+
value: string;
52+
label: string;
53+
type?: string;
54+
description?: string;
55+
tooltip?: string;
56+
icon?: ComponentType<SvelteComponent>;
57+
group?: string;
58+
}[]
5159
| undefined = undefined;
5260
export let additionalClass = "";
5361
export let onInput: (

web-common/src/components/forms/Select.svelte

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
disabled?: boolean;
2424
tooltip?: string;
2525
icon?: ComponentType<SvelteComponent>;
26+
/** Optional section heading; a heading is rendered above the first
27+
* option of each consecutive group. */
28+
group?: string;
2629
}[];
2730
export let optionsLoading: boolean = false;
2831
export let onAddNew: (() => void) | null = null;
@@ -182,7 +185,14 @@
182185
</div>
183186
</div>
184187
{:else}
185-
{#each filteredOptions as { type, value, label, description, disabled, tooltip, icon } (value)}
188+
{#each filteredOptions as { type, value, label, description, disabled, tooltip, icon, group }, i (value)}
189+
{#if group && group !== filteredOptions[i - 1]?.group}
190+
<div
191+
class="px-2 pt-2 pb-1 text-fg-secondary text-[11px] font-semibold uppercase tracking-wide"
192+
>
193+
{group}
194+
</div>
195+
{/if}
186196
<Select.Item
187197
{value}
188198
{label}
@@ -192,7 +202,9 @@
192202
>
193203
{#if tooltip}
194204
<Tooltip.Root>
195-
<Tooltip.Trigger class="select-tooltip cursor-default">
205+
<Tooltip.Trigger
206+
class="inline-flex items-center gap-x-2 cursor-default text-left"
207+
>
196208
{#if icon}
197209
<svelte:component this={icon} size="16px" />
198210
{:else if type}

web-common/src/features/dashboards/filters/Filters.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,8 @@
200200
$: timeDimensionOptions = $timeDimensions.map((timeDim) => {
201201
return {
202202
value: timeDim.name!,
203-
label: timeDim.name!,
203+
label: timeDim.displayName || timeDim.name!,
204+
description: timeDim.description,
204205
};
205206
});
206207

web-common/src/features/dashboards/time-controls/super-pill/SuperPill.svelte

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@
4545
export let side: "top" | "right" | "bottom" | "left" = "bottom";
4646
export let primaryTimeDimension: string | undefined = undefined;
4747
export let selectedTimeDimension: string | undefined = undefined;
48-
export let timeDimensions: { value: string; label: string }[] = [];
48+
export let timeDimensions: {
49+
value: string;
50+
label: string;
51+
description?: string;
52+
}[] = [];
4953
export let onSelectRange: (range: NamedRange | ISODurationString) => void;
5054
export let onPan: (direction: "left" | "right") => void;
5155
export let onTimeGrainSelect: (timeGrain: V1TimeGrain) => void;

web-common/src/features/dashboards/time-controls/super-pill/new-time-dropdown/PrimaryRangeTooltip.svelte

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,22 @@
44
55
export let timeString: string | undefined;
66
export let interval: Interval<true>;
7+
// Set only when the active time axis is a time dimension (not a raw column).
8+
export let timeDimensionLabel: string | undefined = undefined;
9+
export let timeDimensionDescription: string | undefined = undefined;
710
</script>
811

912
<TooltipContent class="flex-col flex items-center gap-y-0 p-3">
13+
{#if timeDimensionLabel}
14+
<div class="flex flex-col items-center mb-1">
15+
<span class="font-bold text-fg-inverse">{timeDimensionLabel}</span>
16+
{#if timeDimensionDescription}
17+
<span class="text-fg-inverse/70 text-center max-w-64">
18+
{timeDimensionDescription}
19+
</span>
20+
{/if}
21+
</div>
22+
{/if}
1023
<span class="font-semibold italic mb-1">{timeString}</span>
1124
{#if interval.isValid}
1225
<span

web-common/src/features/dashboards/time-controls/super-pill/new-time-dropdown/RangePickerV2.svelte

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@
6262
export let availableTimeZones: string[];
6363
export let lockTimeZone = false;
6464
export let showFullRange = true;
65-
export let timeDimensions: { value: string; label: string }[];
65+
export let timeDimensions: {
66+
value: string;
67+
label: string;
68+
description?: string;
69+
}[];
6670
export let primaryTimeDimension: string | undefined;
6771
export let selectedTimeDimension: string | undefined;
6872
export let onTimeDimensionSelect: ((dimension: string) => void) | undefined =
@@ -109,6 +113,13 @@
109113
110114
$: selectedLabel = getRangeLabel(timeString);
111115
116+
// Resolve the active time axis to a defined time dimension, if any. When the
117+
// timeseries points at a raw column this is undefined and the tooltip omits
118+
// the dimension name and description.
119+
$: activeTimeDimension = timeDimensions.find(
120+
({ value }) => value === (selectedTimeDimension || primaryTimeDimension),
121+
);
122+
112123
$: zoneAbbreviation = getAbbreviationForIANA(maxDate ?? DateTime.now(), zone);
113124
114125
$: smallestTimeGrainOrder = getGrainOrder(
@@ -269,7 +280,12 @@
269280

270281
<Tooltip.Content side="bottom" sideOffset={8} class="z-50">
271282
{#if interval}
272-
<PrimaryRangeTooltip {timeString} {interval} />
283+
<PrimaryRangeTooltip
284+
{timeString}
285+
{interval}
286+
timeDimensionLabel={activeTimeDimension?.label}
287+
timeDimensionDescription={activeTimeDimension?.description}
288+
/>
273289
{/if}
274290
</Tooltip.Content>
275291
</Tooltip.Root>
@@ -429,7 +445,7 @@
429445
</div>
430446
{/if}
431447

432-
{#if timeDimensions.length && onTimeDimensionSelect}
448+
{#if timeDimensions.length > 1 && onTimeDimensionSelect}
433449
<div class="w-full h-fit px-1">
434450
<div class="h-px w-full bg-gray-200 my-1"></div>
435451

@@ -446,11 +462,11 @@
446462
<Clock size="14px" />
447463
</div>
448464
<div class="mr-auto">Time axis</div>
449-
<div class="sr-only group-hover:not-sr-only">
450-
<SyntaxElement
451-
range={selectedTimeDimension || primaryTimeDimension}
452-
/>
453-
</div>
465+
{#if activeTimeDimension}
466+
<div class="sr-only group-hover:not-sr-only">
467+
<SyntaxElement range={activeTimeDimension.label} />
468+
</div>
469+
{/if}
454470
<CaretDownIcon className="-rotate-90" size="14px" />
455471
</Popover.Trigger>
456472

@@ -460,21 +476,41 @@
460476
sideOffset={12}
461477
class="p-1 z-50"
462478
>
463-
{#each timeDimensions as { value, label } (value)}
464-
<button
465-
class="item"
466-
aria-label="Select {label} time dimension"
467-
onclick={() => {
468-
onTimeDimensionSelect(value);
469-
closeMenu();
470-
timeAxisPickerOpen = false;
471-
}}
472-
>
473-
{label}
474-
{#if value === (selectedTimeDimension || primaryTimeDimension)}
475-
<Check class="size-4" color="var(--color-gray-800)" />
479+
{#each timeDimensions as { value, label, description } (value)}
480+
<Tooltip.Root>
481+
<Tooltip.Trigger>
482+
{#snippet child({ props })}
483+
<button
484+
{...props}
485+
class="item"
486+
aria-label="Select {label} time dimension"
487+
onclick={() => {
488+
onTimeDimensionSelect(value);
489+
closeMenu();
490+
timeAxisPickerOpen = false;
491+
}}
492+
>
493+
{label}
494+
{#if value === (selectedTimeDimension || primaryTimeDimension)}
495+
<Check
496+
class="size-4"
497+
color="var(--color-gray-800)"
498+
/>
499+
{/if}
500+
</button>
501+
{/snippet}
502+
</Tooltip.Trigger>
503+
{#if description}
504+
<Tooltip.Content
505+
side="right"
506+
sideOffset={8}
507+
class="max-w-64"
508+
>
509+
<div class="font-bold text-fg-inverse">{label}</div>
510+
<div class="text-fg-inverse/70">{description}</div>
511+
</Tooltip.Content>
476512
{/if}
477-
</button>
513+
</Tooltip.Root>
478514
{/each}
479515
</Popover.Content>
480516
</Popover.Root>

web-common/src/features/workspaces/VisualMetrics.svelte

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
type V1Resource,
2424
} from "@rilldata/web-common/runtime-client/gen/index.schemas";
2525
import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2";
26-
import { PlusIcon } from "lucide-svelte";
26+
import { Clock, PlusIcon } from "lucide-svelte";
2727
import { tick } from "svelte";
2828
import { parseDocument, Scalar, YAMLMap, YAMLSeq } from "yaml";
2929
import ConnectorExplorer from "../connectors/explorer/ConnectorExplorer.svelte";
@@ -216,9 +216,7 @@
216216
217217
$: columns = columnsResponse?.profileColumns ?? [];
218218
219-
$: timeOptions = columns
220-
.filter(({ type }) => type && TIMESTAMPS.has(type))
221-
.map(({ name }) => ({ value: name ?? "", label: name ?? "" }));
219+
$: timeColumns = columns.filter(({ type }) => type && TIMESTAMPS.has(type));
222220
223221
$: typeOfSelectedTimeDimension = columns.find(
224222
({ name }) => name === timeDimension,
@@ -249,6 +247,39 @@
249247
: [],
250248
};
251249
250+
// Time dimensions defined in the metrics view can be selected as the primary
251+
// time dimension in addition to raw timestamp columns.
252+
$: timeDimensionOptions = itemGroups.dimensions
253+
.filter((d) => d.type === "time")
254+
.map((d) => {
255+
const value = d.name || d.resourceName;
256+
return {
257+
value,
258+
label: d.display_name || value,
259+
tooltip: d.description || undefined,
260+
icon: Clock,
261+
group: "Time dimensions",
262+
};
263+
})
264+
.filter(({ value }) => value);
265+
266+
$: timeDimensionValues = new Set(
267+
timeDimensionOptions.map(({ value }) => value),
268+
);
269+
270+
$: timeOptions = [
271+
...timeDimensionOptions,
272+
...timeColumns
273+
.filter(({ name }) => !timeDimensionValues.has(name ?? ""))
274+
.map(({ name, type }) => ({
275+
value: name ?? "",
276+
label: name ?? "",
277+
type,
278+
// Only label the columns section when time dimensions are also listed.
279+
group: timeDimensionOptions.length ? "Columns" : undefined,
280+
})),
281+
];
282+
252283
$: dimensionNamesAndLabels = itemGroups.dimensions.reduce(
253284
(acc, { name, display_name, resourceName }) => {
254285
acc.name = Math.max(acc.name, name.length || resourceName?.length || 0);
@@ -685,7 +716,7 @@
685716
disabledMessage={!hasValidModelOrSourceSelection
686717
? "No model selected"
687718
: "No timestamp columns in model"}
688-
hint="Column from model that will be used as primary time dimension in dashboards"
719+
hint="Time dimension or timestamp column used as the primary time dimension in dashboards"
689720
onChange={async (value) => {
690721
await updateProperties({ timeseries: value });
691722
}}

0 commit comments

Comments
 (0)