Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rendering D3 code that is not hard coded

Tags:

reactjs

d3.js

I notice that in much of the D3 documentation the charts, graphs, margins and so on are often hard coded. The xAxis is 500px, etc.. This isn't very helpful for me. So I'm trying to think of how I can accomplish a dynamic approach to rendering D3 content in React.

For example, in the following code I am simply rendering a line based on some time-series stock-price data. I have some D3 code in componentDidMount but given the way D3 works it wants concrete width and height values. But in componentDidMount I don't have those values yet. Lets say that this single line plot is one of 100 other plots each in a div in a grid layout.

So how can I get the width/height of the div/svg and only then compute my D3 code and render svgs?

componentDidMount() {

        console.log("componentDidMount")


        const data = this.props.data;
        const selectX = this.props.selectX;
        const selectY = this.props.selectY;

        console.log(data)

        const xScale = d3ScaleTime()
            .domain(d3ArrayExtent(data, selectX))
            .range([0, 1]);

        const yScale = d3ScaleTime()
            .domain(d3ArrayExtent(data, selectY))
            .range([1, 0]);

        const xAxis = d3AxisBottom()
            .scale(xScale)
            .ticks(data.length / 8);

        const yAxis = d3AxisLeft()
            .scale(yScale)
            .ticks(3);

        const selectScaledX = datum => xScale(selectX(datum));
        const selectScaledY = datum => yScale(selectY(datum));

        const sparkLine = d3Line()
            .x(selectScaledX)
            .y(selectScaledY);

        const linePath = sparkLine(data);

        console.log(linePath);

        this.setState({
            linePath: linePath
        });
    }
like image 284
Alex Bollbach Avatar asked Oct 28 '22 23:10

Alex Bollbach


1 Answers

I tried to imitate your issue in the demo below.

class Chart extends React.Component {
  componentDidMount() {
  	var data = this.props.data;
		var containerDOMElementWidth = ReactDOM.findDOMNode(this).getBoundingClientRect().width
    var chartHeight = containerDOMElementWidth / 2;

		this.drawChart(data, containerDOMElementWidth, chartHeight);
  }
  
  drawChart(data, chartWidth, chartHeight) {
    var margin = { top: 30, right: 20, bottom: 30, left: 50 };

    var width = chartWidth - margin.left - margin.right;
    var height = chartHeight - margin.top - margin.bottom;

    var parseDate = d3.timeParse("%d-%b-%y");

    var x = d3.scaleTime().range([0, width]);
    var y = d3.scaleLinear().range([height, 0]);

    var xAxis = d3.axisBottom().scale(x)
        .ticks(2);

    var yAxis = d3.axisLeft().scale(y)
        .ticks(2);

    var valueline = d3.line()
        .x(function (d) {
          return x(d.date);
        })
        .y(function (d) {
          return y(d.close);
        });

    var svg = d3.select("body")
        .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    data.forEach(function (d) {
        d.date = parseDate(d.date);
        d.close = +d.close;
    });

    // Scale the range of the data
    x.domain(d3.extent(data, function (d) {
        return d.date;
        }));
    y.domain([0, d3.max(data, function (d) {
        return d.close;
        })]);

    svg.append("path").attr('class', 'line-chart') // Add the valueline path.
    .attr("d", valueline(data));

    svg.append("g") // Add the X Axis
    .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

    svg.append("g") // Add the Y Axis
    .attr("class", "y axis")
        .call(yAxis);
  }
  
  render() {
    return <div></div>;
  }
}

function getRandomData() {
	return [{
        date: "1-May-12",
        close: Math.random() * 90
    }, {
        date: "30-Apr-12",
        close: Math.random() * 90
    }, {
        date: "27-Apr-12",
        close: Math.random() * 90
    }, {
        date: "26-Apr-12",
        close: Math.random() * 90
    }, {
        date: "25-Apr-12",
        close: Math.random() * 90
    }];
}

ReactDOM.render(
  <div className="charts-container">
    <div className="chart-wrapper">
      <Chart data={getRandomData()} />
    </div>
        <div className="chart-wrapper">
      <Chart data={getRandomData()} />
    </div>
  </div>,
  document.getElementById('container')
);
.line-chart {
  fill: none;
  stroke: blue
}

.charts-container {
  display: flex;
}

.chart-wrapper {
  width: 100%;
}
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
<div id="container">
</div>

Here we draw two charts one by one and calculate their width and height this way:

componentDidMount() {
  var data = this.props.data;

  // gets the width of container div element with ReactDOM.findDOMNode
  var containerDOMElementWidth = ReactDOM.findDOMNode(this).getBoundingClientRect().width

  // chart height have to be a half of width 
  var chartHeight = containerDOMElementWidth / 2;

  // pass width and height as an arguments
  this.drawChart(data, containerDOMElementWidth, chartHeight);
}

Our drawChart method look like:

