Coronazahlen Weltweit

Zeigt die aktuellen Coronazahlen weltweit an Script defekt
API nicht mehr erreichbar

Der hier verwendete 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>