Zeigt die aktuellen Coronazahlen weltweit an
<span style="color:#fd0808">Script defekt<br>API nicht mehr erreichbar</span>
Code
<style>
@charset "UTF-8";
@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css");
@import url("https://fonts.googleapis.com/css2?family=Fira+Sans:wght@200;400;500&display=swap");
:root {
--current-hue: 45;
--confirmed-hue: 45;
--recovered-hue: 164;
--deaths-hue: 353;
--compl-hue: calc(var(--current-hue) - 15);
--map-rotate-x: 0deg;
--map-rotate-y: 0deg;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
#app {
font-family: "Fira Sans", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
width: 100vw;
height: 100vh;
}
body {
background-color: #f3f4f6;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
margin: 0;
}
#notification {
position: fixed;
top: 1vh;
right: 1vw;
border-left: 5px solid;
min-width: 200px;
background-color: #e74c3c;
min-height: 50px;
z-index: 50;
display: flex;
align-items: center;
padding: 0.8em 1em;
}
#notification.error {
border-color: #bf2718;
color: #200704;
}
#notification .icon {
padding-right: 0.5em;
font-size: 1.3em;
}
#notification .text {
font-size: 0.9em;
}
.main {
height: 100%;
display: flex;
position: relative;
background: url("https://www.toptal.com/designers/subtlepatterns/patterns/dot-grid.png");
overflow: hidden;
}
.main[data-show-map=true] .content-wrapper {
transform: scale(0.9) translateX(10%);
}
.main .reset-map {
position: absolute;
bottom: 1em;
right: 1em;
font-size: 1.4em;
padding: 0.3em 0.4em;
z-index: 10;
transition: all 0.4s cubic-bezier(0.5, 0, 1, 0.81);
opacity: 0;
cursor: pointer;
background-color: rgba(255, 255, 255, 0.5);
}
.main .reset-map:hover {
background-color: rgba(255, 255, 255, 0.8);
}
.main .reset-map.show {
opacity: 1;
}
.top-nav .current-location {
padding-top: 5em;
margin-bottom: 5em;
}
.top-nav .current-location .flag-wrapper .flag {
border-radius: 3px;
width: 40%;
overflow: hidden;
}
.top-nav .current-location .flag-wrapper.world {
display: flex;
justify-content: center;
}
.top-nav .current-location .flag-wrapper.world .flag {
background-color: white;
padding: 0.3em;
font-size: 1.8em;
width: 90px;
height: 60px;
display: flex;
justify-content: center;
align-items: center;
}
.top-nav .current-location .location-title {
color: white;
margin-top: 0.5em;
}
.map-countries-container {
height: 70%;
overflow: hidden;
transition: all 0.4s cubic-bezier(0.5, 0, 1, 0.81);
position: relative;
max-height: 70%;
}
.sidebar {
justify-content: center;
display: flex;
flex-direction: column;
background-image: linear-gradient(60deg, #21425f, #2e5c85);
width: 250px;
padding: 1em;
z-index: 5;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.sidebar::before, .sidebar::after {
background-image: radial-gradient(#4284bd, #7ba9d1);
content: "";
position: absolute;
width: 40em;
height: 40em;
z-index: -1;
top: -5%;
left: 50%;
border-radius: 50%;
transform: translateX(-50%);
filter: blur(50px);
opacity: 0.3;
}
.sidebar::after {
top: 80%;
transform: translateX(-20%) rotate(180deg);
}
.sidebar .case-type {
color: white;
font-size: 1rem;
padding: 1em;
margin-bottom: 0.5em;
display: flex;
cursor: pointer;
user-select: none;
transition: all 0.2s ease-in-out;
border-radius: 6px;
height: 70px;
}
.sidebar .case-type.confirmed .case-type-icon {
color: hsl(var(--confirmed-hue), 100%, 50%);
background-color: hsl(var(--confirmed-hue), 100%, 95%);
}
.sidebar .case-type.deaths .case-type-icon {
color: hsl(var(--deaths-hue), 100%, 50%);
background-color: hsl(var(--deaths-hue), 100%, 95%);
}
.sidebar .case-type.recovered .case-type-icon {
color: hsl(var(--recovered-hue), 60%, 34%);
background-color: hsl(var(--recovered-hue), 100%, 95%);
}
.sidebar .case-type.active {
background-color: rgba(255, 225, 255, 0.2);
}
.sidebar .case-type.active .case-type-inner .case-title {
font-size: 1em;
color: white;
}
.sidebar .case-type.active .case-type-inner .case-count {
font-size: 0.7em;
}
.sidebar .case-type:hover:not(.active) {
background-color: rgba(255, 225, 255, 0.1);
}
.sidebar .case-type:hover:not(.active) i {
font-size: 1.1em;
}
.sidebar .case-type:hover:not(.active) .case-title {
font-size: 1em;
color: white;
}
.sidebar .case-type:hover:not(.active) .case-count {
font-size: 0.7em;
}
.sidebar .case-type .case-type-icon {
padding: 1em;
width: 25%;
display: flex;
justify-content: center;
align-items: center;
background-color: white;
border-radius: 5px;
}
.sidebar .case-type .case-type-icon i {
position: absolute;
transition: all 0.2s ease-in-out;
}
.sidebar .case-type .case-type-inner {
flex-grow: 1;
text-align: left;
margin-left: 1em;
}
.sidebar .case-type .case-type-inner .case-title {
font-size: 0.7em;
text-transform: uppercase;
font-weight: normal;
color: #b0b0b0;
transition: all 0.2s ease-in-out;
}
.sidebar .case-type .case-type-inner .case-count {
font-size: 1.1em;
font-feature-settings: "tnum";
font-variant-numeric: tabular-nums;
transition: all 0.2s ease-in-out;
}
.sidebar .bottom-nav {
display: flex;
justify-content: space-around;
margin-top: auto;
}
.sidebar .bottom-nav > div {
cursor: pointer;
padding: 1.4em;
color: white;
position: relative;
font-size: 1.5em;
border-radius: 5px;
transition: all 0.2s ease-in-out;
}
.sidebar .bottom-nav > div:hover:not(.active) {
background-color: rgba(255, 225, 255, 0.1);
}
.sidebar .bottom-nav > div.active {
background-color: rgba(255, 225, 255, 0.2);
}
.sidebar .bottom-nav > div i {
position: absolute;
color: white;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.sidebar .info-nav {
display: flex;
justify-content: space-around;
color: white;
margin-top: 1em;
padding-top: 1em;
border-top: 1px dashed rgba(255, 225, 255, 0.3);
}
.sidebar .info-nav > div,
.sidebar .info-nav > a {
padding: 0.5em 1em;
border-radius: 4px;
transition: all 0.2s ease-in-out;
cursor: pointer;
color: white;
font-size: 1.2em;
display: flex;
justify-content: center;
align-items: center;
text-decoration: none;
}
.sidebar .info-nav > div.twitter,
.sidebar .info-nav > a.twitter {
color: #1c9cea;
background-color: rgba(255, 255, 255, 0.8);
}
.sidebar .info-nav > div.twitter:hover,
.sidebar .info-nav > a.twitter:hover {
background-color: white;
}
.sidebar .info-nav > div:hover,
.sidebar .info-nav > a:hover {
background-color: rgba(255, 225, 255, 0.1);
}
.sidebar .info-nav > div.active,
.sidebar .info-nav > a.active {
background-color: rgba(255, 225, 255, 0.2);
}
.sidebar .copyright-wrapper {
background-color: rgba(0, 0, 0, 0.2);
margin: 1em -1em -1em -1em;
padding: 1em;
text-align: left;
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0, transparent 2%);
overflow: auto;
}
.sidebar .copyright-wrapper a {
display: block;
font-size: 0.8em;
margin: 0.2em 0;
padding: 0.4em 0.8em;
color: white;
text-decoration: none;
transition: all 0.2s;
border-radius: 2px;
}
.sidebar .copyright-wrapper a::after {
font-family: "Font Awesome 5 Free";
font-weight: 900;
-webkit-font-smoothing: antialiased;
display: inline-block;
font-style: normal;
font-variant: normal;
text-rendering: auto;
line-height: 1;
font-size: 0.8em;
margin-left: 1em;
}
.sidebar .copyright-wrapper a:hover {
background-color: rgba(255, 225, 255, 0.1);
}
.sidebar .copyright-wrapper a:hover::after {
content: "";
}
.content-wrapper {
height: 100%;
flex-grow: 1;
transition: all 0.4s cubic-bezier(0.5, 0, 1, 0.81);
position: relative;
padding: 2em;
}
.content-wrapper .series-container {
height: 40%;
position: absolute;
bottom: 0;
z-index: 1;
width: 100%;
padding: inherit;
left: 0;
transition: all 0.4s cubic-bezier(0.5, 0, 1, 0.81);
}
.fadeslide-enter-active {
animation: bounce-in 0.5s;
}
.fadeslide-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: translateX(10%);
opacity: 0;
}
100% {
transform: translateX(0%);
opacity: 1;
}
}
#countries-list-wrapper {
height: 100%;
background-color: #e1ebf4;
width: 350px;
transform: translateX(-50%);
z-index: 3;
position: absolute;
transition: all 0.4s cubic-bezier(0.5, 0, 1, 0.81);
left: 0;
display: flex;
flex-direction: column;
}
#countries-list-wrapper.active {
transform: translateX(0);
left: 250px;
}
#countries-list-wrapper .countries-search-wrapper {
padding: 1em 2em;
}
#countries-list-wrapper .countries-search-wrapper input {
border: 0px solid;
background-color: rgba(255, 255, 255, 0.6);
width: 100%;
border-radius: 5px;
transition: all 0.2s ease-in-out;
font-size: 0.8em;
padding: 1em 1em;
outline: 0;
}
#countries-list-wrapper .countries-search-wrapper input:focus {
background-color: white;
}
#countries-list-wrapper .countries-list-container {
height: 100%;
display: flex;
flex-direction: column;
overflow: auto;
padding: 1em;
}
#countries-list-wrapper .countries-list-container::-webkit-scrollbar {
width: 6px;
height: 6px;
}
#countries-list-wrapper .countries-list-container::-webkit-scrollbar-thumb {
background: #396791;
}
#countries-list-wrapper .countries-list-container::-webkit-scrollbar-track {
background: rgba(57, 103, 145, 0.2);
}
#countries-list-wrapper .countries-list-container {
scrollbar-face-color: #396791;
scrollbar-track-color: rgba(57, 103, 145, 0.2);
}
#countries-list-wrapper .countries-list-container .countries-list {
height: 100%;
}
#countries-list-wrapper .countries-list-container .countries-list.is-loading .country-wrapper {
cursor: default;
}
#countries-list-wrapper .countries-list-container .countries-list.is-loading .country-wrapper:hover {
background-color: initial;
}
#countries-list-wrapper .countries-list-container .countries-list.is-loading .country-wrapper .country-content,
#countries-list-wrapper .countries-list-container .countries-list.is-loading .country-wrapper .flag-wrapper {
opacity: 0.6;
}
#countries-list-wrapper .countries-list-container .countries-list.is-loading .country-wrapper.loading .country-content,
#countries-list-wrapper .countries-list-container .countries-list.is-loading .country-wrapper.loading .flag-wrapper {
opacity: 0.1;
filter: blur(1px);
}
#countries-list-wrapper .countries-list-container .countries-list .country-wrapper {
display: flex;
font-size: 0.8rem;
padding: 1em;
width: 100%;
cursor: pointer;
transition: all 0.3s;
position: relative;
}
#countries-list-wrapper .countries-list-container .countries-list .country-wrapper:hover {
background-color: hsla(var(--current-hue), 30%, 80%, 0.5);
}
#countries-list-wrapper .countries-list-container .countries-list .country-wrapper.active {
background-color: hsla(var(--current-hue), 30%, 70%, 0.8);
}
#countries-list-wrapper .countries-list-container .countries-list .country-wrapper .flag-wrapper {
font-size: 2em;
transition: all 0.2s;
}
#countries-list-wrapper .countries-list-container .countries-list .country-wrapper .country-content {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
text-align: left;
flex-grow: 1;
margin-left: 1em;
transition: all 0.2s;
}
#countries-list-wrapper .countries-list-container .countries-list .country-wrapper .country-content .country-name {
grid-column: 1/2;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
#countries-list-wrapper .countries-list-container .countries-list .country-wrapper .country-content .country-count-diff {
grid-column: 1/2;
grid-row: 2/3;
font-size: 0.8em;
}
#countries-list-wrapper .countries-list-container .countries-list .country-wrapper .country-content .country-count-diff.extra {
color: red;
}
#countries-list-wrapper .countries-list-container .countries-list .country-wrapper .country-content .country-count-diff.less {
color: green;
}
#countries-list-wrapper .countries-list-container .countries-list .country-wrapper .country-content .country-count {
grid-column: 2/3;
grid-row: 1/3;
text-align: right;
font-size: 1.2em;
font-weight: bold;
}
@keyframes loader-bar {
0% {
background-position: 0 0;
}
100% {
background-position: 15px 15px;
}
}
.flag {
position: relative;
display: inline-flex;
width: 2.1em;
}
.flag.fa-globe {
height: 43px;
background-color: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
overflow: hidden;
width: 58px;
font-size: 0.8em;
}
.flag img {
max-width: 100%;
}
.flag::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
display: block;
mix-blend-mode: overlay;
box-sizing: border-box;
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.3) 2%, rgba(255, 255, 255, 0.7) 100%);
}
#chartjs-tooltip {
background: hsl(var(--compl-hue), 73%, 90%);
filter: drop-shadow(0px 0px 1px hsl(var(--compl-hue), 100%, 30%));
transform: translate(-50%, -25px);
z-index: 50;
padding: 1em;
pointer-events: none;
border-radius: 0.2em;
position: absolute;
top: 0;
font-size: 0.9em;
text-align: left;
min-width: 150px;
opacity: 0;
}
#chartjs-tooltip .title {
white-space: nowrap;
font-weight: bold;
}
#chartjs-tooltip .count-wrapper {
white-space: nowrap;
font-size: 0.9em;
text-transform: capitalize;
}
#chartjs-tooltip .count-wrapper .count-total-type {
white-space: nowrap;
}
#chartjs-tooltip .count-wrapper .count-total-number {
padding: 0.2em;
}
#chartjs-tooltip::after {
position: absolute;
top: 100%;
left: 50%;
background-color: hsl(var(--compl-hue), 73%, 90%);
width: 1em;
height: 1em;
transform: translate(-50%, -70%) rotate(45deg);
content: "";
}
.series-data-wrapper {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.series-data-wrapper .series-data-options {
font-size: 0.8em;
width: 100%;
padding: 1em 2em;
text-align: left;
transition: all 0.4s cubic-bezier(0.5, 0, 1, 0.81);
display: flex;
}
.series-data-wrapper .series-data-options > div {
display: flex;
margin: 0 1em;
border-radius: 0.2em;
overflow: hidden;
}
.series-data-wrapper .series-data-options > div span {
padding: 0.3em 0.9em;
background-color: hsl(var(--compl-hue), 73%, 30%);
transition: all 0.3s;
display: inline-block;
color: hsl(var(--compl-hue), 73%, 75%);
}
.series-data-wrapper .series-data-options > div span:not(.active) {
cursor: pointer;
}
.series-data-wrapper .series-data-options > div span:not(.active):hover {
background-color: hsl(var(--compl-hue), 73%, 20%);
}
.series-data-wrapper .series-data-options > div span.active {
background-color: hsl(var(--compl-hue), 73%, 15%);
color: white;
}
.series-data-wrapper .series-data {
width: 100%;
flex-grow: 1;
position: relative;
height: 80%;
}
.loader-wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
z-index: 50;
max-width: 1000px;
}
.loader-wrapper .loader-bar {
width: 100%;
height: 3px;
position: relative;
box-shadow: 0 0 2em 0.5em rgba(103, 103, 103, 0.3);
}
.loader-wrapper .loader-bar.indeterminant .loader-bar-loaded {
width: 100%;
animation: loader-bar 1s linear infinite;
background-size: 15px 15px;
background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.5) 25%, transparent 25% 50%, rgba(255, 255, 255, 0.5) 50% 75%, transparent 75% 100%);
}
@keyframes loader-bar {
0% {
background-position: 0 0;
}
100% {
background-position: 15px 15px;
}
}
.loader-wrapper .loader-bar .loader-bar-loaded {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: hsl(var(--current-hue), 50%, 50%);
transition: all 0.3s;
}
.loader-wrapper .loader-text {
text-align: center;
font-size: 1em;
margin-top: 1em;
}
#location-tooltip {
display: block;
z-index: 50;
transition: opacity 0.1s linear;
pointer-events: none;
min-width: 200px;
left: 0;
top: 0;
position: fixed;
transform: translate(-50%, 20%);
font-size: 0.9em;
}
#location-tooltip .tooltip-inner {
background: hsl(var(--current-hue), 30%, 20%);
color: white;
border-radius: 3px;
padding: 1em;
text-align: left;
}
#location-tooltip .tooltip-inner .tooltip-title {
border-bottom: 1px solid white;
margin-bottom: 1em;
padding-bottom: 0.3em;
}
#location-tooltip .tooltip-inner .tooltip-content {
font-size: 0.8em;
margin-top: 1em;
}
#location-tooltip .tooltip-inner .tooltip-content > div {
display: flex;
}
#location-tooltip .tooltip-inner .tooltip-content > div span {
width: 50%;
white-space: nowrap;
padding: 0.2em 0.2em;
}
#location-tooltip .tooltip-inner .tooltip-content > div span:first-child {
text-align: right;
}
#location-tooltip .tooltip-inner .tooltip-content > div span:last-child {
text-transform: capitalize;
}
.map-container {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
max-width: 1000px;
transform: translate(-50%, -60%) perspective(1000px) rotateX(var(--map-rotate-x)) rotateY(var(--map-rotate-y));
max-height: 100%;
padding: 2em 2em;
box-sizing: initial;
background-color: white;
box-shadow: 0 0 1em 1em hsla(var(--current-hue), 100%, 10%, 0.1);
}
.map-container .svg-wrapper {
max-width: 100%;
max-height: 100%;
position: relative;
top: 50%;
left: 50%;
}
.map-container svg {
max-width: 100%;
max-height: 100%;
position: relative;
filter: drop-shadow(0px 0px 2em hsla(var(--current-hue), 100%, 10%, 0.5));
width: 1000px;
height: 400px;
transition: all 0.2s;
}
.map-container svg.loading {
opacity: 0.5;
}
.map-container g {
transition: all 0.2s ease-in-out;
cursor: pointer;
}
.map-container g.no-select {
cursor: default;
}
.map-container g.active path {
stroke-width: 0;
stroke: purple;
z-index: 5;
}
.map-container:not([data-status=single-location]):hover g {
fill-opacity: 0.5;
}
.map-container:not([data-status=single-location]):hover g:hover {
fill-opacity: 1;
}
.map-container[data-status=single-location] g {
fill-opacity: 0.1;
}
.map-container[data-status=single-location] g:hover {
fill-opacity: 0.3;
}
.map-container[data-status=single-location] g[data-status=selected] {
fill-opacity: 1;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.6.0/vuex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.6.0/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.css">
<div id="app">
<notify
:show="getNotifyDetails.show"
:type="getNotifyDetails.type"
:message="getNotifyDetails.message"
></notify>
<div class="main" :data-show-map="show_countries_list">
<div class="sidebar">
<div class="top-nav">
<div class="current-location">
<div class="flag-wrapper" v-if="getCurrentLocation !== 'WO'">
<flag :iso="getCurrentLocation.toLowerCase()"></flag>
</div>
<div class="flag-wrapper world" v-if="getCurrentLocation === 'WO'">
<i class="flag fas fa-globe"></i>
</div>
<div class="location-title">{{ getCountriesList[getCurrentLocation] }}</div>
</div>
</div>
<div class="cases-nav">
<div
class="case-type confirmed"
@click="changeCaseType('confirmed')"
:class="{ active: getDisplayedCasesType == 'confirmed' }"
>
<div class="case-type-icon">
<i class="fas fa-virus"></i>
</div>
<div class="case-type-inner">
<div class="case-title">Confiremd</div>
<div class="case-count">{{ confirmedCount }}</div>
</div>
</div>
<div
class="case-type recovered"
@click="changeCaseType('recovered')"
:class="{ active: getDisplayedCasesType == 'recovered' }"
>
<div class="case-type-icon">
<i class="fas fa-first-aid"></i>
</div>
<div class="case-type-inner">
<div class="case-title">Recovered</div>
<div class="case-count">{{ recoveredCount }}</div>
</div>
</div>
<div
class="case-type deaths"
@click="changeCaseType('deaths')"
:class="{ active: getDisplayedCasesType == 'deaths' }"
>
<div class="case-type-icon">
<i class="fas fa-biohazard"></i>
</div>
<div class="case-type-inner">
<div class="case-title">Deaths</div>
<div class="case-count">{{ deathsCount }}</div>
</div>
</div>
</div>
<div class="bottom-nav">
<div class="fill-screen" @click="is_full_screen = !is_full_screen">
<i class="fas fa-expand" v-if="is_full_screen == false"></i>
<i class="fas fa-compress" v-if="is_full_screen == true"></i>
</div>
<div
class="locations-list"
:class="{ active: show_countries_list == true }"
@click="show_countries_list = !show_countries_list"
>
<i class="fas fa-map-marked-alt"></i>
</div>
</div>
<div class="info-nav">
<div
class="copyright-button"
:class="{ active: show_copyright == true }"
@click="show_copyright = !show_copyright"
>
<i class="far fa-copyright"></i>
</div>
<a target="_blank" href="https://codepen.io/khr2003"> <i class="fab fa-codepen"></i></a>
<a
target="_top"
class="twitter"
href="https://twitter.com/intent/follow?screen_name=kalimahapps&tw_p=followbutton"
><i class="fab fa-twitter"></i
></a>
</div>
<div class="copyright-wrapper" v-if="show_copyright">
<a target="_blank" :href="link" v-for="( link, text) in copyright_data">{{text}}</a>
</div>
</div>
<CountriesList :class="{ active: show_countries_list == true }"></CountriesList>
<div class="content-wrapper">
<WorldMap></WorldMap>
<div class="series-container">
<series></series>
</div>
</div>
</div>
<location-tooltip></location-tooltip>
</div>
<!-- Countries list -->
<template id="countries-list-template">
<div id="countries-list-wrapper">
<div class="countries-search-wrapper">
<input
type="text"
class="search-countries"
placeholder="Search Countries ..."
@input="filterCountries"
/>
</div>
<div class="countries-list-container">
<div class="countries-list" :class="{'is-loading': getLocationLoading != false}">
<div
class="country-wrapper"
:class="{ active: key == getCurrentLocation, loading: getLocationLoading == key }"
v-for="(country, key) in filterCountriesList"
:key="`country-${key}`"
:data-loading="!is_loading[key] || is_loading[key] == 'undefined' ? 'no' : 'yes'"
@click="selectCountry(key)"
>
<loader-bar
v-if="getLocationLoading == key"
:loading-indeterminant="true"
></loader-bar>
<div class="flag-wrapper" v-if="key.toLowerCase() !== 'wo'">
<flag :iso="key.toLowerCase()"></flag>
</div>
<div class="flag-wrapper world" v-if="key.toLowerCase() === 'wo'">
<i class="flag fas fa-globe"></i>
</div>
<div class="country-content">
<div class="country-name" :title="country.name">{{ country.name }}</div>
<div class="country-count-diff" :class="country.diff.type">
<span class="type-operator">
<span class="extra" v-if="country.diff.type == 'extra'">+</span>
<span class="extra" v-if="country.diff.type == 'less'">-</span>
</span>
<span class="diff-count">{{ country.diff.value.toLocaleString('en-US') }}</span>
</div>
<div class="country-count">{{ country.value.toLocaleString('en-US') }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<!-- Notify Template -->
<template id="notify-template">
<transition name="fadeslide">
<div id="notification" :class="type" v-if="show_notify">
<div class="icon">
<i class="fas fa-exclamation-triangle" v-if="type == 'error'"></i>
<i class="fas fa-check-circle" v-if="type == 'success'"></i>
</div>
<div class="text">{{message}}</div>
</div>
</transition>
</template>
<!-- Flag Template -->
<template id="flag-template">
<span class="flag">
<img :src="flag" />
</span>
</template>
<!-- Loader Bar Template -->
<template id="loader-bar-template">
<div class="loader-wrapper">
<div class="loader-bar" :class="{ indeterminant: loadingIndeterminant == true }">
<div class="loader-bar-loaded" :style="{ width: `${loadedPercentage}%` }"></div>
</div>
<div class="loader-text">{{ loadingText }}</div>
</div>
</template>
<!-- Worldmap Template -->
<template id="worldmap-template">
<div class="map-countries-container">
<loader-bar
v-if="loading == true"
:loading-text="loading_text"
:loaded-percentage="loaded_percentage"
></loader-bar>
<loader-bar
v-if="getLocationLoading != false"
loading-text="Loading Location Data"
:loading-indeterminant="true"
></loader-bar>
<div
class="map-container"
:data-status="getCurrentLocation != 'WO' ? 'single-location' : 'world'"
ref="world_map_container"
:style="getMapStyle"
@mouseleave="stopRotateMap"
@mousemove="rotateMap"
>
<div
class="reset-map"
:class="{ show: getCurrentLocation != 'WO' }"
@click="resetLocation()"
>
<i class="fas fa-home"></i>
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
ref="world_map"
:view-box.camel="viewbox"
:class="{loading: getLocationLoading != false}"
>
<g
v-for="(data, key) in getWorlMapWithColors"
:id="key"
:key="key"
:fill="data.color"
:ref="`location-${key}`"
stroke="white"
:data-count="data.value"
@click="selectLocation(key)"
@mouseenter="handleMoustEnter($event, key, data.value)"
@mouseleave="handleMoustOut"
@mousemove="handleMouseMove"
:class="{ active: key == getCurrentLocation, 'no-select': typeof getDayCaseByType[key] == 'undefined' }"
:data-status="getCurrentLocation == key ? 'selected' : ''"
>
<path
:d="data.path"
:key="`path-${key}`"
stroke-width="0.3"
stroke-opacity="0.5"
></path>
</g>
</svg>
</div>
</div>
</template>
<!-- Location details tooltip -->
<template id="location-tooltip-template">
<div id="location-tooltip" role="tooltip" ref="tooltip" :style="getTooltipStyle">
<div class="tooltip-inner">
<div class="tooltip-title">{{ this.getTooltipData.title }}</div>
<div class="tooltip-content" v-if="'count' in this.getTooltipData">
<div class="cases-type">
<span>{{ this.getTooltipData.count }}</span>
<span>{{ this.getTooltipData.type }}</span>
</div>
<div class="cases-diff">
<span>{{ this.getTooltipData.diff }}</span>
<span>Daily {{ this.getTooltipData.difftext }}</span>
</div>
<div class="cases-percentage">
<span>{{ this.getTooltipData.percentage }}%</span>
<span>Daily change</span>
</div>
</div>
<div v-else>No data available</div>
</div>
<div class="arrow" data-popper-arrow></div>
</div>
</template>
<!-- Series Tempmate -->
<template id="series-template">
<div class="series-data-wrapper">
<loader-bar
v-if="series_loading == true"
:loading-indeterminant="true"
loading-text="Loading Series"
></loader-bar>
<div class="series-data-options" v-if="series_loading == false">
<div class="bar-line-options">
<span class="bar" @click="chart_type = 'bar'" :class="{ active: chart_type == 'bar' }"
>Bar</span
>
<span
class="line"
@click="chart_type = 'line'"
:class="{ active: chart_type == 'line' }"
>Line</span
>
</div>
<div class="daily-weekly">
<span
class="daily"
@click="series_type = 'daily'"
:class="{ active: series_type == 'daily' }"
>Daily</span
>
<span
class="weekly"
@click="series_type = 'weekly'"
:class="{ active: series_type == 'weekly' }"
>Weekly</span
>
</div>
<div class="chart-data-representation">
<span
class="change"
@click="series_representation = 'change'"
:class="{ active: series_representation == 'change' }"
>Change</span
>
<span
class="cumulative"
@click="series_representation = 'cumulative'"
:class="{ active: series_representation == 'cumulative' }"
>Cumulative</span
>
<span
class="log"
@click="series_representation = 'log'"
:class="{ active: series_representation == 'log' }"
>Log</span
>
</div>
</div>
<div class="series-data" v-show="series_loading == false">
<canvas ref="chart"></canvas>
</div>
<div id="chartjs-tooltip">
<div class="title"></div>
<div class="count-wrapper">
<div class="count-total-type"></div>
<div class="count-total-number"></div>
</div>
</div>
</div>
</template>
<script>
const countriesIso = {
AF: 'Afghanistan',
AX: 'Åland Islands',
AL: 'Albania',
DZ: 'Algeria',
AS: 'American Samoa',
AD: 'Andorra',
AO: 'Angola',
AI: 'Anguilla',
AQ: 'Antarctica',
AG: 'Antigua and Barbuda',
AR: 'Argentina',
AM: 'Armenia',
AW: 'Aruba',
AU: 'Australia',
AT: 'Austria',
AZ: 'Azerbaijan',
BS: 'Bahamas',
BH: 'Bahrain',
BD: 'Bangladesh',
BB: 'Barbados',
BY: 'Belarus',
BE: 'Belgium',
BZ: 'Belize',
BJ: 'Benin',
BM: 'Bermuda',
BT: 'Bhutan',
BO: 'Bolivia',
BA: 'Bosnia and Herzegovina',
BW: 'Botswana',
BL: 'Saint Barthélemy',
BV: 'Bouvet Island',
BR: 'Brazil',
IO: 'British Indian Ocean Territory',
BN: 'Brunei Darussalam',
BQ: 'BONAIRE, SINT EUSTATIUS AND SABA',
BG: 'Bulgaria',
BF: 'Burkina Faso',
BI: 'Burundi',
KH: 'Cambodia',
CM: 'Cameroon',
CA: 'Canada',
CV: 'Cape Verde',
CW: 'Curaçao',
MF: 'Saint Martin',
KY: 'Cayman Islands',
CF: 'Central African Republic',
TD: 'Chad',
CL: 'Chile',
CN: 'China',
CX: 'Christmas Island',
CC: 'Cocos (Keeling) Islands',
CO: 'Colombia',
KM: 'Comoros',
CG: 'Congo',
CD: 'The Democratic Republic of the Congo',
CK: 'Cook Islands',
CR: 'Costa Rica',
CI: "Cote D'Ivoire",
HR: 'Croatia',
CU: 'Cuba',
CY: 'Cyprus',
CZ: 'Czech Republic',
DK: 'Denmark',
DJ: 'Djibouti',
DM: 'Dominica',
DO: 'Dominican Republic',
EC: 'Ecuador',
EG: 'Egypt',
SV: 'El Salvador',
GQ: 'Equatorial Guinea',
ER: 'Eritrea',
EE: 'Estonia',
ET: 'Ethiopia',
FK: 'Falkland Islands (Malvinas)',
FO: 'Faroe Islands',
FJ: 'Fiji',
FI: 'Finland',
FR: 'France',
GF: 'French Guiana',
PF: 'French Polynesia',
TF: 'French Southern Territories',
GA: 'Gabon',
GM: 'Gambia',
GE: 'Georgia',
DE: 'Germany',
GH: 'Ghana',
GI: 'Gibraltar',
GR: 'Greece',
GL: 'Greenland',
GD: 'Grenada',
GP: 'Guadeloupe',
GU: 'Guam',
GT: 'Guatemala',
GG: 'Guernsey',
GN: 'Guinea',
GW: 'Guinea-Bissau',
GY: 'Guyana',
HT: 'Haiti',
HM: 'Heard Island and Mcdonald Islands',
VA: 'Holy See (Vatican City State)',
HN: 'Honduras',
HK: 'Hong Kong',
HU: 'Hungary',
IS: 'Iceland',
IN: 'India',
ID: 'Indonesia',
IR: 'Islamic Republic Of Iran',
IQ: 'Iraq',
IE: 'Ireland',
IM: 'Isle of Man',
IL: 'Israel',
IT: 'Italy',
JM: 'Jamaica',
JP: 'Japan',
JE: 'Jersey',
JO: 'Jordan',
KZ: 'Kazakhstan',
KE: 'Kenya',
KI: 'Kiribati',
KP: "Democratic People's Republic of Korea",
KR: 'Republic of Korea',
XK: 'Kosovo',
KW: 'Kuwait',
KG: 'Kyrgyzstan',
LA: "Lao People's Democratic Republic",
LV: 'Latvia',
LB: 'Lebanon',
LS: 'Lesotho',
LR: 'Liberia',
LY: 'Libyan Arab Jamahiriya',
LI: 'Liechtenstein',
LT: 'Lithuania',
LU: 'Luxembourg',
MO: 'Macao',
MK: 'The Former Yugoslav Republic of Macedonia',
MG: 'Madagascar',
MW: 'Malawi',
MY: 'Malaysia',
MV: 'Maldives',
ML: 'Mali',
MT: 'Malta',
MH: 'Marshall Islands',
MQ: 'Martinique',
MR: 'Mauritania',
MU: 'Mauritius',
YT: 'Mayotte',
MX: 'Mexico',
FM: 'Federated States of Micronesia',
MD: 'Republic of Moldova',
MC: 'Monaco',
MN: 'Mongolia',
ME: 'Montenegro',
MS: 'Montserrat',
MA: 'Morocco',
MZ: 'Mozambique',
MM: 'Myanmar',
NA: 'Namibia',
NR: 'Nauru',
NP: 'Nepal',
NL: 'Netherlands',
AN: 'Netherlands Antilles',
NC: 'New Caledonia',
NZ: 'New Zealand',
NI: 'Nicaragua',
NE: 'Niger',
NG: 'Nigeria',
NU: 'Niue',
NF: 'Norfolk Island',
MP: 'Northern Mariana Islands',
NO: 'Norway',
OM: 'Oman',
PK: 'Pakistan',
PW: 'Palau',
PS: 'Occupied Palestinian Territory',
PA: 'Panama',
PG: 'Papua New Guinea',
PY: 'Paraguay',
PE: 'Peru',
PH: 'Philippines',
PN: 'Pitcairn',
PL: 'Poland',
PT: 'Portugal',
PR: 'Puerto Rico',
QA: 'Qatar',
RE: 'Reunion',
RO: 'Romania',
RU: 'Russian Federation',
RW: 'Rwanda',
SH: 'Saint Helena',
KN: 'Saint Kitts and Nevis',
LC: 'Saint Lucia',
PM: 'Saint Pierre and Miquelon',
VC: 'Saint Vincent and the Grenadines',
WS: 'Samoa',
SM: 'San Marino',
ST: 'Sao Tome and Principe',
SA: 'Saudi Arabia',
SN: 'Senegal',
RS: 'Serbia',
SC: 'Seychelles',
SL: 'Sierra Leone',
SG: 'Singapore',
SK: 'Slovakia',
SI: 'Slovenia',
SS: 'South Sudan',
SB: 'Solomon Islands',
SO: 'Somalia',
ZA: 'South Africa',
GS: 'South Georgia and the South Sandwich Islands',
ES: 'Spain',
LK: 'Sri Lanka',
SD: 'Sudan',
SR: 'Suriname',
SJ: 'Svalbard and Jan Mayen',
SZ: 'Swaziland',
SE: 'Sweden',
CH: 'Switzerland',
SY: 'Syrian Arab Republic',
TW: 'Taiwan',
TJ: 'Tajikistan',
TZ: 'United Republic of Tanzania',
TH: 'Thailand',
TL: 'Timor-Leste',
TG: 'Togo',
TK: 'Tokelau',
TO: 'Tonga',
TT: 'Trinidad and Tobago',
TN: 'Tunisia',
TR: 'Turkey',
TM: 'Turkmenistan',
TC: 'Turks and Caicos Islands',
TV: 'Tuvalu',
UG: 'Uganda',
UA: 'Ukraine',
SX: 'Sint Maarten',
AE: 'United Arab Emirates',
GB: 'United Kingdom',
US: 'United States',
UM: 'United States Minor Outlying Islands',
UY: 'Uruguay',
UZ: 'Uzbekistan',
VU: 'Vanuatu',
VE: 'Venezuela',
VN: 'Viet Nam',
VG: 'British Virgin Islands',
VI: 'U.S. Virgin Islands',
WF: 'Wallis and Futuna',
EH: 'Western Sahara',
YE: 'Yemen',
ZM: 'Zambia',
ZW: 'Zimbabwe',
WO: 'World'
};
const store = new Vuex.Store({
state: {
day_cases: {
by_locations: {
confirmed: [],
recovered: [],
deaths: []
},
max: {
confirmed: 0,
recovered: 0,
deaths: 0
}
},
displayed_series: {
aggregate: {
confirmed: [],
recovered: [],
deaths: []
},
by_locations: {
confirmed: [],
recovered: [],
deaths: []
},
max: {
confirmed: 0,
recovered: 0,
deaths: 0
},
total: {
confirmed: 0,
recovered: 0,
deaths: 0
}
},
all_series: {},
cases_type: 'confirmed',
current_location: '',
worldmap: {},
current_hover_location: '',
notify: {
show: false,
message: '',
type: 'error'
},
location_loading: false
},
mutations: {
update_location_loading(state, status) {
state.location_loading = status;
},
update_day_cases(state, payload) {
const cases = payload.data;
// Get max of each data series
state.day_cases = cases;
},
update_cases_type(state, type) {
state.cases_type = type;
// Set current hue to modify colors
const hue = getComputedStyle(document.documentElement).getPropertyValue(`--${type}-hue`);
document.documentElement.style.setProperty('--current-hue', hue);
},
update_displayed_series(state, data) {
state.series = data;
},
update_all_series(state, payload) {
const { data, extra } = payload;
const { iso } = extra;
state.all_series[iso] = data;
},
update_location(state, location) {
state.current_location = location;
},
update_world_map(state, payload) {
const map = payload.data;
state.worldmap = JSON.parse(map);
},
update_current_hover_location(state, iso) {
state.current_hover_location = iso;
},
update_notify(state, data) {
state.notify = { ...state.notify, ...data };
}
},
getters: {
getLocationLoading(state) {
return state.location_loading;
},
getNotifyDetails(state) {
return state.notify;
},
getCountriesList() {
return countriesIso;
},
getCurrentHoverLocation(state) {
return state.current_hover_location;
},
getWorldMap(state) {
return state.worldmap;
},
/**
* Get world map path and value for each location
*
* @param {object} state
* @param {object} getters
*/
getWorldMapData(state, getters) {
const cases = getters.getDayCaseByType;
const mapData = Object.keys(getters.getWorldMap).reduce((acc, key) => {
const value = cases[key]?.value || 0;
acc[key] = {
path: getters.getWorldMap[key],
value
};
return acc;
}, {});
return mapData.length === 0 ? [] : mapData;
},
/**
* Retrun cases based on selected type ('confirmed', 'recovered', 'deaths')
*
* @param {object} state
*/
getDayCaseByType(state) {
const cases = state.day_cases.by_locations[state.cases_type];
const keys = Object.keys(cases).sort((a, b) => cases[b].value - cases[a].value);
const casesOrdered = {};
keys.forEach(k => (casesOrdered[k] = cases[k]));
return casesOrdered.length === 0 ? {} : casesOrdered;
},
/**
* Get total day cases count
*
* @param {object} state
*/
getDayCasesTotalCount(state) {
const casesTypes = ['confirmed', 'deaths', 'recovered'];
const casesTotal = {};
casesTypes.forEach(caseType => {
casesTotal[caseType] =
state.day_cases.by_locations[caseType][state.current_location]?.value;
});
return casesTotal;
},
getMaxCountPerDay(state) {
const max = state.day_cases.max[state.cases_type];
return max || 0;
},
getDisplayedCasesType(state) {
return state.cases_type;
},
getAllSeries(state) {
return state.all_series;
},
getDisplayedSeries(state) {
return state.all_series[state.current_location];
},
getSeriesByCaseType(state, getters) {
return getters.getDisplayedSeries?.aggregate[state.cases_type] || [];
},
getCurrentLocation(state) {
return state.current_location;
}
},
actions: {
loadFromBackend({ commit }, { endpoint, request_data = {}, config = {}, mutation_key }) {
return new Promise((resolve, reject) => {
this._vm.$http
.post(endpoint, request_data, config)
.then(response => {
commit(mutation_key, { data: response.data, extra: request_data });
resolve(response.data);
})
.catch(error => {
commit('update_notify', { message: error, show: true });
reject(error.response);
});
});
}
}
});
const { mapGetters, mapMutations } = Vuex;
let axi = axios.create({
baseURL: 'https://covid.kalimah-apps.com/wp-json/api/v1/',
// baseURL: 'http://kitab.test/wp-json/api/v1/',
timeout: 100000
});
Vue.prototype.$http = axi;
Vue.component('loaderBar', {
template: '#loader-bar-template',
props: ['loading', 'loadingText', 'loadedPercentage', 'loadingIndeterminant']
});
Vue.component('countrieslist', {
template: '#countries-list-template',
data() {
return {
search_keywords: '',
is_loading: {}
};
},
computed: {
...mapGetters([
'getDayCaseByType',
'getCountriesList',
'getCurrentLocation',
'getAllSeries',
'getLocationLoading'
]),
getCasesWithCountriesNames() {
return Object.keys(this.getDayCaseByType).reduce((list, iso) => {
list[iso] = this.getDayCaseByType[iso];
list[iso].name = this.getCountriesList[iso];
return list;
}, {});
},
filterCountriesList() {
const locationsList = this.getCasesWithCountriesNames;
if (this.search_keywords === '') return locationsList;
return Object.keys(locationsList).reduce((list, iso) => {
const location = locationsList[iso];
if (iso !== 'none' && location.name.toLowerCase().indexOf(this.search_keywords) > -1) {
list[iso] = locationsList[iso];
}
return list;
}, {});
}
},
methods: {
...mapMutations(['update_location', 'update_location_loading']),
selectCountry(iso) {
// check if the data for this location has been loaded already
if (iso in this.getAllSeries) {
this.update_location(iso);
return;
}
// Don't process if there is another country data being loaded
if (this.getLocationLoading !== false) return;
// show loader
//this.$set(this.is_loading, iso, true);
this.update_location_loading(iso);
this.$store
.dispatch('loadFromBackend', {
endpoint: 'get-total-series',
request_data: { iso },
mutation_key: 'update_all_series'
})
.then(() => {
this.update_location(iso);
this.update_location_loading(false);
})
.catch(() => {
this.update_location_loading(false);
});
},
filterCountries(e) {
this.search_keywords = e.target.value.toLowerCase();
}
}
});
Vue.component('flag', {
template: '#flag-template',
props: ['iso'],
computed: {
flag() {
if (this.iso === 'none' || this.iso === '') return '';
//return `https://covid.kalimah-apps.com/index.php?flag_iso=${this.iso}`;
return `https://raw.githubusercontent.com/lipis/flag-icon-css/master/flags/4x3/${this.iso}.svg`;
}
}
});
Vue.component('location-tooltip', {
template: '#location-tooltip-template',
data() {
return {
tooltip_x: 0,
tooltip_y: 0,
show_tooltip: false
};
},
mounted() {
this.$root.$on('mouse_coord', coord => {
this.tooltip_x = coord.tooltip_x;
this.tooltip_y = coord.tooltip_y;
});
this.$root
.$on('tooltip_on', () => {
this.show_tooltip = true;
})
.$on('tooltip_off', () => {
this.show_tooltip = false;
});
},
computed: {
...mapGetters([
'getDayCaseByType',
'getCountriesList',
'getCurrentHoverLocation',
'getDisplayedCasesType'
]),
getTooltipData() {
const location = this.getCurrentHoverLocation;
if (location === 'WO' || location === '') return {};
const data = this.getDayCaseByType[location];
// Some countires (Turkminstan and Greenland) don't have data
if (typeof data === 'undefined') {
return {
title: this.getCountriesList[location]
};
}
// Get data and return an object
const dailyDiffValue = data.diff.value;
const dailyDiffType = data.diff.type;
// Make sure percentage is only showing two decimal points
const dailyPercentage = (data.diff.value * 100) / data.value;
const dailyPercentageRounded = Math.round(dailyPercentage * 100) / 100;
return {
title: this.getCountriesList[location],
type: this.getDisplayedCasesType,
count: data.value.toLocaleString('en-US'),
diff: dailyDiffValue.toLocaleString('en-US'),
difftext: dailyDiffType === 'extra' ? 'Increase' : 'Decrease',
percentage: dailyPercentageRounded
};
},
getTooltipStyle() {
const left = this.tooltip_x;
const top = this.tooltip_y;
return {
opacity: this.show_tooltip === true ? 1 : 0,
left: `${left}px`,
top: `${top}px`
};
}
}
});
Vue.component('notify', {
template: '#notify-template',
props: ['show', 'type', 'message'],
data() {
return {
show_notify: false
};
},
methods: {
...mapMutations(['update_notify'])
},
watch: {
show(newShowValue) {
this.show_notify = newShowValue;
if (newShowValue === false) return;
setTimeout(() => {
this.update_notify({ message: '', show: false });
}, 8000);
}
}
});
Vue.component('worldmap', {
template: '#worldmap-template',
data() {
return {
map_container: null,
map_svg: null,
loading: true,
loading_text: 'Loading Map',
loaded_percentage: 0,
viewbox: '0 267 1000 400',
show_tooltip_timer: null,
show_tooltip: false,
tooltip_x: 0,
tooltip_y: 0,
x_rotate: 0,
y_rotate: 0
};
},
mounted() {
this.$store.dispatch('loadFromBackend', {
endpoint: 'get-day-cases',
mutation_key: 'update_day_cases'
});
// Load world map on start
this.$store
.dispatch('loadFromBackend', {
endpoint: 'get-world-map',
mutation_key: 'update_world_map',
config: {
onDownloadProgress: progressEvent => {
const total = parseFloat(
progressEvent.currentTarget.getResponseHeader('x-map-filesize')
);
const current = progressEvent.loaded;
const percentCompleted = Math.floor((current / total) * 100);
this.loaded_percentage = percentCompleted;
}
}
})
.then(() => {
this.loading = false;
})
.catch(() => {
this.loading = false;
});
this.map_container = this.$refs.world_map_container;
this.map_svg = this.$refs.world_map;
},
methods: {
...mapMutations([
'update_location',
'update_current_hover_location',
'update_location_loading'
]),
resetLocation() {
this.update_location('WO');
},
/**
* Rotate map on mouse move
*
* @param {object} e Event
*/
rotateMap(e) {
const rect = this.map_svg.getBoundingClientRect();
const left = e.x - rect.x;
const top = e.y - rect.y;
const horizontalMiddle = rect.height / 2;
const verticalMiddle = rect.width / 2;
this.x_rotate = (((top - horizontalMiddle) * 4) / horizontalMiddle) * -1;
this.y_rotate = ((left - verticalMiddle) * 4) / verticalMiddle;
},
stopRotateMap() {
setTimeout(() => {
this.x_rotate = 0;
this.y_rotate = 0;
}, 200);
},
selectLocation(iso) {
// Dont process click if not data available
const data = this.getDayCaseByType[iso];
if (typeof data === 'undefined') {
return;
}
// check if the data for this location has been loaded already
if (iso in this.getAllSeries) {
this.update_location(iso);
return;
}
// show loader
this.update_location_loading(true);
this.$store
.dispatch('loadFromBackend', {
endpoint: 'get-total-series',
request_data: { iso },
mutation_key: 'update_all_series'
})
.then(() => {
this.update_location(iso);
this.update_location_loading(false);
})
.catch(() => {
this.update_location_loading(false);
});
},
/**
* Each additional decimal place means differnt range of lightness.
* 100, 1000, 10000 and so on.
* This function calculate the lightness within the range of the decimal places
*/
getLightRange(max, range) {
const split = max.toString().split('');
const l = split.length - 1;
return {
max: Math.round((50 / l) * range),
min: Math.round((50 / l) * range - 1)
};
},
/**
* Get percentage number within a provided range
*/
getPercentageRangeNumber(min, max, percent) {
return Math.round(min + ((max - min) * percent) / 50);
},
/**
* Get color of location based on its value
* Darker means higher value
*/
getColor(value) {
// Get decimal places
const decimalPlaces = value.toString().split('').length;
const maxCount = this.getMaxCountPerDay;
// Get lightness percentage between 0 and 50
const lightnessPercent = Math.round((value * 50) / maxCount);
const range = this.getLightRange(maxCount, decimalPlaces);
const adjustedLightnessPercentage = this.getPercentageRangeNumber(
range.min,
range.max,
lightnessPercent
);
const hueColor = `${this.getCurrentHueColor()}deg`;
/*
* return hsl color. 80 is used to reverse colors
* to represent higher values with dark color accents and
* lower values with light color accents
*/
return `hsl(${hueColor}, 70%, ${80 - adjustedLightnessPercentage}%)`;
},
/**
* Handle mouse enter on specific location to show tooltip
*
* @param {object} event
* @param {string} iso
*/
handleMoustEnter(event, iso) {
clearTimeout(this.show_tooltip_timer);
this.update_current_hover_location(iso);
this.$root.$emit('tooltip_on');
},
/**
* Emit coordinates on mouse move to move tooltip
*
* @param {object} e Mouse event object
*/
handleMouseMove(e) {
this.$root.$emit('mouse_coord', { tooltip_x: e.pageX, tooltip_y: e.pageY });
},
/**
* Hide tooltip on mouse out
*/
handleMoustOut() {
clearTimeout(this.show_tooltip_timer);
this.show_tooltip_timer = setTimeout(() => {
// this.show_tooltip = false;
this.$root.$emit('tooltip_off');
}, 100);
},
getCurrentHueColor() {
return getComputedStyle(document.documentElement).getPropertyValue('--current-hue');
}
},
computed: {
...mapGetters([
'getDayCaseByType',
'getWorldMapData',
'getMaxCountPerDay',
'getDisplayedCasesType',
'getCountriesList',
'getCurrentLocation',
'getAllSeries',
'getCurrentHoverLocation',
'getLocationLoading'
]),
getMapStyle() {
return {
'--map-rotate-x': `${this.x_rotate}deg`,
'--map-rotate-y': `${this.y_rotate}deg`
};
},
/**
* Return world map data with choropleth hsl values
*/
getWorlMapWithColors() {
return Object.keys(this.getWorldMapData).reduce((acc, key) => {
acc[key] = this.getWorldMapData[key];
acc[key].color = this.getColor(acc[key].value);
return acc;
}, {});
},
getMaxCount() {
return Object.keys(this.getWorldMapData);
}
},
watch: {
getWorlMapWithColors() {
this.$nextTick(() => {});
},
getCurrentLocation(newLocationsIso) {
const iso = newLocationsIso;
// let viewbox = '0 0 700.9375 337.375';
let viewbox = '0 267.77886962890625 1000 400';
// get locations iso (if world is not selected)
if (iso !== 'WO' && this.map_svg != null) {
const { width, height, x, y } = document.querySelector(`#${iso}`).getBBox();
const halfWidth = width / 2;
const halfHeight = height / 2;
viewbox = `${x - halfWidth} ${y - halfHeight} ${width + width} ${height + height}`;
}
// animate viewbox for selected ISO
TweenLite.to(this.$data, {
duration: 1.5,
ease: 'expo.out',
viewbox: viewbox
});
}
}
});
Vue.component('series', {
template: '#series-template',
data() {
return {
chart: null,
chart_type: 'bar',
series_type: 'daily',
series_representation: 'change',
series_loading: true,
monthNames: [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec'
]
};
},
mounted() {
// Check if there is data in localStorage and apply it
const keys = ['chart_type', 'series_type', 'series_representation'];
keys.forEach(k => {
const stateValue = localStorage.getItem(k);
if (stateValue != null) this[k] = stateValue;
});
/**
* Show a dashed line vertically on line graphs
*/
Chart.plugins.register({
afterDatasetDraw: (chart, dataset) => {
if (chart.tooltip._active && chart.tooltip._active.length && dataset.meta.type === 'line') {
const activePoint = chart.tooltip._active[0];
const { ctx } = chart;
const yAxis = chart.scales['y-axis-0'];
const { x } = activePoint.tooltipPosition();
const topY = yAxis.top;
const bottomY = yAxis.bottom; // activePoint.tooltipPosition().y;
// Get current hue and darken it
const currentHue = this.getCurrentHueColor();
// draw line
ctx.save();
ctx.beginPath();
ctx.setLineDash([3, 3]);
ctx.moveTo(x, topY);
ctx.lineTo(x, bottomY);
ctx.lineWidth = 1;
ctx.strokeStyle = `hsla(${currentHue}deg, 100%, 15%, 0.8)`;
ctx.stroke();
ctx.restore();
}
}
});
// Setup chart
Chart.defaults.scale.gridLines.drawOnChartArea = false;
this.chart = new Chart(this.$refs.chart, {
type: 'bar',
data: {
labels: [],
datasets: [
{
label: 'Count',
backgroundColor: '#3e95cd',
hoverBackgroundColor: 'blue'
},
{
label: 'Count',
fill: false,
type: 'line',
pointRadius: 0,
pointHoverRadius: 6,
pointHoverBackgroundColor: 'red',
pointHoverBorderWidth: 0,
hidden: true
}
]
},
options: {
hover: {
mode: 'index',
intersect: false,
animationDuration: 0
},
tooltips: {
intersect: false,
enabled: false,
mode: 'index',
/**
* Custom tooltip with div
*/
custom(tooltipModel) {
// Tooltip Element
const tooltipEl = document.querySelector('#chartjs-tooltip');
const title = tooltipEl.querySelector('.title');
const body = tooltipEl.querySelector('.count-total-number');
// Hide/show tooltip
tooltipEl.style.opacity = tooltipModel.opacity;
// Set Text
if (tooltipModel.body) {
const titleLines = tooltipModel.title || [];
const bodyLines = tooltipModel.body.map(bodyItem => bodyItem.lines);
title.innerHTML = titleLines[0];
body.innerHTML = bodyLines[0];
}
tooltipEl.style.left = `${tooltipModel.caretX}px`;
},
callbacks: {
label: tooltipItem => {
let label = this.getDisplayedCasesType;
if (label) {
label += ': ';
}
label += tooltipItem.yLabel.toLocaleString('en-US');
return label;
}
}
},
maintainAspectRatio: false,
legend: { display: false },
scales: {
yAxes: [
{
ticks: {
beginAtZero: true,
callback: value => this.format(value)
}
}
],
xAxes: [
{
type: 'time',
offset: true
}
]
}
}
});
// check if the data for this location has been loaded already
if ('WO' in this.getAllSeries) {
this.update_location('WO');
this.series_loading = false;
return;
}
this.$store
.dispatch('loadFromBackend', {
endpoint: 'get-total-series',
request_data: { iso: 'WO' },
mutation_key: 'update_all_series'
})
.then(() => {
this.series_loading = false;
this.update_location('WO');
this.updateChart();
})
.catch(() => {
this.series_loading = false;
});
},
methods: {
...mapMutations(['update_location']),
/**
* Format long numbers to display short numbers with B for billions,
* M for millions and K for thousands
*/
format(num) {
const number = Math.abs(Number(num));
let formattedNumber = number;
// Nine Zeroes for Billions
switch (true) {
case number >= 1.0e9:
formattedNumber = `${Math.round(number) / 1.0e9} B`;
break;
case number >= 1.0e6:
formattedNumber = `${Math.round(number) / 1.0e6} M`;
break;
case number >= 1.0e3:
formattedNumber = `${Math.round(number) / 1.0e3} K`;
break;
default:
formattedNumber = number;
break;
}
return formattedNumber;
},
getCurrentHueColor() {
return getComputedStyle(document.documentElement).getPropertyValue('--current-hue');
},
/**
* Update chart to reflect changes. FromTo daily and weekly. FromTo bar and line,
* FromTo cumlative, change and log
*/
updateChart(enableAnimation = false) {
const newType = this.series_type;
const seriesData = this.getSeriesByCaseType;
if (seriesData.length === 0) return;
const data = [];
const labels = [];
let xAxes = {};
const rep = this.series_representation;
// if series type is set week, aggregate data
if (newType === 'weekly') {
let lastWeek = 0;
let count = 0;
seriesData.forEach(series => {
const currentWeek = moment(series.date).week();
count += rep === 'cumulative' ? parseInt(series.value) : parseInt(series.diff.value);
if (lastWeek !== currentWeek) {
labels.push(series.date);
data.push(count);
count = 0;
lastWeek = currentWeek;
}
});
xAxes = {
barPercentage: 1.1,
categoryPercentage: 1.2,
time: {
unit: 'week',
unitStepSize: 5,
displayFormats: { week: 'w - YYYY' }
}
};
} else {
seriesData.forEach(series => {
data.push(rep === 'cumulative' ? series.value : series.diff.value);
labels.push(series.date);
});
xAxes = {
barPercentage: 0.8,
categoryPercentage: 0.9,
time: {
unit: 'month',
unitStepSize: 1,
displayFormats: { month: 'MMM YYYY' }
}
};
}
// update x axes
this.chart.options.scales.xAxes[0] = { ...this.chart.options.scales.xAxes[0], ...xAxes };
// Show logarthmic or linear scale
let yAxesType = 'linear';
if (this.series_representation === 'log') yAxesType = 'logarithmic';
this.chart.options.scales.yAxes[0].type = yAxesType;
// update dataset
this.chart.data.datasets.forEach((dataset, i) => {
dataset.data = data;
// updte color baased on series selected
dataset.backgroundColor = `hsl(${this.getCurrentHueColor()}deg, 80%, 50%)`;
dataset.borderColor = `hsl(${this.getCurrentHueColor()}deg, 80%, 50%)`;
// Bar hover color
dataset.hoverBackgroundColor = `hsl(${this.getCurrentHueColor()}deg, 100%, 10%)`;
// line point (circle) background color and border color
dataset.pointHoverBackgroundColor = `hsl(${this.getCurrentHueColor()}deg, 80%, 50%)`;
dataset.pointHoverBorderColor = `hsl(${this.getCurrentHueColor()}deg, 80%, 50%)`;
// Hide/show dataset based on chart type
if (this.chart_type === 'bar') {
dataset.hidden = i !== 0;
} else {
dataset.hidden = i !== 1;
}
});
this.chart.data.labels = labels;
if (!enableAnimation) this.chart.update(0);
else this.chart.update();
}
},
computed: {
...mapGetters(['getSeriesByCaseType', 'getDisplayedCasesType', 'getAllSeries'])
},
watch: {
chart_type(newType) {
localStorage.setItem('chart_type', newType);
this.updateChart(true);
},
series_type(newType) {
localStorage.setItem('series_type', newType);
this.updateChart();
},
series_representation(newRepresentation) {
localStorage.setItem('series_representation', newRepresentation);
this.updateChart(true);
},
getDisplayedCasesType() {
// update chart
this.updateChart(true);
},
getSeriesByCaseType() {
this.updateChart(true);
}
}
});
new Vue({
el: '#app',
store,
data: {
show_countries_list: false,
confirmed: 0,
recovered: 0,
deaths: 0,
show_copyright: false,
is_full_screen: false,
copyright_data: {
'Covid Data': 'https://github.com/CSSEGISandData/COVID-19',
Flags: 'https://github.com/lipis/flag-icon-css',
Map: 'https://github.com/markmarkoh/datamaps',
FontAwesome: 'https://fontawesome.com/',
'Chart.js': 'https://www.chartjs.org/',
gsap: 'https://greensock.com/gsap/',
'moment.js': 'https://momentjs.com/',
'Subtle Patterns': 'https://www.toptal.com/designers/subtlepatterns/'
}
},
methods: {
...mapMutations({ changeCaseType: 'update_cases_type' }),
enterFullScreen() {
const element = document.querySelector('#app');
const requestFullScreen =
element.requestFullscreen ||
element.webkitRequestFullScreen ||
element.mozRequestFullScreen ||
element.msRequestFullScreen;
requestFullScreen.call(element);
},
exitFullScreen() {
const cancellFullScreen =
document.exitFullscreen ||
document.mozCancelFullScreen ||
document.webkitExitFullscreen ||
document.msExitFullscreen;
cancellFullScreen.call(document);
}
},
computed: {
...mapGetters([
'getDayCasesTotalCount',
'getDisplayedCasesType',
'getCurrentLocation',
'getCountriesList',
'getNotifyDetails'
]),
recoveredCount() {
return Number.isNaN(this.recovered) ? 'No Data' : this.recovered.toLocaleString('en-US');
},
deathsCount() {
return this.deaths.toLocaleString('en-US');
},
confirmedCount() {
return this.confirmed.toLocaleString('en-US');
}
},
watch: {
/**
* Animate count change on every update
*/
getDayCasesTotalCount(newTotals) {
Object.keys(newTotals).forEach(caseType => {
TweenLite.to(this.$data, 1, {
[caseType]: newTotals[caseType],
roundProps: caseType,
ease: 'expo.out'
});
});
},
is_full_screen(newValue) {
if (newValue === true) this.enterFullScreen();
else this.exitFullScreen();
}
}
});
</script>