支持拖拉拽的可视化图表解决方案

标签: 拖拉 可视化 图表 | 发表时间:2023-03-09 18:49 | 作者:Bigger
出处:https://juejin.cn/frontend

theme: channing-cyan

本文正在参加 「金石计划」

支持拖拉拽的可视化图表解决方案

背景

《xxx-xxxx》需要做一个可视化图表模块:

  1、支持表格、折线图、饼图、柱状图、数值图、横向柱状图、仪表盘等图表。
2、支持表单配置图表参数,以及数据筛选参数。
3、支持拖拽排序、支持伸缩调整图表大小。

了解到这需求其实之前就有,但是之前的小伙伴一直没有找到合适的解决方案,所以就耽搁了。

历程

拆解需求,按照重要的点拆分,一步一步实现:

  1、图表的选型及组件封装
2、页面的布局及表单的配置实现
3、拖拽伸缩的功能实现

图表的选型及组件封装

图表库的选择

主流的 web 图表库有:ECharts、Highcharts、D3.js、antv(G2,G6,F2)...

ECharts,一个使用 JavaScript 实现的开源可视化库,可以流畅的运行在 PC 和移动设备上,兼容当前绝大部分浏览器(IE8/9/10/11,Chrome,Firefox,Safari等),底层依赖矢量图形库 ZRender,提供直观,交互丰富,可高度个性化定制的数据可视化图表。

ECharts 提供了常规的折线图、柱状图、散点图、饼图、K线图,用于统计的盒形图,用于地理数据可视化的地图、热力图、线图,用于关系数据可视化的关系图、treemap、旭日图,多维数据可视化的平行坐标,还有用于 BI 的漏斗图,仪表盘,并且支持图与图之间的混搭。

Highcharts 是一个用纯 JavaScript 编写的一个图表库, 能够很简单便捷的在web网站或是web应用程序添加有交互性的图表,HighCharts 支持的图表类型有曲线图、区域图、柱状图、饼状图、散状点图和综合图表。但非商业免费,商业需授权。

D3 的全称是(Data-Driven Documents)。D3.js是一个 JavaScript 库(是一个被数据驱动的文档),用于在浏览器中创建交互式可视化。

D3 js库允许我们在数据集的上下文中操作网页的元素。这些元素可以是 HTML,SVG,或画布元素,可以根据数据集的内容进行引入,删除或编辑.它是一个用于操作 DOM 对象的库. D3.js 可以成为数据探索的宝贵帮助,它可以让您控制数据的表示,并允许您添加交互性。

@antv/g2 是一套基于可视化编码的图形语法,以数据驱动,具有高度的易用性和扩展性,用户无需关注各种繁琐的实现细节,一条语句即可构建出各种各样的可交互的统计图表。

@antv/g2 是一个图形语法, 通过数据之间的关系, 来生成一张图. 所以 G2 的定制化确实更加强大, 但是我们做业务的, 一般就是需要一个特定的图然后填充数据, 和将数据和数据之间的关系描述出来再实现图表有点出入,还有 G2 的文档一般,上手难度。

其中最后的选择落在 echarts 和 G2,echart 使用就是一些配置项,G2 是图形语法,通过数据之间关系,来生成一张图,虽说这样 G2 定制化更加强大。但是我们的业务,一般就是需要一个特定的图然后再填充数据,还能支持配置,无疑 Echarts 更符合我们的业务场景。所以最终图表库选择 echarts

组件的封装

封装思路:

  1. 利用 echarts 的配置项,把固定的配置项内容设置为默认 defaultOptions,把需要改变的配置项当作参数传进组件,组件内部再进行合并。

  2. 图表渲染的数据,可以当作参数传给组件,组件去 deep watch, 触发 setOption 方法修改配置或者渲染数据。

  3. 由于我们的图表是可以拖拉拽移动伸缩的,所以特别注意 chart.resize,可以帮助图表自适应。

页面的布局及表单的配置实现

image.png

拖拽伸缩的功能实现

起初,我的理解是把拖拽(排序) + 伸缩(调整大小)分开去实现,先实现拖拽,因为拖拽这块我这边也熟悉,之前做低代码项目也使用过。所以也就有了第一版的拖拽版本。

起初我是使用了 JS 的原生拖拽属性(transition-group、draggable="true"、dragstart、drag、dragend)

  <transition-group
  name="drag"
  class="list"
  tag="ul"
>
  <el-col
    v-for="(item, index) in chartList"
    :key="item.id"
    :span="12"
    :id="`${item.id}${index}`">
    <div
      draggable
      class="drag-box"
      @dragenter="dragenter($event, index)"
      @dragover="dragover($event, index)"
      @dragstart="dragstart(index)">
      <el-card>
        ....
        ....
        ....
      </el-card>
    </div>
  </el-col>
</transition-group>

上处代码实现了拖拽,整体效果还可以,但是有一个严重的兼容 bug,那就是拖拽表格的时候,表格 table 通过 overflow: hidden 隐藏的滚动不可见区域居然又展示出来了,经过各种尝试解决,都未能取得明显的效果,本着造轮子失败的心理去使用了 Vue.Draggable 插件库。

Vue.Draggable

  <draggable
  v-model="chartList"
  chosen-class="chosen"
  force-fallback="true"
  group="people"
  animation="1000"
  class="drag-box"
  @start="onStart"
  @end="onEnd">
  <transition-group
    name="drag"
    class="list"
    tag="ul"
  >
    <el-col
      v-for="(item, index) in chartList"
      :key="item.id"
      :span="12"
      :id="`${item.id}${index}`"
    >
      <el-card>
        ....
        ....
        ....
      </el-card>
    </el-col>
  </transition-group>
</draggable>

果不其然,使用第三方插件确实没有上述 bug 了,还是经过多年、多人开发的 Vue.Draggable 强。真的很好奇,那个 table 问题,它是怎么兼容过去的。

带着好奇心,大致去了解了一下其原理:Vue.Draggable 插件本质上是基于 Sortable.js 用以实现拖拽功能实现了一个全局组件 vue-draggable。

首先,它是在 mounted 生命周期中:

  1. 配置了 sortablejs 相关的回调函数、属性等
  2. 合并配置选项,实例化 sortablejs
  3. 计算列表项 vnode 在真实 dom 对象中的位置,并保存一个 index 数组做映射

接着,在真实的拖拽中,触发拖拽事件:

  1. 通过 sortablejs 返回的 domIndex 来找到真实数据中对应的位置,这样就可以找到数据在列表数据里的位置。
  2. 通过数据更新通知 vue 视图更新,也就是数据和视图双向绑定。

基本大致就是以上的一个流程,如有感兴趣的,可以去看源码,也就 400 多行。

用 Vue.Draggable 虽然实现了拖拽排序的功能,但是,别忘了,我们还有一个伸缩的功能,就找到了 vue-draggable-resizable 插件。

vue-draggable-resizable

vue-draggable-resizable 是一个 Vue2 的组件,用于可拖动和可调整大小的元素。

使用 Vue.Draggable 的拖拽位置能力 和 vue-draggable-resizable 的伸缩调整大小的能力,调试了一下,发现它和 Vue.Draggable 不兼容不能一起使用。其实 vue-draggable-resizable 自身就是可支持拖动和调整大小的两个功能,但是如果单独只使用它,也是不满足于我们的场景,它更像是支持那种可视化编辑拖拉拽的场景,也就是低代码的场景。

无奈,又去开始查阅各种资料,以及重新整理对需求的思考。

思考后,感觉自己走了弯路,看问题片面化了,其实重新理解一下,该需求类似于一种瀑布流的效果,但是增加了可视化调整大小的能力。这么理解的话,这算是一种布局,就是一个大盒子也就是父组件里边,有很多子组件列表,子组件可以随意调整排列顺序,而且可以支持伸缩调整大小,重新更新受影响组件布局。

按照这种思路,我找到了 React-Grid-Layout (适应于 React)和 vue-grid-layout (适用于 Vue)。由于我们的技术栈对应 Vue,所以我们选择 Vue Grid Layout。

Vue Grid Layout

一个类似于 Gridster 的栅格布局系统, 适用于 Vue.js。

github 地址 文档地址

  <grid-layout
  :layout.sync="positions"
  :col-num="12"
  :row-height="50"..
  :is-draggable="true"
  :is-resizable="true"
  :is-mirrored="false"
  :vertical-compact="true"
  :margin="[10, 10]"
  :use-css-transforms="true"
