Add markers cluster support & cleanup/refactor code.

This commit is contained in:
Arik Fraimovich 2016-09-22 23:08:32 +03:00
parent e5146c3755
commit 26bd08bb2b
5 changed files with 183 additions and 188 deletions

View File

@ -36,7 +36,8 @@
"material-design-iconic-font": "^2.2.0",
"plotly.js": "~1.16.0",
"angular-ui-ace": "bower",
"angular-vs-repeat": "^1.1.7"
"angular-vs-repeat": "^1.1.7",
"leaflet.markercluster": "^0.5.0"
},
"devDependencies": {
"angular-mocks": "1.2.18",

View File

@ -20,6 +20,8 @@
<link rel="stylesheet" href="/bower_components/font-awesome/css/font-awesome.css">
<link rel="stylesheet" href="/bower_components/leaflet/dist/leaflet.css">
<link rel="stylesheet" href="/bower_components/angular-resizable/src/angular-resizable.css">
<link rel="stylesheet" href="/bower_components/leaflet.markercluster/dist/MarkerCluster.css">
<link rel="stylesheet" href="/bower_components/leaflet.markercluster/dist/MarkerCluster.Default.css">
<link rel="stylesheet" href="/styles/redash.css">
<!-- endbuild -->

View File

@ -11,9 +11,9 @@
var editTemplate = '<map-editor></map-editor>';
var defaultOptions = {
'height': 500,
'draw': 'Marker',
'classify':'none'
height: 500,
classify: 'none',
clusterMarkers: true
};
VisualizationProvider.registerVisualization({
@ -31,16 +31,35 @@
restrict: 'E',
templateUrl: '/views/visualizations/map.html',
link: function($scope, elm, attrs) {
$scope.$watch('[queryResult && queryResult.getData(), visualization.options.draw,visualization.options.latColName,'+
'visualization.options.lonColName,visualization.options.classify,visualization.options.classify]',
render, true);
$scope.$watch('queryResult && queryResult.getData()', render, true);
$scope.$watch('visualization.options', render, true);
angular.element(window).on("resize", resize);
$scope.$watch('visualization.options.height', resize);
var color = d3.scale.category10();
var map = L.map(elm[0].children[0].children[0], {scrollWheelZoom: false});
var mapControls = L.control.layers().addTo(map);
var layers = {};
map.on('focus',function(){
map.on('moveend', getBounds);
});
map.on('blur',function(){
map.off('moveend', getBounds);
});
// Following line is used to avoid "Couldn't autodetect L.Icon.Default.imagePath" error
// https://github.com/Leaflet/Leaflet/issues/766#issuecomment-7741039
L.Icon.Default.imagePath = L.Icon.Default.imagePath || "//api.tiles.mapbox.com/mapbox.js/v2.2.1/images";
L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
function resize() {
if (!$scope.map) return;
$scope.map.invalidateSize(false);
if (!map) return;
map.invalidateSize(false);
setBounds();
}
@ -48,164 +67,132 @@
var b = $scope.visualization.options.bounds;
if(b){
$scope.map.fitBounds([[b._southWest.lat, b._southWest.lng],[b._northEast.lat, b._northEast.lng]]);
} else if ($scope.features.length > 0){
var group= new L.featureGroup($scope.features);
$scope.map.fitBounds(group.getBounds());
map.fitBounds([[b._southWest.lat, b._southWest.lng],[b._northEast.lat, b._northEast.lng]]);
} else if (layers){
var allMarkers = _.flatten(_.map(_.values(layers), function(l) { return l.getLayers() }));
var group = new L.featureGroup(allMarkers);
map.fitBounds(group.getBounds());
}
};
function render() {
var marker = function(lat,lon){
if (lat == null || lon == null) return;
var createMarker = function(lat,lon){
if (lat == null || lon == null) return;
return L.marker([lat, lon]);
return L.marker([lat, lon]);
};
var heatpoint = function(lat, lon, color){
if (lat == null || lon == null) return;
var style = {
fillColor:color,
fillOpacity:0.9,
stroke:false
};
var heatpoint = function(lat,lon,obj){
if (lat == null || lon == null) return;
return L.circleMarker([lat,lon],style)
};
var color = 'red';
function getBounds() {
$scope.visualization.options.bounds = map.getBounds();
}
if (obj &&
obj[$scope.visualization.options.classify] &&
$scope.visualization.options.classification){
var v = $.grep($scope.visualization.options.classification,function(e){
return e.value == obj[$scope.visualization.options.classify];
});
if (v.length >0) color = v[0].color;
function createDescription(latCol, lonCol, row) {
var lat = row[latCol];
var lon = row[lonCol];
var description = '<ul style="list-style-type: none;padding-left: 0">';
description += "<li><strong>"+lat+ ", " + lon + "</strong>";
for (var k in row){
if (!(k == latCol || k == lonCol)) {
description += "<li>" + k + ": " + row[k] + "</li>";
}
var style = {
fillColor:color,
fillOpacity:0.5,
stroke:false
};
return L.circleMarker([lat,lon],style)
};
var color = function(val){
// taken from http://jsfiddle.net/xgJ2e/2/
var h= Math.floor((100 - val) * 120 / 100);
var s = Math.abs(val - 50)/50;
var v = 1;
var rgb, i, data = [];
if (s === 0) {
rgb = [v,v,v];
} else {
h = h / 60;
i = Math.floor(h);
data = [v*(1-s), v*(1-s*(h-i)), v*(1-s*(1-(h-i)))];
switch(i) {
case 0:
rgb = [v, data[2], data[0]];
break;
case 1:
rgb = [data[1], v, data[0]];
break;
case 2:
rgb = [data[0], v, data[2]];
break;
case 3:
rgb = [data[0], data[1], v];
break;
case 4:
rgb = [data[2], data[0], v];
break;
default:
rgb = [v, data[0], data[1]];
break;
}
}
return '#' + rgb.map(function(x){
return ("0" + Math.round(x*255).toString(16)).slice(-2);
}).join('');
};
// Following line is used to avoid "Couldn't autodetect L.Icon.Default.imagePath" error
// https://github.com/Leaflet/Leaflet/issues/766#issuecomment-7741039
L.Icon.Default.imagePath = L.Icon.Default.imagePath || "//api.tiles.mapbox.com/mapbox.js/v2.2.1/images";
function getBounds(e) {
$scope.visualization.options.bounds = $scope.map.getBounds();
}
return description;
}
function removeLayer(layer) {
if (layer) {
mapControls.removeLayer(layer);
map.removeLayer(layer);
}
}
function addLayer(name, points) {
var latCol = $scope.visualization.options.latColName || 'lat';
var lonCol = $scope.visualization.options.lonColName || 'lon';
var classify = $scope.visualization.options.classify;
var markers;
if ($scope.visualization.options.clusterMarkers) {
markers = L.markerClusterGroup();
} else {
markers = L.layerGroup();
}
// create markers
_.each(points, function(row) {
var marker;
var lat = row[latCol];
var lon = row[lonCol];
if (classify && classify != 'none') {
var color = $scope.visualization.options.groups[name].color;
marker = heatpoint(lat, lon, color);
} else {
marker = createMarker(lat, lon);
}
if (!marker) return;
marker.bindPopup(createDescription(latCol, lonCol, row));
markers.addLayer(marker);
});
markers.addTo(map);
layers[name] = markers;
mapControls.addOverlay(markers, name);
}
function render() {
var queryData = $scope.queryResult.getData();
var classify = $scope.visualization.options.classify;
if ($scope.visualization.options.clusterMarkers === undefined) {
$scope.visualization.options.clusterMarkers = true;
}
if (queryData) {
$scope.visualization.options.classification = [];
for (var row in queryData) {
if (queryData[row][classify] &&
$.grep($scope.visualization.options.classification, function (e) {
return e.value == queryData[row][classify]
}).length == 0) {
$scope.visualization.options.classification.push({value: queryData[row][classify], color: null});
}
var pointGroups;
if (classify && classify != 'none') {
pointGroups = _.groupBy(queryData, classify);
} else {
pointGroups = {'All': queryData};
}
$.each($scope.visualization.options.classification, function (i, c) {
c.color = color(parseInt((i / $scope.visualization.options.classification.length) * 100));
var groupNames = _.keys(pointGroups);
var options = _.map(groupNames, function(group) {
if ($scope.visualization.options.groups && $scope.visualization.options.groups[group]) {
return $scope.visualization.options.groups[group];
}
return {color: color(group)};
});
if (!$scope.map) {
$scope.map = L.map(elm[0].children[0].children[0])
}
$scope.visualization.options.groups = _.object(groupNames, options);
L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
}).addTo($scope.map);
$scope.features = $scope.features || [];
var tmp_features = [];
var lat_col = $scope.visualization.options.latColName || 'lat';
var lon_col = $scope.visualization.options.lonColName || 'lon';
for (var row in queryData) {
var feature;
if ($scope.visualization.options.draw == 'Marker') {
feature = marker(queryData[row][lat_col], queryData[row][lon_col])
} else if ($scope.visualization.options.draw == 'Color') {
feature = heatpoint(queryData[row][lat_col], queryData[row][lon_col], queryData[row])
}
if (!feature) continue;
var obj_description = '<ul style="list-style-type: none;padding-left: 0">';
for (var k in queryData[row]){
obj_description += "<li>" + k + ": " + queryData[row][k] + "</li>";
}
obj_description += '</ul>';
feature.bindPopup(obj_description);
tmp_features.push(feature);
}
$.each($scope.features, function (i, f) {
$scope.map.removeLayer(f);
_.each(layers, function(v, k) {
removeLayer(v);
});
$scope.features = tmp_features;
$.each($scope.features, function (i, f) {
f.addTo($scope.map)
_.each(pointGroups, function(v, k) {
addLayer(k, v);
});
setBounds();
$scope.map.on('focus',function(){
$scope.map.on('moveend', getBounds);
});
$scope.map.on('blur',function(){
$scope.map.off('moveend', getBounds);
});
}
}
@ -218,7 +205,7 @@
restrict: 'E',
templateUrl: '/views/visualizations/map_editor.html',
link: function($scope, elm, attrs) {
$scope.draw_options = ['Marker','Color'];
$scope.currentTab = 'general';
$scope.classify_columns = $scope.queryResult.columnNames.concat('none');
}
}

View File

@ -42,4 +42,5 @@
<script src="/bower_components/angular-ui-sortable/sortable.js"></script>
<script src="/bower_components/angular-ui-ace/ui-ace.js"></script>
<script src="/bower_components/angular-vs-repeat/src/angular-vs-repeat.js"></script>
<script src="/bower_components/leaflet.markercluster/dist/leaflet.markercluster-src.js"></script>
<!-- endbuild -->

View File

@ -1,55 +1,59 @@
<div class="form-horizontal">
<div>
<ul class="tab-nav">
<li ng-class="{active: currentTab == 'general'}"><a ng-click="currentTab='general'">General</a></li>
<li ng-class="{active: currentTab == 'groups'}"><a ng-click="currentTab='groups'">Groups</a></li>
<li ng-class="{active: currentTab == 'map'}"><a ng-click="currentTab='map'">Map Settings</a></li>
</ul>
<div ng-show="currentTab == 'general'">
<div class="form-group">
<label class="col-lg-2">Map height (px)</label>
<div class="col-sm-4">
<input class="form-control" type="number" ng-model = "visualization.options.height" />
</div>
<label class="control-label">Latitude Column Name</label>
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.latColName"
class="form-control"></select>
</div>
<div class="form-group">
<label class="col-lg-2">Draw option</label>
<div class="col-sm-4">
<select ng-options="opt for opt in draw_options" ng-model="visualization.options.draw" class="form-control"></select>
</div>
<label class="control-label">Longitude Column Name</label>
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.lonColName"
class="form-control"></select>
</div>
<div class="form-group">
<label class="col-lg-2">Latitude column name</label>
<div class="col-sm-4">
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.latColName" class="form-control"></select>
</div>
<label class="control-label">Group By</label>
<select ng-options="name for name in classify_columns" ng-model="visualization.options.classify"
class="form-control"></select>
</div>
</div>
<div ng-show="currentTab == 'groups'">
<table class="table table-condensed col-table">
<thead>
<th>Name</th>
<th>Color</th>
</thead>
<tbody>
<tr ng-repeat="(name, options) in visualization.options.groups">
<td>{{name}}</td>
<td>
<input class="form-control" type="color" ng-model="options.color"/>
</td>
</tr>
</tbody>
</table>
</div>
<div ng-show="currentTab == 'map'">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="visualization.options.clusterMarkers">
<i class="input-helper"></i> Cluster Markers
</label>
</div>
<div class="form-group">
<label class="col-lg-2">Longitude column name</label>
<div class="col-sm-4">
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.lonColName" class="form-control"></select>
</div>
<label class="control-label">Map Height (px)</label>
<input class="form-control" type="number" ng-model="visualization.options.height"/>
</div>
</div>
<div ng-show = "visualization.options.draw == 'Color'">
<div class="form-group">
<label class="col-lg-2">Classify by column</label>
<div class="col-sm-4">
<select ng-options="name for name in classify_columns" ng-model="visualization.options.classify" class="form-control"></select>
</div>
</div>
<div class="row" >
<div class="col-lg-6">
<div ng-repeat="element in visualization.options.classification" class="list-group">
<div class="list-group-item active">
{{element.value}}
</div>
<div class="list-group-item">
<div class="form-group">
<label class="col-lg-4">Color</label>
<div class="col-sm-4">
<input class="form-control" style="background-color:{{element.color}};" type="text" ng-model = "element.color" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>