使用fabric.js根据坐标生成svg图,并使用echarts显示

仍然是 在图片上特定区域根据数值显示不同的颜色 的需求。拖了这么久,最终的解决方案终于定下来了:使用aoi检测设备导出的坐标来标定需显示数值和颜色的区域,如此一来就不需要人操作UI界面来标定数值的显示区域。

最终使用echarts显示的方法有2种:

  • 地图map+使用坐标标记区域且区域有name属性的svg,根据区域的name属性显示对应的值
  • 图片直接转成svg图作为地图+地理坐标系+散点图

地图+区域有name属性的svg

先将背景图放入images文件夹中,利用上传的xlsx文件中的坐标标记背景图的区域,最后打包下载svg图和背景图。

<script src="../plugins/fabric.min.js"></script>
<script src="../plugins/jquery/jquery-3.3.1.js"></script>
<script type="text/javascript" src="../plugins/jszip.min.js"></script>
<script src="../plugins/FileSaver.js"></script>
<script src="../plugins/xlsx.full.min.js"></script>
<input type="file" name="file" id="uploadFile" size="10" onchange="readFile(this);" />
<canvas id="example" style="border: solid 1px #ccc"></canvas>
<script>
  function handleSvg(data) {
    const img_folder_path = "../images/";
    const local_url = "http://localhost:3000/html-demos/images/";
    const replace_url = encodeURI(`http://localhost:3000/html-demos/images/`);

    console.log(data);
    Promise.all(
      Object.keys(data)
        .map((filename, idx) => {
          return new Promise((resolve, reject) => {
            const img_path = img_folder_path + filename + ".jpg";
            fabric.Image.fromURL(img_path, (bg_img) => {
              const canvas = new fabric.StaticCanvas("example");
              canvas.setBackgroundImage(bg_img, canvas.renderAll.bind(canvas));
              canvas.setWidth(bg_img.width);
              canvas.setHeight(bg_img.height);

              const coord_arr = data[filename]["coords"];
              const name_arr = data[filename]["names"];
              const area_width = 20;
              const area_height = 20;
              for (let i in coord_arr) {
                const rect = new fabric.Rect({
                  left: coord_arr[i][0] - area_width / 2,
                  top: coord_arr[i][1] - area_height / 2,
                  originX: "left",
                  originY: "top",
                  width: area_width,
                  height: area_height,
                  fill: "rgba(255, 0, 0, 1)",
                });
                rect.toSVG = (function (toSVG) {
                  return function () {
                    const svgString = toSVG.call(this);
                    const domParser = new DOMParser();
                    const doc = domParser.parseFromString(svgString, "image/svg+xml");
                    let type = this.type;
                    const parentG = doc.querySelector(`${type}`);
                    parentG.setAttribute("name", name_arr[i]);
                    return doc.documentElement.outerHTML;
                  };
                })(rect.toSVG);
                canvas.add(rect);
              }
              const svgString = canvas.toSVG(null, function (svg) {
                return svg.replace(local_url, replace_url);
              });
              resolve({ [filename + ".svg"]: svgString });
            });
          });
        })
        .concat(
          Object.keys(data).map((filename, idx) => {
            return new Promise((resolve, reject) => {
              const img = new Image();
              img.crossOrigin = "Anonymous"; // 图片可以跨域访问
              img.onload = () => {
                const canvas = document.createElement("canvas");
                canvas.width = img.width;
                canvas.height = img.height;
                const ctx = canvas.getContext("2d");
                ctx.drawImage(img, 0, 0);
                canvas.toBlob((blob) => {
                  resolve({ [filename + ".jpg"]: blob });
                });
              };
              img.onerror = reject;
              img.src = img_folder_path + filename + ".jpg";
            });
          })
        )
    )
      .then((images) => {
        const zip = new JSZip();
        images.forEach((image) => {
          Object.keys(image).forEach((key) => {
            zip.file(key, image[key]);
          });
        });
        zip.generateAsync({ type: "blob" }).then((blob) => {
          saveAs(blob, "svg.zip");
        });
      })
      .catch((err) => {
        console.error("Error packaging images:", err);
      });
  }

  function readFile() {
    const file = $("#uploadFile")[0].files[0];
    const fileTypes = ["application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"];
    if (fileTypes.indexOf(file.type) === -1) {
      alert("文件类型错误");
      return;
    }
    const reader = new FileReader();
    const data_res = {};
    reader.onload = function (e) {
      const data = e.target.result;
      const workbook = XLSX.read(data, { type: "binary", cellDates: true });
      const sheetNames = workbook.SheetNames;
      const worksheet = workbook.Sheets[sheetNames[0]];
      const res = XLSX.utils.sheet_to_json(worksheet);
      for (let i in res) {
        const values = Object.values(res[i]);
        if (values.length < 4) continue;
        // const key = values[0] + "_" + values[1];
        const key = values[0];
        if (key in data_res) {
          data_res[key]["coords"].push(values[3].split(","));
          data_res[key]["names"].push(values[2]);
        } else {
          data_res[key] = {
            coords: [values[3].split(",")],
            names: [values[2]],
          };
        }
      }
      handleSvg(data_res);
    };
    reader.readAsBinaryString(file);
  }
