Das D3-Klickereignis funktioniert nach dem erneuten Anhängen des Pfads nicht
Ich versuche, diesen ziehbaren Globus von Mike Bostock mit D3, aber als SVG-Version , neu zu erstellen . Aufgrund von Leistungsproblemen beim Ziehen rendere ich den Globus neu. So weit, ist es gut. Jetzt möchte ich ein Klickereignis implementieren, aber es funktioniert nicht. Hier wird erwähnt, dass das Problem das erneute Anhängen sein könnte. Ein Mousedown-Ereignis funktioniert einwandfrei, beeinträchtigt jedoch später das Ziehen. Warum funktioniert das Mousedown-Ereignis und das Click-Ereignis nicht? Hinweise zur Umstrukturierung des Codes, um dies zu lösen, werden sehr geschätzt.
Ich habe eine Geige zum besseren Verständnis erstellt: Geige
PS. Ich bin neu in Programmierung und D3, also sei bitte nicht zu hart :)
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)
}
Antworten
Sie werden den Klick nie so auslösen, wie Sie ihn gerade gesehen haben. Dies liegt daran, dass ein Klick sowohl die Maus nach unten als auch die Maus nach oben umfasst. Diese interagieren mit dem Drag-Verhalten, wobei jedes das Start- bzw. Endereignis auslöst.
In Ihrem Fall löst das Mousedown das Ziehen aus und der Mousedown-Listener, den Sie den Pfaden hinzugefügt haben. Mit der Maus nach oben wird der Listener für Drag-Events zuerst ausgelöst und entfernt alle Pfade und zugehörigen Listener. Die Renderfunktion fügt nachträglich neue Pfade hinzu, zu spät, um das Ereignis zu registrieren.
Es gibt eine Reihe von Lösungen, aber am einfachsten ist es, den Listener für das Drag-End zu entfernen und ihn nur zu ersetzen, wenn tatsächlich ein Drag auftritt (nicht im Startereignis, im Drag-Ereignis):
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))
}
Mit dieser event.on()
Methode können Listener nur für die aktuelle Geste angewendet werden. Der Drag-Listener wird nur ausgelöst, wenn zwischen Mousedown und Mouseup eine Mausbewegung ausgeführt wird. Daher wird dieser End-Listener nicht verwendet, wenn nur ein einfacher Klick erfolgt.
Hier ist eine gegabelte Geige .
Es gibt eine ganze Reihe alternativer Lösungen für dieses Problem. Die hier ist zwar einfach, reagiert jedoch möglicherweise etwas empfindlich auf Bewegungen während des Klicks. Hier ist eine mögliche Grundlage für einen alternativen Ansatz zur Unterscheidung von Mouseup-bezogenen Ereignissen (Klicken und Ziehen des Endes).