Skip to content

Commit d8965d6

Browse files
authored
Merge pull request #4 from balladaniel/legend_HTML_template
Legend rows: custom HTML template option - replaced the complex 'if' logic in legend row creation with a smarter function, which makes use of new option `legendTemplate` (suggested by ConsultBuckner in issue #3) - New option: `legendTemplate`: object with distinct formatting for the 'highest', 'middle', 'lowest' classes using properties, which all accept HTML template strings. In the strings, {low} and {high} placeholders can be used, low/high values are understood in the context of the current class interval. - updated documentation - updated examples (`points_c.html` and `combined.html` both showcase HTML templating)
2 parents bfd7663 + cc7981e commit d8965d6

File tree

4 files changed

+84
-12
lines changed

4 files changed

+84
-12
lines changed

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Aims to simplify data visualization and creation of elegant thematic web maps wi
2121
- Legend generation with options for:
2222
- class order (ascending/descending)
2323
- legend header (title)
24+
- custom HTML templating of legend rows
2425
- rounding of class boundary values to n decimals or up/down to the nearest 10, 100, 1000 etc. numbers
2526
- modifying class boundary values in legend by dividing/multiplying by a number (to easily change unit of measurement from m to km for example)
2627
- positioning (L.control options)
@@ -67,6 +68,11 @@ const layer = L.dataClassification(data, {
6768
legendTitle: 'Density (pop/km²)',
6869
legendPosition: 'bottomleft',
6970
legendAscending: false,
71+
legendTemplate: {
72+
highest: '{low} and above',
73+
middle: '{low} – {high}',
74+
lowest: 'below {high}'
75+
},
7076
unitModifier: {action: 'divide', by: 1000},
7177
style: {
7278
fillColor: 'purple', // marker fill color in point/size mode
@@ -107,6 +113,10 @@ const layer = L.dataClassification(data, {
107113
- `legendTitle <string>`: legend header (usually a description of visualized data, with a unit of measurement). HTML-markdown and styling allowed. To hide header, set this as ''. (by default it inherits target attribute field name, on which the classification is based on)
108114
- `legendPosition <string>`: ['topleft'|'topright'|'bottomleft'|'bottomright'] legend position, L.control option. (default: 'bottomleft')
109115
- `legendAscending <boolean>`: if true, value classes in legend will be ascending (low first, high last) (default: false)
116+
- `legendTemplate <object>`: custom HTML formatting of legend rows using {high} and {low} placeholders (interpreted as high/low value in the context of a given class interval). Distinct formatting for the highest, lowest and middle class intervals. Middle class format requires both {high} and {low}, highest only {low} and lowest only {high}.
117+
- `highest <string>`: template for the upper end of classes, "highest value and above" (default: '{low} <')
118+
- `middle <string>`: template for rows in the middle, "low to high" (default: '{low} – {high}')
119+
- `lowest <string>`: template for the lower end of classes, "lowest value and below" (default: '< {high}')
110120
- `unitModifier <object>`: modifies the final class boundary values in order to multiply/divide them by a number. Useful for example when a dataset attribute is in metres, but kilometres would fit the legend better (786000 metres shown as 786 km). Purely visual, only affects legend. Happens after classRounding.
111121
- `action <string>`: ['divide'|'multiply'] action to take on the number specified by `by`. Required for `unitModifier`.
112122
- `by <number>`: a number to divide/multiply class boundary values with. Required for `unitModifier`.

examples/combined.html

+5
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
legendTitle: 'Population of capitals',
8080
attribution: "US Capitals: <a href='https://mangomap.com/examples/data/7986d760_1129_11e6_8e77_22000bb3a3a1/us-state-capitals' target='_blank'>Mangomap (2018)</a>",
8181
pane: 'front',
82+
legendTemplate: {highest: '{low}+'},
8283
onEachFeature: tooltip
8384
}).addTo(map);
8485
});
@@ -137,6 +138,10 @@
137138
action: 'divide',
138139
by: 1000
139140
},
141+
legendTemplate: {
142+
highest: '{low} and above',
143+
lowest: 'under {high}'
144+
},
140145
attribution: "Rivers (10m scale): <a href='https://www.naturalearthdata.com/' target='_blank'>Natural Earth</a>",
141146
pane: 'front',
142147
onEachFeature: tooltip

examples/points_c.html

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
classRounding: 2,
7373
legendTitle: 'Diesel prices (€/l)<br>on 7-Jul-2023 ',
7474
legendPosition: 'bottomright',
75+
legendTemplate: {highest: '{low} and above', lowest: 'under {high}'},
7576
attribution: "Diesel prices (7-Jul-2023): <a href='https://www.prix-carburants.gouv.fr/rubrique/opendata/'>Ministère de l'Economie, de l'Industrie et du Numérique</a>",
7677
onEachFeature: tooltip
7778
}).addTo(map);

