Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Map visualization #650

Merged
merged 4 commits into from
Jun 24, 2016
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
landscaping & eslint & use izip
  • Loading branch information
georgeke committed Jun 24, 2016
commit ae032dfffbe3def666d6894498689d9e1a824250
2 changes: 1 addition & 1 deletion caravel/assets/visualizations/mapbox.css
Original file line number Diff line number Diff line change
@@ -13,4 +13,4 @@ div.widget .slice_container:active {

.slice_container div {
padding-top: 0px;
}
}
77 changes: 38 additions & 39 deletions caravel/assets/visualizations/mapbox.jsx
Original file line number Diff line number Diff line change
@@ -6,8 +6,6 @@ require('./mapbox.css');

import React from 'react';
import ReactDOM from 'react-dom';
import mapboxgl from 'mapbox-gl';
import MapGL from 'react-map-gl';
import ScatterPlotOverlay from 'react-map-gl/src/overlays/scatterplot.react.js';
import Immutable from 'immutable';
import supercluster from 'supercluster';
@@ -16,6 +14,14 @@ import ViewportMercator from 'viewport-mercator-project';
const earthCircumferenceKm = 40075.16;

class ScatterPlotGlowOverlay extends ScatterPlotOverlay {
_kmToPixels(kilometers, latitude, zoomLevel) {
// Algorithm from: http://wiki.openstreetmap.org/wiki/Zoom_levels
var latitudeRad = latitude * (Math.PI / 180);
// Seems like the zoomLevel is off by one
var kmPerPixel = earthCircumferenceKm * Math.cos(latitudeRad) / Math.pow(2, zoomLevel + 8 + 1);
return d3.round(kilometers / kmPerPixel, 2);
}

_isNumeric(num) {
return !isNaN(parseFloat(num)) && isFinite(num);
}
@@ -55,19 +61,21 @@ class ScatterPlotGlowOverlay extends ScatterPlotOverlay {
// Modified from https://github.com/uber/react-map-gl/blob/master/src/overlays/scatterplot.react.js
_redraw() {
const milesToKm = 1.60934;
var pixelRatio = window.devicePixelRatio || 1;
var canvas = this.refs.overlay;
var ctx = canvas.getContext('2d');
var props = this.props;
var radius = this.props.dotRadius;
var fill = this.props.dotFill;
var pixelRatio = window.devicePixelRatio || 1,
canvas = this.refs.overlay,
ctx = canvas.getContext('2d'),
props = this.props,
radius = props.dotRadius,
mercator = ViewportMercator(props),
maxCount = -1,
clusterLabelMap = [],
rgb = props.rgb;

ctx.save();
ctx.scale(pixelRatio, pixelRatio);
ctx.clearRect(0, 0, props.width, props.height);
ctx.globalCompositeOperation = this.props.compositeOperation;
var mercator = ViewportMercator(this.props);
var maxCount = -1;
var clusterLabelMap = [];

props.locations.forEach(function (location, i) {
if (location.get("properties").get("cluster")) {
var clusterLabel = location.get("properties").get("metric")
@@ -94,18 +102,18 @@ class ScatterPlotGlowOverlay extends ScatterPlotOverlay {
clusterLabelMap[i] = clusterLabel;
}
}, this);
var rgb = props.rgb;

if ((this.props.renderWhileDragging || !this.props.isDragging) && this.props.locations) {
if ((props.renderWhileDragging || !props.isDragging) && props.locations) {
this.props.locations.forEach(function _forEach(location, i) {
var pixel = mercator.project(this.props.lngLatAccessor(location));
var pixelRounded = [d3.round(pixel[0], 1), d3.round(pixel[1], 1)];
if (pixelRounded[0] + radius >= 0 &&
pixelRounded[0] - radius < props.width &&
pixelRounded[1] + radius >= 0 &&
pixelRounded[1] - radius < props.height) {
ctx.beginPath();
var pixel = mercator.project(props.lngLatAccessor(location)),
pixelRounded = [d3.round(pixel[0], 1), d3.round(pixel[1], 1)];

if (pixelRounded[0] + radius >= 0
&& pixelRounded[0] - radius < props.width
&& pixelRounded[1] + radius >= 0
&& pixelRounded[1] - radius < props.height) {

ctx.beginPath();
if (location.get("properties").get("cluster")) {
var clusterLabel = clusterLabelMap[i],
scaledRadius = d3.round(
@@ -138,10 +146,10 @@ class ScatterPlotGlowOverlay extends ScatterPlotOverlay {
if (radiusProperty !== null) {
if (props.pointRadiusUnit === "Kilometers") {
pointLabel = d3.round(pointRadius, 2) + "km";
pointRadius = props.kmToPixels(pointRadius, props.latitude, props.zoom);
pointRadius = this._kmToPixels(pointRadius, props.latitude, props.zoom);
} else if (props.pointRadiusUnit === "Miles") {
pointLabel = d3.round(pointRadius, 2) + "mi";
pointRadius = props.kmToPixels(pointRadius * milesToKm, props.latitude, props.zoom);
pointRadius = this._kmToPixels(pointRadius * milesToKm, props.latitude, props.zoom);
}
}

@@ -166,6 +174,7 @@ class ScatterPlotGlowOverlay extends ScatterPlotOverlay {
}
}, this);
}

ctx.restore();
}
}
@@ -201,7 +210,7 @@ class MapboxViz extends React.Component {
width: this.props.sliceWidth,
height: this.props.sliceHeight,
longitude: this.state.viewport.longitude,
latitude: this.state.viewport.latitude,
latitude: this.state.viewport.latitude
}),
topLeft = mercator.unproject([0, 0]),
bottomRight = mercator.unproject([this.props.sliceWidth, this.props.sliceHeight]),
@@ -222,13 +231,12 @@ class MapboxViz extends React.Component {
onChangeViewport={this.onChangeViewport}>
<ScatterPlotGlowOverlay
{...this.state.viewport}
isDragging={this.state.viewport.isDragging !== undefined ? this.state.viewport.isDragging : false}
isDragging={this.state.viewport.isDragging === undefined ? false : this.state.viewport.isDragging}
width={this.props.sliceWidth}
height={this.props.sliceHeight}
locations={Immutable.fromJS(clusters)}
dotRadius={this.props.pointRadius}
pointRadiusUnit={this.props.pointRadiusUnit}
kmToPixels={this.props.kmToPixels}
rgb={this.props.rgb}
globalOpacity={this.props.globalOpacity}
compositeOperation={"screen"}
@@ -243,14 +251,6 @@ class MapboxViz extends React.Component {
}
}

function kmToPixels(kilometers, latitude, zoomLevel) {
// Algorithm from: http://wiki.openstreetmap.org/wiki/Zoom_levels
var latitudeRad = latitude * (Math.PI / 180);
// Seems like the zoomLevel is off by one
var kmPerPixel = earthCircumferenceKm * Math.cos(latitudeRad) / Math.pow(2, zoomLevel + 8 + 1);
return d3.round(kilometers / kmPerPixel, 2);
};

function mapbox(slice) {
const defaultPointRadius = 60;
var div = d3.select(slice.selector);
@@ -287,18 +287,18 @@ function mapbox(slice) {
reducer = function (a, b) {
if (a instanceof Array) {
if (b instanceof Array) {
return a.concat(b)
return a.concat(b);
}
a.push(b);
return a;
} else {
if (b instanceof Array) {
b.push(a);
return b
return b;
}
return [a, b];
}
}
};
}

clusterer = supercluster({
@@ -319,8 +319,7 @@ function mapbox(slice) {
sliceWidth={slice.width()}
clusterer={clusterer}
pointRadius={defaultPointRadius}
aggregatorName={aggName}
kmToPixels={kmToPixels}/>,
aggregatorName={aggName}/>,
div.node()
);

@@ -332,6 +331,6 @@ function mapbox(slice) {
render: render,
resize: function () {}
};
};
}

module.exports = mapbox;
1 change: 0 additions & 1 deletion caravel/data/__init__.py
Original file line number Diff line number Diff line change
@@ -1000,7 +1000,6 @@ def load_long_lat_data():
"datasource_name": "long_lat",
"datasource_type": "table",
"granularity": "day",
"row_limit": config.get("ROW_LIMIT"),
"since": "2014-01-01",
"until": "2016-12-12",
"where": "",
6 changes: 4 additions & 2 deletions caravel/forms.py
Original file line number Diff line number Diff line change
@@ -872,7 +872,8 @@ def __init__(self, viz):
'global_opacity': (DecimalField, {
"label": _("Opacity"),
"default": 1,
"description": _("Opacity of all clusters, points, and labels. Between 0 and 1."),
"description": _("Opacity of all clusters, points, and labels. "
"Between 0 and 1."),
}),
'viewport_zoom': (DecimalField, {
"label": _("Zoom"),
@@ -896,7 +897,8 @@ def __init__(self, viz):
'render_while_dragging': (BetterBooleanField, {
"label": _("Live render"),
"default": True,
"description": _("Points and clusters will update as viewport is being changed")
"description": _("Points and clusters will update as viewport "
"is being changed")
}),
'mapbox_color': (FreeFormSelectField, {
"label": _("RGB Color"),
44 changes: 23 additions & 21 deletions caravel/viz.py
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@
from markdown import markdown
import simplejson as json
from six import string_types
from six.moves import zip
from werkzeug.datastructures import ImmutableMultiDict, MultiDict
from werkzeug.urls import Href
from dateutil import relativedelta as rdelta
@@ -1665,6 +1666,7 @@ class HorizonViz(NVD3TimeSeriesViz):
('series_height', 'horizon_color_scale'),
), }]


class MapboxViz(BaseViz):

"""Rich maps made with Mapbox"""
@@ -1760,18 +1762,18 @@ def query_obj(self):
else:
# Ensuring columns chosen are all in group by
if (label_col and len(label_col) >= 1
and label_col[0] != "count"
and label_col[0] not in fd.get('groupby')):
and label_col[0] != "count"
and label_col[0] not in fd.get('groupby')):
raise Exception(
"Choice of [Label] must be present in [Group By]")

if (fd.get("point_radius") != "Auto"
and fd.get("point_radius") not in fd.get('groupby')):
and fd.get("point_radius") not in fd.get('groupby')):
raise Exception(
"Choice of [Point Radius] must be present in [Group By]")

if (fd.get('all_columns_x') not in fd.get('groupby') or
fd.get('all_columns_y') not in fd.get('groupby')):
if (fd.get('all_columns_x') not in fd.get('groupby')
or fd.get('all_columns_y') not in fd.get('groupby')):
raise Exception(
"[Longitude] and [Latitude] columns must be present in [Group By]")
return d
@@ -1790,40 +1792,40 @@ def get_data(self):
else:
metric_col = df[label_col[0]]
point_radius_col = ([None] * len(df.index)
if fd.get("point_radius") == "Auto"
else df[fd.get("point_radius")])
if fd.get("point_radius") == "Auto"
else df[fd.get("point_radius")])

# using geoJSON formatting
geo_json = {
"type": "FeatureCollection",
"features": [
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"metric": metric,
"radius": point_radius,
},
"geometry": {
"type": "Point",
"coordinates": [lon, lat],
}
"type": "Feature",
"properties": {
"metric": metric,
"radius": point_radius,
},
"geometry": {
"type": "Point",
"coordinates": [lon, lat],
}
}
for lon, lat, metric, point_radius
in zip(
df[fd.get('all_columns_x')],
df[fd.get('all_columns_y')],
metric_col, point_radius_col)
]
]
}

return {
"geoJSON": geo_json,
"customMetric": custom_metric,
"mapboxApiKey": config.get('MAPBOX_API_KEY'),
"mapStyle": fd.get("mapbox_style"),
"aggregatorName": fd.get("pandas_aggfunc"),
"customMetric": custom_metric,
"clusteringRadius": fd.get("clustering_radius"),
"pointRadiusUnit": fd.get("point_radius_unit"),
"mapboxApiKey": config.get('MAPBOX_API_KEY'),
"globalOpacity": fd.get("global_opacity"),
"viewportLongitude": fd.get("viewport_longitude"),
"viewportLatitude": fd.get("viewport_latitude"),