├── 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 | 
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));
--------------------------------------------------------------------------------