drawChart(data, chartWidth, chartHeight) {
  var margin = { top: 30, right: 20, bottom: 30, left: 50 };

  var width = chartWidth - margin.left - margin.right;
  var height = chartHeight - margin.top - margin.bottom;
  ... // code for the chart drawing 

render:

ReactDOM.render(
  <div className="charts-container">
    <div className="chart-wrapper">
      <Chart data={getRandomData()} />
    </div>
    <div className="chart-wrapper">
      <Chart data={getRandomData()} />
    </div>
  </div>,
  document.getElementById('container')
);

If we will render only one chart it also works fine without any code changing because of we calculate the width of the chart as width of parent div element:

class Chart extends React.Component {
  componentDidMount() {
  	var data = this.props.data;
		var containerDOMElementWidth = ReactDOM.findDOMNode(this).getBoundingClientRect().width
    var chartHeight = containerDOMElementWidth / 2;

		this.drawChart(data, containerDOMElementWidth, chartHeight);
  }
  
  drawChart(data, chartWidth, chartHeight) {
    var margin = { top: 30, right: 20, bottom: 30, left: 50 };

    var width = chartWidth - margin.left - margin.right;
    var height = chartHeight - margin.top - margin.bottom;

    var parseDate = d3.timeParse("%d-%b-%y");

    var x = d3.scaleTime().range([0, width]);
    var y = d3.scaleLinear().range([height, 0]);

    var xAxis = d3.axisBottom().scale(x)
        .ticks(2);

    var yAxis = d3.axisLeft().scale(y)
        .ticks(2);

    var valueline = d3.line()
        .x(function (d) {
          return x(d.date);
        })
        .y(function (d) {
          return y(d.close);
        });

    var svg = d3.select("body")
        .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    data.forEach(function (d) {
        d.date = parseDate(d.date);
        d.close = +d.close;
    });

    // Scale the range of the data
    x.domain(d3.extent(data, function (d) {
        return d.date;
        }));
    y.domain([0, d3.max(data, function (d) {
        return d.close;
        })]);

    svg.append("path").attr('class', 'line-chart') // Add the valueline path.
    .attr("d", valueline(data));

    svg.append("g") // Add the X Axis
    .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

    svg.append("g") // Add the Y Axis
    .attr("class", "y axis")
        .call(yAxis);
  }
  
  render() {
    return <div></div>;
  }
}

function getRandomData() {
	return [{
        date: "1-May-12",
        close: Math.random() * 90
    }, {
        date: "30-Apr-12",
        close: Math.random() * 90
    }, {
        date: "27-Apr-12",
        close: Math.random() * 90
    }, {
        date: "26-Apr-12",
        close: Math.random() * 90
    }, {
        date: "25-Apr-12",
        close: Math.random() * 90
    }];
}

ReactDOM.render(
  <div className="charts-container">
    <div className="chart-wrapper">
      <Chart data={getRandomData()} />
    </div>
  </div>,
  document.getElementById('container')
);
.line-chart {
  fill: none;
  stroke: blue
}

.charts-container {
  display: flex;
}

.chart-wrapper {
  width: 100%;
}
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
<div id="container">
</div>

The same thing for three charts:

class Chart extends React.Component {
  componentDidMount() {
  	var data = this.props.data;
		var containerDOMElementWidth = ReactDOM.findDOMNode(this).getBoundingClientRect().width
    var chartHeight = containerDOMElementWidth / 2;

		this.drawChart(data, containerDOMElementWidth, chartHeight);
  }
  
  drawChart(data, chartWidth, chartHeight) {
    var margin = { top: 30, right: 20, bottom: 30, left: 50 };

    var width = chartWidth - margin.left - margin.right;
    var height = chartHeight - margin.top - margin.bottom;

    var parseDate = d3.timeParse("%d-%b-%y");

    var x = d3.scaleTime().range([0, width]);
    var y = d3.scaleLinear().range([height, 0]);

    var xAxis = d3.axisBottom().scale(x)
        .ticks(2);

    var yAxis = d3.axisLeft().scale(y)
        .ticks(2);

    var valueline = d3.line()
        .x(function (d) {
          return x(d.date);
        })
        .y(function (d) {
          return y(d.close);
        });

    var svg = d3.select("body")
        .append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    data.forEach(function (d) {
        d.date = parseDate(d.date);
        d.close = +d.close;
    });

    // Scale the range of the data
    x.domain(d3.extent(data, function (d) {
        return d.date;
        }));
    y.domain([0, d3.max(data, function (d) {
        return d.close;
        })]);

    svg.append("path").attr('class', 'line-chart') // Add the valueline path.
    .attr("d", valueline(data));

    svg.append("g") // Add the X Axis
    .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

    svg.append("g") // Add the Y Axis
    .attr("class", "y axis")
        .call(yAxis);
  }
  
  render() {
    return <div></div>;
  }
}

function getRandomData() {
	return [{
        date: "1-May-12",
        close: Math.random() * 90
    }, {
        date: "30-Apr-12",
        close: Math.random() * 90
    }, {
        date: "27-Apr-12",
        close: Math.random() * 90
    }, {
        date: "26-Apr-12",
        close: Math.random() * 90
    }, {
        date: "25-Apr-12",
        close: Math.random() * 90
    }];
}

ReactDOM.render(
  <div className="charts-container">
    <div className="chart-wrapper">
      <Chart data={getRandomData()} />
    </div>
        <div className="chart-wrapper">
      <Chart data={getRandomData()} />
    </div>
        <div className="chart-wrapper">
      <Chart data={getRandomData()} />
    </div>
  </div>,
  document.getElementById('container')
);
.line-chart {
  fill: none;
  stroke: blue
}

.charts-container {
  display: flex;
}

.chart-wrapper {
  width: 100%;
}
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.11.0/d3.min.js"></script>
<div id="container">
</div>
like image 55
Mikhail Shabrikov Avatar answered Nov 15 '22 06:11

Mikhail Shabrikov