diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/sections/chartTitle.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/sections/chartTitle.tsx
index 314e983c589ae..eda3ac33152d4 100644
--- a/superset-frontend/packages/superset-ui-chart-controls/src/sections/chartTitle.tsx
+++ b/superset-frontend/packages/superset-ui-chart-controls/src/sections/chartTitle.tsx
@@ -21,8 +21,10 @@ import { t } from '@superset-ui/core';
import { ControlPanelSectionConfig } from '../types';
import { formatSelectOptions } from '../utils';
-const TITLE_MARGIN_OPTIONS: number[] = [15, 30, 50, 75, 100, 125, 150, 200];
-const TITLE_POSITION_OPTIONS: string[] = ['Left', 'Top'];
+export const TITLE_MARGIN_OPTIONS: number[] = [
+ 15, 30, 50, 75, 100, 125, 150, 200,
+];
+export const TITLE_POSITION_OPTIONS: string[] = ['Left', 'Top'];
export const titleControls: ControlPanelSectionConfig = {
label: t('Chart Title'),
tabOverride: 'customize',
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
index a3b74aa12f4fe..8716bffe2d389 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx
@@ -21,8 +21,11 @@ import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core';
import {
ControlPanelConfig,
ControlPanelsContainerProps,
+ ControlSetRow,
+ ControlStateMapping,
D3_TIME_FORMAT_DOCS,
emitFilterControl,
+ formatSelectOptions,
sections,
sharedControls,
} from '@superset-ui/chart-controls';
@@ -30,6 +33,7 @@ import {
import {
DEFAULT_FORM_DATA,
EchartsTimeseriesContributionType,
+ OrientationType,
} from '../../types';
import {
legendSection,
@@ -49,7 +53,217 @@ const {
yAxisBounds,
zoomable,
xAxisLabelRotation,
+ orientation,
} = 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 = {
controlPanelSections: [
sections.legacyTimeseriesTime,
@@ -87,7 +301,39 @@ const config: ControlPanelConfig = {
sections.advancedAnalyticsControls,
sections.annotationsAndLayersControls,
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: [
+ [
{t('X Axis')}
],
+ ...createAxisTitleControl('x'),
+ [{t('Y Axis')}
],
+ ...createAxisTitleControl('y'),
+ ],
+ },
{
label: t('Chart Options'),
expanded: true,
@@ -140,101 +386,10 @@ const config: ControlPanelConfig = {
],
...legendSection,
[{t('X Axis')}
],
- [
- {
- 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
+ ...createAxisControl('x'),
...richTooltipSection,
- // eslint-disable-next-line react/jsx-key
[{t('Y Axis')}
],
-
- ['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),
- },
- },
- ],
+ ...createAxisControl('y'),
],
},
],
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
index 3a9491153b65d..85fa656f2ed01 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts
@@ -39,6 +39,7 @@ import {
EchartsTimeseriesFormData,
EchartsTimeseriesSeriesType,
TimeseriesChartTransformedProps,
+ OrientationType,
} from './types';
import { ForecastSeriesEnum, ForecastValue } from '../types';
import { parseYAxisBound } from '../utils/controls';
@@ -138,16 +139,19 @@ export default function transformProps(
yAxisTitlePosition,
sliceId,
timeGrainSqla,
+ orientation,
}: EchartsTimeseriesFormData = { ...DEFAULT_FORM_DATA, ...formData };
const colorScale = CategoricalColorNamespace.getScale(colorScheme as string);
const rebasedData = rebaseForecastDatum(data, verboseMap);
const xAxisCol =
verboseMap[xAxisOrig] || getColumnLabel(xAxisOrig || DTTM_ALIAS);
+ const isHorizontal = orientation === OrientationType.horizontal;
const rawSeries = extractSeries(rebasedData, {
fillNeighborValue: stack && !forecastEnabled ? 0 : undefined,
xAxis: xAxisCol,
removeNulls: seriesType === EchartsTimeseriesSeriesType.Scatter,
+ isHorizontal,
});
const seriesContexts = extractForecastSeriesContexts(
Object.values(rawSeries).map(series => series.name as string),
@@ -213,6 +217,7 @@ export default function transformProps(
thresholdValues,
richTooltip,
sliceId,
+ isHorizontal,
});
if (transformedSeries) series.push(transformedSeries);
});
@@ -325,57 +330,66 @@ export default function transformProps(
.map(entry => entry.name || '')
.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 = {
useUTC: true,
grid: {
...defaultGrid,
...padding,
},
- xAxis: {
- type: xAxisType,
- 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',
- },
+ xAxis,
+ yAxis,
tooltip: {
...defaultTooltip,
appendToBody: true,
trigger: richTooltip ? 'axis' : 'item',
formatter: (params: any) => {
+ const [xIndex, yIndex] = isHorizontal ? [1, 0] : [0, 1];
const xValue: number = richTooltip
- ? params[0].value[0]
- : params.value[0];
+ ? params[0].value[xIndex]
+ : params.value[xIndex];
const forecastValue: any[] = richTooltip ? params : [params];
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 = [`${tooltipFormatter(xValue)}`];
const forecastValues: Record =
- extractForecastValuesFromTooltipParams(forecastValue);
+ extractForecastValuesFromTooltipParams(forecastValue, isHorizontal);
Object.keys(forecastValues).forEach(key => {
const value = forecastValues[key];
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
index 48b7763458e3c..d514f969d623f 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts
@@ -86,6 +86,7 @@ export function transformSeries(
richTooltip?: boolean;
seriesKey?: OptionName;
sliceId?: number;
+ isHorizontal?: boolean;
},
): SeriesOption | undefined {
const { name } = series;
@@ -108,6 +109,7 @@ export function transformSeries(
richTooltip,
seriesKey,
sliceId,
+ isHorizontal = false,
} = opts;
const contexts = seriesContexts[name || ''] || [];
const hasForecast =
@@ -217,14 +219,10 @@ export function transformSeries(
symbolSize: markerSize,
label: {
show: !!showValue,
- position: 'top',
+ position: isHorizontal ? 'right' : 'top',
formatter: (params: any) => {
- const {
- value: [, numericValue],
- dataIndex,
- seriesIndex,
- seriesName,
- } = params;
+ const { value, dataIndex, seriesIndex, seriesName } = params;
+ const numericValue = isHorizontal ? value[0] : value[1];
const isSelectedLegend = currentSeries.legend === seriesName;
if (!formatter) return numericValue;
if (!stack || isSelectedLegend) return formatter(numericValue);
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts
index 81b274632a2c2..0d2499ccfcf6d 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts
@@ -38,6 +38,11 @@ export enum EchartsTimeseriesContributionType {
Column = 'column',
}
+export enum OrientationType {
+ vertical = 'vertical',
+ horizontal = 'horizontal',
+}
+
export enum EchartsTimeseriesSeriesType {
Line = 'line',
Scatter = 'scatter',
@@ -82,6 +87,7 @@ export type EchartsTimeseriesFormData = QueryFormData & {
showValue: boolean;
onlyTotal: boolean;
percentageThreshold: number;
+ orientation?: OrientationType;
} & EchartsLegendFormData &
EchartsTitleFormData;
@@ -119,6 +125,7 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = {
showValue: false,
onlyTotal: false,
percentageThreshold: 0,
+ orientation: OrientationType.vertical,
...DEFAULT_TITLE_FORM_DATA,
};
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts
index 63de4f4f6592a..94e4630bf455e 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/forecast.ts
@@ -53,12 +53,13 @@ export const extractForecastSeriesContexts = (
export const extractForecastValuesFromTooltipParams = (
params: any[],
+ isHorizontal = false,
): Record => {
const values: Record = {};
params.forEach(param => {
const { marker, seriesId, value } = param;
const context = extractForecastSeriesContext(seriesId);
- const numericValue = (value as [Date, number])[1];
+ const numericValue = isHorizontal ? value[0] : value[1];
if (numericValue) {
if (!(context.name in values))
values[context.name] = {
diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
index a262251b541e8..4da3681b7e231 100644
--- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
+++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts
@@ -42,9 +42,15 @@ export function extractSeries(
fillNeighborValue?: number;
xAxis?: string;
removeNulls?: boolean;
+ isHorizontal?: boolean;
} = {},
): SeriesOption[] {
- const { fillNeighborValue, xAxis = DTTM_ALIAS, removeNulls = false } = opts;
+ const {
+ fillNeighborValue,
+ xAxis = DTTM_ALIAS,
+ removeNulls = false,
+ isHorizontal = false,
+ } = opts;
if (data.length === 0) return [];
const rows: DataRecord[] = data.map(datum => ({
...datum,
@@ -69,7 +75,8 @@ export function extractSeries(
: 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)),
}));
}