D3JS: set colour gradient above a certain threshold - javascript

I'm trying out this D3 heatmap example. I'd like to implement slightly more complex colouring logic - all values below 30 will simply have a red fill, and above 30 to follow the Blues colour gradient.
Related question, but this applies constant colours to different parts of a domain (e.g. red for values 0 to 50, blue for 50 to 100...) rather than flipping from a constant colour to a colour gradient. I could follow that answer and manually implement a domain of a single red and a sufficiently large set of blues from raw HTML colour codes, but I'd like a more elegant solution that implements the scale of blues for me, particularly because I won't know what the maximum value of possible range inputs will be.
I tried to modify the attr("fill") line to the following, but it won't work - it applies the red correctly, but the non-red squares just appear black, which tells me that the colour gradient isn't kicking in for some reason:
...
// Build color scale
var myColor = d3
.scaleSequential()
.interpolator(d3.interpolate)
.domain([30, 100]); //Doesn't matter if I set the domain to [0.001, 100] or [30, 100]
...
.style("fill", function (d) {
if (d.value < 30) {
return "red"
} else {
return myColor(d.value);
};
})
...

You need to specify a function to interpolate between colors. You've specified d3.interpolate, but you haven't given it any values to interpolate between, eg:
d3.interpolate("steelblue","yellow")
// Build color scale
var myColor = d3
.scaleSequential()
.interpolator(d3.interpolate("steelblue","yellow"))
.domain([30, 100]);
var data = d3.range(100)
d3.select("body")
.append("svg")
.attr("height", 300)
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width", 20)
.attr("height", 20)
.attr("x", (d,i) => i%10 * 22)
.attr("y", (d,i) => Math.floor(i/10) * 22)
.style("fill", function (d) {
if (d < 30) {
return "red"
} else {
return myColor(d);
};
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Or passed it a pre-built interpolator for different color schemes, eg:
d3.interpolateBlues
// Build color scale
var myColor = d3
.scaleSequential()
.interpolator(d3.interpolateBlues)
.domain([30, 100]);
var data = d3.range(100)
d3.select("body")
.append("svg")
.attr("height", 300)
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width", 20)
.attr("height", 20)
.attr("x", (d,i) => i%10 * 22)
.attr("y", (d,i) => Math.floor(i/10) * 22)
.style("fill", function (d) {
if (d < 30) {
return "red"
} else {
return myColor(d);
};
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
You can use d3.scaleSequential(interpoloator) instead of specifying the interpolator using the .interpoloator method:
// Build color scale
var myColor = d3
.scaleSequential(d3.interpolateBlues)
.domain([30, 100]);
var data = d3.range(100)
d3.select("body")
.append("svg")
.attr("height", 300)
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width", 20)
.attr("height", 20)
.attr("x", (d,i) => i%10 * 22)
.attr("y", (d,i) => Math.floor(i/10) * 22)
.style("fill", function (d) {
if (d < 30) {
return "red"
} else {
return myColor(d);
};
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Related

How do I match up text labels in a legend created in d3

I am building a data visualization project utilizing the d3 library. I have created a legend and am trying to match up text labels with that legend.
To elaborate further, I have 10 rect objects created and colored per each line of my graph. I want text to appear adjacent to each rect object corresponding with the line's color.
My Problem
-Right now, an array containing all words that correspond to each line appears adjacent to the top rect object. And that's it.
I think it could be because I grouped my data using the d3.nest function. Also, I noticed only one text element is created in the HTML. Can anyone take a look and tell me what I'm doing wrong?
JS Code
const margin = { top: 20, right: 30, bottom: 30, left: 0 },
width = 1000 - margin.left - margin.right;
height = 600 - margin.top - margin.bottom;
// maybe a translate line
// document.body.append(svg);
const div_block = document.getElementById("main-div");
// console.log(div_block);
const svg = d3
.select("svg")
.attr("width", width + margin.left + margin.right) // viewport size
.attr("height", height + margin.top + margin.bottom) // viewport size
.append("g")
.attr("transform", "translate(40, 20)"); // center g in svg
// load csv
d3.csv("breitbartData.csv").then((data) => {
// convert Count column values to numbers
data.forEach((d) => {
d.Count = +d.Count;
d.Date = new Date(d.Date);
});
// group the data with the word as the key
const words = d3
.nest()
.key(function (d) {
return d.Word;
})
.entries(data);
// create x scale
const x = d3
.scaleTime() // creaters linear scale for time
.domain(
d3.extent(
data,
// d3.extent returns [min, max]
(d) => d.Date
)
)
.range([margin.left - -30, width - margin.right]);
// x axis
svg
.append("g")
.attr("class", "x-axis")
.style("transform", `translate(-3px, 522px)`)
.call(d3.axisBottom(x))
.append("text")
.attr("class", "axis-label-x")
.attr("x", "55%")
.attr("dy", "4em")
// .attr("dy", "20%")
.style("fill", "black")
.text("Months");
// create y scale
const y = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.Count)])
.range([height - margin.bottom, margin.top]);
// y axis
svg
.append("g")
.attr("class", "y-axis")
.style("transform", `translate(27px, 0px)`)
.call(d3.axisLeft(y));
// line colors
const line_colors = words.map(function (d) {
return d.key; // list of words
});
const color = d3
.scaleOrdinal()
.domain(line_colors)
.range([
"#e41a1c",
"#377eb8",
"#4daf4a",
"#984ea3",
"#ff7f00",
"#ffff33",
"#a65628",
"#f781bf",
"#999999",
"#872ff8",
]); //https://observablehq.com/#d3/d3-scaleordinal
// craete legend variable
const legend = svg
.append("g")
.attr("class", "legend")
.attr("height", 100)
.attr("width", 100)
.attr("transform", "translate(-20, 50)");
// create legend shapes and locations
legend
.selectAll("rect")
.data(words)
.enter()
.append("rect")
.attr("x", width + 65)
.attr("y", function (d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d) {
return color(d.key);
});
// create legend labels
legend
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i * 20 + 9;
})
// .attr("dy", "0.32em")
.text(
words.map(function (d, i) {
return d.key; // list of words
})
);
// returning an array as text
// });
svg
.selectAll(".line")
.data(words)
.enter()
.append("path")
.attr("fill", "none")
.attr("stroke", function (d) {
return color(d.key);
})
.attr("stroke-width", 1.5)
.attr("d", function (d) {
return d3
.line()
.x(function (d) {
return x(d.Date);
})
.y(function (d) {
return y(d.Count);
})(d.values);
});
});
Image of the problem:
P.S. I cannot add a JSfiddle because I am hosting this page on a web server, as that is the only way chrome can read in my CSV containing the data.
My Temporary Solution
function leg_labels() {
let the_word = "";
let num = 0;
for (i = 0; i < words.length; i++) {
the_word = words[i].key;
num += 50;
d3.selectAll(".legend")
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i + num;
})
// .attr("dy", "0.32em")
.text(the_word);
}
}
leg_labels();
Problem
Your problem has to do with this code
legend
.append("text")
.attr("x", width + 85)
.attr("y", function (d, i) {
return i * 20 + 9;
})
// .attr("dy", "0.32em")
.text(
words.map(function (d, i) {
return d.key; // list of words
})
);
You are appending only a single text element and in the text function you are returning the complete array of words, which is why all words are shown.
Solution
Create a corresponding text element for each legend rectangle and provide the correct word. There are multiple ways to go about it.
You could use foreignObject to append HTML inside your SVG, which is very helpful for text, but for single words, plain SVG might be enough.
I advise to use a g element for each legend item. This makes positioning a lot easier, as you only need to position the rectangle and text relative to the group, not to the whole chart.
Here is my example:
let legendGroups = legend
.selectAll("g.legend-item")
.data(words)
.enter()
.append("g")
.attr("class", "legend-item")
.attr("transform", function(d, i) {
return `translate(${width + 65}px, ${i * 20}px)`;
});
legendGroups
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d) {
return color(d.key);
});
legendGroups
.append("text")
.attr("x", 20)
.attr("y", 9)
.text(function(d, i) { return words[i].key; });
This should work as expected.
Please note the use of groups for easier positioning.

