This is an automated email from the ASF dual-hosted git repository.

elizabeth pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new a09e5557bc feat: Period over Period Big Number comparison chart 
(#26908)
a09e5557bc is described below

commit a09e5557bc8b40e46495b9473959327118dfaacf
Author: Elizabeth Thompson <[email protected]>
AuthorDate: Wed Jan 31 15:44:25 2024 -0800

    feat: Period over Period Big Number comparison chart (#26908)
    
    Co-authored-by: Fernando <[email protected]>
    Co-authored-by: Antonio Rivero <[email protected]>
---
 RESOURCES/FEATURE_FLAGS.md                         |   1 +
 superset-frontend/package-lock.json                |  44 +++
 superset-frontend/package.json                     |   1 +
 .../superset-ui-core/src/utils/featureFlags.ts     |   1 +
 .../plugin-chart-period-over-period-kpi/README.md  |  87 ++++++
 .../package.json                                   |  33 +++
 .../src/PopKPI.tsx                                 |  96 +++++++
 .../src/images/thumbnail.png                       | Bin 0 -> 23099 bytes
 .../src/index.ts                                   |  27 ++
 .../src/plugin/buildQuery.ts                       | 299 +++++++++++++++++++++
 .../src/plugin/controlPanel.ts                     | 169 ++++++++++++
 .../src/plugin/index.ts                            |  51 ++++
 .../src/plugin/transformProps.ts                   | 142 ++++++++++
 .../src/types.ts                                   |  56 ++++
 .../tsconfig.json                                  |  25 ++
 .../types/types/external.d.ts                      |  23 ++
 .../src/visualizations/presets/MainPreset.js       |  10 +-
 superset/config.py                                 |   2 +
 18 files changed, 1066 insertions(+), 1 deletion(-)

diff --git a/RESOURCES/FEATURE_FLAGS.md b/RESOURCES/FEATURE_FLAGS.md
index 8700bcb981..0aa5aca9c7 100644
--- a/RESOURCES/FEATURE_FLAGS.md
+++ b/RESOURCES/FEATURE_FLAGS.md
@@ -31,6 +31,7 @@ These features are considered **unfinished** and should only 
be used on developm
 - PRESTO_EXPAND_DATA
 - SHARE_QUERIES_VIA_KV_STORE
 - TAGGING_SYSTEM
+- CHART_PLUGINS_EXPERIMENTAL
 
 ## In Testing
 
diff --git a/superset-frontend/package-lock.json 
b/superset-frontend/package-lock.json
index e61729a2a9..2e8441220c 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -43,6 +43,7 @@
         "@superset-ui/legacy-preset-chart-nvd3": 
"file:./plugins/legacy-preset-chart-nvd3",
         "@superset-ui/plugin-chart-echarts": 
"file:./plugins/plugin-chart-echarts",
         "@superset-ui/plugin-chart-handlebars": 
"file:./plugins/plugin-chart-handlebars",
+        "@superset-ui/plugin-chart-period-over-period-kpi": 
"file:./plugins/plugin-chart-period-over-period-kpi",
         "@superset-ui/plugin-chart-pivot-table": 
"file:./plugins/plugin-chart-pivot-table",
         "@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
         "@superset-ui/plugin-chart-word-cloud": 
"file:./plugins/plugin-chart-word-cloud",
@@ -19084,6 +19085,10 @@
       "resolved": "plugins/plugin-chart-handlebars",
       "link": true
     },
+    "node_modules/@superset-ui/plugin-chart-period-over-period-kpi": {
+      "resolved": "plugins/plugin-chart-period-over-period-kpi",
+      "link": true
+    },
     "node_modules/@superset-ui/plugin-chart-pivot-table": {
       "resolved": "plugins/plugin-chart-pivot-table",
       "link": true
@@ -69566,6 +69571,30 @@
       "integrity": 
"sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
       "dev": true
     },
+    "plugins/plugin-chart-period-over-period-kpi": {
+      "version": "0.1.0",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "moment": "^2.30.1"
+      },
+      "devDependencies": {
+        "@types/jest": "^26.0.4",
+        "jest": "^26.6.3"
+      },
+      "peerDependencies": {
+        "@superset-ui/chart-controls": "*",
+        "@superset-ui/core": "*",
+        "react": "^16.13.1"
+      }
+    },
+    "plugins/plugin-chart-period-over-period-kpi/node_modules/moment": {
+      "version": "2.30.1",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz";,
+      "integrity": 
"sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+      "engines": {
+        "node": "*"
+      }
+    },
     "plugins/plugin-chart-pivot-table": {
       "name": "@superset-ui/plugin-chart-pivot-table",
       "version": "0.18.25",
@@ -86107,6 +86136,21 @@
         }
       }
     },
