Compare commits
No commits in common. "master" and "gh-pages" have entirely different histories.
396
LICENSE
|
@ -1,396 +0,0 @@
|
||||||
Attribution 4.0 International
|
|
||||||
|
|
||||||
=======================================================================
|
|
||||||
|
|
||||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
|
||||||
does not provide legal services or legal advice. Distribution of
|
|
||||||
Creative Commons public licenses does not create a lawyer-client or
|
|
||||||
other relationship. Creative Commons makes its licenses and related
|
|
||||||
information available on an "as-is" basis. Creative Commons gives no
|
|
||||||
warranties regarding its licenses, any material licensed under their
|
|
||||||
terms and conditions, or any related information. Creative Commons
|
|
||||||
disclaims all liability for damages resulting from their use to the
|
|
||||||
fullest extent possible.
|
|
||||||
|
|
||||||
Using Creative Commons Public Licenses
|
|
||||||
|
|
||||||
Creative Commons public licenses provide a standard set of terms and
|
|
||||||
conditions that creators and other rights holders may use to share
|
|
||||||
original works of authorship and other material subject to copyright
|
|
||||||
and certain other rights specified in the public license below. The
|
|
||||||
following considerations are for informational purposes only, are not
|
|
||||||
exhaustive, and do not form part of our licenses.
|
|
||||||
|
|
||||||
Considerations for licensors: Our public licenses are
|
|
||||||
intended for use by those authorized to give the public
|
|
||||||
permission to use material in ways otherwise restricted by
|
|
||||||
copyright and certain other rights. Our licenses are
|
|
||||||
irrevocable. Licensors should read and understand the terms
|
|
||||||
and conditions of the license they choose before applying it.
|
|
||||||
Licensors should also secure all rights necessary before
|
|
||||||
applying our licenses so that the public can reuse the
|
|
||||||
material as expected. Licensors should clearly mark any
|
|
||||||
material not subject to the license. This includes other CC-
|
|
||||||
licensed material, or material used under an exception or
|
|
||||||
limitation to copyright. More considerations for licensors:
|
|
||||||
wiki.creativecommons.org/Considerations_for_licensors
|
|
||||||
|
|
||||||
Considerations for the public: By using one of our public
|
|
||||||
licenses, a licensor grants the public permission to use the
|
|
||||||
licensed material under specified terms and conditions. If
|
|
||||||
the licensor's permission is not necessary for any reason--for
|
|
||||||
example, because of any applicable exception or limitation to
|
|
||||||
copyright--then that use is not regulated by the license. Our
|
|
||||||
licenses grant only permissions under copyright and certain
|
|
||||||
other rights that a licensor has authority to grant. Use of
|
|
||||||
the licensed material may still be restricted for other
|
|
||||||
reasons, including because others have copyright or other
|
|
||||||
rights in the material. A licensor may make special requests,
|
|
||||||
such as asking that all changes be marked or described.
|
|
||||||
Although not required by our licenses, you are encouraged to
|
|
||||||
respect those requests where reasonable. More considerations
|
|
||||||
for the public:
|
|
||||||
wiki.creativecommons.org/Considerations_for_licensees
|
|
||||||
|
|
||||||
=======================================================================
|
|
||||||
|
|
||||||
Creative Commons Attribution 4.0 International Public License
|
|
||||||
|
|
||||||
By exercising the Licensed Rights (defined below), You accept and agree
|
|
||||||
to be bound by the terms and conditions of this Creative Commons
|
|
||||||
Attribution 4.0 International Public License ("Public License"). To the
|
|
||||||
extent this Public License may be interpreted as a contract, You are
|
|
||||||
granted the Licensed Rights in consideration of Your acceptance of
|
|
||||||
these terms and conditions, and the Licensor grants You such rights in
|
|
||||||
consideration of benefits the Licensor receives from making the
|
|
||||||
Licensed Material available under these terms and conditions.
|
|
||||||
|
|
||||||
|
|
||||||
Section 1 -- Definitions.
|
|
||||||
|
|
||||||
a. Adapted Material means material subject to Copyright and Similar
|
|
||||||
Rights that is derived from or based upon the Licensed Material
|
|
||||||
and in which the Licensed Material is translated, altered,
|
|
||||||
arranged, transformed, or otherwise modified in a manner requiring
|
|
||||||
permission under the Copyright and Similar Rights held by the
|
|
||||||
Licensor. For purposes of this Public License, where the Licensed
|
|
||||||
Material is a musical work, performance, or sound recording,
|
|
||||||
Adapted Material is always produced where the Licensed Material is
|
|
||||||
synched in timed relation with a moving image.
|
|
||||||
|
|
||||||
b. Adapter's License means the license You apply to Your Copyright
|
|
||||||
and Similar Rights in Your contributions to Adapted Material in
|
|
||||||
accordance with the terms and conditions of this Public License.
|
|
||||||
|
|
||||||
c. Copyright and Similar Rights means copyright and/or similar rights
|
|
||||||
closely related to copyright including, without limitation,
|
|
||||||
performance, broadcast, sound recording, and Sui Generis Database
|
|
||||||
Rights, without regard to how the rights are labeled or
|
|
||||||
categorized. For purposes of this Public License, the rights
|
|
||||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
|
||||||
Rights.
|
|
||||||
|
|
||||||
d. Effective Technological Measures means those measures that, in the
|
|
||||||
absence of proper authority, may not be circumvented under laws
|
|
||||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
|
||||||
Treaty adopted on December 20, 1996, and/or similar international
|
|
||||||
agreements.
|
|
||||||
|
|
||||||
e. Exceptions and Limitations means fair use, fair dealing, and/or
|
|
||||||
any other exception or limitation to Copyright and Similar Rights
|
|
||||||
that applies to Your use of the Licensed Material.
|
|
||||||
|
|
||||||
f. Licensed Material means the artistic or literary work, database,
|
|
||||||
or other material to which the Licensor applied this Public
|
|
||||||
License.
|
|
||||||
|
|
||||||
g. Licensed Rights means the rights granted to You subject to the
|
|
||||||
terms and conditions of this Public License, which are limited to
|
|
||||||
all Copyright and Similar Rights that apply to Your use of the
|
|
||||||
Licensed Material and that the Licensor has authority to license.
|
|
||||||
|
|
||||||
h. Licensor means the individual(s) or entity(ies) granting rights
|
|
||||||
under this Public License.
|
|
||||||
|
|
||||||
i. Share means to provide material to the public by any means or
|
|
||||||
process that requires permission under the Licensed Rights, such
|
|
||||||
as reproduction, public display, public performance, distribution,
|
|
||||||
dissemination, communication, or importation, and to make material
|
|
||||||
available to the public including in ways that members of the
|
|
||||||
public may access the material from a place and at a time
|
|
||||||
individually chosen by them.
|
|
||||||
|
|
||||||
j. Sui Generis Database Rights means rights other than copyright
|
|
||||||
resulting from Directive 96/9/EC of the European Parliament and of
|
|
||||||
the Council of 11 March 1996 on the legal protection of databases,
|
|
||||||
as amended and/or succeeded, as well as other essentially
|
|
||||||
equivalent rights anywhere in the world.
|
|
||||||
|
|
||||||
k. You means the individual or entity exercising the Licensed Rights
|
|
||||||
under this Public License. Your has a corresponding meaning.
|
|
||||||
|
|
||||||
|
|
||||||
Section 2 -- Scope.
|
|
||||||
|
|
||||||
a. License grant.
|
|
||||||
|
|
||||||
1. Subject to the terms and conditions of this Public License,
|
|
||||||
the Licensor hereby grants You a worldwide, royalty-free,
|
|
||||||
non-sublicensable, non-exclusive, irrevocable license to
|
|
||||||
exercise the Licensed Rights in the Licensed Material to:
|
|
||||||
|
|
||||||
a. reproduce and Share the Licensed Material, in whole or
|
|
||||||
in part; and
|
|
||||||
|
|
||||||
b. produce, reproduce, and Share Adapted Material.
|
|
||||||
|
|
||||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
|
||||||
Exceptions and Limitations apply to Your use, this Public
|
|
||||||
License does not apply, and You do not need to comply with
|
|
||||||
its terms and conditions.
|
|
||||||
|
|
||||||
3. Term. The term of this Public License is specified in Section
|
|
||||||
6(a).
|
|
||||||
|
|
||||||
4. Media and formats; technical modifications allowed. The
|
|
||||||
Licensor authorizes You to exercise the Licensed Rights in
|
|
||||||
all media and formats whether now known or hereafter created,
|
|
||||||
and to make technical modifications necessary to do so. The
|
|
||||||
Licensor waives and/or agrees not to assert any right or
|
|
||||||
authority to forbid You from making technical modifications
|
|
||||||
necessary to exercise the Licensed Rights, including
|
|
||||||
technical modifications necessary to circumvent Effective
|
|
||||||
Technological Measures. For purposes of this Public License,
|
|
||||||
simply making modifications authorized by this Section 2(a)
|
|
||||||
(4) never produces Adapted Material.
|
|
||||||
|
|
||||||
5. Downstream recipients.
|
|
||||||
|
|
||||||
a. Offer from the Licensor -- Licensed Material. Every
|
|
||||||
recipient of the Licensed Material automatically
|
|
||||||
receives an offer from the Licensor to exercise the
|
|
||||||
Licensed Rights under the terms and conditions of this
|
|
||||||
Public License.
|
|
||||||
|
|
||||||
b. No downstream restrictions. You may not offer or impose
|
|
||||||
any additional or different terms or conditions on, or
|
|
||||||
apply any Effective Technological Measures to, the
|
|
||||||
Licensed Material if doing so restricts exercise of the
|
|
||||||
Licensed Rights by any recipient of the Licensed
|
|
||||||
Material.
|
|
||||||
|
|
||||||
6. No endorsement. Nothing in this Public License constitutes or
|
|
||||||
may be construed as permission to assert or imply that You
|
|
||||||
are, or that Your use of the Licensed Material is, connected
|
|
||||||
with, or sponsored, endorsed, or granted official status by,
|
|
||||||
the Licensor or others designated to receive attribution as
|
|
||||||
provided in Section 3(a)(1)(A)(i).
|
|
||||||
|
|
||||||
b. Other rights.
|
|
||||||
|
|
||||||
1. Moral rights, such as the right of integrity, are not
|
|
||||||
licensed under this Public License, nor are publicity,
|
|
||||||
privacy, and/or other similar personality rights; however, to
|
|
||||||
the extent possible, the Licensor waives and/or agrees not to
|
|
||||||
assert any such rights held by the Licensor to the limited
|
|
||||||
extent necessary to allow You to exercise the Licensed
|
|
||||||
Rights, but not otherwise.
|
|
||||||
|
|
||||||
2. Patent and trademark rights are not licensed under this
|
|
||||||
Public License.
|
|
||||||
|
|
||||||
3. To the extent possible, the Licensor waives any right to
|
|
||||||
collect royalties from You for the exercise of the Licensed
|
|
||||||
Rights, whether directly or through a collecting society
|
|
||||||
under any voluntary or waivable statutory or compulsory
|
|
||||||
licensing scheme. In all other cases the Licensor expressly
|
|
||||||
reserves any right to collect such royalties.
|
|
||||||
|
|
||||||
|
|
||||||
Section 3 -- License Conditions.
|
|
||||||
|
|
||||||
Your exercise of the Licensed Rights is expressly made subject to the
|
|
||||||
following conditions.
|
|
||||||
|
|
||||||
a. Attribution.
|
|
||||||
|
|
||||||
1. If You Share the Licensed Material (including in modified
|
|
||||||
form), You must:
|
|
||||||
|
|
||||||
a. retain the following if it is supplied by the Licensor
|
|
||||||
with the Licensed Material:
|
|
||||||
|
|
||||||
i. identification of the creator(s) of the Licensed
|
|
||||||
Material and any others designated to receive
|
|
||||||
attribution, in any reasonable manner requested by
|
|
||||||
the Licensor (including by pseudonym if
|
|
||||||
designated);
|
|
||||||
|
|
||||||
ii. a copyright notice;
|
|
||||||
|
|
||||||
iii. a notice that refers to this Public License;
|
|
||||||
|
|
||||||
iv. a notice that refers to the disclaimer of
|
|
||||||
warranties;
|
|
||||||
|
|
||||||
v. a URI or hyperlink to the Licensed Material to the
|
|
||||||
extent reasonably practicable;
|
|
||||||
|
|
||||||
b. indicate if You modified the Licensed Material and
|
|
||||||
retain an indication of any previous modifications; and
|
|
||||||
|
|
||||||
c. indicate the Licensed Material is licensed under this
|
|
||||||
Public License, and include the text of, or the URI or
|
|
||||||
hyperlink to, this Public License.
|
|
||||||
|
|
||||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
|
||||||
reasonable manner based on the medium, means, and context in
|
|
||||||
which You Share the Licensed Material. For example, it may be
|
|
||||||
reasonable to satisfy the conditions by providing a URI or
|
|
||||||
hyperlink to a resource that includes the required
|
|
||||||
information.
|
|
||||||
|
|
||||||
3. If requested by the Licensor, You must remove any of the
|
|
||||||
information required by Section 3(a)(1)(A) to the extent
|
|
||||||
reasonably practicable.
|
|
||||||
|
|
||||||
4. If You Share Adapted Material You produce, the Adapter's
|
|
||||||
License You apply must not prevent recipients of the Adapted
|
|
||||||
Material from complying with this Public License.
|
|
||||||
|
|
||||||
|
|
||||||
Section 4 -- Sui Generis Database Rights.
|
|
||||||
|
|
||||||
Where the Licensed Rights include Sui Generis Database Rights that
|
|
||||||
apply to Your use of the Licensed Material:
|
|
||||||
|
|
||||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
|
||||||
to extract, reuse, reproduce, and Share all or a substantial
|
|
||||||
portion of the contents of the database;
|
|
||||||
|
|
||||||
b. if You include all or a substantial portion of the database
|
|
||||||
contents in a database in which You have Sui Generis Database
|
|
||||||
Rights, then the database in which You have Sui Generis Database
|
|
||||||
Rights (but not its individual contents) is Adapted Material; and
|
|
||||||
|
|
||||||
c. You must comply with the conditions in Section 3(a) if You Share
|
|
||||||
all or a substantial portion of the contents of the database.
|
|
||||||
|
|
||||||
For the avoidance of doubt, this Section 4 supplements and does not
|
|
||||||
replace Your obligations under this Public License where the Licensed
|
|
||||||
Rights include other Copyright and Similar Rights.
|
|
||||||
|
|
||||||
|
|
||||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
|
||||||
|
|
||||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
|
||||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
|
||||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
|
||||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
|
||||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
|
||||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
|
||||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
|
||||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
|
||||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
|
||||||
|
|
||||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
|
||||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
|
||||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
|
||||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
|
||||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
|
||||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
|
||||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
|
||||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
|
||||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
|
||||||
|
|
||||||
c. The disclaimer of warranties and limitation of liability provided
|
|
||||||
above shall be interpreted in a manner that, to the extent
|
|
||||||
possible, most closely approximates an absolute disclaimer and
|
|
||||||
waiver of all liability.
|
|
||||||
|
|
||||||
|
|
||||||
Section 6 -- Term and Termination.
|
|
||||||
|
|
||||||
a. This Public License applies for the term of the Copyright and
|
|
||||||
Similar Rights licensed here. However, if You fail to comply with
|
|
||||||
this Public License, then Your rights under this Public License
|
|
||||||
terminate automatically.
|
|
||||||
|
|
||||||
b. Where Your right to use the Licensed Material has terminated under
|
|
||||||
Section 6(a), it reinstates:
|
|
||||||
|
|
||||||
1. automatically as of the date the violation is cured, provided
|
|
||||||
it is cured within 30 days of Your discovery of the
|
|
||||||
violation; or
|
|
||||||
|
|
||||||
2. upon express reinstatement by the Licensor.
|
|
||||||
|
|
||||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
|
||||||
right the Licensor may have to seek remedies for Your violations
|
|
||||||
of this Public License.
|
|
||||||
|
|
||||||
c. For the avoidance of doubt, the Licensor may also offer the
|
|
||||||
Licensed Material under separate terms or conditions or stop
|
|
||||||
distributing the Licensed Material at any time; however, doing so
|
|
||||||
will not terminate this Public License.
|
|
||||||
|
|
||||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
|
||||||
License.
|
|
||||||
|
|
||||||
|
|
||||||
Section 7 -- Other Terms and Conditions.
|
|
||||||
|
|
||||||
a. The Licensor shall not be bound by any additional or different
|
|
||||||
terms or conditions communicated by You unless expressly agreed.
|
|
||||||
|
|
||||||
b. Any arrangements, understandings, or agreements regarding the
|
|
||||||
Licensed Material not stated herein are separate from and
|
|
||||||
independent of the terms and conditions of this Public License.
|
|
||||||
|
|
||||||
|
|
||||||
Section 8 -- Interpretation.
|
|
||||||
|
|
||||||
a. For the avoidance of doubt, this Public License does not, and
|
|
||||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
|
||||||
conditions on any use of the Licensed Material that could lawfully
|
|
||||||
be made without permission under this Public License.
|
|
||||||
|
|
||||||
b. To the extent possible, if any provision of this Public License is
|
|
||||||
deemed unenforceable, it shall be automatically reformed to the
|
|
||||||
minimum extent necessary to make it enforceable. If the provision
|
|
||||||
cannot be reformed, it shall be severed from this Public License
|
|
||||||
without affecting the enforceability of the remaining terms and
|
|
||||||
conditions.
|
|
||||||
|
|
||||||
c. No term or condition of this Public License will be waived and no
|
|
||||||
failure to comply consented to unless expressly agreed to by the
|
|
||||||
Licensor.
|
|
||||||
|
|
||||||
d. Nothing in this Public License constitutes or may be interpreted
|
|
||||||
as a limitation upon, or waiver of, any privileges and immunities
|
|
||||||
that apply to the Licensor or You, including from the legal
|
|
||||||
processes of any jurisdiction or authority.
|
|
||||||
|
|
||||||
|
|
||||||
=======================================================================
|
|
||||||
|
|
||||||
Creative Commons is not a party to its public
|
|
||||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
|
||||||
its public licenses to material it publishes and in those instances
|
|
||||||
will be considered the “Licensor.” The text of the Creative Commons
|
|
||||||
public licenses is dedicated to the public domain under the CC0 Public
|
|
||||||
Domain Dedication. Except for the limited purpose of indicating that
|
|
||||||
material is shared under a Creative Commons public license or as
|
|
||||||
otherwise permitted by the Creative Commons policies published at
|
|
||||||
creativecommons.org/policies, Creative Commons does not authorize the
|
|
||||||
use of the trademark "Creative Commons" or any other trademark or logo
|
|
||||||
of Creative Commons without its prior written consent including,
|
|
||||||
without limitation, in connection with any unauthorized modifications
|
|
||||||
to any of its public licenses or any other arrangements,
|
|
||||||
understandings, or agreements concerning use of licensed material. For
|
|
||||||
the avoidance of doubt, this paragraph does not form part of the
|
|
||||||
public licenses.
|
|
||||||
|
|
||||||
Creative Commons may be contacted at creativecommons.org.
|
|
||||||
|
|
61
README.md
|
@ -1,61 +0,0 @@
|
||||||
# Coronamap
|
|
||||||
|
|
||||||
Coronamap is an interactive thematic map that animates the spread of the coronavirus.
|
|
||||||
|
|
||||||
Check out the [deployment](../../deployments "Deployment").
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="docs/assets/img/desktop.png" width=55% alt="Coronmap on laptop">
|
|
||||||
<img src="docs/assets/img/mobile.png" width=20% alt="Coronamap on phone"/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
## Preview
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<img src="docs/assets/img/animation.gif" width="75%" alt="Coronamap preview"/>
|
|
||||||
</p>
|
|
||||||
Disease data: <a href="https://github.com/CSSEGISandData/COVID-19">Johns Hopkins CSSE</a> (https://github.com/CSSEGISandData/COVID-19)
|
|
||||||
|
|
||||||
### Date Slider
|
|
||||||
|
|
||||||
<img src="docs/assets/img/timecontrol.png" height="32px" alt="The date slider consisting of several form controls, as follows">
|
|
||||||
|
|
||||||
- <img src="docs/assets/img/timecontrol-play.png" height="28px" alt="Right-pointing triangle button">: Play
|
|
||||||
- <img src="docs/assets/img/timecontrol-reverse.png" height="28px" alt="Left-pointing triangle button">: Reverse
|
|
||||||
- <img src="docs/assets/img/timecontrol-forward.png" height="28px" alt="Right-pointing double triangle button">: Move to the next day
|
|
||||||
- <img src="docs/assets/img/timecontrol-backward.png" height="28px" alt="Left-pointing double triangle button">: Move to the previous day
|
|
||||||
- <img src="docs/assets/img/timecontrol-loop.png" height="28px" alt="Button with circular arrow pointing in a clockwise direction">: Loop
|
|
||||||
- <img src="docs/assets/img/timecontrol-dateslider.png" height="28px" alt="Range input type positioned to the middle">: Date progress bar
|
|
||||||
- <img src="docs/assets/img/timecontrol-fps.png" height="28px" alt="Range input type labeled with fps value and a clock icon to the left">: Playback speed
|
|
||||||
|
|
||||||
### Colored Geographical Areas
|
|
||||||
|
|
||||||
<img src="docs/assets/img/choropleth-legend.png" alt="A yellow to red gradient color ramp legend labeled with the range of infected cases" height="32px">
|
|
||||||
|
|
||||||
The yellow-orange-red sequential color scheme shows the *number of infected cases*.
|
|
||||||
|
|
||||||
### Circle Markers
|
|
||||||
|
|
||||||
<img src="docs/assets/img/circlemarker.png" alt="" width=10> <img src="docs/assets/img/circlemarker.png" alt="" width=15> <img src="docs/assets/img/circlemarker.png" alt="" width=20> <img src="docs/assets/img/circlemarker.png" alt="" width=25> <img src="docs/assets/img/circlemarker.png" alt="" width=30>
|
|
||||||
|
|
||||||
The size of a circle marker scales to the *number of deaths*.
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
* Node.js
|
|
||||||
```bash
|
|
||||||
$ yum install nodejs
|
|
||||||
$ yum install npm
|
|
||||||
```
|
|
||||||
|
|
||||||
* Install npm packages
|
|
||||||
```bash
|
|
||||||
$ npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development
|
|
||||||
* Watch for updates to code and compile automatically: `npm run develop`
|
|
||||||
* Build the optimized production: `npm run build`
|
|
||||||
* Run all unit tests: `npm run test`
|
|
Before Width: | Height: | Size: 17 MiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 808 B |
Before Width: | Height: | Size: 123 KiB |
Before Width: | Height: | Size: 162 KiB |
Before Width: | Height: | Size: 527 B |
Before Width: | Height: | Size: 573 B |
Before Width: | Height: | Size: 562 B |
Before Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 502 B |
Before Width: | Height: | Size: 489 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 738 B |
Before Width: | Height: | Size: 225 B |
Before Width: | Height: | Size: 275 B |
Before Width: | Height: | Size: 5 KiB |
4463
package-lock.json
generated
17
package.json
|
@ -1,17 +0,0 @@
|
||||||
{
|
|
||||||
"devDependencies": {
|
|
||||||
"ts-loader": "^6.2.1",
|
|
||||||
"typescript": "^3.8.3",
|
|
||||||
"webpack": "^4.42.0",
|
|
||||||
"webpack-cli": "^3.3.11"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@mapbox/geojson-merge": "^1.1.1",
|
|
||||||
"@types/node": "^13.11.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo 'test not implemented'",
|
|
||||||
"develop": "webpack --mode development --watch",
|
|
||||||
"build": "webpack --mode production"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,200 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { TimeDimension } from "./TimeDimension";
|
|
||||||
var geojsonMerge = require('@mapbox/geojson-merge');
|
|
||||||
|
|
||||||
export abstract class AbstractReader {
|
|
||||||
public static readonly BASE_URL: string = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/";
|
|
||||||
private confirmedGeoJson: object;
|
|
||||||
private deathsGeoJson: object;
|
|
||||||
private recoveredGeoJson: object;
|
|
||||||
|
|
||||||
public constructor() { }
|
|
||||||
|
|
||||||
public abstract init(): void;
|
|
||||||
|
|
||||||
public abstract loadGeoJsonFile(): object;
|
|
||||||
|
|
||||||
// Retrieve csv data from url
|
|
||||||
public readCsv(scope: string): string {
|
|
||||||
var response;
|
|
||||||
$.ajax({
|
|
||||||
url: scope,
|
|
||||||
async: false,
|
|
||||||
success: function (data) {
|
|
||||||
response = data;
|
|
||||||
},
|
|
||||||
dataType: 'text'
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
};
|
|
||||||
|
|
||||||
public csvToObject(csv: string): object {
|
|
||||||
return $.csv.toObjects(csv);
|
|
||||||
}
|
|
||||||
|
|
||||||
public replaceColumnKeys(csv: any, dict: object): string {
|
|
||||||
var header = csv.split("\n")[0];
|
|
||||||
for (let key of Object.keys(dict)) {
|
|
||||||
header = header.replace(key, dict[key]);
|
|
||||||
}
|
|
||||||
return header + '\r\n' + csv.split("\n").slice(1).join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find different data between geojson and csv, and equalize different region names between geojson and csv data
|
|
||||||
public replaceColumnValues(csv: any, dict: object, colName: string): object {
|
|
||||||
var regex = new RegExp('^' + Object.keys(dict).map(_ => _.replace(/[+?^*${}()|[\]\\]/ig, '\\$&')).join('$|^') + '$', 'gi');
|
|
||||||
|
|
||||||
$(csv).each(function(index, value) {
|
|
||||||
value[colName] = value[colName].replace(regex, function(match) { return dict[match]; });
|
|
||||||
});
|
|
||||||
|
|
||||||
return csv;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return property values in csv, not in geojson, or empty object if there is no different values between geojson and csv
|
|
||||||
public comparePropertyValues(csv: any, geoJson: object, colName: string, propertyKey: string): object {
|
|
||||||
return $(csv.map(_ => _[colName])).not(geoJson).get().filter(function(v, i, _) { return _.indexOf(v) >= i; });
|
|
||||||
}
|
|
||||||
|
|
||||||
public setPropertyValues(csv: any, geoJson: any, colName: string, geoJsonKey: string): object {
|
|
||||||
for (let value of geoJson.features) {
|
|
||||||
var id = 0;
|
|
||||||
for (let csvIdx in csv) {
|
|
||||||
// If geojson properties name value matches to csv column value
|
|
||||||
if (value.properties[geoJsonKey] == csv[csvIdx][colName]) {
|
|
||||||
// Set properties in geojson with new values
|
|
||||||
value.properties[id] = csv[csvIdx];
|
|
||||||
id++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.pushTotalNumberOfCaseToProperties(geoJson);
|
|
||||||
return geoJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getProperty(features: any, propertyNames: string[]): any[] {
|
|
||||||
var values = [];
|
|
||||||
for (let i in Object.values(features)) {
|
|
||||||
if (features[i].properties['Country/Region']) {
|
|
||||||
for (let pname of propertyNames) {
|
|
||||||
if (!values[pname]) {
|
|
||||||
values[pname] = [];
|
|
||||||
}
|
|
||||||
values[pname].push(features[i].properties[pname]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static mergeGeoJsons(geoJson: object, otherGeoJson: object): object {
|
|
||||||
var mergedGeoJson = geojsonMerge.merge([
|
|
||||||
geoJson,
|
|
||||||
otherGeoJson
|
|
||||||
]);
|
|
||||||
return mergedGeoJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setConfirmedGeoJson(geoJson: object): void {
|
|
||||||
this.confirmedGeoJson = geoJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getConfirmedGeoJson(): object {
|
|
||||||
return this.confirmedGeoJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setDeathsGeoJson(geoJson: object): void {
|
|
||||||
this.deathsGeoJson = geoJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getDeathsGeoJson(): object {
|
|
||||||
return this.deathsGeoJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setRecoveredGeoJson(geoJson: object): void {
|
|
||||||
this.recoveredGeoJson = geoJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRecoveredGeoJson(): object {
|
|
||||||
return this.recoveredGeoJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
public replaceText(text: string, from: string, to: string, repeat: boolean = false): string {
|
|
||||||
return text.replace(repeat ? '\/' + from + '\/g' : from, to);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNumberOfCase(features: any, time: string, latlong: [number, number]): number {
|
|
||||||
for (let featIdx = 0, len = Object.keys(features).length; featIdx < len; featIdx++) {
|
|
||||||
var prop = features[featIdx]['properties'];
|
|
||||||
|
|
||||||
if (prop.Lat == latlong[0] && prop.Long == latlong[1]) {
|
|
||||||
return prop[time];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let propIdx = 0, len = Object.keys(prop).length; propIdx < len; propIdx++) {
|
|
||||||
if (prop[propIdx]) {
|
|
||||||
if (prop[propIdx].Lat == latlong[0] && prop[propIdx].Long == latlong[1]) {
|
|
||||||
return prop[propIdx][time];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
public subtractRecoveredFromConfirmed(confirmedGeoJson: any, recoveredGeoJson: any): object {
|
|
||||||
var clonedConfirmedGeoJson = confirmedGeoJson;
|
|
||||||
// Iterate each country in confirmed geojson
|
|
||||||
for (let confirmedCountry of Object.values(clonedConfirmedGeoJson)) {
|
|
||||||
// Iterate each country in recovered geojson
|
|
||||||
for (let recoveredCountry of Object.values(recoveredGeoJson)) {
|
|
||||||
// For each key from each country
|
|
||||||
for (let key of Object.keys(confirmedCountry)) {
|
|
||||||
// Find the exact same country and state names in two geojsons
|
|
||||||
if (confirmedCountry['Province/State'] === recoveredCountry['Province/State'] && confirmedCountry['Country/Region'] === recoveredCountry['Country/Region']) {
|
|
||||||
// If key is date
|
|
||||||
if (moment(key).isValid()) {
|
|
||||||
// Subtract number of recovered cases from number of confirmed cases on the date
|
|
||||||
confirmedCountry[key] -= recoveredCountry[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return clonedConfirmedGeoJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
private pushTotalNumberOfCaseToProperties(geoJson: any): void {
|
|
||||||
// Iterate geojson
|
|
||||||
for (let featIdx = 0, len = Object.keys(geoJson.features).length; featIdx < len; featIdx++) {
|
|
||||||
var totalCaseByDate = {};
|
|
||||||
// Iterate properties
|
|
||||||
for (let keyIdx = 0, len = Object.keys(geoJson.features[featIdx].properties).length; keyIdx < len; keyIdx++) {
|
|
||||||
var key = Object.keys(geoJson.features[featIdx].properties)[keyIdx];
|
|
||||||
// Cities and counties are object
|
|
||||||
if(geoJson.features[featIdx].properties[key] && typeof geoJson.features[featIdx].properties[key] === 'object') {
|
|
||||||
// Iterate keys in each object
|
|
||||||
for (let objectKey of Object.keys(geoJson.features[featIdx].properties[key])) {
|
|
||||||
// If key is date
|
|
||||||
if (moment(objectKey).isValid()) {
|
|
||||||
var date = moment(objectKey).format(TimeDimension.DATE_FORMAT);
|
|
||||||
// Stack number of cases in this city or county
|
|
||||||
if (!totalCaseByDate[date]) {
|
|
||||||
totalCaseByDate[date] = parseInt(geoJson.features[featIdx].properties[key][objectKey]);
|
|
||||||
} else if (totalCaseByDate[date] > 0) {
|
|
||||||
totalCaseByDate[date] += parseInt(geoJson.features[featIdx].properties[key][objectKey]);
|
|
||||||
}
|
|
||||||
} else if (objectKey === 'Country/Region') {
|
|
||||||
totalCaseByDate[objectKey] = geoJson.features[featIdx].properties[key][objectKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Add total number of cases in scope of properties
|
|
||||||
for (let country in totalCaseByDate) {
|
|
||||||
geoJson.features[featIdx].properties[country] = totalCaseByDate[country];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,174 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { Map } from "./Map";
|
|
||||||
import { ITemporal } from "./ITemporal";
|
|
||||||
|
|
||||||
export enum ChoroplethMode {
|
|
||||||
Quantile = "q",
|
|
||||||
Equidistant = "e",
|
|
||||||
Kmeans = "k"
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Choropleth implements ITemporal {
|
|
||||||
public static readonly DEFAULT_SCALE: string[] = ['#fff', '#ffefac', '#fbc750', '#f6b340', '#f09e33', '#e98828', '#e1731e', '#d95b17', '#d14211', '#c81e0d'];
|
|
||||||
public static readonly DEFAULT_BORDER_COLOR: string = '#666';
|
|
||||||
public static readonly DEFAULT_FILL_OPACITY: number = 0.6;
|
|
||||||
public static readonly HIGHLIGHT_FILL_OPACITY: number = 0.65;
|
|
||||||
public static readonly DEFAULT_MODE: ChoroplethMode = ChoroplethMode.Quantile;
|
|
||||||
public static readonly DEFAULT_WEIGHT: number = 1;
|
|
||||||
public static readonly HIGHLIGHT_WEIGHT: number = 1.75;
|
|
||||||
public static readonly LEGEND_CONTROL_POSITION: string = 'bottomleft';
|
|
||||||
public static readonly LEGEND_ELEMENT_CLASS: string = 'choropleth-legend';
|
|
||||||
public static readonly LEGEND_COLOR_WIDTHS: number[] = [1, 1, 1.5, 1.5, 1.5, 2, 2.5, 3, 5];
|
|
||||||
private scale: string[];
|
|
||||||
private borderColor: string;
|
|
||||||
private fillOpacity: number;
|
|
||||||
private mode: ChoroplethMode;
|
|
||||||
|
|
||||||
public constructor(scale?: string[], borderColor?: string, fillOpacity?: number, mode?: ChoroplethMode) {
|
|
||||||
this.scale = !scale || scale.length < 1 ? Choropleth.DEFAULT_SCALE : scale;
|
|
||||||
this.borderColor = !borderColor ? Choropleth.DEFAULT_BORDER_COLOR : borderColor;
|
|
||||||
this.fillOpacity = !fillOpacity ? Choropleth.DEFAULT_FILL_OPACITY : fillOpacity;
|
|
||||||
this.mode = !mode ? Choropleth.DEFAULT_MODE : mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(geoJson: object): any {
|
|
||||||
var choroplethLayer = L.choropleth(geoJson, {
|
|
||||||
// Set properties to use
|
|
||||||
valueProperty: Map.getInstance().getTimeDimension().getCurrentTime(),
|
|
||||||
scale: this.scale,
|
|
||||||
mode: this.mode,
|
|
||||||
step: this.scale.length,
|
|
||||||
style: {
|
|
||||||
color: this.borderColor,
|
|
||||||
weight: 1,
|
|
||||||
fillOpacity: this.fillOpacity
|
|
||||||
},
|
|
||||||
onEachFeature: onEachFeature
|
|
||||||
});
|
|
||||||
|
|
||||||
this.createLegend(choroplethLayer);
|
|
||||||
|
|
||||||
return choroplethLayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public createLegend(choroplethLayer: any): void {
|
|
||||||
// Adapted from Tim Wisniewski's example: https://github.com/timwis/leaflet-choropleth/blob/gh-pages/examples/legend/demo.js retrieved in March 2020.
|
|
||||||
var legend = L.control({ position: Choropleth.LEGEND_CONTROL_POSITION });
|
|
||||||
legend.onAdd = function() {
|
|
||||||
var div: Element;
|
|
||||||
if ($('div.' + Choropleth.LEGEND_ELEMENT_CLASS).length > 0) {
|
|
||||||
div = document.querySelectorAll('div.' + Choropleth.LEGEND_ELEMENT_CLASS)[0];
|
|
||||||
} else {
|
|
||||||
div = L.DomUtil.create('div', Choropleth.LEGEND_ELEMENT_CLASS);
|
|
||||||
}
|
|
||||||
|
|
||||||
var limits = Choropleth.pushAverageOfEveryElement(choroplethLayer.options.limits);
|
|
||||||
limits[0] = 0;
|
|
||||||
choroplethLayer.options.limits = limits;
|
|
||||||
|
|
||||||
var colors = Choropleth.DEFAULT_SCALE;
|
|
||||||
colors = colors.slice(1, colors.length);
|
|
||||||
choroplethLayer.options.colors = colors;
|
|
||||||
|
|
||||||
var labels = [];
|
|
||||||
|
|
||||||
// Add min and max labels
|
|
||||||
div.innerHTML = '<div class="min"><span>Cases:</span> ' + limits[0] + '</div><div class="max">' + limits[limits.length - 1] + '</div></div>';
|
|
||||||
// Style
|
|
||||||
limits.forEach(function (limit, index) {
|
|
||||||
labels.push('<li style="background-color:' + colors[index]
|
|
||||||
+ ';width:' + Choropleth.LEGEND_COLOR_WIDTHS[index] + 'vw;"></li>');
|
|
||||||
});
|
|
||||||
div.innerHTML += '<ul>' + labels.join('') + '</ul>';
|
|
||||||
|
|
||||||
return div;
|
|
||||||
}
|
|
||||||
legend.addTo(Map.getInstance().getMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
public setScale(scale: string[]): void {
|
|
||||||
this.scale = scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getScale(): string[] {
|
|
||||||
return this.scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setBorderColor(borderColor: string): void {
|
|
||||||
this.borderColor = borderColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getBorderColor(): string {
|
|
||||||
return this.borderColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setFillOpacity(fillOpacity: number): void {
|
|
||||||
this.fillOpacity = fillOpacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFillOpacity(): number {
|
|
||||||
return this.fillOpacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setMode(mode: ChoroplethMode): void {
|
|
||||||
this.mode = mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMode(): ChoroplethMode {
|
|
||||||
return this.mode;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static pushAverageOfEveryElement(array: number[]): number[] {
|
|
||||||
var newArray = [];
|
|
||||||
for (let element in array) {
|
|
||||||
var index = parseInt(element);
|
|
||||||
newArray.push(array[index]);
|
|
||||||
newArray.push((array[index] + array[index + 1]) / 2);
|
|
||||||
}
|
|
||||||
newArray.pop();
|
|
||||||
return newArray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adapted from Leaflet's example: https://leafletjs.com/examples/choropleth retrieved in March 2020.
|
|
||||||
export function onEachFeature(_, layer) {
|
|
||||||
layer.on({
|
|
||||||
mouseover: highlightFeature,
|
|
||||||
mouseout: resetHighlight,
|
|
||||||
click: zoomToFeature
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetHighlight(e) {
|
|
||||||
// Iterate each map layer and run resetStyle() function
|
|
||||||
$.each(Map.getInstance().getTimeDimension().getLayers(), function(index, value) {
|
|
||||||
try {
|
|
||||||
var layer = e.target;
|
|
||||||
layer.setStyle({
|
|
||||||
weight: Choropleth.DEFAULT_WEIGHT,
|
|
||||||
color: Choropleth.DEFAULT_BORDER_COLOR,
|
|
||||||
fillOpacity: Choropleth.DEFAULT_FILL_OPACITY,
|
|
||||||
});
|
|
||||||
if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
|
|
||||||
layer.bringToFront();
|
|
||||||
}
|
|
||||||
} catch(e) { }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function zoomToFeature(e) {
|
|
||||||
Map.getInstance().getMap().fitBounds(e.target.getBounds());
|
|
||||||
}
|
|
||||||
|
|
||||||
function highlightFeature(e) {
|
|
||||||
var layer = e.target;
|
|
||||||
layer.setStyle({
|
|
||||||
weight: Choropleth.HIGHLIGHT_WEIGHT,
|
|
||||||
fillOpacity: Choropleth.HIGHLIGHT_FILL_OPACITY,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
|
|
||||||
layer.bringToFront();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { Map } from "./Map";
|
|
||||||
import { ITemporal } from "./ITemporal";
|
|
||||||
import { AbstractReader } from "./AbstractReader";
|
|
||||||
|
|
||||||
export class CircleMarker implements ITemporal {
|
|
||||||
public static readonly PANE_NAME: string = 'circle-marker-pane';
|
|
||||||
public static readonly COLOR: string = '#a80808';
|
|
||||||
public static readonly OPACITY: number = 0.27;
|
|
||||||
public static readonly BORDER_WEIGHT: number = 0.4;
|
|
||||||
public static readonly RADIUS_NOISE: number = 0.0015;
|
|
||||||
public static readonly RADIUS_MAX: number = 20;
|
|
||||||
public static readonly RADIUS_MIN: number = 2.5;
|
|
||||||
private circles: object[];
|
|
||||||
private fileReader: AbstractReader;
|
|
||||||
private confirmedFeatures: object;
|
|
||||||
|
|
||||||
public constructor(fileReader: AbstractReader, confirmedFeatures: object) {
|
|
||||||
this.fileReader = fileReader;
|
|
||||||
this.confirmedFeatures = confirmedFeatures;
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(geoJson: any) {
|
|
||||||
Map.getInstance().getMap().createPane(CircleMarker.PANE_NAME);
|
|
||||||
|
|
||||||
// Clear all circles
|
|
||||||
this.circles = [];
|
|
||||||
|
|
||||||
// Create circles
|
|
||||||
var features: object[];
|
|
||||||
// Extract a portion of states from end of geojson
|
|
||||||
features = geoJson.features.slice(1).slice(-50);
|
|
||||||
this.createCircleMarkers(features, false);
|
|
||||||
|
|
||||||
// Extract all except states at the end of geojson
|
|
||||||
features = geoJson.features.slice(0, -50);
|
|
||||||
this.createCircleMarkers(features, true);
|
|
||||||
|
|
||||||
return L.layerGroup(this.circles).addTo(Map.getInstance().getMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCircles(): any[] {
|
|
||||||
return this.circles;
|
|
||||||
}
|
|
||||||
|
|
||||||
public createTooltip(marker: any, contentText: string, options?: {}): any {
|
|
||||||
var tooltip = L.tooltip(options).setContent(contentText);
|
|
||||||
// Attach tooltip to circle marker
|
|
||||||
marker.bindTooltip(tooltip).openPopup();
|
|
||||||
return tooltip;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createCircleMarkers(features: any, recursive: boolean = false): void {
|
|
||||||
if (!features || features.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!recursive) {
|
|
||||||
for (let stateIdx = 0, len = Object.keys(features).length; stateIdx < len; stateIdx++) {
|
|
||||||
var state = features[stateIdx].properties;
|
|
||||||
var currentTime = Map.getInstance().getTimeDimension().getCurrentTime();
|
|
||||||
var numOfDeathsAtCurrentTime = state[currentTime];
|
|
||||||
if (numOfDeathsAtCurrentTime && numOfDeathsAtCurrentTime > 0) {
|
|
||||||
try {
|
|
||||||
// Create circle marker
|
|
||||||
var circleMarker = L.circleMarker([state['Lat'], state['Long']], {
|
|
||||||
radius: this.setRadius(numOfDeathsAtCurrentTime * CircleMarker.RADIUS_NOISE),
|
|
||||||
color: CircleMarker.COLOR,
|
|
||||||
fillOpacity: CircleMarker.OPACITY,
|
|
||||||
weight: CircleMarker.BORDER_WEIGHT,
|
|
||||||
pane: CircleMarker.PANE_NAME,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
// Get geographic name
|
|
||||||
var countryName = state['Country/Region'] ? state['Country/Region'] : '';
|
|
||||||
var stateName = state['Province/State'] ? state['Province/State'] : state['name'];
|
|
||||||
if (!stateName) {
|
|
||||||
stateName = '';
|
|
||||||
} else if (countryName) {
|
|
||||||
// Add a comma between country name and state name if a geographical area has both
|
|
||||||
stateName += ', ';
|
|
||||||
}
|
|
||||||
// Show number of confirmed cases
|
|
||||||
var numOfConfirmedCases = this.fileReader.getNumberOfCase(this.confirmedFeatures, currentTime, [state['Lat'], state['Long']]);
|
|
||||||
|
|
||||||
this.createTooltip(circleMarker,
|
|
||||||
'<b>' + stateName + countryName + '</b><br/>' +
|
|
||||||
'Confirmed: ' + numOfConfirmedCases + '<br/>' +
|
|
||||||
'Deaths: ' + numOfDeathsAtCurrentTime
|
|
||||||
, {
|
|
||||||
opacity: 1
|
|
||||||
});
|
|
||||||
this.circles.push(circleMarker);
|
|
||||||
} catch (e) { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var countryFeatures = [];
|
|
||||||
for (let countryIdx = 0, len = Object.keys(features).length; countryIdx < len; countryIdx++) {
|
|
||||||
for (let stateIdx = 0, len = Object.keys(features[countryIdx].properties).length; stateIdx < len; stateIdx++) {
|
|
||||||
var state = features[countryIdx].properties[stateIdx];
|
|
||||||
if (state && typeof state === 'object') {
|
|
||||||
var feature: any = {};
|
|
||||||
feature.properties = state;
|
|
||||||
countryFeatures.push(feature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.createCircleMarkers(countryFeatures, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setRadius(radius: number): number {
|
|
||||||
if (radius > CircleMarker.RADIUS_MAX) {
|
|
||||||
return CircleMarker.RADIUS_MAX;
|
|
||||||
} else if (radius < CircleMarker.RADIUS_MIN) {
|
|
||||||
return CircleMarker.RADIUS_MIN;
|
|
||||||
}
|
|
||||||
return radius;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { Map } from "./Map";
|
|
||||||
import { Database } from "./Database";
|
|
||||||
import { AbstractReader } from "./AbstractReader";
|
|
||||||
import { WorldFileReader } from "./WorldReader";
|
|
||||||
import { StatesFileReader } from "./StatesReader";
|
|
||||||
import { TimeDimension } from "./TimeDimension";
|
|
||||||
import { ITemporal } from "./ITemporal";
|
|
||||||
import { Choropleth } from "./Choropleth";
|
|
||||||
import { CircleMarker } from "./CircleMarker";
|
|
||||||
import { View } from "./View";
|
|
||||||
moment.suppressDeprecationWarnings = true;
|
|
||||||
|
|
||||||
// Measure the time it takes to fully load page
|
|
||||||
const t0 = performance.now();
|
|
||||||
|
|
||||||
var map = Map.getInstance();
|
|
||||||
map.init();
|
|
||||||
var worldFr: AbstractReader = new WorldFileReader();
|
|
||||||
var usFr: AbstractReader = new StatesFileReader();
|
|
||||||
var db = new Database();
|
|
||||||
db.isExpired().then(function(value) {
|
|
||||||
if (value) {
|
|
||||||
worldFr.init();
|
|
||||||
usFr.init();
|
|
||||||
db.clear();
|
|
||||||
db.setExpiryTime();
|
|
||||||
db.setItem(Database.KEY_CONFIRMED_GEOJSON, AbstractReader.mergeGeoJsons(worldFr.getConfirmedGeoJson(), usFr.getConfirmedGeoJson()));
|
|
||||||
db.setItem(Database.KEY_DEATHS_GEOJSON, AbstractReader.mergeGeoJsons(worldFr.getDeathsGeoJson(), usFr.getDeathsGeoJson()));
|
|
||||||
}
|
|
||||||
}).then(() => {
|
|
||||||
db.getItem(Database.KEY_CONFIRMED_GEOJSON).then(function(mergedConfirmedGeoJson) {
|
|
||||||
var choropleth = new Choropleth();
|
|
||||||
var timedimension = new TimeDimension(Array<ITemporal>(choropleth));
|
|
||||||
map.attachTimeDimension(timedimension);
|
|
||||||
timedimension.update(mergedConfirmedGeoJson);
|
|
||||||
|
|
||||||
db.getItem(Database.KEY_DEATHS_GEOJSON).then(function(mergedDeathsGeoJson) {
|
|
||||||
var circleMarker = new CircleMarker(worldFr, mergedConfirmedGeoJson.features);
|
|
||||||
timedimension = new TimeDimension(Array<ITemporal>(circleMarker));
|
|
||||||
map.attachTimeDimension(timedimension);
|
|
||||||
timedimension.update(mergedDeathsGeoJson);
|
|
||||||
|
|
||||||
var view = new View(mergedConfirmedGeoJson, mergedDeathsGeoJson);
|
|
||||||
view.init();
|
|
||||||
timedimension = new TimeDimension(Array<ITemporal>(view));
|
|
||||||
map.attachTimeDimension(timedimension);
|
|
||||||
timedimension.update();
|
|
||||||
|
|
||||||
// Remove spinner when the page is fully loaded
|
|
||||||
document.getElementById('spinner').outerHTML = '';
|
|
||||||
|
|
||||||
console.log(`Performance: ${performance.now() - t0} milliseconds.`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,85 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
export class Database {
|
|
||||||
public static readonly DB_NAME: string = 'CoronamapStorage';
|
|
||||||
public static readonly KEY_EXPIRY_TIME: string = 'Expiry Time';
|
|
||||||
public static readonly KEY_CONFIRMED_GEOJSON: string = 'Confirmed Json';
|
|
||||||
public static readonly KEY_DEATHS_GEOJSON: string = 'Deaths Json';
|
|
||||||
// Stored data expires next day 3am UTC when the automated update is available by public data
|
|
||||||
public static readonly DEFAULT_EXPIRY_TIME: number = new Date().setUTCHours(24 + 3, 0, 0, 0);
|
|
||||||
// Use temporary storage if false
|
|
||||||
private static hasLocalStorage: boolean = true;
|
|
||||||
// Temporary storage expires after page refresh
|
|
||||||
private static temporaryStorage: Object = {};
|
|
||||||
|
|
||||||
public constructor() {
|
|
||||||
localforage.config({
|
|
||||||
driver: localforage.INDEXEDDB,
|
|
||||||
name: Database.DB_NAME
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public setItem(key: string, value: any): void {
|
|
||||||
if (Database.hasLocalStorage) {
|
|
||||||
localforage.setItem(key, value);
|
|
||||||
} else {
|
|
||||||
Database.temporaryStorage[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getItem(key: string): Promise<any> {
|
|
||||||
if (Database.hasLocalStorage) {
|
|
||||||
return localforage.getItem(key).then((value) => {
|
|
||||||
if (value) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}).catch((err) => {
|
|
||||||
Database.hasLocalStorage = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return Database.temporaryStorage[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public clear(): void {
|
|
||||||
if (Database.hasLocalStorage) {
|
|
||||||
localforage.clear();
|
|
||||||
} else {
|
|
||||||
Database.temporaryStorage = new Object();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public supports(): boolean {
|
|
||||||
return localforage.supports(localforage.INDEXEDDB);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async isExpired(): Promise<any> {
|
|
||||||
var extime;
|
|
||||||
if (Database.hasLocalStorage) {
|
|
||||||
extime = await this.getItem(Database.KEY_EXPIRY_TIME);
|
|
||||||
}
|
|
||||||
if (extime) {
|
|
||||||
return new Date().getTime() > extime;
|
|
||||||
}
|
|
||||||
// Expire if database does not contain expiry time
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setExpiryTime(extime?: any): void {
|
|
||||||
if (Database.hasLocalStorage) {
|
|
||||||
this.isExpired().then(function(value) {
|
|
||||||
if (value) {
|
|
||||||
localforage.setItem(Database.KEY_EXPIRY_TIME, !extime ? Database.DEFAULT_EXPIRY_TIME : extime);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getStorage(): object {
|
|
||||||
if (Database.hasLocalStorage) {
|
|
||||||
return localforage._dbInfo;
|
|
||||||
} else {
|
|
||||||
return Database.temporaryStorage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
export interface ITemporal {
|
|
||||||
|
|
||||||
// Return a layer, or empty layer L.layerGroup().addTo(Map.getInstance().getMap());
|
|
||||||
update(geoJson: object): any;
|
|
||||||
}
|
|
106
src/Map.ts
|
@ -1,106 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { TimeDimension } from "./TimeDimension";
|
|
||||||
|
|
||||||
export class Map {
|
|
||||||
public static readonly BUTTON_CONTROL_POSITION: string = 'bottomright';
|
|
||||||
public static readonly TIMEDIMENSION_POSITION: string = 'topright';
|
|
||||||
public static readonly SCALE_CONTROL_POSITION: string = 'bottomright';
|
|
||||||
public static readonly LINK_VIEW_SOURCE: string = 'https://github.com/7ae/coronamap';
|
|
||||||
private static instance: Map = null;
|
|
||||||
private map: any;
|
|
||||||
private timeDimension: any;
|
|
||||||
|
|
||||||
private constructor() { }
|
|
||||||
|
|
||||||
public static getInstance(): Map {
|
|
||||||
if (this.instance === null) {
|
|
||||||
this.instance = new Map();
|
|
||||||
}
|
|
||||||
return this.instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(): void {
|
|
||||||
this.map = L.map('map', {
|
|
||||||
zoomControl: true,
|
|
||||||
zoomDelta: 0.5,
|
|
||||||
zoomSnap: 0,
|
|
||||||
minZoom: 1.5,
|
|
||||||
maxZoom: 6,
|
|
||||||
worldCopyJump: true,
|
|
||||||
timeDimension: true,
|
|
||||||
timeDimensionOptions: {
|
|
||||||
timeInterval: '2020-01-22/' + new Date(Date.now() - 864e5).toJSON().slice(0, 10),
|
|
||||||
period: 'P1D',
|
|
||||||
buffer: 1,
|
|
||||||
},
|
|
||||||
}).setView([30, 0], 2.0);
|
|
||||||
|
|
||||||
// Add scale to map
|
|
||||||
L.control.scale({position: Map.SCALE_CONTROL_POSITION, metric: false}).addTo(this.map);
|
|
||||||
|
|
||||||
this.createTimeDimensionControl();
|
|
||||||
|
|
||||||
// Load map tiles
|
|
||||||
var osmLayer = L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png', {
|
|
||||||
attribution: 'Disease data © <a href="https://systems.jhu.edu/">Johns Hopkins CSSE</a> Map © <a href="https://osm.org/copyright">OpenStreetMap</a> contributors, <a href="https://leafletjs.com">Leaflet</a>',
|
|
||||||
tileSize: 512,
|
|
||||||
zoomOffset: -1
|
|
||||||
}).addTo(this.map);
|
|
||||||
this.map.attributionControl.setPrefix('<a href="' + Map.LINK_VIEW_SOURCE + '">View Source</a>');
|
|
||||||
this.createFullscreenControl(Map.BUTTON_CONTROL_POSITION);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach fullscreen control to zoom buttons
|
|
||||||
private createFullscreenControl(controlPosition: string): void {
|
|
||||||
// Move zoom buttons
|
|
||||||
this.map.zoomControl.setPosition(controlPosition);
|
|
||||||
// Attach fullscreen control to zoom buttons
|
|
||||||
L.control.fullscreen({
|
|
||||||
position: controlPosition,
|
|
||||||
forceSeparateButton: false
|
|
||||||
}).addTo(this.map);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach timedimension control to the map
|
|
||||||
private createTimeDimensionControl(): void {
|
|
||||||
L.Control.TimeDimensionCustom = L.Control.TimeDimension.extend({
|
|
||||||
//@override
|
|
||||||
_getDisplayDateFormat: function(date: any) {
|
|
||||||
return moment(date).add(1, 'days').format('dddd, LL');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
var timeDimensionControl = new L.Control.TimeDimensionCustom({
|
|
||||||
position: Map.TIMEDIMENSION_POSITION,
|
|
||||||
minSpeed: 0.25,
|
|
||||||
maxSpeed: 2,
|
|
||||||
speedStep: 0.25,
|
|
||||||
timeSliderDragUpdate: true,
|
|
||||||
autoPlay: false,
|
|
||||||
loopButton: true,
|
|
||||||
playReverseButton: true,
|
|
||||||
timeZones: ['Local'],
|
|
||||||
playerOptions: {
|
|
||||||
loop: true,
|
|
||||||
startOver: true,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.map.addControl(timeDimensionControl);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMap(): any {
|
|
||||||
return this.map;
|
|
||||||
}
|
|
||||||
|
|
||||||
public attachTimeDimension(td: TimeDimension): void {
|
|
||||||
this.timeDimension = td;
|
|
||||||
}
|
|
||||||
|
|
||||||
public dettachTimeDimension(): void {
|
|
||||||
this.timeDimension = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTimeDimension(): TimeDimension {
|
|
||||||
return this.timeDimension;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { AbstractReader } from "./AbstractReader";
|
|
||||||
import * as usStatesGeoJSONFile from '../dist/src/us-states.json';
|
|
||||||
|
|
||||||
export class StatesFileReader extends AbstractReader {
|
|
||||||
public static readonly CONFIRMED_FILE_SCOPE: string = 'time_series_covid19_confirmed_US.csv';
|
|
||||||
public static readonly DEATHS_FILE_SCOPE: string = 'time_series_covid19_deaths_US.csv';
|
|
||||||
public static readonly CENTER_OF_LAT_KEY: string = "Lat";
|
|
||||||
public static readonly CENTER_OF_LONG_KEY: string = "Long";
|
|
||||||
|
|
||||||
public constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(): void {
|
|
||||||
var centerOfLatLong = this.readCsv("https://raw.githubusercontent.com/jayinsf/coronamap/master/dist/src/states.csv");
|
|
||||||
|
|
||||||
var confirmed: any = this.readCsv(AbstractReader.BASE_URL + StatesFileReader.CONFIRMED_FILE_SCOPE);
|
|
||||||
var deaths: any = this.readCsv(AbstractReader.BASE_URL + StatesFileReader.DEATHS_FILE_SCOPE);
|
|
||||||
|
|
||||||
confirmed = this.replaceColumnKeys(confirmed);
|
|
||||||
deaths = this.replaceColumnKeys(deaths);
|
|
||||||
|
|
||||||
confirmed = this.csvToObject(confirmed);
|
|
||||||
deaths = this.csvToObject(deaths);
|
|
||||||
|
|
||||||
confirmed = this.replaceColumnValues(confirmed);
|
|
||||||
deaths = this.replaceColumnValues(deaths);
|
|
||||||
|
|
||||||
super.setConfirmedGeoJson(this.setPropertyValues(confirmed, this.loadGeoJsonFile()));
|
|
||||||
super.setDeathsGeoJson(this.setPropertyValues(deaths, this.loadGeoJsonFile()));
|
|
||||||
|
|
||||||
this.setConfirmedGeoJson(this.addCenterOfCordinates(this.csvToObject(centerOfLatLong), this.getConfirmedGeoJson()));
|
|
||||||
this.setDeathsGeoJson(this.addCenterOfCordinates(this.csvToObject(centerOfLatLong), this.getDeathsGeoJson()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load geojson file and return copy of geojson object
|
|
||||||
public loadGeoJsonFile(): object {
|
|
||||||
return JSON.parse(JSON.stringify(usStatesGeoJSONFile.default));
|
|
||||||
}
|
|
||||||
|
|
||||||
public replaceColumnKeys(csv: any): string {
|
|
||||||
const dict = {
|
|
||||||
"Province_State": "Province/State",
|
|
||||||
"Country_Region": "Country/Region",
|
|
||||||
"Long_": "Long",
|
|
||||||
}
|
|
||||||
return super.replaceColumnKeys(csv, dict);
|
|
||||||
}
|
|
||||||
|
|
||||||
public replaceColumnValues(csv: any): object {
|
|
||||||
const dict = {}
|
|
||||||
return super.replaceColumnValues(csv, dict, 'Province/State');
|
|
||||||
}
|
|
||||||
|
|
||||||
public setPropertyValues(csv: any, geoJson: object): object {
|
|
||||||
return super.setPropertyValues(csv, geoJson, 'Province/State', 'name');
|
|
||||||
}
|
|
||||||
|
|
||||||
public addCenterOfCordinates(csv: any, geoJson: any): object {
|
|
||||||
for (let feature of geoJson.features) {
|
|
||||||
for (let csvRow of csv) {
|
|
||||||
if (feature.id == csvRow.state) {
|
|
||||||
feature.properties[StatesFileReader.CENTER_OF_LAT_KEY] = csvRow.latitude;
|
|
||||||
feature.properties[StatesFileReader.CENTER_OF_LONG_KEY] = csvRow.longitude;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return geoJson;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { Map } from "./Map";
|
|
||||||
import { ITemporal } from "./ITemporal";
|
|
||||||
|
|
||||||
export class TimeDimension {
|
|
||||||
public static readonly DATE_FORMAT: string = 'M/D/YY';
|
|
||||||
public static readonly WAIT_ON_LOAD_TIMER: number = 2;
|
|
||||||
private map: Map;
|
|
||||||
private temporal: Array<ITemporal>;
|
|
||||||
private layerGroup: any;
|
|
||||||
private layers: {[key: number]: any};
|
|
||||||
private hasTimeChanged: boolean;
|
|
||||||
private currentTime: string;
|
|
||||||
|
|
||||||
public constructor(temporal: ITemporal[]) {
|
|
||||||
this.map = Map.getInstance();
|
|
||||||
this.temporal = temporal;
|
|
||||||
this.layerGroup = L.layerGroup().addTo(this.map.getMap());
|
|
||||||
this.layers = {};
|
|
||||||
this.hasTimeChanged = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(geoJson: object = null) {
|
|
||||||
setInterval(() => {
|
|
||||||
this.hasTimeChanged = this.currentTime !== this.getCurrentTime();
|
|
||||||
this.currentTime = this.getCurrentTime();
|
|
||||||
if (!this.hasTimeChanged) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.clearLayerGroup();
|
|
||||||
|
|
||||||
for (let singleTemporal of this.temporal) {
|
|
||||||
let layer = singleTemporal.update(geoJson).addTo(this.layerGroup);
|
|
||||||
let layerId = L.stamp(layer) < Number.MAX_VALUE ? L.stamp(layer) : 0;
|
|
||||||
this.layers[layerId] = layer;
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getLayers(): {[key: number]: any} {
|
|
||||||
return this.layers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearLayerGroup(): void {
|
|
||||||
if(this.layers) {
|
|
||||||
for (let i in this.layerGroup._layers) {
|
|
||||||
if (this.layerGroup.hasLayer(i)) {
|
|
||||||
this.layerGroup.removeLayer(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.layers = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCurrentTime(): string {
|
|
||||||
var currentTimeInMillisecond = this.map.getMap().timeDimension.getCurrentTime();
|
|
||||||
var currentTime = this.millisecondToDate(currentTimeInMillisecond);
|
|
||||||
return moment(currentTime).format(TimeDimension.DATE_FORMAT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private millisecondToDate(millisecond: number): string {
|
|
||||||
return new Date(millisecond).toJSON().slice(0, 10);
|
|
||||||
}
|
|
||||||
}
|
|
100
src/View.ts
|
@ -1,100 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { Map } from "./Map";
|
|
||||||
import { AbstractReader } from "./AbstractReader";
|
|
||||||
|
|
||||||
export class View {
|
|
||||||
private table: any;
|
|
||||||
private confirmedWorldFeatures: any;
|
|
||||||
private deathsWorldFeatures: any;
|
|
||||||
private confirmedUSFeatures: any;
|
|
||||||
private deathsUSFeatures: any;
|
|
||||||
|
|
||||||
constructor(confirmedGeoJson: any, deathsGeoJson: any) {
|
|
||||||
document.getElementById('table').style.visibility = 'visible';
|
|
||||||
|
|
||||||
this.confirmedWorldFeatures = confirmedGeoJson.features.slice(0, -50);
|
|
||||||
this.deathsWorldFeatures = deathsGeoJson.features.slice(0, -50);
|
|
||||||
|
|
||||||
this.confirmedUSFeatures = confirmedGeoJson.features.slice(1).slice(-50);
|
|
||||||
this.deathsUSFeatures = deathsGeoJson.features.slice(1).slice(-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(): void {
|
|
||||||
this.table = $('#datatables').DataTable({
|
|
||||||
autoWidth: false,
|
|
||||||
columnDefs: [{
|
|
||||||
targets: -1,
|
|
||||||
className: 'all'
|
|
||||||
}],
|
|
||||||
paging: false,
|
|
||||||
|
|
||||||
"columns": [
|
|
||||||
{ "data": "country" },
|
|
||||||
{ "data": "confirmed" },
|
|
||||||
{ "data": "death" },
|
|
||||||
],
|
|
||||||
|
|
||||||
//adds icons
|
|
||||||
dom: 'Bfrtip',
|
|
||||||
buttons: [
|
|
||||||
{ extend: 'csvHtml5', text: '<i class="far fa-file-alt"></i>', titleAttr: 'CSV' },
|
|
||||||
{ extend: 'excelHtml5', text: '<i class="far fa-file-excel"></i>', titleAttr: 'Excel' },
|
|
||||||
{ extend: 'pdfHtml5', text: '<i class="far fa-file-pdf"></i>', titleAttr: 'PDF' },
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(): object {
|
|
||||||
this.table.clear().draw();
|
|
||||||
//extracts all except states at the end of geojson
|
|
||||||
this.createRows(this.confirmedWorldFeatures, this.deathsWorldFeatures);
|
|
||||||
this.createRows(this.confirmedUSFeatures, this.deathsUSFeatures, true);
|
|
||||||
return L.layerGroup().addTo(Map.getInstance().getMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
public createRows(confirmedFeatures: object, deathsFeatures: object, isStates: boolean = false): any {
|
|
||||||
var currentTime = Map.getInstance().getTimeDimension().getCurrentTime();
|
|
||||||
var confirmed = AbstractReader.getProperty(confirmedFeatures, ['Country/Region', currentTime]);
|
|
||||||
var deaths = AbstractReader.getProperty(deathsFeatures, [currentTime]);
|
|
||||||
|
|
||||||
if (isStates) {
|
|
||||||
this.createOneRow([
|
|
||||||
{
|
|
||||||
'country': confirmed['Country/Region'][0],
|
|
||||||
'confirmed': this.sumArrayItems(confirmed[currentTime]).toLocaleString(),
|
|
||||||
'death': this.sumArrayItems(deaths[currentTime]).toLocaleString(),
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dataset: object[] = [];
|
|
||||||
for (let i in confirmed['Country/Region']) {
|
|
||||||
// Fix DataTables warning: table id=datatables - Requested unknown parameter 'confirmed' for row 0, column 1. For more information about this error, please see http://datatables.net/tn/4
|
|
||||||
try {
|
|
||||||
dataset.push({
|
|
||||||
'country': confirmed['Country/Region'][i],
|
|
||||||
'confirmed': confirmed[currentTime][i].toLocaleString(),
|
|
||||||
'death': deaths[currentTime][i].toLocaleString()
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// Throw exception if today's disease data is not found on public
|
|
||||||
console.log(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.createOneRow(dataset);
|
|
||||||
}
|
|
||||||
|
|
||||||
private createOneRow(dataset: object[]): void {
|
|
||||||
this.table.rows.add(dataset).draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
private sumArrayItems(arr: number[]): number {
|
|
||||||
var sum = 0;
|
|
||||||
for (let i in arr) {
|
|
||||||
sum += arr[i];
|
|
||||||
}
|
|
||||||
return sum;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
"use strict";
|
|
||||||
|
|
||||||
import { AbstractReader } from "./AbstractReader";
|
|
||||||
import * as worldCountriesGeoJSONFile from '../dist/src/world-countries.json';
|
|
||||||
|
|
||||||
export class WorldFileReader extends AbstractReader {
|
|
||||||
public static readonly CONFIRMED_FILE_SCOPE = 'time_series_covid19_confirmed_global.csv';
|
|
||||||
public static readonly DEATHS_FILE_SCOPE = 'time_series_covid19_deaths_global.csv';
|
|
||||||
public static readonly RECOVERED_FILE_SCOPE = 'time_series_covid19_recovered_global.csv';
|
|
||||||
|
|
||||||
public constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public init(): void {
|
|
||||||
var confirmed: any = this.readCsv(AbstractReader.BASE_URL + WorldFileReader.CONFIRMED_FILE_SCOPE);
|
|
||||||
var deaths: any = this.readCsv(AbstractReader.BASE_URL + WorldFileReader.DEATHS_FILE_SCOPE);
|
|
||||||
var recovered: any = this.readCsv(AbstractReader.BASE_URL + WorldFileReader.RECOVERED_FILE_SCOPE);
|
|
||||||
|
|
||||||
confirmed = this.replaceText(confirmed, 'Greenland,Denmark', 'Greenland,Greenland');
|
|
||||||
deaths = this.replaceText(deaths, 'Greenland,Denmark', 'Greenland,Greenland');
|
|
||||||
recovered = this.replaceText(recovered, 'Greenland,Denmark', 'Greenland,Greenland');
|
|
||||||
|
|
||||||
confirmed = this.csvToObject(confirmed);
|
|
||||||
deaths = this.csvToObject(deaths);
|
|
||||||
recovered = this.csvToObject(recovered);
|
|
||||||
|
|
||||||
confirmed = this.replaceColumnValues(confirmed);
|
|
||||||
deaths = this.replaceColumnValues(deaths);
|
|
||||||
recovered = this.replaceColumnValues(recovered);
|
|
||||||
|
|
||||||
confirmed = this.subtractRecoveredFromConfirmed(confirmed, recovered);
|
|
||||||
|
|
||||||
super.setConfirmedGeoJson(this.setPropertyValues(confirmed, this.loadGeoJsonFile()));
|
|
||||||
super.setDeathsGeoJson(this.setPropertyValues(deaths, this.loadGeoJsonFile()));
|
|
||||||
super.setRecoveredGeoJson(this.setPropertyValues(recovered, this.loadGeoJsonFile()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public loadGeoJsonFile(): object {
|
|
||||||
return JSON.parse(JSON.stringify(worldCountriesGeoJSONFile.default));
|
|
||||||
}
|
|
||||||
|
|
||||||
public replaceColumnValues(csv: any): object {
|
|
||||||
const dict = {
|
|
||||||
"Burma": "Myanmar",
|
|
||||||
"Czechia": "Czech Republic",
|
|
||||||
"Korea, South": "South Korea",
|
|
||||||
"Congo (Kinshasa)": "Democratic Republic of the Congo",
|
|
||||||
"Congo (Brazzaville)": "Republic of the Congo",
|
|
||||||
"Taiwan*": 'Taiwan',
|
|
||||||
"occupied Palestinian territory": "Palestine",
|
|
||||||
"Bahamas, The": "Bahamas",
|
|
||||||
"Gambia, The": "Gambia",
|
|
||||||
}
|
|
||||||
return super.replaceColumnValues(csv, dict, 'Country/Region');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append csv data to geojson
|
|
||||||
public setPropertyValues(csv: any, geoJson: object): object {
|
|
||||||
return super.setPropertyValues(csv, geoJson, 'Country/Region', 'name');
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 337 B After Width: | Height: | Size: 337 B |
Before Width: | Height: | Size: 426 B After Width: | Height: | Size: 426 B |
Before Width: | Height: | Size: 334 B After Width: | Height: | Size: 334 B |
17
src/vendor.d.ts
vendored
|
@ -1,17 +0,0 @@
|
||||||
// JQuery
|
|
||||||
declare var $: any;
|
|
||||||
|
|
||||||
// Leaflet.js
|
|
||||||
declare var L: any;
|
|
||||||
|
|
||||||
// Files with .json extension
|
|
||||||
declare module "*.json" {
|
|
||||||
const value: any;
|
|
||||||
export default value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// momentjs
|
|
||||||
declare var moment: any;
|
|
||||||
|
|
||||||
// localforage
|
|
||||||
declare var localforage: any;
|
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "./dist/",
|
|
||||||
"sourceMap": true,
|
|
||||||
"noImplicitAny": false,
|
|
||||||
"module": "es6",
|
|
||||||
"target": "es5",
|
|
||||||
"allowJs": true,
|
|
||||||
"watch": true,
|
|
||||||
"moduleResolution": "node"
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"test.ts",
|
|
||||||
"node_modules",
|
|
||||||
]
|
|
||||||
}
|
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 737 B After Width: | Height: | Size: 737 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 730 KiB |
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
Before Width: | Height: | Size: 896 KiB After Width: | Height: | Size: 896 KiB |
Before Width: | Height: | Size: 215 B After Width: | Height: | Size: 215 B |
Before Width: | Height: | Size: 139 B After Width: | Height: | Size: 139 B |
|
@ -1,26 +0,0 @@
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entry: './src/Coronamap.ts',
|
|
||||||
devtool: 'inline-source-map',
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
test: /\.tsx?$/,
|
|
||||||
use: 'ts-loader',
|
|
||||||
exclude: /node_modules/,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
node: {
|
|
||||||
fs: 'empty'
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
extensions: ['.js', '.ts', '.tsx'],
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
filename: 'bundle.js',
|
|
||||||
path: path.resolve(__dirname, 'dist/js'),
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|