[TSVB] Use default Kibana palette for split series (#62241)
* [TSVB] Rainbow palette shares colors with dashboard * Add migration * Add charts as NP dependency Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
44a2c2f8f7
commit
7e3de56303
12 changed files with 218 additions and 50 deletions
|
@ -4,6 +4,6 @@
|
|||
"kibanaVersion": "kibana",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["data", "expressions", "visualizations"],
|
||||
"requiredPlugins": ["charts", "data", "expressions", "visualizations"],
|
||||
"optionalPlugins": ["usageCollection"]
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ export const TimeseriesConfig = injectI18n(function(props) {
|
|||
point_size: '',
|
||||
value_template: '{{value}}',
|
||||
offset_time: '',
|
||||
split_color_mode: 'gradient',
|
||||
split_color_mode: 'kibana',
|
||||
axis_min: '',
|
||||
axis_max: '',
|
||||
stacked: STACKED_OPTIONS.NONE,
|
||||
|
@ -140,10 +140,10 @@ export const TimeseriesConfig = injectI18n(function(props) {
|
|||
const splitColorOptions = [
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'visTypeTimeseries.timeSeries.gradientLabel',
|
||||
defaultMessage: 'Gradient',
|
||||
id: 'visTypeTimeseries.timeSeries.defaultPaletteLabel',
|
||||
defaultMessage: 'Default palette',
|
||||
}),
|
||||
value: 'gradient',
|
||||
value: 'kibana',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
|
@ -152,6 +152,13 @@ export const TimeseriesConfig = injectI18n(function(props) {
|
|||
}),
|
||||
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;
|
||||
|
|
|
@ -33,7 +33,7 @@ import {
|
|||
import { EuiIcon } from '@elastic/eui';
|
||||
import { getTimezone } from '../../../lib/get_timezone';
|
||||
import { eventBus, ACTIVE_CURSOR } from '../../lib/active_cursor';
|
||||
import { getUISettings } from '../../../../services';
|
||||
import { getUISettings, getChartsSetup } from '../../../../services';
|
||||
import { GRID_LINE_CONFIG, ICON_TYPES_MAP, STACKED_OPTIONS } from '../../constants';
|
||||
import { AreaSeriesDecorator } from './decorators/area_decorator';
|
||||
import { BarSeriesDecorator } from './decorators/bar_decorator';
|
||||
|
@ -94,6 +94,12 @@ export const TimeSeries = ({
|
|||
// apply legend style change if bgColor is configured
|
||||
const classes = classNames('tvbVisTimeSeries', getChartClasses(backgroundColor));
|
||||
|
||||
// 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 { colors } = getChartsSetup();
|
||||
colors.mappedColors.mapKeys(series.filter(({ color }) => !color).map(({ label }) => label));
|
||||
|
||||
return (
|
||||
<Chart ref={chartRef} renderer="canvas" className={classes}>
|
||||
<Settings
|
||||
|
@ -163,6 +169,8 @@ export const TimeSeries = ({
|
|||
const stackAccessors = getStackAccessors(stack);
|
||||
const isPercentage = stack === STACKED_OPTIONS.PERCENT;
|
||||
const key = `${id}-${label}`;
|
||||
// Only use color mapping if there is no color from the server
|
||||
const finalColor = color ?? colors.mappedColors.mapping[label];
|
||||
|
||||
if (bars.show) {
|
||||
return (
|
||||
|
@ -174,7 +182,7 @@ export const TimeSeries = ({
|
|||
data={data}
|
||||
hideInLegend={hideInLegend}
|
||||
bars={bars}
|
||||
color={color}
|
||||
color={finalColor}
|
||||
stackAccessors={stackAccessors}
|
||||
stackAsPercentage={isPercentage}
|
||||
xScaleType={xScaleType}
|
||||
|
@ -199,7 +207,7 @@ export const TimeSeries = ({
|
|||
data={data}
|
||||
hideInLegend={hideInLegend}
|
||||
lines={lines}
|
||||
color={color}
|
||||
color={finalColor}
|
||||
stackAccessors={stackAccessors}
|
||||
stackAsPercentage={isPercentage}
|
||||
points={points}
|
||||
|
|
|
@ -43,6 +43,7 @@ export const metricsVisDefinition = {
|
|||
id: '61ca57f1-469d-11e7-af02-69e470af7417',
|
||||
color: '#68BC00',
|
||||
split_mode: 'everything',
|
||||
split_color_mode: 'kibana',
|
||||
metrics: [
|
||||
{
|
||||
id: '61ca57f2-469d-11e7-af02-69e470af7417',
|
||||
|
|
|
@ -32,13 +32,16 @@ import {
|
|||
setFieldFormats,
|
||||
setCoreStart,
|
||||
setDataStart,
|
||||
setChartsSetup,
|
||||
} from './services';
|
||||
import { DataPublicPluginStart } from '../../data/public';
|
||||
import { ChartsPluginSetup } from '../../charts/public';
|
||||
|
||||
/** @internal */
|
||||
export interface MetricsPluginSetupDependencies {
|
||||
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;
|
||||
visualizations: VisualizationsSetup;
|
||||
charts: ChartsPluginSetup;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -56,10 +59,11 @@ export class MetricsPlugin implements Plugin<Promise<void>, void> {
|
|||
|
||||
public async setup(
|
||||
core: CoreSetup,
|
||||
{ expressions, visualizations }: MetricsPluginSetupDependencies
|
||||
{ expressions, visualizations, charts }: MetricsPluginSetupDependencies
|
||||
) {
|
||||
expressions.registerFunction(createMetricsFn);
|
||||
setUISettings(core.uiSettings);
|
||||
setChartsSetup(charts);
|
||||
visualizations.createReactVisualization(metricsVisDefinition);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import { I18nStart, SavedObjectsStart, IUiSettingsClient, CoreStart } from 'src/core/public';
|
||||
import { createGetterSetter } from '../../kibana_utils/public';
|
||||
import { ChartsPluginSetup } from '../../charts/public';
|
||||
import { DataPublicPluginStart } from '../../data/public';
|
||||
|
||||
export const [getUISettings, setUISettings] = createGetterSetter<IUiSettingsClient>('UISettings');
|
||||
|
@ -36,3 +37,7 @@ export const [getCoreStart, setCoreStart] = createGetterSetter<CoreStart>('CoreS
|
|||
export const [getDataStart, setDataStart] = createGetterSetter<DataPublicPluginStart>('DataStart');
|
||||
|
||||
export const [getI18n, setI18n] = createGetterSetter<I18nStart>('I18n');
|
||||
|
||||
export const [getChartsSetup, setChartsSetup] = createGetterSetter<ChartsPluginSetup>(
|
||||
'ChartsPluginSetup'
|
||||
);
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
import Color from 'color';
|
||||
|
||||
export function getSplitColors(inputColor, size = 10, style = 'gradient') {
|
||||
export function getSplitColors(inputColor, size = 10, style = 'kibana') {
|
||||
const color = new Color(inputColor);
|
||||
const colors = [];
|
||||
let workingColor = Color.hsl(color.hsl().object());
|
||||
|
@ -49,7 +49,7 @@ export function getSplitColors(inputColor, size = 10, style = 'gradient') {
|
|||
'#0F1419',
|
||||
'#666666',
|
||||
];
|
||||
} else {
|
||||
} else if (style === 'gradient') {
|
||||
colors.push(color.string());
|
||||
const rotateBy = color.luminosity() / (size - 1);
|
||||
for (let i = 0; i < size - 1; i++) {
|
||||
|
|
|
@ -106,7 +106,7 @@ describe('getSplits(resp, panel, series)', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('should return a splits for terms group bys', () => {
|
||||
describe('terms group bys', () => {
|
||||
const resp = {
|
||||
aggregations: {
|
||||
SERIES: {
|
||||
|
@ -126,38 +126,89 @@ describe('getSplits(resp, panel, series)', () => {
|
|||
},
|
||||
},
|
||||
};
|
||||
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(getSplits(resp, panel, series)).toEqual([
|
||||
{
|
||||
id: 'SERIES:example-01',
|
||||
key: 'example-01',
|
||||
label: 'example-01',
|
||||
meta: { bucketSize: 10 },
|
||||
color: 'rgb(255, 0, 0)',
|
||||
timeseries: { buckets: [] },
|
||||
SIBAGG: { value: 1 },
|
||||
},
|
||||
{
|
||||
id: 'SERIES:example-02',
|
||||
key: 'example-02',
|
||||
label: 'example-02',
|
||||
meta: { bucketSize: 10 },
|
||||
color: 'rgb(147, 0, 0)',
|
||||
timeseries: { buckets: [] },
|
||||
SIBAGG: { value: 2 },
|
||||
},
|
||||
]);
|
||||
|
||||
test('should return a splits with no color', () => {
|
||||
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(getSplits(resp, panel, series)).toEqual([
|
||||
{
|
||||
id: 'SERIES:example-01',
|
||||
key: 'example-01',
|
||||
label: 'example-01',
|
||||
meta: { bucketSize: 10 },
|
||||
color: undefined,
|
||||
timeseries: { buckets: [] },
|
||||
SIBAGG: { value: 1 },
|
||||
},
|
||||
{
|
||||
id: 'SERIES:example-02',
|
||||
key: 'example-02',
|
||||
label: 'example-02',
|
||||
meta: { bucketSize: 10 },
|
||||
color: undefined,
|
||||
timeseries: { buckets: [] },
|
||||
SIBAGG: { value: 2 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should return gradient color', () => {
|
||||
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(getSplits(resp, panel, series)).toEqual([
|
||||
expect.objectContaining({
|
||||
color: 'rgb(255, 0, 0)',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
color: 'rgb(147, 0, 0)',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test('should return rainbow color', () => {
|
||||
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(getSplits(resp, panel, series)).toEqual([
|
||||
expect.objectContaining({
|
||||
color: '#68BC00',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
color: '#009CE0',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should return a splits for filters group bys', () => {
|
||||
|
|
|
@ -1460,4 +1460,62 @@ describe('migration visualization', () => {
|
|||
expect(migratedParams.gauge_color_rules[1]).toEqual(params.gauge_color_rules[1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('7.8.0 tsvb split_color_mode', () => {
|
||||
const migrate = (doc: any) =>
|
||||
visualizationSavedObjectTypeMigrations['7.8.0'](
|
||||
doc as Parameters<SavedObjectMigrationFn>[0],
|
||||
savedObjectMigrationContext
|
||||
);
|
||||
|
||||
const generateDoc = (params: any) => ({
|
||||
attributes: {
|
||||
title: 'My Vis',
|
||||
type: 'visualization',
|
||||
description: 'This is my super cool vis.',
|
||||
visState: JSON.stringify(params),
|
||||
uiStateJSON: '{}',
|
||||
version: 1,
|
||||
kibanaSavedObjectMeta: {
|
||||
searchSourceJSON: '{}',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it('should change a missing split_color_mode to gradient', () => {
|
||||
const params = { type: 'metrics', params: { series: [{}] } };
|
||||
const testDoc1 = generateDoc(params);
|
||||
const migratedTestDoc1 = migrate(testDoc1);
|
||||
const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series;
|
||||
|
||||
expect(series[0].split_color_mode).toEqual('gradient');
|
||||
});
|
||||
|
||||
it('should not change the color mode if it is set', () => {
|
||||
const params = { type: 'metrics', params: { series: [{ split_color_mode: 'gradient' }] } };
|
||||
const testDoc1 = generateDoc(params);
|
||||
const migratedTestDoc1 = migrate(testDoc1);
|
||||
const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series;
|
||||
|
||||
expect(series[0].split_color_mode).toEqual('gradient');
|
||||
});
|
||||
|
||||
it('should not change the color mode if it is non-default', () => {
|
||||
const params = { type: 'metrics', params: { series: [{ split_color_mode: 'rainbow' }] } };
|
||||
const testDoc1 = generateDoc(params);
|
||||
const migratedTestDoc1 = migrate(testDoc1);
|
||||
const series = JSON.parse(migratedTestDoc1.attributes.visState).params.series;
|
||||
|
||||
expect(series[0].split_color_mode).toEqual('rainbow');
|
||||
});
|
||||
|
||||
it('should not migrate a visualization of unknown type', () => {
|
||||
const params = { type: 'unknown', params: { series: [{}] } };
|
||||
const doc = generateDoc(params);
|
||||
const migratedDoc = migrate(doc);
|
||||
const series = JSON.parse(migratedDoc.attributes.visState).params.series;
|
||||
|
||||
expect(series[0].split_color_mode).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -602,7 +602,39 @@ const migrateMatchAllQuery: SavedObjectMigrationFn = doc => {
|
|||
};
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
|
||||
// [TSVB] Default color palette is changing, keep the default for older viz
|
||||
const migrateTsvbDefaultColorPalettes: SavedObjectMigrationFn = doc => {
|
||||
const visStateJSON = get<string>(doc, 'attributes.visState');
|
||||
let visState;
|
||||
|
||||
if (visStateJSON) {
|
||||
try {
|
||||
visState = JSON.parse(visStateJSON);
|
||||
} catch (e) {
|
||||
// Let it go, the data is invalid and we'll leave it as is
|
||||
}
|
||||
if (visState && visState.type === 'metrics') {
|
||||
const series: any[] = get(visState, 'params.series') || [];
|
||||
|
||||
series.forEach(part => {
|
||||
// The default value was not saved before
|
||||
if (!part.split_color_mode) {
|
||||
part.split_color_mode = 'gradient';
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...doc,
|
||||
attributes: {
|
||||
...doc.attributes,
|
||||
visState: JSON.stringify(visState),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return doc;
|
||||
};
|
||||
|
||||
|
@ -639,4 +671,5 @@ export const visualizationSavedObjectTypeMigrations = {
|
|||
'7.3.1': flow<SavedObjectMigrationFn>(migrateFiltersAggQueryStringQueries),
|
||||
'7.4.2': flow<SavedObjectMigrationFn>(transformSplitFiltersStringToQueryObject),
|
||||
'7.7.0': flow<SavedObjectMigrationFn>(migrateOperatorKeyTypo),
|
||||
'7.8.0': flow<SavedObjectMigrationFn>(migrateTsvbDefaultColorPalettes),
|
||||
};
|
||||
|
|
|
@ -25,7 +25,6 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
const retry = getService('retry');
|
||||
const log = getService('log');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const testSubjects = getService('testSubjects');
|
||||
|
||||
describe('visual builder', function describeIndexTests() {
|
||||
beforeEach(async () => {
|
||||
|
@ -126,20 +125,18 @@ export default function({ getPageObjects, getService }: FtrProviderContext) {
|
|||
expect(actualCountMin).to.be('3 hours');
|
||||
});
|
||||
|
||||
// --reversed class is not implemented in @elastic\chart
|
||||
describe.skip('Dark mode', () => {
|
||||
describe('Dark mode', () => {
|
||||
before(async () => {
|
||||
await kibanaServer.uiSettings.update({
|
||||
'theme:darkMode': true,
|
||||
});
|
||||
});
|
||||
|
||||
it(`viz should have 'reversed' class when background color is white`, async () => {
|
||||
it(`viz should have light class when background color is white`, async () => {
|
||||
await visualBuilder.clickPanelOptions('timeSeries');
|
||||
await visualBuilder.setBackgroundColor('#FFFFFF');
|
||||
|
||||
const classNames = await testSubjects.getAttribute('timeseriesChart', 'class');
|
||||
expect(classNames.includes('tvbVisTimeSeries--reversed')).to.be(true);
|
||||
expect(await visualBuilder.checkTimeSeriesIsLight()).to.be(true);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
|
|
@ -71,6 +71,10 @@ export function VisualBuilderPageProvider({ getService, getPageObjects }: FtrPro
|
|||
}
|
||||
}
|
||||
|
||||
public async checkTimeSeriesIsLight() {
|
||||
return await find.existsByCssSelector('.tvbVisTimeSeriesLight');
|
||||
}
|
||||
|
||||
public async checkTimeSeriesLegendIsPresent() {
|
||||
const isPresent = await find.existsByCssSelector('.echLegend');
|
||||
if (!isPresent) {
|
||||
|
|
Loading…
Reference in a new issue