From ff490e77ce86b1115398cac4c148a1b59b98846f Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Wed, 31 Jan 2024 15:44:25 -0800 Subject: [PATCH] feat: Period over Period Big Number comparison chart (#26908) Co-authored-by: Fernando Co-authored-by: Antonio Rivero (cherry picked from commit a09e5557bc8b40e46495b9473959327118dfaacf) --- RESOURCES/FEATURE_FLAGS.md | 1 + superset-frontend/package-lock.json | 45 +++ superset-frontend/package.json | 1 + .../src/utils/featureFlags.ts | 1 + .../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 | 7 + superset/config.py | 2 + 18 files changed, 1065 insertions(+) create mode 100644 superset-frontend/plugins/plugin-chart-period-over-period-kpi/README.md create mode 100644 superset-frontend/plugins/plugin-chart-period-over-period-kpi/package.json create mode 100644 superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx create mode 100644 superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/images/thumbnail.png create mode 100644 superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/index.ts create mode 100644 superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts create mode 100644 superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts create mode 100644 superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/index.ts create mode 100644 superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts create mode 100644 superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts create mode 100644 superset-frontend/plugins/plugin-chart-period-over-period-kpi/tsconfig.json create mode 100644 superset-frontend/plugins/plugin-chart-period-over-period-kpi/types/types/external.d.ts diff --git a/RESOURCES/FEATURE_FLAGS.md b/RESOURCES/FEATURE_FLAGS.md index d029ca6c3cbd6..1ef66de8deb92 100644 --- a/RESOURCES/FEATURE_FLAGS.md +++ b/RESOURCES/FEATURE_FLAGS.md @@ -32,6 +32,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 d84c688b128d7..6d98145d5f553 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -45,6 +45,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", @@ -18362,6 +18363,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 @@ -63798,6 +63803,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 a2bb57fe4c1c9..b2199daaf6fa9 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 6bc77e0e87a1a..4b2b0a49f46d5 100644 --- a/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts +++ b/superset-frontend/packages/superset-ui-core/src/utils/featureFlags.ts @@ -25,6 +25,7 @@ export enum FeatureFlag { ALERTS_ATTACH_REPORTS = 'ALERTS_ATTACH_REPORTS', ALERT_REPORTS = 'ALERT_REPORTS', ALLOW_FULL_CSV_EXPORT = 'ALLOW_FULL_CSV_EXPORT', + CHART_PLUGINS_EXPERIMENTAL = 'CHART_PLUGINS_EXPERIMENTAL', CLIENT_CACHE = 'CLIENT_CACHE', DASHBOARD_CROSS_FILTERS = 'DASHBOARD_CROSS_FILTERS', DASHBOARD_FILTERS_EXPERIMENTAL = 'DASHBOARD_FILTERS_EXPERIMENTAL', 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..2e3fbea212b5d --- /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 0000000000000..49f8f2935e5d6 --- /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 0000000000000..e780e93ca4efb --- /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` + ${({ 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(); + 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 ( +
+
{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 0000000000000000000000000000000000000000..30c9e07b0ccae4a12fda680d3b3b111b65230c99 GIT binary patch literal 23099 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 literal 0 HcmV?d00001 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..202063c13c5f8 --- /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 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..2ea1b94bdb554 --- /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 0000000000000..437641143cd3d --- /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 0000000000000..b13f2115ef819 --- /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 0000000000000..b6bfaa2d98446 --- /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 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 f37a9155e1d71..ba419e64d2597 100644 --- a/superset-frontend/src/visualizations/presets/MainPreset.js +++ b/superset-frontend/src/visualizations/presets/MainPreset.js @@ -79,6 +79,7 @@ import { import { PivotTableChartPlugin as PivotTableChartPluginV2 } from '@superset-ui/plugin-chart-pivot-table'; import { HandlebarsChartPlugin } from '@superset-ui/plugin-chart-handlebars'; import FilterBoxChartPlugin from '../FilterBox/FilterBoxChartPlugin'; +import { PopKPIPlugin } from '@superset-ui/plugin-chart-period-over-period-kpi'; import TimeTableChartPlugin from '../TimeTable'; export default class MainPreset extends Preset { @@ -88,6 +89,11 @@ export default class MainPreset extends Preset { ) ? [new GroupByFilterPlugin().configure({ key: 'filter_groupby' })] : []; + const experimentalChartPlugins = isFeatureEnabled( + FeatureFlag.CHART_PLUGINS_EXPERIMENTAL, + ) + ? [new PopKPIPlugin().configure({ key: 'pop_kpi' })] + : []; super({ name: 'Legacy charts', @@ -167,6 +173,7 @@ export default class MainPreset extends Preset { new HandlebarsChartPlugin().configure({ key: 'handlebars' }), new EchartsBubbleChartPlugin().configure({ key: 'bubble_v2' }), ...experimentalplugins, + ...experimentalChartPlugins, ], }); } diff --git a/superset/config.py b/superset/config.py index 348baef5454af..9bf120debd3f2 100644 --- a/superset/config.py +++ b/superset/config.py @@ -507,6 +507,8 @@ class D3Format(TypedDict, total=False): # 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, } # ------------------------------