chore(explore): Migrate BigNumber to v1 api [ID-28][ID-55] (#17587)

* chore(explore): Migrate BigNumber to v1 api

* Move to echarts

* Use Echarts trendline

* Fix imports

* Fix parsing dates as strings

* Add from_dttm and to_dttm to v1 chart response

* Fix post processing

* Fix timeRangeFixed

* Fix tests

* Remove from and to dttm from cache

* Cleanup date formatting

* Fix storybook

* Fix missing types

* Fix timestamp with timezone

* Add types to demo's tsconfig

* bug fix

* fix import

* Fix cypress tests

* add sort

* add resample to handle missing values properly

* Sync ChartDataResponseResult schema with ts interface

* Lint fix

* Add migration

* Fix migration

* Remove pass

* Re-raise the exception in migration

* Typo fix

* Update revision

Co-authored-by: Ville Brofeldt <ville.v.brofeldt@gmail.com>
This commit is contained in:
Kamil Gabryjelski 2021-12-15 10:15:14 +01:00 committed by GitHub
parent 142b5bc506
commit 124af4c566
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 765 additions and 583 deletions

View File

@ -16,6 +16,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { interceptChart } from 'cypress/utils';
describe('Visualization > Big Number with Trendline', () => { describe('Visualization > Big Number with Trendline', () => {
const BIG_NUMBER_FORM_DATA = { const BIG_NUMBER_FORM_DATA = {
datasource: '2__table', datasource: '2__table',
@ -42,21 +44,21 @@ describe('Visualization > Big Number with Trendline', () => {
function verify(formData) { function verify(formData) {
cy.visitChartByParams(JSON.stringify(formData)); cy.visitChartByParams(JSON.stringify(formData));
cy.verifySliceSuccess({ cy.verifySliceSuccess({
waitAlias: '@getJson', waitAlias: '@chartData',
chartSelector: '.superset-legacy-chart-big-number', chartSelector: '.superset-legacy-chart-big-number',
}); });
} }
beforeEach(() => { beforeEach(() => {
cy.login(); cy.login();
cy.intercept('POST', '/superset/explore_json/**').as('getJson'); interceptChart({ legacy: false }).as('chartData');
}); });
it('should work', () => { it('should work', () => {
verify(BIG_NUMBER_FORM_DATA); verify(BIG_NUMBER_FORM_DATA);
cy.get('.chart-container .header-line'); cy.get('.chart-container .header-line');
cy.get('.chart-container .subheader-line'); cy.get('.chart-container .subheader-line');
cy.get('.chart-container svg path.vx-linepath'); cy.get('.chart-container canvas');
}); });
it('should work without subheader', () => { it('should work without subheader', () => {
@ -66,7 +68,7 @@ describe('Visualization > Big Number with Trendline', () => {
}); });
cy.get('.chart-container .header-line'); cy.get('.chart-container .header-line');
cy.get('.chart-container .subheader-line').should('not.exist'); cy.get('.chart-container .subheader-line').should('not.exist');
cy.get('.chart-container svg path.vx-linepath'); cy.get('.chart-container canvas');
}); });
it('should not render trendline when hidden', () => { it('should not render trendline when hidden', () => {
@ -76,6 +78,6 @@ describe('Visualization > Big Number with Trendline', () => {
}); });
cy.get('[data-test="chart-container"] .header-line'); cy.get('[data-test="chart-container"] .header-line');
cy.get('[data-test="chart-container"] .subheader-line'); cy.get('[data-test="chart-container"] .subheader-line');
cy.get('[data-test="chart-container"] svg').should('not.exist'); cy.get('[data-test="chart-container"] canvas').should('not.exist');
}); });
}); });

View File

@ -16,6 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { interceptChart } from 'cypress/utils';
import { FORM_DATA_DEFAULTS, NUM_METRIC } from './shared.helper'; import { FORM_DATA_DEFAULTS, NUM_METRIC } from './shared.helper';
describe('Visualization > Big Number Total', () => { describe('Visualization > Big Number Total', () => {
@ -26,15 +27,15 @@ describe('Visualization > Big Number Total', () => {
beforeEach(() => { beforeEach(() => {
cy.login(); cy.login();
cy.intercept('POST', '/superset/explore_json/**').as('getJson'); interceptChart({ legacy: false }).as('chartData');
}); });
it('Test big number chart with adhoc metric', () => { it('Test big number chart with adhoc metric', () => {
const formData = { ...BIG_NUMBER_DEFAULTS, metric: NUM_METRIC }; const formData = { ...BIG_NUMBER_DEFAULTS, metric: NUM_METRIC };
cy.visitChartByParams(JSON.stringify(formData)); cy.visitChartByParams(formData);
cy.verifySliceSuccess({ cy.verifySliceSuccess({
waitAlias: '@getJson', waitAlias: '@chartData',
querySubstring: NUM_METRIC.label, querySubstring: NUM_METRIC.label,
}); });
}); });
@ -58,8 +59,8 @@ describe('Visualization > Big Number Total', () => {
adhoc_filters: filters, adhoc_filters: filters,
}; };
cy.visitChartByParams(JSON.stringify(formData)); cy.visitChartByParams(formData);
cy.verifySliceSuccess({ waitAlias: '@getJson' }); cy.verifySliceSuccess({ waitAlias: '@chartData' });
}); });
it('Test big number chart ignores groupby', () => { it('Test big number chart ignores groupby', () => {
@ -69,11 +70,11 @@ describe('Visualization > Big Number Total', () => {
groupby: ['state'], groupby: ['state'],
}; };
cy.visitChartByParams(JSON.stringify(formData)); cy.visitChartByParams(formData);
cy.wait(['@getJson']).then(async ({ response }) => { cy.wait(['@chartData']).then(async ({ response }) => {
cy.verifySliceContainer(); cy.verifySliceContainer();
const responseBody = response?.body; const responseBody = response?.body;
expect(responseBody.query).not.contains(formData.groupby[0]); expect(responseBody.result[0].query).not.contains(formData.groupby[0]);
}); });
}); });
}); });

View File

@ -40,6 +40,8 @@ const V1_PLUGINS = [
'word_cloud', 'word_cloud',
'pie', 'pie',
'table', 'table',
'big_number',
'big_number_total',
]; ];
export const DASHBOARD_CHART_ALIAS_PREFIX = 'getChartData_'; export const DASHBOARD_CHART_ALIAS_PREFIX = 'getChartData_';

View File

