├── hooks ├── initSockets.js ├── useColors.js └── useCanvasResizer.js ├── components ├── CounterGroup.jsx ├── context │ └── regiao.js ├── CustomCheckbox.jsx ├── DatePickerButton.module.scss ├── Card.jsx ├── Footer.module.scss ├── Footer.jsx ├── Counter.jsx ├── graphs │ ├── RamGruposPrioritarios.jsx │ ├── PieVacinadosInfectadosRecuperadosObitos.jsx │ ├── PieRecebidasAdquiridas.jsx │ ├── PieAdministradasDoses.jsx │ ├── PieSuscetiveisProporcao.jsx │ ├── BarsVacinacaoArs.jsx │ ├── LineVacinadosInfecoesRecuperados.jsx │ ├── LineVacinadosEu.jsx │ ├── BarVacinadosEu.jsx │ ├── RaaMapa.jsx │ ├── RamMapa.jsx │ ├── BarVacinasRecebidaDia.jsx │ ├── RamBarAdministradasPorFaixaEtaria.jsx │ └── LineAdministradasPorFaixaEtaria.jsx ├── SimpleCounter.jsx ├── Notifications.jsx ├── CustomBarChart copy.jsx ├── CustomCheckbox.module.scss ├── DatePickerButton.jsx ├── Card.module.scss ├── Header.jsx ├── MetaTags.jsx └── Header.module.scss ├── .dockerignore ├── automation ├── fcm-conf.json ├── twitter-conf.json ├── onesignal_no_daily.txt ├── update.js ├── onesignal.txt ├── twitter_no_daily.txt ├── twitter.txt ├── pusher.js ├── convert-xls.js ├── ecdc_parser.py ├── azores_parser.py ├── convert-csv:cases.js ├── convert-csv.js ├── twitter.js ├── madeira_parser.py ├── convert-csv:vaccines.js ├── fcm.py ├── sesaram.js └── owid_parser.py ├── requirements.txt ├── public ├── favicon.ico ├── imagem.png ├── apple-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-96x96.png ├── ms-icon-144x144.png ├── ms-icon-150x150.png ├── ms-icon-310x310.png ├── ms-icon-70x70.png ├── mstile-150x150.png ├── apple-icon-57x57.png ├── apple-icon-60x60.png ├── apple-icon-72x72.png ├── apple-icon-76x76.png ├── apple-touch-icon.png ├── android-icon-144x144.png ├── android-icon-192x192.png ├── android-icon-36x36.png ├── android-icon-48x48.png ├── android-icon-72x72.png ├── android-icon-96x96.png ├── apple-icon-114x114.png ├── apple-icon-120x120.png ├── apple-icon-144x144.png ├── apple-icon-152x152.png ├── apple-icon-180x180.png ├── android-chrome-192x192.png ├── android-chrome-256x256.png ├── apple-icon-precomposed.png ├── browserconfig.xml ├── site.webmanifest ├── sitemap.xml ├── manifest.json ├── firebase-messaging-sw.js └── sw.js ├── _design ├── design.gvdesign ├── Untitled.gvdesign ├── favicon.gvdesign └── icons_svg.gvdesign ├── _readme ├── asset │ ├── infra.png │ └── exemplo.png ├── taxas.md ├── FLIGHT_CHECKLIST.md ├── SETUP.md ├── TODO_MADEIRA.MD └── SOURCES.md ├── styles ├── colors.scss ├── Home.module.scss └── globals.scss ├── pages ├── api │ ├── weeks.js │ ├── owid.js │ ├── acores │ │ ├── index.js │ │ └── pontosituacao.js │ ├── madeira │ │ ├── index.js │ │ └── pontosituacao.js │ ├── vaccinesold.js │ ├── rt │ │ ├── index.js │ │ └── [...regiao].js │ ├── messaging │ │ ├── register.js │ │ └── unregister.js │ ├── ars.js │ ├── sns.js │ ├── ecdc.js │ ├── vaccines.js │ ├── cases.js │ ├── vaccinesdssg.js │ ├── sns2.js │ ├── hooks.js │ └── sesaram.js ├── 404.js ├── index.js └── _app.js ├── assets ├── arrow.svg ├── plus.svg ├── twitter.svg ├── bell.svg ├── portugal.svg └── madeira.svg ├── wydr.js ├── Dockerfile ├── .babelrc ├── next.config.js ├── data └── last-update.json ├── run.sh ├── .env_example ├── .github └── workflows │ ├── servicos_minimos_scrap.yml │ ├── servicos_minimos.yml │ ├── servicos_minimos_owid.yml │ ├── servicos_minimos_vac.yml │ └── build.yml ├── babel.config.js.bck ├── .gitignore ├── connectors ├── mongodb.js └── firebase.js ├── package.json ├── README.md ├── utils.js └── constants.js /hooks/initSockets.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/CounterGroup.jsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /automation/fcm-conf.json: -------------------------------------------------------------------------------- 1 | {"last_update": "2021-09-27"} -------------------------------------------------------------------------------- /automation/twitter-conf.json: -------------------------------------------------------------------------------- 1 | {"last_update":"2021-10-05T01:00:00.000Z"} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | python-dateutil 3 | firebase-admin 4 | waybackpy -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/imagem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/imagem.png -------------------------------------------------------------------------------- /public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/apple-icon.png -------------------------------------------------------------------------------- /_design/design.gvdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/_design/design.gvdesign -------------------------------------------------------------------------------- /_readme/asset/infra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/_readme/asset/infra.png -------------------------------------------------------------------------------- /_design/Untitled.gvdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/_design/Untitled.gvdesign -------------------------------------------------------------------------------- /_design/favicon.gvdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/_design/favicon.gvdesign -------------------------------------------------------------------------------- /_design/icons_svg.gvdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/_design/icons_svg.gvdesign -------------------------------------------------------------------------------- /_readme/asset/exemplo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/_readme/asset/exemplo.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/mstile-150x150.png -------------------------------------------------------------------------------- /components/context/regiao.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export const RegiaoContext = React.createContext('portugal'); 3 | -------------------------------------------------------------------------------- /public/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/android-icon-144x144.png -------------------------------------------------------------------------------- /public/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/android-icon-192x192.png -------------------------------------------------------------------------------- /public/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/android-icon-36x36.png -------------------------------------------------------------------------------- /public/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/android-icon-48x48.png -------------------------------------------------------------------------------- /public/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/android-icon-72x72.png -------------------------------------------------------------------------------- /public/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/android-icon-96x96.png -------------------------------------------------------------------------------- /public/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/apple-icon-180x180.png -------------------------------------------------------------------------------- /styles/colors.scss: -------------------------------------------------------------------------------- 1 | $main: #01ae97; 2 | $shade: scale-color($main, $blackness: 40%); 3 | $tint: scale-color($main, $whiteness: 80%); -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/android-chrome-256x256.png -------------------------------------------------------------------------------- /public/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alicescfernandes/mapa-vacinacao-c19/HEAD/public/apple-icon-precomposed.png -------------------------------------------------------------------------------- /pages/api/weeks.js: -------------------------------------------------------------------------------- 1 | import weeks from './../../data/weeks.json'; 2 | 3 | export default async function handler(req, res) { 4 | res.statusCode = 200; 5 | res.json(weeks); 6 | } 7 | -------------------------------------------------------------------------------- /pages/api/owid.js: -------------------------------------------------------------------------------- 1 | import owid from './../../data/owid_filter.json'; 2 | 3 | export default async function handler(req, res) { 4 | res.statusCode = 200; 5 | res.json(owid); 6 | } 7 | -------------------------------------------------------------------------------- /pages/api/acores/index.js: -------------------------------------------------------------------------------- 1 | import acores from './../../../data/acores.json'; 2 | 3 | export default async function handler(req, res) { 4 | res.statusCode = 200; 5 | res.json(acores); 6 | } 7 | -------------------------------------------------------------------------------- /pages/api/madeira/index.js: -------------------------------------------------------------------------------- 1 | import madeira from './../../../data/madeira.json'; 2 | 3 | export default async function handler(req, res) { 4 | res.statusCode = 200; 5 | res.json(madeira); 6 | } 7 | -------------------------------------------------------------------------------- /pages/api/vaccinesold.js: -------------------------------------------------------------------------------- 1 | import vaccines from './../../data/vaccines.json'; 2 | 3 | export default async function handler(req, res) { 4 | res.statusCode = 200; 5 | res.json(vaccines); 6 | } 7 | -------------------------------------------------------------------------------- /automation/onesignal_no_daily.txt: -------------------------------------------------------------------------------- 1 | Dados da #vacinacao de 🇵🇹 Continental de hoje 2 | 3 | 👉 Total:{{total_total}} 4 | 👉 Total 1ª Dose:{{total_in1}}) 5 | 👉 Total vacinados {{total_in2}} 6 | 7 | -------------------------------------------------------------------------------- /automation/update.js: -------------------------------------------------------------------------------- 1 | let fs = require('fs'); 2 | let json = require('./../data/last-update.json'); 3 | 4 | json.date = Date.now(); 5 | fs.writeFile('./data/last-update.json', JSON.stringify(json), function () {}); 6 | -------------------------------------------------------------------------------- /pages/api/acores/pontosituacao.js: -------------------------------------------------------------------------------- 1 | import acores_pds from './../../../data/acores_pds.json'; 2 | 3 | export default async function handler(req, res) { 4 | res.statusCode = 200; 5 | res.json(acores_pds); 6 | } 7 | -------------------------------------------------------------------------------- /pages/api/madeira/pontosituacao.js: -------------------------------------------------------------------------------- 1 | import madeira_pds from './../../../data/madeira_pds.json'; 2 | 3 | export default async function handler(req, res) { 4 | res.statusCode = 200; 5 | res.json(madeira_pds); 6 | } 7 | -------------------------------------------------------------------------------- /automation/onesignal.txt: -------------------------------------------------------------------------------- 1 | Dados da #vacinacao de 🇵🇹 Continental de hoje 2 | 👉 +{{novas_total}} doses (total:{{total_total}}) 3 | 👉 +{{novas_in1}} parcialmente vacinados (total:{{total_in1}}) 4 | 👉 +{{novas_in2}} total vacinados (total:{{total_in2}}) -------------------------------------------------------------------------------- /pages/404.js: -------------------------------------------------------------------------------- 1 | // 404.js 2 | import Link from 'next/link'; 3 | 4 | export default function FourOhFour() { 5 | return ( 6 | <> 7 |

404 - Page Not Found

8 | 9 | Go back home 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /assets/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /_readme/taxas.md: -------------------------------------------------------------------------------- 1 | ## Por 100: 2 | 3 | ### 2ªs Doses por 100 pessoas 4 | 2ª Inoculação (Acumulado) / 10286300 * 100 5 | 6 | ### 1ªs Doses por 100 pessoas 7 | 1ª Inoculação (Acumulado) / 10286300 * 100 8 | 9 | ## X por pessoas para antingir imunidade de grupo 10 | (10286300 * 0.7) / 10286300 * 100 -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /wydr.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | if (process.env.NODE_ENV === 'development') { 4 | if (typeof window !== 'undefined') { 5 | const whyDidYouRender = require('@welldone-software/why-did-you-render'); 6 | whyDidYouRender(React, { 7 | trackAllPureComponents: true, 8 | }); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | MAINTAINER Alice Fernandes 4 | WORKDIR /usr/src/app 5 | 6 | COPY package.json /usr/src/app 7 | COPY yarn.lock /usr/src/app 8 | 9 | RUN yarn --frozen-lockfile 10 | 11 | COPY . ./ 12 | 13 | RUN npm link 14 | 15 | EXPOSE 3000 16 | 17 | CMD ["yarn", "start"] -------------------------------------------------------------------------------- /automation/twitter_no_daily.txt: -------------------------------------------------------------------------------- 1 | Dados da #vacinacao de 🇵🇹 Continental para {{dia}}/{{mes}}/{{ano}} 2 | 👉 Total:{{total_total}} 3 | 👉 Total 1ª Dose:{{total_in1}}) 4 | 👉 Total vacinados {{total_in2}} 5 | 6 | ➕ info 7 | covid19.min-saude.pt/vacinacao/ 8 | sns.gov.pt/monitorizacao-do-sns/vacinas-covid-19/ 9 | bit.ly/3kI7Ddo 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "next/babel", 5 | { 6 | "preset-env": { 7 | "targets": { 8 | "browsers": [ 9 | "last 10 versions", "> 0.05%" ,"ie>9" 10 | 11 | ] 12 | }, 13 | "corejs": "3.0.0", 14 | "useBuiltIns": "entry" 15 | } 16 | } 17 | ] 18 | ] 19 | } -------------------------------------------------------------------------------- /automation/twitter.txt: -------------------------------------------------------------------------------- 1 | Dados da #vacinacao de 🇵🇹 Continental para {{dia}}/{{mes}}/{{ano}} 2 | 👉 +{{novas_total}} doses (total:{{total_total}}) 3 | 👉 +{{novas_in1}} 1ª dose (total:{{total_in1}}) 4 | 👉 +{{novas_in2}} total vacinados (total:{{total_in2}}) 5 | 6 | ➕ info 7 | covid19.min-saude.pt/vacinacao/ 8 | sns.gov.pt/monitorizacao-do-sns/vacinas-covid-19/ 9 | bit.ly/3kI7Ddo 10 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withReactSvg = require('next-react-svg'); 2 | const path = require('path'); 3 | module.exports = withReactSvg({ 4 | distDir: process.env.NODE_ENV === 'development' ? '.next_dev' : '.next', 5 | include: path.resolve(__dirname, 'assets/'), 6 | optimizeFonts: false, 7 | webpack5: false, 8 | eslint: { 9 | ignoreDuringBuilds: true, //TODO: Setup eslint 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /data/last-update.json: -------------------------------------------------------------------------------- 1 | {"date":1645450820524,"dateVaccines":"2022-02-19T00:00:00.000Z","dateSnsStartWeirdFormat":"26/07/21","dateSnsStart":"Mon Nov 08 2021","dateSns":"Sun Nov 14 2021","dateEcdc":"2022-01-09","dateOwid":"2022-01-19","dateCases":1645315200000,"dateMadeira":"2021-08-01","dateMadeiraCases":"2022-01-31","dateAcores":"2021-07-29","dateAcoresCases":"2021-06-01","week":48,"dateRt":"2020-02-21T00:00:00.000Z"} -------------------------------------------------------------------------------- /pages/api/rt/index.js: -------------------------------------------------------------------------------- 1 | const regioes = { 2 | continente: '/api/rt/continente', 3 | nacional: '/api/rt/nacional', 4 | alentejo: '/api/rt/alentejo', 5 | algarve: '/api/rt/algarve', 6 | lvt: '/api/rt/lvt', 7 | madeira: '/api/rt/madeira', 8 | acores: '/api/rt/acores', 9 | todas: '/api/rt/todas', 10 | }; 11 | 12 | export default async function handler(req, res) { 13 | res.end(JSON.stringify(regioes)); 14 | } 15 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo 1 3 | # cd ~/mapa-vacinacao-c19 4 | # git checkout gh-pages -f 5 | # git checkout master -f 6 | #git pull 7 | #yarn 8 | #pm2 stop next 9 | #pm2 restart next 10 | #pm2 start yarn --name "next" -x -- start 11 | 12 | 13 | git checkout master -f 14 | git pull 15 | yarn install --frozen-lockfile 16 | yarn build 17 | #pm2 stop next 18 | pm2 restart next 19 | #pm2 start yarn --name "next" -x -- start 20 | -------------------------------------------------------------------------------- /pages/api/messaging/register.js: -------------------------------------------------------------------------------- 1 | import { FirebaseConnector } from './../../../connectors/firebase'; 2 | 3 | export default async function handler(req, res) { 4 | if (req.method === 'POST' && req.body?.fcm_token !== undefined) { 5 | let firebase = new FirebaseConnector(); 6 | firebase.registerDevice(req.body.fcm_token).then(() => { 7 | console.log('registered'); 8 | }); 9 | } 10 | res.statusCode = 200; 11 | res.json({}); 12 | } 13 | -------------------------------------------------------------------------------- /pages/api/messaging/unregister.js: -------------------------------------------------------------------------------- 1 | import { FirebaseConnector } from './../../../connectors/firebase'; 2 | 3 | export default async function handler(req, res) { 4 | if (req.method === 'POST' && req.body?.fcm_token !== undefined) { 5 | let firebase = new FirebaseConnector(); 6 | firebase.unregisterDevice(req.body.fcm_token).then(() => { 7 | console.log('registered'); 8 | }); 9 | } 10 | res.statusCode = 200; 11 | res.json({}); 12 | } 13 | -------------------------------------------------------------------------------- /_readme/FLIGHT_CHECKLIST.md: -------------------------------------------------------------------------------- 1 | 2 | # Down Server 3 | In case of a down server 4 | 1. Head up to Cloudflare 5 | 2. Activate the Backup Server Rule 6 | 2. Deactivate the AWS Server Rule 7 | 3. Await for the redirects to take place 8 | 4. Log into machine 9 | 5. `pm2 logs --lines 100` 10 | 6. Debug build until it works. 11 | Since the requests will be redirected to the backup server, you will need to rely on https://aws.vacinacaocovid19.pt to see the aws build 12 | -------------------------------------------------------------------------------- /automation/pusher.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: './../.env' }); 2 | const Pusher = require('pusher'); 3 | 4 | const pusher = new Pusher({ 5 | appId: process.env.PUSHER_APP_ID, 6 | key: process.env.PUSHER_APP_KEY, 7 | secret: process.env.PUSHER_APP_SECRET, 8 | cluster: 'eu', 9 | useTLS: true, 10 | }); 11 | 12 | function publishEvent(type, data) { 13 | pusher.trigger('covid19', 'update', { 14 | type, 15 | data, 16 | }); 17 | } 18 | 19 | publishEvent('reload'); 20 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-256x256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import { Col, Container, Row } from 'react-bootstrap'; 2 | 3 | import { Card } from '../components/Card'; 4 | 5 | export default function Home() { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 12 |

Este projeto já não está a ser mantido, e já não vai ser mais atualizado

13 |

A quem acompanhou e participou, obrigado.

