Skip to content

Instantly share code, notes, and snippets.

@gopperman
Last active March 5, 2019 04:01
Show Gist options
  • Save gopperman/2a67b764ef28935c3854eba41985a710 to your computer and use it in GitHub Desktop.
Save gopperman/2a67b764ef28935c3854eba41985a710 to your computer and use it in GitHub Desktop.
Worcester
license: mit
height: 500
border: no

Map of my badge progress in Worcester, Massachusetts Pokemon gyms.

Made with blockup

body,svg{font-family:sans-serif}.info__title,tspan{font-weight:700}*{box-sizing:border-box}body{color:#666;font-size:1em}main{max-width:calc(125vh);margin:0 auto}.footnote{color:#999;font-style:italic;font-size:.8em}svg{width:100%;stroke-linejoin:round;stroke-linecap:round}path.feature{stroke-width:1px;stroke:#222}.background{stroke:#dedede;fill:#fafafa}.geopath{stroke:#666;stroke-width:.5px;opacity:.8}.info__title{font-size:1.6em}.info__summary{fill:#b7e1cd}.divider{fill:#222;font-weight:400}.to-do,.todo{fill:#f4c7c3}.bronze{fill:#cd7f32}.silver{fill:#999}.gold{fill:gold}.gym{background:#dedede;border:1px solid #fff;display:inline-block;float:left;position:relative;height:15px;width:15px;transition:all .2s}.gym--bronze{background:#cd7f32}.gym--silver{background:#999}.gym--gold{background:gold}.gym__info{display:none}.gym__title{font-size:1em;font-weight:700}.gym:hover{border:1px solid navy;cursor:pointer}.gym:hover .gym__info{background:#fff;border:2px solid navy;font-size:.8em;padding:5px;display:block;position:absolute;left:0;bottom:15px;width:195px;z-index:99}.gym:hover .gym__info p{margin:0}.gymlist{display:-ms-grid;display:grid;-ms-grid-columns:(minmax(15px,1fr)) [auto-fill];grid-template-columns:repeat(auto-fill,minmax(15px,1fr));grid-gap:0;width:100%}
!function(g){function n(c){if(t[c])return t[c].exports;var e=t[c]={i:c,l:!1,exports:{}};return g[c].call(e.exports,e,e.exports,n),e.l=!0,e.exports}var t={};n.m=g,n.c=t,n.i=function(g){return g},n.d=function(g,t,c){n.o(g,t)||Object.defineProperty(g,t,{configurable:!1,enumerable:!0,get:c})},n.n=function(g){var t=g&&g.__esModule?function(){return g.default}:function(){return g};return n.d(t,"a",t),t},n.o=function(g,n){return Object.prototype.hasOwnProperty.call(g,n)},n.p="",n(n.s=1)}([function(module,exports,__webpack_require__){"use strict";eval('\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nvar geoJson = function geoJson(gym) {\n return {\n geometry: {\n type: "Point",\n coordinates: [gym.gsx$long.$t, gym.gsx$lat.$t]\n },\n type: "Feature",\n "properties": {\n name: gym.gsx$name.$t,\n address: gym.gsx$address.$t,\n neighborhood: gym.gsx$neighborhood.$t,\n city: gym.gsx$city.$t,\n state: gym.gsx$state.$t,\n zip: gym.gsx$zip.$t,\n badge: gym.gsx$gopperman.$t\n }\n };\n};\n\nvar gymSummary = function gymSummary(data) {\n var badges = data.reduce(function (acc, item) {\n var badge = item.gsx$gopperman.$t.toLowerCase();\n var count = _.get(acc, badge, 0) + 1;\n\n acc[badge] = count;\n return acc;\n }, {});\n\n var total = data.length,\n bronze = badges.bronze || 0,\n silver = badges.silver || 0,\n gold = badges.gold || 0;\n\n return {\n total: total,\n bronze: bronze,\n silver: silver,\n gold: gold,\n goldComplete: gold / total,\n silverComplete: (silver + gold) / total || 1,\n bronzeComplete: (silver + gold + bronze) / total || 1,\n todo: badges[\'to-do\'] || 0\n };\n};\n\nvar pctForDisplay = function pctForDisplay(num) {\n return (num * 100).toFixed(1);\n};\n\nvar color = d3.scaleLinear().domain([1, 100]).interpolate(d3.interpolateHcl).range([d3.rgb("#CC211F"), d3.rgb(\'#FFD700\')]);\n\nexports.geoJson = geoJson;\nexports.color = color;\nexports.gymSummary = gymSummary;\nexports.pctForDisplay = pctForDisplay;//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMC5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy91dGlsLmpzPzYzZjMiXSwic291cmNlc0NvbnRlbnQiOlsiY29uc3QgZ2VvSnNvbiA9IChneW0pID0+IHtcbiAgcmV0dXJuIHtcbiAgICBnZW9tZXRyeToge1xuICAgICAgdHlwZTogXCJQb2ludFwiLFxuICAgICAgY29vcmRpbmF0ZXM6IFtcbiAgICAgICAgZ3ltLmdzeCRsb25nLiR0LFxuICAgICAgICBneW0uZ3N4JGxhdC4kdFxuICAgICAgXVxuICAgIH0sXG4gICAgdHlwZTogXCJGZWF0dXJlXCIsXG4gICAgXCJwcm9wZXJ0aWVzXCI6IHtcbiAgICAgIG5hbWU6IGd5bS5nc3gkbmFtZS4kdCxcbiAgICAgIGFkZHJlc3M6IGd5bS5nc3gkYWRkcmVzcy4kdCxcbiAgICAgIG5laWdoYm9yaG9vZDogZ3ltLmdzeCRuZWlnaGJvcmhvb2QuJHQsXG4gICAgICBjaXR5OiBneW0uZ3N4JGNpdHkuJHQsXG4gICAgICBzdGF0ZTogZ3ltLmdzeCRzdGF0ZS4kdCxcbiAgICAgIHppcDogZ3ltLmdzeCR6aXAuJHQsXG4gICAgICBiYWRnZTogZ3ltLmdzeCRnb3BwZXJtYW4uJHRcbiAgICB9XG4gIH1cbn1cblxuY29uc3QgZ3ltU3VtbWFyeSA9IChkYXRhKSA9PiB7XG4gIGNvbnN0IGJhZGdlcyA9IGRhdGEucmVkdWNlKChhY2MsIGl0ZW0pID0+IHtcbiAgICAgIGNvbnN0IGJhZGdlID0gaXRlbS5nc3gkZ29wcGVybWFuLiR0LnRvTG93ZXJDYXNlKClcbiAgICAgIGNvbnN0IGNvdW50ID0gXy5nZXQoYWNjLCBiYWRnZSwgMCkgKyAxXG5cbiAgICAgIGFjY1tiYWRnZV0gPSBjb3VudFxuICAgICAgcmV0dXJuIGFjY1xuICB9LCB7fSlcblxuICBjb25zdCB0b3RhbCA9IGRhdGEubGVuZ3RoLFxuICAgIGJyb256ZSA9IGJhZGdlcy5icm9uemUgfHwgMCxcbiAgICBzaWx2ZXIgPSBiYWRnZXMuc2lsdmVyIHx8IDAsXG4gICAgZ29sZCA9IGJhZGdlcy5nb2xkIHx8IDBcblxuICByZXR1cm4ge1xuICAgIHRvdGFsOiB0b3RhbCxcbiAgICBicm9uemUsXG4gICAgc2lsdmVyLFxuICAgIGdvbGQsXG4gICAgZ29sZENvbXBsZXRlOiAoZ29sZCAvIHRvdGFsKSxcbiAgICBzaWx2ZXJDb21wbGV0ZTogKChzaWx2ZXIgKyBnb2xkKSAvIHRvdGFsKSB8fCAxLFxuICAgIGJyb256ZUNvbXBsZXRlOiAoKHNpbHZlciArIGdvbGQgKyBicm9uemUpIC8gdG90YWwpIHx8IDEsXG4gICAgdG9kbzogYmFkZ2VzWyd0by1kbyddIHx8IDAsXG4gIH1cbn1cblxuY29uc3QgcGN0Rm9yRGlzcGxheSA9IChudW0pID0+IHtcbiAgcmV0dXJuIChudW0gKiAxMDApLnRvRml4ZWQoMSlcbn1cblxuY29uc3QgY29sb3IgPSBkMy5zY2FsZUxpbmVhcigpLmRvbWFpbihbMSwxMDBdKVxuICAuaW50ZXJwb2xhdGUoZDMuaW50ZXJwb2xhdGVIY2wpXG4gIC5yYW5nZShbZDMucmdiKFwiI0NDMjExRlwiKSwgZDMucmdiKCcjRkZENzAwJyldKVxuXG5leHBvcnQge1xuICBnZW9Kc29uLFxuICBjb2xvcixcbiAgZ3ltU3VtbWFyeSxcbiAgcGN0Rm9yRGlzcGxheVxufVxuXG5cblxuLy8gV0VCUEFDSyBGT09URVIgLy9cbi8vIHV0aWwuanMiXSwibWFwcGluZ3MiOiI7Ozs7O0FBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUZBO0FBT0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBUEE7QUFUQTtBQW1CQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUFBO0FBQUE7QUFDQTtBQUlBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQVJBO0FBVUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUlBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///0\n')},function(module,exports,__webpack_require__){"use strict";eval("\n\nvar _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };\n\nvar _util = __webpack_require__(0);\n\nvar margin = { top: 10, right: 30, bottom: 30, left: 30 },\n width = 960 - margin.left - margin.right,\n height = 500 - margin.top - margin.bottom;\n\nvar zips = ['01601', '01602', '01603', '01604', '01605', '01606', '01607', '01608', '01609', '01610', '01613', '01614', '01653', '01654', '01655'];\n\nvar footnote = document.querySelector('.footnote');\nvar centered = void 0,\n gymProgress = void 0,\n worcesterGyms = void 0;\n\nvar drawGyms = function drawGyms(zip) {\n var gyms = worcesterGyms.filter(function (gym) {\n return gym.gsx$zip.$t === zip;\n });\n\n var points = gyms.map(function (gym) {\n return (0, _util.geoJson)(gym);\n });\n\n map.selectAll('circle').data(points).enter().append('circle').attr(\"cx\", function (d) {\n return projection(d.geometry.coordinates)[0];\n }).attr(\"cy\", function (d) {\n return projection(d.geometry.coordinates)[1];\n }).attr(\"r\", \"3px\").attr('class', function (d) {\n return 'geopath ' + d.properties.badge.toLowerCase();\n }).attr('data-name', function (d) {\n return d.properties.name;\n });\n};\n\nvar updateInfo = function updateInfo() {\n var d = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;\n\n var zip = _.get(d, 'properties.ZCTA5CE10', 'overall');\n\n var divider = function divider() {\n infoBody.append('tspan').attr('class', 'divider').attr('dx', 8).attr('dy', 0).text('|');\n };\n\n var badgeSpan = function badgeSpan(badge) {\n var first = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;\n\n var tspan = infoBody.append('tspan').attr('class', badge).text(_.get(gymProgress, zip + '.' + badge));\n\n if (first) {\n tspan.attr('dx', 0).attr('dy', '1.2em');\n } else {\n tspan.attr('dx', 8).attr('dy', 0);\n }\n };\n\n infoTitle.text(zip !== 'overall' ? zip : 'Worcester');\n\n infoBody.selectAll('*').remove();\n\n badgeSpan('todo', true);\n divider(infoBody);\n\n badgeSpan('bronze');\n divider(infoBody);\n\n badgeSpan('silver');\n divider(infoBody);\n\n badgeSpan('gold');\n\n infoBody.append('tspan').attr('class', 'info__summary').attr('x', 20).attr('dy', '1.2em').text('t: ' + _.get(gymProgress, zip + '.total'));\n\n infoBody.append('tspan').attr('class', 'info__summary').attr('dx', 5).attr('dy', 0).text('comp: ' + (0, _util.pctForDisplay)(_.get(gymProgress, zip + '.goldComplete')) + '%');\n};\n\nvar mouseover = function mouseover(d) {\n // WHY DOESNT THIS WORK :(\n d3.select(undefined).style('fill', 'orange');\n\n if (!centered) {\n updateInfo(d);\n }\n};\n\nvar mouseout = function mouseout(d) {\n if (!centered) {\n updateInfo();\n }\n};\n\n// When clicked, zoom in\nvar click = function click(d) {\n var zip = _.get(d, 'properties.ZCTA5CE10');\n var x = void 0,\n y = void 0,\n k = void 0;\n\n map.selectAll('.geopath').remove();\n\n // Compute centroid of the selected path\n if (d && centered !== d) {\n var centroid = geoPathGenerator.centroid(d);\n x = centroid[0];\n y = centroid[1];\n k = 2;\n centered = d;\n } else {\n x = width / 2;\n y = height / 2;\n k = 1;\n centered = null;\n }\n\n // Highlight the clicked area\n map.selectAll('path').style('fill', function (d) {\n return centered && d === centered ? '#B6BFF0' : (0, _util.color)((0, _util.pctForDisplay)(_.get(gymProgress, _.get(d, 'properties.ZCTA5CE10') + '.goldComplete')));\n });\n\n var transformation = 'translate(' + width / 2 + ',' + height / 2 + ') scale(' + k + ') translate(' + -x + ',' + -y + ')';\n\n // Zoom\n map.transition().duration(750).attr('transform', transformation);\n\n if (_.get(d, 'properties', false)) {\n infoTitle.text(zip);\n }\n\n updateInfo(d);\n\n if (centered) {\n drawGyms(zip);\n }\n};\n\n// make D3 aware of the <svg> element in the HTML\nvar svg = d3.select(\"svg\");\n\n// get <svg> width and height from HTML instead of hard-coding values\nvar svgWidth = +svg.attr(\"viewBox\").split(\" \")[2],\n svgHeight = +svg.attr(\"viewBox\").split(\" \")[3];\n\n// define the map projection\nvar projection = d3.geoMercator().translate([svgWidth / 2, svgHeight / 2]).center([-71.8022934, 42.2754136]).rotate([0, 0, 0]).scale(170000);\n\nvar geoPathGenerator = d3.geoPath().projection(projection);\n\nvar background = svg.append('rect').attr('class', 'background').attr('width', '100%').attr('height', '100%').on('click', click);\n\nvar map = svg.append('g').attr('class', 'map');\n\nvar info = svg.append('g').attr('class', 'info');\n\nvar infoTitle = info.append('text').text('Worcester').attr('class', 'info__title').attr('x', 20).attr('y', 45);\n\nvar infoBody = info.append('text').attr('class', 'info__body').attr('x', 20).attr('y', 50);\n\n// Here comes the gym data\nd3.json('https://spreadsheets.google.com/feeds/list/1WDnFBw2ObCwRs3lQSk-dep3lh0jXqoSja59mXXb21as/1/public/values?alt=json').then(function (data) {\n worcesterGyms = _.sortBy(data.feed.entry.filter(function (d) {\n return d.gsx$city.$t === 'Worcester';\n }), function (o) {\n return o.gsx$neighborhood.$t;\n });\n\n gymProgress = _extends({\n overall: (0, _util.gymSummary)(worcesterGyms)\n }, zips.reduce(function (acc, zip) {\n acc[zip] = (0, _util.gymSummary)(worcesterGyms.filter(function (d) {\n return d.gsx$zip.$t === zip;\n }));\n return acc;\n }, {}));\n\n // Update the summary\n footnote.innerHTML = 'Gyms: ' + _.get(gymProgress, 'overall.total', 0) + '; Complete: ' + (0, _util.pctForDisplay)(_.get(gymProgress, 'overall.goldComplete', 0)) + '%';\n\n var gymList = d3.select('.gymlist');\n worcesterGyms.forEach(function (gym) {\n gymList.append('div').attr('class', 'gym gym--' + gym.gsx$gopperman.$t.toLowerCase()).append('div').attr('class', 'gym__info').html('\\n <p class=\"gym__title\">' + gym.gsx$name.$t + '</p>\\n <p>' + gym.gsx$neighborhood.$t + ', ' + gym.gsx$zip.$t + '</p>\\n <p><a target=\"_blank\" href=\"https://www.google.com/maps/search/?api=1&query=' + gym.gsx$lat.$t + ',' + gym.gsx$long.$t + '\">(map)</a></p>\\n ');\n });\n\n // Draw the map\n d3.json(\"https://raw.githubusercontent.com/OpenDataDE/State-zip-code-GeoJSON/master/ma_massachusetts_zip_codes_geo.min.json\").then(function (data) {\n var worcester = data.features.filter(function (d) {\n return zips.includes(d.properties.ZCTA5CE10);\n });\n\n map.selectAll('path').data(worcester).enter().append('path').attr('d', geoPathGenerator).attr('vector-effect', 'non-scaling-stroke').attr('class', 'feature').style('fill', function (d) {\n var zip = _.get(d, 'properties.ZCTA5CE10');\n return (0, _util.color)((0, _util.pctForDisplay)(_.get(gymProgress, zip + '.goldComplete')));\n }).attr('data-zip', function (d) {\n return _.get(d, 'properties.ZCTA5CE10', 'overall');\n }).on('mouseover', mouseover).on('mouseout', mouseout).on('click', click);\n\n updateInfo();\n });\n});//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMS5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9zY3JpcHQuanM/OWE5NSJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBjb2xvciwgZ2VvSnNvbiwgZ3ltU3VtbWFyeSwgcGN0Rm9yRGlzcGxheSB9IGZyb20gJy4vdXRpbC5qcydcblxuY29uc3QgbWFyZ2luID0geyB0b3A6IDEwLCByaWdodDogMzAsIGJvdHRvbTogMzAsIGxlZnQ6IDMwIH0sXG4gIHdpZHRoID0gOTYwIC0gbWFyZ2luLmxlZnQgLSBtYXJnaW4ucmlnaHQsXG4gIGhlaWdodCA9IDUwMCAtIG1hcmdpbi50b3AgLSBtYXJnaW4uYm90dG9tXG5cbmNvbnN0IHppcHMgPSBbXG4gICcwMTYwMScsXG4gICcwMTYwMicsXG4gICcwMTYwMycsXG4gICcwMTYwNCcsXG4gICcwMTYwNScsXG4gICcwMTYwNicsXG4gICcwMTYwNycsXG4gICcwMTYwOCcsXG4gICcwMTYwOScsXG4gICcwMTYxMCcsXG4gICcwMTYxMycsXG4gICcwMTYxNCcsXG4gICcwMTY1MycsXG4gICcwMTY1NCcsXG4gICcwMTY1NSdcbl1cblxuY29uc3QgZm9vdG5vdGUgPSBkb2N1bWVudC5xdWVyeVNlbGVjdG9yKCcuZm9vdG5vdGUnKVxubGV0IGNlbnRlcmVkLCBneW1Qcm9ncmVzcywgd29yY2VzdGVyR3ltc1xuXG5jb25zdCBkcmF3R3ltcyA9ICh6aXApID0+IHtcbiAgY29uc3QgZ3ltcyA9IHdvcmNlc3Rlckd5bXMuZmlsdGVyKGd5bSA9PiB7XG4gICAgcmV0dXJuIGd5bS5nc3gkemlwLiR0ID09PSB6aXBcbiAgfSlcblxuICBjb25zdCBwb2ludHMgPSBneW1zLm1hcChneW0gPT4gZ2VvSnNvbihneW0pKVxuXG4gIG1hcC5zZWxlY3RBbGwoJ2NpcmNsZScpXG4gICAgLmRhdGEocG9pbnRzKS5lbnRlcigpXG4gICAgLmFwcGVuZCgnY2lyY2xlJylcbiAgICAuYXR0cihcImN4XCIsIGQgPT4gcHJvamVjdGlvbihkLmdlb21ldHJ5LmNvb3JkaW5hdGVzKVswXSlcbiAgICAuYXR0cihcImN5XCIsIGQgPT4gcHJvamVjdGlvbihkLmdlb21ldHJ5LmNvb3JkaW5hdGVzKVsxXSlcbiAgICAuYXR0cihcInJcIiwgXCIzcHhcIilcbiAgICAuYXR0cignY2xhc3MnLCBkID0+IGBnZW9wYXRoICR7ZC5wcm9wZXJ0aWVzLmJhZGdlLnRvTG93ZXJDYXNlKCl9YClcbiAgICAuYXR0cignZGF0YS1uYW1lJywgZCA9PiBkLnByb3BlcnRpZXMubmFtZSlcbn1cblxuY29uc3QgdXBkYXRlSW5mbyA9IChkID0gbnVsbCkgPT4ge1xuICBjb25zdCB6aXAgPSBfLmdldChkLCAncHJvcGVydGllcy5aQ1RBNUNFMTAnLCAnb3ZlcmFsbCcpXG5cbiAgY29uc3QgZGl2aWRlciA9ICgpID0+IHtcbiAgICBpbmZvQm9keS5hcHBlbmQoJ3RzcGFuJylcbiAgICAgIC5hdHRyKCdjbGFzcycsICdkaXZpZGVyJylcbiAgICAgIC5hdHRyKCdkeCcsIDgpXG4gICAgICAuYXR0cignZHknLCAwKVxuICAgICAgLnRleHQoJ3wnKVxuICB9XG5cbiAgY29uc3QgYmFkZ2VTcGFuID0gKGJhZGdlLCBmaXJzdCA9IGZhbHNlKSA9PiB7XG4gICAgY29uc3QgdHNwYW4gPSBpbmZvQm9keS5hcHBlbmQoJ3RzcGFuJylcbiAgICAgIC5hdHRyKCdjbGFzcycsIGJhZGdlKVxuICAgICAgLnRleHQoXy5nZXQoZ3ltUHJvZ3Jlc3MsIGAke3ppcH0uJHtiYWRnZX1gKSlcblxuICAgIGlmIChmaXJzdCkge1xuICAgICAgdHNwYW5cbiAgICAgICAgLmF0dHIoJ2R4JywgMClcbiAgICAgICAgLmF0dHIoJ2R5JywgJzEuMmVtJylcbiAgICB9IGVsc2Uge1xuICAgICAgdHNwYW5cbiAgICAgICAgLmF0dHIoJ2R4JywgOClcbiAgICAgICAgLmF0dHIoJ2R5JywgMClcbiAgICB9XG4gIH1cblxuICBpbmZvVGl0bGUudGV4dCh6aXAgIT09ICdvdmVyYWxsJyA/IHppcCA6ICdXb3JjZXN0ZXInKVxuXG4gIGluZm9Cb2R5XG4gICAgLnNlbGVjdEFsbCgnKicpXG4gICAgLnJlbW92ZSgpXG5cbiAgYmFkZ2VTcGFuKCd0b2RvJywgdHJ1ZSlcbiAgZGl2aWRlcihpbmZvQm9keSlcblxuICBiYWRnZVNwYW4oJ2Jyb256ZScpXG4gIGRpdmlkZXIoaW5mb0JvZHkpXG5cbiAgYmFkZ2VTcGFuKCdzaWx2ZXInKVxuICBkaXZpZGVyKGluZm9Cb2R5KVxuXG4gIGJhZGdlU3BhbignZ29sZCcpXG5cbiAgaW5mb0JvZHkuYXBwZW5kKCd0c3BhbicpXG4gICAgLmF0dHIoJ2NsYXNzJywgJ2luZm9fX3N1bW1hcnknKVxuICAgIC5hdHRyKCd4JywgMjApXG4gICAgLmF0dHIoJ2R5JywgJzEuMmVtJylcbiAgICAudGV4dChgdDogJHtfLmdldChneW1Qcm9ncmVzcywgYCR7emlwfS50b3RhbGApfWApXG5cbiAgaW5mb0JvZHkuYXBwZW5kKCd0c3BhbicpXG4gICAgLmF0dHIoJ2NsYXNzJywgJ2luZm9fX3N1bW1hcnknKVxuICAgIC5hdHRyKCdkeCcsIDUpXG4gICAgLmF0dHIoJ2R5JywgMClcbiAgICAudGV4dChgY29tcDogJHtwY3RGb3JEaXNwbGF5KF8uZ2V0KGd5bVByb2dyZXNzLCBgJHt6aXB9LmdvbGRDb21wbGV0ZWApKX0lYClcbn1cblxuY29uc3QgbW91c2VvdmVyID0gKGQpID0+IHtcbiAgLy8gV0hZIERPRVNOVCBUSElTIFdPUksgOihcbiAgZDMuc2VsZWN0KHRoaXMpXG4gICAgLnN0eWxlKCdmaWxsJywgJ29yYW5nZScpXG5cbiAgaWYgKCFjZW50ZXJlZCkge1xuICAgIHVwZGF0ZUluZm8oZClcbiAgfVxufVxuXG5jb25zdCBtb3VzZW91dCA9IChkKSA9PiB7XG4gIGlmICghY2VudGVyZWQpIHtcbiAgICB1cGRhdGVJbmZvKClcbiAgfVxufVxuXG4vLyBXaGVuIGNsaWNrZWQsIHpvb20gaW5cbmNvbnN0IGNsaWNrID0gKGQpID0+IHtcbiAgY29uc3QgemlwID0gXy5nZXQoZCwgJ3Byb3BlcnRpZXMuWkNUQTVDRTEwJylcbiAgbGV0IHgsIHksIGtcblxuICBtYXAuc2VsZWN0QWxsKCcuZ2VvcGF0aCcpXG4gICAgLnJlbW92ZSgpXG5cbiAgLy8gQ29tcHV0ZSBjZW50cm9pZCBvZiB0aGUgc2VsZWN0ZWQgcGF0aFxuICBpZiAoZCAmJiBjZW50ZXJlZCAhPT0gZCkge1xuICAgIGNvbnN0IGNlbnRyb2lkID0gZ2VvUGF0aEdlbmVyYXRvci5jZW50cm9pZChkKVxuICAgIHggPSBjZW50cm9pZFswXVxuICAgIHkgPSBjZW50cm9pZFsxXVxuICAgIGsgPSAyXG4gICAgY2VudGVyZWQgPSBkXG4gIH0gZWxzZSB7XG4gICAgeCA9IHdpZHRoIC8gMlxuICAgIHkgPSBoZWlnaHQgLyAyXG4gICAgayA9IDE7XG4gICAgY2VudGVyZWQgPSBudWxsXG4gIH1cblxuICAvLyBIaWdobGlnaHQgdGhlIGNsaWNrZWQgYXJlYVxuICBtYXAuc2VsZWN0QWxsKCdwYXRoJylcbiAgICAuc3R5bGUoJ2ZpbGwnLCAoZCkgPT4ge1xuICAgICAgcmV0dXJuIGNlbnRlcmVkICYmIGQgPT09IGNlbnRlcmVkID8gJyNCNkJGRjAnIDogY29sb3IocGN0Rm9yRGlzcGxheShfLmdldChneW1Qcm9ncmVzcywgYCR7Xy5nZXQoZCwgJ3Byb3BlcnRpZXMuWkNUQTVDRTEwJyl9LmdvbGRDb21wbGV0ZWApKSlcbiAgICB9KVxuXG4gIGNvbnN0IHRyYW5zZm9ybWF0aW9uID0gYHRyYW5zbGF0ZSgke3dpZHRoIC8gMn0sJHtoZWlnaHQvMn0pIHNjYWxlKCR7a30pIHRyYW5zbGF0ZSgkey14fSwkey15fSlgO1xuXG4gIC8vIFpvb21cbiAgbWFwLnRyYW5zaXRpb24oKVxuICAgIC5kdXJhdGlvbig3NTApXG4gICAgLmF0dHIoJ3RyYW5zZm9ybScsIHRyYW5zZm9ybWF0aW9uKTtcblxuICBpZiAoXy5nZXQoZCwgJ3Byb3BlcnRpZXMnLCBmYWxzZSkpIHtcbiAgICBpbmZvVGl0bGUudGV4dCh6aXApXG4gIH1cblxuICB1cGRhdGVJbmZvKGQpXG5cbiAgaWYgKGNlbnRlcmVkKSB7XG4gICAgZHJhd0d5bXMoemlwKVxuICB9XG59XG5cbi8vIG1ha2UgRDMgYXdhcmUgb2YgdGhlIDxzdmc+IGVsZW1lbnQgaW4gdGhlIEhUTUxcbmNvbnN0IHN2ZyA9IGQzLnNlbGVjdChcInN2Z1wiKVxuXG4vLyBnZXQgPHN2Zz4gd2lkdGggYW5kIGhlaWdodCBmcm9tIEhUTUwgaW5zdGVhZCBvZiBoYXJkLWNvZGluZyB2YWx1ZXNcbmNvbnN0IHN2Z1dpZHRoID0gK3N2Zy5hdHRyKFwidmlld0JveFwiKS5zcGxpdChcIiBcIilbMl0sXG4gIHN2Z0hlaWdodCA9ICtzdmcuYXR0cihcInZpZXdCb3hcIikuc3BsaXQoXCIgXCIpWzNdO1xuXG4vLyBkZWZpbmUgdGhlIG1hcCBwcm9qZWN0aW9uXG5jb25zdCBwcm9qZWN0aW9uID0gZDNcbiAgLmdlb01lcmNhdG9yKClcbiAgLnRyYW5zbGF0ZShbc3ZnV2lkdGggLyAyLCBzdmdIZWlnaHQgLyAyXSlcbiAgLmNlbnRlcihbLTcxLjgwMjI5MzQsIDQyLjI3NTQxMzZdKVxuICAucm90YXRlKFswLCAwLCAwXSlcbiAgLnNjYWxlKDE3MDAwMCk7XG5cbmNvbnN0IGdlb1BhdGhHZW5lcmF0b3IgPSBkMy5nZW9QYXRoKCkucHJvamVjdGlvbihwcm9qZWN0aW9uKVxuXG5jb25zdCBiYWNrZ3JvdW5kID0gc3ZnLmFwcGVuZCgncmVjdCcpXG4gIC5hdHRyKCdjbGFzcycsICdiYWNrZ3JvdW5kJylcbiAgLmF0dHIoJ3dpZHRoJywgJzEwMCUnKVxuICAuYXR0cignaGVpZ2h0JywgJzEwMCUnKVxuICAub24oJ2NsaWNrJywgY2xpY2spXG5cbmNvbnN0IG1hcCA9IHN2Z1xuICAuYXBwZW5kKCdnJylcbiAgLmF0dHIoJ2NsYXNzJywgJ21hcCcpXG5cbmNvbnN0IGluZm8gPSBzdmdcbiAgLmFwcGVuZCgnZycpXG4gIC5hdHRyKCdjbGFzcycsICdpbmZvJylcblxuY29uc3QgaW5mb1RpdGxlID0gaW5mb1xuICAuYXBwZW5kKCd0ZXh0JylcbiAgLnRleHQoJ1dvcmNlc3RlcicpXG4gIC5hdHRyKCdjbGFzcycsICdpbmZvX190aXRsZScpXG4gIC5hdHRyKCd4JywgMjApXG4gIC5hdHRyKCd5JywgNDUpXG5cbmNvbnN0IGluZm9Cb2R5ID0gaW5mb1xuICAuYXBwZW5kKCd0ZXh0JylcbiAgLmF0dHIoJ2NsYXNzJywgJ2luZm9fX2JvZHknKVxuICAuYXR0cigneCcsIDIwKVxuICAuYXR0cigneScsIDUwKVxuXG4vLyBIZXJlIGNvbWVzIHRoZSBneW0gZGF0YVxuZDNcbiAgLmpzb24oJ2h0dHBzOi8vc3ByZWFkc2hlZXRzLmdvb2dsZS5jb20vZmVlZHMvbGlzdC8xV0RuRkJ3Mk9iQ3dSczNsUVNrLWRlcDNsaDBqWHFvU2phNTltWFhiMjFhcy8xL3B1YmxpYy92YWx1ZXM/YWx0PWpzb24nKVxuICAudGhlbihkYXRhID0+IHtcbiAgICB3b3JjZXN0ZXJHeW1zID0gXy5zb3J0QnkoXG4gICAgICBkYXRhLmZlZWQuZW50cnkuZmlsdGVyKGQgPT4gZC5nc3gkY2l0eS4kdCA9PT0gJ1dvcmNlc3RlcicpLFxuICAgICAgbyA9PiBvLmdzeCRuZWlnaGJvcmhvb2QuJHRcbiAgICApXG5cbiAgICBneW1Qcm9ncmVzcyA9IHtcbiAgICAgIG92ZXJhbGw6IGd5bVN1bW1hcnkod29yY2VzdGVyR3ltcyksXG4gICAgICAuLi56aXBzLnJlZHVjZSgoYWNjLCB6aXApID0+IHtcbiAgICAgICAgYWNjW3ppcF0gPSBneW1TdW1tYXJ5KHdvcmNlc3Rlckd5bXMuZmlsdGVyKGQgPT4gZC5nc3gkemlwLiR0ID09PSB6aXApKVxuICAgICAgICByZXR1cm4gYWNjXG4gICAgICB9LCB7fSlcblxuICAgIH1cblxuICAgIC8vIFVwZGF0ZSB0aGUgc3VtbWFyeVxuICAgIGZvb3Rub3RlLmlubmVySFRNTCA9IGBHeW1zOiAke18uZ2V0KGd5bVByb2dyZXNzLCAnb3ZlcmFsbC50b3RhbCcsIDApfTsgQ29tcGxldGU6ICR7cGN0Rm9yRGlzcGxheShfLmdldChneW1Qcm9ncmVzcywgJ292ZXJhbGwuZ29sZENvbXBsZXRlJywgMCkpfSVgXG5cbiAgICBjb25zdCBneW1MaXN0ID0gZDMuc2VsZWN0KCcuZ3ltbGlzdCcpXG4gICAgd29yY2VzdGVyR3ltcy5mb3JFYWNoKGd5bSA9PiB7XG4gICAgICBneW1MaXN0LmFwcGVuZCgnZGl2JylcbiAgICAgICAgLmF0dHIoJ2NsYXNzJywgYGd5bSBneW0tLSR7Z3ltLmdzeCRnb3BwZXJtYW4uJHQudG9Mb3dlckNhc2UoKX1gKVxuICAgICAgICAuYXBwZW5kKCdkaXYnKVxuICAgICAgICAgIC5hdHRyKCdjbGFzcycsICdneW1fX2luZm8nKVxuICAgICAgICAgIC5odG1sKGBcbiAgICAgICAgICAgIDxwIGNsYXNzPVwiZ3ltX190aXRsZVwiPiR7Z3ltLmdzeCRuYW1lLiR0fTwvcD5cbiAgICAgICAgICAgIDxwPiR7Z3ltLmdzeCRuZWlnaGJvcmhvb2QuJHR9LCAke2d5bS5nc3gkemlwLiR0fTwvcD5cbiAgICAgICAgICAgIDxwPjxhIHRhcmdldD1cIl9ibGFua1wiIGhyZWY9XCJodHRwczovL3d3dy5nb29nbGUuY29tL21hcHMvc2VhcmNoLz9hcGk9MSZxdWVyeT0ke2d5bS5nc3gkbGF0LiR0fSwke2d5bS5nc3gkbG9uZy4kdH1cIj4obWFwKTwvYT48L3A+XG4gICAgICAgICAgYClcbiAgICB9KVxuXG4gICAgLy8gRHJhdyB0aGUgbWFwXG4gICAgZDMuanNvbihcImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9PcGVuRGF0YURFL1N0YXRlLXppcC1jb2RlLUdlb0pTT04vbWFzdGVyL21hX21hc3NhY2h1c2V0dHNfemlwX2NvZGVzX2dlby5taW4uanNvblwiKVxuICAgICAgLnRoZW4oZGF0YSA9PiB7XG4gICAgICAgIGNvbnN0IHdvcmNlc3RlciA9IGRhdGEuZmVhdHVyZXMuZmlsdGVyKGQgPT4gemlwcy5pbmNsdWRlcyhkLnByb3BlcnRpZXMuWkNUQTVDRTEwKSlcblxuICAgICAgICBtYXBcbiAgICAgICAgICAuc2VsZWN0QWxsKCdwYXRoJylcbiAgICAgICAgICAuZGF0YSh3b3JjZXN0ZXIpXG4gICAgICAgIC5lbnRlcigpLmFwcGVuZCgncGF0aCcpXG4gICAgICAgICAgLmF0dHIoJ2QnLCBnZW9QYXRoR2VuZXJhdG9yKVxuICAgICAgICAgIC5hdHRyKCd2ZWN0b3ItZWZmZWN0JywgJ25vbi1zY2FsaW5nLXN0cm9rZScpXG4gICAgICAgICAgLmF0dHIoJ2NsYXNzJywgJ2ZlYXR1cmUnKVxuICAgICAgICAgIC5zdHlsZSgnZmlsbCcsIChkKSA9PiB7XG4gICAgICAgICAgICBjb25zdCB6aXAgPSBfLmdldChkLCAncHJvcGVydGllcy5aQ1RBNUNFMTAnKVxuICAgICAgICAgICAgcmV0dXJuIGNvbG9yKHBjdEZvckRpc3BsYXkoXy5nZXQoZ3ltUHJvZ3Jlc3MsIGAke3ppcH0uZ29sZENvbXBsZXRlYCkpKVxuICAgICAgICAgIH0pXG4gICAgICAgICAgLmF0dHIoJ2RhdGEtemlwJywgKGQpID0+IHtcbiAgICAgICAgICAgIHJldHVybiBfLmdldChkLCAncHJvcGVydGllcy5aQ1RBNUNFMTAnLCAnb3ZlcmFsbCcpXG4gICAgICAgICAgfSlcbiAgICAgICAgICAub24oJ21vdXNlb3ZlcicsIG1vdXNlb3ZlcilcbiAgICAgICAgICAub24oJ21vdXNlb3V0JywgbW91c2VvdXQpXG4gICAgICAgICAgLm9uKCdjbGljaycsIGNsaWNrKVxuXG4gICAgICAgICAgdXBkYXRlSW5mbygpO1xuICAgICAgfSlcblxuXG4gIH0pXG5cblxuXG5cblxuLy8gV0VCUEFDSyBGT09URVIgLy9cbi8vIHNjcmlwdC5qcyJdLCJtYXBwaW5ncyI6Ijs7OztBQUFBO0FBQ0E7QUFDQTtBQUFBO0FBQUE7QUFDQTtBQUdBO0FBQ0E7QUFpQkE7QUFDQTtBQUFBO0FBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBQUE7QUFDQTtBQUNBO0FBR0E7QUFBQTtBQUNBO0FBQUE7QUFFQTtBQUFBO0FBQ0E7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBQ0E7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUtBO0FBQ0E7QUFDQTtBQUFBO0FBQ0E7QUFBQTtBQUNBO0FBR0E7QUFDQTtBQUdBO0FBQ0E7QUFHQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUdBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBS0E7QUFLQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBR0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUNBO0FBRUE7QUFDQTtBQUNBO0FBTUE7QUFDQTtBQUNBO0FBQ0E7QUFLQTtBQUNBO0FBR0E7QUFDQTtBQUdBO0FBQ0E7QUFNQTtBQUNBO0FBS0E7QUFDQTtBQUdBO0FBQ0E7QUFBQTtBQUNBO0FBQUE7QUFDQTtBQUVBO0FBQ0E7QUFEQTtBQUdBO0FBQUE7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUdBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQVNBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFBQTtBQUFBO0FBQ0E7QUFDQTtBQVFBO0FBQ0E7QUFDQTtBQUVBO0FBQ0E7QUFDQTtBQUlBO0FBQ0E7QUFHQSIsInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///1\n")}]);
<!DOCTYPE html>
<head>
<title>blockup</title>
<link href='dist.css' rel='stylesheet' />
</head>
<body>
<main>
<svg viewBox="0 0 960 600"></svg>
<div class="gymlist"></div>
<div class="footnote"></div>
</main>
<script src='https://d3js.org/d3.v5.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js'></script>
<script src='https://unpkg.com/[email protected]/dist/topojson-client.min.js'></script>
<script src='dist.js'></script>
</body>
{
"standard": {
"globals": [
"d3"
]
}
}
import { color, geoJson, gymSummary, pctForDisplay } from './util.js'
const margin = { top: 10, right: 30, bottom: 30, left: 30 },
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom
const zips = [
'01601',
'01602',
'01603',
'01604',
'01605',
'01606',
'01607',
'01608',
'01609',
'01610',
'01613',
'01614',
'01653',
'01654',
'01655'
]
const footnote = document.querySelector('.footnote')
let centered, gymProgress, worcesterGyms
const drawGyms = (zip) => {
const gyms = worcesterGyms.filter(gym => {
return gym.gsx$zip.$t === zip
})
const points = gyms.map(gym => geoJson(gym))
map.selectAll('circle')
.data(points).enter()
.append('circle')
.attr("cx", d => projection(d.geometry.coordinates)[0])
.attr("cy", d => projection(d.geometry.coordinates)[1])
.attr("r", "3px")
.attr('class', d => `geopath ${d.properties.badge.toLowerCase()}`)
.attr('data-name', d => d.properties.name)
}
const updateInfo = (d = null) => {
const zip = _.get(d, 'properties.ZCTA5CE10', 'overall')
const divider = () => {
infoBody.append('tspan')
.attr('class', 'divider')
.attr('dx', 8)
.attr('dy', 0)
.text('|')
}
const badgeSpan = (badge, first = false) => {
const tspan = infoBody.append('tspan')
.attr('class', badge)
.text(_.get(gymProgress, `${zip}.${badge}`))
if (first) {
tspan
.attr('dx', 0)
.attr('dy', '1.2em')
} else {
tspan
.attr('dx', 8)
.attr('dy', 0)
}
}
infoTitle.text(zip !== 'overall' ? zip : 'Worcester')
infoBody
.selectAll('*')
.remove()
badgeSpan('todo', true)
divider(infoBody)
badgeSpan('bronze')
divider(infoBody)
badgeSpan('silver')
divider(infoBody)
badgeSpan('gold')
infoBody.append('tspan')
.attr('class', 'info__summary')
.attr('x', 20)
.attr('dy', '1.2em')
.text(`t: ${_.get(gymProgress, `${zip}.total`)}`)
infoBody.append('tspan')
.attr('class', 'info__summary')
.attr('dx', 5)
.attr('dy', 0)
.text(`comp: ${pctForDisplay(_.get(gymProgress, `${zip}.goldComplete`))}%`)
}
const mouseover = (d) => {
// WHY DOESNT THIS WORK :(
d3.select(this)
.style('fill', 'orange')
if (!centered) {
updateInfo(d)
}
}
const mouseout = (d) => {
if (!centered) {
updateInfo()
}
}
// When clicked, zoom in
const click = (d) => {
const zip = _.get(d, 'properties.ZCTA5CE10')
let x, y, k
map.selectAll('.geopath')
.remove()
// Compute centroid of the selected path
if (d && centered !== d) {
const centroid = geoPathGenerator.centroid(d)
x = centroid[0]
y = centroid[1]
k = 2
centered = d
} else {
x = width / 2
y = height / 2
k = 1;
centered = null
}
// Highlight the clicked area
map.selectAll('path')
.style('fill', (d) => {
return centered && d === centered ? '#B6BFF0' : color(pctForDisplay(_.get(gymProgress, `${_.get(d, 'properties.ZCTA5CE10')}.goldComplete`)))
})
const transformation = `translate(${width / 2},${height/2}) scale(${k}) translate(${-x},${-y})`;
// Zoom
map.transition()
.duration(750)
.attr('transform', transformation);
if (_.get(d, 'properties', false)) {
infoTitle.text(zip)
}
updateInfo(d)
if (centered) {
drawGyms(zip)
}
}
// make D3 aware of the <svg> element in the HTML
const svg = d3.select("svg")
// get <svg> width and height from HTML instead of hard-coding values
const svgWidth = +svg.attr("viewBox").split(" ")[2],
svgHeight = +svg.attr("viewBox").split(" ")[3];
// define the map projection
const projection = d3
.geoMercator()
.translate([svgWidth / 2, svgHeight / 2])
.center([-71.8022934, 42.2754136])
.rotate([0, 0, 0])
.scale(170000);
const geoPathGenerator = d3.geoPath().projection(projection)
const background = svg.append('rect')
.attr('class', 'background')
.attr('width', '100%')
.attr('height', '100%')
.on('click', click)
const map = svg
.append('g')
.attr('class', 'map')
const info = svg
.append('g')
.attr('class', 'info')
const infoTitle = info
.append('text')
.text('Worcester')
.attr('class', 'info__title')
.attr('x', 20)
.attr('y', 45)
const infoBody = info
.append('text')
.attr('class', 'info__body')
.attr('x', 20)
.attr('y', 50)
// Here comes the gym data
d3
.json('https://spreadsheets.google.com/feeds/list/1WDnFBw2ObCwRs3lQSk-dep3lh0jXqoSja59mXXb21as/1/public/values?alt=json')
.then(data => {
worcesterGyms = _.sortBy(
data.feed.entry.filter(d => d.gsx$city.$t === 'Worcester'),
o => o.gsx$neighborhood.$t
)
gymProgress = {
overall: gymSummary(worcesterGyms),
...zips.reduce((acc, zip) => {
acc[zip] = gymSummary(worcesterGyms.filter(d => d.gsx$zip.$t === zip))
return acc
}, {})
}
// Update the summary
footnote.innerHTML = `Gyms: ${_.get(gymProgress, 'overall.total', 0)}; Complete: ${pctForDisplay(_.get(gymProgress, 'overall.goldComplete', 0))}%`
const gymList = d3.select('.gymlist')
worcesterGyms.forEach(gym => {
gymList.append('div')
.attr('class', `gym gym--${gym.gsx$gopperman.$t.toLowerCase()}`)
.append('div')
.attr('class', 'gym__info')
.html(`
<p class="gym__title">${gym.gsx$name.$t}</p>
<p>${gym.gsx$neighborhood.$t}, ${gym.gsx$zip.$t}</p>
<p><a target="_blank" href="https://www.google.com/maps/search/?api=1&query=${gym.gsx$lat.$t},${gym.gsx$long.$t}">(map)</a></p>
`)
})
// Draw the map
d3.json("https://raw.githubusercontent.com/OpenDataDE/State-zip-code-GeoJSON/master/ma_massachusetts_zip_codes_geo.min.json")
.then(data => {
const worcester = data.features.filter(d => zips.includes(d.properties.ZCTA5CE10))
map
.selectAll('path')
.data(worcester)
.enter().append('path')
.attr('d', geoPathGenerator)
.attr('vector-effect', 'non-scaling-stroke')
.attr('class', 'feature')
.style('fill', (d) => {
const zip = _.get(d, 'properties.ZCTA5CE10')
return color(pctForDisplay(_.get(gymProgress, `${zip}.goldComplete`)))
})
.attr('data-zip', (d) => {
return _.get(d, 'properties.ZCTA5CE10', 'overall')
})
.on('mouseover', mouseover)
.on('mouseout', mouseout)
.on('click', click)
updateInfo();
})
})
*
box-sizing border-box
body
color #666
font-size 1em
font-family sans-serif
main
max-width calc(125vh)
margin 0 auto
.footnote
color #999
font-style italic
font-size .8em
svg
font-family sans-serif
width 100%
stroke-linejoin round
stroke-linecap round
path
&.feature
stroke-width 1px
stroke #222
tspan
font-weight bold
.background
stroke #dedede
fill #fafafa
.geopath
stroke #666
stroke-width .5px
opacity .8
.info
&__title
font-size 1.6em
font-weight bold
&__summary
fill #b7e1cd
.divider
fill #222
font-weight normal
.todo, .to-do
fill #f4c7c3
.bronze
fill #cd7f32
.silver
fill #999
.gold
fill gold
.gym
background #dedede
border 1px solid #fff
display inline-block
float left
position relative
height 15px
width 15px
transition all .2s
&--bronze
background #cd7f32
&--silver
background #999
&--gold
background gold
&__info
display none
&__title
font-size 1em
font-weight bold
&:hover
border 1px solid navy
cursor: pointer
.gym__info
background #fff
border 2px solid navy
font-size .8em
padding 5px
display block
position absolute
left 0
bottom 15px
width 195px
z-index 99
p
margin 0
.gymlist
display grid
grid-template-columns repeat(auto-fill, minmax(15px, 1fr))
grid-gap 0px
width 100%
const geoJson = (gym) => {
return {
geometry: {
type: "Point",
coordinates: [
gym.gsx$long.$t,
gym.gsx$lat.$t
]
},
type: "Feature",
"properties": {
name: gym.gsx$name.$t,
address: gym.gsx$address.$t,
neighborhood: gym.gsx$neighborhood.$t,
city: gym.gsx$city.$t,
state: gym.gsx$state.$t,
zip: gym.gsx$zip.$t,
badge: gym.gsx$gopperman.$t
}
}
}
const gymSummary = (data) => {
const badges = data.reduce((acc, item) => {
const badge = item.gsx$gopperman.$t.toLowerCase()
const count = _.get(acc, badge, 0) + 1
acc[badge] = count
return acc
}, {})
const total = data.length,
bronze = badges.bronze || 0,
silver = badges.silver || 0,
gold = badges.gold || 0
return {
total: total,
bronze,
silver,
gold,
goldComplete: (gold / total),
silverComplete: ((silver + gold) / total) || 1,
bronzeComplete: ((silver + gold + bronze) / total) || 1,
todo: badges['to-do'] || 0,
}
}
const pctForDisplay = (num) => {
return (num * 100).toFixed(1)
}
const color = d3.scaleLinear().domain([1,100])
.interpolate(d3.interpolateHcl)
.range([d3.rgb("#CC211F"), d3.rgb('#FFD700')])
export {
geoJson,
color,
gymSummary,
pctForDisplay
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment