近期做的绘图业务:地图(echarts)、3D环形图(highcharts)、流程图(bpmn)、课程地图(antv6)

0 阅读4分钟

绘制地图(echarts)

下载地图文件

datav.aliyun.com/portal/scho… 首先需要去这里下载地图数据,例如我们下载了浙江省的地图数据放到我们的本地项目中。

a25a5850-cc04-4279-a320-da65b93681ab.png

地图注册

echarts.registerMap('ZJ', geoJson); // 先注册地图

渲染地图,展示数据

import geoJson from './zhejiang.json'
echarts.registerMap('ZJ', geoJson); // 先注册地图
 var option = {
    title: {
      text: '',
      left: 'center'
    },
    tooltip: {
      trigger: 'item'
    },
    series: [
      {
        name: '浙江省',
        type: 'map',
        mapType: 'ZJ', // 注册的地图类型
        roam: true, // 允许缩放和平移
        label: {
          show: true
        },
        data: [{
          name: '杭州市',
          value: 3
        }] // 显示的数据
      }
    ]
  };
  var dom = chartDom.value;
  myChart = echarts.init(dom);

  // 使用配置项显示地图
  myChart.setOption(option);

3D饼图、3D环形图(highcharts)

echarts的方式

3D图用echarts实现,官方没有Demo,只在可视化社区的网站里找到了一些例子。www.makeapie.cn/echarts

对于可视化的业务来说,Demo的意义也是不可忽视的。

事实上,这个Demo虽然能画出一个3D的环形图,但效果并不是特别好,并且echarts版本切换一下,就无法使用了。

www.makeapie.cn_echarts_content_xX9xcKXRjq.html.png

使用highcharts去绘制

相比echarts,highcharts官方提供了Demo和源码:

beijing-2022-gold-medals.png

我们只需要调整一下参数,即可绘制一个比较好看的3D环形图了:

beat-the-buzzer.github.io_vue3-demo_.png

代码地址:github.com/beat-the-bu…

  const getChartOptions = (data) => ({
    chart: {
      type: 'pie',
      options3d: {
        enabled: true,
        alpha: 60,
        beta: 0,
        depth: 40,
        viewDistance: 25,
      },
    },
    title: { text: '' },
    subtitle: { text: '' },
    legend: { enabled: false },
    tooltip: {
      backgroundColor: 'rgba(255, 255, 255, 0.9)',
      borderColor: '#ddd',
      borderWidth: 1,
      borderRadius: 6,
      padding: 10,
      shadow: true,
      formatter: function () {
        return `${this.point.name}${this.point.y},占比: ${(
          ((this.point.y / this.point.total) * 100) || 0).toFixed(1)}%`;
      },
      useHTML: true,
      style: {
        boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
        color: '#333',
        fontWeight: 'bold',
      },
    },
    plotOptions: {
      pie: {
        allowPointSelect: true,
        cursor: 'pointer',
        size: 150,
        depth: 40,
        borderWidth: 4,
        borderColor: '#fff',
        shadow: {
          color: 'rgba(0, 0, 0, 0.3)',
          width: 8,
          offsetX: 2,
          offsetY: 2,
        },
        innerSize: '60%', // 甜甜圈内径
        dataLabels: {
          enabled: true,
          distance: 30,
          color: '#333',
          style: {
            textOutline: 'none',
            fontSize: '12px',
            fontWeight: 'bold',
          },
          formatter: function () {
            return `${this.point.name}`;
          },
        },
      },
    },
    credits: { enabled: false },
    series: [
      {
        type: 'pie',
        data: data, // 使用传入的data
        startAngle: 0,
      },
    ],
  });

AI时代的工作方式:Highcharts GPT,简单描述一下需求,就可以快速生成代码。

image.png

也支持在线编辑代码,直接在线上把页面调整成我们想要的效果:

image.png

流程图(bpmn.js)

最终效果:

beat-the-buzzer.github.io_vue3-demo_ (1).png

整体结构绘制

在bpmn.js里,流程图本质上是一个xml格式的数据,并且后端也有相应的解析的库。

import BpmnViewer from 'bpmn-js';
import 'bpmn-js/dist/assets/diagram-js.css';
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';

const viewer = new BpmnViewer({ container: '#bpmn-container' });
// 加载你的 BPMN XML 字符串
await viewer.importXML(bpmnXml);

bpmn有两种模式,一种是只读,一种是编辑:

  • Viewer:只读模式,仅用于展示流程图,不可编辑
  • Modeler:全功能编辑模式,支持拖拽节点、连线、配置属性

一般的使用流程是这样的:

  1. 先用编辑模式将流程图的整体结构绘制出来,如上图,有开始、结束、用户任务、网关再加上连线,这几种基础元件。

  2. 当流程配置完成之后,在只读模式上,编辑每个节点的相关属性:用户任务的节点,通常会配置审批人;网关节点,通常会配置条件。

例如:一个请假申请的流程,一天以内的假期,只需要上级领导审批;超过一天的假期,要求部门领导审批。

一般比较复杂的界面交互,都在配置条件上。为此用AI简单生成了一个条件编辑器:

586168b7-e1f5-4eb9-83b0-413fecc31ce6.png

需要服务端去解析json配置并且取出其中的条件。

课程地图(antv X6)

预览地址:beat-the-buzzer.github.io/antv-x6-dem…

预览效果:

image.png

import { Graph, Shape } from '@antv/x6'
import { register } from '@antv/x6-vue-shape'
import VueNode from './VueNode.vue'

let graph = new Graph({
    container: document.getElementById('graph-container'),
    width: window.innerWidth - 60,
    height: window.innerHeight - 70,
    panning: true,
    interacting: false,
    mousewheel: {
      enabled: true,
      zoomAtMousePosition: true,
      modifiers: 'ctrl',
      minScale: 0.5,
      maxScale: 3,
    },
    connecting: {
      router: 'manhattan',
      connector: {
        name: 'rounded',
        args: {
          radius: 8,
        },
      },
      anchor: 'center',
      connectionPoint: 'anchor',
      allowBlank: false,
      snap: {
        radius: 20,
        anchor: 'center'
      },
      createEdge() {
        return new Shape.Edge({
          attrs: {
            line: {
              stroke: '#A2B1C3',
              strokeWidth: 2,
              targetMarker: {
                name: 'block',
                width: 12,
                height: 8,
              },
            },
          },
          zIndex: 0,
        })
      },
      validateConnection({ targetMagnet }) {
        return !!targetMagnet
      },
    },
    highlighting: {
      magnetAdsorbed: {
        name: 'stroke',
        args: {
          attrs: {
            fill: '#5F95FF',
            stroke: '#5F95FF',
          },
        },
      },
    },
  })

   register({
    shape: 'custom-vue-node',
    width: 120,
    height: 75,
    component: VueNode,
  })

建立绘图的公共配置,其中注册custom-vue-node的行为,支持我们用Vue组件去渲染节点,这样可以做到节点高度的定制化。

为了让数据的结构更清晰,这里没有写循环,上方学年学期的节点逐个添加,见文件源码的地址

课程数据,关键在于position的计算,对数据做遍历,然后计算x、y的值,最后画上连接线。

  for (let i = 0; i < res.length; i++) {
    let classIndex = genArrIndex(res[i])
    if (classIndex !== false) {
      classMap[classIndex].push({
        ...res[i]
      })
    }
  }
  console.log('第一步,', classMap, '按照学年学期把数据进行分组')
  let classCellsMap = {}
  for (let i = 0; i < classMap.length; i++) {
    for (let j = 0; j < classMap[i].length; j++) {
      cellItem = JSON.parse(JSON.stringify(example6))
      cellItem.attrs.text.text = classMap[i][j].kcmc;
      cellItem.position.x = genArrIndex(classMap[i][j]) * (150 + 150);
      cellItem.position.y = 200 + 100 * j;
      cellItem.attrs.custom = genColorAttr(classMap[i][j])
      cellItem.ports.items[0].id = `${genId(classMap[i][j])}-${'000' + (Number(j) + 1)}-0000-ports`;  // 大类  左右分支  分支排序  节点位置
      cellItem.ports.items[1].id = `${genId(classMap[i][j])}-${'000' + (Number(j) + 1)}-0001-ports`;
      cellItem.ports.items[2].id = `${genId(classMap[i][j])}-${'000' + (Number(j) + 1)}-0002-ports`;
      cellItem.ports.items[3].id = `${genId(classMap[i][j])}-${'000' + (Number(j) + 1)}-0003-ports`;
      cellItem.id = `${genId(classMap[i][j])}-${'000' + (Number(j) + 1)}-0000-node`
      cellItem.i = i
      cellItem.j = j
      cellItem.origin = classMap[i][j] // 原始数据存一下 或许点击的时候会用到
      cells.push(cellItem)
      classCellsMap[classMap[i][j].kcmc] = JSON.parse(JSON.stringify(cellItem))
      // 如果不是第一个,就画个线
      if (j > 0) {
        cellItem = JSON.parse(JSON.stringify(example7));
        cellItem.source.cell = `000${genArrIndexRoot(classMap[i][j])}-000${genArrIndexLevel2(classMap[i][j])}-000${(Number(j))}-0000-node`;
        cellItem.source.port = `000${genArrIndexRoot(classMap[i][j])}-000${genArrIndexLevel2(classMap[i][j])}-000${(Number(j))}-0002-ports`;
        cellItem.target.cell = `000${genArrIndexRoot(classMap[i][j])}-000${genArrIndexLevel2(classMap[i][j])}-000${(Number(j) + 1)}-0000-node`;
        cellItem.target.port = `000${genArrIndexRoot(classMap[i][j])}-000${genArrIndexLevel2(classMap[i][j])}-000${(Number(j) + 1)}-0000-ports`;
        cellItem.id = `0000-0000-${'000' + (Number(i) + 1)}-${'000' + (Number(j) + 1)}`;
        cellItem.attrs = { line: { targetMarker: null } }
        cells.push(cellItem);
      }
    }
  }
  console.table(cells)

如果希望自己配置节点的样式,可以把CSS属性放到服务端维护,传给Vue节点,然后再进行样式自定义的实现。