├── .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 | 14 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 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 |
242 | 243 |
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 |
  1. 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 |
  2. 38 |
  3. 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 |
  4. 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 | Priorizador 19 |
20 | Reaccion 30 | Codium 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 | 11 | ); 12 | }; 13 | 14 | const DataSourceSelector = ({ onChange, value, list }) => ( 15 | 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 | 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 | 14 | -------------------------------------------------------------------------------- /visualizations_dash/.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /visualizations_dash/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | --------------------------------------------------------------------------------