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/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/__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"
/>
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,
@@ -484,7 +485,7 @@ export function XYChart({
? getLinesCausedPaddings(visualConfigs, yAxesMap, shouldRotate)
: {};
- const getYAxesStyle = (axis: AxisConfiguration) => {
+ const getYAxesStyle = (axis: AxisConfiguration): RecursivePartial => {
const tickVisible = axis.showLabels;
const position = getOriginalAxisPosition(axis.position, shouldRotate);
@@ -702,6 +703,7 @@ export function XYChart({
visible: xAxisConfig?.showGridLines,
strokeWidth: 1,
};
+
const xAxisStyle: RecursivePartial = isHorizontalTimeAxis
? {
tickLabel: {
@@ -947,17 +949,18 @@ 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}
showDuplicatedTicks={xAxisConfig?.showDuplicates}
+ tickLabelMaxLength={
+ xAxisConfig?.truncate ?? (isHorizontalBarChart ? '40%' : undefined)
+ }
+ tickLabelTruncate={
+ // If legacy truncate is set, preserve end truncation behavior.
+ isHorizontalBarChart ? (xAxisConfig?.truncate ? 'end' : 'middle') : undefined
+ }
{...getOverridesFor(overrides, 'axisX')}
/>
{isSplitChart && splitTable && (
@@ -983,18 +986,13 @@ 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)}
showOverlappingLabels={axis.showOverlappingLabels}
showDuplicatedTicks={axis.showDuplicates}
+ tickLabelMaxLength={axis.truncate}
{...getOverridesFor(
overrides,
/left/i.test(axis.groupId) ? 'axisLeft' : 'axisRight'