# D3.js render Vietnam map (ok)

<figure><img src="/files/Fk1UPTLuxCFhR29uNugw" alt=""><figcaption></figcaption></figure>

{% file src="/files/lBrIMwtjlDOVsPlqHrwd" %}

{% file src="/files/Xl2jxsRQz9pj1S30H7q7" %}

```
<div id="wrapper">
  <div class="scatterplot-wrapper"></div>
  <div class="legend-wrapper">
    <h1>Population density</h1>
    <div class="index-level-bar">
      <div class="bar-legend"></div>
      <div class="bar-label">Lower denisty</div>
      <div class="bar-label second">Higher denisty</div>
      <div style="clear: both;"></div>
    </div>
  </div>
  <div id="info">
    <div id="header-info">
      <span>Area, population and population density in 2011 by province (https://www.gso.gov.vn)</span>
      <table id="feature-info">
        <tbody>
          <tr>
            <th class="name" style="width:25%;">Name</th>
            <th class="population" style="width:20%;">Population</th>
            <th class="area" style="width:15%;">Area</th>
            <th class="density" style="width:15%;">Density</th>
            <th class="capital" style="width:25%;">Capital</th>
          </tr>
        </tbody>
      </table>
    </div>
    <div id="loading" class="hidden">
      <a class="fa fa-spinner"></a>
    </div>
  </div>
  <div id="map-canvas"></div>
</div>

```

```
var width = 960,
  height = 700;
var active = d3.select(null);
var zoomScale, zoomTranslate;

// We create a quantile scale to categorize the values in 5 groups.
// The domain is static and has a minimum/maximum of population/density.
// The scale returns text values which can be used for the color CSS
// classes (q0-9, q1-9 ... q8-9)
var quantiles = d3.scale.quantile()
  .range(d3.range(9).map(function(i) {
    return 'q' + i + '-9';
  }));

var svg, g, path, zoom, projection, tooltip, scatterplot;

$(document).ready(function() {
  // Area, population and population density in 2011 by province
  // get from https://www.gso.gov.vn
  // Average population (Thous. pers.)/Area (Km2)/Population density (Person/km2)
  // Load in popuplation data with D3 (or jQuery)
  d3.select('#loading').classed('hidden', false);

  tooltip = d3.select('body').append('div')
    .attr('class', 'tooltip')
    .style('opacity', 0)
    .on('click', stopped, true);

  d3.csv('https://raw.githubusercontent.com/gponster/d3tuts/master/vn-population-2011.csv', function(error, rows) {
    if (error) {
      return console.warn(error);
    }

    loadTopoJson(rows);
  });
});

function loadTopoJson(data) {

  // @see http://www.gadm.org/
  // GADM is a spatial database of the location of the world's
  // administrative areas (or adminstrative boundaries) for use in GIS and similar software.
  // @see http://mapshaper.org/ for simplify
  // A tool for topologically aware shape simplification. Reads and
  // writes Shapefile, GeoJSON and TopoJSON formats.
  d3.json('https://raw.githubusercontent.com/gponster/d3tuts/master/vn-states.json', function(error, json) {

    if (error) {
      return console.warn(error);
    }

    d3.select('#loading').classed('hidden', true);

    // While our data can be stored more efficiently in TopoJSON,
    // we must convert back to GeoJSON for display.
    var features = topojson.feature(json, json.objects.states).features;

    // Merge the ag. data and GeoJSON
    // Loop through once for each ag. data value
    for (var i = 0; i < data.length; i++) {
      // Grab state name
      var dataIso = data[i].iso;
      // Grab data value, and convert from string to float
      var density = parseFloat(data[i].density);
      var population = parseFloat(data[i].population);
      var area = parseFloat(data[i].area);

      //Find the corresponding state inside the GeoJSON
      for (var j = 0; j < features.length; j++) {

        var jsonIso = features[j].properties.iso;
        if (dataIso == jsonIso) {
          // Copy the data value into the JSON
          features[j].properties.density = density;
          features[j].properties.population = population;
          features[j].properties.area = area;

          // Stop looking through the JSON
          break;
        }
      }
    }

    // Set the domain of the values
    quantiles.domain(features.map(function(d) {
      return d.properties.density;
    }));

    var legend = d3.select('.bar-legend').append('svg')
      .attr('width', 240)
      .attr('height', 12);

    legend.selectAll('rect')
      .data(d3.range(9).map(function(i) {
        return 'q' + i + '-9';
      }))
      .enter().append('rect')
      .attr('width', 240 / 9)
      .attr('height', 12)
      .attr('x', function(d, i) {
        return (240 / 9) * i;
      })
      .attr('data-level', function(d, i) {
        return i;
      })
      .attr('class', function(d) {
        return 'legend ' + d;
      }).on('mouseover', function(type) {
        d3.selectAll('.legend')
          .style('opacity', .3);
        d3.select(this)
          .style('opacity', 1);

        var level = d3.select(this).attr('data-level');

        d3.selectAll('.feature')
          .style('opacity', .1)
          .filter('.q' + level + '-9')
          .style('opacity', 1);

        d3.selectAll('.bubble')
          .style('fill-opacity', .1)
          .filter('.q' + level + '-9')
          .style('fill-opacity', .75);

        d3.selectAll('.state-boundary')
          .style('stroke-opacity', .3);
      })
      .on('mouseout', function(type) {
        d3.selectAll('.legend')
          .style('opacity', 1);
        d3.selectAll('.feature')
          .style('opacity', 1);
        d3.selectAll('.bubble')
          .style('fill-opacity', .75);

        d3.selectAll('.state-boundary')
          .style('stroke-opacity', 1);
      });

    drawMap(json, features);
    drawScatterplot(features);
  });
};

function drawScatterplot(data) {
  var margin = {
    top: 20,
    right: 10,
    bottom: 80,
    left: 40
  };
  var w = 270 - margin.left - margin.right;
  var h = 270 - margin.top - margin.bottom;

  var scatter = d3.select('.scatterplot-wrapper')
    .append('svg')
    .attr('width', w + margin.left + margin.right)
    .attr('height', h + margin.top + margin.bottom)
    .append('g')
    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');

  /// Population (x)
  var xscale = d3.scale.linear()
    .domain([d3.min(data, function(d) {
        return d.properties.population;
      }),
      d3.max(data, function(d) {
        return d.properties.population;
      })
    ])
    .range([0, w]);

  // Area (y)
  var yscale = d3.scale.linear()
    .domain([d3.min(data, function(d) {
        return d.properties.area / 1000;
      }),
      d3.max(data, function(d) {
        return d.properties.area / 1000;
      })
    ])
    .range([h, 0]);

  var rscale = d3.scale.sqrt()
    .domain([d3.min(data, function(d) {
        return d.properties.density;
      }),
      d3.max(data, function(d) {
        return d.properties.density;
      })
    ])
    .range([3, 15]);

  // Define X axis
  var xaxis = d3.svg.axis()
    .scale(xscale)
    .orient('bottom')
    .tickSize(-h)
    .tickFormat(d3.format('s'));

  // Define Y axis
  var yaxis = d3.svg.axis()
    .scale(yscale)
    .orient('left')
    .ticks(6)
    .tickSize(-w);

  // Create X axis
  scatter.append('g')
    .attr('class', 'x-axis axis')
    .attr('transform', 'translate(0,' + (h) + ')')
    .call(xaxis);

  // Create Y axis
  scatter.append('g')
    .attr('class', 'y-axis axis')
    .attr('transform', 'translate(' + 0 + ',0)')
    .call(yaxis);

  // Add label to X axis
  scatter.append('text')
    .attr('class', 'x label')
    .attr('text-anchor', 'middle')
    .attr('x', w - w / 2)
    .attr('y', h + margin.bottom / 2)
    .text('Population');

  // Add label to Y axis
  scatter.append('text')
    .attr('class', 'y label')
    .attr('text-anchor', 'middle')
    .attr('y', -margin.left + 5)
    .attr('x', 0 - (h / 2))
    .attr('dy', '1em')
    .attr('transform', 'rotate(-90)')
    .text('Area (1000 km2)');

  var clr = d3.scale.category20();

  var circles = scatter.selectAll('circle')
    .data(data).enter()
    .append('circle')
    .attr('cx', function(d) {
      return xscale(d.properties.population);
    })
    .attr('cy', function(d) {
      return yscale(d.properties.area / 1000);
    })
    .attr('r', function(d) {
      return rscale(d.properties.density);
    })
    .attr('class', function(d) {
      return 'bubble state-' + d.properties.iso + ' ' +
        quantiles(d.properties.density);
    })
    .on('mouseover', function(d) {
      tooltip.transition().duration(300)
        .style('opacity', 1);

      tooltip.text(d.properties.name)
        .style('left', d3.event.pageX + 'px')
        .style('top', (d3.event.pageY - 50) + 'px');
    }).on('mouseout', function(d) {
      tooltip.transition().duration(300)
        .style('opacity', 0);
    });
}

function drawMap(json, features) {
  // @see http://geojson.org/
  // Create a first guess for the projection
  //var center = d3.geo.centroid(json);
  var center = [106.34899620666437, 16.553160650957434];
  var scale = 2500;
  var offset = [width / 2, height / 2 - 50];

  // The projection function takes a location [longitude, latitude]
  // and returns a Cartesian coordinates [x,y] (in pixels).
  //
  // D3 has several built-in projections. Albers USA is a composite projection
  // that nicely tucks Alaska and Hawaii beneath the Southwest.
  //
  // Albers USA (albersUsa) is actually the default projection for d3.path.geo()
  // The default scale value is 1,000. Anything smaller will shrink the map;
  // anything larger will expand it.
  //
  // Add a scale() method with 800 to our projection in order to shrink things down a bit
  //var projection = d3.geo.albersUsa()
  //     .translate([w / 2, h / 2]).scale([800]);
  projection = d3.geo.mercator()
    .translate(offset)
    .scale([scale])
    .center(center);

  // We define our first path generator for translating that
  // mess of GeoJSON coordinates into even messier messes of SVG path codes.
  // Tell the path generator explicitly that it should reference our customized
  // projection when generating all those paths
  path = d3.geo.path()
    .projection(projection);

  zoom = d3.behavior.zoom()
    .translate([0, 0])
    .scale(1)
    .scaleExtent([1, 13])
    .on('zoom', zoomed);

  svg = d3.select('#map-canvas').append('svg')
    .attr('width', width)
    .attr('height', height)
    .on('click', stopped, true);

  svg.append('rect')
    .attr('class', 'overlay')
    .attr('width', width)
    .attr('height', height)
    .on('click', function() {
      zoomScale = 1;
      zoomTranslate = [0, 0];

      reset();
    }, true);

  g = svg.append('g');

  // Create g before call zoom
  

  //-----------------------------------------------------------------
  // For country boundary and state mesh/not data binding
  //-----------------------------------------------------------------
  var boundary = g.append('g')
    .attr('class', 'boundary');

  g.attr('class', 'states')
    .selectAll('path') // select all the current path nodes
    .data(features) // bind these to the features array in json
    .enter().append('g') // if not enough elements create a new group
    .attr('class', function(d) {
      return 'state state-' + d.properties.iso;
    })
    .on('mouseover', function(d) {
      //---------------------------------------------------------
      // Tooltip
      //---------------------------------------------------------
      tooltip.transition().duration(300)
        .style('opacity', 1);

      tooltip.text(d.properties.name)
        .style('left', d3.event.pageX + 'px')
        .style('top', (d3.event.pageY - 50) + 'px');
      //---------------------------------------------------------

      //---------------------------------------------------------
      // Feature info
      //---------------------------------------------------------
      $('#feature-info').find('tr:gt(0)').remove();

      var html = '<tr><td>' + d.properties.name + '</td>' +
        '<td>' + d.properties.population.toFixed(2) + '</td>' +
        '<td>' + d.properties.area.toFixed(2) + '</td>' +
        '<td>' + d.properties.density.toFixed(2) + '</td>' +
        '<td>' + d.properties.capital + '</td></tr>';
      $('#feature-info tr:last').after(html);
      //---------------------------------------------------------

      //---------------------------------------------------------
      // Bubble
      //---------------------------------------------------------
      d3.selectAll('.bubble')
        .style('fill-opacity', .1)
        .filter('.state-' + d.properties.iso)
        .classed('highlight', true);
    })
    .on('mouseout', function(d) {
      tooltip.transition().duration(300)
        .style('opacity', 0);

      $('#feature-info').find('tr:gt(0)').remove();

      d3.selectAll('.bubble')
        .style('fill-opacity', .75)
        .classed('highlight', false);
    })
    .on('click', clicked)
    .append('path')
    .attr('class', function(d) {
      // Use the quantiled value for the class
      return 'feature ' + quantiles(d.properties.density);
    }) // add attribute class and fill with result from quantiles
    .attr('d', path);

  //-----------------------------------------------------------------
  // Now we can draw boundary, prevent lost data cause by merging and meshing
  //-----------------------------------------------------------------
  // Country boundary from merge all geometries
  boundary.append('path')
    .attr('class', 'country-boundary')
    .datum(topojson.merge(json, json.objects.states.geometries))
    .attr('d', path);

  // State mesh
  boundary.append('path')
    .attr('class', 'state-boundary')
    .datum(topojson.mesh(json, json.objects.states, function(a, b) {
      return a !== b;
    })).attr('d', path);
  //-----------------------------------------------------------------

  //-----------------------------------------------------------------
  // State names
  //-----------------------------------------------------------------
  g.append('g')
    .attr('class', 'state-labels')
    .selectAll('text') // select all the current path nodes
    .data(features)
    .enter().append('text') // if not enough elements create a text
    .attr('class', function(d) {
      // To make contract text
      var className = 'state-label state-' + d.properties.iso;
      return className + ' ' + quantiles(d.properties.density);
    })
    .text(function(d) {
      // Name from bound data we already binded using .data(features)
      return d.properties.name;
    })
    // Using transform equivalent to x, y
    .attr('transform', function(d) {
      return 'translate(' + path.centroid(d) + ')';
    })
    //.attr('x', function (d) {
    //    return path.centroid(d)[0];
    //})
    //.attr('y', function (d) {
    //    return path.centroid(d)[1];
    //})
    // The dy attribute indicates a shift along the y-axis on the position
    // of an element or its content. What exactly is shifted
    // depends on the element for which this attribute is set.
    .attr('dy', '.35em');

  drawCities();

  d3.select(self.frameElement).style('height', height + 'px');
}

function drawCities() {
  // Cities group
  g.append('g')
    .attr('class', 'cities');

  d3.csv('vn-cities.csv', function(error, rows) {
    if (error) {
      return console.warn(error);
    }

    rows.forEach(function(row, i) {
      // Create new group and binding data
      var sg = g.selectAll('.cities')
        .append('g').datum(row)
        .attr('class', function(d) {
          return 'city city-' + d.code + ' level-' + d.level;
        });

      // Append circle to group of city
      sg.append('circle')
        .attr('class', function(d) {
          return 'city-place';
        })
        .attr('visibility', function(d) {
          return d.level < 3 ? 'visible' : 'hidden';
        })
        .attr('cx', function(d) {
          return projection([d.lng, d.lat])[0];
        })
        .attr('cy', function(d) {
          return projection([d.lng, d.lat])[1];
        })
        .attr('r', 2)
        .style('fill', 'white')
        .style('stroke', 'black')
        .style('stroke-width', 2)
        .style('opacity', 0.85)
        // Modification of custom tooltip code provided by Malcolm Maclean, "D3 Tips and Tricks"
        // http://www.d3noob.org/2013/01/adding-tooltips-to-d3js-graph.html
        .on('mouseover', function(d) {
          //div.transition()
          //    .duration(200)
          //    .style('opacity', .9);
        })
        // fade out
        .on('mouseout', function(d) {
          //div.transition()
          //    .duration(500)
          //    .style('opacity', 0);
        });

      sg.append('text')
        .attr('class', function(d) {
          return 'city-label';
        })
        .text(function(d) {
          return d.name;
        })
        .attr('visibility', function(d) {
          return d.level < 2 ? 'visible' : 'hidden';
        })
        .attr('x', function(d) {
          return projection([d.lng, d.lat])[0];
        })
        .attr('y', function(d) {
          return projection([d.lng, d.lat])[1];
        })
        .attr('text-anchor', function(d) {
          return d.lng > 105.7 ? 'start' : 'end';
        })
        .attr('dx', function(d) {
          return (d.lng > 105.7 ? 1 : -1) * 0.7 + 'em';
        })
        // The dy attribute indicates a shift along the y-axis on the position
        // of an element or its content. What exactly is shifted
        // depends on the element for which this attribute is set.
        .attr('dy', '.35em');
    });
  });
}

function zoomed() {
  g.selectAll('.country-boundary').style('stroke-width', 1 / d3.event.scale + 'px');

  g.selectAll('.feature').style('stroke-width', 2 / (d3.event.scale + 0.5) + 'px');
  g.attr('transform', 'translate(' + d3.event.translate + ')scale(' + d3.event.scale + ')');

  g.selectAll('.state-label').style('font-size', (8 / d3.event.scale + 2) + 'px')

  g.selectAll('.city-place')
    .style('r', 1 / d3.event.scale + 0.7)
    .style('stroke-width', 2 / (d3.event.scale + 0.5) + 'px');

  g.selectAll('.city-label')
    .style('font-size', (12 / d3.event.scale + 1.5) + 'px')
    .attr('dy', d3.event.scale == 1 ? '0.35em' : (((1 / d3.event.scale) * 0.35 + 0.2) + 'em'));

  g.selectAll('.level-3 .city-label')
    .style('font-size', (6 / d3.event.scale + 1.5) + 'px');

  g.selectAll('.level-4 .city-label')
    .style('font-size', (5 / d3.event.scale + 1.5) + 'px');

  if (d3.event.scale < 2) {
    g.selectAll('.level-2 .city-label').attr('visibility', 'hidden');
  } else {
    g.selectAll('.level-2 .city-label').attr('visibility', 'visible');
  }

  if (d3.event.scale < 3.3) {
    g.selectAll('.level-3 .city-label').attr('visibility', 'hidden');
    g.selectAll('.level-3 .city-place').attr('visibility', 'hidden');

    g.selectAll('.level-4 .city-label').attr('visibility', 'hidden');
    g.selectAll('.level-4 .city-place').attr('visibility', 'hidden');
  } else {
    g.selectAll('.level-3 .city-label').attr('visibility', 'visible');
    g.selectAll('.level-3 .city-place').attr('visibility', 'visible');

    g.selectAll('.level-4 .city-label').attr('visibility', 'visible');
    g.selectAll('.level-4 .city-place').attr('visibility', 'visible');
  }
}

// If the drag behavior prevents the default click,
// also stop propagation so we don’t click-to-zoom.
function stopped() {
  if (d3.event.defaultPrevented) {
    d3.event.stopPropagation();
  }
}

function reset() {
  active.classed('active', false);
  active = d3.select(null);

  zoomScale = zoomScale || 1;
  zoomTranslate = zoomTranslate || [0, 0];

  svg.transition()
    .duration(750)
    .call(zoom.translate(zoomTranslate).scale(zoomScale).event);
}

function clicked(d) {
  if (active.node() === this) {
    return reset();
  }

  active.classed('active', false);
  active = d3.select(this).classed('active', true);

  // Save current zoom and translate
  zoomScale = zoom.scale();
  zoomTranslate = zoom.translate();

  var bounds = path.bounds(d),
    dx = bounds[1][0] - bounds[0][0],
    dy = bounds[1][1] - bounds[0][1],
    x = (bounds[0][0] + bounds[1][0]) / 2,
    y = (bounds[0][1] + bounds[1][1]) / 2,
    scale = Math.max(1, Math.min(8, 0.9 / Math.max(dx / width, dy / height))),
    translate = [width / 2 - scale * x, height / 2 - scale * y];

  svg.transition()
    .duration(750)
    .call(zoom.translate(translate).scale(scale).event);
}

```

```
#wrapper {
  padding-top: 15px;
}

.bar-label {
  float: left;
  margin-top: 5px;
  line-height: 12px;
  font-size: 12px;
  font-family: 'Helvetica';
}

.bar-label.second {
  float: right;
}

table {
  display: table;
  border-collapse: separate;
  border-spacing: 2px;
  border-color: grey;
  padding: 0px;
}

th {
  font-size: 16px;
  font-family: sans-serif;
  font-weight: normal;
  text-align: left;
}

th.name {
  color: #E7BA52;
}

th.area {
  color: #FC9E27;
}

th.population {
  color: #3A7FA3;
}

th.density {
  color: #B5CF6B;
}

th.capital {
  color: #D6616B;
}

th,
td {
  color: #555;
}

.h2,
h2 {
  font-size: 30px;
}

.h1,
.h2,
.h3,
h1,
h2,
h3 {
  margin-top: 20px;
  margin-bottom: 10px;
  color: #555;
}

.scatterplot-wrapper {
  position: fixed;
  z-index: 2;
  left: 15px;
  bottom: 300px;
  width: 270px;
  text-align: center;
}

.axis path,
.axis line {
  fill: none;
  stroke-width: 1px;
  stroke: #e7e7e7;
  stroke-opacity: .5;
  shape-rendering: crispEdges;
}

.axis text {
  line-height: 12px;
  font-size: 12px;
  font-family: 'Helvetica';
  fill: #666;
}

.label {
  line-height: 12px;
  font-size: 12px;
  font-family: 'Helvetica';
}

.scatterplot-wrapper circle {
  fill-opacity: .75;
}

.scatterplot-wrapper circle:hover {
  fill-opacity: 1;
}

.legend-wrapper {
  position: fixed;
  z-index: 2;
  left: 15px;
  bottom: 150px;
  width: 270px;
  text-align: center;
}

.legend-wrapper h1 {
  font: 22px 'oswaldregular';
}

#map-canvas {
  width: 900px;
  margin-left: 290px;
}

#info {
  height: 90px;
}

#header-info {
  margin-left: 290px;
}

#feature-info {
  width: 650px;
}

#loading {
  width: 50px;
  margin: 0 auto;
}


/* On mouse hover, lighten state color */

.feature:hover {
  fill: yellow;
  fill-opacity: .35;
}

.bubble.highlight {
  stroke: #3A7FA3;
  fill-opacity: .75;
}

.active .feature {
  fill: yellow;
  opacity: 0.75;
}

.city-label {
  font: 12px "Helvetica Neue", Helvetica, Arial, sans-serif;
  pointer-events: none;
  fill: #444;
  fill-opacity: .75;
}

.state-label {
  fill: #777 !important;
  fill-opacity: .5;
  font-size: 8px;
  text-anchor: middle;
  pointer-events: none;
}

.feature,
.legend {
  cursor: pointer;
}

.country-boundary {
  fill: none;
  stroke: #37C3BC;
  stroke-linejoin: round;
}

.state-boundary {
  fill: none;
  stroke: #003568;
  stroke-dasharray: 5, 3;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}


/* http://colorbrewer2.org/ */

.q0-9 {
  fill: #F7FCF0;
}

.q1-9 {
  fill: #E0F3DB;
}

.q2-9 {
  fill: #CCEBC5;
}

.q3-9 {
  fill: #A8DDB5;
}

.q4-9 {
  /* lighten #7BCCC4 */
  fill: #BBF3FF;
}

.q5-9 {
  /* lighten #4EB3D3 */
  fill: #A1D9FF;
}

.q6-9 {
  /* lighten #2B8CBE */
  fill: #87BFFF;
}

.q7-9 {
  /* lighten #0868AC */
  fill: #6EA6E7;
}

.q8-9 {
  /* lighten #084081 */
  fill: #558DCE;
}

.state-label.q5-9 {
  fill: #A8DDB5 !important;
}

.state-label.q6-9 {
  fill: #CCEBC5 !important;
}

.state-label.q7-9 {
  fill: #E0F3DB !important;
}

.state-label.q8-9 {
  fill: #F7FCF0 !important;
}

.overlay {
  fill: none;
  pointer-events: all;
}

.fa-spinner {
  -webkit-animation: spin 1300ms infinite linear;
  -moz-animation: spin 1300ms infinite linear;
  -ms-animation: spin 1300ms infinite linear;
  -o-animation: spin 1300ms infinite linear;
  animation: spin 1300ms infinite linear;
  font-size: 41px;
  text-decoration: none;
  color: #C0C0C0;
}

.hidden {
  display: none;
  visibility: hidden;
}

div.tooltip {
  position: absolute;
  text-align: center;
  border-radius: 4px;
  pointer-events: none;
  font-family: arial, helvetica, sans-serif;
  font-size: 12px;
  border: solid 1px #ccc;
  background: rgba(255, 255, 255, .75);
  padding: 4px 10px;
  position: absolute;
  z-index: 1000;
  box-shadow: 0 0 8px rgba(0, 0, 0, .2);
  pointer-events: none;
  width: 100px;
  margin-left: -55px;
}

@-webkit-keyframes spin {
  to {
    -webkit-transform: rotate(360deg);
  }
}

@-moz-keyframes spin {
  to {
    -moz-transform: rotate(360deg);
  }
}

@-ms-keyframes spin {
  to {
    -ms-transform: rotate(360deg);
  }
}

@-o-keyframes spin {
  to {
    -o-transform: rotate(360deg);
  }
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://learnhtml.gitbook.io/project/d3.js-render-vietnam-map-ok.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
