mirror of
https://github.com/apache/superset.git
synced 2024-09-17 11:09:47 -04:00
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:
parent
9f1aafa628
commit
e916fd9015
@ -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),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -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: {...},
|
||||
}}
|
||||
/>
|
||||
```
|
@ -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"
|
||||
}
|
||||
}
|
@ -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} />;
|
||||
}
|
@ -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);
|
||||
}
|
@ -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 |
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
@ -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;
|
||||
};
|
@ -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} />;
|
||||
}
|
@ -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,
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
@ -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;
|
Binary file not shown.
After Width: | Height: | Size: 126 KiB |
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
@ -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;
|
||||
};
|
@ -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} />;
|
||||
}
|
@ -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
|
||||
*/
|
@ -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;
|
||||
};
|
@ -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;
|
||||
});
|
||||
};
|
@ -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]]),
|
||||
}));
|
||||
}
|
@ -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']);
|
||||
});
|
||||
});
|
@ -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',
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
@ -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)');
|
||||
});
|
||||
});
|
@ -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],
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -0,0 +1,4 @@
|
||||
declare module '*.png' {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user