codesuki / react-d3-components

D3 Components for React
http://codesuki.github.io/react-d3-components/example.html
MIT License
1.62k stars 205 forks source link

Dynamic sizes of charts #9

Open hemm1 opened 9 years ago

hemm1 commented 9 years ago

Cool library!

We are trying to use this on our project to create a pretty straightforward bar chart, but we are having a hard time making the chart responsive. It seems to me that there is no way around specifying the width and the height of a chart when you create it. We might be missing something, though.

For now, we are trying out a workaround where we specify the width and height of the BarChart based on the viewport height and width. I am not completely satisfied with it. In my mind, it would be ideal if there was a way to make the chart stretch to 100% of its container's size. Do you have any plans to implement something like that? Or do you think it is even technically possible?

codesuki commented 9 years ago

Thank you, I am glad the library is useful for you!

I think that should be possible. Let me try around how to do it and I will come back to you. Are you embedding the Chart in a another React Component?

hemm1 commented 9 years ago

Great, thanks!

Yes, our whole application is written in React. I was briefly trying out something myself, but it couldn't quickly figure out a way of knowing the width of the containing component before it is actually rendered, because I guess it will need to render all it's child components before it actually can be rendered itself. I'm quite new to React, though, so there is a good chance that this is still possible.

Craga89 commented 9 years ago

I actually implemented something for this a couple of days ago, take a look (ES6 ahead):

let AreaGraph = React.createClass({
    mixins: [React.addons.PureRenderMixin],

    getInitialState() {
        return {
            parentWidth: 0
        }
    },

    getDefaultProps() {
        return {
            width: '100%',
            height: 300,
            margin: { left: -1, top: 10, bottom: 0, right: 1 }
        }
    },

    handleResize(e) {
        let elem = this.getDOMNode();
        let width = elem.offsetWidth;

        this.setState({
            parentWidth: width
        });
    },

    componentDidMount() {
        if(this.props.width === '100%') {
            window.addEventListener('resize', this.handleResize);
        }
        this.handleResize();
    },

    componentWillUnmount() {
        if(this.props.width === '100%') {
            window.removeEventListener('resize', this.handleResize);
        }
    },

    render() {
        let { width, height, margin, xScale, yScale, xAxis, ...props } = this.props;

        // Determine the right graph width to use if it's set to be responsive
        if(width === '100%') {
            width = this.state.parentWidth || 400;
        }

        // Set scale ranges
        xScale && xScale.range([0, width - (margin.left + margin.right)]);
        yScale && yScale.range([height - (margin.top + margin.bottom), 0]);

        return (
            <div className={"usage__cpu__graph "+props.className}>
                <AreaChart
                    ref="chart"
                    width={width}
                    height={height}
                    margin={margin} 
                    xScale={xScale}
                    yScale={yScale}
                    xAxis={xAxis}
                    {...props} 
                />
            </div>
        );
    }
})

This could definitely be improved on, most notably by debouncing the the resize event with requestAnimationFrame to prevent janking, but overall it works well

codesuki commented 9 years ago

Cool! That's how I was thinking to do it too, with resize listeners. Very nice! You could also use RxJS to get the resize event and debounce with that :) I am thinking of breaking apart the Chart components a little bit more, because I thought it would be cool if we could just render a line chart inside the brush for example (like: http://bl.ocks.org/mbostock/1667367 ) And then making a resizable version of the Chart would be very nice! In case this library manages to get 500 stars I will be able to work 1 day per week full time on it :)

hemm1 commented 9 years ago

Wow, cool fix, Craga89! I will definitely try this out on our when I get the time!

You have gotten a few more stars from our team at least, codesuki ;) Keep up the good work :+1:

codesuki commented 9 years ago

Thanks! I Appreciate that :) Next on my list are Transitions and Zoom.

luisrudge commented 9 years ago

I just contributed with 1 star ;) But I think we really need something easier to handle dynamic sizes. I'd like to use 100% for width without having to have this logic in all my components. Is that possible?

codesuki commented 9 years ago

Thanks! And yes, I think that is possible. Recently have been too busy with other things but this week I should have some time again to improve everything.

luisrudge commented 9 years ago

Thanks!

joshhornby commented 9 years ago

Any update on this @codesuki would love to see this feature.

collinwu commented 9 years ago

Just started a project importing this library, but really wanted to use variable widths. Any update on native support for this yet? Is Craga89's example of extending some of the existing components in the library the way to go?

codesuki commented 9 years ago

It's the way to go. I am still working on improving all the parts (slowly these days) but since I will probably implement it in the same or a very similar way it's just a matter of maybe removing code in the future.

collinwu commented 9 years ago

awesome! thanks for the note.

hopefully I can contribute to the repo. :D

codesuki commented 9 years ago

That would be great and very welcome!

f15gdsy commented 9 years ago

Based on @Craga89 's code, I wrote a wrapper to make the charts responsive universally.

How to use

import {ResponsiveBarChart
ResponsiveLineChart,
ResponsiveAreaChart,
ResponsiveScatterPlot,
ResponsivePieChart} from './ResponsiveChart';

// the <div> tag is used to position and wrap the chart.
<div style={myPositionStyle}>
    <ResponsiveLineChart data={myData} />
</div>

Code

'use strict';

import React from 'react';
import D3 from 'react-d3-components';

let {
    BarChart,
    LineChart,
    AreaChart,
    ScatterPlot,
    PieChart
} = D3;

let createClass = function (chartType) {
    return React.createClass({
        getDefaultProps () {
            return {
                 margin: {top: 25, bottom: 25, left: 25, right: 25}
            };
        },

        getInitialState () {
            return {
                size: {w: 0, h: 0}
            };
        },

        componentDidMount () {
            this.fitToParentSize();
        },

        componentWillReceiveProps () {
            this.fitToParentSize();
        },

        fitToParentSize () {
            let elem = this.getDOMNode();
            let w = elem.parentNode.offsetWidth;
            let h = elem.parentNode.offsetHeight;
            let currentSize = this.state.size;

            if (w !== currentSize.w || h !== currentSize.h) {
                this.setState({
                    size: {
                        w: w,
                        h: h
                    }
                });
            }
        },

        getChartClass () {
            let Component;

            switch (chartType) {
                case 'BarChart':
                    Component = BarChart;
                    break;
                case 'LineChart':
                    Component = LineChart;
                    break;
                case 'AreaChart':
                    Component = AreaChart;
                    break;
                case 'ScatterPlot':
                    Component = ScatterPlot;
                    break;
                case 'PieChart':
                    Component = PieChart;
                    break;
                default:
                    console.error('Invalid Chart Type name.');
                    break;
            }

            return Component;
        },

        render () {
            // Component name has to start with CAPITAL
            let Component = this.getChartClass();

            let {width, height, margin, ...others} = this.props;

            width = this.state.size.w || 100;
            height = this.state.size.h || 100;

            return (
                <Component
                    width = {width}
                    height = {height}
                    margin = {margin}
                    {...others}
                />
            );
        }
    });
};

let ResponsiveBarChart = createClass('BarChart');
let ResponsiveLineChart = createClass('LineChart');
let ResponsiveAreaChart = createClass('AreaChart');
let ResponsiveScatterPlot = createClass('ScatterPlot');
let ResponsivePieChart = createClass('PieChart');

export {
    ResponsiveBarChart,
    ResponsiveLineChart,
    ResponsiveAreaChart,
    ResponsiveScatterPlot,
    ResponsivePieChart
};

export default {
    ResponsiveBarChart: ResponsiveBarChart,
    ResponsiveLineChart: ResponsiveLineChart,
    ResponsiveAreaChart: ResponsiveAreaChart,
    ResponsiveScatterPlot: ResponsiveScatterPlot,
    ResponsivePieChart: ResponsivePieChart
};
codesuki commented 9 years ago

That was fast! Thanks for contributing! I will integrate your code if that's OK with you. As soon as I find some time.

f15gdsy commented 9 years ago

I'm glad that I can help! :)

amicke commented 8 years ago

@codesuki any ETA on the responsive chart? Would love to see this feature.

codesuki commented 8 years ago

A little bird told me there are some contributions coming in soon and this is on the list as far as I know. Personally I am out of resources :(

codesuki commented 8 years ago

Could just copy and paste the code from above until then. Let me see.

devholic commented 8 years ago

I added resizing event listener to @f15gdsy 's wrapper. 😄

import React from 'react';
import ReactDOM from 'react-dom';
import D3 from 'react-d3-components';

const {
  BarChart,
  LineChart,
  AreaChart,
  ScatterPlot,
  PieChart,
} = D3;

const createClass = (chartType) => {
  class Chart extends React.Component {
    constructor() {
      super();
      this.state = { size: { w: 0, h: 0 } };
    }

    fitToParentSize() {
      const elem = ReactDOM.findDOMNode(this);
      const w = elem.parentNode.offsetWidth;
      const h = elem.parentNode.offsetHeight;
      const currentSize = this.state.size;
      if (w !== currentSize.w || h !== currentSize.h) {
        this.setState({
          size: { w, h },
        });
      }
    }

    getChartClass() {
      let Component;
      switch (chartType) {
        case 'BarChart':
          Component = BarChart;
          break;
        case 'LineChart':
          Component = LineChart;
          break;
        case 'AreaChart':
          Component = AreaChart;
          break;
        case 'ScatterPlot':
          Component = ScatterPlot;
          break;
        case 'PieChart':
          Component = PieChart;
          break;
        default:
          console.error('Invalid Chart Type name.');
          break;
      }
      return Component;
    }

    componentDidMount() {
      window.addEventListener('resize', ::this.fitToParentSize);
      this.fitToParentSize();
    }

    componentWillReceiveProps() {
      this.fitToParentSize();
    }

    componentWillUnmount() {
      window.removeEventListener('resize', ::this.fitToParentSize);
    }

    render() {
      let Component = this.getChartClass();
      let { width, height, margin, ...others } = this.props;

      width = this.state.size.w || 100;
      height = this.state.size.h || 100;

      return (
        <Component
          width = {width}
          height = {height}
          margin = {margin}
          {...others}
        />
      );
    }
  }
  Chart.defaultProps = {
    margin: {
      top: 50,
      bottom: 50,
      left: 50,
      right: 50,
    },
  };
  Chart.propTypes = {
    width: React.PropTypes.number,
    height: React.PropTypes.number,
    margin: React.PropTypes.object,
  };
  return Chart;
};

const ResponsiveBarChart = createClass('BarChart');
const ResponsiveLineChart = createClass('LineChart');
const ResponsiveAreaChart = createClass('AreaChart');
const ResponsiveScatterPlot = createClass('ScatterPlot');
const ResponsivePieChart = createClass('PieChart');

export {
  ResponsiveBarChart,
  ResponsiveLineChart,
  ResponsiveAreaChart,
  ResponsiveScatterPlot,
  ResponsivePieChart,
};

export default {
  ResponsiveBarChart,
  ResponsiveLineChart,
  ResponsiveAreaChart,
  ResponsiveScatterPlot,
  ResponsivePieChart,
};
codesuki commented 8 years ago

I'll add that in the project. I hope that's ok!

devholic commented 8 years ago

😄

JLongley commented 7 years ago

Note: to get this to work with React 15, you'll need to replace

const elem = ReactDOM.findDOMNode(this);

with

      import  ReactDOM from 'react-dom';
      ...
      let elem = ReactDOM.findDOMNode(this);

as per http://stackoverflow.com/questions/29527309/react-0-13-this-getdomnode-equivalent-to-react-finddomnode