diff --git a/src/plugins/vis_type_timeseries/common/types.ts b/src/plugins/vis_type_timeseries/common/types.ts index 155474e64b36e..7d93232f310c9 100644 --- a/src/plugins/vis_type_timeseries/common/types.ts +++ b/src/plugins/vis_type_timeseries/common/types.ts @@ -25,7 +25,7 @@ export type PanelSchema = TypeOf; export type VisPayload = TypeOf; export type FieldObject = TypeOf; -interface PanelData { +export interface PanelData { id: string; label: string; data: Array<[number, number]>; @@ -57,3 +57,8 @@ export interface SanitizedFieldType { type: string; label?: string; } + +export enum PALETTES { + GRADIENT = 'gradient', + RAINBOW = 'rainbow', +} diff --git a/src/plugins/vis_type_timeseries/common/vis_schema.ts b/src/plugins/vis_type_timeseries/common/vis_schema.ts index f5c48c2c8b2eb..9c7e8ab04fc1d 100644 --- a/src/plugins/vis_type_timeseries/common/vis_schema.ts +++ b/src/plugins/vis_type_timeseries/common/vis_schema.ts @@ -172,6 +172,10 @@ export const seriesItems = schema.object({ series_interval: stringOptionalNullable, series_drop_last_bucket: numberIntegerOptional, split_color_mode: stringOptionalNullable, + palette: schema.object({ + type: stringRequired, + name: stringRequired, + }), split_filters: schema.maybe(schema.arrayOf(splitFiltersItems)), split_mode: stringRequired, stacked: stringRequired, diff --git a/src/plugins/vis_type_timeseries/public/application/components/palette_picker.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/palette_picker.test.tsx new file mode 100644 index 0000000000000..7ee98317882cd --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/palette_picker.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { ReactWrapper } from 'enzyme'; +import { PalettePicker, PalettePickerProps } from './palette_picker'; +import { chartPluginMock } from '../../../../charts/public/mocks'; +import { EuiColorPalettePicker } from '@elastic/eui'; +import { PALETTES } from '../../../common/types'; + +describe('PalettePicker', function () { + let props: PalettePickerProps; + let component: ReactWrapper; + + beforeAll(() => { + props = { + palettes: chartPluginMock.createPaletteRegistry(), + activePalette: { + type: 'palette', + name: 'kibana_palette', + }, + setPalette: jest.fn(), + color: '#68BC00', + }; + }); + + it('renders the EuiPalettePicker', () => { + component = mountWithIntl(); + expect(component.find(EuiColorPalettePicker).length).toBe(1); + }); + + it('renders the default palette if not activePalette is given', function () { + const { activePalette, ...newProps } = props; + component = mountWithIntl(); + const palettePicker = component.find(EuiColorPalettePicker); + expect(palettePicker.props().valueOfSelected).toBe('default'); + }); + + it('renders the activePalette palette if given', function () { + component = mountWithIntl(); + const palettePicker = component.find(EuiColorPalettePicker); + expect(palettePicker.props().valueOfSelected).toBe('kibana_palette'); + }); + + it('renders two additional palettes, rainbow and gradient', function () { + component = mountWithIntl(); + const palettePicker = component.find(EuiColorPalettePicker); + expect(palettePicker.props().palettes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + value: PALETTES.RAINBOW, + }), + expect.objectContaining({ + value: PALETTES.GRADIENT, + }), + ]) + ); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/palette_picker.tsx b/src/plugins/vis_type_timeseries/public/application/components/palette_picker.tsx new file mode 100644 index 0000000000000..e094cb9add978 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/palette_picker.tsx @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import { EuiColorPalettePicker } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { rainbowColors } from '../lib/rainbow_colors'; +import { computeGradientFinalColor } from '../lib/compute_gradient_final_color'; +import { PALETTES } from '../../../common/types'; + +export interface PalettePickerProps { + activePalette?: PaletteOutput; + palettes: PaletteRegistry; + setPalette: (value: PaletteOutput) => void; + color: string; +} + +export function PalettePicker({ activePalette, palettes, setPalette, color }: PalettePickerProps) { + const finalGradientColor = computeGradientFinalColor(color); + + return ( + !internal) + .map(({ id, title, getColors }) => { + return { + value: id, + title, + type: 'fixed' as const, + palette: getColors(10), + }; + }), + { + value: PALETTES.GRADIENT, + title: i18n.translate('visTypeTimeseries.timeSeries.gradientLabel', { + defaultMessage: 'Gradient', + }), + type: 'fixed', + palette: palettes + .get('custom') + .getColors(10, { colors: [color, finalGradientColor], gradient: true }), + }, + { + value: PALETTES.RAINBOW, + title: i18n.translate('visTypeTimeseries.timeSeries.rainbowLabel', { + defaultMessage: 'Rainbow', + }), + type: 'fixed', + palette: palettes + .get('custom') + .getColors(10, { colors: rainbowColors.slice(0, 10), gradient: false }), + }, + ]} + onChange={(newPalette) => { + if (newPalette === PALETTES.RAINBOW) { + setPalette({ + type: 'palette', + name: PALETTES.RAINBOW, + params: { + colors: rainbowColors, + gradient: false, + }, + }); + } else if (newPalette === PALETTES.GRADIENT) { + setPalette({ + type: 'palette', + name: PALETTES.GRADIENT, + params: { + colors: [color, finalGradientColor], + gradient: true, + }, + }); + } else { + setPalette({ + type: 'palette', + name: newPalette, + }); + } + }} + valueOfSelected={activePalette?.name || 'default'} + selectionDisplay={'palette'} + /> + ); +} diff --git a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx index 9c8944a2e6e62..ac15a788d6dab 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/timeseries_visualization.tsx @@ -11,6 +11,7 @@ import React, { useCallback, useEffect } from 'react'; import { IUiSettingsClient } from 'src/core/public'; import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { PersistedState } from 'src/plugins/visualizations/public'; +import { PaletteRegistry } from 'src/plugins/charts/public'; // @ts-expect-error import { ErrorComponent } from './error'; @@ -25,6 +26,8 @@ interface TimeseriesVisualizationProps { model: TimeseriesVisParams; visData: TimeseriesVisData; uiState: PersistedState; + syncColors: boolean; + palettesService: PaletteRegistry; } function TimeseriesVisualization({ @@ -34,6 +37,8 @@ function TimeseriesVisualization({ handlers, uiState, getConfig, + syncColors, + palettesService, }: TimeseriesVisualizationProps) { const onBrush = useCallback( (gte: string, lte: string) => { @@ -91,6 +96,8 @@ function TimeseriesVisualization({ uiState={uiState} onBrush={onBrush} onUiState={handleUiState} + syncColors={syncColors} + palettesService={palettesService} /> ); } diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts index 150a3a716a879..0e169c50e4db6 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/index.ts @@ -10,6 +10,7 @@ import React, { lazy } from 'react'; import { IUiSettingsClient } from 'src/core/public'; import { PersistedState } from 'src/plugins/visualizations/public'; +import { PaletteRegistry } from 'src/plugins/charts/public'; import { TimeseriesVisParams } from '../../../types'; import { TimeseriesVisData } from '../../../../common/types'; @@ -54,4 +55,6 @@ export interface TimeseriesVisProps { uiState: PersistedState; visData: TimeseriesVisData; getConfig: IUiSettingsClient['get']; + syncColors: boolean; + palettesService: PaletteRegistry; } diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js index b0dd5ea4572af..3df12dafd5a66 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/config.js @@ -7,7 +7,7 @@ */ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { DataFormatPicker } from '../../data_format_picker'; import { createSelectHandler } from '../../lib/create_select_handler'; import { YesNo } from '../../yes_no'; @@ -28,7 +28,8 @@ import { } from '@elastic/eui'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import { SeriesConfigQueryBarWithIgnoreGlobalFilter } from '../../series_config_query_bar_with_ignore_global_filter'; - +import { PalettePicker } from '../../palette_picker'; +import { getChartsSetup } from '../../../../services'; import { isPercentDisabled } from '../../lib/stacked'; import { STACKED_OPTIONS } from '../../../visualizations/constants/chart'; @@ -41,7 +42,6 @@ export const TimeseriesConfig = injectI18n(function (props) { point_size: '', value_template: '{{value}}', offset_time: '', - split_color_mode: 'kibana', axis_min: '', axis_max: '', stacked: STACKED_OPTIONS.NONE, @@ -124,33 +124,23 @@ export const TimeseriesConfig = injectI18n(function (props) { const selectedChartTypeOption = chartTypeOptions.find((option) => { return model.chart_type === option.value; }); + const { palettes } = getChartsSetup(); + const [palettesRegistry, setPalettesRegistry] = useState(null); - const splitColorOptions = [ - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.defaultPaletteLabel', - defaultMessage: 'Default palette', - }), - value: 'kibana', - }, - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.rainbowLabel', - defaultMessage: 'Rainbow', - }), - value: 'rainbow', - }, - { - label: intl.formatMessage({ - id: 'visTypeTimeseries.timeSeries.gradientLabel', - defaultMessage: 'Gradient', - }), - value: 'gradient', - }, - ]; - const selectedSplitColorOption = splitColorOptions.find((option) => { - return model.split_color_mode === option.value; - }); + useEffect(() => { + const fetchPalettes = async () => { + const palettesService = await palettes.getPalettes(); + setPalettesRegistry(palettesService); + }; + fetchPalettes(); + }, [palettes]); + + const handlePaletteChange = (val) => { + props.onChange({ + split_color_mode: null, + palette: val, + }); + }; let type; @@ -342,6 +332,14 @@ export const TimeseriesConfig = injectI18n(function (props) { ? props.model.series_index_pattern : props.indexPatternForQuery; + const initialPalette = { + ...model.palette, + name: + model.split_color_mode === 'kibana' + ? 'kibana_palette' + : model.split_color_mode || model.palette.name, + }; + return (
@@ -420,25 +418,26 @@ export const TimeseriesConfig = injectI18n(function (props) { - - + + } + > + - } - > - - - + + + )} diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index fb99b890d2327..5a2fc05817f71 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -132,7 +132,7 @@ class TimeseriesVisualization extends Component { }; render() { - const { model, visData, onBrush } = this.props; + const { model, visData, onBrush, syncColors, palettesService } = this.props; const series = get(visData, `${model.id}.series`, []); const interval = getInterval(visData, model); const yAxisIdGenerator = htmlIdGenerator('yaxis'); @@ -163,6 +163,13 @@ class TimeseriesVisualization extends Component { seriesGroup, this.props.getConfig ); + const palette = { + ...seriesGroup.palette, + name: + seriesGroup.split_color_mode === 'kibana' + ? 'kibana_palette' + : seriesGroup.split_color_mode || seriesGroup.palette?.name, + }; const yScaleType = hasSeparateAxis ? TimeseriesVisualization.getAxisScaleType(seriesGroup) : mainAxisScaleType; @@ -182,6 +189,9 @@ class TimeseriesVisualization extends Component { seriesDataRow.groupId = groupId; seriesDataRow.yScaleType = yScaleType; seriesDataRow.hideInLegend = Boolean(seriesGroup.hide_in_legend); + seriesDataRow.palette = palette; + seriesDataRow.baseColor = seriesGroup.color; + seriesDataRow.isSplitByTerms = seriesGroup.split_mode === 'terms'; }); if (isCustomDomain) { @@ -223,6 +233,8 @@ class TimeseriesVisualization extends Component { xAxisLabel={getAxisLabelString(interval)} xAxisFormatter={this.xAxisFormatter(interval)} annotations={this.prepareAnnotations()} + syncColors={syncColors} + palettesService={palettesService} />
); diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js index 59f4724c1394c..7dc6a26185e2b 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js @@ -6,15 +6,40 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { getDisplayName } from './lib/get_display_name'; import { labelDateFormatter } from './lib/label_date_formatter'; import { findIndex, first } from 'lodash'; import { emptyLabel } from '../../../common/empty_label'; +import { getSplitByTermsColor } from '../lib/get_split_by_terms_color'; export function visWithSplits(WrappedComponent) { function SplitVisComponent(props) { - const { model, visData } = props; + const { model, visData, syncColors, palettesService } = props; + + const getSeriesColor = useCallback( + (seriesName, seriesId, baseColor) => { + const palette = { + ...model.series[0].palette, + name: + model.series[0].split_color_mode === 'kibana' + ? 'kibana_palette' + : model.series[0].split_color_mode || model.series[0].palette.name, + }; + const props = { + seriesById: visData[model.id].series, + seriesName, + seriesId, + baseColor, + seriesPalette: palette, + palettesRegistry: palettesService, + syncColors, + }; + return getSplitByTermsColor(props) || null; + }, + [model, palettesService, syncColors, visData] + ); + if (!model || !visData || !visData[model.id]) return ; if (visData[model.id].series.every((s) => s.id.split(':').length === 1)) { return ; @@ -36,11 +61,14 @@ export function visWithSplits(WrappedComponent) { } const labelHasKeyPlaceholder = /{{\s*key\s*}}/.test(seriesModel.label); + const color = series.color || seriesModel.color; + const finalColor = + model.series[0].split_mode === 'terms' ? getSeriesColor(label, series.id, color) : color; acc[splitId].series.push({ ...series, id: seriesId, - color: series.color || seriesModel.color, + color: finalColor, label: seriesModel.label && !labelHasKeyPlaceholder ? seriesModel.label : label, }); return acc; diff --git a/src/plugins/vis_type_timeseries/public/application/lib/compute_gradient_final_color.test.ts b/src/plugins/vis_type_timeseries/public/application/lib/compute_gradient_final_color.test.ts new file mode 100644 index 0000000000000..326b14c112c35 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/lib/compute_gradient_final_color.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { computeGradientFinalColor } from './compute_gradient_final_color'; + +describe('computeGradientFinalColor Function', () => { + it('Should compute the gradient final color correctly for rgb color', () => { + const color = computeGradientFinalColor('rgba(211,96,134,1)'); + expect(color).toEqual('rgb(145, 40, 75)'); + }); + + it('Should compute the gradient final color correctly for hex color', () => { + const color = computeGradientFinalColor('#6092C0'); + expect(color).toEqual('rgb(43, 77, 108)'); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/lib/compute_gradient_final_color.ts b/src/plugins/vis_type_timeseries/public/application/lib/compute_gradient_final_color.ts new file mode 100644 index 0000000000000..3dbd6796c0574 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/lib/compute_gradient_final_color.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import Color from 'color'; + +export const computeGradientFinalColor = (color: string): string => { + let inputColor = new Color(color); + const hsl = inputColor.hsl().object(); + hsl.l -= inputColor.luminosity() * 100; + inputColor = Color.hsl(hsl); + return inputColor.rgb().toString(); +}; diff --git a/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.test.ts b/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.test.ts new file mode 100644 index 0000000000000..1d81531642566 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { chartPluginMock } from '../../../../charts/public/mocks'; +import { getSplitByTermsColor, SplitByTermsColorProps } from './get_split_by_terms_color'; + +const chartsRegistry = chartPluginMock.createPaletteRegistry(); +const props = ({ + seriesById: [ + { + id: '61ca57f1-469d-11e7-af02-69e470af7417', + label: 'Count', + color: 'rgb(104, 188, 0)', + data: [ + [1615273200000, 45], + [1615284000000, 78], + ], + seriesId: '61ca57f1-469d-11e7-af02-69e470af7417', + stack: 'none', + lines: { + show: true, + fill: 0.5, + lineWidth: 1, + steps: false, + }, + points: { + show: true, + radius: 1, + lineWidth: 1, + }, + bars: { + show: false, + fill: 0.5, + lineWidth: 1, + }, + groupId: 'yaxis_2b3507e0-8630-11eb-b627-ff396f1f7246_main_group', + yScaleType: 'linear', + }, + ], + seriesName: 'Count', + seriesId: '61ca57f1-469d-11e7-af02-69e470af7417', + baseColor: '#68BC00', + seriesPalette: { + name: 'rainbow', + params: { + colors: ['#0F1419', '#666666'], + gradient: false, + }, + type: 'palette', + }, + palettesRegistry: chartsRegistry, + syncColors: false, +} as unknown) as SplitByTermsColorProps; + +describe('getSplitByTermsColor Function', () => { + it('Should return null if no palette given', () => { + const newProps = ({ ...props, seriesPalette: null } as unknown) as SplitByTermsColorProps; + const color = getSplitByTermsColor(newProps); + expect(color).toEqual(null); + }); + + it('Should return color for empty seriesName', () => { + const newProps = { ...props, seriesName: '' }; + const color = getSplitByTermsColor(newProps); + expect(color).toEqual('blue'); + }); + + it('Should return color for the given palette', () => { + const color = getSplitByTermsColor(props); + expect(color).toEqual('blue'); + }); + + it('Should call the `get` palette method with the correct arguments', () => { + const spy = jest.spyOn(chartsRegistry, 'get'); + const gradientPalette = { + name: 'gradient', + params: { + colors: ['#68BC00', '#666666'], + gradient: true, + }, + }; + const newProps = ({ + ...props, + seriesPalette: gradientPalette, + } as unknown) as SplitByTermsColorProps; + getSplitByTermsColor(newProps); + expect(spy).toHaveBeenCalledWith('custom'); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.ts b/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.ts new file mode 100644 index 0000000000000..e8f81bd8c6045 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/lib/get_split_by_terms_color.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public'; +import { PALETTES, PanelData } from '../../../common/types'; +import { computeGradientFinalColor } from './compute_gradient_final_color'; +import { rainbowColors } from './rainbow_colors'; +import { emptyLabel } from '../../../common/empty_label'; + +interface PaletteParams { + colors: string[]; + gradient: boolean; +} + +export interface SplitByTermsColorProps { + seriesById: PanelData[]; + seriesName: string; + seriesId: string; + baseColor: string; + seriesPalette: PaletteOutput; + palettesRegistry: PaletteRegistry; + syncColors: boolean; +} + +export const getSplitByTermsColor = ({ + seriesById, + seriesName, + seriesId, + baseColor, + seriesPalette, + palettesRegistry, + syncColors, +}: SplitByTermsColorProps) => { + if (!seriesPalette) { + return null; + } + const paletteName = + seriesPalette.name === PALETTES.RAINBOW || seriesPalette.name === PALETTES.GRADIENT + ? 'custom' + : seriesPalette.name; + + const paletteParams = + seriesPalette.name === PALETTES.GRADIENT + ? { + ...seriesPalette.params, + colors: [baseColor, computeGradientFinalColor(baseColor)], + gradient: true, + } + : seriesPalette.name === PALETTES.RAINBOW + ? { + ...seriesPalette.params, + colors: rainbowColors, + } + : seriesPalette.params; + + const outputColor = palettesRegistry?.get(paletteName).getColor( + [ + { + name: seriesName || emptyLabel, + rankAtDepth: seriesById.findIndex(({ id }) => id === seriesId), + totalSeriesAtDepth: seriesById.length, + }, + ], + { + maxDepth: 1, + totalSeries: seriesById.length, + behindText: false, + syncColors, + }, + paletteParams + ); + return outputColor; +}; diff --git a/src/plugins/vis_type_timeseries/public/application/lib/rainbow_colors.ts b/src/plugins/vis_type_timeseries/public/application/lib/rainbow_colors.ts new file mode 100644 index 0000000000000..5ad8668326cd4 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/lib/rainbow_colors.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/** + * Using a random color generator presented awful colors and unpredictable color schemes. + * So we needed to come up with a color scheme of our own that creates consistent, pleasing color patterns. + * The order allows us to guarantee that 1st, 2nd, 3rd, etc values always get the same color. + */ +export const rainbowColors: string[] = [ + '#68BC00', + '#009CE0', + '#B0BC00', + '#16A5A5', + '#D33115', + '#E27300', + '#FCC400', + '#7B64FF', + '#FA28FF', + '#333333', + '#808080', + '#194D33', + '#0062B1', + '#808900', + '#0C797D', + '#9F0500', + '#C45100', + '#FB9E00', + '#653294', + '#AB149E', + '#0F1419', + '#666666', +]; diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index d306704463c2e..537344a6da39a 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useCallback } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { labelDateFormatter } from '../../../components/lib/label_date_formatter'; @@ -31,6 +31,7 @@ import { BarSeriesDecorator } from './decorators/bar_decorator'; import { getStackAccessors } from './utils/stack_format'; import { getBaseTheme, getChartClasses } from './utils/theme'; import { emptyLabel } from '../../../../../common/empty_label'; +import { getSplitByTermsColor } from '../../../lib/get_split_by_terms_color'; const generateAnnotationData = (values, formatter) => values.map(({ key, docs }) => ({ @@ -59,8 +60,11 @@ export const TimeSeries = ({ onBrush, xAxisFormatter, annotations, + syncColors, + palettesService, }) => { const chartRef = useRef(); + // const [palettesRegistry, setPalettesRegistry] = useState(null); useEffect(() => { const updateCursor = (cursor) => { @@ -87,10 +91,9 @@ export const TimeSeries = ({ // If the color isn't configured by the user, use the color mapping service // to assign a color from the Kibana palette. Colors will be shared across the // session, including dashboards. - const { legacyColors: colors, theme: themeService } = getChartsSetup(); - const baseTheme = getBaseTheme(themeService.useChartsBaseTheme(), backgroundColor); + const { theme: themeService } = getChartsSetup(); - colors.mappedColors.mapKeys(series.filter(({ color }) => !color).map(({ label }) => label)); + const baseTheme = getBaseTheme(themeService.useChartsBaseTheme(), backgroundColor); const onBrushEndListener = ({ x }) => { if (!x) { @@ -100,6 +103,23 @@ export const TimeSeries = ({ onBrush(min, max); }; + const getSeriesColor = useCallback( + (seriesName, seriesGroupId, seriesId) => { + const seriesById = series.filter((s) => s.seriesId === seriesGroupId); + const props = { + seriesById, + seriesName, + seriesId, + baseColor: seriesById[0].baseColor, + seriesPalette: seriesById[0].palette, + palettesRegistry: palettesService, + syncColors, + }; + return getSplitByTermsColor(props) || null; + }, + [palettesService, series, syncColors] + ); + return ( ({ help: '', }, }, - async fn(input, args, { getSearchSessionId }) { + async fn(input, args, { getSearchSessionId, isSyncColorsEnabled }) { const visParams: TimeseriesVisParams = JSON.parse(args.params); const uiState = JSON.parse(args.uiState); + const syncColors = isSyncColorsEnabled?.() ?? false; const response = await metricsRequestHandler({ input, @@ -70,6 +72,7 @@ export const createMetricsFn = (): TimeseriesExpressionFunctionDefinition => ({ value: { visParams, visData: response, + syncColors, }, }; }, diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index a19d664a94927..9e996fcc74833 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -31,7 +31,10 @@ export const metricsVisDefinition = { id: '61ca57f1-469d-11e7-af02-69e470af7417', color: '#68BC00', split_mode: 'everything', - split_color_mode: 'kibana', + palette: { + type: 'palette', + name: 'default', + }, metrics: [ { id: '61ca57f2-469d-11e7-af02-69e470af7417', diff --git a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx b/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx index 06c5d20f08a7c..c314594aa5420 100644 --- a/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx +++ b/src/plugins/vis_type_timeseries/public/timeseries_vis_renderer.tsx @@ -16,6 +16,7 @@ import { ExpressionRenderDefinition } from '../../expressions/common/expression_ import { TimeseriesRenderValue } from './metrics_fn'; import { TimeseriesVisData } from '../common/types'; import { TimeseriesVisParams } from './types'; +import { getChartsSetup } from './services'; const TimeseriesVisualization = lazy( () => import('./application/components/timeseries_visualization') @@ -39,8 +40,10 @@ export const getTimeseriesVisRenderer: (deps: { handlers.onDestroy(() => { unmountComponentAtNode(domNode); }); + const { palettes } = getChartsSetup(); const showNoResult = !checkIfDataExists(config.visData, config.visParams); + const palettesService = await palettes.getPalettes(); render( , domNode diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_split_colors.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_split_colors.js deleted file mode 100644 index 7a4d61487f718..0000000000000 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_split_colors.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import Color from 'color'; - -export function getSplitColors(inputColor, size = 10, style = 'kibana') { - const color = new Color(inputColor); - const colors = []; - let workingColor = Color.hsl(color.hsl().object()); - - if (style === 'rainbow') { - return [ - '#68BC00', - '#009CE0', - '#B0BC00', - '#16A5A5', - '#D33115', - '#E27300', - '#FCC400', - '#7B64FF', - '#FA28FF', - '#333333', - '#808080', - '#194D33', - '#0062B1', - '#808900', - '#0C797D', - '#9F0500', - '#C45100', - '#FB9E00', - '#653294', - '#AB149E', - '#0F1419', - '#666666', - ]; - } else if (style === 'gradient') { - colors.push(color.string()); - const rotateBy = color.luminosity() / (size - 1); - for (let i = 0; i < size - 1; i++) { - const hsl = workingColor.hsl().object(); - hsl.l -= rotateBy * 100; - workingColor = Color.hsl(hsl); - colors.push(workingColor.rgb().toString()); - } - } - - return colors; -} diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js index 952696644c7d9..f22226e03a5aa 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.js @@ -10,7 +10,6 @@ import Color from 'color'; import { calculateLabel } from '../../../../common/calculate_label'; import _ from 'lodash'; import { getLastMetric } from './get_last_metric'; -import { getSplitColors } from './get_split_colors'; import { formatKey } from './format_key'; const getTimeSeries = (resp, series) => @@ -30,14 +29,12 @@ export async function getSplits(resp, panel, series, meta, extractFields) { if (buckets) { if (Array.isArray(buckets)) { - const size = buckets.length; - const colors = getSplitColors(series.color, size, series.split_color_mode); return buckets.map((bucket) => { bucket.id = `${series.id}:${bucket.key}`; bucket.splitByLabel = splitByLabel; bucket.label = formatKey(bucket.key, series); bucket.labelFormatted = bucket.key_as_string ? formatKey(bucket.key_as_string, series) : ''; - bucket.color = panel.type === 'top_n' ? color.string() : colors.shift(); + bucket.color = color.string(); bucket.meta = meta; return bucket; }); diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js index d605bfc9d8de7..e2ae404d98970 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/helpers/get_splits.test.js @@ -223,118 +223,6 @@ describe('getSplits(resp, panel, series)', () => { ]); }); - describe('terms group bys', () => { - const resp = { - aggregations: { - SERIES: { - buckets: [ - { - key: 'example-01', - timeseries: { buckets: [] }, - SIBAGG: { value: 1 }, - }, - { - key: 'example-02', - timeseries: { buckets: [] }, - SIBAGG: { value: 2 }, - }, - ], - meta: { bucketSize: 10 }, - }, - }, - }; - - test('should return a splits with no color', async () => { - const series = { - id: 'SERIES', - color: '#F00', - split_mode: 'terms', - terms_field: 'beat.hostname', - terms_size: 10, - metrics: [ - { id: 'AVG', type: 'avg', field: 'cpu' }, - { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, - ], - }; - const panel = { type: 'timeseries' }; - - expect(await getSplits(resp, panel, series)).toEqual([ - { - id: 'SERIES:example-01', - key: 'example-01', - label: 'example-01', - labelFormatted: '', - meta: { bucketSize: 10 }, - color: undefined, - splitByLabel: 'Overall Average of Average of cpu', - timeseries: { buckets: [] }, - SIBAGG: { value: 1 }, - }, - { - id: 'SERIES:example-02', - key: 'example-02', - label: 'example-02', - labelFormatted: '', - meta: { bucketSize: 10 }, - color: undefined, - splitByLabel: 'Overall Average of Average of cpu', - timeseries: { buckets: [] }, - SIBAGG: { value: 2 }, - }, - ]); - }); - - test('should return gradient color', async () => { - const series = { - id: 'SERIES', - color: '#F00', - split_mode: 'terms', - split_color_mode: 'gradient', - terms_field: 'beat.hostname', - terms_size: 10, - metrics: [ - { id: 'AVG', type: 'avg', field: 'cpu' }, - { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, - ], - }; - const panel = { type: 'timeseries' }; - - expect(await getSplits(resp, panel, series)).toEqual([ - expect.objectContaining({ - color: 'rgb(255, 0, 0)', - }), - expect.objectContaining({ - color: 'rgb(147, 0, 0)', - }), - ]); - }); - - test('should return rainbow color', async () => { - const series = { - id: 'SERIES', - color: '#F00', - split_mode: 'terms', - split_color_mode: 'rainbow', - terms_field: 'beat.hostname', - terms_size: 10, - metrics: [ - { id: 'AVG', type: 'avg', field: 'cpu' }, - { id: 'SIBAGG', type: 'avg_bucket', field: 'AVG' }, - ], - }; - const panel = { type: 'timeseries' }; - - expect(await getSplits(resp, panel, series)).toEqual([ - expect.objectContaining({ - color: '#68BC00', - }), - expect.objectContaining({ - color: '#009CE0', - }), - ]); - }); - }); - test('should return a splits for filters group bys', async () => { const resp = { aggregations: { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 53245fde2796f..ad3cf86081e15 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4438,7 +4438,6 @@ "visTypeTimeseries.timeSeries.chartLine.stepsLabel": "ステップ", "visTypeTimeseries.timeSeries.cloneSeriesTooltip": "数列のクローンを作成", "visTypeTimeseries.timeseries.dataTab.dataButtonLabel": "データ", - "visTypeTimeseries.timeSeries.defaultPaletteLabel": "既定のパレット", "visTypeTimeseries.timeSeries.deleteSeriesTooltip": "数列を削除", "visTypeTimeseries.timeSeries.gradientLabel": "グラデーション", "visTypeTimeseries.timeSeries.hideInLegendLabel": "凡例で非表示", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d26a0111fe382..babbccfab4dde 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4464,7 +4464,6 @@ "visTypeTimeseries.timeSeries.chartLine.stepsLabel": "步长", "visTypeTimeseries.timeSeries.cloneSeriesTooltip": "克隆序列", "visTypeTimeseries.timeseries.dataTab.dataButtonLabel": "数据", - "visTypeTimeseries.timeSeries.defaultPaletteLabel": "默认调色板", "visTypeTimeseries.timeSeries.deleteSeriesTooltip": "删除序列", "visTypeTimeseries.timeSeries.gradientLabel": "渐变", "visTypeTimeseries.timeSeries.hideInLegendLabel": "在图例中隐藏",