赞
踩
- // 生成力
- const force = d3
- .forceSimulation()
- .force('link',d3.forceLink().id((d) => d.id),)
- .force('collide', d3.forceCollide(72).strength(0.1))
- .force('charge',d3.forceManyBody().strength(-400),)
- .force('center', d3.forceCenter());
处理一下线的数据, 两个点可能出现多条线的情况
- export const setLinkNumber = (group, type) => {
- if (group.length === 0) return;
- const linksA = [];
- const linksB = [];
- for (let i = 0; i < group.length; i++) {
- const link = group[i];
- // 对该分组内的关系按照方向进行分类,此处根据连接的实体ASCII值大小分成两部分
- if (link.source.id < link.target.id) {
- linksA.push(link);
- } else {
- linksB.push(link);
- }
- }
- // 确定关系最大编号。为了使得连接两个实体的关系曲线呈现对称,根据关系数量奇偶性进行平分。
- // 特殊情况:当关系都是连接到同一个实体时,不平分
- let maxLinkNumber = 0;
- if (type === 'self') {
- maxLinkNumber = group.length;
- } else {
- maxLinkNumber = group.length % 2 === 0 ? group.length / 2 : (group.length + 1) / 2;
- }
- // 如果两个方向的关系数量一样多,直接分别设置编号即可
- if (linksA.length === linksB.length) {
- let startLinkNumber = 1;
- for (let i = 0; i < linksA.length; i++) {
- linksA[i].linknum = startLinkNumber++;
- }
- startLinkNumber = 1;
- for (let i = 0; i < linksB.length; i++) {
- linksB[i].linknum = startLinkNumber++;
- }
- } else {
- // 当两个方向的关系数量不对等时,先对数量少的那组关系从最大编号值进行逆序编号,然后在对另一组数量多的关系从编号1一直编号到最大编号,再对剩余关系进行负编号
- // 如果抛开负号,可以发现,最终所有关系的编号序列一定是对称的(对称是为了保证后续绘图时曲线的弯曲程度也是对称的)
- let biggerLinks;
- let smallerLinks;
- if (linksA.length > linksB.length) {
- biggerLinks = linksA;
- smallerLinks = linksB;
- } else {
- biggerLinks = linksB;
- smallerLinks = linksA;
- }
- let startLinkNumber = maxLinkNumber;
- for (let i = 0; i < smallerLinks.length; i++) {
- smallerLinks[i].linknum = startLinkNumber--;
- }
- const tmpNumber = startLinkNumber;
- startLinkNumber = 1;
- let p = 0;
- while (startLinkNumber <= maxLinkNumber) {
- biggerLinks[p++].linknum = startLinkNumber++;
- }
- // 开始负编号
- startLinkNumber = 0 - tmpNumber;
- for (let i = p; i < biggerLinks.length; i++) {
- biggerLinks[i].linknum = startLinkNumber++;
- }
- }
- };
-
- function getKey(target, source) {
- const result = target > source ? `${target}:${source}` : `${source}:${target}`;
- return result;
- }
-
- export const operationData = (chartData, clickType) => {
- const linkmap = {};
- const linkGroup = {};
- const { links, dots } = chartData;
- for (let i = 0; i < links.length; i++) {
- const link = links[i];
- const { target, source } = link;
- const key = getKey(target, source);
- if (linkGroup[key]) {
- linkGroup[key].push(link);
- linkmap[key] += 1;
- } else {
- linkGroup[key] = [links[i]];
- }
- }
- Object.keys(linkGroup).forEach((groupKey) => {
- linkmap[groupKey] = linkGroup[groupKey].length;
- });
- // 关联线与点
- JSON.parse(JSON.stringify(links)).forEach((e) => {
- const sourceNode = dots.filter((n) => n.id === e.source)[0];
- const targetNode = dots.filter((n) => n.id === e.target)[0];
- const nowIndex = links.findIndex(
- (n) => n.source === e.source && n.target === e.target,
- );
- if (!sourceNode || !targetNode) {
- links.splice(nowIndex, 1);
- } else {
- links[nowIndex].source = sourceNode;
- links[nowIndex].target = targetNode;
- }
- });
- for (let i = 0; i < links.length; i++) {
- let { target, source } = links[i];
- target = target.id;
- source = source.id;
- const link = links[i];
- const key = getKey(target, source);
- link.size = linkmap[key];
- const group = linkGroup[key];
- const type = 'noself'; // 标示该组关系是指向两个不同实体还是同一个实体
- setLinkNumber(group, type);
- }
- return { links, dots };
- };
处理好的数据丢到d3里面去
- // tick 渲染时执行的方法
- force.nodes(dots).alpha(0.01).on('tick', this.tick).restart();
- force.force('link').links(links).distance(150);
经d3处理后,会出来有 x,y 的数据。如:
数据处理完后,接下来创建dom。
线: 线是一个g标签包含着N条线(a)
线内包含着2条线以及线相关的箭头
两条线的目的是因为1条线很细的情况下会很不好hover 到。所以1条粗线 一条细线 ,直接把透明度(opacity) 属性 设置为0 即可。效果如下:
所有箭头状态的属性,因为箭头和线不是"一体"的,所以当hover 的时候,圆点(dot) 的大小会发生变化,箭头的refX,refY,也会发生变化,甚至 箭头的大小变化 refX 和refY也得改变。
- export const styleSize = {
- normal: {
- refX: 30,
- markerHeight: 8,
- markerWidth: 8,
- },
- hover: {
- refX: 28,
- markerHeight: 10,
- markerWidth: 10,
- },
- click: {
- refX: 19,
- markerHeight: 17.5,
- markerWidth: 17.5,
- },
- dotnormal: {
- refX: 35,
- markerHeight: 8,
- markerWidth: 8,
- },
- dothover: {
- refX: 43,
- markerHeight: 10,
- markerWidth: 10,
- },
- dotclick: {
- refX: 48,
- markerHeight: 10,
- markerWidth: 10,
- },
-
- dotlineclick: {
- refX: 28,
- markerHeight: 25,
- markerWidth: 20,
- },
- };
-
- // isThumb 是否是缩略图
- export const drawLine = (svg, type, links) => {
- const isThumb = type === 'thumb';
- const warp = isThumb ? svg.insert('g', '.dragThumb') : svg.append('g');
- const lineWarp = warp
- .attr('class', `${isThumb ? 'thumbG' : 'forceLines forceMainG'}`)
- .selectAll('g')
- .data(links)
- .enter()
- .append('a')
- const { refX, markerWidth, markerHeight } = styleSize.normal;
- const markerId = (d) => `marker-${(d.id)}`;
- lineWarp
- .append('marker')
- .attr('id', markerId)
- .attr('markerUnits', 'userSpaceOnUse')
- .attr('viewBox', '0 -5 10 10') // 坐标系的区域
- .attr('refX', refX) // 箭头坐标
- .attr('markerWidth', markerWidth) // 标识的大小
- .attr('markerHeight', markerHeight)
- .attr('orient', 'auto') // 绘制方向,可设定为:auto(自动确认方向)和 角度值
- .attr('stroke-width', 2) // 箭头宽度
- .append('path')
- .attr('d', 'M0,-5L10,0L0,5') // 箭头的路径
- .attr('fill', 'inherit'); //箭头的颜色, 设置箭头的颜色 不可以直接找到箭头然后更改fill 因为真正有颜色的是 箭头里面的dom
-
- // 展示的线
- const line = lineWarp.append('path')
- //实线虚线自己控制
- .attr('stroke-dasharray', (d) => ('虚线' ? '8,5' : ''))
- .attr('marker-end', (d) => {
- if (isThumb) return '';
- return `url(#${(markerId(d))})`;
- });
- const bkLine = lineWarp.append('path')
- // 实际hover 以及 点击的线
- .attr('stroke-width', 10)
- .attr('stroke', 'red')
- .attr('fill', 'none')
- .attr('opacity', '0')
- return {
- lineWarp,
- line,
- bkLine,
- };
- };
- export const drawCircle = (svg, nodes, type) => {
- const dotWarp = svg
- .append('g')
- .attr('class', 'forceNodes forceMainG')
- .selectAll('g')
- .data(nodes)
- .enter()
- .append('a')
- .attr('xlink:href', 'js:void(0)');
- const circle = dotWarp.append('circle')
- .attr('class', 'forceNode regionNode');
- return {
- circle,
- dotWarp
- };
- };
在之前的tick 中去改变 path 的d属性 以及 点的 位置
- this.paths.attr('d', function (data) {
- return pathD(data, this);
- });
- this.bkLine.attr('d', function (data) {
- return pathD(data, this);
- });
- this.dotWarp.attr('transform', setTransform); // 圆圈
- export const pathD = (d, dom) => {
- const { x: sx, y: sy } = d.source;
- const { x: tx, y: ty } = d.target;
- let dr;
- // 如果连接线连接的是同一个实体,则对path属性进行调整,绘制的圆弧属于长圆弧,同时对终点坐标进行微调,避免因坐标一致导致弧无法绘制
- if (d.target === d.source) {
- dr = 30 / d.linknum;
- return `M ${sx}, ${sy}A${dr},${dr} 0 1,1 ${tx},${ty + 1}`;
- }
- if (d.size % 2 !== 0 && d.linknum === 1) {
- // 如果两个节点之间的连接线数量为奇数条,则设置编号为1的连接线为直线,其他连接线会均分在两边
- return `M ${sx},${sy},L ${tx},${ty}`;
- }
- // 根据连接线编号值来动态确定该条椭圆弧线的长半轴和短半轴,当两者一致时绘制的是圆弧
- // 注意A属性后面的参数,前两个为长半轴和短半轴,第三个默认为0,
- // 第四个表示弧度大于180度则为1,小于则为0,这在绘制连接到相同节点的连接线时用到;
- // 第五个参数,0表示正角,1表示负角,即用来控制弧形凹凸的方向。本文正是结合编号的正负情况来控制该条连接线的凹凸方向,从而达到连接线对称的效果
- const curve = 1.5;
- const homogeneous = 2;
- const dx = d.target.x - d.source.x;
- const dy = d.target.y - d.source.y;
- dr = (Math.sqrt(dx * dx + dy * dy) * (d.linknum + homogeneous)) / (curve * homogeneous);
- // 当节点编号为负数时,对弧形进行反向凹凸,达到对称效果
- if (d.linknum < 0) {
- if (dom) {
- d3.select(dom.previousElementSibling).attr('refY', 4).attr('oldRefY', 4);
- }
- dr = (Math.sqrt(dx * dx + dy * dy) * (-1 * d.linknum + homogeneous)) / (curve * homogeneous);
- return `M${sx},${sy}A${dr},${dr} 0 0,0 ${tx},${ty}`;
- }
- if (dom) {
- d3.select(dom.previousElementSibling).attr('refY', -4).attr('oldRefY', -4);
- }
- return `M${sx},${sy}A${dr},${dr} 0 0,1 ${tx},${ty}`;
- };
- export const setTransform = (node) => {
- const { x, y, k } = node;
- let result = '';
- if (x && y) result += `translate(${x},${y})`;
- if (k)result += ` scale(${k})`;
- return result;
- };
固定节点的方法就是 设置fx fy=null
- // 创建完dotWarp的时候 可以直接绑定
- dotWarp.call(
- d3.drag()
- .on('start', this.started)
- .on('drag', this.dragged)
- .on('end', this.ended),
- );
- started(d) {
- const { force } = this;
- if (!d3.event.active) {
- force.alphaTarget(0.2).restart(); // 设置衰减系数,对节点位置移动过程的模拟,数值越高移动越快,数值范围[0,1]
- }
- d.fx = d.x;
- d.fy = d.y;
- },
- dragged(d) {
- const { x, y } = d3.event;
- if (this.inBoundaries(x, y).isIn) {
- d.fx = x;
- d.fy = y;
- }
- d.fx = d3.event.x;
- d.fy = d3.event.y;
- },
- ended(d) {
- const { force } = this;
- if (!d3.event.active) {
- force.alphaTarget(0);
- }
- d.fx = null;
- d.fy = null;
- },
- /**
- * @params
- * zoomMin 最小缩小倍数
- * zoomMax 最大放大倍数
- */
- const zoom = d3
- .zoom()
- .scaleExtent([zoomMin, zoomMax])
- .on('zoom', () => {
- const transInfo = d3.event.transform; //绘制框选的时候需要用到
- mainSvg.selectAll('g').attr('transform', transInfo);
- this.transInfo = transInfo;
- this.$emit('zoom', transInfo); // 告诉外层 发生了拖拽
- });
- mainSvg.call(zoom).on('dblclick.zoom', null); // 注销双击缩放
放大/缩小
- //svg.transition().duration(750).call(zoom.scaleBy,放大的倍数);
- // 缩小 0.9倍直到 缩小到最小倍数
- svg.transition().duration(750).call(zoom.scaleBy, 0.9);
- // 放大 1.1倍直到 放大到最大倍数
- svg.transition().duration(750).call(zoom.scaleBy, 1.1);
缩略图目前的逻辑是主图的最大倍数作为背景,主图的宽高作为缩略图视野(蓝框)的宽高。因为缩略图的dom 的宽高是css 定死的,所以给定主图(正常)的宽高 会自动缩放。
- /**
- * @params
- * width 缩略图宽度
- * height 缩略图高度
- * mainWidth 主图的宽度
- * mainHeight 主图的高度
- * zoomMax 最大缩放比例
- */
- thumbSvg = d3
- .select('#thumbWarp')
- .append('svg');
- dragThumb = thumbSvg.append('rect')
- .attr('class', 'dragThumb')
- .attr('fill', 'none');
- let w; let h; let x = 0; let y = 0;
- thumbSvg.attr('width', width)
- .attr('height', height)
- .attr('id', 'thumbSvg')
- .attr('viewBox', () => {
- // 缩略图的宽高为 主图的 最大缩略比例
- w = mainWidth * zoomMax;
- h = mainHeight * zoomMax;
- // 设置偏移 让背景图移至中心,缩略图与主图的差/ 2 就是需要移动的距离
- x = -(w - mainWidth) / 2;
- y = -(h - mainHeight) / 2;
- return `${x} ${y} ${w} ${h}`;
- });
- dragThumb.attr('width', mainWidth)
- .attr('height', mainHeight);
主图的拖拽与缩放 在调用上面的缩放的时候会调用zoom 的on zoom 方法,并将缩放以及拖拽的距离传给 缩略图,因为缩放会造成 主图的 translate 发生变化 与手动拖拽造成的translate 会有差 所以 要扣除缩放造成的偏移
- if (!mainTransform.x && !mainTransform.y && mainTransform.k === 1) {
- this.initSvg();
- return;
- }
- const {
- innerZoomInfo, mainWidth, mainHeight,
- } = this;
- // 如果传入的 缩放值与之前记录的缩放值不一致 则认为发生了缩放 记录发生缩放后偏移值
- if (!innerZoomInfo || innerZoomInfo.k !== mainTransform.k) {
- this.moveDiff = {
- x: (mainWidth - innerZoomInfo.k * mainWidth) / 2,
- y: (mainHeight - innerZoomInfo.k * mainHeight) / 2,
- };
- }
- const { x: diffX, y: diffY } = this.moveDiff;
- const { x, y, k } = mainTransform; // 主图偏移以及缩放数据
- this.dragThumb
- .attr('width', mainWidth / k)
- .attr('height', mainHeight / k)
- .attr('transform', () => setTransform({
- x: -((x - diffX) / k), // 这个地方应该不能直接 除 k 这里的x,y 应该是放大后的x,y应该减去缩放的差值 再 除K
- y: -((y - diffY) / k),
- }));
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。