</script>

 文件格式如下:

 生成的zip文件解压如下:

使用echarts加载svg图并填入数值

<script src="../plugins/echarts.min.v5.4.2.js"></script>
<script src="../plugins/jquery/jquery-3.3.1.js"></script>
<body>
  <div id="mapChart" style="height: 400px"></div>
  <script>
    function drawChart() {
      var chartDom = document.getElementById("mapChart");
      var myChart = echarts.init(chartDom);
      var option;

      $.get("../images/a.svg", function (svg) {
        echarts.registerMap("svg_map", { svg: svg });
        option = {
          tooltip: {},
          visualMap: {
            left: "left",
            top: 10,
            min: 0,
            max: 100,
            orient: "horizontal",
            text: ["", "Value"],
            realtime: true,
            calculable: true,
            inRange: {
              color: ["#0732FC", "#F92606"],
            },
          },
          series: [
            {
              name: "Value",
              type: "map",
              map: "svg_map",
              roam: true,
              emphasis: {
                label: {
                  show: false,
                },
              },
              selectedMode: false,
              data: [{ name: "aa", value: 95 }],
            },
          ],
        };
        myChart.setOption(option);
      });

      option && myChart.setOption(option);
    }

    drawChart();
  </script>
</body>

 

 svg图+地理坐标系+散点图 

<script src="../plugins/fabric.min.js"></script>
<script src="../plugins/jquery/jquery-3.3.1.js"></script>
<script type="text/javascript" src="../plugins/jszip.min.js"></script>
<script src="../plugins/FileSaver.js"></script>
<canvas id="example" style="border: solid 1px #ccc"></canvas>
<script>
  function handleSvg(data) {
    const img_folder_path = "../images/";
    const local_url = "http://localhost:3000/html-demos/images/";
    const replace_url = encodeURI(`http://localhost:3000/html-demos/images/`);

    console.log(data);
    Promise.all(
      data
        .map((filename, idx) => {
          return new Promise((resolve, reject) => {
            const img_path = img_folder_path + filename + ".jpg";
            fabric.Image.fromURL(img_path, (bg_img) => {
              const canvas = new fabric.StaticCanvas("example");
              canvas.setBackgroundImage(bg_img, canvas.renderAll.bind(canvas));
              canvas.setWidth(bg_img.width);
              canvas.setHeight(bg_img.height);

              const svgString = canvas.toSVG(null, function (svg) {
                return svg.replace(local_url, replace_url);
              });
              resolve({ [filename + ".svg"]: svgString });
            });
          });
        })
        .concat(
          data.map((filename, idx) => {
            return new Promise((resolve, reject) => {
              const img = new Image();
              img.crossOrigin = "Anonymous"; // 图片可以跨域访问
              img.onload = () => {
                const canvas = document.createElement("canvas");
                canvas.width = img.width;
                canvas.height = img.height;
                const ctx = canvas.getContext("2d");
                ctx.drawImage(img, 0, 0);
                canvas.toBlob((blob) => {
                  resolve({ [filename + ".jpg"]: blob });
                });
              };
              img.onerror = reject;
              img.src = img_folder_path + filename + ".jpg";
            });
          })
        )
    )
      .then((images) => {
        const zip = new JSZip();
        images.forEach((image) => {
          Object.keys(image).forEach((key) => {
            zip.file(key, image[key]);
          });
        });
        zip.generateAsync({ type: "blob" }).then((blob) => {
          saveAs(blob, "svg.zip");
        });
      })
      .catch((err) => {
        console.error("Error packaging images:", err);
      });
  }

  handleSvg(["a", "b"]);
</script>

 使用echarts的geo坐标系显示数值区域

