mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Lens] Categorical color palettes (#75309)
This commit is contained in:
parent
7abb1e3033
commit
fe3b0538ff
123 changed files with 2942 additions and 776 deletions
33
src/plugins/charts/common/constants.ts
Normal file
33
src/plugins/charts/common/constants.ts
Normal 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',
|
||||
];
|
|
@ -18,3 +18,5 @@
|
|||
*/
|
||||
|
||||
export const COLOR_MAPPING_SETTING = 'visualization:colorMapping';
|
||||
export * from './palette';
|
||||
export * from './constants';
|
||||
|
|
102
src/plugins/charts/common/palette.test.ts
Normal file
102
src/plugins/charts/common/palette.test.ts
Normal 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');
|
||||
});
|
||||
});
|
160
src/plugins/charts/common/palette.ts
Normal file
160
src/plugins/charts/common/palette.ts
Normal 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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -3,5 +3,6 @@
|
|||
"version": "kibana",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["expressions"],
|
||||
"requiredBundles": ["visDefaultEditor"]
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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!,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,5 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { ColorsService } from './colors';
|
||||
export { LegacyColorsService } from './legacy_colors';
|
||||
export { ThemeService } from './theme';
|
||||
|
|
|
@ -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();
|
||||
});
|
|
@ -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;
|
|
@ -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;
|
20
src/plugins/charts/public/services/legacy_colors/index.ts
Normal file
20
src/plugins/charts/public/services/legacy_colors/index.ts
Normal 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';
|
|
@ -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;
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { ColorsService } from './colors';
|
||||
export * from './mapped_colors';
|
|
@ -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
|
|
@ -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) {
|
|
@ -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);
|
||||
});
|
||||
});
|
37
src/plugins/charts/public/services/palettes/lighten_color.ts
Normal file
37
src/plugins/charts/public/services/palettes/lighten_color.ts
Normal 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();
|
||||
}
|
58
src/plugins/charts/public/services/palettes/mock.ts
Normal file
58
src/plugins/charts/public/services/palettes/mock.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
261
src/plugins/charts/public/services/palettes/palettes.test.tsx
Normal file
261
src/plugins/charts/public/services/palettes/palettes.test.tsx
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
240
src/plugins/charts/public/services/palettes/palettes.tsx
Normal file
240
src/plugins/charts/public/services/palettes/palettes.tsx
Normal 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>,
|
||||
};
|
||||
};
|
56
src/plugins/charts/public/services/palettes/service.ts
Normal file
56
src/plugins/charts/public/services/palettes/service.ts
Normal 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!);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
118
src/plugins/charts/public/services/palettes/types.ts
Normal file
118
src/plugins/charts/public/services/palettes/types.ts
Normal 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>>;
|
||||
}
|
21
src/plugins/charts/public/static/colors/index.ts
Normal file
21
src/plugins/charts/public/static/colors/index.ts
Normal 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';
|
|
@ -18,4 +18,5 @@
|
|||
*/
|
||||
|
||||
export * from './color_maps';
|
||||
export * from './colors';
|
||||
export * from './components';
|
||||
|
|
|
@ -18,5 +18,12 @@
|
|||
*/
|
||||
|
||||
import { ChartsServerPlugin } from './plugin';
|
||||
export {
|
||||
PaletteOutput,
|
||||
CustomPaletteArguments,
|
||||
CustomPaletteState,
|
||||
SystemPaletteArguments,
|
||||
paletteIds,
|
||||
} from '../common';
|
||||
|
||||
export const plugin = () => new ChartsServerPlugin();
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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));
|
||||
},
|
||||
};
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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(' ');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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';
|
||||
|
|
|
@ -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?',
|
||||
}),
|
||||
},
|
||||
};
|
|
@ -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.',
|
||||
}),
|
||||
|
|
|
@ -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.',
|
||||
}),
|
||||
|
|
|
@ -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`,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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
|
206
x-pack/plugins/canvas/public/functions/pie.ts
Normal file
206
x-pack/plugins/canvas/public/functions/pie.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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
|
|
@ -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 = {
|
|
@ -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 {
|
|
@ -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', () => {
|
|
@ -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
|
|
@ -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', () => {
|
|
@ -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 = {
|
177
x-pack/plugins/canvas/public/functions/plot/index.ts
Normal file
177
x-pack/plugins/canvas/public/functions/plot/index.ts
Normal 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));
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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', () => {
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { SeriesStyle } from '../../../../types';
|
||||
import { SeriesStyle } from '../../../types';
|
||||
|
||||
export const seriesStyleToFlot = (seriesStyle: SeriesStyle) => {
|
||||
if (!seriesStyle) {
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"ui": true,
|
||||
"requiredPlugins": [
|
||||
"data",
|
||||
"charts",
|
||||
"expressions",
|
||||
"navigation",
|
||||
"urlForwarding",
|
||||
|
|
|
@ -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: {},
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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}
|
||||
/>,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -22,6 +22,10 @@ export interface Document {
|
|||
datasourceStates: Record<string, unknown>;
|
||||
visualization: unknown;
|
||||
query: Query;
|
||||
globalPalette?: {
|
||||
activePaletteId: string;
|
||||
state?: unknown;
|
||||
};
|
||||
filters: PersistableFilter[];
|
||||
};
|
||||
references: SavedObjectReference[];
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()} />
|
||||
|
|
|
@ -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)';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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]
|
||||
? {
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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 });
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue