mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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—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—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:
parent
2b276a9dca
commit
83dd5f84fc
33 changed files with 598 additions and 363 deletions
|
@ -53,7 +53,7 @@ pageLoadAssetSize:
|
|||
expressions: 140958
|
||||
expressionShape: 34008
|
||||
expressionTagcloud: 27505
|
||||
expressionXY: 38500
|
||||
expressionXY: 39500
|
||||
features: 21723
|
||||
fieldFormats: 65209
|
||||
files: 22673
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -72,6 +72,7 @@ export interface AxisExtentConfig {
|
|||
lowerBound?: number;
|
||||
upperBound?: number;
|
||||
enforce?: boolean;
|
||||
niceValues?: boolean;
|
||||
}
|
||||
|
||||
export interface AxisConfig {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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,
|
|
@ -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),
|
||||
};
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { BucketAxisBoundsControl } from './axis_extent_settings';
|
||||
export { AxisBoundsControl } from './axis_extent_settings';
|
||||
export {
|
||||
validateAxisDomain,
|
||||
validateZeroInclusivityExtent,
|
|
@ -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],
|
||||
},
|
||||
},
|
||||
],
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
|
|
@ -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 {
|
||||
/**
|
|
@ -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,
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "基準線",
|
||||
|
|
|
@ -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": "参考线",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue