d3 / d3-scale-chromatic

Sequential, diverging and categorical color scales.
https://d3js.org/d3-scale-chromatic
Other
801 stars 106 forks source link

Calculate color with two variables (t1, t2)? #32

Open jfiala opened 3 years ago

jfiala commented 3 years ago

We'd like to calculate the color value on a 2-dimensional matrix with 4 colors:

farbverläufe

Is that possible with d3js-scale-chromatic? It appears all functions provide only one variable t.

Any ideas? :)

Thank you & Best regards, Johannes

curran commented 3 years ago

I was looking at a similar problem lately, and have been meaning to try a solution where two separate scales are used, then the outputs can be blended into a matrix like the one you shared. I'm not sure what the best way of blending the colors is, or if there's any utility that does this inside D3 (color interpolate maybe?), but I did come across this neat library: https://github.com/Loilo/color-blend

Tangentially related:

Will post here if I find anything more.

curran commented 3 years ago

Oh, here are some things https://observablehq.com/search?query=bivariate

curran commented 3 years ago

In particular https://observablehq.com/@d3/bivariate-choropleth image

curran commented 3 years ago

And there's this:

colors = blendMode => {
  const scale1 = chroma.scale([color1, lightest]).mode(colorMode).correctLightness().colors(rows)
  const scale2 = chroma.scale([color2, lightest]).mode(colorMode).correctLightness().colors(rows)

  const data = []

  for(let i = 0; i < rows; i++) {
    for(let j = 0; j < rows; j++) {
      data[i * rows + j] =  {
        color: chroma.blend(scale1[i], scale2[j], blendMode),
        x: i * size / rows,
        y: j * size / rows
      }
    }
  }

  return data
}

From https://observablehq.com/@benjaminadk/bivariate-choropleth-color-generator

This blends the colors using https://vis4.net/chromajs/#chroma-blend

curran commented 3 years ago

FWIW:

It's funny that blending is part of the WC3 Spec (CSS does this blending), but do it in JS you need to re-implement blending. I wonder if there's any way to do color blending in JS without one of those libraries... Maybe draw pixels on a Canvas and read the result or something.

mbostock commented 3 years ago

The typical way of doing this is bilinear interpolation, which you can certainly do using D3. However, if you’re specifically looking to make a bivariate choropleth, I suggest you use a well-designed color palette rather than creating one yourself. This is linked from the D3 bivariate choropleth example:

https://www.joshuastevens.net/cartography/make-a-bivariate-choropleth-map/

curran commented 3 years ago

How might one approach bilinear interpolation using D3?

This is the closest I could find, which implements the interpolation outside D3: https://observablehq.com/@sw1227/bilinear-interpolation-of-tile

Also I'm curious, how might one generalize the approach to more than two variables, for a multivariate choropleth map? I'm investigating how one might approximate the colors of a dot density map, like this one, using multiple single-hue color ramps. Blending the RGBA colors is what came to mind, but perhaps there's a notion of "multilinear interpolation" or something like that.

image

Related:

mbostock commented 3 years ago
const i1 = d3.interpolateLab(color1, color2);
const i2 = d3.interpolateLab(color3, color4);
const i3 = d3.interpolateLab(i1(t1), i2(t1));
return i3(t2);
curran commented 3 years ago

I assume color1, color2, color3 and color4 are outputs from color 4 separate color ramps that go from opacity 0 to opacity 1 with a constant hue, and that t1, t2 should be 0.5?

This works for an even number of colors, but what about an odd number like 3 or 5?

const color1Scale = scaleLinear()
    .domain([0, max(data, color1Value(d))])
    .range(['rgba(85, 255, 0, 0)', 'rgba(85, 255, 0, 0)']);
...
const color1 = color1Scale(color1Value(d)); // White
const color2 = color2Scale(color2Value(d)); // Black
const color3 = color3Scale(color3Value(d)); // Asian
const color4 = color4Scale(color4Value(d)); // Hispanic
const color5 = color5Scale(color5Value(d)); // Other

const t1 = 0.5;
const t2 = 0.5; // Maybe this could be tweaked to get the right balance?

const i1 = d3.interpolateLab(color1, color2);
const i2 = d3.interpolateLab(color3, color4);
const i3 = d3.interpolateLab(i1(t1), i2(t1));
const i4 = d3.interpolateLab(i3(t2), color5); // Valid? Too much weight for color5?
return i4(t2);
mbostock commented 3 years ago

You’re describing a multivariate color scheme, not a bivariate one.

jfiala commented 3 years ago

Whow, many thanks for all the inputs! I'd like to have a chromatic matrix-map (e.g. with 1.000x1.000 "pixels") instead of the bivariate map with 3x3 or 4x4 in my sample.

The easy way would be to use a 3x3 matrix and then interpolate between the 9 colors, right as Mike suggested? const i1 = d3.interpolateLab(color1, color2);

I'll give that a try.

And yes, of course it would be nice to have this also for > 2 variables = multivariate.

jfiala commented 3 years ago

We generated a chromatic color scheme for one of the palettes of Joshua Stevens ( https://www.joshuastevens.net/cartography/make-a-bivariate-choropleth-map/):

chromatic_colormap_small

This is what we meant. Of course it would also be great to have that for 3+ variables, but for now we go with the 2 :).

jfiala commented 3 years ago

We were discussing whether for map visualization it really makes sense to actually used the chromatic scheme.

Would a 10x10 matrix (as an improvement of the 3x3 matrix, which is definitely too scarce in variation) suffice? Any opinions on that?

IMHO the main advantage of using the chromatic scheme is to get the precise best matching color for a value t, but for humans probably the difference is not visible :).

Colorbrewer supports up to 9 classes for one variable.

This generator here up to 6 classes: https://observablehq.com/@benjaminadk/bivariate-choropleth-color-generator

Any opinions on that?

curran commented 3 years ago

Opinions on this vary wildly.

Some argue that because the eye can't really differentiate between more than 12 or so distinct colors, one should not use more than that.

Others (myself include) argue that ideally you'd use a continuous color ramp (which could be approximated with more classes, maybe 100 classes or so), because the eye is really good at picking up on subtle color differences when the polygons share a border and there's no stroke in between them.

jfiala commented 3 years ago

Thanks for your opinion, I'll also follow that continuous approach, especially as it has the advantage of not having to classify the data. Thanks for the hint regarding removing the borders, we'll give that a try and let you know...!