Closed mbostock closed 5 years ago
Very cool idea! I've come across a need for such a thing when desiring the fill and stroke colors both to be data-driven, but having the fill more opaque than the stroke for a nice "popping" effect.
FWIW, I have been using the following CSS technique to accomplish this:
.mark {
fill: currentColor;
stroke: currentColor;
fill-opacity: 0.3;
}
I learned only recently about the currentColor
CSS keyword (HT @seemantk), which will use the value of the color
attribute, so you can compute the colors only once with selection.attr("color", ...)
.
Here's an example: Popping Effect.
In the spirit of brighter()
/darker()
, would it make sense to also have a corresponding method that ups the opacity? (I have no idea what a good verb for "make more opaque" would be)
Alternatively, a single function that takes a factor that multiplies the current opacity:
e.g. color.opacitize(factor)
≈ color.opacity = Math.max(0, Math.min(1, color.opacity * factor))
Perhaps we could have an operator that can apply generically to any channel, including opacity. For example, here’s how you might implement rgb.brighter followed by color.fade:
const blue = rgb("steelblue")
.withR(r => r / 0.7)
.withG(g => g / 0.7)
.withB(b => b / 0.7)
.withOpacity(o => o - 0.2);
The idea is that the with methods can take either a function or a constant; if a constant, the channel adopts the specified value; if a function, the function is passed the current value, and returns the new value. Here’s another example of “brighter” by converting to Lab:
const blue = lab(rgb("steelblue"))
.withL(l => l + 18)
.rgb();
Of course, that’s still quite a bit more verbose than color.brighter. And I wonder if you’d want an operation to modify multiple channels simultaneously. Like, I dunno…
const blue = rgb("steelblue")
.call(c => rgb(c.r / 0.7, c.g / 0.7, c.b / 0.7, c.opacity - 0.2));
If there were also a generic d3.brighter function that takes a color and returns a brighter color, you could say:
const blue = rgb("steelblue")
.call(brighter);
You could also easily convert between color spaces while chaining:
const blue = rgb("steelblue")
.call(lab)
.call(c => lab(c.l + 20, c.a, c.b, c.opacity))
.call(rgb);
d3.saturate() / desaturate() would be a useful complement.
there is a version of saturate in https://observablehq.com/@mbostock/working-with-color#saturate
Re: withR/withL, I like the idea, but not much a fan of the syntax.
I’ve added color.copy which uses Object.assign internally:
function color_copy(channels) {
return Object.assign(new this.constructor, this, channels);
}
It’s not quite as fancy as the above examples, but you can easily derive a copy of a color with a different opacity as color.copy({opacity: 0.2})
.
Similar to color.brighter, but for succinctly modifying the opacity channel. It could return a new color whose opacity is max(0, min(1, this.opacity - delta)).