Commit 067bede7 authored by 郭铭瑶's avatar 郭铭瑶 🤘

点击触发菜单

parent 9cb21f8a
This diff is collapsed.
......@@ -7,7 +7,7 @@
"build": "vite build"
},
"dependencies": {
"d3": "^5.15.0",
"d3": "^7.0.0",
"vue": "^3.0.5"
},
"devDependencies": {
......
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1628754321993" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2721" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M622.89 78.05A448.53 448.53 0 0 0 511.68 64C311.92 64 129.79 198.66 78.05 401.11 16.81 640.77 161.44 884.7 401.11 946a448.53 448.53 0 0 0 111.21 14C712.08 960 894.21 825.34 946 622.89c61.19-239.66-83.44-483.59-323.11-544.84z m261.05 529C840.46 777.18 687.65 896 512.32 896A385.66 385.66 0 0 1 417 883.94 383.9 383.9 0 0 1 140.06 417c43.47-170.17 196.3-289 371.65-289a385.54 385.54 0 0 1 95.34 12.06 383.9 383.9 0 0 1 276.89 467z" p-id="2722" fill="#ffffff"></path><path d="M705.6 479.9h-160v-160a32 32 0 0 0-64 0v160h-160a32 32 0 0 0 0 64h160v160a32 32 0 0 0 64 0v-160h160a32 32 0 0 0 0-64z" p-id="2723" data-spm-anchor-id="a313x.7781069.0.i0" fill="#ffffff"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1628754470052" class="icon" viewBox="0 0 1170 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6123" xmlns:xlink="http://www.w3.org/1999/xlink" width="146.25" height="128"><defs><style type="text/css"></style></defs><path d="M619.556571 546.139429v139.940571c78.555429 15.817143 137.691429 84.681143 137.691429 167.259429C757.248 947.565714 680.192 1024 585.142857 1024s-172.105143-76.416-172.105143-170.660571c0-82.578286 59.136-151.442286 137.691429-167.259429v-139.940571H240.932571c-19.017143 0-34.413714 15.268571-34.413714 34.121142v105.819429c78.555429 15.817143 137.691429 84.681143 137.691429 167.259429C344.210286 947.565714 267.154286 1024 172.105143 1024S0 947.584 0 853.339429c0-82.578286 59.117714-151.442286 137.691429-167.259429v-105.819429c0-56.539429 46.226286-102.4 103.241142-102.4h309.796572V337.92c-78.555429-15.817143-137.691429-84.681143-137.691429-167.259429C413.037714 76.434286 490.093714 0 585.142857 0s172.105143 76.416 172.105143 170.660571c0 82.578286-59.136 151.442286-137.691429 167.259429v139.940571h309.796572c57.014857 0 103.259429 45.860571 103.259428 102.4v105.819429c78.555429 15.817143 137.673143 84.681143 137.673143 167.259429C1170.285714 947.565714 1093.229714 1024 998.180571 1024s-172.105143-76.416-172.105142-170.660571c0-82.578286 59.136-151.442286 137.691428-167.259429v-105.819429c0-18.834286-15.414857-34.121143-34.413714-34.121142H619.556571zM585.142857 273.060571c57.033143 0 103.259429-45.842286 103.259429-102.4 0-56.539429-46.226286-102.4-103.259429-102.4s-103.259429 45.860571-103.259428 102.4c0 56.557714 46.226286 102.4 103.259428 102.4zM172.105143 955.739429c57.033143 0 103.259429-45.860571 103.259428-102.4 0-56.557714-46.226286-102.4-103.259428-102.4s-103.259429 45.842286-103.259429 102.4c0 56.539429 46.226286 102.4 103.259429 102.4z m413.037714 0c57.033143 0 103.259429-45.860571 103.259429-102.4 0-56.557714-46.226286-102.4-103.259429-102.4s-103.259429 45.842286-103.259428 102.4c0 56.539429 46.226286 102.4 103.259428 102.4z m413.037714 0c57.033143 0 103.259429-45.860571 103.259429-102.4 0-56.557714-46.226286-102.4-103.259429-102.4s-103.259429 45.842286-103.259428 102.4c0 56.539429 46.226286 102.4 103.259428 102.4z" p-id="6124" fill="#ffffff"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1628754405394" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3608" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M178.016 946.016q-0.992 0-3.008-0.992l0.992 0.512 2.016 0.512z m-8-4q-3.008-2.016-4.992-3.008 2.016 0.992 4.992 3.008z m780.48-738.528q-10.496-10.496-26.496-10.496h-184V134.976q0-28.992-20.992-49.504t-50.016-20.512h-314.016q-30.016 0-50.496 20.512t-20.512 49.504v58.016H98.976q-15.008 0-25.504 10.496t-10.496 24.992 10.496 24.992 25.504 10.496h31.008v623.008q0 28.992 20.992 49.504t50.016 20.512h632q28.992 0 50.016-20.512t20.992-49.504V263.968h20q15.008 0 26.016-10.496t11.008-24.992-10.496-24.992zM354.016 136h315.008v56.992h-315.008V136z m478.976 751.008H200.992V264h632v623.008zM511.488 384q-14.496 0-24.992 10.496t-10.496 25.504v334.016q0 15.008 10.496 25.504t24.992 10.496 24.992-10.496 10.496-25.504V420q0-15.008-10.496-25.504T511.488 384z m-194.496 0q-15.008 0-25.504 10.496t-10.496 25.504v334.016q0 15.008 10.496 25.504t25.504 10.496 25.504-10.496 10.496-25.504V420q0-15.008-10.496-25.504T316.992 384zM704 384q-15.008 0-25.504 10.496t-10.496 25.504v334.016q0 15.008 10.496 25.504t25.504 10.496 25.504-10.496 10.496-25.504V420q0-15.008-10.496-25.504T704 384z" p-id="3609" fill="#ffffff"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1628754440065" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4473" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><defs><style type="text/css"></style></defs><path d="M480 480H272c-114.4 0-208-93.6-208-208S157.6 64 272 64s208 93.6 208 208v208zM544 480h208c114.4 0 208-93.6 208-208S866.4 64 752 64s-208 93.6-208 208v208zM544 544h208c114.4 0 208 93.6 208 208s-93.6 208-208 208-208-93.6-208-208V544zM480 544H272c-114.4 0-208 93.6-208 208s93.6 208 208 208 208-93.6 208-208V544z" fill="#ffffff" p-id="4474"></path></svg>
\ No newline at end of file
......@@ -9,7 +9,9 @@
<script>
import D3 from './d3.vue'
import mockData from '@/util/mock.js'
import { ref } from '@vue/reactivity'
import { ref } from 'vue'
import RadialMenu from '@/util/menu.js'
export default {
name: 'Main',
components: { D3 },
......@@ -28,4 +30,17 @@ export default {
}
</script>
<style lang="stylus" scoped></style>
<style lang="stylus">
.menu-segment {
cursor: pointer;
fill: #2c3e50;
}
.menu-segment:hover {
fill: #58b2dc;
}
.menu-icon {
pointer-events: none;
}
</style>
import * as d3 from 'd3'
export default function RadialMenu() {
// Protect against missing new keyword
if (!(this instanceof RadialMenu)) {
return new RadialMenu()
}
//#region Local Variables
// The following variables have getter/setter functions exposed so are configurable
let data = [{}]
let padding = 1
let radius = 50
let thickness = 20
let iconSize = 16
let animationDuration = 250 // The duration to run animations for
let onClick = function (a) {
alert(a)
}
// Private Variables
let offsetAngleDeg = -180 / data.length // Initial rotation angle designed to put centre the first segment at the top
const control = {} // The control that will be augmented and returned
let pie // The pie layout
let arc // The arc generator
let segmentLayer // The layer that contains the segments
//#endregion
//#region Getter/Setter Accessors
/**
* The function to execute on a menu click
* @param {object} onClick - The function to execute on a menu click
* @returns {number} The function to execute on a menu click or the control
*/
control.onClick = function (_) {
if (!arguments.length) return onClick
onClick = _
return control
}
/**
* Time in ms to animate transitions
* @param {object} animationDuration - The time in ms to animate transitions
* @returns {number} The time in ms to animate transitions or the control
*/
control.animationDuration = function (_) {
if (!arguments.length) return animationDuration
animationDuration = _
return control
}
/**
* Padding between segments
* @param {object} padding - The padding between segments
* @returns {number} The padding between segments or the control
*/
control.padding = function (_) {
if (!arguments.length) return padding
padding = _
return control
}
/**
* Size of the icons within the segments
* @param {object} iconSize - Size of the icons within the segments
* @returns {number} The Size of the icons within the segments or the control
*/
control.iconSize = function (_) {
if (!arguments.length) return iconSize
iconSize = _
return control
}
/**
* Changes the inner radius of the menu
* @param {object} radius - The inner radius
* @returns {number} The inner radius or the control
*/
control.radius = function (_) {
if (!arguments.length) return radius
radius = _
arc.innerRadius(radius)
arc.outerRadius(radius + thickness)
return control
}
/**
* Changes the thickness of the menu
* @param {object} thickness - The thickness of the menu
* @returns {number} The thickness of the menu or the control
*/
control.thickness = function (_) {
if (!arguments.length) return thickness
thickness = _
arc.outerRadius(radius + thickness)
return control
}
//#endregion
//#region Private Functions
/**
* Calculates the mid point of an arc
* @param {object} d - The D3 data object that represents the arc
* @returns {object} A co-ordinate with an x, y location
*/
function calcMidPoint(d) {
const angle = d.startAngle + (d.endAngle - d.startAngle) / 2
const r = radius + thickness / 2
return {
x: r * Math.sin(angle),
y: -r * Math.cos(angle),
}
}
/**
* Initializes the control
*/
function init() {
// Create pie layout
pie = d3
.pie()
.value(function (d) {
return data.length
})
.padAngle((padding * Math.PI) / 180)
// Create the arc function
arc = d3
.arc()
.innerRadius(radius)
.outerRadius(radius + thickness)
}
/**
* Appends the control to the DOM underneath the given target
* @param {selector} target - Either a D3 object or a string selector to insert the menu into. Must be an SVG element, or child of an SVG element
* @returns {object} The control
*/
control.appendTo = function (target) {
// Convert the target into a valid D3 selection
// that we can append our menu into
target = d3.select(target)
// Create the visualiziation
segmentLayer = target
.append('g')
.attr('transform', 'rotate(' + offsetAngleDeg + ')')
return control
}
/**
* Display the menu
* @returns {object} The control
*/
control.show = function (_) {
// Calculate the new offset angle based on the number of data items and
// then rotate the menu to re-centre the first segment
data = _
offsetAngleDeg = -180 / data.length
segmentLayer.attr('transform', 'rotate(' + offsetAngleDeg + ')')
// Join the data to the elements
const dataJoin = segmentLayer
.selectAll('.menu-segment-container')
.data(pie(data))
// Updates first
// Update the segments first to make space for any new ones
dataJoin
.select('.menu-segment')
.transition()
.duration(animationDuration)
.attrTween('d', function (a) {
// interpolate the objects - which is going to allow updating
// the angles of the segments within the arc function
const i = d3.interpolate(this._current, a)
this._current = i(0)
return function (t) {
return arc(i(t))
}
})
// Update the location of the icons
dataJoin
.select('.menu-icon')
.transition()
.attr('x', function (d) {
return calcMidPoint(d).x - iconSize / 2
})
.attr('y', function (d) {
return calcMidPoint(d).y - iconSize / 2
})
.attr('transform', function (d) {
const mp = calcMidPoint(d)
const angle = -offsetAngleDeg
return 'rotate(' + angle + ',' + mp.x + ',' + mp.y + ')'
})
// Enter new actors
// Enter the groups
const menuSegments = dataJoin
.enter()
.append('g')
.attr('class', 'menu-segment-container')
// Add the segments
menuSegments
.append('path')
.attr('class', 'menu-segment')
.each(function (d) {
this._current = d
}) // store the initial data value for later
.on('click', function (e, d) {
console.log(d.data.action)
})
.transition()
.duration(animationDuration)
.attrTween('d', function (a) {
// Create interpolations from the 0 to radius - to give the impression of an expanding menu
const innerTween = d3.interpolate(0, radius)
const outerTween = d3.interpolate(0, arc.outerRadius()())
return function (t) {
// Re-configure the radius of the arc
return arc.innerRadius(innerTween(t)).outerRadius(outerTween(t))(a)
}
})
// Add the icons
menuSegments
.append('image')
.attr('class', 'menu-icon')
.attr('xlink:href', function (d) {
return d.data.icon
})
.attr('width', iconSize)
.attr('height', iconSize)
.attr('x', function (d) {
return calcMidPoint(d).x - iconSize / 2
})
.attr('y', function (d) {
return calcMidPoint(d).y - iconSize / 2
})
// .attr('transform', function (d) {
// // We need to rotate the images backwards to compensate for the rotation of the menu as a whole
// const mp = calcMidPoint(d)
// const angle = -offsetAngleDeg
// return 'rotate(' + angle + ',' + mp.x + ',' + mp.y + ')'
// })
.style('opacity', 0)
.transition()
.delay(animationDuration)
.style('opacity', 1) // Fade in the icons
// Remove old groups
dataJoin.exit().remove()
return control
}
/**
* Hide the menu
* @returns {object} The control
*/
control.hide = function () {
// Join the data with an empty array so that we'll exit all actors
const dataJoin = segmentLayer
.selectAll('.menu-segment-container')
.data(pie([]))
.exit()
// Select all the icons and fade them out
dataJoin
.select('.menu-icon')
.style('opacity', 1)
.transition()
.delay(animationDuration)
.style('opacity', 0)
// Select all the segments and animate them back into the centre
dataJoin
.select('path')
.transition()
.delay(animationDuration) // wait for the icons to fade
.duration(animationDuration)
.attrTween('d', function (a) {
// Create interpolations from the radius to 0 - to give the impression of a shrinking menu
const innerTween = d3.interpolate(radius, 0)
const outerTween = d3.interpolate(arc.outerRadius()(), 0)
return function (t) {
// Re-configure the radius of the arc
return arc.innerRadius(innerTween(t)).outerRadius(outerTween(t))(a)
}
})
// .each('end', function (d) {
// dataJoin.remove() // Remove all of the segment groups once the transition has completed
// })
dataJoin.remove()
segmentLayer.remove()
return control
}
// Initialize and then return the control
init()
return control
}
import * as d3 from 'd3'
import RadialMenu from './menu'
import branch from '@/assets/images/branch.svg'
import more from '@/assets/images/more.svg'
import add from '@/assets/images/add.svg'
import del from '@/assets/images/delete.svg'
// 求两点间的距离
function getDis(s, t) {
return Math.sqrt((s.x - t.x) * (s.x - t.x) + (s.y - t.y) * (s.y - t.y))
......@@ -84,6 +90,14 @@ const defaultConfig = {
strokeWidth: 0, // 圈圈外围包裹的宽度
}
const menuData = [
{ icon: branch, action: 'branch' },
{ icon: more, action: 'more' },
{ icon: add, action: 'add' },
{ icon: del, action: 'del' },
]
let menu = null
export default class RelationGraph {
constructor(selector, data, configs = {}) {
const mapW = selector.offsetWidth
......@@ -155,20 +169,20 @@ export default class RelationGraph {
// 2.创建svg标签
this.SVG = this.graph
.append('svg')
.attr('class', 'svgclass')
.attr('id', 'graph-svg-container')
.attr('width', this.config.width)
.attr('height', this.config.height)
.call(
d3
.zoom()
.scaleExtent(this.config.scaleExtent)
.on('zoom', () => {
.on('zoom', (e) => {
if (this.config.isScale) {
this.relMap_g.attr('transform', d3.event.transform)
this.relMap_g.attr('transform', e.transform)
}
})
)
.on('click', () => console.log('画布 click'))
.on('click', () => menu && menu.hide())
.on('dblclick.zoom', null)
// 3.defs <defs>标签的内容不会显示,只有调用的时候才显示
......@@ -247,7 +261,7 @@ export default class RelationGraph {
.enter()
.append('g')
.attr('class', 'edge')
.on('mouseover', function (d) {
.on('mouseover', function (e, d) {
if (self.config.isHighLight) {
self.highlightLinks(d)
}
......@@ -261,7 +275,7 @@ export default class RelationGraph {
d3.select(this).selectAll('path.links').attr('stroke-width', 1)
d3.select(this).selectAll('.rect_g text').style('font-weight', 'normal')
})
.on('click', function (d) {
.on('click', function (e, d) {
console.log('线click')
})
.attr('fill', function (d) {
......@@ -322,14 +336,23 @@ export default class RelationGraph {
.append('div')
.attr('class', 'relation-tooltip')
.style('opacity', 0)
// 6.关系图添加用于显示圈圈的节点
this.circles = this.relMap_g
.selectAll('circle.circleclass')
this.circlesWrapper = this.relMap_g
.selectAll('g.circle')
.data(this.config.nodes)
.enter()
// .append('rect')
// .attr('width', this.config.r * 2)
// .attr('height', this.config.r * 2)
.append('g')
.attr('class', 'circle')
// 6.关系图添加用于显示圈圈的节点
// this.circles = this.relMap_g
// .selectAll('circle.circleclass')
// .data(this.config.nodes)
// .enter()
// // .append('rect')
// // .attr('width', this.config.r * 2)
// // .attr('height', this.config.r * 2)
this.circles = this.circlesWrapper
.append('circle')
.attr('r', this.config.r)
.attr('class', 'circleclass')
......@@ -345,7 +368,7 @@ export default class RelationGraph {
})
.attr('stroke', this.config.strokeColor)
.attr('stroke-width', this.config.strokeWidth)
.on('mouseover', function (d) {
.on('mouseover', function (e, d) {
d3.select(this).attr(
'stroke-width',
self.config.strokeWidth == 0 ? 5 : 1.5 * self.config.strokeWidth
......@@ -356,11 +379,11 @@ export default class RelationGraph {
}
tooltip
.html(d.name)
.style('left', d3.event.pageX + 10 + 'px')
.style('top', d3.event.pageY + 10 + 'px')
.style('left', e.pageX + 10 + 'px')
.style('top', e.pageY + 10 + 'px')
.style('opacity', 1)
})
.on('mouseout', function (d) {
.on('mouseout', function (e, d) {
d3.select(this).attr('stroke-width', self.config.strokeWidth)
d3.select(this).attr('stroke', d.strokeColor || self.config.strokeColor)
if (self.config.isHighLight) {
......@@ -368,47 +391,59 @@ export default class RelationGraph {
}
tooltip.style('opacity', 0)
})
// .on('click', function (d) {
// console.log('圈圈节点click', d)
// // 展示方式2 :浮窗展示
// event = d3.event || window.event
// var pageX = event.pageX ? event.pageX : (event.clientX + (document.body.scrollLeft || document.documentElement.scrollLeft))
// var pageY = event.pageY ? event.pageY : (event.clientY + (document.body.scrollTop || document.documentElement.scrollTop))
// // console.log('pagex', pageX);
// // console.log('pageY', pageY);
// //阻止事件冒泡 阻止事件默认行为
// event.stopPropagation ? (event.stopPropagation()) : (event.cancelBubble = true)
// event.preventDefault ? (event.preventDefault()) : (event.returnValue = false)
// })
// .on('contextmenu', function () { //鼠标右键菜单
// event = event || window.event
// event.cancelBubble = true
// event.returnValue = false
// })
.on('click', function (e, d) {
menu && menu.hide()
menu = new RadialMenu()
.radius(50)
.thickness(40)
.appendTo(this.parentNode)
.show(menuData)
// console.log('圈圈节点click', d)
// // 展示方式2 :浮窗展示
// const pageX = e.pageX
// ? e.pageX
// : e.clientX +
// (document.body.scrollLeft || document.documentElement.scrollLeft)
// const pageY = e.pageY
// ? e.pageY
// : e.clientY +
// (document.body.scrollTop || document.documentElement.scrollTop)
// // console.log('pagex', pageX);
// // console.log('pageY', pageY);
//阻止事件冒泡 阻止事件默认行为
e.stopPropagation ? e.stopPropagation() : (e.cancelBubble = true)
e.preventDefault ? e.preventDefault() : (e.returnValue = false)
})
.on('contextmenu', function (e) {
//鼠标右键菜单
// 取消默认行为
e.cancelBubble = true
e.returnValue = false
})
// 应用 自定义的 拖拽事件
.call(
d3
.drag()
.on('start', (d) => {
d3.event.sourceEvent.stopPropagation()
.on('start', (e, d) => {
e.sourceEvent.stopPropagation()
// restart()方法重新启动模拟器的内部计时器并返回模拟器。
// 与simulation.alphaTarget或simulation.alpha一起使用时,此方法可用于在交互
// 过程中进行“重新加热”模拟,例如在拖动节点时,在simulation.stop暂停之后恢复模拟。
// 当前alpha值为0,需设置alphaTarget让节点动起来
if (!d3.event.active) this.simulation.alphaTarget(0.3).restart()
if (!e.active) this.simulation.alphaTarget(0.3).restart()
d.fx = d.x
d.fy = d.y
})
.on('drag', (d) => {
.on('drag', (e, d) => {
// d.fx属性- 节点的固定x位置
// 在每次tick结束时,d.x被重置为d.fx ,并将节点 d.vx设置为零
// 要取消节点,请将节点 .fx和节点 .fy设置为空,或删除这些属性。
d.fx = d3.event.x
d.fy = d3.event.y
d.fx = e.x
d.fy = e.y
})
.on('end', (d) => {
.on('end', (e, d) => {
// 让alpha目标值值恢复为默认值0,停止力模型
if (!d3.event.active) this.simulation.alphaTarget(0)
if (!e.active) this.simulation.alphaTarget(0)
d.fx = null
d.fy = null
})
......@@ -473,6 +508,18 @@ export default class RelationGraph {
.attr('cy', function (d) {
return d.y
})
// 让menu随圆圈移动
this.circles.each(function () {
const menuEle = d3.select(this.nextSibling)
if (!menuEle.empty()) {
const self = d3.select(this)
const cx = self.attr('cx')
const cy = self.attr('cy')
menuEle.attr('transform', `translate(${cx},${cy})`)
}
})
// .attr('x', function (d) {
// return d.x - d3.select(this).attr('width') / 2
// })
......@@ -564,9 +611,3 @@ export default class RelationGraph {
}
}
}
; (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? (module.exports = factory)
: ((global = global || self), (global.RelationGraph = factory))
})(this, RelationGraph)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment