Jump to content
Search Community

staggering data from multiple arrays?

UnioninDesign test
Moderator Tag

Warning: Please note

This thread was started before GSAP 3 was released. Some information, especially the syntax, may be out of date for GSAP 3. Please see the GSAP 3 migration guide and release notes for more information about how to update the code to GSAP 3's syntax. 

Recommended Posts

Hello community - my first post! I discovered GSAP on Friday, got it working a few days later...mind=blown! I'd be happy to supply a codepen but have more a general/best practices question.

 

My stack: React, D3...or rather, react-simple-maps npm package to use D3's map projections...and of course GSAP!

 

My project: I'm a new front-end developer, hired as a data visualization analyst, and my first project is to build an interactive map to show bookings. with hundreds of bookings to render, it would be 'data overload' and lousy UI with hundreds of markers across the US all at the same time... I started racking my brain to try and find a way to map these out over a timeline to stagger the bookings...enter GSAP!

 

Question: The map shows (first) the location of the customer who made the booking, our client's location, and a line connecting the two. I have a data object with nested client and customer data, looks something like this below.

 

In order to generate map markers for both the client and the customer, I have already used .map to go through each array and create the marker. I have staggered the animations, but at runtime, all customers are rendered one at a time...then all clients are rendered one at a time, followed by all lines. Surely there's a way to to use the key to go by each bookingId and render the customer unique to that ID, then the client, and then animate a line connecting the two before applying that same animation to the next booking?

 

My janky code for the animations (first-timer over here, go easy on me! lol)  - I know the best practice in React is to use createRef instead of classeNames - will migrate soon! Likely to move this to a separate animation.js file and call the functions later in componentDidMount(). Happy to send more code examples and thanks!

    componentDidMount() {
        bookingInterval();
        //Animations with GSAP
        const tl = new TimelineLite();
        tl.staggerFrom(
            ".custMarkers",
            1,
            {
                y: 500,
                cycle: { x: [-500, 500] },
                ease: Power1.easeOut,
                opacity: 0.1,
                scale: 10
            },
            0.5
        )
            .staggerFrom(
                ".clientMarkers",
                1,
                {
                    cycle: { x: [-500, 500] },
                    y: 500,
                    ease: Power1.easeOut,
                    opacity: 0.1,
                    scale: 7
                },
                0.5
            )
            .staggerFrom(
                ".lineAmation",
                2,
                {
                    y: 500,
                    x: -500,
                    ease: Power1.easeOut,
                    opacity: 0.1,
                    scale: 7,
                    rotation: 360
                },
                0.5
            );
    }

 

Data:

{
        BookingId: 123456,
        client: {
            ZipCode: 55555,
            coordinates: [-80.6998, 28.3716],
            clientName: "name of the client",
        },
        customer: {
            ZipCode: 11111,
            coordinates: [-122.3122, 47.5838],
            location: "city, state"
        },
        CreatedAt: "timestamp string"
    },
Link to comment
Share on other sites

Welcome to the forums, @UnioninDesign!

 

If I understand your question/goal correctly, all you'd need to do is add the position parameter to your 2nd and 3rd staggerFrom() calls so that they are positioned in the timeline properly. By default, animations are always sequenced one-after-the-other. So, for example, if you've got 100 elements that each animate for 1 second, but they're staggered by 0.1 seconds, that means that the last one would finish at 11 seconds (0.1 * 100 for the stagger, plus 1 second for the animation itself). So if you do another staggerFrom() call after that, it's gonna start at 11 seconds. But in your case, it sounds like you want that second staggerFrom() to start much earlier. Perhaps 0.2 seconds after the very first stagger. No problem! Just add 0.2 as the position parameter to tell it to start that stagger at exactly 0.2 seconds into the timeline. Done. 

 

More about the position parameter: https://greensock.com/position-parameter

 

Another option is to build a function that handles all of the animation for just ONE chunk, and then loop through however many elements you have and feed them to that function which then spits back a TimelineLite which you can nest into a master timeline in a staggered fashion. This is slightly more advanced, but once you get the hang of how to do this kind of thing, it can make your animation code much more intuitive and unencumbered. For more about that, read this article: https://css-tricks.com/writing-smarter-animation-code/

 

Happy tweening!

  • Like 1
Link to comment
Share on other sites

