├── screenshot.png ├── src ├── Leaflet.DonutCluster.css └── Leaflet.DonutCluster.js ├── README.md └── test ├── basic.html ├── sum.html ├── basic-jsfiddle.html └── sum-jsfiddle.html /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drFabio/Leaflet.DonutCluster/master/screenshot.png -------------------------------------------------------------------------------- /src/Leaflet.DonutCluster.css: -------------------------------------------------------------------------------- 1 | .donut-text { 2 | color: black; 3 | display: block; 4 | position: absolute; 5 | top: 50%; 6 | left: 0; 7 | z-index: 2; 8 | line-height: 0; 9 | width: 100%; 10 | text-align: center; 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Leaflet.DonutCluster 2 | ===================== 3 | 4 | A lightweight standalone [Leaflet](https://leafletjs.com) plugin to display donut charts instead of circles in map when using [Leaflet marker cluster](https://github.com/Leaflet/Leaflet.markercluster). This lib copies the codes which generate the donut svg from [donutjs](https://github.com/finom/donutjs). 5 | 6 | 7 | **Only depends on Leaflet and Leaflet.markercluster, NOT on other chart library like d3.js** 8 | 9 | - [Online Demo -- basic](https://jsfiddle.net/b43c1xkf/1/embedded/result,html/) 10 | - [Online Demo -- sum by field](https://jsfiddle.net/mfxd015b/3/embedded/result,html/) 11 | 12 | ![cluster map example](screenshot.png) 13 | 14 | 15 | 16 | ## Usage 17 | First include the Leaflet.DonutCluster.js, if you want to improve your performance, you could include the optional Leaflet.DonutCluster.css file, 18 | But you should comment the line 85 in the Leaflet.DonutCluster.js file as well. 19 | ```javascript 20 | text.setAttribute('style', ...) 21 | ``` 22 | Then use L.DonutCluster to create a markercluster instance. 23 | ```javascript 24 | //create the markercluster 25 | var markers = L.DonutCluster( 26 | //the first parameter is markercluster's configuration file 27 | { 28 | chunkedLoading: true 29 | } 30 | //the second parameter is DonutCluster's configuration file 31 | , { 32 | key: 'title', //mandotary, indicates the grouped field, set it in the options of marker 33 | sumField: 'value', // optional, indicates the value field to sum. set it in the options of marker 34 | order: ['A', 'D', 'B', 'C'], // optional, indicates the group order. 35 | title: ['Type A','Type D','Type B','Type C' ], // optional, indicates the group title, when it is an array, the order option must be specified. or use an object.{A:'Type A',D: 'Type D',B:'Type B',C:'Type C' } 36 | arcColorDict: { // mandotary, the arc color for each group. 37 | A: 'red', 38 | B: 'blue', 39 | C: 'yellow', 40 | D: 'black' 41 | } 42 | }) 43 | ``` 44 | Then add the marker into the markercluster. 45 | ```javascript 46 | var marker = L.marker(L.latLng(a[0], a[1]), { 47 | title: title //the value to group 48 | }); 49 | 50 | ... 51 | 52 | markers.addLayer(marker); 53 | ``` 54 | 55 | ## License 56 | 57 | MIT -------------------------------------------------------------------------------- /test/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Leaflet Demo 9 | 10 | 11 | 12 | 13 | 14 | 15 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /test/sum.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Leaflet Demo 9 | 10 | 11 | 12 | 13 | 14 | 25 | 26 | 27 | 28 |
29 | 30 | 31 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /test/basic-jsfiddle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Leaflet Demo 9 | 10 | 11 | 12 | 13 | 14 | 25 | 26 | 27 | 28 |
29 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /test/sum-jsfiddle.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Leaflet Demo 9 | 10 | 11 | 12 | 13 | 14 | 25 | 26 | 27 | 28 |
29 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /src/Leaflet.DonutCluster.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | (function (factory, window) { 3 | /*globals define, module, require*/ 4 | 5 | // define an AMD module that relies on 'leaflet' 6 | if (typeof define === 'function' && define.amd) { 7 | define(['leaflet'], factory); 8 | 9 | 10 | // define a Common JS module that relies on 'leaflet' 11 | } else if (typeof exports === 'object') { 12 | module.exports = factory(require('leaflet')); 13 | } 14 | 15 | // attach your plugin to the global 'L' variable 16 | if(typeof window !== 'undefined' && window.L){ 17 | factory(window.L); 18 | } 19 | 20 | }(function (L) { 21 | function roundToTwo(num) { 22 | return +(Math.round(num + "e+2") + "e-2"); 23 | } 24 | 25 | function readable(val) { 26 | if (val >= 1000 && val < 1000000) 27 | val = roundToTwo(val / 1000) + 'K' 28 | else if (val >= 1000000 && val < 1000000000) 29 | val = roundToTwo(val / 1000000) + 'M' 30 | else if (val >= 1000000000) 31 | val = roundToTwo(val / 1000000000) + 'B' 32 | return val; 33 | } 34 | var doc = document, 35 | M = Math, 36 | donutData = {}, 37 | dataIndex = 0; 38 | 39 | function donut(options) { 40 | var div = doc.createElement('div'), 41 | size = options.size || 100, 42 | size0 = size + 10, 43 | data = options.data || [{ 44 | value: 1 45 | }], 46 | weight = options.weight || 20, 47 | colors = options.colors || ['#555'], 48 | fillColor = options.fillColor || '#f1d357', 49 | el = options.el, 50 | r = size / 2, 51 | PI = M.PI, 52 | sin = M.sin, 53 | cos = M.cos, 54 | sum = 0, 55 | i, 56 | value, 57 | arc, 58 | text, 59 | legend, 60 | setAttribute = function (el, o) { 61 | for (var j in o) { 62 | el.setAttribute(j, o[j]); 63 | } 64 | }; 65 | 66 | for (i = 0; i < data.length; i++) { 67 | sum += data[i].value; 68 | } 69 | 70 | if (sum == 0) { 71 | for (i = 0; i < data.length; i++) { 72 | data[i].value = 1; 73 | sum += data[i].value; 74 | } 75 | } 76 | div.className = 'donut'; 77 | div.style.width = div.style.height = size0 + 'px'; 78 | div.style.position = 'relative'; 79 | 80 | text = div.appendChild(document.createElement('span')); 81 | 82 | text.className = 'donut-text'; 83 | 84 | //if css is included, please comment the next line for performance. 85 | text.setAttribute('style', 'color: black;display: block;position: absolute;top: 50%;left: 0;z-index: 2;line-height: 0;width: 100%;text-align: center;') 86 | 87 | text.innerHTML = readable(sum); 88 | legend = document.createElement('div'); 89 | 90 | 91 | var NS = 'http://www.w3.org/2000/svg', 92 | svg = doc.createElementNS(NS, 'svg'), 93 | startAngle = -PI / 2, 94 | arcRadius = r - weight / 2; 95 | 96 | svg.setAttribute('height', size0 + 'px'); 97 | svg.setAttribute('width', size0 + 'px'); 98 | 99 | var circle = doc.createElementNS(NS, 'circle'); 100 | circle.setAttribute('cx', size0 / 2.0); 101 | circle.setAttribute('cy', size0 / 2.0); 102 | circle.setAttribute('r', arcRadius - weight / 2); 103 | circle.setAttribute('fill', fillColor); 104 | circle.setAttribute('fill-opacity', 0.6); 105 | svg.appendChild(circle); 106 | // svg.innerHTML = '' 107 | 108 | div.appendChild(svg); 109 | 110 | for (i = 0; i < data.length; i++) { 111 | value = data[i].value / sum; 112 | value = value === 1 ? .99999 : value; 113 | arc = doc.createElementNS(NS, 'path'); 114 | var r1 = r + 5; 115 | var segmentAngle = value * PI * 2, 116 | endAngle = segmentAngle + startAngle, 117 | largeArc = ((endAngle - startAngle) % (PI * 2)) > PI ? 1 : 0, 118 | startX = r1 + cos(startAngle) * arcRadius, 119 | startY = r1 + sin(startAngle) * arcRadius, 120 | endX = r1 + cos(endAngle) * arcRadius, 121 | endY = r1 + sin(endAngle) * arcRadius; 122 | 123 | var name = data[i].name, 124 | c = Array.isArray(colors) ? colors[i % colors.length] : (colors[name] || '#fff'); 125 | 126 | startAngle = endAngle; 127 | 128 | setAttribute(arc, { 129 | d: [ 130 | 'M', startX, startY, 131 | 'A', arcRadius, arcRadius, 0, largeArc, 1, endX, endY 132 | ].join(' '), 133 | stroke: c, 134 | 'stroke-opacity': "0.7", 135 | 'stroke-width': weight, 136 | fill: 'none', 137 | 'data-name': name, 138 | 'class': 'donut-arc' 139 | }); 140 | donut.data(arc, data[i]); 141 | 142 | (function (d, c, perc) { 143 | if (perc == '99.99') 144 | perc = '100' 145 | if (options.onclick) { 146 | arc.addEventListener('click', function (e) { 147 | var t = e.target, 148 | val = readable(d.value); 149 | if (t.parentNode.stick != t) { 150 | t.parentNode.stick = t; 151 | } else t.parentNode.stick = false; 152 | options.onclick(d.name, !!t.parentNode.stick); 153 | }) 154 | } 155 | 156 | arc.addEventListener('mouseenter', function (e) { 157 | var t = e.target, 158 | val = readable(d.value); 159 | 160 | 161 | t.setAttribute('stroke-width', weight + 5); 162 | legend.setAttribute('class', 'legend'); 163 | div.zIndex = div.parentNode.style.zIndex; 164 | div.parentNode.style.zIndex = 100000; 165 | text.innerHTML = val; 166 | t.saved = { 167 | val: d.value, 168 | legend: '' + (d.title || d.name) + ': ' + perc + '%' 169 | } 170 | legend.innerHTML = t.saved.legend; 171 | }) 172 | arc.addEventListener('mouseleave', function (e) { 173 | var t = e.target, 174 | stick = t.parentNode.stick; 175 | if (stick == t) { 176 | return; 177 | } 178 | t.setAttribute('stroke-width', weight); 179 | var saved = { 180 | val: sum, 181 | legend: '' 182 | } 183 | if (stick) { 184 | saved = stick.saved; 185 | } 186 | div.parentNode.style.zIndex = div.zIndex; 187 | text.innerHTML = readable(saved.val); 188 | legend.innerHTML = saved.legend; 189 | }) 190 | })(data[i], c, (value * 100 + '').substr(0, 5)) 191 | svg.appendChild(arc); 192 | if (data[i].active) { 193 | svg.stick = arc; 194 | var event = new MouseEvent('mouseenter', { 195 | view: window, 196 | bubbles: false, 197 | cancelable: true 198 | }); 199 | arc.dispatchEvent(event); 200 | arc.setAttribute('stroke-width', weight); 201 | } 202 | } 203 | 204 | 205 | div.appendChild(legend); 206 | if (el) { 207 | el.appendChild(div) 208 | } 209 | 210 | return div; 211 | }; 212 | 213 | 214 | donut.data = function (arc, data) { 215 | if (typeof data === 'undefined') { 216 | return donutData[arc._DONUT]; 217 | } else { 218 | donutData[arc._DONUT = arc._DONUT || ++dataIndex] = data; 219 | return arc; 220 | } 221 | }; 222 | 223 | donut.setColor = function (arc, color) { 224 | arc.setAttribute('stroke', color); 225 | return arc; 226 | }; 227 | 228 | function createDonut(points, opt, cfgFn) { 229 | var blocks = {}, 230 | count = points.length, 231 | key = opt.key, 232 | sumField = opt.sumField, 233 | fieldList = opt.order || (opt.order = []), 234 | fieldDict = opt.orderDict || (opt.orderDict={}), 235 | titleDict = opt.title || {}, 236 | cfg = {}; 237 | if (typeof cfgFn == 'function') 238 | cfg = cfgFn(points); 239 | else if (typeof cfgFn == 'object') { 240 | cfg = cfgFn 241 | } 242 | if(Array.isArray(opt.title) && opt.order){ 243 | titleDict = {}; 244 | for(var i in opt.title){ 245 | titleDict[opt.order[i]] = opt.title[i] 246 | } 247 | opt.title = titleDict; 248 | } 249 | for(var i in fieldList){ 250 | fieldDict[fieldList[i]] = 1; 251 | } 252 | 253 | for (var i = 0; i < count; i++) { 254 | var s = points[i].options[key] 255 | if (!blocks[s]) blocks[s] = 0; 256 | if (!fieldDict[s]) { 257 | fieldDict[s] = 1; 258 | fieldList.push(s); 259 | } 260 | 261 | if (!sumField) 262 | blocks[s]++; 263 | else blocks[s] += points[i].options[sumField]; 264 | } 265 | var list = []; 266 | 267 | for(var i in fieldList){ 268 | var s = fieldList[i]; 269 | list.push({ 270 | value: blocks[s] || 0, 271 | name: s, 272 | title: titleDict[s], 273 | active: cfg.active && cfg.active == s 274 | }); 275 | } 276 | 277 | var size = cfg.size || 50, 278 | weight = cfg.weight || 10, 279 | colors = cfg.colors; 280 | 281 | var myDonut = donut({ 282 | size: size, 283 | weight: weight, 284 | data: list, 285 | onclick: cfg.onclick, 286 | colors: colors, 287 | fillColor: cfg.fillColor 288 | }); 289 | myDonut.config = cfg; 290 | return myDonut; 291 | } 292 | // to use donut as icon 293 | L.DivIcon.prototype.createIcon = function (oldIcon) { 294 | var div = (oldIcon && oldIcon.tagName === 'DIV') ? oldIcon : document.createElement('div'), 295 | options = this.options; 296 | 297 | if (options.el) { 298 | div.appendChild(options.el); 299 | } else 300 | div.innerHTML = options.html !== false ? options.html : ''; 301 | 302 | if (options.bgPos) { 303 | var bgPos = L.Point(options.bgPos); 304 | div.style.backgroundPosition = (-bgPos.x) + 'px ' + (-bgPos.y) + 'px'; 305 | } 306 | this._setIconStyles(div, 'icon'); 307 | 308 | return div; 309 | } 310 | 311 | function defaultStyle(points) { 312 | var count = points.length, 313 | size, weight, fill; 314 | if (count < 10) { 315 | size = 40; 316 | weight = 8; 317 | // c += 'small'; 318 | fill = '#6ecc39'; 319 | } else if (count < 100) { 320 | size = 50; 321 | weight = 10; 322 | // c += 'medium'; 323 | fill = '#f1d357' 324 | } else { 325 | size = 60; 326 | weight = 12; 327 | // c += 'large'; 328 | fill = '#fd9c73' 329 | } 330 | return { 331 | size: size, 332 | weight: weight, 333 | fill: fill 334 | } 335 | } 336 | /** 337 | * 338 | * @param {object} opt marker cluster's options 339 | * @param {object} donutOpt donut cluster's options 340 | * 341 | */ 342 | L.DonutCluster = function (opt, donutOpt) { 343 | 344 | 345 | var createIcon = function (cluster) { 346 | var markers = cluster.getAllChildMarkers(); 347 | var myDonut = createDonut(markers, donutOpt, function (points) { 348 | var style; 349 | if (!donutOpt.style) { 350 | style = defaultStyle(points) 351 | } else { 352 | if (typeof donutOpt.style == 'function') { 353 | style = donutOpt.style(points); 354 | } else style = donutOpt.style; 355 | } 356 | return { 357 | size: style.size, 358 | weigth: style.weight, 359 | colors: donutOpt.arcColorDict, 360 | fillColor: style.fill 361 | } 362 | }) 363 | 364 | return new L.DivIcon({ 365 | el: myDonut, 366 | iconSize: new L.Point(myDonut.config.size + 10, myDonut.config.size + 10), 367 | className: 'donut-cluster' 368 | }); 369 | 370 | } 371 | opt.iconCreateFunction = createIcon; 372 | return L.markerClusterGroup(opt, donutOpt); 373 | } 374 | 375 | // donut.readable = readable; 376 | // window.donut = donut; 377 | }, window)); --------------------------------------------------------------------------------