Skip to content

unicef/magicbox-app

Repository files navigation

Magicbox App

This application is a WIP React/Redux frontend for the geospatial visualization web app component of UNICEF's Magicbox project, the UNICEF Office of Innovation/ICTD initiative around Data Science and Artificial Intelligence.

This application is a built to be an interface for geospatial insights from the data science team. The first prototype to be visualized using this software framework is a data science model of HDI estimation using Twitter data.

Running the application

Create a .env file in the root directory of the application with the content:

REACT_APP_MAPBOX_TOKEN=your_mapbox_token_goes_here

Run the command below in your terminal

make run

or simply

make

How does it work?

This application uses kepler.gl as a library to display our visualizations. Each page is composed using a “view”, a json config that stores:

  • data for the visualization components (“appConfig”)
  • map configuration (as generated by kepler; “mapConfig”)

A theme is a set of visual components that has all the information to correctly display the data associated with a view. A visualization is a set of views that uses the same theme. Each visualization is associated with one global view and many country views. Each visualization is associated with a specific dataset/insight (school-mapping, poverty-radar). Each view configuration is a set of attributes stored in the same json file configurations for the global view and countries that display. This json file contains two subsections, the “mapConfig” and the “appConfig”.

What is the “mapConfig”?

The “mapConfig” portion of the file is a json config that can be generated by kepler.gl using the save function of the Schema Manager.

The configuration could also be built using the kepler.gl browser interface and then exported (as the Schema Manager is what is happening under the hood). To export in a json format, on the kepler.gl side panel:

Share > Export Map > json > Export. 

This same portion of code is passed back to kepler.gl to render the map using addDataToMap() function of the kepler.gl library, and renders the map portion of the view:

In order for the mapConfig to work with the rest of the application, however, there are several portions of the config that have been changed in the visConfig of the layer. This allows customized design and user interactions while retaining the baseline kepler.gl functionality.

Custom Colors

kepler.gl has a variety of available color palettes that are both sequential and divergent, which allow a user to color by the values of their data. This config has been updated with custom hex values and uses the “divergent” type of gradient, even though the custom colors are sequential, because it allows for an override of the colors. These values are updated as needed for each layer.

Custom Interactions

The “id” for the layer is by default a randomly generated alphanumeric string. This value, along with the “isVisible”, is the way that the custom “Layer Toggler” (or any customized non-kepler component) can control the layers seen by the user. This id is used in both this config and in the appConfig (explanation following). For ease of use, we have replaced the random string with a descriptive name of the layer.

Code

Below is a snippet of the portion of the configuration that has been updated for this project to have both custom colors and interactions. The code in bold indicates code sections required for customization. (explain more about the layers and the difference between global/vs country, etc.)

"config": {
      "version": "v1",
      "config": {
        "visState": {
          "filters": [],
          "layers": [
            {
              "id": "estimate", // layer id, needed for custom interactions
              "type": "geojson",
              "config": {
                "dataId": "7ypkouio4",
                "label": "Estimated HDI",
                //...
                "isVisible": true, // layer visibility, needed for custom interactions
                "visConfig": {
                  "opacity": 1,
                  "thickness": 0.5,
                  "colorRange": {
                    "name": "Uber Viz Diverging 3.5",
                    "type": "diverging", // category of gradient, for custom colors 
                    "category": "Uber",
                    "colors": [ // custom colors, sequenced in order in the arrray
                      "#9e9e9e",
                      "#E4E6EA",
                      "#CACFE2",
                      "#B1B8DA",
                      "#97A1D2",
                      "#7E8ACA",
                      "#6473C2",
                      "#4B5CBA",
                      "#3145B2",
                      "#182FAB"
                    ],

What is the “appConfig”?

The "appConfig" part of the json file provides data to page elements that are not the kepler.gl map so the app can appropriately render each of these React components. The config contains the following kinds of data:

  • What content is displayed: Text that would appear in an "About" section? A chart or graph?
  • How content is displayed: A paragraph? A header? What color is it? (Note: the css lives in the component, but props are passed through this data)
  • How content is connected: Is there a link to an external source? Is a labeled radio button a way to toggle between the layers of the kepler.gl map?
  • Where content belongs: Which component on the page will be responsible for rendering the content?

The appConfig is intended to be flexible and completely customizable, so that the same redux architecture can handle many configurations with minimal changes. Below is a condensed “directory” of the current key-value pairs that are used in this application, with type and description for each value. The “dataInfo” and “sidePanel” directly correlate with these two React components.

Config Object Reference

{ 
  title: string; the name of the view; reference only (not rendered/visible)
  dataInfoOpen: boolean; controls DataInfo component state (open/closed)
  dataInfo: [ array of objects that compose the data info component
    {title: string; the title used in the lefthand vertical navigation tabs
     content: [ array of objects that compose the content sections
      { id: string; used as a unique key/reference for the content section
        header: string; text rendered as a header
        content: string; text rendered in a p tag
        contentGray: string; grey text rendered in a p tag, but is gray
        titleLink: string; header that is a clickable link (note: requires "href")
        href: string; link that will be used in conjunction with the titleLink
        emailLink: string; an email visible as text and is a mailto link 
      }
     ]
     order:integer; dictates the sequence of the navigation tabs
    }
  ]
  sidePanel: [ array of objects that compose side panel component and its children
    { component: string; name of the react component to which this data belongs (must match the file/class name of the component file(s))
      props: the props object for the component
        { title: string; title of the component (visible on the page, in component)
          popupContent: [ array of objects that composes a materialUI popover
            { content: string; text visible in open popover }
            { tag, content, href: strings; used to create an embedded link }]
          scaleTitle: string; title at the top of the scale component
          numericRange:[ array of integers for a 0-1 index/gradient scale ] 
          divergentRange:[ array of strings for legends with categories, not numbers ]
          layers :[ an array of objects, each of which creates a labeled radio button, used to toggle between layers of the kepler map
                { id: string; reference for which layer (note, this value is the same as is used in the kepler map, and must match exactly) 
                  label: string; text that is visible in the side panel}
              ]
        }
      order:integer; dictates the sequence of the component in the side panel
    }
  ]
}

Below are guides that indicate the location and style by which this data is rendered on the page.

DataInfo: Visual Guide

SidePanel: Visual Guide

Config, State, and Reducers

Below is the initial app state for the HDI/poverty version of this map. This can be fully customized, with keys/values added to created to support new React components.

// reducers.js 

const DEFAULT_DATASET_NAME = '';
const DEFAULT_PATH = '/';
const DEFAULT_TITLE = 'POVERTY MAP';

export const INITIAL_APP_STATE = {
  title: DEFAULT_TITLE,
  dataInfo: [],
  sidePanel: [],
  ui: {
    sidePanelOpen: true,
    dataInfoOpen: false,
    isLoading: false,
    loading: 1,
    error: null,
  },
  data: {
    country: null,
    path: DEFAULT_PATH,
    datasetName: DEFAULT_DATASET_NAME,
    dataset: null,
  },
};

The main content of the dataInfo and sidePanel are managed with those keys in the state component, and their visibility is controlled in the ui layer of the state.

How are routes handled?

Routes in this application indicates the dataset and view level selected to be displayed and is consistent across the stack with the magicbox-app-backend repository.

On the front end, the value of these routes is constructed in the reducer with the state values data.datasetName and data.path. We map the values to the routing convention so that the datasetName is the name of the theme (i.e. ‘/poverty-radar’ or ‘/school-mapping’) and the path to be a more specific view within the theme, (i.e. ‘/c/nepal’). The following table shows the patterns and the specific view to load.

Route View Example State Value (React App)
/ default global view path: DEFAULT_PATH,
datasetName: DEFAULT_DATASET_NAME
/:theme :theme in global view datasetName: ‘poverty-radar’
/:theme/c/:country :country level with given :theme path: ‘/c/nepal’,
datasetName: ‘poverty-radar’

The route can be constructed with any combination or absence of these two parameters, allowing flexibility to construct paths for a variety of use cases without causing errors. In particular, we imagine a future use case where there is a path but no theme, such as if a user creates their own map (route: ‘/u/:username’, path: ‘/u/mmaki’).

Component Structure/Relationships

For a comprehensive understanding of component structure, please refer to this component map.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages