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
25 changes: 21 additions & 4 deletions web-common/src/features/canvas/components/kpi/KPI.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import type { ChartDataPoint } from "@rilldata/web-common/components/time-series-chart/types";
import * as Tooltip from "@rilldata/web-common/components/tooltip-v2";
import BigNumberTooltipContent from "@rilldata/web-common/features/dashboards/big-number/BigNumberTooltipContent.svelte";
import { measureSupportsTotalsQuery } from "@rilldata/web-common/features/dashboards/state-managers/selectors/measures";
import { cellInspectorStore } from "@rilldata/web-common/features/dashboards/stores/cell-inspector-store";
import RangeDisplay from "@rilldata/web-common/features/dashboards/time-controls/super-pill/components/RangeDisplay.svelte";
import { copyToClipboard } from "@rilldata/web-common/lib/actions/copy-to-clipboard";
Expand Down Expand Up @@ -50,6 +51,11 @@

$: measureIsPercentage = measure?.formatPreset === FormatPreset.PERCENTAGE;

// Measures with required dimensions (e.g. a rolling window ordered by the time
// dimension) have no single total; the provider skips the totals queries and
// we show an explanatory hint instead.
$: supportsTotal = !measure || measureSupportsTotalsQuery(measure);

$: measureValueFormatter = measure
? createMeasureValueFormatter<null>(measure, "big-number")
: () => "no data";
Expand Down Expand Up @@ -163,6 +169,9 @@
$: activeValue = getValueForType(hoveredValue);

