[7.x] [Pie] New implementation of the vislib pie chart with es-charts (#83929) (#101292)

* [Pie] New implementation of the vislib pie chart with es-charts (#83929)

* es lint fix

* Add formatter on the buckets labels

* Config the new plugin, toggle tooltip

* Aff filtering on slice click

* minor fixes

* fix eslint error

* use legacy palette for now

* Add color picker to legend colors

* Fix ts error

* Add legend actions

* Fix bug on Color Picker and remove local state as it is unecessary

* Fix some bugs on colorPicker

* Add setting for the user to select between the legacy palette or the eui ones

* small enhancements, treat empty labels with (empty)

* Fix color picker bugs with multiple layers

* fixes on internationalization

* Create migration script for pie chart and legacy palette

* Add unit tests (wip) and a small refactoring

* Add unit tests and move some things to utils, useMemo and useCallback where it should

* Add jest config file

* Fix jest test

* fix api integration failure

* Fix to_ast_esaggs for new pie plugin

* Close legendColorPicker popover when user clicks outside

* Fix warning

* Remove getter/setters and refactor

* Remove kibanaUtils from pie plugin as it is not needed

* Add new values to the migration script

* Fix bug on not changing color for expty string

* remove from migration script as they don't need it

* Fix editor settings for old and new implementation

* fix uistate type

* Disable split chart for the new plugin for now

* Remove temp folder

* Move translations to the pie plugin

* Fix CI failures

* Add unit test for the editor config

* Types cleanup

* Fix types vol2

* Minor improvements

* Display data on the inspector

* Cleanup translations

* Add telemetry for new editor pie options

* Fix missing translation

* Use Eui component to detect click outside the color picker popover

* Retrieve color picker from editor and syncColors on dashboard

* Lazy load palette service

* Add the new plugin to ts references, fix tests, refactor

* Fix ci failure

* Move charts library switch to vislib plugin

* Remove cyclic dependencies

* Modify license headers

* Move charts library switch to visualizations plugin

* Fix i18n on the switch moved to visualizations plugin

* Update license

* Fix tests

* Fix bugs created by new charts version

* Fix the i18n switch problem

* Update the migration script

* Identify if colorIsOverwritten or not

* Small multiples, missing the click event

* Fixes the UX for small multiples part1

* Distinct colors per slice implementation

* Fix ts references problem

* Fix some small multiples bugs

* Add unit tests

* Fix ts ref problem

* Fix TS problems caused by es-charts new version

* Update the sample pie visualizations with the new eui palette

* Allows filtering by the small multiples value

* Apply sortPredicate on partition layers

* Fix vilib test

* Enable functional tests for new plugin

* Fix some functional tests

* Minor fix

* Fix functional tests

* Fix dashboard tests

* Fix all dashboard tests

* Apply some improvements

* Explicit params instead of visConfig Json

* Fix i18n failure

* Add top level setting

* Minor fix

* Fix jest tests

* Address PR comments

* Fix i18n error

* fix functional test

* Add an icon tip on the distinct colors per slice switch

* Fix some of the PR comments

* Address more PR comments

* Small fix

* Functional test

* address some PR comments

* Add padding to the pie container

* Add a max width to the container

* Improve dashboard functional test

* Move the labels expression function to the pie plugin

* Fix i18n

* Fix functional test

* Apply PR comments

* Do not forget to also add the migration to them embeddable too :D

* Fix distinct colors for IP range layer

* Remove console errors

* Fix small mulitples colors with multiple layers

* Fix lint problem

* Fix problems created from merging with master

* Address PR comments

* Change the config in order the pie chart to not appear so huge on the editor

* Address PR comments

* Change the max percentage digits to 4

* Change the max size to 1000

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
# Conflicts:
#	.github/CODEOWNERS
#	packages/kbn-optimizer/limits.yml
#	test/functional/apps/visualize/_pie_chart.ts

* Fix functional test

* Revert change - backport missing

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Stratoula Kalafateli 2021-06-04 18:15:27 +03:00 committed by GitHub
parent 78731f4c35
commit 10dd00f6a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
88 changed files with 5601 additions and 1678 deletions

View file

@ -56,6 +56,7 @@
"visTypeVega": "src/plugins/vis_type_vega",
"visTypeVislib": "src/plugins/vis_type_vislib",
"visTypeXy": "src/plugins/vis_type_xy",
"visTypePie": "src/plugins/vis_type_pie",
"visualizations": "src/plugins/visualizations",
"visualize": "src/plugins/visualize",
"apmOss": "src/plugins/apm_oss",

View file

@ -261,6 +261,10 @@ The plugin exposes the static DefaultEditorController class to consume.
|Contains the metric visualization.
|{kib-repo}blob/{branch}/src/plugins/vis_type_pie/README.md[visTypePie]
|Contains the pie chart implementation using the elastic-charts library. The goal is to eventually deprecate the old implementation and keep only this. Until then, the library used is defined by the Legacy charts library advanced setting.
|{kib-repo}blob/{branch}/src/plugins/vis_type_table/README.md[visTypeTable]
|Contains the data table visualization, that allows presenting data in a simple table format.

View file

@ -88,6 +88,7 @@ pageLoadAssetSize:
visTypeMarkdown: 30896
visTypeMetric: 42790
visTypeTable: 95078
visTypePie: 34051
visTypeTagcloud: 37575
visTypeTimelion: 68883
visTypeTimeseries: 55347

View file

@ -14,6 +14,7 @@ export { ChartsPluginSetup, ChartsPluginStart } from './plugin';
export * from './static';
export * from './services/palettes/types';
export { lightenColor } from './services/palettes/lighten_color';
export {
PaletteOutput,
CustomPaletteArguments,

View file

@ -18,7 +18,7 @@ import {
EuiFlexGroup,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { lightenColor } from '../../services/palettes/lighten_color';
import './color_picker.scss';
export const legacyColors: string[] = [
@ -105,6 +105,14 @@ interface ColorPickerProps {
* Callback for onKeyPress event
*/
onKeyDown?: (e: React.KeyboardEvent<HTMLElement>) => void;
/**
* Optional define the series maxDepth
*/
maxDepth?: number;
/**
* Optional define the layer index
*/
layerIndex?: number;
}
const euiColors = euiPaletteColorBlind({ rotations: 4, order: 'group' });
@ -115,6 +123,8 @@ export const ColorPicker = ({
useLegacyColors = true,
colorIsOverwritten = true,
onKeyDown,
maxDepth,
layerIndex,
}: ColorPickerProps) => {
const legendColors = useLegacyColors ? legacyColors : euiColors;
@ -159,13 +169,18 @@ export const ColorPicker = ({
))}
</EuiFlexGroup>
</fieldset>
{legendColors.some((c) => c === selectedColor) && colorIsOverwritten && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty size="s" onClick={(e: any) => onChange(null, e)}>
<FormattedMessage id="charts.colorPicker.clearColor" defaultMessage="Reset color" />
</EuiButtonEmpty>
</EuiFlexItem>
)}
{legendColors.some(
(c) =>
c === selectedColor ||
(layerIndex && maxDepth && lightenColor(c, layerIndex, maxDepth) === selectedColor)
) &&
colorIsOverwritten && (
<EuiFlexItem grow={false}>
<EuiButtonEmpty size="s" onClick={(e: any) => onChange(null, e)}>
<FormattedMessage id="charts.colorPicker.clearColor" defaultMessage="Reset color" />
</EuiButtonEmpty>
</EuiFlexItem>
)}
</div>
);
};

View file

@ -45,7 +45,7 @@ export const getSavedObjects = (): SavedObject[] => [
defaultMessage: '[eCommerce] Sales by Gender',
}),
visState:
'{"title":"[eCommerce] Sales by Gender","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"customer_gender","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}',
'{"title":"[eCommerce] Sales by Gender","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100},"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"customer_gender","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}',
uiStateJSON: '{}',
description: '',
version: 1,

View file

@ -100,7 +100,7 @@ export const getSavedObjects = (): SavedObject[] => [
defaultMessage: '[Flights] Airline Carrier',
}),
visState:
'{"title":"[Flights] Airline Carrier","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"Carrier","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}',
'{"title":"[Flights] Airline Carrier","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100},"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"Carrier","size":5,"order":"desc","orderBy":"1","otherBucket":false,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing"}}]}',
uiStateJSON: '{"vis":{"legendOpen":false}}',
description: '',
version: 1,

View file

@ -234,7 +234,7 @@ export const getSavedObjects = (): SavedObject[] => [
defaultMessage: '[Logs] Visitors by OS',
}),
visState:
'{"title":"[Logs] Visitors by OS","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"machine.os.keyword","otherBucket":true,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1"}}]}',
'{"title":"[Logs] Visitors by OS","type":"pie","params":{"type":"pie","addTooltip":true,"addLegend":true,"legendPosition":"right","isDonut":true,"labels":{"show":true,"values":true,"last_level":true,"truncate":100},"palette":{"type":"palette","name":"default"}},"aggs":[{"id":"1","enabled":true,"type":"count","schema":"metric","params":{}},{"id":"2","enabled":true,"type":"terms","schema":"segment","params":{"field":"machine.os.keyword","otherBucket":true,"otherBucketLabel":"Other","missingBucket":false,"missingBucketLabel":"Missing","size":10,"order":"desc","orderBy":"1"}}]}',
uiStateJSON: '{}',
description: '',
version: 1,

View file

@ -0,0 +1 @@
Contains the pie chart implementation using the elastic-charts library. The goal is to eventually deprecate the old implementation and keep only this. Until then, the library used is defined by the Legacy charts library advanced setting.

View file

@ -6,6 +6,4 @@
* Side Public License, v 1.
*/
import { VisTypeXyServerPlugin } from './plugin';
export const plugin = () => new VisTypeXyServerPlugin();
export const DEFAULT_PERCENT_DECIMALS = 2;

View file

@ -0,0 +1,13 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/src/plugins/vis_type_pie'],
};

View file

@ -0,0 +1,8 @@
{
"id": "visTypePie",
"version": "kibana",
"ui": true,
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"],
"requiredBundles": ["visDefaultEditor"]
}

View file

@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`interpreter/functions#pie returns an object with the correct structure 1`] = `
Object {
"as": "pie_vis",
"type": "render",
"value": Object {
"params": Object {
"listenOnChange": true,
},
"syncColors": false,
"visConfig": Object {
"addLegend": true,
"addTooltip": true,
"buckets": undefined,
"dimensions": Object {
"buckets": undefined,
"metric": Object {
"accessor": 0,
"aggType": "count",
"format": Object {
"id": "number",
},
"params": Object {},
},
"splitColumn": undefined,
"splitRow": undefined,
},
"distinctColors": false,
"isDonut": true,
"labels": Object {
"percentDecimals": 2,
"position": "default",
"show": false,
"truncate": 100,
"values": true,
"valuesFormat": "percent",
},
"legendPosition": "right",
"metric": Object {
"accessor": 0,
"aggType": "count",
"format": Object {
"id": "number",
},
"params": Object {},
},
"nestedLegend": true,
"palette": Object {
"name": "kibana_palette",
"type": "palette",
},
"splitColumn": undefined,
"splitRow": undefined,
},
"visData": Object {
"columns": Array [
Object {
"id": "col-0-1",
"name": "Count",
},
],
"rows": Array [
Object {
"col-0-1": 0,
},
],
"type": "datatable",
},
"visType": "pie",
},
}
`;

View file

@ -0,0 +1,122 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`vis type pie vis toExpressionAst function should match basic snapshot 1`] = `
Object {
"chain": Array [
Object {
"arguments": Object {
"aggs": Array [],
"index": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"id": Array [
"123",
],
},
"function": "indexPatternLoad",
"type": "function",
},
],
"type": "expression",
},
],
"metricsAtAllLevels": Array [
true,
],
"partialRows": Array [
false,
],
},
"function": "esaggs",
"type": "function",
},
Object {
"arguments": Object {
"addLegend": Array [
true,
],
"addTooltip": Array [
true,
],
"buckets": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"accessor": Array [
1,
],
"format": Array [
"terms",
],
"formatParams": Array [
"{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}",
],
},
"function": "visdimension",
"type": "function",
},
],
"type": "expression",
},
],
"isDonut": Array [
true,
],
"labels": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"lastLevel": Array [
true,
],
"show": Array [
true,
],
"truncate": Array [
100,
],
"values": Array [
true,
],
},
"function": "pielabels",
"type": "function",
},
],
"type": "expression",
},
],
"legendPosition": Array [
"right",
],
"metric": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"accessor": Array [
0,
],
"format": Array [
"number",
],
},
"function": "visdimension",
"type": "function",
},
],
"type": "expression",
},
],
},
"function": "pie_vis",
"type": "function",
},
],
"type": "expression",
}
`;

View file

@ -0,0 +1,18 @@
.pieChart__wrapper,
.pieChart__container {
display: flex;
flex: 1 1 auto;
min-height: 0;
min-width: 0;
}
.pieChart__container {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: $euiSizeS;
margin-left: auto;
margin-right: auto;
}

View file

@ -0,0 +1,67 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { Accessor, AccessorFn, GroupBy, GroupBySort, SmallMultiples } from '@elastic/charts';
import { DatatableColumn } from '../../../expressions/public';
import { SplitDimensionParams } from '../types';
interface ChartSplitProps {
splitColumnAccessor?: Accessor | AccessorFn;
splitRowAccessor?: Accessor | AccessorFn;
splitDimension?: DatatableColumn;
}
const CHART_SPLIT_ID = '__pie_chart_split__';
export const SMALL_MULTIPLES_ID = '__pie_chart_sm__';
export const ChartSplit = ({
splitColumnAccessor,
splitRowAccessor,
splitDimension,
}: ChartSplitProps) => {
if (!splitColumnAccessor && !splitRowAccessor) return null;
let sort: GroupBySort = 'alphaDesc';
if (splitDimension?.meta?.params?.id === 'terms') {
const params = splitDimension?.meta?.sourceParams?.params as SplitDimensionParams;
sort = params?.order === 'asc' ? 'alphaAsc' : 'alphaDesc';
}
return (
<>
<GroupBy
id={CHART_SPLIT_ID}
by={(spec, datum) => {
const splitTypeAccessor = splitColumnAccessor || splitRowAccessor;
if (splitTypeAccessor) {
return typeof splitTypeAccessor === 'function'
? splitTypeAccessor(datum)
: datum[splitTypeAccessor];
}
return spec.id;
}}
sort={sort}
/>
<SmallMultiples
id={SMALL_MULTIPLES_ID}
splitVertically={splitRowAccessor ? CHART_SPLIT_ID : undefined}
splitHorizontally={splitColumnAccessor ? CHART_SPLIT_ID : undefined}
style={{
verticalPanelPadding: {
outer: 0.1,
inner: 0.1,
},
horizontalPanelPadding: {
outer: 0.1,
inner: 0.1,
},
}}
/>
</>
);
};

View file

@ -0,0 +1,40 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { LabelPositions, ValueFormats } from '../types';
export const getLabelPositions = [
{
text: i18n.translate('visTypePie.labelPositions.insideText', {
defaultMessage: 'Inside',
}),
value: LabelPositions.INSIDE,
},
{
text: i18n.translate('visTypePie.labelPositions.insideOrOutsideText', {
defaultMessage: 'Inside or outside',
}),
value: LabelPositions.DEFAULT,
},
];
export const getValuesFormats = [
{
text: i18n.translate('visTypePie.valuesFormats.percent', {
defaultMessage: 'Show percent',
}),
value: ValueFormats.PERCENT,
},
{
text: i18n.translate('visTypePie.valuesFormats.value', {
defaultMessage: 'Show value',
}),
value: ValueFormats.VALUE,
},
];

View file

@ -0,0 +1,26 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { lazy } from 'react';
import { VisEditorOptionsProps } from '../../../../visualizations/public';
import { PieVisParams, PieTypeProps } from '../../types';
const PieOptionsLazy = lazy(() => import('./pie'));
export const getPieOptions = ({
showElasticChartsOptions,
palettes,
trackUiMetric,
}: PieTypeProps) => (props: VisEditorOptionsProps<PieVisParams>) => (
<PieOptionsLazy
{...props}
palettes={palettes}
showElasticChartsOptions={showElasticChartsOptions}
trackUiMetric={trackUiMetric}
/>
);

View file

@ -0,0 +1,124 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { mountWithIntl } from '@kbn/test/jest';
import { ReactWrapper } from 'enzyme';
import PieOptions, { PieOptionsProps } from './pie';
import { chartPluginMock } from '../../../../charts/public/mocks';
import { findTestSubject } from '@elastic/eui/lib/test';
import { act } from 'react-dom/test-utils';
describe('PalettePicker', function () {
let props: PieOptionsProps;
let component: ReactWrapper<PieOptionsProps>;
beforeAll(() => {
props = ({
palettes: chartPluginMock.createSetupContract().palettes,
showElasticChartsOptions: true,
vis: {
type: {
editorConfig: {
collections: {
legendPositions: [
{
text: 'Top',
value: 'top',
},
{
text: 'Left',
value: 'left',
},
{
text: 'Right',
value: 'right',
},
{
text: 'Bottom',
value: 'bottom',
},
],
},
},
},
},
stateParams: {
isDonut: true,
legendPosition: 'left',
labels: {
show: true,
},
},
setValue: jest.fn(),
} as unknown) as PieOptionsProps;
});
it('renders the nested legend switch for the elastic charts implementation', async () => {
component = mountWithIntl(<PieOptions {...props} />);
await act(async () => {
expect(findTestSubject(component, 'visTypePieNestedLegendSwitch').length).toBe(1);
});
});
it('not renders the nested legend switch for the vislib implementation', async () => {
component = mountWithIntl(<PieOptions {...props} showElasticChartsOptions={false} />);
await act(async () => {
expect(findTestSubject(component, 'visTypePieNestedLegendSwitch').length).toBe(0);
});
});
it('renders the label position dropdown for the elastic charts implementation', async () => {
component = mountWithIntl(<PieOptions {...props} />);
await act(async () => {
expect(findTestSubject(component, 'visTypePieLabelPositionSelect').length).toBe(1);
});
});
it('not renders the label position dropdown for the vislib implementation', async () => {
component = mountWithIntl(<PieOptions {...props} showElasticChartsOptions={false} />);
await act(async () => {
expect(findTestSubject(component, 'visTypePieLabelPositionSelect').length).toBe(0);
});
});
it('renders the top level switch for the elastic charts implementation', async () => {
component = mountWithIntl(<PieOptions {...props} />);
await act(async () => {
expect(findTestSubject(component, 'visTypePieTopLevelSwitch').length).toBe(1);
});
});
it('renders the top level switch for the vislib implementation', async () => {
component = mountWithIntl(<PieOptions {...props} showElasticChartsOptions={false} />);
await act(async () => {
expect(findTestSubject(component, 'visTypePieTopLevelSwitch').length).toBe(1);
});
});
it('renders the value format dropdown for the elastic charts implementation', async () => {
component = mountWithIntl(<PieOptions {...props} />);
await act(async () => {
expect(findTestSubject(component, 'visTypePieValueFormatsSelect').length).toBe(1);
});
});
it('not renders the value format dropdown for the vislib implementation', async () => {
component = mountWithIntl(<PieOptions {...props} showElasticChartsOptions={false} />);
await act(async () => {
expect(findTestSubject(component, 'visTypePieValueFormatsSelect').length).toBe(0);
});
});
it('renders the percent slider for the elastic charts implementation', async () => {
component = mountWithIntl(<PieOptions {...props} />);
await act(async () => {
expect(findTestSubject(component, 'visTypePieValueDecimals').length).toBe(1);
});
});
});

View file

@ -0,0 +1,287 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState, useEffect } from 'react';
import { METRIC_TYPE } from '@kbn/analytics';
import {
EuiPanel,
EuiTitle,
EuiSpacer,
EuiRange,
EuiFormRow,
EuiIconTip,
EuiFlexItem,
EuiFlexGroup,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
BasicOptions,
SwitchOption,
SelectOption,
PalettePicker,
} from '../../../../vis_default_editor/public';
import { VisEditorOptionsProps } from '../../../../visualizations/public';
import { TruncateLabelsOption } from './truncate_labels';
import { PaletteRegistry } from '../../../../charts/public';
import { DEFAULT_PERCENT_DECIMALS } from '../../../common';
import { PieVisParams, LabelPositions, ValueFormats, PieTypeProps } from '../../types';
import { getLabelPositions, getValuesFormats } from '../collections';
import { getLegendPositions } from '../positions';
export interface PieOptionsProps extends VisEditorOptionsProps<PieVisParams>, PieTypeProps {}
function DecimalSlider<ParamName extends string>({
paramName,
value,
setValue,
}: {
value: number;
paramName: ParamName;
setValue: (paramName: ParamName, value: number) => void;
}) {
return (
<EuiFormRow
fullWidth
label={i18n.translate('visTypePie.editors.pie.decimalSliderLabel', {
defaultMessage: 'Maximum decimal places for percent',
})}
data-test-subj="visTypePieValueDecimals"
>
<EuiRange
value={value}
min={0}
max={4}
showInput
compressed
onChange={(e) => {
setValue(paramName, Number(e.currentTarget.value));
}}
/>
</EuiFormRow>
);
}
const PieOptions = (props: PieOptionsProps) => {
const { stateParams, setValue, aggs } = props;
const setLabels = <T extends keyof PieVisParams['labels']>(
paramName: T,
value: PieVisParams['labels'][T]
) => setValue('labels', { ...stateParams.labels, [paramName]: value });
const legendUiStateValue = props.uiState?.get('vis.legendOpen');
const [palettesRegistry, setPalettesRegistry] = useState<PaletteRegistry | undefined>(undefined);
const [legendVisibility, setLegendVisibility] = useState<boolean>(() => {
const bwcLegendStateDefault = stateParams.addLegend == null ? false : stateParams.addLegend;
return props.uiState?.get('vis.legendOpen', bwcLegendStateDefault) as boolean;
});
const hasSplitChart = Boolean(aggs?.aggs?.find((agg) => agg.schema === 'split' && agg.enabled));
const segments = aggs?.aggs?.filter((agg) => agg.schema === 'segment' && agg.enabled) ?? [];
useEffect(() => {
setLegendVisibility(legendUiStateValue);
}, [legendUiStateValue]);
useEffect(() => {
const fetchPalettes = async () => {
const palettes = await props.palettes?.getPalettes();
setPalettesRegistry(palettes);
};
fetchPalettes();
}, [props.palettes]);
return (
<>
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="visTypePie.editors.pie.pieSettingsTitle"
defaultMessage="Pie settings"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<SwitchOption
label={i18n.translate('visTypePie.editors.pie.donutLabel', {
defaultMessage: 'Donut',
})}
paramName="isDonut"
value={stateParams.isDonut}
setValue={setValue}
/>
<BasicOptions {...props} legendPositions={getLegendPositions} />
{props.showElasticChartsOptions && (
<>
<EuiFormRow>
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
<EuiFlexItem grow={false}>
<SwitchOption
label={i18n.translate('visTypePie.editors.pie.distinctColorsLabel', {
defaultMessage: 'Use distinct colors per slice',
})}
paramName="distinctColors"
value={stateParams.distinctColors}
disabled={segments?.length <= 1 && !hasSplitChart}
setValue={setValue}
data-test-subj="visTypePiedistinctColorsSwitch"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiIconTip
content="Use with multi-layer chart or multiple charts."
position="top"
type="iInCircle"
color="subdued"
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
<SwitchOption
label={i18n.translate('visTypePie.editors.pie.addLegendLabel', {
defaultMessage: 'Show legend',
})}
paramName="addLegend"
value={legendVisibility}
setValue={(paramName, value) => {
setLegendVisibility(value);
setValue(paramName, value);
}}
data-test-subj="visTypePieAddLegendSwitch"
/>
<SwitchOption
label={i18n.translate('visTypePie.editors.pie.nestedLegendLabel', {
defaultMessage: 'Nest legend',
})}
paramName="nestedLegend"
value={stateParams.nestedLegend}
disabled={!stateParams.addLegend}
setValue={(paramName, value) => {
if (props.trackUiMetric) {
props.trackUiMetric(METRIC_TYPE.CLICK, 'nested_legend_switched');
}
setValue(paramName, value);
}}
data-test-subj="visTypePieNestedLegendSwitch"
/>
</>
)}
{props.showElasticChartsOptions && palettesRegistry && (
<PalettePicker
palettes={palettesRegistry}
activePalette={stateParams.palette}
paramName="palette"
setPalette={(paramName, value) => {
if (props.trackUiMetric) {
props.trackUiMetric(METRIC_TYPE.CLICK, 'palette_selected');
}
setValue(paramName, value);
}}
/>
)}
</EuiPanel>
<EuiSpacer size="s" />
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="visTypePie.editors.pie.labelsSettingsTitle"
defaultMessage="Labels settings"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<SwitchOption
label={i18n.translate('visTypePie.editors.pie.showLabelsLabel', {
defaultMessage: 'Show labels',
})}
paramName="show"
value={stateParams.labels.show}
setValue={setLabels}
/>
{props.showElasticChartsOptions && (
<SelectOption
label={i18n.translate('visTypePie.editors.pie.labelPositionLabel', {
defaultMessage: 'Label position',
})}
disabled={!stateParams.labels.show || hasSplitChart}
options={getLabelPositions}
paramName="position"
value={
hasSplitChart
? LabelPositions.INSIDE
: stateParams.labels.position || LabelPositions.DEFAULT
}
setValue={(paramName, value) => {
if (props.trackUiMetric) {
props.trackUiMetric(METRIC_TYPE.CLICK, 'label_position_selected');
}
setLabels(paramName, value);
}}
data-test-subj="visTypePieLabelPositionSelect"
/>
)}
<SwitchOption
label={i18n.translate('visTypePie.editors.pie.showTopLevelOnlyLabel', {
defaultMessage: 'Show top level only',
})}
disabled={
!stateParams.labels.show ||
(props.showElasticChartsOptions &&
stateParams.labels.position === LabelPositions.INSIDE)
}
paramName="last_level"
value={stateParams.labels.last_level}
setValue={setLabels}
data-test-subj="visTypePieTopLevelSwitch"
/>
<SwitchOption
label={i18n.translate('visTypePie.editors.pie.showValuesLabel', {
defaultMessage: 'Show values',
})}
disabled={!stateParams.labels.show}
paramName="values"
value={stateParams.labels.values}
setValue={setLabels}
/>
{props.showElasticChartsOptions && (
<>
<SelectOption
label={i18n.translate('visTypePie.editors.pie.valueFormatsLabel', {
defaultMessage: 'Values',
})}
disabled={!stateParams.labels.values}
options={getValuesFormats}
paramName="valuesFormat"
value={stateParams.labels.valuesFormat || ValueFormats.PERCENT}
setValue={(paramName, value) => {
if (props.trackUiMetric) {
props.trackUiMetric(METRIC_TYPE.CLICK, 'values_format_selected');
}
setLabels(paramName, value);
}}
data-test-subj="visTypePieValueFormatsSelect"
/>
<DecimalSlider
paramName="percentDecimals"
value={stateParams.labels.percentDecimals ?? DEFAULT_PERCENT_DECIMALS}
setValue={setLabels}
/>
</>
)}
<TruncateLabelsOption value={stateParams.labels.truncate} setValue={setLabels} />
</EuiPanel>
</>
);
};
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { PieOptions as default };

View file

@ -0,0 +1,51 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { mountWithIntl } from '@kbn/test/jest';
import { ReactWrapper } from 'enzyme';
import { TruncateLabelsOption, TruncateLabelsOptionProps } from './truncate_labels';
import { findTestSubject } from '@elastic/eui/lib/test';
describe('TruncateLabelsOption', function () {
let props: TruncateLabelsOptionProps;
let component: ReactWrapper<TruncateLabelsOptionProps>;
beforeAll(() => {
props = {
disabled: false,
value: 20,
setValue: jest.fn(),
};
});
it('renders an input type number', () => {
component = mountWithIntl(<TruncateLabelsOption {...props} />);
expect(findTestSubject(component, 'pieLabelTruncateInput').length).toBe(1);
});
it('renders the value on the input number', function () {
component = mountWithIntl(<TruncateLabelsOption {...props} />);
const input = findTestSubject(component, 'pieLabelTruncateInput');
expect(input.props().value).toBe(20);
});
it('disables the input if disabled prop is given', function () {
const newProps = { ...props, disabled: true };
component = mountWithIntl(<TruncateLabelsOption {...newProps} />);
const input = findTestSubject(component, 'pieLabelTruncateInput');
expect(input.props().disabled).toBe(true);
});
it('should set the new value', function () {
component = mountWithIntl(<TruncateLabelsOption {...props} />);
const input = findTestSubject(component, 'pieLabelTruncateInput');
input.simulate('change', { target: { value: 100 } });
expect(props.setValue).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,43 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { ChangeEvent } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiFieldNumber } from '@elastic/eui';
export interface TruncateLabelsOptionProps {
disabled?: boolean;
value?: number | null;
setValue: (paramName: 'truncate', value: null | number) => void;
}
function TruncateLabelsOption({ disabled, value = null, setValue }: TruncateLabelsOptionProps) {
const onChange = (ev: ChangeEvent<HTMLInputElement>) =>
setValue('truncate', ev.target.value === '' ? null : parseFloat(ev.target.value));
return (
<EuiFormRow
label={i18n.translate('visTypePie.controls.truncateLabel', {
defaultMessage: 'Truncate',
})}
fullWidth
display="rowCompressed"
>
<EuiFieldNumber
data-test-subj="pieLabelTruncateInput"
disabled={disabled}
value={value || ''}
onChange={onChange}
fullWidth
compressed
/>
</EuiFormRow>
);
}
export { TruncateLabelsOption };

View file

@ -0,0 +1,37 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { Position } from '@elastic/charts';
export const getLegendPositions = [
{
text: i18n.translate('visTypePie.legendPositions.topText', {
defaultMessage: 'Top',
}),
value: Position.Top,
},
{
text: i18n.translate('visTypePie.legendPositions.leftText', {
defaultMessage: 'Left',
}),
value: Position.Left,
},
{
text: i18n.translate('visTypePie.legendPositions.rightText', {
defaultMessage: 'Right',
}),
value: Position.Right,
},
{
text: i18n.translate('visTypePie.legendPositions.bottomText', {
defaultMessage: 'Bottom',
}),
value: Position.Bottom,
},
];

View file

@ -0,0 +1,113 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import {
ExpressionFunctionDefinition,
Datatable,
ExpressionValueBoxed,
} from '../../../expressions/public';
interface Arguments {
show: boolean;
position: string;
values: boolean;
truncate: number | null;
valuesFormat: string;
lastLevel: boolean;
percentDecimals: number;
}
export type ExpressionValuePieLabels = ExpressionValueBoxed<
'pie_labels',
{
show: boolean;
position: string;
values: boolean;
truncate: number | null;
valuesFormat: string;
last_level: boolean;
percentDecimals: number;
}
>;
export const pieLabels = (): ExpressionFunctionDefinition<
'pielabels',
Datatable | null,
Arguments,
ExpressionValuePieLabels
> => ({
name: 'pielabels',
help: i18n.translate('visTypePie.function.pieLabels.help', {
defaultMessage: 'Generates the pie labels object',
}),
type: 'pie_labels',
args: {
show: {
types: ['boolean'],
help: i18n.translate('visTypePie.function.pieLabels.show.help', {
defaultMessage: 'Displays the pie labels',
}),
required: true,
},
position: {
types: ['string'],
default: 'default',
help: i18n.translate('visTypePie.function.pieLabels.position.help', {
defaultMessage: 'Defines the label position',
}),
},
values: {
types: ['boolean'],
help: i18n.translate('visTypePie.function.pieLabels.values.help', {
defaultMessage: 'Displays the values inside the slices',
}),
default: true,
},
percentDecimals: {
types: ['number'],
help: i18n.translate('visTypePie.function.pieLabels.percentDecimals.help', {
defaultMessage: 'Defines the number of decimals that will appear on the values as percent',
}),
default: 2,
},
lastLevel: {
types: ['boolean'],
help: i18n.translate('visTypePie.function.pieLabels.lastLevel.help', {
defaultMessage: 'Show top level labels only',
}),
default: true,
},
truncate: {
types: ['number', 'null'],
help: i18n.translate('visTypePie.function.pieLabels.truncate.help', {
defaultMessage: 'Defines the number of characters that the slice value will display',
}),
default: null,
},
valuesFormat: {
types: ['string'],
default: 'percent',
help: i18n.translate('visTypePie.function.pieLabels.valuesFormat.help', {
defaultMessage: 'Defines the format of the values',
}),
},
},
fn: (context, args) => {
return {
type: 'pie_labels',
show: args.show,
position: args.position,
percentDecimals: args.percentDecimals,
values: args.values,
truncate: args.truncate,
valuesFormat: args.valuesFormat,
last_level: args.lastLevel,
};
},
});

View file

@ -0,0 +1,14 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { VisTypePiePlugin } from './plugin';
export { pieVisType } from './vis_type';
export { Dimensions, Dimension } from './types';
export const plugin = () => new VisTypePiePlugin();

View file

@ -0,0 +1,328 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Datatable } from '../../expressions/public';
import { BucketColumns, PieVisParams, LabelPositions, ValueFormats } from './types';
export const createMockBucketColumns = (): BucketColumns[] => {
return [
{
id: 'col-0-2',
name: 'Carrier: Descending',
meta: {
type: 'string',
field: 'Carrier',
index: 'kibana_sample_data_flights',
params: {
id: 'terms',
params: {
id: 'string',
otherBucketLabel: 'Other',
missingBucketLabel: 'Missing',
},
},
source: 'esaggs',
sourceParams: {
indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
id: '2',
enabled: true,
type: 'terms',
params: {
field: 'Carrier',
orderBy: '1',
order: 'desc',
size: 5,
otherBucket: false,
otherBucketLabel: 'Other',
missingBucket: false,
missingBucketLabel: 'Missing',
},
schema: 'segment',
},
},
format: {
id: 'terms',
params: {
id: 'string',
},
},
},
{
id: 'col-2-3',
name: 'Cancelled: Descending',
meta: {
type: 'boolean',
field: 'Cancelled',
index: 'kibana_sample_data_flights',
params: {
id: 'terms',
params: {
id: 'boolean',
otherBucketLabel: 'Other',
missingBucketLabel: 'Missing',
},
},
source: 'esaggs',
sourceParams: {
indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
id: '3',
enabled: true,
type: 'terms',
params: {
field: 'Cancelled',
orderBy: '1',
order: 'desc',
size: 5,
otherBucket: false,
otherBucketLabel: 'Other',
missingBucket: false,
missingBucketLabel: 'Missing',
},
schema: 'segment',
},
},
format: {
id: 'terms',
params: {
id: 'boolean',
},
},
},
];
};
export const createMockVisData = (): Datatable => {
return {
type: 'datatable',
rows: [
{
'col-0-2': 'Logstash Airways',
'col-2-3': 0,
'col-1-1': 797,
'col-3-1': 689,
},
{
'col-0-2': 'Logstash Airways',
'col-2-3': 1,
'col-1-1': 797,
'col-3-1': 108,
},
{
'col-0-2': 'JetBeats',
'col-2-3': 0,
'col-1-1': 766,
'col-3-1': 654,
},
{
'col-0-2': 'JetBeats',
'col-2-3': 1,
'col-1-1': 766,
'col-3-1': 112,
},
{
'col-0-2': 'ES-Air',
'col-2-3': 0,
'col-1-1': 744,
'col-3-1': 665,
},
{
'col-0-2': 'ES-Air',
'col-2-3': 1,
'col-1-1': 744,
'col-3-1': 79,
},
{
'col-0-2': 'Kibana Airlines',
'col-2-3': 0,
'col-1-1': 731,
'col-3-1': 655,
},
{
'col-0-2': 'Kibana Airlines',
'col-2-3': 1,
'col-1-1': 731,
'col-3-1': 76,
},
],
columns: [
{
id: 'col-0-2',
name: 'Carrier: Descending',
meta: {
type: 'string',
field: 'Carrier',
index: 'kibana_sample_data_flights',
params: {
id: 'terms',
params: {
id: 'string',
otherBucketLabel: 'Other',
missingBucketLabel: 'Missing',
},
},
source: 'esaggs',
sourceParams: {
indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
id: '2',
enabled: true,
type: 'terms',
params: {
field: 'Carrier',
orderBy: '1',
order: 'desc',
size: 5,
otherBucket: false,
otherBucketLabel: 'Other',
missingBucket: false,
missingBucketLabel: 'Missing',
},
schema: 'segment',
},
},
},
{
id: 'col-1-1',
name: 'Count',
meta: {
type: 'number',
index: 'kibana_sample_data_flights',
params: {
id: 'number',
},
source: 'esaggs',
sourceParams: {
indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
id: '1',
enabled: true,
type: 'count',
params: {},
schema: 'metric',
},
},
},
{
id: 'col-2-3',
name: 'Cancelled: Descending',
meta: {
type: 'boolean',
field: 'Cancelled',
index: 'kibana_sample_data_flights',
params: {
id: 'terms',
params: {
id: 'boolean',
otherBucketLabel: 'Other',
missingBucketLabel: 'Missing',
},
},
source: 'esaggs',
sourceParams: {
indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
id: '3',
enabled: true,
type: 'terms',
params: {
field: 'Cancelled',
orderBy: '1',
order: 'desc',
size: 5,
otherBucket: false,
otherBucketLabel: 'Other',
missingBucket: false,
missingBucketLabel: 'Missing',
},
schema: 'segment',
},
},
},
{
id: 'col-3-1',
name: 'Count',
meta: {
type: 'number',
index: 'kibana_sample_data_flights',
params: {
id: 'number',
},
source: 'esaggs',
sourceParams: {
indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
id: '1',
enabled: true,
type: 'count',
params: {},
schema: 'metric',
},
},
},
],
};
};
export const createMockPieParams = (): PieVisParams => {
return ({
addLegend: true,
addTooltip: true,
isDonut: true,
labels: {
position: LabelPositions.DEFAULT,
show: true,
truncate: 100,
values: true,
valuesFormat: ValueFormats.PERCENT,
percentDecimals: 2,
},
legendPosition: 'right',
nestedLegend: false,
distinctColors: false,
palette: {
name: 'default',
type: 'palette',
},
type: 'pie',
dimensions: {
metric: {
accessor: 1,
format: {
id: 'number',
},
params: {},
label: 'Count',
aggType: 'count',
},
buckets: [
{
accessor: 0,
format: {
id: 'terms',
params: {
id: 'string',
otherBucketLabel: 'Other',
missingBucketLabel: 'Missing',
},
},
label: 'Carrier: Descending',
aggType: 'terms',
},
{
accessor: 2,
format: {
id: 'terms',
params: {
id: 'boolean',
otherBucketLabel: 'Other',
missingBucketLabel: 'Missing',
},
},
label: 'Cancelled: Descending',
aggType: 'terms',
},
],
},
} as unknown) as PieVisParams;
};

View file

@ -0,0 +1,123 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { Settings, TooltipType, SeriesIdentifier } from '@elastic/charts';
import { chartPluginMock } from '../../charts/public/mocks';
import { dataPluginMock } from '../../data/public/mocks';
import { shallow, mount } from 'enzyme';
import { findTestSubject } from '@elastic/eui/lib/test';
import { act } from 'react-dom/test-utils';
import PieComponent, { PieComponentProps } from './pie_component';
import { createMockPieParams, createMockVisData } from './mocks';
jest.mock('@elastic/charts', () => {
const original = jest.requireActual('@elastic/charts');
return {
...original,
getSpecId: jest.fn(() => {}),
};
});
const chartsThemeService = chartPluginMock.createSetupContract().theme;
const palettesRegistry = chartPluginMock.createPaletteRegistry();
const visParams = createMockPieParams();
const visData = createMockVisData();
const mockState = new Map();
const uiState = {
get: jest
.fn()
.mockImplementation((key, fallback) => (mockState.has(key) ? mockState.get(key) : fallback)),
set: jest.fn().mockImplementation((key, value) => mockState.set(key, value)),
emit: jest.fn(),
setSilent: jest.fn(),
} as any;
describe('PieComponent', function () {
let wrapperProps: PieComponentProps;
beforeAll(() => {
wrapperProps = {
chartsThemeService,
palettesRegistry,
visParams,
visData,
uiState,
syncColors: false,
fireEvent: jest.fn(),
renderComplete: jest.fn(),
services: dataPluginMock.createStartContract(),
};
});
it('renders the legend on the correct position', () => {
const component = shallow(<PieComponent {...wrapperProps} />);
expect(component.find(Settings).prop('legendPosition')).toEqual('right');
});
it('renders the legend toggle component', async () => {
const component = mount(<PieComponent {...wrapperProps} />);
await act(async () => {
expect(findTestSubject(component, 'vislibToggleLegend').length).toBe(1);
});
});
it('hides the legend if the legend toggle is clicked', async () => {
const component = mount(<PieComponent {...wrapperProps} />);
findTestSubject(component, 'vislibToggleLegend').simulate('click');
await act(async () => {
expect(component.find(Settings).prop('showLegend')).toEqual(false);
});
});
it('defaults on showing the legend for the inner cicle', () => {
const component = shallow(<PieComponent {...wrapperProps} />);
expect(component.find(Settings).prop('legendMaxDepth')).toBe(1);
});
it('shows the nested legend when the user requests it', () => {
const newParams = { ...visParams, nestedLegend: true };
const newProps = { ...wrapperProps, visParams: newParams };
const component = shallow(<PieComponent {...newProps} />);
expect(component.find(Settings).prop('legendMaxDepth')).toBeUndefined();
});
it('defaults on displaying the tooltip', () => {
const component = shallow(<PieComponent {...wrapperProps} />);
expect(component.find(Settings).prop('tooltip')).toStrictEqual({ type: TooltipType.Follow });
});
it('doesnt show the tooltip when the user requests it', () => {
const newParams = { ...visParams, addTooltip: false };
const newProps = { ...wrapperProps, visParams: newParams };
const component = shallow(<PieComponent {...newProps} />);
expect(component.find(Settings).prop('tooltip')).toStrictEqual({ type: TooltipType.None });
});
it('calls filter callback', () => {
const component = shallow(<PieComponent {...wrapperProps} />);
component.find(Settings).first().prop('onElementClick')!([
[
[
{
groupByRollup: 6,
value: 6,
depth: 1,
path: [],
sortIndex: 1,
smAccessorValue: 'Logstash Airways',
},
],
{} as SeriesIdentifier,
],
]);
expect(wrapperProps.fireEvent).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,355 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { memo, useCallback, useMemo, useState, useEffect, useRef } from 'react';
import {
Chart,
Datum,
LayerValue,
Partition,
Position,
Settings,
RenderChangeListener,
TooltipProps,
TooltipType,
SeriesIdentifier,
} from '@elastic/charts';
import {
LegendToggle,
ClickTriggerEvent,
ChartsPluginSetup,
PaletteRegistry,
} from '../../charts/public';
import { DataPublicPluginStart, FieldFormat } from '../../data/public';
import type { PersistedState } from '../../visualizations/public';
import { Datatable, DatatableColumn, IInterpreterRenderHandlers } from '../../expressions/public';
import { DEFAULT_PERCENT_DECIMALS } from '../common';
import { PieVisParams, BucketColumns, ValueFormats, PieContainerDimensions } from './types';
import {
getColorPicker,
getLayers,
getLegendActions,
canFilter,
getFilterClickData,
getFilterEventData,
getConfig,
getColumns,
getSplitDimensionAccessor,
} from './utils';
import { ChartSplit, SMALL_MULTIPLES_ID } from './components/chart_split';
import './chart.scss';
declare global {
interface Window {
/**
* Flag used to enable debugState on elastic charts
*/
_echDebugStateFlag?: boolean;
}
}
export interface PieComponentProps {
visParams: PieVisParams;
visData: Datatable;
uiState: PersistedState;
fireEvent: IInterpreterRenderHandlers['event'];
renderComplete: IInterpreterRenderHandlers['done'];
chartsThemeService: ChartsPluginSetup['theme'];
palettesRegistry: PaletteRegistry;
services: DataPublicPluginStart;
syncColors: boolean;
}
const PieComponent = (props: PieComponentProps) => {
const chartTheme = props.chartsThemeService.useChartsTheme();
const chartBaseTheme = props.chartsThemeService.useChartsBaseTheme();
const [showLegend, setShowLegend] = useState<boolean>(() => {
const bwcLegendStateDefault =
props.visParams.addLegend == null ? false : props.visParams.addLegend;
return props.uiState?.get('vis.legendOpen', bwcLegendStateDefault) as boolean;
});
const [dimensions, setDimensions] = useState<undefined | PieContainerDimensions>();
const parentRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (parentRef && parentRef.current) {
const parentHeight = parentRef.current!.getBoundingClientRect().height;
const parentWidth = parentRef.current!.getBoundingClientRect().width;
setDimensions({ width: parentWidth, height: parentHeight });
}
}, [parentRef]);
const onRenderChange = useCallback<RenderChangeListener>(
(isRendered) => {
if (isRendered) {
props.renderComplete();
}
},
[props]
);
// handles slice click event
const handleSliceClick = useCallback(
(
clickedLayers: LayerValue[],
bucketColumns: Array<Partial<BucketColumns>>,
visData: Datatable,
splitChartDimension?: DatatableColumn,
splitChartFormatter?: FieldFormat
): void => {
const data = getFilterClickData(
clickedLayers,
bucketColumns,
visData,
splitChartDimension,
splitChartFormatter
);
const event = {
name: 'filterBucket',
data: { data },
};
props.fireEvent(event);
},
[props]
);
// handles legend action event data
const getLegendActionEventData = useCallback(
(visData: Datatable) => (series: SeriesIdentifier): ClickTriggerEvent | null => {
const data = getFilterEventData(visData, series);
return {
name: 'filterBucket',
data: {
negate: false,
data,
},
};
},
[]
);
const handleLegendAction = useCallback(
(event: ClickTriggerEvent, negate = false) => {
props.fireEvent({
...event,
data: {
...event.data,
negate,
},
});
},
[props]
);
const toggleLegend = useCallback(() => {
setShowLegend((value) => {
const newValue = !value;
props.uiState?.set('vis.legendOpen', newValue);
return newValue;
});
}, [props.uiState]);
useEffect(() => {
setShowLegend(props.visParams.addLegend);
props.uiState?.set('vis.legendOpen', props.visParams.addLegend);
}, [props.uiState, props.visParams.addLegend]);
const setColor = useCallback(
(newColor: string | null, seriesLabel: string | number) => {
const colors = props.uiState?.get('vis.colors') || {};
if (colors[seriesLabel] === newColor || !newColor) {
delete colors[seriesLabel];
} else {
colors[seriesLabel] = newColor;
}
props.uiState?.setSilent('vis.colors', null);
props.uiState?.set('vis.colors', colors);
props.uiState?.emit('reload');
},
[props.uiState]
);
const { visData, visParams, services, syncColors } = props;
function getSliceValue(d: Datum, metricColumn: DatatableColumn) {
if (typeof d[metricColumn.id] === 'number' && d[metricColumn.id] !== 0) {
return d[metricColumn.id];
}
return Number.EPSILON;
}
// formatters
const metricFieldFormatter = services.fieldFormats.deserialize(
visParams.dimensions.metric.format
);
const splitChartFormatter = visParams.dimensions.splitColumn
? services.fieldFormats.deserialize(visParams.dimensions.splitColumn[0].format)
: visParams.dimensions.splitRow
? services.fieldFormats.deserialize(visParams.dimensions.splitRow[0].format)
: undefined;
const percentFormatter = services.fieldFormats.deserialize({
id: 'percent',
params: {
pattern: `0,0.[${'0'.repeat(visParams.labels.percentDecimals ?? DEFAULT_PERCENT_DECIMALS)}]%`,
},
});
const { bucketColumns, metricColumn } = useMemo(() => getColumns(visParams, visData), [
visData,
visParams,
]);
const layers = useMemo(
() =>
getLayers(
bucketColumns,
visParams,
props.uiState?.get('vis.colors', {}),
visData.rows,
props.palettesRegistry,
services.fieldFormats,
syncColors
),
[
bucketColumns,
visParams,
props.uiState,
props.palettesRegistry,
visData.rows,
services.fieldFormats,
syncColors,
]
);
const config = useMemo(() => getConfig(visParams, chartTheme, dimensions), [
chartTheme,
visParams,
dimensions,
]);
const tooltip: TooltipProps = {
type: visParams.addTooltip ? TooltipType.Follow : TooltipType.None,
};
const legendPosition = visParams.legendPosition ?? Position.Right;
const legendColorPicker = useMemo(
() =>
getColorPicker(
legendPosition,
setColor,
bucketColumns,
visParams.palette.name,
visData.rows,
props.uiState,
visParams.distinctColors
),
[
legendPosition,
setColor,
bucketColumns,
visParams.palette.name,
visParams.distinctColors,
visData.rows,
props.uiState,
]
);
const splitChartColumnAccessor = visParams.dimensions.splitColumn
? getSplitDimensionAccessor(
services.fieldFormats,
visData.columns
)(visParams.dimensions.splitColumn[0])
: undefined;
const splitChartRowAccessor = visParams.dimensions.splitRow
? getSplitDimensionAccessor(
services.fieldFormats,
visData.columns
)(visParams.dimensions.splitRow[0])
: undefined;
const splitChartDimension = visParams.dimensions.splitColumn
? visData.columns[visParams.dimensions.splitColumn[0].accessor]
: visParams.dimensions.splitRow
? visData.columns[visParams.dimensions.splitRow[0].accessor]
: undefined;
return (
<div className="pieChart__container" data-test-subj="visTypePieChart">
<div className="pieChart__wrapper" ref={parentRef}>
<LegendToggle
onClick={toggleLegend}
showLegend={showLegend}
legendPosition={legendPosition}
/>
<Chart size="100%">
<ChartSplit
splitColumnAccessor={splitChartColumnAccessor}
splitRowAccessor={splitChartRowAccessor}
splitDimension={splitChartDimension}
/>
<Settings
debugState={window._echDebugStateFlag ?? false}
showLegend={showLegend}
legendPosition={legendPosition}
legendMaxDepth={visParams.nestedLegend ? undefined : 1}
legendColorPicker={legendColorPicker}
flatLegend={Boolean(splitChartDimension)}
tooltip={tooltip}
onElementClick={(args) => {
handleSliceClick(
args[0][0] as LayerValue[],
bucketColumns,
visData,
splitChartDimension,
splitChartFormatter
);
}}
legendAction={getLegendActions(
canFilter,
getLegendActionEventData(visData),
handleLegendAction,
visParams,
services.actions,
services.fieldFormats
)}
theme={chartTheme}
baseTheme={chartBaseTheme}
onRenderChange={onRenderChange}
/>
<Partition
id="pie"
smallMultiples={SMALL_MULTIPLES_ID}
data={visData.rows}
valueAccessor={(d: Datum) => getSliceValue(d, metricColumn)}
percentFormatter={(d: number) => percentFormatter.convert(d / 100)}
valueGetter={
!visParams.labels.show ||
visParams.labels.valuesFormat === ValueFormats.VALUE ||
!visParams.labels.values
? undefined
: 'percent'
}
valueFormatter={(d: number) =>
!visParams.labels.show || !visParams.labels.values
? ''
: metricFieldFormatter.convert(d)
}
layers={layers}
config={config}
topGroove={!visParams.labels.show ? 0 : undefined}
/>
</Chart>
</div>
</div>
);
};
// eslint-disable-next-line import/no-default-export
export default memo(PieComponent);

View file

@ -0,0 +1,53 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { functionWrapper } from '../../expressions/common/expression_functions/specs/tests/utils';
import { createPieVisFn } from './pie_fn';
describe('interpreter/functions#pie', () => {
const fn = functionWrapper(createPieVisFn());
const context = {
type: 'datatable',
rows: [{ 'col-0-1': 0 }],
columns: [{ id: 'col-0-1', name: 'Count' }],
};
const visConfig = {
addTooltip: true,
addLegend: true,
legendPosition: 'right',
isDonut: true,
nestedLegend: true,
distinctColors: false,
palette: 'kibana_palette',
labels: {
show: false,
values: true,
position: 'default',
valuesFormat: 'percent',
percentDecimals: 2,
truncate: 100,
},
metric: {
accessor: 0,
format: {
id: 'number',
},
params: {},
aggType: 'count',
},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('returns an object with the correct structure', async () => {
const actual = await fn(context, visConfig);
expect(actual).toMatchSnapshot();
});
});

View file

@ -0,0 +1,153 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition, Datatable, Render } from '../../expressions/public';
import { PieVisParams, PieVisConfig } from './types';
export const vislibPieName = 'pie_vis';
export interface RenderValue {
visData: Datatable;
visType: string;
visConfig: PieVisParams;
syncColors: boolean;
}
export type VisTypePieExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof vislibPieName,
Datatable,
PieVisConfig,
Render<RenderValue>
>;
export const createPieVisFn = (): VisTypePieExpressionFunctionDefinition => ({
name: vislibPieName,
type: 'render',
inputTypes: ['datatable'],
help: i18n.translate('visTypePie.functions.help', {
defaultMessage: 'Pie visualization',
}),
args: {
metric: {
types: ['vis_dimension'],
help: i18n.translate('visTypePie.function.args.metricHelpText', {
defaultMessage: 'Metric dimensions config',
}),
required: true,
},
buckets: {
types: ['vis_dimension'],
help: i18n.translate('visTypePie.function.args.bucketsHelpText', {
defaultMessage: 'Buckets dimensions config',
}),
multi: true,
},
splitColumn: {
types: ['vis_dimension'],
help: i18n.translate('visTypePie.function.args.splitColumnHelpText', {
defaultMessage: 'Split by column dimension config',
}),
multi: true,
},
splitRow: {
types: ['vis_dimension'],
help: i18n.translate('visTypePie.function.args.splitRowHelpText', {
defaultMessage: 'Split by row dimension config',
}),
multi: true,
},
addTooltip: {
types: ['boolean'],
help: i18n.translate('visTypePie.function.args.addTooltipHelpText', {
defaultMessage: 'Show tooltip on slice hover',
}),
default: true,
},
addLegend: {
types: ['boolean'],
help: i18n.translate('visTypePie.function.args.addLegendHelpText', {
defaultMessage: 'Show legend chart legend',
}),
},
legendPosition: {
types: ['string'],
help: i18n.translate('visTypePie.function.args.legendPositionHelpText', {
defaultMessage: 'Position the legend on top, bottom, left, right of the chart',
}),
},
nestedLegend: {
types: ['boolean'],
help: i18n.translate('visTypePie.function.args.nestedLegendHelpText', {
defaultMessage: 'Show a more detailed legend',
}),
default: false,
},
distinctColors: {
types: ['boolean'],
help: i18n.translate('visTypePie.function.args.distinctColorsHelpText', {
defaultMessage:
'Maps different color per slice. Slices with the same value have the same color',
}),
default: false,
},
isDonut: {
types: ['boolean'],
help: i18n.translate('visTypePie.function.args.isDonutHelpText', {
defaultMessage: 'Displays the pie chart as donut',
}),
default: false,
},
palette: {
types: ['string'],
help: i18n.translate('visTypePie.function.args.paletteHelpText', {
defaultMessage: 'Defines the chart palette name',
}),
default: 'default',
},
labels: {
types: ['pie_labels'],
help: i18n.translate('visTypePie.function.args.labelsHelpText', {
defaultMessage: 'Pie labels config',
}),
},
},
fn(context, args, handlers) {
const visConfig = {
...args,
palette: {
type: 'palette',
name: args.palette,
},
dimensions: {
metric: args.metric,
buckets: args.buckets,
splitColumn: args.splitColumn,
splitRow: args.splitRow,
},
} as PieVisParams;
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.logDatatable('default', context);
}
return {
type: 'render',
as: vislibPieName,
value: {
visData: context,
visConfig,
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
visType: 'pie',
params: {
listenOnChange: true,
},
},
};
},
});

View file

@ -0,0 +1,63 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { lazy } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { I18nProvider } from '@kbn/i18n/react';
import { ExpressionRenderDefinition } from '../../expressions/public';
import { VisualizationContainer } from '../../visualizations/public';
import type { PersistedState } from '../../visualizations/public';
import { VisTypePieDependencies } from './plugin';
import { RenderValue, vislibPieName } from './pie_fn';
const PieComponent = lazy(() => import('./pie_component'));
function shouldShowNoResultsMessage(visData: any): boolean {
const rows: object[] | undefined = visData?.rows;
const isZeroHits = visData?.hits === 0 || (rows && !rows.length);
return Boolean(isZeroHits);
}
export const getPieVisRenderer: (
deps: VisTypePieDependencies
) => ExpressionRenderDefinition<RenderValue> = ({ theme, palettes, getStartDeps }) => ({
name: vislibPieName,
displayName: 'Pie visualization',
reuseDomNode: true,
render: async (domNode, { visConfig, visData, syncColors }, handlers) => {
const showNoResult = shouldShowNoResultsMessage(visData);
handlers.onDestroy(() => {
unmountComponentAtNode(domNode);
});
const services = await getStartDeps();
const palettesRegistry = await palettes.getPalettes();
render(
<I18nProvider>
<VisualizationContainer handlers={handlers} showNoResult={showNoResult}>
<PieComponent
chartsThemeService={theme}
palettesRegistry={palettesRegistry}
visParams={visConfig}
visData={visData}
renderComplete={handlers.done}
fireEvent={handlers.event}
uiState={handlers.uiState as PersistedState}
services={services.data}
syncColors={syncColors}
/>
</VisualizationContainer>
</I18nProvider>,
domNode
);
},
});

View file

@ -0,0 +1,73 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { CoreSetup, DocLinksStart } from 'src/core/public';
import { VisualizationsSetup } from '../../visualizations/public';
import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public';
import { ChartsPluginSetup } from '../../charts/public';
import { UsageCollectionSetup } from '../../usage_collection/public';
import { DataPublicPluginStart } from '../../data/public';
import { LEGACY_CHARTS_LIBRARY } from '../../visualizations/common/constants';
import { pieLabels as pieLabelsExpressionFunction } from './expression_functions/pie_labels';
import { createPieVisFn } from './pie_fn';
import { getPieVisRenderer } from './pie_renderer';
import { pieVisType } from './vis_type';
/** @internal */
export interface VisTypePieSetupDependencies {
visualizations: VisualizationsSetup;
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;
charts: ChartsPluginSetup;
usageCollection: UsageCollectionSetup;
}
/** @internal */
export interface VisTypePiePluginStartDependencies {
data: DataPublicPluginStart;
}
/** @internal */
export interface VisTypePieDependencies {
theme: ChartsPluginSetup['theme'];
palettes: ChartsPluginSetup['palettes'];
getStartDeps: () => Promise<{ data: DataPublicPluginStart; docLinks: DocLinksStart }>;
}
export class VisTypePiePlugin {
setup(
core: CoreSetup<VisTypePiePluginStartDependencies>,
{ expressions, visualizations, charts, usageCollection }: VisTypePieSetupDependencies
) {
if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false)) {
const getStartDeps = async () => {
const [coreStart, deps] = await core.getStartServices();
return {
data: deps.data,
docLinks: coreStart.docLinks,
};
};
const trackUiMetric = usageCollection?.reportUiCounter.bind(usageCollection, 'vis_type_pie');
expressions.registerFunction(createPieVisFn);
expressions.registerRenderer(
getPieVisRenderer({ theme: charts.theme, palettes: charts.palettes, getStartDeps })
);
expressions.registerFunction(pieLabelsExpressionFunction);
visualizations.createBaseVisualization(
pieVisType({
showElasticChartsOptions: true,
palettes: charts.palettes,
trackUiMetric,
})
);
}
return {};
}
start() {}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,31 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Vis } from '../../visualizations/public';
import { PieVisParams } from './types';
import { samplePieVis } from './sample_vis.test.mocks';
import { toExpressionAst } from './to_ast';
describe('vis type pie vis toExpressionAst function', () => {
let vis: Vis<PieVisParams>;
const params = {
timefilter: {},
timeRange: {},
abortSignal: {},
} as any;
beforeEach(() => {
vis = samplePieVis as any;
});
it('should match basic snapshot', async () => {
const actual = await toExpressionAst(vis, params);
expect(actual).toMatchSnapshot();
});
});

View file

@ -0,0 +1,71 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getVisSchemas, VisToExpressionAst, SchemaConfig } from '../../visualizations/public';
import { buildExpression, buildExpressionFunction } from '../../expressions/public';
import { PieVisParams, LabelsParams } from './types';
import { vislibPieName, VisTypePieExpressionFunctionDefinition } from './pie_fn';
import { getEsaggsFn } from './to_ast_esaggs';
const prepareDimension = (params: SchemaConfig) => {
const visdimension = buildExpressionFunction('visdimension', { accessor: params.accessor });
if (params.format) {
visdimension.addArgument('format', params.format.id);
visdimension.addArgument('formatParams', JSON.stringify(params.format.params));
}
return buildExpression([visdimension]);
};
const prepareLabels = (params: LabelsParams) => {
const pieLabels = buildExpressionFunction('pielabels', {
show: params.show,
lastLevel: params.last_level,
values: params.values,
truncate: params.truncate,
});
if (params.position) {
pieLabels.addArgument('position', params.position);
}
if (params.valuesFormat) {
pieLabels.addArgument('valuesFormat', params.valuesFormat);
}
if (params.percentDecimals != null) {
pieLabels.addArgument('percentDecimals', params.percentDecimals);
}
return buildExpression([pieLabels]);
};
export const toExpressionAst: VisToExpressionAst<PieVisParams> = async (vis, params) => {
const schemas = getVisSchemas(vis, params);
const args = {
// explicitly pass each param to prevent extra values trapping
addTooltip: vis.params.addTooltip,
addLegend: vis.params.addLegend,
legendPosition: vis.params.legendPosition,
nestedLegend: vis.params?.nestedLegend,
distinctColors: vis.params?.distinctColors,
isDonut: vis.params.isDonut,
palette: vis.params?.palette?.name,
labels: prepareLabels(vis.params.labels),
metric: schemas.metric.map(prepareDimension),
buckets: schemas.segment?.map(prepareDimension),
splitColumn: schemas.split_column?.map(prepareDimension),
splitRow: schemas.split_row?.map(prepareDimension),
};
const visTypePie = buildExpressionFunction<VisTypePieExpressionFunctionDefinition>(
vislibPieName,
args
);
const ast = buildExpression([getEsaggsFn(vis), visTypePie]);
return ast.toAst();
};

