为了提高出图效率,我做了一款可视化制作气泡图谱的小工具

嗨,大家好,我是徐小夕,之前和大家分享了很多可视化低代码的最佳实践,今天和大家分享一下我基于实际场景开发的小工具——BubbleMap

c26303ebe2ebfb1cf67b54fc3d0faf1c.gif


demo地址:http://wep.turntip.cn/design/bubbleMap

开发背景

之前在公司做图表开发的时候涉及到了气泡图的开发,但是由于运营部对这种图需求比较大,所以每次都要找研发人员来支持,做图表数据更新。长此以往就导致研发小伙伴占用了很多琐碎的时间来做这种基础任务,运营小同学也觉得很不方便。

4257040512cbcb37bca7826696dc5e69.png
image.png

基于这样的场景,我就想到了能不能提供一种可视化的方案,让运营人员全权接管这类需求,然后我就开始规划,其实只需要几步:

  • 气泡图谱实现

  • 在线编辑数据

  • 实时更新图表

最后基于不断的演算推理+实践,这款小工具也成功上线,如果大家有类似的需要,也可以直接免费使用。接下来我就和大家分享一下它的实现思路。(PS: 如果大家想参考实现源码,可以在趣谈前端公众号回复气泡源码)

实现思路

3412fed20ae4a1b8581688cec5080c29.png
image.png

整个工具其实只需要分为两部分:

  • 画布图表区

  • 数据编辑区

画布图表区用来预览图表效果,我们可以使用市面上比较成熟的开源图表库比如EchartAntv来实现,这里我选择了蚂蚁的Antv

15c5936b80713b244a44fdccce9c024e.png
image.png

对于数据编辑区,我们可以用很多方式来实现,比如:

  • 表格组件

6236b17229cdb0f858255298589886d2.png
image.png

首先想到的就是 antd 的可编辑表格组件,它提供了完整的案例demo,我们直接基于源码改吧改吧就能用。

  • 电子表格

576d09535791227b59740ac538669731.png
image.png

电子表格也是不错的选择,我们可以用 excel 的表格编辑方式来编辑数据, 比如常用的表格开源项目handsontable.js

  • 嵌套表单

58011ee5b9ddf8b7cb2207166c4ccd54.gif
6241.gif

当然这种方式成本也很低,前端小伙伴们可以用antdform组件或者其他UI组件库实现类似的效果。我在实现气泡图谱工具的时候就是采用的这种方案。

嵌套表单代码案例如下:

import React from 'react';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Form, Input, Space } from 'antd';

const onFinish = (values: any) => {
  console.log('Received values of form:', values);
};

