[Lens] Enable nice rounding for scalar axis (#149388)

## Summary

Fix #145245 

This PR adds the "axis rounding to nice values" enabled by default in
Lens. Other editors are not affected by this change.
The feature can be manually disabled by the user in the relative axis
popover menu, when a compatibility mode is chosen (`full` for metrics,
`dataBounds` for bucketed).

<img width="920" alt="Screenshot 2023-01-24 at 09 34 07"
src="https://user-images.githubusercontent.com/924948/214260732-712820e9-3127-4a7c-8851-9c093f321aeb.png">

In case of incompatible mode the rounding option is hidden (and
internally ignored):

<img width="396" alt="Screenshot 2023-01-24 at 09 35 25"
src="https://user-images.githubusercontent.com/924948/214261330-8aece151-b972-4071-9474-06cf7539956c.png">
<img width="476" alt="Screenshot 2023-01-24 at 09 35 42"
src="https://user-images.githubusercontent.com/924948/214261355-4a0dc9e0-d69d-4a59-bed9-3b69de23664f.png">
<img width="396" alt="Screenshot 2023-01-24 at 09 36 08"
src="https://user-images.githubusercontent.com/924948/214261383-7c050366-0cb2-498e-a3af-2f7509a87934.png">

Here's a dashboard with several combinations of Lens XY configurations
with (upper row) and without value nice-ing enabled (bottom row):

<img width="1497" alt="Screenshot 2023-01-24 at 10 08 27"
src="https://user-images.githubusercontent.com/924948/214261007-7bff4be1-16c1-40e6-95f4-b718973877a6.png">

## Note

The PR was also an opportunity to do a little shared component
refactoring for axes.

### Checklist

Delete any items that are not applicable to this PR.

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] If a plugin configuration key changed, check if it needs to be
allowlisted in the cloud and added to the [docker
list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### Risk Matrix

Delete this section if it is not applicable to this PR.

Before closing this PR, invite QA, stakeholders, and other developers to
identify risks that should be tested prior to the change/feature
release.

When forming the risk matrix, consider some of the following examples
and how they may potentially impact the change:

| Risk | Probability | Severity | Mitigation/Notes |

|---------------------------|-------------|----------|-------------------------|
| Multiple Spaces&mdash;unexpected behavior in non-default Kibana Space.
| Low | High | Integration tests will verify that all features are still
supported in non-default Kibana Space and when user switches between
spaces. |
| Multiple nodes&mdash;Elasticsearch polling might have race conditions
when multiple Kibana nodes are polling for the same tasks. | High | Low
| Tasks are idempotent, so executing them multiple times will not result
in logical error, but will degrade performance. To test for this case we
add plenty of unit tests around this logic and document manual testing
procedure. |
| Code should gracefully handle cases when feature X or plugin Y are
disabled. | Medium | High | Unit tests will verify that any feature flag
or plugin combination still results in our service operational. |
| [See more potential risk
examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) |


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Marco Liberati 2023-01-27 10:26:28 +01:00 committed by GitHub
parent 2b276a9dca
commit 83dd5f84fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 598 additions and 363 deletions

View file

@ -53,7 +53,7 @@ pageLoadAssetSize:
expressions: 140958
expressionShape: 34008
expressionTagcloud: 27505
expressionXY: 38500
expressionXY: 39500
features: 21723
fieldFormats: 65209
files: 22673

View file

@ -59,6 +59,12 @@ export const axisExtentConfigFunction: ExpressionFunctionDefinition<
defaultMessage: 'Enforce extent params.',
}),
},
niceValues: {
types: ['boolean'],
help: i18n.translate('expressionXY.axisExtentConfig.niceValues.help', {
defaultMessage: 'Enable axis extents value rounding',
}),
},
},
fn(input, args) {
if (args.mode === AxisExtentModes.CUSTOM) {

View file

@ -72,6 +72,7 @@ export interface AxisExtentConfig {
lowerBound?: number;
upperBound?: number;
enforce?: boolean;
niceValues?: boolean;
}
export interface AxisConfig {

View file

@ -1219,6 +1219,19 @@ exports[`XYChart component it renders area 1`] = `
}
}
valueLabels="hide"
xAxisConfiguration={
Object {
"groupId": "bottom",
"labelsOrientation": 0,
"position": "bottom",
"series": Array [],
"showGridLines": true,
"showLabels": true,
"showTitle": true,
"title": "",
"type": "xAxisConfig",
}
}
yAxesConfiguration={
Array [
Object {
@ -2206,6 +2219,19 @@ exports[`XYChart component it renders bar 1`] = `
}
}
valueLabels="hide"
xAxisConfiguration={
Object {
"groupId": "bottom",
"labelsOrientation": 0,
"position": "bottom",
"series": Array [],
"showGridLines": true,
"showLabels": true,
"showTitle": true,
"title": "",
"type": "xAxisConfig",
}
}
yAxesConfiguration={
Array [
Object {
@ -3193,6 +3219,19 @@ exports[`XYChart component it renders horizontal bar 1`] = `
}
}
valueLabels="hide"
xAxisConfiguration={
Object {
"groupId": "bottom",
"labelsOrientation": 0,
"position": "right",
"series": Array [],
"showGridLines": true,
"showLabels": true,
"showTitle": true,
"title": "",
"type": "xAxisConfig",
}
}
yAxesConfiguration={
Array [
Object {
@ -4180,6 +4219,19 @@ exports[`XYChart component it renders line 1`] = `
}
}
valueLabels="hide"
xAxisConfiguration={
Object {
"groupId": "bottom",
"labelsOrientation": 0,
"position": "bottom",
"series": Array [],
"showGridLines": true,
"showLabels": true,
"showTitle": true,
"title": "",
"type": "xAxisConfig",
}
}
yAxesConfiguration={
Array [
Object {
@ -5167,6 +5219,19 @@ exports[`XYChart component it renders stacked area 1`] = `
}
}
valueLabels="hide"
xAxisConfiguration={
Object {
"groupId": "bottom",
"labelsOrientation": 0,
"position": "bottom",
"series": Array [],
"showGridLines": true,
"showLabels": true,
"showTitle": true,
"title": "",
"type": "xAxisConfig",
}
}
yAxesConfiguration={
Array [
Object {
@ -6154,6 +6219,19 @@ exports[`XYChart component it renders stacked bar 1`] = `
}
}
valueLabels="hide"
xAxisConfiguration={
Object {
"groupId": "bottom",
"labelsOrientation": 0,
"position": "bottom",
"series": Array [],
"showGridLines": true,
"showLabels": true,
"showTitle": true,
"title": "",
"type": "xAxisConfig",
}
}
yAxesConfiguration={
Array [
Object {
@ -7141,6 +7219,19 @@ exports[`XYChart component it renders stacked horizontal bar 1`] = `
}
}
valueLabels="hide"
xAxisConfiguration={
Object {
"groupId": "bottom",
"labelsOrientation": 0,
"position": "right",
"series": Array [],
"showGridLines": true,
"showLabels": true,
"showTitle": true,
"title": "",
"type": "xAxisConfig",
}
}
yAxesConfiguration={
Array [
Object {
@ -8358,6 +8449,19 @@ exports[`XYChart component split chart should render split chart if both, splitR
}
}
valueLabels="hide"
xAxisConfiguration={
Object {
"groupId": "bottom",
"labelsOrientation": 0,
"position": "bottom",
"series": Array [],
"showGridLines": true,
"showLabels": true,
"showTitle": true,
"title": "",
"type": "xAxisConfig",
}
}
yAxesConfiguration={
Array [
Object {
@ -9576,6 +9680,19 @@ exports[`XYChart component split chart should render split chart if splitColumnA
}
}
valueLabels="hide"
xAxisConfiguration={
Object {
"groupId": "bottom",
"labelsOrientation": 0,
"position": "bottom",
"series": Array [],
"showGridLines": true,
"showLabels": true,
"showTitle": true,
"title": "",
"type": "xAxisConfig",
}
}
yAxesConfiguration={
Array [
Object {
@ -10794,6 +10911,19 @@ exports[`XYChart component split chart should render split chart if splitRowAcce
}
}
valueLabels="hide"
xAxisConfiguration={
Object {
"groupId": "bottom",
"labelsOrientation": 0,
"position": "bottom",
"series": Array [],
"showGridLines": true,
"showLabels": true,
"showTitle": true,
"title": "",
"type": "xAxisConfig",
}
}
yAxesConfiguration={
Array [
Object {

View file

@ -42,6 +42,7 @@ interface Props {
formatFactory: FormatFactory;
chartHasMoreThanOneBarSeries?: boolean;
yAxesConfiguration: GroupsConfiguration;
xAxisConfiguration?: GroupsConfiguration[number];
fittingFunction?: FittingFunction;
endValue?: EndValue | undefined;
paletteService: PaletteRegistry;
@ -71,6 +72,7 @@ export const DataLayers: FC<Props> = ({
fittingFunction,
emphasizeFitting,
yAxesConfiguration,
xAxisConfiguration,
shouldShowValueLabels,
formattedDatatables,
chartHasMoreThanOneBarSeries,
@ -157,6 +159,7 @@ export const DataLayers: FC<Props> = ({
formattedDatatableInfo,
syncColors,
yAxis,
xAxis: xAxisConfiguration,
timeZone,
emphasizeFitting,
fillOpacity,

View file

@ -1033,6 +1033,9 @@ export function XYChart({
fittingFunction={fittingFunction}
emphasizeFitting={emphasizeFitting}
yAxesConfiguration={yAxesConfiguration}
xAxisConfiguration={
xAxisConfig ? axesConfiguration[axesConfiguration.length - 1] : undefined
}
shouldShowValueLabels={shouldShowValueLabels}
formattedDatatables={formattedDatatables}
chartHasMoreThanOneBarSeries={chartHasMoreThanOneBarSeries}

View file

@ -47,6 +47,7 @@ type GetSeriesPropsFn = (config: {
paletteService: PaletteRegistry;
syncColors?: boolean;
yAxis?: GroupsConfiguration[number];
xAxis?: GroupsConfiguration[number];
timeZone?: string;
emphasizeFitting?: boolean;
fillOpacity?: number;
@ -388,6 +389,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({
paletteService,
syncColors,
yAxis,
xAxis,
timeZone,
emphasizeFitting,
fillOpacity,
@ -546,5 +548,7 @@ export const getSeriesProps: GetSeriesPropsFn = ({
name(d) {
return getSeriesNameFn(d);
},
yNice: Boolean(yAxis?.extent?.niceValues),
xNice: Boolean(xAxis?.extent?.niceValues),
};
};

View file

@ -0,0 +1,286 @@
/*
* 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 { EuiFormRow, EuiButtonGroup, htmlIdGenerator, EuiSwitch } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RangeInputField } from '../../range_input_field';
import { validateExtent } from './helpers';
import type { UnifiedAxisExtentConfig } from './types';
const idPrefix = htmlIdGenerator()();
interface DataBoundsObject {
min: number;
max: number;
}
export function AxisBoundsControl({
type,
canHaveNiceValues,
disableCustomRange,
...props
}: {
type: 'metric' | 'bucket';
extent: UnifiedAxisExtentConfig;
setExtent: (newExtent: UnifiedAxisExtentConfig | undefined) => void;
dataBounds: DataBoundsObject | undefined;
shouldIncludeZero: boolean;
disableCustomRange: boolean;
testSubjPrefix: string;
canHaveNiceValues?: boolean;
}) {
const { extent, shouldIncludeZero, setExtent, dataBounds, testSubjPrefix } = props;
const { inclusiveZeroError, boundaryError } = validateExtent(shouldIncludeZero, extent);
// Bucket type does not have the "full" mode
const modeForNiceValues = type === 'metric' ? 'full' : 'dataBounds';
const canShowNiceValues = canHaveNiceValues && extent.mode === modeForNiceValues;
const canShowCustomRanges =
extent?.mode === 'custom' && (type === 'bucket' || !disableCustomRange);
const ModeAxisBoundsControl =
type === 'metric' ? MetricAxisBoundsControl : BucketAxisBoundsControl;
return (
<ModeAxisBoundsControl {...props} disableCustomRange={disableCustomRange}>
{canShowNiceValues ? (
<EuiFormRow
label={i18n.translate('xpack.lens.fullExtent.niceValues', {
defaultMessage: 'Round to nice values',
})}
display="columnCompressedSwitch"
fullWidth
>
<EuiSwitch
showLabel={false}
label={i18n.translate('xpack.lens.fullExtent.niceValues', {
defaultMessage: 'Round to nice values',
})}
data-test-subj={`${testSubjPrefix}_axisExtent_niceValues`}
checked={Boolean(extent.niceValues == null || extent.niceValues)}
onChange={() => {
setExtent({
...extent,
mode: modeForNiceValues,
niceValues: !Boolean(extent.niceValues == null || extent.niceValues),
});
}}
compressed
/>
</EuiFormRow>
) : null}
{canShowCustomRanges ? (
<RangeInputField
isInvalid={inclusiveZeroError || boundaryError}
label={' '}
helpText={
shouldIncludeZero && (!inclusiveZeroError || boundaryError)
? i18n.translate('xpack.lens.axisExtent.inclusiveZero', {
defaultMessage: 'Bounds must include zero.',
})
: undefined
}
error={
boundaryError
? i18n.translate('xpack.lens.axisExtent.boundaryError', {
defaultMessage: 'Lower bound has to be larger than upper bound',
})
: shouldIncludeZero && inclusiveZeroError
? i18n.translate('xpack.lens.axisExtent.inclusiveZero', {
defaultMessage: 'Bounds must include zero.',
})
: undefined
}
testSubjLayout={`${testSubjPrefix}_axisExtent_customBounds`}
testSubjLower={`${testSubjPrefix}_axisExtent_lowerBound`}
testSubjUpper={`${testSubjPrefix}_axisExtent_upperBound`}
lowerValue={extent.lowerBound ?? ''}
onLowerValueChange={(e) => {
const val = Number(e.target.value);
const isEmptyValue = e.target.value === '' || Number.isNaN(Number(val));
setExtent({
...extent,
lowerBound: isEmptyValue ? undefined : val,
});
}}
onLowerValueBlur={() => {
if (extent.lowerBound === undefined && dataBounds) {
setExtent({
...extent,
lowerBound: Math.min(0, dataBounds.min),
});
}
}}
upperValue={extent.upperBound ?? ''}
onUpperValueChange={(e) => {
const val = Number(e.target.value);
const isEmptyValue = e.target.value === '' || Number.isNaN(Number(val));
setExtent({
...extent,
upperBound: isEmptyValue ? undefined : val,
});
}}
onUpperValueBlur={() => {
if (extent.upperBound === undefined && dataBounds) {
setExtent({
...extent,
upperBound: dataBounds.max,
});
}
}}
/>
) : null}
</ModeAxisBoundsControl>
);
}
interface ModeAxisBoundsControlProps {
extent: UnifiedAxisExtentConfig;
setExtent: (newExtent: UnifiedAxisExtentConfig | undefined) => void;
dataBounds: DataBoundsObject | undefined;
shouldIncludeZero: boolean;
disableCustomRange: boolean;
testSubjPrefix: string;
children: React.ReactNode;
}
function MetricAxisBoundsControl({
extent,
setExtent,
dataBounds,
shouldIncludeZero,
disableCustomRange,
testSubjPrefix,
children,
}: ModeAxisBoundsControlProps) {
return (
<>
<EuiFormRow
display="columnCompressed"
fullWidth
label={i18n.translate('xpack.lens.axisExtent.label', {
defaultMessage: 'Bounds',
})}
helpText={
shouldIncludeZero
? i18n.translate('xpack.lens.axisExtent.disabledDataBoundsMessage', {
defaultMessage: 'Only line charts can be fit to the data bounds',
})
: undefined
}
>
<EuiButtonGroup
isFullWidth
legend={i18n.translate('xpack.lens.axisExtent.label', {
defaultMessage: 'Bounds',
})}
data-test-subj={`${testSubjPrefix}_axisBounds_groups`}
name="axisBounds"
buttonSize="compressed"
options={[
{
id: `${idPrefix}full`,
label: i18n.translate('xpack.lens.axisExtent.full', {
defaultMessage: 'Full',
}),
'data-test-subj': `${testSubjPrefix}_axisExtent_groups_full'`,
},
{
id: `${idPrefix}dataBounds`,
label: i18n.translate('xpack.lens.axisExtent.axisExtent.dataBounds', {
defaultMessage: 'Data',
}),
'data-test-subj': `${testSubjPrefix}_axisExtent_groups_DataBounds'`,
isDisabled: shouldIncludeZero,
},
{
id: `${idPrefix}custom`,
label: i18n.translate('xpack.lens.axisExtent.axisExtent.custom', {
defaultMessage: 'Custom',
}),
'data-test-subj': `${testSubjPrefix}_axisExtent_groups_custom'`,
isDisabled: disableCustomRange,
},
]}
idSelected={`${idPrefix}${
(shouldIncludeZero && extent.mode === 'dataBounds') || disableCustomRange
? 'full'
: extent.mode
}`}
onChange={(id) => {
const newMode = id.replace(idPrefix, '') as UnifiedAxisExtentConfig['mode'];
setExtent({
...extent,
mode: newMode,
lowerBound:
newMode === 'custom' && dataBounds ? Math.min(0, dataBounds.min) : undefined,
upperBound: newMode === 'custom' && dataBounds ? dataBounds.max : undefined,
});
}}
/>
</EuiFormRow>
{children}
</>
);
}
function BucketAxisBoundsControl({
extent,
setExtent,
dataBounds,
testSubjPrefix,
children,
}: ModeAxisBoundsControlProps) {
return (
<>
<EuiFormRow
display="columnCompressed"
fullWidth
label={i18n.translate('xpack.lens.axisExtent.label', {
defaultMessage: 'Bounds',
})}
>
<EuiButtonGroup
isFullWidth
legend={i18n.translate('xpack.lens.axisExtent.label', {
defaultMessage: 'Bounds',
})}
data-test-subj={`${testSubjPrefix}_axisBounds_groups`}
name="axisBounds"
buttonSize="compressed"
options={[
{
id: `${idPrefix}dataBounds`,
label: i18n.translate('xpack.lens.axisExtent.dataBounds', {
defaultMessage: 'Data',
}),
'data-test-subj': `${testSubjPrefix}_axisExtent_groups_DataBounds'`,
},
{
id: `${idPrefix}custom`,
label: i18n.translate('xpack.lens.axisExtent.custom', {
defaultMessage: 'Custom',
}),
'data-test-subj': `${testSubjPrefix}_axisExtent_groups_custom'`,
},
]}
idSelected={`${idPrefix}${extent.mode ?? 'dataBounds'}`}
onChange={(id) => {
const newMode = id.replace(idPrefix, '') as UnifiedAxisExtentConfig['mode'];
setExtent({
...extent,
mode: newMode,
lowerBound: newMode === 'custom' && dataBounds ? dataBounds.min : undefined,
upperBound: newMode === 'custom' && dataBounds ? dataBounds.max : undefined,
});
}}
/>
</EuiFormRow>
{children}
</>
);
}

View file

@ -6,8 +6,8 @@
*/
import { Datatable } from '@kbn/expressions-plugin/common';
import { createMockDatasource } from '../../mocks';
import { OperationDescriptor, DatasourcePublicAPI } from '../../types';
import { createMockDatasource } from '../../../mocks';
import { OperationDescriptor, DatasourcePublicAPI } from '../../../types';
import {
hasNumericHistogramDimension,
validateAxisDomain,

View file

@ -5,8 +5,9 @@
* 2.0.
*/
import { Datatable } from '@kbn/expressions-plugin/common';
import type { DatasourcePublicAPI } from '../../types';
import type { Datatable } from '@kbn/expressions-plugin/common';
import type { DatasourcePublicAPI } from '../../../types';
import type { UnifiedAxisExtentConfig } from './types';
/**
* Returns true if the provided extent includes 0
@ -84,3 +85,10 @@ export function getDataBounds(
}
}
}
export function validateExtent(shouldIncludeZero: boolean, extent?: UnifiedAxisExtentConfig) {
return {
inclusiveZeroError: shouldIncludeZero && !validateZeroInclusivityExtent(extent),
boundaryError: !validateAxisDomain(extent),
};
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
export { BucketAxisBoundsControl } from './axis_extent_settings';
export { AxisBoundsControl } from './axis_extent_settings';
export {
validateAxisDomain,
validateZeroInclusivityExtent,

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { Ast } from '@kbn/interpreter';
import { UnifiedAxisExtentConfig } from './types';
import type { Ast } from '@kbn/interpreter';
import type { UnifiedAxisExtentConfig } from './types';
// TODO: import it from the expression config directly?
const CHART_TO_FN_NAME = {
@ -25,8 +25,10 @@ export const axisExtentConfigToExpression = (
arguments: {
// rely on expression default value here
mode: extent?.mode ? [extent.mode] : [],
lowerBound: extent?.lowerBound !== undefined ? [extent?.lowerBound] : [],
upperBound: extent?.upperBound !== undefined ? [extent?.upperBound] : [],
lowerBound: extent?.lowerBound != null ? [extent.lowerBound] : [],
upperBound: extent?.upperBound != null ? [extent.upperBound] : [],
// be explicit in this case
niceValues: extent?.niceValues != null ? [extent.niceValues] : [true],
},
},
],

View file

@ -9,7 +9,7 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import { mountWithIntl as mount } from '@kbn/test-jest-helpers';
import { AxisTitleSettings, AxisTitleSettingsProps } from './axis_title_settings';
import { Label, VisLabel } from './vis_label';
import { Label, VisLabel } from '../../vis_label';
describe('Axes Title settings', () => {
let props: AxisTitleSettingsProps;

View file

@ -8,8 +8,8 @@
import React, { useCallback, useMemo } from 'react';
import { EuiSpacer, EuiFormRow } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { AxesSettingsConfig } from '../visualizations/xy/types';
import { LabelMode, useDebouncedValue, VisLabel } from '.';
import type { AxesSettingsConfig } from '../../../visualizations/xy/types';
import { type LabelMode, useDebouncedValue, VisLabel } from '../..';
type AxesSettingsConfigKeys = keyof AxesSettingsConfig;

View file

@ -1,125 +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 { EuiFormRow, EuiButtonGroup, htmlIdGenerator } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { RangeInputField } from '../range_input_field';
import { validateAxisDomain } from './helpers';
import { UnifiedAxisExtentConfig } from './types';
const idPrefix = htmlIdGenerator()();
export function BucketAxisBoundsControl({
testSubjPrefix,
extent,
setExtent,
dataBounds,
}: {
testSubjPrefix: string;
extent: UnifiedAxisExtentConfig;
setExtent: (newExtent: UnifiedAxisExtentConfig | undefined) => void;
dataBounds: { min: number; max: number } | undefined;
}) {
const boundaryError = !validateAxisDomain(extent);
return (
<>
<EuiFormRow
display="columnCompressed"
fullWidth
label={i18n.translate('xpack.lens.axisExtent.label', {
defaultMessage: 'Bounds',
})}
>
<EuiButtonGroup
isFullWidth
legend={i18n.translate('xpack.lens.axisExtent.label', {
defaultMessage: 'Bounds',
})}
data-test-subj={`${testSubjPrefix}_axisBounds_groups`}
name="axisBounds"
buttonSize="compressed"
options={[
{
id: `${idPrefix}dataBounds`,
label: i18n.translate('xpack.lens.axisExtent.dataBounds', {
defaultMessage: 'Data',
}),
'data-test-subj': `${testSubjPrefix}_axisExtent_groups_DataBounds'`,
},
{
id: `${idPrefix}custom`,
label: i18n.translate('xpack.lens.axisExtent.custom', {
defaultMessage: 'Custom',
}),
'data-test-subj': `${testSubjPrefix}_axisExtent_groups_custom'`,
},
]}
idSelected={`${idPrefix}${extent.mode ?? 'dataBounds'}`}
onChange={(id) => {
const newMode = id.replace(idPrefix, '') as UnifiedAxisExtentConfig['mode'];
setExtent({
...extent,
mode: newMode,
lowerBound: newMode === 'custom' && dataBounds ? dataBounds.min : undefined,
upperBound: newMode === 'custom' && dataBounds ? dataBounds.max : undefined,
});
}}
/>
</EuiFormRow>
{extent?.mode === 'custom' && (
<RangeInputField
isInvalid={boundaryError}
label={' '}
error={
boundaryError
? i18n.translate('xpack.lens.boundaryError', {
defaultMessage: 'Lower bound has to be larger than upper bound',
})
: undefined
}
testSubjLayout={`${testSubjPrefix}_axisExtent_customBounds`}
testSubjLower={`${testSubjPrefix}_axisExtent_lowerBound`}
testSubjUpper={`${testSubjPrefix}_axisExtent_upperBound`}
lowerValue={extent.lowerBound ?? ''}
onLowerValueChange={(e) => {
const val = Number(e.target.value);
const isEmptyValue = e.target.value === '' || Number.isNaN(Number(val));
setExtent({
...extent,
lowerBound: isEmptyValue ? undefined : val,
});
}}
onLowerValueBlur={() => {
if (extent.lowerBound === undefined && dataBounds) {
setExtent({
...extent,
lowerBound: dataBounds.min,
});
}
}}
upperValue={extent.upperBound ?? ''}
onUpperValueChange={(e) => {
const val = Number(e.target.value);
const isEmptyValue = e.target.value === '' || Number.isNaN(Number(val));
setExtent({
...extent,
upperBound: isEmptyValue ? undefined : val,
});
}}
onUpperValueBlur={() => {
if (extent.upperBound === undefined && dataBounds) {
setExtent({
...extent,
upperBound: dataBounds.max,
});
}
}}
/>
)}
</>
);
}

View file

@ -7,7 +7,7 @@
export type { ToolbarPopoverProps } from './toolbar_popover';
export { ToolbarPopover } from './toolbar_popover';
export { LegendSettingsPopover } from './legend_settings_popover';
export { LegendSettingsPopover } from './legend/legend_settings_popover';
export { PalettePicker } from './palette_picker';
export { FieldPicker, TruncatedLabel } from './field_picker';
export type { FieldOption, FieldOptionValue } from './field_picker';
@ -21,21 +21,21 @@ export {
} from './drag_drop_bucket';
export { RangeInputField } from './range_input_field';
export {
BucketAxisBoundsControl,
AxisBoundsControl,
validateAxisDomain,
validateZeroInclusivityExtent,
hasNumericHistogramDimension,
getDataBounds,
axisExtentConfigToExpression,
} from './axis_extent';
} from './axis/extent';
export { TooltipWrapper } from './tooltip_wrapper';
export * from './coloring';
export { useDebouncedValue } from './debounced_value';
export * from './helpers';
export { LegendActionPopover } from './legend_action_popover';
export { LegendActionPopover } from './legend/action/legend_action_popover';
export { NameInput } from './name_input';
export { ValueLabelsSettings } from './value_labels_settings';
export { AxisTitleSettings } from './axis_title_settings';
export { AxisTitleSettings } from './axis/title/axis_title_settings';
export { DimensionEditorSection } from './dimension_section';
export { FilterQueryInput } from './filter_query_input';
export * from './static_header';

View file

@ -8,7 +8,7 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiFieldNumber, EuiFormRow } from '@elastic/eui';
import { useDebouncedValue } from './debounced_value';
import { useDebouncedValue } from '../../debounced_value';
export const DEFAULT_FLOATING_COLUMNS = 1;

View file

@ -17,11 +17,11 @@ import {
import { Position, VerticalAlignment, HorizontalAlignment } from '@elastic/charts';
import { ToolbarButtonProps } from '@kbn/kibana-react-plugin/public';
import { LegendSize } from '@kbn/visualizations-plugin/public';
import { ToolbarPopover } from '.';
import { LegendLocationSettings } from './legend_location_settings';
import { ColumnsNumberSetting } from './columns_number_setting';
import { LegendSizeSettings } from './legend_size_settings';
import { useDebouncedValue } from './debounced_value';
import { ToolbarPopover } from '../toolbar_popover';
import { LegendLocationSettings } from './location/legend_location_settings';
import { ColumnsNumberSetting } from './layout/columns_number_setting';
import { LegendSizeSettings } from './size/legend_size_settings';
import { useDebouncedValue } from '../debounced_value';
export interface LegendSettingsPopoverProps {
/**

View file

@ -131,7 +131,6 @@ Object {
"chain": Array [
Object {
"arguments": Object {
"extent": Array [],
"id": Array [
"x",
],
@ -166,6 +165,27 @@ Object {
"chain": Array [
Object {
"arguments": Object {
"extent": Array [
Object {
"chain": Array [
Object {
"arguments": Object {
"lowerBound": Array [],
"mode": Array [
"full",
],
"niceValues": Array [
true,
],
"upperBound": Array [],
},
"function": "axisExtentConfig",
"type": "function",
},
],
"type": "expression",
},
],
"labelsOrientation": Array [
-90,
],
@ -209,6 +229,9 @@ Object {
"mode": Array [
"custom",
],
"niceValues": Array [
true,
],
"upperBound": Array [
456,
],

View file

@ -59,7 +59,10 @@ import {
getAnnotationsLayers,
} from './visualization_helpers';
import { getUniqueLabels } from './annotations/helpers';
import { axisExtentConfigToExpression } from '../../shared_components';
import {
axisExtentConfigToExpression,
hasNumericHistogramDimension,
} from '../../shared_components';
import type { CollapseExpressionFunction } from '../../../common/expressions';
export const getSortedAccessors = (
@ -314,7 +317,13 @@ export const buildXYExpression = (
showLabels: state?.tickLabelsVisibilitySettings?.x ?? true,
showGridLines: state?.gridlinesVisibilitySettings?.x ?? true,
labelsOrientation: state?.labelsOrientation?.x ?? 0,
extent: state.xExtent ? [axisExtentConfigToExpression(state.xExtent)] : [],
extent:
state.xExtent ||
validDataLayers.some((layer) =>
hasNumericHistogramDimension(datasourceLayers[layer.layerId], layer.xAccessor)
)
? [axisExtentConfigToExpression(state.xExtent ?? { mode: 'dataBounds', niceValues: true })]
: undefined,
});
const layeredXyVisFn = buildExpressionFunction<LayeredXyVisFn>('layeredXyVis', {
@ -385,7 +394,7 @@ const yAxisConfigsToExpression = (yAxisConfigs: AxisConfig[]): Ast[] => {
buildExpressionFunction<YAxisConfigFn>('yAxisConfig', {
id: axis.id,
position: axis.position,
extent: axis.extent ? axisExtentConfigToExpression(axis.extent) : undefined,
extent: axisExtentConfigToExpression(axis.extent ?? { mode: 'full', niceValues: true }),
showTitle: axis.showTitle ?? true,
title: axis.title,
showLabels: axis.showLabels ?? true,

View file

@ -12,16 +12,26 @@ import { ToolbarPopover } from '../../../shared_components';
import { LayerTypes } from '@kbn/expression-xy-plugin/public';
import { ShallowWrapper } from 'enzyme';
function getRangeInputComponent(component: ShallowWrapper) {
return component
.find('[testSubjPrefix="lnsXY"]')
.shallow()
function getExtentControl(root: ShallowWrapper) {
return root.find('[testSubjPrefix="lnsXY"]').shallow();
}
function getRangeInputComponent(root: ShallowWrapper) {
return getExtentControl(root)
.find('RangeInputField')
.shallow()
.find('EuiFormControlLayoutDelimited')
.shallow();
}
function getModeButtonsComponent(root: ShallowWrapper) {
return getExtentControl(root).find('[testSubjPrefix="lnsXY"]').shallow();
}
function getNiceValueSwitch(root: ShallowWrapper) {
return getExtentControl(root).find('[data-test-subj="lnsXY_axisExtent_niceValues"]');
}
describe('Axes Settings', () => {
let props: AxisSettingsPopoverProps;
beforeEach(() => {
@ -141,7 +151,7 @@ describe('Axes Settings', () => {
describe('axis extent', () => {
it('hides the extent section if no extent is passed in', () => {
const component = shallow(<AxisSettingsPopover {...props} />);
expect(component.find('[data-test-subj="lnsXY_axisBounds_groups"]').length).toBe(0);
expect(component.find('[testSubjPrefix="lnsXY"]').isEmptyRender()).toBe(true);
});
it('renders 3 options for metric bound inputs', () => {
@ -154,11 +164,38 @@ describe('Axes Settings', () => {
setExtent={setSpy}
/>
);
const boundInput = component.find('[testSubjPrefix="lnsXY"]').shallow();
const buttonGroup = boundInput.find('[data-test-subj="lnsXY_axisBounds_groups"]');
const buttonGroup = getModeButtonsComponent(component).find(
'[data-test-subj="lnsXY_axisBounds_groups"]'
);
expect(buttonGroup.prop('options')).toHaveLength(3);
});
it('renders nice values enabled by default if mode is full for metric', () => {
const setSpy = jest.fn();
const component = shallow(
<AxisSettingsPopover {...props} axis="yLeft" extent={{ mode: 'full' }} setExtent={setSpy} />
);
const niceValuesSwitch = getNiceValueSwitch(component);
expect(niceValuesSwitch.prop('checked')).toBe(true);
});
it('should not renders nice values if mode is custom for metric', () => {
const setSpy = jest.fn();
const component = shallow(
<AxisSettingsPopover
{...props}
extent={{ mode: 'custom', lowerBound: 123, upperBound: 456 }}
axis="yLeft"
setExtent={setSpy}
/>
);
expect(
getExtentControl(component)
.find('[data-test-subj="lnsXY_axisExtent_niceValues"]')
.isEmptyRender()
).toBe(true);
});
it('renders metric (y) bound inputs if mode is custom', () => {
const setSpy = jest.fn();
const component = shallow(
@ -176,7 +213,7 @@ describe('Axes Settings', () => {
expect(upper.prop('value')).toEqual(456);
});
it('renders 2 options for metric bound inputs', () => {
it('renders 2 options for bucket bound inputs', () => {
const setSpy = jest.fn();
const component = shallow(
<AxisSettingsPopover
@ -186,11 +223,43 @@ describe('Axes Settings', () => {
setExtent={setSpy}
/>
);
const boundInput = component.find('[testSubjPrefix="lnsXY"]').shallow();
const buttonGroup = boundInput.find('[data-test-subj="lnsXY_axisBounds_groups"]');
const buttonGroup = getModeButtonsComponent(component).find(
'[data-test-subj="lnsXY_axisBounds_groups"]'
);
expect(buttonGroup.prop('options')).toHaveLength(2);
});
it('renders nice values enabled by default if mode is dataBounds for bucket', () => {
const setSpy = jest.fn();
const component = shallow(
<AxisSettingsPopover
{...props}
axis="x"
extent={{ mode: 'dataBounds' }}
setExtent={setSpy}
/>
);
const niceValuesSwitch = getNiceValueSwitch(component);
expect(niceValuesSwitch.prop('checked')).toBe(true);
});
it('should not renders nice values if mode is custom for bucket', () => {
const setSpy = jest.fn();
const component = shallow(
<AxisSettingsPopover
{...props}
extent={{ mode: 'custom', lowerBound: 123, upperBound: 456 }}
axis="x"
setExtent={setSpy}
/>
);
expect(
getExtentControl(component)
.find('[data-test-subj="lnsXY_axisExtent_niceValues"]')
.isEmptyRender()
).toBe(true);
});
it('renders bucket (x) bound inputs if mode is custom', () => {
const setSpy = jest.fn();
const component = shallow(

View file

@ -6,14 +6,7 @@
*/
import React, { useCallback } from 'react';
import {
EuiSwitch,
IconType,
EuiFormRow,
EuiButtonGroup,
htmlIdGenerator,
EuiSelect,
} from '@elastic/eui';
import { EuiSwitch, IconType, EuiFormRow, EuiButtonGroup, EuiSelect } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isEqual } from 'lodash';
import { AxisExtentConfig, YScaleType } from '@kbn/expression-xy-plugin/common';
@ -29,8 +22,7 @@ import {
ToolbarPopover,
useDebouncedValue,
AxisTitleSettings,
RangeInputField,
BucketAxisBoundsControl,
AxisBoundsControl,
} from '../../../shared_components';
import { XYLayerConfig, AxesSettingsConfig } from '../types';
import { validateExtent } from '../axes_configuration';
@ -214,7 +206,6 @@ const axisOrientationOptions: Array<{
},
];
const idPrefix = htmlIdGenerator()();
export const AxisSettingsPopover: React.FunctionComponent<AxisSettingsPopoverProps> = ({
layers,
axis,
@ -426,170 +417,19 @@ export const AxisSettingsPopover: React.FunctionComponent<AxisSettingsPopoverPro
/>
</EuiFormRow>
)}
{localExtent &&
setLocalExtent &&
(axis !== 'x' ? (
<MetricAxisBoundsControl
extent={localExtent}
setExtent={setLocalExtent}
dataBounds={dataBounds}
hasBarOrAreaOnAxis={hasBarOrAreaOnAxis}
hasPercentageAxis={hasPercentageAxis}
testSubjPrefix="lnsXY"
/>
) : (
<BucketAxisBoundsControl
extent={localExtent}
setExtent={setLocalExtent}
dataBounds={dataBounds}
testSubjPrefix="lnsXY"
/>
))}
{localExtent && setLocalExtent && (
<AxisBoundsControl
type={axis !== 'x' ? 'metric' : 'bucket'}
extent={localExtent}
setExtent={setLocalExtent}
dataBounds={dataBounds}
shouldIncludeZero={hasBarOrAreaOnAxis}
disableCustomRange={hasPercentageAxis}
testSubjPrefix="lnsXY"
// X axis is passing the extent object only in case of numeric histogram
canHaveNiceValues={axis !== 'x' || Boolean(extent)}
/>
)}
</ToolbarPopover>
);
};
function MetricAxisBoundsControl({
extent,
setExtent,
dataBounds,
hasBarOrAreaOnAxis,
hasPercentageAxis,
testSubjPrefix,
}: Required<Pick<AxisSettingsPopoverProps, 'extent' | 'setExtent'>> & {
dataBounds: AxisSettingsPopoverProps['dataBounds'];
hasBarOrAreaOnAxis: boolean;
hasPercentageAxis: boolean;
testSubjPrefix: string;
}) {
const { inclusiveZeroError, boundaryError } = validateExtent(hasBarOrAreaOnAxis, extent);
return (
<>
<EuiFormRow
display="columnCompressed"
fullWidth
label={i18n.translate('xpack.lens.xyChart.axisExtent.label', {
defaultMessage: 'Bounds',
})}
helpText={
hasBarOrAreaOnAxis
? i18n.translate('xpack.lens.xyChart.axisExtent.disabledDataBoundsMessage', {
defaultMessage: 'Only line charts can be fit to the data bounds',
})
: undefined
}
>
<EuiButtonGroup
isFullWidth
legend={i18n.translate('xpack.lens.xyChart.axisExtent.label', {
defaultMessage: 'Bounds',
})}
data-test-subj={`${testSubjPrefix}_axisBounds_groups`}
name="axisBounds"
buttonSize="compressed"
options={[
{
id: `${idPrefix}full`,
label: i18n.translate('xpack.lens.xyChart.axisExtent.full', {
defaultMessage: 'Full',
}),
'data-test-subj': `${testSubjPrefix}_axisExtent_groups_full'`,
},
{
id: `${idPrefix}dataBounds`,
label: i18n.translate('xpack.lens.xyChart.axisExtent.dataBounds', {
defaultMessage: 'Data',
}),
'data-test-subj': `${testSubjPrefix}_axisExtent_groups_DataBounds'`,
isDisabled: hasBarOrAreaOnAxis,
},
{
id: `${idPrefix}custom`,
label: i18n.translate('xpack.lens.xyChart.axisExtent.custom', {
defaultMessage: 'Custom',
}),
'data-test-subj': `${testSubjPrefix}_axisExtent_groups_custom'`,
isDisabled: hasPercentageAxis,
},
]}
idSelected={`${idPrefix}${
(hasBarOrAreaOnAxis && extent.mode === 'dataBounds') || hasPercentageAxis
? 'full'
: extent.mode
}`}
onChange={(id) => {
const newMode = id.replace(idPrefix, '') as AxisExtentConfig['mode'];
setExtent({
...extent,
mode: newMode,
lowerBound:
newMode === 'custom' && dataBounds ? Math.min(0, dataBounds.min) : undefined,
upperBound: newMode === 'custom' && dataBounds ? dataBounds.max : undefined,
});
}}
/>
</EuiFormRow>
{extent?.mode === 'custom' && !hasPercentageAxis && (
<RangeInputField
isInvalid={inclusiveZeroError || boundaryError}
label={' '}
helpText={
hasBarOrAreaOnAxis && (!inclusiveZeroError || boundaryError)
? i18n.translate('xpack.lens.xyChart.inclusiveZero', {
defaultMessage: 'Bounds must include zero.',
})
: undefined
}
error={
boundaryError
? i18n.translate('xpack.lens.xyChart.boundaryError', {
defaultMessage: 'Lower bound has to be larger than upper bound',
})
: hasBarOrAreaOnAxis && inclusiveZeroError
? i18n.translate('xpack.lens.xyChart.inclusiveZero', {
defaultMessage: 'Bounds must include zero.',
})
: undefined
}
testSubjLayout={`${testSubjPrefix}_axisExtent_customBounds`}
testSubjLower={`${testSubjPrefix}_axisExtent_lowerBound`}
testSubjUpper={`${testSubjPrefix}_axisExtent_upperBound`}
lowerValue={extent.lowerBound ?? ''}
onLowerValueChange={(e) => {
const val = Number(e.target.value);
const isEmptyValue = e.target.value === '' || Number.isNaN(Number(val));
setExtent({
...extent,
lowerBound: isEmptyValue ? undefined : val,
});
}}
onLowerValueBlur={() => {
if (extent.lowerBound === undefined && dataBounds) {
setExtent({
...extent,
lowerBound: Math.min(0, dataBounds.min),
});
}
}}
upperValue={extent.upperBound ?? ''}
onUpperValueChange={(e) => {
const val = Number(e.target.value);
const isEmptyValue = e.target.value === '' || Number.isNaN(Number(val));
setExtent({
...extent,
upperBound: isEmptyValue ? undefined : val,
});
}}
onUpperValueBlur={() => {
if (extent.upperBound === undefined && dataBounds) {
setExtent({
...extent,
upperBound: dataBounds.max,
});
}
}}
/>
)}
</>
);
}

View file

@ -11,6 +11,7 @@ import { Position, ScaleType } from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { AxisExtentConfig } from '@kbn/expression-xy-plugin/common';
import { LegendSize } from '@kbn/visualizations-plugin/public';
import type { LegendSettingsPopoverProps } from '../../../shared_components/legend/legend_settings_popover';
import type { VisualizationToolbarProps, FramePublicAPI } from '../../../types';
import { State, XYState, AxesSettingsConfig } from '../types';
import { isHorizontalChart } from '../state_helpers';
@ -22,7 +23,6 @@ import { getScaleType } from '../to_expression';
import { TooltipWrapper } from '../../../shared_components';
import { getDefaultVisualValuesForLayer } from '../../../shared_components/datasource_default_values';
import { getDataLayers } from '../visualization_helpers';
import { LegendSettingsPopoverProps } from '../../../shared_components/legend_settings_popover';
type UnwrapArray<T> = T extends Array<infer P> ? P : T;
type AxesSettingsConfigKeys = keyof AxesSettingsConfig;

View file

@ -18241,7 +18241,6 @@
"xpack.lens.axisExtent.label": "Limites",
"xpack.lens.badge.readOnly.text": "Lecture seule",
"xpack.lens.badge.readOnly.tooltip": "Impossible d'enregistrer les visualisations dans la bibliothèque",
"xpack.lens.boundaryError": "La limite inférieure doit être plus grande que la limite supérieure",
"xpack.lens.breadcrumbsByValue": "Modifier la visualisation",
"xpack.lens.breadcrumbsCreate": "Créer",
"xpack.lens.breadcrumbsTitle": "Bibliothèque Visualize",
@ -18995,11 +18994,6 @@
"xpack.lens.xyChart.annotations.keepGlobalFiltersLabel": "Conserver les filtres globaux",
"xpack.lens.xyChart.appearance": "Apparence",
"xpack.lens.xyChart.applyAsRange": "Appliquer en tant que plage",
"xpack.lens.xyChart.axisExtent.custom": "Personnalisé",
"xpack.lens.xyChart.axisExtent.dataBounds": "Données",
"xpack.lens.xyChart.axisExtent.disabledDataBoundsMessage": "Seuls les graphiques linéaires peuvent être adaptés aux limites de données",
"xpack.lens.xyChart.axisExtent.full": "Plein",
"xpack.lens.xyChart.axisExtent.label": "Limites",
"xpack.lens.xyChart.axisOrientation.angled": "En angle",
"xpack.lens.xyChart.axisOrientation.horizontal": "Horizontal",
"xpack.lens.xyChart.axisOrientation.label": "Orientation",
@ -19012,7 +19006,6 @@
"xpack.lens.xyChart.axisSide.top": "Haut",
"xpack.lens.xyChart.bottomAxisDisabledHelpText": "Ce paramètre s'applique uniquement lorsque l'axe du bas est activé.",
"xpack.lens.xyChart.bottomAxisLabel": "Axe du bas",
"xpack.lens.xyChart.boundaryError": "La limite inférieure doit être plus grande que la limite supérieure",
"xpack.lens.xyChart.curveStyleLabel": "Courbes",
"xpack.lens.xyChart.defaultAnnotationLabel": "Événement",
"xpack.lens.xyChart.defaultRangeAnnotationLabel": "Plage d'événements",
@ -19047,7 +19040,6 @@
"xpack.lens.xyChart.iconSelect.starLabel": "Étoile",
"xpack.lens.xyChart.iconSelect.tagIconLabel": "Balise",
"xpack.lens.xyChart.iconSelect.triangleIconLabel": "Triangle",
"xpack.lens.xyChart.inclusiveZero": "Les limites doivent inclure zéro.",
"xpack.lens.xyChart.layerAnnotation": "Annotation",
"xpack.lens.xyChart.layerAnnotationsLabel": "Annotations",
"xpack.lens.xyChart.layerReferenceLine": "Ligne de référence",

View file

@ -18223,7 +18223,6 @@
"xpack.lens.axisExtent.label": "境界",
"xpack.lens.badge.readOnly.text": "読み取り専用",
"xpack.lens.badge.readOnly.tooltip": "ビジュアライゼーションをライブラリに保存できません",
"xpack.lens.boundaryError": "下界は上界よりも大きくなければなりません",
"xpack.lens.breadcrumbsByValue": "ビジュアライゼーションを編集",
"xpack.lens.breadcrumbsCreate": "作成",
"xpack.lens.breadcrumbsTitle": "Visualizeライブラリ",
@ -18977,11 +18976,6 @@
"xpack.lens.xyChart.annotations.keepGlobalFiltersLabel": "グローバルフィルターを保持",
"xpack.lens.xyChart.appearance": "見た目",
"xpack.lens.xyChart.applyAsRange": "範囲として適用",
"xpack.lens.xyChart.axisExtent.custom": "カスタム",
"xpack.lens.xyChart.axisExtent.dataBounds": "データ",
"xpack.lens.xyChart.axisExtent.disabledDataBoundsMessage": "折れ線グラフのみをデータ境界に合わせることができます",
"xpack.lens.xyChart.axisExtent.full": "完全",
"xpack.lens.xyChart.axisExtent.label": "境界",
"xpack.lens.xyChart.axisOrientation.angled": "傾斜",
"xpack.lens.xyChart.axisOrientation.horizontal": "横",
"xpack.lens.xyChart.axisOrientation.label": "向き",
@ -18994,7 +18988,6 @@
"xpack.lens.xyChart.axisSide.top": "トップ",
"xpack.lens.xyChart.bottomAxisDisabledHelpText": "この設定は、下の軸が有効であるときにのみ適用されます。",
"xpack.lens.xyChart.bottomAxisLabel": "下の軸",
"xpack.lens.xyChart.boundaryError": "下界は上界よりも大きくなければなりません",
"xpack.lens.xyChart.curveStyleLabel": "曲線",
"xpack.lens.xyChart.defaultAnnotationLabel": "イベント",
"xpack.lens.xyChart.defaultRangeAnnotationLabel": "イベント範囲",
@ -19029,7 +19022,6 @@
"xpack.lens.xyChart.iconSelect.starLabel": "星",
"xpack.lens.xyChart.iconSelect.tagIconLabel": "タグ",
"xpack.lens.xyChart.iconSelect.triangleIconLabel": "三角形",
"xpack.lens.xyChart.inclusiveZero": "境界にはゼロを含める必要があります。",
"xpack.lens.xyChart.layerAnnotation": "注釈",
"xpack.lens.xyChart.layerAnnotationsLabel": "注釈",
"xpack.lens.xyChart.layerReferenceLine": "基準線",

View file

@ -18248,7 +18248,6 @@
"xpack.lens.axisExtent.label": "边界",
"xpack.lens.badge.readOnly.text": "只读",
"xpack.lens.badge.readOnly.tooltip": "无法将可视化保存到库",
"xpack.lens.boundaryError": "下边界必须大于上边界",
"xpack.lens.breadcrumbsByValue": "编辑可视化",
"xpack.lens.breadcrumbsCreate": "创建",
"xpack.lens.breadcrumbsTitle": "Visualize 库",
@ -19002,11 +19001,6 @@
"xpack.lens.xyChart.annotations.keepGlobalFiltersLabel": "保留全局筛选",
"xpack.lens.xyChart.appearance": "外观",
"xpack.lens.xyChart.applyAsRange": "应用为范围",
"xpack.lens.xyChart.axisExtent.custom": "定制",
"xpack.lens.xyChart.axisExtent.dataBounds": "数据",
"xpack.lens.xyChart.axisExtent.disabledDataBoundsMessage": "仅折线图可适应数据边界",
"xpack.lens.xyChart.axisExtent.full": "实线",
"xpack.lens.xyChart.axisExtent.label": "边界",
"xpack.lens.xyChart.axisOrientation.angled": "带角度",
"xpack.lens.xyChart.axisOrientation.horizontal": "水平",
"xpack.lens.xyChart.axisOrientation.label": "方向",
@ -19019,7 +19013,6 @@
"xpack.lens.xyChart.axisSide.top": "顶部",
"xpack.lens.xyChart.bottomAxisDisabledHelpText": "此设置仅在启用底轴时应用。",
"xpack.lens.xyChart.bottomAxisLabel": "底轴",
"xpack.lens.xyChart.boundaryError": "下边界必须大于上边界",
"xpack.lens.xyChart.curveStyleLabel": "曲线",
"xpack.lens.xyChart.defaultAnnotationLabel": "事件",
"xpack.lens.xyChart.defaultRangeAnnotationLabel": "事件范围",
@ -19054,7 +19047,6 @@
"xpack.lens.xyChart.iconSelect.starLabel": "五角星",
"xpack.lens.xyChart.iconSelect.tagIconLabel": "标签",
"xpack.lens.xyChart.iconSelect.triangleIconLabel": "三角形",
"xpack.lens.xyChart.inclusiveZero": "边界必须包括零。",
"xpack.lens.xyChart.layerAnnotation": "标注",
"xpack.lens.xyChart.layerAnnotationsLabel": "标注",
"xpack.lens.xyChart.layerReferenceLine": "参考线",