View file

@ -0,0 +1,33 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Vis } from '../../visualizations/public';
import { buildExpression, buildExpressionFunction } from '../../expressions/public';
import {
EsaggsExpressionFunctionDefinition,
IndexPatternLoadExpressionFunctionDefinition,
} from '../../data/public';
import { PieVisParams } from './types';
/**
* Get esaggs expressions function
* @param vis
*/
export function getEsaggsFn(vis: Vis<PieVisParams>) {
return buildExpressionFunction<EsaggsExpressionFunctionDefinition>('esaggs', {
index: buildExpression([
buildExpressionFunction<IndexPatternLoadExpressionFunctionDefinition>('indexPatternLoad', {
id: vis.data.indexPattern!.id!,
}),
]),
metricsAtAllLevels: vis.isHierarchical(),
partialRows: false,
aggs: vis.data.aggs!.aggs.map((agg) => buildExpression(agg.toExpressionAst())),
});
}

View file

@ -0,0 +1,9 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export * from './types';

View file

@ -0,0 +1,96 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Position } from '@elastic/charts';
import { UiCounterMetricType } from '@kbn/analytics';
import { DatatableColumn, SerializedFieldFormat } from '../../../expressions/public';
import { ExpressionValueVisDimension } from '../../../visualizations/public';
import { ExpressionValuePieLabels } from '../expression_functions/pie_labels';
import { PaletteOutput, ChartsPluginSetup } from '../../../charts/public';
export interface Dimension {
accessor: number;
format: {
id?: string;
params?: SerializedFieldFormat<object>;
};
}
export interface Dimensions {
metric: Dimension;
buckets?: Dimension[];
splitRow?: Dimension[];
splitColumn?: Dimension[];
}
interface PieCommonParams {
addTooltip: boolean;
addLegend: boolean;
legendPosition: Position;
nestedLegend: boolean;
distinctColors: boolean;
isDonut: boolean;
}
export interface LabelsParams {
show: boolean;
last_level: boolean;
position: LabelPositions;
values: boolean;
truncate: number | null;
valuesFormat: ValueFormats;
percentDecimals: number;
}
export interface PieVisParams extends PieCommonParams {
dimensions: Dimensions;
labels: LabelsParams;
palette: PaletteOutput;
}
export interface PieVisConfig extends PieCommonParams {
buckets?: ExpressionValueVisDimension[];
metric: ExpressionValueVisDimension;
splitColumn?: ExpressionValueVisDimension[];
splitRow?: ExpressionValueVisDimension[];
labels: ExpressionValuePieLabels;
palette: string;
}
export interface BucketColumns extends DatatableColumn {
format?: {
id?: string;
params?: SerializedFieldFormat<object>;
};
}
export enum LabelPositions {
INSIDE = 'inside',
DEFAULT = 'default',
}
export enum ValueFormats {
PERCENT = 'percent',
VALUE = 'value',
}
export interface PieTypeProps {
showElasticChartsOptions?: boolean;
palettes?: ChartsPluginSetup['palettes'];
trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void;
}
export interface SplitDimensionParams {
order?: string;
orderBy?: string;
}
export interface PieContainerDimensions {
width: number;
height: number;
}

View file

@ -0,0 +1,98 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { DatatableColumn } from '../../../expressions/public';
import { getFilterClickData, getFilterEventData } from './filter_helpers';
import { createMockBucketColumns, createMockVisData } from '../mocks';
const bucketColumns = createMockBucketColumns();
const visData = createMockVisData();
describe('getFilterClickData', () => {
it('returns the correct filter data for the specific layer', () => {
const clickedLayers = [
{
groupByRollup: 'Logstash Airways',
value: 729,
depth: 1,
path: [],
sortIndex: 1,
smAccessorValue: '',
},
];
const data = getFilterClickData(clickedLayers, bucketColumns, visData);
expect(data.length).toEqual(clickedLayers.length);
expect(data[0].value).toEqual('Logstash Airways');
expect(data[0].row).toEqual(0);
expect(data[0].column).toEqual(0);
});
it('changes the filter if the user clicks on another layer', () => {
const clickedLayers = [
{
groupByRollup: 'ES-Air',
value: 572,
depth: 1,
path: [],
sortIndex: 1,
smAccessorValue: '',
},
];
const data = getFilterClickData(clickedLayers, bucketColumns, visData);
expect(data.length).toEqual(clickedLayers.length);
expect(data[0].value).toEqual('ES-Air');
expect(data[0].row).toEqual(4);
expect(data[0].column).toEqual(0);
});
it('returns the correct filters for small multiples', () => {
const clickedLayers = [
{
groupByRollup: 'ES-Air',
value: 572,
depth: 1,
path: [],
sortIndex: 1,
smAccessorValue: 1,
},
];
const splitDimension = {
id: 'col-2-3',
name: 'Cancelled: Descending',
} as DatatableColumn;
const data = getFilterClickData(clickedLayers, bucketColumns, visData, splitDimension);
expect(data.length).toEqual(2);
expect(data[0].value).toEqual('ES-Air');
expect(data[0].row).toEqual(5);
expect(data[0].column).toEqual(0);
expect(data[1].value).toEqual(1);
});
});
describe('getFilterEventData', () => {
it('returns the correct filter data for the specific series', () => {
const series = {
key: 'Kibana Airlines',
specId: 'pie',
};
const data = getFilterEventData(visData, series);
expect(data[0].value).toEqual('Kibana Airlines');
expect(data[0].row).toEqual(6);
expect(data[0].column).toEqual(0);
});
it('changes the filter if the user clicks on another series', () => {
const series = {
key: 'JetBeats',
specId: 'pie',
};
const data = getFilterEventData(visData, series);
expect(data[0].value).toEqual('JetBeats');
expect(data[0].row).toEqual(2);
expect(data[0].column).toEqual(0);
});
});

View file

@ -0,0 +1,89 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { LayerValue, SeriesIdentifier } from '@elastic/charts';
import { Datatable, DatatableColumn } from '../../../expressions/public';
import { DataPublicPluginStart, FieldFormat } from '../../../data/public';
import { ClickTriggerEvent } from '../../../charts/public';
import { ValueClickContext } from '../../../embeddable/public';
import { BucketColumns } from '../types';
export const canFilter = async (
event: ClickTriggerEvent | null,
actions: DataPublicPluginStart['actions']
): Promise<boolean> => {
if (!event) {
return false;
}
const filters = await actions.createFiltersFromValueClickAction(event.data);
return Boolean(filters.length);
};
export const getFilterClickData = (
clickedLayers: LayerValue[],
bucketColumns: Array<Partial<BucketColumns>>,
visData: Datatable,
splitChartDimension?: DatatableColumn,
splitChartFormatter?: FieldFormat
): ValueClickContext['data']['data'] => {
const data: ValueClickContext['data']['data'] = [];
const matchingIndex = visData.rows.findIndex((row) =>
clickedLayers.every((layer, index) => {
const columnId = bucketColumns[index].id;
if (!columnId) return;
const isCurrentLayer = row[columnId] === layer.groupByRollup;
if (!splitChartDimension) {
return isCurrentLayer;
}
const value =
splitChartFormatter?.convert(row[splitChartDimension.id]) || row[splitChartDimension.id];
return isCurrentLayer && value === layer.smAccessorValue;
})
);
data.push(
...clickedLayers.map((clickedLayer, index) => ({
column: visData.columns.findIndex((col) => col.id === bucketColumns[index].id),
row: matchingIndex,
value: clickedLayer.groupByRollup,
table: visData,
}))
);
// Allows filtering with the small multiples value
if (splitChartDimension) {
data.push({
column: visData.columns.findIndex((col) => col.id === splitChartDimension.id),
row: matchingIndex,
table: visData,
value: clickedLayers[0].smAccessorValue,
});
}
return data;
};
export const getFilterEventData = (
visData: Datatable,
series: SeriesIdentifier
): ValueClickContext['data']['data'] => {
return visData.columns.reduce<ValueClickContext['data']['data']>((acc, { id }, column) => {
const value = series.key;
const row = visData.rows.findIndex((r) => r[id] === value);
if (row > -1) {
acc.push({
table: visData,
column,
row,
value,
});
}
return acc;
}, []);
};

View file

@ -0,0 +1,116 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { LegendColorPickerProps } from '@elastic/charts';
import { EuiPopover } from '@elastic/eui';
import { mountWithIntl } from '@kbn/test/jest';
import { ComponentType, ReactWrapper } from 'enzyme';
import { getColorPicker } from './get_color_picker';
import { ColorPicker } from '../../../charts/public';
import type { PersistedState } from '../../../visualizations/public';
import { createMockBucketColumns, createMockVisData } from '../mocks';
const bucketColumns = createMockBucketColumns();
const visData = createMockVisData();
jest.mock('@elastic/charts', () => {
const original = jest.requireActual('@elastic/charts');
return {
...original,
getSpecId: jest.fn(() => {}),
};
});
describe('getColorPicker', function () {
const mockState = new Map();
const uiState = ({
get: jest
.fn()
.mockImplementation((key, fallback) => (mockState.has(key) ? mockState.get(key) : fallback)),
set: jest.fn().mockImplementation((key, value) => mockState.set(key, value)),
emit: jest.fn(),
setSilent: jest.fn(),
} as unknown) as PersistedState;
let wrapperProps: LegendColorPickerProps;
const Component: ComponentType<LegendColorPickerProps> = getColorPicker(
'left',
jest.fn(),
bucketColumns,
'default',
visData.rows,
uiState,
false
);
let wrapper: ReactWrapper<LegendColorPickerProps>;
beforeAll(() => {
wrapperProps = {
color: 'rgb(109, 204, 177)',
onClose: jest.fn(),
onChange: jest.fn(),
anchor: document.createElement('div'),
seriesIdentifiers: [
{
key: 'Logstash Airways',
specId: 'pie',
},
],
};
});
it('renders the color picker for default palette and inner layer', () => {
wrapper = mountWithIntl(<Component {...wrapperProps} />);
expect(wrapper.find(ColorPicker).length).toBe(1);
});
it('renders the picker on the correct position', () => {
wrapper = mountWithIntl(<Component {...wrapperProps} />);
expect(wrapper.find(EuiPopover).prop('anchorPosition')).toEqual('rightCenter');
});
it('converts the color to the right hex and passes it to the color picker', () => {
wrapper = mountWithIntl(<Component {...wrapperProps} />);
expect(wrapper.find(ColorPicker).prop('color')).toEqual('#6dccb1');
});
it('doesnt render the picker for default palette and not inner layer', () => {
const newProps = { ...wrapperProps, seriesIdentifier: { key: '1', specId: 'pie' } };
wrapper = mountWithIntl(<Component {...newProps} />);
expect(wrapper).toEqual({});
});
it('renders the color picker with the colorIsOverwritten prop set to false if color is not overwritten for the specific series', () => {
wrapper = mountWithIntl(<Component {...wrapperProps} />);
expect(wrapper.find(ColorPicker).prop('colorIsOverwritten')).toBe(false);
});
it('renders the color picker with the colorIsOverwritten prop set to true if color is overwritten for the specific series', () => {
uiState.set('vis.colors', { 'Logstash Airways': '#6092c0' });
wrapper = mountWithIntl(<Component {...wrapperProps} />);
expect(wrapper.find(ColorPicker).prop('colorIsOverwritten')).toBe(true);
});
it('renders the picker for kibana palette and not distinctColors', () => {
const LegacyPaletteComponent: ComponentType<LegendColorPickerProps> = getColorPicker(
'left',
jest.fn(),
bucketColumns,
'kibana_palette',
visData.rows,
uiState,
true
);
const newProps = { ...wrapperProps, seriesIdentifier: { key: '1', specId: 'pie' } };
wrapper = mountWithIntl(<LegacyPaletteComponent {...newProps} />);
expect(wrapper.find(ColorPicker).length).toBe(1);
expect(wrapper.find(ColorPicker).prop('useLegacyColors')).toBe(true);
});
});

View file

@ -0,0 +1,121 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useCallback } from 'react';
import Color from 'color';
import { LegendColorPicker, Position } from '@elastic/charts';
import { PopoverAnchorPosition, EuiPopover, EuiOutsideClickDetector } from '@elastic/eui';
import { DatatableRow } from '../../../expressions/public';
import type { PersistedState } from '../../../visualizations/public';
import { ColorPicker } from '../../../charts/public';
import { BucketColumns } from '../types';
const KEY_CODE_ENTER = 13;
function getAnchorPosition(legendPosition: Position): PopoverAnchorPosition {
switch (legendPosition) {
case Position.Bottom:
return 'upCenter';
case Position.Top:
return 'downCenter';
case Position.Left:
return 'rightCenter';
default:
return 'leftCenter';
}
}
function getLayerIndex(
seriesKey: string,
data: DatatableRow[],
layers: Array<Partial<BucketColumns>>
): number {
const row = data.find((d) => Object.keys(d).find((key) => d[key] === seriesKey));
const bucketId = row && Object.keys(row).find((key) => row[key] === seriesKey);
return layers.findIndex((layer) => layer.id === bucketId) + 1;
}
function isOnInnerLayer(
firstBucket: Partial<BucketColumns>,
data: DatatableRow[],
seriesKey: string
): DatatableRow | undefined {
return data.find((d) => firstBucket.id && d[firstBucket.id] === seriesKey);
}
export const getColorPicker = (
legendPosition: Position,
setColor: (newColor: string | null, seriesKey: string | number) => void,
bucketColumns: Array<Partial<BucketColumns>>,
palette: string,
data: DatatableRow[],
uiState: PersistedState,
distinctColors: boolean
): LegendColorPicker => ({
anchor,
color,
onClose,
onChange,
seriesIdentifiers: [seriesIdentifier],
}) => {
const seriesName = seriesIdentifier.key;
const overwriteColors: Record<string, string> = uiState?.get('vis.colors', {}) ?? {};
const colorIsOverwritten = Object.keys(overwriteColors).includes(seriesName.toString());
let keyDownEventOn = false;
const handleChange = (newColor: string | null) => {
if (newColor) {
onChange(newColor);
}
setColor(newColor, seriesName);
// close the popover if no color is applied or the user has clicked a color
if (!newColor || !keyDownEventOn) {
onClose();
}
};
const onKeyDown = (e: React.KeyboardEvent<HTMLElement>) => {
if (e.keyCode === KEY_CODE_ENTER) {
onClose?.();
}
keyDownEventOn = true;
};
const handleOutsideClick = useCallback(() => {
onClose?.();
}, [onClose]);
if (!distinctColors) {
const enablePicker = isOnInnerLayer(bucketColumns[0], data, seriesName) || !bucketColumns[0].id;
if (!enablePicker) return null;
}
const hexColor = new Color(color).hex();
return (
<EuiOutsideClickDetector onOutsideClick={handleOutsideClick}>
<EuiPopover
isOpen
ownFocus
display="block"
button={anchor}
anchorPosition={getAnchorPosition(legendPosition)}
closePopover={onClose}
panelPaddingSize="s"
>
<ColorPicker
color={palette === 'kibana_palette' ? hexColor : hexColor.toLowerCase()}
onChange={handleChange}
label={seriesName}
maxDepth={bucketColumns.length}
layerIndex={getLayerIndex(seriesName, data, bucketColumns)}
useLegacyColors={palette === 'kibana_palette'}
colorIsOverwritten={colorIsOverwritten}
onKeyDown={onKeyDown}
/>
</EuiPopover>
</EuiOutsideClickDetector>
);
};

View file

@ -0,0 +1,222 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getColumns } from './get_columns';
import { PieVisParams } from '../types';
import { createMockPieParams, createMockVisData } from '../mocks';
const visParams = createMockPieParams();
const visData = createMockVisData();
describe('getColumns', () => {
it('should return the correct bucket columns if visParams returns dimensions', () => {
const { bucketColumns } = getColumns(visParams, visData);
expect(bucketColumns.length).toEqual(visParams.dimensions.buckets?.length);
expect(bucketColumns).toEqual([
{
format: {
id: 'terms',
params: {
id: 'string',
missingBucketLabel: 'Missing',
otherBucketLabel: 'Other',
},
},
id: 'col-0-2',
meta: {
field: 'Carrier',
index: 'kibana_sample_data_flights',
params: {
id: 'terms',
params: {
id: 'string',
missingBucketLabel: 'Missing',
otherBucketLabel: 'Other',
},
},
source: 'esaggs',
sourceParams: {
enabled: true,
id: '2',
indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
params: {
field: 'Carrier',
missingBucket: false,
missingBucketLabel: 'Missing',
order: 'desc',
orderBy: '1',
otherBucket: false,
otherBucketLabel: 'Other',
size: 5,
},
schema: 'segment',
type: 'terms',
},
type: 'string',
},
name: 'Carrier: Descending',
},
{
format: {
id: 'terms',
params: {
id: 'boolean',
missingBucketLabel: 'Missing',
otherBucketLabel: 'Other',
},
},
id: 'col-2-3',
meta: {
field: 'Cancelled',
index: 'kibana_sample_data_flights',
params: {
id: 'terms',
params: {
id: 'boolean',
missingBucketLabel: 'Missing',
otherBucketLabel: 'Other',
},
},
source: 'esaggs',
sourceParams: {
enabled: true,
id: '3',
indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
params: {
field: 'Cancelled',
missingBucket: false,
missingBucketLabel: 'Missing',
order: 'desc',
orderBy: '1',
otherBucket: false,
otherBucketLabel: 'Other',
size: 5,
},
schema: 'segment',
type: 'terms',
},
type: 'boolean',
},
name: 'Cancelled: Descending',
},
]);
});
it('should return the correct metric column if visParams returns dimensions', () => {
const { metricColumn } = getColumns(visParams, visData);
expect(metricColumn).toEqual({
id: 'col-3-1',
meta: {
index: 'kibana_sample_data_flights',
params: { id: 'number' },
source: 'esaggs',
sourceParams: {
enabled: true,
id: '1',
indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
params: {},
schema: 'metric',
type: 'count',
},
type: 'number',
},
name: 'Count',
});
});
it('should return the first data column if no buckets specified', () => {
const visParamsOnlyMetric = ({
addLegend: true,
addTooltip: true,
isDonut: true,
labels: {
position: 'default',
show: true,
truncate: 100,
values: true,
valuesFormat: 'percent',
percentDecimals: 2,
},
legendPosition: 'right',
nestedLegend: false,
palette: {
name: 'default',
type: 'palette',
},
type: 'pie',
dimensions: {
metric: {
accessor: 1,
format: {
id: 'number',
},
params: {},
label: 'Count',
aggType: 'count',
},
},
} as unknown) as PieVisParams;
const { metricColumn } = getColumns(visParamsOnlyMetric, visData);
expect(metricColumn).toEqual({
id: 'col-1-1',
meta: {
index: 'kibana_sample_data_flights',
params: {
id: 'number',
},
source: 'esaggs',
sourceParams: {
enabled: true,
id: '1',
indexPatternId: 'd3d7af60-4c81-11e8-b3d7-01146121b73d',
params: {},
schema: 'metric',
type: 'count',
},
type: 'number',
},
name: 'Count',
});
});
it('should return an object with the name of the metric if no buckets specified', () => {
const visParamsOnlyMetric = ({
addLegend: true,
addTooltip: true,
isDonut: true,
labels: {
position: 'default',
show: true,
truncate: 100,
values: true,
valuesFormat: 'percent',
percentDecimals: 2,
},
legendPosition: 'right',
nestedLegend: false,
palette: {
name: 'default',
type: 'palette',
},
type: 'pie',
dimensions: {
metric: {
accessor: 1,
format: {
id: 'number',
},
params: {},
label: 'Count',
aggType: 'count',
},
},
} as unknown) as PieVisParams;
const { bucketColumns, metricColumn } = getColumns(visParamsOnlyMetric, visData);
expect(bucketColumns).toEqual([{ name: metricColumn.name }]);
});
});

View file

@ -0,0 +1,43 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { DatatableColumn, Datatable } from '../../../expressions/public';
import { BucketColumns, PieVisParams } from '../types';
export const getColumns = (
visParams: PieVisParams,
visData: Datatable
): {
metricColumn: DatatableColumn;
bucketColumns: Array<Partial<BucketColumns>>;
} => {
if (visParams.dimensions.buckets && visParams.dimensions.buckets.length > 0) {
const bucketColumns: Array<Partial<BucketColumns>> = visParams.dimensions.buckets.map(
({ accessor, format }) => ({
...visData.columns[accessor],
format,
})
);
const lastBucketId = bucketColumns[bucketColumns.length - 1].id;
const matchingIndex = visData.columns.findIndex((col) => col.id === lastBucketId);
return {
bucketColumns,
metricColumn: visData.columns[matchingIndex + 1],
};
}
const metricAccessor = visParams?.dimensions?.metric.accessor ?? 0;
const metricColumn = visData.columns[metricAccessor];
return {
metricColumn,
bucketColumns: [
{
name: metricColumn.name,
},
],
};
};

View file

@ -0,0 +1,76 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PartitionConfig, PartitionLayout, RecursivePartial, Theme } from '@elastic/charts';
import { LabelPositions, PieVisParams, PieContainerDimensions } from '../types';
const MAX_SIZE = 1000;
export const getConfig = (
visParams: PieVisParams,
chartTheme: RecursivePartial<Theme>,
dimensions?: PieContainerDimensions
): RecursivePartial<PartitionConfig> => {
// On small multiples we want the labels to only appear inside
const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow);
const usingMargin =
dimensions && !isSplitChart
? {
margin: {
top: (1 - Math.min(1, MAX_SIZE / dimensions?.height)) / 2,
bottom: (1 - Math.min(1, MAX_SIZE / dimensions?.height)) / 2,
left: (1 - Math.min(1, MAX_SIZE / dimensions?.width)) / 2,
right: (1 - Math.min(1, MAX_SIZE / dimensions?.width)) / 2,
},
}
: null;
const usingOuterSizeRatio =
dimensions && !isSplitChart
? {
outerSizeRatio: MAX_SIZE / Math.min(dimensions?.width, dimensions?.height),
}
: null;
const config: RecursivePartial<PartitionConfig> = {
partitionLayout: PartitionLayout.sunburst,
fontFamily: chartTheme.barSeriesStyle?.displayValue?.fontFamily,
...usingOuterSizeRatio,
specialFirstInnermostSector: false,
minFontSize: 10,
maxFontSize: 16,
linkLabel: {
maxCount: 5,
fontSize: 11,
textColor: chartTheme.axes?.axisTitle?.fill,
maxTextLength: visParams.labels.truncate ?? undefined,
},
sectorLineStroke: chartTheme.lineSeriesStyle?.point?.fill,
sectorLineWidth: 1.5,
circlePadding: 4,
emptySizeRatio: visParams.isDonut ? 0.3 : 0,
...usingMargin,
};
if (!visParams.labels.show) {
// Force all labels to be linked, then prevent links from showing
config.linkLabel = { maxCount: 0, maximumSection: Number.POSITIVE_INFINITY };
}
if (visParams.labels.last_level && visParams.labels.show) {
config.linkLabel = {
maxCount: Number.POSITIVE_INFINITY,
maximumSection: Number.POSITIVE_INFINITY,
};
}
if (
(visParams.labels.position === LabelPositions.INSIDE || isSplitChart) &&
visParams.labels.show
) {
config.linkLabel = { maxCount: 0 };
}
return config;
};

View file

@ -0,0 +1,30 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getDistinctSeries } from './get_distinct_series';
import { createMockVisData, createMockBucketColumns } from '../mocks';
const visData = createMockVisData();
const buckets = createMockBucketColumns();
describe('getDistinctSeries', () => {
it('should return the distinct values for all buckets', () => {
const { allSeries } = getDistinctSeries(visData.rows, buckets);
expect(allSeries).toEqual(['Logstash Airways', 'JetBeats', 'ES-Air', 'Kibana Airlines', 0, 1]);
});
it('should return only the distinct values for the parent bucket', () => {
const { parentSeries } = getDistinctSeries(visData.rows, buckets);
expect(parentSeries).toEqual(['Logstash Airways', 'JetBeats', 'ES-Air', 'Kibana Airlines']);
});
it('should return empty array for empty buckets', () => {
const { parentSeries } = getDistinctSeries(visData.rows, [{ name: 'Count' }]);
expect(parentSeries.length).toEqual(0);
});
});

View file

@ -0,0 +1,31 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { DatatableRow } from '../../../expressions/public';
import { BucketColumns } from '../types';
export const getDistinctSeries = (rows: DatatableRow[], buckets: Array<Partial<BucketColumns>>) => {
const parentBucketId = buckets[0].id;
const parentSeries: string[] = [];
const allSeries: string[] = [];
buckets.forEach(({ id }) => {
if (!id) return;
rows.forEach((row) => {
const name = row[id];
if (!allSeries.includes(name)) {
allSeries.push(name);
}
if (id === parentBucketId && !parentSeries.includes(row[parentBucketId])) {
parentSeries.push(row[parentBucketId]);
}
});
});
return {
allSeries,
parentSeries,
};
};

View file

@ -0,0 +1,114 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ShapeTreeNode } from '@elastic/charts';
import { PaletteDefinition, SeriesLayer } from '../../../charts/public';
import { computeColor } from './get_layers';
import { createMockVisData, createMockBucketColumns, createMockPieParams } from '../mocks';
const visData = createMockVisData();
const buckets = createMockBucketColumns();
const visParams = createMockPieParams();
const colors = ['color1', 'color2', 'color3', 'color4'];
export const getPaletteRegistry = () => {
const mockPalette1: jest.Mocked<PaletteDefinition> = {
id: 'default',
title: 'My Palette',
getCategoricalColor: jest.fn((layer: SeriesLayer[]) => colors[layer[0].rankAtDepth]),
getCategoricalColors: jest.fn((num: number) => colors),
toExpression: jest.fn(() => ({
type: 'expression',
chain: [
{
type: 'function',
function: 'system_palette',
arguments: {
name: ['default'],
},
},
],
})),
};
return {
get: () => mockPalette1,
getAll: () => [mockPalette1],
};
};
describe('computeColor', () => {
it('should return the correct color based on the parent sortIndex', () => {
const d = ({
dataName: 'ES-Air',
depth: 1,
sortIndex: 0,
parent: {
children: [['ES-Air'], ['Kibana Airlines']],
depth: 0,
sortIndex: 0,
},
} as unknown) as ShapeTreeNode;
const color = computeColor(
d,
false,
{},
buckets,
visData.rows,
visParams,
getPaletteRegistry(),
false
);
expect(color).toEqual(colors[0]);
});
it('slices with the same label should have the same color for small multiples', () => {
const d = ({
dataName: 'ES-Air',
depth: 1,
sortIndex: 0,
parent: {
children: [['ES-Air'], ['Kibana Airlines']],
depth: 0,
sortIndex: 0,
},
} as unknown) as ShapeTreeNode;
const color = computeColor(
d,
true,
{},
buckets,
visData.rows,
visParams,
getPaletteRegistry(),
false
);
expect(color).toEqual('color3');
});
it('returns the overwriteColor if exists', () => {
const d = ({
dataName: 'ES-Air',
depth: 1,
sortIndex: 0,
parent: {
children: [['ES-Air'], ['Kibana Airlines']],
depth: 0,
sortIndex: 0,
},
} as unknown) as ShapeTreeNode;
const color = computeColor(
d,
true,
{ 'ES-Air': '#000028' },
buckets,
visData.rows,
visParams,
getPaletteRegistry(),
false
);
expect(color).toEqual('#000028');
});
});

View file

@ -0,0 +1,186 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import {
Datum,
PartitionFillLabel,
PartitionLayer,
ShapeTreeNode,
ArrayEntry,
} from '@elastic/charts';
import { isEqual } from 'lodash';
import { SeriesLayer, PaletteRegistry, lightenColor } from '../../../charts/public';
import { DataPublicPluginStart } from '../../../data/public';
import { DatatableRow } from '../../../expressions/public';
import { BucketColumns, PieVisParams, SplitDimensionParams } from '../types';
import { getDistinctSeries } from './get_distinct_series';
const EMPTY_SLICE = Symbol('empty_slice');
export const computeColor = (
d: ShapeTreeNode,
isSplitChart: boolean,
overwriteColors: { [key: string]: string },
columns: Array<Partial<BucketColumns>>,
rows: DatatableRow[],
visParams: PieVisParams,
palettes: PaletteRegistry | null,
syncColors: boolean
) => {
const { parentSeries, allSeries } = getDistinctSeries(rows, columns);
if (visParams.distinctColors) {
const dataName = d.dataName;
if (Object.keys(overwriteColors).includes(dataName.toString())) {
return overwriteColors[dataName];
}
const index = allSeries.findIndex((name) => isEqual(name, dataName));
const isSplitParentLayer = isSplitChart && parentSeries.includes(dataName);
return palettes?.get(visParams.palette.name).getCategoricalColor(
[
{
name: dataName,
rankAtDepth: isSplitParentLayer
? parentSeries.findIndex((name) => name === dataName)
: index > -1
? index
: 0,
totalSeriesAtDepth: isSplitParentLayer ? parentSeries.length : allSeries.length || 1,
},
],
{
maxDepth: 1,
totalSeries: allSeries.length || 1,
behindText: visParams.labels.show,
syncColors,
}
);
}
const seriesLayers: SeriesLayer[] = [];
let tempParent: typeof d | typeof d['parent'] = d;
while (tempParent.parent && tempParent.depth > 0) {
const seriesName = String(tempParent.parent.children[tempParent.sortIndex][0]);
const isSplitParentLayer = isSplitChart && parentSeries.includes(seriesName);
seriesLayers.unshift({
name: seriesName,
rankAtDepth: isSplitParentLayer
? parentSeries.findIndex((name) => name === seriesName)
: tempParent.sortIndex,
totalSeriesAtDepth: isSplitParentLayer
? parentSeries.length
: tempParent.parent.children.length,
});
tempParent = tempParent.parent;
}
let overwriteColor;
seriesLayers.forEach((layer) => {
if (Object.keys(overwriteColors).includes(layer.name)) {
overwriteColor = overwriteColors[layer.name];
}
});
if (overwriteColor) {
return lightenColor(overwriteColor, seriesLayers.length, columns.length);
}
return palettes?.get(visParams.palette.name).getCategoricalColor(seriesLayers, {
behindText: visParams.labels.show,
maxDepth: columns.length,
totalSeries: rows.length,
syncColors,
});
};
export const getLayers = (
columns: Array<Partial<BucketColumns>>,
visParams: PieVisParams,
overwriteColors: { [key: string]: string },
rows: DatatableRow[],
palettes: PaletteRegistry | null,
formatter: DataPublicPluginStart['fieldFormats'],
syncColors: boolean
): PartitionLayer[] => {
const fillLabel: Partial<PartitionFillLabel> = {
textInvertible: true,
valueFont: {
fontWeight: 700,
},
};
if (!visParams.labels.values) {
fillLabel.valueFormatter = () => '';
}
const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow);
return columns.map((col) => {
return {
groupByRollup: (d: Datum) => {
return col.id ? d[col.id] : col.name;
},
showAccessor: (d: Datum) => d !== EMPTY_SLICE,
nodeLabel: (d: unknown) => {
if (d === '') {
return i18n.translate('visTypePie.emptyLabelValue', {
defaultMessage: '(empty)',
});
}
if (col.format) {
const formattedLabel = formatter.deserialize(col.format).convert(d) ?? '';
if (visParams.labels.truncate && formattedLabel.length <= visParams.labels.truncate) {
return formattedLabel;
} else {
return `${formattedLabel.slice(0, Number(visParams.labels.truncate))}\u2026`;
}
}
return String(d);
},
sortPredicate: ([name1, node1]: ArrayEntry, [name2, node2]: ArrayEntry) => {
const params = col.meta?.sourceParams?.params as SplitDimensionParams | undefined;
const sort: string | undefined = params?.orderBy;
// unconditionally put "Other" to the end (as the "Other" slice may be larger than a regular slice, yet should be at the end)
if (name1 === '__other__' && name2 !== '__other__') return 1;
if (name2 === '__other__' && name1 !== '__other__') return -1;
// metric sorting
if (sort !== '_key') {
if (params?.order === 'desc') {
return node2.value - node1.value;
} else {
return node1.value - node2.value;
}
// alphabetical sorting
} else {
if (name1 > name2) {
return params?.order === 'desc' ? -1 : 1;
}
if (name2 > name1) {
return params?.order === 'desc' ? 1 : -1;
}
}
return 0;
},
fillLabel,
shape: {
fillColor: (d) => {
const outputColor = computeColor(
d,
isSplitChart,
overwriteColors,
columns,
rows,
visParams,
palettes,
syncColors
);
return outputColor || 'rgba(0,0,0,0)';
},
},
};
});
};

