Map of my badge progress in Worcester, Massachusetts Pokemon gyms.
Last active
March 5, 2019 04:01
-
-
Save gopperman/2a67b764ef28935c3854eba41985a710 to your computer and use it in GitHub Desktop.
Worcester
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit | |
height: 500 | |
border: no |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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%} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
!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")}]); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"standard": { | |
"globals": [ | |
"d3" | |
] | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
}) | |
}) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
* | |
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% |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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