问题
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:
- Find the color channel that has the greatest extent between the min and max values of the range (min and max are:
a
,b
). - Extract the same color channel from the color that needs inverting:
y
. - De-interpolate
y
betweena
andb
, giving ust
- 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