From bf07ac926369ae8abe5d6b4fd70645faa85076d4 Mon Sep 17 00:00:00 2001 From: Romeo Van Snick <romeo@romeovansnick.be> Date: Wed, 14 Aug 2024 15:42:00 +0200 Subject: [PATCH] Moving average with offset clause (#46726) * Add type picker to comparison modal * Adjust offset input labels based of comarison type * Pass comparisonType to column picker * Add current period picker * Use deepCompareEffect to avoid rerender loops * Add moving average helpers * Add moving average clause to query * Prevent less than 1 period moving average from being created * Fix unit test for CompareAggregations * Use the ComparisonType type for the comparisonType arg * Hardcode the mapping for offset <-> moving-average column types * Convert column types in the comparisonType callback instead of a hook * Remove unused parameter * Automatically change to 2 when setting an invalid value for moving average periods * Use expression parts in test * Use named arguments for movingAverage * Fixup test for moving average offset input * Fix type issue * Pass on includePreviousPeriod to getAggregations --- frontend/src/metabase-lib/index.ts | 1 + frontend/src/metabase-lib/moving-average.ts | 163 +++ .../metabase-lib/moving-average.unit.spec.ts | 1285 +++++++++++++++++ .../CompareAggregations.tsx | 70 +- .../CompareAggregations.unit.spec.tsx | 33 +- .../components/ColumnPicker/ColumnPicker.tsx | 67 +- .../ComparisonTypePicker.tsx | 32 + .../components/ComparisonTypePicker/index.tsx | 1 + .../CurrentPeriodInput/CurrentPeriodInput.tsx | 26 + .../components/CurrentPeriodInput/index.ts | 1 + .../components/OffsetInput/OffsetInput.tsx | 30 +- .../components/OffsetInput/utils.ts | 26 +- .../CompareAggregations/components/index.ts | 2 + .../components/CompareAggregations/types.ts | 9 +- .../components/CompareAggregations/utils.ts | 39 + 15 files changed, 1731 insertions(+), 54 deletions(-) create mode 100644 frontend/src/metabase-lib/moving-average.ts create mode 100644 frontend/src/metabase-lib/moving-average.unit.spec.ts create mode 100644 frontend/src/metabase/query_builder/components/CompareAggregations/components/ComparisonTypePicker/ComparisonTypePicker.tsx create mode 100644 frontend/src/metabase/query_builder/components/CompareAggregations/components/ComparisonTypePicker/index.tsx create mode 100644 frontend/src/metabase/query_builder/components/CompareAggregations/components/CurrentPeriodInput/CurrentPeriodInput.tsx create mode 100644 frontend/src/metabase/query_builder/components/CompareAggregations/components/CurrentPeriodInput/index.ts diff --git a/frontend/src/metabase-lib/index.ts b/frontend/src/metabase-lib/index.ts index 49abee69e7a..d6cf12041b5 100644 --- a/frontend/src/metabase-lib/index.ts +++ b/frontend/src/metabase-lib/index.ts @@ -16,6 +16,7 @@ export * from "./join"; export * from "./limit"; export * from "./metadata"; export * from "./metrics"; +export * from "./moving-average"; export * from "./native"; export * from "./offset"; export * from "./order_by"; diff --git a/frontend/src/metabase-lib/moving-average.ts b/frontend/src/metabase-lib/moving-average.ts new file mode 100644 index 00000000000..d73b107dd07 --- /dev/null +++ b/frontend/src/metabase-lib/moving-average.ts @@ -0,0 +1,163 @@ +import { t } from "ttag"; + +import { breakoutColumn, breakouts } from "./breakout"; +import { isTemporal } from "./column_types"; +import { expressionClause, withExpressionName } from "./expression"; +import { describeTemporalUnit, displayInfo } from "./metadata"; +import { temporalBucket } from "./temporal_bucket"; +import type { AggregationClause, ExpressionClause, Query } from "./types"; + +function movingAverage({ + clause, + offset, + start, +}: { + clause: AggregationClause | ExpressionClause; + offset: number; + start: number; +}) { + const clauses = []; + for (let i = 0; i > offset; i--) { + const period = start + i; + + if (period === 0) { + clauses.push(clause); + } else { + clauses.push(expressionClause("offset", [clause, period])); + } + } + + const newClause = expressionClause("/", [ + expressionClause("+", clauses), + Math.abs(offset), + ]); + + return newClause; +} + +export function movingAverageClause({ + query, + stageIndex, + clause, + offset, + includeCurrentPeriod, +}: { + query: Query; + stageIndex: number; + clause: AggregationClause | ExpressionClause; + offset: number; + includeCurrentPeriod: boolean; +}): ExpressionClause { + const start = includeCurrentPeriod ? 0 : -1; + const newClause = movingAverage({ clause, offset, start }); + + const newName = getMovingAverageClauseName({ + query, + stageIndex, + clause, + offset, + prefix: "", + }); + + return withExpressionName(newClause, newName); +} + +export function diffMovingAverageClause({ + query, + stageIndex, + clause, + offset, + includeCurrentPeriod, +}: { + query: Query; + stageIndex: number; + clause: AggregationClause | ExpressionClause; + offset: number; + includeCurrentPeriod: boolean; +}): ExpressionClause { + const start = includeCurrentPeriod ? 0 : -1; + const average = movingAverage({ clause, offset, start }); + const newClause = expressionClause("-", [clause, average]); + + const newName = getMovingAverageClauseName({ + query, + stageIndex, + clause, + offset, + prefix: t`vs `, + }); + + return withExpressionName(newClause, newName); +} + +export function percentDiffMovingAverageClause({ + query, + stageIndex, + clause, + offset, + includeCurrentPeriod, +}: { + query: Query; + stageIndex: number; + clause: AggregationClause | ExpressionClause; + offset: number; + includeCurrentPeriod: boolean; +}): ExpressionClause { + const start = includeCurrentPeriod ? 0 : -1; + const average = movingAverage({ clause, offset, start }); + const newClause = expressionClause("/", [clause, average]); + + const newName = getMovingAverageClauseName({ + query, + stageIndex, + clause, + offset, + prefix: t`% vs `, + }); + + return withExpressionName(newClause, newName); +} + +function getMovingAverageClauseName({ + query, + stageIndex, + clause, + offset, + prefix = "", +}: { + query: Query; + stageIndex: number; + clause: AggregationClause | ExpressionClause; + offset: number; + prefix: string; +}) { + if (offset >= 0) { + throw new Error( + "non-negative offset values aren't supported in 'getAverageClauseName'", + ); + } + const absoluteOffset = Math.abs(offset); + const { displayName } = displayInfo(query, stageIndex, clause); + const firstBreakout = breakouts(query, stageIndex)[0]; + + if (!firstBreakout) { + return t`${displayName} (${prefix}${absoluteOffset}-period moving average)`; + } + + const firstBreakoutColumn = breakoutColumn(query, stageIndex, firstBreakout); + + if (!isTemporal(firstBreakoutColumn)) { + return t`${displayName} (${prefix}${absoluteOffset}-row moving average)`; + } + + const bucket = temporalBucket(firstBreakout); + + if (!bucket) { + return t`${displayName} (${prefix}${absoluteOffset}-period moving average)`; + } + + const bucketInfo = displayInfo(query, stageIndex, bucket); + const period = describeTemporalUnit(bucketInfo.shortName, 1).toLowerCase(); + + return t`${displayName} (${prefix}${absoluteOffset}-${period} moving average)`; +} diff --git a/frontend/src/metabase-lib/moving-average.unit.spec.ts b/frontend/src/metabase-lib/moving-average.unit.spec.ts new file mode 100644 index 00000000000..d6bcb29d1af --- /dev/null +++ b/frontend/src/metabase-lib/moving-average.unit.spec.ts @@ -0,0 +1,1285 @@ +import { ORDERS_ID } from "metabase-types/api/mocks/presets"; + +import { aggregate, aggregations } from "./aggregation"; +import { expressionParts } from "./expression"; +import { displayInfo } from "./metadata"; +import { + diffMovingAverageClause, + movingAverageClause, + percentDiffMovingAverageClause, +} from "./moving-average"; +import { toLegacyQuery } from "./query"; +import { SAMPLE_DATABASE, createQueryWithClauses } from "./test-helpers"; +import type { Query } from "./types"; + +const stageIndex = -1; + +const queryNoBreakout = createQueryWithClauses({ + aggregations: [{ operatorName: "count" }], +}); + +const queryDateBreakoutNoBinning = createQueryWithClauses({ + query: queryNoBreakout, + breakouts: [ + { + columnName: "CREATED_AT", + tableName: "ORDERS", + }, + ], +}); + +const queryDateBreakoutBinning = createQueryWithClauses({ + query: queryNoBreakout, + breakouts: [ + { + columnName: "CREATED_AT", + tableName: "ORDERS", + temporalBucketName: "Month", + }, + ], +}); + +const queryCategoryBreakout = createQueryWithClauses({ + query: queryNoBreakout, + breakouts: [ + { + columnName: "CATEGORY", + tableName: "PRODUCTS", + }, + ], +}); + +describe("movingAverageClause", () => { + const setup = ( + query: Query, + offset: number, + includeCurrentPeriod: boolean = false, + ) => { + const [aggregation] = aggregations(query, stageIndex); + const clause = movingAverageClause({ + query, + stageIndex, + clause: aggregation, + offset, + includeCurrentPeriod, + }); + const finalQuery = aggregate(query, stageIndex, clause); + + return { + clause, + query: finalQuery, + }; + }; + + describe("includeCurrentPeriod = false", () => { + const includeCurrentPeriod = false; + + describe("offset = -2", () => { + const offset = -2; + + describe("no breakout", () => { + const { query, clause } = setup( + queryNoBreakout, + offset, + includeCurrentPeriod, + ); + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (2-period moving average)"); + }); + + it("produces correct aggregation clause", () => { + expect(expressionParts(query, -1, clause)).toEqual({ + operator: "/", + args: [ + { + operator: "+", + args: [ + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -1, + ], + options: {}, + }, + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -2, + ], + options: {}, + }, + ], + options: {}, + }, + 2, + ], + options: {}, + }); + }); + }); + + describe("breakout on binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (2-month moving average)"); + }); + }); + + describe("breakout on non-binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutNoBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (2-period moving average)"); + }); + }); + + describe("breakout on non-datetime column", () => { + const { query, clause } = setup( + queryCategoryBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (2-row moving average)"); + }); + }); + }); + + describe("offset < -2", () => { + const offset = -3; + + describe("no breakout", () => { + const { query, clause } = setup( + queryNoBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (3-period moving average)"); + }); + }); + + describe("breakout on binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (3-month moving average)"); + }); + }); + + describe("breakout on non-binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutNoBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (3-period moving average)"); + }); + }); + + describe("breakout on non-datetime column", () => { + const { query, clause } = setup( + queryCategoryBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (3-row moving average)"); + }); + }); + }); + }); + + describe("includeCurrentPeriod = true", () => { + const includeCurrentPeriod = true; + + describe("offset = -2", () => { + const offset = -2; + + describe("no breakout", () => { + const { query, clause } = setup( + queryNoBreakout, + offset, + includeCurrentPeriod, + ); + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (2-period moving average)"); + }); + + it("produces correct aggregation clause", () => { + expect(expressionParts(query, -1, clause)).toEqual({ + operator: "/", + args: [ + { + operator: "+", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -1, + ], + options: {}, + }, + ], + options: {}, + }, + 2, + ], + options: {}, + }); + }); + }); + + describe("breakout on binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (2-month moving average)"); + }); + }); + + describe("breakout on non-binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutNoBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (2-period moving average)"); + }); + }); + + describe("breakout on non-datetime column", () => { + const { query, clause } = setup( + queryCategoryBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (2-row moving average)"); + }); + }); + }); + + describe("offset < -2", () => { + const offset = -3; + + describe("no breakout", () => { + const { query, clause } = setup( + queryNoBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (3-period moving average)"); + }); + }); + + describe("breakout on binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (3-month moving average)"); + }); + }); + + describe("breakout on non-binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutNoBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (3-period moving average)"); + }); + }); + + describe("breakout on non-datetime column", () => { + const { query, clause } = setup( + queryCategoryBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (3-row moving average)"); + }); + }); + }); + }); +}); + +describe("diffMovingAverageClause", () => { + const setup = ( + query: Query, + offset: number, + includeCurrentPeriod: boolean, + ) => { + const [clause] = aggregations(query, stageIndex); + const offsettedClause = diffMovingAverageClause({ + query, + stageIndex, + clause, + offset, + includeCurrentPeriod, + }); + const finalQuery = aggregate(query, stageIndex, offsettedClause); + + return { + clause: offsettedClause, + query: finalQuery, + }; + }; + + describe("includeCurrentPeriod = false", () => { + const includeCurrentPeriod = false; + + describe("offset = -2", () => { + const offset = -2; + + describe("no breakout", () => { + const { query, clause } = setup( + queryNoBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 2-period moving average)"); + }); + + it("produces correct aggregation clause", () => { + expect(expressionParts(query, -1, clause)).toEqual({ + operator: "-", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + { + operator: "/", + args: [ + { + operator: "+", + args: [ + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -1, + ], + options: {}, + }, + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -2, + ], + options: {}, + }, + ], + options: {}, + }, + 2, + ], + options: {}, + }, + ], + options: {}, + }); + }); + }); + + describe("breakout on binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 2-month moving average)"); + }); + }); + + describe("breakout on non-binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutNoBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 2-period moving average)"); + }); + }); + + describe("breakout on non-datetime column", () => { + const { query, clause } = setup( + queryCategoryBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 2-row moving average)"); + }); + }); + }); + + describe("offset < -2", () => { + const offset = -3; + + describe("no breakout", () => { + const { query, clause } = setup( + queryNoBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 3-period moving average)"); + }); + + it("produces correct aggregation clause", () => { + expect(expressionParts(query, -1, clause)).toEqual({ + operator: "-", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + { + operator: "/", + args: [ + { + operator: "+", + args: [ + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -1, + ], + options: {}, + }, + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -2, + ], + options: {}, + }, + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -3, + ], + options: {}, + }, + ], + options: {}, + }, + 3, + ], + options: {}, + }, + ], + options: {}, + }); + }); + }); + + describe("breakout on binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 3-month moving average)"); + }); + }); + + describe("breakout on non-binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutNoBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 3-period moving average)"); + }); + }); + + describe("breakout on non-datetime column", () => { + const { query, clause } = setup( + queryCategoryBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 3-row moving average)"); + }); + }); + }); + }); + + describe("includeCurrentPeriod = true", () => { + const includeCurrentPeriod = true; + + describe("offset = -2", () => { + const offset = -2; + + describe("no breakout", () => { + const { query, clause } = setup( + queryNoBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 2-period moving average)"); + }); + + it("produces correct aggregation clause", () => { + expect(expressionParts(query, -1, clause)).toEqual({ + operator: "-", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + { + operator: "/", + args: [ + { + operator: "+", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -1, + ], + options: {}, + }, + ], + options: {}, + }, + 2, + ], + options: {}, + }, + ], + options: {}, + }); + }); + }); + + describe("breakout on binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 2-month moving average)"); + }); + }); + + describe("breakout on non-binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutNoBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 2-period moving average)"); + }); + }); + + describe("breakout on non-datetime column", () => { + const { query, clause } = setup( + queryCategoryBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 2-row moving average)"); + }); + }); + }); + + describe("offset < -2", () => { + const offset = -3; + + describe("no breakout", () => { + const { query, clause } = setup( + queryNoBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 3-period moving average)"); + }); + + it("produces correct aggregation clause", () => { + expect(expressionParts(query, -1, clause)).toEqual({ + operator: "-", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + { + operator: "/", + args: [ + { + operator: "+", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -1, + ], + options: {}, + }, + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -2, + ], + options: {}, + }, + ], + options: {}, + }, + 3, + ], + options: {}, + }, + ], + options: {}, + }); + }); + }); + + describe("breakout on binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 3-month moving average)"); + }); + }); + + describe("breakout on non-binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutNoBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 3-period moving average)"); + }); + }); + + describe("breakout on non-datetime column", () => { + const { query, clause } = setup( + queryCategoryBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (vs 3-row moving average)"); + }); + }); + }); + }); +}); + +describe("percentDiffMovingAverageClause", () => { + const setup = ( + query: Query, + offset: number, + includeCurrentPeriod: boolean, + ) => { + const [clause] = aggregations(query, stageIndex); + const offsettedClause = percentDiffMovingAverageClause({ + query, + stageIndex, + clause, + offset, + includeCurrentPeriod, + }); + const finalQuery = aggregate(query, stageIndex, offsettedClause); + + return { + clause: offsettedClause, + query: finalQuery, + }; + }; + + describe("includeCurrentPeriod = false", () => { + const includeCurrentPeriod = false; + + describe("offset = -2", () => { + const offset = -2; + + describe("no breakout", () => { + const { query, clause } = setup( + queryNoBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 2-period moving average)"); + }); + + it("produces correct aggregation clause", () => { + expect(expressionParts(query, -1, clause)).toEqual({ + operator: "/", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + { + operator: "/", + args: [ + { + operator: "+", + args: [ + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -1, + ], + options: {}, + }, + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -2, + ], + options: {}, + }, + ], + options: {}, + }, + 2, + ], + options: {}, + }, + ], + options: {}, + }); + }); + }); + + describe("breakout on binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 2-month moving average)"); + }); + }); + + describe("breakout on non-binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutNoBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 2-period moving average)"); + }); + }); + + describe("breakout on non-datetime column", () => { + const { query, clause } = setup( + queryCategoryBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 2-row moving average)"); + }); + }); + }); + + describe("offset < -2", () => { + const offset = -3; + + describe("no breakout", () => { + const { query, clause } = setup( + queryNoBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 3-period moving average)"); + }); + + it("produces correct aggregation clause", () => { + expect(toLegacyQuery(query)).toMatchObject({ + database: SAMPLE_DATABASE.id, + query: { + aggregation: [ + ["count"], + [ + "aggregation-options", + [ + "/", + ["count"], + [ + "/", + [ + "+", + ["offset", expect.anything(), ["count"], -1], + ["offset", expect.anything(), ["count"], -2], + ["offset", expect.anything(), ["count"], -3], + ], + 3, + ], + ], + { + name: "Count (% vs 3-period moving average)", + "display-name": "Count (% vs 3-period moving average)", + }, + ], + ], + "source-table": ORDERS_ID, + }, + type: "query", + }); + }); + }); + + describe("breakout on binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 3-month moving average)"); + }); + }); + + describe("breakout on non-binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutNoBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 3-period moving average)"); + }); + }); + + describe("breakout on non-datetime column", () => { + const { query, clause } = setup( + queryCategoryBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 3-row moving average)"); + }); + }); + }); + }); + + describe("includeCurrentPeriod = true", () => { + const includeCurrentPeriod = true; + + describe("offset = -2", () => { + const offset = -2; + + describe("no breakout", () => { + const { query, clause } = setup( + queryNoBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 2-period moving average)"); + }); + + it("produces correct aggregation clause", () => { + expect(expressionParts(query, -1, clause)).toEqual({ + operator: "/", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + { + operator: "/", + args: [ + { + operator: "+", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -1, + ], + options: {}, + }, + ], + options: {}, + }, + 2, + ], + options: {}, + }, + ], + options: {}, + }); + }); + }); + + describe("breakout on binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 2-month moving average)"); + }); + }); + + describe("breakout on non-binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutNoBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 2-period moving average)"); + }); + }); + + describe("breakout on non-datetime column", () => { + const { query, clause } = setup( + queryCategoryBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 2-row moving average)"); + }); + }); + }); + + describe("offset < -2", () => { + const offset = -3; + + describe("no breakout", () => { + const { query, clause } = setup( + queryNoBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 3-period moving average)"); + }); + + it("produces correct aggregation clause", () => { + expect(expressionParts(query, -1, clause)).toEqual({ + operator: "/", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + { + operator: "/", + args: [ + { + operator: "+", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -1, + ], + options: {}, + }, + { + operator: "offset", + args: [ + { + operator: "count", + args: [], + options: {}, + }, + -2, + ], + options: {}, + }, + ], + options: {}, + }, + 3, + ], + options: {}, + }, + ], + options: {}, + }); + }); + }); + + describe("breakout on binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 3-month moving average)"); + }); + }); + + describe("breakout on non-binned datetime column", () => { + const { query, clause } = setup( + queryDateBreakoutNoBinning, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 3-period moving average)"); + }); + }); + + describe("breakout on non-datetime column", () => { + const { query, clause } = setup( + queryCategoryBreakout, + offset, + includeCurrentPeriod, + ); + + it("produces correct aggregation name", () => { + const info = displayInfo(query, stageIndex, clause); + expect(info.displayName).toBe("Count (% vs 3-row moving average)"); + }); + }); + }); + }); +}); diff --git a/frontend/src/metabase/query_builder/components/CompareAggregations/CompareAggregations.tsx b/frontend/src/metabase/query_builder/components/CompareAggregations/CompareAggregations.tsx index c8e57fa6120..35d0cb19368 100644 --- a/frontend/src/metabase/query_builder/components/CompareAggregations/CompareAggregations.tsx +++ b/frontend/src/metabase/query_builder/components/CompareAggregations/CompareAggregations.tsx @@ -1,5 +1,5 @@ import type { FormEvent } from "react"; -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { t } from "ttag"; import { Box, Button, Flex, Stack } from "metabase/ui"; @@ -11,8 +11,10 @@ import { ColumnPicker, OffsetInput, ReferenceAggregationPicker, + ComparisonTypePicker, + CurrentPerionInput, } from "./components"; -import type { ColumnType } from "./types"; +import type { ColumnType, ComparisonType } from "./types"; import { canSubmit, getBreakout, @@ -29,6 +31,7 @@ interface Props { } const DEFAULT_OFFSET = 1; +const DEFAULT_COMPARISON_TYPE = "offset"; const DEFAULT_COLUMNS: ColumnType[] = ["offset", "percent-diff-offset"]; const STEP_1_WIDTH = 378; const STEP_2_WIDTH = 472; @@ -51,6 +54,10 @@ export const CompareAggregations = ({ const [offset, setOffset] = useState<number | "">(DEFAULT_OFFSET); const [columns, setColumns] = useState<ColumnType[]>(DEFAULT_COLUMNS); + const [comparisonType, setComparisonType] = useState<ComparisonType>( + DEFAULT_COMPARISON_TYPE, + ); + const [includeCurrentPeriod, setIncludeCurrentPeriod] = useState(false); const width = aggregation ? STEP_2_WIDTH : STEP_1_WIDTH; const title = useMemo( @@ -58,6 +65,17 @@ export const CompareAggregations = ({ [query, stageIndex, aggregation], ); + const handleComparisonTypeChange = useCallback( + (comparisonType: ComparisonType) => { + setComparisonType(comparisonType); + setColumns(convertColumnTypes(columns, comparisonType)); + if (comparisonType === "moving-average" && offset !== "" && offset <= 1) { + setOffset(2); + } + }, + [offset, columns], + ); + const handleBack = () => { if (hasManyAggregations && aggregation) { setAggregation(undefined); @@ -80,6 +98,7 @@ export const CompareAggregations = ({ offset, columns, columnAndBucket, + includeCurrentPeriod, ); if (!next) { @@ -106,14 +125,31 @@ export const CompareAggregations = ({ <form onSubmit={handleSubmit}> <Stack p="lg" spacing="xl"> <Stack spacing="md"> + <ComparisonTypePicker + value={comparisonType} + onChange={handleComparisonTypeChange} + /> + <OffsetInput query={query} stageIndex={stageIndex} + comparisonType={comparisonType} value={offset} onChange={setOffset} /> - <ColumnPicker value={columns} onChange={setColumns} /> + {comparisonType === "moving-average" && ( + <CurrentPerionInput + value={includeCurrentPeriod} + onChange={setIncludeCurrentPeriod} + /> + )} + + <ColumnPicker + value={columns} + onChange={setColumns} + comparisonType={comparisonType} + /> </Stack> <Flex justify="flex-end"> @@ -129,3 +165,31 @@ export const CompareAggregations = ({ </Box> ); }; + +const comparisonTypeMapping = { + offset: { + offset: "offset", + "diff-offset": "diff-offset", + "percent-diff-offset": "percent-diff-offset", + "moving-average": "offset", + "diff-moving-average": "diff-offset", + "percent-diff-moving-average": "percent-diff-offset", + }, + "moving-average": { + offset: "moving-average", + "diff-offset": "diff-moving-average", + "percent-diff-offset": "percent-diff-moving-average", + "moving-average": "moving-average", + "diff-moving-average": "diff-moving-average", + "percent-diff-moving-average": "percent-diff-moving-average", + }, +} as const; + +function convertColumnTypes( + columnTypes: ColumnType[], + comparisonType: ComparisonType, +): ColumnType[] { + return columnTypes.map( + columnType => comparisonTypeMapping[comparisonType][columnType], + ); +} diff --git a/frontend/src/metabase/query_builder/components/CompareAggregations/CompareAggregations.unit.spec.tsx b/frontend/src/metabase/query_builder/components/CompareAggregations/CompareAggregations.unit.spec.tsx index ef92cb39f77..3e69b238a76 100644 --- a/frontend/src/metabase/query_builder/components/CompareAggregations/CompareAggregations.unit.spec.tsx +++ b/frontend/src/metabase/query_builder/components/CompareAggregations/CompareAggregations.unit.spec.tsx @@ -120,7 +120,7 @@ describe("CompareAggregations", () => { it("does not allow negative values", async () => { setup({ query: queryWithCountAggregation }); - const input = screen.getByLabelText("Previous period"); + const input = screen.getByLabelText("Compare to"); await userEvent.clear(input); await userEvent.type(input, "-5"); @@ -132,7 +132,7 @@ describe("CompareAggregations", () => { it("does not allow non-integer values", async () => { setup({ query: queryWithCountAggregation }); - const input = screen.getByLabelText("Previous period"); + const input = screen.getByLabelText("Compare to"); await userEvent.clear(input); await userEvent.type(input, "1.234"); @@ -146,7 +146,7 @@ describe("CompareAggregations", () => { it("is submittable by default", () => { setup({ query: queryWithCountAggregation }); - expect(screen.getByLabelText("Previous period")).toHaveValue(1); + expect(screen.getByLabelText("Compare to")).toHaveValue(1); expect(screen.getByText("Previous value")).toBeInTheDocument(); expect(screen.getByText("Percentage difference")).toBeInTheDocument(); expect(screen.queryByText("Value difference")).not.toBeInTheDocument(); @@ -156,7 +156,7 @@ describe("CompareAggregations", () => { it("disables the submit button when offset input is empty", async () => { setup({ query: queryWithCountAggregation }); - const input = screen.getByLabelText("Previous period"); + const input = screen.getByLabelText("Compare to"); await userEvent.clear(input); @@ -199,4 +199,29 @@ describe("CompareAggregations", () => { expect(aggregations).toHaveLength(2); }); }); + + describe("moving average", () => { + it("allows switching to moving averages", async () => { + setup({ query: queryWithCountAggregation }); + expect(screen.getByText("Moving average")).toBeInTheDocument(); + await userEvent.click(screen.getByText("Moving average")); + + expect(screen.getByText("Include current period")).toBeInTheDocument(); + }); + + it("should not allow setting a moving average for less than 2 periods", async () => { + setup({ query: queryWithCountAggregation }); + + expect(screen.getByText("Moving average")).toBeInTheDocument(); + await userEvent.click(screen.getByText("Moving average")); + + const input = screen.getByLabelText("Compare to"); + expect(input).toHaveValue(2); + await userEvent.clear(input); + await userEvent.type(input, "1"); + await userEvent.tab(); + + expect(input).toHaveValue(2); + }); + }); }); diff --git a/frontend/src/metabase/query_builder/components/CompareAggregations/components/ColumnPicker/ColumnPicker.tsx b/frontend/src/metabase/query_builder/components/CompareAggregations/components/ColumnPicker/ColumnPicker.tsx index 9ecde7e521e..a520a30b10b 100644 --- a/frontend/src/metabase/query_builder/components/CompareAggregations/components/ColumnPicker/ColumnPicker.tsx +++ b/frontend/src/metabase/query_builder/components/CompareAggregations/components/ColumnPicker/ColumnPicker.tsx @@ -4,7 +4,7 @@ import { t } from "ttag"; import { Checkbox, Flex, MultiSelect, Text } from "metabase/ui"; -import type { ColumnType } from "../../types"; +import type { ColumnType, ComparisonType } from "../../types"; import S from "./ColumnPicker.module.css"; @@ -15,29 +15,12 @@ interface ItemType { } interface Props { + comparisonType: ComparisonType; value: ColumnType[]; onChange: (value: ColumnType[]) => void; } -const COLUMN_OPTIONS: ItemType[] = [ - { - example: "1826, 3004", - label: t`Previous value`, - value: "offset", - }, - { - example: "+2.3%, -0.1%", - label: t`Percentage difference`, - value: "percent-diff-offset", - }, - { - example: "+42, -3", - label: t`Value difference`, - value: "diff-offset", - }, -]; - -export const ColumnPicker = ({ value, onChange }: Props) => { +export const ColumnPicker = ({ value, onChange, comparisonType }: Props) => { const handleChange = useCallback( (values: string[]) => { onChange(values as ColumnType[]); @@ -47,7 +30,7 @@ export const ColumnPicker = ({ value, onChange }: Props) => { return ( <MultiSelect - data={COLUMN_OPTIONS} + data={getColumnOptions(comparisonType)} data-testid="column-picker" disableSelectedItemFiltering itemComponent={Item} @@ -94,3 +77,45 @@ const Item = forwardRef< </div> ); }); + +function getColumnOptions(comparisonType: string): ItemType[] { + if (comparisonType === "offset") { + return [ + { + example: "1826, 3004", + label: t`Previous value`, + value: "offset", + }, + { + example: "+2.3%, -0.1%", + label: t`Percentage difference`, + value: "percent-diff-offset", + }, + { + example: "+42, -3", + label: t`Value difference`, + value: "diff-offset", + }, + ]; + } + if (comparisonType === "moving-average") { + return [ + { + example: "1826, 3004", + label: t`Moving average value`, + value: "moving-average", + }, + { + example: "+2.3%, -0.1%", + label: t`Percentage difference with moving average`, + value: "percent-diff-moving-average", + }, + { + example: "+42, -3", + label: t`Value difference with moving average`, + value: "diff-moving-average", + }, + ]; + } + return []; +} diff --git a/frontend/src/metabase/query_builder/components/CompareAggregations/components/ComparisonTypePicker/ComparisonTypePicker.tsx b/frontend/src/metabase/query_builder/components/CompareAggregations/components/ComparisonTypePicker/ComparisonTypePicker.tsx new file mode 100644 index 00000000000..b90b151e456 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/CompareAggregations/components/ComparisonTypePicker/ComparisonTypePicker.tsx @@ -0,0 +1,32 @@ +import { t } from "ttag"; + +import { Input, Button, Flex, Stack } from "metabase/ui"; + +import type { ComparisonType } from "../../types"; + +type Props = { + value: ComparisonType; + onChange: (value: ComparisonType) => void; +}; + +export function ComparisonTypePicker({ onChange, value }: Props) { + return ( + <Stack spacing="sm"> + <Input.Label>{t`How to compare`}</Input.Label> + <Flex gap="sm"> + <Button + variant={value === "offset" ? "filled" : "default"} + radius="xl" + p="sm" + onClick={() => onChange("offset")} + >{t`Compare values`}</Button> + <Button + variant={value === "moving-average" ? "filled" : "default"} + radius="xl" + p="sm" + onClick={() => onChange("moving-average")} + >{t`Moving average`}</Button> + </Flex> + </Stack> + ); +} diff --git a/frontend/src/metabase/query_builder/components/CompareAggregations/components/ComparisonTypePicker/index.tsx b/frontend/src/metabase/query_builder/components/CompareAggregations/components/ComparisonTypePicker/index.tsx new file mode 100644 index 00000000000..eec0fca1fa9 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/CompareAggregations/components/ComparisonTypePicker/index.tsx @@ -0,0 +1 @@ +export * from "./ComparisonTypePicker"; diff --git a/frontend/src/metabase/query_builder/components/CompareAggregations/components/CurrentPeriodInput/CurrentPeriodInput.tsx b/frontend/src/metabase/query_builder/components/CompareAggregations/components/CurrentPeriodInput/CurrentPeriodInput.tsx new file mode 100644 index 00000000000..8f2ba064e22 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/CompareAggregations/components/CurrentPeriodInput/CurrentPeriodInput.tsx @@ -0,0 +1,26 @@ +import { useCallback } from "react"; +import { t } from "ttag"; + +import { Checkbox } from "metabase/ui"; + +type Props = { + value: boolean; + onChange: (value: boolean) => void; +}; + +export function CurrentPerionInput({ value, onChange }: Props) { + const handleChange = useCallback( + (evt: React.ChangeEvent<HTMLInputElement>) => { + onChange(evt.target.checked); + }, + [onChange], + ); + + return ( + <Checkbox + checked={value} + onChange={handleChange} + label={t`Include current period`} + /> + ); +} diff --git a/frontend/src/metabase/query_builder/components/CompareAggregations/components/CurrentPeriodInput/index.ts b/frontend/src/metabase/query_builder/components/CompareAggregations/components/CurrentPeriodInput/index.ts new file mode 100644 index 00000000000..e8f8c2c5044 --- /dev/null +++ b/frontend/src/metabase/query_builder/components/CompareAggregations/components/CurrentPeriodInput/index.ts @@ -0,0 +1 @@ +export * from "./CurrentPeriodInput"; diff --git a/frontend/src/metabase/query_builder/components/CompareAggregations/components/OffsetInput/OffsetInput.tsx b/frontend/src/metabase/query_builder/components/CompareAggregations/components/OffsetInput/OffsetInput.tsx index 24b90fec8d2..c5eef5aff93 100644 --- a/frontend/src/metabase/query_builder/components/CompareAggregations/components/OffsetInput/OffsetInput.tsx +++ b/frontend/src/metabase/query_builder/components/CompareAggregations/components/OffsetInput/OffsetInput.tsx @@ -1,31 +1,45 @@ import { useCallback, useMemo } from "react"; +import { t } from "ttag"; import { Flex, NumberInput, Text } from "metabase/ui"; import type * as Lib from "metabase-lib"; +import type { ComparisonType } from "../../types"; + import S from "./OffsetInput.module.css"; -import { getHelp, getLabel } from "./utils"; +import { getHelp } from "./utils"; interface Props { query: Lib.Query; stageIndex: number; value: number | ""; onChange: (value: number | "") => void; + comparisonType: ComparisonType; } -export const OffsetInput = ({ query, stageIndex, value, onChange }: Props) => { - const label = useMemo(() => getLabel(query, stageIndex), [query, stageIndex]); - const help = useMemo(() => getHelp(query, stageIndex), [query, stageIndex]); +export const OffsetInput = ({ + query, + stageIndex, + value, + onChange, + comparisonType, +}: Props) => { + const help = useMemo( + () => getHelp(query, stageIndex, comparisonType), + [query, stageIndex, comparisonType], + ); + + const minimum = comparisonType === "offset" ? 1 : 2; const handleChange = useCallback( (value: number | "") => { if (typeof value === "number") { - onChange(Math.floor(Math.max(Math.abs(value), 1))); + onChange(Math.floor(Math.max(Math.abs(value), minimum))); } else { onChange(value); } }, - [onChange], + [onChange, minimum], ); return ( @@ -35,8 +49,8 @@ export const OffsetInput = ({ query, stageIndex, value, onChange }: Props) => { input: S.input, wrapper: S.wrapper, }} - label={label} - min={1} + label={t`Compare to`} + min={minimum} precision={0} size="md" step={1} diff --git a/frontend/src/metabase/query_builder/components/CompareAggregations/components/OffsetInput/utils.ts b/frontend/src/metabase/query_builder/components/CompareAggregations/components/OffsetInput/utils.ts index be4e2814ff2..1bff83d8d2f 100644 --- a/frontend/src/metabase/query_builder/components/CompareAggregations/components/OffsetInput/utils.ts +++ b/frontend/src/metabase/query_builder/components/CompareAggregations/components/OffsetInput/utils.ts @@ -2,27 +2,19 @@ import { t } from "ttag"; import * as Lib from "metabase-lib"; -export const getLabel = (query: Lib.Query, stageIndex: number): string => { - const firstBreakout = Lib.breakouts(query, stageIndex)[0]; +import type { ComparisonType } from "../../types"; - if (firstBreakout) { - const firstBreakoutColumn = Lib.breakoutColumn( - query, - stageIndex, - firstBreakout, - ); +export const getHelp = ( + query: Lib.Query, + stageIndex: number, + comparisonType: ComparisonType, +): string => { + const firstBreakout = Lib.breakouts(query, stageIndex)[0]; - if (!Lib.isTemporal(firstBreakoutColumn)) { - return t`Row for comparison`; - } + if (comparisonType === "moving-average") { + return t`period moving average`; } - return t`Previous period`; -}; - -export const getHelp = (query: Lib.Query, stageIndex: number): string => { - const firstBreakout = Lib.breakouts(query, stageIndex)[0]; - if (!firstBreakout) { return t`periods ago based on grouping`; } diff --git a/frontend/src/metabase/query_builder/components/CompareAggregations/components/index.ts b/frontend/src/metabase/query_builder/components/CompareAggregations/components/index.ts index f9448e4a379..6e269b55471 100644 --- a/frontend/src/metabase/query_builder/components/CompareAggregations/components/index.ts +++ b/frontend/src/metabase/query_builder/components/CompareAggregations/components/index.ts @@ -1,3 +1,5 @@ export * from "./ColumnPicker"; export * from "./OffsetInput"; export * from "./ReferenceAggregationPicker"; +export * from "./ComparisonTypePicker"; +export * from "./CurrentPeriodInput"; diff --git a/frontend/src/metabase/query_builder/components/CompareAggregations/types.ts b/frontend/src/metabase/query_builder/components/CompareAggregations/types.ts index 2577e73cd5e..f7de041cfae 100644 --- a/frontend/src/metabase/query_builder/components/CompareAggregations/types.ts +++ b/frontend/src/metabase/query_builder/components/CompareAggregations/types.ts @@ -1 +1,8 @@ -export type ColumnType = "offset" | "diff-offset" | "percent-diff-offset"; +export type ColumnType = + | "offset" + | "diff-offset" + | "percent-diff-offset" + | "moving-average" + | "diff-moving-average" + | "percent-diff-moving-average"; +export type ComparisonType = "offset" | "moving-average"; diff --git a/frontend/src/metabase/query_builder/components/CompareAggregations/utils.ts b/frontend/src/metabase/query_builder/components/CompareAggregations/utils.ts index a44a07c1016..c4fca902b07 100644 --- a/frontend/src/metabase/query_builder/components/CompareAggregations/utils.ts +++ b/frontend/src/metabase/query_builder/components/CompareAggregations/utils.ts @@ -58,6 +58,7 @@ export const getAggregations = ( aggregation: Lib.AggregationClause | Lib.ExpressionClause, columns: ColumnType[], offset: number, + includeCurrentPeriod: boolean, ): Lib.ExpressionClause[] => { const aggregations: Lib.ExpressionClause[] = []; @@ -79,6 +80,42 @@ export const getAggregations = ( ); } + if (columns.includes("moving-average")) { + aggregations.push( + Lib.movingAverageClause({ + query, + stageIndex, + clause: aggregation, + offset: -offset, + includeCurrentPeriod, + }), + ); + } + + if (columns.includes("diff-moving-average")) { + aggregations.push( + Lib.diffMovingAverageClause({ + query, + stageIndex, + clause: aggregation, + offset: -offset, + includeCurrentPeriod, + }), + ); + } + + if (columns.includes("percent-diff-moving-average")) { + aggregations.push( + Lib.percentDiffMovingAverageClause({ + query, + stageIndex, + clause: aggregation, + offset: -offset, + includeCurrentPeriod, + }), + ); + } + return aggregations; }; @@ -149,6 +186,7 @@ export function updateQueryWithCompareOffsetAggregations( offset: "" | number, columns: ColumnType[], columnAndBucket: BreakoutColumnAndBucket, + includeCurrentPeriod: boolean, ): UpdatedQuery | null { if (!aggregation || offset === "") { return null; @@ -189,6 +227,7 @@ export function updateQueryWithCompareOffsetAggregations( aggregation, columns, offset, + includeCurrentPeriod, ); nextQuery = aggregations.reduce( -- GitLab