kibana/x-pack/plugins/lens/public/pie_visualization/visualization.tsx

281 lines
7.9 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { render } from 'react-dom';
import { i18n } from '@kbn/i18n';
import { I18nProvider } from '@kbn/i18n/react';
import { PaletteRegistry } from 'src/plugins/charts/public';
import { Visualization, OperationMetadata, AccessorConfig } from '../types';
import { toExpression, toPreviewExpression } from './to_expression';
import { LayerState, PieVisualizationState } from './types';
import { suggestions } from './suggestions';
import { CHART_NAMES, MAX_PIE_BUCKETS, MAX_TREEMAP_BUCKETS } from './constants';
import { DimensionEditor, PieToolbar } from './toolbar';
function newLayerState(layerId: string): LayerState {
return {
layerId,
groups: [],
metric: undefined,
numberDisplay: 'percent',
categoryDisplay: 'default',
legendDisplay: 'default',
nestedLegend: false,
};
}
const bucketedOperations = (op: OperationMetadata) => op.isBucketed;
const numberMetricOperations = (op: OperationMetadata) =>
!op.isBucketed && op.dataType === 'number';
export const getPieVisualization = ({
paletteService,
}: {
paletteService: PaletteRegistry;
}): Visualization<PieVisualizationState> => ({
id: 'lnsPie',
visualizationTypes: [
{
id: 'donut',
icon: CHART_NAMES.donut.icon,
label: CHART_NAMES.donut.label,
},
{
id: 'pie',
icon: CHART_NAMES.pie.icon,
label: CHART_NAMES.pie.label,
},
{
id: 'treemap',
icon: CHART_NAMES.treemap.icon,
label: CHART_NAMES.treemap.label,
},
],
getVisualizationTypeId(state) {
return state.shape;
},
getLayerIds(state) {
return state.layers.map((l) => l.layerId);
},
clearLayer(state) {
return {
shape: state.shape,
layers: state.layers.map((l) => newLayerState(l.layerId)),
};
},
getDescription(state) {
if (state.shape === 'treemap') {
return CHART_NAMES.treemap;
}
if (state.shape === 'donut') {
return CHART_NAMES.donut;
}
return CHART_NAMES.pie;
},
switchVisualizationType: (visualizationTypeId, state) => ({
...state,
shape: visualizationTypeId as PieVisualizationState['shape'],
}),
initialize(frame, state, mainPalette) {
return (
state || {
shape: 'donut',
layers: [newLayerState(frame.addNewLayer())],
palette: mainPalette,
}
);
},
getMainPalette: (state) => (state ? state.palette : undefined),
getSuggestions: suggestions,
getConfiguration({ state, frame, layerId }) {
const layer = state.layers.find((l) => l.layerId === layerId);
if (!layer) {
return { groups: [] };
}
const datasource = frame.datasourceLayers[layer.layerId];
const originalOrder = datasource
.getTableSpec()
.map(({ columnId }) => columnId)
.filter((columnId) => columnId !== layer.metric);
// When we add a column it could be empty, and therefore have no order
const sortedColumns: AccessorConfig[] = Array.from(
new Set(originalOrder.concat(layer.groups))
).map((accessor) => ({ columnId: accessor }));
if (sortedColumns.length > 0) {
sortedColumns[0] = {
columnId: sortedColumns[0].columnId,
triggerIcon: 'colorBy',
palette: paletteService
.get(state.palette?.name || 'default')
.getColors(10, state.palette?.params),
};
}
if (state.shape === 'treemap') {
return {
groups: [
{
groupId: 'groups',
groupLabel: i18n.translate('xpack.lens.pie.treemapGroupLabel', {
defaultMessage: 'Group by',
}),
layerId,
accessors: sortedColumns,
supportsMoreColumns: sortedColumns.length < MAX_TREEMAP_BUCKETS,
filterOperations: bucketedOperations,
required: true,
dataTestSubj: 'lnsPie_groupByDimensionPanel',
enableDimensionEditor: true,
},
{
groupId: 'metric',
groupLabel: i18n.translate('xpack.lens.pie.groupsizeLabel', {
defaultMessage: 'Size by',
}),
layerId,
accessors: layer.metric ? [{ columnId: layer.metric }] : [],
supportsMoreColumns: !layer.metric,
filterOperations: numberMetricOperations,
required: true,
dataTestSubj: 'lnsPie_sizeByDimensionPanel',
},
],
};
}
return {
groups: [
{
groupId: 'groups',
groupLabel: i18n.translate('xpack.lens.pie.sliceGroupLabel', {
defaultMessage: 'Slice by',
}),
layerId,
accessors: sortedColumns,
supportsMoreColumns: sortedColumns.length < MAX_PIE_BUCKETS,
filterOperations: bucketedOperations,
required: true,
dataTestSubj: 'lnsPie_sliceByDimensionPanel',
enableDimensionEditor: true,
},
{
groupId: 'metric',
groupLabel: i18n.translate('xpack.lens.pie.groupsizeLabel', {
defaultMessage: 'Size by',
}),
layerId,
accessors: layer.metric ? [{ columnId: layer.metric }] : [],
supportsMoreColumns: !layer.metric,
filterOperations: numberMetricOperations,
required: true,
dataTestSubj: 'lnsPie_sizeByDimensionPanel',
},
],
};
},
setDimension({ prevState, layerId, columnId, groupId }) {
return {
...prevState,
shape:
prevState.shape === 'donut' && prevState.layers.every((l) => l.groups.length === 1)
? 'pie'
: prevState.shape,
layers: prevState.layers.map((l) => {
if (l.layerId !== layerId) {
return l;
}
if (groupId === 'groups') {
return { ...l, groups: [...l.groups, columnId] };
}
return { ...l, metric: columnId };
}),
};
},
removeDimension({ prevState, layerId, columnId }) {
return {
...prevState,
layers: prevState.layers.map((l) => {
if (l.layerId !== layerId) {
return l;
}
if (l.metric === columnId) {
return { ...l, metric: undefined };
}
return { ...l, groups: l.groups.filter((c) => c !== columnId) };
}),
};
},
renderDimensionEditor(domElement, props) {
render(
<I18nProvider>
<DimensionEditor {...props} />
</I18nProvider>,
domElement
);
},
toExpression: (state, layers, attributes) =>
toExpression(state, layers, paletteService, attributes),
toPreviewExpression: (state, layers) => toPreviewExpression(state, layers, paletteService),
renderToolbar(domElement, props) {
render(
<I18nProvider>
<PieToolbar {...props} />
</I18nProvider>,
domElement
);
},
getWarningMessages(state, frame) {
if (state?.layers.length === 0 || !frame.activeData) {
return;
}
const metricColumnsWithArrayValues = [];
for (const layer of state.layers) {
const { layerId, metric } = layer;
const rows = frame.activeData[layerId] && frame.activeData[layerId].rows;
if (!rows || !metric) {
break;
}
const columnToLabel = frame.datasourceLayers[layerId].getOperationForColumnId(metric)?.label;
const hasArrayValues = rows.some((row) => Array.isArray(row[metric]));
if (hasArrayValues) {
metricColumnsWithArrayValues.push(columnToLabel || metric);
}
}
return metricColumnsWithArrayValues.map((label) => (
<>
<strong>{label}</strong> contains array values. Your visualization may not render as
expected.
</>
));
},
getErrorMessages(state, frame) {
// not possible to break it?
return undefined;
},
});