I have this piece of code in which circles are drawn, I need to put a text inside each circle, I would also like to know how I can put a certain size to each of the elements of the circle.
Thank you very much.
svg = d3.select(selector)
.append('svg')
.attr('width', width)
.attr('height', height);
// Bind nodes data to what will become DOM elements to represent them.
bubbles = svg.selectAll('.bubble')
.data(nodes, function (d) { return d.id; });
// Create new circle elements each with class `bubble`.
// There will be one circle.bubble for each object in the nodes array.
// Initially, their radius (r attribute) will be 0.
bubbles.enter().append('circle')
.classed('bubble', true)
.attr('r', 0)
.attr('fill', function (d) { return fillColor(d.group); })
.attr('stroke', function (d) { return d3.rgb(fillColor(d.group)).darker(); })
.attr('stroke-width', 2)
.on('mouseover', showDetail)
.on('mouseout', hideDetail);
// Fancy transition to make bubbles appear, ending with the
// correct radius
bubbles.transition()
.duration(2000)
.attr('r', function (d) { return d.radius; });
A good practice would be to create a group element for each bubble because they will be composed of two elements - a circle and text.
// Bind nodes data to what will become DOM elements to represent them.
bubbles = svg.selectAll('.bubble')
.data(nodes, function(d) {
return d.id;
})
.enter()
.append('g')
.attr("transform", d => `translate(${d.x}, ${d.y})`)
.classed('bubble', true)
.on('mouseover', showDetail)
.on('mouseout', hideDetail)
After that, circles and texts can be appended:
circles = bubbles.append('circle')
.attr('r', 0)
.attr('stroke-width', 2)
texts = bubbles.append('text')
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.style('font-size', d => d.radius * 0.4 + 'px')
.attr('fill-opacity', 0)
.attr('fill', 'white')
.text(d => d.text)
// Fancy transition to make bubbles appear, ending with the
// correct radius
circles.transition()
.duration(2000)
.attr('r', function(d) {
return d.radius;
});
For hiding/showing text, you can use fill-opacity attribute and set it 0 when the text should be hidden, and 1 if it should be shown:
function showDetail(d, i) {
d3.select(this.childNodes[1]).attr('fill-opacity', 1)
}
function hideDetail(d, i) {
d3.select(this.childNodes[1]).attr('fill-opacity', 0)
}
example: https://jsfiddle.net/r880wm24/
Related
Here is a demo
When new data hits my d3 service it loads the new data set but the old isn't removed. Therefore I have duplicate nodes inside its parent node 'g' element. New to d3, however I've done lots of reading around selection.join() instead of enter().append(). I've also read up on ways to add node.exit().remove(); and node.merge(node); at specific points.
As you can see from the dom, all new node properties are in the <g class="node"> element, duplicated, not replacing the original data. Therefore I get a overlapping of content.
Here is the way my nodes are built...
const zoomContainer = d3.select('svg g');
const node = zoomContainer.selectAll('g').data(nodes, function (d) {
return d.id;
});
//zoomContainer.selectAll('.node').data(node).exit().remove();
const nodeEnter = node
.join('g')
.attr('class', 'node')
.call(
d3
.drag()
.on('start', (d) => this.dragended(d3, d, simulation))
.on('drag', function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
})
.on('end', (d) => this.dragended(d3, d, simulation))
);
nodeEnter
.append('circle')
.style('fill', '#fff')
.style('cursor', 'pointer')
.style('fill-opacity', '1')
.style('stroke-opacity', '0.5')
.attr('id', (d, i) => d.id)
.attr('r', 28);
nodeEnter
.append('image')
.attr('xlink:href', 'https://github.com/favicon.ico')
.attr('x', -15)
.attr('y', -60)
.attr('width', 16)
.attr('class', 'image')
.style('cursor', 'pointer')
.attr('height', 16);
const nodeText = nodeEnter
.data(nodes)
.append('text')
.style('text-anchor', 'middle')
.style('cursor', 'pointer')
.attr('dy', -3)
.attr('y', -25)
.attr('class', 'nodeText')
.attr('id', 'nodeText');
nodeText
.selectAll('tspan')
.data((d, i) => d.label)
.join('tspan')
.attr('class', 'nodeTextTspan')
.text((d) => d)
.style('font-size', '12px')
.attr('x', -10)
.attr('dx', 10)
.attr('dy', 15);
I probably could clear the graph by force but I like and need the way .join() can compare what's changed and the options to use enter().append().exit(). If anybody can see why duplicates are not being removed/merged I would appreciate it.
UPDATE:
If I use enter().append('g') instead of join('g') I then get a better result. I can use zoomContainer.selectAll('.node').data(node).exit().remove(); before hand and my nodes do get updated but only after clicking update twice. If I use join('g') they duplicate and I am unable to use zoomContainer.selectAll('.node').data(node).exit().remove();
Here is a demo
Targeting the <g> element rather than the class and then using exit().remove() seemed to have done the trick... I was adding a class attribute at the .enter() level in .join() and then doing the exit on that. Demo here
const node = zoomContainer
.selectAll('.node')
.data(this.nodes, function (d) {
return d.id;
});
zoomContainer.selectAll('g').data(node).exit().remove();
I am trying to add some text at the end of the bars of a d3js bar chart.
The bar chart has transition with a delay. The source code can be found here https://bl.ocks.org/deciob/ffd5c65629e43449246cb80a0af280c7.
Unfortunately, with my code below the text does not follow the bars and I am not sure what I am doing wrong.
I thought the append text should be placed in the drawBars function no?
function drawBars(el, data, t) {
let barsG = el.select('.bars-g')
if (barsG.empty()) {
barsG = el.append('g')
.attr('class', 'bars-g');
}
const bars = barsG
.selectAll('.bar')
.data(data, yAccessor);
bars.exit()
.remove();
bars.enter()
.append('rect')
.attr('class', d => d.geoCode === 'WLD' ? 'bar wld' : 'bar')
.attr('x', leftPadding)
.attr('fill', function (d) {return d.geoColor;})
bars.enter()
.append('text')
.attr('x', d => xScale(xAccessor(d)))
.attr('y', d => yScale(yAccessor(d)))
.text('Hello')
.merge(bars).transition(t)
.attr('y', d => yScale(yAccessor(d)))
.attr('width', d => xScale(xAccessor(d)))
.attr('height', yScale.bandwidth())
.delay(delay)
}
What I am trying to achieve is for the text to follow the bars (and also later for the text to be updated to another value).
Thanks for any kind of help.
Found the answer, for anyone wondering you need to create a new function (eg: drawText()) and call it in later just below where the drawBars() function is called:
function drawText(el, data, t) {
var labels = svg.selectAll('.label')
.data(data, yAccessor);
var new_labels = labels
.enter()
.append('text')
.attr('class', 'label')
.attr('opacity', 0)
.attr('y', d => yScale(yAccessor(d)))
.attr('fill', 'blue')
.attr('text-anchor', 'middle')
new_labels.merge(labels)
.transition(t)
.attr('opacity', 1)
.attr('x', d => xScale(xAccessor(d))+50)
.attr('y', d => yScale(yAccessor(d)))
.text(function(d) {
return d.value;
});
labels
.exit()
.transition(t)
.attr('y', height)
.attr('opacity', 0)
.remove();
}
I have some data with 2 attributes: colour and value
I use the D3 enter selection to create circle elements, and append them to the body of the page. Their fill colour is determined by the "colour" attribute.
Then, I append text elements to the page. The text contents are determined by the "value" attribute.
Here is what I am working with:
// Set up svg element
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 300)
.style("background", "lightblue");
var dataset = [
{"colour":"red", "value":"First set of text"},
{"colour":"green", "value":"Second attempt"},
{"colour":"blue", "value":"Third and final!"}
];
// Create circles from the data
// On mouseover, give them a border (remove on mouseout)
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("r", 40)
.attr("cy", function(d, i) { return i*80 + 40; })
.attr("cx", 50)
.style("fill", function(d) {return d.colour;})
// HERE
// Can I somehow show and hide the text component that is
// associated with this circle when the circle is hovered, rather
// than the text itself?
.on("mouseover", function(d) {
d3.select(this).style("stroke", "black")
.style("stroke-width", 2)
})
.on("mouseout", function(d) {d3.select(this).style("stroke", "none")});
// Now add the text for each circle
// Same thing with mouseover and mouseout
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("y", function(d, i) { return i*80 + 40; })
.attr("x", 50)
.style("opacity", 0)
.on("mouseover", function(d) {d3.select(this).style("opacity", 1)})
.on("mouseout", function(d) {d3.select(this).style("opacity", 0)})
.text(function(d) { return d.value;});
I would like for the text to be hidden, until the associated circle is hovered over. How can I connect the text element with a particular circle, so that I can toggle whether the text is shown by hovering over the circle?
This fiddle below is an outline of what I am trying to do, and what I have got so far. I have the text showing up only when hovered, but not when the circle is hovered.
https://jsfiddle.net/aj4zpn6z/
There are several ways for achieving this. Since both circles and texts use the same dataset, my solution uses filter.
First, let's name the variables for the texts and circles:
var circles = svg.selectAll("circle")
//etc...
var texts = svg.selectAll("text")
//etc...
Then, inside the circles mouseover function, we filter the texts that have the same colour attribute:
.on("mouseover", function(d){
d3.select(this).style("stroke", "black").style("stroke-width", 2);
var tempTexts = texts.filter(function(e){
return e.colour === d.colour
});
tempTexts.style("opacity", 1);
});
This is your updated fiddle: https://jsfiddle.net/wxh95e9u/
I am making grouped bar chart based on Mike Bostock's tutorial.
I can't figure out how to put circles on top of my bars to act as tooltip when hovering, just like in this tutorial except it's on bars and not on a line.
I tried appending the circles like this :
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d) { return x1(d.name); })
.attr("cy", function(d) { return y(d.value); })
});
But I get NaN values. I am very confused about which variable I should use to get the right cx and cy.
Here is my code.
Any ideas ?
Thank you
You will get NaN values since your data join is not correct, you are trying to get values that are not currently present in your data. In order to get those values you would need to make a reference to data.years.
Here is my approach:
// Inheriting data from parent node and setting it up,
// add year to each object so we can make use for our
// mouse interactions.
year.selectAll('.gender-circles')
.data(function(data) {
return data.years.map(function(d) {
d.year = data.year;
return d;
})
})
.enter().append('circle')
.attr("class", function(d) {
return "gender-circles gender-circles-" + d.year;
})
.attr("r", 10)
.attr('cx', function(d) {
console.log(d)
return x1(d.name) + 6.5;
})
.attr('cy', function(d) {
return y(d.value) - 15;
})
.style('display', 'none'); // default display
// ....
// Using an invisible rect for mouseover interactions
year.selectAll('.gender-rect-interaction')
.data(function(d) { // Inheriting data from parent node and setting it up
return [d];
})
.enter().append('rect')
.attr("width", x0.rangeBand()) // full width of x0 rangeband
.attr("x", function(d) {
return 0;
})
.attr("y", function(d) {
return 0;
})
.attr("height", function(d) { // full height
return height;
})
.style('opacity', 0) // invisible!
.on('mousemove', function(d) { // show all our circles by class
d3.selectAll('.gender-circles-' + d.year)
.style('display', 'block');
})
.on('mouseout', function(d) { // hide all our circles by class
d3.selectAll('.gender-circles-' + d.year)
.style('display', 'none');
});
Working plnkr: https://plnkr.co/edit/oH4KXdxdIW82nLGv46NI?p=preview
Any help would be greatly appreciated.
Basically, the mouseover was working fine until I added a transition to the line chart. The transition takes the circles' opacity from zero to one.
var dots = svg.selectAll('circle')
.data(data)
.enter()
.append('svg:circle')
.attr('cx', function(d, i){ return ((width - tickOffset) / (data.length - 1)) * i; })
.attr('cy', function(d){ return y(d.value); })
.attr('r', 4)
.attr('class', 'circle')
.style('opacity', 0)
.transition()
.duration(circleAnimation)
.delay(function(d,i){ return i * (circleAnimation / 4); })
.style('opacity', 1);
dots.on('mouseover', function(d, i){
// show tooltip
})
.on('mouseout', function(d, i){
// hide tooltip
});
When I implement the transition, the console throws the following error
TypeError: 'undefined' is not a function (evaluating 'dots.on')
This same issue was happening on a bar chart I just created and simply stopping the method chaining and starting it again fixed the problem. That's why in this example I've stopped the method chaining and started it again with "dots.on('mouseover..."
As soon as you call .transition(), the selection you have becomes a transition. This is what you save in dots and then try to call .on() on. Instead, save the selection and set the transition and event handlers on it:
var dots = svg.selectAll('circle')
.data(data)
.enter()
.append('svg:circle')
.attr('cx', function(d, i){ return ((width - tickOffset) / (data.length - 1)) * i; })
.attr('cy', function(d){ return y(d.value); })
.attr('r', 4)
.attr('class', 'circle')
.style('opacity', 0);
dots.transition()
.duration(circleAnimation)
.delay(function(d,i){ return i * (circleAnimation / 4); })
.style('opacity', 1);
dots.on('mouseover', function(d, i){
// show tooltip
})
.on('mouseout', function(d, i){
// hide tooltip
});