Skip to content

Commit

Permalink
[Lens] Pie and treemap charts (elastic#55477)
Browse files Browse the repository at this point in the history
* [Lens] Add pie and treemap visualizations

* Fix types

* Update to new platform

* Support 2-layer treemap and legends, dark mode

* Significant restructuring of code

* Upgrade to latest charts library

* Commit yarn.lock

* chore: update elastic-charts

* fix types after merge master

* Add settings panel and merge visualizations

* Fix tests

* build: upgrade @elastic/charts to 19.0.0

* refactor: onBrushEnd breaking changes

* fix: missing onBrushEnd argument changes

* More updates

* Fix XY rendering issue when all dates are empty

* Fix bugs and tests

* Use shared services location

* Fix bug in XY chart

* fix: update ech to 19.1.1

* fix: lens onBrushEnd breaking changes

* Change how pie/treemap settings work

* [Design] Fix up settings panel

* [Design] Update partition chart config styles

* fix eslint

* Fix legend issues in pie and XY, add some tests

* update to 19.1.2

* Fix text color for treemap

* Fix chart switch bug

* Fix suggestions

Co-authored-by: Marta Bondyra <[email protected]>
Co-authored-by: Elastic Machine <[email protected]>
Co-authored-by: Marco Vettorello <[email protected]>
Co-authored-by: cchaos <[email protected]>
  • Loading branch information
5 people authored and wylieconlon committed May 5, 2020
1 parent 9eadb64 commit 185ed8c
Show file tree
Hide file tree
Showing 32 changed files with 2,393 additions and 90 deletions.
4 changes: 4 additions & 0 deletions x-pack/plugins/lens/public/assets/chart_donut.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions x-pack/plugins/lens/public/assets/chart_pie.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions x-pack/plugins/lens/public/assets/chart_treemap.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ export const datatableVisualization: Visualization<
return [
{
title,
// table with >= 10 columns will have a score of 0.6, fewer columns reduce score
score: (Math.min(table.columns.length, 10) / 10) * 0.6,
// table with >= 10 columns will have a score of 0.4, fewer columns reduce score
score: (Math.min(table.columns.length, 10) / 10) * 0.4,
state: {
layers: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ describe('chart_switch', () => {
return {
...createMockVisualization(),
id,
getVisualizationTypeId: jest.fn(_state => id),
visualizationTypes: [
{
icon: 'empty',
id: `sub${id}`,
id,
label: `Label ${id}`,
},
],
Expand All @@ -51,6 +52,7 @@ describe('chart_switch', () => {
visB: generateVisualization('visB'),
visC: {
...generateVisualization('visC'),
initialize: jest.fn((_frame, state) => state ?? { type: 'subvisC1' }),
visualizationTypes: [
{
icon: 'empty',
Expand All @@ -68,15 +70,23 @@ describe('chart_switch', () => {
label: 'C3',
},
],
getVisualizationTypeId: jest.fn(state => state.type),
getSuggestions: jest.fn(options => {
if (options.subVisualizationId === 'subvisC2') {
return [];
}
// Multiple suggestions need to be filtered
return [
{
score: 1,
title: 'Primary suggestion',
state: { type: 'subvisC3' },
previewIcon: 'empty',
},
{
score: 1,
title: '',
state: `suggestion`,
state: { type: 'subvisC1', notPrimary: true },
previewIcon: 'empty',
},
];
Expand Down Expand Up @@ -162,7 +172,7 @@ describe('chart_switch', () => {
const component = mount(
<ChartSwitch
visualizationId="visA"
visualizationState={{}}
visualizationState={'state from a'}
visualizationMap={visualizations}
dispatch={dispatch}
framePublicAPI={mockFrame(['a'])}
Expand All @@ -171,7 +181,7 @@ describe('chart_switch', () => {
/>
);

switchTo('subvisB', component);
switchTo('visB', component);

expect(dispatch).toHaveBeenCalledWith({
initialState: 'suggestion visB',
Expand Down Expand Up @@ -201,7 +211,7 @@ describe('chart_switch', () => {
/>
);

switchTo('subvisB', component);
switchTo('visB', component);

expect(frame.removeLayers).toHaveBeenCalledWith(['a']);

Expand Down Expand Up @@ -265,7 +275,7 @@ describe('chart_switch', () => {
/>
);

expect(getMenuItem('subvisB', component).prop('betaBadgeIconType')).toEqual('alert');
expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert');
});

it('should indicate data loss if not all layers will be used', () => {
Expand All @@ -285,7 +295,7 @@ describe('chart_switch', () => {
/>
);

expect(getMenuItem('subvisB', component).prop('betaBadgeIconType')).toEqual('alert');
expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert');
});

it('should indicate data loss if no data will be used', () => {
Expand All @@ -306,7 +316,7 @@ describe('chart_switch', () => {
/>
);

expect(getMenuItem('subvisB', component).prop('betaBadgeIconType')).toEqual('alert');
expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toEqual('alert');
});

it('should not indicate data loss if there is no data', () => {
Expand All @@ -328,22 +338,22 @@ describe('chart_switch', () => {
/>
);

expect(getMenuItem('subvisB', component).prop('betaBadgeIconType')).toBeUndefined();
expect(getMenuItem('visB', component).prop('betaBadgeIconType')).toBeUndefined();
});

it('should not show a warning when the subvisualization is the same', () => {
const dispatch = jest.fn();
const frame = mockFrame(['a', 'b', 'c']);
const visualizations = mockVisualizations();
visualizations.visC.getVisualizationTypeId.mockReturnValue('subvisC2');
const switchVisualizationType = jest.fn(() => 'therebedragons');
const switchVisualizationType = jest.fn(() => ({ type: 'subvisC1' }));

visualizations.visC.switchVisualizationType = switchVisualizationType;

const component = mount(
<ChartSwitch
visualizationId="visC"
visualizationState={'therebegriffins'}
visualizationState={{ type: 'subvisC2' }}
visualizationMap={visualizations}
dispatch={dispatch}
framePublicAPI={frame}
Expand Down Expand Up @@ -373,7 +383,7 @@ describe('chart_switch', () => {
/>
);

switchTo('subvisB', component);
switchTo('visB', component);

expect(frame.removeLayers).toHaveBeenCalledTimes(1);
expect(frame.removeLayers).toHaveBeenCalledWith(['a', 'b', 'c']);
Expand Down Expand Up @@ -403,7 +413,7 @@ describe('chart_switch', () => {
const component = mount(
<ChartSwitch
visualizationId="visC"
visualizationState={'therebegriffins'}
visualizationState={{ type: 'subvisC1' }}
visualizationMap={visualizations}
dispatch={dispatch}
framePublicAPI={frame}
Expand All @@ -413,7 +423,7 @@ describe('chart_switch', () => {
);

switchTo('subvisC3', component);
expect(switchVisualizationType).toHaveBeenCalledWith('subvisC3', 'suggestion');
expect(switchVisualizationType).toHaveBeenCalledWith('subvisC3', { type: 'subvisC3' });
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
type: 'SWITCH_VISUALIZATION',
Expand Down Expand Up @@ -471,7 +481,7 @@ describe('chart_switch', () => {
/>
);

switchTo('subvisB', component);
switchTo('visB', component);

expect(dispatch).toHaveBeenCalledWith({
type: 'SWITCH_VISUALIZATION',
Expand Down Expand Up @@ -503,17 +513,43 @@ describe('chart_switch', () => {
/>
);

switchTo('subvisB', component);
switchTo('visB', component);

expect(dispatch).toHaveBeenCalledWith({
initialState: 'suggestion visB subvisB',
initialState: 'suggestion visB visB',
newVisualizationId: 'visB',
type: 'SWITCH_VISUALIZATION',
datasourceId: 'testDatasource',
datasourceState: {},
});
});

it('should use the suggestion that matches the subtype', () => {
const dispatch = jest.fn();
const visualizations = mockVisualizations();
const switchVisualizationType = jest.fn();

visualizations.visC.switchVisualizationType = switchVisualizationType;

const component = mount(
<ChartSwitch
visualizationId="visC"
visualizationState={{ type: 'subvisC3' }}
visualizationMap={visualizations}
dispatch={dispatch}
framePublicAPI={mockFrame(['a'])}
datasourceMap={mockDatasourceMap()}
datasourceStates={mockDatasourceStates()}
/>
);

switchTo('subvisC1', component);
expect(switchVisualizationType).toHaveBeenCalledWith('subvisC1', {
type: 'subvisC1',
notPrimary: true,
});
});

it('should show all visualization types', () => {
const component = mount(
<ChartSwitch
Expand All @@ -529,7 +565,7 @@ describe('chart_switch', () => {

showFlyout(component);

const allDisplayed = ['subvisA', 'subvisB', 'subvisC1', 'subvisC2'].every(
const allDisplayed = ['visA', 'visB', 'subvisC1', 'subvisC2', 'subvisC3'].every(
subType => component.find(`[data-test-subj="lnsChartSwitchPopover_${subType}"]`).length > 0
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,10 @@ function getTopSuggestion(
}).filter(suggestion => {
// don't use extended versions of current data table on switching between visualizations
// to avoid confusing the user.
return suggestion.changeType !== 'extended';
return (
suggestion.changeType !== 'extended' &&
newVisualization.getVisualizationTypeId(suggestion.visualizationState) === subVisualizationId
);
});

// We prefer unchanged or reduced suggestions when switching
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import React, { useState } from 'react';
import { EuiPopover, EuiButtonIcon } from '@elastic/eui';
import { EuiPopover, EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { NativeRenderer } from '../../../native_renderer';
import { Visualization, VisualizationLayerWidgetProps } from '../../../types';
Expand All @@ -28,21 +28,27 @@ export function LayerSettings({
return (
<EuiPopover
id={`lnsLayerPopover_${layerId}`}
panelPaddingSize="s"
panelPaddingSize="m"
ownFocus
button={
<EuiButtonIcon
iconType={activeVisualization.getLayerContextMenuIcon?.(layerConfigProps) || 'gear'}
aria-label={i18n.translate('xpack.lens.editLayerSettings', {
<EuiToolTip
content={i18n.translate('xpack.lens.editLayerSettings', {
defaultMessage: 'Edit layer settings',
})}
onClick={() => setIsOpen(!isOpen)}
data-test-subj="lns_layer_settings"
/>
>
<EuiButtonIcon
iconType={activeVisualization.getLayerContextMenuIcon?.(layerConfigProps) || 'gear'}
aria-label={i18n.translate('xpack.lens.editLayerSettings', {
defaultMessage: 'Edit layer settings',
})}
onClick={() => setIsOpen(!isOpen)}
data-test-subj="lns_layer_settings"
/>
</EuiToolTip>
}
isOpen={isOpen}
closePopover={() => setIsOpen(false)}
anchorPosition="leftUp"
anchorPosition="downLeft"
>
<NativeRenderer
render={activeVisualization.renderLayerContextMenu}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,7 @@ describe('editor_frame', () => {
it('should use suggestions to switch to new visualization', async () => {
const initialState = { suggested: true };
mockVisualization2.initialize.mockReturnValueOnce({ initial: true });
mockVisualization2.getVisualizationTypeId.mockReturnValueOnce('testVis2');
mockVisualization2.getSuggestions.mockReturnValueOnce([
{
title: 'Suggested vis',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,10 +272,10 @@ describe('IndexPattern Data Source', () => {
"1",
],
"metricsAtAllLevels": Array [
false,
true,
],
"partialRows": Array [
false,
true,
],
"timeFields": Array [
"timestamp",
Expand All @@ -287,7 +287,7 @@ describe('IndexPattern Data Source', () => {
Object {
"arguments": Object {
"idMap": Array [
"{\\"col-0-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-1-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}",
"{\\"col--1-col1\\":{\\"label\\":\\"Count of records\\",\\"dataType\\":\\"number\\",\\"isBucketed\\":false,\\"sourceField\\":\\"Records\\",\\"operationType\\":\\"count\\",\\"id\\":\\"col1\\"},\\"col-2-col2\\":{\\"label\\":\\"Date\\",\\"dataType\\":\\"date\\",\\"isBucketed\\":true,\\"operationType\\":\\"date_histogram\\",\\"sourceField\\":\\"timestamp\\",\\"params\\":{\\"interval\\":\\"1d\\"},\\"id\\":\\"col2\\"}}",
],
},
"function": "lens_rename_columns",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,35 @@ function getExpressionForLayer(
}

const columnEntries = columnOrder.map(colId => [colId, columns[colId]] as const);
const bucketsCount = columnEntries.filter(([, entry]) => entry.isBucketed).length;
const metricsCount = columnEntries.length - bucketsCount;

if (columnEntries.length) {
const aggs = columnEntries.map(([colId, col]) => {
return getEsAggsConfig(col, colId);
});

const idMap = columnEntries.reduce((currentIdMap, [colId], index) => {
/**
* Because we are turning on metrics at all levels, the sequence generation
* logic here is more complicated. Examples follow:
*
* Example 1: [Count]
* Output: [`col-0-count`]
*
* Example 2: [Terms, Terms, Count]
* Output: [`col-0-terms0`, `col-2-terms1`, `col-3-count`]
*
* Example 3: [Terms, Terms, Count, Max]
* Output: [`col-0-terms0`, `col-3-terms1`, `col-4-count`, `col-5-max`]
*/
const idMap = columnEntries.reduce((currentIdMap, [colId, column], index) => {
const newIndex = column.isBucketed
? index * (metricsCount + 1) // Buckets are spaced apart by N + 1
: (index ? index + 1 : 0) - bucketsCount + (bucketsCount - 1) * (metricsCount + 1);
return {
...currentIdMap,
[`col-${index}-${colId}`]: {
...columns[colId],
[`col-${columnEntries.length === 1 ? 0 : newIndex}-${colId}`]: {
...column,
id: colId,
},
};
Expand Down Expand Up @@ -83,8 +101,8 @@ function getExpressionForLayer(
function: 'esaggs',
arguments: {
index: [indexPattern.id],
metricsAtAllLevels: [false],
partialRows: [false],
metricsAtAllLevels: [true],
partialRows: [true],
includeFormatHints: [true],
timeFields: allDateHistogramFields,
aggConfigs: [JSON.stringify(aggs)],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ describe('metric_suggestions', () => {
expect(suggestion).toMatchInlineSnapshot(`
Object {
"previewIcon": "test-file-stub",
"score": 0.5,
"score": 0.1,
"state": Object {
"accessor": "bytes",
"layerId": "l1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function getSuggestion(table: TableSuggestion): VisualizationSuggestion<State> {

return {
title,
score: 0.5,
score: 0.1,
previewIcon: chartMetricSVG,
state: {
layerId: table.layerId,
Expand Down
Loading

0 comments on commit 185ed8c

Please sign in to comment.