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();
}
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 have a stacked bar chart made in D3 v5.
https://codepen.io/bental/pen/oMbrjL
I haven't quite got my head around the update pattern, the bars overwrite.
I think I have to grab the inner rects on the update, but I cant quite make this work. I'm sure my misunderstanding and error lies in the plotArea() function
function plotArea() {
const stackedData = d3.stack().keys(keys)(data);
const layer = svg.append('g')
.selectAll('g')
.data(stackedData);
layer
.transition()
.attr('x', (d) => graphAxes.x(d.data.interval))
.attr('y', (d) => graphAxes.y(d[1]))
.attr('height', (d) => graphAxes.y(d[0]) - graphAxes.y(d[1]))
.attr('width', graphAxes.x.bandwidth());
layer
.enter().append('g')
.attr('class', d => 'data-path type-' + d.key)
.selectAll('rect')
.data(d => d)
.enter().append('rect')
.attr('x', (d) => {
return graphAxes.x(d.data.interval)
})
.attr('y', (d) => graphAxes.y(d[1]))
.attr('width', graphAxes.x.bandwidth())
.attr('height', (d) => graphAxes.y(d[0]) - graphAxes.y(d[1]));
layer.exit()
.transition()
.delay(function(d, i) {
return 30 * i;
})
.duration(1500)
.attr('y', graphAxes.y(0))
.attr('height', graphDimensions.height - graphAxes.y(0))
.remove();
}
Any help appreciated
I have the following enter / update / exit phases defined.
// this.x = my x time scale
// this.y = my y scale
// this.c = a color scale with 2 colors (red,blue)
// this.chart = D3.select() element
let series = D3.stack().keys(['point', 'topPoint'])(<any[]>this.barData);
this.chart
.append('g')
.selectAll('g')
.data(series)
.enter().append('g')
.attr('class', (d) => {return d.key + ' layer';})
.attr('fill', (d) => {return this.c(d.key);})
.selectAll('.bar')
.data((d) => {return d;})
.enter()
.append('rect')
.attr('class', 'bar');
// Update Phase
this.chart.selectAll('.bar').transition()
.attr('x', (d) => {return this.x(this._parseTime(d.data.date));})
.attr('y', (d) => {return this.y(d[1]); })
.attr('height', (d) => {return this.y(d[0]) - this.y(d[1]);})
.attr('width', 15);
// Exit phase
this.chart.selectAll('.point.layer').selectAll('.bar').exit().remove();
this.chart.selectAll('.topPoint.layer').selectAll('.bar').exit().remove();
When the data changes, the new bars are drawn, but they are drawn over the old bars.
if you use d3 v4 try this:
let series = D3.stack().keys(['point', 'topPoint'])(<any[]>this.barData);
const elements = this.chart
.append('g')
.selectAll('g')
.data(series);
elements.enter().append('g')
.attr('class', (d) => {return d.key + ' layer';})
.attr('fill', (d) => {return this.c(d.key);})
.each(function(d){
d3.select(this)
.append('rect')
.attr('class', 'bar');
})
.merge(elements) // updatePhase
.each(function(d){
d3.select(this).select(".bar")
.transition()
.attr('x', (d) => {return this.x(this._parseTime(d.data.date));})
.attr('y', (d) => {return this.y(d[1]); })
.attr('height', (d) => {return this.y(d[0]) - this.y(d[1]);})
.attr('width', 15);
}
// Exit phase
elements.exit().remove();
So the problem was my selecting of the elements I wish to bind and unbind.
this.chart
.selectAll('.layer')
.data(series)
.enter()
.append('g')
.attr('class', (d) => {return d.key + ' layer';});
// Set the enter phase for the bars within the groups, with the data derived from the layer data binding
this.chart.selectAll('.layer')
.selectAll('.bar')
.data((d) => {return d;})
.enter()
.append('rect')
.attr('class', 'bar');
// Set the update phase for the layers to fill the groups with the relevant color
let layers = this.chart.selectAll('.layer').attr('fill', (d) => {return this.c(d.key);});
// Update Phase
let bars;
if(this.animate) {
// Set the update phase of the bar data based on the data derived from the layer update phase
bars = layers.selectAll('.bar').data((d) => {return d;}).transition();
} else {
bars = layers.selectAll('.bar').data((d) => {return d;});
}
// Set the update phase of the bar data based on the data derived from the layer update phase
bars.attr('x', (d) => {return this.x(this._parseTime(d.data.date));})
.attr('y', (d) => {return this.y(d[1]); })
.attr('height', (d) => {return this.y(d[0]) - this.y(d[1]);})
.attr('width', 15);
// Exit phase
this.chart.selectAll('.layer').data(series).exit().remove();
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/
hello world> I have been battling this problem for a while now. Im trying to create a bar graph that will take an array of objects as data with time being a date object and value being a number.
My Scale looks like this
d3.time.scale.utc()
.domain(d3.extent(data, function (d) { return d.time; }))
.rangeRound([20, this.state.width - 60]).nice(data.length);
My rectangles are being drawn like this, using the same scale
const self = this,
xScale = this.state.xScale,
yScale = this.state.yScale,
barWidth = this.getBarWidth(data.length),
bars = chart.selectAll('rect')
.data(data);
// UPDATE
bars
.transition()
.duration(500)
.style('fill', color)
.attr('x', function(d) {
console.log(xScale(d.time)- (barWidth / 2));
return xScale(d.time) - (barWidth / 2);
})
.attr('width', barWidth)
.attr('y', function(d) { return yScale(d.value); })
.attr('height', function(d) { return self.state.height - yScale(d.value); });
// ENTER
bars
.enter()
.append('rect')
.style('fill', color)
.attr('class', 'bar')
.attr('x', function(d) {
console.log(xScale(d.time) - barWidth);
return xScale(d.time) - barWidth;
})
.attr('width', barWidth + (barWidth / data.length))
.attr('y', function(d) { return yScale(d.value); })
.attr('height', function(d) { return self.state.height - yScale(d.value); });
// EXIT
bars
.exit()
.transition()
.duration(500)
.style('fill', 'red')
.style('opacity', 0)
.remove();
I get the problem below where the ticks have some length and the axis another and the tick marks don't match the bars.
Please friends, help me find a solution to the below problem.
My bar problem