create color scale between two colors - javascript

let colors = ["#5D2EE8", "#2F9EEE", "#2FC8EE", "#2DD91A", "#CBF22C",
"#F2CE2C", "#F06E1D", "#E61717", "#3C2EA8", "#7A2EA1"];
let colorRange = d3.scale.quantile()
.domain([0, 31])
.range(colors);
I want to replace this implementation with the creation of a color palette of x colors between two colors ?
For example I want to map my set of 90 numbers between 0 and 31 to 9 colors between yellow and red. Is this possible ?
I'm using v3

You can create a sequential scale with a custom interpolator, or a threshold scale with a custom array of colors.
However, since you want to conveniently use any two colors as the start and end colors, I reckon that the easiest solution is using two scales.
The first one maps all the numbers from 0 to 31 to 9 values only:
var scale = d3.scaleQuantize()
.range(d3.range(9))
.domain([0, 31]);
Then, another scale maps those 9 values to 9 colors between any two given colors. In your case, "yellow" and "red":
var colorScale = d3.scaleLinear()
.range(["yellow", "red"])
.domain([0, 9]);
I'm using v4 here, you can just change them to d3.scale.quantize() and d3.scale.linear().
Here is a demo (also using v4), I'm creating 150 divs, each one representing a number between 0 and 31. The output has only 9 colors:
var scale = d3.scaleQuantize()
.range(d3.range(9))
.domain([0, 31]);
var colorScale = d3.scaleLinear()
.range(["yellow", "red"])
.domain([0, 9]);
var divs = d3.select("body").selectAll(null)
.data(d3.range(0, 31.2, 0.2))
.enter()
.append("div")
.style("background-color", function(d) {
return colorScale(scale(d))
});
div {
margin: 2px;
width: 15px;
height: 15px;
display: inline-block;
border: 1px solid gray;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
If you don't want to use two scales, just use math before passing the number to the color scale for creating 9 bins.

Related

Personalized axis range

I have values going these kinds of range (400k to 600 million)
And I would like to create a comprehensive y-axis on a D3 plot.
If I use a log scale, all the variations in the huge numbers are erased, If I use a linearScale, all variations in the small numbers is also erased.
Therefore I thought of doing two-axis (one over the other like in the picture below) but I don't know if there is a simpler way.
Can I specify all the tick values to get an axis where all the variations would be visible?
Thank you.
Use a regular linear scale with more than two values in both the domain and range, thus creating a piecewise scale.
For instance:
const scale = d3.scaleLinear()
.domain([0, 5e7, 1e8, 6e8])
.range([h-10, h/2 + 10, h/2 - 10, 10]);
Here is the running code:
const svg = d3.select("svg");
const h = 600;
const scale = d3.scaleLinear()
.domain([0, 5e7, 1e8, 6e8])
.range([h - 10, h / 2 + 10, h / 2 - 10, 10]);
const axis = d3.axisLeft(scale).tickValues(scale.ticks().concat(d3.range(0, 5e7, 5e6)))(svg.append("g").attr("transform", "translate(100,0)"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="200", height="600"></svg>
Do not forget to make sure that the user understands the y axis is truncated, preferably with a clear annotation showing that.

Display dynamic frequencies on axis

If I've got a list of frequencies, all in the same unit (Hz, MHz, etc.), how can I use a scale to display them nicely on the x-axis? I want them to be labeled Hz/kHz/MHz and scaled appropriately.
What you actually want is simply a number with a SI prefix followed by Hz as the unit of measurement.
That being said, you can use a linear scale and a specific tickFormat:
const format = d3.format(".0s");
axis.tickFormat(function(d) {
return format(d) + "Hz";
});
Where axis is your axis generator, whatever it is. Here, ".0s" in the specifier formats the number to use SI prefixes and no decimal places. Then, you get the resulting string and add "Hz" to it.
Here is a demo, the domain changes continually between [0, 1] and [0, 100000000000]:
const svg = d3.select("svg");
const scale = d3.scaleLinear()
.domain([0, 1000])
.range([30, 470]);
const format = d3.format(".0s");
const axis = d3.axisBottom(scale)
.tickFormat(function(d) {
return format(d) + "Hz";
});
const g = svg.append("g")
.attr("transform", "translate(0,50)");
g.call(axis);
d3.interval(function() {
scale.domain([0, Math.pow(10, ~~(Math.random() * 12))]);
g.transition()
.duration(1000)
.call(axis)
}, 2000);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg width="500" height="100"></svg>

d3 quantile scale force first quantile

I'm building a heat map with a color gradient from green to red. I want cells with value 0 to be green, and values greater or equal than 1 to take the other colors.
I'm building the scale this way :
var colors = [
'#27C24C',
'#7DB22E',
'#D4A10F',
'#F97C20',
'#F35F40',
'#FF0000'
];
var colorScale = d3.scale.quantile()
.domain([0, d3.max(data, function (d) { return d.value; })])
.range(colors);
But this returns me the following quantiles :
[239.16666666666677, 478.3333333333332, 717.5, 956.6666666666664, 1195.8333333333335]
Therefore, I have the following heatmap :
But I would like the pointed cell to be the second shade of green, since its value is strictly greater than 0.
You cannot use only quantile scale in this case. Write custom scale function to treat the zero value separately.
var colors = [
// '#27C24C', this value must not be included in the internal range
'#7DB22E',
'#D4A10F',
'#F97C20',
'#F35F40',
'#FF0000'
];
var colorScaleInternal = d3.scale.quantile()
.domain([0, d3.max(data, function (d) { return d.value; })])
.range(colors);
var colorScale = function(value) {
return !!value ? colorScaleInternal(value) : '#27C24C';
};
While I couldn't find support in D3 for this functionality, I was able to work around it by altering the range array sent to d3. The idea is to check with D3 if the quartiles are repeating, and if so, keep the same color for all of them:
var scale = d3.scale.quantile().domain(domain).range(range);
var quantiles = scale.quantiles();
quantiles.unshift(d3.min(domain));
// Now that you have the quantiles, you can see if some of them are holding the same value,
// and it that case set the minimum value to all of them.
var modifiedRange = [range[0]];
for (var i = 1; i < range.length; i++) {
if (quantiles[i] === quantiles[i - 1]) {
modifiedRange.push(modifiedRange[i - 1]);
} else {
modifiedRange.push(range[i]);
}
}
// set the new scale
scale.range(modifiedRange);

Color scale not working appropriately in D3/JavaScript [duplicate]

This question already has answers here:
d3.scale.category10() not behaving as expected
(3 answers)
Closed 8 years ago.
Im using a color scale :
var color = d3.scale.category10();
Im using this to color the edges of a force layout graph according to their value
var links = inner.selectAll("line.link")
.style("stroke", function(d) { return color(d.label); });
Now I need a 'legend' to show the user what each color means:
edgesArray = [];
edgesArrayIndex = [];
for (i=0;i<data.edges.length;i++) {
if(!edgesArray[data.edges[i].name])
{
edgesArray[data.edges[i].name]=1;
edgesArrayIndex.push(data.edges[i].name);
}
}
var colourWidth = 160;
var colourHeight = 25;
for(i=0; i<edgesArrayIndex.length; i++){
if (edgeColour == true){
svg.append('rect')
.attr("width", colourWidth)
.attr("height", colourHeight)
.attr("x", Marg*2)
.attr("y", Marg*2 + [i]*colourHeight)
.style("fill", color(i))
;
svg.append("text")
.attr("x", Marg*3)
.attr("y", Marg*2 + [i]*colourHeight + colourHeight/2)
.attr("dy", ".35em")
.text(color(i) + " : " + data.edges[i].name);
}
//console.log(edgesArrayIndex);
}
The colors are all wrong. When I attach the colors to the graph the first 6 colors get attached to the edges as there are 6 different types of edges.
But when I apply the color scale to the 6 'rects' I appended to the SVG its as if the first 6 colors of the color scale array got used up when applying then to the edges and when i do the for loop starting at color(0) it actually gives me the color at color(5) (as 0 is first).
For example, say ive got red,blue,green,white,black,pink,orange,yellow,indigo,aqua.
My edges get - red,blue,green,white,black,pink
and now when i want to apply the scale to the rects I made, I would expect the rects to have the same values - red,blue,green,white,black,pink.
But they actually have : orange,yellow,indigo,aqua, red, blue.
They start at color[5] and its as if they wrap round to the beginning and go back to red, blue and so on.
You are using two different parts of the data for the colour scale -- first the label, then the name. By definition, this will give you different colours (different inputs map to different outputs as far as possible given the output range).
To get the same colours, pass in the same attributes, e.g. the labels (if I understood your data structure correctly):
svg.append('rect')
.style("fill", color(data.edges[i].label))
this question helped me -
d3.scale.category10() not behaving as expected
basically you have to put a domain on the scale. So instead of
var color = d3.scale.category10();
Change it to:
var color = d3.scale.category10().domain([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);//-colour scale

Make circles not go outside of the chart bounds with D3

I am working on a chart looking like this now:
I use d3 scales and ranges to setup sizes and coordinates of circles, from JSON data.
All works fine but I need to make sure those circles that are close to extreme values don't overlap the sides of the chart (like orange circle on the top right and blue one on the bottom side), so I think I need to play with ranges and change coordinates in case they overlap or is there a better tried way to do this?
When drawing circles, in addition to the x and y scaling functions we also use an r scaling function:
var rScale = d3.scale.linear()
.domain([0, maxR])
.range([0, maxBubbleRadius]);
var xScale = d3.scale.linear()
.domain([minX, maxX])
.range([0, chartWidth]);
var yScale = d3.scale.linear()
.domain([minY, maxY])
.range([chartHeight, 0]);
where maxR is the largest r value in your dataset and maxBubbleRadius is however large you want the largest circle to be, when you plot it.
Using the x and y scaling functions it is easy to calculate where the centre of each circle will be plotted, we can then add on the (scaled) r value to see if the circle will spill over a chart boundary. With a scenario like the first chart below we can see that 4 of the circles spill over. The first step to remedy this is to find out how many vertical and horizontal units we spill over by and then increase the minimum and maximum x and y values to take this into account, before recalculating the xScale and yScale vars. If we were to then plot the chart again, the boundary would move out but there would probably still be some visible spillage (depending on actual values used); this is because the radius for a given circle is a fixed number of pixels and will therefore take up a different number of x and y units on the chart, from when we initially calculated how much it spilled over. We therefore need to take an iterative approach and keep applying the above logic until we get to where we want to be.
The code below shows how I iteratively achieve an acceptable scaling factor so that all the circles will plot without spilling. Note that I do this 10 times (as seen in the loop) - I've just found that this number works well for all the data that I've plotted so far. Ideally though, I should calculate a delta (the amount of spillage) and iterate until it is zero (this would also require overshooting on the first iteration, else we'd never reach our solution!).
updateXYScalesBasedOnBubbleEdges = function() {
var bubbleEdgePixels = [];
// find out where the edges of each bubble will be, in terms of pixels
for (var i = 0; i < dataLength; i++) {
var rPixels = rScale(_data[i].r),
rInTermsOfX = Math.abs(minX - xScale.invert(rPixels)),
rInTermsOfY = Math.abs(maxY - yScale.invert(rPixels));
var upperPixelsY = _data[i].y + rInTermsOfY;
var lowerPixelsY = _data[i].y - rInTermsOfY;
var upperPixelsX = _data[i].x + rInTermsOfX;
var lowerPixelsX = _data[i].x - rInTermsOfX;
bubbleEdgePixels.push({
highX: upperPixelsX,
highY: upperPixelsY,
lowX: lowerPixelsX,
lowY: lowerPixelsY
});
}
var minEdgeX = d3.min(bubbleEdgePixels, function(d) {
return d.lowX;
});
var maxEdgeX = d3.max(bubbleEdgePixels, function(d) {
return d.highX;
});
var minEdgeY = d3.min(bubbleEdgePixels, function(d) {
return d.lowY;
});
var maxEdgeY = d3.max(bubbleEdgePixels, function(d) {
return d.highY;
});
maxY = maxEdgeY;
minY = minEdgeY;
maxX = maxEdgeX;
minX = minEdgeX;
// redefine the X Y scaling functions, now that we have this new information
xScale = d3.scale.linear()
.domain([minX, maxX])
.range([0, chartWidth]);
yScale = d3.scale.linear()
.domain([minY, maxY])
.range([chartHeight, 0]);
};
// TODO: break if delta is small, rather than a specific number of interations
for (var scaleCount = 0; scaleCount < 10; scaleCount++) {
updateXYScalesBasedOnBubbleEdges();
}
}

Categories