sghall / resonance

:black_medium_small_square:Resonance | 5kb React animation library
https://sghall.github.io/resonance
MIT License
1k stars 27 forks source link

[Question] Pie chart example #21

Closed YouriT closed 7 years ago

YouriT commented 7 years ago

Greetings

First of all I would like to thank you for this. It definitely looks like the solution I was looking for, just need to get to know it a bit better. As a start point, I tried to use your bar-chart and convert it to a pie chart. I think I'm almost there but still I can't get the transition work properly.

If you could give me a quick hint on what's going on that would save my day!

Happy to build you a proper PR out of it once it's working to add it your examples following your pattern of course as a way to thank you! With a proper style review (I know it's a bit messy right now...)

Code

import React from 'react';
import { shuffle } from 'lodash';
import { NodeGroup } from 'resonance';
import { scaleBand } from 'd3-scale';
import { pie, arc } from 'd3-shape';
import { easeLinear } from 'd3-ease';

const mockData = [
  {
    name: 'Linktype',
    value: 45,
  }, {
    name: 'Quaxo',
    value: 53,
  }, {
    name: 'Skynoodle',
    value: 86,
  }, {
    name: 'Realmix',
    value: 36,
  }, {
    name: 'Jetpulse',
    value: 54,
  }, {
    name: 'Chatterbridge',
    value: 91,
  }, {
    name: 'Riffpedia',
    value: 67,
  }, {
    name: 'Layo',
    value: 12,
  }, {
    name: 'Oyoba',
    value: 69,
  }, {
    name: 'Ntags',
    value: 17,
  }, {
    name: 'Brightbean',
    value: 73,
  }, {
    name: 'Blogspan',
    value: 25,
  }, {
    name: 'Twitterlist',
    value: 73,
  }, {
    name: 'Rhycero',
    value: 67,
  }, {
    name: 'Trunyx',
    value: 52,
  }, {
    name: 'Browsecat',
    value: 90,
  }, {
    name: 'Skinder',
    value: 88,
  }, {
    name: 'Tagpad',
    value: 83,
  }, {
    name: 'Gabcube',
    value: 6,
  }, {
    name: 'Jabberstorm',
    value: 19,
  },
];

const view = [300, 250];      // [width, height]
const trbl = [10, 10, 10, 10]; // [top, right, bottom, left] margins

export class PieChart extends React.Component {
  state = {
    data: shuffle(mockData).slice(0, 15),
  };

  update = () => {
    this.setState({
      data: shuffle(mockData).slice(0, 15),
    });
  }

  render() {
    const pieMe = pie()
      .value((d) => {
        return d.value;
      })
      .padAngle(0.02)
      .sort(null);

    const newData = pieMe(this.state.data);

    const radius = view[0];

    const outerRadius = radius / 2;
    const innerRadius = radius / 3.3;

    const arcMe = arc()
      .outerRadius(outerRadius)
      .innerRadius(innerRadius);

    function arcTween(data) {
      return () => {
        return arcMe(data);
      };
    }

    const update = this.update;

    return (
      <div>
        <button onClick={this.update}>
          Update
        </button>
        <span style={{ margin: 5 }}>
          Bar Count: {this.state.data.length}
        </span>
        <svg width={view[0]} height={view[0]}>
          <g transform={`translate(${view[0] / 2},${view[0] / 2})`}>
            <NodeGroup
              data={newData}
              keyAccessor={(d) => d.data.name}

              start={(node) => {
                return {
                  opacity: 0.6,
                  d: arcMe(node),
                  timing: { duration: 2000, ease: easeLinear },
                };
              }}

              enter={(node) => {
                return {
                  fill: 'blue',
                };
              }}

              update={(node) => {
                return {
                  opacity: 0.6,
                  d: arcTween(node),
                  timing: { duration: 2000 },
                };
              }}

              leave={(node, index, remove) => {
                return {
                  fill: 'red',
                  timing: { duration: 1500 },
                  events: { end: remove },
                };
              }}

              render={(data, state) => {
                const { d } = state;

                return (
                  <path d={d} />
                );
              }}
            />
          </g>
        </svg>
      </div>
    );
  }
}
sghall commented 7 years ago

Hey. Thanks. Appreciate it! Sure, if you want to do a PR with a pie chart example that would be great. I think a lot of people would be looking for that. I kind of hacked what you had a little. Needs some work, but it does animate the arcs. The trick with the pie charts is caching the previous arc data.

Here's the basic idea...

