[Lens] Categorical color palettes (#75309)

This commit is contained in:
Joe Reuter 2020-11-04 11:27:52 +01:00 committed by GitHub
parent 7abb1e3033
commit fe3b0538ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
123 changed files with 2942 additions and 776 deletions

View file

@ -0,0 +1,33 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// Currently supported palettes. This list might be extended dynamically in a later release
export const paletteIds = [
'default',
'kibana_palette',
'custom',
'status',
'temperature',
'complimentary',
'negative',
'positive',
'cool',
'warm',
'gray',
];

View file

@ -18,3 +18,5 @@
*/
export const COLOR_MAPPING_SETTING = 'visualization:colorMapping';
export * from './palette';
export * from './constants';

View file

@ -0,0 +1,102 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import {
palette,
defaultCustomColors,
systemPalette,
PaletteOutput,
CustomPaletteState,
} from './palette';
import { functionWrapper } from 'src/plugins/expressions/common/expression_functions/specs/tests/utils';
describe('palette', () => {
const fn = functionWrapper(palette()) as (
context: null,
args?: { color?: string[]; gradient?: boolean; reverse?: boolean }
) => PaletteOutput<CustomPaletteState>;
it('results a palette', () => {
const result = fn(null);
expect(result).toHaveProperty('type', 'palette');
});
describe('args', () => {
describe('color', () => {
it('sets colors', () => {
const result = fn(null, { color: ['red', 'green', 'blue'] });
expect(result.params!.colors).toEqual(['red', 'green', 'blue']);
});
it('defaults to pault_tor_14 colors', () => {
const result = fn(null);
expect(result.params!.colors).toEqual(defaultCustomColors);
});
});
describe('gradient', () => {
it('sets gradient', () => {
let result = fn(null, { gradient: true });
expect(result.params).toHaveProperty('gradient', true);
result = fn(null, { gradient: false });
expect(result.params).toHaveProperty('gradient', false);
});
it('defaults to false', () => {
const result = fn(null);
expect(result.params).toHaveProperty('gradient', false);
});
});
describe('reverse', () => {
it('reverses order of the colors', () => {
const result = fn(null, { reverse: true });
expect(result.params!.colors).toEqual(defaultCustomColors.reverse());
});
it('keeps the original order of the colors', () => {
const result = fn(null, { reverse: false });
expect(result.params!.colors).toEqual(defaultCustomColors);
});
it(`defaults to 'false`, () => {
const result = fn(null);
expect(result.params!.colors).toEqual(defaultCustomColors);
});
});
});
});
describe('system_palette', () => {
const fn = functionWrapper(systemPalette()) as (
context: null,
args: { name: string; params?: unknown }
) => PaletteOutput<unknown>;
it('results a palette', () => {
const result = fn(null, { name: 'test' });
expect(result).toHaveProperty('type', 'palette');
});
it('returns the name', () => {
const result = fn(null, { name: 'test' });
expect(result).toHaveProperty('name', 'test');
});
});

View file

@ -0,0 +1,160 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { i18n } from '@kbn/i18n';
import { paletteIds } from './constants';
export interface CustomPaletteArguments {
color?: string[];
gradient: boolean;
reverse?: boolean;
}
export interface CustomPaletteState {
colors: string[];
gradient: boolean;
}
export interface SystemPaletteArguments {
name: string;
}
export interface PaletteOutput<T = unknown> {
type: 'palette';
name: string;
params?: T;
}
export const defaultCustomColors = [
// This set of defaults originated in Canvas, which, at present, is the primary
// consumer of this function. Changing this default requires a change in Canvas
// logic, which would likely be a breaking change in 7.x.
'#882E72',
'#B178A6',
'#D6C1DE',
'#1965B0',
'#5289C7',
'#7BAFDE',
'#4EB265',
'#90C987',
'#CAE0AB',
'#F7EE55',
'#F6C141',
'#F1932D',
'#E8601C',
'#DC050C',
];
export function palette(): ExpressionFunctionDefinition<
'palette',
null,
CustomPaletteArguments,
PaletteOutput<CustomPaletteState>
> {
return {
name: 'palette',
aliases: [],
type: 'palette',
inputTypes: ['null'],
help: i18n.translate('charts.functions.paletteHelpText', {
defaultMessage: 'Creates a color palette.',
}),
args: {
color: {
aliases: ['_'],
multi: true,
types: ['string'],
help: i18n.translate('charts.functions.palette.args.colorHelpText', {
defaultMessage:
'The palette colors. Accepts an {html} color name, {hex}, {hsl}, {hsla}, {rgb}, or {rgba}.',
values: {
html: 'HTML',
rgb: 'RGB',
rgba: 'RGBA',
hex: 'HEX',
hsl: 'HSL',
hsla: 'HSLA',
},
}),
required: false,
},
gradient: {
types: ['boolean'],
default: false,
help: i18n.translate('charts.functions.palette.args.gradientHelpText', {
defaultMessage: 'Make a gradient palette where supported?',
}),
options: [true, false],
},
reverse: {
types: ['boolean'],
default: false,
help: i18n.translate('charts.functions.palette.args.reverseHelpText', {
defaultMessage: 'Reverse the palette?',
}),
options: [true, false],
},
},
fn: (input, args) => {
const { color, reverse, gradient } = args;
const colors = ([] as string[]).concat(color || defaultCustomColors);
return {
type: 'palette',
name: 'custom',
params: {
colors: reverse ? colors.reverse() : colors,
gradient,
},
};
},
};
}
export function systemPalette(): ExpressionFunctionDefinition<
'system_palette',
null,
SystemPaletteArguments,
PaletteOutput
> {
return {
name: 'system_palette',
aliases: [],
type: 'palette',
inputTypes: ['null'],
help: i18n.translate('charts.functions.systemPaletteHelpText', {
defaultMessage: 'Creates a dynamic color palette.',
}),
args: {
name: {
types: ['string'],
help: i18n.translate('charts.functions.systemPalette.args.nameHelpText', {
defaultMessage: 'Name of the palette in the palette list',
}),
options: paletteIds,
},
},
fn: (input, args) => {
return {
type: 'palette',
name: args.name,
};
},
};
}

View file

@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["expressions"],
"requiredBundles": ["visDefaultEditor"]
}

View file

@ -21,7 +21,14 @@ import { ChartsPlugin } from './plugin';
export const plugin = () => new ChartsPlugin();
export type ChartsPluginSetup = ReturnType<ChartsPlugin['setup']>;
export type ChartsPluginStart = ReturnType<ChartsPlugin['start']>;
export { ChartsPluginSetup, ChartsPluginStart } from './plugin';
export * from './static';
export * from './services/palettes/types';
export {
PaletteOutput,
CustomPaletteArguments,
CustomPaletteState,
SystemPaletteArguments,
paletteIds,
} from '../common';

View file

@ -19,19 +19,22 @@
import { ChartsPlugin } from './plugin';
import { themeServiceMock } from './services/theme/mock';
import { colorsServiceMock } from './services/colors/mock';
import { colorsServiceMock } from './services/legacy_colors/mock';
import { getPaletteRegistry, paletteServiceMock } from './services/palettes/mock';
export type Setup = jest.Mocked<ReturnType<ChartsPlugin['setup']>>;
export type Start = jest.Mocked<ReturnType<ChartsPlugin['start']>>;
const createSetupContract = (): Setup => ({
colors: colorsServiceMock,
legacyColors: colorsServiceMock,
theme: themeServiceMock,
palettes: paletteServiceMock.setup({} as any, {} as any),
});
const createStartContract = (): Start => ({
colors: colorsServiceMock,
legacyColors: colorsServiceMock,
theme: themeServiceMock,
palettes: paletteServiceMock.setup({} as any, {} as any),
});
export { colorMapsMock } from './static/color_maps/mock';
@ -39,4 +42,5 @@ export { colorMapsMock } from './static/color_maps/mock';
export const chartPluginMock = {
createSetupContract,
createStartContract,
createPaletteRegistry: getPaletteRegistry,
};

View file

