diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index f5d80e0b1f098..496d82c9c9284 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",
@@ -18459,6 +18460,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
@@ -63723,6 +63728,31 @@
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==",
"peer": true
},
+ "plugins/plugin-chart-period-over-period-kpi": {
+ "name": "@superset-ui/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",
@@ -78514,6 +78544,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 ad843b0fe2b68..d1a127b5289b0 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -112,6 +112,7 @@
"@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",
+ "@superset-ui/plugin-chart-period-over-period-kpi": "file:./plugins/plugin-chart-period-over-period-kpi",
"@superset-ui/switchboard": "file:./packages/superset-ui-switchboard",
"@types/d3-format": "^3.0.1",
"@visx/axis": "^3.0.1",
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 0000000000000..fad228dfd4591
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/README.md
@@ -0,0 +1,65 @@
+# custom-viz
+
+This is the Custom Viz Superset Chart Plugin.
+
+### 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 ../../custom-viz
+```
+
+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 { CustomViz } from 'custom-viz';
+```
+
+to import the plugin and later add the following to the array that's passed to the `plugins` property:
+```js
+new CustomViz().configure({ key: 'custom-viz' }),
+```
+
+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 0000000000000..387803f2ea34b
--- /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": "Period Over Period KPI",
+ "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 0000000000000..d38426494056b
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx
@@ -0,0 +1,105 @@
+/**
+ * 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, { useEffect, createRef } from 'react';
+import {
+ styled,
+ } from '@superset-ui/core';
+import { PopKPIProps, PopKPIStylesProps } from './types';
+
+// The following Styles component is a
element, which has been styled using Emotion
+// For docs, visit https://emotion.sh/docs/styled
+
+// Theming variables are provided for your use via a ThemeProvider
+// imported from @superset-ui/core. For variables available, please visit
+// https://github.com/apache-superset/superset-ui/blob/master/packages/superset-ui-core/src/style/index.ts
+
+const Styles = styled.div
`
+
+ font-family: ${({ theme }) => theme.typography.families.sansSerif};
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: ${({ theme }) => theme.tdUnit * 4}px;
+ border-radius: ${({ theme }) => theme.tdUnit * 2}px;
+ height: ${({ height }) => height}px;
+ width: ${({ width }) => width}px;
+`;
+
+const BigValueContainer = styled.div`
+ font-size: ${props=> props.headerFontSize ? props.headerFontSize : 60}px;
+ font-weight: ${({ theme }) => theme.typography.weights.normal};
+ text-align: center;
+`;
+
+const TableContainer = styled.div`
+ width: 100%;
+ display: table;
+`
+
+const ComparisonContainer = styled.div`
+ display: table-row;
+`;
+
+const ComparisonValue = styled.div`
+ font-weight: ${({ theme }) => theme.typography.weights.light};
+ width: 33%;
+ display: table-cell;
+ font-size: ${props=> props.subheaderFontSize ? props.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();
+
+ useEffect(() => {
+ const root = rootElem.current as HTMLElement;
+ });
+
+ return (
+
+
+ {bigNumber}
+
+
+ #: {prevNumber}
+ Δ: {valueDifference}
+ %: {percentDifference}
+
+
+
+ );
+}
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 0000000000000..30c9e07b0ccae
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 0000000000000..e9fe3ec782104
--- /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 0000000000000..b009bf90383f6
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
@@ -0,0 +1,283 @@
+/**
+ * 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 { 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 _relativeStart = relativeStart || "today";
+ const _relativeEnd = 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 + _relativeEnd;
+ }
+
+ if (timeRange?.startsWith('next') && !timeRange.includes(separator)) {
+ modTimeRange = _relativeStart + 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 timeFilter: any = formData.adhoc_filters?.find(
+ ({ operator }: { operator: string }) => operator === 'TEMPORAL_RANGE',
+ );
+
+ const timeFilterIndex: any = formData.adhoc_filters?.findIndex(
+ ({ operator }: { operator: string }) => operator === 'TEMPORAL_RANGE',
+ );
+
+ const [testSince, testUntil] = getSinceUntil(
+ timeFilter.comparator.toLowerCase(),
+ );
+
+ 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 = {
+ ...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 0000000000000..82379745fd10f
--- /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 0000000000000..5cf80d1efcbd3
--- /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('Period Over Period KPI'),
+ 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 0000000000000..0f5723264cdbf
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
@@ -0,0 +1,129 @@
+/**
+ * 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 {
+ ChartProps,
+ TimeseriesDataRecord,
+ getMetricLabel,
+ getValueFormatter,
+ NumberFormats,
+ getNumberFormatter,
+} from '@superset-ui/core';
+
+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 dataA = queriesData[0].data as TimeseriesDataRecord[];
+ const dataB = queriesData[1].data as TimeseriesDataRecord[];
+ const data = dataA;
+ const metricName = getMetricLabel(metrics[0]);
+ let bigNumber = data.length === 0 ? null : data[0][metricName];
+ let prevNumber = dataB.length === 0 ? null : 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 = 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 0000000000000..31bd8f2a5c953
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts
@@ -0,0 +1,52 @@
+/**
+ * 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 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 0000000000000..29d6c9cd4d10f
--- /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"
+ }
+ ]
+}
\ No newline at end of file
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 0000000000000..a273f3a2ba3ed
--- /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 e96b528c9dea2..57b6626a35f9f 100644
--- a/superset-frontend/src/visualizations/presets/MainPreset.js
+++ b/superset-frontend/src/visualizations/presets/MainPreset.js
@@ -77,6 +77,7 @@ import {
import { PivotTableChartPlugin as PivotTableChartPluginV2 } from '@superset-ui/plugin-chart-pivot-table';
import { HandlebarsChartPlugin } from '@superset-ui/plugin-chart-handlebars';
import TimeTableChartPlugin from '../TimeTable';
+import { PopKPIPlugin } from '@superset-ui/plugin-chart-period-over-period-kpi'
export default class MainPreset extends Preset {
constructor() {
@@ -155,6 +156,7 @@ export default class MainPreset extends Preset {
new EchartsSunburstChartPlugin().configure({ key: 'sunburst_v2' }),
new HandlebarsChartPlugin().configure({ key: 'handlebars' }),
new EchartsBubbleChartPlugin().configure({ key: 'bubble_v2' }),
+ new PopKPIPlugin().configure({ key: 'pop_kpi'}),
],
});
}