Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 6 additions & 1 deletion packages/charts/api/charts.api.md
Comment thread
nickofthyme marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export interface ArrayNode extends NodeDescriptor {
}

// @public
export const Axis: FC<SFProps<AxisSpec, "chartType" | "specType", "position" | "hide" | "groupId" | "showOverlappingTicks" | "showOverlappingLabels" | "timeAxisLayerCount", "style" | "title" | "domain" | "maximumFractionDigits" | "tickFormat" | "ticks" | "gridLine" | "labelFormat" | "integersOnly" | "showDuplicatedTicks", "id">>;
export const Axis: FC<SFProps<AxisSpec, "chartType" | "specType", "position" | "hide" | "groupId" | "showOverlappingTicks" | "showOverlappingLabels" | "timeAxisLayerCount", "style" | "title" | "domain" | "maximumFractionDigits" | "tickFormat" | "ticks" | "tickLabelMaxLength" | "tickLabelTruncate" | "gridLine" | "labelFormat" | "integersOnly" | "showDuplicatedTicks", "id">>;

// @public (undocumented)
export type AxisId = string;
Expand Down Expand Up @@ -275,6 +275,8 @@ export interface AxisSpec extends Spec {
specType: typeof SpecType.Axis;
style?: RecursivePartial<Omit<AxisStyle, 'gridLine'>>;
tickFormat?: TickFormatter;
tickLabelMaxLength?: Pixels | string;
tickLabelTruncate?: Truncate;
ticks?: number;
// @alpha
timeAxisLayerCount: number;
Expand Down Expand Up @@ -3539,6 +3541,9 @@ export interface TreeNode extends AngleFromTo {
y1: TreeLevel;
}

// @public (undocumented)
export type Truncate = 'start' | 'middle' | 'end';

// @public
export interface UnaryAccessorFn<D extends BaseDatum = any, Return = any> {
// (undocumented)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,12 @@ function nodeToLinkLabel({
text: valueText,
isValue: false,
});
const widthAdjustment = valueWidth + 2 * linkLabel.fontSize; // gap between label and value, plus possibly 2em wide ellipsis

// label text removes space allotted for value and gaps, then tries to fit as much as possible
const labelText = cutToLength(rawLabelText, maxTextLength);
const allottedLabelWidth = Math.max(
0,
rightSide ? rectWidth - diskCenter.x - translateX - widthAdjustment : diskCenter.x + translateX - widthAdjustment,
rightSide ? rectWidth - diskCenter.x - translateX - valueWidth : diskCenter.x + translateX - valueWidth,
);
const { text, width } =
linkLabel.fontSize / 2 <= cy + diskCenter.y && cy + diskCenter.y <= rectHeight - linkLabel.fontSize / 2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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 { withTickLabelTruncation } from './axis_tick_formatter';
import * as textUtils from '../../../../common/text_utils';
import { MockGlobalSpec } from '../../../../mocks/specs';
import { LIGHT_THEME } from '../../../../utils/themes/light_theme';

const {
axes: { tickLabel },
} = LIGHT_THEME;

