feat: Add ECharts Timeseries plugin (#737)

* make it work

* Add color scheme and timeseries limits

* latest improvements

* bump

* moving dependencies to plugin

* Shuffling logic to transformProps, making Typescript happy.

* zoom controls!

* declaration for the dang PNG files

* Revert "declaration for the dang PNG files"

This reverts commit b37f01076e36ba2b05424f861187a182f4d327d6.

* PIE! (super basic)

* lowercase import name, moving types.

* capitalization fix

* nixing console log

* removing echarts peer dependency (missed it earlier)

* basic pie controls/types

* typescript fixes and whatnot

* yarn alphabetizing peerDependencies

* fixing Pie chart typing

* less enthusiasm

* fixing resize and data redraw quirks

* fixing zoom display quirks

* add predictive analytics

* fix controls

* improve typing and tests

* add rebasing to forecasts

* improve stacking etc

* Minor improvements

* add tooltip

* Charts draw and resize correctly

* clean up code

* lint

* yet more lint

* fix unit tests

* fix unit tests

* fix tests

* add useEchartsComponent and address comments

* address comments

* address more comments

* Add Echart component

* bump echarts to 4.9.0

* clean up Echart component

* add storybook

* replace radios with boolean

* address review comments

Co-authored-by: Evan Rusackas <evan@preset.io>
This commit is contained in:
Ville Brofeldt 2020-09-01 09:00:51 +03:00 committed by Yongjie Zhao
parent 9f1aafa628
commit e916fd9015
31 changed files with 5979 additions and 13 deletions

View File

@ -0,0 +1,59 @@
import React from 'react';
import { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/chart';
import { boolean, number, select, withKnobs } from '@storybook/addon-knobs';
import { EchartsTimeseriesChartPlugin } from '@superset-ui/plugin-chart-echarts';
import transformProps from '@superset-ui/plugin-chart-echarts/lib/Timeseries/transformProps';
import { withResizableChartDemo } from '../../../shared/components/ResizableChartDemo';
import data from './data';
new EchartsTimeseriesChartPlugin().configure({ key: 'echarts-timeseries' }).register();
getChartTransformPropsRegistry().registerValue('echarts-timeseries', transformProps);
export default {
title: 'Chart Plugins|plugin-chart-echarts',
decorators: [withKnobs, withResizableChartDemo],
};
export const Timeseries = ({ width, height }) => {
const forecastEnabled = boolean('Enable forecast', true);
const queryData = data
.map(row =>
forecastEnabled
? row
: {
__timestamp: row.__timestamp,
Boston: row.Boston,
California: row.California,
WestTexNewMexico: row.WestTexNewMexico,
},
)
.filter(row => forecastEnabled || !!row.Boston);
return (
<SuperChart
chartType="echarts-timeseries"
width={width}
height={height}
queryData={{ data: queryData }}
formData={{
contributionMode: undefined,
forecastEnabled,
colorScheme: 'supersetColors',
seriesType: select(
'Line type',
['line', 'scatter', 'smooth', 'bar', 'start', 'middle', 'end'],
'line',
),
logAxis: boolean('Log axis', false),
yAxisFormat: 'SMART_NUMBER',
stack: boolean('Stack', false),
area: boolean('Area chart', false),
markerEnabled: boolean('Enable markers', false),
markerSize: number('Marker Size', 6),
minorSplitLine: boolean('Minor splitline', false),
opacity: number('Opacity', 0.2),
zoomable: boolean('Zoomable', false),
}}
/>
);
};

View File

@ -0,0 +1,40 @@
## @superset-ui/plugin-chart-echarts
[![Version](https://img.shields.io/npm/v/@superset-ui/plugin-chart-echarts.svg?style=flat-square)](https://img.shields.io/npm/v/@superset-ui/plugin-chart-echarts.svg?style=flat-square)
[![David (path)](https://img.shields.io/david/apache-superset/superset-ui.svg?path=packages%2Fsuperset-ui-plugin-chart-echarts&style=flat-square)](https://david-dm.org/apache-superset/superset-ui?path=packages/superset-ui-plugin-chart-echarts)
This plugin provides Echarts viz plugins for Superset:
- Timeseries Chart (combined line, area bar with support for predictive analytics)
- Pie Chart
### Usage
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 {
EchartsTimeseriesChartPlugin,
EchartsPieChartPlugin,
} from '@superset-ui/plugin-chart-echarts';
new EchartsTimeseriesChartPlugin()
.configure({ key: 'echarts-ts' })
.register();
new EchartsPieChartPlugin()
.configure({ key: 'echarts-pie' })
.register();
```
Then use it via `SuperChart`. See [storybook](https://apache-superset.github.io/superset-ui/?selectedKind=chart-plugins-plugin-chart-echarts) for more details.
```js
<SuperChart
chartType="echarts-ts"
width={600}
height={600}
formData={...}
queryData={{
data: {...},
}}
/>
```

View File

@ -0,0 +1,48 @@
{
"name": "@superset-ui/plugin-chart-echarts",
"version": "0.0.0",
"description": "Superset Chart - Echarts",
"sideEffects": false,
"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"
},
"peerDependencies": {
"@superset-ui/chart": "^0.14.1",
"@superset-ui/chart-controls": "^0.14.0",
"@superset-ui/color": "^0.14.1",
"@superset-ui/number-format": "^0.14.0",
"@superset-ui/query": "^0.14.1",
"@superset-ui/style": "^0.14.0",
"@superset-ui/translation": "^0.14.0",
"@superset-ui/validator": "^0.14.1",
"react": "^16.13.1"
},
"devDependencies": {
"@types/jest": "^26.0.0",
"jest": "^26.0.1"
},
"dependencies": {
"@superset-ui/time-format": "^0.14.9",
"@types/echarts": "^4.6.3",
"echarts": "^4.9.0"
}
}

View File

@ -0,0 +1,25 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { EchartsPieProps } from './types';
import Echart from '../components/Echart';
export default function EchartsPie({ height, width, echartOptions }: EchartsPieProps) {
return <Echart height={height} width={width} echartOptions={echartOptions} />;
}

View File

@ -0,0 +1,23 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { buildQueryContext, QueryFormData } from '@superset-ui/query';
export default function buildQuery(formData: QueryFormData) {
return buildQueryContext(formData);
}

View File

@ -0,0 +1,77 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@superset-ui/translation';
import { validateNonEmpty } from '@superset-ui/validator';
import { ControlPanelConfig } from '@superset-ui/chart-controls';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [['groupby'], ['metrics'], ['adhoc_filters'], ['row_limit', null]],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
[
{
name: 'outerRadius',
config: {
type: 'SliderControl',
label: t('Outer Radius'),
renderTrigger: true,
min: 10,
max: 100,
step: 1,
default: 70,
description: t('Outer edge of Pie chart'),
},
},
{
name: 'innerRadius',
config: {
type: 'SliderControl',
label: t('Inner Radius'),
renderTrigger: true,
min: 0,
max: 100,
step: 1,
default: 50,
description: t('Inner radius of donut hole'),
},
},
],
],
},
],
controlOverrides: {
series: {
validators: [validateNonEmpty],
clearable: false,
},
row_limit: {
default: 100,
},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,52 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
const metadata = new ChartMetadata({
description: 'Pie chart using ECharts',
name: t('EchartsPie'),
thumbnail,
});
export default class EchartsPieChartPlugin extends ChartPlugin {
/**
* The constructor is used to pass relevant metadata and callbacks that get
* registered in respective registries that are used throughout the library
* and application. A more thorough description of each property is given in
* the respective imported file.
*
* It is worth noting that `buildQuery` and is optional, and only needed for
* advanced visualizations that require either post processing operations
* (pivoting, rolling aggregations, sorting etc) or submitting multiple queries.
*/
constructor() {
super({
buildQuery,
controlPanel,
loadChart: () => import('./EchartsPie'),
metadata,
transformProps,
});
}
}

View File

@ -0,0 +1,87 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ChartProps, DataRecord } from '@superset-ui/chart';
import { EchartsPieProps } from './types';
export default function transformProps(chartProps: ChartProps): EchartsPieProps {
/*
TODO:
- add support for multiple groupby (requires post transform op)
- add support for ad-hoc metrics (currently only supports datasource metrics)
- add support for superset colors
- add support for control values in legacy pie chart
*/
const { width, height, formData, queryData } = chartProps;
const data: DataRecord[] = queryData.data || [];
const { innerRadius = 50, outerRadius = 70, groupby = [], metrics = [] } = formData;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const keys = Array.from(new Set(data.map(datum => datum[groupby[0]])));
const transformedData = data.map(datum => {
return {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
value: datum[metrics[0]],
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
name: datum[groupby[0]],
};
});
const echartOptions = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)',
},
legend: {
orient: 'vertical',
left: 10,
data: keys,
},
series: [
{
type: 'pie',
radius: [`${innerRadius}%`, `${outerRadius}%`],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '30',
fontWeight: 'bold',
},
},
labelLine: {
show: false,
},
data: transformedData,
},
],
};
return {
width,
height,
// @ts-ignore
echartOptions,
};
}

View File

@ -0,0 +1,42 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { QueryFormData } from '@superset-ui/query';
import { EchartsProps } from '../types';
export type PieChartFormData = QueryFormData & {
groupby?: string[];
metrics?: string[];
outerRadius?: number;
innerRadius?: number;
};
export type EchartsPieProps = EchartsProps & {
formData: PieChartFormData;
area: number;
colorScheme: string;
contributionMode?: string;
zoomable?: boolean;
seriesType: string;
logAxis: boolean;
stack: boolean;
markerEnabled: boolean;
markerSize: number;
minorSplitLine: boolean;
opacity: number;
};

View File

@ -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 React from 'react';
import { EchartsTimeseriesProps } from './types';
import Echart from '../components/Echart';
export default function EchartsTimeseries({
height,
width,
echartOptions,
}: EchartsTimeseriesProps) {
return <Echart height={height} width={width} echartOptions={echartOptions} />;
}

View File

@ -0,0 +1,82 @@
/**
* 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, QueryFormData } from '@superset-ui/query';
/**
* The buildQuery function is used to create an instance of QueryContext that's
* sent to the chart data endpoint. In addition to containing information of which
* datasource to use, it specifies the type (e.g. full payload, samples, query) and
* format (e.g. CSV or JSON) of the result and whether or not to force refresh the data from
* the datasource as opposed to using a cached copy of the data, if available.
*
* More importantly though, QueryContext contains a property `queries`, which is an array of
* QueryObjects specifying individual data requests to be made. A QueryObject specifies which
* columns, metrics and filters, among others, to use during the query. Usually it will be enough
* to specify just one query based on the baseQueryObject, but for some more advanced use cases
* it is possible to define post processing operations in the QueryObject, or multiple queries
* if a viz needs multiple different result sets.
*/
export default function buildQuery(formData: QueryFormData) {
return buildQueryContext(formData, baseQueryObject => {
const baseQueryMetrics = baseQueryObject?.metrics ? baseQueryObject.metrics : [];
return [
{
...baseQueryObject,
// Time series charts need to set the `is_timeseries` flag to true
is_timeseries: true,
post_processing: [
{
operation: 'pivot',
options: {
index: ['__timestamp'],
columns: formData.groupby,
// Create 'dummy' sum aggregates to assign cell values in pivot table
aggregates: Object.fromEntries(
baseQueryMetrics.map(metric => [metric.label, { operator: 'sum' }]),
),
},
},
formData.contributionMode
? {
operation: 'contribution',
options: {
orientation: formData.contributionMode,
},
}
: undefined,
formData.forecastEnabled
? {
operation: 'prophet',
options: {
time_grain: formData.time_grain_sqla,
periods: parseInt(formData.forecastPeriods, 10),
confidence_interval: parseFloat(formData.forecastInterval),
yearly_seasonality: formData.forecastSeasonalityYearly,
weekly_seasonality: formData.forecastSeasonalityWeekly,
daily_seasonality: formData.forecastSeasonalityDaily,
},
}
: undefined,
],
},
];
});
}

View File

@ -0,0 +1,308 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@superset-ui/translation';
import {
validateNonEmpty,
legacyValidateInteger,
legacyValidateNumber,
} from '@superset-ui/validator';
import { ControlPanelConfig } from '@superset-ui/chart-controls';
const config: ControlPanelConfig = {
controlPanelSections: [
{
label: t('Query'),
expanded: true,
controlSetRows: [
['metrics'],
['groupby'],
[
{
name: 'contributionMode',
config: {
type: 'SelectControl',
label: t('Contribution Mode'),
default: null,
choices: [
[null, 'None'],
['row', 'Total'],
['column', 'Series'],
],
description: t('Calculate contribution per series or total'),
},
},
],
['adhoc_filters'],
['limit', 'timeseries_limit_metric'],
[
{
name: 'order_desc',
config: {
type: 'CheckboxControl',
label: t('Sort Descending'),
default: true,
description: t('Whether to sort descending or ascending'),
},
},
],
['row_limit', null],
],
},
{
label: t('Predictive Analytics'),
expanded: true,
controlSetRows: [
[
{
name: 'forecastEnabled',
config: {
type: 'CheckboxControl',
label: t('Enable forecast'),
renderTrigger: false,
default: false,
description: t('Enable forecasting'),
},
},
],
[
{
name: 'forecastPeriods',
config: {
type: 'TextControl',
label: t('Forecast periods'),
validators: [legacyValidateInteger],
default: 10,
description: t('How many periods into the future do we want to predict'),
},
},
],
[
{
name: 'forecastInterval',
config: {
type: 'TextControl',
label: t('Confidence interval'),
validators: [legacyValidateNumber],
default: 0.8,
description: t('Width of the confidence interval. Should be between 0 and 1'),
},
},
{
name: 'forecastSeasonalityYearly',
config: {
type: 'SelectControl',
freeForm: true,
label: 'Yearly seasonality',
choices: [
[null, 'default'],
[true, 'Yes'],
[false, 'No'],
],
default: null,
description: t(
'Should yearly seasonality be applied. An integer value will specify Fourier order of seasonality.',
),
},
},
],
[
{
name: 'forecastSeasonalityWeekly',
config: {
type: 'SelectControl',
freeForm: true,
label: 'Weekly seasonality',
choices: [
[null, 'default'],
[true, 'Yes'],
[false, 'No'],
],
default: null,
description: t(
'Should weekly seasonality be applied. An integer value will specify Fourier order of seasonality.',
),
},
},
{
name: 'forecastSeasonalityDaily',
config: {
type: 'SelectControl',
freeForm: true,
label: 'Daily seasonality',
choices: [
[null, 'default'],
[true, 'Yes'],
[false, 'No'],
],
default: null,
description: t(
'Should daily seasonality be applied. An integer value will specify Fourier order of seasonality.',
),
},
},
],
],
},
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme', 'label_colors'],
[
{
name: 'seriesType',
config: {
type: 'SelectControl',
label: t('Series Style'),
renderTrigger: true,
default: 'line',
choices: [
['line', 'Line'],
['scatter', 'Scatter'],
['smooth', 'Smooth Line'],
['bar', 'Bar'],
['start', 'Step - start'],
['middle', 'Step - middle'],
['end', 'Step - end'],
],
description: t('Series chart type (line, bar etc)'),
},
},
],
['y_axis_format'],
[
{
name: 'stack',
config: {
type: 'CheckboxControl',
label: t('Stack Lines'),
renderTrigger: true,
default: false,
description: t('Stack series on top of each other'),
},
},
],
[
{
name: 'area',
config: {
type: 'CheckboxControl',
label: t('Area Chart'),
renderTrigger: true,
default: false,
description: t('Draw area under curves. Only applicable for line types.'),
},
},
{
name: 'opacity',
config: {
type: 'SliderControl',
label: t('Opacity'),
renderTrigger: true,
min: 0,
max: 1,
step: 0.1,
default: 0.2,
description: t('Opacity of Area Chart. Also applies to confidence band.'),
},
},
],
[
{
name: 'markerEnabled',
config: {
type: 'CheckboxControl',
label: t('Marker'),
renderTrigger: true,
default: false,
description: t('Draw a marker on data points. Only applicable for line types.'),
},
},
{
name: 'markerSize',
config: {
type: 'SliderControl',
label: t('Marker Size'),
renderTrigger: true,
min: 0,
max: 100,
default: 6,
description: t('Size of marker. Also applies to forecast observations.'),
},
},
],
[
{
name: 'logAxis',
config: {
type: 'CheckboxControl',
label: t('Logarithmic y-axis'),
renderTrigger: true,
default: false,
description: t('Logarithmic y-axis'),
},
},
{
name: 'minorSplitLine',
config: {
type: 'CheckboxControl',
label: t('Minor Split Line'),
renderTrigger: true,
default: false,
description: t('Draw split lines for minor y-axis ticks'),
},
},
],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: false,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
],
},
],
// Time series charts need to override the `druidTimeSeries` and `sqlaTimeSeries`
// sections to add the time grain dropdown.
sectionOverrides: {
druidTimeSeries: {
controlSetRows: [['granularity', 'druid_time_origin'], ['time_range']],
},
sqlaTimeSeries: {
controlSetRows: [['granularity_sqla', 'time_grain_sqla'], ['time_range']],
},
},
controlOverrides: {
series: {
validators: [validateNonEmpty],
clearable: false,
},
row_limit: {
default: 100,
},
},
};
export default config;

View File

@ -0,0 +1,52 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
const metadata = new ChartMetadata({
description: 'ECharts Timeseries',
name: t('ECharts Timeseries'),
thumbnail,
});
export default class EchartsTimeseriesChartPlugin extends ChartPlugin {
/**
* The constructor is used to pass relevant metadata and callbacks that get
* registered in respective registries that are used throughout the library
* and application. A more thorough description of each property is given in
* the respective imported file.
*
* It is worth noting that `buildQuery` and is optional, and only needed for
* advanced visualizations that require either post processing operations
* (pivoting, rolling aggregations, sorting etc) or submitting multiple queries.
*/
constructor() {
super({
buildQuery,
controlPanel,
loadChart: () => import('./EchartsTimeseries'),
metadata,
transformProps,
});
}
}

View File

@ -0,0 +1,202 @@
/**
* 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 { ChartProps } from '@superset-ui/chart';
import { CategoricalColorNamespace } from '@superset-ui/color';
import { getNumberFormatter } from '@superset-ui/number-format';
import { smartDateVerboseFormatter } from '@superset-ui/time-format';
import { EchartsTimeseriesProps } from './types';
import { ForecastSeriesEnum } from '../types';
import { extractTimeseriesSeries } from '../utils/series';
import {
extractForecastSeriesContext,
extractProphetValuesFromTooltipParams,
formatProphetTooltipSeries,
rebaseTimeseriesDatum,
} from '../utils/prophet';
export default function transformProps(chartProps: ChartProps): EchartsTimeseriesProps {
const { width, height, formData, queryData } = chartProps;
const {
area,
colorScheme,
contributionMode,
forecastEnabled,
seriesType,
logAxis,
opacity,
stack,
markerEnabled,
markerSize,
minorSplitLine,
yAxisFormat,
zoomable,
} = formData;
const colorFn = CategoricalColorNamespace.getScale(colorScheme as string);
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const rebasedData = rebaseTimeseriesDatum(queryData.data || []);
const rawSeries = extractTimeseriesSeries(rebasedData);
const series: echarts.EChartOption.Series[] = [];
const formatter = getNumberFormatter(contributionMode ? ',.0%' : yAxisFormat);
rawSeries.forEach(entry => {
const forecastSeries = extractForecastSeriesContext(entry.name || '');
const isConfidenceBand =
forecastSeries.type === ForecastSeriesEnum.ForecastLower ||
forecastSeries.type === ForecastSeriesEnum.ForecastUpper;
const isObservation = forecastSeries.type === ForecastSeriesEnum.Observation;
const isTrend = forecastSeries.type === ForecastSeriesEnum.ForecastTrend;
let stackId;
if (isConfidenceBand) {
stackId = forecastSeries.name;
} else if (stack && isObservation) {
// the suffix of the observation series is '' (falsy), which disables
// stacking. Therefore we need to set something that is truthy.
stackId = 'obs';
} else if (stack && isTrend) {
stackId = forecastSeries.type;
}
let plotType;
if (!isConfidenceBand && (seriesType === 'scatter' || (forecastEnabled && isObservation))) {
plotType = 'scatter';
} else if (isConfidenceBand) {
plotType = 'line';
} else {
plotType = seriesType === 'bar' ? 'bar' : 'line';
}
const lineStyle = isConfidenceBand ? { opacity: 0 } : {};
if (!((stack || area) && isConfidenceBand))
series.push({
...entry,
id: entry.name,
name: forecastSeries.name,
itemStyle: {
color: colorFn(forecastSeries.name),
},
type: plotType,
// @ts-ignore
smooth: seriesType === 'smooth',
step: ['start', 'middle', 'end'].includes(seriesType as string) ? seriesType : undefined,
stack: stackId,
lineStyle,
areaStyle: {
opacity: forecastSeries.type === ForecastSeriesEnum.ForecastUpper || area ? opacity : 0,
},
symbolSize:
!isConfidenceBand &&
(plotType === 'scatter' || (forecastEnabled && isObservation) || markerEnabled)
? markerSize
: 0,
});
});
const echartOptions: echarts.EChartOption = {
grid: {
top: 30,
bottom: zoomable ? 80 : 0,
left: 20,
right: 20,
containLabel: true,
},
xAxis: { type: 'time' },
yAxis: {
type: logAxis ? 'log' : 'value',
min: contributionMode === 'row' && stack ? 0 : undefined,
max: contributionMode === 'row' && stack ? 1 : undefined,
minorTick: { show: true },
minorSplitLine: { show: minorSplitLine },
axisLabel: { formatter },
},
tooltip: {
trigger: 'axis',
formatter: (params, ticket, callback) => {
// @ts-ignore
const rows = [`${smartDateVerboseFormatter(params[0].value[0])}`];
// @ts-ignore
const prophetValues = extractProphetValuesFromTooltipParams(params);
Object.keys(prophetValues).forEach(key => {
const value = prophetValues[key];
rows.push(
formatProphetTooltipSeries({
...value,
seriesName: key,
formatter,
}),
);
});
setTimeout(() => {
callback(ticket, rows.join('<br />'));
}, 50);
return 'loading';
},
},
legend: {
type: 'scroll',
data: rawSeries
.filter(
entry =>
extractForecastSeriesContext(entry.name || '').type === ForecastSeriesEnum.Observation,
)
.map(entry => entry.name || ''),
right: zoomable ? 80 : 'auto',
},
series,
toolbox: {
show: zoomable,
feature: {
dataZoom: {
yAxisIndex: false,
title: {
zoom: 'zoom area',
back: 'restore zoom',
},
},
},
},
dataZoom: zoomable
? [
{
type: 'slider',
start: 0,
end: 100,
bottom: 20,
},
]
: [],
};
return {
area,
colorScheme,
contributionMode,
// @ts-ignore
echartOptions,
seriesType,
logAxis,
opacity,
stack,
markerEnabled,
markerSize,
minorSplitLine,
width,
height,
};
}

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 { DataRecord, DataRecordValue } from '@superset-ui/chart';
import { EchartsProps } from '../types';
export type TimestampType = string | number | Date;
export interface TimeseriesDataRecord extends DataRecord {
__timestamp: TimestampType;
}
export type EchartsBaseTimeseriesSeries = {
name: string;
data: [Date, DataRecordValue][];
};
export type EchartsTimeseriesSeries = EchartsBaseTimeseriesSeries & {
color: string;
stack?: string;
type: 'bar' | 'line';
smooth: boolean;
step?: 'start' | 'middle' | 'end';
areaStyle: {
opacity: number;
};
symbolSize: number;
};
export type EchartsTimeseriesProps = EchartsProps & {
area: number;
colorScheme: string;
contributionMode?: string;
zoomable?: boolean;
seriesType: string;
logAxis: boolean;
stack: boolean;
markerEnabled: boolean;
markerSize: number;
minorSplitLine: boolean;
opacity: number;
};

View File

@ -0,0 +1,48 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useRef, useEffect } from 'react';
import styled from '@superset-ui/style';
import echarts from 'echarts';
import { EchartsProps, EchartsStylesProps } from '../types';
const Styles = styled.div<EchartsStylesProps>`
height: ${({ height }) => height};
width: ${({ width }) => width};
`;
export default function Echart({ width, height, echartOptions }: EchartsProps) {
const divRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<echarts.ECharts>();
useEffect(() => {
if (!divRef.current) return;
if (!chartRef.current) {
chartRef.current = echarts.init(divRef.current);
}
chartRef.current.setOption(echartOptions, true);
}, [echartOptions]);
useEffect(() => {
if (chartRef.current) {
chartRef.current.resize({ width, height });
}
}, [width, height]);
return <Styles ref={divRef} height={height} width={width} />;
}

View File

@ -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.
*/
// eslint-disable-next-line import/prefer-default-export
export { default as EchartsTimeseriesChartPlugin } from './Timeseries';
export { default as EchartsPieChartPlugin } from './Pie';
/**
* Note: this file exports the default export from EchartsTimeseries.tsx.
* If you want to export multiple visualization modules, you will need to
* either add additional plugin folders (similar in structure to ./plugin)
* OR export multiple instances of `ChartPlugin` extensions in ./plugin/index.ts
* which in turn load exports from EchartsTimeseries.tsx
*/

View File

@ -0,0 +1,48 @@
/**
* 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.
*/
export type EchartsStylesProps = {
height: number;
width: number;
};
export interface EchartsProps {
height: number;
width: number;
echartOptions: echarts.EChartOption;
}
export enum ForecastSeriesEnum {
Observation = '',
ForecastTrend = '__yhat',
ForecastUpper = '__yhat_upper',
ForecastLower = '__yhat_lower',
}
export type ForecastSeriesContext = {
name: string;
type: ForecastSeriesEnum;
};
export type ProphetValue = {
marker: string;
observation?: number;
forecastTrend?: number;
forecastLower?: number;
forecastUpper?: number;
};

View File

@ -0,0 +1,113 @@
/**
* 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 { DataRecord } from '@superset-ui/chart';
import { NumberFormatter } from '@superset-ui/number-format';
import { ForecastSeriesContext, ForecastSeriesEnum, ProphetValue } from '../types';
import { TimeseriesDataRecord } from '../Timeseries/types';
const seriesTypeRegex = new RegExp(
`(.+)(${ForecastSeriesEnum.ForecastLower}|${ForecastSeriesEnum.ForecastTrend}|${ForecastSeriesEnum.ForecastUpper})$`,
);
export const extractForecastSeriesContext = (seriesName: string): ForecastSeriesContext => {
const regexMatch = seriesTypeRegex.exec(seriesName);
if (!regexMatch) return { name: seriesName, type: ForecastSeriesEnum.Observation };
return {
name: regexMatch[1],
type: regexMatch[2] as ForecastSeriesEnum,
};
};
export const extractProphetValuesFromTooltipParams = (
params: (echarts.EChartOption.Tooltip.Format & { seriesId: string })[],
): Record<string, ProphetValue> => {
const values: Record<string, ProphetValue> = {};
params.forEach(param => {
const { marker, seriesId, value } = param;
const context = extractForecastSeriesContext(seriesId);
const numericValue = (value as [Date, number])[1];
if (numericValue) {
if (!(context.name in values))
values[context.name] = {
marker: marker || '',
};
const prophetValues = values[context.name];
if (context.type === ForecastSeriesEnum.Observation) prophetValues.observation = numericValue;
if (context.type === ForecastSeriesEnum.ForecastTrend)
prophetValues.forecastTrend = numericValue;
if (context.type === ForecastSeriesEnum.ForecastLower)
prophetValues.forecastLower = numericValue;
if (context.type === ForecastSeriesEnum.ForecastUpper)
prophetValues.forecastUpper = numericValue;
}
});
return values;
};
export const formatProphetTooltipSeries = ({
seriesName,
observation,
forecastTrend,
forecastLower,
forecastUpper,
marker,
formatter,
}: ProphetValue & {
seriesName: string;
marker: string;
formatter: NumberFormatter;
}): string => {
let row = `${marker}${seriesName}: `;
let isObservation = false;
if (observation) {
isObservation = true;
row += `${formatter(observation)}`;
}
if (forecastTrend) {
if (isObservation) row += ', ';
row += `ŷ = ${formatter(forecastTrend)}`;
if (forecastLower && forecastUpper)
// the lower bound needs to be added to the upper bound
row += ` (${formatter(forecastLower)}, ${formatter(forecastLower + forecastUpper)})`;
}
return `${row.trim()}`;
};
export const rebaseTimeseriesDatum = (data: DataRecord[]): TimeseriesDataRecord[] => {
const keys = data.length > 0 ? Object.keys(data[0]) : [];
return data.map(row => {
const newRow: TimeseriesDataRecord = { __timestamp: '' };
keys.forEach(key => {
const forecastContext = extractForecastSeriesContext(key);
const lowerKey = `${forecastContext.name}${ForecastSeriesEnum.ForecastLower}`;
let value = row[key];
if (
forecastContext.type === ForecastSeriesEnum.ForecastUpper &&
keys.includes(lowerKey) &&
value !== null &&
row[lowerKey] !== null
) {
// @ts-ignore
value -= row[lowerKey];
}
newRow[key] = value;
});
return newRow;
});
};

View File

@ -0,0 +1,39 @@
/* 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 { TimeseriesDataRecord } from '../Timeseries/types';
// eslint-disable-next-line import/prefer-default-export
export function extractTimeseriesSeries(
data: TimeseriesDataRecord[],
): echarts.EChartOption.Series[] {
if (data.length === 0) return [];
const rows = data.map(datum => ({
...datum,
__timestamp: new Date(datum.__timestamp),
}));
return Object.keys(rows[0])
.filter(key => key !== '__timestamp')
.map(key => ({
name: key,
// @ts-ignore
data: rows.map(datum => [datum.__timestamp, datum[key]]),
}));
}

View File

@ -0,0 +1,35 @@
/**
* 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 buildQuery from '../../src/Timeseries/buildQuery';
describe('EchartsTimeseries buildQuery', () => {
const formData = {
datasource: '5__table',
granularity_sqla: 'ds',
series: 'foo',
viz_type: 'my_chart',
queryFields: { series: 'groupby' },
};
it('should build groupby with series in form data', () => {
const queryContext = buildQuery(formData);
const [query] = queryContext.queries;
expect(query.groupby).toEqual(['foo']);
});
});

View File

@ -0,0 +1,56 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import 'babel-polyfill';
import { ChartProps } from '@superset-ui/chart';
import transformProps from '../../src/Timeseries/transformProps';
describe('EchartsTimeseries tranformProps', () => {
const formData = {
colorScheme: 'bnbColors',
datasource: '3__table',
granularity_sqla: 'ds',
metric: 'sum__num',
series: 'name',
};
const chartProps = new ChartProps({
formData,
width: 800,
height: 600,
queryData: {
data: [{ sum__num: 1, __timestamp: 599616000000 }],
},
});
it('should tranform chart props for viz', () => {
expect(transformProps(chartProps)).toEqual(
expect.objectContaining({
width: 800,
height: 600,
echartOptions: expect.objectContaining({
series: expect.arrayContaining([
expect.objectContaining({
data: [[new Date(599616000000), 1]],
id: 'sum__num',
}),
]),
}),
}),
);
});
});

View File

@ -0,0 +1,26 @@
/**
* 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 { EchartsPieChartPlugin, EchartsTimeseriesChartPlugin } from '../src';
describe('@superset-ui/plugin-chart-echarts', () => {
it('exists', () => {
expect(EchartsPieChartPlugin).toBeDefined();
expect(EchartsTimeseriesChartPlugin).toBeDefined();
});
});

View File

@ -0,0 +1,175 @@
/**
* 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, NumberFormats } from '@superset-ui/number-format';
import {
extractForecastSeriesContext,
extractProphetValuesFromTooltipParams,
formatProphetTooltipSeries,
rebaseTimeseriesDatum,
} from '../../src/utils/prophet';
import { ForecastSeriesEnum } from '../../src/types';
describe('extractForecastSeriesContext', () => {
it('should extract the correct series name and type', () => {
expect(extractForecastSeriesContext('abcd')).toEqual({
name: 'abcd',
type: ForecastSeriesEnum.Observation,
});
expect(extractForecastSeriesContext('qwerty__yhat')).toEqual({
name: 'qwerty',
type: ForecastSeriesEnum.ForecastTrend,
});
expect(extractForecastSeriesContext('X Y Z___yhat_upper')).toEqual({
name: 'X Y Z_',
type: ForecastSeriesEnum.ForecastUpper,
});
expect(extractForecastSeriesContext('1 2 3__yhat_lower')).toEqual({
name: '1 2 3',
type: ForecastSeriesEnum.ForecastLower,
});
});
});
describe('rebaseTimeseriesDatum', () => {
it('should subtract lower confidence level from upper value', () => {
expect(
rebaseTimeseriesDatum([
{
__timestamp: new Date('2001-01-01'),
abc: 10,
abc__yhat_lower: 1,
abc__yhat_upper: 20,
},
{
__timestamp: new Date('2002-01-01'),
abc: 10,
abc__yhat_lower: null,
abc__yhat_upper: 20,
},
{
__timestamp: new Date('2003-01-01'),
abc: 10,
abc__yhat_lower: 1,
abc__yhat_upper: null,
},
]),
).toEqual([
{
__timestamp: new Date('2001-01-01'),
abc: 10,
abc__yhat_lower: 1,
abc__yhat_upper: 19,
},
{
__timestamp: new Date('2002-01-01'),
abc: 10,
abc__yhat_lower: null,
abc__yhat_upper: 20,
},
{
__timestamp: new Date('2003-01-01'),
abc: 10,
abc__yhat_lower: 1,
abc__yhat_upper: null,
},
]);
});
});
describe('extractProphetValuesFromTooltipParams', () => {
it('should extract the proper data from tooltip params', () => {
expect(
extractProphetValuesFromTooltipParams([
{
marker: '<img>',
seriesId: 'abc',
value: [new Date(0), 10],
},
{
marker: '<img>',
seriesId: 'abc__yhat',
value: [new Date(0), 1],
},
{
marker: '<img>',
seriesId: 'abc__yhat_lower',
value: [new Date(0), 5],
},
{
marker: '<img>',
seriesId: 'abc__yhat_upper',
value: [new Date(0), 6],
},
{
marker: '<img>',
seriesId: 'qwerty',
value: [new Date(0), 2],
},
]),
).toEqual({
abc: {
marker: '<img>',
observation: 10,
forecastTrend: 1,
forecastLower: 5,
forecastUpper: 6,
},
qwerty: {
marker: '<img>',
observation: 2,
},
});
});
});
const formatter = getNumberFormatter(NumberFormats.INTEGER);
describe('formatProphetTooltipSeries', () => {
it('should generate a proper series tooltip', () => {
expect(
formatProphetTooltipSeries({
seriesName: 'abc',
marker: '<img>',
observation: 10.1,
formatter,
}),
).toEqual('<img>abc: 10');
expect(
formatProphetTooltipSeries({
seriesName: 'qwerty',
marker: '<img>',
observation: 10.1,
forecastTrend: 20.1,
forecastLower: 5.1,
forecastUpper: 7.1,
formatter,
}),
).toEqual('<img>qwerty: 10, ŷ = 20 (5, 12)');
expect(
formatProphetTooltipSeries({
seriesName: 'qwerty',
marker: '<img>',
forecastTrend: 20,
forecastLower: 5,
forecastUpper: 7,
formatter,
}),
).toEqual('<img>qwerty: ŷ = 20 (5, 12)');
});
});

View File

@ -0,0 +1,59 @@
/**
* 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 { extractTimeseriesSeries } from '../../src/utils/series';
describe('extractTimeseriesSeries', () => {
it('should generate a valid ECharts timeseries series object', () => {
const data = [
{
__timestamp: '2000-01-01',
Hulk: null,
abc: 2,
},
{
__timestamp: '2000-02-01',
Hulk: 2,
abc: 10,
},
{
__timestamp: '2000-03-01',
Hulk: 1,
abc: 5,
},
];
expect(extractTimeseriesSeries(data)).toEqual([
{
name: 'Hulk',
data: [
[new Date('2000-01-01'), null],
[new Date('2000-02-01'), 2],
[new Date('2000-03-01'), 1],
],
},
{
name: 'abc',
data: [
[new Date('2000-01-01'), 2],
[new Date('2000-02-01'), 10],
[new Date('2000-03-01'), 5],
],
},
]);
});
});

View File

@ -0,0 +1,4 @@
declare module '*.png' {
const value: any;
export default value;
}

View File

@ -1,4 +1,3 @@
import 'babel-polyfill';
import buildQuery from '../../src/plugin/buildQuery';
describe('WordCloud buildQuery', () => {

File diff suppressed because it is too large Load Diff