├── now.json ├── src ├── utils │ ├── flags.ts │ ├── sort.ts │ ├── getRandomFromRange.ts │ ├── last.ts │ ├── consts.ts │ ├── numberWithCommas.ts │ ├── shuffleArray.ts │ ├── extractKeyFromNestedObj.ts │ ├── countryToCode.ts │ ├── memoryState.ts │ ├── breakpoints.ts │ ├── useWindowWidth.tsx │ ├── googleAnalytics.ts │ ├── downloadCsv.ts │ ├── rgbToHsl.ts │ ├── getDatesFromDataRow.ts │ ├── generateNewColors.ts │ └── colors.ts ├── react-app-env.d.ts ├── assets │ ├── logo_pink.png │ ├── logo_white.png │ ├── pinksmoke-min.jpg │ ├── logo_white_narrow.png │ ├── pinksmoke-small-min.jpg │ └── logo_square_white_transparent.png ├── components │ ├── Dashboard │ │ ├── README.md │ │ ├── Title.js │ │ ├── Tooltip.tsx │ │ ├── Brush.tsx │ │ ├── CurrentCount.tsx │ │ ├── YAxis.tsx │ │ ├── Select.tsx │ │ ├── Orders.js │ │ ├── Chart.tsx │ │ ├── ListItems.tsx │ │ └── Dashboard.tsx │ ├── PerCapitaSwitch.tsx │ ├── Collapsable.tsx │ ├── CustomChip.tsx │ ├── IOSSlider.tsx │ ├── Snackbar.tsx │ ├── NumberWithTitle.tsx │ ├── BottomNavigationBar.tsx │ ├── Share.tsx │ ├── MapChart.tsx │ ├── MultiChart.tsx │ └── UsaMapChart.tsx ├── App.test.ts ├── data │ ├── utils.ts │ ├── stateNames.json │ ├── allstates.json │ ├── whoDataStore.ts │ ├── dataStore.ts │ └── continentsArray.json ├── theme.ts ├── index.tsx ├── pages │ ├── Todo.tsx │ ├── Dashboard.tsx │ └── Comparison.tsx ├── index.css ├── Switch.tsx ├── App.tsx ├── serviceWorker.js └── logo.svg ├── backend ├── .gitignore ├── env.json ├── src │ └── synchronize-data │ │ ├── environment-variables.ts │ │ ├── create-branch-name.ts │ │ ├── get-formatted-current-date.ts │ │ ├── app.ts │ │ ├── covid-file-contents.ts │ │ ├── handle-synchronize.ts │ │ └── create-pull-request-for-covid-data.ts ├── tsconfig.json ├── deploy.ps1 ├── .vscode │ └── launch.json ├── template.yml ├── package.json └── webpack.config.js ├── public ├── robots.txt ├── favicon.ico ├── logo200.png ├── logo_pink.png ├── logo_white.png ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── logo_square_small.png ├── logo_white_narrow.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── logo_square_small_white.png ├── logo_square_white_transparent.png ├── site.webmanifest ├── manifest.json └── index.html ├── .vscode └── settings.json ├── .gitignore ├── tsconfig.json ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── tslint.json ├── README.md ├── LICENSE └── package.json /now.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/utils/flags.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # aws-sam 5 | /.aws-sam -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/logo200.png -------------------------------------------------------------------------------- /backend/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "SynchronizeData": { 3 | "GITHUB_ACCESS_TOKEN": "" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/logo_pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/logo_pink.png -------------------------------------------------------------------------------- /public/logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/logo_white.png -------------------------------------------------------------------------------- /src/utils/sort.ts: -------------------------------------------------------------------------------- 1 | function sort(arr, func) { 2 | return arr.concat().sort(func); 3 | } 4 | 5 | export default sort; 6 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/logo_pink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/src/assets/logo_pink.png -------------------------------------------------------------------------------- /src/assets/logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/src/assets/logo_white.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/logo_square_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/logo_square_small.png -------------------------------------------------------------------------------- /public/logo_white_narrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/logo_white_narrow.png -------------------------------------------------------------------------------- /src/assets/pinksmoke-min.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/src/assets/pinksmoke-min.jpg -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/logo_white_narrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/src/assets/logo_white_narrow.png -------------------------------------------------------------------------------- /public/logo_square_small_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/logo_square_small_white.png -------------------------------------------------------------------------------- /src/assets/pinksmoke-small-min.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/src/assets/pinksmoke-small-min.jpg -------------------------------------------------------------------------------- /backend/src/synchronize-data/environment-variables.ts: -------------------------------------------------------------------------------- 1 | export const getGitHubAccessToken = (): string => { 2 | return process.env.GITHUB_ACCESS_TOKEN!; 3 | }; 4 | -------------------------------------------------------------------------------- /public/logo_square_white_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/public/logo_square_white_transparent.png -------------------------------------------------------------------------------- /src/assets/logo_square_white_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m3h0w/covid19-coronavirus-react-visualization/HEAD/src/assets/logo_square_white_transparent.png -------------------------------------------------------------------------------- /src/utils/getRandomFromRange.ts: -------------------------------------------------------------------------------- 1 | function getRandomFromRange(min: number, max: number) { 2 | return Math.random() * (max - min) + min; 3 | } 4 | 5 | export default getRandomFromRange; 6 | -------------------------------------------------------------------------------- /backend/src/synchronize-data/create-branch-name.ts: -------------------------------------------------------------------------------- 1 | export const createBranchName = ( 2 | getFormattedDate: () => string 3 | ) => (): string => { 4 | return `${getFormattedDate()}-data-update`; 5 | }; 6 | -------------------------------------------------------------------------------- /src/utils/last.ts: -------------------------------------------------------------------------------- 1 | const last = (array: T[] | undefined): T | undefined => { 2 | if (!array || !array.length) { 3 | return; 4 | } 5 | return array[array.length - 1]; 6 | }; 7 | 8 | export default last; 9 | -------------------------------------------------------------------------------- /src/utils/consts.ts: -------------------------------------------------------------------------------- 1 | export const animationTime = 1000; 2 | export const SIDEBAR_WIDTH = 56; 3 | export const US_NAME = 'United States'; 4 | export const SOUTH_KOREA = 'South Korea'; 5 | export const GLOBAL_PAPER_OPACITY = 0.9; 6 | -------------------------------------------------------------------------------- /backend/src/synchronize-data/get-formatted-current-date.ts: -------------------------------------------------------------------------------- 1 | export const getFormattedCurrentDate = (): string => { 2 | const date = new Date(); 3 | return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`; 4 | }; 5 | -------------------------------------------------------------------------------- /src/components/Dashboard/README.md: -------------------------------------------------------------------------------- 1 | # Dashboard template 2 | 3 | ## Usage 4 | 5 | Simply copy the files into your project, or one of the [example applications](https://github.com/mui-org/material-ui/tree/master/examples), and import and use the `Dashboard` component. 6 | -------------------------------------------------------------------------------- /src/utils/numberWithCommas.ts: -------------------------------------------------------------------------------- 1 | export default function numberWithCommas(x: number | string | undefined): string | undefined { 2 | if (x === undefined || x === null) { 3 | return undefined; 4 | } 5 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); 6 | } 7 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/utils/shuffleArray.ts: -------------------------------------------------------------------------------- 1 | function shuffleArray(array: any[]) { 2 | for (let i = array.length - 1; i > 0; i--) { 3 | const j = Math.floor(Math.random() * (i + 1)); 4 | [array[i], array[j]] = [array[j], array[i]]; 5 | } 6 | return array; 7 | } 8 | 9 | export default shuffleArray; 10 | -------------------------------------------------------------------------------- /src/App.test.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.jsxSingleQuote": true, 3 | "prettier.singleQuote": true, 4 | "prettier.trailingComma": "es5", 5 | "editor.formatOnSave": true, 6 | "prettier.printWidth": 100, 7 | "spellright.language": ["en"], 8 | "spellright.documentTypes": ["markdown", "latex", "plaintext"] 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/extractKeyFromNestedObj.ts: -------------------------------------------------------------------------------- 1 | const extractKeyFromNestedObj = (obj: object, keyToExtract: string) => 2 | Object.keys(obj).reduce((acc: { [countryKey: string]: number } | {}, countryKey: string) => { 3 | acc[countryKey] = obj[countryKey][keyToExtract]; 4 | return acc; 5 | }, {}); 6 | 7 | export default extractKeyFromNestedObj; 8 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "allowJs": true, 6 | "checkJs": true, 7 | "sourceMap": true, 8 | "noImplicitAny": false, 9 | "strict": true, 10 | "esModuleInterop": true 11 | }, 12 | "include": ["src/**/*.ts", "src/**/*.js"] 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/countryToCode.ts: -------------------------------------------------------------------------------- 1 | import countryCodes from '../data/countryCodes.json'; 2 | 3 | const countryToCode = (country: string) => { 4 | let c = country; 5 | if (country === 'Czechia') { 6 | c = 'Czech Republic'; 7 | } 8 | return countryCodes.find((v) => v.country_name === c.replace('*', ''))?.country_code; 9 | }; 10 | 11 | export default countryToCode; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # **/pages/assets 26 | .env -------------------------------------------------------------------------------- /src/components/Dashboard/Title.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Typography from '@material-ui/core/Typography'; 4 | 5 | export default function Title(props) { 6 | return ( 7 | 8 | {props.children} 9 | 10 | ); 11 | } 12 | 13 | Title.propTypes = { 14 | children: PropTypes.node, 15 | }; 16 | -------------------------------------------------------------------------------- /src/data/utils.ts: -------------------------------------------------------------------------------- 1 | import { US_NAME, SOUTH_KOREA } from '../utils/consts'; 2 | export const namesMap = { 3 | US: US_NAME, 4 | 'Korea, South': SOUTH_KOREA, 5 | 'Bahamas, The': 'Bahamas', 6 | [`Cote d'Ivoire`]: 'Ivory Coast', 7 | 'Gambia, The': 'Gambia', 8 | 'Taiwan*': 'Taiwan', 9 | }; 10 | export const swapName = (name: string): string => { 11 | if (!Object.keys(namesMap).includes(name)) { 12 | return name; 13 | } 14 | return namesMap[name]; 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/synchronize-data/app.ts: -------------------------------------------------------------------------------- 1 | import "source-map-support/register"; 2 | import { handleSynchronize } from "./handle-synchronize"; 3 | import { 4 | getConfirmedFileContent, 5 | getDeathsFileContent 6 | } from "./covid-file-contents"; 7 | 8 | export const handler = async (): Promise => { 9 | try { 10 | await handleSynchronize(getConfirmedFileContent, getDeathsFileContent)(); 11 | } catch (e) { 12 | console.log(e); 13 | throw e; 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/memoryState.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const createPersistedState = () => { 4 | let savedValue: any; 5 | function useMemoryState(initialState?: any) { 6 | const [value, setValue] = useState(savedValue ?? initialState); 7 | 8 | useEffect(() => { 9 | savedValue = value; 10 | }, [value]); 11 | 12 | return [value, setValue]; 13 | } 14 | return useMemoryState; 15 | }; 16 | 17 | export default createPersistedState; 18 | -------------------------------------------------------------------------------- /backend/deploy.ps1: -------------------------------------------------------------------------------- 1 | npm run build 2 | sam deploy ` 3 | --template-file .aws-sam/build/template.yaml ` 4 | --stack-name covid-19-data-synchronizer ` 5 | --s3-bucket serverless-source-code-bucket ` 6 | --s3-prefix covid-19-data-synchronizer ` 7 | --region eu-west-1 ` 8 | --profile private ` 9 | --capabilities CAPABILITY_IAM ` 10 | --no-fail-on-empty-changeset ` 11 | --parameter-overrides ParameterKey=GithubAccessToken,ParameterValue= -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Coronavirus covid19 pink dashboard", 3 | "short_name": "COVID19.PINK", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } -------------------------------------------------------------------------------- /src/components/PerCapitaSwitch.tsx: -------------------------------------------------------------------------------- 1 | import { observable } from 'mobx'; 2 | 3 | export class PerCapitaState { 4 | @observable public perCapitaBool: boolean; 5 | 6 | constructor(initialValue: boolean = true) { 7 | this.perCapitaBool = initialValue; 8 | } 9 | 10 | public set = (value: boolean) => { 11 | this.perCapitaBool = value; 12 | }; 13 | 14 | public toggle = () => { 15 | this.perCapitaBool = !this.perCapitaBool; 16 | }; 17 | } 18 | export const perCapitaState = new PerCapitaState(); 19 | -------------------------------------------------------------------------------- /src/utils/breakpoints.ts: -------------------------------------------------------------------------------- 1 | const ResponsiveBreakpoints = { 2 | xs: 0, 3 | sm: 600, 4 | md: 960, 5 | lg: 1280, 6 | xl: 1920, 7 | }; 8 | 9 | const getWindowWidth = () => window.innerWidth; 10 | 11 | export const smUp = () => getWindowWidth() >= ResponsiveBreakpoints.sm; 12 | 13 | export const mdUp = () => getWindowWidth() >= ResponsiveBreakpoints.md; 14 | 15 | export const smDown = () => getWindowWidth() <= ResponsiveBreakpoints.md; 16 | 17 | export const xsDown = () => getWindowWidth() <= ResponsiveBreakpoints.sm; 18 | -------------------------------------------------------------------------------- /src/components/Dashboard/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LabelFormatter, Tooltip, TooltipFormatter } from 'recharts'; 3 | import numberWithCommas from '../../utils/numberWithCommas'; 4 | 5 | const getTooltip = (labelFormatter?: LabelFormatter, formatter?: TooltipFormatter) => ( 6 | 12 | ); 13 | 14 | export default getTooltip; 15 | -------------------------------------------------------------------------------- /src/utils/useWindowWidth.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const useWindowWidth = () => { 4 | const [windowWidth, setWindowWidth] = useState(window.innerWidth); 5 | 6 | const handleWindowResize = () => { 7 | setWindowWidth(window.innerWidth); 8 | }; 9 | 10 | useEffect(() => { 11 | window.addEventListener('resize', handleWindowResize); 12 | return () => window.removeEventListener('resize', handleWindowResize); 13 | }, []); 14 | 15 | return windowWidth; 16 | }; 17 | 18 | export default useWindowWidth; 19 | -------------------------------------------------------------------------------- /backend/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "SynchronizeData", 6 | "type": "node", 7 | "request": "attach", 8 | "address": "localhost", 9 | "port": 5858, 10 | "localRoot": "${workspaceRoot}/.aws-sam/build/SynchronizeData", 11 | "remoteRoot": "/var/task", 12 | "protocol": "inspector", 13 | "stopOnEntry": false, 14 | "outFiles": [ 15 | "${workspaceRoot}/.aws-sam/build/SynchronizeData/app.js" 16 | ], 17 | "sourceMaps": true 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /backend/src/synchronize-data/covid-file-contents.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | export const getConfirmedFileContent = async (): Promise => { 4 | const result = await axios.get( 5 | "https://www.soothsawyer.com/wp-content/uploads/2020/03/time_series_19-covid-Confirmed.csv" 6 | ); 7 | return result.data; 8 | }; 9 | 10 | export const getDeathsFileContent = async (): Promise => { 11 | const result = await axios.get( 12 | "https://www.soothsawyer.com/wp-content/uploads/2020/03/time_series_19-covid-Deaths.csv" 13 | ); 14 | return result.data; 15 | }; 16 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { createMuiTheme } from '@material-ui/core'; 2 | 3 | export default createMuiTheme({ 4 | palette: { 5 | primary: { 6 | main: '#f06292', 7 | }, 8 | secondary: { 9 | main: '#69f0ae', 10 | }, 11 | }, 12 | typography: { 13 | fontFamily: [ 14 | 'Roboto', 15 | 'Caveat', 16 | '-apple-system', 17 | 'BlinkMacSystemFont', 18 | '"Segoe UI"', 19 | '"Helvetica Neue"', 20 | 'Arial', 21 | 'sans-serif', 22 | '"Apple Color Emoji"', 23 | '"Segoe UI Emoji"', 24 | '"Segoe UI Symbol"', 25 | ].join(','), 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react", 17 | "baseUrl": "src", 18 | "experimentalDecorators": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["src"] 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/googleAnalytics.ts: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga'; 2 | 3 | class GoogleAnalytics { 4 | constructor() { 5 | this.init(); 6 | } 7 | 8 | private init = () => { 9 | ReactGA.initialize('UA-129359323-2'); 10 | }; 11 | 12 | public pageView = () => { 13 | ReactGA.pageview(window.location.pathname + window.location.search); 14 | // console.log(`Page view ${window.location.pathname + window.location.search}`); 15 | }; 16 | 17 | public setUser = (userId: string) => { 18 | ReactGA.set({ userId }); 19 | }; 20 | } 21 | 22 | const googleAnalyticsInstance = new GoogleAnalytics(); 23 | 24 | export default googleAnalyticsInstance; 25 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | import { MuiThemeProvider } from '@material-ui/core'; 6 | import theme from './theme'; 7 | import './index.css'; 8 | 9 | ReactDOM.render( 10 | , 11 | document.getElementById('app') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /src/components/Dashboard/Brush.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Brush, TickFormatterFunction } from 'recharts'; 3 | 4 | interface IProps { 5 | color: string; 6 | tickFormatter?: TickFormatterFunction; 7 | data: any; 8 | children: JSX.Element; 9 | dataKey?: string; 10 | } 11 | 12 | const getBrush = ({ color, tickFormatter, data, children, dataKey = 'timestamp' }: IProps) => ( 13 | 22 | {children} 23 | 24 | ); 25 | 26 | export default getBrush; 27 | -------------------------------------------------------------------------------- /backend/src/synchronize-data/handle-synchronize.ts: -------------------------------------------------------------------------------- 1 | import { createPullRequestWithCovidData } from "./create-pull-request-for-covid-data"; 2 | import { getGitHubAccessToken } from "./environment-variables"; 3 | 4 | export const handleSynchronize = ( 5 | getConfirmedFileContent: () => Promise, 6 | getDeathsFileContent: () => Promise 7 | ) => async (): Promise => { 8 | const getConfirmedFileContentPromise = getConfirmedFileContent(); 9 | const getDeathsFileContentPromise = getDeathsFileContent(); 10 | await createPullRequestWithCovidData(getGitHubAccessToken)( 11 | await getConfirmedFileContentPromise, 12 | await getDeathsFileContentPromise 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /src/pages/Todo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Dashboard from 'components/Dashboard/Dashboard'; 3 | 4 | const todos = [ 5 | 'Add info button next to Infection Trajectories and redirect to an FAQ (knowledge base with static text)', 6 | 'Per capita switch for map and charts (global?)', 7 | 'Gapminder-like plot with time travel', 8 | 'Radar chart (Uber react-vis) per country', 9 | 'ML for predicting cases and deaths per country', 10 | ]; 11 | 12 | const Todo = () => ( 13 | 14 |
    15 | {todos.map((todo: string) => { 16 | return
  • {todo}
  • ; 17 | })} 18 |
19 |
20 | ); 21 | 22 | export default Todo; 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /backend/template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Backend application for providing COVID-19 data 4 | Parameters: 5 | GithubAccessToken: 6 | Type: String 7 | Resources: 8 | SynchronizeData: 9 | Type: AWS::Serverless::Function 10 | Properties: 11 | CodeUri: src/synchronize-data 12 | Handler: app.handler 13 | Runtime: nodejs12.x 14 | Timeout: 180 15 | MemorySize: 128 16 | Environment: 17 | Variables: 18 | GITHUB_ACCESS_TOKEN: !Ref GithubAccessToken 19 | Events: 20 | SynchronizationSchedule: 21 | Type: Schedule 22 | Properties: 23 | Schedule: cron(0 8 * * ? *) 24 | -------------------------------------------------------------------------------- /src/utils/downloadCsv.ts: -------------------------------------------------------------------------------- 1 | import { csv } from 'd3-request'; 2 | 3 | const deadUrl = 4 | 'https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_19-covid-Deaths.csv'; 5 | const confirmedUrl = 6 | 'https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_19-covid-Confirmed.csv'; 7 | 8 | const fetchCsv = (dataType: 'confirmed' | 'dead', callback: (data: any) => void) => { 9 | const url = { 10 | dead: deadUrl, 11 | confirmed: confirmedUrl, 12 | }[dataType]; 13 | csv(url, (err, data) => { 14 | if (err) { 15 | console.error(err); 16 | callback(undefined); 17 | } 18 | 19 | callback(data); 20 | }); 21 | }; 22 | 23 | export default fetchCsv; 24 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "covid-19-data-synchronizer", 4 | "scripts": { 5 | "build": "webpack-cli", 6 | "clean": "rimraf .aws-sam .vscode", 7 | "prebuild": "npm run clean", 8 | "prewatch": "npm run clean", 9 | "watch": "webpack-cli -w" 10 | }, 11 | "dependencies": { 12 | "@octokit/rest": "^17.1.2", 13 | "aws-sdk": "^2.647.0", 14 | "axios": "^0.19.2", 15 | "octokit-create-pull-request": "^1.4.2", 16 | "source-map-support": "^0.5.16" 17 | }, 18 | "devDependencies": { 19 | "@types/aws-lambda": "^8.10.46", 20 | "@types/node": "^13.9.5", 21 | "aws-sam-webpack-plugin": "^0.6.0", 22 | "ts-loader": "^6.2.2", 23 | "typescript": "^3.8.3", 24 | "webpack": "^4.42.1", 25 | "webpack-cli": "^3.3.11" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "max-line-length": { 4 | "options": [ 5 | 120 6 | ], 7 | "severity": "warning" 8 | }, 9 | "no-lowlevel-commenting": false, 10 | "curly": { 11 | "severity": "warning" 12 | }, 13 | "jsx-wrap-multiline": false, 14 | "typedef": false, 15 | "jsx-no-multiline-js": false, 16 | "discreet-ternary": false, 17 | "jsx-boolean-value": false, 18 | "ter-arrow-parens": false, 19 | "jsx-no-lambda": false 20 | }, 21 | "linterOptions": { 22 | "exclude": [ 23 | "config/**/*.js", 24 | "node_modules/**/*.ts", 25 | "coverage/lcov-report/*.js", 26 | "webpack.config.js" 27 | ] 28 | }, 29 | "extends": [ 30 | "tslint:recommended", 31 | "tslint-react", 32 | "tslint-config-airbnb", 33 | "tslint-config-silind" 34 | ] 35 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COVID19.PINK 2 | ### Coronavirus (Covid19) Animated Map and Infection Trajectories comparison 3 | 4 | 5 | Main ideas: 6 | - easier-to-understand, animated map with time travel capability 7 | - charts for comparing countries and estimating where they are in their infection trajectory stage 8 | 9 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), uses TypeScript, Material UI, Recharts, react-simple-maps, and now.sh for deployment. 10 | 11 | Reach out if you'd like to contribute. 12 | 13 | 14 | Data source: [2019 Novel Coronavirus COVID-19 (2019-nCoV) Data Repository by Johns Hopkins CSSE](https://github.com/CSSEGISandData/COVID-19/tree/master/csse_covid_19_data) 15 | 16 | Tech stack: React, TypeScript, [now.sh](https://zeit.co/), Recharts, Material UI, [React Simple Maps](https://www.react-simple-maps.io/) 17 | -------------------------------------------------------------------------------- /src/utils/rgbToHsl.ts: -------------------------------------------------------------------------------- 1 | function rgbToHsl(rgbcode: string) { 2 | var r = parseInt(rgbcode.slice(1, 3), 16), 3 | g = parseInt(rgbcode.slice(3, 5), 16), 4 | b = parseInt(rgbcode.slice(5, 7), 16); 5 | 6 | r /= 255; 7 | g /= 255; 8 | b /= 255; 9 | 10 | var max = Math.max(r, g, b), 11 | min = Math.min(r, g, b); 12 | var h, 13 | s, 14 | l = (max + min) / 2; 15 | 16 | if (max === min) { 17 | h = s = 0; // achromatic 18 | } else { 19 | var d = max - min; 20 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min); 21 | switch (max) { 22 | case r: 23 | h = (g - b) / d + (g < b ? 6 : 0); 24 | break; 25 | case g: 26 | h = (b - r) / d + 2; 27 | break; 28 | case b: 29 | h = (r - g) / d + 4; 30 | break; 31 | } 32 | h /= 6; 33 | } 34 | 35 | return [h, s, l]; 36 | } 37 | 38 | export default rgbToHsl; 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | margin: 0; 7 | padding: 0; 8 | background: #666; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 13 | } 14 | 15 | /* html, body, #root { 16 | display: block; 17 | width: 100%; 18 | height: 100%; 19 | } */ 20 | 21 | #root { 22 | /* height: 3000px; */ 23 | } 24 | 25 | canvas { 26 | display: block; 27 | position: fixed; 28 | width: 100vw; 29 | height: 100vh; 30 | z-index: -1; 31 | } 32 | 33 | .link { 34 | position: absolute; 35 | bottom: 20px; 36 | left: 20px; 37 | text-decoration: none; 38 | color: white; 39 | font-family: Fira Code, monospace; 40 | font-size: 0.75rem; 41 | background: rgba(0, 0, 0, 0.5); 42 | padding: 0.5rem; 43 | } 44 | 45 | #root { 46 | /* position: fixed; */ 47 | background: #666; 48 | z-index: -1; 49 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Michał Gacka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/Dashboard/CurrentCount.tsx: -------------------------------------------------------------------------------- 1 | import { getCapitaScaleString } from 'data/dataStore'; 2 | import React from 'react'; 3 | 4 | import { Typography } from '@material-ui/core'; 5 | 6 | import numberWithCommas from '../../utils/numberWithCommas'; 7 | import NumberWithTitle from '../NumberWithTitle'; 8 | 9 | export default function CurrentCount({ style, confirmedCases, deaths, mortalityRate, perCapita }) { 10 | return ( 11 |
12 | {perCapita && ( 13 | 14 | {`(Values per ${getCapitaScaleString()} inhabitants)`} 15 | 16 | )} 17 | 22 | 23 | {mortalityRate ? ( 24 | 29 | ) : undefined} 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/synchronize-data/create-pull-request-for-covid-data.ts: -------------------------------------------------------------------------------- 1 | import { Octokit } from "@octokit/rest"; 2 | import createPullRequest from "octokit-create-pull-request"; 3 | import { createBranchName } from "./create-branch-name"; 4 | import { getFormattedCurrentDate } from "./get-formatted-current-date"; 5 | 6 | const OctokitWithPlugin = Octokit.plugin(createPullRequest); 7 | 8 | export const createPullRequestWithCovidData = ( 9 | getGitHubAccessToken: () => string 10 | ) => async (confirmedContent: string, deathsContent: string): Promise => { 11 | const octokit = new OctokitWithPlugin({ 12 | auth: getGitHubAccessToken() 13 | }); 14 | const targetRepositoryOwner = "m3h0w"; 15 | const targetRepositoryName = "covid19-coronavirus-react-visualization"; 16 | const commitAndTitleMessage = "Update COVID-19 data"; 17 | 18 | await octokit.createPullRequest({ 19 | owner: targetRepositoryOwner, 20 | repo: targetRepositoryName, 21 | title: commitAndTitleMessage, 22 | head: createBranchName(getFormattedCurrentDate)(), 23 | changes: { 24 | files: { 25 | "src/data/confirmed_global.csv": confirmedContent, 26 | "src/data/deaths_global.csv": deathsContent 27 | }, 28 | commit: commitAndTitleMessage 29 | } 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/Collapsable.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | import { Collapse } from '@material-ui/core'; 3 | import Divider from '@material-ui/core/Divider'; 4 | import IconButton from '@material-ui/core/IconButton'; 5 | import ExpandLessIcon from '@material-ui/icons/ExpandLess'; 6 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 7 | import { makeStyles } from '@material-ui/core/styles'; 8 | 9 | const useStyles = makeStyles((theme) => ({ 10 | maxHeight: { maxHeight: '40vh', overflow: 'auto' }, 11 | })); 12 | 13 | interface IProps { 14 | startingOpen: boolean; 15 | } 16 | 17 | const Collapsable: FC = ({ startingOpen = false, children }) => { 18 | const classes = useStyles(); 19 | const [expanded, setExpanded] = useState(startingOpen); 20 | 21 | return ( 22 | <> 23 | 24 | {children} 25 | 26 | 27 | { 29 | setExpanded(!expanded); 30 | }} 31 | style={{ width: '100%', textAlign: 'center', borderRadius: '3px' }} 32 | > 33 | {expanded ? : } 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default Collapsable; 40 | -------------------------------------------------------------------------------- /src/components/CustomChip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Chip } from '@material-ui/core'; 3 | import { getContrastYIQ } from '../utils/colors'; 4 | import { makeStyles, createStyles } from '@material-ui/core/styles'; 5 | 6 | const useStyles = makeStyles((theme) => 7 | createStyles({ 8 | chip: { 9 | margin: 5, 10 | backgroundColor: (props: { backgroundColor: string }) => props.backgroundColor, 11 | opacity: 0.9, 12 | color: (props: { backgroundColor: string }) => 13 | props.backgroundColor ? getContrastYIQ(props.backgroundColor) : 'inherit', 14 | '& *': { 15 | color: (props: { backgroundColor: string }) => 16 | props.backgroundColor ? getContrastYIQ(props.backgroundColor) : 'inherit', 17 | }, 18 | '&:focus,&:hover,&$active': { 19 | backgroundColor: (props: { backgroundColor: string }) => props.backgroundColor, 20 | opacity: 1.0, 21 | }, 22 | }, 23 | }) 24 | ); 25 | 26 | const CustomChip = (props) => { 27 | const { handleDelete, label, backgroundColor, avatar } = props; 28 | const classes = useStyles({ backgroundColor }); 29 | return ( 30 | 38 | ); 39 | }; 40 | 41 | export default CustomChip; 42 | -------------------------------------------------------------------------------- /src/utils/getDatesFromDataRow.ts: -------------------------------------------------------------------------------- 1 | import moment, { Moment } from 'moment'; 2 | import { Row } from '../components/MultiChart'; 3 | 4 | export const FIRST_DATE = '1/22/2020'; 5 | export const FIRST_DATE_WHO = '1/21/2020'; 6 | 7 | export const momentToFormat = (m: Moment): string => m.format('M/D/YYYY'); 8 | 9 | export const momentToFormatLong = (m: Moment) => m.format('M/D/YYYY'); 10 | 11 | export const getDatesFromDataRow = (data: Row | undefined) => { 12 | if (!data) { 13 | return; 14 | } 15 | const firstDateM = moment(FIRST_DATE); 16 | const nowM = moment(); 17 | const days = nowM.diff(firstDateM, 'days'); 18 | const dates: Moment[] = []; 19 | for (let i = 0; i < days + 1; i = i + 1) { 20 | const newDate = moment(FIRST_DATE).add(i, 'days'); 21 | if (momentToFormat(newDate) in data) { 22 | dates.push(newDate); 23 | } 24 | } 25 | return dates; 26 | }; 27 | 28 | export const getDatesFromDataRowWho = (data: Row | undefined) => { 29 | if (!data) { 30 | return; 31 | } 32 | const firstDateM = moment(FIRST_DATE_WHO); 33 | const nowM = moment(); 34 | const days = nowM.diff(firstDateM, 'days'); 35 | const dates: Moment[] = []; 36 | for (let i = 0; i < days + 1; i = i + 1) { 37 | const newDate = moment(FIRST_DATE_WHO).add(i, 'days'); 38 | // console.log(momentToFormatWho(newDate), data); 39 | if (momentToFormatLong(newDate) in data) { 40 | dates.push(newDate); 41 | } 42 | } 43 | return dates; 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/IOSSlider.tsx: -------------------------------------------------------------------------------- 1 | import { Slider, withStyles } from '@material-ui/core'; 2 | 3 | const iOSBoxShadow = 4 | '0 3px 1px rgba(0,0,0,0.1),0 4px 8px rgba(0,0,0,0.13),0 0 0 1px rgba(0,0,0,0.02)'; 5 | 6 | const IOSSlider = withStyles((theme) => ({ 7 | root: { 8 | color: theme.palette.secondary.main, 9 | height: 2, 10 | padding: '15px 0', 11 | }, 12 | thumb: { 13 | height: 28, 14 | width: 16, 15 | borderRadius: 3, 16 | backgroundColor: '#fff', 17 | boxShadow: iOSBoxShadow, 18 | marginTop: -14, 19 | marginLeft: -14, 20 | '&:focus,&:hover,&$active': { 21 | boxShadow: '0 3px 1px rgba(0,0,0,0.1),0 4px 8px rgba(0,0,0,0.3),0 0 0 1px rgba(0,0,0,0.02)', 22 | // Reset on touch devices, it doesn't add specificity 23 | '@media (hover: none)': { 24 | boxShadow: iOSBoxShadow, 25 | }, 26 | }, 27 | }, 28 | active: {}, 29 | valueLabel: { 30 | left: 'calc(-50% + 11px)', 31 | top: -22, 32 | '& *': { 33 | background: 'transparent', 34 | color: '#000', 35 | }, 36 | }, 37 | track: { 38 | height: 2, 39 | }, 40 | rail: { 41 | height: 2, 42 | opacity: 0.5, 43 | backgroundColor: '#bfbfbf', 44 | }, 45 | mark: { 46 | backgroundColor: '#bfbfbf', 47 | height: 8, 48 | width: 1, 49 | marginTop: -3, 50 | }, 51 | markActive: { 52 | opacity: 1, 53 | backgroundColor: 'currentColor', 54 | }, 55 | }))(Slider); 56 | 57 | export default IOSSlider; 58 | -------------------------------------------------------------------------------- /backend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const AwsSamPlugin = require("aws-sam-webpack-plugin"); 3 | 4 | const awsSamPlugin = new AwsSamPlugin(); 5 | 6 | module.exports = { 7 | // Loads the entry object from the AWS::Serverless::Function resources in your 8 | // SAM config. Setting this to a function will 9 | entry: () => awsSamPlugin.entry(), 10 | 11 | // Write the output to the .aws-sam/build folder 12 | output: { 13 | filename: chunkData => awsSamPlugin.filename(chunkData), 14 | libraryTarget: "commonjs2", 15 | path: path.resolve(".") 16 | }, 17 | 18 | // Create source maps 19 | devtool: "source-map", 20 | 21 | // Resolve .ts and .js extensions 22 | resolve: { 23 | extensions: [".ts", ".js"] 24 | }, 25 | 26 | // Target node 27 | target: "node", 28 | 29 | // AWS recommends always including the aws-sdk in your Lambda package but excluding can significantly reduce 30 | // the size of your deployment package. If you want to always include it then comment out this line. It has 31 | // been included conditionally because the node10.x docker image used by SAM local doesn't include it. 32 | externals: process.env.NODE_ENV === "development" ? [] : ["aws-sdk"], 33 | 34 | // Set the webpack mode 35 | mode: process.env.NODE_ENV || "production", 36 | 37 | // Add the TypeScript loader 38 | module: { 39 | rules: [{ test: /\.tsx?$/, loader: "ts-loader" }] 40 | }, 41 | 42 | // Add the AWS SAM Webpack plugin 43 | plugins: [awsSamPlugin] 44 | }; 45 | -------------------------------------------------------------------------------- /src/data/stateNames.json: -------------------------------------------------------------------------------- 1 | { 2 | "AL": "Alabama", 3 | "AK": "Alaska", 4 | "AS": "American Samoa", 5 | "AZ": "Arizona", 6 | "AR": "Arkansas", 7 | "CA": "California", 8 | "CO": "Colorado", 9 | "CT": "Connecticut", 10 | "DE": "Delaware", 11 | "DC": "District Of Columbia", 12 | "FM": "Federated States Of Micronesia", 13 | "FL": "Florida", 14 | "GA": "Georgia", 15 | "GU": "Guam", 16 | "HI": "Hawaii", 17 | "ID": "Idaho", 18 | "IL": "Illinois", 19 | "IN": "Indiana", 20 | "IA": "Iowa", 21 | "KS": "Kansas", 22 | "KY": "Kentucky", 23 | "LA": "Louisiana", 24 | "ME": "Maine", 25 | "MH": "Marshall Islands", 26 | "MD": "Maryland", 27 | "MA": "Massachusetts", 28 | "MI": "Michigan", 29 | "MN": "Minnesota", 30 | "MS": "Mississippi", 31 | "MO": "Missouri", 32 | "MT": "Montana", 33 | "NE": "Nebraska", 34 | "NV": "Nevada", 35 | "NH": "New Hampshire", 36 | "NJ": "New Jersey", 37 | "NM": "New Mexico", 38 | "NY": "New York", 39 | "NC": "North Carolina", 40 | "ND": "North Dakota", 41 | "MP": "Northern Mariana Islands", 42 | "OH": "Ohio", 43 | "OK": "Oklahoma", 44 | "OR": "Oregon", 45 | "PW": "Palau", 46 | "PA": "Pennsylvania", 47 | "PR": "Puerto Rico", 48 | "RI": "Rhode Island", 49 | "SC": "South Carolina", 50 | "SD": "South Dakota", 51 | "TN": "Tennessee", 52 | "TX": "Texas", 53 | "UT": "Utah", 54 | "VT": "Vermont", 55 | "VI": "Virgin Islands", 56 | "VA": "Virginia", 57 | "WA": "Washington", 58 | "WV": "West Virginia", 59 | "WI": "Wisconsin", 60 | "WY": "Wyoming" 61 | } -------------------------------------------------------------------------------- /src/components/Dashboard/YAxis.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { LabelProps, YAxis } from 'recharts'; 3 | 4 | const getYAxis = ( 5 | yLabel: string, 6 | logScale: boolean = false, 7 | hide: boolean = false, 8 | orientation?: 'left' | 'right' | undefined, 9 | mirror: boolean = true, 10 | allowDecimals: boolean = true, 11 | domain?: [any, any] 12 | ) => { 13 | const isSmall = window.innerWidth < 600; 14 | 15 | const yaxisDefaults = { 16 | allowDataOverflow: true, 17 | axisLine: false, 18 | tickLine: false, 19 | type: 'number' as 'number' | 'category' | undefined, 20 | }; 21 | 22 | const getYLabelConfig = (yLabel: string): LabelProps => { 23 | return { 24 | value: yLabel, 25 | angle: -90, 26 | dx: 7, 27 | position: 'insideLeft', 28 | style: { textAnchor: 'middle', fontSize: '80%' }, 29 | }; 30 | }; 31 | return ( 32 | dataMax * 2] 45 | : [0, (dataMax) => Math.ceil(dataMax * 1.1)] 46 | : domain 47 | } 48 | {...yaxisDefaults} 49 | /> 50 | ); 51 | }; 52 | 53 | export default getYAxis; 54 | -------------------------------------------------------------------------------- /src/Switch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch } from 'react-router'; 3 | import { Route, Redirect } from 'react-router-dom'; 4 | import DashboardPage from './pages/Dashboard'; 5 | import MapPage from './pages/Map'; 6 | import ComparisonPage from './pages/Comparison'; 7 | import Todo from './pages/Todo'; 8 | import { BooleanParam, useQueryParam, withDefault } from 'use-query-params'; 9 | import { perCapitaState } from './components/PerCapitaSwitch'; 10 | import { autorun, reaction } from 'mobx'; 11 | import { observer } from 'mobx-react-lite'; 12 | import useDataStore from './data/dataStore'; 13 | 14 | const CustomSwitch = observer(() => { 15 | const [perCapita, setPerCapita] = useQueryParam( 16 | 'per_capita', 17 | withDefault(BooleanParam, false) 18 | ); 19 | const dataStore = useDataStore(); 20 | 21 | React.useEffect(() => { 22 | dataStore.perCapita = perCapita; 23 | }, [perCapita]); 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }); 38 | 39 | export default CustomSwitch; 40 | -------------------------------------------------------------------------------- /src/utils/generateNewColors.ts: -------------------------------------------------------------------------------- 1 | import ColorScheme from 'color-scheme'; 2 | import getRandomFromRange from './getRandomFromRange'; 3 | import shuffleArray from './shuffleArray'; 4 | import rgbToHsl from './rgbToHsl'; 5 | 6 | const newColorScheme = () => { 7 | var scheme = new ColorScheme(); 8 | scheme.from_hue(getRandomFromRange(0, 100)).scheme('tetrade').distance(0.8).variation('hard'); 9 | var colors = shuffleArray(scheme.colors()); 10 | return colors; 11 | }; 12 | 13 | const generateNewColors = (length: number): string[] => { 14 | if (!length) { 15 | return []; 16 | } 17 | const array = []; 18 | let mainCounter = 0; 19 | let moduloCounter = 0; 20 | let colors = newColorScheme(); 21 | while (colors.length && array.length < length && mainCounter < length * 10) { 22 | moduloCounter = mainCounter % colors.length; 23 | if (moduloCounter === 0) { 24 | colors = shuffleArray(colors); 25 | } 26 | const colorHex = `#${colors[moduloCounter]}`; 27 | const l = rgbToHsl(colorHex)[2]; 28 | if (!l) { 29 | continue; 30 | } 31 | if (mainCounter < colors.length * 3) { 32 | if (l < 0.5) { 33 | array.push(colorHex); 34 | colors.splice(moduloCounter, 1); 35 | } 36 | } else { 37 | colors = newColorScheme(); 38 | mainCounter = 0; 39 | } 40 | mainCounter += 1; 41 | } 42 | 43 | if (mainCounter >= length * 50) { 44 | throw new Error(`Failed generating colors for length: ${length}`); 45 | } 46 | 47 | if (array.length !== length) { 48 | console.error('Didnt generate enough colors', array.length, length); 49 | } 50 | 51 | return array; 52 | }; 53 | 54 | export default generateNewColors; 55 | -------------------------------------------------------------------------------- /src/components/Snackbar.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, FC } from 'react'; 2 | import PubSub from 'pubsub-js'; 3 | import { withSnackbar, WithSnackbarProps } from 'notistack'; 4 | 5 | export enum SeverityLevel { 6 | error = 'error', 7 | warning = 'warning', 8 | info = 'info', 9 | success = 'success', 10 | } 11 | 12 | interface IProps { 13 | severity: SeverityLevel; 14 | text: string; 15 | autoHideDuration?: number; 16 | } 17 | 18 | const DEFAULT_AUTOHIDE_DURATION = 6000; 19 | 20 | export const SNACK_BAR_TOPIC = 'SNACK_BAR_TOPIC'; 21 | 22 | const CustomizedSnackbar_: FC = ({ enqueueSnackbar }) => { 23 | useEffect(() => { 24 | const token = PubSub.subscribe(SNACK_BAR_TOPIC, (msg, data) => { 25 | enqueueSnackbar(data.text, data); 26 | }); 27 | return () => PubSub.unsubscribe(token); 28 | }, [enqueueSnackbar]); 29 | 30 | return ''; 31 | }; 32 | 33 | const CustomizedSnackbar = withSnackbar(CustomizedSnackbar_); 34 | export default CustomizedSnackbar; 35 | 36 | export function showSnackBar( 37 | variant: SeverityLevel, 38 | text: string | { message: string }, 39 | duration: number = DEFAULT_AUTOHIDE_DURATION 40 | ) { 41 | PubSub.publish(SNACK_BAR_TOPIC, { 42 | variant, 43 | text: text.message ? text.message : text, 44 | autoHideDuration: duration, 45 | }); 46 | } 47 | 48 | export const showErrorSnackBar = (text: string, duration: number = DEFAULT_AUTOHIDE_DURATION) => 49 | showSnackBar(SeverityLevel.error, text, duration); 50 | 51 | export const showSuccessSnackBar = (text: string, duration: number = DEFAULT_AUTOHIDE_DURATION) => 52 | showSnackBar(SeverityLevel.success, text, duration); 53 | 54 | export const showInfoSnackBar = (text: string, duration: number = DEFAULT_AUTOHIDE_DURATION) => 55 | showSnackBar(SeverityLevel.info, text, duration); 56 | -------------------------------------------------------------------------------- /src/components/NumberWithTitle.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import Title from 'components/Dashboard/Title'; 3 | import Typography from '@material-ui/core/Typography'; 4 | import { makeStyles } from '@material-ui/core/styles'; 5 | import { CSSProperties } from '@material-ui/core/styles/withStyles'; 6 | import numberWithCommas from '../utils/numberWithCommas'; 7 | 8 | const useStyles = makeStyles((theme) => ({ 9 | typo: { marginTop: '-11px', marginBottom: '2px' }, 10 | })); 11 | 12 | interface IProps { 13 | color?: 'primary' | 'secondary' | 'initial'; 14 | title: string; 15 | number: string | number; 16 | version?: 'large'; 17 | centered?: boolean; 18 | onClick?: () => void; 19 | style?: CSSProperties; 20 | } 21 | 22 | const NumberWithTitle: FC = ({ 23 | color = 'primary', 24 | title, 25 | number, 26 | version, 27 | centered = false, 28 | onClick, 29 | style, 30 | }) => { 31 | const classes = useStyles(); 32 | if (version === 'large') { 33 | return ( 34 |
35 | 36 | {title} 37 | 38 | 45 | {numberWithCommas(number)} 46 | 47 |
48 | ); 49 | } 50 | 51 | return ( 52 |
53 | {title} 54 | 55 | {number} 56 | 57 |
58 | ); 59 | }; 60 | 61 | export default NumberWithTitle; 62 | -------------------------------------------------------------------------------- /src/utils/colors.ts: -------------------------------------------------------------------------------- 1 | import theme from 'theme'; 2 | // const Colors: { names?: { [key: string]: string }; random?: () => string } | undefined = {}; 3 | 4 | class Colors { 5 | defaultColor = '#000'; 6 | 7 | darkColors = { 8 | aqua: '#00ffff', 9 | black: '#000000', 10 | blue: '#0000ff', 11 | brown: '#a52a2a', 12 | cyan: '#00ffff', 13 | darkblue: '#00008b', 14 | darkcyan: '#008b8b', 15 | darkgrey: '#a9a9a9', 16 | darkgreen: '#006400', 17 | darkkhaki: '#bdb76b', 18 | darkmagenta: '#8b008b', 19 | darkolivegreen: '#556b2f', 20 | darkorange: '#ff8c00', 21 | darkorchid: '#9932cc', 22 | darkred: '#8b0000', 23 | darksalmon: '#e9967a', 24 | darkviolet: '#9400d3', 25 | fuchsia: '#ff00ff', 26 | gold: '#ffd700', 27 | green: '#008000', 28 | indigo: '#4b0082', 29 | lime: '#00ff00', 30 | magenta: '#ff00ff', 31 | maroon: '#800000', 32 | navy: '#000080', 33 | olive: '#808000', 34 | orange: '#ffa500', 35 | pink: '#ffc0cb', 36 | purple: '#800080', 37 | violet: '#800080', 38 | red: '#ff0000', 39 | }; 40 | 41 | public getRandomColor() { 42 | let color = this.defaultColor; 43 | 44 | const colorKeys = Object.keys(this.darkColors); 45 | const length = colorKeys.length; 46 | if (length > 0) { 47 | const index = Math.floor(length * Math.random()); 48 | 49 | const colorKey = colorKeys[index]; 50 | 51 | color = this.darkColors[colorKey]; 52 | delete this.darkColors[colorKey]; 53 | } 54 | 55 | return color; 56 | } 57 | } 58 | 59 | export function getContrastYIQ(hexcolor) { 60 | hexcolor = hexcolor.replace('#', ''); 61 | var r = parseInt(hexcolor.substr(0, 2), 16); 62 | var g = parseInt(hexcolor.substr(2, 2), 16); 63 | var b = parseInt(hexcolor.substr(4, 2), 16); 64 | var yiq = (r * 299 + g * 587 + b * 114) / 1000; 65 | return yiq >= 128 ? theme.palette.grey[800] : theme.palette.grey[200]; 66 | } 67 | 68 | export default Colors; 69 | -------------------------------------------------------------------------------- /src/components/Dashboard/Select.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core'; 3 | import Autocomplete from '@material-ui/lab/Autocomplete'; 4 | import TextField from '@material-ui/core/TextField'; 5 | 6 | interface IProps { 7 | label: string; 8 | disabled?: boolean; 9 | selectedValue: string; 10 | handleChange: (e) => void; 11 | possibleValues: string[]; 12 | id: string; 13 | width: string | number; 14 | } 15 | 16 | const CustomSelect: FC = ({ 17 | label, 18 | disabled, 19 | selectedValue, 20 | handleChange, 21 | possibleValues, 22 | id, 23 | width = '100%', 24 | }) => { 25 | return ( 26 | 27 | {label} 28 | 44 | 45 | ); 46 | }; 47 | 48 | export const CustomAutocomplete: FC = ({ 49 | label, 50 | disabled, 51 | selectedValue, 52 | handleChange, 53 | possibleValues, 54 | id, 55 | width = '100%', 56 | }) => { 57 | return ( 58 | 59 | { 62 | if (newValue || newValue === null) { 63 | handleChange(newValue); 64 | return; 65 | } 66 | return; 67 | }} 68 | id='combo-box-demo' 69 | options={possibleValues} 70 | getOptionLabel={(option: string) => option} 71 | style={{ width }} 72 | renderInput={(params) => } 73 | autoHighlight={true} 74 | // autoSelect={true} 75 | /> 76 | 77 | ); 78 | }; 79 | 80 | export default CustomSelect; 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "covid19.pink", 3 | "version": "0.1.1", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "4.9.14", 7 | "@material-ui/icons": "4.9.1", 8 | "@material-ui/lab": "4.0.0-alpha.53", 9 | "@types/d3-request": "^1.0.5", 10 | "@types/d3-scale": "^2.2.0", 11 | "@types/helmet": "0.0.45", 12 | "@types/jest": "^24.0.18", 13 | "@types/node": "^12.7.11", 14 | "@types/react": "^16.9.5", 15 | "@types/react-dom": "^16.9.1", 16 | "@types/react-router": "^5.1.1", 17 | "@types/react-router-dom": "^5.1.0", 18 | "@types/react-share": "^3.0.3", 19 | "@types/react-simple-maps": "^1.0.1", 20 | "@types/react-tooltip": "^3.11.0", 21 | "@types/recharts": "^1.8.7", 22 | "color-scheme": "^1.0.1", 23 | "d3-request": "^1.0.6", 24 | "d3-scale": "^3.2.1", 25 | "mobx": "^5.15.4", 26 | "mobx-react-lite": "^1.5.2", 27 | "moment": "^2.24.0", 28 | "notistack": "^0.9.9", 29 | "persistence-hooks": "^1.1.0", 30 | "pubsub-js": "^1.8.0", 31 | "query-string": "^6.13.7", 32 | "react": "^16.10.2", 33 | "react-country-flag": "^2.0.1", 34 | "react-dom": "^16.10.2", 35 | "react-ga": "^2.7.0", 36 | "react-helmet": "^5.2.1", 37 | "react-icons": "^3.7.0", 38 | "react-image-appear": "^1.1.22", 39 | "react-router": "^5.1.2", 40 | "react-router-dom": "^5.1.2", 41 | "react-script": "^2.0.5", 42 | "react-scripts": "4.0.0", 43 | "react-share": "^4.1.0", 44 | "react-simple-maps": "^1.0.0-beta.0", 45 | "react-tooltip": "^4.1.2", 46 | "recharts": "^1.8.5", 47 | "typescript": "3.9.2", 48 | "use-query-params": "^1.1.9" 49 | }, 50 | "scripts": { 51 | "start": "react-scripts start", 52 | "build": "react-scripts build", 53 | "test": "react-scripts test", 54 | "eject": "react-scripts eject" 55 | }, 56 | "eslintConfig": { 57 | "extends": "react-app" 58 | }, 59 | "browserslist": { 60 | "production": [ 61 | ">0.2%", 62 | "not dead", 63 | "not op_mini all" 64 | ], 65 | "development": [ 66 | "last 1 chrome version", 67 | "last 1 firefox version", 68 | "last 1 safari version" 69 | ] 70 | }, 71 | "devDependencies": { 72 | "tslint-config-airbnb": "5.11.2", 73 | "tslint-config-silind": "1.0.21", 74 | "tslint-react": "^4.1.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, createRef } from 'react'; 2 | import { Switch, useHistory } from 'react-router'; 3 | import { Route, BrowserRouter, Redirect } from 'react-router-dom'; 4 | import DashboardPage from './pages/Dashboard'; 5 | import ComparisonPage from './pages/Comparison'; 6 | import Todo from './pages/Todo'; 7 | import MapPage from './pages/Map'; 8 | import googleAnalyticsInstance from './utils/googleAnalytics'; 9 | import { SnackbarProvider } from 'notistack'; 10 | import IconButton from '@material-ui/core/IconButton'; 11 | import CloseIcon from '@material-ui/icons/Close'; 12 | import CustomizedSnackbar from './components/Snackbar'; 13 | import { QueryParamProvider } from 'use-query-params'; 14 | import CustomSwitch from './Switch'; 15 | 16 | const Routes = () => { 17 | const history = useHistory(); 18 | 19 | useEffect(() => { 20 | let prevLocation: Location; 21 | const unlisten = history.listen((location, action) => { 22 | if (!prevLocation || prevLocation.pathname !== location.pathname) { 23 | googleAnalyticsInstance.pageView(); 24 | } 25 | prevLocation = location; 26 | }); 27 | return () => unlisten(); 28 | }, [history]); 29 | 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | const App = (): JSX.Element => { 38 | const notistackRef = createRef(); 39 | const onClickDismiss = (key: string | number | undefined) => () => { 40 | if (notistackRef && notistackRef.current) { 41 | notistackRef.current.closeSnackbar(key); 42 | } 43 | }; 44 | return ( 45 | // 46 | ( 49 | 55 | 56 | 57 | )} 58 | maxSnack={3} 59 | anchorOrigin={{ 60 | vertical: 'bottom', 61 | horizontal: 'center', 62 | }} 63 | dense={false} 64 | > 65 | 66 | 67 | 68 | 69 | 70 | 71 | // 72 | ); 73 | }; 74 | 75 | export default App; 76 | -------------------------------------------------------------------------------- /src/components/Dashboard/Orders.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from '@material-ui/core/Link'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import Table from '@material-ui/core/Table'; 5 | import TableBody from '@material-ui/core/TableBody'; 6 | import TableCell from '@material-ui/core/TableCell'; 7 | import TableHead from '@material-ui/core/TableHead'; 8 | import TableRow from '@material-ui/core/TableRow'; 9 | import Title from './Title'; 10 | 11 | // Generate Order Data 12 | function createData(id, date, name, shipTo, paymentMethod, amount) { 13 | return { id, date, name, shipTo, paymentMethod, amount }; 14 | } 15 | 16 | const rows = [ 17 | createData(0, '16 Mar, 2019', 'Elvis Presley', 'Tupelo, MS', 'VISA ⠀•••• 3719', 312.44), 18 | createData(1, '16 Mar, 2019', 'Paul McCartney', 'London, UK', 'VISA ⠀•••• 2574', 866.99), 19 | createData(2, '16 Mar, 2019', 'Tom Scholz', 'Boston, MA', 'MC ⠀•••• 1253', 100.81), 20 | createData(3, '16 Mar, 2019', 'Michael Jackson', 'Gary, IN', 'AMEX ⠀•••• 2000', 654.39), 21 | createData(4, '15 Mar, 2019', 'Bruce Springsteen', 'Long Branch, NJ', 'VISA ⠀•••• 5919', 212.79), 22 | ]; 23 | 24 | function preventDefault(event) { 25 | event.preventDefault(); 26 | } 27 | 28 | const useStyles = makeStyles(theme => ({ 29 | seeMore: { 30 | marginTop: theme.spacing(3), 31 | }, 32 | })); 33 | 34 | export default function Orders() { 35 | const classes = useStyles(); 36 | return ( 37 | 38 | Recent Orders 39 | 40 | 41 | 42 | Date 43 | Name 44 | Ship To 45 | Payment Method 46 | Sale Amount 47 | 48 | 49 | 50 | {rows.map(row => ( 51 | 52 | {row.date} 53 | {row.name} 54 | {row.shipTo} 55 | {row.paymentMethod} 56 | {row.amount} 57 | 58 | ))} 59 | 60 |
61 |
62 | 63 | See more orders 64 | 65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/components/BottomNavigationBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Share from 'components/Share'; 3 | import { makeStyles, Theme } from '@material-ui/core'; 4 | import BottomNavigation from '@material-ui/core/BottomNavigation'; 5 | import BottomNavigationAction from '@material-ui/core/BottomNavigationAction'; 6 | import PublicIcon from '@material-ui/icons/Public'; 7 | import TrendingUpIcon from '@material-ui/icons/TrendingUp'; 8 | import DashboardIcon from '@material-ui/icons/Dashboard'; 9 | import { useHistory, useLocation } from 'react-router-dom'; 10 | import { useTheme } from '@material-ui/core/styles'; 11 | 12 | const useStyles = makeStyles(() => ({ 13 | navWrapper: { 14 | position: 'fixed', 15 | width: '100vw', 16 | display: 'flex', 17 | flexDirection: 'row', 18 | justifyContent: 'center', 19 | alignItems: 'center', 20 | zIndex: 0, 21 | bottom: 0, 22 | left: 0, 23 | }, 24 | })); 25 | 26 | const useActionStyles = makeStyles((theme: Theme) => ({ 27 | root: { 28 | '&$selected': { 29 | color: theme.palette.secondary.main, 30 | }, 31 | }, 32 | selected: {}, 33 | })); 34 | 35 | const BottomNavigationBar = () => { 36 | const classes = useStyles(); 37 | const actionElementClasses = useActionStyles(); 38 | const location = useLocation(); 39 | const history = useHistory(); 40 | const theme = useTheme(); 41 | 42 | let value; 43 | switch (location.pathname.split('/')[1]) { 44 | case 'world': 45 | value = 0; 46 | break; 47 | case 'dashboard': 48 | value = 1; 49 | break; 50 | case 'infection-trajectories': 51 | value = 2; 52 | break; 53 | } 54 | 55 | return ( 56 |
57 | 58 |
59 | { 62 | switch (actionValue) { 63 | case 0: 64 | history.push('/world'); 65 | break; 66 | case 1: 67 | history.push('/dashboard'); 68 | break; 69 | case 2: 70 | history.push('/infection-trajectories'); 71 | break; 72 | } 73 | }} 74 | showLabels 75 | style={{ width: '100%' }} 76 | color={theme.palette.secondary.main} 77 | > 78 | } 82 | /> 83 | } 87 | /> 88 | } 92 | /> 93 | 94 |
95 |
96 | ); 97 | }; 98 | 99 | export default BottomNavigationBar; 100 | -------------------------------------------------------------------------------- /src/data/allstates.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "AL", 4 | "val": "01" 5 | }, 6 | { 7 | "id": "AK", 8 | "val": "02" 9 | }, 10 | { 11 | "id": "AS", 12 | "val": "60" 13 | }, 14 | { 15 | "id": "AZ", 16 | "val": "04" 17 | }, 18 | { 19 | "id": "AR", 20 | "val": "05" 21 | }, 22 | { 23 | "id": "CA", 24 | "val": "06" 25 | }, 26 | { 27 | "id": "CO", 28 | "val": "08" 29 | }, 30 | { 31 | "id": "CT", 32 | "val": "09" 33 | }, 34 | { 35 | "id": "DE", 36 | "val": "10" 37 | }, 38 | { 39 | "id": "DC", 40 | "val": "11" 41 | }, 42 | { 43 | "id": "FL", 44 | "val": "12" 45 | }, 46 | { 47 | "id": "FM", 48 | "val": "64" 49 | }, 50 | { 51 | "id": "GA", 52 | "val": "13" 53 | }, 54 | { 55 | "id": "GU", 56 | "val": "66" 57 | }, 58 | { 59 | "id": "HI", 60 | "val": "15" 61 | }, 62 | { 63 | "id": "ID", 64 | "val": "16" 65 | }, 66 | { 67 | "id": "IL", 68 | "val": "17" 69 | }, 70 | { 71 | "id": "IN", 72 | "val": "18" 73 | }, 74 | { 75 | "id": "IA", 76 | "val": "19" 77 | }, 78 | { 79 | "id": "KS", 80 | "val": "20" 81 | }, 82 | { 83 | "id": "KY", 84 | "val": "21" 85 | }, 86 | { 87 | "id": "LA", 88 | "val": "22" 89 | }, 90 | { 91 | "id": "ME", 92 | "val": "23" 93 | }, 94 | { 95 | "id": "MH", 96 | "val": "68" 97 | }, 98 | { 99 | "id": "MD", 100 | "val": "24" 101 | }, 102 | { 103 | "id": "MA", 104 | "val": "25" 105 | }, 106 | { 107 | "id": "MI", 108 | "val": "26" 109 | }, 110 | { 111 | "id": "MN", 112 | "val": "27" 113 | }, 114 | { 115 | "id": "MS", 116 | "val": "28" 117 | }, 118 | { 119 | "id": "MO", 120 | "val": "29" 121 | }, 122 | { 123 | "id": "MT", 124 | "val": "30" 125 | }, 126 | { 127 | "id": "NE", 128 | "val": "31" 129 | }, 130 | { 131 | "id": "NV", 132 | "val": "32" 133 | }, 134 | { 135 | "id": "NH", 136 | "val": "33" 137 | }, 138 | { 139 | "id": "NJ", 140 | "val": "34" 141 | }, 142 | { 143 | "id": "NM", 144 | "val": "35" 145 | }, 146 | { 147 | "id": "NY", 148 | "val": "36" 149 | }, 150 | { 151 | "id": "NC", 152 | "val": "37" 153 | }, 154 | { 155 | "id": "ND", 156 | "val": "38" 157 | }, 158 | { 159 | "id": "MP", 160 | "val": "69" 161 | }, 162 | { 163 | "id": "OH", 164 | "val": "39" 165 | }, 166 | { 167 | "id": "OK", 168 | "val": "40" 169 | }, 170 | { 171 | "id": "OR", 172 | "val": "41" 173 | }, 174 | { 175 | "id": "PW", 176 | "val": "70" 177 | }, 178 | { 179 | "id": "PA", 180 | "val": "42" 181 | }, 182 | { 183 | "id": "PR", 184 | "val": "72" 185 | }, 186 | { 187 | "id": "RI", 188 | "val": "44" 189 | }, 190 | { 191 | "id": "SC", 192 | "val": "45" 193 | }, 194 | { 195 | "id": "SD", 196 | "val": "46" 197 | }, 198 | { 199 | "id": "TN", 200 | "val": "47" 201 | }, 202 | { 203 | "id": "TX", 204 | "val": "48" 205 | }, 206 | { 207 | "id": "UM", 208 | "val": "74" 209 | }, 210 | { 211 | "id": "UT", 212 | "val": "49" 213 | }, 214 | { 215 | "id": "VT", 216 | "val": "50" 217 | }, 218 | { 219 | "id": "VA", 220 | "val": "51" 221 | }, 222 | { 223 | "id": "VI", 224 | "val": "78" 225 | }, 226 | { 227 | "id": "WA", 228 | "val": "53" 229 | }, 230 | { 231 | "id": "WV", 232 | "val": "54" 233 | }, 234 | { 235 | "id": "WI", 236 | "val": "55" 237 | }, 238 | { 239 | "id": "WY", 240 | "val": "56" 241 | } 242 | ] -------------------------------------------------------------------------------- /src/components/Share.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Grid from '@material-ui/core/Grid'; 3 | import Title from 'components/Dashboard/Title'; 4 | import Paper from '@material-ui/core/Paper'; 5 | import Card from '@material-ui/core/Card'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import makeStyles from '@material-ui/core/styles/makeStyles'; 8 | import useTheme from '@material-ui/core/styles/useTheme'; 9 | import useDataStore from '../data/dataStore'; 10 | import GitHubIcon from '@material-ui/icons/GitHub'; 11 | import ButtonBase from '@material-ui/core/ButtonBase'; 12 | import Divider from '@material-ui/core/Divider'; 13 | import clsx from 'clsx'; 14 | import { GLOBAL_PAPER_OPACITY } from '../utils/consts'; 15 | import { useLocation } from 'react-router-dom'; 16 | import { 17 | FacebookShareButton, 18 | FacebookIcon, 19 | LinkedinShareButton, 20 | LinkedinIcon, 21 | WhatsappShareButton, 22 | WhatsappIcon, 23 | } from 'react-share'; 24 | 25 | const useStyles = makeStyles((theme) => ({ 26 | paper: { 27 | opacity: `${GLOBAL_PAPER_OPACITY} !important`, 28 | padding: theme.spacing(2), 29 | display: 'flex', 30 | overflow: 'visible', 31 | flexDirection: 'column', 32 | '&.btn': { 33 | flexDirection: 'row-reverse', 34 | justifyContent: 'space-around', 35 | paddingLeft: '25%', 36 | paddingRight: '25%', 37 | }, 38 | }, 39 | shareButton: { 40 | opacity: 1, 41 | '&:hover': { 42 | color: `${theme.palette.secondary.main} !important`, 43 | }, 44 | }, 45 | })); 46 | const Share = () => { 47 | const classes = useStyles(); 48 | const theme = useTheme(); 49 | const dataStore = useDataStore(); 50 | const location = useLocation(); 51 | if (!dataStore.ready) { 52 | return null; 53 | } 54 | return ( 55 | 56 | 57 | {/* */} 58 | 68 | Share: 69 |
70 | 71 | 75 | 76 | 77 | 78 | 79 | 80 | 84 | 85 | 86 | 87 | 88 | 89 | 93 | 94 | 95 | 96 |
97 |
98 | {/*
*/} 99 |
100 | 101 | 102 | { 110 | window.location.assign( 111 | `https://github.com/m3h0w/covid19-coronavirus-react-visualization` 112 | ); 113 | }} 114 | > 115 | Contribute 116 | 117 | 118 | 119 | 120 | 121 | {/* Empty Grid items because I couldn't find a way to leave space at the end. Needs to be fixed */} 122 | 123 | 124 | 125 | ); 126 | }; 127 | 128 | export default Share; 129 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 35 | COVID19.PINK 36 | 37 | 38 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | 134 |
135 | 136 | 137 | -------------------------------------------------------------------------------- /src/components/MapChart.tsx: -------------------------------------------------------------------------------- 1 | import { scaleLog } from 'd3-scale'; 2 | import { observer } from 'mobx-react-lite'; 3 | import { useStateAndLocalStorage } from 'persistence-hooks'; 4 | import React from 'react'; 5 | import { useHistory } from 'react-router'; 6 | import { ComposableMap, Geographies, Geography, Graticule, ZoomableGroup } from 'react-simple-maps'; 7 | 8 | import { Fade, Theme } from '@material-ui/core'; 9 | import { useTheme } from '@material-ui/core/styles'; 10 | 11 | import useDataStore, { DataStore } from '../data/dataStore'; 12 | import geoUrl from '../data/worldMap.json'; 13 | import { smUp } from '../utils/breakpoints'; 14 | import { showInfoSnackBar } from './Snackbar'; 15 | import { DataType } from '../pages/Map'; 16 | import numberWithCommas from 'utils/numberWithCommas'; 17 | 18 | const getMatchingCountryKey = (dataStore: DataStore, geo) => { 19 | for (const key of Object.keys(geo.properties)) { 20 | const countryName = geo.properties[key]; 21 | if (dataStore.possibleCountries.includes(countryName)) { 22 | return countryName; 23 | } 24 | } 25 | return undefined; 26 | }; 27 | 28 | export const getColorsScale = (dataType: DataType, theme: Theme) => { 29 | switch (dataType) { 30 | case 'confirmed': 31 | return scaleLog() 32 | .domain([1, 30000, 500000, 1000000]) 33 | .range(['#F2EAEA', '#FA4590', '#611835', '#000000']); 34 | case 'dead': 35 | return scaleLog().domain([1, 40000, 100000]).range(['#F2EAEA', '#222', '#000000']); 36 | } 37 | }; 38 | 39 | const MapChart = observer( 40 | ({ 41 | date, 42 | setTooltipContent, 43 | dataType, 44 | }: { 45 | date: string; 46 | setTooltipContent: (content: string) => void; 47 | dataType: 'confirmed' | 'dead'; 48 | }) => { 49 | const theme = useTheme(); 50 | const dataStore = useDataStore(); 51 | const history = useHistory(); 52 | const colorScale = getColorsScale(dataType, theme); 53 | const [shownSnackbar, setShownSnackbar] = useStateAndLocalStorage( 54 | false, 55 | 'shownMapClickSnackbar' 56 | ); 57 | const routeChange = (country: string) => { 58 | history.push(`/dashboard?country=${country}`); 59 | }; 60 | 61 | return ( 62 | 63 | 64 | 65 | {/* {dataStore.ready && ( */} 66 | 67 | 68 | {({ geographies }) => 69 | geographies.map((geo) => { 70 | const countryKey = getMatchingCountryKey(dataStore, geo); 71 | const d = dataStore.getCountryData(countryKey); 72 | return ( 73 | { 77 | // const { NAME, POP_EST } = geo.properties; 78 | const { NAME } = geo.properties; 79 | if (d && d[dataType] && d[dataType][date]) { 80 | setTooltipContent(`${NAME} — ${numberWithCommas(d[dataType][date])}`); 81 | } else { 82 | setTooltipContent(`${NAME} — 0 ${dataType}`); 83 | } 84 | 85 | if (!shownSnackbar && smUp()) { 86 | showInfoSnackBar( 87 | 'Click on a country to go directly to its dashboard 💨', 88 | 3000 89 | ); 90 | setShownSnackbar(true); 91 | } 92 | }} 93 | onMouseLeave={() => { 94 | setTooltipContent(''); 95 | }} 96 | onClick={ 97 | smUp() 98 | ? () => { 99 | routeChange(countryKey); 100 | } 101 | : undefined 102 | } 103 | style={{ 104 | default: { 105 | transition: 'fill 0.6s linear', 106 | fill: 107 | d && d[dataType] && d[dataType][date] 108 | ? colorScale(d[dataType][date]) 109 | : '#F4EEEE', 110 | 111 | outline: 'none', 112 | }, 113 | hover: { 114 | fill: theme.palette.secondary.main, 115 | outline: 'none', 116 | cursor: 'pointer', 117 | }, 118 | pressed: { 119 | fill: theme.palette.secondary.dark, 120 | outline: 'none', 121 | }, 122 | }} 123 | /> 124 | ); 125 | }) 126 | } 127 | 128 | 129 | {/* )} */} 130 | 131 | 132 | ); 133 | } 134 | ); 135 | 136 | export default MapChart; 137 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/components/MultiChart.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from 'mobx-react-lite'; 2 | import React, { FC } from 'react'; 3 | import { CartesianGrid, Line, LineChart, ResponsiveContainer, XAxis } from 'recharts'; 4 | import { useTheme } from '@material-ui/core/styles'; 5 | import { BooleanParam, useQueryParam, withDefault } from 'use-query-params'; 6 | import { xsDown } from '../utils/breakpoints'; 7 | import getBrush from './Dashboard/Brush'; 8 | import Title from './Dashboard/Title'; 9 | import getTooltip from './Dashboard/Tooltip'; 10 | import getYAxis from './Dashboard/YAxis'; 11 | import useDataStore, { getCapitaScaleString } from '../data/dataStore'; 12 | import { Typography } from '@material-ui/core'; 13 | 14 | export type Row = { 15 | [key in Column]: string; 16 | }; 17 | 18 | type Column = 'Province/State' | 'Country/Region' | 'Lat' | 'Long' | string; 19 | 20 | interface IProps { 21 | title: string; 22 | yLabel: string; 23 | countries: string[]; 24 | colors: { [country: string]: string }; 25 | syncId: string; 26 | dataType: 'confirmed' | 'dead'; 27 | logScale: boolean; 28 | perCapita: boolean; 29 | } 30 | 31 | const MultiChart: FC = observer( 32 | ({ title, yLabel, countries, dataType, colors, syncId, logScale, perCapita }) => { 33 | const theme = useTheme(); 34 | const dataStore = useDataStore(); 35 | const data = dataStore.dataForAfter100Cases(dataType, countries); 36 | 37 | const CustomizedDot = (props: any) => { 38 | const { cx, cy, stroke, payload, lastX, country } = props; 39 | if (payload.time === lastX) { 40 | return ( 41 | <> 42 | 43 | 51 | 52 | 60 | 66 | {country} 67 | 68 | 69 | 70 | ); 71 | } 72 | 73 | return
; 74 | }; 75 | 76 | const getFormattedLine = (dot: boolean = true) => { 77 | if (!data) { 78 | return null; 79 | } 80 | 81 | return countries.map((country: string, i: number) => { 82 | const values = Object.values(data.map((el) => el[country])).filter((v) => v !== undefined); 83 | const times = Object.values(data.map((el) => el.time)); 84 | return ( 85 | } 93 | strokeWidth={1.5} 94 | opacity={0.8} 95 | /> 96 | ); 97 | }); 98 | }; 99 | 100 | const brush = 101 | Boolean(countries.length) && 102 | data && 103 | getBrush({ 104 | data: data, 105 | color: theme.palette.text.secondary, 106 | dataKey: 'time', 107 | children: ( 108 | 109 | {getYAxis(yLabel, logScale, true)} 110 | {getFormattedLine(false)} 111 | 112 | ), 113 | }); 114 | 115 | return ( 116 | <> 117 |
127 | 128 | {title} {logScale ? (xsDown() ? '(log)' : '(logarithmic scale)') : null} 129 | 130 | 131 | {perCapita ? `per ${getCapitaScaleString()} inhabitants` : ''} 132 | 133 |
134 | 135 | {Boolean(countries.length) ? ( 136 | 146 | 147 | 162 | {getYAxis(yLabel, logScale)} 163 | {getFormattedLine(true)} 164 | {brush} 165 | {getTooltip((s) => `${s} days after the 100th case`)} 166 | 167 | ) : ( 168 |
169 | )} 170 |
171 | 172 | ); 173 | } 174 | ); 175 | 176 | // const TIME_FORMAT = 'MMM Do'; 177 | // const formatXAxis: TickFormatterFunction = (tickItem: number) => 178 | // moment(tickItem * 1000).format(TIME_FORMAT); 179 | 180 | export default MultiChart; 181 | -------------------------------------------------------------------------------- /src/data/whoDataStore.ts: -------------------------------------------------------------------------------- 1 | import { csv } from 'd3-request'; 2 | import { computed, observable } from 'mobx'; 3 | import { Moment } from 'moment'; 4 | import { createContext, useContext } from 'react'; 5 | 6 | import { Row } from '../components/MultiChart'; 7 | import confirmedGlobalCsvUrl from '../data/confirmed_global.csv'; 8 | import deathsGlobalCsvUrl from '../data/deaths_global.csv'; 9 | import { DataType } from '../pages/Map'; 10 | import { getDatesFromDataRowWho, momentToFormatLong } from '../utils/getDatesFromDataRow'; 11 | import continentArray from './continentsArray.json'; 12 | import { namesMap, swapName } from './utils'; 13 | 14 | const USE_LOCAL_DATA = true; 15 | 16 | interface ICountryData { 17 | confirmed: Row | undefined; 18 | dead: Row | undefined; 19 | } 20 | 21 | function isNumber(n) { 22 | return !isNaN(parseFloat(n)) && !isNaN(n - 0); 23 | } 24 | 25 | // _.groupBy 26 | function groupByContinent(arr) { 27 | let reducer = (grouped, item) => { 28 | // if (item[COUNTRY_KEY] === 'China') { 29 | // if (item[STATE_KEY]) { 30 | // return grouped; 31 | // } 32 | // } else { 33 | // if (excludedProviceStateValues.includes(item[STATE_KEY])) { 34 | // return grouped; 35 | // } 36 | // if (excludedCountryRegionValues.includes(item[COUNTRY_KEY]) || !item[COUNTRY_KEY]) { 37 | // return grouped; 38 | // } 39 | // } 40 | let country = item[COUNTRY_KEY]; 41 | if (Object.keys(namesMap).includes(country)) { 42 | country = swapName(country.replace('*', '')); 43 | } 44 | let group_value = continentArray.find((v) => v.country === country.replace('*', ''))?.continent; 45 | if (!group_value) { 46 | // group_value = 'Other'; 47 | return grouped; 48 | } 49 | if (!grouped[group_value]) { 50 | grouped[group_value] = {}; 51 | } 52 | 53 | Object.keys(item).forEach((rowKey) => { 54 | let v = item[rowKey]; 55 | if (!v) { 56 | return grouped; 57 | } 58 | if (!grouped[group_value][rowKey]) { 59 | grouped[group_value][rowKey] = 0; 60 | } 61 | if (v && isNumber(v)) { 62 | grouped[group_value][rowKey] += parseFloat(v); 63 | } else { 64 | if (Object.keys(namesMap).includes(v)) { 65 | v = swapName(v); 66 | } 67 | grouped[group_value][rowKey] = v; 68 | } 69 | }); 70 | 71 | return grouped; 72 | }; 73 | return arr.reduce(reducer, {}); 74 | } 75 | 76 | type GroupedData = { [countryName: string]: Row }; 77 | 78 | const COUNTRY_KEY = 'Country/Region'; 79 | // const STATE_KEY = 'Province/States'; 80 | // const WHO_REGION_KEY = 'WHO region'; 81 | 82 | export class WhoDataStore { 83 | @observable public whoCasesData: any | undefined = undefined; 84 | @observable public whoDeathsData: any | undefined = undefined; 85 | 86 | constructor() { 87 | if (USE_LOCAL_DATA) { 88 | csv(confirmedGlobalCsvUrl, (err, data: any) => { 89 | if (data) { 90 | this.whoCasesData = groupByContinent(data); 91 | } else { 92 | throw new Error(`Data wasn't loaded correctly`); 93 | } 94 | }); 95 | csv(deathsGlobalCsvUrl, (err, data: any) => { 96 | if (data) { 97 | this.whoDeathsData = groupByContinent(data); 98 | } else { 99 | throw new Error(`Data wasn't loaded correctly`); 100 | } 101 | }); 102 | } 103 | } 104 | 105 | public getDataArrayWithTime(dataType: DataType, length?: number) { 106 | if (!this.ready) { 107 | return; 108 | } 109 | if (dataType === 'confirmed') { 110 | return this.casesDataArrayWithTime?.slice( 111 | 0, 112 | length ? length + 1 : this.casesDataArrayWithTime.length + 1 113 | ); 114 | } 115 | if (dataType === 'dead') { 116 | return this.deathsDataArrayWithTime?.slice( 117 | 0, 118 | length ? length + 1 : this.deathsDataArrayWithTime.length + 1 119 | ); 120 | } 121 | return; 122 | } 123 | 124 | @computed get deathsDataArrayWithTime() { 125 | return this.dates?.map((date: Moment, i: number) => { 126 | const d: { [key: string]: string | number } = { 127 | time: date.unix(), 128 | number: i, 129 | }; 130 | this.possibleRegions?.forEach((region) => { 131 | const value = this.whoDeathsData[region][momentToFormatLong(date)]; 132 | d[region] = value; 133 | }); 134 | return d; 135 | }); 136 | } 137 | 138 | @computed get casesDataArrayWithTime() { 139 | return this.dates?.map((date: Moment, i: number) => { 140 | const d: { [key: string]: string | number } = { 141 | time: date.unix(), 142 | number: i, 143 | }; 144 | this.possibleRegions?.forEach((region) => { 145 | const value = this.whoCasesData[region][momentToFormatLong(date)]; 146 | d[region] = value; 147 | }); 148 | return d; 149 | }); 150 | } 151 | 152 | @computed get dates() { 153 | if (this.ready && this.whoCasesData) { 154 | for (const region of Object.keys(this.whoCasesData)) { 155 | const row = this.whoCasesData[region]; 156 | const dates = getDatesFromDataRowWho(row); 157 | if (dates) { 158 | return dates; 159 | } 160 | } 161 | } 162 | return undefined; 163 | } 164 | 165 | @computed get datesConverted() { 166 | if (this.dates) { 167 | return this.dates.map(momentToFormatLong); 168 | } 169 | 170 | return undefined; 171 | } 172 | 173 | @computed get possibleRegions() { 174 | if (!this.ready) { 175 | return undefined; 176 | } 177 | return Object.keys(this.whoCasesData); 178 | } 179 | 180 | @computed get ready() { 181 | return Boolean(this.whoCasesData); 182 | } 183 | } 184 | 185 | const getStore = () => new WhoDataStore(); 186 | 187 | let whoDataStore: WhoDataStore | undefined = getStore(); 188 | let whoDataStoreContext = createContext(whoDataStore); 189 | 190 | export const clearWhoDataStore = () => { 191 | whoDataStore = getStore(); 192 | whoDataStoreContext = createContext(whoDataStore); 193 | }; 194 | 195 | const useWhoDataStore = () => useContext(whoDataStoreContext); 196 | export default useWhoDataStore; 197 | -------------------------------------------------------------------------------- /src/components/UsaMapChart.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react'; 2 | import { geoCentroid } from 'd3-geo'; 3 | import { 4 | ComposableMap, 5 | Geographies, 6 | Geography, 7 | Marker, 8 | Annotation, 9 | Graticule, 10 | } from 'react-simple-maps'; 11 | import geoUrl from 'data/statesMap.json'; 12 | import allStates from 'data/allstates.json'; 13 | import { DataStore } from '../data/dataStore'; 14 | import { useTheme } from '@material-ui/core/styles'; 15 | import useDataStore from '../data/dataStore'; 16 | import { observer } from 'mobx-react-lite'; 17 | import { CSSProperties } from '@material-ui/core/styles/withStyles'; 18 | import { Fade } from '@material-ui/core'; 19 | import { getColorsScale } from './MapChart'; 20 | 21 | const offsets = { 22 | VT: [50, -8], 23 | NH: [34, 2], 24 | MA: [30, -1], 25 | RI: [28, 2], 26 | CT: [35, 10], 27 | NJ: [34, 1], 28 | DE: [33, 0], 29 | MD: [47, 10], 30 | DC: [49, 21], 31 | }; 32 | 33 | const getMatchingStateKey = (dataStore: DataStore, geo) => { 34 | for (const key of Object.keys(geo.properties)) { 35 | const regionName = geo.properties[key]; 36 | if (dataStore.possibleRegions.includes(regionName)) { 37 | return regionName; 38 | } 39 | } 40 | return undefined; 41 | }; 42 | 43 | const UsaMapChart = observer( 44 | ({ 45 | date, 46 | dataType, 47 | style, 48 | onClick, 49 | selectedRegion, 50 | }: { 51 | date: string; 52 | setTooltipContent: (content: string) => void; 53 | dataType: 'confirmed' | 'dead'; 54 | style: CSSProperties; 55 | onClick: () => void; 56 | selectedRegion: string; 57 | }) => { 58 | const theme = useTheme(); 59 | const dataStore = useDataStore(); 60 | const colorScale = getColorsScale(dataType, theme); 61 | 62 | const getDefaultFill = useCallback( 63 | (stateKey) => { 64 | if (selectedRegion === stateKey) { 65 | return theme.palette.secondary.main; 66 | } 67 | 68 | const d = dataStore.getRegionData(stateKey); 69 | return d && d[dataType] && d[dataType][date] ? colorScale(d[dataType][date]) : '#F4EEEE'; 70 | }, 71 | [selectedRegion, dataStore, dataType, date, colorScale, theme.palette.secondary.main] 72 | ); 73 | 74 | return ( 75 | 76 | 77 | 78 | 79 | {({ geographies }) => ( 80 | <> 81 | {geographies.map((geo) => { 82 | const stateKey = getMatchingStateKey(dataStore, geo); 83 | if (!stateKey) { 84 | return null; 85 | } 86 | const d = dataStore.getRegionData(stateKey); 87 | // console.log({ stateKey }); 88 | // console.log({ d }); 89 | return ( 90 | { 94 | // const { NAME, POP_EST } = geo.properties; 95 | // console.log({ NAME }); 96 | if (d && d[dataType] && d[dataType][date]) { 97 | // setTooltipContent(`${stateKey} — ${d[dataType][date]} ${dataType}`); 98 | } else { 99 | // setTooltipContent(`${stateKey} — 0 ${dataType}`); 100 | } 101 | 102 | // if (!shownSnackbar) { 103 | // showInfoSnackBar( 104 | // 'Click on a country to go directly to its dashboard 💨', 105 | // 3000 106 | // ); 107 | // setShownSnackbar(true); 108 | // } 109 | }} 110 | onMouseLeave={() => { 111 | // setTooltipContent(''); 112 | }} 113 | onClick={() => onClick(stateKey)} 114 | style={{ 115 | default: { 116 | transition: 'fill 0.6s linear', 117 | fill: getDefaultFill(stateKey), 118 | outline: 'none', 119 | }, 120 | hover: { 121 | fill: theme.palette.secondary.light, 122 | outline: 'none', 123 | cursor: 'pointer', 124 | }, 125 | pressed: { 126 | fill: theme.palette.secondary.dark, 127 | outline: 'none', 128 | }, 129 | }} 130 | /> 131 | ); 132 | })} 133 | {geographies.map((geo) => { 134 | const centroid = geoCentroid(geo); 135 | const cur = allStates.find((s) => s.val === geo.id); 136 | return ( 137 | 138 | {cur && 139 | centroid[0] > -160 && 140 | centroid[0] < -67 && 141 | (Object.keys(offsets).indexOf(cur.id) === -1 ? ( 142 | 143 | 144 | {cur.id} 145 | 146 | 147 | ) : ( 148 | 153 | 154 | {cur.id} 155 | 156 | 157 | ))} 158 | 159 | ); 160 | })} 161 | 162 | )} 163 | 164 | 165 | 166 | ); 167 | } 168 | ); 169 | 170 | export default UsaMapChart; 171 | -------------------------------------------------------------------------------- /src/components/Dashboard/Chart.tsx: -------------------------------------------------------------------------------- 1 | import moment, { Moment } from 'moment'; 2 | import React, { FC, useEffect, useState, ReactText } from 'react'; 3 | import ReactCountryFlag from 'react-country-flag'; 4 | import { 5 | Bar, 6 | Line, 7 | CartesianGrid, 8 | ResponsiveContainer, 9 | TickFormatterFunction, 10 | XAxis, 11 | YAxis, 12 | ComposedChart, 13 | } from 'recharts'; 14 | 15 | import { useTheme } from '@material-ui/core/styles'; 16 | import { BooleanParam, useQueryParam, withDefault } from 'use-query-params'; 17 | import countryToCode from '../../utils/countryToCode'; 18 | import { FIRST_DATE, momentToFormat } from '../../utils/getDatesFromDataRow'; 19 | import getBrush from './Brush'; 20 | import Title from './Title'; 21 | import getTooltip from './Tooltip'; 22 | import getYAxis from './YAxis'; 23 | import numberWithCommas from '../../utils/numberWithCommas'; 24 | import useDataStore from '../../data/dataStore'; 25 | import { getCapitaScaleString } from '../../data/dataStore'; 26 | import { Typography } from '@material-ui/core'; 27 | 28 | export type Row = { 29 | [key in Column]: string; 30 | }; 31 | 32 | type Column = 'Province/State' | 'Country/Region' | 'Lat' | 'Long' | string; 33 | 34 | interface IProps { 35 | rowData: { confirmed: Row; dead: Row }; 36 | dates: Moment[]; 37 | showingDataFor: string; 38 | } 39 | 40 | const Chart: FC = ({ rowData, dates, showingDataFor }) => { 41 | const theme = useTheme(); 42 | // const [firstCaseDate, setFirstCaseDate] = useState(); 43 | const [data, setData] = useState(); 44 | const dataStore = useDataStore(); 45 | const [perCapita, setPerCapita] = useQueryParam( 46 | 'per_capita', 47 | withDefault(BooleanParam, false) 48 | ); 49 | 50 | useEffect(() => { 51 | if (rowData && rowData.confirmed && rowData.dead) { 52 | let lastZeroDay: Moment | undefined = moment(FIRST_DATE); 53 | const d = dates 54 | .map((date) => { 55 | const confirmedCases = Number(rowData.confirmed[momentToFormat(date)]); 56 | let deaths = Number(rowData.dead[momentToFormat(date)]); 57 | if (deaths === undefined || deaths === null) { 58 | deaths = 0; 59 | } 60 | let fatalityRate: number | undefined; 61 | if (!perCapita) { 62 | if (deaths === 0) { 63 | fatalityRate = 0; 64 | } else { 65 | fatalityRate = confirmedCases 66 | ? Math.round(((deaths / confirmedCases) * 100 + Number.EPSILON) * 100) / 100 67 | : undefined; 68 | } 69 | } 70 | 71 | if (lastZeroDay?.isSame(moment(FIRST_DATE)) && confirmedCases > 0) { 72 | lastZeroDay = date.clone(); 73 | lastZeroDay.subtract(2, 'days'); 74 | } 75 | return { 76 | confirmedCases, 77 | deaths, 78 | fatalityRate, 79 | time: date.unix(), 80 | }; 81 | }) 82 | .filter((el) => { 83 | return moment(el.time * 1000).isAfter(lastZeroDay); 84 | }); 85 | setData(d); 86 | // setFirstCaseDate(lastZeroDay); 87 | } 88 | }, [rowData, dates, perCapita]); 89 | 90 | const getFormattedLine = ( 91 | dataKey, 92 | name, 93 | stroke?: string, 94 | dot: boolean = true, 95 | yAxisId: 'left' | 'right' = 'left' 96 | ) => ( 97 | 106 | ); 107 | 108 | // const lines = [ 109 | // getFormattedLine('confirmedCases', 'Confirmed cases'), 110 | // getFormattedLine('deaths', 'Deaths', '#000'), 111 | // ]; 112 | 113 | const bars = [ 114 | , 121 | , 122 | ]; 123 | 124 | const brush = getBrush({ 125 | data, 126 | color: theme.palette.text.secondary, 127 | tickFormatter: formatXAxis, 128 | dataKey: 'time', 129 | children: ( 130 | 131 | 132 | {bars} 133 | 134 | ), 135 | }); 136 | 137 | if (!data) { 138 | return null; 139 | } 140 | 141 | const cc = countryToCode(showingDataFor); 142 | 143 | return ( 144 | <> 145 | 146 | {`Cases & Deaths`} 147 | {showingDataFor && `: ${showingDataFor} `} 148 | {cc && <ReactCountryFlag countryCode={cc} svg style={{ marginTop: -5 }} />} 149 | 150 | 151 | {perCapita ? ` per ${getCapitaScaleString()} inhabitants` : ''} 152 | 153 | 154 | 163 | 164 | 170 | {getYAxis('No. of cases & deaths', undefined, undefined, undefined, undefined, false)} 171 | {!perCapita && 172 | getYAxis('Case fatality rate [%]', false, undefined, 'right', false, false, [ 173 | 0, 174 | (v: number) => Math.ceil(v * 1.1), 175 | ])} 176 | {/* {lines} */} 177 | {bars} 178 | {!perCapita && 179 | getFormattedLine( 180 | 'fatalityRate', 181 | 'Case fatality rate', 182 | theme.palette.secondary.main, 183 | false, 184 | 'right' 185 | )} 186 | {brush} 187 | {getTooltip(formatXAxis, (value: string | number | ReactText[], name: string) => { 188 | if (value === undefined || value === null) { 189 | return '-'; 190 | } 191 | if (name === 'Case fatality rate') { 192 | return `${value}%`; 193 | } 194 | return numberWithCommas(value); 195 | })} 196 | 197 | 198 | 199 | ); 200 | }; 201 | 202 | const TIME_FORMAT = 'MMM Do'; 203 | const formatXAxis: TickFormatterFunction = (tickItem: number) => 204 | moment(tickItem * 1000).format(TIME_FORMAT); 205 | 206 | export default Chart; 207 | -------------------------------------------------------------------------------- /src/components/Dashboard/ListItems.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import ListItem from '@material-ui/core/ListItem'; 3 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 4 | import ListItemText from '@material-ui/core/ListItemText'; 5 | import ListSubheader from '@material-ui/core/ListSubheader'; 6 | import DashboardIcon from '@material-ui/icons/Dashboard'; 7 | import AssignmentIcon from '@material-ui/icons/Assignment'; 8 | import { Link as RouterLink, useLocation } from 'react-router-dom'; 9 | import { useTheme, makeStyles } from '@material-ui/core/styles'; 10 | import { withStyles, createStyles, Hidden, SvgIconProps } from '@material-ui/core'; 11 | import GitHubIcon from '@material-ui/icons/GitHub'; 12 | import Link from '@material-ui/core/Link'; 13 | import CallMadeIcon from '@material-ui/icons/CallMade'; 14 | import { CSSProperties } from '@material-ui/core/styles/withStyles'; 15 | import List from '@material-ui/core/List'; 16 | import Divider from '@material-ui/core/Divider'; 17 | import TrendingUpIcon from '@material-ui/icons/TrendingUp'; 18 | import PublicIcon from '@material-ui/icons/Public'; 19 | import { 20 | FacebookShareButton, 21 | // FacebookIcon, 22 | LinkedinShareButton, 23 | // LinkedinIcon, 24 | WhatsappShareButton, 25 | // WhatsappIcon, 26 | } from 'react-share'; 27 | import FacebookIcon from '@material-ui/icons/Facebook'; 28 | import WhatsappIcon from '@material-ui/icons/WhatsApp'; 29 | import LinkedinIcon from '@material-ui/icons/LinkedIn'; 30 | import Typography from '@material-ui/core/Typography'; 31 | 32 | const StyledListItemIcon = withStyles((theme) => ({ 33 | root: { minWidth: '45px' }, 34 | }))(ListItemIcon); 35 | 36 | interface ICustomListItemProps { 37 | to?: string; 38 | href?: string; 39 | text: string; 40 | Icon: (p: SvgIconProps) => JSX.Element; 41 | AfterIcon?: (p: SvgIconProps) => JSX.Element; 42 | style?: CSSProperties; 43 | } 44 | 45 | interface IStyleProps { 46 | isSelected: boolean; 47 | style?: CSSProperties; 48 | } 49 | 50 | const useStyles = makeStyles((theme) => 51 | createStyles({ 52 | root: (props: IStyleProps) => ({ 53 | textDecoration: props.isSelected ? 'underline' : 'initial', 54 | textDecorationColor: theme.palette.secondary.main, 55 | '&:hover': { 56 | background: `${theme.palette.grey['200']} !important`, 57 | textDecoration: 'underline', 58 | textDecorationColor: theme.palette.grey['500'], 59 | }, 60 | ...props.style, 61 | }), 62 | facebookShareButton: { 63 | display: 'flex', 64 | marginTop: '8px', 65 | // marginRight: '10px', 66 | opacity: 1, 67 | '&:hover': { 68 | color: `${theme.palette.secondary.main} !important`, 69 | }, 70 | }, 71 | }) 72 | ); 73 | 74 | const CustomListItem: FC = ({ to, href, text, Icon, AfterIcon, style }) => { 75 | const location = useLocation(); 76 | const theme = useTheme(); 77 | const isSelected = to ? location.pathname.split('/')[1] === to.split('/')[1] : false; 78 | const classes = useStyles({ isSelected, style }); 79 | 80 | let LinkElement: any = RouterLink; 81 | if (href) { 82 | LinkElement = Link; 83 | } 84 | 85 | return ( 86 | 96 | 97 | 100 | 101 | 102 | {AfterIcon && } 103 | 104 | ); 105 | }; 106 | 107 | export const MainListItems = () => { 108 | const classes = useStyles({ isSelected: false, style: {} }); 109 | const location = useLocation(); 110 | return ( 111 | 112 | 113 | 114 | 119 | 120 | 121 |
129 | SHARE 130 | 134 | 142 | 143 | 147 | 148 | 149 | 153 | 154 | 155 |
156 |
157 | 158 | 159 | 166 |
167 | ); 168 | }; 169 | 170 | export const secondaryListItems = ( 171 |
172 | Saved reports 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 |
192 | ); 193 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Dashboard from 'components/Dashboard/Dashboard'; 3 | import { observer } from 'mobx-react-lite'; 4 | import React, { FC, useCallback, useEffect, useState } from 'react'; 5 | import { RouteComponentProps, useHistory } from 'react-router'; 6 | import ReactTooltip from 'react-tooltip'; 7 | import { 8 | ButtonBase, 9 | Card, 10 | Grow, 11 | Hidden, 12 | Slide, 13 | Table, 14 | TableBody, 15 | TableCell, 16 | TableHead, 17 | TableRow, 18 | Typography, 19 | } from '@material-ui/core'; 20 | import Grid from '@material-ui/core/Grid'; 21 | import Paper from '@material-ui/core/Paper'; 22 | import { makeStyles, useTheme } from '@material-ui/core/styles'; 23 | import Collapsable from '../components/Collapsable'; 24 | import Chart from '../components/Dashboard/Chart'; 25 | import CurrentCount from '../components/Dashboard/CurrentCount'; 26 | import { CustomAutocomplete } from '../components/Dashboard/Select'; 27 | import UsaMapChart from '../components/UsaMapChart'; 28 | import useDataStore from '../data/dataStore'; 29 | import { animationTime, GLOBAL_PAPER_OPACITY, US_NAME } from '../utils/consts'; 30 | import last from '../utils/last'; 31 | import createPersistedState from '../utils/memoryState'; 32 | import numberWithCommas from '../utils/numberWithCommas'; 33 | import { useQueryParam, withDefault, StringParam, BooleanParam } from 'use-query-params'; 34 | 35 | const drawerWidth = 240; 36 | 37 | const useStyles = makeStyles((theme) => ({ 38 | root: { 39 | display: 'flex', 40 | }, 41 | toolbar: { 42 | paddingRight: 24, // keep right padding when drawer closed 43 | color: 'white', 44 | }, 45 | toolbarIcon: { 46 | display: 'flex', 47 | alignItems: 'center', 48 | justifyContent: 'flex-end', 49 | padding: '0 8px', 50 | ...theme.mixins.toolbar, 51 | }, 52 | appBar: { 53 | zIndex: theme.zIndex.drawer + 1, 54 | transition: theme.transitions.create(['width', 'margin'], { 55 | easing: theme.transitions.easing.sharp, 56 | duration: theme.transitions.duration.leavingScreen, 57 | }), 58 | }, 59 | appBarShift: { 60 | marginLeft: drawerWidth, 61 | width: `calc(100% - ${drawerWidth}px)`, 62 | transition: theme.transitions.create(['width', 'margin'], { 63 | easing: theme.transitions.easing.sharp, 64 | duration: theme.transitions.duration.enteringScreen, 65 | }), 66 | }, 67 | menuButton: { 68 | marginRight: 15, 69 | }, 70 | menuButtonHidden: { 71 | display: 'none', 72 | }, 73 | title: { 74 | flexGrow: 1, 75 | }, 76 | drawerPaper: { 77 | opacity: `${GLOBAL_PAPER_OPACITY} !important`, 78 | position: 'relative', 79 | whiteSpace: 'nowrap', 80 | width: drawerWidth, 81 | transition: theme.transitions.create('width', { 82 | easing: theme.transitions.easing.sharp, 83 | duration: theme.transitions.duration.enteringScreen, 84 | }), 85 | }, 86 | drawerPaperClose: { 87 | overflowX: 'hidden', 88 | transition: theme.transitions.create('width', { 89 | easing: theme.transitions.easing.sharp, 90 | duration: theme.transitions.duration.leavingScreen, 91 | }), 92 | width: theme.spacing(7), 93 | [theme.breakpoints.up('sm')]: { 94 | width: theme.spacing(9), 95 | }, 96 | }, 97 | appBarSpacer: theme.mixins.toolbar, 98 | content: { 99 | flexGrow: 1, 100 | height: '100vh', 101 | overflow: 'auto', 102 | display: 'flex', 103 | flexDirection: 'column', 104 | }, 105 | container: { 106 | paddingTop: theme.spacing(4), 107 | paddingBottom: theme.spacing(4), 108 | }, 109 | paper: { 110 | opacity: `${GLOBAL_PAPER_OPACITY} !important`, 111 | padding: theme.spacing(2), 112 | display: 'flex', 113 | overflow: 'visible', 114 | flexDirection: 'column', 115 | }, 116 | fixedHeight: { 117 | height: 400, 118 | maxHeight: '80vh', 119 | }, 120 | })); 121 | 122 | const DashboardPage: FC = observer((props) => { 123 | const classes = useStyles(); 124 | const [selectedCountry, setSelectedCountry] = useQueryParam( 125 | 'country', 126 | withDefault(StringParam, US_NAME) 127 | ); 128 | const [selectedRegion, setSelectedRegion] = useQueryParam( 129 | 'region', 130 | withDefault(StringParam, '') 131 | ); 132 | const [perCapita, setPerCapita] = useQueryParam( 133 | 'per_capita', 134 | withDefault(BooleanParam, false) 135 | ); 136 | const dataStore = useDataStore(); 137 | const possibleCountries = dataStore.possibleCountries; 138 | const fixedHeightPaper = clsx(classes.paper, classes.fixedHeight); 139 | const history = useHistory(); 140 | const theme = useTheme(); 141 | const [tooltipContent, setTooltipContent] = useState(); 142 | 143 | if (perCapita && selectedRegion) { 144 | setSelectedRegion(''); 145 | } 146 | 147 | const selectCountry = useCallback( 148 | (country: string) => { 149 | if (possibleCountries.includes(country)) { 150 | setSelectedCountry(country); 151 | setSelectedRegion(''); 152 | } 153 | }, 154 | [setSelectedRegion, setSelectedCountry, possibleCountries] 155 | ); 156 | 157 | const selectRegion = (region: string) => { 158 | if (dataStore.possibleRegions.includes(region) || region === null) { 159 | setSelectedRegion(region); 160 | } 161 | }; 162 | 163 | let rowData = dataStore.getCountryData(selectedCountry); 164 | if (selectedRegion) { 165 | rowData = dataStore.getRegionData(selectedRegion); 166 | } 167 | 168 | const cases = 169 | (rowData && 170 | rowData.confirmed && 171 | (Object.values(rowData.confirmed)[Object.values(rowData.confirmed).length - 1] as number)) || 172 | 0; 173 | const deaths = 174 | (rowData && 175 | rowData.dead && 176 | (Object.values(rowData.dead)[Object.values(rowData.dead).length - 1] as number)) || 177 | 0; 178 | const mortalityRate = dataStore.fatalityRateArray 179 | ? last(dataStore.fatalityRateArray)[selectedCountry] 180 | : undefined; 181 | const isUs = selectedCountry === US_NAME; 182 | 183 | const possibleRegionsForSelectedCountry = dataStore.getPossibleRegionsByCountry( 184 | selectedCountry, 185 | true 186 | ); 187 | const hasRegions = Boolean(possibleRegionsForSelectedCountry.length); 188 | 189 | return ( 190 | 191 | 192 | 199 | 200 | { 203 | selectCountry(v); 204 | }} 205 | selectedValue={selectedCountry} 206 | possibleValues={dataStore.possibleCountriesSortedByCases} 207 | id={'select-country'} 208 | width={'auto'} 209 | /> 210 | {hasRegions && !perCapita ? ( 211 | 219 | ) : null} 220 | 221 | 222 | 223 |
224 | 225 | 226 |
227 | 228 | 229 | 230 | {rowData && rowData.confirmed && rowData.dead && ( 231 | 238 | )} 239 | {hasRegions && !perCapita ? ( 240 | 241 | 242 | 243 | 244 | Region / State 245 | Cases 246 | Deaths 247 | 248 | 249 | 250 | {possibleRegionsForSelectedCountry.map((region) => { 251 | return ( 252 | { 255 | selectRegion(region); 256 | }} 257 | style={{ cursor: 'pointer' }} 258 | > 259 | 260 | {region} 261 | 262 | 263 | {numberWithCommas(dataStore.getLastRegionCases(region))} 264 | 265 | 266 | {numberWithCommas(dataStore.getLastRegionDeaths(region))} 267 | 268 | 269 | ); 270 | })} 271 | 272 |
273 |
274 | ) : null} 275 |
276 |
277 | 278 | 279 | 280 | 281 | 286 | 287 | 288 |
289 | 290 | 291 | { 299 | history.push(`/infection-trajectories/${selectedCountry}`); 300 | }} 301 | > 302 | Compare infection trajectories 303 | 304 | 305 | 306 | 307 | {isUs && !perCapita && ( 308 | 309 | 310 | { 316 | selectRegion(stateKey); 317 | }} 318 | selectedRegion={selectedRegion} 319 | /> 320 | 325 | Confirmed cases 326 | 327 | 328 | {tooltipContent}{' '} 329 | 330 | )} 331 | {isUs && !perCapita && ( 332 | 333 | 334 | { 340 | selectRegion(stateKey); 341 | }} 342 | selectedRegion={selectedRegion} 343 | /> 344 | 349 | Deaths 350 | 351 | 352 | {tooltipContent} 353 | 354 | )} 355 | 356 | ); 357 | }); 358 | 359 | export default DashboardPage; 360 | -------------------------------------------------------------------------------- /src/pages/Comparison.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Dashboard from 'components/Dashboard/Dashboard'; 3 | import { reaction } from 'mobx'; 4 | import { observer } from 'mobx-react-lite'; 5 | import { useStateAndLocalStorage } from 'persistence-hooks'; 6 | import React, { FC, useCallback, useEffect, useState } from 'react'; 7 | import ReactCountryFlag from 'react-country-flag'; 8 | import { RouteComponentProps, useHistory } from 'react-router'; 9 | import { 10 | useQueryParam, 11 | NumberParam, 12 | StringParam, 13 | withDefault, 14 | ArrayParam, 15 | BooleanParam, 16 | DelimitedArrayParam, 17 | } from 'use-query-params'; 18 | 19 | import { Button, Fab, Grow, Slide } from '@material-ui/core'; 20 | import Grid from '@material-ui/core/Grid'; 21 | import Paper from '@material-ui/core/Paper'; 22 | import { makeStyles } from '@material-ui/core/styles'; 23 | 24 | import CustomChip from '../components/CustomChip'; 25 | import { Row } from '../components/Dashboard/Chart'; 26 | import { CustomAutocomplete } from '../components/Dashboard/Select'; 27 | import MultiChart from '../components/MultiChart'; 28 | import { showInfoSnackBar } from '../components/Snackbar'; 29 | import useDataStore from '../data/dataStore'; 30 | import { animationTime, GLOBAL_PAPER_OPACITY } from '../utils/consts'; 31 | import countryToCode from '../utils/countryToCode'; 32 | import generateNewColors from '../utils/generateNewColors'; 33 | import last from '../utils/last'; 34 | import createPersistedState from '../utils/memoryState'; 35 | import sort from '../utils/sort'; 36 | 37 | const drawerWidth = 240; 38 | 39 | const useStyles = makeStyles((theme) => ({ 40 | root: { 41 | display: 'flex', 42 | }, 43 | toolbar: { 44 | paddingRight: 24, // keep right padding when drawer closed 45 | color: 'white', 46 | }, 47 | toolbarIcon: { 48 | display: 'flex', 49 | alignItems: 'center', 50 | justifyContent: 'flex-end', 51 | padding: '0 8px', 52 | ...theme.mixins.toolbar, 53 | }, 54 | appBar: { 55 | zIndex: theme.zIndex.drawer + 1, 56 | transition: theme.transitions.create(['width', 'margin'], { 57 | easing: theme.transitions.easing.sharp, 58 | duration: theme.transitions.duration.leavingScreen, 59 | }), 60 | }, 61 | appBarShift: { 62 | marginLeft: drawerWidth, 63 | width: `calc(100% - ${drawerWidth}px)`, 64 | transition: theme.transitions.create(['width', 'margin'], { 65 | easing: theme.transitions.easing.sharp, 66 | duration: theme.transitions.duration.enteringScreen, 67 | }), 68 | }, 69 | menuButton: { 70 | marginRight: 15, 71 | }, 72 | menuButtonHidden: { 73 | display: 'none', 74 | }, 75 | title: { 76 | flexGrow: 1, 77 | }, 78 | drawerPaper: { 79 | opacity: `${GLOBAL_PAPER_OPACITY} !important`, 80 | position: 'relative', 81 | whiteSpace: 'nowrap', 82 | width: drawerWidth, 83 | transition: theme.transitions.create('width', { 84 | easing: theme.transitions.easing.sharp, 85 | duration: theme.transitions.duration.enteringScreen, 86 | }), 87 | }, 88 | drawerPaperClose: { 89 | overflowX: 'hidden', 90 | transition: theme.transitions.create('width', { 91 | easing: theme.transitions.easing.sharp, 92 | duration: theme.transitions.duration.leavingScreen, 93 | }), 94 | width: theme.spacing(7), 95 | [theme.breakpoints.up('sm')]: { 96 | width: theme.spacing(9), 97 | }, 98 | }, 99 | appBarSpacer: theme.mixins.toolbar, 100 | content: { 101 | flexGrow: 1, 102 | height: '100vh', 103 | overflow: 'auto', 104 | display: 'flex', 105 | flexDirection: 'column', 106 | }, 107 | container: { 108 | paddingTop: theme.spacing(4), 109 | paddingBottom: theme.spacing(4), 110 | }, 111 | paper: { 112 | opacity: `${GLOBAL_PAPER_OPACITY} !important`, 113 | padding: theme.spacing(2), 114 | display: 'flex', 115 | overflow: 'visible', 116 | flexDirection: 'column', 117 | }, 118 | fixedHeight: { 119 | height: '60vh', 120 | }, 121 | clipWrapper: { 122 | display: 'flex', 123 | maxWidth: '100%', 124 | flexWrap: 'wrap', 125 | }, 126 | logScaleSwitch: { 127 | [theme.breakpoints.down('sm')]: { 128 | fontSize: 8, 129 | }, 130 | }, 131 | })); 132 | 133 | interface IRowData { 134 | confirmed: Row | undefined; 135 | dead: Row | undefined; 136 | } 137 | const useMemoryStateA = createPersistedState(); 138 | 139 | const ComparisonPage: FC> = observer((props) => { 140 | const classes = useStyles(); 141 | const [selectedCountry, setSelectedCountry] = useState(null); 142 | const fixedHeightPaper = clsx(classes.paper, classes.fixedHeight); 143 | const dataStore = useDataStore(); 144 | const [colors, setColors] = useMemoryStateA<{ [country: string]: string }>({}); 145 | const [countries, setCountries] = useQueryParam( 146 | 'countries', 147 | withDefault(DelimitedArrayParam, []) 148 | ); 149 | const history = useHistory(); 150 | const [shownSnackbar, setShownSnackbar] = useStateAndLocalStorage(false, 'shownLogLinSnackbar'); 151 | const [logScale, setLogScale] = useQueryParam('log_scale', withDefault(BooleanParam, 0)); 152 | const [perCapita, setPerCapita] = useQueryParam( 153 | 'per_capita', 154 | withDefault(BooleanParam, false) 155 | ); 156 | 157 | useEffect(() => { 158 | if (!shownSnackbar && dataStore.ready) { 159 | showInfoSnackBar( 160 | 'Use the button on the top bar to switch between logarithmic and linear scales 🤓' 161 | ); 162 | setShownSnackbar(true); 163 | } 164 | }, [shownSnackbar, dataStore.ready, setShownSnackbar]); 165 | 166 | const getNewColors = useCallback(() => { 167 | const newColors = generateNewColors(countries.length); 168 | setColors( 169 | newColors.reduce((colorsObj, color, index) => { 170 | colorsObj[countries[index]] = color; 171 | return colorsObj; 172 | }, {}) 173 | ); 174 | }, [setColors, countries]); 175 | 176 | useEffect(() => { 177 | getNewColors(); 178 | }, [getNewColors, countries]); 179 | 180 | const resetGraph = () => { 181 | setTimeout(() => { 182 | setLogScale((prev) => !prev); 183 | setTimeout(() => { 184 | setLogScale((prev) => !prev); 185 | }, 10); 186 | }, 1); 187 | }; 188 | 189 | const addCountries = useCallback( 190 | (newCountries: string[]) => { 191 | resetGraph(); 192 | setCountries((prevCountries: string[]) => [...new Set([...newCountries, ...prevCountries])]); 193 | }, 194 | [setCountries] 195 | ); 196 | 197 | const addMostCasesCountries = useCallback(() => { 198 | const newCountries = dataStore.possibleCountriesSortedByCases.slice(0, 8); 199 | if ( 200 | newCountries.length !== countries.length || 201 | newCountries.some((value, index) => value !== countries[index]) 202 | ) { 203 | resetGraph(); 204 | setCountries(newCountries); 205 | setSelectedCountry(null); 206 | } 207 | }, [countries, setCountries, dataStore.possibleCountriesSortedByCases]); 208 | 209 | const addMostDeathsCountries = useCallback(() => { 210 | const newCountries = dataStore.possibleCountriesSortedByDeaths.slice(0, 8); 211 | if ( 212 | newCountries.length !== countries.length || 213 | newCountries.some((value, index) => value !== countries[index]) 214 | ) { 215 | resetGraph(); 216 | setCountries(newCountries); 217 | setSelectedCountry(null); 218 | } 219 | }, [countries, setCountries, dataStore.possibleCountriesSortedByDeaths]); 220 | 221 | useEffect(() => { 222 | const r = reaction( 223 | () => dataStore.ready, 224 | () => { 225 | if (!countries || countries.length === 0) { 226 | addMostCasesCountries(); 227 | } 228 | } 229 | ); 230 | 231 | return r; 232 | }, [countries]); 233 | 234 | const routeChange = (country: string) => { 235 | history.push(`/dashboard?country=${country}`); 236 | }; 237 | 238 | const LogScaleSwitch = () => { 239 | return ( 240 | { 242 | setLogScale(!logScale); 243 | }} 244 | variant='extended' 245 | size='small' 246 | color='primary' 247 | aria-label='add' 248 | className={classes.logScaleSwitch} 249 | > 250 | {logScale ? <>Scale: LOG. : <>Scale: LIN.} 251 | 252 | ); 253 | }; 254 | 255 | return ( 256 | 257 | 258 | 265 | 266 | { 269 | if (country) { 270 | addCountries([country]); 271 | } 272 | setSelectedCountry(null); 273 | }} 274 | selectedValue={selectedCountry} 275 | possibleValues={sort( 276 | dataStore.countriesWithOver100Cases, 277 | (a, b) => 278 | dataStore.getCountryData(b)?.confirmed[last(dataStore.datesConverted)] - 279 | dataStore.getCountryData(a)?.confirmed[last(dataStore.datesConverted)] 280 | ).filter((country) => !countries.includes(country))} 281 | id={'select-country'} 282 | width={'auto'} 283 | /> 284 | 295 | 306 | 317 | 318 | 319 | 320 | 321 | 322 | 323 |
324 | {dataStore.ready && 325 | countries.map((country: string, i: number) => { 326 | const cc = countryToCode(country); 327 | return ( 328 | 336 | ) : null 337 | } 338 | style={{ cursor: 'pointer' }} 339 | onClick={() => { 340 | routeChange(country); 341 | }} 342 | key={i} 343 | handleDelete={() => { 344 | setCountries(countries.filter((c) => c !== country)); 345 | resetGraph(); 346 | }} 347 | label={country} 348 | backgroundColor={colors[country]} 349 | /> 350 | ); 351 | })} 352 | {Boolean(countries.length) && ( 353 | { 358 | setCountries([]); 359 | }} 360 | handleDelete={() => { 361 | setCountries([]); 362 | }} 363 | /> 364 | )} 365 |
366 |
367 |
368 |
369 | 370 | 371 | 372 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 398 | 399 | 400 | 401 |
402 | ); 403 | }); 404 | 405 | export default ComparisonPage; 406 | -------------------------------------------------------------------------------- /src/components/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, FC } from 'react'; 2 | import clsx from 'clsx'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import CssBaseline from '@material-ui/core/CssBaseline'; 5 | import Drawer from '@material-ui/core/Drawer'; 6 | import AppBar from '@material-ui/core/AppBar'; 7 | import Toolbar from '@material-ui/core/Toolbar'; 8 | import Typography from '@material-ui/core/Typography'; 9 | import Divider from '@material-ui/core/Divider'; 10 | import IconButton from '@material-ui/core/IconButton'; 11 | import Container from '@material-ui/core/Container'; 12 | import Grid from '@material-ui/core/Grid'; 13 | import MenuIcon from '@material-ui/icons/Menu'; 14 | import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; 15 | import { MainListItems } from './ListItems'; 16 | import useDataStore from '../../data/dataStore'; 17 | import { Hidden, Fade, CircularProgress, Fab } from '@material-ui/core'; 18 | import backgroundSmoke from '../../assets/pinksmoke-min.jpg'; 19 | import backgroundSmokeMobile from '../../assets/pinksmoke-min.jpg'; 20 | import { GLOBAL_PAPER_OPACITY, SIDEBAR_WIDTH } from '../../utils/consts'; 21 | import logo from '../../assets/logo_square_white_transparent.png'; 22 | import { useLocation } from 'react-router-dom'; 23 | import BottomNavigation from 'components/BottomNavigationBar'; 24 | import { BooleanParam, useQueryParam, withDefault } from 'use-query-params'; 25 | import { FacebookShareButton, LinkedinShareButton, WhatsappShareButton } from 'react-share'; 26 | import FacebookIcon from '@material-ui/icons/Facebook'; 27 | import WhatsappIcon from '@material-ui/icons/WhatsApp'; 28 | import LinkedinIcon from '@material-ui/icons/LinkedIn'; 29 | import useWindowWidth from '../../utils/useWindowWidth'; 30 | 31 | const drawerWidth = 240; 32 | const toolbarHeight = 48; 33 | 34 | const useStyles = makeStyles((theme) => ({ 35 | root: { 36 | display: 'flex', 37 | position: 'relative', 38 | }, 39 | toolbar: { 40 | paddingRight: 24, // keep right padding when drawer closed 41 | paddingLeft: 16, 42 | [theme.breakpoints.down('xs')]: { 43 | paddingLeft: 6, 44 | paddingRight: 6, 45 | }, 46 | color: 'white', 47 | minHeight: toolbarHeight, 48 | }, 49 | toolbarIcon: { 50 | display: 'flex', 51 | alignItems: 'center', 52 | justifyContent: 'flex-end', 53 | padding: '0 8px', 54 | minHeight: toolbarHeight, 55 | // ...theme.mixins.toolbar, 56 | }, 57 | appBar: { 58 | background: 'linear-gradient(to right bottom, #fe217d, #fca2c0)', 59 | opacity: GLOBAL_PAPER_OPACITY, 60 | zIndex: theme.zIndex.drawer + 1, 61 | transition: theme.transitions.create(['width', 'margin'], { 62 | easing: theme.transitions.easing.sharp, 63 | duration: theme.transitions.duration.leavingScreen, 64 | }), 65 | }, 66 | appBarShift: { 67 | marginLeft: drawerWidth, 68 | width: `calc(100% - ${drawerWidth}px)`, 69 | transition: theme.transitions.create(['width', 'margin'], { 70 | easing: theme.transitions.easing.sharp, 71 | duration: theme.transitions.duration.enteringScreen, 72 | }), 73 | }, 74 | menuButton: { 75 | marginRight: 10, 76 | [theme.breakpoints.down('xs')]: { 77 | display: 'none', 78 | }, 79 | }, 80 | menuButtonHidden: { 81 | display: 'none', 82 | }, 83 | title: { 84 | flexGrow: 1, 85 | display: 'flex', 86 | flexDirection: 'row', 87 | alignItems: 'center', 88 | 89 | '& > *': { 90 | marginRight: 10, 91 | }, 92 | 93 | [theme.breakpoints.down('sm')]: { 94 | '& > h1,h2,h3,h4,h5': { 95 | fontSize: '0.8rem !important', 96 | }, 97 | }, 98 | }, 99 | drawerPaper: { 100 | opacity: `${GLOBAL_PAPER_OPACITY} !important`, 101 | position: 'relative', 102 | whiteSpace: 'nowrap', 103 | width: drawerWidth, 104 | transition: theme.transitions.create('width', { 105 | easing: theme.transitions.easing.sharp, 106 | duration: theme.transitions.duration.enteringScreen, 107 | }), 108 | }, 109 | drawerPaperClose: { 110 | opacity: `${GLOBAL_PAPER_OPACITY} !important`, 111 | overflowX: 'hidden', 112 | transition: theme.transitions.create('width', { 113 | easing: theme.transitions.easing.sharp, 114 | duration: theme.transitions.duration.leavingScreen, 115 | }), 116 | width: SIDEBAR_WIDTH, 117 | // [theme.breakpoints.up('sm')]: { 118 | // width: theme.spacing(7), 119 | // }, 120 | }, 121 | appBarSpacer: { 122 | minHeight: toolbarHeight, 123 | // ...theme.mixins.toolbar, 124 | }, 125 | content: { 126 | flexGrow: 1, 127 | height: '100vh', 128 | overflow: 'auto', 129 | overflowX: 'hidden', 130 | display: 'flex', 131 | flexDirection: 'column', 132 | backgroundColor: theme.palette.grey[100], 133 | backgroundSize: 'cover', 134 | }, 135 | container: { 136 | paddingTop: (props) => (props.paddingTop ? theme.spacing(2) : 0), 137 | paddingBottom: theme.spacing(8), 138 | [theme.breakpoints.down('xs')]: { paddingRight: '5px', paddingLeft: '5px' }, 139 | }, 140 | paper: { 141 | opacity: `${GLOBAL_PAPER_OPACITY} !important`, 142 | // padding: theme.spacing(2), 143 | display: 'flex', 144 | overflow: 'visible', 145 | flexDirection: 'column', 146 | }, 147 | fixedHeight: { 148 | height: 350, 149 | maxHeight: '80vh', 150 | }, 151 | hidden: { 152 | display: 'none', 153 | }, 154 | facebookShareButton: { 155 | display: 'flex', 156 | marginTop: '2px', 157 | marginRight: '10px', 158 | opacity: 1, 159 | '&:hover': { 160 | color: `${theme.palette.secondary.main} !important`, 161 | }, 162 | }, 163 | switch: { 164 | [theme.breakpoints.down('sm')]: { 165 | fontSize: 8, 166 | }, 167 | }, 168 | })); 169 | 170 | interface IProps { 171 | title: string; 172 | icon: FC; 173 | Icon: () => null; 174 | grid?: boolean; 175 | startOpen: boolean; 176 | paddingTop?: boolean; 177 | } 178 | 179 | const Dashboard: FC = ({ 180 | title, 181 | children, 182 | Icon = () => null, 183 | grid = true, 184 | startOpen = false, 185 | paddingTop = true, 186 | showPerCapitaSwitch = true, 187 | }) => { 188 | const classes = useStyles({ paddingTop }); 189 | const [open, setOpen] = useState(startOpen); 190 | const dataStore = useDataStore(); 191 | const location = useLocation(); 192 | const backgroundUrl = useWindowWidth() >= 650 ? backgroundSmoke : backgroundSmokeMobile; 193 | const [perCapita, setPerCapita] = useQueryParam( 194 | 'per_capita', 195 | withDefault(BooleanParam, false) 196 | ); 197 | 198 | const handleDrawerOpen = () => { 199 | setOpen(true); 200 | }; 201 | const handleDrawerClose = () => { 202 | setOpen(false); 203 | }; 204 | 205 | if (!showPerCapitaSwitch && perCapita) { 206 | setPerCapita(false); 207 | } 208 | 209 | // console.log(`https://covid19.pink${location.pathname}`); 210 | 211 | const PerCapitaSwitch = () => { 212 | return ( 213 | { 215 | setPerCapita((prev) => !prev); 216 | }} 217 | variant='extended' 218 | size='small' 219 | color='primary' 220 | aria-label='add' 221 | style={{ padding: '0 12px' }} 222 | className={classes.switch} 223 | > 224 | {perCapita ? ( 225 | <> 226 | {/* */} 227 | per capita 228 | 229 | ) : ( 230 | <> 231 | {/* */} 232 | absolute 233 | 234 | )} 235 | 236 | ); 237 | }; 238 | 239 | return ( 240 |
241 | 242 | 243 | 244 | 251 | 252 | 253 |
254 | 264 | covid19.pink logo 265 | 266 | 273 | COVID19.PINK 274 | 275 | 276 | 277 | 283 | 284 | {title} 285 | 286 | 287 | {showPerCapitaSwitch && } 288 |
289 | 290 | 291 |
292 | 293 | Share: 294 | 295 | 296 | Share: 297 | 298 | 302 | 303 | 304 | 308 | 309 | 310 | 314 | 315 | 316 | 322 |
323 |
324 | 325 | {dataStore.ready && ( 326 | 327 | Last updated:{' '} 328 | {dataStore.dates[dataStore.dates.length - 1] 329 | .clone() 330 | .add(1, 'day') 331 | .format('MMMM Do')} 332 | 333 | )} 334 | 335 | 336 | {dataStore.ready && ( 337 | 338 | Last updated:{' '} 339 | {dataStore.dates[dataStore.dates.length - 1] 340 | .clone() 341 | .add(1, 'day') 342 | .format('MMMM Do')} 343 | 344 | )} 345 | 346 |
347 |
348 | 349 | 356 |
357 | 358 | 359 | 360 |
361 | 362 | 363 |
364 |
365 |
366 | {!dataStore.ready && ( 367 |
376 | 377 |
378 | )} 379 | {grid 380 | ? dataStore.ready && ( 381 | <> 382 |
383 | 384 | 385 | 386 | {children} 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | ) 397 | : dataStore.ready && ( 398 | <> 399 |
400 | {children} 401 | 402 | )} 403 |
404 |
405 | ); 406 | }; 407 | 408 | export default Dashboard; 409 | -------------------------------------------------------------------------------- /src/data/dataStore.ts: -------------------------------------------------------------------------------- 1 | import { csv } from 'd3-request'; 2 | import stateNames from 'data/stateNames.json'; 3 | import { computed, observable } from 'mobx'; 4 | import { createContext, useContext } from 'react'; 5 | import last from 'utils/last'; 6 | 7 | import { Row } from '../components/MultiChart'; 8 | import confirmedGlobalCsvUrl from '../data/confirmed_global.csv'; 9 | import deathsGlobalCsvUrl from '../data/deaths_global.csv'; 10 | import { US_NAME } from '../utils/consts'; 11 | import { getDatesFromDataRow, momentToFormat } from '../utils/getDatesFromDataRow'; 12 | import sort from '../utils/sort'; 13 | import { namesMap, swapName } from './utils'; 14 | import countryByPopulation from '../data/countryByPopulation.json'; 15 | 16 | const USE_LOCAL_DATA = true; 17 | export const CAPITA_SCALE = 1000000; 18 | export const getCapitaScaleString = () => { 19 | switch (CAPITA_SCALE) { 20 | case 1000000: 21 | return '1 million'; 22 | default: 23 | break; 24 | } 25 | }; 26 | 27 | interface ICountryData { 28 | confirmed: Row | undefined; 29 | dead: Row | undefined; 30 | } 31 | 32 | function isNumber(n) { 33 | return !isNaN(parseFloat(n)) && !isNaN(n - 0); 34 | } 35 | 36 | function getPopulation(place: string) { 37 | const placeMap = { 38 | Czechia: 'Czech Republic', 39 | Russia: 'Russian Federation', 40 | 'Republic of the Congo': 'Congo', 41 | }; 42 | let placeName = place; 43 | if (placeName in placeMap) { 44 | placeName = placeMap[placeName]; 45 | } 46 | 47 | return parseFloat(countryByPopulation.find((v) => v.country === placeName)?.population); 48 | } 49 | 50 | // _.groupBy 51 | function groupBy(arr: any[], key: string, divide: boolean = false) { 52 | let reducer = (grouped: object, item: object) => { 53 | let group_value = item[key]; 54 | if ( 55 | item[STATE_KEY] === 'Recovered' || 56 | item[STATE_KEY] === 'Confirmed' || 57 | item[STATE_KEY] === 'Deaths' 58 | ) { 59 | return grouped; 60 | } 61 | 62 | if (group_value === US_NAME) { 63 | if (!Object.values(stateNames).includes(item[STATE_KEY])) { 64 | return grouped; 65 | } 66 | } 67 | 68 | group_value = swapName(group_value); 69 | if (!grouped[group_value]) { 70 | grouped[group_value] = {}; 71 | } 72 | 73 | let pop = 1; 74 | if (divide) { 75 | pop = getPopulation(group_value) / CAPITA_SCALE; 76 | if (!pop) { 77 | // console.log('no pop', group_value); 78 | return grouped; 79 | } 80 | } 81 | 82 | Object.keys(item).forEach((rowKey) => { 83 | let v = item[rowKey]; 84 | if (!grouped[group_value][rowKey]) { 85 | grouped[group_value][rowKey] = 0; 86 | } 87 | if (v && isNumber(v)) { 88 | if (group_value === US_NAME) { 89 | if (!Object.values(stateNames).includes(item[STATE_KEY])) { 90 | return grouped; 91 | } 92 | } 93 | grouped[group_value][rowKey] += parseFloat(v) / pop; 94 | } else { 95 | if (v && (typeof v === 'string' || v instanceof String)) { 96 | if (Object.keys(namesMap).includes(v)) { 97 | v = swapName(v); 98 | } 99 | grouped[group_value][rowKey] = v; 100 | } 101 | } 102 | }); 103 | 104 | return grouped; 105 | }; 106 | const res = arr.reduce(reducer, {}); 107 | if (!divide) { 108 | return res; 109 | } 110 | 111 | Object.keys(res).forEach((c) => { 112 | const v = res[c]; 113 | // console.log(c, v); 114 | Object.keys(v).forEach((z) => { 115 | const val = v[z]; 116 | if (val && isNumber(val)) { 117 | res[c][z] = Math.round(val); 118 | } 119 | }); 120 | }); 121 | 122 | return res; 123 | } 124 | 125 | type GroupedData = { [countryName: string]: Row }; 126 | 127 | export const COUNTRY_KEY = 'Country/Region'; 128 | export const STATE_KEY = 'Province/State'; 129 | 130 | export class DataStore { 131 | @observable public confirmedByRegion: GroupedData | undefined = undefined; 132 | @observable public deadByRegion: GroupedData | undefined = undefined; 133 | @observable public confirmedByCountry: GroupedData | undefined = undefined; 134 | @observable public deadByCountry: GroupedData | undefined = undefined; 135 | @observable public confirmedByCountryPerCapita: GroupedData | undefined = undefined; 136 | @observable public deadByCountryPerCapita: GroupedData | undefined = undefined; 137 | @observable public perCapita: boolean = true; 138 | 139 | constructor() { 140 | if (USE_LOCAL_DATA) { 141 | csv(confirmedGlobalCsvUrl, (err, data: any) => { 142 | if (data) { 143 | this.confirmedByCountry = groupBy(data, COUNTRY_KEY); 144 | this.confirmedByRegion = groupBy(data, STATE_KEY); 145 | this.confirmedByCountryPerCapita = groupBy(data, COUNTRY_KEY, true); 146 | } else { 147 | throw new Error(`Data wasn't loaded correctly`); 148 | } 149 | }); 150 | 151 | csv(deathsGlobalCsvUrl, (err, data: any) => { 152 | if (data) { 153 | this.deadByCountry = groupBy(data, COUNTRY_KEY); 154 | this.deadByRegion = groupBy(data, STATE_KEY); 155 | this.deadByCountryPerCapita = groupBy(data, COUNTRY_KEY, true); 156 | } else { 157 | throw new Error(`Data wasn't loaded correctly`); 158 | } 159 | }); 160 | } 161 | } 162 | 163 | @computed get totalConfirmedCasesArray(): 164 | | Array<{ time: number; totalCases: number }> 165 | | undefined { 166 | return this.confirmedCasesArray?.map((el) => { 167 | return { 168 | time: el.time, 169 | totalCases: Object.keys(el).reduce((acc: number, key: string) => { 170 | if (key !== 'time') { 171 | acc = acc + el[key]; 172 | } 173 | return acc; 174 | }, 0), 175 | }; 176 | }); 177 | } 178 | 179 | @computed get totalDeathsArray(): Array<{ time: number; totalDeaths: number }> | undefined { 180 | return this.deathsArray?.map((el) => { 181 | return { 182 | time: el.time, 183 | totalDeaths: Object.keys(el).reduce((acc: number, key: string) => { 184 | if (key !== 'time') { 185 | acc = acc + el[key]; 186 | } 187 | return acc; 188 | }, 0), 189 | }; 190 | }); 191 | } 192 | 193 | @computed get dayOf100CasesByCountry() { 194 | const threshold = 100; 195 | if (!this.ready) { 196 | return undefined; 197 | } 198 | return this.possibleCountries.reduce((acc, country) => { 199 | const countryData = this.getCountryData(country, true); 200 | const values = this.datesConverted.map((date) => countryData?.confirmed[date]); 201 | const day = values.findIndex((v) => v > threshold); 202 | acc[country] = day !== -1 ? day : undefined; 203 | return acc; 204 | }, {}); 205 | } 206 | 207 | public dataForAfter100Cases(type: 'confirmed' | 'dead', countries: string[]) { 208 | if (!this.ready) { 209 | return undefined; 210 | } 211 | return this.dates 212 | ?.map((_, i: number) => { 213 | const d = { 214 | time: i, 215 | }; 216 | this.possibleCountries.forEach((country) => { 217 | const dayOf100Cases = this.dayOf100CasesByCountry[country]; 218 | if (dayOf100Cases === undefined) { 219 | return; 220 | } 221 | const index = dayOf100Cases + i; 222 | if (index <= this.dates.length - 1) { 223 | const date = this.dates[index]; 224 | if (countries.includes(country)) { 225 | const countryData = this.getCountryData(country); 226 | if (countryData) { 227 | let value = countryData[type][momentToFormat(date)]; 228 | if (value === 0) { 229 | value = null; 230 | } 231 | d[country] = value; 232 | } 233 | } 234 | } 235 | }); 236 | return d; 237 | }) 238 | .filter((el) => 239 | Object.keys(el).some((k) => { 240 | if (k === 'time') { 241 | return undefined; 242 | } 243 | return el[k]; 244 | }) 245 | ); 246 | } 247 | 248 | @computed get confirmedCasesArray() { 249 | return this.dates?.map((date) => { 250 | const d = { 251 | time: date.unix(), 252 | }; 253 | this.possibleCountries.forEach((country) => { 254 | d[country] = this.confirmedByCountry[country][momentToFormat(date)]; 255 | }); 256 | return d; 257 | }); 258 | } 259 | 260 | @computed get deathsArray() { 261 | return this.dates?.map((date) => { 262 | const d = { 263 | time: date.unix(), 264 | }; 265 | this.possibleCountries.forEach((country) => { 266 | d[country] = this.deadByCountry[country][momentToFormat(date)]; 267 | }); 268 | return d; 269 | }); 270 | } 271 | 272 | @computed get fatalityRateArray() { 273 | return this.dates?.map((date) => { 274 | const d = { 275 | time: date.unix(), 276 | }; 277 | this.possibleCountries.forEach((country) => { 278 | const conf = this.confirmedByCountry[country][momentToFormat(date)]; 279 | d[country] = conf ? this.deadByCountry[country][momentToFormat(date)] / conf : undefined; 280 | }); 281 | return d; 282 | }); 283 | } 284 | 285 | public getDataArrayWithTime(type: 'confirmed' | 'dead', countries: string[]) { 286 | return this.dates?.map((date, i: number) => { 287 | const d = { 288 | time: date.unix(), 289 | number: i, 290 | }; 291 | this.possibleCountries.forEach((country) => { 292 | if (countries.includes(country)) { 293 | const value = this.getCountryData(country)[type][momentToFormat(date)]; 294 | d[country] = value; 295 | } 296 | }); 297 | return d; 298 | }); 299 | } 300 | 301 | public getDataArrayWithTimeOffset(type: 'confirmed' | 'dead', countries: string[]) { 302 | return this.dates?.map((date, i: number) => { 303 | const d = { 304 | time: date.unix(), 305 | }; 306 | this.possibleCountries.forEach((country) => { 307 | if (countries.includes(country)) { 308 | const value = this.getCountryData(country)[type][momentToFormat(date)]; 309 | d[country] = value; 310 | } 311 | }); 312 | return d; 313 | }); 314 | } 315 | 316 | public getLastRegionCases = (region) => 317 | this.getRegionData(region)?.confirmed[last(this.datesConverted)]; 318 | public getLastRegionDeaths = (region) => 319 | this.getRegionData(region)?.dead[last(this.datesConverted)]; 320 | 321 | public getRegionData = (region: string) => { 322 | if (!this.confirmedByRegion || !this.deadByRegion) { 323 | return; 324 | } 325 | return { 326 | confirmed: this.confirmedByRegion[region], 327 | dead: this.deadByRegion[region], 328 | }; 329 | }; 330 | 331 | public getPossibleRegionsByCountry = (country: string, sorted: boolean = false) => { 332 | const possibleRegions = this.possibleRegions.filter((region) => { 333 | if (country === US_NAME) { 334 | if (!Object.values(stateNames).includes(region)) { 335 | return false; 336 | } 337 | } 338 | return this.getRegionData(region)?.confirmed[COUNTRY_KEY] === country; 339 | }); 340 | 341 | if (sorted) { 342 | return sort( 343 | possibleRegions, 344 | (a: string, b: string) => this.getLastRegionCases(b) - this.getLastRegionCases(a) 345 | ); 346 | } 347 | 348 | return possibleRegions; 349 | }; 350 | 351 | public getCountryData = (country: string, ignorePerCapita: boolean = false) => { 352 | if (this.perCapita && !ignorePerCapita) { 353 | if (!this.confirmedByCountryPerCapita || !this.deadByCountryPerCapita) { 354 | return; 355 | } 356 | return { 357 | confirmed: this.confirmedByCountryPerCapita[country], 358 | dead: this.deadByCountryPerCapita[country], 359 | }; 360 | } 361 | 362 | if (!this.confirmedByCountry || !this.deadByCountry) { 363 | return; 364 | } 365 | return { 366 | confirmed: this.confirmedByCountry[country], 367 | dead: this.deadByCountry[country], 368 | }; 369 | }; 370 | 371 | @computed get possibleCountries() { 372 | if (this.ready && this.confirmedByCountry) { 373 | return Object.keys(this.confirmedByCountry).sort(); 374 | } 375 | return []; 376 | } 377 | 378 | @computed get possibleCountriesSortedByCases() { 379 | let ret = []; 380 | const lastCases = last(this.confirmedCasesArray); 381 | if (this.ready && this.confirmedByCountry) { 382 | ret = Object.keys(this.confirmedByCountry).sort(); 383 | } 384 | ret.sort((a: string, b: string) => lastCases[b] - lastCases[a]); 385 | return ret; 386 | } 387 | 388 | @computed get possibleCountriesSortedByDeaths() { 389 | let ret = []; 390 | const lastCases = last(this.deathsArray); 391 | if (this.ready && this.deadByCountry) { 392 | ret = Object.keys(this.deadByCountry).sort(); 393 | } 394 | ret.sort((a: string, b: string) => lastCases[b] - lastCases[a]); 395 | return ret; 396 | } 397 | 398 | @computed get countriesWithOver100Cases() { 399 | if (this.ready && this.confirmedByCountry) { 400 | return Object.keys(this.confirmedByCountry) 401 | .filter((key) => this.dayOf100CasesByCountry[key] !== undefined) 402 | .sort(); 403 | } 404 | return []; 405 | } 406 | 407 | @computed get possibleRegions() { 408 | let ret = []; 409 | if (this.ready && this.confirmedByRegion) { 410 | ret = Object.keys(this.confirmedByRegion); 411 | } 412 | ret.sort(); 413 | 414 | return ret; 415 | } 416 | 417 | @computed get dates() { 418 | if (this.ready && this.confirmedByCountry) { 419 | for (const countryName of Object.keys(this.confirmedByCountry)) { 420 | const row = this.confirmedByCountry[countryName]; 421 | const dates = getDatesFromDataRow(row); 422 | if (dates) { 423 | return dates; 424 | } 425 | } 426 | } 427 | return undefined; 428 | } 429 | 430 | @computed get datesConverted() { 431 | if (this.dates) { 432 | return this.dates.map(momentToFormat); 433 | } 434 | 435 | return undefined; 436 | } 437 | 438 | @computed get ready(): boolean { 439 | return Boolean(this.confirmedByCountry) && Boolean(this.deadByCountry); 440 | } 441 | } 442 | 443 | const getStore = () => new DataStore(); 444 | 445 | let dataStore: DataStore | undefined = getStore(); 446 | let dataStoreContext = createContext(dataStore); 447 | 448 | export const clearSensorInfoStore = () => { 449 | dataStore = getStore(); 450 | dataStoreContext = createContext(dataStore); 451 | }; 452 | 453 | const useDataStore = () => useContext(dataStoreContext); 454 | export default useDataStore; 455 | -------------------------------------------------------------------------------- /src/data/continentsArray.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "country": "Afghanistan", 4 | "continent": "Asia" 5 | }, 6 | { 7 | "country": "Albania", 8 | "continent": "Europe" 9 | }, 10 | { 11 | "country": "Algeria", 12 | "continent": "Africa" 13 | }, 14 | { 15 | "country": "American Samoa", 16 | "continent": "Oceania" 17 | }, 18 | { 19 | "country": "Andorra", 20 | "continent": "Europe" 21 | }, 22 | { 23 | "country": "Angola", 24 | "continent": "Africa" 25 | }, 26 | { 27 | "country": "Anguilla", 28 | "continent": "North America" 29 | }, 30 | { 31 | "country": "Antarctica", 32 | "continent": "Antarctica" 33 | }, 34 | { 35 | "country": "Antigua and Barbuda", 36 | "continent": "North America" 37 | }, 38 | { 39 | "country": "Argentina", 40 | "continent": "South America" 41 | }, 42 | { 43 | "country": "Armenia", 44 | "continent": "Asia" 45 | }, 46 | { 47 | "country": "Aruba", 48 | "continent": "North America" 49 | }, 50 | { 51 | "country": "Australia", 52 | "continent": "Oceania" 53 | }, 54 | { 55 | "country": "Austria", 56 | "continent": "Europe" 57 | }, 58 | { 59 | "country": "Azerbaijan", 60 | "continent": "Asia" 61 | }, 62 | { 63 | "country": "Bahamas", 64 | "continent": "North America" 65 | }, 66 | { 67 | "country": "Bahrain", 68 | "continent": "Asia" 69 | }, 70 | { 71 | "country": "Bangladesh", 72 | "continent": "Asia" 73 | }, 74 | { 75 | "country": "Barbados", 76 | "continent": "North America" 77 | }, 78 | { 79 | "country": "Belarus", 80 | "continent": "Europe" 81 | }, 82 | { 83 | "country": "Belgium", 84 | "continent": "Europe" 85 | }, 86 | { 87 | "country": "Belize", 88 | "continent": "North America" 89 | }, 90 | { 91 | "country": "Benin", 92 | "continent": "Africa" 93 | }, 94 | { 95 | "country": "Bermuda", 96 | "continent": "North America" 97 | }, 98 | { 99 | "country": "Bhutan", 100 | "continent": "Asia" 101 | }, 102 | { 103 | "country": "Bolivia", 104 | "continent": "South America" 105 | }, 106 | { 107 | "country": "Bosnia and Herzegovina", 108 | "continent": "Europe" 109 | }, 110 | { 111 | "country": "Botswana", 112 | "continent": "Africa" 113 | }, 114 | { 115 | "country": "Bouvet Island", 116 | "continent": "Antarctica" 117 | }, 118 | { 119 | "country": "Brazil", 120 | "continent": "South America" 121 | }, 122 | { 123 | "country": "British Indian Ocean Territory", 124 | "continent": "Africa" 125 | }, 126 | { 127 | "country": "Brunei", 128 | "continent": "Asia" 129 | }, 130 | { 131 | "country": "Bulgaria", 132 | "continent": "Europe" 133 | }, 134 | { 135 | "country": "Burkina Faso", 136 | "continent": "Africa" 137 | }, 138 | { 139 | "country": "Burundi", 140 | "continent": "Africa" 141 | }, 142 | { 143 | "country": "Cambodia", 144 | "continent": "Asia" 145 | }, 146 | { 147 | "country": "Cameroon", 148 | "continent": "Africa" 149 | }, 150 | { 151 | "country": "Canada", 152 | "continent": "North America" 153 | }, 154 | { 155 | "country": "Cape Verde", 156 | "continent": "Africa" 157 | }, 158 | { 159 | "country": "Cayman Islands", 160 | "continent": "North America" 161 | }, 162 | { 163 | "country": "Central African Republic", 164 | "continent": "Africa" 165 | }, 166 | { 167 | "country": "Chad", 168 | "continent": "Africa" 169 | }, 170 | { 171 | "country": "Chile", 172 | "continent": "South America" 173 | }, 174 | { 175 | "country": "China", 176 | "continent": "Asia" 177 | }, 178 | { 179 | "country": "Christmas Island", 180 | "continent": "Oceania" 181 | }, 182 | { 183 | "country": "Cocos (Keeling) Islands", 184 | "continent": "Oceania" 185 | }, 186 | { 187 | "country": "Colombia", 188 | "continent": "South America" 189 | }, 190 | { 191 | "country": "Comoros", 192 | "continent": "Africa" 193 | }, 194 | { 195 | "country": "Congo", 196 | "continent": "Africa" 197 | }, 198 | { 199 | "country": "Cook Islands", 200 | "continent": "Oceania" 201 | }, 202 | { 203 | "country": "Costa Rica", 204 | "continent": "North America" 205 | }, 206 | { 207 | "country": "Croatia", 208 | "continent": "Europe" 209 | }, 210 | { 211 | "country": "Cuba", 212 | "continent": "North America" 213 | }, 214 | { 215 | "country": "Cyprus", 216 | "continent": "Asia" 217 | }, 218 | { 219 | "country": "Czechia", 220 | "continent": "Europe" 221 | }, 222 | { 223 | "country": "Denmark", 224 | "continent": "Europe" 225 | }, 226 | { 227 | "country": "Djibouti", 228 | "continent": "Africa" 229 | }, 230 | { 231 | "country": "Dominica", 232 | "continent": "North America" 233 | }, 234 | { 235 | "country": "Dominican Republic", 236 | "continent": "North America" 237 | }, 238 | { 239 | "country": "East Timor", 240 | "continent": "Asia" 241 | }, 242 | { 243 | "country": "Ecuador", 244 | "continent": "South America" 245 | }, 246 | { 247 | "country": "Egypt", 248 | "continent": "Africa" 249 | }, 250 | { 251 | "country": "El Salvador", 252 | "continent": "North America" 253 | }, 254 | { 255 | "country": "England", 256 | "continent": "Europe" 257 | }, 258 | { 259 | "country": "Equatorial Guinea", 260 | "continent": "Africa" 261 | }, 262 | { 263 | "country": "Eritrea", 264 | "continent": "Africa" 265 | }, 266 | { 267 | "country": "Estonia", 268 | "continent": "Europe" 269 | }, 270 | { 271 | "country": "Ethiopia", 272 | "continent": "Africa" 273 | }, 274 | { 275 | "country": "Falkland Islands", 276 | "continent": "South America" 277 | }, 278 | { 279 | "country": "Faroe Islands", 280 | "continent": "Europe" 281 | }, 282 | { 283 | "country": "Fiji", 284 | "continent": "Oceania" 285 | }, 286 | { 287 | "country": "Finland", 288 | "continent": "Europe" 289 | }, 290 | { 291 | "country": "France", 292 | "continent": "Europe" 293 | }, 294 | { 295 | "country": "French Guiana", 296 | "continent": "South America" 297 | }, 298 | { 299 | "country": "French Polynesia", 300 | "continent": "Oceania" 301 | }, 302 | { 303 | "country": "French Southern territories", 304 | "continent": "Antarctica" 305 | }, 306 | { 307 | "country": "Gabon", 308 | "continent": "Africa" 309 | }, 310 | { 311 | "country": "Gambia", 312 | "continent": "Africa" 313 | }, 314 | { 315 | "country": "Georgia", 316 | "continent": "Asia" 317 | }, 318 | { 319 | "country": "Germany", 320 | "continent": "Europe" 321 | }, 322 | { 323 | "country": "Ghana", 324 | "continent": "Africa" 325 | }, 326 | { 327 | "country": "Gibraltar", 328 | "continent": "Europe" 329 | }, 330 | { 331 | "country": "Greece", 332 | "continent": "Europe" 333 | }, 334 | { 335 | "country": "Greenland", 336 | "continent": "North America" 337 | }, 338 | { 339 | "country": "Grenada", 340 | "continent": "North America" 341 | }, 342 | { 343 | "country": "Guadeloupe", 344 | "continent": "North America" 345 | }, 346 | { 347 | "country": "Guam", 348 | "continent": "Oceania" 349 | }, 350 | { 351 | "country": "Guatemala", 352 | "continent": "North America" 353 | }, 354 | { 355 | "country": "Guinea", 356 | "continent": "Africa" 357 | }, 358 | { 359 | "country": "Guinea-Bissau", 360 | "continent": "Africa" 361 | }, 362 | { 363 | "country": "Guyana", 364 | "continent": "South America" 365 | }, 366 | { 367 | "country": "Haiti", 368 | "continent": "North America" 369 | }, 370 | { 371 | "country": "Heard Island and McDonald Islands", 372 | "continent": "Antarctica" 373 | }, 374 | { 375 | "country": "Holy See (Vatican City State)", 376 | "continent": "Europe" 377 | }, 378 | { 379 | "country": "Honduras", 380 | "continent": "North America" 381 | }, 382 | { 383 | "country": "Hong Kong", 384 | "continent": "Asia" 385 | }, 386 | { 387 | "country": "Kosovo", 388 | "continent": "Europe" 389 | }, 390 | { 391 | "country": "Hungary", 392 | "continent": "Europe" 393 | }, 394 | { 395 | "country": "Iceland", 396 | "continent": "Europe" 397 | }, 398 | { 399 | "country": "India", 400 | "continent": "Asia" 401 | }, 402 | { 403 | "country": "Indonesia", 404 | "continent": "Asia" 405 | }, 406 | { 407 | "country": "Iran", 408 | "continent": "Asia" 409 | }, 410 | { 411 | "country": "Iraq", 412 | "continent": "Asia" 413 | }, 414 | { 415 | "country": "Ireland", 416 | "continent": "Europe" 417 | }, 418 | { 419 | "country": "Israel", 420 | "continent": "Asia" 421 | }, 422 | { 423 | "country": "Italy", 424 | "continent": "Europe" 425 | }, 426 | { 427 | "country": "Ivory Coast", 428 | "continent": "Africa" 429 | }, 430 | { 431 | "country": "Jamaica", 432 | "continent": "North America" 433 | }, 434 | { 435 | "country": "Japan", 436 | "continent": "Asia" 437 | }, 438 | { 439 | "country": "Jordan", 440 | "continent": "Asia" 441 | }, 442 | { 443 | "country": "Kazakhstan", 444 | "continent": "Asia" 445 | }, 446 | { 447 | "country": "Kenya", 448 | "continent": "Africa" 449 | }, 450 | { 451 | "country": "Kiribati", 452 | "continent": "Oceania" 453 | }, 454 | { 455 | "country": "Kuwait", 456 | "continent": "Asia" 457 | }, 458 | { 459 | "country": "Kyrgyzstan", 460 | "continent": "Asia" 461 | }, 462 | { 463 | "country": "Laos", 464 | "continent": "Asia" 465 | }, 466 | { 467 | "country": "Latvia", 468 | "continent": "Europe" 469 | }, 470 | { 471 | "country": "Lebanon", 472 | "continent": "Asia" 473 | }, 474 | { 475 | "country": "Lesotho", 476 | "continent": "Africa" 477 | }, 478 | { 479 | "country": "Liberia", 480 | "continent": "Africa" 481 | }, 482 | { 483 | "country": "Libyan Arab Jamahiriya", 484 | "continent": "Africa" 485 | }, 486 | { 487 | "country": "Montenegro", 488 | "continent": "Europe" 489 | }, 490 | { 491 | "country": "Liechtenstein", 492 | "continent": "Europe" 493 | }, 494 | { 495 | "country": "Lithuania", 496 | "continent": "Europe" 497 | }, 498 | { 499 | "country": "Luxembourg", 500 | "continent": "Europe" 501 | }, 502 | { 503 | "country": "Macao", 504 | "continent": "Asia" 505 | }, 506 | { 507 | "country": "North Macedonia", 508 | "continent": "Europe" 509 | }, 510 | { 511 | "country": "Madagascar", 512 | "continent": "Africa" 513 | }, 514 | { 515 | "country": "Malawi", 516 | "continent": "Africa" 517 | }, 518 | { 519 | "country": "Malaysia", 520 | "continent": "Asia" 521 | }, 522 | { 523 | "country": "Maldives", 524 | "continent": "Asia" 525 | }, 526 | { 527 | "country": "Mali", 528 | "continent": "Africa" 529 | }, 530 | { 531 | "country": "Malta", 532 | "continent": "Europe" 533 | }, 534 | { 535 | "country": "Marshall Islands", 536 | "continent": "Oceania" 537 | }, 538 | { 539 | "country": "Martinique", 540 | "continent": "North America" 541 | }, 542 | { 543 | "country": "Mauritania", 544 | "continent": "Africa" 545 | }, 546 | { 547 | "country": "Mauritius", 548 | "continent": "Africa" 549 | }, 550 | { 551 | "country": "Mayotte", 552 | "continent": "Africa" 553 | }, 554 | { 555 | "country": "Mexico", 556 | "continent": "North America" 557 | }, 558 | { 559 | "country": "Micronesia, Federated States of", 560 | "continent": "Oceania" 561 | }, 562 | { 563 | "country": "Moldova", 564 | "continent": "Europe" 565 | }, 566 | { 567 | "country": "Monaco", 568 | "continent": "Europe" 569 | }, 570 | { 571 | "country": "Mongolia", 572 | "continent": "Asia" 573 | }, 574 | { 575 | "country": "Montserrat", 576 | "continent": "North America" 577 | }, 578 | { 579 | "country": "Morocco", 580 | "continent": "Africa" 581 | }, 582 | { 583 | "country": "Mozambique", 584 | "continent": "Africa" 585 | }, 586 | { 587 | "country": "Myanmar", 588 | "continent": "Asia" 589 | }, 590 | { 591 | "country": "Namibia", 592 | "continent": "Africa" 593 | }, 594 | { 595 | "country": "Nauru", 596 | "continent": "Oceania" 597 | }, 598 | { 599 | "country": "Nepal", 600 | "continent": "Asia" 601 | }, 602 | { 603 | "country": "Netherlands", 604 | "continent": "Europe" 605 | }, 606 | { 607 | "country": "Netherlands Antilles", 608 | "continent": "North America" 609 | }, 610 | { 611 | "country": "New Caledonia", 612 | "continent": "Oceania" 613 | }, 614 | { 615 | "country": "New Zealand", 616 | "continent": "Oceania" 617 | }, 618 | { 619 | "country": "Nicaragua", 620 | "continent": "North America" 621 | }, 622 | { 623 | "country": "Niger", 624 | "continent": "Africa" 625 | }, 626 | { 627 | "country": "Nigeria", 628 | "continent": "Africa" 629 | }, 630 | { 631 | "country": "Niue", 632 | "continent": "Oceania" 633 | }, 634 | { 635 | "country": "Norfolk Island", 636 | "continent": "Oceania" 637 | }, 638 | { 639 | "country": "North Korea", 640 | "continent": "Asia" 641 | }, 642 | { 643 | "country": "Northern Ireland", 644 | "continent": "Europe" 645 | }, 646 | { 647 | "country": "Northern Mariana Islands", 648 | "continent": "Oceania" 649 | }, 650 | { 651 | "country": "Norway", 652 | "continent": "Europe" 653 | }, 654 | { 655 | "country": "Oman", 656 | "continent": "Asia" 657 | }, 658 | { 659 | "country": "Pakistan", 660 | "continent": "Asia" 661 | }, 662 | { 663 | "country": "Palau", 664 | "continent": "Oceania" 665 | }, 666 | { 667 | "country": "Palestine", 668 | "continent": "Asia" 669 | }, 670 | { 671 | "country": "Panama", 672 | "continent": "North America" 673 | }, 674 | { 675 | "country": "Papua New Guinea", 676 | "continent": "Oceania" 677 | }, 678 | { 679 | "country": "Paraguay", 680 | "continent": "South America" 681 | }, 682 | { 683 | "country": "Peru", 684 | "continent": "South America" 685 | }, 686 | { 687 | "country": "Philippines", 688 | "continent": "Asia" 689 | }, 690 | { 691 | "country": "Pitcairn", 692 | "continent": "Oceania" 693 | }, 694 | { 695 | "country": "Poland", 696 | "continent": "Europe" 697 | }, 698 | { 699 | "country": "Portugal", 700 | "continent": "Europe" 701 | }, 702 | { 703 | "country": "Puerto Rico", 704 | "continent": "North America" 705 | }, 706 | { 707 | "country": "Qatar", 708 | "continent": "Asia" 709 | }, 710 | { 711 | "country": "Reunion", 712 | "continent": "Africa" 713 | }, 714 | { 715 | "country": "Romania", 716 | "continent": "Europe" 717 | }, 718 | { 719 | "country": "Russia", 720 | "continent": "Europe" 721 | }, 722 | { 723 | "country": "Rwanda", 724 | "continent": "Africa" 725 | }, 726 | { 727 | "country": "Saint Helena", 728 | "continent": "Africa" 729 | }, 730 | { 731 | "country": "Serbia", 732 | "continent": "Europe" 733 | }, 734 | { 735 | "country": "Saint Kitts and Nevis", 736 | "continent": "North America" 737 | }, 738 | { 739 | "country": "Saint Lucia", 740 | "continent": "North America" 741 | }, 742 | { 743 | "country": "Saint Pierre and Miquelon", 744 | "continent": "North America" 745 | }, 746 | { 747 | "country": "Saint Vincent and the Grenadines", 748 | "continent": "North America" 749 | }, 750 | { 751 | "country": "Samoa", 752 | "continent": "Oceania" 753 | }, 754 | { 755 | "country": "San Marino", 756 | "continent": "Europe" 757 | }, 758 | { 759 | "country": "Sao Tome and Principe", 760 | "continent": "Africa" 761 | }, 762 | { 763 | "country": "Saudi Arabia", 764 | "continent": "Asia" 765 | }, 766 | { 767 | "country": "Scotland", 768 | "continent": "Europe" 769 | }, 770 | { 771 | "country": "Senegal", 772 | "continent": "Africa" 773 | }, 774 | { 775 | "country": "Seychelles", 776 | "continent": "Africa" 777 | }, 778 | { 779 | "country": "Sierra Leone", 780 | "continent": "Africa" 781 | }, 782 | { 783 | "country": "Singapore", 784 | "continent": "Asia" 785 | }, 786 | { 787 | "country": "Slovakia", 788 | "continent": "Europe" 789 | }, 790 | { 791 | "country": "Slovenia", 792 | "continent": "Europe" 793 | }, 794 | { 795 | "country": "Solomon Islands", 796 | "continent": "Oceania" 797 | }, 798 | { 799 | "country": "Somalia", 800 | "continent": "Africa" 801 | }, 802 | { 803 | "country": "South Africa", 804 | "continent": "Africa" 805 | }, 806 | { 807 | "country": "South Georgia and the South Sandwich Islands", 808 | "continent": "Antarctica" 809 | }, 810 | { 811 | "country": "South Korea", 812 | "continent": "Asia" 813 | }, 814 | { 815 | "country": "Taiwan", 816 | "continent": "Asia" 817 | }, 818 | { 819 | "country": "South Sudan", 820 | "continent": "Africa" 821 | }, 822 | { 823 | "country": "Spain", 824 | "continent": "Europe" 825 | }, 826 | { 827 | "country": "Sri Lanka", 828 | "continent": "Asia" 829 | }, 830 | { 831 | "country": "Sudan", 832 | "continent": "Africa" 833 | }, 834 | { 835 | "country": "Suriname", 836 | "continent": "South America" 837 | }, 838 | { 839 | "country": "Svalbard and Jan Mayen", 840 | "continent": "Europe" 841 | }, 842 | { 843 | "country": "Swaziland", 844 | "continent": "Africa" 845 | }, 846 | { 847 | "country": "Sweden", 848 | "continent": "Europe" 849 | }, 850 | { 851 | "country": "Switzerland", 852 | "continent": "Europe" 853 | }, 854 | { 855 | "country": "Syria", 856 | "continent": "Asia" 857 | }, 858 | { 859 | "country": "Tajikistan", 860 | "continent": "Asia" 861 | }, 862 | { 863 | "country": "Tanzania", 864 | "continent": "Africa" 865 | }, 866 | { 867 | "country": "Thailand", 868 | "continent": "Asia" 869 | }, 870 | { 871 | "country": "Republic of the Congo", 872 | "continent": "Africa" 873 | }, 874 | { 875 | "country": "Democratic Republic of the Congo", 876 | "continent": "Africa" 877 | }, 878 | { 879 | "country": "Eswatini", 880 | "continent": "Africa" 881 | }, 882 | { 883 | "country": "Congo (Kinshasa)", 884 | "continent": "Africa" 885 | }, 886 | { 887 | "country": "Congo (Brazzaville)", 888 | "continent": "Africa" 889 | }, 890 | { 891 | "country": "Togo", 892 | "continent": "Africa" 893 | }, 894 | { 895 | "country": "Tokelau", 896 | "continent": "Oceania" 897 | }, 898 | { 899 | "country": "Tonga", 900 | "continent": "Oceania" 901 | }, 902 | { 903 | "country": "Trinidad and Tobago", 904 | "continent": "North America" 905 | }, 906 | { 907 | "country": "Tunisia", 908 | "continent": "Africa" 909 | }, 910 | { 911 | "country": "Turkey", 912 | "continent": "Asia" 913 | }, 914 | { 915 | "country": "Turkmenistan", 916 | "continent": "Asia" 917 | }, 918 | { 919 | "country": "Turks and Caicos Islands", 920 | "continent": "North America" 921 | }, 922 | { 923 | "country": "Tuvalu", 924 | "continent": "Oceania" 925 | }, 926 | { 927 | "country": "Uganda", 928 | "continent": "Africa" 929 | }, 930 | { 931 | "country": "Ukraine", 932 | "continent": "Europe" 933 | }, 934 | { 935 | "country": "United Arab Emirates", 936 | "continent": "Asia" 937 | }, 938 | { 939 | "country": "United Kingdom", 940 | "continent": "Europe" 941 | }, 942 | { 943 | "country": "United States", 944 | "continent": "North America" 945 | }, 946 | { 947 | "country": "United States Minor Outlying Islands", 948 | "continent": "Oceania" 949 | }, 950 | { 951 | "country": "Uruguay", 952 | "continent": "South America" 953 | }, 954 | { 955 | "country": "Uzbekistan", 956 | "continent": "Asia" 957 | }, 958 | { 959 | "country": "Vanuatu", 960 | "continent": "Oceania" 961 | }, 962 | { 963 | "country": "Venezuela", 964 | "continent": "South America" 965 | }, 966 | { 967 | "country": "Vietnam", 968 | "continent": "Asia" 969 | }, 970 | { 971 | "country": "Virgin Islands, British", 972 | "continent": "North America" 973 | }, 974 | { 975 | "country": "Virgin Islands, U.S.", 976 | "continent": "North America" 977 | }, 978 | { 979 | "country": "Wales", 980 | "continent": "Europe" 981 | }, 982 | { 983 | "country": "Wallis and Futuna", 984 | "continent": "Oceania" 985 | }, 986 | { 987 | "country": "Western Sahara", 988 | "continent": "Africa" 989 | }, 990 | { 991 | "country": "Yemen", 992 | "continent": "Asia" 993 | }, 994 | { 995 | "country": "Yugoslavia", 996 | "continent": "Europe" 997 | }, 998 | { 999 | "country": "Zambia", 1000 | "continent": "Africa" 1001 | }, 1002 | { 1003 | "country": "Zimbabwe", 1004 | "continent": "Africa" 1005 | } 1006 | ] --------------------------------------------------------------------------------