diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap
index b5783803b803c..19ea75239ddb2 100644
--- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap
+++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/to_expression.test.ts.snap
@@ -8,6 +8,25 @@ Object {
"fittingFunction": Array [
"Carry",
],
+ "gridlinesVisibilitySettings": Array [
+ Object {
+ "chain": Array [
+ Object {
+ "arguments": Object {
+ "x": Array [
+ false,
+ ],
+ "y": Array [
+ true,
+ ],
+ },
+ "function": "lens_xy_gridlinesConfig",
+ "type": "function",
+ },
+ ],
+ "type": "expression",
+ },
+ ],
"layers": Array [
Object {
"chain": Array [
@@ -73,11 +92,36 @@ Object {
"type": "expression",
},
],
+ "showXAxisTitle": Array [
+ true,
+ ],
+ "showYAxisTitle": Array [
+ true,
+ ],
+ "tickLabelsVisibilitySettings": Array [
+ Object {
+ "chain": Array [
+ Object {
+ "arguments": Object {
+ "x": Array [
+ false,
+ ],
+ "y": Array [
+ true,
+ ],
+ },
+ "function": "lens_xy_tickLabelsConfig",
+ "type": "function",
+ },
+ ],
+ "type": "expression",
+ },
+ ],
"xTitle": Array [
- "col_a",
+ "",
],
"yTitle": Array [
- "col_b",
+ "",
],
},
"function": "lens_xy_chart",
diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap
index c7c173f87ad7c..f0c233b44a285 100644
--- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap
+++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/xy_expression.test.tsx.snap
@@ -20,9 +20,14 @@ exports[`xy_expression XYChart component it renders area 1`] = `
}
/>
@@ -146,9 +151,14 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
}
/>
@@ -262,9 +272,14 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
}
/>
@@ -378,9 +393,14 @@ exports[`xy_expression XYChart component it renders line 1`] = `
}
/>
@@ -504,9 +524,14 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = `
}
/>
@@ -628,9 +653,14 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
}
/>
@@ -752,9 +782,14 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
}
/>
diff --git a/x-pack/plugins/lens/public/xy_visualization/index.ts b/x-pack/plugins/lens/public/xy_visualization/index.ts
index 77cab1ee21344..fddcad7989b25 100644
--- a/x-pack/plugins/lens/public/xy_visualization/index.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/index.ts
@@ -10,7 +10,7 @@ import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'
import { UI_SETTINGS } from '../../../../../src/plugins/data/public';
import { xyVisualization } from './xy_visualization';
import { xyChart, getXyChartRenderer } from './xy_expression';
-import { legendConfig, layerConfig, yAxisConfig } from './types';
+import { legendConfig, layerConfig, yAxisConfig, tickLabelsConfig, gridlinesConfig } from './types';
import { EditorFrameSetup, FormatFactory } from '../types';
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
@@ -39,6 +39,8 @@ export class XyVisualization {
) {
expressions.registerFunction(() => legendConfig);
expressions.registerFunction(() => yAxisConfig);
+ expressions.registerFunction(() => tickLabelsConfig);
+ expressions.registerFunction(() => gridlinesConfig);
expressions.registerFunction(() => layerConfig);
expressions.registerFunction(() => xyChart);
diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts
index 31b34e41e82db..876d1141740e1 100644
--- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts
@@ -41,6 +41,8 @@ describe('#toExpression', () => {
legend: { position: Position.Bottom, isVisible: true },
preferredSeriesType: 'bar',
fittingFunction: 'Carry',
+ tickLabelsVisibilitySettings: { x: false, y: true },
+ gridlinesVisibilitySettings: { x: false, y: true },
layers: [
{
layerId: 'first',
@@ -77,6 +79,27 @@ describe('#toExpression', () => {
).toEqual('None');
});
+ it('should default the showXAxisTitle and showYAxisTitle to true', () => {
+ const expression = xyVisualization.toExpression(
+ {
+ legend: { position: Position.Bottom, isVisible: true },
+ preferredSeriesType: 'bar',
+ layers: [
+ {
+ layerId: 'first',
+ seriesType: 'area',
+ splitAccessor: 'd',
+ xAccessor: 'a',
+ accessors: ['b', 'c'],
+ },
+ ],
+ },
+ frame
+ ) as Ast;
+ expect(expression.chain[0].arguments.showXAxisTitle[0]).toBe(true);
+ expect(expression.chain[0].arguments.showYAxisTitle[0]).toBe(true);
+ });
+
it('should not generate an expression when missing x', () => {
expect(
xyVisualization.toExpression(
@@ -140,8 +163,8 @@ describe('#toExpression', () => {
expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('b');
expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('c');
expect(mockDatasource.publicAPIMock.getOperationForColumnId).toHaveBeenCalledWith('d');
- expect(expression.chain[0].arguments.xTitle).toEqual(['col_a']);
- expect(expression.chain[0].arguments.yTitle).toEqual(['col_b']);
+ expect(expression.chain[0].arguments.xTitle).toEqual(['']);
+ expect(expression.chain[0].arguments.yTitle).toEqual(['']);
expect(
(expression.chain[0].arguments.layers[0] as Ast).chain[0].arguments.columnToLabel
).toEqual([
@@ -152,4 +175,54 @@ describe('#toExpression', () => {
}),
]);
});
+
+ it('should default the tick labels visibility settings to true', () => {
+ const expression = xyVisualization.toExpression(
+ {
+ legend: { position: Position.Bottom, isVisible: true },
+ preferredSeriesType: 'bar',
+ layers: [
+ {
+ layerId: 'first',
+ seriesType: 'area',
+ splitAccessor: 'd',
+ xAccessor: 'a',
+ accessors: ['b', 'c'],
+ },
+ ],
+ },
+ frame
+ ) as Ast;
+ expect(
+ (expression.chain[0].arguments.tickLabelsVisibilitySettings[0] as Ast).chain[0].arguments
+ ).toEqual({
+ x: [true],
+ y: [true],
+ });
+ });
+
+ it('should default the gridlines visibility settings to true', () => {
+ const expression = xyVisualization.toExpression(
+ {
+ legend: { position: Position.Bottom, isVisible: true },
+ preferredSeriesType: 'bar',
+ layers: [
+ {
+ layerId: 'first',
+ seriesType: 'area',
+ splitAccessor: 'd',
+ xAccessor: 'a',
+ accessors: ['b', 'c'],
+ },
+ ],
+ },
+ frame
+ ) as Ast;
+ expect(
+ (expression.chain[0].arguments.gridlinesVisibilitySettings[0] as Ast).chain[0].arguments
+ ).toEqual({
+ x: [true],
+ y: [true],
+ });
+ });
});
diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
index b17704b38cdec..9b9c159af265e 100644
--- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
@@ -13,28 +13,6 @@ interface ValidLayer extends LayerConfig {
xAccessor: NonNullable;
}
-function xyTitles(layer: LayerConfig, frame: FramePublicAPI) {
- const defaults = {
- xTitle: 'x',
- yTitle: 'y',
- };
-
- if (!layer || !layer.accessors.length) {
- return defaults;
- }
- const datasource = frame.datasourceLayers[layer.layerId];
- if (!datasource) {
- return defaults;
- }
- const x = layer.xAccessor ? datasource.getOperationForColumnId(layer.xAccessor) : null;
- const y = layer.accessors[0] ? datasource.getOperationForColumnId(layer.accessors[0]) : null;
-
- return {
- xTitle: x ? x.label : defaults.xTitle,
- yTitle: y ? y.label : defaults.yTitle,
- };
-}
-
export const toExpression = (state: State, frame: FramePublicAPI): Ast | null => {
if (!state || !state.layers.length) {
return null;
@@ -52,7 +30,7 @@ export const toExpression = (state: State, frame: FramePublicAPI): Ast | null =>
});
});
- return buildExpression(state, metadata, frame, xyTitles(state.layers[0], frame));
+ return buildExpression(state, metadata, frame);
};
export function toPreviewExpression(state: State, frame: FramePublicAPI) {
@@ -99,8 +77,7 @@ export function getScaleType(metadata: OperationMetadata | null, defaultScale: S
export const buildExpression = (
state: State,
metadata: Record>,
- frame?: FramePublicAPI,
- { xTitle, yTitle }: { xTitle: string; yTitle: string } = { xTitle: '', yTitle: '' }
+ frame?: FramePublicAPI
): Ast | null => {
const validLayers = state.layers.filter((layer): layer is ValidLayer =>
Boolean(layer.xAccessor && layer.accessors.length)
@@ -116,8 +93,8 @@ export const buildExpression = (
type: 'function',
function: 'lens_xy_chart',
arguments: {
- xTitle: [xTitle],
- yTitle: [yTitle],
+ xTitle: [state.xTitle || ''],
+ yTitle: [state.yTitle || ''],
legend: [
{
type: 'expression',
@@ -137,6 +114,38 @@ export const buildExpression = (
},
],
fittingFunction: [state.fittingFunction || 'None'],
+ showXAxisTitle: [state.showXAxisTitle ?? true],
+ showYAxisTitle: [state.showYAxisTitle ?? true],
+ tickLabelsVisibilitySettings: [
+ {
+ type: 'expression',
+ chain: [
+ {
+ type: 'function',
+ function: 'lens_xy_tickLabelsConfig',
+ arguments: {
+ x: [state?.tickLabelsVisibilitySettings?.x ?? true],
+ y: [state?.tickLabelsVisibilitySettings?.y ?? true],
+ },
+ },
+ ],
+ },
+ ],
+ gridlinesVisibilitySettings: [
+ {
+ type: 'expression',
+ chain: [
+ {
+ type: 'function',
+ function: 'lens_xy_gridlinesConfig',
+ arguments: {
+ x: [state?.gridlinesVisibilitySettings?.x ?? true],
+ y: [state?.gridlinesVisibilitySettings?.y ?? true],
+ },
+ },
+ ],
+ },
+ ],
layers: validLayers.map((layer) => {
const columnToLabel: Record = {};
diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts
index 605119535d1f0..ab689ceb183be 100644
--- a/x-pack/plugins/lens/public/xy_visualization/types.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/types.ts
@@ -75,6 +75,81 @@ export const legendConfig: ExpressionFunctionDefinition<
},
};
+export interface AxesSettingsConfig {
+ x: boolean;
+ y: boolean;
+}
+
+type TickLabelsConfigResult = AxesSettingsConfig & { type: 'lens_xy_tickLabelsConfig' };
+
+export const tickLabelsConfig: ExpressionFunctionDefinition<
+ 'lens_xy_tickLabelsConfig',
+ null,
+ AxesSettingsConfig,
+ TickLabelsConfigResult
+> = {
+ name: 'lens_xy_tickLabelsConfig',
+ aliases: [],
+ type: 'lens_xy_tickLabelsConfig',
+ help: `Configure the xy chart's tick labels appearance`,
+ inputTypes: ['null'],
+ args: {
+ x: {
+ types: ['boolean'],
+ help: i18n.translate('xpack.lens.xyChart.xAxisTickLabels.help', {
+ defaultMessage: 'Specifies whether or not the tick labels of the x-axis are visible.',
+ }),
+ },
+ y: {
+ types: ['boolean'],
+ help: i18n.translate('xpack.lens.xyChart.yAxisTickLabels.help', {
+ defaultMessage: 'Specifies whether or not the tick labels of the y-axis are visible.',
+ }),
+ },
+ },
+ fn: function fn(input: unknown, args: AxesSettingsConfig) {
+ return {
+ type: 'lens_xy_tickLabelsConfig',
+ ...args,
+ };
+ },
+};
+
+type GridlinesConfigResult = AxesSettingsConfig & { type: 'lens_xy_gridlinesConfig' };
+
+export const gridlinesConfig: ExpressionFunctionDefinition<
+ 'lens_xy_gridlinesConfig',
+ null,
+ AxesSettingsConfig,
+ GridlinesConfigResult
+> = {
+ name: 'lens_xy_gridlinesConfig',
+ aliases: [],
+ type: 'lens_xy_gridlinesConfig',
+ help: `Configure the xy chart's gridlines appearance`,
+ inputTypes: ['null'],
+ args: {
+ x: {
+ types: ['boolean'],
+ help: i18n.translate('xpack.lens.xyChart.xAxisGridlines.help', {
+ defaultMessage: 'Specifies whether or not the gridlines of the x-axis are visible.',
+ }),
+ },
+ y: {
+ types: ['boolean'],
+ help: i18n.translate('xpack.lens.xyChart.yAxisgridlines.help', {
+ defaultMessage: 'Specifies whether or not the gridlines of the y-axis are visible.',
+ }),
+ },
+ },
+ fn: function fn(input: unknown, args: AxesSettingsConfig) {
+ return {
+ type: 'lens_xy_gridlinesConfig',
+ ...args,
+ };
+ },
+};
+
interface AxisConfig {
title: string;
hide?: boolean;
@@ -243,6 +318,10 @@ export interface XYArgs {
legend: LegendConfig & { type: 'lens_xy_legendConfig' };
layers: LayerArgs[];
fittingFunction?: FittingFunction;
+ showXAxisTitle?: boolean;
+ showYAxisTitle?: boolean;
+ tickLabelsVisibilitySettings?: AxesSettingsConfig & { type: 'lens_xy_tickLabelsConfig' };
+ gridlinesVisibilitySettings?: AxesSettingsConfig & { type: 'lens_xy_gridlinesConfig' };
}
// Persisted parts of the state
@@ -251,6 +330,12 @@ export interface XYState {
legend: LegendConfig;
fittingFunction?: FittingFunction;
layers: LayerConfig[];
+ xTitle?: string;
+ yTitle?: string;
+ showXAxisTitle?: boolean;
+ showYAxisTitle?: boolean;
+ tickLabelsVisibilitySettings?: AxesSettingsConfig;
+ gridlinesVisibilitySettings?: AxesSettingsConfig;
}
export type State = XYState;
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx
index 375eaf736cc95..31ba1bc83d970 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.test.tsx
@@ -109,7 +109,6 @@ describe('XY Config panels', () => {
it('should disable the select if there is no unstacked area or line series', () => {
const state = testState();
-
const component = shallow(
{
expect(component.find(EuiSuperSelect).prop('disabled')).toEqual(true);
});
+
+ it('should show the values of the X and Y axes titles on the corresponding input text', () => {
+ const state = testState();
+ const component = shallow(
+
+ );
+
+ expect(component.find('[data-test-subj="lnsXAxisTitle"]').prop('value')).toBe(
+ 'My custom X axis title'
+ );
+ expect(component.find('[data-test-subj="lnsYAxisTitle"]').prop('value')).toBe(
+ 'My custom Y axis title'
+ );
+ });
+
+ it('should disable the input texts if the switch is off', () => {
+ const state = testState();
+ const component = shallow(
+
+ );
+
+ expect(component.find('[data-test-subj="lnsXAxisTitle"]').prop('disabled')).toBe(true);
+ expect(component.find('[data-test-subj="lnsYAxisTitle"]').prop('disabled')).toBe(true);
+ });
+
+ it('has the tick labels buttons enabled', () => {
+ const state = testState();
+ const component = shallow();
+
+ const options = component
+ .find('[data-test-subj="lnsTickLabelsSettings"]')
+ .prop('options') as EuiButtonGroupProps['options'];
+
+ expect(options!.map(({ label }) => label)).toEqual(['X-axis', 'Y-axis']);
+
+ const selections = component
+ .find('[data-test-subj="lnsTickLabelsSettings"]')
+ .prop('idToSelectedMap');
+
+ expect(selections!).toEqual({ x: true, y: true });
+ });
+
+ it('has the gridlines buttons enabled', () => {
+ const state = testState();
+ const component = shallow();
+
+ const selections = component
+ .find('[data-test-subj="lnsGridlinesSettings"]')
+ .prop('idToSelectedMap');
+
+ expect(selections!).toEqual({ x: true, y: true });
+ });
});
});
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
index e4bc6de5cc68a..d64eb9451a50e 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel.tsx
@@ -5,7 +5,7 @@
*/
import './xy_config_panel.scss';
-import React, { useState } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { Position } from '@elastic/charts';
import { debounce } from 'lodash';
@@ -24,14 +24,17 @@ import {
EuiColorPickerProps,
EuiToolTip,
EuiIcon,
+ EuiFieldText,
+ EuiSwitch,
EuiHorizontalRule,
+ EuiTitle,
} from '@elastic/eui';
import {
VisualizationLayerWidgetProps,
VisualizationDimensionEditorProps,
VisualizationToolbarProps,
} from '../types';
-import { State, SeriesType, visualizationTypes, YAxisMode } from './types';
+import { State, SeriesType, visualizationTypes, YAxisMode, AxesSettingsConfig } from './types';
import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers';
import { trackUiEvent } from '../lens_ui_telemetry';
import { fittingFunctionDefinitions } from './fitting_functions';
@@ -118,14 +121,117 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) {
}
export function XyToolbar(props: VisualizationToolbarProps) {
+ const axes = [
+ {
+ id: 'x',
+ label: 'X-axis',
+ },
+ {
+ id: 'y',
+ label: 'Y-axis',
+ },
+ ];
+
+ const { frame, state, setState } = props;
+
const [open, setOpen] = useState(false);
- const hasNonBarSeries = props.state?.layers.some(
+ const hasNonBarSeries = state?.layers.some(
(layer) => layer.seriesType === 'line' || layer.seriesType === 'area'
);
+
+ const [xAxisTitle, setXAxisTitle] = useState(state?.xTitle);
+ const [yAxisTitle, setYAxisTitle] = useState(state?.yTitle);
+
+ const xyTitles = useCallback(() => {
+ const defaults = {
+ xTitle: xAxisTitle,
+ yTitle: yAxisTitle,
+ };
+ const layer = state?.layers[0];
+ if (!layer || !layer.accessors.length) {
+ return defaults;
+ }
+ const datasource = frame.datasourceLayers[layer.layerId];
+ if (!datasource) {
+ return defaults;
+ }
+ const x = layer.xAccessor ? datasource.getOperationForColumnId(layer.xAccessor) : null;
+ const y = layer.accessors[0] ? datasource.getOperationForColumnId(layer.accessors[0]) : null;
+
+ return {
+ xTitle: defaults.xTitle || x?.label,
+ yTitle: defaults.yTitle || y?.label,
+ };
+ /* We want this callback to run only if open changes its state. What we want to accomplish here is to give the user a better UX.
+ By default these input fields have the axis legends. If the user changes the input text, the axis legends should also change.
+ BUT if the user cleans up the input text, it should remain empty until the user closes and reopens the panel.
+ In that case, the default axes legend should appear. */
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [open]);
+
+ useEffect(() => {
+ const {
+ xTitle,
+ yTitle,
+ }: { xTitle: string | undefined; yTitle: string | undefined } = xyTitles();
+ setXAxisTitle(xTitle);
+ setYAxisTitle(yTitle);
+ }, [xyTitles]);
+
+ const onXTitleChange = (value: string): void => {
+ setXAxisTitle(value);
+ setState({ ...state, xTitle: value });
+ };
+
+ const onYTitleChange = (value: string): void => {
+ setYAxisTitle(value);
+ setState({ ...state, yTitle: value });
+ };
+
+ type AxesSettingsConfigKeys = keyof AxesSettingsConfig;
+
+ const tickLabelsVisibilitySettings = {
+ x: state?.tickLabelsVisibilitySettings?.x ?? true,
+ y: state?.tickLabelsVisibilitySettings?.y ?? true,
+ };
+
+ const onTickLabelsVisibilitySettingsChange = (optionId: string): void => {
+ const id = optionId as AxesSettingsConfigKeys;
+ const newTickLabelsVisibilitySettings = {
+ ...tickLabelsVisibilitySettings,
+ ...{
+ [id]: !tickLabelsVisibilitySettings[id],
+ },
+ };
+ setState({
+ ...state,
+ tickLabelsVisibilitySettings: newTickLabelsVisibilitySettings,
+ });
+ };
+
+ const gridlinesVisibilitySettings = {
+ x: state?.gridlinesVisibilitySettings?.x ?? true,
+ y: state?.gridlinesVisibilitySettings?.y ?? true,
+ };
+
+ const onGridlinesVisibilitySettingsChange = (optionId: string): void => {
+ const id = optionId as AxesSettingsConfigKeys;
+ const newGridlinesVisibilitySettings = {
+ ...gridlinesVisibilitySettings,
+ ...{
+ [id]: !gridlinesVisibilitySettings[id],
+ },
+ };
+ setState({
+ ...state,
+ gridlinesVisibilitySettings: newGridlinesVisibilitySettings,
+ });
+ };
+
const legendMode =
- props.state?.legend.isVisible && !props.state?.legend.showSingleSeries
+ state?.legend.isVisible && !state?.legend.showSingleSeries
? 'auto'
- : !props.state?.legend.isVisible
+ : !state?.legend.isVisible
? 'hide'
: 'show';
return (
@@ -183,8 +289,8 @@ export function XyToolbar(props: VisualizationToolbarProps) {
inputDisplay: title,
};
})}
- valueOfSelected={props.state?.fittingFunction || 'None'}
- onChange={(value) => props.setState({ ...props.state, fittingFunction: value })}
+ valueOfSelected={state?.fittingFunction || 'None'}
+ onChange={(value) => setState({ ...state, fittingFunction: value })}
itemLayoutAlign="top"
hasDividers
/>
@@ -209,19 +315,19 @@ export function XyToolbar(props: VisualizationToolbarProps) {
onChange={(optionId) => {
const newMode = legendOptions.find(({ id }) => id === optionId)!.value;
if (newMode === 'auto') {
- props.setState({
- ...props.state,
- legend: { ...props.state.legend, isVisible: true, showSingleSeries: false },
+ setState({
+ ...state,
+ legend: { ...state.legend, isVisible: true, showSingleSeries: false },
});
} else if (newMode === 'show') {
- props.setState({
- ...props.state,
- legend: { ...props.state.legend, isVisible: true, showSingleSeries: true },
+ setState({
+ ...state,
+ legend: { ...state.legend, isVisible: true, showSingleSeries: true },
});
} else if (newMode === 'hide') {
- props.setState({
- ...props.state,
- legend: { ...props.state.legend, isVisible: false, showSingleSeries: false },
+ setState({
+ ...state,
+ legend: { ...state.legend, isVisible: false, showSingleSeries: false },
});
}
}}
@@ -242,15 +348,130 @@ export function XyToolbar(props: VisualizationToolbarProps) {
{ value: Position.Right, text: 'Right' },
{ value: Position.Bottom, text: 'Bottom' },
]}
- value={props.state?.legend.position}
+ value={state?.legend.position}
onChange={(e) => {
- props.setState({
- ...props.state,
- legend: { ...props.state.legend, position: e.target.value as Position },
+ setState({
+ ...state,
+ legend: { ...state.legend, position: e.target.value as Position },
});
}}
/>
+
+
+ onTickLabelsVisibilitySettingsChange(id)}
+ buttonSize="compressed"
+ isFullWidth
+ type="multi"
+ />
+
+
+ onGridlinesVisibilitySettingsChange(id)}
+ buttonSize="compressed"
+ isFullWidth
+ type="multi"
+ />
+
+
+
+
+ {i18n.translate('xpack.lens.xyChart.axisTitles', { defaultMessage: 'Axis titles' })}
+
+
+
+ X-axis
+
+
+ setState({ ...state, showXAxisTitle: target.checked })
+ }
+ checked={state?.showXAxisTitle ?? true}
+ />
+
+
+ }
+ >
+ onXTitleChange(target.value)}
+ aria-label={i18n.translate('xpack.lens.xyChart.overwriteXaxis', {
+ defaultMessage: 'Overwrite X-axis title',
+ })}
+ />
+
+
+ Y-axis
+
+
+ setState({ ...state, showYAxisTitle: target.checked })
+ }
+ checked={state?.showYAxisTitle ?? true}
+ />
+
+
+ }
+ >
+ onYTitleChange(target.value)}
+ aria-label={i18n.translate('xpack.lens.xyChart.overwriteYaxis', {
+ defaultMessage: 'Overwrite Y-axis title',
+ })}
+ />
+
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
index c880cbb641e5d..ba1ff6a1df030 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
@@ -22,7 +22,16 @@ import { LensMultiTable } from '../types';
import { KibanaDatatable, KibanaDatatableRow } from '../../../../../src/plugins/expressions/public';
import React from 'react';
import { shallow } from 'enzyme';
-import { XYArgs, LegendConfig, legendConfig, layerConfig, LayerArgs } from './types';
+import {
+ XYArgs,
+ LegendConfig,
+ legendConfig,
+ layerConfig,
+ LayerArgs,
+ AxesSettingsConfig,
+ tickLabelsConfig,
+ gridlinesConfig,
+} from './types';
import { createMockExecutionContext } from '../../../../../src/plugins/expressions/common/mocks';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
@@ -211,6 +220,18 @@ const createArgsWithLayers = (layers: LayerArgs[] = [sampleLayer]): XYArgs => ({
isVisible: false,
position: Position.Top,
},
+ showXAxisTitle: true,
+ showYAxisTitle: true,
+ tickLabelsVisibilitySettings: {
+ type: 'lens_xy_tickLabelsConfig',
+ x: true,
+ y: false,
+ },
+ gridlinesVisibilitySettings: {
+ type: 'lens_xy_gridlinesConfig',
+ x: true,
+ y: false,
+ },
layers,
});
@@ -267,6 +288,34 @@ describe('xy_expression', () => {
});
});
+ test('tickLabelsConfig produces the correct arguments', () => {
+ const args: AxesSettingsConfig = {
+ x: true,
+ y: false,
+ };
+
+ const result = tickLabelsConfig.fn(null, args, createMockExecutionContext());
+
+ expect(result).toEqual({
+ type: 'lens_xy_tickLabelsConfig',
+ ...args,
+ });
+ });
+
+ test('gridlinesConfig produces the correct arguments', () => {
+ const args: AxesSettingsConfig = {
+ x: true,
+ y: false,
+ };
+
+ const result = gridlinesConfig.fn(null, args, createMockExecutionContext());
+
+ expect(result).toEqual({
+ type: 'lens_xy_gridlinesConfig',
+ ...args,
+ });
+ });
+
describe('xyChart', () => {
test('it renders with the specified data and args', () => {
const { data, args } = sampleArgs();
@@ -1365,6 +1414,35 @@ describe('xy_expression', () => {
expect(convertSpy).toHaveBeenCalledWith('I');
});
+ test('it should not pass the formatter function to the x axis if the visibility of the tick labels is off', () => {
+ const { data, args } = sampleArgs();
+
+ args.tickLabelsVisibilitySettings = { x: false, y: true, type: 'lens_xy_tickLabelsConfig' };
+
+ const instance = shallow(
+
+ );
+
+ const tickFormatter = instance.find(Axis).first().prop('tickFormat');
+
+ if (!tickFormatter) {
+ throw new Error('tickFormatter prop not found');
+ }
+
+ tickFormatter('I');
+
+ expect(convertSpy).toHaveBeenCalledTimes(0);
+ });
+
test('it should remove invalid rows', () => {
const data: LensMultiTable = {
type: 'lens_multitable',
@@ -1400,6 +1478,16 @@ describe('xy_expression', () => {
xTitle: '',
yTitle: '',
legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top },
+ tickLabelsVisibilitySettings: {
+ type: 'lens_xy_tickLabelsConfig',
+ x: true,
+ y: true,
+ },
+ gridlinesVisibilitySettings: {
+ type: 'lens_xy_gridlinesConfig',
+ x: true,
+ y: false,
+ },
layers: [
{
layerId: 'first',
@@ -1469,6 +1557,16 @@ describe('xy_expression', () => {
xTitle: '',
yTitle: '',
legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top },
+ tickLabelsVisibilitySettings: {
+ type: 'lens_xy_tickLabelsConfig',
+ x: true,
+ y: false,
+ },
+ gridlinesVisibilitySettings: {
+ type: 'lens_xy_gridlinesConfig',
+ x: true,
+ y: false,
+ },
layers: [
{
layerId: 'first',
@@ -1525,6 +1623,16 @@ describe('xy_expression', () => {
xTitle: '',
yTitle: '',
legend: { type: 'lens_xy_legendConfig', isVisible: true, position: Position.Top },
+ tickLabelsVisibilitySettings: {
+ type: 'lens_xy_tickLabelsConfig',
+ x: true,
+ y: false,
+ },
+ gridlinesVisibilitySettings: {
+ type: 'lens_xy_gridlinesConfig',
+ x: true,
+ y: false,
+ },
layers: [
{
layerId: 'first',
@@ -1683,5 +1791,68 @@ describe('xy_expression', () => {
expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.None });
});
+
+ test('it should apply the xTitle if is specified', () => {
+ const { data, args } = sampleArgs();
+
+ args.xTitle = 'My custom x-axis title';
+
+ const component = shallow(
+
+ );
+
+ expect(component.find(Axis).at(0).prop('title')).toEqual('My custom x-axis title');
+ });
+
+ test('it should hide the X axis title if the corresponding switch is off', () => {
+ const { data, args } = sampleArgs();
+
+ args.showXAxisTitle = false;
+
+ const component = shallow(
+
+ );
+
+ expect(component.find(Axis).at(0).prop('title')).toEqual(undefined);
+ });
+
+ test('it should show the X axis gridlines if the setting is on', () => {
+ const { data, args } = sampleArgs();
+
+ args.gridlinesVisibilitySettings = { x: true, y: false, type: 'lens_xy_gridlinesConfig' };
+
+ const component = shallow(
+
+ );
+
+ expect(component.find(Axis).at(0).prop('showGridLines')).toBeTruthy();
+ });
});
});
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
index a3468e109e75b..2037a3dbe6623 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
@@ -102,6 +102,30 @@ export const xyChart: ExpressionFunctionDefinition<
defaultMessage: 'Define how missing values are treated',
}),
},
+ tickLabelsVisibilitySettings: {
+ types: ['lens_xy_tickLabelsConfig'],
+ help: i18n.translate('xpack.lens.xyChart.tickLabelsSettings.help', {
+ defaultMessage: 'Show x and y axes tick labels',
+ }),
+ },
+ gridlinesVisibilitySettings: {
+ types: ['lens_xy_gridlinesConfig'],
+ help: i18n.translate('xpack.lens.xyChart.gridlinesSettings.help', {
+ defaultMessage: 'Show x and y axes gridlines',
+ }),
+ },
+ showXAxisTitle: {
+ types: ['boolean'],
+ help: i18n.translate('xpack.lens.xyChart.showXAxisTitle.help', {
+ defaultMessage: 'Show x axis title',
+ }),
+ },
+ showYAxisTitle: {
+ types: ['boolean'],
+ help: i18n.translate('xpack.lens.xyChart.showYAxisTitle.help', {
+ defaultMessage: 'Show y axis title',
+ }),
+ },
layers: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
types: ['lens_xy_layer'] as any,
@@ -199,7 +223,7 @@ export function XYChart({
onClickValue,
onSelectRange,
}: XYChartRenderProps) {
- const { legend, layers, fittingFunction } = args;
+ const { legend, layers, fittingFunction, gridlinesVisibilitySettings } = args;
const chartTheme = chartsThemeService.useChartsTheme();
const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
@@ -237,7 +261,10 @@ export function XYChart({
shouldRotate
);
- const xTitle = (xAxisColumn && xAxisColumn.name) || args.xTitle;
+ const xTitle = args.xTitle || (xAxisColumn && xAxisColumn.name);
+ const showXAxisTitle = args.showXAxisTitle ?? true;
+ const showYAxisTitle = args.showYAxisTitle ?? true;
+ const tickLabelsVisibilitySettings = args.tickLabelsVisibilitySettings || { x: true, y: true };
function calculateMinInterval() {
// check all the tables to see if all of the rows have the same timestamp
@@ -279,6 +306,22 @@ export function XYChart({
}
: undefined;
+ const getYAxesTitles = (
+ axisSeries: Array<{ layer: string; accessor: string }>,
+ index: number
+ ) => {
+ if (index > 0 && args.yTitle) return;
+ return (
+ args.yTitle ||
+ axisSeries
+ .map(
+ (series) =>
+ data.tables[series.layer].columns.find((column) => column.id === series.accessor)?.name
+ )
+ .filter((name) => Boolean(name))[0]
+ );
+ };
+
return (
xAxisFormatter.convert(d)}
+ tickFormat={tickLabelsVisibilitySettings?.x ? (d) => xAxisFormatter.convert(d) : () => ''}
/>
{yAxesConfiguration.map((axis, index) => (
@@ -389,18 +433,10 @@ export function XYChart({
id={axis.groupId}
groupId={axis.groupId}
position={axis.position}
- title={
- axis.series
- .map(
- (series) =>
- data.tables[series.layer].columns.find((column) => column.id === series.accessor)
- ?.name
- )
- .filter((name) => Boolean(name))[0] || args.yTitle
- }
- showGridLines={false}
+ title={showYAxisTitle ? getYAxesTitles(axis.series, index) : undefined}
+ showGridLines={gridlinesVisibilitySettings?.y}
hide={filteredLayers[0].hide}
- tickFormat={(d) => axis.formatter.convert(d)}
+ tickFormat={tickLabelsVisibilitySettings?.y ? (d) => axis.formatter.convert(d) : () => ''}
/>
))}
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts
index 7b3398658a500..632f6fc8861a4 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.test.ts
@@ -445,6 +445,10 @@ describe('xy_suggestions', () => {
const currentState: XYState = {
legend: { isVisible: true, position: 'bottom' },
fittingFunction: 'None',
+ showXAxisTitle: true,
+ showYAxisTitle: true,
+ gridlinesVisibilitySettings: { x: true, y: true },
+ tickLabelsVisibilitySettings: { x: true, y: false },
preferredSeriesType: 'bar',
layers: [
{
@@ -483,6 +487,10 @@ describe('xy_suggestions', () => {
legend: { isVisible: true, position: 'bottom' },
preferredSeriesType: 'bar',
fittingFunction: 'None',
+ showXAxisTitle: true,
+ showYAxisTitle: true,
+ gridlinesVisibilitySettings: { x: true, y: true },
+ tickLabelsVisibilitySettings: { x: true, y: false },
layers: [
{
accessors: ['price', 'quantity'],
@@ -592,6 +600,10 @@ describe('xy_suggestions', () => {
legend: { isVisible: true, position: 'bottom' },
preferredSeriesType: 'bar',
fittingFunction: 'None',
+ showXAxisTitle: true,
+ showYAxisTitle: true,
+ gridlinesVisibilitySettings: { x: true, y: true },
+ tickLabelsVisibilitySettings: { x: true, y: false },
layers: [
{
accessors: ['price', 'quantity'],
@@ -631,6 +643,10 @@ describe('xy_suggestions', () => {
legend: { isVisible: true, position: 'bottom' },
preferredSeriesType: 'bar',
fittingFunction: 'None',
+ showXAxisTitle: true,
+ showYAxisTitle: true,
+ gridlinesVisibilitySettings: { x: true, y: true },
+ tickLabelsVisibilitySettings: { x: true, y: false },
layers: [
{
accessors: ['price'],
@@ -671,6 +687,10 @@ describe('xy_suggestions', () => {
legend: { isVisible: true, position: 'bottom' },
preferredSeriesType: 'bar',
fittingFunction: 'None',
+ showXAxisTitle: true,
+ showYAxisTitle: true,
+ gridlinesVisibilitySettings: { x: true, y: true },
+ tickLabelsVisibilitySettings: { x: true, y: false },
layers: [
{
accessors: ['price', 'quantity'],
diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts
index 1be8d566a8b64..387d56c03e31a 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_suggestions.ts
@@ -407,6 +407,18 @@ function buildSuggestion({
const state: State = {
legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right },
fittingFunction: currentState?.fittingFunction || 'None',
+ xTitle: currentState?.xTitle,
+ yTitle: currentState?.yTitle,
+ showXAxisTitle: currentState?.showXAxisTitle ?? true,
+ showYAxisTitle: currentState?.showYAxisTitle ?? true,
+ tickLabelsVisibilitySettings: currentState?.tickLabelsVisibilitySettings || {
+ x: true,
+ y: true,
+ },
+ gridlinesVisibilitySettings: currentState?.gridlinesVisibilitySettings || {
+ x: true,
+ y: true,
+ },
preferredSeriesType: seriesType,
layers: Object.keys(existingLayer).length ? keptLayers : [...keptLayers, newLayer],
};