Tworzenie mapy D3 danych obwiedni elipsy

16

Mam ten zestaw danych, który zawiera elipsy, a dokładniej elipsy „koperty”. Zastanawiałem się, czy ktoś ma porady, jak je narysować na mapie D3. Mam już konfigurację mapy z rzutowaniem mercatora. Ta odpowiedź na przepełnienie stosu ma funkcję createEllipse, która mnie zbliżyła, ale chcę się upewnić, że poprawnie interpretuję dane.

Podłączyłem wartości osi głównej / pomocniczej elipsy z danych i użyłem azymutu do obrotu, czy to byłoby poprawne? Nie rozumiem też tak naprawdę części „koperty”. W jaki sposób kilka elips w każdej strefie tworzy pojedynczy ciągły kształt?

Wszelkie porady będą mile widziane.

wprowadź opis zdjęcia tutaj

  const margin  = {top:0, right:0, bottom:0, left:0},
        width   = 1000 - margin.left - margin.right,
        height  = 800  - margin.top - margin.bottom;

  const svg = d3.select('body')
      .append('svg')
      .attr('width', '100%')
      .attr('height', '100%')
      .attr('viewBox', `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`);

  const chart = svg.append('g')
      .attr('transform', `translate(${margin.left},${margin.top})`);

  //a/b are ellipse axes, x/y is center
  const createEllipse = function createEllipse(a, b, x = 0, y = 0, rotation = 0) {
    let k = Math.ceil(36 * (Math.max(a/b,b/a))); // sample angles
    let coords = [];
    for (let i = 0; i <= k; i++) {
      let angle = Math.PI*2 / k * i + rotation;
      let r = a * b / Math.sqrt(a*a*Math.sin(angle)*Math.sin(angle) + b*b*Math.cos(angle)*Math.cos(angle));
      coords.push(getLatLong([x,y],angle,r));
    }
    return { 'type':'Polygon', 'coordinates':[coords] };
  }

  const getLatLong = function getLatLong(center,angle,radius) {
    let rEarth = 6371; // kilometers
    x0 = center[0] * Math.PI / 180; // convert to radians.
    y0 = center[1] * Math.PI / 180;
    let y1 = Math.asin( Math.sin(y0)*Math.cos(radius/rEarth) + Math.cos(y0)*Math.sin(radius/rEarth)*Math.cos(angle) );
    let x1 = x0 + Math.atan2(Math.sin(angle)*Math.sin(radius/rEarth)*Math.cos(y0), Math.cos(radius/rEarth)-Math.sin(y0)*Math.sin(y1));
    y1 = y1 * 180 / Math.PI;
    x1 = x1 * 180 / Math.PI;
    return [x1,y1];
  } 


  d3.json('https://media.journalism.berkeley.edu/upload/2019/11/kazakhstan.json').then((data) => {

      const ellipses = [
        {lat: 48.6,    lng: 64.7,     axis_x: 30, axis_y: 16, azimuth: 26.5, area_hectar: 0.0713,  zone: 'U1'},
        {lat: 48.625,  lng: 64.625,   axis_x: 30, axis_y: 16, azimuth: 26.5, area_hectar: 0.0713,  zone: 'U1'},
        {lat: 48.366,  lng: 65.44166, axis_x: 50, axis_y: 30, azimuth: 40,   area_hectar: 0.11775, zone: 'U2'},
        {lat: 48.85,   lng: 65.61666, axis_x: 20, axis_y: 22, azimuth: 29,   area_hectar: 0.17584, zone: 'U3'},
        {lat: 48.9333, lng: 65.8,     axis_x: 22, axis_y: 22, azimuth: 28,   area_hectar: 0.17584, zone: 'U3'},
        {lat: 48.9166, lng: 66.05,    axis_x: 50, axis_y: 20, azimuth: 38,   area_hectar: 0.17584, zone: 'U3'},
        {lat: 48.9166, lng: 65.68333, axis_x: 20, axis_y: 22, azimuth: 29,   area_hectar: 0.17584, zone: 'U3'},
        {lat: 49,      lng: 65.86666, axis_x: 22, axis_y: 22, azimuth: 29,   area_hectar: 0.17584, zone: 'U3'}
      ]

      const projection = d3.geoMercator()
        .fitExtent([[0,0],[width,height]], data)

      const path = d3.geoPath()
        .projection(projection);


      chart.selectAll('path')
        .data(data.features)
        .enter()
        .append('path')
        .attr('d',  path)
        .attr('stroke', 'black')
        .attr('strok-width', '1px')
        .attr('fill', 'none');

      chart.selectAll(".ellipses")
        .data(ellipses.map((d) => createEllipse(d.axis_x, d.axis_y, d.lng, d.lat, d.azimuth)))
        .enter()
        .append('path')
        .attr('d', path)
        .attr('stroke', 'black')
        .attr('stroke-width', '1px')
        .attr('fill', 'orange');

  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="chart"></div>

jrue
źródło

Odpowiedzi:

1

Wygląda na to, że interpretujesz wyniki prawie właściwie.

Jednym z naprawionych przeze mnie błędów jest to, że twój kod nie uwzględnia azymutu.

Inna możliwa kwestia może być związana z osiami. W podanej tabeli są one nazwane jako „wymiary osi”, które brzmią jak wymiary elipsy, natomiast funkcja createEllipse przyjmuje promienie jako parametry. Proszę spojrzeć na powiększoną wizualizację z wyżej wymienionymi problemami. Etykietka po najechaniu myszką została dodana dla odniesienia.

Trzeci problem jest dyskusyjny i zależy od formatu danych określonego w tabeli. Mam na myśli, że x nie zawsze oznacza długość geograficzną, a y - szerokość geograficzną. Ale logicznie wydaje się, że elipsy dłuższe wartości (wartości „x” są większe lub równe wartościom „y”) powinny odpowiadać kierunkowi poziomemu.

Na marginesie: na dokładność wizualizacji wpływa również użycie przybliżonego promienia Ziemi, ale jest to niewielkie.

Przez „obwiednię” rozumiemy tutaj prawdopodobnie, że elipsa otacza pewien obszar zainteresowania, który leży wewnątrz, biorąc pod uwagę fakt, że podane wartości pola są znacznie mniejsze niż obszar elipsy.

Anbu Agarwal
źródło
To ogromnie pomaga. Dziękujemy za odpowiedź i próbkę kodu! Otrzymuję więcej informacji na temat zestawu danych. (Dane dotyczą spadających szczątków rakiety). Uważam, że koperta to region, w którym znajdują się wszystkie elipsy.
jrue