14 |
15 | 16 |
17 |
18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /public/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | https://vacinacaocovid19.pt/ 12 | 2021-03-02T13:55:38+00:00 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.env_example: -------------------------------------------------------------------------------- 1 | CONSUMER_KEY="" 2 | CONSUMER_SECRET="" 3 | 4 | ACCESS_TOKEN_KEY="" 5 | ACCESS_TOKEN_SECRET="" 6 | 7 | PUSHER_APP_ID="" 8 | PUSHER_APP_KEY="" 9 | PUSHER_APP_SECRET="" 10 | 11 | HOOTSUITE_KEY="" 12 | HOOTSUITE_SECRET="" 13 | 14 | LOGFLARE_API_KEY="" 15 | LOGFLARE_SOURCE="" 16 | 17 | ONESIGNAL_REST_API_KEY='' 18 | ONESIGNAL_APP_ID='' 19 | 20 | HOOKS_SECRET='' 21 | HOOKS_USER="" 22 | HOOKS_HEADER_NAME="" 23 | HOOKS_SHA="" 24 | 25 | FIREBASE_type = "" 26 | FIREBASE_project_id = "" 27 | FIREBASE_private_key_id = "" 28 | FIREBASE_private_key = "" 29 | FIREBASE_client_email = "" 30 | FIREBASE_client_id = "" -------------------------------------------------------------------------------- /pages/api/ars.js: -------------------------------------------------------------------------------- 1 | import Cors from 'cors'; 2 | import ars from './../../data/ars.json'; 3 | function initMiddleware(middleware) { 4 | return (req, res) => 5 | new Promise((resolve, reject) => { 6 | middleware(req, res, (result) => { 7 | if (result instanceof Error) { 8 | return reject(result); 9 | } 10 | return resolve(result); 11 | }); 12 | }); 13 | } 14 | 15 | const cors = initMiddleware( 16 | Cors({ 17 | methods: ['GET'], 18 | }) 19 | ); 20 | 21 | export default async function handler(req, res) { 22 | await cors(req, res); 23 | res.statusCode = 200; 24 | res.json(ars); 25 | } 26 | -------------------------------------------------------------------------------- /pages/api/sns.js: -------------------------------------------------------------------------------- 1 | import Cors from 'cors'; 2 | import sns from './../../data/sns.json'; 3 | function initMiddleware(middleware) { 4 | return (req, res) => 5 | new Promise((resolve, reject) => { 6 | middleware(req, res, (result) => { 7 | if (result instanceof Error) { 8 | return reject(result); 9 | } 10 | return resolve(result); 11 | }); 12 | }); 13 | } 14 | 15 | const cors = initMiddleware( 16 | Cors({ 17 | methods: ['GET'], 18 | }) 19 | ); 20 | 21 | export default async function handler(req, res) { 22 | await cors(req, res); 23 | res.statusCode = 200; 24 | res.json(sns); 25 | } 26 | -------------------------------------------------------------------------------- /assets/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/api/ecdc.js: -------------------------------------------------------------------------------- 1 | import Cors from 'cors'; 2 | import ecdc from './../../data/ecdc_filtered.json'; 3 | 4 | function initMiddleware(middleware) { 5 | return (req, res) => 6 | new Promise((resolve, reject) => { 7 | middleware(req, res, (result) => { 8 | if (result instanceof Error) { 9 | return reject(result); 10 | } 11 | return resolve(result); 12 | }); 13 | }); 14 | } 15 | 16 | const cors = initMiddleware( 17 | Cors({ 18 | methods: ['GET'], 19 | }) 20 | ); 21 | 22 | export default async function handler(req, res) { 23 | await cors(req, res); 24 | res.statusCode = 200; 25 | res.json(ecdc); 26 | } 27 | -------------------------------------------------------------------------------- /pages/api/vaccines.js: -------------------------------------------------------------------------------- 1 | import Cors from 'cors'; 2 | import vaccines from './../../data/vaccines_v2.json'; 3 | function initMiddleware(middleware) { 4 | return (req, res) => 5 | new Promise((resolve, reject) => { 6 | middleware(req, res, (result) => { 7 | if (result instanceof Error) { 8 | return reject(result); 9 | } 10 | return resolve(result); 11 | }); 12 | }); 13 | } 14 | 15 | const cors = initMiddleware( 16 | Cors({ 17 | methods: ['GET'], 18 | }) 19 | ); 20 | 21 | export default async function handler(req, res) { 22 | await cors(req, res); 23 | res.statusCode = 200; 24 | res.json(vaccines); 25 | } 26 | -------------------------------------------------------------------------------- /_readme/SETUP.md: -------------------------------------------------------------------------------- 1 | ## Project Setup 2 | 3 | 1. Create a Twitter account and get the developer mode enabled 4 | 2. Create a Hootsuite account and get the developer mode enabled 5 | 3. Create a Pusher account 6 | 4. Create a Logflare account 7 | 4. Setup Github Hooks 8 | 5. Write a .env file with the keys and secrets. Look to the `.env_example` for the keys you have to setup 9 | 6. Run `npm link` 10 | 7. Run `daemon_data` from the root folder 11 | 8. Alternatively, you can run `./daemon.js` from the root folder 12 | 9. The updater script will run between 12h and 15h every day. You can run this on a `screen` 13 | 9. You can also deploy using `pm2` -------------------------------------------------------------------------------- /pages/api/rt/[...regiao].js: -------------------------------------------------------------------------------- 1 | const regioes = { 2 | continente: 'rt_continente.json', 3 | nacional: 'rt_nacional.json', 4 | alentejo: 'rt_alentejo.json', 5 | algarve: 'rt_algarve.json', 6 | lvt: 'rt_lvt.json', 7 | madeira: 'rt_ram.json', 8 | centro: 'rt_centro.json', 9 | norte: 'rt_norte.json', 10 | acores: 'rt_raa.json', 11 | todas: 'rt_todas.json', 12 | }; 13 | 14 | export default async function handler(req, res) { 15 | const { regiao } = req.query; 16 | if (!regiao || !(regiao in regioes)) { 17 | res.statusCode = 404; 18 | res.end(); 19 | } 20 | 21 | let data = await import(`./../../../data/rt/${regioes[regiao]}`); 22 | res.end(JSON.stringify(data.default)); 23 | } 24 | -------------------------------------------------------------------------------- /components/CustomCheckbox.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import styles from './CustomCheckbox.module.scss'; 3 | 4 | export function CustomCheckbox({ onChange, label, checked, styles: propStyles }) { 5 | let [isChecked, setIsChecked] = useState(checked || false); 6 | function wrapper(el) { 7 | setIsChecked(el.target.checked); 8 | onChange(el.target.checked); 9 | } 10 | 11 | return ( 12 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/DatePickerButton.module.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/colors.scss'; 2 | 3 | 4 | .datepicker { 5 | appearance: none; 6 | background: none; 7 | border: none; 8 | font-weight: 400; 9 | color: #444444; 10 | vertical-align: middle; 11 | line-height: 12px; 12 | 13 | svg { 14 | fill: $main; 15 | display: inline; 16 | vertical-align: middle; 17 | 18 | &:hover { 19 | fill: scale-color($main, $blackness: 35%); // #81717fd; 20 | display: inline; 21 | vertical-align: middle; 22 | } 23 | } 24 | 25 | &:first-child { 26 | svg { 27 | transform: rotate(180deg); 28 | } 29 | } 30 | &:disabled { 31 | opacity: 0.3; 32 | 33 | svg { 34 | fill: #ddd; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pages/api/cases.js: -------------------------------------------------------------------------------- 1 | import convertCases from '../../automation/convert-csv:cases'; 2 | import * as fs from 'fs'; 3 | 4 | /* import cases from './../../data/cases_v2.json'; 5 | 6 | export default async function handler_old(req, res) { 7 | res.statusCode = 200; 8 | res.json(cases); 9 | } 10 | 11 | */ 12 | export default async function handler(req, res) { 13 | let data = {}; 14 | try { 15 | data = await convertCases(); 16 | console.log('remote'); 17 | } catch (e) { 18 | let filedir = process.cwd() + '/data/cases_v2.json'; 19 | data = JSON.parse(fs.readFileSync(filedir)); //fallback to the stored data 20 | } 21 | res.statusCode = 200; 22 | res.json(data); //get data directly from the enpoint 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/servicos_minimos_scrap.yml: -------------------------------------------------------------------------------- 1 | name: Serviços Minimos - WaybackPI 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | logLevel: 6 | description: 'Log level' 7 | required: true 8 | default: 'warning' 9 | schedule: 10 | - cron: '00 21 * * *' 11 | jobs: 12 | waybackpi: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Wayback PI 16 | run: | 17 | pip3 install waybackpy 18 | waybackpy --save --url "https://www.sns.gov.pt/monitorizacao-do-sns/vacinas-covid-19/" 19 | waybackpy --save --url "https://vacinacao-covid19.azores.gov.pt/" 20 | -------------------------------------------------------------------------------- /babel.config.js.bck: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = function (api) { 3 | //const isServer = api.caller((caller) => caller.isServer); 4 | //const isCallerDevelopment = api.caller((caller) => caller.isDev); 5 | 6 | const presets = [ 7 | [ 8 | 'next/babel', 9 | { 10 | 'preset-react': { 11 | //importSource: !isServer && isCallerDevelopment ? '@welldone-software/why-did-you-render' : 'react', 12 | importSource: 'react', 13 | }, 14 | 'preset-env': { 15 | targets: { 16 | browsers: ['last 10 versions', '> 0.05%', 'ie>11'], 17 | }, 18 | corejs: '3.0.0', 19 | useBuiltIns: 'entry', 20 | }, 21 | }, 22 | ], 23 | ]; 24 | 25 | return { presets }; 26 | }; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /automation/node_modules 6 | .env 7 | /.pnp 8 | .pnp.js 9 | 10 | .history/* 11 | 12 | #automation/twitter-conf.json 13 | automation/onesignal-conf.json 14 | #automation/fcm-conf.json 15 | automation/hootsuite-conf.json 16 | # testing 17 | /coverage 18 | 19 | /.ignore 20 | firebase_account.json 21 | 22 | .env_keys.json 23 | # next.js 24 | /.next_dev/ 25 | /out/ 26 | 27 | # production 28 | /build 29 | 30 | # misc 31 | .DS_Store 32 | *.pem 33 | 34 | # debug 35 | npm-debug.log* 36 | yarn-debug.log* 37 | yarn-error.log* 38 | 39 | # local env files 40 | .env.local 41 | .env.development.local 42 | .env.test.local 43 | .env.production.local 44 | 45 | # vercel 46 | .vercel 47 | -------------------------------------------------------------------------------- /connectors/mongodb.js: -------------------------------------------------------------------------------- 1 | //var client = require('mongodb').MongoClient; 2 | //var url = 'mongodb://localhost:27017/fcm'; 3 | /* client.connect(url, function (err, db) { 4 | if (err) throw err; 5 | var dbo = db.db('fcm'); 6 | let collection = dbo.collection('fcm_tokens'); 7 | let record_exists = false; 8 | let record = { token: req.body.fcm_token }; 9 | collection.findOne(record, function (err, res) { 10 | if (err) { 11 | res.statusCode = 400; 12 | } 13 | 14 | if (res !== null) { 15 | record_exists = true; 16 | console.log('fcm exists'); 17 | } else { 18 | collection.insertOne(record, function (err, res) { 19 | if (err) { 20 | res.statusCode = 400; 21 | } 22 | console.log('fcm added'); 23 | db.close(); 24 | }); 25 | } 26 | }); 27 | }); */ 28 | -------------------------------------------------------------------------------- /components/Card.jsx: -------------------------------------------------------------------------------- 1 | import styles from './Card.module.scss'; 2 | import classNames from 'classnames'; 3 | 4 | export function Card({ children, type, allowOverflow, isUpdating, textLeft, classes, sticky, is_dynamic_scroll = true, is_fixed_scroll }) { 5 | let styles2 = {}; 6 | styles2[styles.card_align_left] = textLeft; 7 | styles2[styles.card_graph_updated] = isUpdating; 8 | styles2[styles.card_chart] = allowOverflow; 9 | styles2[styles.card_sticky] = sticky; 10 | styles2[styles.card_counter] = type === 'counter'; 11 | styles2[styles.card_fixed_scroll] = is_fixed_scroll && type !== 'counter'; 12 | styles2[styles.card_dynamic_scroll] = is_dynamic_scroll && !is_fixed_scroll && type !== 'counter'; 13 | 14 | let className = classNames(styles.card_graph, styles2); 15 | return ( 16 |
17 |
{children}
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /hooks/useColors.js: -------------------------------------------------------------------------------- 1 | // import { useEffect, useState } from 'react'; 2 | import { useState } from 'react'; 3 | import * as constants from '../constants'; 4 | 5 | export function useColors() { 6 | let [colors, setColors] = useState([constants.FOREGROUND_COLOR, constants.COLOR_1, constants.COLOR_2, constants.COLOR_3, constants.COLOR_4]); 7 | 8 | let [tints, setTints] = useState([constants.TINT_30, constants.TINT_50, constants.TINT_70]); 9 | let [shades, setShades] = useState([constants.SHADE_30, constants.SHADE_50, constants.SHADE_70]); 10 | let [complements, setComplements] = useState([constants.COMPLEMENT_1, constants.COMPLEMENT_2, constants.COMPLEMENT_3]); 11 | 12 | let [foreground, color_1, color_2, color_3, color_4] = colors; 13 | return { 14 | colors: [foreground, color_1, color_2, color_3, color_4], 15 | colors_v2: { 16 | main: constants.FOREGROUND_COLOR, 17 | tints, 18 | shades, 19 | complements, 20 | }, 21 | setColors, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /assets/bell.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/Footer.module.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/colors.scss'; 2 | 3 | .footer { 4 | background-color: white; 5 | height: 80px; 6 | display: flex; 7 | align-items: center; 8 | border-top: 1px solid rgba(0, 0, 0, 0.1); 9 | .content { 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | 14 | justify-content: space-between; 15 | 16 | ul { 17 | padding: 0; 18 | margin: 0; 19 | } 20 | 21 | a { 22 | color: inherit; 23 | } 24 | 25 | li, 26 | p { 27 | display: inline-block; 28 | margin: 0px; 29 | text-align: left; 30 | font-weight: bold; 31 | font-size: 13px; 32 | color: #444444; 33 | &:last-child { 34 | margin-right: 0px; 35 | } 36 | 37 | &.update { 38 | font-weight: 400; 39 | margin-top: 5px; 40 | } 41 | } 42 | li { 43 | margin-right: 15px; 44 | } 45 | } 46 | .logo { 47 | color: $main; 48 | font-weight: 900; 49 | text-transform: uppercase; 50 | font-size: 20px; 51 | margin: 0; 52 | } 53 | } 54 | 55 | p.update { 56 | font-weight: 400; 57 | margin-top: 5px; 58 | } 59 | -------------------------------------------------------------------------------- /pages/api/vaccinesdssg.js: -------------------------------------------------------------------------------- 1 | import Cors from 'cors'; 2 | import convertVaccines from '../../automation/convert-csv:vaccines'; 3 | import * as fs from 'fs'; 4 | 5 | function initMiddleware(middleware) { 6 | //comment 7 | return (req, res) => 8 | new Promise((resolve, reject) => { 9 | middleware(req, res, (result) => { 10 | if (result instanceof Error) { 11 | return reject(result); 12 | } 13 | return resolve(result); 14 | }); 15 | }); 16 | } 17 | 18 | const cors = initMiddleware( 19 | Cors({ 20 | methods: ['GET'], 21 | }) 22 | ); 23 | 24 | export default async function handler(req, res) { 25 | await cors(req, res); 26 | let data = {}; 27 | console.log(1); 28 | 29 | data = await convertVaccines(); 30 | 31 | res.statusCode = 200; 32 | res.json(data); //get data directly from the enpoint 33 | } 34 | 35 | /* 36 | //Use the next-js way 37 | 38 | import vaccines_json from './../../data/vaccines_dssg.json'; 39 | 40 | export async function handler_local(req, res) { 41 | await cors(req, res); 42 | res.statusCode = 200; 43 | res.json(vaccines_json); 44 | } 45 | */ 46 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme_color" : "#01AE97", 3 | "background_color" : "#01AE97", 4 | "display" : "standalone", 5 | "scope" : "/", 6 | "start_url" : "/", 7 | "name" : "Vacinação - COVID19", 8 | "short_name" : "Vacinação - COVID19", 9 | "permissions": [ 10 | "background" 11 | ], 12 | "icons": [ 13 | { 14 | "src": "\/android-icon-36x36.png", 15 | "sizes": "36x36", 16 | "type": "image\/png", 17 | "density": "0.75" 18 | }, 19 | { 20 | "src": "\/android-icon-48x48.png", 21 | "sizes": "48x48", 22 | "type": "image\/png", 23 | "density": "1.0" 24 | }, 25 | { 26 | "src": "\/android-icon-72x72.png", 27 | "sizes": "72x72", 28 | "type": "image\/png", 29 | "density": "1.5" 30 | }, 31 | { 32 | "src": "\/android-icon-96x96.png", 33 | "sizes": "96x96", 34 | "type": "image\/png", 35 | "density": "2.0" 36 | }, 37 | { 38 | "src": "\/android-icon-144x144.png", 39 | "sizes": "144x144", 40 | "type": "image\/png", 41 | "density": "3.0" 42 | }, 43 | { 44 | "src": "\/android-icon-192x192.png", 45 | "sizes": "192x192", 46 | "type": "image\/png", 47 | "density": "4.0" 48 | } 49 | ] 50 | } -------------------------------------------------------------------------------- /connectors/firebase.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: './../.env' }); 2 | const admin = require('firebase-admin'); 3 | 4 | export class FirebaseConnector { 5 | initialized = false; 6 | fcm = null; 7 | constructor() { 8 | if (!admin.apps.length) { 9 | admin.initializeApp({ 10 | credential: admin.credential.cert({ 11 | project_id: process.env.FIREBASE_project_id, 12 | private_key_id: process.env.FIREBASE_private_key_id, 13 | private_key: process.env.FIREBASE_private_key.replace(/\\n/g, '\n'), 14 | client_email: process.env.FIREBASE_client_email, 15 | client_id: process.env.FIREBASE_client_id, 16 | }), 17 | }); 18 | } 19 | this.initialized = true; 20 | this.fcm = admin.messaging(); 21 | } 22 | 23 | registerDevice(token) { 24 | console.log('cenas', token); 25 | return this.fcm.subscribeToTopic([token], 'covid19'); 26 | /* .then(function (response) { 27 | console.log('Successfully subscribed to topic:', response); 28 | }) 29 | .catch(function (error) { 30 | console.log('Error subscribing to topic:', error); 31 | }); */ 32 | } 33 | 34 | unregisterDevice(token) { 35 | return this.fcm.unsubscribeToTopic([token], 'covid19'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import './../wydr'; 2 | import 'bootstrap/dist/css/bootstrap.min.css'; 3 | import 'react-datepicker/dist/react-datepicker.css'; 4 | import '../styles/globals.scss'; 5 | import '@babel/polyfill'; 6 | import { Footer } from '../components/Footer'; 7 | import { Metatags } from '../components/MetaTags'; 8 | import { Header } from '../components/Header'; 9 | import { RegiaoContext } from './../components/context/regiao'; 10 | import { useEffect } from 'react'; 11 | 12 | let allowed_regioes = ['/', 'acores', 'madeira']; 13 | function NextApp({ Component, props }) { 14 | // Unconventional way of not having multiple sockets connected between pages 15 | useEffect(() => {}, []); 16 | return ( 17 | <> 18 | 19 |
20 | 21 |
22 | 23 | ); 24 | } 25 | 26 | NextApp.getInitialProps = async (app) => { 27 | let url = app?.ctx?.req?.url.replace('/', '') || app.ctx.pathname.replace('/', ''); 28 | url = url.split('?')[0]; 29 | let regiao = 'portugal'; 30 | if (url !== '') { 31 | regiao = url; 32 | } 33 | 34 | if (!allowed_regioes.includes(regiao)) regiao = 'portugal'; 35 | return { props: { regiao: regiao, pusher: '' } }; 36 | }; 37 | 38 | export default NextApp; 39 | -------------------------------------------------------------------------------- /hooks/useCanvasResizer.js: -------------------------------------------------------------------------------- 1 | // import { useEffect, useState } from 'react'; 2 | import { useEffect, useState } from 'react'; 3 | import { RESIZE_TRESHOLD } from '../constants'; 4 | 5 | export function useCanvasResizer() { 6 | let canvasLoaded = false; 7 | let [canvasNode, setCanvasNode] = useState(false); 8 | 9 | function attatchResizer(canvasNode) { 10 | if (canvasLoaded === false && canvasNode !== false) { 11 | canvasLoaded = true; 12 | 13 | if (window.innerWidth <= RESIZE_TRESHOLD) { 14 | canvasNode.style.width = RESIZE_TRESHOLD + 'px'; 15 | } else { 16 | canvasNode.style.width = 'auto'; 17 | } 18 | window.addEventListener('resize', () => { 19 | if (window.innerWidth <= RESIZE_TRESHOLD) { 20 | canvasNode.style.width = RESIZE_TRESHOLD + 'px'; 21 | } else { 22 | canvasNode.style.width = '100%'; 23 | } 24 | }); 25 | 26 | return () => { 27 | window.removeEventListener('resize', () => { 28 | if (window.innerWidth <= RESIZE_TRESHOLD) { 29 | canvasNode.style.width = RESIZE_TRESHOLD + 'px'; 30 | } else { 31 | canvasNode.style.width = '100%'; 32 | } 33 | }); 34 | }; 35 | } 36 | } 37 | 38 | useEffect(() => attatchResizer(canvasNode), [canvasNode]); 39 | 40 | return { 41 | canvasNode, 42 | setCanvasNode, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /styles/Home.module.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/colors.scss'; 2 | 3 | .datepickerRow { 4 | margin-top: 20px; 5 | margin-bottom: 20px; 6 | } 7 | 8 | .link { 9 | border-bottom: 2px solid $main; 10 | } 11 | 12 | .sources_block { 13 | padding: 20px 15px; 14 | } 15 | 16 | .title { 17 | font-weight: bold; 18 | color: #444; 19 | font-size: 15px; 20 | margin-bottom: 8px; 21 | padding-right: 15px; 22 | } 23 | 24 | .subtitle { 25 | color: darkgrey; 26 | font-size: 13px; 27 | margin-bottom: 8px; 28 | line-height: 20px; 29 | padding-right: 15px; 30 | } 31 | 32 | .text { 33 | color: #444; 34 | font-size: 15px; 35 | line-height: 28px; 36 | } 37 | 38 | .alert { 39 | border-radius: 5px; 40 | font-family: 'Courier New', Courier, monospace; 41 | padding: 15px 5px; 42 | width: 100vw; 43 | p { 44 | margin: 0px; 45 | font-size: 13px; 46 | line-height: 20px; 47 | } 48 | 49 | &_fill { 50 | color: white; 51 | margin: 0px; 52 | border-radius: 0px; 53 | background-color: $main; 54 | border-bottom: 0px; 55 | font-weight: 500; 56 | } 57 | } 58 | 59 | .datepicker_static { 60 | appearance: none; 61 | background: none; 62 | border: none; 63 | font-weight: 400; 64 | color: #444444; 65 | vertical-align: middle; 66 | /* line-height: 12px; */ 67 | text-transform: none; 68 | text-align: center; 69 | font-size: 16px !important; 70 | margin: 20px 0px; 71 | } 72 | -------------------------------------------------------------------------------- /components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import { Container, Row, Col } from 'react-bootstrap'; 2 | import styles from './Footer.module.scss'; 3 | import json from './../data/last-update.json'; 4 | import cardStyles from '../components/Card.module.scss'; 5 | 6 | export function Footer({ last }) { 7 | let options = { 8 | year: 'numeric', 9 | month: 'long', 10 | day: 'numeric', 11 | hour: 'numeric', 12 | minute: 'numeric', 13 | second: 'numeric', 14 | }; 15 | let f = new Intl.DateTimeFormat('pt-PT', options); 16 | let options2 = { 17 | year: 'numeric', 18 | month: 'long', 19 | day: 'numeric', 20 | }; 21 | let f2 = new Intl.DateTimeFormat('pt-PT', options2); 22 | return ( 23 | <> 24 | 45 | 46 | ); 47 | // 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/servicos_minimos.yml: -------------------------------------------------------------------------------- 1 | name: Serviços Minimos - SESARAM & Cases 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | logLevel: 6 | description: 'Log level' 7 | required: true 8 | default: 'warning' 9 | schedule: 10 | - cron: '00 22 * * *' 11 | #some comment 12 | jobs: 13 | build-and-deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 🛎️ 17 | uses: actions/checkout@v2.3.1 18 | with: 19 | fetch-depth: 3 20 | token: ${{ secrets.PAT}} 21 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 22 | run: yarn install --frozen-lockfile 23 | - name: Update SESARAM 24 | run: | 25 | echo $USER 26 | git fetch 27 | git config --global user.name "$user" 28 | git config --global user.email "$email" 29 | ./daemon.js --scrap sesaram 30 | ./daemon.js --scrap cases 31 | 32 | env: 33 | email: ${{secrets.EMAIL}} 34 | user: ${{secrets.USER}} 35 | -------------------------------------------------------------------------------- /pages/api/sns2.js: -------------------------------------------------------------------------------- 1 | const url = 'https://raw.githubusercontent.com/dssg-pt/covid19pt-data/master/vacinas_detalhe.csv'; 2 | var csv = require('csvtojson'); 3 | import Cors from 'cors'; 4 | function initMiddleware(middleware) { 5 | return (req, res) => 6 | new Promise((resolve, reject) => { 7 | middleware(req, res, (result) => { 8 | if (result instanceof Error) { 9 | return reject(result); 10 | } 11 | return resolve(result); 12 | }); 13 | }); 14 | } 15 | 16 | const cors = initMiddleware( 17 | Cors({ 18 | methods: ['GET'], 19 | }) 20 | ); 21 | 22 | export default async function handler(req, res) { 23 | await cors(req, res); 24 | res.statusCode = 200; 25 | 26 | let contents = await fetch(url).then((res) => res.buffer()); 27 | 28 | csv({ 29 | colParser: { 30 | doses: 'number', 31 | doses_novas: 'number', 32 | doses1: 'number', 33 | doses1_novas: 'number', 34 | doses2: 'number', 35 | doses2_novas: 'number', 36 | pessoas_vacinadas_completamente: 'number', 37 | pessoas_vacinadas_completamente_novas: 'number', 38 | pessoas_vacinadas_parcialmente: 'number', 39 | pessoas_vacinadas_parcialmente_novas: 'number', 40 | pessoas_inoculadas: 'number', 41 | pessoas_inoculadas_novas: 'number', 42 | vacinas: 'number', 43 | vacinas_novas: 'number', 44 | }, 45 | }) 46 | .fromString(contents.toString()) 47 | .then(function (jsonArrayObj) { 48 | res.json(jsonArrayObj); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /public/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | importScripts('https://www.gstatic.com/firebasejs/8.3.1/firebase-app.js'); 2 | importScripts('https://www.gstatic.com/firebasejs/8.3.1/firebase-messaging.js'); 3 | 4 | var firebaseConfig = { 5 | apiKey: 'AIzaSyBcHZc1Rk5CeJDkwwaBOdCzYgdA6V5WK3g', 6 | authDomain: 'covid19-249f1.firebaseapp.com', 7 | projectId: 'covid19-249f1', 8 | storageBucket: 'covid19-249f1.appspot.com', 9 | messagingSenderId: '636238011730', 10 | appId: '1:636238011730:web:bf4a0deef86c884c3b6e8b', 11 | measurementId: 'G-DYYRVR03RS', 12 | }; 13 | 14 | firebase.initializeApp(firebaseConfig); 15 | const messaging = firebase.messaging(); 16 | 17 | messaging.onBackgroundMessage((payload) => { 18 | console.log('[firebase-messaging-sw.js] Received background message ', payload); 19 | // Customize notification here 20 | const notificationTitle = payload.notification.title; 21 | const notificationOptions = { 22 | body: payload.notification.body, 23 | icon: '/android-icon-192x192.png', 24 | data: { 25 | url: '/', // This is returning null 26 | id: '', // And this is returning null 27 | }, 28 | }; 29 | self.registration.showNotification(notificationTitle, notificationOptions); 30 | }); 31 | 32 | self.addEventListener('notificationclick', function (event) { 33 | event.notification.close(); 34 | event.waitUntil( 35 | clients.openWindow('https://www.vacinacaocovid19.pt/?utm_source=notifications&utm_medium=notifications&utm_campaign=notifications') 36 | ); 37 | }); 38 | -------------------------------------------------------------------------------- /.github/workflows/servicos_minimos_owid.yml: -------------------------------------------------------------------------------- 1 | name: Serviços Minimos - OWID, RT, Cases Ilhas e ECDC 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | logLevel: 6 | description: 'Log level' 7 | required: true 8 | default: 'warning' 9 | schedule: 10 | - cron: '30 10,17 * * *' 11 | #some comment 12 | jobs: 13 | build-and-deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 🛎️ 17 | uses: actions/checkout@v2.3.1 18 | with: 19 | fetch-depth: 3 20 | token: ${{ secrets.PAT}} 21 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 22 | run: | 23 | yarn install --frozen-lockfile 24 | pip3 install -r requirements.txt 25 | - name: Cache 26 | uses: actions/cache@v2.1.6 27 | with: 28 | path: 'node_modules' 29 | key: 'node_modules' 30 | - name: Update OWID, Rt and Cases Ilhas 31 | run: | 32 | git fetch 33 | git config --global user.name "$user" 34 | git config --global user.email "$email" 35 | python3 ./automation/ecdc_parser.py 36 | ./daemon.js --scrap owid 37 | 38 | env: 39 | email: ${{secrets.EMAIL}} 40 | user: ${{secrets.USER}} 41 | -------------------------------------------------------------------------------- /automation/convert-xls.js: -------------------------------------------------------------------------------- 1 | const excelToJson = require('convert-excel-to-json'); 2 | const { default: fetch } = require('node-fetch'); 3 | const fs = require('fs'); 4 | var shell = require('shelljs'); 5 | const path = require('path'); 6 | let rts = {}; 7 | let json = require('./../data/last-update.json'); 8 | 9 | const scrapRt = async function (onUpdate) { 10 | let files = await fetch( 11 | 'http://www.insa.min-saude.pt/category/areas-de-atuacao/epidemiologia/covid-19-curva-epidemica-e-parametros-de-transmissibilidade/' 12 | ) 13 | .then((res) => res.text()) 14 | .then((text) => text.match(/http.*xlsx/gm)); 15 | 16 | for await (file of files) { 17 | let parsed = path.parse(file); 18 | let contents = await fetch(file).then((res) => res.buffer()); 19 | const result = excelToJson({ 20 | source: contents, 21 | sheets: [ 22 | { 23 | name: 'Sheet 1', 24 | header: { 25 | rows: 1, 26 | }, 27 | columnToKey: { 28 | A: 'data_rt', 29 | B: 'rt_numero_de_reproducao', 30 | C: 'limite_inferior_IC95', 31 | D: 'limite_superior_IC95', 32 | }, 33 | }, 34 | ], 35 | }); 36 | rts[parsed.name.toLowerCase()] = result['Sheet 1']; 37 | fs.writeFileSync(`./data/rt/${parsed.name.toLowerCase()}.json`, JSON.stringify(result['Sheet 1']), () => {}); 38 | } 39 | 40 | fs.writeFileSync(`./data/rt/rt_todas.json`, JSON.stringify(rts), () => {}); 41 | 42 | json.dateRt = rts.rt_nacional.reverse()[0].data_rt; 43 | 44 | fs.writeFileSync('./data/last-update.json', JSON.stringify(json), () => {}); 45 | 46 | shell.exec('git status | grep rt_todas.json', { silent: true }, (code, stdout) => { 47 | if (code == 0 && onUpdate) { 48 | onUpdate(); 49 | } 50 | }); 51 | }; 52 | scrapRt(); 53 | module.exports = scrapRt; 54 | -------------------------------------------------------------------------------- /pages/api/hooks.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: './../.env' }); 2 | const crypto = require('crypto'); 3 | var shell = require('shelljs'); 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: false, 8 | }, 9 | }; 10 | 11 | const webhookPayloadParser = (req) => 12 | new Promise((resolve) => { 13 | let data = ''; 14 | req.on('data', (chunk) => { 15 | data += chunk; 16 | }); 17 | req.on('end', () => { 18 | resolve(Buffer.from(data).toString()); 19 | }); 20 | }); 21 | 22 | function verifyPostData(req, res) { 23 | return new Promise((resolve, rej) => { 24 | if (!req.rawBody) { 25 | resolve(false); 26 | } 27 | const sig = Buffer.from(req.headers[process.env.HOOKS_HEADER_NAME] || '', 'utf8'); 28 | const hmac = crypto.createHmac(process.env.HOOKS_SHA, process.env.HOOKS_SECRET); 29 | const digest = Buffer.from(process.env.HOOKS_SHA + '=' + hmac.update(req.rawBody).digest('hex'), 'utf8'); 30 | 31 | if (sig.length !== digest.length || !crypto.timingSafeEqual(digest, sig)) { 32 | console.log(`Request body digest (${digest}) did not match ${process.env.HOOKS_HEADER_NAME} (${sig})`); 33 | resolve(false); 34 | } else { 35 | let json_string = decodeURIComponent(req.rawBody).split('payload=')[1]; 36 | let json = JSON.parse(json_string); 37 | 38 | resolve(json.ref === 'refs/heads/master' ? true : false); 39 | } 40 | }); 41 | } 42 | 43 | export default async function handler(req, res) { 44 | const data = await webhookPayloadParser(req); 45 | req.rawBody = data; 46 | let allowed = await verifyPostData(req, res); 47 | console.log('hook received'); 48 | 49 | //set out to execute the command 50 | res.statusCode = 200; 51 | res.json({}); 52 | 53 | if (allowed) { 54 | shell.exec('echo "working" & yarn update:server &'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /components/Counter.jsx: -------------------------------------------------------------------------------- 1 | import styles from './Card.module.scss'; 2 | import * as React from 'react'; 3 | export function Counter({ to, yesterday, title, subtitle, ps, colors, digits, suffix, tempo }) { 4 | if (!tempo) tempo = window.innerWidth <= 500 ? 'ontem' : 'no dia anterior'; 5 | 6 | let numberFormatter = new Intl.NumberFormat('en-US', { 7 | maximumFractionDigits: 2, 8 | }); 9 | let difference = to - yesterday || 0; 10 | let [foreground] = colors; 11 | 12 | return ( 13 | <> 14 | {title == '' ? '' :

