Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SVG Animations with D3 #316

Closed
amcdnl opened this issue Nov 9, 2018 · 16 comments
Closed

SVG Animations with D3 #316

amcdnl opened this issue Nov 9, 2018 · 16 comments

Comments

@amcdnl
Copy link

amcdnl commented Nov 9, 2018

I'm using react-move and wanting to try out react-spring for performance reasons.

When I tried to move the code over, my transitions are not working right; basically i'm moving opacity and x but the console shows the numbers just spinning on forever.

I added duration to the config and it actually rendered but it was really slow.

In react-move I have the following code:

export class ScatterSeries extends Component<ScatterSeriesProps, {}> {
  getKeyAccessor = (data) => {
    return data.key;
 }

  getStart = (data) => {
    const cx = this.props.xScale(data.x);
    const cy = this.props.yScale(data.y);
      return {
        opacity: 0,
        cx,
        cy: this.props.height
      };
  }

  getEnterUpdate = (data, index) => {
    return {
      opacity: [1],
      cx: [this.props.xScale(data.x)],
      cy: [this.props.yScale(data.y)],
      timing: { duration: 300, delay: index * 10 }
    };
  }

  render() {
    const { data, ...rest } = this.props;

    return (
          <NodeGroup
            data={data}
            keyAccessor={this.getKeyAccessor}
            start={this.getStart}
            enter={this.getEnterUpdate}
            update={this.getEnterUpdate}
          >
            {transitionData => (
              <Fragment>
                {transitionData.map((point, i) => (
                  <g key={i} style={{ opacity: point.state.opacity }}>
                    <ScatterPoint
                      {...rest}
                      data={point.data}
                      cx={point.state.cx}
                      cy={point.state.cy}
                    />
                  </g>
                ))}
              </Fragment>
            )}
          </NodeGroup>
    );
  }

my migrated code looks like this:

  getStart(data) {
    const cx = this.props.xScale(data.x);
    const cy = this.props.yScale(data.y);
      return {
        opacity: 0,
        cx,
        cy: this.props.height
      };
  }

  getEnterUpdate(data) {
    return {
      opacity: 1,
      cx: this.props.xScale(data.x),
      cy: this.props.yScale(data.y)
    };
  }

  render() {
    const { data, ...rest } = this.props;

    return (
          <Transition
            items={items}
            keys={d => d.key}
            from={d => this.getStart(d)}
            enter={d => this.getEnterUpdate(d)}
            update={d => this.getEnterUpdate(d)}
            leave={d => this.getStart(d)}
            trail={10}
          >
            {item => props => (
              <g style={{ opacity: props.opacity }}>
                <ScatterPoint
                  {...rest}
                  data={item}
                  cx={props.cx}
                  cy={props.cy}
                />
              </g>
            )}
          </Transition>
    );
  }

Any hints to what it might be would be very helpful!

@drcmda
Copy link
Member

drcmda commented Nov 10, 2018

Can you put this into a codesandbox?

@drcmda
Copy link
Member

drcmda commented Nov 10, 2018

Just by looking at it, props.cx/cy aren’t numbers, they’re classes, Scatterpoint isn’t gonna understand their meaning (unless that’s your own class and it uses animate.g/div/path/etc inside). Remove the „native“ flag first, that’s used when you animate native dom styles, attributes or innerText. The g.style/opacity could be animated like that, but it’s gotta be animated.g in that case. In your case it wouldn’t make much difference because foreign React components can only be animated by rendering out their props 60fps. See: http://react-spring.surge.sh/perf

Btw the accessors can all be:

from={this.getStart}
enter={this.getEnterUpdate}
update={this.getEnterUpdate}
leave={this.getStart}

@amcdnl
Copy link
Author

amcdnl commented Nov 10, 2018

@drcmda - thanks for the feedback. I’ll try to build up a example, gonna be tricky due to number of dependencies but it if helps them absolutely!

Few answers to your questions:

