# React 可视化

在学习使用过React一段时间后,感觉React的使用还是比较舒适的。了解React的组件生命周期,路由,及通信你会发现React对于开发真的很便利。

引言
偶然间,看见一篇博客关于React可视化的,自己本身这半年多使用React开发也有一些感触,就借鉴这篇博客写下这篇随文。

参考: React可视化的
antd のdemo: antd-demo

推荐

脚手架

React组件库

React 可视化

回到正题。这篇主要是介绍些React中的可视化使用。

说到可视化,无外乎使用原生的canvas、svg或一些可视化库进行可视化开发。

canvas

class Graphic extends React.Component {
  constructor(){
    super();
    this.state = {
      color: 'green'
    }
    //this.onChange = this.onChange.bind(this);
  }

  componentDidMount(){
    const context = ReactDOM.findDOMNode(document.getElementById('canvas')).getContext('2d');
    this.paint(context);
  }
componentDidUpdate(){
    const context = ReactDOM.findDOMNode(document.getElementById('canvas')).getContext('2d');
  context.clearRect(0, 0, 200, 200);
    this.paint(context);
  }
  paint(context) {
    context.save();
    context.fillStyle = this.state.color;
    context.fillRect(0, 0, 100, 100);
    context.restore();
  }
  onChange(e){
    this.setState({
      color: e.target.value
    });
  }
  render(){
    return (
      <div>
        <canvas id='canvas' width='200' height='200' style= ></canvas>
        <input type='color' value={this.state.color} onChange={::this.onChange}/>
       </div>
    )
  }
}
ReactDOM.render(
  <Graphic />,
  document.getElementById('root')
);

svg

class SVG extends React.Component {
  constructor(){
    super();
  
  }
  render(){
    return (
      <svg width='200' height='200' viewBox='0 0 200 200'>
           <circle cx='50' cy='50' r='20'/>
           <circle cx='100' cy='50' r='20'/>
           <circle cx='75' cy='100' r='40'/>
           <circle cx='60' cy='90' r='10' fill='#fff' />
           <circle cx='90' cy='90' r='10' fill='#fff' />
      </svg>
    )
  }
}
ReactDOM.render(
  <SVG />,
  document.getElementById('root')
);

可视化组件包装

利用一些可视化库插件进行包装

Echarts

  • echarts配置文件
  • echarts渲染

options

const LineScore = (data) => {
    const option = {
    title: {
        text: '近年消费趋势'
    },
    tooltip: {
        trigger: 'axis'
    },
    legend: {
        data:['clothes','foods','home','travel']
    },
    grid: {
        left: '3%',
        right: '4%',
        bottom: '3%',
        containLabel: true
    },
    toolbox: {
        feature: {
            saveAsImage: {}
        }
    },
    xAxis: {
        type: 'category',
        boundaryGap: false,
        data: ['2010','2011','2012','2013','2014','2015','2016']
    },
    yAxis: {
        type: 'value',
        name:'¥(元)'
    },
    series: [
        {
            name:'clothes',
            type:'line',
            smooth: true,
            data:[1200, 1320, 1010, 1340, 1900, 2300, 2100]
        },
        {
            name:'foods',
            type:'line',
            smooth: true,
            data:[2200, 1820, 1910, 2340, 2900, 3300, 3200]
        },
        {
            name:'home',
            type:'line',
            smooth: true,
            data:[800, 1020, 1210, 1540, 1800, 2300, 3100]
        },
        {
            name:'travel',
            type:'line',
            smooth: true,
            data:[600, 920, 1010, 1340, 1700, 2300, 2230]
        }
    ]
    };
 
    return option
}
export {
    LineScore
}

使用

import {LineScore} from './chartsOption'
import {EchartsChart} from 'components'
    
const ChartDom = (props) => {
    const chart_options = LineScore()
    return(
        <div style={{height:'300px'}}>
            <EchartsChart options={chart_options}/>
        </div>
    )
}