describe('withTickLabelTruncation', () => {
const measure = jest.fn((text: string) => ({ width: text.length, height: tickLabel.fontSize }));
const fitTextSpy = jest.spyOn(textUtils, 'fitText').mockReturnValue({ width: 0, text: 'tickLabel' });
it('does not call fitText when maxLength is undefined', () => {
const axisSpec = MockGlobalSpec.yAxis({ tickLabelMaxLength: undefined, tickLabelTruncate: 'end' });

withTickLabelTruncation(measure, tickLabel, axisSpec, 200)((v: number) => `${v}`);
expect(fitTextSpy).not.toHaveBeenCalled();
});

it("calls fitText with half the container width when maxLength is '50%'", () => {
const containerWidth = 200;
const axisSpec = MockGlobalSpec.yAxis({ tickLabelMaxLength: '50%', tickLabelTruncate: 'end' });
const format = withTickLabelTruncation(measure, tickLabel, axisSpec, containerWidth)((v) => `${v}`);
format('tickLabel');
expect(fitTextSpy).toHaveBeenCalledWith(
measure,
'tickLabel',
containerWidth / 2,
tickLabel.fontSize,
expect.any(Object),
axisSpec.tickLabelTruncate ?? 'end',
);
});

it('calls fitText with the numeric maxLength as maximum width', () => {
const maxLengthPx = 72;
const axisSpec = MockGlobalSpec.yAxis({ tickLabelMaxLength: maxLengthPx, tickLabelTruncate: 'end' });
const format = withTickLabelTruncation(measure, tickLabel, axisSpec, 400)((v) => `${v}`);
format('tickLabel');

expect(fitTextSpy).toHaveBeenCalledWith(
measure,
'tickLabel',
maxLengthPx,
tickLabel.fontSize,
expect.any(Object),
axisSpec.tickLabelTruncate ?? 'end',
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@

import { getScaleConfigsFromSpecsSelector } from './get_api_scale_configs';
import { getAxisSpecsSelector, getSeriesSpecsSelector } from './get_specs';
import type { Font } from '../../../../common/text_utils';
import { fitText } from '../../../../common/text_utils';
import { createCustomCachedSelector } from '../../../../state/create_selector';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
import type { Rotation } from '../../../../utils/common';
import type { TextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator';
import { getPercentageValue, type Rotation } from '../../../../utils/common';
import type { SpecId } from '../../../../utils/ids';
import type { AxisStyle } from '../../../../utils/themes/theme';
import { defaultTickFormatter, isXDomain } from '../../utils/axis_utils';
import { groupBy } from '../../utils/group_data_series';
import type { AxisSpec } from '../../utils/specs';
Expand All @@ -22,6 +26,30 @@ export type AxisLabelFormatter<V = unknown> = (value: V) => string;
/** @internal */
export type AxisLabelFormatters = { x: Map<SpecId, AxisLabelFormatter>; y: Map<SpecId, AxisLabelFormatter> };

/** @internal */
export function withTickLabelTruncation(
measure: TextMeasure,
tickLabel: AxisStyle['tickLabel'],
axisSpec: AxisSpec,
containerWidth: number,
Comment thread
biamalveiro marked this conversation as resolved.
): <V>(formatter: AxisLabelFormatter<V>) => AxisLabelFormatter<V> {
const { fontSize, fontStyle, fontFamily, fill } = tickLabel;
const { tickLabelMaxLength: maxLength, tickLabelTruncate: truncate } = axisSpec;

const maxWidth = maxLength ? getPercentageValue(maxLength, containerWidth, 0) : undefined;
if (maxWidth === undefined || maxWidth <= 0 || maxWidth > containerWidth) return (formatter) => formatter;

const font: Font = {
fontStyle: fontStyle ?? 'normal',
fontFamily,
fontWeight: 'normal',
fontVariant: 'normal',
textColor: fill,
};

return (formatter) => (value) => fitText(measure, formatter(value), maxWidth, fontSize, font, truncate ?? 'end').text;
}

/** @internal */
export const getAxisTickLabelFormatter = createCustomCachedSelector(
[getSeriesSpecsSelector, getAxisSpecsSelector, getSettingsSpecSelector, getScaleConfigsFromSpecsSelector],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/

import type { AxisLabelFormatter } from './axis_tick_formatter';
import { getAxisTickLabelFormatter } from './axis_tick_formatter';
import { getAxisTickLabelFormatter, withTickLabelTruncation } from './axis_tick_formatter';
import { computeSeriesDomainsSelector } from './compute_series_domains';
import { countBarsInClusterSelector } from './count_bars_in_cluster';
import { getAxesStylesSelector } from './get_axis_styles';
Expand All @@ -16,6 +16,7 @@ import { getAxisSpecsSelector, getSeriesSpecsSelector } from './get_specs';
import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled';
import type { ScaleBand, ScaleContinuous } from '../../../../scales';
import { createCustomCachedSelector } from '../../../../state/create_selector';
import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions';
import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
import type { TextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator';
Expand Down Expand Up @@ -142,16 +143,23 @@ export const getLabelBox = (

/** @internal */
export const computeAxisTicksDimensionsSelector = createCustomCachedSelector(
[getJoinedVisibleAxesData],
(joinedAxesData): AxesTicksDimensions =>
[getJoinedVisibleAxesData, getChartContainerDimensionsSelector],
(joinedAxesData, chartContainerDimensions): AxesTicksDimensions =>
withTextMeasure(
(textMeasure): AxesTicksDimensions =>
[...joinedAxesData].reduce<AxesTicksDimensions>(
(axesTicksDimensions, [id, { axisSpec, scale, axesStyle, gridLine, labelFormatter }]) =>
axesTicksDimensions.set(
(axesTicksDimensions, [id, { axisSpec, scale, axesStyle, gridLine, labelFormatter }]) => {
const truncatedLabelFormatter = withTickLabelTruncation(
textMeasure,
axesStyle.tickLabel,
axisSpec,
chartContainerDimensions.width,
)(labelFormatter);
return axesTicksDimensions.set(
id,
getLabelBox(axesStyle, scale.ticks(), labelFormatter, textMeasure, axisSpec, gridLine),
),
getLabelBox(axesStyle, scale.ticks(), truncatedLabelFormatter, textMeasure, axisSpec, gridLine),
);
},
new Map(),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import type { AxisLabelFormatter } from './axis_tick_formatter';
import { withTickLabelTruncation } from './axis_tick_formatter';
import type { JoinedAxisData } from './compute_axis_ticks_dimensions';
import { getJoinedVisibleAxesData, getLabelBox } from './compute_axis_ticks_dimensions';
import { computeSeriesDomainsSelector } from './compute_series_domains';
Expand All @@ -24,11 +25,12 @@ import { isContinuousScale } from '../../../../scales/types';
import type { AxisSpec, SettingsSpec } from '../../../../specs';
import { createCustomCachedSelector } from '../../../../state/create_selector';
import { computeSmallMultipleScalesSelector } from '../../../../state/selectors/compute_small_multiple_scales';
import { getChartContainerDimensionsSelector } from '../../../../state/selectors/get_chart_container_dimensions';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
import { withTextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator';
import type { Position, Rotation } from '../../../../utils/common';
import { isFiniteNumber, isRTLString } from '../../../../utils/common';
import type { Size } from '../../../../utils/dimensions';
import type { Dimensions, Size } from '../../../../utils/dimensions';
import type { AxisId } from '../../../../utils/ids';
import { multilayerAxisEntry } from '../../axes/timeslip/multilayer_ticks';
import { isHorizontalAxis, isVerticalAxis } from '../../utils/axis_type_utils';
Expand Down Expand Up @@ -232,6 +234,7 @@ export const getVisibleTickSetsSelector = createCustomCachedSelector(
getSettingsSpecSelector,
getScaleConfigsFromSpecsSelector,
getJoinedVisibleAxesData,
getChartContainerDimensionsSelector,
computeSeriesDomainsSelector,
computeSmallMultipleScalesSelector,
countBarsInClusterSelector,
Expand All @@ -245,6 +248,7 @@ function getVisibleTickSets(
{ rotation: chartRotation, locale, dow }: Pick<SettingsSpec, 'rotation' | 'locale' | 'dow'>,
scaleConfigs: ScaleConfigs,
joinedAxesData: Map<AxisId, JoinedAxisData>,
chartContainerDimensions: Dimensions,
{ xDomain, yDomains }: Pick<SeriesDomainsAndData, 'xDomain' | 'yDomains'>,
smScales: SmallMultipleScales,
totalGroupsCount: number,
Expand All @@ -255,6 +259,12 @@ function getVisibleTickSets(
const panel = getPanelSize(smScales);
return [...joinedAxesData].reduce(
(acc, [axisId, { axisSpec, axesStyle, gridLine, isXAxis, labelFormatter: userProvidedLabelFormatter }]) => {
const tickLabelFormatter = withTickLabelTruncation(
textMeasure,
axesStyle.tickLabel,
axisSpec,
chartContainerDimensions.width,
)(userProvidedLabelFormatter);
const { groupId, integersOnly, maximumFractionDigits: mfd, timeAxisLayerCount } = axisSpec;
const yDomain = yDomains.find((yd) => yd.groupId === groupId);
const domain = isXAxis ? xDomain : yDomain;
Expand Down Expand Up @@ -318,7 +328,7 @@ function getVisibleTickSets(
const scale = getScale(triedTickCount);
const actualTickCount = scale?.ticks().length ?? 0;
if (!scale || actualTickCount === previousActualTickCount || actualTickCount < 2) continue;
const raster = getMeasuredTicks(scale, scale.ticks(), undefined, 0, userProvidedLabelFormatter);
const raster = getMeasuredTicks(scale, scale.ticks(), undefined, 0, tickLabelFormatter);
const nonZeroLengthTicks = raster.ticks.filter((tick) => tick.label.length > 0);
const uniqueLabels = new Set(raster.ticks.map((tick) => tick.label));
const areLabelsUnique = raster.ticks.length === uniqueLabels.size;
Expand Down Expand Up @@ -378,8 +388,7 @@ function getVisibleTickSets(

// todo dry it up
const scale = getScale(adaptiveTickCount ? fallbackAskedTickCount : maxTickCount);
const lastResortCandidate =
scale && getMeasuredTicks(scale, scale.ticks(), undefined, 0, userProvidedLabelFormatter);
const lastResortCandidate = scale && getMeasuredTicks(scale, scale.ticks(), undefined, 0, tickLabelFormatter);
return lastResortCandidate ? acc.set(axisId, lastResortCandidate) : acc;
},
new Map(),
Expand Down
13 changes: 13 additions & 0 deletions packages/charts/src/chart_types/xy_chart/utils/specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { $Values } from 'utility-types';
import type { XYChartSeriesIdentifier, DataSeriesDatum } from './series';
import type { ChartType } from '../..';
import type { Color } from '../../../common/colors';
import type { Pixels } from '../../../common/geometry';
import type { TooltipPortalSettings } from '../../../components/portal/types';
import type { LogScaleOptions, ScaleContinuousType } from '../../../scales';
import type { ScaleType } from '../../../scales/constants';
Expand Down Expand Up @@ -703,6 +704,9 @@ export const HistogramModeAlignments = Object.freeze({
/** @public */
export type HistogramModeAlignment = 'start' | 'center' | 'end';

/** @public */
export type Truncate = 'start' | 'middle' | 'end';

/**
* This spec describe the configuration for a chart axis.
* @public
Expand Down Expand Up @@ -740,6 +744,15 @@ export interface AxisSpec extends Spec {
* overrides tickFormat for axis labels
*/
labelFormat?: TickFormatter;
/**
* The position of the ellipsis when the tick label overflows. Defaults to 'end'.
*/
tickLabelTruncate?: Truncate;
/**
* The maximum size of the tick label.
* If a number, it is in pixels. If a string, it is relative to the container width, e.g. '20%'.
*/
tickLabelMaxLength?: Pixels | string;
/** An approximate count of how many ticks will be generated */
ticks?: number;
/** The axis title */
Expand Down
56 changes: 56 additions & 0 deletions packages/charts/src/common/text_utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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 type { Font } from './text_utils';
import { fitText } from './text_utils';
import type { TextMeasure } from '../utils/bbox/canvas_text_bbox_calculator';

const monospaceMeasure: TextMeasure = (text) => ({
width: text.length,
height: 12,
});

const font: Font = {
fontStyle: 'normal',
fontVariant: 'normal',
fontWeight: 400,
fontFamily: 'sans-serif',
textColor: '#000',
};

const fontSize = 12;

describe('fitText', () => {
it('returns the full string when it already fits (end)', () => {
expect(fitText(monospaceMeasure, 'abc', 10, fontSize, font, 'end')).toEqual({
width: 3,
text: 'abc',
});
});

it('truncates at the end with an ellipsis when width is constrained', () => {
expect(fitText(monospaceMeasure, 'abcdef', 4, fontSize, font, 'end')).toEqual({
width: 4,
text: 'abc…',
});
});

it('truncates at the start when position is start', () => {
expect(fitText(monospaceMeasure, 'abcdef', 4, fontSize, font, 'start')).toEqual({
width: 4,
text: '…def',
});
});

it('truncates in the middle when position is middle', () => {
expect(fitText(monospaceMeasure, 'abcdef', 4, fontSize, font, 'middle')).toEqual({
width: 4,
text: 'ab…f',
});
});
});
Loading