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
Related
In the following code I tried to create a visualization for a market on which one buys per hour. I tried to follow v5s update pattern but it won't let me join two text different <text> elements. The last added join overwrites the first so 8
I've looked around but I can not find anything related to an update pattern for two of the same elements.
https://jsfiddle.net/itsahoax/gd2uew73/7/
const updateCircles = () => {
const circles = d3.select('svg')
.selectAll('circle');
circles
.data(dataPoints)
.join('circle')
.attr('cx', xPosition)
.attr('cy', canvasHeight)
.attr('r', circleRadius)
.attr('id', (d) => d.uniqueid)
.attr('fill', (d) => d.color);
const text = d3.select('svg')
.selectAll('text')
.data(dataPoints);
text
.join()
.attr('x', xPosition)
.attr('y', canvasHeight)
.attr('id', (d) => d.uniqueid)
.text((d) => d.description);
text
.join()
.attr('x', xPosition)
.attr('y', canvasHeight + 15)
.attr('id', (d) => d.uniqueid)
.text((d) => `${d.value} KwH`);
};
if (update === true) {
updateCircles();
} else {
const circles = selection.selectAll('circle')
.data(dataPoints, (d) => d.id);
const text = selection.selectAll('text')
.data(dataPoints);
circles
.enter().append('circle')
.attr('cx', xPosition)
.attr('cy', canvasHeight)
.attr('r', circleRadius)
.attr('id', (d) => d.uniqueid)
.attr('fill', (d) => d.color)
.merge(circles);
text
.enter().append('text')
.attr('x', xPosition)
.attr('y', canvasHeight)
.attr('id', (d) => d.uniqueid)
.merge(text)
.text((d) => d.description);
text
.enter().append('text')
.attr('x', xPosition)
.attr('y', canvasHeight + 15)
.attr('id', (d) => d.uniqueid)
.merge(text)
.text((d) => `${d.value} KwH`);
}
};
Do not use an element selector if you have multiple elements with different content with the same selector (e.g <text>). Add them class and use .selectAll('.className')
There is a working example using selection.join JSFiddle.
More information about selection.join here.
// render code
const circles = (selection, dataPoints, isUpdate) => {
const xPosition = (d, i) => +i * 180 + 100;
const updateCircles = (data) => {
const circles = d3.select('svg').selectAll('.circle-area').data(data);
circles
.join((enter) => {
enter
.append('circle')
.attr('class', 'circle-area')
.attr('cx', xPosition)
.attr('cy', canvasHeight)
.attr('r', circleRadius)
.attr('id', (d) => d.uniqueid)
.attr('fill', (d) => d.color);
}, (update) => {
update.attr('fill', (d) => d.color);
}, (exit) => {
exit.remove();
});
const descriptionText = d3.select('svg').selectAll('.kwh-description').data(data);
descriptionText
.join((enter) => {
enter
.append('text')
.attr('class', 'kwh-description')
.attr('x', xPosition)
.attr('y', canvasHeight)
.attr('id', (d) => `description-${d.uniqueid}`)
.text((d) => d.description);
}, (update) => {
update.text((d) => d.description);
}, (exit) => {
exit.remove();
});
const valueText = d3.select('svg').selectAll('.kwh-value').data(data);
valueText
.join((enter) => {
enter
.append('text')
.attr('class', 'kwh-value')
.attr('x', xPosition)
.attr('y', canvasHeight + 15)
.attr('id', (d) => `value-${d.uniqueid}`)
.text((d) => `${d.value} KwH`);
}, (update) => {
update.text((d) => `${d.value} KwH`);
}, (exit) => {
exit.remove();
});
};
if (isUpdate) {
console.log(dataPoints)
updateCircles(dataPoints);
}
};
The text for the cells in my d3 treemap don't wrap and overflow the other cells. This my project
I want the text to look like this project. I've looked at their code (and many others) but I can't get it to work in my project.
The problem area is:
svg.append('text')
.selectAll('tspan')
.data(root.leaves())
.enter()
.append('tspan')
.attr("x", (d) => d.x0 + 5)
.attr("y", (d) => d.y0 + 20)
.text( (d) => d.data.name) //.html( (d) => d.data.name.replace(/\s/g, "<br>"))
.attr("font-size", "0.6em")
.attr("fill", "white");
I tried using .html rather than .text as in the comment. In Safari and Chrome the text still overflowed the cells. In Firefox only the first word of the movie name was displayed.
We have two options to display the text ina similar way to the example you provide.
The first and easisest approach is to keep your code structure and make a similar procedure to split the text as the example provided:
d.data.name.split(/(?=[A-Z][^A-Z])/g)
So lets change your code a little bit:
svg.selectAll('text')
.data(root.leaves())
.enter()
.append('text')
.selectAll('tspan')
.data(d => {
return d.data.name.split(/(?=[A-Z][^A-Z])/g) // split the name of movie
.map(v => {
return {
text: v,
x0: d.x0, // keep x0 reference
y0: d.y0 // keep y0 reference
}
});
})
.enter()
.append('tspan')
.attr("x", (d) => d.x0 + 5)
.attr("y", (d, i) => d.y0 + 15 + (i * 10)) // offset by index
.text((d) => d.text)
.attr("font-size", "0.6em")
.attr("fill", "white");
This should accomplish the desired display. We have to take into account that labels are a very difficult to position and display in way which avoids overlapping since it would require a little more computation at build time.
The second approach is to change a little the code structure and create cells, pretty much like the example provided:
const cell = svg.selectAll('g')
.data(root.leaves())
.enter()
.append('g') // create a group for each cell / movie
.attr('transform', d => `translate(${d.x0},${d.y0})`) // let the group element handle the general positioning
.on('mousemove', d => {
//...
})
.on('mouseout', d => {
//...
});
cell.append('rect') // append rect for each cell / movie
.attr('id', d => d.data.id)
.attr('class', 'tile')
.attr('data-name', d => d.data.name)
.attr('data-value', d => d.data.value)
.attr('data-category', d => d.data.category)
.attr('width', d => d.x1 - d.x0)
.attr('height', d => d.y1 - d.y0)
.attr('fill', d => color(d.data.category));
cell.append('text') // append text node for each cell / movie
.selectAll('tspan')
.data(d => d.data.name.split(/(?=[A-Z][^A-Z])/g)) // split the name and use that as data to create indiviual tspan elements
.enter()
.append('tspan') // append tspan node for each element of the string which got split
.attr('font-size', '8px')
.attr('x', 4)
.attr('y', (d, i) => 13 + 10 * i) // offset the y positioning with the index of the data
.text(d => d);
CodePen for approach 1
CodePen for approach 2
Full code for approach 1:
// !! IMPORTANT README:
// You may add additional external JS and CSS as needed to complete the project, however the current external resource MUST remain in place for the tests to work. BABEL must also be left in place.
const w = 960;
const h = 600;
const padding = 60;
const svg = d3.select("#container").append("svg")
.attr("width", w).attr("height", h);
const legendsvg = d3.select("#legend").append("svg")
.attr("width", 960).attr("height", 50);
const legendPadding = 10;
d3.json("https://cdn.rawgit.com/freeCodeCamp/testable-projects-fcc/a80ce8f9/src/data/tree_map/movie-data.json")
.then(function(data) {
var root = d3.hierarchy(data).sum(function(d){ return d.value});
var treeMap = d3.treemap()
.size([w, h])
.paddingInner(1);
treeMap(root);
const toolTip = d3
.select("#container")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var color = d3.scaleOrdinal()
.domain(["Action", "Drama", "Adventure", "Family", "Animation", "Comedy", "Biography"])
.range(["#db8a00", "#75b0ff", "#13ad37", "#5d6d00", "#757582", "#d37cff", "#f96868"])
svg.selectAll("rect")
.data(root.leaves())
.enter().append("rect")
.attr("class", "tile")
.attr("data-name", (d) => d.data.name)
.attr("data-category", (d) => d.data.category)
.attr("data-value", (d) => d.data.value)
.attr('x', (d) => d.x0)
.attr('y', (d) => d.y0)
.attr('width', (d) => d.x1 - d.x0)
.attr('height', (d) => d.y1 - d.y0)
.style("stroke", "black")
.style("fill", (d) => color(d.parent.data.name))
.on("mouseover", (d, i) => {
toolTip
.transition()
.duration(0)
.style("opacity", 0.8);
toolTip
.attr("id", "tooltip")
.html(function() {
return "<span>" + "Name: " + d.data.name + "<br />" + "Category: " + d.data.category + "<br />" + "Value: " + d.data.value + "</span>";
})
.style("left", d3.event.pageX - 87.5 + "px") // -87.5 is half width of tooltip in css
.style("top", d3.event.pageY - 75 + "px")
.attr("data-value", d.data.value);
})
.on("mouseout", function(d) {
toolTip
.transition()
.duration(0)
.style("opacity", 0);
});
svg.selectAll('text')
.data(root.leaves())
.enter()
.append('text')
.selectAll('tspan')
.data(d => {
return d.data.name.split(/(?=[A-Z][^A-Z])/g) // split the name of movie
.map(v => {
return {
text: v,
x0: d.x0, // keep x0 reference
y0: d.y0 // keep y0 reference
}
});
})
.enter()
.append('tspan')
.attr("x", (d) => d.x0 + 5)
.attr("y", (d, i) => d.y0 + 15 + (i * 10)) // offset by index
.text((d) => d.text)
.attr("font-size", "0.6em")
.attr("fill", "white");
console.log(root.leaves());
/*svg.selectAll("text")
.data(root.leaves())
.enter()
.append("text")
.attr("x", function(d){ return d.x0+5})
.attr("y", function(d){ return d.y0+20})
.text(function(d){ return d.data.name })
.attr("font-size", "0.6em")
.attr("fill", "white")*/
legendsvg.selectAll('rect')
.data(root.children)
.enter()
.append('rect')
.attr('class', 'legend-item')
.style('stroke', 'white')
.attr('x', (d,i) => i*140 )
.attr('width', 130)
.attr('height', 20)
.style('fill', d => color(d.data.name))
legendsvg.selectAll('text')
.data(root.children)
.enter()
.append('text')
.attr('x', (d,i) => i*140)
.attr('y', 40)
.text(d => d.data.name);
//had to change the legend below because it wouldn't pass fcc test
/*legendsvg.append("g").classed("legend", true).classed("legend-item", true);
const legend = d3.legendColor().shape("rect")
.shapeWidth(90).cells(7).orient("horizontal").scale(color);
legendsvg.select(".legend").call(legend);*/
});
Full code for approach 2:
// !! IMPORTANT README:
// You may add additional external JS and CSS as needed to complete the project, however the current external resource MUST remain in place for the tests to work. BABEL must also be left in place.
const w = 960;
const h = 600;
const padding = 60;
const svg = d3.select("#container").append("svg")
.attr("width", w).attr("height", h);
const legendsvg = d3.select("#legend").append("svg")
.attr("width", 960).attr("height", 50);
const legendPadding = 10;
d3.json("https://cdn.rawgit.com/freeCodeCamp/testable-projects-fcc/a80ce8f9/src/data/tree_map/movie-data.json")
.then(function(data) {
var root = d3.hierarchy(data).sum(function(d){ return d.value});
var treeMap = d3.treemap()
.size([w, h])
.paddingInner(1);
treeMap(root);
const toolTip = d3
.select("#container")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var color = d3.scaleOrdinal()
.domain(["Action", "Drama", "Adventure", "Family", "Animation", "Comedy", "Biography"])
.range(["#db8a00", "#75b0ff", "#13ad37", "#5d6d00", "#757582", "#d37cff", "#f96868"])
const cell = svg.selectAll('g')
.data(root.leaves())
.enter()
.append('g')
.attr('transform', d => `translate(${d.x0},${d.y0})`)
.on('mousemove', d => {
toolTip.transition()
.duration(200)
.style('opacity', 0.75);
toolTip.attr('data-value', d.data.value);
toolTip.html(
'Name: ' + d.data.name + '<br>' +
'Category: ' + d.data.category + '<br>' +
'Value: ' + d.data.value
)
.style('top', `${d3.event.pageY + 10}px`)
.style('left', `${d3.event.pageX + 8}px`);
})
.on('mouseout', d => {
toolTip.transition()
.duration(200)
.style('opacity', 0);
});
cell.append('rect')
.attr('id', d => d.data.id)
.attr('class', 'tile')
.attr('data-name', d => d.data.name)
.attr('data-value', d => d.data.value)
.attr('data-category', d => d.data.category)
.attr('width', d => d.x1 - d.x0)
.attr('height', d => d.y1 - d.y0)
.attr('fill', d => color(d.data.category));
cell.append('text')
.selectAll('tspan')
.data(d => d.data.name.split(/(?=[A-Z][^A-Z])/g))
.enter()
.append('tspan')
.attr('font-size', '8px')
.attr('x', 4)
.attr('y', (d, i) => 13 + 10*i)
.text(d => d);
legendsvg.selectAll('rect')
.data(root.children)
.enter()
.append('rect')
.attr('class', 'legend-item')
.style('stroke', 'white')
.attr('x', (d,i) => i*140 )
.attr('width', 130)
.attr('height', 20)
.style('fill', d => color(d.data.name))
legendsvg.selectAll('text')
.data(root.children)
.enter()
.append('text')
.attr('x', (d,i) => i*140)
.attr('y', 40)
.text(d => d.data.name);
//had to change the legend below because it wouldn't pass fcc test
/*legendsvg.append("g").classed("legend", true).classed("legend-item", true);
const legend = d3.legendColor().shape("rect")
.shapeWidth(90).cells(7).orient("horizontal").scale(color);
legendsvg.select(".legend").call(legend);*/
});
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 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();
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