feat: Period over Period Big Number comparison chart (#26908)

Co-authored-by: Fernando <frodriguez@bytecode.io>
Co-authored-by: Antonio Rivero <antonioriverocode@gmail.com>
This commit is contained in:
Elizabeth Thompson 2024-01-31 15:44:25 -08:00 committed by GitHub
parent bd9afcda99
commit a09e5557bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1066 additions and 1 deletions

View File

@ -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

View File

@ -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": {

View File

@ -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",

View File

@ -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',

View File

@ -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
```

View File

@ -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"
}
}

View File

@ -0,0 +1,96 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { createRef } from 'react';
import { css, styled, useTheme } from '@superset-ui/core';
import { PopKPIComparisonValueStyleProps, PopKPIProps } from './types';
const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
${({ theme, subheaderFontSize }) => `
font-weight: ${theme.typography.weights.light};
width: 33%;
display: table-cell;
font-size: ${subheaderFontSize || 20}px;
text-align: center;
`}
`;
export default function PopKPI(props: PopKPIProps) {
const {
height,
width,
bigNumber,
prevNumber,
valueDifference,
percentDifference,
headerFontSize,
subheaderFontSize,
} = props;
const rootElem = createRef<HTMLDivElement>();
const theme = useTheme();
const wrapperDivStyles = css`
font-family: ${theme.typography.families.sansSerif};
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
padding: ${theme.gridUnit * 4}px;
border-radius: ${theme.gridUnit * 2}px;
height: ${height}px;
width: ${width}px;
`;
const bigValueContainerStyles = css`
font-size: ${headerFontSize || 60}px;
font-weight: ${theme.typography.weights.normal};
text-align: center;
`;
return (
<div ref={rootElem} css={wrapperDivStyles}>
<div css={bigValueContainerStyles}>{bigNumber}</div>
<div
css={css`
width: 100%;
display: table;
`}
>
<div
css={css`
display: table-row;
`}
>
<ComparisonValue subheaderFontSize={subheaderFontSize}>
{' '}
#: {prevNumber}
</ComparisonValue>
<ComparisonValue subheaderFontSize={subheaderFontSize}>
{' '}
Δ: {valueDifference}
</ComparisonValue>
<ComparisonValue subheaderFontSize={subheaderFontSize}>
{' '}
%: {percentDifference}
</ComparisonValue>
</div>
</div>
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -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
*/

View File

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

View File

@ -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;

View File

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

View File

@ -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,
};
}

View File

@ -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;
};

View File

@ -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"
}
]
}

View File

@ -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;
}

View File

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

View File

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