Wow thanks! That does help, but not quite what I am looking for. To dig a little deeper, and this may be due to the third party component library I am using, to render two separate sets of map markers, my data is split up into three different arrays now - one for the customer data and map markers, one for the client, anda third array containing the coordinates of each to create the line connecting the two.

 

So to recap...someone makes a booking, but they might live in a different part of the country? Wouldn't it be cool to generate a map marker for the customer, followed by an animated line to connect that customer to the client they booked with? And then - move on to the next booking via it's booking ID or key?

 

What happens now is that, with my dummy data of 11 bookings - all 11 customer markers render, in staggered animation, followed by all 11 clients, and then after that 11 lines. The effect I am going for is (Customer of booking#1)------animatedLine-----(Client of booking #1), and then delay by a second or two, and then repeat that series for next bookingId (Customer of booking#2)-----animatedLine-----(Client of booking #2).

 

Here's an example of how the map markers are generated from my dataset using react-simple-maps, starting with the customer who makes the booking:

    <Markers>
                                        {bookingData.map((data, iCust) => (
                                            <Marker
                                                ref={this.custRef}
                                                key={iCust}
                                                marker={data.customer}
                                                style={{
                                                    default: { fill: "#1B9750" },
                                                    hover: { fill: "#FFFFFF" },
                                                    pressed: { fill: "#0A6ECE" }
                                                }}>
                                                <circle
                                                    className="custMarkers"
                                                    cx={0}
                                                    cy={0}
                                                    r={3}
                                                    style={{
                                                        stroke: "#1B9750",
                                                        strokeWidth: 3,
                                                        opacity: 0.7
                                                    }}
                                                />
                                            </Marker>
                                        ))}
                                    </Markers>

 

Then you do this same thing again for the client, and then again for the lines! The result is that I have taken one array of data, but used the .map method three different times! While the animations look amazing, I think I'll need to restructure the way I'm generating these markers, or re-join them based on the bookingId in order to get the animation effect I am looking for? Any ideas are much appreciated!

Link to comment
Share on other sites

Yeah, sorry, I'm a little lost especially without a demo. It kinda sounds like you're asking a data structuring question rather than a GSAP-related one. (?) 

 

The solution I gave you actually does seem like it'd accomplish what you're after. But maybe I'm misunderstanding. You need to add another numeric parameter to your staggerFrom() calls (at the end) to control where they start getting inserted into the timeline. Perhaps the stagger amount is "1" (because you wanted 1 second inbetween each individual customer) and then the position parameter for the 2nd staggerFrom() would be something like 0.1, and then the position parameter for the 3rd staggerFrom() might be 0.2. That way, the customer marker animates first, quickly followed by the client marker, and then finally the line...and then it starts again for the next customer marker, and so on. 

 

If you still need some help, please provide a reduced case in codepen so we can see what you're working with. The less code the better :)

  • Like 2
Link to comment
Share on other sites

Wow Mikel! Thanks so much, this works exactly like what I was looking for! Thanks Jack as well, and sorry I didn't give you guys a better code example to look at...this is turning into a large full stack application with a fairly complex component tree, but once I have it all working I will update this thread with a simpler codepen example or repo on github for anyone else that is working on data visualization with React and GSAP, and D3 maps! I've been all over the web looking for examples of something like this and haven't found anything, which is surprising to me! The timeline features in GSAP are great for animation, but make this platform a powerful tool for visualizing data over time!

 

I also believed, like Jack, that this was a data structuring issue, and with the data split into three separate arrays, already sorted with .map, that there would be some extra steps involved to pass in the bookingId to the 'target' of your animations...maybe some kind of for loop function  to give 'target' both the classname for the item you want to animate (or ref to expose the dom node where necessary to be react-y), along with the index of each booking or something like that. I'm thrilled that some kind of function to re-join the data was not at all necessary - this only speaks to the depth and flexibility of GSAP. I'm super impressed with both GSAP and the support from this community!  Thank you both!

 

 

  • Like 2
Link to comment
Share on other sites

2 hours ago, UnioninDesign said:

I'm thrilled that some kind of function to re-join the data was not at all necessary - this only speaks to the depth and flexibility of GSAP. I'm super impressed with both GSAP and the support from this community! 

 

Fantastic, love to hear that @UnioninDesign. I hope your project goes smoothly, and thanks for your willingness to share solutions (eventually). 

 

21 minutes ago, mikel said:

Maybe we still get an idea from our experts, if and how svg data can be cycled.

 

I didn't quite understand what you were asking here, @mikel. Can you elaborate a bit? How can I help? Are you looking for a way to dynamically create <line> elements with their x1/y1/x2/y2 attributes populated from an array of data? 

  • Like 1
Link to comment
Share on other sites

To chime in on my end, the react-simple-maps (using D3) component library will generate the lines/markers when passing in mercator coordinates for your locations in your data. I do have a few stumbling blocks to sort out with this, for example this library has a <ZoomableGroup> component that makes it easy to zoom in/out on your map...doing so kills the GSAP animations so I'll have to find another way to zoom! (edit: My guess is that the zoom button resets the state of the map...) I appreciate that Mikel took the time to hard code x and y values to create a working example, but at the moment I don't believe a relationship exists in between the coordinates to points on the map v.  the x and y coordinates used by GSAP?

Link to comment
Share on other sites

I really appreciate the help on this! I did get the sequence timed out the way I want, but had a follow up questions on callbacks in case either of you two gentleman would like to kick this can a little further down the road?

 

Case: so we're building a map, and using this animation I'll get  report every 15 minutes with 100 - 500 bookings to display! Now that I've got a nice stagger effect so you don't see all the bookings rendered simultaneously, I'm noticing that the map starts to get cluttered? I'd like to write some kind of callback (if necessary) to make the bookings fade out after 5-7 seconds ish? I think I'm close but haven't quite nailed it just yet :-)

 

