D3 in react is creating multiple graphs of value update - d3.js

I am trying to update the color of the graph, on updating, all the previous graphs are also visible
Here is my code:-
class BarChart extends Component {
state = {
color: "green",
};
componentDidUpdate = (prevProps, prevState) => {
if (prevState.color != this.props.color) {
this.drawChart();
}
};
drawChart() {
const data = [12, 5, 6, 6];
const svg = d3
.select("body")
.append("svg")
.attr("width", 400)
.attr("height", 400)
svg
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("x", (d, i) => i * 70)
.attr("y", (d, i) => 400 - 10 * d)
.attr("width", 65)
.attr("height", (d, i) => d * 10)
.attr("fill", this.props.color);
svg
.selectAll("text")
.data(data)
.enter()
.append("text")
.text((d) => d)
.attr("x", (d, i) => i * 70)
.attr("y", (d, i) => 400 - 10 * d - 3);
svg.data(data).exit().remove();
}
render() {
return <div>{this.drawChart}</div>
);
}
}
I've figured that I need to change the selectAll part, but don't know exactly how to change it?

You need to include an .exit declaration
svg
.selectAll("text")
.data(data)
.exit()
.remove()
svg
.selectAll("rect")
.data(data)
.exit()
.remove()
http://bl.ocks.org/alansmithy/e984477a741bc56db5a5
You may want to instead use a .selectAll("g") element/container to avoid having to maintain 'text' and 'rect' selections separately.

Related

How to display an image inside a circle inside an arc in d3

