Skip to content

Contrasting text color? #540

Open
Open
@mbostock

Description

@mbostock

Often it’s desirable for a text label to have a strong contrast against its background. If the background color is constant (e.g., white in the empty space of the chart), then it’s fine to hard-code the text color. But if the background color is variable, as when labeling cells, then it’s more difficult. For example, the extreme values (e.g., 4.5 and 9.2) are hard to read here:

Screen Shot 2021-09-12 at 10 09 06 AM

It’s difficult to do this well in Plot currently.

One approach is to define the fill color as a channel. However, since the fill channel is bound to the color scale, you cannot express the color literally; you must specify an abstract value that is passed through the color scale. Hence, this is generally not an option. This problem was discussed in #56, and identity scales were added in #305, however, this isn’t really a good fix for this problem because we don’t want to disable the color scale entirely; we just want to disable it for this specific mark channel.

Another approach is to repeat the text mark definition twice, once for light text and once for dark text, and use the filter option to control which data gets which color. This produces the desired result, but it’s tedious.

Screen Shot 2021-09-12 at 10 17 36 AM

Plot.text(simpsons, {
  x: "season",
  y: "number_in_season",
  text: d => d.imdb_rating?.toFixed(1),
  title: "title",
  filter: d => d.imdb_rating >= 5.5 && d.imdb_rating <= 8.5,
  fill: "black"
}),
Plot.text(simpsons, {
  x: "season",
  y: "number_in_season",
  text: d => d.imdb_rating?.toFixed(1),
  title: "title",
  filter: d => d.imdb_rating < 5.5 || d.imdb_rating > 8.5,
  fill: "white"
})

This can be alleviated with a helper mark:

function ctext(data, {invert, ...options} = {}) {
  const filter = Plot.valueof(data, invert);
  return Plot.marks(
    Plot.text(data, {...options, filter, fill: "white"}),
    Plot.text(data, {...options, filter: filter.map(d => !d)})
  );
}

But even so, it requires manually specifying thresholds in data space to determine which text labels should be inverted, which is tedious and prone to error. It’d be better to take advantage of the color scale definition, but this isn’t available at the time the mark channels are being constructed (since there’s a potential circularity there).

It’s sort of possible to do this in CSS using mixBlendMode:

Screen Shot 2021-09-12 at 10 09 54 AM

Plot.text(simpsons, {
  x: "season",
  y: "number_in_season",
  text: d => d.imdb_rating?.toFixed(1),
  title: "title",
  fill: "white",
  mixBlendMode: "difference"
})

The result isn’t perfect (the text inherits a color rather than being either white or black), and the technique isn’t especially memorable but it is concise.

One thought is that maybe text marks could have a “background” color channel, which if specified, will automatically invert the fill color to maximize contrast.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestquestionFurther information is needed

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions