Imagine this: you’re planning your next getaway, and the last thing you want is to vacation in an area teeming with COVID infections. Being the data-driven person you are, you decide to download an open dataset of COVID infections across your country—each infected person marked as a latitude-longitude point (yeah, I know this dataset wouldn’t be GDPR compliant, but let’s roll with it for this example).
When you plot all these points on a map, you end up with a chaotic scatter of dots:
import%20marimo%20as%20mo%0A%23import%20polars%20as%20pl%0A%23pl.__version__
import%20anywidget%0Aimport%20traitlets%0A%0Aclass%20MAUPWidget(anywidget.AnyWidget)%3A%0A%20%20%20%20_esm%20%3D%20r%22%22%22%0A%20%20%20%20function%20render(%7B%20model%2C%20el%20%7D)%20%7B%0A%20%20%20%20%20%20let%20canvasSize%20%3D%20model.get(%22canvas_size%22)%20%7C%7C%20500%3B%0A%20%20%20%20%20%20let%20gridType%20%3D%20model.get(%22grid_type%22)%20%7C%7C%20%22square%22%3B%0A%20%20%20%20%20%20let%20cellSize%20%3D%20parseFloat(model.get(%22cell_size%22)%20%7C%7C%20%2250%22)%3B%0A%20%20%20%20%20%20let%20orientation%20%3D%20parseFloat(model.get(%22orientation%22)%20%7C%7C%20%220%22)%3B%0A%20%20%20%20%20%20let%20points%20%3D%20model.get(%22points%22)%20%7C%7C%20%5B%5D%3B%0A%20%20%20%20%20%20let%20heatmapColors%20%3D%20model.get(%22heatmap_colors%22)%3B%0A%20%20%20%20%20%20let%20offsetX%20%3D%20parseFloat(model.get(%22grid_origin_x%22)%20%7C%7C%20%220%22)%3B%0A%20%20%20%20%20%20let%20offsetY%20%3D%20parseFloat(model.get(%22grid_origin_y%22)%20%7C%7C%20%220%22)%3B%0A%20%20%20%20%20%20let%20applyFilter%20%3D%20model.get(%22apply_filter%22)%3B%20%20%2F%2F%20new%20flag%0A%0A%20%20%20%20%20%20const%20canvas%20%3D%20document.createElement(%22canvas%22)%3B%0A%20%20%20%20%20%20canvas.width%20%3D%20canvasSize%3B%0A%20%20%20%20%20%20canvas.height%20%3D%20canvasSize%3B%0A%20%20%20%20%20%20canvas.style.border%20%3D%20%221px%20solid%20%23ccc%22%3B%0A%20%20%20%20%20%20el.innerHTML%20%3D%20%22%22%3B%0A%20%20%20%20%20%20el.appendChild(canvas)%3B%0A%20%20%20%20%20%20const%20ctx%20%3D%20canvas.getContext(%222d%22)%3B%0A%0A%20%20%20%20%20%20let%20dragging%20%3D%20false%3B%0A%20%20%20%20%20%20let%20dragStartX%20%3D%200%2C%20dragStartY%20%3D%200%3B%0A%20%20%20%20%20%20let%20startOffsetX%20%3D%20offsetX%2C%20startOffsetY%20%3D%20offsetY%3B%0A%0A%20%20%20%20%20%20const%20sqrt3%20%3D%20Math.sqrt(3)%3B%0A%0A%20%20%20%20%20%20function%20parseHexColor(hex)%20%7B%0A%20%20%20%20%20%20%20%20let%20r%20%3D%20parseInt(hex.slice(1%2C3)%2C%2016)%3B%0A%20%20%20%20%20%20%20%20let%20g%20%3D%20parseInt(hex.slice(3%2C5)%2C%2016)%3B%0A%20%20%20%20%20%20%20%20let%20b%20%3D%20parseInt(hex.slice(5%2C7)%2C%2016)%3B%0A%20%20%20%20%20%20%20%20let%20a%20%3D%201.0%3B%0A%20%20%20%20%20%20%20%20if(hex.length%20%3D%3D%3D%209)%20%7B%0A%20%20%20%20%20%20%20%20%20%20a%20%3D%20parseInt(hex.slice(7%2C9)%2C%2016)%20%2F%20255%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20return%20%7Br%2C%20g%2C%20b%2C%20a%7D%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20function%20interpolateBetween(color1%2C%20color2%2C%20factor)%20%7B%0A%20%20%20%20%20%20%20%20factor%20%3D%20Math.min(Math.max(factor%2C%200)%2C%201)%3B%0A%20%20%20%20%20%20%20%20const%20c1%20%3D%20parseHexColor(color1)%3B%0A%20%20%20%20%20%20%20%20const%20c2%20%3D%20parseHexColor(color2)%3B%0A%20%20%20%20%20%20%20%20const%20r%20%3D%20Math.round(c1.r%20%2B%20factor%20*%20(c2.r%20-%20c1.r))%3B%0A%20%20%20%20%20%20%20%20const%20g%20%3D%20Math.round(c1.g%20%2B%20factor%20*%20(c2.g%20-%20c1.g))%3B%0A%20%20%20%20%20%20%20%20const%20b%20%3D%20Math.round(c1.b%20%2B%20factor%20*%20(c2.b%20-%20c1.b))%3B%0A%20%20%20%20%20%20%20%20const%20a%20%3D%20c1.a%20%2B%20factor%20*%20(c2.a%20-%20c1.a)%3B%0A%20%20%20%20%20%20%20%20return%20%60rgba(%24%7Br%7D%2C%20%24%7Bg%7D%2C%20%24%7Bb%7D%2C%20%24%7Ba%7D)%60%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20function%20getHeatmapColor(intensity)%20%7B%0A%20%20%20%20%20%20%20%20let%20n%20%3D%20heatmapColors.length%3B%0A%20%20%20%20%20%20%20%20if%20(n%20%3D%3D%3D%200)%20return%20%22rgba(0%2C0%2C0%2C1)%22%3B%0A%20%20%20%20%20%20%20%20if%20(n%20%3D%3D%3D%201)%20return%20heatmapColors%5B0%5D%3B%0A%20%20%20%20%20%20%20%20let%20segment%20%3D%20intensity%20*%20(n%20-%201)%3B%0A%20%20%20%20%20%20%20%20let%20index%20%3D%20Math.floor(segment)%3B%0A%20%20%20%20%20%20%20%20let%20factor%20%3D%20segment%20-%20index%3B%0A%20%20%20%20%20%20%20%20if%20(index%20%3E%3D%20n%20-%201)%20return%20heatmapColors%5Bn%20-%201%5D%3B%0A%20%20%20%20%20%20%20%20return%20interpolateBetween(heatmapColors%5Bindex%5D%2C%20heatmapColors%5Bindex%20%2B%201%5D%2C%20factor)%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20function%20toGridCoords(x%2C%20y)%20%7B%0A%20%20%20%20%20%20%20%20const%20rx%20%3D%20x%20-%20offsetX%3B%0A%20%20%20%20%20%20%20%20const%20ry%20%3D%20y%20-%20offsetY%3B%0A%20%20%20%20%20%20%20%20const%20theta%20%3D%20orientation%20*%20Math.PI%20%2F%20180%3B%0A%20%20%20%20%20%20%20%20const%20cosT%20%3D%20Math.cos(theta)%3B%0A%20%20%20%20%20%20%20%20const%20sinT%20%3D%20Math.sin(theta)%3B%0A%20%20%20%20%20%20%20%20return%20%7B%0A%20%20%20%20%20%20%20%20%20%20x%3A%20rx%20*%20cosT%20%2B%20ry%20*%20sinT%2C%0A%20%20%20%20%20%20%20%20%20%20y%3A%20-rx%20*%20sinT%20%2B%20ry%20*%20cosT%0A%20%20%20%20%20%20%20%20%7D%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20function%20getSquareGridBounds()%20%7B%0A%20%20%20%20%20%20%20%20const%20corners%20%3D%20%5B%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%200%2C%20y%3A%200%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%20canvas.width%2C%20y%3A%200%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%20canvas.width%2C%20y%3A%20canvas.height%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%200%2C%20y%3A%20canvas.height%20%7D%0A%20%20%20%20%20%20%20%20%5D%3B%0A%20%20%20%20%20%20%20%20let%20xs%20%3D%20%5B%5D%2C%20ys%20%3D%20%5B%5D%3B%0A%20%20%20%20%20%20%20%20for%20(const%20pt%20of%20corners)%20%7B%0A%20%20%20%20%20%20%20%20%20%20const%20gridPt%20%3D%20toGridCoords(pt.x%2C%20pt.y)%3B%0A%20%20%20%20%20%20%20%20%20%20xs.push(gridPt.x)%3B%0A%20%20%20%20%20%20%20%20%20%20ys.push(gridPt.y)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20const%20minX%20%3D%20Math.min(...xs)%3B%0A%20%20%20%20%20%20%20%20const%20maxX%20%3D%20Math.max(...xs)%3B%0A%20%20%20%20%20%20%20%20const%20minY%20%3D%20Math.min(...ys)%3B%0A%20%20%20%20%20%20%20%20const%20maxY%20%3D%20Math.max(...ys)%3B%0A%20%20%20%20%20%20%20%20const%20iMin%20%3D%20Math.floor(minX%20%2F%20cellSize)%3B%0A%20%20%20%20%20%20%20%20const%20iMax%20%3D%20Math.ceil(maxX%20%2F%20cellSize)%20-%201%3B%0A%20%20%20%20%20%20%20%20const%20jMin%20%3D%20Math.floor(minY%20%2F%20cellSize)%3B%0A%20%20%20%20%20%20%20%20const%20jMax%20%3D%20Math.ceil(maxY%20%2F%20cellSize)%20-%201%3B%0A%20%20%20%20%20%20%20%20return%20%7B%20iMin%2C%20iMax%2C%20jMin%2C%20jMax%20%7D%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20function%20getHexGridBounds()%20%7B%0A%20%20%20%20%20%20%20%20const%20corners%20%3D%20%5B%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%200%2C%20y%3A%200%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%20canvas.width%2C%20y%3A%200%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%20canvas.width%2C%20y%3A%20canvas.height%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%200%2C%20y%3A%20canvas.height%20%7D%0A%20%20%20%20%20%20%20%20%5D%3B%0A%20%20%20%20%20%20%20%20let%20qs%20%3D%20%5B%5D%2C%20rs%20%3D%20%5B%5D%3B%0A%20%20%20%20%20%20%20%20const%20theta%20%3D%20orientation%20*%20Math.PI%20%2F%20180%3B%0A%20%20%20%20%20%20%20%20const%20cosT%20%3D%20Math.cos(theta)%3B%0A%20%20%20%20%20%20%20%20const%20sinT%20%3D%20Math.sin(theta)%3B%0A%20%20%20%20%20%20%20%20for%20(const%20pt%20of%20corners)%20%7B%0A%20%20%20%20%20%20%20%20%20%20const%20rx%20%3D%20pt.x%20-%20offsetX%3B%0A%20%20%20%20%20%20%20%20%20%20const%20ry%20%3D%20pt.y%20-%20offsetY%3B%0A%20%20%20%20%20%20%20%20%20%20const%20gridX%20%3D%20rx%20*%20cosT%20%2B%20ry%20*%20sinT%3B%0A%20%20%20%20%20%20%20%20%20%20const%20gridY%20%3D%20-rx%20*%20sinT%20%2B%20ry%20*%20cosT%3B%0A%20%20%20%20%20%20%20%20%20%20const%20q%20%3D%20(gridX%20*%20(sqrt3%2F3)%20-%20gridY%2F3)%20%2F%20cellSize%3B%0A%20%20%20%20%20%20%20%20%20%20const%20r%20%3D%20(gridY%20*%20(2%2F3))%20%2F%20cellSize%3B%0A%20%20%20%20%20%20%20%20%20%20qs.push(q)%3B%0A%20%20%20%20%20%20%20%20%20%20rs.push(r)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20const%20minQ%20%3D%20Math.floor(Math.min(...qs))%3B%0A%20%20%20%20%20%20%20%20const%20maxQ%20%3D%20Math.ceil(Math.max(...qs))%3B%0A%20%20%20%20%20%20%20%20const%20minR%20%3D%20Math.floor(Math.min(...rs))%3B%0A%20%20%20%20%20%20%20%20const%20maxR%20%3D%20Math.ceil(Math.max(...rs))%3B%0A%20%20%20%20%20%20%20%20return%20%7B%20minQ%2C%20maxQ%2C%20minR%2C%20maxR%20%7D%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F%2F%20---%20Gaussian%20filter%20functions%20---%0A%20%20%20%20%20%20function%20applyGaussianFilterSquare(counts%2C%20bounds)%20%7B%0A%20%20%20%20%20%20%20%20let%20newCounts%20%3D%20new%20Map()%3B%0A%20%20%20%20%20%20%20%20let%20kernel%20%3D%20%5B%0A%20%20%20%20%20%20%20%20%20%20%5B0.0625%2C%200.125%2C%200.0625%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%5B0.125%2C%20%200.25%2C%20%200.125%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%5B0.0625%2C%200.125%2C%200.0625%5D%0A%20%20%20%20%20%20%20%20%5D%3B%0A%20%20%20%20%20%20%20%20for%20(let%20i%20%3D%20bounds.iMin%3B%20i%20%3C%3D%20bounds.iMax%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20for%20(let%20j%20%3D%20bounds.jMin%3B%20j%20%3C%3D%20bounds.jMax%3B%20j%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20let%20sum%20%3D%200%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20(let%20di%20%3D%20-1%3B%20di%20%3C%3D%201%3B%20di%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20for%20(let%20dj%20%3D%20-1%3B%20dj%20%3C%3D%201%3B%20dj%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20let%20key%20%3D%20%60%24%7Bi%2Bdi%7D%2C%24%7Bj%2Bdj%7D%60%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20let%20value%20%3D%20counts.get(key)%20%7C%7C%200%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20sum%20%2B%3D%20kernel%5Bdi%2B1%5D%5Bdj%2B1%5D%20*%20value%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20newCounts.set(%60%24%7Bi%7D%2C%24%7Bj%7D%60%2C%20sum)%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20return%20newCounts%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20function%20applyGaussianFilterHex(counts%2C%20bounds)%20%7B%0A%20%20%20%20%20%20%20%20let%20newCounts%20%3D%20new%20Map()%3B%0A%20%20%20%20%20%20%20%20%2F%2F%20Define%20neighbor%20offsets%20for%20axial%20coordinates%20in%20hex%20grid%3A%0A%20%20%20%20%20%20%20%20let%20neighbors%20%3D%20%5B%5B1%2C0%5D%2C%20%5B-1%2C0%5D%2C%20%5B0%2C1%5D%2C%20%5B0%2C-1%5D%2C%20%5B1%2C-1%5D%2C%20%5B-1%2C1%5D%5D%3B%0A%20%20%20%20%20%20%20%20let%20centerWeight%20%3D%200.5%3B%0A%20%20%20%20%20%20%20%20let%20neighborWeight%20%3D%200.0833333%3B%20%20%2F%2F%201%2F12%20so%20that%20total%20weight%20sums%20to%201%0A%20%20%20%20%20%20%20%20for%20(let%20q%20%3D%20bounds.minQ%20-%201%3B%20q%20%3C%3D%20bounds.maxQ%20%2B%201%3B%20q%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20for%20(let%20r%20%3D%20bounds.minR%20-%201%3B%20r%20%3C%3D%20bounds.maxR%20%2B%201%3B%20r%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20let%20sum%20%3D%20centerWeight%20*%20(counts.get(%60%24%7Bq%7D%2C%24%7Br%7D%60)%20%7C%7C%200)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20(let%20k%20%3D%200%3B%20k%20%3C%20neighbors.length%3B%20k%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20let%20dq%20%3D%20neighbors%5Bk%5D%5B0%5D%2C%20dr%20%3D%20neighbors%5Bk%5D%5B1%5D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20let%20key%20%3D%20%60%24%7Bq%2Bdq%7D%2C%24%7Br%2Bdr%7D%60%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20sum%20%2B%3D%20neighborWeight%20*%20(counts.get(key)%20%7C%7C%200)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20newCounts.set(%60%24%7Bq%7D%2C%24%7Br%7D%60%2C%20sum)%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20return%20newCounts%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%2F%2F%20---%20End%20Gaussian%20filter%20functions%20---%0A%0A%20%20%20%20%20%20%2F%2F%20C%C3%A1lculo%20de%20recuento%20de%20puntos%20for%20square%20grid%0A%20%20%20%20%20%20function%20computeSquareCounts()%20%7B%0A%20%20%20%20%20%20%20%20const%20counts%20%3D%20new%20Map()%3B%0A%20%20%20%20%20%20%20%20const%20theta%20%3D%20orientation%20*%20Math.PI%20%2F%20180%3B%0A%20%20%20%20%20%20%20%20const%20cosT%20%3D%20Math.cos(theta)%3B%0A%20%20%20%20%20%20%20%20const%20sinT%20%3D%20Math.sin(theta)%3B%0A%20%20%20%20%20%20%20%20for%20(const%20p%20of%20points)%20%7B%0A%20%20%20%20%20%20%20%20%20%20const%20rx%20%3D%20p.x%20-%20offsetX%3B%0A%20%20%20%20%20%20%20%20%20%20const%20ry%20%3D%20p.y%20-%20offsetY%3B%0A%20%20%20%20%20%20%20%20%20%20const%20gridX%20%3D%20rx%20*%20cosT%20%2B%20ry%20*%20sinT%3B%0A%20%20%20%20%20%20%20%20%20%20const%20gridY%20%3D%20-rx%20*%20sinT%20%2B%20ry%20*%20cosT%3B%0A%20%20%20%20%20%20%20%20%20%20const%20i%20%3D%20Math.floor(gridX%20%2F%20cellSize)%3B%0A%20%20%20%20%20%20%20%20%20%20const%20j%20%3D%20Math.floor(gridY%20%2F%20cellSize)%3B%0A%20%20%20%20%20%20%20%20%20%20const%20key%20%3D%20%60%24%7Bi%7D%2C%24%7Bj%7D%60%3B%0A%20%20%20%20%20%20%20%20%20%20counts.set(key%2C%20(counts.get(key)%20%7C%7C%200)%20%2B%201)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20return%20counts%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20function%20computeHexCounts()%20%7B%0A%20%20%20%20%20%20%20%20const%20counts%20%3D%20new%20Map()%3B%0A%20%20%20%20%20%20%20%20const%20theta%20%3D%20orientation%20*%20Math.PI%20%2F%20180%3B%0A%20%20%20%20%20%20%20%20const%20cosT%20%3D%20Math.cos(theta)%3B%0A%20%20%20%20%20%20%20%20const%20sinT%20%3D%20Math.sin(theta)%3B%0A%20%20%20%20%20%20%20%20for%20(const%20p%20of%20points)%20%7B%0A%20%20%20%20%20%20%20%20%20%20const%20rx%20%3D%20p.x%20-%20offsetX%3B%0A%20%20%20%20%20%20%20%20%20%20const%20ry%20%3D%20p.y%20-%20offsetY%3B%0A%20%20%20%20%20%20%20%20%20%20const%20gridX%20%3D%20rx%20*%20cosT%20%2B%20ry%20*%20sinT%3B%0A%20%20%20%20%20%20%20%20%20%20const%20gridY%20%3D%20-rx%20*%20sinT%20%2B%20ry%20*%20cosT%3B%0A%20%20%20%20%20%20%20%20%20%20const%20q_frac%20%3D%20(gridX%20*%20(sqrt3%2F3)%20-%20gridY%2F3)%20%2F%20cellSize%3B%0A%20%20%20%20%20%20%20%20%20%20const%20r_frac%20%3D%20(gridY%20*%20(2%2F3))%20%2F%20cellSize%3B%0A%20%20%20%20%20%20%20%20%20%20let%20q%20%3D%20q_frac%2C%20r%20%3D%20r_frac%2C%20s%20%3D%20-q%20-%20r%3B%0A%20%20%20%20%20%20%20%20%20%20let%20rq%20%3D%20Math.round(q)%3B%0A%20%20%20%20%20%20%20%20%20%20let%20rr%20%3D%20Math.round(r)%3B%0A%20%20%20%20%20%20%20%20%20%20let%20rs%20%3D%20Math.round(s)%3B%0A%20%20%20%20%20%20%20%20%20%20const%20q_diff%20%3D%20Math.abs(rq%20-%20q)%3B%0A%20%20%20%20%20%20%20%20%20%20const%20r_diff%20%3D%20Math.abs(rr%20-%20r)%3B%0A%20%20%20%20%20%20%20%20%20%20const%20s_diff%20%3D%20Math.abs(rs%20-%20s)%3B%0A%20%20%20%20%20%20%20%20%20%20if%20(q_diff%20%3E%20r_diff%20%26%26%20q_diff%20%3E%20s_diff)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20rq%20%3D%20-rr%20-%20rs%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%20else%20if%20(r_diff%20%3E%20s_diff)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20rr%20%3D%20-rq%20-%20rs%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%20else%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20rs%20%3D%20-rq%20-%20rr%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20const%20key%20%3D%20%60%24%7Brq%7D%2C%24%7Brr%7D%60%3B%0A%20%20%20%20%20%20%20%20%20%20counts.set(key%2C%20(counts.get(key)%20%7C%7C%200)%20%2B%201)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20return%20counts%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20function%20drawSquareGrid()%20%7B%0A%20%20%20%20%20%20%20%20const%20bounds%20%3D%20getSquareGridBounds()%3B%0A%20%20%20%20%20%20%20%20let%20counts%20%3D%20computeSquareCounts()%3B%0A%20%20%20%20%20%20%20%20if%20(applyFilter)%20%7B%0A%20%20%20%20%20%20%20%20%20%20counts%20%3D%20applyGaussianFilterSquare(counts%2C%20bounds)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20let%20maxCount%20%3D%200%3B%0A%20%20%20%20%20%20%20%20for%20(let%20i%20%3D%20bounds.iMin%3B%20i%20%3C%3D%20bounds.iMax%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20for%20(let%20j%20%3D%20bounds.jMin%3B%20j%20%3C%3D%20bounds.jMax%3B%20j%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20key%20%3D%20%60%24%7Bi%7D%2C%24%7Bj%7D%60%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20count%20%3D%20counts.get(key)%20%7C%7C%200%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(count%20%3E%20maxCount)%20maxCount%20%3D%20count%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20const%20theta%20%3D%20orientation%20*%20Math.PI%20%2F%20180%3B%0A%20%20%20%20%20%20%20%20const%20cosT%20%3D%20Math.cos(theta)%3B%0A%20%20%20%20%20%20%20%20const%20sinT%20%3D%20Math.sin(theta)%3B%0A%0A%20%20%20%20%20%20%20%20for%20(let%20i%20%3D%20bounds.iMin%3B%20i%20%3C%3D%20bounds.iMax%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20for%20(let%20j%20%3D%20bounds.jMin%3B%20j%20%3C%3D%20bounds.jMax%3B%20j%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20key%20%3D%20%60%24%7Bi%7D%2C%24%7Bj%7D%60%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20count%20%3D%20counts.get(key)%20%7C%7C%200%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20intensity%20%3D%20maxCount%20%3F%20(count%20%2F%20maxCount)%20%3A%200%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20color%20%3D%20getHeatmapColor(intensity)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20ctx.fillStyle%20%3D%20color%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20corners%20%3D%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%20i%20*%20cellSize%2C%20%20%20%20%20%20%20%20%20y%3A%20j%20*%20cellSize%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%20(i%2B1)%20*%20cellSize%2C%20%20%20%20%20%20%20y%3A%20j%20*%20cellSize%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%20(i%2B1)%20*%20cellSize%2C%20%20%20%20%20%20%20y%3A%20(j%2B1)%20*%20cellSize%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%20i%20*%20cellSize%2C%20%20%20%20%20%20%20%20%20y%3A%20(j%2B1)%20*%20cellSize%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20ctx.beginPath()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20(let%20k%20%3D%200%3B%20k%20%3C%20corners.length%3B%20k%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20gx%20%3D%20corners%5Bk%5D.x%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20gy%20%3D%20corners%5Bk%5D.y%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20globalX%20%3D%20offsetX%20%2B%20gx%20*%20cosT%20-%20gy%20*%20sinT%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20globalY%20%3D%20offsetY%20%2B%20gx%20*%20sinT%20%2B%20gy%20*%20cosT%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20(k%20%3D%3D%3D%200)%20ctx.moveTo(globalX%2C%20globalY)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20else%20ctx.lineTo(globalX%2C%20globalY)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20ctx.closePath()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20ctx.fill()%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20function%20drawHexGrid()%20%7B%0A%20%20%20%20%20%20%20%20const%20bounds%20%3D%20getHexGridBounds()%3B%0A%20%20%20%20%20%20%20%20let%20counts%20%3D%20computeHexCounts()%3B%0A%20%20%20%20%20%20%20%20if%20(applyFilter)%20%7B%0A%20%20%20%20%20%20%20%20%20%20counts%20%3D%20applyGaussianFilterHex(counts%2C%20bounds)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20let%20maxCount%20%3D%200%3B%0A%20%20%20%20%20%20%20%20for%20(let%20q%20%3D%20bounds.minQ%20-%201%3B%20q%20%3C%3D%20bounds.maxQ%20%2B%201%3B%20q%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20for%20(let%20r%20%3D%20bounds.minR%20-%201%3B%20r%20%3C%3D%20bounds.maxR%20%2B%201%3B%20r%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20key%20%3D%20%60%24%7Bq%7D%2C%24%7Br%7D%60%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20count%20%3D%20counts.get(key)%20%7C%7C%200%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(count%20%3E%20maxCount)%20maxCount%20%3D%20count%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20const%20theta%20%3D%20orientation%20*%20Math.PI%20%2F%20180%3B%0A%20%20%20%20%20%20%20%20const%20cosT%20%3D%20Math.cos(theta)%3B%0A%20%20%20%20%20%20%20%20const%20sinT%20%3D%20Math.sin(theta)%3B%0A%20%20%20%20%20%20%20%20const%20R%20%3D%20cellSize%3B%20%20%2F%2F%20use%20cellSize%20as%20radius%0A%20%20%20%20%20%20%20%20const%20hexCorners%20%3D%20%5B%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%200%2C%20y%3A%20-R%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%20(sqrt3%2F2)%20*%20R%2C%20y%3A%20-0.5%20*%20R%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%20(sqrt3%2F2)%20*%20R%2C%20y%3A%200.5%20*%20R%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%200%2C%20y%3A%20R%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%20-%20(sqrt3%2F2)%20*%20R%2C%20y%3A%200.5%20*%20R%20%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%7B%20x%3A%20-%20(sqrt3%2F2)%20*%20R%2C%20y%3A%20-0.5%20*%20R%20%7D%0A%20%20%20%20%20%20%20%20%5D%3B%0A%0A%20%20%20%20%20%20%20%20for%20(let%20q%20%3D%20bounds.minQ%20-%201%3B%20q%20%3C%3D%20bounds.maxQ%20%2B%201%3B%20q%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20for%20(let%20r%20%3D%20bounds.minR%20-%201%3B%20r%20%3C%3D%20bounds.maxR%20%2B%201%3B%20r%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20key%20%3D%20%60%24%7Bq%7D%2C%24%7Br%7D%60%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20count%20%3D%20counts.get(key)%20%7C%7C%200%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20intensity%20%3D%20maxCount%20%3F%20(count%20%2F%20maxCount)%20%3A%200%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20color%20%3D%20getHeatmapColor(intensity)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20ctx.fillStyle%20%3D%20color%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20centerX%20%3D%20R%20*%20sqrt3%20*%20(q%20%2B%200.5%20*%20r)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20centerY%20%3D%20R%20*%201.5%20*%20r%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20ctx.beginPath()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20(let%20i%20%3D%200%3B%20i%20%3C%20hexCorners.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20corner%20%3D%20hexCorners%5Bi%5D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20gx%20%3D%20centerX%20%2B%20corner.x%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20gy%20%3D%20centerY%20%2B%20corner.y%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20globalX%20%3D%20offsetX%20%2B%20gx%20*%20cosT%20-%20gy%20*%20sinT%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20const%20globalY%20%3D%20offsetY%20%2B%20gx%20*%20sinT%20%2B%20gy%20*%20cosT%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20(i%20%3D%3D%3D%200)%20ctx.moveTo(globalX%2C%20globalY)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20else%20ctx.lineTo(globalX%2C%20globalY)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20ctx.closePath()%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20ctx.fill()%3B%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20function%20drawPoints()%20%7B%0A%20%20%20%20%20%20%20%20ctx.fillStyle%20%3D%20%22black%22%3B%0A%20%20%20%20%20%20%20%20for%20(const%20p%20of%20points)%20%7B%0A%20%20%20%20%20%20%20%20%20%20ctx.fillRect(p.x%20-%201%2C%20p.y%20-%201%2C%203%2C%203)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20function%20draw()%20%7B%0A%20%20%20%20%20%20%20%20ctx.clearRect(0%2C%200%2C%20canvas.width%2C%20canvas.height)%3B%0A%20%20%20%20%20%20%20%20if%20(gridType%20%3D%3D%3D%20%22square%22)%20%7B%0A%20%20%20%20%20%20%20%20%20%20drawSquareGrid()%3B%0A%20%20%20%20%20%20%20%20%7D%20else%20if%20(gridType%20%3D%3D%3D%20%22hex%22)%20%7B%0A%20%20%20%20%20%20%20%20%20%20drawHexGrid()%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20drawPoints()%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F%2F%20Enable%20interactive%20dragging%20if%20flagged.%0A%20%20%20%20%20%20if%20(model.get(%22interactive%22))%20%7B%0A%20%20%20%20%20%20%20%20canvas.addEventListener(%22pointerdown%22%2C%20(e)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20dragging%20%3D%20true%3B%0A%20%20%20%20%20%20%20%20%20%20dragStartX%20%3D%20e.clientX%3B%0A%20%20%20%20%20%20%20%20%20%20dragStartY%20%3D%20e.clientY%3B%0A%20%20%20%20%20%20%20%20%20%20startOffsetX%20%3D%20offsetX%3B%0A%20%20%20%20%20%20%20%20%20%20startOffsetY%20%3D%20offsetY%3B%0A%20%20%20%20%20%20%20%20%20%20canvas.setPointerCapture(e.pointerId)%3B%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20%20%20canvas.addEventListener(%22pointermove%22%2C%20(e)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20if%20(!dragging)%20return%3B%0A%20%20%20%20%20%20%20%20%20%20const%20dx%20%3D%20e.clientX%20-%20dragStartX%3B%0A%20%20%20%20%20%20%20%20%20%20const%20dy%20%3D%20e.clientY%20-%20dragStartY%3B%0A%20%20%20%20%20%20%20%20%20%20offsetX%20%3D%20startOffsetX%20%2B%20dx%3B%0A%20%20%20%20%20%20%20%20%20%20offsetY%20%3D%20startOffsetY%20%2B%20dy%3B%0A%20%20%20%20%20%20%20%20%20%20draw()%3B%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20%20%20canvas.addEventListener(%22pointerup%22%2C%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20dragging%20%3D%20false%3B%0A%20%20%20%20%20%20%20%20%20%20model.set(%22grid_origin_x%22%2C%20offsetX)%3B%0A%20%20%20%20%20%20%20%20%20%20model.set(%22grid_origin_y%22%2C%20offsetY)%3B%0A%20%20%20%20%20%20%20%20%20%20model.save_changes()%3B%0A%20%20%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%2F%2F%20Listen%20for%20parameter%20changes.%0A%20%20%20%20%20%20model.on(%22change%3Acanvas_size%22%2C%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20canvas.width%20%3D%20model.get(%22canvas_size%22)%3B%0A%20%20%20%20%20%20%20%20canvas.height%20%3D%20model.get(%22canvas_size%22)%3B%0A%20%20%20%20%20%20%20%20draw()%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20model.on(%22change%3Agrid_type%22%2C%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20gridType%20%3D%20model.get(%22grid_type%22)%3B%0A%20%20%20%20%20%20%20%20draw()%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20model.on(%22change%3Acell_size%22%2C%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20cellSize%20%3D%20parseFloat(model.get(%22cell_size%22))%3B%0A%20%20%20%20%20%20%20%20draw()%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20model.on(%22change%3Aorientation%22%2C%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20orientation%20%3D%20parseFloat(model.get(%22orientation%22))%3B%0A%20%20%20%20%20%20%20%20draw()%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20model.on(%22change%3Apoints%22%2C%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20points%20%3D%20model.get(%22points%22)%20%7C%7C%20%5B%5D%3B%0A%20%20%20%20%20%20%20%20draw()%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20model.on(%22change%3Aheatmap_colors%22%2C%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20heatmapColors%20%3D%20model.get(%22heatmap_colors%22)%3B%0A%20%20%20%20%20%20%20%20draw()%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20model.on(%22change%3Agrid_origin_x%22%2C%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20offsetX%20%3D%20parseFloat(model.get(%22grid_origin_x%22))%20%7C%7C%200%3B%0A%20%20%20%20%20%20%20%20draw()%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20model.on(%22change%3Agrid_origin_y%22%2C%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20offsetY%20%3D%20parseFloat(model.get(%22grid_origin_y%22))%20%7C%7C%200%3B%0A%20%20%20%20%20%20%20%20draw()%3B%0A%20%20%20%20%20%20%7D)%3B%0A%20%20%20%20%20%20model.on(%22change%3Aapply_filter%22%2C%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20applyFilter%20%3D%20model.get(%22apply_filter%22)%3B%0A%20%20%20%20%20%20%20%20draw()%3B%0A%20%20%20%20%20%20%7D)%3B%0A%0A%20%20%20%20%20%20%2F%2F%20Initial%20draw.%0A%20%20%20%20%20%20draw()%3B%0A%20%20%20%20%20%20model.set(%22ready%22%2C%20true)%3B%0A%20%20%20%20%20%20model.save_changes()%3B%0A%20%20%20%20%7D%0A%20%20%20%20export%20default%20%7B%20render%20%7D%3B%0A%20%20%20%20%22%22%22%0A%0A%20%20%20%20canvas_size%20%3D%20traitlets.Int(500).tag(sync%3DTrue)%0A%20%20%20%20grid_type%20%3D%20traitlets.Unicode(%22square%22).tag(sync%3DTrue)%20%20%20%23%20%22square%22%20or%20%22hex%22%0A%20%20%20%20cell_size%20%3D%20traitlets.Float(50.0).tag(sync%3DTrue)%0A%20%20%20%20orientation%20%3D%20traitlets.Float(0.0).tag(sync%3DTrue)%20%20%20%20%20%20%20%20%20%20%23%20degrees%0A%20%20%20%20points%20%3D%20traitlets.List(trait%3Dtraitlets.Dict()%2C%20default_value%3D%5B%5D).tag(sync%3DTrue)%0A%20%20%20%20heatmap_colors%20%3D%20traitlets.List(trait%3Dtraitlets.Unicode()%2C%20default_value%3D%5B%22%23009900AA%22%2C%20%22%23FFFF00AA%22%2C%20%22%23ff0000AA%22%5D).tag(sync%3DTrue)%0A%20%20%20%20grid_origin_x%20%3D%20traitlets.Float(0.0).tag(sync%3DTrue)%0A%20%20%20%20grid_origin_y%20%3D%20traitlets.Float(0.0).tag(sync%3DTrue)%0A%20%20%20%20interactive%20%3D%20traitlets.Bool(True).tag(sync%3DTrue)%0A%20%20%20%20apply_filter%20%3D%20traitlets.Bool(False).tag(sync%3DTrue)
import%20random%0A%0A%0Adef%20generate_points(n%2C%20canvas_size%2C%20seed%3DNone)%3A%0A%20%20%20%20if%20seed%3A%0A%20%20%20%20%20%20%20%20random.seed(seed)%0A%20%20%20%20return%20%5B%7B%22x%22%3A%20random.uniform(0%2C%20canvas_size)%2C%20%22y%22%3A%20random.uniform(0%2C%20canvas_size)%7D%20for%20_%20in%20range(n)%5D
mo.center(%0A%20%20%20%20MAUPWidget(%0A%20%20%20%20%20%20%20%20canvas_size%20%3D%20300%2C%0A%20%20%20%20%20%20%20%20grid_type%20%3D%20%22square%22%2C%0A%20%20%20%20%20%20%20%20cell_size%20%3D%20300%2C%0A%20%20%20%20%20%20%20%20points%20%3D%20generate_points(100%2C%20300%2C%20seed%3D1)%2C%0A%20%20%20%20%20%20%20%20heatmap_colors%3D%5B%22%23FFFFFF%22%5D%2C%0A%20%20%20%20%20%20%20%20interactive%3DFalse%2C%0A%20%20%20%20)%0A)
At first glance, it’s nearly impossible to spot any clusters or patterns among the noise. So, you think, “How can I make sense of this mess?” The smart move is to aggregate the data. You overlay a grid onto the map and break the country into larger zones. Then, using a choropleth with a well-chosen color scale, you paint areas with fewer infections in calming green and those with more infections in alarming red.
Now, the heatmap below might simply be yelling, “Avoid the east-central region—COVID is going wild there!” With just one quick glance, you can easily pinpoint the spots with the lowest infection rates, making it look like the perfect place for a safe, relaxing holiday.
mo.center(%0A%20%20%20%20MAUPWidget(%0A%20%20%20%20%20%20%20%20canvas_size%3D300%2C%0A%20%20%20%20%20%20%20%20grid_type%3D%22square%22%2C%0A%20%20%20%20%20%20%20%20cell_size%3D50%2C%0A%20%20%20%20%20%20%20%20points%3Dgenerate_points(100%2C%20300%2C%20seed%3D1)%2C%0A%20%20%20%20%20%20%20%20grid_origin_x%3D-127.9337158203125%2C%0A%20%20%20%20%20%20%20%20grid_origin_y%3D-2.0025634765625%2C%0A%20%20%20%20%20%20%20%20interactive%3DFalse%2C%0A%20%20%20%20)%0A)
Well, here’s where things get tricky. Notice that these squares aren’t set in stone, they exist purely by arbitrary choice. The grid’s position is nothing more than a decision made without any inherent logic. Now, imagine if we shifted the grid just a little to one side, the way the data aggregates could change completely.
view1%20%3D%20MAUPWidget(%0A%20%20%20%20canvas_size%3D300%2C%0A%20%20%20%20grid_type%3D%22square%22%2C%0A%20%20%20%20cell_size%3D50%2C%0A%20%20%20%20points%3Dgenerate_points(100%2C%20300%2C%20seed%3D1)%2C%0A%20%20%20%20grid_origin_x%3D-127.9337158203125%2C%0A%20%20%20%20grid_origin_y%3D-2.0025634765625%2C%0A%20%20%20%20interactive%3DFalse%2C%0A)%0Aview2%20%3D%20MAUPWidget(%0A%20%20%20%20canvas_size%20%3D%20300%2C%0A%20%20%20%20grid_type%20%3D%20%22square%22%2C%0A%20%20%20%20cell_size%20%3D%2050%2C%0A%20%20%20%20points%20%3D%20generate_points(100%2C%20300%2C%20seed%3D1)%2C%0A%20%20%20%20grid_origin_x%3D54.2108154296875%2C%0A%20%20%20%20grid_origin_y%3D123.05267333984375%2C%0A%20%20%20%20interactive%3DFalse%2C%0A)%0Amo.hstack(%5Bview1%2C%20mo.md(%22%24%5C%5Cto%24%22)%2C%20view2%5D%2C%20justify%3D%22center%22%2C%20align%3D%22center%22)
The underlaying data is exactly the same, dots have not been modified. However, the center-east of the map no longer looks dangerous. In general all areas are pretty much safe except for one redish square on the right. Different result that lead us to take completly different decisions.
That is what the Modifiable Areal Unit Problem (MAUP) is all about. It is a fancy name for something that boils down to: the way you group your data can massively affect the results you get.
Why Does This Happen?
It all comes down to aggregation. When the map’s dots (each representing an infected individual) are grouped into zones, the size, shape, and position of those zones can change the numbers you see. A tiny shift in the grid could lump a few extra dots into one cell, making it look like a hotspot. Conversely, a different configuration might spread out those dots more evenly, making the area appear safer than it really is.
Arbitrary Boundaries : Whether you’re using squares, hexagons, or even administrative regions, the lines that divide the map are, in many cases, arbitrary. A small nudge of these boundaries might push a few infection dots from one cell into another, altering the average infection rate.
Scale/Resolution : The cell size matters too. Large cells smooth out the data, potentially hiding small clusters of infections. On the other hand, smaller cells can highlight micro-clusters that may not be significant when viewed in a broader context.
Shape Differences : Different shapes capture data differently. A hexagon might cover an area in a way that minimizes boundary issues compared to a square. But again, that doesn’t mean one shape is inherently “better”—it just means your conclusions might vary depending on your choice.
canvas_size%20%3D%20mo.ui.slider(100%2C%20800%2C%20value%3D500%2C%20show_value%3DTrue)%0Aorientation%20%3D%20mo.ui.slider(0%2C%20360%2C%20value%3D0%2C%20show_value%3DTrue)%0Agrid_type%20%3D%20mo.ui.dropdown(options%3D%5B%22square%22%2C%20%22hex%22%5D%2C%20value%3D%22square%22)%0Acell_size%20%3D%20mo.ui.slider(10%2C%20150%2C%20value%3D50%2C%20show_value%3DTrue)%0Anum_points%20%3D%20mo.ui.slider(50%2C%20500%2C%20value%3D200%2C%20show_value%3DTrue)%0Aseed_slider%20%3D%20mo.ui.slider(0%2C%201000%2C%20value%3D124%2C%20show_value%3DTrue)%0Agauss_filter_switch%20%3D%20mo.ui.switch(False)%0A%0Aparams%20%3D%20mo.md(f%22%22%22%0A%7C%20Parameter%20%20%20%20%20%20%20%7C%20Value%20%20%20%20%20%20%20%20%20%7C%0A%7C-----------------%7C---------------%7C%0A%7C%20Canvas%20Size%20%20%20%20%20%7C%20%7Bcanvas_size%7D%20%7C%0A%7C%20Orientation%20%20%20%20%20%7C%20%7Borientation%7D%20%7C%0A%7C%20Grid%20Type%20%20%20%20%20%20%20%7C%20%7Bgrid_type%7D%20%20%20%7C%0A%7C%20Cell%20Size%20%20%20%20%20%20%20%7C%20%7Bcell_size%7D%20%20%20%7C%0A%7C%20N%C2%BA%20Dots%20%20%20%20%20%20%20%20%20%7C%20%7Bnum_points%7D%20%20%7C%0A%7C%20Seed%20%20%20%20%20%20%20%20%20%20%20%20%7C%20%7Bseed_slider%7D%20%7C%0A%7C%20Gaussian%20Filter%20%7C%20%7Bgauss_filter_switch%7D%20%7C%0A%22%22%22)%0A%0Aregenerate%20%3D%20mo.ui.button(label%3D%22Regenerate%22)
Play With It
Do not just take my word for it, try it yourself! Use the sliders to tweak the settings, then click and drag the grid on the canvas to see how different configurations completely transform the visualization.
Note: Setting the seed to 0 means that no fixed seed is used; each generation will be completely random. Simply click the ‘Regenerate’ button to redraw a new set of points and see different outcomes.
widget%20%3D%20MAUPWidget(%0A%20%20%20%20canvas_size%3Dcanvas_size.value%2C%0A%20%20%20%20grid_type%3Dgrid_type.value%2C%0A%20%20%20%20cell_size%3Dcell_size.value%2C%0A%20%20%20%20orientation%3Dorientation.value%2C%0A%20%20%20%20points%3Dgenerate_points(num_points.value%2C%20canvas_size.value%2C%20seed%3Dseed_slider.value%20if%20seed_slider.value%20else%20None)%2C%0A%20%20%20%20apply_filter%3Dgauss_filter_switch.value%2C%0A)%0Amo.vstack(%0A%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20params%2C%0A%20%20%20%20%20%20%20%20regenerate%2C%0A%20%20%20%20%20%20%20%20widget%0A%20%20%20%20%5D%2C%0A%20%20%20%20gap%3D2%2C%0A)
Solutions to MAUP
The MAUP can be addressed by using a variety of techniques, such as spatial smoothing, interpolation, modeling, or testing the sensitivity of the results to different spatial units used to represent data. However, in some cases, these techniques may not be sufficient to completely eliminate the effects of spatial aggregation on the results of the analysis.
For spatial smoothing, you can use the Gaussian filter toggle in the previous visualization. This helps to smooth out different areas by using neighboring values. Choropleth maps are more consistent when some form of smoothing is applied.
Conclusion
In the end, MAUP reminds us that there’s no one “correct” way to see the world—just different perspectives. A slight tweak in your grid can change the story entirely, so never settle for just one view. Explore multiple configurations, question the boundaries, and let curiosity guide you to truly informed insights.