React.js 中使用 D3.js 动态更新数据

目录

这两天尝试下 React.js,将上篇文章的 loadleveler 小应用重写一遍。

最近几天有点儿迷茫,不知道前进方向。去研究 js 库不是个好现象,应该去思考业务相关的事情,而不是一个又一个地看新东西,没了解多少后就抛在一边。

回归正题,需要重写的应用使用 D3 绘制条形图,并动态更新。React 中每个组件都有一个状态 state,当 state 改变时,就会重新绘制组件。而我希望使用 D3 的过渡动画来体现数据变化,不需要重新绘制组件。在 StackOverflow 上找到我需要的方法,初学 React 的我,感觉到 React 的强大。

创建 React 组件

var LoadlevelerTypeChartBoard = React.createClass({
    render: function(){
        return (
            <svg></svg>
        );
    }
});

React 在绘制组件时调用 render 函数,这里我只需要一个 svg 标签,剩下内容由 d3 代码生成。

设计动态数据

设计 React 组件最好的方式是先创建一个静态组件,再添加动态数据。我改写应用时不了解这点,直接使用 state 保存私有数据,代码不够美观,以后花时间再改改看吧。

var LoadlevelerTypeChartBoard = React.createClass({
    getInitialState: function() {
        var component = this;
        var llq_type_chart_svg_attr = {
            height: 180,
            width: 550
        };
        var llq_type_chart_label_width = 120;
        var margin = 10;
        var llq_type_chart_attr = {
            max_width: llq_type_chart_svg_attr.width - llq_type_chart_label_width - margin,
            bar_height: 20,
            bar_step: 30
        };
        return {
            llq_info: [
                {name: 'total', value: 0},
                {name: 'waiting', value: 0},
                {name: 'pending', value: 0},
                {name: 'running', value: 0},
                {name: 'held', value: 0},
                {name: 'preempted', value: 0}
            ],
            llq_type_chart_attr: llq_type_chart_attr,
            llq_type_chart_label_width: llq_type_chart_label_width,
            margin: margin,
            llq_type_chart_svg_attr: llq_type_chart_svg_attr
        }
    }

getInitialState 函数设置初始状态数据,上述代码中只有 llq_info 变量会变化,其它变量不会变化,应该保存在类似 props 的变量中,但我没找到设置 props 的方法,只好保存在 state 中。

绘制初始组件

可以在 render 中绘制初始组件,我则在 componentDidMount 中绘制组件,该函数在 render 结束后调用一次。

var LoadlevelerTypeChartBoard = React.createClass({
    //...
    componentDidMount: function(){
        var component = this;
        var llq_type_chart_svg = d3.select(this.getDOMNode())
            .attr('id', 'llq_type_chart_2')
            .attr('height', this.state.llq_type_chart_svg_attr.height)
            .attr('width', this.state.llq_type_chart_svg_attr.width);
        var llq_type_chart_bar_group = llq_type_chart_svg;
        var llq_type_chart_label_group = llq_type_chart_svg;
        var bar_chart_label_data = llq_type_chart_label_group.selectAll('.llq-type-label').data(this.state.llq_info);
        var bar_chart_label_enter = bar_chart_label_data
            .enter()
            .append('text')
            .attr('x', this.state.llq_type_chart_label_width - this.state.margin)
            .attr('y', function(d,i){
                return i*component.state.llq_type_chart_attr.bar_step + component.state.llq_type_chart_attr.bar_height;
            })
            .attr('text-anchor', 'end')
            .style('font-size', this.state.llq_type_chart_attr.bar_height)
            .text(function(d){ return d.name; });
        //...
   },
   // ...
});

绘制初始svg图。但这样的图是不会变化的,需要在数据变化时修改组件的 state 值。我使用 socket.io 接受变化的数据,在 componentDidMount 函数中添加 socket 事件响应。

var LoadlevelerTypeChartBoard = React.createClass({
    //...
    componentDidMount: function(){
        //...
        socket.on('send_llq_info', function(msg){
            var total = parseInt(msg.in_queue);
            var waiting = parseInt(msg.waiting);
            var held = parseInt(msg.held);
            var running = parseInt(msg.running);
            var pending = parseInt(msg.pending);
            var preempted = parseInt(msg.preempted);
            component.setState({
                llq_info: [
                    {name: 'total', value: total},
                    {name: 'waiting', value: waiting},
                    {name: 'pending', value: pending},
                    {name: 'running', value: running},
                    {name: 'held', value: held},
                    {name: 'preempted', value: preempted}
                ]
            });
        });
    },
    //...
});

使用 setState 修改组件的状态。

控制组件重绘

默认情况下,state 改变会导致组件重绘。React 提供 shouldComponentUpdate 属性控制组件是否重绘,在不需要重绘的情况下返回 false。我使用 D3 更新 svg,所以在 shouldComponentUpdate 函数中使用 D3 更新图形并返回 false。

var LoadlevelerTypeChartBoard = React.createClass({
    //...
    shouldComponentUpdate: function(nextProps, nextState){
        var component = this;
        var llq_type_chart_svg = d3.select(this.getDOMNode());
        var llq_type_chart_bar_group = llq_type_chart_svg;
        var llq_type_chart_label_group = llq_type_chart_svg;
        var max_number = d3.max(nextState.llq_info, function(d){ return d.value; });
        var x_scale = d3.scale.linear().domain([0, max_number]).range([0, this.state.llq_type_chart_attr.max_width]);
        var bar_chart_data = llq_type_chart_bar_group.selectAll('.llq-type-bar').data(nextState.llq_info);
        var bar_chart_data_enter = bar_chart_data
            .enter()
            .append('rect')
            .attr('class', 'llq-type-bar')
            .attr('height', this.state.llq_type_chart_attr.bar_height)
            .attr('width', function(d){
                return x_scale(d.value);
            })
            .attr('x', this.state.llq_type_chart_label_width)
            .attr('y', function(d,i){
                return i*component.state.llq_type_chart_attr.bar_step;
            })
            .attr('fill', '#ddd');
        var bar_chart_data_modify = bar_chart_data
            .transition()
            .duration(800)
            .attr('width', function(d){
                return x_scale(d.value);
            });
        return false;
    },
    // ...
});

这样就可以实现在 React 中使用 D3 更新数据。