$: tooltipValue = (() => {
if (!supportsTotal) {
return m.kpi_no_total();
}
if (hoveredValue === "percent" && computedValues.percent !== null) {
return numberPartsToString(
formatMeasurePercentageDifference(computedValues.percent),
Expand Down Expand Up @@ -230,9 +239,13 @@
onfocus={() => handleHoverOrFocus("primary")}
onblur={handleLeaveOrBlur}
>
{#if primaryTotalResult.isError}
{#if !supportsTotal}
<span class="text-fg-muted italic text-sm font-normal">
{m.kpi_no_total()}
</span>
{:else if primaryTotalResult.isError}
<AlertTriangleIcon class=" text-red-300" size="34px" />
{:else if primaryTotalResult.isLoading}
{:else if primaryTotalResult.isLoading || !measure}
<div class="loading h-6 w-16"></div>
{:else if primaryTotalResult.data}
<span class:opacity-50={primaryTotalResult.isFetching}>
Expand All @@ -241,7 +254,7 @@
{/if}
</div>

{#if showComparison}
{#if showComparison && supportsTotal}
<div class="comparison-value-wrapper">
{#if comparisonTotalResult.isError}
<div class="text-red-400">
Expand Down Expand Up @@ -337,7 +350,11 @@

{#if measure}
<Tooltip.Content side="top" sideOffset={8}>
<BigNumberTooltipContent {measure} value={tooltipValue ?? "no data"} />
<BigNumberTooltipContent
{measure}
value={tooltipValue ?? "no data"}
note={supportsTotal ? undefined : m.kpi_no_total_note()}
/>
</Tooltip.Content>
{/if}
</Tooltip.Root>
Expand Down
15 changes: 14 additions & 1 deletion web-common/src/features/canvas/components/kpi/KPIProvider.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { measureSupportsTotalsQuery } from "@rilldata/web-common/features/dashboards/state-managers/selectors/measures";
import type { TimeAndFilterStore } from "@rilldata/web-common/features/dashboards/time-controls/time-control-store";
import { TIME_COMPARISON } from "@rilldata/web-common/lib/time/config";
import { V1TimeGrain } from "@rilldata/web-common/runtime-client";
Expand Down Expand Up @@ -50,6 +51,13 @@
$: measureStore = getMeasureForMetricView(measureName, metricsViewName);
$: measure = $measureStore;

// Measures with required dimensions (e.g. a rolling window ordered by the time
// dimension) produce one value per dimension value and have no single total,
// so we skip the totals queries; the KPI shows an explanatory hint instead.
// The measure metadata must have loaded before we can tell, so the queries
// also wait for it.
$: supportsTotal = !!measure && measureSupportsTotalsQuery(measure);

$: showSparkline = sparkline !== "none" && hasTimeSeries;

$: showComparison = !!comparisonOptions?.length && showTimeComparison;
Expand All @@ -76,7 +84,11 @@
},
{
query: {
enabled: isValid && visible && (!hasTimeSeries || (!!start && !!end)),
enabled:
isValid &&
supportsTotal &&
visible &&
(!hasTimeSeries || (!!start && !!end)),
},
},
);
Expand All @@ -96,6 +108,7 @@
comparisonTimeRange &&
showComparison &&
isValid &&
supportsTotal &&
!!start &&
!!end &&
visible,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

export let measure: MetricsViewSpecMeasure;
export let value = "";
export let note: string | undefined = undefined;

$: description =
measure?.description || measure?.displayName || measure?.expression;
Expand All @@ -30,14 +31,20 @@
{description}
</TooltipDescription>

<TooltipShortcutContainer>
<div>
<StackingWord key="shift">{m.chart_copy_to_clipboard()}</StackingWord>
{m.bignumber_copy_value()}
</div>
<Shortcut>
<span style="font-family: var(--system);">⇧</span>
{m.bignumber_shift_click()}
</Shortcut>
</TooltipShortcutContainer>
{#if note}
<TooltipDescription>
{note}
</TooltipDescription>
{:else}
<TooltipShortcutContainer>
<div>
<StackingWord key="shift">{m.chart_copy_to_clipboard()}</StackingWord>
{m.bignumber_copy_value()}
</div>
<Shortcut>
<span style="font-family: var(--system);">⇧</span>
{m.bignumber_shift_click()}
</Shortcut>
</TooltipShortcutContainer>
{/if}
</TooltipContent>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import PercentageChange from "@rilldata/web-common/components/data-types/PercentageChange.svelte";
import Tooltip from "@rilldata/web-common/components/tooltip/Tooltip.svelte";
import InlineErrorIndicator from "@rilldata/web-common/features/dashboards/errors/InlineErrorIndicator.svelte";
import { measureSupportsTotalsQuery } from "@rilldata/web-common/features/dashboards/state-managers/selectors/measures";
import { ExploreStateURLParams } from "@rilldata/web-common/features/dashboards/url-state/url-params";
import DelayedSpinner from "@rilldata/web-common/features/entity-management/DelayedSpinner.svelte";
import { EntityStatus } from "@rilldata/web-common/features/entity-management/types";
Expand Down Expand Up @@ -47,6 +48,11 @@

$: measureName = measure.name ?? "";

// Measures with required dimensions (e.g. a rolling window ordered by the time
// dimension) produce one value per dimension value and have no single total,
// so we skip the totals queries and show an explanatory hint instead.
$: supportsTotal = measureSupportsTotalsQuery(measure);

// Primary totals query
$: primaryQuery = createQueryServiceMetricsViewAggregation(
client,
Expand All @@ -62,7 +68,11 @@
},
{
query: {
enabled: ready && (!!timeStart || !timeDimension) && !!measureName,
enabled:
ready &&
supportsTotal &&
(!!timeStart || !timeDimension) &&
!!measureName,
placeholderData: keepPreviousData,
refetchOnMount: false,
},
Expand All @@ -85,7 +95,11 @@
{
query: {
enabled:
ready && showComparison && !!comparisonTimeStart && !!measureName,
ready &&
supportsTotal &&
showComparison &&
!!comparisonTimeStart &&
!!measureName,
placeholderData: keepPreviousData,
refetchOnMount: false,
},
Expand Down Expand Up @@ -173,8 +187,18 @@
/** when the measure is a percentage, we don't show a percentage change. */
$: measureIsPercentage = measure?.formatPreset === FormatPreset.PERCENTAGE;

$: copyValue = measureValueFormatterUnabridged(value) ?? m.kpi_no_data();
$: tooltipValue = measureValueFormatterTooltip(value) ?? m.kpi_no_data();
// Base values for the primary total, also used to reset the tooltip/copy
// values when the pointer leaves a comparison value.
// When the measure has no total, copying is disabled by leaving the copy value undefined.
$: baseCopyValue = supportsTotal
? (measureValueFormatterUnabridged(value) ?? m.kpi_no_data())
: undefined;
$: baseTooltipValue = supportsTotal
? (measureValueFormatterTooltip(value) ?? m.kpi_no_data())
: m.kpi_no_total();

$: copyValue = baseCopyValue;
$: tooltipValue = baseTooltipValue;

$: tddHref = `?${ExploreStateURLParams.WebView}=tdd&${ExploreStateURLParams.ExpandedMeasure}=${measure.name}`;

Expand Down Expand Up @@ -212,6 +236,7 @@
slot="tooltip-content"
{measure}
value={tooltipValue}
note={supportsTotal ? undefined : m.kpi_no_total_note()}
/>

<svelte:element
Expand Down Expand Up @@ -248,7 +273,9 @@
onfocus={handleFocus}
tabindex="0"
>
{#if value !== null && value !== undefined && status === EntityStatus.Idle}
{#if !supportsTotal}
<span class="text-fg-muted italic text-sm">{m.kpi_no_total()}</span>
{:else if value !== null && value !== undefined && status === EntityStatus.Idle}
<WithTween {value} tweenProps={{ duration: 500 }} let:output>
{measureValueFormatter(output)}
</WithTween>
Expand All @@ -268,10 +295,8 @@
measureValueFormatterUnabridged(diff) ?? m.kpi_no_data();
}}
onmouseleave={() => {
tooltipValue =
measureValueFormatterTooltip(value) ?? m.kpi_no_data();
copyValue =
measureValueFormatterUnabridged(value) ?? m.kpi_no_data();
tooltipValue = baseTooltipValue;
copyValue = baseCopyValue;
}}
>
{#if !noChange}
Expand All @@ -297,10 +322,8 @@
"no data";
}}
onmouseleave={() => {
tooltipValue =
measureValueFormatterUnabridged(value) ?? m.kpi_no_data();
copyValue =
measureValueFormatterUnabridged(value) ?? m.kpi_no_data();
tooltipValue = baseTooltipValue;
copyValue = baseCopyValue;
}}
class="w-fit {comparisonDeltaColorClass}"
class:font-semibold={lowerIsBetter
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { filterOutSomeAdvancedMeasures } from "@rilldata/web-common/features/dashboards/state-managers/selectors/measures";
import {
filterOutSomeAdvancedMeasures,
measureSupportsTotalsQuery,
} from "@rilldata/web-common/features/dashboards/state-managers/selectors/measures";
import type { ExploreState } from "@rilldata/web-common/features/dashboards/stores/explore-state";
import {
type V1MetricsViewSpec,
Expand Down Expand Up @@ -128,4 +131,36 @@ describe("measures selectors", () => {
).toEqual(["mes"]);
});
});

describe("measureSupportsTotalsQuery", () => {
it("supports a measure without requiredDimensions", () => {
expect(measureSupportsTotalsQuery({ name: "mes" })).toBe(true);
});

it("supports a measure with empty requiredDimensions", () => {
expect(
measureSupportsTotalsQuery({ name: "mes", requiredDimensions: [] }),
).toBe(true);
});

it("does not support a measure with a required time dimension", () => {
expect(
measureSupportsTotalsQuery({
name: "mes_time_no_grain",
requiredDimensions: [
{ name: "time", timeGrain: V1TimeGrain.TIME_GRAIN_UNSPECIFIED },
],
}),
).toBe(false);
});

it("does not support a measure with a required categorical dimension", () => {
expect(
measureSupportsTotalsQuery({
name: "mes_per_dim",
requiredDimensions: [{ name: "domain" }],
}),
).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ export const isSimpleMeasure = (measure: MetricsViewSpecMeasure) =>
!measure.window &&
measure.type !== MetricsViewSpecMeasureType.MEASURE_TYPE_TIME_COMPARISON;

/**
* A totals query aggregates a measure to a single value with no dimensions,
* so it cannot satisfy a measure's required dimensions. E.g. a rolling window
* measure ordered by the time dimension produces one value per time bucket and
* has no meaningful single total; the backend rejects a totals query for it
* with "missing required dimension".
*/
export const measureSupportsTotalsQuery = (measure: MetricsViewSpecMeasure) =>
!measure.requiredDimensions?.length;
Comment on lines +107 to +108

/**
* Selects measure valid for current dashboard selections. We filter out advanced measures that are,
* 1. Of type MEASURE_TYPE_TIME_COMPARISON.
Expand Down
2 changes: 2 additions & 0 deletions web-common/src/lib/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1353,6 +1353,8 @@
"kpi_data_aria": "{measure} KPI data",
"kpi_no_change": "no change",
"kpi_no_data": "no data",
"kpi_no_total": "no total",
"kpi_no_total_note": "This measure is computed per dimension value (for example, a rolling average produces one value per day), so it can't be shown as a single total for the selected range.",
"kpi_not_available": "n/a",
"kpi_vs_comparison": "vs {comparison}",
"language_en": "English",
Expand Down
2 changes: 2 additions & 0 deletions web-common/src/lib/i18n/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1362,6 +1362,8 @@
"kpi_data_aria": "Datos de KPI de {measure}",
"kpi_no_change": "sin cambio",
"kpi_no_data": "sin datos",
"kpi_no_total": "sin total",
"kpi_no_total_note": "Esta medida se calcula por valor de dimensión (por ejemplo, un promedio móvil produce un valor por día), por lo que no puede mostrarse como un total único para el rango seleccionado.",
"kpi_not_available": "n/d",
"kpi_vs_comparison": "vs {comparison}",
"language_en": "English",
Expand Down
Loading