+    "@superset-ui/plugin-chart-period-over-period-kpi": {
+      "version": "file:plugins/plugin-chart-period-over-period-kpi",
+      "requires": {
+        "@types/jest": "^26.0.4",
+        "jest": "^26.6.3",
+        "moment": "^2.30.1"
+      },
+      "dependencies": {
+        "moment": {
+          "version": "2.30.1",
+          "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz";,
+          "integrity": 
"sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="
+        }
+      }
+    },
     "@superset-ui/plugin-chart-pivot-table": {
       "version": "file:plugins/plugin-chart-pivot-table",
       "requires": {
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index 5fdd5cc911..f42f7fb237 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -111,6 +111,7 @@
     "@superset-ui/legacy-preset-chart-nvd3": 
"file:./plugins/legacy-preset-chart-nvd3",
     "@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts",
     "@superset-ui/plugin-chart-handlebars": 
"file:./plugins/plugin-chart-handlebars",
+    "@superset-ui/plugin-chart-period-over-period-kpi": 
"file:./plugins/plugin-chart-period-over-period-kpi",
     "@superset-ui/plugin-chart-pivot-table": 
"file:./plugins/plugin-chart-pivot-table",
     "@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table",
     "@superset-ui/plugin-chart-word-cloud": 
"file:./plugins/plugin-chart-word-cloud",
diff --git 
a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts 
b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
index 4f6fece610..f21d20650c 100644
--- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
+++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts
@@ -26,6 +26,7 @@ export enum FeatureFlag {
   ALERT_REPORTS = 'ALERT_REPORTS',
   ALLOW_FULL_CSV_EXPORT = 'ALLOW_FULL_CSV_EXPORT',
   AVOID_COLORS_COLLISION = 'AVOID_COLORS_COLLISION',
+  CHART_PLUGINS_EXPERIMENTAL = 'CHART_PLUGINS_EXPERIMENTAL',
   CONFIRM_DASHBOARD_DIFF = 'CONFIRM_DASHBOARD_DIFF',
   /** @deprecated */
   DASHBOARD_CROSS_FILTERS = 'DASHBOARD_CROSS_FILTERS',
diff --git 
a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/README.md 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/README.md
new file mode 100644
index 0000000000..2e3fbea212
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/README.md
@@ -0,0 +1,87 @@
+/\*\*
+
+- Licensed to the Apache Software Foundation (ASF) under one
+- or more contributor license agreements. See the NOTICE file
+- distributed with this work for additional information
+- regarding copyright ownership. The ASF licenses this file
+- to you under the Apache License, Version 2.0 (the
+- "License"); you may not use this file except in compliance
+- with the License. You may obtain a copy of the License at
+-
+- http://www.apache.org/licenses/LICENSE-2.0
+-
+- Unless required by applicable law or agreed to in writing,
+- software distributed under the License is distributed on an
+- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+- KIND, either express or implied. See the License for the
+- specific language governing permissions and limitations
+- under the License.
+  \*/
+
+# custom-viz
+
+This plugin provides a BigNumber visualization with period over period time 
comparisons
+
+### Usage
+
+To build the plugin, run the following commands:
+
+```
+npm ci
+npm run build
+```
+
+Alternatively, to run the plugin in development mode (=rebuilding whenever 
changes are made), start the dev server with the following command:
+
+```
+npm run dev
+```
+
+To add the package to Superset, go to the `superset-frontend` subdirectory in 
your Superset source folder (assuming both the `custom-viz` plugin and 
`superset` repos are in the same root directory) and run
+
+```
+npm i -S ../../plugin-chart-period-over-period-kpi
+```
+
+If your Superset plugin exists in the `superset-frontend` directory and you 
wish to resolve TypeScript errors about `@superset-ui/core` not being resolved 
correctly, add the following to your `tsconfig.json` file:
+
+```
+"references": [
+  {
+    "path": "../../packages/superset-ui-chart-controls"
+  },
+  {
+    "path": "../../packages/superset-ui-core"
+  }
+]
+```
+
+You may also wish to add the following to the `include` array in 
`tsconfig.json` to make Superset types available to your plugin:
+
+```
+"../../types/**/*"
+```
+
+Finally, if you wish to ensure your plugin `tsconfig.json` is aligned with the 
root Superset project, you may add the following to your `tsconfig.json` file:
+
+```
+"extends": "../../tsconfig.json",
+```
+
+After this edit the 
`superset-frontend/src/visualizations/presets/MainPreset.js` and make the 
following changes:
+
+```js
+import { PopKPIPlugin } from 
'@superset-ui/plugin-chart-period-over-period-kpi';
+```
+
+to import the plugin and later add the following to the array that's passed to 
the `plugins` property:
+
+```js
+new PopKPIPlugin().configure({ key: 'pop_kpi' }),
+```
+
+After that the plugin should show up when you run Superset, e.g. the 
development server:
+
+```
+npm run dev-server
+```
diff --git 
a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/package.json 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/package.json
new file mode 100644
index 0000000000..49f8f2935e
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/package.json
@@ -0,0 +1,33 @@
+{
+  "name": "@superset-ui/plugin-chart-period-over-period-kpi",
+  "version": "0.1.0",
+  "description": "Big Number with Time Period Comparison",
+  "sideEffects": false,
+  "main": "lib/index.js",
+  "module": "esm/index.js",
+  "files": [
+    "esm",
+    "lib"
+  ],
+  "private": true,
+  "keywords": [
+    "superset"
+  ],
+  "author": "Bytecodeio",
+  "license": "Apache-2.0",
+  "publishConfig": {
+    "access": "public"
+  },
+  "dependencies": {
+    "moment": "^2.30.1"
+  },
+  "peerDependencies": {
+    "@superset-ui/chart-controls": "*",
+    "@superset-ui/core": "*",
+    "react": "^16.13.1"
+  },
+  "devDependencies": {
+    "@types/jest": "^26.0.4",
+    "jest": "^26.6.3"
+  }
+}
diff --git 
a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx
new file mode 100644
index 0000000000..e780e93ca4
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx
@@ -0,0 +1,96 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React, { createRef } from 'react';
+import { css, styled, useTheme } from '@superset-ui/core';
+import { PopKPIComparisonValueStyleProps, PopKPIProps } from './types';
+
+const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
+  ${({ theme, subheaderFontSize }) => `
+    font-weight: ${theme.typography.weights.light};
+    width: 33%;
+    display: table-cell;
+    font-size: ${subheaderFontSize || 20}px;
+    text-align: center;
+  `}
+`;
+
+export default function PopKPI(props: PopKPIProps) {
+  const {
+    height,
+    width,
+    bigNumber,
+    prevNumber,
+    valueDifference,
+    percentDifference,
+    headerFontSize,
+    subheaderFontSize,
+  } = props;
+
+  const rootElem = createRef<HTMLDivElement>();
+  const theme = useTheme();
+
+  const wrapperDivStyles = css`
+    font-family: ${theme.typography.families.sansSerif};
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    padding: ${theme.gridUnit * 4}px;
+    border-radius: ${theme.gridUnit * 2}px;
+    height: ${height}px;
+    width: ${width}px;
+  `;
+
+  const bigValueContainerStyles = css`
+    font-size: ${headerFontSize || 60}px;
+    font-weight: ${theme.typography.weights.normal};
+    text-align: center;
+  `;
+
+  return (
+    <div ref={rootElem} css={wrapperDivStyles}>
+      <div css={bigValueContainerStyles}>{bigNumber}</div>
+      <div
+        css={css`
+          width: 100%;
+          display: table;
+        `}
+      >
+        <div
+          css={css`
+            display: table-row;
+          `}
+        >
+          <ComparisonValue subheaderFontSize={subheaderFontSize}>
+            {' '}
+            #: {prevNumber}
+          </ComparisonValue>
+          <ComparisonValue subheaderFontSize={subheaderFontSize}>
+            {' '}
+            Δ: {valueDifference}
+          </ComparisonValue>
+          <ComparisonValue subheaderFontSize={subheaderFontSize}>
+            {' '}
+            %: {percentDifference}
+          </ComparisonValue>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git 
a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/images/thumbnail.png
 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/images/thumbnail.png
new file mode 100644
index 0000000000..30c9e07b0c
Binary files /dev/null and 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/images/thumbnail.png
 differ
diff --git 
a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/index.ts 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/index.ts
new file mode 100644
index 0000000000..e9fe3ec782
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/index.ts
@@ -0,0 +1,27 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+// eslint-disable-next-line import/prefer-default-export
+export { default as PopKPIPlugin } from './plugin';
+/**
+ * Note: this file exports the default export from PopKPI.tsx.
+ * If you want to export multiple visualization modules, you will need to
+ * either add additional plugin folders (similar in structure to ./plugin)
+ * OR export multiple instances of `ChartPlugin` extensions in 
./plugin/index.ts
+ * which in turn load exports from CustomViz.tsx
+ */
diff --git 
a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
new file mode 100644
index 0000000000..202063c13c
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
@@ -0,0 +1,299 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  AdhocFilter,
+  buildQueryContext,
+  QueryFormData,
+} from '@superset-ui/core';
+import moment, { Moment } from 'moment';
+
+/**
+ * The buildQuery function is used to create an instance of QueryContext that's
+ * sent to the chart data endpoint. In addition to containing information of 
which
+ * datasource to use, it specifies the type (e.g. full payload, samples, 
query) and
+ * format (e.g. CSV or JSON) of the result and whether or not to force refresh 
the data from
+ * the datasource as opposed to using a cached copy of the data, if available.
+ *
+ * More importantly though, QueryContext contains a property `queries`, which 
is an array of
+ * QueryObjects specifying individual data requests to be made. A QueryObject 
specifies which
+ * columns, metrics and filters, among others, to use during the query. 
Usually it will be enough
+ * to specify just one query based on the baseQueryObject, but for some more 
advanced use cases
+ * it is possible to define post processing operations in the QueryObject, or 
multiple queries
+ * if a viz needs multiple different result sets.
+ */
+
+type MomentTuple = [moment.Moment | null, moment.Moment | null];
+
+function getSinceUntil(
+  timeRange: string | null = null,
+  relativeStart: string | null = null,
+  relativeEnd: string | null = null,
+): MomentTuple {
+  const separator = ' : ';
+  const effectiveRelativeStart = relativeStart || 'today';
+  const effectiveRelativeEnd = relativeEnd || 'today';
+
+  if (!timeRange) {
+    return [null, null];
+  }
+
+  let modTimeRange: string | null = timeRange;
+
+  if (timeRange === 'NO_TIME_RANGE' || timeRange === '_(NO_TIME_RANGE)') {
+    return [null, null];
+  }
+
+  if (timeRange?.startsWith('last') && !timeRange.includes(separator)) {
+    modTimeRange = timeRange + separator + effectiveRelativeEnd;
+  }
+
+  if (timeRange?.startsWith('next') && !timeRange.includes(separator)) {
+    modTimeRange = effectiveRelativeStart + separator + timeRange;
+  }
+
+  if (
+    timeRange?.startsWith('previous calendar week') &&
+    !timeRange.includes(separator)
+  ) {
+    return [
+      moment().subtract(1, 'week').startOf('week'),
+      moment().startOf('week'),
+    ];
+  }
+
+  if (
+    timeRange?.startsWith('previous calendar month') &&
+    !timeRange.includes(separator)
+  ) {
+    return [
+      moment().subtract(1, 'month').startOf('month'),
+      moment().startOf('month'),
+    ];
+  }
+
+  if (
+    timeRange?.startsWith('previous calendar year') &&
+    !timeRange.includes(separator)
+  ) {
+    return [
+      moment().subtract(1, 'year').startOf('year'),
+      moment().startOf('year'),
+    ];
+  }
+
+  const timeRangeLookup: Array<[RegExp, (...args: string[]) => Moment]> = [
+    [
+      /^last\s+(day|week|month|quarter|year)$/i,
+      (unit: string) =>
+        moment().subtract(1, unit as moment.unitOfTime.DurationConstructor),
+    ],
+    [
+      /^last\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s?$/i,
+      (delta: string, unit: string) =>
+        moment().subtract(delta, unit as 
moment.unitOfTime.DurationConstructor),
+    ],
+    [
+      /^next\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s?$/i,
+      (delta: string, unit: string) =>
+        moment().add(delta, unit as moment.unitOfTime.DurationConstructor),
+    ],
+    [
+      // eslint-disable-next-line no-useless-escape
+      /DATEADD\(DATETIME\("([^"]+)"\),\s*(-?\d+),\s*([^\)]+)\)/i,
+      (timePart: string, delta: string, unit: string) => {
+        if (timePart === 'now') {
+          return moment().add(
+            delta,
+            unit as moment.unitOfTime.DurationConstructor,
+          );
+        }
+        if (moment(timePart.toUpperCase(), true).isValid()) {
+          return moment(timePart).add(
+            delta,
+            unit as moment.unitOfTime.DurationConstructor,
+          );
+        }
+        return moment();
+      },
+    ],
+  ];
+
+  const sinceAndUntilPartition = modTimeRange
+    .split(separator, 2)
+    .map(part => part.trim());
+
+  const sinceAndUntil: (Moment | null)[] = sinceAndUntilPartition.map(part => {
+    if (!part) {
+      return null;
+    }
+
+    let transformedValue: Moment | null = null;
+    // Matching time_range_lookup
+    const matched = timeRangeLookup.some(([pattern, fn]) => {
+      const result = part.match(pattern);
+      if (result) {
+        transformedValue = fn(...result.slice(1));
+        return true;
+      }
+
+      if (part === 'today') {
+        transformedValue = moment().startOf('day');
+        return true;
+      }
+
+      if (part === 'now') {
+        transformedValue = moment();
+        return true;
+      }
+      return false;
+    });
+
+    if (matched && transformedValue !== null) {
+      // Handle the transformed value
+    } else {
+      // Handle the case when there was no match
+      transformedValue = moment(`${part}`);
+    }
+
+    return transformedValue;
+  });
+
+  const [_since, _until] = sinceAndUntil;
+
+  if (_since && _until && _since.isAfter(_until)) {
+    throw new Error('From date cannot be larger than to date');
+  }
+
+  return [_since, _until];
+}
+
+function calculatePrev(
+  startDate: Moment | null,
+  endDate: Moment | null,
+  calcType: String,
+) {
+  if (!startDate || !endDate) {
+    return [null, null];
+  }
+
+  const daysBetween = endDate.diff(startDate, 'days');
+
+  let startDatePrev = moment();
+  let endDatePrev = moment();
+  if (calcType === 'y') {
+    startDatePrev = startDate.subtract(1, 'year');
+    endDatePrev = endDate.subtract(1, 'year');
+  } else if (calcType === 'w') {
+    startDatePrev = startDate.subtract(1, 'week');
+    endDatePrev = endDate.subtract(1, 'week');
+  } else if (calcType === 'm') {
+    startDatePrev = startDate.subtract(1, 'month');
+    endDatePrev = endDate.subtract(1, 'month');
+  } else if (calcType === 'r') {
+    startDatePrev = startDate.clone().subtract(daysBetween.valueOf(), 'day');
+    endDatePrev = startDate;
+  } else {
+    startDatePrev = startDate.subtract(1, 'year');
+    endDatePrev = endDate.subtract(1, 'year');
+  }
+
+  return [startDatePrev, endDatePrev];
+}
+
+export default function buildQuery(formData: QueryFormData) {
+  const { cols: groupby, time_comparison: timeComparison } = formData;
+
+  const queryContextA = buildQueryContext(formData, baseQueryObject => [
+    {
+      ...baseQueryObject,
+      groupby,
+    },
+  ]);
+
+  const timeFilterIndex: number =
+    formData.adhoc_filters?.findIndex(
+      filter => 'operator' in filter && filter.operator === 'TEMPORAL_RANGE',
+    ) ?? -1;
+
+  const timeFilter: AdhocFilter | null =
+    timeFilterIndex !== -1 && formData.adhoc_filters
+      ? formData.adhoc_filters[timeFilterIndex]
+      : null;
+
+  let testSince = null;
+  let testUntil = null;
+
+  if (
+    timeFilter &&
+    'comparator' in timeFilter &&
+    typeof timeFilter.comparator === 'string'
+  ) {
+    [testSince, testUntil] = getSinceUntil(
+      timeFilter.comparator.toLocaleLowerCase(),
+    );
+  }
+
+  let formDataB: QueryFormData;
+
+  if (timeComparison !== 'c') {
+    const [prevStartDateMoment, prevEndDateMoment] = calculatePrev(
+      testSince,
+      testUntil,
+      timeComparison,
+    );
+
+    const queryBComparator = `${prevStartDateMoment?.format(
+      'YYYY-MM-DDTHH:mm:ss',
+    )} : ${prevEndDateMoment?.format('YYYY-MM-DDTHH:mm:ss')}`;
+
+    const queryBFilter: any = {
+      ...timeFilter,
+      comparator: queryBComparator.replace(/Z/g, ''),
+    };
+
+    const otherFilters = formData.adhoc_filters?.filter(
+      (_value: any, index: number) => timeFilterIndex !== index,
+    );
+    const queryBFilters = otherFilters
+      ? [queryBFilter, ...otherFilters]
+      : [queryBFilter];
+
+    formDataB = {
+      ...formData,
+      adhoc_filters: queryBFilters,
+    };
+  } else {
+    formDataB = {
+      ...formData,
+      adhoc_filters: formData.adhoc_custom,
+    };
+  }
+
+  const queryContextB = buildQueryContext(formDataB, baseQueryObject => [
+    {
+      ...baseQueryObject,
+      groupby,
+    },
+  ]);
+
+  return {
+    ...queryContextA,
+    queries: [...queryContextA.queries, ...queryContextB.queries],
+  };
+}
diff --git 
a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts
 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts
new file mode 100644
index 0000000000..82379745fd
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts
@@ -0,0 +1,169 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t, validateNonEmpty } from '@superset-ui/core';
+import {
+  ControlPanelConfig,
+  sharedControls,
+} from '@superset-ui/chart-controls';
+
+const config: ControlPanelConfig = {
+  controlPanelSections: [
+    {
+      label: t('Query'),
+      expanded: true,
+      controlSetRows: [
+        [
+          {
+            name: 'metrics',
+            config: {
+              ...sharedControls.metrics,
+              // it's possible to add validators to controls if
+              // certain selections/types need to be enforced
+              validators: [validateNonEmpty],
+            },
+          },
+        ],
+        ['adhoc_filters'],
+        [
+          {
+            name: 'time_comparison',
+            config: {
+              type: 'SelectControl',
+              label: t('Range for Comparison'),
+              default: 'y',
+              choices: [
+                ['y', 'Year'],
+                ['w', 'Week'],
+                ['m', 'Month'],
+                ['r', 'Range'],
+                ['c', 'Custom'],
+              ],
+            },
+          },
+        ],
+        [
+          {
+            name: 'row_limit',
+            config: sharedControls.row_limit,
+          },
+        ],
+      ],
+    },
+    {
+      label: t('Custom Time Range'),
+      expanded: true,
+      controlSetRows: [
+        [
+          {
+            name: `adhoc_custom`,
+            config: {
+              ...sharedControls.adhoc_filters,
+              label: t('FILTERS (Custom)'),
+              description:
+                'This only applies when selecting the Range for Comparison 
Type- Custom',
+            },
+          },
+        ],
+      ],
+    },
+    {
+      label: t('Chart Options'),
+      expanded: true,
+      controlSetRows: [
+        ['y_axis_format'],
+        ['currency_format'],
+        [
+          {
+            name: 'header_font_size',
+            config: {
+              type: 'SelectControl',
+              label: t('Big Number Font Size'),
+              renderTrigger: true,
+              clearable: false,
+              default: 60,
+              options: [
+                {
+                  label: t('Tiny'),
+                  value: 16,
+                },
+                {
+                  label: t('Small'),
+                  value: 20,
+                },
+                {
+                  label: t('Normal'),
+                  value: 30,
+                },
+                {
+                  label: t('Large'),
+                  value: 48,
+                },
+                {
+                  label: t('Huge'),
+                  value: 60,
+                },
+              ],
+            },
+          },
+        ],
+        [
+          {
+            name: 'subheader_font_size',
+            config: {
+              type: 'SelectControl',
+              label: t('Subheader Font Size'),
+              renderTrigger: true,
+              clearable: false,
+              default: 40,
+              options: [
+                {
+                  label: t('Tiny'),
+                  value: 16,
+                },
+                {
+                  label: t('Small'),
+                  value: 20,
+                },
+                {
+                  label: t('Normal'),
+                  value: 26,
+                },
+                {
+                  label: t('Large'),
+                  value: 32,
+                },
+                {
+                  label: t('Huge'),
+                  value: 40,
+                },
+              ],
+            },
+          },
+        ],
+      ],
+    },
+  ],
+  controlOverrides: {
+    y_axis_format: {
+      label: t('Number format'),
+    },
+  },
+};
+
+export default config;
diff --git 
a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/index.ts
 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/index.ts
new file mode 100644
index 0000000000..2ea1b94bdb
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/index.ts
@@ -0,0 +1,51 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
+import buildQuery from './buildQuery';
+import controlPanel from './controlPanel';
+import transformProps from './transformProps';
+import thumbnail from '../images/thumbnail.png';
+
+export default class PopKPIPlugin extends ChartPlugin {
+  /**
+   * The constructor is used to pass relevant metadata and callbacks that get
+   * registered in respective registries that are used throughout the library
+   * and application. A more thorough description of each property is given in
+   * the respective imported file.
+   *
+   * It is worth noting that `buildQuery` and is optional, and only needed for
+   * advanced visualizations that require either post processing operations
+   * (pivoting, rolling aggregations, sorting etc) or submitting multiple 
queries.
+   */
+  constructor() {
+    const metadata = new ChartMetadata({
+      description: 'KPI viz for comparing multiple period',
+      name: t('Big Number with Time Period Comparison'),
+      thumbnail,
+    });
+
+    super({
+      buildQuery,
+      controlPanel,
+      loadChart: () => import('../PopKPI'),
+      metadata,
+      transformProps,
+    });
+  }
+}
diff --git 
a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
new file mode 100644
index 0000000000..437641143c
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
@@ -0,0 +1,142 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import moment from 'moment';
+import {
+  ChartProps,
+  getMetricLabel,
+  getValueFormatter,
+  NumberFormats,
+  getNumberFormatter,
+} from '@superset-ui/core';
+
+export const parseMetricValue = (metricValue: number | string | null) => {
+  if (typeof metricValue === 'string') {
+    const dateObject = moment.utc(metricValue, moment.ISO_8601, true);
+    if (dateObject.isValid()) {
+      return dateObject.valueOf();
+    }
+    return 0;
+  }
+  return metricValue ?? 0;
+};
+
+export default function transformProps(chartProps: ChartProps) {
+  /**
+   * This function is called after a successful response has been
+   * received from the chart data endpoint, and is used to transform
+   * the incoming data prior to being sent to the Visualization.
+   *
+   * The transformProps function is also quite useful to return
+   * additional/modified props to your data viz component. The formData
+   * can also be accessed from your CustomViz.tsx file, but
+   * doing supplying custom props here is often handy for integrating third
+   * party libraries that rely on specific props.
+   *
+   * A description of properties in `chartProps`:
+   * - `height`, `width`: the height/width of the DOM element in which
+   *   the chart is located
+   * - `formData`: the chart data request payload that was sent to the
+   *   backend.
+   * - `queriesData`: the chart data response payload that was received
+   *   from the backend. Some notable properties of `queriesData`:
+   *   - `data`: an array with data, each row with an object mapping
+   *     the column/alias to its value. Example:
+   *     `[{ col1: 'abc', metric1: 10 }, { col1: 'xyz', metric1: 20 }]`
+   *   - `rowcount`: the number of rows in `data`
+   *   - `query`: the query that was issued.
+   *
+   * Please note: the transformProps function gets cached when the
+   * application loads. When making changes to the `transformProps`
+   * function during development with hot reloading, changes won't
+   * be seen until restarting the development server.
+   */
+  const {
+    width,
+    height,
+    formData,
+    queriesData,
+    datasource: { currencyFormats = {}, columnFormats = {} },
+  } = chartProps;
+  const {
+    boldText,
+    headerFontSize,
+    headerText,
+    metrics,
+    yAxisFormat,
+    currencyFormat,
+    subheaderFontSize,
+  } = formData;
+  const { data: dataA = [] } = queriesData[0];
+  const { data: dataB = [] } = queriesData[1];
+  const data = dataA;
+  const metricName = getMetricLabel(metrics[0]);
+  let bigNumber: number | string =
+    data.length === 0 ? 0 : parseMetricValue(data[0][metricName]);
+  let prevNumber: number | string =
+    data.length === 0 ? 0 : parseMetricValue(dataB[0][metricName]);
+
+  const numberFormatter = getValueFormatter(
+    metrics[0],
+    currencyFormats,
+    columnFormats,
+    yAxisFormat,
+    currencyFormat,
+  );
+
+  const compTitles = {
+    r: 'Range' as string,
+    y: 'Year' as string,
+    m: 'Month' as string,
+    w: 'Week' as string,
+  };
+
+  const formatPercentChange = getNumberFormatter(
+    NumberFormats.PERCENT_SIGNED_1_POINT,
+  );
+
+  let valueDifference: number | string = bigNumber - prevNumber;
+
+  const percentDifferenceNum = prevNumber
+    ? (bigNumber - prevNumber) / Math.abs(prevNumber)
+    : 0;
+
+  const compType = compTitles[formData.timeComparison];
+  bigNumber = numberFormatter(bigNumber);
+  prevNumber = numberFormatter(prevNumber);
+  valueDifference = numberFormatter(valueDifference);
+  const percentDifference: string = formatPercentChange(percentDifferenceNum);
+
+  return {
+    width,
+    height,
+    data,
+    // and now your control data, manipulated as needed, and passed through as 
props!
+    metrics,
+    metricName,
+    bigNumber,
+    prevNumber,
+    valueDifference,
+    percentDifference,
+    boldText,
+    headerFontSize,
+    subheaderFontSize,
+    headerText,
+    compType,
+  };
+}
diff --git 
a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts
new file mode 100644
index 0000000000..b13f2115ef
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts
@@ -0,0 +1,56 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import {
+  QueryFormData,
+  supersetTheme,
+  TimeseriesDataRecord,
+  Metric,
+} from '@superset-ui/core';
+
+export interface PopKPIStylesProps {
+  height: number;
+  width: number;
+  headerFontSize: keyof typeof supersetTheme.typography.sizes;
+  subheaderFontSize: keyof typeof supersetTheme.typography.sizes;
+  boldText: boolean;
+}
+
+interface PopKPICustomizeProps {
+  headerText: string;
+}
+
+export interface PopKPIComparisonValueStyleProps {
+  subheaderFontSize?: keyof typeof supersetTheme.typography.sizes;
+}
+
+export type PopKPIQueryFormData = QueryFormData &
+  PopKPIStylesProps &
+  PopKPICustomizeProps;
+
+export type PopKPIProps = PopKPIStylesProps &
+  PopKPICustomizeProps & {
+    data: TimeseriesDataRecord[];
+    metrics: Metric[];
+    metricName: String;
+    bigNumber: Number;
+    prevNumber: Number;
+    valueDifference: Number;
+    percentDifference: Number;
+    compType: String;
+  };
diff --git 
a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/tsconfig.json 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/tsconfig.json
new file mode 100644
index 0000000000..b6bfaa2d98
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/tsconfig.json
@@ -0,0 +1,25 @@
+{
+  "compilerOptions": {
+    "declarationDir": "lib",
+    "outDir": "lib",
+    "rootDir": "src"
+  },
+  "exclude": [
+    "lib",
+    "test"
+  ],
+  "extends": "../../tsconfig.json",
+  "include": [
+    "src/**/*",
+    "types/**/*",
+    "../../types/**/*"
+  ],
+  "references": [
+    {
+      "path": "../../packages/superset-ui-chart-controls"
+    },
+    {
+      "path": "../../packages/superset-ui-core"
+    }
+  ]
+}
diff --git 
a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/types/types/external.d.ts
 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/types/types/external.d.ts
new file mode 100644
index 0000000000..a273f3a2ba
--- /dev/null
+++ 
b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/types/types/external.d.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+declare module '*.png' {
+  const value: any;
+  export default value;
+}
diff --git a/superset-frontend/src/visualizations/presets/MainPreset.js 
b/superset-frontend/src/visualizations/presets/MainPreset.js
index e96b528c9d..98ee67a5d2 100644
--- a/superset-frontend/src/visualizations/presets/MainPreset.js
+++ b/superset-frontend/src/visualizations/presets/MainPreset.js
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Preset } from '@superset-ui/core';
+import { isFeatureEnabled, FeatureFlag, Preset } from '@superset-ui/core';
 import CalendarChartPlugin from '@superset-ui/legacy-plugin-chart-calendar';
 import ChordChartPlugin from '@superset-ui/legacy-plugin-chart-chord';
 import CountryMapChartPlugin from 
'@superset-ui/legacy-plugin-chart-country-map';
@@ -76,10 +76,17 @@ import {
 } from 'src/filters/components';
 import { PivotTableChartPlugin as PivotTableChartPluginV2 } from 
'@superset-ui/plugin-chart-pivot-table';
 import { HandlebarsChartPlugin } from '@superset-ui/plugin-chart-handlebars';
+import { PopKPIPlugin } from 
'@superset-ui/plugin-chart-period-over-period-kpi';
 import TimeTableChartPlugin from '../TimeTable';
 
 export default class MainPreset extends Preset {
   constructor() {
+    const experimentalPlugins = isFeatureEnabled(
+      FeatureFlag.CHART_PLUGINS_EXPERIMENTAL,
+    )
+      ? [new PopKPIPlugin().configure({ key: 'pop_kpi' })]
+      : [];
+
     super({
       name: 'Legacy charts',
       presets: [new DeckGLChartPreset()],
@@ -155,6 +162,7 @@ export default class MainPreset extends Preset {
         new EchartsSunburstChartPlugin().configure({ key: 'sunburst_v2' }),
         new HandlebarsChartPlugin().configure({ key: 'handlebars' }),
         new EchartsBubbleChartPlugin().configure({ key: 'bubble_v2' }),
+        ...experimentalPlugins,
       ],
     });
   }
diff --git a/superset/config.py b/superset/config.py
index 8b57244d28..d0d519236a 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -485,6 +485,8 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
     # Unlike Selenium, Playwright reports support deck.gl visualizations
     # Enabling this feature flag requires installing "playwright" pip package
     "PLAYWRIGHT_REPORTS_AND_THUMBNAILS": False,
+    # Set to True to enable experimental chart plugins
+    "CHART_PLUGINS_EXPERIMENTAL": False,
 }
 
 # ------------------------------


Reply via email to