mirror of https://github.com/apache/superset.git
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:
parent
bd9afcda99
commit
a09e5557bc
|
@ -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
|
||||
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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
|
||||
*/
|
|
@ -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],
|
||||
};
|
||||
}
|
|
@ -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;
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
23
superset-frontend/plugins/plugin-chart-period-over-period-kpi/types/types/external.d.ts
vendored
Normal file
23
superset-frontend/plugins/plugin-chart-period-over-period-kpi/types/types/external.d.ts
vendored
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
# ------------------------------
|
||||
|
|
Loading…
Reference in New Issue