Description
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:
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.
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:
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.