Here's my animation code - with some parts adapted from your codepens. I also decided to add a sidebar that show text about in each booking so it's not just a bunch of dots on the screen - the target class ".animatedTick" leads off the timeline:

this.tl = new TimelineMax(); // parameter: { onUpdate: updateSlider }
var next = 3;
var bookingDuration = 6;
this.tl
.staggerFrom(
".animatedTick",
3,
{
rotationY: -180,
ease: SlowMo.easeOut,
y: 700,
 
delay: 1,
scale: 2,
opacity: 0,
autoAlpha: 1,
timeResolution: bookingDuration
},
3,
"nextBooking -=3"
)
.staggerFrom(
".custMarkers",
1.5,
{
cycle: { x: [-1000, 1000], y: [-1000, 1000] },
bezier: {
curviness: 1,
values: [{ x: 0, y: 0 }, { x: 125, y: -80 }, { x: 250, y: 0 }]
},
ease: Power1.easeOut,
opacity: 0,
scale: 10,
autoAlpha: 1,
timeResolution: bookingDuration
},
next,
1
)
.staggerFrom(
".clientMarkers",
1.5,
{
cycle: { x: [-1000, 1000], y: [-1000, 1000] },
bezier: {
curviness: 1,
values: [{ x: 0, y: 0 }, { x: 125, y: -80 }, { x: 250, y: 0 }]
},
ease: Power1.easeOut,
opacity: 0,
scale: 7,
autoAlpha: 1,
timeResolution: bookingDuration
},
next,
1.5
)
.staggerFrom(
".lineAmation",
2.1,
{
ease: Sine.easeIn,
opacity: 0,
autoAlpha: 1,
timeResolution: bookingDuration
},
next,
2,
"-=1"
)

I've tried chaining to the end of this with a few different methods, am I close?

 

This one gives me an error 'delayed call is not a function' - should this be passed in as params to the timeline? I have this one chained right after the last staggerFrom...

 

.delayedCall(bookingDuration, bookingFadeout);
function bookingFadeout() {
this.tl.to(
[".animatedTick", ".custMarkers", ".clientMarkers", ".lineAmation"],
bookingDuration,
{ autoAlpha: 0, opacity: 0 }
);
}

 

 

And i also tried to chain this to end of the sequence...

.to(
[".animatedTick", ".custMarkers", ".clientMarkers", ".lineAmation"],
5,
{ opacity: 0, ease: Power1.easeInOut, autoAlpha: 0 }

 

Which does nothing? Another idea was to find a way to go by the number of repeats, but the onRepeat() method executes after each repeat, not say, every 6, 7 or 8 repeats? I have a few things going on here, like trying to set the timeResolution in each staggerFrom tween equal to a global variable called 'bookingDuration'  - also not working...

 

Any ideas are appreciated!

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...