L'evento Click D3 non funziona dopo aver riapplicato il percorso

Jan 03 2021

Sto cercando di ricreare questo globo trascinabile da Mike Bostock utilizzando D3 ma come versione svg. A causa di problemi di prestazioni durante il trascinamento, sto eseguendo nuovamente il rendering del globo. Fin qui tutto bene. Ora voglio implementare un evento clic ma non funziona. Qui si dice che il problema potrebbe essere la riapplicazione. Un evento mousedown funziona correttamente ma interferisce con il trascinamento successivo. Perché l'evento mousedown funziona e l'evento clic no? Suggerimenti per ristrutturare il codice per risolvere questo problema sono molto apprezzati.

Ho creato un violino per una migliore comprensione: violino

PS. Sono nuovo nella programmazione e in D3, quindi per favore non essere troppo duro :)

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)
}

Risposte

1 AndrewReid Jan 03 2021 at 08:51

Non attiverai mai il clic come è adesso, come hai visto. Questo perché un clic coinvolge sia il mouse in basso che il mouse in alto. Questi interagiscono con il comportamento di trascinamento e ciascuno attiva rispettivamente gli eventi di inizio e di fine.

Quello che sta succedendo nel tuo caso è che il mousedown innesca il trascinamento e l'ascoltatore mousedown che hai aggiunto ai percorsi. Quindi, al passaggio del mouse, il listener di eventi di trascinamento si attiva per primo, rimuovendo eventuali percorsi e listener associati. La funzione render aggiunge nuovi percorsi dopo il fatto, troppo tardi per registrare l'evento.

Esistono numerose soluzioni, ma forse la più semplice è rimuovere il listener di fine trascinamento che hai e sostituirlo solo quando si verifica effettivamente un trascinamento (non nell'evento di inizio, nell'evento di trascinamento):

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))
}

Il event.on()metodo consente agli ascoltatori di essere applicati solo per il gesto corrente. Il trascinamento del listener si attiva solo se c'è un mousemove tra mousedown e mouseup, quindi questo end listener non verrà utilizzato se c'è solo un semplice clic.

Ecco un violino biforcuto .

Ci sono un discreto numero di soluzioni alternative a questo problema, quella qui, sebbene semplice, potrebbe essere un po 'sensibile al movimento durante il clic. Ecco una possibile base per un approccio alternativo alla differenziazione degli eventi relativi al mouseup (fare clic e trascinare la fine).