基于echarts的svg曲线绘制工具 2022-01-09

目标

使用echarts的拖拽模板绘制曲线并导出为svg。
顺便练练vue3@vueuseecharts的熟练度。

效果图

在这里插入图片描述

Demo

CodePen地址

源码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body,
    html {
      margin: 0
    }

    .draggable-bar {
      position: fixed;
      z-index: 1001;
      width: 16px;
      height: 16px;
      background-color: rgba(0, 0, 0, 0.2);
      cursor: move;
    }

    .tool-bar {
      position: absolute;
      top: 16px;
    }

    .box {
      position: relative;
      width: max-content;
    }

    .chart-box {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }

    .points {
      width: 100%;
    }
  </style>
</head>

<body>
  <div id="app">
    <div class="draggable-bar" :style="dragStyle" ref="dragEl">
      <div class="tool-bar">
        <input type="number" v-model="pointSize" />
        <input type="checkbox" v-model="isShowPoint" />
        <input type="file" ref="fileEl" v-show="false" accept="image/*" />
        <button @click="handleAdd">+</button>
        <button @click="handleRemove">-</button>
        <button @click="handleSelectImg">选择图片</button>
        <button @click="handleExportSvg">下载svg</button>
        <br />
        <textarea class="points" v-model="pointsJson"></textarea>
      </div>
    </div>
    <div ref="boxEl" class="box">
      <img :src="imgBase64" />
      <div ref="chartBox" class="chart-box"></div>
    </div>
  </div>
  <script src="https://unpkg.com/vue@next"></script>
  <script crossorigin="anonymous"
    integrity="sha512-UN8wX5Zf4Af6/2UJOYTYyWLHdua4SWMd1pnIxNoDCtqdaAMk1TQdvwwgoG7ShvuOS1d9jCerLNzwfvRmL7N4iA=="
    src="https://lib.baomitu.com/echarts/5.2.0/echarts.min.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
  <script src="https://unpkg.com/@vueuse/shared"></script>
  <script src="https://unpkg.com/@vueuse/core"></script>
  <script>
    const { createApp, ref, unref, reactive, computed, watch, getCurrentInstance } = Vue;
    const { useEventListener, useMounted, get, set, tryOnMounted, useLocalStorage, useDraggable, useBase64, useResizeObserver, useToggle } = VueUse;

    window.onload = function () {
      const app = createApp({
        setup() {
          let myChart;
          const boxEl = ref(); // echarts组件外框
          const chartBox = ref(); // echarts组件依赖元素
          const dragEl = ref(); // 工具栏拖拽用元素
          const fileEl = ref(); // 文件选择元素
          const isShowPoint = useLocalStorage("isShowPoint", true); // 是否显示控制点
          const pointSize = useLocalStorage("pointSize", 16); // 控制点大小
          const points = useLocalStorage("points", [[10, 10]]); // 控制点位数组
          const pointsJson = computed({
            get: () => JSON.stringify(get(points)),
            set: str => set(points, JSON.parse(!!str ? str : `[[10, 10]]`))
          }); // 控制点位数组显示用Json串
          const [resizeObserver, boxResize] = useToggle() // 切换图片时用的监听器和监听触发方法
          const imgBase64 = useLocalStorage("imgBase64", null); // 选择完的图片

          const grid = {
            top: 0,
            right: 0,
            bottom: 0,
            left: 0
          };

          const getAxisDef = (other) => {
            return _.assign({
              type: 'value',
              min: 0,
              show: false,
            }, other);
          };

          // x轴配置
          const xAxis = computed(() => {
            get(resizeObserver) // 图片切换时的监听,切换图片会重新计算
            return getAxisDef({ max: get(chartBox)?.offsetWidth ?? 0 });
          });

          // y轴配置
          const yAxis = computed(() => {
            get(resizeObserver) // 图片切换时的监听,切换图片会重新计算
            return getAxisDef({ max: get(chartBox)?.offsetHeight ?? 0 });
          });

          // 制作的曲线的基础配置
          const line = computed(() => {
            return {
              type: 'line',
              smooth: true,
              showSymbol: get(isShowPoint),
              symbolSize: get(pointSize),
              data: get(points),
            }
          });

          // 控制点事件的绑定位置
          const graphic = computed(() => {
            return get(points).map((x, i) => ({
              type: 'circle',
              position: myChart.convertToPixel('grid', x),
              shape: {
                cx: 0,
                cy: 0,
                r: get(pointSize) / 2
              },
              invisible: true,
              draggable: get(isShowPoint),
              ondrag: ({ offsetX, offsetY }) => {
                let newPoint = myChart.convertFromPixel('grid', [offsetX, offsetY]);
                get(points).splice(i, 1, newPoint);
              },
              z: 1000
            }))
          });

          // echarts的配置
          const option = computed(() => {
            return {
              grid,
              xAxis: get(xAxis),
              yAxis: get(yAxis),
              series: [get(line)],
              color: ['#000000'],
            }
          })

          tryOnMounted(() => {
            // 初始化echarts为svg渲染模式
            myChart = echarts.init(get(chartBox), null, { renderer: 'svg' });

            // 首次设定,因为graphic的绘制需要线条先描绘出来,所以还需要再设定一次配置
            myChart.setOption(get(option));

            // 修改配置时,触发配置重新设定,echarts是差分配置,所以放全部配置也无所谓
            watch(() => option, () => {
              myChart.setOption(_.assign({ graphic: get(graphic) }, get(option)));
            }, {
              immediate: true,
              deep: true
            });
          });

          // 注册工具栏拖拽行为的控制器
          const { style: dragStyle } = useDraggable(dragEl, { exact: true });

          // 添加控制点事件
          function handleAdd() {
            let last = _.last(get(points));
            get(points).push(last.map(x => x + get(pointSize)));
          }

          // 删除控制点事件
          function handleRemove() {
            get(points).pop();
          }

          // 选择图片的事件
          function handleSelectImg() {
            let el = get(fileEl);
            el.value = "";
            useEventListener(fileEl, "change", async (e) => {
              const { execute } = useBase64(e.target.files[0]);
              const base64 = await execute();
              set(imgBase64, base64);
            }, { once: true });
            el.click();
          }

          // 切换图片时,触发echarts重绘
          useResizeObserver(boxEl, () => {
            myChart.resize();
            myChart.clear();
            boxResize();
          })

          // 导出svg事件
          function handleExportSvg() {
            const el = unref(chartBox);
            const old = unref(isShowPoint);
            const path = el.querySelector("path");
            set(isShowPoint, false);
            const blob = new Blob([`<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" 
            version="1.1" baseProfile="full" width="${el.offsetWidth}" height="${el.offsetHeight}">
            ${path.outerHTML}</svg>`], { type: "text/xml" });
            const a = document.createElement("a")
            a.href = URL.createObjectURL(blob)
            a.download = `${Date.now()}.svg` // 这里填保存成的文件名
            a.click()
            URL.revokeObjectURL(a.href)
            a.remove();
            set(isShowPoint, old)
          }

          return {
            boxEl,
            chartBox,
            dragEl,
            fileEl,
            dragStyle,
            isShowPoint,
            pointSize,
            pointsJson,
            option,
            imgBase64,
            handleAdd,
            handleRemove,
            handleSelectImg,
            handleExportSvg,
          };
        }
      });
      app.mount("#app");
    }
  </script>
</body>

</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值