├── .dockerignore
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .idea
├── .gitignore
├── inspectionProfiles
│ ├── Project_Default.xml
│ └── profiles_settings.xml
├── misc.xml
├── modules.xml
├── priorizador.iml
└── vcs.xml
├── .prettierrc
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yaml
├── flask_api
├── .dockerignore
├── Dockerfile
├── api-docs
│ ├── get_cestas_submissions.yml
│ ├── get_json.yml
│ └── get_kobo_submissions.yml
├── geojson_api.py
├── requirements.txt
├── test_geojson_api.py
└── utils.py
├── nginx_config
├── nginx.conf
└── priorizador.conf
├── package.json
├── public
├── codium-logo-white.svg
├── favicon.ico
├── index.html
├── isotipo-codium-white.svg
├── isotipo-reaccion-white.svg
├── logo192.png
├── logo512.png
├── manifest.json
├── priorizador-logo-cuadrado.png
├── priorizador_logo_1_blanco_horizontal.png
├── reaccion-logo-white.svg
├── reaccion.png
├── reaccion2.png
└── robots.txt
├── src
├── App.css
├── App.js
├── App.test.js
├── components
│ ├── About.js
│ ├── Analysis.js
│ ├── ColorScale.js
│ ├── CustomHeader.js
│ ├── CustomMap.js
│ ├── DataSourceSelector.js
│ ├── DistrictSelector.js
│ ├── InformationPanel.js
│ ├── Menu.js
│ └── TextSwitch.js
├── css
│ ├── about.css
│ ├── analysis.css
│ ├── colorscale.css
│ ├── menu.css
│ └── sidebar-v2.css
├── index.css
├── index.js
├── logo.svg
├── serviceWorker.js
└── setupTests.js
├── visualizations_dash
├── .idea
│ ├── .gitignore
│ ├── inspectionProfiles
│ │ ├── Project_Default.xml
│ │ └── profiles_settings.xml
│ ├── misc.xml
│ ├── modules.xml
│ ├── prototipo_visualizacion_dash.iml
│ └── vcs.xml
├── Dockerfile
├── Dockerfile.Celery
├── dash_main.py
├── flask_celery.py
└── requirements.txt
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | *-lock.json
2 | node_modules
3 | docker-compose.yaml
4 | *.gitignore
5 | *.git
6 | *.cache
7 | *.md
8 | Dockerfile
9 | nginx_config
10 | flask_api
11 | build
12 | **/*.geojson
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": ["airbnb-base", "prettier"],
4 | "rules": {
5 | "no-restricted-syntax": 0,
6 | "react/jsx-uses-react": "error",
7 | "react/jsx-uses-vars": "error",
8 | "react-hooks/rules-of-hooks": "error",
9 | "react-hooks/exhaustive-deps": "warn",
10 | "linebreak-style": 0
11 | },
12 | "globals": {
13 | "localStorage": true,
14 | "fetch": true
15 | },
16 | "env": { "browser": true, "jest": true },
17 | "plugins": ["react", "react-hooks"]
18 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *-lock.json
2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
3 | *.geojson
4 | # dependencies
5 | /node_modules
6 | /.pnp
7 | .pnp.js
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
26 | #dataset
27 | public/*.geojson
28 |
29 | # Byte-compiled / optimized / DLL files
30 | __pycache__/
31 | *.py[cod]
32 | *$py.class
33 |
34 | # C extensions
35 | *.so
36 |
37 | # Distribution / packaging
38 | .Python
39 | build/
40 | develop-eggs/
41 | dist/
42 | downloads/
43 | eggs/
44 | .eggs/
45 | lib/
46 | lib64/
47 | parts/
48 | sdist/
49 | var/
50 | wheels/
51 | share/python-wheels/
52 | *.egg-info/
53 | .installed.cfg
54 | *.egg
55 | MANIFEST
56 |
57 | # PyInstaller
58 | # Usually these files are written by a python script from a template
59 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
60 | *.manifest
61 | *.spec
62 |
63 | # Installer logs
64 | pip-log.txt
65 | pip-delete-this-directory.txt
66 |
67 | # Unit test / coverage reports
68 | htmlcov/
69 | .tox/
70 | .nox/
71 | .coverage
72 | .coverage.*
73 | .cache
74 | nosetests.xml
75 | coverage.xml
76 | *.cover
77 | *.py,cover
78 | .hypothesis/
79 | .pytest_cache/
80 | cover/
81 |
82 | # Translations
83 | *.mo
84 | *.pot
85 |
86 | # Django stuff:
87 | *.log
88 | local_settings.py
89 | db.sqlite3
90 | db.sqlite3-journal
91 |
92 | # Flask stuff:
93 | instance/
94 | .webassets-cache
95 |
96 | # Scrapy stuff:
97 | .scrapy
98 |
99 | # Sphinx documentation
100 | docs/_build/
101 |
102 | # PyBuilder
103 | .pybuilder/
104 | target/
105 |
106 | # Jupyter Notebook
107 | .ipynb_checkpoints
108 |
109 | # IPython
110 | profile_default/
111 | ipython_config.py
112 |
113 | # pyenv
114 | # For a library or package, you might want to ignore these files since the code is
115 | # intended to run in multiple environments; otherwise, check them in:
116 | # .python-version
117 |
118 | # pipenv
119 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
120 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
121 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
122 | # install all needed dependencies.
123 | #Pipfile.lock
124 |
125 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
126 | __pypackages__/
127 |
128 | # Celery stuff
129 | celerybeat-schedule
130 | celerybeat.pid
131 |
132 | # SageMath parsed files
133 | *.sage.py
134 |
135 | # Environments
136 | .env
137 | .venv
138 | env/
139 | venv/
140 | ENV/
141 | env.bak/
142 | venv.bak/
143 |
144 | # Spyder project settings
145 | .spyderproject
146 | .spyproject
147 |
148 | # Rope project settings
149 | .ropeproject
150 |
151 | # mkdocs documentation
152 | /site
153 |
154 | # mypy
155 | .mypy_cache/
156 | .dmypy.json
157 | dmypy.json
158 |
159 | # Pyre type checker
160 | .pyre/
161 |
162 | # pytype static type analyzer
163 | .pytype/
164 |
165 | # Cython debug symbols
166 | cython_debug/
167 |
168 | # Direnv file
169 | .envrc
170 |
171 | # Text editor files
172 | .vscode/
173 |
174 | # Paw files
175 | *.paw
176 |
177 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/priorizador.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Contributing
2 |
3 | 1. Fork it!
4 | 2. Create your feature branch: `git checkout -b username/my-new-feature`
5 | 3. Commit your changes: `git commit -m 'Add some feature'`
6 | 4. Push to the branch: `git push origin username/my-new-feature`
7 | 5. Add your name and git account to the Contributors section in this `Readme.md`
8 | 6. Submit a pull request to `develop` branch
9 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:lts-alpine AS build
2 | WORKDIR /html
3 | COPY . /html
4 | ARG REACT_APP_API_URL
5 | ENV REACT_APP_API_URL=$REACT_APP_API_URL
6 | ARG REACT_APP_DASH_URL
7 | ENV REACT_APP_DASH_URL=$REACT_APP_DASH_URL
8 | RUN cd /html && yarn install && yarn build
9 |
10 | FROM nginx:1.17 AS base
11 | RUN mkdir /etc/nginx/cache
12 | USER root
13 |
14 | FROM base AS final
15 | WORKDIR /home
16 | COPY --from=build /html/build /usr/share/nginx/html
17 | EXPOSE 8080
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 reAcción
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Priorizador
2 |
3 | ## Environment variables
4 |
5 | ```bash
6 | cd priorizador
7 | touch .env
8 | touch .env.development.local
9 | ```
10 |
11 | .env.development.local
12 |
13 | ```bash
14 | REACT_APP_API_URL='http://localhost:5000/reaccion'
15 | TEKOPORA='1C4YS7tiQxAZ8vH4A46HpSc03xR0PVRA74itIcUdjYjQ'
16 | ALMUERZO='18NgsyLY-BVR9lQ48oDs-2tf3QeQYxSGF0ywf1aW661c'
17 | TECHO='11jSqn_p_uXK3xHntUmjws_Eaka1ei3CNyhZ9VRpKJ-w'
18 | FUNDACION='1TnF5CaBj8EQLa8JbNMVnxYMP6W2YGG56mVDg6PeabLo'
19 | CESTAS='1NSrEt9-z6LB0c1RlgXhbvsXbGWZEqJsfk05NAb94lhM'
20 | KOBO_API_TOKEN='XXXXCHANGEXXXX'
21 | ANDE_QUERY=SELECT "COORD_X", "COORD_Y" from "131e75f7-9073-46f6-9e49-f8673595dfc7" WHERE "DPTO" LIKE 'Alto Paran%' AND ("MUNICIPIO" LIKE 'CIUDAD DEL ESTE' OR "MUNICIPIO" LIKE 'HERNANDARIAS' OR "MUNICIPIO" LIKE 'MINGA GUAZU' OR "MUNICIPIO" LIKE 'PRESIDENTE FRANCO')
22 | DASH_DEBUG_MODE=False
23 | REACT_APP_DASH_URL='http://localhost:8050'
24 | REDIS_SERVER=redis
25 |
26 | ```
27 |
28 | .env
29 |
30 | ```bash
31 | REACT_APP_API_URL='http://localhost:8080/api'
32 | TEKOPORA='1C4YS7tiQxAZ8vH4A46HpSc03xR0PVRA74itIcUdjYjQ'
33 | ALMUERZO='18NgsyLY-BVR9lQ48oDs-2tf3QeQYxSGF0ywf1aW661c'
34 | TECHO='11jSqn_p_uXK3xHntUmjws_Eaka1ei3CNyhZ9VRpKJ-w'
35 | FUNDACION='1TnF5CaBj8EQLa8JbNMVnxYMP6W2YGG56mVDg6PeabLo'
36 | CESTAS='1NSrEt9-z6LB0c1RlgXhbvsXbGWZEqJsfk05NAb94lhM'
37 | KOBO_API_TOKEN='XXXXCHANGEXXXX'
38 | ANDE_QUERY=SELECT "COORD_X", "COORD_Y" from "131e75f7-9073-46f6-9e49-f8673595dfc7" WHERE "DPTO" LIKE 'Alto Paran%' AND ("MUNICIPIO" LIKE 'CIUDAD DEL ESTE' OR "MUNICIPIO" LIKE 'HERNANDARIAS' OR "MUNICIPIO" LIKE 'MINGA GUAZU' OR "MUNICIPIO" LIKE 'PRESIDENTE FRANCO')
39 | DASH_DEBUG_MODE=False
40 | REACT_APP_DASH_URL='http://localhost:8050'
41 | REDIS_SERVER=redis
42 |
43 | ```
44 |
45 | For the API:
46 |
47 | ```bash
48 | cd priorizador/flask_api
49 | touch .env
50 | ```
51 |
52 | .env content
53 |
54 | ```bash
55 | TEKOPORA='1C4YS7tiQxAZ8vH4A46HpSc03xR0PVRA74itIcUdjYjQ'
56 | ALMUERZO='18NgsyLY-BVR9lQ48oDs-2tf3QeQYxSGF0ywf1aW661c'
57 | TECHO='11jSqn_p_uXK3xHntUmjws_Eaka1ei3CNyhZ9VRpKJ-w'
58 | FUNDACION='1TnF5CaBj8EQLa8JbNMVnxYMP6W2YGG56mVDg6PeabLo'
59 | CESTAS='1NSrEt9-z6LB0c1RlgXhbvsXbGWZEqJsfk05NAb94lhM'
60 | KOBO_API_TOKEN='XXXXCHANGEXXXX'
61 | ANDE_QUERY=SELECT "COORD_X", "COORD_Y" from "131e75f7-9073-46f6-9e49-f8673595dfc7" WHERE "DPTO" LIKE 'Alto Paran%' AND ("MUNICIPIO" LIKE 'CIUDAD DEL ESTE' OR "MUNICIPIO" LIKE 'HERNANDARIAS' OR "MUNICIPIO" LIKE 'MINGA GUAZU' OR "MUNICIPIO" LIKE 'PRESIDENTE FRANCO')
62 |
63 | ```
64 |
65 | For Dash App:
66 |
67 | ```bash
68 | cd priorizador/visualizations_dash
69 | touch .env
70 |
71 | ```
72 |
73 | .env content
74 |
75 | ```bash
76 | REACT_APP_API_URL='http://localhost:5000/reaccion'
77 | DASH_DEBUG_MODE=True
78 | REDIS_SERVER=localhost
79 | ```
80 |
81 | ## Running the frontend locally
82 |
83 | ```bash
84 | yarn install
85 | ```
86 |
87 | In the project directory, you can run:
88 |
89 | ```bash
90 | yarn start
91 | ```
92 |
93 | Runs the app in the development mode.
94 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
95 |
96 | The page will reload if you make edits.
97 | You will also see any lint errors in the console.
98 |
99 | ```bash
100 | yarn test
101 | ```
102 |
103 | Launches the test runner in the interactive watch mode.
104 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
105 |
106 | ```bash
107 | yarn build
108 | ```
109 |
110 | Builds the app for production to the `build` folder.
111 | It correctly bundles React in production mode and optimizes the build for the best performance.
112 |
113 | The build is minified and the filenames include the hashes.
114 | Your app is ready to be deployed!
115 |
116 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
117 |
118 | ## Running the API locally
119 |
120 | For Python=3.7
121 |
122 | To use the api you should create a virtual environment with python, install virtualenv with pip
123 |
124 | `pip install virtualenv`
125 |
126 | Then redirect yourself to the flask_api folder and create an env folder
127 |
128 | `cd flask_api/ && mkdir env`
129 |
130 | Then, create the virtual environment and install the requirements
131 |
132 | `python -m venv env && pip install -r requirements.txt`
133 |
134 | Finally to run the tests use the following command and everything should be green
135 |
136 | `pytest`
137 |
138 | Create a folder named _geojson_data_ and download the geojson file available [here](http://geo.stp.gov.py/u/dgeec/tables/dgeec.paraguay_2012_barrrios_y_localidades/public/map) in it, then execute the script with the virtual environment activated.
139 | `python geojson_api.py`
140 |
141 | ## Running the Dash App locally
142 |
143 | For Python=3.7
144 | It is necessary [Redis](https://redis.io/topics/quickstart) and to run the API.
145 |
146 | Redirect yourself to the visualizations_dash folder and create an env folder
147 |
148 | ```bash
149 | cd visualizations_dash/ && mkdir env
150 |
151 | ```
152 |
153 | Then, create the virtual environment and install the requirements
154 |
155 | ```bash
156 | python -m venv env && pip install -r requirements.txt
157 |
158 | ```
159 |
160 | Execute:
161 |
162 | ```bash
163 | . env/bin/activate
164 | python dash_main.py
165 |
166 | ```
167 |
168 | It is possible to pre-calculate the data periodically at the time specified in dash_main.py (celery.conf.beat_schedule dictionary).
169 | To do it:
170 |
171 | Run the Celery Worker:
172 |
173 | ```bash
174 | . env/bin/activate
175 | celery -A dash_main.celery worker --loglevel=info
176 |
177 | ```
178 |
179 | Run the Celery Beat scheduler:
180 |
181 | ```bash
182 | . env/bin/activate
183 | celery -A dash_main.celery beat --loglevel=info
184 |
185 | ```
186 |
187 | ### Deploy with docker-compose
188 |
189 | Install docker (minimun 17.05) and docker-compose in your local environment.
190 |
191 | Execute
192 | `docker-compose build`
193 | `docker-compose up -d`
194 |
195 | For production deployment you should change the localhost and port 8080 with your port and servername int the nginx_config files.
196 |
197 | ## Access the API documentation
198 |
199 | Easy OpenAPI specs and Swagger UI for your Flask API.
200 | Flasgger is a Flask extension to extract OpenAPI-Specification from all Flask views registered in your API.
201 |
202 | To open the API documentation on the browser, open the following URL.
203 |
204 | `http://localhost:5000/apidocs`
205 |
206 | ## Contributors / Thanks
207 |
208 | - Grosip https://github.com/grosip
209 | - Javier Perez https://github.com/javierpf
210 | - Nahuel Hernandez https://github.com/nahu
211 | - Rodrigo Parra https://github.com/rparrapy
212 | - Walter Benitez https://github.com/walter-bd
213 | - Iván Cáceres https://github.com/ivanarielcaceres
214 | - Fernando Cardenas https://github.com/dev-cardenas
215 |
216 | ### Licencia MIT: [Licencia](https://github.com/reaccionpy/priorizador/blob/master/LICENSE)
217 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | flask-api:
5 | build: ./flask_api
6 | image: priorizador-api
7 | networks:
8 | network:
9 | aliases:
10 | - python-svc
11 | restart: 'always'
12 | volumes:
13 | - ./flask_api/geojson_data/:/home/geojson_data
14 | environment:
15 | - TEKOPORA
16 | - TECHO
17 | - ALMUERZO
18 | - FUNDACION
19 | - KOBO_API_TOKEN
20 | - CESTAS
21 | - ANDE_QUERY
22 |
23 | redis:
24 | image: redis
25 | networks:
26 | - network
27 |
28 | celery-worker:
29 | build:
30 | context: ./visualizations_dash
31 | dockerfile: Dockerfile.Celery
32 | image: celery-worker
33 | entrypoint: celery
34 | command: -A dash_main.celery worker --loglevel=info
35 | depends_on:
36 | - redis
37 | - flask-api
38 | networks:
39 | - network
40 | environment:
41 | - REACT_APP_API_URL=http://python-svc:5000/reaccion
42 | - REDIS_SERVER
43 |
44 | celery-beat:
45 | build:
46 | context: ./visualizations_dash
47 | dockerfile: Dockerfile.Celery
48 | image: celery-beat
49 | entrypoint: celery
50 | command: -A dash_main.celery beat --loglevel=info
51 | depends_on:
52 | - celery-worker
53 | - redis
54 | networks:
55 | - network
56 | environment:
57 | - REDIS_SERVER
58 |
59 | dash:
60 | build: ./visualizations_dash
61 | image: dash
62 | ports:
63 | - '8050:8050'
64 | depends_on:
65 | - redis
66 | - flask-api
67 | networks:
68 | - network
69 | environment:
70 | - REACT_APP_API_URL=http://python-svc:5000/reaccion
71 | - DASH_DEBUG_MODE
72 | - REDIS_SERVER
73 |
74 | priorizador:
75 | build:
76 | context: ./
77 | args:
78 | REACT_APP_API_URL: ${REACT_APP_API_URL}
79 | REACT_APP_DASH_URL: ${REACT_APP_DASH_URL}
80 | image: priorizador
81 | ports:
82 | - '8080:8080'
83 | networks:
84 | network:
85 | aliases:
86 | - priorizador
87 | volumes:
88 | - ./nginx_config/priorizador.conf:/etc/nginx/conf.d/priorizador.conf
89 | - ./nginx_config/nginx.conf:/etc/nginx/nginx.conf
90 | restart: 'always'
91 |
92 | networks:
93 | network:
94 |
--------------------------------------------------------------------------------
/flask_api/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | .dockerignore
3 | env
4 |
5 |
--------------------------------------------------------------------------------
/flask_api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.7
2 |
3 | RUN pip install uwsgi
4 | COPY requirements.txt /home/
5 | RUN pip install -r /home/requirements.txt
6 | COPY * /home/
7 | RUN rm /home/requirements.txt
8 | WORKDIR /home
9 | RUN chmod 0775 -R /home /var/run /var/log/
10 |
11 |
12 |
13 | #create a function that checks the containers health
14 | #Example
15 | HEALTHCHECK --interval=60s --timeout=20s \
16 | CMD curl --silent --fail localhost:5000/reaccion/healthcheck || exit 1
17 |
18 | EXPOSE 5000
19 |
20 | CMD ["uwsgi", "--http", "0.0.0.0:5000", "--module", "geojson_api:app", "--processes", "1", "--threads", "5"]
21 | # The command’s exit status indicates the health status of the container. The possible values are:
22 |
23 | #0: success - the container is healthy and ready for use
24 | #1: unhealthy - the container is not working correctly
25 | #2: reserved - do not use this exit code
26 | #Labels of the project
27 | ENV HOME /home
28 | USER 1001
29 | LABEL "company"="Reaccion"
30 | LABEL "author"="Walter Benítez"
31 | LABEL "direction"="Ciudad del Este - Paraguay"
32 | LABEL version="1.0"
33 | LABEL description="Dockerfile to generate the image for the geojson api service"
34 |
--------------------------------------------------------------------------------
/flask_api/api-docs/get_cestas_submissions.yml:
--------------------------------------------------------------------------------
1 | Example api doc for get_kobo_submissions endpoint
2 | ---
3 | responses:
4 | 200:
5 | description: Kobotoolbox form data
6 | schema:
7 | $ref: '#/definitions/CestasData'
8 |
9 | definitions:
10 | GeoJSON:
11 | type: object
12 | properties:
13 | type:
14 | type: string
15 | example: FeatureCollection
16 | features:
17 | $ref: '#/definitions/Features'
18 | Features:
19 | type: array
20 | items:
21 | $ref: '#/definitions/Feature'
22 | Feature:
23 | type: object
24 | properties:
25 | type:
26 | type: string
27 | example: Feature
28 | geometry:
29 | type: object
30 | $ref: '#/definitions/Geometry'
31 | properties:
32 | type: string
33 | $ref: '#/definitions/Properties'
34 |
35 | Geometry:
36 | type: object
37 | properties:
38 | type:
39 | type: string
40 | example: MultiPolygon
41 | coordinates:
42 | type: array
43 | items: []
44 | example: [[[[-54.853733, -25.231389]]]]
45 | Properties:
46 | type: object
47 | properties:
48 | nombre_apellido:
49 | type: string
50 | example: 'Juan Perez'
51 | nro_ci:
52 | type: integer
53 | example: 3543221
54 | nro_telefono:
55 | type: integer
56 | example: 981111111
57 |
--------------------------------------------------------------------------------
/flask_api/api-docs/get_json.yml:
--------------------------------------------------------------------------------
1 | Example api doc for get_json endpoint
2 | ---
3 | parameters:
4 | - name: "departamento"
5 | in: path
6 | type: string
7 | required: false
8 | responses:
9 | 200:
10 | description: GeoJSON data
11 | schema:
12 | $ref: '#/definitions/GeoJSON'
13 |
14 | definitions:
15 | GeoJSON:
16 | type: object
17 | properties:
18 | type:
19 | type: string
20 | example: FeatureCollection
21 | features:
22 | $ref: '#/definitions/Features'
23 | Features:
24 | type: array
25 | items:
26 | $ref: '#/definitions/Feature'
27 | Feature:
28 | type: object
29 | properties:
30 | type:
31 | type: string
32 | example: Feature
33 | geometry:
34 | type: object
35 | $ref: '#/definitions/Geometry'
36 | properties:
37 | type: string
38 | $ref: '#/definitions/Properties'
39 |
40 | Geometry:
41 | type: object
42 | properties:
43 | type:
44 | type: string
45 | example: MultiPolygon
46 | coordinates:
47 | type: array
48 | items: []
49 | example: [[[[-54.853733, -25.231389]]]]
50 | Properties:
51 | type: object
52 | properties:
53 | objectid:
54 | type: integer
55 | example: 29
56 | dpto:
57 | type: string
58 | example: "10"
59 | distrito:
60 | type: string
61 | example: "20"
62 | area:
63 | type: string
64 | example: "6"
65 | cant_viv:
66 | type: integer
67 | example: 10
68 | cartodb_id:
69 | type: integer
70 | example: 29
71 | dist_desc:
72 | type: string
73 | example: "SANTA FE DEL PARANA"
74 | bar_loc:
75 | type: string
76 | example: "430"
77 | barlo_desc:
78 | type: string
79 | example: "ASENT. NI\u00d1O JESUS"
80 | superf:
81 | type: integer
82 | example: 1.08
83 | updated_at:
84 | type: string
85 | example: "2016-03-03T20:21:16-0300"
86 | dpto_desc:
87 | type: string
88 | example: "ALTO PARANA"
89 | codigo:
90 | type: string
91 | example: "10206430"
92 | created_at:
93 | type: string
94 | example: "2016-03-03T20:21:16-0300"
--------------------------------------------------------------------------------
/flask_api/api-docs/get_kobo_submissions.yml:
--------------------------------------------------------------------------------
1 | Example api doc for get_kobo_submissions endpoint
2 | ---
3 | responses:
4 | 200:
5 | description: Kobotoolbox form data
6 | schema:
7 | $ref: '#/definitions/KoboData'
8 |
9 | definitions:
10 | KoboData:
11 | type: object
12 | properties:
13 | id:
14 | type: integer
15 | example: 91255297
16 | donacion:
17 | type: string
18 | example: '100 kits de alimentos'
19 | referencia:
20 | type: string
21 | example: 'Barrio San Agustín'
22 | nro_familias:
23 | type: integer
24 | example: 5
25 | organizacion:
26 | type: string
27 | example: 'Red Solidaria de Kuña Poty'
28 | tipo_ayuda:
29 | type: string
30 | example: 'kits'
31 | coordinates:
32 | type: array
33 | items: []
34 | example: [[[[-54.853733, -25.231389]]]]
35 |
--------------------------------------------------------------------------------
/flask_api/geojson_api.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import os
4 | import pathlib
5 | from datetime import datetime
6 |
7 | from dotenv import load_dotenv
8 | from flasgger import Swagger, swag_from
9 | from flask import Flask, Response, request
10 | from flask_compress import Compress
11 | from flask_cors import CORS
12 |
13 | from utils import (
14 | add_properties_almuerzo,
15 | add_properties_ande,
16 | add_properties_fundacion,
17 | add_properties_techo,
18 | add_properties_tekopora,
19 | get_kobo_data,
20 | get_resource_from_ckan_with_sql_query,
21 | google_sheets_to_df,
22 | )
23 |
24 | load_dotenv()
25 |
26 | COMPRESS_MIMETYPES = [
27 | "text/html",
28 | "text/css",
29 | "text/xml",
30 | "application/json",
31 | "application/javascript",
32 | ]
33 | COMPRESS_LEVEL = 6
34 | COMPRESS_MIN_SIZE = 500
35 |
36 | # Ruta del archivo geojson en el SO
37 | GEOJSON_PATH = str(
38 | pathlib.Path(__file__).parent.absolute()
39 | / "geojson_data/paraguay_2012_barrrios_y_localidades.geojson"
40 | )
41 |
42 | tekopora_key = os.getenv("TEKOPORA")
43 | techo_key = os.getenv("TECHO")
44 | almuerzo_key = os.getenv("ALMUERZO")
45 | fundacion_key = os.getenv("FUNDACION")
46 | cestas_key = os.getenv("CESTAS")
47 | kobo_token = os.getenv("KOBO_API_TOKEN")
48 | ande_query = os.getenv("ANDE_QUERY")
49 | compress = Compress()
50 |
51 | # create logger
52 | logger = logging.getLogger("register")
53 | logger.setLevel(logging.DEBUG)
54 | conn = None
55 |
56 | # create console handler and set level to debug
57 | ch = logging.StreamHandler()
58 | ch.setLevel(logging.DEBUG)
59 |
60 | # create formatter
61 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
62 |
63 | # add formatter to ch
64 | ch.setFormatter(formatter)
65 |
66 | # add ch to logger
67 | logger.addHandler(ch)
68 |
69 |
70 | def start_app():
71 | app = Flask(__name__)
72 | compress.init_app(app)
73 | return app
74 |
75 |
76 | app = start_app()
77 | swagger = Swagger(app)
78 | CORS(app)
79 |
80 | # route for healthcheck
81 | @app.route("/reaccion/healthcheck", methods=["GET"])
82 | def healthcheck():
83 | return Response(response="Healthy\n", status=200, mimetype="text/plain")
84 |
85 | @app.route("/reaccion/get_available_layers", methods=["GET"])
86 | def get_available_layers():
87 | districts_layers = {
88 | 'CIUDAD DEL ESTE': ['tekopora','almuerzo','fundacion','techo','ande'],
89 | 'HERNANDARIAS': ['tekopora','ande'],
90 | 'MINGA GUAZU': ['tekopora','ande'],
91 | 'PRESIDENTE FRANCO': ['tekopora','fundacion','techo','ande']
92 | }
93 |
94 | district_arg = request.args.get("distrito")
95 | if district_arg is None:
96 | district_arg = "CIUDAD DEL ESTE"
97 |
98 | available_layers = {'layers': districts_layers[district_arg]}
99 |
100 | return available_layers
101 |
102 | @app.route("/reaccion/get_tekopora_layer", methods=["GET"])
103 | def get_tekopora_layer():
104 | """Getting geojson for specific region"""
105 | dep = request.args.get("departamento")
106 | distritos = ["01", "02", "05", "11"]
107 | if dep is None:
108 | dep = "10"
109 | # load only the default layer: Tekopora
110 | tekopora_df = google_sheets_to_df(tekopora_key)
111 | # techo_df = google_sheets_to_df(techo_key)
112 | # almuerzo_df = google_sheets_to_df(almuerzo_key)
113 | # fundacion_df = google_sheets_to_df(fundacion_key)
114 | with open(GEOJSON_PATH, "r", encoding="utf8") as f:
115 | shape = json.load(f)
116 | feature_dict = {
117 | f["properties"]["objectid"]: f
118 | for f in shape["features"]
119 | if f["properties"]["dpto"] == dep
120 | and f["properties"]["distrito"] in distritos
121 | }
122 | features = add_properties_tekopora(feature_dict, tekopora_df)
123 | # features = add_properties_techo(features, techo_df)
124 | # features = add_properties_fundacion(features, fundacion_df)
125 | # features = add_properties_almuerzo(features, almuerzo_df)
126 | shape["features"] = features
127 | response_pickled = json.dumps(shape)
128 | return Response(response=response_pickled, status=200, mimetype="application/json")
129 |
130 |
131 | @app.route("/reaccion/get_almuerzo_layer", methods=["GET"])
132 | def get_almuerzo_layer():
133 | """Getting almuerzo escolar geojson for specific region"""
134 | dep = request.args.get("departamento")
135 | distritos = ["01", "02", "05", "11"]
136 | if dep is None:
137 | dep = "10"
138 | almuerzo_df = google_sheets_to_df(almuerzo_key)
139 | with open(GEOJSON_PATH, "r", encoding="utf8") as f:
140 | shape = json.load(f)
141 | feature_dict = {
142 | f["properties"]["objectid"]: f
143 | for f in shape["features"]
144 | if f["properties"]["dpto"] == dep
145 | and f["properties"]["distrito"] in distritos
146 | }
147 |
148 | features = add_properties_almuerzo(feature_dict, almuerzo_df)
149 | shape["features"] = features
150 | response_pickled = json.dumps(shape)
151 | return Response(response=response_pickled, status=200, mimetype="application/json")
152 |
153 |
154 | @app.route("/reaccion/get_fundacion_layer", methods=["GET"])
155 | def get_fundacion_layer():
156 | """Getting almuerzo escolar geojson for specific region"""
157 | dep = request.args.get("departamento")
158 | distritos = ["01", "02", "05", "11"]
159 | if dep is None:
160 | dep = "10"
161 | fundacion_df = google_sheets_to_df(fundacion_key)
162 | with open(GEOJSON_PATH, "r", encoding="utf8") as f:
163 | shape = json.load(f)
164 | feature_dict = {
165 | f["properties"]["objectid"]: f
166 | for f in shape["features"]
167 | if f["properties"]["dpto"] == dep
168 | and f["properties"]["distrito"] in distritos
169 | }
170 | features = add_properties_fundacion(feature_dict, fundacion_df)
171 | shape["features"] = features
172 | response_pickled = json.dumps(shape)
173 | return Response(response=response_pickled, status=200, mimetype="application/json")
174 |
175 |
176 | @app.route("/reaccion/get_techo_layer", methods=["GET"])
177 | def get_techo_layer():
178 | """Getting geojson for specific region"""
179 | dep = request.args.get("departamento")
180 | distritos = ["01", "02", "05", "11"]
181 | if dep is None:
182 | dep = "10"
183 | techo_df = google_sheets_to_df(techo_key)
184 | with open(GEOJSON_PATH, "r", encoding="utf8") as f:
185 | shape = json.load(f)
186 | feature_dict = {
187 | f["properties"]["objectid"]: f
188 | for f in shape["features"]
189 | if f["properties"]["dpto"] == dep
190 | and f["properties"]["distrito"] in distritos
191 | }
192 | features = add_properties_techo(feature_dict, techo_df)
193 | shape["features"] = features
194 | response_pickled = json.dumps(shape)
195 | return Response(response=response_pickled, status=200, mimetype="application/json")
196 |
197 |
198 | @app.route("/reaccion/get_ande_layer", methods=["GET"])
199 | def get_ande_layer():
200 | """Getting geojson for specific region"""
201 | dep = request.args.get("departamento")
202 | distritos = ["01", "02", "05", "11"]
203 | if dep is None:
204 | dep = "10"
205 | print("Descargando datos de ANDE a las: " + str(datetime.now()))
206 | ande_df = get_resource_from_ckan_with_sql_query(ande_query)["result"]["records"]
207 | print("Descarga finalizada a las: " + str(datetime.now()))
208 | with open(GEOJSON_PATH, "r", encoding="utf8") as f:
209 | shape = json.load(f)
210 | feature_dict = {
211 | f["properties"]["objectid"]: f
212 | for f in shape["features"]
213 | if f["properties"]["dpto"] == dep
214 | and f["properties"]["distrito"] in distritos
215 | }
216 | # features = add_properties_techo(feature_dict, techo_df)
217 | features = add_properties_ande(feature_dict, ande_df, dep)
218 | shape["features"] = features
219 | response_pickled = json.dumps(shape)
220 | return Response(response=response_pickled, status=200, mimetype="application/json")
221 |
222 |
223 | @app.route("/reaccion/get_kobo_submissions", methods=["GET"])
224 | @swag_from("./api-docs/get_kobo_submissions.yml")
225 | def get_kobo_submissions():
226 | """Get reports from Kobo API"""
227 | data = get_kobo_data(kobo_token)
228 | return Response(
229 | response=data.to_json(orient="records"), status=200, mimetype="application/json"
230 | )
231 |
232 |
233 | @app.route("/reaccion/get_cestas_submissions", methods=["GET"])
234 | @swag_from("./api-docs/get_cestas_submissions.yml")
235 | def get_cestas_submissions():
236 | data = google_sheets_to_df(cestas_key)
237 | features = [
238 | {
239 | "type": "Feature",
240 | "properties": {
241 | "nombre_apellido": d.nombre_apellido,
242 | "nro_ci": d.nro_ci,
243 | "nro_telefono": d.nro_telefono,
244 | },
245 | "geometry": {"type": "Point", "coordinates": [d.longitud, d.latitud],},
246 | }
247 | for d in data.itertuples()
248 | ]
249 | geojson = {"type": "FeatureCollection", "features": features}
250 | return Response(
251 | response=json.dumps(geojson), status=200, mimetype="application/json"
252 | )
253 |
254 |
255 | if __name__ == "__main__":
256 | logging.basicConfig(level=logging.INFO)
257 | app.run(host="0.0.0.0", debug=False, threaded=True)
258 |
--------------------------------------------------------------------------------
/flask_api/requirements.txt:
--------------------------------------------------------------------------------
1 | appdirs==1.4.3
2 | attrs==19.3.0
3 | black==19.10b0
4 | certifi==2020.4.5.1
5 | chardet==3.0.4
6 | click==7.1.1
7 | flasgger==0.9.4
8 | Flask==1.1.2
9 | Flask-Compress==1.4.0
10 | Flask-Cors==3.0.8
11 | idna==2.9
12 | importlib-metadata==1.6.0
13 | itsdangerous==1.1.0
14 | Jinja2==2.11.3
15 | jsonschema==3.2.0
16 | lat-lon-parser==1.1.2
17 | MarkupSafe==1.1.1
18 | mistune==0.8.4
19 | numpy==1.18.2
20 | pandas==1.0.3
21 | pathspec==0.8.0
22 | pyrsistent==0.16.0
23 | python-dateutil==2.8.1
24 | python-dotenv==0.12.0
25 | pytz==2019.3
26 | PyYAML==5.3.1
27 | regex==2020.4.4
28 | requests==2.23.0
29 | Shapely==1.7.0
30 | six==1.14.0
31 | toml==0.10.0
32 | typed-ast==1.4.1
33 | urllib3==1.26.4
34 | utm==0.6.0
35 | Werkzeug==1.0.1
36 | zipp==3.1.0
37 |
--------------------------------------------------------------------------------
/flask_api/test_geojson_api.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import pathlib
4 | from geojson_api import *
5 |
6 | ABSOLUTE_PATH = str(pathlib.Path(__file__).parent.absolute())
7 | tekopora_key = os.getenv('TEKOPORA')
8 | techo_key = os.getenv('TECHO')
9 | almuerzo_key = os.getenv('ALMUERZO')
10 | fundacion_key = os.getenv('FUNDACION')
11 |
12 | def test_open_geojson_file():
13 | '''
14 | This piece of code tests if the geojson file from geojson_data folder is ok
15 | '''
16 | with open(f"{ABSOLUTE_PATH}/geojson_data/paraguay_2012_barrrios_y_localidades.geojson", "r", encoding='utf-8') as f:
17 | file = json.load(f)
18 | assert(len(file['features']) > 0)
19 |
20 | def test_google_sheets_to_df():
21 | '''
22 | This piece of code tests if google_sheet_to_df works for each google sheet
23 | '''
24 |
25 | tekopora_df = google_sheets_to_df(tekopora_key)
26 | assert(len(tekopora_df) > 0)
27 |
28 | techo_df = google_sheets_to_df(techo_key)
29 | assert(len(techo_df) > 0)
30 |
31 | almuerzo_df = google_sheets_to_df(almuerzo_key)
32 | assert(len(almuerzo_df) > 0)
33 |
34 | fundacion_df = google_sheets_to_df(fundacion_key)
35 | assert(len(fundacion_df) > 0)
36 |
37 | def test_add_properties_tekopora():
38 | '''
39 | This piece of code tests if test_add_properties_tekopora function correctly adds the tekopora property for each row
40 | '''
41 | with open(f"{ABSOLUTE_PATH}/geojson_data/paraguay_2012_barrrios_y_localidades.geojson", "r", encoding='utf-8') as f:
42 | shape = json.load(f)
43 | dep = "10"
44 | tekopora_df = google_sheets_to_df(tekopora_key)
45 | feature_dict = {f["properties"]["objectid"]:f
46 | for f in shape["features"]
47 | if f["properties"]["dpto"] == dep
48 | }
49 | features = add_properties_tekopora(feature_dict, tekopora_df)
50 | assert(len(features) > 0)
51 | # TODO - Test if all the rows has the property tekopora
52 | # for (feature in features):
53 | # if 'tekopora' not in dict.keys():
54 | # assert(False)
55 |
56 | def test_add_properties_techo():
57 | '''
58 | This piece of code tests if test_add_properties_techo function correctly adds the techo property for each row
59 | '''
60 | with open(f"{ABSOLUTE_PATH}/geojson_data/paraguay_2012_barrrios_y_localidades.geojson", "r", encoding='utf-8') as f:
61 | shape = json.load(f)
62 | dep = "10"
63 | tekopora_df = google_sheets_to_df(tekopora_key)
64 | techo_df = google_sheets_to_df(techo_key)
65 | feature_dict = {f["properties"]["objectid"]:f
66 | for f in shape["features"]
67 | if f["properties"]["dpto"] == dep
68 | }
69 | features = add_properties_tekopora(feature_dict, tekopora_df)
70 | features = add_properties_techo(features, techo_df)
71 | assert(len(features) > 0)
72 |
73 | def test_add_properties_fundacion():
74 | '''
75 | This piece of code tests if test_add_properties_fundacion function correctly adds the fundacion property for each row
76 | '''
77 | with open(f"{ABSOLUTE_PATH}/geojson_data/paraguay_2012_barrrios_y_localidades.geojson", "r", encoding='utf-8') as f:
78 | shape = json.load(f)
79 | dep = "10"
80 | tekopora_df = google_sheets_to_df(tekopora_key)
81 | techo_df = google_sheets_to_df(techo_key)
82 | fundacion_df = google_sheets_to_df(fundacion_key)
83 | feature_dict = {f["properties"]["objectid"]:f
84 | for f in shape["features"]
85 | if f["properties"]["dpto"] == dep
86 | }
87 | features = add_properties_tekopora(feature_dict, tekopora_df)
88 | features = add_properties_techo(features, techo_df)
89 | features = add_properties_fundacion(features, fundacion_df)
90 | assert(len(features) > 0)
91 |
92 | def test_add_properties_almuerzo():
93 | '''
94 | This piece of code tests if test_add_properties_almuerzo function correctly adds the almuerzo property for each row
95 | '''
96 | with open(f"{ABSOLUTE_PATH}/geojson_data/paraguay_2012_barrrios_y_localidades.geojson", "r", encoding='utf-8') as f:
97 | shape = json.load(f)
98 | dep = "10"
99 | tekopora_df = google_sheets_to_df(tekopora_key)
100 | techo_df = google_sheets_to_df(techo_key)
101 | fundacion_df = google_sheets_to_df(fundacion_key)
102 | almuerzo_df = google_sheets_to_df(almuerzo_key)
103 | feature_dict = {f["properties"]["objectid"]:f
104 | for f in shape["features"]
105 | if f["properties"]["dpto"] == dep
106 | }
107 | features = add_properties_tekopora(feature_dict, tekopora_df)
108 | features = add_properties_techo(features, techo_df)
109 | features = add_properties_fundacion(features, fundacion_df)
110 | features = add_properties_almuerzo(features, almuerzo_df)
111 | assert(len(features) > 0)
112 |
113 | def test_coordinates_to_feature():
114 | pass
--------------------------------------------------------------------------------
/flask_api/utils.py:
--------------------------------------------------------------------------------
1 | import collections
2 | import datetime
3 | import json
4 | from io import BytesIO
5 |
6 | import lat_lon_parser
7 | import pandas as pd
8 | import requests
9 | from shapely import geometry
10 | import utm
11 | from multiprocessing import Pool
12 |
13 | def google_sheets_to_df(key):
14 | r = requests.get(f"https://docs.google.com/spreadsheet/ccc?key={key}&output=csv")
15 | return pd.read_csv(BytesIO(r.content))
16 |
17 |
18 | def get_resource_from_ckan_with_json_query(query_json):
19 | query_json = json.loads(query_json)
20 | r = requests.get("https://portal.datos.org.py/api/3/action/datastore_search"
21 | , json = query_json, verify = False)
22 | data = r.json()
23 | return data
24 |
25 | def get_resource_from_ckan_with_sql_query(query_sql):
26 | r = requests.get("""https://portal.datos.org.py/api/3/action/datastore_search_sql?sql=""" + query_sql
27 | , verify = False)
28 | data = r.json()
29 | return data
30 |
31 | def add_properties_tekopora(feature_dict, df):
32 | for row in df.itertuples():
33 | try:
34 | feature_dict[row.objectid]["properties"]["tekopora"] = row.CANTIDAD
35 | except KeyError:
36 | print(f"Could not import {row.BARRIO}")
37 | continue
38 | return list(feature_dict.values())
39 |
40 |
41 | def coordinates_to_feature(lat, lng, features):
42 | point = geometry.Point(lng, lat)
43 | # check each polygon to see if it contains the point
44 | for feature in features:
45 | polygon = geometry.shape(feature["geometry"])
46 | if polygon.contains(point):
47 | return feature
48 | return None
49 |
50 | def from_utm_to_degrees(data):
51 | zone_number = 21
52 | zone_letter = 'J'
53 |
54 | coord_x_utc = float(data['COORD_X'].replace(",", "."))
55 | coord_y_utc = float(data['COORD_Y'].replace(",", "."))
56 |
57 | if coord_x_utc >= 100000 and coord_x_utc <= 999999:
58 | coord_utc_to_latlong = utm.to_latlon(coord_x_utc, coord_y_utc,
59 | zone_number, zone_letter)
60 | lat,lng = coord_utc_to_latlong[0],coord_utc_to_latlong[1]
61 | return [lat,lng]
62 |
63 | return []
64 |
65 |
66 | def add_properties_techo(feature_dict, df):
67 |
68 | features = list(feature_dict.values())
69 |
70 | for row in df.itertuples():
71 | coords = [float(v) for v in row.LATITUD_LONGITUD.split(",")]
72 | feature = coordinates_to_feature(coords[0], coords[1], features)
73 | if feature is not None:
74 | feature["properties"].setdefault("techo", 0)
75 | feature["properties"]["techo"] += 1
76 | return features
77 |
78 |
79 | def add_properties_fundacion(feature_dict, df):
80 | poverty_filter = (df["Pobreza extrema"] == "Sí") | (df["Pobreza"] == "Sí")
81 |
82 | features = list(feature_dict.values())
83 |
84 | for row in df[poverty_filter].itertuples():
85 | lat = float(row.Latitude.replace(",", "."))
86 | lng = float(row.Longitude.replace(",", "."))
87 | feature = coordinates_to_feature(lat, lng, features)
88 | if feature is not None:
89 | feature["properties"].setdefault("fundacion", 0)
90 | feature["properties"]["fundacion"] += 1
91 | return features
92 |
93 |
94 | def add_properties_almuerzo(feature_dict, df):
95 | coordinate_filter = (df["LATITUD"].notnull()) & (df["LONGITUD"].notnull())
96 |
97 | features = list(feature_dict.values())
98 |
99 | seen = set()
100 | for row in df[coordinate_filter].itertuples():
101 | lat = lat_lon_parser.parse(row.LATITUD)
102 | lng = lat_lon_parser.parse(row.LONGITUD)
103 | if row._4 not in seen:
104 | feature = coordinates_to_feature(lat, lng, features)
105 | if feature is not None:
106 | feature["properties"].setdefault("almuerzo", 0)
107 | feature["properties"]["almuerzo"] += 1
108 | seen.add(row._4)
109 | return features
110 |
111 |
112 | def add_properties_ande(feature_dict, df, department_number):
113 | processes = 3
114 | dttime = datetime.datetime
115 | departments_limits_utm = [
116 | {
117 | 'dep': "10",
118 | 'coordinates': {
119 | 'coord_x_min': 643802,
120 | 'coord_x_max': 768631,
121 | 'cood_y_min': 7094723,
122 | 'coord_y_max': 7292270
123 | }
124 | }
125 | ]
126 |
127 | features = list(feature_dict.values())
128 |
129 | print("Agregando datos de tarifa social de ANDE")
130 | print("Formateando coordenadas: "+ str(dttime.now()))
131 | # delete data whose coordinates are outside the department area
132 | # obtain the coordinates' range of the department
133 | ranges_coord = [department["coordinates"] for department in departments_limits_utm if department['dep'] == department_number]
134 | ranges_coord = ranges_coord[0]
135 | x_min, x_max = ranges_coord["coord_x_min"], ranges_coord["coord_x_max"]
136 | y_min, y_max = ranges_coord["cood_y_min"], ranges_coord["coord_y_max"]
137 | # excluding wrong data
138 | df = [data for data in df if float(data['COORD_X'].replace(",", "."))>=x_min and float(data['COORD_X'].replace(",", "."))<=x_max]
139 | df = [data for data in df if float(data['COORD_Y'].replace(",", "."))>=y_min and float(data['COORD_Y'].replace(",", "."))<=y_max]
140 |
141 | # transform the coordinates in utm to degrees
142 | with Pool(processes) as p:
143 | list_coordinates_in_degrees = p.map(from_utm_to_degrees, df)
144 |
145 | print("Coordenadas finalmente formateadas a las : "+ str(dttime.now()))
146 |
147 | print("Agregando ande properties a features : "+ str(dttime.now()))
148 | # add de property ande
149 | for location in list_coordinates_in_degrees:
150 | if len(location) > 0:
151 | lat = location[0]
152 | lng = location[1]
153 | feature = coordinates_to_feature(lat, lng, features)
154 | if feature is not None:
155 | feature["properties"].setdefault("ande", 0)
156 | feature["properties"]["ande"] += 1
157 |
158 | print("Ande properties agregadas completamente a las: "+ str(dttime.now()))
159 |
160 | return features
161 |
162 |
163 | def kobo_to_response(kobo_entry):
164 | e = collections.defaultdict(lambda: None, kobo_entry)
165 | return {
166 | "id": e["_id"],
167 | "donacion": e["Favor_indique_cu_nto_limentos_v_veres_don"],
168 | "referencia": e["Favor_indique_el_nom_so_alguna_referencia"],
169 | "nro_familias": e["Favor_indicar_la_can_e_la_familia_ayudada"],
170 | "organizacion": e["Organizacion_Grupo"],
171 | "tipo_ayuda": e["Tipo_de_ayuda"],
172 | }
173 |
174 |
175 | def kobo_volunteer_to_response(kobo_entry):
176 | e = collections.defaultdict(lambda: None, kobo_entry)
177 | return {"id": e["ID"], "coordinates": e["_geolocation"]}
178 |
179 |
180 | def from_last_week(kobo_entry):
181 | d = datetime.datetime.strptime(kobo_entry["_submission_time"], "%Y-%m-%dT%H:%M:%S")
182 | now = datetime.datetime.now()
183 | delta = d - now
184 | return delta.days < 7
185 |
186 |
187 | def get_kobo_data(token):
188 | headers = {"Authorization": f"Token {token}"}
189 | kobo_response = requests.get(
190 | "https://kobo.humanitarianresponse.info/assets/aUGdJQeUkMGKQSsBk2XyKT/submissions?format=json",
191 | headers=headers,
192 | )
193 |
194 | kobo_submissions = [
195 | kobo_to_response(s) for s in json.loads(kobo_response.text) if from_last_week(s)
196 | ]
197 |
198 | kobo_volunteer_response = requests.get(
199 | "https://kobo.humanitarianresponse.info/assets/aDpeBqS6AvaFUUaP2856Fw/submissions?format=json",
200 | headers=headers,
201 | )
202 |
203 | kobo_volunteer_submissions = [
204 | kobo_volunteer_to_response(s)
205 | for s in json.loads(kobo_volunteer_response.text)
206 | if not None in s["_geolocation"]
207 | ]
208 |
209 | main_df = pd.DataFrame(kobo_submissions)
210 | volunteer_df = pd.DataFrame(kobo_volunteer_submissions)
211 | volunteer_df["id"] = volunteer_df["id"].astype("int64")
212 | return main_df.merge(volunteer_df, on="id")
213 |
--------------------------------------------------------------------------------
/nginx_config/nginx.conf:
--------------------------------------------------------------------------------
1 |
2 | user nginx;
3 | worker_processes 1;
4 |
5 | error_log /var/log/nginx/error.log warn;
6 | pid /var/run/nginx.pid;
7 |
8 |
9 | events {
10 | worker_connections 1024;
11 | }
12 |
13 |
14 | http {
15 | proxy_cache_path /etc/nginx/cache levels=1:2 keys_zone=STATIC:10m
16 | inactive=24h max_size=1g;
17 | include /etc/nginx/mime.types;
18 | default_type application/octet-stream;
19 |
20 | log_format main '$remote_addr - $remote_user [$time_local] "$request" '
21 | '$status $body_bytes_sent "$http_referer" '
22 | '"$http_user_agent" "$http_x_forwarded_for"';
23 |
24 | access_log /var/log/nginx/access.log main;
25 |
26 | sendfile on;
27 | #tcp_nopush on;
28 |
29 | keepalive_timeout 65;
30 |
31 | #gzip on;
32 |
33 | include /etc/nginx/conf.d/*.conf;
34 | }
35 |
--------------------------------------------------------------------------------
/nginx_config/priorizador.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen *:8080;
3 | gzip on;
4 | gzip_types text/plain application/x-javascript text/xml text/css application/xml;
5 | gzip_proxied no-cache no-store private expired auth;
6 | gzip_min_length 1000;
7 | server_name localhost;
8 | root /usr/share/nginx/html;
9 | location / {
10 | gzip_static on;
11 | }
12 | location /api {
13 | proxy_pass http://python-svc:5000/reaccion;
14 | proxy_set_header Host $host;
15 | proxy_buffering on;
16 | proxy_cache STATIC;
17 | proxy_cache_valid 200 1d;
18 | proxy_cache_use_stale error timeout invalid_header updating
19 | http_500 http_502 http_503 http_504;
20 | }
21 | location ~* \.(pdf|css|html|js|swf)$ {
22 | expires 2d;
23 | }
24 | location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico)$ {
25 | expires 30d;
26 | add_header Cache-Control "public, no-transform";
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "priorizador",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@ant-design/icons": "^4.0.5",
7 | "@fortawesome/fontawesome-svg-core": "^1.2.30",
8 | "@fortawesome/free-solid-svg-icons": "^5.14.0",
9 | "@fortawesome/react-fontawesome": "^0.1.11",
10 | "antd": "^4.1.0",
11 | "chroma-js": "^2.1.0",
12 | "geojson-geometries-lookup": "^0.3.1",
13 | "leaflet": "^1.6.0",
14 | "localforage": "^1.9.0",
15 | "lodash": "^4.17.15",
16 | "react": "^16.13.1",
17 | "react-dom": "^16.13.1",
18 | "react-leaflet": "^2.6.3",
19 | "react-leaflet-sidebarv2": "^0.6.0",
20 | "react-responsive": "^8.0.3",
21 | "react-router-dom": "^5.2.0",
22 | "react-scripts": "^3.4.3",
23 | "react-spinners": "^0.9.0",
24 | "supercluster": "^7.0.0",
25 | "use-supercluster": "^0.2.8"
26 | },
27 | "scripts": {
28 | "start": "react-scripts start",
29 | "build": "react-scripts build",
30 | "test": "react-scripts test",
31 | "eject": "react-scripts eject"
32 | },
33 | "eslintConfig": {
34 | "extends": "react-app"
35 | },
36 | "browserslist": {
37 | "production": [
38 | ">0.2%",
39 | "not dead",
40 | "not op_mini all"
41 | ],
42 | "development": [
43 | "last 1 chrome version",
44 | "last 1 firefox version",
45 | "last 1 safari version"
46 | ]
47 | },
48 | "husky": {
49 | "hooks": {
50 | "pre-commit": "lint-staged"
51 | }
52 | },
53 | "lint-staged": {
54 | "*.{js,json,css,md}": [
55 | "prettier --write"
56 | ]
57 | },
58 | "repository": {
59 | "type": "git",
60 | "url": "git+ssh://git@github.com/reaccionpy/priorizador.git"
61 | },
62 | "bugs": {
63 | "url": "https://github.com/reaccionpy/priorizador/issues"
64 | },
65 | "devDependencies": {
66 | "@testing-library/jest-dom": "^4.2.4",
67 | "@testing-library/react": "^9.4.0",
68 | "@testing-library/user-event": "^7.1.2",
69 | "babel-eslint": "10.1.0",
70 | "eslint": "^6.8.0",
71 | "eslint-config-airbnb": "^18.0.1",
72 | "eslint-config-prettier": "^6.10.0",
73 | "eslint-plugin-import": "^2.20.1",
74 | "eslint-plugin-jsx-a11y": "^6.2.3",
75 | "eslint-plugin-node": "^11.0.0",
76 | "eslint-plugin-react": "^7.18.3",
77 | "eslint-plugin-react-hooks": "^1.7.0",
78 | "husky": "^4.2.3",
79 | "lint-staged": "^10.0.7",
80 | "nodemon": "^2.0.2",
81 | "prettier": "^1.19.1",
82 | "prettier-eslint": "^9.0.1",
83 | "typescript": "^3.7.5"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/public/codium-logo-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 | codium-white96
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reaccionpy/priorizador/66f0054c66066f4e204b1251a53d670282285ce2/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
20 |
24 |
25 |
34 |
40 |
44 | Priorizador
45 |
46 |
47 | You need to enable JavaScript to run this app.
48 |
49 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/public/isotipo-codium-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 | isotipo-codium-white
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/public/isotipo-reaccion-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 | isotipo-reaccion-white
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reaccionpy/priorizador/66f0054c66066f4e204b1251a53d670282285ce2/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reaccionpy/priorizador/66f0054c66066f4e204b1251a53d670282285ce2/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/priorizador-logo-cuadrado.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reaccionpy/priorizador/66f0054c66066f4e204b1251a53d670282285ce2/public/priorizador-logo-cuadrado.png
--------------------------------------------------------------------------------
/public/priorizador_logo_1_blanco_horizontal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reaccionpy/priorizador/66f0054c66066f4e204b1251a53d670282285ce2/public/priorizador_logo_1_blanco_horizontal.png
--------------------------------------------------------------------------------
/public/reaccion-logo-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 | reaccion-logo-white
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/public/reaccion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reaccionpy/priorizador/66f0054c66066f4e204b1251a53d670282285ce2/public/reaccion.png
--------------------------------------------------------------------------------
/public/reaccion2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reaccionpy/priorizador/66f0054c66066f4e204b1251a53d670282285ce2/public/reaccion2.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | @import '~antd/dist/antd.css';
2 |
3 | .App-logo {
4 | height: 40vmin;
5 | pointer-events: none;
6 | }
7 |
8 | @media (prefers-reduced-motion: no-preference) {
9 | .App-logo {
10 | animation: App-logo-spin infinite 20s linear;
11 | }
12 | }
13 |
14 | .App-link {
15 | color: #61dafb;
16 | }
17 |
18 | @keyframes App-logo-spin {
19 | from {
20 | transform: rotate(0deg);
21 | }
22 | to {
23 | transform: rotate(360deg);
24 | }
25 | }
26 |
27 | .leaflet-container {
28 | width: 100%;
29 | height: 100%;
30 | }
31 |
32 | .information-panel {
33 | width: calc(170px + 20vmin);
34 | z-index: 1000;
35 | position: absolute;
36 | margin-top: 10px;
37 | right: 20px;
38 | opacity: 0.9;
39 | }
40 |
41 | html * {
42 | font-family: 'Nunito', sans-serif;
43 | }
44 |
45 | .header {
46 | display: flex;
47 | align-items: center;
48 | flex: 1;
49 | }
50 |
51 | .header-right {
52 | justify-content: flex-end;
53 | align-items: center;
54 | flex: 1;
55 | display: flex;
56 | }
57 |
58 | .header-logo {
59 | margin-left: 16px;
60 | }
61 |
62 | .header-info {
63 | font-size: calc(8px + 2vmin);
64 | margin-right: 8px;
65 | margin-top: 3px;
66 | color: #fff;
67 | }
68 |
69 | .header-info a {
70 | color: inherit;
71 | text-decoration: none;
72 | }
73 |
74 | .header-title {
75 | font-size: calc(8px + 2vmin);
76 | margin-right: 8px;
77 | color: #fff;
78 | justify-self: flex-start;
79 | }
80 |
81 | .header-selector {
82 | margin-left: 8px;
83 | align-items: center;
84 | width: 180px;
85 | }
86 |
87 | .ant-layout-header {
88 | padding: 0 24px;
89 | background: #121e34;
90 | }
91 |
92 | .information-panel-selector {
93 | margin-bottom: 10px;
94 | width: 100%;
95 | }
96 |
97 | .leaflet-div-icon {
98 | background: none !important;
99 | border: none !important;
100 | }
101 |
102 | .cluster-marker {
103 | color: #fff;
104 | background: #1978c8;
105 | border-radius: 50%;
106 | padding: 10px;
107 | width: 30px;
108 | height: 30px;
109 | display: flex;
110 | justify-content: center;
111 | align-items: center;
112 | }
113 |
114 | .filter-container {
115 | margin-top: 10px;
116 | margin-bottom: 10px;
117 | }
118 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect, useState } from 'react';
2 |
3 | import { useMediaQuery } from 'react-responsive';
4 | import './App.css';
5 | import { Layout } from 'antd';
6 | import CustomHeader from './components/CustomHeader';
7 | import CustomMap from './components/CustomMap';
8 | import InformationPanel from './components/InformationPanel';
9 | import { css } from '@emotion/core';
10 | import PulseLoader from 'react-spinners/PulseLoader';
11 | import localforage from 'localforage';
12 | import moment from 'moment';
13 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
14 | import About from './components/About';
15 | import Analysis from './components/Analysis';
16 |
17 | const { Header, Content } = Layout;
18 |
19 | const defaultSelectorList = [
20 | { title: 'Tekopora', value: 'tekopora', default: true },
21 | { title: 'Almuerzo escolar', value: 'almuerzo', default: false },
22 | { title: 'Fundación Paraguaya', value: 'fundacion', default: false },
23 | { title: 'Techo', value: 'techo', default: false },
24 | { title: 'Tarifa social ANDE', value: 'ande', default: false }
25 | ];
26 |
27 | const defaultHelpSourceList = [
28 | { title: 'Pintá con tu ayuda', value: 'kobo', default: true },
29 | { title: 'Cestas básicas MCDE', value: 'cestas', default: false }
30 | ];
31 |
32 | const endpoints_dict = {
33 | tekopora: 'get_tekopora_layer?departamento=10',
34 | almuerzo: 'get_almuerzo_layer?departamento=10',
35 | fundacion: 'get_fundacion_layer?departamento=10',
36 | techo: 'get_techo_layer?departamento=10',
37 | ande: 'get_ande_layer?departamento=10'
38 | };
39 |
40 | const override = css`
41 | display: block;
42 | margin: 0 auto;
43 | border-color: #121e34;
44 | position: absolute;
45 | top: 50%;
46 | left: 45%;
47 | z-index: 1000;
48 | `;
49 |
50 | function App() {
51 | const scrollInto = useRef(null);
52 |
53 | const isDesktopOrLaptop = useMediaQuery({
54 | query: '(min-width: 768px)'
55 | });
56 |
57 | useEffect(() => {
58 | scrollInto.current.scrollIntoView();
59 | });
60 |
61 | function Mapa() {
62 | const [localities, setLocalities] = useState({
63 | type: 'FeatureCollection',
64 | features: []
65 | });
66 | const [selectorList, setSelectorList] = useState(defaultSelectorList);
67 | const [currentLocality, setCurrentLocality] = useState({
68 | properties: { barlo_desc: ' ' }
69 | });
70 | const [colorBy, setColorBy] = useState('tekopora');
71 | const [district, setDistrict] = useState('CIUDAD DEL ESTE');
72 |
73 | const [markersBy, setMarkersBy] = useState('kobo');
74 | const [koboEntries, setKoboEntries] = useState([]);
75 | const [cestasEntries, setCestasEntries] = useState([]);
76 | const [isLoadingDataset, setIsLoadingDataset] = useState(false);
77 | const [availableLayers, setAvailableLayers] = useState([]);
78 |
79 | const handleLoadingDataset = isLoading => {
80 | var map = document.getElementById('map');
81 |
82 | setIsLoadingDataset(isLoading);
83 |
84 | if (isLoading) {
85 | map.style.opacity = '0.5';
86 | } else {
87 | map.style.opacity = '1';
88 | }
89 | };
90 |
91 | useEffect(() => {
92 | let mounted = true;
93 | fetch(`${process.env.REACT_APP_API_URL}/get_kobo_submissions`)
94 | .then(r => r.json())
95 | .then(data => {
96 | if (mounted) {
97 | setKoboEntries(data);
98 | }
99 | });
100 | return () => (mounted = false);
101 | }, []);
102 |
103 | useEffect(() => {
104 | let mounted = true;
105 | fetch(`${process.env.REACT_APP_API_URL}/get_cestas_submissions`)
106 | .then(r => r.json())
107 | .then(data => {
108 | if (mounted) {
109 | setCestasEntries(data);
110 | }
111 | });
112 | return () => (mounted = false);
113 | }, []);
114 |
115 | useEffect(() => {
116 | let mounted = true;
117 | fetch(
118 | `${process.env.REACT_APP_API_URL}/get_available_layers?distrito=${district}`
119 | )
120 | .then(r => r.json())
121 | .then(data => {
122 | if (mounted) {
123 | setAvailableLayers(data['layers']);
124 | }
125 | });
126 | return () => (mounted = false);
127 | }, [district, setAvailableLayers]);
128 |
129 | useEffect(() => {
130 | let mounted = true;
131 |
132 | if (availableLayers.length > 0) {
133 | if (mounted) {
134 | setSelectorList(
135 | defaultSelectorList.filter(item => {
136 | if (availableLayers.indexOf(item.value) > -1) {
137 | return true;
138 | }
139 | return false;
140 | })
141 | );
142 |
143 | if (availableLayers.indexOf(colorBy) < 0) {
144 | setColorBy(defaultSelectorList[0].value);
145 | }
146 | }
147 | }
148 | return () => (mounted = false);
149 | }, [availableLayers, colorBy]);
150 |
151 | useEffect(() => {
152 | let mounted = true;
153 | handleLoadingDataset(true);
154 | /* if the layer has been loaded and saved before (no more than 7 days),
155 | it is not necessary to load it again, it can be reused
156 | */
157 | localforage.getItem(colorBy).then(layer_data => {
158 | if (mounted) {
159 | // calculate the time difference between the current datetime and the
160 | // moment when de data was saved
161 | if (layer_data != null) {
162 | const now = moment().toDate();
163 | const stored_time = layer_data['stored_time'];
164 | const duration_stored_data = moment(now).diff(stored_time, 'days');
165 | if (layer_data['data'] == null || duration_stored_data >= 7) {
166 | fetch(
167 | `${process.env.REACT_APP_API_URL}/${endpoints_dict[colorBy]}`
168 | )
169 | .then(r => r.json())
170 | .then(data => {
171 | setLocalities(data);
172 | handleLoadingDataset(false);
173 | // save the layer data and current time
174 | const now = moment().toDate();
175 | const data_and_saved_time = {
176 | data: data,
177 | stored_time: now
178 | };
179 | localforage.setItem(colorBy, data_and_saved_time);
180 | });
181 | } else {
182 | // use the saved layer data
183 | setLocalities(layer_data['data']);
184 | handleLoadingDataset(false);
185 | }
186 | } else {
187 | fetch(`${process.env.REACT_APP_API_URL}/${endpoints_dict[colorBy]}`)
188 | .then(r => r.json())
189 | .then(data => {
190 | setLocalities(data);
191 | handleLoadingDataset(false);
192 | // save the layer data and current time
193 | const now = moment().toDate();
194 | const data_and_saved_time = {
195 | data: data,
196 | stored_time: now
197 | };
198 | localforage.setItem(colorBy, data_and_saved_time);
199 | });
200 | }
201 | }
202 | });
203 | return () => (mounted = false);
204 | }, [colorBy]);
205 |
206 | return (
207 | <>
208 |
214 |
215 |
233 | >
234 | );
235 | }
236 |
237 | return (
238 |
239 |
240 |
241 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 | );
261 | }
262 |
263 | export default App;
264 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render( );
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/components/About.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../css/about.css';
3 | import TextSwitch from './TextSwitch';
4 |
5 | export default function About() {
6 | return (
7 | <>
8 |
9 |
Priorizador v1.0
10 |
11 |
12 | El Priorizador es una herramienta que contribuye a identificar los
13 | barrios más vulnerables de un municipio y visibilizar el
14 | destino de la ayuda de fuentes públicas o privadas.
15 |
16 |
17 | La herramienta se encuentra en su primera versión y
18 | seguirá en desarrollo para facilitar la focalización
19 | basada en evidencia de políticas públicas.
20 |
21 |
22 |
23 |
24 |
25 | La pandemia COVID-19 expuso la necesidad de invertir eficientemente
26 | los pocos recursos disponibles. No hay suficientes recursos para
27 | beneficiar a todas las familias que necesitan ayuda por lo que
28 | primero se debe priorizar y canalizar ayuda hacia aquellas que
29 | más necesitan.
30 |
31 |
32 |
33 |
34 | Identificar los barrios donde las necesidades y vulnerabilidades
35 | ya existían antes de la pandemia para permitir la
36 | focalización de la ayuda en donde más se necesita.
37 |
38 |
39 | Visibilizar y analizar si la ayuda de fuentes públicas y
40 | privadas ha alcanzado prioritariamente a los barrios y familias
41 | más necesitadas del municipio.
42 |
43 |
44 |
45 |
46 |
47 |
48 | El Priorizador utiliza distintas bases de datos de fuentes oficiales
49 | del gobierno y de la sociedad civil que podrían sugerir la
50 | concentración de mayores necesidades en ciertos barrios de un
51 | municipio.
52 |
53 |
54 | Entre mayor la superposición de variables que indican
55 | vulnerabilidad en una zona, mayor la certeza de que se trata de un
56 | barrio que necesita ser priorizado. Por ejemplo, la cantidad de
57 | familias beneficiadas por el Programa Tekoporã en un
58 | mismo barrio sugiere que ese espacio geográfico tiene
59 | características diferentes a otros. Para reforzar la
60 | hipótesis de que ese barrio tiene más necesidades que
61 | otros en términos comparativos también se contabilizan
62 | otras variables relacionadas como la cantidad de familias
63 | beneficiadas por la Tarifa Social de la ANDE . Tanto
64 | Tekoporã como la Tarifa Social son programas que se asignan
65 | luego de comprobar que las familias realmente sufren cierto tipo de
66 | vulnerabilidad o pobreza.
67 |
68 |
69 | Se asume que aquellos barrios que ya tenían muchas
70 | necesidades, vulnerabilidades y pobreza antes de la pandemia,
71 | habrán empeorado desde su inicio.
72 |
73 |
74 |
75 |
76 |
77 | La herramienta tiene datos para 4 municipios de Alto Paraná:
78 | Ciudad del Este, Hernandarias, Presidente Franco y Minga
79 | Guazú. Se pueden activar o desactivar dos filtros, cuya
80 | presentación en el mapa es distinta.
81 |
82 |
83 | Todas las variables sobre subsidios que indican necesidades se
84 | exponen como mapas de calor: entre más oscuro el color, mayor
85 | la concentración y por ende mayor la necesidad.
86 |
87 |
88 | Las variables de ayuda se presentan como puntos, permitiendo que se
89 | superpongan al mapa de calor de necesidades.
90 |
91 |
92 | Idealmente, los barrios de color más oscuro en el mapa (los
93 | más necesitados) deberían tener una mayor
94 | concentración de puntos por ayuda entregada. Esto
95 | implicaría que la ayuda se concentró en donde
96 | más se necesitaba. Por otro lado, si los puntos de ayuda se
97 | concentran sobre áreas de color claro, esto
98 | significaría que los recursos no se focalizaron donde
99 | había mayor urgencia.
100 |
101 |
102 |
103 |
104 | Necesidades:
105 |
106 |
107 | Cantidad de beneficiarios del Programa Tekoporã por barrios
108 | (2020).
109 |
110 |
111 | Listado de beneficiarios de la Tarifa Social de la ANDE (2020).
112 |
113 | Mapeo de asentamientos - TECHO (2019).
114 |
115 | Instituciones Educativas que reciben Almuerzo Escolar MEC
116 | (2015-2020).
117 |
118 |
119 | Semáforo de Eliminación de la Pobreza -
120 | Fundación Paraguaya (2015).
121 |
122 |
123 |
124 | Ayuda:
125 |
126 |
127 |
128 | Pintá con Ayuda
129 | {' '}
130 | (2020).
131 |
132 |
133 | Distribución de cestas básicas - Municipalidad de
134 | Ciudad del Este (2020).
135 |
136 |
137 |
138 |
139 |
140 |
141 |
Última actualización: 27/01/2021
142 |
143 |
144 |
145 |
146 | Más información sobre el origen de la herramienta en
147 | este{' '}
148 |
149 | {' '}
150 | link{' '}
151 |
152 | .
153 |
154 |
155 | >
156 | );
157 | }
158 |
--------------------------------------------------------------------------------
/src/components/Analysis.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../css/analysis.css';
3 |
4 | export default function Analysis() {
5 | return (
6 | <>
7 |
8 |
Análisis de datos
9 |
15 |
16 | >
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/ColorScale.js:
--------------------------------------------------------------------------------
1 | import '../css/colorscale.css';
2 | import React, { useEffect, useState } from 'react';
3 |
4 | export default function ColorScale(props) {
5 | const { colorsAndQuantities, colorBy } = props;
6 | const [labels, setLabels] = useState([]);
7 | const [topic, setTopic] = useState('');
8 |
9 | useEffect(() => {
10 | const removeDuplicateArrays = array => {
11 | return Array.from(new Set(array.map(JSON.stringify)), JSON.parse);
12 | };
13 |
14 | var uniqueColorsAndQuantities = removeDuplicateArrays(colorsAndQuantities);
15 |
16 | uniqueColorsAndQuantities = uniqueColorsAndQuantities.sort((a, b) => {
17 | return a[1] - b[1];
18 | });
19 |
20 | const getMaxAndMinQuantitiesFromColor = color => {
21 | const quantitiesWithColor = uniqueColorsAndQuantities.filter(array => {
22 | return array[0] === color;
23 | });
24 | const quantitiesOnly = quantitiesWithColor.map(array => array[1]);
25 | var maxQuantity = Math.max(...quantitiesOnly);
26 | var minQuantity = Math.min(...quantitiesOnly);
27 | return { max: maxQuantity, min: minQuantity };
28 | };
29 |
30 | var availableColors = [
31 | ...new Set(uniqueColorsAndQuantities.map(array => array[0]))
32 | ];
33 |
34 | var labelsArray = [];
35 | for (var index = 0; index < availableColors.length; index++) {
36 | var color = availableColors[index];
37 | var maxAndMinQuantity = getMaxAndMinQuantitiesFromColor(color);
38 | const max = maxAndMinQuantity['max'];
39 | const min = maxAndMinQuantity['min'];
40 | const label = (
41 | <>
42 |
49 | {max !== min ? min + ' - ' + max : min}
50 |
51 |
54 | >
55 | );
56 | labelsArray.push(label);
57 | }
58 | setLabels(labelsArray.slice());
59 | }, [colorsAndQuantities]);
60 |
61 | useEffect(() => {
62 | switch (colorBy) {
63 | case 'tekopora':
64 | setTopic('Cant. de familias beneficiarias');
65 | break;
66 | case 'fundacion':
67 | setTopic('Cant. de familias en vulnerabilidad');
68 | break;
69 | case 'techo':
70 | setTopic('Cantidad de asentamientos');
71 | break;
72 | case 'almuerzo':
73 | setTopic('Cantidad de escuelas priorizadas');
74 | break;
75 | case 'ande':
76 | setTopic('Cantidad de beneficiarios');
77 | break;
78 | default:
79 | break;
80 | }
81 | }, [colorBy]);
82 |
83 | return (
84 | <>
85 | {labels.length > 1 && (
86 | <>
87 |
88 |
89 | {labels.map((label, index) => {
90 | return (
91 |
92 | {label}
93 |
94 | );
95 | })}
96 |
97 |
{topic}
98 |
99 | >
100 | )}
101 | >
102 | );
103 | }
104 |
--------------------------------------------------------------------------------
/src/components/CustomHeader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { MenuOutlined } from '@ant-design/icons';
3 | import Menu from './Menu';
4 |
5 | export default function CustomHeader(props) {
6 | const [showMenu, setShowMenu] = React.useState(false);
7 | return (
8 | <>
9 |
10 |
11 | setShowMenu(!showMenu)} />
12 |
13 |
19 |
20 |
30 |
40 |
41 |
42 |
47 | >
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/CustomMap.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useCallback } from 'react';
2 |
3 | import { Map, TileLayer, GeoJSON, Marker, Popup } from 'react-leaflet';
4 | import { debounce } from 'lodash';
5 | import GeoJsonGeometriesLookup from 'geojson-geometries-lookup';
6 | import chroma from 'chroma-js';
7 | import useSupercluster from 'use-supercluster';
8 | import L from 'leaflet';
9 | import { Sidebar, Tab } from 'react-leaflet-sidebarv2';
10 | import '../css/sidebar-v2.css';
11 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
12 | import {
13 | faFilter,
14 | faTimes,
15 | faMapMarkerAlt,
16 | faDrawPolygon,
17 | faMap
18 | } from '@fortawesome/free-solid-svg-icons';
19 | import DataSourceSelector from './DataSourceSelector';
20 | import DistrictSelector from './DistrictSelector';
21 | import ColorScale from './ColorScale';
22 |
23 | const icons = {};
24 |
25 | const fetchIcon = count => {
26 | if (!icons[count]) {
27 | icons[count] = L.divIcon({
28 | html: `
29 | ${count}
30 |
`
31 | });
32 | }
33 | return icons[count];
34 | };
35 |
36 | export default function CustomMap(props) {
37 | const [position, setPosition] = useState([-25.513475, -54.61544]);
38 |
39 | const geoJsonLayer = React.useRef();
40 |
41 | const [glookup, setGlookup] = React.useState(null);
42 | const [maxColor, setMaxColor] = React.useState(1);
43 | const [bounds, setBounds] = useState(null);
44 | const [zoom, setZoom] = useState(13);
45 | const [colorsAndQuantities, setColorsAndQuantities] = useState([]);
46 |
47 | const mapRef = React.useRef();
48 | function updateMap() {
49 | const b = mapRef.current.leafletElement.getBounds();
50 | setBounds([
51 | b.getSouthWest().lng,
52 | b.getSouthWest().lat,
53 | b.getNorthEast().lng,
54 | b.getNorthEast().lat
55 | ]);
56 | setZoom(mapRef.current.leafletElement.getZoom());
57 | }
58 |
59 | useEffect(() => {
60 | updateMap();
61 | }, []);
62 |
63 | useEffect(() => {
64 | if (geoJsonLayer.current) {
65 | // https://github.com/PaulLeCam/react-leaflet/issues/332
66 | geoJsonLayer.current.leafletElement
67 | .clearLayers()
68 | .addData(props.localities);
69 | setMaxColor(
70 | Math.max(
71 | ...props.localities.features.map(l =>
72 | l.properties.dist_desc === props.district
73 | ? l.properties[props.colorBy] || 1
74 | : 1
75 | )
76 | )
77 | );
78 | setGlookup(new GeoJsonGeometriesLookup(props.localities));
79 | }
80 | }, [props.localities, props.colorBy, props.district]);
81 |
82 | useEffect(() => {
83 | const districtPositionMap = {
84 | 'CIUDAD DEL ESTE': [-25.513475, -54.61544],
85 | HERNANDARIAS: [-25.40944, -54.63819],
86 | 'MINGA GUAZU': [-25.48881, -54.80826],
87 | 'PRESIDENTE FRANCO': [-25.57439, -54.60375]
88 | };
89 | setPosition(districtPositionMap[props.district]);
90 | }, [props.district]);
91 |
92 | const clusterPoints =
93 | props.markersBy === 'cestas' && props.cestasEntries
94 | ? props.cestasEntries.features
95 | : [];
96 |
97 | const { clusters, supercluster } = useSupercluster({
98 | points: clusterPoints,
99 | bounds,
100 | zoom,
101 | options: { radius: 75, maxZoom: 20 }
102 | });
103 |
104 | const transformByVariable = useCallback(
105 | n => {
106 | return props.colorBy === 'tekopora' ? Math.log(n) : n;
107 | },
108 | [props.colorBy]
109 | );
110 |
111 | const getValue = useCallback(
112 | properties => {
113 | const def = props.colorBy === 'tekopora' ? 1 : 0;
114 | return properties.dist_desc === props.district
115 | ? properties[props.colorBy] || def
116 | : def;
117 | },
118 | [props.colorBy, props.district]
119 | );
120 |
121 | function getStyle(feature, layer) {
122 | const scale = chroma
123 | .scale(['#fedb8b', '#a50026'])
124 | .domain([transformByVariable(maxColor), 0])
125 | .classes(3);
126 | const value = getValue(feature.properties);
127 | const color = scale(transformByVariable(value)).hex();
128 | return {
129 | color,
130 | weight: 5,
131 | opacity: 0.65
132 | };
133 | }
134 |
135 | useEffect(() => {
136 | var allColorsAndQuantities = [];
137 |
138 | const getLayerColorAndQuantity = feature => {
139 | const scale = chroma
140 | .scale(['#fedb8b', '#a50026'])
141 | .domain([transformByVariable(maxColor), 0])
142 | .classes(3);
143 | const value = getValue(feature.properties);
144 | const color = scale(transformByVariable(value)).hex();
145 |
146 | return [color, value];
147 | };
148 |
149 | for (let i = 0; i < props.localities.features.length; i++) {
150 | var feature = props.localities.features[i];
151 | var colorAndQuantity = getLayerColorAndQuantity(feature);
152 | allColorsAndQuantities.push(colorAndQuantity);
153 | }
154 | setColorsAndQuantities(allColorsAndQuantities.slice());
155 | }, [props.localities, getValue, maxColor, transformByVariable]);
156 |
157 | function onMouseMove(e) {
158 | const point = {
159 | type: 'Point',
160 | coordinates: [e.latlng.lng, e.latlng.lat]
161 | };
162 |
163 | if (glookup) {
164 | const result = glookup.getContainers(point);
165 | const locality = result.features.length
166 | ? result.features[0]
167 | : {
168 | properties: { barlo_desc: ' ' }
169 | };
170 | if (locality !== props.currentLocality) {
171 | props.onLocalityChange(locality);
172 | }
173 | }
174 | }
175 |
176 | const [collapsed, setCollapsed] = useState(false);
177 | const [selected, setSelected] = useState('home');
178 | const [showSubsidio, setShowSubsidio] = useState(true);
179 | const [showAyuda, setShowAyuda] = useState(true);
180 |
181 | const onClose = () => {
182 | setCollapsed(true);
183 | };
184 |
185 | const onOpen = id => {
186 | setCollapsed(false);
187 | setSelected(id);
188 | };
189 |
190 | return (
191 | <>
192 | }
199 | >
200 | }
204 | >
205 |
206 |
207 | Distrito
208 |
212 |
213 |
214 | Necesidad
215 | setShowSubsidio(!showSubsidio)}
220 | />
221 |
222 |
227 |
228 |
229 | Ayuda entregada
230 | setShowAyuda(!showAyuda)}
234 | />
235 |
236 |
241 |
242 |
243 |
244 |
245 |
254 |
258 | {showAyuda &&
259 | props.markersBy === 'kobo' &&
260 | props.koboEntries.map((value, index) => {
261 | return (
262 |
263 |
264 | {value.organizacion && (
265 |
266 | Organización: {value.organizacion}
267 |
268 | )}
269 | {value.nro_familias && (
270 |
271 | Número de familias: {value.nro_familias}
272 |
273 | )}
274 | {value.tipo_ayuda && (
275 |
276 | Tipo de ayuda: {value.tipo_ayuda}
277 |
278 | )}
279 |
280 |
281 | );
282 | })}
283 | {showAyuda &&
284 | props.markersBy === 'cestas' &&
285 | clusters.map((cluster, index) => {
286 | // every cluster point has coordinates
287 | const [longitude, latitude] = cluster.geometry.coordinates;
288 | // the point may be either a cluster or a crime point
289 | const {
290 | cluster: isCluster,
291 | point_count: pointCount
292 | } = cluster.properties;
293 |
294 | // we have a cluster to render
295 | if (isCluster) {
296 | return (
297 | {
302 | const expansionZoom = Math.min(
303 | supercluster.getClusterExpansionZoom(cluster.id),
304 | 17
305 | );
306 | const leaflet = mapRef.current.leafletElement;
307 | leaflet.setView([latitude, longitude], expansionZoom, {
308 | animate: true
309 | });
310 | }}
311 | />
312 | );
313 | }
314 |
315 | // we have a single point to render
316 | const {
317 | nombre_apellido: nombre,
318 | nro_ci: cedula,
319 | nro_telefono: telefono
320 | } = cluster.properties;
321 | return (
322 |
323 |
324 | {nombre && (
325 |
326 | Nombre: {nombre}
327 |
328 | )}
329 | {cedula && (
330 |
331 | CI: {cedula.toLocaleString('es')}
332 |
333 | )}
334 | {telefono && (
335 |
336 | Teléfono: {telefono}
337 |
338 | )}
339 |
340 |
341 | );
342 | })}
343 | }
344 | {showSubsidio && (
345 |
350 | )}
351 |
352 | {showSubsidio && (
353 |
357 | )}
358 | >
359 | );
360 | }
361 |
--------------------------------------------------------------------------------
/src/components/DataSourceSelector.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Select } from 'antd';
3 |
4 | const { Option } = Select;
5 |
6 | const renderOption = it => {
7 | return (
8 |
9 | {it.title}
10 |
11 | );
12 | };
13 |
14 | const DataSourceSelector = ({ onChange, value, list }) => (
15 |
21 | {list.map(renderOption)}
22 |
23 | );
24 |
25 | export default DataSourceSelector;
26 |
--------------------------------------------------------------------------------
/src/components/DistrictSelector.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Select } from 'antd';
3 |
4 | const { Option } = Select;
5 |
6 | const DistrictSelector = ({ onChange, value }) => (
7 |
13 | Ciudad del Este
14 | Hernandarias
15 | Minga Guazu
16 | Presidente Franco
17 |
18 | );
19 |
20 | export default DistrictSelector;
21 |
--------------------------------------------------------------------------------
/src/components/InformationPanel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card } from 'antd';
3 |
4 | export default function InformationPanel(props) {
5 | const contentRenderers = {
6 | dist_desc: n => (
7 |
8 | Distrito:
9 | {`${n}`}
10 |
11 | ),
12 | tekopora: n => (
13 |
14 | Tekoporã:
15 | {n > 1 ? `${n} familias beneficiarias` : `${n} familia beneficiaria`}
16 |
17 | ),
18 | fundacion: n => (
19 |
20 | Fundación Paraguaya:
21 | {n > 1
22 | ? `${n} familias en vulnerabilidad`
23 | : `${n} familia en vulnerabilidad`}
24 |
25 | ),
26 | techo: n => (
27 |
28 | Techo:
29 | {n > 1 ? `${n} asentamientos` : `${n} asentamiento`}
30 |
31 | ),
32 | almuerzo: n => (
33 |
34 | Almuerzo escolar:
35 | {n > 1 ? `${n} escuelas priorizadas` : `${n} escuela priorizada`}
36 |
37 | ),
38 | ande: n => (
39 |
40 | Tarifa social ANDE:
41 | {n > 1 ? `${n} beneficiarios` : `${n} beneficiario`}
42 |
43 | )
44 | };
45 |
46 | const content = Object.keys(props.locality.properties)
47 | .map(key =>
48 | contentRenderers[key]
49 | ? contentRenderers[key](props.locality.properties[key])
50 | : null
51 | )
52 | .filter(c => c);
53 | return (
54 |
55 | {content && content.length > 0 && (
56 | {content}
57 | )}
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/Menu.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../css/menu.css';
3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4 | import {
5 | faInfoCircle,
6 | faMap,
7 | faChartBar
8 | } from '@fortawesome/free-solid-svg-icons';
9 | import { Link } from 'react-router-dom';
10 |
11 | export default function Menu(props) {
12 | return (
13 | <>
14 | {props.isDesktop && (
15 |
18 |
props.setShowMenu(!props.showMenu)}
22 | >
23 |
24 |
25 |
26 |
27 |
Acerca de
28 |
29 |
30 |
props.setShowMenu(!props.showMenu)}
34 | >
35 |
36 |
37 |
38 |
39 |
Mapa
40 |
41 |
42 |
props.setShowMenu(!props.showMenu)}
46 | >
47 |
48 |
49 |
50 |
51 |
Análisis
52 |
53 |
54 |
55 | )}
56 | {!props.isDesktop && (
57 |
58 |
props.setShowMenu(!props.showMenu)}
62 | >
63 |
Acerca de
64 |
65 |
props.setShowMenu(!props.showMenu)}
69 | >
70 |
Mapa
71 |
72 |
props.setShowMenu(!props.showMenu)}
76 | >
77 |
Análisis
78 |
79 |
80 | )}
81 | >
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/src/components/TextSwitch.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3 | import { faPlusSquare, faMinusSquare } from '@fortawesome/free-solid-svg-icons';
4 |
5 | export default function TextSwitch(props) {
6 | const [showText, setshowText] = useState(
7 | props.showText !== undefined ? props.showText : false
8 | );
9 | let title = props.title !== undefined ? props.title : '';
10 |
11 | const onHandleSwitch = () => {
12 | setshowText(!showText);
13 | };
14 |
15 | return (
16 | <>
17 | onHandleSwitch()}>
18 |
19 |
20 | {title}
21 |
22 |
23 | {showText && props.children}
24 | >
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/css/about.css:
--------------------------------------------------------------------------------
1 | .about-content {
2 | margin: 30px;
3 | font-size: 1.4em;
4 | }
5 |
6 | .about-content h1 {
7 | text-align: center;
8 | }
9 |
10 | /* Responsive section */
11 | @media only screen and (max-width: 768px) {
12 | .about-content p {
13 | text-align: justify;
14 | }
15 |
16 | h2 {
17 | font-size: 1.1em;
18 | }
19 | }
20 | /* End of Responsive section */
21 |
--------------------------------------------------------------------------------
/src/css/analysis.css:
--------------------------------------------------------------------------------
1 | .content {
2 | margin: 30px;
3 | font-size: 1.4em;
4 | }
5 |
6 | .content h1 {
7 | text-align: center;
8 | }
9 |
10 | /* Responsive section */
11 | @media only screen and (max-width: 768px) {
12 | .content {
13 | margin: 10px;
14 | font-size: 1.4em;
15 | }
16 | .content p {
17 | text-align: justify;
18 | }
19 | }
20 | /* End of Responsive section */
21 |
--------------------------------------------------------------------------------
/src/css/colorscale.css:
--------------------------------------------------------------------------------
1 | .info {
2 | padding: 6px 8px;
3 | background: rgba(255, 255, 255, 0.8);
4 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
5 | border-radius: 5px;
6 | position: absolute;
7 | bottom: 20px;
8 | right: 5px;
9 | z-index: 1100;
10 | }
11 |
12 | .legend {
13 | text-align: center;
14 | line-height: 18px;
15 | color: #555;
16 | }
17 |
--------------------------------------------------------------------------------
/src/css/menu.css:
--------------------------------------------------------------------------------
1 | .menu,
2 | .menu-sidebar {
3 | overflow: hidden;
4 | }
5 |
6 | .menu {
7 | max-height: 500px;
8 | font-size: 1.2em;
9 | transition: max-height 0.8s;
10 | }
11 |
12 | .menu.collapsed {
13 | max-height: 0;
14 | }
15 |
16 | .menu .menu-option {
17 | padding-left: 15px;
18 | }
19 |
20 | .menu-option:hover {
21 | background-color: white;
22 | color: #121e34;
23 | }
24 |
25 | .menu-option-icon {
26 | width: 20%;
27 | float: left;
28 | }
29 |
30 | .menu-option-desc {
31 | width: 80%;
32 | }
33 |
34 | .menu-link {
35 | color: white;
36 | }
37 |
38 | /* menu-sidebar (Desktop) */
39 | .menu-sidebar {
40 | background-color: rgba(255, 255, 255, 0.93);
41 | color: #121e34;
42 | max-width: 600px;
43 | position: absolute;
44 | margin-top: 5px;
45 | margin-left: -15px;
46 | padding-right: 15px;
47 | padding-left: 15px;
48 | padding-top: 15px;
49 | text-align: center;
50 | height: 280px;
51 | font-size: 1.7em;
52 | border-radius: 5px;
53 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
54 | transition: all 0.5s;
55 | z-index: 1100;
56 | }
57 |
58 | .menu-sidebar.collapsed {
59 | padding-right: 0;
60 | padding-left: 0;
61 | max-width: 0;
62 | }
63 |
64 | .menu-sidebar .menu-option {
65 | width: 320px;
66 | }
67 |
68 | .menu-sidebar .menu-option:hover {
69 | background-color: #121e34;
70 | color: white;
71 | }
72 | .menu-sidebar .menu-link {
73 | color: #121e34;
74 | }
75 | /* End of menu-sidebar*/
76 |
--------------------------------------------------------------------------------
/src/css/sidebar-v2.css:
--------------------------------------------------------------------------------
1 | .sidebar {
2 | position: absolute;
3 | margin-top: 10px;
4 | bottom: 10px;
5 | width: 70%;
6 | overflow: hidden;
7 | z-index: 1000;
8 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65);
9 | transition: width 0.5s;
10 | height: 270px;
11 | }
12 |
13 | .sidebar.collapsed {
14 | width: 40px;
15 | bottom: auto;
16 | height: auto;
17 | }
18 |
19 | /* Estilo del control del zoom */
20 | .leaflet-left {
21 | left: 71%;
22 | }
23 |
24 | .filter-select {
25 | width: 100%;
26 | }
27 |
28 | .sidebar-map-content {
29 | width: 100%;
30 | }
31 |
32 | /* @media (min-width: 768px) and (max-width: 991px) {
33 | .sidebar {
34 | width: 260px;
35 | }
36 | .sidebar-pane {
37 | min-width: 265px;
38 | }
39 | .filter-select {
40 | width: 80%;
41 | }
42 | } */
43 |
44 | /* @media (min-width: 992px) and (max-width: 1199px) {
45 | .sidebar {
46 | width: 300px;
47 | }
48 | .filter-select {
49 | width: 60%;
50 | }
51 | } */
52 |
53 | @media (min-width: 1200px) {
54 | /* .sidebar {
55 | width: 300px;
56 | }
57 |
58 | .filter-select {
59 | width: 50%;
60 | } */
61 | }
62 |
63 | .sidebar-left {
64 | left: 10px;
65 | bottom: auto;
66 | }
67 |
68 | .sidebar-right {
69 | right: 0;
70 | }
71 |
72 | @media (min-width: 768px) {
73 | /* .sidebar{
74 | top:74px;
75 | bottom:10px;
76 | transition:width .5s
77 | } */
78 | .sidebar-left {
79 | left: 10px;
80 | }
81 | .sidebar-right {
82 | right: 10px;
83 | }
84 |
85 | .sidebar {
86 | width: 300px;
87 | }
88 |
89 | .filter-select {
90 | width: 200px;
91 | }
92 |
93 | .sidebar-map-content {
94 | width: 250px;
95 | }
96 | }
97 |
98 | .sidebar-tabs {
99 | top: 0;
100 | bottom: 0;
101 | height: 100%;
102 | background-color: #fff;
103 | }
104 |
105 | .sidebar-left .sidebar-tabs {
106 | left: 0;
107 | }
108 |
109 | .sidebar-right .sidebar-tabs {
110 | right: 0;
111 | }
112 |
113 | .sidebar-tabs,
114 | .sidebar-tabs > ul {
115 | /*position:absolute;*/
116 | width: 40px;
117 | margin: 0;
118 | padding: 0;
119 | }
120 |
121 | .sidebar-tabs > li,
122 | .sidebar-tabs > ul > li {
123 | width: 100%;
124 | height: 40px;
125 | color: #333;
126 | font-size: 12pt;
127 | overflow: hidden;
128 | transition: all 80ms;
129 | }
130 |
131 | .sidebar-tabs > li:hover,
132 | .sidebar-tabs > ul > li:hover {
133 | color: #000;
134 | background-color: #eee;
135 | }
136 |
137 | .sidebar-tabs > li.active,
138 | .sidebar-tabs > ul > li.active {
139 | color: #fff;
140 | background-color: #121e34;
141 | }
142 |
143 | .sidebar-tabs > li.disabled,
144 | .sidebar-tabs > ul > li.disabled {
145 | color: rgba(51, 51, 51, 0.4);
146 | }
147 |
148 | .sidebar-tabs > li.disabled:hover,
149 | .sidebar-tabs > ul > li.disabled:hover {
150 | background: 0 0;
151 | }
152 |
153 | .sidebar-tabs > li.disabled > a,
154 | .sidebar-tabs > ul > li.disabled > a {
155 | cursor: default;
156 | }
157 |
158 | .sidebar-tabs > li > a,
159 | .sidebar-tabs > ul > li > a {
160 | display: block;
161 | width: 100%;
162 | height: 100%;
163 | line-height: 40px;
164 | color: inherit;
165 | text-decoration: none;
166 | text-align: center;
167 | }
168 |
169 | .sidebar-tabs > ul + ul {
170 | bottom: 0;
171 | }
172 |
173 | .sidebar-content {
174 | position: absolute;
175 | top: 0;
176 | bottom: 0;
177 | background-color: rgba(255, 255, 255, 0.95);
178 | overflow-x: hidden;
179 | overflow-y: auto;
180 | }
181 |
182 | .sidebar-left .sidebar-content {
183 | left: 40px;
184 | right: 0;
185 | }
186 |
187 | .sidebar-right .sidebar-content {
188 | left: 0;
189 | right: 40px;
190 | }
191 |
192 | .sidebar.collapsed > .sidebar-content {
193 | overflow-y: hidden;
194 | }
195 |
196 | .sidebar-pane {
197 | display: none;
198 | left: 0;
199 | right: 0;
200 | box-sizing: border-box;
201 | padding: 10px 20px;
202 | }
203 |
204 | .sidebar-pane.active {
205 | display: block;
206 | }
207 |
208 | .sidebar-header {
209 | margin: -10px -20px 0;
210 | height: 40px;
211 | padding: 0 20px;
212 | line-height: 40px;
213 | font-size: 14.4pt;
214 | color: #fff;
215 | background-color: #121e34;
216 | }
217 |
218 | .sidebar-right .sidebar-header {
219 | padding-left: 40px;
220 | }
221 |
222 | .sidebar-close {
223 | position: absolute;
224 | top: 0;
225 | width: 40px;
226 | height: 40px;
227 | text-align: center;
228 | cursor: pointer;
229 | }
230 |
231 | .sidebar-left .sidebar-close {
232 | right: 0;
233 | }
234 |
235 | .sidebar-right .sidebar-close {
236 | left: 0;
237 | }
238 |
239 | /* .sidebar-left~.sidebar-map{margin-left:40px} */
240 |
241 | /* .sidebar-right~.sidebar-map{margin-right:40px} */
242 |
243 | /* .sidebar.leaflet-touch{
244 | box-shadow:none;
245 | border-right:2px solid rgba(0,0,0,.2)
246 | } */
247 |
248 | /* para animacion y posicion del control de zoom */
249 | /* .sidebar-left~.sidebar-map .leaflet-left{transition:left .5s}
250 | .sidebar-left.collapsed~.sidebar-map .leaflet-left{left:50px} */
251 | .sidebar-left ~ .sidebar-map {
252 | margin-left: 0;
253 | }
254 | .sidebar-right ~ .sidebar-map {
255 | margin-right: 0;
256 | }
257 | .sidebar {
258 | border-radius: 4px;
259 | }
260 | .sidebar.leaflet-touch {
261 | border: 2px solid rgba(0, 0, 0, 0.2);
262 | }
263 | .sidebar-left ~ .sidebar-map .leaflet-left {
264 | transition: left 0.5s;
265 | }
266 | .sidebar-left.collapsed ~ .sidebar-map .leaflet-left {
267 | left: 50px;
268 | }
269 | .sidebar-right ~ .sidebar-map .leaflet-right {
270 | transition: right 0.5s;
271 | }
272 | .sidebar-right.collapsed ~ .sidebar-map .leaflet-right {
273 | right: 50px;
274 | }
275 |
276 | @media (min-width: 768px) {
277 | .sidebar-left ~ .sidebar-map .leaflet-left {
278 | left: 310px;
279 | }
280 | }
281 |
282 | @media (min-width: 768px) and (max-width: 991px) {
283 | /* .sidebar-left ~ .sidebar-map .leaflet-left {
284 | left: 270px;
285 | } */
286 | .sidebar-right ~ .sidebar-map .leaflet-right {
287 | right: 315px;
288 | }
289 | }
290 |
291 | @media (min-width: 992px) and (max-width: 1199px) {
292 | /* .sidebar-pane {
293 | min-width: 350px;
294 | } */
295 | /* .sidebar-left ~ .sidebar-map .leaflet-left {
296 | left: 310px;
297 | } */
298 | .sidebar-right ~ .sidebar-map .leaflet-right {
299 | right: 400px;
300 | }
301 | }
302 |
303 | @media (min-width: 1200px) {
304 | /* .sidebar-pane {
305 | min-width: 420px;
306 | } */
307 | /* .sidebar-left ~ .sidebar-map .leaflet-left {
308 | left: 310px;
309 | } */
310 | .sidebar-right ~ .sidebar-map .leaflet-right {
311 | right: 470px;
312 | }
313 | }
314 |
315 | /* @media (min-width:768px){
316 | .sidebar-left~.sidebar-map{margin-left:0}
317 | .sidebar-right~.sidebar-map{margin-right:0}
318 | .sidebar{border-radius:4px}
319 | .sidebar.leaflet-touch{border:2px solid rgba(0,0,0,.2)}
320 | .sidebar-left~.sidebar-map .leaflet-left{transition:left .5s}
321 | .sidebar-left.collapsed~.sidebar-map .leaflet-left{left:50px}
322 | .sidebar-right~.sidebar-map .leaflet-right{transition:right .5s}
323 | .sidebar-right.collapsed~.sidebar-map .leaflet-right{right:50px}
324 | } */
325 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
13 |
14 | // If you want your app to work offline and load faster, you can change
15 | // unregister() to register() below. Note this comes with some pitfalls.
16 | // Learn more about service workers: https://bit.ly/CRA-PWA
17 | serviceWorker.unregister();
18 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/visualizations_dash/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/visualizations_dash/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/visualizations_dash/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/visualizations_dash/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/visualizations_dash/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/visualizations_dash/.idea/prototipo_visualizacion_dash.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/visualizations_dash/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/visualizations_dash/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.7
2 |
3 | RUN pip install uwsgi
4 | COPY requirements.txt /dash/
5 | RUN pip install -r /dash/requirements.txt
6 | COPY * /dash/
7 | RUN rm /dash/requirements.txt
8 | WORKDIR /dash
9 |
10 | CMD ["uwsgi", "--http", "0.0.0.0:8050", "--http-timeout", "900", "--module", "dash_main:server", "--processes", "1", "--threads", "5"]
11 |
--------------------------------------------------------------------------------
/visualizations_dash/Dockerfile.Celery:
--------------------------------------------------------------------------------
1 | FROM python:3.7
2 |
3 | COPY requirements.txt /celery/
4 | RUN pip install -r /celery/requirements.txt
5 | COPY * /celery/
6 | RUN rm /celery/requirements.txt
7 | WORKDIR /celery
8 |
--------------------------------------------------------------------------------
/visualizations_dash/dash_main.py:
--------------------------------------------------------------------------------
1 | import dash
2 | import dash_core_components as dcc
3 | import dash_html_components as html
4 | import plotly.express as px
5 | import plotly.graph_objects as go
6 | import pandas as pd
7 | import os
8 | from dotenv import load_dotenv
9 | import requests
10 | from shapely import geometry
11 | import statsmodels.api as sm
12 | from flask_caching import Cache
13 | from flask import Flask
14 | import datetime
15 | from flask_celery import make_celery
16 | from celery.schedules import crontab
17 | import redis
18 | import json
19 |
20 | load_dotenv()
21 | data_api_url = os.getenv("REACT_APP_API_URL")
22 | dash_debug_mode = os.getenv("DASH_DEBUG_MODE")
23 | redis_server = os.getenv("REDIS_SERVER")
24 |
25 | server = Flask(__name__)
26 |
27 | app = dash.Dash(server=server)
28 |
29 | cache = Cache(app.server, config={
30 | 'CACHE_TYPE': 'simple',
31 | })
32 | cache_timeout = 14400
33 |
34 | # configure Celery
35 | server.config['CELERY_BROKER_URL'] = 'redis://%s:6379/0' % redis_server
36 | server.config['CELERY_BACKEND'] = 'redis://%s:6379/0' % redis_server
37 | celery = make_celery(server)
38 |
39 | #configure redis
40 | redis = redis.Redis(host=redis_server, port=6379, db=0)
41 |
42 | # available options for x axis
43 | available_x_axis = [
44 | {
45 | "value": "tekopora",
46 | "label": "Cant. de beneficiarios (Tekopora)"
47 | },
48 | {
49 | "value": "almuerzo",
50 | "label": "Cant. de escuelas priorizadas (Almuerzo Escolar)"
51 | },
52 | {
53 | "value": "fundacion",
54 | "label": "Cant. de familias en vulnerabilidad (Fundación Paraguaya)"
55 | },
56 | {
57 | "value": "techo",
58 | "label": "Cant. de asentamientos (Techo)"
59 | },
60 | {
61 | "value": "ande",
62 | "label": "Cant. de beneficiarios (Tarifa social ANDE)"
63 | }
64 | ]
65 |
66 | # available options for y axis
67 | available_y_axis = [
68 | {
69 | "value": "kobo",
70 | "label": "Cant. de ayuda entregada (Pintá con tu Ayuda)"
71 | },
72 | {
73 | "value": "cestas",
74 | "label": "Cant. de cestas entregadas (Cestas básicas MCDE)"
75 | }
76 | ]
77 |
78 | # available api uris
79 | layer_data_api_uris = {
80 | 'tekopora': 'get_tekopora_layer?departamento=10',
81 | 'almuerzo': 'get_almuerzo_layer?departamento=10',
82 | 'fundacion': 'get_fundacion_layer?departamento=10',
83 | 'techo': 'get_techo_layer?departamento=10',
84 | 'ande': 'get_ande_layer?departamento=10',
85 | 'kobo': 'get_kobo_submissions',
86 | 'cestas':'get_cestas_submissions'
87 | }
88 |
89 | redis.set("precalculated_data", "{}")
90 |
91 | app.layout = html.Div([
92 | html.Div([
93 | html.H1(children='Diagrama de dispersión de subsidios y ayuda entregada'),
94 |
95 | html.Div([
96 | dcc.Dropdown(
97 | id='subsidio_type',
98 | options=[{'label': option["label"], 'value': option["value"]}
99 | for option in available_x_axis],
100 | value='tekopora',
101 | clearable=False
102 | ),
103 | dcc.RadioItems(
104 | id='crossfilter-xaxis-type',
105 | options=[{'label': i, 'value': i} for i in ['Lineal', 'Logarítmica']],
106 | value='Lineal',
107 | labelStyle={'display': 'inline-block'}
108 | )
109 | ],
110 | style={'width': '49%', 'display': 'inline-block'}),
111 |
112 | html.Div([
113 | dcc.Dropdown(
114 | id='ayuda_type',
115 | options=[{'label': option["label"], 'value': option["value"]}
116 | for option in available_y_axis],
117 | value='kobo',
118 | clearable=False
119 | ),
120 | dcc.RadioItems(
121 | id='crossfilter-yaxis-type',
122 | options=[{'label': i, 'value': i} for i in ['Lineal', 'Logarítmica']],
123 | value='Lineal',
124 | labelStyle={'display': 'inline-block'}
125 | )
126 | ], style={'width': '49%', 'float': 'right', 'display': 'inline-block'})
127 | ], style={
128 | 'borderBottom': 'thin lightgrey solid',
129 | 'backgroundColor': 'rgb(250, 250, 250)',
130 | 'padding': '10px 5px'
131 | }),
132 |
133 | html.Div([
134 | dcc.Graph(
135 | id='crossfilter-indicator-scatter'
136 | )
137 | ], style={'width': '100%'}),
138 |
139 | html.Div([
140 | dcc.Loading(
141 | id="loading-1",
142 | type="default",
143 | color="#3fa652",
144 | children=html.Div(id="loading-scatter")
145 | ),
146 | ], style={'position': 'absolute', 'top': '325px', 'left': '50%'})
147 | ])
148 |
149 |
150 | @cache.memoize(timeout=cache_timeout)
151 | def get_data_from_api(endpoint):
152 | request = requests.get("%s/%s" % (data_api_url,
153 | layer_data_api_uris[endpoint]))
154 | return request.json()
155 |
156 |
157 | def get_help_quantity_in_polygon(help_type, y_axis_data, polygon):
158 | help_quantity = 0
159 |
160 | if help_type == "kobo":
161 | index = 0
162 | while index in range(len(y_axis_data)):
163 | help_delivered_data = y_axis_data[index]
164 | help_coordinates = help_delivered_data["coordinates"]
165 | lat, lng = help_coordinates[0], help_coordinates[1]
166 | point = geometry.Point(lng, lat)
167 | if polygon.contains(point):
168 | help_quantity += int(help_delivered_data["nro_familias"])
169 | y_axis_data.pop(index)
170 | index -= 1
171 | index += 1
172 | elif help_type == "cestas":
173 | index = 0
174 | while index in range(len(y_axis_data["features"])):
175 | help_delivered_data = y_axis_data["features"][index]
176 | help_coordinates = help_delivered_data["geometry"]["coordinates"]
177 | point = geometry.Point(help_coordinates)
178 | if polygon.contains(point):
179 | help_quantity += 1
180 | y_axis_data["features"].pop(index)
181 | index -= 1
182 | index += 1
183 | return help_quantity
184 |
185 |
186 | def process_data(x_axis_data, y_axis_data, ayuda_type_value,
187 | subsidio_type_value, x_axis_label, y_axis_label):
188 | data = {x_axis_label:[], y_axis_label:[]}
189 |
190 | for feature in x_axis_data["features"]:
191 | polygon = geometry.shape(feature["geometry"])
192 | help_quantity_here = get_help_quantity_in_polygon(
193 | help_type=ayuda_type_value,
194 | y_axis_data=y_axis_data,
195 | polygon=polygon)
196 |
197 | if help_quantity_here != 0 or subsidio_type_value in feature["properties"]:
198 | data[x_axis_label].append(feature["properties"][subsidio_type_value]
199 | if subsidio_type_value in feature["properties"] else 0)
200 | data[y_axis_label].append(help_quantity_here)
201 |
202 | return data
203 |
204 |
205 | def get_data_for_graphic(subsidio_type_value, ayuda_type_value):
206 | x_axis_data = get_data_from_api(subsidio_type_value)
207 | y_axis_data = get_data_from_api(ayuda_type_value)
208 |
209 | x_axis_label = [x_option["label"] for x_option in available_x_axis
210 | if x_option["value"] == subsidio_type_value][0]
211 | y_axis_label = [y_option["label"] for y_option in available_y_axis
212 | if y_option["value"] == ayuda_type_value][0]
213 |
214 | # join data from x axis and y axis
215 | data = process_data(x_axis_data, y_axis_data, ayuda_type_value,
216 | subsidio_type_value, x_axis_label, y_axis_label)
217 |
218 | return data
219 |
220 |
221 | @celery.task(name="dash_main.precalculate_all_data_for_graphics")
222 | def precalculate_all_data_for_graphics():
223 | precalculated_data = {}
224 | all_subsidios = [x_option["value"] for x_option in available_x_axis]
225 | all_ayudas = [y_option["value"] for y_option in available_y_axis]
226 |
227 | for ayuda in all_ayudas:
228 | for subsidio in all_subsidios:
229 | data_key_name = "%s_%s" % (subsidio, ayuda)
230 | print("Precalculating data for %s" % data_key_name)
231 | data = get_data_for_graphic(subsidio, ayuda)
232 | precalculated_data[data_key_name] = {
233 | "data": data,
234 | "precalculation_date": str(datetime.datetime.now())
235 | }
236 | redis.set("precalculated_data", str(precalculated_data))
237 |
238 | @app.callback(
239 | dash.dependencies.Output('crossfilter-indicator-scatter', 'figure'),
240 | dash.dependencies.Output("loading-scatter", "children"),
241 | [dash.dependencies.Input('subsidio_type', 'value'),
242 | dash.dependencies.Input('ayuda_type', 'value'),
243 | dash.dependencies.Input('crossfilter-xaxis-type', 'value'),
244 | dash.dependencies.Input('crossfilter-yaxis-type', 'value')]
245 | )
246 | def update_graph(subsidio_type_value, ayuda_type_value, xaxis_type, yaxis_type):
247 | # get the data
248 | data_key_name = "%s_%s" % (subsidio_type_value, ayuda_type_value)
249 | precalculated_data_bytes = redis.get("precalculated_data")
250 | precalculated_data_str = precalculated_data_bytes.decode("utf-8").replace("'", '"')
251 | precalculated_data = json.loads(precalculated_data_str)
252 | if data_key_name in precalculated_data:
253 | data = precalculated_data[data_key_name]
254 | else:
255 | data = get_data_for_graphic(subsidio_type_value, ayuda_type_value)
256 | new_precalculated_data = {}
257 | new_precalculated_data[data_key_name] = {
258 | "data": data,
259 | "precalculation_date": str(datetime.datetime.now())
260 | }
261 | precalculated_data = {**precalculated_data, **new_precalculated_data}
262 | redis.set("precalculated_data", str(precalculated_data))
263 | data = precalculated_data[data_key_name]
264 | precalculation_date = datetime.datetime.strptime(data["precalculation_date"]
265 | , '%Y-%m-%d %H:%M:%S.%f')
266 |
267 | # get corresponding labels
268 | x_axis_label = [x_option["label"] for x_option in available_x_axis
269 | if x_option["value"] == subsidio_type_value][0]
270 | y_axis_label = [y_option["label"] for y_option in available_y_axis
271 | if y_option["value"] == ayuda_type_value][0]
272 |
273 | # create graphic
274 | df = pd.DataFrame(data["data"])
275 | fig = px.scatter(df, x=x_axis_label, y=y_axis_label,
276 | title="Datos actualizados al %s" % precalculation_date.strftime("%d/%m/%Y, a las %H:%M"))
277 |
278 | # linear regression
279 | if xaxis_type == 'Lineal' and yaxis_type == 'Lineal':
280 | df['regression'] = sm.OLS(df[y_axis_label],sm.add_constant(df[x_axis_label])).fit().fittedvalues
281 | fig.add_trace(go.Scatter(name='Regresión lineal', x=df[x_axis_label], y=df['regression'], mode='lines'))
282 |
283 | fig.update_xaxes(type='linear' if xaxis_type == 'Lineal' else 'log')
284 |
285 | fig.update_yaxes(type='linear' if yaxis_type == 'Lineal' else 'log')
286 |
287 | fig = go.Figure(fig)
288 | fig.update_traces(
289 | marker_size=10,
290 | marker_color='#3fa652',
291 | )
292 |
293 | return fig, None
294 |
295 | @celery.task(name="dash_main.test_celery")
296 | def test_celery():
297 | return "Testing Celery: Ok"
298 |
299 |
300 | # Configure Celery's tasks scheduler
301 | celery.conf.beat_schedule = {
302 | "update_all_precalculated_data": {
303 | "task": "dash_main.precalculate_all_data_for_graphics",
304 | "schedule": crontab(hour='3', minute='0')
305 | },
306 | "test_celery": {
307 | "task": "dash_main.test_celery",
308 | "schedule": crontab(minute='*')
309 | }
310 | }
311 |
312 |
313 | if __name__ == '__main__':
314 | debug = False if dash_debug_mode == "False" else True
315 | app.run_server(host="0.0.0.0", port=8050, debug=debug)
316 |
--------------------------------------------------------------------------------
/visualizations_dash/flask_celery.py:
--------------------------------------------------------------------------------
1 | from celery import Celery
2 |
3 | def make_celery(app):
4 | celery = Celery(
5 | app.import_name,
6 | backend=app.config['CELERY_BACKEND'],
7 | broker=app.config['CELERY_BROKER_URL']
8 | )
9 | celery.conf.update(app.config)
10 |
11 | class ContextTask(celery.Task):
12 | def __call__(self, *args, **kwargs):
13 | with app.app_context():
14 | return self.run(*args, **kwargs)
15 |
16 | celery.Task = ContextTask
17 | return celery
18 |
--------------------------------------------------------------------------------
/visualizations_dash/requirements.txt:
--------------------------------------------------------------------------------
1 | amqp==5.0.2
2 | billiard==3.6.3.0
3 | Brotli==1.0.9
4 | celery==5.0.5
5 | certifi==2020.12.5
6 | chardet==4.0.0
7 | click==7.1.2
8 | click-didyoumean==0.0.3
9 | click-plugins==1.1.1
10 | click-repl==0.1.6
11 | dash==1.18.1
12 | dash-core-components==1.14.1
13 | dash-html-components==1.1.1
14 | dash-renderer==1.8.3
15 | dash-table==4.11.1
16 | Flask==1.1.2
17 | Flask-Caching==1.9.0
18 | Flask-Compress==1.8.0
19 | future==0.18.2
20 | idna==2.10
21 | itsdangerous==1.1.0
22 | Jinja2==2.11.3
23 | joblib==1.0.0
24 | kombu==5.0.2
25 | MarkupSafe==1.1.1
26 | numpy==1.19.4
27 | pandas==1.2.0
28 | patsy==0.5.1
29 | plotly==4.14.1
30 | prompt-toolkit==3.0.10
31 | python-dateutil==2.8.1
32 | python-dotenv==0.15.0
33 | pytz==2020.5
34 | redis==3.5.3
35 | requests==2.25.1
36 | retrying==1.3.3
37 | scipy==1.6.0
38 | Shapely==1.7.1
39 | six==1.15.0
40 | statsmodels==0.12.1
41 | threadpoolctl==2.1.0
42 | urllib3==1.26.3
43 | vine==5.0.0
44 | wcwidth==0.2.5
45 | Werkzeug==1.0.1
46 |
--------------------------------------------------------------------------------