├── 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 |
20 |
21 |
22 |
23 |
Explore supermarkets in the UK
24 |
25 |
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 | }
--------------------------------------------------------------------------------