[PieVis] Lens adaptation. (#122420)

* Added config for mosaic/pie/donut/treemap/waffle.

* Added sortPredicate functionality for waffle/mosaic/treemap/pie/donut

* Added Donut handling.

* Refactored get_color.

* Merged color computation for lens and vis_types.

* Added isFlatLegend support.

* Added showValuesInLegend for waffle and fixed tests.

* Removed not used position, which is equivalent to labels.show = false.

* legendDisplay added.

* Added migrations for pieVis addLegend argument.

* Added startFromSecondLargestSlice and support of correct formatters.

* Updated docs.

* Added functionality for truncate.

* Added unit tests for pie and partial for donut/waffle.

* Addressed issue with label truncation by default.

* Addressed issue with formatters.

* Added tests for accessor.test.ts

* Added support of formatter by meta data from columns at splitChartAccessors.

* Added tests for filterOutConfig.

* Added tests for getFormatters.

* Added tests for getAvailableFormatter.

* Added tests for getFormatter.

* Added tests for get_split_dimension_accessor.

* Add is legend scenario.

* Added tests for legend.

* Replaced sortPredicate, relying on the internal terms params, with the mosaic one.

* Fixed pie snapshot and added new snapshot for treemap.

* Added snapshots for mosaicVis.

* Added snapshot to waffleVis.

* Updated unit tests for *_vis_function's.

* Added storybook.

* Added snapshots for partition vis component.

* Added expression error on providing both, splitColumn && splitRow.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Yaroslav Kuznietsov 2022-02-04 20:14:34 +02:00 committed by GitHub
parent f7661c007b
commit d9aa72c7f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
125 changed files with 7718 additions and 1556 deletions

2
.github/CODEOWNERS vendored
View file

@ -40,7 +40,7 @@
/src/plugins/chart_expressions/expression_metric/ @elastic/kibana-vis-editors
/src/plugins/chart_expressions/expression_heatmap/ @elastic/kibana-vis-editors
/src/plugins/chart_expressions/expression_gauge/ @elastic/kibana-vis-editors
/src/plugins/chart_expressions/expression_pie/ @elastic/kibana-vis-editors
/src/plugins/chart_expressions/expression_partition_vis/ @elastic/kibana-vis-editors
/src/plugins/url_forwarding/ @elastic/kibana-vis-editors
/packages/kbn-tinymath/ @elastic/kibana-vis-editors
/x-pack/test/functional/apps/lens @elastic/kibana-vis-editors

View file

@ -25,7 +25,7 @@
"expressionImage": "src/plugins/expression_image",
"expressionMetric": "src/plugins/expression_metric",
"expressionMetricVis": "src/plugins/chart_expressions/expression_metric",
"expressionPie": "src/plugins/chart_expressions/expression_pie",
"expressionPartitionVis": "src/plugins/chart_expressions/expression_partition_vis",
"expressionRepeatImage": "src/plugins/expression_repeat_image",
"expressionRevealImage": "src/plugins/expression_reveal_image",
"expressions": "src/plugins/expressions",

View file

@ -118,8 +118,8 @@ This API doesn't support angular, for registering angular dev tools, bootstrap a
|Expression MetricVis plugin adds a metric renderer and function to the expression plugin. The renderer will display the metric chart.
|{kib-repo}blob/{branch}/src/plugins/chart_expressions/expression_pie/README.md[expressionPie]
|Expression Pie plugin adds a pie renderer and function to the expression plugin. The renderer will display the Pie chart.
|{kib-repo}blob/{branch}/src/plugins/chart_expressions/expression_partition_vis/README.md[expressionPartitionVis]
|Expression Partition Visualization plugin adds a partitionVis renderer and pieVis, mosaicVis, treemapVis, waffleVis functions to the expression plugin. The renderer will display the pie, waffle, treemap and mosaic charts.
|{kib-repo}blob/{branch}/src/plugins/expression_repeat_image/README.md[expressionRepeatImage]

View file

@ -119,6 +119,6 @@ pageLoadAssetSize:
screenshotting: 17017
expressionGauge: 25000
controls: 34788
expressionPie: 26338
expressionPartitionVis: 26338
sharedUX: 16225
ux: 20784

View file

@ -24,7 +24,7 @@ export const storybookAliases = {
expression_image: 'src/plugins/expression_image/.storybook',
expression_metric_vis: 'src/plugins/chart_expressions/expression_metric/.storybook',
expression_metric: 'src/plugins/expression_metric/.storybook',
expression_pie: 'src/plugins/chart_expressions/expression_pie/.storybook',
expression_partition_vis: 'src/plugins/chart_expressions/expression_partition_vis/.storybook',
expression_repeat_image: 'src/plugins/expression_repeat_image/.storybook',
expression_reveal_image: 'src/plugins/expression_reveal_image/.storybook',
expression_shape: 'src/plugins/expression_shape/.storybook',

View file

@ -0,0 +1,9 @@
# expressionPartitionVis
Expression Partition Visualization plugin adds a `partitionVis` renderer and `pieVis`, `mosaicVis`, `treemapVis`, `waffleVis` functions to the expression plugin. The renderer will display the `pie`, `waffle`, `treemap` and `mosaic` charts.
---
## Development
See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment.

View file

@ -0,0 +1,20 @@
/*
* 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 const PLUGIN_ID = 'expressionPartitionVis';
export const PLUGIN_NAME = 'expressionPartitionVis';
export const PIE_VIS_EXPRESSION_NAME = 'pieVis';
export const TREEMAP_VIS_EXPRESSION_NAME = 'treemapVis';
export const MOSAIC_VIS_EXPRESSION_NAME = 'mosaicVis';
export const WAFFLE_VIS_EXPRESSION_NAME = 'waffleVis';
export const PARTITION_VIS_RENDERER_NAME = 'partitionVis';
export const PARTITION_LABELS_VALUE = 'partitionLabelsValue';
export const PARTITION_LABELS_FUNCTION = 'partitionLabels';
export const DEFAULT_PERCENT_DECIMALS = 2;

View file

@ -0,0 +1,188 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`interpreter/functions#mosaicVis logs correct datatable to inspector 1`] = `
Object {
"columns": Array [
Object {
"id": "col-0-1",
"meta": Object {
"dimensionName": "Slice size",
"type": "number",
},
"name": "Field 1",
},
Object {
"id": "col-0-2",
"meta": Object {
"dimensionName": "Slice",
"type": "number",
},
"name": "Field 2",
},
Object {
"id": "col-0-3",
"meta": Object {
"dimensionName": "Slice",
"type": "number",
},
"name": "Field 3",
},
Object {
"id": "col-0-4",
"meta": Object {
"dimensionName": undefined,
"type": "number",
},
"name": "Field 4",
},
],
"rows": Array [
Object {
"col-0-1": 0,
"col-0-2": 0,
"col-0-3": 0,
"col-0-4": 0,
},
],
"type": "datatable",
}
`;
exports[`interpreter/functions#mosaicVis returns an object with the correct structure 1`] = `
Object {
"as": "partitionVis",
"type": "render",
"value": Object {
"params": Object {
"listenOnChange": true,
},
"syncColors": false,
"visConfig": Object {
"addTooltip": true,
"buckets": Array [
Object {
"accessor": 1,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
Object {
"accessor": 2,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
],
"dimensions": Object {
"buckets": Array [
Object {
"accessor": 1,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
Object {
"accessor": 2,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
],
"metric": Object {
"accessor": 0,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"splitColumn": undefined,
"splitRow": undefined,
},
"labels": Object {
"last_level": false,
"percentDecimals": 2,
"position": "default",
"show": false,
"truncate": 100,
"type": "partitionLabelsValue",
"values": true,
"valuesFormat": "percent",
},
"legendDisplay": "show",
"legendPosition": "right",
"maxLegendLines": 2,
"metric": Object {
"accessor": 0,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"nestedLegend": true,
"palette": Object {
"name": "kibana_palette",
"type": "system_palette",
},
"splitColumn": undefined,
"splitRow": undefined,
"truncateLegend": true,
},
"visData": Object {
"columns": Array [
Object {
"id": "col-0-1",
"meta": Object {
"type": "number",
},
"name": "Field 1",
},
Object {
"id": "col-0-2",
"meta": Object {
"type": "number",
},
"name": "Field 2",
},
Object {
"id": "col-0-3",
"meta": Object {
"type": "number",
},
"name": "Field 3",
},
Object {
"id": "col-0-4",
"meta": Object {
"type": "number",
},
"name": "Field 4",
},
],
"rows": Array [
Object {
"col-0-1": 0,
"col-0-2": 0,
"col-0-3": 0,
"col-0-4": 0,
},
],
"type": "datatable",
},
"visType": "mosaic",
},
}
`;
exports[`interpreter/functions#mosaicVis throws error if provided more than 2 buckets 1`] = `"More than 2 buckets are not supported"`;
exports[`interpreter/functions#mosaicVis throws error if provided split row and split column at once 1`] = `"A split row and column are specified. Expression is supporting only one of them at once."`;

View file

@ -0,0 +1,288 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`interpreter/functions#pieVis logs correct datatable to inspector 1`] = `
Object {
"columns": Array [
Object {
"id": "col-0-1",
"meta": Object {
"dimensionName": "Slice size",
"type": "number",
},
"name": "Count",
},
],
"rows": Array [
Object {
"col-0-1": 0,
},
],
"type": "datatable",
}
`;
exports[`interpreter/functions#pieVis returns an object with the correct structure for donut 1`] = `
Object {
"as": "partitionVis",
"type": "render",
"value": Object {
"params": Object {
"listenOnChange": true,
},
"syncColors": false,
"visConfig": Object {
"addTooltip": true,
"buckets": Array [
Object {
"accessor": 1,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
Object {
"accessor": 2,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
Object {
"accessor": 3,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
],
"dimensions": Object {
"buckets": Array [
Object {
"accessor": 1,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
Object {
"accessor": 2,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
Object {
"accessor": 3,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
],
"metric": Object {
"accessor": 0,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"splitColumn": undefined,
"splitRow": undefined,
},
"distinctColors": false,
"emptySizeRatio": 0.3,
"isDonut": true,
"labels": Object {
"last_level": false,
"percentDecimals": 2,
"position": "default",
"show": false,
"truncate": 100,
"type": "partitionLabelsValue",
"values": true,
"valuesFormat": "percent",
},
"legendDisplay": "show",
"legendPosition": "right",
"maxLegendLines": 2,
"metric": Object {
"accessor": 0,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"nestedLegend": true,
"palette": Object {
"name": "kibana_palette",
"type": "system_palette",
},
"respectSourceOrder": true,
"splitColumn": undefined,
"splitRow": undefined,
"startFromSecondLargestSlice": true,
"truncateLegend": true,
},
"visData": Object {
"columns": Array [
Object {
"id": "col-0-1",
"meta": Object {
"type": "number",
},
"name": "Count",
},
],
"rows": Array [
Object {
"col-0-1": 0,
},
],
"type": "datatable",
},
"visType": "donut",
},
}
`;
exports[`interpreter/functions#pieVis returns an object with the correct structure for pie 1`] = `
Object {
"as": "partitionVis",
"type": "render",
"value": Object {
"params": Object {
"listenOnChange": true,
},
"syncColors": false,
"visConfig": Object {
"addTooltip": true,
"buckets": Array [
Object {
"accessor": 1,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
Object {
"accessor": 2,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
Object {
"accessor": 3,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
],
"dimensions": Object {
"buckets": Array [
Object {
"accessor": 1,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
Object {
"accessor": 2,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
Object {
"accessor": 3,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
],
"metric": Object {
"accessor": 0,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"splitColumn": undefined,
"splitRow": undefined,
},
"distinctColors": false,
"emptySizeRatio": 0.3,
"isDonut": false,
"labels": Object {
"last_level": false,
"percentDecimals": 2,
"position": "default",
"show": false,
"truncate": 100,
"type": "partitionLabelsValue",
"values": true,
"valuesFormat": "percent",
},
"legendDisplay": "show",
"legendPosition": "right",
"maxLegendLines": 2,
"metric": Object {
"accessor": 0,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"nestedLegend": true,
"palette": Object {
"name": "kibana_palette",
"type": "system_palette",
},
"respectSourceOrder": true,
"splitColumn": undefined,
"splitRow": undefined,
"startFromSecondLargestSlice": true,
"truncateLegend": true,
},
"visData": Object {
"columns": Array [
Object {
"id": "col-0-1",
"meta": Object {
"type": "number",
},
"name": "Count",
},
],
"rows": Array [
Object {
"col-0-1": 0,
},
],
"type": "datatable",
},
"visType": "pie",
},
}
`;
exports[`interpreter/functions#pieVis throws error if provided split row and split column at once 1`] = `"A split row and column are specified. Expression is supporting only one of them at once."`;

View file

@ -0,0 +1,188 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`interpreter/functions#treemapVis logs correct datatable to inspector 1`] = `
Object {
"columns": Array [
Object {
"id": "col-0-1",
"meta": Object {
"dimensionName": "Slice size",
"type": "number",
},
"name": "Field 1",
},
Object {
"id": "col-0-2",
"meta": Object {
"dimensionName": "Slice",
"type": "number",
},
"name": "Field 2",
},
Object {
"id": "col-0-3",
"meta": Object {
"dimensionName": "Slice",
"type": "number",
},
"name": "Field 3",
},
Object {
"id": "col-0-4",
"meta": Object {
"dimensionName": undefined,
"type": "number",
},
"name": "Field 4",
},
],
"rows": Array [
Object {
"col-0-1": 0,
"col-0-2": 0,
"col-0-3": 0,
"col-0-4": 0,
},
],
"type": "datatable",
}
`;
exports[`interpreter/functions#treemapVis returns an object with the correct structure 1`] = `
Object {
"as": "partitionVis",
"type": "render",
"value": Object {
"params": Object {
"listenOnChange": true,
},
"syncColors": false,
"visConfig": Object {
"addTooltip": true,
"buckets": Array [
Object {
"accessor": 1,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
Object {
"accessor": 2,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
],
"dimensions": Object {
"buckets": Array [
Object {
"accessor": 1,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
Object {
"accessor": 2,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
],
"metric": Object {
"accessor": 0,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"splitColumn": undefined,
"splitRow": undefined,
},
"labels": Object {
"last_level": false,
"percentDecimals": 2,
"position": "default",
"show": false,
"truncate": 100,
"type": "partitionLabelsValue",
"values": true,
"valuesFormat": "percent",
},
"legendDisplay": "show",
"legendPosition": "right",
"maxLegendLines": 2,
"metric": Object {
"accessor": 0,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"nestedLegend": true,
"palette": Object {
"name": "kibana_palette",
"type": "system_palette",
},
"splitColumn": undefined,
"splitRow": undefined,
"truncateLegend": true,
},
"visData": Object {
"columns": Array [
Object {
"id": "col-0-1",
"meta": Object {
"type": "number",
},
"name": "Field 1",
},
Object {
"id": "col-0-2",
"meta": Object {
"type": "number",
},
"name": "Field 2",
},
Object {
"id": "col-0-3",
"meta": Object {
"type": "number",
},
"name": "Field 3",
},
Object {
"id": "col-0-4",
"meta": Object {
"type": "number",
},
"name": "Field 4",
},
],
"rows": Array [
Object {
"col-0-1": 0,
"col-0-2": 0,
"col-0-3": 0,
"col-0-4": 0,
},
],
"type": "datatable",
},
"visType": "treemap",
},
}
`;
exports[`interpreter/functions#treemapVis throws error if provided more than 2 buckets 1`] = `"More than 2 buckets are not supported"`;
exports[`interpreter/functions#treemapVis throws error if provided split row and split column at once 1`] = `"A split row and column are specified. Expression is supporting only one of them at once."`;

View file

@ -0,0 +1,168 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`interpreter/functions#waffleVis logs correct datatable to inspector 1`] = `
Object {
"columns": Array [
Object {
"id": "col-0-1",
"meta": Object {
"dimensionName": "Slice size",
"type": "number",
},
"name": "Field 1",
},
Object {
"id": "col-0-2",
"meta": Object {
"dimensionName": "Slice",
"type": "number",
},
"name": "Field 2",
},
Object {
"id": "col-0-3",
"meta": Object {
"dimensionName": undefined,
"type": "number",
},
"name": "Field 3",
},
Object {
"id": "col-0-4",
"meta": Object {
"dimensionName": undefined,
"type": "number",
},
"name": "Field 4",
},
],
"rows": Array [
Object {
"col-0-1": 0,
"col-0-2": 0,
"col-0-3": 0,
"col-0-4": 0,
},
],
"type": "datatable",
}
`;
exports[`interpreter/functions#waffleVis returns an object with the correct structure 1`] = `
Object {
"as": "partitionVis",
"type": "render",
"value": Object {
"params": Object {
"listenOnChange": true,
},
"syncColors": false,
"visConfig": Object {
"addTooltip": true,
"bucket": Object {
"accessor": 1,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"dimensions": Object {
"buckets": Array [
Object {
"accessor": 1,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
],
"metric": Object {
"accessor": 0,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"splitColumn": undefined,
"splitRow": undefined,
},
"labels": Object {
"last_level": false,
"percentDecimals": 2,
"position": "default",
"show": false,
"truncate": 100,
"type": "partitionLabelsValue",
"values": true,
"valuesFormat": "percent",
},
"legendDisplay": "show",
"legendPosition": "right",
"maxLegendLines": 2,
"metric": Object {
"accessor": 0,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"palette": Object {
"name": "kibana_palette",
"type": "system_palette",
},
"showValuesInLegend": true,
"splitColumn": undefined,
"splitRow": undefined,
"truncateLegend": true,
},
"visData": Object {
"columns": Array [
Object {
"id": "col-0-1",
"meta": Object {
"type": "number",
},
"name": "Field 1",
},
Object {
"id": "col-0-2",
"meta": Object {
"type": "number",
},
"name": "Field 2",
},
Object {
"id": "col-0-3",
"meta": Object {
"type": "number",
},
"name": "Field 3",
},
Object {
"id": "col-0-4",
"meta": Object {
"type": "number",
},
"name": "Field 4",
},
],
"rows": Array [
Object {
"col-0-1": 0,
"col-0-2": 0,
"col-0-3": 0,
"col-0-4": 0,
},
],
"type": "datatable",
},
"visType": "waffle",
},
}
`;
exports[`interpreter/functions#waffleVis throws error if provided split row and split column at once 1`] = `"A split row and column are specified. Expression is supporting only one of them at once."`;

View file

@ -0,0 +1,126 @@
/*
* 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';
export const strings = {
getPieVisFunctionName: () =>
i18n.translate('expressionPartitionVis.pieVis.function.help', {
defaultMessage: 'Pie visualization',
}),
getMetricArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.metricHelpText', {
defaultMessage: 'Metric dimensions config',
}),
getBucketsArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.bucketsHelpText', {
defaultMessage: 'Buckets dimensions config',
}),
getBucketArgHelp: () =>
i18n.translate('expressionPartitionVis.waffle.function.args.bucketHelpText', {
defaultMessage: 'Bucket dimensions config',
}),
getSplitColumnArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.splitColumnHelpText', {
defaultMessage: 'Split by column dimension config',
}),
getSplitRowArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.splitRowHelpText', {
defaultMessage: 'Split by row dimension config',
}),
getAddTooltipArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.addTooltipHelpText', {
defaultMessage: 'Show tooltip on slice hover',
}),
getLegendDisplayArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.legendDisplayHelpText', {
defaultMessage: 'Show legend chart legend',
}),
getLegendPositionArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.legendPositionHelpText', {
defaultMessage: 'Position the legend on top, bottom, left, right of the chart',
}),
getNestedLegendArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.nestedLegendHelpText', {
defaultMessage: 'Show a more detailed legend',
}),
getTruncateLegendArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.truncateLegendHelpText', {
defaultMessage: 'Defines if the legend items will be truncated or not',
}),
getMaxLegendLinesArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.maxLegendLinesHelpText', {
defaultMessage: 'Defines the number of lines per legend item',
}),
getDistinctColorsArgHelp: () =>
i18n.translate('expressionPartitionVis.pieVis.function.args.distinctColorsHelpText', {
defaultMessage:
'Maps different color per slice. Slices with the same value have the same color',
}),
getIsDonutArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.isDonutHelpText', {
defaultMessage: 'Displays the pie chart as donut',
}),
getRespectSourceOrderArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.respectSourceOrderHelpText', {
defaultMessage: 'Keeps an order of the elements, returned from the datasource',
}),
getStartFromSecondLargestSliceArgHelp: () =>
i18n.translate(
'expressionPartitionVis.reusable.function.args.startPlacementWithSecondLargestSliceHelpText',
{
defaultMessage: 'Starts placement with the second largest slice',
}
),
getEmptySizeRatioArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.emptySizeRatioHelpText', {
defaultMessage: 'Defines donut inner empty area size',
}),
getPaletteArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.paletteHelpText', {
defaultMessage: 'Defines the chart palette name',
}),
getLabelsArgHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.args.labelsHelpText', {
defaultMessage: 'Pie labels config',
}),
getShowValuesInLegendArgHelp: () =>
i18n.translate('expressionPartitionVis.waffle.function.args.showValuesInLegendHelpText', {
defaultMessage: 'Show values in legend',
}),
getSliceSizeHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.dimension.metric', {
defaultMessage: 'Slice size',
}),
getSliceHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.dimension.buckets', {
defaultMessage: 'Slice',
}),
getColumnSplitHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.dimension.splitcolumn', {
defaultMessage: 'Column split',
}),
getRowSplitHelp: () =>
i18n.translate('expressionPartitionVis.reusable.function.dimension.splitrow', {
defaultMessage: 'Row split',
}),
};
export const errors = {
moreThanNBucketsAreNotSupportedError: (maxLength: number) =>
i18n.translate('expressionPartitionVis.reusable.function.errors.moreThenNumberBuckets', {
defaultMessage: 'More than {maxLength} buckets are not supported',
values: { maxLength },
}),
splitRowAndSplitColumnAreSpecifiedError: () =>
i18n.translate('expressionPartitionVis.reusable.function.errors.splitRowAndColumnSpecified', {
defaultMessage:
'A split row and column are specified. Expression is supporting only one of them at once.',
}),
};

View file

@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export { pieVisFunction } from './pie_vis_function';
export { treemapVisFunction } from './treemap_vis_function';
export { mosaicVisFunction } from './mosaic_vis_function';
export { waffleVisFunction } from './waffle_vis_function';
export { partitionLabelsFunction } from './partition_labels_function';

View file

@ -0,0 +1,145 @@
/*
* 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 {
MosaicVisConfig,
LabelPositions,
ValueFormats,
LegendDisplay,
} from '../types/expression_renderers';
import { ExpressionValueVisDimension } from '../../../../visualizations/common';
import { Datatable } from '../../../../expressions/common/expression_types/specs';
import { mosaicVisFunction } from './mosaic_vis_function';
import { PARTITION_LABELS_VALUE } from '../constants';
describe('interpreter/functions#mosaicVis', () => {
const fn = functionWrapper(mosaicVisFunction());
const context: Datatable = {
type: 'datatable',
rows: [{ 'col-0-1': 0, 'col-0-2': 0, 'col-0-3': 0, 'col-0-4': 0 }],
columns: [
{ id: 'col-0-1', name: 'Field 1', meta: { type: 'number' } },
{ id: 'col-0-2', name: 'Field 2', meta: { type: 'number' } },
{ id: 'col-0-3', name: 'Field 3', meta: { type: 'number' } },
{ id: 'col-0-4', name: 'Field 4', meta: { type: 'number' } },
],
};
const visConfig: MosaicVisConfig = {
addTooltip: true,
legendDisplay: LegendDisplay.SHOW,
legendPosition: 'right',
nestedLegend: true,
truncateLegend: true,
maxLegendLines: 2,
palette: {
type: 'system_palette',
name: 'kibana_palette',
},
labels: {
type: PARTITION_LABELS_VALUE,
show: false,
values: true,
position: LabelPositions.DEFAULT,
valuesFormat: ValueFormats.PERCENT,
percentDecimals: 2,
truncate: 100,
last_level: false,
},
metric: {
type: 'vis_dimension',
accessor: 0,
format: {
id: 'number',
params: {},
},
},
buckets: [
{
type: 'vis_dimension',
accessor: 1,
format: {
id: 'number',
params: {},
},
},
{
type: 'vis_dimension',
accessor: 2,
format: {
id: 'number',
params: {},
},
},
],
};
beforeEach(() => {
jest.clearAllMocks();
});
it('returns an object with the correct structure', async () => {
const actual = await fn(context, visConfig);
expect(actual).toMatchSnapshot();
});
it('throws error if provided more than 2 buckets', async () => {
expect(() =>
fn(context, {
...visConfig,
buckets: [
...(visConfig.buckets ?? []),
{
type: 'vis_dimension',
accessor: 3,
format: {
id: 'number',
params: {},
},
},
],
})
).toThrowErrorMatchingSnapshot();
});
it('throws error if provided split row and split column at once', async () => {
const splitDimension: ExpressionValueVisDimension = {
type: 'vis_dimension',
accessor: 3,
format: {
id: 'number',
params: {},
},
};
expect(() =>
fn(context, {
...visConfig,
splitColumn: [splitDimension],
splitRow: [splitDimension],
})
).toThrowErrorMatchingSnapshot();
});
it('logs correct datatable to inspector', async () => {
let loggedTable: Datatable;
const handlers = {
inspectorAdapters: {
tables: {
logDatatable: (name: string, datatable: Datatable) => {
loggedTable = datatable;
},
},
},
};
await fn(context, visConfig, handlers as any);
expect(loggedTable!).toMatchSnapshot();
});
});

View file

@ -0,0 +1,131 @@
/*
* 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 { LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
import { prepareLogTable } from '../../../../visualizations/common/prepare_log_table';
import { ChartTypes, MosaicVisExpressionFunctionDefinition } from '../types';
import {
PARTITION_LABELS_FUNCTION,
PARTITION_LABELS_VALUE,
PARTITION_VIS_RENDERER_NAME,
MOSAIC_VIS_EXPRESSION_NAME,
} from '../constants';
import { errors, strings } from './i18n';
export const mosaicVisFunction = (): MosaicVisExpressionFunctionDefinition => ({
name: MOSAIC_VIS_EXPRESSION_NAME,
type: 'render',
inputTypes: ['datatable'],
help: strings.getPieVisFunctionName(),
args: {
metric: {
types: ['vis_dimension'],
help: strings.getMetricArgHelp(),
required: true,
},
buckets: {
types: ['vis_dimension'],
help: strings.getBucketsArgHelp(),
multi: true,
},
splitColumn: {
types: ['vis_dimension'],
help: strings.getSplitColumnArgHelp(),
multi: true,
},
splitRow: {
types: ['vis_dimension'],
help: strings.getSplitRowArgHelp(),
multi: true,
},
addTooltip: {
types: ['boolean'],
help: strings.getAddTooltipArgHelp(),
default: true,
},
legendDisplay: {
types: ['string'],
help: strings.getLegendDisplayArgHelp(),
options: [LegendDisplay.SHOW, LegendDisplay.HIDE, LegendDisplay.DEFAULT],
default: LegendDisplay.HIDE,
},
legendPosition: {
types: ['string'],
help: strings.getLegendPositionArgHelp(),
},
nestedLegend: {
types: ['boolean'],
help: strings.getNestedLegendArgHelp(),
default: false,
},
truncateLegend: {
types: ['boolean'],
help: strings.getTruncateLegendArgHelp(),
default: true,
},
maxLegendLines: {
types: ['number'],
help: strings.getMaxLegendLinesArgHelp(),
},
palette: {
types: ['palette', 'system_palette'],
help: strings.getPaletteArgHelp(),
default: '{palette}',
},
labels: {
types: [PARTITION_LABELS_VALUE],
help: strings.getLabelsArgHelp(),
default: `{${PARTITION_LABELS_FUNCTION}}`,
},
},
fn(context, args, handlers) {
const maxSupportedBuckets = 2;
if ((args.buckets ?? []).length > maxSupportedBuckets) {
throw new Error(errors.moreThanNBucketsAreNotSupportedError(maxSupportedBuckets));
}
if (args.splitColumn && args.splitRow) {
throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError());
}
const visConfig: PartitionVisParams = {
...args,
palette: args.palette,
dimensions: {
metric: args.metric,
buckets: args.buckets,
splitColumn: args.splitColumn,
splitRow: args.splitRow,
},
};
if (handlers?.inspectorAdapters?.tables) {
const logTable = prepareLogTable(context, [
[[args.metric], strings.getSliceSizeHelp()],
[args.buckets, strings.getSliceHelp()],
[args.splitColumn, strings.getColumnSplitHelp()],
[args.splitRow, strings.getRowSplitHelp()],
]);
handlers.inspectorAdapters.tables.logDatatable('default', logTable);
}
return {
type: 'render',
as: PARTITION_VIS_RENDERER_NAME,
value: {
visData: context,
visConfig,
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
visType: ChartTypes.MOSAIC,
params: {
listenOnChange: true,
},
},
};
},
});

View file

@ -0,0 +1,105 @@
/*
* 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 } from '../../../../expressions/common';
import { PARTITION_LABELS_FUNCTION, PARTITION_LABELS_VALUE } from '../constants';
import {
ExpressionValuePartitionLabels,
LabelPositions,
PartitionLabelsArguments,
ValueFormats,
} from '../types';
export const partitionLabelsFunction = (): ExpressionFunctionDefinition<
typeof PARTITION_LABELS_FUNCTION,
Datatable | null,
PartitionLabelsArguments,
ExpressionValuePartitionLabels
> => ({
name: PARTITION_LABELS_FUNCTION,
help: i18n.translate('expressionPartitionVis.partitionLabels.function.help', {
defaultMessage: 'Generates the partition labels object',
}),
type: PARTITION_LABELS_VALUE,
args: {
show: {
types: ['boolean'],
help: i18n.translate('expressionPartitionVis.partitionLabels.function.args.show.help', {
defaultMessage: 'Displays the partition chart labels',
}),
default: true,
},
position: {
types: ['string'],
default: 'default',
help: i18n.translate('expressionPartitionVis.partitionLabels.function.args.position.help', {
defaultMessage: 'Defines the label position',
}),
options: [LabelPositions.DEFAULT, LabelPositions.INSIDE],
},
values: {
types: ['boolean'],
help: i18n.translate('expressionPartitionVis.partitionLabels.function.args.values.help', {
defaultMessage: 'Displays the values inside the slices',
}),
default: true,
},
percentDecimals: {
types: ['number'],
help: i18n.translate(
'expressionPartitionVis.partitionLabels.function.args.percentDecimals.help',
{
defaultMessage:
'Defines the number of decimals that will appear on the values as percent',
}
),
default: 2,
},
// Deprecated
last_level: {
types: ['boolean'],
help: i18n.translate('expressionPartitionVis.partitionLabels.function.args.last_level.help', {
defaultMessage: 'Show top level labels only for multilayer pie/donut charts',
}),
default: false,
},
// Deprecated
truncate: {
types: ['number', 'null'],
help: i18n.translate('expressionPartitionVis.partitionLabels.function.args.truncate.help', {
defaultMessage:
'Defines the number of characters that the slice value will display only for multilayer pie/donut charts',
}),
default: null,
},
valuesFormat: {
types: ['string'],
default: 'percent',
help: i18n.translate(
'expressionPartitionVis.partitionLabels.function.args.valuesFormat.help',
{
defaultMessage: 'Defines the format of the values',
}
),
options: [ValueFormats.PERCENT, ValueFormats.VALUE],
},
},
fn: (context, args) => {
return {
type: PARTITION_LABELS_VALUE,
show: args.show,
position: args.position,
percentDecimals: args.percentDecimals,
values: args.values,
truncate: args.truncate,
valuesFormat: args.valuesFormat,
last_level: args.last_level,
};
},
});

View file

@ -12,21 +12,24 @@ import {
EmptySizeRatios,
LabelPositions,
ValueFormats,
LegendDisplay,
} from '../types/expression_renderers';
import { pieVisFunction } from './pie_vis_function';
import { ExpressionValueVisDimension } from '../../../../visualizations/common';
import { Datatable } from '../../../../expressions/common/expression_types/specs';
import { pieVisFunction } from './pie_vis_function';
import { PARTITION_LABELS_VALUE } from '../constants';
describe('interpreter/functions#pie', () => {
describe('interpreter/functions#pieVis', () => {
const fn = functionWrapper(pieVisFunction());
const context = {
const context: Datatable = {
type: 'datatable',
rows: [{ 'col-0-1': 0 }],
columns: [{ id: 'col-0-1', name: 'Count' }],
} as unknown as Datatable;
columns: [{ id: 'col-0-1', name: 'Count', meta: { type: 'number' } }],
};
const visConfig: PieVisConfig = {
addTooltip: true,
addLegend: true,
legendDisplay: LegendDisplay.SHOW,
legendPosition: 'right',
isDonut: true,
emptySizeRatio: EmptySizeRatios.SMALL,
@ -39,7 +42,7 @@ describe('interpreter/functions#pie', () => {
name: 'kibana_palette',
},
labels: {
type: 'pie_labels_value',
type: PARTITION_LABELS_VALUE,
show: false,
values: true,
position: LabelPositions.DEFAULT,
@ -56,17 +59,67 @@ describe('interpreter/functions#pie', () => {
params: {},
},
},
buckets: [
{
type: 'vis_dimension',
accessor: 1,
format: {
id: 'number',
params: {},
},
},
{
type: 'vis_dimension',
accessor: 2,
format: {
id: 'number',
params: {},
},
},
{
type: 'vis_dimension',
accessor: 3,
format: {
id: 'number',
params: {},
},
},
],
};
beforeEach(() => {
jest.clearAllMocks();
});
it('returns an object with the correct structure', async () => {
it('returns an object with the correct structure for pie', async () => {
const actual = await fn(context, { ...visConfig, isDonut: false });
expect(actual).toMatchSnapshot();
});
it('returns an object with the correct structure for donut', async () => {
const actual = await fn(context, visConfig);
expect(actual).toMatchSnapshot();
});
it('throws error if provided split row and split column at once', async () => {
const splitDimension: ExpressionValueVisDimension = {
type: 'vis_dimension',
accessor: 3,
format: {
id: 'number',
params: {},
},
};
expect(() =>
fn(context, {
...visConfig,
splitColumn: [splitDimension],
splitRow: [splitDimension],
})
).toThrowErrorMatchingSnapshot();
});
it('logs correct datatable to inspector', async () => {
let loggedTable: Datatable;
const handlers = {

View file

@ -0,0 +1,153 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { Position } from '@elastic/charts';
import { EmptySizeRatios, LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
import { prepareLogTable } from '../../../../visualizations/common/prepare_log_table';
import { ChartTypes, PieVisExpressionFunctionDefinition } from '../types';
import {
PARTITION_LABELS_FUNCTION,
PARTITION_LABELS_VALUE,
PIE_VIS_EXPRESSION_NAME,
PARTITION_VIS_RENDERER_NAME,
} from '../constants';
import { errors, strings } from './i18n';
export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
name: PIE_VIS_EXPRESSION_NAME,
type: 'render',
inputTypes: ['datatable'],
help: strings.getPieVisFunctionName(),
args: {
metric: {
types: ['vis_dimension'],
help: strings.getMetricArgHelp(),
required: true,
},
buckets: {
types: ['vis_dimension'],
help: strings.getBucketsArgHelp(),
multi: true,
},
splitColumn: {
types: ['vis_dimension'],
help: strings.getSplitColumnArgHelp(),
multi: true,
},
splitRow: {
types: ['vis_dimension'],
help: strings.getSplitRowArgHelp(),
multi: true,
},
addTooltip: {
types: ['boolean'],
help: strings.getAddTooltipArgHelp(),
default: true,
},
legendDisplay: {
types: ['string'],
help: strings.getLegendDisplayArgHelp(),
options: [LegendDisplay.SHOW, LegendDisplay.HIDE, LegendDisplay.DEFAULT],
default: LegendDisplay.HIDE,
},
legendPosition: {
types: ['string'],
help: strings.getLegendPositionArgHelp(),
options: [Position.Top, Position.Right, Position.Bottom, Position.Left],
},
nestedLegend: {
types: ['boolean'],
help: strings.getNestedLegendArgHelp(),
default: false,
},
truncateLegend: {
types: ['boolean'],
help: strings.getTruncateLegendArgHelp(),
default: true,
},
maxLegendLines: {
types: ['number'],
help: strings.getMaxLegendLinesArgHelp(),
},
distinctColors: {
types: ['boolean'],
help: strings.getDistinctColorsArgHelp(),
default: false,
},
respectSourceOrder: {
types: ['boolean'],
help: strings.getRespectSourceOrderArgHelp(),
default: true,
},
isDonut: {
types: ['boolean'],
help: strings.getIsDonutArgHelp(),
default: false,
},
emptySizeRatio: {
types: ['number'],
help: strings.getEmptySizeRatioArgHelp(),
default: EmptySizeRatios.SMALL,
},
palette: {
types: ['palette', 'system_palette'],
help: strings.getPaletteArgHelp(),
default: '{palette}',
},
labels: {
types: [PARTITION_LABELS_VALUE],
help: strings.getLabelsArgHelp(),
default: `{${PARTITION_LABELS_FUNCTION}}`,
},
startFromSecondLargestSlice: {
types: ['boolean'],
help: strings.getStartFromSecondLargestSliceArgHelp(),
default: true,
},
},
fn(context, args, handlers) {
if (args.splitColumn && args.splitRow) {
throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError());
}
const visConfig: PartitionVisParams = {
...args,
palette: args.palette,
dimensions: {
metric: args.metric,
buckets: args.buckets,
splitColumn: args.splitColumn,
splitRow: args.splitRow,
},
};
if (handlers?.inspectorAdapters?.tables) {
const logTable = prepareLogTable(context, [
[[args.metric], strings.getSliceSizeHelp()],
[args.buckets, strings.getSliceHelp()],
[args.splitColumn, strings.getColumnSplitHelp()],
[args.splitRow, strings.getRowSplitHelp()],
]);
handlers.inspectorAdapters.tables.logDatatable('default', logTable);
}
return {
type: 'render',
as: PARTITION_VIS_RENDERER_NAME,
value: {
visData: context,
visConfig,
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
visType: args.isDonut ? ChartTypes.DONUT : ChartTypes.PIE,
params: {
listenOnChange: true,
},
},
};
},
});

View file

@ -0,0 +1,145 @@
/*
* 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 {
TreemapVisConfig,
LabelPositions,
ValueFormats,
LegendDisplay,
} from '../types/expression_renderers';
import { ExpressionValueVisDimension } from '../../../../visualizations/common';
import { Datatable } from '../../../../expressions/common/expression_types/specs';
import { treemapVisFunction } from './treemap_vis_function';
import { PARTITION_LABELS_VALUE } from '../constants';
describe('interpreter/functions#treemapVis', () => {
const fn = functionWrapper(treemapVisFunction());
const context: Datatable = {
type: 'datatable',
rows: [{ 'col-0-1': 0, 'col-0-2': 0, 'col-0-3': 0, 'col-0-4': 0 }],
columns: [
{ id: 'col-0-1', name: 'Field 1', meta: { type: 'number' } },
{ id: 'col-0-2', name: 'Field 2', meta: { type: 'number' } },
{ id: 'col-0-3', name: 'Field 3', meta: { type: 'number' } },
{ id: 'col-0-4', name: 'Field 4', meta: { type: 'number' } },
],
};
const visConfig: TreemapVisConfig = {
addTooltip: true,
legendDisplay: LegendDisplay.SHOW,
legendPosition: 'right',
nestedLegend: true,
truncateLegend: true,
maxLegendLines: 2,
palette: {
type: 'system_palette',
name: 'kibana_palette',
},
labels: {
type: PARTITION_LABELS_VALUE,
show: false,
values: true,
position: LabelPositions.DEFAULT,
valuesFormat: ValueFormats.PERCENT,
percentDecimals: 2,
truncate: 100,
last_level: false,
},
metric: {
type: 'vis_dimension',
accessor: 0,
format: {
id: 'number',
params: {},
},
},
buckets: [
{
type: 'vis_dimension',
accessor: 1,
format: {
id: 'number',
params: {},
},
},
{
type: 'vis_dimension',
accessor: 2,
format: {
id: 'number',
params: {},
},
},
],
};
beforeEach(() => {
jest.clearAllMocks();
});
it('returns an object with the correct structure', async () => {
const actual = await fn(context, visConfig);
expect(actual).toMatchSnapshot();
});
it('throws error if provided more than 2 buckets', async () => {
expect(() =>
fn(context, {
...visConfig,
buckets: [
...(visConfig.buckets ?? []),
{
type: 'vis_dimension',
accessor: 3,
format: {
id: 'number',
params: {},
},
},
],
})
).toThrowErrorMatchingSnapshot();
});
it('throws error if provided split row and split column at once', async () => {
const splitDimension: ExpressionValueVisDimension = {
type: 'vis_dimension',
accessor: 3,
format: {
id: 'number',
params: {},
},
};
expect(() =>
fn(context, {
...visConfig,
splitColumn: [splitDimension],
splitRow: [splitDimension],
})
).toThrowErrorMatchingSnapshot();
});
it('logs correct datatable to inspector', async () => {
let loggedTable: Datatable;
const handlers = {
inspectorAdapters: {
tables: {
logDatatable: (name: string, datatable: Datatable) => {
loggedTable = datatable;
},
},
},
};
await fn(context, visConfig, handlers as any);
expect(loggedTable!).toMatchSnapshot();
});
});

View file

@ -0,0 +1,131 @@
/*
* 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 { LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
import { prepareLogTable } from '../../../../visualizations/common/prepare_log_table';
import { ChartTypes, TreemapVisExpressionFunctionDefinition } from '../types';
import {
PARTITION_LABELS_FUNCTION,
PARTITION_LABELS_VALUE,
PARTITION_VIS_RENDERER_NAME,
TREEMAP_VIS_EXPRESSION_NAME,
} from '../constants';
import { errors, strings } from './i18n';
export const treemapVisFunction = (): TreemapVisExpressionFunctionDefinition => ({
name: TREEMAP_VIS_EXPRESSION_NAME,
type: 'render',
inputTypes: ['datatable'],
help: strings.getPieVisFunctionName(),
args: {
metric: {
types: ['vis_dimension'],
help: strings.getMetricArgHelp(),
required: true,
},
buckets: {
types: ['vis_dimension'],
help: strings.getBucketsArgHelp(),
multi: true,
},
splitColumn: {
types: ['vis_dimension'],
help: strings.getSplitColumnArgHelp(),
multi: true,
},
splitRow: {
types: ['vis_dimension'],
help: strings.getSplitRowArgHelp(),
multi: true,
},
addTooltip: {
types: ['boolean'],
help: strings.getAddTooltipArgHelp(),
default: true,
},
legendDisplay: {
types: ['string'],
help: strings.getLegendDisplayArgHelp(),
options: [LegendDisplay.SHOW, LegendDisplay.HIDE, LegendDisplay.DEFAULT],
default: LegendDisplay.HIDE,
},
legendPosition: {
types: ['string'],
help: strings.getLegendPositionArgHelp(),
},
nestedLegend: {
types: ['boolean'],
help: strings.getNestedLegendArgHelp(),
default: false,
},
truncateLegend: {
types: ['boolean'],
help: strings.getTruncateLegendArgHelp(),
default: true,
},
maxLegendLines: {
types: ['number'],
help: strings.getMaxLegendLinesArgHelp(),
},
palette: {
types: ['palette', 'system_palette'],
help: strings.getPaletteArgHelp(),
default: '{palette}',
},
labels: {
types: [PARTITION_LABELS_VALUE],
help: strings.getLabelsArgHelp(),
default: `{${PARTITION_LABELS_FUNCTION}}`,
},
},
fn(context, args, handlers) {
const maxSupportedBuckets = 2;
if ((args.buckets ?? []).length > maxSupportedBuckets) {
throw new Error(errors.moreThanNBucketsAreNotSupportedError(maxSupportedBuckets));
}
if (args.splitColumn && args.splitRow) {
throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError());
}
const visConfig: PartitionVisParams = {
...args,
palette: args.palette,
dimensions: {
metric: args.metric,
buckets: args.buckets,
splitColumn: args.splitColumn,
splitRow: args.splitRow,
},
};
if (handlers?.inspectorAdapters?.tables) {
const logTable = prepareLogTable(context, [
[[args.metric], strings.getSliceSizeHelp()],
[args.buckets, strings.getSliceHelp()],
[args.splitColumn, strings.getColumnSplitHelp()],
[args.splitRow, strings.getRowSplitHelp()],
]);
handlers.inspectorAdapters.tables.logDatatable('default', logTable);
}
return {
type: 'render',
as: PARTITION_VIS_RENDERER_NAME,
value: {
visData: context,
visConfig,
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
visType: ChartTypes.TREEMAP,
params: {
listenOnChange: true,
},
},
};
},
});

View file

@ -0,0 +1,116 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { functionWrapper } from '../../../../expressions/common/expression_functions/specs/tests/utils';
import {
WaffleVisConfig,
LabelPositions,
ValueFormats,
LegendDisplay,
} from '../types/expression_renderers';
import { ExpressionValueVisDimension } from '../../../../visualizations/common';
import { Datatable } from '../../../../expressions/common/expression_types/specs';
import { waffleVisFunction } from './waffle_vis_function';
import { PARTITION_LABELS_VALUE } from '../constants';
describe('interpreter/functions#waffleVis', () => {
const fn = functionWrapper(waffleVisFunction());
const context: Datatable = {
type: 'datatable',
rows: [{ 'col-0-1': 0, 'col-0-2': 0, 'col-0-3': 0, 'col-0-4': 0 }],
columns: [
{ id: 'col-0-1', name: 'Field 1', meta: { type: 'number' } },
{ id: 'col-0-2', name: 'Field 2', meta: { type: 'number' } },
{ id: 'col-0-3', name: 'Field 3', meta: { type: 'number' } },
{ id: 'col-0-4', name: 'Field 4', meta: { type: 'number' } },
],
};
const visConfig: WaffleVisConfig = {
addTooltip: true,
showValuesInLegend: true,
legendDisplay: LegendDisplay.SHOW,
legendPosition: 'right',
truncateLegend: true,
maxLegendLines: 2,
palette: {
type: 'system_palette',
name: 'kibana_palette',
},
labels: {
type: PARTITION_LABELS_VALUE,
show: false,
values: true,
position: LabelPositions.DEFAULT,
valuesFormat: ValueFormats.PERCENT,
percentDecimals: 2,
truncate: 100,
last_level: false,
},
metric: {
type: 'vis_dimension',
accessor: 0,
format: {
id: 'number',
params: {},
},
},
bucket: {
type: 'vis_dimension',
accessor: 1,
format: {
id: 'number',
params: {},
},
},
};
beforeEach(() => {
jest.clearAllMocks();
});
it('returns an object with the correct structure', async () => {
const actual = await fn(context, visConfig);
expect(actual).toMatchSnapshot();
});
it('throws error if provided split row and split column at once', async () => {
const splitDimension: ExpressionValueVisDimension = {
type: 'vis_dimension',
accessor: 3,
format: {
id: 'number',
params: {},
},
};
expect(() =>
fn(context, {
...visConfig,
splitColumn: [splitDimension],
splitRow: [splitDimension],
})
).toThrowErrorMatchingSnapshot();
});
it('logs correct datatable to inspector', async () => {
let loggedTable: Datatable;
const handlers = {
inspectorAdapters: {
tables: {
logDatatable: (name: string, datatable: Datatable) => {
loggedTable = datatable;
},
},
},
};
await fn(context, visConfig, handlers as any);
expect(loggedTable!).toMatchSnapshot();
});
});

View file

@ -0,0 +1,126 @@
/*
* 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 { LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
import { prepareLogTable } from '../../../../visualizations/common/prepare_log_table';
import { ChartTypes, WaffleVisExpressionFunctionDefinition } from '../types';
import {
PARTITION_LABELS_FUNCTION,
PARTITION_LABELS_VALUE,
PARTITION_VIS_RENDERER_NAME,
WAFFLE_VIS_EXPRESSION_NAME,
} from '../constants';
import { errors, strings } from './i18n';
export const waffleVisFunction = (): WaffleVisExpressionFunctionDefinition => ({
name: WAFFLE_VIS_EXPRESSION_NAME,
type: 'render',
inputTypes: ['datatable'],
help: strings.getPieVisFunctionName(),
args: {
metric: {
types: ['vis_dimension'],
help: strings.getMetricArgHelp(),
required: true,
},
bucket: {
types: ['vis_dimension'],
help: strings.getBucketArgHelp(),
},
splitColumn: {
types: ['vis_dimension'],
help: strings.getSplitColumnArgHelp(),
multi: true,
},
splitRow: {
types: ['vis_dimension'],
help: strings.getSplitRowArgHelp(),
multi: true,
},
addTooltip: {
types: ['boolean'],
help: strings.getAddTooltipArgHelp(),
default: true,
},
legendDisplay: {
types: ['string'],
help: strings.getLegendDisplayArgHelp(),
options: [LegendDisplay.SHOW, LegendDisplay.HIDE, LegendDisplay.DEFAULT],
default: LegendDisplay.HIDE,
},
legendPosition: {
types: ['string'],
help: strings.getLegendPositionArgHelp(),
},
truncateLegend: {
types: ['boolean'],
help: strings.getTruncateLegendArgHelp(),
default: true,
},
maxLegendLines: {
types: ['number'],
help: strings.getMaxLegendLinesArgHelp(),
},
palette: {
types: ['palette', 'system_palette'],
help: strings.getPaletteArgHelp(),
default: '{palette}',
},
labels: {
types: [PARTITION_LABELS_VALUE],
help: strings.getLabelsArgHelp(),
default: `{${PARTITION_LABELS_FUNCTION}}`,
},
showValuesInLegend: {
types: ['boolean'],
help: strings.getShowValuesInLegendArgHelp(),
default: false,
},
},
fn(context, args, handlers) {
if (args.splitColumn && args.splitRow) {
throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError());
}
const buckets = args.bucket ? [args.bucket] : [];
const visConfig: PartitionVisParams = {
...args,
palette: args.palette,
dimensions: {
metric: args.metric,
buckets,
splitColumn: args.splitColumn,
splitRow: args.splitRow,
},
};
if (handlers?.inspectorAdapters?.tables) {
const logTable = prepareLogTable(context, [
[[args.metric], strings.getSliceSizeHelp()],
[buckets, strings.getSliceHelp()],
[args.splitColumn, strings.getColumnSplitHelp()],
[args.splitRow, strings.getRowSplitHelp()],
]);
handlers.inspectorAdapters.tables.logDatatable('default', logTable);
}
return {
type: 'render',
as: PARTITION_VIS_RENDERER_NAME,
value: {
visData: context,
visConfig,
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
visType: ChartTypes.WAFFLE,
params: {
listenOnChange: true,
},
},
};
},
});

View file

@ -0,0 +1,52 @@
/*
* 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 {
PLUGIN_ID,
PLUGIN_NAME,
PIE_VIS_EXPRESSION_NAME,
TREEMAP_VIS_EXPRESSION_NAME,
MOSAIC_VIS_EXPRESSION_NAME,
WAFFLE_VIS_EXPRESSION_NAME,
PARTITION_LABELS_VALUE,
PARTITION_LABELS_FUNCTION,
} from './constants';
export {
pieVisFunction,
treemapVisFunction,
waffleVisFunction,
mosaicVisFunction,
partitionLabelsFunction,
} from './expression_functions';
export type {
ExpressionValuePartitionLabels,
PieVisExpressionFunctionDefinition,
TreemapVisExpressionFunctionDefinition,
MosaicVisExpressionFunctionDefinition,
WaffleVisExpressionFunctionDefinition,
} from './types/expression_functions';
export type {
PartitionVisParams,
PieVisConfig,
TreemapVisConfig,
MosaicVisConfig,
WaffleVisConfig,
LabelsParams,
Dimension,
Dimensions,
} from './types/expression_renderers';
export {
ValueFormats,
LabelPositions,
EmptySizeRatios,
LegendDisplay,
} from './types/expression_renderers';

View file

@ -0,0 +1,93 @@
/*
* 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 {
PARTITION_LABELS_VALUE,
PIE_VIS_EXPRESSION_NAME,
TREEMAP_VIS_EXPRESSION_NAME,
MOSAIC_VIS_EXPRESSION_NAME,
WAFFLE_VIS_EXPRESSION_NAME,
} from '../constants';
import {
ExpressionFunctionDefinition,
Datatable,
ExpressionValueRender,
ExpressionValueBoxed,
} from '../../../../expressions/common';
import {
RenderValue,
PieVisConfig,
LabelPositions,
ValueFormats,
TreemapVisConfig,
MosaicVisConfig,
WaffleVisConfig,
} from './expression_renderers';
export interface PartitionLabelsArguments {
show: boolean;
position: LabelPositions;
values: boolean;
valuesFormat: ValueFormats;
percentDecimals: number;
/** @deprecated This field is deprecated and going to be removed in the futher release versions. */
truncate?: number | null;
/** @deprecated This field is deprecated and going to be removed in the futher release versions. */
last_level?: boolean;
}
export type ExpressionValuePartitionLabels = ExpressionValueBoxed<
typeof PARTITION_LABELS_VALUE,
{
show: boolean;
position: LabelPositions;
values: boolean;
valuesFormat: ValueFormats;
percentDecimals: number;
/** @deprecated This field is deprecated and going to be removed in the futher release versions. */
truncate?: number | null;
/** @deprecated This field is deprecated and going to be removed in the futher release versions. */
last_level?: boolean;
}
>;
export type PieVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof PIE_VIS_EXPRESSION_NAME,
Datatable,
PieVisConfig,
ExpressionValueRender<RenderValue>
>;
export type TreemapVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof TREEMAP_VIS_EXPRESSION_NAME,
Datatable,
TreemapVisConfig,
ExpressionValueRender<RenderValue>
>;
export type MosaicVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof MOSAIC_VIS_EXPRESSION_NAME,
Datatable,
MosaicVisConfig,
ExpressionValueRender<RenderValue>
>;
export type WaffleVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof WAFFLE_VIS_EXPRESSION_NAME,
Datatable,
WaffleVisConfig,
ExpressionValueRender<RenderValue>
>;
export enum ChartTypes {
PIE = 'pie',
DONUT = 'donut',
TREEMAP = 'treemap',
MOSAIC = 'mosaic',
WAFFLE = 'waffle',
}

View file

@ -11,7 +11,7 @@ import { Datatable, DatatableColumn } from '../../../../expressions/common';
import { SerializedFieldFormat } from '../../../../field_formats/common';
import { ExpressionValueVisDimension } from '../../../../visualizations/common';
import { PaletteOutput } from '../../../../charts/common';
import { ExpressionValuePieLabels } from './expression_functions';
import { ChartTypes, ExpressionValuePartitionLabels } from './expression_functions';
export enum EmptySizeRatios {
SMALL = 0.3,
@ -28,7 +28,7 @@ export interface Dimension {
}
export interface Dimensions {
metric: ExpressionValueVisDimension;
metric?: ExpressionValueVisDimension;
buckets?: ExpressionValueVisDimension[];
splitRow?: ExpressionValueVisDimension[];
splitColumn?: ExpressionValueVisDimension[];
@ -36,45 +36,74 @@ export interface Dimensions {
export interface LabelsParams {
show: boolean;
last_level: boolean;
position: LabelPositions;
values: boolean;
truncate: number | null;
valuesFormat: ValueFormats;
percentDecimals: number;
/** @deprecated This field is deprecated and going to be removed in the futher release versions. */
truncate?: number | null;
/** @deprecated This field is deprecated and going to be removed in the futher release versions. */
last_level?: boolean;
}
interface PieCommonParams {
interface VisCommonParams {
addTooltip: boolean;
addLegend: boolean;
legendDisplay: LegendDisplay;
legendPosition: Position;
nestedLegend: boolean;
truncateLegend: boolean;
maxLegendLines: number;
distinctColors: boolean;
isDonut: boolean;
emptySizeRatio?: EmptySizeRatios;
}
export interface PieVisParams extends PieCommonParams {
dimensions: Dimensions;
labels: LabelsParams;
palette: PaletteOutput;
}
export interface PieVisConfig extends PieCommonParams {
buckets?: ExpressionValueVisDimension[];
interface VisCommonConfig extends VisCommonParams {
metric: ExpressionValueVisDimension;
splitColumn?: ExpressionValueVisDimension[];
splitRow?: ExpressionValueVisDimension[];
labels: ExpressionValuePieLabels;
labels: ExpressionValuePartitionLabels;
palette: PaletteOutput;
}
export interface PartitionVisParams extends VisCommonParams {
dimensions: Dimensions;
labels: LabelsParams;
palette: PaletteOutput;
isDonut?: boolean;
showValuesInLegend?: boolean;
respectSourceOrder?: boolean;
emptySizeRatio?: EmptySizeRatios;
startFromSecondLargestSlice?: boolean;
distinctColors?: boolean;
nestedLegend?: boolean;
}
export interface PieVisConfig extends VisCommonConfig {
buckets?: ExpressionValueVisDimension[];
isDonut: boolean;
emptySizeRatio?: EmptySizeRatios;
respectSourceOrder?: boolean;
startFromSecondLargestSlice?: boolean;
distinctColors?: boolean;
nestedLegend: boolean;
}
export interface TreemapVisConfig extends VisCommonConfig {
buckets?: ExpressionValueVisDimension[];
nestedLegend: boolean;
}
export interface MosaicVisConfig extends VisCommonConfig {
buckets?: ExpressionValueVisDimension[];
nestedLegend: boolean;
}
export interface WaffleVisConfig extends VisCommonConfig {
bucket?: ExpressionValueVisDimension;
showValuesInLegend: boolean;
}
export interface RenderValue {
visData: Datatable;
visType: string;
visConfig: PieVisParams;
visType: ChartTypes;
visConfig: PartitionVisParams;
syncColors: boolean;
}
@ -88,6 +117,12 @@ export enum ValueFormats {
VALUE = 'value',
}
export enum LegendDisplay {
SHOW = 'show',
HIDE = 'hide',
DEFAULT = 'default',
}
export interface BucketColumns extends DatatableColumn {
format?: {
id?: string;

View file

@ -9,11 +9,11 @@
module.exports = {
preset: '@kbn/test',
rootDir: '../../../../',
roots: ['<rootDir>/src/plugins/chart_expressions/expression_pie'],
roots: ['<rootDir>/src/plugins/chart_expressions/expression_partition_vis'],
coverageDirectory:
'<rootDir>/target/kibana-coverage/jest/src/plugins/chart_expressions/expression_pie',
'<rootDir>/target/kibana-coverage/jest/src/plugins/chart_expressions/expression_partition_vis',
coverageReporters: ['text', 'html'],
collectCoverageFrom: [
'<rootDir>/src/plugins/chart_expressions/expression_pie/{common,public,server}/**/*.{ts,tsx}',
'<rootDir>/src/plugins/chart_expressions/expression_partition_vis/{common,public,server}/**/*.{ts,tsx}',
],
};

View file

@ -1,12 +1,12 @@
{
"id": "expressionPie",
"id": "expressionPartitionVis",
"version": "1.0.0",
"kibanaVersion": "kibana",
"owner": {
"name": "Vis Editors",
"githubTeam": "kibana-vis-editors"
},
"description": "Expression Pie plugin adds a `pie` renderer and function to the expression plugin. The renderer will display the `pie` chart.",
"description": "Expression Partition Visualization plugin adds a `partitionVis` renderer and `pieVis`, `mosaicVis`, `treemapVis`, `waffleVis` functions to the expression plugin. The renderer will display the `pie`, `waffle`, `treemap` and `mosaic` charts.",
"server": true,
"ui": true,
"extraPublicDirs": [

View file

@ -8,6 +8,6 @@
export const getFormatService = () => ({
deserialize: (target: any) => ({
convert: (text: string, format: string) => text,
convert: (text: string, format: string) => `${text}`,
}),
});

View file

@ -21,10 +21,15 @@ export const getPaletteRegistry = () => {
'#AA6556',
'#E7664C',
];
let counter = 0;
const mockPalette: PaletteDefinition = {
id: 'default',
title: 'My Palette',
getCategoricalColor: (_: SeriesLayer[]) => colors[0],
getCategoricalColor: (_: SeriesLayer[]) => {
counter++;
if (counter > colors.length - 1) counter = 0;
return colors[counter];
},
getCategoricalColors: (num: number) => colors,
toExpression: () => ({
type: 'expression',

View file

@ -8,5 +8,9 @@
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ThemeService } from '../../../../charts/public/services';
import { uiSettings } from './ui_settings';
export const theme = new ThemeService();
const theme = new ThemeService();
theme.init(uiSettings);
export { theme };

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { IUiSettingsClient, PublicUiSettingsParams, UserProvidedValues } from 'kibana/public';
import { Observable } from 'rxjs';
export const uiSettings: IUiSettingsClient = {
set: (key: string, value: any) => Promise.resolve(true),
remove: (key: string) => Promise.resolve(true),
isCustom: (key: string) => false,
isOverridden: (key: string) => Boolean(uiSettings.getAll()[key].isOverridden),
getUpdate$: () =>
new Observable<{
key: string;
newValue: any;
oldValue: any;
}>(),
isDeclared: (key: string) => true,
isDefault: (key: string) => true,
getUpdateErrors$: () => new Observable<Error>(),
get: (key: string, defaultOverride?: any): any => uiSettings.getAll()[key] || defaultOverride,
get$: (key: string) => new Observable<any>(uiSettings.get(key)),
getAll: (): Readonly<Record<string, PublicUiSettingsParams & UserProvidedValues>> => {
return {};
},
};

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FC } from 'react';
import { ComponentStory } from '@storybook/react';
import { Render } from '../../../../presentation_util/public/__stories__';
import { getPartitionVisRenderer } from '../expression_renderers';
import { ChartTypes, RenderValue } from '../../common/types';
import { palettes, theme, getStartDeps } from '../__mocks__';
import { mosaicArgTypes, treemapMosaicConfig, data } from './shared';
const containerSize = {
width: '700px',
height: '700px',
};
const PartitionVisRenderer = () => getPartitionVisRenderer({ palettes, theme, getStartDeps });
type Props = {
visType: RenderValue['visType'];
syncColors: RenderValue['syncColors'];
} & RenderValue['visConfig'];
const PartitionVis: ComponentStory<FC<Props>> = ({
visType,
syncColors,
children,
...visConfig
}) => (
<Render
renderer={PartitionVisRenderer}
config={{ visType, syncColors, visConfig, visData: data }}
{...containerSize}
/>
);
export default {
title: 'renderers/mosaicVis',
component: PartitionVis,
argTypes: mosaicArgTypes,
};
const Default = PartitionVis.bind({});
Default.args = { ...treemapMosaicConfig, visType: ChartTypes.MOSAIC, syncColors: false };
export { Default };

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FC } from 'react';
import { ComponentStory } from '@storybook/react';
import { Render } from '../../../../presentation_util/public/__stories__';
import { getPartitionVisRenderer } from '../expression_renderers';
import { ChartTypes, RenderValue } from '../../common/types';
import { palettes, theme, getStartDeps } from '../__mocks__';
import { pieDonutArgTypes, pieConfig, data } from './shared';
const containerSize = {
width: '700px',
height: '700px',
};
const PartitionVisRenderer = () => getPartitionVisRenderer({ palettes, theme, getStartDeps });
type Props = {
visType: RenderValue['visType'];
syncColors: RenderValue['syncColors'];
} & RenderValue['visConfig'];
const PartitionVis: ComponentStory<FC<Props>> = ({
visType,
syncColors,
children,
...visConfig
}) => (
<Render
renderer={PartitionVisRenderer}
config={{ visType, syncColors, visConfig, visData: data }}
{...containerSize}
/>
);
export default {
title: 'renderers/pieVis',
component: PartitionVis,
argTypes: pieDonutArgTypes,
};
const Default = PartitionVis.bind({});
Default.args = { ...pieConfig, visType: ChartTypes.PIE, syncColors: false };
export { Default };

View file

@ -0,0 +1,216 @@
/*
* 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 { ArgTypes } from '@storybook/addons';
import { EmptySizeRatios, LegendDisplay } from '../../../common';
import { ChartTypes } from '../../../common/types';
const visConfigName = 'visConfig';
export const argTypes: ArgTypes = {
addTooltip: {
name: `${visConfigName}.addTooltip`,
description: 'Add tooltip on hover',
type: { name: 'boolean', required: false },
table: { type: { summary: 'boolean' }, defaultValue: { summary: true } },
control: { type: 'boolean' },
},
legendDisplay: {
name: `${visConfigName}.legendDisplay`,
description: 'Legend mode of displaying',
type: { name: 'string', required: false },
table: { type: { summary: 'string' }, defaultValue: { summary: LegendDisplay.HIDE } },
options: Object.values(LegendDisplay),
control: { type: 'select' },
},
legendPosition: {
name: `${visConfigName}.legendPosition`,
description: 'Legend position',
type: { name: 'string', required: false },
table: { type: { summary: 'string' }, defaultValue: { summary: Position.Bottom } },
options: Object.values(Position),
control: { type: 'select' },
},
truncateLegend: {
name: `${visConfigName}.truncateLegend`,
description: 'Truncate too long legend',
type: { name: 'boolean', required: false },
table: { type: { summary: 'boolean' }, defaultValue: { summary: true } },
control: { type: 'boolean' },
},
maxLegendLines: {
name: `${visConfigName}.maxLegendLines`,
description: 'Legend maximum number of lines',
type: { name: 'number', required: false },
table: { type: { summary: 'number' } },
control: { type: 'number' },
},
palette: {
name: `${visConfigName}.palette`,
description: 'Palette',
type: { name: 'palette', required: false },
table: { type: { summary: 'object' } },
control: { type: 'object' },
},
labels: {
name: `${visConfigName}.labels`,
description: 'Labels configuration',
type: { name: 'object', required: false },
table: {
type: {
summary: 'object',
detail: `Labels configuration consists of further fields:
- show: boolean. Default: true.
- position: string. Options: 'default', 'inside'. Default: 'default'.
- values: boolean. Default: true.
- percentDecimals: number. Default: 2.
- last_level: boolean. Default: false. DEPRECATED.
- truncate: number. Default: null.
- valuesFormat: string. Options: 'percent', 'value'. Default: percent.
`,
},
},
control: { type: 'object' },
},
dimensions: {
name: `${visConfigName}.dimensions`,
description: 'dimensions configuration',
type: { name: 'object', required: false },
table: {
type: {
summary: 'object',
detail: `Dimensions configuration consists of two fields:
- metric: visdimension.
- buckets: visdimension[].
`,
},
},
control: { type: 'object' },
},
};
export const pieDonutArgTypes: ArgTypes = {
...argTypes,
visType: {
name: `visType`,
description: 'Type of the chart',
type: { name: 'string', required: false },
table: {
type: { summary: 'string' },
defaultValue: { summary: `${ChartTypes.PIE} | ${ChartTypes.DONUT}` },
},
control: { type: 'text', disable: true },
},
isDonut: {
name: `${visConfigName}.isDonut`,
description: 'Render a donut chart',
type: { name: 'boolean', required: false },
table: { type: { summary: 'boolean' }, defaultValue: { summary: false } },
control: { type: 'boolean' },
},
emptySizeRatio: {
name: `${visConfigName}.emptySizeRatio`,
description: 'The hole size of the donut chart',
type: { name: 'number', required: false },
table: { type: { summary: 'number' }, defaultValue: { summary: EmptySizeRatios.SMALL } },
options: [EmptySizeRatios.SMALL, EmptySizeRatios.MEDIUM, EmptySizeRatios.LARGE],
control: { type: 'select' },
},
distinctColors: {
name: `${visConfigName}.distinctColors`,
description: 'Enable distinct colors',
type: { name: 'boolean', required: false },
table: { type: { summary: 'boolean' }, defaultValue: { summary: false } },
control: { type: 'boolean' },
},
respectSourceOrder: {
name: `${visConfigName}.respectSourceOrder`,
description: 'Save default order of the incomming data',
type: { name: 'boolean', required: false },
table: { type: { summary: 'boolean' }, defaultValue: { summary: true } },
control: { type: 'boolean' },
},
startFromSecondLargestSlice: {
name: `${visConfigName}.startFromSecondLargestSlice`,
description: 'Start placement of slices from the second largest slice',
type: { name: 'boolean', required: false },
table: { type: { summary: 'boolean' }, defaultValue: { summary: true } },
control: { type: 'boolean' },
},
nestedLegend: {
name: `${visConfigName}.nestedLegend`,
description: 'Enable nested legend',
type: { name: 'boolean', required: false },
table: { type: { summary: 'boolean' }, defaultValue: { summary: false } },
control: { type: 'boolean' },
},
};
export const treemapArgTypes: ArgTypes = {
visType: {
name: `visType`,
description: 'Type of the chart',
type: { name: 'string', required: false },
table: {
type: { summary: 'string' },
defaultValue: { summary: `${ChartTypes.TREEMAP}` },
},
control: { type: 'text', disable: true },
},
...argTypes,
nestedLegend: {
name: `${visConfigName}.nestedLegend`,
description: 'Enable nested legend',
type: { name: 'boolean', required: false },
table: { type: { summary: 'boolean' }, defaultValue: { summary: false } },
control: { type: 'boolean' },
},
};
export const mosaicArgTypes: ArgTypes = {
visType: {
name: `visType`,
description: 'Type of the chart',
type: { name: 'string', required: false },
table: {
type: { summary: 'string' },
defaultValue: { summary: `${ChartTypes.MOSAIC}` },
},
control: { type: 'text', disable: true },
},
...argTypes,
nestedLegend: {
name: `${visConfigName}.nestedLegend`,
description: 'Enable nested legend',
type: { name: 'boolean', required: false },
table: { type: { summary: 'boolean' }, defaultValue: { summary: false } },
control: { type: 'boolean' },
},
};
export const waffleArgTypes: ArgTypes = {
visType: {
name: `visType`,
description: 'Type of the chart',
type: { name: 'string', required: false },
table: {
type: { summary: 'string' },
defaultValue: { summary: `${ChartTypes.WAFFLE}` },
},
control: { type: 'text', disable: true },
},
...argTypes,
showValuesInLegend: {
name: `${visConfigName}.nestedLegend`,
description: 'Enable displaying values in the legend',
type: { name: 'boolean', required: false },
table: { type: { summary: 'boolean' }, defaultValue: { summary: false } },
control: { type: 'boolean' },
},
};

View file

@ -0,0 +1,129 @@
/*
* 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 {
LabelPositions,
LegendDisplay,
RenderValue,
PartitionVisParams,
ValueFormats,
} from '../../../common/types';
export const config: RenderValue['visConfig'] = {
addTooltip: true,
legendDisplay: LegendDisplay.HIDE,
truncateLegend: true,
respectSourceOrder: true,
legendPosition: Position.Bottom,
maxLegendLines: 1,
palette: {
type: 'palette',
name: 'system_palette',
},
labels: {
show: true,
position: LabelPositions.DEFAULT,
percentDecimals: 2,
values: true,
truncate: 0,
valuesFormat: ValueFormats.PERCENT,
last_level: false,
},
dimensions: {
metric: {
type: 'vis_dimension',
accessor: {
id: 'percent_uptime',
name: 'percent_uptime',
meta: {
type: 'number',
},
},
format: {
id: 'string',
params: {},
},
},
},
};
export const pieConfig: PartitionVisParams = {
...config,
isDonut: false,
emptySizeRatio: 0,
distinctColors: false,
nestedLegend: false,
dimensions: {
...config.dimensions,
buckets: [
{
type: 'vis_dimension',
accessor: {
id: 'project',
name: 'project',
meta: {
type: 'string',
},
},
format: {
id: 'string',
params: {},
},
},
],
},
startFromSecondLargestSlice: true,
};
export const treemapMosaicConfig: PartitionVisParams = {
...config,
nestedLegend: false,
dimensions: {
...config.dimensions,
buckets: [
{
type: 'vis_dimension',
accessor: {
id: 'project',
name: 'project',
meta: {
type: 'string',
},
},
format: {
id: 'string',
params: {},
},
},
],
},
};
export const waffleConfig: PartitionVisParams = {
...config,
dimensions: {
...config.dimensions,
buckets: [
{
type: 'vis_dimension',
accessor: {
id: 'project',
name: 'project',
meta: {
type: 'string',
},
},
format: {
id: 'string',
params: {},
},
},
],
},
showValuesInLegend: false,
};

View file

@ -0,0 +1,207 @@
/*
* 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 { RenderValue } from '../../../common/types';
export const data: RenderValue['visData'] = {
type: 'datatable',
columns: [
{
id: '@timestamp',
name: '@timestamp',
meta: {
type: 'date',
},
},
{
id: 'time',
name: 'time',
meta: {
type: 'date',
},
},
{
id: 'cost',
name: 'cost',
meta: {
type: 'number',
},
},
{
id: 'username',
name: 'username',
meta: {
type: 'string',
},
},
{
id: 'price',
name: 'price',
meta: {
type: 'number',
},
},
{
id: 'age',
name: 'age',
meta: {
type: 'number',
},
},
{
id: 'country',
name: 'country',
meta: {
type: 'string',
},
},
{
id: 'state',
name: 'state',
meta: {
type: 'string',
},
},
{
id: 'project',
name: 'project',
meta: {
type: 'string',
},
},
{
id: 'percent_uptime',
name: 'percent_uptime',
meta: {
type: 'number',
},
},
],
rows: [
{
age: 63,
cost: 32.15,
country: 'US',
price: 53,
project: 'elasticsearch',
state: 'running',
time: 1546334211208,
'@timestamp': 1546334211208,
username: 'aevans2e',
percent_uptime: 0.83,
},
{
age: 68,
cost: 20.52,
country: 'JP',
price: 33,
project: 'beats',
state: 'done',
time: 1546351551031,
'@timestamp': 1546351551031,
username: 'aking2c',
percent_uptime: 0.9,
},
{
age: 57,
cost: 21.15,
country: 'UK',
price: 59,
project: 'apm',
state: 'running',
time: 1546352631083,
'@timestamp': 1546352631083,
username: 'mmoore2o',
percent_uptime: 0.96,
},
{
age: 73,
cost: 35.64,
country: 'CN',
price: 71,
project: 'machine-learning',
state: 'start',
time: 1546402490956,
'@timestamp': 1546402490956,
username: 'wrodriguez1r',
percent_uptime: 0.61,
},
{
age: 38,
cost: 27.19,
country: 'TZ',
price: 36,
project: 'kibana',
state: 'done',
time: 1546467111351,
'@timestamp': 1546467111351,
username: 'wrodriguez1r',
percent_uptime: 0.72,
},
{
age: 61,
cost: 49.95,
country: 'NL',
price: 65,
project: 'machine-learning',
state: 'start',
time: 1546473771019,
'@timestamp': 1546473771019,
username: 'mmoore2o',
percent_uptime: 0.72,
},
{
age: 53,
cost: 27.36,
country: 'JP',
price: 60,
project: 'x-pack',
state: 'running',
time: 1546482171310,
'@timestamp': 1546482171310,
username: 'hcrawford2h',
percent_uptime: 0.65,
},
{
age: 31,
cost: 33.77,
country: 'AZ',
price: 77,
project: 'kibana',
state: 'start',
time: 1546493451206,
'@timestamp': 1546493451206,
username: 'aking2c',
percent_uptime: 0.92,
},
{
age: 71,
cost: 20.2,
country: 'TZ',
price: 57,
project: 'swiftype',
state: 'running',
time: 1546494651235,
'@timestamp': 1546494651235,
username: 'jlawson2p',
percent_uptime: 0.59,
},
{
age: 54,
cost: 36.65,
country: 'TZ',
price: 72,
project: 'apm',
state: 'done',
time: 1546498431195,
'@timestamp': 1546498431195,
username: 'aking2c',
percent_uptime: 1,
},
],
};

View file

@ -0,0 +1,17 @@
/*
* 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 { data } from './data';
export { config, pieConfig, treemapMosaicConfig, waffleConfig } from './config';
export {
argTypes,
pieDonutArgTypes,
treemapArgTypes,
mosaicArgTypes,
waffleArgTypes,
} from './arg_types';

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FC } from 'react';
import { ComponentStory } from '@storybook/react';
import { Render } from '../../../../presentation_util/public/__stories__';
import { getPartitionVisRenderer } from '../expression_renderers';
import { ChartTypes, RenderValue } from '../../common/types';
import { palettes, theme, getStartDeps } from '../__mocks__';
import { treemapArgTypes, treemapMosaicConfig, data } from './shared';
const containerSize = {
width: '700px',
height: '700px',
};
const PartitionVisRenderer = () => getPartitionVisRenderer({ palettes, theme, getStartDeps });
type Props = {
visType: RenderValue['visType'];
syncColors: RenderValue['syncColors'];
} & RenderValue['visConfig'];
const PartitionVis: ComponentStory<FC<Props>> = ({
visType,
syncColors,
children,
...visConfig
}) => (
<Render
renderer={PartitionVisRenderer}
config={{ visType, syncColors, visConfig, visData: data }}
{...containerSize}
/>
);
export default {
title: 'renderers/treemapVis',
component: PartitionVis,
argTypes: treemapArgTypes,
};
const Default = PartitionVis.bind({});
Default.args = { ...treemapMosaicConfig, visType: ChartTypes.TREEMAP, syncColors: false };
export { Default };

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import React, { FC } from 'react';
import { ComponentStory } from '@storybook/react';
import { Render } from '../../../../presentation_util/public/__stories__';
import { getPartitionVisRenderer } from '../expression_renderers';
import { ChartTypes, RenderValue } from '../../common/types';
import { palettes, theme, getStartDeps } from '../__mocks__';
import { waffleArgTypes, waffleConfig, data } from './shared';
const containerSize = {
width: '700px',
height: '700px',
};
const PartitionVisRenderer = () => getPartitionVisRenderer({ palettes, theme, getStartDeps });
type Props = {
visType: RenderValue['visType'];
syncColors: RenderValue['syncColors'];
} & RenderValue['visConfig'];
const PartitionVis: ComponentStory<FC<Props>> = ({
visType,
syncColors,
children,
...visConfig
}) => (
<Render
renderer={PartitionVisRenderer}
config={{ visType, syncColors, visConfig, visData: data }}
{...containerSize}
/>
);
export default {
title: 'renderers/waffleVis',
component: PartitionVis,
argTypes: waffleArgTypes,
};
const Default = PartitionVis.bind({});
Default.args = { ...waffleConfig, visType: ChartTypes.WAFFLE, syncColors: false };
export { Default };

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export { getPieVisRenderer } from './pie_vis_renderer';
export * from './partition_vis_component';

View file

@ -9,15 +9,15 @@
import { EuiThemeComputed } from '@elastic/eui';
import { css } from '@emotion/react';
export const pieChartWrapperStyle = css({
export const partitionVisWrapperStyle = css({
display: 'flex',
flex: '1 1 auto',
minHeight: 0,
minWidth: 0,
});
export const pieChartContainerStyleFactory = (theme: EuiThemeComputed) => css`
${pieChartWrapperStyle};
export const partitionVisContainerStyleFactory = (theme: EuiThemeComputed) => css`
${partitionVisWrapperStyle};
position: absolute;
top: 0;
@ -27,4 +27,5 @@ export const pieChartContainerStyleFactory = (theme: EuiThemeComputed) => css`
padding: ${theme.size.s};
margin-left: auto;
margin-right: auto;
overflow: hidden;
`;

View file

@ -15,8 +15,15 @@ import type { Datatable } from '../../../../expressions/public';
import { shallow, mount } from 'enzyme';
import { findTestSubject } from '@elastic/eui/lib/test';
import { act } from 'react-dom/test-utils';
import PieComponent, { PieComponentProps } from './pie_vis_component';
import { createMockPieParams, createMockVisData } from '../mocks';
import PartitionVisComponent, { PartitionVisComponentProps } from './partition_vis_component';
import {
createMockDonutParams,
createMockPieParams,
createMockTreemapMosaicParams,
createMockVisData,
createMockWaffleParams,
} from '../mocks';
import { ChartTypes } from '../../common/types';
jest.mock('@elastic/charts', () => {
const original = jest.requireActual('@elastic/charts');
@ -42,8 +49,8 @@ const uiState = {
setSilent: jest.fn(),
} as any;
describe('PieComponent', function () {
let wrapperProps: PieComponentProps;
describe('PartitionVisComponent', function () {
let wrapperProps: PartitionVisComponentProps;
beforeAll(() => {
wrapperProps = {
@ -51,6 +58,7 @@ describe('PieComponent', function () {
palettesRegistry,
visParams,
visData,
visType: ChartTypes.PIE,
uiState,
syncColors: false,
fireEvent: jest.fn(),
@ -62,20 +70,81 @@ describe('PieComponent', function () {
};
});
it('should render correct structure for pie', function () {
const component = shallow(<PartitionVisComponent {...wrapperProps} />);
expect(component).toMatchSnapshot();
});
it('should render correct structure for donut', function () {
const donutVisParams = createMockDonutParams();
const component = shallow(
<PartitionVisComponent
{...{
...wrapperProps,
visType: ChartTypes.DONUT,
visParams: donutVisParams,
}}
/>
);
expect(component).toMatchSnapshot();
});
it('should render correct structure for treemap', function () {
const treemapVisParams = createMockTreemapMosaicParams();
const component = shallow(
<PartitionVisComponent
{...{
...wrapperProps,
visType: ChartTypes.TREEMAP,
visParams: treemapVisParams,
}}
/>
);
expect(component).toMatchSnapshot();
});
it('should render correct structure for mosaic', function () {
const mosaicVisParams = createMockTreemapMosaicParams();
const component = shallow(
<PartitionVisComponent
{...{
...wrapperProps,
visType: ChartTypes.MOSAIC,
visParams: mosaicVisParams,
}}
/>
);
expect(component).toMatchSnapshot();
});
it('should render correct structure for waffle', function () {
const waffleVisParams = createMockWaffleParams();
const component = shallow(
<PartitionVisComponent
{...{
...wrapperProps,
visType: ChartTypes.MOSAIC,
visParams: waffleVisParams,
}}
/>
);
expect(component).toMatchSnapshot();
});
it('renders the legend on the correct position', () => {
const component = shallow(<PieComponent {...wrapperProps} />);
const component = shallow(<PartitionVisComponent {...wrapperProps} />);
expect(component.find(Settings).prop('legendPosition')).toEqual('right');
});
it('renders the legend toggle component', async () => {
const component = mount(<PieComponent {...wrapperProps} />);
const component = mount(<PartitionVisComponent {...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} />);
const component = mount(<PartitionVisComponent {...wrapperProps} />);
findTestSubject(component, 'vislibToggleLegend').simulate('click');
await act(async () => {
expect(component.find(Settings).prop('showLegend')).toEqual(false);
@ -83,31 +152,31 @@ describe('PieComponent', function () {
});
it('defaults on showing the legend for the inner cicle', () => {
const component = shallow(<PieComponent {...wrapperProps} />);
const component = shallow(<PartitionVisComponent {...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} />);
const component = shallow(<PartitionVisComponent {...newProps} />);
expect(component.find(Settings).prop('legendMaxDepth')).toBeUndefined();
});
it('defaults on displaying the tooltip', () => {
const component = shallow(<PieComponent {...wrapperProps} />);
const component = shallow(<PartitionVisComponent {...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} />);
const component = shallow(<PartitionVisComponent {...newProps} />);
expect(component.find(Settings).prop('tooltip')).toStrictEqual({ type: TooltipType.None });
});
it('calls filter callback', () => {
const component = shallow(<PieComponent {...wrapperProps} />);
const component = shallow(<PartitionVisComponent {...wrapperProps} />);
component.find(Settings).first().prop('onElementClick')!([
[
[
@ -130,14 +199,14 @@ describe('PieComponent', function () {
const newVisData = {
type: 'datatable',
columns: [
{
id: 'col-1-1',
name: 'Count',
},
{
id: 'col-0-2',
name: 'filters',
},
{
id: 'col-1-1',
name: 'Count',
},
],
rows: [
{
@ -151,7 +220,7 @@ describe('PieComponent', function () {
],
} as unknown as Datatable;
const newProps = { ...wrapperProps, visData: newVisData };
const component = mount(<PieComponent {...newProps} />);
const component = mount(<PartitionVisComponent {...newProps} />);
expect(findTestSubject(component, 'pieVisualizationError').text()).toEqual('No results found');
});
@ -159,14 +228,14 @@ describe('PieComponent', function () {
const newVisData = {
type: 'datatable',
columns: [
{
id: 'col-1-1',
name: 'Count',
},
{
id: 'col-0-2',
name: 'filters',
},
{
id: 'col-1-1',
name: 'Count',
},
],
rows: [
{
@ -180,7 +249,7 @@ describe('PieComponent', function () {
],
} as unknown as Datatable;
const newProps = { ...wrapperProps, visData: newVisData };
const component = mount(<PieComponent {...newProps} />);
const component = mount(<PartitionVisComponent {...newProps} />);
expect(findTestSubject(component, 'pieVisualizationError').text()).toEqual(
"Pie/donut charts can't render with negative values."
);

View file

@ -18,7 +18,6 @@ import {
TooltipProps,
TooltipType,
SeriesIdentifier,
PartitionLayout,
} from '@elastic/charts';
import { useEuiTheme } from '@elastic/eui';
import {
@ -36,7 +35,7 @@ import {
import type { FieldFormat } from '../../../../field_formats/common';
import { DEFAULT_PERCENT_DECIMALS } from '../../common/constants';
import {
PieVisParams,
PartitionVisParams,
BucketColumns,
ValueFormats,
PieContainerDimensions,
@ -53,11 +52,21 @@ import {
getColumns,
getSplitDimensionAccessor,
getColumnByAccessor,
isLegendFlat,
shouldShowLegend,
generateFormatters,
getFormatter,
getPartitionType,
} from '../utils';
import { ChartSplit, SMALL_MULTIPLES_ID } from './chart_split';
import { VisualizationNoResults } from './visualization_noresults';
import { VisTypePiePluginStartDependencies } from '../plugin';
import { pieChartWrapperStyle, pieChartContainerStyleFactory } from './pie_vis_component.styles';
import {
partitionVisWrapperStyle,
partitionVisContainerStyleFactory,
} from './partition_vis_component.styles';
import { ChartTypes } from '../../common/types';
import { filterOutConfig } from '../utils/filter_out_config';
declare global {
interface Window {
@ -67,9 +76,10 @@ declare global {
_echDebugStateFlag?: boolean;
}
}
export interface PieComponentProps {
visParams: PieVisParams;
export interface PartitionVisComponentProps {
visParams: PartitionVisParams;
visData: Datatable;
visType: ChartTypes;
uiState: PersistedState;
fireEvent: IInterpreterRenderHandlers['event'];
renderComplete: IInterpreterRenderHandlers['done'];
@ -79,15 +89,31 @@ export interface PieComponentProps {
syncColors: boolean;
}
const PieComponent = (props: PieComponentProps) => {
const PartitionVisComponent = (props: PartitionVisComponentProps) => {
const { visData, visParams: preVisParams, visType, services, syncColors } = props;
const visParams = useMemo(() => filterOutConfig(visType, preVisParams), [preVisParams, visType]);
const theme = useEuiTheme();
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) ?? bwcLegendStateDefault;
});
const { bucketColumns, metricColumn } = useMemo(
() => getColumns(props.visParams, props.visData),
[props.visData, props.visParams]
);
const formatters = useMemo(
() => generateFormatters(visParams, visData, services.fieldFormats.deserialize),
[services.fieldFormats.deserialize, visData, visParams]
);
const showLegendDefault = useCallback(() => {
const showLegendDef = shouldShowLegend(visType, visParams.legendDisplay, bucketColumns);
return props.uiState?.get('vis.legendOpen', showLegendDef) ?? showLegendDef;
}, [bucketColumns, props.uiState, visParams.legendDisplay, visType]);
const [showLegend, setShowLegend] = useState<boolean>(() => showLegendDefault());
const [dimensions, setDimensions] = useState<undefined | PieContainerDimensions>();
const parentRef = useRef<HTMLDivElement>(null);
@ -100,6 +126,12 @@ const PieComponent = (props: PieComponentProps) => {
}
}, [parentRef]);
useEffect(() => {
const legendShow = showLegendDefault();
setShowLegend(legendShow);
props.uiState?.set('vis.legendOpen', legendShow);
}, [showLegendDefault, props.uiState]);
const onRenderChange = useCallback<RenderChangeListener>(
(isRendered) => {
if (isRendered) {
@ -113,15 +145,15 @@ const PieComponent = (props: PieComponentProps) => {
const handleSliceClick = useCallback(
(
clickedLayers: LayerValue[],
bucketColumns: Array<Partial<BucketColumns>>,
visData: Datatable,
buckets: Array<Partial<BucketColumns>>,
vData: Datatable,
splitChartDimension?: DatatableColumn,
splitChartFormatter?: FieldFormat
): void => {
const data = getFilterClickData(
clickedLayers,
bucketColumns,
visData,
buckets,
vData,
splitChartDimension,
splitChartFormatter
);
@ -136,9 +168,9 @@ const PieComponent = (props: PieComponentProps) => {
// handles legend action event data
const getLegendActionEventData = useCallback(
(visData: Datatable) =>
(vData: Datatable) =>
(series: SeriesIdentifier): ClickTriggerEvent | null => {
const data = getFilterEventData(visData, series);
const data = getFilterEventData(vData, series);
return {
name: 'filterBucket',
@ -172,11 +204,6 @@ const PieComponent = (props: PieComponentProps) => {
});
}, [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') || {};
@ -192,22 +219,22 @@ const PieComponent = (props: PieComponentProps) => {
[props.uiState]
);
const { visData, visParams, services, syncColors } = props;
function getSliceValue(d: Datum, metricColumn: DatatableColumn) {
const value = d[metricColumn.id];
const getSliceValue = useCallback((d: Datum, metric: DatatableColumn) => {
const value = d[metric.id];
return Number.isFinite(value) && value >= 0 ? value : 0;
}
}, []);
const defaultFormatter = services.fieldFormats.deserialize;
// 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)
const metricFieldFormatter = getFormatter(metricColumn, formatters, defaultFormatter);
const { splitColumn, splitRow } = visParams.dimensions;
const splitChartFormatter = splitColumn
? getFormatter(splitColumn[0], formatters, defaultFormatter)
: splitRow
? getFormatter(splitRow[0], formatters, defaultFormatter)
: undefined;
const percentFormatter = services.fieldFormats.deserialize({
id: 'percent',
params: {
@ -215,70 +242,71 @@ const PieComponent = (props: PieComponentProps) => {
},
});
const { bucketColumns, metricColumn } = useMemo(
() => getColumns(visParams, visData),
[visData, visParams]
);
const isDarkMode = props.chartsThemeService.useDarkMode();
const layers = useMemo(
() =>
getLayers(
visType,
bucketColumns,
visParams,
visData,
props.uiState?.get('vis.colors', {}),
visData.rows,
props.palettesRegistry,
formatters,
services.fieldFormats,
syncColors
syncColors,
isDarkMode
),
[
visType,
bucketColumns,
visParams,
visData,
props.uiState,
props.palettesRegistry,
visData.rows,
formatters,
services.fieldFormats,
syncColors,
isDarkMode,
]
);
const rescaleFactor = useMemo(() => {
const overallSum = visData.rows.reduce((sum, row) => sum + row[metricColumn.id], 0);
const slices = visData.rows.map((row) => row[metricColumn.id] / overallSum);
const smallSlices = slices.filter((value) => value < 0.02).length;
if (smallSlices) {
const smallSlices = slices.filter((value) => value < 0.02) ?? [];
if (smallSlices.length) {
// shrink up to 20% to give some room for the linked values
return 1 / (1 + Math.min(smallSlices * 0.05, 0.2));
return 1 / (1 + Math.min(smallSlices.length * 0.05, 0.2));
}
return 1;
}, [visData.rows, metricColumn]);
const themeOverrides = useMemo(
() => getPartitionTheme(visParams, chartTheme, dimensions, rescaleFactor),
[chartTheme, visParams, dimensions, rescaleFactor]
() => getPartitionTheme(visType, visParams, chartTheme, dimensions, rescaleFactor),
[visType, visParams, chartTheme, dimensions, rescaleFactor]
);
const fixedViewPort = document.getElementById('app-fixed-viewport');
const tooltip: TooltipProps = {
...(fixedViewPort ? { boundary: fixedViewPort } : {}),
type: visParams.addTooltip ? TooltipType.Follow : TooltipType.None,
};
const legendPosition = visParams.legendPosition ?? Position.Right;
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])
const splitChartColumnAccessor = splitColumn
? getSplitDimensionAccessor(visData.columns, splitColumn[0], formatters, defaultFormatter)
: undefined;
const splitChartDimension = visParams.dimensions.splitColumn
? getColumnByAccessor(visParams.dimensions.splitColumn[0].accessor, visData.columns)
: visParams.dimensions.splitRow
? getColumnByAccessor(visParams.dimensions.splitRow[0].accessor, visData.columns)
const splitChartRowAccessor = splitRow
? getSplitDimensionAccessor(visData.columns, splitRow[0], formatters, defaultFormatter)
: undefined;
const splitChartDimension = splitColumn
? getColumnByAccessor(splitColumn[0].accessor, visData.columns)
: splitRow
? getColumnByAccessor(splitRow[0].accessor, visData.columns)
: undefined;
/**
@ -302,15 +330,16 @@ const PieComponent = (props: PieComponentProps) => {
}),
[visData.rows, metricColumn]
);
const flatLegend = isLegendFlat(visType, splitChartDimension);
const canShowPieChart = !isAllZeros && !hasNegative;
const partitionType = getPartitionType(visType);
return (
<div css={pieChartContainerStyleFactory(theme.euiTheme)} data-test-subj="visTypePieChart">
<div css={partitionVisContainerStyleFactory(theme.euiTheme)} data-test-subj="visTypePieChart">
{!canShowPieChart ? (
<VisualizationNoResults hasNegativeValues={hasNegative} />
) : (
<div css={pieChartWrapperStyle} ref={parentRef}>
<div css={partitionVisWrapperStyle} ref={parentRef}>
<LegendColorPickerWrapperContext.Provider
value={{
legendPosition,
@ -319,7 +348,7 @@ const PieComponent = (props: PieComponentProps) => {
palette: visParams.palette.name,
data: visData.rows,
uiState: props.uiState,
distinctColors: visParams.distinctColors,
distinctColors: visParams.distinctColors ?? false,
}}
>
<LegendToggle
@ -338,8 +367,9 @@ const PieComponent = (props: PieComponentProps) => {
legendPosition={legendPosition}
legendMaxDepth={visParams.nestedLegend ? undefined : 1}
legendColorPicker={props.uiState ? LegendColorPickerWrapper : undefined}
flatLegend={Boolean(splitChartDimension)}
flatLegend={flatLegend}
tooltip={tooltip}
showLegendExtra={visParams.showValuesInLegend}
onElementClick={(args) => {
handleSliceClick(
args[0][0] as LayerValue[],
@ -374,11 +404,11 @@ const PieComponent = (props: PieComponentProps) => {
onRenderChange={onRenderChange}
/>
<Partition
id="pie"
id={visType}
smallMultiples={SMALL_MULTIPLES_ID}
data={visData.rows}
layout={PartitionLayout.sunburst}
specialFirstInnermostSector={false}
layout={partitionType}
specialFirstInnermostSector={visParams.startFromSecondLargestSlice}
valueAccessor={(d: Datum) => getSliceValue(d, metricColumn)}
percentFormatter={(d: number) => percentFormatter.convert(d / 100)}
valueGetter={
@ -386,7 +416,7 @@ const PieComponent = (props: PieComponentProps) => {
visParams.labels.valuesFormat === ValueFormats.VALUE ||
!visParams.labels.values
? undefined
: 'percent'
: ValueFormats.PERCENT
}
valueFormatter={(d: number) =>
!visParams.labels.show || !visParams.labels.values
@ -405,4 +435,4 @@ const PieComponent = (props: PieComponentProps) => {
};
// eslint-disable-next-line import/no-default-export
export default memo(PieComponent);
export default memo(PartitionVisComponent);

View file

@ -19,10 +19,10 @@ export const VisualizationNoResults = ({ hasNegativeValues = false }) => {
body={
<EuiText size="xs">
{hasNegativeValues
? i18n.translate('expressionPie.negativeValuesFound', {
? i18n.translate('expressionPartitionVis.negativeValuesFound', {
defaultMessage: "Pie/donut charts can't render with negative values.",
})
: i18n.translate('expressionPie.noResultsFoundTitle', {
: i18n.translate('expressionPartitionVis.noResultsFoundTitle', {
defaultMessage: 'No results found',
})}
</EuiText>

View file

@ -6,5 +6,4 @@
* Side Public License, v 1.
*/
export { pieVisFunction } from './pie_vis_function';
export { pieLabelsFunction } from './pie_labels_function';
export { getPartitionVisRenderer } from './partition_vis_renderer';

View file

@ -15,23 +15,23 @@ import { VisualizationContainer } from '../../../../visualizations/public';
import type { PersistedState } from '../../../../visualizations/public';
import { KibanaThemeProvider } from '../../../../kibana_react/public';
import { PIE_VIS_EXPRESSION_NAME } from '../../common/constants';
import { RenderValue } from '../../common/types';
import { PARTITION_VIS_RENDERER_NAME } from '../../common/constants';
import { ChartTypes, RenderValue } from '../../common/types';
import { VisTypePieDependencies } from '../plugin';
export const strings = {
getDisplayName: () =>
i18n.translate('expressionPie.renderer.pieVis.displayName', {
i18n.translate('expressionPartitionVis.renderer.pieVis.displayName', {
defaultMessage: 'Pie visualization',
}),
getHelpDescription: () =>
i18n.translate('expressionPie.renderer.pieVis.helpDescription', {
i18n.translate('expressionPartitionVis.renderer.pieVis.helpDescription', {
defaultMessage: 'Render a pie',
}),
};
const PieComponent = lazy(() => import('../components/pie_vis_component'));
const PartitionVisComponent = lazy(() => import('../components/partition_vis_component'));
function shouldShowNoResultsMessage(visData: Datatable | undefined): boolean {
const rows: object[] | undefined = visData?.rows;
@ -40,14 +40,14 @@ function shouldShowNoResultsMessage(visData: Datatable | undefined): boolean {
return Boolean(isZeroHits);
}
export const getPieVisRenderer: (
export const getPartitionVisRenderer: (
deps: VisTypePieDependencies
) => ExpressionRenderDefinition<RenderValue> = ({ theme, palettes, getStartDeps }) => ({
name: PIE_VIS_EXPRESSION_NAME,
name: PARTITION_VIS_RENDERER_NAME,
displayName: strings.getDisplayName(),
help: strings.getHelpDescription(),
reuseDomNode: true,
render: async (domNode, { visConfig, visData, syncColors }, handlers) => {
render: async (domNode, { visConfig, visData, visType, syncColors }, handlers) => {
const showNoResult = shouldShowNoResultsMessage(visData);
handlers.onDestroy(() => {
@ -61,11 +61,12 @@ export const getPieVisRenderer: (
<I18nProvider>
<KibanaThemeProvider theme$={services.kibanaTheme.theme$}>
<VisualizationContainer handlers={handlers} showNoResult={showNoResult}>
<PieComponent
<PartitionVisComponent
chartsThemeService={theme}
palettesRegistry={palettesRegistry}
visParams={visConfig}
visData={visData}
visType={visConfig.isDonut ? ChartTypes.DONUT : visType}
renderComplete={handlers.done}
fireEvent={handlers.event}
uiState={handlers.uiState as PersistedState}

View file

@ -6,10 +6,10 @@
* Side Public License, v 1.
*/
import { ExpressionPiePlugin } from './plugin';
import { ExpressionPartitionVisPlugin } from './plugin';
export function plugin() {
return new ExpressionPiePlugin();
return new ExpressionPartitionVisPlugin();
}
export type { ExpressionPiePluginSetup, ExpressionPiePluginStart } from './types';
export type { ExpressionPartitionVisPluginSetup, ExpressionPartitionVisPluginStart } from './types';

View file

@ -9,10 +9,11 @@
import { Datatable } from '../../../expressions/public';
import {
BucketColumns,
PieVisParams,
PartitionVisParams,
LabelPositions,
ValueFormats,
} from '../common/types/expression_renderers';
LegendDisplay,
} from '../common/types';
export const createMockBucketColumns = (): BucketColumns[] => {
return [
@ -107,50 +108,50 @@ export const createMockVisData = (): Datatable => {
rows: [
{
'col-0-2': 'Logstash Airways',
'col-2-3': 0,
'col-1-1': 797,
'col-2-3': 0,
'col-3-1': 689,
},
{
'col-0-2': 'Logstash Airways',
'col-2-3': 1,
'col-1-1': 797,
'col-2-3': 1,
'col-3-1': 108,
},
{
'col-0-2': 'JetBeats',
'col-2-3': 0,
'col-1-1': 766,
'col-2-3': 0,
'col-3-1': 654,
},
{
'col-0-2': 'JetBeats',
'col-2-3': 1,
'col-1-1': 766,
'col-2-3': 1,
'col-3-1': 112,
},
{
'col-0-2': 'ES-Air',
'col-2-3': 0,
'col-1-1': 744,
'col-2-3': 0,
'col-3-1': 665,
},
{
'col-0-2': 'ES-Air',
'col-2-3': 1,
'col-1-1': 744,
'col-2-3': 1,
'col-3-1': 79,
},
{
'col-0-2': 'Kibana Airlines',
'col-2-3': 0,
'col-1-1': 731,
'col-2-3': 0,
'col-3-1': 655,
},
{
'col-0-2': 'Kibana Airlines',
'col-2-3': 1,
'col-1-1': 731,
'col-2-3': 1,
'col-3-1': 76,
},
],
@ -269,9 +270,9 @@ export const createMockVisData = (): Datatable => {
};
};
export const createMockPieParams = (): PieVisParams => {
export const createMockPartitionVisParams = (): PartitionVisParams => {
return {
addLegend: true,
legendDisplay: LegendDisplay.SHOW,
addTooltip: true,
isDonut: true,
labels: {
@ -291,19 +292,20 @@ export const createMockPieParams = (): PieVisParams => {
name: 'default',
type: 'palette',
},
type: 'pie',
dimensions: {
metric: {
type: 'vis_dimension',
accessor: 1,
format: {
id: 'number',
params: {
id: 'number',
},
},
params: {},
label: 'Count',
aggType: 'count',
},
buckets: [
{
type: 'vis_dimension',
accessor: 0,
format: {
id: 'terms',
@ -313,10 +315,9 @@ export const createMockPieParams = (): PieVisParams => {
missingBucketLabel: 'Missing',
},
},
label: 'Carrier: Descending',
aggType: 'terms',
},
{
type: 'vis_dimension',
accessor: 2,
format: {
id: 'terms',
@ -326,10 +327,56 @@ export const createMockPieParams = (): PieVisParams => {
missingBucketLabel: 'Missing',
},
},
label: 'Cancelled: Descending',
aggType: 'terms',
},
],
},
} as unknown as PieVisParams;
};
};
export const createMockPieParams = (): PartitionVisParams => {
return {
...createMockPartitionVisParams(),
isDonut: false,
distinctColors: false,
};
};
export const createMockDonutParams = (): PartitionVisParams => {
return {
...createMockPartitionVisParams(),
isDonut: true,
emptySizeRatio: 0.3,
};
};
export const createMockTreemapMosaicParams = (): PartitionVisParams => {
return {
...createMockPartitionVisParams(),
nestedLegend: true,
};
};
export const createMockWaffleParams = (): PartitionVisParams => {
const visParams = createMockPartitionVisParams();
return {
...visParams,
dimensions: {
...visParams.dimensions,
buckets: [
{
type: 'vis_dimension',
accessor: 0,
format: {
id: 'terms',
params: {
id: 'string',
otherBucketLabel: 'Other',
missingBucketLabel: 'Missing',
},
},
},
],
},
showValuesInLegend: true,
};
};

View file

@ -10,9 +10,20 @@ import { FieldFormatsStart } from '../../../field_formats/public';
import { CoreSetup, CoreStart, ThemeServiceStart } from '../../../../core/public';
import { ChartsPluginSetup } from '../../../charts/public';
import { DataPublicPluginStart } from '../../../data/public';
import { pieLabelsFunction, pieVisFunction } from '../common';
import { getPieVisRenderer } from './expression_renderers';
import { ExpressionPiePluginSetup, ExpressionPiePluginStart, SetupDeps, StartDeps } from './types';
import {
partitionLabelsFunction,
pieVisFunction,
treemapVisFunction,
mosaicVisFunction,
waffleVisFunction,
} from '../common';
import { getPartitionVisRenderer } from './expression_renderers';
import {
ExpressionPartitionVisPluginSetup,
ExpressionPartitionVisPluginStart,
SetupDeps,
StartDeps,
} from './types';
/** @internal */
export interface VisTypePieDependencies {
@ -30,13 +41,16 @@ export interface VisTypePiePluginStartDependencies {
fieldFormats: FieldFormatsStart;
}
export class ExpressionPiePlugin {
export class ExpressionPartitionVisPlugin {
public setup(
core: CoreSetup<VisTypePiePluginStartDependencies>,
{ expressions, charts }: SetupDeps
): ExpressionPiePluginSetup {
expressions.registerFunction(pieLabelsFunction);
): ExpressionPartitionVisPluginSetup {
expressions.registerFunction(partitionLabelsFunction);
expressions.registerFunction(pieVisFunction);
expressions.registerFunction(treemapVisFunction);
expressions.registerFunction(mosaicVisFunction);
expressions.registerFunction(waffleVisFunction);
const getStartDeps = async () => {
const [coreStart, deps] = await core.getStartServices();
@ -46,11 +60,11 @@ export class ExpressionPiePlugin {
};
expressions.registerRenderer(
getPieVisRenderer({ theme: charts.theme, palettes: charts.palettes, getStartDeps })
getPartitionVisRenderer({ theme: charts.theme, palettes: charts.palettes, getStartDeps })
);
}
public start(core: CoreStart, deps: StartDeps): ExpressionPiePluginStart {}
public start(core: CoreStart, deps: StartDeps): ExpressionPartitionVisPluginStart {}
public stop() {}
}

View file

@ -8,8 +8,8 @@
import { ChartsPluginSetup } from '../../../charts/public';
import { ExpressionsPublicPlugin, ExpressionsServiceStart } from '../../../expressions/public';
export type ExpressionPiePluginSetup = void;
export type ExpressionPiePluginStart = void;
export type ExpressionPartitionVisPluginSetup = void;
export type ExpressionPartitionVisPluginStart = void;
export interface SetupDeps {
expressions: ReturnType<ExpressionsPublicPlugin['setup']>;

View file

@ -0,0 +1,50 @@
/*
* 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 { ExpressionValueVisDimension } from '../../../../visualizations/common';
import { createMockVisData } from '../mocks';
import { getColumnByAccessor } from './accessor';
const visData = createMockVisData();
describe('getColumnByAccessor', () => {
it('returns column by the index', () => {
const index = 1;
const column = getColumnByAccessor(index, visData.columns);
expect(column).toEqual(visData.columns[index]);
});
it('returns undefiend if the index is higher then amount of columns', () => {
const index = visData.columns.length;
const column = getColumnByAccessor(index, visData.columns);
expect(column).toBeUndefined();
});
it('returns column by id', () => {
const column = visData.columns[1];
const accessor: ExpressionValueVisDimension['accessor'] = {
id: column.id,
name: '',
meta: { type: column.meta.type },
};
const foundColumn = getColumnByAccessor(accessor, visData.columns);
expect(foundColumn).toEqual(column);
});
it('returns undefined for the accessor to non-existent column', () => {
const accessor: ExpressionValueVisDimension['accessor'] = {
id: 'non-existent-column',
name: '',
meta: { type: 'number' },
};
const column = getColumnByAccessor(accessor, visData.columns);
expect(column).toBeUndefined();
});
});

View file

@ -0,0 +1,47 @@
/*
* 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 { ChartTypes } from '../../common/types';
import { createMockDonutParams, createMockPieParams } from '../mocks';
import { filterOutConfig } from './filter_out_config';
describe('filterOutConfig', () => {
const config = createMockPieParams();
const { last_level: lastLevel, truncate, ...restLabels } = config.labels;
const configWithoutTruncateAndLastLevel = { ...config, labels: restLabels };
it('returns full configuration for pie visualization', () => {
const fullConfig = filterOutConfig(ChartTypes.PIE, config);
expect(fullConfig).toEqual(config);
});
it('returns full configuration for donut visualization', () => {
const donutConfig = createMockDonutParams();
const fullDonutConfig = filterOutConfig(ChartTypes.DONUT, donutConfig);
expect(fullDonutConfig).toEqual(donutConfig);
});
it('excludes truncate and last_level from labels for treemap', () => {
const filteredOutConfig = filterOutConfig(ChartTypes.TREEMAP, config);
expect(filteredOutConfig).toEqual(configWithoutTruncateAndLastLevel);
});
it('excludes truncate and last_level from labels for mosaic', () => {
const filteredOutConfig = filterOutConfig(ChartTypes.MOSAIC, config);
expect(filteredOutConfig).toEqual(configWithoutTruncateAndLastLevel);
});
it('excludes truncate and last_level from labels for waffle', () => {
const filteredOutConfig = filterOutConfig(ChartTypes.WAFFLE, config);
expect(filteredOutConfig).toEqual(configWithoutTruncateAndLastLevel);
});
});

View file

@ -0,0 +1,22 @@
/*
* 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 { PartitionVisParams, ChartTypes } from '../../common/types';
export const filterOutConfig = (visType: ChartTypes, visConfig: PartitionVisParams) => {
if ([ChartTypes.PIE, ChartTypes.DONUT].includes(visType)) {
return visConfig;
}
const { last_level: lastLevel, truncate, ...restLabelsConfig } = visConfig.labels;
return {
...visConfig,
labels: restLabelsConfig,
};
};

View file

@ -0,0 +1,186 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { fieldFormatsMock } from '../../../../field_formats/common/mocks';
import { Datatable } from '../../../../expressions';
import { createMockPieParams, createMockVisData } from '../mocks';
import { generateFormatters, getAvailableFormatter, getFormatter } from './formatters';
import { BucketColumns } from '../../common/types';
describe('generateFormatters', () => {
const visParams = createMockPieParams();
const visData = createMockVisData();
const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args));
beforeEach(() => {
defaultFormatter.mockClear();
});
it('returns empty object, if labels should not be should ', () => {
const formatters = generateFormatters(
{ ...visParams, labels: { ...visParams.labels, show: false } },
visData,
defaultFormatter
);
expect(formatters).toEqual({});
expect(defaultFormatter).toHaveBeenCalledTimes(0);
});
it('returns formatters, if columns have meta parameters', () => {
const formatters = generateFormatters(visParams, visData, defaultFormatter);
const formattingResult = fieldFormatsMock.deserialize();
const serializedFormatters = Object.keys(formatters).reduce(
(serialized, formatterId) => ({
...serialized,
[formatterId]: formatters[formatterId]?.toJSON(),
}),
{}
);
expect(serializedFormatters).toEqual({
'col-0-2': formattingResult.toJSON(),
'col-1-1': formattingResult.toJSON(),
'col-2-3': formattingResult.toJSON(),
'col-3-1': formattingResult.toJSON(),
});
expect(defaultFormatter).toHaveBeenCalledTimes(visData.columns.length);
visData.columns.forEach((col) => {
expect(defaultFormatter).toHaveBeenCalledWith(col.meta.params);
});
});
it('returns undefined formatters for columns without meta parameters', () => {
const newVisData: Datatable = {
...visData,
columns: visData.columns.map(({ meta, ...col }) => ({ ...col, meta: { type: 'string' } })),
};
const formatters = generateFormatters(visParams, newVisData, defaultFormatter);
expect(formatters).toEqual({
'col-0-2': undefined,
'col-1-1': undefined,
'col-2-3': undefined,
'col-3-1': undefined,
});
expect(defaultFormatter).toHaveBeenCalledTimes(0);
});
});
describe('getAvailableFormatter', () => {
const visData = createMockVisData();
const preparedFormatter1 = jest.fn((...args) => fieldFormatsMock.deserialize(...args));
const preparedFormatter2 = jest.fn((...args) => fieldFormatsMock.deserialize(...args));
const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args));
beforeEach(() => {
defaultFormatter.mockClear();
preparedFormatter1.mockClear();
preparedFormatter2.mockClear();
});
const formatters: Record<string, any> = {
[visData.columns[0].id]: preparedFormatter1(),
[visData.columns[1].id]: preparedFormatter2(),
};
it('returns formatter from formatters, if meta.params are present ', () => {
const formatter = getAvailableFormatter(visData.columns[1], formatters, defaultFormatter);
expect(formatter).toEqual(formatters[visData.columns[1].id]);
expect(defaultFormatter).toHaveBeenCalledTimes(0);
});
it('returns formatter from defaultFormatter factory, if meta.params are not present and format is present at column', () => {
const column: Partial<BucketColumns> = {
...visData.columns[1],
meta: { type: 'string' },
format: {
id: 'string',
params: {},
},
};
const formatter = getAvailableFormatter(column, formatters, defaultFormatter);
expect(formatter).not.toBeNull();
expect(typeof formatter).toBe('object');
expect(defaultFormatter).toHaveBeenCalledTimes(1);
expect(defaultFormatter).toHaveBeenCalledWith(column.format);
});
it('returns undefined, if meta.params and format are not present', () => {
const column: Partial<BucketColumns> = {
...visData.columns[1],
meta: { type: 'string' },
};
const formatter = getAvailableFormatter(column, formatters, defaultFormatter);
expect(formatter).toBeUndefined();
expect(defaultFormatter).toHaveBeenCalledTimes(0);
});
});
describe('getFormatter', () => {
const visData = createMockVisData();
const preparedFormatter1 = jest.fn((...args) => fieldFormatsMock.deserialize(...args));
const preparedFormatter2 = jest.fn((...args) => fieldFormatsMock.deserialize(...args));
const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args));
beforeEach(() => {
defaultFormatter.mockClear();
preparedFormatter1.mockClear();
preparedFormatter2.mockClear();
});
const formatters: Record<string, any> = {
[visData.columns[0].id]: preparedFormatter1(),
[visData.columns[1].id]: preparedFormatter2(),
};
it('returns formatter from formatters, if meta.params are present ', () => {
const formatter = getFormatter(visData.columns[1], formatters, defaultFormatter);
expect(formatter).toEqual(formatters[visData.columns[1].id]);
expect(defaultFormatter).toHaveBeenCalledTimes(0);
});
it('returns formatter from defaultFormatter factory, if meta.params are not present and format is present at column', () => {
const column: Partial<BucketColumns> = {
...visData.columns[1],
meta: { type: 'string' },
format: {
id: 'string',
params: {},
},
};
const formatter = getFormatter(column, formatters, defaultFormatter);
expect(formatter).not.toBeNull();
expect(typeof formatter).toBe('object');
expect(defaultFormatter).toHaveBeenCalledTimes(1);
expect(defaultFormatter).toHaveBeenCalledWith(column.format);
});
it('returns defaultFormatter, if meta.params and format are not present', () => {
const column: Partial<BucketColumns> = {
...visData.columns[1],
meta: { type: 'string' },
};
const formatter = getFormatter(column, formatters, defaultFormatter);
expect(formatter).not.toBeNull();
expect(typeof formatter).toBe('object');
expect(defaultFormatter).toHaveBeenCalledTimes(1);
expect(defaultFormatter).toHaveBeenCalledWith();
});
});

View file

@ -0,0 +1,52 @@
/*
* 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 type { FieldFormat, FormatFactory } from '../../../../field_formats/common';
import type { Datatable } from '../../../../expressions/public';
import { BucketColumns, PartitionVisParams } from '../../common/types';
export const generateFormatters = (
visParams: PartitionVisParams,
visData: Datatable,
formatFactory: FormatFactory
) => {
if (!visParams.labels.show) {
return {};
}
return visData.columns.reduce<Record<string, ReturnType<FormatFactory> | undefined>>(
(newFormatters, column) => ({
...newFormatters,
[column.id]: column?.meta?.params ? formatFactory(column.meta.params) : undefined,
}),
{}
);
};
export const getAvailableFormatter = (
column: Partial<BucketColumns>,
formatters: Record<string, FieldFormat | undefined>,
defaultFormatFactory: FormatFactory
) => {
if (column?.meta?.params) {
const formatter = column?.id ? formatters[column?.id] : undefined;
if (formatter) {
return formatter;
}
}
if (column?.format) {
return defaultFormatFactory(column.format);
}
};
export const getFormatter = (
column: Partial<BucketColumns>,
formatters: Record<string, FieldFormat | undefined>,
defaultFormatFactory: FormatFactory
) => getAvailableFormatter(column, formatters, defaultFormatFactory) ?? defaultFormatFactory();

View file

@ -7,7 +7,12 @@
*/
import { getColumns } from './get_columns';
import { PieVisParams } from '../../common/types';
import {
LabelPositions,
LegendDisplay,
PartitionVisParams,
ValueFormats,
} from '../../common/types';
import { createMockPieParams, createMockVisData } from '../mocks';
const visParams = createMockPieParams();
@ -108,7 +113,16 @@ describe('getColumns', () => {
});
it('should return the correct metric column if visParams returns dimensions', () => {
const { metricColumn } = getColumns(visParams, visData);
const { metricColumn } = getColumns(
{
...visParams,
dimensions: {
...visParams.dimensions,
metric: undefined,
},
},
visData
);
expect(metricColumn).toEqual({
id: 'col-3-1',
meta: {
@ -130,39 +144,38 @@ describe('getColumns', () => {
});
it('should return the first data column if no buckets specified', () => {
const visParamsOnlyMetric = {
addLegend: true,
const visParamsOnlyMetric: PartitionVisParams = {
legendDisplay: LegendDisplay.SHOW,
addTooltip: true,
isDonut: true,
labels: {
position: 'default',
position: LabelPositions.DEFAULT,
show: true,
truncate: 100,
values: true,
valuesFormat: 'percent',
valuesFormat: ValueFormats.PERCENT,
percentDecimals: 2,
last_level: false,
},
legendPosition: 'right',
nestedLegend: false,
maxLegendLines: 1,
truncateLegend: false,
distinctColors: false,
palette: {
name: 'default',
type: 'palette',
},
type: 'pie',
dimensions: {
metric: {
type: 'vis_dimension',
accessor: 1,
format: {
id: 'number',
params: {},
},
params: {},
label: 'Count',
aggType: 'count',
},
},
} as unknown as PieVisParams;
};
const { metricColumn } = getColumns(visParamsOnlyMetric, visData);
expect(metricColumn).toEqual({
id: 'col-1-1',
@ -187,37 +200,39 @@ describe('getColumns', () => {
});
it('should return an object with the name of the metric if no buckets specified', () => {
const visParamsOnlyMetric = {
addLegend: true,
const visParamsOnlyMetric: PartitionVisParams = {
legendDisplay: LegendDisplay.SHOW,
addTooltip: true,
isDonut: true,
labels: {
position: 'default',
position: LabelPositions.DEFAULT,
show: true,
truncate: 100,
values: true,
valuesFormat: 'percent',
valuesFormat: ValueFormats.PERCENT,
percentDecimals: 2,
last_level: false,
},
truncateLegend: false,
maxLegendLines: 100,
distinctColors: false,
legendPosition: 'right',
nestedLegend: false,
palette: {
name: 'default',
type: 'palette',
},
type: 'pie',
dimensions: {
metric: {
type: 'vis_dimension',
accessor: 1,
format: {
id: 'number',
params: {},
},
params: {},
label: 'Count',
aggType: 'count',
},
},
} as unknown as PieVisParams;
};
const { bucketColumns, metricColumn } = getColumns(visParamsOnlyMetric, visData);
expect(bucketColumns).toEqual([{ name: metricColumn.name }]);
});

View file

@ -6,39 +6,43 @@
* Side Public License, v 1.
*/
import { getColumnByAccessor } from './accessor';
import { ExpressionValueVisDimension } from '../../../../visualizations/common';
import { DatatableColumn, Datatable } from '../../../../expressions/public';
import { BucketColumns, PieVisParams } from '../../common/types';
import { BucketColumns, PartitionVisParams } from '../../common/types';
import { getColumnByAccessor } from './accessor';
const getMetricColumn = (
metricAccessor: ExpressionValueVisDimension['accessor'],
visData: Datatable
) => {
return getColumnByAccessor(metricAccessor, visData.columns);
};
export const getColumns = (
visParams: PieVisParams,
visParams: PartitionVisParams,
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 }) => ({
...getColumnByAccessor(accessor, visData.columns),
format,
})
);
const { metric, buckets } = visParams.dimensions;
if (buckets && buckets.length > 0) {
const bucketColumns: Array<Partial<BucketColumns>> = buckets.map(({ accessor, format }) => ({
...getColumnByAccessor(accessor, visData.columns),
format,
}));
const lastBucketId = bucketColumns[bucketColumns.length - 1].id;
const matchingIndex = visData.columns.findIndex((col) => col.id === lastBucketId);
return {
bucketColumns,
metricColumn: visData.columns[matchingIndex + 1],
metricColumn: getMetricColumn(metric?.accessor ?? matchingIndex + 1, visData),
};
}
const metricAccessor = visParams?.dimensions?.metric.accessor ?? 0;
const metricColumn = getColumnByAccessor(metricAccessor, visData.columns);
const metricColumn = getMetricColumn(metric?.accessor ?? 0, visData);
return {
metricColumn,
bucketColumns: [
{
name: metricColumn.name,
},
],
bucketColumns: [{ name: metricColumn.name }],
};
};

View file

@ -8,7 +8,15 @@
import { DatatableRow } from '../../../../expressions/public';
import { BucketColumns } from '../../common/types';
export const getDistinctSeries = (rows: DatatableRow[], buckets: Array<Partial<BucketColumns>>) => {
export interface DistinctSeries {
allSeries: string[];
parentSeries: string[];
}
export const getDistinctSeries = (
rows: DatatableRow[],
buckets: Array<Partial<BucketColumns>>
): DistinctSeries => {
const parentBucketId = buckets[0].id;
const parentSeries: string[] = [];
const allSeries: string[] = [];
@ -24,8 +32,5 @@ export const getDistinctSeries = (rows: DatatableRow[], buckets: Array<Partial<B
}
});
});
return {
allSeries,
parentSeries,
};
return { allSeries, parentSeries };
};

View file

@ -12,7 +12,7 @@ import { i18n } from '@kbn/i18n';
import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui';
import { LegendAction, SeriesIdentifier, useLegendAction } from '@elastic/charts';
import { DataPublicPluginStart } from '../../../../data/public';
import { PieVisParams } from '../../common/types';
import { PartitionVisParams } from '../../common/types';
import { ClickTriggerEvent } from '../../../../charts/public';
import { FieldFormatsStart } from '../../../../field_formats/public';
@ -23,7 +23,7 @@ export const getLegendActions = (
) => Promise<boolean>,
getFilterEventData: (series: SeriesIdentifier) => ClickTriggerEvent | null,
onFilter: (data: ClickTriggerEvent, negate?: any) => void,
visParams: PieVisParams,
visParams: PartitionVisParams,
actions: DataPublicPluginStart['actions'],
formatter: FieldFormatsStart
): LegendAction => {
@ -56,7 +56,7 @@ export const getLegendActions = (
title: `${title}`,
items: [
{
name: i18n.translate('expressionPie.legend.filterForValueButtonAriaLabel', {
name: i18n.translate('expressionPartitionVis.legend.filterForValueButtonAriaLabel', {
defaultMessage: 'Filter for value',
}),
'data-test-subj': `legend-${title}-filterIn`,
@ -67,7 +67,7 @@ export const getLegendActions = (
},
},
{
name: i18n.translate('expressionPie.legend.filterOutValueButtonAriaLabel', {
name: i18n.translate('expressionPartitionVis.legend.filterOutValueButtonAriaLabel', {
defaultMessage: 'Filter out value',
}),
'data-test-subj': `legend-${title}-filterOut`,
@ -114,7 +114,7 @@ export const getLegendActions = (
}}
panelPaddingSize="none"
anchorPosition="upLeft"
title={i18n.translate('expressionPie.legend.filterOptionsLegend', {
title={i18n.translate('expressionPartitionVis.legend.filterOptionsLegend', {
defaultMessage: '{legendDataLabel}, filter options',
values: { legendDataLabel: title },
})}

View file

@ -0,0 +1,496 @@
/*
* 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 { ExpressionValueVisDimension } from '../../../../visualizations/common';
import { getPartitionTheme } from './get_partition_theme';
import { createMockPieParams, createMockDonutParams, createMockPartitionVisParams } from '../mocks';
import { ChartTypes, LabelPositions, PartitionVisParams } from '../../common/types';
import { RecursivePartial } from '@elastic/eui';
import { Theme } from '@elastic/charts';
const column: ExpressionValueVisDimension = {
type: 'vis_dimension',
accessor: { id: 'col-1-1', name: 'Count', meta: { type: 'number' } },
format: {
id: 'terms',
params: {
id: 'string',
otherBucketLabel: 'Other',
missingBucketLabel: 'Missing',
},
},
};
const splitRows = [column];
const splitColumns = [column];
const chartTheme: RecursivePartial<Theme> = {
barSeriesStyle: { displayValue: { fontFamily: 'Arial' } },
lineSeriesStyle: { point: { fill: '#fff' } },
axes: { axisTitle: { fill: '#000' } },
};
const linkLabelWithEnoughSpace = (visParams: PartitionVisParams) => ({
maxCount: Number.POSITIVE_INFINITY,
maximumSection: Number.POSITIVE_INFINITY,
maxTextLength: visParams.labels.truncate ?? undefined,
});
const linkLabelsWithoutSpaceForOuterLabels = { maxCount: 0 };
const linkLabelsWithoutSpaceForLabels = {
maxCount: 0,
maximumSection: Number.POSITIVE_INFINITY,
};
const getStaticThemePartition = (
theme: RecursivePartial<Theme>,
visParams: PartitionVisParams
) => ({
fontFamily: theme.barSeriesStyle?.displayValue?.fontFamily,
outerSizeRatio: 1,
minFontSize: 10,
maxFontSize: 16,
emptySizeRatio: visParams.emptySizeRatio ?? 0,
sectorLineStroke: theme.lineSeriesStyle?.point?.fill,
sectorLineWidth: 1.5,
circlePadding: 4,
});
const getStaticThemeOptions = (theme: RecursivePartial<Theme>, visParams: PartitionVisParams) => ({
partition: getStaticThemePartition(theme, visParams),
chartMargins: { top: 0, left: 0, bottom: 0, right: 0 },
});
const getDefaultLinkLabel = (visParams: PartitionVisParams, theme: RecursivePartial<Theme>) => ({
maxCount: 5,
fontSize: 11,
textColor: theme.axes?.axisTitle?.fill,
maxTextLength: visParams.labels.truncate ?? undefined,
});
const dimensions = undefined;
const runPieDonutWaffleTestSuites = (chartType: ChartTypes, visParams: PartitionVisParams) => {
const vParamsSplitRows = {
...visParams,
dimensions: { ...visParams.dimensions, splitRow: splitRows },
};
const vParamsSplitColumns = {
...visParams,
dimensions: { ...visParams.dimensions, splitColumn: splitColumns },
};
it('should return correct default theme options', () => {
const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
partition: {
...getStaticThemePartition(chartTheme, visParams),
outerSizeRatio: undefined,
linkLabel: getDefaultLinkLabel(visParams, chartTheme),
},
});
});
it('should not return padding settings if dimensions are not specified', () => {
const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
partition: {
...getStaticThemePartition(chartTheme, visParams),
outerSizeRatio: undefined,
linkLabel: getDefaultLinkLabel(visParams, chartTheme),
},
});
});
it('should not return padding settings if split column or row are specified', () => {
const themeForSplitColumns = getPartitionTheme(
chartType,
vParamsSplitColumns,
chartTheme,
dimensions
);
expect(themeForSplitColumns).toEqual({
...getStaticThemeOptions(chartTheme, vParamsSplitColumns),
partition: {
...getStaticThemePartition(chartTheme, vParamsSplitColumns),
outerSizeRatio: undefined,
linkLabel: linkLabelsWithoutSpaceForOuterLabels,
},
});
const themeForSplitRows = getPartitionTheme(
chartType,
vParamsSplitRows,
chartTheme,
dimensions
);
expect(themeForSplitRows).toEqual({
...getStaticThemeOptions(chartTheme, vParamsSplitRows),
partition: {
...getStaticThemePartition(chartTheme, vParamsSplitRows),
outerSizeRatio: undefined,
linkLabel: linkLabelsWithoutSpaceForOuterLabels,
},
});
});
it('should return adjusted padding settings if dimensions are specified', () => {
const specifiedDimensions = { width: 2000, height: 2000 };
const theme = getPartitionTheme(chartType, visParams, chartTheme, specifiedDimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 },
partition: {
...getStaticThemePartition(chartTheme, visParams),
linkLabel: getDefaultLinkLabel(visParams, chartTheme),
},
});
});
it('should return right settings for the theme related fields', () => {
const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
partition: {
...getStaticThemePartition(chartTheme, visParams),
outerSizeRatio: undefined,
linkLabel: getDefaultLinkLabel(visParams, chartTheme),
},
});
});
it('should return undefined outerSizeRatio for split chart and show labels', () => {
const specifiedDimensions = { width: 2000, height: 2000 };
const theme = getPartitionTheme(chartType, vParamsSplitRows, chartTheme, specifiedDimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, vParamsSplitRows),
partition: {
...getStaticThemePartition(chartTheme, vParamsSplitRows),
outerSizeRatio: undefined,
linkLabel: linkLabelsWithoutSpaceForOuterLabels,
},
});
const themeForSplitColumns = getPartitionTheme(
chartType,
vParamsSplitColumns,
chartTheme,
specifiedDimensions
);
expect(themeForSplitColumns).toEqual({
...getStaticThemeOptions(chartTheme, vParamsSplitColumns),
partition: {
...getStaticThemePartition(chartTheme, vParamsSplitColumns),
outerSizeRatio: undefined,
linkLabel: linkLabelsWithoutSpaceForOuterLabels,
},
});
});
it(
'should return undefined outerSizeRatio for not specified dimensions, visible labels,' +
'and default labels position and not split chart',
() => {
const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
partition: {
...getStaticThemePartition(chartTheme, visParams),
outerSizeRatio: undefined,
linkLabel: getDefaultLinkLabel(visParams, chartTheme),
},
});
}
);
it(
'should return rescaleFactor value for outerSizeRatio if dimensions are specified,' +
' is not split chart, labels are shown and labels position is not `inside`',
() => {
const specifiedDimensions = { width: 2000, height: 2000 };
const rescaleFactor = 2;
const theme = getPartitionTheme(
chartType,
visParams,
chartTheme,
specifiedDimensions,
rescaleFactor
);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 },
partition: {
...getStaticThemePartition(chartTheme, visParams),
outerSizeRatio: rescaleFactor,
linkLabel: getDefaultLinkLabel(visParams, chartTheme),
},
});
}
);
it(
'should return adjusted rescaleFactor for outerSizeRatio if dimensions are specified,' +
' is not split chart, labels position is `inside` and labels are shown',
() => {
const specifiedDimensions = { width: 2000, height: 2000 };
const rescaleFactor = 1;
const vParams = {
...visParams,
labels: { ...visParams.labels, position: LabelPositions.INSIDE },
};
const theme = getPartitionTheme(
chartType,
vParams,
chartTheme,
specifiedDimensions,
rescaleFactor
);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, vParams),
chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 },
partition: {
...getStaticThemePartition(chartTheme, vParams),
outerSizeRatio: 0.5,
linkLabel: linkLabelsWithoutSpaceForOuterLabels,
},
});
}
);
it(
'should return linkLabel with enough space if labels are shown,' +
' labels position is `default` and need to show the last level only.',
() => {
const specifiedDimensions = { width: 2000, height: 2000 };
const vParams = {
...visParams,
labels: { ...visParams.labels, last_level: true },
};
const theme = getPartitionTheme(chartType, vParams, chartTheme, specifiedDimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, vParams),
chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 },
partition: {
...getStaticThemePartition(chartTheme, vParams),
linkLabel: linkLabelWithEnoughSpace(vParams),
},
});
}
);
it('should hide links if position is `inside` or is split chart, and labels are shown', () => {
const vParams = {
...visParams,
labels: { ...visParams.labels, position: LabelPositions.INSIDE },
};
const theme = getPartitionTheme(chartType, vParams, chartTheme, dimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, vParams),
partition: {
...getStaticThemePartition(chartTheme, vParams),
outerSizeRatio: undefined,
linkLabel: linkLabelsWithoutSpaceForOuterLabels,
},
});
const themeSplitColumns = getPartitionTheme(
chartType,
vParamsSplitColumns,
chartTheme,
dimensions
);
expect(themeSplitColumns).toEqual({
...getStaticThemeOptions(chartTheme, vParamsSplitColumns),
partition: {
...getStaticThemePartition(chartTheme, vParamsSplitColumns),
outerSizeRatio: undefined,
linkLabel: linkLabelsWithoutSpaceForOuterLabels,
},
});
const themeSplitRows = getPartitionTheme(chartType, vParamsSplitRows, chartTheme, dimensions);
expect(themeSplitRows).toEqual({
...getStaticThemeOptions(chartTheme, vParamsSplitRows),
partition: {
...getStaticThemePartition(chartTheme, vParamsSplitRows),
outerSizeRatio: undefined,
linkLabel: linkLabelsWithoutSpaceForOuterLabels,
},
});
});
it('should hide links if labels are not shown', () => {
const vParams = { ...visParams, labels: { ...visParams.labels, show: false } };
const theme = getPartitionTheme(chartType, vParams, chartTheme, dimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, vParams),
partition: {
...getStaticThemePartition(chartTheme, vParams),
outerSizeRatio: undefined,
linkLabel: linkLabelsWithoutSpaceForLabels,
},
});
});
};
const runTreemapMosaicTestSuites = (chartType: ChartTypes, visParams: PartitionVisParams) => {
const vParamsSplitRows = {
...visParams,
dimensions: { ...visParams.dimensions, splitRow: splitRows },
};
const vParamsSplitColumns = {
...visParams,
dimensions: { ...visParams.dimensions, splitColumn: splitColumns },
};
it('should return correct theme options', () => {
const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
partition: {
...getStaticThemePartition(chartTheme, visParams),
linkLabel: getDefaultLinkLabel(visParams, chartTheme),
},
});
});
it('should return empty padding settings if dimensions are not specified', () => {
const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
partition: {
...getStaticThemePartition(chartTheme, visParams),
linkLabel: getDefaultLinkLabel(visParams, chartTheme),
},
});
});
it('should return padding settings if split column or row are specified', () => {
const themeForSplitColumns = getPartitionTheme(
chartType,
vParamsSplitColumns,
chartTheme,
dimensions
);
expect(themeForSplitColumns).toEqual({
...getStaticThemeOptions(chartTheme, vParamsSplitColumns),
partition: {
...getStaticThemePartition(chartTheme, vParamsSplitColumns),
linkLabel: getDefaultLinkLabel(vParamsSplitColumns, chartTheme),
},
});
const themeForSplitRows = getPartitionTheme(
chartType,
vParamsSplitRows,
chartTheme,
dimensions
);
expect(themeForSplitRows).toEqual({
...getStaticThemeOptions(chartTheme, vParamsSplitRows),
partition: {
...getStaticThemePartition(chartTheme, vParamsSplitRows),
linkLabel: getDefaultLinkLabel(vParamsSplitRows, chartTheme),
},
});
});
it('should return fullfilled padding settings if dimensions are specified', () => {
const specifiedDimensions = { width: 2000, height: 2000 };
const theme = getPartitionTheme(chartType, visParams, chartTheme, specifiedDimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
chartPaddings: { top: 500, bottom: 500, left: 500, right: 500 },
partition: {
...getStaticThemePartition(chartTheme, visParams),
linkLabel: getDefaultLinkLabel(visParams, chartTheme),
},
});
});
it('should return settings for the theme related fields', () => {
const theme = getPartitionTheme(chartType, visParams, chartTheme, dimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
partition: {
...getStaticThemePartition(chartTheme, visParams),
linkLabel: getDefaultLinkLabel(visParams, chartTheme),
},
});
});
it('should make color transparent if labels are hidden', () => {
const vParams = { ...visParams, labels: { ...visParams.labels, show: false } };
const theme = getPartitionTheme(chartType, vParams, chartTheme, dimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, vParams),
partition: {
...getStaticThemePartition(chartTheme, vParams),
linkLabel: getDefaultLinkLabel(visParams, chartTheme),
fillLabel: { textColor: 'rgba(0,0,0,0)' },
},
});
});
};
describe('Pie getPartitionTheme', () => {
runPieDonutWaffleTestSuites(ChartTypes.PIE, createMockPieParams());
});
describe('Donut getPartitionTheme', () => {
const visParams = createMockDonutParams();
const chartType = ChartTypes.DONUT;
runPieDonutWaffleTestSuites(chartType, visParams);
it('should return correct empty size ratio and partitionLayout', () => {
const theme = getPartitionTheme(ChartTypes.DONUT, visParams, chartTheme, dimensions);
expect(theme).toEqual({
...getStaticThemeOptions(chartTheme, visParams),
outerSizeRatio: undefined,
partition: {
...getStaticThemePartition(chartTheme, visParams),
linkLabel: getDefaultLinkLabel(visParams, chartTheme),
outerSizeRatio: undefined,
},
});
});
});
describe('Waffle getPartitionTheme', () => {
runPieDonutWaffleTestSuites(ChartTypes.WAFFLE, createMockPartitionVisParams());
});
describe('Mosaic getPartitionTheme', () => {
runTreemapMosaicTestSuites(ChartTypes.MOSAIC, createMockPartitionVisParams());
});
describe('Treemap getPartitionTheme', () => {
runTreemapMosaicTestSuites(ChartTypes.TREEMAP, createMockPartitionVisParams());
});

View file

@ -0,0 +1,165 @@
/*
* 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 { RecursivePartial, Theme, PartialTheme } from '@elastic/charts';
import {
ChartTypes,
LabelPositions,
PartitionVisParams,
PieContainerDimensions,
} from '../../common/types';
type GetThemeByTypeFn = (
chartType: ChartTypes,
visParams: PartitionVisParams,
dimensions?: PieContainerDimensions,
rescaleFactor?: number
) => PartialTheme;
type GetThemeFn = (
chartType: ChartTypes,
visParams: PartitionVisParams,
chartTheme: RecursivePartial<Theme>,
dimensions?: PieContainerDimensions,
rescaleFactor?: number
) => PartialTheme;
type GetPieDonutWaffleThemeFn = (
visParams: PartitionVisParams,
dimensions?: PieContainerDimensions,
rescaleFactor?: number
) => PartialTheme;
type GetTreemapMosaicThemeFn = (visParams: PartitionVisParams) => PartialTheme;
const MAX_SIZE = 1000;
const getPieDonutWaffleCommonTheme: GetPieDonutWaffleThemeFn = (
visParams,
dimensions,
rescaleFactor = 1
) => {
const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow);
const preventLinksFromShowing =
(visParams.labels.position === LabelPositions.INSIDE || isSplitChart) && visParams.labels.show;
const usingOuterSizeRatio =
dimensions && !isSplitChart
? {
outerSizeRatio:
// Cap the ratio to 1 and then rescale
rescaleFactor * Math.min(MAX_SIZE / Math.min(dimensions?.width, dimensions?.height), 1),
}
: { outerSizeRatio: undefined };
const theme: PartialTheme = {};
theme.partition = { ...(usingOuterSizeRatio ?? {}) };
if (
visParams.labels.show &&
visParams.labels.position === LabelPositions.DEFAULT &&
visParams.labels.last_level
) {
theme.partition.linkLabel = {
maxCount: Number.POSITIVE_INFINITY,
maximumSection: Number.POSITIVE_INFINITY,
maxTextLength: visParams.labels.truncate ?? undefined,
};
}
if (preventLinksFromShowing || !visParams.labels.show) {
// Prevent links from showing
theme.partition.linkLabel = {
maxCount: 0,
...(!visParams.labels.show ? { maximumSection: Number.POSITIVE_INFINITY } : {}),
};
}
if (!preventLinksFromShowing && dimensions && !isSplitChart) {
// shrink up to 20% to give some room for the linked values
theme.partition.outerSizeRatio = rescaleFactor;
}
return theme;
};
const getDonutSpecificTheme: GetPieDonutWaffleThemeFn = (visParams, ...args) => {
const { partition, ...restTheme } = getPieDonutWaffleCommonTheme(visParams, ...args);
return { ...restTheme, partition: { ...partition, emptySizeRatio: visParams.emptySizeRatio } };
};
const getTreemapMosaicCommonTheme: GetTreemapMosaicThemeFn = (visParams) => {
if (!visParams.labels.show) {
return {
partition: {
fillLabel: { textColor: 'rgba(0,0,0,0)' },
},
};
}
return {};
};
const getSpecificTheme: GetThemeByTypeFn = (chartType, visParams, dimensions, rescaleFactor) =>
({
[ChartTypes.PIE]: () => getPieDonutWaffleCommonTheme(visParams, dimensions, rescaleFactor),
[ChartTypes.DONUT]: () => getDonutSpecificTheme(visParams, dimensions, rescaleFactor),
[ChartTypes.TREEMAP]: () => getTreemapMosaicCommonTheme(visParams),
[ChartTypes.MOSAIC]: () => getTreemapMosaicCommonTheme(visParams),
[ChartTypes.WAFFLE]: () => getPieDonutWaffleCommonTheme(visParams, dimensions, rescaleFactor),
}[chartType]());
export const getPartitionTheme: GetThemeFn = (
chartType,
visParams,
chartTheme,
dimensions,
rescaleFactor = 1
) => {
// On small multiples we want the labels to only appear inside
const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow);
const paddingProps: PartialTheme | null =
dimensions && !isSplitChart
? {
chartPaddings: {
top: ((1 - Math.min(1, MAX_SIZE / dimensions?.height)) / 2) * dimensions?.height,
bottom: ((1 - Math.min(1, MAX_SIZE / dimensions?.height)) / 2) * dimensions?.height,
left: ((1 - Math.min(1, MAX_SIZE / dimensions?.width)) / 2) * dimensions?.height,
right: ((1 - Math.min(1, MAX_SIZE / dimensions?.width)) / 2) * dimensions?.height,
},
}
: null;
const partition = {
fontFamily: chartTheme.barSeriesStyle?.displayValue?.fontFamily,
outerSizeRatio: 1,
minFontSize: 10,
maxFontSize: 16,
emptySizeRatio: 0,
sectorLineStroke: chartTheme.lineSeriesStyle?.point?.fill,
sectorLineWidth: 1.5,
circlePadding: 4,
linkLabel: {
maxCount: 5,
fontSize: 11,
textColor: chartTheme.axes?.axisTitle?.fill,
maxTextLength: visParams.labels.truncate ?? undefined,
},
};
const { partition: specificPartition = {}, ...restSpecificTheme } = getSpecificTheme(
chartType,
visParams,
dimensions,
rescaleFactor
);
return {
partition: { ...partition, ...specificPartition },
chartMargins: { top: 0, bottom: 0, left: 0, right: 0 },
...(paddingProps ?? {}),
...restSpecificTheme,
};
};

View file

@ -0,0 +1,19 @@
/*
* 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 { PartitionLayout } from '@elastic/charts';
import { ChartTypes } from '../../common/types';
export const getPartitionType = (chartType: ChartTypes) =>
({
[ChartTypes.PIE]: PartitionLayout.sunburst,
[ChartTypes.DONUT]: PartitionLayout.sunburst,
[ChartTypes.TREEMAP]: PartitionLayout.treemap,
[ChartTypes.MOSAIC]: PartitionLayout.mosaic,
[ChartTypes.WAFFLE]: PartitionLayout.waffle,
}[chartType]);

View file

@ -0,0 +1,166 @@
/*
* 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 { fieldFormatsMock } from '../../../../field_formats/common/mocks';
import { DatatableColumn } from '../../../../expressions';
import { createMockVisData } from '../mocks';
import { getSplitDimensionAccessor } from './get_split_dimension_accessor';
import { BucketColumns } from '../../common/types';
import { ExpressionValueVisDimension } from '../../../../visualizations/common';
describe('getSplitDimensionAccessor', () => {
const visData = createMockVisData();
const preparedFormatter1 = jest.fn((...args) => fieldFormatsMock.deserialize(...args));
const preparedFormatter2 = jest.fn((...args) => fieldFormatsMock.deserialize(...args));
const defaultFormatter = jest.fn((...args) => fieldFormatsMock.deserialize(...args));
beforeEach(() => {
defaultFormatter.mockClear();
preparedFormatter1.mockClear();
preparedFormatter2.mockClear();
});
const formatters: Record<string, any> = {
[visData.columns[0].id]: preparedFormatter1(),
[visData.columns[1].id]: preparedFormatter2(),
};
const splitDimension: ExpressionValueVisDimension = {
type: 'vis_dimension',
accessor: {
id: visData.columns[1].id,
name: visData.columns[1].name,
meta: visData.columns[1].meta,
},
format: {
params: {},
},
};
it('returns accessor which is using formatter from formatters, if meta.params are present at accessing column', () => {
const accessor = getSplitDimensionAccessor(
visData.columns,
splitDimension,
formatters,
defaultFormatter
);
const formatter = formatters[visData.columns[1].id];
const spyOnFormatterConvert = jest.spyOn(formatter, 'convert');
expect(defaultFormatter).toHaveBeenCalledTimes(0);
expect(typeof accessor).toBe('function');
accessor(visData.rows[0]);
expect(spyOnFormatterConvert).toHaveBeenCalledTimes(1);
});
it('returns accessor which is using default formatter, if meta.params are not present and format is present at accessing column', () => {
const column: Partial<BucketColumns> = {
...visData.columns[1],
meta: { type: 'string' },
format: {
id: 'string',
params: {},
},
};
const columns = [visData.columns[0], column, visData.columns[2]] as DatatableColumn[];
const defaultFormatterReturnedVal = fieldFormatsMock.deserialize();
const spyOnDefaultFormatterConvert = jest.spyOn(defaultFormatterReturnedVal, 'convert');
defaultFormatter.mockReturnValueOnce(defaultFormatterReturnedVal);
const accessor = getSplitDimensionAccessor(
columns,
splitDimension,
formatters,
defaultFormatter
);
expect(defaultFormatter).toHaveBeenCalledTimes(1);
expect(defaultFormatter).toHaveBeenCalledWith(column.format);
expect(typeof accessor).toBe('function');
accessor(visData.rows[0]);
expect(spyOnDefaultFormatterConvert).toHaveBeenCalledTimes(1);
});
it('returns accessor which is using default formatter, if meta.params and format are not present', () => {
const column: Partial<BucketColumns> = {
...visData.columns[1],
meta: { type: 'string' },
};
const columns = [visData.columns[0], column, visData.columns[2]] as DatatableColumn[];
const defaultFormatterReturnedVal = fieldFormatsMock.deserialize();
const spyOnDefaultFormatterConvert = jest.spyOn(defaultFormatterReturnedVal, 'convert');
defaultFormatter.mockReturnValueOnce(defaultFormatterReturnedVal);
const accessor = getSplitDimensionAccessor(
columns,
splitDimension,
formatters,
defaultFormatter
);
expect(defaultFormatter).toHaveBeenCalledTimes(1);
expect(defaultFormatter).toHaveBeenCalledWith();
expect(typeof accessor).toBe('function');
accessor(visData.rows[0]);
expect(spyOnDefaultFormatterConvert).toHaveBeenCalledTimes(1);
});
it('returns accessor which returns undefined, if such column is not present', () => {
const accessor1 = getSplitDimensionAccessor(
visData.columns,
splitDimension,
formatters,
defaultFormatter
);
expect(typeof accessor1).toBe('function');
const result1 = accessor1({});
expect(result1).toBeUndefined();
const column2: Partial<BucketColumns> = {
...visData.columns[1],
meta: { type: 'string' },
};
const columns2 = [visData.columns[0], column2, visData.columns[2]] as DatatableColumn[];
const accessor2 = getSplitDimensionAccessor(
columns2,
splitDimension,
formatters,
defaultFormatter
);
expect(typeof accessor2).toBe('function');
const result2 = accessor1({});
expect(result2).toBeUndefined();
const column3: Partial<BucketColumns> = {
...visData.columns[1],
meta: { type: 'string' },
format: {
id: 'string',
params: {},
},
};
const columns3 = [visData.columns[0], column3, visData.columns[2]] as DatatableColumn[];
const accessor3 = getSplitDimensionAccessor(
columns3,
splitDimension,
formatters,
defaultFormatter
);
expect(typeof accessor3).toBe('function');
const result3 = accessor3({});
expect(result3).toBeUndefined();
});
});

View file

@ -0,0 +1,36 @@
/*
* 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 { getColumnByAccessor } from './accessor';
import { DatatableColumn } from '../../../../expressions/public';
import { FieldFormat, FormatFactory } from '../../../../field_formats/common';
import { ExpressionValueVisDimension } from '../../../../visualizations/common';
import { getFormatter } from './formatters';
export const getSplitDimensionAccessor = (
columns: DatatableColumn[],
splitDimension: ExpressionValueVisDimension,
formatters: Record<string, FieldFormat | undefined>,
defaultFormatFactory: FormatFactory
): AccessorFn => {
const splitChartColumn = getColumnByAccessor(splitDimension.accessor, columns);
const accessor = splitChartColumn.id;
const formatter = getFormatter(splitChartColumn, formatters, defaultFormatFactory);
const fn: AccessorFn = (d) => {
const v = d[accessor];
if (v === undefined) {
return;
}
const f = formatter.convert(v);
return f;
};
return fn;
};

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
export { getLayers } from './get_layers';
export { getLayers } from './layers';
export { LegendColorPickerWrapper, LegendColorPickerWrapperContext } from './get_color_picker';
export { getLegendActions } from './get_legend_actions';
export { canFilter, getFilterClickData, getFilterEventData } from './filter_helpers';
@ -15,3 +15,6 @@ export { getColumns } from './get_columns';
export { getSplitDimensionAccessor } from './get_split_dimension_accessor';
export { getDistinctSeries } from './get_distinct_series';
export { getColumnByAccessor } from './accessor';
export { isLegendFlat, shouldShowLegend } from './legend';
export { generateFormatters, getAvailableFormatter, getFormatter } from './formatters';
export { getPartitionType } from './get_partition_type';

View file

@ -0,0 +1,237 @@
/*
* 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 { isEqual } from 'lodash';
import type { FieldFormatsStart } from '../../../../../field_formats/public';
import {
SeriesLayer,
PaletteRegistry,
lightenColor,
PaletteDefinition,
PaletteOutput,
} from '../../../../../charts/public';
import type { Datatable, DatatableRow } from '../../../../../expressions/public';
import { BucketColumns, ChartTypes, PartitionVisParams } from '../../../common/types';
import { DistinctSeries, getDistinctSeries } from '../get_distinct_series';
const isTreemapOrMosaicChart = (shape: ChartTypes) =>
[ChartTypes.MOSAIC, ChartTypes.TREEMAP].includes(shape);
export const byDataColorPaletteMap = (
rows: Datatable['rows'],
columnId: string,
paletteDefinition: PaletteDefinition,
{ params }: PaletteOutput
) => {
const colorMap = new Map<string, string | undefined>(
rows.map((item) => [String(item[columnId]), undefined])
);
let rankAtDepth = 0;
return {
getColor: (item: unknown) => {
const key = String(item);
if (!colorMap.has(key)) return;
let color = colorMap.get(key);
if (color) {
return color;
}
color =
paletteDefinition.getCategoricalColor(
[
{
name: key,
totalSeriesAtDepth: colorMap.size,
rankAtDepth: rankAtDepth++,
},
],
{ behindText: false },
params
) || undefined;
colorMap.set(key, color);
return color;
},
};
};
const getDistinctColor = (
d: ShapeTreeNode,
isSplitChart: boolean,
overwriteColors: { [key: string]: string } = {},
visParams: PartitionVisParams,
palettes: PaletteRegistry | null,
syncColors: boolean,
{ parentSeries, allSeries }: DistinctSeries,
name: string
) => {
let overwriteColor;
// this is for supporting old visualizations (created by vislib plugin)
// it seems that there for some aggs, the uiState saved from vislib is
// different than the es-charts handle it
if (overwriteColors.hasOwnProperty(name)) {
overwriteColor = overwriteColors[name];
}
if (Object.keys(overwriteColors).includes(d.dataName.toString())) {
overwriteColor = overwriteColors[d.dataName];
}
if (overwriteColor) {
return overwriteColor;
}
const index = allSeries.findIndex((dataName) => isEqual(dataName, d.dataName));
const isSplitParentLayer = isSplitChart && parentSeries.includes(d.dataName);
return palettes?.get(visParams.palette.name).getCategoricalColor(
[
{
name: d.dataName,
rankAtDepth: isSplitParentLayer
? parentSeries.findIndex((dataName) => dataName === d.dataName)
: index > -1
? index
: 0,
totalSeriesAtDepth: isSplitParentLayer ? parentSeries.length : allSeries.length || 1,
},
],
{
maxDepth: 1,
totalSeries: allSeries.length || 1,
behindText: visParams.labels.show,
syncColors,
},
visParams.palette?.params ?? { colors: [] }
);
};
const createSeriesLayers = (
d: ShapeTreeNode,
parentSeries: DistinctSeries['parentSeries'],
isSplitChart: boolean
) => {
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;
}
return seriesLayers;
};
const overrideColorForOldVisualization = (
seriesLayers: SeriesLayer[],
overwriteColors: { [key: string]: string },
name: string
) => {
let overwriteColor;
// this is for supporting old visualizations (created by vislib plugin)
// it seems that there for some aggs, the uiState saved from vislib is
// different than the es-charts handle it
if (overwriteColors.hasOwnProperty(name)) {
overwriteColor = overwriteColors[name];
}
seriesLayers.forEach((layer) => {
if (Object.keys(overwriteColors).includes(layer.name)) {
overwriteColor = overwriteColors[layer.name];
}
});
return overwriteColor;
};
export const getColor = (
chartType: ChartTypes,
d: ShapeTreeNode,
layerIndex: number,
isSplitChart: boolean,
overwriteColors: { [key: string]: string } = {},
columns: Array<Partial<BucketColumns>>,
rows: DatatableRow[],
visParams: PartitionVisParams,
palettes: PaletteRegistry | null,
byDataPalette: ReturnType<typeof byDataColorPaletteMap>,
syncColors: boolean,
isDarkMode: boolean,
formatter: FieldFormatsStart,
format?: BucketColumns['format']
) => {
const distinctSeries = getDistinctSeries(rows, columns);
const { parentSeries } = distinctSeries;
const dataName = d.dataName;
// Mind the difference here: the contrast computation for the text ignores the alpha/opacity
// therefore change it for dask mode
const defaultColor = isDarkMode ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)';
let name = '';
if (format) {
name = formatter.deserialize(format).convert(dataName) ?? '';
}
if (visParams.distinctColors) {
return (
getDistinctColor(
d,
isSplitChart,
overwriteColors,
visParams,
palettes,
syncColors,
distinctSeries,
name
) || defaultColor
);
}
const seriesLayers = createSeriesLayers(d, parentSeries, isSplitChart);
const overwriteColor = overrideColorForOldVisualization(seriesLayers, overwriteColors, name);
if (overwriteColor) {
return lightenColor(overwriteColor, seriesLayers.length, columns.length);
}
if (chartType === ChartTypes.MOSAIC && byDataPalette && seriesLayers[1]) {
return byDataPalette.getColor(seriesLayers[1].name) || defaultColor;
}
if (isTreemapOrMosaicChart(chartType)) {
if (layerIndex < columns.length - 1) {
return defaultColor;
}
// only use the top level series layer for coloring
if (seriesLayers.length > 1) {
seriesLayers.pop();
}
}
const outputColor = palettes?.get(visParams.palette.name).getCategoricalColor(
seriesLayers,
{
behindText: visParams.labels.show || isTreemapOrMosaicChart(chartType),
maxDepth: columns.length,
totalSeries: rows.length,
syncColors,
},
visParams.palette?.params ?? { colors: [] }
);
return outputColor || defaultColor;
};

View file

@ -6,11 +6,12 @@
* Side Public License, v 1.
*/
import { ShapeTreeNode } from '@elastic/charts';
import { PaletteDefinition, SeriesLayer } from '../../../../charts/public';
import { dataPluginMock } from '../../../../data/public/mocks';
import type { DataPublicPluginStart } from '../../../../data/public';
import { computeColor } from './get_layers';
import { createMockVisData, createMockBucketColumns, createMockPieParams } from '../mocks';
import { PaletteDefinition, SeriesLayer } from '../../../../../charts/public';
import { dataPluginMock } from '../../../../../data/public/mocks';
import type { DataPublicPluginStart } from '../../../../../data/public';
import { getColor } from './get_color';
import { createMockVisData, createMockBucketColumns, createMockPieParams } from '../../mocks';
import { ChartTypes } from '../../../common/types';
const visData = createMockVisData();
const buckets = createMockBucketColumns();
@ -68,14 +69,18 @@ describe('computeColor', () => {
sortIndex: 0,
},
} as unknown as ShapeTreeNode;
const color = computeColor(
const color = getColor(
ChartTypes.PIE,
d,
0,
false,
{},
buckets,
visData.rows,
visParams,
getPaletteRegistry(),
{ getColor: () => undefined },
false,
false,
dataMock.fieldFormats
);
@ -93,14 +98,18 @@ describe('computeColor', () => {
sortIndex: 0,
},
} as unknown as ShapeTreeNode;
const color = computeColor(
const color = getColor(
ChartTypes.PIE,
d,
0,
true,
{},
buckets,
visData.rows,
visParams,
getPaletteRegistry(),
{ getColor: () => undefined },
false,
false,
dataMock.fieldFormats
);
@ -117,14 +126,18 @@ describe('computeColor', () => {
sortIndex: 0,
},
} as unknown as ShapeTreeNode;
const color = computeColor(
const color = getColor(
ChartTypes.PIE,
d,
0,
true,
{ 'ES-Air': '#000028' },
buckets,
visData.rows,
visParams,
getPaletteRegistry(),
{ getColor: () => undefined },
false,
false,
dataMock.fieldFormats
);
@ -162,14 +175,18 @@ describe('computeColor', () => {
...visParams,
distinctColors: true,
};
const color = computeColor(
const color = getColor(
ChartTypes.PIE,
d,
0,
true,
{ '≥ 1000 and < 2000': '#3F6833' },
buckets,
visData.rows,
visParamsNew,
getPaletteRegistry(),
{ getColor: () => undefined },
false,
false,
dataMock.fieldFormats,
{

View file

@ -0,0 +1,84 @@
/*
* 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 { Datum, PartitionLayer } from '@elastic/charts';
import { FieldFormat } from '../../../../../field_formats/common';
import type { FieldFormatsStart } from '../../../../../field_formats/public';
import { PaletteRegistry } from '../../../../../charts/public';
import type { Datatable, DatatableRow } from '../../../../../expressions/public';
import { BucketColumns, ChartTypes, PartitionVisParams } from '../../../common/types';
import { sortPredicateByType } from './sort_predicate';
import { byDataColorPaletteMap, getColor } from './get_color';
import { getNodeLabel } from './get_node_labels';
const EMPTY_SLICE = Symbol('empty_slice');
export const getLayers = (
chartType: ChartTypes,
columns: Array<Partial<BucketColumns>>,
visParams: PartitionVisParams,
visData: Datatable,
overwriteColors: { [key: string]: string } = {},
rows: DatatableRow[],
palettes: PaletteRegistry | null,
formatters: Record<string, FieldFormat | undefined>,
formatter: FieldFormatsStart,
syncColors: boolean,
isDarkMode: boolean
): PartitionLayer[] => {
const fillLabel: PartitionLayer['fillLabel'] = {
valueFont: {
fontWeight: 700,
},
};
if (!visParams.labels.values) {
fillLabel.valueFormatter = () => '';
}
const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow);
let byDataPalette: ReturnType<typeof byDataColorPaletteMap>;
if (!syncColors && columns[1]?.id && palettes && visParams.palette) {
byDataPalette = byDataColorPaletteMap(
rows,
columns[1].id,
palettes?.get(visParams.palette.name),
visParams.palette
);
}
const sortPredicate = sortPredicateByType(chartType, visParams, visData, columns);
return columns.map((col, layerIndex) => {
return {
groupByRollup: (d: Datum) => (col.id ? d[col.id] ?? EMPTY_SLICE : col.name),
showAccessor: (d: Datum) => d !== EMPTY_SLICE,
nodeLabel: (d: unknown) => getNodeLabel(d, col, formatters, formatter.deserialize),
fillLabel,
sortPredicate,
shape: {
fillColor: (d) =>
getColor(
chartType,
d,
layerIndex,
isSplitChart,
overwriteColors,
columns,
rows,
visParams,
palettes,
byDataPalette,
syncColors,
isDarkMode,
formatter,
col.format
),
},
};
});
};

View file

@ -0,0 +1,25 @@
/*
* 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 type { FieldFormat, FormatFactory } from '../../../../../field_formats/common';
import { BucketColumns } from '../../../common/types';
import { getAvailableFormatter } from '../formatters';
export const getNodeLabel = (
nodeName: unknown,
column: Partial<BucketColumns>,
formatters: Record<string, FieldFormat | undefined>,
defaultFormatFactory: FormatFactory
) => {
const formatter = getAvailableFormatter(column, formatters, defaultFormatFactory);
if (formatter) {
return formatter.convert(nodeName) ?? '';
}
return String(nodeName);
};

View file

@ -6,4 +6,4 @@
* Side Public License, v 1.
*/
export * from './pie_vis_component';
export { getLayers } from './get_layers';

View file

@ -0,0 +1,77 @@
/*
* 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 { ArrayEntry } from '@elastic/charts';
import { Datatable } from '../../../../../../../src/plugins/expressions';
import { BucketColumns, ChartTypes, PartitionVisParams } from '../../../common/types';
type SortFn = (([name1, node1]: ArrayEntry, [name2, node2]: ArrayEntry) => number) | undefined;
type SortPredicateDefaultFn = (
visData: Datatable,
columns: Array<Partial<BucketColumns>>
) => SortFn;
type SortPredicatePieDonutFn = (visParams: PartitionVisParams) => SortFn;
type SortPredicatePureFn = () => SortFn;
export const extractUniqTermsMap = (dataTable: Datatable, columnId: string) =>
[...new Set(dataTable.rows.map((item) => item[columnId]))].reduce(
(acc, item, index) => ({
...acc,
[item]: index,
}),
{}
);
const sortPredicateSaveSourceOrder: SortPredicatePureFn =
() =>
([, node1], [, node2]) => {
const [index1] = node1.inputIndex ?? [];
if (index1 !== undefined) {
return index1;
}
return node2.value - node1.value;
};
const sortPredicatePieDonut: SortPredicatePieDonutFn = (visParams) =>
visParams.respectSourceOrder ? sortPredicateSaveSourceOrder() : undefined;
const sortPredicateMosaic: SortPredicateDefaultFn = (visData, columns) => {
const sortingMap = columns[0]?.id ? extractUniqTermsMap(visData, columns[0].id) : {};
return ([name1, node1], [, node2]) => {
// Sorting for first group
if (columns.length === 1 || (node1.children.length && name1 in sortingMap)) {
return sortingMap[name1];
}
// Sorting for second group
return node2.value - node1.value;
};
};
const sortPredicateWaffle: SortPredicatePureFn =
() =>
([, node1], [, node2]) =>
node2.value - node1.value;
export const sortPredicateByType = (
chartType: ChartTypes,
visParams: PartitionVisParams,
visData: Datatable,
columns: Array<Partial<BucketColumns>>
) =>
({
[ChartTypes.PIE]: () => sortPredicatePieDonut(visParams),
[ChartTypes.DONUT]: () => sortPredicatePieDonut(visParams),
[ChartTypes.WAFFLE]: () => sortPredicateWaffle(),
[ChartTypes.TREEMAP]: () => undefined,
[ChartTypes.MOSAIC]: () => sortPredicateMosaic(visData, columns),
}[chartType]());

View file

@ -0,0 +1,140 @@
/*
* 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 { ChartTypes, LegendDisplay } from '../../common/types';
import { createMockVisData } from '../mocks';
import { isLegendFlat, shouldShowLegend } from './legend';
describe('isLegendFlat', () => {
const visData = createMockVisData();
const splitChartDimension = visData.columns[0];
const runIsFlatCommonScenario = (chartType: ChartTypes) => {
it(`legend should be flat for ${chartType} if split dimension is specified`, () => {
const flat = isLegendFlat(chartType, splitChartDimension);
expect(flat).toBeTruthy();
});
it(`legend should be not flat for ${chartType} if split dimension is not specified`, () => {
const flat = isLegendFlat(chartType, undefined);
expect(flat).toBeFalsy();
});
};
runIsFlatCommonScenario(ChartTypes.PIE);
runIsFlatCommonScenario(ChartTypes.DONUT);
runIsFlatCommonScenario(ChartTypes.TREEMAP);
runIsFlatCommonScenario(ChartTypes.MOSAIC);
it('legend should be flat for Waffle if split dimension is specified', () => {
const flat = isLegendFlat(ChartTypes.WAFFLE, splitChartDimension);
expect(flat).toBeTruthy();
});
it('legend should be flat for Waffle if split dimension is not specified', () => {
const flat = isLegendFlat(ChartTypes.WAFFLE, undefined);
expect(flat).toBeTruthy();
});
});
describe('shouldShowLegend', () => {
const visData = createMockVisData();
const runCommonShouldShowLegendScenario = (chartType: ChartTypes) => {
it(`should hide legend if legendDisplay = hide for ${chartType}`, () => {
const show = shouldShowLegend(chartType, LegendDisplay.HIDE);
expect(show).toBeFalsy();
});
it(`should show legend if legendDisplay = show for ${chartType}`, () => {
const show = shouldShowLegend(chartType, LegendDisplay.SHOW);
expect(show).toBeTruthy();
});
};
const runShouldShowLegendDefaultBucketsScenario = (chartType: ChartTypes) => {
it(`should show legend if legendDisplay = default and multiple buckets for ${chartType}`, () => {
const show = shouldShowLegend(chartType, LegendDisplay.DEFAULT, [
visData.columns[0],
visData.columns[1],
]);
expect(show).toBeTruthy();
});
it(`should hide legend if legendDisplay = default and one bucket or less for ${chartType}`, () => {
const show1 = shouldShowLegend(chartType, LegendDisplay.DEFAULT, [visData.columns[0]]);
expect(show1).toBeFalsy();
const show2 = shouldShowLegend(chartType, LegendDisplay.DEFAULT, []);
expect(show2).toBeFalsy();
const show3 = shouldShowLegend(chartType, LegendDisplay.DEFAULT);
expect(show3).toBeFalsy();
});
};
const runShouldShowLegendDefaultAlwaysFalsyScenario = (chartType: ChartTypes) => {
it(`should hide legend if legendDisplay = default and multiple buckets for ${chartType}`, () => {
const show = shouldShowLegend(chartType, LegendDisplay.DEFAULT, [
visData.columns[0],
visData.columns[1],
]);
expect(show).toBeFalsy();
});
it(`should hide legend if legendDisplay = default and one bucket or less for ${chartType}`, () => {
const show1 = shouldShowLegend(chartType, LegendDisplay.DEFAULT, [visData.columns[0]]);
expect(show1).toBeFalsy();
const show2 = shouldShowLegend(chartType, LegendDisplay.DEFAULT, []);
expect(show2).toBeFalsy();
const show3 = shouldShowLegend(chartType, LegendDisplay.DEFAULT);
expect(show3).toBeFalsy();
});
};
const runShouldShowLegendDefaultAlwaysTruthyScenario = (chartType: ChartTypes) => {
it(`should show legend if legendDisplay = default and multiple buckets for ${chartType}`, () => {
const show = shouldShowLegend(chartType, LegendDisplay.DEFAULT, [
visData.columns[0],
visData.columns[1],
]);
expect(show).toBeTruthy();
});
it(`should show legend if legendDisplay = default and one bucket or less for ${chartType}`, () => {
const show1 = shouldShowLegend(chartType, LegendDisplay.DEFAULT, [visData.columns[0]]);
expect(show1).toBeTruthy();
const show2 = shouldShowLegend(chartType, LegendDisplay.DEFAULT, []);
expect(show2).toBeTruthy();
const show3 = shouldShowLegend(chartType, LegendDisplay.DEFAULT);
expect(show3).toBeTruthy();
});
};
runCommonShouldShowLegendScenario(ChartTypes.PIE);
runShouldShowLegendDefaultBucketsScenario(ChartTypes.PIE);
runCommonShouldShowLegendScenario(ChartTypes.DONUT);
runShouldShowLegendDefaultBucketsScenario(ChartTypes.DONUT);
runCommonShouldShowLegendScenario(ChartTypes.TREEMAP);
runShouldShowLegendDefaultAlwaysFalsyScenario(ChartTypes.TREEMAP);
runCommonShouldShowLegendScenario(ChartTypes.MOSAIC);
runShouldShowLegendDefaultAlwaysFalsyScenario(ChartTypes.MOSAIC);
runCommonShouldShowLegendScenario(ChartTypes.WAFFLE);
runShouldShowLegendDefaultAlwaysTruthyScenario(ChartTypes.WAFFLE);
});

View file

@ -0,0 +1,45 @@
/*
* 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';
import { BucketColumns, ChartTypes, LegendDisplay } from '../../common/types';
type GetLegendIsFlatFn = (splitChartDimension: DatatableColumn | undefined) => boolean;
const isLegendFlatCommon: GetLegendIsFlatFn = (splitChartDimension) => Boolean(splitChartDimension);
export const isLegendFlat = (
visType: ChartTypes,
splitChartDimension: DatatableColumn | undefined
) =>
({
[ChartTypes.PIE]: () => isLegendFlatCommon(splitChartDimension),
[ChartTypes.DONUT]: () => isLegendFlatCommon(splitChartDimension),
[ChartTypes.TREEMAP]: () => isLegendFlatCommon(splitChartDimension),
[ChartTypes.MOSAIC]: () => isLegendFlatCommon(splitChartDimension),
[ChartTypes.WAFFLE]: () => true,
}[visType]());
const showIfBuckets = (bucketColumns: Array<Partial<BucketColumns>>) => bucketColumns.length > 1;
const showLegendDefault = (visType: ChartTypes, bucketColumns: Array<Partial<BucketColumns>>) =>
({
[ChartTypes.PIE]: () => showIfBuckets(bucketColumns),
[ChartTypes.DONUT]: () => showIfBuckets(bucketColumns),
[ChartTypes.TREEMAP]: () => false,
[ChartTypes.MOSAIC]: () => false,
[ChartTypes.WAFFLE]: () => true,
}[visType]());
export const shouldShowLegend = (
visType: ChartTypes,
legendDisplay: LegendDisplay,
bucketColumns: Array<Partial<BucketColumns>> = []
) =>
legendDisplay === LegendDisplay.SHOW ||
(legendDisplay === LegendDisplay.DEFAULT && showLegendDefault(visType, bucketColumns));

View file

@ -6,10 +6,10 @@
* Side Public License, v 1.
*/
import { ExpressionPiePlugin } from './plugin';
import { ExpressionPartitionVisPlugin } from './plugin';
export function plugin() {
return new ExpressionPiePlugin();
return new ExpressionPartitionVisPlugin();
}
export type { ExpressionPiePluginSetup, ExpressionPiePluginStart } from './types';
export type { ExpressionPartitionVisPluginSetup, ExpressionPartitionVisPluginStart } from './types';

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { CoreSetup, CoreStart, Plugin } from '../../../../core/server';
import {
partitionLabelsFunction,
pieVisFunction,
treemapVisFunction,
mosaicVisFunction,
waffleVisFunction,
} from '../common';
import {
ExpressionPartitionVisPluginSetup,
ExpressionPartitionVisPluginStart,
SetupDeps,
StartDeps,
} from './types';
export class ExpressionPartitionVisPlugin
implements
Plugin<
ExpressionPartitionVisPluginSetup,
ExpressionPartitionVisPluginStart,
SetupDeps,
StartDeps
>
{
public setup(core: CoreSetup, { expressions }: SetupDeps) {
expressions.registerFunction(partitionLabelsFunction);
expressions.registerFunction(pieVisFunction);
expressions.registerFunction(treemapVisFunction);
expressions.registerFunction(mosaicVisFunction);
expressions.registerFunction(waffleVisFunction);
}
public start(core: CoreStart, deps: StartDeps) {}
public stop() {}
}

View file

@ -7,8 +7,8 @@
*/
import { ExpressionsServerStart, ExpressionsServerSetup } from '../../../expressions/server';
export type ExpressionPiePluginSetup = void;
export type ExpressionPiePluginStart = void;
export type ExpressionPartitionVisPluginSetup = void;
export type ExpressionPartitionVisPluginStart = void;
export interface SetupDeps {
expressions: ExpressionsServerSetup;

View file

@ -1,9 +0,0 @@
# expressionPie
Expression Pie plugin adds a `pie` renderer and function to the expression plugin. The renderer will display the `Pie` chart.
---
## Development
See the [kibana contributing guide](https://github.com/elastic/kibana/blob/main/CONTRIBUTING.md) for instructions setting up your development environment.

View file

@ -1,16 +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.
*/
export const PLUGIN_ID = 'expressionPie';
export const PLUGIN_NAME = 'expressionPie';
export const PIE_VIS_EXPRESSION_NAME = 'pie_vis';
export const PIE_LABELS_VALUE = 'pie_labels_value';
export const PIE_LABELS_FUNCTION = 'pie_labels';
export const DEFAULT_PERCENT_DECIMALS = 2;

View file

@ -1,98 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`interpreter/functions#pie logs correct datatable to inspector 1`] = `
Object {
"columns": Array [
Object {
"id": "col-0-1",
"meta": Object {
"dimensionName": "Slice size",
},
"name": "Count",
},
],
"rows": Array [
Object {
"col-0-1": 0,
},
],
"type": "datatable",
}
`;
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,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"splitColumn": undefined,
"splitRow": undefined,
},
"distinctColors": false,
"emptySizeRatio": 0.3,
"isDonut": true,
"labels": Object {
"last_level": false,
"percentDecimals": 2,
"position": "default",
"show": false,
"truncate": 100,
"type": "pie_labels_value",
"values": true,
"valuesFormat": "percent",
},
"legendPosition": "right",
"maxLegendLines": 2,
"metric": Object {
"accessor": 0,
"format": Object {
"id": "number",
"params": Object {},
},
"type": "vis_dimension",
},
"nestedLegend": true,
"palette": Object {
"name": "kibana_palette",
"type": "system_palette",
},
"splitColumn": undefined,
"splitRow": undefined,
"truncateLegend": true,
},
"visData": Object {
"columns": Array [
Object {
"id": "col-0-1",
"name": "Count",
},
],
"rows": Array [
Object {
"col-0-1": 0,
},
],
"type": "datatable",
},
"visType": "pie",
},
}
`;

View file

@ -1,88 +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 { ExpressionFunctionDefinition, Datatable } from '../../../../expressions/common';
import { PIE_LABELS_FUNCTION, PIE_LABELS_VALUE } from '../constants';
import { ExpressionValuePieLabels, PieLabelsArguments } from '../types/expression_functions';
export const pieLabelsFunction = (): ExpressionFunctionDefinition<
typeof PIE_LABELS_FUNCTION,
Datatable | null,
PieLabelsArguments,
ExpressionValuePieLabels
> => ({
name: PIE_LABELS_FUNCTION,
help: i18n.translate('expressionPie.pieLabels.function.help', {
defaultMessage: 'Generates the pie labels object',
}),
type: PIE_LABELS_VALUE,
args: {
show: {
types: ['boolean'],
help: i18n.translate('expressionPie.pieLabels.function.args.show.help', {
defaultMessage: 'Displays the pie labels',
}),
default: true,
},
position: {
types: ['string'],
default: 'default',
help: i18n.translate('expressionPie.pieLabels.function.args.position.help', {
defaultMessage: 'Defines the label position',
}),
},
values: {
types: ['boolean'],
help: i18n.translate('expressionPie.pieLabels.function.args.values.help', {
defaultMessage: 'Displays the values inside the slices',
}),
default: true,
},
percentDecimals: {
types: ['number'],
help: i18n.translate('expressionPie.pieLabels.function.args.percentDecimals.help', {
defaultMessage: 'Defines the number of decimals that will appear on the values as percent',
}),
default: 2,
},
lastLevel: {
types: ['boolean'],
help: i18n.translate('expressionPie.pieLabels.function.args.lastLevel.help', {
defaultMessage: 'Show top level labels only',
}),
default: true,
},
truncate: {
types: ['number'],
help: i18n.translate('expressionPie.pieLabels.function.args.truncate.help', {
defaultMessage: 'Defines the number of characters that the slice value will display',
}),
default: null,
},
valuesFormat: {
types: ['string'],
default: 'percent',
help: i18n.translate('expressionPie.pieLabels.function.args.valuesFormat.help', {
defaultMessage: 'Defines the format of the values',
}),
},
},
fn: (context, args) => {
return {
type: PIE_LABELS_VALUE,
show: args.show,
position: args.position,
percentDecimals: args.percentDecimals,
values: args.values,
truncate: args.truncate,
valuesFormat: args.valuesFormat,
last_level: args.lastLevel,
};
},
});

View file

@ -1,183 +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 { EmptySizeRatios, PieVisParams } from '../types/expression_renderers';
import { prepareLogTable } from '../../../../visualizations/common/prepare_log_table';
import { PieVisExpressionFunctionDefinition } from '../types/expression_functions';
import { PIE_LABELS_FUNCTION, PIE_LABELS_VALUE, PIE_VIS_EXPRESSION_NAME } from '../constants';
export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
name: PIE_VIS_EXPRESSION_NAME,
type: 'render',
inputTypes: ['datatable'],
help: i18n.translate('expressionPie.pieVis.function.help', {
defaultMessage: 'Pie visualization',
}),
args: {
metric: {
types: ['vis_dimension'],
help: i18n.translate('expressionPie.pieVis.function.args.metricHelpText', {
defaultMessage: 'Metric dimensions config',
}),
required: true,
},
buckets: {
types: ['vis_dimension'],
help: i18n.translate('expressionPie.pieVis.function.args.bucketsHelpText', {
defaultMessage: 'Buckets dimensions config',
}),
multi: true,
},
splitColumn: {
types: ['vis_dimension'],
help: i18n.translate('expressionPie.pieVis.function.args.splitColumnHelpText', {
defaultMessage: 'Split by column dimension config',
}),
multi: true,
},
splitRow: {
types: ['vis_dimension'],
help: i18n.translate('expressionPie.pieVis.function.args.splitRowHelpText', {
defaultMessage: 'Split by row dimension config',
}),
multi: true,
},
addTooltip: {
types: ['boolean'],
help: i18n.translate('expressionPie.pieVis.function.args.addTooltipHelpText', {
defaultMessage: 'Show tooltip on slice hover',
}),
default: true,
},
addLegend: {
types: ['boolean'],
help: i18n.translate('expressionPie.pieVis.function.args.addLegendHelpText', {
defaultMessage: 'Show legend chart legend',
}),
},
legendPosition: {
types: ['string'],
help: i18n.translate('expressionPie.pieVis.function.args.legendPositionHelpText', {
defaultMessage: 'Position the legend on top, bottom, left, right of the chart',
}),
},
nestedLegend: {
types: ['boolean'],
help: i18n.translate('expressionPie.pieVis.function.args.nestedLegendHelpText', {
defaultMessage: 'Show a more detailed legend',
}),
default: false,
},
truncateLegend: {
types: ['boolean'],
help: i18n.translate('expressionPie.pieVis.function.args.truncateLegendHelpText', {
defaultMessage: 'Defines if the legend items will be truncated or not',
}),
default: true,
},
maxLegendLines: {
types: ['number'],
help: i18n.translate('expressionPie.pieVis.function.args.maxLegendLinesHelpText', {
defaultMessage: 'Defines the number of lines per legend item',
}),
},
distinctColors: {
types: ['boolean'],
help: i18n.translate('expressionPie.pieVis.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('expressionPie.pieVis.function.args.isDonutHelpText', {
defaultMessage: 'Displays the pie chart as donut',
}),
default: false,
},
emptySizeRatio: {
types: ['number'],
help: i18n.translate('expressionPie.pieVis.function.args.emptySizeRatioHelpText', {
defaultMessage: 'Defines donut inner empty area size',
}),
default: EmptySizeRatios.SMALL,
},
palette: {
types: ['palette', 'system_palette'],
help: i18n.translate('expressionPie.pieVis.function.args.paletteHelpText', {
defaultMessage: 'Defines the chart palette name',
}),
default: '{palette}',
},
labels: {
types: [PIE_LABELS_VALUE],
help: i18n.translate('expressionPie.pieVis.function.args.labelsHelpText', {
defaultMessage: 'Pie labels config',
}),
default: `{${PIE_LABELS_FUNCTION}}`,
},
},
fn(context, args, handlers) {
const visConfig: PieVisParams = {
...args,
palette: args.palette,
dimensions: {
metric: args.metric,
buckets: args.buckets,
splitColumn: args.splitColumn,
splitRow: args.splitRow,
},
};
if (handlers?.inspectorAdapters?.tables) {
const logTable = prepareLogTable(context, [
[
[args.metric],
i18n.translate('expressionPie.pieVis.function.dimension.metric', {
defaultMessage: 'Slice size',
}),
],
[
args.buckets,
i18n.translate('expressionPie.pieVis.function.dimension.buckets', {
defaultMessage: 'Slice',
}),
],
[
args.splitColumn,
i18n.translate('expressionPie.pieVis.function.dimension.splitcolumn', {
defaultMessage: 'Column split',
}),
],
[
args.splitRow,
i18n.translate('expressionPie.pieVis.function.dimension.splitrow', {
defaultMessage: 'Row split',
}),
],
]);
handlers.inspectorAdapters.tables.logDatatable('default', logTable);
}
return {
type: 'render',
as: PIE_VIS_EXPRESSION_NAME,
value: {
visData: context,
visConfig,
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
visType: 'pie',
params: {
listenOnChange: true,
},
},
};
},
});

View file

@ -1,32 +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.
*/
export {
PLUGIN_ID,
PLUGIN_NAME,
PIE_VIS_EXPRESSION_NAME,
PIE_LABELS_VALUE,
PIE_LABELS_FUNCTION,
} from './constants';
export { pieVisFunction, pieLabelsFunction } from './expression_functions';
export type {
ExpressionValuePieLabels,
PieVisExpressionFunctionDefinition,
} from './types/expression_functions';
export type {
PieVisParams,
PieVisConfig,
LabelsParams,
Dimension,
Dimensions,
} from './types/expression_renderers';
export { ValueFormats, LabelPositions, EmptySizeRatios } from './types/expression_renderers';

View file

@ -1,46 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { PIE_LABELS_VALUE, PIE_VIS_EXPRESSION_NAME } from '../constants';
import {
ExpressionFunctionDefinition,
Datatable,
ExpressionValueRender,
ExpressionValueBoxed,
} from '../../../../expressions/common';
import { RenderValue, PieVisConfig, LabelPositions, ValueFormats } from './expression_renderers';
export interface PieLabelsArguments {
show: boolean;
position: LabelPositions;
values: boolean;
truncate: number | null;
valuesFormat: ValueFormats;
lastLevel: boolean;
percentDecimals: number;
}
export type ExpressionValuePieLabels = ExpressionValueBoxed<
typeof PIE_LABELS_VALUE,
{
show: boolean;
position: LabelPositions;
values: boolean;
truncate: number | null;
valuesFormat: ValueFormats;
last_level: boolean;
percentDecimals: number;
}
>;
export type PieVisExpressionFunctionDefinition = ExpressionFunctionDefinition<
typeof PIE_VIS_EXPRESSION_NAME,
Datatable,
PieVisConfig,
ExpressionValueRender<RenderValue>
>;

View file

@ -1,115 +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 { storiesOf } from '@storybook/react';
import { Datatable } from '../../../../expressions';
import { Render } from '../../../../presentation_util/public/__stories__';
import { getPieVisRenderer } from '../expression_renderers';
import { LabelPositions, RenderValue, ValueFormats } from '../../common/types';
import { palettes, theme, getStartDeps } from '../__mocks__';
const visData: Datatable = {
type: 'datatable',
columns: [
{ id: 'cost', name: 'cost', meta: { type: 'number' } },
{ id: 'age', name: 'age', meta: { type: 'number' } },
{ id: 'price', name: 'price', meta: { type: 'number' } },
{ id: 'project', name: 'project', meta: { type: 'string' } },
{ id: '@timestamp', name: '@timestamp', meta: { type: 'date' } },
],
rows: [
{
cost: 32.15,
age: 63,
price: 53,
project: 'elasticsearch',
'@timestamp': 1546334211208,
},
{
cost: 20.52,
age: 68,
price: 33,
project: 'beats',
'@timestamp': 1546351551031,
},
{
cost: 21.15,
age: 57,
price: 59,
project: 'apm',
'@timestamp': 1546352631083,
},
{
cost: 35.64,
age: 73,
price: 71,
project: 'machine-learning',
'@timestamp': 1546402490956,
},
{
cost: 27.19,
age: 38,
price: 36,
project: 'kibana',
'@timestamp': 1546467111351,
},
],
};
const config: RenderValue = {
visType: 'pie_vis',
visData,
visConfig: {
dimensions: {
metric: {
type: 'vis_dimension',
accessor: { id: 'cost', name: 'cost', meta: { type: 'number' } },
format: { id: 'number', params: {} },
},
buckets: [
{
type: 'vis_dimension',
accessor: { id: 'age', name: 'age', meta: { type: 'number' } },
format: { id: 'number', params: {} },
},
],
},
palette: { type: 'system_palette', name: 'default' },
addTooltip: false,
addLegend: false,
legendPosition: 'right',
nestedLegend: false,
truncateLegend: false,
distinctColors: false,
isDonut: false,
emptySizeRatio: 0.37,
maxLegendLines: 1,
labels: {
show: false,
last_level: false,
position: LabelPositions.DEFAULT,
values: false,
truncate: null,
valuesFormat: ValueFormats.VALUE,
percentDecimals: 1,
},
},
syncColors: false,
};
const containerSize = {
width: '700px',
height: '700px',
};
const pieRenderer = getPieVisRenderer({ palettes, theme, getStartDeps });
storiesOf('renderers/pieVis', module).add('Default', () => {
return <Render renderer={() => pieRenderer} config={config} {...containerSize} />;
});

View file

@ -1,201 +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 { Datum, PartitionLayer, ShapeTreeNode, ArrayEntry } from '@elastic/charts';
import { isEqual } from 'lodash';
import type { FieldFormatsStart } from 'src/plugins/field_formats/public';
import { SeriesLayer, PaletteRegistry, lightenColor } from '../../../../charts/public';
import type { DatatableRow } from '../../../../expressions/public';
import type { BucketColumns, PieVisParams, SplitDimensionParams } from '../../common/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,
formatter: FieldFormatsStart,
format?: BucketColumns['format']
) => {
const { parentSeries, allSeries } = getDistinctSeries(rows, columns);
const dataName = d.dataName;
let formattedLabel = '';
if (format) {
formattedLabel = formatter.deserialize(format).convert(dataName) ?? '';
}
if (visParams.distinctColors) {
let overwriteColor;
// this is for supporting old visualizations (created by vislib plugin)
// it seems that there for some aggs, the uiState saved from vislib is
// different than the es-charts handle it
if (overwriteColors.hasOwnProperty(formattedLabel)) {
overwriteColor = overwriteColors[formattedLabel];
}
if (Object.keys(overwriteColors).includes(dataName.toString())) {
overwriteColor = overwriteColors[dataName];
}
if (overwriteColor) {
return overwriteColor;
}
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,
},
visParams.palette?.params ?? { colors: [] }
);
}
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;
// this is for supporting old visualizations (created by vislib plugin)
// it seems that there for some aggs, the uiState saved from vislib is
// different than the es-charts handle it
if (overwriteColors.hasOwnProperty(formattedLabel)) {
overwriteColor = overwriteColors[formattedLabel];
}
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,
},
visParams.palette?.params ?? { colors: [] }
);
};
export const getLayers = (
columns: Array<Partial<BucketColumns>>,
visParams: PieVisParams,
overwriteColors: { [key: string]: string } = {},
rows: DatatableRow[],
palettes: PaletteRegistry | null,
formatter: FieldFormatsStart,
syncColors: boolean
): PartitionLayer[] => {
const fillLabel: PartitionLayer['fillLabel'] = {
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 (col.format) {
return formatter.deserialize(col.format).convert(d) ?? '';
}
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 && 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,
formatter,
col.format
);
return outputColor || 'rgba(0,0,0,0)';
},
},
};
});
};

View file

@ -1,72 +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 { getPartitionTheme } from './get_partition_theme';
import { createMockPieParams } from '../mocks';
const visParams = createMockPieParams();
describe('getConfig', () => {
it('should cap the outerSizeRatio to 1', () => {
expect(
getPartitionTheme(visParams, {}, { width: 400, height: 400 }).partition?.outerSizeRatio
).toBe(1);
});
it('should not have outerSizeRatio for split chart', () => {
expect(
getPartitionTheme(
{
...visParams,
dimensions: {
...visParams.dimensions,
splitColumn: [
{
type: 'vis_dimension',
accessor: 1,
format: {
id: 'number',
params: {},
},
},
],
},
},
{},
{ width: 400, height: 400 }
).partition?.outerSizeRatio
).toBeUndefined();
expect(
getPartitionTheme(
{
...visParams,
dimensions: {
...visParams.dimensions,
splitRow: [
{
type: 'vis_dimension',
accessor: 1,
format: {
id: 'number',
params: {},
},
},
],
},
},
{},
{ width: 400, height: 400 }
).partition?.outerSizeRatio
).toBeUndefined();
});
it('should not set outerSizeRatio if dimensions are not defined', () => {
expect(getPartitionTheme(visParams, {}).partition?.outerSizeRatio).toBeUndefined();
});
});

View file

@ -1,85 +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 { PartialTheme } from '@elastic/charts';
import { Required } from '@kbn/utility-types';
import { LabelPositions, PieVisParams, PieContainerDimensions } from '../../common/types';
const MAX_SIZE = 1000;
export const getPartitionTheme = (
visParams: PieVisParams,
chartTheme: PartialTheme,
dimensions?: PieContainerDimensions,
rescaleFactor: number = 1
): PartialTheme => {
// On small multiples we want the labels to only appear inside
const isSplitChart = Boolean(visParams.dimensions.splitColumn || visParams.dimensions.splitRow);
const paddingProps: PartialTheme | null =
dimensions && !isSplitChart
? {
chartPaddings: {
// TODO: simplify ratio logic to be static px units
top: ((1 - Math.min(1, MAX_SIZE / dimensions?.height)) / 2) * dimensions?.height,
bottom: ((1 - Math.min(1, MAX_SIZE / dimensions?.height)) / 2) * dimensions?.height,
left: ((1 - Math.min(1, MAX_SIZE / dimensions?.width)) / 2) * dimensions?.height,
right: ((1 - Math.min(1, MAX_SIZE / dimensions?.width)) / 2) * dimensions?.height,
},
}
: null;
const outerSizeRatio: PartialTheme['partition'] | null =
dimensions && !isSplitChart
? {
outerSizeRatio:
// Cap the ratio to 1 and then rescale
rescaleFactor * Math.min(MAX_SIZE / Math.min(dimensions?.width, dimensions?.height), 1),
}
: null;
const theme: Required<PartialTheme, 'partition'> = {
chartMargins: { top: 0, bottom: 0, left: 0, right: 0 },
...paddingProps,
partition: {
fontFamily: chartTheme.barSeriesStyle?.displayValue?.fontFamily,
...outerSizeRatio,
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 ? visParams.emptySizeRatio : 0,
},
};
if (!visParams.labels.show) {
// Force all labels to be linked, then prevent links from showing
theme.partition.linkLabel = { maxCount: 0, maximumSection: Number.POSITIVE_INFINITY };
}
if (visParams.labels.last_level && visParams.labels.show) {
theme.partition.linkLabel = {
maxCount: Number.POSITIVE_INFINITY,
maximumSection: Number.POSITIVE_INFINITY,
maxTextLength: visParams.labels.truncate ?? undefined,
};
}
if (
(visParams.labels.position === LabelPositions.INSIDE || isSplitChart) &&
visParams.labels.show
) {
theme.partition.linkLabel = { maxCount: 0 };
}
return theme;
};

Some files were not shown because too many files have changed in this diff Show more