@ -18,16 +18,24 @@
*/
import { Plugin, CoreSetup } from 'kibana/public';
import { ExpressionsSetup } from '../../expressions/public';
import { palette, systemPalette } from '../common';
import { ThemeService, ColorsService } from './services';
import { ThemeService, LegacyColorsService } from './services';
import { PaletteService } from './services/palettes/service';
export type Theme = Omit<ThemeService, 'init'>;
export type Color = Omit<ColorsService, 'init'>;
export type Color = Omit<LegacyColorsService, 'init'>;
interface SetupDependencies {
expressions: ExpressionsSetup;
}
/** @public */
export interface ChartsPluginSetup {
colors: Color;
legacyColors: Color;
theme: Theme;
palettes: ReturnType<PaletteService['setup']>;
}
/** @public */
@ -36,22 +44,30 @@ export type ChartsPluginStart = ChartsPluginSetup;
/** @public */
export class ChartsPlugin implements Plugin<ChartsPluginSetup, ChartsPluginStart> {
private readonly themeService = new ThemeService();
private readonly colorsService = new ColorsService();
private readonly legacyColorsService = new LegacyColorsService();
private readonly paletteService = new PaletteService();
public setup({ uiSettings }: CoreSetup): ChartsPluginSetup {
this.themeService.init(uiSettings);
this.colorsService.init(uiSettings);
private palettes: undefined | ReturnType<PaletteService['setup']>;
public setup(core: CoreSetup, dependencies: SetupDependencies): ChartsPluginSetup {
dependencies.expressions.registerFunction(palette);
dependencies.expressions.registerFunction(systemPalette);
this.themeService.init(core.uiSettings);
this.legacyColorsService.init(core.uiSettings);
this.palettes = this.paletteService.setup(core, this.legacyColorsService);
return {
colors: this.colorsService,
legacyColors: this.legacyColorsService,
theme: this.themeService,
palettes: this.palettes,
};
}
public start(): ChartsPluginStart {
return {
colors: this.colorsService,
legacyColors: this.legacyColorsService,
theme: this.themeService,
palettes: this.palettes!,
};
}
}

View file

@ -17,5 +17,5 @@
* under the License.
*/
export { ColorsService } from './colors';
export { LegacyColorsService } from './legacy_colors';
export { ThemeService } from './theme';

View file

@ -19,14 +19,14 @@
import { coreMock } from '../../../../../core/public/mocks';
import { COLOR_MAPPING_SETTING } from '../../../common';
import { seedColors } from './seed_colors';
import { ColorsService } from './colors';
import { seedColors } from '../../static/colors';
import { LegacyColorsService } from './colors';
// Local state for config
const config = new Map<string, any>();
describe('Vislib Color Service', () => {
const colors = new ColorsService();
const colors = new LegacyColorsService();
const mockUiSettings = coreMock.createSetup().uiSettings;
mockUiSettings.get.mockImplementation((a) => config.get(a));
mockUiSettings.set.mockImplementation((...a) => config.set(...a) as any);
@ -55,7 +55,7 @@ describe('Vislib Color Service', () => {
});
it('should throw error if not initialized', () => {
const colorsBad = new ColorsService();
const colorsBad = new LegacyColorsService();
expect(() => colorsBad.createColorLookupFunction(arr, {})).toThrowError();
});

View file

@ -21,8 +21,8 @@ import _ from 'lodash';
import { CoreSetup } from 'kibana/public';
import { MappedColors } from './mapped_colors';
import { seedColors } from './seed_colors';
import { MappedColors } from '../mapped_colors';
import { seedColors } from '../../static/colors';
/**
* Accepts an array of strings or numbers that are used to create a
@ -30,7 +30,7 @@ import { seedColors } from './seed_colors';
* Returns a function that accepts a value (i.e. a string or number)
* and returns a hex color associated with that value.
*/
export class ColorsService {
export class LegacyColorsService {
private _mappedColors?: MappedColors;
public readonly seedColors = seedColors;

View file

@ -17,8 +17,8 @@
* under the License.
*/
import { seedColors } from './seed_colors';
import { createColorPalette } from './color_palette';
import { seedColors } from '../../static/colors';
import { createColorPalette } from '../../static/colors';
describe('Color Palette', () => {
const num1 = 45;

View file

@ -0,0 +1,20 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { LegacyColorsService } from './colors';

View file

@ -17,12 +17,16 @@
* under the License.
*/
import { ColorsService } from './colors';
import { LegacyColorsService } from './colors';
import { coreMock } from '../../../../../core/public/mocks';
const colors = new ColorsService();
const colors = new LegacyColorsService();
colors.init(coreMock.createSetup().uiSettings);
export const colorsServiceMock: ColorsService = {
export const colorsServiceMock: LegacyColorsService = {
createColorLookupFunction: jest.fn(colors.createColorLookupFunction.bind(colors)),
mappedColors: {
mapKeys: jest.fn(),
get: jest.fn(),
},
} as any;

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { ColorsService } from './colors';
export * from './mapped_colors';

View file

@ -22,7 +22,7 @@ import Color from 'color';
import { coreMock } from '../../../../../core/public/mocks';
import { COLOR_MAPPING_SETTING } from '../../../common';
import { seedColors } from './seed_colors';
import { seedColors } from '../../static/colors';
import { MappedColors } from './mapped_colors';
// Local state for config

View file

@ -23,7 +23,7 @@ import Color from 'color';
import { CoreSetup } from 'kibana/public';
import { COLOR_MAPPING_SETTING } from '../../../common';
import { createColorPalette } from './color_palette';
import { createColorPalette } from '../../static/colors';
const standardizeColor = (color: string) => new Color(color).hex().toLowerCase();
@ -36,7 +36,10 @@ export class MappedColors {
private _oldMap: any;
private _mapping: any;
constructor(private uiSettings: CoreSetup['uiSettings']) {
constructor(
private uiSettings: CoreSetup['uiSettings'],
private colorPaletteFn: (num: number) => string[] = createColorPalette
) {
this._oldMap = {};
this._mapping = {};
}
@ -57,6 +60,10 @@ export class MappedColors {
return this.getConfigColorMapping()[key as any] || this._mapping[key];
}
getColorFromConfig(key: string | number) {
return this.getConfigColorMapping()[key as any];
}
flush() {
this._oldMap = _.clone(this._mapping);
this._mapping = {};
@ -89,7 +96,7 @@ export class MappedColors {
// Generate a color palette big enough that all new keys can have unique color values
const allColors = _(this._mapping).values().union(configColors).union(oldColors).value();
const colorPalette = createColorPalette(allColors.length + keysToMap.length);
const colorPalette = this.colorPaletteFn(allColors.length + keysToMap.length);
let newColors = _.difference(colorPalette, allColors);
while (keysToMap.length > newColors.length) {

View file

@ -0,0 +1,44 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import color from 'color';
import { lightenColor } from './lighten_color';
describe('lighten_color', () => {
it('should keep existing color if there is a single color step', () => {
expect(lightenColor('#FF0000', 1, 1)).toEqual('#FF0000');
});
it('should keep existing color for the first step', () => {
expect(lightenColor('#FF0000', 1, 10)).toEqual('#FF0000');
});
it('should lighten color', () => {
const baseLightness = color('#FF0000', 'hsl').lightness();
const result1 = lightenColor('#FF0000', 5, 10);
const result2 = lightenColor('#FF0000', 10, 10);
expect(baseLightness).toBeLessThan(color(result1, 'hsl').lightness());
expect(color(result1, 'hsl').lightness()).toBeLessThan(color(result2, 'hsl').lightness());
});
it('should not exceed top lightness', () => {
const result = lightenColor('#c0c0c0', 10, 10);
expect(color(result, 'hsl').lightness()).toBeLessThan(95);
});
});

View file

@ -0,0 +1,37 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import color from 'color';
const MAX_LIGHTNESS = 93;
const MAX_LIGHTNESS_SPACE = 20;
export function lightenColor(baseColor: string, step: number, totalSteps: number) {
if (totalSteps === 1) {
return baseColor;
}
const hslColor = color(baseColor, 'hsl');
const outputColorLightness = hslColor.lightness();
const lightnessSpace = Math.min(MAX_LIGHTNESS - outputColorLightness, MAX_LIGHTNESS_SPACE);
const currentLevelTargetLightness =
outputColorLightness + lightnessSpace * ((step - 1) / (totalSteps - 1));
const lightenedColor = hslColor.lightness(currentLevelTargetLightness);
return lightenedColor.hex();
}

View file

@ -0,0 +1,58 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PublicMethodsOf } from '@kbn/utility-types';
import { PaletteService } from './service';
import { PaletteDefinition, SeriesLayer } from './types';
export const getPaletteRegistry = () => {
const mockPalette: jest.Mocked<PaletteDefinition> = {
id: 'default',
title: 'My Palette',
getColor: jest.fn((_: SeriesLayer[]) => 'black'),
getColors: jest.fn((num: number) => ['red', 'black']),
toExpression: jest.fn(() => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: ['default'],
},
},
],
})),
};
return {
get: (_: string) => mockPalette,
getAll: () => [mockPalette],
};
};
export const paletteServiceMock: PublicMethodsOf<PaletteService> = {
setup() {
return {
getPalettes: async () => {
return getPaletteRegistry();
},
};
},
};

View file

@ -0,0 +1,261 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { coreMock } from '../../../../../core/public/mocks';
import { PaletteDefinition } from './types';
import { buildPalettes } from './palettes';
import { colorsServiceMock } from '../legacy_colors/mock';
describe('palettes', () => {
const palettes: Record<string, PaletteDefinition> = buildPalettes(
coreMock.createStart().uiSettings,
colorsServiceMock
);
describe('default palette', () => {
it('should return different colors based on behind text flag', () => {
const palette = palettes.default;
const color1 = palette.getColor([
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
]);
const color2 = palette.getColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
],
{
behindText: true,
}
);
expect(color1).not.toEqual(color2);
});
it('should return different colors based on rank at current series', () => {
const palette = palettes.default;
const color1 = palette.getColor([
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
]);
const color2 = palette.getColor([
{
name: 'abc',
rankAtDepth: 1,
totalSeriesAtDepth: 5,
},
]);
expect(color1).not.toEqual(color2);
});
it('should return the same color for different positions on outer series layers', () => {
const palette = palettes.default;
const color1 = palette.getColor([
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
{
name: 'def',
rankAtDepth: 0,
totalSeriesAtDepth: 2,
},
]);
const color2 = palette.getColor([
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
{
name: 'ghj',
rankAtDepth: 1,
totalSeriesAtDepth: 1,
},
]);
expect(color1).toEqual(color2);
});
});
describe('gradient palette', () => {
const palette = palettes.warm;
it('should use the whole gradient', () => {
const wholePalette = palette.getColors(10);
const color1 = palette.getColor([
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
]);
const color2 = palette.getColor([
{
name: 'def',
rankAtDepth: 9,
totalSeriesAtDepth: 10,
},
]);
expect(color1).toEqual(wholePalette[0]);
expect(color2).toEqual(wholePalette[9]);
});
});
describe('legacy palette', () => {
const palette = palettes.kibana_palette;
beforeEach(() => {
(colorsServiceMock.mappedColors.mapKeys as jest.Mock).mockClear();
(colorsServiceMock.mappedColors.get as jest.Mock).mockClear();
});
it('should query legacy color service', () => {
palette.getColor([
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
]);
expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']);
expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc');
});
it('should always use root series', () => {
palette.getColor([
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
{
name: 'def',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
]);
expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledTimes(1);
expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']);
expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledTimes(1);
expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc');
});
});
describe('custom palette', () => {
const palette = palettes.custom;
it('should return different colors based on rank at current series', () => {
const color1 = palette.getColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
],
{},
{
colors: ['#00ff00', '#000000'],
}
);
const color2 = palette.getColor(
[
{
name: 'abc',
rankAtDepth: 1,
totalSeriesAtDepth: 5,
},
],
{},
{
colors: ['#00ff00', '#000000'],
}
);
expect(color1).not.toEqual(color2);
});
it('should return the same color for different positions on outer series layers', () => {
const color1 = palette.getColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
{
name: 'def',
rankAtDepth: 0,
totalSeriesAtDepth: 2,
},
],
{},
{
colors: ['#00ff00', '#000000'],
}
);
const color2 = palette.getColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
{
name: 'ghj',
rankAtDepth: 1,
totalSeriesAtDepth: 1,
},
],
{},
{
colors: ['#00ff00', '#000000'],
}
);
expect(color1).toEqual(color2);
});
it('should use passed in colors', () => {
const color = palette.getColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
],
{},
{
colors: ['#00ff00', '#000000'],
gradient: true,
}
);
expect(color).toEqual('#00ff00');
});
});
});

View file

@ -0,0 +1,240 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// @ts-ignore
import chroma from 'chroma-js';
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from 'src/core/public';
import {
euiPaletteColorBlind,
euiPaletteCool,
euiPaletteGray,
euiPaletteNegative,
euiPalettePositive,
euiPaletteWarm,
euiPaletteColorBlindBehindText,
euiPaletteForStatus,
euiPaletteForTemperature,
euiPaletteComplimentary,
} from '@elastic/eui';
import { ChartsPluginSetup } from '../../../../../../src/plugins/charts/public';
import { lightenColor } from './lighten_color';
import { ChartColorConfiguration, PaletteDefinition, SeriesLayer } from './types';
import { LegacyColorsService } from '../legacy_colors';
function buildRoundRobinCategoricalWithMappedColors(): Omit<PaletteDefinition, 'title'> {
const colors = euiPaletteColorBlind({ rotations: 2 });
const behindTextColors = euiPaletteColorBlindBehindText({ rotations: 2 });
function getColor(
series: SeriesLayer[],
chartConfiguration: ChartColorConfiguration = { behindText: false }
) {
const outputColor = chartConfiguration.behindText
? behindTextColors[series[0].rankAtDepth % behindTextColors.length]
: colors[series[0].rankAtDepth % colors.length];
if (!chartConfiguration.maxDepth || chartConfiguration.maxDepth === 1) {
return outputColor;
}
return lightenColor(outputColor, series.length, chartConfiguration.maxDepth);
}
return {
id: 'default',
getColor,
getColors: () => euiPaletteColorBlind(),
toExpression: () => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: ['default'],
},
},
],
}),
};
}
function buildGradient(
id: string,
colors: (n: number) => string[]
): Omit<PaletteDefinition, 'title'> {
function getColor(
series: SeriesLayer[],
chartConfiguration: ChartColorConfiguration = { behindText: false }
) {
const totalSeriesAtDepth = series[0].totalSeriesAtDepth;
const rankAtDepth = series[0].rankAtDepth;
const actualColors = colors(totalSeriesAtDepth);
const outputColor = actualColors[rankAtDepth];
if (!chartConfiguration.maxDepth || chartConfiguration.maxDepth === 1) {
return outputColor;
}
return lightenColor(outputColor, series.length, chartConfiguration.maxDepth);
}
return {
id,
getColor,
getColors: colors,
toExpression: () => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: [id],
},
},
],
}),
};
}
function buildSyncedKibanaPalette(
colors: ChartsPluginSetup['legacyColors']
): Omit<PaletteDefinition, 'title'> {
function getColor(series: SeriesLayer[], chartConfiguration: ChartColorConfiguration = {}) {
colors.mappedColors.mapKeys([series[0].name]);
const outputColor = colors.mappedColors.get(series[0].name);
if (!chartConfiguration.maxDepth || chartConfiguration.maxDepth === 1) {
return outputColor;
}
return lightenColor(outputColor, series.length, chartConfiguration.maxDepth);
}
return {
id: 'kibana_palette',
getColor,
getColors: () => colors.seedColors.slice(0, 10),
toExpression: () => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: ['kibana_palette'],
},
},
],
}),
};
}
function buildCustomPalette(): PaletteDefinition {
return {
id: 'custom',
getColor: (
series: SeriesLayer[],
chartConfiguration: ChartColorConfiguration = { behindText: false },
{ colors, gradient }: { colors: string[]; gradient: boolean }
) => {
const actualColors = gradient
? chroma.scale(colors).colors(series[0].totalSeriesAtDepth)
: colors;
const outputColor = actualColors[series[0].rankAtDepth % actualColors.length];
if (!chartConfiguration.maxDepth || chartConfiguration.maxDepth === 1) {
return outputColor;
}
return lightenColor(outputColor, series.length, chartConfiguration.maxDepth);
},
internal: true,
title: i18n.translate('charts.palettes.customLabel', { defaultMessage: 'Custom' }),
getColors: (size: number, { colors, gradient }: { colors: string[]; gradient: boolean }) => {
return gradient ? chroma.scale(colors).colors(size) : colors;
},
toExpression: ({ colors, gradient }: { colors: string[]; gradient: boolean }) => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'palette',
arguments: {
color: colors,
gradient: [gradient],
},
},
],
}),
} as PaletteDefinition<unknown>;
}
export const buildPalettes: (
uiSettings: IUiSettingsClient,
legacyColorsService: LegacyColorsService
) => Record<string, PaletteDefinition> = (uiSettings, legacyColorsService) => {
return {
default: {
title: i18n.translate('charts.palettes.defaultPaletteLabel', {
defaultMessage: 'Default',
}),
...buildRoundRobinCategoricalWithMappedColors(),
},
status: {
title: i18n.translate('charts.palettes.statusLabel', { defaultMessage: 'Status' }),
...buildGradient('status', euiPaletteForStatus),
},
temperature: {
title: i18n.translate('charts.palettes.temperatureLabel', { defaultMessage: 'Temperature' }),
...buildGradient('temperature', euiPaletteForTemperature),
},
complimentary: {
title: i18n.translate('charts.palettes.complimentaryLabel', {
defaultMessage: 'Complimentary',
}),
...buildGradient('complimentary', euiPaletteComplimentary),
},
negative: {
title: i18n.translate('charts.palettes.negativeLabel', { defaultMessage: 'Negative' }),
...buildGradient('negative', euiPaletteNegative),
},
positive: {
title: i18n.translate('charts.palettes.positiveLabel', { defaultMessage: 'Positive' }),
...buildGradient('positive', euiPalettePositive),
},
cool: {
title: i18n.translate('charts.palettes.coolLabel', { defaultMessage: 'Cool' }),
...buildGradient('cool', euiPaletteCool),
},
warm: {
title: i18n.translate('charts.palettes.warmLabel', { defaultMessage: 'Warm' }),
...buildGradient('warm', euiPaletteWarm),
},
gray: {
title: i18n.translate('charts.palettes.grayLabel', { defaultMessage: 'Gray' }),
...buildGradient('gray', euiPaletteGray),
},
kibana_palette: {
title: i18n.translate('charts.palettes.kibanaPaletteLabel', {
defaultMessage: 'Compatibility',
}),
...buildSyncedKibanaPalette(legacyColorsService),
},
custom: buildCustomPalette() as PaletteDefinition<unknown>,
};
};

View file

@ -0,0 +1,56 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { CoreSetup } from 'kibana/public';
import { ExpressionsSetup } from '../../../../../../src/plugins/expressions/public';
import {
ChartsPluginSetup,
PaletteDefinition,
PaletteRegistry,
} from '../../../../../../src/plugins/charts/public';
import { LegacyColorsService } from '../legacy_colors';
export interface PaletteSetupPlugins {
expressions: ExpressionsSetup;
charts: ChartsPluginSetup;
}
export class PaletteService {
private palettes: Record<string, PaletteDefinition<unknown>> | undefined = undefined;
constructor() {}
public setup(core: CoreSetup, colorsService: LegacyColorsService) {
return {
getPalettes: async (): Promise<PaletteRegistry> => {
if (!this.palettes) {
const { buildPalettes } = await import('./palettes');
this.palettes = buildPalettes(core.uiSettings, colorsService);
}
return {
get: (name: string) => {
return this.palettes![name];
},
getAll: () => {
return Object.values(this.palettes!);
},
};
},
};
}
}

View file

@ -0,0 +1,118 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Ast } from '@kbn/interpreter/common';
/**
* Information about a series in a chart used to determine its color.
* Series layers can be nested, this means each series layer can have an ancestor.
*/
export interface SeriesLayer {
/**
* Name of the series (can be used for lookup-based coloring)
*/
name: string;
/**
* Rank of the series compared to siblings with the same ancestor
*/
rankAtDepth: number;
/**
* Total number of series with the same ancestor
*/
totalSeriesAtDepth: number;
}
/**
* Information about the structure of a chart to determine the color of a series within it.
*/
export interface ChartColorConfiguration {
/**
* Overall number of series in the current chart
*/
totalSeries?: number;
/**
* Max nesting depth of the series tree
*/
maxDepth?: number;
/**
* Flag whether the color will be used behind text. The palette can use this information to
* adjust colors for better a11y. Might be ignored depending on the palette.
*/
behindText?: boolean;
}
/**
* Definition of a global palette.
*
* A palette controls the appearance of Lens charts on an editor level.
* The palette wont get reset when switching charts.
*
* A palette can hold internal state (e.g. for customizations) and also includes
* an editor component to edit the internal state.
*/
export interface PaletteDefinition<T = unknown> {
/**
* Unique id of the palette (this will be persisted along with the visualization state)
*/
id: string;
/**
* User facing title (should be i18n-ized)
*/
title: string;
/**
* Flag indicating whether users should be able to pick this palette manually.
*/
internal?: boolean;
/**
* Serialize the internal state of the palette into an expression function.
* This function should be used to pass the palette to the expression function applying color and other styles
* @param state The internal state of the palette
*/
toExpression: (state?: T) => Ast;
/**
* Renders the UI for editing the internal state of the palette.
* Not each palette has to feature an internal state, so this is an optional property.
* @param domElement The dom element to the render the editor UI into
* @param props Current state and state setter to issue updates
*/
renderEditor?: (
domElement: Element,
props: { state?: T; setState: (updater: (oldState: T) => T) => void }
) => void;
/**
* Color a series according to the internal rules of the palette.
* @param series The current series along with its ancestors.
* @param state The internal state of the palette
*/
getColor: (
series: SeriesLayer[],
chartConfiguration?: ChartColorConfiguration,
state?: T
) => string | null;
/**
* Get a spectrum of colors of the current palette.
* This can be used if the chart wants to control color assignment locally.
*/
getColors: (size: number, state?: T) => string[];
}
export interface PaletteRegistry {
get: (name: string) => PaletteDefinition<unknown>;
getAll: () => Array<PaletteDefinition<unknown>>;
}

View file

@ -0,0 +1,21 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export * from './color_palette';
export * from './seed_colors';

View file

@ -18,4 +18,5 @@
*/
export * from './color_maps';
export * from './colors';
export * from './components';

View file

@ -18,5 +18,12 @@
*/
import { ChartsServerPlugin } from './plugin';
export {
PaletteOutput,
CustomPaletteArguments,
CustomPaletteState,
SystemPaletteArguments,
paletteIds,
} from '../common';
export const plugin = () => new ChartsServerPlugin();

View file

