Hello! The reason why you can't observe an SVG is because SVG's are different than HTML nodes. SVG's don't have a content box only a bound box and the ResizeObserver observes changes to the content box.
There's a much simpler explanation: the SVG has fixed width, so when the window is being resized, instead of resizing the SVG (which is the ref given to the observer), the width of the SVG doesn't change, so the resizeObserver callback doesn't get triggered because the SVG DOM node is not being resized. To make sure that the resizeObserver callback gets called, you need to attach the ref on a DOM node which has a non-fixed width (like that div that has 100% width) so that its width varies with the size of the window. That's really all there is to it. But wrapping the SVG in a div is not such a bad idea to be honest. This way you can let the SVG have its own styling and use the wrapping div to do stuff like responsiveness but also padding, margins, etc.
This is a really excellent series, explained super well! Towards the end, isn't your browser dying because the resize observer is being allowed to trigger without any limits? Shouldn't we be doing requestAnimationFrame or something to ensure that we have smooth resizing? I'm not sure exactly where the requestAnimationFrame should go
No, why would it? The Resize Observer only triggers on actual resizes, which doesn't happen all that often, realistically. "requestAnimationFrame" is not a necessity here, I believe.
@@muratorium Thanks for the quick reply! I mean, during the resize, you basically get an event triggered at a crazy rate because as long as you are moving your mouse while resizing, you're firing a tonne of events. If you try to re-render \ console.log for every single resize event then you're going to get super stuttery behaviour because you can't keep up during the resize. What I understood is that request Animation frame can slow this down somewhat. So the re-render or console.log is only triggered when the renderer is able to process the next frame. This means that if you have like 60fps available, then you'd only react to the resize on each frame. Even if there are actually 100 resize events happening between each frame.
@@muratorium Actually I did some digging and I can see that resizeObserver calls requestAnimationFrame under the hood anyway - so we totally don't need to add an additional request!
@@TheHive95 Yes, guess you even can debounce / throttle these events, but the real performance sucker in the example are the console.logs of the ResizeObserver, not the Observer itself in this case.
I have questions about d3. Now I use ResizeObserver and I didn't add inside svg inside html in advance. I add it this way svg .append(`g`) .attr('class', "x-axis") .style("transform", `translateY(${dimensions.height}px)`) .call(xAxis); When I test the responsiveness, it keeps add new axis but not delete the old ones. How can it solve?
Hey Isaac, sorry for the late response, didn't get notified about this comment. The thing about append is that it will create an element for you, and the code that you write in "useEffect" is getting triggered everytime some of your dependencies change (in this case, the dimensions). So that means your code will always append a new group, every time it is executed. You need to check if the g element is there or not, before you decide to append a group. That is why I created that g element with .x-axis, so that I can re-use it without checking in code / useEffect if it is there or not.
If you wanted to append the g element dynamically, you could say: svg.selectAll(".x-axis").data([true]).join("g").attr("class", "x-axis").call(xAxis) This code will create a g element for the first time and reuse it when the dimensions change.
@@muratorium Thanks. But if I try to select the xaxis in the following way svg .selectAll(`x-axis`) .append(`g`) .attr('class', "x-axis") .style("transform", `translateY(${dimensions.height}px)`) .call(xAxis) My axis will not be drawn.......
Isaac Yuen the reason for that is simple: You are missing the "." at selectAll("x-axis"). You are selecting an element by class, so you need to say selectAll(".x-axis").
A truly great series of tutorials. However, from a UX/UI design perspective, keeping related CSS as inline styles and within the component makes global CSS management and consistency progressively harder. That's why external stylesheets were invented. I recommend using classes within React components for all CSS styles, with a structure and naming convention like BEM or SMACSS, and all SCSS/LESS/CSS in separate files, perhaps as component files in the same directory, or a separate directory purely for CSS.
One important question: The height never changes - 'dimensions.height' stays at 150, even if you reduce the window vertically, it remains at 150 while 'dimensions.width' is dynamic. I dealt with it by setting svg { ... height: 300px; } in App.css to get the 300px height I needed. Is there another solution? Thanks again for the video!
Hey Mark! In order to make the "height" responsive, the height needs to depend on the size relative to the window. The width of our SVG was 100% of our current container, so that is why dimensions.width is dynamic, but the height isn't. You would need to set the height as a percentage or something, as in this video, we didn't change the default height (150px).
Hey Muri, any reason to use "ResizeObserver" over "viewBox", and vice versa? The viewbox attribute has always worked fine for me when making responsive D3 graphs
How do you do that? Do you just set the SVG to be 100% wide and then set a fixed value for viewBox? How do you get around without calculating the exact width/size of the SVG? If you don't set the viewBox with the exact width and height of the SVG, you will have scaling inside of your SVG.
Hi Murat, Can you tell me how I can append multiple dom elements as siblings in a data join? I want to create patterns dynamically which contain a rect and an image as sibling elements. The image should not be inside the rect.
Thank you.
And the good news is ResizeObserver is now supported in Edge & Safari
Just wow! This API is now supported 100% everywhere.
Just what I needed. Great video, and it will be my first time using Hooks in production. Thanks!
You sir, are a godsend! Thanks so much for the videos, they've been incredibly helpful for me in college. Subbed and wishing you the best :)
Learnt something new. Resize observer API
me too!
Thank you so much for this series!! Awesome
Fetele Andra You are super welcome! Hope this helps with any dataviz project of yours.
@@muratorium It has already helped a lot. :) I will watch the whole series
Great video! The first part is an explanation of resizeObersever - you can go straight to the d3 part at 16:30.
Amazing series, and very well explained. Thank you so much.
Besten Dank! Hab Deine Videoserie leider jetzt erst entdeckt, aber zum Glück hab ich sie entdeckt :-)
man, great video, I really like this series.
Really like these videos mate. Good job.
Thank you for such a great video. Super helpful 👍
You're very welcome!
Amazing video! Thank you!
Hello!
The reason why you can't observe an SVG is because SVG's are different than HTML nodes. SVG's don't have a content box only a bound box and the ResizeObserver observes changes to the content box.
Oh wow, thanks for the explanation!
There's a much simpler explanation: the SVG has fixed width, so when the window is being resized, instead of resizing the SVG (which is the ref given to the observer), the width of the SVG doesn't change, so the resizeObserver callback doesn't get triggered because the SVG DOM node is not being resized.
To make sure that the resizeObserver callback gets called, you need to attach the ref on a DOM node which has a non-fixed width (like that div that has 100% width) so that its width varies with the size of the window. That's really all there is to it. But wrapping the SVG in a div is not such a bad idea to be honest. This way you can let the SVG have its own styling and use the wrapping div to do stuff like responsiveness but also padding, margins, etc.
This is a really excellent series, explained super well! Towards the end, isn't your browser dying because the resize observer is being allowed to trigger without any limits? Shouldn't we be doing requestAnimationFrame or something to ensure that we have smooth resizing? I'm not sure exactly where the requestAnimationFrame should go
No, why would it? The Resize Observer only triggers on actual resizes, which doesn't happen all that often, realistically. "requestAnimationFrame" is not a necessity here, I believe.
@@muratorium Thanks for the quick reply! I mean, during the resize, you basically get an event triggered at a crazy rate because as long as you are moving your mouse while resizing, you're firing a tonne of events. If you try to re-render \ console.log for every single resize event then you're going to get super stuttery behaviour because you can't keep up during the resize.
What I understood is that request Animation frame can slow this down somewhat. So the re-render or console.log is only triggered when the renderer is able to process the next frame.
This means that if you have like 60fps available, then you'd only react to the resize on each frame. Even if there are actually 100 resize events happening between each frame.
@@muratorium Actually I did some digging and I can see that resizeObserver calls requestAnimationFrame under the hood anyway - so we totally don't need to add an additional request!
@@TheHive95 Yes, guess you even can debounce / throttle these events, but the real performance sucker in the example are the console.logs of the ResizeObserver, not the Observer itself in this case.
@@TheHive95 Good to know, thanks!
thanks a lot for the video, I have learnt a lot!
Very nice, thanks for this!
Very helpful. Thank you very much for the video
great work!!
Can we also use viewBox to resize?
I have questions about d3. Now I use ResizeObserver and I didn't add inside svg inside html in advance. I add it this way svg
.append(`g`)
.attr('class', "x-axis")
.style("transform", `translateY(${dimensions.height}px)`)
.call(xAxis);
When I test the responsiveness, it keeps add new axis but not delete the old ones. How can it solve?
Hey Isaac, sorry for the late response, didn't get notified about this comment.
The thing about append is that it will create an element for you, and the code that you write in "useEffect" is getting triggered everytime some of your dependencies change (in this case, the dimensions). So that means your code will always append a new group, every time it is executed. You need to check if the g element is there or not, before you decide to append a group. That is why I created that g element with .x-axis, so that I can re-use it without checking in code / useEffect if it is there or not.
If you wanted to append the g element dynamically, you could say:
svg.selectAll(".x-axis").data([true]).join("g").attr("class", "x-axis").call(xAxis)
This code will create a g element for the first time and reuse it when the dimensions change.
@@muratorium Thanks. But if I try to select the xaxis in the following way
svg
.selectAll(`x-axis`)
.append(`g`)
.attr('class', "x-axis")
.style("transform", `translateY(${dimensions.height}px)`)
.call(xAxis)
My axis will not be drawn.......
Isaac Yuen the reason for that is simple: You are missing the "." at selectAll("x-axis"). You are selecting an element by class, so you need to say selectAll(".x-axis").
@@muratorium No I just type wrong on TH-cam. Even with dot to select class it doesn't work.
const { width, height } =
dimensions || wrapperRef.current.getBoundingClientRect()
const margin = { top: 20, right: 10, bottom: 20, left: 40 }
const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom
const svg = d3.select(svgRef.current)
const clipPathId = `clipPath`
svg
.append(`defs`)
.append(`clipPath`)
.attr(`id`, clipPathId)
.append(`rect`)
.attr(`x`, margin.left)
.attr(`y`, margin.top)
.attr(`width`, innerWidth)
.attr(`height`, innerHeight)
const xScale = d3
.scaleBand()
.domain(data.map(({ key }) => key))
.range([margin.left, width - margin.right])
.padding(0.1)
const xAxis = d3
.axisBottom(xScale)
// .tickFormat(i => data[i].key)
.tickSize(-innerHeight)
.tickSizeOuter(0)
const gX = svg
.append(`g`)
.attr(`class`, `axis axis-x`)
// .attr(`clip-path`, `url(#${clipPathId})`)
.attr('transform', `translate(0,${height - margin.bottom})`)
gX.select(`.axis-x`).call(xAxis) //
A truly great series of tutorials. However, from a UX/UI design perspective, keeping related CSS as inline styles and within the component makes global CSS management and consistency progressively harder. That's why external stylesheets were invented. I recommend using classes within React components for all CSS styles, with a structure and naming convention like BEM or SMACSS, and all SCSS/LESS/CSS in separate files, perhaps as component files in the same directory, or a separate directory purely for CSS.
One important question: The height never changes - 'dimensions.height' stays at 150, even if you reduce the window vertically, it remains at 150 while 'dimensions.width' is dynamic. I dealt with it by setting svg { ... height: 300px; } in App.css to get the 300px height I needed. Is there another solution? Thanks again for the video!
Hey Mark! In order to make the "height" responsive, the height needs to depend on the size relative to the window. The width of our SVG was 100% of our current container, so that is why dimensions.width is dynamic, but the height isn't. You would need to set the height as a percentage or something, as in this video, we didn't change the default height (150px).
Hey Muri, any reason to use "ResizeObserver" over "viewBox", and vice versa? The viewbox attribute has always worked fine for me when making responsive D3 graphs
How do you do that? Do you just set the SVG to be 100% wide and then set a fixed value for viewBox? How do you get around without calculating the exact width/size of the SVG?
If you don't set the viewBox with the exact width and height of the SVG, you will have scaling inside of your SVG.
The chart renders over the previous render when I try this. I am using groups inside svg for my chart. What could be wrong!
Hey hassan, any chance you can share your code? I can't help you from here.
@@muratorium Hi Murat. Below is my code:
useEffect(() => {
const margin = { top: 30, right: 30, bottom: 10, left: 100 };
const innerWidth = dimensions.width - margin.left - margin.right;
const innerHeight = dimensions.height - margin.top - margin.bottom;
console.log(innerWidth);
const svg = select(svgRef.current)
.attr("width", innerWidth)
.attr("height", innerHeight);
// X-Values
const xValue = (d) => d.Population;
// Y-Values
const yValue = (d) => d.Country;
// X-Scale
const xScale = scaleLinear()
.domain([0, max(data, xValue)])
.range([0, innerWidth]);
// Y-Scale
const yScale = scaleBand()
.padding(0.15)
.domain(data.map(yValue))
.range([0, innerHeight]);
const g = svg
.append("g")
.attr("width", innerWidth)
.attr("height", innerHeight)
.style("border", "1px solid black")
.attr("class", "grp-main")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
const xAxisTickFormat = (number) => format(".2s")(number).replace("G", "B");
const xAxis = axisTop(xScale)
.tickFormat(xAxisTickFormat)
.tickSize(-innerHeight + 55);
const xAxisG = g.append("g").call(xAxis);
xAxisG.select(".domain").remove();
g.selectAll(".bar")
.data(data)
.join("rect")
.attr("class", "bar")
.attr("y", (d) => yScale(yValue(d)))
.attr("width", (d) => xScale(xValue(d)))
.attr("height", yScale.bandwidth())
.attr("fill", "steelblue");
}, [data, dimensions]);
Hey Murat, I was able to figure out what was wrong.. I read your comments below and use join with className instead of append. works fine now.
Hi Murat, Can you tell me how I can append multiple dom elements as siblings in a data join? I want to create patterns dynamically which contain a rect and an image as sibling elements. The image should not be inside the rect.