  • cx and cy svg attributes on a circle. They are positions / numbers for the circle.
  • Scatterpoint is just a wrapper for the svg circle with some special styles
  • I don’t believe I have the native flag turned on? I’m planning to do that once I get som initial plumbing done though.
  • ah yes, I was just copying pasting around, th accessors should be changed

Any idea why it wouldn’t be rendering at all except when duration is specified?

@drcmda
Copy link
Member

drcmda commented Nov 10, 2018

Oh, I thought I saw native in there. Duration shouldn’t interfere with rendering. A real example makes it easier for me to help out, often it’s some minor typo or wrong config that messes stuff up, I’ll see it right away. If it’s a bug I’ll fix it.

@amcdnl
Copy link
Author

amcdnl commented Nov 12, 2018

@drcmda - I found the issue. The item count I'm passing is about 150, if I change that to something like 10, it works just fine.

Here is 10 - https://www.screencast.com/t/CZpF3tAG8
Here is 150 - https://www.screencast.com/t/MpfDU8CdjZd

In the 150 scenario it basically never renders. Now if I add duration to the props it will work (though very poorly).

Here is a demo, it shows 10 by default then you can comment out the splice to see it fail. https://codesandbox.io/s/8kqoyx9262

@amcdnl
Copy link
Author

amcdnl commented Nov 14, 2018

Any ideas @drcmda ?

@drcmda
Copy link
Member

drcmda commented Nov 14, 2018

It's probably too many. Transition isn't virtualized and 450 items means 450 keyframes + springs + requestAnimationFrames. "trail" also adds up, 450 * 100ms is 45 seconds delay for the last item.

Springs can take array data, that's always been my approach with stuff like d3, one spring, and interpolated arrays.

<Spring to={{ data: [..........] } ...

@drcmda
Copy link
Member

drcmda commented Nov 14, 2018

Something like this maybe: https://codesandbox.io/embed/wkxz90lj58

This uses a relatively cheap spring and interpolates the trailing difference. Makes me think, maybe supporting config={[....]} array would be a nice addition, that way you could hand out different configs (with additive delay in them).

Edit:

it's even easier, just use a delta of 0-1 and interpolate.

@amcdnl
Copy link
Author

amcdnl commented Nov 14, 2018

@drcmda - Interesting - the spring 0-1 feels like an odd API but I'll give it a try.

@drcmda
Copy link
Member

drcmda commented Nov 14, 2018

made some more edits to clean it up. 0-1 is basically a timeline, start to finish. With a data-set that can grow i wouldn't risk using springs for each and every element.

@amcdnl
Copy link
Author

amcdnl commented Nov 14, 2018

Gotcha.

The API feels a little odd because why would I not ever want it to go from 0 to 1? This specification feels like an implementation detail that should be handled under the hood.

Is there anyway to make the transition api work like this?

How would I handle exits?

Im getting some type errors with what you posted in typescript.
img

Thanks for the help; this seems to solve my problem I was experiencing. You should add this to the demos!

@amcdnl amcdnl closed this as completed Nov 14, 2018
@amcdnl amcdnl reopened this Nov 14, 2018
@amcdnl
Copy link
Author

amcdnl commented Nov 14, 2018

How would I make this animate on the update now?

Example a user resorts the data, it should animate to its new position. I added the reset flag but all that does is make it start as if it were entering again.

I suppose I would need to track the previous and the target positions which is gonna get really nasty.

Also, its only 150, not 450 ;P

@drcmda
Copy link
Member

drcmda commented Nov 15, 2018

in a previous version i've used arrays, that'll work, too. then you can update and track positions. enter/exit type of stuff with hundreds of elements is gonna get hard. Perhaps i can optimize more and use a single RAF queue for all springs, but that won't happen until the next major.

TS is maintained by contributors, i don't know/use it. Could you add a PR?

@amcdnl
Copy link
Author

amcdnl commented Nov 15, 2018

@drcmda - can you show me an example of what you are thinking? Maybe I can help.

@drcmda
Copy link
Member

drcmda commented Nov 15, 2018

array animations like:

<Spring native from={{ x: items.map(d => 0) }} to={{ x: items.map(d => d.x) }}>
  {props => items.map((item, index) => (
    <animated.circle cx={props.x.payload[index]} />

x would be a real entity now that you can change. That doesn't solve list mutations though, when you add, remove or re-order elements. It's a hard problem - transitions are too costly for lots and lots of items, it probably will have to be custom code around a spring in some way or another.

@amcdnl
Copy link
Author

amcdnl commented Nov 15, 2018

This is pretty rough but something like this might work:

const SpringRange = (p) => {
  const { items, children, keys, from, enter, exit, update, ...rest } = p;
  let map = new Map();

  const renderChild = ({ delta }, item, index) => {
    const range = [index / items.length, 1]
    const itemKey = keys(item, index);
    let prevItem = map.get(index);

    console.log(prevItem)

    let fromProps;
    let toProps;
    if (!prevItem) {
      fromProps = from({ item, index });
      toProps = enter({ item, index });
    } else {
      fromProps = prevItem;
      toProps = update({ item, index });
    }

    const props = {};
    for (const k in fromProps) {
      const fromVal = fromProps[k];
      const toVal = toProps[k];
      if (toVal !== undefined) {
        props[k] = delta.interpolate(range, [fromVal, toVal]);
      } else {
        props[k] = fromVal;
      }
    }

    map.set(index, props);

    return children({ item, index, props, key: itemKey });
  };

  return (
    <Spring
      native={true}
      config={{ duration: 1000, easing: easePolyOut }}
      from={{ delta: 0 }}
      to={{ delta: 1 }}
      reset={true}
      {...rest}
    >
      {props => items.map((item, index) => renderChild(props as any, item, index))}
    </Spring>
  )
};

Might need to use a merge diff sort like react-move and react-motion uses too - https://github.com/chenglou/react-motion/blob/master/src/mergeDiff.js

@drcmda drcmda closed this as completed Dec 4, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants