Expression: Add render mode and use it for canvas interactivity (#83559)

This commit is contained in:
Joe Reuter 2020-11-24 18:42:02 +01:00 committed by GitHub
parent 5bc4d75256
commit 38a09b99c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 344 additions and 96 deletions

View file

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

View file

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

View file

@ -21,6 +21,7 @@ export interface IExpressionLoaderParams
| [disableCaching](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.disablecaching.md) | <code>boolean</code> | |
| [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | <code>Adapters</code> | |
| [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | <code>RenderErrorHandlerFnType</code> | |
| [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> | |
| [uiState](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.uistate.md) | <code>unknown</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; [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md)
## IExpressionLoaderParams.renderMode property
<b>Signature:</b>
```typescript
renderMode?: RenderMode;
```

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; [getRenderMode](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.getrendermode.md)
## IInterpreterRenderHandlers.getRenderMode property
<b>Signature:</b>
```typescript
getRenderMode: () => RenderMode;
```

View file

@ -16,6 +16,7 @@ export interface IInterpreterRenderHandlers
| --- | --- | --- |
| [done](./kibana-plugin-plugins-expressions-public.iinterpreterrenderhandlers.done.md) | <code>() =&gt; void</code> | Done increments the number of rendering successes |
| [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> | |
| [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>PersistedState</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-server](./kibana-plugin-plugins-expressions-server.md) &gt; [IInterpreterRenderHandlers](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.md) &gt; [getRenderMode](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.getrendermode.md)
## IInterpreterRenderHandlers.getRenderMode property
<b>Signature:</b>
```typescript
getRenderMode: () => RenderMode;
```

View file

@ -16,6 +16,7 @@ export interface IInterpreterRenderHandlers
| --- | --- | --- |
| [done](./kibana-plugin-plugins-expressions-server.iinterpreterrenderhandlers.done.md) | <code>() =&gt; void</code> | Done increments the number of rendering successes |
| [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> | |
| [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>PersistedState</code> | |

View file

@ -61,6 +61,18 @@ export interface ExpressionRenderDefinition<Config = unknown> {
export type AnyExpressionRenderDefinition = ExpressionRenderDefinition<any>;
/**
* Mode of the expression render environment.
* This value can be set from a consumer embedding an expression renderer and is accessible
* from within the active render function as part of the handlers.
* The following modes are supported:
* * display (default): The chart is rendered in a container with the main purpose of viewing the chart (e.g. in a container like dashboard or canvas)
* * preview: The chart is rendered in very restricted space (below 100px width and height) and should only show a rough outline
* * edit: The chart is rendered within an editor and configuration elements within the chart should be displayed
* * noInteractivity: The chart is rendered in a non-interactive environment and should not provide any affordances for interaction like brushing
*/
export type RenderMode = 'noInteractivity' | 'edit' | 'preview' | 'display';
export interface IInterpreterRenderHandlers {
/**
* Done increments the number of rendering successes
@ -70,5 +82,6 @@ export interface IInterpreterRenderHandlers {
reload: () => void;
update: (params: any) => void;
event: (event: any) => void;
getRenderMode: () => RenderMode;
uiState?: PersistedState;
}

View file

@ -20,17 +20,24 @@
import { first, skip, toArray } from 'rxjs/operators';
import { loader, ExpressionLoader } from './loader';
import { Observable } from 'rxjs';
import { parseExpression, IInterpreterRenderHandlers } from '../common';
import {
parseExpression,
IInterpreterRenderHandlers,
RenderMode,
AnyExpressionFunctionDefinition,
} from '../common';
// eslint-disable-next-line
const { __getLastExecution } = require('./services');
const { __getLastExecution, __getLastRenderMode } = require('./services');
const element: HTMLElement = null as any;
jest.mock('./services', () => {
let renderMode: RenderMode | undefined;
const renderers: Record<string, unknown> = {
test: {
render: (el: HTMLElement, value: unknown, handlers: IInterpreterRenderHandlers) => {
renderMode = handlers.getRenderMode();
handlers.done();
},
},
@ -39,9 +46,18 @@ jest.mock('./services', () => {
// eslint-disable-next-line
const service = new (require('../common/service/expressions_services').ExpressionsService as any)();
const testFn: AnyExpressionFunctionDefinition = {
fn: () => ({ type: 'render', as: 'test' }),
name: 'testrender',
args: {},
help: '',
};
service.registerFunction(testFn);
const moduleMock = {
__execution: undefined,
__getLastExecution: () => moduleMock.__execution,
__getLastRenderMode: () => renderMode,
getRenderersRegistry: () => ({
get: (id: string) => renderers[id],
}),
@ -130,6 +146,14 @@ describe('ExpressionLoader', () => {
expect(response).toBe(2);
});
it('passes mode to the renderer', async () => {
const expressionLoader = new ExpressionLoader(element, 'testrender', {
renderMode: 'edit',
});
await expressionLoader.render$.pipe(first()).toPromise();
expect(__getLastRenderMode()).toEqual('edit');
});
it('cancels the previous request when the expression is updated', () => {
const expressionLoader = new ExpressionLoader(element, 'var foo', {});
const execution = __getLastExecution();

View file

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

View file

@ -530,7 +530,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 }?: Partial<ExpressionRenderHandlerParams>);
constructor(element: HTMLElement, { onRenderError, renderMode }?: Partial<ExpressionRenderHandlerParams>);
// (undocumented)
destroy: () => void;
// (undocumented)
@ -891,6 +891,10 @@ export interface IExpressionLoaderParams {
//
// (undocumented)
onRenderError?: RenderErrorHandlerFnType;
// Warning: (ae-forgotten-export) The symbol "RenderMode" needs to be exported by the entry point index.d.ts
//
// (undocumented)
renderMode?: RenderMode;
// (undocumented)
searchContext?: SerializableState_2;
// (undocumented)
@ -909,6 +913,8 @@ export interface IInterpreterRenderHandlers {
// (undocumented)
event: (event: any) => void;
// (undocumented)
getRenderMode: () => RenderMode;
// (undocumented)
onDestroy: (fn: () => void) => void;
// (undocumented)
reload: () => void;

View file

@ -22,7 +22,7 @@ import { Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
import { ExpressionRenderError, RenderErrorHandlerFnType, IExpressionLoaderParams } from './types';
import { renderErrorHandler as defaultRenderErrorHandler } from './render_error_handler';
import { IInterpreterRenderHandlers, ExpressionAstExpression } from '../common';
import { IInterpreterRenderHandlers, ExpressionAstExpression, RenderMode } from '../common';
import { getRenderersRegistry } from './services';
@ -30,6 +30,7 @@ export type IExpressionRendererExtraHandlers = Record<string, any>;
export interface ExpressionRenderHandlerParams {
onRenderError: RenderErrorHandlerFnType;
renderMode: RenderMode;
}
export interface ExpressionRendererEvent {
@ -58,7 +59,7 @@ export class ExpressionRenderHandler {
constructor(
element: HTMLElement,
{ onRenderError }: Partial<ExpressionRenderHandlerParams> = {}
{ onRenderError, renderMode }: Partial<ExpressionRenderHandlerParams> = {}
) {
this.element = element;
@ -92,6 +93,9 @@ export class ExpressionRenderHandler {
event: (data) => {
this.eventsSubject.next(data);
},
getRenderMode: () => {
return renderMode || 'display';
},
};
}

View file

@ -23,6 +23,7 @@ import {
ExpressionValue,
ExpressionsService,
SerializableState,
RenderMode,
} from '../../common';
/**
@ -54,6 +55,7 @@ export interface IExpressionLoaderParams {
inspectorAdapters?: Adapters;
onRenderError?: RenderErrorHandlerFnType;
searchSessionId?: string;
renderMode?: RenderMode;
}
export interface ExpressionRenderError extends Error {

View file

@ -729,6 +729,10 @@ export interface IInterpreterRenderHandlers {
done: () => void;
// (undocumented)
event: (event: any) => void;
// Warning: (ae-forgotten-export) The symbol "RenderMode" needs to be exported by the entry point index.d.ts
//
// (undocumented)
getRenderMode: () => RenderMode;
// (undocumented)
onDestroy: (fn: () => void) => void;
// (undocumented)

View file

@ -83,6 +83,7 @@ export function savedLens(): ExpressionFunctionDefinition<
title: args.title === null ? undefined : args.title,
disableTriggers: true,
palette: args.palette,
renderMode: 'noInteractivity',
},
embeddableType: EmbeddableTypes.lens,
generatedAt: Date.now(),

View file

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

View file

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

View file

@ -262,6 +262,45 @@ describe('embeddable', () => {
expect(expressionRenderer.mock.calls[0][0].searchSessionId).toBe(input.searchSessionId);
});
it('should pass render mode to expression', async () => {
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
const query: Query = { language: 'kquery', query: '' };
const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }];
const input = {
savedObjectId: '123',
timeRange,
query,
filters,
renderMode: 'noInteractivity',
} as LensEmbeddableInput;
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
}),
},
input
);
await embeddable.initializeSavedVis(input);
embeddable.render(mountpoint);
expect(expressionRenderer.mock.calls[0][0].renderMode).toEqual('noInteractivity');
});
it('should merge external context with query and filters of the saved object', async () => {
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
const query: Query = { language: 'kquery', query: 'external filter' };

View file

@ -20,6 +20,7 @@ import { PaletteOutput } from 'src/plugins/charts/public';
import { Subscription } from 'rxjs';
import { toExpression, Ast } from '@kbn/interpreter/common';
import { RenderMode } from 'src/plugins/expressions';
import {
ExpressionRendererEvent,
ReactExpressionRendererType,
@ -53,6 +54,7 @@ export type LensByValueInput = {
export type LensByReferenceInput = SavedObjectEmbeddableInput & EmbeddableInput;
export type LensEmbeddableInput = (LensByValueInput | LensByReferenceInput) & {
palette?: PaletteOutput;
renderMode?: RenderMode;
};
export interface LensEmbeddableOutput extends EmbeddableOutput {
@ -192,6 +194,7 @@ export class Embeddable
variables={input.palette ? { theme: { palette: input.palette } } : {}}
searchSessionId={this.input.searchSessionId}
handleEvent={this.handleEvent}
renderMode={input.renderMode}
/>,
domNode
);

View file

@ -13,6 +13,7 @@ import {
ReactExpressionRendererType,
} from 'src/plugins/expressions/public';
import { ExecutionContextSearch } from 'src/plugins/data/public';
import { RenderMode } from 'src/plugins/expressions';
import { getOriginalRequestErrorMessage } from '../error_helper';
export interface ExpressionWrapperProps {
@ -22,6 +23,7 @@ export interface ExpressionWrapperProps {
searchContext: ExecutionContextSearch;
searchSessionId?: string;
handleEvent: (event: ExpressionRendererEvent) => void;
renderMode?: RenderMode;
}
export function ExpressionWrapper({
@ -31,6 +33,7 @@ export function ExpressionWrapper({
variables,
handleEvent,
searchSessionId,
renderMode,
}: ExpressionWrapperProps) {
return (
<I18nProvider>
@ -57,6 +60,7 @@ export function ExpressionWrapper({
expression={expression}
searchContext={searchContext}
searchSessionId={searchSessionId}
renderMode={renderMode}
renderError={(errorMessage, error) => (
<div data-test-subj="expression-renderer-error">
<EuiFlexGroup direction="column" alignItems="center" justifyContent="center">

View file

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

View file

@ -70,6 +70,7 @@ describe('PieVisualization component', () => {
onClickValue: jest.fn(),
chartsThemeService,
paletteService: chartPluginMock.createPaletteRegistry(),
renderMode: 'display' as const,
};
}
@ -266,6 +267,14 @@ describe('PieVisualization component', () => {
`);
});
test('does not set click listener on noInteractivity render mode', () => {
const defaultArgs = getDefaultArgs();
const component = shallow(
<PieComponent args={{ ...args }} {...defaultArgs} renderMode="noInteractivity" />
);
expect(component.find(Settings).first().prop('onElementClick')).toBeUndefined();
});
test('it shows emptyPlaceholder for undefined grouped data', () => {
const defaultData = getDefaultArgs().data;
const emptyData: LensMultiTable = {

View file

@ -20,7 +20,9 @@ import {
RecursivePartial,
Position,
Settings,
ElementClickListener,
} from '@elastic/charts';
import { RenderMode } from 'src/plugins/expressions';
import { FormatFactory, LensFilterEvent } from '../types';
import { VisualizationContainer } from '../visualization_container';
import { CHART_NAMES, DEFAULT_PERCENT_DECIMALS } from './constants';
@ -44,6 +46,7 @@ export function PieComponent(
chartsThemeService: ChartsPluginSetup['theme'];
paletteService: PaletteRegistry;
onClickValue: (data: LensFilterEvent['data']) => void;
renderMode: RenderMode;
}
) {
const [firstTable] = Object.values(props.data.tables);
@ -228,6 +231,12 @@ export function PieComponent(
</EuiText>
);
}
const onElementClickHandler: ElementClickListener = (args) => {
const context = getFilterContext(args[0][0] as LayerValue[], groups, firstTable);
onClickValue(desanitizeFilterContext(context));
};
return (
<VisualizationContainer
reportTitle={props.args.title}
@ -248,11 +257,9 @@ export function PieComponent(
}
legendPosition={legendPosition || Position.Right}
legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */}
onElementClick={(args) => {
const context = getFilterContext(args[0][0] as LayerValue[], groups, firstTable);
onClickValue(desanitizeFilterContext(context));
}}
onElementClick={
props.renderMode !== 'noInteractivity' ? onElementClickHandler : undefined
}
theme={{
...chartTheme,
background: {

View file

@ -427,6 +427,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -451,6 +452,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'line' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -504,6 +506,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={undefined}
@ -541,6 +544,7 @@ describe('xy_expression', () => {
args={multiLayerArgs}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -578,6 +582,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -596,6 +601,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -617,6 +623,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -638,6 +645,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_horizontal' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -664,6 +672,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -688,6 +697,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -773,6 +783,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -791,6 +802,27 @@ describe('xy_expression', () => {
});
});
test('onBrushEnd is not set on noInteractivity mode', () => {
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}
/>
);
expect(wrapper.find(Settings).first().prop('onBrushEnd')).toBeUndefined();
});
test('onElementClick returns correct context data', () => {
const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null, datum: {} };
const series = {
@ -825,6 +857,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -855,6 +888,27 @@ describe('xy_expression', () => {
});
});
test('onElementClick is not triggering event on noInteractivity mode', () => {
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}
/>
);
expect(wrapper.find(Settings).first().prop('onElementClick')).toBeUndefined();
});
test('it renders stacked bar', () => {
const { data, args } = sampleArgs();
const component = shallow(
@ -863,6 +917,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'bar_stacked' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -884,6 +939,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], seriesType: 'area_stacked' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -908,6 +964,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -941,6 +998,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -961,6 +1019,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="CEST"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -987,6 +1046,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [firstLayer] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1007,6 +1067,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [firstLayer] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1030,6 +1091,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [firstLayer, secondLayer] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1058,6 +1120,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1080,6 +1143,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1481,6 +1545,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], xScaleType: 'ordinal' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1501,6 +1566,7 @@ describe('xy_expression', () => {
args={{ ...args, layers: [{ ...args.layers[0], yScaleType: 'sqrt' }] }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1521,6 +1587,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1544,6 +1611,7 @@ describe('xy_expression', () => {
paletteService={paletteService}
minInterval={50}
timeZone="UTC"
renderMode="display"
onClickValue={onClickValue}
onSelectRange={onSelectRange}
/>
@ -1563,6 +1631,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1598,6 +1667,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1631,6 +1701,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1664,6 +1735,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1697,6 +1769,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1797,6 +1870,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1871,6 +1945,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1943,6 +2018,7 @@ describe('xy_expression', () => {
args={args}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1967,6 +2043,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -1990,6 +2067,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -2013,6 +2091,7 @@ describe('xy_expression', () => {
}}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -2048,6 +2127,7 @@ describe('xy_expression', () => {
args={{ ...args, fittingFunction: 'Carry' }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -2075,6 +2155,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -2097,6 +2178,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -2124,6 +2206,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}
@ -2157,6 +2240,7 @@ describe('xy_expression', () => {
args={{ ...args }}
formatFactory={getFormatSpy}
timeZone="UTC"
renderMode="display"
chartsThemeService={chartsThemeService}
paletteService={paletteService}
minInterval={50}

View file

@ -21,6 +21,8 @@ import {
StackMode,
VerticalAlignment,
HorizontalAlignment,
ElementClickListener,
BrushEndListener,
} from '@elastic/charts';
import { I18nProvider } from '@kbn/i18n/react';
import {
@ -31,6 +33,7 @@ import {
} from 'src/plugins/expressions/public';
import { IconType } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RenderMode } from 'src/plugins/expressions';
import {
LensMultiTable,
FormatFactory,
@ -81,6 +84,7 @@ type XYChartRenderProps = XYChartProps & {
minInterval: number | undefined;
onClickValue: (data: LensFilterEvent['data']) => void;
onSelectRange: (data: LensBrushEvent['data']) => void;
renderMode: RenderMode;
};
export const xyChart: ExpressionFunctionDefinition<
@ -235,6 +239,7 @@ export const getXyChartRenderer = (dependencies: {
minInterval={await calculateMinInterval(config, dependencies.getIntervalByColumn)}
onClickValue={onClickValue}
onSelectRange={onSelectRange}
renderMode={handlers.getRenderMode()}
/>
</I18nProvider>,
domNode,
@ -303,6 +308,7 @@ export function XYChart({
minInterval,
onClickValue,
onSelectRange,
renderMode,
}: XYChartRenderProps) {
const { legend, layers, fittingFunction, gridlinesVisibilitySettings, valueLabels } = args;
const chartTheme = chartsThemeService.useChartsTheme();
@ -415,6 +421,87 @@ export function XYChart({
const colorAssignments = getColorAssignments(args.layers, data, formatFactory);
const clickHandler: ElementClickListener = ([[geometry, series]]) => {
// for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue
const xySeries = series as XYChartSeriesIdentifier;
const xyGeometry = geometry as GeometryValue;
const layer = filteredLayers.find((l) =>
xySeries.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString()))
);
if (!layer) {
return;
}
const table = data.tables[layer.layerId];
const points = [
{
row: table.rows.findIndex((row) => {
if (layer.xAccessor) {
if (layersAlreadyFormatted[layer.xAccessor]) {
// stringify the value to compare with the chart value
return xAxisFormatter.convert(row[layer.xAccessor]) === xyGeometry.x;
}
return row[layer.xAccessor] === xyGeometry.x;
}
}),
column: table.columns.findIndex((col) => col.id === layer.xAccessor),
value: xyGeometry.x,
},
];
if (xySeries.seriesKeys.length > 1) {
const pointValue = xySeries.seriesKeys[0];
points.push({
row: table.rows.findIndex(
(row) => layer.splitAccessor && row[layer.splitAccessor] === pointValue
),
column: table.columns.findIndex((col) => col.id === layer.splitAccessor),
value: pointValue,
});
}
const xAxisFieldName = table.columns.find((el) => el.id === layer.xAccessor)?.meta?.field;
const timeFieldName = xDomain && xAxisFieldName;
const context: LensFilterEvent['data'] = {
data: points.map((point) => ({
row: point.row,
column: point.column,
value: point.value,
table,
})),
timeFieldName,
};
onClickValue(desanitizeFilterContext(context));
};
const brushHandler: BrushEndListener = ({ x }) => {
if (!x) {
return;
}
const [min, max] = x;
if (!xAxisColumn || !isHistogramViz) {
return;
}
const table = data.tables[filteredLayers[0].layerId];
const xAxisColumnIndex = table.columns.findIndex((el) => el.id === filteredLayers[0].xAccessor);
const timeFieldName = isTimeViz ? table.columns[xAxisColumnIndex]?.meta?.field : undefined;
const context: LensBrushEvent['data'] = {
range: [min, max],
table,
column: xAxisColumnIndex,
timeFieldName,
};
onSelectRange(context);
};
return (
<Chart>
<Settings
@ -441,89 +528,8 @@ export function XYChart({
}}
rotation={shouldRotate ? 90 : 0}
xDomain={xDomain}
onBrushEnd={({ x }) => {
if (!x) {
return;
}
const [min, max] = x;
if (!xAxisColumn || !isHistogramViz) {
return;
}
const table = data.tables[filteredLayers[0].layerId];
const xAxisColumnIndex = table.columns.findIndex(
(el) => el.id === filteredLayers[0].xAccessor
);
const timeFieldName = isTimeViz
? table.columns[xAxisColumnIndex]?.meta?.field
: undefined;
const context: LensBrushEvent['data'] = {
range: [min, max],
table,
column: xAxisColumnIndex,
timeFieldName,
};
onSelectRange(context);
}}
onElementClick={([[geometry, series]]) => {
// for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue
const xySeries = series as XYChartSeriesIdentifier;
const xyGeometry = geometry as GeometryValue;
const layer = filteredLayers.find((l) =>
xySeries.seriesKeys.some((key: string | number) => l.accessors.includes(key.toString()))
);
if (!layer) {
return;
}
const table = data.tables[layer.layerId];
const points = [
{
row: table.rows.findIndex((row) => {
if (layer.xAccessor) {
if (layersAlreadyFormatted[layer.xAccessor]) {
// stringify the value to compare with the chart value
return xAxisFormatter.convert(row[layer.xAccessor]) === xyGeometry.x;
}
return row[layer.xAccessor] === xyGeometry.x;
}
}),
column: table.columns.findIndex((col) => col.id === layer.xAccessor),
value: xyGeometry.x,
},
];
if (xySeries.seriesKeys.length > 1) {
const pointValue = xySeries.seriesKeys[0];
points.push({
row: table.rows.findIndex(
(row) => layer.splitAccessor && row[layer.splitAccessor] === pointValue
),
column: table.columns.findIndex((col) => col.id === layer.splitAccessor),
value: pointValue,
});
}
const xAxisFieldName = table.columns.find((el) => el.id === layer.xAccessor)?.meta?.field;
const timeFieldName = xDomain && xAxisFieldName;
const context: LensFilterEvent['data'] = {
data: points.map((point) => ({
row: point.row,
column: point.column,
value: point.value,
table,
})),
timeFieldName,
};
onClickValue(desanitizeFilterContext(context));
}}
onBrushEnd={renderMode !== 'noInteractivity' ? brushHandler : undefined}
onElementClick={renderMode !== 'noInteractivity' ? clickHandler : undefined}
/>
<Axis