Skip to content

Commit e769860

Browse files
akacarlyannryanbaumann
authored andcommitted
Variable radius legend (#135)
* Add function * Add test notebook for variable radius legend * Fix link markup in readme (#124) * Update README.rst (#123) Add import so that example runs * upgrade to gl js v49 (#125) * upgrade to v0.9.0 * Support vector tile source for additional viz types (#120) * Update text-size to use expression based on viz.label_size property * Add new label properties to choropleth viz templates * Move label properties to base class (leverage inheritance to reduce repeated arguments) * Extract vector_color_map and numeric map (for height or line_width) to a VectorMixin class; fixes linestring bug with interpolation of color for certain color lookups with match-type * Extend CircleViz with VectorMixin (start GraduatedCircleViz, HeatmapViz, ClusteredCircleViz) * Extend vector data loading to base map and CircleMap -- datadriven styling for radius needs work * Refine color mapping in VectorMixin and update templates, viz.py * Update CircleViz template files with Jinja inheritance, establish {% block circle %} tag * Update Vector layer example for CircleViz; add geojson_file_to_dict utility and logic to viz.py to facilitate loading data from JSON object, list of Python dicts, GeoJSON filename * Refine function for parsing GeoJSON and JSON input (esp for vector visualizations) and add SourceDataError * Add support for GraduatedCircleViz template to use vector source data layer * Add support for HeatmapViz to use vector data source * Add and refine tests; update utility name for geojson_to_dict etc. * Change FileNotFoundError to IOError for Python2.7 support * Enable data from vector layers to be used for data-driven style (without using data-join technique) * Update docs for VectorMixin class, vector properties and label properties inherited from MapViz parent class * Update geojson_to_dict to geojson_to_dict_list and add docs; organize MapBox.create_html() * Fix bug with df_to_geojson and non-sequential indices (#132) * Support variable radius legend with legend_function='radius' setting for GraduatedCircleViz * Update docs * Move LegendError to method `create_html` * Bugfix for LegendError test * Refactor legend placement scripts and add updateAttribMargin and updateLegendMargin functions in main.html to automatically place secondary legends
1 parent 941525a commit e769860

7 files changed

Lines changed: 217 additions & 35 deletions

File tree

docs/viz.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ The `MapViz` class is the parent class of the various `mapboxgl-jupyter` visuali
2222

2323

2424
### Params
25-
**MapViz**(_data, vector_url=None, vector_layer_name=None, vector_join_property=None, data_join_property=None, disable_data_join=False, access_token=None, center=(0, 0), below_layer='', opacity=1, div_id='map', height='500px', style='mapbox://styles/mapbox/light-v9?optimize=true', label_property=None, label_size=8, label_color='#131516', label_halo_color='white', label_halo_width=1, width='100%', zoom=0, min_zoom=0, max_zoom=24, pitch=0, bearing=0, box_zoom_on=True, double_click_zoom_on=True, scroll_zoom_on=True, touch_zoom_on=True, legend=True, legend_layout='vertical', legend_gradient=False, legend_style='', legend_fill='white', legend_header_fill='white', legend_text_color='#6e6e6e', legend_text_numeric_precision=None, legend_title_halo_color='white', legend_key_shape='square', legend_key_borders_on=True, popup_open_action='hover'_)
25+
**MapViz**(_data, vector_url=None, vector_layer_name=None, vector_join_property=None, data_join_property=None, disable_data_join=False, access_token=None, center=(0, 0), below_layer='', opacity=1, div_id='map', height='500px', style='mapbox://styles/mapbox/light-v9?optimize=true', label_property=None, label_size=8, label_color='#131516', label_halo_color='white', label_halo_width=1, width='100%', zoom=0, min_zoom=0, max_zoom=24, pitch=0, bearing=0, box_zoom_on=True, double_click_zoom_on=True, scroll_zoom_on=True, touch_zoom_on=True, legend=True, legend_layout='vertical', legend_function='color', legend_gradient=False, legend_style='', legend_fill='white', legend_header_fill='white', legend_text_color='#6e6e6e', legend_text_numeric_precision=None, legend_title_halo_color='white', legend_key_shape='square', legend_key_borders_on=True, popup_open_action='hover'_)
2626

2727
Parameter | Description | Example
2828
--|--|--
@@ -57,6 +57,7 @@ label_halo_color | color of text halo outline | 'white'
5757
label_halo_width | width (in pixels) of text halo outline | 1
5858
legend | controls visibility of map legend | True
5959
legend_layout | controls orientation of map legend | 'horizontal'
60+
legend_function | controls whether legend is color or radius-based | 'color'
6061
legend_style | reserved for future custom CSS loading | ''
6162
legend_gradient | boolean to determine appearance of legend keys; takes precedent over legend_key_shape | False
6263
legend_fill | string background color for legend | 'white'

examples/notebooks/legend-controls.ipynb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,33 @@
215215
"viz2.show()"
216216
]
217217
},
218+
{
219+
"cell_type": "markdown",
220+
"metadata": {},
221+
"source": [
222+
"## Variable Radius Legend for a graduated circle viz"
223+
]
224+
},
225+
{
226+
"cell_type": "code",
227+
"execution_count": null,
228+
"metadata": {},
229+
"outputs": [],
230+
"source": [
231+
"# Modify the viz\n",
232+
"viz2.legend_layout = 'horizontal'\n",
233+
"viz2.legend_text_numeric_precision = 0\n",
234+
"\n",
235+
"# Switch to a legend based on the radius property\n",
236+
"viz2.legend_function = 'radius'\n",
237+
"\n",
238+
"# Variable radius legend uses MapViz.color_default to set legend item color\n",
239+
"viz2.color_default = '#0d3d79'\n",
240+
"\n",
241+
"# Show updated viz\n",
242+
"viz2.show()"
243+
]
244+
},
218245
{
219246
"cell_type": "markdown",
220247
"metadata": {},

mapboxgl/errors.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ class ValueError(ValueError):
77

88

99
class SourceDataError(ValueError):
10-
pass
10+
pass
11+
12+
13+
class LegendError(ValueError):
14+
pass
1115

1216

1317
class DateConversionError(ValueError):
14-
pass
18+
pass

mapboxgl/templates/graduated_circle.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,19 @@
1010
{% block legend %}
1111

1212
{% if showLegend %}
13+
1314
{% if colorStops and colorProperty and radiusProperty %}
14-
calcColorLegend({{ colorStops }}, "{{ colorProperty }} vs. {{ radiusProperty }}");
15+
16+
calcColorLegend({{ colorStops }}, "{{ colorProperty }}");
17+
18+
{% endif %}
19+
20+
{% if radiusStops and radiusProperty %}
21+
22+
calcRadiusLegend({{ radiusStops }}, "{{ radiusProperty }}", "{{ defaultColor }}");
23+
1524
{% endif %}
25+
1626
{% endif %}
1727

1828
{% endblock legend %}

mapboxgl/templates/main.html

Lines changed: 143 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
.legend.vertical .legend-item {white-space: nowrap;}
5050
.legend-value {display: inline-block; line-height: 18px; vertical-align: top;}
5151
.legend.horizontal ul.legend-content li.legend-item .legend-value,
52-
.legend.horizontal ul.legend-content li.legend-item {display: inline-block; float: left; width: 30px; margin-bottom: 0; text-align: center; height: 30px;}
52+
.legend.horizontal ul.legend-content li.legend-item {display: inline-block; float: left; width: 30px; margin-bottom: 0; text-align: center; min-height: 30px;}
5353

5454
/* legend key styles */
5555
.legend-key {display: inline-block; height: 10px;}
@@ -72,8 +72,14 @@
7272
.legend.vertical.contig li.legend-item {height: 15px;}
7373
.legend.vertical.contig {padding-bottom: 6px;}
7474

75+
/* vertical radius legend */
76+
.legend.horizontal.legend-variable-radius ul.legend-content li.legend-item .legend-value,
77+
.legend.horizontal.legend-variable-radius ul.legend-content li.legend-item {width: 30px; min-height: 20px;}
78+
7579
</style>
80+
7681
{% block extra_css %}{% endblock extra_css %}
82+
7783
</head>
7884
<body>
7985

@@ -84,19 +90,18 @@
8490
var legendHeader;
8591

8692
function calcColorLegend(myColorStops, title) {
87-
8893
// create legend
89-
var legend = document.createElement('div');
94+
var legend = document.createElement('div'),
95+
legendContainer = document.getElementsByClassName('mapboxgl-ctrl-bottom-right')[0];
96+
9097
if ('{{ legendKeyShape }}' === 'contiguous-bar') {
9198
legend.className = 'legend {{ legendLayout }} contig';
9299
}
93100
else {
94101
legend.className = 'legend {{ legendLayout }}';
95102
}
96-
97-
legend.id = 'legend';
103+
legend.id = 'legend-0';
98104
document.body.appendChild(legend);
99-
100105
// add legend header and content elements
101106
var mytitle = document.createElement('div'),
102107
legendContent = document.createElement('ul');
@@ -108,49 +113,42 @@
108113
legendHeader.appendChild(mytitle);
109114
legend.appendChild(legendHeader);
110115
legend.appendChild(legendContent);
111-
112116
if ({{ legendGradient|safe }} === true) {
113-
var gradientText = 'linear-gradient(to right, ';
114-
var gradient = document.createElement('div');
117+
var gradientText = 'linear-gradient(to right, ',
118+
gradient = document.createElement('div');
115119
gradient.className = 'gradient-bar';
116120
legend.appendChild(gradient);
117121
}
118-
119122
// calculate a legend entries on a Mapbox GL Style Spec property function stops array
120123
for (p = 0; p < myColorStops.length; p++) {
121-
if (!!document.getElementById('legend-points-value-' + p)) {
122-
//update the legend if it already exists
123-
document.getElementById('legend-points-value-' + p).textContent = myColorStops[p][0];
124-
document.getElementById('legend-points-id-' + p).style.backgroundColor = myColorStops[p][1];
124+
if (!!document.getElementById('legend-color-points-value-' + p)) {
125+
// update the legend if it already exists
126+
document.getElementById('legend-color-points-value-' + p).textContent = myColorStops[p][0];
127+
document.getElementById('legend-color-points-id-' + p).style.backgroundColor = myColorStops[p][1];
125128
}
126129
else {
127130
// create the legend if it doesn't yet exist
128131
var item = document.createElement('li');
129132
item.className = 'legend-item';
130-
131133
var key = document.createElement('span');
132134
key.className = 'legend-key {{ legendKeyShape }}';
133-
key.id = 'legend-points-id-' + p;
134-
key.style.backgroundColor = myColorStops[p][1];
135-
135+
key.id = 'legend-color-points-id-' + p;
136+
key.style.backgroundColor = myColorStops[p][1];
136137
var value = document.createElement('span');
137138
value.className = 'legend-value';
138-
value.id = 'legend-points-value-' + p;
139-
139+
value.id = 'legend-color-points-value-' + p;
140140
item.appendChild(key);
141141
item.appendChild(value);
142142
legendContent.appendChild(item);
143143

144-
data = document.getElementById('legend-points-value-' + p)
145-
144+
data = document.getElementById('legend-color-points-value-' + p)
146145
// round number values in legend if precision defined
147146
if ((typeof(myColorStops[p][0]) == 'number') && (typeof({{ legendNumericPrecision }}) == 'number')) {
148147
data.textContent = myColorStops[p][0].toFixed({{ legendNumericPrecision }});
149148
}
150149
else {
151150
data.textContent = myColorStops[p][0];
152151
}
153-
154152
// add color stop to gradient list
155153
if ({{ legendGradient|safe }} === true) {
156154
if (p < myColorStops.length - 1) {
@@ -165,22 +163,18 @@
165163
}
166164
}
167165
}
168-
169166
if ({{ legendGradient|safe }} === true) {
170167
// convert to gradient scale appearance
171168
gradient.style.background = gradientText;
172-
173169
// hide legend keys generated above
174170
var keys = document.getElementsByClassName('legend-key');
175171
for (var i=0; i < keys.length; i++) {
176172
keys[i].style.visibility = 'hidden';
177173
}
178-
179174
if ('{{ legendLayout }}' === 'vertical') {
180175
gradient.style.height = (legendContent.offsetHeight - 6) + 'px';
181176
}
182177
}
183-
184178
// add class for styling bordered legend keys
185179
if ({{ legendKeyBordersOn|safe }}) {
186180
var keys = document.getElementsByClassName('legend-key');
@@ -196,11 +190,132 @@
196190
}
197191
}
198192
}
193+
// update right-margin for compact Mapbox attribution based on calculated legend width
194+
updateAttribMargin(legend);
195+
updateLegendMargin(legend);
196+
}
197+
198+
199+
function calcRadiusLegend(myRadiusStops, title, color) {
200+
201+
// maximum legend item height
202+
var maxLegendItemHeight = 2 * myRadiusStops[myRadiusStops.length - 1][1];
203+
204+
// create legend
205+
var legend = document.createElement('div');
206+
legend.className = 'legend {{ legendLayout }} legend-variable-radius';
207+
208+
legend.id = 'legend-1';
209+
document.body.appendChild(legend);
210+
211+
// add legend header and content elements
212+
var mytitle = document.createElement('div'),
213+
legendContent = document.createElement('ul');
214+
legendHeader = document.createElement('div');
215+
mytitle.textContent = title;
216+
mytitle.className = 'legend-title'
217+
legendHeader.className = 'legend-header'
218+
legendContent.className = 'legend-content'
219+
legendHeader.appendChild(mytitle);
220+
legend.appendChild(legendHeader);
221+
legend.appendChild(legendContent);
222+
223+
// calculate a legend entries on a Mapbox GL Style Spec property function stops array
224+
for (p = 0; p < myRadiusStops.length; p++) {
225+
if (!!document.getElementById('legend-radius-points-value-' + p)) {
226+
//update the legend if it already exists
227+
document.getElementById('legend-radius-points-value-' + p).textContent = myRadiusStops[p][0];
228+
document.getElementById('legend-radius-points-id-' + p).style.backgroundColor = color;
229+
}
230+
else {
231+
// create the legend if it doesn't yet exist
232+
var item = document.createElement('li');
233+
item.className = 'legend-item';
234+
item.height = '' + maxLegendItemHeight + 'px';
235+
236+
var key = document.createElement('span');
237+
key.className = 'legend-key {{ legendKeyShape }}';
238+
key.id = 'legend-radius-points-id-' + p;
239+
key.style.backgroundColor = color;
240+
241+
key.style.width = '' + myRadiusStops[p][1] * 2 + 'px';
242+
key.style.height = '' + myRadiusStops[p][1] * 2 + 'px';
243+
244+
keyVerticalMargin = (maxLegendItemHeight - myRadiusStops[p][1] * 2) * 0.5;
245+
key.style.marginTop = '' + keyVerticalMargin + 'px';
246+
key.style.marginBottom = '' + keyVerticalMargin + 'px';
247+
248+
var value = document.createElement('span');
249+
value.className = 'legend-value';
250+
value.id = 'legend-radius-points-value-' + p;
251+
252+
item.appendChild(key);
253+
item.appendChild(value);
254+
legendContent.appendChild(item);
255+
256+
data = document.getElementById('legend-radius-points-value-' + p)
257+
258+
// round number values in legend if precision defined
259+
if ((typeof(myRadiusStops[p][0]) == 'number') && (typeof({{ legendNumericPrecision }}) == 'number')) {
260+
data.textContent = myRadiusStops[p][0].toFixed({{ legendNumericPrecision }});
261+
}
262+
else {
263+
data.textContent = myRadiusStops[p][0];
264+
}
265+
}
266+
}
267+
268+
// add class for styling bordered legend keys
269+
if ({{ legendKeyBordersOn|safe }}) {
270+
var keys = document.getElementsByClassName('legend-key');
271+
for (var i=0; i < keys.length; i++) {
272+
if (keys[i]) {
273+
keys[i].classList.add('bordered');
274+
}
275+
}
276+
}
199277

200278
// update right-margin for compact Mapbox attribution based on calculated legend width
279+
updateAttribMargin(legend);
280+
updateLegendMargin(legend);
281+
282+
}
283+
284+
285+
function updateAttribMargin(legend) {
286+
287+
// default margin is based on calculated legend width
201288
var attribMargin = legend.offsetWidth + 15;
202-
document.getElementsByClassName('mapboxgl-ctrl-attrib')[0].style.marginRight = attribMargin.toString() + 'px';
289+
290+
// if horizontal legend layout (multiple legends are stacked vertically)
291+
if ('{{ legendLayout }}' === 'horizontal') {
292+
document.getElementsByClassName('mapboxgl-ctrl-attrib')[0].style.marginRight = (attribMargin).toString() + 'px';
293+
}
294+
// vertical legend layout means multiple legends are side-by-side
295+
else if ('{{ legendLayout }}' === 'vertical') {
296+
var currentMargin = Number(document.getElementsByClassName('mapboxgl-ctrl-attrib')[0].style.marginRight.replace('px', ''));
297+
document.getElementsByClassName('mapboxgl-ctrl-attrib')[0].style.marginRight = (attribMargin + currentMargin).toString() + 'px';
298+
}
299+
}
300+
203301

302+
function updateLegendMargin(legend) {
303+
304+
var verticalLegends = document.getElementsByClassName('legend vertical'),
305+
horizontalLegends = document.getElementsByClassName('legend horizontal');
306+
307+
if (verticalLegends.length > 1) {
308+
for (i = 1; i < verticalLegends.length; i++) {
309+
verticalLegends[i].style.marginRight = (legend.offsetWidth - 5).toString() + 'px';
310+
var legend = verticalLegends[i];
311+
}
312+
}
313+
else if (horizontalLegends.length > 1) {
314+
for (i = 1; i < horizontalLegends.length; i++) {
315+
horizontalLegends[i].style.marginBottom = (legend.offsetHeight + 15).toString() + 'px';
316+
var legend = horizontalLegends[i];
317+
}
318+
}
204319
}
205320

206321

0 commit comments

Comments
 (0)