├── 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 |
--------------------------------------------------------------------------------
/_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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 [](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 | 
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 |
--------------------------------------------------------------------------------