diff --git a/RESOURCES/FEATURE_FLAGS.md b/RESOURCES/FEATURE_FLAGS.md index 8700bcb98..0aa5aca9c 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 e61729a2a..2e8441220 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 5fdd5cc91..f42f7fb23 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 4f6fece61..f21d20650 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 000000000..2e3fbea21 --- /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 000000000..49f8f2935 --- /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 000000000..e780e93ca --- /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 000000000..30c9e07b0 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 000000000..e9fe3ec78 --- /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 000000000..202063c13 --- /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 000000000..82379745f --- /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 000000000..2ea1b94bd --- /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 000000000..437641143 --- /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 000000000..b13f2115e --- /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 000000000..b6bfaa2d9 --- /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 000000000..a273f3a2b --- /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 e96b528c9..98ee67a5d 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 8b57244d2..d0d519236 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, } # ------------------------------