← Examples
countries · languages · choropleth · multi-color

Languages

World countries colored by official language, using the opt-in languages property. The 8 most common languages each get their own color; every other language (~65 of them, from Aymara to Zulu) folds into other so the legend stays readable. A country with one language gets a solid fill; a country with several — Switzerland, Singapore, most of West Africa — is striped, one band per distinct language color. Hover any country to see its full language list.

Legend counts are countries that speak each language at all, not just countries where it's the only one — a striped country counts toward every language shown in its stripes.

API Call

https://api.mapjson.com/v1/geo?layer=countries&detail=medium&properties=name,languages

Code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Languages</title>
  <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/topojson-client@3/dist/topojson-client.min.js"></script>
  <style>
    body { margin: 0; background: #cfe0ea; }
    svg { width: 100%; height: 100vh; display: block; }
    .country { stroke: #b8ad9e; stroke-width: 0.4; }
    .tooltip {
      position: fixed;
      background: #1a1a1a;
      color: #fafafa;
      font-family: monospace;
      font-size: 11px;
      padding: 6px 10px;
      pointer-events: none;
      opacity: 0;
      transition: opacity 0.1s;
      line-height: 1.6;
    }
  </style>
</head>
<body>
  <div class="tooltip" id="tooltip"></div>
  <svg id="map"></svg>
  <script>
    const url = "https://api.mapjson.com/v1/geo?layer=countries&detail=medium&properties=name,languages";

    // Fixed hue order — never cycled — plus a neutral "other" bucket for the
    // long tail. 8 categorical slots max; the rest fold into gray.
    const TOP_LANGUAGES = ["English", "French", "Arabic", "Spanish", "Portuguese", "Russian", "German", "Italian"];
    const COLORS = ["#2a78d6", "#1baf7a", "#eda100", "#008300", "#4a3aa7", "#e34948", "#e87ba4", "#eb6834"];
    const OTHER_COLOR = "#c7c3ba";
    const color = d3.scaleOrdinal().domain(TOP_LANGUAGES).range(COLORS).unknown(OTHER_COLOR);

    // Distinct colors this country needs — dedup so several "other" languages
    // collapse to one gray band instead of stacking duplicates.
    function languageColors(d) {
      const langs = d.properties.languages;
      if (!langs || !langs.length) return [OTHER_COLOR];
      const colors = [];
      for (const l of langs) {
        const c = color(l.name);
        if (!colors.includes(c)) colors.push(c);
      }
      return colors;
    }

    // Solid fill for one color; a diagonal stripe pattern — one band per
    // distinct language color, cached in <defs> — for two or more.
    // objectBoundingBox (not userSpaceOnUse) so the stripe tile scales to each
    // country's own size — a fixed pixel tile is bigger than Switzerland or
    // Belgium at world-map scale, so only one color would ever show.
    const patternIds = new Map();
    const TILE = 0.22; // fraction of each country's own bounding box
    function fillFor(defs, colors) {
      if (colors.length === 1) return colors[0];
      const key = colors.join(",");
      if (patternIds.has(key)) return `url(#${patternIds.get(key)})`;
      const id = `lang-stripe-${patternIds.size}`;
      const band = TILE / colors.length;
      const pattern = defs.append("pattern")
        .attr("id", id)
        .attr("patternUnits", "objectBoundingBox")
        .attr("patternContentUnits", "objectBoundingBox")
        .attr("width", TILE).attr("height", TILE)
        .attr("patternTransform", "rotate(45)");
      colors.forEach((c, i) => {
        pattern.append("rect").attr("x", 0).attr("y", i * band).attr("width", TILE).attr("height", band).attr("fill", c);
      });
      patternIds.set(key, id);
      return `url(#${id})`;
    }

    const width = 960, height = 500;
    const tooltip = document.getElementById("tooltip");
    const svg = d3.select("#map").attr("viewBox", `0 0 ${width} ${height}`);
    const defs = svg.append("defs");
    const projection = d3.geoEqualEarth().scale(160).translate([width / 2, height / 2 + 20]);
    const path = d3.geoPath(projection);

    svg.append("path").datum({ type: "Sphere" }).attr("fill", "#22485e").attr("d", path);
    svg.append("path").datum(d3.geoGraticule()()).attr("fill", "none")
      .attr("stroke", "#a9c3d1").attr("stroke-width", 0.3).attr("d", path);

    d3.json(url).then(topo => {
      const geojson = topojson.feature(topo, topo.objects.geo);

      svg.selectAll(".country")
        .data(geojson.features)
        .join("path")
        .attr("class", "country")
        .attr("d", path)
        .attr("fill", (d) => fillFor(defs, languageColors(d)))
        .on("mousemove", function(event, d) {
          const langs = d.properties.languages;
          tooltip.style.left = (event.clientX + 12) + "px";
          tooltip.style.top  = (event.clientY - 10) + "px";
          tooltip.style.opacity = 1;
          tooltip.innerHTML =
            `<strong>${d.properties.name || d.properties.gid}</strong><br>` +
            (langs?.length ? langs.map((l) => l.name).join(", ") : "no language data");
        })
        .on("mouseleave", () => { tooltip.style.opacity = 0; });
    });
  </script>
</body>
</html>