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 48c70e0a4a05b..0d5efb5012d6d 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
@@ -35,6 +35,7 @@ exports[`xy_expression XYChart component it renders area 1`] = `
title="a"
/>
{
+ if (layer.splitAccessor) {
+ return null;
+ }
+ return (
+ layer?.yConfig?.find((yConfig: YConfig) => yConfig.forAccessor === accessor)?.color || null
+ );
+};
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 6ec22270d8b18..b5b796dc019de 100644
--- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts
@@ -188,7 +188,8 @@ export const buildExpression = (
function: 'lens_xy_yConfig',
arguments: {
forAccessor: [yConfig.forAccessor],
- axisMode: [yConfig.axisMode],
+ axisMode: yConfig.axisMode ? [yConfig.axisMode] : [],
+ color: yConfig.color ? [yConfig.color] : [],
},
},
],
diff --git a/x-pack/plugins/lens/public/xy_visualization/types.ts b/x-pack/plugins/lens/public/xy_visualization/types.ts
index e62c5f60a58e1..8ea9683ca042c 100644
--- a/x-pack/plugins/lens/public/xy_visualization/types.ts
+++ b/x-pack/plugins/lens/public/xy_visualization/types.ts
@@ -100,6 +100,10 @@ export const yAxisConfig: ExpressionFunctionDefinition<
options: ['auto', 'left', 'right'],
help: 'The axis mode of the metric',
},
+ color: {
+ types: ['string'],
+ help: 'The color of the series',
+ },
},
fn: function fn(input: unknown, args: YConfig) {
return {
@@ -195,6 +199,7 @@ export type YAxisMode = 'auto' | 'left' | 'right';
export interface YConfig {
forAccessor: string;
axisMode?: YAxisMode;
+ color?: string;
}
export interface LayerConfig {
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 3e73cd256bdbf..e6c284f09ab4e 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
@@ -4,12 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import React from 'react';
+import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiButtonGroup, EuiFormRow, htmlIdGenerator } from '@elastic/eui';
+import { debounce } from 'lodash';
+import {
+ EuiButtonGroup,
+ EuiFormRow,
+ htmlIdGenerator,
+ EuiForm,
+ EuiColorPicker,
+ EuiColorPickerProps,
+ EuiToolTip,
+ EuiIcon,
+} from '@elastic/eui';
import { State, SeriesType, visualizationTypes, YAxisMode } from './types';
import { VisualizationDimensionEditorProps, VisualizationLayerWidgetProps } from '../types';
-import { isHorizontalChart, isHorizontalSeries } from './state_helpers';
+import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers';
import { trackUiEvent } from '../lens_ui_telemetry';
type UnwrapArray = T extends Array ? P : T;
@@ -70,70 +80,176 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps) {
const idPrefix = htmlIdGenerator()();
-export function DimensionEditor({
- state,
- setState,
- layerId,
- accessor,
-}: VisualizationDimensionEditorProps) {
+export function DimensionEditor(props: VisualizationDimensionEditorProps) {
+ const { state, setState, layerId, accessor } = props;
const index = state.layers.findIndex((l) => l.layerId === layerId);
const layer = state.layers[index];
const axisMode =
(layer.yConfig &&
layer.yConfig?.find((yAxisConfig) => yAxisConfig.forAccessor === accessor)?.axisMode) ||
'auto';
+
return (
-
-
+
+
+ {
- const newMode = id.replace(idPrefix, '') as YAxisMode;
- const newYAxisConfigs = [...(layer.yConfig || [])];
- const existingIndex = newYAxisConfigs.findIndex(
- (yAxisConfig) => yAxisConfig.forAccessor === accessor
- );
- if (existingIndex !== -1) {
- newYAxisConfigs[existingIndex].axisMode = newMode;
+ >
+ {
+ const newMode = id.replace(idPrefix, '') as YAxisMode;
+ const newYAxisConfigs = [...(layer.yConfig || [])];
+ const existingIndex = newYAxisConfigs.findIndex(
+ (yAxisConfig) => yAxisConfig.forAccessor === accessor
+ );
+ if (existingIndex !== -1) {
+ newYAxisConfigs[existingIndex].axisMode = newMode;
+ } else {
+ newYAxisConfigs.push({
+ forAccessor: accessor,
+ axisMode: newMode,
+ });
+ }
+ setState(updateLayer(state, { ...layer, yConfig: newYAxisConfigs }, index));
+ }}
+ />
+
+
+ );
+}
+
+const tooltipContent = {
+ auto: i18n.translate('xpack.lens.configPanel.color.tooltip.auto', {
+ defaultMessage: 'Lens automatically picks colors for you unless you specify a custom color.',
+ }),
+ custom: i18n.translate('xpack.lens.configPanel.color.tooltip.custom', {
+ defaultMessage: 'Clear the custom color to return to “Auto” mode.',
+ }),
+ disabled: i18n.translate('xpack.lens.configPanel.color.tooltip.disabled', {
+ defaultMessage:
+ 'Individual series cannot be custom colored when the layer includes a “Break down by“',
+ }),
+};
+
+const ColorPicker = ({
+ state,
+ setState,
+ layerId,
+ accessor,
+}: VisualizationDimensionEditorProps) => {
+ const index = state.layers.findIndex((l) => l.layerId === layerId);
+ const layer = state.layers[index];
+ const disabled = !!layer.splitAccessor;
+
+ const [color, setColor] = useState(getSeriesColor(layer, accessor));
+
+ const handleColor: EuiColorPickerProps['onChange'] = (text, output) => {
+ setColor(text);
+ if (output.isValid || text === '') {
+ updateColorInState(text, output);
+ }
+ };
+
+ const updateColorInState: EuiColorPickerProps['onChange'] = React.useMemo(
+ () =>
+ debounce((text, output) => {
+ const newYConfigs = [...(layer.yConfig || [])];
+ const existingIndex = newYConfigs.findIndex((yConfig) => yConfig.forAccessor === accessor);
+ if (existingIndex !== -1) {
+ if (text === '') {
+ delete newYConfigs[existingIndex].color;
} else {
- newYAxisConfigs.push({
- forAccessor: accessor,
- axisMode: newMode,
- });
+ newYConfigs[existingIndex].color = output.hex;
}
- setState(updateLayer(state, { ...layer, yConfig: newYAxisConfigs }, index));
- }}
- />
+ } else {
+ newYConfigs.push({
+ forAccessor: accessor,
+ color: output.hex,
+ });
+ }
+ setState(updateLayer(state, { ...layer, yConfig: newYConfigs }, index));
+ }, 256),
+ [state, layer, accessor, index]
+ );
+
+ return (
+
+
+ {i18n.translate('xpack.lens.xyChart.seriesColor.label', {
+ defaultMessage: 'Series color',
+ })}{' '}
+
+
+
+ }
+ >
+ {disabled ? (
+
+
+
+ ) : (
+
+ )}
);
-}
+};
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 34f2a9111253b..472b48491886b 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
@@ -993,6 +993,75 @@ describe('xy_expression', () => {
});
});
+ describe('y series coloring', () => {
+ test('color is applied to chart for multiple series', () => {
+ const args = createArgsWithLayers();
+ const newArgs = {
+ ...args,
+ layers: [
+ {
+ ...args.layers[0],
+ splitAccessor: undefined,
+ accessors: ['a', 'b'],
+ yConfig: [
+ {
+ forAccessor: 'a',
+ color: '#550000',
+ },
+ {
+ forAccessor: 'b',
+ color: '#FFFF00',
+ },
+ ],
+ },
+ {
+ ...args.layers[0],
+ splitAccessor: undefined,
+ accessors: ['c'],
+ yConfig: [
+ {
+ forAccessor: 'c',
+ color: '#FEECDF',
+ },
+ ],
+ },
+ ],
+ } as XYArgs;
+
+ const component = getRenderedComponent(dataWithoutFormats, newArgs);
+ expect((component.find(LineSeries).at(0).prop('color') as Function)!()).toEqual('#550000');
+ expect((component.find(LineSeries).at(1).prop('color') as Function)!()).toEqual('#FFFF00');
+ expect((component.find(LineSeries).at(2).prop('color') as Function)!()).toEqual('#FEECDF');
+ });
+ test('color is not applied to chart when splitAccessor is defined or when yConfig is not configured', () => {
+ const args = createArgsWithLayers();
+ const newArgs = {
+ ...args,
+ layers: [
+ {
+ ...args.layers[0],
+ accessors: ['a'],
+ yConfig: [
+ {
+ forAccessor: 'a',
+ color: '#550000',
+ },
+ ],
+ },
+ {
+ ...args.layers[0],
+ splitAccessor: undefined,
+ accessors: ['c'],
+ },
+ ],
+ } as XYArgs;
+
+ const component = getRenderedComponent(dataWithoutFormats, newArgs);
+ expect((component.find(LineSeries).at(0).prop('color') as Function)!()).toEqual(null);
+ expect((component.find(LineSeries).at(1).prop('color') as Function)!()).toEqual(null);
+ });
+ });
+
describe('provides correct series naming', () => {
const nameFnArgs = {
seriesKeys: [],
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 17ed04aa0e9c4..1f43a4117db0c 100644
--- a/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
+++ b/x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
@@ -36,7 +36,7 @@ import {
} from '../types';
import { XYArgs, SeriesType, visualizationTypes } from './types';
import { VisualizationContainer } from '../visualization_container';
-import { isHorizontalChart } from './state_helpers';
+import { isHorizontalChart, getSeriesColor } from './state_helpers';
import { parseInterval } from '../../../../../src/plugins/data/common';
import { EmptyPlaceholder } from '../shared_components';
import { desanitizeFilterContext } from '../utils';
@@ -430,6 +430,7 @@ export function XYChart({
data: rows,
xScaleType,
yScaleType,
+ color: () => getSeriesColor(layer, accessor),
groupId: yAxesConfiguration.find((axisConfiguration) =>
axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor)
)?.groupId,