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