const App: React.FC = () => (
  <Form
    name="dynamic_form_nest_item"
    onFinish={onFinish}
    style={{ maxWidth: 600 }}
    autoComplete="off"
  >
    <Form.List name="data">
      {(fields, { add, remove }) => (
        <>
          {fields.map(({ key, name, ...restField }) => (
            <Space key={key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
              <Form.Item
                {...restField}
                name={[name, 'name']}
                rules={[{ required: true, message: '请输入字段名称' }]}
              >
                <Input placeholder="字段名称" />
              </Form.Item>
              <Form.Item
                {...restField}
                name={[name, 'value']}
                rules={[{ required: true, message: '请输入字段值' }]}
              >
                <Input placeholder="字段值" />
              </Form.Item>
              <MinusCircleOutlined onClick={() => remove(name)} />
            </Space>
          ))}
          <Form.Item>
            <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
              Add field
            </Button>
          </Form.Item>
        </>
      )}
    </Form.List>
    <Form.Item>
      <Button type="primary" htmlType="submit">
        Submit
      </Button>
    </Form.Item>
  </Form>
);

export default App;

当然气泡图我这里采用的是antv/g6:

fed084c7d695c8a432e9cbf3fe93e736.png
image.png

由于g6学习有一定成本,这里简单介绍一下使用。

我们先注册一个气泡的节点:

G6.registerNode(
          'bubble',
          {
            drawShape(cfg: any, group: any) {
              const self: any = this;
              const r = cfg.size / 2;
              // a circle by path
              const path = [
                ['M', -r, 0],
                ['C', -r, r / 2, -r / 2, r, 0, r],
                ['C', r / 2, r, r, r / 2, r, 0],
                ['C', r, -r / 2, r / 2, -r, 0, -r],
                ['C', -r / 2, -r, -r, -r / 2, -r, 0],
                ['Z'],
              ];
              const keyShape = group.addShape('path', {
                attrs: {
                  x: 0,
                  y: 0,
                  path,
                  fill: cfg.color || 'steelblue',
                },
                // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
                name: 'path-shape',
              });
        
              const mask = group.addShape('path', {
                attrs: {
                  x: 0,
                  y: 0,
                  path,
                  opacity: 0.25,
                  fill: cfg.color || 'steelblue',
                  shadowColor: cfg.color.split(' ')[2].substr(2),
                  shadowBlur: 40,
                  shadowOffsetX: 0,
                  shadowOffsetY: 30,
                },
                // must be assigned in G6 3.3 and later versions. it can be any string you want, but should be unique in a custom item type
                name: 'mask-shape',
              });
        
              const spNum = 10; // split points number
              const directions: number[] = [],
                rs: number[] = [];

              self.changeDirections(spNum, directions);
              for (let i = 0; i < spNum; i++) {
                const rr = r + directions[i] * ((Math.random() * r) / 1000); // +-r/6, the sign according to the directions
                if (rs[i] < 0.97 * r) rs[i] = 0.97 * r;
                else if (rs[i] > 1.03 * r) rs[i] = 1.03 * r;
                rs.push(rr);
              }
              keyShape.animate(
                () => {
                  const path = self.getBubblePath(r, spNum, directions, rs);
                  return { path };
                },
                {
                  repeat: true,
                  duration: 10000,
                },
              );
        
              const directions2: number[] = [],
                rs2: number[] = [];
              self.changeDirections(spNum, directions2);
              for (let i = 0; i < spNum; i++) {
                const rr = r + directions2[i] * ((Math.random() * r) / 1000); // +-r/6, the sign according to the directions
                if (rs2[i] < 0.97 * r) rs2[i] = 0.97 * r;
                else if (rs2[i] > 1.03 * r) rs2[i] = 1.03 * r;
                rs2.push(rr);
              }
              mask.animate(
                () => {
                  const path = self.getBubblePath(r, spNum, directions2, rs2);
                  return { path };
                },
                {
                  repeat: true,
                  duration: 10000,
                },
              );
              return keyShape;
            },
            changeDirections(num: number, directions: number[]) {
              for (let i = 0; i < num; i++) {
                if (!directions[i]) {
                  const rand = Math.random();
                  const dire = rand > 0.5 ? 1 : -1;
                  directions.push(dire);
                } else {
                  directions[i] = -1 * directions[i];
                }
              }
              return directions;
            },
            getBubblePath(r: number, spNum: number, directions: number[], rs: number[]) {
              const path = [];
              const cpNum = spNum * 2; // control points number
              const unitAngle = (Math.PI * 2) / spNum; // base angle for split points
              let angleSum = 0;
              const sps = [];
              const cps = [];
              for (let i = 0; i < spNum; i++) {
                const speed = 0.001 * Math.random();
                rs[i] = rs[i] + directions[i] * speed * r; // +-r/6, the sign according to the directions
                if (rs[i] < 0.97 * r) {
                  rs[i] = 0.97 * r;
                  directions[i] = -1 * directions[i];
                } else if (rs[i] > 1.03 * r) {
                  rs[i] = 1.03 * r;
                  directions[i] = -1 * directions[i];
                }
                const spX = rs[i] * Math.cos(angleSum);
                const spY = rs[i] * Math.sin(angleSum);
                sps.push({ x: spX, y: spY });
                for (let j = 0; j < 2; j++) {
                  const cpAngleRand = unitAngle / 3;
                  const cpR = rs[i] / Math.cos(cpAngleRand);
                  const sign = j === 0 ? -1 : 1;
                  const x = cpR * Math.cos(angleSum + sign * cpAngleRand);
                  const y = cpR * Math.sin(angleSum + sign * cpAngleRand);
                  cps.push({ x, y });
                }
                angleSum += unitAngle;
              }
              path.push(['M', sps[0].x, sps[0].y]);
              for (let i = 1; i < spNum; i++) {
                path.push([
                  'C',
                  cps[2 * i - 1].x,
                  cps[2 * i - 1].y,
                  cps[2 * i].x,
                  cps[2 * i].y,
                  sps[i].x,
                  sps[i].y,
                ]);
              }
              path.push(['C', cps[cpNum - 1].x, cps[cpNum - 1].y, cps[0].x, cps[0].y, sps[0].x, sps[0].y]);
              path.push(['Z']);
              return path;
            },
            // @ts-ignore
            setState(name: string, value: number, item: any) {
              const shape = item.get('keyShape');
              if (name === 'dark') {
                if (value) {
                  if (shape.attr('fill') !== '#fff') {
                    shape.oriFill = shape.attr('fill');
                    const uColor = unlightColorMap.get(shape.attr('fill'));
                    shape.attr('fill', uColor);
                  } else {
                    shape.attr('opacity', 0.2);
                  }
                } else {
                  if (shape.attr('fill') !== '#fff') {
                    shape.attr('fill', shape.oriFill || shape.attr('fill'));
                  } else {
                    shape.attr('opacity', 1);
                  }
                }
              }
            },
          },
          'single-node',
        );

然后用g6的动画和渲染API来渲染出气泡图谱的动画效果和样式,即可。

最后实现的效果如下:

d361e5e11b7321916fc91fc18a7a33d2.png
image.png

效果演示

在实现好这个小工具之后,我来带大家演示一下:

10456cfd884fa74d2223de705f04e094.gif


我们可以在右侧编辑修改数据,点击生成即可更新图谱。

后期展望

后续会持续优化它,来满足更多图表的支持,大家感兴趣的可以体验反馈~

demo地址:http://wep.turntip.cn/design/bubbleMap

e86c2850defe75511a75c5436c472f3d.png

往期精彩

  • 零代码+AI的阶段性复盘

  • 文档引擎+AI可视化打造下一代文档编辑器

  • 爆肝1000小时, Dooring零代码搭建平台3.5正式上线

  • 从零打造一款基于Nextjs+antd5.0的中后台管理系统

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/745581.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LINUX操作系统:Mx Linux,用虚拟机VMware Workstation安装体验

需求说明&#xff1a; 操作系统目前流行有Windows、Linux、Unix等&#xff0c;中国人应该要知道国有操作系统&#xff0c;也要支持国产操作系统&#xff0c;为了更好支持国产操作系统&#xff0c;我们也要知己知彼&#xff0c;那么今天就来体验一把操作系统Mx_Linux_23.2的安装…

mac m芯片下安装nacos

背景&#xff1a;最近再研究 下载地址&#xff1a; https://nacos.io/download/nacos-server/ 解压zip包 unzip nacos-server-2.3.2.zip启动 进入到bin目录下 ./startup.sh -m standalone访问可视化界面 账号密码都是nacos&#xff0c;进行登录即可&#xff0c;nacos的端口为…

打破网络通信界限,推动供应链数字化转型

在当前全球经济低迷的背景下&#xff0c;中国经济的发展模式正在转变&#xff0c;从规模扩张到品质提升&#xff0c;通过优势的整合和叠加&#xff0c;释放出新的生产力。2023年前三季度&#xff0c;中国国内生产总值达到91.3万亿元&#xff0c;同比增长了5.2%1&#xff0c;增速…

红酒与珠宝:璀璨与醇香的奢华交响,双重诱惑难挡

在璀璨的灯光下&#xff0c;红酒与珠宝各自闪耀着迷人的光芒&#xff0c;它们如同夜空中的繁星&#xff0c;交相辉映&#xff0c;共同演绎着奢华的双重诱惑。今天&#xff0c;就让我们一起走进这个充满魅力的世界&#xff0c;感受红酒与珠宝带来的无尽魅力。 首先&#xff0c;让…

1966 ssm 流浪猫领养网站系统开发mysql数据库web结构java编程计算机网页源码eclipse项目

一、源码特点 ssm 流浪猫领养网站系统是一套完善的信息系统&#xff0c;结合springMVC框架完成本系统&#xff0c;对理解JSP java编程开发语言有帮助系统采用SSM框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/…

红酒舞动,运动风采,品味力与美

当夜幕降临&#xff0c;城市的灯火渐次亮起&#xff0c;忙碌了一天的人们开始寻找那份属于自己的宁静与愉悦。在这个时刻&#xff0c;红酒与运动&#xff0c;这两个看似截然不同的元素&#xff0c;却能以它们不同的魅力&#xff0c;为我们带来一场视觉与感官的盛宴。 红酒&…

【八股系列】Vue中的<keep-alive>组件:深入解析与实践指南

&#x1f389; 博客主页&#xff1a;【剑九 六千里-CSDN博客】 &#x1f3a8; 上一篇文章&#xff1a;【探索响应式布局的奥秘&#xff1a;关键技术与实战代码示例】 &#x1f3a0; 系列专栏&#xff1a;【面试题-八股系列】 &#x1f496; 感谢大家点赞&#x1f44d;收藏⭐评论…

如何在前端项目中制定代码注释规范

本文是前端代码规范系列文章&#xff0c;将涵盖前端领域各方面规范整理&#xff0c;其他完整文章可前往主页查阅~ 开始之前&#xff0c;介绍一下​最近很火的开源技术&#xff0c;低代码。 作为一种软件开发技术逐渐进入了人们的视角里&#xff0c;它利用自身独特的优势占领市…

要离职了,记录一下个人在用的 Mac 应用

大家好&#xff0c;我是楷鹏。 通用 飞书 说起来不信&#xff0c;第一个推荐的是【飞书】&#xff0c;飞书是目前用过最舒服的项目管理应用了。 单拎出来一个飞书文档&#xff0c;功能和体验远超市面上腾讯文档、石墨文档、语雀等等。 现在飞书还支持个人版&#xff0c;No…

ChatGPT的Mac客户端正式发布了

ChatGPT的Mac客户端正式发布了&#xff01;Mac用户有福了 &#x1f389; 大家好&#xff0c;我是猫头虎&#xff0c;科技自媒体博主。今天我带来了一个超级重磅的消息 &#x1f4e2;&#xff0c;就是 ChatGPT 的客户端终于来了&#xff01;这对我们所有 Mac 用户&#xff0c;尤…

vue-cli 搭建项目,ElementUI的搭建和使用

vue-cli 官方提供的一个脚手架&#xff0c;用于快速生成一个vue的项目模板&#xff1b;预先定义 好的目录结构及基础代码&#xff0c;就好比咱们在创建Maven项目时可以选择创建一个 骨架项目&#xff0c;这个骨架项目就是脚手架&#xff0c;我们的开发更加的快速&#xff1b; …

DHT11

第一个传感----DHT11 通过前面的学习&#xff0c;你已经学会了控制IO口、延时函数、串口的收发。接下来&#xff0c;你就可以借助以上的知识点完成自己的第一个传感器--DHT11啦&#xff01; DHT11 数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器。应用非常广…

中文+Midjourney,能描画出什么样的作品呢?保姆级上手指南送给你

中文Midjourney&#xff0c;能描画出什么样的作品呢&#xff1f; 中文版Midjourney来了&#xff01; 没有一点预热&#xff0c;Midjourney中文版&#xff08;以下简称 MJCN&#xff09;在本周开放了两次内测邀请&#xff0c;只需用 QQ 扫描邀请码&#xff0c;就可以在 QQ 频道…

PMP证书在国内已经泛滥了,大家怎么看?

目前&#xff0c;越来越多的人获得了PMP证书。自1999年PMP引入中国以来&#xff0c;全国累计PMP考试人数接近60万人次&#xff0c;通过PMP认证的人数约为42万人。虽然这个数据看起来很大&#xff0c;但绝对不能说是过多。 首先&#xff0c;PMP在中国并不普遍。根据美国项目管理…

浮动IP绑定主机步骤

文章目录 1&#xff0c;查看本机的原有IP2&#xff0c;绑定浮动IP3&#xff0c;验证绑定情况4&#xff0c;解绑浮动IP 1&#xff0c;查看本机的原有IP ifconfig得到本机的ip是192.168.10.128 2&#xff0c;绑定浮动IP ifconfig ens33:1 192.168.10.10 netmask 255.255.255.0…

Shopee、Lazada测评,是找服务商呢?还是建议自己养号补单呢?

目前大部分Shopee、Lazada的卖家由于运营成本的增加&#xff0c;都会找服务商测评来打造权重&#xff0c;但是找服务商有很多不靠谱&#xff0c;建议还是自行精养一批号&#xff0c;账号在手里比较安全可控&#xff0c;随时随地可以送测&#xff0c;精准搜索关键词货比三家下单…

【SQL】数据操作语言(DML) - 删除数据:精细管理数据的利刃

目录 前言 DELETE语句的基础使用 删除指定记录 清空表与删除表数据的区别 注意 前言 在数据库管理的日常工作中&#xff0c;数据的删除是一项需要格外小心的操作&#xff0c;因为一旦数据被删除&#xff0c;往往难以恢复。数据操作语言(DML)中的DELETE语句&am…

COMSOL -电力输电线的电场和磁场仿真

为确保电力输电线周围人员和环境的安全&#xff0c;工程师必须对电力线产生的电场和磁场进行监控。通过多物理场仿真&#xff0c;工程师能够预测电力线产生的场如何从电力线中扩散&#xff0c;以及如何影响其辐射至地面的强度。这篇文章&#xff0c;我们将使用两个示例模型来说…

私有化部署ChatGPT:潜力与挑战

背景 以ChatGPT为代表的大语言模型服务在2023年初开始大规模爆发&#xff0c;AI技术从来没有如此接近普通民众。随着以Microsoft&#xff0c; Google&#xff0c; Meta &#xff08;Facebook&#xff09;为代表的科技巨头在AI技术领域相继发布重量级产品和服务&#xff0c;国内…