arrow-left

Only this pageAll pages
gitbookPowered by GitBook
1 of 68

Wazimap technical handbook

Loading...

System Architecture

Loading...

Loading...

Loading...

Loading...

Loading...

Development

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...

Testing

Loading...

Loading...

Loading...

Loading...

Design

Loading...

Change Proposals

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Tutorials

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Configuration

Loading...

Loading...

Loading...

Loading...

API

Loading...

Loading...

Loading...

Loading...

Introduction

This is the technical handbook for Wazimap NGarrow-up-right

This is intended for

  • Profile administrators - how to configure profiles

  • Developers - how we work on this project.

Profile administrators should see the .

Profile Curation Guidearrow-up-right

Rules for Webflow exports

  1. Do no change existing class names (unless strictly necessary to implement a request)

  2. If a class name is change, ensure that change is discussed with a developer and documented in the Gitbook.

  3. 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".

  4. Ensure that any change is documented in the Gitbook.

October 2022

hashtag
2022-10-03

Fix to map zoom positioned items

file-archive
4MB
wazimap-ng.webflow (map-zoom-position-fix-10032022).zip
archive
arrow-up-right-from-squareOpen
  • Changed the position of the download button

  • Changed the position of the geo select panel

  • Changed the position of the map options panel

hashtag
2022-10-02

Move custom stylesheet code from body to head tag

file-archive
4MB
wazimap-ng.webflow (2).zip
archive
arrow-up-right-from-squareOpen

April 2022

hashtag
Removed hardcoded script in index.html

file-archive
3MB
wazimap-ng.webflow (removed-index-script-04202022-reupload).zip
archive
arrow-up-right-from-squareOpen
  • On request from Emre, I have removed the second script in the screenshot above.

  • UPDATE: Reuploaded a version with both the scripts in the screenshot removed.

Critical Paths

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

Creating a new admin user

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:

  1. Navigate to the user creation page: https://api.wazimap.com/admin/auth/user/add/arrow-up-right

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

Deployment to Dokku

hashtag
On the server

  • Install dokkuarrow-up-right

  • Install the .

  • Install the

  • Install the

  • 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:

hashtag
On your local machine

  • Add a git remote for deployment git remote add dokku:wazimap

  • Deploy git push dokku staging:master (if deploying the staging branch)

Profile Collection Configuration

Configuration of the theme points can be set using the Configuration field of profile collections e.g.

hashtag
Filterable Fields

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

postgres pluginarrow-up-right
letsencrypt pluginarrow-up-right
redis pluginarrow-up-right
dokku postgres:create wazimap-db \
-i kartoza/postgis \
-p <database user password> \
-r <root password> \
-I 11.0-2.5 \
-C "POSTGRES_MULTIPLE_EXTENSIONS=postgis,pg_trgm;POSTGRES_USER=postgres;POSTGRES_PASS=<database user password>"
dokku redis:create wazimap-redis
dokku apps:create wazimap
dokku postgres:link wazimap-db wazimap
dokku redis:link wazimap-redis wazimap
NEW_URL=$(dokku config:get wazimap DATABASE_URL | sed 's/^postgres/postgis/') dokku config:set wazimap DATABASE_URL=$NEW_URL
dokku config:set wazimap \
    AWS_ACCESS_KEY_ID=<access key id> \
    AWS_S3_REGION_NAME=<region name> \
    AWS_SECRET_ACCESS_KEY=<secret key> \
    AWS_STORAGE_BUCKET_NAME=<storage bucket> \
    DEFAULT_FILE_STORAGE=storages.backends.s3boto3.S3Boto3Storage \
    DJANGO_CONFIGURATION=Production \
    DJANGO_DEBUG=False \
    DJANGO_SECRET_KEY=<django secret key>
dokku domains:add wazimap wazimap.com
dokku letsencrypt:enable wazimap
# Wazimap does not listen on port 5000 by default
echo dokku proxy:ports-add wazimap http:80:8000
dokku proxy:ports-add wazimap https:443:8000
dokku proxy:ports-remove wazimap http:80:5000
dokku proxy:ports-remove wazimap https:443:5000
dokku nginx:set wazimap client-max-body-size 100m

The values in filterable_fields array that are not attributes of the category will be ignored

hashtag
Field Types

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

https://staging.wazimap-ng.openup.org.za/admin/points/profilecategory/580/change/arrow-up-right
  "filterable_fields": [
    "campus"
  ]
  "filterable_fields": [
    "campus", "random", "values", "facility type"
  ]
"field_type": {
    "more info": "html",
    "report error": "text"
}

Database Models

Entity-Relationship Diagram for Wazimap-NG

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.

hashtag
Datasets App

Entity Relationship Diagram for the Datasets App

hashtag
Dataset and DatasetData

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:

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.

hashtag
Indicators and IndicatorData

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:

The actual structure of IndicatorData objects is a little more complicated. More detail can be found here: .

hashtag
Universe

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:

Other noteworthy models are Geography and GeographyHierarchy. These are discussed in more detail here: .

hashtag
Profile App

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:

ProfileIndicator, ProfileKeyMetrics, and ProfileHighlight. ProfileIndicator is the most commonly used of the three.

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 .

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.

hashtag
Points App

20

15

ZA

Female

21

13

...

WC

Male

20

5

...

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

Data Administrator
Geography Hierarchy
IndicatorData
Geography Hierarchies
Profile Administrator
choropleth maps
Relationship between Indicator and IndicatorData
Example of how ProfileHighlights are displayed on the frontend.

Female

Geography Hierarchies

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:

  1. It is a directed acyclic graph (DAG)

  2. Child geographies are completely contained by their parent.

  3. Sibling geographies do not overlap.

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 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.

hashtag
Hierarchical Structure

hashtag
Not linear

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.

hashtag
No universal levels

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.

hashtag
No wall-to-wall coverage

Boundaries do not need to cover the entire area of their parent, holes are possible.

hashtag
Multiple simultaneously display levels

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.

hashtag
Technical Details

hashtag
Backend

is used to manage the model. Then enables fast querying of hierarchies which is usually hard to do in naive SQL table structures. Spatial data is stored in objects. Geographies are to GeoJSON before being served to the client.

hashtag
Compression

Overly detailed geographies can result in large downloads. The GeographyBoundary models uses a to automatically compress boundaries everytime the model is . 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.

hashtag
Frontend

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.

hashtag
See also

{
    Geography: ZA # Geography is actually stored in its own field and not in the JSON field. 
    Gender: Male,
    Age: 20,
    Count: 10
}
Select
    Geography,
    Gender,
    Sum(Count)
From 
    Dataset d
Where
    d.id = XXX
Group by
    Geography,
    Gender
Select
    Geography,
    Age,
    Sum(Count)
From 
    Dataset d
Where
    d.id = XXX
Group by
    Geography,
    Age
{
  Geography: ZA,
  subindicators: {
   Male: 22,
   Female: 28
  }
  ... # other information is stored here and is described elsewhere in this manual.
}
Select
    Geography,
    Age,
    Sum(Count)
From 
    Dataset d
Where
    d.id = XXX
    and Gender = Female
Group by
    Geography,
    Age
{
    'Age Group__in': ['60-64', '65-69', '70-74', '75-79', '80-84', '85+']
}
Dataset.objects.filter(**universe.filters) # pseudocode

IndicatorData

This page is a stub.

Below is an example of an Indicator Data object

'groups': {'race': {'Other': [{'count': 14034.3619100001,
     'region of birth': 'Other'},
    {'count': 23014.7457499993, 'region of birth': 'Rest of africa'},
    {'count': 34546.705719999, 'region of birth': 'Sadc'},
    {'count': 33844.2467799998, 'region of birth': 'South africa'},
    {'count': 8627.21382000001, 'region of birth': 'Unspecified'}],
   'White': [{'count': 17755.8348600004, 'region of birth': 'Other'},
    {'count': 1076.82088, 'region of birth': 'Rest of africa'},
    {'count': 19345.9559600004, 'region of birth': 'Sadc'},
    {'count': 1175191.32683901, 'region of birth': 'South africa'},
    {'count': 9675.18685000005, 'region of birth': 'Unspecified'}],
   'Coloured': [{'count': 1104.4229, 'region of birth': 'Other'},
    {'count': 842.120029999999, 'region of birth': 'Rest of africa'},
    {'count': 5666.85164000002, 'region of birth': 'Sadc'},
    {'count': 1575281.6271999, 'region of birth': 'South africa'},
    {'count': 1675.21038, 'region of birth': 'Unspecified'}],
   'Black african': [{'count': 9463.15047999995, 'region of birth': 'Other'},
    {'count': 68003.2315900028, 'region of birth': 'Rest of africa'},
    {'count': 810647.802930328, 'region of birth': 'Sadc'},
    {'count': 14739761.6385047, 'region of birth': 'South africa'},
    {'count': 85310.3884700055, 'region of birth': 'Unspecified'}],
   'Indian or asian': [{'count': 38132.814809999, 'region of birth': 'Other'},
    {'count': 5076.67313999999, 'region of birth': 'Rest of africa'},
    {'count': 2539.20952000001, 'region of birth': 'Sadc'},
    {'count': 414713.136599942, 'region of birth': 'South africa'},
    {'count': 5150.74462, 'region of birth': 'Unspecified'}]},
  'gender': {'Male': [{'count': 55748.008589999, 'region of birth': 'Other'},
    {'count': 72957.3858900018, 'region of birth': 'Rest of africa'},
    {'count': 517617.920130245, 'region of birth': 'Sadc'},
    {'count': 8727584.65349521, 'region of birth': 'South africa'},
    {'count': 63433.5451700032, 'region of birth': 'Unspecified'}],
   'Female': [{'count': 24742.5763700004, 'region of birth': 'Other'},
    {'count': 25056.2055000003, 'region of birth': 'Rest of africa'},
    {'count': 355128.605640082, 'region of birth': 'Sadc'},
    {'count': 9211207.32242836, 'region of birth': 'South africa'},
    {'count': 47005.1989700023, 'region of birth': 'Unspecified'}]},
  'age group': {'15-19': [{'count': 6298.26723000001,
     'region of birth': 'Other'},
    {'count': 6312.14378000002, 'region of birth': 'Rest of africa'},
    {'count': 68137.1359800096, 'region of birth': 'Sadc'},
    {'count': 4775821.0196088, 'region of birth': 'South africa'},
    {'count': 9359.01092000012, 'region of birth': 'Unspecified'}],
   '20-24': [{'count': 16982.6305099999, 'region of birth': 'Other'},
    {'count': 22846.8292099995, 'region of birth': 'Rest of africa'},
    {'count': 229155.41202007, 'region of birth': 'Sadc'},
    {'count': 4684128.15334481, 'region of birth': 'South africa'},
    {'count': 27011.4452600009, 'region of birth': 'Unspecified'}],
   '25-29': [{'count': 26769.6408399996, 'region of birth': 'Other'},
    {'count': 34202.7678200008, 'region of birth': 'Rest of africa'},
    {'count': 309901.929190191, 'region of birth': 'Sadc'},
    {'count': 4354313.53260714, 'region of birth': 'South africa'},
    {'count': 37417.0233700027, 'region of birth': 'Unspecified'}],
   '30-35': [{'count': 30440.0463799999, 'region of birth': 'Other'},
    {'count': 34651.8505800018, 'region of birth': 'Rest of africa'},
    {'count': 265552.048580056, 'region of birth': 'Sadc'},
    {'count': 4124529.27036281, 'region of birth': 'South africa'},
    {'count': 36651.2645900019, 'region of birth': 'Unspecified'}]}},
 'subindicators': {'Sadc': 872746.525770327,
  'Other': 80490.5849599994,
  'Unspecified': 110438.744140006,
  'South africa': 17938791.9759236,
  'Rest of africa': 98013.5913900021}}
