Skip to content

Commit

Permalink
Merge pull request #466 from BrunoSalerno/leaflet-visualization
Browse files Browse the repository at this point in the history
Feature: Map visualization (using Leaflet)
  • Loading branch information
arikfr committed Jul 1, 2015
2 parents 9cdc2cb + b743cce commit e04833c
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 2 deletions.
5 changes: 4 additions & 1 deletion rd_ui/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<link rel="stylesheet" href="/bower_components/pace/themes/pace-theme-minimal.css">
<link rel="stylesheet" href="/bower_components/font-awesome/css/font-awesome.css">
<link rel="stylesheet" href="/bower_components/codemirror/addon/hint/show-hint.css">
<link rel="stylesheet" href="/bower_components/leaflet/dist/leaflet.css">
<link rel="stylesheet" href="/styles/redash.css">
<!-- endbuild -->

Expand Down Expand Up @@ -137,7 +138,8 @@
<script src="/bower_components/mustache/mustache.js"></script>
<script src="/bower_components/canvg/rgbcolor.js"></script>
<script src="/bower_components/canvg/StackBlur.js"></script>
<script src="/bower_components/canvg/canvg.js"></script>
<script src="/bower_components/canvg/canvg.js"></script>
<script src="/bower_components/leaflet/dist/leaflet.js"></script>
<!-- endbuild -->

<!-- build:js({.tmp,app}) /scripts/scripts.js -->
Expand All @@ -154,6 +156,7 @@
<script src="/scripts/visualizations/base.js"></script>
<script src="/scripts/visualizations/chart.js"></script>
<script src="/scripts/visualizations/cohort.js"></script>
<script src="/scripts/visualizations/map.js"></script>
<script src="/scripts/visualizations/counter.js"></script>
<script src="/scripts/visualizations/table.js"></script>
<script src="/scripts/visualizations/pivot.js"></script>
Expand Down
235 changes: 235 additions & 0 deletions rd_ui/app/scripts/visualizations/map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
'use strict';

(function() {
var module = angular.module('redash.visualization');

module.config(['VisualizationProvider', function(VisualizationProvider) {
var renderTemplate =
'<map-renderer ' +
'options="visualization.options" query-result="queryResult">' +
'</map-renderer>';

var editTemplate = '<map-editor></map-editor>';
var defaultOptions = { 'height': 500 };

VisualizationProvider.registerVisualization({
type: 'MAP',
name: 'Map',
renderTemplate: renderTemplate,
editorTemplate: editTemplate,
defaultOptions: defaultOptions
});
}
]);

module.directive('mapRenderer', function() {
return {
restrict: 'E',
templateUrl: '/views/visualizations/map.html',
link: function($scope, elm, attrs) {

var setBounds = function(){
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());
}
};

$scope.$watch('[queryResult && queryResult.getData(), visualization.options.draw,visualization.options.latColName,'+
'visualization.options.lonColName,visualization.options.classify,visualization.options.classify]',
function() {
var marker = function(lat,lon){
if (lat == null || lon == null) return;

return L.marker([lat, lon]);
};

var heatpoint = function(lat,lon,obj){
if (lat == null || lon == null) return;

var color = 'red';

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;
}

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('');
};



function getBounds(e) {
$scope.visualization.options.bounds = $scope.map.getBounds();
}

var queryData = $scope.queryResult.getData();
var classify = $scope.visualization.options.classify;

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});
}
}

$.each($scope.visualization.options.classification, function (i, c) {
c.color = color(parseInt((i / $scope.visualization.options.classification.length) * 100));
});

if (!$scope.map) {
$scope.map = L.map(elm[0].children[0].children[0])
}

L.tileLayer('http://{s}.tile.osm.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);
});

$scope.features = tmp_features;

$.each($scope.features, function (i, f) {
f.addTo($scope.map)
});

setBounds();

$scope.map.on('focus',function(){
$scope.map.on('moveend', getBounds);
});

$scope.map.on('blur',function(){
$scope.map.off('moveend', getBounds);
});


// We redraw the map if it was loaded in a hidden tab
if ($('a[href="#'+$scope.visualization.id+'"]').length > 0) {

$('a[href="#'+$scope.visualization.id+'"]').on('click', function () {
setTimeout(function() {
$scope.map.invalidateSize(false);

setBounds();
},500);
});
}

}
}, true);

$scope.$watch('visualization.options.height', function() {

if (!$scope.map) return;
$scope.map.invalidateSize(false);
setBounds();

});
}
}
});

module.directive('mapEditor', function() {
return {
restrict: 'E',
templateUrl: '/views/visualizations/map_editor.html',
link: function($scope, elm, attrs) {
$scope.visualization.options.draw_options = ['Marker','Color'];
$scope.visualization.options.classify_columns = $scope.queryResult.columnNames.concat('none');

//FIXME: The following line should be removed when defaultOptions work
$scope.visualization.options.height = $scope.visualization.options.height || 500;
}
}
});

})();
3 changes: 3 additions & 0 deletions rd_ui/app/views/visualizations/map.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div style='margin:1%;width:98%;height:{{visualization.options.height}}px'>
<div style="width:100%; height:100%;"></div>
</div>
55 changes: 55 additions & 0 deletions rd_ui/app/views/visualizations/map_editor.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<div class="form-horizontal">
<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>
</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 visualization.options.draw_options" ng-model="visualization.options.draw" class="form-control"></select>
</div>
</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>
</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>
</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 visualization.options.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>
3 changes: 2 additions & 1 deletion rd_ui/bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"font-awesome": "~4.2.0",
"mustache": "~1.0.0",
"canvg": "gabelerner/canvg",
"angular-ui-bootstrap-bower": "~0.12.1"
"angular-ui-bootstrap-bower": "~0.12.1",
"leaflet":"~0.7.3"
},
"devDependencies": {
"angular-mocks": "1.2.18",
Expand Down

0 comments on commit e04833c

Please sign in to comment.