refactor(monorepo): move superset-ui to superset(stage 2) (#17552)

This commit is contained in:
Yongjie Zhao
2021-11-30 08:29:57 +08:00
committed by GitHub
parent bfba4f1689
commit 3c41ff68a4
1315 changed files with 27755 additions and 15167 deletions

View File

@@ -0,0 +1,293 @@
/**
* 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 { t } from '@superset-ui/core';
import {
ControlPanelConfig,
ControlPanelsContainerProps,
D3_TIME_FORMAT_DOCS,
emitFilterControl,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
import {
DEFAULT_FORM_DATA,
EchartsTimeseriesContributionType,
EchartsTimeseriesSeriesType,
} from '../types';
import {
legendSection,
richTooltipSection,
showValueSection,
} from '../../controls';
const {
contributionMode,
logAxis,
markerEnabled,
markerSize,
minorSplitLine,
opacity,
rowLimit,
seriesType,
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
sections.legacyTimeseriesTime,
{
label: t('Query'),
expanded: true,
controlSetRows: [
['metrics'],
['groupby'],
[
{
name: 'contributionMode',
config: {
type: 'SelectControl',
label: t('Contribution Mode'),
default: contributionMode,
choices: [
[null, 'None'],
[EchartsTimeseriesContributionType.Row, 'Total'],
[EchartsTimeseriesContributionType.Column, 'Series'],
],
description: t('Calculate contribution per series or total'),
},
},
],
['adhoc_filters'],
emitFilterControl,
['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'],
],
},
sections.advancedAnalyticsControls,
sections.annotationsAndLayersControls,
sections.forecastIntervalControls,
sections.titleControls,
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
[
{
name: 'seriesType',
config: {
type: 'SelectControl',
label: t('Series Style'),
renderTrigger: true,
default: seriesType,
choices: [
[EchartsTimeseriesSeriesType.Line, 'Line'],
[EchartsTimeseriesSeriesType.Smooth, 'Smooth Line'],
[EchartsTimeseriesSeriesType.Start, 'Step - start'],
[EchartsTimeseriesSeriesType.Middle, 'Step - middle'],
[EchartsTimeseriesSeriesType.End, 'Step - end'],
],
description: t('Series chart type (line, bar etc)'),
},
},
],
[
{
name: 'opacity',
config: {
type: 'SliderControl',
label: t('Area chart opacity'),
renderTrigger: true,
min: 0,
max: 1,
step: 0.1,
default: opacity,
description: t(
'Opacity of Area Chart. Also applies to confidence band.',
),
},
},
],
...showValueSection,
[
{
name: 'markerEnabled',
config: {
type: 'CheckboxControl',
label: t('Marker'),
renderTrigger: true,
default: markerEnabled,
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: 20,
default: markerSize,
description: t(
'Size of marker. Also applies to forecast observations.',
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.markerEnabled?.value),
},
},
],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
...legendSection,
[<h1 className="section-header">{t('X Axis')}</h1>],
[
{
name: 'x_axis_time_format',
config: {
...sharedControls.x_axis_time_format,
default: 'smart_date',
description: `${D3_TIME_FORMAT_DOCS}. ${t(
'When using other than adaptive formatting, labels may overlap.',
)}`,
},
},
],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
...richTooltipSection,
// eslint-disable-next-line react/jsx-key
[<h1 className="section-header">{t('Y Axis')}</h1>],
['y_axis_format'],
[
{
name: 'logAxis',
config: {
type: 'CheckboxControl',
label: t('Logarithmic y-axis'),
renderTrigger: true,
default: logAxis,
description: t('Logarithmic y-axis'),
},
},
],
[
{
name: 'minorSplitLine',
config: {
type: 'CheckboxControl',
label: t('Minor Split Line'),
renderTrigger: true,
default: minorSplitLine,
description: t('Draw split lines for minor y-axis ticks'),
},
},
],
[
{
name: 'truncateYAxis',
config: {
type: 'CheckboxControl',
label: t('Truncate Y Axis'),
default: truncateYAxis,
renderTrigger: true,
description: t(
'Truncate Y Axis. Can be overridden by specifying a min or max bound.',
),
},
},
],
[
{
name: 'y_axis_bounds',
config: {
type: 'BoundsControl',
label: t('Y Axis Bounds'),
renderTrigger: true,
default: yAxisBounds,
description: t(
'Bounds for the Y-axis. When left empty, the bounds are ' +
'dynamically defined based on the min/max of the data. Note that ' +
"this feature will only expand the axis range. It won't " +
"narrow the data's extent.",
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.truncateYAxis?.value),
},
},
],
],
},
],
controlOverrides: {
row_limit: {
default: rowLimit,
},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -0,0 +1,92 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
t,
ChartMetadata,
ChartPlugin,
AnnotationType,
Behavior,
} from '@superset-ui/core';
import buildQuery from '../buildQuery';
import controlPanel from './controlPanel';
import transformProps from '../transformProps';
import thumbnail from './images/thumbnail.png';
import {
EchartsTimeseriesChartProps,
EchartsTimeseriesFormData,
} from '../types';
import example1 from './images/Area1.png';
const areaTransformProps = (chartProps: EchartsTimeseriesChartProps) =>
transformProps({
...chartProps,
formData: { ...chartProps.formData, area: true },
});
export default class EchartsAreaChartPlugin extends ChartPlugin<
EchartsTimeseriesFormData,
EchartsTimeseriesChartProps
> {
/**
* 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: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: t(
'Time-series Area chart are similar to line chart in that they represent variables with the same scale, but area charts stack the metrics on top of each other. An area chart in Superset can be stream, stack, or expand.',
),
exampleGallery: [{ url: example1 }],
supportedAnnotationTypes: [
AnnotationType.Event,
AnnotationType.Formula,
AnnotationType.Interval,
AnnotationType.Timeseries,
],
name: t('Time-series Area Chart'),
tags: [
t('ECharts'),
t('Predictive'),
t('Advanced-Analytics'),
t('Aesthetic'),
t('Time'),
t('Line'),
t('Transformable'),
t('Stacked'),
t('Popular'),
],
thumbnail,
}),
transformProps: areaTransformProps,
});
}
}

View File

@@ -0,0 +1,208 @@
/**
* 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, { useCallback, useRef } from 'react';
import { ViewRootGroup } from 'echarts/types/src/util/types';
import GlobalModel from 'echarts/types/src/model/Global';
import ComponentModel from 'echarts/types/src/model/Component';
import { EchartsHandler, EventHandlers } from '../types';
import Echart from '../components/Echart';
import { TimeseriesChartTransformedProps } from './types';
import { currentSeries } from '../utils/series';
const TIMER_DURATION = 300;
// @ts-ignore
export default function EchartsTimeseries({
formData,
height,
width,
echartOptions,
groupby,
labelMap,
selectedValues,
setDataMask,
legendData = [],
}: TimeseriesChartTransformedProps) {
const { emitFilter, stack } = formData;
const echartRef = useRef<EchartsHandler | null>(null);
const lastTimeRef = useRef(Date.now());
const lastSelectedLegend = useRef('');
const clickTimer = useRef<ReturnType<typeof setTimeout>>();
const handleDoubleClickChange = useCallback(
(name?: string) => {
const echartInstance = echartRef.current?.getEchartInstance();
if (!name) {
currentSeries.legend = '';
echartInstance?.dispatchAction({
type: 'legendAllSelect',
});
} else {
legendData.forEach(datum => {
if (datum === name) {
currentSeries.legend = datum;
echartInstance?.dispatchAction({
type: 'legendSelect',
name: datum,
});
} else {
echartInstance?.dispatchAction({
type: 'legendUnSelect',
name: datum,
});
}
});
}
},
[legendData],
);
const getModelInfo = (target: ViewRootGroup, globalModel: GlobalModel) => {
let el = target;
let model: ComponentModel | null = null;
while (el) {
// eslint-disable-next-line no-underscore-dangle
const modelInfo = el.__ecComponentInfo;
if (modelInfo != null) {
model = globalModel.getComponent(modelInfo.mainType, modelInfo.index);
break;
}
el = el.parent;
}
return model;
};
const handleChange = useCallback(
(values: string[]) => {
if (!emitFilter) {
return;
}
const groupbyValues = values.map(value => labelMap[value]);
setDataMask({
extraFormData: {
filters:
values.length === 0
? []
: groupby.map((col, idx) => {
const val = groupbyValues.map(v => v[idx]);
if (val === null || val === undefined)
return {
col,
op: 'IS NULL',
};
return {
col,
op: 'IN',
val: val as (string | number | boolean)[],
};
}),
},
filterState: {
label: groupbyValues.length ? groupbyValues : undefined,
value: groupbyValues.length ? groupbyValues : null,
selectedValues: values.length ? values : null,
},
});
},
[groupby, labelMap, setDataMask],
);
const eventHandlers: EventHandlers = {
click: props => {
if (clickTimer.current) {
clearTimeout(clickTimer.current);
}
// Ensure that double-click events do not trigger single click event. So we put it in the timer.
clickTimer.current = setTimeout(() => {
const { seriesName: name } = props;
const values = Object.values(selectedValues);
if (values.includes(name)) {
handleChange(values.filter(v => v !== name));
} else {
handleChange([name]);
}
}, TIMER_DURATION);
},
mouseout: () => {
currentSeries.name = '';
},
mouseover: params => {
currentSeries.name = params.seriesName;
},
legendselectchanged: payload => {
const currentTime = Date.now();
// TIMER_DURATION is the interval between two legendselectchanged event
if (
currentTime - lastTimeRef.current < TIMER_DURATION &&
lastSelectedLegend.current === payload.name
) {
// execute dbclick
handleDoubleClickChange(payload.name);
} else {
lastTimeRef.current = currentTime;
// remember last selected legend
lastSelectedLegend.current = payload.name;
}
// if all legend is unselected, we keep all selected
if (Object.values(payload.selected).every(i => !i)) {
handleDoubleClickChange();
}
},
};
const zrEventHandlers: EventHandlers = {
dblclick: params => {
// clear single click timer
if (clickTimer.current) {
clearTimeout(clickTimer.current);
}
const pointInPixel = [params.offsetX, params.offsetY];
const echartInstance = echartRef.current?.getEchartInstance();
if (echartInstance?.containPixel('grid', pointInPixel)) {
// do not trigger if click unstacked chart's blank area
if (!stack && params.target?.type === 'ec-polygon') return;
// @ts-ignore
const globalModel = echartInstance.getModel();
const model = getModelInfo(params.target, globalModel);
const seriesCount = globalModel.getSeriesCount();
const currentSeriesIndices = globalModel.getCurrentSeriesIndices();
if (model) {
const { name } = model;
if (seriesCount !== currentSeriesIndices.length) {
handleDoubleClickChange();
} else {
handleDoubleClickChange(name);
}
}
}
},
};
return (
<Echart
ref={echartRef}
height={height}
width={width}
echartOptions={echartOptions}
eventHandlers={eventHandlers}
zrEventHandlers={zrEventHandlers}
selectedValues={selectedValues}
/>
);
}

View File

@@ -0,0 +1,256 @@
/**
* 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 { t } from '@superset-ui/core';
import {
ControlPanelConfig,
ControlPanelsContainerProps,
D3_TIME_FORMAT_DOCS,
emitFilterControl,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
import {
DEFAULT_FORM_DATA,
EchartsTimeseriesContributionType,
} from '../../types';
import {
legendSection,
richTooltipSection,
showValueSection,
} from '../../../controls';
const {
contributionMode,
logAxis,
markerEnabled,
markerSize,
minorSplitLine,
rowLimit,
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
sections.legacyTimeseriesTime,
{
label: t('Query'),
expanded: true,
controlSetRows: [
['metrics'],
['groupby'],
[
{
name: 'contributionMode',
config: {
type: 'SelectControl',
label: t('Contribution Mode'),
default: contributionMode,
choices: [
[null, 'None'],
[EchartsTimeseriesContributionType.Row, 'Total'],
[EchartsTimeseriesContributionType.Column, 'Series'],
],
description: t('Calculate contribution per series or total'),
},
},
],
['adhoc_filters'],
emitFilterControl,
['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'],
],
},
sections.advancedAnalyticsControls,
sections.annotationsAndLayersControls,
sections.forecastIntervalControls,
sections.titleControls,
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
...showValueSection,
[
{
name: 'markerEnabled',
config: {
type: 'CheckboxControl',
label: t('Marker'),
renderTrigger: true,
default: markerEnabled,
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: 20,
default: markerSize,
description: t(
'Size of marker. Also applies to forecast observations.',
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.markerEnabled?.value),
},
},
],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
...legendSection,
[<h1 className="section-header">{t('X Axis')}</h1>],
[
{
name: 'x_axis_time_format',
config: {
...sharedControls.x_axis_time_format,
default: 'smart_date',
description: `${D3_TIME_FORMAT_DOCS}. ${t(
'When using other than adaptive formatting, labels may overlap.',
)}`,
},
},
],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
// eslint-disable-next-line react/jsx-key
...richTooltipSection,
// eslint-disable-next-line react/jsx-key
[<h1 className="section-header">{t('Y Axis')}</h1>],
['y_axis_format'],
[
{
name: 'logAxis',
config: {
type: 'CheckboxControl',
label: t('Logarithmic y-axis'),
renderTrigger: true,
default: logAxis,
description: t('Logarithmic y-axis'),
},
},
],
[
{
name: 'minorSplitLine',
config: {
type: 'CheckboxControl',
label: t('Minor Split Line'),
renderTrigger: true,
default: minorSplitLine,
description: t('Draw split lines for minor y-axis ticks'),
},
},
],
[
{
name: 'truncateYAxis',
config: {
type: 'CheckboxControl',
label: t('Truncate Y Axis'),
default: truncateYAxis,
renderTrigger: true,
description: t(
'Its not recommended to truncate y-axis in Bar chart.',
),
},
},
],
[
{
name: 'y_axis_bounds',
config: {
type: 'BoundsControl',
label: t('Y Axis Bounds'),
renderTrigger: true,
default: yAxisBounds,
description: t(
'Bounds for the Y-axis. When left empty, the bounds are ' +
'dynamically defined based on the min/max of the data. Note that ' +
"this feature will only expand the axis range. It won't " +
"narrow the data's extent.",
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.truncateYAxis?.value),
},
},
],
],
},
],
controlOverrides: {
row_limit: {
default: rowLimit,
},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,93 @@
/**
* 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,
ChartMetadata,
ChartPlugin,
AnnotationType,
Behavior,
} from '@superset-ui/core';
import buildQuery from '../../buildQuery';
import controlPanel from './controlPanel';
import transformProps from '../../transformProps';
import thumbnail from './images/thumbnail.png';
import {
EchartsTimeseriesChartProps,
EchartsTimeseriesFormData,
EchartsTimeseriesSeriesType,
} from '../../types';
import example1 from './images/Bar1.png';
import example2 from './images/Bar2.png';
import example3 from './images/Bar3.png';
const barTransformProps = (chartProps: EchartsTimeseriesChartProps) =>
transformProps({
...chartProps,
formData: {
...chartProps.formData,
seriesType: EchartsTimeseriesSeriesType.Bar,
},
});
export default class EchartsTimeseriesBarChartPlugin extends ChartPlugin<
EchartsTimeseriesFormData,
EchartsTimeseriesChartProps
> {
constructor() {
super({
buildQuery,
controlPanel,
loadChart: () => import('../../EchartsTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: t(
'Time-series Bar Charts are used to show the changes in a metric over time as a series of bars.',
),
exampleGallery: [
{ url: example1 },
{ url: example2 },
{ url: example3 },
],
supportedAnnotationTypes: [
AnnotationType.Event,
AnnotationType.Formula,
AnnotationType.Interval,
AnnotationType.Timeseries,
],
name: t('Time-series Bar Chart v2'),
tags: [
t('ECharts'),
t('Predictive'),
t('Advanced-Analytics'),
t('Aesthetic'),
t('Time'),
t('Transformable'),
t('Stacked'),
t('Vertical'),
t('Bar'),
t('Popular'),
],
thumbnail,
}),
transformProps: barTransformProps,
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -0,0 +1,84 @@
/**
* 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,
ChartMetadata,
ChartPlugin,
AnnotationType,
Behavior,
} from '@superset-ui/core';
import buildQuery from '../../buildQuery';
import controlPanel from '../controlPanel';
import transformProps from '../../transformProps';
import thumbnail from './images/thumbnail.png';
import {
EchartsTimeseriesChartProps,
EchartsTimeseriesFormData,
EchartsTimeseriesSeriesType,
} from '../../types';
import example1 from './images/Line1.png';
import example2 from './images/Line2.png';
const lineTransformProps = (chartProps: EchartsTimeseriesChartProps) =>
transformProps({
...chartProps,
formData: {
...chartProps.formData,
seriesType: EchartsTimeseriesSeriesType.Line,
},
});
export default class EchartsTimeseriesLineChartPlugin extends ChartPlugin<
EchartsTimeseriesFormData,
EchartsTimeseriesChartProps
> {
constructor() {
super({
buildQuery,
controlPanel,
loadChart: () => import('../../EchartsTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: t(
'Time-series line chart is used to visualize repeated measurements taken over regular time intervals. Line chart is a type of chart which displays information as a series of data points connected by straight line segments. It is a basic type of chart common in many fields.',
),
exampleGallery: [{ url: example1 }, { url: example2 }],
supportedAnnotationTypes: [
AnnotationType.Event,
AnnotationType.Formula,
AnnotationType.Interval,
AnnotationType.Timeseries,
],
name: t('Time-series Line Chart'),
tags: [
t('ECharts'),
t('Predictive'),
t('Advanced-Analytics'),
t('Aesthetic'),
t('Line'),
t('Popular'),
],
thumbnail,
}),
transformProps: lineTransformProps,
});
}
}

View File

@@ -0,0 +1,236 @@
/**
* 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 { t } from '@superset-ui/core';
import {
ControlPanelConfig,
ControlPanelsContainerProps,
D3_TIME_FORMAT_DOCS,
emitFilterControl,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA } from '../../types';
import {
legendSection,
richTooltipSection,
showValueSection,
} from '../../../controls';
const {
logAxis,
markerEnabled,
markerSize,
minorSplitLine,
rowLimit,
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
sections.legacyTimeseriesTime,
{
label: t('Query'),
expanded: true,
controlSetRows: [
['metrics'],
['groupby'],
['adhoc_filters'],
emitFilterControl,
['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'],
],
},
sections.advancedAnalyticsControls,
sections.annotationsAndLayersControls,
sections.forecastIntervalControls,
sections.titleControls,
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
...showValueSection,
[
{
name: 'markerEnabled',
config: {
type: 'CheckboxControl',
label: t('Marker'),
renderTrigger: true,
default: markerEnabled,
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: markerSize,
description: t(
'Size of marker. Also applies to forecast observations.',
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.markerEnabled?.value),
},
},
],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
...legendSection,
[<h1 className="section-header">{t('X Axis')}</h1>],
[
{
name: 'x_axis_time_format',
config: {
...sharedControls.x_axis_time_format,
default: 'smart_date',
description: `${D3_TIME_FORMAT_DOCS}. ${t(
'When using other than adaptive formatting, labels may overlap.',
)}`,
},
},
],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
// eslint-disable-next-line react/jsx-key
...richTooltipSection,
// eslint-disable-next-line react/jsx-key
[<h1 className="section-header">{t('Y Axis')}</h1>],
['y_axis_format'],
[
{
name: 'logAxis',
config: {
type: 'CheckboxControl',
label: t('Logarithmic y-axis'),
renderTrigger: true,
default: logAxis,
description: t('Logarithmic y-axis'),
},
},
],
[
{
name: 'minorSplitLine',
config: {
type: 'CheckboxControl',
label: t('Minor Split Line'),
renderTrigger: true,
default: minorSplitLine,
description: t('Draw split lines for minor y-axis ticks'),
},
},
],
[
{
name: 'truncateYAxis',
config: {
type: 'CheckboxControl',
label: t('Truncate Y Axis'),
default: truncateYAxis,
renderTrigger: true,
description: t(
'Truncate Y Axis. Can be overridden by specifying a min or max bound.',
),
},
},
],
[
{
name: 'y_axis_bounds',
config: {
type: 'BoundsControl',
label: t('Y Axis Bounds'),
renderTrigger: true,
default: yAxisBounds,
description: t(
'Bounds for the Y-axis. When left empty, the bounds are ' +
'dynamically defined based on the min/max of the data. Note that ' +
"this feature will only expand the axis range. It won't " +
"narrow the data's extent.",
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.truncateYAxis?.value),
},
},
],
],
},
],
controlOverrides: {
row_limit: {
default: rowLimit,
},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,85 @@
/**
* 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,
ChartMetadata,
ChartPlugin,
AnnotationType,
Behavior,
} from '@superset-ui/core';
import buildQuery from '../../buildQuery';
import controlPanel from './controlPanel';
import transformProps from '../../transformProps';
import thumbnail from './images/thumbnail.png';
import {
EchartsTimeseriesChartProps,
EchartsTimeseriesFormData,
EchartsTimeseriesSeriesType,
} from '../../types';
import example1 from './images/Scatter1.png';
const scatterTransformProps = (chartProps: EchartsTimeseriesChartProps) =>
transformProps({
...chartProps,
formData: {
...chartProps.formData,
seriesType: EchartsTimeseriesSeriesType.Scatter,
},
});
export default class EchartsTimeseriesScatterChartPlugin extends ChartPlugin<
EchartsTimeseriesFormData,
EchartsTimeseriesChartProps
> {
constructor() {
super({
buildQuery,
controlPanel,
loadChart: () => import('../../EchartsTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: t(
'Time-series Scatter Plot has time on the horizontal axis in linear units, and the points are connected in order. It shows a statistical relationship between two variables.',
),
exampleGallery: [{ url: example1 }],
supportedAnnotationTypes: [
AnnotationType.Event,
AnnotationType.Formula,
AnnotationType.Interval,
AnnotationType.Timeseries,
],
name: t('Time-series Scatter Plot'),
tags: [
t('ECharts'),
t('Predictive'),
t('Advanced-Analytics'),
t('Aesthetic'),
t('Time'),
t('Transformable'),
t('Scatter'),
t('Popular'),
],
thumbnail,
}),
transformProps: scatterTransformProps,
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@@ -0,0 +1,84 @@
/**
* 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,
ChartMetadata,
ChartPlugin,
AnnotationType,
Behavior,
} from '@superset-ui/core';
import buildQuery from '../../buildQuery';
import controlPanel from '../controlPanel';
import transformProps from '../../transformProps';
import thumbnail from './images/thumbnail.png';
import {
EchartsTimeseriesChartProps,
EchartsTimeseriesFormData,
EchartsTimeseriesSeriesType,
} from '../../types';
import example1 from './images/SmoothLine1.png';
const smoothTransformProps = (chartProps: EchartsTimeseriesChartProps) =>
transformProps({
...chartProps,
formData: {
...chartProps.formData,
seriesType: EchartsTimeseriesSeriesType.Smooth,
},
});
export default class EchartsTimeseriesSmoothLineChartPlugin extends ChartPlugin<
EchartsTimeseriesFormData,
EchartsTimeseriesChartProps
> {
constructor() {
super({
buildQuery,
controlPanel,
loadChart: () => import('../../EchartsTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: t(
'Time-series Smooth-line is a variation of line chart. Without angles and hard edges, Smooth-line looks more smarter and more professional.',
),
exampleGallery: [{ url: example1 }],
supportedAnnotationTypes: [
AnnotationType.Event,
AnnotationType.Formula,
AnnotationType.Interval,
AnnotationType.Timeseries,
],
name: t('Time-series Smooth Line'),
tags: [
t('ECharts'),
t('Predictive'),
t('Advanced-Analytics'),
t('Aesthetic'),
t('Time'),
t('Line'),
t('Transformable'),
],
thumbnail,
}),
transformProps: smoothTransformProps,
});
}
}

View File

@@ -0,0 +1,253 @@
/**
* 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 { t } from '@superset-ui/core';
import {
ControlPanelConfig,
ControlPanelsContainerProps,
D3_TIME_FORMAT_DOCS,
emitFilterControl,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
import { DEFAULT_FORM_DATA, EchartsTimeseriesContributionType } from '../types';
import {
legendSection,
richTooltipSection,
showValueSection,
} from '../../controls';
const {
contributionMode,
logAxis,
markerEnabled,
markerSize,
minorSplitLine,
rowLimit,
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
sections.legacyTimeseriesTime,
{
label: t('Query'),
expanded: true,
controlSetRows: [
['metrics'],
['groupby'],
[
{
name: 'contributionMode',
config: {
type: 'SelectControl',
label: t('Contribution Mode'),
default: contributionMode,
choices: [
[null, 'None'],
[EchartsTimeseriesContributionType.Row, 'Total'],
[EchartsTimeseriesContributionType.Column, 'Series'],
],
description: t('Calculate contribution per series or total'),
},
},
],
['adhoc_filters'],
emitFilterControl,
['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'],
],
},
sections.advancedAnalyticsControls,
sections.annotationsAndLayersControls,
sections.forecastIntervalControls,
sections.titleControls,
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
...showValueSection,
[
{
name: 'markerEnabled',
config: {
type: 'CheckboxControl',
label: t('Marker'),
renderTrigger: true,
default: markerEnabled,
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: 20,
default: markerSize,
description: t(
'Size of marker. Also applies to forecast observations.',
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.markerEnabled?.value),
},
},
],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
...legendSection,
[<h1 className="section-header">{t('X Axis')}</h1>],
[
{
name: 'x_axis_time_format',
config: {
...sharedControls.x_axis_time_format,
default: 'smart_date',
description: `${D3_TIME_FORMAT_DOCS}. ${t(
'When using other than adaptive formatting, labels may overlap.',
)}`,
},
},
],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
// eslint-disable-next-line react/jsx-key
...richTooltipSection,
// eslint-disable-next-line react/jsx-key
[<h1 className="section-header">{t('Y Axis')}</h1>],
['y_axis_format'],
[
{
name: 'logAxis',
config: {
type: 'CheckboxControl',
label: t('Logarithmic y-axis'),
renderTrigger: true,
default: logAxis,
description: t('Logarithmic y-axis'),
},
},
],
[
{
name: 'minorSplitLine',
config: {
type: 'CheckboxControl',
label: t('Minor Split Line'),
renderTrigger: true,
default: minorSplitLine,
description: t('Draw split lines for minor y-axis ticks'),
},
},
],
[
{
name: 'truncateYAxis',
config: {
type: 'CheckboxControl',
label: t('Truncate Y Axis'),
default: truncateYAxis,
renderTrigger: true,
description: t(
'Truncate Y Axis. Can be overridden by specifying a min or max bound.',
),
},
},
],
[
{
name: 'y_axis_bounds',
config: {
type: 'BoundsControl',
label: t('Y Axis Bounds'),
renderTrigger: true,
default: yAxisBounds,
description: t(
'Bounds for the Y-axis. When left empty, the bounds are ' +
'dynamically defined based on the min/max of the data. Note that ' +
"this feature will only expand the axis range. It won't " +
"narrow the data's extent.",
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.truncateYAxis?.value),
},
},
],
],
},
],
controlOverrides: {
row_limit: {
default: rowLimit,
},
},
};
export default config;

View File

@@ -0,0 +1,309 @@
/**
* 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 { t } from '@superset-ui/core';
import {
ControlPanelConfig,
ControlPanelsContainerProps,
D3_TIME_FORMAT_DOCS,
sections,
sharedControls,
emitFilterControl,
} from '@superset-ui/chart-controls';
import {
DEFAULT_FORM_DATA,
EchartsTimeseriesContributionType,
EchartsTimeseriesSeriesType,
} from '../types';
import {
legendSection,
richTooltipSection,
showValueSection,
} from '../../controls';
const {
area,
contributionMode,
logAxis,
markerEnabled,
markerSize,
minorSplitLine,
opacity,
rowLimit,
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
sections.legacyTimeseriesTime,
{
label: t('Query'),
expanded: true,
controlSetRows: [
['metrics'],
['groupby'],
[
{
name: 'contributionMode',
config: {
type: 'SelectControl',
label: t('Contribution Mode'),
default: contributionMode,
choices: [
[null, 'None'],
[EchartsTimeseriesContributionType.Row, 'Total'],
[EchartsTimeseriesContributionType.Column, 'Series'],
],
description: t('Calculate contribution per series or total'),
},
},
],
['adhoc_filters'],
emitFilterControl,
['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'],
],
},
sections.advancedAnalyticsControls,
sections.annotationsAndLayersControls,
sections.forecastIntervalControls,
sections.titleControls,
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
[
{
name: 'seriesType',
config: {
type: 'SelectControl',
label: t('Step type'),
renderTrigger: true,
default: EchartsTimeseriesSeriesType.Start,
choices: [
[EchartsTimeseriesSeriesType.Start, 'Start'],
[EchartsTimeseriesSeriesType.Middle, 'Middle'],
[EchartsTimeseriesSeriesType.End, 'End'],
],
description: t(
'Defines whether the step should appear at the beginning, middle or end between two data points',
),
},
},
],
...showValueSection,
[
{
name: 'area',
config: {
type: 'CheckboxControl',
label: t('Area Chart'),
renderTrigger: true,
default: area,
description: t(
'Draw area under curves. Only applicable for line types.',
),
},
},
],
[
{
name: 'opacity',
config: {
type: 'SliderControl',
label: t('Area chart opacity'),
renderTrigger: true,
min: 0,
max: 1,
step: 0.1,
default: opacity,
description: t(
'Opacity of Area Chart. Also applies to confidence band.',
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.area?.value),
},
},
],
[
{
name: 'markerEnabled',
config: {
type: 'CheckboxControl',
label: t('Marker'),
renderTrigger: true,
default: markerEnabled,
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: 20,
default: markerSize,
description: t(
'Size of marker. Also applies to forecast observations.',
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.markerEnabled?.value),
},
},
],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
...legendSection,
[<h1 className="section-header">{t('X Axis')}</h1>],
[
{
name: 'x_axis_time_format',
config: {
...sharedControls.x_axis_time_format,
default: 'smart_date',
description: `${D3_TIME_FORMAT_DOCS}. ${t(
'When using other than adaptive formatting, labels may overlap.',
)}`,
},
},
],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
...richTooltipSection,
// eslint-disable-next-line react/jsx-key
[<h1 className="section-header">{t('Y Axis')}</h1>],
['y_axis_format'],
[
{
name: 'logAxis',
config: {
type: 'CheckboxControl',
label: t('Logarithmic y-axis'),
renderTrigger: true,
default: logAxis,
description: t('Logarithmic y-axis'),
},
},
],
[
{
name: 'minorSplitLine',
config: {
type: 'CheckboxControl',
label: t('Minor Split Line'),
renderTrigger: true,
default: minorSplitLine,
description: t('Draw split lines for minor y-axis ticks'),
},
},
],
[
{
name: 'truncateYAxis',
config: {
type: 'CheckboxControl',
label: t('Truncate Y Axis'),
default: truncateYAxis,
renderTrigger: true,
description: t(
'Truncate Y Axis. Can be overridden by specifying a min or max bound.',
),
},
},
],
[
{
name: 'y_axis_bounds',
config: {
type: 'BoundsControl',
label: t('Y Axis Bounds'),
renderTrigger: true,
default: yAxisBounds,
description: t(
'Bounds for the Y-axis. When left empty, the bounds are ' +
'dynamically defined based on the min/max of the data. Note that ' +
"this feature will only expand the axis range. It won't " +
"narrow the data's extent.",
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.truncateYAxis?.value),
},
},
],
],
},
],
controlOverrides: {
row_limit: {
default: rowLimit,
},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,75 @@
/**
* 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,
ChartMetadata,
ChartPlugin,
AnnotationType,
Behavior,
} from '@superset-ui/core';
import buildQuery from '../buildQuery';
import controlPanel from './controlPanel';
import transformProps from '../transformProps';
import thumbnail from './images/thumbnail.png';
import {
EchartsTimeseriesChartProps,
EchartsTimeseriesFormData,
} from '../types';
import example1 from './images/Step1.png';
import example2 from './images/Step2.png';
export default class EchartsTimeseriesStepChartPlugin extends ChartPlugin<
EchartsTimeseriesFormData,
EchartsTimeseriesChartProps
> {
constructor() {
super({
buildQuery,
controlPanel,
loadChart: () => import('../EchartsTimeseries'),
metadata: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: t(
'Time-series Stepped-line graph (also called step chart) is a variation of line chart but with the line forming a series of steps between data points. A step chart can be useful when you want to show the changes that occur at irregular intervals.',
),
exampleGallery: [{ url: example1 }, { url: example2 }],
supportedAnnotationTypes: [
AnnotationType.Event,
AnnotationType.Formula,
AnnotationType.Interval,
AnnotationType.Timeseries,
],
name: t('Time-series Stepped Line'),
tags: [
t('ECharts'),
t('Predictive'),
t('Advanced-Analytics'),
t('Aesthetic'),
t('Time'),
t('Transformable'),
t('Stacked'),
],
thumbnail,
}),
transformProps,
});
}
}

View File

@@ -0,0 +1,79 @@
/**
* 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,
normalizeOrderBy,
RollingType,
PostProcessingPivot,
} from '@superset-ui/core';
import {
rollingWindowOperator,
timeCompareOperator,
isValidTimeCompare,
sortOperator,
pivotOperator,
resampleOperator,
contributionOperator,
prophetOperator,
} from '@superset-ui/chart-controls';
export default function buildQuery(formData: QueryFormData) {
return buildQueryContext(formData, baseQueryObject => {
const pivotOperatorInRuntime: PostProcessingPivot | undefined =
pivotOperator(formData, {
...baseQueryObject,
is_timeseries: true,
});
if (
pivotOperatorInRuntime &&
Object.values(RollingType).includes(formData.rolling_type)
) {
pivotOperatorInRuntime.options = {
...pivotOperatorInRuntime.options,
...{
flatten_columns: false,
reset_index: false,
},
};
}
return [
{
...baseQueryObject,
is_timeseries: true,
// todo: move `normalizeOrderBy to extractQueryFields`
orderby: normalizeOrderBy(baseQueryObject).orderby,
time_offsets: isValidTimeCompare(formData, baseQueryObject)
? formData.time_compare
: [],
post_processing: [
resampleOperator(formData, baseQueryObject),
timeCompareOperator(formData, baseQueryObject),
sortOperator(formData, { ...baseQueryObject, is_timeseries: true }),
// in order to be able to rolling in multiple series, must do pivot before rollingOperator
pivotOperatorInRuntime,
rollingWindowOperator(formData, baseQueryObject),
contributionOperator(formData, baseQueryObject),
prophetOperator(formData, baseQueryObject),
],
},
];
});
}

View File

@@ -0,0 +1,312 @@
/**
* 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 { t } from '@superset-ui/core';
import {
ControlPanelConfig,
ControlPanelsContainerProps,
D3_TIME_FORMAT_DOCS,
sections,
sharedControls,
emitFilterControl,
} from '@superset-ui/chart-controls';
import {
DEFAULT_FORM_DATA,
EchartsTimeseriesContributionType,
EchartsTimeseriesSeriesType,
} from './types';
import {
legendSection,
richTooltipSection,
showValueSection,
} from '../controls';
const {
area,
contributionMode,
logAxis,
markerEnabled,
markerSize,
minorSplitLine,
opacity,
rowLimit,
seriesType,
truncateYAxis,
yAxisBounds,
zoomable,
xAxisLabelRotation,
} = DEFAULT_FORM_DATA;
const config: ControlPanelConfig = {
controlPanelSections: [
sections.legacyTimeseriesTime,
{
label: t('Query'),
expanded: true,
controlSetRows: [
['metrics'],
['groupby'],
[
{
name: 'contributionMode',
config: {
type: 'SelectControl',
label: t('Contribution Mode'),
default: contributionMode,
choices: [
[null, 'None'],
[EchartsTimeseriesContributionType.Row, 'Total'],
[EchartsTimeseriesContributionType.Column, 'Series'],
],
description: t('Calculate contribution per series or total'),
},
},
],
['adhoc_filters'],
emitFilterControl,
['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'],
],
},
sections.advancedAnalyticsControls,
sections.annotationsAndLayersControls,
sections.forecastIntervalControls,
sections.titleControls,
{
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['color_scheme'],
[
{
name: 'seriesType',
config: {
type: 'SelectControl',
label: t('Series Style'),
renderTrigger: true,
default: seriesType,
choices: [
[EchartsTimeseriesSeriesType.Line, 'Line'],
[EchartsTimeseriesSeriesType.Scatter, 'Scatter'],
[EchartsTimeseriesSeriesType.Smooth, 'Smooth Line'],
[EchartsTimeseriesSeriesType.Bar, 'Bar'],
[EchartsTimeseriesSeriesType.Start, 'Step - start'],
[EchartsTimeseriesSeriesType.Middle, 'Step - middle'],
[EchartsTimeseriesSeriesType.End, 'Step - end'],
],
description: t('Series chart type (line, bar etc)'),
},
},
],
...showValueSection,
[
{
name: 'area',
config: {
type: 'CheckboxControl',
label: t('Area Chart'),
renderTrigger: true,
default: area,
description: t(
'Draw area under curves. Only applicable for line types.',
),
},
},
],
[
{
name: 'opacity',
config: {
type: 'SliderControl',
label: t('Area chart opacity'),
renderTrigger: true,
min: 0,
max: 1,
step: 0.1,
default: opacity,
description: t(
'Opacity of Area Chart. Also applies to confidence band.',
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.area?.value),
},
},
],
[
{
name: 'markerEnabled',
config: {
type: 'CheckboxControl',
label: t('Marker'),
renderTrigger: true,
default: markerEnabled,
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: 20,
default: markerSize,
description: t(
'Size of marker. Also applies to forecast observations.',
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.markerEnabled?.value),
},
},
],
[
{
name: 'zoomable',
config: {
type: 'CheckboxControl',
label: t('Data Zoom'),
default: zoomable,
renderTrigger: true,
description: t('Enable data zooming controls'),
},
},
],
...legendSection,
[<h1 className="section-header">{t('X Axis')}</h1>],
[
{
name: 'x_axis_time_format',
config: {
...sharedControls.x_axis_time_format,
default: 'smart_date',
description: `${D3_TIME_FORMAT_DOCS}. ${t(
'When using other than adaptive formatting, labels may overlap.',
)}`,
},
},
],
[
{
name: 'xAxisLabelRotation',
config: {
type: 'SelectControl',
freeForm: true,
clearable: false,
label: t('Rotate x axis label'),
choices: [
[0, '0°'],
[45, '45°'],
],
default: xAxisLabelRotation,
renderTrigger: true,
description: t(
'Input field supports custom rotation. e.g. 30 for 30°',
),
},
},
],
...richTooltipSection,
// eslint-disable-next-line react/jsx-key
[<h1 className="section-header">{t('Y Axis')}</h1>],
['y_axis_format'],
[
{
name: 'logAxis',
config: {
type: 'CheckboxControl',
label: t('Logarithmic y-axis'),
renderTrigger: true,
default: logAxis,
description: t('Logarithmic y-axis'),
},
},
],
[
{
name: 'minorSplitLine',
config: {
type: 'CheckboxControl',
label: t('Minor Split Line'),
renderTrigger: true,
default: minorSplitLine,
description: t('Draw split lines for minor y-axis ticks'),
},
},
],
[
{
name: 'truncateYAxis',
config: {
type: 'CheckboxControl',
label: t('Truncate Y Axis'),
default: truncateYAxis,
renderTrigger: true,
description: t(
'Truncate Y Axis. Can be overridden by specifying a min or max bound.',
),
},
},
],
[
{
name: 'y_axis_bounds',
config: {
type: 'BoundsControl',
label: t('Y Axis Bounds'),
renderTrigger: true,
default: yAxisBounds,
description: t(
'Bounds for the Y-axis. When left empty, the bounds are ' +
'dynamically defined based on the min/max of the data. Note that ' +
"this feature will only expand the axis range. It won't " +
"narrow the data's extent.",
),
visibility: ({ controls }: ControlPanelsContainerProps) =>
Boolean(controls?.truncateYAxis?.value),
},
},
],
],
},
],
controlOverrides: {
row_limit: {
default: rowLimit,
},
},
};
export default config;

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,83 @@
/**
* 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,
ChartMetadata,
ChartPlugin,
AnnotationType,
Behavior,
} from '@superset-ui/core';
import buildQuery from './buildQuery';
import controlPanel from './controlPanel';
import transformProps from './transformProps';
import thumbnail from './images/thumbnail.png';
import {
EchartsTimeseriesChartProps,
EchartsTimeseriesFormData,
} from './types';
import example from './images/Time-series_Chart.jpg';
export default class EchartsTimeseriesChartPlugin extends ChartPlugin<
EchartsTimeseriesFormData,
EchartsTimeseriesChartProps
> {
/**
* 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: new ChartMetadata({
behaviors: [Behavior.INTERACTIVE_CHART],
category: t('Evolution'),
credits: ['https://echarts.apache.org'],
description: t(
'Swiss army knife for visualizing time series data. Choose between step, line, scatter, and bar charts. This viz type has many customization options as well.',
),
exampleGallery: [{ url: example }],
supportedAnnotationTypes: [
AnnotationType.Event,
AnnotationType.Formula,
AnnotationType.Interval,
AnnotationType.Timeseries,
],
name: t('Time-series Chart'),
tags: [
t('Advanced-Analytics'),
t('Aesthetic'),
t('Line'),
t('Predictive'),
t('Time'),
t('Transformable'),
],
thumbnail,
}),
transformProps,
});
}
}

View File

@@ -0,0 +1,372 @@
/**
* 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 camelcase */
import {
AnnotationLayer,
CategoricalColorNamespace,
getNumberFormatter,
isEventAnnotationLayer,
isFormulaAnnotationLayer,
isIntervalAnnotationLayer,
isTimeseriesAnnotationLayer,
TimeseriesChartDataResponseResult,
DataRecordValue,
} from '@superset-ui/core';
import { EChartsCoreOption, SeriesOption } from 'echarts';
import {
DEFAULT_FORM_DATA,
EchartsTimeseriesChartProps,
EchartsTimeseriesFormData,
TimeseriesChartTransformedProps,
} from './types';
import { ForecastSeriesEnum, ProphetValue } from '../types';
import { parseYAxisBound } from '../utils/controls';
import {
dedupSeries,
extractTimeseriesSeries,
getLegendProps,
currentSeries,
} from '../utils/series';
import { extractAnnotationLabels } from '../utils/annotation';
import {
extractForecastSeriesContext,
extractForecastSeriesContexts,
extractProphetValuesFromTooltipParams,
formatProphetTooltipSeries,
rebaseTimeseriesDatum,
} from '../utils/prophet';
import { defaultGrid, defaultTooltip, defaultYAxis } from '../defaults';
import {
getPadding,
getTooltipTimeFormatter,
getXAxisFormatter,
transformEventAnnotation,
transformFormulaAnnotation,
transformIntervalAnnotation,
transformSeries,
transformTimeseriesAnnotation,
} from './transformers';
import { TIMESERIES_CONSTANTS } from '../constants';
export default function transformProps(
chartProps: EchartsTimeseriesChartProps,
): TimeseriesChartTransformedProps {
const {
width,
height,
filterState,
formData,
hooks,
queriesData,
datasource,
} = chartProps;
const { verboseMap = {} } = datasource;
const { annotation_data: annotationData_, data = [] } =
queriesData[0] as TimeseriesChartDataResponseResult;
const annotationData = annotationData_ || {};
const {
area,
annotationLayers,
colorScheme,
contributionMode,
forecastEnabled,
legendOrientation,
legendType,
legendMargin,
logAxis,
markerEnabled,
markerSize,
opacity,
minorSplitLine,
seriesType,
showLegend,
stack,
truncateYAxis,
yAxisFormat,
xAxisTimeFormat,
yAxisBounds,
tooltipTimeFormat,
tooltipSortByMetric,
zoomable,
richTooltip,
xAxisLabelRotation,
emitFilter,
groupby,
showValue,
onlyTotal,
xAxisTitle,
yAxisTitle,
xAxisTitleMargin,
yAxisTitleMargin,
yAxisTitlePosition,
}: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
const colorScale = CategoricalColorNamespace.getScale(colorScheme as string);
const rebasedData = rebaseTimeseriesDatum(data, verboseMap);
const rawSeries = extractTimeseriesSeries(rebasedData, {
fillNeighborValue: stack && !forecastEnabled ? 0 : undefined,
});
const seriesContexts = extractForecastSeriesContexts(
Object.values(rawSeries).map(series => series.name as string),
);
const series: SeriesOption[] = [];
const formatter = getNumberFormatter(contributionMode ? ',.0%' : yAxisFormat);
const totalStackedValues: number[] = [];
const showValueIndexes: number[] = [];
rebasedData.forEach(data => {
const values = Object.keys(data).reduce((prev, curr) => {
if (curr === '__timestamp') {
return prev;
}
const value = data[curr] || 0;
return prev + (value as number);
}, 0);
totalStackedValues.push(values);
});
if (stack) {
rawSeries.forEach((entry, seriesIndex) => {
const { data = [] } = entry;
(data as [Date, number][]).forEach((datum, dataIndex) => {
if (datum[1] !== null) {
showValueIndexes[dataIndex] = seriesIndex;
}
});
});
}
rawSeries.forEach(entry => {
const transformedSeries = transformSeries(entry, colorScale, {
area,
filterState,
seriesContexts,
markerEnabled,
markerSize,
areaOpacity: opacity,
seriesType,
stack,
formatter,
showValue,
onlyTotal,
totalStackedValues,
showValueIndexes,
richTooltip,
});
if (transformedSeries) series.push(transformedSeries);
});
const selectedValues = (filterState.selectedValues || []).reduce(
(acc: Record<string, number>, selectedValue: string) => {
const index = series.findIndex(({ name }) => name === selectedValue);
return {
...acc,
[index]: selectedValue,
};
},
{},
);
annotationLayers
.filter((layer: AnnotationLayer) => layer.show)
.forEach((layer: AnnotationLayer) => {
if (isFormulaAnnotationLayer(layer))
series.push(transformFormulaAnnotation(layer, data, colorScale));
else if (isIntervalAnnotationLayer(layer)) {
series.push(
...transformIntervalAnnotation(
layer,
data,
annotationData,
colorScale,
),
);
} else if (isEventAnnotationLayer(layer)) {
series.push(
...transformEventAnnotation(layer, data, annotationData, colorScale),
);
} else if (isTimeseriesAnnotationLayer(layer)) {
series.push(
...transformTimeseriesAnnotation(
layer,
markerSize,
data,
annotationData,
),
);
}
});
// yAxisBounds need to be parsed to replace incompatible values with undefined
let [min, max] = (yAxisBounds || []).map(parseYAxisBound);
// default to 0-100% range when doing row-level contribution chart
if (contributionMode === 'row' && stack) {
if (min === undefined) min = 0;
if (max === undefined) max = 1;
}
const tooltipFormatter = getTooltipTimeFormatter(tooltipTimeFormat);
const xAxisFormatter = getXAxisFormatter(xAxisTimeFormat);
const labelMap = series.reduce(
(acc: Record<string, DataRecordValue[]>, datum) => {
const name: string = datum.name as string;
return {
...acc,
[name]: [name],
};
},
{},
);
const { setDataMask = () => {} } = hooks;
const addYAxisLabelOffset = !!yAxisTitle;
const addXAxisLabelOffset = !!xAxisTitle;
const padding = getPadding(
showLegend,
legendOrientation,
addYAxisLabelOffset,
zoomable,
legendMargin,
addXAxisLabelOffset,
yAxisTitlePosition,
yAxisTitleMargin,
xAxisTitleMargin,
);
const legendData = rawSeries
.filter(
entry =>
extractForecastSeriesContext(entry.name || '').type ===
ForecastSeriesEnum.Observation,
)
.map(entry => entry.name || '')
.concat(extractAnnotationLabels(annotationLayers, annotationData));
const echartOptions: EChartsCoreOption = {
useUTC: true,
grid: {
...defaultGrid,
...padding,
},
xAxis: {
type: 'time',
name: xAxisTitle,
nameGap: xAxisTitleMargin,
nameLocation: 'middle',
axisLabel: {
hideOverlap: true,
formatter: xAxisFormatter,
rotate: xAxisLabelRotation,
},
},
yAxis: {
...defaultYAxis,
type: logAxis ? 'log' : 'value',
min,
max,
minorTick: { show: true },
minorSplitLine: { show: minorSplitLine },
axisLabel: { formatter },
scale: truncateYAxis,
name: yAxisTitle,
nameGap: yAxisTitleMargin,
nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
},
tooltip: {
...defaultTooltip,
appendToBody: true,
trigger: richTooltip ? 'axis' : 'item',
formatter: (params: any) => {
const xValue: number = richTooltip
? params[0].value[0]
: params.value[0];
const prophetValue: any[] = richTooltip ? params : [params];
if (richTooltip && tooltipSortByMetric) {
prophetValue.sort((a, b) => b.data[1] - a.data[1]);
}
const rows: Array<string> = [`${tooltipFormatter(xValue)}`];
const prophetValues: Record<string, ProphetValue> =
extractProphetValuesFromTooltipParams(prophetValue);
Object.keys(prophetValues).forEach(key => {
const value = prophetValues[key];
const content = formatProphetTooltipSeries({
...value,
seriesName: key,
formatter,
});
if (currentSeries.name === key) {
rows.push(`<span style="font-weight: 700">${content}</span>`);
} else {
rows.push(`<span style="opacity: 0.7">${content}</span>`);
}
});
return rows.join('<br />');
},
},
legend: {
...getLegendProps(legendType, legendOrientation, showLegend, zoomable),
data: legendData as string[],
},
series: dedupSeries(series),
toolbox: {
show: zoomable,
top: TIMESERIES_CONSTANTS.toolboxTop,
right: TIMESERIES_CONSTANTS.toolboxRight,
feature: {
dataZoom: {
yAxisIndex: false,
title: {
zoom: 'zoom area',
back: 'restore zoom',
},
},
},
},
dataZoom: zoomable
? [
{
type: 'slider',
start: TIMESERIES_CONSTANTS.dataZoomStart,
end: TIMESERIES_CONSTANTS.dataZoomEnd,
bottom: TIMESERIES_CONSTANTS.zoomBottom,
},
]
: [],
};
return {
echartOptions,
emitFilter,
formData,
groupby,
height,
labelMap,
selectedValues,
setDataMask,
width,
legendData,
};
}

View File

@@ -0,0 +1,484 @@
/**
* 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 {
AnnotationData,
AnnotationOpacity,
CategoricalColorScale,
EventAnnotationLayer,
FilterState,
FormulaAnnotationLayer,
getTimeFormatter,
IntervalAnnotationLayer,
isTimeseriesAnnotationResult,
NumberFormatter,
smartDateDetailedFormatter,
smartDateFormatter,
TimeFormatter,
TimeseriesAnnotationLayer,
TimeseriesDataRecord,
} from '@superset-ui/core';
import { SeriesOption } from 'echarts';
import {
CallbackDataParams,
DefaultStatesMixin,
ItemStyleOption,
LineStyleOption,
OptionName,
SeriesLabelOption,
SeriesLineLabelOption,
ZRLineType,
} from 'echarts/types/src/util/types';
import {
MarkArea1DDataItemOption,
MarkArea2DDataItemOption,
} from 'echarts/types/src/component/marker/MarkAreaModel';
import { MarkLine1DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel';
import { extractForecastSeriesContext } from '../utils/prophet';
import { ForecastSeriesEnum, LegendOrientation } from '../types';
import { EchartsTimeseriesSeriesType } from './types';
import {
evalFormula,
extractRecordAnnotations,
formatAnnotationLabel,
parseAnnotationOpacity,
} from '../utils/annotation';
import { currentSeries, getChartPadding } from '../utils/series';
import { OpacityEnum, TIMESERIES_CONSTANTS } from '../constants';
export function transformSeries(
series: SeriesOption,
colorScale: CategoricalColorScale,
opts: {
area?: boolean;
filterState?: FilterState;
seriesContexts?: { [key: string]: ForecastSeriesEnum[] };
markerEnabled?: boolean;
markerSize?: number;
areaOpacity?: number;
seriesType?: EchartsTimeseriesSeriesType;
stack?: boolean;
yAxisIndex?: number;
showValue?: boolean;
onlyTotal?: boolean;
formatter?: NumberFormatter;
totalStackedValues?: number[];
showValueIndexes?: number[];
richTooltip?: boolean;
},
): SeriesOption | undefined {
const { name } = series;
const {
area,
filterState,
seriesContexts = {},
markerEnabled,
markerSize,
areaOpacity = 1,
seriesType,
stack,
yAxisIndex = 0,
showValue,
onlyTotal,
formatter,
totalStackedValues = [],
showValueIndexes = [],
richTooltip,
} = opts;
const contexts = seriesContexts[name || ''] || [];
const hasForecast =
contexts.includes(ForecastSeriesEnum.ForecastTrend) ||
contexts.includes(ForecastSeriesEnum.ForecastLower) ||
contexts.includes(ForecastSeriesEnum.ForecastUpper);
const forecastSeries = extractForecastSeriesContext(name || '');
const isConfidenceBand =
forecastSeries.type === ForecastSeriesEnum.ForecastLower ||
forecastSeries.type === ForecastSeriesEnum.ForecastUpper;
const isFiltered =
filterState?.selectedValues && !filterState?.selectedValues.includes(name);
const opacity = isFiltered
? OpacityEnum.SemiTransparent
: OpacityEnum.NonTransparent;
// don't create a series if doing a stack or area chart and the result
// is a confidence band
if ((stack || area) && isConfidenceBand) return undefined;
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' || (hasForecast && isObservation))
) {
plotType = 'scatter';
} else if (isConfidenceBand) {
plotType = 'line';
} else {
plotType = seriesType === 'bar' ? 'bar' : 'line';
}
const itemStyle = {
color: colorScale(forecastSeries.name),
opacity,
};
let emphasis = {};
let showSymbol = false;
if (!isConfidenceBand) {
if (plotType === 'scatter') {
showSymbol = true;
} else if (hasForecast && isObservation) {
showSymbol = true;
} else if (plotType === 'line' && showValue) {
showSymbol = true;
} else if (plotType === 'line' && !richTooltip && !markerEnabled) {
// this is hack to make timeseries line chart clickable when tooltip trigger is 'item'
// so that the chart can emit cross-filtering
showSymbol = true;
itemStyle.opacity = 0;
emphasis = {
itemStyle: {
opacity: 1,
},
};
} else if (markerEnabled) {
showSymbol = true;
}
}
const lineStyle = isConfidenceBand
? { opacity: OpacityEnum.Transparent }
: { opacity };
return {
...series,
yAxisIndex,
name: forecastSeries.name,
itemStyle,
// @ts-ignore
type: plotType,
smooth: seriesType === 'smooth',
triggerLineEvent: true,
// @ts-ignore
step: ['start', 'middle', 'end'].includes(seriesType as string)
? seriesType
: undefined,
stack: stackId,
lineStyle,
areaStyle:
area || forecastSeries.type === ForecastSeriesEnum.ForecastUpper
? {
opacity: opacity * areaOpacity,
}
: undefined,
emphasis,
showSymbol,
symbolSize: markerSize,
label: {
show: !!showValue,
position: 'top',
formatter: (params: any) => {
const {
value: [, numericValue],
dataIndex,
seriesIndex,
seriesName,
} = params;
const isSelectedLegend = currentSeries.legend === seriesName;
if (!formatter) return numericValue;
if (!stack || !onlyTotal || isSelectedLegend) {
return formatter(numericValue);
}
if (seriesIndex === showValueIndexes[dataIndex]) {
return formatter(totalStackedValues[dataIndex]);
}
return '';
},
},
};
}
export function transformFormulaAnnotation(
layer: FormulaAnnotationLayer,
data: TimeseriesDataRecord[],
colorScale: CategoricalColorScale,
): SeriesOption {
const { name, color, opacity, width, style } = layer;
return {
name,
id: name,
itemStyle: {
color: color || colorScale(name),
},
lineStyle: {
opacity: parseAnnotationOpacity(opacity),
type: style as ZRLineType,
width,
},
type: 'line',
smooth: true,
data: evalFormula(layer, data),
symbolSize: 0,
};
}
export function transformIntervalAnnotation(
layer: IntervalAnnotationLayer,
data: TimeseriesDataRecord[],
annotationData: AnnotationData,
colorScale: CategoricalColorScale,
): SeriesOption[] {
const series: SeriesOption[] = [];
const annotations = extractRecordAnnotations(layer, annotationData);
annotations.forEach(annotation => {
const { name, color, opacity, showLabel } = layer;
const { descriptions, intervalEnd, time, title } = annotation;
const label = formatAnnotationLabel(name, title, descriptions);
const intervalData: (
| MarkArea1DDataItemOption
| MarkArea2DDataItemOption
)[] = [
[
{
name: label,
xAxis: time,
},
{
xAxis: intervalEnd,
},
],
];
const intervalLabel: SeriesLabelOption = showLabel
? {
show: true,
color: '#000000',
position: 'insideTop',
verticalAlign: 'top',
fontWeight: 'bold',
// @ts-ignore
emphasis: {
position: 'insideTop',
verticalAlign: 'top',
backgroundColor: '#ffffff',
},
}
: {
show: false,
color: '#000000',
// @ts-ignore
emphasis: {
fontWeight: 'bold',
show: true,
position: 'insideTop',
verticalAlign: 'top',
backgroundColor: '#ffffff',
},
};
series.push({
id: `Interval - ${label}`,
type: 'line',
animation: false,
markArea: {
silent: false,
itemStyle: {
color: color || colorScale(name),
opacity: parseAnnotationOpacity(opacity || AnnotationOpacity.Medium),
emphasis: {
opacity: 0.8,
},
} as ItemStyleOption,
label: intervalLabel,
data: intervalData,
},
});
});
return series;
}
export function transformEventAnnotation(
layer: EventAnnotationLayer,
data: TimeseriesDataRecord[],
annotationData: AnnotationData,
colorScale: CategoricalColorScale,
): SeriesOption[] {
const series: SeriesOption[] = [];
const annotations = extractRecordAnnotations(layer, annotationData);
annotations.forEach(annotation => {
const { name, color, opacity, style, width, showLabel } = layer;
const { descriptions, time, title } = annotation;
const label = formatAnnotationLabel(name, title, descriptions);
const eventData: MarkLine1DDataItemOption[] = [
{
name: label,
xAxis: time as unknown as number,
},
];
const lineStyle: LineStyleOption & DefaultStatesMixin['emphasis'] = {
width,
type: style as ZRLineType,
color: color || colorScale(name),
opacity: parseAnnotationOpacity(opacity),
emphasis: {
width: width ? width + 1 : width,
opacity: 1,
},
};
const eventLabel: SeriesLineLabelOption = showLabel
? {
show: true,
color: '#000000',
position: 'insideEndTop',
fontWeight: 'bold',
formatter: (params: CallbackDataParams) => params.name,
// @ts-ignore
emphasis: {
backgroundColor: '#ffffff',
},
}
: {
show: false,
color: '#000000',
position: 'insideEndTop',
// @ts-ignore
emphasis: {
formatter: (params: CallbackDataParams) => params.name,
fontWeight: 'bold',
show: true,
backgroundColor: '#ffffff',
},
};
series.push({
id: `Event - ${label}`,
type: 'line',
animation: false,
markLine: {
silent: false,
symbol: 'none',
lineStyle,
label: eventLabel,
data: eventData,
},
});
});
return series;
}
export function transformTimeseriesAnnotation(
layer: TimeseriesAnnotationLayer,
markerSize: number,
data: TimeseriesDataRecord[],
annotationData: AnnotationData,
): SeriesOption[] {
const series: SeriesOption[] = [];
const { hideLine, name, opacity, showMarkers, style, width } = layer;
const result = annotationData[name];
if (isTimeseriesAnnotationResult(result)) {
result.forEach(annotation => {
const { key, values } = annotation;
series.push({
type: 'line',
id: key,
name: key,
data: values.map(row => [row.x, row.y] as [OptionName, number]),
symbolSize: showMarkers ? markerSize : 0,
lineStyle: {
opacity: parseAnnotationOpacity(opacity),
type: style as ZRLineType,
width: hideLine ? 0 : width,
},
});
});
}
return series;
}
export function getPadding(
showLegend: boolean,
legendOrientation: LegendOrientation,
addYAxisTitleOffset: boolean,
zoomable: boolean,
margin?: string | number | null,
addXAxisTitleOffset?: boolean,
yAxisTitlePosition?: string,
yAxisTitleMargin?: number,
xAxisTitleMargin?: number,
): {
bottom: number;
left: number;
right: number;
top: number;
} {
const yAxisOffset = addYAxisTitleOffset
? TIMESERIES_CONSTANTS.yAxisLabelTopOffset
: 0;
const xAxisOffset = addXAxisTitleOffset ? xAxisTitleMargin || 0 : 0;
return getChartPadding(showLegend, legendOrientation, margin, {
top:
yAxisTitlePosition && yAxisTitlePosition === 'Top'
? TIMESERIES_CONSTANTS.gridOffsetTop + (yAxisTitleMargin || 0)
: TIMESERIES_CONSTANTS.gridOffsetTop + yAxisOffset,
bottom: zoomable
? TIMESERIES_CONSTANTS.gridOffsetBottomZoomable + xAxisOffset
: TIMESERIES_CONSTANTS.gridOffsetBottom + xAxisOffset,
left:
yAxisTitlePosition === 'Left'
? TIMESERIES_CONSTANTS.gridOffsetLeft + (yAxisTitleMargin || 0)
: TIMESERIES_CONSTANTS.gridOffsetLeft,
right:
showLegend && legendOrientation === LegendOrientation.Right
? 0
: TIMESERIES_CONSTANTS.gridOffsetRight,
});
}
export function getTooltipTimeFormatter(
format?: string,
): TimeFormatter | StringConstructor {
if (format === smartDateFormatter.id) {
return smartDateDetailedFormatter;
}
if (format) {
return getTimeFormatter(format);
}
return String;
}
export function getXAxisFormatter(
format?: string,
): TimeFormatter | StringConstructor | undefined {
if (format === smartDateFormatter.id || !format) {
return undefined;
}
if (format) {
return getTimeFormatter(format);
}
return String;
}

View File

@@ -0,0 +1,129 @@
/**
* 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 {
AnnotationLayer,
ChartDataResponseResult,
ChartProps,
QueryFormColumn,
QueryFormData,
TimeGranularity,
} from '@superset-ui/core';
import { sections } from '@superset-ui/chart-controls';
import {
DEFAULT_LEGEND_FORM_DATA,
EchartsLegendFormData,
EChartTransformedProps,
EchartsTitleFormData,
DEFAULT_TITLE_FORM_DATA,
} from '../types';
export enum EchartsTimeseriesContributionType {
Row = 'row',
Column = 'column',
}
export enum EchartsTimeseriesSeriesType {
Line = 'line',
Scatter = 'scatter',
Smooth = 'smooth',
Bar = 'bar',
Start = 'start',
Middle = 'middle',
End = 'end',
}
export type EchartsTimeseriesFormData = QueryFormData & {
annotationLayers: AnnotationLayer[];
area: boolean;
colorScheme?: string;
contributionMode?: EchartsTimeseriesContributionType;
forecastEnabled: boolean;
forecastPeriods: number;
forecastInterval: number;
forecastSeasonalityDaily: null;
forecastSeasonalityWeekly: null;
forecastSeasonalityYearly: null;
logAxis: boolean;
markerEnabled: boolean;
markerSize: number;
minorSplitLine: boolean;
opacity: number;
orderDesc: boolean;
rowLimit: number;
seriesType: EchartsTimeseriesSeriesType;
stack: boolean;
tooltipTimeFormat?: string;
truncateYAxis: boolean;
yAxisFormat?: string;
xAxisTimeFormat?: string;
timeGrainSqla?: TimeGranularity;
yAxisBounds: [number | undefined | null, number | undefined | null];
zoomable: boolean;
richTooltip: boolean;
xAxisLabelRotation: number;
emitFilter: boolean;
groupby: QueryFormColumn[];
showValue: boolean;
onlyTotal: boolean;
} & EchartsLegendFormData &
EchartsTitleFormData;
// @ts-ignore
export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
...DEFAULT_LEGEND_FORM_DATA,
annotationLayers: sections.annotationLayers,
area: false,
forecastEnabled: sections.FORECAST_DEFAULT_DATA.forecastEnabled,
forecastInterval: sections.FORECAST_DEFAULT_DATA.forecastInterval,
forecastPeriods: sections.FORECAST_DEFAULT_DATA.forecastPeriods,
forecastSeasonalityDaily:
sections.FORECAST_DEFAULT_DATA.forecastSeasonalityDaily,
forecastSeasonalityWeekly:
sections.FORECAST_DEFAULT_DATA.forecastSeasonalityWeekly,
forecastSeasonalityYearly:
sections.FORECAST_DEFAULT_DATA.forecastSeasonalityYearly,
logAxis: false,
markerEnabled: false,
markerSize: 6,
minorSplitLine: false,
opacity: 0.2,
orderDesc: true,
rowLimit: 10000,
seriesType: EchartsTimeseriesSeriesType.Line,
stack: false,
tooltipTimeFormat: 'smart_date',
truncateYAxis: false,
yAxisBounds: [null, null],
zoomable: false,
richTooltip: true,
xAxisLabelRotation: 0,
emitFilter: false,
groupby: [],
showValue: false,
onlyTotal: false,
...DEFAULT_TITLE_FORM_DATA,
};
export interface EchartsTimeseriesChartProps extends ChartProps {
formData: EchartsTimeseriesFormData;
queriesData: ChartDataResponseResult[];
}
export type TimeseriesChartTransformedProps =
EChartTransformedProps<EchartsTimeseriesFormData>;