View file

@ -0,0 +1,117 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { useState, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui';
import { LegendAction, SeriesIdentifier } from '@elastic/charts';
import { DataPublicPluginStart } from '../../../data/public';
import { PieVisParams } from '../types';
import { ClickTriggerEvent } from '../../../charts/public';
export const getLegendActions = (
canFilter: (
data: ClickTriggerEvent | null,
actions: DataPublicPluginStart['actions']
) => Promise<boolean>,
getFilterEventData: (series: SeriesIdentifier) => ClickTriggerEvent | null,
onFilter: (data: ClickTriggerEvent, negate?: any) => void,
visParams: PieVisParams,
actions: DataPublicPluginStart['actions'],
formatter: DataPublicPluginStart['fieldFormats']
): LegendAction => {
return ({ series: [pieSeries] }) => {
const [popoverOpen, setPopoverOpen] = useState(false);
const [isfilterable, setIsfilterable] = useState(true);
const filterData = getFilterEventData(pieSeries);
useEffect(() => {
(async () => setIsfilterable(await canFilter(filterData, actions)))();
}, [filterData]);
if (!isfilterable || !filterData) {
return null;
}
let formattedTitle = '';
if (visParams.dimensions.buckets) {
const column = visParams.dimensions.buckets.find(
(bucket) => bucket.accessor === filterData.data.data[0].column
);
formattedTitle = formatter.deserialize(column?.format).convert(pieSeries.key) ?? '';
}
const title = formattedTitle || pieSeries.key;
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 'main',
title: `${title}`,
items: [
{
name: i18n.translate('visTypePie.legend.filterForValueButtonAriaLabel', {
defaultMessage: 'Filter for value',
}),
'data-test-subj': `legend-${title}-filterIn`,
icon: <EuiIcon type="plusInCircle" size="m" />,
onClick: () => {
setPopoverOpen(false);
onFilter(filterData);
},
},
{
name: i18n.translate('visTypePie.legend.filterOutValueButtonAriaLabel', {
defaultMessage: 'Filter out value',
}),
'data-test-subj': `legend-${title}-filterOut`,
icon: <EuiIcon type="minusInCircle" size="m" />,
onClick: () => {
setPopoverOpen(false);
onFilter(filterData, true);
},
},
],
},
];
const Button = (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
marginLeft: 4,
marginRight: 4,
}}
data-test-subj={`legend-${title}`}
onKeyPress={() => undefined}
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('visTypePie.legend.filterOptionsLegend', {
defaultMessage: '{legendDataLabel}, filter options',
values: { legendDataLabel: title },
})}
>
<EuiContextMenu initialPanelId="main" panels={panels} />
</EuiPopover>
);
};
};

View file

@ -0,0 +1,31 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { AccessorFn } from '@elastic/charts';
import { FieldFormatsStart } from '../../../data/public';
import { DatatableColumn } from '../../../expressions/public';
import { Dimension } from '../types';
export const getSplitDimensionAccessor = (
fieldFormats: FieldFormatsStart,
columns: DatatableColumn[]
) => (splitDimension: Dimension): AccessorFn => {
const formatter = fieldFormats.deserialize(splitDimension.format);
const splitChartColumn = columns[splitDimension.accessor];
const accessor = splitChartColumn.id;
const fn: AccessorFn = (d) => {
const v = d[accessor];
if (v === undefined) {
return;
}
const f = formatter.convert(v);
return f;
};
return fn;
};

View file

@ -0,0 +1,16 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { getLayers } from './get_layers';
export { getColorPicker } from './get_color_picker';
export { getLegendActions } from './get_legend_actions';
export { canFilter, getFilterClickData, getFilterEventData } from './filter_helpers';
export { getConfig } from './get_config';
export { getColumns } from './get_columns';
export { getSplitDimensionAccessor } from './get_split_dimension_accessor';
export { getDistinctSeries } from './get_distinct_series';

View file

@ -0,0 +1,14 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { getPieVisTypeDefinition } from './pie';
import type { PieTypeProps } from '../types';
export const pieVisType = (props: PieTypeProps) => {
return getPieVisTypeDefinition(props);
};

View file

@ -0,0 +1,98 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { Position } from '@elastic/charts';
import { AggGroupNames } from '../../../data/public';
import { VIS_EVENT_TO_TRIGGER, VisTypeDefinition } from '../../../visualizations/public';
import { DEFAULT_PERCENT_DECIMALS } from '../../common';
import { PieVisParams, LabelPositions, ValueFormats, PieTypeProps } from '../types';
import { toExpressionAst } from '../to_ast';
import { getPieOptions } from '../editor/components';
export const getPieVisTypeDefinition = ({
showElasticChartsOptions = false,
palettes,
trackUiMetric,
}: PieTypeProps): VisTypeDefinition<PieVisParams> => ({
name: 'pie',
title: i18n.translate('visTypePie.pie.pieTitle', { defaultMessage: 'Pie' }),
icon: 'visPie',
description: i18n.translate('visTypePie.pie.pieDescription', {
defaultMessage: 'Compare data in proportion to a whole.',
}),
toExpressionAst,
getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter],
visConfig: {
defaults: {
type: 'pie',
addTooltip: true,
addLegend: !showElasticChartsOptions,
legendPosition: Position.Right,
nestedLegend: false,
distinctColors: false,
isDonut: true,
palette: {
type: 'palette',
name: 'default',
},
labels: {
show: true,
last_level: !showElasticChartsOptions,
values: true,
valuesFormat: ValueFormats.PERCENT,
percentDecimals: DEFAULT_PERCENT_DECIMALS,
truncate: 100,
position: LabelPositions.DEFAULT,
},
},
},
editorConfig: {
optionsTemplate: getPieOptions({
showElasticChartsOptions,
palettes,
trackUiMetric,
}),
schemas: [
{
group: AggGroupNames.Metrics,
name: 'metric',
title: i18n.translate('visTypePie.pie.metricTitle', {
defaultMessage: 'Slice size',
}),
min: 1,
max: 1,
aggFilter: ['sum', 'count', 'cardinality', 'top_hits'],
defaults: [{ schema: 'metric', type: 'count' }],
},
{
group: AggGroupNames.Buckets,
name: 'segment',
title: i18n.translate('visTypePie.pie.segmentTitle', {
defaultMessage: 'Split slices',
}),
min: 0,
max: Infinity,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
},
{
group: AggGroupNames.Buckets,
name: 'split',
title: i18n.translate('visTypePie.pie.splitTitle', {
defaultMessage: 'Split chart',
}),
mustBeFirst: true,
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
},
],
},
hierarchicalData: true,
requiresSearch: true,
});

View file

@ -0,0 +1,24 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./target/types",
"emitDeclarationOnly": true,
"declaration": true,
"declarationMap": true
},
"include": [
"common/**/*",
"public/**/*",
"server/**/*"
],
"references": [
{ "path": "../../core/tsconfig.json" },
{ "path": "../charts/tsconfig.json" },
{ "path": "../data/tsconfig.json" },
{ "path": "../expressions/tsconfig.json" },
{ "path": "../visualizations/tsconfig.json" },
{ "path": "../usage_collection/tsconfig.json" },
{ "path": "../vis_default_editor/tsconfig.json" },
]
}

View file

@ -4,5 +4,5 @@
"server": true,
"ui": true,
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "kibanaLegacy"],
"requiredBundles": ["kibanaUtils", "visDefaultEditor", "visTypeXy"]
"requiredBundles": ["kibanaUtils", "visDefaultEditor", "visTypeXy", "visTypePie"]
}

View file

@ -10,21 +10,15 @@ import React, { lazy } from 'react';
import { VisEditorOptionsProps } from 'src/plugins/visualizations/public';
import { GaugeVisParams } from '../../gauge';
import { PieVisParams } from '../../pie';
import { HeatmapVisParams } from '../../heatmap';
const GaugeOptionsLazy = lazy(() => import('./gauge'));
const PieOptionsLazy = lazy(() => import('./pie'));
const HeatmapOptionsLazy = lazy(() => import('./heatmap'));
export const GaugeOptions = (props: VisEditorOptionsProps<GaugeVisParams>) => (
<GaugeOptionsLazy {...props} />
);
export const PieOptions = (props: VisEditorOptionsProps<PieVisParams>) => (
<PieOptionsLazy {...props} />
);
export const HeatmapOptions = (props: VisEditorOptionsProps<HeatmapVisParams>) => (
<HeatmapOptionsLazy {...props} />
);

View file

@ -1,97 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React from 'react';
import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { VisEditorOptionsProps } from 'src/plugins/visualizations/public';
import { BasicOptions, SwitchOption } from '../../../../vis_default_editor/public';
import { TruncateLabelsOption, getPositions } from '../../../../vis_type_xy/public';
import { PieVisParams } from '../../pie';
const legendPositions = getPositions();
function PieOptions(props: VisEditorOptionsProps<PieVisParams>) {
const { stateParams, setValue } = props;
const setLabels = <T extends keyof PieVisParams['labels']>(
paramName: T,
value: PieVisParams['labels'][T]
) => setValue('labels', { ...stateParams.labels, [paramName]: value });
return (
<>
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="visTypeVislib.editors.pie.pieSettingsTitle"
defaultMessage="Pie settings"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<SwitchOption
label={i18n.translate('visTypeVislib.editors.pie.donutLabel', {
defaultMessage: 'Donut',
})}
paramName="isDonut"
value={stateParams.isDonut}
setValue={setValue}
/>
<BasicOptions {...props} legendPositions={legendPositions} />
</EuiPanel>
<EuiSpacer size="s" />
<EuiPanel paddingSize="s">
<EuiTitle size="xs">
<h3>
<FormattedMessage
id="visTypeVislib.editors.pie.labelsSettingsTitle"
defaultMessage="Labels settings"
/>
</h3>
</EuiTitle>
<EuiSpacer size="s" />
<SwitchOption
label={i18n.translate('visTypeVislib.editors.pie.showLabelsLabel', {
defaultMessage: 'Show labels',
})}
paramName="show"
value={stateParams.labels.show}
setValue={setLabels}
/>
<SwitchOption
label={i18n.translate('visTypeVislib.editors.pie.showTopLevelOnlyLabel', {
defaultMessage: 'Show top level only',
})}
paramName="last_level"
value={stateParams.labels.last_level}
setValue={setLabels}
/>
<SwitchOption
label={i18n.translate('visTypeVislib.editors.pie.showValuesLabel', {
defaultMessage: 'Show values',
})}
paramName="values"
value={stateParams.labels.values}
setValue={setLabels}
/>
<TruncateLabelsOption value={stateParams.labels.truncate} setValue={setLabels} />
</EuiPanel>
</>
);
}
// default export required for React.Lazy
// eslint-disable-next-line import/no-default-export
export { PieOptions as default };

View file

@ -6,14 +6,9 @@
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { Position } from '@elastic/charts';
import { AggGroupNames } from '../../data/public';
import { VisTypeDefinition, VIS_EVENT_TO_TRIGGER } from '../../visualizations/public';
import { pieVisType } from '../../vis_type_pie/public';
import { VisTypeDefinition } from '../../visualizations/public';
import { CommonVislibParams } from './types';
import { PieOptions } from './editor';
import { toExpressionAst } from './to_ast_pie';
export interface PieVisParams extends CommonVislibParams {
@ -27,67 +22,7 @@ export interface PieVisParams extends CommonVislibParams {
};
}
export const pieVisTypeDefinition: VisTypeDefinition<PieVisParams> = {
name: 'pie',
title: i18n.translate('visTypeVislib.pie.pieTitle', { defaultMessage: 'Pie' }),
icon: 'visPie',
description: i18n.translate('visTypeVislib.pie.pieDescription', {
defaultMessage: 'Compare data in proportion to a whole.',
}),
getSupportedTriggers: () => [VIS_EVENT_TO_TRIGGER.filter],
export const pieVisTypeDefinition = {
...pieVisType({}),
toExpressionAst,
visConfig: {
defaults: {
type: 'pie',
addTooltip: true,
addLegend: true,
legendPosition: Position.Right,
isDonut: true,
labels: {
show: false,
values: true,
last_level: true,
truncate: 100,
},
},
},
editorConfig: {
optionsTemplate: PieOptions,
schemas: [
{
group: AggGroupNames.Metrics,
name: 'metric',
title: i18n.translate('visTypeVislib.pie.metricTitle', {
defaultMessage: 'Slice size',
}),
min: 1,
max: 1,
aggFilter: ['sum', 'count', 'cardinality', 'top_hits'],
defaults: [{ schema: 'metric', type: 'count' }],
},
{
group: AggGroupNames.Buckets,
name: 'segment',
title: i18n.translate('visTypeVislib.pie.segmentTitle', {
defaultMessage: 'Split slices',
}),
min: 0,
max: Infinity,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
},
{
group: AggGroupNames.Buckets,
name: 'split',
title: i18n.translate('visTypeVislib.pie.splitTitle', {
defaultMessage: 'Split chart',
}),
mustBeFirst: true,
min: 0,
max: 1,
aggFilter: ['!geohash_grid', '!geotile_grid', '!filter'],
},
],
},
hierarchicalData: true,
requiresSearch: true,
};
} as VisTypeDefinition<PieVisParams>;

View file

@ -13,7 +13,7 @@ import { VisualizationsSetup } from '../../visualizations/public';
import { ChartsPluginSetup } from '../../charts/public';
import { DataPublicPluginStart } from '../../data/public';
import { KibanaLegacyStart } from '../../kibana_legacy/public';
import { LEGACY_CHARTS_LIBRARY } from '../../vis_type_xy/public';
import { LEGACY_CHARTS_LIBRARY } from '../../visualizations/common/constants';
import { createVisTypeVislibVisFn } from './vis_type_vislib_vis_fn';
import { createPieVisFn } from './pie_fn';
@ -53,9 +53,8 @@ export class VisTypeVislibPlugin
if (!core.uiSettings.get(LEGACY_CHARTS_LIBRARY, false)) {
// Register only non-replaced vis types
convertedTypeDefinitions.forEach(visualizations.createBaseVisualization);
visualizations.createBaseVisualization(pieVisTypeDefinition);
expressions.registerRenderer(getVislibVisRenderer(core, charts));
[createVisTypeVislibVisFn(), createPieVisFn()].forEach(expressions.registerFunction);
expressions.registerFunction(createVisTypeVislibVisFn());
} else {
// Register all vis types
visLibVisTypeDefinitions.forEach(visualizations.createBaseVisualization);

View file

@ -10,7 +10,7 @@ import { Vis } from '../../visualizations/public';
import { buildExpression } from '../../expressions/public';
import { PieVisParams } from './pie';
import { samplePieVis } from '../../vis_type_xy/public/sample_vis.test.mocks';
import { samplePieVis } from '../../vis_type_pie/public/sample_vis.test.mocks';
import { toExpressionAst } from './to_ast_pie';
jest.mock('../../expressions/public', () => ({

View file

@ -5,8 +5,8 @@
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { buildHierarchicalData, Dimensions, Dimension } from './build_hierarchical_data';
import type { Dimensions, Dimension } from '../../../../../vis_type_pie/public';
import { buildHierarchicalData } from './build_hierarchical_data';
import { Table, TableParent } from '../../types';
function tableVisResponseHandler(table: Table, dimensions: Dimensions) {

View file

@ -7,24 +7,9 @@
*/
import { toArray } from 'lodash';
import { SerializedFieldFormat } from '../../../../../expressions/common/types';
import { getFormatService } from '../../../services';
import { Table } from '../../types';
export interface Dimension {
accessor: number;
format: {
id?: string;
params?: SerializedFieldFormat<object>;
};
}
export interface Dimensions {
metric: Dimension;
buckets?: Dimension[];
splitRow?: Dimension[];
splitColumn?: Dimension[];
}
import type { Dimensions } from '../../../../../vis_type_pie/public';
interface Slice {
name: string;

View file

@ -22,5 +22,6 @@
{ "path": "../kibana_utils/tsconfig.json" },
{ "path": "../vis_default_editor/tsconfig.json" },
{ "path": "../vis_type_xy/tsconfig.json" },
{ "path": "../vis_type_pie/tsconfig.json" },
]
}

View file

@ -19,5 +19,3 @@ export enum ChartType {
* Type of xy visualizations
*/
export type XyVisType = ChartType | 'horizontal_bar';
export const LEGACY_CHARTS_LIBRARY = 'visualization:visualize:legacyChartsLibrary';

View file

@ -1,7 +1,6 @@
{
"id": "visTypeXy",
"version": "kibana",
"server": true,
"ui": true,
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"],
"requiredBundles": ["kibanaUtils", "visDefaultEditor"]

View file

@ -23,7 +23,7 @@ import {
} from './services';
import { visTypesDefinitions } from './vis_types';
import { LEGACY_CHARTS_LIBRARY } from '../common';
import { LEGACY_CHARTS_LIBRARY } from '../../visualizations/common/constants';
import { xyVisRenderer } from './vis_renderer';
import * as expressionFunctions from './expression_functions';

File diff suppressed because one or more lines are too long

View file

@ -1,46 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { CoreSetup, Plugin, UiSettingsParams } from 'kibana/server';
import { LEGACY_CHARTS_LIBRARY } from '../common';
export const getUiSettingsConfig: () => Record<string, UiSettingsParams<boolean>> = () => ({
// TODO: Remove this when vis_type_vislib is removed
// https://github.com/elastic/kibana/issues/56143
[LEGACY_CHARTS_LIBRARY]: {
name: i18n.translate('visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name', {
defaultMessage: 'Legacy charts library',
}),
requiresPageReload: true,
value: false,
description: i18n.translate(
'visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description',
{
defaultMessage: 'Enables legacy charts library for area, line and bar charts in visualize.',
}
),
category: ['visualization'],
schema: schema.boolean(),
},
});
export class VisTypeXyServerPlugin implements Plugin<object, object> {
public setup(core: CoreSetup) {
core.uiSettings.register(getUiSettingsConfig());
return {};
}
public start() {
return {};
}
}

View file

@ -7,3 +7,4 @@
*/
export const VISUALIZE_ENABLE_LABS_SETTING = 'visualize:enableLabs';
export const LEGACY_CHARTS_LIBRARY = 'visualization:visualize:legacyChartsLibrary';

View file

@ -12,5 +12,6 @@
"savedObjects"
],
"optionalPlugins": ["usageCollection"],
"requiredBundles": ["kibanaUtils", "discover"]
"requiredBundles": ["kibanaUtils", "discover"],
"extraPublicDirs": ["common/constants"]
}

View file

@ -13,6 +13,7 @@ import {
commonAddSupportOfDualIndexSelectionModeInTSVB,
commonHideTSVBLastValueIndicator,
commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel,
commonMigrateVislibPie,
commonAddEmptyValueColorRule,
} from '../migrations/visualization_common_migrations';
@ -44,6 +45,13 @@ const byValueAddEmptyValueColorRule = (state: SerializableState) => {
};
};
const byValueMigrateVislibPie = (state: SerializableState) => {
return {
...state,
savedVis: commonMigrateVislibPie(state.savedVis),
};
};
export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => {
return {
id: 'visualization',
@ -55,7 +63,7 @@ export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => {
byValueHideTSVBLastValueIndicator,
byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel
)(state),
'7.14.0': (state) => flow(byValueAddEmptyValueColorRule)(state),
'7.14.0': (state) => flow(byValueAddEmptyValueColorRule, byValueMigrateVislibPie)(state),
},
};
};

View file

@ -91,3 +91,26 @@ export const commonAddEmptyValueColorRule = (visState: any) => {
return visState;
};
export const commonMigrateVislibPie = (visState: any) => {
if (visState && visState.type === 'pie') {
const { params } = visState;
const hasPalette = params?.palette;
return {
...visState,
params: {
...visState.params,
...(!hasPalette && {
palette: {
type: 'palette',
name: 'kibana_palette',
},
}),
distinctColors: true,
},
};
}
return visState;
};

View file

@ -2114,4 +2114,52 @@ describe('migration visualization', () => {
checkRuleIsNotAddedToArray('gauge_color_rules', params, migratedParams, rule4);
});
});
describe('7.14.0 update pie visualization defaults', () => {
const migrate = (doc: any) =>
visualizationSavedObjectTypeMigrations['7.14.0'](
doc as Parameters<SavedObjectMigrationFn>[0],
savedObjectMigrationContext
);
const getTestDoc = (hasPalette = false) => ({
attributes: {
title: 'My Vis',
description: 'This is my super cool vis.',
visState: JSON.stringify({
type: 'pie',
title: '[Flights] Delay Type',
params: {
type: 'pie',
...(hasPalette && {
palette: {
type: 'palette',
name: 'default',
},
}),
},
}),
},
});
it('should decorate existing docs with the kibana legacy palette if the palette is not defined - pie', () => {
const migratedTestDoc = migrate(getTestDoc());
const { palette } = JSON.parse(migratedTestDoc.attributes.visState).params;
expect(palette.name).toEqual('kibana_palette');
});
it('should not overwrite the palette with the legacy one if the palette already exists in the saved object', () => {
const migratedTestDoc = migrate(getTestDoc(true));
const { palette } = JSON.parse(migratedTestDoc.attributes.visState).params;
expect(palette.name).toEqual('default');
});
it('should default the distinct colors per slice setting to true', () => {
const migratedTestDoc = migrate(getTestDoc());
const { distinctColors } = JSON.parse(migratedTestDoc.attributes.visState).params;
expect(distinctColors).toBe(true);
});
});
});

