Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lens][PoC] partition chart as sum of multiple fields #140093

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { Position } from '@elastic/charts';
import { prepareLogTable, validateAccessor } from '@kbn/visualizations-plugin/common/utils';
import { DEFAULT_LEGEND_SIZE, LegendSize } from '@kbn/visualizations-plugin/common/constants';
import { Datatable, DatatableColumn, DatatableRow } from '@kbn/expressions-plugin/common';
import { EmptySizeRatios, LegendDisplay, PartitionVisParams } from '../types/expression_renderers';
import { ChartTypes, PieVisExpressionFunctionDefinition } from '../types';
import {
Expand All @@ -19,6 +20,51 @@ import {
} from '../constants';
import { errors, strings } from './i18n';

const transposeTable = (
table: Datatable
): {
table: Datatable;
metricAccessor: string;
bucketAccessor: string;
} => {
const oldRow = table.rows[0]; // only supports single row tables for now

const nameColumnId = 'name';
const valueColumnId = 'value';

const transposedRows: DatatableRow[] = table.columns.map((column) => ({
[nameColumnId]: column.name,
[valueColumnId]: oldRow[column.id],
}));

const transposedColumns: DatatableColumn[] = [
{
id: nameColumnId,
name: nameColumnId,
meta: {
type: 'string',
},
},
{
id: valueColumnId,
name: valueColumnId,
meta: {
type: 'number',
},
},
];

return {
metricAccessor: valueColumnId,
bucketAccessor: nameColumnId,
table: {
type: 'datatable',
columns: transposedColumns,
rows: transposedRows,
},
};
};

export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
name: PIE_VIS_EXPRESSION_NAME,
type: 'render',
Expand All @@ -28,13 +74,17 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
metric: {
types: ['vis_dimension', 'string'],
help: strings.getMetricArgHelp(),
required: true,
},
buckets: {
types: ['vis_dimension', 'string'],
help: strings.getBucketsArgHelp(),
multi: true,
},
// TODO - revisit arg name (columnsAsSlices?)
partitionByColumn: {
types: ['boolean'],
help: 'whether or not to transpose the datatable before rendering the slices',
},
splitColumn: {
types: ['vis_dimension', 'string'],
help: strings.getSplitColumnArgHelp(),
Expand Down Expand Up @@ -137,7 +187,9 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
throw new Error(errors.splitRowAndSplitColumnAreSpecifiedError());
}

validateAccessor(args.metric, context.columns);
if (args.metric) {
validateAccessor(args.metric, context.columns);
}
if (args.buckets) {
args.buckets.forEach((bucket) => validateAccessor(bucket, context.columns));
}
Expand All @@ -148,6 +200,16 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
args.splitRow.forEach((splitRow) => validateAccessor(splitRow, context.columns));
}

let transposedTable;
let transposedMetricAccessor;
let transposedBucketAccessor;
if (args.partitionByColumn) {
const result = transposeTable(context);
transposedTable = result.table;
transposedMetricAccessor = result.metricAccessor;
transposedBucketAccessor = result.bucketAccessor;
}

const visConfig: PartitionVisParams = {
...args,
ariaLabel:
Expand All @@ -156,22 +218,26 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
handlers.getExecutionContext?.()?.description,
palette: args.palette,
dimensions: {
metric: args.metric,
buckets: args.buckets,
metric: transposedMetricAccessor ?? args.metric,
buckets: transposedBucketAccessor ? [transposedBucketAccessor] : args.buckets,
splitColumn: args.splitColumn,
splitRow: args.splitRow,
},
};

// TODO fix inspector
if (handlers?.inspectorAdapters?.tables) {
handlers.inspectorAdapters.tables.reset();
handlers.inspectorAdapters.tables.allowCsvExport = true;

const logTable = prepareLogTable(
context,
[
[[args.metric], strings.getSliceSizeHelp()],
[args.buckets, strings.getSliceHelp()],
[[transposedMetricAccessor ?? args.metric], strings.getSliceSizeHelp()],
[
transposedBucketAccessor ? [transposedBucketAccessor] : args.buckets,
strings.getSliceHelp(),
],
[args.splitColumn, strings.getColumnSplitHelp()],
[args.splitRow, strings.getRowSplitHelp()],
],
Expand All @@ -184,7 +250,7 @@ export const pieVisFunction = (): PieVisExpressionFunctionDefinition => ({
type: 'render',
as: PARTITION_VIS_RENDERER_NAME,
value: {
visData: context,
visData: transposedTable ?? context,
visConfig,
syncColors: handlers?.isSyncColorsEnabled?.() ?? false,
visType: args.isDonut ? ChartTypes.DONUT : ChartTypes.PIE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export interface PartitionVisParams extends VisCommonParams {

export interface PieVisConfig extends VisCommonConfig {
buckets?: Array<ExpressionValueVisDimension | string>;
partitionByColumn?: boolean;
isDonut: boolean;
emptySizeRatio?: EmptySizeRatios;
respectSourceOrder?: boolean;
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/lens/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export enum EmptySizeRatios {
export interface SharedPieLayerState {
groups: string[];
metric?: string;
partitionByDimension?: boolean;
numberDisplay: NumberDisplayType;
categoryDisplay: CategoryDisplayType;
legendDisplay: LegendDisplayType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@ import {
EuiButtonEmpty,
EuiButtonIcon,
EuiCheckbox,
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiContextMenuPanelItemDescriptor,
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiPopover,
useGeneratedHtmlId,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { Visualization } from '../../../types';
import { StateSetter, Visualization } from '../../../types';
import { LocalStorageLens, LOCAL_STORAGE_LENS_KEY } from '../../../settings_storage';
import { LayerType, layerTypes } from '../../..';

Expand All @@ -45,8 +51,6 @@ const getButtonCopy = (
canBeRemoved?: boolean,
isOnlyLayer?: boolean
) => {
let ariaLabel;

const layerTypeCopy =
layerType === layerTypes.DATA
? i18n.translate('xpack.lens.modalTitle.layerType.data', {
Expand Down Expand Up @@ -80,6 +84,7 @@ const getButtonCopy = (
modalDesc = modalDescRefLine;
}

let ariaLabel;
if (!canBeRemoved) {
ariaLabel = i18n.translate('xpack.lens.resetVisualizationAriaLabel', {
defaultMessage: 'Reset visualization',
Expand All @@ -96,33 +101,102 @@ const getButtonCopy = (
});
}

let optionLabel;
if (!canBeRemoved) {
optionLabel = i18n.translate('xpack.lens.resetVisualizationOptionLabel', {
defaultMessage: 'Reset visualization',
});
} else if (isOnlyLayer) {
optionLabel = i18n.translate('xpack.lens.resetLayerOptionLabel', {
defaultMessage: 'Reset layer',
});
} else {
optionLabel = i18n.translate('xpack.lens.deleteLayerOptionLabel', {
defaultMessage: `Delete layer`,
});
}

return {
optionLabel,
ariaLabel,
modalTitle,
modalDesc,
};
};

export function RemoveLayerButton({
// TODO - clean up remove option generation logic
export function LayerContextMenu({
onRemoveLayer,
layerId,
layerIndex,
isOnlyLayer,
activeVisualization,
layerType,
visualizationState,
updateVisualization,
}: {
onRemoveLayer: () => void;
layerId: string;
layerIndex: number;
isOnlyLayer: boolean;
activeVisualization: Visualization;
layerType?: LayerType;
visualizationState: unknown;
updateVisualization: StateSetter<unknown, unknown>;
}) {
const { ariaLabel, modalTitle, modalDesc } = getButtonCopy(
const contextMenuPopoverId = useGeneratedHtmlId({
prefix: 'lnsLayerContextMenuPopover',
});

const [contextMenuOpen, setContextMenuOpen] = useState(false);

const closeContextMenu = () => {
setContextMenuOpen(false);
};

const { optionLabel, ariaLabel, modalTitle, modalDesc } = getButtonCopy(
layerIndex,
layerType || layerTypes.DATA,
!!activeVisualization.removeLayer,
isOnlyLayer
);

const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
items: [
{
name: optionLabel,
icon: <EuiIcon type={isOnlyLayer ? 'eraser' : 'trash'} color="danger" />,
onClick: () => {
closeContextMenu();
if (lensLocalStorage?.skipDeleteModal) {
return onRemoveLayer();
}
return showModal();
},
'data-test-subj': 'lnsLayerRemove',
'aria-label': ariaLabel,
},
...(activeVisualization.getLayerActions
? activeVisualization.getLayerActions(layerId, visualizationState)
: []
).map(
(item) =>
({
...item,
onClick: () => {
updateVisualization(
activeVisualization.onLayerAction!(layerId, item.actionId, visualizationState)
);
closeContextMenu();
},
} as EuiContextMenuPanelItemDescriptor)
),
],
},
];

const [isModalVisible, setIsModalVisible] = useState(false);
const [lensLocalStorage, setLensLocalStorage] = useLocalStorage<LocalStorageLens>(
LOCAL_STORAGE_LENS_KEY,
Expand All @@ -138,36 +212,26 @@ export function RemoveLayerButton({
const closeModal = () => setIsModalVisible(false);
const showModal = () => setIsModalVisible(true);

const removeLayer = () => {
// If we don't blur the remove / clear button, it remains focused
// which is a strange UX in this case. e.target.blur doesn't work
// due to who knows what, but probably event re-writing. Additionally,
// activeElement does not have blur so, we need to do some casting + safeguards.
const el = document.activeElement as unknown as { blur: () => void };

if (el?.blur) {
el.blur();
}

onRemoveLayer();
};

return (
<>
<EuiButtonIcon
size="xs"
iconType={isOnlyLayer ? 'eraser' : 'trash'}
color="danger"
data-test-subj="lnsLayerRemove"
aria-label={ariaLabel}
title={ariaLabel}
onClick={() => {
if (lensLocalStorage?.skipDeleteModal) {
return removeLayer();
}
return showModal();
}}
/>
<EuiPopover
id={contextMenuPopoverId}
button={
<EuiButtonIcon
iconType={'boxesHorizontal'}
aria-label={i18n.translate('xpack.lens.layerContextMenu', {
defaultMessage: 'Layer options',
})}
onClick={() => setContextMenuOpen(!contextMenuOpen)}
/>
}
isOpen={contextMenuOpen}
closePopover={closeContextMenu}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
{isModalVisible ? (
<RemoveConfirmModal
modalTitle={modalTitle}
Expand All @@ -176,7 +240,7 @@ export function RemoveLayerButton({
closeModal={closeModal}
skipDeleteModal={lensLocalStorage?.skipDeleteModal}
onChangeShouldShowModal={onChangeShouldShowModal}
removeLayer={removeLayer}
removeLayer={onRemoveLayer}
/>
) : null}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { DragDropIdentifier, ReorderProvider } from '../../../drag_drop';
import { LayerSettings } from './layer_settings';
import { LayerPanelProps, ActiveDimensionState } from './types';
import { DimensionContainer } from './dimension_container';
import { RemoveLayerButton } from './remove_layer_button';
import { LayerContextMenu } from './layer_context_menu';
import { EmptyDimensionButton } from './buttons/empty_dimension_button';
import { DimensionButton } from './buttons/dimension_button';
import { DraggableDimensionButton } from './buttons/draggable_dimension_button';
Expand Down Expand Up @@ -327,12 +327,15 @@ export function LayerPanel(
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<RemoveLayerButton
<LayerContextMenu
onRemoveLayer={onRemoveLayer}
layerId={layerId}
layerIndex={layerIndex}
isOnlyLayer={isOnlyLayer}
activeVisualization={activeVisualization}
layerType={activeVisualization.getLayerType(layerId, visualizationState)}
visualizationState={visualizationState}
updateVisualization={props.updateVisualization}
/>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
Loading