leaflet-dataclassification.js

+68-12
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ L.DataClassification = L.GeoJSON.extend({
3838
classRounding: null, // class boundary value rounding. Positive numbers round to x decimals, zero will round to whole numbers, negative numbers will round values to the nearest 10, 100, 1000, etc. (default: null - no rounding, values are used as-is)
3939
unitModifier: null, // modifies the final class boundary values in order to multiply/divide them. Useful when a dataset attribute is in metres, but kilometres would fit the legend better, for example 786000 metres shown as 786 km. Purely visual, only affects legend.
4040
legendPosition: 'bottomleft', // Legend position (L.control option: 'topleft', 'topright', 'bottomleft' or 'bottomright')
41+
legendTemplate: { // Legend row template for custom formatting using {high} and {low} placeholders (interpreted as high/low value in the context of a given class). Distinct formatting for the highest, lowest and middle classes (legend rows). Middle class format requires both {high} and {low}, highest only {low} and lowest only {high}
42+
highest: '{low} <',
43+
middle: '{low} – {high}',
44+
lowest: '< {high}',
45+
},
4146

4247
style: {
4348
fillColor: 'orange',
@@ -60,6 +65,7 @@ L.DataClassification = L.GeoJSON.extend({
6065
_linecolor: '',
6166
_lineweight: '',
6267
_legendPos: '',
68+
_legendTemplate: {},
6369

6470
// value evaluators to match classes
6571
/**
@@ -324,15 +330,58 @@ L.DataClassification = L.GeoJSON.extend({
324330
return;
325331
},
326332

333+
_legendRowFormatter(low, high, i, asc) {
334+
// solve row based on the 3 row templates
335+
switch (asc) {
336+
case true:
337+
if (i == classes.length-1) {
338+
// highest
339+
let solved_high = template.highest.replace(/({high})/i, high)
340+
solved_high = solved_high.replace(/({low})/i, low)
341+
return solved_high;
342+
} else if (i == 0) {
343+
// lowest
344+
let solved_low = template.lowest.replace(/({high})/i, high)
345+
solved_low = solved_low.replace(/({low})/i, low)
346+
return solved_low;
347+
} else {
348+
// middle
349+
let solved_mid = template.middle.replace(/({high})/i, high)
350+
solved_mid = solved_mid.replace(/({low})/i, low)
351+
return solved_mid;
352+
};
353+
case false:
354+
if (i == classes.length) {
355+
// highest
356+
let solved_high = template.highest.replace(/({high})/i, high)
357+
solved_high = solved_high.replace(/({low})/i, low)
358+
return solved_high;
359+
} else if (i == 1) {
360+
// lowest
361+
let solved_low = template.lowest.replace(/({high})/i, high)
362+
solved_low = solved_low.replace(/({low})/i, low)
363+
return solved_low;
364+
} else {
365+
// middle
366+
let solved_mid = template.middle.replace(/({high})/i, high)
367+
solved_mid = solved_mid.replace(/({low})/i, low)
368+
return solved_mid;
369+
};
370+
}
371+
},
372+
327373
_generateLegend(title, asc, mode_line, mode_point, typeOfFeatures, pfc) {
328374
svgCreator = this._svgCreator;
329375
legendPP_unitMod = this._legendPostProc_unitModifier;
376+
legendRowFormatter = this._legendRowFormatter;
330377
unitMod_options = this._unitMod;
331378
position = this._legendPos;
332379
ps = this._pointShape;
333380
lc = this._linecolor;
334381
lw = this._lineweight;
335382

383+
template = this._legendTemplate;
384+
336385
// unitModifier process:
337386
if (unitMod_options != null) {
338387
if (unitMod_options.hasOwnProperty('action') && unitMod_options.action != null && typeof unitMod_options.action == "string" && unitMod_options.hasOwnProperty('by') && unitMod_options.by != null && typeof unitMod_options.by == "number") {
@@ -373,7 +422,7 @@ L.DataClassification = L.GeoJSON.extend({
373422
container +=
374423
'<div style="display: flex; flex-direction: row; align-items: center">'+
375424
svgCreator({shape: ps, color: colors[i]})+
376-
'<div>'+ (i == 0 ? '< ' + high : (!high ? low + ' <' : low + ' &ndash; ' + high)) +'</div>'+
425+
'<div>'+ legendRowFormatter(low, high, i, asc) +'</div>'+
377426
'</div>';
378427
}
379428
break;
@@ -386,7 +435,7 @@ L.DataClassification = L.GeoJSON.extend({
386435
container +=
387436
'<div style="display: flex; flex-direction: row; align-items: center">'+
388437
svgCreator({shape: ps, size: radiuses[i], color: pfc})+
389-
'<div>'+ (i == 0 ? '< ' + high : (!high ? low + ' <' : low + ' &ndash; ' + high)) +'</div>'+
438+
'<div>'+ legendRowFormatter(low, high, i, asc) +'</div>'+
390439
'</div>';
391440
}
392441
break;
@@ -404,20 +453,22 @@ L.DataClassification = L.GeoJSON.extend({
404453
container +=
405454
'<div style="display: flex; flex-direction: row; align-items: center">'+
406455
svgCreator({shape: ps, color: colors[i-1]})+
407-
'<div>'+ (!high ? low + ' <' : (i != 1 ? low + ' &ndash; ' + high : '< ' + high))+'</div>'+
456+
'<div>'+ legendRowFormatter(low, high, i, asc) +'</div>'+
408457
'</div>';
409458
}
410459
break;
411460
case 'size':
412461
// size (radius) based categories
413462
for (var i = classes.length; i > 0; i--) {
414-
/*console.log('Legend: building line', i)*/
463+
// decide low and high boundary values for current legend row (class)
415464
let low = classes[i-1];
416465
let high = classes[i];
417-
container +=
466+
467+
// generate row with symbol
468+
container +=
418469
'<div style="display: flex; flex-direction: row; align-items: center">'+
419470
svgCreator({shape: ps, size: radiuses[i-1], color: pfc})+
420-
'<div>'+ (!high ? low + ' <' : (i != 1 ? low + ' &ndash; ' + high : '< ' + high))+'</div>'+
471+
'<div>'+ legendRowFormatter(low, high, i, asc) +'</div>'+
421472
'</div>';
422473
}
423474
break;
@@ -441,7 +492,7 @@ L.DataClassification = L.GeoJSON.extend({
441492
'<svg width="25" height="25" viewBox="0 0 25 25" style="margin-left: 4px; margin-right: 10px">'+
442493
'<line x1="0" y1="12.5" x2="25" y2="12.5" style="stroke-width: '+lw+'; stroke: '+colors[i]+';"/>'+
443494
'</svg>' +
444-
'<div>'+ (i == 0 ? '< ' + high : (!high ? low + ' <' : low + ' &ndash; ' + high)) +'</div>'+
495+
'<div>'+ legendRowFormatter(low, high, i, asc) +'</div>'+
445496
'</div>';
446497
}
447498
break;
@@ -456,7 +507,7 @@ L.DataClassification = L.GeoJSON.extend({
456507
'<svg width="25" height="25" viewBox="0 0 25 25" style="margin-left: 4px; margin-right: 10px">'+
457508
'<line x1="0" y1="12.5" x2="25" y2="12.5" style="stroke-width: '+widths[i]+'; stroke: '+lc+';"/>'+
458509
'</svg>'+
459-
'<div>'+ (i == 0 ? '< ' + high : (!high ? low + ' <' : low + ' &ndash; ' + high)) +'</div>'+
510+
'<div>'+ legendRowFormatter(low, high, i, asc) +'</div>'+
460511
'</div>';
461512
}
462513
break;
@@ -476,7 +527,7 @@ L.DataClassification = L.GeoJSON.extend({
476527
'<svg width="25" height="25" viewBox="0 0 25 25" style="margin-left: 4px; margin-right: 10px">'+
477528
'<line x1="0" y1="12.5" x2="25" y2="12.5" style="stroke-width: '+lw+'; stroke: '+colors[i-1]+';"/>'+
478529
'</svg>' +
479-
'<div>'+ (!high ? low + ' <' : (i != 1 ? low + ' &ndash; ' + high : '< ' + high))+'</div>'+
530+
'<div>'+ legendRowFormatter(low, high, i, asc) +'</div>'+
480531
'</div>'
481532
}
482533
break;
@@ -491,7 +542,7 @@ L.DataClassification = L.GeoJSON.extend({
491542
'<svg width="25" height="25" viewBox="0 0 25 25" style="margin-left: 4px; margin-right: 10px">'+
492543
'<line x1="0" y1="12.5" x2="25" y2="12.5" style="stroke-width: '+widths[i-1]+'; stroke: '+lc+';"/>'+
493544
'</svg>'+
494-
'<div>'+ (!high ? low + ' <' : (i != 1 ? low + ' &ndash; ' + high : '< ' + high))+'</div>'+
545+
'<div>'+ legendRowFormatter(low, high, i, asc) +'</div>'+
495546
'</div>'
496547
}
497548
break;
@@ -510,7 +561,7 @@ L.DataClassification = L.GeoJSON.extend({
510561
container +=
511562
'<div style="display: flex; flex-direction: row; align-items: center">'+
512563
'<i style="background: ' + colors[i] + '"></i> ' +
513-
'<div>'+ (i == 0 ? '< ' + high : (!high ? low + ' <' : low + ' &ndash; ' + high)) +'</div>'+
564+
'<div>'+ legendRowFormatter(low, high, i, asc) +'</div>'+
514565
'</div>';
515566
}
516567
break;
@@ -523,7 +574,7 @@ L.DataClassification = L.GeoJSON.extend({
523574
container +=
524575
'<div style="display: flex; flex-direction: row; align-items: center">'+
525576
'<i style="background: ' + colors[i-1] + '"></i>' +
526-
'<div>'+ (!high ? low + ' <' : (i != 1 ? low + ' &ndash; ' + high : '< ' + high))+'</div>'+
577+
'<div>'+ legendRowFormatter(low, high, i, asc) +'</div>'+
527578
'</div>'
528579
}
529580
break;
@@ -652,6 +703,11 @@ L.DataClassification = L.GeoJSON.extend({
652703
var pointfillcolor = this.options.style.fillColor;
653704
this._unitMod = this.options.unitModifier;
654705
this._legendPos = this.options.legendPosition;
706+
this._legendTemplate = this.options.legendTemplate;
707+
// fallback to default when user only specified one of the three custom templates
708+
if (!this._legendTemplate.hasOwnProperty('highest')) {this._legendTemplate.highest = '{low} <'};
709+
if (!this._legendTemplate.hasOwnProperty('middle')) {this._legendTemplate.middle = '{low} – {high}'};
710+
if (!this._legendTemplate.hasOwnProperty('lowest')) {this._legendTemplate.lowest = '< {high}'};
655711

656712
// classification process
657713
var success = false;

0 commit comments

Comments
 (0)