├── .gitignore
├── MANIFEST.in
├── README.md
├── ckanext
├── __init__.py
└── mapviews
│ ├── __init__.py
│ ├── plugin.py
│ ├── tests
│ ├── __init__.py
│ └── test_view.py
│ └── theme
│ ├── public
│ ├── choroplethmap.css
│ ├── choroplethmap.js
│ ├── ckan_map_modules.js
│ ├── navigablemap.css
│ ├── navigablemap.js
│ ├── resource.config
│ ├── vendor
│ │ ├── backend.ckan.js
│ │ ├── d3.scale.quantize.js
│ │ ├── excanvas.js
│ │ ├── leaflet.css
│ │ ├── leaflet.js
│ │ ├── leaflet.label.css
│ │ ├── leaflet.label.js
│ │ └── queryStringToJSON.js
│ └── webassets.yml
│ └── templates
│ ├── base.html
│ ├── choroplethmap_form.html
│ ├── choroplethmap_view.html
│ ├── navigablemap_form.html
│ ├── navigablemap_view.html
│ ├── page.html
│ └── snippets
│ ├── mapviews_asset.html
│ └── mapviews_resource.html
├── doc
├── img
│ ├── pakistan.png
│ └── worldwide-internet-usage.png
└── internet-users-per-100-people.csv
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | syntax: glob
2 | *.pyc
3 | *.egg-info
4 | *.swp
5 | *~
6 | dist
7 | build
8 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | recursive-include ckanext/mapviews/theme *
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ckanext-mapviews
2 | ================
3 |
4 | 
5 |
6 | This extension adds regular and choropleth maps to CKAN, using the new Resource
7 | View being developed on CKAN's master branch (currently unreleased).
8 |
9 | It uses [LeafletJS](http://leafletjs.com), which is compatible with all major
10 | browsers (including IE7+).
11 |
12 | Installation
13 | ------------
14 |
15 | Clone this repository and run ```python setup.py install```. Then add
16 | ```navigablemap``` and/or ```choroplethmap``` to the list in ```ckan.plugins```
17 | in your CKAN config file.
18 |
19 | Restart your webserver. You should see the new "Navigable Map" and/or
20 | "Choropleth Map" chart types (depending on which plugins you added to the list)
21 | as options in the view type's list on any resource that's in the DataStore.
22 |
23 | Usage
24 | -----
25 |
26 | ### Pre-requisites
27 |
28 | To start creating choropleth maps, you need two things: the data you want to
29 | plot, and a GeoJSON defining the geographical regions you'd like to plot it.
30 | The data itself needs to be in a resource inside the DataStore, and the map
31 | needs to be in the same domain as CKAN itself (to avoid [same-origin
32 | policy](http://en.wikipedia.org/wiki/Same-origin_policy) issues). The easiest
33 | way to do so is to upload the GeoJSON as another resource.
34 |
35 | Each GeoJSON feature needs a property related to a column in the data. It can
36 | be an id, name, or anythings that uniquely identifies that feature, so we know
37 | where to plot the data.
38 |
39 | ### Example
40 |
41 | We'll create a map to try to understand the internet usage across the world. To
42 | do so, we need a worldmap in GeoJSON and the internet usage data.
43 |
44 | A good source of GeoJSON files is the [Natural Earth
45 | Data](http://naturalearthdata.com/) website. We'll be using their [world map at
46 | 1:110 million
47 | scale](https://github.com/nvkelso/natural-earth-vector/blob/master/geojson/ne_110m_admin_0_countries.geojson).
48 |
49 | We'll be plotting the [Internet usage per 100
50 | people in 2012](doc/internet-users-per-100-people.csv) all across the world. The data
51 | comes from the great [World Bank's Data
52 | Bank](http://databank.worldbank.org/data/home.aspx). It looks like this:
53 |
54 | | Country Name | Country Code | Indicator Name | Indicator Code | 2012 | ... |
55 | | -------------- | ------------ | ------------------------------- | -------------- | ---------------- | --- |
56 | | Afghanistan | AFG | Internet users (per 100 people) | IT.NET.USER.P2 | 5.45454545454545 | ... |
57 | | Albania | ALB | Internet users (per 100 people) | IT.NET.USER.P2 | 54.6559590399494 | ... |
58 | | Algeria | DZA | Internet users (per 100 people) | IT.NET.USER.P2 | 15.2280267564417 | ... |
59 | | American Samoa | ASM | Internet users (per 100 people) | IT.NET.USER.P2 | | ... |
60 | | Andorra | ADO | Internet users (per 100 people) | IT.NET.USER.P2 | 86.4344246167258 | ... |
61 | | ... | ... | ... | ... | ... | ... |
62 |
63 | To identify each country, we have its name and code. We need to have either
64 | attribute in the GeoJSON feature's properties. Opening that file, we see:
65 |
66 | ```javascript
67 | {
68 | "features": [
69 | {
70 | "geometry": {
71 | "coordinates": [
72 | // ...
73 | ],
74 | "type": "Polygon"
75 | },
76 | "properties": {
77 | "name": "Afghanistan",
78 | "region_un": "Asia",
79 | "region_wb": "South Asia",
80 | "wb_a3": "AFG",
81 | // ...
82 | },
83 | "type": "Feature"
84 | },
85 | // ...
86 | ]
87 | }
88 | ```
89 |
90 | We can map either ```Country Name``` with ```name```, or ```Country Code```
91 | with ```wb_a3```. Let's use the country code.
92 |
93 | In your CKAN instance, create a new dataset (i.e. "World Bank's Indicators"),
94 | and upload two resources: the GeoJSON and the data file.
95 |
96 | Go to the data file's manage resource page and create a new ```Choropleth
97 | Map``` view. You'll see a form with a few fields. Use "Internet usage across
98 | the globe" as a title, leave the description empty (if you want). Now we need
99 | to add the GeoJSON.
100 |
101 | Select in the ```GeoJSON Resource``` field the resource you just created. The
102 | ```GeoJSON Key Field``` should be ```wb_a3```, as we found out before. We'll
103 | link that field to the ```Country Code``` column in our data, so set it
104 | in the ```Key``` field.
105 |
106 | Now, we just need to select what value we want to plot (let's use the latest
107 | year, in the ```2012``` column), and what label to use (```Country Name```).
108 | You can leave the remaining fields blank. In the end, we'll have:
109 |
110 | | Attribute | Value |
111 | | ----------------- | -------------------------------- |
112 | | GeoJSON Resource | _(Your GeoJSON Resource's name)_ |
113 | | GeoJSON Key Field | wb_a3 |
114 | | Key | Country Code |
115 | | Value | 2012 |
116 | | Label | Country Name |
117 | | Redirect to URL | |
118 | | Fields | |
119 |
120 | Click on ```Preview``` and you should see a map like:
121 |
122 | 
123 |
124 | Congratulations! You've just created your first choropleth map. You can go
125 | ahead and see how the maps look like in other years. We can't compare the maps
126 | directly, as the scales change depending on the data, but the difference from
127 | 2000 to 2012 is still impressive.
128 |
129 | ### Filters
130 |
131 | If the map is shown in places other than its original URL in the resource
132 | view's list (for example, inside a
133 | [ckanext-dashboard](//github.com/ckan/ckanext-dashboard) or
134 | [ckanext-page](//github.com/ckan/ckanext-pages)), its regions become clickable.
135 |
136 | When the user clicks on a region, we'll add a filter to the current page using
137 | the `Key` attribute. Using the previous example, if I clicked on Brazil, it'll
138 | add the filters `Country Code:BRA` to the current page.
139 |
140 | The user can deactivate the filters by clicking on the same region again, and
141 | can activate multiple filters.
142 |
143 | If you'd like to, when clicking on a region, redirect the user to another page
144 | with that filter set (for example, another resource view or a dashboard),
145 | you can add the target URL to the `Redirect to URL` field. If that's left
146 | blank, it'll simply add filters to the current page.
147 |
148 | To learn more about it, check the
149 | [ckanext-viewhelpers](//github.com/ckan/ckanext-viewhelpers) page.
150 |
151 | License
152 | -------
153 |
154 | Copyright (C) 2014 Open Knowledge Foundation
155 |
156 | This program is free software: you can redistribute it and/or modify
157 | it under the terms of the GNU Affero General Public License as published
158 | by the Free Software Foundation, either version 3 of the License, or
159 | (at your option) any later version.
160 |
161 | This program is distributed in the hope that it will be useful,
162 | but WITHOUT ANY WARRANTY; without even the implied warranty of
163 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
164 | GNU Affero General Public License for more details.
165 |
166 | You should have received a copy of the GNU Affero General Public License
167 | along with this program. If not, see .
168 |
--------------------------------------------------------------------------------
/ckanext/__init__.py:
--------------------------------------------------------------------------------
1 | # namespace package
2 | try:
3 | import pkg_resources
4 | pkg_resources.declare_namespace(__name__)
5 | except ImportError:
6 | import pkgutil
7 | __path__ = pkgutil.extend_path(__path__, __name__)
8 |
--------------------------------------------------------------------------------
/ckanext/mapviews/__init__.py:
--------------------------------------------------------------------------------
1 | # namespace package
2 | try:
3 | import pkg_resources
4 | pkg_resources.declare_namespace(__name__)
5 | except ImportError:
6 | import pkgutil
7 | __path__ = pkgutil.extend_path(__path__, __name__)
8 |
--------------------------------------------------------------------------------
/ckanext/mapviews/plugin.py:
--------------------------------------------------------------------------------
1 | import ckan.plugins as p
2 |
3 | try:
4 | import urlparse
5 | except:
6 | import urllib.parse as urlparse
7 |
8 | try:
9 | import pylons.config as config
10 | except:
11 | config = p.toolkit.config
12 |
13 |
14 | Invalid = p.toolkit.Invalid
15 | _ = p.toolkit._
16 | not_empty = p.toolkit.get_validator('not_empty')
17 | ignore_missing = p.toolkit.get_validator('ignore_missing')
18 | aslist = p.toolkit.aslist
19 |
20 |
21 | def url_is_relative_or_in_same_domain(url):
22 | site_url = urlparse.urlparse(config.get('ckan.site_url', ''))
23 | parsed_url = urlparse.urlparse(url)
24 |
25 | is_relative = (parsed_url.netloc == '')
26 | is_in_same_domain = (parsed_url.netloc == site_url.netloc)
27 |
28 | if not (is_relative or is_in_same_domain):
29 | message = _('Must be a relative URL or in the same domain as CKAN')
30 | raise Invalid(message)
31 |
32 | return url
33 |
34 |
35 | class NavigableMap(p.SingletonPlugin):
36 | '''Creates a map view'''
37 |
38 | p.implements(p.IConfigurer, inherit=True)
39 | p.implements(p.IResourceView, inherit=True)
40 |
41 | def update_config(self, config):
42 | p.toolkit.add_template_directory(config, 'theme/templates')
43 | p.toolkit.add_resource('theme/public', 'mapviews')
44 |
45 | def info(self):
46 | schema = {
47 | 'geojson_url': [not_empty, url_is_relative_or_in_same_domain],
48 | 'geojson_key_field': [not_empty],
49 | 'resource_key_field': [not_empty],
50 | 'resource_label_field': [not_empty],
51 | 'redirect_to_url': [ignore_missing],
52 | #'filter_fields': [ignore_missing],
53 | }
54 |
55 | return {'name': 'navigable-map',
56 | 'title': 'Navigable Map',
57 | 'icon': 'map-marker',
58 | 'schema': schema,
59 | 'iframed': False,
60 | 'filterable': True,
61 | }
62 |
63 | def can_view(self, data_dict):
64 | return data_dict['resource'].get('datastore_active', False)
65 |
66 | def setup_template_variables(self, context, data_dict):
67 | resource = data_dict['resource']
68 | resource_view = data_dict['resource_view']
69 | #filter_fields = aslist(resource_view.get('filter_fields', []))
70 | #resource_view['filter_fields'] = filter_fields
71 | geojson_resources = _get_geojson_resources()
72 | fields = _get_fields(resource)
73 | fields_without_id = _remove_id_and_prepare_to_template(fields)
74 | numeric_fields = _filter_numeric_fields_without_id(fields)
75 | textual_fields = _filter_textual_fields_without_id(fields)
76 |
77 | return {'resource': resource,
78 | 'resource_view': resource_view,
79 | 'geojson_resources': geojson_resources,
80 | 'fields': fields_without_id,
81 | 'numeric_fields': numeric_fields,
82 | 'textual_fields': textual_fields}
83 |
84 | def view_template(self, context, data_dict):
85 | return 'navigablemap_view.html'
86 |
87 | def form_template(self, context, data_dict):
88 | return 'navigablemap_form.html'
89 |
90 |
91 | class ChoroplethMap(NavigableMap):
92 | '''Creates a choropleth map view'''
93 |
94 | def info(self):
95 | info = super(ChoroplethMap, self).info()
96 | info['name'] = 'choropleth-map'
97 | info['title'] = 'Choropleth Map'
98 | info['schema']['resource_value_field'] = [not_empty]
99 | info['filterable'] = True
100 |
101 | return info
102 |
103 | def view_template(self, context, data_dict):
104 | return 'choroplethmap_view.html'
105 |
106 | def form_template(self, context, data_dict):
107 | return 'choroplethmap_form.html'
108 |
109 |
110 | def _get_geojson_resources():
111 | data = {
112 | 'query': 'format:geojson',
113 | 'order_by': 'name',
114 | }
115 | result = p.toolkit.get_action('resource_search')({}, data)
116 | return [{'text': r['name'], 'value': r['url']}
117 | for r in result.get('results', [])]
118 |
119 |
120 | def _get_fields(resource):
121 | data = {
122 | 'resource_id': resource['id'],
123 | 'limit': 0
124 | }
125 | result = p.toolkit.get_action('datastore_search')({}, data)
126 | return result.get('fields', [])
127 |
128 |
129 | def _remove_id_and_prepare_to_template(fields):
130 | isnt_id = lambda v: v['id'] != '_id'
131 | return [{'value': v['id']} for v in fields if isnt_id(v)]
132 |
133 |
134 | def _filter_numeric_fields_without_id(fields):
135 | isnt_id = lambda v: v['id'] != '_id'
136 | is_numeric = lambda v: v['type'] == 'numeric'
137 | return [{'value': v['id']} for v in fields if isnt_id(v) and is_numeric(v)]
138 |
139 |
140 | def _filter_textual_fields_without_id(fields):
141 | isnt_id = lambda v: v['id'] != '_id'
142 | is_text = lambda v: v['type'] == 'text'
143 | return [{'value': v['id']} for v in fields if isnt_id(v) and is_text(v)]
144 |
--------------------------------------------------------------------------------
/ckanext/mapviews/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ckan/ckanext-mapviews/caf1d0f6ab4168f3ac10a75881fa537144b01d34/ckanext/mapviews/tests/__init__.py
--------------------------------------------------------------------------------
/ckanext/mapviews/tests/test_view.py:
--------------------------------------------------------------------------------
1 | import os
2 | import mock
3 | import inspect
4 | import nose.tools
5 | import pylons.config as config
6 |
7 | import ckan.plugins as p
8 | import ckanext.mapviews.plugin as plugin
9 |
10 |
11 | url_is_relative_or_in_same_domain = plugin.url_is_relative_or_in_same_domain
12 | Invalid = p.toolkit.Invalid
13 |
14 |
15 | def test_url_is_relative_or_in_same_domain_accepts_urls_on_same_domain():
16 | site_url = config.get('ckan.site_url')
17 | url = site_url + "/dataset/something"
18 |
19 | assert url_is_relative_or_in_same_domain(url) == url
20 |
21 |
22 | def test_url_is_relative_or_in_same_domain_accepts_relative_urls():
23 | url = "/dataset/something"
24 | assert url_is_relative_or_in_same_domain(url) == url
25 |
26 |
27 | @nose.tools.raises(Invalid)
28 | def test_url_is_relative_or_in_same_domain_raises_if_not_on_the_same_domain():
29 | url_is_relative_or_in_same_domain("http://some-other-domain.com")
30 |
31 |
32 | class TestNavigableMap(object):
33 |
34 | @classmethod
35 | def setup_class(cls):
36 | p.load('navigablemap')
37 | cls.plugin = p.get_plugin('navigablemap')
38 |
39 | @classmethod
40 | def teardown_class(cls):
41 | p.unload('navigablemap')
42 |
43 | def test_plugin_templates_path_is_added_to_config(self):
44 | filename = inspect.getfile(inspect.currentframe())
45 | path = os.path.dirname(filename)
46 | templates_path = os.path.abspath(path + "/../theme/templates")
47 |
48 | assert templates_path in config['extra_template_paths'], templates_path
49 |
50 | def test_can_view_is_true_when_datastore_is_active(self):
51 | active_datastore_data_dict = {
52 | 'resource': { 'datastore_active': True }
53 | }
54 | assert self.plugin.can_view(active_datastore_data_dict)
55 |
56 | def test_can_view_is_false_when_datastore_is_inactive(self):
57 | inactive_datastore_data_dict = {
58 | 'resource': { 'datastore_active': False }
59 | }
60 | assert not self.plugin.can_view(inactive_datastore_data_dict)
61 |
62 | def test_view_template_is_correct(self):
63 | view_template = self.plugin.view_template({}, {})
64 | assert view_template == 'navigablemap_view.html'
65 |
66 | def test_form_template_is_correct(self):
67 | form_template = self.plugin.form_template({}, {})
68 | assert form_template == 'navigablemap_form.html'
69 |
70 | def test_schema_exists(self):
71 | schema = self.plugin.info()['schema']
72 | assert schema is not None, 'Plugin should define schema'
73 |
74 | def test_schema_has_geojson_url(self):
75 | schema = self.plugin.info()['schema']
76 | assert schema.get('geojson_url') is not None, \
77 | 'Schema should define "geojson_url"'
78 |
79 | def test_schema_geojson_url_isnt_empty(self):
80 | schema = self.plugin.info()['schema']
81 | not_empty = p.toolkit.get_validator('not_empty')
82 | assert not_empty in schema['geojson_url'], \
83 | '"geojson_url" should not be empty'
84 |
85 | def test_schema_geojson_url_is_relative_or_in_same_domain(self):
86 | schema = self.plugin.info()['schema']
87 |
88 | assert url_is_relative_or_in_same_domain in schema['geojson_url'], \
89 | '"geojson_url" should be relative or in same domain'
90 |
91 | def test_schema_has_geojson_key_field(self):
92 | schema = self.plugin.info()['schema']
93 | assert schema.get('geojson_key_field') is not None, \
94 | 'Schema should define "geojson_key_field"'
95 |
96 | def test_schema_geojson_key_field_isnt_empty(self):
97 | schema = self.plugin.info()['schema']
98 | not_empty = p.toolkit.get_validator('not_empty')
99 | assert not_empty in schema['geojson_key_field'], \
100 | '"geojson_key_field" should not be empty'
101 |
102 | def test_schema_has_resource_key_field(self):
103 | schema = self.plugin.info()['schema']
104 | assert schema.get('resource_key_field') is not None, \
105 | 'Schema should define "resource_key_field"'
106 |
107 | def test_schema_resource_key_field_isnt_empty(self):
108 | schema = self.plugin.info()['schema']
109 | not_empty = p.toolkit.get_validator('not_empty')
110 | assert not_empty in schema['resource_key_field'], \
111 | '"resource_key_field" should not be empty'
112 |
113 | def test_schema_has_resource_label_field(self):
114 | schema = self.plugin.info()['schema']
115 | assert schema.get('resource_label_field') is not None, \
116 | 'Schema should define "resource_label_field"'
117 |
118 | def test_schema_resource_label_field_isnt_empty(self):
119 | schema = self.plugin.info()['schema']
120 | not_empty = p.toolkit.get_validator('not_empty')
121 | assert not_empty in schema['resource_label_field'], \
122 | '"resource_label_field" should not be empty'
123 |
124 | def test_schema_has_redirect_to_url(self):
125 | schema = self.plugin.info()['schema']
126 | assert schema.get('redirect_to_url') is not None, \
127 | 'Schema should define "redirect_to_url"'
128 |
129 | def test_schema_redirect_to_url_isnt_required(self):
130 | schema = self.plugin.info()['schema']
131 | ignore_missing = p.toolkit.get_validator('ignore_missing')
132 | assert ignore_missing in schema['redirect_to_url'], \
133 | '"redirect_to_url" should not be required'
134 |
135 | def test_schema_has_filter_fields(self):
136 | schema = self.plugin.info()['schema']
137 | assert schema.get('filter_fields') is not None, \
138 | 'Schema should define "filter_fields"'
139 |
140 | def test_schema_filter_fields_isnt_required(self):
141 | schema = self.plugin.info()['schema']
142 | ignore_missing = p.toolkit.get_validator('ignore_missing')
143 | assert ignore_missing in schema['filter_fields'], \
144 | '"filter_fields" should not be required'
145 |
146 | def test_plugin_isnt_iframed(self):
147 | iframed = self.plugin.info().get('iframed', True)
148 | assert not iframed, 'Plugin should not be iframed'
149 |
150 | @mock.patch('ckan.plugins.toolkit.get_action')
151 | def test_setup_template_variables_adds_resource(self, _):
152 | resource = {
153 | 'id': 'resource_id',
154 | }
155 |
156 | template_variables = self._setup_template_variables(resource)
157 |
158 | assert 'resource' in template_variables
159 | assert template_variables['resource'] == resource
160 |
161 | @mock.patch('ckan.plugins.toolkit.get_action')
162 | def test_setup_template_variables_adds_resource_view(self, _):
163 | resource_view = {
164 | 'id': 'resource_id',
165 | 'other_attribute': 'value'
166 | }
167 |
168 | template_variables = \
169 | self._setup_template_variables(resource_view=resource_view)
170 |
171 | assert 'resource_view' in template_variables
172 | assert template_variables['resource_view'] == resource_view
173 |
174 | @mock.patch('ckan.plugins.toolkit.get_action')
175 | def test_setup_template_variables_resource_view_converts_filter_fields_to_list(self, _):
176 | resource_view = {
177 | 'filter_fields': 'value'
178 | }
179 |
180 | template_variables = \
181 | self._setup_template_variables(resource_view=resource_view)
182 |
183 | resource_view = template_variables['resource_view']
184 | assert resource_view['filter_fields'] == ['value']
185 |
186 | @mock.patch('ckan.plugins.toolkit.get_action')
187 | def test_setup_template_variables_adds_geojson_resources(self, get_action):
188 | fields = [
189 | {
190 | 'name': 'Map',
191 | 'url': 'http://demo.ckan.org/map.json',
192 | }
193 | ]
194 | expected_fields = [{
195 | 'text': 'Map',
196 | 'value': 'http://demo.ckan.org/map.json'
197 | }]
198 |
199 | get_action.return_value.return_value = {
200 | 'count': 1L,
201 | 'results': fields
202 | }
203 | template_variables = self._setup_template_variables()
204 |
205 | returned_fields = template_variables.get('geojson_resources')
206 | assert returned_fields is not None
207 | assert returned_fields == expected_fields
208 |
209 | @mock.patch('ckan.plugins.toolkit.get_action')
210 | def test_setup_template_variables_adds_fields_without_the_id(self, get_action):
211 | fields = [
212 | {'id': '_id', 'type': 'int4'},
213 | {'id': 'price', 'type': 'numeric'},
214 | ]
215 | expected_fields = [{'value': 'price'}]
216 |
217 | get_action.return_value.return_value = {
218 | 'fields': fields,
219 | 'records': {}
220 | }
221 | template_variables = self._setup_template_variables()
222 |
223 | returned_fields = template_variables.get('fields')
224 | assert returned_fields is not None
225 | assert returned_fields == expected_fields
226 |
227 | @mock.patch('ckan.plugins.toolkit.get_action')
228 | def test_setup_template_variables_adds_numeric_fields(self, get_action):
229 | fields = [
230 | {'id': '_id', 'type': 'int4'},
231 | {'id': 'price', 'type': 'numeric'},
232 | {'id': 'name', 'type': 'text'}
233 | ]
234 | expected_fields = [{'value': 'price'}]
235 |
236 | get_action.return_value.return_value = {
237 | 'fields': fields,
238 | 'records': {}
239 | }
240 | template_variables = self._setup_template_variables()
241 |
242 | returned_fields = template_variables.get('numeric_fields')
243 | assert returned_fields is not None
244 | assert returned_fields == expected_fields
245 |
246 | @mock.patch('ckan.plugins.toolkit.get_action')
247 | def test_setup_template_variables_adds_textual_fields(self, get_action):
248 | fields = [
249 | {'id': '_id', 'type': 'int4'},
250 | {'id': 'price', 'type': 'numeric'},
251 | {'id': 'name', 'type': 'text'}
252 | ]
253 | expected_fields = [{'value': 'name'}]
254 |
255 | get_action.return_value.return_value = {
256 | 'fields': fields,
257 | 'records': {}
258 | }
259 | template_variables = self._setup_template_variables()
260 |
261 | returned_fields = template_variables.get('textual_fields')
262 | assert returned_fields is not None
263 | assert returned_fields == expected_fields
264 |
265 | @mock.patch('ckan.plugins.toolkit.get_action')
266 | def test_setup_template_variables_adds_default_filter_fields(self, get_action):
267 | template_variables = self._setup_template_variables()
268 |
269 | filter_fields = template_variables['resource_view'].get('filter_fields')
270 | assert filter_fields is not None
271 | assert filter_fields == []
272 |
273 | def _setup_template_variables(self, resource={'id': 'id'}, resource_view={}):
274 | context = {}
275 | data_dict = {
276 | 'resource': resource,
277 | 'resource_view': resource_view
278 | }
279 | return self.plugin.setup_template_variables(context, data_dict)
280 |
281 |
282 | class TestChoroplethMap(object):
283 |
284 | @classmethod
285 | def setup_class(cls):
286 | p.load('choroplethmap')
287 | cls.plugin = p.get_plugin('choroplethmap')
288 |
289 | @classmethod
290 | def teardown_class(cls):
291 | p.unload('choroplethmap')
292 |
293 | def test_view_template_is_correct(self):
294 | view_template = self.plugin.view_template({}, {})
295 | assert view_template == 'choroplethmap_view.html'
296 |
297 | def test_form_template_is_correct(self):
298 | form_template = self.plugin.form_template({}, {})
299 | assert form_template == 'choroplethmap_form.html'
300 |
301 | def test_schema_has_resource_value_field(self):
302 | schema = self.plugin.info()['schema']
303 | assert schema.get('resource_value_field') is not None, \
304 | 'Schema should define "resource_value_field"'
305 |
306 | def test_schema_resource_value_field_isnt_empty(self):
307 | schema = self.plugin.info()['schema']
308 | not_empty = p.toolkit.get_validator('not_empty')
309 | assert not_empty in schema['resource_value_field'], \
310 | '"resource_value_field" should not be empty'
311 |
--------------------------------------------------------------------------------
/ckanext/mapviews/theme/public/choroplethmap.css:
--------------------------------------------------------------------------------
1 | .choroplethmap .legend {
2 | margin: 0;
3 | padding: 0;
4 | list-style: none;
5 | }
6 |
7 | .choroplethmap .legend span {
8 | width: 16px;
9 | height: 16px;
10 | float: left;
11 | margin-right: 8px;
12 | border: 1px solid #969696;
13 | }
14 |
--------------------------------------------------------------------------------
/ckanext/mapviews/theme/public/choroplethmap.js:
--------------------------------------------------------------------------------
1 | this.ckan = this.ckan || {};
2 | this.ckan.views = this.ckan.views || {};
3 | this.ckan.views.mapviews = this.ckan.views.mapviews || {};
4 |
5 | this.ckan.views.mapviews.choroplethmap = (function () {
6 | 'use strict';
7 |
8 | var noDataColor = '#F7FBFF',
9 | opacity = 0.7,
10 | colors = ['#C6DBEF', '#9ECAE1', '#6BAED6', '#4292C6',
11 | '#2171B5', '#08519C', '#08306B'],
12 | defaultStyle = {
13 | // fillColor will be set depending on the feature's value
14 | fillOpacity: opacity,
15 | };
16 |
17 | function initialize(element, options, noDataLabel, geojson, featuresValues) {
18 | var map = ckan.views.mapviews.navigablemap(element, options, noDataLabel, geojson, featuresValues),
19 | scale = _createScale(featuresValues, geojson),
20 | onEachFeature = _onEachFeature(options.geojsonKeyField, featuresValues, noDataLabel, scale);
21 |
22 | _addLegend(map, scale, opacity, noDataLabel);
23 |
24 | $.each(map._layers, function (i, layer) {
25 | if (layer.feature !== undefined) {
26 | onEachFeature(layer.feature, layer);
27 | }
28 | });
29 | }
30 |
31 | function _createScale(featuresValues, geojson) {
32 | var values = $.map(featuresValues, function (feature, key) {
33 | return feature.value;
34 | }).sort(function (a, b) { return a - b; }),
35 | min = values[0],
36 | max = values[values.length - 1];
37 |
38 | return d3.scale.quantize()
39 | .domain([min, max])
40 | .range(colors);
41 | }
42 |
43 | function _addLegend(map, scale, opacity, noDataLabel) {
44 | var legend = L.control({ position: 'bottomright' });
45 |
46 | legend.onAdd = function (map) {
47 | var div = L.DomUtil.create('div', 'info'),
48 | ul = L.DomUtil.create('ul', 'legend'),
49 | domain = scale.domain(),
50 | range = scale.range(),
51 | min = domain[0] + 0.0000000001,
52 | max = domain[domain.length - 1],
53 | step = (max - min)/range.length,
54 | grades = $.map(range, function (_, i) { return (min + step * i); }),
55 | labels = [];
56 |
57 | div.appendChild(ul);
58 | for (var i = 0, len = grades.length; i < len; i++) {
59 | ul.innerHTML +=
60 | '
' +
61 | _formatNumber(grades[i]) +
62 | (grades[i + 1] ? '–' + _formatNumber(grades[i + 1]) + '' : '+');
63 | }
64 |
65 | ul.innerHTML +=
66 | ' ' +
67 | noDataLabel + '';
68 |
69 | return div;
70 | };
71 |
72 | legend.addTo(map);
73 | }
74 |
75 | function _onEachFeature(geojsonKeyField, featuresValues, noDataLabel, scale) {
76 | return function (feature, layer) {
77 | var elementData = featuresValues[feature.properties[geojsonKeyField]],
78 | value = elementData && elementData.value,
79 | label = elementData && elementData.label,
80 | color = (value) ? scale(value) : noDataColor;
81 |
82 | layer.setStyle($.extend({ fillColor: color }, defaultStyle));
83 |
84 | if (label) {
85 | var layerLabel = elementData.label + ': ' + (value || noDataLabel);
86 | layer.bindLabel(layerLabel);
87 | } else {
88 | layer.bindLabel(noDataLabel);
89 | }
90 | };
91 | }
92 |
93 | function _formatNumber(num) {
94 | return (num % 1 ? num.toFixed(2) : num);
95 | }
96 |
97 | return initialize;
98 | })();
99 |
--------------------------------------------------------------------------------
/ckanext/mapviews/theme/public/ckan_map_modules.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | 'use strict';
3 |
4 | function moduleInitializationFor(mapType) {
5 | return function ($, _) {
6 | function initialize() {
7 | var self = this,
8 | el = self.el,
9 | options = self.options,
10 | noDataLabel = self.i18n('noData'),
11 | filterFields = self.options.filterFields,
12 | resource = {
13 | id: options.resourceId,
14 | endpoint: options.endpoint || self.sandbox.client.endpoint + '/api'
15 | };
16 |
17 | var filters = [];
18 | for (var filter in filterFields){
19 | if (filterFields.hasOwnProperty(filter)) {
20 | filters.push({
21 | "type": "term",
22 | "field": filter,
23 | "term": filterFields[filter]
24 | });
25 | }
26 | };
27 |
28 | var query = {
29 | size: 1000,
30 | filters: filters
31 | };
32 |
33 | $.when(
34 | $.getJSON(options.geojsonUrl),
35 | recline.Backend.Ckan.query(query, resource)
36 | ).done(function (geojson, query) {
37 | var featuresValues = _mapResourceKeyFieldToValues(options.resourceKeyField,
38 | options.resourceValueField,
39 | options.resourceLabelField,
40 | options.geojsonKeyField,
41 | geojson[0],
42 | query.hits);
43 |
44 | ckan.views.mapviews[mapType](el, options, noDataLabel, geojson[0], featuresValues);
45 | });
46 | }
47 |
48 | return {
49 | options: {
50 | i18n: {
51 | noData: _('No data')
52 | }
53 | },
54 | initialize: initialize
55 | };
56 | };
57 | }
58 |
59 | function _mapResourceKeyFieldToValues(resourceKeyField, resourceValueField, resourceLabelField, geojsonKeyField, geojson, data) {
60 | var mapping = {},
61 | geojsonKeys = _getGeojsonKeys(geojsonKeyField, geojson);
62 |
63 | $.each(data, function (i, d) {
64 | var key = d[resourceKeyField],
65 | label = d[resourceLabelField],
66 | value = d[resourceValueField];
67 |
68 | if (geojsonKeys.indexOf(key) === -1) {
69 | return;
70 | }
71 | mapping[key] = {
72 | key: key,
73 | label: label,
74 | data: d
75 | };
76 |
77 | if (value) {
78 | mapping[key].value = parseFloat(value);
79 | }
80 | });
81 |
82 | return mapping;
83 | }
84 |
85 | function _getGeojsonKeys(geojsonKeyField, geojson) {
86 | var result = [],
87 | features = geojson.features,
88 | i,
89 | len = features.length;
90 |
91 | for (i = 0; i < len; i++) {
92 | result.push(features[i].properties[geojsonKeyField]);
93 | }
94 |
95 | return result;
96 | }
97 |
98 | ckan.module('navigablemap', moduleInitializationFor('navigablemap'));
99 | ckan.module('choroplethmap', moduleInitializationFor('choroplethmap'));
100 | })();
101 |
--------------------------------------------------------------------------------
/ckanext/mapviews/theme/public/navigablemap.css:
--------------------------------------------------------------------------------
1 | .navigablemap {
2 | height: 500px;
3 | cursor: default;
4 | }
5 |
6 | .dashboard-grid .navigablemap {
7 | width: 100%;
8 | height: 100%;
9 | }
10 |
11 | .navigablemap .non-clickable {
12 | cursor: default;
13 | }
14 |
15 | .navigablemap .info {
16 | line-height: 18px;
17 | background-color: white;
18 | padding: 0.5em;
19 | }
20 |
21 | .choropleth-map-removeField {
22 | cursor: pointer;
23 | color: #bd362f;
24 | }
25 |
--------------------------------------------------------------------------------
/ckanext/mapviews/theme/public/navigablemap.js:
--------------------------------------------------------------------------------
1 | this.ckan = this.ckan || {};
2 | this.ckan.views = this.ckan.views || {};
3 | this.ckan.views.mapviews = this.ckan.views.mapviews || {};
4 |
5 | this.ckan.views.mapviews.navigablemap = (function () {
6 | 'use strict';
7 |
8 | var borderColor = '#031127',
9 | defaultStyle = {
10 | fillColor: '#4292C6',
11 | fillOpacity: 0.7,
12 | opacity: 0.1,
13 | weight: 2,
14 | color: borderColor
15 | },
16 | highlightStyle = {
17 | weight: 5
18 | },
19 | nonHighlightedStyle = {
20 | weight: 2
21 | },
22 | activeStyle = {
23 | opacity: 1,
24 | color: '#d73027'
25 | };
26 |
27 | function initialize(element, options, noDataLabel, geojson, featuresValues) {
28 | var elementId = element['0'].id,
29 | geojsonUrl = options.geojsonUrl,
30 | geojsonKeyField = options.geojsonKeyField,
31 | resourceKeyField = options.resourceKeyField,
32 | redirectToUrl = (options.redirectToUrl === true) ? '' : options.redirectToUrl,
33 | filterFields = options.filterFields,
34 | map = L.map(elementId),
35 | geojsonLayer,
36 | bounds,
37 | maxBounds,
38 | router;
39 |
40 | var isInOwnResourceViewPage = $(element.parent()).hasClass('ckanext-datapreview');
41 | if (!isInOwnResourceViewPage) {
42 | router = _router(resourceKeyField, geojsonKeyField, redirectToUrl, filterFields, featuresValues);
43 | }
44 |
45 | _addBaseLayer(map);
46 | geojsonLayer = _addGeoJSONLayer(map, geojson, geojsonKeyField, noDataLabel, featuresValues, router);
47 | bounds = geojsonLayer.getBounds();
48 | maxBounds = bounds.pad(0.1);
49 |
50 | map.fitBounds(bounds);
51 | map.setMaxBounds(maxBounds);
52 |
53 | return map;
54 | }
55 |
56 | function _addBaseLayer(map) {
57 | var attribution = '© OpenStreetMap contributors';
58 |
59 | return L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
60 | attribution: attribution
61 | }).addTo(map);
62 | }
63 |
64 | function _addGeoJSONLayer(map, geojson, geojsonKeyField, noDataLabel, featuresValues, router) {
65 | return L.geoJson(geojson, {
66 | style: _style(),
67 | onEachFeature: _onEachFeature(geojsonKeyField, featuresValues, router, noDataLabel)
68 | }).addTo(map);
69 | }
70 |
71 | function _style() {
72 | return defaultStyle;
73 | }
74 |
75 | function _onEachFeature(geojsonKeyField, featuresValues, router, noDataLabel) {
76 | var eventsCallbacks = {
77 | mouseover: _highlightFeature,
78 | mouseout: _resetHighlight
79 | };
80 |
81 | if (router) {
82 | eventsCallbacks.click = router.toggleActive;
83 | }
84 |
85 | return function (feature, layer) {
86 | var elementData = featuresValues[feature.properties[geojsonKeyField]];
87 |
88 | if (router && elementData) {
89 | router.activateIfNeeded(layer);
90 | } else {
91 | layer.setStyle({ className: 'non-clickable' });
92 | }
93 |
94 | if (elementData && elementData.label) {
95 | layer.bindLabel(elementData.label);
96 | layer.on(eventsCallbacks);
97 | }
98 | };
99 | }
100 |
101 | function _formatNumber(num) {
102 | return (num % 1 ? num.toFixed(2) : num);
103 | }
104 |
105 | function _highlightFeature(e) {
106 | var layer = e.target;
107 |
108 | layer.setStyle(highlightStyle);
109 |
110 | if (!L.Browser.ie && !L.Browser.opera) {
111 | layer.bringToFront();
112 | }
113 | }
114 |
115 | function _resetHighlight(e) {
116 | e.target.setStyle(nonHighlightedStyle);
117 | }
118 |
119 | function _router(resourceKeyField, geojsonKeyField, redirectToUrl, filterFields, featuresValues) {
120 | var activeFeatures = _getActiveFeatures(resourceKeyField, featuresValues),
121 | filterFieldsWithResourceKeyField = Array.isArray(filterFields) ? filterFields.slice() : [];
122 |
123 | filterFieldsWithResourceKeyField.push(resourceKeyField);
124 |
125 | function _getActiveFeatures(filterName, features) {
126 | var filters = ckan.views.filters ? ckan.views.filters.get() : {},
127 | activeFeaturesKeys = filters[filterName] || [],
128 | result = [];
129 |
130 | $.each(activeFeaturesKeys, function (i, key) {
131 | if (features[key]) {
132 | result.push(features[key]);
133 | }
134 | });
135 |
136 | return result;
137 | }
138 |
139 | function toggleActive(e) {
140 | var layer = e.target,
141 | id = layer.feature.properties[geojsonKeyField],
142 | feature = featuresValues[id],
143 | index = $.inArray(feature, activeFeatures);
144 |
145 | // Toggle this feature
146 | if (index !== -1) {
147 | activeFeatures.splice(index, 1);
148 | layer.setStyle(defaultStyle);
149 | } else {
150 | activeFeatures.push(feature);
151 | layer.setStyle(activeStyle);
152 | }
153 |
154 | // Update filters
155 | var updatedFilters = _array_unique($.map(activeFeatures, function (feature) {
156 | return $.map(filterFieldsWithResourceKeyField, function (field) {
157 | return feature.data[field];
158 | });
159 | }));
160 |
161 | ckan.views.filters.setAndRedirectTo(resourceKeyField,
162 | updatedFilters,
163 | redirectToUrl);
164 | }
165 |
166 | function activateIfNeeded(layer) {
167 | var id = layer.feature.properties[geojsonKeyField],
168 | feature = featuresValues[id];
169 |
170 | if ($.inArray(feature, activeFeatures) !== -1) {
171 | layer.setStyle(activeStyle);
172 | }
173 | }
174 |
175 | function _array_unique(array) {
176 | var result = [],
177 | i;
178 |
179 | for (i = 0; i < array.length; i++) {
180 | if (result.indexOf(array[i]) === -1) {
181 | result.push(array[i]);
182 | }
183 | }
184 |
185 | return result;
186 | }
187 |
188 | return {
189 | toggleActive: toggleActive,
190 | activateIfNeeded: activateIfNeeded
191 | };
192 | }
193 |
194 | return initialize;
195 | })();
196 |
--------------------------------------------------------------------------------
/ckanext/mapviews/theme/public/resource.config:
--------------------------------------------------------------------------------
1 | [IE conditional]
2 |
3 | lte IE 8 =
4 | vendor/excanvas.js
5 |
6 | [depends]
7 |
8 | main = base/main
9 |
10 | [groups]
11 |
12 | main =
13 | vendor/leaflet.js
14 | vendor/leaflet.css
15 | vendor/leaflet.label.js
16 | vendor/leaflet.label.css
17 | vendor/d3.scale.quantize.js
18 | vendor/backend.ckan.js
19 | vendor/queryStringToJSON.js
20 | ckan_map_modules.js
21 |
22 | navigablemap.js
23 | navigablemap.css
24 |
25 | choroplethmap.js
26 | choroplethmap.css
27 |
--------------------------------------------------------------------------------
/ckanext/mapviews/theme/public/vendor/backend.ckan.js:
--------------------------------------------------------------------------------
1 | this.recline = this.recline || {};
2 | this.recline.Backend = this.recline.Backend || {};
3 | this.recline.Backend.Ckan = this.recline.Backend.Ckan || {};
4 |
5 | (function(my) {
6 | // ## CKAN Backend
7 | //
8 | // This provides connection to the CKAN DataStore (v2)
9 | //
10 | // General notes
11 | //
12 | // We need 2 things to make most requests:
13 | //
14 | // 1. CKAN API endpoint
15 | // 2. ID of resource for which request is being made
16 | //
17 | // There are 2 ways to specify this information.
18 | //
19 | // EITHER (checked in order):
20 | //
21 | // * Every dataset must have an id equal to its resource id on the CKAN instance
22 | // * The dataset has an endpoint attribute pointing to the CKAN API endpoint
23 | //
24 | // OR:
25 | //
26 | // Set the url attribute of the dataset to point to the Resource on the CKAN instance. The endpoint and id will then be automatically computed.
27 |
28 | my.__type__ = 'ckan';
29 |
30 |
31 | // use either jQuery or Underscore Deferred depending on what is available
32 | var underscoreOrJquery = this.jQuery || this._;
33 | var _map = underscoreOrJquery.map;
34 | var _each = function _each(list, iterator) {
35 | if (this.jQuery) {
36 | this.jQuery.each(list, function (index, value) {
37 | iterator(value, index);
38 | });
39 | } else {
40 | this._.each(list, iterator);
41 | }
42 | }
43 | var _deferred = underscoreOrJquery.Deferred;
44 |
45 | // Default CKAN API endpoint used for requests (you can change this but it will affect every request!)
46 | //
47 | // DEPRECATION: this will be removed in v0.7. Please set endpoint attribute on dataset instead
48 | my.API_ENDPOINT = 'http://datahub.io/api';
49 |
50 | // ### fetch
51 | my.fetch = function(dataset) {
52 | var wrapper;
53 | if (dataset.endpoint) {
54 | wrapper = my.DataStore(dataset.endpoint);
55 | } else {
56 | var out = my._parseCkanResourceUrl(dataset.url);
57 | dataset.id = out.resource_id;
58 | wrapper = my.DataStore(out.endpoint);
59 | }
60 | var dfd = new _deferred();
61 | var jqxhr = wrapper.search({resource_id: dataset.id, limit: 0});
62 | jqxhr.done(function(results) {
63 | // map ckan types to our usual types ...
64 | var fields = _map(results.result.fields, function(field) {
65 | field.type = field.type in CKAN_TYPES_MAP ? CKAN_TYPES_MAP[field.type] : field.type;
66 | return field;
67 | });
68 | var out = {
69 | fields: fields,
70 | useMemoryStore: false
71 | };
72 | dfd.resolve(out);
73 | });
74 | return dfd.promise();
75 | };
76 |
77 | // only put in the module namespace so we can access for tests!
78 | my._normalizeQuery = function(queryObj, dataset) {
79 | var actualQuery = {
80 | resource_id: dataset.id,
81 | q: queryObj.q,
82 | filters: {},
83 | limit: queryObj.size || 10,
84 | offset: queryObj.from || 0
85 | };
86 |
87 | if (queryObj.sort && queryObj.sort.length > 0) {
88 | var _tmp = _map(queryObj.sort, function(sortObj) {
89 | return sortObj.field + ' ' + (sortObj.order || '');
90 | });
91 | actualQuery.sort = _tmp.join(',');
92 | }
93 |
94 | if (queryObj.filters && queryObj.filters.length > 0) {
95 | _each(queryObj.filters, function(filter) {
96 | if (filter.type === "term") {
97 | actualQuery.filters[filter.field] = filter.term;
98 | }
99 | });
100 | }
101 | return actualQuery;
102 | };
103 |
104 | my.query = function(queryObj, dataset) {
105 | var wrapper;
106 | if (dataset.endpoint) {
107 | wrapper = my.DataStore(dataset.endpoint);
108 | } else {
109 | var out = my._parseCkanResourceUrl(dataset.url);
110 | dataset.id = out.resource_id;
111 | wrapper = my.DataStore(out.endpoint);
112 | }
113 | var actualQuery = my._normalizeQuery(queryObj, dataset);
114 | var dfd = new _deferred();
115 | var jqxhr = wrapper.search(actualQuery);
116 | jqxhr.done(function(results) {
117 | var out = {
118 | total: results.result.total,
119 | hits: results.result.records
120 | };
121 | dfd.resolve(out);
122 | });
123 | return dfd.promise();
124 | };
125 |
126 | my.search_sql = function(sql, dataset) {
127 | var wrapper;
128 | if (dataset.endpoint) {
129 | wrapper = my.DataStore(dataset.endpoint);
130 | } else {
131 | var out = my._parseCkanResourceUrl(dataset.url);
132 | dataset.id = out.resource_id;
133 | wrapper = my.DataStore(out.endpoint);
134 | }
135 | var dfd = new _deferred();
136 | var jqxhr = wrapper.search_sql(sql);
137 | jqxhr.done(function(results) {
138 | var out = {
139 | hits: results.result.records
140 | };
141 | dfd.resolve(out);
142 | });
143 | return dfd.promise();
144 | }
145 |
146 | // ### DataStore
147 | //
148 | // Simple wrapper around the CKAN DataStore API
149 | //
150 | // @param endpoint: CKAN api endpoint (e.g. http://datahub.io/api)
151 | my.DataStore = function(endpoint) {
152 | var that = {endpoint: endpoint || my.API_ENDPOINT};
153 |
154 | that.search = function(data) {
155 | var searchUrl = that.endpoint + '/3/action/datastore_search';
156 | var jqxhr = jQuery.ajax({
157 | url: searchUrl,
158 | type: 'POST',
159 | data: JSON.stringify(data)
160 | });
161 | return jqxhr;
162 | };
163 |
164 | that.search_sql = function(sql) {
165 | var searchUrl = that.endpoint + '/3/action/datastore_search_sql';
166 | var jqxhr = jQuery.ajax({
167 | url: searchUrl,
168 | type: 'GET',
169 | data: {
170 | sql: sql
171 | }
172 | });
173 | return jqxhr;
174 | }
175 |
176 | return that;
177 | };
178 |
179 | // Parse a normal CKAN resource URL and return API endpoint etc
180 | //
181 | // Normal URL is something like http://demo.ckan.org/dataset/some-dataset/resource/eb23e809-ccbb-4ad1-820a-19586fc4bebd
182 | my._parseCkanResourceUrl = function(url) {
183 | parts = url.split('/');
184 | var len = parts.length;
185 | return {
186 | resource_id: parts[len-1],
187 | endpoint: parts.slice(0,[len-4]).join('/') + '/api'
188 | };
189 | };
190 |
191 | var CKAN_TYPES_MAP = {
192 | 'int4': 'integer',
193 | 'int8': 'integer',
194 | 'float8': 'float'
195 | };
196 |
197 | }(this.recline.Backend.Ckan));
198 |
--------------------------------------------------------------------------------
/ckanext/mapviews/theme/public/vendor/d3.scale.quantize.js:
--------------------------------------------------------------------------------
1 | // Imported from d3js 3.4.1, commit b3d9f5e6
2 |
3 | this.d3 = this.d3 || {};
4 | this.d3.scale = this.d3.scale || {};
5 |
6 | (function () {
7 | function d3_scaleExtent(domain) {
8 | var start = domain[0], stop = domain[domain.length - 1];
9 | return start < stop ? [start, stop] : [stop, start];
10 | }
11 |
12 | function d3_scaleRange(scale) {
13 | return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range());
14 | }
15 |
16 | this.d3.scale.quantize = function() {
17 | return d3_scale_quantize(0, 1, [0, 1]);
18 | };
19 |
20 | function d3_scale_quantize(x0, x1, range) {
21 | var kx, i;
22 |
23 | function scale(x) {
24 | return range[Math.max(0, Math.min(i, Math.floor(kx * (x - x0))))];
25 | }
26 |
27 | function rescale() {
28 | kx = range.length / (x1 - x0);
29 | i = range.length - 1;
30 | return scale;
31 | }
32 |
33 | scale.domain = function(x) {
34 | if (!arguments.length) return [x0, x1];
35 | x0 = +x[0];
36 | x1 = +x[x.length - 1];
37 | return rescale();
38 | };
39 |
40 | scale.range = function(x) {
41 | if (!arguments.length) return range;
42 | range = x;
43 | return rescale();
44 | };
45 |
46 | scale.invertExtent = function(y) {
47 | y = range.indexOf(y);
48 | y = y < 0 ? NaN : y / kx + x0;
49 | return [y, y + 1 / kx];
50 | };
51 |
52 | scale.copy = function() {
53 | return d3_scale_quantize(x0, x1, range); // copy on write
54 | };
55 |
56 | return rescale();
57 | }
58 | })();
59 |
--------------------------------------------------------------------------------
/ckanext/mapviews/theme/public/vendor/excanvas.js:
--------------------------------------------------------------------------------
1 | // Copyright 2006 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 |
16 | // Known Issues:
17 | //
18 | // * Patterns are not implemented.
19 | // * Radial gradient are not implemented. The VML version of these look very
20 | // different from the canvas one.
21 | // * Clipping paths are not implemented.
22 | // * Coordsize. The width and height attribute have higher priority than the
23 | // width and height style values which isn't correct.
24 | // * Painting mode isn't implemented.
25 | // * Canvas width/height should is using content-box by default. IE in
26 | // Quirks mode will draw the canvas using border-box. Either change your
27 | // doctype to HTML5
28 | // (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
29 | // or use Box Sizing Behavior from WebFX
30 | // (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
31 | // * Non uniform scaling does not correctly scale strokes.
32 | // * Optimize. There is always room for speed improvements.
33 |
34 | // Only add this code if we do not already have a canvas implementation
35 | if (!document.createElement('canvas').getContext) {
36 |
37 | (function() {
38 |
39 | // alias some functions to make (compiled) code shorter
40 | var m = Math;
41 | var mr = m.round;
42 | var ms = m.sin;
43 | var mc = m.cos;
44 | var abs = m.abs;
45 | var sqrt = m.sqrt;
46 |
47 | // this is used for sub pixel precision
48 | var Z = 10;
49 | var Z2 = Z / 2;
50 |
51 | /**
52 | * This funtion is assigned to the