Skip to content
Draft
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
14 changes: 12 additions & 2 deletions web-common/src/features/canvas/components/charts/BaseChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import type {
V1Resource,
} from "@rilldata/web-common/runtime-client";
import { get, writable, type Readable, type Writable } from "svelte/store";
import type { V1MetricsViewAggregationResponseDataItem } from "@rilldata/web-common/runtime-client";
import type { OtherGroupResult } from "../../../components/charts/circular/other-grouping";
import type {
ChartDataQuery,
ChartDomainValues,
Expand Down Expand Up @@ -104,11 +106,19 @@ export abstract class BaseChart<
abstract chartTitle(fields: ChartFieldsMap): string;

getChartDomainValues(): ChartDomainValues {
// Default implementation returns empty metadata
// Subclasses can override to provide specific metadata
return {};
}

getDataTransformer(): ((
data: V1MetricsViewAggregationResponseDataItem[],
) => V1MetricsViewAggregationResponseDataItem[]) | undefined {
return undefined;
}

getOtherGroupResult(): OtherGroupResult | undefined {
return undefined;
}

protected getDefaultFieldConfig(): Partial<FieldConfig> {
return {
showAxisTitle: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
bind:view={viewVL}
themeMode={$isThemeModeDark ? "dark" : "light"}
theme={currentTheme}
otherGroupResult={component.getOtherGroupResult()}
/>
{/if}
{:else}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ export function getChartDataForCanvas(
themeStore: ctx.canvasEntity.theme,
timeAndFilterStore,
themeModeStore,
dataTransformer: component.getDataTransformer(),
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import {
CircularChartProvider,
type CircularChartSpec as CircularChartSpecBase,
} from "@rilldata/web-common/features/components/charts/circular/CircularChartProvider";
import type { OtherGroupResult } from "@rilldata/web-common/features/components/charts/circular/other-grouping";
import {
ChartSortType,
type ChartFieldsMap,
} from "@rilldata/web-common/features/components/charts/types";
import type { TimeAndFilterStore } from "@rilldata/web-common/features/dashboards/time-controls/time-control-store";
import {
MetricsViewSpecDimensionType,
type V1MetricsViewAggregationResponseDataItem,
type V1MetricsViewSpec,
type V1Resource,
} from "@rilldata/web-common/runtime-client";
Expand Down Expand Up @@ -112,6 +114,16 @@ export class CircularChartComponent extends BaseChart<CircularCanvasChartSpec> {
return this.provider.getChartDomainValues();
}

getDataTransformer(): ((
data: V1MetricsViewAggregationResponseDataItem[],
) => V1MetricsViewAggregationResponseDataItem[]) | undefined {
return (data) => this.provider.transformData(data);
}

getOtherGroupResult(): OtherGroupResult | undefined {
return this.provider.otherGroupResult;
}

chartTitle(fields: ChartFieldsMap) {
return this.provider.chartTitle(fields);
}
Expand Down
79 changes: 76 additions & 3 deletions web-common/src/features/components/charts/Chart.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,18 @@
compileToBrushedVegaSpec,
createAdaptiveScrubHandler,
} from "./brush-builder";
import type { ChartDataResult, ChartType } from "./types";
import type { VLTooltipFormatter } from "@rilldata/web-common/components/vega/types";
import {
OTHER_SLICE_COLOR_DARK,
OTHER_SLICE_COLOR_LIGHT,
OTHER_SLICE_LABEL,
} from "./circular/other-grouping";
import type { OtherGroupResult } from "./circular/other-grouping";
import {
createPieTooltipFormatter,
type PieTooltipConfig,
} from "./circular/pie-tooltip";
import type { ChartDataResult, ChartType, ColorMapping } from "./types";
import { generateSpec, getColorMappingForChart } from "./util";

export let chartType: ChartType;
Expand All @@ -42,6 +53,7 @@
*/
export let theme: Record<string, string> | undefined = undefined;
export let isCanvas: boolean;
export let otherGroupResult: OtherGroupResult | undefined = undefined;

export let isScrubbing: boolean = false;
export let temporalField: string | undefined = undefined;
Expand Down Expand Up @@ -130,12 +142,72 @@
// Color mapping needs to be reactive to theme mode changes (light/dark)
// because colors are resolved differently for each mode
$: isThemeModeDark = themeMode === "dark";
$: colorMapping = getColorMappingForChart(
$: colorMapping = applyOtherColor(
getColorMappingForChart(chartSpec, domainValues, isThemeModeDark),
isThemeModeDark,
);

$: isCircularChart = chartType === "donut_chart" || chartType === "pie_chart";

$: pieTooltipFormatter = buildPieTooltipFormatter(
isCircularChart,
chartSpec,
domainValues,
otherGroupResult,
colorMapping,
isThemeModeDark,
measureFormatters,
chartDataWithTheme,
);

function buildPieTooltipFormatter(
isCircular: boolean,
spec: CanvasChartSpec,
otherResult: OtherGroupResult | undefined,
mapping: ColorMapping | undefined,
isDark: boolean,
formatters: Record<string, (val: number | null | undefined) => string>,
chartDataResult: ChartDataResult,
): VLTooltipFormatter | undefined {
if (!isCircular || !("measure" in spec) || !("color" in spec)) return undefined;
const measureField = (spec.measure as { field?: string })?.field;
const colorField = (spec.color as { field?: string })?.field;
if (!measureField || !colorField) return undefined;

const grandTotal = otherResult?.total ??
(chartDataResult.domainValues?.["__otherTotal"]?.[0] as number | undefined) ?? 0;
if (grandTotal <= 0) return undefined;

const measureMeta = chartDataResult.fields[measureField];
const colorMeta = chartDataResult.fields[colorField];
const formatter = formatters[sanitizeFieldName(measureField)];

const cfg: PieTooltipConfig = {
colorField,
measureField,
colorFieldLabel: colorMeta?.displayName || colorField,
measureFieldLabel: measureMeta?.displayName || measureField,
otherItems: otherResult?.otherItems ?? [],
grandTotal,
colorMapping: mapping ?? [],
isDarkMode: isDark,
formatValue: (val) => (formatter ? formatter(val) : String(val)),
};

return createPieTooltipFormatter(cfg);
}

function applyOtherColor(
mapping: ColorMapping | undefined,
isDark: boolean,
): ColorMapping | undefined {
if (!mapping) return mapping;
return mapping.map((m) =>
m.value === OTHER_SLICE_LABEL
? { ...m, color: isDark ? OTHER_SLICE_COLOR_DARK : OTHER_SLICE_COLOR_LIGHT }
: m,
);
}

const scrubHandler = createAdaptiveScrubHandler((interval) =>
onBrush?.(interval),
);
Expand Down Expand Up @@ -227,6 +299,7 @@
renderer="canvas"
{expressionFunctions}
{hasComparison}
tooltipFormatter={pieTooltipFormatter}
config={getRillTheme(isThemeModeDark, theme)}
/>
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
V1Expression,
V1MetricsViewAggregationDimension,
V1MetricsViewAggregationMeasure,
V1MetricsViewAggregationResponseDataItem,
V1MetricsViewAggregationSort,
} from "@rilldata/web-common/runtime-client";
import { getQueryServiceMetricsViewAggregationQueryOptions } from "@rilldata/web-common/runtime-client";
Expand All @@ -28,12 +29,18 @@ import {
type Writable,
} from "svelte/store";
import { getFilterWithNullHandling } from "../query-util";
import {
computeOtherGrouping,
OTHER_SLICE_LABEL,
type OtherGroupResult,
} from "./other-grouping";

export type CircularChartSpec = {
metrics_view: string;
measure?: FieldConfig<"quantitative">;
color?: FieldConfig<"nominal">;
innerRadius?: number;
showOther?: boolean;
};

export type CircularChartDefaultOptions = {
Expand All @@ -51,6 +58,7 @@ export class CircularChartProvider {

customColorValues: string[] = [];
totalsValue: number | undefined = undefined;
otherGroupResult: OtherGroupResult | undefined = undefined;

combinedWhere: Writable<V1Expression | undefined> = writable(undefined);

Expand Down Expand Up @@ -81,12 +89,21 @@ export class CircularChartProvider {
}

let colorSort: V1MetricsViewAggregationSort | undefined;
let limit: number;
let queryLimit: number = this.defaultColorLimit;
const colorDimensionName = config.color?.field;
const showTotal = config.measure?.showTotal;
const userLimit = config.color?.limit;

if (colorDimensionName) {
limit = config.color?.limit || this.defaultColorLimit;
const showOtherEnabled = config.showOther !== false;
if (showOtherEnabled) {
queryLimit = Math.max(
userLimit ?? this.defaultColorLimit,
this.defaultColorLimit,
);
} else {
queryLimit = userLimit || this.defaultColorLimit;
}
dimensions = [{ name: colorDimensionName }];
colorSort = this.getColorSort(config);
}
Expand Down Expand Up @@ -114,7 +131,7 @@ export class CircularChartProvider {
sort: colorSort ? [colorSort] : undefined,
where: topNWhere,
timeRange,
limit: limit?.toString(),
limit: queryLimit?.toString(),
},
{
query: {
Expand All @@ -127,13 +144,16 @@ export class CircularChartProvider {

const topNColorQuery = createQuery(topNColorQueryOptionsStore);

const showOther = config.showOther !== false;
const needsTotal = showTotal || showOther;

const totalQueryOptionsStore = derived(
[timeAndFilterStore, visibleStore],
([$timeAndFilterStore, $visible]) => {
const { timeRange, where, hasTimeSeries } = $timeAndFilterStore;
const enabled =
$visible &&
!!showTotal &&
!!needsTotal &&
(!hasTimeSeries || (!!timeRange?.start && !!timeRange?.end)) &&
!!config.measure?.field;

Expand Down Expand Up @@ -205,7 +225,7 @@ export class CircularChartProvider {
where: combinedWhere,
sort: colorSort ? [colorSort] : undefined,
timeRange,
limit: limit?.toString(),
limit: queryLimit?.toString(),
},
{
query: {
Expand All @@ -215,7 +235,7 @@ export class CircularChartProvider {
},
);

if (showTotal && config.measure?.field) {
if (needsTotal && config.measure?.field) {
this.totalsValue = $totalQuery?.data?.data?.[0]?.[
config.measure?.field
] as number;
Expand Down Expand Up @@ -265,6 +285,49 @@ export class CircularChartProvider {
};
}

/**
* Transforms raw query data to apply "Other" grouping for pie/donut charts.
* Called by the data provider pipeline before data reaches the chart spec.
*/
transformData(
data: V1MetricsViewAggregationResponseDataItem[],
): V1MetricsViewAggregationResponseDataItem[] {
const config = get(this.spec);
const measureField = config.measure?.field;
const colorField = config.color?.field;

if (!measureField || !colorField) {
this.otherGroupResult = undefined;
return data;
}

const showOther = config.showOther !== false;
const userLimit = config.color?.limit;
const isExplicitLimit =
userLimit !== undefined && userLimit !== this.defaultColorLimit;

const result = computeOtherGrouping(data, measureField, colorField, {
limit: isExplicitLimit ? userLimit : undefined,
showOther,
grandTotal: this.totalsValue,
});

this.otherGroupResult = result;

if (result.hasOther) {
this.customColorValues = result.visibleData
.map((d) => String(d[colorField] ?? ""))
.filter((v) => v !== OTHER_SLICE_LABEL);
this.customColorValues.push(OTHER_SLICE_LABEL);
}

if (this.totalsValue === undefined && result.total > 0) {
this.totalsValue = result.total;
}

return result.visibleData;
}

getChartDomainValues(): ChartDomainValues {
const config = get(this.spec);
const result: Record<string, string[] | number[] | undefined> = {};
Expand All @@ -280,6 +343,10 @@ export class CircularChartProvider {
result["total"] = [this.totalsValue];
}

if (this.otherGroupResult) {
result["__otherTotal"] = [this.otherGroupResult.total];
}

return result;
}

Expand Down
Loading
Loading