From 5b14c7de5f973de79897006e19eb6df6cfeb546a Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Wed, 28 May 2014 13:58:09 +0200 Subject: [PATCH 01/16] Draw clickable provinces onto map. --- .gitignore | 1 + assets/js/activity.js | 1 + assets/js/maps.js | 46 +++++++++++++++++++++++++++++++++++++------ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 6fd38e15..515126eb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ log dexter/public .idea cache +resources/mysql/places.sql \ No newline at end of file diff --git a/assets/js/activity.js b/assets/js/activity.js index 0f5cd91c..e022e0ad 100644 --- a/assets/js/activity.js +++ b/assets/js/activity.js @@ -69,6 +69,7 @@ if (!self.placesSetup) { Dexter.maps.loadAndDrawPlaces(); self.placesSetup = true; + Dexter.maps.drawProvinces(); } }; diff --git a/assets/js/maps.js b/assets/js/maps.js index 03bb4bb8..39989efb 100644 --- a/assets/js/maps.js +++ b/assets/js/maps.js @@ -16,7 +16,7 @@ var osm = new L.TileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { minZoom: 1, maxZoom: 16, - attribution: 'Map data © OpenStreetMap contributors'}); + attribution: 'Map data © OpenStreetMap contributors'}); self.map.addLayer(osm); }; @@ -35,17 +35,17 @@ return url + "format=places-json"; }; - + self.loadAndDrawPlaces = function() { $.getJSON(self.placesUrl(), self.drawPlaces); }; self.drawPlaceMarker = function(place, coords, radius) { L.circleMarker(coords, { - color: 'red', - fillColor: '#f03', - fillOpacity: 0.5 - }) + color: 'red', + fillColor: '#f03', + fillOpacity: 0.5 + }) .setRadius(radius) .addTo(self.map) .bindPopup(place.full_name + " (" + place.documents.length + ")"); @@ -75,6 +75,40 @@ } }); }; + self.drawProvinces = function(){ + // add province boundaries + $.getJSON("http://maps.code4sa.org/political/2011/province?quantization=1000", function (topo) { + if (!topo) + return; + var featureLayer = L.geoJson(topojson.feature(topo, topo.objects.demarcation), { + style: { + "clickable": true, + "color": "#00d", + "fillColor": "#ccc", + "weight": 1.0, + "opacity": 0.3, + "fillOpacity": 0.3, + }, + onEachFeature: function (feature, layer) { + var name = feature.properties['province_name']; + var code = feature.properties['province']; + + layer.on('mouseover', function () { + layer.setStyle({ + "fillOpacity": 0.5, + }); + }); + layer.on('mouseout', function () { + layer.setStyle({ + "fillOpacity": 0.3, + }); + }); + layer.bindPopup(name); + }, + }); + self.map.addLayer(featureLayer); + }); + } }; })(jQuery, window); From 703fdc814c9b942a0f9922942be1213c0fd50321 Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Thu, 29 May 2014 11:56:11 +0200 Subject: [PATCH 02/16] Add separate endpoint for /coverage-map. --- assets/js/activity.js | 1 - assets/js/coveragemap.js | 57 ++++++++++++++++++ dexter/assets.py | 6 ++ dexter/dashboard.py | 62 ++++++++++++++++++++ dexter/templates/dashboard/coverage-map.haml | 49 ++++++++++++++++ 5 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 assets/js/coveragemap.js create mode 100644 dexter/templates/dashboard/coverage-map.haml diff --git a/assets/js/activity.js b/assets/js/activity.js index e022e0ad..0f5cd91c 100644 --- a/assets/js/activity.js +++ b/assets/js/activity.js @@ -69,7 +69,6 @@ if (!self.placesSetup) { Dexter.maps.loadAndDrawPlaces(); self.placesSetup = true; - Dexter.maps.drawProvinces(); } }; diff --git a/assets/js/coveragemap.js b/assets/js/coveragemap.js new file mode 100644 index 00000000..56849747 --- /dev/null +++ b/assets/js/coveragemap.js @@ -0,0 +1,57 @@ +(function($, exports) { + if (typeof exports.Dexter == 'undefined') exports.Dexter = {}; + var Dexter = exports.Dexter; + + // view when looking at media coverage + Dexter.CoverageView = function() { + var self = this; + + self.placesSetup = false; + + self.init = function() { + $('form.activity-refine .btn.download').on('click', function(e) { + e.preventDefault(); + + $('form.activity-refine').append(''); + $('form.activity-refine').submit(); + $('form.activity-refine input[name="format"]').remove(); + }); + + // invalidate the map so that it gets resized correctly + $($(this).attr('href') + ' .leaflet-container').each(function(i, map) { + Dexter.maps.invalidate(); + }); + + if (!self.placesSetup) { + Dexter.maps.loadAndDrawPlaces(); + self.placesSetup = true; + Dexter.maps.drawProvinces(); + } + }; + + self.datePairs = function(data) { + // transform {"YYYY/MM/DD": 10} into [msecs, 10], sorted by date + return _.map(_.keys(data).sort(), function(key) { + return [moment.utc(key, 'YYYY/MM/DD').valueOf(), data[key]]; + }); + }; + + self.fillDates = function(data) { + // ensure that we have datapoints for all dates in this range + var keys = _.keys(data).sort(); + var min = moment(keys[0], 'YYYY-MM-DD'), + max = moment(keys[keys.length-1], 'YYYY-MM-DD'); + + for (var d = min.clone(); !d.isAfter(max); d.add(1, 'days')) { + var s = d.format('YYYY/MM/DD'); + if (!(s in data)) { + data[s] = 0; + } + } + }; + }; +})(jQuery, window); + +$(function() { + new Dexter.CoverageView().init(); +}); diff --git a/dexter/assets.py b/dexter/assets.py index 0843e790..211c0c47 100644 --- a/dexter/assets.py +++ b/dexter/assets.py @@ -67,6 +67,12 @@ 'js/activity.js', output='js/activity.%(version)s.js')) +assets.register('coveragemap', + Bundle( + maps, + 'js/coveragemap.js', + output='js/activity.%(version)s.js')) + assets.register('documents', Bundle( maps, diff --git a/dexter/dashboard.py b/dexter/dashboard.py index ea7736d0..e34cc77c 100644 --- a/dexter/dashboard.py +++ b/dexter/dashboard.py @@ -65,6 +65,68 @@ def monitor_dashboard(): doc_groups=doc_groups) +@app.route('/coverage-map') +def coverage_map(): + per_page = 100 + + form = ActivityForm(request.args) + + try: + page = int(request.args.get('page', 1)) + except ValueError: + page = 1 + + if form.format.data == 'chart-json': + # chart data in json format + return jsonify(ActivityChartHelper(form).chart_data()) + + elif form.format.data == 'places-json': + # places in json format + query = Document.query\ + .options(joinedload('places').joinedload('place')) + query = form.filter_query(query) + + return jsonify(DocumentPlace.summary_for_docs(query.all())) + + elif form.format.data == 'xlsx': + # excel spreadsheet + excel = XLSXBuilder(form).build() + + response = make_response(excel) + response.headers["Content-Disposition"] = "attachment; filename=%s" % form.filename() + response.headers["Content-Type"] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + return response + + + query = Document.query\ + .options( + joinedload(Document.created_by), + joinedload(Document.medium), + joinedload(Document.topic), + joinedload(Document.origin), + joinedload(Document.fairness), + joinedload(Document.sources).lazyload('*') + ) + query = form.filter_query(query) + + # do manual pagination + query = query.order_by(Document.created_at.desc()) + items = query.limit(per_page).offset((page - 1) * per_page).all() + if not items and page != 1: + abort(404) + total = form.filter_query(db.session.query(func.count(distinct(Document.id)))).scalar() + paged_docs = Pagination(query, page, min(per_page, len(items)), total, items) + + # group by date added + doc_groups = [] + for date, group in groupby(paged_docs.items, lambda d: d.created_at.date()): + doc_groups.append([date, list(group)]) + + return render_template('dashboard/coverage-map.haml', + form=form, + paged_docs=paged_docs, + doc_groups=doc_groups) + @app.route('/activity') @login_required diff --git a/dexter/templates/dashboard/coverage-map.haml b/dexter/templates/dashboard/coverage-map.haml new file mode 100644 index 00000000..eb81b6bf --- /dev/null +++ b/dexter/templates/dashboard/coverage-map.haml @@ -0,0 +1,49 @@ +%%inherit(file="../layout.haml") +%%namespace(file="../paginator.haml", **{'import': '*'}) +%%namespace(file="../bootstrap_wtf.haml", **{'import': '*'}) + +%%block(name='title') + Coverage map + +%%block(name="extra_header_tags") + - for url in webassets('maps-css'): + %link(rel="stylesheet", href=url) + + +%%block(name='extra_javascript') + - for url in webassets('coveragemap'): + %script(src=url) + + +%article#activity + %h3 + &= paged_docs.total + Article${'' if paged_docs.total == 1 else 's'} + - if form.user(): + added by ${form.user().short_name()|h} + + .row + .col-sm-9 + #tab-map.tab-pane + #slippy-map + + .col-sm-3 + %form.activity-refine(action=url_for('coverage_map'), method='GET') + + .panel.panel-default + .panel-heading + %h3.panel-title + Refine + + .panel-body + = vertical_field(form.medium_id, class_='select2') + .form-group + = form.published_at.label() + .input-group + = form.published_at(class_='form-control use-daterangepicker') + %span.input-group-btn + %button.btn.btn-default(dataClear='input-group') + %i.fa.fa-times + + .panel-footer + %input.btn.btn-success(type='Submit', value='Update') From 65539c6edbdf41f99431a66a095d4536624630ec Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Thu, 29 May 2014 12:37:15 +0200 Subject: [PATCH 03/16] Plot Province / Municipality shapes. --- assets/js/coveragemap.js | 19 +++---------- assets/js/maps.js | 60 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 18 deletions(-) diff --git a/assets/js/coveragemap.js b/assets/js/coveragemap.js index 56849747..38e5a17a 100644 --- a/assets/js/coveragemap.js +++ b/assets/js/coveragemap.js @@ -6,27 +6,16 @@ Dexter.CoverageView = function() { var self = this; - self.placesSetup = false; - self.init = function() { - $('form.activity-refine .btn.download').on('click', function(e) { - e.preventDefault(); - - $('form.activity-refine').append(''); - $('form.activity-refine').submit(); - $('form.activity-refine input[name="format"]').remove(); - }); - // invalidate the map so that it gets resized correctly $($(this).attr('href') + ' .leaflet-container').each(function(i, map) { Dexter.maps.invalidate(); }); + Dexter.maps.map.options.maxZoom = 8; + Dexter.maps.loadAndDrawPlaces(); +// Dexter.maps.drawProvinces(); + Dexter.maps.drawMunicipalities("EC"); - if (!self.placesSetup) { - Dexter.maps.loadAndDrawPlaces(); - self.placesSetup = true; - Dexter.maps.drawProvinces(); - } }; self.datePairs = function(data) { diff --git a/assets/js/maps.js b/assets/js/maps.js index 39989efb..a24c76eb 100644 --- a/assets/js/maps.js +++ b/assets/js/maps.js @@ -14,8 +14,8 @@ self.map.setView({lat: -28.4796, lng: 24.698445}, 5); var osm = new L.TileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - minZoom: 1, - maxZoom: 16, + minZoom: 5, + maxZoom: 12, attribution: 'Map data © OpenStreetMap contributors'}); self.map.addLayer(osm); }; @@ -75,6 +75,7 @@ } }); }; + self.drawProvinces = function(){ // add province boundaries $.getJSON("http://maps.code4sa.org/political/2011/province?quantization=1000", function (topo) { @@ -93,6 +94,8 @@ var name = feature.properties['province_name']; var code = feature.properties['province']; + console.log(feature) + layer.on('mouseover', function () { layer.setStyle({ "fillOpacity": 0.5, @@ -103,7 +106,58 @@ "fillOpacity": 0.3, }); }); - layer.bindPopup(name); + layer.on('mouseover', function(e) { + console.log(feature.id) +// //open popup; +// var popup = L.popup() +// .setLatLng(e.latlng) +// .setContent(name + " (" + feature.id + ")") +// .openOn(self.map); + }); + }, + }); + self.map.addLayer(featureLayer); + }); + } + + self.drawMunicipalities = function(province_id){ + // add province boundaries + $.getJSON("http://maps.code4sa.org/political/2011/municipality?quantization=1000&filter[province]=" + province_id, function (topo) { + if (!topo) + return; + var featureLayer = L.geoJson(topojson.feature(topo, topo.objects.demarcation), { + style: { + "clickable": true, + "color": "#00d", + "fillColor": "#ccc", + "weight": 1.0, + "opacity": 0.3, + "fillOpacity": 0.3, + }, + onEachFeature: function (feature, layer) { + var name = feature.properties['province_name']; + var code = feature.properties['province']; + + console.log(feature) + + layer.on('mouseover', function () { + layer.setStyle({ + "fillOpacity": 0.5, + }); + }); + layer.on('mouseout', function () { + layer.setStyle({ + "fillOpacity": 0.3, + }); + }); + layer.on('mouseover', function(e) { + console.log(feature.id) +// //open popup; +// var popup = L.popup() +// .setLatLng(e.latlng) +// .setContent(name + " (" + feature.id + ")") +// .openOn(self.map); + }); }, }); self.map.addLayer(featureLayer); From 612e72280103e805d9730658d926080b658e88e6 Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Thu, 29 May 2014 15:28:01 +0200 Subject: [PATCH 04/16] Clickable shapes. --- assets/js/coveragemap.js | 29 +++++- assets/js/maps.js | 30 +++--- dexter/dashboard.py | 100 ++++++++++++------- dexter/templates/dashboard/coverage-map.haml | 12 ++- 4 files changed, 108 insertions(+), 63 deletions(-) diff --git a/assets/js/coveragemap.js b/assets/js/coveragemap.js index 38e5a17a..2c856838 100644 --- a/assets/js/coveragemap.js +++ b/assets/js/coveragemap.js @@ -6,6 +6,10 @@ Dexter.CoverageView = function() { var self = this; + var form = $(".coverage-refine") + var level = form.find("input#level") + var selected_area = form.find("input#selected_area") + self.init = function() { // invalidate the map so that it gets resized correctly $($(this).attr('href') + ' .leaflet-container').each(function(i, map) { @@ -13,9 +17,26 @@ }); Dexter.maps.map.options.maxZoom = 8; Dexter.maps.loadAndDrawPlaces(); -// Dexter.maps.drawProvinces(); - Dexter.maps.drawMunicipalities("EC"); - + if(level.val() == "country") + { + var fit_screen = true; + Dexter.maps.drawProvinces(fit_screen, function(province_id){ + level.val("province"); + selected_area.val(province_id); + form.submit() + }); + } + else + { + var fit_screen = false; + Dexter.maps.drawProvinces(fit_screen, function(province_id){ + level.val("province"); + selected_area.val(province_id); + form.submit() + }); + fit_screen = true; + Dexter.maps.drawMunicipalities(fit_screen, selected_area.val()); + } }; self.datePairs = function(data) { @@ -29,7 +50,7 @@ // ensure that we have datapoints for all dates in this range var keys = _.keys(data).sort(); var min = moment(keys[0], 'YYYY-MM-DD'), - max = moment(keys[keys.length-1], 'YYYY-MM-DD'); + max = moment(keys[keys.length-1], 'YYYY-MM-DD'); for (var d = min.clone(); !d.isAfter(max); d.add(1, 'days')) { var s = d.format('YYYY/MM/DD'); diff --git a/assets/js/maps.js b/assets/js/maps.js index a24c76eb..797e447b 100644 --- a/assets/js/maps.js +++ b/assets/js/maps.js @@ -76,7 +76,7 @@ }); }; - self.drawProvinces = function(){ + self.drawProvinces = function(fit_screen, click_callback){ // add province boundaries $.getJSON("http://maps.code4sa.org/political/2011/province?quantization=1000", function (topo) { if (!topo) @@ -106,21 +106,20 @@ "fillOpacity": 0.3, }); }); - layer.on('mouseover', function(e) { - console.log(feature.id) -// //open popup; -// var popup = L.popup() -// .setLatLng(e.latlng) -// .setContent(name + " (" + feature.id + ")") -// .openOn(self.map); + layer.on('click', function () { + click_callback(feature.id); }); }, }); self.map.addLayer(featureLayer); + if(fit_screen) + { + self.map.fitBounds(featureLayer.getBounds()); + } }); } - self.drawMunicipalities = function(province_id){ + self.drawMunicipalities = function(fit_screen, province_id, click_callback){ // add province boundaries $.getJSON("http://maps.code4sa.org/political/2011/municipality?quantization=1000&filter[province]=" + province_id, function (topo) { if (!topo) @@ -150,17 +149,16 @@ "fillOpacity": 0.3, }); }); - layer.on('mouseover', function(e) { - console.log(feature.id) -// //open popup; -// var popup = L.popup() -// .setLatLng(e.latlng) -// .setContent(name + " (" + feature.id + ")") -// .openOn(self.map); + layer.on('click', function () { + click_callback(feature.id); }); }, }); self.map.addLayer(featureLayer); + if(fit_screen) + { + self.map.fitBounds(featureLayer.getBounds()); + } }); } }; diff --git a/dexter/dashboard.py b/dexter/dashboard.py index e34cc77c..d5859d4c 100644 --- a/dexter/dashboard.py +++ b/dexter/dashboard.py @@ -67,20 +67,10 @@ def monitor_dashboard(): @app.route('/coverage-map') def coverage_map(): - per_page = 100 - - form = ActivityForm(request.args) - try: - page = int(request.args.get('page', 1)) - except ValueError: - page = 1 - - if form.format.data == 'chart-json': - # chart data in json format - return jsonify(ActivityChartHelper(form).chart_data()) + form = CoverageForm(request.args) - elif form.format.data == 'places-json': + if form.format.data == 'places-json': # places in json format query = Document.query\ .options(joinedload('places').joinedload('place')) @@ -88,44 +78,78 @@ def coverage_map(): return jsonify(DocumentPlace.summary_for_docs(query.all())) - elif form.format.data == 'xlsx': - # excel spreadsheet - excel = XLSXBuilder(form).build() - - response = make_response(excel) - response.headers["Content-Disposition"] = "attachment; filename=%s" % form.filename() - response.headers["Content-Type"] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - return response - - query = Document.query\ .options( - joinedload(Document.created_by), joinedload(Document.medium), joinedload(Document.topic), - joinedload(Document.origin), - joinedload(Document.fairness), - joinedload(Document.sources).lazyload('*') + joinedload(Document.origin).lazyload('*') ) query = form.filter_query(query) # do manual pagination query = query.order_by(Document.created_at.desc()) - items = query.limit(per_page).offset((page - 1) * per_page).all() - if not items and page != 1: - abort(404) - total = form.filter_query(db.session.query(func.count(distinct(Document.id)))).scalar() - paged_docs = Pagination(query, page, min(per_page, len(items)), total, items) - - # group by date added - doc_groups = [] - for date, group in groupby(paged_docs.items, lambda d: d.created_at.date()): - doc_groups.append([date, list(group)]) + document_count = form.filter_query(db.session.query(func.count(distinct(Document.id)))).scalar() + paged_docs = query.all() return render_template('dashboard/coverage-map.haml', form=form, - paged_docs=paged_docs, - doc_groups=doc_groups) + document_count=document_count, + paged_docs=paged_docs) + + +class CoverageForm(Form): + medium_id = SelectMultipleField('Medium', [validators.Optional()], default='') + published_at = TextField('Published', [validators.Optional()]) + format = HiddenField('format', default='html') + level = HiddenField('level', default='country') + selected_area = HiddenField('selected_area', default='RSA') + + def __init__(self, *args, **kwargs): + super(CoverageForm, self).__init__(*args, **kwargs) + + self.medium_id.choices = [(str(m.id), m.name) for m in Medium.query.order_by(Medium.name).all()] + + # dynamic default + if not self.published_at.data: + self.published_at.data = ' - '.join(d.strftime("%Y/%m/%d") for d in [datetime.utcnow() - timedelta(days=14), datetime.utcnow()]) + + def media(self): + if self.medium_id.data: + return Medium.query.filter(Medium.id.in_(self.medium_id.data)) + else: + return None + + @property + def published_from(self): + if self.published_at.data: + return self.published_at.data.split(' - ')[0].strip() + else: + return None + + @property + def published_to(self): + if self.published_at.data and ' - ' in self.published_at.data: + return self.published_at.data.split(' - ')[1].strip() + ' 23:59:59' + else: + return self.published_from + + def filter_query(self, query): + # if self.level and self.level == "province": + # + # query = query.filter(Document.places.contains()) + + if self.medium_id.data: + query = query.filter(Document.medium_id.in_(self.medium_id.data)) + + if self.published_from: + query = query.filter(Document.published_at >= self.published_from) + + if self.published_to: + query = query.filter(Document.published_at <= self.published_to) + return query + + def as_dict(self): + return dict((f.name, f.data) for f in self if f.name != 'csrf_token') @app.route('/activity') diff --git a/dexter/templates/dashboard/coverage-map.haml b/dexter/templates/dashboard/coverage-map.haml index eb81b6bf..c2add092 100644 --- a/dexter/templates/dashboard/coverage-map.haml +++ b/dexter/templates/dashboard/coverage-map.haml @@ -17,10 +17,8 @@ %article#activity %h3 - &= paged_docs.total - Article${'' if paged_docs.total == 1 else 's'} - - if form.user(): - added by ${form.user().short_name()|h} + &= document_count + Article${'' if document_count == 1 else 's'} .row .col-sm-9 @@ -28,7 +26,7 @@ #slippy-map .col-sm-3 - %form.activity-refine(action=url_for('coverage_map'), method='GET') + %form.coverage-refine(action=url_for('coverage_map'), method='GET') .panel.panel-default .panel-heading @@ -44,6 +42,10 @@ %span.input-group-btn %button.btn.btn-default(dataClear='input-group') %i.fa.fa-times + .form-group + .input-group + = form.level + = form.selected_area .panel-footer %input.btn.btn-success(type='Submit', value='Update') From 42785d95aa3e62ed1a5463945f28a9e0df44d0b0 Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Thu, 29 May 2014 17:37:12 +0200 Subject: [PATCH 05/16] Calculate report count per place. --- assets/js/coveragemap.js | 4 +++- assets/js/maps.js | 4 ---- dexter/dashboard.py | 12 ++++++------ dexter/models/place.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/assets/js/coveragemap.js b/assets/js/coveragemap.js index 2c856838..4f0b5f28 100644 --- a/assets/js/coveragemap.js +++ b/assets/js/coveragemap.js @@ -16,7 +16,9 @@ Dexter.maps.invalidate(); }); Dexter.maps.map.options.maxZoom = 8; - Dexter.maps.loadAndDrawPlaces(); + + $.getJSON(Dexter.maps.placesUrl(), function(data){console.log(data)}); + if(level.val() == "country") { var fit_screen = true; diff --git a/assets/js/maps.js b/assets/js/maps.js index 797e447b..1ec859a1 100644 --- a/assets/js/maps.js +++ b/assets/js/maps.js @@ -94,8 +94,6 @@ var name = feature.properties['province_name']; var code = feature.properties['province']; - console.log(feature) - layer.on('mouseover', function () { layer.setStyle({ "fillOpacity": 0.5, @@ -137,8 +135,6 @@ var name = feature.properties['province_name']; var code = feature.properties['province']; - console.log(feature) - layer.on('mouseover', function () { layer.setStyle({ "fillOpacity": 0.5, diff --git a/dexter/dashboard.py b/dexter/dashboard.py index d5859d4c..5f580b25 100644 --- a/dexter/dashboard.py +++ b/dexter/dashboard.py @@ -8,9 +8,9 @@ from flask.ext.login import login_required, current_user from flask.ext.sqlalchemy import Pagination from sqlalchemy.sql import func, distinct -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import joinedload, subqueryload -from dexter.models import db, Document, Entity, Medium, User, DocumentSource, DocumentPlace, DocumentFairness, Fairness, Topic +from dexter.models import db, Document, Entity, Medium, User, DocumentSource, DocumentPlace, DocumentFairness, Fairness, Topic, Place from dexter.models.document import DocumentAnalysisProblem from wtforms import validators, HiddenField, TextField, SelectMultipleField @@ -76,7 +76,7 @@ def coverage_map(): .options(joinedload('places').joinedload('place')) query = form.filter_query(query) - return jsonify(DocumentPlace.summary_for_docs(query.all())) + return jsonify(DocumentPlace.summary_for_coverage(query.all())) query = Document.query\ .options( @@ -134,9 +134,9 @@ def published_to(self): return self.published_from def filter_query(self, query): - # if self.level and self.level == "province": - # - # query = query.filter(Document.places.contains()) + # Note: this filter is not working as expected + # if self.level.data and self.level.data == "province": + # query = query.filter(Place.province_code == self.selected_area.data) if self.medium_id.data: query = query.filter(Document.medium_id.in_(self.medium_id.data)) diff --git a/dexter/models/place.py b/dexter/models/place.py index 39c9eb51..e6c09cd6 100644 --- a/dexter/models/place.py +++ b/dexter/models/place.py @@ -225,6 +225,35 @@ def summary_for_docs(cls, docs): 'origins': origins, } + @classmethod + def summary_for_coverage(cls, docs): + """ + Generate a summary description for plotting on a coverage map. + """ + report_count = {'total': 0, 'topic_breakdown': {}, 'provinces': {}} + + for d in docs: + report_count['total'] += 1 + places = d.get_places() + if places: + tmp_provinces = {} + for dp in places: + if not dp.place.province_code in tmp_provinces: + tmp_provinces[dp.place.province_code] = [] + if dp.place.municipality_code: + tmp_provinces[dp.place.province_code].append(dp.place.municipality_code) + for province_code, municipality_codes in tmp_provinces.iteritems(): + if not report_count['provinces'].get(province_code): + report_count['provinces'][province_code] = {'total': 0, 'topic_breakdown': {}, 'municipalities': {}} + report_count['provinces'][province_code]['total'] += 1 + for municipality_code in municipality_codes: + if not report_count['provinces'][province_code]['municipalities'].get(municipality_code): + report_count['provinces'][province_code]['municipalities'][municipality_code] = \ + {'total': 0, 'topic_breakdown': {}} + report_count['provinces'][province_code]['municipalities'][municipality_code]['total'] += 1 + + return report_count + # Places we know aren't in SA, but sometimes match something in our DB PLACE_STOPWORDS = set(x.strip() for x in """ From e50c1946e0492ac0b93fdc590b2a43713132022c Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Fri, 30 May 2014 08:04:20 +0200 Subject: [PATCH 06/16] Only show navbar controls to signed-in users. --- dexter/templates/layout.haml | 45 ++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/dexter/templates/layout.haml b/dexter/templates/layout.haml index 59f7e903..913c9dfe 100644 --- a/dexter/templates/layout.haml +++ b/dexter/templates/layout.haml @@ -28,25 +28,25 @@ %a.navbar-brand(href="/") Dexter - .navbar-nav.navbar-btn - %a.btn.btn-primary(href=url_for('new_article')) - %i.fa.fa-plus - Add article - - %ul.nav.navbar-nav - %li(class_=('active' if request.url_rule.endpoint == 'activity' else '')) - %a.btn.btn-link(href=url_for('activity')) - Reports - %li(class_=('active' if request.url_rule.endpoint == 'monitor_dashboard' else '')) - %a.btn.btn-link(href=url_for('monitor_dashboard')) - My Dashboard - - - if current_user.admin: - %li - %a.btn.btn-link(href='/admin/') - Dexter Admin - - if current_user.is_authenticated(): + .navbar-nav.navbar-btn + %a.btn.btn-primary(href=url_for('new_article')) + %i.fa.fa-plus + Add article + + %ul.nav.navbar-nav + %li(class_=('active' if request.url_rule.endpoint == 'activity' else '')) + %a.btn.btn-link(href=url_for('activity')) + Reports + %li(class_=('active' if request.url_rule.endpoint == 'monitor_dashboard' else '')) + %a.btn.btn-link(href=url_for('monitor_dashboard')) + My Dashboard + + - if current_user.admin: + %li + %a.btn.btn-link(href='/admin/') + Dexter Admin + .navbar-nav.navbar-right %ul.nav.navbar-nav %li @@ -61,10 +61,11 @@ %a(href=url_for('user_logout'), dataMethod='post') Logout - %form.navbar-form.navbar-right(action=url_for('search')) - .form-group - %input.form-control.allow-enter(type='text', name='q', placeholder='Find people') - %button.btn.btn-default(type='submit') Go + - if current_user.is_authenticated(): + %form.navbar-form.navbar-right(action=url_for('search')) + .form-group + %input.form-control.allow-enter(type='text', name='q', placeholder='Find people') + %button.btn.btn-default(type='submit') Go .container -# show flash messages From 11b101d5226719b62da3c4690751c1106ae3d275 Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Fri, 30 May 2014 08:43:24 +0200 Subject: [PATCH 07/16] Add simple popups on mouseover. --- assets/js/coveragemap.js | 2 +- assets/js/maps.js | 26 ++++++++++++++++++-- dexter/templates/dashboard/coverage-map.haml | 5 ++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/assets/js/coveragemap.js b/assets/js/coveragemap.js index 4f0b5f28..bea1c347 100644 --- a/assets/js/coveragemap.js +++ b/assets/js/coveragemap.js @@ -37,7 +37,7 @@ form.submit() }); fit_screen = true; - Dexter.maps.drawMunicipalities(fit_screen, selected_area.val()); + Dexter.maps.drawMunicipalities(fit_screen, selected_area.val(), function(municipality_id){}); } }; diff --git a/assets/js/maps.js b/assets/js/maps.js index 1ec859a1..3e3d4403 100644 --- a/assets/js/maps.js +++ b/assets/js/maps.js @@ -97,11 +97,22 @@ layer.on('mouseover', function () { layer.setStyle({ "fillOpacity": 0.5, + "weight": 2.0, + "opacity": 0.5, }); + // it's a region, get the centroid + var coords = d3.geo.centroid(feature.geometry); + //open popup + self.popup = L.popup() + .setLatLng([coords[1], coords[0]]) + .setContent(name + " (" + code + ")") + .openOn(self.map); }); layer.on('mouseout', function () { layer.setStyle({ "fillOpacity": 0.3, + "weight": 1.0, + "opacity": 0.3, }); }); layer.on('click', function () { @@ -132,17 +143,28 @@ "fillOpacity": 0.3, }, onEachFeature: function (feature, layer) { - var name = feature.properties['province_name']; - var code = feature.properties['province']; + var name = feature.properties['municipality_name']; + var code = feature.properties['municipality']; layer.on('mouseover', function () { layer.setStyle({ "fillOpacity": 0.5, + "weight": 2.0, + "opacity": 0.5, }); + // it's a region, get the centroid + var coords = d3.geo.centroid(feature.geometry); + //open popup + self.popup = L.popup() + .setLatLng([coords[1], coords[0]]) + .setContent(name + " (" + code + ")") + .openOn(self.map); }); layer.on('mouseout', function () { layer.setStyle({ "fillOpacity": 0.3, + "weight": 1.0, + "opacity": 0.3, }); }); layer.on('click', function () { diff --git a/dexter/templates/dashboard/coverage-map.haml b/dexter/templates/dashboard/coverage-map.haml index c2add092..ca83f99c 100644 --- a/dexter/templates/dashboard/coverage-map.haml +++ b/dexter/templates/dashboard/coverage-map.haml @@ -17,8 +17,9 @@ %article#activity %h3 + South African media coverage (based on &= document_count - Article${'' if document_count == 1 else 's'} + Article${'' if document_count == 1 else 's'}) .row .col-sm-9 @@ -31,7 +32,7 @@ .panel.panel-default .panel-heading %h3.panel-title - Refine + Filters .panel-body = vertical_field(form.medium_id, class_='select2') From 40b98be949da5cb3f74fb778554949e9b9b46ae8 Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Fri, 30 May 2014 10:15:56 +0200 Subject: [PATCH 08/16] Draw barchart with story counts, based on selected area. Remove popups. --- assets/css/activity.scss | 4 + assets/js/coveragemap.js | 151 ++++++++++++++++--- assets/js/maps.js | 41 ++--- dexter/assets.py | 1 + dexter/dashboard.py | 4 +- dexter/models/place.py | 64 +++++--- dexter/templates/dashboard/coverage-map.haml | 25 +-- 7 files changed, 209 insertions(+), 81 deletions(-) diff --git a/assets/css/activity.scss b/assets/css/activity.scss index a4556430..ec147e26 100644 --- a/assets/css/activity.scss +++ b/assets/css/activity.scss @@ -34,4 +34,8 @@ .chart.chart-media { height: 630px; } + + .chart.chart-media-coverage { + height: 300px; + } } diff --git a/assets/js/coveragemap.js b/assets/js/coveragemap.js index bea1c347..1903fff0 100644 --- a/assets/js/coveragemap.js +++ b/assets/js/coveragemap.js @@ -6,9 +6,22 @@ Dexter.CoverageView = function() { var self = this; + // set global variables var form = $(".coverage-refine") - var level = form.find("input#level") - var selected_area = form.find("input#selected_area") + var input_selected_province = form.find("input#selected_province") + var input_selected_municipality = form.find("input#selected_municipality") + var selected_province = input_selected_province.val() + var selected_municipality = input_selected_municipality.val() + var span_title = $("#map-area-title") + + if(selected_province) + console.log(selected_province) + else + console.log("no province selected") + if(selected_municipality) + console.log(selected_municipality) + else + console.log("no municipality selected") self.init = function() { // invalidate the map so that it gets resized correctly @@ -17,29 +30,70 @@ }); Dexter.maps.map.options.maxZoom = 8; - $.getJSON(Dexter.maps.placesUrl(), function(data){console.log(data)}); + self.load_title() - if(level.val() == "country") + Dexter.maps.drawProvinces(self.click_province); + if(selected_province) { - var fit_screen = true; - Dexter.maps.drawProvinces(fit_screen, function(province_id){ - level.val("province"); - selected_area.val(province_id); - form.submit() - }); + Dexter.maps.drawMunicipalities(selected_province, self.click_municipality); +// Dexter.maps.map.fitBounds(Dexter.maps.map.municipalityLayer.getBounds()); } - else +// else +// Dexter.maps.map.fitBounds(Dexter.maps.map.provinceLayer.getBounds()); + + self.load_and_draw_chart() + }; + + self.load_title = function(){ + if(selected_province) { - var fit_screen = false; - Dexter.maps.drawProvinces(fit_screen, function(province_id){ - level.val("province"); - selected_area.val(province_id); - form.submit() + var selected_level = "province" + var selected_area_id = selected_province + if(selected_municipality) + { + selected_level = "municipality" + selected_area_id = selected_municipality + } + var query_str = selected_level + '?filter[' + selected_level + ']=' + selected_area_id + // load selected area's details from MAPS API + $.getJSON("https://maps.code4sa.org/political/2011/" + query_str + '&quantization=5000', function (topo) { + if (!topo) + return; + // set the page title + console.log(topo.objects.demarcation.geometries[0]) + if(selected_level == "municipality") + span_title.text(topo.objects.demarcation.geometries[0].properties.municipality_name) + else + span_title.text(topo.objects.demarcation.geometries[0].properties.province_name) }); - fit_screen = true; - Dexter.maps.drawMunicipalities(fit_screen, selected_area.val(), function(municipality_id){}); } - }; + } + + self.load_and_draw_chart = function(){ + // load chart data + $.getJSON(Dexter.maps.placesUrl(), function(data){ + console.log(data) + if(selected_province) + data = data['provinces'][selected_province] + if(selected_municipality) + data = data['municipalities'][selected_municipality] + if(data) + self.drawChart(data); + else + $(".chart.chart-media-coverage").text("No data available.") + }); + } + + self.click_province = function(province_id){ + input_selected_province.val(province_id); + input_selected_municipality.val(''); + form.submit() + } + + self.click_municipality = function(municipality_id){ + input_selected_municipality.val(municipality_id); + form.submit() + } self.datePairs = function(data) { // transform {"YYYY/MM/DD": 10} into [msecs, 10], sorted by date @@ -61,9 +115,66 @@ } } }; + + self.drawChart = function(chart_data) { + + // charts + var cats = [] + var vals = [] + var medium_breakdown = chart_data.medium_breakdown; + + for (var medium in medium_breakdown) { + if (medium_breakdown.hasOwnProperty(medium)) { + cats.push(medium); + vals.push(medium_breakdown[medium]); + } + } + + $('.chart-media-coverage').highcharts({ + chart: { + type: 'column' + }, + title: { + text: '', + style: { + display: 'none' + } + }, + subtitle: { + text: '', + style: { + display: 'none' + } + }, + xAxis: { + categories: cats, + labels: { + step: 1, + formatter: function(v) { + if (this.value.length > 15) { + return this.value.slice(0, 15) + "..."; + } else { + return this.value; + } + } + } + }, + yAxis: { + title: { + text: 'number of stories' + } + }, + series: [{ + showInLegend: false, + data: vals + }], + }); + } }; + })(jQuery, window); $(function() { - new Dexter.CoverageView().init(); + var coverage_view = new Dexter.CoverageView + coverage_view.init() }); diff --git a/assets/js/maps.js b/assets/js/maps.js index 3e3d4403..efb36047 100644 --- a/assets/js/maps.js +++ b/assets/js/maps.js @@ -76,7 +76,12 @@ }); }; - self.drawProvinces = function(fit_screen, click_callback){ +// self.fitBounds = function(featureLayer) +// { +// self.map.fitBounds(featureLayer.getBounds()); +// } + + self.drawProvinces = function(click_callback){ // add province boundaries $.getJSON("http://maps.code4sa.org/political/2011/province?quantization=1000", function (topo) { if (!topo) @@ -100,13 +105,6 @@ "weight": 2.0, "opacity": 0.5, }); - // it's a region, get the centroid - var coords = d3.geo.centroid(feature.geometry); - //open popup - self.popup = L.popup() - .setLatLng([coords[1], coords[0]]) - .setContent(name + " (" + code + ")") - .openOn(self.map); }); layer.on('mouseout', function () { layer.setStyle({ @@ -120,15 +118,12 @@ }); }, }); - self.map.addLayer(featureLayer); - if(fit_screen) - { - self.map.fitBounds(featureLayer.getBounds()); - } + self.map.provinceLayer = featureLayer; + self.map.addLayer(self.map.provinceLayer); }); } - self.drawMunicipalities = function(fit_screen, province_id, click_callback){ + self.drawMunicipalities = function(province_id, click_callback){ // add province boundaries $.getJSON("http://maps.code4sa.org/political/2011/municipality?quantization=1000&filter[province]=" + province_id, function (topo) { if (!topo) @@ -152,13 +147,6 @@ "weight": 2.0, "opacity": 0.5, }); - // it's a region, get the centroid - var coords = d3.geo.centroid(feature.geometry); - //open popup - self.popup = L.popup() - .setLatLng([coords[1], coords[0]]) - .setContent(name + " (" + code + ")") - .openOn(self.map); }); layer.on('mouseout', function () { layer.setStyle({ @@ -168,15 +156,16 @@ }); }); layer.on('click', function () { + // it's a region, get the centroid + var coords = d3.geo.centroid(feature.geometry); + // center map around centroid + self.map.panTo({lat: coords[1], lng: coords[0]}); click_callback(feature.id); }); }, }); - self.map.addLayer(featureLayer); - if(fit_screen) - { - self.map.fitBounds(featureLayer.getBounds()); - } + self.map.municipalityLayer = featureLayer; + self.map.addLayer(self.map.municipalityLayer); }); } }; diff --git a/dexter/assets.py b/dexter/assets.py index 211c0c47..d0d65cb9 100644 --- a/dexter/assets.py +++ b/dexter/assets.py @@ -70,6 +70,7 @@ assets.register('coveragemap', Bundle( maps, + 'js/highcharts-4.0.1.js', 'js/coveragemap.js', output='js/activity.%(version)s.js')) diff --git a/dexter/dashboard.py b/dexter/dashboard.py index 5f580b25..00672715 100644 --- a/dexter/dashboard.py +++ b/dexter/dashboard.py @@ -101,8 +101,8 @@ class CoverageForm(Form): medium_id = SelectMultipleField('Medium', [validators.Optional()], default='') published_at = TextField('Published', [validators.Optional()]) format = HiddenField('format', default='html') - level = HiddenField('level', default='country') - selected_area = HiddenField('selected_area', default='RSA') + selected_province = HiddenField('selected_province') + selected_municipality = HiddenField('selected_municipality') def __init__(self, *args, **kwargs): super(CoverageForm, self).__init__(*args, **kwargs) diff --git a/dexter/models/place.py b/dexter/models/place.py index e6c09cd6..0df463b2 100644 --- a/dexter/models/place.py +++ b/dexter/models/place.py @@ -8,7 +8,7 @@ String, func, or_, - ) +) from sqlalchemy.orm import relationship, backref import logging @@ -106,7 +106,7 @@ def as_dict(self): 'code': self.code, 'full_name': self.full_name, 'name': self.name, - } + } if d['type'] == 'point': d['coordinates'] = [self.lat, self.lng] @@ -116,8 +116,8 @@ def as_dict(self): def __repr__(self): return "" % ( - self.level, self.province_code, self.municipality_code, - self.mainplace_name, self.subplace_name) + self.level, self.province_code, self.municipality_code, + self.mainplace_name, self.subplace_name) @classmethod @@ -128,25 +128,25 @@ def find(cls, term): if term in PLACE_STOPWORDS: return - p = Place.query\ - .filter(Place.level == 'province')\ - .filter(Place.province_name == term).first() + p = Place.query \ + .filter(Place.level == 'province') \ + .filter(Place.province_name == term).first() if p: return p - p = Place.query\ - .filter(Place.level == 'municipality')\ - .filter(or_( - Place.municipality_name == term, - Place.municipality_name == 'City of %s' % term)).first() + p = Place.query \ + .filter(Place.level == 'municipality') \ + .filter(or_( + Place.municipality_name == term, + Place.municipality_name == 'City of %s' % term)).first() if p: return p - p = Place.query\ - .filter(Place.level == 'mainplace')\ - .filter(or_( - Place.mainplace_name == term, - Place.mainplace_name == '%s MP' % term)).first() + p = Place.query \ + .filter(Place.level == 'mainplace') \ + .filter(or_( + Place.mainplace_name == term, + Place.mainplace_name == '%s MP' % term)).first() if p: return p @@ -177,7 +177,7 @@ class DocumentPlace(db.Model, WithOffsets): # offsets in the document, a space-separated list of offset:length pairs. offset_list = Column(String(1024)) - + created_at = Column(DateTime(timezone=True), index=True, unique=False, nullable=False, server_default=func.now()) updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.current_timestamp()) @@ -223,17 +223,24 @@ def summary_for_docs(cls, docs): 'document_count': count, 'mentions': mentions.values(), 'origins': origins, - } + } @classmethod def summary_for_coverage(cls, docs): """ Generate a summary description for plotting on a coverage map. """ - report_count = {'total': 0, 'topic_breakdown': {}, 'provinces': {}} + + report_count = {'total': 0, 'medium_breakdown': {}, 'topic_breakdown': {}, 'provinces': {}} for d in docs: + # count the total number of articles report_count['total'] += 1 + # count articles per medium + if d.medium: + if not report_count['medium_breakdown'].get(d.medium.name): + report_count['medium_breakdown'][d.medium.name] = 0 + report_count['medium_breakdown'][d.medium.name] += 1 places = d.get_places() if places: tmp_provinces = {} @@ -244,14 +251,25 @@ def summary_for_coverage(cls, docs): tmp_provinces[dp.place.province_code].append(dp.place.municipality_code) for province_code, municipality_codes in tmp_provinces.iteritems(): if not report_count['provinces'].get(province_code): - report_count['provinces'][province_code] = {'total': 0, 'topic_breakdown': {}, 'municipalities': {}} + report_count['provinces'][province_code] = {'total': 0, 'medium_breakdown': {}, 'topic_breakdown': {}, 'municipalities': {}} + # count the number of articles per province report_count['provinces'][province_code]['total'] += 1 + # breakdown per medium + if d.medium: + if not report_count['provinces'][province_code]['medium_breakdown'].get(d.medium.name): + report_count['provinces'][province_code]['medium_breakdown'][d.medium.name] = 0 + report_count['provinces'][province_code]['medium_breakdown'][d.medium.name] += 1 for municipality_code in municipality_codes: if not report_count['provinces'][province_code]['municipalities'].get(municipality_code): report_count['provinces'][province_code]['municipalities'][municipality_code] = \ - {'total': 0, 'topic_breakdown': {}} + {'total': 0, 'medium_breakdown': {}, 'topic_breakdown': {}} + # count the number of articles per municipality report_count['provinces'][province_code]['municipalities'][municipality_code]['total'] += 1 - + # breakdown per medium + if d.medium: + if not report_count['provinces'][province_code]['municipalities'][municipality_code]['medium_breakdown'].get(d.medium.name): + report_count['provinces'][province_code]['municipalities'][municipality_code]['medium_breakdown'][d.medium.name] = 0 + report_count['provinces'][province_code]['municipalities'][municipality_code]['medium_breakdown'][d.medium.name] += 1 return report_count diff --git a/dexter/templates/dashboard/coverage-map.haml b/dexter/templates/dashboard/coverage-map.haml index ca83f99c..64400725 100644 --- a/dexter/templates/dashboard/coverage-map.haml +++ b/dexter/templates/dashboard/coverage-map.haml @@ -15,20 +15,25 @@ %script(src=url) -%article#activity - %h3 - South African media coverage (based on - &= document_count - Article${'' if document_count == 1 else 's'}) +%article#coverage .row - .col-sm-9 + .col-sm-8 + %h3 + Media coverage in + %span#map-area-title South Africa #tab-map.tab-pane #slippy-map - .col-sm-3 - %form.coverage-refine(action=url_for('coverage_map'), method='GET') + .col-sm-4 + .panel.panel-default#charts + .panel-heading + %h4.panel-title Media + .panel-body + .chart.chart-media-coverage + Loading... + %form.coverage-refine(action=url_for('coverage_map'), method='GET') .panel.panel-default .panel-heading %h3.panel-title @@ -45,8 +50,8 @@ %i.fa.fa-times .form-group .input-group - = form.level - = form.selected_area + = form.selected_province + = form.selected_municipality .panel-footer %input.btn.btn-success(type='Submit', value='Update') From aada2a0171e13dafabbf4e0a6230d58661df4714 Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Fri, 30 May 2014 15:41:12 +0200 Subject: [PATCH 09/16] Fit map to bounds of selected province. --- assets/js/coveragemap.js | 13 ++++++------- assets/js/maps.js | 12 ++++++------ 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/assets/js/coveragemap.js b/assets/js/coveragemap.js index 1903fff0..f60d71b9 100644 --- a/assets/js/coveragemap.js +++ b/assets/js/coveragemap.js @@ -10,9 +10,10 @@ var form = $(".coverage-refine") var input_selected_province = form.find("input#selected_province") var input_selected_municipality = form.find("input#selected_municipality") + var span_title = $("#map-area-title") + var selected_province = input_selected_province.val() var selected_municipality = input_selected_municipality.val() - var span_title = $("#map-area-title") if(selected_province) console.log(selected_province) @@ -36,10 +37,7 @@ if(selected_province) { Dexter.maps.drawMunicipalities(selected_province, self.click_municipality); -// Dexter.maps.map.fitBounds(Dexter.maps.map.municipalityLayer.getBounds()); } -// else -// Dexter.maps.map.fitBounds(Dexter.maps.map.provinceLayer.getBounds()); self.load_and_draw_chart() }; @@ -86,13 +84,14 @@ self.click_province = function(province_id){ input_selected_province.val(province_id); - input_selected_municipality.val(''); - form.submit() + input_selected_municipality.val(null); + selected_province = input_selected_province.val() + selected_municipality = input_selected_municipality.val() + self.init() } self.click_municipality = function(municipality_id){ input_selected_municipality.val(municipality_id); - form.submit() } self.datePairs = function(data) { diff --git a/assets/js/maps.js b/assets/js/maps.js index efb36047..b65da24b 100644 --- a/assets/js/maps.js +++ b/assets/js/maps.js @@ -76,11 +76,6 @@ }); }; -// self.fitBounds = function(featureLayer) -// { -// self.map.fitBounds(featureLayer.getBounds()); -// } - self.drawProvinces = function(click_callback){ // add province boundaries $.getJSON("http://maps.code4sa.org/political/2011/province?quantization=1000", function (topo) { @@ -118,13 +113,17 @@ }); }, }); + if(!self.map.provinceLayer) + self.map.fitBounds(featureLayer); self.map.provinceLayer = featureLayer; self.map.addLayer(self.map.provinceLayer); }); } self.drawMunicipalities = function(province_id, click_callback){ - // add province boundaries + if(self.map.municipalityLayer) + self.map.removeLayer(self.map.municipalityLayer); + // add municipality boundaries $.getJSON("http://maps.code4sa.org/political/2011/municipality?quantization=1000&filter[province]=" + province_id, function (topo) { if (!topo) return; @@ -164,6 +163,7 @@ }); }, }); + self.map.fitBounds(featureLayer); self.map.municipalityLayer = featureLayer; self.map.addLayer(self.map.municipalityLayer); }); From de1e56867e29357aa12bc79388fda415839e0392 Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Mon, 2 Jun 2014 14:22:22 +0200 Subject: [PATCH 10/16] Remove two unused functions from coverage_map.js. --- assets/js/coveragemap.js | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/assets/js/coveragemap.js b/assets/js/coveragemap.js index f60d71b9..18757294 100644 --- a/assets/js/coveragemap.js +++ b/assets/js/coveragemap.js @@ -24,6 +24,11 @@ else console.log("no municipality selected") + // highlight selected province, fit map to province bounds, and load municipality shapes + self.select_province = function(){ + + } + self.init = function() { // invalidate the map so that it gets resized correctly $($(this).attr('href') + ' .leaflet-container').each(function(i, map) { @@ -94,27 +99,6 @@ input_selected_municipality.val(municipality_id); } - self.datePairs = function(data) { - // transform {"YYYY/MM/DD": 10} into [msecs, 10], sorted by date - return _.map(_.keys(data).sort(), function(key) { - return [moment.utc(key, 'YYYY/MM/DD').valueOf(), data[key]]; - }); - }; - - self.fillDates = function(data) { - // ensure that we have datapoints for all dates in this range - var keys = _.keys(data).sort(); - var min = moment(keys[0], 'YYYY-MM-DD'), - max = moment(keys[keys.length-1], 'YYYY-MM-DD'); - - for (var d = min.clone(); !d.isAfter(max); d.add(1, 'days')) { - var s = d.format('YYYY/MM/DD'); - if (!(s in data)) { - data[s] = 0; - } - } - }; - self.drawChart = function(chart_data) { // charts From ed23e9d91dd836f851c55fe7f9626bc37addf0e5 Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Mon, 2 Jun 2014 16:30:40 +0200 Subject: [PATCH 11/16] Remove maps.js import. Add d3 to coverage_map. --- dexter/assets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dexter/assets.py b/dexter/assets.py index d0d65cb9..7a8a63d4 100644 --- a/dexter/assets.py +++ b/dexter/assets.py @@ -69,7 +69,8 @@ assets.register('coveragemap', Bundle( - maps, + 'js/d3.v3.min.js', + 'js/topojson.v1.min.js', 'js/highcharts-4.0.1.js', 'js/coveragemap.js', output='js/activity.%(version)s.js')) From 2be2850b51b60f9dbf711fe3bc14be29c903740b Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Mon, 2 Jun 2014 16:31:09 +0200 Subject: [PATCH 12/16] Remove redundant shape plotting functions. --- assets/js/maps.js | 93 ----------------------------------------------- 1 file changed, 93 deletions(-) diff --git a/assets/js/maps.js b/assets/js/maps.js index b65da24b..705f7169 100644 --- a/assets/js/maps.js +++ b/assets/js/maps.js @@ -75,99 +75,6 @@ } }); }; - - self.drawProvinces = function(click_callback){ - // add province boundaries - $.getJSON("http://maps.code4sa.org/political/2011/province?quantization=1000", function (topo) { - if (!topo) - return; - var featureLayer = L.geoJson(topojson.feature(topo, topo.objects.demarcation), { - style: { - "clickable": true, - "color": "#00d", - "fillColor": "#ccc", - "weight": 1.0, - "opacity": 0.3, - "fillOpacity": 0.3, - }, - onEachFeature: function (feature, layer) { - var name = feature.properties['province_name']; - var code = feature.properties['province']; - - layer.on('mouseover', function () { - layer.setStyle({ - "fillOpacity": 0.5, - "weight": 2.0, - "opacity": 0.5, - }); - }); - layer.on('mouseout', function () { - layer.setStyle({ - "fillOpacity": 0.3, - "weight": 1.0, - "opacity": 0.3, - }); - }); - layer.on('click', function () { - click_callback(feature.id); - }); - }, - }); - if(!self.map.provinceLayer) - self.map.fitBounds(featureLayer); - self.map.provinceLayer = featureLayer; - self.map.addLayer(self.map.provinceLayer); - }); - } - - self.drawMunicipalities = function(province_id, click_callback){ - if(self.map.municipalityLayer) - self.map.removeLayer(self.map.municipalityLayer); - // add municipality boundaries - $.getJSON("http://maps.code4sa.org/political/2011/municipality?quantization=1000&filter[province]=" + province_id, function (topo) { - if (!topo) - return; - var featureLayer = L.geoJson(topojson.feature(topo, topo.objects.demarcation), { - style: { - "clickable": true, - "color": "#00d", - "fillColor": "#ccc", - "weight": 1.0, - "opacity": 0.3, - "fillOpacity": 0.3, - }, - onEachFeature: function (feature, layer) { - var name = feature.properties['municipality_name']; - var code = feature.properties['municipality']; - - layer.on('mouseover', function () { - layer.setStyle({ - "fillOpacity": 0.5, - "weight": 2.0, - "opacity": 0.5, - }); - }); - layer.on('mouseout', function () { - layer.setStyle({ - "fillOpacity": 0.3, - "weight": 1.0, - "opacity": 0.3, - }); - }); - layer.on('click', function () { - // it's a region, get the centroid - var coords = d3.geo.centroid(feature.geometry); - // center map around centroid - self.map.panTo({lat: coords[1], lng: coords[0]}); - click_callback(feature.id); - }); - }, - }); - self.map.fitBounds(featureLayer); - self.map.municipalityLayer = featureLayer; - self.map.addLayer(self.map.municipalityLayer); - }); - } }; })(jQuery, window); From 1da052a96ba76c714c6e3f418165254bf3ef8eab Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Mon, 2 Jun 2014 16:36:27 +0200 Subject: [PATCH 13/16] Remove references to maps.js. --- assets/js/coveragemap.js | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/assets/js/coveragemap.js b/assets/js/coveragemap.js index 18757294..6a778e2f 100644 --- a/assets/js/coveragemap.js +++ b/assets/js/coveragemap.js @@ -30,20 +30,6 @@ } self.init = function() { - // invalidate the map so that it gets resized correctly - $($(this).attr('href') + ' .leaflet-container').each(function(i, map) { - Dexter.maps.invalidate(); - }); - Dexter.maps.map.options.maxZoom = 8; - - self.load_title() - - Dexter.maps.drawProvinces(self.click_province); - if(selected_province) - { - Dexter.maps.drawMunicipalities(selected_province, self.click_municipality); - } - self.load_and_draw_chart() }; @@ -74,7 +60,7 @@ self.load_and_draw_chart = function(){ // load chart data - $.getJSON(Dexter.maps.placesUrl(), function(data){ + $.getJSON(self.placesUrl(), function(data){ console.log(data) if(selected_province) data = data['provinces'][selected_province] @@ -87,6 +73,18 @@ }); } + self.placesUrl = function() { + var url = document.location; + + if (document.location.search === "") { + url = url + "?"; + } else { + url = url + "&"; + } + + return url + "format=places-json"; + }; + self.click_province = function(province_id){ input_selected_province.val(province_id); input_selected_municipality.val(null); From 3a60770ee28e1ab68d7930f1d7c5e7ef2ad77a4d Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Mon, 2 Jun 2014 17:59:23 +0200 Subject: [PATCH 14/16] Basic Province map with D3. --- assets/css/activity.scss | 7 ++ assets/js/coveragemap.js | 75 +++++++++----------- dexter/templates/dashboard/coverage-map.haml | 2 +- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/assets/css/activity.scss b/assets/css/activity.scss index ec147e26..25174222 100644 --- a/assets/css/activity.scss +++ b/assets/css/activity.scss @@ -39,3 +39,10 @@ height: 300px; } } + +#coverage-map{ + .province{ + fill: #ddc; + stroke: #333; + } +} \ No newline at end of file diff --git a/assets/js/coveragemap.js b/assets/js/coveragemap.js index 6a778e2f..36bde78f 100644 --- a/assets/js/coveragemap.js +++ b/assets/js/coveragemap.js @@ -24,40 +24,45 @@ else console.log("no municipality selected") - // highlight selected province, fit map to province bounds, and load municipality shapes - self.select_province = function(){ - - } + //Width and height + var w = 800; + var h = 600; + + //Define map projection + var projection = d3.geo.mercator() + .translate([w/2, h/2]) + .center([25.48, -28.76]) + .scale([2000]); + + //Define path generator + var path = d3.geo.path() + .projection(projection); + + //Create SVG element + var svg = d3.select("#coverage-map") + .append("svg") + .attr("width", w) + .attr("height", h); self.init = function() { + d3.json("https://maps.code4sa.org/political/2011/province?filter&quantization=5000", function(error, topo) { + if (error){ + $("#slippy-map").text("No data available.") + return + } + + //Bind data and create one path per GeoJSON feature + svg.selectAll("path") + .data(topojson.feature(topo, topo.objects.demarcation).features) + .enter() + .append("path") + .attr("class", function(d) { return "province " + d.id; }) + .attr("d", path); + }); + self.load_and_draw_chart() }; - self.load_title = function(){ - if(selected_province) - { - var selected_level = "province" - var selected_area_id = selected_province - if(selected_municipality) - { - selected_level = "municipality" - selected_area_id = selected_municipality - } - var query_str = selected_level + '?filter[' + selected_level + ']=' + selected_area_id - // load selected area's details from MAPS API - $.getJSON("https://maps.code4sa.org/political/2011/" + query_str + '&quantization=5000', function (topo) { - if (!topo) - return; - // set the page title - console.log(topo.objects.demarcation.geometries[0]) - if(selected_level == "municipality") - span_title.text(topo.objects.demarcation.geometries[0].properties.municipality_name) - else - span_title.text(topo.objects.demarcation.geometries[0].properties.province_name) - }); - } - } - self.load_and_draw_chart = function(){ // load chart data $.getJSON(self.placesUrl(), function(data){ @@ -85,18 +90,6 @@ return url + "format=places-json"; }; - self.click_province = function(province_id){ - input_selected_province.val(province_id); - input_selected_municipality.val(null); - selected_province = input_selected_province.val() - selected_municipality = input_selected_municipality.val() - self.init() - } - - self.click_municipality = function(municipality_id){ - input_selected_municipality.val(municipality_id); - } - self.drawChart = function(chart_data) { // charts diff --git a/dexter/templates/dashboard/coverage-map.haml b/dexter/templates/dashboard/coverage-map.haml index 64400725..63651a6f 100644 --- a/dexter/templates/dashboard/coverage-map.haml +++ b/dexter/templates/dashboard/coverage-map.haml @@ -23,7 +23,7 @@ Media coverage in %span#map-area-title South Africa #tab-map.tab-pane - #slippy-map + #coverage-map .col-sm-4 .panel.panel-default#charts From 9c20b745829be1718b9178984a5f8c9ae3c91c58 Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Mon, 2 Jun 2014 21:19:25 +0200 Subject: [PATCH 15/16] Clickable provinces & municipalities. --- assets/css/activity.scss | 28 ++++++++-- assets/js/coveragemap.js | 110 +++++++++++++++++++++++++++++++++++---- 2 files changed, 124 insertions(+), 14 deletions(-) diff --git a/assets/css/activity.scss b/assets/css/activity.scss index 25174222..a2da6ed9 100644 --- a/assets/css/activity.scss +++ b/assets/css/activity.scss @@ -41,8 +41,30 @@ } #coverage-map{ - .province{ - fill: #ddc; - stroke: #333; + .feature{ + fill: #ccc; + cursor: pointer; } + + .feature.municipality{ + opacity: 0; + } + + .background { + fill: none; + pointer-events: all; + } + + .feature.active { + fill: orange; + opacity: 1; + } + + .mesh { + fill: none; + stroke: #fff; + stroke-linecap: round; + stroke-linejoin: round; + } + } \ No newline at end of file diff --git a/assets/js/coveragemap.js b/assets/js/coveragemap.js index 36bde78f..a72003a9 100644 --- a/assets/js/coveragemap.js +++ b/assets/js/coveragemap.js @@ -25,12 +25,15 @@ console.log("no municipality selected") //Width and height - var w = 800; - var h = 600; + var width = 750; + var height = 600; + var active = d3.select(null); + + var transition_duration = 300; //Define map projection var projection = d3.geo.mercator() - .translate([w/2, h/2]) + .translate([width/2, height/2]) .center([25.48, -28.76]) .scale([2000]); @@ -41,27 +44,112 @@ //Create SVG element var svg = d3.select("#coverage-map") .append("svg") - .attr("width", w) - .attr("height", h); + .attr("width", width) + .attr("height", height); + + svg.append("rect") + .attr("class", "background") + .attr("width", width) + .attr("height", height) + .on("click", reset); + + var g = svg.append("g") + .style("stroke-width", "1.5px"); + + function clicked(d) { + if (active.node() === this) return reset(); + var feature_id = d.id; + var feature_name = null + if(d.properties.hasOwnProperty('province_name')) + { + feature_name = d.properties.province_name; + load_municipalities(feature_id) + } + else if(d.properties.hasOwnProperty('municipality_name')) + feature_name = d.properties.municipality_name; + + var centroid = projection(d3.geo.centroid(d)); + console.log(feature_id) + + active.classed("active", false); + active = d3.select(this).classed("active", true); + + var bounds = path.bounds(d), + dx = bounds[1][0] - bounds[0][0], + dy = bounds[1][1] - bounds[0][1], + x = (bounds[0][0] + bounds[1][0]) / 2, + y = (bounds[0][1] + bounds[1][1]) / 2, + scale = .9 / Math.max(dx / width, dy / height), + translate = [width / 2 - scale * x, height / 2 - scale * y]; + + g.transition() + .duration(transition_duration) + .style("stroke-width", 1.5 / scale + "px") + .attr("transform", "translate(" + translate + ")scale(" + scale + ")"); + } - self.init = function() { - d3.json("https://maps.code4sa.org/political/2011/province?filter&quantization=5000", function(error, topo) { + function reset() { + active.classed("active", false); + active = d3.select(null); + + g.transition() + .duration(transition_duration) + .style("stroke-width", "1.5px") + .attr("transform", ""); + } + + function load_provinces(){ + d3.json("https://maps.code4sa.org/political/2011/province?filter&quantization=2000", function(error, topo) { if (error){ - $("#slippy-map").text("No data available.") + $("#coverage-map").text("No data available.") return } //Bind data and create one path per GeoJSON feature - svg.selectAll("path") + g.selectAll("path.province") .data(topojson.feature(topo, topo.objects.demarcation).features) .enter() .append("path") - .attr("class", function(d) { return "province " + d.id; }) + .attr("class", function(d) { return "feature province " + d.id; }) + .attr("d", path) + .on("click", clicked); + + g.append("path") + .datum(topojson.mesh(topo, topo.objects.demarcation, function(a, b) { return a !== b; })) + .attr("class", "mesh province") .attr("d", path); }); + } + + function load_municipalities(province_id){ + d3.json("https://maps.code4sa.org/political/2011/municipality?filter&quantization=1000&filter[province]=" + province_id, function(error, topo) { + if (error){ + $("#coverage-map").text("No data available.") + return + } + + g.selectAll("path.municipality").remove() + //Bind data and create one path per GeoJSON feature + g.selectAll("path.municipality") + .data(topojson.feature(topo, topo.objects.demarcation).features) + .enter() + .append("path") + .attr("class", function(d) { return "feature municipality " + d.id; }) + .attr("d", path) + .on("click", clicked); + + g.append("path") + .datum(topojson.mesh(topo, topo.objects.demarcation, function(a, b) { return a !== b; })) + .attr("class", "mesh municipality") + .attr("d", path); + }); + } + + self.init = function() { + load_provinces() self.load_and_draw_chart() - }; + } self.load_and_draw_chart = function(){ // load chart data From 774a1f2e37ade315e078f7c1ea17b53ef95ab3aa Mon Sep 17 00:00:00 2001 From: "Petrus J.v.Rensburg" Date: Tue, 3 Jun 2014 08:15:39 +0200 Subject: [PATCH 16/16] Update chart after clicking area. --- assets/js/coveragemap.js | 94 +++++++++++++++----- dexter/templates/dashboard/coverage-map.haml | 7 +- 2 files changed, 76 insertions(+), 25 deletions(-) diff --git a/assets/js/coveragemap.js b/assets/js/coveragemap.js index a72003a9..5167ecc3 100644 --- a/assets/js/coveragemap.js +++ b/assets/js/coveragemap.js @@ -10,7 +10,12 @@ var form = $(".coverage-refine") var input_selected_province = form.find("input#selected_province") var input_selected_municipality = form.find("input#selected_municipality") - var span_title = $("#map-area-title") + var input_published_at = form.find("input#published_at") + + var map_title = $("#coverage-map-title") + var map_subtitle = $("#coverage-map-subtitle") + var chart_title = $("#medium-breakdown-chart-title") + var chart_subtitle = $("#medium-breakdown-chart-subtitle") var selected_province = input_selected_province.val() var selected_municipality = input_selected_municipality.val() @@ -56,24 +61,7 @@ var g = svg.append("g") .style("stroke-width", "1.5px"); - function clicked(d) { - if (active.node() === this) return reset(); - var feature_id = d.id; - var feature_name = null - if(d.properties.hasOwnProperty('province_name')) - { - feature_name = d.properties.province_name; - load_municipalities(feature_id) - } - else if(d.properties.hasOwnProperty('municipality_name')) - feature_name = d.properties.municipality_name; - - var centroid = projection(d3.geo.centroid(d)); - console.log(feature_id) - - active.classed("active", false); - active = d3.select(this).classed("active", true); - + function pan_and_zoom(d){ var bounds = path.bounds(d), dx = bounds[1][0] - bounds[0][0], dy = bounds[1][1] - bounds[0][1], @@ -88,10 +76,58 @@ .attr("transform", "translate(" + translate + ")scale(" + scale + ")"); } + function update_selection(province_id, municipality_id){ + input_selected_province.val(province_id); + input_selected_municipality.val(municipality_id); + selected_province = input_selected_province.val() + selected_municipality = input_selected_municipality.val() + } + + function click_province(d) { + if (active.node() === this) return reset(); + var feature_id = d.id; + var feature_name = d.properties.province_name; + + load_municipalities(feature_id); + + active.classed("active", false); + active = d3.select(this).classed("active", true); + + map_title.text(feature_name) + update_selection(feature_id, null) + self.load_and_draw_chart() + + pan_and_zoom(d); + } + + function click_municipality(d) { + if (active.node() === this) return reset(); + var feature_id = d.id; + var feature_name = d.properties.municipality_name; + + active.classed("active", false); + active = d3.select(this).classed("active", true); + + map_title.text(feature_name) + update_selection(selected_province, feature_id) + self.load_and_draw_chart() + + pan_and_zoom(d); + } + function reset() { active.classed("active", false); active = d3.select(null); + // reset map title and chart + map_title.text("South Africa") + update_selection(null, null) + self.load_and_draw_chart() + + // remove municipality shapes + g.selectAll("path.municipality").remove() + + // zoom out g.transition() .duration(transition_duration) .style("stroke-width", "1.5px") @@ -112,8 +148,9 @@ .append("path") .attr("class", function(d) { return "feature province " + d.id; }) .attr("d", path) - .on("click", clicked); + .on("click", click_province); + // draw lines around shapes g.append("path") .datum(topojson.mesh(topo, topo.objects.demarcation, function(a, b) { return a !== b; })) .attr("class", "mesh province") @@ -127,7 +164,7 @@ $("#coverage-map").text("No data available.") return } - + g.selectAll("path.municipality").remove() //Bind data and create one path per GeoJSON feature @@ -137,8 +174,9 @@ .append("path") .attr("class", function(d) { return "feature municipality " + d.id; }) .attr("d", path) - .on("click", clicked); + .on("click", click_municipality); + // draw lines around shapes g.append("path") .datum(topojson.mesh(topo, topo.objects.demarcation, function(a, b) { return a !== b; })) .attr("class", "mesh municipality") @@ -152,6 +190,10 @@ } self.load_and_draw_chart = function(){ + // update map subtitle + var published_at = input_published_at.val() + if(published_at) + map_subtitle.text(published_at) // load chart data $.getJSON(self.placesUrl(), function(data){ console.log(data) @@ -160,9 +202,15 @@ if(selected_municipality) data = data['municipalities'][selected_municipality] if(data) + { + chart_subtitle.text("(" + data.total + " articles)"); self.drawChart(data); + } else + { $(".chart.chart-media-coverage").text("No data available.") + chart_subtitle.text(""); + } }); } @@ -223,7 +271,7 @@ }, yAxis: { title: { - text: 'number of stories' + text: '# Articles' } }, series: [{ diff --git a/dexter/templates/dashboard/coverage-map.haml b/dexter/templates/dashboard/coverage-map.haml index 63651a6f..e3f79eb3 100644 --- a/dexter/templates/dashboard/coverage-map.haml +++ b/dexter/templates/dashboard/coverage-map.haml @@ -21,14 +21,17 @@ .col-sm-8 %h3 Media coverage in - %span#map-area-title South Africa + %span#coverage-map-title South Africa + %nobr %small#coverage-map-subtitle ... + #tab-map.tab-pane #coverage-map .col-sm-4 .panel.panel-default#charts .panel-heading - %h4.panel-title Media + %h4.medium-breakdown-chart-title Articles per publisher + %nobr %small#medium-breakdown-chart-subtitle ... .panel-body .chart.chart-media-coverage Loading...