import React, { PureComponent } from 'react';
import NodeGroup from 'resonance/NodeGroup';
import Surface from 'docs/src/components/Surface';
import { pie, arc } from 'd3-shape';
import { scaleOrdinal } from 'd3-scale';
import { interpolate } from 'd3-interpolate';
import { shuffle } from 'd3-array';

const colors = scaleOrdinal()
  .range([
    '#6C6B74',
    '#2E303E',
    '#9199BE',
    '#54678F',
    '#212624',
    '#3A4751',
    '#1E272E',
    '#0A151D',
    '#030C12',
    '#253517',
    '#5D704E',
    '#324027',
    '#19280C',
    '#0D1903',
  ]);

// **************************************************
//  SVG Layout
// **************************************************
const view = [1000, 250];      // [width, height]
const trbl = [10, 10, 10, 10]; // [top, right, bottom, left] margins

const dims = [ // Adjusted dimensions [width, height]
  view[0] - trbl[1] - trbl[3],
  view[1] - trbl[0] - trbl[2],
];

// **************************************************
//  Mock Data
// **************************************************
const mockData = [
  {
    name: 'Linktype',
    value: 45,
  }, {
    name: 'Quaxo',
    value: 53,
  }, {
    name: 'Skynoodle',
    value: 86,
  }, {
    name: 'Realmix',
    value: 36,
  }, {
    name: 'Jetpulse',
    value: 54,
  }, {
    name: 'Chatterbridge',
    value: 91,
  }, {
    name: 'Riffpedia',
    value: 67,
  }, {
    name: 'Layo',
    value: 12,
  }, {
    name: 'Oyoba',
    value: 69,
  }, {
    name: 'Ntags',
    value: 17,
  }, {
    name: 'Brightbean',
    value: 73,
  }, {
    name: 'Blogspan',
    value: 25,
  }, {
    name: 'Twitterlist',
    value: 73,
  }, {
    name: 'Rhycero',
    value: 67,
  }, {
    name: 'Trunyx',
    value: 52,
  }, {
    name: 'Browsecat',
    value: 90,
  }, {
    name: 'Skinder',
    value: 88,
  }, {
    name: 'Tagpad',
    value: 83,
  }, {
    name: 'Gabcube',
    value: 6,
  }, {
    name: 'Jabberstorm',
    value: 19,
  },
];

const getArc = pie()
  .value((d) => d.value);

const arcPath = arc()
  .innerRadius(0)
  .outerRadius((dims[1] / 2));

function arcTween(beg, end) {
  const i = interpolate(beg, end);

  return (t) => {
    return arcPath(i(t));
  };
}

class Example extends PureComponent {
  constructor(props) {
    super(props);

    const data = shuffle(mockData).slice(0, 10);

    this.state = {
      data,
      arcs: getArc(data),
      arcsCache: {},
    };
  }

  update = () => {
    this.setState((prevState) => {
      const data = shuffle(mockData).slice(0, 10);

      return {
        data,
        arcs: getArc(data),
        arcsCache: prevState.arcs.reduce((m, n) => {
          return Object.assign(m, { [n.data.name]: n });
        }, {}),
      };
    });
  }

  render() {
    const { arcs, arcsCache } = this.state;

    return (
      <div>
        <button onClick={this.update}>
          Update
        </button>
        <span style={{ margin: 5 }}>
          Arcs Count: {arcs.length}
        </span>
        <Surface view={view} trbl={trbl}>
          <g transform={`translate(${dims[0] / 2}, ${dims[1] / 2})`}>
            <NodeGroup
              data={arcs}
              keyAccessor={(d) => d.data.name}

              start={(d) => ({
                fill: colors(d.data.name),
                d: arcPath(d),
                opacity: 0,
              })}

              enter={() => ({
                opacity: [1],
                timing: { duration: 1500 },
              })}

              update={(d) => ({
                opacity: [1],
                d: arcTween(arcsCache[d.data.name], d),
                timing: { duration: 1500 },
              })}

              leave={(d, index, remove) => {
                remove();
              }}

              render={(data, state) => {
                return (
                  <path key={data.data.name} {...state} />
                );
              }}
            />
          </g>

        </Surface>
      </div>
    );
  }
}

export default Example;
sghall commented 7 years ago

Wait, you don't need that "key" in the render function. Should be just...

render={(data, state) => (
  <path {...state} />
)}
YouriT commented 7 years ago

Great thank you very much. Indeed I even saw this trick about caching on bloks but somehow I did not implement it... Anyhow thanks a lot :)