ReactDOM.render(
  <ChartDom />,
  document.getElementById('root')
);

组件开发

EchartsChart
|- index.js   组件的入口文件
|- index.less  组件的样式
|- main.js  组件的可视化库包装
  • index.js , 里面使用的一些判断非空 及组件渲染优化,想了解都可以fork我的antd-demo
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import ReactDM from 'react-dom'
import classnames from 'classnames'
import Chart from './main'
import styles from './index.less'
import {tools, shouldComponentUpdate} from 'utils'

class EchatsChart extends Component {
    constructor(props) {
        super();
        this.state = {} 
    }

    componentDidMount() {
        this.container = ReactDM.findDOMNode(this.refs.echarts)
        if(this.props.options && !tools.emptyObj(this.props.options)) {

            this.renderChart(this.props)
        }
    }

    componentWillReceiveProps(nextProps) {
        if('options' in nextProps && nextProps.options != this.props.options) {
            
            this.renderChart(nextProps)
        }
    }

    shouldComponentUpdate = shouldComponentUpdate

    renderChart(props) {
        new Chart({
            container:this.container,
            ...props
        })
    }

    render() {
        return (
            <div className={styles.echarts_chart}>
                <div className={styles.echarts_chart_box} ref='echarts'>
                </div>
            </div>
        )
    }
}

export default EchatsChart;
  • main.js ,组件的核心,可视化实现
/*
  chart base on echarts
*/
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import echarts from 'echarts'
import {tools, request} from 'utils'

class Chart extends Component {
  constructor(config) {
    super()
    this.config = config;
    this.container = config.container;
    
    this.init()
  }
  
  init() {
    const _this = this
    if(_this.config.showMapName) { //判断是否需要展示地图
        
        async function getMap(){
              return request({
                url: `/data/map/${_this.config.showMapName}.json`,
                method: 'get',
                data: {},
              })
            }
        
        getMap().then(res => {
            console.log(res)            
                res && echarts.registerMap(_this.config.showMapName, res);
                _this.renderChart()
            });     
            
    }
    else {
        _this.renderChart()
    }
  }
  
  renderChart() {
    const myChart = echarts.init(this.container);

    if(this.config.options && !tools.emptyObj(this.config.options)) {
      this.config.options.color = ['#64ea91','#8fc9fb', '#d897eb', '#f69899', '#f8c82e','#f797d6',  '#ca8622', '#bda29a','#6e7074', '#546570', '#c4ccd3']
      myChart.setOption(this.config.options);
    }

    window.addEventListener("resize",function(){ //响应式
      myChart.resize();
    });

    const _this = this
    for (let key in this.config) { //添加事件监听
      if(/^on[a-zA-Z]*$/.test(key)) {
        const even = key.substring(2);
        myChart.on(even, function (params) {
          _this.config[key] && _this.config[key](params)
        });
      }
    } 
  }
}
Chart.PropTypes = {
  container: PropTypes.object,
  showMapName:PropTypes.string,
  options: PropTypes.object,
  customProp(props) {
    if(!props.options) {
      return new Error('You echarts chart need a options!')
    }
  }
}

export default Chart

我以前编写echarts类型得组件,是将一种类型单独编写组件的,但后来嫌经常需要做些重复的编码工作,所以就合并了。如果你需要单独编写,当然这样个性化,独立性更好,维护好。可以查看https://zhuanlan.zhihu.com/p/28331793

d3JS

当然现在d3已经到v4了,支持canvas了。我这实现和React可视化的不同。

  • D3支持数据和节点绑定,数据变化,相应的节点也发生变化。React推崇单向数据流,数据从父组件到子组件,每个子组件只实现简单的一个模块
  • D3实现一套selector机制, 能够让开发址直接操作DOM节点、SVG节点。React使用虚拟DOM和高性能DOM diff算法,开发者无须关注节点操作。
import d3 from 'd3'

/*
    base on  d3.js-v3
*/

class AtlasChart {
    constructor(props) {
        this.props = props;
        this.container = props.container;
        this.orgData = props.atlasData;
        this.width = (props.options && props.options.width) || this.container.clientWidth;
        this.height = (props.options && props.options.height) || this.container.clientHeight;

      const defaultOption = {
        zoom: {
          scale:0.8, 
          x:this.width/2, 
          y:this.height/2
        }
      }
        this.options = Object.assign(defaultOption, props.options);

        this.init();
    }

    init() {
        const chartData = this.orgData;
        this.renderChart(chartData);
    }

    renderChart(chartData) {
        const _this = this;
        const W = this.width,
            H = this.height,
            rx = W / 2,
            ry = H / 2; 
        const n = {
      margin: 0,
      radiusR: 130
    };
    const Roate = _this.options.zoom.rotate;

        const color = ['#1f77b4', '#778ae6', '#b46bc5', '#eda61d', '#c3d41b', '#91dc8a', '#24a6da', '#aec7e8', '#ff9896', '#2ca02c'];

        d3.select(this.container).html('');

        var cluster = d3.layout.cluster()
    .size([360, ry - 30])
    .separation(function(e, t) {
      return (e.parent == t.parent ? 1 : 2) / e.depth
    })
    .sort(null);

    var lineScale = d3.scale.linear().domain([0, 2]).range([10, 7]);
        var diagonal = d3.svg.diagonal.radial()
        .projection(function(d) { return [d.y, d.x / 180 * Math.PI]; });

        function zoom() {
            // console.log(d3.event)
      _this.options.zoom.x = d3.event.translate[0];
      _this.options.zoom.y = d3.event.translate[1];
      _this.options.zoom.scale = d3.event.scale;
    
      svg_center.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
    }

        const zoomListener = d3.behavior.zoom()
      .scaleExtent([0.1, 3])
      .scale(_this.options.zoom.scale)
      .translate([_this.options.zoom.x,_this.options.zoom.y])
      .on("zoom", zoom);

    // 自定义提示框
    let tip = d3.select(this.container)
      .append("div")
      .attr('class','tooltips');
        
        let svg = d3.select(this.container)
            .append('svg')
            .attr('width', W)
            .attr('height', H)
            .attr('id', 'atlas-chart')
            .call(zoomListener)
            .on('dblclick.zoom', null);

        let svg_center = svg.append('g')
            .attr('id','svg_center')
            .attr("transform", "translate(" + [_this.options.zoom.x, _this.options.zoom.y] + ")scale(" + _this.options.zoom.scale + ")" );

        let nodes = cluster.nodes(chartData);

        nodes.forEach(function(d) {
      d.y = n.radiusR * d.depth,
      d.depth != 0 && (d.x += Roate,
      d.x >= 360 ? d.x -= 360 : d.x < 0 && (d.x += 360))
    });

        let link = svg_center.selectAll("path.link")
      .data(cluster.links(nodes))
      .enter().append("svg:path")
      .attr("class", "link")
      .attr("d", diagonal)
      .style('stroke', d => {
        return d.target.color ? d.target.color : color[d.target.group]
      });

    let node = svg_center.selectAll("g.node")
      .data(nodes)
      .enter().append("svg:g")
      .attr("class", "node")
      .attr("transform", function(d) { return d.parent ? "rotate(" + (d.x - 90) + ")translate(" + d.y + ")" : ''; })
      .on('mouseover', (d) => {
        d3.event.stopPropagation();
        tip.html(`${d.node_name}`)
          .style('left', `${d3.mouse(this.container)[0] + 20}px`)
          .style('top', `${d3.mouse(this.container)[1] + 20}px`)
          .style('display', 'block')
      })
      .on('mousemove', () => {
        tip.style('left', `${d3.mouse(this.container)[0] + 20}px`)
          .style('top', `${d3.mouse(this.container)[1] + 20}px`)
      })
      .on('mouseout', () => {
        tip.style('display', 'none')
      });

    node.append("svg:circle")
      .attr("r", 6)
      .attr('class', d => {
        return d.depth == 1 || d.depth == 0 ? 'node_breath': ''
      })
      .style('fill', d => {
        return d.color? d.color : color[d.group]
      });

    node.append("svg:text")
        .attr('class', 'node-text')
      .attr("dx", function(d) { return d.x < 180 ? 8 : -8; })
      .attr("dy", d => {
        return d.depth?".31em":'-1.5em'
      })
      .attr("text-anchor", function(d) { return d.parent ? d.x < 180 ? "start" : "end" : "start"; })
      .attr("transform", function(d) { 
        let t =  "rotate(180)"
        if(d.depth) {
            t = d.x < 180 ? null : "rotate(180)"; 
        }
        else {
            t = 'translate(' + (-(d.node_name.length * 12 / 2)) + ")";
        }

        return t
      })
      .text(function(d) { 
        return d.node_name.length > 10 ? d.node_name.slice(0, 10) + '...' : d.node_name         
      });

    }
}

