diff --git a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts index 1badfae02f..419a583840 100644 --- a/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts +++ b/packages/charts/src/chart_types/xy_chart/state/selectors/compute_series_geometries.ts @@ -11,6 +11,7 @@ import { computeSeriesDomainsSelector } from './compute_series_domains'; import { getSeriesColorsSelector } from './get_series_color_map'; import { getSeriesSpecsSelector, getAxisSpecsSelector } from './get_specs'; import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled'; +import { getVisibleTickSetsSelector } from './visible_ticks'; import { createCustomCachedSelector } from '../../../../state/create_selector'; import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales'; import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme'; @@ -28,10 +29,11 @@ export const computeSeriesGeometriesSelector = createCustomCachedSelector( getSettingsSpecSelector, getAxisSpecsSelector, computeSmallMultipleScalesSelector, + getVisibleTickSetsSelector, isHistogramModeEnabledSelector, getFallBackTickFormatter, ], - (specs, domain, colors, theme, settings, axis, smScales, isHistogram, fallbackFormatter) => { + (specs, domain, colors, theme, settings, axis, smScales, visibleTicksSet, isHistogram, fallbackFormatter) => { return withTextMeasure((measureText) => computeSeriesGeometries( specs, @@ -41,6 +43,7 @@ export const computeSeriesGeometriesSelector = createCustomCachedSelector( settings, axis, smScales, + visibleTicksSet, isHistogram, fallbackFormatter, measureText, diff --git a/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts b/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts index 31745ece0f..ab6c542a5f 100644 --- a/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts +++ b/packages/charts/src/chart_types/xy_chart/state/utils/utils.ts @@ -21,7 +21,7 @@ import { isFiniteNumber, isNil, isUniqueArray, mergePartial } from '../../../../ import { CurveType } from '../../../../utils/curves'; import type { Dimensions, Size } from '../../../../utils/dimensions'; import type { AreaGeometry, BarGeometry, BubbleGeometry, LineGeometry, PerPanel } from '../../../../utils/geometry'; -import type { GroupId, SpecId } from '../../../../utils/ids'; +import type { AxisId, GroupId, SpecId } from '../../../../utils/ids'; import type { SeriesCompareFn } from '../../../../utils/series_sort'; import type { ColorConfig, Theme } from '../../../../utils/themes/theme'; import type { XDomain } from '../../domains/types'; @@ -32,6 +32,7 @@ import { renderBars } from '../../rendering/bars'; import { renderBubble } from '../../rendering/bubble'; import { renderLine } from '../../rendering/line'; import { getAreaSeriesStyles, getLineSeriesStyles } from '../../rendering/line_area_style'; +import { isXDomain } from '../../utils/axis_utils'; import { defaultXYSeriesSort } from '../../utils/default_series_sort_fn'; import { fillSeries } from '../../utils/fill_series'; import { groupBy } from '../../utils/group_data_series'; @@ -51,6 +52,7 @@ import { isLineSeriesSpec, } from '../../utils/specs'; import type { ScaleConfigs } from '../selectors/get_api_scale_configs'; +import type { Projection } from '../selectors/visible_ticks'; /** * Return map association between `seriesKey` and only the custom colors string @@ -175,10 +177,24 @@ export function computeSeriesGeometries( { rotation: chartRotation }: Pick, axesSpecs: AxisSpec[], smallMultiplesScales: SmallMultipleScales, + visibleTicksSet: Map, enableHistogramMode: boolean, fallbackTickFormatter: TickFormatter, measureText: TextMeasure, ): ComputedGeometries { + const adaptedTickCountMap = axesSpecs.reduce<{ x?: number; y: Map }>( + (acc, axis) => { + const ticks = (visibleTicksSet.get(axis.id)?.ticks.length ?? NaN) - 1; + if (isNaN(ticks)) return acc; + if (isXDomain(axis.position, chartRotation)) { + acc.x = ticks; + } else { + acc.y.set(axis.groupId, ticks); + } + return acc; + }, + { y: new Map() }, + ); const chartColors: ColorConfig = chartTheme.colors; const formattedDataSeries = nonFilteredDataSeries.filter(({ isFiltered }) => !isFiltered); const barDataSeries = formattedDataSeries.filter(({ spec }) => isBarSeriesSpec(spec)); @@ -197,15 +213,22 @@ export function computeSeriesGeometries( }, {}); const { horizontal, vertical } = smallMultiplesScales; - + const adaptedXDomain = { + ...xDomain, + desiredTickCount: adaptedTickCountMap.x ?? xDomain.desiredTickCount, + }; + const adaptedYDomains = yDomains.map((d) => ({ + ...d, + desiredTickCount: adaptedTickCountMap.y.get(d.groupId) ?? d.desiredTickCount, + })); const yScales = computeYScales({ - yDomains, + yDomains: adaptedYDomains, range: [isHorizontalRotation(chartRotation) ? vertical.bandwidth : horizontal.bandwidth, 0], }); const computedGeoms = renderGeometries( formattedDataSeries, - xDomain, + adaptedXDomain, yScales, vertical, horizontal, @@ -224,7 +247,7 @@ export function computeSeriesGeometries( const totalBarsInCluster = Object.values(barIndexByPanel).reduce((acc, curr) => Math.max(acc, curr.length), 0); const xScale = computeXScale({ - xDomain, + xDomain: adaptedXDomain, totalBarsInCluster, range: [0, isHorizontalRotation(chartRotation) ? horizontal.bandwidth : vertical.bandwidth], barsPadding: enableHistogramMode ? chartTheme.scales.histogramPadding : chartTheme.scales.barsPadding, diff --git a/storybook/stories/test_cases/15_linear_nicing.story.tsx b/storybook/stories/test_cases/15_linear_nicing.story.tsx new file mode 100644 index 0000000000..77d21ff8bf --- /dev/null +++ b/storybook/stories/test_cases/15_linear_nicing.story.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { boolean } from '@storybook/addon-knobs'; +import React from 'react'; + +import { Axis, Chart, LineSeries, Position, ScaleType, Settings } from '@elastic/charts'; +import { getRandomNumberGenerator } from '@elastic/charts/src/mocks/utils'; + +import type { ChartsStory } from '../../types'; +import { useBaseTheme } from '../../use_base_theme'; + +const rng = getRandomNumberGenerator(); + +// This data is carefully picked to trigger adaptive tick placements +// See https://github.com/elastic/elastic-charts/issues/2687 +const data = new Array(20).fill(1).map((_, i) => ({ + x: i === 0 ? -1e6 : (i - 1) * 13e6, + y: (i === 0 ? 0 : i === 2 ? -5.2 : i === 12 ? 21 : rng(-4, 20)) * 1e5, +})); + +export const Example: ChartsStory = (_, { title, description }) => { + const xNice = boolean('Nice x ticks', true); + const yNice = boolean('Nice y ticks', true); + + return ( + + + + + + + ); +}; + +Example.parameters = { + resize: true, +}; diff --git a/storybook/stories/test_cases/test_cases.stories.tsx b/storybook/stories/test_cases/test_cases.stories.tsx index f0384d68cd..2da5430f2a 100644 --- a/storybook/stories/test_cases/test_cases.stories.tsx +++ b/storybook/stories/test_cases/test_cases.stories.tsx @@ -26,4 +26,5 @@ export { Example as startDayOfWeek } from './11_start_day_of_week.story'; export { Example as logWithNegativeValues } from './12_log_with_negative_values.story'; export { Example as pointStyleOverrides } from './13_point_style_overrides.story'; export { Example as errorBoundary } from './14_error_boundary.story'; +export { Example as linearNicing } from './15_linear_nicing.story'; export { Example as lensStressTest } from './33_lens_stress.story';