├── README.md ├── LICENSE.txt ├── examples └── uk-supermarkets │ ├── index.html │ └── base.css └── lib └── voronoi_map.js /README.md: -------------------------------------------------------------------------------- 1 | ### Voronoi Maps 2 | 3 | This project can be used to generate [Voronoi maps](http://chriszetter.com/blog/2014/06/14/visualising-supermarkets-with-a-voronoi-diagram/) using D3 and Leaflet. 4 | 5 | See an [explanation of the code](http://chriszetter.com/blog/2014/06/15/building-a-voronoi-map-with-d3-and-leaflet/) and a [demo](http://chriszetter.com/voronoi-map/examples/uk-supermarkets/). 6 | 7 | The code is released under the The MIT License. The names and the details the stores used in the `uk-supermarkets` example remain copyright of their respective owners. 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Chris Zetter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/uk-supermarkets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 | Choose what supermarkets to display 14 |
15 | Hide 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |

Explore supermarkets in the UK

24 |
25 |
26 | About 27 |

28 | Explore UK supermarkets using a voronoi diagram. Created by Chris Zetter, powered by data from SuperLocate, maps copyright 29 | Mapbox and OpenStreetMap. 30 | Hide 31 |

32 | 33 | 34 | 35 | 36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/uk-supermarkets/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: Helvetica, Arial, sans-serif; 4 | font-size: 14px; 5 | } 6 | 7 | p { 8 | margin: 0; 9 | margin-bottom: 10px; 10 | } 11 | 12 | .point-cell { 13 | fill: none; 14 | pointer-events: all; 15 | stroke: #000; 16 | stroke-opacity: .2; 17 | } 18 | 19 | .point-cell:hover, .point-cell.selected { 20 | fill: none; 21 | stroke: #000; 22 | stroke-opacity: .6; 23 | stroke-width: 2px; 24 | } 25 | 26 | .point-cell.selected { 27 | stroke-opacity: 1; 28 | stroke-width: 3px; 29 | } 30 | 31 | .point circle { 32 | pointer-events: none; 33 | } 34 | 35 | #map { 36 | position:absolute; 37 | top:0; 38 | bottom:0; 39 | width:100%; 40 | } 41 | 42 | #selected, 43 | #selections, 44 | #loading:after, 45 | #about { 46 | position:absolute; 47 | background-color: #FFF; 48 | opacity: 0.8; 49 | border-radius: 2px; 50 | padding: 10px 10px 0 10px; 51 | } 52 | 53 | #about { 54 | bottom: 10px; 55 | right: 10px; 56 | } 57 | 58 | #about.visible { 59 | width: 200px; 60 | } 61 | 62 | #about .hide { 63 | padding-bottom: 0; 64 | text-align: right; 65 | } 66 | 67 | #loading.visible:after { 68 | top: 50%; 69 | left: 50%; 70 | height: 28px; 71 | width: 80px; 72 | margin-left: -50px; 73 | margin-top: -30px; 74 | content: 'drawing...'; 75 | font-size: 18px; 76 | } 77 | 78 | #selections { 79 | right:10px; 80 | top:10px; 81 | width: 190px; 82 | } 83 | 84 | #selections label { 85 | display: block; 86 | padding-bottom: 8px; 87 | } 88 | 89 | #selections input[type=checkbox] { 90 | position: relative; 91 | top: -1px; 92 | } 93 | 94 | #selections .key { 95 | display: inline-block; 96 | width: 12px; 97 | height: 12px; 98 | border-radius: 6px; 99 | margin: 0 5px; 100 | } 101 | 102 | #selected { 103 | bottom: 10px; 104 | left: 10px; 105 | height: 28px; 106 | } 107 | 108 | #selected h1 { 109 | font-size: 20px; 110 | margin: 0px; 111 | line-height: 20px; 112 | font-weight: bold; 113 | white-space: nowrap; 114 | overflow: hidden; 115 | text-overflow: ellipsis; 116 | } 117 | 118 | .hide, 119 | .show { 120 | padding-bottom: 10px; 121 | display: block; 122 | } 123 | 124 | .content { 125 | display: none; 126 | } 127 | 128 | @media (min-width: 480px) { 129 | .selections .content { 130 | display: block; 131 | } 132 | .selections .show { 133 | display: none; 134 | } 135 | } 136 | 137 | .hidden .content, 138 | .visible .show { 139 | display: none; 140 | } 141 | 142 | .hidden .show, 143 | .visible .content { 144 | display: block; 145 | } 146 | 147 | @media (max-width: 480px) { 148 | #selected { 149 | box-sizing: border-box; 150 | width: 80%; 151 | height: 32px; 152 | } 153 | 154 | #selected h1 { 155 | font-size: 15px; 156 | line-height: 15px; 157 | font-weight: bold; 158 | } 159 | } 160 | 161 | .mapbox-control-info { 162 | display: none !important; 163 | } -------------------------------------------------------------------------------- /lib/voronoi_map.js: -------------------------------------------------------------------------------- 1 | showHide = function(selector) { 2 | d3.select(selector).select('.hide').on('click', function(){ 3 | d3.select(selector) 4 | .classed('visible', false) 5 | .classed('hidden', true); 6 | }); 7 | 8 | d3.select(selector).select('.show').on('click', function(){ 9 | d3.select(selector) 10 | .classed('visible', true) 11 | .classed('hidden', false); 12 | }); 13 | } 14 | 15 | voronoiMap = function(map, url, initialSelections) { 16 | var pointTypes = d3.map(), 17 | points = [], 18 | lastSelectedPoint; 19 | 20 | var voronoi = d3.geom.voronoi() 21 | .x(function(d) { return d.x; }) 22 | .y(function(d) { return d.y; }); 23 | 24 | var selectPoint = function() { 25 | d3.selectAll('.selected').classed('selected', false); 26 | 27 | var cell = d3.select(this), 28 | point = cell.datum(); 29 | 30 | lastSelectedPoint = point; 31 | cell.classed('selected', true); 32 | 33 | d3.select('#selected h1') 34 | .html('') 35 | .append('a') 36 | .text(point.name) 37 | .attr('href', point.url) 38 | .attr('target', '_blank') 39 | } 40 | 41 | var drawPointTypeSelection = function() { 42 | showHide('#selections') 43 | labels = d3.select('#toggles').selectAll('input') 44 | .data(pointTypes.values()) 45 | .enter().append("label"); 46 | 47 | labels.append("input") 48 | .attr('type', 'checkbox') 49 | .property('checked', function(d) { 50 | return initialSelections === undefined || initialSelections.has(d.type) 51 | }) 52 | .attr("value", function(d) { return d.type; }) 53 | .on("change", drawWithLoading); 54 | 55 | labels.append("span") 56 | .attr('class', 'key') 57 | .style('background-color', function(d) { return '#' + d.color; }); 58 | 59 | labels.append("span") 60 | .text(function(d) { return d.type; }); 61 | } 62 | 63 | var selectedTypes = function() { 64 | return d3.selectAll('#toggles input[type=checkbox]')[0].filter(function(elem) { 65 | return elem.checked; 66 | }).map(function(elem) { 67 | return elem.value; 68 | }) 69 | } 70 | 71 | var pointsFilteredToSelectedTypes = function() { 72 | var currentSelectedTypes = d3.set(selectedTypes()); 73 | return points.filter(function(item){ 74 | return currentSelectedTypes.has(item.type); 75 | }); 76 | } 77 | 78 | var drawWithLoading = function(e){ 79 | d3.select('#loading').classed('visible', true); 80 | if (e && e.type == 'viewreset') { 81 | d3.select('#overlay').remove(); 82 | } 83 | setTimeout(function(){ 84 | draw(); 85 | d3.select('#loading').classed('visible', false); 86 | }, 0); 87 | } 88 | 89 | var draw = function() { 90 | d3.select('#overlay').remove(); 91 | 92 | var bounds = map.getBounds(), 93 | topLeft = map.latLngToLayerPoint(bounds.getNorthWest()), 94 | bottomRight = map.latLngToLayerPoint(bounds.getSouthEast()), 95 | existing = d3.set(), 96 | drawLimit = bounds.pad(0.4); 97 | 98 | filteredPoints = pointsFilteredToSelectedTypes().filter(function(d) { 99 | var latlng = new L.LatLng(d.latitude, d.longitude); 100 | 101 | if (!drawLimit.contains(latlng)) { return false }; 102 | 103 | var point = map.latLngToLayerPoint(latlng); 104 | 105 | key = point.toString(); 106 | if (existing.has(key)) { return false }; 107 | existing.add(key); 108 | 109 | d.x = point.x; 110 | d.y = point.y; 111 | return true; 112 | }); 113 | 114 | voronoi(filteredPoints).forEach(function(d) { d.point.cell = d; }); 115 | 116 | var svg = d3.select(map.getPanes().overlayPane).append("svg") 117 | .attr('id', 'overlay') 118 | .attr("class", "leaflet-zoom-hide") 119 | .style("width", map.getSize().x + 'px') 120 | .style("height", map.getSize().y + 'px') 121 | .style("margin-left", topLeft.x + "px") 122 | .style("margin-top", topLeft.y + "px"); 123 | 124 | var g = svg.append("g") 125 | .attr("transform", "translate(" + (-topLeft.x) + "," + (-topLeft.y) + ")"); 126 | 127 | var svgPoints = g.attr("class", "points") 128 | .selectAll("g") 129 | .data(filteredPoints) 130 | .enter().append("g") 131 | .attr("class", "point"); 132 | 133 | var buildPathFromPoint = function(point) { 134 | return "M" + point.cell.join("L") + "Z"; 135 | } 136 | 137 | svgPoints.append("path") 138 | .attr("class", "point-cell") 139 | .attr("d", buildPathFromPoint) 140 | .on('click', selectPoint) 141 | .classed("selected", function(d) { return lastSelectedPoint == d} ); 142 | 143 | svgPoints.append("circle") 144 | .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }) 145 | .style('fill', function(d) { return '#' + d.color } ) 146 | .attr("r", 2); 147 | } 148 | 149 | var mapLayer = { 150 | onAdd: function(map) { 151 | map.on('viewreset moveend', drawWithLoading); 152 | drawWithLoading(); 153 | } 154 | }; 155 | 156 | showHide('#about'); 157 | 158 | map.on('ready', function() { 159 | d3.csv(url, function(csv) { 160 | points = csv; 161 | points.forEach(function(point) { 162 | pointTypes.set(point.type, {type: point.type, color: point.color}); 163 | }) 164 | drawPointTypeSelection(); 165 | map.addLayer(mapLayer); 166 | }) 167 | }); 168 | } --------------------------------------------------------------------------------