[Lens] Configurable color syncing (#86180) (#86798)

This commit is contained in:
Joe Reuter 2020-12-23 09:22:09 +01:00 committed by GitHub
parent ce0c66aa0c
commit e0623903b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 574 additions and 560 deletions

View file

@ -17,5 +17,6 @@ export declare type EmbeddableInput = {
disabledActions?: string[];
disableTriggers?: boolean;
searchSessionId?: string;
syncColors?: boolean;
};
```

View file

@ -9,7 +9,7 @@ Constructs a new instance of the `ExpressionRenderHandler` class
<b>Signature:</b>
```typescript
constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams);
constructor(element: HTMLElement, { onRenderError, renderMode, syncColors, hasCompatibleActions, }?: ExpressionRenderHandlerParams);
```
## Parameters
@ -17,5 +17,5 @@ constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActi
| Parameter | Type | Description |
| --- | --- | --- |
| element | <code>HTMLElement</code> | |
| { onRenderError, renderMode, hasCompatibleActions, } | <code>ExpressionRenderHandlerParams</code> | |
| { onRenderError, renderMode, syncColors, hasCompatibleActions, } | <code>ExpressionRenderHandlerParams</code> | |

View file

@ -14,7 +14,7 @@ export declare class ExpressionRenderHandler
| Constructor | Modifiers | Description |
| --- | --- | --- |
| [(constructor)(element, { onRenderError, renderMode, hasCompatibleActions, })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the <code>ExpressionRenderHandler</code> class |
| [(constructor)(element, { onRenderError, renderMode, syncColors, hasCompatibleActions, })](./kibana-plugin-plugins-expressions-public.expressionrenderhandler._constructor_.md) | | Constructs a new instance of the <code>ExpressionRenderHandler</code> class |
## Properties

View file

@ -25,6 +25,7 @@ export interface IExpressionLoaderParams
| [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | <code>RenderMode</code> | |
| [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | <code>SerializableState</code> | |
| [searchSessionId](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md) | <code>string</code> | |
| [syncColors](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md) | <code>boolean</code> | |
| [uiState](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.uistate.md) | <code>unknown</code> | |
| [variables](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.variables.md) | <code>Record&lt;string, any&gt;</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) &gt; [syncColors](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.synccolors.md)
## IExpressionLoaderParams.syncColors property
<b>Signature:</b>
```typescript
syncColors?: boolean;
```

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.md) &gt; [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.issynccolorsenabled.md)
## IInterpreterRenderHandlers.isSyncColorsEnabled property
<b>Signature:</b>
```typescript
isSyncColorsEnabled: () => boolean;
```

View file

@ -18,6 +18,7 @@ export interface IInterpreterRenderHandlers
| [event](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.event.md) | <code>(event: any) =&gt; void</code> | |
| [getRenderMode](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.getrendermode.md) | <code>() =&gt; RenderMode</code> | |
| [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.hascompatibleactions.md) | <code>(event: any) =&gt; Promise&lt;boolean&gt;</code> | |
| [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.issynccolorsenabled.md) | <code>() =&gt; boolean</code> | |
| [onDestroy](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.ondestroy.md) | <code>(fn: () =&gt; void) =&gt; void</code> | |
| [reload](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.reload.md) | <code>() =&gt; void</code> | |
| [uiState](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.uistate.md) | <code>unknown</code> | This uiState interface is actually <code>PersistedState</code> from the visualizations plugin, but expressions cannot know about vis or it creates a mess of circular dependencies. Downstream consumers of the uiState handler will need to cast for now. |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-server](./kibana-plugin-plugins-expressions-server.md) &gt; [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md) &gt; [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.issynccolorsenabled.md)
## IInterpreterRenderHandlers.isSyncColorsEnabled property
<b>Signature:</b>
```typescript
isSyncColorsEnabled: () => boolean;
```

View file

@ -18,6 +18,7 @@ export interface IInterpreterRenderHandlers
| [event](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.event.md) | <code>(event: any) =&gt; void</code> | |
| [getRenderMode](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.getrendermode.md) | <code>() =&gt; RenderMode</code> | |
| [hasCompatibleActions](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.hascompatibleactions.md) | <code>(event: any) =&gt; Promise&lt;boolean&gt;</code> | |
| [isSyncColorsEnabled](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.issynccolorsenabled.md) | <code>() =&gt; boolean</code> | |
| [onDestroy](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.ondestroy.md) | <code>(fn: () =&gt; void) =&gt; void</code> | |
| [reload](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.reload.md) | <code>() =&gt; void</code> | |
| [uiState](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.uistate.md) | <code>unknown</code> | This uiState interface is actually <code>PersistedState</code> from the visualizations plugin, but expressions cannot know about vis or it creates a mess of circular dependencies. Downstream consumers of the uiState handler will need to cast for now. |

View file

@ -81,6 +81,21 @@ Put the dashboard in *Edit* mode, then use the following options:
* To delete, open the panel menu, then select *Delete from dashboard*. When you delete a panel from the dashboard, the
visualization or saved search from the panel is still available in Kibana.
[float]
[[sync-colors]]
=== Synchronize colors
By default, dashboard panels that share a non-gradient based color palette will synchronize their color assignment to improve readability.
Color assignment is based on the series name, and the total number of colors is based on the number of unique series names.
The color synchronizing logic can make the dashboard less readable when there are too many unique series names. It is possible to disable the synchronization behavior:
. Put the dashboard in *Edit* mode.
. Click the "Options" button in the top navigation bar.
. Disable "Sync color palettes across panels".
[float]
[[clone-panels]]
=== Clone panels

View file

@ -37,15 +37,15 @@ export class MappedColors {
private _mapping: any;
constructor(
private uiSettings: CoreSetup['uiSettings'],
private uiSettings?: CoreSetup['uiSettings'],
private colorPaletteFn: (num: number) => string[] = createColorPalette
) {
this._oldMap = {};
this._mapping = {};
}
private getConfigColorMapping() {
return _.mapValues(this.uiSettings.get(COLOR_MAPPING_SETTING), standardizeColor);
private getConfigColorMapping(): Record<string, string> {
return _.mapValues(this.uiSettings?.get(COLOR_MAPPING_SETTING) || {}, standardizeColor);
}
public get oldMap(): any {

View file

@ -18,9 +18,11 @@
*/
import { coreMock } from '../../../../../core/public/mocks';
import { createColorPalette as createLegacyColorPalette } from '../../../../../../src/plugins/charts/public';
import { PaletteDefinition } from './types';
import { buildPalettes } from './palettes';
import { colorsServiceMock } from '../legacy_colors/mock';
import { euiPaletteColorBlind, euiPaletteColorBlindBehindText } from '@elastic/eui';
describe('palettes', () => {
const palettes: Record<string, PaletteDefinition> = buildPalettes(
@ -28,79 +30,257 @@ describe('palettes', () => {
colorsServiceMock
);
describe('default palette', () => {
it('should return different colors based on behind text flag', () => {
const palette = palettes.default;
describe('syncColors: false', () => {
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(
[
const color1 = palette.getColor([
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
],
{
behindText: true,
}
);
expect(color1).not.toEqual(color2);
]);
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);
});
});
it('should return different colors based on rank at current series', () => {
const palette = palettes.default;
describe('syncColors: true', () => {
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: 1,
totalSeriesAtDepth: 5,
},
]);
expect(color1).not.toEqual(color2);
});
const color1 = palette.getColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
],
{
syncColors: true,
}
);
const color2 = palette.getColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
],
{
behindText: true,
syncColors: true,
}
);
expect(color1).not.toEqual(color2);
});
it('should return the same color for different positions on outer series layers', () => {
const palette = palettes.default;
it('should return different colors for different keys', () => {
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);
const color1 = palette.getColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
],
{
syncColors: true,
}
);
const color2 = palette.getColor(
[
{
name: 'def',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
],
{
syncColors: true,
}
);
expect(color1).not.toEqual(color2);
});
it('should return the same color for the same key, irregardless of rank', () => {
const palette = palettes.default;
const color1 = palette.getColor(
[
{
name: 'hij',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
],
{
syncColors: true,
}
);
const color2 = palette.getColor(
[
{
name: 'hij',
rankAtDepth: 5,
totalSeriesAtDepth: 5,
},
],
{
syncColors: true,
}
);
expect(color1).toEqual(color2);
});
it('should return the same color for different positions on outer series layers', () => {
const palette = palettes.default;
const color1 = palette.getColor(
[
{
name: 'klm',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
{
name: 'def',
rankAtDepth: 0,
totalSeriesAtDepth: 2,
},
],
{
syncColors: true,
}
);
const color2 = palette.getColor(
[
{
name: 'klm',
rankAtDepth: 3,
totalSeriesAtDepth: 5,
},
{
name: 'ghj',
rankAtDepth: 1,
totalSeriesAtDepth: 1,
},
],
{
syncColors: true,
}
);
expect(color1).toEqual(color2);
});
it('should return the same index of the behind text palette for same key', () => {
const palette = palettes.default;
const color1 = palette.getColor(
[
{
name: 'klm',
rankAtDepth: 0,
totalSeriesAtDepth: 5,
},
{
name: 'def',
rankAtDepth: 0,
totalSeriesAtDepth: 2,
},
],
{
syncColors: true,
}
);
const color2 = palette.getColor(
[
{
name: 'klm',
rankAtDepth: 3,
totalSeriesAtDepth: 5,
},
{
name: 'ghj',
rankAtDepth: 1,
totalSeriesAtDepth: 1,
},
],
{
syncColors: true,
behindText: true,
}
);
const color1Index = euiPaletteColorBlind({ rotations: 2 }).indexOf(color1!);
const color2Index = euiPaletteColorBlindBehindText({ rotations: 2 }).indexOf(color2!);
expect(color1Index).toEqual(color2Index);
});
});
});
@ -136,35 +316,87 @@ describe('palettes', () => {
(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');
describe('syncColors: false', () => {
it('should not query legacy color service', () => {
palette.getColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
],
{
syncColors: false,
}
);
expect(colorsServiceMock.mappedColors.mapKeys).not.toHaveBeenCalled();
expect(colorsServiceMock.mappedColors.get).not.toHaveBeenCalled();
});
it('should return a color from the legacy palette based on position of first series', () => {
const result = palette.getColor(
[
{
name: 'abc',
rankAtDepth: 2,
totalSeriesAtDepth: 10,
},
{
name: 'def',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
],
{
syncColors: false,
}
);
expect(result).toEqual(createLegacyColorPalette(20)[2]);
});
});
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('syncColors: true', () => {
it('should query legacy color service', () => {
palette.getColor(
[
{
name: 'abc',
rankAtDepth: 0,
totalSeriesAtDepth: 10,
},
],
{
syncColors: true,
}
);
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,
},
],
{
syncColors: true,
}
);
expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledTimes(1);
expect(colorsServiceMock.mappedColors.mapKeys).toHaveBeenCalledWith(['abc']);
expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledTimes(1);
expect(colorsServiceMock.mappedColors.get).toHaveBeenCalledWith('abc');
});
});
});

View file

@ -28,26 +28,45 @@ import {
euiPaletteNegative,
euiPalettePositive,
euiPaletteWarm,
euiPaletteColorBlindBehindText,
euiPaletteForStatus,
euiPaletteForTemperature,
euiPaletteComplimentary,
euiPaletteColorBlindBehindText,
} from '@elastic/eui';
import { ChartsPluginSetup } from '../../../../../../src/plugins/charts/public';
import { flatten, zip } from 'lodash';
import {
ChartsPluginSetup,
createColorPalette as createLegacyColorPalette,
} from '../../../../../../src/plugins/charts/public';
import { lightenColor } from './lighten_color';
import { ChartColorConfiguration, PaletteDefinition, SeriesLayer } from './types';
import { LegacyColorsService } from '../legacy_colors';
import { MappedColors } from '../mapped_colors';
function buildRoundRobinCategoricalWithMappedColors(): Omit<PaletteDefinition, 'title'> {
const colors = euiPaletteColorBlind({ rotations: 2 });
const behindTextColors = euiPaletteColorBlindBehindText({ rotations: 2 });
const behindTextColorMap: Record<string, string> = Object.fromEntries(
zip(colors, behindTextColors)
);
const mappedColors = new MappedColors(undefined, (num: number) => {
return flatten(new Array(Math.ceil(num / 10)).fill(colors)).map((color) => color.toLowerCase());
});
function getColor(
series: SeriesLayer[],
chartConfiguration: ChartColorConfiguration = { behindText: false }
) {
const outputColor = chartConfiguration.behindText
? behindTextColors[series[0].rankAtDepth % behindTextColors.length]
: colors[series[0].rankAtDepth % colors.length];
let outputColor: string;
if (chartConfiguration.syncColors) {
const colorKey = series[0].name;
mappedColors.mapKeys([colorKey]);
const mappedColor = mappedColors.get(colorKey);
outputColor = chartConfiguration.behindText ? behindTextColorMap[mappedColor] : mappedColor;
} else {
outputColor = chartConfiguration.behindText
? behindTextColors[series[0].rankAtDepth % behindTextColors.length]
: colors[series[0].rankAtDepth % colors.length];
}
if (!chartConfiguration.maxDepth || chartConfiguration.maxDepth === 1) {
return outputColor;
@ -115,9 +134,15 @@ function buildGradient(
function buildSyncedKibanaPalette(
colors: ChartsPluginSetup['legacyColors']
): Omit<PaletteDefinition, 'title'> {
const staticColors = createLegacyColorPalette(20);
function getColor(series: SeriesLayer[], chartConfiguration: ChartColorConfiguration = {}) {
colors.mappedColors.mapKeys([series[0].name]);
const outputColor = colors.mappedColors.get(series[0].name);
let outputColor: string;
if (chartConfiguration.syncColors) {
colors.mappedColors.mapKeys([series[0].name]);
outputColor = colors.mappedColors.get(series[0].name);
} else {
outputColor = staticColors[series[0].rankAtDepth % staticColors.length];
}
if (!chartConfiguration.maxDepth || chartConfiguration.maxDepth === 1) {
return outputColor;

View file

@ -55,6 +55,11 @@ export interface ChartColorConfiguration {
* adjust colors for better a11y. Might be ignored depending on the palette.
*/
behindText?: boolean;
/**
* Flag whether a color assignment to a given key should be remembered and re-used the next time the key shows up.
* This setting might be ignored based on the palette.
*/
syncColors?: boolean;
}
/**

View file

@ -151,6 +151,7 @@ export const getDashboardContainerInput = ({
description: dashboardStateManager.getDescription(),
id: dashboardStateManager.savedDashboard.id || '',
useMargins: dashboardStateManager.getUseMargins(),
syncColors: dashboardStateManager.getSyncColors(),
viewMode: dashboardStateManager.getViewMode(),
filters: query.filterManager.getFilters(),
query: dashboardStateManager.getQuery(),

View file

@ -68,6 +68,7 @@ describe('DashboardState', function () {
query: {} as DashboardContainerInput['query'],
timeRange: {} as DashboardContainerInput['timeRange'],
useMargins: true,
syncColors: false,
title: 'ultra awesome test dashboard',
isFullScreenMode: false,
panels: {} as DashboardContainerInput['panels'],

View file

@ -404,6 +404,15 @@ export class DashboardStateManager {
this.stateContainer.transitions.setOption('useMargins', useMargins);
}
public getSyncColors() {
// Existing dashboards that don't define this should default to true.
return this.appState.options.syncColors === undefined ? true : this.appState.options.syncColors;
}
public setSyncColors(syncColors: boolean) {
this.stateContainer.transitions.setOption('syncColors', syncColors);
}
public getHidePanelTitles() {
return this.appState.options.hidePanelTitles;
}

View file

@ -59,6 +59,7 @@ export interface DashboardContainerInput extends ContainerInput {
timeRange: TimeRange;
description?: string;
useMargins: boolean;
syncColors?: boolean;
viewMode: ViewMode;
filters: Filter[];
title: string;
@ -93,6 +94,7 @@ export interface InheritedChildInput extends IndexSignature {
hidePanelTitles?: boolean;
id: string;
searchSessionId?: string;
syncColors?: boolean;
}
export type DashboardReactContextValue = KibanaReactContextValue<DashboardContainerServices>;
@ -269,6 +271,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
hidePanelTitles,
filters,
searchSessionId,
syncColors,
} = this.input;
return {
filters,
@ -279,6 +282,7 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
viewMode,
id,
searchSessionId,
syncColors,
};
}
}

View file

@ -62,6 +62,7 @@ export class DashboardContainerFactoryDefinition
isEmbeddedExternally: false,
isFullScreenMode: false,
useMargins: true,
syncColors: true,
};
}

View file

@ -369,6 +369,10 @@ export function DashboardTopNav({
onUseMarginsChange: (isChecked: boolean) => {
dashboardStateManager.setUseMargins(isChecked);
},
syncColors: dashboardStateManager.getSyncColors(),
onSyncColorsChange: (isChecked: boolean) => {
dashboardStateManager.setSyncColors(isChecked);
},
hidePanelTitles: dashboardStateManager.getHidePanelTitles(),
onHidePanelTitlesChange: (isChecked: boolean) => {
dashboardStateManager.setHidePanelTitles(isChecked);

View file

@ -27,17 +27,21 @@ interface Props {
onUseMarginsChange: (useMargins: boolean) => void;
hidePanelTitles: boolean;
onHidePanelTitlesChange: (hideTitles: boolean) => void;
syncColors: boolean;
onSyncColorsChange: (syncColors: boolean) => void;
}
interface State {
useMargins: boolean;
hidePanelTitles: boolean;
syncColors: boolean;
}
export class OptionsMenu extends Component<Props, State> {
state = {
useMargins: this.props.useMargins,
hidePanelTitles: this.props.hidePanelTitles,
syncColors: this.props.syncColors,
};
constructor(props: Props) {
@ -56,6 +60,12 @@ export class OptionsMenu extends Component<Props, State> {
this.setState({ hidePanelTitles: isChecked });
};
handleSyncColorsChange = (evt: any) => {
const isChecked = evt.target.checked;
this.props.onSyncColorsChange(isChecked);
this.setState({ syncColors: isChecked });
};
render() {
return (
<EuiForm data-test-subj="dashboardOptionsMenu">
@ -80,6 +90,17 @@ export class OptionsMenu extends Component<Props, State> {
data-test-subj="dashboardPanelTitlesCheckbox"
/>
</EuiFormRow>
<EuiFormRow>
<EuiSwitch
label={i18n.translate('dashboard.topNav.options.syncColorsBetweenPanelsSwitchLabel', {
defaultMessage: 'Sync color palettes across panels',
})}
checked={this.state.syncColors}
onChange={this.handleSyncColorsChange}
data-test-subj="dashboardSyncColorsCheckbox"
/>
</EuiFormRow>
</EuiForm>
);
}

View file

@ -39,10 +39,14 @@ export function showOptionsPopover({
onUseMarginsChange,
hidePanelTitles,
onHidePanelTitlesChange,
syncColors,
onSyncColorsChange,
}: {
anchorElement: HTMLElement;
useMargins: boolean;
onUseMarginsChange: (useMargins: boolean) => void;
syncColors: boolean;
onSyncColorsChange: (syncColors: boolean) => void;
hidePanelTitles: boolean;
onHidePanelTitlesChange: (hideTitles: boolean) => void;
}) {
@ -62,6 +66,8 @@ export function showOptionsPopover({
onUseMarginsChange={onUseMarginsChange}
hidePanelTitles={hidePanelTitles}
onHidePanelTitlesChange={onHidePanelTitlesChange}
syncColors={syncColors}
onSyncColorsChange={onSyncColorsChange}
/>
</EuiWrappingPopover>
</I18nProvider>

View file

@ -78,6 +78,7 @@ export interface DashboardAppState {
options: {
hidePanelTitles: boolean;
useMargins: boolean;
syncColors?: boolean;
};
query: Query | string;
filters: Filter[];

View file

@ -55,6 +55,11 @@ export type EmbeddableInput = {
* Search session id to group searches
*/
searchSessionId?: string;
/**
* Flag whether colors should be synced with other panels
*/
syncColors?: boolean;
};
export interface PanelState<E extends EmbeddableInput & { id: string } = { id: string }> {

View file

@ -411,6 +411,7 @@ export type EmbeddableInput = {
disabledActions?: string[];
disableTriggers?: boolean;
searchSessionId?: string;
syncColors?: boolean;
};
// Warning: (ae-missing-release-tag) "EmbeddableInstanceConfiguration" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)

View file

@ -82,6 +82,7 @@ export interface IInterpreterRenderHandlers {
event: (event: any) => void;
hasCompatibleActions?: (event: any) => Promise<boolean>;
getRenderMode: () => RenderMode;
isSyncColorsEnabled: () => boolean;
/**
* This uiState interface is actually `PersistedState` from the visualizations plugin,
* but expressions cannot know about vis or it creates a mess of circular dependencies.

View file

@ -64,6 +64,7 @@ export class ExpressionLoader {
this.renderHandler = new ExpressionRenderHandler(element, {
onRenderError: params && params.onRenderError,
renderMode: params?.renderMode,
syncColors: params?.syncColors,
hasCompatibleActions: params?.hasCompatibleActions,
});
this.render$ = this.renderHandler.render$;

View file

@ -531,7 +531,7 @@ export interface ExpressionRenderError extends Error {
// @public (undocumented)
export class ExpressionRenderHandler {
// Warning: (ae-forgotten-export) The symbol "ExpressionRenderHandlerParams" needs to be exported by the entry point index.d.ts
constructor(element: HTMLElement, { onRenderError, renderMode, hasCompatibleActions, }?: ExpressionRenderHandlerParams);
constructor(element: HTMLElement, { onRenderError, renderMode, syncColors, hasCompatibleActions, }?: ExpressionRenderHandlerParams);
// (undocumented)
destroy: () => void;
// (undocumented)
@ -903,6 +903,8 @@ export interface IExpressionLoaderParams {
// (undocumented)
searchSessionId?: string;
// (undocumented)
syncColors?: boolean;
// (undocumented)
uiState?: unknown;
// (undocumented)
variables?: Record<string, any>;
@ -920,6 +922,8 @@ export interface IInterpreterRenderHandlers {
// (undocumented)
hasCompatibleActions?: (event: any) => Promise<boolean>;
// (undocumented)
isSyncColorsEnabled: () => boolean;
// (undocumented)
onDestroy: (fn: () => void) => void;
// (undocumented)
reload: () => void;

View file

@ -170,7 +170,12 @@ export const ReactExpressionRenderer = ({
errorRenderHandlerRef.current = null;
};
}, [hasCustomRenderErrorHandler, onEvent]);
}, [
hasCustomRenderErrorHandler,
onEvent,
expressionLoaderOptions.renderMode,
expressionLoaderOptions.syncColors,
]);
useEffect(() => {
const subscription = reload$?.subscribe(() => {

View file

@ -31,6 +31,7 @@ export type IExpressionRendererExtraHandlers = Record<string, any>;
export interface ExpressionRenderHandlerParams {
onRenderError?: RenderErrorHandlerFnType;
renderMode?: RenderMode;
syncColors?: boolean;
hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise<boolean>;
}
@ -63,6 +64,7 @@ export class ExpressionRenderHandler {
{
onRenderError,
renderMode,
syncColors,
hasCompatibleActions = async () => false,
}: ExpressionRenderHandlerParams = {}
) {
@ -101,6 +103,9 @@ export class ExpressionRenderHandler {
getRenderMode: () => {
return renderMode || 'display';
},
isSyncColorsEnabled: () => {
return syncColors || false;
},
hasCompatibleActions,
};
}

View file

@ -57,6 +57,7 @@ export interface IExpressionLoaderParams {
onRenderError?: RenderErrorHandlerFnType;
searchSessionId?: string;
renderMode?: RenderMode;
syncColors?: boolean;
hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions'];
}

View file

@ -737,6 +737,8 @@ export interface IInterpreterRenderHandlers {
// (undocumented)
hasCompatibleActions?: (event: any) => Promise<boolean>;
// (undocumented)
isSyncColorsEnabled: () => boolean;
// (undocumented)
onDestroy: (fn: () => void) => void;
// (undocumented)
reload: () => void;

View file

@ -12,6 +12,7 @@ export const defaultHandlers: RendererHandlers = {
getElementId: () => 'element-id',
getFilter: () => 'filter',
getRenderMode: () => 'display',
isSyncColorsEnabled: () => false,
onComplete: (fn) => undefined,
onEmbeddableDestroyed: action('onEmbeddableDestroyed'),
onEmbeddableInputChange: action('onEmbeddableInputChange'),

View file

@ -26,6 +26,9 @@ export const createHandlers = (): RendererHandlers => ({
getRenderMode() {
return 'display';
},
isSyncColorsEnabled() {
return false;
},
onComplete(fn: () => void) {
this.done = fn;
},

View file

@ -260,6 +260,7 @@ export class Embeddable
handleEvent={this.handleEvent}
onData$={this.updateActiveData}
renderMode={input.renderMode}
syncColors={input.syncColors}
hasCompatibleActions={this.hasCompatibleActions}
/>,
domNode

View file

@ -29,6 +29,7 @@ export interface ExpressionWrapperProps {
inspectorAdapters?: Partial<DefaultInspectorAdapters> | undefined
) => void;
renderMode?: RenderMode;
syncColors?: boolean;
hasCompatibleActions?: ReactExpressionRendererProps['hasCompatibleActions'];
}
@ -41,6 +42,7 @@ export function ExpressionWrapper({
searchSessionId,
onData$,
renderMode,
syncColors,
hasCompatibleActions,
}: ExpressionWrapperProps) {
return (
@ -70,6 +72,7 @@ export function ExpressionWrapper({
searchSessionId={searchSessionId}
onData$={onData$}
renderMode={renderMode}
syncColors={syncColors}
renderError={(errorMessage, error) => (
<div data-test-subj="expression-renderer-error">
<EuiFlexGroup direction="column" alignItems="center" justifyContent="center">

View file

@ -140,6 +140,7 @@ export const getPieRenderer = (dependencies: {
paletteService={dependencies.paletteService}
onClickValue={onClickValue}
renderMode={handlers.getRenderMode()}
syncColors={handlers.isSyncColorsEnabled()}
/>
</I18nProvider>,
domNode,

View file

@ -71,6 +71,7 @@ describe('PieVisualization component', () => {
chartsThemeService,
paletteService: chartPluginMock.createPaletteRegistry(),
renderMode: 'display' as const,
syncColors: false,
};
}
@ -172,6 +173,7 @@ describe('PieVisualization component', () => {
{
maxDepth: 2,
totalSeries: 5,
syncColors: false,
behindText: true,
},
undefined

View file

@ -47,12 +47,13 @@ export function PieComponent(
paletteService: PaletteRegistry;
onClickValue: (data: LensFilterEvent['data']) => void;
renderMode: RenderMode;
syncColors: boolean;
}
) {
const [firstTable] = Object.values(props.data.tables);
const formatters: Record<string, ReturnType<FormatFactory>> = {};
const { chartsThemeService, paletteService, onClickValue } = props;
const { chartsThemeService, paletteService, syncColors, onClickValue } = props;
const {
shape,
groups,
@ -145,6 +146,7 @@ export function PieComponent(
behindText: categoryDisplay !== 'hide',
maxDepth: bucketColumns.length,
totalSeries: totalSeriesCount,
syncColors,
},
palette.params
);

View file

@ -18,7 +18,13 @@ import {
Fit,
} from '@elastic/charts';
import { PaletteOutput } from 'src/plugins/charts/public';
import { calculateMinInterval, xyChart, XYChart, XYChartProps } from './expression';
import {
calculateMinInterval,
xyChart,
XYChart,
XYChartProps,
XYChartRenderProps,
} from './expression';
import { LensMultiTable } from '../types';
import { Datatable, DatatableRow } from '../../../../../src/plugins/expressions/public';
import React from 'react';
@ -382,6 +388,7 @@ describe('xy_expression', () => {
describe('XYChart component', () => {
let getFormatSpy: jest.Mock;
let convertSpy: jest.Mock;
let defaultProps: Omit<XYChartRenderProps, 'data' | 'args'>;
const dataWithoutFormats: LensMultiTable = {
type: 'lens_multitable',
@ -421,26 +428,25 @@ describe('xy_expression', () => {
};
const getRenderedComponent = (data: LensMultiTable, args: XYArgs) => {
return shallow(
<XYChart
data={data}
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
return shallow(<XYChart {...defaultProps} data={data} args={args} />);
};
beforeEach(() => {
convertSpy = jest.fn((x) => x);
getFormatSpy = jest.fn();
getFormatSpy.mockReturnValue({ convert: convertSpy });
defaultProps = {
formatFactory: getFormatSpy,
timeZone: 'UTC',
renderMode: 'display',
chartsThemeService,
paletteService,
minInterval: 50,
onClickValue,
onSelectRange,
syncColors: false,
};
});
test('it renders line', () => {
@ -448,16 +454,9 @@ describe('xy_expression', () => {
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'line' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -493,6 +492,7 @@ describe('xy_expression', () => {
const component = shallow(
<XYChart
{...defaultProps}
data={{
...data,
dateRange: {
@ -504,14 +504,7 @@ describe('xy_expression', () => {
...args,
layers: [{ ...args.layers[0], seriesType: 'line', xScaleType: 'time' }],
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={undefined}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(Settings).prop('xDomain')).toMatchInlineSnapshot(`
@ -534,6 +527,7 @@ describe('xy_expression', () => {
const component = shallow(
<XYChart
{...defaultProps}
data={{
...data,
dateRange: {
@ -542,14 +536,6 @@ describe('xy_expression', () => {
},
}}
args={multiLayerArgs}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -569,6 +555,7 @@ describe('xy_expression', () => {
const component = shallow(
<XYChart
{...defaultProps}
data={{
...data,
dateRange: {
@ -580,14 +567,6 @@ describe('xy_expression', () => {
...args,
layers: [{ ...args.layers[0], seriesType: 'line', xScaleType: 'linear' }],
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(Settings).prop('xDomain')).toBeUndefined();
@ -597,16 +576,9 @@ describe('xy_expression', () => {
const { data, args } = sampleArgs();
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -619,16 +591,9 @@ describe('xy_expression', () => {
const { data, args } = sampleArgs();
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -641,16 +606,9 @@ describe('xy_expression', () => {
const { data, args } = sampleArgs();
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_horizontal' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -666,20 +624,7 @@ describe('xy_expression', () => {
// send empty data to the chart
data.tables.first.rows = [];
const component = shallow(
<XYChart
data={data}
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
const component = shallow(<XYChart {...defaultProps} data={data} args={args} />);
expect(component.find(BarSeries)).toHaveLength(0);
expect(component.find(EmptyPlaceholder).prop('icon')).toBeDefined();
@ -690,19 +635,12 @@ describe('xy_expression', () => {
const wrapper = mountWithIntl(
<XYChart
{...defaultProps}
data={dateHistogramData}
args={{
...args,
layers: [dateHistogramLayer],
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -776,19 +714,12 @@ describe('xy_expression', () => {
const wrapper = mountWithIntl(
<XYChart
{...defaultProps}
data={numberHistogramData}
args={{
...args,
layers: [numberLayer],
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -806,18 +737,7 @@ describe('xy_expression', () => {
const { args, data } = sampleArgs();
const wrapper = mountWithIntl(
<XYChart
data={data}
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="noInteractivity"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
<XYChart {...defaultProps} data={data} args={args} renderMode="noInteractivity" />
);
expect(wrapper.find(Settings).first().prop('onBrushEnd')).toBeUndefined();
@ -837,6 +757,7 @@ describe('xy_expression', () => {
const wrapper = mountWithIntl(
<XYChart
{...defaultProps}
data={data}
args={{
...args,
@ -855,14 +776,6 @@ describe('xy_expression', () => {
},
],
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -892,18 +805,7 @@ describe('xy_expression', () => {
const { args, data } = sampleArgs();
const wrapper = mountWithIntl(
<XYChart
data={data}
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="noInteractivity"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
<XYChart {...defaultProps} data={data} args={args} renderMode="noInteractivity" />
);
expect(wrapper.find(Settings).first().prop('onElementClick')).toBeUndefined();
@ -913,16 +815,9 @@ describe('xy_expression', () => {
const { data, args } = sampleArgs();
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_stacked' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -935,16 +830,9 @@ describe('xy_expression', () => {
const { data, args } = sampleArgs();
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area_stacked' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -957,19 +845,12 @@ describe('xy_expression', () => {
const { data, args } = sampleArgs();
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{
...args,
layers: [{ ...args.layers[0], seriesType: 'bar_horizontal_stacked' }],
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component).toMatchSnapshot();
@ -984,6 +865,7 @@ describe('xy_expression', () => {
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{
...args,
@ -996,14 +878,6 @@ describe('xy_expression', () => {
},
],
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -1014,18 +888,7 @@ describe('xy_expression', () => {
test('it passes time zone to the series', () => {
const { data, args } = sampleArgs();
const component = shallow(
<XYChart
data={data}
args={args}
formatFactory={getFormatSpy}
timeZone="CEST"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
<XYChart {...defaultProps} data={data} args={args} timeZone="CEST" />
);
expect(component.find(LineSeries).at(0).prop('timeZone')).toEqual('CEST');
expect(component.find(LineSeries).at(1).prop('timeZone')).toEqual('CEST');
@ -1041,18 +904,7 @@ describe('xy_expression', () => {
};
delete firstLayer.splitAccessor;
const component = shallow(
<XYChart
data={data}
args={{ ...args, layers: [firstLayer] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
<XYChart {...defaultProps} data={data} args={{ ...args, layers: [firstLayer] }} />
);
expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(true);
});
@ -1062,18 +914,7 @@ describe('xy_expression', () => {
const firstLayer: LayerArgs = { ...args.layers[0], seriesType: 'bar', isHistogram: true };
delete firstLayer.splitAccessor;
const component = shallow(
<XYChart
data={data}
args={{ ...args, layers: [firstLayer] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
<XYChart {...defaultProps} data={data} args={{ ...args, layers: [firstLayer] }} />
);
expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(false);
expect(component.find(BarSeries).at(1).prop('enableHistogramMode')).toEqual(false);
@ -1087,16 +928,9 @@ describe('xy_expression', () => {
delete secondLayer.splitAccessor;
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{ ...args, layers: [firstLayer, secondLayer] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(LineSeries).at(0).prop('enableHistogramMode')).toEqual(true);
@ -1107,6 +941,7 @@ describe('xy_expression', () => {
const { data, args } = sampleArgs();
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{
...args,
@ -1118,14 +953,6 @@ describe('xy_expression', () => {
},
],
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(true);
@ -1136,19 +963,12 @@ describe('xy_expression', () => {
const { data, args } = sampleArgs();
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{
...args,
layers: [{ ...args.layers[0], seriesType: 'bar', isHistogram: true }],
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(BarSeries).at(0).prop('enableHistogramMode')).toEqual(false);
@ -1541,16 +1361,9 @@ describe('xy_expression', () => {
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{ ...args, layers: [{ ...args.layers[0], xScaleType: 'ordinal' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(LineSeries).at(0).prop('xScaleType')).toEqual(ScaleType.Ordinal);
@ -1562,16 +1375,9 @@ describe('xy_expression', () => {
const component = shallow(
<XYChart
{...defaultProps}
data={data}
args={{ ...args, layers: [{ ...args.layers[0], yScaleType: 'sqrt' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(component.find(LineSeries).at(0).prop('yScaleType')).toEqual(ScaleType.Sqrt);
@ -1581,20 +1387,7 @@ describe('xy_expression', () => {
test('it gets the formatter for the x axis', () => {
const { data, args } = sampleArgs();
shallow(
<XYChart
data={{ ...data }}
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
shallow(<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />);
expect(getFormatSpy).toHaveBeenCalledWith({ id: 'string' });
});
@ -1604,16 +1397,9 @@ describe('xy_expression', () => {
shallow(
<XYChart
{...defaultProps}
data={{ ...data }}
args={{ ...args, layers: [{ ...args.layers[0], accessors: ['a'] }] }}
formatFactory={getFormatSpy}
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
timeZone="UTC"
renderMode="display"
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
expect(getFormatSpy).toHaveBeenCalledWith({
@ -1625,20 +1411,7 @@ describe('xy_expression', () => {
test('it should pass the formatter function to the axis', () => {
const { data, args } = sampleArgs();
const instance = shallow(
<XYChart
data={{ ...data }}
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
const instance = shallow(<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />);
const tickFormatter = instance.find(Axis).first().prop('tickFormat');
@ -1661,20 +1434,7 @@ describe('xy_expression', () => {
type: 'lens_xy_tickLabelsConfig',
};
const instance = shallow(
<XYChart
data={{ ...data }}
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
const instance = shallow(<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />);
const axisStyle = instance.find(Axis).first().prop('style');
@ -1695,20 +1455,7 @@ describe('xy_expression', () => {
type: 'lens_xy_tickLabelsConfig',
};
const instance = shallow(
<XYChart
data={{ ...data }}
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
const instance = shallow(<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />);
const axisStyle = instance.find(Axis).at(1).prop('style');
@ -1729,20 +1476,7 @@ describe('xy_expression', () => {
type: 'lens_xy_tickLabelsConfig',
};
const instance = shallow(
<XYChart
data={{ ...data }}
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
const instance = shallow(<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />);
const axisStyle = instance.find(Axis).first().prop('style');
@ -1763,20 +1497,7 @@ describe('xy_expression', () => {
type: 'lens_xy_tickLabelsConfig',
};
const instance = shallow(
<XYChart
data={{ ...data }}
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
const instance = shallow(<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />);
const axisStyle = instance.find(Axis).at(1).prop('style');
@ -1864,20 +1585,7 @@ describe('xy_expression', () => {
],
};
const component = shallow(
<XYChart
data={data}
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
const component = shallow(<XYChart {...defaultProps} data={data} args={args} />);
const series = component.find(LineSeries);
@ -1939,20 +1647,7 @@ describe('xy_expression', () => {
],
};
const component = shallow(
<XYChart
data={data}
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
const component = shallow(<XYChart {...defaultProps} data={data} args={args} />);
const series = component.find(LineSeries);
@ -2012,20 +1707,7 @@ describe('xy_expression', () => {
],
};
const component = shallow(
<XYChart
data={data}
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
const component = shallow(<XYChart {...defaultProps} data={data} args={args} />);
expect(component.find(Settings).prop('showLegend')).toEqual(true);
});
@ -2035,20 +1717,13 @@ describe('xy_expression', () => {
const component = shallow(
<XYChart
{...defaultProps}
data={{ ...data }}
args={{
...args,
layers: [{ ...args.layers[0], accessors: ['a'], splitAccessor: undefined }],
legend: { ...args.legend, isVisible: true, showSingleSeries: true },
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -2060,19 +1735,12 @@ describe('xy_expression', () => {
const component = shallow(
<XYChart
{...defaultProps}
data={{ ...data }}
args={{
...args,
legend: { ...args.legend, isVisible: false },
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -2084,19 +1752,12 @@ describe('xy_expression', () => {
const component = shallow(
<XYChart
{...defaultProps}
data={{ ...data }}
args={{
...args,
legend: { ...args.legend, position: 'top' },
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -2123,16 +1784,9 @@ describe('xy_expression', () => {
const component = shallow(
<XYChart
{...defaultProps}
data={{ ...data }}
args={{ ...args, fittingFunction: 'Carry' }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
);
@ -2150,18 +1804,7 @@ describe('xy_expression', () => {
args.layers[0].accessors = ['a'];
const component = shallow(
<XYChart
data={{ ...data }}
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />
);
expect(component.find(LineSeries).prop('fit')).toEqual({ type: Fit.None });
@ -2173,18 +1816,7 @@ describe('xy_expression', () => {
args.xTitle = 'My custom x-axis title';
const component = shallow(
<XYChart
data={{ ...data }}
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />
);
expect(component.find(Axis).at(0).prop('title')).toEqual('My custom x-axis title');
@ -2201,18 +1833,7 @@ describe('xy_expression', () => {
};
const component = shallow(
<XYChart
data={{ ...data }}
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />
);
const axisStyle = component.find(Axis).first().prop('style');
@ -2235,18 +1856,7 @@ describe('xy_expression', () => {
};
const component = shallow(
<XYChart
data={{ ...data }}
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
<XYChart {...defaultProps} data={{ ...data }} args={{ ...args }} />
);
expect(component.find(Axis).at(0).prop('gridLine')).toMatchObject({

View file

@ -76,7 +76,7 @@ export interface XYRender {
value: XYChartProps;
}
type XYChartRenderProps = XYChartProps & {
export type XYChartRenderProps = XYChartProps & {
chartsThemeService: ChartsPluginSetup['theme'];
paletteService: PaletteRegistry;
formatFactory: FormatFactory;
@ -85,6 +85,7 @@ type XYChartRenderProps = XYChartProps & {
onClickValue: (data: LensFilterEvent['data']) => void;
onSelectRange: (data: LensBrushEvent['data']) => void;
renderMode: RenderMode;
syncColors: boolean;
};
export const xyChart: ExpressionFunctionDefinition<
@ -240,6 +241,7 @@ export const getXyChartRenderer = (dependencies: {
onClickValue={onClickValue}
onSelectRange={onSelectRange}
renderMode={handlers.getRenderMode()}
syncColors={handlers.isSyncColorsEnabled()}
/>
</I18nProvider>,
domNode,
@ -309,6 +311,7 @@ export function XYChart({
onClickValue,
onSelectRange,
renderMode,
syncColors,
}: XYChartRenderProps) {
const { legend, layers, fittingFunction, gridlinesVisibilitySettings, valueLabels } = args;
const chartTheme = chartsThemeService.useChartsTheme();
@ -681,6 +684,7 @@ export function XYChart({
maxDepth: 1,
behindText: false,
totalSeries: colorAssignment.totalSeriesCount,
syncColors,
},
palette.params
);