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
* under the License.
*/
import { interceptChart } from 'cypress/utils';
describe('Visualization > Big Number with Trendline', () => {
const BIG_NUMBER_FORM_DATA = {
datasource: '2__table',
@ -42,21 +44,21 @@ describe('Visualization > Big Number with Trendline', () => {
function verify(formData) {
cy.visitChartByParams(JSON.stringify(formData));
cy.verifySliceSuccess({
waitAlias: '@getJson',
waitAlias: '@chartData',
chartSelector: '.superset-legacy-chart-big-number',
});
}
beforeEach(() => {
cy.login();
cy.intercept('POST', '/superset/explore_json/**').as('getJson');
interceptChart({ legacy: false }).as('chartData');
});
it('should work', () => {
verify(BIG_NUMBER_FORM_DATA);
cy.get('.chart-container .header-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', () => {
@ -66,7 +68,7 @@ describe('Visualization > Big Number with Trendline', () => {
});
cy.get('.chart-container .header-line');
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', () => {
@ -76,6 +78,6 @@ describe('Visualization > Big Number with Trendline', () => {
});
cy.get('[data-test="chart-container"] .header-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
* under the License.
*/
import { interceptChart } from 'cypress/utils';
import { FORM_DATA_DEFAULTS, NUM_METRIC } from './shared.helper';
describe('Visualization > Big Number Total', () => {
@ -26,15 +27,15 @@ describe('Visualization > Big Number Total', () => {
beforeEach(() => {
cy.login();
cy.intercept('POST', '/superset/explore_json/**').as('getJson');
interceptChart({ legacy: false }).as('chartData');
});
it('Test big number chart with adhoc metric', () => {
const formData = { ...BIG_NUMBER_DEFAULTS, metric: NUM_METRIC };
cy.visitChartByParams(JSON.stringify(formData));
cy.visitChartByParams(formData);
cy.verifySliceSuccess({
waitAlias: '@getJson',
waitAlias: '@chartData',
querySubstring: NUM_METRIC.label,
});
});
@ -58,8 +59,8 @@ describe('Visualization > Big Number Total', () => {
adhoc_filters: filters,
};
cy.visitChartByParams(JSON.stringify(formData));
cy.verifySliceSuccess({ waitAlias: '@getJson' });
cy.visitChartByParams(formData);
cy.verifySliceSuccess({ waitAlias: '@chartData' });
});
it('Test big number chart ignores groupby', () => {
@ -69,11 +70,11 @@ describe('Visualization > Big Number Total', () => {
groupby: ['state'],
};
cy.visitChartByParams(JSON.stringify(formData));
cy.wait(['@getJson']).then(async ({ response }) => {
cy.visitChartByParams(formData);
cy.wait(['@chartData']).then(async ({ response }) => {
cy.verifySliceContainer();
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',
'pie',
'table',
'big_number',
'big_number_total',
];
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-treemap": "^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-nvd3": "^0.18.25",
"@superset-ui/plugin-chart-echarts": "^0.18.25",
@ -198,6 +197,7 @@
"@types/redux-localstorage": "^1.0.8",
"@types/redux-mock-store": "^1.0.2",
"@types/rison": "0.0.6",
"@types/shortid": "^0.0.29",
"@types/sinon": "^9.0.5",
"@types/yargs": "12 - 15",
"@typescript-eslint/eslint-plugin": "^5.3.0",
@ -21125,10 +21125,6 @@
"resolved": "plugins/legacy-plugin-chart-world-map",
"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": {
"resolved": "plugins/legacy-preset-chart-deckgl",
"link": true
@ -22433,7 +22429,8 @@
"node_modules/@types/shortid": {
"version": "0.0.29",
"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": {
"version": "9.0.5",
@ -60726,7 +60723,6 @@
"@superset-ui/legacy-plugin-chart-time-table": "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-preset-chart-big-number": "0.18.25",
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.13",
"@superset-ui/legacy-preset-chart-nvd3": "0.18.25",
"@superset-ui/plugin-chart-echarts": "0.18.25",
@ -61502,6 +61498,7 @@
"plugins/legacy-preset-chart-big-number": {
"name": "@superset-ui/legacy-preset-chart-big-number",
"version": "0.18.25",
"extraneous": true,
"license": "Apache-2.0",
"dependencies": {
"@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-treemap": "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-nvd3": "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": {
"version": "file:plugins/legacy-preset-chart-deckgl",
"requires": {
@ -79940,7 +79924,8 @@
"@types/shortid": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz",
"integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps="
"integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=",
"dev": true
},
"@types/sinon": {
"version": "9.0.5",

View File

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

View File

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

View File

@ -61,7 +61,6 @@
"@superset-ui/legacy-plugin-chart-time-table": "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-preset-chart-big-number": "0.18.25",
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.13",
"@superset-ui/legacy-preset-chart-nvd3": "0.18.25",
"@superset-ui/plugin-chart-echarts": "0.18.25",

View File

@ -18,7 +18,7 @@
*/
import React from 'react';
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';
new BigNumberChartPlugin().configure({ key: 'big-number' }).register();
@ -56,7 +56,7 @@ function withNulls(origData: object[], nullPosition = 3) {
}
export default {
title: 'Legacy Chart Plugins/legacy-preset-big-number/BigNumber',
title: 'Legacy Chart Plugins/legacy-preset-big-number/BigNumberWithTrendline',
};
export const basicWithTrendline = () => (

View File

@ -18,7 +18,7 @@
*/
import React from 'react';
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';
new BigNumberTotalChartPlugin()

View File

@ -25,7 +25,7 @@ import {
ChartDataProvider,
SupersetClient,
} 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 LegacySunburstPlugin from '@superset-ui/legacy-plugin-chart-sunburst';
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 = 'new_word_cloud';
new LegacyBigNumberPlugin().configure({ key: BIG_NUMBER }).register();
new BigNumberChartPlugin().configure({ key: BIG_NUMBER }).register();
// eslint-disable-next-line
new LegacySankeyPlugin().configure({ key: SANKEY }).register();
// eslint-disable-next-line

View File

@ -16,5 +16,6 @@
"storybook",
"../**/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
* under the License.
*/
declare module '@data-ui/xy-chart';
declare module '*.png';
declare module '*.jpg';
import { buildQueryContext, QueryFormData } from '@superset-ui/core';
export default function buildQuery(formData: QueryFormData) {
return buildQueryContext(formData, baseQueryObject => [baseQueryObject]);
}

View File

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

View File

@ -18,13 +18,12 @@
*/
import { t, ChartMetadata, ChartPlugin } from '@superset-ui/core';
import controlPanel from './controlPanel';
import transformProps, {
BigNumberChartProps,
BigNumberFormData,
} from '../BigNumber/transformProps';
import transformProps from './transformProps';
import buildQuery from './buildQuery';
import example1 from './images/BigNumber.jpg';
import example2 from './images/BigNumber2.jpg';
import thumbnail from './images/thumbnail.png';
import { BigNumberTotalChartProps, BigNumberTotalFormData } from '../types';
const metadata = new ChartMetadata({
category: t('KPI'),
@ -47,17 +46,17 @@ const metadata = new ChartMetadata({
t('Description'),
],
thumbnail,
useLegacyApi: true,
});
export default class BigNumberTotalChartPlugin extends ChartPlugin<
BigNumberFormData,
BigNumberChartProps
BigNumberTotalFormData,
BigNumberTotalChartProps
> {
constructor() {
super({
loadChart: () => import('../BigNumber/BigNumber'),
loadChart: () => import('../BigNumberViz'),
metadata,
buildQuery,
transformProps,
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.
*/
import React from 'react';
import shortid from 'shortid';
import {
t,
getNumberFormatter,
@ -28,22 +27,12 @@ import {
BRAND_COLOR,
styled,
} from '@superset-ui/core';
import {
XYChart,
AreaSeries,
CrossHair,
LinearGradient,
} from '@data-ui/xy-chart';
import { EChartsCoreOption } from 'echarts';
import Echart from '../components/Echart';
import { TimeSeriesDatum } from './types';
const defaultNumberFormatter = getNumberFormatter();
const CHART_MARGIN = {
top: 4,
right: 4,
bottom: 4,
left: 4,
};
const PROPORTION = {
// text size: proportion of the chart container sans trendline
KICKER: 0.1,
@ -53,32 +42,6 @@ const PROPORTION = {
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 = {
className?: string;
width: number;
@ -87,8 +50,6 @@ type BigNumberVisProps = {
bigNumberFallback?: TimeSeriesDatum;
headerFormatter: NumberFormatter | TimeFormatter;
formatTime: TimeFormatter;
fromDatetime?: number;
toDatetime?: number;
headerFontSize: number;
kickerFontSize: number;
subheader: string;
@ -100,11 +61,10 @@ type BigNumberVisProps = {
timestamp?: number;
trendLineData?: TimeSeriesDatum[];
mainColor: string;
echartOptions: EChartsCoreOption;
};
class BigNumberVis extends React.PureComponent<BigNumberVisProps, {}> {
private gradientId: string = shortid.generate();
class BigNumberVis extends React.PureComponent<BigNumberVisProps> {
static defaultProps = {
className: '',
headerFormatter: defaultNumberFormatter,
@ -146,7 +106,7 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps, {}> {
role="alert"
title={t(
`Last available value seen on %s`,
formatTime(bigNumberFallback.x),
formatTime(bigNumberFallback[0]),
)}
>
{t('Not up to date')}
@ -254,79 +214,19 @@ class BigNumberVis extends React.PureComponent<BigNumberVisProps, {}> {
}
renderTrendline(maxHeight: number) {
const {
width,
trendLineData,
mainColor,
subheader,
startYAxisAtZero,
headerFormatter,
formatTime,
fromDatetime,
timeRangeFixed,
} = this.props;
const { width, trendLineData, echartOptions } = this.props;
// 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;
}
// 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 (
<XYChart
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,
}}
<Echart
width={Math.floor(width)}
height={maxHeight}
margin={CHART_MARGIN}
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>
echartOptions={echartOptions}
/>
);
}

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
* under the License.
*/
import { t } from '@superset-ui/core';
import { smartDateFormatter, t } from '@superset-ui/core';
import {
ControlPanelConfig,
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',
@ -142,6 +128,35 @@ const config: ControlPanelConfig = {
['color_picker', null],
[headerFontSize],
[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 controlPanel from './controlPanel';
import transformProps, {
BigNumberChartProps,
BigNumberFormData,
} from './transformProps';
import transformProps from './transformProps';
import buildQuery from './buildQuery';
import example from './images/Big_Number_Trendline.jpg';
import thumbnail from './images/thumbnail.png';
import {
BigNumberWithTrendlineChartProps,
BigNumberWithTrendlineFormData,
} from '../types';
const metadata = new ChartMetadata({
category: t('KPI'),
@ -43,17 +45,17 @@ const metadata = new ChartMetadata({
t('Trend'),
],
thumbnail,
useLegacyApi: true,
});
export default class BigNumberChartPlugin extends ChartPlugin<
BigNumberFormData,
BigNumberChartProps
export default class BigNumberWithTrendlineChartPlugin extends ChartPlugin<
BigNumberWithTrendlineFormData,
BigNumberWithTrendlineChartProps
> {
constructor() {
super({
loadChart: () => import('./BigNumber'),
loadChart: () => import('../BigNumberViz'),
metadata,
buildQuery,
transformProps,
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.
*/
export { default as BigNumberChartPlugin } from './BigNumber/index';
export { default as BigNumberTotalChartPlugin } from './BigNumberTotal/index';
export { default as BigNumberChartPreset } from './preset';
export { default as BigNumberChartPlugin } from './BigNumberWithTrendline';
export { default as BigNumberTotalChartPlugin } from './BigNumberTotal';

View File

@ -17,7 +17,7 @@
* 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 { 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
* under the License.
*/
import { Preset } from '@superset-ui/core';
import BigNumberChartPlugin from './BigNumber';
import BigNumberTotalChartPlugin from './BigNumberTotal';
export default class BigNumberChartPreset extends Preset {
constructor() {
super({
name: 'BigNumber charts',
plugins: [
new BigNumberChartPlugin().configure({ key: 'big_number' }),
new BigNumberTotalChartPlugin().configure({ key: 'big_number_total' }),
],
});
import moment from 'moment';
import {
getTimeFormatter,
getTimeFormatterForGranularity,
smartDateFormatter,
TimeGranularity,
} 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 EchartsTreeChartPlugin } from './Tree';
export { default as EchartsTreemapChartPlugin } from './Treemap';
export { BigNumberChartPlugin, BigNumberTotalChartPlugin } from './BigNumber';
export { default as BoxPlotTransformProps } from './BoxPlot/transformProps';
export { default as FunnelTransformProps } from './Funnel/transformProps';

View File

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

View File

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

View File

@ -17,10 +17,6 @@
* under the License.
*/
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 ChordChartPlugin from '@superset-ui/legacy-plugin-chart-chord';
import CountryMapChartPlugin from '@superset-ui/legacy-plugin-chart-country-map';
@ -54,6 +50,8 @@ import {
} from '@superset-ui/legacy-preset-chart-nvd3';
import { DeckGLChartPreset } from '@superset-ui/legacy-preset-chart-deckgl';
import {
BigNumberChartPlugin,
BigNumberTotalChartPlugin,
EchartsPieChartPlugin,
EchartsBoxPlotChartPlugin,
EchartsAreaChartPlugin,

View File

@ -1240,12 +1240,22 @@ class ChartDataResponseResult(Schema):
description="Amount of rows in result set", allow_none=False,
)
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(
fields.Dict(), description="A list with applied filters"
)
rejected_filters = fields.List(
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):

View File

@ -144,6 +144,8 @@ class QueryContextProcessor:
"status": cache.status,
"stacktrace": cache.stacktrace,
"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]:
@ -201,6 +203,8 @@ class QueryContextProcessor:
result.df = df
result.query = query
result.from_dttm = query_object.from_dttm
result.to_dttm = query_object.to_dttm
return result
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,
error_message: Optional[str] = None,
errors: Optional[List[Dict[str, Any]]] = None,
from_dttm: Optional[datetime] = None,
to_dttm: Optional[datetime] = None,
) -> None:
self.df = df
self.query = query
@ -454,6 +456,8 @@ class QueryResult: # pylint: disable=too-few-public-methods
self.status = status
self.error_message = error_message
self.errors = errors or []
self.from_dttm = from_dttm
self.to_dttm = to_dttm
class ExtraJSONMixin: