D3 Inverting Color Scale Map to get amplitude

Deadly 提交于 2019-12-24 02:09:07

问题


I have a colorscaleMap lets assume something like this

d3.scale.linear()
 .domain([0.01,0.02,0.03,0.04,0.05])
 .range(["#5100ff", "#00f1ff", "#00ff30", "#fcff00", "#ff0000"])

Now I want to use the invert function

Let's say invert("#5100ff") and I am expecting the output to be 0.01 but invert functionality does not work on non-numeric values. As stated in https://github.com/d3/d3-scale This method is only supported if the range is numeric. If the range is not numeric, returns NaN

Problem:- This is related to a large data Spectrogram with more than 10000 data points. Y - Axis is Frequency, X Axis is TIME and colors represent an amplitude. Writing Index function will be inefficient I suppose

Does anyone have a solution to this problem?


回答1:


This is not an easy task, and it may be best if the issue can be avoided by using bound data if possible.

But, this is a very interesting problem. When inverting numbers you are at least bound to a single dimension, a number will fall within the domain of interpolatable values (with some specific exceptions). With three dimensional color space, this gets a bit harder, especially with segmented domains and ranges. But even if we put aside the provision of colors to the invert function that can't be produced by the scale, things still are difficult.

As noted d3-scale doesn't support non-numerical inversions, d3-interpolate doesn't offer invert support either. Nor can we have non numerical domains (this would provide the easiest solution) on a continuous scale. Without re-writing each color interpolator, any solution will probably be specific to a certain interpolation (the default is a linear interpolation on each channel, so I'll use that below).

If we can beat the above issues, a last challenge is that you can't have a ton of precision: RGB channel values are rounded to a 8 bit number from 0 to 255. Inverting this number will generate rounding errors.

This rounding issue is usually compounded by the fact that many color scales don't use the full range of 0 - 255 on any given channel. If going from red to orange, the channel with the greatest change is change is green, with a change of 165. This will definitely reduce the precision of any invert function.

That said, we could make simple invert function for a scale with a range in color space. This could take a few steps:

  1. Find the color channel that has the greatest extent between the min and max values of the range (min and max are: a,b).
  2. Extract the same color channel from the color that needs inverting: y.
  3. De-interpolate y between a and b, giving us t
  4. Take t and interpolate between the min and max of the domain of the scale

A linear interpolator looks like: y = a + t * (b - a)

With that we can deinterpolate with: t = (y - a)/(b - a)

t is a number between 0 and 1 where (when applied to the interpolator) 0 would produce the min value in the domain and 1 would produce the max value in the domain.

Here's how that might look:

var scale = d3.scaleLinear()
  .domain([0,100])
  .range(["orange","red"])


 scale.invertColor = function(x) {
    // get the color into a common form
    var color = d3.color(x); 
    
    // do the same for the range:
    var min = d3.color(this.range()[0]);
    var max = d3.color(this.range()[1]);
	
    // find out which channel offerrs the greatest spread between min and max:
    var spreads = [max.r-min.r,max.g-min.g,max.b-min.g].map(function(d) { return Math.abs(d); });
    var i = spreads.indexOf(Math.max(...spreads));
    var channel = d3.keys(color)[i];
	
    // The deinterpolation function	
    var deinterpolate = function (value,a,b) {
      if(b-a==0) return 0;  
      return Math.abs((value - a) / (b - a))
    }

	// Get t
	var t = deinterpolate(color[channel],min[channel],max[channel]);

    // Interpolate between domain values with t to get the inverted value:
    return d3.interpolate(...this.domain())(t)

};
  

  
var test = [0,10,50,90,100];
console.log("Test values:");
console.log(test)
console.log("Scaling and then Inverting values:");
console.log(test.map(function(d) { return scale.invertColor(scale(d)); }));
console.log("Original values minus scaled and inverted values:");
console.log(test.map(function(d) { return (d - scale.invertColor(scale(d))); }));
<script src="https://d3js.org/d3.v5.min.js"></script>

There are limits to the above, you can feed this any color. It'll interpolate a value for you, even if that scale can't produce that value.

It's pretty easy to modify this for multiple segments by using the above function multiple times. We can check each segment to see if a value once inverted scales back to itself for each segment. By doing so only colors produced by the scale can be inverted:

var scale = d3.scaleLinear()
  .domain([0,100,200])
  .range(["orange","red","purple"])


scale.invertColor = function(x) {
  var domain = this.domain();
  var range = this.range();
		
  // check each segment to see if the inverted value scales back to the original value:
  for(var i = 0; i < domain.length - 1; i++) {
    var d = [domain[i], domain[i+1]];
    var r = [range[i], range[i+1]];
		  
    var inverted = (invert(x,d,r))
    if (d3.color(this(inverted)).toString() == d3.color(x).toString()) return inverted;		
  }
  return NaN;  // if nothing matched
		
		
  function invert(x,d,r) {
    // get the color into a common form
    var color = d3.color(x); 
        
    // do the same for the range:
    var min = d3.color(r[0]);
    var max = d3.color(r[1]);
    	
    // find out which channel offerrs the greatest spread between min and max:
    var spreads = [max.r-min.r,max.g-min.g,max.b-min.g].map(function(d) { return Math.abs(d); });
    var i = spreads.indexOf(Math.max(...spreads));
    var channel = d3.keys(color)[i];
    	
    // The deinterpolation function	
    var deinterpolate = function (value,a,b) {
      if(b-a==0) return 0;  
      return Math.abs((value - a) / (b - a))
    }

    // Get t
    var t = deinterpolate(color[channel],min[channel],max[channel]);

    // Interpolate between domain values with t to get the inverted value:
    return d3.interpolate(...d)(t)	
  }
		
};
      

      
    var test = [0,10,50,90,100,110,150,190,200];
    console.log("Test values:");
    console.log(test)
    console.log("Scaling and then Inverting values:");
    console.log(test.map(function(d) { return scale.invertColor(scale(d)); }));
    console.log("Original values minus scaled and inverted values:");
    console.log(test.map(function(d) { return (d - scale.invertColor(scale(d))); }));
<script src="https://d3js.org/d3.v5.min.js"></script>


来源:https://stackoverflow.com/questions/51427754/d3-inverting-color-scale-map-to-get-amplitude

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!