一起来做一个数据可视化,可拖拽的, 高度自定义的图表看板吧

3,861 阅读2分钟

前言

这个项目比较复杂, 预计要分三部分来讲, 项目也还未完工, 怕工期拖长了, 前面忘了,这里先做下记录。

项目在线体验地址: charts

项目源码地址在Library文件夹下: 源码

项目用到的技术

react-dnd

react-dnd-html5-backend

react

echarts

echarts-for-react

数据结构

嵌套对象: 外层对象的属性是每个图表的id, 内层对象保存图表的各项属性。

长这样:

chartsObj = {
    line1: {
        width: x,
        height: x,
        top: x,
        left: x,
    }
    ...
}

这里使用的react的hook:useReducer创建的对象。

开始没想到会有这么多type, 后面重构换成switch-case:

const [chartsObj, dispatch] = React.useReducer((state, action) => {
    const { id, left, top, active, type, width, height, view, value } = action
    if (type === 'move') {
        return { ...state, [id]: { ...state[id], left, top } }
    }
    if (type === "delete") {
        delete state[id]
        return { ...state }
    }
    if (type === 'activeClass') {
        Object.keys(state).map(item => {
            if (item === id) {
                state[item].active = true
                return item
            }
            state[item].active = false
            return item
        })
        return { ...state }
    }
    if (type === 'changeview') {
        return { ...state, [id]: { ...state[id], [view]: value } }
    }
    return { ...state, [id]: { id, type, active, left, top, width, height } }
}, {});

创建图表

当点击左侧菜单栏时生成对应的基本图表: 类似点击柱图生成柱图。

思路:

  1. 封装对应图表的echarts基本样式。

  2. 当点击item时, 使用dispatch通知state做变更。

  3. 数据驱动视图, 生成对应的图表。

实现:

这里使用echarts-for-react将echarts引入项目中的, 用过echarts的都知道, echarts主要的定义都在option属性上, 我们只要把定义好的option传进去就ok了,不懂echarts的道友可以先去echarts官网学习下, 官网api很详细: echarts

import ReactEcharts from 'echarts-for-react';

<ReactEcharts
    option={option}
    theme="Imooc"
    style={{ width: `${width}px`, height: `${height}px` }}
    ref={chartRef}
/>

封装好图表之后, 下一步就是点击事件改变数据源, 使用useReducer提供的dispatch:

const createChart = (item) => {
    switch (item.key) {
        case "lineBasic":
            dispatch({ type: 'lineBasic', id: `lineBasic${Object.keys(chartsObj).length}`, active: false, left: 20, top: 20, width: 300, height: 250 })
            break
        case "barBasic":
            dispatch({ type: 'barBasic', id: `barBasic${Object.keys(chartsObj).length}`, left: 20, top: 20, width: 300, height: 250 })
            break
        case "pieBasic":
            dispatch({ type: 'pieBasic', id: `pieBasic${Object.keys(chartsObj).length}`, left: 20, top: 20, width: 300, height: 250 })
            break
        default:
            return
    }
}

遍历state,生成图表:

{Object.keys(chartsObj).map(v => {
    const { left, top, id, type, active, width, height } = chartsObj[v]
    return (
        <Chart
            type={type}
            key={id}
            id={id}
            left={left}
            top={top}
            width={width}
            height={height}
            active={active}
            deleteChart={deleteChart}
            selectChart={selectChart}
        >

        </Chart>
    )
})}

支持拖拽

拖拽主要使用的react-dnd, 也尝试了react-beautiful-dnd, react-sortable-hoc,最后发现还是react-dnd最强大。

使用react-dnd的道友一定注意, 这里有个大坑: 引入react-dnd报错, 原因是react-dnd插件内部也引入了react。如果和我们的react版本不一致就会报错。解决方法:

webpack.config.js的resolve下配置下alias:

alias: {
    '@': paths.appSrc,
    react: path.resolve('./node_modules/react'),        //解决react两个版本的问题
}

react-dnd依赖react-dnd-html5-backend做扩展, 所以必须一起安装。

  1. 在项目的根目录下:
//app.js

import { useDrop, DndProvider } from 'react-dnd'
import HTML5Backend from 'react-dnd-html5-backend'


export default function RouteConfigExample () {
    return (
        <DndProvider backend={HTML5Backend}>

            <BrowserRouter>
                <Switch>
                    {RouteGlobal.map((route, i) => (
                        <RouteWithSubRoutes key={i} {...route} />
                    ))}
                </Switch>
            </BrowserRouter>
        </DndProvider>
        ...
        
  1. 在准备作为拖拽区域的组件内:

    const [, drop] = useDrop({
        accept: 'box',
        drop (item, monitor) {
            const delta = monitor.getDifferenceFromInitialOffset()
            const left = Math.round(item.left + delta.x)
            const top = Math.round(item.top + delta.y)
            moveBox(item.id, left, top)
            return undefined
        },
    })
    const moveBox = (id, left, top) => {
        dispatch({ type: 'move', id, left, top })

    }
    ...
    <div ref={drop} className="charts-middle">
        <Chart>
    </div
  1. 设置拖拽源:
    const [{ isDragging }, drag] = useDrag({
        item: { id, left, top, type: 'box' },
        collect: monitor => ({
            isDragging: monitor.isDragging(),
        }),
    })
    
    ...
    
    <div
        ref={drag}
        style={{ ...style, left, top }}
    >
        <ReactEcharts
            option={option}
            theme="Imooc"
            style={{ width: `${width}px`, height: `${height}px` }}
            ref={chartRef}
        />
    </div>
    

全屏操作

操作全屏主要用到了requestFullscreenexitFullScreen两个api, 各个浏览器的兼容性不一样:

const fullscreen = () => {
        let ele = document.querySelector('.charts-box')
        if (ele.requestFullscreen) {
            ele.requestFullscreen();
        } else if (ele.mozRequestFullScreen) {
            ele.mozRequestFullScreen();
        } else if (ele.webkitRequestFullscreen) {
            ele.webkitRequestFullscreen();
        } else if (ele.msRequestFullscreen) {
            ele.msRequestFullscreen();
        }
        setFullFlag(true)
    }
const exitFullscreen = () => {
    if (document.exitFullScreen) {
        document.exitFullScreen();
    } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen();
    } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen();
    } else if (document.msExitFullscreen) {
        document.msExitFullscreen();
    }
    setFullFlag(false)
}
...

{
    fullFlag
        ?
        <Button className="charts-left-button" onClick={exitFullscreen}><Icon type="fullscreen-exit" /></Button>
        :
        <Button className="charts-left-button" onClick={fullscreen}><Icon type="fullscreen" /></Button>
}
...

后话

具体的操作可到线上demo体验, 下步准备实现:

图表样式支持配置;

数据源可以配置,使用真实数据源, 展示真实数据;