mirror of
https://github.com/apache/superset.git
synced 2024-09-19 12:09:42 -04:00
feat(plugin-chart-echarts): add support for formula annotations (#817)
* feat(plugin-chart-echarts): add support for formula annotations * address comments
This commit is contained in:
parent
85f9cdad0a
commit
06ef549122
@ -1,8 +1,11 @@
|
||||
/* eslint camelcase: 0 */
|
||||
|
||||
export type AnnotationOpacity = '' | 'opacityLow' | 'opacityMedium' | 'opacityHigh';
|
||||
|
||||
type BaseAnnotationLayer = {
|
||||
color?: string | null;
|
||||
name: string;
|
||||
opacity?: '' | 'opacityLow' | 'opacityMedium' | 'opacityHigh';
|
||||
opacity?: AnnotationOpacity;
|
||||
show: boolean;
|
||||
style: 'dashed' | 'dotted' | 'solid' | 'longDashed';
|
||||
width?: number;
|
||||
|
@ -29,7 +29,9 @@
|
||||
"@superset-ui/chart-controls": "0.15.10",
|
||||
"@superset-ui/core": "0.15.10",
|
||||
"@types/echarts": "^4.6.3",
|
||||
"echarts": "^4.9.0"
|
||||
"@types/mathjs": "^6.0.6",
|
||||
"echarts": "^4.9.0",
|
||||
"mathjs": "^7.5.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1"
|
||||
|
@ -60,9 +60,28 @@ const config: ControlPanelConfig = {
|
||||
['row_limit', null],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Annotations and Layers'),
|
||||
expanded: false,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
name: 'annotation_layers',
|
||||
config: {
|
||||
type: 'AnnotationLayerControl',
|
||||
label: '',
|
||||
default: [],
|
||||
description: 'Annotation Layers',
|
||||
renderTrigger: true,
|
||||
tabOverride: 'data',
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Predictive Analytics'),
|
||||
expanded: true,
|
||||
expanded: false,
|
||||
controlSetRows: [
|
||||
[
|
||||
{
|
||||
|
@ -41,6 +41,7 @@ export default class EchartsTimeseriesChartPlugin extends ChartPlugin {
|
||||
metadata: new ChartMetadata({
|
||||
credits: ['https://echarts.apache.org'],
|
||||
description: 'Time-series (Apache ECharts)',
|
||||
supportedAnnotationTypes: ['FORMULA'],
|
||||
name: t('Time-series Chart'),
|
||||
thumbnail,
|
||||
}),
|
||||
|
@ -17,15 +17,19 @@
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
AnnotationLayer,
|
||||
isFormulaAnnotationLayer,
|
||||
ChartProps,
|
||||
CategoricalColorNamespace,
|
||||
getNumberFormatter,
|
||||
smartDateVerboseFormatter,
|
||||
TimeseriesDataRecord,
|
||||
} from '@superset-ui/core';
|
||||
import { EchartsTimeseriesProps } from './types';
|
||||
import { ForecastSeriesEnum } from '../types';
|
||||
import { parseYAxisBound } from '../utils/controls';
|
||||
import { extractTimeseriesSeries } from '../utils/series';
|
||||
import { evalFormula, parseAnnotationOpacity } from '../utils/annotation';
|
||||
import {
|
||||
extractForecastSeriesContext,
|
||||
extractProphetValuesFromTooltipParams,
|
||||
@ -36,7 +40,9 @@ import { defaultGrid, defaultTooltip, defaultYAxis } from '../defaults';
|
||||
|
||||
export default function transformProps(chartProps: ChartProps): EchartsTimeseriesProps {
|
||||
const { width, height, formData, queryData } = chartProps;
|
||||
const { data = [] }: { data?: TimeseriesDataRecord[] } = queryData;
|
||||
const {
|
||||
annotationLayers = [],
|
||||
area,
|
||||
colorScheme,
|
||||
contributionMode,
|
||||
@ -55,10 +61,8 @@ export default function transformProps(chartProps: ChartProps): EchartsTimeserie
|
||||
} = formData;
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||
|
||||
const rebasedData = rebaseTimeseriesDatum(queryData.data || []);
|
||||
const rebasedData = rebaseTimeseriesDatum(data);
|
||||
const rawSeries = extractTimeseriesSeries(rebasedData);
|
||||
|
||||
const series: echarts.EChartOption.Series[] = [];
|
||||
const formatter = getNumberFormatter(contributionMode ? ',.0%' : yAxisFormat);
|
||||
|
||||
@ -114,6 +118,36 @@ export default function transformProps(chartProps: ChartProps): EchartsTimeserie
|
||||
});
|
||||
});
|
||||
|
||||
annotationLayers.forEach((layer: AnnotationLayer) => {
|
||||
const {
|
||||
name,
|
||||
color,
|
||||
opacity: annotationOpacity,
|
||||
width: annotationWidth,
|
||||
show: annotationShow,
|
||||
style,
|
||||
} = layer;
|
||||
if (annotationShow && isFormulaAnnotationLayer(layer)) {
|
||||
series.push({
|
||||
name,
|
||||
id: name,
|
||||
itemStyle: {
|
||||
color: color || colorFn(name),
|
||||
},
|
||||
lineStyle: {
|
||||
opacity: parseAnnotationOpacity(annotationOpacity),
|
||||
type: style,
|
||||
width: annotationWidth,
|
||||
},
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
// @ts-ignore
|
||||
data: evalFormula(layer, data),
|
||||
symbolSize: 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// yAxisBounds need to be parsed to replace incompatible values with undefined
|
||||
let [min, max] = (yAxisBounds || []).map(parseYAxisBound);
|
||||
|
||||
@ -169,7 +203,8 @@ export default function transformProps(chartProps: ChartProps): EchartsTimeserie
|
||||
entry =>
|
||||
extractForecastSeriesContext(entry.name || '').type === ForecastSeriesEnum.Observation,
|
||||
)
|
||||
.map(entry => entry.name || ''),
|
||||
.map(entry => entry.name || '')
|
||||
.concat(annotationLayers.map((layer: AnnotationLayer) => layer.name)),
|
||||
right: zoomable ? 80 : 'auto',
|
||||
},
|
||||
series,
|
||||
|
@ -0,0 +1,46 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
/**
|
||||
* 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 { AnnotationOpacity, FormulaAnnotationLayer, TimeseriesDataRecord } from '@superset-ui/core';
|
||||
import { parse as mathjsParse } from 'mathjs';
|
||||
|
||||
export function evalFormula(
|
||||
formula: FormulaAnnotationLayer,
|
||||
data: TimeseriesDataRecord[],
|
||||
): [Date, number][] {
|
||||
const { value } = formula;
|
||||
const node = mathjsParse(value);
|
||||
const func = node.compile();
|
||||
return data.map(row => {
|
||||
return [new Date(Number(row.__timestamp)), func.evaluate({ x: row.__timestamp }) as number];
|
||||
});
|
||||
}
|
||||
|
||||
export function parseAnnotationOpacity(opacity?: AnnotationOpacity): number {
|
||||
switch (opacity) {
|
||||
case 'opacityLow':
|
||||
return 0.2;
|
||||
case 'opacityMedium':
|
||||
return 0.5;
|
||||
case 'opacityHigh':
|
||||
return 0.8;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import 'babel-polyfill';
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
import { ChartProps, FormulaAnnotationLayer } from '@superset-ui/core';
|
||||
import transformProps from '../../src/Timeseries/transformProps';
|
||||
|
||||
describe('EchartsTimeseries tranformProps', () => {
|
||||
@ -69,4 +69,62 @@ describe('EchartsTimeseries tranformProps', () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should add a formula to viz', () => {
|
||||
const formula: FormulaAnnotationLayer = {
|
||||
name: 'My Formula',
|
||||
annotationType: 'FORMULA',
|
||||
value: 'x+1',
|
||||
style: 'solid',
|
||||
show: true,
|
||||
};
|
||||
const formulaChartProps = new ChartProps({
|
||||
formData: {
|
||||
...formData,
|
||||
annotationLayers: [formula],
|
||||
},
|
||||
width: 800,
|
||||
height: 600,
|
||||
queryData: {
|
||||
data: [
|
||||
{ 'San Francisco': 1, 'New York': 2, __timestamp: 599616000000 },
|
||||
{ 'San Francisco': 3, 'New York': 4, __timestamp: 599916000000 },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(transformProps(formulaChartProps)).toEqual(
|
||||
expect.objectContaining({
|
||||
width: 800,
|
||||
height: 600,
|
||||
echartOptions: expect.objectContaining({
|
||||
legend: expect.objectContaining({
|
||||
data: ['San Francisco', 'New York', 'My Formula'],
|
||||
}),
|
||||
series: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[new Date(599616000000), 1],
|
||||
[new Date(599916000000), 3],
|
||||
],
|
||||
name: 'San Francisco',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[new Date(599616000000), 2],
|
||||
[new Date(599916000000), 4],
|
||||
],
|
||||
name: 'New York',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
data: [
|
||||
[new Date(599616000000), 599616000001],
|
||||
[new Date(599916000000), 599916000001],
|
||||
],
|
||||
name: 'My Formula',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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 { parseAnnotationOpacity } from '../../src/utils/annotation';
|
||||
|
||||
describe('extractForecastSeriesContext', () => {
|
||||
it('should extract the correct series name and type', () => {
|
||||
expect(parseAnnotationOpacity('opacityLow')).toEqual(0.2);
|
||||
expect(parseAnnotationOpacity('opacityMedium')).toEqual(0.5);
|
||||
expect(parseAnnotationOpacity('opacityHigh')).toEqual(0.8);
|
||||
expect(parseAnnotationOpacity('')).toEqual(1);
|
||||
expect(parseAnnotationOpacity(undefined)).toEqual(1);
|
||||
});
|
||||
});
|
@ -4177,6 +4177,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/match-sorter/-/match-sorter-4.0.0.tgz#8a7286019d4e328c09422bb2af2403a94b7038fd"
|
||||
integrity sha512-JK7HNHXZA7i/nEp6fbNAxoX/1j1ysZXmv2/nlkt2UpX1LiUWKLtyt/dMmDTlMPR6t6PkwMmIr2W2AAyu6oELNw==
|
||||
|
||||
"@types/mathjs@^6.0.6":
|
||||
version "6.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/mathjs/-/mathjs-6.0.6.tgz#47cc33adb7eeec58f657465017594a2cdc84eead"
|
||||
integrity sha512-FGXJ5r51lmHkoDNNyjNw01gShCR7vMQRTyCLVj7XD8C51bTC+elc7BZBcfcsv5SeQQOVirEY6hbo5ZWvjIF79Q==
|
||||
dependencies:
|
||||
decimal.js "^10.0.0"
|
||||
|
||||
"@types/micromatch@^4.0.1":
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.1.tgz#9381449dd659fc3823fd2a4190ceacc985083bc7"
|
||||
@ -7238,6 +7245,11 @@ complex.js@2.0.4:
|
||||
resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.4.tgz#d8e7cfb9652d1e853e723386421c1a0ca7a48373"
|
||||
integrity sha512-Syl95HpxUTS0QjwNxencZsKukgh1zdS9uXeXX2Us0pHaqBR6kiZZi0AkZ9VpZFwHJyVIUVzI4EumjWdXP3fy6w==
|
||||
|
||||
complex.js@^2.0.11:
|
||||
version "2.0.11"
|
||||
resolved "https://registry.yarnpkg.com/complex.js/-/complex.js-2.0.11.tgz#09a873fbf15ffd8c18c9c2201ccef425c32b8bf1"
|
||||
integrity sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw==
|
||||
|
||||
component-emitter@^1.2.1:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
|
||||
@ -8256,6 +8268,11 @@ decimal.js@9.0.1:
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-9.0.1.tgz#1cc8b228177da7ab6498c1cc06eb130a290e6e1e"
|
||||
integrity sha512-2h0iKbJwnImBk4TGk7CG1xadoA0g3LDPlQhQzbZ221zvG0p2YVUedbKIPsOZXKZGx6YmZMJKYOalpCMxSdDqTQ==
|
||||
|
||||
decimal.js@^10.0.0, decimal.js@^10.2.1:
|
||||
version "10.2.1"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3"
|
||||
integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==
|
||||
|
||||
decimal.js@^10.2.0:
|
||||
version "10.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.0.tgz#39466113a9e036111d02f82489b5fd6b0b5ed231"
|
||||
@ -9094,7 +9111,7 @@ escape-html@^1.0.3, escape-html@~1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
|
||||
|
||||
escape-latex@^1.0.0:
|
||||
escape-latex@^1.0.0, escape-latex@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-latex/-/escape-latex-1.2.0.tgz#07c03818cf7dac250cce517f4fda1b001ef2bca1"
|
||||
integrity sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==
|
||||
@ -10117,6 +10134,11 @@ fraction.js@4.0.4:
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.4.tgz#04e567110718adf7b52974a10434ab4c67a5183e"
|
||||
integrity sha512-aK/oGatyYLTtXRHjfEsytX5fieeR5H4s8sLorzcT12taFS+dbMZejnvm9gRa8mZAPwci24ucjq9epDyaq5u8Iw==
|
||||
|
||||
fraction.js@^4.0.12:
|
||||
version "4.0.12"
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.0.12.tgz#0526d47c65a5fb4854df78bc77f7bec708d7b8c3"
|
||||
integrity sha512-8Z1K0VTG4hzYY7kA/1sj4/r1/RWLBD3xwReT/RCrUCbzPszjNQCCsy3ktkU/eaEqX3MYa4pY37a52eiBlPMlhA==
|
||||
|
||||
fragment-cache@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
|
||||
@ -11980,7 +12002,7 @@ java-properties@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/java-properties/-/java-properties-1.0.2.tgz#ccd1fa73907438a5b5c38982269d0e771fe78211"
|
||||
integrity sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==
|
||||
|
||||
javascript-natural-sort@0.7.1:
|
||||
javascript-natural-sort@0.7.1, javascript-natural-sort@^0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz#f9e2303d4507f6d74355a73664d1440fb5a0ef59"
|
||||
integrity sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k=
|
||||
@ -13624,6 +13646,20 @@ mathjs@^3.20.2:
|
||||
tiny-emitter "2.0.2"
|
||||
typed-function "0.10.7"
|
||||
|
||||
mathjs@^7.5.1:
|
||||
version "7.5.1"
|
||||
resolved "https://registry.yarnpkg.com/mathjs/-/mathjs-7.5.1.tgz#eb125295310a99ddcaf6145c47b09aab36e48274"
|
||||
integrity sha512-H2q/Dq0qxBLMw+G84SSXmGqo/znihuxviGgAQwAcyeFLwK2HksvSGNx4f3dllZF51bWOnu2op60VZxH2Sb51Pw==
|
||||
dependencies:
|
||||
complex.js "^2.0.11"
|
||||
decimal.js "^10.2.1"
|
||||
escape-latex "^1.2.0"
|
||||
fraction.js "^4.0.12"
|
||||
javascript-natural-sort "^0.7.1"
|
||||
seed-random "^2.2.0"
|
||||
tiny-emitter "^2.1.0"
|
||||
typed-function "^2.0.0"
|
||||
|
||||
md5.js@^1.3.4:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
|
||||
@ -17001,7 +17037,7 @@ scoped-regex@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8"
|
||||
integrity sha1-o0a7Gs1CB65wvXwMfKnlZra63bg=
|
||||
|
||||
seed-random@2.2.0:
|
||||
seed-random@2.2.0, seed-random@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/seed-random/-/seed-random-2.2.0.tgz#2a9b19e250a817099231a5b99a4daf80b7fbed54"
|
||||
integrity sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=
|
||||
@ -18303,7 +18339,7 @@ tiny-emitter@2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c"
|
||||
integrity sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow==
|
||||
|
||||
tiny-emitter@^2.0.0:
|
||||
tiny-emitter@^2.0.0, tiny-emitter@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
|
||||
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
|
||||
@ -18578,6 +18614,11 @@ typed-function@0.10.7:
|
||||
resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-0.10.7.tgz#f702af7d77a64b61abf86799ff2d74266ebc4477"
|
||||
integrity sha512-3mlZ5AwRMbLvUKkc8a1TI4RUJUS2H27pmD5q0lHRObgsoWzhDAX01yg82kwSP1FUw922/4Y9ZliIEh0qJZcz+g==
|
||||
|
||||
typed-function@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/typed-function/-/typed-function-2.0.0.tgz#15ab3825845138a8b1113bd89e60cd6a435739e8"
|
||||
integrity sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA==
|
||||
|
||||
typed-styles@^0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
|
||||
|
Loading…
Reference in New Issue
Block a user