[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:
parent
e90e5bae8c
commit
7d303eb42d
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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'}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)');
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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',
|
||||
];
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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": "凡例で非表示",
|
||||
|
|
|
@ -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": "在图例中隐藏",
|
||||
|
|
Loading…
Reference in a new issue