>
  <grid-item
    v-for="(item, index) in positions"
    :x="item.x"
    :y="item.y"
    :w="item.w"
    :h="item.h"
    :i="item.i"
    :min-w="2"
    :min-h="1"
    :key="item.i"
    style="touch-action: none"
    @resized="resizedEvent"
    @moved="movedEvent"
  >
    <el-card
      v-loading="chartList[index]?false:true"
      :body-style="{ padding: 0, margin: '0 0 20px', height: bodyHeight }"
      :id="`${item.i}${index}`"
      element-loading-text="拼命加载中..."
      element-loading-spinner="el-icon-loading"
      element-loading-background="rgba(0, 0, 0, 0.6)"
      class="box-card"
    >
      <template v-if="chartList[index]">
        ....
        ....
        ....
      </template>
    </el-card>
  </grid-item>
</grid-layout>

遇到问题

首次加载页面时,echarts 图表并不显示

echarts 图形需要在 vue-grid-layout 加载完成后才能进行初始化,所以需要在 echarts 加载加上异步,刚好我们的图表数据需要一个个从后台接口异步获取。

新建的图表模块怎么增加进去?

  deal_unsorted_list() {
  const len = this.old_unsorted_list.length
  const num = Math.ceil(len / 2)
  this.positions.map(item => {
    item.y += 6 * num // 每行默认 50,默认是 6 行
    return item
  })
  const list = []
  let num_y = 0
  this.old_unsorted_list.map(item => {
    if (list.length % 2 === 0) {
      list.push({ x: 0, y: num_y, w: 6, h: 6, i: item.id })
    } else {
      list.push({ x: 6, y: num_y, w: 6, h: 6, i: item.id })
      num_y++
    }
  })
  this.positions = list.concat(this.positions)
  this.onEnd()
},

思路就是:判断是否有新增的图表,如果有,就计算出 新的需要占的总高度,把老的图表重新计算 y 轴上的位置,统一往下赶一赶,这样就给新的在顶部(倒序)留出出空的位置了。然后再把新的没有拍过位置的计算排一下位置。

上述是算是倒叙在增加新的图表,但是我们有的场景也是正序加载新的图标,新的图表直接加载最后。接下来我们也实现一下,正序加载图表。

  deal_unsorted_list_zheng() {
  let start_x = 0
  let start_y = 0
  if (this.positions.length > 0) {
    const last_p = this.positions[this.positions.length - 1]
    if (12 - last_p.x - last_p.w >= 6) {
      start_x = last_p.x + last_p.w
      start_y = last_p.y
    } else {
      start_y = last_p.y + 6
    }
  }
  const list = []
  this.old_unsorted_list.map(item => {
    list.push({ x: start_x, y: start_y, w: 6, h: 6, i: item.id })
    start_x = start_x + 6 >= 12 ? 0 : start_x + 6
    start_y = start_x === 0 ? start_y + 6 : start_y
  })
  this.positions = this.positions.concat(list)
  this.onEnd()
}

更新位置图表响应式

  // ...
watch: {
  position: {
    deep: true,
    handler() {
      this.chart && this.chart.resize()
    }
  }
}

给图表设置响应式。

每次拖拽后,前端需要给出正确的排序

每次拖拽后,前端需要给出正确的排序传给后端保存起来,保证下次进来是按照顺序懒加载的,也就是从上到下一个个去加载图表数据。这样用户体验会好一点。那么怎么排序呢?

比如:如下坐标系,要求先按照 y 的大小从小排序,再按照 x 的大小从小排序,排 x 的时候不能影响之前 y 排好的顺序。

  "pxy": [
    {"x": 0, "y": 16,}, 
    {"x": 3, "y": 0,}, 
    {"x": 6, "y": 0,}, 
    {"x": 9, "y": 10,}, 
    {"x": 4, "y": 16,}, 
    {"x": 8, "y": 5,}, 
    {"x": 0, "y": 10,}, 
    {"x": 0, "y": 0,}, 
    {"x": 9, "y": 0,}, 
    {"x": 0, "y": 5,}, 
    {"x": 4,"y": 5,}, 
    {"x": 3,"y": 10,}
]

答案也就如下操作:

  onEnd() {
  this.sort_id = []
  this.positions.sort((a, b) => a.y === b.y ? a.x - b.x : a.y - b.y)
  this.positions.map(item => {
    this.sort_id.push(item.i)
  })
  this.editDashboard()
}

图表加载体验

上部分也提到了按照顺序从上向下加载,因为我们的这个仪表盘列表可能会有很多图表,如果一次性去请求一个接口加载数据,后端接口就会很吃力,前端的页面用户体验也不好。

