mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
parent
c0c398a185
commit
495fc8b8e2
23 changed files with 604 additions and 128 deletions
|
@ -22,7 +22,7 @@ import { PaletteService } from './service';
|
|||
import { PaletteDefinition, SeriesLayer } from './types';
|
||||
|
||||
export const getPaletteRegistry = () => {
|
||||
const mockPalette: jest.Mocked<PaletteDefinition> = {
|
||||
const mockPalette1: jest.Mocked<PaletteDefinition> = {
|
||||
id: 'default',
|
||||
title: 'My Palette',
|
||||
getColor: jest.fn((_: SeriesLayer[]) => 'black'),
|
||||
|
@ -41,9 +41,28 @@ export const getPaletteRegistry = () => {
|
|||
})),
|
||||
};
|
||||
|
||||
const mockPalette2: jest.Mocked<PaletteDefinition> = {
|
||||
id: 'mocked',
|
||||
title: 'Mocked Palette',
|
||||
getColor: jest.fn((_: SeriesLayer[]) => 'blue'),
|
||||
getColors: jest.fn((num: number) => ['blue', 'yellow']),
|
||||
toExpression: jest.fn(() => ({
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'system_palette',
|
||||
arguments: {
|
||||
name: ['mocked'],
|
||||
},
|
||||
},
|
||||
],
|
||||
})),
|
||||
};
|
||||
|
||||
return {
|
||||
get: (_: string) => mockPalette,
|
||||
getAll: () => [mockPalette],
|
||||
get: (name: string) => (name !== 'default' ? mockPalette2 : mockPalette1),
|
||||
getAll: () => [mockPalette1, mockPalette2],
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -284,7 +284,7 @@ describe('Datatable Visualization', () => {
|
|||
state: { layers: [layer] },
|
||||
frame,
|
||||
}).groups[1].accessors
|
||||
).toEqual(['c', 'b']);
|
||||
).toEqual([{ columnId: 'c' }, { columnId: 'b' }]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -149,9 +149,9 @@ export const datatableVisualization: Visualization<DatatableVisualizationState>
|
|||
defaultMessage: 'Break down by',
|
||||
}),
|
||||
layerId: state.layers[0].layerId,
|
||||
accessors: sortedColumns.filter(
|
||||
(c) => datasource!.getOperationForColumnId(c)?.isBucketed
|
||||
),
|
||||
accessors: sortedColumns
|
||||
.filter((c) => datasource!.getOperationForColumnId(c)?.isBucketed)
|
||||
.map((accessor) => ({ columnId: accessor })),
|
||||
supportsMoreColumns: true,
|
||||
filterOperations: (op) => op.isBucketed,
|
||||
dataTestSubj: 'lnsDatatable_column',
|
||||
|
@ -162,9 +162,9 @@ export const datatableVisualization: Visualization<DatatableVisualizationState>
|
|||
defaultMessage: 'Metrics',
|
||||
}),
|
||||
layerId: state.layers[0].layerId,
|
||||
accessors: sortedColumns.filter(
|
||||
(c) => !datasource!.getOperationForColumnId(c)?.isBucketed
|
||||
),
|
||||
accessors: sortedColumns
|
||||
.filter((c) => !datasource!.getOperationForColumnId(c)?.isBucketed)
|
||||
.map((accessor) => ({ columnId: accessor })),
|
||||
supportsMoreColumns: true,
|
||||
filterOperations: (op) => !op.isBucketed,
|
||||
required: true,
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
// Drop area will be replacing existing content
|
||||
.lnsDragDrop-isReplacing {
|
||||
&,
|
||||
.lnsLayerPanel__triggerLink {
|
||||
.lnsLayerPanel__triggerText {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AccessorConfig } from '../../../types';
|
||||
|
||||
export function ColorIndicator({
|
||||
accessorConfig,
|
||||
children,
|
||||
}: {
|
||||
accessorConfig: AccessorConfig;
|
||||
children: React.ReactChild;
|
||||
}) {
|
||||
let indicatorIcon = null;
|
||||
if (accessorConfig.triggerIcon && accessorConfig.triggerIcon !== 'none') {
|
||||
const baseIconProps = {
|
||||
size: 's',
|
||||
className: 'lnsLayerPanel__colorIndicator',
|
||||
} as const;
|
||||
|
||||
indicatorIcon = (
|
||||
<EuiFlexItem grow={false}>
|
||||
{accessorConfig.triggerIcon === 'color' && accessorConfig.color && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
color={accessorConfig.color}
|
||||
type="stopFilled"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.colorIndicatorLabel', {
|
||||
defaultMessage: 'Color of this dimension: {hex}',
|
||||
values: {
|
||||
hex: accessorConfig.color,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{accessorConfig.triggerIcon === 'disabled' && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
type="stopSlash"
|
||||
color="subdued"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.noColorIndicatorLabel', {
|
||||
defaultMessage: 'This dimension does not have an individual color',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{accessorConfig.triggerIcon === 'colorBy' && (
|
||||
<EuiIcon
|
||||
{...baseIconProps}
|
||||
type="brush"
|
||||
color="text"
|
||||
aria-label={i18n.translate('xpack.lens.editorFrame.paletteColorIndicatorLabel', {
|
||||
defaultMessage: 'This dimension is using a palette',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="none" alignItems="center">
|
||||
{indicatorIcon}
|
||||
<EuiFlexItem>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -52,6 +52,7 @@
|
|||
align-items: center;
|
||||
overflow: hidden;
|
||||
min-height: $euiSizeXXL;
|
||||
position: relative;
|
||||
|
||||
// NativeRenderer is messing this up
|
||||
> div {
|
||||
|
@ -80,28 +81,18 @@
|
|||
margin-right: $euiSizeS;
|
||||
}
|
||||
|
||||
.lnsLayerPanel__triggerLink {
|
||||
.lnsLayerPanel__triggerText {
|
||||
width: 100%;
|
||||
padding: $euiSizeS;
|
||||
min-height: $euiSizeXXL - 2;
|
||||
word-break: break-word;
|
||||
|
||||
&:focus {
|
||||
background-color: transparent !important; // sass-lint:disable-line no-important
|
||||
outline: none !important; // sass-lint:disable-line no-important
|
||||
}
|
||||
|
||||
&:focus .lnsLayerPanel__triggerLinkLabel,
|
||||
&:focus-within .lnsLayerPanel__triggerLinkLabel {
|
||||
background-color: transparentize($euiColorVis1, .9);
|
||||
}
|
||||
}
|
||||
|
||||
.lnsLayerPanel__triggerLinkLabel {
|
||||
.lnsLayerPanel__triggerTextLabel {
|
||||
transition: background-color $euiAnimSpeedFast ease-in-out;
|
||||
}
|
||||
|
||||
.lnsLayerPanel__triggerLinkContent {
|
||||
.lnsLayerPanel__triggerTextContent {
|
||||
// Make EUI button content not centered
|
||||
justify-content: flex-start;
|
||||
padding: 0 !important; // sass-lint:disable-line no-important
|
||||
|
@ -111,3 +102,32 @@
|
|||
.lnsLayerPanel__styleEditor {
|
||||
padding: 0 $euiSizeS $euiSizeS;
|
||||
}
|
||||
|
||||
.lnsLayerPanel__colorIndicator {
|
||||
margin-left: $euiSizeS;
|
||||
}
|
||||
|
||||
.lnsLayerPanel__paletteContainer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.lnsLayerPanel__paletteColor {
|
||||
height: $euiSizeXS;
|
||||
}
|
||||
|
||||
.lnsLayerPanel__dimensionLink {
|
||||
width: 100%;
|
||||
|
||||
&:focus {
|
||||
background-color: transparent !important; // sass-lint:disable-line no-important
|
||||
outline: none !important; // sass-lint:disable-line no-important
|
||||
}
|
||||
|
||||
&:focus .lnsLayerPanel__triggerTextLabel,
|
||||
&:focus-within .lnsLayerPanel__triggerTextLabel {
|
||||
background-color: transparentize($euiColorVis1, .9);
|
||||
}
|
||||
}
|
|
@ -137,7 +137,7 @@ describe('LayerPanel', () => {
|
|||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: ['x'],
|
||||
accessors: [{ columnId: 'x' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: false,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
|
@ -177,7 +177,7 @@ describe('LayerPanel', () => {
|
|||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: ['x'],
|
||||
accessors: [{ columnId: 'x' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: false,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
|
@ -209,7 +209,7 @@ describe('LayerPanel', () => {
|
|||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: ['newid'],
|
||||
accessors: [{ columnId: 'newid' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
|
@ -257,7 +257,7 @@ describe('LayerPanel', () => {
|
|||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: ['newid'],
|
||||
accessors: [{ columnId: 'newid' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: false,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
|
@ -302,7 +302,7 @@ describe('LayerPanel', () => {
|
|||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: ['newid'],
|
||||
accessors: [{ columnId: 'newid' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: false,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
|
@ -377,7 +377,7 @@ describe('LayerPanel', () => {
|
|||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: ['a'],
|
||||
accessors: [{ columnId: 'a' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
|
@ -416,7 +416,7 @@ describe('LayerPanel', () => {
|
|||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: ['a'],
|
||||
accessors: [{ columnId: 'a' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: false,
|
||||
dataTestSubj: 'lnsGroupA',
|
||||
|
@ -424,7 +424,7 @@ describe('LayerPanel', () => {
|
|||
{
|
||||
groupLabel: 'B',
|
||||
groupId: 'b',
|
||||
accessors: ['b'],
|
||||
accessors: [{ columnId: 'b' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroupB',
|
||||
|
@ -480,7 +480,7 @@ describe('LayerPanel', () => {
|
|||
{
|
||||
groupLabel: 'A',
|
||||
groupId: 'a',
|
||||
accessors: ['a', 'b'],
|
||||
accessors: [{ columnId: 'a' }, { columnId: 'b' }],
|
||||
filterOperations: () => true,
|
||||
supportsMoreColumns: true,
|
||||
dataTestSubj: 'lnsGroup',
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
EuiFormRow,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -25,6 +26,8 @@ import { trackUiEvent } from '../../../lens_ui_telemetry';
|
|||
import { generateId } from '../../../id_generator';
|
||||
import { ConfigPanelWrapperProps, ActiveDimensionState } from './types';
|
||||
import { DimensionContainer } from './dimension_container';
|
||||
import { ColorIndicator } from './color_indicator';
|
||||
import { PaletteIndicator } from './palette_indicator';
|
||||
|
||||
const initialActiveDimensionState = {
|
||||
isNew: false,
|
||||
|
@ -181,6 +184,10 @@ export function LayerPanel(
|
|||
const newId = generateId();
|
||||
const isMissing = !isEmptyLayer && group.required && group.accessors.length === 0;
|
||||
|
||||
const triggerLinkA11yText = i18n.translate('xpack.lens.configure.editConfig', {
|
||||
defaultMessage: 'Click to edit configuration or drag to move',
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiFormRow
|
||||
className={
|
||||
|
@ -207,7 +214,8 @@ export function LayerPanel(
|
|||
>
|
||||
<>
|
||||
<ReorderProvider id={group.groupId} className={'lnsLayerPanel__group'}>
|
||||
{group.accessors.map((accessor) => {
|
||||
{group.accessors.map((accessorConfig) => {
|
||||
const accessor = accessorConfig.columnId;
|
||||
const { dragging } = dragDropContext;
|
||||
const dragType =
|
||||
isDraggedOperation(dragging) && accessor === dragging.columnId
|
||||
|
@ -253,7 +261,9 @@ export function LayerPanel(
|
|||
dragType={dragType}
|
||||
dropType={dropType}
|
||||
data-test-subj={group.dataTestSubj}
|
||||
itemsInGroup={group.accessors}
|
||||
itemsInGroup={group.accessors.map((a) =>
|
||||
typeof a === 'string' ? a : a.columnId
|
||||
)}
|
||||
className={'lnsLayerPanel__dimensionContainer'}
|
||||
value={{
|
||||
columnId: accessor,
|
||||
|
@ -304,25 +314,33 @@ export function LayerPanel(
|
|||
}}
|
||||
>
|
||||
<div className="lnsLayerPanel__dimension">
|
||||
<NativeRenderer
|
||||
render={props.datasourceMap[datasourceId].renderDimensionTrigger}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
onClick: () => {
|
||||
if (activeId) {
|
||||
setActiveDimension(initialActiveDimensionState);
|
||||
} else {
|
||||
setActiveDimension({
|
||||
isNew: false,
|
||||
activeGroup: group,
|
||||
activeId: accessor,
|
||||
});
|
||||
}
|
||||
},
|
||||
<EuiLink
|
||||
className="lnsLayerPanel__dimensionLink"
|
||||
onClick={() => {
|
||||
if (activeId) {
|
||||
setActiveDimension(initialActiveDimensionState);
|
||||
} else {
|
||||
setActiveDimension({
|
||||
isNew: false,
|
||||
activeGroup: group,
|
||||
activeId: accessor,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
aria-label={triggerLinkA11yText}
|
||||
title={triggerLinkA11yText}
|
||||
>
|
||||
<ColorIndicator accessorConfig={accessorConfig}>
|
||||
<NativeRenderer
|
||||
render={props.datasourceMap[datasourceId].renderDimensionTrigger}
|
||||
nativeProps={{
|
||||
...layerDatasourceConfigProps,
|
||||
columnId: accessor,
|
||||
filterOperations: group.filterOperations,
|
||||
}}
|
||||
/>
|
||||
</ColorIndicator>
|
||||
</EuiLink>
|
||||
<EuiButtonIcon
|
||||
className="lnsLayerPanel__dimensionRemove"
|
||||
data-test-subj="indexPattern-dimension-remove"
|
||||
|
@ -356,6 +374,7 @@ export function LayerPanel(
|
|||
);
|
||||
}}
|
||||
/>
|
||||
<PaletteIndicator accessorConfig={accessorConfig} />
|
||||
</div>
|
||||
</DragDrop>
|
||||
);
|
||||
|
@ -409,12 +428,12 @@ export function LayerPanel(
|
|||
>
|
||||
<div className="lnsLayerPanel__dimension lnsLayerPanel__dimension--empty">
|
||||
<EuiButtonEmpty
|
||||
className="lnsLayerPanel__triggerLink"
|
||||
className="lnsLayerPanel__triggerText"
|
||||
color="text"
|
||||
size="xs"
|
||||
iconType="plusInCircleFilled"
|
||||
contentProps={{
|
||||
className: 'lnsLayerPanel__triggerLinkContent',
|
||||
className: 'lnsLayerPanel__triggerTextContent',
|
||||
}}
|
||||
data-test-subj="lns-empty-dimension"
|
||||
onClick={() => {
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { AccessorConfig } from '../../../types';
|
||||
|
||||
export function PaletteIndicator({ accessorConfig }: { accessorConfig: AccessorConfig }) {
|
||||
if (accessorConfig.triggerIcon !== 'colorBy' || !accessorConfig.palette) return null;
|
||||
return (
|
||||
<EuiFlexGroup className="lnsLayerPanel__paletteContainer" gutterSize="none" alignItems="center">
|
||||
{accessorConfig.palette.map((color) => (
|
||||
<EuiFlexItem
|
||||
key={color}
|
||||
className="lnsLayerPanel__paletteColor"
|
||||
grow={true}
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiLink, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiText, EuiIcon, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
|
||||
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
|
||||
import { DatasourceDimensionTriggerProps, DatasourceDimensionEditorProps } from '../../types';
|
||||
|
@ -66,10 +66,6 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens
|
|||
}
|
||||
const formattedLabel = wrapOnDot(uniqueLabel);
|
||||
|
||||
const triggerLinkA11yText = i18n.translate('xpack.lens.configure.editConfig', {
|
||||
defaultMessage: 'Click to edit configuration or drag to move',
|
||||
});
|
||||
|
||||
if (currentFieldIsInvalid) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
|
@ -86,14 +82,12 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens
|
|||
}
|
||||
anchorClassName="eui-displayBlock"
|
||||
>
|
||||
<EuiLink
|
||||
<EuiText
|
||||
size="s"
|
||||
color="danger"
|
||||
id={columnId}
|
||||
className="lnsLayerPanel__triggerLink"
|
||||
onClick={props.onClick}
|
||||
className="lnsLayerPanel__triggerText"
|
||||
data-test-subj="lns-dimensionTrigger"
|
||||
aria-label={triggerLinkA11yText}
|
||||
title={triggerLinkA11yText}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -101,26 +95,24 @@ export const IndexPatternDimensionTriggerComponent = function IndexPatternDimens
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={true}>{selectedColumn.label}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiLink
|
||||
<EuiText
|
||||
size="s"
|
||||
id={columnId}
|
||||
className="lnsLayerPanel__triggerLink"
|
||||
onClick={props.onClick}
|
||||
className="lnsLayerPanel__triggerText"
|
||||
data-test-subj="lns-dimensionTrigger"
|
||||
aria-label={triggerLinkA11yText}
|
||||
title={triggerLinkA11yText}
|
||||
>
|
||||
<EuiFlexItem grow={true}>
|
||||
<span>
|
||||
<span className="lnsLayerPanel__triggerLinkLabel">{formattedLabel}</span>
|
||||
<span className="lnsLayerPanel__triggerTextLabel">{formattedLabel}</span>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
</EuiLink>
|
||||
</EuiText>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ export const metricVisualization: Visualization<State> = {
|
|||
groupId: 'metric',
|
||||
groupLabel: i18n.translate('xpack.lens.metric.label', { defaultMessage: 'Metric' }),
|
||||
layerId: props.state.layerId,
|
||||
accessors: props.state.accessor ? [props.state.accessor] : [],
|
||||
accessors: props.state.accessor ? [{ columnId: props.state.accessor }] : [],
|
||||
supportsMoreColumns: !props.state.accessor,
|
||||
filterOperations: (op: OperationMetadata) => !op.isBucketed && op.dataType === 'number',
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@ 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 } from '../types';
|
||||
import { Visualization, OperationMetadata, AccessorConfig } from '../types';
|
||||
import { toExpression, toPreviewExpression } from './to_expression';
|
||||
import { LayerState, PieVisualizationState } from './types';
|
||||
import { suggestions } from './suggestions';
|
||||
|
@ -113,7 +113,18 @@ export const getPieVisualization = ({
|
|||
.map(({ columnId }) => columnId)
|
||||
.filter((columnId) => columnId !== layer.metric);
|
||||
// When we add a column it could be empty, and therefore have no order
|
||||
const sortedColumns = Array.from(new Set(originalOrder.concat(layer.groups)));
|
||||
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 {
|
||||
|
@ -137,7 +148,7 @@ export const getPieVisualization = ({
|
|||
defaultMessage: 'Size by',
|
||||
}),
|
||||
layerId,
|
||||
accessors: layer.metric ? [layer.metric] : [],
|
||||
accessors: layer.metric ? [{ columnId: layer.metric }] : [],
|
||||
supportsMoreColumns: !layer.metric,
|
||||
filterOperations: numberMetricOperations,
|
||||
required: true,
|
||||
|
@ -168,7 +179,7 @@ export const getPieVisualization = ({
|
|||
defaultMessage: 'Size by',
|
||||
}),
|
||||
layerId,
|
||||
accessors: layer.metric ? [layer.metric] : [],
|
||||
accessors: layer.metric ? [{ columnId: layer.metric }] : [],
|
||||
supportsMoreColumns: !layer.metric,
|
||||
filterOperations: numberMetricOperations,
|
||||
required: true,
|
||||
|
|
|
@ -242,7 +242,6 @@ export type DatasourceDimensionEditorProps<T = unknown> = DatasourceDimensionPro
|
|||
|
||||
export type DatasourceDimensionTriggerProps<T> = DatasourceDimensionProps<T> & {
|
||||
dragDropContext: DragContextState;
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export interface DatasourceLayerPanelProps<T> {
|
||||
|
@ -341,12 +340,19 @@ export type VisualizationDimensionEditorProps<T = unknown> = VisualizationConfig
|
|||
setState: (newState: T) => void;
|
||||
};
|
||||
|
||||
export interface AccessorConfig {
|
||||
columnId: string;
|
||||
triggerIcon?: 'color' | 'disabled' | 'colorBy' | 'none';
|
||||
color?: string;
|
||||
palette?: string[];
|
||||
}
|
||||
|
||||
export type VisualizationDimensionGroupConfig = SharedDimensionProps & {
|
||||
groupLabel: string;
|
||||
|
||||
/** ID is passed back to visualization. For example, `x` */
|
||||
groupId: string;
|
||||
accessors: string[];
|
||||
accessors: AccessorConfig[];
|
||||
supportsMoreColumns: boolean;
|
||||
/** If required, a warning will appear if accessors are empty */
|
||||
required?: boolean;
|
||||
|
|
|
@ -128,6 +128,31 @@ describe('color_assignment', () => {
|
|||
expect(assignments.palette2.totalSeriesCount).toEqual(2 * 3);
|
||||
expect(formatMock).toHaveBeenCalledWith(complexObject);
|
||||
});
|
||||
|
||||
it('should handle missing tables', () => {
|
||||
const assignments = getColorAssignments(layers, { ...data, tables: {} }, formatFactory);
|
||||
// if there is no data, just assume a single split
|
||||
expect(assignments.palette1.totalSeriesCount).toEqual(2);
|
||||
});
|
||||
|
||||
it('should handle missing columns', () => {
|
||||
const assignments = getColorAssignments(
|
||||
layers,
|
||||
{
|
||||
...data,
|
||||
tables: {
|
||||
...data.tables,
|
||||
'1': {
|
||||
...data.tables['1'],
|
||||
columns: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
formatFactory
|
||||
);
|
||||
// if the split column is missing, just assume a single split
|
||||
expect(assignments.palette1.totalSeriesCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRank', () => {
|
||||
|
@ -178,5 +203,30 @@ describe('color_assignment', () => {
|
|||
// 3 series in front of (complex object)/y1 - abc/y1, abc/y2
|
||||
expect(assignments.palette1.getRank(layers[0], 'formatted', 'y1')).toEqual(2);
|
||||
});
|
||||
|
||||
it('should handle missing tables', () => {
|
||||
const assignments = getColorAssignments(layers, { ...data, tables: {} }, formatFactory);
|
||||
// if there is no data, assume it is the first splitted series. One series in front - 0/y1
|
||||
expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should handle missing columns', () => {
|
||||
const assignments = getColorAssignments(
|
||||
layers,
|
||||
{
|
||||
...data,
|
||||
tables: {
|
||||
...data.tables,
|
||||
'1': {
|
||||
...data.tables['1'],
|
||||
columns: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
formatFactory
|
||||
);
|
||||
// if the split column is missing, assume it is the first splitted series. One series in front - 0/y1
|
||||
expect(assignments.palette1.getRank(layers[0], '2', 'y2')).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,20 +5,36 @@
|
|||
*/
|
||||
|
||||
import { uniq, mapValues } from 'lodash';
|
||||
import { FormatFactory, LensMultiTable } from '../types';
|
||||
import { LayerArgs, LayerConfig } from './types';
|
||||
import { PaletteOutput } from 'src/plugins/charts/public';
|
||||
import { Datatable } from 'src/plugins/expressions';
|
||||
import { FormatFactory } from '../types';
|
||||
|
||||
const isPrimitive = (value: unknown): boolean => value != null && typeof value !== 'object';
|
||||
|
||||
interface LayerColorConfig {
|
||||
palette?: PaletteOutput;
|
||||
splitAccessor?: string;
|
||||
accessors: string[];
|
||||
layerId: string;
|
||||
}
|
||||
|
||||
export type ColorAssignments = Record<
|
||||
string,
|
||||
{
|
||||
totalSeriesCount: number;
|
||||
getRank(layer: LayerColorConfig, seriesKey: string, yAccessor: string): number;
|
||||
}
|
||||
>;
|
||||
|
||||
export function getColorAssignments(
|
||||
layers: LayerArgs[],
|
||||
data: LensMultiTable,
|
||||
layers: LayerColorConfig[],
|
||||
data: { tables: Record<string, Datatable> },
|
||||
formatFactory: FormatFactory
|
||||
) {
|
||||
const layersPerPalette: Record<string, LayerConfig[]> = {};
|
||||
): ColorAssignments {
|
||||
const layersPerPalette: Record<string, LayerColorConfig[]> = {};
|
||||
|
||||
layers.forEach((layer) => {
|
||||
const palette = layer.palette?.name || 'palette';
|
||||
const palette = layer.palette?.name || 'default';
|
||||
if (!layersPerPalette[palette]) {
|
||||
layersPerPalette[palette] = [];
|
||||
}
|
||||
|
@ -31,18 +47,21 @@ export function getColorAssignments(
|
|||
return { numberOfSeries: layer.accessors.length, splits: [] };
|
||||
}
|
||||
const splitAccessor = layer.splitAccessor;
|
||||
const column = data.tables[layer.layerId].columns.find(({ id }) => id === splitAccessor)!;
|
||||
const splits = uniq(
|
||||
data.tables[layer.layerId].rows.map((row) => {
|
||||
let value = row[splitAccessor];
|
||||
if (value && !isPrimitive(value)) {
|
||||
value = formatFactory(column.meta.params).convert(value);
|
||||
} else {
|
||||
value = String(value);
|
||||
}
|
||||
return value;
|
||||
})
|
||||
);
|
||||
const column = data.tables[layer.layerId]?.columns.find(({ id }) => id === splitAccessor);
|
||||
const splits =
|
||||
!column || !data.tables[layer.layerId]
|
||||
? []
|
||||
: uniq(
|
||||
data.tables[layer.layerId].rows.map((row) => {
|
||||
let value = row[splitAccessor];
|
||||
if (value && !isPrimitive(value)) {
|
||||
value = formatFactory(column.meta.params).convert(value);
|
||||
} else {
|
||||
value = String(value);
|
||||
}
|
||||
return value;
|
||||
})
|
||||
);
|
||||
return { numberOfSeries: (splits.length || 1) * layer.accessors.length, splits };
|
||||
});
|
||||
const totalSeriesCount = seriesPerLayer.reduce(
|
||||
|
@ -51,18 +70,17 @@ export function getColorAssignments(
|
|||
);
|
||||
return {
|
||||
totalSeriesCount,
|
||||
getRank(layer: LayerArgs, seriesKey: string, yAccessor: string) {
|
||||
getRank(layer: LayerColorConfig, seriesKey: string, yAccessor: string) {
|
||||
const layerIndex = paletteLayers.indexOf(layer);
|
||||
const currentSeriesPerLayer = seriesPerLayer[layerIndex];
|
||||
const splitRank = currentSeriesPerLayer.splits.indexOf(seriesKey);
|
||||
return (
|
||||
(layerIndex === 0
|
||||
? 0
|
||||
: seriesPerLayer
|
||||
.slice(0, layerIndex)
|
||||
.reduce((sum, perLayer) => sum + perLayer.numberOfSeries, 0)) +
|
||||
(layer.splitAccessor
|
||||
? currentSeriesPerLayer.splits.indexOf(seriesKey) * layer.accessors.length
|
||||
: 0) +
|
||||
(layer.splitAccessor && splitRank !== -1 ? splitRank * layer.accessors.length : 0) +
|
||||
layer.accessors.indexOf(yAccessor)
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1386,13 +1386,13 @@ describe('xy_expression', () => {
|
|||
yAccessor: 'a',
|
||||
seriesKeys: ['a'],
|
||||
})
|
||||
).toEqual('black');
|
||||
).toEqual('blue');
|
||||
expect(
|
||||
(component.find(LineSeries).at(1).prop('color') as Function)!({
|
||||
yAccessor: 'c',
|
||||
seriesKeys: ['c'],
|
||||
})
|
||||
).toEqual('black');
|
||||
).toEqual('blue');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'
|
|||
import { UI_SETTINGS } from '../../../../../src/plugins/data/public';
|
||||
import { EditorFrameSetup, FormatFactory } from '../types';
|
||||
import { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
|
||||
import { LensPluginStartDependencies } from '../plugin';
|
||||
|
||||
export interface XyVisualizationPluginSetupPlugins {
|
||||
expressions: ExpressionsSetup;
|
||||
|
@ -31,7 +32,7 @@ export class XyVisualization {
|
|||
constructor() {}
|
||||
|
||||
setup(
|
||||
core: CoreSetup,
|
||||
core: CoreSetup<LensPluginStartDependencies, void>,
|
||||
{ expressions, formatFactory, editorFrame, charts }: XyVisualizationPluginSetupPlugins
|
||||
) {
|
||||
editorFrame.registerVisualization(async () => {
|
||||
|
@ -46,6 +47,7 @@ export class XyVisualization {
|
|||
getXyChartRenderer,
|
||||
getXyVisualization,
|
||||
} = await import('../async_services');
|
||||
const [, { data }] = await core.getStartServices();
|
||||
const palettes = await charts.palettes.getPalettes();
|
||||
expressions.registerFunction(() => legendConfig);
|
||||
expressions.registerFunction(() => yAxisConfig);
|
||||
|
@ -64,7 +66,7 @@ export class XyVisualization {
|
|||
histogramBarTarget: core.uiSettings.get<number>(UI_SETTINGS.HISTOGRAM_BAR_TARGET),
|
||||
})
|
||||
);
|
||||
return getXyVisualization({ paletteService: palettes });
|
||||
return getXyVisualization({ paletteService: palettes, data });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
|
||||
import { FramePublicAPI } from '../types';
|
||||
import { FramePublicAPI, DatasourcePublicAPI } from '../types';
|
||||
import { SeriesType, visualizationTypes, LayerConfig, YConfig, ValidLayer } from './types';
|
||||
|
||||
export function isHorizontalSeries(seriesType: SeriesType) {
|
||||
|
@ -39,6 +39,18 @@ export const getSeriesColor = (layer: LayerConfig, accessor: string) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const getColumnToLabelMap = (layer: LayerConfig, datasource: DatasourcePublicAPI) => {
|
||||
const columnToLabel: Record<string, string> = {};
|
||||
|
||||
layer.accessors.concat(layer.splitAccessor ? [layer.splitAccessor] : []).forEach((accessor) => {
|
||||
const operation = datasource.getOperationForColumnId(accessor);
|
||||
if (operation?.label) {
|
||||
columnToLabel[accessor] = operation.label;
|
||||
}
|
||||
});
|
||||
return columnToLabel;
|
||||
};
|
||||
|
||||
export function hasHistogramSeries(
|
||||
layers: ValidLayer[] = [],
|
||||
datasourceLayers?: FramePublicAPI['datasourceLayers']
|
||||
|
|
|
@ -10,10 +10,12 @@ import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'
|
|||
import { getXyVisualization } from './xy_visualization';
|
||||
import { Operation } from '../types';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
|
||||
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
|
||||
|
||||
describe('#toExpression', () => {
|
||||
const xyVisualization = getXyVisualization({
|
||||
paletteService: chartPluginMock.createPaletteRegistry(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
});
|
||||
let mockDatasource: ReturnType<typeof createMockDatasource>;
|
||||
let frame: ReturnType<typeof createMockFramePublicAPI>;
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ScaleType } from '@elastic/charts';
|
|||
import { PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { State, ValidLayer, LayerConfig } from './types';
|
||||
import { OperationMetadata, DatasourcePublicAPI } from '../types';
|
||||
import { getColumnToLabelMap } from './state_helpers';
|
||||
|
||||
export const getSortedAccessors = (datasource: DatasourcePublicAPI, layer: LayerConfig) => {
|
||||
const originalOrder = datasource
|
||||
|
@ -196,17 +197,7 @@ export const buildExpression = (
|
|||
],
|
||||
valueLabels: [state?.valueLabels || 'hide'],
|
||||
layers: validLayers.map((layer) => {
|
||||
const columnToLabel: Record<string, string> = {};
|
||||
|
||||
const datasource = datasourceLayers[layer.layerId];
|
||||
layer.accessors
|
||||
.concat(layer.splitAccessor ? [layer.splitAccessor] : [])
|
||||
.forEach((accessor) => {
|
||||
const operation = datasource.getOperationForColumnId(accessor);
|
||||
if (operation?.label) {
|
||||
columnToLabel[accessor] = operation.label;
|
||||
}
|
||||
});
|
||||
const columnToLabel = getColumnToLabelMap(layer, datasourceLayers[layer.layerId]);
|
||||
|
||||
const xAxisOperation =
|
||||
datasourceLayers &&
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
import { getXyVisualization } from './visualization';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { Operation } from '../types';
|
||||
import { State, SeriesType } from './types';
|
||||
import { State, SeriesType, LayerConfig } from './types';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../editor_frame_service/mocks';
|
||||
import { LensIconChartBar } from '../assets/chart_bar';
|
||||
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
|
||||
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
|
||||
|
||||
function exampleState(): State {
|
||||
return {
|
||||
|
@ -28,9 +29,12 @@ function exampleState(): State {
|
|||
],
|
||||
};
|
||||
}
|
||||
const paletteServiceMock = chartPluginMock.createPaletteRegistry();
|
||||
const dataMock = dataPluginMock.createStartContract();
|
||||
|
||||
const xyVisualization = getXyVisualization({
|
||||
paletteService: chartPluginMock.createPaletteRegistry(),
|
||||
paletteService: paletteServiceMock,
|
||||
data: dataMock,
|
||||
});
|
||||
|
||||
describe('xy_visualization', () => {
|
||||
|
@ -307,6 +311,14 @@ describe('xy_visualization', () => {
|
|||
frame.datasourceLayers = {
|
||||
first: mockDatasource.publicAPIMock,
|
||||
};
|
||||
|
||||
frame.activeData = {
|
||||
first: {
|
||||
type: 'datatable',
|
||||
rows: [],
|
||||
columns: [],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should return options for 3 dimensions', () => {
|
||||
|
@ -408,6 +420,145 @@ describe('xy_visualization', () => {
|
|||
];
|
||||
expect(ops.filter(filterOperations).map((x) => x.dataType)).toEqual(['number']);
|
||||
});
|
||||
|
||||
describe('color assignment', () => {
|
||||
function callConfig(layerConfigOverride: Partial<LayerConfig>) {
|
||||
const baseState = exampleState();
|
||||
const options = xyVisualization.getConfiguration({
|
||||
state: {
|
||||
...baseState,
|
||||
layers: [
|
||||
{
|
||||
...baseState.layers[0],
|
||||
splitAccessor: undefined,
|
||||
...layerConfigOverride,
|
||||
},
|
||||
],
|
||||
},
|
||||
frame,
|
||||
layerId: 'first',
|
||||
}).groups;
|
||||
return options;
|
||||
}
|
||||
|
||||
function callConfigForYConfigs(layerConfigOverride: Partial<LayerConfig>) {
|
||||
return callConfig(layerConfigOverride).find(({ groupId }) => groupId === 'y');
|
||||
}
|
||||
|
||||
function callConfigForBreakdownConfigs(layerConfigOverride: Partial<LayerConfig>) {
|
||||
return callConfig(layerConfigOverride).find(({ groupId }) => groupId === 'breakdown');
|
||||
}
|
||||
|
||||
function callConfigAndFindYConfig(
|
||||
layerConfigOverride: Partial<LayerConfig>,
|
||||
assertionAccessor: string
|
||||
) {
|
||||
const accessorConfig = callConfigForYConfigs(layerConfigOverride)?.accessors.find(
|
||||
(accessor) => typeof accessor !== 'string' && accessor.columnId === assertionAccessor
|
||||
);
|
||||
if (!accessorConfig || typeof accessorConfig === 'string') {
|
||||
throw new Error('could not find accessor');
|
||||
}
|
||||
return accessorConfig;
|
||||
}
|
||||
|
||||
it('should pass custom y color in accessor config', () => {
|
||||
const accessorConfig = callConfigAndFindYConfig(
|
||||
{
|
||||
yConfig: [
|
||||
{
|
||||
forAccessor: 'b',
|
||||
color: 'red',
|
||||
},
|
||||
],
|
||||
},
|
||||
'b'
|
||||
);
|
||||
expect(accessorConfig.triggerIcon).toEqual('color');
|
||||
expect(accessorConfig.color).toEqual('red');
|
||||
});
|
||||
|
||||
it('should query palette to fill in colors for other dimensions', () => {
|
||||
const palette = paletteServiceMock.get('default');
|
||||
(palette.getColor as jest.Mock).mockClear();
|
||||
const accessorConfig = callConfigAndFindYConfig({}, 'c');
|
||||
expect(accessorConfig.triggerIcon).toEqual('color');
|
||||
// black is the color returned from the palette mock
|
||||
expect(accessorConfig.color).toEqual('black');
|
||||
expect(palette.getColor).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
name: 'c',
|
||||
// rank 1 because it's the second y metric
|
||||
rankAtDepth: 1,
|
||||
totalSeriesAtDepth: 2,
|
||||
},
|
||||
],
|
||||
{ maxDepth: 1, totalSeries: 2 },
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass name of current series along', () => {
|
||||
(frame.datasourceLayers.first.getOperationForColumnId as jest.Mock).mockReturnValue({
|
||||
label: 'Overwritten label',
|
||||
});
|
||||
const palette = paletteServiceMock.get('default');
|
||||
(palette.getColor as jest.Mock).mockClear();
|
||||
callConfigAndFindYConfig({}, 'c');
|
||||
expect(palette.getColor).toHaveBeenCalledWith(
|
||||
[
|
||||
expect.objectContaining({
|
||||
name: 'Overwritten label',
|
||||
}),
|
||||
],
|
||||
expect.anything(),
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
it('should use custom palette if layer contains palette', () => {
|
||||
const palette = paletteServiceMock.get('mock');
|
||||
callConfigAndFindYConfig(
|
||||
{
|
||||
palette: { type: 'palette', name: 'mock', params: {} },
|
||||
},
|
||||
'c'
|
||||
);
|
||||
expect(palette.getColor).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not show any indicator as long as there is no data', () => {
|
||||
frame.activeData = undefined;
|
||||
const yConfigs = callConfigForYConfigs({});
|
||||
expect(yConfigs!.accessors.length).toEqual(2);
|
||||
yConfigs!.accessors.forEach((accessor) => {
|
||||
expect(accessor.triggerIcon).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show disable icon for splitted series', () => {
|
||||
const accessorConfig = callConfigAndFindYConfig(
|
||||
{
|
||||
splitAccessor: 'd',
|
||||
},
|
||||
'b'
|
||||
);
|
||||
expect(accessorConfig.triggerIcon).toEqual('disabled');
|
||||
});
|
||||
|
||||
it('should show current palette for break down by dimension', () => {
|
||||
const palette = paletteServiceMock.get('mock');
|
||||
const customColors = ['yellow', 'green'];
|
||||
(palette.getColors as jest.Mock).mockReturnValue(customColors);
|
||||
const breakdownConfig = callConfigForBreakdownConfigs({
|
||||
palette: { type: 'palette', name: 'mock', params: {} },
|
||||
splitAccessor: 'd',
|
||||
});
|
||||
const accessorConfig = breakdownConfig!.accessors[0];
|
||||
expect(typeof accessorConfig !== 'string' && accessorConfig.palette).toEqual(customColors);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getErrorMessages', () => {
|
||||
|
|
|
@ -10,16 +10,24 @@ import { render } from 'react-dom';
|
|||
import { Position } from '@elastic/charts';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { PaletteOutput, PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { DataPublicPluginStart } from 'src/plugins/data/public';
|
||||
import { getSuggestions } from './xy_suggestions';
|
||||
import { LayerContextMenu, XyToolbar, DimensionEditor } from './xy_config_panel';
|
||||
import { Visualization, OperationMetadata, VisualizationType } from '../types';
|
||||
import {
|
||||
Visualization,
|
||||
OperationMetadata,
|
||||
VisualizationType,
|
||||
AccessorConfig,
|
||||
FramePublicAPI,
|
||||
} from '../types';
|
||||
import { State, SeriesType, visualizationTypes, LayerConfig } from './types';
|
||||
import { isHorizontalChart } from './state_helpers';
|
||||
import { getColumnToLabelMap, isHorizontalChart } from './state_helpers';
|
||||
import { toExpression, toPreviewExpression, getSortedAccessors } from './to_expression';
|
||||
import { LensIconChartBarStacked } from '../assets/chart_bar_stacked';
|
||||
import { LensIconChartMixedXy } from '../assets/chart_mixed_xy';
|
||||
import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal';
|
||||
import { ColorAssignments, getColorAssignments } from './color_assignment';
|
||||
|
||||
const defaultIcon = LensIconChartBarStacked;
|
||||
const defaultSeriesType = 'bar_stacked';
|
||||
|
@ -76,8 +84,10 @@ function getDescription(state?: State) {
|
|||
|
||||
export const getXyVisualization = ({
|
||||
paletteService,
|
||||
data,
|
||||
}: {
|
||||
paletteService: PaletteRegistry;
|
||||
data: DataPublicPluginStart;
|
||||
}): Visualization<State> => ({
|
||||
id: 'lnsXY',
|
||||
|
||||
|
@ -168,7 +178,25 @@ export const getXyVisualization = ({
|
|||
|
||||
const datasource = frame.datasourceLayers[layer.layerId];
|
||||
|
||||
const sortedAccessors = getSortedAccessors(datasource, layer);
|
||||
const sortedAccessors: string[] = getSortedAccessors(datasource, layer);
|
||||
let mappedAccessors: AccessorConfig[] = sortedAccessors.map((accessor) => ({
|
||||
columnId: accessor,
|
||||
}));
|
||||
|
||||
if (frame.activeData) {
|
||||
const colorAssignments = getColorAssignments(
|
||||
state.layers,
|
||||
{ tables: frame.activeData },
|
||||
data.fieldFormats.deserialize
|
||||
);
|
||||
mappedAccessors = getAccessorColorConfig(
|
||||
colorAssignments,
|
||||
frame,
|
||||
layer,
|
||||
sortedAccessors,
|
||||
paletteService
|
||||
);
|
||||
}
|
||||
|
||||
const isHorizontal = isHorizontalChart(state.layers);
|
||||
return {
|
||||
|
@ -176,7 +204,7 @@ export const getXyVisualization = ({
|
|||
{
|
||||
groupId: 'x',
|
||||
groupLabel: getAxisName('x', { isHorizontal }),
|
||||
accessors: layer.xAccessor ? [layer.xAccessor] : [],
|
||||
accessors: layer.xAccessor ? [{ columnId: layer.xAccessor }] : [],
|
||||
filterOperations: isBucketed,
|
||||
supportsMoreColumns: !layer.xAccessor,
|
||||
dataTestSubj: 'lnsXY_xDimensionPanel',
|
||||
|
@ -184,7 +212,7 @@ export const getXyVisualization = ({
|
|||
{
|
||||
groupId: 'y',
|
||||
groupLabel: getAxisName('y', { isHorizontal }),
|
||||
accessors: sortedAccessors,
|
||||
accessors: mappedAccessors,
|
||||
filterOperations: isNumericMetric,
|
||||
supportsMoreColumns: true,
|
||||
required: true,
|
||||
|
@ -196,7 +224,17 @@ export const getXyVisualization = ({
|
|||
groupLabel: i18n.translate('xpack.lens.xyChart.splitSeries', {
|
||||
defaultMessage: 'Break down by',
|
||||
}),
|
||||
accessors: layer.splitAccessor ? [layer.splitAccessor] : [],
|
||||
accessors: layer.splitAccessor
|
||||
? [
|
||||
{
|
||||
columnId: layer.splitAccessor,
|
||||
triggerIcon: 'colorBy',
|
||||
palette: paletteService
|
||||
.get(layer.palette?.name || 'default')
|
||||
.getColors(10, layer.palette?.params),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
filterOperations: isBucketed,
|
||||
supportsMoreColumns: !layer.splitAccessor,
|
||||
dataTestSubj: 'lnsXY_splitDimensionPanel',
|
||||
|
@ -333,6 +371,51 @@ export const getXyVisualization = ({
|
|||
},
|
||||
});
|
||||
|
||||
function getAccessorColorConfig(
|
||||
colorAssignments: ColorAssignments,
|
||||
frame: FramePublicAPI,
|
||||
layer: LayerConfig,
|
||||
sortedAccessors: string[],
|
||||
paletteService: PaletteRegistry
|
||||
): AccessorConfig[] {
|
||||
const layerContainsSplits = Boolean(layer.splitAccessor);
|
||||
const currentPalette: PaletteOutput = layer.palette || { type: 'palette', name: 'default' };
|
||||
const totalSeriesCount = colorAssignments[currentPalette.name].totalSeriesCount;
|
||||
return sortedAccessors.map((accessor) => {
|
||||
const currentYConfig = layer.yConfig?.find((yConfig) => yConfig.forAccessor === accessor);
|
||||
if (layerContainsSplits) {
|
||||
return {
|
||||
columnId: accessor as string,
|
||||
triggerIcon: 'disabled',
|
||||
};
|
||||
}
|
||||
const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layer.layerId]);
|
||||
const rank = colorAssignments[currentPalette.name].getRank(
|
||||
layer,
|
||||
columnToLabel[accessor] || accessor,
|
||||
accessor
|
||||
);
|
||||
const customColor =
|
||||
currentYConfig?.color ||
|
||||
paletteService.get(currentPalette.name).getColor(
|
||||
[
|
||||
{
|
||||
name: columnToLabel[accessor] || accessor,
|
||||
rankAtDepth: rank,
|
||||
totalSeriesAtDepth: totalSeriesCount,
|
||||
},
|
||||
],
|
||||
{ maxDepth: 1, totalSeries: totalSeriesCount },
|
||||
currentPalette.params
|
||||
);
|
||||
return {
|
||||
columnId: accessor as string,
|
||||
triggerIcon: customColor ? 'color' : 'disabled',
|
||||
color: customColor ? customColor : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function validateLayersForDimension(
|
||||
dimension: string,
|
||||
layers: LayerConfig[],
|
||||
|
|
|
@ -15,12 +15,14 @@ import { State, XYState, visualizationTypes } from './types';
|
|||
import { generateId } from '../id_generator';
|
||||
import { getXyVisualization } from './xy_visualization';
|
||||
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
|
||||
import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks';
|
||||
import { PaletteOutput } from 'src/plugins/charts/public';
|
||||
|
||||
jest.mock('../id_generator');
|
||||
|
||||
const xyVisualization = getXyVisualization({
|
||||
paletteService: chartPluginMock.createPaletteRegistry(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
});
|
||||
|
||||
describe('xy_suggestions', () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue