mirror of https://github.com/apache/superset.git
feat(plugin-chart-echarts): support horizontal bar chart (#19918)
* feat(plugin-chart-echarts): support horizontal bar chart * Update superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx Co-authored-by: Evan Rusackas <evan@preset.io> * Update superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com> * Update superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com> * Update superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com> * improve controlpanel * default value * fix ut Co-authored-by: Evan Rusackas <evan@preset.io> Co-authored-by: Ville Brofeldt <33317356+villebro@users.noreply.github.com>
This commit is contained in:
parent
d5802f7896
commit
9854d2d0e8
|
@ -21,8 +21,10 @@ import { t } from '@superset-ui/core';
|
||||||
import { ControlPanelSectionConfig } from '../types';
|
import { ControlPanelSectionConfig } from '../types';
|
||||||
import { formatSelectOptions } from '../utils';
|
import { formatSelectOptions } from '../utils';
|
||||||
|
|
||||||
const TITLE_MARGIN_OPTIONS: number[] = [15, 30, 50, 75, 100, 125, 150, 200];
|
export const TITLE_MARGIN_OPTIONS: number[] = [
|
||||||
const TITLE_POSITION_OPTIONS: string[] = ['Left', 'Top'];
|
15, 30, 50, 75, 100, 125, 150, 200,
|
||||||
|
];
|
||||||
|
export const TITLE_POSITION_OPTIONS: string[] = ['Left', 'Top'];
|
||||||
export const titleControls: ControlPanelSectionConfig = {
|
export const titleControls: ControlPanelSectionConfig = {
|
||||||
label: t('Chart Title'),
|
label: t('Chart Title'),
|
||||||
tabOverride: 'customize',
|
tabOverride: 'customize',
|
||||||
|
|
|
@ -21,8 +21,11 @@ import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
ControlPanelConfig,
|
ControlPanelConfig,
|
||||||
ControlPanelsContainerProps,
|
ControlPanelsContainerProps,
|
||||||
|
ControlSetRow,
|
||||||
|
ControlStateMapping,
|
||||||
D3_TIME_FORMAT_DOCS,
|
D3_TIME_FORMAT_DOCS,
|
||||||
emitFilterControl,
|
emitFilterControl,
|
||||||
|
formatSelectOptions,
|
||||||
sections,
|
sections,
|
||||||
sharedControls,
|
sharedControls,
|
||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
|
@ -30,6 +33,7 @@ import {
|
||||||
import {
|
import {
|
||||||
DEFAULT_FORM_DATA,
|
DEFAULT_FORM_DATA,
|
||||||
EchartsTimeseriesContributionType,
|
EchartsTimeseriesContributionType,
|
||||||
|
OrientationType,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import {
|
import {
|
||||||
legendSection,
|
legendSection,
|
||||||
|
@ -49,7 +53,217 @@ const {
|
||||||
yAxisBounds,
|
yAxisBounds,
|
||||||
zoomable,
|
zoomable,
|
||||||
xAxisLabelRotation,
|
xAxisLabelRotation,
|
||||||
|
orientation,
|
||||||
} = DEFAULT_FORM_DATA;
|
} = DEFAULT_FORM_DATA;
|
||||||
|
|
||||||
|
function createAxisTitleControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||||
|
const isXAxis = axis === 'x';
|
||||||
|
const isVertical = (controls: ControlStateMapping) =>
|
||||||
|
Boolean(controls?.orientation.value === OrientationType.vertical);
|
||||||
|
const isHorizental = (controls: ControlStateMapping) =>
|
||||||
|
Boolean(controls?.orientation.value === OrientationType.horizontal);
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'x_axis_title',
|
||||||
|
config: {
|
||||||
|
type: 'TextControl',
|
||||||
|
label: t('Axis Title'),
|
||||||
|
renderTrigger: true,
|
||||||
|
default: '',
|
||||||
|
description: t('Changing this control takes effect instantly'),
|
||||||
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
|
isXAxis ? isVertical(controls) : isHorizental(controls),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'x_axis_title_margin',
|
||||||
|
config: {
|
||||||
|
type: 'SelectControl',
|
||||||
|
freeForm: true,
|
||||||
|
clearable: true,
|
||||||
|
label: t('AXIS TITLE MARGIN'),
|
||||||
|
renderTrigger: true,
|
||||||
|
default: sections.TITLE_MARGIN_OPTIONS[0],
|
||||||
|
choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS),
|
||||||
|
description: t('Changing this control takes effect instantly'),
|
||||||
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
|
isXAxis ? isVertical(controls) : isHorizental(controls),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'y_axis_title',
|
||||||
|
config: {
|
||||||
|
type: 'TextControl',
|
||||||
|
label: t('Axis Title'),
|
||||||
|
renderTrigger: true,
|
||||||
|
default: '',
|
||||||
|
description: t('Changing this control takes effect instantly'),
|
||||||
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
|
isXAxis ? isHorizental(controls) : isVertical(controls),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'y_axis_title_margin',
|
||||||
|
config: {
|
||||||
|
type: 'SelectControl',
|
||||||
|
freeForm: true,
|
||||||
|
clearable: true,
|
||||||
|
label: t('AXIS TITLE MARGIN'),
|
||||||
|
renderTrigger: true,
|
||||||
|
default: sections.TITLE_MARGIN_OPTIONS[0],
|
||||||
|
choices: formatSelectOptions(sections.TITLE_MARGIN_OPTIONS),
|
||||||
|
description: t('Changing this control takes effect instantly'),
|
||||||
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
|
isXAxis ? isHorizental(controls) : isVertical(controls),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'y_axis_title_position',
|
||||||
|
config: {
|
||||||
|
type: 'SelectControl',
|
||||||
|
freeForm: true,
|
||||||
|
clearable: false,
|
||||||
|
label: t('AXIS TITLE POSITION'),
|
||||||
|
renderTrigger: true,
|
||||||
|
default: sections.TITLE_POSITION_OPTIONS[0],
|
||||||
|
choices: formatSelectOptions(sections.TITLE_POSITION_OPTIONS),
|
||||||
|
description: t('Changing this control takes effect instantly'),
|
||||||
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
|
isXAxis ? isHorizental(controls) : isVertical(controls),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] {
|
||||||
|
const isXAxis = axis === 'x';
|
||||||
|
const isVertical = (controls: ControlStateMapping) =>
|
||||||
|
Boolean(controls?.orientation.value === OrientationType.vertical);
|
||||||
|
const isHorizental = (controls: ControlStateMapping) =>
|
||||||
|
Boolean(controls?.orientation.value === OrientationType.horizontal);
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
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.',
|
||||||
|
)}`,
|
||||||
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
|
isXAxis ? isVertical(controls) : isHorizental(controls),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'xAxisLabelRotation',
|
||||||
|
config: {
|
||||||
|
type: 'SelectControl',
|
||||||
|
freeForm: true,
|
||||||
|
clearable: false,
|
||||||
|
label: t('Rotate axis label'),
|
||||||
|
choices: [
|
||||||
|
[0, '0°'],
|
||||||
|
[45, '45°'],
|
||||||
|
],
|
||||||
|
default: xAxisLabelRotation,
|
||||||
|
renderTrigger: true,
|
||||||
|
description: t(
|
||||||
|
'Input field supports custom rotation. e.g. 30 for 30°',
|
||||||
|
),
|
||||||
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
|
isXAxis ? isVertical(controls) : isHorizental(controls),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'y_axis_format',
|
||||||
|
config: {
|
||||||
|
...sharedControls.y_axis_format,
|
||||||
|
label: t('Axis Format'),
|
||||||
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
|
isXAxis ? isHorizental(controls) : isVertical(controls),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'logAxis',
|
||||||
|
config: {
|
||||||
|
type: 'CheckboxControl',
|
||||||
|
label: t('Logarithmic axis'),
|
||||||
|
renderTrigger: true,
|
||||||
|
default: logAxis,
|
||||||
|
description: t('Logarithmic axis'),
|
||||||
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
|
isXAxis ? isHorizental(controls) : isVertical(controls),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'minorSplitLine',
|
||||||
|
config: {
|
||||||
|
type: 'CheckboxControl',
|
||||||
|
label: t('Minor Split Line'),
|
||||||
|
renderTrigger: true,
|
||||||
|
default: minorSplitLine,
|
||||||
|
description: t('Draw split lines for minor axis ticks'),
|
||||||
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
|
isXAxis ? isHorizental(controls) : isVertical(controls),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'truncateYAxis',
|
||||||
|
config: {
|
||||||
|
type: 'CheckboxControl',
|
||||||
|
label: t('Truncate Axis'),
|
||||||
|
default: truncateYAxis,
|
||||||
|
renderTrigger: true,
|
||||||
|
description: t('It’s not recommended to truncate axis in Bar chart.'),
|
||||||
|
visibility: ({ controls }: ControlPanelsContainerProps) =>
|
||||||
|
isXAxis ? isHorizental(controls) : isVertical(controls),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'y_axis_bounds',
|
||||||
|
config: {
|
||||||
|
type: 'BoundsControl',
|
||||||
|
label: t('Axis Bounds'),
|
||||||
|
renderTrigger: true,
|
||||||
|
default: yAxisBounds,
|
||||||
|
description: t(
|
||||||
|
'Bounds for the 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) &&
|
||||||
|
(isXAxis ? isHorizental(controls) : isVertical(controls)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const config: ControlPanelConfig = {
|
const config: ControlPanelConfig = {
|
||||||
controlPanelSections: [
|
controlPanelSections: [
|
||||||
sections.legacyTimeseriesTime,
|
sections.legacyTimeseriesTime,
|
||||||
|
@ -87,7 +301,39 @@ const config: ControlPanelConfig = {
|
||||||
sections.advancedAnalyticsControls,
|
sections.advancedAnalyticsControls,
|
||||||
sections.annotationsAndLayersControls,
|
sections.annotationsAndLayersControls,
|
||||||
sections.forecastIntervalControls,
|
sections.forecastIntervalControls,
|
||||||
sections.titleControls,
|
{
|
||||||
|
label: t('Chart Orientation'),
|
||||||
|
expanded: true,
|
||||||
|
controlSetRows: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'orientation',
|
||||||
|
config: {
|
||||||
|
type: 'RadioButtonControl',
|
||||||
|
renderTrigger: true,
|
||||||
|
label: t('Bar orientation'),
|
||||||
|
default: orientation,
|
||||||
|
options: [
|
||||||
|
[OrientationType.vertical, t('Vertical')],
|
||||||
|
[OrientationType.horizontal, t('Horizontal')],
|
||||||
|
],
|
||||||
|
description: t('Orientation of bar chart'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('Chart Title'),
|
||||||
|
tabOverride: 'customize',
|
||||||
|
expanded: true,
|
||||||
|
controlSetRows: [
|
||||||
|
[<div className="section-header">{t('X Axis')}</div>],
|
||||||
|
...createAxisTitleControl('x'),
|
||||||
|
[<div className="section-header">{t('Y Axis')}</div>],
|
||||||
|
...createAxisTitleControl('y'),
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t('Chart Options'),
|
label: t('Chart Options'),
|
||||||
expanded: true,
|
expanded: true,
|
||||||
|
@ -140,101 +386,10 @@ const config: ControlPanelConfig = {
|
||||||
],
|
],
|
||||||
...legendSection,
|
...legendSection,
|
||||||
[<div className="section-header">{t('X Axis')}</div>],
|
[<div className="section-header">{t('X Axis')}</div>],
|
||||||
[
|
...createAxisControl('x'),
|
||||||
{
|
|
||||||
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,
|
...richTooltipSection,
|
||||||
// eslint-disable-next-line react/jsx-key
|
|
||||||
[<div className="section-header">{t('Y Axis')}</div>],
|
[<div className="section-header">{t('Y Axis')}</div>],
|
||||||
|
...createAxisControl('y'),
|
||||||
['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(
|
|
||||||
'It’s 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),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
EchartsTimeseriesFormData,
|
EchartsTimeseriesFormData,
|
||||||
EchartsTimeseriesSeriesType,
|
EchartsTimeseriesSeriesType,
|
||||||
TimeseriesChartTransformedProps,
|
TimeseriesChartTransformedProps,
|
||||||
|
OrientationType,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { ForecastSeriesEnum, ForecastValue } from '../types';
|
import { ForecastSeriesEnum, ForecastValue } from '../types';
|
||||||
import { parseYAxisBound } from '../utils/controls';
|
import { parseYAxisBound } from '../utils/controls';
|
||||||
|
@ -138,16 +139,19 @@ export default function transformProps(
|
||||||
yAxisTitlePosition,
|
yAxisTitlePosition,
|
||||||
sliceId,
|
sliceId,
|
||||||
timeGrainSqla,
|
timeGrainSqla,
|
||||||
|
orientation,
|
||||||
}: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
|
}: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
|
||||||
|
|
||||||
const colorScale = CategoricalColorNamespace.getScale(colorScheme as string);
|
const colorScale = CategoricalColorNamespace.getScale(colorScheme as string);
|
||||||
const rebasedData = rebaseForecastDatum(data, verboseMap);
|
const rebasedData = rebaseForecastDatum(data, verboseMap);
|
||||||
const xAxisCol =
|
const xAxisCol =
|
||||||
verboseMap[xAxisOrig] || getColumnLabel(xAxisOrig || DTTM_ALIAS);
|
verboseMap[xAxisOrig] || getColumnLabel(xAxisOrig || DTTM_ALIAS);
|
||||||
|
const isHorizontal = orientation === OrientationType.horizontal;
|
||||||
const rawSeries = extractSeries(rebasedData, {
|
const rawSeries = extractSeries(rebasedData, {
|
||||||
fillNeighborValue: stack && !forecastEnabled ? 0 : undefined,
|
fillNeighborValue: stack && !forecastEnabled ? 0 : undefined,
|
||||||
xAxis: xAxisCol,
|
xAxis: xAxisCol,
|
||||||
removeNulls: seriesType === EchartsTimeseriesSeriesType.Scatter,
|
removeNulls: seriesType === EchartsTimeseriesSeriesType.Scatter,
|
||||||
|
isHorizontal,
|
||||||
});
|
});
|
||||||
const seriesContexts = extractForecastSeriesContexts(
|
const seriesContexts = extractForecastSeriesContexts(
|
||||||
Object.values(rawSeries).map(series => series.name as string),
|
Object.values(rawSeries).map(series => series.name as string),
|
||||||
|
@ -213,6 +217,7 @@ export default function transformProps(
|
||||||
thresholdValues,
|
thresholdValues,
|
||||||
richTooltip,
|
richTooltip,
|
||||||
sliceId,
|
sliceId,
|
||||||
|
isHorizontal,
|
||||||
});
|
});
|
||||||
if (transformedSeries) series.push(transformedSeries);
|
if (transformedSeries) series.push(transformedSeries);
|
||||||
});
|
});
|
||||||
|
@ -325,57 +330,66 @@ export default function transformProps(
|
||||||
.map(entry => entry.name || '')
|
.map(entry => entry.name || '')
|
||||||
.concat(extractAnnotationLabels(annotationLayers, annotationData));
|
.concat(extractAnnotationLabels(annotationLayers, annotationData));
|
||||||
|
|
||||||
|
let xAxis: any = {
|
||||||
|
type: xAxisType,
|
||||||
|
name: xAxisTitle,
|
||||||
|
nameGap: convertInteger(xAxisTitleMargin),
|
||||||
|
nameLocation: 'middle',
|
||||||
|
axisLabel: {
|
||||||
|
hideOverlap: true,
|
||||||
|
formatter: xAxisFormatter,
|
||||||
|
rotate: xAxisLabelRotation,
|
||||||
|
},
|
||||||
|
minInterval:
|
||||||
|
xAxisType === 'time' && timeGrainSqla
|
||||||
|
? TimeGrainToTimestamp[timeGrainSqla]
|
||||||
|
: 0,
|
||||||
|
};
|
||||||
|
let yAxis: any = {
|
||||||
|
...defaultYAxis,
|
||||||
|
type: logAxis ? 'log' : 'value',
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
minorTick: { show: true },
|
||||||
|
minorSplitLine: { show: minorSplitLine },
|
||||||
|
axisLabel: { formatter },
|
||||||
|
scale: truncateYAxis,
|
||||||
|
name: yAxisTitle,
|
||||||
|
nameGap: convertInteger(yAxisTitleMargin),
|
||||||
|
nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isHorizontal) {
|
||||||
|
[xAxis, yAxis] = [yAxis, xAxis];
|
||||||
|
[padding.bottom, padding.left] = [padding.left, padding.bottom];
|
||||||
|
}
|
||||||
|
|
||||||
const echartOptions: EChartsCoreOption = {
|
const echartOptions: EChartsCoreOption = {
|
||||||
useUTC: true,
|
useUTC: true,
|
||||||
grid: {
|
grid: {
|
||||||
...defaultGrid,
|
...defaultGrid,
|
||||||
...padding,
|
...padding,
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis,
|
||||||
type: xAxisType,
|
yAxis,
|
||||||
name: xAxisTitle,
|
|
||||||
nameGap: convertInteger(xAxisTitleMargin),
|
|
||||||
nameLocation: 'middle',
|
|
||||||
axisLabel: {
|
|
||||||
hideOverlap: true,
|
|
||||||
formatter: xAxisFormatter,
|
|
||||||
rotate: xAxisLabelRotation,
|
|
||||||
},
|
|
||||||
minInterval:
|
|
||||||
xAxisType === 'time' && timeGrainSqla
|
|
||||||
? TimeGrainToTimestamp[timeGrainSqla]
|
|
||||||
: 0,
|
|
||||||
},
|
|
||||||
yAxis: {
|
|
||||||
...defaultYAxis,
|
|
||||||
type: logAxis ? 'log' : 'value',
|
|
||||||
min,
|
|
||||||
max,
|
|
||||||
minorTick: { show: true },
|
|
||||||
minorSplitLine: { show: minorSplitLine },
|
|
||||||
axisLabel: { formatter },
|
|
||||||
scale: truncateYAxis,
|
|
||||||
name: yAxisTitle,
|
|
||||||
nameGap: convertInteger(yAxisTitleMargin),
|
|
||||||
nameLocation: yAxisTitlePosition === 'Left' ? 'middle' : 'end',
|
|
||||||
},
|
|
||||||
tooltip: {
|
tooltip: {
|
||||||
...defaultTooltip,
|
...defaultTooltip,
|
||||||
appendToBody: true,
|
appendToBody: true,
|
||||||
trigger: richTooltip ? 'axis' : 'item',
|
trigger: richTooltip ? 'axis' : 'item',
|
||||||
formatter: (params: any) => {
|
formatter: (params: any) => {
|
||||||
|
const [xIndex, yIndex] = isHorizontal ? [1, 0] : [0, 1];
|
||||||
const xValue: number = richTooltip
|
const xValue: number = richTooltip
|
||||||
? params[0].value[0]
|
? params[0].value[xIndex]
|
||||||
: params.value[0];
|
: params.value[xIndex];
|
||||||
const forecastValue: any[] = richTooltip ? params : [params];
|
const forecastValue: any[] = richTooltip ? params : [params];
|
||||||
|
|
||||||
if (richTooltip && tooltipSortByMetric) {
|
if (richTooltip && tooltipSortByMetric) {
|
||||||
forecastValue.sort((a, b) => b.data[1] - a.data[1]);
|
forecastValue.sort((a, b) => b.data[yIndex] - a.data[yIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rows: Array<string> = [`${tooltipFormatter(xValue)}`];
|
const rows: Array<string> = [`${tooltipFormatter(xValue)}`];
|
||||||
const forecastValues: Record<string, ForecastValue> =
|
const forecastValues: Record<string, ForecastValue> =
|
||||||
extractForecastValuesFromTooltipParams(forecastValue);
|
extractForecastValuesFromTooltipParams(forecastValue, isHorizontal);
|
||||||
|
|
||||||
Object.keys(forecastValues).forEach(key => {
|
Object.keys(forecastValues).forEach(key => {
|
||||||
const value = forecastValues[key];
|
const value = forecastValues[key];
|
||||||
|
|
|
@ -86,6 +86,7 @@ export function transformSeries(
|
||||||
richTooltip?: boolean;
|
richTooltip?: boolean;
|
||||||
seriesKey?: OptionName;
|
seriesKey?: OptionName;
|
||||||
sliceId?: number;
|
sliceId?: number;
|
||||||
|
isHorizontal?: boolean;
|
||||||
},
|
},
|
||||||
): SeriesOption | undefined {
|
): SeriesOption | undefined {
|
||||||
const { name } = series;
|
const { name } = series;
|
||||||
|
@ -108,6 +109,7 @@ export function transformSeries(
|
||||||
richTooltip,
|
richTooltip,
|
||||||
seriesKey,
|
seriesKey,
|
||||||
sliceId,
|
sliceId,
|
||||||
|
isHorizontal = false,
|
||||||
} = opts;
|
} = opts;
|
||||||
const contexts = seriesContexts[name || ''] || [];
|
const contexts = seriesContexts[name || ''] || [];
|
||||||
const hasForecast =
|
const hasForecast =
|
||||||
|
@ -217,14 +219,10 @@ export function transformSeries(
|
||||||
symbolSize: markerSize,
|
symbolSize: markerSize,
|
||||||
label: {
|
label: {
|
||||||
show: !!showValue,
|
show: !!showValue,
|
||||||
position: 'top',
|
position: isHorizontal ? 'right' : 'top',
|
||||||
formatter: (params: any) => {
|
formatter: (params: any) => {
|
||||||
const {
|
const { value, dataIndex, seriesIndex, seriesName } = params;
|
||||||
value: [, numericValue],
|
const numericValue = isHorizontal ? value[0] : value[1];
|
||||||
dataIndex,
|
|
||||||
seriesIndex,
|
|
||||||
seriesName,
|
|
||||||
} = params;
|
|
||||||
const isSelectedLegend = currentSeries.legend === seriesName;
|
const isSelectedLegend = currentSeries.legend === seriesName;
|
||||||
if (!formatter) return numericValue;
|
if (!formatter) return numericValue;
|
||||||
if (!stack || isSelectedLegend) return formatter(numericValue);
|
if (!stack || isSelectedLegend) return formatter(numericValue);
|
||||||
|
|
|
@ -38,6 +38,11 @@ export enum EchartsTimeseriesContributionType {
|
||||||
Column = 'column',
|
Column = 'column',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrientationType {
|
||||||
|
vertical = 'vertical',
|
||||||
|
horizontal = 'horizontal',
|
||||||
|
}
|
||||||
|
|
||||||
export enum EchartsTimeseriesSeriesType {
|
export enum EchartsTimeseriesSeriesType {
|
||||||
Line = 'line',
|
Line = 'line',
|
||||||
Scatter = 'scatter',
|
Scatter = 'scatter',
|
||||||
|
@ -82,6 +87,7 @@ export type EchartsTimeseriesFormData = QueryFormData & {
|
||||||
showValue: boolean;
|
showValue: boolean;
|
||||||
onlyTotal: boolean;
|
onlyTotal: boolean;
|
||||||
percentageThreshold: number;
|
percentageThreshold: number;
|
||||||
|
orientation?: OrientationType;
|
||||||
} & EchartsLegendFormData &
|
} & EchartsLegendFormData &
|
||||||
EchartsTitleFormData;
|
EchartsTitleFormData;
|
||||||
|
|
||||||
|
@ -119,6 +125,7 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
|
||||||
showValue: false,
|
showValue: false,
|
||||||
onlyTotal: false,
|
onlyTotal: false,
|
||||||
percentageThreshold: 0,
|
percentageThreshold: 0,
|
||||||
|
orientation: OrientationType.vertical,
|
||||||
...DEFAULT_TITLE_FORM_DATA,
|
...DEFAULT_TITLE_FORM_DATA,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -53,12 +53,13 @@ export const extractForecastSeriesContexts = (
|
||||||
|
|
||||||
export const extractForecastValuesFromTooltipParams = (
|
export const extractForecastValuesFromTooltipParams = (
|
||||||
params: any[],
|
params: any[],
|
||||||
|
isHorizontal = false,
|
||||||
): Record<string, ForecastValue> => {
|
): Record<string, ForecastValue> => {
|
||||||
const values: Record<string, ForecastValue> = {};
|
const values: Record<string, ForecastValue> = {};
|
||||||
params.forEach(param => {
|
params.forEach(param => {
|
||||||
const { marker, seriesId, value } = param;
|
const { marker, seriesId, value } = param;
|
||||||
const context = extractForecastSeriesContext(seriesId);
|
const context = extractForecastSeriesContext(seriesId);
|
||||||
const numericValue = (value as [Date, number])[1];
|
const numericValue = isHorizontal ? value[0] : value[1];
|
||||||
if (numericValue) {
|
if (numericValue) {
|
||||||
if (!(context.name in values))
|
if (!(context.name in values))
|
||||||
values[context.name] = {
|
values[context.name] = {
|
||||||
|
|
|
@ -42,9 +42,15 @@ export function extractSeries(
|
||||||
fillNeighborValue?: number;
|
fillNeighborValue?: number;
|
||||||
xAxis?: string;
|
xAxis?: string;
|
||||||
removeNulls?: boolean;
|
removeNulls?: boolean;
|
||||||
|
isHorizontal?: boolean;
|
||||||
} = {},
|
} = {},
|
||||||
): SeriesOption[] {
|
): SeriesOption[] {
|
||||||
const { fillNeighborValue, xAxis = DTTM_ALIAS, removeNulls = false } = opts;
|
const {
|
||||||
|
fillNeighborValue,
|
||||||
|
xAxis = DTTM_ALIAS,
|
||||||
|
removeNulls = false,
|
||||||
|
isHorizontal = false,
|
||||||
|
} = opts;
|
||||||
if (data.length === 0) return [];
|
if (data.length === 0) return [];
|
||||||
const rows: DataRecord[] = data.map(datum => ({
|
const rows: DataRecord[] = data.map(datum => ({
|
||||||
...datum,
|
...datum,
|
||||||
|
@ -69,7 +75,8 @@ export function extractSeries(
|
||||||
: row[key],
|
: row[key],
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
.filter(obs => !removeNulls || (obs[0] !== null && obs[1] !== null)),
|
.filter(obs => !removeNulls || (obs[0] !== null && obs[1] !== null))
|
||||||
|
.map(obs => (isHorizontal ? [obs[1], obs[0]] : obs)),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue