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,
}
# ------------------------------