<script src="../plugins/echarts.min.js"></script>
<script src="../plugins/jquery/jquery-3.6.0.min.js"></script>
<div style="width: 100%; height: 500px" id="main"></div>
<script>
  const ROOT_PATH = "http://localhost:3000/html-demos/images/";

  $.get(ROOT_PATH + "b.svg", function (svg) {
    //保证加载到图表中的svg图不变形
    const svg_width = parseInt(svg.documentElement.getAttribute("width"));
    const svg_height = parseInt(svg.documentElement.getAttribute("height"));
    const svg_ratio = svg_width / svg_height;
    const map_height = 500;
    const map_width = Math.ceil(svg_ratio * map_height);

    const chartDom = document.getElementById("main");
    elem.style.width = map_width + "px";
    const myChart = echarts.init(chartDom);
    let option;
    echarts.registerMap("svg_map", { svg: svg });
    option = {
      tooltip: {
        formatter: (params) => {
          return `${params.seriesName} <br/>
                  ${params.marker}Value:${params.value[2]}`;
        },
      },
      geo: {
        tooltip: {
          show: true,
        },
        map: "svg_map",
        roam: true,
      },
      series: [
        {
          type: "scatter",
          name: "aa",
          coordinateSystem: "geo",
          geoIndex: 0,
          symbol: "rect",
          symbolSize: 10,
          itemStyle: {
            color: "#b02a02",
          },
          encode: {
            tooltip: 2,
          },
          data: [
            [100, 200, 95],
            [150, 150, 30],
          ],
        },
      ],
    };
    myChart.setOption(option);
  });

</script>

总的来说:使用geo坐标系比较方便,在本例中的缺点需要自己计算颜色,且series中很多项(不同的数值显示不同颜色)。

另外需注意svg图的加载问题:svg图占用空间较小,svg图加载到页面上后,会继续加载svg文件中引用的图片,因此一定要保证图片可访问。

 

另外一种生成svg的方式是将背景图内容base64编码后直接放入svg图中,则不会有加载2次图片的问题:

<script>
    ...
    const svgString = canvas.toSVG(null, function (svg) {
        const url_val = encodeURI(`http://localhost:3000/html-demos/images/${filename}.jpg`);
        return svg.replace(url_val, bg_img.toDataURL());
     });
     ...
</script>
  

 生成svg图内容如下:这种还是比较方便的,不用去加载2次图片,也没有原图片的路径问题

顺便记录下canvas处理图片为灰度图,并将图片转成svg格式

<script type="text/javascript" src="../plugins/jszip.min.js"></script>
<script src="../plugins/FileSaver.js"></script>
<script>
  function imgToSvg(data) {
    Promise.all(
      data.map((filename, idx) => {
        return new Promise((resolve, reject) => {
          const local_url = encodeURI(`http://localhost:3000/html-demos/${filename}`);

          const canvas = document.createElement("canvas");
          const ctx = canvas.getContext("2d");
          const img = new Image();
          img.src = filename;
          img.onload = function () {
            canvas.height = img.height;
            canvas.width = img.width;

            ctx.drawImage(img, 0, 0);
            const imgdata = ctx.getImageData(0, 0, canvas.width, canvas.height);
            const data = imgdata.data;
            for (let i = 0, n = data.length; i < n; i += 4) {
              const grayscale = data[i] * 0.3 + data[i + 1] * 0.59 + data[i + 2] * 0.11;
              data[i] = grayscale;
              data[i + 1] = grayscale;
              data[i + 2] = grayscale;
            }
            ctx.putImageData(imgdata, 0, 0);

            const width = canvas.width;
            const height = canvas.height;
            const dataUrl = canvas.toDataURL();
            const svgXML = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${width}" height="${height}">
                        <image xlink:href="${dataUrl}" width="${width}" height="${height}" />
                    </svg>`;
            const svgBlob = new Blob([svgXML], { type: "image/svg+xml;charset=utf-8" });
            resolve({ [filename + ".svg"]: svgBlob });
          };
        });
      })
    )
      .then((images) => {
        const zip = new JSZip();
        images.forEach((image) => {
          Object.keys(image).forEach((key) => {
            zip.file(key, image[key]);
          });
        });
        zip.generateAsync({ type: "blob" }).then((blob) => {
          saveAs(blob, "svg.zip");
        });
      })
      .catch((err) => {
        console.error("Error packaging images:", err);
      });
  }

  imgToSvg(["../images/1.jpg", "../images/2.jpg"]);
</script>

 

posted @ 2024-04-15 16:31  carol2014  阅读(125)  评论(0编辑  收藏  举报