markmarkoh / datamaps

Customizable SVG map visualizations for the web in a single Javascript file using D3.js
http://datamaps.github.io
MIT License
3.78k stars 1.01k forks source link

Click to rotate to country using orthographic projection #438

Closed joshuaiz closed 6 years ago

joshuaiz commented 6 years ago

I'm have a legend with names of the specific countries I have highlighted on a world map using the orthographic projection.

I have the basic orthographic globe working here: https://studiobiodev.com/jb.dev/wp/partners/

What I would like to achieve is to click on a country name (on the left) and then have the map rotate and center on that country.

The closest thing I have found is this oft cited example: https://bl.ocks.org/mbostock/4183330 but I don't really know how to integrate what's there with Datamaps.

Any help or pointers in the right direction would be great. Thanks!

Here is my current code including qTip2 popups (instead of the built-in Datamaps popups):

var map;
var globalRotation = [90,-30];

function redraw() {
    d3.select("#map").html('');
    init();
} // redraw

function init() {
    map = new Datamap({
        scope: 'world',
        element: document.getElementById('map'),
        projection: 'orthographic',
        hideAntarctica: false,
        aspectRatio: 0.5,
        projectionConfig: {
            rotation: globalRotation
        },
        // done: function() {
        //  redraw('#map');
        // },
        responsive: true,
        fills: {
            // set fills
            defaultFill: "#cdcdcd",
            // partner: "#CD0000"
            partner: "#B8252F"
        },
        data: {
            ARG: { fillKey: "partner",}, // Argentina
            AUS: { fillKey: "partner" }, // Australia
            CHL: { fillKey: "partner" }, // Chile
            CHN: { fillKey: "partner" }, // China
            // COL: { fillKey: "partner" }, // Colombia
            CRI: { fillKey: "partner" }, // Costa Rica
            DEU: { fillKey: "partner" }, // Germany
            HKG: { fillKey: "partner" }, // Hong Kong
            IND: { fillKey: "partner" }, // India
            IDN: { fillKey: "partner" }, // Indonesia
            ITA: { fillKey: "partner" }, // Italy
            JPN: { fillKey: "partner" }, // Japan
            KOR: { fillKey: "partner" }, // Republic of Korea
            MYS: { fillKey: "partner" }, // Malaysia
            MEX: { fillKey: "partner" }, // Mexico
            NZL: { fillKey: "partner" }, // New Zealand
            PAK: { fillKey: "partner" }, // Pakistan
            PAN: { fillKey: "partner" }, // Panama
            PER: { fillKey: "partner" }, // Peru
            PHL: { fillKey: "partner" }, // Philippines
            SGP: { fillKey: "partner" }, // Singapore
            ZAF: { fillKey: "partner" }, // South Africa
            THA: { fillKey: "partner" }, // Thailand
            VNM: { fillKey: "partner" }, // Viet Nam
            USA: { fillKey: "partner" }, // United States
        },

        geographyConfig: {
            popupOnHover: false,
            // highlightFillColor: "#333",
            borderWidth: 1,
            borderOpacity: 1,
            borderColor: '#FDFDFD',
            highlightFillColor: false,
            highlightBorderColor: '#ffffff',

        },

    });

    window.addEventListener('resize', function() {
        map.resize();
        redraw();
    });

var drag = d3.behavior.drag().on('drag', function() {
    var dx = d3.event.dx;
    var dy = d3.event.dy;

    // var rotation = livemapScope.rotation;
    var rotation = map.projection.rotate();
    var radius = map.projection.scale();
    var scale = d3.scale.linear()
    .domain([-1 * radius, radius])
    .range([-90, 90]);
    var degX = scale(dx);
    var degY = scale(dy);
    rotation[0] += degX;
    rotation[1] -= degY;
    if (rotation[1] > 90) rotation[1] = 90;
    if (rotation[1] < -90) rotation[1] = -90;

    if (rotation[0] >= 180) rotation[0] -= 360;
    globalRotation = rotation;
    redraw();
})

d3.select("#map").select("svg").call(drag);

// qTip2 pop-ups
$('.datamaps-subunit').each(function() {
    // get #ids of ACF partner fields (ISO-3166)
    var $id = $('.map-partner').attr('id');
    // get ISO-3166 country code from class of map paths
    var $class = $(this).attr('class').split(' ')[1];

    // if hovering on map region, grab data from ACF fields for that region
    if ( $id == $class ) {
        // console.log($id, $class);
        $(this).qtip({
            content: {
                text: $('#' + $id)
            },
            position: {
                my: 'right bottom',
                at: ' top left',
                viewport: true,
            },
            style: { 
                classes: 'partner-info qtip-dark' 
            },
            hide: {
                event: 'unfocus'
            }
        });
    }
});

map.graticule();

// console.log(map.options.data);

} // init

redraw();

$('.close').click(function() {
    $(this).closest('.qtip').hide();
});
joshuaiz commented 6 years ago

Still stuck on this...anyone?

zaryab91 commented 6 years ago

Once the globe is initialized (In your case after redraw()), you can provide your desired destination.

function rotate2Destination(dest) {
    nextRotate = [dest.longitude * -1, dest.latitude * -1];
    d3.select("g")
        .transition()
        .attrTween("d", function (d) {
            var r = d3.interpolate(globalRotation, nextRotate);
            return function (t) {
                globalRotation = r(t);    
                return redraw();
            };
        })
        .duration(3000);
}

And once this rotation is completed you can Add legends/arcs/bubbles/highlightRegions etc.

joshuaiz commented 6 years ago

Hi @zaryab91 - thanks for this. I'm still stuck on how to target the respective region. In my case, I have say a element with the ISO country codes as classes so for example, Argentina has a class of 'ARG'. How do I send that to the function or does it need latitude/longitude? Or something else?

But thanks so much - I think this gets me a lot closer.

zaryab91 commented 6 years ago

Hi @joshuaiz, the above-provided function is a custom made function. Since globalRotation is an array of [long, lat] and for rotation, you need a combination of [long,lat] that too with inverted signs. Whereas the ISO class will give you lat-long. If you are able to fetch the underlying information from ISO you can modify this function accordingly.

joshuaiz commented 6 years ago

@zaryab91 got it.

This is what worked for me: I dragged/rotated the globe to each of the countries I wanted links for and got the long and lat for each one. In my case, it is only a few countries.

I then added the coordinates as data attributes to my link like so:

<a class="partner-name" data-longitude="68.40" data-latitude="39.84">Argentina</a>

Then here is my jQuery click handler:

$('a[data-id]').on('click', function() {
    var long = $(this).data('longitude');
    var lat = $(this).data('latitude');
    rotate2Destination(long, lat);
})

And then a slightly adjusted function from above, sending the long,lat as parameters:

function rotate2Destination(long, lat) {
    nextRotate = [long, lat];
    d3.select("g")
    .transition()
    .attrTween("d", function (d) {
        var r = d3.interpolate(globalRotation, nextRotate);
        return function (t) {
            globalRotation = r(t);    
            return redraw();
        };
    })
    .duration(3000);
}

The -1 in the original function weren't working but just sending the coordinates directly works.

An improvement on this would be to get the coordinates programmatically. Anyway, I'm pretty happy with this so thanks again.

joshuaiz commented 6 years ago

Going to close this but thanks @zaryab91 again for the help!