[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:
Wylie Conlon 2020-04-20 12:43:21 -04:00 committed by GitHub
parent 44a2c2f8f7
commit 7e3de56303
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 218 additions and 50 deletions

View file

@ -4,6 +4,6 @@
"kibanaVersion": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["data", "expressions", "visualizations"],
"requiredPlugins": ["charts", "data", "expressions", "visualizations"],
"optionalPlugins": ["usageCollection"]
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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++) {

View file

@ -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', () => {

View file

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

View file

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

View file

@ -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 () => {

View file

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