mirror of
https://github.com/valitydev/redash.git
synced 2024-11-07 01:25:16 +00:00
Add markers cluster support & cleanup/refactor code.
This commit is contained in:
parent
e5146c3755
commit
26bd08bb2b
@ -36,7 +36,8 @@
|
|||||||
"material-design-iconic-font": "^2.2.0",
|
"material-design-iconic-font": "^2.2.0",
|
||||||
"plotly.js": "~1.16.0",
|
"plotly.js": "~1.16.0",
|
||||||
"angular-ui-ace": "bower",
|
"angular-ui-ace": "bower",
|
||||||
"angular-vs-repeat": "^1.1.7"
|
"angular-vs-repeat": "^1.1.7",
|
||||||
|
"leaflet.markercluster": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"angular-mocks": "1.2.18",
|
"angular-mocks": "1.2.18",
|
||||||
|
@ -20,6 +20,8 @@
|
|||||||
<link rel="stylesheet" href="/bower_components/font-awesome/css/font-awesome.css">
|
<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/leaflet/dist/leaflet.css">
|
||||||
<link rel="stylesheet" href="/bower_components/angular-resizable/src/angular-resizable.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">
|
<link rel="stylesheet" href="/styles/redash.css">
|
||||||
<!-- endbuild -->
|
<!-- endbuild -->
|
||||||
|
|
||||||
|
@ -11,9 +11,9 @@
|
|||||||
|
|
||||||
var editTemplate = '<map-editor></map-editor>';
|
var editTemplate = '<map-editor></map-editor>';
|
||||||
var defaultOptions = {
|
var defaultOptions = {
|
||||||
'height': 500,
|
height: 500,
|
||||||
'draw': 'Marker',
|
classify: 'none',
|
||||||
'classify':'none'
|
clusterMarkers: true
|
||||||
};
|
};
|
||||||
|
|
||||||
VisualizationProvider.registerVisualization({
|
VisualizationProvider.registerVisualization({
|
||||||
@ -31,16 +31,35 @@
|
|||||||
restrict: 'E',
|
restrict: 'E',
|
||||||
templateUrl: '/views/visualizations/map.html',
|
templateUrl: '/views/visualizations/map.html',
|
||||||
link: function($scope, elm, attrs) {
|
link: function($scope, elm, attrs) {
|
||||||
$scope.$watch('[queryResult && queryResult.getData(), visualization.options.draw,visualization.options.latColName,'+
|
$scope.$watch('queryResult && queryResult.getData()', render, true);
|
||||||
'visualization.options.lonColName,visualization.options.classify,visualization.options.classify]',
|
$scope.$watch('visualization.options', render, true);
|
||||||
render, true);
|
|
||||||
|
|
||||||
angular.element(window).on("resize", resize);
|
angular.element(window).on("resize", resize);
|
||||||
$scope.$watch('visualization.options.height', 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: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
if (!$scope.map) return;
|
if (!map) return;
|
||||||
$scope.map.invalidateSize(false);
|
map.invalidateSize(false);
|
||||||
setBounds();
|
setBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,164 +67,132 @@
|
|||||||
var b = $scope.visualization.options.bounds;
|
var b = $scope.visualization.options.bounds;
|
||||||
|
|
||||||
if(b){
|
if(b){
|
||||||
$scope.map.fitBounds([[b._southWest.lat, b._southWest.lng],[b._northEast.lat, b._northEast.lng]]);
|
map.fitBounds([[b._southWest.lat, b._southWest.lng],[b._northEast.lat, b._northEast.lng]]);
|
||||||
} else if ($scope.features.length > 0){
|
} else if (layers){
|
||||||
var group= new L.featureGroup($scope.features);
|
var allMarkers = _.flatten(_.map(_.values(layers), function(l) { return l.getLayers() }));
|
||||||
$scope.map.fitBounds(group.getBounds());
|
var group = new L.featureGroup(allMarkers);
|
||||||
|
map.fitBounds(group.getBounds());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function render() {
|
var createMarker = function(lat,lon){
|
||||||
var marker = function(lat,lon){
|
if (lat == null || lon == null) return;
|
||||||
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){
|
return L.circleMarker([lat,lon],style)
|
||||||
if (lat == null || lon == null) return;
|
};
|
||||||
|
|
||||||
var color = 'red';
|
function getBounds() {
|
||||||
|
$scope.visualization.options.bounds = map.getBounds();
|
||||||
|
}
|
||||||
|
|
||||||
if (obj &&
|
function createDescription(latCol, lonCol, row) {
|
||||||
obj[$scope.visualization.options.classify] &&
|
var lat = row[latCol];
|
||||||
$scope.visualization.options.classification){
|
var lon = row[lonCol];
|
||||||
var v = $.grep($scope.visualization.options.classification,function(e){
|
|
||||||
return e.value == obj[$scope.visualization.options.classify];
|
var description = '<ul style="list-style-type: none;padding-left: 0">';
|
||||||
});
|
description += "<li><strong>"+lat+ ", " + lon + "</strong>";
|
||||||
if (v.length >0) color = v[0].color;
|
|
||||||
|
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 queryData = $scope.queryResult.getData();
|
||||||
var classify = $scope.visualization.options.classify;
|
var classify = $scope.visualization.options.classify;
|
||||||
|
|
||||||
|
if ($scope.visualization.options.clusterMarkers === undefined) {
|
||||||
|
$scope.visualization.options.clusterMarkers = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (queryData) {
|
if (queryData) {
|
||||||
$scope.visualization.options.classification = [];
|
var pointGroups;
|
||||||
|
if (classify && classify != 'none') {
|
||||||
for (var row in queryData) {
|
pointGroups = _.groupBy(queryData, classify);
|
||||||
if (queryData[row][classify] &&
|
} else {
|
||||||
$.grep($scope.visualization.options.classification, function (e) {
|
pointGroups = {'All': queryData};
|
||||||
return e.value == queryData[row][classify]
|
|
||||||
}).length == 0) {
|
|
||||||
$scope.visualization.options.classification.push({value: queryData[row][classify], color: null});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$.each($scope.visualization.options.classification, function (i, c) {
|
var groupNames = _.keys(pointGroups);
|
||||||
c.color = color(parseInt((i / $scope.visualization.options.classification.length) * 100));
|
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.visualization.options.groups = _.object(groupNames, options);
|
||||||
$scope.map = L.map(elm[0].children[0].children[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
_.each(layers, function(v, k) {
|
||||||
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
removeLayer(v);
|
||||||
}).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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.features = tmp_features;
|
_.each(pointGroups, function(v, k) {
|
||||||
|
addLayer(k, v);
|
||||||
$.each($scope.features, function (i, f) {
|
|
||||||
f.addTo($scope.map)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setBounds();
|
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',
|
restrict: 'E',
|
||||||
templateUrl: '/views/visualizations/map_editor.html',
|
templateUrl: '/views/visualizations/map_editor.html',
|
||||||
link: function($scope, elm, attrs) {
|
link: function($scope, elm, attrs) {
|
||||||
$scope.draw_options = ['Marker','Color'];
|
$scope.currentTab = 'general';
|
||||||
$scope.classify_columns = $scope.queryResult.columnNames.concat('none');
|
$scope.classify_columns = $scope.queryResult.columnNames.concat('none');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,4 +42,5 @@
|
|||||||
<script src="/bower_components/angular-ui-sortable/sortable.js"></script>
|
<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-ui-ace/ui-ace.js"></script>
|
||||||
<script src="/bower_components/angular-vs-repeat/src/angular-vs-repeat.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 -->
|
<!-- endbuild -->
|
||||||
|
@ -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">
|
<div class="form-group">
|
||||||
<label class="col-lg-2">Map height (px)</label>
|
<label class="control-label">Latitude Column Name</label>
|
||||||
<div class="col-sm-4">
|
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.latColName"
|
||||||
<input class="form-control" type="number" ng-model = "visualization.options.height" />
|
class="form-control"></select>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-lg-2">Draw option</label>
|
<label class="control-label">Longitude Column Name</label>
|
||||||
<div class="col-sm-4">
|
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.lonColName"
|
||||||
<select ng-options="opt for opt in draw_options" ng-model="visualization.options.draw" class="form-control"></select>
|
class="form-control"></select>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-lg-2">Latitude column name</label>
|
<label class="control-label">Group By</label>
|
||||||
<div class="col-sm-4">
|
<select ng-options="name for name in classify_columns" ng-model="visualization.options.classify"
|
||||||
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.latColName" class="form-control"></select>
|
class="form-control"></select>
|
||||||
</div>
|
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label class="col-lg-2">Longitude column name</label>
|
<label class="control-label">Map Height (px)</label>
|
||||||
<div class="col-sm-4">
|
<input class="form-control" type="number" ng-model="visualization.options.height"/>
|
||||||
<select ng-options="name for name in queryResult.columnNames" ng-model="visualization.options.lonColName" class="form-control"></select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user