Skip to content

Commit

Permalink
OWMap: add legend for selected color, shape, size
Browse files Browse the repository at this point in the history
  • Loading branch information
kernc committed Nov 9, 2016
1 parent 25e0080 commit 01a0052
Show file tree
Hide file tree
Showing 5 changed files with 258 additions and 13 deletions.
103 changes: 103 additions & 0 deletions Orange/widgets/visualize/_owmap/legend-sizes-indicator.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions Orange/widgets/visualize/_owmap/orange-style.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
html, body, #map { margin: 0; padding: 0; width: 100%; height: 100%; user-select: none !important;}
.leaflet-control-attribution { pointer-events: none; }
.leaflet-popup-pane { cursor: default; }
.leaflet-popup-close-button { display: none; }

.custom-button { width:16px;vertical-align: text-bottom; }
.custom-button-toggle { background: #ccf !important;}
canvas#heatmap_canvas,
canvas#icons_canvas {display: none}

.orange-marker {
/* Make points at low zoom (<100%) haligned. */
line-height: 0;
Expand Down Expand Up @@ -31,3 +41,38 @@
-webkit-filter: drop-shadow(0px 0px 1.5px lime) drop-shadow(0px 0px 1.5px lime) !important;
filter: drop-shadow(0px 0px 1.5px lime) drop-shadow(0px 0px 1.5px lime) !important;
}

.legend {
background: rgba(240, 230, 230, .85);
padding: 1em;
border: 1px solid gray;
cursor: default;
}
.legend h3 {
margin-top: 1em;
margin-bottom: 0;
font-size: 1.1em;
}
.legend .legend-box:first-child h3 {
margin: 0;
}
.legend hr { margin: .1em; }

.legend-box {
max-height: 30%;
}
.legend-box table.colors,
.legend-box table.sizes {
height: 10em;
}
.legend-box table.colors img {
height: 100%;
}
.legend-box table tr:first-child td:last-child {vertical-align: top}
.legend-box table tr:last-child td:last-child {vertical-align: bottom}
.legend-box table.sizes tr:last-child td:last-child {position:relative; bottom: 3px}

.legend-icon {
width: 2em;
display: inline-block;
}
12 changes: 1 addition & 11 deletions Orange/widgets/visualize/_owmap/owmap.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,12 @@
<link rel="stylesheet" href="MarkerCluster.css" />
<link rel="stylesheet" href="MarkerCluster.Default.css" />
<link rel="stylesheet" href="easy-button.css" />

<script src="jquery.js"></script>
<script src="leaflet.js"></script>
<script src="leaflet-providers.js"></script>
<script src="leaflet.markercluster.js"></script>
<script src="easy-button.js"></script>

<style>
html, body, #map { margin: 0; padding: 0; width: 100%; height: 100%; user-select: none;}
.leaflet-control-attribution { pointer-events: none; }
.leaflet-popup-pane { cursor: default; }
.leaflet-popup-close-button { display: none; }
.custom-button { width:16px;vertical-align: text-bottom; }
.custom-button-toggle { background: #ccf !important;}
canvas#heatmap_canvas,
canvas#icons_canvas {display: none}
</style>
</head>
<body>
<div id="map"></div>
Expand Down
63 changes: 63 additions & 0 deletions Orange/widgets/visualize/_owmap/owmap.js
Original file line number Diff line number Diff line change
Expand Up @@ -552,3 +552,66 @@ $(document).ready(function() {
setTimeout(function() { map.on('moveend', reset_heatmap); }, 100);
setTimeout(function() { map.on('moveend', redraw_markers_overlay_image); }, 100);
});


var legendControl = L.control({position: 'topright'}),
legend_colors = [],
legend_shapes = [],
legend_sizes = [];
legendControl.onAdd = function () {
if (legend_colors.length +
legend_shapes.length +
legend_sizes.length == 0)
return L.DomUtil.create('span');

var div = L.DomUtil.create('div', 'legend');

if (legend_colors.length) {
var box = L.DomUtil.create('div', 'legend-box', div);
box.innerHTML += '<h3>Color</h3><hr/>';
if (legend_colors[0] == 'c') {
box.innerHTML += L.Util.template(
'<table class="colors">' + // I'm sorry
'<tr><td rowspan="2" style="width:2em; background:linear-gradient({colors})"></td><td> {minval}</td></tr>' +
'<tr><td> {maxval}</td></tr>' +
'</table>', {
minval: legend_colors[1][0],
maxval: legend_colors[1][1],
colors: legend_colors[2].join(',')
});
} else {
var str = '';
for (var i=0; i<legend_colors[1].length; ++i)
str += L.Util.template(
'<div><div class="legend-icon" style="background:{color}">&nbsp;</div> {value}</div>', {
color: legend_colors[2][i],
value: legend_colors[1][i]});
box.innerHTML += str;
}
}

if (legend_shapes.length) {
var box = L.DomUtil.create('div', 'legend-box', div);
var str = '';
for (var i=0; i<legend_shapes.length; ++i)
str += L.Util.template(
'<div><img class="legend-icon" style="vertical-align:middle" src="{shape}"/> {value}</div>', {
shape: _construct_icon(i, '#555'),
value: legend_shapes[i]});
box.innerHTML = '<h3>Shape</h3><hr/>' + str;
}

if (legend_sizes.length) {
var box = L.DomUtil.create('div', 'legend-box', div);
box.innerHTML += '<h3>Size</h3><hr/>' + L.Util.template(
'<table class="sizes">' + // I'm sorry
'<tr><td rowspan="2"><img src="legend-sizes-indicator.svg"></td><td> {minval}</td></tr>' +
'<tr><td> {maxval}</td></tr>' +
'</table>', {
minval: legend_sizes[0],
maxval: legend_sizes[1],
});
}

return div;
};
48 changes: 46 additions & 2 deletions Orange/widgets/visualize/owmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from Orange.util import color_to_hex
from Orange.base import Learner
from Orange.data.util import scale
from Orange.data import Table, Domain
from Orange.data import Table, Domain, TimeVariable
from Orange.widgets import gui, widget, settings
from Orange.widgets.utils.itemmodels import VariableListModel
from Orange.widgets.utils.webview import WebviewWidget
Expand Down Expand Up @@ -47,6 +47,9 @@ def __init__(self, parent=None):
self._label_attr = None
self._shape_attr = None
self._size_attr = None
self._legend_colors = []
self._legend_shapes = []
self._legend_sizes = []

self._drawing_args = None
self._image_token = None
Expand Down Expand Up @@ -117,22 +120,44 @@ def set_jittering(self, jittering):
'''.format(jittering))
self.redraw_markers_overlay_image()

def _legend_values(self, variable, values):
strs = [variable.repr_val(val) for val in values]
if any(len(val) > 10 for val in strs):
if isinstance(variable, TimeVariable):
strs = [s.replace(' ', '<br>') for s in strs]
elif variable.is_continuous:
strs = ['{:.4e}'.format(val) for val in values]
elif variable.is_discrete:
strs = [s if len(s) <= 12 else (s[:6] + '…' + s[-5:])
for s in strs]
return strs

def set_marker_color(self, attr, update=True):
try:
self._color_attr = variable = self.data.domain[attr]
except Exception:
self._color_attr = None
self._legend_colors = []
else:
if variable.is_continuous:
self._raw_color_values = values = self.data.get_column_view(variable)[0]
self._scaled_color_values = scale(values)
self._colorgen = ContinuousPaletteGenerator(*variable.colors)
min = np.nanmin(values)
self._legend_colors = (['c',
self._legend_values(variable, [min, np.nanmax(values)]),
[color_to_hex(i) for i in variable.colors if i]]
if not np.isnan(min) else [])
elif variable.is_discrete:
_values = np.asarray(self.data.domain[attr].values)
__values = self.data.get_column_view(variable)[0].astype(np.uint16)
self._raw_color_values = _values[__values] # The joke's on you
self._scaled_color_values = __values
self._colorgen = ColorPaletteGenerator(len(variable.colors), variable.colors)
self._legend_colors = ['d',
self._legend_values(variable, range(len(_values))),
[color_to_hex(self._colorgen.getRGB(i))
for i in range(len(_values))]]
finally:
if update:
self.redraw_markers_overlay_image()
Expand All @@ -158,11 +183,13 @@ def set_marker_shape(self, attr, update=True):
self._shape_attr = variable = self.data.domain[attr]
except Exception:
self._shape_attr = None
self._legend_shapes = []
else:
assert variable.is_discrete
_values = np.asarray(self.data.domain[attr].values)
self._shape_values = __values = self.data.get_column_view(variable)[0].astype(np.uint16)
self._raw_shape_values = _values[__values]
self._legend_shapes = self._legend_values(variable, range(len(_values)))
finally:
if update:
self.redraw_markers_overlay_image()
Expand All @@ -172,10 +199,15 @@ def set_marker_size(self, attr, update=True):
self._size_attr = variable = self.data.domain[attr]
except Exception:
self._size_attr = None
self._legend_sizes = []
else:
assert variable.is_continuous
self._raw_sizes = values = self.data.get_column_view(variable)[0]
# Note, [5, 60] is also hardcoded in legend-size-indicator.svg
self._sizes = scale(values, 5, 60).astype(np.uint8)
min = np.nanmin(values)
self._legend_sizes = self._legend_values(variable,
[min, np.nanmax(values)]) if not np.isnan(min) else []
finally:
if update:
self.redraw_markers_overlay_image()
Expand Down Expand Up @@ -294,7 +326,19 @@ def redraw_markers_overlay_image(self, *args):
visible = ((lat <= north) & (lat >= south) &
(lon <= east) & (lon >= west)).nonzero()[0]

if len(visible) <= 500:
is_js_path = len(visible) <= 500

self.evalJS('''
window.legend_colors = %s;
window.legend_shapes = %s;
window.legend_sizes = %s;
legendControl.remove();
legendControl.addTo(map);
''' % (self._legend_colors,
self._legend_shapes if is_js_path else [],
self._legend_sizes))

if is_js_path:
self.evalJS('clear_markers_overlay_image()')
self._update_js_markers(visible)
self._owwidget.disable_some_controls(False)
Expand Down

0 comments on commit 01a0052

Please sign in to comment.