Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Critical paths of the application to be tested by E2E tests using the user interface. Include all the steps in one go. If you can use the BDD type AS P, WHEN x, AND y, THEN z
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
This page is a stub
The Wazimap-NG frontend uses a custom component architecture. Over time, this will likely evolve to use a more standard framework such as web components.
All components extend the Component
abstract class which itself extends the Observable
abstract class. Observable
implements the Observer design pattern. Extending it gives a class the ability to register listeners and trigger events [Note, we may move to use native CustomEvents if we adopt the web component architecture]. Events are arbitrary string identifiers although they are often namespaced to the component itself.
For instance, when a dropdown element is selected, a dropdown component might fire the dropdown.selected
event.
Here's an example:
The payload
is any arbitrary object and datatype that the class seeks to send to any listeners on that event.
Calling code might look something like this:
whenever the onElementSelected
method is called, every listener will receive the payload.
Component
extends Observable
and adds a parent-child relationship enabling components built from other components.
Instead of our Dropdown extending Observable
, it now extends Component
.
The calling code would look like this:
Doing this simply stores the parent and child components. In future this may be used to bubble events or in someway communicate across the entire component hierarchy. [Note, the Component class is still being developed].
Apart from extending Component
Wazimap-NG components use a Model View Controller design. The view comprises an HTML fragment, usually created by the web designer. [These are currently embeded in index.html but may in future be separated into individual component html files.]
The model stores the state and associated logic of the component and the controller acts as the glue between model and view, as well as handling events and interactions.
Here is a basic implementation of the DropDown component.
A few points worth noting:
All components receive their parent component as the first argument. In many cases the second argument is the DOM element that contains the view HTML.
By convention, the constructor calls prepareDomElements
which prepares various DOM elements used in the component. Note how each of the lines in prepareDomElements
ends in [0]
. This is due to the fact that JQuery's find
method returns a JQuery object. By convention, DOM objects are used in models, these can easily be converted into JQuery objects using the $ function, e.g. $(this._textArea)
.
A prepareEvents
method is often also used to wire up events from the constructor.
It's is important to note that only the web designer edits the HTML directly. If addition DOM elements are needed, the component will clone existing elements:
While it is tempting to create the Component using a single class, separating the model from the view often results in cleaner, more robust, and easier-to-test code. The model only stores the state of the component and does not interact with either the view or the controller directly.
It is important that the model is agnostic of the controller. In other words, the model should not hold a reference to the controller and should not call its methods. Communication takes place using either events or callbacks. While not mandatory, events are preferred as callbacks can result in slightly less-readable code.
In the case of a DropDown component, the state that needs to be stored includes:
Current options available in the dropdown
The currently selected item
Default text to display if no item is selected.
most of the model class looks like boilerplate, the most interesting method is the setter for currentIndex
Here we check if the index of the selected item has changed, if so, it is update and a DropDownModel.EVENTS.changeValue
is fired. Recall from the DropDownComponent class above, we add a listener to this event:
which then gets the appropriate value from the model:
The current index value on the model is updated when the li element is clicked:
The benefit of using a decoupled MVC framework is that the resulting code is uncomplicated and reusable. Using this structure, it would be possible to have two controllers working with a single model. When a user interacts with the first component, the second one will automatically update.
To ensure that the Model is decoupled from the Controller, it is usually best to separate events into two types, user interaction events and state change events. A user clicking on a box is an example of a user interaction event. They are typically primitive events. The controller notifies the model of the event by calling a method on the model object. This method will likely modify the internal state of the model. Once the state changes, a state change event is fired from the model. An example is the changeValue
event. These events usually operate at a higher semantic level. The controller receives the state change event and updates the view accordingly. In this case, the text shown.
Prefer using the _
prefix as a signifier of a private attribute, e.g.
Use getters and setters
Where possible provide the simplest possible arguments to methods, e.g.
Methods should receive only information that they need. Large application-wide state objects should be avoided.
Wazimap-NG is multi-tenanted. A single backend can host multiple profiles, e.g. and both use the same server and database.
In order to determine which profile to use, the client sends a wm-hostname
header with this api call: /api/v1/profile_by_url/?format=json
this is received by the server which then matches the hostname with available profiles. You can determine which profiles are currently served by a particular backend using the following url: /api/v1/profiles/
. It will return a list of profiles with their configurations, e.g.
In this case, when the server receives wm-hostname
set to geo.vulekamali.gov.za, it returns profile 2. A single profile may match multiple urls.
This is the technical handbook for
This is intended for
Profile administrators - how to configure profiles
Developers - how we work on this project.
Profile administrators should see the .
The Django apps and database models are divided into roughly two groups: models that store data, and models that are used to present information to the end-user.
Only the Geography, Count, and at least one additional column are required. An example table might look as follows:
When this file is uploaded, a new Dataset object is created. Each row is stored in a DatasetData object. A typical DatasetData object might look as follows:
All groups and the Count column are stored in a JSONField.
Another key concept is an Indicator. Indicators represent saved aggregations and filters on a dataset. For example, the above Dataset can be used to create an Indicator containing population per geography disaggregated by gender. The equivalent query in SQL would look something like this:
Similarly, another indicator can be created to return population disaggregated by age.
When a new indicator is created, data from DatasetData is processed to create an IndicatorData object, one per geography. A simplified version of and IndicatorData would like something like this:
Universes represent saved filters on queries and enable the Data Administrator to run a query on a subset of the database. The default Universe is the total of all the distinct observations in a geography (e.g. the total population of the geography). It is possible to create a custom Universe and apply it to an Indicator.
A Universe which creates a filter on gender can enable queries on Female exclusively. PseudoSQL to represent this operation
The Universe filters field contains a dictionary that will be used in a Django ORM filter method. Below is an example filter to extract adults 60 and older.
This filter is then passed to the Django ORM as follows:
ProfileIndicator, ProfileKeyMetrics, and ProfileHighlight. ProfileIndicator is the most commonly used of the three.
ProfileKeyMetrics display only a single value from an Indicator. For instance, the number of youth between 15-24 living in the area.
ProfileHighlights are similar to ProfileKeyMetrics in that they display a single value from an Indicator, but are displayed in the Map View rather than the Rich Data View.
Data models can be found in the datasets app. The central model is Dataset. It represents a dataset that was uploaded by the . Each dataset is associated with a . Data files uploaded to the system are expected to have the following structure:
The actual structure of IndicatorData objects is a little more complicated. More detail can be found here: .
Other noteworthy models are Geography and GeographyHierarchy. These are discussed in more detail here: .
Whereas models in the Datasets app focus on data, Profile App models are for presentation to end-users. The key model is Profile. A profile is a view of the data curated by the . Each profile can be considered to be a complete Wazimap instance. A profile organises tabular data in Categories (IndicatorCategory) and Subcategories (IndicatorSubcategory). This data can be presented using three different models:
ProfileIndicators present Indicators. They provide explanatory text, a custom label, and other attributes that control presentation. They are used in the Rich Data Panel in the form of graphs and the Data Mapper Panel in the form of .
Geography
Group 1
Group 2
...
Group N
Count
geography
Value 1
Value 2
Value N
#observations
Geography
Gender
Age
Count
ZA
Male
20
10
ZA
Male
21
12
ZA
Female
20
15
ZA
Female
21
13
...
WC
Male
20
5
...
Do no change existing class names (unless strictly necessary to implement a request)
If a class name is change, ensure that change is discussed with a developer and documented in the Gitbook.
When making sweeping changes to an already existing component, be sure to duplicate the original component and mark the new component with a versioned name so that any changes do not break the production site. eg. ".map-options" -> ".map-options--v2".
Ensure that any change is documented in the Gitbook.
This page is a stub.
Below is an example of an Indicator Data object
Your code will be reviewed. And you can can take steps to make it easier for the reviewer and yourself.
The review is there to improve the code quality and to ensure architectural direction and correctness.
The big benefit of a review is to help you get better. You will learn from Code Reviews.
Everybody's code can be improved. And your code will get better with reviews. Programming is a skill that improves with training.
Code Reviews help reveal implicit knowledge that is not expressed in code.
Four eyes see more than just two. Even the best testing does not lead to 100% bug-free code. It helps, but Code Reviews are a big part of providing confidence in the code.
Everyone can submit their review and we encourage everyone to do so. But only the PM and the Lead Developers can approve a PR.
Once the PR is approved anyone can merge the PR. And you are encouraged to merge the PR once it is approved.
Create a PR as early as possible, and push frequently.
Use the --allow-empty
flag with git to create an empty commit and push it to create a PR. Use the branch naming as described in the git handbook.
Create a Draft Pull Request to signal, this is a work in progress. Change to Ready to review once you're confident that it's ready to be reviewed.
If you share your progress early on, the reviewer can take a glance at your work early, and help you move in the right direction.
The template is there to help you and the reviewer to make the process as easy and fast as possible.
The description should reference the issue that the PR is addressing. It should include, in your own words, what you are trying to accomplish. How you understand the issue. It will help the issue creator and the reviewer to better understand and address potential misunderstandings early on.
The description should also include your thinking process. How do you approach this issue.
And It should include how you went about testing this locally. For the frontend which buttons to click on which pages, for the backend which API endpoint to call and how.
As well as anything you’d like the reviewers opinion on specifically (eg class names, time complexity of code, etc)
Run codeclimate locally and address issues early on. It will show you issues that will be raised once you push the code.
Make sure you run the tests locally (no need to run E2E) to make sure they pass.
Don't mark it ready too soon, when you still working on it or did not push the latest changes. Take a look if the code diff looks like you would expect.
If you wanna be proactive leave your own review and comment on the lines that you think might be worth explaining to the reviewer. You will also find issues you didn't notice while you were coding when you take a reviewer perspective.
djfldsjfdjfdfkdsl
read through story - what's the point of the change?
read through PR description
what change is the dev trying to make?
what tests does this rely on to keep working? which ones are new?
run the tests - do the tests actually pass?
review the code
Does the code fit in with the context?
Is the code of sufficient quality?
Does anything look scary? Risky?
Does anything not look right?
Does it solve the problem sufficiently generally or is it too specific?
Have we considered the
resolve conflicts if possible
approve
More QA
For all the different sorts of data, will this work?
For all the different sorts of silly things users and admins can do, will this work?
PO review process
Does it address the business problem?
The central unit of analysis in NG is a geography. These represent spatial boundaries which can be associated with data that describes it. For example, a typical geography may represent a Province. Associated data may include demographics in the area, the crime rate, economic activity, education, or any other arbitrary data.
Geographies are related to each other in a tree-like structure (strictly a directed acyclic graph) where each geography in a level is either a root node or has exactly one parent.
The most common hierarchy that we use for the South African context starts with Country at the top. This is followed by Provinces, all of which are non-overlapping and completely contained with Country. Below Province we find District, Municipality, Mainplace and Subplace. Another path may be Municipality -> Ward.
The assumption about the geographical hierarchy is as follows:
It is a directed acyclic graph (DAG)
Child geographies are completely contained by their parent.
Sibling geographies do not overlap.
Each geography has only one parent.
Forks can occur at any part of the hierarchy and are dependent on the current geographical hierarchy in use. For the purpose of illustration, here is a more extensive hierarchy:
Each Geography in a hierarchy should have the follow four fields:
Code - An identifier (not necessarily unique). Where possible, this code should follow an existing standard such as the ISO 3166 for countries.
Boundaries - A spatial boundary definition.
Parent Geography - A reference to its parent, or null if it is a root geography. This reference should use the parent’s code.
Version - For a number of reasons, boundaries may change while codes remain the same. For instance, municipal boundaries in South Africa change every 5 years based on population growth and migration patterns. In some cases, old municipalities may disappear and new ones are created. In other cases minor boundary changes are made to existing geographies. In these cases, it is important to record an identifier that reflects this ‘version’ change, even when the code remains the same.
The approach in NG is to define a geographical hierarchy which then associates the various levels. When a request is received for a particular geography, it sends a list of its children to enable drilling downwards into the hierarchy.
Hierarchies are not linear but are usually considered to be tree-like although strictly directed acyclic graphs. This means that one parent can have many types of levels - e.g. Municipality may have Wards and Mainplaces. These levels overlap. Less common is for a child to have multiple parent levels, e.g. A Ward can be a child of either Metro or a Municipality.
There is no assumption that every parent level will have the same child levels. For example, when exploring a world geography, each country may have different levels specific to it.
Boundaries do not need to cover the entire area of their parent, holes are possible.
Many levels are not compatible with each other as they overlap, Wards and Mainplaces are one such example. There are however instances where they do not overlap such as Districts and Metros. In each case, it is up to the client to decide how these should be rendered.
Treebeard is used to manage the Geography model. Then enables fast querying of hierarchies which is usually hard to do in naive SQL table structures. Spatial data is stored in GeographyBoundary objects. Geographies are serialized to GeoJSON before being served to the client.
Overly detailed geographies can result in large downloads. The GeographyBoundary models uses a CachedMultipolygonField to automatically compress boundaries everytime the model is saved. Downloads are still often large and can be reduced further by converting to TopoJSON. A custom serializer needs to be written to do this. Leaflet will also need a plugin to be able to convert TopoJSON to GeoJSON.
Geographies are displayed on a Leaflet map drawn using Canvas. The client receives a preferred_children
configuration key which provides guidance for which level to display when there are multiple options. For example:
every key represents a level, following by an ordered list of children that it can display. A municipality has two types of children mainplace and ward. According to this configuration, mainplaces should be preferred to wards and will be displayed as default. A toggle exists on the user interface to switch between geographies. If data for a particular level is not available then that should not be shown to the user.
Developer creates a draft PR
Developer completes code and removes the draft label from the PR.
The Trello ticket is moved from In progress to Code Review.
The lead dev merges the latest staging into the PR and reviews. Once the code is ready, the PR is updated with the merged code.
The Trello ticket is then moved into Review (Product Owner)
The Product Owner reviews the Netlify deploy preview. Once the implementation is approved, the Trello ticket moves to To be deployed
The Lead Developer merges the PR into staging
The Product Owner reviews the staging server (on a scheduled, or ad hoc basis)
Once approved, staging is merged into master which is then pushed to the production server.
This process allows to PO to review a feature before it is merged into the staging branch thereby removing the need to rollback a PR that isn't approved by the PO. One gap in this process if a bug enters the codebase during merging into staging. This can occur in the following cases:
PR 1 is approved, PR 2 is approved, PR 1 is merged in staging, PR 2 is merged into staging. The PO has not had the opportunity to review PR 1 and PR 2 at the same time.
Resolution of conflicts.
Stable version: ... | Latest version: https://wazi-rich-data.webflow.io/
Rationale for recent structure update: The latest version of wazimap has been reworked to ensure we can include the following features:
In-page anchor linking and page navigation
A component based approach to populating pages
An improved mobile experience
Filtering of sub-indicator
Every other element is arranged in relation to this panel (using position fixed). This is so that we can maintain in page anchor linking and page navigation.
These items sit on the left hand side of the rich data panel and act as anchors to jump to different content. They also allow users to know which section they are currently in.
Default state: .rich-data-nav__ list will be populated with the .rich-data-nav__item "summary" and the location pin icon. This item links to the top of the rich data panel using the anchor link #top.
Anchor links: Anchor link named need to be added when implementing a component. They should be added to .section-link within .section within .rich-data__content as they are brought into the page. See image below. Position adjustment has been made to this .section-link to ensure users scroll to the correct position, taking into account navigation elements etc.
Call .rich-data-nav__item into .rich-data-nav__list
Once you're strong enough, save the world:
Instructions for merging new webflow exports
Obtain the latest Webflow export from
Run path-to-webflow-export.zip
Check that everything works as expected
Start the local dev server and try it
Run automated tests
Add all the changes to git, commit and push to your branch.
If you have conflicts with webflow-controlled files, these can be fixed as follows:
Fix any conflicts in non-webflow-controlled files as usual.
Import the latest webflow export.
Test the changes, and add all the changes to git and commit as usual.
This only works when the latest webflow export is safe to import, which should always be the case, by versioning components with breaking changes.
The functional styles div resides on the main mapping page (hidden using .hidden). A copy of it can be found here (). This copy needs to be updated as elements are added to the main section or visa versa. Elements can be called as needed into different areas. This approach was aimed at improving loading times and initial states that would have incorrect information.
Adjusted the alignment of the "map-location__tags" to fix the scrolling issue on mobile
Adjusted the width of "location-highlight" to not be a fixed width and cause wrapping issues
Made map location chips panel overflow with items aligned right so that the current filter shows first
Made the .map-bottom-items and map-bottoms-items have a the correct width and padding to make sure its contents does not overlap with other items on small screens. The only thing i couldnt test was whether the zoom buttons are working fine after this change. This seemed like a better way of handling the “map-options” change suggested as this would ensure no items in that parent div overlap rather than just 1.
Positioning of the left side panel toggles is fixed on may side, they are lower and dont hit the map location chips
Changed the position of the geo select and its z index. This should be changed to be more dynamic and responsive to the state of other items at the bottom, but for not it should make the mobile experience better.
Set max width on rich data content as per suggestion.
Added truncation to long filters
Removed generic-modal
Added a element .facility-tooltip__scroll inside .facility-tooltip is--cluster which has overflow: auto to accommodate a long scrolling list.
Ensured that point mapper labels are correctly truncated.
Documentation of indicator version: https://wazimap-ng.webflow.io/documentation#rich-data
Documentation of highlight element: https://wazimap-ng.webflow.io/documentation#utility
Documentation of generic modal: https://wazimap-ng.webflow.io/documentation#modals
Generic Modal:
Documentation of indicator version: https://wazimap-ng.webflow.io/documentation#rich-data
Fixed:
Fixed a small bug where the facilities facet number was being pushed too far left if there was not a download button visible. Now, if the download button is hidden, the facet will align correctly with the right side of the panel. This fix was imlemented by changing the display of the parent from display: grid
to display: flex
Changed:
Added loading state (hidden) for the location facilities title (.location__facilities_title--loading)
Added loading state (hidden) for hide/show facilities button (.location-facilities__trigger--loading)
Added loading state (hidden) for location facility block items (.location-facility__item--loading)
Fixed what colour is automatically inherited for the point mapper dropdown headers so that it is not overridden with grey when using a theme colour.
Documentation: https://wazimap-ng.webflow.io/documentation#tooltips
.facility-tooltip is--cluster
to the styles panel for use when clustering nearby points on the map.
Documentation: https://wazimap-ng.webflow.io/documentation#point-mapper
Added .point-mapper__h1_checkbox
that is hidden by default
Added .point-filters panel
in .map-bottom-items
(hidden by default)
Restricted max height to 90vh on the tutorial modal
Feature preview: https://wazimap-ng.webflow.io/feature-previews/rich-data-nav-fix#top
Changed:
Made the section nav scroll when height is restricted
Changed:
add the following code to the print css:
Added margin-bottom to .profile-indicator__table_inner
to fix spacing issues in the rich-data panel
z-depth: 999 added to items within the .map-bottoms-items
panel
Added .point-legend__remove
to the .point-legend contained within the .map-print
panel and added a .hidden
class to it. It has also been moved further down the DOM so that the .point-legend
that gets copied is the correct one.
Added .hidden
class to .data-category__h1_icon
by default to avoid incorrect icons
Added .slide-info__introduction
to every tutorial slide
There was a miscommunication about what options users preferred and we implemented the incorrect solution. This update implements the map-options filtering approach in the rich-data panel.
Please note that due to slow performance and export speeds, the backlog of old feature previews has been purged. I have created a backup on the webflow side to revert back if this causes any issues. I don't foresee there being a problem. Hopefully the performance issues are noticeable.
Reverted to old implementation of .map-options__filters
Changed .profile-indicator__new-filter
to be a wide button to replicate the map-options button
Changed:
Removed the indicator context tooltip from the bottom right of the map-options panel
Changed the way we implement filter buttons in the map-options panel to bring it in line with the rich-data panel
Added .mapping-options__filter-buttons
with the following buttons:
.mapping-options__new-filter
.mapping-options__remove-filter
(hidden by default)
Added .disabled
state to .map-download
button by default.
Remove this class to set state to enabled.
Removed the .disabled
class from .dropdown-menu__trigger
The disabled class is now only on .mapping-options__filter
Added code to disabled filters that have the disabled class:
Please note that this element was changed as per a feature request: https://app.gitbook.com/@openup/s/wazi-ng-technical/~/drafts/-Md_pTCg5RT4tBx8Ni2q/development-process/changelog#05-21-2021-additive-filtering
Added .map-options__filters_content
back into .map-options__filters
in the .map-options
panel.
.map-options__filters_content
is hidden by default using the .hidden
class
Added code to force truncation on the table cells
Set text within .map-tooltip__geography-chip
to breaking: no-wrap;
Adjusted the svg height value for .location-facility__icon
to 24px to avoid console errors.
Removed wazimap-ng.css script call
renamed the disabled class on the rich-text dropdowns to .disabled
from .is--disabled
Fixed the short truncation on map options title. It now extends the full width of the panel.
Removed capitalization on the map options title.
This feature change was requested because dropdowns were not working due to them being duplicates and not being cloned from the styles panel (similar to how it is handled in the rich-data panel).
The backend will need to clone the .map-options__filters_content
in the styles panel for the map-options panel as needed. The hope is that this will prevent the dropdowns from breaking.
These two implementation of this feature work in a similar ways
.profile-indicator__filter-labels
now contains the column labels for each filter column
Each filter row is now within .profile-indicator__filter-row
and .mapping-options__filter-row
Possible buttons to the right of the filters (add filter and remove filter) are within .profile-indicator__filter-buttons
and .mapping-options__filter-buttons
These buttons are .profile-indicator__remove-filter
and .profile-indicator__new-filter
in the rich data panel
These buttons are .mapping-options__remove-filter
in the mapping options
.profile-indicator__new-filter
is visible by default in the rich data panel
.profile-indicator__remove-filter
is hidden by default in the rich data panel
Added border-bottom: 1px;
and padding-bottom: 12px;
to .profile-indicator__header
This change was requested by Mila directly and does not have an associated trello card or feature preview.
Added class="i18n" to all instances of "Point Mapper" and "locations"
Point mapper panel and toggle tooltips
Locations section in the rich data view (includes the Locations count and the buttons)
Added data-i18n="Point Mapper" to all instances of "Point Mapper"
Added data-i18n="locations" to all instances of "locations"
Changed the wording of "Download all facilities" to "Download all locations" in the rich data view.
Added .location__facilities_download.location__facilities_download-all--header
button to .location__facilities_header
. This button is only visible on desktop. On mobile it hides and the button moves down to below the location cards. This is because downloading takes less priority on mobile devices.
Added .location__facilities_download.location__facilities_download-all--footer
. This button shows on smaller screen sizes.
.location__facilities_header-wrapper
to contain the icon and title. Please note that this may cause integrations with backend data to break.
Added data attributes to the buttons in the chart dropdown menu
The following "data-id" attributes have been added:
data-id="Percentage"
data-id="Value"
data-id="csv"
data-id="excel"
data-id="json"
Added .profile-indicator__table
to the .styles
section
.profile-indicator__table_row.profile-indicator__table_row--header
is the header row for the table and styles the row accordingly.
At the moment, only 3 column tables are supported
.profile-indicator__table_row
's contain .profile-indicator__table_cell
's
The first .profile-indicator__table_cell
in each row has the class .profile-indicator__table_cell--first
applied in order to apply specific styling.
By default the .profile-indicator__table_show-more
is hidden (comprised of the "Load more rows" and "Showing" info).
When the table exceeds a certain number of rows (dev decision), subsequent rows are not shown and can be toggled to show using the load more rows button.
There is no functionality from the Webflow side to handle this "expansion". Please advise if there is anything on my end that would assist in getting this functionality working.
Added print button to .facility-info
.facility-info__print
is now a child of .facility-info__header
Added .facility-info__view-google-map
as a child of .facility-info
.
Component has class .hidden
applied by default and can be removed to show when available
Trello card: https://trello.com/c/qEwQAdRg
Added .rich-data__print
to the rich data panel
Trello cards: https://trello.com/c/iqyaamZj + https://trello.com/c/AllyWK4N
Info text changed from "This location has 0000 locations in 0000 categories." to "This area has 0000 locations in 0000 categories."
Button colour changed to green
Button wording changed from "facilities" to "locations"
Trello card: https://trello.com/c/iqyaamZj
Feature preview: https://wazimap-ng.webflow.io/feature-previews/map-legend-02122021
Made .map-options
and .map-point-legend
children of .map-bottom-items
(new div)
.map-options
and .map-point-legend
have a .hidden
applied by default while waiting for the user to add a point layer or choropleth to the map.
Changes to .map-options
to make it's width consistent with other items of the ui
.map-options
now stretches to the width of its parent .map-bottom-items
width: 95%; max-width: 650px;
Added .point-legend__remove
to the .point-legend
so that the user can remove a point layer from the map without needing to open the point mapper tab
This functionality needs to be added on the backend
If this functionality is not ready for deployment, the .point-legend__remove
item can be hidden before cloning
Trello card: https://trello.com/c/iqyaamZj
Added .map-print
to the .map
div
Added .map-print__point-legend
to .map-print
.map-print
has a .hidden
class by default which is removed when required
.point-legend
contains .point-legend__color
and .point-legend__text
.point-legend__text
has a default state of loading...
.point-legend__color
is fill: rgba(0, 0, 0, 0.06)
by default
.point-legend__color
fill must be updated to the value of the point
.point-legend
can be duplicated for every instance required
This item has been added to map-print to accommodate functionality that was being hacked together previously.
The functionality for this item is the same as the default choropleth legend.
Please contact Matthew if you have any questions about the intention of this component.
Trello card: https://trello.com/c/m1uc8zOh
Added data-element="chart-value-select"
to the hover menu chart value type selector
Added data-element="chart-download-data"
to hover menu download data options
Removed .webflow
from .hover-menu__content
that was not removed before previous export
This class is used to show items when working on them in webflow. This will mean that the hover menu will start off open. With this fix it should return to a default of closed.
Webflow feature preview: https://wazimap-ng.webflow.io/feature-previews/chart-dropdown-percentage-02162021
Added a div for .hover-menu__chart-value
and .hover-menu__download-data
Add .hidden
to remove this group from the menu
Changed the way the .last
class is applied to .hover-menu__content_list-item
to make the active class behave as intended.
Old method was a single class .hover-menu__content_list-item--last
New approach is a combo class added. eg. .hover-menu__content_list-item.last
To make the a list-item "active", just add the active class
eg..hover-menu__content_list-item.last.active
Related Trello card: https://trello.com/c/KqAdJ01z
Webflow feature preview: https://wazimap-ng.webflow.io/feature-previews/map-title-14012021
Changed default state for .map-title
to display: block
Added .hidden
class to .block-title
to hide by default.
Remove .hidden
to show
Related Trello card: https://trello.com/c/0tGnWAkN
Webflow feature preview: https://wazimap-ng.webflow.io/feature-previews/tab-notice
Added .tab-notice
as a child of .main
Added .hidden
class to .tab-notice
to hide by default.
Remove .hidden
to show
Adjust <a>
on .tab-notice__content
to adjust the link of the notice
Adjust .tab-notice__text
to adjust the text within the notice (default is "loading...")
Added map-bottom-items--v2 version with updated functionality and styling
Removed various leaflet styling code from the webflow side
Removed webflow interactions for opening and closing map option panels. Now controlled by devs using hidden classes.
Added .map-credit to .map.
This item is shown by default and the link directs to Wazimap NG product page.
Added .is--shown
and .is--hidden
for the .point-filters_content
element.
Backend should toggle this to open and close the modal.
Backend needs to hide show the arrow icons on the right hand side since this was controlled by interaction before.
The .is--hidden
class is applied as default.
Removed the interaction that used to hide and show this content.
Added .point-filters__no-data
to the .point-filters_content
block for when there is no data available.
Added .map-options__no-data
to the .map-options__filters_content
.map-options__no-data
is hidden by default
Changed class name .hdden
to .hidden
for .mapping-options__add-filter
.
Added transition styling to the v2 data mapper content items and made max-height on closed !important.
Added v2 versions of all clickable triggers and content blocks:
data-category__h1_trigger--v2
data-category__h1_content--v2
data-category__h2_trigger--v2
data-category__h2_content--v2
data-category__h3_trigger--v2
data-category__h3_content--v2
Use the class .is--closed
on the content blocks to toggle them to closed.
The arrow icons in .data-category__h1_trigger--v2 is controlled by adding or removing is--closed to BOTH icons. This will hide the one and show the other.
Changelog:
Rolled back the changes to the tutorial modal. Includes tutorial content.
Changelog:
Replaced all dummy content in the tutorial modal with "Loading..."
Removed the default background image for ."tutorial-slide__image".
Left the same number of slides to not break the JS.
Removed "municipality" dummy text from ".map-tooltip__geography-chip"
Removed "location type" dummy text from ".location-tag__type"
Changelog:
Removed ".bar-chart" from ".indicator-chart" to prevent it from accidentally ending up in production.
Changed all styles panel text to "Loading..." where there was any risk of it ending up in production.
Try to write tests first. You don't need to practice TDD, but if you write a test first it will make it easier for yourself to write the correct code.
Always start a test with red (failing test). If your test is green from the beginning you might not catch potential bugs. Always make sure the tests fail first, even if you wrote the correct code already. Use a different assumption then, to make sure the test fails and you see the actual output. After the test is green, try to refactor your code (make it better).
Don't write your test assertions by using the output of a failing test. You should know the output (assertion) before the test runner tells you the failure.
The ideal set of tests tends to result in ...
small but right number of unit tests.
large number of integration tests.
small number of E2E tests as kind off smoke tests.
Do not only test the obvious path but also the not so obvious. Use parameterized tests in pytest for that.
Test business logic, don't test libraries. We assume library code is tested by the developers of those libraries. If not the integration and e2e tests will catch those. Business logic: All code that is unique, that provides value to the codebase. That drives the app. CRUD is not business logic. State management is usally not business logic.
We want to test code in isolation. It is ok to mock most of the side effects. Although side affects are a smell and should be investigated. If you find yourself mocking a lot of parts or have a hard time writing a test: This is a signal, refactor your code!
Side effects are things like database updates or sending messages.
Integration tests test at least two parts of a unit. It is ok to mock certain boundaries but be precise and document what these boundaries are. Integration tests don't test the UI. Either use an unit test or an E2E test for that.
Typical boundaries:
API calls
User Interface
Third party services
In index.html adding .i18n
class to an element makes the i18n library translate the text of that element on the fly. .18n
class can be added using the js/webflow/import.js
. Using the data-i18n
attribute, we can define which property to be used from the translations config.
In a loop,
We check all the elements that have .18n
class
Search their data-i18n
attribute value in the translations config(i.e "Point Mapper" in the sample code above)
Modify the element's text using the value that corresponds to the key(i.e "Services" in the sample code above)
The modified #test
element will look like :
We use Cypress for GUI tests along with Cucumber
All the GUI tests could be found in __tests__/gui
folder
The test steps are in the .feature
files and the test step definitions are in the .js
files in the folder that shares the same name as the .feature
file
For example the step definitions of facility_modal.feature
need to be in the facility_modal
folder(name of the js files are not important)
We intercept all the requests and respond with local data. The data for each test are kept in json files in the folder of the corresponding test.
Start the project
In a new terminal running yarn cypress:open
will open the cypress window.
In the cypress window you can see all the e2e and gui tests and by clicking on any of it you can start testing
Without opening the cypress window, there are 2 ways to run the gui tests
yarn cypress:gui
runs all the gui tests and saves their result in cypress dashboard
yarn cypress:gui-local
runs all the gui tests without saving anything to the cypress dashboard. While working locally, this script should be used to prevent exceeding dashboard monthly test quota
The credentials for the dashboard could be found in OpenUp general credentials file
Everytime cypress:gui
script is run, the test results, logs and their videos are saved to the dashboard. Github actions runs this script too.
By selecting WazimapNG
in the Projects page, all the latest test runs could be viewed.
By clicking on any of the test runs, you can view
Overview : How many tests are failed / passed / skipped.... etc
Test results : Details, statuses, result logs... etc
Specs : Screenshots, videos... etc
More useful details(Average run duration, Top failures, Slowest tests...) could be found in Analytics menu of the dashboard.
Removed color: inherit styling
from h1__point-mapper_trigger
which was causing there to be an issue with the default colour of icons in the point mapper
Fixed truncation on
Fixed truncation on point mapper themes and cat
This marker is based on the look of the google pin style and is able to accommodate icons and text abbreviations easily.
At all zoom levels, this pin should be 21px wide.
Only adjust the colour of the element with the specified colour (#4693EF)
The stroke around the edge is defined as 8% black and does not need to be changed
Text abbreviations within the marker should be 8.5px
This marker is based on the look of the google pin style and is able to accommodate icons and text abbreviations easily.
At all zoom levels, this pin should be 21px wide.
Only adjust the colour of the element with the specified colour (#4693EF)
Text abbreviations within the marker should be 8px
Guidance for decision making around choosing the correct icon and using it in a way that makes sense.
When choosing an icon, the following things should be taken into consideration.
The icon should be generalized and not reliant on knowledge of a specific language or region.
The icon should follow established norms for icons of it's type in similar applications.
When the choice deviates from a norm, it should only be done for very good reasons.
Icons should be made distinct enough from others in the UI
Icons should generally have the following size properties:
Icons should be no larger than 24px wide and 24px tall.
Icons that are smaller than 24px in size, should fit within a bounding box that is 24px.
This is generally the approach taken for icon sets like Material and FontAwesome.
Visual weight refers to the amount of prevalence an icon has when looking at it's role in the UI combined with how important the action that icon represents is to the user. When deciding on the visual weight of an icon, the following considerations need to be made.
How unique is the icon in the UI
If there is only 1 instance of this icon in the entire UI, it generally denotes it requiring a higher visual weight. eg. search, data mapper, rich data view.
How important is the action the icon represents
If the action has high importance it should generally be given more weight in the UI, either through, size, colour or containing element settings (eg. size of button, space around button etc.).
NG Proposal 2
Representing geographical hierarchies to the user.
Proposed by: Adi Eyal
Date: 2020-09-02
Status: Work in progress
Read about Geography Hierarchies here.
The non-linear structure of Geography Hierarchies poses a challenge to presenting information on a map. When in the context of a Municipality, the user will either want to see Wards or Mainplaces. These cannot be shown simultaneously as they overlap with each other. A graphical toggle would be required to change between these two levels.
Forks can occur at any part of the hierarchy and are dependent on the current geographical hierarchy in use. For the purpose of illustration, here is a more extensive hierarchy:
In this document we discuss the most appropriate user interface to navigate this hierarchy.
At the time of writing, this is how Wazimap depicts the Wards in the Cape Town metro.
Cape Town Metro with Wards displayed
Cape Town Metro with Mainplaces displayed
A typical user requirement would be to switch between Wards and Mainplaces.
The first approach to address this is to use a contextual toggle. The select box on the right is populated with the child levels available at the current geography. When the current geography changes, the options change with it.
This approach requires minor UI changes and can be implemented relatively quickly. Users may however may not necessarily understand the geographical hierarchy and contextual changes may be hard to follow. For example, it may not be obvious that Subplaces are not available below Wards.
Login to the heroku dashboard
Go to the wazimap pipeline
Click "Create review app" for your pull request
Find the URL to the deployed review app in the pull request or by clicking "View app" in heroku.
Migrations and demodata are only loaded when the app is created. If you need to recreate the database, destroy the app and recreate it.
This is probably not helpful any more now that real review apps work again, but we're leaving this here for now in case it's needed.
We were creating "review apps" in the staging "stage" of the wazimap pipeline in heroku while heroku review apps were not working due to the exfiltration of github oauth keys.
We name apps according to the pull request number, e.g. wazimap-pr-1234 for PR #1234
heroku login
In the project directory (to update your project git remotes):
heroku apps:create wazimap-pr-1234 --remote heroku-pr-1234
heroku pipelines:add wazimap --app wazimap-pr-1234 --stage staging
Login into Heroku Web
Go to wazimap pipeline
heroku stack:set container --app wazimap-pr-1234
Adding Addons:
heroku addons:add heroku-postgresql:hobby-dev --app wazimap-pr-1234
heroku addons:add heroku-redis:hobby-dev --app wazimap-pr-1234
Adding Config vars:
heroku config:set DJANGO_SECRET_KEY=3423424 AWS_ACCESS_KEY_ID=xxx AWS_SECRET_ACCESS_KEY=xxx AWS_STORAGE_BUCKET_NAME=xxx AWS_S3_REGION_NAME=xxx --app wazimap-pr-1234
git add and commit everything you want to deploy
git push heroku-pr-1234 BranchName:master
Start Worker dynos
Via CLI:
heroku ps:scale worker=1 --app
wazimap-pr-1234
Via GUI:
heroku run python3 manage.py migrate --app
wazimap-pr-1234
heroku run python3 manage.py loaddata demodata.json --app
wazimap-pr-1234
heroku run python3 manage.py createsuperuser --app
wazimap-pr-1234
if you want to create new supruser
Check configs for review app via cli
heroku config --app
wazimap-pr-1234
Define region while creating app
heroku apps:create
wazimap-pr-1234 --remote staging --region eu
Checking logs
worker logs : heroku logs --dyno=worker --app
wazimap-pr-1234 --tail
web logs: heroku logs --dyno=web --app
wazimap-pr-1234 --tail
heroku apps:destroy wazimap-pr-1234
Copy link for frontend review app ex:
Add it to the configuration in profiles for the backend review app and save
Go to Ui deploy preview add ?dev-tools=true to enable dev tools and add deploy-preview-542--wazimap-staging.netlify.app
to Hostname and backend review app url to Api url https://wazireview.herokuapp.com
and enter
After opening wazimap pipeline there is an option to add new staging app
Open the newly created app and go to deploy
Copy command to add remote for existing repo and paste it to your command line inside wazi backend repo to add this app as heroku remote
Go to overview on app in heroku dev center and there will option to see dyno formation click on configure dyno link on right hand side
Switch on the dyno to run Qcluster (click on edit and turn the toggle on)
Copy link for frontend review app ex:
Add it to the configuration in profiles for the backend review app and save
Go to Ui deploy preview add ?dev-tools=true to enable dev tools and add deploy-preview-542--wazimap-staging.netlify.app
to Hostname and backend review app url to Api url https://wazireview.herokuapp.com
and enter
Proposed by: Adi Eyal
Date: 2020-09-14
Status: Work in progress
Wazimap-NG Indicators contain different types of numbers that need to be formatted. Some Profile Administrators may want to round floating point numbers to 1 decimal place, while others prefer 2. Some numbers are percentages and should be displayed as such. Currently the Wazimap front-end does not receive sufficient information from the API to help determine the type of number received nor is it possible for administrators to configure the formatting of that number. This NGP discusses a few approaches to resolving this issue.
Each Indicator may require its own specialised formatting. To achieve
Whereas indicators can now be mapped using a choropleth. Superindicators cannot since a choropleth can only display one Count variable at a time. This can be addressed by another select box allowing the user to choose the Count variable of interest or mapping multi-count variables could be prevented entirely.
Whereas indicators can now be mapped using a choropleth. Superindicators cannot since a choropleth can only display one Count variable at a time. This can be addressed by another select box allowing the user to choose the Count variable of interest or mapping multi-count variables could be prevented entirely
Proposed by: JD Bothma
Date: 2022-04-21
Status: Spike probable solution
We would like to be able to serve open graph metadata specific to each wazimap NG profile. Wazimap profiles are designed as stand-alone sites or sub-sites often set up as subdomains of other sites. Open graph metadata can significantly increase the chance of someone clicking a link to a wazimap profile found in social media or search engines. While Google takes Open Graph metadata updated client-side using javascript into account, we are not aware of social media or other search engines that do that. To reach any of these audiences except Google, we need to serve open graph metadata specific to a profile server-side.
The fields that would benefit from being served this way are
Page title - often used as an emphasised heading
Image - very effective at being eye-catching and drawing interest from potential users
Description - this can both convince users that something might be worth clicking, and also set their expectations to reduce surprise and this bouncing from the site.
Favicon - some tools like bookmark and link aggregators and search engines also use this. While browsers support dynamically-set favicons, it's not clear how many other tools do.
Open Graph Metadata also help SEO. Additional content that can help SEO is the introductory content currently located on the profile landing pages.
Landing pages
Most sites have a landing page made in webflow or Wordpress. These can easily serve profile-specific branded metadata to accomplish most of the above. This then links to the relevant wazimap profile with a clear call to action. This content can be quite effective for SEO.
We would like to move these into wazimap NG to be able to maintain that content from one interface, reduce the jumps users have to make, and reduce the hosting costs of a wazimap profile. If we move these into wazimap NG, we lose this ability to serve the open graph metadata need.
Hardcoded metadata
The open graph metadata is currently the same for all wazimap profiles, hardcoded on the webflow side and imported to the frontend app from there. This can be updated both in webflow and overridden in the import script in the frontend app.
Updating this to something that is at least sensible as generic content is a high priority until this content is dynamic and profile-specific.
In ascending order of value:
Better hardcoded metadata (get rid of the webflow favicon, ensuring webflow changes don't introduce odd things like "2021" in the title
Profile-specific metadata, e.g. "Youth Explorer" or "Vulekamali Geospatial data viewer" in the title, branding and screenshot in the image, etc.
Profile- and geography-specific metadata, e.g. "Tswhane - Youth Explorer" or "Eastern Cape - Vulekamali Geospatial".
Specific indicator and geography, e.g. "Multidimensional youth poverty - Tshwane - Youth Explorer" in the title, perhaps some or all of the indicator description in the open graph metadata
dynamic image, e.g. the selected geography on the map, perhaps dynamically-branded; perhaps a choropleth or rich data view chart image for the selected geography and indicator.
We're aiming around level 2 for now.
Level 3 depends on changing from fragment identifier (#geo:CPT) to querystring (?geo=CPT)
The frontend would be deployed as it currently is, but Javascript is executed server-side to inspect the request, fetch the profile-specific data via the API, templates the metadata into the HTML page, and serves the response.
https://www.npmjs.com/package/mustache-express
A prerequisite for next.js would be replacing webflow or horrid hacks to import our webflow export as a custom "Document".
a lot of people are doing this these days
the frontend remains rather decoupled from the backend
Very little changes in terms of developing on the frontend - it remains just a yarn start
kind of process
Do we have experience of serving stuff with node.js in prod?
Same as above, but on a (not so?)-static hosting platform like netlify or Cloudflare apps instead of a dokku server.
Each profile hostname has to be added as a custom domain in Netlify or whatever the platform is. Netlify doesn't support more than 100 custom domains (it's not clear what the technical limit is) on an app and we have already had issues with their restriction of the apex domain being allowed only on one netlify team without manual intervention from their support. See NGP7 - Wazimap profile domain management.
Index.html becomes a django template. Django templates in the data before serving /
The backend will need to respond to requests for all profile hostnames, affecting how the backend infra gets configured.
We have lots of experience of this
no additional api requests to serve / - it can access django models immediately.
frontend dev can easily get coupled with running the backend, needing at least a basic backend setup to run, even if subsequent requests to the api go to another backend (e.g. prod). But this can add confusion and/or complexity (it is possible to keep them separate. Just hard.)
Proposal:
Quick win with minimal interruption:
Use minimal server-side templating like express-handlebars to query and render profile details
enables all achievement levels but certainly 2-3
eventually consider full-blown server-side rendering next.js style
enables pre-rendering javascript-drawn content, skipping several data roundtrips
watch out: next.js is possibly not the right tool - apparently more page-content oriented than SPA.
And explanation of the items in the Template
Use the Issue Number and the issue title for the title of your PR.
Add a good description here. Use the issue description to inform your own understanding of this issue. See How to prepare a Pull Request
Create a link to the related Trello Card here. So that it can be easily referenced.
Explain how you tested the changes locally and made sure that the PR does what you intend to. For example if you working on Frontend Code describe which page you should open and what steps to take to see the changes you made. For the backend, which endpoint to hit with which params, best to provide an example like a curl command.
If on FE, upload the desired outcome (design) and create a Screenshot, once you complete your work. Even better create a gif which shows how it works.
Create a detailed changelog, if possible. This changelog can be informed by the commits you create (reference the commit part in the git handbook). They should contain parts that you changed. This can be a bullet list, no need to create full sentences. Should be functions, classes, API Endpoints, etc.
Added What has been added.
Updated What has been updated.
Removed What has been removed.
This part should be checked off accordingly, when you're ready.
Proposed by: Adi Eyal
Date: 2020-08-31
Status: Proposed
The data model that NG has been built around is the concept of a universe, i.e. the total number of people in a particular context. This universe can then be disaggregated by a number of attributes. For instance the country population is a universe that is divided into male and female, i.e.:
# male + # female = total population
Similarly:
# Left-handers in WC + # Right-handers in WC = total population of WC
The input file might look like this:
Geography
Preferred hand
Count
Western Cape
Left
30
Western Cape
Right
50
Eastern Cape
Left
80
Eastern Cape
Right
95
Table 1
That works well for census data where you are disaggregating a universe. It falls short when you would like to compare two unrelated datasets side-by-side. As an example:
Geography
Access to drinking water
Year
Count
Western Cape
Have access to drinking water
2016
30,000
Western Cape
Have access to drinking water
2017
35,000
Table 2
In this case, we cannot sum the two rows to get the universe, i.e. there aren’t 30,000 + 35,000 = 65,000 people with access to water in the Western Cape. We do however ever want to be able to compare these two figures.
The problem does not only apply to time-based data, e.g.
Geography
Age Group
Employment Status
Count
Western Cape
15-24
Employed
40,000
Western Cape
12-24
Unemployed
60,000
Western Cape
15-35
Employed
80,000
Western Cape
15-35
Unemployed
120,000
Table 3
In this case we have two universes which overlap but we still want to be able to compare them:
An underlying assumption of the dataset model is that there is only one count field in every file. We could change this assumption by allowing multiple counts, effectively pivoting our table.
Geography
Access to drinking water
Year - 2016
Year - 2017
Western Cape
Have access to drinking water
30,000
35,000
Table 4
This solution will require significant changes to the following components:
Data Import
Variable Creation (data is aggregated at this level)
API
Front-end data model
Front-end visualisations
We would also need to decide how the system would identify count columns. The current convention is to match by name. We could use a similar approach by requiring a standard prefix, e.g. Count: 2016. Alternatively, the administrator could identify these columns once the data has been uploaded.
An alternative approach would be to designate a pivot column, e.g. Year.
This is a robust approach that ensures compatibility between Count columns (compared with Solution 2 below). Every column available for the first Count column will be available to the second one.
A significant amount of effort is required to make this change.
It limits comparisons of datasets to those that were included in the initial upload. If data from a new year becomes available, it is not possible to include it.
Depending on implementation, may place an additional burden on the Data Administrator requiring a special naming of columns in the spreadsheet to be uploaded.
Using this approach we add the concept of a super indicator which ties together two or more indicators together. For instance, table 3 becomes two separate indicators:
Geography
Age Group
Employment Status
Count
Western Cape
15-24
Employed
40,000
Western Cape
12-24
Unemployed
60,000
Table 5
Geography
Age Group
Employment Status
Count
Western Cape
15-35
Employed
80,000
Western Cape
15-35
Unemployed
120,000
Table 6 These are then associated in the backend.
This solution would affect the following components:
A new database model would be required
The API will need to be changed
Front-end data model
Front-end visualisations
Overall fewer changes are necessary to implement this feature
Data Administrators are able to associate arbitrary indicators without pre-planning.
An almost identical workflow is used as the current approach.
It is possible to bind two, completely unrelated or incompatible indicators. For instance, the number of bankruptcies vs Child pregnancies.
Less extreme but equally problematic is two related datasets with different groups, e.g. 2016 Matric passes disaggregated by gender vs 2017 Matric passes without disaggregation. In this case, we will need to decide how this will be displayed on the frontend, especially in graph filters
In the case of two incompatible datasets, we need to decide whether filters are available for missing groups.
Whereas indicators can now be mapped using a choropleth. Superindicators cannot since a choropleth can only display one Count variable at a time. This can be addressed by another select box allowing the user to choose the Count variable of interest or mapping multi-count variables could be prevented entirely.
Another approach to addressing this issue is to recognise that the cause of this problem is the concept of a non-overlapping universe introduce by the Dataset model. This approach does not always make sense as in the examples above. To create an Indicator, a background process is fired that groups DatasetData objects appropriately. In cases like the ones described here, it might be easier to create the indicator directly and avoid datasets entirely.
When creating a new indicator, the admin is asked whether it should be created from a dataset (the current process) or whether it should be uploaded from a file. This latter approach would simply create the relevant IndicatorData directly. Since an indicator must link to a dataset, one can be created automatically. Creating new indicators from this dataset should not be allowed however.
This is by far the easiest approach to implement. Very little needs to change with the exception of the file upload mechanism when creating a new indicator. Once the IndicatorData objects are created, downstream users of this data remain unchanged.
It also allows flexibility for data administrators to shape data without too many constraints on the format.
Data re-use is limited. Datasets provide opportunities to create multiple indicators. The superindicator concept allows even more mixing and matching of indicators.
This problem was finally addressed by marking columns as aggregatable or not aggregateable. If a column is not aggregateable, different values in that column need to be shown separately. This can be implemented as a dropdown filter, e.g. you always need to choose a year. Alternatively, a grouped bar chart could be used, e.g. 2016 and 2017 bars.
The change also involved removing all aggregation from the backend and sending raw data to the frontend.
Proposed by: JD Bothma
Date: April 2022
Status: Spike probable solution
As wazimap scales in the number of profiles it hosts, we will need to serve on more and more hostnames. Each hostname we serve has to be valid for the TLS certificate, and traffic for that hostname has to be routed to the Wazimap NG frontend. That imposes the following requirements for adding a new profile:
Come up with an appropriate hostname
currently: usually a geo. prefix to the client's domain
also: a sandbox-geo. prefix for a second profile to use as sandbox
often: initially just a projectname.wazimap.openup.org.za subdomain to get things moving quickly
Relate the hostname to the relevant profile in Wazimap admin
A Profile Admin level user can do this. It's currently JSON but can be made simpler.
Configure the frontend web server to serve that hostname
currently: add it as a custom domain in netlify
Provision a new TLS certificate which includes the new domain as a Subject Alternate Name
currently: Click renew certificate, then verify DNS, then renew certificate, perhaps a number of times, until it's happy - in netlify
A networking-aware developer has to do the DNS and custom domain configuration
Stale domains can break certificate renewal
Netlify has a limit to the number of custom domains allowed - they have warned us not to add more than 100
The Certificate is growing bigger and bigger. We aren't sure of the consequences of this.
We are aiming for 40 profile subscriptions by the end of 2022-2023 financial year.
Pending: Feedback from current profile owners on the importance of custom domains for their profiles.
Based on the current profiles, we guess that at least 50% would consider custom domains important.
Certificates per Registered Domain (50 per week) - Only really relevant if we try to have a distinct certificate per wazimap.co.za subdomain instead of a wildcard certificate for that domain. Renewal could be staggered. This would probably happen naturally by renewing daily a month before expiry as is currently done.
Names per Certificate (100) - Relevant if all domains are on a single certificate. This (all domains added as Subject Alternative Name values on a single certificate for the app) is the usual approach dokku-letsencrypt and netlify uses for domains added to the same app.
Failed Validation limit of 5 failures per account, per hostname, per hour. - Relevant if one certificate is used for multiple domains, and DNS for one of the domains is not resolving to us yet. We'd need to be careful about accidental or intentional denial of service here.
Wildcard domains are supported on Pro, but not at the same time as custom domains
TLS Certificates can only be handled by Netlify if Netlify DNS is used for the wildcard cart
Only 100 custom domains are supported on a site. (Possibly due to letsencrypt Names per Cert limit)
Youth explorer very much is an established brand in their circles.
Could you please also let us know what the technical costs are for keeping a custom domain.
whowhatwhere.org.za
We are fine with http://whowhatwhere.wazimap.co.za/ being the domain
Power BI
Carto
Tableau
Requirements:
Once-off wildcard DNS for the base domain, e.g. *.wazimap.co.za
Supported by netlify, dokku
2-monthly wildcard TLS certificate for the wildcard domain
Supported by netlify, dokku
Wildcard reverse proxying to the frontend web server
Supported by netlify, dokku
Requirements:
Same as solution 1 for wildcard DNS/cert/proxying
The solution 1 requirements for about 5 custom domains
Supported by dokku
Not supported by netlify ("You can’t use domain aliases on a site with wildcard subdomains enabled")
Approach
DNS
Wildcard DNS for subdomains of our base domain
Any DNS CNAME to to our frontend web server
Virtual hosting (reverse proxy or static server)
Subdomains are handled automatically by dokku/nginx wicard domain
Custom domains: ??? manually-added or automated?
TLS certificate
dokku letsencrypt - wildcard + up to 99 custom domains via dokku letsencrypt (100 30-char domains like e.g youthexplorer.wazimap.co.za is 3kb baggage per new TLS connection)
alternative to dokku letsencrypt: certbot + vhost per custom domain + dokku app listening on a host port for non-dokku reverse proxy
Adding/removing a domain manually would entail
Add the domain to the profile in Admin
Add CNAME record pointing to the server, or ask the client to do so
SSH to the server
Add domain to dokku/nginx vhost
Renew TLS certificate
For 20 custom domains, we might have to do this on average 30 times or just over once every 2 weeks.
Beware cert renewal errors.
Can one hostname break renewal for all or is that one skipped?
We can monitor expiry.
We can monitor the renewal cron output.
-> Urgent manual devops action until we automate.
Same as Solution 3 but instead of adding/removing domains manually by SSHing to the server, we automate that, triggered by domain modifications in admin.
Approach:
Add the domain to the profile in Admin
Add CNAME record pointing to the server, or ask the client to do so
...Automation to add/remove domain config to server
...Automation to renew certificate
...Automation to let admin know it's all up and running or feed back errors
Try option 3 until the effort of maintaining custom domains manually and the value to be gained by automating is worth the effort of automating handling custom domains.
Proposed by: JD Bothma
Date: 2022-04-21
Status: Spike probable solution
Using webflow as the frontend framework makes concurrent frontend development error-prone and requires complicated synchronisation between team members to plan changes. It also often requires multiple round-trips between Matt making changes in Webflow, frontend devs trying to use those changes, Matt then having to make tweaks, until it is done. Each change requires a webflow export, sharing a big zip file, requiring an import which may bring surprise changes in the diff introduced by webflow, adding cognitive load to the code review.
The plan thus far has been to introduce React for the next interactive component we need to modify or introduce.
We have added a small non-interactive component successfully using custom Javascript creating requisite markup, and custom CSS in a project-specific CSS bundle.
We are now looking at introducing a frontend framework, or the frontend libraries, needed to eventually be completely free from Webflow in our frontend dev process.
(n) means there can be multiple instances
Page header
Logo
Search box
Tutorial button
Panels (on the left)
Rich Data View
Point data summary
Total point and profile (point) collection count
Download all button
Show services button
Point theme card (n)
Profile (point) collection item (n)
Category (n)
Subcategory (n)
Indicator (n)
Filters Section
Chart
Description
Source
Table
Point Mapper
Point theme (n)
Enable-all toggle-switch
Profile (point) collection button (n)
Data Mapper
Category (n)
Subcategory (n)
Subindicator (n)
Map
Map Download button
"Map Chip" (Non-modal fixed dialogue for data mapper controls)
"Point filters dialogue"
Breadcrumbs / Profile highlights container
Feedback button
Web components
Maintenance mode, referring to Lit.
IMPORTANT: Material Web is a work in progress and subject to major changes until 1.0 release.
Implement the search/select-one component and style accordingly
Implement the filter-reset snackbar
Do either of them risk breaking styling of existing parts?
Does it look like both approaches to styling scale nicely - Is consistent styling convenient enough with both options as we eventually replace all components on the site?
Interactive/reactive markup library: React
Component library: To be determined - see spike above
Styling/CSS: To be determined - see spike above
State management: custom event system and controller until enough of UI is react that changing to something standard is easier than maintaining the custom approach
Framework: Nothing more than what we have for now. Revisit things like next.js but for SPAs when most of the webflow dependency is gone
Proposed by: Adi Eyal
Date: 2020-09-06
Status: Work in progress
Read about Geography Hierarchies here.
The current architecture considers levels to belong to only a single hierarchy. For instance Wards version 2016, belongs to the 2016 Boundaries Hierarchy. There are occasions where this level might be relevant to another hierarchy. South African Provinces are useful in both South African hierarchies as well as a World hierarchy. For this level to belong to both hierarchies, another version of the boundaries must be uploaded.
This limitation is inconvenient and creates database bloat by storing a second copy of the boundaries.
Sharing datasets across hierarchies
The limitation above also has an impact on datasets associated with these geographies. Since datasets are tied to a particular hierarchy, it is impossible to use the same data with another hierarchy, even if they share boundaries.
Children can only have one parent
Child geographies explicitly identify their parent when uploaded. This prevents hierarchies where a child can be reached through different paths.
Geographies belong to a particular level and version. There are cases where the same term may be used for different levels causing confusion. For instance, a District in South African may not be comparable to a district in another country which may fit in a different position in the hierarchy.
You need to know which version of ancestors to show to potentially navigate away from the selected geography.
In the image below, the hexagons should be children of the 2016 Mangaung but are incorrectly added as children of the smaller 2011 Mangaung. Hexagons are technically not part of the municipal demarcation, but in one demarcation version, hexagon a might be best presented a child of one municipality, and in another demarcation version, it would be a child of another.
This raises the question of which demarcation version to use to determine the ancestors for the breadcrumb and ancestor siblings for the map navigation.
If there is only one hierarchy and only part of it is relevant, e.g. Gauteng for GCRO, then search should be restricted to only Gauteng, and only the boundary types that are used on the GCRO profile.
Decoupling levels from hierarchies would add flexibility when constructing hierarchies. Geographies are not associated with any particular hierarchy when they are uploaded. An admin is then able to create a hierarchy by choosing which levels to include in it. Geographies are differentiated by the combination of their namespace and code, whereas child geographies refer to their parents only by code. To make this concrete, a useful example is to consider that South Africa changes municipal boundaries every five years. The current boundaries were drawn up in 2016 and the previous ones in 2011. I can upload 2011 municipalities and 2016 municipalities. Many of the municipalities in each geography set may have the same code, e.g. City of Cape Town has the code CPT in both sets. We can uniquely identify a particular boundary by prefixing it with its namespace.
In both cases, CPT points to WC as its parent. The namespace is not explicit. This allows us to create a new Geography Hierarchy that combines 2011 Municipalities with 2016 Provinces. We simply add these sets of Geographies to the same Geography Hierarchy. A 2011 Geography will identify any available parent Geography which has the correct code. In this case, 2011 CPT will link to 2016 WC.
The Geography Hierarchy itself does not actually encode the hierarchical relationship between Geographies. This is already encoded in the Geography itself.
Implementing this change would require a change in the database structure of how hierarchies are stored in the database. Treebeard is currently being used to manage hierarchies using Materialized Path Trees. These data structures effectively hard-code geography hierarchies. Materialized Path Trees are useful for efficient multi-generational queries, such as finding all descendants of a selected geography. No compelling use case has been presented that requires these types of queries. A simplified table structure may be appropriate in this case.
This proposal not only addresses the current limitation of not being able to share Geographies across Geography Hierarchies, but we also benefit from not requiring to duplicate data. Data is associated with a Geography, irrespective of the hierarchy it is in. A minor modification is for new data uploads to be assigned a namespace since data files would typically not be explicit about the namespace.
Unfortunately, this proposal cannot accommodate a Geography with multiple parents. A modification to this design may be able to address this issue as well. The source of this limitation is how spatial data is imported into the database. Each Geography already names the code of a single parent. A possible direction to explore is creating a many-to-many table when Geographies are loaded into the system. Using this table, it would be possible to query the children of a particular Geography, and not only child Geographies that have a given Geography as a parent.
.
Quick tutorial on creating a new profile.
Here is a quick tutorial that explains how to create a new profile. There are two parts, a) the frontend site and b) the backend profile.
Cloudflare manages the DNS records for openup.org.za. If we will be using an openup.org.za subdomain, add a record there. On the Cloudflare dash, find the domain you would like to use and add a CNAME record pointing to inspiring-dubinsky-c19ab4.netlify.com
Create a new domain name and point it to the Wazimap-NG server. In this case I am creating a new CNAME DNS entry called covid.openup.org.za that redirects to Netlify which is currently hosting the Wazi-NG frontend.
Add the new domain name to the Netlify configuration. Find the configuration on this page.
After adding the alias the SSL/TLS certificate should renew to be valid for the new hostname.
Look for the lock icon when you visit the new hostname
Check the status at the HTTPS section:
If after 10 minutes it hasn't renewed, click Renew Certificate and check in another 10 minutes.
The backend is also simple to create. Login at https://production.wazimap-ng.openup.org.za/admin and add a new profile https://production.wazimap-ng.openup.org.za/admin/profile/profile/add/.
Provide a useful name for the profile - this one will be called Covid. Choose a Geography Hierarchy. I am choosing the pre-installed 2016 boundaries with wards hierarchy. This uses boundaries used in the 2016 South African municipal elections.
Assign the new domain name created above in the config field
Here is my completed profile screen
Save and you're done.
Visit http://covid.openup.org.za/ to see if everything is setup. It should look like the image below.
A Wazimap-NG instance can be set to private, which means that a user will require credentials to login and view the data.
One can create a non-admin account as follows:
Navigate to the user creation page:
Select a username and password (this will be a temporary password or can be generated from if you'd prefer to do so)
Press Save
Copy the link to send to the user to change their password (see image below). NOTE: This link is specific to the user and should not be shared with anyone but that user.
Add details under Personal Information (optional)
Under Permissions, DO NOT assign Staff or Superuser status!
In the Groups selector, add the groups applicable for the user. In the screenshot below, the user has been given access to the SIOC dashboard and SIOC dashboard SANDBOX groups. This means that the user will only be able to log into those instances of Wazimap-NG.
7. When done, press Save
Guides for loading data can be found here:
Below are some direct links:
Creating datasets:
Creating indicators:
Creating universes:
Loading point collections:
Admin users are responsible for loading data and managing profiles. Be careful when giving someone admin access. Wazimap does not currently have an UNDO feature which means that any accidental deletion may require a database restore in order to recover.
Create a new admin account as follows:
Navigate to the user creation page:
2. Select a username and secure password (important for the production site)
3. Press Save and continue editing (don't only press save)
4. Add details under Personal info (optional)
5. Under Permissions, select Staff status
6. Don't provide superuser access unless you know what you're doing.
7. Under groups, select Data Admin and Profile Admin
8. In the Available groups selector, also select all of the profiles that that administrator should have access to.
9. Push Save
If a geography label needs to change, e.g. Districts => Districts and Metros, follow the following steps:
Geography.objects.filter(level="district", version="2011 Boundaries").update(level="Districts and Metros")
Make sure you select the appropriate version
Change the preferred_children object in the Profile configuration json for each profile that uses that geography hierarchy, e.g.:
3. from a python terminal run the following: from django.core.cache import cache; cache.clear()
Done
Here is a quick tutorial for loading up new geography files into Wazimap. To start you need your boundaries in ESRI Shapefile format. In this example, I am going to upload 2011 Wards.
I downloaded the shapefiles from the Municipal Demarcation Board website and loaded them up into QGIS. Here is what the wards look like:

Each layer needs to have four fields:
Name - The common name of the boundary, e.g. Western. Cape
Code - A unique identifier, this can be anything but it is best if it is a commonly used identifier such as ISO3166 for countries
Parent_cod (this is due to the field length limitation in shapefiles) - The code of the parent Geography. This field can be blank if it is a top-level geography such as a country. ESRI shapefiles limit field names to 10 letters which is why this is truncated.
Area - This field has been included for historic reasons and will likely be dropped in future.
Back to QGIS, we inspect our attribute table. The above-mentioned fields need to be added. We can also see that wards do not have a parent geography. Extraneously fields such as OBJECTID, WARNo, etc need to be removed.
All the fields except for PklWardID are extraneous. We will remove them now.
First, make sure that you select the toggle editing option in the Layer menu
Now select the delete option and remove all of the unnecessary fields.
You will be left with only one field which is the ward code. Wards do not have a separate name and so we will use the code as the name. We will use the field calculator to create both name and code fields.
Here is an example of what the dialogue box should look like:
Do the same for code.
The attribute table show now look like this:
Now we can delete the PklWardID field as before.
The next field to create Is Area. We can use the field calculator area function to do this. $area is given as square meters, divide by a million to get square kilometres.
You will note that we do not have a parent geography as an attribute. This is required by Wazimap in order to create geography hierarchies. We can use the Join Attributes by Location tool until the Vector -> Data Management Tools menu. We load up another layer which contains the parent geographies and then use the tool to run a spatial join between the two layers. The tool will then create a copy of our Ward layer with the desired code field from the parent layer. In this case, our parent layer is municipalities.
A quick look at the municipalities layer’s attribute table shows the following:
In this case, the code column with be used as parent_cod of the wards.
Run the Join Attributes by Location tool with the following settings:
In brief, the input layer represents the child layer, i.e. wards, the join layer is the parent layer, i.e. municipalities. I choose to join based on wards that are are within municipalities. I also ticked the intersects box because there are some tiny overlaps between wards and municipalities in my shapefiles. You will need to check that this does what you expect.
Once you click run, it will create a new layer with the fields that we want. Note that on my computer, this dialogue is occasionally displayed behind the main QGIS window. Keep this in mind if your dialogue disappears.
The attribute table of the new layer now contains the parent code in the code_2 column.
All that’s left is to create a new Parent_cod column by copying the value from code_2 and then deleting code_2 (unfortunately it isn’t possible to rename the column).
Make sure your shapefile is in the Coordinate Reference System (CRS) EPSG:4326 - WGS 84
If it isn't, boundaries might be completely absurd and not show properly on the map.
The file produced in the previous step was 122mb. Before loading boundaries into Wazimap, I use mapshaper.org to compress them by removing unnecessary detail.
When importing, be sure to import all of the various files associated with the shapefile, i.e. wards.shp, wards.dbf, wards.prj, and any other files that are provided. Also, tick the snap vertices checkbox to ensure that almost identical points are snapped together.
Now select simplify from the top-right menu. Accept the default options or experiment with the settings to get the best results.
You are presented with a slider which you can use to decide what level of simplification you are happy with.
Too much simplification results in strange shapes
To choose an appropriate trade-off between file size and detail, I typically zoom into the smallest boundary that I expect the user to be interested in and simplify until it becomes unrecognizable, then I dial it back a little. In this case, I simplified to 11.2%. Once I export the file again as a shapefile, it has been reduced to 16mb.
To load the file into Wazimap you will now need to run the loadshp management command
python3 manage.py loadshp /tmp/wards.shp code=code,name=name,parent_cod=parent_code,area=area ward "2011 Boundaries"
/tmp/wards.shp - path to the shapefile I exported from map shaper
code=code,name=name,parent_cod=parent_code,area=area ward - a mapping between the attribute names in the shapefile and the expected field names, in this case they are almost identical as we gave the attributes the correct names in QGIS.
ward - a label for the type of geography
"2011 Boundaries" - This is the label (version) for the geography hierarchy that the boundaries belong to.
When importing, the script will look for the parent geography found in the parent_cod field with the same version. If the parent is not found the geography will not be loaded. Geographies without values in parent_cod are considered to be root Geographies.
You can load multiple shapefiles in the previous step to create a hierarchy of linked geographies. In order to use these geographies, you need to create a GeographyHierarchy. You can find the dialogue here: /admin/datasets/geographyhierarchy/add/.
All datasets that are loaded will need to be associated with this GeographyHierarchy in order to use with your profile. Data files that are uploaded will need a Geography column that uses the same codes as those in your shapefiles in order to join correctly.
We work in an agile environment. We use scrum.
.
Look for the highest priority available task in the current sprint.
If you can't find any available tasks in the current sprint
ask the rest of the team if you can help with anything,
let the PO and PM know that you can't find anything to work on
groom the backlog of upcoming sprints
Start working on tasks in the upcoming sprint
The priority of what to work on is as follows:
Deploy something that is Ready to Deploy unless we're waiting for a particular deploy time (unless it will take more than a few minutes to deploy)
data mis-representation bugs and bugs seriously affecting production (these should be moved to the top of the sprint)
The story highest up in the sprint that is not finished
Developers should only work on one task at a time.
Once you decide to start working on a task, this is your process:
Assign the task to yourself
Post in the Slack channel on which issue you‘re starting to work on
Create a Draft PR (use the --allow-empty
git command to create an empty commit and push the branch to the repository).
Adjust the description of the PR with a description of the ticket in your own words.
Сheck off the item good description in the PR checklist
Adjust the title of the PR
Check off the item good title in the PR checklist
Create a link to the issue best to use closes #...
issue linked in the checklist
On the right side of the PR make sure the PR is linked to a ticket and therefore the GitHub Project WazimapNG is added (otherwise the automation won‘t work).
Add Design Screenshots (if necessary)
Start working on the ticket
Once you‘re done working on the issue follow this process
Run all tests locally
Check off the item ran tests locally & are passing in the checklist
Run the build (FE npm run build
& BE docker-compose up
)
check-off does it work (build) locally from the checklist
Write down how to test your changes locally and add them to the PR (check off the item)
Go through the Code Quality Checklist
make sure you do not commit commented out code
Make sure you do not have unnecessary login (console.log, print, etc)
Make sure you did not add magic numbers
Fill out the changelog
Do a review yourself
Look at the changed files
make sure you have not committed anything outside the issue
make sure the CI builds are green
make sure the items in the checklist are checked-off
Move the card to Needs code review
Post to slack that you finished the task and link the PR
These are things we've seen cause bugs when we didn't consider them. We should try and have them in mind when implementing and writing tests for anything on this project.
Data can exist for some geographies in a set of siblings but not all
An admin can configure a default filter value which doesn't exist in the data for a given geography
Cloudflare pages requires apex domains for Pages sites to be zones on cloudflare
is a github org of component repos
Try the
Try the with React integration
(in our case this is probably desirable)
You will need to create a new profile that uses this hierarchy. A tutorial to do this can be found . Your new profile will need to also define how geographies will be displayed. Below is an example configuration for the profile. In this case preferred_children
provide instructions for which geography levels will be displayed when multiple options exist.
An explanation of the most important classes and how they relate to Datasets can be found .
Install dokku
Install the postgres plugin.
Install the letsencrypt plugin
Install the redis plugin
Create a postgis database
Create a redis database
Create a dokku app
Link the postgres and redis databases to the app
Change the database url to use postgis instead of postgres
Setup some environment variables
Setup the domain and SSL certificates
Setup the appropriate proxy ports:
Make sure that large uploads are allowed:
Add a git remote for deployment git remote add dokku:wazimap
Deploy git push dokku staging:master
(if deploying the staging branch)
Each ProfileIndicator can be individually configured in the Admin backend using a json dictionary. A typical configuration might look as follows:
All values are optional. Below is a description of each configuration option:
formatting
: How numbers are formatting on the graph. A description of the specification used can be found here: https://github.com/d3/d3-format. The default formatting for Value
is ~s
and .0%
for Percentage
.
minX
: minimum value on the x-axis. This defaults to 0
.
maxX
: maximum value on the x-axis. This defaults to the maximum value in the data.
disableToggle
: By default, graphs can be toggled to display both a percentage and a value view. Setting this value to false removes that toggle from the chart context (hamburger) menu. The toggle is enabled by default.
defaultType
: Sets whether the toggle defaults to Percentage or Value. If disableToggle
is set to false, then this is the only visible view. The Percentage view is set as default.
xTicks
: Sets the number of ticks on the X-Axis.
Filter options are available for any dimensions other than the indicator variable.
To specify that a filter must be applied automatically without user interaction, default filters can be specified.
To do so, add an object with keys name
and value
to the filter.defaults
array.
name
should be the subindicator group name
value
should be the value that should be selected by default.
e.g. to ensure that "Locally generated(%)" is matched on the "income sources" column, add the following configuration to the profile indicator:
It is possible to hide profile indicators in data mapper using the exclude
property.exclude
is optional. If a profile indicator does not have this property, it will not be excluded.
For pre-calculated percentages, usually with absolute choropleth method.
Ideal for population data, were you would like a thousand separator and whole numbers.
How to upload new and update existing datasets
Authoritation is done in the header via an user-based token. The token can be generated via the Admin Interface
POST
/api/v1/datasets/
Authorization
string
file
string
profile
string
Name
Description
required
file
the file you want to update
True
update
not just update the dataset but also update the corresponding Indicator Data
False
overwrite
remove the previous data (including the Indicator Data) and replace with new data from dataset
False
A number of configuration options are available to control how the application is set-up. They can be set in the configuration field of the profile model, e.g.: https://production.wazimap-ng.openup.org.za/admin/profile/profile/
Here is an example of how such a configuration might look.
This section is used to determine which profile information should be used. It matches the URL of the client application. In this case, a website with https://wazimap-ng.africa as a URL will use this profile. A number of URLs are possible to map to a single profile.
page_title
overrides the title on the frontend.
The chart atttribution variable controls the attribution text on images of downloaded charts. In the example below attribution is set to "South Africa"
This section determines the colours used for the choropleths creating in the map explorer.
positive_color_range : [lightest color for the positive values, darkest color for the positive values]
zero_color: color for the zero value in the legend
negative_color_range : [darkest color for the negative values, lightest color for the negative values]
We plot zero on the legend if the minimum value of only positives includes zero, or if the values include negatives and positives.
only positives: scale(positive min, positive max) e.g. light to dark brown
only negatives: scale(negative max, negative min) e.g. dark to light blue
pos and neg: negative scale(negative max, zero colour) positive scale(zero colour, positive max) e.g. dark blue to white to dark brown
example config negative: [dark blue, light blue] positive: [light brown, dark brown] zero: white
if choropleth contains positive and negative values, the legend scale is -(max(mag(neg max), pos max)) to max(mag(neg max), pos max)
e.g. values -2 to 20 legend: -20 to 20 -2 is light light blue 20 dark brown
Previous versions of Wazimap assumed a linear geography hierarchy. The current version allows for a tree-like structure. When a particular level has two potential children, e.g. country
might be the parent of both province
and state
. When a choice is available, the user interface provides the user with a select box to choose which geographies to show. The preferred_children
specifies which geographies are the default.
In some cases, not all side panels need to be visible. For instance, where no point data is going to be displayed, the point data panel can be hidden. This can be configured using the following:
Each of these values is optional. If it is missing then that panel will be displayed by default.
It is possible to customise the text and images for the tutorial by added the "tutorial" key as shown below
It's possible to use Leaflet configuration options as described here: https://leafletjs.com/reference-1.7.1.html#map-l-map.
Custom styles can be injected through the profile, e.g.:
rootGeography: This is the default geography - currently hard-coded as ZA but in time we need to cater for other starting geographies
admins to select one of the previously loaded geographies
individualMarkerLevels: level at which individual point markers are shown instead of dots
admins to select one of the previously loaded geographies
defaultCoordinates : currently hard-coded as {"lat": -28.995409163308832, "long": 25.093833387362697};
admins to select a point off of the map
default zoom level : this defines the zoom level of the map. currently hard-coded as 6
admins to select available zoom levels from a dropdown list
map selected boundary color and hover over color
admins to select colours from predefined swatches (Jen to check with Matt)
translations
property consists of key-value pairs. The keys of the translations property(i.e "Point Mapper" in the sample config below) must be added as an attribute(data-i18n
) to the element that wraps the text to be translated. This can be done by a developer or a designer in index.html.
This can also be used to relable terms to be more appropriate to a specifc profile, e.g. presenting the default "points"
as "services"
'No filters available for the selected data'
'No facilities for this location'
'locations'
'Point Mapper'
'View location in Google Maps'
'Select the category, or specific type of point data you would like to overlay onto the map.'
'No points available for this location'
'Point Filters'
Clustering is disabled in default. Adding the JSON below to the profile config enables clustering.
When clustering is disabled :
When clustering is enabled :
Watermark can be enabled/disabled by
default value is true
Cc license(© 2023. This work is openly licensed via CC BY-NC-ND 4.0) can be added to the page by
default value is false
Point search by distance can be enabled/disabled by
default value is true
Site-wide filters can be enabled/disabled by
default value is true
Default filters can be set for all the profile indicators in a profile by
this config will make sure whenever a profile indicator has "language" group, it will be filtered by "English" in default and whenever a profile indicator has "gender" group, it will be filtered by "Female" in default. If the groups or the values are not available for a profile indicator, it will not create any issues or throw any errors. But it will be logged to developer console.
Available values for a group can be restricted for all the profile indicators in a profile by
this config will make sure whenever a profile indicator has "age" group, the only available filter options will be 15-35 (ZA), 15-24 (Intl) and 30-35 * Key metrics aren't supported in restricted values: Key metrics will not take restrict_values under consideration when calculating metrics.
Default filters and restricting values can be done view-based. This config will override the global default_filters
and restrict_values
if the url contains ?view=youth
View label can be defined using the label
key. If label
key does not exist, view name will be used as the view label.
Url of a view can be defined using the url
key. If url
key does not exist, view url will be created as ${current url}?view=${view}
. url
must be an absolute url - not a relative path
order
key can be used to order the view options in the dropdown
* Key metrics aren't supported in view data whitelists: Key metrics will still appear even if subindicator is not present in restricted values
Default view label can be defined using the profile.default_view_label
If the default_view_label
does not exist, the default label will be assumed "Default"
Tabular comparison link can be added using the tabular_link_enabled
Default value for tabular_link_enabled
is false.
Configuration of the theme points can be set using the Configuration
field of profile collections e.g. https://staging.wazimap-ng.openup.org.za/admin/points/profilecategory/580/change/
For a point attribute to be filterable, it needs to be included in filterable_fields
array of the configuration. In default none of the fields are filterable
The values in filterable_fields
array that are not attributes of the category will be ignored
We can define field type in profile collection config. There are 2 types of type supported currently.
Text - renders data as text
HTML - clean up tags according to allowed tags and renders HTML
Here more info & report error
are key for data fields in locations object.
Allowed Tags : a, b, em, span, i, div, p, ul, li, ol, table, tr, td, th
Allowed Attrs : class, target, href, data-*, style
How to add HTML to the data field of locations:
File upload: Add HTML to extra fields of points upload and it will be saved in DB
Editing location object in admin: If we want to change data for some specific location. We can edit the location object in the points admin
Shapefiles to load geographies and boundaries into geography hierarchies
See also discussion of architecture.
A profile has a geography hierarchy which represents the set of geographies it will make available to users.
A hierarchy has a root, a default demarcation version, and a full list of the versions it supports. When a user loads a profile URL, the default version will be selected. The user can change the selected version using the child type/version dropdown on the bottom right, or by selecting an indicator which only has data for that version.
A demarcation version is used to distinguish multiple versions of the same geography boundary, e.g. wards in 2011, 2016, and 2021.
A dataset is related to a specific geography hierarchy through the profile, and a specific demarcation version. The data is related to specific boundaries via the geography codes.
When loading shapefiles, they are related to a specific geography hierarchy and demarcation version by their names. Levels in the hierarchy are distinguished via the "level" field, e.g. country
, ward
, mainplace
, equal area hexagon
. All levels that have a parent in another level must be to determine their priority for selection as the current child type shown on the map when their parent geography is selected by the user.
There are some issues with hierarchies that are beginning to show their teeth.
I think at least the upload script currently requires each geo to have a parent in the same version. That is probably not really necessary for the frontend or backend in normal use, it means redundant provinces and ZA boundaries, and weirdly the equal area hexagons are "in" the 2016 demarcation rather than just having the 2016 metros as their parents
Versions are keyed on their name, but they can only exist in one hierarchy, so to add a second "2016 Boundaries" I had to add (ye) to its name to distinguish it from another one yet be similar to its peer "2011 Boundaries" in youth explorer.
These are already simplified and have the right fields to import to Wazimap.
See similarly. It also demonstrates uploading.
The zip file says 2021 because that's the election when it took effect but the files are labeled 2020 because that's when the changes were announced by the demarcation board.
ZA
Provinces
Districts - district and metro municipalities
LocalMuni
Wards
Attributes
Name
Code
Parent_cod
Area
za - parent is NULL to make it a root in the hierarchy
pr
dc - district and metro municipalities
local-mn
Wards_2016_geomfix
Attributes
code
parent_cod
except ZA which has parent
name
area
Attributes:
code
parent_cod
area
name
Resolution 7 hexagons.
Only hexagons overlapping with metros in the South Africa 2016 demarcation are included.
The metro with the greatest overlapping area was selected when multiple options were available using the QGIS Processing Toolbox > Join Attributes by location tool.
Point data is arranged in the following structure below a given Wazimap NG Profile:
Themes
Categories
Points
A common way to fetch points is to
Fetch the point themes for a profile, which includes their categories
Fetch the points in a category.
Example themes:
Example point categories
Profile Collections vs Categories
Note that what is called Profile Collections in Admin is called Categories in the points API.
Point data identifiers and updates
Point data themes and collections are continually curated to provide the best user experience.
Avoid hard-coding theme, category and point IDs without documented agreement with profile maintainers that those will remain consistent. Rather agree on names for themes and categories, and notification procedures for updates.
To update points in-place rather than replace entire categories of points, agree on a consistent unique identifier that will be available in the point data fields with the profile administrators.
GET
https://production.wazimap-ng.openup.org.za/api/v1/profiles/
Profile id is required to make requests to points API. Get the ID of the profile from the Profile list.
GET
https://production.wazimap-ng.openup.org.za/api/v1/profile/:profile_id/points/themes/
This API endpoint will return all Themes that are linked to specific Profile
A Profile can be linked to multiple themes and Themes contains multiple categories.
Example request:
GET https://production.wazimap-ng.openup.org.za/api/v1/profile/8/points/themes/
GET
https://production.wazimap-ng.openup.org.za/api/v1/profile/:profile_id/points/theme/:theme_id/profile_categories/
Get all categories under a Theme
GET
https://production.wazimap-ng.openup.org.za/api/v1/profile/:profile_id/points/category/:category_id/points/
Get coordinates and location details for a category.
Example request:
GET https://production.wazimap-ng.openup.org.za/api/v1/profile/8/points/category/578/points/
GET
https://production.wazimap-ng.openup.org.za/api/v1/profile/:profile_id/points/geography/:geography_code/points/
Get points within a Geography. API returns Categories within Geography with all the points associated with specific Category inside requested Geo Code
GET
https://production.wazimap-ng.openup.org.za/api/v1/profile/:profile_id/points/category/:category_id/geography/:geography_code/points/
Get all Points for a Category within a specific Geo Code
Field
Detail
id
ID of the Profile
name
Profile Name
permission_type
Public | Private - Profile Admin can specify which type of user should be able to view data linked to profile
requires_authentication
Boolean - Decides if the user needs authentication to view data
geography_hierarchy
Hierarchy for the Profile
description
TextField - Contains short intro about Profile
configuration
Profile configurations set up by profile admin
profile_id
number
ID of the profile
Field
Description
ID
Theme ID
name
Name of the Theme
icon
Icon used to display on Theme
order
Order in which themes are displayed in UI
profile
ID of the Linked Profile
categories
List of sub-data that displays more information and points available for a Theme
profile_id
number
ID of the Profile
theme_id
number
ID of the theme
Field
Description
ID
Category ID
name
Name of category
description
Description to explain info about category
theme
Linked Theme obj Details
Metadata
Information about the source of data
profile_id
number
ID of the Profile
category_id
number
ID of the Category
Field
Description
features
List of coordinates inside a category
features > id
Location ID
features > geometry
Coordinate details for a Location
features > properties
Data associated with coordinates. It can include anything profile admin wants to display in association with location. ex: Name, Phone number, Detailed address etc.
There is also option for profile admin to have url and image in feature properties
profile_id
number
ID of the profile
geography_code
string
Geo Code for Geography
Field
Description
count
Total Number of Categories with in a Geography
results
List of Detailed points collection for Categories
results > category
Category Name
results > features
List of locations with details for a category within Geography
profile_id
number
ID of the Profile
category_id
number
ID for the Category
geography_code
string
Geo Code for Geography