在學(xué)習(xí)使用過React一段時間后面氓,感覺React的使用還是比較舒適的兵钮。了解React的組件生命周期,路由舌界,及通信你會發(fā)現(xiàn)React對于開發(fā)真的很便利掘譬。
引言
偶然間,看見一篇博客關(guān)于React可視化的呻拌,自己本身這半年多使用React開發(fā)也有一些感觸葱轩,就借鑒這篇博客寫下這篇隨文。
推薦
腳手架
- dva-cli: https://github.com/dvajs/dva/blob/master/README_zh-CN.md
- create-bfd-app: https://github.com/baifendian/create-bfd-app
- create-react-app: https://github.com/facebookincubator/create-react-app
React組件庫
- Antd Design: https://ant.design/index-cn
- bfd-ui: http://ui.baifendian.com/bian
React 可視化
回到正題藐握。這篇主要是介紹些React中的可視化使用靴拱。
說到可視化,無外乎使用原生的canvas猾普、svg或一些可視化庫進行可視化開發(fā)袜炕。
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')
);
組件開發(fā)
EchartsChart
|- index.js 組件的入口文件
|- index.less 組件的樣式
|- main.js 組件的可視化庫包裝
- index.js , 里面使用的一些判斷非空 及組件渲染優(yōu)化,想了解都可以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 ,組件的核心初家,可視化實現(xiàn)
/*
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(){ //響應(yīng)式
myChart.resize();
});
const _this = this
for (let key in this.config) { //添加事件監(jiān)聽
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類型得組件偎窘,是將一種類型單獨編寫組件的,但后來嫌經(jīng)常需要做些重復(fù)的編碼工作溜在,所以就合并了陌知。如果你需要單獨編寫,當然這樣個性化掖肋,獨立性更好仆葡,維護好∨嘧瘢可以查看https://zhuanlan.zhihu.com/p/28331793
d3JS
當然現(xiàn)在d3已經(jīng)到v4了浙芙,支持canvas了登刺。我這實現(xiàn)和React可視化的不同。
- D3支持數(shù)據(jù)和節(jié)點綁定嗡呼,數(shù)據(jù)變化纸俭,相應(yīng)的節(jié)點也發(fā)生變化。React推崇單向數(shù)據(jù)流南窗,數(shù)據(jù)從父組件到子組件揍很,每個子組件只實現(xiàn)簡單的一個模塊
- D3實現(xiàn)一套selector機制, 能夠讓開發(fā)址直接操作DOM節(jié)點万伤、SVG節(jié)點窒悔。React使用虛擬DOM和高性能DOM diff算法,開發(fā)者無須關(guān)注節(jié)點操作敌买。
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
以上代碼實現(xiàn)的是一種關(guān)系圖譜简珠。
Recharts
這是一種將各種圖表進行細分為標簽組件進行組合形成可視化。
http://recharts.org/#/en-US/api
Recharts優(yōu)點
- 聲明式標簽
- 貼近原生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
題外
組件化這些思想不只是在可視化組件中可以應(yīng)用,很多組件也可以考慮利用這種思想來實現(xiàn)芙粱,例如表格組件就可以抽取Table 和 Column 兩個組件祭玉,然后大家使用表格也非常簡單:
<Table data={data}>
<Column name="名稱" dataKey="name"/>
<Column name="數(shù)量" dataKey="count" align="right" th={<SortableTh order="asc" onChange={handleSort}/>}/>
<Column name="金額" dataKey="amt" td="float" align="right"/>
</Table>
最后
放出我的學(xué)習(xí)antd時做的一個小demo,里面有這上面的可視化組件代碼,當然還有一些其他antd的開發(fā)套路春畔。
歡迎指導(dǎo)O(∩_∩)O哈哈~