Skip to content

[Legend] Current value layout shift due to decimal and adaptive units #2806

@maryam-saeidi

Description

@maryam-saeidi

The Problem

The getLongestLegendFormattedValueSelector in get_legend_max_formatted_value.ts makes a flawed assumption: it assumes that formatting the Y domain maximum value will produce the longest formatted string. This is not true for some Kibana formatters. Example:

Image

Here's the key code:

const maxYValue = yDomains?.[0]?.domain?.[1];
// ...
const formatter = spec.tickFormat ?? yAxis?.tickFormat ?? defaultTickFormatter;
const formatted = formatter(maxYValue);

It only formats domain[1] (the max value) and uses that to compute the reserved minWidth. But for formatters with optional decimals or adaptive units, the domain max can actually produce a shorter string than intermediate data values.

Why It Fails

Example with optional decimals (common in average/percentile metrics):

Suppose the data is [3.5, 7.25, 12]:

  • Domain: [0, 12], so domain[1] = 12
  • formatter(12) = "12" (2 chars) — used for min-width calculation
  • formatter(7.25) = "7.25" (4 chars) — actual hover value, wider than reserved space

The min-width is computed from "VALUE: 12", but when you hover over the bar for 7.25, the legend shows "VALUE: 7.25" which is wider, causing layout shift.

Example with bytes formatter (adaptive units):

  • Domain max: 1024"1 KB" (4 chars)
  • Hover value: 500"500 Bytes" (9 chars) — much wider
Additional info

Additional Issues in the Same Selector

  1. Only the first Y domain is checked (yDomains?.[0]): If there are multiple Y axis groups (left/right axes), only the first group's domain max is used. Series on other axes may have values that format to longer strings.

  2. String length comparison instead of pixel width: The code uses formatted.length > result.length to pick the "longest" formatted string across series. But character count doesn't equal pixel width in non-monospace fonts (e.g., "111" is narrower than "000" in Inter).

The Fix

The fix should be in the elastic-charts repo, in get_legend_max_formatted_value.ts. Instead of only formatting the domain max, it should consider values that might produce longer formatted strings. Some options:

  1. Format both domain[0] (min) and domain[1] (max), and also format representative values with more decimal places — but this is fragile.

  2. Iterate all Y domains (not just yDomains[0]) and format both endpoints of each domain.

  3. Use actual data values: scan through the data series to find the value that produces the longest formatted string (most robust but potentially expensive).

  4. Use pixel width comparison instead of string length when picking the longest formatted value.

The most practical fix would be to iterate over all Y domains and format multiple representative values (including the min, max, and perhaps a value like max - 0.001 to trigger decimal formatting), then pick the widest one by pixel measurement rather than character count.

Metadata

Metadata

Assignees

No one assigned

    Labels

    :legendLegend related issue

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions