Apply transition while keeping the old elements' positions - javascript

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>

Related

D3JS: set colour gradient above a certain threshold

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>

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.

Using D3 in Jupyter - Text not displayed [duplicate]

This question already has answers here:
D3 Appending Text to a SVG Rectangle
(2 answers)
Display text on rect using D3.js
(2 answers)
SVG: text inside rect
(5 answers)
Closed 2 years ago.
I am currently trying to get D3 running in Jupyter Notebook, kind of following this guide: https://www.stefaanlippens.net/jupyter-custom-d3-visualization.html
Using the following code i wanted to add numbers to the bars in the bar chart:
%%javascript
(function(element) {
require(['d3'], function(d3) {
var data = [1, 2, 4, 8, 16, 8, 4, 2, 1]
var svg = d3.select(element.get(0)).append('svg')
.attr('width', 400)
.attr('height', 200);
svg.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('width', function(d) {return d*10})
.attr('height', 24)
.attr('x', 0)
.attr('y', function(d,i) {return i*30})
.style('fill', 'darkgreen')
svg.selectAll('rect')
.append('text')
.text('mynumberhere')
.attr('color', 'FF0000')
})
})(element);
Currently only the bar chart is displayed, but no numbers are displayed. Using the HTML inspector though, i can see that inside the element is a element. Although it is not diplayed. Any ideas why that could be?
It's not possible to append a text to a rect element, they're not meant to have children! You can choose to use g-groups instead, and put both the rect and the text inside:
(function(element) {
var data = [1, 2, 4, 8, 16, 8, 4, 2, 1]
var svg = d3.select(element).append('svg')
.attr('width', 400)
.attr('height', 200);
var g = svg.selectAll('g')
.data(data)
.enter()
.append('g');
g.append('rect')
.attr('width', function(d) {
return d * 10
})
.attr('height', 24)
.attr('x', 0)
.attr('y', function(d, i) {
return i * 30
})
.style('fill', 'darkgreen')
g
.append('text')
.text(function(d) { return d; })
.attr('color', 'FF0000')
.attr('dy', 16)
.attr('dx', 3)
.attr('x', function(d) {
return d * 10
})
.attr('y', function(d, i) {
return i * 30
})
})(document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

How to append 2 items in a d3 v4 force layout that updates?

I have a d3 v4 force layout and currently in my updateForceLayout function that I call when I add or remove a node I have this:
// Add and remove message nodes
const join = d3
.select(".container")
.selectAll("g")
.data(data);
// Remove old groups
join
.exit()
.remove();
// Create the new groups
const groups = join
.enter()
.append("g")
.attr("class", "outer");
// Merge the new groups with the existing groups
// and apply an appropriate translation
const innerJoin = groups
.merge(join)
.attr("transform", d => `translate(${d.x},${d.y})`)
.selectAll(".inner")
.data(d => d.m);
// Remove old ones
innerJoin
.exit()
.remove()
let newCircles = innerJoin
.enter()
.append('rect')
.attr("class", "innerbox")
.attr("y", (d,i) => {
return -65 - (i * 15)
})
.attr("width", 70)
.attr("height", 10)
.style("fill", "green")
newCircles.merge(join)
This is a nested selection so it's a bit more complex but the problem is simple and only lies in the last part. I want to be able to add text and a rect to "newCircles", but can only add one or the other. When I try this at the end instead:
let newCircles = innerJoin
.enter()
.append('g')
newCircles
.append('rect')
.attr("class", "innerbox")
.attr("y", (d,i) => {
return -65 - (i * 15)
})
.attr("width", 70)
.attr("height", 10)
.style("fill", "green")
newCircles
.append("text")
.attr("class", "innertext")
.text(function(d){return d})
.attr("y", (d,i) => {
return -55 - (i * 15)
})
.style("white-space", "pre");
My force layout bugs out and one of the nodes jumps off of the screen when I update the nodes. I am assuming I am not adding or removing or merging correctly now that I have a group with 2 elements inside and not just 1 element. What is the correct way to achieve this?

Add labels to D3 Chord diagram

I'm a rookie programmer, so this one will probably be an easy one for most of you. What lines of code do I need for labels and/or mouse-over text for this Chord diagram?
http://mbostock.github.com/d3/ex/chord.html
I need it to display the name of the category in the outer strip. When you mouse over, I want to display the exact number and both categories. Something like this: 'A: 5 thing from B'.
EDIT:
I still can't figure out how to implement it in my code. Can someone fill in my example code an explain what's going on?
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Selecties EK 2010</title>
<script type="text/javascript" src="d3.v2.js"></script>
<link type="text/css" rel="stylesheet" href="ek2010.css"/>
</head>
<body>
<div id="chart"></div>
<script type="text/javascript" src="ek2010.js"></script>
</body>
</html>
and
// From http://mkweb.bcgsc.ca/circos/guide/tables/
var chord = d3.layout.chord()
.padding(.05)
.sortSubgroups(d3.descending)
.matrix([
[0, 0, 7, 5],
[0, 0, 8, 3],
[7, 8, 0, 0],
[5, 3, 0, 0]
]);
var width = 1000,
height = 1000,
innerRadius = Math.min(width, height) * .3,
outerRadius = innerRadius * 1.1;
var fill = d3.scale.ordinal()
.domain(d3.range(4))
.range(["#000000", "#FFDD89", "#957244", "#F26223"]);
var svg = d3.select("#chart")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
svg.append("g")
.selectAll("path")
.data(chord.groups)
.enter().append("path")
.style("fill", function(d) { return fill(d.index); })
.style("stroke", function(d) { return fill(d.index); })
.attr("d", d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius))
.on("mouseover", fade(.1))
.on("mouseout", fade(1));
var ticks = svg.append("g")
.selectAll("g")
.data(chord.groups)
.enter().append("g")
.selectAll("g")
.data(groupTicks)
.enter().append("g")
.attr("transform", function(d) {
return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
+ "translate(" + outerRadius + ",0)";
});
ticks.append("line")
.attr("x1", 1)
.attr("y1", 0)
.attr("x2", 5)
.attr("y2", 0)
.style("stroke", "#000");
ticks.append("text")
.attr("x", 8)
.attr("dy", ".35em")
.attr("text-anchor", function(d) {
return d.angle > Math.PI ? "end" : null;
})
.attr("transform", function(d) {
return d.angle > Math.PI ? "rotate(180)translate(-16)" : null;
})
.text(function(d) { return d.label; });
svg.append("g")
.attr("class", "chord")
.selectAll("path")
.data(chord.chords)
.enter().append("path")
.style("fill", function(d) { return fill(d.target.index); })
.attr("d", d3.svg.chord().radius(innerRadius))
.style("opacity", 1);
/** Returns an array of tick angles and labels, given a group. */
function groupTicks(d) {
var k = (d.endAngle - d.startAngle) / d.value;
return d3.range(0, d.value, 1).map(function(v, i) {
return {
angle: v * k + d.startAngle,
label: i % 5 ? null : v / 1 + " internat."
};
});
}
/** Returns an event handler for fading a given chord group. */
function fade(opacity) {
return function(g, i) {
svg.selectAll("g.chord path")
.filter(function(d) {
return d.source.index != i && d.target.index != i;
})
.transition()
.style("opacity", opacity);
};
}
Add text elements to display labels. Alternatively, use textPath elements if you want to display text along a path. Two examples of labeled chord diagrams:
http://mbostock.github.com/d3/talk/20111018/chord.html
http://bl.ocks.org/1308257
You need to look at the (selection.on()) event handler in the d3.js wiki on Github. That shows you how to add events to elements including mouseover and mouseout. If you look at that example you linked to, you can see an instance of it already:
svg.append("g")
.selectAll("path")
.data(chord.groups)
.enter().append("path")
.style("fill", function(d) { return fill(d.index); })
.style("stroke", function(d) { return fill(d.index); })
.attr("d", d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius))
.on("mouseover", fade(.1))
.on("mouseout", fade(1));
If you hover the mouse over the chord groups in the outer ring you will see all the other chord groups fade out.
If you want labels to pop-up that contain strings (text) you will need to define them using another JS library. One I know that works is Tipsyand there is an example using it together with d3 here. You should then be able to simply use a selector to choose which SVG element you want to illustrate this behavior.
Hope that helps.

Categories