diff --git a/.idea/dengue-singapore.iml b/.idea/dengue-singapore.iml index 2bfa945..f7c20ac 100644 --- a/.idea/dengue-singapore.iml +++ b/.idea/dengue-singapore.iml @@ -7,7 +7,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 24eb271..cd18038 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,4 +3,5 @@ + \ No newline at end of file diff --git a/package.json b/package.json index 8d598b7..1df37e3 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,11 @@ "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^7.1.2", "autoprefixer": "^9.8.6", + "chart.js": "^2.9.3", "leaflet": "^1.6.0", "postcss-cli": "^7.1.1", "react": "^16.13.1", + "react-chartjs-2": "^2.10.0", "react-dom": "^16.13.1", "react-helmet": "^6.1.0", "react-leaflet": "^2.7.0", diff --git a/python/download_dengue_data.py b/python/download_dengue_data.py index f82ed15..9d93b83 100644 --- a/python/download_dengue_data.py +++ b/python/download_dengue_data.py @@ -22,7 +22,7 @@ z = zipfile.ZipFile(io.BytesIO(r.content)) file = z.open("dengue-clusters-kml.kml") -soup = Soup(file.read(), 'lxml') # Parse as XML +soup = Soup(file.read(), 'lxml') # Parse as XML place_marks = soup.find_all('placemark') @@ -30,7 +30,9 @@ for place_mark in place_marks: - location_name = place_mark.find('td').string.split("(")[0].split("[")[0].split(",")[0].split("/")[0].strip() + location_name = \ + place_mark.find('td').string.split("(")[0].split("[")[0].split(",")[0].split("/")[ + 0].strip() number_cases = int(place_mark.find('simpledata', {"name": "CASE_SIZE"}).string) coordinates = place_mark.find('coordinates') @@ -66,4 +68,4 @@ with open(save_dir, 'w') as fp: json.dump(dict_to_export, fp) -print("data correctly downloaded") \ No newline at end of file +print("data correctly downloaded") diff --git a/python/download_infectious_disease.py b/python/download_infectious_disease.py new file mode 100644 index 0000000..a153499 --- /dev/null +++ b/python/download_infectious_disease.py @@ -0,0 +1,73 @@ +import json +from bs4 import BeautifulSoup as Soup +import pandas as pd +import requests +import zipfile +import io +import os +import numpy as np + + +class NpEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, np.integer): + return int(obj) + elif isinstance(obj, np.floating): + return float(obj) + elif isinstance(obj, np.ndarray): + return obj.tolist() + else: + return super(NpEncoder, self).default(obj) + + +data_gov = "https://data.gov.sg/dataset/weekly-infectious-disease-bulletin-cases" +data_gov = "https://data.gov.sg/api/action/datastore_search?resource_id=ef7e44f1-9b14-4680-a60a-37d2c9dda390&q=dengue" + +html_text = requests.get(data_gov).text +soup = Soup(html_text, 'html.parser') + +data_id = soup.find('a', {"class": "ga-dataset-download"}).get("href") + +url_data = f"https://data.gov.sg{data_id}" + +r = requests.get(url_data, stream=True) +z = zipfile.ZipFile(io.BytesIO(r.content)) +file = z.open("weekly-infectious-disease-bulletin-cases.csv") + +df = pd.read_csv(file) + +df_dengue = df[df.disease.isin(['Dengue Fever'])].copy() + +df_dengue[["year", "week"]] = df_dengue["epi_week"].str.split("-", expand=True) + +dict_to_export = {} + +color = ["#2ca02c", "#1f77b4", "#fcc105", "#ff7f0e", "#d62728", "#9467bd", "#800000", + "#2ca02c", "#1f77b4", "#fcc105", "#ff7f0e", "#d62728", "#9467bd"] + +for ix, year in enumerate(df_dengue["year"].unique()): + + # if int(year) > 2016: + + dict_to_export[year] = {} + dict_to_export[year]['cases'] = list( + df_dengue[df_dengue['year'] == year]['no._of_cases'].values) + dict_to_export[year]['color'] = color[ix] + + # df_year = df_dengue[df_dengue['year'] == year]['no._of_cases'].values + # + # for week in df_dengue["week"].unique(): + # + # df_week = df_dengue[df_dengue['week'] == week] + # dict_to_export[year][week] = df_week[["disease", "no._of_cases"]].set_index( + # "disease").to_dict() + +save_dir = os.path.join(os.path.dirname(os.getcwd()), "src", "Data", + "infectious_disease.json") + +save_dir = os.path.join('C:\\Users\\sbbfti\\Desktop\\github-projects\\dengue-singapore', "src", "Data", "infectious_disease.json") + +with open(save_dir, 'w') as fp: + json.dump(dict_to_export, fp, cls=NpEncoder) + +print("data correctly downloaded") diff --git a/python/requirements.txt b/python/requirements.txt index d9a1b63..06c5828 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -1,2 +1,3 @@ lxml==4.5.2 -beautifulsoup4==4.9.1 \ No newline at end of file +beautifulsoup4==4.9.1 +pandas \ No newline at end of file diff --git a/src/Components/BarChartWeeklyDengue.js b/src/Components/BarChartWeeklyDengue.js new file mode 100644 index 0000000..7abbbf2 --- /dev/null +++ b/src/Components/BarChartWeeklyDengue.js @@ -0,0 +1,129 @@ +import React from "react"; + +import { Line } from "react-chartjs-2"; + +function BarChartWeeklyDengue() { + const { innerWidth: width } = window; + + let chartHeight; + if (width > 500) { + chartHeight = 250; + } else { + chartHeight = 350; + } + + let weekly_disease = require("../Data/infectious_disease.json"); + + const data = { + labels: [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "32", + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "50", + "51", + "52", + ], + datasets: [], + }; + + Object.keys(weekly_disease).map((year) => { + data.datasets.push({ + label: year, + backgroundColor: "rgba(0, 0, 0, 0)", + borderColor: weekly_disease[year].color, + borderWidth: 1, + hoverBackgroundColor: weekly_disease[year].color, + hoverBorderColor: weekly_disease[year].color, + data: weekly_disease[year].cases, + yAxisID: "y1", + }); + }); + + return ( + + ); +} + +export default BarChartWeeklyDengue; diff --git a/src/Components/HomeView.js b/src/Components/HomeView.js index c8ff0c4..4aa05f7 100644 --- a/src/Components/HomeView.js +++ b/src/Components/HomeView.js @@ -2,6 +2,7 @@ import React from "react"; import { Map, Polygon, Popup, TileLayer } from "react-leaflet"; import "leaflet/dist/leaflet.css"; import TableCases from "./TableCases"; +import BarChartWeeklyDengue from "./BarChartWeeklyDengue"; function HomeView() { const position = [1.35, 103.825]; @@ -44,11 +45,30 @@ function HomeView() { ))} - -

- Click on the cluster to learn more about the number of people that were - infected. + {" "} +

+ Click on each cluster to learn more about the number of cases since + start of that cluster.

+
+

+ This website uses data provided by the{" "} + + Singaporean government. + {" "} + Data is updated on a daly basis at 1 am.{" "} +

+

+ A dengue cluster is a locality where two or more cases have onset + within 14 days and are located within 150m of each other. While NEA + categorizes clusters in three alert levels: red (high risk with more + than 10 cases); yellow (high risk with less than 10 cases), and; green + (no new cases but under surveillance for 21 days). This website uses + another color legend to better distinguish between dengue's clusters. + See legend over the map for more information. +

+
+ ); } diff --git a/src/Data/infectious_disease.json b/src/Data/infectious_disease.json new file mode 100644 index 0000000..fa94001 --- /dev/null +++ b/src/Data/infectious_disease.json @@ -0,0 +1,498 @@ +{ + "2012": { + "cases": [ + 74, + 64, + 60, + 50, + 84, + 87, + 65, + 50, + 55, + 45, + 64, + 72, + 48, + 52, + 81, + 77, + 64, + 86, + 96, + 80, + 80, + 81, + 99, + 87, + 128, + 151, + 140, + 131, + 124, + 110, + 94, + 127, + 115, + 92, + 102, + 75, + 73, + 72, + 99, + 103, + 122, + 105, + 84, + 104, + 99, + 87, + 99, + 80, + 78, + 104, + 92, + 111 + ], + "color": "#2ca02c" + }, + "2013": { + "cases": [ + 132, + 204, + 219, + 264, + 292, + 322, + 246, + 294, + 247, + 272, + 307, + 305, + 314, + 402, + 491, + 505, + 536, + 547, + 556, + 611, + 637, + 742, + 813, + 805, + 838, + 802, + 676, + 540, + 389, + 305, + 288, + 255, + 378, + 338, + 382, + 449, + 337, + 370, + 402, + 439, + 435, + 371, + 495, + 457, + 401, + 442, + 365, + 372, + 347, + 380, + 371, + 414 + ], + "color": "#1f77b4" + }, + "2014": { + "cases": [ + 436, + 479, + 401, + 336, + 234, + 273, + 369, + 193, + 186, + 209, + 224, + 210, + 225, + 240, + 244, + 235, + 282, + 251, + 261, + 291, + 427, + 456, + 458, + 506, + 550, + 671, + 888, + 818, + 744, + 633, + 484, + 545, + 438, + 418, + 367, + 339, + 362, + 396, + 344, + 339, + 278, + 297, + 213, + 186, + 168, + 158, + 149, + 162, + 212, + 177, + 198, + 188, + 158 + ], + "color": "#fcc105" + }, + "2015": { + "cases": [ + 256, + 228, + 237, + 259, + 212, + 173, + 99, + 172, + 189, + 110, + 91, + 129, + 90, + 107, + 137, + 113, + 120, + 135, + 157, + 109, + 178, + 151, + 192, + 207, + 240, + 242, + 272, + 263, + 293, + 250, + 226, + 221, + 250, + 225, + 246, + 217, + 303, + 214, + 257, + 235, + 235, + 224, + 246, + 266, + 198, + 254, + 285, + 259, + 356, + 333, + 372, + 458 + ], + "color": "#ff7f0e" + }, + "2016": { + "cases": [ + 549, + 628, + 635, + 629, + 528, + 420, + 592, + 508, + 412, + 396, + 373, + 306, + 376, + 290, + 225, + 231, + 244, + 181, + 217, + 212, + 198, + 219, + 159, + 192, + 215, + 234, + 246, + 223, + 266, + 221, + 211, + 197, + 221, + 274, + 310, + 242, + 174, + 173, + 138, + 125, + 94, + 108, + 84, + 85, + 84, + 72, + 74, + 86, + 59, + 81, + 72, + 64 + ], + "color": "#d62728" + }, + "2017": { + "cases": [ + 70, + 90, + 74, + 66, + 62, + 77, + 51, + 54, + 41, + 32, + 34, + 32, + 49, + 37, + 32, + 51, + 37, + 42, + 48, + 48, + 50, + 66, + 62, + 79, + 73, + 75, + 63, + 53, + 51, + 66, + 60, + 56, + 52, + 51, + 33, + 40, + 53, + 58, + 39, + 51, + 55, + 62, + 77, + 62, + 44, + 50, + 24, + 37, + 33, + 40, + 51, + 66 + ], + "color": "#9467bd" + }, + "2018": { + "cases": [ + 83, + 68, + 54, + 45, + 48, + 50, + 28, + 38, + 51, + 37, + 37, + 36, + 24, + 38, + 38, + 56, + 55, + 74, + 62, + 63, + 64, + 52, + 75, + 55, + 59, + 75, + 56, + 44, + 41, + 56, + 50, + 59, + 42, + 66, + 75, + 75, + 49, + 63, + 40, + 51, + 51, + 60, + 78, + 75, + 71, + 78, + 97, + 109, + 114, + 107, + 127, + 160 + ], + "color": "#800000" + }, + "2019": { + "cases": [ + 205, + 245, + 207, + 221, + 179, + 135, + 232, + 181, + 156, + 133, + 109, + 96, + 99, + 108, + 125, + 134, + 156, + 192, + 287, + 307, + 376, + 400, + 396, + 464, + 430, + 492, + 591, + 661, + 644, + 601, + 597, + 521, + 522, + 474, + 412, + 326, + 314, + 299, + 258, + 243, + 226, + 239, + 241, + 306, + 321, + 372, + 329, + 295, + 280, + 257, + 226, + 290 + ], + "color": "#2ca02c" + }, + "2020": { + "cases": [ + 302, + 342, + 402, + 307, + 370, + 400, + 378, + 380, + 373, + 375, + 389, + 367, + 377, + 315, + 341, + 360, + 399, + 390, + 502, + 527, + 622, + 732, + 870, + 1156, + 1373, + 1465, + 1449, + 1669, + 1729, + 1792, + 1379, + 1667, + 1339, + 1288 + ], + "color": "#1f77b4" + } +} diff --git a/yarn.lock b/yarn.lock index 6fdc680..91ea0d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2968,6 +2968,29 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +chart.js@^2.9.3: + version "2.9.3" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.3.tgz#ae3884114dafd381bc600f5b35a189138aac1ef7" + integrity sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw== + dependencies: + chartjs-color "^2.1.0" + moment "^2.10.2" + +chartjs-color-string@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71" + integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A== + dependencies: + color-name "^1.0.0" + +chartjs-color@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0" + integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w== + dependencies: + chartjs-color-string "^0.6.0" + color-convert "^1.9.3" + chokidar@^2.0.2, chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -3121,7 +3144,7 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0, color-convert@^1.9.1: +color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -7153,6 +7176,11 @@ mkdirp@^0.5.5: dependencies: minimist "^1.2.5" +moment@^2.10.2: + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -8985,6 +9013,14 @@ react-app-polyfill@^1.0.6: regenerator-runtime "^0.13.3" whatwg-fetch "^3.0.0" +react-chartjs-2@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-2.10.0.tgz#857e7f4788cae27e872624ba7826d53cf82f1ee6" + integrity sha512-1MjWEkUn8LLFf6GVyYUOrruJTW3yVU5hlEJOwGj3MiokuC+jH/BahjWVGAMonbe9UYbEIUbd2Rn36iVlC0Hb7w== + dependencies: + lodash "^4.17.19" + prop-types "^15.7.2" + react-dev-utils@^10.2.1: version "10.2.1" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-10.2.1.tgz#f6de325ae25fa4d546d09df4bb1befdc6dd19c19"