所以我们就把接口拆分为两部分,第一部分是图表的位置数据及每个图表的对应id,第二部分就是每个图表的详情数据。也就是先把列表大概渲染出来,每个位置对应的加载出来,再按照顺序遍历 id 串联的去加载每个图表的数据。

效果就是用户先看到所有的图表从上到下的依次加载,也适应用户喜欢从上向下浏览的体验。

优化体验

增加水印

图表一般都是数据类相关的,所以针对数据的保密性、安全性也是需要考虑的,所以就加上水印。

水印这块,我是使用的 github 开源的 watermark,在其基础上进行了二次开发,修改了其中一些 bug,增加了一些个人需求。同时也是支持防控制台删除功能。因为是原生 js 写的,所以不分前端框架,不管 Vue、还是 React 都支持。

  mounted() {
    watermark.init({ 
        watermark_txt: ['xxx-xxxx', username], // 数组有几项,就代表几行
        watermark_parent_node: 'LookChart' 
    })
},

beforeDestroy() {
    watermark.remove()
},

增加了全屏切换体验

全屏切换我这快使用了 github 上的一个开源方案 screenfull,整体使用感觉还不错,也推荐大家使用。

做这个全屏 screenfull,还特意去了解了一下 top-layout,感兴趣的也可以去看一下。

做全屏这块,需要注意的点有:整体页面全屏 和 单个图表全屏之间切换问题,所以这块最好去判断是哪种情况的全屏,全屏最好绑定在指定元素上。

  // 页面全屏
fullScreenClick() {
  if (!screenfull.enabled) {
    this.$message({
      message: 'you browser can not work',
      type: 'warning'
    })
    return false
  }
  if (screenfull.isFullscreen) {
    screenfull.exit()
  } else {
    const element = document.getElementById('LookChart')
    screenfull.request(element)
  }
},

// 单个图表全屏
chartScreenClick(item, index) {
  if (!screenfull.enabled) {
    this.$message({
      message: 'you browser can not work',
      type: 'warning'
    })
    return false
  }

  const elementLookChart = document.getElementById('LookChart')
  // 判断是否是页面全屏状态
  if (screenfull.isFullscreen && screenfull.element !== elementLookChart) {
    screenfull.exit()
  } else {
    this.bodyHeight =
    this.bodyHeight === 'calc(100% - 40px)' ? '95vh' : 'calc(100% - 40px)'
    const element = document.getElementById(`${item.id}${index}`)
    screenfull.request(element)
  }
},

增加了暗黑模式切换体验

做图表,配上暗黑模式,瞬间科技感就提升了很多,高大上了许多。目前包括系统、浏览器、各种技术文档等都几乎提供了暗黑模式,可见这是一种趋势。也是一种现代审美。

那简单的基本做法就是:

  // 加载本页面、本模块设置暗黑模式切换
mounted() {
    window.document.documentElement.setAttribute('data-theme', 'normal')
},
  
// 暗黑模式切换
switchChange() {
    if (this.switchValue) {
        window.document.documentElement.setAttribute('data-theme', 'normal')
    } else {
        window.document.documentElement.setAttribute('data-theme', 'dark')
    }
},

光改 js 逻辑肯定不行啊,肯定 需要 改 css 来支持:

  [data-theme="normal"] & {
    .Look{
      height: 100%;
      background-color: #f0f2f5;
    }
    // ....
}

[data-theme="dark"] & {
    .Look{
      height: 100%;
      background-color: rgba(0, 21, 41, 0.9);
      color: #fff;
    }
    // ....
}

增加了颜色主题切换体验

每个用户的喜好不同,对色彩的喜好也不同,为了能满足更多的用户,增加以下几个主题的切换。

image.png

秋日橙主题

以橙色调为主的主题色板,整体基调是秋日的丰收色,同时适应深浅模式,可按需替换默认主题。

image.png

1605580842997-9b92adc4-f786-4b01-8948-3b9b65137266.gif

马卡龙主题

以粉紫色调为主的主题色板,整体基调是马卡龙糖果色,同时适应深浅模式,风格活泼明快,可按需替换默认主题。

image.png

1605580855576-8e823c86-0992-48fd-bcd0-1e0a3a715438.gif

强对比主题

强对比分类主题,颜色饱和度较高,强对比度,在投屏场景等降对比场景使用。

image.png

1605580864671-02473b66-3913-48df-949e-837baa570f69.gif

小结

整体的效果:

image.png

整体的历程和遇到问题,不仅仅局限于上处所列的。只是提到一些典型而已。如果大家有类似需求,或者文中有不当之处,再或者有更优解,都可留言评论或者私信沟通互相学习。

感谢大家支持!

相关 [拖拉 可视化 图表] 推荐:

支持拖拉拽的可视化图表解决方案

- - 掘金 前端
支持拖拉拽的可视化图表解决方案. 《xxx-xxxx》需要做一个可视化图表模块:. 1、支持表格、折线图、饼图、柱状图、数值图、横向柱状图、仪表盘等图表. 2、支持表单配置图表参数,以及数据筛选参数. 3、支持拖拽排序、支持伸缩调整图表大小. 了解到这需求其实之前就有,但是之前的小伙伴一直没有找到合适的解决方案,所以就耽搁了.

[JS]36个卓越的可视化数据工具(图表库)

- - 设计达人
如果将数据直接以文字形式展示,这样用户阅读起来是非常困难的,所以如果我们使用可视化形式来展示数据,这就会更清晰易懂. 这里由smashingapps整理了36个卓越的可视化数据工具,有地图图表、有柱状图表、饼状、散点等等,是一个不错的list收集. 下面来看看这些可视化数据图表工具,或许会对你的项目带来帮助.

数据可视化平台 Plotly 开源强大的 JS 图表库

- - 开源中国社区最新新闻
数据可视化平台 Plotly 开源旗下强大的 JavaScript 图表库,支持三种不同类型的图表,包括地图,箱形图和密度图,以及更常见的产品如,条状和线形图. 源代码已发布在 GitHub. (已收录开源中国软件库 plotly.js)最新版本的 Plotly.js 可以免费、无限制地用于任何项目.

数据可视化

- Sillywolf - ISD Webteam

可视化编程

- - 酷 壳 - CoolShell.cn
本文来自《 Visual Programming Languages – Snapshots》,作者 Eric Hosick收集了一堆关于可视化编程的工具,好多我都听都没听说过,我一股脑的全转过来,给大家看看,算是开开眼界了. 本文也是参考了Wikipedia的  Visual Programming Language 词条.

数据可视化

- - 人月神话的BLOG
推荐阅读知乎的关于有哪些可视化工具推荐的回答,内容已经相当全面了. 要注意的是当前主流的仍然是基于javasrcirpt开发的图表库,对于偏重的flex不应该做为选择的基础. 下面对一些选择的思路做些简单的说明. 首先可选的主流图表库包括了百度的 Echart,Highchart,D3.js这三个.

大数据的可视化

- - CSDN博客云计算推荐文章
       现在数据管理面临的一个关键性问题是如何将这些海量的来自于四面八方的非结构化数据可视化. 不管你从事于什么行业或者正在从事于哪一方面的研究,正将是你经常会触及的问题. 最近,埃里克•奥彭肖和JR里根做客了商务博客的“金融时代”专栏,讨论的主题为“大数据可视化是‘大数据’的关键机会”,分析了今天大数据的使用者们面临的可视化问题,以及公司为应对这个挑战而付诸的一些创新的方法.

可视化系统搭建

- - 腾讯ISUX – 社交用户体验设计
如何搭建数据可视化系统,用丰富的设计语言清晰表达复杂和庞大数据,并形成鲜明的设计风格. 我们把数据可视化的元素进行拆分并建立相应的规范体系. 六种基本图表涵盖了大部分图表使用场景,也是做数据可视化最常用的图表类型:. 柱状图   分类照片照片什么照片什么什么项目之间的比较;. 饼图   构成即部分占总体的比例;.

Visual.ly:可视化数据探索平台

- kaichun - TechWeb 新酷网站 RSS阅读
Visual.ly相关图片(图片来源:Techweb.com.cn).   【TechWeb报道】4月12日消息,新酷网站:可视化数据探索平台Visual.ly.   我们生活在数据收集和内容创作的时代. Visual.ly正是这个数据时代当产物,一个全新的可视化信息图形新平台. 信息图形将极大的刺激视觉表现,促进用户间相互学习、讨论.

可视化的排序过程

- 天下 - 酷壳 - CoolShell.cn
下面是一个日本程序员制做的一个可视化的排序过程,包括了各种经典的排序算法,你可以调整速度和需要排序的个数. 酷壳以前也介绍过几篇相关的文章 一个排序算法比较的网站,一个显示排序过程的Python脚本 关于各种排序算法的运行复杂度比较,请参看Wikipedia的排序算法比较. 2010年07月12日 -- 一些重要的算法.