目录
步骤一:点击工具栏(Tools组件)内按钮触发画布(D3Canvas组件)绘制图元的方法:
D3.js绘图工具系列文章总提纲:【传送门】
效果图:
步骤一:点击工具栏(Tools组件)内按钮触发画布(D3Canvas组件)绘制图元的方法:
工具栏(Tools)组件:
@/views/D3/tools/index.vue
<template>
<div class="left-menu">
<div>
<el-button type="primary" @click="addCircle">圆点</el-button>
</div>
</div>
</template>
<script setup>
const emit = defineEmits(['addCircle']);
function addCircle() {
emit('addCircle');
}
</script>
画布(D3Canvas)组件:
@/views/D3/canvas/D3Canvas.vue
<template>
<div id="d3CanvasBox" class="main"></div>
</template>
<script setup>
import d3Canvas from '@/D3/main.js';
const svg = new d3Canvas('d3CanvasBox');
const drawCircle = () => svg.drawCircle();
onMounted(() => {
svg.init();
});
defineExpose({
drawCircle
})
</script>
<style lang="scss" scoped>
.main {
width: 100%;
height: 100%;
}
</style>
画布(D3Canvas)组件内所引用的画布实例(不全):
@/D3/main.js
import deepCompare from '@/D3/utils/deepCompare';
export default class d3Canvas {
constructor(parentNodeId) {
this.parentNodeId = parentNodeId;
this.svg = reactive({});
this.currentGraph = ref(null);
this.currentWatch = null;
}
init() {
// 获取父节点
var parentNode = document.getElementById(this.parentNodeId);
if (!parentNode) {
console.error('父节点不存在');
return;
}
// 初始化画布元素
var d3Canvas = document.createElement('div');
// 默认id为d3Canvas
d3Canvas.id = 'd3Canvas';
parentNode.appendChild(d3Canvas);
this.svg = this.initD3Canvas();
}
initD3Canvas() {
// 获取容积
const parentNode = document.getElementById(this.parentNodeId);
const currentWidth = parentNode.clientWidth;
const currentHeight = parentNode.clientHeight;
const d3Canvas = d3
.select('#d3Canvas')
.append("svg")
.attr("id", "svg")
.attr("width", currentWidth)
.attr("height", currentHeight)
.attr("viewBox", "0 0 " + currentWidth + " " + currentHeight);
return d3Canvas;
}
// 模拟id
generateUniqueId() {
// 生成随机数作为ID的一部分
// 生成6位长度的随机字符串
const randomPart = Math.random().toString(36).substr(2, 6);
// 获取当前时间戳作为ID的另一部分
// 取最后2位作为时间戳的字符串表示
const timestampPart = new Date().getTime().toString(36).substr(-2);
// 将随机数和时间戳组合成ID
const uniqueId = randomPart + timestampPart;
return uniqueId;
}
// 绘制圆
drawCircle() {
const _this = this;
let id = this.generateUniqueId();
let circle = this.svg.append("circle")
.attr("cx", 5)
.attr("cy", 5)
.attr("r", 2)
.style("fill", "blue")
.attr("id", `circle-${id}`);
}
}
步骤二:双击图元,属性栏可加载出该图元所对应的属性:
需要对图元添加的点击事件,即再上述@/D3/main.js所构造的d3Canvas中,添加点击事件方法并绑定于所绘制的图元上。
画布实例补全
@/D3/main.js
import useAttrStore from '@/store/modules/attr';
import deepCompare from '@/D3/utils/deepCompare';
export default class d3Canvas {
constructor(parentNodeId) {
……
}
……省略
// 绘制圆形
drawCircle() {
……
let circle =
……
.on("dblclick", function (event) {
let arg = d3.select(this);
_this.clickedEvent(event, arg, 'circle')
});
}
// 点击元素时执行的函数
clickedEvent(event, arg, type) {
const attrStore = useAttrStore();
if (this.currentGraph.value !== null) {
this.currentGraph.value = null;
attrStore.cleanAttr();
// 停止之前的 watch 监听器
if (this.currentWatch !== null) {
this.currentWatch();
this.currentWatch = null;
}
}
// 显示输入框
document.getElementById(`${type}Form`).style.display = "block";
// 获取被点击的圆形元素
const clickedArg = arg;
this.currentGraph.value = clickedArg;
var args = {
id: clickedArg.attr("id"),
cx: clickedArg.attr("cx"),
cy: clickedArg.attr("cy"),
r: clickedArg.attr("r"),
fill: clickedArg.style("fill"),
}
var keysArray = Object.keys(args);
keysArray.forEach(key => {
attrStore.setAttr(key, args[key])
})
// 调用attrStore中的mutations内的方法
attrStore.setAttrType(type);
// 旧属性数据
let oldAttrList = [];
this.currentWatch = watch(
() => attrStore.attrList,
(newValue) => {
if (attrStore.attrType != '') {
for (let i = 0; i < newValue.length; i++) {
if (!deepCompare(newValue[i], oldAttrList[i])) {
if (newValue[i].key == 'fill') {
clickedArg.style("fill", newValue[i].value);
} else {
clickedArg.attr(newValue[i].key, newValue[i].value);
}
}
}
// 更新旧值
oldAttrList = newValue.map(item => ({ ...item }));
}
},
{ deep: true } // 深层监听
);
}
}
属性(attr)的pinia管理
此处涉及到属性的存储及传递,我的处理方式是将参数属性均暂存于 pinia 中管理。
@/store/modules/attr
const useAttrStore = defineStore(
'attr',
{
state: () => ({
attrType: '',
attrList: new Array(),
}),
// actions
actions: {
getAttr(_key) {
if (_key == null && _key == "") {
return null;
}
try {
for (let i = 0; i < this.attrList.length; i++) {
if (this.attrList[i].key == _key) {
return this.attrList[i].value;
}
}
} catch (e) {
return null;
}
},
setAttr(_key, value) {
if (_key !== null && _key !== "") {
this.attrList.push({
key: _key,
value: value
});
}
},
changeAttr(_key, _value) {
let _index = this.attrList.findIndex(item => item.key === _key);
this.attrList[_index].key = _key;
this.attrList[_index].value = _value;
},
setAttrType(_type) {
this.attrType = _type;
},
// 删除属性
removeAttr(_key) {
var bln = false;
try {
for (let i = 0; i < this.attrList.length; i++) {
if (this.attrList[i].key == _key) {
this.attrList.splice(i, 1);
return true;
}
}
} catch (e) {
bln = false;
}
return bln;
},
// 清空属性列表
cleanAttr() {
this.attrType = '';
this.attrList = new Array();
},
// 初始化属性列表
initAttr() {
}
},
// getters
getters: {
attrObject(state) {
// 将数组转为对象
let obj = {};
for (let i = 0; i < state.attrList.length; i++) {
obj[state.attrList[i].key] = state.attrList[i].value;
}
return obj;
}
}
}
);
export default useAttrStore;
在属性栏中监听所对应的参数
属性栏(Attributes)组件:
@/views/D3/attributes/index.vue
<template>
<div class="right-side">
<circleForm id="circleForm" :style="{ display: circleVisiable ? 'block' : 'none' }" />
</div>
</template>
<script setup>
import circleForm from './circle.vue';
const props = defineProps({
visiable: { type: Object, default: () => ({ circleVisiable: true }) }
})
const { circleVisiable } = toRefs(props.visiable);
</script>
属性栏(Attributes)组件内所引用的圆属性的表单:
@/views/D3/attributes/circle.vue
<template>
<div class="container">
<header>圆</header>
<section>
<el-form :model="form" label-width="auto" style="max-width: 600px">
<el-form-item label="cx">
<el-input v-model="form.cx" id="cx" @change="change('cx')" />
</el-form-item>
<el-form-item label="cy">
<el-input v-model="form.cy" id="cy" @change="change('cy')" />
</el-form-item>
<el-form-item label="r">
<el-input v-model="form.r" id="r" @change="change('r')" />
</el-form-item>
<el-form-item label="fill">
<el-input v-model="form.fill" id="fill" @change="change('fill')" />
</el-form-item>
</el-form>
</section>
</div>
</template>
<script setup>
import useAttrStore from '@/store/modules/attr'
import { reactive } from 'vue';
const attrStore = useAttrStore();
let attrObject = reactive({});
let form = reactive(
{
x: 0,
y: 0,
r: 0,
fill: ''
}
);
// 订阅函数
attrStore.$subscribe((mutation, state) => {
if (state.attrType === 'circle') {
attrObject = attrStore.attrObject;
for (let key in attrObject) {
form[key] = attrObject[key];
}
}
})
function change(_key) {
attrStore.changeAttr(_key, form[_key]);
}
</script>
通过订阅的形式更新 input 内所对应的各项参数,且监听 input @change事件更新 pinia 中的 attrList 参数,并通过在绘制图元时所绑定的双击事件中的watch监听事件判定,若参数有变化,则修改图元所对应的属性。