[Lens] New value labels config option for bar charts (#81776)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
03ee1a6476
commit
d3d3fa7bd2
|
@ -11,7 +11,7 @@ import { EuiIconLegend } from '../assets/legend';
|
|||
|
||||
const typeToIconMap: { [type: string]: string | IconType } = {
|
||||
legend: EuiIconLegend as IconType,
|
||||
values: 'visText',
|
||||
values: 'number',
|
||||
};
|
||||
|
||||
export interface ToolbarPopoverProps {
|
||||
|
|
|
@ -279,6 +279,15 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
displayValueSettings={
|
||||
Object {
|
||||
"hideClippedValue": true,
|
||||
"isAlternatingValueLabel": false,
|
||||
"isValueContainedInElement": true,
|
||||
"showValueLabel": false,
|
||||
"valueFormatter": [Function],
|
||||
}
|
||||
}
|
||||
enableHistogramMode={false}
|
||||
groupId="left"
|
||||
id="d-a"
|
||||
|
@ -334,6 +343,15 @@ exports[`xy_expression XYChart component it renders bar 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
displayValueSettings={
|
||||
Object {
|
||||
"hideClippedValue": true,
|
||||
"isAlternatingValueLabel": false,
|
||||
"isValueContainedInElement": true,
|
||||
"showValueLabel": false,
|
||||
"valueFormatter": [Function],
|
||||
}
|
||||
}
|
||||
enableHistogramMode={false}
|
||||
groupId="left"
|
||||
id="d-b"
|
||||
|
@ -457,6 +475,15 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
displayValueSettings={
|
||||
Object {
|
||||
"hideClippedValue": true,
|
||||
"isAlternatingValueLabel": false,
|
||||
"isValueContainedInElement": true,
|
||||
"showValueLabel": false,
|
||||
"valueFormatter": [Function],
|
||||
}
|
||||
}
|
||||
enableHistogramMode={false}
|
||||
groupId="left"
|
||||
id="d-a"
|
||||
|
@ -512,6 +539,15 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
displayValueSettings={
|
||||
Object {
|
||||
"hideClippedValue": true,
|
||||
"isAlternatingValueLabel": false,
|
||||
"isValueContainedInElement": true,
|
||||
"showValueLabel": false,
|
||||
"valueFormatter": [Function],
|
||||
}
|
||||
}
|
||||
enableHistogramMode={false}
|
||||
groupId="left"
|
||||
id="d-b"
|
||||
|
@ -1019,6 +1055,15 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
displayValueSettings={
|
||||
Object {
|
||||
"hideClippedValue": true,
|
||||
"isAlternatingValueLabel": false,
|
||||
"isValueContainedInElement": true,
|
||||
"showValueLabel": false,
|
||||
"valueFormatter": [Function],
|
||||
}
|
||||
}
|
||||
enableHistogramMode={false}
|
||||
groupId="left"
|
||||
id="d-a"
|
||||
|
@ -1078,6 +1123,15 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = `
|
|||
},
|
||||
]
|
||||
}
|
||||
displayValueSettings={
|
||||
Object {
|
||||
"hideClippedValue": true,
|
||||
"isAlternatingValueLabel": false,
|
||||
"isValueContainedInElement": true,
|
||||
"showValueLabel": false,
|
||||
"valueFormatter": [Function],
|
||||
}
|
||||
}
|
||||
enableHistogramMode={false}
|
||||
groupId="left"
|
||||
id="d-b"
|
||||
|
@ -1205,6 +1259,15 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
|
|||
},
|
||||
]
|
||||
}
|
||||
displayValueSettings={
|
||||
Object {
|
||||
"hideClippedValue": true,
|
||||
"isAlternatingValueLabel": false,
|
||||
"isValueContainedInElement": true,
|
||||
"showValueLabel": false,
|
||||
"valueFormatter": [Function],
|
||||
}
|
||||
}
|
||||
enableHistogramMode={false}
|
||||
groupId="left"
|
||||
id="d-a"
|
||||
|
@ -1264,6 +1327,15 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] =
|
|||
},
|
||||
]
|
||||
}
|
||||
displayValueSettings={
|
||||
Object {
|
||||
"hideClippedValue": true,
|
||||
"isAlternatingValueLabel": false,
|
||||
"isValueContainedInElement": true,
|
||||
"showValueLabel": false,
|
||||
"valueFormatter": [Function],
|
||||
}
|
||||
}
|
||||
enableHistogramMode={false}
|
||||
groupId="left"
|
||||
id="d-b"
|
||||
|
|
|
@ -145,6 +145,9 @@ Object {
|
|||
"title": Array [
|
||||
"",
|
||||
],
|
||||
"valueLabels": Array [
|
||||
"hide",
|
||||
],
|
||||
"xTitle": Array [
|
||||
"",
|
||||
],
|
||||
|
|
|
@ -256,6 +256,7 @@ const createArgsWithLayers = (layers: LayerArgs[] = [sampleLayer]): XYArgs => ({
|
|||
isVisible: false,
|
||||
position: Position.Top,
|
||||
},
|
||||
valueLabels: 'hide',
|
||||
axisTitlesVisibilitySettings: {
|
||||
type: 'lens_xy_axisTitlesVisibilityConfig',
|
||||
x: true,
|
||||
|
@ -1867,6 +1868,7 @@ describe('xy_expression', () => {
|
|||
yTitle: '',
|
||||
yRightTitle: '',
|
||||
legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top },
|
||||
valueLabels: 'hide',
|
||||
tickLabelsVisibilitySettings: {
|
||||
type: 'lens_xy_tickLabelsConfig',
|
||||
x: true,
|
||||
|
@ -1952,6 +1954,7 @@ describe('xy_expression', () => {
|
|||
yTitle: '',
|
||||
yRightTitle: '',
|
||||
legend: { type: 'lens_xy_legendConfig', isVisible: false, position: Position.Top },
|
||||
valueLabels: 'hide',
|
||||
tickLabelsVisibilitySettings: {
|
||||
type: 'lens_xy_tickLabelsConfig',
|
||||
x: true,
|
||||
|
@ -2023,6 +2026,7 @@ describe('xy_expression', () => {
|
|||
yTitle: '',
|
||||
yRightTitle: '',
|
||||
legend: { type: 'lens_xy_legendConfig', isVisible: true, position: Position.Top },
|
||||
valueLabels: 'hide',
|
||||
tickLabelsVisibilitySettings: {
|
||||
type: 'lens_xy_tickLabelsConfig',
|
||||
x: true,
|
||||
|
|
|
@ -20,6 +20,10 @@ import {
|
|||
GeometryValue,
|
||||
XYChartSeriesIdentifier,
|
||||
StackMode,
|
||||
RecursivePartial,
|
||||
Theme,
|
||||
VerticalAlignment,
|
||||
HorizontalAlignment,
|
||||
} from '@elastic/charts';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import {
|
||||
|
@ -131,6 +135,11 @@ export const xyChart: ExpressionFunctionDefinition<
|
|||
defaultMessage: 'Define how missing values are treated',
|
||||
}),
|
||||
},
|
||||
valueLabels: {
|
||||
types: ['string'],
|
||||
options: ['hide', 'inside'],
|
||||
help: '',
|
||||
},
|
||||
tickLabelsVisibilitySettings: {
|
||||
types: ['lens_xy_tickLabelsConfig'],
|
||||
help: i18n.translate('xpack.lens.xyChart.tickLabelsSettings.help', {
|
||||
|
@ -214,6 +223,40 @@ export const getXyChartRenderer = (dependencies: {
|
|||
},
|
||||
});
|
||||
|
||||
function mergeThemeWithValueLabelsStyling(
|
||||
theme: RecursivePartial<Theme>,
|
||||
valuesLabelMode: string = 'hide',
|
||||
isHorizontal: boolean
|
||||
) {
|
||||
const VALUE_LABELS_MAX_FONTSIZE = 15;
|
||||
const VALUE_LABELS_MIN_FONTSIZE = 10;
|
||||
const VALUE_LABELS_VERTICAL_OFFSET = -10;
|
||||
const VALUE_LABELS_HORIZONTAL_OFFSET = 10;
|
||||
|
||||
if (valuesLabelMode === 'hide') {
|
||||
return theme;
|
||||
}
|
||||
return {
|
||||
...theme,
|
||||
...{
|
||||
barSeriesStyle: {
|
||||
...theme.barSeriesStyle,
|
||||
displayValue: {
|
||||
fontSize: { min: VALUE_LABELS_MIN_FONTSIZE, max: VALUE_LABELS_MAX_FONTSIZE },
|
||||
fill: { textInverted: true, textBorder: 2 },
|
||||
alignment: isHorizontal
|
||||
? {
|
||||
vertical: VerticalAlignment.Middle,
|
||||
}
|
||||
: { horizontal: HorizontalAlignment.Center },
|
||||
offsetX: isHorizontal ? VALUE_LABELS_HORIZONTAL_OFFSET : 0,
|
||||
offsetY: isHorizontal ? 0 : VALUE_LABELS_VERTICAL_OFFSET,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getIconForSeriesType(seriesType: SeriesType): IconType {
|
||||
return visualizationTypes.find((c) => c.id === seriesType)!.icon || 'empty';
|
||||
}
|
||||
|
@ -254,7 +297,7 @@ export function XYChart({
|
|||
onClickValue,
|
||||
onSelectRange,
|
||||
}: XYChartRenderProps) {
|
||||
const { legend, layers, fittingFunction, gridlinesVisibilitySettings } = args;
|
||||
const { legend, layers, fittingFunction, gridlinesVisibilitySettings, valueLabels } = args;
|
||||
const chartTheme = chartsThemeService.useChartsTheme();
|
||||
const chartBaseTheme = chartsThemeService.useChartsBaseTheme();
|
||||
|
||||
|
@ -396,6 +439,16 @@ export function XYChart({
|
|||
return style;
|
||||
};
|
||||
|
||||
const shouldShowValueLabels =
|
||||
// No stacked bar charts
|
||||
filteredLayers.every((layer) => !layer.seriesType.includes('stacked')) &&
|
||||
// No histogram charts
|
||||
!isHistogramViz;
|
||||
|
||||
const baseThemeWithMaybeValueLabels = !shouldShowValueLabels
|
||||
? chartTheme
|
||||
: mergeThemeWithValueLabelsStyling(chartTheme, valueLabels, shouldRotate);
|
||||
|
||||
const colorAssignments = getColorAssignments(args.layers, data, formatFactory);
|
||||
|
||||
return (
|
||||
|
@ -408,7 +461,7 @@ export function XYChart({
|
|||
}
|
||||
legendPosition={legend.position}
|
||||
showLegendExtra={false}
|
||||
theme={chartTheme}
|
||||
theme={baseThemeWithMaybeValueLabels}
|
||||
baseTheme={chartBaseTheme}
|
||||
tooltip={{
|
||||
headerFormatter: (d) => safeXAccessorLabelRenderer(d.value),
|
||||
|
@ -613,6 +666,10 @@ export function XYChart({
|
|||
});
|
||||
}
|
||||
|
||||
const yAxis = yAxesConfiguration.find((axisConfiguration) =>
|
||||
axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor)
|
||||
);
|
||||
|
||||
const seriesProps: SeriesSpec = {
|
||||
splitSeriesAccessors: splitAccessor ? [splitAccessor] : [],
|
||||
stackAccessors: seriesType.includes('stacked') ? [xAccessor as string] : [],
|
||||
|
@ -649,9 +706,7 @@ export function XYChart({
|
|||
palette.params
|
||||
);
|
||||
},
|
||||
groupId: yAxesConfiguration.find((axisConfiguration) =>
|
||||
axisConfiguration.series.find((currentSeries) => currentSeries.accessor === accessor)
|
||||
)?.groupId,
|
||||
groupId: yAxis?.groupId,
|
||||
enableHistogramMode:
|
||||
isHistogram &&
|
||||
(seriesType.includes('stacked') || !splitAccessor) &&
|
||||
|
@ -723,7 +778,19 @@ export function XYChart({
|
|||
case 'bar_horizontal':
|
||||
case 'bar_horizontal_stacked':
|
||||
case 'bar_horizontal_percentage_stacked':
|
||||
return <BarSeries key={index} {...seriesProps} />;
|
||||
const valueLabelsSettings = {
|
||||
displayValueSettings: {
|
||||
// This format double fixes two issues in elastic-chart
|
||||
// * when rotating the chart, the formatter is not correctly picked
|
||||
// * in some scenarios value labels are not strings, and this breaks the elastic-chart lib
|
||||
valueFormatter: (d: unknown) => yAxis?.formatter?.convert(d) || '',
|
||||
showValueLabel: shouldShowValueLabels && valueLabels !== 'hide',
|
||||
isAlternatingValueLabel: false,
|
||||
isValueContainedInElement: true,
|
||||
hideClippedValue: true,
|
||||
},
|
||||
};
|
||||
return <BarSeries key={index} {...seriesProps} {...valueLabelsSettings} />;
|
||||
case 'area_stacked':
|
||||
case 'area_percentage_stacked':
|
||||
return (
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
*/
|
||||
|
||||
import { EuiIconType } from '@elastic/eui/src/components/icon/icon';
|
||||
import { SeriesType, visualizationTypes, LayerConfig, YConfig } from './types';
|
||||
import { FramePublicAPI } from '../types';
|
||||
import { SeriesType, visualizationTypes, LayerConfig, YConfig, ValidLayer } from './types';
|
||||
|
||||
export function isHorizontalSeries(seriesType: SeriesType) {
|
||||
return (
|
||||
|
@ -37,3 +38,23 @@ export const getSeriesColor = (layer: LayerConfig, accessor: string) => {
|
|||
layer?.yConfig?.find((yConfig: YConfig) => yConfig.forAccessor === accessor)?.color || null
|
||||
);
|
||||
};
|
||||
|
||||
export function hasHistogramSeries(
|
||||
layers: ValidLayer[] = [],
|
||||
datasourceLayers?: FramePublicAPI['datasourceLayers']
|
||||
) {
|
||||
if (!datasourceLayers) {
|
||||
return false;
|
||||
}
|
||||
const validLayers = layers.filter(({ accessors }) => accessors.length);
|
||||
|
||||
return validLayers.some(({ layerId, xAccessor }: ValidLayer) => {
|
||||
const xAxisOperation = datasourceLayers[layerId].getOperationForColumnId(xAccessor);
|
||||
return (
|
||||
xAxisOperation &&
|
||||
xAxisOperation.isBucketed &&
|
||||
xAxisOperation.scale &&
|
||||
xAxisOperation.scale !== 'ordinal'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ describe('#toExpression', () => {
|
|||
xyVisualization.toExpression(
|
||||
{
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
fittingFunction: 'Carry',
|
||||
tickLabelsVisibilitySettings: { x: false, yLeft: true, yRight: true },
|
||||
|
@ -67,6 +68,7 @@ describe('#toExpression', () => {
|
|||
(xyVisualization.toExpression(
|
||||
{
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -87,6 +89,7 @@ describe('#toExpression', () => {
|
|||
const expression = xyVisualization.toExpression(
|
||||
{
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -113,6 +116,7 @@ describe('#toExpression', () => {
|
|||
const expression = xyVisualization.toExpression(
|
||||
{
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -136,6 +140,7 @@ describe('#toExpression', () => {
|
|||
xyVisualization.toExpression(
|
||||
{
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -156,6 +161,7 @@ describe('#toExpression', () => {
|
|||
const expression = xyVisualization.toExpression(
|
||||
{
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -191,6 +197,7 @@ describe('#toExpression', () => {
|
|||
const expression = xyVisualization.toExpression(
|
||||
{
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -217,6 +224,7 @@ describe('#toExpression', () => {
|
|||
const expression = xyVisualization.toExpression(
|
||||
{
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -238,4 +246,25 @@ describe('#toExpression', () => {
|
|||
yRight: [true],
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly report the valueLabels visibility settings', () => {
|
||||
const expression = xyVisualization.toExpression(
|
||||
{
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'inside',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
layerId: 'first',
|
||||
seriesType: 'area',
|
||||
splitAccessor: 'd',
|
||||
xAccessor: 'a',
|
||||
accessors: ['b', 'c'],
|
||||
},
|
||||
],
|
||||
},
|
||||
frame.datasourceLayers
|
||||
) as Ast;
|
||||
expect(expression.chain[0].arguments.valueLabels[0] as Ast).toEqual('inside');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,13 +7,9 @@
|
|||
import { Ast } from '@kbn/interpreter/common';
|
||||
import { ScaleType } from '@elastic/charts';
|
||||
import { PaletteRegistry } from 'src/plugins/charts/public';
|
||||
import { State, LayerConfig } from './types';
|
||||
import { State, ValidLayer, LayerConfig } from './types';
|
||||
import { OperationMetadata, DatasourcePublicAPI } from '../types';
|
||||
|
||||
interface ValidLayer extends LayerConfig {
|
||||
xAccessor: NonNullable<LayerConfig['xAccessor']>;
|
||||
}
|
||||
|
||||
export const getSortedAccessors = (datasource: DatasourcePublicAPI, layer: LayerConfig) => {
|
||||
const originalOrder = datasource
|
||||
.getTableSpec()
|
||||
|
@ -60,6 +56,7 @@ export function toPreviewExpression(
|
|||
...state.legend,
|
||||
isVisible: false,
|
||||
},
|
||||
valueLabels: 'hide',
|
||||
},
|
||||
datasourceLayers,
|
||||
paletteService,
|
||||
|
@ -197,6 +194,7 @@ export const buildExpression = (
|
|||
],
|
||||
},
|
||||
],
|
||||
valueLabels: [state?.valueLabels || 'hide'],
|
||||
layers: validLayers.map((layer) => {
|
||||
const columnToLabel: Record<string, string> = {};
|
||||
|
||||
|
|
|
@ -364,6 +364,8 @@ export type SeriesType =
|
|||
|
||||
export type YAxisMode = 'auto' | 'left' | 'right';
|
||||
|
||||
export type ValueLabelConfig = 'hide' | 'inside' | 'outside';
|
||||
|
||||
export interface YConfig {
|
||||
forAccessor: string;
|
||||
axisMode?: YAxisMode;
|
||||
|
@ -381,6 +383,10 @@ export interface LayerConfig {
|
|||
palette?: PaletteOutput;
|
||||
}
|
||||
|
||||
export interface ValidLayer extends LayerConfig {
|
||||
xAccessor: NonNullable<LayerConfig['xAccessor']>;
|
||||
}
|
||||
|
||||
export type LayerArgs = LayerConfig & {
|
||||
columnToLabel?: string; // Actually a JSON key-value pair
|
||||
yScaleType: 'time' | 'linear' | 'log' | 'sqrt';
|
||||
|
@ -398,6 +404,7 @@ export interface XYArgs {
|
|||
yTitle: string;
|
||||
yRightTitle: string;
|
||||
legend: LegendConfig & { type: 'lens_xy_legendConfig' };
|
||||
valueLabels: ValueLabelConfig;
|
||||
layers: LayerArgs[];
|
||||
fittingFunction?: FittingFunction;
|
||||
axisTitlesVisibilitySettings?: AxesSettingsConfig & {
|
||||
|
@ -411,6 +418,7 @@ export interface XYArgs {
|
|||
export interface XYState {
|
||||
preferredSeriesType: SeriesType;
|
||||
legend: LegendConfig;
|
||||
valueLabels?: ValueLabelConfig;
|
||||
fittingFunction?: FittingFunction;
|
||||
layers: LayerConfig[];
|
||||
xTitle?: string;
|
||||
|
|
|
@ -15,6 +15,7 @@ import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'
|
|||
function exampleState(): State {
|
||||
return {
|
||||
legend: { position: Position.Bottom, isVisible: true },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -150,6 +151,7 @@ describe('xy_visualization', () => {
|
|||
},
|
||||
"preferredSeriesType": "bar_stacked",
|
||||
"title": "Empty XY chart",
|
||||
"valueLabels": "hide",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -145,6 +145,7 @@ export const getXyVisualization = ({
|
|||
state || {
|
||||
title: 'Empty XY chart',
|
||||
legend: { isVisible: true, position: Position.Right },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: defaultSeriesType,
|
||||
layers: [
|
||||
{
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
.lnsXyToolbar__popover {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ describe('XY Config panels', () => {
|
|||
function testState(): State {
|
||||
return {
|
||||
legend: { isVisible: true, position: Position.Right },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -115,8 +116,9 @@ describe('XY Config panels', () => {
|
|||
expect(component.find(EuiSuperSelect).prop('valueOfSelected')).toEqual('Carry');
|
||||
});
|
||||
|
||||
it('should disable the popover if there is no area or line series', () => {
|
||||
it('should show currently selected value labels display setting', () => {
|
||||
const state = testState();
|
||||
|
||||
const component = shallow(
|
||||
<XyToolbar
|
||||
frame={frame}
|
||||
|
@ -125,11 +127,148 @@ describe('XY Config panels', () => {
|
|||
...state,
|
||||
layers: [{ ...state.layers[0], seriesType: 'bar' }],
|
||||
fittingFunction: 'Carry',
|
||||
valueLabels: 'inside',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(ToolbarPopover).at(0).prop('isDisabled')).toEqual(true);
|
||||
expect(component.find(EuiButtonGroup).prop('idSelected')).toEqual('value_labels_inside');
|
||||
});
|
||||
|
||||
it('should disable the popover for stacked bar charts', () => {
|
||||
const state = testState();
|
||||
const component = shallow(
|
||||
<XyToolbar
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
state={{
|
||||
...state,
|
||||
layers: [{ ...state.layers[0], seriesType: 'bar_stacked' }],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should disable the popover for percentage area charts', () => {
|
||||
const state = testState();
|
||||
const component = shallow(
|
||||
<XyToolbar
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
state={{
|
||||
...state,
|
||||
layers: [{ ...state.layers[0], seriesType: 'area_percentage_stacked' }],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should disabled the popover if there is histogram series', () => {
|
||||
// make it detect an histogram series
|
||||
frame.datasourceLayers.first.getOperationForColumnId = jest.fn().mockReturnValueOnce({
|
||||
isBucketed: true,
|
||||
scale: 'interval',
|
||||
});
|
||||
const state = testState();
|
||||
const component = shallow(
|
||||
<XyToolbar
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
state={{
|
||||
...state,
|
||||
layers: [{ ...state.layers[0] }],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.find(ToolbarPopover).prop('isDisabled')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should show the popover and display field enabled for bar and horizontal_bar series', () => {
|
||||
const state = testState();
|
||||
|
||||
const component = shallow(
|
||||
<XyToolbar
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
state={{
|
||||
...state,
|
||||
layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' }],
|
||||
fittingFunction: 'Carry',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should hide the fitting option for bar series', () => {
|
||||
const state = testState();
|
||||
const component = shallow(
|
||||
<XyToolbar
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
state={{
|
||||
...state,
|
||||
layers: [{ ...state.layers[0], seriesType: 'bar_horizontal' }],
|
||||
fittingFunction: 'Carry',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.exists('[data-test-subj="lnsMissingValuesSelect"]')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should hide in the popover the display option for area and line series', () => {
|
||||
const state = testState();
|
||||
const component = shallow(
|
||||
<XyToolbar
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
state={{
|
||||
...state,
|
||||
layers: [{ ...state.layers[0], seriesType: 'area' }],
|
||||
fittingFunction: 'Carry',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(false);
|
||||
});
|
||||
|
||||
it('should keep the display option for bar series with multiple layers', () => {
|
||||
frame.datasourceLayers = {
|
||||
...frame.datasourceLayers,
|
||||
second: createMockDatasource('test').publicAPIMock,
|
||||
};
|
||||
|
||||
const state = testState();
|
||||
const component = shallow(
|
||||
<XyToolbar
|
||||
frame={frame}
|
||||
setState={jest.fn()}
|
||||
state={{
|
||||
...state,
|
||||
layers: [
|
||||
{ ...state.layers[0], seriesType: 'bar' },
|
||||
{
|
||||
seriesType: 'bar',
|
||||
layerId: 'second',
|
||||
splitAccessor: 'baz',
|
||||
xAccessor: 'foo',
|
||||
accessors: ['bar'],
|
||||
},
|
||||
],
|
||||
fittingFunction: 'Carry',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.exists('[data-test-subj="lnsValueLabelsDisplay"]')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should disable the popover if there is no right axis', () => {
|
||||
|
|
|
@ -27,8 +27,20 @@ import {
|
|||
VisualizationToolbarProps,
|
||||
VisualizationDimensionEditorProps,
|
||||
} from '../types';
|
||||
import { State, SeriesType, visualizationTypes, YAxisMode, AxesSettingsConfig } from './types';
|
||||
import { isHorizontalChart, isHorizontalSeries, getSeriesColor } from './state_helpers';
|
||||
import {
|
||||
State,
|
||||
SeriesType,
|
||||
visualizationTypes,
|
||||
YAxisMode,
|
||||
AxesSettingsConfig,
|
||||
ValidLayer,
|
||||
} from './types';
|
||||
import {
|
||||
isHorizontalChart,
|
||||
isHorizontalSeries,
|
||||
getSeriesColor,
|
||||
hasHistogramSeries,
|
||||
} from './state_helpers';
|
||||
import { trackUiEvent } from '../lens_ui_telemetry';
|
||||
import { fittingFunctionDefinitions } from './fitting_functions';
|
||||
import { ToolbarPopover, LegendSettingsPopover } from '../shared_components';
|
||||
|
@ -74,6 +86,27 @@ const legendOptions: Array<{ id: string; value: 'auto' | 'show' | 'hide'; label:
|
|||
},
|
||||
];
|
||||
|
||||
const valueLabelsOptions: Array<{
|
||||
id: string;
|
||||
value: 'hide' | 'inside' | 'outside';
|
||||
label: string;
|
||||
}> = [
|
||||
{
|
||||
id: `value_labels_hide`,
|
||||
value: 'hide',
|
||||
label: i18n.translate('xpack.lens.xyChart.valueLabelsVisibility.auto', {
|
||||
defaultMessage: 'Hide',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: `value_labels_inside`,
|
||||
value: 'inside',
|
||||
label: i18n.translate('xpack.lens.xyChart.valueLabelsVisibility.inside', {
|
||||
defaultMessage: 'Show',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export function LayerContextMenu(props: VisualizationLayerWidgetProps<State>) {
|
||||
const { state, layerId } = props;
|
||||
const horizontalOnly = isHorizontalChart(state.layers);
|
||||
|
@ -118,12 +151,24 @@ export function LayerContextMenu(props: VisualizationLayerWidgetProps<State>) {
|
|||
}
|
||||
|
||||
export function XyToolbar(props: VisualizationToolbarProps<State>) {
|
||||
const { state, setState } = props;
|
||||
const { state, setState, frame } = props;
|
||||
|
||||
const hasNonBarSeries = state?.layers.some(({ seriesType }) =>
|
||||
['area_stacked', 'area', 'line'].includes(seriesType)
|
||||
);
|
||||
|
||||
const hasBarNotStacked = state?.layers.some(({ seriesType }) =>
|
||||
['bar', 'bar_horizontal'].includes(seriesType)
|
||||
);
|
||||
|
||||
const isAreaPercentage = state?.layers.some(
|
||||
({ seriesType }) => seriesType === 'area_percentage_stacked'
|
||||
);
|
||||
|
||||
const isHistogramSeries = Boolean(
|
||||
hasHistogramSeries(state?.layers as ValidLayer[], frame.datasourceLayers)
|
||||
);
|
||||
|
||||
const shouldRotate = state?.layers.length ? isHorizontalChart(state.layers) : false;
|
||||
const axisGroups = getAxesConfiguration(state?.layers, shouldRotate);
|
||||
|
||||
|
@ -191,54 +236,99 @@ export function XyToolbar(props: VisualizationToolbarProps<State>) {
|
|||
: !state?.legend.isVisible
|
||||
? 'hide'
|
||||
: 'show';
|
||||
|
||||
const valueLabelsVisibilityMode = state?.valueLabels || 'hide';
|
||||
|
||||
const isValueLabelsEnabled = !hasNonBarSeries && hasBarNotStacked && !isHistogramSeries;
|
||||
const isFittingEnabled = hasNonBarSeries;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="m" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="none" responsive={false}>
|
||||
<TooltipWrapper
|
||||
tooltipContent={i18n.translate('xpack.lens.xyChart.fittingDisabledHelpText', {
|
||||
defaultMessage: 'This setting only applies to line and area charts.',
|
||||
})}
|
||||
condition={!hasNonBarSeries}
|
||||
tooltipContent={
|
||||
isAreaPercentage
|
||||
? i18n.translate('xpack.lens.xyChart.valuesPercentageDisabledHelpText', {
|
||||
defaultMessage: 'This setting cannot be changed on percentage area charts.',
|
||||
})
|
||||
: i18n.translate('xpack.lens.xyChart.valuesStackedDisabledHelpText', {
|
||||
defaultMessage: 'This setting cannot be changed on histograms.',
|
||||
})
|
||||
}
|
||||
condition={!isValueLabelsEnabled && !isFittingEnabled}
|
||||
>
|
||||
<ToolbarPopover
|
||||
title={i18n.translate('xpack.lens.xyChart.valuesLabel', {
|
||||
defaultMessage: 'Values',
|
||||
})}
|
||||
isDisabled={!hasNonBarSeries}
|
||||
type="values"
|
||||
groupPosition="left"
|
||||
buttonDataTestSubj="lnsMissingValuesButton"
|
||||
buttonDataTestSubj="lnsValuesButton"
|
||||
isDisabled={!isValueLabelsEnabled && !isFittingEnabled}
|
||||
>
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.xyChart.missingValuesLabel', {
|
||||
defaultMessage: 'Missing values',
|
||||
})}
|
||||
>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="lnsMissingValuesSelect"
|
||||
compressed
|
||||
options={fittingFunctionDefinitions.map(({ id, title, description }) => {
|
||||
return {
|
||||
value: id,
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>{title}</strong>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
inputDisplay: title,
|
||||
};
|
||||
{isValueLabelsEnabled ? (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={
|
||||
<span>
|
||||
{i18n.translate('xpack.lens.shared.chartValueLabelVisibilityLabel', {
|
||||
defaultMessage: 'Labels',
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<EuiButtonGroup
|
||||
isFullWidth
|
||||
legend={i18n.translate('xpack.lens.shared.chartValueLabelVisibilityLabel', {
|
||||
defaultMessage: 'Labels',
|
||||
})}
|
||||
data-test-subj="lnsValueLabelsDisplay"
|
||||
name="valueLabelsDisplay"
|
||||
buttonSize="compressed"
|
||||
options={valueLabelsOptions}
|
||||
idSelected={
|
||||
valueLabelsOptions.find(({ value }) => value === valueLabelsVisibilityMode)!
|
||||
.id
|
||||
}
|
||||
onChange={(modeId) => {
|
||||
const newMode = valueLabelsOptions.find(({ id }) => id === modeId)!.value;
|
||||
setState({ ...state, valueLabels: newMode });
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : null}
|
||||
{isFittingEnabled ? (
|
||||
<EuiFormRow
|
||||
display="columnCompressed"
|
||||
label={i18n.translate('xpack.lens.xyChart.missingValuesLabel', {
|
||||
defaultMessage: 'Missing values',
|
||||
})}
|
||||
valueOfSelected={state?.fittingFunction || 'None'}
|
||||
onChange={(value) => setState({ ...state, fittingFunction: value })}
|
||||
itemLayoutAlign="top"
|
||||
hasDividers
|
||||
/>
|
||||
</EuiFormRow>
|
||||
>
|
||||
<EuiSuperSelect
|
||||
data-test-subj="lnsMissingValuesSelect"
|
||||
compressed
|
||||
options={fittingFunctionDefinitions.map(({ id, title, description }) => {
|
||||
return {
|
||||
value: id,
|
||||
dropdownDisplay: (
|
||||
<>
|
||||
<strong>{title}</strong>
|
||||
<EuiText size="xs" color="subdued">
|
||||
<p>{description}</p>
|
||||
</EuiText>
|
||||
</>
|
||||
),
|
||||
inputDisplay: title,
|
||||
};
|
||||
})}
|
||||
valueOfSelected={state?.fittingFunction || 'None'}
|
||||
onChange={(value) => setState({ ...state, fittingFunction: value })}
|
||||
itemLayoutAlign="top"
|
||||
hasDividers
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : null}
|
||||
</ToolbarPopover>
|
||||
</TooltipWrapper>
|
||||
<LegendSettingsPopover
|
||||
|
|
|
@ -137,6 +137,7 @@ describe('xy_suggestions', () => {
|
|||
keptLayerIds: [],
|
||||
state: {
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -249,6 +250,7 @@ describe('xy_suggestions', () => {
|
|||
keptLayerIds: ['first'],
|
||||
state: {
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -289,6 +291,7 @@ describe('xy_suggestions', () => {
|
|||
keptLayerIds: ['first', 'second'],
|
||||
state: {
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -523,6 +526,7 @@ describe('xy_suggestions', () => {
|
|||
},
|
||||
state: {
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
{
|
||||
|
@ -575,6 +579,7 @@ describe('xy_suggestions', () => {
|
|||
test('keeps existing seriesType for initial tables', () => {
|
||||
const currentState: XYState = {
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
valueLabels: 'hide',
|
||||
fittingFunction: 'None',
|
||||
preferredSeriesType: 'line',
|
||||
layers: [
|
||||
|
@ -608,6 +613,7 @@ describe('xy_suggestions', () => {
|
|||
test('makes a visible seriesType suggestion for unchanged table without split', () => {
|
||||
const currentState: XYState = {
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
valueLabels: 'hide',
|
||||
fittingFunction: 'None',
|
||||
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
|
@ -648,6 +654,7 @@ describe('xy_suggestions', () => {
|
|||
test('suggests seriesType and stacking when there is a split', () => {
|
||||
const currentState: XYState = {
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
fittingFunction: 'None',
|
||||
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
|
@ -693,6 +700,7 @@ describe('xy_suggestions', () => {
|
|||
(generateId as jest.Mock).mockReturnValueOnce('dummyCol');
|
||||
const currentState: XYState = {
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
valueLabels: 'hide',
|
||||
fittingFunction: 'None',
|
||||
preferredSeriesType: 'bar',
|
||||
layers: [
|
||||
|
@ -725,6 +733,7 @@ describe('xy_suggestions', () => {
|
|||
test('suggests stacking for unchanged table that has a split', () => {
|
||||
const currentState: XYState = {
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
fittingFunction: 'None',
|
||||
layers: [
|
||||
|
@ -760,6 +769,7 @@ describe('xy_suggestions', () => {
|
|||
test('keeps column to dimension mappings on extended tables', () => {
|
||||
const currentState: XYState = {
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
fittingFunction: 'None',
|
||||
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
|
@ -802,6 +812,7 @@ describe('xy_suggestions', () => {
|
|||
test('changes column mappings when suggestion is reorder', () => {
|
||||
const currentState: XYState = {
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
fittingFunction: 'None',
|
||||
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
|
@ -845,6 +856,7 @@ describe('xy_suggestions', () => {
|
|||
(generateId as jest.Mock).mockReturnValueOnce('dummyCol');
|
||||
const currentState: XYState = {
|
||||
legend: { isVisible: true, position: 'bottom' },
|
||||
valueLabels: 'hide',
|
||||
preferredSeriesType: 'bar',
|
||||
fittingFunction: 'None',
|
||||
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
|
|
|
@ -509,6 +509,7 @@ function buildSuggestion({
|
|||
|
||||
const state: State = {
|
||||
legend: currentState ? currentState.legend : { isVisible: true, position: Position.Right },
|
||||
valueLabels: currentState?.valueLabels || 'hide',
|
||||
fittingFunction: currentState?.fittingFunction || 'None',
|
||||
xTitle: currentState?.xTitle,
|
||||
yTitle: currentState?.yTitle,
|
||||
|
|
|
@ -10937,7 +10937,6 @@
|
|||
"xpack.lens.xyChart.chartTypeLabel": "チャートタイプ",
|
||||
"xpack.lens.xyChart.chartTypeLegend": "チャートタイプ",
|
||||
"xpack.lens.xyChart.emptyXLabel": "(空)",
|
||||
"xpack.lens.xyChart.fittingDisabledHelpText": "この設定は折れ線グラフとエリアグラフでのみ適用されます。",
|
||||
"xpack.lens.xyChart.fittingFunction.help": "欠測値の処理方法を定義",
|
||||
"xpack.lens.xyChart.Gridlines": "グリッド線",
|
||||
"xpack.lens.xyChart.gridlinesSettings.help": "xおよびy軸のグリッド線を表示",
|
||||
|
|
|
@ -10950,7 +10950,6 @@
|
|||
"xpack.lens.xyChart.chartTypeLabel": "图表类型",
|
||||
"xpack.lens.xyChart.chartTypeLegend": "图表类型",
|
||||
"xpack.lens.xyChart.emptyXLabel": "(空)",
|
||||
"xpack.lens.xyChart.fittingDisabledHelpText": "此设置仅适用于折线图和面积图。",
|
||||
"xpack.lens.xyChart.fittingFunction.help": "定义处理缺失值的方式",
|
||||
"xpack.lens.xyChart.Gridlines": "网格线",
|
||||
"xpack.lens.xyChart.gridlinesSettings.help": "显示 x 和 y 轴网格线",
|
||||
|
|
|
@ -222,8 +222,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
},
|
||||
async editMissingValues(option: string) {
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('lnsMissingValuesButton');
|
||||
await testSubjects.exists('lnsMissingValuesSelect');
|
||||
await testSubjects.click('lnsValuesButton');
|
||||
await testSubjects.exists('lnsValuesButton');
|
||||
});
|
||||
await testSubjects.click('lnsMissingValuesSelect');
|
||||
const optionSelector = await find.byCssSelector(`#${option}`);
|
||||
|
|
Loading…
Reference in a new issue