From bd5d097ce967be013005ce68a2184cb7cb9e23b9 Mon Sep 17 00:00:00 2001 From: Beatriz Malveiro Date: Tue, 5 May 2026 17:17:19 +0100 Subject: [PATCH 1/8] feat: wire axis tick truncation to elastic-charts in --- .../expression_xy/common/i18n/index.tsx | 2 +- .../public/components/xy_chart.tsx | 25 ++++++++----------- .../public/visualizations/xy/to_expression.ts | 12 ++++++++- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/platform/plugins/shared/chart_expressions/expression_xy/common/i18n/index.tsx b/src/platform/plugins/shared/chart_expressions/expression_xy/common/i18n/index.tsx index e1f796729c9f5..545c2cd60e637 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_xy/common/i18n/index.tsx +++ b/src/platform/plugins/shared/chart_expressions/expression_xy/common/i18n/index.tsx @@ -332,7 +332,7 @@ export const strings = { }), getAxisTruncateHelp: () => i18n.translate('expressionXY.axisConfig.truncate.help', { - defaultMessage: 'The number of symbols before truncating', + defaultMessage: 'Maximum tick label width in pixels before truncating', }), getReferenceLineNameHelp: () => i18n.translate('expressionXY.referenceLine.name.help', { diff --git a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx index a44bd4bb793c2..f2f141aba0af9 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -20,6 +20,7 @@ import type { XYChartElementEvent, XYChartSeriesIdentifier, SettingsProps, + Truncate, } from '@elastic/charts'; import { Chart, @@ -484,10 +485,14 @@ export function XYChart({ ? getLinesCausedPaddings(visualConfigs, yAxesMap, shouldRotate) : {}; + const getTickLabelTruncationStyle = ( + width: Truncate['width'] | undefined, + position: Truncate['position'] = 'end' + ): Truncate | undefined => (width !== undefined ? { width, position } : undefined); + const getYAxesStyle = (axis: AxisConfiguration) => { const tickVisible = axis.showLabels; const position = getOriginalAxisPosition(axis.position, shouldRotate); - const style = { tickLabel: { fill: axis.labelColor, @@ -499,6 +504,7 @@ export function XYChart({ inner: linesPaddings[position], } : undefined, + truncation: getTickLabelTruncationStyle(axis.truncate), }, axisTitle: { visible: axis.showTitle, @@ -721,6 +727,7 @@ export function XYChart({ rotation: xAxisConfig?.labelsOrientation, padding: linesPaddings.bottom != null ? { inner: linesPaddings.bottom } : undefined, fill: xAxisConfig?.labelColor, + truncation: getTickLabelTruncationStyle(xAxisConfig?.truncate, 'middle'), }, axisTitle: { visible: xAxisConfig?.showTitle, @@ -947,13 +954,7 @@ export function XYChart({ title={xTitle} gridLine={gridLineStyle} hide={xAxisConfig?.hide || dataLayers[0]?.simpleView || !dataLayers[0]?.xAccessor} - tickFormat={(d) => { - let value = safeXAccessorLabelRenderer(d) || ''; - if (xAxisConfig?.truncate && value.length > xAxisConfig.truncate) { - value = `${value.slice(0, xAxisConfig.truncate)}...`; - } - return value; - }} + tickFormat={(d) => safeXAccessorLabelRenderer(d) || ''} maximumFractionDigits={xTickDecimals} style={xAxisStyle} showOverlappingLabels={xAxisConfig?.showOverlappingLabels} @@ -983,13 +984,7 @@ export function XYChart({ visible: axis.showGridLines, }} hide={axis.hide || dataLayers[0]?.simpleView} - tickFormat={(d) => { - let value = axis.formatter?.convert(d) || ''; - if (axis.truncate && value.length > axis.truncate) { - value = `${value.slice(0, axis.truncate)}...`; - } - return value; - }} + tickFormat={(d) => axis.formatter?.convert(d) || ''} maximumFractionDigits={tickDecimals} style={getYAxesStyle(axis)} domain={getYAxisDomain(axis)} diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/xy/to_expression.ts b/x-pack/platform/plugins/shared/lens/public/visualizations/xy/to_expression.ts index 6b75323ece46d..56c5038becb22 100644 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/xy/to_expression.ts +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/xy/to_expression.ts @@ -51,7 +51,7 @@ import type { ValidXYDataLayerConfig, XYLayerConfig, } from './types'; -import { getColumnToLabelMap } from './state_helpers'; +import { getColumnToLabelMap, hasBarSeries, isHorizontalChart } from './state_helpers'; import { getDefaultPalette } from './default_palette'; import { defaultReferenceLineColor } from './color_assignment'; import { getDefaultVisualValuesForLayer } from '../../shared_components/datasource_default_values'; @@ -75,6 +75,11 @@ type XYLayerConfigWithSimpleView = XYLayerConfig & { simpleView?: boolean }; type XYAnnotationLayerConfigWithSimpleView = XYAnnotationLayerConfig & { simpleView?: boolean }; type State = Omit & { layers: XYLayerConfigWithSimpleView[] }; +/** + * Pixel width for X-axis (dimension) tick label truncation on horizontal bar charts + */ +export const DEFAULT_HORIZONTAL_BAR_X_AXIS_TRUNCATE_PX = 25; // just for testing, should be higher + export const getSortedAccessors = ( datasource: DatasourcePublicAPI | undefined, layer: XYDataLayerConfig | XYReferenceLineLayerConfig @@ -336,6 +341,10 @@ export const buildXYExpression = ( ) ? [axisExtentConfigToExpression(state.xExtent ?? { mode: 'dataBounds', niceValues: true })] : undefined, + truncate: + isHorizontalChart(state.layers) && hasBarSeries(state.layers) + ? DEFAULT_HORIZONTAL_BAR_X_AXIS_TRUNCATE_PX + : undefined, }); const layeredXyVisFn = buildExpressionFunction('layeredXyVis', { @@ -415,6 +424,7 @@ const yAxisConfigsToExpression = (yAxisConfigs: AxisConfig[]): Ast[] => { showGridLines: axis.showGridLines ?? true, labelsOrientation: axis.labelsOrientation, scaleType: axis.scaleType, + truncate: axis.truncate, }), ]).toAst() ); From 029ea563e093a0678a691607f42e3bd113c65c33 Mon Sep 17 00:00:00 2001 From: Beatriz Malveiro Date: Wed, 6 May 2026 18:11:22 +0100 Subject: [PATCH 2/8] feat: support relative truncation --- .../public/components/xy_chart.tsx | 157 +++++++++++------- .../public/visualizations/xy/to_expression.ts | 11 +- 2 files changed, 101 insertions(+), 67 deletions(-) diff --git a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx index f2f141aba0af9..7ec92b4f40caa 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { css } from '@emotion/react'; import type { ElementClickListener, @@ -16,7 +16,6 @@ import type { LegendPositionConfig, DisplayValueStyle, RecursivePartial, - AxisStyle, XYChartElementEvent, XYChartSeriesIdentifier, SettingsProps, @@ -311,6 +310,27 @@ export function XYChart({ const dataLayers: CommonXYDataLayerConfig[] = filteredLayers.filter(isDataLayer); + const chartContainerRef = useRef(null); + const [chartContainerWidth, setChartContainerWidth] = useState(0); + + useLayoutEffect(() => { + const element = chartContainerRef.current; + if (!element) { + return undefined; + } + const observer = new ResizeObserver((entries) => { + const width = entries[0]?.contentRect.width; + if (typeof width === 'number') { + setChartContainerWidth(width); + } + }); + observer.observe(element); + setChartContainerWidth(element.getBoundingClientRect().width); + return () => { + observer.disconnect(); + }; + }, [dataLayers.length]); + const isTimeViz = isTimeChart(dataLayers); useEffect(() => { @@ -485,41 +505,6 @@ export function XYChart({ ? getLinesCausedPaddings(visualConfigs, yAxesMap, shouldRotate) : {}; - const getTickLabelTruncationStyle = ( - width: Truncate['width'] | undefined, - position: Truncate['position'] = 'end' - ): Truncate | undefined => (width !== undefined ? { width, position } : undefined); - - const getYAxesStyle = (axis: AxisConfiguration) => { - const tickVisible = axis.showLabels; - const position = getOriginalAxisPosition(axis.position, shouldRotate); - const style = { - tickLabel: { - fill: axis.labelColor, - visible: tickVisible, - rotation: axis.labelsOrientation, - padding: - linesPaddings[position] != null - ? { - inner: linesPaddings[position], - } - : undefined, - truncation: getTickLabelTruncationStyle(axis.truncate), - }, - axisTitle: { - visible: axis.showTitle, - // if labels are not visible add the padding to the title - padding: - !tickVisible && linesPaddings[position] != null - ? { - inner: linesPaddings[position], - } - : undefined, - }, - }; - return style; - }; - const getYAxisDomain = (axis: GroupsConfiguration[number]) => { const extent: AxisExtentConfigResult = axis.extent || { type: 'axisExtentConfig', @@ -708,11 +693,65 @@ export function XYChart({ visible: xAxisConfig?.showGridLines, strokeWidth: 1, }; - const xAxisStyle: RecursivePartial = isHorizontalTimeAxis - ? { + + const getYAxesStyle = (axis: AxisConfiguration) => { + const tickVisible = axis.showLabels; + const position = getOriginalAxisPosition(axis.position, shouldRotate); + const style = { + tickLabel: { + fill: axis.labelColor, + visible: tickVisible, + rotation: axis.labelsOrientation, + padding: + linesPaddings[position] != null + ? { + inner: linesPaddings[position], + } + : undefined, + truncation: axis.truncate ? { width: axis.truncate, position: 'end' as const } : undefined, + }, + axisTitle: { + visible: axis.showTitle, + // if labels are not visible add the padding to the title + padding: + !tickVisible && linesPaddings[position] != null + ? { + inner: linesPaddings[position], + } + : undefined, + }, + }; + return style; + }; + + const getXAxisStyle = () => { + const DEFAULT_HORIZONTAL_BAR_X_AXIS_TICK_TRUNCATE_MIN_PX = 200; + const DEFAULT_HORIZONTAL_BAR_X_AXIS_TICK_TRUNCATE_WIDTH_RELATIVE = 0.3; + + let truncateStyle: Truncate | undefined = + xAxisConfig?.truncate !== undefined + ? { width: xAxisConfig.truncate, position: 'end' } + : undefined; + + if (truncateStyle === undefined && isHorizontalChart(dataLayers) && hasBars) { + const chartWidth = + Number.isFinite(chartContainerWidth) && chartContainerWidth > 0 ? chartContainerWidth : 0; + + const value = Math.floor( + Math.max( + DEFAULT_HORIZONTAL_BAR_X_AXIS_TICK_TRUNCATE_MIN_PX, + DEFAULT_HORIZONTAL_BAR_X_AXIS_TICK_TRUNCATE_WIDTH_RELATIVE * chartWidth + ) + ); + truncateStyle = { width: value, position: 'middle' as const }; + } + + if (isHorizontalTimeAxis) { + return { tickLabel: { visible: Boolean(xAxisConfig?.showLabels), fill: xAxisConfig?.labelColor, + truncation: truncateStyle, }, tickLine: { visible: Boolean(xAxisConfig?.showLabels), @@ -720,23 +759,27 @@ export function XYChart({ axisTitle: { visible: xAxisConfig?.showTitle, }, - } - : { - tickLabel: { - visible: xAxisConfig?.showLabels, - rotation: xAxisConfig?.labelsOrientation, - padding: linesPaddings.bottom != null ? { inner: linesPaddings.bottom } : undefined, - fill: xAxisConfig?.labelColor, - truncation: getTickLabelTruncationStyle(xAxisConfig?.truncate, 'middle'), - }, - axisTitle: { - visible: xAxisConfig?.showTitle, - padding: - !xAxisConfig?.showLabels && linesPaddings.bottom != null - ? { inner: linesPaddings.bottom } - : undefined, - }, }; + } + + return { + tickLabel: { + visible: xAxisConfig?.showLabels, + rotation: xAxisConfig?.labelsOrientation, + padding: linesPaddings.bottom != null ? { inner: linesPaddings.bottom } : undefined, + fill: xAxisConfig?.labelColor, + truncation: truncateStyle, + }, + axisTitle: { + visible: xAxisConfig?.showTitle, + padding: + !xAxisConfig?.showLabels && linesPaddings.bottom != null + ? { inner: linesPaddings.bottom } + : undefined, + }, + }; + }; + const isSplitChart = splitColumnAccessor || splitRowAccessor; const splitTable = isSplitChart ? dataLayers[0].table : undefined; const splitColumnId = @@ -774,7 +817,7 @@ export function XYChart({ return ( <> -
+
{showLegend !== undefined && uiState && ( safeXAccessorLabelRenderer(d) || ''} maximumFractionDigits={xTickDecimals} - style={xAxisStyle} + style={getXAxisStyle()} showOverlappingLabels={xAxisConfig?.showOverlappingLabels} showDuplicatedTicks={xAxisConfig?.showDuplicates} {...getOverridesFor(overrides, 'axisX')} diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/xy/to_expression.ts b/x-pack/platform/plugins/shared/lens/public/visualizations/xy/to_expression.ts index 56c5038becb22..d9f4c2f38d85f 100644 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/xy/to_expression.ts +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/xy/to_expression.ts @@ -51,7 +51,7 @@ import type { ValidXYDataLayerConfig, XYLayerConfig, } from './types'; -import { getColumnToLabelMap, hasBarSeries, isHorizontalChart } from './state_helpers'; +import { getColumnToLabelMap } from './state_helpers'; import { getDefaultPalette } from './default_palette'; import { defaultReferenceLineColor } from './color_assignment'; import { getDefaultVisualValuesForLayer } from '../../shared_components/datasource_default_values'; @@ -75,11 +75,6 @@ type XYLayerConfigWithSimpleView = XYLayerConfig & { simpleView?: boolean }; type XYAnnotationLayerConfigWithSimpleView = XYAnnotationLayerConfig & { simpleView?: boolean }; type State = Omit & { layers: XYLayerConfigWithSimpleView[] }; -/** - * Pixel width for X-axis (dimension) tick label truncation on horizontal bar charts - */ -export const DEFAULT_HORIZONTAL_BAR_X_AXIS_TRUNCATE_PX = 25; // just for testing, should be higher - export const getSortedAccessors = ( datasource: DatasourcePublicAPI | undefined, layer: XYDataLayerConfig | XYReferenceLineLayerConfig @@ -341,10 +336,6 @@ export const buildXYExpression = ( ) ? [axisExtentConfigToExpression(state.xExtent ?? { mode: 'dataBounds', niceValues: true })] : undefined, - truncate: - isHorizontalChart(state.layers) && hasBarSeries(state.layers) - ? DEFAULT_HORIZONTAL_BAR_X_AXIS_TRUNCATE_PX - : undefined, }); const layeredXyVisFn = buildExpressionFunction('layeredXyVis', { From 81f105c0067979c0f79e1bd58ecf5a978978b495 Mon Sep 17 00:00:00 2001 From: Beatriz Malveiro Date: Mon, 11 May 2026 17:22:20 +0100 Subject: [PATCH 3/8] feat: use tick label max length with relative size --- .../public/components/xy_chart.tsx | 63 ++++--------------- 1 file changed, 13 insertions(+), 50 deletions(-) diff --git a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx index 7ec92b4f40caa..77ff83ba9ff49 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { css } from '@emotion/react'; import type { ElementClickListener, @@ -19,7 +19,7 @@ import type { XYChartElementEvent, XYChartSeriesIdentifier, SettingsProps, - Truncate, + AxisStyle, } from '@elastic/charts'; import { Chart, @@ -309,28 +309,6 @@ export function XYChart({ ); const dataLayers: CommonXYDataLayerConfig[] = filteredLayers.filter(isDataLayer); - - const chartContainerRef = useRef(null); - const [chartContainerWidth, setChartContainerWidth] = useState(0); - - useLayoutEffect(() => { - const element = chartContainerRef.current; - if (!element) { - return undefined; - } - const observer = new ResizeObserver((entries) => { - const width = entries[0]?.contentRect.width; - if (typeof width === 'number') { - setChartContainerWidth(width); - } - }); - observer.observe(element); - setChartContainerWidth(element.getBoundingClientRect().width); - return () => { - observer.disconnect(); - }; - }, [dataLayers.length]); - const isTimeViz = isTimeChart(dataLayers); useEffect(() => { @@ -694,7 +672,7 @@ export function XYChart({ strokeWidth: 1, }; - const getYAxesStyle = (axis: AxisConfiguration) => { + const getYAxesStyle = (axis: AxisConfiguration): RecursivePartial => { const tickVisible = axis.showLabels; const position = getOriginalAxisPosition(axis.position, shouldRotate); const style = { @@ -708,7 +686,7 @@ export function XYChart({ inner: linesPaddings[position], } : undefined, - truncation: axis.truncate ? { width: axis.truncate, position: 'end' as const } : undefined, + maxLength: axis.truncate, }, axisTitle: { visible: axis.showTitle, @@ -724,34 +702,16 @@ export function XYChart({ return style; }; - const getXAxisStyle = () => { - const DEFAULT_HORIZONTAL_BAR_X_AXIS_TICK_TRUNCATE_MIN_PX = 200; - const DEFAULT_HORIZONTAL_BAR_X_AXIS_TICK_TRUNCATE_WIDTH_RELATIVE = 0.3; - - let truncateStyle: Truncate | undefined = - xAxisConfig?.truncate !== undefined - ? { width: xAxisConfig.truncate, position: 'end' } - : undefined; - - if (truncateStyle === undefined && isHorizontalChart(dataLayers) && hasBars) { - const chartWidth = - Number.isFinite(chartContainerWidth) && chartContainerWidth > 0 ? chartContainerWidth : 0; - - const value = Math.floor( - Math.max( - DEFAULT_HORIZONTAL_BAR_X_AXIS_TICK_TRUNCATE_MIN_PX, - DEFAULT_HORIZONTAL_BAR_X_AXIS_TICK_TRUNCATE_WIDTH_RELATIVE * chartWidth - ) - ); - truncateStyle = { width: value, position: 'middle' as const }; - } + const getXAxisStyle = (): RecursivePartial => { + const isHorizontalBarChart = isHorizontalChart(dataLayers) && hasBars; + const MAX_LENGTH_FOR_HORIZONTAL_BAR_CHART = '40%'; if (isHorizontalTimeAxis) { return { tickLabel: { visible: Boolean(xAxisConfig?.showLabels), fill: xAxisConfig?.labelColor, - truncation: truncateStyle, + maxLength: xAxisConfig?.truncate, }, tickLine: { visible: Boolean(xAxisConfig?.showLabels), @@ -768,7 +728,10 @@ export function XYChart({ rotation: xAxisConfig?.labelsOrientation, padding: linesPaddings.bottom != null ? { inner: linesPaddings.bottom } : undefined, fill: xAxisConfig?.labelColor, - truncation: truncateStyle, + maxLength: + xAxisConfig?.truncate ?? + (isHorizontalBarChart ? MAX_LENGTH_FOR_HORIZONTAL_BAR_CHART : undefined), + truncate: isHorizontalBarChart ? ('middle' as const) : undefined, }, axisTitle: { visible: xAxisConfig?.showTitle, @@ -817,7 +780,7 @@ export function XYChart({ return ( <> -
+
{showLegend !== undefined && uiState && ( Date: Fri, 15 May 2026 15:53:12 +0100 Subject: [PATCH 4/8] remove settings from axis style --- .../expression_xy/public/components/xy_chart.tsx | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx index 77ff83ba9ff49..fa5f6058c88ab 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -419,6 +419,7 @@ export function XYChart({ const isHistogramViz = dataLayers.every((l) => l.isHistogram); const isEsqlMode = dataLayers.some((l) => l.table?.meta?.type === ESQL_TABLE_TYPE); const hasBars = dataLayers.some((l) => l.seriesType === SeriesTypes.BAR); + const isHorizontalBarChart = isHorizontalChart(dataLayers) && hasBars; const { baseDomain: rawXDomain, extendedDomain: xDomain } = getXDomain( data.datatableUtilities, @@ -686,7 +687,6 @@ export function XYChart({ inner: linesPaddings[position], } : undefined, - maxLength: axis.truncate, }, axisTitle: { visible: axis.showTitle, @@ -703,15 +703,11 @@ export function XYChart({ }; const getXAxisStyle = (): RecursivePartial => { - const isHorizontalBarChart = isHorizontalChart(dataLayers) && hasBars; - const MAX_LENGTH_FOR_HORIZONTAL_BAR_CHART = '40%'; - if (isHorizontalTimeAxis) { return { tickLabel: { visible: Boolean(xAxisConfig?.showLabels), fill: xAxisConfig?.labelColor, - maxLength: xAxisConfig?.truncate, }, tickLine: { visible: Boolean(xAxisConfig?.showLabels), @@ -728,10 +724,6 @@ export function XYChart({ rotation: xAxisConfig?.labelsOrientation, padding: linesPaddings.bottom != null ? { inner: linesPaddings.bottom } : undefined, fill: xAxisConfig?.labelColor, - maxLength: - xAxisConfig?.truncate ?? - (isHorizontalBarChart ? MAX_LENGTH_FOR_HORIZONTAL_BAR_CHART : undefined), - truncate: isHorizontalBarChart ? ('middle' as const) : undefined, }, axisTitle: { visible: xAxisConfig?.showTitle, @@ -965,6 +957,10 @@ export function XYChart({ style={getXAxisStyle()} showOverlappingLabels={xAxisConfig?.showOverlappingLabels} showDuplicatedTicks={xAxisConfig?.showDuplicates} + tickLabelMaxLength={ + xAxisConfig?.truncate ?? (isHorizontalBarChart ? '40%' : undefined) + } + tickLabelTruncate={isHorizontalBarChart ? ('middle' as const) : undefined} {...getOverridesFor(overrides, 'axisX')} /> {isSplitChart && splitTable && ( @@ -996,6 +992,7 @@ export function XYChart({ domain={getYAxisDomain(axis)} showOverlappingLabels={axis.showOverlappingLabels} showDuplicatedTicks={axis.showDuplicates} + tickLabelMaxLength={axis.truncate} {...getOverridesFor( overrides, /left/i.test(axis.groupId) ? 'axisLeft' : 'axisRight' From e0867a8a1f0ccd37b52b87fbfc02affcf1b68427 Mon Sep 17 00:00:00 2001 From: Beatriz Malveiro Date: Fri, 22 May 2026 10:42:30 +0100 Subject: [PATCH 5/8] test: update snapshots --- .../public/components/__snapshots__/xy_chart.test.tsx.snap | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap index 1ebe423526833..a6160566b7081 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap +++ b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/__snapshots__/xy_chart.test.tsx.snap @@ -4179,6 +4179,8 @@ exports[`XYChart component it renders horizontal bar 1`] = ` } } tickFormat={[Function]} + tickLabelMaxLength="40%" + tickLabelTruncate="middle" title="c" /> Date: Fri, 22 May 2026 11:04:25 +0100 Subject: [PATCH 6/8] refactor: revert edits in axis styles --- .../public/components/xy_chart.tsx | 101 +++++++++--------- 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx index fa5f6058c88ab..189c96a08d313 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -309,6 +309,7 @@ export function XYChart({ ); const dataLayers: CommonXYDataLayerConfig[] = filteredLayers.filter(isDataLayer); + const isTimeViz = isTimeChart(dataLayers); useEffect(() => { @@ -484,6 +485,36 @@ export function XYChart({ ? getLinesCausedPaddings(visualConfigs, yAxesMap, shouldRotate) : {}; + const getYAxesStyle = (axis: AxisConfiguration): RecursivePartial => { + const tickVisible = axis.showLabels; + const position = getOriginalAxisPosition(axis.position, shouldRotate); + + const style = { + tickLabel: { + fill: axis.labelColor, + visible: tickVisible, + rotation: axis.labelsOrientation, + padding: + linesPaddings[position] != null + ? { + inner: linesPaddings[position], + } + : undefined, + }, + axisTitle: { + visible: axis.showTitle, + // if labels are not visible add the padding to the title + padding: + !tickVisible && linesPaddings[position] != null + ? { + inner: linesPaddings[position], + } + : undefined, + }, + }; + return style; + }; + const getYAxisDomain = (axis: GroupsConfiguration[number]) => { const extent: AxisExtentConfigResult = axis.extent || { type: 'axisExtentConfig', @@ -673,38 +704,8 @@ export function XYChart({ strokeWidth: 1, }; - const getYAxesStyle = (axis: AxisConfiguration): RecursivePartial => { - const tickVisible = axis.showLabels; - const position = getOriginalAxisPosition(axis.position, shouldRotate); - const style = { - tickLabel: { - fill: axis.labelColor, - visible: tickVisible, - rotation: axis.labelsOrientation, - padding: - linesPaddings[position] != null - ? { - inner: linesPaddings[position], - } - : undefined, - }, - axisTitle: { - visible: axis.showTitle, - // if labels are not visible add the padding to the title - padding: - !tickVisible && linesPaddings[position] != null - ? { - inner: linesPaddings[position], - } - : undefined, - }, - }; - return style; - }; - - const getXAxisStyle = (): RecursivePartial => { - if (isHorizontalTimeAxis) { - return { + const xAxisStyle: RecursivePartial = isHorizontalTimeAxis + ? { tickLabel: { visible: Boolean(xAxisConfig?.showLabels), fill: xAxisConfig?.labelColor, @@ -715,26 +716,22 @@ export function XYChart({ axisTitle: { visible: xAxisConfig?.showTitle, }, + } + : { + tickLabel: { + visible: xAxisConfig?.showLabels, + rotation: xAxisConfig?.labelsOrientation, + padding: linesPaddings.bottom != null ? { inner: linesPaddings.bottom } : undefined, + fill: xAxisConfig?.labelColor, + }, + axisTitle: { + visible: xAxisConfig?.showTitle, + padding: + !xAxisConfig?.showLabels && linesPaddings.bottom != null + ? { inner: linesPaddings.bottom } + : undefined, + }, }; - } - - return { - tickLabel: { - visible: xAxisConfig?.showLabels, - rotation: xAxisConfig?.labelsOrientation, - padding: linesPaddings.bottom != null ? { inner: linesPaddings.bottom } : undefined, - fill: xAxisConfig?.labelColor, - }, - axisTitle: { - visible: xAxisConfig?.showTitle, - padding: - !xAxisConfig?.showLabels && linesPaddings.bottom != null - ? { inner: linesPaddings.bottom } - : undefined, - }, - }; - }; - const isSplitChart = splitColumnAccessor || splitRowAccessor; const splitTable = isSplitChart ? dataLayers[0].table : undefined; const splitColumnId = @@ -954,7 +951,7 @@ export function XYChart({ hide={xAxisConfig?.hide || dataLayers[0]?.simpleView || !dataLayers[0]?.xAccessor} tickFormat={(d) => safeXAccessorLabelRenderer(d) || ''} maximumFractionDigits={xTickDecimals} - style={getXAxisStyle()} + style={xAxisStyle} showOverlappingLabels={xAxisConfig?.showOverlappingLabels} showDuplicatedTicks={xAxisConfig?.showDuplicates} tickLabelMaxLength={ From a09996682748cb7da3bad1c2324768099ff62fde Mon Sep 17 00:00:00 2001 From: Beatriz Malveiro Date: Fri, 22 May 2026 16:16:18 +0100 Subject: [PATCH 7/8] fix: convert character truncate limits to pixels --- .../private/vis_types/xy/public/to_ast.ts | 4 ++- .../xy/public/utils/truncate.test.ts | 27 +++++++++++++++++ .../vis_types/xy/public/utils/truncate.ts | 30 +++++++++++++++++++ .../public/visualizations/xy/to_expression.ts | 1 - 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 src/platform/plugins/private/vis_types/xy/public/utils/truncate.test.ts create mode 100644 src/platform/plugins/private/vis_types/xy/public/utils/truncate.ts diff --git a/src/platform/plugins/private/vis_types/xy/public/to_ast.ts b/src/platform/plugins/private/vis_types/xy/public/to_ast.ts index 27401a2e666c8..b57fb0e8295f9 100644 --- a/src/platform/plugins/private/vis_types/xy/public/to_ast.ts +++ b/src/platform/plugins/private/vis_types/xy/public/to_ast.ts @@ -18,6 +18,7 @@ import type { TimeRangeBounds } from '@kbn/data-plugin/common'; import type { PaletteOutput } from '@kbn/charts-plugin/common/expressions/palette/types'; import type { DateHistogramParams, HistogramParams } from '@kbn/chart-expressions-common'; import { LegendSize } from '@kbn/chart-expressions-common'; +import { charsToPixels } from './utils/truncate'; import type { Dimensions, Dimension, @@ -149,7 +150,8 @@ const prepareLayers = ( const getLabelArgs = (data: CategoryAxis, isTimeChart?: boolean) => { return { - truncate: data.labels.truncate, + // axis expressions expect pixels, we approximate pixels from character count + truncate: charsToPixels(data.labels.truncate), labelsOrientation: -(data.labels.rotate ?? (isTimeChart ? 0 : 90)), showOverlappingLabels: data.labels.filter === false, showDuplicates: data.labels.filter === false, diff --git a/src/platform/plugins/private/vis_types/xy/public/utils/truncate.test.ts b/src/platform/plugins/private/vis_types/xy/public/utils/truncate.test.ts new file mode 100644 index 0000000000000..7014475a3634a --- /dev/null +++ b/src/platform/plugins/private/vis_types/xy/public/utils/truncate.test.ts @@ -0,0 +1,27 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { charsToPixels } from './truncate'; + +describe('charsToPixels', () => { + it('returns undefined when truncate is missing or non-positive', () => { + expect(charsToPixels(undefined)).toBeUndefined(); + expect(charsToPixels(null)).toBeUndefined(); + expect(charsToPixels(0)).toBeUndefined(); + expect(charsToPixels(-1)).toBeUndefined(); + }); + + it('converts character counts to an approximate pixel width', () => { + expect(charsToPixels(100)).toBe(660); + }); + + it('uses the provided font size', () => { + expect(charsToPixels(10, 12)).toBe(72); + }); +}); diff --git a/src/platform/plugins/private/vis_types/xy/public/utils/truncate.ts b/src/platform/plugins/private/vis_types/xy/public/utils/truncate.ts new file mode 100644 index 0000000000000..ce94dd16cd8d7 --- /dev/null +++ b/src/platform/plugins/private/vis_types/xy/public/utils/truncate.ts @@ -0,0 +1,30 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +/** + * Average glyph width relative to tick label font size (Open Sans–like axis labels). + */ +const CHAR_WIDTH_RATIO = 0.6; + +/** Default Elastic Charts axis tick label font size used for conversion. */ +const DEFAULT_FONT_SIZE = 11; + +/** + * Converts character count approximate width in pixels. + */ +export const charsToPixels = ( + truncate: number | null | undefined, + fontSize: number = DEFAULT_FONT_SIZE +): number | undefined => { + if (truncate == null || truncate <= 0) { + return undefined; + } + + return Math.round(truncate * fontSize * CHAR_WIDTH_RATIO); +}; diff --git a/x-pack/platform/plugins/shared/lens/public/visualizations/xy/to_expression.ts b/x-pack/platform/plugins/shared/lens/public/visualizations/xy/to_expression.ts index d9f4c2f38d85f..6b75323ece46d 100644 --- a/x-pack/platform/plugins/shared/lens/public/visualizations/xy/to_expression.ts +++ b/x-pack/platform/plugins/shared/lens/public/visualizations/xy/to_expression.ts @@ -415,7 +415,6 @@ const yAxisConfigsToExpression = (yAxisConfigs: AxisConfig[]): Ast[] => { showGridLines: axis.showGridLines ?? true, labelsOrientation: axis.labelsOrientation, scaleType: axis.scaleType, - truncate: axis.truncate, }), ]).toAst() ); From 814c283e394f07a48d4b6f99c8b3205d78446ce4 Mon Sep 17 00:00:00 2001 From: Beatriz Malveiro Date: Mon, 25 May 2026 14:24:51 +0100 Subject: [PATCH 8/8] fix: use end trucation for legacy xy charts --- .../expression_xy/public/components/xy_chart.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx index 189c96a08d313..cd4967b29d530 100644 --- a/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx +++ b/src/platform/plugins/shared/chart_expressions/expression_xy/public/components/xy_chart.tsx @@ -957,7 +957,10 @@ export function XYChart({ tickLabelMaxLength={ xAxisConfig?.truncate ?? (isHorizontalBarChart ? '40%' : undefined) } - tickLabelTruncate={isHorizontalBarChart ? ('middle' as const) : undefined} + tickLabelTruncate={ + // If legacy truncate is set, preserve end truncation behavior. + isHorizontalBarChart ? (xAxisConfig?.truncate ? 'end' : 'middle') : undefined + } {...getOverridesFor(overrides, 'axisX')} /> {isSplitChart && splitTable && (