Skip to content

Commit

Permalink
Variable radius legend (#135)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
akacarlyann authored and ryanbaumann committed Mar 19, 2019
1 parent 941525a commit e769860
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 35 deletions.
3 changes: 2 additions & 1 deletion docs/viz.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ The `MapViz` class is the parent class of the various `mapboxgl-jupyter` visuali


### Params
**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'_)
**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'_)

Parameter | Description | Example
--|--|--
Expand Down Expand Up @@ -57,6 +57,7 @@ label_halo_color | color of text halo outline | 'white'
label_halo_width | width (in pixels) of text halo outline | 1
legend | controls visibility of map legend | True
legend_layout | controls orientation of map legend | 'horizontal'
legend_function | controls whether legend is color or radius-based | 'color'
legend_style | reserved for future custom CSS loading | ''
legend_gradient | boolean to determine appearance of legend keys; takes precedent over legend_key_shape | False
legend_fill | string background color for legend | 'white'
Expand Down
27 changes: 27 additions & 0 deletions examples/notebooks/legend-controls.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,33 @@
"viz2.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Variable Radius Legend for a graduated circle viz"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Modify the viz\n",
"viz2.legend_layout = 'horizontal'\n",
"viz2.legend_text_numeric_precision = 0\n",
"\n",
"# Switch to a legend based on the radius property\n",
"viz2.legend_function = 'radius'\n",
"\n",
"# Variable radius legend uses MapViz.color_default to set legend item color\n",
"viz2.color_default = '#0d3d79'\n",
"\n",
"# Show updated viz\n",
"viz2.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down
8 changes: 6 additions & 2 deletions mapboxgl/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ class ValueError(ValueError):


class SourceDataError(ValueError):
pass
pass


class LegendError(ValueError):
pass


class DateConversionError(ValueError):
pass
pass
12 changes: 11 additions & 1 deletion mapboxgl/templates/graduated_circle.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,19 @@
{% block legend %}

{% if showLegend %}

{% if colorStops and colorProperty and radiusProperty %}
calcColorLegend({{ colorStops }}, "{{ colorProperty }} vs. {{ radiusProperty }}");

calcColorLegend({{ colorStops }}, "{{ colorProperty }}");

{% endif %}

{% if radiusStops and radiusProperty %}

calcRadiusLegend({{ radiusStops }}, "{{ radiusProperty }}", "{{ defaultColor }}");

{% endif %}

{% endif %}

{% endblock legend %}
Expand Down
171 changes: 143 additions & 28 deletions mapboxgl/templates/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
.legend.vertical .legend-item {white-space: nowrap;}
.legend-value {display: inline-block; line-height: 18px; vertical-align: top;}
.legend.horizontal ul.legend-content li.legend-item .legend-value,
.legend.horizontal ul.legend-content li.legend-item {display: inline-block; float: left; width: 30px; margin-bottom: 0; text-align: center; height: 30px;}
.legend.horizontal ul.legend-content li.legend-item {display: inline-block; float: left; width: 30px; margin-bottom: 0; text-align: center; min-height: 30px;}

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

/* vertical radius legend */
.legend.horizontal.legend-variable-radius ul.legend-content li.legend-item .legend-value,
.legend.horizontal.legend-variable-radius ul.legend-content li.legend-item {width: 30px; min-height: 20px;}

</style>

{% block extra_css %}{% endblock extra_css %}

</head>
<body>

Expand All @@ -84,19 +90,18 @@
var legendHeader;

function calcColorLegend(myColorStops, title) {

// create legend
var legend = document.createElement('div');
var legend = document.createElement('div'),
legendContainer = document.getElementsByClassName('mapboxgl-ctrl-bottom-right')[0];

if ('{{ legendKeyShape }}' === 'contiguous-bar') {
legend.className = 'legend {{ legendLayout }} contig';
}
else {
legend.className = 'legend {{ legendLayout }}';
}

legend.id = 'legend';
legend.id = 'legend-0';
document.body.appendChild(legend);

// add legend header and content elements
var mytitle = document.createElement('div'),
legendContent = document.createElement('ul');
Expand All @@ -108,49 +113,42 @@
legendHeader.appendChild(mytitle);
legend.appendChild(legendHeader);
legend.appendChild(legendContent);

if ({{ legendGradient|safe }} === true) {
var gradientText = 'linear-gradient(to right, ';
var gradient = document.createElement('div');
var gradientText = 'linear-gradient(to right, ',
gradient = document.createElement('div');
gradient.className = 'gradient-bar';
legend.appendChild(gradient);
}

// calculate a legend entries on a Mapbox GL Style Spec property function stops array
for (p = 0; p < myColorStops.length; p++) {
if (!!document.getElementById('legend-points-value-' + p)) {
//update the legend if it already exists
document.getElementById('legend-points-value-' + p).textContent = myColorStops[p][0];
document.getElementById('legend-points-id-' + p).style.backgroundColor = myColorStops[p][1];
if (!!document.getElementById('legend-color-points-value-' + p)) {
// update the legend if it already exists
document.getElementById('legend-color-points-value-' + p).textContent = myColorStops[p][0];
document.getElementById('legend-color-points-id-' + p).style.backgroundColor = myColorStops[p][1];
}
else {
// create the legend if it doesn't yet exist
var item = document.createElement('li');
item.className = 'legend-item';

var key = document.createElement('span');
key.className = 'legend-key {{ legendKeyShape }}';
key.id = 'legend-points-id-' + p;
key.style.backgroundColor = myColorStops[p][1];

key.id = 'legend-color-points-id-' + p;
key.style.backgroundColor = myColorStops[p][1];
var value = document.createElement('span');
value.className = 'legend-value';
value.id = 'legend-points-value-' + p;

value.id = 'legend-color-points-value-' + p;
item.appendChild(key);
item.appendChild(value);
legendContent.appendChild(item);

data = document.getElementById('legend-points-value-' + p)

data = document.getElementById('legend-color-points-value-' + p)
// round number values in legend if precision defined
if ((typeof(myColorStops[p][0]) == 'number') && (typeof({{ legendNumericPrecision }}) == 'number')) {
data.textContent = myColorStops[p][0].toFixed({{ legendNumericPrecision }});
}
else {
data.textContent = myColorStops[p][0];
}

// add color stop to gradient list
if ({{ legendGradient|safe }} === true) {
if (p < myColorStops.length - 1) {
Expand All @@ -165,22 +163,18 @@
}
}
}

if ({{ legendGradient|safe }} === true) {
// convert to gradient scale appearance
gradient.style.background = gradientText;

// hide legend keys generated above
var keys = document.getElementsByClassName('legend-key');
for (var i=0; i < keys.length; i++) {
keys[i].style.visibility = 'hidden';
}

if ('{{ legendLayout }}' === 'vertical') {
gradient.style.height = (legendContent.offsetHeight - 6) + 'px';
}
}

// add class for styling bordered legend keys
if ({{ legendKeyBordersOn|safe }}) {
var keys = document.getElementsByClassName('legend-key');
Expand All @@ -196,11 +190,132 @@
}
}
}
// update right-margin for compact Mapbox attribution based on calculated legend width
updateAttribMargin(legend);
updateLegendMargin(legend);
}


function calcRadiusLegend(myRadiusStops, title, color) {

// maximum legend item height
var maxLegendItemHeight = 2 * myRadiusStops[myRadiusStops.length - 1][1];

// create legend
var legend = document.createElement('div');
legend.className = 'legend {{ legendLayout }} legend-variable-radius';

legend.id = 'legend-1';
document.body.appendChild(legend);

// add legend header and content elements
var mytitle = document.createElement('div'),
legendContent = document.createElement('ul');
legendHeader = document.createElement('div');
mytitle.textContent = title;
mytitle.className = 'legend-title'
legendHeader.className = 'legend-header'
legendContent.className = 'legend-content'
legendHeader.appendChild(mytitle);
legend.appendChild(legendHeader);
legend.appendChild(legendContent);

// calculate a legend entries on a Mapbox GL Style Spec property function stops array
for (p = 0; p < myRadiusStops.length; p++) {
if (!!document.getElementById('legend-radius-points-value-' + p)) {
//update the legend if it already exists
document.getElementById('legend-radius-points-value-' + p).textContent = myRadiusStops[p][0];
document.getElementById('legend-radius-points-id-' + p).style.backgroundColor = color;
}
else {
// create the legend if it doesn't yet exist
var item = document.createElement('li');
item.className = 'legend-item';
item.height = '' + maxLegendItemHeight + 'px';

var key = document.createElement('span');
key.className = 'legend-key {{ legendKeyShape }}';
key.id = 'legend-radius-points-id-' + p;
key.style.backgroundColor = color;

key.style.width = '' + myRadiusStops[p][1] * 2 + 'px';
key.style.height = '' + myRadiusStops[p][1] * 2 + 'px';

keyVerticalMargin = (maxLegendItemHeight - myRadiusStops[p][1] * 2) * 0.5;
key.style.marginTop = '' + keyVerticalMargin + 'px';
key.style.marginBottom = '' + keyVerticalMargin + 'px';

var value = document.createElement('span');
value.className = 'legend-value';
value.id = 'legend-radius-points-value-' + p;

item.appendChild(key);
item.appendChild(value);
legendContent.appendChild(item);

data = document.getElementById('legend-radius-points-value-' + p)

// round number values in legend if precision defined
if ((typeof(myRadiusStops[p][0]) == 'number') && (typeof({{ legendNumericPrecision }}) == 'number')) {
data.textContent = myRadiusStops[p][0].toFixed({{ legendNumericPrecision }});
}
else {
data.textContent = myRadiusStops[p][0];
}
}
}

// add class for styling bordered legend keys
if ({{ legendKeyBordersOn|safe }}) {
var keys = document.getElementsByClassName('legend-key');
for (var i=0; i < keys.length; i++) {
if (keys[i]) {
keys[i].classList.add('bordered');
}
}
}

// update right-margin for compact Mapbox attribution based on calculated legend width
updateAttribMargin(legend);
updateLegendMargin(legend);

}


function updateAttribMargin(legend) {

// default margin is based on calculated legend width
var attribMargin = legend.offsetWidth + 15;
document.getElementsByClassName('mapboxgl-ctrl-attrib')[0].style.marginRight = attribMargin.toString() + 'px';

// if horizontal legend layout (multiple legends are stacked vertically)
if ('{{ legendLayout }}' === 'horizontal') {
document.getElementsByClassName('mapboxgl-ctrl-attrib')[0].style.marginRight = (attribMargin).toString() + 'px';
}
// vertical legend layout means multiple legends are side-by-side
else if ('{{ legendLayout }}' === 'vertical') {
var currentMargin = Number(document.getElementsByClassName('mapboxgl-ctrl-attrib')[0].style.marginRight.replace('px', ''));
document.getElementsByClassName('mapboxgl-ctrl-attrib')[0].style.marginRight = (attribMargin + currentMargin).toString() + 'px';
}
}


function updateLegendMargin(legend) {

var verticalLegends = document.getElementsByClassName('legend vertical'),
horizontalLegends = document.getElementsByClassName('legend horizontal');

if (verticalLegends.length > 1) {
for (i = 1; i < verticalLegends.length; i++) {
verticalLegends[i].style.marginRight = (legend.offsetWidth - 5).toString() + 'px';
var legend = verticalLegends[i];
}
}
else if (horizontalLegends.length > 1) {
for (i = 1; i < horizontalLegends.length; i++) {
horizontalLegends[i].style.marginBottom = (legend.offsetHeight + 15).toString() + 'px';
var legend = horizontalLegends[i];
}
}
}


Expand Down
Loading

0 comments on commit e769860

Please sign in to comment.