AstroDraw / AstroChart

A free and open-source TypeScript library for generating SVG charts to display planets in astrology.
https://astrodraw.github.io
MIT License
200 stars 51 forks source link

Responsive Layouts #14

Closed aintHuman closed 1 year ago

aintHuman commented 2 years ago

Which is the best way to handle responsive layouts ?

In your code you have instantiated the chart object during the window.onLoad(...) event, with a pre-defined and fixed set of dimensions.

However, in responsive layouts, we would expect the dimensions to change as the window is resized.

I have placed the the instantiation command (ie new window.astrology.Chart(...)) inside a React side effect, where the dimensions are held in a state variable. This seems to work as expected in the sense that the chart appears to resize with the page, however, upon closer inspection of the source html, as the window is re-sized, a new chart is rendered, but the old chart HTML is still present in the overall page html (albeit hidden / not visible).

Is there a method to change the dimensions of the chart object once it has been created?, that way I could use the onLoad event, and then in the react side effect, just modify the chart instances dimensions.

By react side-effect, I am using like this:

    // Redraw
    React.useEffect(()=>{
        if(data){
            // Updated options
            const optionsLocal = {
                // Removed for brevity
            }

            let chart        = new window.astrology.Chart(id, dims.width - 16, dims.width - 16, optionsLocal);
            const dataRadix  = processHoroscopeData(data); // my method for putting my astro data in the format expected
            const radix      = chart.radix(dataRadix);      
            radix.aspects();
        }

    },[id, data, dims])

In the above, basically dims are a state variable (ie const [dims,setDims] = React.useState({width:600,height:600});), which changes as the container is resized. When the dims change, the side-effect is triggered, causing a new chart to be rendered at the target div id. Incidentally the chart will also re-render if the container id is changed or if the horoscope data changes.;

--

So here is the html when first loaded (only one SVG container)

Screen Shot 2021-11-29 at 11 58 55 am

And after I resize the browser a few times (multiple SVG containers, one for each dim change):

Screen Shot 2021-11-29 at 11 59 33 am

In the latter, it is only the first SVG container which is visible to the user. Subsequent SVG containers are not visible, but they are still present. In any case, instances of the SVG container start to spool, the old instances are not destroyed when a new one is created. If I were to keep resizing the browser (by dragging the corner of the browser back and forth), the quantity of SVG containers would quickly increase to hundreds or thousands of instances.

--

Update: Solution.

So, I realised it still wouldn't work for me if I were to put the chart instantiation function in the window.onLoad(...) event, and that is because, the data to render wouldn't be available at such time.

So what I decided to do, was to remove all children of the chart parent, as a cleanup function to the react hook:

    // Remove children of id
    const removeOldChartInstances = React.useCallback((id) => {
        const parent = document.getElementById(id);
        if(parent){
            while (parent.firstChild) {
                parent.removeChild(parent.firstChild);
            }
        }
    },[])

    // Redraw
    React.useEffect(()=>{
        if(data){
            // Updated options
            const optionsLocal = {
                // Removed for brevity
            }

            let chart        = new window.astrology.Chart(id, dims.width - 16, dims.width - 16, optionsLocal);
            const dataRadix  = processHoroscopeData(data); // my method for putting my astro data in the format expected
            const radix      = chart.radix(dataRadix);      
            radix.aspects();
        }

        // Cleanup
        return () => {
            removeOldChartInstances(id);
        }

    },[id, data, dims, removeOldChartInstances])

So what this means, for anyone not familiar with react, is that when the side-effect is triggered the first time, the chart renders. If one of the effect dependencies changes (such as the dim change), the cleanup function is executed (which removes any children to the chart parent container) prior to re-executing the code inside the effect which renders a new chart inside the parent. This ensures the spooling above never takes place.

afucher commented 2 years ago

Hey @aintHuman ,

What I have done to make it responsive is to add width and height to be 100%, so when I resize the window it resizes the chart also. So it will fill the container that it is inside, and when the container change the size it also changes.

About a lot of svg elements being created, I saw that you already had a solution, sorry for the delay in the answer. I also work with react and I have a similar solution:

//this is the useEffect return
return () => {
      const element = document.getElementById(chartId)
      if(element != null)  element.innerHTML = ''
    }