@ -20,10 +20,17 @@
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { CoreSetup, Plugin } from 'kibana/server';
import { COLOR_MAPPING_SETTING } from '../common';
import { COLOR_MAPPING_SETTING, palette, systemPalette } from '../common';
import { ExpressionsServerSetup } from '../../expressions/server';
interface SetupDependencies {
expressions: ExpressionsServerSetup;
}
export class ChartsServerPlugin implements Plugin<object, object> {
public setup(core: CoreSetup) {
public setup(core: CoreSetup, dependencies: SetupDependencies) {
dependencies.expressions.registerFunction(palette);
dependencies.expressions.registerFunction(systemPalette);
core.uiSettings.register({
[COLOR_MAPPING_SETTING]: {
name: i18n.translate('charts.advancedSettings.visualization.colorMappingTitle', {

View file

@ -38,7 +38,7 @@ export interface TagCloudPluginSetupDependencies {
/** @internal */
export interface TagCloudVisDependencies {
colors: ChartsPluginSetup['colors'];
colors: ChartsPluginSetup['legacyColors'];
}
/** @internal */
@ -59,7 +59,7 @@ export class TagCloudPlugin implements Plugin<void, void> {
{ expressions, visualizations, charts }: TagCloudPluginSetupDependencies
) {
const visualizationDependencies: TagCloudVisDependencies = {
colors: charts.colors,
colors: charts.legacyColors,
};
expressions.registerFunction(createTagCloudFn);
expressions.registerRenderer(getTagCloudVisRenderer(visualizationDependencies));

View file

@ -97,7 +97,7 @@ 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 { colors, theme: themeService } = getChartsSetup();
const { legacyColors: colors, theme: themeService } = getChartsSetup();
const baseTheme = getBaseTheme(themeService.useChartsBaseTheme(), backgroundColor);
colors.mappedColors.mapKeys(series.filter(({ color }) => !color).map(({ label }) => label));

View file

@ -57,7 +57,7 @@ export class Vis extends EventEmitter {
this.data,
this.uiState,
this.element,
this.charts.colors.createColorLookupFunction.bind(this.charts.colors)
this.charts.legacyColors.createColorLookupFunction.bind(this.charts.legacyColors)
);
}

View file

@ -6,5 +6,8 @@
import { functions as browserFns } from '../../canvas_plugin_src/functions/browser';
import { ExpressionFunction } from '../../../../../src/plugins/expressions';
import { initFunctions } from '../../public/functions';
export const functionSpecs = browserFns.map((fn) => new ExpressionFunction(fn()));
export const functionSpecs = browserFns
.concat(...(initFunctions({} as any) as any))
.map((fn) => new ExpressionFunction(fn()));

View file

@ -6,6 +6,7 @@
export const testPlot = {
type: 'pointseries',
palette: { type: 'palette', name: 'custom' },
columns: {
x: { type: 'date', role: 'dimension', expression: 'time' },
y: {
@ -77,6 +78,7 @@ export const testPlot = {
export const testPie = {
type: 'pointseries',
palette: { type: 'palette', name: 'custom' },
columns: {
color: {
type: 'string',

View file

@ -60,14 +60,20 @@ export const seriesStyle = {
export const grayscalePalette = {
type: 'palette',
colors: ['#FFFFFF', '#888888', '#000000'],
gradient: false,
name: 'custom',
params: {
colors: ['#FFFFFF', '#888888', '#000000'],
gradient: false,
},
};
export const gradientPalette = {
type: 'palette',
colors: ['#FFFFFF', '#000000'],
gradient: true,
name: 'custom',
params: {
colors: ['#FFFFFF', '#000000'],
gradient: true,
},
};
export const xAxisConfig = {

View file

@ -37,9 +37,6 @@ import { mapColumn } from './mapColumn';
import { math } from './math';
import { metric } from './metric';
import { neq } from './neq';
import { palette } from './palette';
import { pie } from './pie';
import { plot } from './plot';
import { ply } from './ply';
import { progress } from './progress';
import { render } from './render';
@ -95,9 +92,6 @@ export const functions = [
math,
metric,
neq,
palette,
pie,
plot,
ply,
progress,
render,

View file

@ -1,64 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { functionWrapper } from '../../../__tests__/helpers/function_wrapper';
import { paulTor14 } from '../../../common/lib/palettes';
import { palette } from './palette';
describe('palette', () => {
const fn = functionWrapper(palette);
it('results a palette', () => {
const result = fn(null);
expect(result).toHaveProperty('type', 'palette');
});
describe('args', () => {
describe('color', () => {
it('sets colors', () => {
const result = fn(null, { color: ['red', 'green', 'blue'] });
expect(result.colors).toEqual(['red', 'green', 'blue']);
});
it('defaults to pault_tor_14 colors', () => {
const result = fn(null);
expect(result.colors).toEqual(paulTor14.colors);
});
});
describe('gradient', () => {
it('sets gradient', () => {
let result = fn(null, { gradient: true });
expect(result).toHaveProperty('gradient', true);
result = fn(null, { gradient: false });
expect(result).toHaveProperty('gradient', false);
});
it('defaults to false', () => {
const result = fn(null);
expect(result).toHaveProperty('gradient', false);
});
});
describe('reverse', () => {
it('reverses order of the colors', () => {
const result = fn(null, { reverse: true });
expect(result.colors).toEqual(paulTor14.colors.reverse());
});
it('keeps the original order of the colors', () => {
const result = fn(null, { reverse: false });
expect(result.colors).toEqual(paulTor14.colors);
});
it(`defaults to 'false`, () => {
const result = fn(null);
expect(result.colors).toEqual(paulTor14.colors);
});
});
});
});

View file

@ -1,63 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { paulTor14 } from '../../../common/lib/palettes';
import { getFunctionHelp } from '../../../i18n';
interface Arguments {
color: string[];
gradient: boolean;
reverse: boolean;
}
interface Output {
type: 'palette';
colors: string[];
gradient: boolean;
}
export function palette(): ExpressionFunctionDefinition<'palette', null, Arguments, Output> {
const { help, args: argHelp } = getFunctionHelp().palette;
return {
name: 'palette',
aliases: [],
type: 'palette',
inputTypes: ['null'],
help,
args: {
color: {
aliases: ['_'],
multi: true,
types: ['string'],
help: argHelp.color,
},
gradient: {
types: ['boolean'],
default: false,
help: argHelp.gradient,
options: [true, false],
},
reverse: {
types: ['boolean'],
default: false,
help: argHelp.reverse,
options: [true, false],
},
},
fn: (input, args) => {
const { color, reverse, gradient } = args;
const colors = ([] as string[]).concat(color || paulTor14.colors);
return {
type: 'palette',
colors: reverse ? colors.reverse() : colors,
gradient,
};
},
};
}

View file

@ -1,192 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { get, keyBy, map, groupBy } from 'lodash';
// @ts-expect-error untyped local
import { getColorsFromPalette } from '../../../common/lib/get_colors_from_palette';
// @ts-expect-error untyped local
import { getLegendConfig } from '../../../common/lib/get_legend_config';
import { getFunctionHelp } from '../../../i18n';
import {
Legend,
Palette,
PointSeries,
Render,
SeriesStyle,
Style,
ExpressionFunctionDefinition,
} from '../../../types';
interface PieSeriesOptions {
show: boolean;
innerRadius: number;
stroke: {
width: number;
};
label: {
show: boolean;
radius: number;
};
tilt: number;
radius: number | 'auto';
}
interface PieOptions {
canvas: boolean;
colors: string[];
legend: {
show: boolean;
backgroundOpacity?: number;
labelBoxBorderColor?: string;
position?: Legend;
};
grid: {
show: boolean;
};
series: {
pie: PieSeriesOptions;
};
}
interface PieData {
label: string;
data: number[];
color?: string;
}
export interface Pie {
font: Style;
data: PieData[];
options: PieOptions;
}
interface Arguments {
palette: Palette;
seriesStyle: SeriesStyle[];
radius: number | 'auto';
hole: number;
labels: boolean;
labelRadius: number;
font: Style;
legend: Legend | false;
tilt: number;
}
export function pie(): ExpressionFunctionDefinition<'pie', PointSeries, Arguments, Render<Pie>> {
const { help, args: argHelp } = getFunctionHelp().pie;
return {
name: 'pie',
aliases: [],
type: 'render',
inputTypes: ['pointseries'],
help,
args: {
font: {
types: ['style'],
help: argHelp.font,
default: '{font}',
},
hole: {
types: ['number'],
default: 0,
help: argHelp.hole,
},
labelRadius: {
types: ['number'],
default: 100,
help: argHelp.labelRadius,
},
labels: {
types: ['boolean'],
default: true,
help: argHelp.labels,
},
legend: {
types: ['string', 'boolean'],
help: argHelp.legend,
default: false,
options: [...Object.values(Legend), false],
},
palette: {
types: ['palette'],
help: argHelp.palette,
default: '{palette}',
},
radius: {
types: ['string', 'number'],
help: argHelp.radius,
default: 'auto',
},
seriesStyle: {
multi: true,
types: ['seriesStyle'],
help: argHelp.seriesStyle,
},
tilt: {
types: ['number'],
default: 1,
help: argHelp.tilt,
},
},
fn: (input, args) => {
const { tilt, radius, labelRadius, labels, hole, legend, palette, font, seriesStyle } = args;
const seriesStyles = keyBy(seriesStyle || [], 'label') || {};
const data: PieData[] = map(groupBy(input.rows, 'color'), (series, label = '') => {
const item: PieData = {
label,
data: series.map((point) => point.size || 1),
};
const style = seriesStyles[label];
// append series style, if there is a match
if (style) {
item.color = get(style, 'color');
}
return item;
});
return {
type: 'render',
as: 'pie',
value: {
font,
data,
options: {
canvas: false,
colors: getColorsFromPalette(palette, data.length),
legend: getLegendConfig(legend, data.length),
grid: {
show: false,
},
series: {
pie: {
show: true,
innerRadius: Math.max(hole, 0) / 100,
stroke: {
width: 0,
},
label: {
show: labels,
radius: (labelRadius >= 0 ? labelRadius : 100) / 100,
},
tilt,
radius,
},
bubbles: {
show: false,
},
shadowSize: 0,
},
},
},
};
},
};
}

View file

@ -1,172 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { set } from '@elastic/safer-lodash-set';
import { groupBy, get, keyBy, map, sortBy } from 'lodash';
import { ExpressionFunctionDefinition, Style } from 'src/plugins/expressions';
// @ts-expect-error untyped local
import { getColorsFromPalette } from '../../../../common/lib/get_colors_from_palette';
// @ts-expect-error untyped local
import { getLegendConfig } from '../../../../common/lib/get_legend_config';
import { getFlotAxisConfig } from './get_flot_axis_config';
import { getFontSpec } from './get_font_spec';
import { seriesStyleToFlot } from './series_style_to_flot';
import { getTickHash } from './get_tick_hash';
import { getFunctionHelp } from '../../../../i18n';
import { AxisConfig, PointSeries, Render, SeriesStyle, Palette, Legend } from '../../../../types';
interface Arguments {
seriesStyle: SeriesStyle[];
defaultStyle: SeriesStyle;
palette: Palette;
font: Style;
legend: Legend | boolean;
xaxis: AxisConfig | boolean;
yaxis: AxisConfig | boolean;
}
export function plot(): ExpressionFunctionDefinition<'plot', PointSeries, Arguments, Render<any>> {
const { help, args: argHelp } = getFunctionHelp().plot;
return {
name: 'plot',
aliases: [],
type: 'render',
inputTypes: ['pointseries'],
help,
args: {
defaultStyle: {
multi: false,
types: ['seriesStyle'],
help: argHelp.defaultStyle,
default: '{seriesStyle points=5}',
},
font: {
types: ['style'],
help: argHelp.font,
default: '{font}',
},
legend: {
types: ['string', 'boolean'],
help: argHelp.legend,
default: 'ne',
options: [...Object.values(Legend), false],
},
palette: {
types: ['palette'],
help: argHelp.palette,
default: '{palette}',
},
seriesStyle: {
multi: true,
types: ['seriesStyle'],
help: argHelp.seriesStyle,
},
xaxis: {
types: ['boolean', 'axisConfig'],
help: argHelp.xaxis,
default: true,
},
yaxis: {
types: ['boolean', 'axisConfig'],
help: argHelp.yaxis,
default: true,
},
},
fn: (input, args) => {
const seriesStyles: { [key: string]: SeriesStyle } =
keyBy(args.seriesStyle || [], 'label') || {};
const sortedRows = sortBy(input.rows, ['x', 'y', 'color', 'size', 'text']);
const ticks = getTickHash(input.columns, sortedRows);
const font = args.font ? getFontSpec(args.font) : {};
const data = map(groupBy(sortedRows, 'color'), (series, label) => {
const seriesStyle = {
...args.defaultStyle,
...seriesStyles[label as string],
};
const flotStyle = seriesStyle ? seriesStyleToFlot(seriesStyle) : {};
return {
...flotStyle,
label,
data: series.map((point) => {
const attrs: {
size?: number;
text?: string;
} = {};
const x = get(input.columns, 'x.type') === 'string' ? ticks.x.hash[point.x] : point.x;
const y = get(input.columns, 'y.type') === 'string' ? ticks.y.hash[point.y] : point.y;
if (point.size != null) {
attrs.size = point.size;
} else if (get(seriesStyle, 'points')) {
attrs.size = seriesStyle.points;
set(flotStyle, 'bubbles.size.min', seriesStyle.points);
}
if (point.text != null) {
attrs.text = point.text;
}
return [x, y, attrs];
}),
};
});
const gridConfig = {
borderWidth: 0,
borderColor: null,
color: 'rgba(0,0,0,0)',
labelMargin: 30,
margin: {
right: 30,
top: 20,
bottom: 0,
left: 0,
},
};
const output = {
type: 'render',
as: 'plot',
value: {
font: args.font,
data: sortBy(data, 'label'),
options: {
canvas: false,
colors: getColorsFromPalette(args.palette, data.length),
legend: getLegendConfig(args.legend, data.length),
grid: gridConfig,
xaxis: getFlotAxisConfig('x', args.xaxis, {
columns: input.columns,
ticks,
font,
}),
yaxis: getFlotAxisConfig('y', args.yaxis, {
columns: input.columns,
ticks,
font,
}),
series: {
shadowSize: 0,
...seriesStyleToFlot(args.defaultStyle),
},
},
},
};
// fix the issue of plot sometimes re-rendering with an empty chart
// TODO: holy hell, why does this work?! the working theory is that some values become undefined
// and serializing the result here causes them to be dropped off, and this makes flot react differently.
// It's also possible that something else ends up mutating this object, but that seems less likely.
return JSON.parse(JSON.stringify(output));
},
};
}

View file

@ -5,6 +5,7 @@
*/
import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common';
import { PaletteOutput } from 'src/plugins/charts/common';
import { TimeRange, Filter as DataFilter } from 'src/plugins/data/public';
import { EmbeddableInput } from 'src/plugins/embeddable/public';
import { getQueryFilters } from '../../../public/lib/build_embeddable_filters';
@ -20,12 +21,14 @@ interface Arguments {
id: string;
title: string | null;
timerange: TimeRangeArg | null;
palette?: PaletteOutput;
}
export type SavedLensInput = EmbeddableInput & {
id: string;
timeRange?: TimeRange;
filters: DataFilter[];
palette?: PaletteOutput;
};
const defaultTimeRange = {
@ -61,6 +64,11 @@ export function savedLens(): ExpressionFunctionDefinition<
help: argHelp.title,
required: false,
},
palette: {
types: ['palette'],
help: argHelp.palette!,
required: false,
},
},
type: EmbeddableExpressionType,
fn: (input, args) => {
@ -74,6 +82,7 @@ export function savedLens(): ExpressionFunctionDefinition<
timeRange: args.timerange || defaultTimeRange,
title: args.title === null ? undefined : args.title,
disableTriggers: true,
palette: args.palette,
},
embeddableType: EmbeddableTypes.lens,
generatedAt: Date.now(),

View file

@ -5,6 +5,7 @@
*/
import { CoreSetup, CoreStart, Plugin } from 'src/core/public';
import { ChartsPluginStart } from 'src/plugins/charts/public';
import { CanvasSetup } from '../public';
import { EmbeddableStart } from '../../../../src/plugins/embeddable/public';
import { UiActionsStart } from '../../../../src/plugins/ui_actions/public';
@ -32,6 +33,7 @@ export interface StartDeps {
embeddable: EmbeddableStart;
uiActions: UiActionsStart;
inspector: InspectorStart;
charts: ChartsPluginStart;
}
export type SetupInitializer<T> = (core: CoreSetup<StartDeps>, plugins: SetupDeps) => T;

View file

@ -68,11 +68,17 @@ export const embeddableRendererFactory = (
const embeddableObject = await factory.createFromSavedObject(input.id, input);
const palettes = await plugins.charts.palettes.getPalettes();
embeddablesRegistry[uniqueId] = embeddableObject;
ReactDOM.unmountComponentAtNode(domNode);
const subscription = embeddableObject.getInput$().subscribe(function (updatedInput) {
const updatedExpression = embeddableInputToExpression(updatedInput, embeddableType);
const updatedExpression = embeddableInputToExpression(
updatedInput,
embeddableType,
palettes
);
if (updatedExpression) {
handlers.onEmbeddableInputChange(updatedExpression);

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
import {
embeddableInputToExpression,
inputToExpressionTypeMap,
@ -21,7 +22,11 @@ describe('input to expression', () => {
const mockReturn = 'expression';
inputToExpressionTypeMap[newType] = jest.fn().mockReturnValue(mockReturn);
const expression = embeddableInputToExpression(input, newType);
const expression = embeddableInputToExpression(
input,
newType,
chartPluginMock.createPaletteRegistry()
);
expect(expression).toBe(mockReturn);
});

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PaletteRegistry } from 'src/plugins/charts/public';
import { EmbeddableTypes, EmbeddableInput } from '../../expression_types';
import { toExpression as mapToExpression } from './input_type_to_expression/map';
import { toExpression as visualizationToExpression } from './input_type_to_expression/visualization';
@ -20,9 +21,10 @@ export const inputToExpressionTypeMap = {
*/
export function embeddableInputToExpression(
input: EmbeddableInput,
embeddableType: string
embeddableType: string,
palettes: PaletteRegistry
): string | undefined {
if (inputToExpressionTypeMap[embeddableType]) {
return inputToExpressionTypeMap[embeddableType](input as any);
return inputToExpressionTypeMap[embeddableType](input as any, palettes);
}
}

View file

@ -7,6 +7,7 @@
import { toExpression } from './lens';
import { SavedLensInput } from '../../../functions/external/saved_lens';
import { fromExpression, Ast } from '@kbn/interpreter/common';
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
const baseEmbeddableInput = {
id: 'embeddableId',
@ -19,7 +20,7 @@ describe('toExpression', () => {
...baseEmbeddableInput,
};
const expression = toExpression(input);
const expression = toExpression(input, chartPluginMock.createPaletteRegistry());
const ast = fromExpression(expression);
expect(ast.type).toBe('expression');
@ -41,7 +42,7 @@ describe('toExpression', () => {
},
};
const expression = toExpression(input);
const expression = toExpression(input, chartPluginMock.createPaletteRegistry());
const ast = fromExpression(expression);
expect(ast.chain[0].arguments).toHaveProperty('title', [input.title]);
@ -59,7 +60,7 @@ describe('toExpression', () => {
title: '',
};
const expression = toExpression(input);
const expression = toExpression(input, chartPluginMock.createPaletteRegistry());
const ast = fromExpression(expression);
expect(ast.chain[0].arguments).toHaveProperty('title', [input.title]);

View file

@ -4,9 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { toExpression as toExpressionString } from '@kbn/interpreter/common';
import { PaletteRegistry } from 'src/plugins/charts/public';
import { SavedLensInput } from '../../../functions/external/saved_lens';
export function toExpression(input: SavedLensInput): string {
export function toExpression(input: SavedLensInput, palettes: PaletteRegistry): string {
const expressionParts = [] as string[];
expressionParts.push('savedLens');
@ -23,5 +25,13 @@ export function toExpression(input: SavedLensInput): string {
);
}
if (input.palette) {
expressionParts.push(
`palette={${toExpressionString(
palettes.get(input.palette.name).toExpression(input.palette.params)
)}}`
);
}
return expressionParts.join(' ');
}

View file

@ -9,7 +9,7 @@ import 'jquery';
import { debounce } from 'lodash';
import { RendererStrings } from '../../../i18n';
import { Pie } from '../../functions/common/pie';
import { Pie } from '../../../public/functions/pie';
import { RendererFactory } from '../../../types';
const { pie: strings } = RendererStrings;

View file

@ -1,10 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import chroma from 'chroma-js';
export const getColorsFromPalette = (palette, size) =>
palette.gradient ? chroma.scale(palette.colors).colors(size) : palette.colors;

View file

@ -1,40 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import {
grayscalePalette,
gradientPalette,
} from '../../canvas_plugin_src/functions/common/__tests__/fixtures/test_styles';
import { getColorsFromPalette } from './get_colors_from_palette';
describe('getColorsFromPalette', () => {
it('returns the array of colors from a palette object when gradient is false', () => {
expect(getColorsFromPalette(grayscalePalette, 20)).toBe(grayscalePalette.colors);
});
it('returns an array of colors with equidistant colors with length equal to the number of series when gradient is true', () => {
const result = getColorsFromPalette(gradientPalette, 16);
expect(result).toEqual([
'#ffffff',
'#eeeeee',
'#dddddd',
'#cccccc',
'#bbbbbb',
'#aaaaaa',
'#999999',
'#888888',
'#777777',
'#666666',
'#555555',
'#444444',
'#333333',
'#222222',
'#111111',
'#000000',
]);
expect(result).toHaveLength(16);
});
});

View file

@ -15,8 +15,6 @@ export * from './errors';
export * from './expression_form_handlers';
export * from './fetch';
export * from './fonts';
// @ts-expect-error missing local definition
export * from './get_colors_from_palette';
export * from './get_field_type';
// @ts-expect-error missing local definition
export * from './get_legend_config';

View file

@ -1,36 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { i18n } from '@kbn/i18n';
import { palette } from '../../../canvas_plugin_src/functions/common/palette';
import { FunctionHelp } from '../function_help';
import { FunctionFactory } from '../../../types';
export const help: FunctionHelp<FunctionFactory<typeof palette>> = {
help: i18n.translate('xpack.canvas.functions.paletteHelpText', {
defaultMessage: 'Creates a color palette.',
}),
args: {
color: i18n.translate('xpack.canvas.functions.palette.args.colorHelpText', {
defaultMessage:
'The palette colors. Accepts an {html} color name, {hex}, {hsl}, {hsla}, {rgb}, or {rgba}.',
values: {
html: 'HTML',
rgb: 'RGB',
rgba: 'RGBA',
hex: 'HEX',
hsl: 'HSL',
hsla: 'HSLA',
},
}),
gradient: i18n.translate('xpack.canvas.functions.palette.args.gradientHelpText', {
defaultMessage: 'Make a gradient palette where supported?',
}),
reverse: i18n.translate('xpack.canvas.functions.palette.args.reverseHelpText', {
defaultMessage: 'Reverse the palette?',
}),
},
};

View file

@ -5,13 +5,12 @@
*/
import { i18n } from '@kbn/i18n';
import { pie } from '../../../canvas_plugin_src/functions/common/pie';
import { FunctionHelp } from '../function_help';
import { FunctionFactory } from '../../../types';
import { pieFunctionFactory } from '../../../public/functions/pie';
import { FunctionFactoryHelp } from '../function_help';
import { Legend } from '../../../types';
import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_FALSE } from '../../constants';
export const help: FunctionHelp<FunctionFactory<typeof pie>> = {
export const help: FunctionFactoryHelp<typeof pieFunctionFactory> = {
help: i18n.translate('xpack.canvas.functions.pieHelpText', {
defaultMessage: 'Configures a pie chart element.',
}),

View file

@ -5,13 +5,12 @@
*/
import { i18n } from '@kbn/i18n';
import { plot } from '../../../canvas_plugin_src/functions/common/plot';
import { FunctionHelp } from '../function_help';
import { FunctionFactory } from '../../../types';
import { plotFunctionFactory } from '../../../public/functions/plot';
import { FunctionFactoryHelp } from '../function_help';
import { Legend } from '../../../types';
import { CSS, FONT_FAMILY, FONT_WEIGHT, BOOLEAN_FALSE } from '../../constants';
export const help: FunctionHelp<FunctionFactory<typeof plot>> = {
export const help: FunctionFactoryHelp<typeof plotFunctionFactory> = {
help: i18n.translate('xpack.canvas.functions.plotHelpText', {
defaultMessage: 'Configures a chart element.',
}),

View file

@ -23,5 +23,8 @@ export const help: FunctionHelp<FunctionFactory<typeof savedLens>> = {
title: i18n.translate('xpack.canvas.functions.savedLens.args.titleHelpText', {
defaultMessage: `The title for the Lens visualization object`,
}),
palette: i18n.translate('xpack.canvas.functions.savedLens.args.paletteHelpText', {
defaultMessage: `The palette used for the Lens visualization`,
}),
},
};

View file

@ -6,7 +6,7 @@
import { ExpressionFunctionDefinition } from 'src/plugins/expressions';
import { UnionToIntersection } from '@kbn/utility-types';
import { CanvasFunction } from '../../types';
import { CanvasFunction, FunctionFactory } from '../../types';
import { help as all } from './dict/all';
import { help as alterColumn } from './dict/alter_column';
@ -50,7 +50,6 @@ import { help as markdown } from './dict/markdown';
import { help as math } from './dict/math';
import { help as metric } from './dict/metric';
import { help as neq } from './dict/neq';
import { help as palette } from './dict/palette';
import { help as pie } from './dict/pie';
import { help as plot } from './dict/plot';
import { help as ply } from './dict/ply';
@ -122,6 +121,15 @@ export type FunctionHelp<T> = T extends ExpressionFunctionDefinition<
}
: never;
/**
* Helper type to use `FunctionHelp` for function definitions wrapped into factory functions.
* It creates a strongly typed entry for the `FunctionHelpMap` for the function definition generated
* by the passed in factory: `type MyFnHelp = FunctionFactoryHelp<typeof myFnFactory>`
*/
export type FunctionFactoryHelp<T extends (...args: any) => any> = FunctionHelp<
FunctionFactory<ReturnType<T>>
>;
// This internal type infers a Function name and uses `FunctionHelp` above to build
// a dictionary entry. This can be used to ensure every Function is defined and all
// Arguments have help strings.
@ -205,7 +213,6 @@ export const getFunctionHelp = (): FunctionHelpDict => ({
math,
metric,
neq,
palette,
pie,
plot,
ply,

View file

@ -5,7 +5,7 @@
"configPath": ["xpack", "canvas"],
"server": true,
"ui": true,
"requiredPlugins": ["bfetch", "data", "embeddable", "expressions", "features", "inspector", "uiActions"],
"requiredPlugins": ["bfetch", "data", "embeddable", "expressions", "features", "inspector", "uiActions", "charts"],
"optionalPlugins": ["usageCollection", "home"],
"requiredBundles": ["kibanaReact", "maps", "lens", "visualizations", "kibanaUtils", "kibanaLegacy", "discover", "savedObjects", "reporting", "home"]
}

View file

@ -86,6 +86,7 @@ export const initializeCanvas = async (
timefilter: setupPlugins.data.query.timefilter.timefilter,
prependBasePath: coreSetup.http.basePath.prepend,
types: setupPlugins.expressions.getTypes(),
paletteService: await setupPlugins.charts.palettes.getPalettes(),
});
for (const fn of canvasFunctions) {

View file

@ -4,14 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PaletteRegistry } from 'src/plugins/charts/public';
import { asset } from './asset';
import { filtersFunctionFactory } from './filters';
import { timelionFunctionFactory } from './timelion';
import { toFunctionFactory } from './to';
import { CanvasSetupDeps, CoreSetup } from '../plugin';
import { plotFunctionFactory } from './plot';
import { pieFunctionFactory } from './pie';
export interface InitializeArguments {
prependBasePath: CoreSetup['http']['basePath']['prepend'];
paletteService: PaletteRegistry;
types: ReturnType<CanvasSetupDeps['expressions']['getTypes']>;
timefilter: CanvasSetupDeps['data']['query']['timefilter']['timefilter'];
}
@ -22,5 +26,7 @@ export function initFunctions(initialize: InitializeArguments) {
filtersFunctionFactory(initialize),
timelionFunctionFactory(initialize),
toFunctionFactory(initialize),
pieFunctionFactory(initialize.paletteService),
plotFunctionFactory(initialize.paletteService),
];
}

View file

@ -4,13 +4,23 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { functionWrapper } from '../../../__tests__/helpers/function_wrapper';
import { testPie } from './__tests__/fixtures/test_pointseries';
import { fontStyle, grayscalePalette, seriesStyle } from './__tests__/fixtures/test_styles';
import { pie } from './pie';
import { functionWrapper } from '../../__tests__/helpers/function_wrapper';
import { testPie } from '../../canvas_plugin_src/functions/common/__tests__/fixtures/test_pointseries';
import {
fontStyle,
grayscalePalette,
seriesStyle,
} from '../../canvas_plugin_src/functions/common/__tests__/fixtures/test_styles';
import { pieFunctionFactory } from './pie';
describe('pie', () => {
const fn = functionWrapper(pie);
const fn = functionWrapper(
pieFunctionFactory({
get: () => ({
getColors: () => ['red', 'black'],
}),
})
);
it('returns a render as pie', () => {
const result = fn(testPie);
@ -44,9 +54,18 @@ describe('pie', () => {
describe('args', () => {
describe('palette', () => {
it('sets the color palette', () => {
const result = fn(testPie, { palette: grayscalePalette }).value.options;
const mockedColors = jest.fn(() => ['#FFFFFF', '#888888', '#000000']);
const mockedFn = functionWrapper(
pieFunctionFactory({
get: () => ({
getColors: mockedColors,
}),
})
);
const result = mockedFn(testPie, { palette: grayscalePalette }).value.options;
expect(result).toHaveProperty('colors');
expect(result.colors).toEqual(grayscalePalette.colors);
expect(result.colors).toEqual(['#FFFFFF', '#888888', '#000000']);
expect(mockedColors).toHaveBeenCalledWith(5, grayscalePalette.params);
});
// TODO: write test when using an instance of the interpreter

View file

@ -0,0 +1,206 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { get, keyBy, map, groupBy } from 'lodash';
import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
// @ts-expect-error untyped local
import { getLegendConfig } from '../../common/lib/get_legend_config';
import { getFunctionHelp } from '../../i18n';
import {
Legend,
PointSeries,
Render,
SeriesStyle,
Style,
ExpressionFunctionDefinition,
} from '../../types';
interface PieSeriesOptions {
show: boolean;
innerRadius: number;
stroke: {
width: number;
};
label: {
show: boolean;
radius: number;
};
tilt: number;
radius: number | 'auto';
}
interface PieOptions {
canvas: boolean;
colors: string[];
legend: {
show: boolean;
backgroundOpacity?: number;
labelBoxBorderColor?: string;
position?: Legend;
};
grid: {
show: boolean;
};
series: {
pie: PieSeriesOptions;
};
}
interface PieData {
label: string;
data: number[];
color?: string;
}
export interface Pie {
font: Style;
data: PieData[];
options: PieOptions;
}
interface Arguments {
palette: PaletteOutput;
seriesStyle: SeriesStyle[];
radius: number | 'auto';
hole: number;
labels: boolean;
labelRadius: number;
font: Style;
legend: Legend | false;
tilt: number;
}
export function pieFunctionFactory(
paletteService: PaletteRegistry
): () => ExpressionFunctionDefinition<'pie', PointSeries, Arguments, Render<Pie>> {
return () => {
const { help, args: argHelp } = getFunctionHelp().pie;
return {
name: 'pie',
aliases: [],
type: 'render',
inputTypes: ['pointseries'],
help,
args: {
font: {
types: ['style'],
help: argHelp.font,
default: '{font}',
},
hole: {
types: ['number'],
default: 0,
help: argHelp.hole,
},
labelRadius: {
types: ['number'],
default: 100,
help: argHelp.labelRadius,
},
labels: {
types: ['boolean'],
default: true,
help: argHelp.labels,
},
legend: {
types: ['string', 'boolean'],
help: argHelp.legend,
default: false,
options: [...Object.values(Legend), false],
},
palette: {
types: ['palette'],
help: argHelp.palette,
default: '{palette}',
},
radius: {
types: ['string', 'number'],
help: argHelp.radius,
default: 'auto',
},
seriesStyle: {
multi: true,
types: ['seriesStyle'],
help: argHelp.seriesStyle,
},
tilt: {
types: ['number'],
default: 1,
help: argHelp.tilt,
},
},
fn: (input, args) => {
const {
tilt,
radius,
labelRadius,
labels,
hole,
legend,
palette,
font,
seriesStyle,
} = args;
const seriesStyles = keyBy(seriesStyle || [], 'label') || {};
const data: PieData[] = map(groupBy(input.rows, 'color'), (series, label = '') => {
const item: PieData = {
label,
data: series.map((point) => point.size || 1),
};
const style = seriesStyles[label];
// append series style, if there is a match
if (style) {
item.color = get(style, 'color');
}
return item;
});
return {
type: 'render',
as: 'pie',
value: {
font,
data,
options: {
canvas: false,
colors: paletteService
.get(palette.name || 'custom')
.getColors(data.length, palette.params),
legend: getLegendConfig(legend, data.length),
grid: {
show: false,
},
series: {
pie: {
show: true,
innerRadius: Math.max(hole, 0) / 100,
stroke: {
width: 0,
},
label: {
show: labels,
radius: (labelRadius >= 0 ? labelRadius : 100) / 100,
},
tilt,
radius,
},
bubbles: {
show: false,
},
shadowSize: 0,
},
},
},
};
},
};
};
}

View file

@ -4,21 +4,26 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { functionWrapper } from '../../../__tests__/helpers/function_wrapper';
import { testPlot } from './__tests__/fixtures/test_pointseries';
import { functionWrapper } from '../../__tests__/helpers/function_wrapper';
import { testPlot } from '../../canvas_plugin_src/functions/common/__tests__/fixtures/test_pointseries';
import {
fontStyle,
grayscalePalette,
gradientPalette,
yAxisConfig,
xAxisConfig,
seriesStyle,
defaultStyle,
} from './__tests__/fixtures/test_styles';
import { plot } from './plot';
} from '../../canvas_plugin_src/functions/common/__tests__/fixtures/test_styles';
import { plotFunctionFactory } from './plot';
describe('plot', () => {
const fn = functionWrapper(plot);
const fn = functionWrapper(
plotFunctionFactory({
get: () => ({
getColors: () => ['red', 'black'],
}),
})
);
it('returns a render as plot', () => {
const result = fn(testPlot);
@ -111,15 +116,18 @@ describe('plot', () => {
describe('palette', () => {
it('sets the color palette', () => {
const result = fn(testPlot, { palette: grayscalePalette }).value.options;
const mockedColors = jest.fn(() => ['#FFFFFF', '#888888', '#000000']);
const mockedFn = functionWrapper(
plotFunctionFactory({
get: () => ({
getColors: mockedColors,
}),
})
);
const result = mockedFn(testPlot, { palette: grayscalePalette }).value.options;
expect(result).toHaveProperty('colors');
expect(result.colors).toEqual(grayscalePalette.colors);
});
it('creates a new set of colors from a color scale when gradient is true', () => {
const result = fn(testPlot, { palette: gradientPalette }).value.options;
expect(result).toHaveProperty('colors');
expect(result.colors).toEqual(['#ffffff', '#aaaaaa', '#555555', '#000000']);
expect(result.colors).toEqual(['#FFFFFF', '#888888', '#000000']);
expect(mockedColors).toHaveBeenCalledWith(4, grayscalePalette.params);
});
// TODO: write test when using an instance of the interpreter

View file

@ -4,8 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { xAxisConfig, yAxisConfig, hideAxis } from './__tests__/fixtures/test_styles';
import { getFlotAxisConfig } from './plot/get_flot_axis_config';
import {
xAxisConfig,
yAxisConfig,
hideAxis,
} from '../../../canvas_plugin_src/functions/common/__tests__/fixtures/test_styles';
import { getFlotAxisConfig } from './get_flot_axis_config';
describe('getFlotAxisConfig', () => {
const columns = {

View file

@ -5,8 +5,8 @@
*/
import { get, map } from 'lodash';
import { Ticks, AxisConfig, isAxisConfig } from '../../../../types';
import { Style, PointSeriesColumns } from '../../../../../../../src/plugins/expressions/common';
import { Ticks, AxisConfig, isAxisConfig } from '../../../types';
import { Style, PointSeriesColumns } from '../../../../../../src/plugins/expressions/common';
type Position = 'bottom' | 'top' | 'left' | 'right';
interface Config {

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { fontStyle } from './__tests__/fixtures/test_styles';
import { defaultSpec, getFontSpec } from './plot/get_font_spec';
import { fontStyle } from '../../../canvas_plugin_src/functions/common/__tests__/fixtures/test_styles';
import { defaultSpec, getFontSpec } from './get_font_spec';
describe('getFontSpec', () => {
describe('default output', () => {

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { openSans } from '../../../../common/lib/fonts';
import { Style } from '../../../../types';
import { openSans } from '../../../common/lib/fonts';
import { Style } from '../../../types';
// converts the output of the font function to a flot font spec
// for font spec, see https://github.com/flot/flot/blob/master/API.md#customizing-the-axes

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { getTickHash } from './plot/get_tick_hash';
import { getTickHash } from './get_tick_hash';
describe('getTickHash', () => {
it('creates a hash for tick marks for string columns only', () => {

View file

@ -5,7 +5,7 @@
*/
import { get, sortBy } from 'lodash';
import { PointSeriesColumns, DatatableRow, Ticks } from '../../../../types';
import { PointSeriesColumns, DatatableRow, Ticks } from '../../../types';
export const getTickHash = (columns: PointSeriesColumns, rows: DatatableRow[]) => {
const ticks: Ticks = {

View file

@ -0,0 +1,177 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { set } from '@elastic/safer-lodash-set';
import { groupBy, get, keyBy, map, sortBy } from 'lodash';
import { ExpressionFunctionDefinition, Style } from 'src/plugins/expressions';
import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
// @ts-expect-error untyped local
import { getLegendConfig } from '../../../common/lib/get_legend_config';
import { getFlotAxisConfig } from './get_flot_axis_config';
import { getFontSpec } from './get_font_spec';
import { seriesStyleToFlot } from './series_style_to_flot';
import { getTickHash } from './get_tick_hash';
import { getFunctionHelp } from '../../../i18n';
import { AxisConfig, PointSeries, Render, SeriesStyle, Legend } from '../../../types';
interface Arguments {
seriesStyle: SeriesStyle[];
defaultStyle: SeriesStyle;
palette: PaletteOutput;
font: Style;
legend: Legend | boolean;
xaxis: AxisConfig | boolean;
yaxis: AxisConfig | boolean;
}
export function plotFunctionFactory(
paletteService: PaletteRegistry
): () => ExpressionFunctionDefinition<'plot', PointSeries, Arguments, Render<any>> {
return () => {
const { help, args: argHelp } = getFunctionHelp().plot;
return {
name: 'plot',
aliases: [],
type: 'render',
inputTypes: ['pointseries'],
help,
args: {
defaultStyle: {
multi: false,
types: ['seriesStyle'],
help: argHelp.defaultStyle,
default: '{seriesStyle points=5}',
},
font: {
types: ['style'],
help: argHelp.font,
default: '{font}',
},
legend: {
types: ['string', 'boolean'],
help: argHelp.legend,
default: 'ne',
options: [...Object.values(Legend), false],
},
palette: {
types: ['palette'],
help: argHelp.palette,
default: '{palette}',
},
seriesStyle: {
multi: true,
types: ['seriesStyle'],
help: argHelp.seriesStyle,
},
xaxis: {
types: ['boolean', 'axisConfig'],
help: argHelp.xaxis,
default: true,
},
yaxis: {
types: ['boolean', 'axisConfig'],
help: argHelp.yaxis,
default: true,
},
},
fn: (input, args) => {
const seriesStyles: { [key: string]: SeriesStyle } =
keyBy(args.seriesStyle || [], 'label') || {};
const sortedRows = sortBy(input.rows, ['x', 'y', 'color', 'size', 'text']);
const ticks = getTickHash(input.columns, sortedRows);
const font = args.font ? getFontSpec(args.font) : {};
const data = map(groupBy(sortedRows, 'color'), (series, label) => {
const seriesStyle = {
...args.defaultStyle,
...seriesStyles[label as string],
};
const flotStyle = seriesStyle ? seriesStyleToFlot(seriesStyle) : {};
return {
...flotStyle,
label,
data: series.map((point) => {
const attrs: {
size?: number;
text?: string;
} = {};
const x = get(input.columns, 'x.type') === 'string' ? ticks.x.hash[point.x] : point.x;
const y = get(input.columns, 'y.type') === 'string' ? ticks.y.hash[point.y] : point.y;
if (point.size != null) {
attrs.size = point.size;
} else if (get(seriesStyle, 'points')) {
attrs.size = seriesStyle.points;
set(flotStyle, 'bubbles.size.min', seriesStyle.points);
}
if (point.text != null) {
attrs.text = point.text;
}
return [x, y, attrs];
}),
};
});
const gridConfig = {
borderWidth: 0,
borderColor: null,
color: 'rgba(0,0,0,0)',
labelMargin: 30,
margin: {
right: 30,
top: 20,
bottom: 0,
left: 0,
},
};
const output = {
type: 'render',
as: 'plot',
value: {
font: args.font,
data: sortBy(data, 'label'),
options: {
canvas: false,
colors: paletteService
.get(args.palette.name || 'custom')
.getColors(data.length, args.palette.params),
legend: getLegendConfig(args.legend, data.length),
grid: gridConfig,
xaxis: getFlotAxisConfig('x', args.xaxis, {
columns: input.columns,
ticks,
font,
}),
yaxis: getFlotAxisConfig('y', args.yaxis, {
columns: input.columns,
ticks,
font,
}),
series: {
shadowSize: 0,
...seriesStyleToFlot(args.defaultStyle),
},
},
},
};
// fix the issue of plot sometimes re-rendering with an empty chart
// TODO: holy hell, why does this work?! the working theory is that some values become undefined
// and serializing the result here causes them to be dropped off, and this makes flot react differently.
// It's also possible that something else ends up mutating this object, but that seems less likely.
return JSON.parse(JSON.stringify(output));
},
};
};
}

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { seriesStyleToFlot } from './plot/series_style_to_flot';
import { seriesStyleToFlot } from './series_style_to_flot';
describe('seriesStyleToFlot', () => {
it('returns an empty object if seriesStyle is not provided', () => {

View file

@ -5,7 +5,7 @@
*/
import { get } from 'lodash';
import { SeriesStyle } from '../../../../types';
import { SeriesStyle } from '../../../types';
export const seriesStyleToFlot = (seriesStyle: SeriesStyle) => {
if (!seriesStyle) {

View file

@ -5,6 +5,7 @@
*/
import { BehaviorSubject } from 'rxjs';
import { ChartsPluginSetup, ChartsPluginStart } from 'src/plugins/charts/public';
import {
CoreSetup,
CoreStart,
@ -43,6 +44,7 @@ export interface CanvasSetupDeps {
home?: HomePublicPluginSetup;
usageCollection?: UsageCollectionSetup;
bfetch: BfetchPublicSetup;
charts: ChartsPluginSetup;
}
export interface CanvasStartDeps {
@ -50,6 +52,7 @@ export interface CanvasStartDeps {
expressions: ExpressionsStart;
inspector: InspectorStart;
uiActions: UiActionsStart;
charts: ChartsPluginStart;
}
/**

View file

@ -6,6 +6,7 @@
"ui": true,
"requiredPlugins": [
"data",
"charts",
"expressions",
"navigation",
"urlForwarding",

View file

@ -6,12 +6,13 @@
import { Ast } from '@kbn/interpreter/common';
import { buildExpression } from '../../../../../src/plugins/expressions/public';
import { createMockDatasource } from '../editor_frame_service/mocks';
import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
import { DatatableVisualizationState, datatableVisualization } from './visualization';
import { Operation, DataType, FramePublicAPI, TableSuggestionColumn } from '../types';
function mockFrame(): FramePublicAPI {
return {
...createMockFramePublicAPI(),
addNewLayer: () => 'aaa',
removeLayers: () => {},
datasourceLayers: {},

View file

@ -23,6 +23,7 @@ import { DragDrop } from '../../drag_drop';
import { FrameLayout } from './frame_layout';
import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks';
import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks';
function generateSuggestion(state = {}): DatasourceSuggestion {
@ -55,7 +56,9 @@ function getDefaultProps() {
uiActions: uiActionsPluginMock.createStartContract(),
data: dataPluginMock.createStartContract(),
expressions: expressionsPluginMock.createStartContract(),
charts: chartPluginMock.createStartContract(),
},
palettes: chartPluginMock.createPaletteRegistry(),
showNoDataPopover: jest.fn(),
};
}
@ -233,10 +236,11 @@ describe('editor_frame', () => {
});
it('should pass the public frame api into visualization initialize', async () => {
const defaultProps = getDefaultProps();
await act(async () => {
mount(
<EditorFrame
{...getDefaultProps()}
{...defaultProps}
visualizationMap={{
testVis: mockVisualization,
}}
@ -259,6 +263,7 @@ describe('editor_frame', () => {
query: { query: '', language: 'lucene' },
filters: [],
dateRange: { fromDate: 'now-7d', toDate: 'now' },
availablePalettes: defaultProps.palettes,
});
});
@ -963,6 +968,7 @@ describe('editor_frame', () => {
expect.objectContaining({
datasourceLayers: expect.objectContaining({ first: mockDatasource.publicAPIMock }),
}),
undefined,
undefined
);
expect(mockVisualization2.getConfiguration).toHaveBeenCalledWith(

View file

@ -6,6 +6,7 @@
import React, { useEffect, useReducer, useState } from 'react';
import { CoreSetup, CoreStart } from 'kibana/public';
import { PaletteRegistry } from 'src/plugins/charts/public';
import { ReactExpressionRendererType } from '../../../../../../src/plugins/expressions/public';
import { Datasource, FramePublicAPI, Visualization } from '../../types';
import { reducer, getInitialState } from './state_management';
@ -31,6 +32,7 @@ export interface EditorFrameProps {
initialDatasourceId: string | null;
initialVisualizationId: string | null;
ExpressionRenderer: ReactExpressionRendererType;
palettes: PaletteRegistry;
onError: (e: { message: string }) => void;
core: CoreSetup | CoreStart;
plugins: EditorFrameStartPlugins;
@ -103,6 +105,8 @@ export function EditorFrame(props: EditorFrameProps) {
query: props.query,
filters: props.filters,
availablePalettes: props.palettes,
addNewLayer() {
const newLayerId = generateId();

View file

@ -5,7 +5,7 @@
*/
import { getSavedObjectFormat, Props } from './save';
import { createMockDatasource, createMockVisualization } from '../mocks';
import { createMockDatasource, createMockFramePublicAPI, createMockVisualization } from '../mocks';
import { esFilters, IIndexPattern, IFieldType } from '../../../../../../src/plugins/data/public';
jest.mock('./expression_helpers');
@ -37,6 +37,7 @@ describe('save editor frame state', () => {
visualization: { activeId: '2', state: {} },
},
framePublicAPI: {
...createMockFramePublicAPI(),
addNewLayer: jest.fn(),
removeLayers: jest.fn(),
datasourceLayers: {

View file

@ -12,6 +12,7 @@ import { coreMock } from 'src/core/public/mocks';
import { uiActionsPluginMock } from '../../../../../../src/plugins/ui_actions/public/mocks';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
import { expressionsPluginMock } from '../../../../../../src/plugins/expressions/public/mocks';
import { chartPluginMock } from 'src/plugins/charts/public/mocks';
describe('editor_frame state management', () => {
describe('initialization', () => {
@ -31,7 +32,9 @@ describe('editor_frame state management', () => {
uiActions: uiActionsPluginMock.createStartContract(),
data: dataPluginMock.createStartContract(),
expressions: expressionsPluginMock.createStartContract(),
charts: chartPluginMock.createStartContract(),
},
palettes: chartPluginMock.createPaletteRegistry(),
dateRange: { fromDate: 'now-7d', toDate: 'now' },
query: { query: '', language: 'lucene' },
filters: [],

View file

@ -7,6 +7,7 @@
import { getSuggestions } from './suggestion_helpers';
import { createMockVisualization, createMockDatasource, DatasourceMock } from '../mocks';
import { TableSuggestion, DatasourceSuggestion } from '../../types';
import { PaletteOutput } from 'src/plugins/charts/public';
const generateSuggestion = (state = {}, layerId: string = 'first'): DatasourceSuggestion => ({
state,
@ -413,4 +414,62 @@ describe('suggestion helpers', () => {
})
);
});
it('should pass passed in main palette if specified', () => {
const mockVisualization1 = createMockVisualization();
const mockVisualization2 = createMockVisualization();
const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' };
datasourceMap.mock.getDatasourceSuggestionsFromCurrentState.mockReturnValue([
generateSuggestion(0),
generateSuggestion(1),
]);
getSuggestions({
visualizationMap: {
vis1: mockVisualization1,
vis2: mockVisualization2,
},
activeVisualizationId: 'vis1',
visualizationState: {},
datasourceMap,
datasourceStates,
mainPalette,
});
expect(mockVisualization1.getSuggestions).toHaveBeenCalledWith(
expect.objectContaining({
mainPalette,
})
);
expect(mockVisualization2.getSuggestions).toHaveBeenCalledWith(
expect.objectContaining({
mainPalette,
})
);
});
it('should query active visualization for main palette if not specified', () => {
const mockVisualization1 = createMockVisualization();
const mockVisualization2 = createMockVisualization();
const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' };
mockVisualization1.getMainPalette = jest.fn(() => mainPalette);
datasourceMap.mock.getDatasourceSuggestionsFromCurrentState.mockReturnValue([
generateSuggestion(0),
generateSuggestion(1),
]);
getSuggestions({
visualizationMap: {
vis1: mockVisualization1,
vis2: mockVisualization2,
},
activeVisualizationId: 'vis1',
visualizationState: {},
datasourceMap,
datasourceStates,
});
expect(mockVisualization1.getMainPalette).toHaveBeenCalledWith({});
expect(mockVisualization2.getSuggestions).toHaveBeenCalledWith(
expect.objectContaining({
mainPalette,
})
);
});
});

View file

@ -7,6 +7,7 @@
import _ from 'lodash';
import { Ast } from '@kbn/interpreter/common';
import { IconType } from '@elastic/eui/src/components/icon/icon';
import { PaletteOutput } from 'src/plugins/charts/public';
import { VisualizeFieldContext } from '../../../../../../src/plugins/ui_actions/public';
import {
Visualization,
@ -49,6 +50,7 @@ export function getSuggestions({
visualizationState,
field,
visualizeTriggerFieldContext,
mainPalette,
}: {
datasourceMap: Record<string, Datasource>;
datasourceStates: Record<
@ -64,6 +66,7 @@ export function getSuggestions({
visualizationState: unknown;
field?: unknown;
visualizeTriggerFieldContext?: VisualizeFieldContext;
mainPalette?: PaletteOutput;
}): Suggestion[] {
const datasources = Object.entries(datasourceMap).filter(
([datasourceId]) => datasourceStates[datasourceId] && !datasourceStates[datasourceId].isLoading
@ -100,13 +103,21 @@ export function getSuggestions({
const table = datasourceSuggestion.table;
const currentVisualizationState =
visualizationId === activeVisualizationId ? visualizationState : undefined;
const palette =
mainPalette ||
(activeVisualizationId &&
visualizationMap[activeVisualizationId] &&
visualizationMap[activeVisualizationId].getMainPalette
? visualizationMap[activeVisualizationId].getMainPalette!(visualizationState)
: undefined);
return getVisualizationSuggestions(
visualization,
table,
visualizationId,
datasourceSuggestion,
currentVisualizationState,
subVisualizationId
subVisualizationId,
palette
);
})
)
@ -165,7 +176,8 @@ function getVisualizationSuggestions(
visualizationId: string,
datasourceSuggestion: DatasourceSuggestion & { datasourceId: string },
currentVisualizationState: unknown,
subVisualizationId?: string
subVisualizationId?: string,
mainPalette?: PaletteOutput
) {
return visualization
.getSuggestions({
@ -173,6 +185,7 @@ function getVisualizationSuggestions(
state: currentVisualizationState,
keptLayerIds: datasourceSuggestion.keptLayerIds,
subVisualizationId,
mainPalette,
})
.map(({ state, ...visualizationSuggestion }) => ({
...visualizationSuggestion,

View file

@ -16,6 +16,7 @@ import { mountWithIntl as mount } from 'test_utils/enzyme_helpers';
import { Visualization, FramePublicAPI, DatasourcePublicAPI } from '../../../types';
import { Action } from '../state_management';
import { ChartSwitch } from './chart_switch';
import { PaletteOutput } from 'src/plugins/charts/public';
describe('chart_switch', () => {
function generateVisualization(id: string): jest.Mocked<Visualization> {
@ -449,6 +450,39 @@ describe('chart_switch', () => {
);
});
it('should query main palette from active chart and pass into suggestions', () => {
const dispatch = jest.fn();
const visualizations = mockVisualizations();
const mockPalette: PaletteOutput = { type: 'palette', name: 'mock' };
visualizations.visA.getMainPalette = jest.fn(() => mockPalette);
visualizations.visB.getSuggestions.mockReturnValueOnce([]);
const frame = mockFrame(['a', 'b', 'c']);
const currentVisState = {};
const component = mount(
<ChartSwitch
visualizationId="visA"
visualizationState={currentVisState}
visualizationMap={visualizations}
dispatch={dispatch}
framePublicAPI={frame}
datasourceMap={mockDatasourceMap()}
datasourceStates={mockDatasourceStates()}
/>
);
switchTo('visB', component);
expect(visualizations.visA.getMainPalette).toHaveBeenCalledWith(currentVisState);
expect(visualizations.visB.getSuggestions).toHaveBeenCalledWith(
expect.objectContaining({
keptLayerIds: ['a'],
mainPalette: mockPalette,
})
);
});
it('should not remove layers when switching between subtypes', () => {
const dispatch = jest.fn();
const frame = mockFrame(['a', 'b', 'c']);

View file

@ -167,7 +167,15 @@ export function ChartSwitch(props: Props) {
subVisualizationId,
newVisualization.initialize(
props.framePublicAPI,
props.visualizationId === newVisualization.id ? props.visualizationState : undefined
props.visualizationId === newVisualization.id
? props.visualizationState
: undefined,
props.visualizationId &&
props.visualizationMap[props.visualizationId].getMainPalette
? props.visualizationMap[props.visualizationId].getMainPalette!(
props.visualizationState
)
: undefined
)
);
},
@ -304,6 +312,12 @@ function getTopSuggestion(
newVisualization: Visualization<unknown>,
subVisualizationId?: string
): Suggestion | undefined {
const mainPalette =
props.visualizationId &&
props.visualizationMap[props.visualizationId] &&
props.visualizationMap[props.visualizationId].getMainPalette
? props.visualizationMap[props.visualizationId].getMainPalette!(props.visualizationState)
: undefined;
const unfilteredSuggestions = getSuggestions({
datasourceMap: props.datasourceMap,
datasourceStates: props.datasourceStates,
@ -311,6 +325,7 @@ function getTopSuggestion(
activeVisualizationId: props.visualizationId,
visualizationState: props.visualizationState,
subVisualizationId,
mainPalette,
});
const suggestions = unfilteredSuggestions.filter((suggestion) => {
// don't use extended versions of current data table on switching between visualizations

View file

@ -98,6 +98,12 @@ export function WorkspacePanel({
(datasource) => datasource.getTableSpec().length > 0
);
const mainPalette =
activeVisualizationId &&
visualizationMap[activeVisualizationId] &&
visualizationMap[activeVisualizationId].getMainPalette
? visualizationMap[activeVisualizationId].getMainPalette!(visualizationState)
: undefined;
const suggestions = getSuggestions({
datasourceMap: { [activeDatasourceId]: datasourceMap[activeDatasourceId] },
datasourceStates,
@ -108,6 +114,7 @@ export function WorkspacePanel({
activeVisualizationId,
visualizationState,
field: dragDropContext.dragging,
mainPalette,
});
return suggestions.find((s) => s.visualizationId === activeVisualizationId) || suggestions[0];

View file

@ -16,6 +16,7 @@ import {
IndexPattern,
} from 'src/plugins/data/public';
import { ExecutionContextSearch } from 'src/plugins/expressions';
import { PaletteOutput } from 'src/plugins/charts/public';
import { Subscription } from 'rxjs';
import { toExpression, Ast } from '@kbn/interpreter/common';
@ -50,7 +51,9 @@ export type LensByValueInput = {
} & EmbeddableInput;
export type LensByReferenceInput = SavedObjectEmbeddableInput & EmbeddableInput;
export type LensEmbeddableInput = LensByValueInput | LensByReferenceInput;
export type LensEmbeddableInput = (LensByValueInput | LensByReferenceInput) & {
palette?: PaletteOutput;
};
export interface LensEmbeddableOutput extends EmbeddableOutput {
indexPatterns?: IIndexPattern[];
@ -172,11 +175,13 @@ export class Embeddable
if (!this.savedVis || !this.isInitialized) {
return;
}
const input = this.getInput();
render(
<ExpressionWrapper
ExpressionRenderer={this.expressionRenderer}
expression={this.expression || null}
searchContext={this.getMergedSearchContext()}
variables={input.palette ? { theme: { palette: input.palette } } : {}}
searchSessionId={this.input.searchSessionId}
handleEvent={this.handleEvent}
/>,

View file

@ -18,6 +18,7 @@ import { getOriginalRequestErrorMessage } from '../error_helper';
export interface ExpressionWrapperProps {
ExpressionRenderer: ReactExpressionRendererType;
expression: string | null;
variables?: Record<string, unknown>;
searchContext: ExecutionContextSearch;
searchSessionId?: string;
handleEvent: (event: ExpressionRendererEvent) => void;
@ -27,6 +28,7 @@ export function ExpressionWrapper({
ExpressionRenderer: ExpressionRendererComponent,
expression,
searchContext,
variables,
handleEvent,
searchSessionId,
}: ExpressionWrapperProps) {
@ -51,6 +53,7 @@ export function ExpressionWrapper({
<ExpressionRendererComponent
className="lnsExpressionRenderer__component"
padding="s"
variables={variables}
expression={expression}
searchContext={searchContext}
searchSessionId={searchSessionId}

View file

@ -5,6 +5,7 @@
*/
import React from 'react';
import { PaletteDefinition } from 'src/plugins/charts/public';
import {
ReactExpressionRendererProps,
ExpressionsSetup,
@ -15,6 +16,7 @@ import { expressionsPluginMock } from '../../../../../src/plugins/expressions/pu
import { DatasourcePublicAPI, FramePublicAPI, Datasource, Visualization } from '../types';
import { EditorFrameSetupPlugins, EditorFrameStartPlugins } from './service';
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
export function createMockVisualization(): jest.Mocked<Visualization> {
return {
@ -95,7 +97,28 @@ export function createMockDatasource(id: string): DatasourceMock {
export type FrameMock = jest.Mocked<FramePublicAPI>;
export function createMockPaletteDefinition(): jest.Mocked<PaletteDefinition> {
return {
getColors: jest.fn((_) => ['#ff0000', '#00ff00']),
title: 'Mock Palette',
id: 'default',
renderEditor: jest.fn(),
toExpression: jest.fn(() => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'mock_palette',
arguments: {},
},
],
})),
getColor: jest.fn().mockReturnValue('#ff0000'),
};
}
export function createMockFramePublicAPI(): FrameMock {
const palette = createMockPaletteDefinition();
return {
datasourceLayers: {},
addNewLayer: jest.fn(() => ''),
@ -103,6 +126,10 @@ export function createMockFramePublicAPI(): FrameMock {
dateRange: { fromDate: 'now-7d', toDate: 'now' },
query: { query: '', language: 'lucene' },
filters: [],
availablePalettes: {
get: () => palette,
getAll: () => [palette],
},
};
}
@ -128,6 +155,7 @@ export function createMockSetupDependencies() {
data: dataPluginMock.createSetupContract(),
embeddable: embeddablePluginMock.createSetupContract(),
expressions: expressionsPluginMock.createSetupContract(),
charts: chartPluginMock.createSetupContract(),
} as unknown) as MockedSetupDependencies;
}
@ -136,5 +164,6 @@ export function createMockStartDependencies() {
data: dataPluginMock.createSetupContract(),
embeddable: embeddablePluginMock.createStartContract(),
expressions: expressionsPluginMock.createStartContract(),
charts: chartPluginMock.createStartContract(),
} as unknown) as MockedStartDependencies;
}

View file

@ -25,6 +25,7 @@ import { Document } from '../persistence/saved_object_store';
import { mergeTables } from './merge_tables';
import { EmbeddableFactory, LensEmbeddableStartServices } from './embeddable/embeddable_factory';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
import { DashboardStart } from '../../../../../src/plugins/dashboard/public';
import { LensAttributeService } from '../lens_attribute_service';
@ -32,6 +33,7 @@ export interface EditorFrameSetupPlugins {
data: DataPublicPluginSetup;
embeddable?: EmbeddableSetup;
expressions: ExpressionsSetup;
charts: ChartsPluginSetup;
}
export interface EditorFrameStartPlugins {
@ -40,6 +42,7 @@ export interface EditorFrameStartPlugins {
dashboard?: DashboardStart;
expressions: ExpressionsStart;
uiActions?: UiActionsStart;
charts: ChartsPluginSetup;
}
async function collectAsyncDefinitions<T extends { id: string }>(
@ -143,6 +146,8 @@ export class EditorFrameService {
const { EditorFrame, getActiveDatasourceIdFromDoc } = await import('../async_services');
const palettes = await plugins.charts.palettes.getPalettes();
render(
<I18nProvider>
<EditorFrame
@ -158,6 +163,7 @@ export class EditorFrameService {
core={core}
plugins={plugins}
ExpressionRenderer={plugins.expressions.ReactExpressionRenderer}
palettes={palettes}
doc={doc}
dateRange={dateRange}
query={query}

View file

@ -22,6 +22,10 @@ export interface Document {
datasourceStates: Record<string, unknown>;
visualization: unknown;
query: Query;
globalPalette?: {
activePaletteId: string;
state?: unknown;
};
filters: PersistableFilter[];
};
references: SavedObjectReference[];

View file

@ -17,7 +17,7 @@ import {
import { LensMultiTable, FormatFactory, LensFilterEvent } from '../types';
import { PieExpressionProps, PieExpressionArgs } from './types';
import { PieComponent } from './render_function';
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
import { ChartsPluginSetup, PaletteRegistry } from '../../../../../src/plugins/charts/public';
export interface PieRender {
type: 'render';
@ -91,6 +91,11 @@ export const pie: ExpressionFunctionDefinition<
types: ['number'],
help: '',
},
palette: {
default: `{theme "palette" default={system_palette name="default"} }`,
help: '',
types: ['palette'],
},
},
inputTypes: ['lens_multitable'],
fn(data: LensMultiTable, args: PieExpressionArgs) {
@ -108,6 +113,7 @@ export const pie: ExpressionFunctionDefinition<
export const getPieRenderer = (dependencies: {
formatFactory: Promise<FormatFactory>;
chartsThemeService: ChartsPluginSetup['theme'];
paletteService: PaletteRegistry;
}): ExpressionRenderDefinition<PieExpressionProps> => ({
name: 'lens_pie_renderer',
displayName: i18n.translate('xpack.lens.pie.visualizationName', {
@ -131,6 +137,7 @@ export const getPieRenderer = (dependencies: {
{...config}
formatFactory={formatFactory}
chartsThemeService={dependencies.chartsThemeService}
paletteService={dependencies.paletteService}
onClickValue={onClickValue}
/>
</I18nProvider>,

View file

@ -29,7 +29,8 @@ export class PieVisualization {
{ expressions, formatFactory, editorFrame, charts }: PieVisualizationPluginSetupPlugins
) {
editorFrame.registerVisualization(async () => {
const { pieVisualization, pie, getPieRenderer } = await import('../async_services');
const { getPieVisualization, pie, getPieRenderer } = await import('../async_services');
const palettes = await charts.palettes.getPalettes();
expressions.registerFunction(() => pie);
@ -37,9 +38,10 @@ export class PieVisualization {
getPieRenderer({
formatFactory,
chartsThemeService: charts.theme,
paletteService: palettes,
})
);
return pieVisualization;
return getPieVisualization({ paletteService: palettes });
});
}
}

View file

@ -5,7 +5,12 @@
*/
import React from 'react';
import { SeriesIdentifier, Settings } from '@elastic/charts';
import { Partition, SeriesIdentifier, Settings } from '@elastic/charts';
import {
NodeColorAccessor,
ShapeTreeNode,
} from '@elastic/charts/dist/chart_types/partition_chart/layout/types/viewmodel_types';
import { HierarchyOfArrays } from '@elastic/charts/dist/chart_types/partition_chart/layout/utils/group_by_rollup';
import { shallow } from 'enzyme';
import { LensMultiTable } from '../types';
import { PieComponent } from './render_function';
@ -55,6 +60,7 @@ describe('PieVisualization component', () => {
nestedLegend: false,
percentDecimals: 3,
hideLabels: false,
palette: { name: 'mock', type: 'palette' },
};
function getDefaultArgs() {
@ -63,6 +69,7 @@ describe('PieVisualization component', () => {
formatFactory: getFormatSpy,
onClickValue: jest.fn(),
chartsThemeService,
paletteService: chartPluginMock.createPaletteRegistry(),
};
}
@ -92,6 +99,84 @@ describe('PieVisualization component', () => {
expect(component.find(Settings).prop('showLegend')).toEqual(false);
});
test('it calls the color function with the right series layers', () => {
const defaultArgs = getDefaultArgs();
const component = shallow(
<PieComponent
args={args}
{...defaultArgs}
data={{
...data,
tables: {
first: {
...data.tables.first,
rows: [
{ a: 'empty', b: 'first', c: 1, d: 'Row 1' },
{ a: 'css', b: 'first', c: 1, d: 'Row 1' },
{ a: 'css', b: 'second', c: 1, d: 'Row 1' },
{ a: 'css', b: 'third', c: 1, d: 'Row 1' },
{ a: 'gz', b: 'first', c: 1, d: 'Row 1' },
],
},
},
}}
/>
);
(component.find(Partition).prop('layers')![1].shape!.fillColor as NodeColorAccessor)(
({
dataName: 'third',
depth: 2,
parent: {
children: [
['first', {}],
['second', {}],
['third', {}],
],
depth: 1,
value: 200,
dataName: 'css',
parent: {
children: [
['empty', {}],
['css', {}],
['gz', {}],
],
depth: 0,
sortIndex: 0,
value: 500,
},
sortIndex: 1,
},
value: 41,
sortIndex: 2,
} as unknown) as ShapeTreeNode,
0,
[] as HierarchyOfArrays
);
expect(defaultArgs.paletteService.get('mock').getColor).toHaveBeenCalledWith(
[
{
name: 'css',
rankAtDepth: 1,
totalSeriesAtDepth: 3,
},
{
name: 'third',
rankAtDepth: 2,
totalSeriesAtDepth: 3,
},
],
{
maxDepth: 2,
totalSeries: 5,
behindText: true,
},
undefined
);
});
test('it hides legend with 2 groups for treemap', () => {
const component = shallow(
<PieComponent args={{ ...args, shape: 'treemap' }} {...getDefaultArgs()} />

View file

@ -4,25 +4,22 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useState, useEffect } from 'react';
import color from 'color';
import { uniq } from 'lodash';
import React, { useEffect, useState } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { EuiText } from '@elastic/eui';
// @ts-ignore no types
import { euiPaletteColorBlindBehindText } from '@elastic/eui/lib/services';
import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
import {
Chart,
Datum,
Settings,
LayerValue,
Partition,
PartitionConfig,
PartitionLayer,
PartitionLayout,
PartitionFillLabel,
RecursivePartial,
LayerValue,
Position,
Settings,
} from '@elastic/charts';
import { FormatFactory, LensFilterEvent } from '../types';
import { VisualizationContainer } from '../visualization_container';
@ -32,24 +29,27 @@ import { getSliceValue, getFilterContext } from './render_helpers';
import { EmptyPlaceholder } from '../shared_components';
import './visualization.scss';
import { desanitizeFilterContext } from '../utils';
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
import {
ChartsPluginSetup,
PaletteRegistry,
SeriesLayer,
} from '../../../../../src/plugins/charts/public';
import { LensIconChartDonut } from '../assets/chart_donut';
const EMPTY_SLICE = Symbol('empty_slice');
const sortedColors = euiPaletteColorBlindBehindText();
export function PieComponent(
props: PieExpressionProps & {
formatFactory: FormatFactory;
chartsThemeService: ChartsPluginSetup['theme'];
paletteService: PaletteRegistry;
onClickValue: (data: LensFilterEvent['data']) => void;
}
) {
const [firstTable] = Object.values(props.data.tables);
const formatters: Record<string, ReturnType<FormatFactory>> = {};
const { chartsThemeService, onClickValue } = props;
const { chartsThemeService, paletteService, onClickValue } = props;
const {
shape,
groups,
@ -61,8 +61,8 @@ export function PieComponent(
nestedLegend,
percentDecimals,
hideLabels,
palette,
} = props.args;
const isDarkMode = chartsThemeService.useDarkMode();
const chartTheme = chartsThemeService.useChartsTheme();
const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
@ -73,7 +73,7 @@ export function PieComponent(
}
const fillLabel: Partial<PartitionFillLabel> = {
textInvertible: false,
textInvertible: true,
valueFont: {
fontWeight: 700,
},
@ -86,6 +86,11 @@ export function PieComponent(
}
const bucketColumns = firstTable.columns.filter((col) => groups.includes(col.id));
const totalSeriesCount = uniq(
firstTable.rows.map((row) => {
return bucketColumns.map(({ id: columnId }) => row[columnId]).join(',');
})
).length;
const layers: PartitionLayer[] = bucketColumns.map((col, layerIndex) => {
return {
@ -100,34 +105,45 @@ export function PieComponent(
}
return String(d);
},
fillLabel:
isDarkMode &&
shape === 'treemap' &&
layerIndex < bucketColumns.length - 1 &&
categoryDisplay !== 'hide'
? { ...fillLabel, textColor: euiDarkVars.euiTextColor }
: fillLabel,
fillLabel,
shape: {
fillColor: (d) => {
const seriesLayers: SeriesLayer[] = [];
// Color is determined by round-robin on the index of the innermost slice
// This has to be done recursively until we get to the slice index
let parentIndex = 0;
let tempParent: typeof d | typeof d['parent'] = d;
while (tempParent.parent && tempParent.depth > 0) {
parentIndex = tempParent.sortIndex;
seriesLayers.unshift({
name: String(tempParent.parent.children[tempParent.sortIndex][0]),
rankAtDepth: tempParent.sortIndex,
totalSeriesAtDepth: tempParent.parent.children.length,
});
tempParent = tempParent.parent;
}
// Look up round-robin color from default palette
const outputColor = sortedColors[parentIndex % sortedColors.length];
if (shape === 'treemap') {
// Only highlight the innermost color of the treemap, as it accurately represents area
return layerIndex < bucketColumns.length - 1 ? 'rgba(0,0,0,0)' : outputColor;
if (layerIndex < bucketColumns.length - 1) {
return 'rgba(0,0,0,0)';
}
// only use the top level series layer for coloring
if (seriesLayers.length > 1) {
seriesLayers.pop();
}
}
const lighten = (d.depth - 1) / (bucketColumns.length * 2);
return color(outputColor, 'hsl').lighten(lighten).hex();
const outputColor = paletteService.get(palette.name).getColor(
seriesLayers,
{
behindText: categoryDisplay !== 'hide',
maxDepth: bucketColumns.length,
totalSeries: totalSeriesCount,
},
palette.params
);
return outputColor || 'rgba(0,0,0,0)';
},
},
};

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PaletteOutput } from 'src/plugins/charts/public';
import { DataType } from '../types';
import { suggestions } from './suggestions';
@ -311,7 +312,38 @@ describe('suggestions', () => {
);
});
it('should keep the layer settings when switching from treemap', () => {
it('should keep passed in palette', () => {
const mainPalette: PaletteOutput = { type: 'palette', name: 'mock' };
const results = suggestions({
table: {
layerId: 'first',
isMultiRow: true,
columns: [
{
columnId: 'a',
operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true },
},
{
columnId: 'b',
operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true },
},
{
columnId: 'e',
operation: { label: 'Count', dataType: 'number' as DataType, isBucketed: false },
},
],
changeType: 'initial',
},
state: undefined,
keptLayerIds: ['first'],
mainPalette,
});
expect(results[0].state.palette).toEqual(mainPalette);
});
it('should keep the layer settings and palette when switching from treemap', () => {
const palette: PaletteOutput = { type: 'palette', name: 'mock' };
expect(
suggestions({
table: {
@ -331,6 +363,7 @@ describe('suggestions', () => {
},
state: {
shape: 'treemap',
palette,
layers: [
{
layerId: 'first',
@ -351,6 +384,7 @@ describe('suggestions', () => {
expect.objectContaining({
state: {
shape: 'donut',
palette,
layers: [
{
layerId: 'first',

View file

@ -23,6 +23,7 @@ export function suggestions({
table,
state,
keptLayerIds,
mainPalette,
}: SuggestionRequest<PieVisualizationState>): Array<
VisualizationSuggestion<PieVisualizationState>
> {
@ -57,6 +58,7 @@ export function suggestions({
score: state && state.shape !== 'treemap' ? 0.6 : 0.4,
state: {
shape: newShape,
palette: mainPalette || state?.palette,
layers: [
state?.layers[0]
? {
@ -108,6 +110,7 @@ export function suggestions({
score: state?.shape === 'treemap' ? 0.7 : 0.5,
state: {
shape: 'treemap',
palette: mainPalette || state?.palette,
layers: [
state?.layers[0]
? {

View file

@ -5,6 +5,7 @@
*/
import { Ast } from '@kbn/interpreter/common';
import { PaletteRegistry } from 'src/plugins/charts/public';
import { Operation, DatasourcePublicAPI } from '../types';
import { DEFAULT_PERCENT_DECIMALS } from './constants';
import { PieVisualizationState } from './types';
@ -12,14 +13,19 @@ import { PieVisualizationState } from './types';
export function toExpression(
state: PieVisualizationState,
datasourceLayers: Record<string, DatasourcePublicAPI>,
paletteService: PaletteRegistry,
attributes: Partial<{ title: string; description: string }> = {}
) {
return expressionHelper(state, datasourceLayers, { ...attributes, isPreview: false });
return expressionHelper(state, datasourceLayers, paletteService, {
...attributes,
isPreview: false,
});
}
function expressionHelper(
state: PieVisualizationState,
datasourceLayers: Record<string, DatasourcePublicAPI>,
paletteService: PaletteRegistry,
attributes: { isPreview: boolean; title?: string; description?: string } = { isPreview: false }
): Ast | null {
const layer = state.layers[0];
@ -50,6 +56,29 @@ function expressionHelper(
legendPosition: [layer.legendPosition || 'right'],
percentDecimals: [layer.percentDecimals ?? DEFAULT_PERCENT_DECIMALS],
nestedLegend: [!!layer.nestedLegend],
...(state.palette
? {
palette: [
{
type: 'expression',
chain: [
{
type: 'function',
function: 'theme',
arguments: {
variable: ['palette'],
default: [
paletteService
.get(state.palette.name)
.toExpression(state.palette.params),
],
},
},
],
},
],
}
: {}),
},
},
],
@ -58,7 +87,8 @@ function expressionHelper(
export function toPreviewExpression(
state: PieVisualizationState,
datasourceLayers: Record<string, DatasourcePublicAPI>
datasourceLayers: Record<string, DatasourcePublicAPI>,
paletteService: PaletteRegistry
) {
return expressionHelper(state, datasourceLayers, { isPreview: true });
return expressionHelper(state, datasourceLayers, paletteService, { isPreview: true });
}

View file

@ -18,8 +18,9 @@ import {
import { Position } from '@elastic/charts';
import { DEFAULT_PERCENT_DECIMALS } from './constants';
import { PieVisualizationState, SharedLayerState } from './types';
import { VisualizationToolbarProps } from '../types';
import { VisualizationDimensionEditorProps, VisualizationToolbarProps } from '../types';
import { ToolbarPopover, LegendSettingsPopover } from '../shared_components';
import { PalettePicker } from '../shared_components';
const numberOptions: Array<{ value: SharedLayerState['numberDisplay']; inputDisplay: string }> = [
{
@ -244,3 +245,17 @@ const DecimalPlaceSlider = ({
/>
);
};
export function DimensionEditor(props: VisualizationDimensionEditorProps<PieVisualizationState>) {
return (
<>
<PalettePicker
palettes={props.frame.availablePalettes}
activePalette={props.state.palette}
setPalette={(newPalette) => {
props.setState({ ...props.state, palette: newPalette });
}}
/>
</>
);
}

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PaletteOutput } from 'src/plugins/charts/public';
import { LensMultiTable } from '../types';
export interface SharedLayerState {
@ -24,6 +25,7 @@ export type LayerState = SharedLayerState & {
export interface PieVisualizationState {
shape: 'donut' | 'pie' | 'treemap';
layers: LayerState[];
palette?: PaletteOutput;
}
export type PieExpressionArgs = SharedLayerState & {
@ -31,6 +33,7 @@ export type PieExpressionArgs = SharedLayerState & {
description?: string;
shape: 'pie' | 'donut' | 'treemap';
hideLabels: boolean;
palette: PaletteOutput;
};
export interface PieExpressionProps {

View file

@ -8,12 +8,13 @@ import React from 'react';
import { render } from 'react-dom';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import { PaletteRegistry } from 'src/plugins/charts/public';
import { Visualization, OperationMetadata } from '../types';
import { toExpression, toPreviewExpression } from './to_expression';
import { LayerState, PieVisualizationState } from './types';
import { suggestions } from './suggestions';
import { CHART_NAMES, MAX_PIE_BUCKETS, MAX_TREEMAP_BUCKETS } from './constants';
import { PieToolbar } from './toolbar';
import { DimensionEditor, PieToolbar } from './toolbar';
function newLayerState(layerId: string): LayerState {
return {
@ -31,7 +32,11 @@ const bucketedOperations = (op: OperationMetadata) => op.isBucketed;
const numberMetricOperations = (op: OperationMetadata) =>
!op.isBucketed && op.dataType === 'number';
export const pieVisualization: Visualization<PieVisualizationState> = {
export const getPieVisualization = ({
paletteService,
}: {
paletteService: PaletteRegistry;
}): Visualization<PieVisualizationState> => ({
id: 'lnsPie',
visualizationTypes: [
@ -82,15 +87,18 @@ export const pieVisualization: Visualization<PieVisualizationState> = {
shape: visualizationTypeId as PieVisualizationState['shape'],
}),
initialize(frame, state) {
initialize(frame, state, mainPalette) {
return (
state || {
shape: 'donut',
layers: [newLayerState(frame.addNewLayer())],
palette: mainPalette,
}
);
},
getMainPalette: (state) => (state ? state.palette : undefined),
getSuggestions: suggestions,
getConfiguration({ state, frame, layerId }) {
@ -121,6 +129,7 @@ export const pieVisualization: Visualization<PieVisualizationState> = {
filterOperations: bucketedOperations,
required: true,
dataTestSubj: 'lnsPie_groupByDimensionPanel',
enableDimensionEditor: true,
},
{
groupId: 'metric',
@ -151,6 +160,7 @@ export const pieVisualization: Visualization<PieVisualizationState> = {
filterOperations: bucketedOperations,
required: true,
dataTestSubj: 'lnsPie_sliceByDimensionPanel',
enableDimensionEditor: true,
},
{
groupId: 'metric',
@ -202,9 +212,18 @@ export const pieVisualization: Visualization<PieVisualizationState> = {
}),
};
},
renderDimensionEditor(domElement, props) {
render(
<I18nProvider>
<DimensionEditor {...props} />
</I18nProvider>,
domElement
);
},
toExpression,
toPreviewExpression,
toExpression: (state, layers, attributes) =>
toExpression(state, layers, paletteService, attributes),
toPreviewExpression: (state, layers) => toPreviewExpression(state, layers, paletteService),
renderToolbar(domElement, props) {
render(
@ -214,4 +233,4 @@ export const pieVisualization: Visualization<PieVisualizationState> = {
domElement
);
},
};
});

Some files were not shown because too many files have changed in this diff Show more