I've got a sunburst chart drawn and this works fine. I've tried to add a circle that displays an image within each segment (path) and I can't get them to show up. Weirdly if I inspect the DOM the circles are exactly where I want them to be in the tree but they simply don't display. Is anybody able to help?
.create("svg")
.attr("viewBox", [0, 0, width, width])
.style("font-size", ".6rem");
const g = svg
.append("g")
.attr("transform", `translate(${width / 2},${width / 2})`);
const path = g
.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.join("path")
.attr("fill", (d) => {
while (d.depth > 1) d = d.parent;
return color(d.data.name);
})
.attr("fill-opacity", (d) =>
arcVisible(d.current) ? (d.children ? 0.7 : 0.5) : 0
)
.attr("d", (d) => arc(d.current));
path
.filter((d) => d.children)
.style("cursor", "pointer")
.on("click", clicked);
const defs = path.append("defs").attr("id", "imgdefs");
const iconPattern = defs
.append("pattern")
.attr("id", "iconPattern")
.attr("height", 1)
.attr("width", 1)
.attr("x", "0")
.attr("y", "0");
iconPattern
.append("image")
.attr("xlink:href", function (d) {
return d.data.img;
})
.attr("height", 15)
.attr("width", 15);
path
.append("circle")
.attr("r", 5)
.attr("cy", 0)
.attr("cx", 0)
.attr("fill", "url(#iconPattern)");
path
.append("title")
.attr("pointer-events", "none")
.attr("color", "white")
.on("click", clicked)
.text((d) => d.data.name);```
Here is your code sample refactored to add g instead of path and add path, circle, and text under g:
const segments = g
.selectAll("g.segment")
.data(root.descendants().slice(1))
.join("g")
.classed("segment", true)
segments.append("path)
.attr("fill", ...)
.attr("fill-opacity", ...);
segments.filter((d) => d.children)
.style("cursor", "pointer")
.on("click", clicked);
const defs = segments.append("defs").attr("id", "imgdefs");
const iconPattern = defs
.append("pattern")
...
iconPattern
.append("image")
...
segments.append("circle")
.attr("r", 5)
.attr("cy", 0)
.attr("cx", 0)
.attr("fill", "url(#iconPattern)");
segments.append("title")
.attr("pointer-events", "none")
.attr("color", "white")
.on("click", clicked)
.text((d) => d.data.name);

Drag rect not working as expected

I have created a d3 visualization that takes an json data, create a rect for each data point, and then displays the text in the rect. However, drag
works only for the 1st rect.
I am wondering how to acts a natural drag action for each rect.
my codepen project: https://codepen.io/moriakijp/project/editor/ZRnVwr
here is the code:
drawNumbers = layout => {
const width = innerWidth;
const height = width * 0.5;
const margin = {
top: height * 0.05,
bottom: height * 0.05,
left: width * 0.05,
right: width * 0.05
};
d3.json(layout).then(data => {
const colsize = data[data.length-1].col;
const rowsize = data[data.length-1].row;
const blocksize = colsize < rowsize ?
(width - margin.left - margin.right) / colsize:
(height - margin.left - margin.right) / rowsize;
function dragstarted(d) {
}
function dragged(d) {
d3
.select(this)
.select("rect")
.attr("x", (d.x = d3.event.x))
.attr("y", (d.y = d3.event.y));
d3
.select(this)
.select("text")
.attr("x", (d.x = d3.event.x))
.attr("y", (d.y = d3.event.y));
}
const dragended = (d) => {
}
const drag = d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
const svg = d3
.select("#heatmap")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", `translate(${margin.left}, ${margin.top})`)
.selectAll("g")
.data(data)
.enter()
.append("g")
.call(drag)
svg
.selectAll("g")
.data(data)
.enter()
.append("rect")
.attr("id", "block")
.attr("class", "block")
.attr("x", (d, i) => blocksize * (i % colsize)) // relative to 'svg'
.attr("y", (d, i) => blocksize * (data[i].row - 1)) // relative to 'svg'
.attr("width", blocksize)
.attr("height", blocksize)
.attr("fill", "#d00a")
.style("opacity", 0.5)
.attr("stroke", "#000")
.attr("stroke-width", "2")
svg
.selectAll("g")
.data(data)
.enter()
.append("text")
.attr("id", "text")
.attr("class", "text")
.text(d => `${d.char}`)
.attr("x", (d, i) => blocksize * (i % colsize))
.attr("y", (d, i) => blocksize * (data[i].row - 1))
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", "#333")
.attr("dx", blocksize / 2)
.attr("dy", blocksize / 2)
.style("font-size", blocksize / 2 );
});
};
drawNumbers('number.json');
You aren't quite using the enter pattern correctly if you want to take "data, create a rect for each data point, and then displays the text in the rect."
Let's break down what you have:
const svg = d3
.select("#heatmap")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", `translate(${margin.left}, ${margin.top})`)
.selectAll("g")
.data(data)
.enter()
.append("g")
.call(drag)
Here you select the element with id heatmap append an svg, and then enter a g for each item in your data array. As such, svg is a selection of three g elements, and you call the drag on these g elements.
Next you take this selection of three g elements and select child g elements. As there are no child g elements (this is an empty selection), entering and appending (rects), creates three child rectangles for each g in the selection svg:
svg
.selectAll("g")
.data(data)
.enter()
.append("rect")
....
You do the same thing with the text. Now we have 9 rectangles and 9 texts, three each in each of the parent g elements (held the selection svg). Each of those parent g elements has a drag function that positions the first rectangle in it:
d3
.select(this)
.select("rect") // select first matching element
.attr("x", (d.x = d3.event.x))
.attr("y", (d.y = d3.event.y));
As each g has three rectangles, only the first one will be moved.
One solution would be to not do an enter cycle for each g in svg: your data is not nested, we already have a g for each item in the data array. So we just need to append a single text element and a single rect element to each g:
svg.append("rect").attr("x", function(d) {...
The data bound originally to the g is also bound to this child element, no need to rebind data. Though, I'd rename svg to something else so that it is more reflective of its role and contents though.
Overall this might look something like:
const g = d3
.select("#heatmap")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", `translate(${margin.left}, ${margin.top})`)
.selectAll("g")
.data(data)
.enter() // create a g for each item in the data array
.append("g")
.call(drag)
// add a rect to each g
g.append("rect")
.attr("id", "block")
.attr("class", "block")
.attr("x", (d, i) => blocksize * (i % colsize)) // relative to 'svg'
.attr("y", (d, i) => blocksize * (data[i].row - 1)) // relative to 'svg'
.attr("width", blocksize)
.attr("height", blocksize)
.attr("fill", "#d00a")
.style("opacity", 0.5)
.attr("stroke", "#000")
.attr("stroke-width", "2")
// add text to each g
g.append("text")
.attr("id", "text")
.attr("class", "text")
.text(d => `${d.char}`)
.attr("x", (d, i) => blocksize * (i % colsize))
.attr("y", (d, i) => blocksize * (data[i].row - 1))
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", "#333")
.attr("dx", blocksize / 2)
.attr("dy", blocksize / 2)
.style("font-size", blocksize / 2 );
Here's a running example with the above modification.

Transition height stacked bar chart

I have a codepen here - https://codepen.io/anon/pen/GybENz
I've created a simple stacked bar chart with a legend to filter the chart.
I'd like to animated the height of the bar from the bottom axis up.
Currently its animating from the left and down
let layersBar = layersBarArea.selectAll('.layer').data(stackedSeries)
.enter()
.append('g')
.attr('class', 'layer')
.style('fill', (d, i) => {
return colors[i];
});
layersBar.selectAll('rect')
.data((d) => {
return d
})
.enter()
.append('rect')
.attr('height', 100)
.transition()
.duration(400)
.attr('height', (d, i) => {
return y(d[0]) - y(d[1]);
})
.attr('y', 0)
.attr('y', (d) => {
return y(d[1]);
})
.attr('x', (d, i) => {
return x(d.data.date)
})
.attr('width', x.bandwidth());
}
Set the x position, the width, the y position (as the baseline) and the height (as zero) before the transition:
.attr('height', 0)
.attr("y", h - margin.bottom - margin.top)
.attr('x', (d, i) => {
return x(d.data.date)
})
.attr('width', x.bandwidth())
Here is the updated CodePen: https://codepen.io/anon/pen/ypdoMK?editors=0010
PS: It would be a good idea transitioning each rectangle individually. For instance, if the user clicked usedInf, you should transition only those rectangles... however, since you did this...
layersBarArea.selectAll('g.layer').remove();
... at the beginning of your drawChart function, which is a wrong approach, such suggestion will need a big refactor, out of the scope of this question/answer.

How Can I Use Symbols With a Legend

I've got a legend, with colored rectangles...
I'd like to replace the rectangles with symbols (i.e., circle, cross, diamond, square). I can't figure out how to do that.
I've been using variations of .attr("d", d3.svg.symbol().type('circle'). For instance, I tried:
legendRect
.attr("d", d3.svg.symbol().type(function (d) { return d[2] })
and I tried:
legendRect.append("svg:path")
.attr("d", d3.svg.symbol().type((d: any) => { return d[2] }))
d[2] is "supposed to be" pulling from legendData, as shown in the below code example...like it does with d[1] for the fill.
But I don't ever see anything change.
Here's the code I'm using for the legend, without the symbol stuff, below. What am I doing wrong and how can I change the rectangles to symbols? Where do I need to add what?
var legendData = [["OA", "yellow", "circle"], ["OI", "blue", "cross"], ["RARC", "green", "diamond"], ["CAPE", "red", "square"], ["Other", "black", "triangleDown"]];
var legend = this.svg.append("g")
.attr("class", "legend")
.attr("height", 0)
.attr("width", 0)
.attr('transform', 'translate(-20,250)');
var legendRect = legend.selectAll('rect').data(legendData);
legendRect.enter()
.append("rect")
.attr("x", width - 65)
.attr("width", 10)
.attr("height", 10)
;
legendRect
.attr("y", function (d, i) {
return i * 20;
})
.style("fill", function (d) {
return d[1];
})
var legendText = legend.selectAll('text').data(legendData);
legendText.enter()
.append("text")
.attr("x", width - 52);
legendText
.attr("y", function (d, i) {
return i * 20 + 9;
})
.text(function (d) {
return d[0];
});
Here's how I would code it. Notice, that I data-bind to a wrapper g element and then place the symbol and text into it for each legend item. You can then position the g instead of positioning the text and "symbol" separately. This also removes the need for double-binding the data.
var legendData = [["OA", "yellow", "circle"], ["OI", "blue", "cross"], ["RARC", "green", "diamond"], ["CAPE", "red", "square"], ["Other", "black", "triangleDown"]];
var svg = d3.select('body').append('svg').attr('width', 500).attr('height', 500);
var legend = svg.append('g')
.attr("class", "legend")
.attr("height", 0)
.attr("width", 0)
.attr('transform', 'translate(20,20)');
var legendRect = legend
.selectAll('g')
.data(legendData);
var legendRectE = legendRect.enter()
.append("g")
.attr("transform", function(d,i){
return 'translate(0, ' + (i * 20) + ')';
});
legendRectE
.append('path')
.attr("d", d3.svg.symbol().type((d) => { return d[2] }))
.style("fill", function (d) {
return d[1];
});
legendRectE
.append("text")
.attr("x", 10)
.attr("y", 5)
.text(function (d) {
return d[0];
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
This is a implementation which uses symbols for your legend. You can use the symbols like the following:
svg.selectAll('.symbol')
.data(legendData)
.enter()
.append('path')
.attr('transform', function(d, i) {
return 'translate(' + (20) + ',' + ((i * 20) + 10) + ')';
})
.attr('d', d3.symbol().type(function(d, i) {
if (d[2] === "circle") {
return d3.symbolCircle;
} else if (d[2] === "cross") {
return d3.symbolCross;
} else if (d[2] === "diamond") {
return d3.symbolDiamond;
} else if (d[2] === "square") {
return d3.symbolSquare;
} else {
return d3.symbolTriangle;
}
})
.size(100))
.style("fill", function(d) {
return d[1];
});
Then you can set your legend labels like the following:
svg.selectAll('.label')
.data(legendData)
.enter()
.append('text')
.attr("x", "40")
.attr("y", function(d, i){ return ((i * 20)+15);})
.text(function(d) {
return d[0];
});
Check fiddle here - https://jsfiddle.net/zoxckLe3/
P.S. - Above solution uses d3 v4. To achieve the same in v3, use the following line .attr('d', d3.svg.symbol().type(function(d){return d[2];})) instead of the part where I match d[2] to the symbol name.
For adding image icons, you can use below code.
legend.append("**image**")
.attr("x", 890)
.attr("y", 70)
.attr("width", 20)
.attr("height", 18)
.attr("xlink:href",function (d) {
**return "../assets/images/dev/"+d+".png";**
})
This works for me..

How to fill half of the rectangle with color in d3.js

Hi want a rectangle of width 250, and i want to fill the rectangle with the color based on the input value.I tried to create one rectangle of gray and another one of color skyblue on the same position to acheive this but it update the rectangle only. cant create another rectangle on the previous one. what to do.? My Js fiddle is http://jsfiddle.net/sSqmV/ i want to create an second rectangle of sky blue over the previous one of white color to acheive my task.
var chart = d3.select("div.dev1").append("svg")
.attr("width", 292)
.attr("height", 300);
var dataset = [0, 1, 2];
var dataset2 = [0, 1, 2,3];
var rects1 = chart.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", 10)
.attr("y", function (d, i) { return (i + 1) * 60; })
.attr("height", 6)
.attr("width", 250)
.attr("fill", "white");
var rects = chart.selectAll("rect")
.data(dataset2)
.enter()
.append("rect")
.attr("x", 10)
.attr("y", function (d, i) { return (i + 1) * 60; })
.attr("height", 6)
.attr("width", 250)
.attr("fill", "skyblue");
var texts = chart.selectAll("text").data(dataset2).enter().append("text")
.text("18% of the questions were ANSWERED")
.attr("x", 10)
.attr("y", function (d, i) { return 90+(i*60); });
You can do something like this:
var chart = d3.select("div.dev1").append("svg")
.attr("width", 292)
.attr("height", 300);
var dataset = [0, 1, 2];
var dataset2 = [0, 1, 2, 3];
var rects = chart.selectAll(".rect1")
.data(dataset2)
.enter()
.append("rect")
.attr('class', 'rect1')
.attr("x", 10)
.attr("y", function (d, i) {
return (i + 1) * 60;
})
.attr("height", 6)
.attr("width", 250)
.attr("fill", "skyblue");
var rects1 = chart.selectAll(".rect")
.data(dataset)
.enter()
.append("rect")
.attr('class', 'rect')
.attr("x", 10)
.attr("y", function (d, i) {
return (i + 1) * 60;
})
.attr("height", 6)
.attr("width", 125)
.attr("fill", "white");
var texts = chart.selectAll("text").data(dataset2).enter().append("text")
.text("18% of the questions were ANSWERED")
.attr("x", 10)
.attr("y", function (d, i) {
return 90 + (i * 60);
});
Here is jsfiddle: http://jsfiddle.net/cuckovic/sSqmV/2/

Resources