{title}

} 15 | 16 | {subtitle == '' ?
:

{subtitle}

} 17 | {to === null ? ( 18 | 19 | N/A 20 | 21 | ) : ( 22 | <> 23 | 24 | {numberFormatter.format(to).replace(/,/gm, ' ')} {suffix ? suffix : ''} 25 | 26 | {yesterday && Math.abs(to - yesterday) > 0 ? ( 27 | <> 28 |

29 | 30 | {Math.sign(difference) == 1 ? '+' : '-'} {numberFormatter.format(Math.abs(difference)).replace(',', ' ')} 31 | 32 |   que {tempo} 33 |

34 | 35 | ) : ( 36 | '' 37 | )} 38 | 39 | {ps != null ? ( 40 | <> 41 |

42 | {ps.split('\n').map((el, idx) => ( 43 | 44 | {el}
45 |
46 | ))} 47 |

48 | 49 | ) : ( 50 | '' 51 | )} 52 | 53 | )} 54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/servicos_minimos_vac.yml: -------------------------------------------------------------------------------- 1 | name: Serviços Minimos - Vacinas & Casos 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | logLevel: 6 | description: 'Log level' 7 | required: true 8 | default: 'warning' 9 | schedule: 10 | - cron: '0/30 13-22 * * *' 11 | jobs: 12 | build-and-deploy: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 🛎️ 16 | uses: actions/checkout@v2.3.1 17 | with: 18 | fetch-depth: 3 19 | token: ${{ secrets.PAT}} 20 | 21 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 22 | run: yarn install --frozen-lockfile 23 | - name: Cache 24 | uses: actions/cache@v2.1.6 25 | with: 26 | path: 'node_modules' 27 | key: 'node_modules' 28 | - name: Update Vaccines 29 | run: | 30 | curl -Ls $env --output .env 31 | curl -Ls $json_url --output $json_file 32 | pip3 install firebase_admin python-dateutil 33 | git fetch 34 | git config --global user.name "$user" 35 | git config --global user.email "$email" 36 | HARDWARE=ci ./daemon.js --scrap vaccines 37 | 38 | env: 39 | email: ${{secrets.EMAIL}} 40 | user: ${{secrets.USER}} 41 | env: ${{secrets.ENV_FILE}} 42 | json_url: ${{secrets.JSON_URL}} 43 | json_file: ${{secrets.JSON_FILE}} 44 | -------------------------------------------------------------------------------- /_readme/TODO_MADEIRA.MD: -------------------------------------------------------------------------------- 1 | - Obter dados gerais demográficos da Madeira 2 | - ~~População~~ 3 | - Outros dados relevantes (ver ainda quais são) 4 | - ~~Obter SVG para gráfico personalizado (com Leaflet)~~ 5 | - Acabar de transcrever os relatórios 6 | - Obter dados por freguesia (?) 7 | - ~~Introduzir sub-páginas~~ 8 | - ~~Garantir que só se chama os dados quando se está na página certa~~ 9 | - ~~Planear gráficos~~ 10 | - ~~Número total de vacinas administradas~~ 11 | - ~~Número de doses administradas - 1ª Dose~~ 12 | - ~~Número de doses administradas - 2ª Dose~~ 13 | - ~~Percentagem pde população inoculada com a 2ª dose~~ 14 | - ~~Percentagem para atingir imunidade de grupo~~ 15 | - ~~Número de vacinas administradas (sem as marcas)~~ 16 | - ~~Número de vacinas administradas por semana~~ 17 | - ~~Rt Madeira~~ 18 | - ~~Numeros dos grupos prioritários~~ 19 | - ~~Por região~~ 20 | - ~~Número de doses recebidas por semana~~ 21 | - ~~Número de doses recebidas (acumulado)~~ 22 | - ~~Proporção de doses administradas relativamente às doses recebidas~~ 23 | - ~~Número de doses administradas por semana e faixa etária~~ 24 | - ~~Doses totais administradas por faixa etária~~ 25 | - ~~Número de vacinas administradas por dia com o número de infectados e de recuperados nos últimos 14 dias~~ 26 | - ~~Proporção do número total de vacinas administradas com o número de infectados, recuperados e óbitos~~ 27 | - ~~Proporção do número total de vacinas administradas com o número de infectados, recuperados e óbitos e população suscetível~~ 28 | - ~~Alterar notas e fontes~~ 29 | - ~~Terminar descrições de cada gráfico~~ 30 | - ~~Reactivar Pusher & OneSignal~~ 31 | - ~~Fazer menu mobile~~ 32 | - ~~Fazer ajustes de CSS~~ 33 | - ~~Fazer ajustes de CSS (Mobile)~~ 34 | - ~~Adicionar Lazyload aos componentes Madeira~~ 35 | 36 | -------------------------------------------------------------------------------- /assets/portugal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | const PRECACHE = 'precache'; 2 | const RUNTIME = 'runtime'; 3 | 4 | // A list of local resources we always want to be cached. 5 | const PRECACHE_URLS = ['']; 6 | 7 | // The install handler takes care of precaching the resources we always need. 8 | self.addEventListener('install', (event) => { 9 | self.skipWaiting(); 10 | /* event.waitUntil( 11 | caches 12 | .open(RUNTIME) 13 | .then((cache) => cache.addAll(PRECACHE_URLS)) 14 | .then(self.skipWaiting()) 15 | ); */ 16 | }); 17 | 18 | // The activate handler takes care of cleaning up old caches. 19 | self.addEventListener('activate', (event) => { 20 | const currentCaches = [PRECACHE]; 21 | event.waitUntil( 22 | caches 23 | .keys() 24 | .then((cacheNames) => { 25 | return cacheNames.filter((cacheName) => !currentCaches.includes(cacheName)); 26 | }) 27 | .then((cachesToDelete) => { 28 | return Promise.all( 29 | cachesToDelete.map((cacheToDelete) => { 30 | return caches.delete(cacheToDelete); 31 | }) 32 | ); 33 | }) 34 | .then(() => self.clients.claim()) 35 | ); 36 | }); 37 | 38 | self.addEventListener('fetch', (event) => { 39 | let url = new URL(event.request.url); 40 | let isRoot = url.pathname === '/' || url.pathname === '/acores' || url.pathname === '/madeira'; 41 | if (event.request.url.startsWith(self.location.origin) && event.request.url.match('/') && event.request.method === 'GET') { 42 | event.respondWith( 43 | caches.match(event.request).then((cachedResponse) => { 44 | if (cachedResponse && !isRoot) { 45 | return cachedResponse; 46 | } 47 | 48 | return caches.open(isRoot ? PRECACHE : RUNTIME).then((cache) => { 49 | return fetch(event.request).then((response) => { 50 | return cache.put(event.request, response.clone()).then(() => { 51 | return response; 52 | }); 53 | }); 54 | }); 55 | }) 56 | ); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /components/graphs/RamGruposPrioritarios.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Card } from './../Card'; 3 | import { Col, Row } from 'react-bootstrap'; 4 | import cardStyles from './../Card.module.scss'; 5 | import ReactCountTo from 'react-count-to'; 6 | 7 | export function RamGruposPrioritarios({ statistics, colors }) { 8 | let [loading, setLoading] = useState(true); 9 | let [graphData, setGraphData] = useState({}); 10 | 11 | useEffect(() => { 12 | statistics.getArquipelagoData().then((data) => { 13 | setGraphData(data[8]); 14 | setLoading(false); 15 | }); 16 | }, []); 17 | 18 | let numberFormatter = new Intl.NumberFormat('en-US', { 19 | maximumFractionDigits: 2, 20 | }); 21 | 22 | const fn = (value) =>

{numberFormatter.format(value).replace(/,/gm, ' ')}

; 23 | 24 | function renderGrupo(el) { 25 | if (!el.nome) return; 26 | return ( 27 | 28 | 29 | 30 |
31 |

{el.nome}

32 |
33 | 34 | 35 |
36 |

1º Dose

37 | 38 | {fn} 39 | 40 |
41 | 42 | 43 |
44 |

2º Dose

45 | 46 | {fn} 47 | 48 |
49 | 50 |
51 | 52 | ); 53 | } 54 | return ( 55 | 56 |
57 | {!loading ? ( 58 | <> 59 | {Object.values(graphData.grupos).map(renderGrupo)} 60 | 61 | ) : ( 62 | '' 63 | )} 64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /automation/ecdc_parser.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from dateutil.parser import parse 4 | from dateutil.relativedelta import * 5 | import os 6 | from datetime import datetime 7 | 8 | 9 | today = datetime.now() 10 | print("ECDC Running", today.weekday()) 11 | print("today is ", today.weekday()) 12 | if(today.weekday() != 3): 13 | exit() 14 | 15 | #open owid file 16 | owid_file = open("./data/ecdc_filtered.json") 17 | total_items = len(json.load(owid_file)) 18 | owid_file.close() 19 | 20 | url = 'https://opendata.ecdc.europa.eu/covid19/vaccine_tracker/json' 21 | 22 | res = requests.get(url) 23 | json_res = res.json() 24 | filtered_arr = [] 25 | 26 | records = json_res['records'] 27 | for k in records: 28 | if(k['ReportingCountry'] == 'PT'): 29 | filtered_arr.append(k) 30 | 31 | 32 | if(len(filtered_arr) != total_items): 33 | 34 | owid_file = open("./data/ecdc_filtered.json", "w") 35 | owid_file.write(json.dumps(filtered_arr)) 36 | owid_file.close() 37 | 38 | weeks_file = open('./data/weeks.json') 39 | weeks_data = json.load(weeks_file) 40 | 41 | last_item = filtered_arr[-1] 42 | 43 | weeks_file.close() 44 | selected_week = weeks_data[last_item['YearWeekISO']] 45 | ecdc_date = parse(selected_week['to']) 46 | json_file = open('./data/last-update.json', 'r+') 47 | json_datas = json.load(json_file) 48 | json_datas['dateEcdc'] = ecdc_date.strftime('%Y-%m-%d') 49 | json_datas['week'] = int(json_datas['week'])+1 50 | json_file.seek(0) 51 | json_file.write(json.dumps(json_datas)) 52 | json_file.close() 53 | 54 | # prepare commit 55 | stream = os.popen('git status | grep ecdc_filtered.json') 56 | output = stream.read() 57 | 58 | if(output): 59 | os.system('git add .') 60 | os.system('git commit -m "covid update - ecdc - week ' + str(int(json_datas['week'])-1) + '"') 61 | os.system('git push') 62 | os.system('yarn deploy') 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /pages/api/sesaram.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | export default async function handler(req, res) { 4 | let resSesaram = await fetch('https://web.sesaram.pt/COVID19_INFO', { 5 | headers: { 6 | accept: '*/*', 7 | origin: 'https://web.sesaram.pt', 8 | 'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8', 9 | 'content-type': 'application/x-www-form-urlencoded', 10 | 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 11 | 'sec-ch-ua-mobile': '?0', 12 | 'sec-fetch-dest': 'empty', 13 | 'sec-fetch-mode': 'cors', 14 | 'sec-fetch-site': 'same-origin', 15 | }, 16 | referrer: 'https://web.sesaram.pt/COVID19_INFO', 17 | referrerPolicy: 'strict-origin-when-cross-origin', 18 | method: 'POST', 19 | mode: 'cors', 20 | credentials: 'omit', 21 | }); 22 | 23 | let text = await resSesaram.text(); 24 | let url = new RegExp(/action="(.*)".*target/gm); 25 | let a = url.exec(text)[1]; 26 | 27 | resSesaram = await fetch('https://web.sesaram.pt/' + a, { 28 | headers: { 29 | accept: '*/*', 30 | origin: 'https://web.sesaram.pt', 31 | 'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8', 32 | 'content-type': 'application/x-www-form-urlencoded', 33 | 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 34 | 'sec-ch-ua-mobile': '?0', 35 | 'sec-fetch-dest': 'empty', 36 | 'sec-fetch-mode': 'cors', 37 | 'sec-fetch-site': 'same-origin', 38 | }, 39 | referrer: 'https://web.sesaram.pt/COVID19_INFO', 40 | referrerPolicy: 'strict-origin-when-cross-origin', 41 | body: 'WD_ACTION_=AJAXEXECUTE&EXECUTEPROC=PAGE_VACINACAO.obtemTotalVacinasAdministradas&WD_CONTEXTE_=', 42 | method: 'POST', 43 | mode: 'cors', 44 | credentials: 'omit', 45 | }); 46 | text = await resSesaram.text(); 47 | let [d1, d2, total] = text.split('|'); 48 | 49 | let body = { 50 | total: parseInt(total), 51 | dose_1: parseInt(d1), 52 | dose_2: parseInt(d2), 53 | data: Date.now(), 54 | }; 55 | 56 | res.statusCode = 200; 57 | res.json(body); 58 | } 59 | -------------------------------------------------------------------------------- /components/SimpleCounter.jsx: -------------------------------------------------------------------------------- 1 | import CountTo from 'react-count-to'; 2 | import styles from './Card.module.scss'; 3 | import * as React from 'react'; 4 | export function Counter({ from, to, yesterday, title, subtitle, ps, colors, digits, suffix, tempo }) { 5 | if (!tempo) tempo = 'no dia anterior'; 6 | if (!digits) { 7 | digits = 0; 8 | } 9 | let numberFormatter = new Intl.NumberFormat('en-US', { 10 | maximumFractionDigits: 2, 11 | }); 12 | let difference = to - yesterday || 0; 13 | let [foreground] = colors; 14 | const fn = (value) => ( 15 | 16 | {numberFormatter.format(value).replace(/,/gm, ' ')} {suffix ? suffix : ''} 17 | 18 | ); 19 | 20 | return ( 21 | <> 22 | {title == '' ? '' :

{title}

} 23 | 24 | {subtitle == '' ?
:

{subtitle}

} 25 | {to === null ? ( 26 | 27 | N/A 28 | 29 | ) : ( 30 | <> 31 | 32 | {fn} 33 | 34 | 35 | {Math.abs(to - yesterday) > 0 ? ( 36 | <> 37 |

38 | 39 | {Math.sign(difference) == 1 ? '+' : '-'} {numberFormatter.format(Math.abs(difference)).replace(',', ' ')} 40 | 41 |   que {tempo} 42 |

43 | 44 | ) : ( 45 | '' 46 | )} 47 | 48 | {ps != null ? ( 49 | <> 50 |

51 | {ps.split('\n').map((el, idx) => ( 52 | 53 | {el}
54 |
55 | ))} 56 |

57 | 58 | ) : ( 59 | '' 60 | )} 61 | 62 | )} 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /components/graphs/PieVacinadosInfectadosRecuperadosObitos.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { Pie } from 'react-chartjs-2'; 3 | import { Card } from './../Card'; 4 | import { formatNumber } from '../../utils'; 5 | export function PieVacinadosInfectadosRecuperadosObitos({ statistics, colors }) { 6 | let vaccines = statistics.getLastVaccineAvaliable(); 7 | let lastCase = statistics.getLastCaseAvaliable(); 8 | let { main, tints, shades, complements } = colors; 9 | 10 | const data = (canvas) => { 11 | return { 12 | labels: ['Vacinação Iniciada', 'Vacinação Completa', 'Casos Ativos', 'Casos Recuperados', 'Óbitos'], 13 | datasets: [ 14 | { 15 | backgroundColor: [tints[1], main, complements[0], complements[2], shades[2]], 16 | data: [Math.abs(vaccines.dose_1 - vaccines.dose_2), vaccines.dose_2, lastCase.ativos, lastCase.recuperados, lastCase.obitos], 17 | }, 18 | ], 19 | }; 20 | }; 21 | const options = () => { 22 | return { 23 | maintainAspectRatio: false, 24 | plugins: { 25 | datalabels: { 26 | color: 'white', 27 | formatter: (value, chart) => { 28 | let sum = chart.dataset.data.reduce((prev, curr) => { 29 | return prev + curr; 30 | }, 0); 31 | sum = (value / sum) * 100; 32 | 33 | if (sum > 5) { 34 | return sum.toFixed(2) + '%'; 35 | } 36 | return ''; 37 | }, 38 | }, 39 | legend: { 40 | position: 'bottom', 41 | align: 'start', 42 | }, 43 | }, 44 | onResize: (a, b, c) => {}, 45 | 46 | animation: { 47 | duration: 1000, 48 | }, 49 | tooltips: { 50 | callbacks: { 51 | label: function ({ index }, { datasets, labels }) { 52 | let label = labels[index]; 53 | let data = datasets[0].data[index]; 54 | return `${label}: ${formatNumber(data)}`; 55 | }, 56 | }, 57 | }, 58 | }; 59 | }; 60 | 61 | return ( 62 | 63 |
{}
64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /components/graphs/PieRecebidasAdquiridas.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Pie } from 'react-chartjs-2'; 3 | import { Card } from './../Card'; 4 | import { formatNumber } from './../../utils'; 5 | export function PieRecebidasAdquiridas({ statistics, colors }) { 6 | let [loading, setLoading] = useState(true); 7 | let { main } = colors; 8 | 9 | const data = (canvas) => { 10 | return { 11 | labels: ['Doses a Receber', 'Doses Recebidas'], 12 | datasets: [ 13 | { 14 | backgroundColor: ['transparent', main], 15 | borderColor: [main, main], 16 | data: [statistics.encomendadas - statistics.recebidas, statistics.recebidas], 17 | }, 18 | ], 19 | }; 20 | }; 21 | const options = () => { 22 | return { 23 | maintainAspectRatio: false, 24 | plugins: { 25 | datalabels: { 26 | color: [main, 'white'], 27 | formatter: (value, chart) => { 28 | let sum = 0; 29 | 30 | if (value === statistics.encomendadas - statistics.recebidas) { 31 | sum = 1 - statistics.recebidas / statistics.encomendadas; 32 | } 33 | sum = sum * 100; 34 | if (sum > 10) { 35 | return `${formatNumber(value)} ( ${sum.toFixed(2)}% )`; 36 | } 37 | return ''; 38 | }, 39 | }, 40 | legend: { 41 | position: 'bottom', 42 | align: 'start', 43 | }, 44 | }, 45 | onResize: (a, b, c) => {}, 46 | 47 | animation: { 48 | duration: 1000, 49 | }, 50 | tooltips: { 51 | callbacks: { 52 | label: function ({ index }, { datasets, labels }) { 53 | let label = labels[index]; 54 | let data = datasets[0].data[index]; 55 | if (label === 'Doses Adquiridas') { 56 | data = statistics.encomendadas; 57 | } 58 | return `${label}: ${formatNumber(data)}`; 59 | }, 60 | }, 61 | }, 62 | }; 63 | }; 64 | 65 | useEffect(() => { 66 | setLoading(false); 67 | }, []); 68 | 69 | return ( 70 | 71 |
{!loading ? : ''}
72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | on: 3 | push: 4 | branches: 5 | - master 6 | workflow_dispatch: 7 | inputs: 8 | logLevel: 9 | description: 'Log level' 10 | required: true 11 | default: 'warning' 12 | jobs: 13 | build-and-deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 🛎️ 17 | uses: actions/checkout@v2.3.1 18 | with: 19 | token: ${{ secrets.PAT}} 20 | - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built. 21 | run: | 22 | echo ${{ secrets.TEST_SECRET }} 23 | yarn install --frozen-lockfile 24 | yarn build 25 | env: 26 | CI: true 27 | API_KEY: ${{ secrets.TEST_SECRET }} 28 | FIREBASE_project_id: ${{ secrets.FIREBASE_project_id }} 29 | FIREBASE_private_key_id: ${{ secrets.FIREBASE_private_key_id }} 30 | FIREBASE_private_key: ${{ secrets.FIREBASE_private_key }} 31 | FIREBASE_client_email: ${{ secrets.FIREBASE_client_email }} 32 | FIREBASE_client_id: ${{ secrets.FIREBASE_client_id }} 33 | 34 | - name: Create folder 35 | run: | 36 | mkdir output 37 | cp package.json output/package.json 38 | cp next.config.js output/next.config.js 39 | cp .gitignore output/.gitignore 40 | cp run.sh output/run.sh 41 | cp -r public output/public 42 | cp -r .next output/.next 43 | 44 | - name: Deploy 🚀 45 | uses: JamesIves/github-pages-deploy-action@4.1.0 46 | with: 47 | branch: compiled # The branch the action should deploy to. 48 | folder: output # The folder the action should deploy. 49 | -------------------------------------------------------------------------------- /automation/azores_parser.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from dateutil.parser import parse 4 | from dateutil.relativedelta import * 5 | import os 6 | from datetime import datetime 7 | 8 | acores_file = open("./data/acores_pds.json") 9 | total_items = json.load(acores_file) 10 | acores_file.close() 11 | 12 | 13 | url = 'https://servicos-sraa.azores.gov.pt/covid19/ee/Default.aspx?s=gtee' 14 | 15 | res = requests.get(url) 16 | json_res = res.json() 17 | 18 | dados_transformados = { 19 | "data":json_res["dataAtualizacaoTotal"], 20 | "total":int(json_res["casosconfirmados"]), 21 | "total_novos":0, 22 | "recuperados":int(json_res["recuperacoes"]), 23 | "recuperados_novos":0, 24 | "obitos":int(json_res["obitos"]), 25 | "obitos_var":0, 26 | "ativos":int(json_res['casosativos']), 27 | "ativos_var":0, 28 | } 29 | 30 | if (len(total_items) > 0): 31 | dia_anterior = total_items[len(total_items)-1] 32 | dados_transformados["total_novos"] = dados_transformados["total"] - int(dia_anterior["total"]) 33 | dados_transformados["recuperados_novos"] = dados_transformados["recuperados"] - int(dia_anterior["recuperados"]) 34 | dados_transformados["obitos_var"] = dados_transformados["obitos"] - int(dia_anterior["obitos"] ) 35 | dados_transformados["ativos_var"] = dados_transformados["ativos"] - int(dia_anterior["ativos"] ) 36 | 37 | 38 | total_items.append(dados_transformados) 39 | owid_file = open("./data/acores_pds.json", "w") 40 | owid_file.write(json.dumps(total_items)) 41 | owid_file.close() 42 | 43 | last_item = dados_transformados 44 | print(last_item) 45 | acores_last_date = last_item['data'] 46 | 47 | json_file = open('./data/last-update.json', 'r+') 48 | json_datas = json.load(json_file) 49 | json_datas['dateAcoresCases'] = acores_last_date 50 | json_file.seek(0) 51 | json_file.write(json.dumps(json_datas)) 52 | json_file.close() 53 | 54 | """ 55 | # prepare commit 56 | stream = os.popen('git status | grep acores_pds.json') 57 | output = stream.read() 58 | if(output): 59 | os.system('git add .') 60 | os.system('git commit -m "covid update - madeira cases - ' + str(acores_last_date) + '"') 61 | os.system('git push') 62 | os.system('yarn deploy') 63 | """ -------------------------------------------------------------------------------- /automation/convert-csv:cases.js: -------------------------------------------------------------------------------- 1 | var csv = require('csvtojson'); 2 | const { default: fetch } = require('node-fetch'); 3 | let fs = require('fs'); 4 | let json = require('./../data/last-update.json'); 5 | var parse = require('date-fns/parse'); 6 | var format = require('date-fns/format'); 7 | var isAfter = require('date-fns/isAfter'); 8 | var parseISO = require('date-fns/parseISO'); 9 | 10 | async function convertCases(cb = null, error_cb = null) { 11 | try { 12 | let contents = await fetch('https://raw.githubusercontent.com/dssg-pt/covid19pt-data/master/data.csv').then((res) => res.buffer()); 13 | 14 | let rawJsonArrayObj = await csv({ 15 | includeColumns: /(data|confirmados|confirmados_novos|obitos|recuperados)\b/, 16 | }).fromString(contents.toString()); 17 | 18 | let jsonArrayObj = rawJsonArrayObj.filter((item) => isAfter(parse(item.data, 'dd-MM-yyyy', new Date()), parseISO('2020-12-20'))); 19 | 20 | for (var i = 0; i !== jsonArrayObj.length; i++) { 21 | let item = jsonArrayObj[i]; 22 | delete item.n_confirmados; 23 | item.confirmados = parseInt(item.confirmados); 24 | item.data_cases = parse(item.data, 'dd-MM-yyyy', new Date()).getTime(); 25 | item.confirmados_novos = parseInt(item.confirmados_novos); 26 | item.obitos = parseInt(item.obitos); 27 | item.recuperados = parseInt(item.recuperados); 28 | 29 | if (i > 0) { 30 | let item_anterior = jsonArrayObj[i - 1]; 31 | item.var_recuperados = parseInt(item.recuperados) - parseInt(item_anterior.recuperados); 32 | item.var_obitos = parseInt(item.obitos) - parseInt(item_anterior.obitos); 33 | item.ativos = parseInt(item.confirmados) - parseInt(item.obitos) - parseInt(item.recuperados); 34 | } else { 35 | item.var_recuperados = 0; 36 | item.var_obitos = 0; 37 | item.ativos = 0; 38 | } 39 | } 40 | if (cb) cb(jsonArrayObj); 41 | return jsonArrayObj; 42 | /* fs.writeFileSync('./data/cases_v2.json', JSON.stringify(jsonArrayObj)); 43 | json.date = new Date(); 44 | json.dateCases = jsonArrayObj.reverse()[0].data; 45 | fs.writeFileSync('./data/last-update.json', JSON.stringify(json)); */ 46 | } catch (e) { 47 | if (error_cb) error_cb(); 48 | } 49 | } 50 | //(async () => { 51 | // await convertCases(); 52 | //})(); 53 | 54 | module.exports = convertCases; 55 | -------------------------------------------------------------------------------- /components/graphs/PieAdministradasDoses.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Pie } from 'react-chartjs-2'; 3 | import { Card } from './../Card'; 4 | import { formatNumber } from './../../utils'; 5 | export function PieAdministradasDoses({ statistics, colors }) { 6 | let [loading, setLoading] = useState(true); 7 | 8 | let { main, shades } = colors; 9 | 10 | const data = (canvas) => { 11 | return { 12 | labels: ['Vacinação Iniciada', 'Vacinação Completa', 'Doses por administrar'], 13 | datasets: [ 14 | { 15 | backgroundColor: [main, shades[0], shades[1]], 16 | data: [statistics.iniciada, statistics.completa, statistics.recebidas - statistics.administradas], 17 | }, 18 | ], 19 | }; 20 | }; 21 | const options = () => { 22 | return { 23 | maintainAspectRatio: false, 24 | plugins: { 25 | datalabels: { 26 | color: 'white', 27 | formatter: (value, chart) => { 28 | let sum = 0; 29 | if (value === statistics.primeiras) { 30 | sum = statistics.primeiras / statistics.recebidas; 31 | } 32 | 33 | if (value === statistics.segundas) { 34 | sum = statistics.segundas / statistics.recebidas; 35 | } 36 | 37 | if (value === statistics.recebidas - statistics.administradas) { 38 | sum = 1 - statistics.administradas / statistics.recebidas; 39 | } 40 | 41 | sum = sum * 100; 42 | 43 | if (sum > 10) { 44 | return sum.toFixed(2) + '%'; 45 | } 46 | return ''; 47 | }, 48 | }, 49 | legend: { 50 | position: 'bottom', 51 | align: 'start', 52 | }, 53 | }, 54 | onResize: (a, b, c) => {}, 55 | 56 | animation: { 57 | duration: 1000, 58 | }, 59 | tooltips: { 60 | callbacks: { 61 | label: function ({ index }, { datasets, labels }) { 62 | let label = labels[index]; 63 | let data = datasets[0].data[index]; 64 | if (label === 'Doses Recebidas') { 65 | data = statistics.recebidas; 66 | } 67 | return `${label}: ${formatNumber(data)}`; 68 | }, 69 | }, 70 | }, 71 | }; 72 | }; 73 | 74 | useEffect(() => { 75 | setLoading(false); 76 | }, []); 77 | 78 | return ( 79 | 80 |
{!loading ? : ''}
81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /automation/convert-csv.js: -------------------------------------------------------------------------------- 1 | var csv = require('csvtojson'); 2 | const { default: fetch } = require('node-fetch'); 3 | let fs = require('fs'); 4 | let json = require('./../data/last-update.json'); 5 | const { parse, add, sub } = require('date-fns'); 6 | 7 | (async () => { 8 | /* csv() 9 | .fromFile('./data/csv/ecdc.csv') 10 | .then(function (jsonArrayObj) { 11 | //when parse finished, result will be emitted here. 12 | fs.writeFile('./data/ecdc.json', JSON.stringify(jsonArrayObj), () => {}); 13 | }); 14 | */ 15 | /* let contents = await fetch( 16 | 'https://docs.google.com/spreadsheets/d/16wucf-R89vxoL_QCmYL2ChYi-iKFVMQyIr7qsV_5kw0/export?format=csv&gid=0' 17 | ).then((res) => res.buffer()); 18 | 19 | csv() 20 | .fromString(contents.toString()) 21 | .then(function (jsonArrayObj) { 22 | //when parse finished, result will be emitted here. 23 | fs.writeFile('./data/madeira_pds.json', JSON.stringify(jsonArrayObj), () => {}); 24 | json.dateMadeiraCases = jsonArrayObj[jsonArrayObj.length - 1].data; 25 | fs.writeFile('./data/last-update.json', JSON.stringify(json), function () {}); 26 | }); 27 | */ 28 | let contents = await fetch( 29 | 'https://docs.google.com/spreadsheets/d/16wucf-R89vxoL_QCmYL2ChYi-iKFVMQyIr7qsV_5kw0/export?format=csv&gid=1371982439' 30 | ).then((res) => res.buffer()); 31 | 32 | csv() 33 | .fromString(contents.toString()) 34 | .then(function (jsonArrayObj) { 35 | //when parse finished, result will be emitted here. 36 | fs.writeFile('./data/acores_pds.json', JSON.stringify(jsonArrayObj), () => {}); 37 | json.dateAcoresCases = jsonArrayObj[jsonArrayObj.length - 1].data; 38 | fs.writeFile('./data/last-update.json', JSON.stringify(json), function () {}); 39 | }); 40 | 41 | const url = 'https://raw.githubusercontent.com/dssg-pt/covid19pt-data/master/vacinas_detalhe.csv'; 42 | 43 | let contents2 = await fetch(url).then((res) => res.buffer()); 44 | 45 | csv() 46 | .fromString(contents2.toString()) 47 | .then(function (jsonArrayObj) { 48 | //when parse finished, result will be emitted here. 49 | let data = jsonArrayObj[jsonArrayObj.length - 1].data; 50 | let parseDate = parse(data, 'dd-MM-yyyy', new Date()); 51 | json.dateSnsStart = sub(parseDate, { days: 7 }).toDateString(); 52 | json.dateSns = sub(parseDate, { days: 1 }).toDateString(); 53 | console.log(json); 54 | fs.writeFile('./data/last-update.json', JSON.stringify(json), function () {}); 55 | }); 56 | })(); 57 | -------------------------------------------------------------------------------- /components/Notifications.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | var firebaseConfig = { 4 | apiKey: 'AIzaSyBcHZc1Rk5CeJDkwwaBOdCzYgdA6V5WK3g', 5 | authDomain: 'covid19-249f1.firebaseapp.com', 6 | projectId: 'covid19-249f1', 7 | storageBucket: 'covid19-249f1.appspot.com', 8 | messagingSenderId: '636238011730', 9 | appId: '1:636238011730:web:bf4a0deef86c884c3b6e8b', 10 | measurementId: 'G-DYYRVR03RS', 11 | }; 12 | 13 | export function Notifications({ children }) { 14 | function registerOnFirebase(callback) { 15 | if (firebase.apps.length === 0) { 16 | firebase.initializeApp(firebaseConfig); 17 | } 18 | const messaging = firebase.messaging(); 19 | messaging 20 | .getToken({ vapidKey: 'BHtOyn7DJeWzTT1uCITnVOzCpFI4jyOGNo_NQCKoJktP56tHqSVCPtyn99tgpWPRsWzRTu07ahM6fjljP_01K3g' }) 21 | .then((currentToken, b, c) => { 22 | if (currentToken) { 23 | fetch('/api/messaging/register', { 24 | method: 'POST', 25 | body: JSON.stringify({ fcm_token: currentToken }), 26 | headers: { 'content-type': 'application/json' }, 27 | }).then((res) => { 28 | callback?.(); 29 | }); 30 | } else { 31 | console.log('No registration token available. Request permission to generate one.'); 32 | } 33 | }) 34 | .catch((err) => { 35 | alert('Não conseguimos ativar as notificações. Certifique-se que não estão bloqueadas para este site ou tente mais tarde.'); 36 | }); 37 | messaging.onMessage((payload) => { 38 | let n = new Notification(payload.notification.title, { 39 | body: payload.notification.body, 40 | icon: '/android-icon-192x192.png', 41 | }); 42 | 43 | n.onclick = function (event) { 44 | window.open('https://www.vacinacaocovid19.pt/?utm_source=notifications&utm_medium=notifications&utm_campaign=notifications'); 45 | }; 46 | }); 47 | } 48 | function allowNotifications() { 49 | if (Notification.permission === 'granted') { 50 | alert('Já recebes as nossas notificações'); 51 | return; 52 | } 53 | 54 | registerOnFirebase(function () { 55 | new Notification('Vacinação COVID-19', { 56 | body: 'Subscreveste às nossas notificações diárias com os dados das vacinas', 57 | icon: '/android-icon-192x192.png', 58 | }); 59 | }); 60 | } 61 | useEffect(function () { 62 | if (Notification.permission === 'granted') { 63 | registerOnFirebase(); 64 | } 65 | }, []); 66 | 67 | return {children} ; 68 | } 69 | -------------------------------------------------------------------------------- /components/CustomBarChart copy.jsx: -------------------------------------------------------------------------------- 1 | /* import ReactTooltip from 'react-tooltip'; 2 | export function CustomBarChart({ data, options, showHeading }) { 3 | let bar3_percentage = '100%'; 4 | let bar2_percentage = (data.datasets.dose_2.data / data.datasets.total.data) * 100 + '%'; 5 | let bar1_percentage = (data.datasets.dose_1.data / data.datasets.total.data) * 100 + '%'; 6 | 7 | function makeTooltip() { 8 | return 'asd'; 9 | } 10 | return ( 11 | <> 12 | 13 | {showHeading ? ( 14 | <> 15 |
16 |

{data.label}

17 |
18 | 19 | ) : ( 20 | '' 21 | )} 22 | 23 |
24 |
25 |
26 |
27 | 28 |
29 |

{data.datasets.dose_2.data}

30 |

{data.datasets.total.data}

31 |

{data.datasets.dose_1.data}

32 |
33 |
34 | 35 | ); 36 | } 37 | */ 38 | -------------------------------------------------------------------------------- /components/CustomCheckbox.module.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/colors.scss'; 2 | 3 | .container { 4 | display: inline-block; 5 | position: relative; 6 | cursor: pointer; 7 | margin: 0; 8 | 9 | /* margin-right: 10px; 10 | margin-bottom: 10px; */ 11 | -webkit-user-select: none; 12 | -moz-user-select: none; 13 | -ms-user-select: none; 14 | user-select: none; 15 | vertical-align: middle; 16 | 17 | p { 18 | margin: 0px; 19 | } 20 | } 21 | 22 | .card-actions .container { 23 | margin-right: 10px; 24 | margin-bottom: 10px; 25 | } 26 | 27 | /* Hide the browser's default checkbox */ 28 | .container input { 29 | position: absolute; 30 | opacity: 0; 31 | cursor: pointer; 32 | height: 0; 33 | width: 0; 34 | } 35 | 36 | /* Create a custom checkbox */ 37 | .label { 38 | display: inline-block; 39 | vertical-align: middle; 40 | font-size: 15px; 41 | margin-left: 5px; 42 | font-size: 13px; 43 | } 44 | 45 | /* Create a custom checkbox */ 46 | p .checkmark { 47 | position: relative; 48 | top: 0; 49 | left: 0; 50 | height: 20px; 51 | line-height: 20px; 52 | width: 20px; 53 | display: inline-block; 54 | vertical-align: middle; 55 | transition: all linear 50ms; 56 | } 57 | 58 | /* On mouse-over, add a grey background color */ 59 | .container:hover input ~ p .checkmark { 60 | background-color: rgba(0, 0, 0, 0.05); 61 | transition: all linear 50ms; 62 | } 63 | 64 | .container input ~ p .checkmark { 65 | background-color: white; 66 | transition: all linear 50ms; 67 | border: 1px solid #eee; 68 | } 69 | 70 | /* When the checkbox is checked, add a blue background */ 71 | .container input:checked ~ p .checkmark { 72 | background-color: white; 73 | transition: all linear 50ms; 74 | border: 1px solid $main; 75 | } 76 | 77 | /* Create the checkmark/indicator (hidden when not checked) */ 78 | p .checkmark:after { 79 | content: ''; 80 | position: absolute; 81 | display: none; 82 | transition: all linear 50ms; 83 | } 84 | 85 | /* Show the checkmark when checked */ 86 | .container input:checked ~ p .checkmark:after { 87 | display: block; 88 | } 89 | 90 | /* Style the checkmark/indicator */ 91 | .container p .checkmark:after { 92 | left: 7px; 93 | top: 3px; 94 | width: 5px; 95 | height: 10px; 96 | border: solid $main; 97 | border-width: 0 2px 2px 0; 98 | -webkit-transform: rotate(45deg); 99 | -ms-transform: rotate(45deg); 100 | transform: rotate(45deg); 101 | } 102 | -------------------------------------------------------------------------------- /automation/twitter.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: './../.env' }); 2 | let data = require('../data/vaccines_dssg.json'); 3 | let fs = require('fs'); 4 | let twitterText = fs.readFileSync('./twitter.txt').toString(); 5 | let numberFormatter = new Intl.NumberFormat(); 6 | let twitterLastUpdate = { 7 | last_update: 0, 8 | }; 9 | if (fs.existsSync('./twitter-conf.json')) { 10 | twitterLastUpdate = JSON.parse(fs.readFileSync('./twitter-conf.json')); //do not cache this pls 11 | } 12 | 13 | let todayDate = new Date(); 14 | todayDate.setMinutes(0); 15 | todayDate.setMilliseconds(0); 16 | todayDate.setSeconds(0); 17 | todayDate.setHours(0); 18 | 19 | let Twitter = require('twitter'); 20 | 21 | var client = new Twitter({ 22 | consumer_key: process.env.CONSUMER_KEY, 23 | consumer_secret: process.env.CONSUMER_SECRET, 24 | access_token_key: process.env.ACCESS_TOKEN_KEY, 25 | access_token_secret: process.env.ACCESS_TOKEN_SECRET, 26 | }); 27 | 28 | if (data[data.length - 1].data_vac_iso != twitterLastUpdate.last_update) { 29 | let yesterday = data[data.length - 2]; 30 | let today = data[data.length - 1]; 31 | twitterLastUpdate.last_update = today.data_vac_iso; 32 | if (yesterday.doses == null) { 33 | twitterText = fs.readFileSync('./twitter_no_daily.txt').toString(); 34 | } 35 | fs.writeFileSync('./twitter-conf.json', JSON.stringify(twitterLastUpdate)); 36 | let postVariables = { 37 | '{{novas_total}}': numberFormatter.format(today.doses - yesterday.doses).replace(/,/gm, ' '), 38 | '{{total_total}}': numberFormatter.format(today.doses).replace(/,/gm, ' '), 39 | '{{novas_in1}}': numberFormatter.format(today.doses1 - yesterday.doses1).replace(/,/gm, ' '), 40 | '{{novas_in2}}': numberFormatter.format(today.doses2 - yesterday.doses2).replace(/,/gm, ' '), 41 | '{{total_in1}}': numberFormatter.format(today.doses1).replace(/,/gm, ' '), 42 | '{{total_in2}}': numberFormatter.format(today.doses2).replace(/,/gm, ' '), 43 | '{{dia}}': todayDate.getDate().toLocaleString('en-US', { 44 | minimumIntegerDigits: 2, 45 | }), 46 | '{{mes}}': (todayDate.getMonth() + 1).toLocaleString('en-US', { 47 | minimumIntegerDigits: 2, 48 | }), 49 | '{{ano}}': todayDate.getFullYear(), 50 | }; 51 | let post = twitterText; 52 | 53 | for (let key of Object.keys(postVariables)) { 54 | post = post.replace(key, postVariables[key]); 55 | } 56 | 57 | client.post('statuses/update', { status: post }, function (error, tweet, response) { 58 | if (!error) { 59 | console.log(post); 60 | } 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /assets/madeira.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /components/DatePickerButton.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, useEffect, useState } from 'react'; 2 | import DatePicker from 'react-datepicker'; 3 | import { format, subDays } from 'date-fns'; 4 | import styles from './DatePickerButton.module.scss'; 5 | 6 | import Arrow from '../assets/arrow.svg'; 7 | import { pt } from 'date-fns/locale'; 8 | 9 | export function DatePickerButton({ minDate, maxDate, onDateSelect, colors }) { 10 | const [startDate, setStartDate] = useState(new Date(maxDate)); 11 | 12 | var [first, ...restDate] = format(startDate, "eeee, dd 'de' LLLL 'de' yyyy", { 13 | locale: pt, 14 | }) 15 | .replace('-feira', '') 16 | .split(''); 17 | 18 | let d = [first.toUpperCase(), ...restDate].join(''); 19 | 20 | var [first, ...restDate] = format(startDate, "dd 'de' LLL 'de' yyyy", { 21 | locale: pt, 22 | }) 23 | .replace('-feira', '') 24 | .split(''); 25 | 26 | let d2 = [first.toUpperCase(), ...restDate].join(''); 27 | 28 | useEffect(() => { 29 | onDateSelect?.(new Date(startDate)); 30 | }, [startDate]); 31 | 32 | useEffect(() => { 33 | setStartDate(new Date(maxDate)); 34 | }, [maxDate]); 35 | 36 | class ExampleCustomInput extends Component { 37 | constructor(props) { 38 | super(props); 39 | } 40 | 41 | render() { 42 | let { onClick } = this.props; 43 | return ( 44 | <> 45 |
46 | 56 | 57 | 61 | 71 |
72 | 73 | ); 74 | } 75 | } 76 | 77 | return ( 78 | { 83 | setStartDate(date); 84 | }} 85 | customInput={} 86 | /> 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /_readme/SOURCES.md: -------------------------------------------------------------------------------- 1 | 2 | # Origem dos dados 3 | 4 | 5 | O processo de recolha de dados é automatizado com um pequeno *script* em node que vai ver se já existem dados recentes, e se existirem, adiciona e publica esses dados neste repositório. A fonte dos dados varia de gráfico para gráfico, mas estas são as fontes que são consultadas: 6 | 7 | - [Monitorização do SNS da Direção-Geral da Saúde](https://www.sns.gov.pt/monitorizacao-do-sns/vacinas-covid-19/) 8 | - [Ponto de Situação Direção-Geral da Saúde](https://covid19.min-saude.pt/ponto-de-situacao-atual-em-portugal/) 9 | - [Relatórios de Vacinação - Direção-Geral da Saúde](https://covid19.min-saude.pt/relatorio-de-vacinacao/) 10 | - [Our World of Data](https://github.com/owid/covid-19-data/blob/master/public/data/vaccinations/country_data/Portugal.csv) 11 | - [Centro Europeu de Controlo de Doenças](https://covid19-vaccine-report.ecdc.europa.eu/) 12 | - [Instituto Nacional Doutor Ricardo Jorge](http://www.insa.min-saude.pt/category/areas-de-atuacao/epidemiologia/covid-19-curva-epidemica-e-parametros-de-transmissibilidade/) 13 | 14 | - [Região Autónoma da Madeira - Ponto de Situação Vacinação COVID-19 ](https://covidmadeira.pt/ponto-de-situacao-vacinacao-covid-19/) 15 | - [Região Autónoma da Madeira - Ponto de Situação Diário](https://covidmadeira.pt/ponto-de-situacao/). Atualizamos diariamente estes dados (transcrevemos das imagens) neste [Google Sheets](https://docs.google.com/spreadsheets/d/16wucf-R89vxoL_QCmYL2ChYi-iKFVMQyIr7qsV_5kw0/edit?usp=sharing), que também podes descarregar em [CSV](https://docs.google.com/spreadsheets/d/16wucf-R89vxoL_QCmYL2ChYi-iKFVMQyIr7qsV_5kw0/export?format=csv&gid=0). **Estamos à procura de voluntários para nos ajudar a manter esta informação atualizada, e também para transcrever os boletins anteriores** 16 | - [Região Autónoma da Açores - Vacinação COVID-19 ](https://vacinacao-covid19.azores.gov.pt/) 17 | - [Região Autónoma da Açores - Portal COVID19](https://destinoseguro.azores.gov.pt/). Atualizamos diariamente estes dados (transcrevemos das imagens) neste [Google Sheets](https://docs.google.com/spreadsheets/d/16wucf-R89vxoL_QCmYL2ChYi-iKFVMQyIr7qsV_5kw0/edit?usp=sharing), que também podes descarregar em [CSV](https://docs.google.com/spreadsheets/d/16wucf-R89vxoL_QCmYL2ChYi-iKFVMQyIr7qsV_5kw0/export?format=csv&gid=1371982439). **Estamos à procura de voluntários para nos ajudar a manter esta informação atualizada, e também para transcrever os boletins anteriores** -------------------------------------------------------------------------------- /automation/madeira_parser.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from dateutil.parser import parse 4 | from dateutil.relativedelta import * 5 | import os 6 | from datetime import datetime 7 | 8 | madeira_file = open("./data/ecdc_filtered.json") 9 | total_items = len(json.load(madeira_file)) 10 | madeira_file.close() 11 | 12 | url = 'https://www.dnoticias.pt/covid.json' 13 | 14 | res = requests.get(url) 15 | json_res = res.json() 16 | json_res.reverse() 17 | filtered_arr = [] 18 | 19 | index = 0; 20 | for record in json_res: 21 | dados_transformados = { 22 | "dia":record["day"], 23 | "total":int(record["confirmed_cases"]), 24 | "total_novos":0, 25 | "recuperados":int(record["total_cases_recovered"]), 26 | "recuperados_novos":0, 27 | "obitos":int(record["total_deaths"]), 28 | "obitos_var":0, 29 | "ativos":int(record["confirmed_cases"]) - int(record["total_deaths"]) - int(record["total_cases_recovered"]), 30 | "ativos_var":0, 31 | } 32 | if (index > 0): 33 | dia_anterior = filtered_arr[index-1] 34 | dados_transformados["total_novos"] = dados_transformados["total"] - dia_anterior["total"] 35 | dados_transformados["recuperados_novos"] = dados_transformados["recuperados"] - dia_anterior["recuperados"] 36 | dados_transformados["obitos_var"] = dados_transformados["obitos"] - dia_anterior["obitos"] 37 | dados_transformados["ativos_var"] = dados_transformados["ativos"] - dia_anterior["ativos"] 38 | 39 | 40 | index+=1 41 | filtered_arr.append(dados_transformados) 42 | 43 | if(len(filtered_arr) != total_items): 44 | owid_file = open("./data/madeira_pds.json", "w") 45 | owid_file.write(json.dumps(filtered_arr)) 46 | owid_file.close() 47 | 48 | weeks_file = open('./data/weeks.json') 49 | weeks_data = json.load(weeks_file) 50 | 51 | last_item = filtered_arr[::-1][0]; 52 | 53 | madeira_cases_date = parse(last_item['dia']) 54 | json_file = open('./data/last-update.json', 'r+') 55 | json_datas = json.load(json_file) 56 | json_datas['dateMadeiraCases'] = madeira_cases_date.strftime('%Y-%m-%d') 57 | json_file.seek(0) 58 | json_file.write(json.dumps(json_datas)) 59 | json_file.close() 60 | 61 | # prepare commit 62 | stream = os.popen('git status | grep madeira_pds.json') 63 | output = stream.read() 64 | if(output): 65 | os.system('git add .') 66 | os.system('git commit -m "covid update - madeira cases - ' + str(madeira_cases_date) + '"') 67 | os.system('git push') 68 | os.system('yarn deploy') 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /components/graphs/PieSuscetiveisProporcao.jsx: -------------------------------------------------------------------------------- 1 | import { Pie } from 'react-chartjs-2'; 2 | import { Card } from './../Card'; 3 | import { formatNumber } from './../../utils'; 4 | export function PieSuscetiveisProporcao({ statistics, colors }) { 5 | let vaccines = statistics.getLastVaccineAvaliable(); 6 | let lastCase = statistics.getLastCaseAvaliable(); 7 | 8 | // let infetadosVacinados = vaccines.dose_2 - lastCase.confirmados; 9 | let vacinados_apenas_uma = Math.abs(vaccines.dose_1 - vaccines.dose_2); 10 | let populacao_suscetivel = 11 | lastCase.populacao - (vacinados_apenas_uma + vaccines.dose_2 + lastCase.ativos + lastCase.recuperados + lastCase.obitos); 12 | //let populacao_suscetivel = 10286300 - (vaccines[vaccines.length - 1].Inoculacao2_Ac + infetadosVacinados + firstItem.recuperados + firstItem.obitos); 13 | let { main, tints, shades, complements } = colors; 14 | 15 | const data = () => { 16 | return { 17 | //labels: ['Vacinados (com as duas doses)', 'Casos Ativos', 'Casos Recuperados', 'Óbitos', 'População suscetível'], 18 | labels: ['Vacinação Iniciada', 'Vacinação Completa', 'Casos Ativos', 'Casos Recuperados', 'Óbitos', 'População suscetível'], 19 | datasets: [ 20 | { 21 | backgroundColor: [tints[1], main, complements[0], complements[2], shades[2], complements[1]], 22 | data: [vacinados_apenas_uma, vaccines.dose_2, lastCase.ativos, lastCase.recuperados, lastCase.obitos, populacao_suscetivel], 23 | //data: [vaccines.dose_2 , valueCasesDiarios.reverse()[0].ativos, valueCasesDiarios.reverse()[0].recuperados, valueCasesDiarios.reverse()[0].obitos, populacao_suscetivel], 24 | }, 25 | ], 26 | }; 27 | }; 28 | const options = () => { 29 | return { 30 | maintainAspectRatio: false, 31 | plugins: { 32 | datalabels: { 33 | color: 'white', 34 | formatter: (value, chart) => { 35 | let sum = chart.dataset.data.reduce((prev, curr) => { 36 | return prev + curr; 37 | }, 0); 38 | sum = (value / sum) * 100; 39 | 40 | if (sum > 10) { 41 | return sum.toFixed(2) + '%'; 42 | } 43 | return ''; 44 | }, 45 | }, 46 | legend: { 47 | position: 'bottom', 48 | align: 'start', 49 | }, 50 | }, 51 | onResize: (a, b, c) => {}, 52 | 53 | animation: { 54 | duration: 1000, 55 | }, 56 | tooltips: { 57 | callbacks: { 58 | label: function ({ index }, { datasets, labels }) { 59 | let label = labels[index]; 60 | let data = datasets[0].data[index]; 61 | return `${label}: ${formatNumber(data)}`; 62 | }, 63 | }, 64 | }, 65 | }; 66 | }; 67 | 68 | return ( 69 | 70 |
{}
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /automation/convert-csv:vaccines.js: -------------------------------------------------------------------------------- 1 | var csv = require('csvtojson'); 2 | const { default: fetch } = require('node-fetch'); 3 | let fs = require('fs'); 4 | let json = require('./../data/last-update.json'); 5 | var parse = require('date-fns/parse'); 6 | 7 | const { zonedTimeToUtc, utcToZonedTime } = require('date-fns-tz'); 8 | const { tr } = require('date-fns/locale'); 9 | 10 | const timeZone = 'Europe/London'; 11 | 12 | async function convertVaccines(cb = null, write = false, error_cb = null, fetch_remote = true) { 13 | try { 14 | if (fetch_remote === false) { 15 | throw new Error('fetch local'); 16 | } 17 | console.log(); 18 | console.log('fetch remote'); 19 | let contents = await fetch('https://raw.githubusercontent.com/dssg-pt/covid19pt-data/master/vacinas.csv').then((res) => res.buffer()); 20 | let rawJsonArrayObj = await csv({ 21 | colParser: { 22 | doses: 'number', 23 | doses_novas: 'number', 24 | doses1: 'number', 25 | doses1_novas: 'number', 26 | doses2: 'number', 27 | doses2_novas: 'number', 28 | pessoas_vacinadas_completamente: 'number', 29 | pessoas_vacinadas_completamente_novas: 'number', 30 | pessoas_vacinadas_parcialmente: 'number', 31 | pessoas_vacinadas_parcialmente_novas: 'number', 32 | pessoas_inoculadas: 'number', 33 | pessoas_inoculadas_novas: 'number', 34 | vacinas: 'number', 35 | vacinas_novas: 'number', 36 | }, 37 | }).fromString(contents.toString()); 38 | 39 | let jsonArrayObj = rawJsonArrayObj.map((item) => { 40 | let date_utc = parse(item.data, 'dd-MM-yyyy', new Date()); 41 | date_utc = zonedTimeToUtc(date_utc, 'UTC'); 42 | const zonedDate = utcToZonedTime(date_utc, timeZone); 43 | return { 44 | data_vac: item.data, 45 | doses: item.doses || null, 46 | doses_novas: item.doses_novas || null, 47 | doses1: item.doses1 || null, 48 | doses1_novas: item.doses1_novas || null, 49 | doses2: item.doses2 || null, 50 | doses2_novas: item.doses2_novas || null, 51 | pessoas_vacinadas_completamente: item.pessoas_vacinadas_completamente || null, 52 | pessoas_vacinadas_completamente_novas: item.pessoas_vacinadas_completamente_novas || null, 53 | pessoas_vacinadas_parcialmente: item.pessoas_vacinadas_parcialmente || null, 54 | pessoas_vacinadas_parcialmente_novas: item.pessoas_vacinadas_parcialmente_novas || null, 55 | pessoas_inoculadas: item.pessoas_inoculadas || null, 56 | pessoas_inoculadas_novas: item.pessoas_inoculadas_novas || null, 57 | vacinas: item.vacinas || null, 58 | vacinas_novas: item.vacinas_novas || null, 59 | data_vac_iso: zonedDate, 60 | }; 61 | }); 62 | if (cb) cb(jsonArrayObj); 63 | return jsonArrayObj; 64 | } catch (e) { 65 | let filedir = process.cwd() + '/data/vaccines_dssg.json'; 66 | data = JSON.parse(fs.readFileSync(filedir)); //fallback to the stored data 67 | if (error_cb) error_cb(); 68 | return data; 69 | } 70 | } 71 | 72 | module.exports = convertVaccines; 73 | -------------------------------------------------------------------------------- /automation/fcm.py: -------------------------------------------------------------------------------- 1 | import firebase_admin 2 | from firebase_admin import credentials 3 | from firebase_admin import messaging 4 | import datetime 5 | import json 6 | from dateutil.relativedelta import * 7 | import locale 8 | locale.setlocale(locale.LC_ALL, '') 9 | 10 | 11 | def format_number(n): 12 | return "{:,d}".format(n).replace(',', ' ') 13 | 14 | def pad(n): 15 | return str(n).zfill(2) 16 | 17 | try: 18 | json_file = open('./automation/fcm-conf.json', 'r+') 19 | json_datas = json.load(json_file) 20 | except: 21 | json_datas = json.loads('{"last_update":0}') 22 | 23 | last_update = datetime.datetime.fromisoformat(json_datas['last_update']) 24 | 25 | # Get the last vaccination date and convert to a format that python understands 26 | vaccines = open('./data/vaccines_dssg.json', 'r') 27 | parsed = json.load(vaccines) 28 | last_vaccine = parsed[-1] 29 | last_vaccine_date = last_vaccine['data_vac_iso'].replace('Z','') 30 | last_vaccine_date = datetime.datetime.fromisoformat(last_vaccine_date) 31 | 32 | 33 | if(last_update.date() != last_vaccine_date.date()): 34 | text = open('./automation/onesignal.txt', 'r') 35 | text = text.read() 36 | prev_last_vaccine2 =parsed[-2] 37 | 38 | if prev_last_vaccine2['doses'] == None: 39 | text = open('./automation/onesignal_no_daily.txt', 'r') 40 | text = text.read() 41 | text = text.replace("{{total_total}}", format_number(last_vaccine['doses'])) 42 | text = text.replace("{{total_in1}}", format_number(last_vaccine['doses1'])) 43 | text = text.replace("{{total_in2}}", format_number(last_vaccine['doses2'])) 44 | else: 45 | text = text.replace("{{total_total}}", format_number(last_vaccine['doses'])) 46 | text = text.replace("{{total_in1}}", format_number(last_vaccine['doses1'])) 47 | text = text.replace("{{total_in2}}", format_number(last_vaccine['doses2'])) 48 | 49 | text = text.replace("{{novas_total}}", format_number(last_vaccine['doses'] - prev_last_vaccine2['doses'])) 50 | text = text.replace("{{novas_in1}}", format_number(last_vaccine['doses1'] - prev_last_vaccine2['doses1'])) 51 | text = text.replace("{{novas_in2}}", format_number(last_vaccine['doses2'] - prev_last_vaccine2['doses2'])) 52 | 53 | cred = credentials.Certificate("./firebase_account.json") 54 | firebase_admin.initialize_app(cred) 55 | messaging = firebase_admin.messaging 56 | 57 | topic = 'covid19' 58 | message = messaging.Message( 59 | notification=messaging.Notification( 60 | title="Os dados da vacinação de {0}/{1}/{2}".format(pad(last_vaccine_date.day),pad(last_vaccine_date.month),last_vaccine_date.year), 61 | body=text, 62 | ), 63 | topic=topic, 64 | ) 65 | 66 | response = messaging.send(message) 67 | print(text) 68 | 69 | json_datas['last_update'] = str(last_vaccine_date.date()) 70 | json_file = open('./automation/fcm-conf.json', 'w') 71 | json_file.write(json.dumps(json_datas)) 72 | json_file.close() 73 | 74 | 75 | -------------------------------------------------------------------------------- /components/Card.module.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/colors.scss'; 2 | 3 | .card { 4 | .container { 5 | padding: 0; 6 | .collapsed { 7 | height: 0; 8 | overflow: hidden; 9 | } 10 | } 11 | 12 | &_graph { 13 | text-align: center; 14 | min-height: 50px; 15 | padding: 10px 0px; 16 | border-radius: 5px; 17 | margin-bottom: 15px; 18 | overflow: hidden; 19 | transition: background linear 500ms; 20 | } 21 | 22 | &_counter { 23 | padding: 20px 5px; 24 | } 25 | &_checkboxes { 26 | label { 27 | margin-bottom: 10px; 28 | margin-right: 10px; 29 | } 30 | } 31 | 32 | &_align_left { 33 | text-align: left; 34 | } 35 | 36 | &_graph_updated { 37 | background: radial-gradient(#f3fffd, #fff); 38 | transition: background linear 500ms; 39 | } 40 | 41 | &_highlight { 42 | font-size: 35px; 43 | // color: $main; 44 | font-weight: bolder; 45 | } 46 | &_highlight_2 { 47 | font-size: 30px; 48 | // color: $main; 49 | font-weight: bold; 50 | } 51 | &_title { 52 | margin-bottom: 0px; 53 | color: #444; 54 | font-size: 15px; 55 | margin-bottom: 5px; 56 | } 57 | 58 | &_title_2 { 59 | margin-bottom: 0px; 60 | color: #444; 61 | font-weight: bold; 62 | font-size: 14px; 63 | margin-bottom: 5px; 64 | text-transform: uppercase; 65 | } 66 | 67 | &_subtitle_2 { 68 | margin-bottom: 0px; 69 | color: #444; 70 | font-size: 13px; 71 | //text-transform: uppercase; 72 | margin: -5px 0px -13px 0px; 73 | } 74 | 75 | &_subtitle { 76 | margin-bottom: 0px; 77 | font-size: 13px; 78 | 79 | &_highlight { 80 | margin-bottom: 0px; 81 | // color: $main; 82 | font-weight: bold; 83 | } 84 | } 85 | 86 | &_fixed_scroll, 87 | &_dynamic_scroll { 88 | overflow-x: scroll; 89 | } 90 | 91 | &_sticky { 92 | position: sticky; 93 | left: 0; 94 | } 95 | } 96 | 97 | /* Extra large devices (large desktops, 1200px and up)*/ 98 | @media (min-width: 1200px) { 99 | } 100 | 101 | @media (max-width: 992px) { 102 | .card { 103 | &_dynamic_scroll { 104 | //min-width: 1000px; 105 | } 106 | } 107 | } 108 | 109 | @media (min-width: 992px) { 110 | .card { 111 | &_graph { 112 | //min-height: 250px; 113 | } 114 | } 115 | } 116 | 117 | @media screen and (min-width: 1040px) { 118 | .card { 119 | &_dynamic_scroll { 120 | overflow-x: hidden; 121 | } 122 | } 123 | } 124 | 125 | @media screen and (min-width: 1500px) { 126 | .card { 127 | &_graph { 128 | //min-height: 210px; 129 | } 130 | } 131 | } 132 | .text_left { 133 | text-align: left; 134 | } 135 | .ram { 136 | &_subchart { 137 | p { 138 | font-weight: bold; 139 | font-size: 13px; 140 | text-align: center; 141 | } 142 | 143 | &_bar { 144 | padding: 5px; 145 | } 146 | } 147 | h3 { 148 | //font-weight: bold; 149 | font-size: 13px; 150 | text-align: center; 151 | } 152 | h2 { 153 | font-size: 40px !important; 154 | font-weight: bold; 155 | color: rgb(1, 174, 151); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "c19-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "browserslist": "last 10 versions and > 0.05% or ie>9", 6 | "scripts": { 7 | "deploy": "git checkout master && git pull --rebase && git merge develop --strategy-option theirs --no-ff --no-edit && git push", 8 | "update": "node automation/update.js && git add data/last-update.json", 9 | "no-commit-to-master": "if [ \"$(git rev-parse --abbrev-ref HEAD)\" = 'master' ]; then echo 'You cant commit directly to master branch';exit 1; fi;", 10 | "dev": "next dev -p 5500", 11 | "v": "next -v", 12 | "build": "next build", 13 | "start": "next start", 14 | "export": "next build && next export -o out2", 15 | "image": "cd automation/ && node instagram.js && cd - && git add automation", 16 | "twitter": "cd automation/ && node twitter.js && cd - ", 17 | "socket:refresh": "cd automation/ && node pusher.js && cd - ", 18 | "notification:push": "python3 automation/fcm.py", 19 | "deploy:manual": "yarn deploy && (yarn twitter & yarn pusher & yarn notification:push)", 20 | "convert:csv": "node automation/convert-csv.js", 21 | "convert:xls": "node automation/convert-xls.js", 22 | "convert": "yarn convert:csv & yarn convert:xls", 23 | "delete": "(git reset .next && git checkout .next) & git clean -fd .next", 24 | "update:server": "sh run.sh", 25 | "bot:start": "HARDWARE=raspberry pm2 start daemon_data --name \"daemon\"", 26 | "bot:manual": "HARDWARE=raspberry node daemon.js", 27 | "casos:ram": "python3 ./automation/madeira_parser.py" 28 | }, 29 | "browser": { 30 | "fs": false, 31 | "path": false, 32 | "os": false 33 | }, 34 | "bin": { 35 | "daemon_data": "./daemon.js" 36 | }, 37 | "pre-commit": [ 38 | "delete", 39 | "no-commit-to-master", 40 | "update" 41 | ], 42 | "dependencies": { 43 | "@babel/polyfill": "^7.12.1", 44 | "bootstrap": "^5.1.0", 45 | "chart.js": "^3.0.0", 46 | "chartjs-plugin-annotation": "^1.0.0", 47 | "chartjs-plugin-datalabels": "^2.0.0-beta.1", 48 | "classnames": "^2.2.6", 49 | "core-js": "3", 50 | "cors": "^2.8.5", 51 | "csvtojson": "^2.0.10", 52 | "date-fns": "^2.17.0", 53 | "date-fns-tz": "^1.1.6", 54 | "dotenv": "^10.0.0", 55 | "firebase-admin": "^9.6.0", 56 | "minimist": "^1.2.5", 57 | "mongodb": "^4.1.0", 58 | "next": "^11.0.1", 59 | "next-react-svg": "^1.1.2", 60 | "node-fetch": "^2.6.1", 61 | "plausible-tracker": "^0.3.1", 62 | "pusher": "^5.0.0", 63 | "react": "17.0.2", 64 | "react-bootstrap": "^1.4.3", 65 | "react-chartjs-2": "^3.0.2", 66 | "react-count-to": "^0.12.0", 67 | "react-datepicker": "^4.2.0", 68 | "react-dom": "17.0.2", 69 | "react-lazyload": "^3.2.0", 70 | "react-spinners-kit": "^1.9.1", 71 | "sass": "^1.32.5", 72 | "shelljs": "^0.8.4" 73 | }, 74 | "devDependencies": { 75 | "@babel/core": "^7.12.16", 76 | "@babel/preset-env": "^7.12.16", 77 | "@welldone-software/why-did-you-render": "^6.2.0", 78 | "convert-excel-to-json": "^1.7.0", 79 | "follow-redirects": "^1.13.2", 80 | "node-schedule": "^2.0.0", 81 | "pre-commit": "^1.2.2", 82 | "twitter": "^1.7.1" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /components/graphs/BarsVacinacaoArs.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Bar } from 'react-chartjs-2'; 3 | import { formatNumber } from '../../utils'; 4 | import { Card } from './../Card'; 5 | import { useCanvasResizer } from '../../hooks/useCanvasResizer'; 6 | 7 | export function BarsVacinacaoArs({ statistics, colors }) { 8 | let [loading, setLoading] = useState(true); 9 | let { main, tints } = colors; 10 | const [snsData, setSNSData] = useState({}); 11 | let { setCanvasNode } = useCanvasResizer(); 12 | 13 | const data = (canvas) => { 14 | setCanvasNode(canvas.parentNode); 15 | 16 | return { 17 | labels: snsData.filter((el) => el.REGION != 'All').map((el) => el.REGION.replace('All', 'Nacional')), 18 | datasets: [ 19 | { 20 | label: '1ª Dose', 21 | backgroundColor: main, 22 | data: snsData.filter((el) => el.REGION != 'All').map((el) => el.CUMUL_VAC_1), 23 | 24 | fill: false, 25 | stack: 'stack1', 26 | }, 27 | { 28 | label: '2ª Dose', 29 | backgroundColor: tints[1], 30 | data: snsData.filter((el) => el.REGION != 'All').map((el) => el.CUMUL_VAC_2), 31 | stack: 'stack1', 32 | }, 33 | ], 34 | }; 35 | }; 36 | 37 | const options = () => { 38 | return { 39 | plugins: { 40 | datalabels: { 41 | display: false, 42 | color: 'white', 43 | }, 44 | }, 45 | layout: { 46 | padding: -5, 47 | }, 48 | legend: { 49 | display: true, 50 | position: 'top', 51 | align: 'start', 52 | onHover: function (event, legend) { 53 | document.body.classList.add('mouse-pointer'); 54 | }, 55 | onLeave: function (event, legend) { 56 | document.body.classList.remove('mouse-pointer'); 57 | }, 58 | }, 59 | 60 | animation: { 61 | duration: 1000, 62 | }, 63 | tooltips: { 64 | mode: 'index', 65 | intersect: false, 66 | callbacks: { 67 | label: (tooltipItem, data) => { 68 | var label = data.datasets[tooltipItem.datasetIndex].label; 69 | return label + ': ' + formatNumber(parseInt(tooltipItem.value), false); 70 | }, 71 | title: () => { 72 | return ''; 73 | }, 74 | }, 75 | }, 76 | scales: { 77 | yAxes: [ 78 | { 79 | gridLines: { 80 | display: true, 81 | }, 82 | ticks: { 83 | display: true, 84 | maxTicksLimit: 7, 85 | minTicksLimit: 7, 86 | callback: (value) => formatNumber(value, false), 87 | }, 88 | }, 89 | ], 90 | xAxes: [ 91 | { 92 | stacked: true, 93 | gridLines: { 94 | display: true, 95 | }, 96 | 97 | ticks: { 98 | beginAtZero: true, 99 | display: true, 100 | }, 101 | }, 102 | ], 103 | }, 104 | }; 105 | }; 106 | 107 | useEffect(async () => { 108 | setSNSData(await statistics.getTotalSNS()); 109 | setLoading(false); 110 | }, []); 111 | return !loading === true ? ( 112 | 113 |
114 | {!loading ? ( 115 | <> 116 | 117 | 118 | ) : ( 119 | '' 120 | )} 121 |
122 |
123 | ) : ( 124 | '' 125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /components/Header.jsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { useContext, useEffect, useState } from 'react'; 3 | import { Container } from 'react-bootstrap'; 4 | import { RegiaoContext } from './context/regiao'; 5 | import styles from './Header.module.scss'; 6 | import { Notifications } from './Notifications'; 7 | import Bell from '../assets/bell.svg'; 8 | import Twitter from '../assets/twitter.svg'; 9 | import Plus from '../assets/plus.svg'; 10 | 11 | export function Header() { 12 | let [supportsNotifications, setSupportsNotifications] = useState(false); 13 | let [isSidebarOpen, setIsSidebarOpen] = useState(false); 14 | let regiao = useContext(RegiaoContext); 15 | let regioes = { 16 | portugal: { 17 | nome: '', 18 | tagline: 'Dados atualizados diariamente entre as 13h e as 14h', 19 | }, 20 | madeira: { 21 | nome: ' - Madeira', 22 | tagline: 'Dados atualizados semanalmente', 23 | }, 24 | acores: { 25 | nome: ' - Açores', 26 | tagline: 'Dados atualizados semanalmente', 27 | }, 28 | }; 29 | 30 | useEffect(() => { 31 | setSupportsNotifications('Notification' in window); 32 | }, []); 33 | 34 | function renderLocalInfo() { 35 | return ( 36 | 96 | ); 97 | } 98 | 99 | return ( 100 | <> 101 |
102 | 103 |
104 |

vacinação COVID 19

105 |

106 |
107 |
108 |
109 |
110 | 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /automation/sesaram.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: './../.env' }); 2 | let fs = require('fs'); 3 | const fetch = require('node-fetch'); 4 | 5 | function createDate(date) { 6 | let dia = date.getDate().toLocaleString('en-US', { 7 | minimumIntegerDigits: 2, 8 | }); 9 | let mes = (date.getMonth() + 1).toLocaleString('en-US', { 10 | minimumIntegerDigits: 2, 11 | }); 12 | let ano = date.getFullYear(); 13 | return `${ano}-${mes}-${dia}`; 14 | } 15 | 16 | async function parseSesaram() { 17 | let resSesaram = await fetch('https://web.sesaram.pt/COVID19_INFO', { 18 | headers: { 19 | accept: '*/*', 20 | origin: 'https://web.sesaram.pt', 21 | 'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8', 22 | 'content-type': 'application/x-www-form-urlencoded', 23 | 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 24 | 'sec-ch-ua-mobile': '?0', 25 | 'sec-fetch-dest': 'empty', 26 | 'sec-fetch-mode': 'cors', 27 | 'sec-fetch-site': 'same-origin', 28 | }, 29 | referrer: 'https://web.sesaram.pt/COVID19_INFO', 30 | referrerPolicy: 'strict-origin-when-cross-origin', 31 | method: 'POST', 32 | mode: 'cors', 33 | credentials: 'omit', 34 | }); 35 | 36 | let text = await resSesaram.text(); 37 | let url = new RegExp(/action="(.*)".*target/gm); 38 | let a = url.exec(text)[1]; 39 | 40 | resSesaram = await fetch('https://web.sesaram.pt/' + a, { 41 | headers: { 42 | accept: '*/*', 43 | origin: 'https://web.sesaram.pt', 44 | 'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8', 45 | 'content-type': 'application/x-www-form-urlencoded', 46 | 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90"', 47 | 'sec-ch-ua-mobile': '?0', 48 | 'sec-fetch-dest': 'empty', 49 | 'sec-fetch-mode': 'cors', 50 | 'sec-fetch-site': 'same-origin', 51 | }, 52 | referrer: 'https://web.sesaram.pt/COVID19_INFO', 53 | referrerPolicy: 'strict-origin-when-cross-origin', 54 | body: 'WD_ACTION_=AJAXEXECUTE&EXECUTEPROC=PAGE_VACINACAO.obtemTotalVacinasAdministradas&WD_CONTEXTE_=', 55 | method: 'POST', 56 | mode: 'cors', 57 | credentials: 'omit', 58 | }); 59 | text = await resSesaram.text(); 60 | let [d1, d2, total] = text.split('|'); 61 | let date = new Date(); 62 | 63 | let body = { 64 | data: createDate(date), 65 | last_update: date.getTime(), 66 | total: parseInt(total), 67 | dose_1: parseInt(d1), 68 | dose_2: parseInt(d2), 69 | }; 70 | 71 | return body; 72 | } 73 | 74 | const scrapSesaram = async function (onUpdate) { 75 | let sesaram = []; 76 | console.log('Updating SESARAM'); 77 | 78 | if (fs.existsSync('./data/sesaram.json')) { 79 | sesaram = JSON.parse(fs.readFileSync('./data/sesaram.json')); //do not cache this pls 80 | } 81 | 82 | let data = await parseSesaram(); 83 | 84 | let idx = sesaram.findIndex((i) => { 85 | return i.data === data.data; 86 | }); 87 | 88 | if (idx > -1) { 89 | if (data.total > sesaram[idx].total) { 90 | sesaram[idx] = data; 91 | if (onUpdate) { 92 | fs.writeFileSync('././data/sesaram.json', JSON.stringify(sesaram)); 93 | console.log('Updated SESARAM'); 94 | onUpdate(); 95 | } 96 | } 97 | } else { 98 | sesaram.push(data); 99 | if (onUpdate) { 100 | fs.writeFileSync('././data/sesaram.json', JSON.stringify(sesaram)); 101 | console.log('Updated SESARAM'); 102 | onUpdate(); 103 | } 104 | } 105 | }; 106 | 107 | module.exports = scrapSesaram; 108 | -------------------------------------------------------------------------------- /components/MetaTags.jsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | export function Metatags({ isUpdating }) { 3 | let title = 'Vacinação COVID-19 - Dashboard sobre os dados da campanha de vacinação contra a COVID-19 em Portugal e Arquipélagos'; 4 | let descricao = `Site informativo sobre a administração das vacinas em Portugal. É atualizado sempre que possível, assim que os dados forem sendo atualizados. Contamos com dados da Direção-Geral da Saúde, Our World in Data, Centro Europeu de Controlo de Doenças e informação do Governo de Portugal, temos gráficos sobre as vacinas administradas por dia e desde o início da campanha de vacinação, compradas, a faixa etária dos inoculados, infeções, óbitos, entre outros e temos números relacionados com a imunidade de grupo entre outros. Todo o nosso código é open-source, e pode ser consultado no github onde está alojado. Temos ainda uma conta no twitter onde pode seguir as últimas atualizações em relação aos números da campanha de vacinação da COVID-19.`; 5 | 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {title} 41 | 42 | 43 |

{descricao}

44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🇵🇹💉 Vacinação COVID19 - Dashboard [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Falicescfernandes%2Fmapa-vacinacao-c19) 2 | 3 | ## Olá! Este projeto já não está a ser mantido, e já não vai ser mais atualizado. A quem acompanhou, obrigado! 4 | 5 | Este projeto foi desenvolvido com o intuito de mostrar o estado atual do plano de vacinação contra a covid-19. É um trabalho em progresso que vai sofrer atualizações ao longo do tempo, e também recebe os últimos números relacionados com administração das vacinas. 6 | Todo o código é público (incluindo também os desenhos originais do site), para que seja possível ser adaptado para outras utilizações, e os dados também são atualizados diretamente neste repositório. Para do _dashboard_, é também disponibilizada um [API](https://vacinacaocovid19.pt/api/vaccines) que pode ser consumida por outras aplicações. Se tiveres alguma sugestão de gráficos que sejam pertinentes, podes contribuir diretamente para o código, ou podes enviar sugestões por e-mail. 7 | ## Referências 8 | - [Plausible (dados de analytics)](https://plausible.io/vacinacaocovid19.pt) 9 | - [API](https://vacinacaocovid19.pt/api/vaccines) 10 | 11 | ## Origem dos dados 12 | Ver [SOURCES.MD](/_readme/SOURCES.md) 13 | ## Stack 14 | ![infra](./_readme/asset/infra.png) 15 | 16 | 17 | - ChartJS 18 | - React 19 | - NextJS 20 | - Pusher para sockets 21 | - Firebase Cloud Messaging + Web Push API para notificações (incluindo notificações com o site desligado) 22 | - MongoDB para gerir tokens de notificações 23 | - JSON como serviço de dados 24 | - Github Actions para CI 25 | - Github Hooks (para lançar um novo deploy) 26 | - ~~Vercel~~ Já não estamos na Vercel! 27 | - Cloudflare 28 | 29 | 30 | 31 | ## Fazer setup local (com docker) 32 | ```bash 33 | git clone https://github.com/alicescfernandes/mapa-vacinacao-c19/ 34 | docker build -t vacinacaocovid19 . 35 | docker run -d -p 80:3000 -P --name vacinacaocovid19 vacinacaocovid19 # site em localhost:80 36 | ``` 37 | ## Fazer setup local (sem docker) 38 | ## Criar `.env` 39 | Ver [SETUP.MD](/_readme/SETUP.md) 40 | ### Instalar dependencias 41 | ```bash 42 | pip3 install -r requirements.txt 43 | 44 | # JS 45 | npm install 46 | # ou 47 | yarn 48 | ``` 49 | ### Instalar comandos globais (fazer os symlinks) 50 | ```bash 51 | npm link # ou sudo npm link 52 | daemon_data # comando para fazer update aos dados 53 | ``` 54 | ### Correr projeto 55 | ```bash 56 | npm run start 57 | # ou 58 | yarn start # abre no localhost:3000 59 | ``` 60 | 61 | ## Lançar o scrapper bot _OPCIONAL_ 62 | Os dados são scrappados com um cron job que vai às fontes, retira o JSON e atualiza o respositório ao código. 63 | Existem duas maneiras de lançar o _scrapper_ bot: através de um `screen` ou pelo o `pm2` 64 | 65 | 66 | - Screen 67 | ```bash 68 | sudo apt install screen 69 | # este comando vai correr o daemon_data num screen chamado daemon. 70 | # -S daemon [nome], -d -> detached mode, -m -> abrir sempre uma nova janela; -dm -> abrir uma nova janela em detached mode 71 | screen -S daemon -dm bash -c "daemon_data; exec bash" 72 | 73 | screen -S daemon -X quit # para terminar o screen chamado daemon 74 | screen -r daemon # entrar dentro do screen chamado daemon. Para sair é pressionar CTRL+A e depois D 75 | ``` 76 | 77 | - PM2 78 | ```bash 79 | npm install -g pm2 80 | pm2 start daemon_data --name "daemon" # lançar o processo 81 | pm2 restart daemon # restart do processo 82 | pm2 stop daemon # parar o processo 83 | pm2 logs daemon # Ler logs do processo 84 | pm2 list # Ver todos os processos geridos pelo pm2 85 | pm2 monit # Ver status de todos os processos geridos pelo pm2 86 | ``` 87 | 88 | 89 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns'; 2 | import { pt } from 'date-fns/locale'; 3 | import fetchNode from 'node-fetch'; 4 | import { RESIZE_TRESHOLD } from './constants'; 5 | 6 | export const formatNumber = (number, isDate = true) => { 7 | let numberFormatter = new Intl.NumberFormat('pt-PT', { 8 | minimumIntegerDigits: isDate ? 2 : 1, 9 | }); 10 | return numberFormatter.format(number).replace(/,/gm, ' '); 11 | }; 12 | 13 | import { populacao } from './data/generic.json'; 14 | //https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb 15 | export function hexToRgb(hex) { 16 | var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); 17 | return result 18 | ? { 19 | r: parseInt(result[1], 16), 20 | g: parseInt(result[2], 16), 21 | b: parseInt(result[3], 16), 22 | } 23 | : null; 24 | } 25 | 26 | export function dateWithoutTimezone(unix) { 27 | const dt = new Date(unix); 28 | return new Date(dt.valueOf() + dt.getTimezoneOffset() * 60 * 1000); 29 | } 30 | 31 | export function downloadPNG(canvasElement, graphName) { 32 | var link = document.createElement('a'); 33 | link.download = 'filename.png'; 34 | link.href = canvasElement.toDataURL(); 35 | link.click(); 36 | } 37 | 38 | export function perHundred(total, populacaoTotal = populacao.valor) { 39 | return (total / populacaoTotal) * 100; 40 | } 41 | 42 | export function isSameDay(dateA, dateB) { 43 | let parsedDateA = new Date(dateA); 44 | let parsedDateB = new Date(dateB); 45 | let isSame = parsedDateA.toLocaleDateString() === parsedDateB.toLocaleDateString(); 46 | return isSame; 47 | } 48 | 49 | export function fetchWithLocalCache(url, options) { 50 | let useCache = true; 51 | let [path, cacheBuster] = url.split('?'); 52 | 53 | let items = JSON.parse(JSON.stringify(localStorage)); 54 | 55 | for (var k in items) { 56 | let [lsPath, lsCacheBuster] = k.split('?'); 57 | if (lsPath === path && lsCacheBuster !== cacheBuster) { 58 | useCache = false; 59 | localStorage.removeItem(k); 60 | } 61 | } 62 | 63 | if (window && localStorage.getItem(url) && useCache === true) { 64 | let data = JSON.parse(localStorage.getItem(url)); 65 | return Promise.resolve(data); 66 | } else { 67 | return fetch(url, { 68 | ...options, 69 | headers: { 70 | 'X-Request-Self': true, 71 | }, 72 | }) 73 | .then((res) => res.json()) 74 | .then((data) => { 75 | localStorage.setItem(url, JSON.stringify(data)); 76 | return data; 77 | }); 78 | } 79 | } 80 | 81 | export function formatDateShort(date) { 82 | return format(new Date(date), "dd'/'MM'/'yyyy", { 83 | locale: pt, 84 | }); 85 | } 86 | 87 | export function makeAnnotations(annotationsArray) { 88 | let annotationBoilerplate = { 89 | type: 'line', 90 | mode: 'horizontal', 91 | scaleID: 'y', 92 | value: null, 93 | borderColor: '#0A9DD1', 94 | borderWidth: 1, 95 | borderDash: [5, 5], 96 | 97 | label: { 98 | font: { 99 | style: 'normal', 100 | }, 101 | backgroundColor: 'rgba(255,255,255,0.8)', 102 | cornerRadius: 0, 103 | drawTime: 'afterDraw', 104 | color: '#0A9DD1', 105 | rotation: 270, 106 | xAdjust: 8, 107 | //xAdjust: -8, 108 | yAdjust: 0, 109 | fontSize: '13px', 110 | enabled: true, 111 | content: '', 112 | }, 113 | }; 114 | let arr = []; 115 | annotationsArray.forEach((el) => { 116 | let annotation = { 117 | ...annotationBoilerplate, 118 | mode: el.mode, 119 | scaleID: el.mode === 'horizontal' ? 'y' : 'x', 120 | borderColor: el.color, 121 | value: el.position, 122 | display: el.display, 123 | label: { 124 | ...annotationBoilerplate.label, 125 | content: el.marcador, 126 | color: el.color, 127 | xAdjust: el.xAdjust ?? 0, 128 | }, 129 | }; 130 | arr.push(annotation); 131 | }); 132 | 133 | return arr; 134 | } 135 | 136 | export function calculateDims() { 137 | if (window.innerWidth <= RESIZE_TRESHOLD) { 138 | return { 139 | width: 2000, 140 | height: 350, 141 | }; 142 | } else { 143 | return { 144 | width: 3000, 145 | height: 500, 146 | }; 147 | } 148 | } 149 | 150 | export function getColor(d) { 151 | if (d >= 80) { 152 | return '#01ae97'; 153 | } 154 | 155 | if (d >= 60) { 156 | return '#4dc6b6'; 157 | } 158 | 159 | if (d >= 40) { 160 | return '#80d7cb'; 161 | } 162 | 163 | if (d >= 20) { 164 | return '#b3e7e0'; 165 | } 166 | 167 | if (d >= 0) { 168 | return '#e6f7f5'; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /components/Header.module.scss: -------------------------------------------------------------------------------- 1 | @import '../styles/colors.scss'; 2 | 3 | .header, 4 | .submenu { 5 | background-color: white; 6 | height: 70px; 7 | text-align: center; 8 | max-width: 100%; 9 | &_mobile { 10 | border-top: 1px solid $main; 11 | display: block; 12 | position: fixed; 13 | bottom: 0; 14 | z-index: 3; 15 | height: initial; 16 | .content { 17 | padding: 0px; 18 | nav { 19 | width: 100vw; 20 | * { 21 | cursor: pointer; 22 | } 23 | li { 24 | width: 25%; 25 | margin: 0; 26 | a { 27 | padding: 5px; 28 | display: inline-block; 29 | font-size: 2px; 30 | &:hover { 31 | border-bottom: none; 32 | } 33 | } 34 | span { 35 | display: block; 36 | font-size: 13px; 37 | } 38 | } 39 | } 40 | } 41 | } 42 | display: flex; 43 | width: 100vw; 44 | align-items: center; 45 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 46 | .content { 47 | display: flex; 48 | flex-direction: row; 49 | align-items: center; 50 | justify-content: center; 51 | 52 | * { 53 | cursor: pointer; 54 | } 55 | ul { 56 | padding: 0; 57 | margin: 0; 58 | } 59 | 60 | a { 61 | border-bottom: 2px solid transparent; 62 | color: inherit; 63 | } 64 | 65 | li { 66 | display: inline-block; 67 | margin: 0px 15px; 68 | font-weight: 500; 69 | font-size: 15px; 70 | color: #444444; 71 | &:last-child { 72 | margin-right: 0px; 73 | } 74 | } 75 | } 76 | .logo { 77 | h1 { 78 | color: $main; 79 | font-weight: 900; 80 | text-transform: uppercase; 81 | font-size: 16px; 82 | margin: 0; 83 | } 84 | p { 85 | font-size: 12px; 86 | font-weight: 500; 87 | margin: 0px; 88 | } 89 | } 90 | } 91 | .submenu { 92 | height: 40px; 93 | text-align: center; 94 | display: none; 95 | nav { 96 | line-height: 40px; 97 | margin: auto; 98 | } 99 | .content li { 100 | font-weight: 500; 101 | font-size: 14px; 102 | } 103 | } 104 | 105 | .highlight { 106 | color: $main; 107 | font-weight: 600; 108 | } 109 | 110 | .sidemenu { 111 | display: none; 112 | position: fixed; 113 | top: 0px; 114 | left: 0px; 115 | width: 100vw; 116 | height: 100vh; 117 | background-color: rgba(255, 255, 255, 0.95); 118 | z-index: 3; 119 | box-sizing: border-box; 120 | &_visible { 121 | display: block; 122 | } 123 | &_container { 124 | padding: 0px 40px; 125 | padding-top: 15px; 126 | box-sizing: border-box; 127 | display: inline-block; 128 | width: 100vw; 129 | } 130 | ul, 131 | li { 132 | display: block; 133 | width: 100%; 134 | text-align: center; 135 | margin: 0px; 136 | padding: 8px 0px; 137 | font-size: 16px; 138 | } 139 | &_close { 140 | position: absolute; 141 | top: 5px; 142 | right: 25px; 143 | font-size: 20px; 144 | padding: 5px; 145 | cursor: pointer; 146 | } 147 | 148 | ul { 149 | padding-bottom: 40px; 150 | } 151 | 152 | h1 { 153 | color: $main; 154 | font-weight: 900; 155 | font-size: 20px; 156 | margin: 0; 157 | padding: 20px 0px; 158 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 159 | text-align: center; 160 | } 161 | } 162 | 163 | .content a:hover { 164 | border-bottom: 2px solid $main; 165 | transition: all 0.2s; 166 | } 167 | .hide_mobile { 168 | display: none !important; 169 | } 170 | 171 | @media screen and (max-width: 899px) { 172 | .header { 173 | .content { 174 | display: flex; 175 | flex-direction: column; 176 | align-items: center; 177 | justify-content: center; 178 | } 179 | } 180 | 181 | .header_mobile { 182 | .hide_mobile { 183 | display: block !important; 184 | } 185 | } 186 | .header_mobile { 187 | display: block; 188 | } 189 | } 190 | 191 | @media (min-width: 900px) { 192 | .hide_mobile { 193 | display: inline-block !important; 194 | } 195 | 196 | .header { 197 | background-color: white; 198 | height: 80px; 199 | text-align: left; 200 | 201 | display: flex; 202 | align-items: center; 203 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 204 | 205 | margin: 0; 206 | .logo { 207 | h1 { 208 | font-size: 20px; 209 | } 210 | } 211 | 212 | &_mobile { 213 | display: none; 214 | } 215 | } 216 | } 217 | 218 | @media (min-width: 1090px) { 219 | .hide_mobile { 220 | display: inline-block !important; 221 | } 222 | 223 | .submenu { 224 | display: block; 225 | } 226 | .header { 227 | background-color: white; 228 | height: 80px; 229 | text-align: left; 230 | 231 | display: flex; 232 | align-items: center; 233 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 234 | 235 | margin: 0; 236 | .logo { 237 | h1 { 238 | font-size: 20px; 239 | } 240 | } 241 | &_mobile { 242 | display: none; 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /components/graphs/LineVacinadosInfecoesRecuperados.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { Bar } from 'react-chartjs-2'; 3 | import { RESIZE_TRESHOLD } from '../../constants'; 4 | import { formatNumber } from '../../utils'; 5 | import { Card } from './../Card'; 6 | import { useCanvasResizer } from '../../hooks/useCanvasResizer'; 7 | 8 | export function LineVacinadosInfecoesRecuperados({ statistics, colors }) { 9 | let [loading, setLoading] = useState(true); 10 | let marriedData = {}; 11 | let { values, labels, valuesIn1, valuesIn2, raw: rawDiarios } = statistics.getDiariosInoculacoes(); 12 | let { raw: rawCasos } = statistics.getDiariosCases(); 13 | let { main, shades, complements } = colors; 14 | const numeroDias = 30; 15 | 16 | let { setCanvasNode } = useCanvasResizer(); 17 | 18 | // map the last {numeroDias} days in data 19 | // Marry the data pls 20 | // https://www.youtube.com/watch?v=O4IgYxHEAuk 21 | if (labels.length > 0) { 22 | let datesVaccines = Array.from(rawDiarios).reverse().slice(0, numeroDias); 23 | let datesCases = Array.from(rawCasos).reverse().slice(0, numeroDias); 24 | datesVaccines.forEach((element) => { 25 | let date = new Date(element.data_vac_iso); 26 | let key = `${date.getUTCFullYear()}_${date.getMonth()}_${date.getDate()}`; 27 | marriedData[key] = element; 28 | }); 29 | 30 | datesCases.forEach((element, i) => { 31 | let date = new Date(element.data_cases); 32 | let key = `${date.getUTCFullYear()}_${date.getMonth()}_${date.getDate()}`; 33 | if (marriedData[key] !== undefined) { 34 | marriedData[key] = { 35 | ...element, 36 | ...marriedData[key], 37 | }; 38 | } 39 | }); 40 | } 41 | marriedData = Object.values(marriedData).reverse(); 42 | 43 | const data = (canvas) => { 44 | setCanvasNode(canvas.parentNode); 45 | 46 | return { 47 | labels: labels.slice(labels.length - numeroDias, labels.length), 48 | datasets: [ 49 | { 50 | label: 'Inoculação - 2ª Dose', 51 | fill: false, 52 | type: 'bar', 53 | backgroundColor: main, 54 | data: valuesIn2.slice(valuesIn2.length - numeroDias, valuesIn2.length), 55 | stack: 'stack0', 56 | order: 1, 57 | }, 58 | { 59 | label: 'Inoculação - 1ª Dose / Unidpse', 60 | backgroundColor: shades[0], 61 | borderColor: shades[0], 62 | data: valuesIn1.slice(valuesIn1.length - numeroDias, valuesIn1.length), 63 | stack: 'stack0', 64 | order: 2, 65 | }, 66 | { 67 | label: 'Vacinas administradas', 68 | backgroundColor: shades[0], 69 | borderColor: shades[0], 70 | data: values.slice(values.length - numeroDias, valuesIn1.length), 71 | stack: 'stack0', 72 | order: 2, 73 | }, 74 | 75 | { 76 | label: 'Número de infectados diário', 77 | type: 'bar', 78 | backgroundColor: complements[1], 79 | data: marriedData.map((el) => el.confirmados_novos), 80 | stack: 'stack1', 81 | order: 4, 82 | }, 83 | { 84 | label: 'Número de recuperados diário', 85 | type: 'bar', 86 | backgroundColor: complements[2], 87 | data: marriedData.map((el) => el.var_recuperados), 88 | stack: 'stack2', 89 | order: 5, 90 | }, 91 | ], 92 | }; 93 | }; 94 | const options = () => { 95 | return { 96 | plugins: { 97 | datalabels: { 98 | display: false, 99 | color: 'blue', 100 | }, 101 | legend: { 102 | position: 'bottom', 103 | align: 'start', 104 | }, 105 | }, 106 | 107 | animation: { 108 | duration: 1000, 109 | }, 110 | tooltips: { 111 | mode: 'index', 112 | intersect: false, 113 | callbacks: { 114 | label: (tooltipItem, data) => { 115 | var label = data.datasets[tooltipItem.datasetIndex].label; 116 | return label + ': ' + formatNumber(parseInt(tooltipItem.value), false); 117 | }, 118 | title: (tooltipItem, data) => { 119 | return 'Dia ' + tooltipItem[0].label; 120 | }, 121 | }, 122 | }, 123 | scales: { 124 | y: { 125 | stacked: true, 126 | ticks: { 127 | maxTicksLimit: window.innerWidth <= RESIZE_TRESHOLD ? 8 : 10, 128 | minTicksLimit: window.innerWidth <= RESIZE_TRESHOLD ? 8 : 10, 129 | callback: function (value, index, values) { 130 | return formatNumber(value, false); 131 | }, 132 | }, 133 | }, 134 | 135 | x: { 136 | stacked: true, 137 | ticks: { 138 | beginAtZero: true, 139 | }, 140 | }, 141 | }, 142 | }; 143 | }; 144 | 145 | useEffect(() => { 146 | if (values.length > 0) { 147 | setLoading(false); 148 | } 149 | }, [values, labels]); 150 | 151 | return ( 152 | 153 |
{!loading ? : ''}
154 |
155 | ); 156 | } 157 | -------------------------------------------------------------------------------- /components/graphs/LineVacinadosEu.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | import { lineChartCommon, RESIZE_TRESHOLD } from '../../constants'; 4 | import { formatNumber } from '../../utils'; 5 | import { Card } from './../Card'; 6 | import classNames from 'classnames'; 7 | import { CustomCheckbox } from '../CustomCheckbox'; 8 | import styles from './../Card.module.scss'; 9 | import { useCanvasResizer } from '../../hooks/useCanvasResizer'; 10 | 11 | export function LineVacinadosEu({ statistics, colors }) { 12 | const [owidData, setOwidData] = useState({ labels: '', pt: '', eu: '' }); 13 | const [loaded, setLoaded] = useState(loaded); 14 | let { main, complements } = colors; 15 | 16 | let [activeDose, setActiveDose] = useState(0); 17 | let doses_map = { 18 | normal: ['total_vaccinations', 'people_vaccinated', 'people_fully_vaccinated'], 19 | per_hundred: ['total_vaccinations_per_hundred', 'people_vaccinated_per_hundred', 'people_fully_vaccinated_per_hundred'], 20 | }; 21 | let [toggleStats, setToggleStats] = useState({ 22 | perHundred: true, 23 | }); 24 | 25 | let { setCanvasNode } = useCanvasResizer(); 26 | 27 | const data = (canvas) => { 28 | setCanvasNode(canvas.parentNode); 29 | 30 | return { 31 | labels: owidData.labels, 32 | datasets: [ 33 | { 34 | ...lineChartCommon, 35 | label: 'Portugal', 36 | backgroundColor: main, 37 | borderColor: main, 38 | type: 'line', 39 | fill: false, 40 | data: owidData.pt.map((el) => { 41 | if (toggleStats.perHundred) { 42 | return el[doses_map.per_hundred[activeDose]]; 43 | } 44 | return el[doses_map.normal[activeDose]]; 45 | }), 46 | }, 47 | { 48 | ...lineChartCommon, 49 | label: 'União Europeia', 50 | type: 'line', 51 | fill: false, 52 | backgroundColor: complements[2], 53 | borderColor: complements[2], 54 | data: owidData.eu.map((el) => { 55 | if (toggleStats.perHundred) { 56 | return el[doses_map.per_hundred[activeDose]]; 57 | } 58 | return el[doses_map.normal[activeDose]]; 59 | }), 60 | }, 61 | ], 62 | }; 63 | }; 64 | const options = () => { 65 | return { 66 | plugins: { 67 | datalabels: { 68 | display: false, 69 | }, 70 | legend: { 71 | position: 'bottom', 72 | align: 'start', 73 | }, 74 | }, 75 | 76 | animation: { 77 | duration: 1000, 78 | }, 79 | tooltips: { 80 | mode: 'index', 81 | intersect: true, 82 | callbacks: { 83 | title: (tooltipItem, data) => { 84 | return 'Dia ' + tooltipItem[0].label; 85 | }, 86 | }, 87 | }, 88 | scales: { 89 | yAxes: [ 90 | { 91 | ticks: { 92 | beginAtZero: false, 93 | maxTicksLimit: window.innerWidth <= RESIZE_TRESHOLD ? 8 : 10, 94 | minTicksLimit: window.innerWidth <= RESIZE_TRESHOLD ? 8 : 10, 95 | callback: (value) => formatNumber(value, false), 96 | }, 97 | }, 98 | ], 99 | xAxes: [ 100 | { 101 | ticks: { 102 | maxTicksLimit: window.innerWidth <= RESIZE_TRESHOLD ? 30 : 60, 103 | minTicksLimit: window.innerWidth <= RESIZE_TRESHOLD ? 30 : 60, 104 | }, 105 | }, 106 | ], 107 | }, 108 | }; 109 | }; 110 | 111 | useEffect(async () => { 112 | let { labels, pt, eu } = await statistics.getOwid(); 113 | setOwidData({ labels, pt, eu }); 114 | setLoaded(true); 115 | }, []); 116 | 117 | return loaded === true ? ( 118 | 119 |
120 |
121 |

122 | 132 | 142 | 152 |

153 |
154 | { 159 | setToggleStats({ 160 | perHundred: checked, 161 | }); 162 | }} 163 | /> 164 |
165 | 166 |
167 | 168 |
169 |
170 | ) : ( 171 | <> 172 | ); 173 | } 174 | -------------------------------------------------------------------------------- /components/graphs/BarVacinadosEu.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | import { RESIZE_TRESHOLD } from '../../constants'; 4 | import { formatNumber } from '../../utils'; 5 | import { Card } from './../Card'; 6 | import classNames from 'classnames'; 7 | import { CustomCheckbox } from '../CustomCheckbox'; 8 | import styles from './../Card.module.scss'; 9 | import { useCanvasResizer } from '../../hooks/useCanvasResizer'; 10 | 11 | export function BarVacinadosEu({ statistics, colors }) { 12 | const [owidData, setOwidData] = useState({ labels: '', pt: '', eu: '' }); 13 | const [loaded, setLoaded] = useState(loaded); 14 | let { main, complements } = colors; 15 | let { setCanvasNode } = useCanvasResizer(); 16 | 17 | let [activeDose, setActiveDose] = useState(0); 18 | let doses_map = { 19 | normal: ['total_vaccinations', 'new_1_doses', 'new_2_doses'], 20 | per_hundred: ['total_vaccinations_per_hundred', 'new_1_doses_per_hundred', 'new_2_doses_per_hundred'], 21 | }; 22 | let [toggleStats, setToggleStats] = useState({ 23 | perHundred: true, 24 | }); 25 | const canvasRef = useRef(null); 26 | 27 | const data = (canvas) => { 28 | setCanvasNode(canvas.parentNode); 29 | 30 | let lineChartCommon = { 31 | lineTension: 0.0, 32 | lineBorder: 0, 33 | borderWidth: 0, 34 | borderJoinStyle: 'miter', 35 | pointBorderWidth: 1, 36 | pointHoverRadius: 3, 37 | pointHoverBorderWidth: 1, 38 | pointRadius: 3, 39 | pointHitRadius: 5, 40 | }; 41 | return { 42 | labels: owidData.labels, 43 | datasets: [ 44 | { 45 | ...lineChartCommon, 46 | label: 'Portugal', 47 | backgroundColor: main, 48 | borderColor: main, 49 | fill: false, 50 | lineTension: 0, 51 | 52 | data: owidData.pt.map((el) => { 53 | if (toggleStats.perHundred) { 54 | return el[doses_map.per_hundred[activeDose]]; 55 | } 56 | return el[doses_map.normal[activeDose]]; 57 | }), 58 | }, 59 | { 60 | ...lineChartCommon, 61 | label: 'União Europeia', 62 | fill: false, 63 | lineTension: 0, 64 | backgroundColor: complements[2], 65 | borderColor: complements[2], 66 | data: owidData.eu.map((el) => { 67 | if (toggleStats.perHundred) { 68 | return el[doses_map.per_hundred[activeDose]]; 69 | } 70 | return el[doses_map.normal[activeDose]]; 71 | }), 72 | }, 73 | ], 74 | }; 75 | }; 76 | const options = () => { 77 | return { 78 | plugins: { 79 | datalabels: { 80 | display: false, 81 | }, 82 | legend: { 83 | position: 'bottom', 84 | align: 'start', 85 | }, 86 | }, 87 | bezierCurve: false, 88 | lineTension: 0, 89 | animation: { 90 | duration: 1000, 91 | }, 92 | tooltips: { 93 | mode: 'index', 94 | intersect: true, 95 | callbacks: { 96 | title: (tooltipItem, data) => { 97 | return 'Dia ' + tooltipItem[0].label; 98 | }, 99 | label: (tooltipItem, data) => { 100 | var label = data.datasets[tooltipItem.datasetIndex].label; 101 | return label + ': ' + parseFloat(tooltipItem.value).toFixed(2); 102 | }, 103 | }, 104 | }, 105 | scales: { 106 | yAxes: [ 107 | { 108 | ticks: { 109 | beginAtZero: true, 110 | maxTicksLimit: window.innerWidth <= RESIZE_TRESHOLD ? 8 : 10, 111 | minTicksLimit: window.innerWidth <= RESIZE_TRESHOLD ? 8 : 10, 112 | callback: (value) => formatNumber(value, false), 113 | }, 114 | }, 115 | ], 116 | xAxes: [ 117 | { 118 | ticks: { 119 | maxTicksLimit: window.innerWidth <= RESIZE_TRESHOLD ? 30 : 60, 120 | minTicksLimit: window.innerWidth <= RESIZE_TRESHOLD ? 30 : 60, 121 | }, 122 | }, 123 | ], 124 | }, 125 | }; 126 | }; 127 | 128 | useEffect(async () => { 129 | let { labels, pt, eu } = await statistics.getOwid(); 130 | setOwidData({ labels, pt, eu }); 131 | setLoaded(true); 132 | }, []); 133 | 134 | return loaded === true ? ( 135 | 136 |
137 |
138 |

139 | 149 | 159 | 169 |

170 |
171 | { 176 | setToggleStats({ 177 | perHundred: checked, 178 | }); 179 | }} 180 | /> 181 |
182 | 183 |
184 | 185 |
186 |
187 | ) : ( 188 | '' 189 | ); 190 | } 191 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | export let FOREGROUND_COLOR = '#01AE97'; 2 | export let COLOR_1 = '#017a6a'; 3 | export let COLOR_2 = '#01574c'; 4 | export let COLOR_3 = '#00342d'; 5 | export let COLOR_4 = '#80d7cb'; 6 | 7 | export let COLOR_5 = '#AD1413'; 8 | export let COLOR_6 = '#0A6CAD'; 9 | export let COLOR_7 = '#AD6E13'; 10 | 11 | // Tints 12 | export let TINT_70 = '#b3e7e0'; 13 | export let TINT_50 = '#80d7cb'; 14 | export let TINT_30 = '#4dc6b6'; 15 | 16 | export let COLOR = '#01AE97'; 17 | 18 | // Shades 19 | export let SHADE_30 = '#017a6a'; 20 | export let SHADE_50 = '#01574c'; 21 | export let SHADE_70 = '#00342d'; 22 | 23 | // Square Complement 24 | export let COMPLEMENT_1 = '#0A9DD1'; 25 | export let COMPLEMENT_2 = '#D11541'; 26 | export let COMPLEMENT_3 = '#D17615'; 27 | 28 | export let RESIZE_TRESHOLD = 1040; 29 | 30 | export let lineChartCommon = { 31 | fill: true, 32 | lineTension: 0.5, 33 | lineBorder: 1, 34 | borderWidth: 3, 35 | borderJoinStyle: 'miter', 36 | pointBorderWidth: 1, 37 | pointHoverRadius: 3, 38 | pointHoverBorderWidth: 2, 39 | pointRadius: 1, 40 | pointHitRadius: 10, 41 | }; 42 | export let lineChartCommon2 = { 43 | fill: false, 44 | lineTension: 0.5, 45 | lineBorder: 1, 46 | borderWidth: 3, 47 | borderJoinStyle: 'miter', 48 | pointBorderWidth: 0, 49 | pointHoverRadius: 0, 50 | pointHoverBorderWidth: 0, 51 | pointRadius: 0, 52 | pointHitRadius: 0, 53 | }; 54 | 55 | export const REGIOES = { 56 | MADEIRA: 'madeira', 57 | PORTUGAL: 'portugal', 58 | ACORES: 'ACORES', 59 | }; 60 | 61 | export const ECDC_MAPPING = { 62 | alentejo: 'PTCSR01', 63 | algarve: 'PTCSR02', 64 | acores: 'PTCSR03', 65 | centro: 'PTCSR04', 66 | lisboa: 'PTCSR05', 67 | madeira: 'PTCSR06', 68 | norte: 'PTCSR07', 69 | portugal: 'PT', 70 | 71 | PTCSR01: 'alentejo', 72 | PTCSR02: 'algarve', 73 | PTCSR03: 'acores', 74 | PTCSR04: 'centro', 75 | PTCSR05: 'lisboa', 76 | PTCSR06: 'madeira', 77 | PTCSR07: 'norte', 78 | PT: 'portugal', 79 | }; 80 | 81 | export const MADEIRA_DICOS = { 82 | 3101: 'calheta', 83 | 3102: 'camara_lobos', 84 | 3108: 'santa_cruz', 85 | 3201: 'porto_santo', 86 | 3106: 'porto_moniz', 87 | 3110: 'svincente', 88 | 3109: 'santana', 89 | 3105: 'ponta_sol', 90 | 3103: 'funchal', 91 | 3104: 'machico', 92 | 3107: 'ribeira_brava', 93 | 94 | calheta: '3101', 95 | camara_lobos: '3102', 96 | santa_cruz: '3108', 97 | porto_santo: '3201', 98 | porto_moniz: '3106', 99 | svincente: '3110', 100 | santana: '3109', 101 | ponta_sol: '3105', 102 | funchal: '3103', 103 | machico: '3104', 104 | ribeira_brava: '3107', 105 | }; 106 | 107 | export const ACORES_DICOS = { 108 | 49: 'corvo', 109 | 48: 'flores', 110 | 47: 'faial', 111 | 46: 'pico', 112 | 45: 'sao_jorge', 113 | 44: 'graciosa', 114 | 43: 'terceira', 115 | 42: 'sao_miguel', 116 | 41: 'santa_maria', 117 | 118 | corvo: 49, 119 | flores: 48, 120 | faial: 47, 121 | pico: 46, 122 | sao_jorge: 45, 123 | graciosa: 44, 124 | terceira: 43, 125 | sao_miguel: 42, 126 | santa_maria: 41, 127 | }; 128 | 129 | export const ACORES_DICOS_CONCELHOS = { 130 | 4901: 'corvo', 131 | 4802: 'flores', 132 | 4801: 'flores', 133 | 4701: 'faial', 134 | 4602: 'pico', 135 | 4603: 'pico', 136 | 4601: 'pico', 137 | 4501: 'sao_jorge', 138 | 4502: 'sao_jorge', 139 | 4401: 'graciosa', 140 | 4301: 'terceira', 141 | 4302: 'terceira', 142 | 4201: 'sao_miguel', 143 | 4202: 'sao_miguel', 144 | 4203: 'sao_miguel', 145 | 4204: 'sao_miguel', 146 | 4205: 'sao_miguel', 147 | 4206: 'sao_miguel', 148 | 4101: 'santa_maria', 149 | }; 150 | 151 | export const ARS_MAPPING = { 152 | alentejo: 'ARS Alentejo', 153 | algarve: 'ARS Algarve', 154 | lvt: 'ARS Lisboa e Vale do Tejo', 155 | norte: 'ARS Norte', 156 | centro: 'ARS Centro', 157 | 158 | 'ARS Alentejo': 'alentejo', 159 | 'ARS Algarve': 'algarve', 160 | 'ARS Lisboa e Vale do Tejo': 'lvt', 161 | 'ARS Norte': 'norte', 162 | 'ARS Centro': 'centro', 163 | }; 164 | 165 | export const grades = [0, 20, 40, 60, 80]; 166 | export const grades_pretty = { 167 | 0: '0% a 19%', 168 | 20: '20% a 39%', 169 | 40: '40% a 59%', 170 | 60: '60% a 89%', 171 | 80: '80% a 100%', 172 | }; 173 | 174 | export const SNS_WEEKS = { 175 | '04-01-2021': '27/12 a 10/01', 176 | '11-01-2021': '11/12 a 17/01', 177 | '18-01-2021': '18/01 a 24/01', 178 | '25-01-2021': '25/01 a 31/01', 179 | '01-02-2021': '01/02 a 07/02', 180 | '08-02-2021': '08/02 a 14/02', 181 | '15-02-2021': '15/02 a 21/02', 182 | '22-02-2021': '22/02 a 28/02', 183 | '01-03-2021': '01/03 a 07/03', 184 | '08-03-2021': '08/03 a 14/03', 185 | '15-03-2021': '15/03 a 21/03', 186 | '22-03-2021': '22/03 a 28/03', 187 | '29-03-2021': '29/03 a 04/04', 188 | '05-04-2021': '05/04 a 11/04', 189 | '12-04-2021': '12/04 a 18/04', 190 | '19-04-2021': '19/04 a 25/04', 191 | '26-04-2021': '26/04 a 02/05', 192 | '03-05-2021': '03/05 a 09/05', 193 | '10-05-2021': '10/05 a 16/05', 194 | '17-05-2021': '17/05 a 23/05', 195 | '24-05-2021': '24/05 a 28/05', 196 | '31-05-2021': '31/05 a 06/06', 197 | '07-06-2021': '07/06 a 13/06', 198 | '07-06-2021': '07/06 a 13/06', 199 | '14-06-2021': '14/06 a 20/06', 200 | '21-06-2021': '21/06 a 27/06', 201 | '28-06-2021': '28/06 a 04/07', 202 | '05-07-2021': '05/07 a 11/07', 203 | '12-07-2021': '12/07 a 16/07', 204 | '19-07-2021': '19/07 a 25/07', 205 | '26-07-2021': '26/07 a 01/08', 206 | '02-08-2021': '02/08 a 08/08', 207 | '09-08-2021': '09/08 a 15/08', 208 | '16-08-2021': '16/08 a 22/08', 209 | '23-08-2021': '23/08 a 29/08', 210 | '30-08-2021': '30/08 a 05/09', 211 | '06-09-2021': '06/09 a 12/09', 212 | '13-09-2021': '13/09 a 19/09', 213 | '20-09-2021': '20/09 a 26/09', 214 | '27-09-2021': '27/09 a 03/10', 215 | }; 216 | -------------------------------------------------------------------------------- /automation/owid_parser.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from dateutil.parser import parse 4 | from dateutil.relativedelta import * 5 | from datetime import datetime 6 | 7 | start_date = parse("2020-12-27") 8 | start_date2 = parse("2020-12-28") 9 | # a = relativedelta(parse("2020-12-20"), parse("2020-12-21")) 10 | # dt = parse("2020-01-27") 11 | # print(a.days > 0) 12 | 13 | 14 | url = 'https://covid.ourworldindata.org/data/owid-covid-data.json' 15 | 16 | res = requests.get(url) 17 | json_res = res.json() 18 | data = { 19 | 'prt': False, 20 | 'eun': False 21 | } 22 | 23 | 24 | def filter_data(arr, populacao): 25 | filtered_arr = [] 26 | for idx, k in enumerate(arr): 27 | date = parse(k['date']) 28 | if(date >= start_date): 29 | k['new_1_doses'] = None 30 | k['new_2_doses'] = None 31 | k['new_1_doses_per_hundred'] = None 32 | k['new_2_doses_per_hundred'] = None 33 | 34 | if(('total_vaccinations' in k) == False): 35 | k['total_vaccinations'] = None 36 | print(k['total_vaccinations']) 37 | 38 | if(('people_vaccinated' in k) == False): 39 | k['people_vaccinated'] = None 40 | 41 | if(('total_vaccinations_per_hundred' in k) == False): 42 | k['total_vaccinations_per_hundred'] = None 43 | 44 | if(('people_vaccinated_per_hundred' in k) == False): 45 | k['people_vaccinated_per_hundred'] = None 46 | 47 | if(('people_fully_vaccinated' in k) == False): 48 | k['people_fully_vaccinated'] = None 49 | 50 | if(('new_vaccinations' in k) == False): 51 | k['new_vaccinations'] = None 52 | 53 | if(('people_vaccinated_per_hundred' in k) == False): 54 | k['people_vaccinated_per_hundred'] = None 55 | 56 | if(('new_vaccinations' in k) == False): 57 | k['new_vaccinations'] = None 58 | 59 | if(('new_vaccinations_smoothed' in k) == False): 60 | k['new_vaccinations_smoothed'] = None 61 | 62 | if(('people_fully_vaccinated_per_hundred' in k) == False): 63 | k['people_fully_vaccinated_per_hundred'] = None 64 | 65 | if(('new_vaccinations_smoothed_per_million' in k) == False): 66 | k['new_vaccinations_smoothed_per_million'] = None 67 | 68 | if(date >= start_date2): 69 | previous_item = arr[idx-1] 70 | if(k['people_vaccinated'] != None and previous_item['people_vaccinated'] != None): 71 | k['new_1_doses'] = (k['people_vaccinated'] if k['people_vaccinated'] 72 | != None else 0) - (previous_item['people_vaccinated'] if previous_item['people_vaccinated'] 73 | != None else 0) 74 | 75 | if(k['people_fully_vaccinated'] != None and previous_item['people_vaccinated'] != None): 76 | k['new_2_doses'] = (k['people_fully_vaccinated'] if k['people_fully_vaccinated'] 77 | != None else 0) - (previous_item['people_fully_vaccinated'] if previous_item['people_fully_vaccinated'] 78 | != None else 0) 79 | 80 | if(k['new_1_doses'] != None and k['new_1_doses'] > 0): 81 | k['new_1_doses_per_hundred'] = ( 82 | k['new_1_doses'] / populacao) * 100 83 | 84 | if(k['new_2_doses'] != None and k['new_2_doses'] > 0): 85 | k['new_2_doses_per_hundred'] = ( 86 | k['new_2_doses'] / populacao) * 100 87 | 88 | item = { 89 | "date": k['date'], 90 | "total_vaccinations": k['total_vaccinations'], 91 | "people_vaccinated": k['people_vaccinated'], 92 | "people_vaccinated_per_hundred": k['people_vaccinated_per_hundred'], 93 | "total_vaccinations_per_hundred": k['total_vaccinations_per_hundred'], 94 | 95 | "people_fully_vaccinated": k['people_fully_vaccinated'], 96 | "new_vaccinations": k['new_vaccinations'], 97 | "new_vaccinations_smoothed": k['new_vaccinations_smoothed'], 98 | "people_fully_vaccinated_per_hundred": k['people_fully_vaccinated_per_hundred'], 99 | "new_vaccinations_smoothed_per_million": k['new_vaccinations_smoothed_per_million'], 100 | 101 | 'new_1_doses': k['new_1_doses'], 102 | 'new_2_doses': k['new_2_doses'], 103 | 'new_1_doses_per_hundred': k['new_1_doses_per_hundred'], 104 | 'new_2_doses_per_hundred': k['new_2_doses_per_hundred'] 105 | } 106 | filtered_arr.append(item) 107 | return filtered_arr 108 | 109 | 110 | for k in json_res: 111 | if k == 'PRT': 112 | json_res[k]['data'] = filter_data( 113 | json_res[k]['data'], json_res[k]['population']) 114 | data['prt'] = json_res[k] 115 | 116 | if k == 'OWID_EUN': 117 | json_res[k]['data'] = filter_data( 118 | json_res[k]['data'], json_res[k]['population']) 119 | data['eun'] = json_res[k] 120 | 121 | 122 | now = datetime.now() 123 | json_file = open('./data/last-update.json', 'r+') 124 | json_datas = json.load(json_file) 125 | json_datas['dateOwid'] = now.strftime('%Y-%m-%d') 126 | json_file.seek(0) 127 | json_file.write(json.dumps(json_datas)) 128 | json_file.close() 129 | 130 | owid_file = open("./data/owid_filter.json", "w") 131 | owid_file.write(json.dumps(data)) 132 | owid_file.close() 133 | -------------------------------------------------------------------------------- /styles/globals.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;700;800&display=swap'); 2 | @import 'colors.scss'; 3 | 4 | :root { 5 | --foreground: $main; 6 | --shade: $shade; 7 | --tint: $tint; 8 | } 9 | 10 | html, 11 | body { 12 | padding: 0; 13 | margin: 0; 14 | /* background-color: #FBFBFB; */ 15 | } 16 | 17 | h2 { 18 | //font-weight: 300 !important; 19 | font-size: 13px !important; 20 | } 21 | .scrollable { 22 | overflow-x: scroll; 23 | } 24 | a { 25 | color: inherit; 26 | text-decoration: none; 27 | } 28 | 29 | * { 30 | box-sizing: border-box; 31 | font-family: 'Open Sans', sans-serif; 32 | } 33 | 34 | hr { 35 | margin-bottom: 6px; 36 | margin-top: 0px; 37 | border-top: 1px solid rgba(0, 0, 0, 0.1); 38 | } 39 | .row.counterRow { 40 | margin-bottom: 30px; 41 | } 42 | .counterRow .card-shadow { 43 | // border: 1px solid rgba(0, 0, 0, 0.1); 44 | /* box-shadow: 0px 0px 5px 0px #0000000d; */ 45 | 46 | padding-right: 8px !important; 47 | padding-left: 8px !important; 48 | border-right: 1px solid #eeeeee; 49 | border-radius: 0px; 50 | } 51 | 52 | .card-shadow-bottom { 53 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 54 | /* box-shadow: 0px 0px 5px 0px #0000000d; */ 55 | } 56 | 57 | a:hover { 58 | text-decoration: none; 59 | color: $main; 60 | } 61 | 62 | .mouse-pointer { 63 | cursor: pointer; 64 | } 65 | 66 | button.toggle_button { 67 | /* color: $tint; */ 68 | border: none; 69 | background-color: white; 70 | /* border-bottom: 1px solid $tint; */ 71 | border-bottom: 1px solid #eee; 72 | padding: 5px 15px; 73 | -moz-transition: all 200ms; 74 | transition: all 200ms; 75 | font-size: 13px; 76 | } 77 | 78 | .toggle_button.active { 79 | border-bottom-color: $main; 80 | /* color: $main; */ 81 | } 82 | 83 | .toggle_button:hover { 84 | border-bottom-color: $shade; 85 | color: $shade; 86 | } 87 | 88 | .toggle_buttons { 89 | text-align: left; 90 | display: inline-block; 91 | margin-bottom: 20px; 92 | 93 | p { 94 | margin-bottom: 0px; 95 | } 96 | } 97 | 98 | .subchart-data p { 99 | font-weight: bold; 100 | font-size: 13px; 101 | text-align: left; 102 | } 103 | 104 | .legends { 105 | font-size: 12px; 106 | text-align: left; 107 | } 108 | 109 | .legend { 110 | margin-right: 10px; 111 | } 112 | 113 | span.color_sample { 114 | width: 40px; 115 | height: 13px; 116 | display: inline-block; 117 | vertical-align: middle; 118 | margin-right: 5px; 119 | box-sizing: border-box; 120 | } 121 | 122 | .vaccine-label { 123 | text-align: right; 124 | font-size: 12px; 125 | line-height: 55px; 126 | } 127 | 128 | sup.new { 129 | text-transform: uppercase; 130 | font-weight: 900; 131 | color: $main; 132 | font-size: 10px; 133 | } 134 | 135 | .hide-except-seo { 136 | width: 0; 137 | height: 0; 138 | overflow: hidden; 139 | padding: 0; 140 | margin: 0; 141 | } 142 | 143 | .row { 144 | margin-right: 0; 145 | margin-left: 0; 146 | } 147 | 148 | [class*='col-'] { 149 | padding: 0; 150 | } 151 | 152 | .leaflet-container { 153 | background-color: #62c3ff; 154 | border-radius: 5px; 155 | z-index: -1; 156 | } 157 | 158 | .leaflet-control-attribution { 159 | display: none; 160 | } 161 | 162 | .info { 163 | padding: 6px 8px; 164 | font-size: 14px; 165 | background: white; 166 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); 167 | border-radius: 2px; 168 | } 169 | .info h4 { 170 | margin: 0 0 5px; 171 | color: #777; 172 | } 173 | 174 | .legend { 175 | line-height: 20px; 176 | color: #555; 177 | p { 178 | font-size: 12px; 179 | margin-bottom: 0px; 180 | line-height: 25px; 181 | text-align: left; 182 | } 183 | i { 184 | width: 25px; 185 | height: 25px; 186 | display: inline-block; 187 | vertical-align: middle; 188 | margin-right: 6px; 189 | margin-bottom: -3px; 190 | } 191 | } 192 | 193 | .leaflet-popup-content-wrapper { 194 | padding: 1px; 195 | text-align: left; 196 | border-radius: 5px; 197 | } 198 | 199 | .leaflet-popup-content { 200 | margin: 0px; 201 | padding: 8px; 202 | line-height: 1.6; 203 | } 204 | 205 | .leaflet-popup-content p { 206 | margin: 0px 0; 207 | } 208 | 209 | .col-lg-6:last-child h2, 210 | .col-lg-6:last-child h3 { 211 | padding: 0px; 212 | } 213 | 214 | @media (min-width: 1090px) { 215 | .hide_mobile { 216 | display: inline-block; 217 | } 218 | } 219 | 220 | @media (max-width: 1090px) { 221 | .hide_mobile { 222 | display: none !important; 223 | } 224 | } 225 | 226 | .show_mobile { 227 | display: none !important; 228 | } 229 | 230 | /* Extra small devices (portrait phones, less than 576px) */ 231 | /* No media query for `xs` since this is the default in Bootstrap */ 232 | #vacin1d, 233 | #vacin2d { 234 | background-color: #eff7f6; 235 | order: 2; 236 | } 237 | #vacinfase { 238 | background-color: white; 239 | order: 1; 240 | } 241 | /* Small devices (landscape phones, 576px and up) */ 242 | 243 | @media (min-width: 576px) { 244 | .container { 245 | max-width: 95vw; 246 | } 247 | } 248 | 249 | /* Medium devices (tablets, 768px and up) */ 250 | @media (min-width: 768px) { 251 | .container { 252 | max-width: 90vw; 253 | } 254 | } 255 | 256 | /* Medium devices (tablets, 768px and up) */ 257 | @media (max-width: 1090px) { 258 | .counterRow .col-12 div { 259 | padding-right: 0px; 260 | border: 0; 261 | } 262 | } 263 | /* Large devices (desktops, 992px and up) */ 264 | @media (min-width: 992px) { 265 | .scrollable { 266 | overflow-x: hidden; 267 | } 268 | .container { 269 | max-width: 90vw; 270 | } 271 | 272 | .row { 273 | margin-bottom: 15px; 274 | } 275 | 276 | .counterRow .col-lg-4:last-child div { 277 | padding-right: 0px; 278 | border: 0; 279 | } 280 | 281 | #vacinfase { 282 | } 283 | 284 | #vacin1d, 285 | #vacin2d { 286 | background-color: #eff7f6; 287 | order: 1; 288 | } 289 | #vacinfase { 290 | background-color: #eff7f6; 291 | order: 2; 292 | } 293 | } 294 | 295 | /* Extra large devices (large desktops, 1200px and up)*/ 296 | @media (min-width: 1200px) { 297 | .container { 298 | max-width: 90vw; 299 | } 300 | } 301 | 302 | /* Extra large devices (large desktops, 1200px and up)*/ 303 | @media (max-width: 900px) { 304 | body { 305 | padding-bottom: 95px; 306 | } 307 | } 308 | 309 | @media (max-width: 424px) { 310 | .container { 311 | max-width: 1520px; 312 | } 313 | 314 | .hide_micro_mobile { 315 | display: none; 316 | } 317 | 318 | .show_micro_mobile { 319 | display: initial; 320 | } 321 | } 322 | 323 | @media (min-width: 425px) { 324 | .container { 325 | max-width: 1520px; 326 | } 327 | 328 | .hide_micro_mobile { 329 | display: initial; 330 | } 331 | 332 | .show_micro_mobile { 333 | display: none; 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /components/graphs/RaaMapa.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Col, Row } from 'react-bootstrap'; 3 | import { Bar } from 'react-chartjs-2'; 4 | import { ACORES_DICOS, grades, grades_pretty, RESIZE_TRESHOLD } from '../../constants'; 5 | import { formatNumber, getColor } from '../../utils'; 6 | import { Card } from '../Card'; 7 | import { populacao_residente_raa } from './../../data/generic.json'; 8 | import cardStyles from './../Card.module.scss'; 9 | 10 | export function RaaMapa({ statistics, colors }) { 11 | let [graphData, setGraphData] = useState(); 12 | let [loaded, setLoaded] = useState(false); 13 | let { main, shades } = colors; 14 | 15 | const renderMap = async (map) => { 16 | const madeira = await fetch('/acores.geojson').then((r) => r.json()); 17 | const madeiraMapa = L.map('map', { 18 | doubleClickZoom: false, 19 | closePopupOnClick: false, 20 | dragging: false, 21 | zoomSnap: false, 22 | zoomDelta: false, 23 | trackResize: false, 24 | touchZoom: false, 25 | scrollWheelZoom: false, 26 | zoomControl: false, 27 | draggable: false, 28 | }); 29 | let layers = L.geoJSON(madeira, { 30 | onEachFeature: (feature, shape) => { 31 | let concelho = ACORES_DICOS[feature.properties.DicoShort]; 32 | let data = graphData.concelhos[concelho]; 33 | 34 | let percentagem_1 = (data.dose_1 / populacao_residente_raa[feature.properties.DicoShort].valor) * 100; 35 | let percentagem_2 = (data.dose_2 / populacao_residente_raa[feature.properties.DicoShort].valor) * 100; 36 | 37 | shape.bindPopup( 38 | `

39 | ${feature.properties.ILHA} 40 |
1ª Dose: ${formatNumber(data.dose_1)} (${percentagem_1.toFixed(2)}%) 41 |
2ª Dose: ${formatNumber(data.dose_2)} (${percentagem_2.toFixed(2)}%) 42 |

` 43 | ); 44 | shape.on('click', () => { 45 | //console.log('click'); 46 | }); 47 | }, 48 | 49 | style: function (feature) { 50 | let concelho = ACORES_DICOS[feature.properties.DicoShort]; 51 | let data = graphData.concelhos[concelho]; 52 | 53 | let percentagem = (data.dose_2 / populacao_residente_raa[feature.properties.DicoShort].valor) * 100; 54 | 55 | return { fillOpacity: 1, fillColor: getColor(percentagem), lineJoin: 'round', stroke: true, weight: 2, color: '#018b79' }; 56 | }, 57 | }).addTo(madeiraMapa); 58 | 59 | layers.eachLayer(function (layer) { 60 | layer.feature.properties.layerID = layer.feature.properties.DICOFRE; 61 | }); 62 | 63 | madeiraMapa.fitBounds(layers.getBounds()); 64 | madeiraMapa.setZoom(7.8); 65 | 66 | //Create legend 67 | var legend = L.control({ position: 'bottomleft' }); 68 | 69 | legend.onAdd = function (map) { 70 | var div = L.DomUtil.create('div', 'info legend'); 71 | 72 | for (var i = 0; i < grades.length; i++) { 73 | let grade = grades[i]; 74 | let grade_pretty = grades_pretty[grade]; 75 | div.innerHTML += `

76 | ${grade_pretty}

`; 77 | } 78 | 79 | return div; 80 | }; 81 | 82 | legend.addTo(madeiraMapa); 83 | }; 84 | 85 | function renderGraph(el) { 86 | const data = () => { 87 | const chartData = { 88 | labels: [''], 89 | datasets: [ 90 | { 91 | label: 'Total de vacinas administradas - 1ª Dose', 92 | borderColor: main, 93 | backgroundColor: main, 94 | stack: 'stack0', 95 | order: 2, 96 | data_actual: el.dose_1, 97 | data: [el.dose_1 - el.dose_2], 98 | }, 99 | { 100 | label: 'Total de vacinas administradas - 2ª Dose', 101 | borderColor: shades[0], 102 | backgroundColor: shades[0], 103 | data_actual: el.dose_2, 104 | data: [el.dose_2], 105 | stack: 'stack0', 106 | order: 1, 107 | }, 108 | ], 109 | }; 110 | 111 | return chartData; 112 | }; 113 | 114 | const options = () => { 115 | let dico = ACORES_DICOS[el.chave]; 116 | let populacao_residente = populacao_residente_raa[dico].valor; 117 | return { 118 | indexAxis: 'y', 119 | plugins: { 120 | tooltip: { 121 | mode: 'index', 122 | intersect: true, 123 | callbacks: { 124 | label: (tooltipItem, b) => { 125 | let data = tooltipItem.dataset.data_actual; 126 | return `${tooltipItem.dataset.label}: ${formatNumber(data, false)}`; 127 | }, 128 | }, 129 | }, 130 | datalabels: { 131 | display: false, 132 | }, 133 | legend: { 134 | position: 'bottom', 135 | align: 'start', 136 | display: false, 137 | }, 138 | }, 139 | layout: { padding: { left: -12 } }, 140 | 141 | animation: { 142 | duration: 1000, 143 | }, 144 | tooltips: { 145 | mode: 'index', 146 | intersect: false, 147 | 148 | callbacks: { 149 | title: (tooltipItem, data) => { 150 | return ''; 151 | }, 152 | }, 153 | }, 154 | scales: { 155 | y: { 156 | stacked: true, 157 | ticks: { 158 | beginAtZero: true, 159 | }, 160 | }, 161 | 162 | x: { 163 | stacked: true, 164 | ticks: { 165 | beginAtZero: true, 166 | stepSize: Math.round(populacao_residente / 5), 167 | callback: (value) => formatNumber(value, false), 168 | }, 169 | max: populacao_residente, 170 | }, 171 | }, 172 | }; 173 | }; 174 | 175 | return ( 176 | 177 |
178 |

{el.nome}

179 | 180 |
181 | 182 | ); 183 | } 184 | 185 | useEffect(async () => { 186 | statistics.getArquipelagoData().then((data) => { 187 | setGraphData(data[data.length - 1]); 188 | 189 | if (loaded === false) { 190 | setLoaded(true); 191 | } 192 | 193 | if (loaded === true) { 194 | renderMap(); 195 | } 196 | }); 197 | }, [loaded]); 198 | return loaded === true ? ( 199 | 200 | 201 | 202 |
203 | 204 |
205 | {Object.values(graphData.concelhos).map(renderGraph)} 206 | 207 |
208 |

209 | 210 | 1ª Dose 211 | 212 | 213 | 2ª Dose 214 | 215 |

216 |
217 |
218 |
219 | ) : ( 220 | '' 221 | ); 222 | } 223 | -------------------------------------------------------------------------------- /components/graphs/RamMapa.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Col, Row } from 'react-bootstrap'; 3 | import { Bar } from 'react-chartjs-2'; 4 | import { grades, grades_pretty, MADEIRA_DICOS, RESIZE_TRESHOLD } from '../../constants'; 5 | import { formatNumber, getColor } from '../../utils'; 6 | import { Card } from '../Card'; 7 | import { populacao_residente_ram } from './../../data/generic.json'; 8 | import cardStyles from './../Card.module.scss'; 9 | 10 | export function RamMapa({ statistics, colors }) { 11 | let [graphData, setGraphData] = useState(); 12 | let [loaded, setLoaded] = useState(false); 13 | let { main, shades } = colors; 14 | 15 | const renderMap = async (map) => { 16 | const madeira = await fetch('/madeira.geojson').then((r) => r.json()); 17 | const madeiraMapa = L.map('map', { 18 | zoomSnap: 0.1, 19 | doubleClickZoom: false, 20 | closePopupOnClick: false, 21 | dragging: false, 22 | zoomDelta: false, 23 | trackResize: false, 24 | touchZoom: false, 25 | scrollWheelZoom: false, 26 | zoomControl: false, 27 | draggable: false, 28 | }); 29 | 30 | let layers = L.geoJSON(madeira, { 31 | onEachFeature: (feature, shape) => { 32 | let concelho = MADEIRA_DICOS[feature.properties.Dico]; 33 | let data = graphData.concelhos[concelho]; 34 | 35 | let percentagem_1 = (data.dose_1 / populacao_residente_ram[feature.properties.Dico].valor) * 100; 36 | let percentagem_2 = (data.dose_2 / populacao_residente_ram[feature.properties.Dico].valor) * 100; 37 | 38 | shape.bindPopup( 39 | `

40 | ${feature.properties.Municipio} 41 |
1ª Dose: ${formatNumber(data.dose_1)} (${percentagem_1.toFixed(2)}%) 42 |
2ª Dose: ${formatNumber(data.dose_2)} (${percentagem_2.toFixed(2)}%) 43 |

` 44 | ); 45 | shape.on('click', () => { 46 | //console.log('click'); 47 | }); 48 | }, 49 | 50 | style: function (feature) { 51 | let concelho = MADEIRA_DICOS[feature.properties.Dico]; 52 | let data = graphData.concelhos[concelho]; 53 | 54 | let percentagem = (data.dose_2 / populacao_residente_ram[feature.properties.Dico].valor) * 100; 55 | 56 | return { fillOpacity: 1, fillColor: getColor(percentagem), lineJoin: 'round', stroke: true, weight: 2, color: '#018b79' }; 57 | }, 58 | }).addTo(madeiraMapa); 59 | 60 | layers.eachLayer(function (layer) { 61 | layer.feature.properties.layerID = layer.feature.properties.DICOFRE; 62 | }); 63 | 64 | madeiraMapa.fitBounds(layers.getBounds()); 65 | madeiraMapa.setZoom(10); 66 | //Create legend 67 | var legend = L.control({ position: 'bottomleft' }); 68 | 69 | legend.onAdd = function (map) { 70 | var div = L.DomUtil.create('div', 'info legend'); 71 | 72 | for (var i = 0; i < grades.length; i++) { 73 | let grade = grades[i]; 74 | let grade_pretty = grades_pretty[grade]; 75 | div.innerHTML += `

76 | ${grade_pretty}

`; 77 | } 78 | 79 | return div; 80 | }; 81 | 82 | legend.addTo(madeiraMapa); 83 | }; 84 | 85 | function renderGraph(el) { 86 | const data = (canvas, cenas) => { 87 | const chartData = { 88 | labels: [''], 89 | datasets: [ 90 | { 91 | label: 'Total de vacinas administradas - 1ª Dose', 92 | borderColor: main, 93 | backgroundColor: main, 94 | stack: 'stack0', 95 | order: 2, 96 | data_actual: el.dose_1, 97 | data: [el.dose_1 - el.dose_2], 98 | }, 99 | { 100 | label: 'Total de vacinas administradas - 2ª Dose', 101 | borderColor: shades[0], 102 | backgroundColor: shades[0], 103 | data: [el.dose_2], 104 | data_actual: el.dose_2, 105 | stack: 'stack0', 106 | order: 1, 107 | }, 108 | ], 109 | }; 110 | 111 | return chartData; 112 | }; 113 | 114 | const options = () => { 115 | let dico = MADEIRA_DICOS[el.chave]; 116 | let populacao_residente = populacao_residente_ram[dico].valor; 117 | return { 118 | indexAxis: 'y', 119 | plugins: { 120 | tooltip: { 121 | mode: 'index', 122 | intersect: true, 123 | callbacks: { 124 | label: (tooltipItem, b) => { 125 | let data = tooltipItem.dataset.data_actual; 126 | return `${tooltipItem.dataset.label}: ${formatNumber(data, false)}`; 127 | }, 128 | }, 129 | }, 130 | datalabels: { 131 | display: false, 132 | }, 133 | legend: { 134 | position: 'bottom', 135 | align: 'start', 136 | display: false, 137 | }, 138 | }, 139 | layout: { padding: { left: -12 } }, 140 | 141 | animation: { 142 | duration: 1000, 143 | }, 144 | tooltips: { 145 | mode: 'index', 146 | intersect: false, 147 | 148 | callbacks: { 149 | title: (tooltipItem, data) => { 150 | return ''; 151 | }, 152 | }, 153 | }, 154 | scales: { 155 | y: { 156 | stacked: true, 157 | id: 'y-axis', 158 | ticks: { 159 | beginAtZero: true, 160 | }, 161 | }, 162 | 163 | x: { 164 | stacked: true, 165 | ticks: { 166 | beginAtZero: true, 167 | max: populacao_residente, 168 | stepSize: Math.round(window.innerWidth <= RESIZE_TRESHOLD ? populacao_residente / 3 : populacao_residente / 6), 169 | callback: (value) => formatNumber(value, false), 170 | }, 171 | }, 172 | }, 173 | }; 174 | }; 175 | 176 | return ( 177 | 178 |
179 |

{el.nome}

180 | 181 |
182 | 183 | ); 184 | } 185 | 186 | useEffect(async () => { 187 | statistics.getArquipelagoData().then((data) => { 188 | setGraphData(data[data.length - 1]); 189 | 190 | if (loaded === false) { 191 | setLoaded(true); 192 | } 193 | 194 | if (loaded === true) { 195 | renderMap(); 196 | } 197 | }); 198 | }, [loaded]); 199 | return loaded === true ? ( 200 | 201 | 202 | 203 |
204 | 205 |
206 | {Object.values(graphData.concelhos).map(renderGraph)} 207 | 208 |
209 |

210 | 211 | 1ª Dose 212 | 213 | 214 | 2ª Dose 215 | 216 |

217 |
218 |
219 |
220 | ) : ( 221 | '' 222 | ); 223 | } 224 | //{renderGraph(graphData.concelhos.ribeira_brava)} 225 | -------------------------------------------------------------------------------- /components/graphs/BarVacinasRecebidaDia.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | //import 'chartjs-plugin-annotation'; 3 | import { Bar } from 'react-chartjs-2'; 4 | import { formatNumber } from '../../utils'; 5 | import { Card } from './../Card'; 6 | import { RESIZE_TRESHOLD } from '../../constants'; 7 | import { useCanvasResizer } from '../../hooks/useCanvasResizer'; 8 | 9 | export function BarVacinasRecebidaDia({ statistics, colors }) { 10 | let [loading, setLoading] = useState(true); 11 | let [graphData, setGraphData] = useState({}); 12 | let [foreground, color_1, color_2, , color_3, ,] = colors; 13 | /* let [annotationsToggle, setAnnotationsToggle] = useState({ 14 | dose: true, 15 | dose2: true, 16 | dose3: true, 17 | }); */ 18 | 19 | let { setCanvasNode } = useCanvasResizer(); 20 | const data = (canvas) => { 21 | let { labels, mod, com, az, janss } = graphData; 22 | 23 | setCanvasNode(canvas.parentNode); 24 | 25 | return { 26 | labels: labels.map(({ from, to }) => { 27 | let fromDate = new Date(from); 28 | let toDate = new Date(to); 29 | 30 | return `De ${formatNumber(fromDate.getDate())}/${formatNumber(fromDate.getMonth() + 1)} a ${formatNumber( 31 | toDate.getDate() 32 | )}/${formatNumber(toDate.getMonth() + 1)}`; 33 | }), 34 | datasets: [ 35 | { 36 | label: 'Comirnaty (Pfizer/BioNTech)', 37 | fill: false, 38 | type: 'bar', 39 | overlayBars: true, 40 | backgroundColor: foreground, 41 | data: com, 42 | order: 2, 43 | display: false, 44 | stack: 'stack0', 45 | }, 46 | 47 | { 48 | label: 'Moderna', 49 | backgroundColor: color_1, 50 | borderColor: color_1, 51 | data: mod, 52 | type: 'bar', 53 | 54 | overlayBars: true, 55 | order: 3, 56 | stack: 'stack0', 57 | }, 58 | { 59 | label: 'AstraZeneca', 60 | backgroundColor: color_3, 61 | borderColor: color_3, 62 | type: 'bar', 63 | data: az, 64 | overlayBars: true, 65 | order: 3, 66 | stack: 'stack0', 67 | }, 68 | { 69 | label: 'Janssen', 70 | backgroundColor: color_2, 71 | borderColor: color_2, 72 | type: 'bar', 73 | data: janss, 74 | overlayBars: true, 75 | order: 3, 76 | stack: 'stack0', 77 | }, 78 | ], 79 | }; 80 | }; 81 | const options = () => { 82 | return { 83 | plugins: { 84 | datalabels: { 85 | display: false, 86 | color: 'blue', 87 | }, 88 | legend: { 89 | position: 'bottom', 90 | align: 'start', 91 | onHover: function (event, legend) { 92 | document.body.classList.add('mouse-pointer'); 93 | }, 94 | onLeave: function (event, legend) { 95 | document.body.classList.remove('mouse-pointer'); 96 | }, 97 | }, 98 | }, 99 | animation: { 100 | duration: 1000, 101 | }, 102 | /* annotation: { 103 | annotations: [ 104 | { 105 | type: 'line', 106 | mode: 'horizontal', 107 | scaleID: 'y-axis-0', 108 | value: annotationsToggle.dose ? generic.doses.valor : null, 109 | borderColor: '#0A9DD1', 110 | borderWidth: 2, 111 | borderDash: [5, 5], 112 | 113 | label: { 114 | backgroundColor: 'rgba(0,0,0,0.0)', 115 | 116 | drawTime: 'afterDatasetsDraw', 117 | 118 | fontSize: 13, 119 | 120 | textAlign: 'left', 121 | fontColor: '#0A9DD1', 122 | position: 'left', 123 | xAdjust: 10, 124 | yAdjust: -10, 125 | fontSize: '13px', 126 | fontStyle: 'bold', 127 | 128 | enabled: true, 129 | content: `Doses adquiridas - ${generic.doses.legenda} (01/03/2021) `, 130 | }, 131 | }, 132 | { 133 | type: 'line', 134 | mode: 'horizontal', 135 | scaleID: 'y-axis-0', 136 | value: annotationsToggle.dose ? 41000000 : null, 137 | borderColor: 'transparent', 138 | borderWidth: 0, 139 | 140 | label: { 141 | backgroundColor: 'rgba(0,0,0,0.0)', 142 | 143 | xAdjust: 0, 144 | yAdjust: -10, 145 | 146 | enabled: false, 147 | }, 148 | }, 149 | { 150 | type: 'line', 151 | mode: 'horizontal', 152 | scaleID: 'y-axis-0', 153 | value: annotationsToggle.dose3 ? generic.doses3.valor : null, 154 | borderColor: '#D17615', 155 | borderWidth: 2, 156 | borderDash: [5, 5], 157 | 158 | label: { 159 | backgroundColor: 'rgba(0,0,0,0.0)', 160 | 161 | drawTime: 'afterDatasetsDraw', 162 | 163 | fontSize: 13, 164 | 165 | textAlign: 'left', 166 | font: { 167 | style: 'bold', 168 | }, 169 | fontStyle: 'bold', 170 | 171 | fontColor: '#D11541', 172 | fontSize: '13px', 173 | position: 'left', 174 | xAdjust: 0, 175 | yAdjust: -10, 176 | enabled: true, 177 | content: `Doses adquiridas - ${generic.doses3.legenda} (21/01/2020) `, 178 | }, 179 | }, 180 | { 181 | type: 'line', 182 | mode: 'horizontal', 183 | scaleID: 'y-axis-0', 184 | value: annotationsToggle.dose2 ? generic.doses2.valor : null, 185 | borderColor: '#D17615', 186 | borderWidth: 2, 187 | borderDash: [5, 5], 188 | 189 | label: { 190 | backgroundColor: 'rgba(0,0,0,0.0)', 191 | 192 | drawTime: 'afterDatasetsDraw', 193 | 194 | fontSize: 13, 195 | 196 | textAlign: 'left', 197 | font: { 198 | style: 'bold', 199 | }, 200 | fontStyle: 'bold', 201 | 202 | fontColor: '#D17615', 203 | fontSize: '13px', 204 | position: 'left', 205 | xAdjust: 0, 206 | yAdjust: -10, 207 | enabled: true, 208 | content: `Doses adquiridas - ${generic.doses2.legenda} (04/12/2020) `, 209 | }, 210 | }, 211 | ], 212 | }, */ 213 | tooltips: { 214 | mode: 'index', 215 | intersect: false, 216 | callbacks: { 217 | label: (tooltipItem, data) => { 218 | var label = data.datasets[tooltipItem.datasetIndex].label; 219 | return label + ': ' + (parseInt(tooltipItem.value) ? formatNumber(parseInt(tooltipItem.value), false) : 0); 220 | }, 221 | title: (tooltipItem, data) => { 222 | return tooltipItem[0].label; 223 | }, 224 | }, 225 | }, 226 | 227 | scales: { 228 | y: { 229 | stacked: true, 230 | 231 | ticks: { 232 | beginAtZero: true, 233 | maxTicksLimit: window.innerWidth <= RESIZE_TRESHOLD ? 8 : 10, 234 | minTicksLimit: window.innerWidth <= RESIZE_TRESHOLD ? 8 : 10, 235 | 236 | callback: (value) => formatNumber(value, false), 237 | }, 238 | }, 239 | 240 | x: { 241 | stacked: true, 242 | ticks: { 243 | beginAtZero: true, 244 | }, 245 | }, 246 | }, 247 | }; 248 | }; 249 | 250 | useEffect(() => { 251 | statistics.getReceivedDosesByBrandByWeek().then((recievedData) => { 252 | setGraphData(recievedData); 253 | setLoading(false); 254 | }); 255 | }, []); 256 | 257 | return ( 258 | 259 |
{!loading ? : ''}
260 |
261 | ); 262 | } 263 | -------------------------------------------------------------------------------- /components/graphs/RamBarAdministradasPorFaixaEtaria.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | import { formatNumber } from '../../utils'; 4 | import { Card } from './../Card'; 5 | import classNames from 'classnames'; 6 | 7 | import { RESIZE_TRESHOLD, lineChartCommon } from './../../constants'; 8 | import { useCanvasResizer } from '../../hooks/useCanvasResizer'; 9 | export function RamBarAdministradasPorFaixaEtaria({ statistics, colors }) { 10 | let [loaded, setLoaded] = useState(false); 11 | let { main, shades, tints, complements } = colors; 12 | let [graphData, setGraphData] = useState({}); 13 | let [activeDose, setActiveDose] = useState('dose_1'); 14 | const canvasRef = useRef(null); 15 | 16 | function generateColor(color) { 17 | return { 18 | borderColor: color, 19 | pointBorderColor: color, 20 | pointBackgroundColor: color, 21 | pointHoverBackgroundColor: color, 22 | pointHoverBorderColor: color, 23 | }; 24 | } 25 | useEffect(() => { 26 | if (canvasRef?.current?.chartInstance) { 27 | canvasRef.current.chartInstance.data.datasets.forEach((dataset) => { 28 | if (dataset.customDose == activeDose) { 29 | dataset.hidden = false; 30 | } else { 31 | dataset.hidden = true; 32 | } 33 | canvasRef.current.chartInstance.update(); 34 | }); 35 | } 36 | }, [activeDose]); 37 | 38 | let { setCanvasNode } = useCanvasResizer(); 39 | const data = (canvas) => { 40 | let { labels, groups } = graphData; 41 | 42 | setCanvasNode(canvas.parentNode); 43 | 44 | return { 45 | labels: Object.keys(graphData.labels).map((key) => { 46 | let fromDate = new Date(labels[key]); 47 | return `${formatNumber(fromDate.getDate())}/${formatNumber(fromDate.getMonth() + 1)}`; 48 | }), 49 | datasets: [ 50 | { 51 | ...lineChartCommon, 52 | ...generateColor(shades[0]), 53 | label: 'Grupo 18/24', 54 | labelGroup: 'Grupo 18/24', 55 | fill: false, 56 | lineTension: 0.3, 57 | 58 | backgroundColor: shades[0], 59 | data: groups.map((group) => group.e1824[activeDose] || 0), 60 | 61 | order: 1, 62 | 63 | customDose: 2, 64 | }, 65 | 66 | { 67 | ...lineChartCommon, 68 | ...generateColor(tints[1]), 69 | label: 'Grupo 25/49', 70 | labelGroup: 'Grupo 25/49', 71 | fill: false, 72 | lineTension: 0.3, 73 | 74 | backgroundColor: tints[1], 75 | data: groups.map((group) => group.e2549[activeDose] || 0), 76 | 77 | order: 3, 78 | 79 | customDose: 2, 80 | }, 81 | 82 | { 83 | ...lineChartCommon, 84 | ...generateColor(main), 85 | label: 'Grupo 50/59', 86 | labelGroup: 'Grupo 50/59', 87 | fill: false, 88 | lineTension: 0.3, 89 | 90 | backgroundColor: main, 91 | data: groups.map((group) => group.e5059[activeDose] || 0), 92 | stack: 'stack1', 93 | order: 5, 94 | 95 | customDose: 2, 96 | }, 97 | 98 | { 99 | ...lineChartCommon, 100 | ...generateColor(shades[2]), 101 | label: 'Grupo 60/69', 102 | labelGroup: 'Grupo 60/69', 103 | fill: false, 104 | lineTension: 0.3, 105 | 106 | backgroundColor: shades[2], 107 | data: groups.map((group) => (group.e6064[activeDose] + group.e6569[activeDose]) / 2 || 0), 108 | 109 | order: 7, 110 | 111 | customDose: 2, 112 | }, 113 | 114 | { 115 | ...lineChartCommon, 116 | ...generateColor(complements[2]), 117 | label: 'Grupo 70/79', 118 | labelGroup: 'Grupo 70/79', 119 | fill: false, 120 | lineTension: 0.3, 121 | 122 | backgroundColor: complements[2], 123 | data: groups.map((group) => group.e7079.dose_2 || 0), 124 | 125 | order: 9, 126 | 127 | customDose: 2, 128 | }, 129 | 130 | { 131 | ...lineChartCommon, 132 | ...generateColor(complements[1]), 133 | label: 'Grupo 80+', 134 | labelGroup: 'Grupo 80+', 135 | backgroundColor: complements[1], 136 | data: groups.map((group) => group.e80.dose_2 || 0), 137 | 138 | order: 11, 139 | 140 | stack: 'stack2', 141 | fill: false, 142 | lineTension: 0.3, 143 | 144 | customDose: 2, 145 | }, 146 | ], 147 | }; 148 | }; 149 | const options = () => { 150 | let maxValue = 100; 151 | return { 152 | plugins: { 153 | datalabels: { 154 | display: false, 155 | }, 156 | legend: { 157 | position: 'bottom', 158 | align: 'start', 159 | onHover: function (event, legend) { 160 | document.body.classList.add('mouse-pointer'); 161 | }, 162 | onLeave: function (event, legend) { 163 | document.body.classList.remove('mouse-pointer'); 164 | }, 165 | }, 166 | }, 167 | onResize: (a, b, c) => { 168 | if (window.innerWidth <= RESIZE_TRESHOLD) { 169 | a.canvas.parentNode.style.width = '1000px'; 170 | } else { 171 | a.canvas.parentNode.style.width = 'auto'; 172 | } 173 | }, 174 | animation: { 175 | duration: 1000, 176 | }, 177 | tooltips: { 178 | mode: 'index', 179 | intersect: false, 180 | callbacks: { 181 | ...lineChartCommon, 182 | ...generateColor(shades[0]), 183 | label: (tooltipItem, data) => { 184 | var label = data.datasets[tooltipItem.datasetIndex].label; 185 | return label.replace('- 1ª Dose', '').replace('- 2ª Dose', '') + ': ' + parseFloat(tooltipItem.value).toFixed(2) + '%'; 186 | }, 187 | title: (tooltipItem, data) => { 188 | return tooltipItem[0].label; 189 | }, 190 | }, 191 | }, 192 | scales: { 193 | y: { 194 | id: 'axis', 195 | stacked: false, 196 | ticks: { 197 | beginAtZero: false, 198 | min: 0, 199 | max: maxValue, 200 | stepSize: (maxValue / 5).toFixed(0), 201 | callback: (value) => formatNumber(value, false) + '%', 202 | }, 203 | }, 204 | 205 | x: { 206 | id: 'xaxis', 207 | stacked: false, 208 | ticks: { 209 | beginAtZero: false, 210 | }, 211 | }, 212 | }, 213 | }; 214 | }; 215 | 216 | useEffect(() => { 217 | statistics.getAdministredDosesByAgeByWeekRam().then((data) => { 218 | setGraphData(data); 219 | setLoaded(true); 220 | setActiveDose('dose_1'); 221 | }); 222 | }, []); 223 | 224 | return ( 225 | 226 |
227 | {loaded === true ? ( 228 | <> 229 |
230 |

231 | 241 | 251 |

252 |
253 |
254 | 255 |
256 | 257 | ) : ( 258 | '' 259 | )} 260 |
261 |
262 | ); 263 | } 264 | -------------------------------------------------------------------------------- /components/graphs/LineAdministradasPorFaixaEtaria.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { Line } from 'react-chartjs-2'; 3 | import { formatNumber } from '../../utils'; 4 | import { Card } from './../Card'; 5 | import classNames from 'classnames'; 6 | 7 | import { RESIZE_TRESHOLD, lineChartCommon, SNS_WEEKS } from './../../constants'; 8 | import { useCanvasResizer } from '../../hooks/useCanvasResizer'; 9 | import { sub, parse, format } from 'date-fns'; 10 | export function LineAdministradasPorFaixaEtaria({ statistics, colors }) { 11 | let [loaded, setLoaded] = useState(false); 12 | let { main, shades, complements } = colors; 13 | let [graphData, setGraphData] = useState({}); 14 | let [activeDose, setActiveDose] = useState(1); 15 | const canvasRef = useRef(null); 16 | 17 | function generateColor(color) { 18 | return { 19 | borderColor: color, 20 | pointBorderColor: color, 21 | pointBackgroundColor: color, 22 | pointHoverBackgroundColor: color, 23 | pointHoverBorderColor: color, 24 | backgroundColor: color, 25 | }; 26 | } 27 | let { setCanvasNode } = useCanvasResizer(); 28 | 29 | const data = (canvas) => { 30 | setCanvasNode(canvas.parentNode); 31 | 32 | let labels = {}; 33 | graphData.map((values) => { 34 | labels[values.data] = ''; 35 | }); 36 | 37 | return { 38 | labels: Object.keys(labels).map((el) => { 39 | let parseDate = parse(el, 'dd-MM-yyyy', new Date()); 40 | let data = sub(parseDate, { days: 7 }); 41 | el = format(data, 'dd-LL-yyyy'); 42 | return SNS_WEEKS[el]; 43 | }), 44 | datasets: [ 45 | { 46 | ...lineChartCommon, 47 | ...generateColor(main), 48 | label: 'Entre os 12 e os 17 anos', 49 | labelGroup: 'Grupo 18/24', 50 | fill: false, 51 | lineTension: 0.3, 52 | 53 | data: graphData.map( 54 | (el) => parseFloat(el[activeDose === 1 ? 'doses1_perc_12_17' : 'pessoas_vacinadas_completamente_perc_12_17']) * 100 55 | ), 56 | 57 | order: 1, 58 | customDose: 2, 59 | }, 60 | { 61 | ...lineChartCommon, 62 | ...generateColor(shades[0]), 63 | label: 'Entre 18 anos e 24 anos', 64 | labelGroup: 'Grupo 18/24', 65 | fill: false, 66 | lineTension: 0.3, 67 | 68 | data: graphData.map( 69 | (el) => parseFloat(el[activeDose === 1 ? 'doses1_perc_18_24' : 'pessoas_vacinadas_completamente_perc_18_24']) * 100 70 | ), 71 | 72 | order: 1, 73 | customDose: 2, 74 | }, 75 | { 76 | ...lineChartCommon, 77 | ...generateColor(complements[2]), 78 | label: 'Entre 25 anos e 49 anos', 79 | labelGroup: 'Grupo 25/49', 80 | fill: false, 81 | lineTension: 0.3, 82 | 83 | data: graphData.map( 84 | (el) => parseFloat(el[activeDose === 1 ? 'doses1_perc_25_49' : 'pessoas_vacinadas_completamente_perc_25_49']) * 100 85 | ), 86 | 87 | order: 1, 88 | customDose: 2, 89 | }, 90 | { 91 | ...lineChartCommon, 92 | ...generateColor(shades[2]), 93 | label: 'Entre 50 anos e 64 anos', 94 | labelGroup: 'Grupo 25/49', 95 | fill: false, 96 | lineTension: 0.3, 97 | 98 | data: graphData.map( 99 | (el) => parseFloat(el[activeDose === 1 ? 'doses1_perc_50_64' : 'pessoas_vacinadas_completamente_perc_50_64']) * 100 100 | ), 101 | 102 | order: 1, 103 | customDose: 2, 104 | }, 105 | { 106 | ...lineChartCommon, 107 | ...generateColor(complements[0]), 108 | label: 'Entre 60 e 79 anos', 109 | labelGroup: 'Grupo 25/49', 110 | fill: false, 111 | lineTension: 0.3, 112 | 113 | data: graphData.map( 114 | (el) => parseFloat(el[activeDose === 1 ? 'doses1_perc_65_79' : 'pessoas_vacinadas_completamente_perc_65_79']) * 100 115 | ), 116 | order: 1, 117 | customDose: 2, 118 | }, 119 | { 120 | ...lineChartCommon, 121 | ...generateColor(complements[1]), 122 | label: '80 ou mais anos', 123 | labelGroup: 'Grupo 25/49', 124 | fill: false, 125 | lineTension: 0.3, 126 | 127 | data: graphData.map( 128 | (el) => parseFloat(el[activeDose === 1 ? 'doses1_perc_80+' : 'pessoas_vacinadas_completamente_perc_80+']) * 100 129 | ), 130 | order: 1, 131 | customDose: 2, 132 | }, 133 | ], 134 | }; 135 | }; 136 | const options = () => { 137 | let maxValue = 100; 138 | return { 139 | //maintainAspectRatio: false, 140 | 141 | plugins: { 142 | datalabels: { 143 | display: false, 144 | color: 'blue', 145 | }, 146 | legend: { 147 | position: 'bottom', 148 | align: 'start', 149 | onHover: function (event, legend) { 150 | document.body.classList.add('mouse-pointer'); 151 | }, 152 | onLeave: function (event, legend) { 153 | document.body.classList.remove('mouse-pointer'); 154 | }, 155 | }, 156 | }, 157 | onResize: (a, b, c) => { 158 | if (window.innerWidth <= RESIZE_TRESHOLD) { 159 | a.canvas.parentNode.style.width = '1000px'; 160 | } else { 161 | a.canvas.parentNode.style.width = 'auto'; 162 | } 163 | }, 164 | 165 | animation: { 166 | duration: 1000, 167 | }, 168 | tooltips: { 169 | mode: 'index', 170 | intersect: false, 171 | callbacks: { 172 | ...lineChartCommon, 173 | ...generateColor(shades[0]), 174 | label: (tooltipItem, data) => { 175 | var label = data.datasets[tooltipItem.datasetIndex].label; 176 | return label.replace('- 1ª Dose', '').replace('- 2ª Dose', '') + ': ' + parseFloat(tooltipItem.value).toFixed(2) + '%'; 177 | }, 178 | title: (tooltipItem, data) => { 179 | return tooltipItem[0].label; 180 | }, 181 | }, 182 | }, 183 | scales: { 184 | y: { 185 | ticks: { 186 | beginAtZero: false, 187 | min: 0, 188 | stepSize: (maxValue / 5).toFixed(0), 189 | callback: (value) => formatNumber(value, false) + '%', 190 | }, 191 | max: maxValue, 192 | }, 193 | 194 | x: { 195 | ticks: { 196 | beginAtZero: false, 197 | }, 198 | }, 199 | }, 200 | }; 201 | }; 202 | //trigger deploy 203 | useEffect(() => { 204 | statistics.getTotalSNSIdade().then((data) => { 205 | setGraphData(data); 206 | setLoaded(true); 207 | setActiveDose(1); 208 | }); 209 | }, []); 210 | 211 | return ( 212 | 213 |
214 | {loaded === true ? ( 215 | <> 216 |
217 |

218 | 228 | 238 |

239 |
240 |
241 | 242 |
243 | 244 | ) : ( 245 | '' 246 | )} 247 |
248 |
249 | ); 250 | } 251 | --------------------------------------------------------------------------------