Apply transition while keeping the old elements' positions

On the page I have a number of rectangles with the same class, say class one.
How do I apply a transition to all those rectangles so they move to a new position with a new class (maybe class two), but keeping those old rectangles stationary in the same position?
Could someone please correct me if I have explained it incorrectly?
For example I have these rectangles with class "start"
d3.select("svg")
.selectAll("rect")
.data([10,20,30,40,50])
.enter()
.append("rect")
.attr("class", "start")
.attr("x", d => d)
.attr("y", 1)
.attr("width", 5)
.attr("height", 5);
These rectangles coordinates are (10, 1), (20, 1), (30, 1) ...
Then I move them
d3.selectAll("rect")
.transition()
.attr("y", (d, i) => i + 5 * 10);
They will appear at the new co-ordinates (10, 50), (20, 51), (30, 52) ...
How can I make it so that the original rectangles with class start at (10, 1), (20, 1), (30, 1) ... are still there but have new rectangles at (10, 50), (20, 51), (30, 52) ... with class stop?
As already made clear in your edit, you don't want to apply the transition to the existing elements: you want to clone them and apply the transition to their clones (or clone them before applying the transition to the original ones, which is the same...).
That being said, D3 has a pretty handy method named clone, which:
Inserts clones of the selected elements immediately following the selected elements and returns a selection of the newly added clones.
So, supposing that your selection is named rectangles (advice: always name your selections), instead of doing this...
rectangles.transition()
.attr("class", "stop")
.attr("y", (d, i) => i + 5 * 10);
...clone them first:
rectangles.each(cloneNodes)
.transition()
.attr("class", "stop")
.attr("y", (d, i) => i + 5 * 10);
function cloneNodes() {
d3.select(this).clone(false);
}
Here is the demo:
const svg = d3.select("svg");
const rectangles = d3.select("svg")
.selectAll(null)
.data([10, 20, 30, 40, 50])
.enter()
.append("rect")
.attr("class", "start")
.attr("x", d => d)
.attr("y", 1)
.attr("width", 5)
.attr("height", 5);
rectangles.each(cloneNodes)
.transition()
.attr("class", "stop")
.attr("y", (d, i) => i + 5 * 10);
function cloneNodes() {
d3.select(this).clone(false);
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
There is no need to use each and a function to clone.
rectangles.clone(false)
.transition()
.attr("class", "stop")
.attr("y", (d, i) => i + 5 * 10);
const svg = d3.select("svg");
const rectangles = d3.select("svg")
.selectAll(null)
.data([10, 20, 30, 40, 50])
.enter()
.append("rect")
.attr("class", "start")
.attr("x", d => d)
.attr("y", 1)
.attr("width", 5)
.attr("height", 5);
rectangles.clone(false)
.transition()
.attr("class", "stop")
.attr("y", (d, i) => i + 5 * 10);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

D3 transition not occurring on svgelement.enter()

I'm throwing in some fairly simple code to build out some rectangles from a data file which is all working fine, however I'm trying to add in a transition for the rectangles.enter() something akin to .transition().duration(1000)
I've looked at using the .on() function prior to the transition, but no matter where I put it in the code either no change, or the whole graph disappears. Is it possible to add in a transition on the enter function, or do I need to work around to use d3.select
d3.json("data/buildings.json").then(function(data){
data.forEach(function(d){
d.height = +d.height;
});
console.log(data);
var svg = d3.select("#chart-area").append("svg")
.attr("width", 400)
.attr("height", 400);
var rectangles = svg.selectAll("rect")
.data(data);
rectangles.enter()
.append("rect")
.attr("x", function(d,i){
return (i * 50) + 25;
})
.attr("y", 25)
.attr("width", 40)
.attr("height",function(d){
return d.height;
})
.attr("fill", "grey")
})
The simple answer is no, for a transition you would need to define two states: the initial state and the final state of the animation. Using the enter - update - exit cycle of d3 you could end up with something like this:
the rectangles fly in from the center of the SVG changing their sizes and color in one smooth transition.
The enter phase sets the initial state of the transition, the update phase performs the changes during the transition to reach the final state. Exit is not really needed for this example. It would take care of removing nodes that no longer exist after the update phase.
There are plenty of good examples and a tutorial about the topic over at https://bl.ocks.org for further reading.
d3.json("data/buildings.json").then(function(data){
data.forEach(function(d){
d.height = +d.height;
});
console.log(data);
var width = 400;
var height = 400;
var svg = d3.select("#chart-area").append("svg")
.attr("width", width)
.attr("height", height);
var rectangles = svg.selectAll("rect")
.data(data);
var rectEnter = rectangles.enter()
.append('rect')
.attr('x', width/2)
.attr('y', height/2)
.attr('width', 1e-6)
.attr('height', 1e-6)
.attr('fill', 'red')
var rectUpdate = rectEnter.merge(rectangles)
rectUpdate.transition()
.duration(1500)
.attr('x', function(d,i) { return (i * 50) + 25 })
.attr('y', 25)
.attr('width', 40)
.attr('height', function(d) { return d.height })
.attr('fill', 'grey')
var rectExit = rectangles.exit().remove()
})
and the dataset buildings.json
[
{
"id": 1,
"height": 20
}, {
"id": 2,
"height": 40
}, {
"id": 3,
"height": 10
}
]

Transitioning a bar chart with negative values for the width

I am creating a horizontal bar chart using d3. And I am using an animation to "grow" the chart at startup. Here is the code.
// Create the svg element
d3.select("#chart-area")
.append("svg")
.attr("height", 800)
.attr("width", 800);
.data(dataValues) // This data is previously prepared
.enter().append("rect")
.style("fill", "blue")
.attr("x", function () { return xScale(0); }) // xScale is defined earlier
.attr("y", function (d) { return yScale(d); }) // yScale is defined earlier
.attr("height", yScale.bandwidth()) // yScale is defined earlier
// Initial value of "width" (before animation)
.attr("width", 0)
// Start of animation transition
.transition()
.duration(5000) // 5 seconds
.ease (d3.easeLinear);
// Final value of "width" (after animation)
.attr("width", function(d) { return Math.abs(xScale(d) - xScale(0)); })
The above code would work without any problem, and the lines would grow as intended, from 0 to whichever width, within 5 seconds.
Now, if we change the easing line to the following
// This line changed
.ease (d3.easeElasticIn);
Then, the ease would try to take the width to a negative value before going to a final positive value. As you can see here, d3.easeElasticIn returns negative values as time goes by, then back to positive, resulting in width being negative at certain points in the animation. So the bars do not render properly (because SVG specs state that if width is negative, then use 0)
I tried every solution to allow the bars to grow negatively then back out. But could not find any. How can I fix this problem?
Thanks.
As you already know, the use of d3.easeElasticIn in your specific code will create negative values for the rectangles' width, which is not allowed.
This basic demo reproduces the issue, the console (your browser's console, not the snippet's console) is populated with error messages, like this:
Error: Invalid negative value for attribute width="-85.90933910798789"
Have a look:
const svg = d3.select("svg");
const margin = 50;
const line = svg.append("line")
.attr("x1", margin)
.attr("x2", margin)
.attr("y1", 0)
.attr("y2", 150)
.style("stroke", "black")
const data = d3.range(10).map(function(d) {
return {
y: "bar" + d,
x: Math.random()
}
});
const yScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.y
}))
.range([0, 150])
.padding(0.2);
const xScale = d3.scaleLinear()
.range([margin, 300]);
const bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", margin)
.attr("width", 0)
.style("fill", "steelblue")
.attr("y", function(d) {
return yScale(d.y)
})
.attr("height", yScale.bandwidth())
.transition()
.duration(2000)
.ease(d3.easeElasticIn)
.attr("width", function(d) {
return xScale(d.x) - margin
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
So, what's the solution?
One of them is catching those negative values as they are generated and, then, moving the rectangle to the left (using the x attribute) and converting those negative numbers to positive ones.
For that to work, we'll have to use attrTween instead of attr in the transition selection.
Like this:
.attrTween("width", function(d) {
return function(t){
return Math.abs(xScale(d.x) * t);
};
})
.attrTween("x", function(d) {
return function(t){
return xScale(d.x) * t < 0 ? margin + xScale(d.x) * t : margin;
};
})
In the snippet above, margin is just a margin that I created so you can see the bars going to the left of the axis.
And here is the demo:
const svg = d3.select("svg");
const margin = 100;
const line = svg.append("line")
.attr("x1", margin)
.attr("x2", margin)
.attr("y1", 0)
.attr("y2", 150)
.style("stroke", "black")
const data = d3.range(10).map(function(d) {
return {
y: "bar" + d,
x: Math.random()
}
});
const yScale = d3.scaleBand()
.domain(data.map(function(d) {
return d.y
}))
.range([0, 150])
.padding(0.2);
const xScale = d3.scaleLinear()
.range([0, 300 - margin]);
const bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
.attr("x", margin)
.attr("width", 0)
.style("fill", "steelblue")
.attr("y", function(d) {
return yScale(d.y)
})
.attr("height", yScale.bandwidth())
.transition()
.duration(2000)
.ease(d3.easeElasticIn)
.attrTween("width", function(d) {
return function(t) {
return Math.abs(xScale(d.x) * t);
};
})
.attrTween("x", function(d) {
return function(t) {
return xScale(d.x) * t < 0 ? margin + xScale(d.x) * t : margin;
};
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

Why is d3 ignoring my color array?

I'm trying to build out a simple color chart, as an introductory d3 exercise, and I'm already stuck.
I have the following:
var colors = ["#ffffcc","#c7e9b4","#7fcdbb","#41b6c4","#2c7fb8","#253494"];
var barHeight = 20,
barWidth = 20,
width = (barWidth + 5) * colors.length;
d3.select("body").selectAll("svg")
.data(colors)
.enter().append("rect")
.attr("class", "block")
.attr("width", barWidth)
.attr("height", barHeight - 1)
.text(function(d) { return d; })
.attr("fill", function(d) { return d; });
https://jsfiddle.net/xryamdkf/1/
The text works fine. I see the hex codes, but the height and width are definitely not respected, and I can't seem to set the color.
This works to set the color: .style("background", function(d) { return d; }) but I think that is the text background, not the rect fill.
What am I doing wrong here? How can I make 20x20 rectangles filled with color in d3?
As you are not giving any index and reference of colors array into your function the code will not understand from where to pick colors. try with below code it will help.
d3.select("body").selectAll("svg")
.data(colors).enter().append("rect")
.attr("class", "block")
.attr("width", barWidth)
.attr("height", barHeight - 1)
.text(function(d) {
return d;
})
.attr("fill", function(d,i) { return colors[i]; });
So, a few things. You should call data() on what will be an empty selection of the things you will be adding.
svg.selectAll("rect").data(colors)
.enter().append("rect")
The rect doesn't have a text property. There is an svg text node that shows text and you'll want to add it separately.
I hope this https://jsfiddle.net/xryamdkf/8/ gets you closer.

Categories