{"id":"1b3eedffde4bea38","slug":"reusable-bivariate-choropleth","first_public_version":1072,"paused_version":null,"likes":4,"publish_level":"live","forks":1,"fork_of":{"id":"8b1da0468b5f1e92","slug":"bivariate-choropleth","title":"Bivariate choropleth","owner":{"id":"863951e3ebe4c0ae","avatar_url":"https://avatars.observableusercontent.com/avatar/5af16e327a90b2873351dda8a596c0d2d3bf954f64523deefe80177c9764d0f7","login":"d3","name":"D3","bio":"Bring your data to life.","home_url":"https://d3js.org","type":"team","tier":"pro_2024"},"version":411},"has_importers":true,"thumbnail":"2e4cf746dff48958e67b9d63f36de7a238c5cae2186c40658cccf3fb669354b7","default_thumbnail":"2e4cf746dff48958e67b9d63f36de7a238c5cae2186c40658cccf3fb669354b7","update_time":"2020-06-29T15:49:07.752Z","publish_time":"2023-09-12T09:35:11.852Z","publish_version":1072,"latest_version":1072,"roles":[],"sharing":null,"owner":{"id":"1a55a189adb52dd7","avatar_url":"https://avatars.observableusercontent.com/avatar/39220b21cbf7eff8318ad71b8f1f1b73aaaa40953e51c89e2bf097d8e901318c","login":"juba","name":"Julien Barnier","bio":"Trying to do things.","home_url":"https://data.nozav.org","type":"team","tier":"starter_2024"},"creator":{"id":"ef385df54c3fe400","avatar_url":"https://avatars.observableusercontent.com/avatar/39220b21cbf7eff8318ad71b8f1f1b73aaaa40953e51c89e2bf097d8e901318c","login":"juba","name":"Julien Barnier","bio":"Trying to do things.","home_url":"https://data.nozav.org","tier":"public"},"authors":[{"id":"ef385df54c3fe400","avatar_url":"https://avatars.observableusercontent.com/avatar/39220b21cbf7eff8318ad71b8f1f1b73aaaa40953e51c89e2bf097d8e901318c","name":"Julien Barnier","login":"juba","bio":"Trying to do things.","home_url":"https://data.nozav.org","tier":"public","approved":true,"description":""}],"files":[{"id":"f2a5e61b1bfcf1745cad68d1410bd694f1e4aeed32c2dbdf2dd9d053b55d51cce316568041210d0685a415ddd4033c7e5ac3d15b3faf66e039b0b48920036ea2","url":"https://static.observableusercontent.com/files/f2a5e61b1bfcf1745cad68d1410bd694f1e4aeed32c2dbdf2dd9d053b55d51cce316568041210d0685a415ddd4033c7e5ac3d15b3faf66e039b0b48920036ea2","download_url":"https://static.observableusercontent.com/files/f2a5e61b1bfcf1745cad68d1410bd694f1e4aeed32c2dbdf2dd9d053b55d51cce316568041210d0685a415ddd4033c7e5ac3d15b3faf66e039b0b48920036ea2?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27dipl2016%25402.csv","name":"dipl2016@2.csv","create_time":"2020-06-26T07:40:41.914Z","mime_type":"text/csv","status":"public","size":998371,"content_encoding":"gzip","private_bucket_id":null},{"id":"80a25f7d0f48ef12eff79019e9322d12fe136548af5779a21d889898d1b11f672c667cd464510bc918c13e7b439ddeee8ad6c4a10e26953fae432c42b4e5beea","url":"https://static.observableusercontent.com/files/80a25f7d0f48ef12eff79019e9322d12fe136548af5779a21d889898d1b11f672c667cd464510bc918c13e7b439ddeee8ad6c4a10e26953fae432c42b4e5beea","download_url":"https://static.observableusercontent.com/files/80a25f7d0f48ef12eff79019e9322d12fe136548af5779a21d889898d1b11f672c667cd464510bc918c13e7b439ddeee8ad6c4a10e26953fae432c42b4e5beea?response-content-disposition=attachment%3Bfilename*%3DUTF-8%27%27dipl2016%25403.csv","name":"dipl2016@3.csv","create_time":"2020-06-27T22:09:41.716Z","mime_type":"text/csv","status":"public","size":998584,"content_encoding":"gzip","private_bucket_id":null}],"comments":[],"commenting_lock":null,"suggestions_to":[],"suggestion_from":null,"collections":[{"id":"7ce57e5117a4f35d","type":"public","slug":"robservable","title":"robservable","description":"","update_time":"2020-07-02T09:30:42.639Z","pinned":false,"ordered":false,"custom_thumbnail":null,"default_thumbnail":"3c0eb3256ff2525b2a55991f8e0f23877a4775d6eab62297c43f308d85f96072","thumbnail":"3c0eb3256ff2525b2a55991f8e0f23877a4775d6eab62297c43f308d85f96072","listing_count":5,"parent_collection_count":0,"owner":{"id":"1a55a189adb52dd7","avatar_url":"https://avatars.observableusercontent.com/avatar/39220b21cbf7eff8318ad71b8f1f1b73aaaa40953e51c89e2bf097d8e901318c","login":"juba","name":"Julien Barnier","bio":"Trying to do things.","home_url":"https://data.nozav.org","type":"team","tier":"starter_2024"}}],"version":1072,"title":"Reusable Bivariate Choropleth","license":null,"copyright":"","nodes":[{"id":0,"value":"md`# Reusable Bivariate Choropleth`","pinned":false,"mode":"js","data":null,"name":null},{"id":993,"value":"md`This notebook is a fork of [Mike Bostock's Bivariate Choropleth notebook](https://observablehq.com/@d3/bivariate-choropleth) with the goal to make it as reusable as possible and updatable with transitions.\n\n[robservable](https://juba.github.io/robservable/) users can call it from R with something like :\n\n\\`\\`\\`r\nrobservable(\n  \"@juba/reusable-bivariate-choropleth\",\n  cell = c(\"chart\", \"draw\"),\n  hide = \"draw\"\n)\n\\`\\`\\`\n\nAnd by updating the [settings](#sec_settings) with a named list as \\`input\\` argument.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":1002,"value":"md`The example below shows a bivariate choropleth, for of all metropolitan french towns in 2016, of the percentage of people with a college degree (\\`Ens. sup.\\`) *vs* the percentage of people without a degree or with a middle-school one (\\`Dipl. min.\\`). Source : [INSEE](https://www.insee.fr/fr/statistiques/4171395?sommaire=4171407).\n\nYou can also take a look at [another example with contour and animated transitions](https://observablehq.com/d/61be71d2b8c0d456).`","pinned":false,"mode":"js","data":null,"name":null},{"id":969,"value":"viewof color_scheme = select({\n  title: \"Color scheme\",\n  options: schemes.map(d => d.name),\n  value: \"BuPu\"\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":14,"value":"chart = {\n  const svg = d3\n    .create(\"svg\")\n    .attr(\"width\", width)\n    .attr(\"height\", height);\n\n  const g = svg.append(\"g\");\n  const polys = g.append(\"g\");\n\n  return Object.assign(svg.node(), {\n    update(settings) {\n      const t = svg.transition().duration(1000);\n\n      const get_key = d => {\n        if (\n          settings.map_id_property === null ||\n          settings.map_id_property === undefined\n        )\n          return d.id;\n        else return d.properties[settings.map_id_property];\n      };\n\n      polys\n        .selectAll(\".polygon\")\n        .data(settings.polygons, d => get_key(d))\n        .join(\n          enter =>\n            enter\n              .append(\"path\")\n              .attr(\"class\", \"polygon\")\n              .attr(\"stroke\", \"none\")\n              .call(selection =>\n                selection\n                  .attr(\"fill\", d =>\n                    settings.color(settings.polygons_data.get(get_key(d)))\n                  )\n                  .attr(\"d\", settings.path)\n                  .append(\"title\")\n                  .text(d => {\n                    const name = get_key(d);\n                    return `${settings.build_label(get_key(d))}`;\n                  })\n              ),\n          update =>\n            update.call(selection =>\n              selection\n                .transition(t)\n                .attr(\"fill\", d =>\n                  settings.color(settings.polygons_data.get(get_key(d)))\n                )\n                .attr(\"d\", settings.path)\n                .select(\"title\")\n                .text(d => {\n                  const name = d.properties[settings.map_id];\n                  return `${settings.build_label(get_key(d))}`;\n                })\n            ),\n          exit =>\n            exit.call(selection =>\n              selection\n                .transition(t)\n                .attr(\"opacity\", 0)\n                .remove()\n            )\n        );\n\n      if (settings.contour_type == \"topojson\") {\n        g.append(\"path\")\n          .datum(settings.contour_path)\n          .attr(\"class\", \"contour\")\n          .call(settings.format_contour);\n      }\n      if (settings.contour_type == \"geojson\") {\n        g.append(\"g\")\n          .selectAll(\".contour\")\n          .data(settings.contour_path)\n          .join(\"path\")\n          .attr(\"class\", \"contour\")\n          .call(settings.format_contour);\n      }\n\n      svg.select(\".legend\").remove();\n      svg\n        .append(settings.legend)\n        .attr(\"transform\", settings.legend_translation);\n\n      const zoomf = d3\n        .zoom()\n        .scaleExtent([1, 8])\n        .on(\"zoom\", zoomed);\n\n      function zoomed() {\n        const { transform } = d3.event;\n        g.attr(\"transform\", transform);\n        g.attr(\"stroke-width\", 1 / transform.k);\n        g.selectAll(\".contour\").attr(\n          \"stroke-width\",\n          settings.contour_width / transform.k\n        );\n      }\n\n      if (settings.zoom) svg.call(zoomf);\n    }\n  });\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":926,"value":"draw = chart.update(settings)","pinned":false,"mode":"js","data":null,"name":null},{"id":644,"value":"sec_settings = md`## Settings`","pinned":false,"mode":"js","data":null,"name":null},{"id":1005,"value":"md`Chart height :`","pinned":false,"mode":"js","data":null,"name":null},{"id":504,"value":"height = 950","pinned":false,"mode":"js","data":null,"name":null},{"id":1020,"value":"md`### Data\n\nThese settings are related to the data used to color polygons. \\`data\\` is the correspondig d3 array, \\`data_ID\\` the data column which contains the polygon ids, \\`data_name\\` the data column which contains the polygon names (showed in the tooltip), \\`data_var1\\` and \\`data_var2\\` the column names of the two displayed variables.`","pinned":false,"mode":"js","data":null,"name":null},{"id":597,"value":"data = d3.csvParse(await FileAttachment(\"dipl2016@3.csv\").text())","pinned":false,"mode":"js","data":null,"name":null},{"id":808,"value":"data_id = \"ID\"","pinned":false,"mode":"js","data":null,"name":null},{"id":811,"value":"data_name = \"LIBGEO\"","pinned":false,"mode":"js","data":null,"name":null},{"id":814,"value":"data_var1 = \"Ens. sup.\"","pinned":false,"mode":"js","data":null,"name":null},{"id":816,"value":"data_var2 = \"Dipl. min.\"","pinned":false,"mode":"js","data":null,"name":null},{"id":1024,"value":"md`### Map\n\\`map\\` is the spatial object containing the polygons. It can be specified either in geoJSON or topoJSON format, and can be either an URL or a JSON String. \\`map_property_id\\` is the name of the property containing the polygon ids (which must match with \\`data_ID\\`, and \\`map_object\\` is the name of the spatial object containing the polygons). If \\`null\\`, the \\`id\\` is used directly instead of a property.`","pinned":false,"mode":"js","data":null,"name":null},{"id":511,"value":"map = \"https://raw.githubusercontent.com/AtelierCartographie/Khartis/master/public/data/map/FR-com-2016/france.json\"","pinned":false,"mode":"js","data":null,"name":null},{"id":588,"value":"map_id_property = \"ID\"","pinned":false,"mode":"js","data":null,"name":null},{"id":423,"value":"map_object = \"poly\"","pinned":false,"mode":"js","data":null,"name":null},{"id":1031,"value":"md`### Contour\n\\`contour\\` is an optional spatial object containing borders which would be superimposed over the polygons. As \\`map\\` it can be a string or an URL in geoJSON or topoJSON format. \\`contour_object\\` is the name of the corresponding object, and \\`contour_stroke\\` and \\`contour_width\\` allows to customize the contour appearance.`","pinned":false,"mode":"js","data":null,"name":null},{"id":611,"value":"contour = null","pinned":false,"mode":"js","data":null,"name":null},{"id":615,"value":"contour_object = \"poly\"","pinned":false,"mode":"js","data":null,"name":null},{"id":674,"value":"contour_stroke = \"white\"","pinned":false,"mode":"js","data":null,"name":null},{"id":676,"value":"contour_width = 2","pinned":false,"mode":"js","data":null,"name":null},{"id":1035,"value":"md`### Misc\n\n\\`legend_position\\` can be either an \\`[x,y]\\` numerical array, or a string among \"bottomleft\", \"bottomright\", \"topleft\" or \"topright\". \\`zoom\\` can be set to \\`false\\` to disable zooming and panning.\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":846,"value":"legend_position = \"bottomleft\"","pinned":false,"mode":"js","data":null,"name":null},{"id":685,"value":"zoom = true","pinned":false,"mode":"js","data":null,"name":null},{"id":216,"value":"labels = [\"low\", \"\", \"high\"]","pinned":false,"mode":"js","data":null,"name":null},{"id":703,"value":"projection = \"geoMercator\"","pinned":false,"mode":"js","data":null,"name":null},{"id":647,"value":"md`## Code`","pinned":false,"mode":"js","data":null,"name":null},{"id":942,"value":"settings = ({\n  polygons: polygons,\n  polygons_data: polygons_data,\n  contour_type: contour_type,\n  contour_path: contour_path,\n  path: path,\n  map_id_property: map_id_property,\n  build_label: build_label,\n  contour_width: contour_width,\n  format_contour: format_contour,\n  zoom: zoom,\n  color: color,\n  legend: legend,\n  legend_translation: legend_translation\n})","pinned":false,"mode":"js","data":null,"name":null},{"id":693,"value":"polygons_data = {\n  const data_map = data.map(d => [d[data_id], [d[data_var1], d[data_var2]]]);\n  return Object.assign(new Map(data_map), {\n    title: [data_var1, data_var2]\n  });\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":734,"value":"names_data = {\n  const data_map = data.map(d => [d[data_id], [d[data_name]]]);\n  return new Map(data_map);\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":565,"value":"polygons_merge = {\n  if (map_type == \"topojson\") {\n    return topojson.merge(map_data, map_data.objects[map_object].geometries);\n  }\n  return map_data;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":536,"value":"polygons = {\n  if (map_type == \"topojson\") {\n    return topojson.feature(map_data, map_object).features;\n  }\n  // geojson\n  if (map_type == \"geojson\") {\n    return map_data.features;\n  }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":912,"value":"map_type = {\n  if (map_data.type == \"Topology\") return \"topojson\";\n  if (map_data.type == \"FeatureCollection\") return \"geojson\";\n  return null;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":830,"value":"map_data = {\n  if (typeof map == \"object\") return map;\n  if (typeof map == \"string\") {\n    if (map.slice(0, 4) == \"http\") {\n      return d3.json(map);\n    } else {\n      return JSON.parse(map);\n    }\n  }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":893,"value":"format_contour = selection => {\n  selection\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", contour_stroke)\n    .attr(\"stroke-width\", contour_width)\n    .attr(\"stroke-linejoin\", \"round\")\n    .attr(\"d\", path);\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":546,"value":"contour_path = {\n  if (contour === null) return null;\n  if (contour_type == \"topojson\") {\n    return topojson.mesh(\n      contour_data,\n      contour_data.objects[contour_object],\n      (a, b) => a !== b\n    );\n  }\n  if (contour_type == \"geojson\") {\n    return contour_data.features;\n  }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":838,"value":"contour_data = {\n  if (typeof contour == \"object\") return contour;\n  if (typeof contour == \"string\") {\n    if (contour.slice(0, 4) == \"http\") {\n      return d3.json(contour);\n    } else {\n      return JSON.parse(contour);\n    }\n  }\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":903,"value":"contour_type = {\n  if (contour !== null && contour_data.type == \"Topology\") return \"topojson\";\n  if (contour !== null && contour_data.type == \"FeatureCollection\")\n    return \"geojson\";\n  return null;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":395,"value":"path = d3.geoPath(d3[projection]().fitSize([width, height], polygons_merge))","pinned":false,"mode":"js","data":null,"name":null},{"id":75,"value":"build_label = id => {\n  const name = names_data.get(id);\n  const label_data = polygons_data.get(id);\n  if (!label_data)\n    return `${name}\nN/A`;\n  let [a, b] = label_data;\n  return `${name}\n${a} ${polygons_data.title[0]}${labels[x(a)] && ` (${labels[x(a)]})`}\n${b} ${polygons_data.title[1]}${labels[y(b)] && ` (${labels[y(b)]})`}`;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":849,"value":"legend_translation = {\n  let res;\n  if (Array.isArray(legend_position)) {\n    res = legend_position;\n  }\n  if (typeof legend_position == \"string\") {\n    switch (legend_position) {\n      case \"topleft\":\n        res = [100, 100];\n        break;\n      case \"topright\":\n        res = [width - 100, 100];\n        break;\n      case \"bottomleft\":\n        res = [100, height - 100];\n        break;\n      case \"bottomright\":\n        res = [width - 100, height - 100];\n        break;\n    }\n  }\n  return `translate(${res[0]}, ${res[1]})`;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":126,"value":"legend = () => {\n  const k = 24;\n  const arrow = DOM.uid();\n  return svg`<g font-family=sans-serif font-size=10 class=\"legend\">\n  <g transform=\"translate(-${(k * n) / 2},-${(k * n) / 2}) rotate(-45 ${(k *\n    n) /\n    2},${(k * n) / 2})\">\n    <rect fill=\"white\" width=\"110\" height=\"110\" transform=\"translate(-22, -16)\"/>\n    <marker id=\"${\n      arrow.id\n    }\" markerHeight=10 markerWidth=10 refX=6 refY=3 orient=auto>\n      <path d=\"M0,0L9,3L0,6Z\" />\n    </marker>\n    ${d3.cross(d3.range(n), d3.range(n)).map(\n      ([i, j]) => svg`<rect width=${k} height=${k} x=${i * k} y=${(n - 1 - j) *\n        k} fill=${colors[j * n + i]}>\n      <title>${polygons_data.title[0]}${labels[j] && ` (${labels[j]})`}\n${polygons_data.title[1]}${labels[i] && ` (${labels[i]})`}</title>\n    </rect>`\n    )}\n    <line marker-end=\"${arrow}\" x1=0 x2=${n * k} y1=${n * k} y2=${n *\n    k} stroke=black stroke-width=1.5 />\n    <line marker-end=\"${arrow}\" y2=0 y1=${n *\n    k} stroke=black stroke-width=1.5 />\n    <text font-weight=\"bold\" dy=\"0.71em\" transform=\"rotate(90) translate(${(n /\n      2) *\n      k},6)\" text-anchor=\"middle\">${escape_html(polygons_data.title[0])}</text>\n    <text font-weight=\"bold\" dy=\"0.71em\" transform=\"translate(${(n / 2) *\n      k},${n * k + 6})\" text-anchor=\"middle\">${escape_html(\n    polygons_data.title[1]\n  )}</text>\n  </g>\n</g>`;\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":359,"value":"schemes = [\n  {\n    name: \"RdBu\", \n    colors: [\n      \"#e8e8e8\", \"#e4acac\", \"#c85a5a\",\n      \"#b0d5df\", \"#ad9ea5\", \"#985356\",\n      \"#64acbe\", \"#627f8c\", \"#574249\"\n    ]\n  },\n  {\n    name: \"BuPu\", \n    colors: [\n      \"#e8e8e8\", \"#ace4e4\", \"#5ac8c8\",\n      \"#dfb0d6\", \"#a5add3\", \"#5698b9\", \n      \"#be64ac\", \"#8c62aa\", \"#3b4994\"\n    ]\n  },\n  {\n    name: \"GnBu\", \n    colors: [\n      \"#e8e8e8\", \"#b5c0da\", \"#6c83b5\",\n      \"#b8d6be\", \"#90b2b3\", \"#567994\",\n      \"#73ae80\", \"#5a9178\", \"#2a5a5b\"\n    ]\n  },\n  {\n    name: \"PuOr\", \n    colors: [\n      \"#e8e8e8\", \"#e4d9ac\", \"#c8b35a\",\n      \"#cbb8d7\", \"#c8ada0\", \"#af8e53\",\n      \"#9972af\", \"#976b82\", \"#804d36\"\n    ]\n  }\n]","pinned":false,"mode":"js","data":null,"name":null},{"id":981,"value":"colors = schemes.filter(d => d.name == color_scheme)[0].colors","pinned":false,"mode":"js","data":null,"name":null},{"id":69,"value":"color = {\n  return value => {\n    if (!value) return \"#ccc\";\n    let [a, b] = value;\n    return colors[y(b) + x(a) * n];\n  };\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":207,"value":"n = Math.floor(Math.sqrt(colors.length))","pinned":false,"mode":"js","data":null,"name":null},{"id":209,"value":"x = d3.scaleQuantile(Array.from(polygons_data.values(), d => d[0]), d3.range(n))","pinned":false,"mode":"js","data":null,"name":null},{"id":204,"value":"y = d3.scaleQuantile(Array.from(polygons_data.values(), d => d[1]), d3.range(n))","pinned":false,"mode":"js","data":null,"name":null},{"id":1066,"value":"escape_html = str =>\n  str\n    .replace(/&/g, \"&amp;\")\n    .replace(/</g, \"&lt;\")\n    .replace(/>/g, \"&gt;\")\n    .replace(/\"/g, \"&quot;\")\n    .replace(/'/g, \"&#039;\")","pinned":false,"mode":"js","data":null,"name":null},{"id":637,"value":"md`## Imports`","pinned":false,"mode":"js","data":null,"name":null},{"id":6,"value":"topojson = require(\"topojson-client@3\")","pinned":false,"mode":"js","data":null,"name":null},{"id":973,"value":"import { select } from \"@jashkenas/inputs\"","pinned":false,"mode":"js","data":null,"name":null},{"id":3,"value":"d3 = require(\"d3@5\", \"d3-fetch\")","pinned":false,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}