เหตุการณ์ D3 Click ไม่ทำงานหลังจากต่อท้ายเส้นทางอีกครั้ง
ฉันพยายามสร้างโลกที่ลากได้นี้ขึ้นมาใหม่จาก Mike Bostock โดยใช้ D3 แต่เป็นเวอร์ชัน svg เนื่องจากปัญหาด้านประสิทธิภาพขณะลากฉันกำลังเรนเดอร์โลก จนถึงตอนนี้ดีมาก ตอนนี้ฉันต้องการใช้เหตุการณ์การคลิก แต่ไม่ได้ผล นี่มันบอกว่าอาจจะมีปัญหาอีกครั้งท้าย เหตุการณ์ที่เลื่อนลงทำงานได้ดี แต่จะรบกวนการลากในภายหลัง เหตุใดเหตุการณ์ mousedown จึงทำงานและเหตุการณ์คลิกไม่ได้ คำแนะนำในการปรับโครงสร้างโค้ดเพื่อแก้ปัญหานี้ได้รับการชื่นชมอย่างมาก
ฉันสร้าง Fiddle เพื่อความเข้าใจที่ดีขึ้น: Fiddle
ปล. ฉันยังใหม่กับการเขียนโปรแกรมและ D3 ดังนั้นโปรดอย่ารุนแรงเกินไป :)
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3</title>
</head>
<body>
<div id="world"></div>
<script src="https://d3js.org/d3.v6.min.js"></script>
<script src="https://unpkg.com/topojson-client@2"></script>
</body>
</html>
JS:
let width, height
height = 150
width = 150
const projection = d3.geoOrthographic()
.scale((height - 10) / 2)
.translate([100, height / 2])
.precision(0);
let path = d3.geoPath().projection(projection)
const svg = d3.select("#world")
.append("svg")
const g = svg.append("g")
d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json").then(data => {
let data1 = data
renderGlobe(data1);
})
function renderGlobe(world){
g.call(drag(projection)
.on("drag.render", ()=>render(world, true))
.on("end.render", ()=>render(world, false) ))
.call( () => render(world, false))
}
function render(world, x){
if(x){
variable = "land"
world = topojson.feature(world, world.objects.land).features;
}
else{
variable = "countries"
world = topojson.feature(world, world.objects.countries).features;
}
g.selectAll("path").remove()
g.selectAll(`${variable}`) .data(world) .enter().append("path") .attr("class", `${variable}`)
.attr("d", path)
// This click event doesn't work
.on("click",()=>console.log("Do something"))
// But mousedown event works
.on("mousedown",()=>console.log("Mousedown event works"))
}
function drag(projection){
var LonLatStart, eulerStart
function dragstarted(event){
LonLatStart = projection.invert(d3.pointer(event))
eulerStart = projection.rotate()
}
var LonLatEnd, eulerEnd
function dragged(event){
LonLatEnd = projection.rotate(eulerStart).invert(d3.pointer(event))
eulerEnd = getEulerAngles(LonLatStart, eulerStart, LonLatEnd)
projection.rotate(eulerEnd)
refresh()
}
return drag = d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
}
function refresh(){
svg.selectAll("path").attr("d", path)
}
// Dragging Math
let cos = Math.cos,
acos = Math.acos,
sin = Math.sin,
asin = Math.asin,
atan2 = Math.atan2,
sqrt = Math.sqrt,
min = Math.min,
max = Math.max,
PI = Math.PI,
radians = PI / 180,
degrees = 180 / PI;
// a: original vector, b: ending vector
function crossProduct(a, b){
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0]
]
}
function dotProduct(a, b){
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
function LengthOfVector(c){
return sqrt(c[0] * c[0] + c[1] * c[1] + c[2] * c[2])
}
function quaternionEulerFormula(a, b){
let rotationAxis = crossProduct(a,b) , normalizationFactor = sqrt(dotProduct(rotationAxis,rotationAxis))
if (!normalizationFactor) return [1, 0, 0, 0]
let theta = acos(max(-1, min(1, dotProduct(a, b))))
return [
cos(theta / 2),
sin(theta / 2) * rotationAxis[2] / normalizationFactor,
- sin(theta / 2) * rotationAxis[1] / normalizationFactor,
sin(theta / 2) * rotationAxis[0] / normalizationFactor
]
}
// returns unit quaternion from euler angles [λ, φ, γ]
function unitQuaternion(d){
var lambda = d[0] / 2 * radians, cosLambda = cos(lambda), sinLambda = sin(lambda),
phi = d[1] / 2 * radians, cosPhi = cos(phi), sinPhi = sin(phi),
gamma = d[2] / 2 * radians, cosGamma = cos(gamma), sinGamma = sin(gamma)
return [
cosLambda * cosPhi * cosGamma + sinLambda * sinPhi * sinGamma,
sinLambda * cosPhi * cosGamma - cosLambda * sinPhi * sinGamma,
cosLambda * sinPhi * cosGamma + sinLambda * cosPhi * sinGamma,
cosLambda * cosPhi * sinGamma - sinLambda * sinPhi * cosGamma,
]
}
// quaternion multiplication, returns another quaternion which represents the rotation
function quaternionMultiplication(q0 , q1){
return [
q0[0] * q1[0] - q0[1] * q1[1] - q0[2] * q1[2] - q0[3] * q1[3],
q0[0] * q1[1] + q0[1] * q1[0] + q0[2] * q1[3] - q0[3] * q1[2],
q0[0] * q1[2] - q0[1] * q1[3] + q0[2] * q1[0] + q0[3] * q1[1],
q0[0] * q1[3] + q0[1] * q1[2] - q0[2] * q1[1] + q0[3] * q1[0]
]
}
// converts quaternion to euler angles
function quaternion2eulerAngles(q){
return [
atan2(2 * (q[0] * q[1] + q[2] * q[3]), 1 - 2 * (q[1] * q[1] + q[2] * q[2])) * degrees,
//asin(2 * (q[0] * q[2] - q[3] * q[1])) * degrees,
asin(max(-1, min(1, 2 * (q[0] * q[2] - q[3] * q[1])))) * degrees,
atan2(2 * (q[0] * q[3] + q[1] * q[2]), 1 - 2 * (q[2] * q[2] + q[3] * q[3])) * degrees
]
}
// converts long, lat to cartesian coordinates x,y,z
function lonlat2cartesian(e) {
let l = e[0] * radians, p = e[1] * radians, cp = cos(p);
return [cp * cos(l), cp * sin(l), sin(p)];
};
function getEulerAngles(positionLonLatStart, eulerAnglesStart, positionLonLatEnd){
let v0 = lonlat2cartesian(positionLonLatStart)
let v1 = lonlat2cartesian(positionLonLatEnd)
let quaternionEnd = quaternionMultiplication(unitQuaternion(eulerAnglesStart), quaternionEulerFormula(v0,v1))
return quaternion2eulerAngles(quaternionEnd)
}
คำตอบ
คุณจะไม่ทำให้เกิดการคลิกอย่างที่เป็นอยู่ในขณะนี้ซึ่งคุณเคยเห็น เนื่องจากการคลิกเกี่ยวข้องกับทั้งเมาส์ลงและเลื่อนเมาส์ขึ้น สิ่งเหล่านี้โต้ตอบกับพฤติกรรมการลากโดยแต่ละเหตุการณ์จะทริกเกอร์เหตุการณ์เริ่มต้นและสิ้นสุดตามลำดับ
สิ่งที่เกิดขึ้นในกรณีของคุณคือมูสดาวน์ทริกเกอร์การลากและตัวฟังมูสดาวน์ที่คุณเพิ่มลงในพา ธ จากนั้นเมื่อเลื่อนเมาส์ขึ้นตัวฟังเหตุการณ์การลากจะเริ่มทำงานก่อนโดยลบเส้นทางและตัวฟังที่เกี่ยวข้องออก ฟังก์ชั่นการแสดงผลจะเพิ่มเส้นทางใหม่หลังจากข้อเท็จจริงสายเกินไปที่จะลงทะเบียนเหตุการณ์
มีวิธีแก้ปัญหาหลายวิธี แต่วิธีที่ง่ายที่สุดคือการลบตัวฟังปลายลากที่คุณมีและแทนที่เมื่อเกิดการลากจริงเท่านั้น (ไม่ใช่ในเหตุการณ์เริ่มต้นในเหตุการณ์ลาก):
function renderGlobe(world){
g.call(drag(projection)
.on("drag.render", function(event) {
render(world, true)
event.on("end.render",()=>render(world,false))
}))
.call( () => render(world, false))
}
event.on()
วิธีการช่วยให้ผู้ฟังจะนำมาใช้เฉพาะสำหรับท่าทางปัจจุบัน ฟังก์ชั่นการลากจะทริกเกอร์เฉพาะเมื่อมีการเคลื่อนไหวระหว่างการเลื่อนเมาส์ลงและการวางเมาส์ดังนั้นตัวฟังจบนี้จะไม่ถูกใช้หากมีการคลิกเพียงครั้งเดียว
นี่เป็นง่ามซอ
มีทางเลือกอื่นในการแก้ปัญหาจำนวนมากวิธีหนึ่งที่นี่ในขณะที่เรียบง่ายอาจไวต่อการเคลื่อนไหวในระหว่างการคลิกเล็กน้อย นี่เป็นพื้นฐานที่เป็นไปได้สำหรับแนวทางอื่นในการแยกความแตกต่างของเหตุการณ์ที่เกี่ยวข้องกับการวางเมาส์ (คลิกและลากจุดสิ้นสุด)