mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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>
This commit is contained in:
parent
c260407640
commit
a9a9013120
89 changed files with 5602 additions and 1678 deletions
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -24,6 +24,7 @@
|
|||
/src/plugins/vis_type_vega/ @elastic/kibana-app
|
||||
/src/plugins/vis_type_vislib/ @elastic/kibana-app
|
||||
/src/plugins/vis_type_xy/ @elastic/kibana-app
|
||||
/src/plugins/vis_type_pie/ @elastic/kibana-app
|
||||
/src/plugins/visualize/ @elastic/kibana-app
|
||||
/src/plugins/visualizations/ @elastic/kibana-app
|
||||
/packages/kbn-tinymath/ @elastic/kibana-app
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -265,6 +265,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.
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@ pageLoadAssetSize:
|
|||
visDefaultEditor: 50178
|
||||
visTypeMarkdown: 30896
|
||||
visTypeMetric: 42790
|
||||
visTypePie: 34051
|
||||
visTypeTable: 94934
|
||||
visTypeTagcloud: 37575
|
||||
visTypeTimelion: 68883
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
1
src/plugins/vis_type_pie/README.md
Normal file
1
src/plugins/vis_type_pie/README.md
Normal 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.
|
|
@ -6,6 +6,4 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { VisTypeXyServerPlugin } from './plugin';
|
||||
|
||||
export const plugin = () => new VisTypeXyServerPlugin();
|
||||
export const DEFAULT_PERCENT_DECIMALS = 2;
|
13
src/plugins/vis_type_pie/jest.config.js
Normal file
13
src/plugins/vis_type_pie/jest.config.js
Normal 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'],
|
||||
};
|
8
src/plugins/vis_type_pie/kibana.json
Normal file
8
src/plugins/vis_type_pie/kibana.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"id": "visTypePie",
|
||||
"version": "kibana",
|
||||
"ui": true,
|
||||
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"],
|
||||
"requiredBundles": ["visDefaultEditor"]
|
||||
}
|
||||
|
73
src/plugins/vis_type_pie/public/__snapshots__/pie_fn.test.ts.snap
generated
Normal file
73
src/plugins/vis_type_pie/public/__snapshots__/pie_fn.test.ts.snap
generated
Normal 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",
|
||||
},
|
||||
}
|
||||
`;
|
122
src/plugins/vis_type_pie/public/__snapshots__/to_ast.test.ts.snap
generated
Normal file
122
src/plugins/vis_type_pie/public/__snapshots__/to_ast.test.ts.snap
generated
Normal 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",
|
||||
}
|
||||
`;
|
18
src/plugins/vis_type_pie/public/chart.scss
Normal file
18
src/plugins/vis_type_pie/public/chart.scss
Normal 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;
|
||||
}
|
67
src/plugins/vis_type_pie/public/components/chart_split.tsx
Normal file
67
src/plugins/vis_type_pie/public/components/chart_split.tsx
Normal 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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
40
src/plugins/vis_type_pie/public/editor/collections.ts
Normal file
40
src/plugins/vis_type_pie/public/editor/collections.ts
Normal 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,
|
||||
},
|
||||
];
|
26
src/plugins/vis_type_pie/public/editor/components/index.tsx
Normal file
26
src/plugins/vis_type_pie/public/editor/components/index.tsx
Normal 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}
|
||||
/>
|
||||
);
|
124
src/plugins/vis_type_pie/public/editor/components/pie.test.tsx
Normal file
124
src/plugins/vis_type_pie/public/editor/components/pie.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
287
src/plugins/vis_type_pie/public/editor/components/pie.tsx
Normal file
287
src/plugins/vis_type_pie/public/editor/components/pie.tsx
Normal 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 };
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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 };
|
37
src/plugins/vis_type_pie/public/editor/positions.ts
Normal file
37
src/plugins/vis_type_pie/public/editor/positions.ts
Normal 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,
|
||||
},
|
||||
];
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
14
src/plugins/vis_type_pie/public/index.ts
Normal file
14
src/plugins/vis_type_pie/public/index.ts
Normal 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();
|
328
src/plugins/vis_type_pie/public/mocks.ts
Normal file
328
src/plugins/vis_type_pie/public/mocks.ts
Normal 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;
|
||||
};
|
123
src/plugins/vis_type_pie/public/pie_component.test.tsx
Normal file
123
src/plugins/vis_type_pie/public/pie_component.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
355
src/plugins/vis_type_pie/public/pie_component.tsx
Normal file
355
src/plugins/vis_type_pie/public/pie_component.tsx
Normal 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);
|
53
src/plugins/vis_type_pie/public/pie_fn.test.ts
Normal file
53
src/plugins/vis_type_pie/public/pie_fn.test.ts
Normal 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();
|
||||
});
|
||||
});
|
153
src/plugins/vis_type_pie/public/pie_fn.ts
Normal file
153
src/plugins/vis_type_pie/public/pie_fn.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
63
src/plugins/vis_type_pie/public/pie_renderer.tsx
Normal file
63
src/plugins/vis_type_pie/public/pie_renderer.tsx
Normal 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
|
||||
);
|
||||
},
|
||||
});
|
73
src/plugins/vis_type_pie/public/plugin.ts
Normal file
73
src/plugins/vis_type_pie/public/plugin.ts
Normal 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() {}
|
||||
}
|
1332
src/plugins/vis_type_pie/public/sample_vis.test.mocks.ts
Normal file
1332
src/plugins/vis_type_pie/public/sample_vis.test.mocks.ts
Normal file
File diff suppressed because one or more lines are too long
31
src/plugins/vis_type_pie/public/to_ast.test.ts
Normal file
31
src/plugins/vis_type_pie/public/to_ast.test.ts
Normal 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();
|
||||
});
|
||||
});
|
71
src/plugins/vis_type_pie/public/to_ast.ts
Normal file
71
src/plugins/vis_type_pie/public/to_ast.ts
Normal 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();
|
||||
};
|
33
src/plugins/vis_type_pie/public/to_ast_esaggs.ts
Normal file
33
src/plugins/vis_type_pie/public/to_ast_esaggs.ts
Normal 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())),
|
||||
});
|
||||
}
|
9
src/plugins/vis_type_pie/public/types/index.ts
Normal file
9
src/plugins/vis_type_pie/public/types/index.ts
Normal 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';
|
96
src/plugins/vis_type_pie/public/types/types.ts
Normal file
96
src/plugins/vis_type_pie/public/types/types.ts
Normal 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;
|
||||
}
|
98
src/plugins/vis_type_pie/public/utils/filter_helpers.test.ts
Normal file
98
src/plugins/vis_type_pie/public/utils/filter_helpers.test.ts
Normal 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);
|
||||
});
|
||||
});
|
89
src/plugins/vis_type_pie/public/utils/filter_helpers.ts
Normal file
89
src/plugins/vis_type_pie/public/utils/filter_helpers.ts
Normal 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;
|
||||
}, []);
|
||||
};
|
116
src/plugins/vis_type_pie/public/utils/get_color_picker.test.tsx
Normal file
116
src/plugins/vis_type_pie/public/utils/get_color_picker.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
121
src/plugins/vis_type_pie/public/utils/get_color_picker.tsx
Normal file
121
src/plugins/vis_type_pie/public/utils/get_color_picker.tsx
Normal 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>
|
||||
);
|
||||
};
|
222
src/plugins/vis_type_pie/public/utils/get_columns.test.ts
Normal file
222
src/plugins/vis_type_pie/public/utils/get_columns.test.ts
Normal 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 }]);
|
||||
});
|
||||
});
|
43
src/plugins/vis_type_pie/public/utils/get_columns.ts
Normal file
43
src/plugins/vis_type_pie/public/utils/get_columns.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
76
src/plugins/vis_type_pie/public/utils/get_config.ts
Normal file
76
src/plugins/vis_type_pie/public/utils/get_config.ts
Normal 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;
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
31
src/plugins/vis_type_pie/public/utils/get_distinct_series.ts
Normal file
31
src/plugins/vis_type_pie/public/utils/get_distinct_series.ts
Normal 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,
|
||||
};
|
||||
};
|
114
src/plugins/vis_type_pie/public/utils/get_layers.test.ts
Normal file
114
src/plugins/vis_type_pie/public/utils/get_layers.test.ts
Normal 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');
|
||||
});
|
||||
});
|
186
src/plugins/vis_type_pie/public/utils/get_layers.ts
Normal file
186
src/plugins/vis_type_pie/public/utils/get_layers.ts
Normal 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)';
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
117
src/plugins/vis_type_pie/public/utils/get_legend_actions.tsx
Normal file
117
src/plugins/vis_type_pie/public/utils/get_legend_actions.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
};
|
16
src/plugins/vis_type_pie/public/utils/index.ts
Normal file
16
src/plugins/vis_type_pie/public/utils/index.ts
Normal 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';
|
14
src/plugins/vis_type_pie/public/vis_type/index.ts
Normal file
14
src/plugins/vis_type_pie/public/vis_type/index.ts
Normal 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);
|
||||
};
|
98
src/plugins/vis_type_pie/public/vis_type/pie.ts
Normal file
98
src/plugins/vis_type_pie/public/vis_type/pie.ts
Normal 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,
|
||||
});
|
24
src/plugins/vis_type_pie/tsconfig.json
Normal file
24
src/plugins/vis_type_pie/tsconfig.json
Normal 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" },
|
||||
]
|
||||
}
|
|
@ -4,5 +4,5 @@
|
|||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "kibanaLegacy"],
|
||||
"requiredBundles": ["kibanaUtils", "visDefaultEditor", "visTypeXy"]
|
||||
"requiredBundles": ["kibanaUtils", "visDefaultEditor", "visTypeXy", "visTypePie"]
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
);
|
||||
|
|
|
@ -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 };
|
|
@ -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>;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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', () => ({
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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" },
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"id": "visTypeXy",
|
||||
"version": "kibana",
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "usageCollection"],
|
||||
"requiredBundles": ["kibanaUtils", "visDefaultEditor"]
|
||||
|
|
|
@ -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
|
@ -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 {};
|
||||
}
|
||||
}
|
|
@ -7,3 +7,4 @@
|
|||
*/
|
||||
|
||||
export const VISUALIZE_ENABLE_LABS_SETTING = 'visualize:enableLabs';
|
||||
export const LEGACY_CHARTS_LIBRARY = 'visualization:visualize:legacyChartsLibrary';
|
||||
|
|
|
@ -12,5 +12,6 @@
|
|||
"savedObjects"
|
||||
],
|
||||
"optionalPlugins": ["usageCollection"],
|
||||
"requiredBundles": ["kibanaUtils", "discover"]
|
||||
"requiredBundles": ["kibanaUtils", "discover"],
|
||||
"extraPublicDirs": ["common/constants"]
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -49,6 +49,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 () {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -4902,12 +4902,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": "自動",
|
||||
|
@ -4929,11 +4923,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} でフィルタリング",
|
||||
|
@ -4945,8 +4945,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": "エリア",
|
||||
|
|
|
@ -4929,12 +4929,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": "自动",
|
||||
|
@ -4956,11 +4950,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}",
|
||||
|
@ -4972,8 +4972,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": "面积图",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue