自定义的数据可视化大屏

5,544 阅读4分钟

前言

项目实现了: 生成图表, 任意拖拽, 改变图表大小, 样式(输入input按回车enter), 双击删除, 可全屏的, 高度自定义的数据可视化大屏。

项目体验地址:数据可视化大屏

项目源码地址: 源码

技术栈: react + antd + echarts + react-hooks + react-dnd

项目预览

实现

生成图表, 实现拖拽, 全屏可参考上一篇文章: 生成图表

封装图表

可参考echarts的社区: echarts

这里面有很多大佬制作了很多不错的echarts图表, 博主偷懒, 都是从大佬那嫖来的图表。

import ReactEcharts from 'echarts-for-react';

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

通过echarts-for-react和社区里拿到图表样式option, 我们可以很轻松的封装多种图表的样式,博主做上面几套样式花了不到20分钟。

改变图表样式同步更新视图

项目在视图同步更新这卡了一会, 这里的难点是在我们改变了右侧的状态后, 让图表重渲染。

开始想自己用echarts 的rerender。 后来发现echarts-for-react给我们提供了获取图表dom并提供了api。

通过ref我们拿到库提供的getEchartsInstance方法, 这上面有echarts自己的所有方法, 通过重新setOption就可以实现图表rerender。

实现思路:

当我们在右侧更新状态按下回车键(enter)触发dispatch, 改变数据中心对应的数据,此时我们的chart组件也会rerender, 触发useEffect钩子函数,完成视图的同步更新。

const chartRef = React.useRef()

React.useEffect(() => {
    chartRef.current.getEchartsInstance().setOption(option)
}, [option])

return (
    <div
        className={ClassNames({ 'active': active })}
        onDoubleClick={(e) => deleteChart(id)}
        onClick={(e) => selectChart(e, id)}
        ref={drag}
        style={{ ...style, left, top }}
    >
        <ReactEcharts
            option={option}
            theme="Imooc"
            style={{ width: `${width}px`, height: `${height}px` }}
            ref={chartRef}
        />
    </div>
)

图表样式栏

当我们选中一个图表时, 获取图表的option, 并解析, 转化成对应的dom标签。

这里主要的问题是, option是深层对象结构, 我们需要一层层的循环遍历, 获取到对象属性。

思虑良久, 还是没想到很好的解决方案。 最后只能通过最笨的方法, 一层层的解析判断,当是不能展示在侧边栏的数据都给过滤掉。

这样的实现很蠢:

<Menu
    className="tab-menu"
    theme="dark"
    mode="inline"
    openKeys={openKeys}
    onOpenChange={(v) => onOpenChange(v)}
    style={{ width: '100%', height: '100%' }}
>
    {
        Object.keys(view).map(v => {
            if (v === 'data' || v === 'series') {
                return null
            }
            return (
                <SubMenu
                    className="tab-submenu"
                    key={v}
                    title={
                        <span>
                            <span>{v}</span>
                        </span>
                    }
                >
                    {
                        Object.keys(view[v]).map(item => {
                            if (item === 'data' || item === 'series') {
                                return null
                            }
                            if (typeof view[v][item] === 'object') {
                                return (
                                    <SubMenu
                                        className="tab-subitemmenu"
                                        key={item}
                                        title={
                                            <span>
                                                <span>{item}</span>
                                            </span>
                                        }
                                    >
                                        {
                                            Object.keys(view[v][item]).map(subitem => {
                                                return (
                                                    <Menu.Item key={subitem} className="tab-submenuitem">
                                                        <span style={{ marginRight: '5px' }}>{subitem}</span>
                                                        <Input
                                                            onPressEnter={(e) => onPressEnter(e, subitem, item, v)}
                                                            className="submenuitem-input"
                                                            onChange={e => inputOnChange(e, subitem, item, v)}
                                                            value={view[v][subitem]}
                                                        />
                                                    </Menu.Item>
                                                )
                                            })
                                        }
                                    </SubMenu>
                                )

                            }
                            return (
                                <Menu.Item key={item} className="tab-menuitem">
                                    <span style={{ marginRight: '5px' }}>{item}</span>
                                    <Input
                                        onPressEnter={(e) => onPressEnter(e, item, v)}
                                        className="menuitem-input"
                                        onChange={e => inputOnChange(e, item, v)}
                                        value={view[v][item]}
                                    />
                                </Menu.Item>
                            )

                        })
                    }
                </SubMenu>
            )
        })
    }
</Menu>

选中样式

当我们点击一个图表时给它添加class: active, 点击其他地方移除样式。

思路:

这里我使用了一个classnames插件。 它允许我们这样:

className={ClassNames({ 'active': active })}

其中的active是一个布尔值,当为true时,添加active样式, false时, 移除active样式。

实现:

我在每个图表的state里增加active一项,默认都为false, 当点击时, 将其变为true, 并让其他的都为false。

注意: 要阻止冒泡。

case 'activeClass':                                 //给选中的图表添加选中的样式
    Object.keys(state).map(item => {
        if (item === id) {
            state[item].active = true
            return item
        }
        state[item].active = false
        return item
    })
    return { ...state }
...

const selectChart = (e, id) => {                               //设置选中图表, 并添加被选中的样式
    e.stopPropagation()
    setSelect(chartsObj[id])
    dispatch({ type: "activeClass", id })
}

当点击非图表区域时, 再dispatch将所有的都变为false:


const cancelSelect = () => {                                //取消选中样式
    setSelect({})
    dispatch({ type: "activeClass" })
}

后话

后面要做的:

最主要就是把数据跑通, 可配置数据源, 并自定义生成大屏, 保存在服务器上。

次要是调整图表样式(目前这些封装的基本是网上copy下来的, 后续开发些合适的图表)