D3 Bar Chart + React Hooks - exit().remove() not working - reactjs

I'm using react and d3, trying to create a simple bar chart that updates the chart when data is refreshed. The chart is updating when the data changes, but it seems to be layering on top of the old chart. I think the issue is with the d3 exit().remove() function.
As I understand it d3's exit method should return an array of items to be removed, however when I console log it I see an array of "undefined"s. I'm super grateful for any help!
Here is the codesandbox:
https://codesandbox.io/s/gifted-field-n66hw?file=/src/Barchart.js
Here is the code snippet:
import React, { useEffect, useRef } from "react";
import * as d3 from "d3";
const BarChart = props => {
const { randomData, width, height, padding } = props;
const ref = useRef(null);
function colorGradient(v) {
return "rgb(0, " + v * 5 + ", 0";
}
//insert & remove elements using D3
useEffect(() => {
if (randomData.length > 0 && ref.current) {
const group = d3.select(ref.current);
// the data operator binds data items with DOM elements
// the resulting selection contains the enter and exit subselections
const update = group
.append("g")
.selectAll("rect")
.data(randomData);
let bars = update
.enter() // create new dom elements for added data items
.append("rect")
.merge(update)
.attr("x", (d, i) => i * (width / randomData.length))
.attr("y", d => height - d * 5)
.attr("width", width / randomData.length - padding)
.attr("height", d => d * 5)
.attr("fill", d => colorGradient(d));
let labels = update
.enter()
.append("text")
.text(d => d)
.attr("text-anchor", "middle")
.attr(
"x",
(d, i) =>
i * (width / randomData.length) +
(width / randomData.length - padding) / 2
)
.attr("y", d => height - d * 5 + 12)
.style("font-family", "sans-serif")
.style("font-size", 12)
.style("fill", "#ffffff");
update.exit().remove();
}
}, [randomData, height, padding, width]);
return (
<svg width={width} height={height}>
<g ref={ref} />
</svg>
);
};
export default BarChart;