export default AtlasChart

以上代码实现的是一种关系图谱。


Recharts

这是一种将各种图表进行细分为标签组件进行组合形成可视化。
http://recharts.org/#/en-US/api

Recharts优点

  • 声明式标签
  • 贴近原生SVG配置
  • 接口式API,解决各种个性化需求
const {LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend} = Recharts;
const data = [
      {name: 'Page A', uv: 4000, pv: 2400, amt: 2400},
      {name: 'Page B', uv: 3000, pv: 1398, amt: 2210},
      {name: 'Page C', uv: 2000, pv: 9800, amt: 2290},
      {name: 'Page D', uv: 2780, pv: 3908, amt: 2000},
      {name: 'Page E', uv: 1890, pv: 4800, amt: 2181},
      {name: 'Page F', uv: 2390, pv: 3800, amt: 2500},
      {name: 'Page G', uv: 3490, pv: 4300, amt: 2100},
];
const SimpleLineChart = React.createClass({
    render () {
    return (
        <LineChart width={600} height={300} data={data}
            margin={{top: 5, right: 30, left: 20, bottom: 5}}>
       <XAxis dataKey="name"/>
       <YAxis/>
       <CartesianGrid strokeDasharray="3 3"/>
       <Tooltip/>
       <Legend />
       <Line type="monotone" dataKey="pv" stroke="#8884d8" activeDot={{r: 8}}/>
       <Line type="monotone" dataKey="uv" stroke="#82ca9d" />
      </LineChart>
    );
  }
})

ReactDOM.render(
  <SimpleLineChart />,
  document.getElementById('container')
);

如果想了解下Recharts的几大特性,可以看看https://zhuanlan.zhihu.com/p/20641029

题外

组件化这些思想不只是在可视化组件中可以应用,很多组件也可以考虑利用这种思想来实现,例如表格组件就可以抽取Table 和 Column 两个组件,然后大家使用表格也非常简单:

<Table data={data}>
  <Column name="名称" dataKey="name"/>
  <Column name="数量" dataKey="count" align="right" th={<SortableTh order="asc" onChange={handleSort}/>}/>
  <Column name="金额" dataKey="amt" td="float" align="right"/>
</Table>

最后

放出我的学习antd时做的一个小demo,里面有这上面的可视化组件代码,当然还有一些其他antd的开发套路。

antd-demo

欢迎指导O(∩_∩)O哈哈~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,077评论 25 707
  • 持续更新中...... 一套企业级的 UI 设计语言和 React 实现。 https://mobile.ant....
    日不落000阅读 5,681评论 0 35
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,094评论 4 62
  • 项目一直没定下来方向,大概就是定了一个UITabBarController的样子是中间带圈的 哈哈 现在可能很流行...
    夏点阅读 581评论 1 2
  • “慎独”一词,出自秦汉之际儒家著作《礼记•中庸》一书:“莫见乎隐,莫显乎微,故君子慎其独也。”所谓慎独,就是在别人...
    尹润中阅读 706评论 0 1