Skip to content

Commit

Permalink
[Lens] Adds filter from legend in xy and partition charts (#102026)
Browse files Browse the repository at this point in the history
* WIP add filtering capabilities to XY legend

* Fix filter by legend on xy axis charts

* Filter pie and xy axis by legend

* create a shared component

* Add functional test

* Add functional test for pie

* Make the buttons keyboard accessible

* Fix functional test

* move function to retry

* Give another try

* Enable the rest od the tests

* Address PR comments

* Address PR comments

* Apply PR comments, fix popover label for alreadyformatted layers

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
stratoula and kibanamachine committed Jun 22, 2021
1 parent 42fc797 commit 9e1390e
Show file tree
Hide file tree
Showing 12 changed files with 659 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { LegendActionProps, SeriesIdentifier } from '@elastic/charts';
import { EuiPopover } from '@elastic/eui';
import { mountWithIntl } from '@kbn/test/jest';
import { ComponentType, ReactWrapper } from 'enzyme';
import type { Datatable } from 'src/plugins/expressions/public';
import { getLegendAction } from './get_legend_action';
import { LegendActionPopover } from '../shared_components';

const table: Datatable = {
type: 'datatable',
columns: [
{ id: 'a', name: 'A', meta: { type: 'string' } },
{ id: 'b', name: 'B', meta: { type: 'number' } },
],
rows: [
{ a: 'Hi', b: 2 },
{ a: 'Test', b: 4 },
{ a: 'Foo', b: 6 },
],
};

describe('getLegendAction', function () {
let wrapperProps: LegendActionProps;
const Component: ComponentType<LegendActionProps> = getLegendAction(table, jest.fn());
let wrapper: ReactWrapper<LegendActionProps>;

beforeAll(() => {
wrapperProps = {
color: 'rgb(109, 204, 177)',
label: 'Bar',
series: ([
{
specId: 'donut',
key: 'Bar',
},
] as unknown) as SeriesIdentifier[],
};
});

it('is not rendered if row does not exist', () => {
wrapper = mountWithIntl(<Component {...wrapperProps} />);
expect(wrapper).toEqual({});
expect(wrapper.find(EuiPopover).length).toBe(0);
});

it('is rendered if row is detected', () => {
const newProps = {
...wrapperProps,
label: 'Hi',
series: ([
{
specId: 'donut',
key: 'Hi',
},
] as unknown) as SeriesIdentifier[],
};
wrapper = mountWithIntl(<Component {...newProps} />);
expect(wrapper.find(EuiPopover).length).toBe(1);
expect(wrapper.find(EuiPopover).prop('title')).toEqual('Hi, filter options');
expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({
data: [
{
column: 0,
row: 0,
table,
value: 'Hi',
},
],
});
});
});
44 changes: 44 additions & 0 deletions x-pack/plugins/lens/public/pie_visualization/get_legend_action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';

import type { LegendAction } from '@elastic/charts';
import type { Datatable } from 'src/plugins/expressions/public';
import type { LensFilterEvent } from '../types';
import { LegendActionPopover } from '../shared_components';

export const getLegendAction = (
table: Datatable,
onFilter: (data: LensFilterEvent['data']) => void
): LegendAction =>
React.memo(({ series: [pieSeries], label }) => {
const data = table.columns.reduce<LensFilterEvent['data']['data']>((acc, { id }, column) => {
const value = pieSeries.key;
const row = table.rows.findIndex((r) => r[id] === value);
if (row > -1) {
acc.push({
table,
column,
row,
value,
});
}

return acc;
}, []);

if (data.length === 0) {
return null;
}

const context: LensFilterEvent['data'] = {
data,
};

return <LegendActionPopover label={label} context={context} onFilter={onFilter} />;
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
SeriesLayer,
} from '../../../../../src/plugins/charts/public';
import { LensIconChartDonut } from '../assets/chart_donut';
import { getLegendAction } from './get_legend_action';

declare global {
interface Window {
Expand Down Expand Up @@ -281,6 +282,7 @@ export function PieComponent(
onElementClick={
props.renderMode !== 'noInteractivity' ? onElementClickHandler : undefined
}
legendAction={getLegendAction(firstTable, onClickValue)}
theme={{
...chartTheme,
background: {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/lens/public/shared_components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export { TooltipWrapper } from './tooltip_wrapper';
export * from './coloring';
export { useDebouncedValue } from './debounced_value';
export * from './helpers';
export { LegendActionPopover } from './legend_action_popover';
102 changes: 102 additions & 0 deletions x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useState } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui';
import type { LensFilterEvent } from '../types';
import { desanitizeFilterContext } from '../utils';

export interface LegendActionPopoverProps {
/**
* Determines the panels label
*/
label: string;
/**
* Callback on filter value
*/
onFilter: (data: LensFilterEvent['data']) => void;
/**
* Determines the filter event data
*/
context: LensFilterEvent['data'];
}

export const LegendActionPopover: React.FunctionComponent<LegendActionPopoverProps> = ({
label,
onFilter,
context,
}) => {
const [popoverOpen, setPopoverOpen] = useState(false);
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 'main',
title: label,
items: [
{
name: i18n.translate('xpack.lens.shared.legend.filterForValueButtonAriaLabel', {
defaultMessage: 'Filter for value',
}),
'data-test-subj': `legend-${label}-filterIn`,
icon: <EuiIcon type="plusInCircle" size="m" />,
onClick: () => {
setPopoverOpen(false);
onFilter(desanitizeFilterContext(context));
},
},
{
name: i18n.translate('xpack.lens.shared.legend.filterOutValueButtonAriaLabel', {
defaultMessage: 'Filter out value',
}),
'data-test-subj': `legend-${label}-filterOut`,
icon: <EuiIcon type="minusInCircle" size="m" />,
onClick: () => {
setPopoverOpen(false);
onFilter(desanitizeFilterContext({ ...context, negate: true }));
},
},
],
},
];

const Button = (
<div
tabIndex={0}
role="button"
aria-pressed="false"
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
marginLeft: 4,
marginRight: 4,
}}
data-test-subj={`legend-${label}`}
onKeyPress={() => setPopoverOpen(!popoverOpen)}
onClick={() => setPopoverOpen(!popoverOpen)}
>
<EuiIcon size="s" type="boxesVertical" />
</div>
);
return (
<EuiPopover
id="contextMenuNormal"
button={Button}
isOpen={popoverOpen}
closePopover={() => setPopoverOpen(false)}
panelPaddingSize="none"
anchorPosition="upLeft"
title={i18n.translate('xpack.lens.shared.legend.filterOptionsLegend', {
defaultMessage: '{legendDataLabel}, filter options',
values: { legendDataLabel: label },
})}
>
<EuiContextMenu initialPanelId="main" panels={panels} />
</EuiPopover>
);
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions x-pack/plugins/lens/public/xy_visualization/expression.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { fittingFunctionDefinitions, getFitOptions } from './fitting_functions';
import { getAxesConfiguration, GroupsConfiguration, validateExtent } from './axes_configuration';
import { getColorAssignments } from './color_assignment';
import { getXDomain, XyEndzones } from './x_domain';
import { getLegendAction } from './get_legend_action';

declare global {
interface Window {
Expand Down Expand Up @@ -390,6 +391,7 @@ export function XYChart({
);
const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.meta?.params);
const layersAlreadyFormatted: Record<string, boolean> = {};

// This is a safe formatter for the xAccessor that abstracts the knowledge of already formatted layers
const safeXAccessorLabelRenderer = (value: unknown): string =>
xAxisColumn && layersAlreadyFormatted[xAxisColumn.id]
Expand Down Expand Up @@ -629,6 +631,13 @@ export function XYChart({
xDomain={xDomain}
onBrushEnd={renderMode !== 'noInteractivity' ? brushHandler : undefined}
onElementClick={renderMode !== 'noInteractivity' ? clickHandler : undefined}
legendAction={getLegendAction(
filteredLayers,
data.tables,
onClickValue,
formatFactory,
layersAlreadyFormatted
)}
showLegendExtra={isHistogramViz && valuesInLegend}
/>

Expand Down
Loading

0 comments on commit 9e1390e

Please sign in to comment.