Everytime you update the chart you run this:
const update = group
.append("g") // create a new g
.selectAll("rect") // select all the rectangles in that g (which are none)
.data(randomData);
update is now an empty selection, there are no rects to select in the newly created g. So when we use update.enter(), a DOM element is created for every item in the data array. Using enter will create an element for every item in the data array that doesn't have a corresponding element already.
update.exit() will be empty, because there are no elements selected in update, so nothing will be removed. Previously created bars are not touched, you aren't selecting them.
If we change your code just to remove the .append("g"), it gets us closer to working (eg). The bars were colored white, so they were not visible, I've changed the fill color so the update selection is visible
If we remove .append("g") we have some other problems on update now:
you are not exiting text (as you are not selecting text with .selectAll(), only rect elements), and
you're merging text and rectangles into one selection, which is a bit problematic if you want to position and color them differently on update.
The second problem could be explained a bit more:
update.enter().append("text") // returns a selection of newly created text elements
.merge(update) // merges the selection of newly created text with existing rectangles
.attr("fill", .... // affects both text and rects.
These two issues can be resolved by using the enter/update/exit cycle correctly.
One thing to note is that D3's enter update exit pattern isn't designed to enter elements more than once with the same statement, you're entering text and rects with the same enter statement, see here.
Therefore, one option is to use two selections, one for text and one for rects:
const updateRect = group
.selectAll("rect")
.data(randomData);
let bars = updateRect
.enter() // create new dom elements for added data items
.append("rect")
.merge(updateRect)
.attr("x", (d, i) => i * (width / randomData.length))
.attr("y", d => height - d * 5)
.attr("width", width / randomData.length - padding)
.attr("height", d => d * 5)
.attr("fill", d => colorGradient(d));
const updateText = group
.selectAll("text")
.data(randomData);
let labels = updateText
.enter()
.append("text")
.merge(updateText)
.text(d => d)
.attr("text-anchor", "middle")
.attr(
"x",
(d, i) =>
i * (width / randomData.length) +
(width / randomData.length - padding) / 2
)
.attr("y", d => height - d * 5 + 12)
.style("font-family", "sans-serif")
.style("font-size", 12)
.style("fill", "#fff");
updateRect.exit().remove();
updateText.exit().remove();
Here in sandbox form.
The other option is to use a parent g to hold both rect and text, this could be done many ways, but if you don't need a transition between values or the number of bars, would probably be most simple like:
const update = group
.selectAll("g")
.data(randomData);
// add a g for every extra datum
const enter = update.enter().append("g")
// give them a rect and text element:
enter.append("rect");
enter.append("text");
// merge update and enter:
const bars = update.merge(enter);
// modify the rects
bars.select("rect")
.attr("x", (d, i) => i * (width / randomData.length))
.attr("y", d => height - d * 5)
.attr("width", width / randomData.length - padding)
.attr("height", d => d * 5)
.attr("fill", d => { return colorGradient(d)});
// modify the texts:
bars.select("text")
.text(d => d)
.attr("text-anchor", "middle")
.attr(
"x",
(d, i) =>
i * (width / randomData.length) +
(width / randomData.length - padding) / 2
)
.attr("y", d => height - d * 5 + 12)
.style("font-family", "sans-serif")
.style("font-size", 12)
.style("fill", "#ffffff");
Here's that in sandox form.
A bit further explanation: selection.select() selects the first matching element for each element in the selection - so we can select the sole rectangle in each parent g (that we add when entering the parent) with bars.select("rect") above. D3 passes the parent datum to the child when appending in the above. Note: If we had nested data (multiple bars or texts per data array item) we'd need to have nested enter/exit/update cycles.

Related

How to add tooltip with eclipse in legend in line chart in D3.js

I want to add eclipse with tooltip in Legend in Line chart in D3.js
I attached my working JSFiddle with Legend here
var legend = svg.append("g")
.attr("class", "legend")
.attr("height", 100)
.attr("width", 100).attr('transform', 'translate(-20,0)');
`
Not able to set eclipse with tooltip if Legend name is too long
I need like this
I search here but all examples I found for word wrap in Legend so please help me to set eclipse with tooltip.
You need to use substring to show few characters of legend and then append a title. It will be like below. I modified your legend text part.
legend.selectAll('text')
.data((this.materialGraphDataSource[0].materials))
.enter()
.append("text")
.attr("x", width - 140)
.attr("y", function (d, i) { return i * 20 + 9; })
.text(function (d, i) {
var text = d.materialName;
return text.substring(0,20)+ "...";
})
.style("font-size", "10px")
.append('title').text(function (d, i) {
var text = d.materialName;
return text;
});

D3 mouse event handler deprecated

We have a heatmap for which I am trying to create a tooltip. The example we were using when building mine had d3.mouse which is deprecated in the D3 version we use with React. We saw different alternatives such as d3.pointer(), but we haven't managed to get it working.
I have attached below the small part of the code where the d3.mouse would have gone. Is there any way to position the tooltip with respect to the mouse without using the deprecated d3.mouse?
svg.selectAll('rect')
.data(data, d => {return (d.group+':'+d.variable)})
.enter()
.append("rect")
.attr("x", d => { return x(d.group) })
.attr("y", d => { return y(d.variable) })
.attr("width", x.bandwidth() )
.attr("height", y.bandwidth() )
.style("fill", function(d) { return myColor(d.value)} )
.on("mouseover", (d,i,e) => {
tooltip.html("The value is : " + i.value)
.style('opacity', .9)
.style("left", (d3.mouse(this)[0]+70) + "px")
.style("top", (d3.mouse(this)[1]) + "px")
})
.on("mouseout", () => {
tooltip.style('opacity', 0)
.style('left', '0px');
})

Conditionally adding additional elements in D3

I've built out a heatmap with D3 in React and the basic result works fine: the rects are rendering and so are the conditional color values, based on my data.
Here is roughly what it looks like:
const dayRects = bounds.selectAll("rect").data(processed)
dayRects
.join("rect")
.attr("x", d => xScale(d.exampleData))
.attr("width", ordinalScale.bandwidth)
.attr("y", d => yScale(d.exampleData))
.attr("height", ordinalScale.bandwidth)
.attr("rx", 3)
.style("fill", d => colorScale(d.moreData))
What I'm struggling to do and cannot find guidance for online, is to conditionally add elements based on data. For example, I'm trying to add an additional rect for items in my array of data that meet certain conditions. This additional rect would be slightly larger than the above rects, have a transparent fill, and a different colored stroke - resulting in, for example, something like this:
When I attempt to do this with something like below the above code
const dayStarted = bounds
.select("rect")
.data(processed.filter(i => i.value === desiredValue))
.join("rect")
.attr("x", d => xScale(d.exampleData))
.attr("width", ordinalScale.bandwidth)
.attr("y", d => yScale(d.exampleData))
.attr("rx", 20)
... it just manipulates the first rect element in the first set of rects (dayRects).
You should bind your data to a g element, position that and then add the rects as sibling in the g.
Here's a quick example:
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
<svg width="125" height="125"></svg>
<script>
var processed = [
{ x: Math.random() * 100, y: Math.random() * 100, value: 1 },
{ x: Math.random() * 100, y: Math.random() * 100, value: 0 },
];
var bounds = d3.select('svg');
const g = bounds
.selectAll('g')
.data(processed)
.join('g')
.attr('transform', (d) => {
// use your scales to position g
return "translate(" + d.x + "," + d.y + ")";
});
g.append('rect') // first rect
.attr('width', 20)
.attr('height', 20)
.attr('rx', 3)
.style('fill', 'orange');
g.filter( d => d.value == 1) //apply filter on g for ones that get 2nd rect
.append('rect')
.attr('width', 20)
.attr('height', 20)
.attr('rx', 3)
.style('fill', 'none')
.style('stroke', 'steelblue')
.style('stroke-width', '3px');
</script>
</body>
</html>

D3 area graph animation

I have an area graph ( see js fiddle https://jsfiddle.net/o7df3tyn/ ) I want to animate this area graph. I tried the approach in this
question , but this doesnt seem to help because I have more line graphs in the the same svg element
var numberOfDays = 30;
var vis = d3.select('#visualisation'),
WIDTH = 1000,
HEIGHT = 400,
MARGINS = {
top: 20,
right: 20,
bottom: 20,
left: 50
};
var drawArea = function (data) {
var areaData = data;
// var areaData = data.data;
var xRange = d3.scale.linear().range([MARGINS.left, WIDTH - MARGINS.right]).domain([0, numberOfDays + 1]),
yRange = d3.scale.linear().range([HEIGHT - MARGINS.top, MARGINS.bottom]).domain([_.min(areaData), _.max(areaData)]);
var area = d3.svg.area()
.interpolate("monotone")
.x(function(d) {
return xRange(areaData.indexOf(d));
})
.y0(HEIGHT)
.y1(function(d) {
return yRange(d);
});
var path = vis.append("path")
.datum(areaData)
.attr("fill", 'lightgrey')
.attr("d", area);
};
var data = [1088,978,1282,755,908,1341,616,727,1281,247,1188,11204,556,15967,623,681,605,7267,4719,9665,5719,5907,3520,1286,1368,3243,2451,1674,1357,7414,2726]
drawArea(data);
So I cant use the curtain approach.
I want to animate the area from bottom.
Any ideas / explanations ?
Just in case anyone else stuck in the same problem, #thatOneGuy nailed the exact problem. My updated fiddle is here https://jsfiddle.net/sahils/o7df3tyn/14/
https://jsfiddle.net/DavidGuan/o7df3tyn/2/
vis.append("clipPath")
.attr("id", "rectClip")
.append("rect")
.attr("width", 0)
.attr("height", HEIGHT);
You can have a try now.
Remember add clip-path attr to the svg elements you want to hide
In this case
var path = vis.append("path")
.datum(areaData)
.attr("fill", 'lightgrey')
.attr("d", area)
.attr("clip-path", "url(#rectClip)")
Update:
If we want to grow the area from bottom:
vis.append("clipPath")
.attr("id", "rectClip")
.append("rect")
.attr("width", WIDTH)
.attr("height", HEIGHT)
.attr("transform", "translate(0," + HEIGHT + ")")
d3.select("#rectClip rect")
.transition().duration(6000)
.attr("transform", "translate(0," + 0 + ")")
The other answer is okay but this doesn't animate the graph.
Here is how I would do it.
I would add an animation tween to the path so it tweens from 0 to the point on the path.
Something like so :
//create an array of 0's the same size as your current array :
var startData = areaData.map(function(datum) {
return 0;
});
//use this and tween between startData and data
var path = vis.append("path")
.datum(startdata1)
.attr("fill", 'lightgrey')
.attr("d", area)
.transition()
.duration(1500)
.attrTween('d', function() {
var interpolator = d3.interpolateArray(startData, areaData );
return function(t) {
return area(interpolator(t));
}
});
The reason why yours wasn't working was because of this line :
.x(function(d) {
return xRange(areaData.indexOf(d));
})
d at this point is a value between 0 and the current piece of data, so areaData.indexOf(d) will not work.
Just change this :
.x(function(d,i) {
return xRange(i);
})
This will increment along the x axis :)
Updated fiddle : https://jsfiddle.net/thatOneGuy/o7df3tyn/17/

drag n drop utility for tree (nodes and connector) ,need to have handle of nodes and connector to generate eventson them. - angular JS

drag n drop utility: need to develop a tree structures with nodes and connectors. Nodes and connectors are to be manually drawn using the tool bar(manually created). On the nodes and connectors need to generate events.Using angular js. Please provide sample code.
Once clicked on the nodes the node gets created in one division and the connector can be used graphically to connect between nodes.
I have achieved this with d3 library which seems to be very useful.
Below is answer:
<div id="drawArea" class="division" ></div>
<script type="text/javascript">
// Create a svg canvas
var svg = d3.select("#drawArea")
.append("svg")
.attr("width", 700)
.attr("height", 500);
//Drag nodes
var drag = d3.behavior.drag()
.on("dragstart", function() {
d3.event.sourceEvent.stopPropagation()
})
.on("drag", dragmove);
//First circle
var g1 = svg.append("g")
.attr("transform", "translate(" + 150 + "," + 100 + ")")
.attr("class", "first")
.call(drag)
.append("circle").attr({
r: 20,
})
.style("fill", "#FFFF00")
//Second circle
var g2 = svg.append("g")
.attr("transform", "translate(" + 250 + "," + 300 + ")")
.attr("class", "second")
.call(drag)
.append("circle").attr({
r: 20,
})
.style("fill", "#00FF00")
svg.on('dblclick', function() {
var coords = d3.mouse(this);
console.log(coords);
drawCircle(coords[0], coords[1]);
});
function drawCircle(x, y) {
var g2 = svg.append("g")
.attr("transform", "translate(" + x + "," + y + ")")
.attr("class", "third")
.call(drag)
.append("circle").attr({
r: 20,
})
.style("fill", "#00F");
}
//Drag handler
function dragmove(d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
if(d3.select(this).attr("class") == "first") {
// line.attr("x1", x);
// line.attr("y1", y);
d3.select(this).attr("cx", x);
d3.select(this).attr("cy", y);
} else {
d3.select(this).attr("cx", x);
d3.select(this).attr("cy", y);
//line.attr("x2", x);
//line.attr("y2", y);
}
}
</script>

Resources