View file

@ -15,6 +15,7 @@ import {
commonAddSupportOfDualIndexSelectionModeInTSVB,
commonHideTSVBLastValueIndicator,
commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel,
commonMigrateVislibPie,
commonAddEmptyValueColorRule,
} from './visualization_common_migrations';
@ -990,6 +991,29 @@ const addEmptyValueColorRule: SavedObjectMigrationFn<any, any> = (doc) => {
return doc;
};
// [Pie Chart] Migrate vislib pie chart to use the new plugin vis_type_pie
const migrateVislibPie: SavedObjectMigrationFn<any, any> = (doc) => {
const visStateJSON = get(doc, 'attributes.visState');
let visState;
if (visStateJSON) {
try {
visState = JSON.parse(visStateJSON);
} catch (e) {
// Let it go, the data is invalid and we'll leave it as is
}
const newVisState = commonMigrateVislibPie(visState);
return {
...doc,
attributes: {
...doc.attributes,
visState: JSON.stringify(newVisState),
},
};
}
return doc;
};
export const visualizationSavedObjectTypeMigrations = {
/**
* We need to have this migration twice, once with a version prior to 7.0.0 once with a version
@ -1036,5 +1060,5 @@ export const visualizationSavedObjectTypeMigrations = {
hideTSVBLastValueIndicator,
removeDefaultIndexPatternAndTimeFieldFromTSVBModel
),
'7.14.0': flow(addEmptyValueColorRule),
'7.14.0': flow(addEmptyValueColorRule, migrateVislibPie),
};

View file

@ -18,7 +18,7 @@ import {
Logger,
} from '../../../core/server';
import { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants';
import { VISUALIZE_ENABLE_LABS_SETTING, LEGACY_CHARTS_LIBRARY } from '../common/constants';
import { visualizationSavedObjectType } from './saved_objects';
@ -58,6 +58,27 @@ export class VisualizationsPlugin
category: ['visualization'],
schema: schema.boolean(),
},
// TODO: Remove this when vis_type_vislib is removed
// https://github.com/elastic/kibana/issues/56143
[LEGACY_CHARTS_LIBRARY]: {
name: i18n.translate(
'visualizations.advancedSettings.visualization.legacyChartsLibrary.name',
{
defaultMessage: 'Legacy charts library',
}
),
requiresPageReload: true,
value: false,
description: i18n.translate(
'visualizations.advancedSettings.visualization.legacyChartsLibrary.description',
{
defaultMessage:
'Enables legacy charts library for area, line, bar, pie charts in visualize.',
}
),
category: ['visualization'],
schema: schema.boolean(),
},
});
if (plugins.usageCollection) {

View file

@ -97,7 +97,8 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
const pieChart = getService('pieChart');
const browser = getService('browser');
const dashboardExpect = getService('dashboardExpect');
const PageObjects = getPageObjects(['common']);
const elasticChart = getService('elasticChart');
const PageObjects = getPageObjects(['common', 'visChart']);
describe('dashboard container', () => {
before(async () => {
@ -109,6 +110,9 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide
});
it('pie charts', async () => {
if (await PageObjects.visChart.isNewChartsLibraryEnabled()) {
await elasticChart.setNewChartUiDebugFlag();
}
await pieChart.expectPieSliceCount(5);
});

View file

@ -256,8 +256,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
describe('for embeddable config color parameters on a visualization', () => {
let originalPieSliceStyle = '';
it('updates a pie slice color on a soft refresh', async function () {
await dashboardAddPanel.addVisualization(PIE_CHART_VIS_NAME);
originalPieSliceStyle = await pieChart.getPieSliceStyle(`80,000`);
await PageObjects.visChart.openLegendOptionColors(
'80,000',
`[data-title="${PIE_CHART_VIS_NAME}"]`
@ -272,7 +275,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const allPieSlicesColor = await pieChart.getAllPieSliceStyles('80,000');
let whitePieSliceCounts = 0;
allPieSlicesColor.forEach((style) => {
if (style.indexOf('rgb(255, 255, 255)') > 0) {
if (style.indexOf('rgb(255, 255, 255)') > -1) {
whitePieSliceCounts++;
}
});
@ -290,14 +293,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('resets a pie slice color to the original when removed', async function () {
const currentUrl = await getUrlFromShare();
const newUrl = currentUrl.replace(`vis:(colors:('80,000':%23FFFFFF))`, '');
const newUrl = isNewChartsLibraryEnabled
? currentUrl.replace(`'80000':%23FFFFFF`, '')
: currentUrl.replace(`vis:(colors:('80,000':%23FFFFFF))`, '');
await browser.get(newUrl.toString(), false);
await PageObjects.header.waitUntilLoadingHasFinished();
await retry.try(async () => {
const pieSliceStyle = await pieChart.getPieSliceStyle(`80,000`);
// The default green color that was stored with the visualization before any dashboard overrides.
expect(pieSliceStyle.indexOf('rgb(87, 193, 123)')).to.be.greaterThan(0);
const pieSliceStyle = await pieChart.getPieSliceStyle('80,000');
// After removing all overrides, pie slice style should match original.
expect(pieSliceStyle).to.be(originalPieSliceStyle);
});
});

View file

@ -15,6 +15,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const filterBar = getService('filterBar');
const pieChart = getService('pieChart');
const inspector = getService('inspector');
const browser = getService('browser');
const kibanaServer = getService('kibanaServer');
const PageObjects = getPageObjects([
'common',
'visualize',
@ -25,9 +28,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
]);
describe('pie chart', function () {
// Used to track flag before and after reset
let isNewChartsLibraryEnabled = false;
const vizName1 = 'Visualization PieChart';
before(async function () {
isNewChartsLibraryEnabled = await PageObjects.visChart.isNewChartsLibraryEnabled();
await PageObjects.visualize.initTests();
if (isNewChartsLibraryEnabled) {
await kibanaServer.uiSettings.update({
'visualization:visualize:legacyChartsLibrary': false,
});
await browser.refresh();
}
log.debug('navigateToApp visualize');
await PageObjects.visualize.navigateToNewAggBasedVisualization();
log.debug('clickPieChart');
@ -84,7 +96,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
describe('other bucket', () => {
it('should show other and missing bucket', async function () {
const expectedTableData = ['win 8', 'win xp', 'win 7', 'ios', 'Missing', 'Other'];
const expectedTableData = ['Missing', 'Other', 'ios', 'win 7', 'win 8', 'win xp'];
await PageObjects.visualize.navigateToNewAggBasedVisualization();
log.debug('clickPieChart');
@ -168,7 +180,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'ID',
'BR',
'Other',
];
].sort();
await PageObjects.visEditor.toggleOpenEditor(2, 'false');
await PageObjects.visEditor.clickBucket('Split slices');
@ -190,7 +202,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('should show correct result with one agg disabled', async () => {
const expectedTableData = ['win 8', 'win xp', 'win 7', 'ios', 'osx'];
const expectedTableData = ['ios', 'osx', 'win 7', 'win 8', 'win xp'];
await PageObjects.visEditor.clickBucket('Split slices');
await PageObjects.visEditor.selectAggregation('Terms');
@ -207,7 +219,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await PageObjects.visualize.loadSavedVisualization(vizName1);
await PageObjects.visChart.waitForRenderingCount();
const expectedTableData = ['win 8', 'win xp', 'win 7', 'ios', 'osx'];
const expectedTableData = ['ios', 'osx', 'win 7', 'win 8', 'win xp'];
await pieChart.expectPieChartLabels(expectedTableData);
});
@ -276,7 +288,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'ios',
'win 8',
'osx',
];
].sort();
await pieChart.expectPieChartLabels(expectedTableData);
});
@ -426,7 +438,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'CN',
'360,000',
'CN',
];
].sort();
if (await PageObjects.visChart.isNewLibraryChart('visTypePieChart')) {
await PageObjects.visEditor.clickOptionsTab();
await PageObjects.visEditor.togglePieLegend();
await PageObjects.visEditor.togglePieNestedLegend();
await PageObjects.visEditor.clickDataTab();
await PageObjects.visEditor.clickGo();
}
await PageObjects.visChart.filterLegend('CN');
await PageObjects.visChart.waitForVisualization();
await pieChart.expectPieChartLabels(expectedTableData);

View file

@ -52,6 +52,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./_point_series_options'));
loadTestFile(require.resolve('./_vertical_bar_chart'));
loadTestFile(require.resolve('./_vertical_bar_chart_nontimeindex'));
loadTestFile(require.resolve('./_pie_chart'));
});
describe('visualize ciGroup9', function () {

View file

@ -7,10 +7,12 @@
*/
import { Position } from '@elastic/charts';
import Color from 'color';
import { FtrProviderContext } from '../ftr_provider_context';
const elasticChartSelector = 'visTypeXyChart';
const xyChartSelector = 'visTypeXyChart';
const pieChartSelector = 'visTypePieChart';
export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrProviderContext) {
const testSubjects = getService('testSubjects');
@ -25,8 +27,8 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
const { common } = getPageObjects(['common']);
class VisualizeChart {
private async getDebugState() {
return await elasticChart.getChartDebugData(elasticChartSelector);
public async getEsChartDebugState(chartSelector: string) {
return await elasticChart.getChartDebugData(chartSelector);
}
/**
@ -45,32 +47,32 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
/**
* Is new charts library enabled and an area, line or histogram chart exists
*/
private async isVisTypeXYChart(): Promise<boolean> {
public async isNewLibraryChart(chartSelector: string): Promise<boolean> {
const enabled = await this.isNewChartsLibraryEnabled();
if (!enabled) {
log.debug(`-- isVisTypeXYChart = false`);
log.debug(`-- isNewLibraryChart = false`);
return false;
}
// check if enabled but not a line, area or histogram chart
// check if enabled but not a line, area, histogram or pie chart
if (await find.existsByCssSelector('.visLib__chart', 1)) {
const chart = await find.byCssSelector('.visLib__chart');
const chartType = await chart.getAttribute('data-vislib-chart-type');
if (!['line', 'area', 'histogram'].includes(chartType)) {
log.debug(`-- isVisTypeXYChart = false`);
if (!['line', 'area', 'histogram', 'pie'].includes(chartType)) {
log.debug(`-- isNewLibraryChart = false`);
return false;
}
}
if (!(await elasticChart.hasChart(elasticChartSelector, 1))) {
if (!(await elasticChart.hasChart(chartSelector, 1))) {
// not be a vislib chart type
log.debug(`-- isVisTypeXYChart = false`);
log.debug(`-- isNewLibraryChart = false`);
return false;
}
log.debug(`-- isVisTypeXYChart = true`);
log.debug(`-- isNewLibraryChart = true`);
return true;
}
@ -81,7 +83,7 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
* @param elasticChartsValue value expected for `@elastic/charts` chart
*/
public async getExpectedValue<T>(vislibValue: T, elasticChartsValue: T): Promise<T> {
if (await this.isVisTypeXYChart()) {
if (await this.isNewLibraryChart(xyChartSelector)) {
return elasticChartsValue;
}
@ -89,8 +91,8 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
}
public async getYAxisTitle() {
if (await this.isVisTypeXYChart()) {
const xAxis = (await this.getDebugState())?.axes?.y ?? [];
if (await this.isNewLibraryChart(xyChartSelector)) {
const xAxis = (await this.getEsChartDebugState(xyChartSelector))?.axes?.y ?? [];
return xAxis[0]?.title;
}
@ -99,8 +101,8 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
}
public async getXAxisLabels() {
if (await this.isVisTypeXYChart()) {
const [xAxis] = (await this.getDebugState())?.axes?.x ?? [];
if (await this.isNewLibraryChart(xyChartSelector)) {
const [xAxis] = (await this.getEsChartDebugState(xyChartSelector))?.axes?.x ?? [];
return xAxis?.labels;
}
@ -112,8 +114,8 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
}
public async getYAxisLabels() {
if (await this.isVisTypeXYChart()) {
const [yAxis] = (await this.getDebugState())?.axes?.y ?? [];
if (await this.isNewLibraryChart(xyChartSelector)) {
const [yAxis] = (await this.getEsChartDebugState(xyChartSelector))?.axes?.y ?? [];
return yAxis?.labels;
}
@ -125,8 +127,8 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
}
public async getYAxisLabelsAsNumbers() {
if (await this.isVisTypeXYChart()) {
const [yAxis] = (await this.getDebugState())?.axes?.y ?? [];
if (await this.isNewLibraryChart(xyChartSelector)) {
const [yAxis] = (await this.getEsChartDebugState(xyChartSelector))?.axes?.y ?? [];
return yAxis?.values;
}
@ -141,8 +143,8 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
* Returns an array of height values
*/
public async getAreaChartData(dataLabel: string, axis = 'ValueAxis-1') {
if (await this.isVisTypeXYChart()) {
const areas = (await this.getDebugState())?.areas ?? [];
if (await this.isNewLibraryChart(xyChartSelector)) {
const areas = (await this.getEsChartDebugState(xyChartSelector))?.areas ?? [];
const points = areas.find(({ name }) => name === dataLabel)?.lines.y1.points ?? [];
return points.map(({ y }) => y);
}
@ -183,8 +185,8 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
* @param dataLabel data-label value
*/
public async getAreaChartPaths(dataLabel: string) {
if (await this.isVisTypeXYChart()) {
const areas = (await this.getDebugState())?.areas ?? [];
if (await this.isNewLibraryChart(xyChartSelector)) {
const areas = (await this.getEsChartDebugState(xyChartSelector))?.areas ?? [];
const path = areas.find(({ name }) => name === dataLabel)?.path ?? '';
return path.split('L');
}
@ -208,9 +210,9 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
* @param axis axis value, 'ValueAxis-1' by default
*/
public async getLineChartData(dataLabel = 'Count', axis = 'ValueAxis-1') {
if (await this.isVisTypeXYChart()) {
if (await this.isNewLibraryChart(xyChartSelector)) {
// For now lines are rendered as areas to enable stacking
const areas = (await this.getDebugState())?.areas ?? [];
const areas = (await this.getEsChartDebugState(xyChartSelector))?.areas ?? [];
const lines = areas.map(({ lines: { y1 }, name, color }) => ({ ...y1, name, color }));
const points = lines.find(({ name }) => name === dataLabel)?.points ?? [];
return points.map(({ y }) => y);
@ -248,8 +250,8 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
* @param axis axis value, 'ValueAxis-1' by default
*/
public async getBarChartData(dataLabel = 'Count', axis = 'ValueAxis-1') {
if (await this.isVisTypeXYChart()) {
const bars = (await this.getDebugState())?.bars ?? [];
if (await this.isNewLibraryChart(xyChartSelector)) {
const bars = (await this.getEsChartDebugState(xyChartSelector))?.bars ?? [];
const values = bars.find(({ name }) => name === dataLabel)?.bars ?? [];
return values.map(({ y }) => y);
}
@ -293,8 +295,9 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
}
public async toggleLegend(show = true) {
const isVisTypeXYChart = await this.isVisTypeXYChart();
const legendSelector = isVisTypeXYChart ? '.echLegend' : '.visLegend';
const isVisTypeXYChart = await this.isNewLibraryChart(xyChartSelector);
const isVisTypePieChart = await this.isNewLibraryChart(pieChartSelector);
const legendSelector = isVisTypeXYChart || isVisTypePieChart ? '.echLegend' : '.visLegend';
await retry.try(async () => {
const isVisible = await find.existsByCssSelector(legendSelector);
@ -321,16 +324,25 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
}
public async doesSelectedLegendColorExist(color: string) {
if (await this.isVisTypeXYChart()) {
const items = (await this.getDebugState())?.legend?.items ?? [];
if (await this.isNewLibraryChart(xyChartSelector)) {
const items = (await this.getEsChartDebugState(xyChartSelector))?.legend?.items ?? [];
return items.some(({ color: c }) => c === color);
}
if (await this.isNewLibraryChart(pieChartSelector)) {
const slices =
(await this.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? [];
return slices.some(({ color: c }) => {
const rgbColor = new Color(color).rgb().toString();
return c === rgbColor;
});
}
return await testSubjects.exists(`legendSelectedColor-${color}`);
}
public async expectError() {
if (!this.isVisTypeXYChart()) {
if (!this.isNewLibraryChart(xyChartSelector)) {
await testSubjects.existOrFail('vislibVisualizeError');
}
}
@ -371,17 +383,25 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
public async waitForVisualization() {
await this.waitForVisualizationRenderingStabilized();
if (!(await this.isVisTypeXYChart())) {
if (!(await this.isNewLibraryChart(xyChartSelector))) {
await find.byCssSelector('.visualization');
}
}
public async getLegendEntries() {
if (await this.isVisTypeXYChart()) {
const items = (await this.getDebugState())?.legend?.items ?? [];
const isVisTypeXYChart = await this.isNewLibraryChart(xyChartSelector);
const isVisTypePieChart = await this.isNewLibraryChart(pieChartSelector);
if (isVisTypeXYChart) {
const items = (await this.getEsChartDebugState(xyChartSelector))?.legend?.items ?? [];
return items.map(({ name }) => name);
}
if (isVisTypePieChart) {
const slices =
(await this.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? [];
return slices.map(({ name }) => name);
}
const legendEntries = await find.allByCssSelector(
'.visLegend__button',
defaultFindTimeout * 2
@ -391,10 +411,13 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
);
}
public async openLegendOptionColors(name: string, chartSelector = elasticChartSelector) {
public async openLegendOptionColors(name: string, chartSelector: string) {
await this.waitForVisualizationRenderingStabilized();
await retry.try(async () => {
if (await this.isVisTypeXYChart()) {
if (
(await this.isNewLibraryChart(xyChartSelector)) ||
(await this.isNewLibraryChart(pieChartSelector))
) {
const chart = await find.byCssSelector(chartSelector);
const legendItemColor = await chart.findByCssSelector(
`[data-ech-series-name="${name}"] .echLegendItem__color`
@ -408,7 +431,9 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
await this.waitForVisualizationRenderingStabilized();
// arbitrary color chosen, any available would do
const arbitraryColor = (await this.isVisTypeXYChart()) ? '#d36086' : '#EF843C';
const arbitraryColor = (await this.isNewLibraryChart(xyChartSelector))
? '#d36086'
: '#EF843C';
const isOpen = await this.doesLegendColorChoiceExist(arbitraryColor);
if (!isOpen) {
throw new Error('legend color selector not open');
@ -524,8 +549,8 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
}
public async getRightValueAxesCount() {
if (await this.isVisTypeXYChart()) {
const yAxes = (await this.getDebugState())?.axes?.y ?? [];
if (await this.isNewLibraryChart(xyChartSelector)) {
const yAxes = (await this.getEsChartDebugState(xyChartSelector))?.axes?.y ?? [];
return yAxes.filter(({ position }) => position === Position.Right).length;
}
const axes = await find.allByCssSelector('.visAxis__column--right g.axis');
@ -544,8 +569,8 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
}
public async getHistogramSeriesCount() {
if (await this.isVisTypeXYChart()) {
const bars = (await this.getDebugState())?.bars ?? [];
if (await this.isNewLibraryChart(xyChartSelector)) {
const bars = (await this.getEsChartDebugState(xyChartSelector))?.bars ?? [];
return bars.filter(({ visible }) => visible).length;
}
@ -554,8 +579,11 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
}
public async getGridLines(): Promise<Array<{ x: number; y: number }>> {
if (await this.isVisTypeXYChart()) {
const { x, y } = (await this.getDebugState())?.axes ?? { x: [], y: [] };
if (await this.isNewLibraryChart(xyChartSelector)) {
const { x, y } = (await this.getEsChartDebugState(xyChartSelector))?.axes ?? {
x: [],
y: [],
};
return [...x, ...y].flatMap(({ gridlines }) => gridlines);
}
@ -574,8 +602,8 @@ export function VisualizeChartPageProvider({ getService, getPageObjects }: FtrPr
}
public async getChartValues() {
if (await this.isVisTypeXYChart()) {
const barSeries = (await this.getDebugState())?.bars ?? [];
if (await this.isNewLibraryChart(xyChartSelector)) {
const barSeries = (await this.getEsChartDebugState(xyChartSelector))?.bars ?? [];
return barSeries.filter(({ visible }) => visible).flatMap((bars) => bars.labels);
}

View file

@ -327,6 +327,14 @@ export function VisualizeEditorPageProvider({ getService, getPageObjects }: FtrP
await testSubjects.click('visualizeEditorAutoButton');
}
public async togglePieLegend() {
await testSubjects.click('visTypePieAddLegendSwitch');
}
public async togglePieNestedLegend() {
await testSubjects.click('visTypePieNestedLegendSwitch');
}
public async isApplyEnabled() {
const applyButton = await testSubjects.find('visualizeEditorRenderButton');
return await applyButton.isEnabled();

View file

@ -9,6 +9,8 @@
import expect from '@kbn/expect';
import { FtrService } from '../../ftr_provider_context';
const pieChartSelector = 'visTypePieChart';
export class PieChartService extends FtrService {
private readonly log = this.ctx.getService('log');
private readonly retry = this.ctx.getService('retry');
@ -18,20 +20,42 @@ export class PieChartService extends FtrService {
private readonly find = this.ctx.getService('find');
private readonly panelActions = this.ctx.getService('dashboardPanelActions');
private readonly defaultFindTimeout = this.config.get('timeouts.find');
private readonly pageObjects = this.ctx.getPageObjects(['visChart']);
private readonly filterActionText = 'Apply filter to current view';
async clickOnPieSlice(name?: string) {
this.log.debug(`PieChart.clickOnPieSlice(${name})`);
if (name) {
await this.testSubjects.click(`pieSlice-${name.split(' ').join('-')}`);
if (await this.pageObjects.visChart.isNewLibraryChart(pieChartSelector)) {
const slices =
(await this.pageObjects.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]
?.partitions ?? [];
let sliceLabel = name || slices[0].name;
if (name === 'Other') {
sliceLabel = '__other__';
}
const pieSlice = slices.find((slice) => slice.name === sliceLabel);
const pie = await this.testSubjects.find(pieChartSelector);
if (pieSlice) {
const pieSize = await pie.getSize();
const pieHeight = pieSize.height;
const pieWidth = pieSize.width;
await pie.clickMouseButton({
xOffset: pieSlice.coords[0] - Math.floor(pieWidth / 2),
yOffset: Math.floor(pieHeight / 2) - pieSlice.coords[1],
});
}
} else {
// If no pie slice has been provided, find the first one available.
await this.retry.try(async () => {
const slices = await this.find.allByCssSelector('svg > g > g.arcs > path.slice');
this.log.debug('Slices found:' + slices.length);
return slices[0].click();
});
if (name) {
await this.testSubjects.click(`pieSlice-${name.split(' ').join('-')}`);
} else {
// If no pie slice has been provided, find the first one available.
await this.retry.try(async () => {
const slices = await this.find.allByCssSelector('svg > g > g.arcs > path.slice');
this.log.debug('Slices found:' + slices.length);
return slices[0].click();
});
}
}
}
@ -63,12 +87,30 @@ export class PieChartService extends FtrService {
async getPieSliceStyle(name: string) {
this.log.debug(`VisualizePage.getPieSliceStyle(${name})`);
if (await this.pageObjects.visChart.isNewLibraryChart(pieChartSelector)) {
const slices =
(await this.pageObjects.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]
?.partitions ?? [];
const selectedSlice = slices.filter((slice) => {
return slice.name.toString() === name.replace(',', '');
});
return selectedSlice[0].color;
}
const pieSlice = await this.getPieSlice(name);
return await pieSlice.getAttribute('style');
}
async getAllPieSliceStyles(name: string) {
this.log.debug(`VisualizePage.getAllPieSliceStyles(${name})`);
if (await this.pageObjects.visChart.isNewLibraryChart(pieChartSelector)) {
const slices =
(await this.pageObjects.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]
?.partitions ?? [];
const selectedSlice = slices.filter((slice) => {
return slice.name.toString() === name.replace(',', '');
});
return selectedSlice.map((slice) => slice.color);
}
const pieSlices = await this.getAllPieSlices(name);
return await Promise.all(
pieSlices.map(async (pieSlice) => await pieSlice.getAttribute('style'))
@ -87,6 +129,24 @@ export class PieChartService extends FtrService {
}
async getPieChartLabels() {
if (await this.pageObjects.visChart.isNewLibraryChart(pieChartSelector)) {
const slices =
(await this.pageObjects.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]
?.partitions ?? [];
return slices.map((slice) => {
if (slice.name === '__missing__') {
return 'Missing';
} else if (slice.name === '__other__') {
return 'Other';
} else if (typeof slice.name === 'number') {
// debugState of escharts returns the numbers without comma
const val = slice.name as number;
return val.toString().replace(/\B(?<!\.\d*)(?=(\d{3})+(?!\d))/g, ',');
} else {
return slice.name;
}
});
}
const chartTypes = await this.find.allByCssSelector('path.slice', this.defaultFindTimeout * 2);
return await Promise.all(
chartTypes.map(async (chart) => await chart.getAttribute('data-label'))
@ -95,10 +155,23 @@ export class PieChartService extends FtrService {
async getPieSliceCount() {
this.log.debug('PieChart.getPieSliceCount');
if (await this.pageObjects.visChart.isNewLibraryChart(pieChartSelector)) {
const slices =
(await this.pageObjects.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]
?.partitions ?? [];
return slices?.length;
}
const slices = await this.find.allByCssSelector('svg > g > g.arcs > path.slice');
return slices.length;
}
async expectPieSliceCountEsCharts(expectedCount: number) {
const slices =
(await this.pageObjects.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]
?.partitions ?? [];
expect(slices.length).to.be(expectedCount);
}
async expectPieSliceCount(expectedCount: number) {
this.log.debug(`PieChart.expectPieSliceCount(${expectedCount})`);
await this.retry.try(async () => {
@ -111,7 +184,7 @@ export class PieChartService extends FtrService {
this.log.debug(`PieChart.expectPieChartLabels(${expectedLabels.join(',')})`);
await this.retry.try(async () => {
const pieData = await this.getPieChartLabels();
expect(pieData).to.eql(expectedLabels);
expect(pieData.sort()).to.eql(expectedLabels);
});
}
}

View file

@ -65,6 +65,7 @@
{ "path": "./src/plugins/vis_type_vislib/tsconfig.json" },
{ "path": "./src/plugins/vis_type_vega/tsconfig.json" },
{ "path": "./src/plugins/vis_type_xy/tsconfig.json" },
{ "path": "./src/plugins/vis_type_pie/tsconfig.json" },
{ "path": "./src/plugins/visualizations/tsconfig.json" },
{ "path": "./src/plugins/visualize/tsconfig.json" },
{ "path": "./src/plugins/index_pattern_management/tsconfig.json" },

View file

@ -52,6 +52,7 @@
{ "path": "./src/plugins/vis_type_vislib/tsconfig.json" },
{ "path": "./src/plugins/vis_type_vega/tsconfig.json" },
{ "path": "./src/plugins/vis_type_xy/tsconfig.json" },
{ "path": "./src/plugins/vis_type_pie/tsconfig.json" },
{ "path": "./src/plugins/visualizations/tsconfig.json" },
{ "path": "./src/plugins/visualize/tsconfig.json" },
{ "path": "./src/plugins/index_pattern_management/tsconfig.json" },

View file

@ -4908,12 +4908,6 @@
"visTypeVislib.editors.heatmap.heatmapSettingsTitle": "ヒートマップ設定",
"visTypeVislib.editors.heatmap.highlightLabel": "ハイライト範囲",
"visTypeVislib.editors.heatmap.highlightLabelTooltip": "チャートのカーソルを当てた部分と凡例の対応するラベルをハイライトします。",
"visTypeVislib.editors.pie.donutLabel": "ドーナッツ",
"visTypeVislib.editors.pie.labelsSettingsTitle": "ラベル設定",
"visTypeVislib.editors.pie.pieSettingsTitle": "パイ設定",
"visTypeVislib.editors.pie.showLabelsLabel": "ラベルを表示",
"visTypeVislib.editors.pie.showTopLevelOnlyLabel": "トップレベルのみ表示",
"visTypeVislib.editors.pie.showValuesLabel": "値を表示",
"visTypeVislib.functions.pie.help": "パイビジュアライゼーション",
"visTypeVislib.functions.vislib.help": "Vislib ビジュアライゼーション",
"visTypeVislib.gauge.alignmentAutomaticTitle": "自動",
@ -4935,11 +4929,17 @@
"visTypeVislib.heatmap.metricTitle": "値",
"visTypeVislib.heatmap.segmentTitle": "X 軸",
"visTypeVislib.heatmap.splitTitle": "チャートを分割",
"visTypeVislib.pie.metricTitle": "スライスサイズ",
"visTypeVislib.pie.pieDescription": "全体に対する比率でデータを比較します。",
"visTypeVislib.pie.pieTitle": "円",
"visTypeVislib.pie.segmentTitle": "スライスの分割",
"visTypeVislib.pie.splitTitle": "チャートを分割",
"visTypePie.pie.metricTitle": "スライスサイズ",
"visTypePie.pie.pieDescription": "全体に対する比率でデータを比較します。",
"visTypePie.pie.pieTitle": "円",
"visTypePie.pie.segmentTitle": "スライスの分割",
"visTypePie.pie.splitTitle": "チャートを分割",
"visTypePie.editors.pie.donutLabel": "ドーナッツ",
"visTypePie.editors.pie.labelsSettingsTitle": "ラベル設定",
"visTypePie.editors.pie.pieSettingsTitle": "パイ設定",
"visTypePie.editors.pie.showLabelsLabel": "ラベルを表示",
"visTypePie.editors.pie.showTopLevelOnlyLabel": "トップレベルのみ表示",
"visTypePie.editors.pie.showValuesLabel": "値を表示",
"visTypeVislib.vislib.errors.noResultsFoundTitle": "結果が見つかりませんでした",
"visTypeVislib.vislib.heatmap.maxBucketsText": "定義された数列が多すぎます ({nr}) 。構成されている最大値は {max} です。",
"visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "値 {legendDataLabel} でフィルタリング",
@ -4951,8 +4951,8 @@
"visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}、トグルオプション",
"visTypeVislib.vislib.tooltip.fieldLabel": "フィールド",
"visTypeVislib.vislib.tooltip.valueLabel": "値",
"visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description": "Visualizeでエリア、折れ線、棒グラフのレガシーグラフライブラリを有効にします。",
"visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name": "レガシーグラフライブラリ",
"visualizations.advancedSettings.visualization.legacyChartsLibrary.description": "Visualizeでエリア、折れ線、棒グラフのレガシーグラフライブラリを有効にします。",
"visualizations.advancedSettings.visualization.legacyChartsLibrary.name": "レガシーグラフライブラリ",
"visTypeXy.aggResponse.allDocsTitle": "すべてのドキュメント",
"visTypeXy.area.areaDescription": "軸と線の間のデータを強調します。",
"visTypeXy.area.areaTitle": "エリア",

View file

@ -4935,12 +4935,6 @@
"visTypeVislib.editors.heatmap.heatmapSettingsTitle": "热图设置",
"visTypeVislib.editors.heatmap.highlightLabel": "高亮范围",
"visTypeVislib.editors.heatmap.highlightLabelTooltip": "高亮显示图表中鼠标悬停的范围以及图例中对应的标签。",
"visTypeVislib.editors.pie.donutLabel": "圆环图",
"visTypeVislib.editors.pie.labelsSettingsTitle": "标签设置",
"visTypeVislib.editors.pie.pieSettingsTitle": "饼图设置",
"visTypeVislib.editors.pie.showLabelsLabel": "显示标签",
"visTypeVislib.editors.pie.showTopLevelOnlyLabel": "仅显示顶级",
"visTypeVislib.editors.pie.showValuesLabel": "显示值",
"visTypeVislib.functions.pie.help": "饼图可视化",
"visTypeVislib.functions.vislib.help": "Vislib 可视化",
"visTypeVislib.gauge.alignmentAutomaticTitle": "自动",
@ -4962,11 +4956,17 @@
"visTypeVislib.heatmap.metricTitle": "值",
"visTypeVislib.heatmap.segmentTitle": "X 轴",
"visTypeVislib.heatmap.splitTitle": "拆分图表",
"visTypeVislib.pie.metricTitle": "切片大小",
"visTypeVislib.pie.pieDescription": "以整体的比例比较数据。",
"visTypeVislib.pie.pieTitle": "饼图",
"visTypeVislib.pie.segmentTitle": "拆分切片",
"visTypeVislib.pie.splitTitle": "拆分图表",
"visTypePie.pie.metricTitle": "切片大小",
"visTypePie.pie.pieDescription": "以整体的比例比较数据。",
"visTypePie.pie.pieTitle": "饼图",
"visTypePie.pie.segmentTitle": "拆分切片",
"visTypePie.pie.splitTitle": "拆分图表",
"visTypePie.editors.pie.donutLabel": "圆环图",
"visTypePie.editors.pie.labelsSettingsTitle": "标签设置",
"visTypePie.editors.pie.pieSettingsTitle": "饼图设置",
"visTypePie.editors.pie.showLabelsLabel": "显示标签",
"visTypePie.editors.pie.showTopLevelOnlyLabel": "仅显示顶级",
"visTypePie.editors.pie.showValuesLabel": "显示值",
"visTypeVislib.vislib.errors.noResultsFoundTitle": "找不到结果",
"visTypeVislib.vislib.heatmap.maxBucketsText": "定义了过多的序列 ({nr})。配置的最大值为 {max}。",
"visTypeVislib.vislib.legend.filterForValueButtonAriaLabel": "筛留值 {legendDataLabel}",
@ -4978,8 +4978,8 @@
"visTypeVislib.vislib.legend.toggleOptionsButtonAriaLabel": "{legendDataLabel}, 切换选项",
"visTypeVislib.vislib.tooltip.fieldLabel": "字段",
"visTypeVislib.vislib.tooltip.valueLabel": "值",
"visTypeXy.advancedSettings.visualization.legacyChartsLibrary.description": "在 Visualize 中启用面积图、折线图和条形图的旧版图表库。",
"visTypeXy.advancedSettings.visualization.legacyChartsLibrary.name": "旧版图表库",
"visualizations.advancedSettings.visualization.legacyChartsLibrary.description": "在 Visualize 中启用面积图、折线图和条形图的旧版图表库。",
"visualizations.advancedSettings.visualization.legacyChartsLibrary.name": "旧版图表库",
"visTypeXy.aggResponse.allDocsTitle": "所有文档",
"visTypeXy.area.areaDescription": "突出轴与线之间的数据。",
"visTypeXy.area.areaTitle": "面积图",

View file

@ -24,6 +24,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'settings',
'copySavedObjectsToSpace',
]);
const queryBar = getService('queryBar');
const pieChart = getService('pieChart');
const log = getService('log');
const browser = getService('browser');
@ -31,6 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const filterBar = getService('filterBar');
const security = getService('security');
const spaces = getService('spaces');
const elasticChart = getService('elasticChart');
describe('Dashboard to dashboard drilldown', function () {
describe('Create & use drilldowns', () => {
@ -211,7 +213,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await navigateWithinDashboard(async () => {
await dashboardDrilldownPanelActions.clickActionByText(DRILLDOWN_TO_PIE_CHART_NAME);
});
await pieChart.expectPieSliceCount(10);
await elasticChart.setNewChartUiDebugFlag();
await queryBar.submitQuery();
await pieChart.expectPieSliceCountEsCharts(10);
});
});
});