[TSVB] Integrates the color service (#93749)

* [TSVB] Integrates the color service

* Fix i18n failure

* Sync colors :)

* Fix unit tests

* Apply the multiple colors also for gauge

* Fix

* More unit tests

* Cleanup

* Be backwards compatible

* Fetch palettesService on vis renderer

* Fix eslint

* Fix jest test

* Fix color mapping for empty labels

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Stratoula Kalafateli 2021-03-23 08:24:58 +02:00 committed by GitHub
parent e90e5bae8c
commit 7d303eb42d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 556 additions and 231 deletions

View file

@ -25,7 +25,7 @@ export type PanelSchema = TypeOf<typeof panel>;
export type VisPayload = TypeOf<typeof visPayloadSchema>;
export type FieldObject = TypeOf<typeof fieldObject>;
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',
}

View file

@ -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,

View file

@ -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<PalettePickerProps>;
beforeAll(() => {
props = {
palettes: chartPluginMock.createPaletteRegistry(),
activePalette: {
type: 'palette',
name: 'kibana_palette',
},
setPalette: jest.fn(),
color: '#68BC00',
};
});
it('renders the EuiPalettePicker', () => {
component = mountWithIntl(<PalettePicker {...props} />);
expect(component.find(EuiColorPalettePicker).length).toBe(1);
});
it('renders the default palette if not activePalette is given', function () {
const { activePalette, ...newProps } = props;
component = mountWithIntl(<PalettePicker {...newProps} />);
const palettePicker = component.find(EuiColorPalettePicker);
expect(palettePicker.props().valueOfSelected).toBe('default');
});
it('renders the activePalette palette if given', function () {
component = mountWithIntl(<PalettePicker {...props} />);
const palettePicker = component.find(EuiColorPalettePicker);
expect(palettePicker.props().valueOfSelected).toBe('kibana_palette');
});
it('renders two additional palettes, rainbow and gradient', function () {
component = mountWithIntl(<PalettePicker {...props} />);
const palettePicker = component.find(EuiColorPalettePicker);
expect(palettePicker.props().palettes).toEqual(
expect.arrayContaining([
expect.objectContaining({
value: PALETTES.RAINBOW,
}),
expect.objectContaining({
value: PALETTES.GRADIENT,
}),
])
);
});
});

View file

@ -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 (
<EuiColorPalettePicker
fullWidth
data-test-subj="visEditorPalettePicker"
compressed
palettes={[
...palettes
.getAll()
.filter(({ internal }) => !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'}
/>
);
}

View file

@ -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}
/>
);
}

View file

@ -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;
}

View file

@ -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 (
<div className="tvbAggRow">
<EuiFlexGroup gutterSize="s">
@ -420,25 +418,26 @@ export const TimeseriesConfig = injectI18n(function (props) {
<EuiSpacer size="s" />
<YesNo value={model.hide_in_legend} name="hide_in_legend" onChange={props.onChange} />
</EuiFlexItem>
<EuiFlexItem grow={true}>
<EuiFormRow
id={htmlId('splitColor')}
label={
<FormattedMessage
id="visTypeTimeseries.timeSeries.splitColorThemeLabel"
defaultMessage="Split color theme"
{palettesRegistry && (
<EuiFlexItem grow={true}>
<EuiFormRow
id={htmlId('splitColor')}
label={
<FormattedMessage
id="visTypeTimeseries.timeSeries.splitColorThemeLabel"
defaultMessage="Split color theme"
/>
}
>
<PalettePicker
palettes={palettesRegistry}
activePalette={initialPalette}
setPalette={handlePaletteChange}
color={model.color}
/>
}
>
<EuiComboBox
isClearable={false}
options={splitColorOptions}
selectedOptions={selectedSplitColorOption ? [selectedSplitColorOption] : []}
onChange={handleSelectChange('split_color_mode')}
singleSelection={{ asPlainText: true }}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFormRow>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiHorizontalRule margin="s" />

View file

@ -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}
/>
</div>
);

View file

@ -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 <WrappedComponent {...props} />;
if (visData[model.id].series.every((s) => s.id.split(':').length === 1)) {
return <WrappedComponent {...props} />;
@ -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;

View file

@ -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)');
});
});

View file

@ -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();
};

View file

@ -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');
});
});

View file

@ -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<PaletteParams>;
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;
};

View file

@ -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',
];

View file

@ -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 (
<Chart ref={chartRef} renderer="canvas" className={classes}>
<Settings
@ -155,6 +175,7 @@ export const TimeSeries = ({
(
{
id,
seriesId,
label,
labelFormatted,
bars,
@ -165,6 +186,7 @@ export const TimeSeries = ({
yScaleType,
groupId,
color,
isSplitByTerms,
stack,
points,
y1AccessorFormat,
@ -177,12 +199,13 @@ export const TimeSeries = ({
const isPercentage = stack === STACKED_OPTIONS.PERCENT;
const isStacked = stack !== STACKED_OPTIONS.NONE;
const key = `${id}-${label}`;
// Only use color mapping if there is no color from the server
const finalColor = color ?? colors.mappedColors.mapping[label];
let seriesName = label.toString();
if (labelFormatted) {
seriesName = labelDateFormatter(labelFormatted);
}
// The colors from the paletteService should be applied only when the timeseries is split by terms
const splitColor = getSeriesColor(seriesName, seriesId, id);
const finalColor = isSplitByTerms && splitColor ? splitColor : color;
if (bars?.show) {
return (
<BarSeriesDecorator

View file

@ -25,6 +25,7 @@ interface Arguments {
export interface TimeseriesRenderValue {
visData: TimeseriesVisData | {};
visParams: TimeseriesVisParams;
syncColors: boolean;
}
export type TimeseriesExpressionFunctionDefinition = ExpressionFunctionDefinition<
@ -53,9 +54,10 @@ export const createMetricsFn = (): TimeseriesExpressionFunctionDefinition => ({
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,
},
};
},

View file

@ -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',

View file

@ -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(
<VisualizationContainer
@ -54,7 +57,9 @@ export const getTimeseriesVisRenderer: (deps: {
handlers={handlers}
model={config.visParams}
visData={config.visData as TimeseriesVisData}
syncColors={config.syncColors}
uiState={handlers.uiState! as PersistedState}
palettesService={palettesService}
/>
</VisualizationContainer>,
domNode

View file

@ -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;
}

View file

@ -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;
});

View file

@ -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: {

View file

@ -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": "凡例で非表示",

View file

@ -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": "在图例中隐藏",