Each geography has only one parent.
ISO 3166arrow-up-right
Treebeardarrow-up-right
Geographyarrow-up-right
GeographyBoundaryarrow-up-right
serializedarrow-up-right
CachedMultipolygonFieldarrow-up-right
savedarrow-up-right
NGP2 - Presenting Geographical Hierarchies to userschevron-right
NGP3 - Change Geography Hierarchieschevron-right
Loading new geographieschevron-right
"preferred_children": {
    "country": [
      "province"
    ],
    "district": [
      "municipality",
      "mainplace",
      "ward"
    ],
    "province": [
      "district",
      "municipality"
    ],
    "mainplace": [
      "subplace"
    ],
    "municipality": [
      "mainplace",
      "ward"
    ]
  }

Code Deployment

hashtag
Frontend code merge

  1. Developer creates a draft PR

  2. Developer completes code and removes the draft label from the PR.

  3. The Trello ticket is moved from In progress to Code Review.

  4. 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.

  5. The Trello ticket is then moved into Review (Product Owner)

  6. The Product Owner reviews the Netlify deploy preview. Once the implementation is approved, the Trello ticket moves to To be deployed

  7. The Lead Developer merges the PR into staging

  8. The Product Owner reviews the staging server (on a scheduled, or ad hoc basis)

  9. 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:

  1. 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.

  2. Resolution of conflicts.

Choropleth Maps

This page is a stub

Component Architecture

The Wazimap-NG frontend uses a custom component architecture. Over time, this will likely evolve to use a more standard framework such as web componentsarrow-up-right.

hashtag
Observable Abstract Class

Component class hierarchy

All components extend the Component abstract class which itself extends the Observable abstract class. Observableimplements the . 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.

hashtag
Component Abstract Class

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].

hashtag
Building Components

Apart from extending Component Wazimap-NG components use a 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.

hashtag
Controller

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:

hashtag
Model

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:

hashtag
MVC Architecture

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.

hashtag
A few points on style

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.

Determine which instance to use

Wazimap-NG is multi-tenanted. A single backend can host multiple profiles, e.g. https://beta.youthexplorer.org.zaarrow-up-right and http://sifar.openup.org.zaarrow-up-right both use the same server and database.

In order to determine which profile to use, the client sends a wm-hostnameheader with this api call: /api/v1/profile_by_url/?format=jsonthis 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.

{
    ...
    "results": [
        {
            "id": 2,
            "name": "Vulekamali",
            ...
            "configuration": {
                "urls": [
                    "geo.vulekamali.gov.za"
                ],
                ...
            }
        },
        {
            "id": 3,
            "name": "Cape Town Against Covid-19",
            ...
            "configuration": {
                "urls": [
                    "capetownagainstcovid19.openup.org.za"
                ],
                ...
            }
        },
        ...
}

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.

Observer design patternarrow-up-right
Model View Controllerarrow-up-right

Code Review Process

Your code will be reviewed. And you can can take steps to make it easier for the reviewer and yourself.

hashtag
What is the job of the Review?

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.

Merging webflow exports

Instructions for merging new webflow exports

  1. Obtain the latest Webflow export from

  2. Run path-to-webflow-export.zip

class DropDown extends Observable {
    static EVENTS = {
        selected: 'dropdown.selected' 
    }
    
    constructor() {
        super();
    }
    
    onElementSelected() {
        let payload = ...
        this.triggerEvent(DropDown.EVENTS.selected, payload)
    }
}
let dropdown = new DropDown();
dropdown.on(DropDown.EVENTS.selected, payload => alert('An element has been selected')
class DropDown extends Component {
    static EVENTS = {
        selected: 'dropdown.selected' 
    }
    
    constructor(parent) {
        super(parent);
    }
    
    onElementSelected() {
        ...
    }
}
class Application extends Component {
    constructor() {
        super()
        this._dropdown = new DropDown(this);
        
        this.registerChild(this._dropdown);
    }
}
// This class implements the controller for the DropDown component.
export class DropDownComponent extends Component {
    constructor(parent, container, values = [], defaultText = '') {
        this._container = container
        this._model = new DropDownModel(values, defaultText);
        this.prepareDomElements();
        this.prepareEvents();
    }
    
    get container() {
        return this._container;
    }
    
    get model() {
        return this._model;
    }
    
    prepareDomElements() {
        this._textArea = $(this.container).find('.textArea')[0];
        this._trigger = $(this.container).find('.trigger')[0];
        this._optionContainer = $(this.container).find('.options')[0];
        this._listItem = $(self._optionContainer).find('.list_item')[0]
    }
    
    prepareEvents() {
        const self = this;
        this._trigger.on('click', () => {
            $(self._optionContainer).show();
        })
        this.model.on(DropDownModel.EVENTS.changeValue, model => {
            self.updateText();
        })
    }
    
    setOptions(values) {
        const self = this;
        this.model.options = values;
        $(this._optionContainer).html('');
        values.array.forEach((element, idx) => {
            let li = self._listItem.clodeNode(true);
            li.text(element);
            $(self._optionContainer).html('');
            let li = listItem.cloneNode(true);
            li.on('click', () => {
                self.model.currentIndex = idx;
            })
            $(self._optionContainer).append(li);
        });
    }
    
    updateText() {
        $(this._textArea).text(this.model.currentValue)
    }
    
    reset() {
        this.model.reset();
    }
}
constructor(parent, container, values = [], defaultText = '')
constructor(...) {
    ...
    this.prepareDomElements();
    ...
}
    
...

prepareDomElements() {
    this._textArea = $(this.container).find('.textArea')[0];
    this._trigger = $(this.container).find('.trigger')[0];
    this._optionContainer = $(this.container).find('.options')[0];
    this._listItem = $(self._optionContainer).find('.list_item')[0]
}
    
...
constructor(...) {
        ...
        this.prepareEvents();
    }
    
    prepareEvents() {
        const self = this;
        this._trigger.on('click', () => {
            $(self._optionContainer).show();
        })
        this.model.on(DropdownModel.EVENTS.changeValue, model => {
            self.updateText();
        })
    }
setOptions(values) {
        ...
        values.array.forEach((element, idx) => {
            let li = self._listItem.clodeNode(true); // <-- Note that the li element is being cloned from an existing one.
            li.text(element);
            ...
            $(self._optionContainer).append(li); // <--- Note how the li element is inserted into the DOM.
        });
    }
class DropDownModel extends Observable {
    static EVENTS = {
        changeValue: 'dropdown.model.changevalue'
    }
    
    constructor(options = [], defaultText = '') {
        this._options = options
        this._defaultText = defaultText;
        this._currentIndex = null;
    }
    
    get options() {
        return this._options;
    }
    
    set options(values) {
        this._options = values;
        this.reset();
    }
    
    get defaultText() {
        return this._defaultText;
    }
    
    get currentIndex() {
        return this._currentIndex;
    }
    
    set currentIndex(newIdx) {
        if (newIdx != this._currentIndex) {
            this._currentIndex = newIdx;
            this.triggerEvent(DropDownModel.EVENTS.changeValue, this)
        }
    }
    
    get currentValue() {
        if (this.currentIndex == null)
            return this.defaultText;
        return this.options[this.currentIndex];
    }
    
    reset() {
        this.currentIndex = null;
    }
}
set currentIndex(newIdx) {
    if (newIdx != this._currentIndex) {
        this._currentIndex = newIdx;
        this.triggerEvent(DropDownModel.EVENTS.changeValue, this)
    }
}
prepareEvents() {
    ...
    this.model.on(DropDownModel.EVENTS.changeValue, model => {
        self.updateText();
    })
    ...
}
updateText() {
    $(this._textArea).text(this.model.currentValue)
}
setOptions(values) {
    ...
    li.on('click', () => {
        self.model.currentIndex = idx;
    })
    ...
}
constructor(options = [], defaultText = '') {
        this._options = options
        this._defaultText = defaultText;
        this._currentIndex = null;
    }
get options() {
        return this._options;
    }
    
    set options(values) {
        this._options = values;
        this.reset();
    }
doSomething(data) {
    console.log(data.key1.key2.value) // bad 
}

doSomething(value) {
    console.log(value);
}
Check that everything works as expected
  1. Start the local dev server and try it

  2. Run automated tests

  • Add all the changes to git, commit and push to your branch.

  • hashtag
    Fixing conflicts

    If you have conflicts with webflow-controlled files, these can be fixed as follows:

    1. Fix any conflicts in non-webflow-controlled files as usual.

    2. Import the latest webflow export.

    3. 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.

    here
    import-webflowarrow-up-right
    $ git checkout master 
    Switched to branch 'master'
    Your branch is behind 'origin/master' by 28 commits, and can be fast-forwarded.
      (use "git pull" to update your local branch)
    
    
    $ git pull
    Updating 5cb0775..d586c13
    Fast-forward
    ...
      src/index.html         |    20 +-
    ...
    
    
    $ git checkout feature-branch
    Switched to branch 'feature-branch'
    
    
    git merge master 
    Auto-merging src/index.html
    CONFLICT (content): Merge conflict in src/index.html
    Automatic merge failed; fix conflicts and then commit the result.
    
    
    import-webflow ~/Downloads/wazimap-ng.webflow\ \(latest-export-10032022\).zip 
    Extracting to temporary directory /tmp/tmp-296926-8rNUgOwtcaJs
    Copying css to src/css
    Copying js to src/js
    Copying images to src/images
    Looking for files matching index.html in /tmp/tmp-296926-8rNUgOwtcaJs
    Reading /tmp/tmp-296926-8rNUgOwtcaJs/index.html
    Transforming using ./src/js/webflow/import.js
    Looking for files matching {401,about}.html in /tmp/tmp-296926-8rNUgOwtcaJs
    Reading /tmp/tmp-296926-8rNUgOwtcaJs/401.html
    No DOM transformation provided
    Writing src/401.html
    Reading /tmp/tmp-296926-8rNUgOwtcaJs/about.html
    No DOM transformation provided
    Writing src/about.html
    Writing src/index.html
    Cleaning up temporary directory /tmp/tmp-296926-8rNUgOwtcaJs
    
    
    
    $ git status
    On branch no-style-tags-in-body
    You have unmerged paths.
      (fix conflicts and run "git commit")
      (use "git merge --abort" to abort the merge)
    
    Changes to be committed:
            ...
    	new file:   __tests__/gui/...
            ...
            
    Unmerged paths:
      (use "git add <file>..." to mark resolution)
    	both modified:   src/index.html
    
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git restore <file>..." to discard changes in working directory)
    	modified:   src/401.html
    	modified:   src/about.html
    	modified:   src/css/wazimap-ng.webflow.css
    	modified:   src/js/webflow.js
    
    
    $ git add src/
    
    $ git commit
    [feature-branch 32ecbac] Merge branch 'master' into feature-branch
    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.

    hashtag
    Who can/should approve a PR

    Everyone can submit their review and we encourage everyone to do so. But only the PM and the Lead Developers can approve a PR.

    hashtag
    Who can merge a PR?

    Once the PR is approved anyone can merge the PR. And you are encouraged to merge the PR once it is approved.

    hashtag
    How to create a good PR

    Create a PR as early as possible, and push frequently.

    hashtag
    Open a Pull Request (PR) once you decide to work on an issue.

    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.

    hashtag
    Use the PR template

    The template is there to help you and the reviewer to make the process as easy and fast as possible.

    hashtag
    Write a good description

    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)

    hashtag
    Run quality checks locally

    Run codeclimate locally and address issues early on. It will show you issues that will be raised once you push the code.

    hashtag
    Run the tests locally

    Make sure you run the tests locally (no need to run E2E) to make sure they pass.

    hashtag
    Only mark the PR to ready for review, when you truly think it's ready

    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.

    hashtag
    Leave your own review

    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?

    • 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?

    Development Process

    We work in an agile environment. We use scrum.

    There is a task board per sprintarrow-up-right.

    Look for the highest priority available task in the current sprint.

    If you can't find any available tasks in the current sprint

    1. ask the rest of the team if you can help with anything,

    2. let the PO and PM know that you can't find anything to work on

    3. groom the backlog of upcoming sprints

    4. Start working on tasks in the upcoming sprint

    The priority of what to work on is as follows:

    1. 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)

    2. data mis-representation bugs and bugs seriously affecting production (these should be moved to the top of the sprint)

    3. The story highest up in the sprint that is not finished

    hashtag
    As a Developer

    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).

    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

    hashtag
    Common broken assumptions

    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

    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

  • 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

  • )
    • 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

  • November 2022

    hashtag
    2022-11-22 - Powered by Wazimap

    file-archive
    4MB
    wazimap-ng.webflow (powered-by-wazimap).zip
    archive
    arrow-up-right-from-squareOpen
    • Added "Powered by Wazimap" watermarks to map, point mapper, data mapper and rich data.

    June 2023

    hashtag
    2023/06/26 - Removal of tutorial button in nav

    file-archive
    4MB
    wazimap-ng.webflow (removed-tutorial).zip
    archive
    arrow-up-right-from-squareOpen

    Pull Request Template (FE)

    hashtag
    Description

    hashtag
    Related Issue

    hashtag
    How to test it locally

    hashtag
    Screenshots

    hashtag
    Changelog

    hashtag
    Added

    hashtag
    Updated

    hashtag
    Removed

    hashtag
    Checklist

    hashtag
    Pull Request

    hashtag
    Commits

    hashtag
    Code Quality

    hashtag
    Testing

    Webflow Integration

    Stable version: ... | Latest version: https://wazi-rich-data.webflow.io/

    hashtag
    General

    Rationale for recent structure update: The latest version of wazimap has been reworked to ensure we can include the following features:

    1. In-page anchor linking and page navigation

    2. A component based approach to populating pages

    3. An improved mobile experience

    4. Filtering of sub-indicator

    hashtag
    Styles section

    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.

    hashtag
    Rich Data Panel

    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.

    hashtag
    1. Rich data nav items

    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.

    circle-info

    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.

    hashtag
    How to implement this component:

    Call .rich-data-nav__item into .rich-data-nav__list

    Once you're strong enough, save the world:

    Webflow exports & changelog

    hashtag

    hashtag
    10/11/2021 - Filter truncation

    file-archive
    4MB
    wazimap-ng.webflow (truncation-fix).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export

    hashtag
    Changes:

    • Added truncation to long filters

    • Removed generic-modal

    hashtag
    10/06/2021 - Cluster scroll fix

    hashtag
    Changed:

    Added a element .facility-tooltip__scroll inside .facility-tooltip is--cluster which has overflow: auto to accommodate a long scrolling list.

    hashtag
    10/06/2021 - Point mapper truncation fix

    Ensured that point mapper labels are correctly truncated.

    hashtag
    10/01/2021 - Map data version support

    circle-info

    Documentation of indicator version:

    circle-info

    Documentation of highlight element:

    circle-info

    Documentation of generic modal:

    Generic Modal:

    circle-info

    Documentation of indicator version:

    hashtag
    09/23/2021 - Facilities loading state

    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)

    hashtag
    09/16/2021 - Point mapper dropdown icon colour fix

    hashtag
    Changed:

    • 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.

    hashtag
    09/16/2021 - Cluster tooltip

    circle-info

    Documentation:

    hashtag
    Added:

    • .facility-tooltip is--cluster to the styles panel for use when clustering nearby points on the map.

    hashtag
    09/15/2021 - Point mapper dropdown and filters

    circle-info

    Documentation:

    hashtag
    Point mapper dropdown with toggle:

    hashtag
    Point filters:

    hashtag
    Changes:

    • Added .point-mapper__h1_checkbox that is hidden by default

    • Added .point-filters panel in .map-bottom-items (hidden by default)

    hashtag
    08/31/2021 - Fix for tutorial modal height

    hashtag
    Changes:

    • Restricted max height to 90vh on the tutorial modal

    hashtag
    08/25/2021 - Rich Data Nav Fix

    circle-info

    Feature preview:

    Changed:

    • Made the section nav scroll when height is restricted

    hashtag
    08/12/2021 - Print CSS test

    Changed:

    • add the following code to the print css:

    hashtag
    07/19/2021- Point-legend, table margin, z-depth bottom-items

    hashtag
    Changed:

    • 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

    hashtag
    07/13/2021- Fixed rich data filter buttons to resemble map-options

    circle-info

    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.

    circle-exclamation

    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.

    hashtag
    Changed:

    • Reverted to old implementation of .map-options__filters

    • Changed .profile-indicator__new-filter to be a wide button to replicate the map-options button

    hashtag
    07/12/2021- Remove map options tooltip and change filter buttons

    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:

    hashtag
    07/05/2021- Disabled state for map download button

    • Added .disabled state to .map-download button by default.

    • Remove this class to set state to enabled.

    hashtag
    07/05/2021- Removed filter disabled

    • Removed the .disabled class from .dropdown-menu__trigger

    • The disabled class is now only on .mapping-options__filter

    hashtag
    07/02/2021 - Map options filters

    circle-exclamation

    Please note that this element was changed as per a feature request:

    hashtag
    Changed:

    • 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

    hashtag
    06/28/2021 - Table content truncation

    hashtag
    Changed:

    • Added code to force truncation on the table cells

    hashtag
    06/28/2021 - Location tag width

    hashtag
    Changed:

    • Set text within .map-tooltip__geography-chip to breaking: no-wrap;

    hashtag
    06/22/2021 - SVG console error fix

    hashtag
    Changed:

    • Adjusted the svg height value for .location-facility__icon to 24px to avoid console errors.

    • Removed wazimap-ng.css script call

    hashtag
    06/21/2021 - .is--disabled to .disabled

    hashtag
    Changed:

    • renamed the disabled class on the rich-text dropdowns to .disabled from .is--disabled

    hashtag
    06/11/2021 - Map options truncation fix

    hashtag
    Fixed:

    Fixed the short truncation on map options title. It now extends the full width of the panel.

    hashtag
    Changed:

    Removed capitalization on the map options title.

    hashtag
    05/25/2021 - Changes to map options filters

    circle-info

    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).

    hashtag
    Changed:

    • 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.

    hashtag
    05/21/2021 - Additive filtering

    circle-info

    Feature preview:

    hashtag
    Added:

    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

    hashtag
    Changed:

    • Added border-bottom: 1px; and padding-bottom: 12px; to .profile-indicator__header

    hashtag
    04/29/2021 - Data attributes and classes for Point Mapper and locations

    circle-info

    This change was requested by Mila directly and does not have an associated trello card or feature preview.

    hashtag
    Changed:

    • 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)

    hashtag
    Fixed:

    • Changed the wording of "Download all facilities" to "Download all locations" in the rich data view.

    hashtag
    04/22/2021 - Download all facilities

    circle-info

    Feature preview:

    hashtag
    Changes:

    • 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.

    hashtag
    04/22/2021 - Chart menu data attributes

    hashtag
    Changes:

    • Added data attributes to the buttons in the chart dropdown menu

    • The following "data-id" attributes have been added:

      • data-id="Percentage"

    hashtag
    04/09/2021 - Rich data tables

    hashtag
    Changes:

    • Added .profile-indicator__table to the .styles section

    hashtag
    Documentation:

    • .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

    hashtag
    03/09/2021 - Print and google maps button for location

    circle-info

    Feature preview:

    hashtag
    Added:

    • Added print button to .facility-info

      • .facility-info__print is now a child of .facility-info__header

    hashtag
    03/01/2021 - Explicit print button in rich data

    circle-info

    Trello card:

    hashtag
    Changed

    • Added .rich-data__print to the rich data panel

    hashtag
    03/01/2021 - Rich data location buttons

    hashtag
    Changed

    circle-info

    Trello cards: +

    circle-info

    Feature preview:

    • 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"

    hashtag
    02/18/2021 - Map legend always showing

    hashtag
    Changed

    circle-info

    Trello card:

    hashtag
    1. Map legend always showing:

    circle-info

    Feature preview:

    • 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.

    hashtag
    2. Version of the point-legend without a remove button for print

    hashtag
    02/18/2021 - Map legend

    hashtag
    Changed

    circle-info

    Trello card:

    hashtag
    1. Map print legend:

    • Added .map-print to the .map div

    • Added .map-print__point-legend to .map-print

    hashtag
    2. Point legend:

    • .point-legend contains .point-legend__color and .point-legend__text

      • .point-legend__text has a default state of loading...

    hashtag
    3. Choropleth legend:

    • 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.

    hashtag
    02/17/2021 - Added data attributes to hover menu items

    hashtag
    Changed

    hashtag
    Added data attributes to hover menu components:

    circle-info

    Trello card:

    • 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

    hashtag
    Fixed

    • 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.

    hashtag
    02/16/2021 - Hiding and styling content in chart hover menus

    circle-info

    Webflow feature preview:

    hashtag
    Changed

    hashtag
    1. Hover menu groups for chart-value and download-data

    circle-info

    Trello card:

    • Added a div for .hover-menu__chart-value and .hover-menu__download-data

    • Add .hidden to remove this group from the menu

    hashtag
    2. Changed css for active state on hover menu list items

    circle-info

    Trello card:

    • 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

    hashtag
    02/12/2021 - Tab notice and map title

    hashtag
    Changed

    hashtag
    Map title:

    circle-info

    Related Trello card:

    circle-info

    Webflow feature preview:

    • Changed default state for .map-title to display: block

    • Added .hidden class to .block-title to hide by default.

    hashtag
    Tab notice:

    circle-info

    Related Trello card:

    circle-info

    Webflow feature preview:

    • Added .tab-notice as a child of .main

    • Added .hidden class to .tab-notice to hide by default.

    March 2023

    hashtag
    2023/03/17 - Unnecessary code removal

    file-archive
    4MB
    wazimap-ng.webflow (css-removal).zip
    archive
    arrow-up-right-from-squareOpen

    hashtag
    2023/03/16 - Location tag scroll bar fix

    file-archive
    4MB
    wazimap-ng.webflow (location-bar-scroll-fix).zip
    archive
    arrow-up-right-from-squareOpen
    • 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

    hashtag
    2023/03/15 - Various mobile changes

    • 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

    Pull Request Template Explanation

    And explanation of the items in the Template

    hashtag
    Explanations

    hashtag
    Good Title:

    Use the Issue Number and the issue title for the title of your PR.

    hashtag
    Description

    Add a good description here. Use the issue description to inform your own understanding of this issue. See

    hashtag
    Related Issue

    Create a link to the related Trello Card here. So that it can be easily referenced.

    hashtag
    How to test it locally

    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.

    hashtag
    Screenshots

    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.

    hashtag
    Changelog

    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.

    hashtag
    Checklists

    This part should be checked off accordingly, when you're ready.

    📖 changelog filled out
    ⚙️ ran jslint
  • 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.

  • file-archive
    4MB
    wazimap-ng.webflow (various-mobile-changes-updated-3).zip
    archive
    arrow-up-right-from-squareOpen
    How to prepare a Pull Request
    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

  • .mapping-options__new-filter

  • .mapping-options__remove-filter (hidden by default)

  • Added code to disabled filters that have the disabled class:
    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-filteris visible by default in the rich data panel

    • .profile-indicator__remove-filter is hidden by default in the rich data panel

    Added data-i18n="Point Mapper" to all instances of "Point Mapper"

  • Added data-i18n="locations" to all instances of "locations"

  • .location__facilities_header-wrapper to contain the icon and title. Please note that this may cause integrations with backend data to break.

    data-id="Value"

  • data-id="csv"

  • data-id="excel"

  • data-id="json"

  • .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 .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

    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

  • .map-print has a .hidden class by default which is removed when required

    .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

  • 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

  • Remove .hidden to show
    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...")

  • file-archive
    4MB
    wazimap-ng.webflow (cluster-scroll-fix - 10062021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    file-archive
    4MB
    wazimap-ng.webflow (pointmapper-truncation - 100621).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    file-archive
    4MB
    wazimap-ng.webflow (indicator-version - 10012021).zip
    archive
    arrow-up-right-from-squareOpen
    https://wazimap-ng.webflow.io/documentation#rich-dataarrow-up-right
    https://wazimap-ng.webflow.io/documentation#utilityarrow-up-right
    https://wazimap-ng.webflow.io/documentation#modalsarrow-up-right
    https://wazimap-ng.webflow.io/documentation#rich-dataarrow-up-right
    file-archive
    4MB
    wazimap-ng.webflow (facilities-loading-state-fixed-2 - 09232021 ).zip
    archive
    arrow-up-right-from-squareOpen
    file-archive
    4MB
    wazimap-ng.webflow (icon-colour-fix - 09162021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow Export
    file-archive
    4MB
    wazimap-ng.webflow (icon-colour-fix - 09162021).zip
    archive
    arrow-up-right-from-squareOpen
    https://wazimap-ng.webflow.io/documentation#tooltipsarrow-up-right
    file-archive
    4MB
    wazimap-ng.webflow (point-mapper-dropdown-filters - 09152021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    https://wazimap-ng.webflow.io/documentation#point-mapperarrow-up-right
    file-archive
    3MB
    wazimap-ng.webflow (tutorial-modal-fix - 08312021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    file-archive
    3MB
    wazimap-ng.webflow (richdata-nav-fix).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    https://wazimap-ng.webflow.io/feature-previews/rich-data-nav-fix#toparrow-up-right
    file-archive
    3MB
    wazimap-ng.webflow (print-css-test).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    file-archive
    3MB
    wazimap-ng.webflow (z-depth-legend-table-margin-legend-fix - 07192021).zip
    archive
    arrow-up-right-from-squareOpen
    file-archive
    3MB
    wazimap-ng.webflow (rich-data-filter-feature-preview-purge - 07132021).zip
    archive
    arrow-up-right-from-squareOpen
    file-archive
    6MB
    wazimap-ng.webflow (remove-options-tooltip-add-filter-buttons - 07122021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    file-archive
    6MB
    wazimap-ng.webflow (map-download-disabled - 07052021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    file-archive
    6MB
    wazimap-ng.webflow (filter-disabled - 07052021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow Export
    file-archive
    6MB
    wazimap-ng.webflow (map-options-filters - 07022021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow Export
    https://app.gitbook.com/@openup/s/wazi-ng-technical/~/drafts/-Md_pTCg5RT4tBx8Ni2q/development-process/changelog#05-21-2021-additive-filteringarrow-up-right
    file-archive
    6MB
    wazimap-ng.webflow (table-cell-truncation - 07012021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow Export
    file-archive
    6MB
    wazimap-ng.webflow (location-tag-width - 06282021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow Export
    file-archive
    6MB
    wazimap-ng.webflow (svg-size-fix - 06222021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    file-archive
    6MB
    wazimap-ng.webflow (disbaled-fix - 06212021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    file-archive
    6MB
    wazimap-ng.webflow (map-options-truncation - 06112021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    file-archive
    6MB
    wazimap-ng.webflow (map-options-filters - 05252021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    file-archive
    6MB
    wazimap-ng.webflow (additive-filtering - 05212021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    https://wazimap-ng.webflow.io/feature-previews/additive-filters-05212021arrow-up-right
    file-archive
    5MB
    wazimap-ng.webflow (data-classes-and-attributes - 04292021).zip
    archive
    arrow-up-right-from-squareOpen
    file-archive
    5MB
    wazimap-ng.webflow (download-all-facilities - 04222021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    https://wazimap-ng.webflow.io/feature-previews/download-all-facilities-04222021arrow-up-right
    file-archive
    5MB
    wazimap-ng.webflow (dropdown-data-id - 04222021).zip
    archive
    arrow-up-right-from-squareOpen
    Webflow export
    file-archive
    5MB
    wazimap-ng.webflow (rich-data-tables - 04092021).zip
    archive
    arrow-up-right-from-squareOpen
    Rich data tables
    file-archive
    5MB
    wazimap-ng.webflow (03092021 - Location print and google maps).zip
    archive
    arrow-up-right-from-squareOpen
    Location print and google maps button
    https://wazimap-ng.webflow.io/feature-previews/google-map-link-02242021arrow-up-right
    file-archive
    5MB
    wazimap-ng.webflow (03012021 - explicit-print-button).zip
    archive
    arrow-up-right-from-squareOpen
    Explicit print button
    https://trello.com/c/qEwQAdRgarrow-up-right
    file-archive
    5MB
    wazimap-ng.webflow (03012021 - rich-data-location-buttons).zip
    archive
    arrow-up-right-from-squareOpen
    Rich data location buttons
    https://trello.com/c/iqyaamZjarrow-up-right
    https://trello.com/c/AllyWK4Narrow-up-right
    https://wazimap-ng.webflow.io/feature-previews/rich-data-location-button-03012021arrow-up-right
    file-archive
    4MB
    wazimap-ng.webflow (map-legend 02182021).zip
    archive
    arrow-up-right-from-squareOpen
    Map legend always showing
    https://trello.com/c/iqyaamZjarrow-up-right
    https://wazimap-ng.webflow.io/feature-previews/map-legend-02122021arrow-up-right
    file-archive
    4MB
    wazimap-ng.webflow (map-print-legend 02182021).zip
    archive
    arrow-up-right-from-squareOpen
    Map legend
    https://trello.com/c/iqyaamZjarrow-up-right
    file-archive
    4MB
    wazimap-ng.webflow (data-attributes - 01172021).zip
    archive
    arrow-up-right-from-squareOpen
    Hover menu data-attributes
    https://trello.com/c/m1uc8zOharrow-up-right
    file-archive
    4MB
    wazimap-ng.webflow (hover-menu-content-styling - 02162021).zip
    archive
    arrow-up-right-from-squareOpen
    Hover menu content styling
    https://wazimap-ng.webflow.io/feature-previews/chart-dropdown-percentage-02162021arrow-up-right
    https://trello.com/c/tNa3oDXM/654-provide-css-to-allow-hiding-of-the-chart-type-toggles-in-the-chart-context-menuarrow-up-right
    https://trello.com/c/KV0Yk0hR/655-fix-the-active-css-for-the-value-toggle-on-the-chart-context-menuarrow-up-right
    file-archive
    4MB
    wazimap-ng.webflow (tab-notice-map-title - 02122021).zip
    archive
    arrow-up-right-from-squareOpen
    Tab notice and map title adjustments
    https://trello.com/c/KqAdJ01zarrow-up-right
    https://wazimap-ng.webflow.io/feature-previews/map-title-14012021arrow-up-right
    https://trello.com/c/0tGnWAkNarrow-up-right
    https://wazimap-ng.webflow.io/feature-previews/tab-noticearrow-up-right
    Added generic modal (.generic-modal)
    Element structure
    .profile-indicator__version
    Added highlight element that notifies users as to where they can change options in future (.highlight)
    Loading state for title and show locations button
    location-facility__item_loading for loading indicator on each row
    Added margin below tables to improve spacing in rich data panel
    Hid icon by default
    Added .slide-info__introduction to all tutorial slides
    Preview of rich-data profile-indicator filters
    Preview of changes
    Preview of disabled state
    Example of issue
    Preview of fix
    Old console errors
    Console errors after fix
    Removed the filters content section from the map-options div
    Added map-options__filters_content to the styles panel for cloning
    Additive filter preview in rich-data panel
    Additive filter preview in map-options
    Preview of "Download all facilities" button
    Revised structure of the facilities header
    Preview of the table (component has the "load more rows" button and showing info hidden)
    Print button and google maps button
    .facility-info__print as a child of .facility-info__header
    Explicit print button within rich-data panel
    Structure of rich-data and print button
    Preview of new implementation
    Preview of the .point-legend showing as part of the non-print ui
    Structure of new .map-bottom-items div
    point-legend__remove added to point-legend
    print version of .point-legend without the remove button
    structure of .point-legend for print version
    map-print contents
    map-print in loading configuration
    Duplicated point-legend
    Preview of the change to the hover-menu structure
    Old approach
    New approach
    Tab notice in loading state
    https://wazi-rich-data.webflow.io/stylesarrow-up-right
    Overall page structure.
    The individual components ready to be cloned.
    The styled components for the rich data panel
    .section-link within a .section
    Example nav items
    Structure of the .rich-data-nav

    May 2022

    hashtag
    Social sharing

    Added social sharing content to the export including:

    • Social sharing image

    • Favicon

    • Social sharing description

    • Renamed the site title (Wazimap NG)

    February 2023

    hashtag
    02/15/2023 - Various mobile UI changes

    • Made the map chips scroll on mobile landscape

    • Adjusted positions of all panel toggles (except my views) on mobile landscape

    • Adjusted max width of data mapper and point mapper to ensure they fit on mobile. (max-width: 83vw)

    hashtag
    02/14/2023 - Rich data content max width

    December 2022

    hashtag
    2022-12-05 - Data mapper content list wrapper

    file-archive
    4MB
    wazimap-ng.webflow (data-mapper-content-wrapper-20221205).zip
    archive
    arrow-up-right-from-squareOpen

    hashtag
    2022-12-02 - Powered by Wazimap Removed

    March 2022

    hashtag
    Roll back tutorial content - 03/29/2022

    file-archive
    3MB
    wazimap-ng.webflow (tutorial-modal-rollback-02292022-reupload).zip
    archive
    arrow-up-right-from-squareOpen

    Changelog:

    • Rolled back the changes to the tutorial modal. Includes tutorial content.

    hashtag
    Remove dummy tutorial content - 03/28/2022

    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.

    hashtag
    Removal of dummy data charts - 03/24/2022

    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.

    Map components

    hashtag
    Location pins

    hashtag
    Google style pin

    file-image
    1KB
    google-style-marker.svg
    image
    arrow-up-right-from-squareOpen
    Google Style Marker

    This marker is based on the look of the google pin style and is able to accommodate icons and text abbreviations easily.

    hashtag
    Dimensions:

    At all zoom levels, this pin should be 21px wide.

    hashtag
    Usage:

    • 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

    hashtag
    White pin

    This marker is based on the look of the google pin style and is able to accommodate icons and text abbreviations easily.

    hashtag
    Dimensions:

    At all zoom levels, this pin should be 21px wide.

    hashtag
    Usage:

    • Only adjust the colour of the element with the specified colour (#4693EF)

    • Text abbreviations within the marker should be 8px

    November 2021

    hashtag
    11/04/2021 - Facilities content toggles

    hashtag
    Changes:

    December 2021

    hashtag
    Map panels update - 12/10/2021

    hashtag
    Changes:

    @media print {
      .page-break-before {
        break-before: page;
      }
    
      body, .main, .rich-data, .rich-data-content, .rich-data-content .section, .profile-indicator{
        float: none !important;
        display: block !important;
      }
    
      .rich-data__print, .tab-notice{
        display: none;
      }
    
      .rich-data[style*="display: none;"]{
        display: none !important;
      }
    
      .facility-info[style*="display: none;"]{
        display: none !important;
      }
    
      .facility-info[style*="display: flex;"]{
        display: inline-table;
        position: absolute;
        width: 100%;
        height: 100%;
        top: 0;
        left: 0;
        background-color: #fff;
        max-height: 100%;
        max-width: 100%;
      }
    
      .facility-info__view-google-map, .facility-info__print, .facility-info__close{
        display: none;
      }
    }
    .mapping-options__filter.disabled {
    	pointer-events: none;
      }
      .profile-indicator__table_cell {
    			width: 100%;
    			white-space: nowrap;
    			overflow: hidden;
    			text-overflow: ellipsis;
    	}
    $ give me super-powers
    hello.sh
    # Ain't no code for that yet, sorry
    echo 'You got to trust me on this, I saved the world'
    file-archive
    4MB
    wazimap-ng.webflow (social-sharing-05162022-updated).zip
    archive
    arrow-up-right-from-squareOpen
    file-archive
    4MB
    wazimap-ng.webflow (mobile-ui-improvements).zip
    archive
    arrow-up-right-from-squareOpen
    file-archive
    4MB
    wazimap-ng.webflow (rich-data-content-max-width).zip
    archive
    arrow-up-right-from-squareOpen

    Added .is--shown and .is--hidden for the .location__facilities_content element.

  • The .is--hidden class is applied as default.

  • Removed the interaction that used to hide and show this content.

  • file-archive
    4MB
    wazimap-ng.webflow (facilities-content-toggle).zip
    archive
    arrow-up-right-from-squareOpen
    Removed "municipality" dummy text from ".map-tooltip__geography-chip"
  • Removed "location type" dummy text from ".location-tag__type"

  • file-archive
    4MB
    wazimap-ng.webflow (remove-tutorial-dummy - 03282022-reupload-4).zip
    archive
    arrow-up-right-from-squareOpen
    file-archive
    4MB
    wazimap-ng.webflow (removal-of-dummy-data-03242022-reupload).zip
    archive
    arrow-up-right-from-squareOpen
    file-archive
    4MB
    wazimap-ng.webflow (powered-by-wazimap-removed).zip
    archive
    arrow-up-right-from-squareOpen

    Testing guidelines

    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.

    hashtag
    red, green, refactor

    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.

    hashtag
    What to test

    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.

    hashtag
    Always test edge cases.

    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.

    hashtag
    Unit tests

    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.

    hashtag
    Integration Tests

    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

    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.

  • hashtag
    Various changes - 12/03/2021

    hashtag
    Map credit:

    Map credit in the bottom left corner.
    • Added .map-credit to .map.

    • This item is shown by default and the link directs to Wazimap NG product page.

    hashtag
    Point filters:

    Point filters closed by default
    • 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.

    hashtag
    Data mapper:

    • Added .map-options__no-data to the .map-options__filters_content

    • .map-options__no-data is hidden by default

    file-archive
    4MB
    wazimap-ng.webflow (map-panels - 12102021).zip
    archive
    arrow-up-right-from-squareOpen
    file-archive
    4MB
    wazimap-ng.webflow (various-changes-12032021).zip
    archive
    arrow-up-right-from-squareOpen
    file-image
    867B
    white-marker.svg
    image
    arrow-up-right-from-squareOpen
    White Marker

    October 2021

    hashtag
    10/26/2021 - Point mapper icon colour fix

    file-archive
    4MB
    wazimap-ng.webflow (point-mapper-colour-fix - 10262021).zip
    archive
    arrow-up-right-from-squareOpen

    hashtag
    Changed:

    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

    hashtag
    10/21/2021 - Point mapper truncation

    hashtag
    Changed:

    Fixed truncation on

    • Fixed truncation on point mapper themes and cat

    Translation

    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 .

    In a loop,

    • We check all the elements that have .18n

    file-archive
    4MB
    wazimap-ng.webflow (theme-truncation - 10212021).zip
    archive
    arrow-up-right-from-squareOpen

    NGPx - Template

    Proposed by:

    Date:

    Status: Proposed

    hashtag
    Context

    hashtag
    Proposed Solution 1

    hashtag
    Benefits

    hashtag
    Disadvantages

    hashtag
    Proposed Solution 2

    hashtag
    Benefits

    hashtag
    Disadvantages

    hashtag
    Other implications

    hashtag
    Proposed Solution 3

    hashtag
    Benefits

    hashtag
    Disadvantages

    hashtag
    Resolution

    This problem was finally addressed by ...

    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 :

  • //sample config
    translations: {
        'en': {
          'Point Mapper': 'Services'
        }
      },
    <!-- sample html -->
    <div data-i18n="Point Mapper" id="test" class="i18n">Point Mapper</div>
    translations config
    <div id="test" class="i18n">Services</div>

    Iconography

    Guidance for decision making around choosing the correct icon and using it in a way that makes sense.

    hashtag
    Icon choice

    When choosing an icon, the following things should be taken into consideration.

    1. The icon should be generalized and not reliant on knowledge of a specific language or region.

    2. The icon should follow established norms for icons of it's type in similar applications.

    3. When the choice deviates from a norm, it should only be done for very good reasons.

    4. Icons should be made distinct enough from others in the UI

    hashtag
    Some examples:

    hashtag
    Size

    Icons should generally have the following size properties:

    1. Icons should be no larger than 24px wide and 24px tall.

    2. Icons that are smaller than 24px in size, should fit within a bounding box that is 24px.

    3. This is generally the approach taken for icon sets like and .

    hashtag
    Visual weight

    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.).

    Heroku Review Apps

    hashtag
    Creating a review app

    1. Login to the heroku dashboard

    2. Go to the wazimap pipeline

    3. 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.

    hashtag
    Connecting a netlify frontend deploy preview to backend review app

    • 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

    hashtag
    Creating staging apps in heroku

    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.

    hashtag
    Create a new app

    We name apps according to the pull request number, e.g. wazimap-pr-1234 for PR #1234

    hashtag
    Steps for creating an app via CL

    • 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

    hashtag
    Steps for creating an app via GUI

    • Login into Heroku Web

    • Go to wazimap pipeline

    • After opening wazimap pipeline there is an option to add new staging app

    hashtag
    Steps for setting up the stack

    • heroku stack:set container --app wazimap-pr-1234

    • Adding Addons:

      • heroku addons:add heroku-postgresql:hobby-dev --app wazimap-pr-1234

    hashtag
    Deploying branch

    • git add and commit everything you want to deploy

    • git push heroku-pr-1234 BranchName:master

    hashtag
    Post-release step

    Start Worker dynos

    Via CLI:

    heroku ps:scale worker=1 --app wazimap-pr-1234

    Via GUI:

    • 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)

    • heroku run python3 manage.py migrate --app wazimap-pr-1234

    • heroku run python3 manage.py loaddata demodata.json --app wazimap-pr-1234

    hashtag
    Usefull commands

    • 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

    hashtag
    Connecting backend review app to deploy preview app

    • 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

    hashtag
    Destroy review apps when branches are merged

    heroku apps:destroy wazimap-pr-1234

    GUI Tests

    hashtag
    File structure

    • 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_modalfolder(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.

    hashtag
    Running the tests

    • 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

    hashtag
    Cypress dashboard

    • 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 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

    • More useful details(Average run duration, Top failures, Slowest tests...) could be found in Analytics menu of the dashboard.

    NGP2 - Presenting Geographical Hierarchies to users

    NG Proposal 2

    Representing geographical hierarchies to the user.

    Proposed by: Adi Eyal

    Date: 2020-09-02

    Status: Work in progress

    hashtag
    Context

    Read about Geography Hierarchies .

    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.

    hashtag
    Approach 1

    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.

    hashtag
    Approach 2

    NGP3 - Change Geography Hierarchies

    hashtag
    NG Proposal 3 - Geography Hierarchies

    Proposed by: Adi Eyal

    Date: 2020-09-06

    Status: Work in progress

    hashtag
    Context

    Read about Geography Hierarchies .

    hashtag
    Current Challenges

    hashtag
    Sharing levels across hierarchies

    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.

    hashtag

    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.

    hashtag
    Insufficient namespacing

    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.

    hashtag
    Version of ancestors

    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.

    hashtag
    Search within a profile should be restricted to the part of the hierarchy they care about

    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.

    hashtag
    Proposed Solution

    hashtag
    Sharing levels across hierarchies

    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. is currently being used to manage hierarchies using . 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.

    hashtag
    Multiple parents

    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.

    .

    hashtag
    Recommendation

    Materialarrow-up-right
    FontAwesomearrow-up-right
    Poor icon choice: An example of a legacy icon that was poorly chosen as it is specific to a region and not generalized.
    Good icon choice: Example of an icon that follows regular norms and is widely understood.
    A 24px bounding box with an icon that is actually smaller than 24px wide.
    When an icon appears multiple times in the UI, it needs to be given less visual weight (left) in order to not overwhelm the user and attract undue attention (right). In this case, the icon was shaded in a lighter colour.
    Download is given similar weight to zoom in/out when placed in the same context as "map options"
    Core functions have a larger button size and are given more space and prominence.

    NGP5 - Multiple count columns

    hashtag
    Stub page

    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

    and enter
    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

  • 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

  • heroku run python3 manage.py createsuperuser --app wazimap-pr-1234 if you want to create new supruser
    Checking logs
    • worker logs : heroku logs --dyno=worker --app wazimap-pr-1234 --tail

    • web logs: heroku logs --dyno=web --app wazimap-pr-1234 --tail

    and enter
    deploy-preview-542--wazimap-staging.netlify.apparrow-up-right
    deploy-preview-542--wazimap-staging.netlify.apparrow-up-right

    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

    By selecting WazimapNG in the Projects page, all the latest test runs could be viewed.

    Specs : Screenshots, videos... etc

    https://dashboard.cypress.io/arrow-up-right
    Projects page
    Latest test runs of WazimapNG project
    Analytics
    here
    here
    Treebeardarrow-up-right
    Materialized Path Treesarrow-up-right
    Province boundaries are identical but used in different hierarchies
    Children cannot have two different types of parents
    Sibling Xhariep of parent Mangaung overlaps hexagons when hovering
    Proposed new database structure

    NGP7 - Wazimap profile domain management

    Proposed by: JD Bothma

    Date: April 2022

    Status: Spike probable solution

    hashtag
    Context

    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:

    1. 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

    hashtag
    Problems with this approach:

    • 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

    hashtag
    Anticipated scale over the next 1-3 years:

    • 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.

    hashtag
    Relevant :

    • 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.

    hashtag
    Limits on Netlify:

    • 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)

    hashtag
    Client wants/needs

    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 being the domain

    hashtag
    Similar functionality on competitors/related tools

    Product
    Subdomains
    Custom domains

    hashtag
    Proposed Solution 1 - Only allow subdomains of our base domain

    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

    hashtag
    Benefits

    hashtag
    Disadvantages

    hashtag
    Proposed Solution 2 - Allow subdomain of our base domain for new sites, and legacy custom domains

    Requirements:

    • Same as solution 1 for wildcard DNS/cert/proxying

    • The solution 1 requirements for about 5 custom domains

      • Supported by dokku

    hashtag
    Benefits

    hashtag
    Disadvantages

    hashtag
    Other implications

    hashtag
    Proposed Solution 3 - subdomains by default, option of custom domains on dokku added manually

    Approach

    • DNS

      • Wildcard DNS for subdomains of our base domain

      • Any DNS CNAME to to our frontend web server

    Adding/removing a domain manually would entail

    1. Add the domain to the profile in Admin

    2. Add CNAME record pointing to the server, or ask the client to do so

    3. SSH to the server

    For 20 custom domains, we might have to do this on average 30 times or just over once every 2 weeks.

    triangle-exclamation

    Beware cert renewal errors.

    Can one hostname break renewal for all or is that one skipped?

    We can monitor expiry.

    hashtag
    Benefits

    hashtag
    Disadvantages

    hashtag
    Proposed Solution 4 - subdomains by default, option of custom domains on dokku automated

    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

    hashtag
    Benefits

    hashtag
    Disadvantages

    hashtag
    Proposed Solution 5

    hashtag
    Benefits

    hashtag
    Disadvantages

    hashtag
    Resolution

    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.

    hashtag
    Further reading

    NGP6 - Profile-specific open graph metadata

    Proposed by: JD Bothma

    Date: 2022-04-21

    Status: Spike probable solution

    hashtag
    Context

    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.

    Open Graph Metadata also help SEO. Additional content that can help SEO is the introductory content currently located on the profile landing pages.

    hashtag
    Current state

    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.

    hashtag
    Levels of achievement

    In ascending order of value:

    1. Better hardcoded metadata (get rid of the webflow favicon, ensuring webflow changes don't introduce odd things like "2021" in the title

    2. Profile-specific metadata, e.g. "Youth Explorer" or "Vulekamali Geospatial data viewer" in the title, branding and screenshot in the image, etc.

    3. Profile- and geography-specific metadata, e.g. "Tswhane - Youth Explorer" or "Eastern Cape - Vulekamali Geospatial".

    We're aiming around level 2 for now.

    Level 3 depends on changing from fragment identifier (#geo:CPT) to querystring (?geo=CPT)

    hashtag
    Proposed Solution 1 - Server-side rendering (SSR) for the frontend app using node.js in a dokku app

    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.

    A prerequisite for next.js would be replacing webflow or horrid hacks to import our webflow export as a .

    hashtag
    Benefits

    • 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

    hashtag
    Disadvantages

    • Do we have experience of serving stuff with node.js in prod?

    hashtag
    Proposed Solution 2 - Server-side rendering for the frontend app using node.js on netlify and similar

    Same as above, but on a (not so?)-static hosting platform like netlify or Cloudflare apps instead of a dokku server.

    hashtag
    Benefits

    hashtag
    Disadvantages

    hashtag
    Other implications

    • 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

    hashtag
    Proposed Solution 3 - Serving HTML, CSS and JS using the (currently) backend django app

    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.

    hashtag
    Benefits

    • We have lots of experience of this

    • no additional api requests to serve / - it can access django models immediately.

    hashtag
    Disadvantages

    • 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.)

    hashtag
    Resolution

    Proposal:

    1. Quick win with minimal interruption:

      1. Use minimal server-side templating like express-handlebars to query and render profile details

      2. enables all achievement levels but certainly 2-3

    Loading new geographies

    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).

    circle-info

    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.

    hashtag
    Simplifying boundary files

    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.

    hashtag
    Loading shapefiles into Wazimap

    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.

    hashtag
    Creating a Geography Hierarchy

    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/.

    hashtag
    Creating a profile

    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.

    hashtag
    Loading datasets

    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.

    An explanation of the most important classes and how they relate to Datasets can be found .

    NGP1 - Changing the data model (Implemented)

    hashtag
    NG Proposal 1 - Changing data model

    Proposed by: Adi Eyal

    Date: 2020-08-31

    Status: Proposed

    hashtag
    Context

    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:

    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:

    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.

    Table 3

    In this case we have two universes which overlap but we still want to be able to compare them:

    hashtag
    Proposed Solution 1

    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.

    Table 4

    This solution will require significant changes to the following components:

    • Data Import

    • Variable Creation (data is aggregated at this level)

    • API

    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.

    hashtag
    Benefits

    • 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.

    hashtag
    Disadvantages

    • 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.

    hashtag
    Proposed Solution 2

    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:

    Table 5

    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

    hashtag
    Benefits

    • 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.

    hashtag
    Disadvantages

    • 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.

    hashtag
    Other implications

    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.

    hashtag
    Proposed Solution 3

    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.

    hashtag
    Benefits

    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.

    hashtag
    Disadvantages

    Data re-use is limited. Datasets provide opportunities to create multiple indicators. The superindicator concept allows even more mixing and matching of indicators.

    hashtag
    Resolution

    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.

    Loading Data

    Guides for loading data can be found here: https://app.gitbook.com/@openup/s/wazimap-ng/arrow-up-right

    Below are some direct links:

    • Creating datasets: https://app.gitbook.com/@openup/s/wazimap-ng/data-admin/creating-datasetsarrow-up-right

    • Creating indicators: https://app.gitbook.com/@openup/s/wazimap-ng/data-admin/uploading-datasetsarrow-up-right

    • Creating universes:

    • Loading point collections:

    Creating a new profile

    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.

    hashtag
    Setting up the DNS

    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

    Creating a non-admin user for a private profile

    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:

    1. Navigate to the user creation page:

    2. Select a username and password (this will be a temporary password or can be generated from if you'd prefer to do so)

    NGP8 - Replacing Webflow as frontend framework

    Proposed by: JD Bothma

    Date: 2022-04-21

    Status: Spike probable solution

    hashtag
    Context

    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.

    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.

  • eventually consider full-blown server-side rendering next.js style

    1. enables pre-rendering javascript-drawn content, skipping several data roundtrips

    2. watch out: next.js is possibly not the right tool - apparently more page-content oriented than SPA.

    https://www.npmjs.com/package/mustache-expressarrow-up-right
    custom "Document"arrow-up-right
    NGP7 - Wazimap profile domain management.
    https://app.gitbook.com/@openup/s/wazimap-ng/data-admin/creating-universesarrow-up-right
    https://app.gitbook.com/@openup/s/wazimap-ng/data-admin/creating-point-collectionsarrow-up-right

    Changing the Geography level name

    If a geography label needs to change, e.g. Districts => Districts and Metros, follow the following steps:

    1. Geography.objects.filter(level="district", version="2011 Boundaries").update(level="Districts and Metros")Make sure you select the appropriate version

    2. Change the preferred_children object in the Profile configuration json for each profile that uses that geography hierarchy, e.g.:

    "preferred_children": {
        "country": [
          "province"
        ],
        "province": [
          "Districts and Metros",
          "municipality"
        ],
        "mainplace": [
          "subplace"
        ],
        "municipality": [
          "mainplace",
          "ward"
        ],
        "Districts and Metros": [
          "municipality",
          "mainplace",
          "ward"
        ]
      }

    3. from a python terminal run the following: from django.core.cache import cache; cache.clear()

    Done

    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

  • The Certificate is growing bigger and bigger. We aren't sure of the consequences of this.

  • Cloudflare pages requires apex domains for Pages sites to be zones on cloudflare

  • 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.

    Supported by netlify, dokku

  • Wildcard reverse proxying to the frontend web server

    • Supported by netlify, dokku

  • Not supported by netlify ("You can’t use domain aliases on a site with wildcard subdomains enabled"arrow-up-right)

    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.zaarrow-up-right 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

  • Add domain to dokku/nginx vhost
  • Renew TLS certificate

  • We can monitor the renewal cron output.

    -> Urgent manual devops action until we automate.

    ...Automation to renew certificate
  • ...Automation to let admin know it's all up and running or feed back errors

  • Power BI

    Carto

    Let's Encrypt limitsarrow-up-right
    youthexplorer.org.zaarrow-up-right
    gdc-projects.org.zaarrow-up-right
    http://whowhatwhere.wazimap.co.za/arrow-up-right
    https://discuss.httparchive.org/t/san-certificates-how-many-alt-names-are-too-many/1867arrow-up-right
    Someone exploring many of the same questions on Netlifyarrow-up-right

    Tableau

    95

    Employed

    80,000

    Western Cape

    15-35

    Unemployed

    120,000

    Front-end data model
  • Front-end visualisations

  • Front-end visualisations

    Geography

    Preferred hand

    Count

    Western Cape

    Left

    30

    Western Cape

    Right

    50

    Eastern Cape

    Left

    80

    Eastern Cape

    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

    Geography

    Age Group

    Employment Status

    Count

    Western Cape

    15-24

    Employed

    40,000

    Western Cape

    12-24

    Unemployed

    60,000

    Western Cape

    Geography

    Access to drinking water

    Year - 2016

    Year - 2017

    Western Cape

    Have access to drinking water

    30,000

    35,000

    Geography

    Age Group

    Employment Status

    Count

    Western Cape

    15-24

    Employed

    40,000

    Western Cape

    12-24

    Unemployed

    60,000

    Geography

    Age Group

    Employment Status

    Count

    Western Cape

    15-35

    Employed

    80,000

    Western Cape

    15-35

    Unemployed

    120,000

    Right

    15-35

    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.zaarrow-up-right that redirects to Netlify which is currently hosting the Wazi-NG frontend.
    Adding a CNAME DNS entry

    hashtag
    Setting up the frontend

    Add the new domain name to the Netlify configuration. Find the configuration on this pagearrow-up-right.

    Create a new domain alias on Netlify

    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.

    hashtag
    Setting up the backend

    The backend is also simple to create. Login at https://api.wazimap.com/adminarrow-up-right

    and add a new profile https://api.wazimap.com/admin/profile/profile/add/arrow-up-right

    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/arrow-up-right to see if everything is setup. It should look like the image below.

    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.

    1. Add details under Personal Information (optional)

    2. Under Permissions, DO NOT assign Staff or Superuser status!

    3. 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

    https://api.wazimap.com/admin/auth/user/add/arrow-up-right
    https://passwordsgenerator.net/arrow-up-right
    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.

    hashtag
    Current components

    (n) means there can be multiple instances

    • Page header

      • Logo

      • Search box

      • Tutorial button

    • Panels (on the left)

      • Rich Data View

        • Point data summary

    • Map

      • Map Download button

    • "Map Chip" (Non-modal fixed dialogue for data mapper controls)

    • "Point filters dialogue"

    • Breadcrumbs / Profile highlights container

    • Feedback button

    hashtag
    Upcoming components

    hashtag
    Libraries and tech to consider

    • Web components

    • https://polymer-library.polymer-project.org/3.0/docs/devguide/feature-overviewarrow-up-right

      • Maintenance mode, referring to Lit.

      • is a github org of component repos

      • IMPORTANT: Material Web is a work in progress and subject to major changes until 1.0 release.

    hashtag
    Spike

    • Try the MUI react material UI component libraryarrow-up-right

    • Try the Semantic UI component libraryarrow-up-right with React integration

    • 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?

    hashtag
    Resolution

    • 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

    hashtag
    See also

    • Using the same Redux store for multiple React instancesarrow-up-right (in our case this is probably desirable)

    here
    here

    Profile Indicator Configuration

    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-formatarrow-up-right. The default formatting for Valueis ~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.

    hashtag
    Default filters

    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:

    hashtag
    Excluding profile indicators

    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.

    hashtag
    Examples

    hashtag
    Value only, formatted as percentage

    For pre-calculated percentages, usually with absolute choropleth method.

    hashtag
    Values formatted as whole numbers with comma thousand separator

    Ideal for population data, were you would like a thousand separator and whole numbers.

    NGP4 - Format configuration

    hashtag
    NG Proposal 4 - Format configuration

    Proposed by: Adi Eyal

    Date: 2020-09-14

    Status: Work in progress

    hashtag
    Context

    Wazimap-NG Indicators contain different types of numbers that need to be formatted. Some 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.

    hashtag
    Proposed Solution 1

    Each Indicator may require its own specialised formatting. To achieve

    hashtag

    hashtag
    Proposed Solution 1

    hashtag
    Proposed Solution 2

    hashtag
    Benefits

    hashtag
    Disadvantages

    hashtag
    Other implications

    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.

    hashtag
    Recommendation

    Upload API

    How to upload new and update existing datasets

    hashtag
    Authorization

    Authoritation is done in the header via an user-based token. The token can be generated via the Admin Interface

    hashtag
    Create new dataset

    POST /api/v1/datasets/

    hashtag
    Headers

    Name
    Type
    Description

    hashtag
    Request Body

    Name
    Type
    Description

    hashtag
    Update existing dataset

    hashtag
    Parameters

    hashtag
    Example Response

    hashtag
    Example

    January 2022

    hashtag
    Class renaming - 01/18/2022

    file-archive
    4MB
    wazimap-ng.webflow (class-renaming - 01182022).zip
    archive
    arrow-up-right-from-squareOpen

    Changed class name .hdden to .hidden for .mapping-options__add-filter.

    hashtag
    Transition CSS - 01/14/2022

    Added transition styling to the v2 data mapper content items and made max-height on closed !important.

    hashtag
    Data mapper v2 - 01/13/2022

    hashtag
    Changes:

    • Added v2 versions of all clickable triggers and content blocks:

      • data-category__h1_trigger--v2

      • data-category__h1_content--v2

    circle-exclamation

    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.

    {
      "urls": [
        "covid.openup.org.za"
      ]
    }
    {
      "urls": [
        "beta.youthexplorer.org.za",
        "localhost"
      ],
      "preferred_children": {
        "country": [
          "province"
        ],
        "district": [
          "municipality",
          "mainplace",
          "ward"
        ],
        "province": [
          "district",
          "municipality"
        ],
        "mainplace": [
          "subplace"
        ],
        "municipality": [
          "mainplace",
          "ward"
        ]
      }
    }
    {
        "types": {
            "Value": {"formatting": "~s", "minX": 0, "maxX": 5000},
            "Percentage": {"formatting": ".0%", "minX": 0, maxX: 100}
        },
        "disableToggle": false,
        "defaultType": "Percentage",
        "xTicks": 2,
        "filter": {
            "defaults": [
                {
                    "name": "income sources",
                    "value": "Locally generated(%)"
                }
            ]
        },
    }
    Authorization : Token yourtoken
    Profile Administrators
    https://storymaps.com/arrow-up-right
    https://storymaps.arcgis.com/arrow-up-right
    https://www.arcgis.com/arrow-up-right

    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)

  • https://github.com/PolymerElementsarrow-up-right
    https://lit.dev/arrow-up-right
    https://semantic-ui.com/arrow-up-right
    https://material.io/components?platform=webarrow-up-right
    https://github.com/material-components/material-webarrow-up-right

    General API Information

    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

    Task Status

    API Endpoint for checking the status of a task

    Check the status of a background task via API.

    GET /api/v1/tasks/:id/

    hashtag
    Response

    {
      "id": ":task_id",
      "status": "running|error|success",
    }
        "filter": {
            "defaults": [
                {
                    "name": "income sources",
                    "value": "Locally generated(%)"
                }
            ]
        },
      "exclude": [
        "data mapper"
      ]
    {
        "types": {
            "Value": {"formatting": ".1%"}
        },
        "disableToggle": true,
        "defaultType": "Value"
    }
    {
      "types": {
        "Value": {
          "formatting": ",.0f"
        }
      }
    }
    POST /api/v1/datasets/:id/upload/
    {
        "id": 78,
        "created": "2021-04-28T09:57:01+0000",
        "updated": "2021-04-28T09:57:10+0000",
        "name": "Cases",
        "groups": [
            "date"
        ],
        "permission_type": "private",
        "profile": 1,
        "geography_hierarchy": 1,
        "upload_task_id": "44ceb2d8acd04343bbcb7ca8f0468b6c",
    }
    curl --location --request POST 'http://api.wazimap.com/api/v1/datasets/84/upload/' \
    --header 'Content-Type: application/x-www-form-urlencoded' \
    --header 'Authorization: Token yourtoken' \
    --form 'file=@/covid_next.csv' \
    --form 'update=True' \
    --form 'overwrite=True'

    data-category__h2_trigger--v2

  • data-category__h2_content--v2

  • data-category__h3_trigger--v2

  • data-category__h3_content--v2

  • file-archive
    4MB
    wazimap-ng.webflow (transition-css-01142022).zip
    archive
    arrow-up-right-from-squareOpen
    file-archive
    4MB
    wazimap-ng.webflow (data-mapper-v2-01132022).zip
    archive
    arrow-up-right-from-squareOpen
    New item in the styles panel. Select data-category--v2 to use class controlled interactions

    Profile Configuration

    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://api.wazimap.com/admin/profile/profile/arrow-up-right

    hashtag
    Full profile configuration example

    Here is an example of how such a configuration might look.

    hashtag
    urls

    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.

    hashtag
    page_title

    page_title overrides the title on the frontend.

    hashtag
    chart_attribution

    The chart atttribution variable controls the attribution text on images of downloaded charts. In the example below attribution is set to "South Africa"

    hashtag
    choropleth

    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

    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

    hashtag
    preferred_children

    hashtag

    Previous versions of Wazimap assumed a linear geography hierarchy. The current version allows for a . 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.

    hashtag
    Side panel configuration

    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.

    hashtag
    Customising the tutorial text

    It is possible to customise the text and images for the tutorial by added the "tutorial" key as shown below

    hashtag
    Leaflet Options

    It's possible to use Leaflet configuration options as described here: .

    hashtag
    Custom styles

    Custom styles can be injected through the profile, e.g.:

    hashtag
    To be cleaned up

    • 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

    hashtag
    Translations

    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"

    hashtag
    Translatable Keys :

    • 'No filters available for the selected data'

    • 'No facilities for this location'

    • 'locations'

    hashtag
    Enabling / Disabling Point Marker Clustering

    Clustering is disabled in default. Adding the JSON below to the profile config enables clustering.

    • When clustering is disabled :

    • When clustering is enabled :

    hashtag
    Watermark

    Watermark can be enabled/disabled by

    default value is true

    hashtag
    Adding Cc License

    Cc license(© 2023. This work is openly licensed via ) can be added to the page by

    default value is false

    hashtag
    Enabling Point Search By Distance

    Point search by distance can be enabled/disabled by

    default value is true

    hashtag
    Site-wide Filters

    Site-wide filters can be enabled/disabled by

    default value is true

    hashtag
    Default Filters

    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.

    hashtag
    Restricting Filter Values

    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.

    hashtag
    View Data Whitelists

    Default filters and restricting values can be done view-based. This config will override the global default_filtersand 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}. urlmust 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

    hashtag
    Default View Label

    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"

    hashtag
    Linking Tabular Comparison

    Tabular comparison link can be added using the tabular_link_enabled

    Default value for tabular_link_enabled is false.

    Geographies, hierarchies and versions

    Shapefiles to load geographies and boundaries into geography hierarchies

    See also discussion of architecture.

    hashtag
    Hierarchy and profile configuration considerations

    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.

    {
      "urls": [
        "wazimap-ng.africa"
      ],
      "choropleth": {
        "zero_color": "#eeeeee",
        "negative_color_range": [
          "#0a3d62",
          "#82ccdd"
        ],
        "positive_color_range": [
          "#fef0d9",
          "#b30000"
        ],
        "opacity": 0.7,
        "opacity_over": 0.8
      },
      "watermark_enabled": true,
      "tile_layers": [
        {
          "url": "https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}.png",
          "pane": "labelsPanel",
          "zIndex": 650
        },
        {
          "url": "https://{s}.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}.png",
          "pane": "tilePane",
          "zIndex": 200
        }
      ],
      "layer_styles": {
        "selected": {
          "out": {
            "color": "#666666",
            "weight": 1,
            "opacity": 0.5,
            "fillColor": "#cccccc",
            "fillOpacity": 0.5
          },
          "over": {
            "color": "#666666",
            "fillColor": "#3BAD84",
            "fillOpacity": 0.6
          }
        },
        "hoverOnly": {
          "out": {
            "stroke": false,
            "fillColor": "#ffffff"
          },
          "over": {
            "fillColor": "3BAD84",
            "fillOpacity": 0.5
          }
        }
      },
      "default_panel": "point_data",
      "root_geography": "CPT",
      "site_wide_filters_enabled": false,
      "leaflet_options": {
        "minZoom": 6
      },
      "chart_attribution": "See more at https://wazimap.ng"
    }

    example config negative: [dark blue, light blue] positive: [light brown, dark brown] zero: white

  • 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)

  • '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'

  • tree-like structure
    https://leafletjs.com/reference-1.7.1.html#map-l-maparrow-up-right
    CC BY-NC-ND 4.0arrow-up-right
    tabular comparison link

    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 configured as "child types" of their parent in the profile configuration to determine their priority for selection as the current child type shown on the map when their parent geography is selected by the user.

    hashtag
    Discussion

    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.

    hashtag
    Shapefiles

    These are already simplified and have the right fields to import to Wazimap.

    See the tutorial for how to format with the appropriate attributes and simplify shapefiles similarly. It also demonstrates uploading.

    hashtag
    South Africa 2020 demarcation

    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

    hashtag
    South Africa 2016 demarcation

    • 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

    hashtag
    South Africa 2011 demarcation

    hashtag
    Equal area hexagons (Uber H3 resolution 7)

    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.

    Geography Hierarchies
    file-archive
    3MB
    2021-demarcation.zip
    archive
    arrow-up-right-from-squareOpen
    file-archive
    98MB
    2016-demarcation.zip
    archive
    arrow-up-right-from-squareOpen
    file-archive
    579KB
    hexagons-metro-children.zip
    archive
    arrow-up-right-from-squareOpen

    Point data API

    Point data is arranged in the following structure below a given Wazimap NG Profile:

    • Themes

      • Categories

    "urls": [
        "wazimap-ng.africa"
    ]
    "page_title": "This is a new title"
    "choropleth": {
      "positive_color_range": [
        "#fef0d9",
        "#b30000"
      ],
      "zero_color": "#eeeeee",
      "negative_color_range": [
        "#0a3d62",
        "#82ccdd"
      ],
      "opacity": 0.7,
      "opacity_over": 0.8
    }
    "preferred_childre": {
        "country": [
            "province", "state"
        ],
        "province": [
            "district",
            "metro"
        ],
        "municipality": ["mainplace", "ward"],
        ...
    }
    
    {
        "panels": {
            "rich_data": {
                "visible": true
            },
            "point_data": {
                "visible": true
            },
            "data_explorer": {
                "visible": false
            },
        },
        "default_panel": "rich_data"
    }
    "tutorial": [
        {
          "body": "This website was developed to enable you to easily find spatial information related to geographic areas. By clicking on the map or using the search bar, you can navigate to an area of interest and find relevant information about it.",
          "image": "https://wazimap-ng.s3-eu-west-1.amazonaws.com/GCRO/Screenshot+1.jpg",
          "title": "Introduction:"
        },
        {
          "body": "To get started, first select a location on the map or use the search box to find the location you would like to analyse.",
          "image": "https://wazimap-ng.s3-eu-west-1.amazonaws.com/GCRO/Screenshot+2.jpg",
          "title": "Location Search:"
        },
        {
          "body": "Basic information for your selected location can be found in the Location Panel. Use the buttons to navigate to parent locations.",
          "image": "https://wazimap-ng.s3-eu-west-1.amazonaws.com/GCRO/Screenshot+3.jpg",
          "title": "Location Panel:"
        },
        {
          "body": "To view rich data specific to the location you selected, open the Rich Data panel in the left toolbar.",
          "image": "https://wazimap-ng.s3-eu-west-1.amazonaws.com/GCRO/Screenshot+4.jpg",
          "title": "Rich Data:"
        },
        {
          "body": "To add point data to the map, use the Point Mapper to show different facilities in that area.",
          "image": "https://wazimap-ng.s3-eu-west-1.amazonaws.com/GCRO/Screenshot+5.jpg",
          "title": "Point Mapper:"
        },
        {
          "body": "To overlay compatible datasets onto your location, open the Data Mapper and choose from the available indicators.",
          "image": "https://wazimap-ng.s3-eu-west-1.amazonaws.com/GCRO/Screenshot+6.jpg",
          "title": "Data Mapper:"
        },
        {
          "body": "Once you've mapped a data-set (eg. gender), use the Data Filter to select a sub-indicator for that data-set (eg. male)",
          "image": "https://wazimap-ng.s3-eu-west-1.amazonaws.com/GCRO/Screenshot+7.jpg",
          "title": "Data Filtering:"
        },
        {
          "body": "For more information on how to use specific features, look out for tutorial icons. For an in-depth guide on using this site, please see our user manual.",
          "image": "https://wazimap-ng.s3-eu-west-1.amazonaws.com/GCRO/Screenshot+7.jpg",
          "title": "Learn more:"
        }
      ]
    "leafletOptions": {
      "minZoom": 4
    }
    "style": {
        "location__search_input": [
            {
                "key": "background-color",
                "value": "blue"
            }
        ]
    }
    //sample config
    translations: {
        'en': {
          'Point Mapper': 'Services'
        }
      },
    
      "point_markers": {
        "clustering": {
          "enabled": true
        }
      },
      "watermark_enabled": true
    "cc_license_enabled": true
    "point_search_enabled": true
    "site_wide_filters_enabled": true
          "default_filters": [
            {
              "name": "language",
              "value": "English"
            },
            {
              "name": "gender",
              "value": "Female"
            }
          ]
          "restrict_values": {
            "age": [
              "15-35 (ZA)",
              "15-24 (Intl)",
              "30-35"
            ]
          }
      "views": {
        "youth": {
          "url": "https://deploy-preview-735--wazimap-production.netlify.app/?dev-tools=true&view=children&profileView=%7B%22filters%22%3A%5B%5D%2C%22hiddenIndicators%22%3A%5B2198%2C2206%2C2201%2C2205%2C2319%2C2320%2C2321%2C2322%2C2323%2C2324%2C2325%2C2330%2C2407%2C2408%2C2411%5D%7D",
          "label": "youth view",
          "order": 4,
          "default_filters": [
            {
              "name": "language",
              "value": "English"
            }
          ],
          "restrict_values": {
            "age": [
              "15-35 (ZA)",
              "15-24 (Intl)",
              "30-35"
            ],
            "race": [
              "Coloured",
              "Black African",
              "Indian or Asian"
            ],
            "language": [
              "English",
              "Afrikaans",
              "Some other"
            ]
          }
        }
      }
    {
        "default_view_label": "Test view"
    }
    {
        "tabular_link_enabled": true
    }

    Points

    A common way to fetch points is to

    1. Fetch the point themes for a profile, which includes their categories

    2. Fetch the points in a category.

    Example themes:

    Example point categories

    circle-info

    Profile Collections vs Categories

    Note that what is called Profile Collections in Admin is called Categories in the points API.

    circle-info

    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.

    hashtag
    Fetch Profile ID

    GET https://api.wazimap.com/api/v1/profiles/

    Profile id is required to make requests to points API. Get the ID of the profile from the Profile list.

    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

    hashtag
    Get Themes for a Profile

    GET https://api.wazimap.com/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://api.wazimap.com/api/v1/profile/8/points/themes/

    hashtag
    Path Parameters

    Name
    Type
    Description

    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

    hashtag
    Get Categories for a Theme

    GET https://api.wazimap.com/api/v1/profile/:profile_id/points/theme/:theme_id/profile_categories/

    Get all categories under a Theme

    hashtag
    Path Parameters

    Name
    Type
    Description

    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

    hashtag
    Get Points for a Category

    GET https://api.wazimap.com/api/v1/profile/:profile_id/points/category/:category_id/points/

    Get coordinates and location details for a category. Example request: GET https://api.wazimap.com/api/v1/profile/8/points/category/578/points/

    hashtag
    Path Parameters

    Name
    Type
    Description

    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

    hashtag
    Get Categories & Points for a Geography

    GET https://api.wazimap.com/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

    hashtag
    Path Parameters

    Name
    Type
    Description

    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

    hashtag
    Get Points for a Category within a Geography

    GET https://api.wazimap.com/api/v1/profile/:profile_id/points/category/:category_id/geography/:geography_code/points/

    Get all Points for a Category within a specific Geo Code

    hashtag
    Path Parameters

    Name
    Type
    Description

    profile_id

    number

    ID of the Profile

    category_id

    number

    ID for the Category

    geography_code

    string

    Geo Code for Geography

    {
        "count": 2,
        "next": "https://api.wazimap.com/api/v1/profiles/?page=2",
        "previous": null,
        "results": [
            {
                "id": :profile_id,
                "name": "Profile1",
                "permission_type": "public",
                "requires_authentication": false,
                "geography_hierarchy": {
                    "id": 2,
                    "name": "2016 SA Boundaries",
                    "root_geography": {
                        "name": "South Africa",
                        "code": "ZA",
                        "level": "country",
                        "version": "2016 Boundaries"
                    },
                    "description": ""
                },
                "description": "",
                "configuration": {}
            },
            {
                "id": :profile_id,
                "name": "Profile2",
                "permission_type": "public",
                "requires_authentication": false,
                "geography_hierarchy": {
                    "id": 2,
                    "name": "2016 SA Boundaries",
                    "root_geography": {
                        "name": "South Africa",
                        "code": "ZA",
                        "level": "country",
                        "version": "2016 Boundaries"
                    },
                    "description": ""
                },
                "description": "",
                "configuration": {}
            },
            
        ]
    }
    [
        {
            "id": :theme_id,
            "categories": [
                {
                    "id": :category_id,
                    "name": "Healthcare Facilities",
                    "description": "Representation of data gathered from various sources",
                    "theme": {
                        "id": :theme_id,
                        "name": "Health",
                        
                    },
                    "metadata": {
                        "source": "",
                        "description": "",
                        "licence": null,
                        "icon": "local_hospital"
                    },
                    "color": "",
                    "visible_tooltip_attributes": []
                }
            ],
            "created": "2020-08-28T07:36:30+0000",
            "updated": "2021-06-08T16:30:14+0000",
            "name": "Health",
            "icon": "local_hospital",
            "order": 1    ,
            "profile": :profile_id
        },
    ]
    [
        {
            "id": :category_id,
            "name": "Healthcare Facilities",
            "description": "Representation of data gathered from various sources",
            "theme": {
                "id": :theme_id,
                "name": "Health",
                "icon": "local_hospital"
            },
            "metadata": {
                "source": "",
                "description": "Representation of data gathered from various sources",
                "licence": null
            },
            "color": "",
            "visible_tooltip_attributes": []
        }
    ]
    {
        "type": "FeatureCollection",
        "features": [
            {
                "id": :location_id,
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [
                        26.643601,
                        -27.748028
                    ]
                },
                "properties": {
                    "data": [],
                    "name": "ALLANRIDGE",
                    "url": null,
                    "image": null
                }
            },
            {
                "id": :location_id,
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [
                        29.110443,
                        -22.680732
                    ]
                },
                "properties": {...}
            },
            {
                "id": :location_id,
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [
                        28.131317,
                        -26.318641
                    ]
                },
                "properties": {...}
            }
        ]
    }
    {
        count: 2,
        results: [
            {
                type: "FeatureCollection",
                features: [
                    {
                        "id": :location_id,
                        "type": "Feature",
                        "geometry": {
                            "type": "Point",
                            "coordinates": [
                                26.643601,
                                -27.748028
                            ]
                        },
                        "properties": {...}
                    },
                ],
                category: "Post Office service points"
            },
            {
                type: "FeatureCollection",
                features: [...],
                category: "SASSA Pay Points"
            }
        ]
    }
    {
        "type": "FeatureCollection",
        "features": [
            {
                "id": :location_id,
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [
                        27.877098,
                        -25.800849
                    ]
                },
                "properties": {
                    "data": [],
                    "name": "BROEDERSTROOM",
                    "url": null,
                    "image": null
                }
            },
            {
                "id": :location_id,
                "type": "Feature",
                "geometry":{...},
                "properties": {...}
            },
            {
                "id": :location_id,
                "type": "Feature",
                "geometry":{...},
                "properties": {...}
            }   
        ]
    }

    description

    TextField - Contains short intro about Profile

    configuration

    Profile configurations set up by profile admin

    categories

    List of sub-data that displays more information and points available for a Theme