mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[PieVis] PartitionVis integration to Lens. (#123937)
* Removed pie/donut/mosaic/treemap expressions from lens * Replaced pie/donut/mosaic/treemap expressions with expressions from expression_partition_vis * Fixed bug with __other__ labels. * Cleaned up not used fields at Lens. * Added support of empty results for multiple chart types.] * Refactored visualization_noresults. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
4c31b157be
commit
54de36f85a
60 changed files with 1065 additions and 2074 deletions
|
@ -12,7 +12,7 @@
|
|||
"extraPublicDirs": [
|
||||
"common"
|
||||
],
|
||||
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "fieldFormats"],
|
||||
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "fieldFormats", "presentationUtil"],
|
||||
"requiredBundles": ["kibanaReact"],
|
||||
"optionalPlugins": []
|
||||
}
|
||||
|
|
|
@ -5,33 +5,34 @@ exports[`PartitionVisComponent should render correct structure for donut 1`] = `
|
|||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1bdmk0u",
|
||||
"name": "13h2mjc",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
display:flex;flex:1 1 auto;min-height:0;min-width:0;;;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 8px;
|
||||
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
;
|
||||
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
padding: 8px;
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
data-test-subj="visTypePieChart"
|
||||
data-test-subj="partitionVisChart"
|
||||
>
|
||||
<div
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1lu5dww",
|
||||
"name": "19k4zle",
|
||||
"next": undefined,
|
||||
"styles": "display:flex;flex:1 1 auto;min-height:0;min-width:0;",
|
||||
"styles": "display:flex;flex:1 1 auto;min-height:0;min-width:0;width:100%;height:100%;",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
|
@ -400,33 +401,34 @@ exports[`PartitionVisComponent should render correct structure for mosaic 1`] =
|
|||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1bdmk0u",
|
||||
"name": "13h2mjc",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
display:flex;flex:1 1 auto;min-height:0;min-width:0;;;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 8px;
|
||||
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
;
|
||||
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
padding: 8px;
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
data-test-subj="visTypePieChart"
|
||||
data-test-subj="partitionVisChart"
|
||||
>
|
||||
<div
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1lu5dww",
|
||||
"name": "19k4zle",
|
||||
"next": undefined,
|
||||
"styles": "display:flex;flex:1 1 auto;min-height:0;min-width:0;",
|
||||
"styles": "display:flex;flex:1 1 auto;min-height:0;min-width:0;width:100%;height:100%;",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
|
@ -826,33 +828,34 @@ exports[`PartitionVisComponent should render correct structure for pie 1`] = `
|
|||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1bdmk0u",
|
||||
"name": "13h2mjc",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
display:flex;flex:1 1 auto;min-height:0;min-width:0;;;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 8px;
|
||||
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
;
|
||||
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
padding: 8px;
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
data-test-subj="visTypePieChart"
|
||||
data-test-subj="partitionVisChart"
|
||||
>
|
||||
<div
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1lu5dww",
|
||||
"name": "19k4zle",
|
||||
"next": undefined,
|
||||
"styles": "display:flex;flex:1 1 auto;min-height:0;min-width:0;",
|
||||
"styles": "display:flex;flex:1 1 auto;min-height:0;min-width:0;width:100%;height:100%;",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
|
@ -1205,33 +1208,34 @@ exports[`PartitionVisComponent should render correct structure for treemap 1`] =
|
|||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1bdmk0u",
|
||||
"name": "13h2mjc",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
display:flex;flex:1 1 auto;min-height:0;min-width:0;;;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 8px;
|
||||
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
;
|
||||
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
padding: 8px;
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
data-test-subj="visTypePieChart"
|
||||
data-test-subj="partitionVisChart"
|
||||
>
|
||||
<div
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1lu5dww",
|
||||
"name": "19k4zle",
|
||||
"next": undefined,
|
||||
"styles": "display:flex;flex:1 1 auto;min-height:0;min-width:0;",
|
||||
"styles": "display:flex;flex:1 1 auto;min-height:0;min-width:0;width:100%;height:100%;",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
|
@ -1615,33 +1619,34 @@ exports[`PartitionVisComponent should render correct structure for waffle 1`] =
|
|||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1bdmk0u",
|
||||
"name": "13h2mjc",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
display:flex;flex:1 1 auto;min-height:0;min-width:0;;;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 8px;
|
||||
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
;
|
||||
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
padding: 8px;
|
||||
",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
data-test-subj="visTypePieChart"
|
||||
data-test-subj="partitionVisChart"
|
||||
>
|
||||
<div
|
||||
css={
|
||||
Object {
|
||||
"map": undefined,
|
||||
"name": "1lu5dww",
|
||||
"name": "19k4zle",
|
||||
"next": undefined,
|
||||
"styles": "display:flex;flex:1 1 auto;min-height:0;min-width:0;",
|
||||
"styles": "display:flex;flex:1 1 auto;min-height:0;min-width:0;width:100%;height:100%;",
|
||||
"toString": [Function],
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,26 +6,31 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiThemeComputed } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { EuiThemeComputed } from '@elastic/eui';
|
||||
|
||||
export const partitionVisWrapperStyle = css({
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
export const partitionVisContainerStyleFactory = (theme: EuiThemeComputed) => css`
|
||||
${partitionVisWrapperStyle};
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: ${theme.size.s};
|
||||
export const partitionVisContainerStyle = css`
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
export const partitionVisContainerWithToggleStyleFactory = (theme: EuiThemeComputed) => css`
|
||||
${partitionVisContainerStyle}
|
||||
|
||||
inset: 0;
|
||||
position: absolute;
|
||||
padding: ${theme.size.s};
|
||||
`;
|
||||
|
|
|
@ -221,7 +221,9 @@ describe('PartitionVisComponent', function () {
|
|||
} as unknown as Datatable;
|
||||
const newProps = { ...wrapperProps, visData: newVisData };
|
||||
const component = mount(<PartitionVisComponent {...newProps} />);
|
||||
expect(findTestSubject(component, 'pieVisualizationError').text()).toEqual('No results found');
|
||||
expect(findTestSubject(component, 'partitionVisEmptyValues').text()).toEqual(
|
||||
'No results found'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the no results component if there are negative values', () => {
|
||||
|
@ -250,8 +252,8 @@ describe('PartitionVisComponent', function () {
|
|||
} as unknown as Datatable;
|
||||
const newProps = { ...wrapperProps, visData: newVisData };
|
||||
const component = mount(<PartitionVisComponent {...newProps} />);
|
||||
expect(findTestSubject(component, 'pieVisualizationError').text()).toEqual(
|
||||
"Pie/donut charts can't render with negative values."
|
||||
expect(findTestSubject(component, 'partitionVisNegativeValues').text()).toEqual(
|
||||
"Pie chart can't render with negative values."
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,12 +20,7 @@ import {
|
|||
SeriesIdentifier,
|
||||
} from '@elastic/charts';
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import {
|
||||
LegendToggle,
|
||||
ClickTriggerEvent,
|
||||
ChartsPluginSetup,
|
||||
PaletteRegistry,
|
||||
} from '../../../../charts/public';
|
||||
import { LegendToggle, ChartsPluginSetup, PaletteRegistry } from '../../../../charts/public';
|
||||
import type { PersistedState } from '../../../../visualizations/public';
|
||||
import {
|
||||
Datatable,
|
||||
|
@ -63,10 +58,12 @@ import { VisualizationNoResults } from './visualization_noresults';
|
|||
import { VisTypePiePluginStartDependencies } from '../plugin';
|
||||
import {
|
||||
partitionVisWrapperStyle,
|
||||
partitionVisContainerStyleFactory,
|
||||
partitionVisContainerStyle,
|
||||
partitionVisContainerWithToggleStyleFactory,
|
||||
} from './partition_vis_component.styles';
|
||||
import { ChartTypes } from '../../common/types';
|
||||
import { filterOutConfig } from '../utils/filter_out_config';
|
||||
import { FilterEvent } from '../types';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -93,7 +90,6 @@ 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();
|
||||
|
||||
|
@ -103,8 +99,8 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
|
|||
);
|
||||
|
||||
const formatters = useMemo(
|
||||
() => generateFormatters(visParams, visData, services.fieldFormats.deserialize),
|
||||
[services.fieldFormats.deserialize, visData, visParams]
|
||||
() => generateFormatters(visData, services.fieldFormats.deserialize),
|
||||
[services.fieldFormats.deserialize, visData]
|
||||
);
|
||||
|
||||
const showLegendDefault = useCallback(() => {
|
||||
|
@ -114,6 +110,8 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
|
|||
|
||||
const [showLegend, setShowLegend] = useState<boolean>(() => showLegendDefault());
|
||||
|
||||
const showToggleLegendElement = props.uiState !== undefined;
|
||||
|
||||
const [dimensions, setDimensions] = useState<undefined | PieContainerDimensions>();
|
||||
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
@ -157,11 +155,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
|
|||
splitChartDimension,
|
||||
splitChartFormatter
|
||||
);
|
||||
const event = {
|
||||
name: 'filterBucket',
|
||||
data: { data },
|
||||
};
|
||||
props.fireEvent(event);
|
||||
props.fireEvent({ name: 'filter', data: { data } });
|
||||
},
|
||||
[props]
|
||||
);
|
||||
|
@ -169,11 +163,11 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
|
|||
// handles legend action event data
|
||||
const getLegendActionEventData = useCallback(
|
||||
(vData: Datatable) =>
|
||||
(series: SeriesIdentifier): ClickTriggerEvent | null => {
|
||||
(series: SeriesIdentifier): FilterEvent => {
|
||||
const data = getFilterEventData(vData, series);
|
||||
|
||||
return {
|
||||
name: 'filterBucket',
|
||||
name: 'filter',
|
||||
data: {
|
||||
negate: false,
|
||||
data,
|
||||
|
@ -184,7 +178,7 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
|
|||
);
|
||||
|
||||
const handleLegendAction = useCallback(
|
||||
(event: ClickTriggerEvent, negate = false) => {
|
||||
(event: FilterEvent, negate = false) => {
|
||||
props.fireEvent({
|
||||
...event,
|
||||
data: {
|
||||
|
@ -318,6 +312,9 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
|
|||
[visData.rows, metricColumn]
|
||||
);
|
||||
|
||||
const isEmpty = visData.rows.length === 0;
|
||||
const isMetricEmpty = visData.rows.every((row) => !row[metricColumn.id]);
|
||||
|
||||
/**
|
||||
* Checks whether data have negative values.
|
||||
* If so, the no data container is loaded.
|
||||
|
@ -330,14 +327,23 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
|
|||
}),
|
||||
[visData.rows, metricColumn]
|
||||
);
|
||||
|
||||
const flatLegend = isLegendFlat(visType, splitChartDimension);
|
||||
const canShowPieChart = !isAllZeros && !hasNegative;
|
||||
|
||||
const canShowPieChart = !isEmpty && !isMetricEmpty && !isAllZeros && !hasNegative;
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const chartContainerStyle = showToggleLegendElement
|
||||
? partitionVisContainerWithToggleStyleFactory(euiTheme)
|
||||
: partitionVisContainerStyle;
|
||||
|
||||
const partitionType = getPartitionType(visType);
|
||||
|
||||
return (
|
||||
<div css={partitionVisContainerStyleFactory(theme.euiTheme)} data-test-subj="visTypePieChart">
|
||||
<div css={chartContainerStyle} data-test-subj="partitionVisChart">
|
||||
{!canShowPieChart ? (
|
||||
<VisualizationNoResults hasNegativeValues={hasNegative} />
|
||||
<VisualizationNoResults hasNegativeValues={hasNegative} chartType={visType} />
|
||||
) : (
|
||||
<div css={partitionVisWrapperStyle} ref={parentRef}>
|
||||
<LegendColorPickerWrapperContext.Provider
|
||||
|
@ -351,11 +357,13 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
|
|||
distinctColors: visParams.distinctColors ?? false,
|
||||
}}
|
||||
>
|
||||
<LegendToggle
|
||||
onClick={toggleLegend}
|
||||
showLegend={showLegend}
|
||||
legendPosition={legendPosition}
|
||||
/>
|
||||
{showToggleLegendElement && (
|
||||
<LegendToggle
|
||||
onClick={toggleLegend}
|
||||
showLegend={showLegend}
|
||||
legendPosition={legendPosition}
|
||||
/>
|
||||
)}
|
||||
<Chart size="100%">
|
||||
<ChartSplit
|
||||
splitColumnAccessor={splitChartColumnAccessor}
|
||||
|
@ -363,7 +371,9 @@ const PartitionVisComponent = (props: PartitionVisComponentProps) => {
|
|||
/>
|
||||
<Settings
|
||||
debugState={window._echDebugStateFlag ?? false}
|
||||
showLegend={showLegend}
|
||||
showLegend={
|
||||
showLegend ?? shouldShowLegend(visType, visParams.legendDisplay, bucketColumns)
|
||||
}
|
||||
legendPosition={legendPosition}
|
||||
legendMaxDepth={visParams.nestedLegend ? undefined : 1}
|
||||
legendColorPicker={props.uiState ? LegendColorPickerWrapper : undefined}
|
||||
|
|
|
@ -6,27 +6,36 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiEmptyPrompt, EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { FC } from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EmptyPlaceholder } from '../../../../charts/public';
|
||||
import { ChartTypes } from '../../common/types';
|
||||
import { getIcon } from '../utils';
|
||||
|
||||
export const VisualizationNoResults = ({ hasNegativeValues = false }) => {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="visualizeApp"
|
||||
iconColor="default"
|
||||
data-test-subj="pieVisualizationError"
|
||||
body={
|
||||
<EuiText size="xs">
|
||||
{hasNegativeValues
|
||||
? i18n.translate('expressionPartitionVis.negativeValuesFound', {
|
||||
defaultMessage: "Pie/donut charts can't render with negative values.",
|
||||
})
|
||||
: i18n.translate('expressionPartitionVis.noResultsFoundTitle', {
|
||||
defaultMessage: 'No results found',
|
||||
})}
|
||||
</EuiText>
|
||||
}
|
||||
/>
|
||||
);
|
||||
interface Props {
|
||||
hasNegativeValues?: boolean;
|
||||
chartType: ChartTypes;
|
||||
}
|
||||
|
||||
export const VisualizationNoResults: FC<Props> = ({ hasNegativeValues = false, chartType }) => {
|
||||
if (hasNegativeValues) {
|
||||
const message = (
|
||||
<FormattedMessage
|
||||
id="expressionPartitionVis.negativeValuesFound"
|
||||
defaultMessage="{chartType} chart can't render with negative values."
|
||||
values={{ chartType: `${chartType[0].toUpperCase()}${chartType.slice(1)}` }}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EmptyPlaceholder
|
||||
dataTestSubj="partitionVisNegativeValues"
|
||||
icon="alert"
|
||||
iconColor="warning"
|
||||
message={message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <EmptyPlaceholder dataTestSubj="partitionVisEmptyValues" icon={getIcon(chartType)} />;
|
||||
};
|
||||
|
|
|
@ -10,35 +10,27 @@ import React, { lazy } from 'react';
|
|||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Datatable, ExpressionRenderDefinition } from '../../../../expressions/public';
|
||||
import { VisualizationContainer } from '../../../../visualizations/public';
|
||||
import { ExpressionRenderDefinition } from '../../../../expressions/public';
|
||||
import type { PersistedState } from '../../../../visualizations/public';
|
||||
import { VisTypePieDependencies } from '../plugin';
|
||||
import { withSuspense } from '../../../../presentation_util/public';
|
||||
import { KibanaThemeProvider } from '../../../../kibana_react/public';
|
||||
|
||||
import { PARTITION_VIS_RENDERER_NAME } from '../../common/constants';
|
||||
import { ChartTypes, RenderValue } from '../../common/types';
|
||||
|
||||
import { VisTypePieDependencies } from '../plugin';
|
||||
|
||||
export const strings = {
|
||||
getDisplayName: () =>
|
||||
i18n.translate('expressionPartitionVis.renderer.pieVis.displayName', {
|
||||
defaultMessage: 'Pie visualization',
|
||||
i18n.translate('expressionPartitionVis.renderer.partitionVis.pie.displayName', {
|
||||
defaultMessage: 'Partition visualization',
|
||||
}),
|
||||
getHelpDescription: () =>
|
||||
i18n.translate('expressionPartitionVis.renderer.pieVis.helpDescription', {
|
||||
defaultMessage: 'Render a pie',
|
||||
i18n.translate('expressionPartitionVis.renderer.partitionVis.pie.helpDescription', {
|
||||
defaultMessage: 'Render pie/donut/treemap/mosaic/waffle charts',
|
||||
}),
|
||||
};
|
||||
|
||||
const PartitionVisComponent = lazy(() => import('../components/partition_vis_component'));
|
||||
|
||||
function shouldShowNoResultsMessage(visData: Datatable | undefined): boolean {
|
||||
const rows: object[] | undefined = visData?.rows;
|
||||
const isZeroHits = !rows || !rows.length;
|
||||
|
||||
return Boolean(isZeroHits);
|
||||
}
|
||||
const LazyPartitionVisComponent = lazy(() => import('../components/partition_vis_component'));
|
||||
const PartitionVisComponent = withSuspense(LazyPartitionVisComponent);
|
||||
|
||||
export const getPartitionVisRenderer: (
|
||||
deps: VisTypePieDependencies
|
||||
|
@ -48,8 +40,6 @@ export const getPartitionVisRenderer: (
|
|||
help: strings.getHelpDescription(),
|
||||
reuseDomNode: true,
|
||||
render: async (domNode, { visConfig, visData, visType, syncColors }, handlers) => {
|
||||
const showNoResult = shouldShowNoResultsMessage(visData);
|
||||
|
||||
handlers.onDestroy(() => {
|
||||
unmountComponentAtNode(domNode);
|
||||
});
|
||||
|
@ -60,7 +50,7 @@ export const getPartitionVisRenderer: (
|
|||
render(
|
||||
<I18nProvider>
|
||||
<KibanaThemeProvider theme$={services.kibanaTheme.theme$}>
|
||||
<VisualizationContainer handlers={handlers} showNoResult={showNoResult}>
|
||||
<div css={{ height: '100%' }}>
|
||||
<PartitionVisComponent
|
||||
chartsThemeService={theme}
|
||||
palettesRegistry={palettesRegistry}
|
||||
|
@ -73,10 +63,13 @@ export const getPartitionVisRenderer: (
|
|||
services={{ data: services.data, fieldFormats: services.fieldFormats }}
|
||||
syncColors={syncColors}
|
||||
/>
|
||||
</VisualizationContainer>
|
||||
</div>
|
||||
</KibanaThemeProvider>
|
||||
</I18nProvider>,
|
||||
domNode
|
||||
domNode,
|
||||
() => {
|
||||
handlers.done();
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { EuiIconProps } from '@elastic/eui';
|
||||
|
||||
export const DonutIcon = ({ title, titleId, ...props }: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg
|
||||
viewBox="0 0 30 22"
|
||||
width={30}
|
||||
height={22}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-labelledby={titleId}
|
||||
{...props}
|
||||
>
|
||||
{title ? <title id={titleId}>{title}</title> : null}
|
||||
<path
|
||||
d="M19.21 21.119a11 11 0 006.595-8.1c.11-.577-.355-1.082-.942-1.082H20.75c-.477 0-.878.342-1.046.788a5.028 5.028 0 11-6.474-6.474c.447-.168.788-.569.788-1.046V1.094c0-.588-.505-1.053-1.082-.943a11 11 0 106.272 20.968h.002z"
|
||||
className="chart-icon__subdued"
|
||||
/>
|
||||
<path
|
||||
d="M22.778 3.176A11 11 0 0017.084.154C16.507.042 16 .507 16 1.095v4.116c0 .475.34.875.784 1.044l.14.055A5.026 5.026 0 0119.7 9.17c.168.445.568.784 1.044.784h4.115c.588 0 1.053-.506.942-1.084a11 11 0 00-3.023-5.694z"
|
||||
className="chart-icon__accent"
|
||||
/>
|
||||
</svg>
|
||||
);
|
|
@ -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 { PieIcon } from './pie';
|
||||
export { DonutIcon } from './donut';
|
||||
export { TreemapIcon } from './treemap';
|
||||
export { MosaicIcon } from './mosaic';
|
||||
export { WaffleIcon } from './waffle';
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 type { EuiIconProps } from '@elastic/eui';
|
||||
|
||||
export const MosaicIcon = ({ title, titleId, ...props }: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg
|
||||
viewBox="0 0 30 22"
|
||||
width={30}
|
||||
height={22}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-labelledby={titleId}
|
||||
{...props}
|
||||
>
|
||||
{title ? <title id={titleId} /> : null}
|
||||
<path
|
||||
className="chart-icon__subdued"
|
||||
d="M2 0a1 1 0 00-1 1v2a1 1 0 001 1h6a1 1 0 001-1V1a1 1 0 00-1-1H2zM2 14a1 1 0 00-1 1v6a1 1 0 001 1h6a1 1 0 001-1v-6a1 1 0 00-1-1H2zM11 13a1 1 0 011-1h6a1 1 0 011 1v8a1 1 0 01-1 1h-6a1 1 0 01-1-1v-8zM12 0a1 1 0 100 2h6a1 1 0 100-2h-6zM21 15a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1h-6a1 1 0 01-1-1v-6zM22 0a1 1 0 00-1 1v4a1 1 0 001 1h6a1 1 0 001-1V1a1 1 0 00-1-1h-6z"
|
||||
/>
|
||||
<path
|
||||
className="chart-icon__accent"
|
||||
d="M11 5a1 1 0 011-1h6a1 1 0 011 1v4a1 1 0 01-1 1h-6a1 1 0 01-1-1V5zM1 7a1 1 0 011-1h6a1 1 0 011 1v4a1 1 0 01-1 1H2a1 1 0 01-1-1V7zM22 8a1 1 0 00-1 1v2a1 1 0 001 1h6a1 1 0 001-1V9a1 1 0 00-1-1h-6z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { EuiIconProps } from '@elastic/eui';
|
||||
|
||||
export const PieIcon = ({ title, titleId, ...props }: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg
|
||||
viewBox="0 0 30 22"
|
||||
width={30}
|
||||
height={22}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-labelledby={titleId}
|
||||
{...props}
|
||||
>
|
||||
{title ? <title id={titleId}>{title}</title> : null}
|
||||
<path
|
||||
d="M17.827 21.189a10.001 10.001 0 005.952-7.148c.124-.578-.343-1.091-.935-1.091H14a1 1 0 01-1-1V3.106c0-.592-.513-1.059-1.092-.935a10 10 0 105.919 19.018z"
|
||||
className="chart-icon__subdued"
|
||||
/>
|
||||
<path
|
||||
d="M22.462 3.538A12.29 12.29 0 0016.094.16C15.512.048 15 .514 15 1.106V10a1 1 0 001 1h8.895c.591 0 1.057-.512.945-1.094a12.288 12.288 0 00-3.378-6.368z"
|
||||
className="chart-icon__accent"
|
||||
/>
|
||||
</svg>
|
||||
);
|
|
@ -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 React from 'react';
|
||||
import { EuiIconProps } from '@elastic/eui';
|
||||
|
||||
export const TreemapIcon = ({ title, titleId, ...props }: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg
|
||||
viewBox="0 0 30 22"
|
||||
width={30}
|
||||
height={22}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-labelledby={titleId}
|
||||
{...props}
|
||||
>
|
||||
{title ? <title id={titleId}>{title}</title> : null}
|
||||
<path
|
||||
d="M0 1a1 1 0 011-1h13a1 1 0 011 1v20a1 1 0 01-1 1H1a1 1 0 01-1-1V1z"
|
||||
className="chart-icon__subdued"
|
||||
/>
|
||||
<path
|
||||
d="M17 1a1 1 0 011-1h11a1 1 0 011 1v12a1 1 0 01-1 1H18a1 1 0 01-1-1V1z"
|
||||
className="chart-icon__accent"
|
||||
/>
|
||||
<path
|
||||
d="M29 16H18a1 1 0 00-1 1v4a1 1 0 001 1h11a1 1 0 001-1v-4a1 1 0 00-1-1z"
|
||||
className="chart-icon__subdued"
|
||||
/>
|
||||
</svg>
|
||||
);
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 type { EuiIconProps } from '@elastic/eui';
|
||||
|
||||
export const WaffleIcon = ({ title, titleId, ...props }: Omit<EuiIconProps, 'type'>) => (
|
||||
<svg
|
||||
viewBox="0 0 30 22"
|
||||
width={30}
|
||||
height={22}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-labelledby={titleId}
|
||||
{...props}
|
||||
>
|
||||
{title ? <title id={titleId} /> : null}
|
||||
<path
|
||||
className="chart-icon__accent"
|
||||
d="M16 1a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V1zM4 13a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1v-2zM17 6a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1V7a1 1 0 00-1-1h-2zM23 0a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1V1a1 1 0 00-1-1h-2zM5 0a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1V1a1 1 0 00-1-1H5zM4 7a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V7zM11 0a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1V1a1 1 0 00-1-1h-2zM10 7a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V7zM11 12a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 00-1-1h-2zM22 7a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1V7z"
|
||||
/>
|
||||
<path
|
||||
className="chart-icon__subdued"
|
||||
d="M22 13a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2zM4 19a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1v-2zM16 19a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2zM11 18a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 00-1-1h-2zM23 18a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 00-1-1h-2zM16 13a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 01-1 1h-2a1 1 0 01-1-1v-2z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
|
@ -5,6 +5,7 @@
|
|||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
import type { ValueClickContext } from '../../../embeddable/public';
|
||||
import { ChartsPluginSetup } from '../../../charts/public';
|
||||
import { ExpressionsPublicPlugin, ExpressionsServiceStart } from '../../../expressions/public';
|
||||
|
||||
|
@ -19,3 +20,8 @@ export interface SetupDeps {
|
|||
export interface StartDeps {
|
||||
expression: ExpressionsServiceStart;
|
||||
}
|
||||
|
||||
export interface FilterEvent {
|
||||
name: 'filter';
|
||||
data: ValueClickContext['data'];
|
||||
}
|
||||
|
|
|
@ -9,13 +9,13 @@
|
|||
import { LayerValue, SeriesIdentifier } from '@elastic/charts';
|
||||
import { Datatable, DatatableColumn } from '../../../../expressions/public';
|
||||
import { DataPublicPluginStart } from '../../../../data/public';
|
||||
import { ClickTriggerEvent } from '../../../../charts/public';
|
||||
import { ValueClickContext } from '../../../../embeddable/public';
|
||||
import type { FieldFormat } from '../../../../field_formats/common';
|
||||
import { BucketColumns } from '../../common/types';
|
||||
import { FilterEvent } from '../types';
|
||||
|
||||
export const canFilter = async (
|
||||
event: ClickTriggerEvent | null,
|
||||
event: FilterEvent | null,
|
||||
actions: DataPublicPluginStart['actions']
|
||||
): Promise<boolean> => {
|
||||
if (!event) {
|
||||
|
|
|
@ -8,31 +8,19 @@
|
|||
|
||||
import { fieldFormatsMock } from '../../../../field_formats/common/mocks';
|
||||
import { Datatable } from '../../../../expressions';
|
||||
import { createMockPieParams, createMockVisData } from '../mocks';
|
||||
import { 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 formatters = generateFormatters(visData, defaultFormatter);
|
||||
const formattingResult = fieldFormatsMock.deserialize();
|
||||
|
||||
const serializedFormatters = Object.keys(formatters).reduce(
|
||||
|
@ -62,7 +50,7 @@ describe('generateFormatters', () => {
|
|||
columns: visData.columns.map(({ meta, ...col }) => ({ ...col, meta: { type: 'string' } })),
|
||||
};
|
||||
|
||||
const formatters = generateFormatters(visParams, newVisData, defaultFormatter);
|
||||
const formatters = generateFormatters(newVisData, defaultFormatter);
|
||||
|
||||
expect(formatters).toEqual({
|
||||
'col-0-2': undefined,
|
||||
|
|
|
@ -8,25 +8,16 @@
|
|||
|
||||
import type { FieldFormat, FormatFactory } from '../../../../field_formats/common';
|
||||
import type { Datatable } from '../../../../expressions/public';
|
||||
import { BucketColumns, PartitionVisParams } from '../../common/types';
|
||||
import { BucketColumns } 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>>(
|
||||
export const generateFormatters = (visData: Datatable, formatFactory: FormatFactory) =>
|
||||
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>,
|
||||
|
|
|
@ -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 { ChartTypes } from '../../common/types';
|
||||
import { PieIcon, DonutIcon, TreemapIcon, MosaicIcon, WaffleIcon } from '../icons';
|
||||
|
||||
export const getIcon = (chart: ChartTypes) =>
|
||||
({
|
||||
[ChartTypes.PIE]: PieIcon,
|
||||
[ChartTypes.DONUT]: DonutIcon,
|
||||
[ChartTypes.TREEMAP]: TreemapIcon,
|
||||
[ChartTypes.MOSAIC]: MosaicIcon,
|
||||
[ChartTypes.WAFFLE]: WaffleIcon,
|
||||
}[chart]);
|
|
@ -13,16 +13,16 @@ import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } fr
|
|||
import { LegendAction, SeriesIdentifier, useLegendAction } from '@elastic/charts';
|
||||
import { DataPublicPluginStart } from '../../../../data/public';
|
||||
import { PartitionVisParams } from '../../common/types';
|
||||
import { ClickTriggerEvent } from '../../../../charts/public';
|
||||
import { FieldFormatsStart } from '../../../../field_formats/public';
|
||||
import { FilterEvent } from '../types';
|
||||
|
||||
export const getLegendActions = (
|
||||
canFilter: (
|
||||
data: ClickTriggerEvent | null,
|
||||
data: FilterEvent | null,
|
||||
actions: DataPublicPluginStart['actions']
|
||||
) => Promise<boolean>,
|
||||
getFilterEventData: (series: SeriesIdentifier) => ClickTriggerEvent | null,
|
||||
onFilter: (data: ClickTriggerEvent, negate?: any) => void,
|
||||
getFilterEventData: (series: SeriesIdentifier) => FilterEvent | null,
|
||||
onFilter: (data: FilterEvent, negate?: any) => void,
|
||||
visParams: PartitionVisParams,
|
||||
actions: DataPublicPluginStart['actions'],
|
||||
formatter: FieldFormatsStart
|
||||
|
|
|
@ -18,3 +18,4 @@ export { getColumnByAccessor } from './accessor';
|
|||
export { isLegendFlat, shouldShowLegend } from './legend';
|
||||
export { generateFormatters, getAvailableFormatter, getFormatter } from './formatters';
|
||||
export { getPartitionType } from './get_partition_type';
|
||||
export { getIcon } from './get_icon';
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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 { PaletteDefinition, PaletteOutput } from '../../../../../charts/public';
|
||||
import { chartPluginMock } from '../../../../../charts/public/mocks';
|
||||
import { Datatable } from '../../../../../expressions';
|
||||
import { byDataColorPaletteMap } from './get_color';
|
||||
|
||||
describe('#byDataColorPaletteMap', () => {
|
||||
let datatable: Datatable;
|
||||
let paletteDefinition: PaletteDefinition;
|
||||
let palette: PaletteOutput;
|
||||
const columnId = 'foo';
|
||||
|
||||
beforeEach(() => {
|
||||
datatable = {
|
||||
rows: [
|
||||
{
|
||||
[columnId]: '1',
|
||||
},
|
||||
{
|
||||
[columnId]: '2',
|
||||
},
|
||||
],
|
||||
} as unknown as Datatable;
|
||||
paletteDefinition = chartPluginMock.createPaletteRegistry().get('default');
|
||||
palette = { type: 'palette' } as PaletteOutput;
|
||||
});
|
||||
|
||||
it('should create byDataColorPaletteMap', () => {
|
||||
expect(byDataColorPaletteMap(datatable.rows, columnId, paletteDefinition, palette))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"getColor": [Function],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should get color', () => {
|
||||
const colorPaletteMap = byDataColorPaletteMap(
|
||||
datatable.rows,
|
||||
columnId,
|
||||
paletteDefinition,
|
||||
palette
|
||||
);
|
||||
|
||||
expect(colorPaletteMap.getColor('1')).toBe('black');
|
||||
});
|
||||
|
||||
it('should return undefined in case if values not in datatable', () => {
|
||||
const colorPaletteMap = byDataColorPaletteMap(
|
||||
datatable.rows,
|
||||
columnId,
|
||||
paletteDefinition,
|
||||
palette
|
||||
);
|
||||
|
||||
expect(colorPaletteMap.getColor('wrong')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should increase rankAtDepth for each new value', () => {
|
||||
const colorPaletteMap = byDataColorPaletteMap(
|
||||
datatable.rows,
|
||||
columnId,
|
||||
paletteDefinition,
|
||||
palette
|
||||
);
|
||||
colorPaletteMap.getColor('1');
|
||||
colorPaletteMap.getColor('2');
|
||||
|
||||
expect(paletteDefinition.getCategoricalColor).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[{ name: '1', rankAtDepth: 0, totalSeriesAtDepth: 2 }],
|
||||
{ behindText: false },
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(paletteDefinition.getCategoricalColor).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[{ name: '2', rankAtDepth: 1, totalSeriesAtDepth: 2 }],
|
||||
{ behindText: false },
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Datatable } from '../../../../../expressions';
|
||||
import { extractUniqTermsMap } from './sort_predicate';
|
||||
|
||||
describe('#extractUniqTermsMap', () => {
|
||||
it('should extract map', () => {
|
||||
const table: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{ id: 'a', name: 'A', meta: { type: 'string' } },
|
||||
{ id: 'b', name: 'B', meta: { type: 'string' } },
|
||||
{ id: 'c', name: 'C', meta: { type: 'number' } },
|
||||
],
|
||||
rows: [
|
||||
{ a: 'Hi', b: 'Two', c: 2 },
|
||||
{ a: 'Test', b: 'Two', c: 5 },
|
||||
{ a: 'Foo', b: 'Three', c: 6 },
|
||||
],
|
||||
};
|
||||
expect(extractUniqTermsMap(table, 'a')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"Foo": 2,
|
||||
"Hi": 0,
|
||||
"Test": 1,
|
||||
}
|
||||
`);
|
||||
expect(extractUniqTermsMap(table, 'b')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"Three": 1,
|
||||
"Two": 0,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -15,6 +15,7 @@
|
|||
"references": [
|
||||
{ "path": "../../../core/tsconfig.json" },
|
||||
{ "path": "../../expressions/tsconfig.json" },
|
||||
{ "path": "../../presentation_util/tsconfig.json" },
|
||||
{ "path": "../../data/tsconfig.json" },
|
||||
{ "path": "../../field_formats/tsconfig.json" },
|
||||
{ "path": "../../charts/tsconfig.json" },
|
||||
|
|
|
@ -13,14 +13,24 @@ import './empty_placeholder.scss';
|
|||
|
||||
export const EmptyPlaceholder = ({
|
||||
icon,
|
||||
iconColor = 'subdued',
|
||||
message = <FormattedMessage id="charts.noDataLabel" defaultMessage="No results found" />,
|
||||
dataTestSubj = 'emptyPlaceholder',
|
||||
}: {
|
||||
icon: IconType;
|
||||
iconColor?: string;
|
||||
message?: JSX.Element;
|
||||
dataTestSubj?: string;
|
||||
}) => (
|
||||
<>
|
||||
<EuiText className="chart__empty-placeholder" textAlign="center" color="subdued" size="xs">
|
||||
<EuiIcon type={icon} color="subdued" size="l" />
|
||||
<EuiText
|
||||
data-test-subj={dataTestSubj}
|
||||
className="chart__empty-placeholder"
|
||||
textAlign="center"
|
||||
color="subdued"
|
||||
size="xs"
|
||||
>
|
||||
<EuiIcon type={icon} color={iconColor} size="l" />
|
||||
<EuiSpacer size="s" />
|
||||
<p>{message}</p>
|
||||
</EuiText>
|
||||
|
|
|
@ -432,7 +432,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
'360,000',
|
||||
'CN',
|
||||
].sort();
|
||||
if (await PageObjects.visChart.isNewLibraryChart('visTypePieChart')) {
|
||||
if (await PageObjects.visChart.isNewLibraryChart('partitionVisChart')) {
|
||||
await PageObjects.visEditor.clickOptionsTab();
|
||||
await PageObjects.visEditor.togglePieLegend();
|
||||
await PageObjects.visEditor.togglePieNestedLegend();
|
||||
|
|
|
@ -11,7 +11,7 @@ import chroma from 'chroma-js';
|
|||
|
||||
import { FtrService } from '../ftr_provider_context';
|
||||
|
||||
const pieChartSelector = 'visTypePieChart';
|
||||
const partitionVisChartSelector = 'partitionVisChart';
|
||||
const heatmapChartSelector = 'heatmapChart';
|
||||
|
||||
export class VisualizeChartPageObject extends FtrService {
|
||||
|
@ -149,7 +149,7 @@ export class VisualizeChartPageObject extends FtrService {
|
|||
}
|
||||
|
||||
private async toggleLegend(force = false) {
|
||||
const isVisTypePieChart = await this.isNewLibraryChart(pieChartSelector);
|
||||
const isVisTypePieChart = await this.isNewLibraryChart(partitionVisChartSelector);
|
||||
const legendSelector = force || isVisTypePieChart ? '.echLegend' : '.visLegend';
|
||||
|
||||
await this.retry.try(async () => {
|
||||
|
@ -182,10 +182,11 @@ export class VisualizeChartPageObject extends FtrService {
|
|||
}
|
||||
|
||||
public async doesSelectedLegendColorExistForPie(matchingColor: string) {
|
||||
if (await this.isNewLibraryChart(pieChartSelector)) {
|
||||
if (await this.isNewLibraryChart(partitionVisChartSelector)) {
|
||||
const hexMatchingColor = chroma(matchingColor).hex().toUpperCase();
|
||||
const slices =
|
||||
(await this.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? [];
|
||||
(await this.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0]?.partitions ??
|
||||
[];
|
||||
return slices.some(({ color }) => {
|
||||
return hexMatchingColor === chroma(color).hex().toUpperCase();
|
||||
});
|
||||
|
@ -195,7 +196,7 @@ export class VisualizeChartPageObject extends FtrService {
|
|||
}
|
||||
|
||||
public async expectError() {
|
||||
if (!this.isNewLibraryChart(pieChartSelector)) {
|
||||
if (!this.isNewLibraryChart(partitionVisChartSelector)) {
|
||||
await this.testSubjects.existOrFail('vislibVisualizeError');
|
||||
}
|
||||
}
|
||||
|
@ -244,12 +245,13 @@ export class VisualizeChartPageObject extends FtrService {
|
|||
}
|
||||
|
||||
public async getLegendEntries() {
|
||||
const isVisTypePieChart = await this.isNewLibraryChart(pieChartSelector);
|
||||
const isVisTypePieChart = await this.isNewLibraryChart(partitionVisChartSelector);
|
||||
const isVisTypeHeatmapChart = await this.isNewLibraryChart(heatmapChartSelector);
|
||||
|
||||
if (isVisTypePieChart) {
|
||||
const slices =
|
||||
(await this.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? [];
|
||||
(await this.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0]?.partitions ??
|
||||
[];
|
||||
return slices.map(({ name }) => name);
|
||||
}
|
||||
|
||||
|
@ -290,7 +292,7 @@ export class VisualizeChartPageObject extends FtrService {
|
|||
public async openLegendOptionColorsForPie(name: string, chartSelector: string) {
|
||||
await this.waitForVisualizationRenderingStabilized();
|
||||
await this.retry.try(async () => {
|
||||
if (await this.isNewLibraryChart(pieChartSelector)) {
|
||||
if (await this.isNewLibraryChart(partitionVisChartSelector)) {
|
||||
const chart = await this.find.byCssSelector(chartSelector);
|
||||
const legendItemColor = await chart.findByCssSelector(
|
||||
`[data-ech-series-name="${name}"] .echLegendItem__color`
|
||||
|
|
|
@ -10,7 +10,7 @@ import expect from '@kbn/expect';
|
|||
import { isNil } from 'lodash';
|
||||
import { FtrService } from '../../ftr_provider_context';
|
||||
|
||||
const pieChartSelector = 'visTypePieChart';
|
||||
const partitionVisChartSelector = 'partitionVisChart';
|
||||
|
||||
export class PieChartService extends FtrService {
|
||||
private readonly log = this.ctx.getService('log');
|
||||
|
@ -27,16 +27,16 @@ export class PieChartService extends FtrService {
|
|||
|
||||
async clickOnPieSlice(name?: string) {
|
||||
this.log.debug(`PieChart.clickOnPieSlice(${name})`);
|
||||
if (await this.visChart.isNewLibraryChart(pieChartSelector)) {
|
||||
if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) {
|
||||
const slices =
|
||||
(await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ??
|
||||
[];
|
||||
(await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0]
|
||||
?.partitions ?? [];
|
||||
let sliceLabel = name || slices[0].name;
|
||||
if (name === 'Other') {
|
||||
sliceLabel = '__other__';
|
||||
}
|
||||
const pieSlice = slices.find((slice) => slice.name === sliceLabel);
|
||||
const pie = await this.testSubjects.find(pieChartSelector);
|
||||
const pie = await this.testSubjects.find(partitionVisChartSelector);
|
||||
if (pieSlice) {
|
||||
const pieSize = await pie.getSize();
|
||||
const pieHeight = pieSize.height;
|
||||
|
@ -88,10 +88,10 @@ export class PieChartService extends FtrService {
|
|||
|
||||
async getPieSliceStyle(name: string) {
|
||||
this.log.debug(`VisualizePage.getPieSliceStyle(${name})`);
|
||||
if (await this.visChart.isNewLibraryChart(pieChartSelector)) {
|
||||
if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) {
|
||||
const slices =
|
||||
(await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ??
|
||||
[];
|
||||
(await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0]
|
||||
?.partitions ?? [];
|
||||
const selectedSlice = slices.filter((slice) => {
|
||||
return slice.name.toString() === name.replace(',', '');
|
||||
});
|
||||
|
@ -103,10 +103,10 @@ export class PieChartService extends FtrService {
|
|||
|
||||
async getAllPieSliceColor(name: string) {
|
||||
this.log.debug(`VisualizePage.getAllPieSliceColor(${name})`);
|
||||
if (await this.visChart.isNewLibraryChart(pieChartSelector)) {
|
||||
if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) {
|
||||
const slices =
|
||||
(await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ??
|
||||
[];
|
||||
(await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0]
|
||||
?.partitions ?? [];
|
||||
const selectedSlice = slices.filter((slice) => {
|
||||
return slice.name.toString() === name.replace(',', '');
|
||||
});
|
||||
|
@ -143,10 +143,10 @@ export class PieChartService extends FtrService {
|
|||
}
|
||||
|
||||
async getPieChartLabels() {
|
||||
if (await this.visChart.isNewLibraryChart(pieChartSelector)) {
|
||||
if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) {
|
||||
const slices =
|
||||
(await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ??
|
||||
[];
|
||||
(await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0]
|
||||
?.partitions ?? [];
|
||||
return slices.map((slice) => {
|
||||
if (slice.name === '__missing__') {
|
||||
return 'Missing';
|
||||
|
@ -169,10 +169,10 @@ export class PieChartService extends FtrService {
|
|||
|
||||
async getPieSliceCount() {
|
||||
this.log.debug('PieChart.getPieSliceCount');
|
||||
if (await this.visChart.isNewLibraryChart(pieChartSelector)) {
|
||||
if (await this.visChart.isNewLibraryChart(partitionVisChartSelector)) {
|
||||
const slices =
|
||||
(await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ??
|
||||
[];
|
||||
(await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0]
|
||||
?.partitions ?? [];
|
||||
return slices?.length;
|
||||
}
|
||||
const slices = await this.find.allByCssSelector('svg > g > g.arcs > path.slice');
|
||||
|
@ -181,8 +181,8 @@ export class PieChartService extends FtrService {
|
|||
|
||||
async expectPieSliceCountEsCharts(expectedCount: number) {
|
||||
const slices =
|
||||
(await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ??
|
||||
[];
|
||||
(await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0]
|
||||
?.partitions ?? [];
|
||||
expect(slices.length).to.be(expectedCount);
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,32 @@ export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations';
|
|||
export const BASE_API_URL = '/api/lens';
|
||||
export const LENS_EDIT_BY_VALUE = 'edit_by_value';
|
||||
|
||||
export const PieChartTypes = {
|
||||
PIE: 'pie',
|
||||
DONUT: 'donut',
|
||||
TREEMAP: 'treemap',
|
||||
MOSAIC: 'mosaic',
|
||||
WAFFLE: 'waffle',
|
||||
} as const;
|
||||
|
||||
export const CategoryDisplay = {
|
||||
DEFAULT: 'default',
|
||||
INSIDE: 'inside',
|
||||
HIDE: 'hide',
|
||||
} as const;
|
||||
|
||||
export const NumberDisplay = {
|
||||
HIDDEN: 'hidden',
|
||||
PERCENT: 'percent',
|
||||
VALUE: 'value',
|
||||
} as const;
|
||||
|
||||
export const LegendDisplay = {
|
||||
DEFAULT: 'default',
|
||||
SHOW: 'show',
|
||||
HIDE: 'hide',
|
||||
} as const;
|
||||
|
||||
export const layerTypes: Record<string, LayerType> = {
|
||||
DATA: 'data',
|
||||
REFERENCELINE: 'referenceLine',
|
||||
|
|
|
@ -12,7 +12,6 @@ export * from './merge_tables';
|
|||
export * from './time_scale';
|
||||
export * from './datatable';
|
||||
export * from './metric_chart';
|
||||
export * from './pie_chart';
|
||||
export * from './xy_chart';
|
||||
|
||||
export * from './expression_types';
|
||||
|
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export { pie } from './pie_chart';
|
||||
|
||||
export type {
|
||||
SharedPieLayerState,
|
||||
PieLayerState,
|
||||
PieVisualizationState,
|
||||
PieExpressionArgs,
|
||||
PieExpressionProps,
|
||||
} from './types';
|
|
@ -1,131 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Position } from '@elastic/charts';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { ExpressionFunctionDefinition } from '../../../../../../src/plugins/expressions/common';
|
||||
import type { LensMultiTable } from '../../types';
|
||||
import type { PieExpressionProps, PieExpressionArgs } from './types';
|
||||
|
||||
interface PieRender {
|
||||
type: 'render';
|
||||
as: 'lens_pie_renderer';
|
||||
value: PieExpressionProps;
|
||||
}
|
||||
|
||||
export const pie: ExpressionFunctionDefinition<
|
||||
'lens_pie',
|
||||
LensMultiTable,
|
||||
PieExpressionArgs,
|
||||
PieRender
|
||||
> = {
|
||||
name: 'lens_pie',
|
||||
type: 'render',
|
||||
help: i18n.translate('xpack.lens.pie.expressionHelpLabel', {
|
||||
defaultMessage: 'Pie renderer',
|
||||
}),
|
||||
args: {
|
||||
title: {
|
||||
types: ['string'],
|
||||
help: 'The chart title.',
|
||||
},
|
||||
description: {
|
||||
types: ['string'],
|
||||
help: '',
|
||||
},
|
||||
groups: {
|
||||
types: ['string'],
|
||||
multi: true,
|
||||
help: '',
|
||||
},
|
||||
metric: {
|
||||
types: ['string'],
|
||||
help: '',
|
||||
},
|
||||
shape: {
|
||||
types: ['string'],
|
||||
options: ['pie', 'donut', 'treemap', 'mosaic'],
|
||||
help: '',
|
||||
},
|
||||
hideLabels: {
|
||||
types: ['boolean'],
|
||||
help: '',
|
||||
},
|
||||
numberDisplay: {
|
||||
types: ['string'],
|
||||
options: ['hidden', 'percent', 'value'],
|
||||
help: '',
|
||||
},
|
||||
categoryDisplay: {
|
||||
types: ['string'],
|
||||
options: ['default', 'inside', 'hide'],
|
||||
help: '',
|
||||
},
|
||||
legendDisplay: {
|
||||
types: ['string'],
|
||||
options: ['default', 'show', 'hide'],
|
||||
help: '',
|
||||
},
|
||||
nestedLegend: {
|
||||
types: ['boolean'],
|
||||
help: '',
|
||||
},
|
||||
legendMaxLines: {
|
||||
types: ['number'],
|
||||
help: '',
|
||||
},
|
||||
truncateLegend: {
|
||||
types: ['boolean'],
|
||||
help: '',
|
||||
},
|
||||
showValuesInLegend: {
|
||||
types: ['boolean'],
|
||||
help: '',
|
||||
},
|
||||
legendPosition: {
|
||||
types: ['string'],
|
||||
options: [Position.Top, Position.Right, Position.Bottom, Position.Left],
|
||||
help: '',
|
||||
},
|
||||
percentDecimals: {
|
||||
types: ['number'],
|
||||
help: '',
|
||||
},
|
||||
palette: {
|
||||
default: `{theme "palette" default={system_palette name="default"} }`,
|
||||
help: '',
|
||||
types: ['palette'],
|
||||
},
|
||||
emptySizeRatio: {
|
||||
types: ['number'],
|
||||
help: '',
|
||||
},
|
||||
ariaLabel: {
|
||||
types: ['string'],
|
||||
help: '',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
inputTypes: ['lens_multitable'],
|
||||
fn(data: LensMultiTable, args: PieExpressionArgs, handlers) {
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'lens_pie_renderer',
|
||||
value: {
|
||||
data,
|
||||
args: {
|
||||
...args,
|
||||
ariaLabel:
|
||||
args.ariaLabel ??
|
||||
(handlers.variables?.embeddableTitle as string) ??
|
||||
handlers.getExecutionContext?.()?.description,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
|
@ -1,51 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PaletteOutput } from '../../../../../../src/plugins/charts/common';
|
||||
import type { LensMultiTable, LayerType } from '../../types';
|
||||
|
||||
export type PieChartTypes = 'donut' | 'pie' | 'treemap' | 'mosaic' | 'waffle';
|
||||
|
||||
export interface SharedPieLayerState {
|
||||
groups: string[];
|
||||
metric?: string;
|
||||
numberDisplay: 'hidden' | 'percent' | 'value';
|
||||
categoryDisplay: 'default' | 'inside' | 'hide';
|
||||
legendDisplay: 'default' | 'show' | 'hide';
|
||||
legendPosition?: 'left' | 'right' | 'top' | 'bottom';
|
||||
showValuesInLegend?: boolean;
|
||||
nestedLegend?: boolean;
|
||||
percentDecimals?: number;
|
||||
emptySizeRatio?: number;
|
||||
legendMaxLines?: number;
|
||||
truncateLegend?: boolean;
|
||||
}
|
||||
|
||||
export type PieLayerState = SharedPieLayerState & {
|
||||
layerId: string;
|
||||
layerType: LayerType;
|
||||
};
|
||||
|
||||
export interface PieVisualizationState {
|
||||
shape: PieChartTypes;
|
||||
layers: PieLayerState[];
|
||||
palette?: PaletteOutput;
|
||||
}
|
||||
|
||||
export type PieExpressionArgs = SharedPieLayerState & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
shape: PieChartTypes;
|
||||
hideLabels: boolean;
|
||||
palette: PaletteOutput;
|
||||
ariaLabel?: string;
|
||||
};
|
||||
|
||||
export interface PieExpressionProps {
|
||||
data: LensMultiTable;
|
||||
args: PieExpressionArgs;
|
||||
}
|
|
@ -6,12 +6,16 @@
|
|||
*/
|
||||
|
||||
import type { Filter, FilterMeta } from '@kbn/es-query';
|
||||
import { Position } from '@elastic/charts';
|
||||
import { $Values } from '@kbn/utility-types';
|
||||
import type {
|
||||
IFieldFormat,
|
||||
SerializedFieldFormat,
|
||||
} from '../../../../src/plugins/field_formats/common';
|
||||
import type { Datatable } from '../../../../src/plugins/expressions/common';
|
||||
import type { PaletteContinuity } from '../../../../src/plugins/charts/common';
|
||||
import type { PaletteOutput } from '../../../../src/plugins/charts/common';
|
||||
import { CategoryDisplay, LegendDisplay, NumberDisplay, PieChartTypes } from './constants';
|
||||
|
||||
export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat;
|
||||
|
||||
|
@ -73,3 +77,41 @@ export type LayerType = 'data' | 'referenceLine';
|
|||
|
||||
// Shared by XY Chart and Heatmap as for now
|
||||
export type ValueLabelConfig = 'hide' | 'inside' | 'outside';
|
||||
|
||||
export type PieChartType = $Values<typeof PieChartTypes>;
|
||||
export type CategoryDisplayType = $Values<typeof CategoryDisplay>;
|
||||
export type NumberDisplayType = $Values<typeof NumberDisplay>;
|
||||
|
||||
export type LegendDisplayType = $Values<typeof LegendDisplay>;
|
||||
|
||||
export enum EmptySizeRatios {
|
||||
SMALL = 0.3,
|
||||
MEDIUM = 0.54,
|
||||
LARGE = 0.7,
|
||||
}
|
||||
|
||||
export interface SharedPieLayerState {
|
||||
groups: string[];
|
||||
metric?: string;
|
||||
numberDisplay: NumberDisplayType;
|
||||
categoryDisplay: CategoryDisplayType;
|
||||
legendDisplay: LegendDisplayType;
|
||||
legendPosition?: Position;
|
||||
showValuesInLegend?: boolean;
|
||||
nestedLegend?: boolean;
|
||||
percentDecimals?: number;
|
||||
emptySizeRatio?: number;
|
||||
legendMaxLines?: number;
|
||||
truncateLegend?: boolean;
|
||||
}
|
||||
|
||||
export type PieLayerState = SharedPieLayerState & {
|
||||
layerId: string;
|
||||
layerType: LayerType;
|
||||
};
|
||||
|
||||
export interface PieVisualizationState {
|
||||
shape: $Values<typeof PieChartTypes>;
|
||||
layers: PieLayerState[];
|
||||
palette?: PaletteOutput;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,8 @@ import type { LensByReferenceInput, LensByValueInput } from './embeddable';
|
|||
import type { Document } from '../persistence';
|
||||
import type { IndexPatternPersistedState } from '../indexpattern_datasource/types';
|
||||
import type { XYState } from '../xy_visualization/types';
|
||||
import type { PieVisualizationState, MetricState } from '../../common/expressions';
|
||||
import type { MetricState } from '../../common/expressions';
|
||||
import type { PieVisualizationState } from '../../common';
|
||||
import type { DatatableVisualizationState } from '../datatable_visualization/visualization';
|
||||
import type { HeatmapVisualizationState } from '../heatmap_visualization/types';
|
||||
import type { GaugeVisualizationState } from '../visualizations/gauge/constants';
|
||||
|
|
|
@ -24,7 +24,6 @@ import { datatableColumn } from '../common/expressions/datatable/datatable_colum
|
|||
|
||||
import { mergeTables } from '../common/expressions/merge_tables';
|
||||
import { renameColumns } from '../common/expressions/rename_columns/rename_columns';
|
||||
import { pie } from '../common/expressions/pie_chart/pie_chart';
|
||||
import { formatColumn } from '../common/expressions/format_column';
|
||||
import { counterRate } from '../common/expressions/counter_rate';
|
||||
import { getTimeScale } from '../common/expressions/time_scale/time_scale';
|
||||
|
@ -39,7 +38,6 @@ export const setupExpressions = (
|
|||
[lensMultitable].forEach((expressionType) => expressions.registerType(expressionType));
|
||||
|
||||
[
|
||||
pie,
|
||||
xyChart,
|
||||
mergeTables,
|
||||
counterRate,
|
||||
|
|
|
@ -14,9 +14,6 @@ export type {
|
|||
export type { XYState } from './xy_visualization/types';
|
||||
export type { DataType, OperationMetadata, Visualization } from './types';
|
||||
export type {
|
||||
PieVisualizationState,
|
||||
PieLayerState,
|
||||
SharedPieLayerState,
|
||||
MetricState,
|
||||
AxesSettingsConfig,
|
||||
XYLayerConfig,
|
||||
|
@ -26,7 +23,13 @@ export type {
|
|||
XYCurveType,
|
||||
YConfig,
|
||||
} from '../common/expressions';
|
||||
export type { ValueLabelConfig } from '../common/types';
|
||||
export type {
|
||||
ValueLabelConfig,
|
||||
PieVisualizationState,
|
||||
PieLayerState,
|
||||
SharedPieLayerState,
|
||||
} from '../common/types';
|
||||
|
||||
export type { DatatableVisualizationState } from './datatable_visualization/visualization';
|
||||
export type { HeatmapVisualizationState } from './heatmap_visualization/types';
|
||||
export type { GaugeVisualizationState } from './visualizations/gauge/constants';
|
||||
|
|
|
@ -6,9 +6,3 @@
|
|||
*/
|
||||
|
||||
export const DEFAULT_PERCENT_DECIMALS = 2;
|
||||
|
||||
export enum EMPTY_SIZE_RATIOS {
|
||||
SMALL = 0.3,
|
||||
MEDIUM = 0.54,
|
||||
LARGE = 0.7,
|
||||
}
|
||||
|
|
|
@ -1,66 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import type {
|
||||
IInterpreterRenderHandlers,
|
||||
ExpressionRenderDefinition,
|
||||
} from 'src/plugins/expressions/public';
|
||||
import { ThemeServiceStart } from 'kibana/public';
|
||||
import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public';
|
||||
import type { LensFilterEvent } from '../types';
|
||||
import { PieComponent } from './render_function';
|
||||
import type { FormatFactory } from '../../common';
|
||||
import type { PieExpressionProps } from '../../common/expressions';
|
||||
import type { ChartsPluginSetup, PaletteRegistry } from '../../../../../src/plugins/charts/public';
|
||||
|
||||
export const getPieRenderer = (dependencies: {
|
||||
formatFactory: FormatFactory;
|
||||
chartsThemeService: ChartsPluginSetup['theme'];
|
||||
paletteService: PaletteRegistry;
|
||||
kibanaTheme: ThemeServiceStart;
|
||||
}): ExpressionRenderDefinition<PieExpressionProps> => ({
|
||||
name: 'lens_pie_renderer',
|
||||
displayName: i18n.translate('xpack.lens.pie.visualizationName', {
|
||||
defaultMessage: 'Pie',
|
||||
}),
|
||||
help: '',
|
||||
validate: () => undefined,
|
||||
reuseDomNode: true,
|
||||
render: (domNode: Element, config: PieExpressionProps, handlers: IInterpreterRenderHandlers) => {
|
||||
const onClickValue = (data: LensFilterEvent['data']) => {
|
||||
handlers.event({ name: 'filter', data });
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<KibanaThemeProvider theme$={dependencies.kibanaTheme.theme$}>
|
||||
<I18nProvider>
|
||||
<MemoizedChart
|
||||
{...config}
|
||||
formatFactory={dependencies.formatFactory}
|
||||
chartsThemeService={dependencies.chartsThemeService}
|
||||
interactive={handlers.isInteractive()}
|
||||
paletteService={dependencies.paletteService}
|
||||
onClickValue={onClickValue}
|
||||
renderMode={handlers.getRenderMode()}
|
||||
syncColors={handlers.isSyncColorsEnabled()}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</KibanaThemeProvider>,
|
||||
domNode,
|
||||
() => {
|
||||
handlers.done();
|
||||
}
|
||||
);
|
||||
handlers.onDestroy(() => ReactDOM.unmountComponentAtNode(domNode));
|
||||
},
|
||||
});
|
||||
|
||||
const MemoizedChart = React.memo(PieComponent);
|
|
@ -1,79 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { LegendActionProps, SeriesIdentifier } from '@elastic/charts';
|
||||
import { EuiPopover } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { ComponentType, ReactWrapper } from 'enzyme';
|
||||
import type { Datatable } from 'src/plugins/expressions/public';
|
||||
import { getLegendAction } from './get_legend_action';
|
||||
import { LegendActionPopover } from '../shared_components';
|
||||
|
||||
const table: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{ id: 'a', name: 'A', meta: { type: 'string' } },
|
||||
{ id: 'b', name: 'B', meta: { type: 'number' } },
|
||||
],
|
||||
rows: [
|
||||
{ a: 'Hi', b: 2 },
|
||||
{ a: 'Test', b: 4 },
|
||||
{ a: 'Foo', b: 6 },
|
||||
],
|
||||
};
|
||||
|
||||
describe('getLegendAction', function () {
|
||||
let wrapperProps: LegendActionProps;
|
||||
const Component: ComponentType<LegendActionProps> = getLegendAction(table, jest.fn());
|
||||
let wrapper: ReactWrapper<LegendActionProps>;
|
||||
|
||||
beforeAll(() => {
|
||||
wrapperProps = {
|
||||
color: 'rgb(109, 204, 177)',
|
||||
label: 'Bar',
|
||||
series: [
|
||||
{
|
||||
specId: 'donut',
|
||||
key: 'Bar',
|
||||
},
|
||||
] as unknown as SeriesIdentifier[],
|
||||
};
|
||||
});
|
||||
|
||||
it('is not rendered if row does not exist', () => {
|
||||
wrapper = mountWithIntl(<Component {...wrapperProps} />);
|
||||
expect(wrapper).toEqual({});
|
||||
expect(wrapper.find(EuiPopover).length).toBe(0);
|
||||
});
|
||||
|
||||
it('is rendered if row is detected', () => {
|
||||
const newProps = {
|
||||
...wrapperProps,
|
||||
label: 'Hi',
|
||||
series: [
|
||||
{
|
||||
specId: 'donut',
|
||||
key: 'Hi',
|
||||
},
|
||||
] as unknown as SeriesIdentifier[],
|
||||
};
|
||||
wrapper = mountWithIntl(<Component {...newProps} />);
|
||||
expect(wrapper.find(EuiPopover).length).toBe(1);
|
||||
expect(wrapper.find(EuiPopover).prop('title')).toEqual('Hi, filter options');
|
||||
expect(wrapper.find(LegendActionPopover).prop('context')).toEqual({
|
||||
data: [
|
||||
{
|
||||
column: 0,
|
||||
row: 0,
|
||||
table,
|
||||
value: 'Hi',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,44 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import type { LegendAction } from '@elastic/charts';
|
||||
import type { Datatable } from 'src/plugins/expressions/public';
|
||||
import type { LensFilterEvent } from '../types';
|
||||
import { LegendActionPopover } from '../shared_components';
|
||||
|
||||
export const getLegendAction = (
|
||||
table: Datatable,
|
||||
onFilter: (data: LensFilterEvent['data']) => void
|
||||
): LegendAction =>
|
||||
React.memo(({ series: [pieSeries], label }) => {
|
||||
const data = table.columns.reduce<LensFilterEvent['data']['data']>((acc, { id }, column) => {
|
||||
const value = pieSeries.key;
|
||||
const row = table.rows.findIndex((r) => r[id] === value);
|
||||
if (row > -1) {
|
||||
acc.push({
|
||||
table,
|
||||
column,
|
||||
row,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const context: LensFilterEvent['data'] = {
|
||||
data,
|
||||
};
|
||||
|
||||
return <LegendActionPopover label={label} context={context} onFilter={onFilter} />;
|
||||
});
|
|
@ -6,16 +6,12 @@
|
|||
*/
|
||||
|
||||
import type { CoreSetup } from 'src/core/public';
|
||||
import type { ExpressionsSetup } from 'src/plugins/expressions/public';
|
||||
import type { EditorFrameSetup } from '../types';
|
||||
import type { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
|
||||
import type { ChartsPluginSetup } from '../../../../../src/plugins/charts/public';
|
||||
import type { FormatFactory } from '../../common';
|
||||
|
||||
export interface PieVisualizationPluginSetupPlugins {
|
||||
editorFrame: EditorFrameSetup;
|
||||
expressions: ExpressionsSetup;
|
||||
formatFactory: FormatFactory;
|
||||
charts: ChartsPluginSetup;
|
||||
}
|
||||
|
||||
|
@ -24,22 +20,11 @@ export interface PieVisualizationPluginStartPlugins {
|
|||
}
|
||||
|
||||
export class PieVisualization {
|
||||
setup(
|
||||
core: CoreSetup,
|
||||
{ expressions, formatFactory, editorFrame, charts }: PieVisualizationPluginSetupPlugins
|
||||
) {
|
||||
setup(core: CoreSetup, { editorFrame, charts }: PieVisualizationPluginSetupPlugins) {
|
||||
editorFrame.registerVisualization(async () => {
|
||||
const { getPieVisualization, getPieRenderer } = await import('../async_services');
|
||||
const { getPieVisualization } = await import('../async_services');
|
||||
const palettes = await charts.palettes.getPalettes();
|
||||
|
||||
expressions.registerRenderer(
|
||||
getPieRenderer({
|
||||
formatFactory,
|
||||
chartsThemeService: charts.theme,
|
||||
paletteService: palettes,
|
||||
kibanaTheme: core.theme,
|
||||
})
|
||||
);
|
||||
return getPieVisualization({ paletteService: palettes, kibanaTheme: core.theme });
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,24 +6,20 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ArrayEntry, PartitionLayout } from '@elastic/charts';
|
||||
import type { EuiIconProps } from '@elastic/eui';
|
||||
|
||||
import type { DatatableColumn } from '../../../../../src/plugins/expressions';
|
||||
import { LensIconChartDonut } from '../assets/chart_donut';
|
||||
import { LensIconChartPie } from '../assets/chart_pie';
|
||||
import { LensIconChartTreemap } from '../assets/chart_treemap';
|
||||
import { LensIconChartMosaic } from '../assets/chart_mosaic';
|
||||
import { LensIconChartWaffle } from '../assets/chart_waffle';
|
||||
import { EMPTY_SIZE_RATIOS } from './constants';
|
||||
|
||||
import type { SharedPieLayerState } from '../../common/expressions';
|
||||
import type { PieChartTypes } from '../../common/expressions/pie_chart/types';
|
||||
import type { DatatableColumn } from '../../../../../src/plugins/expressions';
|
||||
import { CategoryDisplay, NumberDisplay, SharedPieLayerState, EmptySizeRatios } from '../../common';
|
||||
import type { PieChartType } from '../../common/types';
|
||||
|
||||
interface PartitionChartMeta {
|
||||
icon: ({ title, titleId, ...props }: Omit<EuiIconProps, 'type'>) => JSX.Element;
|
||||
label: string;
|
||||
partitionType: PartitionLayout;
|
||||
groupLabel: string;
|
||||
maxBuckets: number;
|
||||
isExperimental?: boolean;
|
||||
|
@ -40,7 +36,7 @@ interface PartitionChartMeta {
|
|||
}>;
|
||||
emptySizeRatioOptions?: Array<{
|
||||
id: string;
|
||||
value: EMPTY_SIZE_RATIOS;
|
||||
value: EmptySizeRatios;
|
||||
label: string;
|
||||
}>;
|
||||
};
|
||||
|
@ -50,10 +46,6 @@ interface PartitionChartMeta {
|
|||
hideNestedLegendSwitch?: boolean;
|
||||
getShowLegendDefault?: (bucketColumns: DatatableColumn[]) => boolean;
|
||||
};
|
||||
sortPredicate?: (
|
||||
bucketColumns: DatatableColumn[],
|
||||
sortingMap: Record<string, number>
|
||||
) => (node1: ArrayEntry, node2: ArrayEntry) => number;
|
||||
}
|
||||
|
||||
const groupLabel = i18n.translate('xpack.lens.pie.groupLabel', {
|
||||
|
@ -62,19 +54,19 @@ const groupLabel = i18n.translate('xpack.lens.pie.groupLabel', {
|
|||
|
||||
const categoryOptions: PartitionChartMeta['toolbarPopover']['categoryOptions'] = [
|
||||
{
|
||||
value: 'default',
|
||||
value: CategoryDisplay.DEFAULT,
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.showCategoriesLabel', {
|
||||
defaultMessage: 'Inside or outside',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'inside',
|
||||
value: CategoryDisplay.INSIDE,
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.fitInsideOnlyLabel', {
|
||||
defaultMessage: 'Inside only',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'hide',
|
||||
value: CategoryDisplay.HIDE,
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', {
|
||||
defaultMessage: 'Hide labels',
|
||||
}),
|
||||
|
@ -83,13 +75,13 @@ const categoryOptions: PartitionChartMeta['toolbarPopover']['categoryOptions'] =
|
|||
|
||||
const categoryOptionsTreemap: PartitionChartMeta['toolbarPopover']['categoryOptions'] = [
|
||||
{
|
||||
value: 'default',
|
||||
value: CategoryDisplay.DEFAULT,
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.showTreemapCategoriesLabel', {
|
||||
defaultMessage: 'Show labels',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'hide',
|
||||
value: CategoryDisplay.HIDE,
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.categoriesInLegendLabel', {
|
||||
defaultMessage: 'Hide labels',
|
||||
}),
|
||||
|
@ -98,19 +90,19 @@ const categoryOptionsTreemap: PartitionChartMeta['toolbarPopover']['categoryOpti
|
|||
|
||||
const numberOptions: PartitionChartMeta['toolbarPopover']['numberOptions'] = [
|
||||
{
|
||||
value: 'hidden',
|
||||
value: NumberDisplay.HIDDEN,
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.hiddenNumbersLabel', {
|
||||
defaultMessage: 'Hide from chart',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'percent',
|
||||
value: NumberDisplay.PERCENT,
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.showPercentValuesLabel', {
|
||||
defaultMessage: 'Show percent',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'value',
|
||||
value: NumberDisplay.VALUE,
|
||||
inputDisplay: i18n.translate('xpack.lens.pieChart.showFormatterValuesLabel', {
|
||||
defaultMessage: 'Show value',
|
||||
}),
|
||||
|
@ -120,34 +112,33 @@ const numberOptions: PartitionChartMeta['toolbarPopover']['numberOptions'] = [
|
|||
const emptySizeRatioOptions: PartitionChartMeta['toolbarPopover']['emptySizeRatioOptions'] = [
|
||||
{
|
||||
id: 'emptySizeRatioOption-small',
|
||||
value: EMPTY_SIZE_RATIOS.SMALL,
|
||||
value: EmptySizeRatios.SMALL,
|
||||
label: i18n.translate('xpack.lens.pieChart.emptySizeRatioOptions.small', {
|
||||
defaultMessage: 'Small',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'emptySizeRatioOption-medium',
|
||||
value: EMPTY_SIZE_RATIOS.MEDIUM,
|
||||
value: EmptySizeRatios.MEDIUM,
|
||||
label: i18n.translate('xpack.lens.pieChart.emptySizeRatioOptions.medium', {
|
||||
defaultMessage: 'Medium',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'emptySizeRatioOption-large',
|
||||
value: EMPTY_SIZE_RATIOS.LARGE,
|
||||
value: EmptySizeRatios.LARGE,
|
||||
label: i18n.translate('xpack.lens.pieChart.emptySizeRatioOptions.large', {
|
||||
defaultMessage: 'Large',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export const PartitionChartsMeta: Record<PieChartTypes, PartitionChartMeta> = {
|
||||
export const PartitionChartsMeta: Record<PieChartType, PartitionChartMeta> = {
|
||||
donut: {
|
||||
icon: LensIconChartDonut,
|
||||
label: i18n.translate('xpack.lens.pie.donutLabel', {
|
||||
defaultMessage: 'Donut',
|
||||
}),
|
||||
partitionType: PartitionLayout.sunburst,
|
||||
groupLabel,
|
||||
maxBuckets: 3,
|
||||
toolbarPopover: {
|
||||
|
@ -164,7 +155,6 @@ export const PartitionChartsMeta: Record<PieChartTypes, PartitionChartMeta> = {
|
|||
label: i18n.translate('xpack.lens.pie.pielabel', {
|
||||
defaultMessage: 'Pie',
|
||||
}),
|
||||
partitionType: PartitionLayout.sunburst,
|
||||
groupLabel,
|
||||
maxBuckets: 3,
|
||||
toolbarPopover: {
|
||||
|
@ -180,7 +170,6 @@ export const PartitionChartsMeta: Record<PieChartTypes, PartitionChartMeta> = {
|
|||
label: i18n.translate('xpack.lens.pie.treemaplabel', {
|
||||
defaultMessage: 'Treemap',
|
||||
}),
|
||||
partitionType: PartitionLayout.treemap,
|
||||
groupLabel,
|
||||
maxBuckets: 2,
|
||||
toolbarPopover: {
|
||||
|
@ -196,7 +185,6 @@ export const PartitionChartsMeta: Record<PieChartTypes, PartitionChartMeta> = {
|
|||
label: i18n.translate('xpack.lens.pie.mosaiclabel', {
|
||||
defaultMessage: 'Mosaic',
|
||||
}),
|
||||
partitionType: PartitionLayout.mosaic,
|
||||
groupLabel,
|
||||
maxBuckets: 2,
|
||||
isExperimental: true,
|
||||
|
@ -208,23 +196,12 @@ export const PartitionChartsMeta: Record<PieChartTypes, PartitionChartMeta> = {
|
|||
getShowLegendDefault: () => false,
|
||||
},
|
||||
requiredMinDimensionCount: 2,
|
||||
sortPredicate:
|
||||
(bucketColumns, sortingMap) =>
|
||||
([name1, node1], [, node2]) => {
|
||||
// Sorting for first group
|
||||
if (bucketColumns.length === 1 || (node1.children.length && name1 in sortingMap)) {
|
||||
return sortingMap[name1];
|
||||
}
|
||||
// Sorting for second group
|
||||
return node2.value - node1.value;
|
||||
},
|
||||
},
|
||||
waffle: {
|
||||
icon: LensIconChartWaffle,
|
||||
label: i18n.translate('xpack.lens.pie.wafflelabel', {
|
||||
defaultMessage: 'Waffle',
|
||||
}),
|
||||
partitionType: PartitionLayout.waffle,
|
||||
groupLabel,
|
||||
maxBuckets: 1,
|
||||
isExperimental: true,
|
||||
|
@ -239,9 +216,5 @@ export const PartitionChartsMeta: Record<PieChartTypes, PartitionChartMeta> = {
|
|||
hideNestedLegendSwitch: true,
|
||||
getShowLegendDefault: () => true,
|
||||
},
|
||||
sortPredicate:
|
||||
() =>
|
||||
([, node1], [, node2]) =>
|
||||
node2.value - node1.value,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,5 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './expression';
|
||||
export * from './visualization';
|
||||
|
|
|
@ -1,430 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Partition,
|
||||
SeriesIdentifier,
|
||||
Settings,
|
||||
NodeColorAccessor,
|
||||
ShapeTreeNode,
|
||||
HierarchyOfArrays,
|
||||
Chart,
|
||||
PartialTheme,
|
||||
} from '@elastic/charts';
|
||||
import { shallow } from 'enzyme';
|
||||
import type { LensMultiTable } from '../../common';
|
||||
import type { PieExpressionArgs } from '../../common/expressions';
|
||||
import { PieComponent } from './render_function';
|
||||
import { VisualizationContainer } from '../visualization_container';
|
||||
import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public';
|
||||
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
|
||||
import { LensIconChartDonut } from '../assets/chart_donut';
|
||||
|
||||
const chartsThemeService = chartPluginMock.createSetupContract().theme;
|
||||
|
||||
describe('PieVisualization component', () => {
|
||||
let getFormatSpy: jest.Mock;
|
||||
let convertSpy: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
convertSpy = jest.fn((x) => x);
|
||||
getFormatSpy = jest.fn();
|
||||
getFormatSpy.mockReturnValue({ convert: convertSpy });
|
||||
});
|
||||
|
||||
describe('legend options', () => {
|
||||
const data: LensMultiTable = {
|
||||
type: 'lens_multitable',
|
||||
tables: {
|
||||
first: {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{ id: 'a', name: 'a', meta: { type: 'number' } },
|
||||
{ id: 'b', name: 'b', meta: { type: 'string' } },
|
||||
{ id: 'c', name: 'c', meta: { type: 'number' } },
|
||||
],
|
||||
rows: [
|
||||
{ a: 6, b: 'I', c: 2, d: 'Row 1' },
|
||||
{ a: 1, b: 'J', c: 5, d: 'Row 2' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const args: PieExpressionArgs = {
|
||||
shape: 'pie',
|
||||
groups: ['a', 'b'],
|
||||
metric: 'c',
|
||||
numberDisplay: 'hidden',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
legendMaxLines: 1,
|
||||
truncateLegend: true,
|
||||
nestedLegend: false,
|
||||
percentDecimals: 3,
|
||||
hideLabels: false,
|
||||
palette: { name: 'mock', type: 'palette' },
|
||||
};
|
||||
|
||||
function getDefaultArgs() {
|
||||
return {
|
||||
data,
|
||||
formatFactory: getFormatSpy,
|
||||
onClickValue: jest.fn(),
|
||||
chartsThemeService,
|
||||
paletteService: chartPluginMock.createPaletteRegistry(),
|
||||
renderMode: 'view' as const,
|
||||
syncColors: false,
|
||||
};
|
||||
}
|
||||
|
||||
test('it shows legend on correct side', () => {
|
||||
const component = shallow(
|
||||
<PieComponent args={{ ...args, legendPosition: 'top' }} {...getDefaultArgs()} />
|
||||
);
|
||||
expect(component.find(Settings).prop('legendPosition')).toEqual('top');
|
||||
});
|
||||
|
||||
test('it shows legend for 2 groups using default legendDisplay', () => {
|
||||
const component = shallow(<PieComponent args={args} {...getDefaultArgs()} />);
|
||||
expect(component.find(Settings).prop('showLegend')).toEqual(true);
|
||||
});
|
||||
|
||||
test('it hides legend for 1 group using default legendDisplay', () => {
|
||||
const component = shallow(
|
||||
<PieComponent args={{ ...args, groups: ['a'] }} {...getDefaultArgs()} />
|
||||
);
|
||||
expect(component.find(Settings).prop('showLegend')).toEqual(false);
|
||||
});
|
||||
|
||||
test('it hides legend that would show otherwise in preview mode', () => {
|
||||
const component = shallow(
|
||||
<PieComponent args={{ ...args, hideLabels: true }} {...getDefaultArgs()} />
|
||||
);
|
||||
expect(component.find(Settings).prop('showLegend')).toEqual(false);
|
||||
});
|
||||
|
||||
test('it sets the correct lines per legend item', () => {
|
||||
const component = shallow(<PieComponent args={args} {...getDefaultArgs()} />);
|
||||
expect(component.find(Settings).prop<PartialTheme[]>('theme')[0]).toMatchObject({
|
||||
background: {
|
||||
color: undefined,
|
||||
},
|
||||
legend: {
|
||||
labelOptions: {
|
||||
maxLines: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('it calls the color function with the right series layers', () => {
|
||||
const defaultArgs = getDefaultArgs();
|
||||
const component = shallow(
|
||||
<PieComponent
|
||||
args={args}
|
||||
{...defaultArgs}
|
||||
data={{
|
||||
...data,
|
||||
tables: {
|
||||
first: {
|
||||
...data.tables.first,
|
||||
rows: [
|
||||
{ a: 'empty', b: 'first', c: 1, d: 'Row 1' },
|
||||
{ a: 'css', b: 'first', c: 1, d: 'Row 1' },
|
||||
{ a: 'css', b: 'second', c: 1, d: 'Row 1' },
|
||||
{ a: 'css', b: 'third', c: 1, d: 'Row 1' },
|
||||
{ a: 'gz', b: 'first', c: 1, d: 'Row 1' },
|
||||
],
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
(component.find(Partition).prop('layers')![1].shape!.fillColor as NodeColorAccessor)(
|
||||
{
|
||||
dataName: 'third',
|
||||
depth: 2,
|
||||
parent: {
|
||||
children: [
|
||||
['first', {}],
|
||||
['second', {}],
|
||||
['third', {}],
|
||||
],
|
||||
depth: 1,
|
||||
value: 200,
|
||||
dataName: 'css',
|
||||
parent: {
|
||||
children: [
|
||||
['empty', {}],
|
||||
['css', {}],
|
||||
['gz', {}],
|
||||
],
|
||||
depth: 0,
|
||||
sortIndex: 0,
|
||||
value: 500,
|
||||
},
|
||||
sortIndex: 1,
|
||||
},
|
||||
value: 41,
|
||||
sortIndex: 2,
|
||||
} as unknown as ShapeTreeNode,
|
||||
0,
|
||||
[] as HierarchyOfArrays
|
||||
);
|
||||
|
||||
expect(defaultArgs.paletteService.get('mock').getCategoricalColor).toHaveBeenCalledWith(
|
||||
[
|
||||
{
|
||||
name: 'css',
|
||||
rankAtDepth: 1,
|
||||
totalSeriesAtDepth: 3,
|
||||
},
|
||||
{
|
||||
name: 'third',
|
||||
rankAtDepth: 2,
|
||||
totalSeriesAtDepth: 3,
|
||||
},
|
||||
],
|
||||
{
|
||||
maxDepth: 2,
|
||||
totalSeries: 5,
|
||||
syncColors: false,
|
||||
behindText: true,
|
||||
},
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
test('it hides legend with 2 groups for treemap', () => {
|
||||
const component = shallow(
|
||||
<PieComponent args={{ ...args, shape: 'treemap' }} {...getDefaultArgs()} />
|
||||
);
|
||||
expect(component.find(Settings).prop('showLegend')).toEqual(false);
|
||||
});
|
||||
|
||||
test('it shows treemap legend only when forced on', () => {
|
||||
const component = shallow(
|
||||
<PieComponent
|
||||
args={{ ...args, legendDisplay: 'show', shape: 'treemap' }}
|
||||
{...getDefaultArgs()}
|
||||
/>
|
||||
);
|
||||
expect(component.find(Settings).prop('showLegend')).toEqual(true);
|
||||
});
|
||||
|
||||
test('it defaults to 1-level legend depth', () => {
|
||||
const component = shallow(<PieComponent args={args} {...getDefaultArgs()} />);
|
||||
expect(component.find(Settings).prop('legendMaxDepth')).toEqual(1);
|
||||
});
|
||||
|
||||
test('it shows nested legend only when forced on', () => {
|
||||
const component = shallow(
|
||||
<PieComponent args={{ ...args, nestedLegend: true }} {...getDefaultArgs()} />
|
||||
);
|
||||
expect(component.find(Settings).prop('legendMaxDepth')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('it calls filter callback with the given context', () => {
|
||||
const defaultArgs = getDefaultArgs();
|
||||
const component = shallow(<PieComponent args={{ ...args }} {...defaultArgs} />);
|
||||
component.find(Settings).first().prop('onElementClick')!([
|
||||
[
|
||||
[
|
||||
{
|
||||
groupByRollup: 6,
|
||||
value: 6,
|
||||
depth: 1,
|
||||
path: [],
|
||||
sortIndex: 1,
|
||||
smAccessorValue: '',
|
||||
},
|
||||
],
|
||||
{} as SeriesIdentifier,
|
||||
],
|
||||
]);
|
||||
|
||||
expect(defaultArgs.onClickValue.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"data": Array [
|
||||
Object {
|
||||
"column": 0,
|
||||
"row": 0,
|
||||
"table": Object {
|
||||
"columns": Array [
|
||||
Object {
|
||||
"id": "a",
|
||||
"meta": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"name": "a",
|
||||
},
|
||||
Object {
|
||||
"id": "b",
|
||||
"meta": Object {
|
||||
"type": "string",
|
||||
},
|
||||
"name": "b",
|
||||
},
|
||||
Object {
|
||||
"id": "c",
|
||||
"meta": Object {
|
||||
"type": "number",
|
||||
},
|
||||
"name": "c",
|
||||
},
|
||||
],
|
||||
"rows": Array [
|
||||
Object {
|
||||
"a": 6,
|
||||
"b": "I",
|
||||
"c": 2,
|
||||
"d": "Row 1",
|
||||
},
|
||||
Object {
|
||||
"a": 1,
|
||||
"b": "J",
|
||||
"c": 5,
|
||||
"d": "Row 2",
|
||||
},
|
||||
],
|
||||
"type": "datatable",
|
||||
},
|
||||
"value": 6,
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('does not set click listener and legend actions on non-interactive mode', () => {
|
||||
const defaultArgs = getDefaultArgs();
|
||||
const component = shallow(
|
||||
<PieComponent args={{ ...args }} {...defaultArgs} interactive={false} />
|
||||
);
|
||||
expect(component.find(Settings).first().prop('onElementClick')).toBeUndefined();
|
||||
expect(component.find(Settings).first().prop('legendAction')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('it renders the empty placeholder when metric contains only falsy data', () => {
|
||||
const defaultData = getDefaultArgs().data;
|
||||
const emptyData: LensMultiTable = {
|
||||
...defaultData,
|
||||
tables: {
|
||||
first: {
|
||||
...defaultData.tables.first,
|
||||
rows: [
|
||||
{ a: 0, b: 'I', c: 0, d: 'Row 1' },
|
||||
{ a: 0, b: 'J', c: null, d: 'Row 2' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const component = shallow(
|
||||
<PieComponent args={args} {...getDefaultArgs()} data={emptyData} />
|
||||
);
|
||||
expect(component.find(VisualizationContainer)).toHaveLength(1);
|
||||
expect(component.find(EmptyPlaceholder)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('it renders the chart when metric contains truthy data and buckets contain only falsy data', () => {
|
||||
const defaultData = getDefaultArgs().data;
|
||||
const emptyData: LensMultiTable = {
|
||||
...defaultData,
|
||||
tables: {
|
||||
first: {
|
||||
...defaultData.tables.first,
|
||||
// a and b are buckets, c is a metric
|
||||
rows: [{ a: 0, b: undefined, c: 12 }],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const component = shallow(
|
||||
<PieComponent args={args} {...getDefaultArgs()} data={emptyData} />
|
||||
);
|
||||
|
||||
expect(component.find(VisualizationContainer)).toHaveLength(1);
|
||||
expect(component.find(EmptyPlaceholder)).toHaveLength(0);
|
||||
expect(component.find(Chart)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('it shows emptyPlaceholder for undefined grouped data', () => {
|
||||
const defaultData = getDefaultArgs().data;
|
||||
const emptyData: LensMultiTable = {
|
||||
...defaultData,
|
||||
tables: {
|
||||
first: {
|
||||
...defaultData.tables.first,
|
||||
rows: [
|
||||
{ a: undefined, b: 'I', c: undefined, d: 'Row 1' },
|
||||
{ a: undefined, b: 'J', c: undefined, d: 'Row 2' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const component = shallow(
|
||||
<PieComponent args={args} {...getDefaultArgs()} data={emptyData} />
|
||||
);
|
||||
expect(component.find(VisualizationContainer)).toHaveLength(1);
|
||||
expect(component.find(EmptyPlaceholder).prop('icon')).toEqual(LensIconChartDonut);
|
||||
});
|
||||
|
||||
test('it should dynamically shrink the chart area to when some small slices are detected', () => {
|
||||
const defaultData = getDefaultArgs().data;
|
||||
const emptyData: LensMultiTable = {
|
||||
...defaultData,
|
||||
tables: {
|
||||
first: {
|
||||
...defaultData.tables.first,
|
||||
rows: [
|
||||
{ a: 60, b: 'I', c: 200, d: 'Row 1' },
|
||||
{ a: 1, b: 'J', c: 0.1, d: 'Row 2' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const component = shallow(
|
||||
<PieComponent args={args} {...getDefaultArgs()} data={emptyData} />
|
||||
);
|
||||
expect(
|
||||
component.find(Settings).prop<PartialTheme[]>('theme')[0].partition?.outerSizeRatio
|
||||
).toBeCloseTo(1 / 1.05);
|
||||
});
|
||||
|
||||
test('it should bound the shrink the chart area to ~20% when some small slices are detected', () => {
|
||||
const defaultData = getDefaultArgs().data;
|
||||
const emptyData: LensMultiTable = {
|
||||
...defaultData,
|
||||
tables: {
|
||||
first: {
|
||||
...defaultData.tables.first,
|
||||
rows: [
|
||||
{ a: 60, b: 'I', c: 200, d: 'Row 1' },
|
||||
{ a: 1, b: 'J', c: 0.1, d: 'Row 2' },
|
||||
{ a: 1, b: 'K', c: 0.1, d: 'Row 3' },
|
||||
{ a: 1, b: 'G', c: 0.1, d: 'Row 4' },
|
||||
{ a: 1, b: 'H', c: 0.1, d: 'Row 5' },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const component = shallow(
|
||||
<PieComponent args={args} {...getDefaultArgs()} data={emptyData} />
|
||||
);
|
||||
expect(
|
||||
component.find(Settings).prop<PartialTheme[]>('theme')[0].partition?.outerSizeRatio
|
||||
).toBeCloseTo(1 / 1.2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,354 +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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { uniq } from 'lodash';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { Required } from '@kbn/utility-types';
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import {
|
||||
Chart,
|
||||
Datum,
|
||||
LayerValue,
|
||||
Partition,
|
||||
PartitionLayer,
|
||||
Position,
|
||||
Settings,
|
||||
ElementClickListener,
|
||||
PartialTheme,
|
||||
} from '@elastic/charts';
|
||||
import { RenderMode } from 'src/plugins/expressions';
|
||||
import type { LensFilterEvent } from '../types';
|
||||
import { VisualizationContainer } from '../visualization_container';
|
||||
import { DEFAULT_PERCENT_DECIMALS } from './constants';
|
||||
import { PartitionChartsMeta } from './partition_charts_meta';
|
||||
import type { FormatFactory } from '../../common';
|
||||
import type { PieExpressionProps } from '../../common/expressions';
|
||||
import {
|
||||
getSliceValue,
|
||||
getFilterContext,
|
||||
isTreemapOrMosaicShape,
|
||||
byDataColorPaletteMap,
|
||||
extractUniqTermsMap,
|
||||
} from './render_helpers';
|
||||
import { EmptyPlaceholder } from '../../../../../src/plugins/charts/public';
|
||||
import './visualization.scss';
|
||||
import {
|
||||
ChartsPluginSetup,
|
||||
PaletteRegistry,
|
||||
SeriesLayer,
|
||||
} from '../../../../../src/plugins/charts/public';
|
||||
import { LensIconChartDonut } from '../assets/chart_donut';
|
||||
import { getLegendAction } from './get_legend_action';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
/**
|
||||
* Flag used to enable debugState on elastic charts
|
||||
*/
|
||||
_echDebugStateFlag?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
const EMPTY_SLICE = Symbol('empty_slice');
|
||||
|
||||
export function PieComponent(
|
||||
props: PieExpressionProps & {
|
||||
formatFactory: FormatFactory;
|
||||
chartsThemeService: ChartsPluginSetup['theme'];
|
||||
interactive?: boolean;
|
||||
paletteService: PaletteRegistry;
|
||||
onClickValue: (data: LensFilterEvent['data']) => void;
|
||||
renderMode: RenderMode;
|
||||
syncColors: boolean;
|
||||
}
|
||||
) {
|
||||
const [firstTable] = Object.values(props.data.tables);
|
||||
const formatters: Record<string, ReturnType<FormatFactory>> = {};
|
||||
|
||||
const { chartsThemeService, paletteService, syncColors, onClickValue } = props;
|
||||
const {
|
||||
shape,
|
||||
groups,
|
||||
metric,
|
||||
numberDisplay,
|
||||
categoryDisplay,
|
||||
legendDisplay,
|
||||
legendPosition,
|
||||
nestedLegend,
|
||||
percentDecimals,
|
||||
emptySizeRatio,
|
||||
legendMaxLines,
|
||||
truncateLegend,
|
||||
hideLabels,
|
||||
palette,
|
||||
showValuesInLegend,
|
||||
} = props.args;
|
||||
const chartTheme = chartsThemeService.useChartsTheme();
|
||||
const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
|
||||
const isDarkMode = chartsThemeService.useDarkMode();
|
||||
|
||||
if (!hideLabels) {
|
||||
firstTable.columns.forEach((column) => {
|
||||
formatters[column.id] = props.formatFactory(column.meta.params);
|
||||
});
|
||||
}
|
||||
|
||||
const fillLabel: PartitionLayer['fillLabel'] = {
|
||||
valueFont: {
|
||||
fontWeight: 700,
|
||||
},
|
||||
};
|
||||
|
||||
if (numberDisplay === 'hidden') {
|
||||
// Hides numbers from appearing inside chart, but they still appear in linkLabel
|
||||
// and tooltips.
|
||||
fillLabel.valueFormatter = () => '';
|
||||
}
|
||||
|
||||
const bucketColumns = firstTable.columns.filter((col) => groups.includes(col.id));
|
||||
const totalSeriesCount = uniq(
|
||||
firstTable.rows.map((row) => {
|
||||
return bucketColumns.map(({ id: columnId }) => row[columnId]).join(',');
|
||||
})
|
||||
).length;
|
||||
|
||||
const shouldUseByDataPalette = !syncColors && ['mosaic'].includes(shape) && bucketColumns[1]?.id;
|
||||
let byDataPalette: ReturnType<typeof byDataColorPaletteMap>;
|
||||
if (shouldUseByDataPalette) {
|
||||
byDataPalette = byDataColorPaletteMap(
|
||||
firstTable,
|
||||
bucketColumns[1].id,
|
||||
paletteService.get(palette.name),
|
||||
palette
|
||||
);
|
||||
}
|
||||
|
||||
let sortingMap: Record<string, number> = {};
|
||||
if (shape === 'mosaic') {
|
||||
sortingMap = extractUniqTermsMap(firstTable, bucketColumns[0].id);
|
||||
}
|
||||
|
||||
const layers: PartitionLayer[] = bucketColumns.map((col, layerIndex) => {
|
||||
return {
|
||||
groupByRollup: (d: Datum) => d[col.id] ?? EMPTY_SLICE,
|
||||
showAccessor: (d: Datum) => d !== EMPTY_SLICE,
|
||||
nodeLabel: (d: unknown) => {
|
||||
if (hideLabels || d === EMPTY_SLICE) {
|
||||
return '';
|
||||
}
|
||||
if (col.meta.params) {
|
||||
return formatters[col.id].convert(d) ?? '';
|
||||
}
|
||||
return String(d);
|
||||
},
|
||||
fillLabel,
|
||||
sortPredicate: PartitionChartsMeta[shape].sortPredicate?.(bucketColumns, sortingMap),
|
||||
shape: {
|
||||
fillColor: (d) => {
|
||||
const seriesLayers: SeriesLayer[] = [];
|
||||
|
||||
// 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)';
|
||||
|
||||
// Color is determined by round-robin on the index of the innermost slice
|
||||
// This has to be done recursively until we get to the slice index
|
||||
let tempParent: typeof d | typeof d['parent'] = d;
|
||||
|
||||
while (tempParent.parent && tempParent.depth > 0) {
|
||||
seriesLayers.unshift({
|
||||
name: String(tempParent.parent.children[tempParent.sortIndex][0]),
|
||||
rankAtDepth: tempParent.sortIndex,
|
||||
totalSeriesAtDepth: tempParent.parent.children.length,
|
||||
});
|
||||
tempParent = tempParent.parent;
|
||||
}
|
||||
|
||||
if (byDataPalette && seriesLayers[1]) {
|
||||
return byDataPalette.getColor(seriesLayers[1].name) || defaultColor;
|
||||
}
|
||||
|
||||
if (isTreemapOrMosaicShape(shape)) {
|
||||
// Only highlight the innermost color of the treemap, as it accurately represents area
|
||||
if (layerIndex < bucketColumns.length - 1) {
|
||||
return defaultColor;
|
||||
}
|
||||
// only use the top level series layer for coloring
|
||||
if (seriesLayers.length > 1) {
|
||||
seriesLayers.pop();
|
||||
}
|
||||
}
|
||||
|
||||
const outputColor = paletteService.get(palette.name).getCategoricalColor(
|
||||
seriesLayers,
|
||||
{
|
||||
behindText: categoryDisplay !== 'hide' || isTreemapOrMosaicShape(shape),
|
||||
maxDepth: bucketColumns.length,
|
||||
totalSeries: totalSeriesCount,
|
||||
syncColors,
|
||||
},
|
||||
palette.params
|
||||
);
|
||||
|
||||
return outputColor || defaultColor;
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const { legend, partitionType, label: chartType } = PartitionChartsMeta[shape];
|
||||
|
||||
const themeOverrides: Required<PartialTheme, 'partition'> = {
|
||||
chartMargins: { top: 0, bottom: 0, left: 0, right: 0 },
|
||||
background: {
|
||||
color: undefined, // removes background for embeddables
|
||||
},
|
||||
legend: {
|
||||
labelOptions: { maxLines: truncateLegend ? legendMaxLines ?? 1 : 0 },
|
||||
},
|
||||
partition: {
|
||||
fontFamily: chartTheme.barSeriesStyle?.displayValue?.fontFamily,
|
||||
outerSizeRatio: 1,
|
||||
minFontSize: 10,
|
||||
maxFontSize: 16,
|
||||
// Labels are added outside the outer ring when the slice is too small
|
||||
linkLabel: {
|
||||
maxCount: 5,
|
||||
fontSize: 11,
|
||||
// Dashboard background color is affected by dark mode, which we need
|
||||
// to account for in outer labels
|
||||
// This does not handle non-dashboard embeddables, which are allowed to
|
||||
// have different backgrounds.
|
||||
textColor: chartTheme.axes?.axisTitle?.fill,
|
||||
},
|
||||
sectorLineStroke: chartTheme.lineSeriesStyle?.point?.fill,
|
||||
sectorLineWidth: 1.5,
|
||||
circlePadding: 4,
|
||||
},
|
||||
};
|
||||
if (isTreemapOrMosaicShape(shape)) {
|
||||
if (hideLabels || categoryDisplay === 'hide') {
|
||||
themeOverrides.partition.fillLabel = { textColor: 'rgba(0,0,0,0)' };
|
||||
}
|
||||
} else {
|
||||
themeOverrides.partition.emptySizeRatio = shape === 'donut' ? emptySizeRatio : 0;
|
||||
|
||||
if (hideLabels || categoryDisplay === 'hide') {
|
||||
// Force all labels to be linked, then prevent links from showing
|
||||
themeOverrides.partition.linkLabel = {
|
||||
maxCount: 0,
|
||||
maximumSection: Number.POSITIVE_INFINITY,
|
||||
};
|
||||
} else if (categoryDisplay === 'inside') {
|
||||
// Prevent links from showing
|
||||
themeOverrides.partition.linkLabel = { maxCount: 0 };
|
||||
} else {
|
||||
// if it contains any slice below 2% reduce the ratio
|
||||
// first step: sum it up the overall sum
|
||||
const overallSum = firstTable.rows.reduce((sum, row) => sum + row[metric!], 0);
|
||||
const slices = firstTable.rows.map((row) => row[metric!] / overallSum);
|
||||
const smallSlices = slices.filter((value) => value < 0.02).length;
|
||||
if (smallSlices) {
|
||||
// shrink up to 20% to give some room for the linked values
|
||||
themeOverrides.partition.outerSizeRatio = 1 / (1 + Math.min(smallSlices * 0.05, 0.2));
|
||||
}
|
||||
}
|
||||
}
|
||||
const metricColumn = firstTable.columns.find((c) => c.id === metric)!;
|
||||
const percentFormatter = props.formatFactory({
|
||||
id: 'percent',
|
||||
params: {
|
||||
pattern: `0,0.[${'0'.repeat(percentDecimals ?? DEFAULT_PERCENT_DECIMALS)}]%`,
|
||||
},
|
||||
});
|
||||
|
||||
const hasNegative = firstTable.rows.some((row) => {
|
||||
const value = row[metricColumn.id];
|
||||
return typeof value === 'number' && value < 0;
|
||||
});
|
||||
|
||||
const isMetricEmpty = firstTable.rows.every((row) => {
|
||||
return !row[metricColumn.id];
|
||||
});
|
||||
|
||||
const isEmpty =
|
||||
firstTable.rows.length === 0 ||
|
||||
firstTable.rows.every((row) => groups.every((colId) => typeof row[colId] === 'undefined')) ||
|
||||
isMetricEmpty;
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<VisualizationContainer className="lnsPieExpression__container">
|
||||
<EmptyPlaceholder icon={LensIconChartDonut} />
|
||||
</VisualizationContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasNegative) {
|
||||
return (
|
||||
<EuiText className="lnsChart__empty" textAlign="center" color="subdued" size="xs">
|
||||
<FormattedMessage
|
||||
id="xpack.lens.pie.pieWithNegativeWarningLabel"
|
||||
defaultMessage="{chartType} charts can't render with negative values."
|
||||
values={{
|
||||
chartType,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
);
|
||||
}
|
||||
|
||||
const onElementClickHandler: ElementClickListener = (args) => {
|
||||
const context = getFilterContext(args[0][0] as LayerValue[], groups, firstTable);
|
||||
|
||||
onClickValue(context);
|
||||
};
|
||||
|
||||
return (
|
||||
<VisualizationContainer className="lnsPieExpression__container">
|
||||
<Chart>
|
||||
<Settings
|
||||
tooltip={{ boundary: document.getElementById('app-fixed-viewport') ?? undefined }}
|
||||
debugState={window._echDebugStateFlag ?? false}
|
||||
// Legend is hidden in many scenarios
|
||||
// - Tiny preview
|
||||
// - Treemap does not need a legend because it uses category labels
|
||||
// - Single layer pie/donut usually shows text, does not need legend
|
||||
showLegend={
|
||||
!hideLabels &&
|
||||
(legendDisplay === 'show' ||
|
||||
(legendDisplay === 'default' &&
|
||||
(legend.getShowLegendDefault?.(bucketColumns) ?? false)))
|
||||
}
|
||||
flatLegend={legend.flat}
|
||||
showLegendExtra={showValuesInLegend}
|
||||
legendPosition={legendPosition || Position.Right}
|
||||
legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */}
|
||||
onElementClick={props.interactive ?? true ? onElementClickHandler : undefined}
|
||||
legendAction={props.interactive ? getLegendAction(firstTable, onClickValue) : undefined}
|
||||
theme={[themeOverrides, chartTheme]}
|
||||
baseTheme={chartBaseTheme}
|
||||
ariaLabel={props.args.ariaLabel}
|
||||
ariaUseDefaultSummary={!props.args.ariaLabel}
|
||||
/>
|
||||
<Partition
|
||||
id={shape}
|
||||
data={firstTable.rows}
|
||||
layout={partitionType}
|
||||
specialFirstInnermostSector
|
||||
valueAccessor={(d: Datum) => getSliceValue(d, metricColumn)}
|
||||
percentFormatter={(d: number) => percentFormatter.convert(d / 100)}
|
||||
valueGetter={hideLabels || numberDisplay === 'value' ? undefined : 'percent'}
|
||||
valueFormatter={(d: number) => (hideLabels ? '' : formatters[metricColumn.id].convert(d))}
|
||||
layers={layers}
|
||||
topGroove={hideLabels || categoryDisplay === 'hide' ? 0 : undefined}
|
||||
/>
|
||||
</Chart>
|
||||
</VisualizationContainer>
|
||||
);
|
||||
}
|
|
@ -6,321 +6,11 @@
|
|||
*/
|
||||
|
||||
import type { Datatable } from 'src/plugins/expressions/public';
|
||||
import type { PaletteDefinition, PaletteOutput } from 'src/plugins/charts/public';
|
||||
|
||||
import {
|
||||
getSliceValue,
|
||||
getFilterContext,
|
||||
byDataColorPaletteMap,
|
||||
extractUniqTermsMap,
|
||||
checkTableForContainsSmallValues,
|
||||
shouldShowValuesInLegend,
|
||||
} from './render_helpers';
|
||||
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
|
||||
import type { PieLayerState } from '../../common/expressions';
|
||||
import { checkTableForContainsSmallValues, shouldShowValuesInLegend } from './render_helpers';
|
||||
import { PieLayerState, PieChartTypes } from '../../common';
|
||||
|
||||
describe('render helpers', () => {
|
||||
describe('#getSliceValue', () => {
|
||||
it('returns the metric when positive number', () => {
|
||||
expect(
|
||||
getSliceValue(
|
||||
{ a: 'Cat', b: 'Home', c: 5 },
|
||||
{
|
||||
id: 'c',
|
||||
name: 'C',
|
||||
meta: { type: 'number' },
|
||||
}
|
||||
)
|
||||
).toEqual(5);
|
||||
});
|
||||
|
||||
it('returns the metric when negative number', () => {
|
||||
expect(
|
||||
getSliceValue(
|
||||
{ a: 'Cat', b: 'Home', c: -100 },
|
||||
{
|
||||
id: 'c',
|
||||
name: 'C',
|
||||
meta: { type: 'number' },
|
||||
}
|
||||
)
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it('returns 0 when metric value is 0', () => {
|
||||
expect(
|
||||
getSliceValue(
|
||||
{ a: 'Cat', b: 'Home', c: 0 },
|
||||
{
|
||||
id: 'c',
|
||||
name: 'C',
|
||||
meta: { type: 'number' },
|
||||
}
|
||||
)
|
||||
).toEqual(0);
|
||||
});
|
||||
|
||||
it('returns 0 when metric value is infinite', () => {
|
||||
expect(
|
||||
getSliceValue(
|
||||
{ a: 'Cat', b: 'Home', c: Number.POSITIVE_INFINITY },
|
||||
{
|
||||
id: 'c',
|
||||
name: 'C',
|
||||
meta: { type: 'number' },
|
||||
}
|
||||
)
|
||||
).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFilterContext', () => {
|
||||
it('handles single slice click for single ring', () => {
|
||||
const table: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{ id: 'a', name: 'A', meta: { type: 'string' } },
|
||||
{ id: 'b', name: 'B', meta: { type: 'number' } },
|
||||
],
|
||||
rows: [
|
||||
{ a: 'Hi', b: 2 },
|
||||
{ a: 'Test', b: 4 },
|
||||
{ a: 'Foo', b: 6 },
|
||||
],
|
||||
};
|
||||
expect(
|
||||
getFilterContext(
|
||||
[
|
||||
{
|
||||
groupByRollup: 'Test',
|
||||
value: 100,
|
||||
depth: 1,
|
||||
path: [],
|
||||
sortIndex: 1,
|
||||
smAccessorValue: '',
|
||||
},
|
||||
],
|
||||
['a'],
|
||||
table
|
||||
)
|
||||
).toEqual({
|
||||
data: [
|
||||
{
|
||||
row: 1,
|
||||
column: 0,
|
||||
value: 'Test',
|
||||
table,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles single slice click with 2 rings', () => {
|
||||
const table: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{ id: 'a', name: 'A', meta: { type: 'string' } },
|
||||
{ id: 'b', name: 'B', meta: { type: 'string' } },
|
||||
{ id: 'c', name: 'C', meta: { type: 'number' } },
|
||||
],
|
||||
rows: [
|
||||
{ a: 'Hi', b: 'Two', c: 2 },
|
||||
{ a: 'Test', b: 'Two', c: 5 },
|
||||
{ a: 'Foo', b: 'Three', c: 6 },
|
||||
],
|
||||
};
|
||||
expect(
|
||||
getFilterContext(
|
||||
[
|
||||
{
|
||||
groupByRollup: 'Test',
|
||||
value: 100,
|
||||
depth: 1,
|
||||
path: [],
|
||||
sortIndex: 1,
|
||||
smAccessorValue: '',
|
||||
},
|
||||
],
|
||||
['a', 'b'],
|
||||
table
|
||||
)
|
||||
).toEqual({
|
||||
data: [
|
||||
{
|
||||
row: 1,
|
||||
column: 0,
|
||||
value: 'Test',
|
||||
table,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('finds right row for multi slice click', () => {
|
||||
const table: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{ id: 'a', name: 'A', meta: { type: 'string' } },
|
||||
{ id: 'b', name: 'B', meta: { type: 'string' } },
|
||||
{ id: 'c', name: 'C', meta: { type: 'number' } },
|
||||
],
|
||||
rows: [
|
||||
{ a: 'Hi', b: 'Two', c: 2 },
|
||||
{ a: 'Test', b: 'Two', c: 5 },
|
||||
{ a: 'Foo', b: 'Three', c: 6 },
|
||||
],
|
||||
};
|
||||
expect(
|
||||
getFilterContext(
|
||||
[
|
||||
{
|
||||
groupByRollup: 'Test',
|
||||
value: 100,
|
||||
depth: 1,
|
||||
path: [],
|
||||
sortIndex: 1,
|
||||
smAccessorValue: '',
|
||||
},
|
||||
{
|
||||
groupByRollup: 'Two',
|
||||
value: 5,
|
||||
depth: 1,
|
||||
path: [],
|
||||
sortIndex: 1,
|
||||
smAccessorValue: '',
|
||||
},
|
||||
],
|
||||
['a', 'b'],
|
||||
table
|
||||
)
|
||||
).toEqual({
|
||||
data: [
|
||||
{
|
||||
row: 1,
|
||||
column: 0,
|
||||
value: 'Test',
|
||||
table,
|
||||
},
|
||||
{
|
||||
row: 1,
|
||||
column: 1,
|
||||
value: 'Two',
|
||||
table,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#extractUniqTermsMap', () => {
|
||||
it('should extract map', () => {
|
||||
const table: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
{ id: 'a', name: 'A', meta: { type: 'string' } },
|
||||
{ id: 'b', name: 'B', meta: { type: 'string' } },
|
||||
{ id: 'c', name: 'C', meta: { type: 'number' } },
|
||||
],
|
||||
rows: [
|
||||
{ a: 'Hi', b: 'Two', c: 2 },
|
||||
{ a: 'Test', b: 'Two', c: 5 },
|
||||
{ a: 'Foo', b: 'Three', c: 6 },
|
||||
],
|
||||
};
|
||||
expect(extractUniqTermsMap(table, 'a')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"Foo": 2,
|
||||
"Hi": 0,
|
||||
"Test": 1,
|
||||
}
|
||||
`);
|
||||
expect(extractUniqTermsMap(table, 'b')).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"Three": 1,
|
||||
"Two": 0,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#byDataColorPaletteMap', () => {
|
||||
let datatable: Datatable;
|
||||
let paletteDefinition: PaletteDefinition;
|
||||
let palette: PaletteOutput;
|
||||
const columnId = 'foo';
|
||||
|
||||
beforeEach(() => {
|
||||
datatable = {
|
||||
rows: [
|
||||
{
|
||||
[columnId]: '1',
|
||||
},
|
||||
{
|
||||
[columnId]: '2',
|
||||
},
|
||||
],
|
||||
} as unknown as Datatable;
|
||||
paletteDefinition = chartPluginMock.createPaletteRegistry().get('default');
|
||||
palette = { type: 'palette' } as PaletteOutput;
|
||||
});
|
||||
|
||||
it('should create byDataColorPaletteMap', () => {
|
||||
expect(byDataColorPaletteMap(datatable, columnId, paletteDefinition, palette))
|
||||
.toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"getColor": [Function],
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should get color', () => {
|
||||
const colorPaletteMap = byDataColorPaletteMap(
|
||||
datatable,
|
||||
columnId,
|
||||
paletteDefinition,
|
||||
palette
|
||||
);
|
||||
|
||||
expect(colorPaletteMap.getColor('1')).toBe('black');
|
||||
});
|
||||
|
||||
it('should return undefined in case if values not in datatable', () => {
|
||||
const colorPaletteMap = byDataColorPaletteMap(
|
||||
datatable,
|
||||
columnId,
|
||||
paletteDefinition,
|
||||
palette
|
||||
);
|
||||
|
||||
expect(colorPaletteMap.getColor('wrong')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should increase rankAtDepth for each new value', () => {
|
||||
const colorPaletteMap = byDataColorPaletteMap(
|
||||
datatable,
|
||||
columnId,
|
||||
paletteDefinition,
|
||||
palette
|
||||
);
|
||||
colorPaletteMap.getColor('1');
|
||||
colorPaletteMap.getColor('2');
|
||||
|
||||
expect(paletteDefinition.getCategoricalColor).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
[{ name: '1', rankAtDepth: 0, totalSeriesAtDepth: 2 }],
|
||||
{ behindText: false },
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(paletteDefinition.getCategoricalColor).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
[{ name: '2', rankAtDepth: 1, totalSeriesAtDepth: 2 }],
|
||||
{ behindText: false },
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#checkTableForContainsSmallValues', () => {
|
||||
let datatable: Datatable;
|
||||
const columnId = 'foo';
|
||||
|
@ -380,23 +70,35 @@ describe('render helpers', () => {
|
|||
describe('#shouldShowValuesInLegend', () => {
|
||||
it('should firstly read the state value', () => {
|
||||
expect(
|
||||
shouldShowValuesInLegend({ showValuesInLegend: true } as PieLayerState, 'waffle')
|
||||
shouldShowValuesInLegend(
|
||||
{ showValuesInLegend: true } as PieLayerState,
|
||||
PieChartTypes.WAFFLE
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
shouldShowValuesInLegend({ showValuesInLegend: false } as PieLayerState, 'waffle')
|
||||
shouldShowValuesInLegend(
|
||||
{ showValuesInLegend: false } as PieLayerState,
|
||||
PieChartTypes.WAFFLE
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should read value from meta in case of value in state is undefined', () => {
|
||||
expect(
|
||||
shouldShowValuesInLegend({ showValuesInLegend: undefined } as PieLayerState, 'waffle')
|
||||
shouldShowValuesInLegend(
|
||||
{ showValuesInLegend: undefined } as PieLayerState,
|
||||
PieChartTypes.WAFFLE
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
expect(shouldShowValuesInLegend({} as PieLayerState, 'waffle')).toBeTruthy();
|
||||
expect(shouldShowValuesInLegend({} as PieLayerState, PieChartTypes.WAFFLE)).toBeTruthy();
|
||||
|
||||
expect(
|
||||
shouldShowValuesInLegend({ showValuesInLegend: undefined } as PieLayerState, 'pie')
|
||||
shouldShowValuesInLegend(
|
||||
{ showValuesInLegend: undefined } as PieLayerState,
|
||||
PieChartTypes.PIE
|
||||
)
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,47 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { Datum, LayerValue } from '@elastic/charts';
|
||||
import type { Datatable, DatatableColumn } from 'src/plugins/expressions/public';
|
||||
import type { LensFilterEvent } from '../types';
|
||||
import type { PieChartTypes, PieLayerState } from '../../common/expressions/pie_chart/types';
|
||||
import type { PaletteDefinition, PaletteOutput } from '../../../../../src/plugins/charts/public';
|
||||
import type { Datatable } from 'src/plugins/expressions/public';
|
||||
import type { PieChartType, PieLayerState } from '../../common/types';
|
||||
import { PartitionChartsMeta } from './partition_charts_meta';
|
||||
|
||||
export function getSliceValue(d: Datum, metricColumn: DatatableColumn) {
|
||||
const value = d[metricColumn.id];
|
||||
return Number.isFinite(value) && value >= 0 ? value : 0;
|
||||
}
|
||||
|
||||
export function getFilterContext(
|
||||
clickedLayers: LayerValue[],
|
||||
layerColumnIds: string[],
|
||||
table: Datatable
|
||||
): LensFilterEvent['data'] {
|
||||
const matchingIndex = table.rows.findIndex((row) =>
|
||||
clickedLayers.every((layer, index) => {
|
||||
const columnId = layerColumnIds[index];
|
||||
return row[columnId] === layer.groupByRollup;
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
data: clickedLayers.map((clickedLayer, index) => ({
|
||||
column: table.columns.findIndex((col) => col.id === layerColumnIds[index]),
|
||||
row: matchingIndex,
|
||||
value: clickedLayer.groupByRollup,
|
||||
table,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export const isPartitionShape = (shape: PieChartTypes | string) =>
|
||||
export const isPartitionShape = (shape: PieChartType | string) =>
|
||||
['donut', 'pie', 'treemap', 'mosaic', 'waffle'].includes(shape);
|
||||
|
||||
export const isTreemapOrMosaicShape = (shape: PieChartTypes | string) =>
|
||||
['treemap', 'mosaic'].includes(shape);
|
||||
|
||||
export const shouldShowValuesInLegend = (layer: PieLayerState, shape: PieChartTypes) => {
|
||||
export const shouldShowValuesInLegend = (layer: PieLayerState, shape: PieChartType) => {
|
||||
if ('showValues' in PartitionChartsMeta[shape]?.legend) {
|
||||
return layer.showValuesInLegend ?? PartitionChartsMeta[shape]?.legend?.showValues ?? true;
|
||||
}
|
||||
|
@ -53,58 +20,6 @@ export const shouldShowValuesInLegend = (layer: PieLayerState, shape: PieChartTy
|
|||
return false;
|
||||
};
|
||||
|
||||
export const extractUniqTermsMap = (dataTable: Datatable, columnId: string) =>
|
||||
[...new Set(dataTable.rows.map((item) => item[columnId]))].reduce(
|
||||
(acc, item, index) => ({
|
||||
...acc,
|
||||
[item]: index,
|
||||
}),
|
||||
{}
|
||||
);
|
||||
|
||||
export const byDataColorPaletteMap = (
|
||||
dataTable: Datatable,
|
||||
columnId: string,
|
||||
paletteDefinition: PaletteDefinition,
|
||||
{ params }: PaletteOutput
|
||||
) => {
|
||||
const colorMap = new Map<string, string | undefined>(
|
||||
dataTable.rows.map((item) => [String(item[columnId]), undefined])
|
||||
);
|
||||
let rankAtDepth = 0;
|
||||
|
||||
return {
|
||||
getColor: (item: unknown) => {
|
||||
const key = String(item);
|
||||
|
||||
if (colorMap.has(key)) {
|
||||
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;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const checkTableForContainsSmallValues = (
|
||||
dataTable: Datatable,
|
||||
columnId: string,
|
||||
|
|
|
@ -8,7 +8,14 @@
|
|||
import { PaletteOutput } from 'src/plugins/charts/public';
|
||||
import { suggestions } from './suggestions';
|
||||
import type { DataType, SuggestionRequest } from '../types';
|
||||
import type { PieLayerState, PieVisualizationState } from '../../common/expressions';
|
||||
import {
|
||||
CategoryDisplay,
|
||||
LegendDisplay,
|
||||
NumberDisplay,
|
||||
PieChartTypes,
|
||||
PieLayerState,
|
||||
PieVisualizationState,
|
||||
} from '../../common';
|
||||
import { layerTypes } from '../../common';
|
||||
|
||||
describe('suggestions', () => {
|
||||
|
@ -53,16 +60,16 @@ describe('suggestions', () => {
|
|||
changeType: 'unchanged',
|
||||
},
|
||||
state: {
|
||||
shape: 'pie',
|
||||
shape: PieChartTypes.PIE,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
layerType: layerTypes.DATA,
|
||||
groups: [],
|
||||
metric: 'a',
|
||||
numberDisplay: 'hidden',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.HIDDEN,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -168,7 +175,7 @@ describe('suggestions', () => {
|
|||
changeType: 'initial',
|
||||
},
|
||||
state: {
|
||||
shape: 'mosaic',
|
||||
shape: PieChartTypes.MOSAIC,
|
||||
layers: [{} as PieLayerState],
|
||||
},
|
||||
keptLayerIds: ['first'],
|
||||
|
@ -380,7 +387,7 @@ describe('suggestions', () => {
|
|||
|
||||
expect(results).toContainEqual(
|
||||
expect.objectContaining({
|
||||
state: expect.objectContaining({ shape: 'donut' }),
|
||||
state: expect.objectContaining({ shape: PieChartTypes.DONUT }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -412,7 +419,7 @@ describe('suggestions', () => {
|
|||
|
||||
expect(results).toContainEqual(
|
||||
expect.objectContaining({
|
||||
state: expect.objectContaining({ shape: 'pie' }),
|
||||
state: expect.objectContaining({ shape: PieChartTypes.PIE }),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@ -542,7 +549,7 @@ describe('suggestions', () => {
|
|||
changeType: 'unchanged',
|
||||
},
|
||||
state: {
|
||||
shape: 'treemap',
|
||||
shape: PieChartTypes.TREEMAP,
|
||||
palette,
|
||||
layers: [
|
||||
{
|
||||
|
@ -551,9 +558,9 @@ describe('suggestions', () => {
|
|||
groups: ['a'],
|
||||
metric: 'b',
|
||||
|
||||
numberDisplay: 'hidden',
|
||||
categoryDisplay: 'inside',
|
||||
legendDisplay: 'show',
|
||||
numberDisplay: NumberDisplay.HIDDEN,
|
||||
categoryDisplay: CategoryDisplay.INSIDE,
|
||||
legendDisplay: LegendDisplay.SHOW,
|
||||
percentDecimals: 0,
|
||||
legendMaxLines: 1,
|
||||
truncateLegend: true,
|
||||
|
@ -566,7 +573,7 @@ describe('suggestions', () => {
|
|||
).toContainEqual(
|
||||
expect.objectContaining({
|
||||
state: {
|
||||
shape: 'donut',
|
||||
shape: PieChartTypes.DONUT,
|
||||
palette,
|
||||
layers: [
|
||||
{
|
||||
|
@ -575,8 +582,8 @@ describe('suggestions', () => {
|
|||
groups: ['a'],
|
||||
metric: 'b',
|
||||
|
||||
numberDisplay: 'hidden',
|
||||
categoryDisplay: 'inside',
|
||||
numberDisplay: NumberDisplay.HIDDEN,
|
||||
categoryDisplay: CategoryDisplay.INSIDE,
|
||||
legendDisplay: 'show',
|
||||
percentDecimals: 0,
|
||||
legendMaxLines: 1,
|
||||
|
@ -601,7 +608,7 @@ describe('suggestions', () => {
|
|||
changeType: 'unchanged',
|
||||
},
|
||||
state: {
|
||||
shape: 'treemap',
|
||||
shape: PieChartTypes.TREEMAP,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
|
@ -609,9 +616,9 @@ describe('suggestions', () => {
|
|||
groups: [],
|
||||
metric: 'a',
|
||||
|
||||
numberDisplay: 'hidden',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.HIDDEN,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -651,16 +658,16 @@ describe('suggestions', () => {
|
|||
changeType: 'extended',
|
||||
},
|
||||
state: {
|
||||
shape: 'treemap',
|
||||
shape: PieChartTypes.TREEMAP,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
layerType: layerTypes.DATA,
|
||||
groups: ['a', 'b'],
|
||||
metric: 'e',
|
||||
numberDisplay: 'value',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.VALUE,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -700,16 +707,16 @@ describe('suggestions', () => {
|
|||
changeType: 'initial',
|
||||
},
|
||||
state: {
|
||||
shape: 'treemap',
|
||||
shape: PieChartTypes.TREEMAP,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
layerType: layerTypes.DATA,
|
||||
groups: ['a', 'b'],
|
||||
metric: 'e',
|
||||
numberDisplay: 'percent',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.PERCENT,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -737,7 +744,7 @@ describe('suggestions', () => {
|
|||
changeType: 'unchanged',
|
||||
},
|
||||
state: {
|
||||
shape: 'pie',
|
||||
shape: PieChartTypes.PIE,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
|
@ -745,9 +752,9 @@ describe('suggestions', () => {
|
|||
groups: ['a'],
|
||||
metric: 'b',
|
||||
|
||||
numberDisplay: 'hidden',
|
||||
categoryDisplay: 'inside',
|
||||
legendDisplay: 'show',
|
||||
numberDisplay: NumberDisplay.HIDDEN,
|
||||
categoryDisplay: CategoryDisplay.INSIDE,
|
||||
legendDisplay: LegendDisplay.SHOW,
|
||||
percentDecimals: 0,
|
||||
legendMaxLines: 1,
|
||||
truncateLegend: true,
|
||||
|
@ -760,7 +767,7 @@ describe('suggestions', () => {
|
|||
).toContainEqual(
|
||||
expect.objectContaining({
|
||||
state: {
|
||||
shape: 'treemap',
|
||||
shape: PieChartTypes.TREEMAP,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
|
@ -768,8 +775,8 @@ describe('suggestions', () => {
|
|||
groups: ['a'],
|
||||
metric: 'b',
|
||||
|
||||
numberDisplay: 'hidden',
|
||||
categoryDisplay: 'default', // This is changed
|
||||
numberDisplay: NumberDisplay.HIDDEN,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT, // This is changed
|
||||
legendDisplay: 'show',
|
||||
percentDecimals: 0,
|
||||
legendMaxLines: 1,
|
||||
|
@ -794,7 +801,7 @@ describe('suggestions', () => {
|
|||
changeType: 'unchanged',
|
||||
},
|
||||
state: {
|
||||
shape: 'mosaic',
|
||||
shape: PieChartTypes.MOSAIC,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
|
@ -802,9 +809,9 @@ describe('suggestions', () => {
|
|||
groups: [],
|
||||
metric: 'a',
|
||||
|
||||
numberDisplay: 'hidden',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.HIDDEN,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -836,7 +843,7 @@ describe('suggestions', () => {
|
|||
changeType: 'unchanged',
|
||||
},
|
||||
state: {
|
||||
shape: 'treemap',
|
||||
shape: PieChartTypes.TREEMAP,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
|
@ -844,9 +851,9 @@ describe('suggestions', () => {
|
|||
groups: ['a', 'b'],
|
||||
metric: 'c',
|
||||
|
||||
numberDisplay: 'hidden',
|
||||
categoryDisplay: 'inside',
|
||||
legendDisplay: 'show',
|
||||
numberDisplay: NumberDisplay.HIDDEN,
|
||||
categoryDisplay: CategoryDisplay.INSIDE,
|
||||
legendDisplay: LegendDisplay.SHOW,
|
||||
percentDecimals: 0,
|
||||
legendMaxLines: 1,
|
||||
truncateLegend: true,
|
||||
|
@ -871,7 +878,7 @@ describe('suggestions', () => {
|
|||
changeType: 'unchanged',
|
||||
},
|
||||
state: {
|
||||
shape: 'waffle',
|
||||
shape: PieChartTypes.WAFFLE,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
|
@ -879,9 +886,9 @@ describe('suggestions', () => {
|
|||
groups: [],
|
||||
metric: 'a',
|
||||
|
||||
numberDisplay: 'hidden',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.HIDDEN,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -909,16 +916,16 @@ describe('suggestions', () => {
|
|||
changeType: 'unchanged',
|
||||
},
|
||||
state: {
|
||||
shape: 'pie',
|
||||
shape: PieChartTypes.PIE,
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
layerType: layerTypes.DATA,
|
||||
groups: ['a', 'b'],
|
||||
metric: 'c',
|
||||
numberDisplay: 'hidden',
|
||||
categoryDisplay: 'inside',
|
||||
legendDisplay: 'show',
|
||||
numberDisplay: NumberDisplay.HIDDEN,
|
||||
categoryDisplay: CategoryDisplay.INSIDE,
|
||||
legendDisplay: LegendDisplay.SHOW,
|
||||
percentDecimals: 0,
|
||||
legendMaxLines: 1,
|
||||
truncateLegend: true,
|
||||
|
|
|
@ -8,11 +8,17 @@
|
|||
import { partition } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { SuggestionRequest, TableSuggestionColumn, VisualizationSuggestion } from '../types';
|
||||
import { layerTypes } from '../../common';
|
||||
import type { PieVisualizationState } from '../../common/expressions';
|
||||
import {
|
||||
CategoryDisplay,
|
||||
layerTypes,
|
||||
LegendDisplay,
|
||||
NumberDisplay,
|
||||
PieChartTypes,
|
||||
PieVisualizationState,
|
||||
} from '../../common';
|
||||
import type { PieChartType } from '../../common/types';
|
||||
import { PartitionChartsMeta } from './partition_charts_meta';
|
||||
import { isPartitionShape } from './render_helpers';
|
||||
import { PieChartTypes } from '../../common/expressions/pie_chart/types';
|
||||
|
||||
function hasIntervalScale(columns: TableSuggestionColumn[]) {
|
||||
return columns.some((col) => col.operation.scale === 'interval');
|
||||
|
@ -43,14 +49,19 @@ function getNewShape(
|
|||
let newShape: PieVisualizationState['shape'] | undefined;
|
||||
|
||||
if (groups.length !== 1 && !subVisualizationId) {
|
||||
newShape = 'pie';
|
||||
newShape = PieChartTypes.PIE;
|
||||
}
|
||||
|
||||
return newShape ?? 'donut';
|
||||
return newShape ?? PieChartTypes.DONUT;
|
||||
}
|
||||
|
||||
function hasCustomSuggestionsExists(shape: PieChartTypes | string | undefined) {
|
||||
return shape ? ['treemap', 'waffle', 'mosaic'].includes(shape) : false;
|
||||
function hasCustomSuggestionsExists(shape: PieChartType | string | undefined) {
|
||||
const shapes: Array<PieChartType | string> = [
|
||||
PieChartTypes.TREEMAP,
|
||||
PieChartTypes.WAFFLE,
|
||||
PieChartTypes.MOSAIC,
|
||||
];
|
||||
return shape ? shapes.includes(shape) : false;
|
||||
}
|
||||
|
||||
const maximumGroupLength = Math.max(
|
||||
|
@ -116,9 +127,9 @@ export function suggestions({
|
|||
layerId: table.layerId,
|
||||
groups: groups.map((col) => col.columnId),
|
||||
metric: metricColumnId,
|
||||
numberDisplay: 'percent',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.PERCENT,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
nestedLegend: false,
|
||||
layerType: layerTypes.DATA,
|
||||
},
|
||||
|
@ -137,13 +148,18 @@ export function suggestions({
|
|||
...baseSuggestion,
|
||||
title: i18n.translate('xpack.lens.pie.suggestionLabel', {
|
||||
defaultMessage: 'As {chartName}',
|
||||
values: { chartName: PartitionChartsMeta[newShape === 'pie' ? 'donut' : 'pie'].label },
|
||||
values: {
|
||||
chartName:
|
||||
PartitionChartsMeta[
|
||||
newShape === PieChartTypes.PIE ? PieChartTypes.DONUT : PieChartTypes.PIE
|
||||
].label,
|
||||
},
|
||||
description: 'chartName is already translated',
|
||||
}),
|
||||
score: 0.1,
|
||||
state: {
|
||||
...baseSuggestion.state,
|
||||
shape: newShape === 'pie' ? 'donut' : 'pie',
|
||||
shape: newShape === PieChartTypes.PIE ? PieChartTypes.DONUT : PieChartTypes.PIE,
|
||||
},
|
||||
hide: true,
|
||||
});
|
||||
|
@ -159,9 +175,9 @@ export function suggestions({
|
|||
}),
|
||||
// Use a higher score when currently active, to prevent chart type switching
|
||||
// on the user unintentionally
|
||||
score: state?.shape === 'treemap' ? 0.7 : 0.5,
|
||||
score: state?.shape === PieChartTypes.TREEMAP ? 0.7 : 0.5,
|
||||
state: {
|
||||
shape: 'treemap',
|
||||
shape: PieChartTypes.TREEMAP,
|
||||
palette: mainPalette || state?.palette,
|
||||
layers: [
|
||||
state?.layers[0]
|
||||
|
@ -171,8 +187,8 @@ export function suggestions({
|
|||
groups: groups.map((col) => col.columnId),
|
||||
metric: metricColumnId,
|
||||
categoryDisplay:
|
||||
state.layers[0].categoryDisplay === 'inside'
|
||||
? 'default'
|
||||
state.layers[0].categoryDisplay === CategoryDisplay.INSIDE
|
||||
? CategoryDisplay.DEFAULT
|
||||
: state.layers[0].categoryDisplay,
|
||||
layerType: layerTypes.DATA,
|
||||
}
|
||||
|
@ -180,9 +196,9 @@ export function suggestions({
|
|||
layerId: table.layerId,
|
||||
groups: groups.map((col) => col.columnId),
|
||||
metric: metricColumnId,
|
||||
numberDisplay: 'percent',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.PERCENT,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
nestedLegend: false,
|
||||
layerType: layerTypes.DATA,
|
||||
},
|
||||
|
@ -194,21 +210,21 @@ export function suggestions({
|
|||
table.changeType === 'reduced' ||
|
||||
!state ||
|
||||
hasIntervalScale(groups) ||
|
||||
(state && state.shape === 'treemap'),
|
||||
(state && state.shape === PieChartTypes.TREEMAP),
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
groups.length <= PartitionChartsMeta.mosaic.maxBuckets &&
|
||||
(!subVisualizationId || subVisualizationId === 'mosaic')
|
||||
(!subVisualizationId || subVisualizationId === PieChartTypes.MOSAIC)
|
||||
) {
|
||||
results.push({
|
||||
title: i18n.translate('xpack.lens.pie.mosaicSuggestionLabel', {
|
||||
defaultMessage: 'As Mosaic',
|
||||
}),
|
||||
score: state?.shape === 'mosaic' ? 0.7 : 0.5,
|
||||
score: state?.shape === PieChartTypes.MOSAIC ? 0.7 : 0.5,
|
||||
state: {
|
||||
shape: 'mosaic',
|
||||
shape: PieChartTypes.MOSAIC,
|
||||
palette: mainPalette || state?.palette,
|
||||
layers: [
|
||||
state?.layers[0]
|
||||
|
@ -217,16 +233,16 @@ export function suggestions({
|
|||
layerId: table.layerId,
|
||||
groups: groups.map((col) => col.columnId),
|
||||
metric: metricColumnId,
|
||||
categoryDisplay: 'default',
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
layerType: layerTypes.DATA,
|
||||
}
|
||||
: {
|
||||
layerId: table.layerId,
|
||||
groups: groups.map((col) => col.columnId),
|
||||
metric: metricColumnId,
|
||||
numberDisplay: 'percent',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.PERCENT,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
nestedLegend: false,
|
||||
layerType: layerTypes.DATA,
|
||||
},
|
||||
|
@ -239,15 +255,15 @@ export function suggestions({
|
|||
|
||||
if (
|
||||
groups.length <= PartitionChartsMeta.waffle.maxBuckets &&
|
||||
(!subVisualizationId || subVisualizationId === 'waffle')
|
||||
(!subVisualizationId || subVisualizationId === PieChartTypes.WAFFLE)
|
||||
) {
|
||||
results.push({
|
||||
title: i18n.translate('xpack.lens.pie.waffleSuggestionLabel', {
|
||||
defaultMessage: 'As Waffle',
|
||||
}),
|
||||
score: state?.shape === 'waffle' ? 0.7 : 0.5,
|
||||
score: state?.shape === PieChartTypes.WAFFLE ? 0.7 : 0.5,
|
||||
state: {
|
||||
shape: 'waffle',
|
||||
shape: PieChartTypes.WAFFLE,
|
||||
palette: mainPalette || state?.palette,
|
||||
layers: [
|
||||
state?.layers[0]
|
||||
|
@ -256,16 +272,16 @@ export function suggestions({
|
|||
layerId: table.layerId,
|
||||
groups: groups.map((col) => col.columnId),
|
||||
metric: metricColumnId,
|
||||
categoryDisplay: 'default',
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
layerType: layerTypes.DATA,
|
||||
}
|
||||
: {
|
||||
layerId: table.layerId,
|
||||
groups: groups.map((col) => col.columnId),
|
||||
metric: metricColumnId,
|
||||
numberDisplay: 'percent',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.PERCENT,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
nestedLegend: false,
|
||||
layerType: layerTypes.DATA,
|
||||
},
|
||||
|
|
|
@ -6,13 +6,62 @@
|
|||
*/
|
||||
|
||||
import type { Ast } from '@kbn/interpreter';
|
||||
import type { PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { Position } from '@elastic/charts';
|
||||
|
||||
import type { PaletteOutput, PaletteRegistry } from '../../../../../src/plugins/charts/public';
|
||||
import {
|
||||
buildExpression,
|
||||
buildExpressionFunction,
|
||||
} from '../../../../../src/plugins/expressions/public';
|
||||
import type { Operation, DatasourcePublicAPI } from '../types';
|
||||
import { DEFAULT_PERCENT_DECIMALS, EMPTY_SIZE_RATIOS } from './constants';
|
||||
import { DEFAULT_PERCENT_DECIMALS } from './constants';
|
||||
import { shouldShowValuesInLegend } from './render_helpers';
|
||||
import type { PieLayerState, PieVisualizationState } from '../../common/expressions';
|
||||
import {
|
||||
CategoryDisplay,
|
||||
NumberDisplay,
|
||||
PieChartTypes,
|
||||
PieLayerState,
|
||||
PieVisualizationState,
|
||||
EmptySizeRatios,
|
||||
LegendDisplay,
|
||||
} from '../../common';
|
||||
import { getDefaultVisualValuesForLayer } from '../shared_components/datasource_default_values';
|
||||
|
||||
interface Attributes {
|
||||
isPreview: boolean;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface OperationColumnId {
|
||||
columnId: string;
|
||||
operation: Operation;
|
||||
}
|
||||
|
||||
type GenerateExpressionAstFunction = (
|
||||
state: PieVisualizationState,
|
||||
attributes: Attributes,
|
||||
operations: OperationColumnId[],
|
||||
layer: PieLayerState,
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>,
|
||||
paletteService: PaletteRegistry
|
||||
) => Ast | null;
|
||||
|
||||
type GenerateExpressionAstArguments = (
|
||||
state: PieVisualizationState,
|
||||
attributes: Attributes,
|
||||
operations: OperationColumnId[],
|
||||
layer: PieLayerState,
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>,
|
||||
paletteService: PaletteRegistry
|
||||
) => Ast['chain'][number]['arguments'];
|
||||
|
||||
type GenerateLabelsAstArguments = (
|
||||
state: PieVisualizationState,
|
||||
attributes: Attributes,
|
||||
layer: PieLayerState
|
||||
) => [Ast];
|
||||
|
||||
export const getSortedGroups = (datasource: DatasourcePublicAPI, layer: PieLayerState) => {
|
||||
const originalOrder = datasource
|
||||
.getTableSpec()
|
||||
|
@ -22,6 +71,199 @@ export const getSortedGroups = (datasource: DatasourcePublicAPI, layer: PieLayer
|
|||
return Array.from(new Set(originalOrder.concat(layer.groups)));
|
||||
};
|
||||
|
||||
const prepareDimension = (accessor: string) => {
|
||||
const visdimension = buildExpressionFunction('visdimension', { accessor });
|
||||
return buildExpression([visdimension]).toAst();
|
||||
};
|
||||
|
||||
const generateCommonLabelsAstArgs: GenerateLabelsAstArguments = (state, attributes, layer) => {
|
||||
const show = [!attributes.isPreview && layer.categoryDisplay !== CategoryDisplay.HIDE];
|
||||
const position = layer.categoryDisplay !== CategoryDisplay.HIDE ? [layer.categoryDisplay] : [];
|
||||
const values = [layer.numberDisplay !== NumberDisplay.HIDDEN];
|
||||
const valuesFormat = layer.numberDisplay !== NumberDisplay.HIDDEN ? [layer.numberDisplay] : [];
|
||||
const percentDecimals = [layer.percentDecimals ?? DEFAULT_PERCENT_DECIMALS];
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'partitionLabels',
|
||||
arguments: { show, position, values, valuesFormat, percentDecimals },
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const generateWaffleLabelsAstArguments: GenerateLabelsAstArguments = (...args) => {
|
||||
const [labelsExpr] = generateCommonLabelsAstArgs(...args);
|
||||
const [labels] = labelsExpr.chain;
|
||||
return [
|
||||
{
|
||||
...labelsExpr,
|
||||
chain: [{ ...labels, percentDecimals: DEFAULT_PERCENT_DECIMALS }],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const generatePaletteAstArguments = (
|
||||
paletteService: PaletteRegistry,
|
||||
palette?: PaletteOutput
|
||||
): [Ast] =>
|
||||
palette
|
||||
? [
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'theme',
|
||||
arguments: {
|
||||
variable: ['palette'],
|
||||
default: [paletteService.get(palette.name).toExpression(palette.params)],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [paletteService.get('default').toExpression()];
|
||||
|
||||
const generateCommonArguments: GenerateExpressionAstArguments = (
|
||||
state,
|
||||
attributes,
|
||||
operations,
|
||||
layer,
|
||||
datasourceLayers,
|
||||
paletteService
|
||||
) => ({
|
||||
labels: generateCommonLabelsAstArgs(state, attributes, layer),
|
||||
buckets: operations.map((o) => o.columnId).map(prepareDimension),
|
||||
metric: layer.metric ? [prepareDimension(layer.metric)] : [],
|
||||
legendDisplay: [attributes.isPreview ? LegendDisplay.HIDE : layer.legendDisplay],
|
||||
legendPosition: [layer.legendPosition || Position.Right],
|
||||
maxLegendLines: [layer.legendMaxLines ?? 1],
|
||||
nestedLegend: [!!layer.nestedLegend],
|
||||
truncateLegend: [
|
||||
layer.truncateLegend ?? getDefaultVisualValuesForLayer(state, datasourceLayers).truncateText,
|
||||
],
|
||||
palette: generatePaletteAstArguments(paletteService, state.palette),
|
||||
});
|
||||
|
||||
const generatePieVisAst: GenerateExpressionAstFunction = (...rest) => ({
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'pieVis',
|
||||
arguments: {
|
||||
...generateCommonArguments(...rest),
|
||||
respectSourceOrder: [false],
|
||||
startFromSecondLargestSlice: [true],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const generateDonutVisAst: GenerateExpressionAstFunction = (...rest) => {
|
||||
const [, , , layer] = rest;
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'pieVis',
|
||||
arguments: {
|
||||
...generateCommonArguments(...rest),
|
||||
respectSourceOrder: [false],
|
||||
isDonut: [true],
|
||||
startFromSecondLargestSlice: [true],
|
||||
emptySizeRatio: [layer.emptySizeRatio ?? EmptySizeRatios.SMALL],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const generateTreemapVisAst: GenerateExpressionAstFunction = (...rest) => {
|
||||
const [, , , layer] = rest;
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'treemapVis',
|
||||
arguments: {
|
||||
...generateCommonArguments(...rest),
|
||||
nestedLegend: [!!layer.nestedLegend],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const generateMosaicVisAst: GenerateExpressionAstFunction = (...rest) => ({
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'mosaicVis',
|
||||
arguments: generateCommonArguments(...rest),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const generateWaffleVisAst: GenerateExpressionAstFunction = (...rest) => {
|
||||
const { buckets, nestedLegend, ...args } = generateCommonArguments(...rest);
|
||||
const [state, attributes, , layer] = rest;
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'waffleVis',
|
||||
arguments: {
|
||||
...args,
|
||||
bucket: buckets,
|
||||
labels: generateWaffleLabelsAstArguments(state, attributes, layer),
|
||||
showValuesInLegend: [shouldShowValuesInLegend(layer, state.shape)],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const generateExprAst: GenerateExpressionAstFunction = (state, ...restArgs) =>
|
||||
({
|
||||
[PieChartTypes.PIE]: () => generatePieVisAst(state, ...restArgs),
|
||||
[PieChartTypes.DONUT]: () => generateDonutVisAst(state, ...restArgs),
|
||||
[PieChartTypes.TREEMAP]: () => generateTreemapVisAst(state, ...restArgs),
|
||||
[PieChartTypes.MOSAIC]: () => generateMosaicVisAst(state, ...restArgs),
|
||||
[PieChartTypes.WAFFLE]: () => generateWaffleVisAst(state, ...restArgs),
|
||||
}[state.shape]());
|
||||
|
||||
function expressionHelper(
|
||||
state: PieVisualizationState,
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>,
|
||||
paletteService: PaletteRegistry,
|
||||
attributes: Attributes = { isPreview: false }
|
||||
): Ast | null {
|
||||
const layer = state.layers[0];
|
||||
const datasource = datasourceLayers[layer.layerId];
|
||||
const groups = getSortedGroups(datasource, layer);
|
||||
|
||||
const operations = groups
|
||||
.map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) }))
|
||||
.filter((o): o is { columnId: string; operation: Operation } => !!o.operation);
|
||||
|
||||
if (!layer.metric || !operations.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return generateExprAst(state, attributes, operations, layer, datasourceLayers, paletteService);
|
||||
}
|
||||
|
||||
export function toExpression(
|
||||
state: PieVisualizationState,
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>,
|
||||
|
@ -34,82 +276,6 @@ export function toExpression(
|
|||
});
|
||||
}
|
||||
|
||||
function expressionHelper(
|
||||
state: PieVisualizationState,
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>,
|
||||
paletteService: PaletteRegistry,
|
||||
attributes: { isPreview: boolean; title?: string; description?: string } = { isPreview: false }
|
||||
): Ast | null {
|
||||
const layer = state.layers[0];
|
||||
const datasource = datasourceLayers[layer.layerId];
|
||||
const groups = getSortedGroups(datasource, layer);
|
||||
|
||||
const operations = groups
|
||||
.map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) }))
|
||||
.filter((o): o is { columnId: string; operation: Operation } => !!o.operation);
|
||||
|
||||
if (!layer.metric || !operations.length) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'lens_pie',
|
||||
arguments: {
|
||||
title: [attributes.title || ''],
|
||||
description: [attributes.description || ''],
|
||||
shape: [state.shape],
|
||||
hideLabels: [attributes.isPreview],
|
||||
groups: operations.map((o) => o.columnId),
|
||||
metric: [layer.metric],
|
||||
numberDisplay: [layer.numberDisplay],
|
||||
categoryDisplay: [layer.categoryDisplay],
|
||||
legendDisplay: [layer.legendDisplay],
|
||||
legendPosition: [layer.legendPosition || 'right'],
|
||||
emptySizeRatio: [layer.emptySizeRatio ?? EMPTY_SIZE_RATIOS.SMALL],
|
||||
showValuesInLegend: [shouldShowValuesInLegend(layer, state.shape)],
|
||||
percentDecimals: [
|
||||
state.shape === 'waffle'
|
||||
? DEFAULT_PERCENT_DECIMALS
|
||||
: layer.percentDecimals ?? DEFAULT_PERCENT_DECIMALS,
|
||||
],
|
||||
legendMaxLines: [layer.legendMaxLines ?? 1],
|
||||
truncateLegend: [
|
||||
layer.truncateLegend ??
|
||||
getDefaultVisualValuesForLayer(state, datasourceLayers).truncateText,
|
||||
],
|
||||
nestedLegend: [!!layer.nestedLegend],
|
||||
...(state.palette
|
||||
? {
|
||||
palette: [
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'theme',
|
||||
arguments: {
|
||||
variable: ['palette'],
|
||||
default: [
|
||||
paletteService
|
||||
.get(state.palette.name)
|
||||
.toExpression(state.palette.params),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export function toPreviewExpression(
|
||||
state: PieVisualizationState,
|
||||
datasourceLayers: Record<string, DatasourcePublicAPI>,
|
||||
|
|
|
@ -20,7 +20,7 @@ import type { Position } from '@elastic/charts';
|
|||
import type { PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { DEFAULT_PERCENT_DECIMALS } from './constants';
|
||||
import { PartitionChartsMeta } from './partition_charts_meta';
|
||||
import type { PieVisualizationState, SharedPieLayerState } from '../../common/expressions';
|
||||
import { LegendDisplay, PieVisualizationState, SharedPieLayerState } from '../../common';
|
||||
import { VisualizationDimensionEditorProps, VisualizationToolbarProps } from '../types';
|
||||
import { ToolbarPopover, LegendSettingsPopover, useDebouncedValue } from '../shared_components';
|
||||
import { PalettePicker } from '../shared_components';
|
||||
|
@ -34,21 +34,21 @@ const legendOptions: Array<{
|
|||
}> = [
|
||||
{
|
||||
id: 'pieLegendDisplay-default',
|
||||
value: 'default',
|
||||
value: LegendDisplay.DEFAULT,
|
||||
label: i18n.translate('xpack.lens.pieChart.legendVisibility.auto', {
|
||||
defaultMessage: 'Auto',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'pieLegendDisplay-show',
|
||||
value: 'show',
|
||||
value: LegendDisplay.SHOW,
|
||||
label: i18n.translate('xpack.lens.pieChart.legendVisibility.show', {
|
||||
defaultMessage: 'Show',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'pieLegendDisplay-hide',
|
||||
value: 'hide',
|
||||
value: LegendDisplay.HIDE,
|
||||
label: i18n.translate('xpack.lens.pieChart.legendVisibility.hide', {
|
||||
defaultMessage: 'Hide',
|
||||
}),
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
.lnsPieExpression__container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
// the FocusTrap is adding extra divs which are making the visualization redraw twice
|
||||
// with a visible glitch. This make the chart library resilient to this extra reflow
|
||||
overflow-x: hidden;
|
||||
}
|
|
@ -6,7 +6,13 @@
|
|||
*/
|
||||
|
||||
import { getPieVisualization } from './visualization';
|
||||
import type { PieVisualizationState } from '../../common/expressions';
|
||||
import {
|
||||
PieVisualizationState,
|
||||
PieChartTypes,
|
||||
CategoryDisplay,
|
||||
NumberDisplay,
|
||||
LegendDisplay,
|
||||
} from '../../common';
|
||||
import { layerTypes } from '../../common';
|
||||
import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks';
|
||||
import { createMockDatasource, createMockFramePublicAPI } from '../mocks';
|
||||
|
@ -24,16 +30,16 @@ const pieVisualization = getPieVisualization({
|
|||
|
||||
function getExampleState(): PieVisualizationState {
|
||||
return {
|
||||
shape: 'pie',
|
||||
shape: PieChartTypes.PIE,
|
||||
layers: [
|
||||
{
|
||||
layerId: LAYER_ID,
|
||||
layerType: layerTypes.DATA,
|
||||
groups: [],
|
||||
metric: undefined,
|
||||
numberDisplay: 'percent',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.PERCENT,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
nestedLegend: false,
|
||||
},
|
||||
],
|
||||
|
@ -81,14 +87,14 @@ describe('pie_visualization', () => {
|
|||
groups: ['a'],
|
||||
layerId: LAYER_ID,
|
||||
layerType: layerTypes.DATA,
|
||||
numberDisplay: 'percent',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.PERCENT,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
nestedLegend: false,
|
||||
metric: undefined,
|
||||
},
|
||||
],
|
||||
shape: 'donut',
|
||||
shape: PieChartTypes.DONUT,
|
||||
};
|
||||
const setDimensionResult = pieVisualization.setDimension({
|
||||
prevState,
|
||||
|
@ -100,7 +106,7 @@ describe('pie_visualization', () => {
|
|||
|
||||
expect(setDimensionResult).toEqual(
|
||||
expect.objectContaining({
|
||||
shape: 'donut',
|
||||
shape: PieChartTypes.DONUT,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -20,21 +20,21 @@ import type {
|
|||
VisualizationDimensionGroupConfig,
|
||||
} from '../types';
|
||||
import { getSortedGroups, toExpression, toPreviewExpression } from './to_expression';
|
||||
import type { PieLayerState, PieVisualizationState } from '../../common/expressions';
|
||||
import { layerTypes } from '../../common';
|
||||
import { CategoryDisplay, layerTypes, LegendDisplay, NumberDisplay } from '../../common';
|
||||
import { suggestions } from './suggestions';
|
||||
import { PartitionChartsMeta } from './partition_charts_meta';
|
||||
import { DimensionEditor, PieToolbar } from './toolbar';
|
||||
import { checkTableForContainsSmallValues } from './render_helpers';
|
||||
import { PieChartTypes, PieLayerState, PieVisualizationState } from '../../common';
|
||||
|
||||
function newLayerState(layerId: string): PieLayerState {
|
||||
return {
|
||||
layerId,
|
||||
groups: [],
|
||||
metric: undefined,
|
||||
numberDisplay: 'percent',
|
||||
categoryDisplay: 'default',
|
||||
legendDisplay: 'default',
|
||||
numberDisplay: NumberDisplay.PERCENT,
|
||||
categoryDisplay: CategoryDisplay.DEFAULT,
|
||||
legendDisplay: LegendDisplay.DEFAULT,
|
||||
nestedLegend: false,
|
||||
layerType: layerTypes.DATA,
|
||||
};
|
||||
|
@ -108,7 +108,7 @@ export const getPieVisualization = ({
|
|||
initialize(addNewLayer, state, mainPalette) {
|
||||
return (
|
||||
state || {
|
||||
shape: 'donut',
|
||||
shape: PieChartTypes.DONUT,
|
||||
layers: [newLayerState(addNewLayer())],
|
||||
palette: mainPalette,
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import type { CoreSetup } from 'kibana/server';
|
||||
import {
|
||||
pie,
|
||||
xyChart,
|
||||
counterRate,
|
||||
metricChart,
|
||||
|
@ -36,7 +35,6 @@ export const setupExpressions = (
|
|||
[lensMultitable].forEach((expressionType) => expressions.registerType(expressionType));
|
||||
|
||||
[
|
||||
pie,
|
||||
xyChart,
|
||||
counterRate,
|
||||
metricChart,
|
||||
|
|
|
@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { FormattedMessage, FormattedDate, FormattedTime, FormattedRelative } from '@kbn/i18n-react';
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {
|
||||
import type {
|
||||
TypedLensByValueInput,
|
||||
PersistedIndexPatternLayer,
|
||||
PieVisualizationState,
|
||||
|
|
|
@ -631,17 +631,14 @@
|
|||
"xpack.lens.pie.addLayer": "ビジュアライゼーションレイヤーを追加",
|
||||
"xpack.lens.pie.arrayValues": "{label}には配列値が含まれます。可視化が想定通りに表示されない場合があります。",
|
||||
"xpack.lens.pie.donutLabel": "ドーナッツ",
|
||||
"xpack.lens.pie.expressionHelpLabel": "円表示",
|
||||
"xpack.lens.pie.groupLabel": "比率",
|
||||
"xpack.lens.pie.groupsizeLabel": "サイズ単位",
|
||||
"xpack.lens.pie.pielabel": "円",
|
||||
"xpack.lens.pie.pieWithNegativeWarningLabel": "{chartType}グラフは負の値では表示できません。",
|
||||
"xpack.lens.pie.sliceGroupLabel": "スライス",
|
||||
"xpack.lens.pie.suggestionLabel": "{chartName}として",
|
||||
"xpack.lens.pie.treemapGroupLabel": "グループ分けの条件",
|
||||
"xpack.lens.pie.treemaplabel": "ツリーマップ",
|
||||
"xpack.lens.pie.treemapSuggestionLabel": "ツリーマップとして",
|
||||
"xpack.lens.pie.visualizationName": "円",
|
||||
"xpack.lens.pieChart.categoriesInLegendLabel": "ラベルを非表示",
|
||||
"xpack.lens.pieChart.fitInsideOnlyLabel": "内部のみ",
|
||||
"xpack.lens.pieChart.hiddenNumbersLabel": "グラフから非表示",
|
||||
|
@ -2983,8 +2980,6 @@
|
|||
"expressionPartitionVis.legend.filterForValueButtonAriaLabel": "値でフィルター",
|
||||
"expressionPartitionVis.legend.filterOptionsLegend": "{legendDataLabel}、フィルターオプション",
|
||||
"expressionPartitionVis.legend.filterOutValueButtonAriaLabel": "値を除外",
|
||||
"expressionPartitionVis.negativeValuesFound": "円/ドーナツグラフは負の値では表示できません。",
|
||||
"expressionPartitionVis.noResultsFoundTitle": "結果が見つかりませんでした",
|
||||
"fieldFormats.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数字フォーマット",
|
||||
"fieldFormats.advancedSettings.format.bytesFormatText": "「バイト」フォーマットのデフォルト{numeralFormatLink}です",
|
||||
"fieldFormats.advancedSettings.format.bytesFormatTitle": "バイトフォーマット",
|
||||
|
|
|
@ -643,17 +643,14 @@
|
|||
"xpack.lens.pie.addLayer": "添加可视化图层",
|
||||
"xpack.lens.pie.arrayValues": "{label} 包含数组值。您的可视化可能无法正常渲染。",
|
||||
"xpack.lens.pie.donutLabel": "圆环图",
|
||||
"xpack.lens.pie.expressionHelpLabel": "饼图呈现器",
|
||||
"xpack.lens.pie.groupLabel": "比例",
|
||||
"xpack.lens.pie.groupsizeLabel": "大小调整依据",
|
||||
"xpack.lens.pie.pielabel": "饼图",
|
||||
"xpack.lens.pie.pieWithNegativeWarningLabel": "{chartType} 图表无法使用负值进行呈现。",
|
||||
"xpack.lens.pie.sliceGroupLabel": "切片依据",
|
||||
"xpack.lens.pie.suggestionLabel": "为 {chartName}",
|
||||
"xpack.lens.pie.treemapGroupLabel": "分组依据",
|
||||
"xpack.lens.pie.treemaplabel": "树状图",
|
||||
"xpack.lens.pie.treemapSuggestionLabel": "为树状图",
|
||||
"xpack.lens.pie.visualizationName": "饼图",
|
||||
"xpack.lens.pieChart.categoriesInLegendLabel": "隐藏标签",
|
||||
"xpack.lens.pieChart.fitInsideOnlyLabel": "仅内部",
|
||||
"xpack.lens.pieChart.hiddenNumbersLabel": "在图表中隐藏",
|
||||
|
@ -2767,8 +2764,6 @@
|
|||
"expressionPartitionVis.legend.filterForValueButtonAriaLabel": "筛留值",
|
||||
"expressionPartitionVis.legend.filterOptionsLegend": "{legendDataLabel}, 筛选选项",
|
||||
"expressionPartitionVis.legend.filterOutValueButtonAriaLabel": "筛除值",
|
||||
"expressionPartitionVis.negativeValuesFound": "饼图/圆环图无法使用负值进行呈现。",
|
||||
"expressionPartitionVis.noResultsFoundTitle": "找不到结果",
|
||||
"fieldFormats.advancedSettings.format.bytesFormat.numeralFormatLinkText": "数值格式",
|
||||
"fieldFormats.advancedSettings.format.bytesFormatText": "“字节”格式的默认{numeralFormatLink}",
|
||||
"fieldFormats.advancedSettings.format.bytesFormatTitle": "字节格式",
|
||||
|
|
|
@ -11,7 +11,6 @@ import { FtrProviderContext } from '../../ftr_provider_context';
|
|||
export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['common', 'dashboard', 'visualize', 'lens', 'timePicker']);
|
||||
|
||||
const find = getService('find');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const dashboardPanelActions = getService('dashboardPanelActions');
|
||||
|
@ -49,8 +48,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await PageObjects.lens.saveAndReturn();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
|
||||
const pieExists = await find.existsByCssSelector('.lnsPieExpression__container');
|
||||
expect(pieExists).to.be(true);
|
||||
const partitionVisExists = await testSubjects.exists('partitionVisChart');
|
||||
expect(partitionVisExists).to.be(true);
|
||||
});
|
||||
|
||||
it('editing and saving a lens by value panel retains number of panels', async () => {
|
||||
|
|
|
@ -103,8 +103,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
|||
await PageObjects.lens.saveAndReturn();
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
|
||||
const pieExists = await find.existsByCssSelector('.lnsPieExpression__container');
|
||||
expect(pieExists).to.be(true);
|
||||
const partitionVisExists = await testSubjects.exists('partitionVisChart');
|
||||
expect(partitionVisExists).to.be(true);
|
||||
});
|
||||
|
||||
it('disables save to library button without visualize save permissions', async () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue