{"id":"b6c566a3b3a914d8","slug":"transitions-from-maps-to-diagrams-part-1","trashed":false,"description":"","likes":40,"publish_level":"live","forks":3,"fork_of":null,"has_importers":false,"update_time":"2018-05-25T05:44:36.435Z","first_public_version":716,"paused_version":null,"publish_time":"2023-04-27T08:47:25.833Z","publish_version":716,"latest_version":716,"thumbnail":"3b9a10cb378721661afd6288eb1b2afc580dc11903cd919f481063a4bec5cc7c","default_thumbnail":"3b9a10cb378721661afd6288eb1b2afc580dc11903cd919f481063a4bec5cc7c","roles":[],"sharing":null,"owner":{"id":"5952e5f3991fd83c","avatar_url":"https://avatars.observableusercontent.com/avatar/6a0df0eeae726b31981d0e7972c010612aa76ce1f8d25615d7a1a554faf9ae1e","login":"floledermann","name":"Florian Ledermann","bio":"Maps, Infovis, Code","home_url":"https://twitter.com/floledermann","type":"team","tier":"starter_2024"},"creator":{"id":"bb2fc280c71d473c","avatar_url":"https://avatars.observableusercontent.com/avatar/6a0df0eeae726b31981d0e7972c010612aa76ce1f8d25615d7a1a554faf9ae1e","login":"floledermann","name":"Florian Ledermann","bio":"Maps, Infovis, Code","home_url":"https://mapstodon.space/@floledermann","tier":"public"},"authors":[{"id":"bb2fc280c71d473c","avatar_url":"https://avatars.observableusercontent.com/avatar/6a0df0eeae726b31981d0e7972c010612aa76ce1f8d25615d7a1a554faf9ae1e","name":"Florian Ledermann","login":"floledermann","bio":"Maps, Infovis, Code","home_url":"https://mapstodon.space/@floledermann","tier":"public","approved":true,"description":""}],"collections":[{"id":"6a8c44e5a4065574","type":"public","slug":"tutorials","title":"Tutorials","description":"","update_time":"2019-06-25T09:39:53.629Z","pinned":false,"ordered":false,"custom_thumbnail":null,"default_thumbnail":"cf6279346499776d90c6b217567f9c69517b2a30601af60b02d945324379700c","thumbnail":"cf6279346499776d90c6b217567f9c69517b2a30601af60b02d945324379700c","listing_count":6,"parent_collection_count":0,"owner":{"id":"5952e5f3991fd83c","avatar_url":"https://avatars.observableusercontent.com/avatar/6a0df0eeae726b31981d0e7972c010612aa76ce1f8d25615d7a1a554faf9ae1e","login":"floledermann","name":"Florian Ledermann","bio":"Maps, Infovis, Code","home_url":"https://twitter.com/floledermann","type":"team","tier":"starter_2024"}}],"files":[],"comments":[],"commenting_lock":null,"suggestion_from":null,"suggestions_to":[],"version":716,"title":"Transitions from Maps to Diagrams, Part 1","license":null,"copyright":"","nodes":[{"id":0,"value":"md`# Transitions from Maps to Diagrams, Part 1\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":439,"value":"md`This is the first part of a [two-part tutorial](/@floledermann/transitions-from-maps-to-diagrams-part-2) on creating animated transitions between map objects and diagrams using D3. This technique will allow you to create maps simliar to the [map of street names and gender in Vienna](http://genderatlas.at/articles/strassennamen.html) that I created for the  [genderATlas project](http://genderatlas.at/).\n\nIf you are new to creating maps with D3, you may want to have a look at my [tutorial on drawing maps from geodata in D3](https://beta.observablehq.com/@floledermann/drawing-maps-from-geodata-in-d3) first for an introduction of the basic techniques used to render geodata in D3.\n\nIn this part, we will start with transitioning a single entity on the map to a more abstract representation - like a straight line, for example. Here's what the result of this part of the tutorial will look like:`","pinned":false,"mode":"js","data":null,"name":null},{"id":503,"value":"preview = {\n  \n  // create SVG element\n  let svg = d3.select(DOM.svg(width, height))\n  svg.style('display', 'block')\n  \n  // construct the element\n  svg.append('path')\n    .datum(street)\n    .attr('d', pathGenerator)\n    .attr('fill', 'none')\n    .attr('stroke', '#999999')\n    .attr('stroke-width', '2')\n  \n  let button = html`<input type=\"button\" value=\"Transition to Diagram\">`\n  button.style.marginBottom = \"0.5em\"\n  \n  let isMap = true\n  \n  function anim() {\n    svg.select('path')\n      .interrupt()          // cancel any ongoing transitions\n      .transition()\n      .duration(1000)       \n      .ease(d3.easeLinear)  // use linear motion\n      .attr('d',isMap ? diagramPath : mapPath)\n      .on('end', function() { d3.timeout(anim, 1000) })\n    \n    button.value = isMap ? \"Transition to Map\" : \"Transition to Diagram\"\n    \n    isMap = !isMap\n  }\n  \n  d3.timeout(anim, 1500)\n\n  // pass to Observable to represent this block\n  return svg.node()\n  \n}","pinned":false,"mode":"js","data":null,"name":null},{"id":447,"value":"md`----\n## Step 1: Loading the geometry, setting up the projection and path generator`","pinned":false,"mode":"js","data":null,"name":null},{"id":611,"value":"md`These steps are covered in my [basic tutorial](https://beta.observablehq.com/@floledermann/drawing-maps-from-geodata-in-d3), so I will not explain the details here.\n\nHere's our GeoJSON data, containing all the streets of Vienna (extracted from [OpenStreetMap](https://www.openstreetmap.org/) using the [Overpass API](https://overpass-turbo.eu/)):`","pinned":false,"mode":"js","data":null,"name":null},{"id":44,"value":"streets = d3.json('https://gist.githubusercontent.com/floledermann/92a56b764a92b55a857f5115236146b2/raw/b80ac35cc40de5651c6715c6abc4f8dd23b78c5e/streetnames_gender.geojson')","pinned":false,"mode":"js","data":null,"name":null},{"id":644,"value":"md`We can retrieve a single street object from our geodata, using [\\`Array.filter()\\`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) to extract the street of a given name from the array of streets:`","pinned":false,"mode":"js","data":null,"name":null},{"id":646,"value":"street = streets.features.filter(street => street.properties.name == \"Billrothstraße\")[0]","pinned":true,"mode":"js","data":null,"name":null},{"id":460,"value":"md`Set up the projection and a path generator (the parameters [\\`width\\`](#width), [\\`height\\`](#height) and [\\`margin\\`](#margin) are defined in the [appendix](#width) of this notebook - you can adjust them there, if you want, to interactively change the size of all the maps in this notebook):`","pinned":false,"mode":"js","data":null,"name":null},{"id":191,"value":"projection = d3.geoMercator().fitExtent([[margin, margin], [width - margin, height - margin]], street)","pinned":true,"mode":"js","data":null,"name":null},{"id":167,"value":"pathGenerator = d3.geoPath().projection(projection)","pinned":true,"mode":"js","data":null,"name":null},{"id":471,"value":"md`----\n## Step 2: Plotting the street to the map`","pinned":false,"mode":"js","data":null,"name":null},{"id":329,"value":"md`We can use the path generator to transform the GeoJSON object into an [SVG path expression](https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths), and plot the \\`path\\` representing the street to the map - that's the basic pattern for mapping out geodata in D3:`","pinned":false,"mode":"js","data":null,"name":null},{"id":466,"value":"mapPath = pathGenerator(street)","pinned":true,"mode":"js","data":null,"name":null},{"id":57,"value":"{\n  \n  // create SVG element\n  let svg = d3.select(DOM.svg(width, height))\n  \n  // construct the element\n  svg.append('path')\n    .datum(street)\n    .attr('d', pathGenerator)\n    .attr('fill', 'none')\n    .attr('stroke', '#999999')\n    .attr('stroke-width', '1.5')\n  \n  // pass to Observable to represent this block\n  return svg.node()\n  \n}","pinned":false,"mode":"js","data":null,"name":null},{"id":475,"value":"md`----\n## Step 3: Linearizing the path`","pinned":false,"mode":"js","data":null,"name":null},{"id":521,"value":"md`D3 comes with built-in [path string interpolation](https://github.com/d3/d3-interpolate#interpolateString) - this means we can simply supply an alternative value for the path's \\`d\\` attribute, and D3 will take care of smoothly interpolating all the vertices of the path from their old to their new position. There is only one caveat: to make the animation look smooth, both path expressions must have the same number of coordinates, so that points in the original expression can be matched up with their new coordinates in the new expression.\n\nFor transitioning the street geometry to a horizontal line, this means we need to come up with the coordinates for that line, containing *as many subdivisions as the original path expression*, for the new value of the path's geometry.\n\nThe *y* coordinate of all points is the same for a horizontal line - let's say we want to draw the line at the map's top margin. For the *x* coordinates, we want to start at the left margin position, and then equally divide the available space to the right margin in as many segments as we need.\n\nWe can write a simple function that produces the coordinates of a point on the line, taking an *alpha* value in the range between 0 and 1 - passing in 0 will give the leftmost point, passing in 1 will give the rightmost point, and passing any value in between will give a point at the proportional distance between the two:\n`","pinned":false,"mode":"js","data":null,"name":null},{"id":250,"value":"getLinearCoords = function(pos) {\n  return [Math.round(margin+(width-2*margin)*pos), margin];\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":531,"value":"md`To compose the new path expression from these coordinates, we need a function that processes the original expression, and replaces each drawing command with an 'L' (lineto) or 'M' (moveto) command, substituting the normalized coordinates for the original ones. The \\`normalizePath\\` function below does just that.\n\n'M' (moveto) commands within a path lead to gaps in the normalized path (because the line will skip from one coordinate to the next). So if we want to close those gaps, we can simply re-use the last coordinate pair for the destination of 'M', effectively reducing the gap to a length of zero. This is what the \\`closeGaps\\` parameter controls.`","pinned":false,"mode":"js","data":null,"name":null},{"id":107,"value":"normalizePath = function(path, geometryFunction, closeGaps) {\n\n  // geometryFunction will be passed a float index from 0..1 and should return a point\n  // on the normalized geometry at that position\n  \n  if (closeGaps === undefined) closeGaps = true;  // close gaps by default\n\n  let command_letters = 'mlhvaqtcs',\n      commands = [],\n      newPath = '';\n  \n  let i;\n\n  // find all command letters in the path\n  // preserve 'M' (moveto) commands, replace all other commends by 'L' (lineto)\n  for (i=0; i<path.length; i++) {\n    let c = path[i].toLowerCase();\n    for (var j=0; j<command_letters.length; j++) {\n      if (c == command_letters[j]) {\n        if (c == 'm') commands.push('M');\n        else commands.push('L');\n        break;\n      }\n    }\n  }\n  \n  let num = commands.length,\n      coords;\n\n  // assemble new path string, using the replaced commands and\n  // the normalized coordinates produced by geometryFunction\n  if (num == 1) {\n    // special case: single command (does not usually happen)\n    coords = geometryFunction(0);\n    newPath = commands[0] + coords[0] + ',' + coords[1];\n  }\n  else {\n    for (i=0; i<num; i++) {\n      newPath += commands[i];\n      if (closeGaps && commands[i] == 'M' && i>0) {\n        // bridge gaps caused by (moveto) commands by using previous coordinates\n        coords = geometryFunction((i-1)/(num-1));\n      }\n      else {\n        coords = geometryFunction(i/(num-1));\n      }\n      newPath += coords[0] + ',' + coords[1];\n    }\n    return newPath;\n  }\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":597,"value":"md`----\n## Step 4: Putting it all together`","pinned":false,"mode":"js","data":null,"name":null},{"id":595,"value":"md`We can now generate our normalized path, and put together the code for triggering the transition:`","pinned":false,"mode":"js","data":null,"name":null},{"id":248,"value":"diagramPath = normalizePath(mapPath, getLinearCoords)","pinned":true,"mode":"js","data":null,"name":null},{"id":536,"value":"streetSVG = {\n  \n  // create SVG element\n  let svg = d3.select(DOM.svg(width, height))\n  \n  // construct the element\n  svg.append('path')\n    .datum(street)\n    .attr('d', pathGenerator)\n    .attr('fill', 'none')\n    .attr('stroke', '#999999')\n    .attr('stroke-width', '2')\n  \n  // pass to Observable to represent this block\n  return svg.node()\n  \n}","pinned":false,"mode":"js","data":null,"name":null},{"id":96,"value":"button = {\n  \n  let button = html`<input type=\"button\" value=\"Transition to Diagram\">`\n  button.style.fontSize = \"1.2em\";\n  \n  let isMap = true\n  \n  button.addEventListener('click', function() {\n    d3.select(streetSVG).select('path')\n      .interrupt()          // cancel any ongoing transitions\n      .transition()\n      .duration(3000)       // make it sloooooow\n      .ease(d3.easeLinear)  // use linear motion\n      .attr('d',isMap ? diagramPath : mapPath)\n    \n    button.value = isMap ? \"Transition to Map\" : \"Transition to Diagram\"\n    \n    isMap = !isMap\n  })\n  return button\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":578,"value":"md`... of course we could transition to a different geometry, e.g. to a circle / pie chart, by simply supplying a different function for calculating the normalized geometry -- this time without closing the gaps, and also transitioning to a different line width...`","pinned":false,"mode":"js","data":null,"name":null},{"id":548,"value":"getCircleCoords = function(pos) {\n  \n  let center = [width/2, height/2],\n      r = Math.min(width, height)/2 - margin\n  \n  // return coordinates on a half-circle\n  return [center[0] + Math.sin((pos-0.5)*Math.PI) * r, center[1] + Math.cos((-pos-0.5)*Math.PI) * r];\n}","pinned":true,"mode":"js","data":null,"name":null},{"id":549,"value":"circlePath = normalizePath(mapPath, getCircleCoords, false)","pinned":true,"mode":"js","data":null,"name":null},{"id":554,"value":"streetSVG2 = {\n  \n  // create SVG element\n  let svg = d3.select(DOM.svg(width, height))\n  \n  // construct the element\n  svg.append('path')\n    .datum(street)\n    .attr('d', circlePath)\n    .attr('fill', 'none')\n    .attr('stroke', '#999999')\n    .attr('stroke-width', 4)\n  \n  // pass to Observable to represent this block\n  return svg.node()\n  \n}","pinned":false,"mode":"js","data":null,"name":null},{"id":556,"value":"{\n  \n  let button = html`<input type=\"button\" value=\"Transition to Map\">`\n  button.style.fontSize = \"1.2em\";\n  \n  let isMap = false\n  \n  button.addEventListener('click', function() {\n    d3.select(streetSVG2).select('path')\n      .interrupt()          // cancel any ongoing transitions\n      .transition()\n      .duration(1000)       // make it looooong\n      .ease(d3.easeLinear)  // use linear motion\n      .attr('d',isMap ? circlePath : mapPath)\n      .attr('stroke-width', isMap ? 4 : 2)\n    \n    button.value = isMap ? \"Transition to Map\" : \"Transition to Diagram\"\n    \n    isMap = !isMap\n  })\n  return button\n}","pinned":false,"mode":"js","data":null,"name":null},{"id":626,"value":"md`In [part 2 of the tutorial](https://beta.observablehq.com/@floledermann/transitions-from-maps-to-diagrams-part-2), we will line up all the streets of Vienna to form a bar chart.\n\nFeedback? Comments? Questions? You can contact me on [Twitter](https://twitter.com/floledermann)!`","pinned":false,"mode":"js","data":null,"name":null},{"id":302,"value":"html`<hr>` ","pinned":false,"mode":"js","data":null,"name":null},{"id":487,"value":"md`### Appendix`","pinned":false,"mode":"js","data":null,"name":null},{"id":431,"value":"md`#### Map parameters:\n\n(Change these to modify the size & layout of the maps in this notebook)`","pinned":false,"mode":"js","data":null,"name":null},{"id":79,"value":"viewof width = html`<input type=\"number\" value=\"300\" min=\"100\" max=\"800\" step=\"20\" style=\"width: 5em\">`","pinned":false,"mode":"js","data":null,"name":null},{"id":83,"value":"viewof height = html`<input type=\"number\" value=\"300\" min=\"100\" max=\"600\" step=\"20\" style=\"width: 5em\">`","pinned":false,"mode":"js","data":null,"name":null},{"id":240,"value":"viewof margin = html`<input type=\"number\" value=\"20\" min=\"0\" max=\"${Math.min(width, height)/4}\" step=\"5\" style=\"width: 5em\">`","pinned":false,"mode":"js","data":null,"name":null},{"id":667,"value":"// currently unused\nviewof streetName = html`<select>\n<option selected>Billrothstraße</option>\n<option>Wilhelminenstraße</option>\n</input>`","pinned":false,"mode":"js","data":null,"name":null},{"id":434,"value":"md`#### Libraries:`","pinned":false,"mode":"js","data":null,"name":null},{"id":38,"value":"d3 = require(\"https://d3js.org/d3.v5.min.js\")","pinned":false,"mode":"js","data":null,"name":null},{"id":484,"value":"md`#### Custom CSS:`","pinned":false,"mode":"js","data":null,"name":null},{"id":464,"value":"style = html`\n[CSS style declaration]\n<style>\nhr {\n  /* make HR a little darker */\n  background-image: linear-gradient(90deg, rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.25));\n  margin: 1em 0;\n}\n\ncode {\n  /* make code snippets in text stand out more */\n  font-weight: bold;\n  background-color: #f0f0f0;\n  padding: 0.1em 0.4em;\n}\n\nsvg {\n  background-color: #fafafa;\n  border: 1px solid #dddddd;\n}\n</style>\n`","pinned":false,"mode":"js","data":null,"name":null}],"resolutions":[],"schedule":null,"last_view_time":null}