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'}), ], }); }