@ -41,7 +41,6 @@
"@superset-ui/legacy-plugin-chart-sunburst": "^0.18.25", "@superset-ui/legacy-plugin-chart-sunburst": "^0.18.25",
"@superset-ui/legacy-plugin-chart-treemap": "^0.18.25", "@superset-ui/legacy-plugin-chart-treemap": "^0.18.25",
"@superset-ui/legacy-plugin-chart-world-map": "^0.18.25", "@superset-ui/legacy-plugin-chart-world-map": "^0.18.25",
"@superset-ui/legacy-preset-chart-big-number": "^0.18.25",
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.13", "@superset-ui/legacy-preset-chart-deckgl": "^0.4.13",
"@superset-ui/legacy-preset-chart-nvd3": "^0.18.25", "@superset-ui/legacy-preset-chart-nvd3": "^0.18.25",
"@superset-ui/plugin-chart-echarts": "^0.18.25", "@superset-ui/plugin-chart-echarts": "^0.18.25",
@ -198,6 +197,7 @@
"@types/redux-localstorage": "^1.0.8", "@types/redux-localstorage": "^1.0.8",
"@types/redux-mock-store": "^1.0.2", "@types/redux-mock-store": "^1.0.2",
"@types/rison": "0.0.6", "@types/rison": "0.0.6",
"@types/shortid": "^0.0.29",
"@types/sinon": "^9.0.5", "@types/sinon": "^9.0.5",
"@types/yargs": "12 - 15", "@types/yargs": "12 - 15",
"@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/eslint-plugin": "^5.3.0",
@ -21125,10 +21125,6 @@
"resolved": "plugins/legacy-plugin-chart-world-map", "resolved": "plugins/legacy-plugin-chart-world-map",
"link": true "link": true
}, },
"node_modules/@superset-ui/legacy-preset-chart-big-number": {
"resolved": "plugins/legacy-preset-chart-big-number",
"link": true
},
"node_modules/@superset-ui/legacy-preset-chart-deckgl": { "node_modules/@superset-ui/legacy-preset-chart-deckgl": {
"resolved": "plugins/legacy-preset-chart-deckgl", "resolved": "plugins/legacy-preset-chart-deckgl",
"link": true "link": true
@ -22433,7 +22429,8 @@
"node_modules/@types/shortid": { "node_modules/@types/shortid": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz",
"integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=" "integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=",
"dev": true
}, },
"node_modules/@types/sinon": { "node_modules/@types/sinon": {
"version": "9.0.5", "version": "9.0.5",
@ -60726,7 +60723,6 @@
"@superset-ui/legacy-plugin-chart-time-table": "0.18.25", "@superset-ui/legacy-plugin-chart-time-table": "0.18.25",
"@superset-ui/legacy-plugin-chart-treemap": "0.18.25", "@superset-ui/legacy-plugin-chart-treemap": "0.18.25",
"@superset-ui/legacy-plugin-chart-world-map": "0.18.25", "@superset-ui/legacy-plugin-chart-world-map": "0.18.25",
"@superset-ui/legacy-preset-chart-big-number": "0.18.25",
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.13", "@superset-ui/legacy-preset-chart-deckgl": "^0.4.13",
"@superset-ui/legacy-preset-chart-nvd3": "0.18.25", "@superset-ui/legacy-preset-chart-nvd3": "0.18.25",
"@superset-ui/plugin-chart-echarts": "0.18.25", "@superset-ui/plugin-chart-echarts": "0.18.25",
@ -61502,6 +61498,7 @@
"plugins/legacy-preset-chart-big-number": { "plugins/legacy-preset-chart-big-number": {
"name": "@superset-ui/legacy-preset-chart-big-number", "name": "@superset-ui/legacy-preset-chart-big-number",
"version": "0.18.25", "version": "0.18.25",
"extraneous": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@data-ui/xy-chart": "^0.0.84", "@data-ui/xy-chart": "^0.0.84",
@ -77854,7 +77851,6 @@
"@superset-ui/legacy-plugin-chart-time-table": "0.18.25", "@superset-ui/legacy-plugin-chart-time-table": "0.18.25",
"@superset-ui/legacy-plugin-chart-treemap": "0.18.25", "@superset-ui/legacy-plugin-chart-treemap": "0.18.25",
"@superset-ui/legacy-plugin-chart-world-map": "0.18.25", "@superset-ui/legacy-plugin-chart-world-map": "0.18.25",
"@superset-ui/legacy-preset-chart-big-number": "0.18.25",
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.13", "@superset-ui/legacy-preset-chart-deckgl": "^0.4.13",
"@superset-ui/legacy-preset-chart-nvd3": "0.18.25", "@superset-ui/legacy-preset-chart-nvd3": "0.18.25",
"@superset-ui/plugin-chart-echarts": "0.18.25", "@superset-ui/plugin-chart-echarts": "0.18.25",
@ -78481,18 +78477,6 @@
} }
} }
}, },
"@superset-ui/legacy-preset-chart-big-number": {
"version": "file:plugins/legacy-preset-chart-big-number",
"requires": {
"@data-ui/xy-chart": "^0.0.84",
"@superset-ui/chart-controls": "0.18.25",
"@superset-ui/core": "0.18.25",
"@types/d3-color": "^1.2.2",
"@types/shortid": "^0.0.29",
"d3-color": "^1.2.3",
"shortid": "^2.2.14"
}
},
"@superset-ui/legacy-preset-chart-deckgl": { "@superset-ui/legacy-preset-chart-deckgl": {
"version": "file:plugins/legacy-preset-chart-deckgl", "version": "file:plugins/legacy-preset-chart-deckgl",
"requires": { "requires": {
@ -79940,7 +79924,8 @@
"@types/shortid": { "@types/shortid": {
"version": "0.0.29", "version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz",
"integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=" "integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=",
"dev": true
}, },
"@types/sinon": { "@types/sinon": {
"version": "9.0.5", "version": "9.0.5",

View File

@ -101,7 +101,6 @@
"@superset-ui/legacy-plugin-chart-sunburst": "^0.18.25", "@superset-ui/legacy-plugin-chart-sunburst": "^0.18.25",
"@superset-ui/legacy-plugin-chart-treemap": "^0.18.25", "@superset-ui/legacy-plugin-chart-treemap": "^0.18.25",
"@superset-ui/legacy-plugin-chart-world-map": "^0.18.25", "@superset-ui/legacy-plugin-chart-world-map": "^0.18.25",
"@superset-ui/legacy-preset-chart-big-number": "^0.18.25",
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.13", "@superset-ui/legacy-preset-chart-deckgl": "^0.4.13",
"@superset-ui/legacy-preset-chart-nvd3": "^0.18.25", "@superset-ui/legacy-preset-chart-nvd3": "^0.18.25",
"@superset-ui/plugin-chart-echarts": "^0.18.25", "@superset-ui/plugin-chart-echarts": "^0.18.25",
@ -258,6 +257,7 @@
"@types/redux-localstorage": "^1.0.8", "@types/redux-localstorage": "^1.0.8",
"@types/redux-mock-store": "^1.0.2", "@types/redux-mock-store": "^1.0.2",
"@types/rison": "0.0.6", "@types/rison": "0.0.6",
"@types/shortid": "^0.0.29",
"@types/sinon": "^9.0.5", "@types/sinon": "^9.0.5",
"@types/yargs": "12 - 15", "@types/yargs": "12 - 15",
"@typescript-eslint/eslint-plugin": "^5.3.0", "@typescript-eslint/eslint-plugin": "^5.3.0",

View File

@ -50,7 +50,7 @@ export interface ChartDataResponseResult {
annotation_data: AnnotationData[] | null; annotation_data: AnnotationData[] | null;
cache_key: string | null; cache_key: string | null;
cache_timeout: number | null; cache_timeout: number | null;
cache_dttm: string | null; cached_dttm: string | null;
/** /**
* Array of data records as dictionary * Array of data records as dictionary
*/ */
@ -76,6 +76,8 @@ export interface ChartDataResponseResult {
| 'scheduled' | 'scheduled'
| 'success' | 'success'
| 'timed_out'; | 'timed_out';
from_dttm: number | null;
to_dttm: number | null;
} }
export interface TimeseriesChartDataResponseResult export interface TimeseriesChartDataResponseResult

View File

@ -61,7 +61,6 @@
"@superset-ui/legacy-plugin-chart-time-table": "0.18.25", "@superset-ui/legacy-plugin-chart-time-table": "0.18.25",
"@superset-ui/legacy-plugin-chart-treemap": "0.18.25", "@superset-ui/legacy-plugin-chart-treemap": "0.18.25",
"@superset-ui/legacy-plugin-chart-world-map": "0.18.25", "@superset-ui/legacy-plugin-chart-world-map": "0.18.25",
"@superset-ui/legacy-preset-chart-big-number": "0.18.25",
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.13", "@superset-ui/legacy-preset-chart-deckgl": "^0.4.13",
"@superset-ui/legacy-preset-chart-nvd3": "0.18.25", "@superset-ui/legacy-preset-chart-nvd3": "0.18.25",
"@superset-ui/plugin-chart-echarts": "0.18.25", "@superset-ui/plugin-chart-echarts": "0.18.25",

View File

@ -18,7 +18,7 @@
*/ */
import React from 'react'; import React from 'react';
import { SuperChart } from '@superset-ui/core'; import { SuperChart } from '@superset-ui/core';
import { BigNumberChartPlugin } from '@superset-ui/legacy-preset-chart-big-number'; import { BigNumberChartPlugin } from '@superset-ui/plugin-chart-echarts';
import testData from './data'; import testData from './data';
new BigNumberChartPlugin().configure({ key: 'big-number' }).register(); new BigNumberChartPlugin().configure({ key: 'big-number' }).register();
@ -56,7 +56,7 @@ function withNulls(origData: object[], nullPosition = 3) {
} }
export default { export default {
title: 'Legacy Chart Plugins/legacy-preset-big-number/BigNumber', title: 'Legacy Chart Plugins/legacy-preset-big-number/BigNumberWithTrendline',
}; };
export const basicWithTrendline = () => ( export const basicWithTrendline = () => (

View File

@ -18,7 +18,7 @@
*/ */
import React from 'react'; import React from 'react';
import { SuperChart } from '@superset-ui/core'; import { SuperChart } from '@superset-ui/core';
import { BigNumberTotalChartPlugin } from '@superset-ui/legacy-preset-chart-big-number'; import { BigNumberTotalChartPlugin } from '@superset-ui/plugin-chart-echarts';
import data from './data'; import data from './data';
new BigNumberTotalChartPlugin() new BigNumberTotalChartPlugin()

View File

@ -25,7 +25,7 @@ import {
ChartDataProvider, ChartDataProvider,
SupersetClient, SupersetClient,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { BigNumberChartPlugin as LegacyBigNumberPlugin } from '@superset-ui/legacy-preset-chart-big-number'; import { BigNumberChartPlugin } from '@superset-ui/plugin-chart-echarts';
import LegacySankeyPlugin from '@superset-ui/legacy-plugin-chart-sankey'; import LegacySankeyPlugin from '@superset-ui/legacy-plugin-chart-sankey';
import LegacySunburstPlugin from '@superset-ui/legacy-plugin-chart-sunburst'; import LegacySunburstPlugin from '@superset-ui/legacy-plugin-chart-sunburst';
import { WordCloudChartPlugin } from '@superset-ui/plugin-chart-word-cloud'; import { WordCloudChartPlugin } from '@superset-ui/plugin-chart-word-cloud';
@ -46,7 +46,7 @@ const SUNBURST = sunburstFormData.viz_type;
const WORD_CLOUD_LEGACY = wordCloudFormData.viz_type; const WORD_CLOUD_LEGACY = wordCloudFormData.viz_type;
const WORD_CLOUD = 'new_word_cloud'; const WORD_CLOUD = 'new_word_cloud';
new LegacyBigNumberPlugin().configure({ key: BIG_NUMBER }).register(); new BigNumberChartPlugin().configure({ key: BIG_NUMBER }).register();
// eslint-disable-next-line // eslint-disable-next-line
new LegacySankeyPlugin().configure({ key: SANKEY }).register(); new LegacySankeyPlugin().configure({ key: SANKEY }).register();
// eslint-disable-next-line // eslint-disable-next-line

View File

@ -16,5 +16,6 @@
"storybook", "storybook",
"../**/src", "../**/src",
"../../plugins/**/src", "../../plugins/**/src",
"../../plugins/**/types",
] ]
} }

View File

@ -1,67 +0,0 @@
<!--
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.
-->
## @superset-ui/legacy-preset-chart-big-number
[![Version](https://img.shields.io/npm/v/@superset-ui/legacy-preset-chart-big-number.svg?style=flat-square)](https://www.npmjs.com/package/@superset-ui/legacy-preset-chart-big-number)
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui-plugins.svg?path=packages%2Fsuperset-ui-legacy-preset-chart-big-number&style=flat-square)](https://david-dm.org/apache-superset/superset-ui-plugins?path=plugins/superset-ui-legacy-preset-chart-big-number)
This plugin provides Big Number for Superset.
### Usage
Import the preset and register. This will register the `BigNumber` and `BigNumberTotal` charts with
key `big-number` and `big-number-total`, respectively.
```js
import { BigNumberChartPreset } from '@superset-ui/legacy-preset-chart-big-number';
new BigNumberChartPreset().register();
```
or register charts one by one. Configure `key`, which can be any `string`, and register the plugin.
This `key` will be used to lookup this chart throughout the app.
```js
import {
BigNumberChartPlugin,
BigNumberTotalChartPlugin,
} from '@superset-ui/legacy-preset-chart-big-number';
new BigNumberChartPlugin().configure({ key: 'big-number' }).register();
new BigNumberTotalChartPlugin()
.configure({ key: 'big-number-total' })
.register();
```
Then use it via `SuperChart`. See
[storybook](https://apache-superset.github.io/superset-ui-plugins/?selectedKind=plugin-chart-big-number)
for more details.
```js
<SuperChart
chartType="big-number"
width={600}
height={600}
formData={...}
queriesData={[{
data: {...},
}]}
/>
```

View File

@ -1,42 +0,0 @@
{
"name": "@superset-ui/legacy-preset-chart-big-number",
"version": "0.18.25",
"description": "Superset Legacy Chart - Big Number",
"sideEffects": [
"*.css"
],
"main": "lib/index.js",
"module": "esm/index.js",
"files": [
"esm",
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/apache-superset/superset-ui.git"
},
"keywords": [
"superset"
],
"author": "Superset",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/apache-superset/superset-ui/issues"
},
"homepage": "https://github.com/apache-superset/superset-ui#readme",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@data-ui/xy-chart": "^0.0.84",
"@superset-ui/chart-controls": "0.18.25",
"@superset-ui/core": "0.18.25",
"@types/d3-color": "^1.2.2",
"@types/shortid": "^0.0.29",
"d3-color": "^1.2.3",
"shortid": "^2.2.14"
},
"peerDependencies": {
"react": "^15 || ^16"
}
}

View File

@ -1,187 +0,0 @@
/**
* 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 * as color from 'd3-color';
import {
extractTimegrain,
getNumberFormatter,
getTimeFormatter,
getTimeFormatterForGranularity,
NumberFormats,
ChartProps,
LegacyQueryData,
QueryFormData,
smartDateFormatter,
} from '@superset-ui/core';
const TIME_COLUMN = '__timestamp';
const formatPercentChange = getNumberFormatter(
NumberFormats.PERCENT_SIGNED_1_POINT,
);
// we trust both the x (time) and y (big number) to be numeric
export interface BigNumberDatum {
[key: string]: number | null;
}
export type BigNumberFormData = QueryFormData & {
colorPicker?: {
r: number;
g: number;
b: number;
};
metric?:
| {
label: string;
}
| string;
compareLag?: string | number;
yAxisFormat?: string;
};
export type BigNumberChartProps = ChartProps & {
formData: BigNumberFormData;
queriesData: (LegacyQueryData & {
data?: BigNumberDatum[];
})[];
};
export default function transformProps(chartProps: BigNumberChartProps) {
const { width, height, queriesData, formData, rawFormData } = chartProps;
const {
colorPicker,
compareLag: compareLag_,
compareSuffix = '',
timeFormat,
headerFontSize,
metric = 'value',
showTimestamp,
showTrendLine,
startYAxisAtZero,
subheader = '',
subheaderFontSize,
vizType,
timeRangeFixed = false,
} = formData;
const granularity = extractTimegrain(rawFormData as QueryFormData);
let { yAxisFormat } = formData;
const { headerFormatSelector, headerTimestampFormat } = formData;
const {
data = [],
from_dttm: fromDatetime,
to_dttm: toDatetime,
} = queriesData[0];
const metricName = typeof metric === 'string' ? metric : metric.label;
const compareLag = Number(compareLag_) || 0;
const supportTrendLine = vizType === 'big_number';
const supportAndShowTrendLine = supportTrendLine && showTrendLine;
let formattedSubheader = subheader;
let mainColor;
if (colorPicker) {
const { r, g, b } = colorPicker;
mainColor = color.rgb(r, g, b).hex();
}
let trendLineData;
let percentChange = 0;
let bigNumber = data.length === 0 ? null : data[0][metricName];
let timestamp = data.length === 0 ? null : data[0][TIME_COLUMN];
let bigNumberFallback;
if (data.length > 0) {
const sortedData = (data as BigNumberDatum[])
.map(d => ({ x: d[TIME_COLUMN], y: d[metricName] }))
// sort in time descending order
.sort((a, b) => (a.x !== null && b.x !== null ? b.x - a.x : 0));
bigNumber = sortedData[0].y;
timestamp = sortedData[0].x;
if (bigNumber === null) {
bigNumberFallback = sortedData.find(d => d.y !== null);
bigNumber = bigNumberFallback ? bigNumberFallback.y : null;
timestamp = bigNumberFallback ? bigNumberFallback.x : null;
}
if (compareLag > 0) {
const compareIndex = compareLag;
if (compareIndex < sortedData.length) {
const compareValue = sortedData[compareIndex].y;
// compare values must both be non-nulls
if (bigNumber !== null && compareValue !== null && compareValue !== 0) {
percentChange = (bigNumber - compareValue) / Math.abs(compareValue);
formattedSubheader = `${formatPercentChange(
percentChange,
)} ${compareSuffix}`;
}
}
}
if (supportTrendLine) {
// must reverse to ascending order otherwise it confuses tooltip triggers
sortedData.reverse();
trendLineData = supportAndShowTrendLine ? sortedData : undefined;
}
}
let className = '';
if (percentChange > 0) {
className = 'positive';
} else if (percentChange < 0) {
className = 'negative';
}
if (!yAxisFormat && chartProps.datasource && chartProps.datasource.metrics) {
chartProps.datasource.metrics.forEach(metricEntry => {
if (metricEntry.metric_name === metric && metricEntry.d3format) {
yAxisFormat = metricEntry.d3format;
}
});
}
const headerFormatter = headerFormatSelector
? getTimeFormatter(headerTimestampFormat)
: getNumberFormatter(yAxisFormat);
const formatTime =
timeFormat === smartDateFormatter.id
? getTimeFormatterForGranularity(granularity)
: getTimeFormatter(timeFormat);
return {
width,
height,
bigNumber,
bigNumberFallback,
className,
headerFormatter,
formatTime,
headerFontSize,
subheaderFontSize,
mainColor,
showTimestamp,
showTrendLine: supportAndShowTrendLine,
startYAxisAtZero,
subheader: formattedSubheader,
timestamp,
trendLineData,
fromDatetime,
toDatetime,
timeRangeFixed,
};
}

View File

@ -1,19 +0,0 @@
{
"compilerOptions": {
"composite": false,
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"extends": "../../../tsconfig.json",
"include": [
"**/*",
"../types/**/*",
"../../../types/**/*"
],
"references": [
{
"path": ".."
}
]
}

View File

@ -1,25 +0,0 @@
{
"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

@ -16,6 +16,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
declare module '@data-ui/xy-chart'; import { buildQueryContext, QueryFormData } from '@superset-ui/core';
declare module '*.png';
declare module '*.jpg'; export default function buildQuery(formData: QueryFormData) {
return buildQueryContext(formData, baseQueryObject => [baseQueryObject]);
}

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { t } from '@superset-ui/core'; import { smartDateFormatter, t } from '@superset-ui/core';
import { import {
ControlPanelConfig, ControlPanelConfig,
D3_FORMAT_DOCS, D3_FORMAT_DOCS,
@ -27,7 +27,7 @@ import { headerFontSize, subheaderFontSize } from '../sharedControls';
export default { export default {
controlPanelSections: [ controlPanelSections: [
sections.legacyRegularTime, sections.legacyTimeseriesTime,
{ {
label: t('Query'), label: t('Query'),
expanded: true, expanded: true,
@ -51,44 +51,45 @@ export default {
}, },
}, },
], ],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
[headerFontSize],
[subheaderFontSize],
['y_axis_format'], ['y_axis_format'],
[ [
{ {
name: 'header_format_selector', name: 'time_format',
config: {
type: 'CheckboxControl',
label: t('Timestamp Format'),
renderTrigger: true,
default: false,
description: t('Whether to format the timestamp'),
},
},
],
[
{
name: 'header_timestamp_format',
config: { config: {
type: 'SelectControl', type: 'SelectControl',
freeForm: true, freeForm: true,
label: t('Date format'), label: t('Date format'),
renderTrigger: true, renderTrigger: true,
choices: D3_TIME_FORMAT_OPTIONS, choices: D3_TIME_FORMAT_OPTIONS,
default: '%d-%m-%Y %H:%M:%S',
description: D3_FORMAT_DOCS, description: D3_FORMAT_DOCS,
visibility(props) { default: smartDateFormatter.id,
const { header_format_selector } = props.form_data; },
return !!header_format_selector; },
}, ],
[
{
name: 'force_timestamp_formatting',
config: {
type: 'CheckboxControl',
label: t('Force date format'),
renderTrigger: true,
default: false,
description: t(
'Use date formatting even when metric value is not a timestamp',
),
}, },
}, },
], ],
], ],
}, },
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [[headerFontSize], [subheaderFontSize]],
},
], ],
controlOverrides: { controlOverrides: {
y_axis_format: { y_axis_format: {

View File

@ -18,13 +18,12 @@
*/ */
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core'; import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
import controlPanel from './controlPanel'; import controlPanel from './controlPanel';
import transformProps, { import transformProps from './transformProps';
BigNumberChartProps, import buildQuery from './buildQuery';
BigNumberFormData,
} from '../BigNumber/transformProps';
import example1 from './images/BigNumber.jpg'; import example1 from './images/BigNumber.jpg';
import example2 from './images/BigNumber2.jpg'; import example2 from './images/BigNumber2.jpg';
import thumbnail from './images/thumbnail.png'; import thumbnail from './images/thumbnail.png';
import { BigNumberTotalChartProps, BigNumberTotalFormData } from '../types';
const metadata = new ChartMetadata({ const metadata = new ChartMetadata({
category: t('KPI'), category: t('KPI'),
@ -47,17 +46,17 @@ const metadata = new ChartMetadata({
t('Description'), t('Description'),
], ],
thumbnail, thumbnail,
useLegacyApi: true,
}); });
export default class BigNumberTotalChartPlugin extends ChartPlugin< export default class BigNumberTotalChartPlugin extends ChartPlugin<
BigNumberFormData, BigNumberTotalFormData,
BigNumberChartProps BigNumberTotalChartProps
> { > {
constructor() { constructor() {
super({ super({
loadChart: () => import('../BigNumber/BigNumber'), loadChart: () => import('../BigNumberViz'),
metadata, metadata,
buildQuery,
transformProps, transformProps,
controlPanel, controlPanel,
}); });

View File

@ -0,0 +1,76 @@
/**
* 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 {
getNumberFormatter,
GenericDataType,
getMetricLabel,
extractTimegrain,
QueryFormData,
} from '@superset-ui/core';
import { BigNumberTotalChartProps } from '../types';
import { getDateFormatter, parseMetricValue } from '../utils';
export default function transformProps(chartProps: BigNumberTotalChartProps) {
const { width, height, queriesData, formData, rawFormData } = chartProps;
const {
headerFontSize,
metric = 'value',
subheader = '',
subheaderFontSize,
forceTimestampFormatting,
timeFormat,
yAxisFormat,
} = formData;
const { data = [], coltypes = [] } = queriesData[0];
const granularity = extractTimegrain(rawFormData as QueryFormData);
const metricName = getMetricLabel(metric);
const formattedSubheader = subheader;
const bigNumber =
data.length === 0 ? null : parseMetricValue(data[0][metricName]);
let metricEntry;
if (chartProps.datasource && chartProps.datasource.metrics) {
metricEntry = chartProps.datasource.metrics.find(
metricItem => metricItem.metric_name === metric,
);
}
const formatTime = getDateFormatter(
timeFormat,
granularity,
metricEntry?.d3format,
);
const headerFormatter =
coltypes[0] === GenericDataType.TEMPORAL ||
coltypes[0] === GenericDataType.STRING ||
forceTimestampFormatting
? formatTime
: getNumberFormatter(yAxisFormat ?? metricEntry?.d3format ?? undefined);
return {
width,
height,
bigNumber,
headerFormatter,
headerFontSize,
subheaderFontSize,
subheader: formattedSubheader,
};
}

View File

@ -17,7 +17,6 @@
* under the License. * under the License.
*/ */
import React from 'react'; import React from 'react';
import shortid from 'shortid';
import { import {
t, t,
getNumberFormatter, getNumberFormatter,
@ -28,22 +27,12 @@ import {
BRAND_COLOR, BRAND_COLOR,
styled, styled,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { import { EChartsCoreOption } from 'echarts';
XYChart, import Echart from '../components/Echart';
AreaSeries, import { TimeSeriesDatum } from './types';
CrossHair,
LinearGradient,
} from '@data-ui/xy-chart';
const defaultNumberFormatter = getNumberFormatter(); const defaultNumberFormatter = getNumberFormatter();
const CHART_MARGIN = {
top: 4,
right: 4,
bottom: 4,
left: 4,
};
const PROPORTION = { const PROPORTION = {
// text size: proportion of the chart container sans trendline // text size: proportion of the chart container sans trendline
KICKER: 0.1, KICKER: 0.1,
@ -53,32 +42,6 @@ const PROPORTION = {
TRENDLINE: 0.3, TRENDLINE: 0.3,
}; };
type TimeSeriesDatum = {
x: number; // timestamp as a number
y: number | null;
};
export function renderTooltipFactory(
formatDate = smartDateVerboseFormatter,
formatValue = defaultNumberFormatter,
) {
return function renderTooltip({
datum: { x, y },
}: {
datum: TimeSeriesDatum;
}) {
// even though `formatDate` supports timestamp as numbers, we need
// `new Date` to pass type check
return (
<div style={{ padding: '4px 8px' }}>
{formatDate(new Date(x))}
<br />
<strong>{y === null ? t('N/A') : formatValue(y)}</strong>
</div>
);
};
}
type BigNumberVisProps = { type BigNumberVisProps = {
className?: string; className?: string;
width: number; width: number;
@ -87,8 +50,6 @@ type BigNumberVisProps = {
bigNumberFallback?: TimeSeriesDatum; bigNumberFallback?: TimeSeriesDatum;
headerFormatter: NumberFormatter | TimeFormatter; headerFormatter: NumberFormatter | TimeFormatter;
formatTime: TimeFormatter; formatTime: TimeFormatter;
fromDatetime?: number;
toDatetime?: number;
headerFontSize: number; headerFontSize: number;
kickerFontSize: number; kickerFontSize: number;
subheader: string; subheader: string;
@ -100,11 +61,10 @@ type BigNumberVisProps = {
timestamp?: number; timestamp?: number;
trendLineData?: TimeSeriesDatum[]; trendLineData?: TimeSeriesDatum[];
mainColor: string; mainColor: string;
echartOptions: EChartsCoreOption;
}; };
class BigNumberVis extends React.PureComponent<BigNumberVisProps, {}> { class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
private gradientId: string = shortid.generate();
static defaultProps = { static defaultProps = {
className: '', className: '',
headerFormatter: defaultNumberFormatter, headerFormatter: defaultNumberFormatter,
@ -146,7 +106,7 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps, {}> {
role="alert" role="alert"
title={t( title={t(
`Last available value seen on %s`, `Last available value seen on %s`,
formatTime(bigNumberFallback.x), formatTime(bigNumberFallback[0]),
)} )}
> >
{t('Not up to date')} {t('Not up to date')}
@ -254,79 +214,19 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps, {}> {
} }
renderTrendline(maxHeight: number) { renderTrendline(maxHeight: number) {
const { const { width, trendLineData, echartOptions } = this.props;
width,
trendLineData,
mainColor,
subheader,
startYAxisAtZero,
headerFormatter,
formatTime,
fromDatetime,
timeRangeFixed,
} = this.props;
// if can't find any non-null values, no point rendering the trendline // if can't find any non-null values, no point rendering the trendline
if (!trendLineData?.some(d => d.y !== null)) { if (!trendLineData?.some(d => d[1] !== null)) {
return null; return null;
} }
// Apply a fixed X range if a time range is specified.
//
// XYChart checks the existence of `domain` property and decide whether to
// apply a domain or not, so it must not be `null` or `undefined`
const xScale: { type: string; domain?: number[] } = { type: 'timeUtc' };
const tooltipData = trendLineData && [...trendLineData];
if (tooltipData && timeRangeFixed && fromDatetime) {
const toDatetime = this.props.toDatetime ?? Date.now();
if (tooltipData[0].x > fromDatetime) {
tooltipData.unshift({
x: fromDatetime,
y: null,
});
}
if (tooltipData[tooltipData.length - 1].x < toDatetime) {
tooltipData.push({
x: toDatetime,
y: null,
});
}
xScale.domain = [fromDatetime, toDatetime];
}
return ( return (
<XYChart <Echart
snapTooltipToDataX
ariaLabel={`Big number visualization ${subheader}`}
// headerFormatter always NumberFormatter in BigNumber with treadline
renderTooltip={renderTooltipFactory(
formatTime,
headerFormatter as NumberFormatter,
)}
xScale={xScale}
yScale={{
type: 'linear',
includeZero: startYAxisAtZero,
}}
width={Math.floor(width)} width={Math.floor(width)}
height={maxHeight} height={maxHeight}
margin={CHART_MARGIN} echartOptions={echartOptions}
eventTrigger="container" />
>
<LinearGradient id={this.gradientId} from={mainColor} to="#fff" />
<AreaSeries
data={tooltipData}
fill={`url(#${this.gradientId})`}
stroke={mainColor}
/>
<CrossHair
fullHeight
stroke={mainColor}
circleFill={mainColor}
circleStroke="#fff"
showHorizontalLine={false}
strokeDasharray="5,2"
/>
</XYChart>
); );
} }

View File

@ -0,0 +1,92 @@
/**
* 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,
DTTM_ALIAS,
PostProcessingResample,
QueryFormData,
} from '@superset-ui/core';
import {
rollingWindowOperator,
TIME_COLUMN,
} from '@superset-ui/chart-controls';
const TIME_GRAIN_MAP: Record<string, string> = {
PT1S: 'S',
PT1M: 'min',
PT5M: '5min',
PT10M: '10min',
PT15M: '15min',
PT30M: '30min',
PT1H: 'H',
P1D: 'D',
P1M: 'M',
P3M: 'Q',
P1Y: 'A',
// TODO: these need to be mapped carefully, as the first day of week
// can vary from engine to engine
// P1W: 'W',
// '1969-12-28T00:00:00Z/P1W': 'W',
// '1969-12-29T00:00:00Z/P1W': 'W',
// 'P1W/1970-01-03T00:00:00Z': 'W',
// 'P1W/1970-01-04T00:00:00Z': 'W',
};
export default function buildQuery(formData: QueryFormData) {
return buildQueryContext(formData, baseQueryObject => {
const rollingProc = rollingWindowOperator(formData, baseQueryObject);
if (rollingProc) {
rollingProc.options = { ...rollingProc.options, is_pivot_df: false };
}
const { time_grain_sqla } = formData;
let resampleProc: PostProcessingResample | undefined;
if (rollingProc && time_grain_sqla) {
const rule = TIME_GRAIN_MAP[time_grain_sqla];
if (rule) {
resampleProc = {
operation: 'resample',
options: {
method: 'asfreq',
rule,
fill_value: null,
time_column: TIME_COLUMN,
},
};
}
}
return [
{
...baseQueryObject,
is_timeseries: true,
post_processing: [
{
operation: 'sort',
options: {
columns: {
[DTTM_ALIAS]: true,
},
},
},
resampleProc,
rollingProc,
],
},
];
});
}

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { t } from '@superset-ui/core'; import { smartDateFormatter, t } from '@superset-ui/core';
import { import {
ControlPanelConfig, ControlPanelConfig,
D3_FORMAT_DOCS, D3_FORMAT_DOCS,
@ -63,20 +63,6 @@ const config: ControlPanelConfig = {
}, },
}, },
], ],
['y_axis_format'],
[
{
name: 'time_format',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Timestamp format'),
renderTrigger: true,
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
},
},
],
[ [
{ {
name: 'show_timestamp', name: 'show_timestamp',
@ -142,6 +128,35 @@ const config: ControlPanelConfig = {
['color_picker', null], ['color_picker', null],
[headerFontSize], [headerFontSize],
[subheaderFontSize], [subheaderFontSize],
['y_axis_format'],
[
{
name: 'time_format',
config: {
type: 'SelectControl',
freeForm: true,
label: t('Date format'),
renderTrigger: true,
choices: D3_TIME_FORMAT_OPTIONS,
description: D3_FORMAT_DOCS,
default: smartDateFormatter.id,
},
},
],
[
{
name: 'force_timestamp_formatting',
config: {
type: 'CheckboxControl',
label: t('Force date format'),
renderTrigger: true,
default: false,
description: t(
'Use date formatting even when metric value is not a timestamp',
),
},
},
],
], ],
}, },
{ {

View File

@ -18,12 +18,14 @@
*/ */
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core'; import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
import controlPanel from './controlPanel'; import controlPanel from './controlPanel';
import transformProps, { import transformProps from './transformProps';
BigNumberChartProps, import buildQuery from './buildQuery';
BigNumberFormData,
} from './transformProps';
import example from './images/Big_Number_Trendline.jpg'; import example from './images/Big_Number_Trendline.jpg';
import thumbnail from './images/thumbnail.png'; import thumbnail from './images/thumbnail.png';
import {
BigNumberWithTrendlineChartProps,
BigNumberWithTrendlineFormData,
} from '../types';
const metadata = new ChartMetadata({ const metadata = new ChartMetadata({
category: t('KPI'), category: t('KPI'),
@ -43,17 +45,17 @@ const metadata = new ChartMetadata({
t('Trend'), t('Trend'),
], ],
thumbnail, thumbnail,
useLegacyApi: true,
}); });
export default class BigNumberChartPlugin extends ChartPlugin< export default class BigNumberWithTrendlineChartPlugin extends ChartPlugin<
BigNumberFormData, BigNumberWithTrendlineFormData,
BigNumberChartProps BigNumberWithTrendlineChartProps
> { > {
constructor() { constructor() {
super({ super({
loadChart: () => import('./BigNumber'), loadChart: () => import('../BigNumberViz'),
metadata, metadata,
buildQuery,
transformProps, transformProps,
controlPanel, controlPanel,
}); });

View File

@ -0,0 +1,252 @@
/**
* 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 {
extractTimegrain,
getNumberFormatter,
NumberFormats,
QueryFormData,
GenericDataType,
getMetricLabel,
t,
smartDateVerboseFormatter,
NumberFormatter,
TimeFormatter,
} from '@superset-ui/core';
import { EChartsCoreOption, graphic } from 'echarts';
import {
BigNumberDatum,
BigNumberWithTrendlineChartProps,
TimeSeriesDatum,
} from '../types';
import { getDateFormatter, parseMetricValue } from '../utils';
const defaultNumberFormatter = getNumberFormatter();
export function renderTooltipFactory(
formatDate: TimeFormatter = smartDateVerboseFormatter,
formatValue: NumberFormatter | TimeFormatter = defaultNumberFormatter,
) {
return function renderTooltip(params: { data: TimeSeriesDatum }[]) {
return `
${formatDate(params[0].data[0])}
<br />
<strong>
${
params[0].data[1] === null ? t('N/A') : formatValue(params[0].data[1])
}
</strong>
`;
};
}
const TIME_COLUMN = '__timestamp';
const formatPercentChange = getNumberFormatter(
NumberFormats.PERCENT_SIGNED_1_POINT,
);
export default function transformProps(
chartProps: BigNumberWithTrendlineChartProps,
) {
const { width, height, queriesData, formData, rawFormData } = chartProps;
const {
colorPicker,
compareLag: compareLag_,
compareSuffix = '',
timeFormat,
headerFontSize,
metric = 'value',
showTimestamp,
showTrendLine,
startYAxisAtZero,
subheader = '',
subheaderFontSize,
forceTimestampFormatting,
yAxisFormat,
timeRangeFixed,
} = formData;
const granularity = extractTimegrain(rawFormData as QueryFormData);
const {
data = [],
colnames = [],
coltypes = [],
from_dttm: fromDatetime,
to_dttm: toDatetime,
} = queriesData[0];
const metricName = getMetricLabel(metric);
const compareLag = Number(compareLag_) || 0;
let formattedSubheader = subheader;
const { r, g, b } = colorPicker;
const mainColor = `rgb(${r}, ${g}, ${b})`;
let trendLineData;
let percentChange = 0;
let bigNumber = data.length === 0 ? null : data[0][metricName];
let timestamp = data.length === 0 ? null : data[0][TIME_COLUMN];
let bigNumberFallback;
const metricColtypeIndex = colnames.findIndex(name => name === metricName);
const metricColtype =
metricColtypeIndex > -1 ? coltypes[metricColtypeIndex] : null;
if (data.length > 0) {
const sortedData = (data as BigNumberDatum[])
.map(d => [d[TIME_COLUMN], parseMetricValue(d[metricName])])
// sort in time descending order
.sort((a, b) => (a[0] !== null && b[0] !== null ? b[0] - a[0] : 0));
bigNumber = sortedData[0][1];
timestamp = sortedData[0][0];
if (bigNumber === null) {
bigNumberFallback = sortedData.find(d => d[1] !== null);
bigNumber = bigNumberFallback ? bigNumberFallback[1] : null;
timestamp = bigNumberFallback ? bigNumberFallback[0] : null;
}
if (compareLag > 0) {
const compareIndex = compareLag;
if (compareIndex < sortedData.length) {
const compareValue = sortedData[compareIndex][1];
// compare values must both be non-nulls
if (bigNumber !== null && compareValue !== null && compareValue !== 0) {
percentChange = (bigNumber - compareValue) / Math.abs(compareValue);
formattedSubheader = `${formatPercentChange(
percentChange,
)} ${compareSuffix}`;
}
}
}
sortedData.reverse();
trendLineData = showTrendLine ? sortedData : undefined;
}
let className = '';
if (percentChange > 0) {
className = 'positive';
} else if (percentChange < 0) {
className = 'negative';
}
let metricEntry;
if (chartProps.datasource && chartProps.datasource.metrics) {
metricEntry = chartProps.datasource.metrics.find(
metricEntry => metricEntry.metric_name === metric,
);
}
const formatTime = getDateFormatter(
timeFormat,
granularity,
metricEntry?.d3format,
);
const headerFormatter =
metricColtype === GenericDataType.TEMPORAL ||
metricColtype === GenericDataType.STRING ||
forceTimestampFormatting
? formatTime
: getNumberFormatter(yAxisFormat ?? metricEntry?.d3format ?? undefined);
if (trendLineData && timeRangeFixed && fromDatetime) {
const toDatetimeOrToday = toDatetime ?? Date.now();
if (!trendLineData[0][0] || trendLineData[0][0] > fromDatetime) {
trendLineData.unshift([fromDatetime, null]);
}
if (
!trendLineData[trendLineData.length - 1][0] ||
trendLineData[trendLineData.length - 1][0]! < toDatetimeOrToday
) {
trendLineData.push([toDatetimeOrToday, null]);
}
}
const echartOptions: EChartsCoreOption = trendLineData
? {
series: [
{
data: trendLineData,
type: 'line',
smooth: true,
symbol: 'circle',
showSymbol: false,
color: mainColor,
areaStyle: {
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: mainColor,
},
{
offset: 1,
color: 'white',
},
]),
},
},
],
xAxis: {
min: trendLineData[0][0],
max: trendLineData[trendLineData.length - 1][0],
show: false,
type: 'value',
},
yAxis: {
scale: !startYAxisAtZero,
show: false,
},
grid: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
tooltip: {
show: true,
trigger: 'axis',
confine: true,
formatter: renderTooltipFactory(formatTime, headerFormatter),
},
aria: {
enabled: true,
label: {
description: `Big number visualization ${subheader}`,
},
},
}
: {};
return {
width,
height,
bigNumber,
bigNumberFallback,
className,
headerFormatter,
formatTime,
headerFontSize,
subheaderFontSize,
mainColor,
showTimestamp,
showTrendLine,
startYAxisAtZero,
subheader: formattedSubheader,
timestamp,
trendLineData,
echartOptions,
};
}

View File

@ -17,6 +17,5 @@
* under the License. * under the License.
*/ */
export { default as BigNumberChartPlugin } from './BigNumber/index'; export { default as BigNumberChartPlugin } from './BigNumberWithTrendline';
export { default as BigNumberTotalChartPlugin } from './BigNumberTotal/index'; export { default as BigNumberTotalChartPlugin } from './BigNumberTotal';
export { default as BigNumberChartPreset } from './preset';

View File

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
// These are control configurations that are shared ONLY within the BigNumber viz plugin repo. // These are control configurations that are shared ONLY within the BigNumberWithTrendline viz plugin repo.
import { t } from '@superset-ui/core'; import { t } from '@superset-ui/core';
import { CustomControlItem } from '@superset-ui/chart-controls'; import { CustomControlItem } from '@superset-ui/chart-controls';

View File

@ -0,0 +1,57 @@
/**
* 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 {
ChartDataResponseResult,
ChartProps,
QueryFormData,
QueryFormMetric,
} from '@superset-ui/core';
export interface BigNumberDatum {
[key: string]: number | null;
}
export type BigNumberTotalFormData = QueryFormData & {
metric?: QueryFormMetric;
yAxisFormat?: string;
forceTimestampFormatting?: boolean;
};
export type BigNumberWithTrendlineFormData = BigNumberTotalFormData & {
colorPicker: {
r: number;
g: number;
b: number;
};
compareLag?: string | number;
};
export type BigNumberTotalChartProps = ChartProps & {
formData: BigNumberTotalFormData;
queriesData: (ChartDataResponseResult & {
data?: BigNumberDatum[];
})[];
};
export type BigNumberWithTrendlineChartProps = BigNumberTotalChartProps & {
formData: BigNumberWithTrendlineFormData;
};
export type TimeSeriesDatum = [number, number | null];

View File

@ -16,18 +16,31 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { Preset } from '@superset-ui/core';
import BigNumberChartPlugin from './BigNumber';
import BigNumberTotalChartPlugin from './BigNumberTotal';
export default class BigNumberChartPreset extends Preset { import moment from 'moment';
constructor() { import {
super({ getTimeFormatter,
name: 'BigNumber charts', getTimeFormatterForGranularity,
plugins: [ smartDateFormatter,
new BigNumberChartPlugin().configure({ key: 'big_number' }), TimeGranularity,
new BigNumberTotalChartPlugin().configure({ key: 'big_number_total' }), } 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 null;
} }
} return metricValue;
};
export const getDateFormatter = (
timeFormat: string,
granularity?: TimeGranularity,
fallbackFormat?: string | null,
) =>
timeFormat === smartDateFormatter.id
? getTimeFormatterForGranularity(granularity)
: getTimeFormatter(timeFormat ?? fallbackFormat);

View File

@ -32,6 +32,7 @@ export { default as EchartsRadarChartPlugin } from './Radar';
export { default as EchartsFunnelChartPlugin } from './Funnel'; export { default as EchartsFunnelChartPlugin } from './Funnel';
export { default as EchartsTreeChartPlugin } from './Tree'; export { default as EchartsTreeChartPlugin } from './Tree';
export { default as EchartsTreemapChartPlugin } from './Treemap'; export { default as EchartsTreemapChartPlugin } from './Treemap';
export { BigNumberChartPlugin, BigNumberTotalChartPlugin } from './BigNumber';
export { default as BoxPlotTransformProps } from './BoxPlot/transformProps'; export { default as BoxPlotTransformProps } from './BoxPlot/transformProps';
export { default as FunnelTransformProps } from './Funnel/transformProps'; export { default as FunnelTransformProps } from './Funnel/transformProps';

View File

@ -17,10 +17,11 @@
* under the License. * under the License.
*/ */
import { DatasourceType, TimeGranularity } from '@superset-ui/core'; import { DatasourceType, TimeGranularity } from '@superset-ui/core';
import transformProps, { import transformProps from '../../src/BigNumber/BigNumberWithTrendline/transformProps';
BignumberChartProps, import {
BigNumberDatum, BigNumberDatum,
} from '../src/BigNumber/transformProps'; BigNumberWithTrendlineChartProps,
} from '../../src/BigNumber/types';
const formData = { const formData = {
metric: 'value', metric: 'value',
@ -33,8 +34,9 @@ const formData = {
compareLag: 1, compareLag: 1,
timeGrainSqla: 'P3M' as TimeGranularity, timeGrainSqla: 'P3M' as TimeGranularity,
compareSuffix: 'over last quarter', compareSuffix: 'over last quarter',
vizType: 'big_number', viz_type: 'big_number',
yAxisFormat: '.3s', yAxisFormat: '.3s',
datasource: 'test_datasource',
}; };
const rawFormData = { const rawFormData = {
@ -56,7 +58,7 @@ function generateProps(
data: BigNumberDatum[], data: BigNumberDatum[],
extraFormData = {}, extraFormData = {},
extraQueryData = {}, extraQueryData = {},
): BignumberChartProps { ): BigNumberWithTrendlineChartProps {
return { return {
width: 200, width: 200,
height: 500, height: 500,
@ -84,10 +86,13 @@ function generateProps(
...extraQueryData, ...extraQueryData,
}, },
], ],
ownState: {},
filterState: {},
behaviors: [],
}; };
} }
describe('BigNumber', () => { describe('BigNumberWithTrendline', () => {
const props = generateProps( const props = generateProps(
[ [
{ {
@ -109,8 +114,8 @@ describe('BigNumber', () => {
const lastDatum = transformed.trendLineData?.pop(); const lastDatum = transformed.trendLineData?.pop();
// should use last available value // should use last available value
expect(lastDatum?.x).toStrictEqual(100); expect(lastDatum?.[0]).toStrictEqual(100);
expect(lastDatum?.y).toBeNull(); expect(lastDatum?.[1]).toBeNull();
// should note this is a fallback // should note this is a fallback
expect(transformed.bigNumber).toStrictEqual(1.2345); expect(transformed.bigNumber).toStrictEqual(1.2345);

View File

@ -68,7 +68,7 @@ const basicChartProps = {
const basicQueryResult: ChartDataResponseResult = { const basicQueryResult: ChartDataResponseResult = {
annotation_data: null, annotation_data: null,
cache_key: null, cache_key: null,
cache_dttm: null, cached_dttm: null,
cache_timeout: null, cache_timeout: null,
data: [], data: [],
colnames: [], colnames: [],

View File

@ -17,10 +17,6 @@
* under the License. * under the License.
*/ */
import { isFeatureEnabled, Preset, FeatureFlag } from '@superset-ui/core'; import { isFeatureEnabled, Preset, FeatureFlag } from '@superset-ui/core';
import {
BigNumberChartPlugin,
BigNumberTotalChartPlugin,
} from '@superset-ui/legacy-preset-chart-big-number';
import CalendarChartPlugin from '@superset-ui/legacy-plugin-chart-calendar'; import CalendarChartPlugin from '@superset-ui/legacy-plugin-chart-calendar';
import ChordChartPlugin from '@superset-ui/legacy-plugin-chart-chord'; import ChordChartPlugin from '@superset-ui/legacy-plugin-chart-chord';
import CountryMapChartPlugin from '@superset-ui/legacy-plugin-chart-country-map'; import CountryMapChartPlugin from '@superset-ui/legacy-plugin-chart-country-map';
@ -54,6 +50,8 @@ import {
} from '@superset-ui/legacy-preset-chart-nvd3'; } from '@superset-ui/legacy-preset-chart-nvd3';
import { DeckGLChartPreset } from '@superset-ui/legacy-preset-chart-deckgl'; import { DeckGLChartPreset } from '@superset-ui/legacy-preset-chart-deckgl';
import { import {
BigNumberChartPlugin,
BigNumberTotalChartPlugin,
EchartsPieChartPlugin, EchartsPieChartPlugin,
EchartsBoxPlotChartPlugin, EchartsBoxPlotChartPlugin,
EchartsAreaChartPlugin, EchartsAreaChartPlugin,

View File

@ -1240,12 +1240,22 @@ class ChartDataResponseResult(Schema):
description="Amount of rows in result set", allow_none=False, description="Amount of rows in result set", allow_none=False,
) )
data = fields.List(fields.Dict(), description="A list with results") data = fields.List(fields.Dict(), description="A list with results")
colnames = fields.List(fields.String(), description="A list of column names")
coltypes = fields.List(
fields.Integer(), description="A list of generic data types of each column"
)
applied_filters = fields.List( applied_filters = fields.List(
fields.Dict(), description="A list with applied filters" fields.Dict(), description="A list with applied filters"
) )
rejected_filters = fields.List( rejected_filters = fields.List(
fields.Dict(), description="A list with rejected filters" fields.Dict(), description="A list with rejected filters"
) )
from_dttm = fields.Integer(
desciption="Start timestamp of time range", required=False, allow_none=True
)
to_dttm = fields.Integer(
desciption="End timestamp of time range", required=False, allow_none=True
)
class ChartDataResponseSchema(Schema): class ChartDataResponseSchema(Schema):

View File

@ -144,6 +144,8 @@ class QueryContextProcessor:
"status": cache.status, "status": cache.status,
"stacktrace": cache.stacktrace, "stacktrace": cache.stacktrace,
"rowcount": len(cache.df.index), "rowcount": len(cache.df.index),
"from_dttm": query_obj.from_dttm,
"to_dttm": query_obj.to_dttm,
} }
def query_cache_key(self, query_obj: QueryObject, **kwargs: Any) -> Optional[str]: def query_cache_key(self, query_obj: QueryObject, **kwargs: Any) -> Optional[str]:
@ -201,6 +203,8 @@ class QueryContextProcessor:
result.df = df result.df = df
result.query = query result.query = query
result.from_dttm = query_object.from_dttm
result.to_dttm = query_object.to_dttm
return result return result
def normalize_df(self, df: pd.DataFrame, query_object: QueryObject) -> pd.DataFrame: def normalize_df(self, df: pd.DataFrame, query_object: QueryObject) -> pd.DataFrame:

View File

@ -0,0 +1,100 @@
# 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.
"""rename_big_viz_total_form_data_fields
Revision ID: fe23025b9441
Revises: 3ba29ecbaac5
Create Date: 2021-12-13 14:06:24.426970
"""
# revision identifiers, used by Alembic.
revision = "fe23025b9441"
down_revision = "3ba29ecbaac5"
import json
import logging
from alembic import op
from sqlalchemy import Column, Integer, String, Text
from sqlalchemy.ext.declarative import declarative_base
from superset import db
Base = declarative_base()
logger = logging.getLogger("alembic")
class Slice(Base):
__tablename__ = "slices"
id = Column(Integer, primary_key=True)
params = Column(Text)
viz_type = Column(String(250))
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind)
slices = session.query(Slice).filter(Slice.viz_type == "big_number_total").all()
for slc in slices:
try:
params = json.loads(slc.params)
header_format_selector = params.pop("header_format_selector", None)
header_timestamp_format = params.pop("header_timestamp_format", None)
if header_format_selector:
params["force_timestamp_formatting"] = header_format_selector
if header_timestamp_format:
params["time_format"] = header_timestamp_format
slc.params = json.dumps(params, sort_keys=True)
except Exception as e:
logger.exception(
f"An error occurred: parsing params for slice {slc.id} failed."
f"You need to fix it before upgrading your DB."
)
raise e
session.commit()
session.close()
def downgrade():
bind = op.get_bind()
session = db.Session(bind=bind)
slices = session.query(Slice).filter(Slice.viz_type == "big_number_total").all()
for slc in slices:
try:
params = json.loads(slc.params)
time_format = params.pop("time_format", None)
force_timestamp_formatting = params.pop("force_timestamp_formatting", None)
if time_format:
params["header_timestamp_format"] = time_format
if force_timestamp_formatting:
params["header_format_selector"] = force_timestamp_formatting
slc.params = json.dumps(params, sort_keys=True)
except Exception as e:
logger.exception(
f"An error occurred: parsing params for slice {slc.id} failed. "
"You need to fix it before downgrading your DB."
)
raise e
session.commit()
session.close()

View File

@ -446,6 +446,8 @@ class QueryResult: # pylint: disable=too-few-public-methods
status: str = QueryStatus.SUCCESS, status: str = QueryStatus.SUCCESS,
error_message: Optional[str] = None, error_message: Optional[str] = None,
errors: Optional[List[Dict[str, Any]]] = None, errors: Optional[List[Dict[str, Any]]] = None,
from_dttm: Optional[datetime] = None,
to_dttm: Optional[datetime] = None,
) -> None: ) -> None:
self.df = df self.df = df
self.query = query self.query = query
@ -454,6 +456,8 @@ class QueryResult: # pylint: disable=too-few-public-methods
self.status = status self.status = status
self.error_message = error_message self.error_message = error_message
self.errors = errors or [] self.errors = errors or []
self.from_dttm = from_dttm
self.to_dttm = to_dttm
class ExtraJSONMixin: class ExtraJSONMixin: