added chart, added full statistics endpoint, refactoring

develop
bvn13 2020-04-20 10:51:50 +03:00
parent 567d8d7841
commit 0010beca9f
33 changed files with 17613 additions and 118 deletions

View File

@ -3,12 +3,11 @@
version=0.0.2
./gradlew :covid19-db-migrator:clean :covid19-db-migrator:assemble
./gradlew :covid19-api:clean :covid19-api:assemble
./gradlew :covid19-site:clean :covid19-site:assemble
./gradlew :covid19-scheduler:clean :covid19-scheduler:assemble
[ ! -d "build" ] && mkdir "build";
cp "covid19-db-migrator/build/libs/covid19-db-migrator-$version.jar" "build/covid19-db-migrator-$version.jar"
cp "covid19-api/build/libs/covid19-api-$version.jar" "build/covid19-api-$version.jar"
cp "covid19-site/build/libs/covid19-site-$version.jar" "build/covid19-site-$version.jar"
cp "covid19-scheduler/build/libs/covid19-scheduler-$version.jar" "build/covid19-scheduler-$version.jar"

View File

@ -1,73 +0,0 @@
/*
Copyright [2020] [bvn13]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.bvn13.covid19.api.controllers;
import com.bvn13.covid19.api.model.CovidStatsInfo;
import com.bvn13.covid19.api.model.CovidStatsResponse;
import com.bvn13.covid19.api.service.CovidStatsMaker;
import com.bvn13.covid19.model.entities.CovidStat;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import java.time.ZoneId;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class ApiController {
private final CovidStatsMaker covidStatsMaker;
@Value("${app.zone-id}")
private String zoneIdStr;
private ZoneId zoneId;
@PostConstruct
public void init() {
zoneId = ZoneId.of(zoneIdStr);
}
@GetMapping
public CovidStatsResponse getStatistics() {
return covidStatsMaker.findLastUpdateInfo()
.map(updateInfo -> CovidStatsResponse.builder()
.datetime(updateInfo.getDatetime())
.updatedOn(updateInfo.getCreatedOn().atZone(zoneId))
.stats(convertStats(covidStatsMaker.findCovidStatsByUpdateInfoId(updateInfo.getId())))
.build())
.orElse(CovidStatsResponse.builder().build());
}
private List<CovidStatsInfo> convertStats(Collection<CovidStat> stats) {
return stats.stream()
.map(stat -> CovidStatsInfo.builder()
.region(stat.getRegion().getName())
.sick(stat.getSick())
.healed(stat.getHealed())
.died(stat.getDied())
.build())
.collect(Collectors.toList());
}
}

View File

@ -6,6 +6,7 @@ dependencies {
compile(project(':covid19-model'))
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package com.bvn13.covid19.api;
package com.bvn13.covid19.site;
import com.bvn13.covid19.model.Covid19ModelConfig;
import org.springframework.boot.SpringApplication;
@ -27,10 +27,10 @@ import org.springframework.context.annotation.Import;
@Import({
Covid19ModelConfig.class
})
public class Covid19ApiApplication {
public class Covid19SiteApplication {
public static void main(String[] args) {
SpringApplication.run(Covid19ApiApplication.class, args);
SpringApplication.run(Covid19SiteApplication.class, args);
}
}

View File

@ -0,0 +1,14 @@
package com.bvn13.covid19.site.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
public class MainConfig {
@Value("${app.main-url}")
private String mainUrl;
}

View File

@ -0,0 +1,95 @@
package com.bvn13.covid19.site.controllers;
import com.bvn13.covid19.model.entities.CovidStat;
import com.bvn13.covid19.site.model.CovidAllStats;
import com.bvn13.covid19.site.model.CovidData;
import com.bvn13.covid19.site.model.CovidDayStats;
import com.bvn13.covid19.site.service.CovidStatsMaker;
import com.bvn13.covid19.site.service.CovidStatsResponseMaker;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@RestController
@RequestMapping("/stats")
public class AllStatsController {
private final CovidStatsMaker covidStatsMaker;
private final CovidStatsResponseMaker covidStatsResponseMaker;
@Value("${app.zone-id}")
private String zoneIdStr;
private ZoneId zoneId;
@PostConstruct
public void init() {
zoneId = ZoneId.of(zoneIdStr);
}
@GetMapping("/all")
public CovidAllStats getStatistics(@RequestParam("region") String regionName) {
if (StringUtils.isNotBlank(regionName)) {
return constructResponseForRegion(regionName);
} else {
return constructResponseForAllRegions();
}
}
private CovidAllStats constructResponseForRegion(String regionName) {
return CovidAllStats.builder()
.regions(Collections.singletonList(regionName))
.progress(covidStatsMaker.findAllLastUpdatesPerDay().stream()
.map(covidUpdate -> CovidDayStats.builder()
.datetime(covidUpdate.getDatetime())
.updatedOn(covidUpdate.getCreatedOn().atZone(zoneId))
.stats(convertStatsToData(findCovidStatsByUpdateIdAndRegion(covidUpdate.getId(), regionName)))
.build())
.sorted(CovidDayStats::compareTo)
.collect(Collectors.toList()))
.build();
}
private CovidAllStats constructResponseForAllRegions() {
return CovidAllStats.builder()
.regions(covidStatsMaker.findAllRegionsNames())
.progress(covidStatsMaker.findAllLastUpdatesPerDay().stream()
.map(covidUpdate -> CovidDayStats.builder()
.datetime(covidUpdate.getDatetime())
.updatedOn(covidUpdate.getCreatedOn().atZone(zoneId))
.stats(convertStatsToData(findCovidStatsByUpdateId(covidUpdate.getId())))
.build())
.sorted(CovidDayStats::compareTo)
.collect(Collectors.toList()))
.build();
}
private List<CovidData> convertStatsToData(Collection<CovidStat> stats) {
return covidStatsResponseMaker.convertStats(stats);
}
private List<CovidStat> findCovidStatsByUpdateIdAndRegion(long updateId, String region) {
return covidStatsMaker.findCovidStatsByUpdateInfoId(updateId).stream()
.filter(covidStat -> region.equals(covidStat.getRegion().getName()))
.collect(Collectors.toList());
}
private List<CovidStat> findCovidStatsByUpdateId(long updateId) {
return new ArrayList<>(covidStatsMaker.findCovidStatsByUpdateInfoId(updateId));
}
}

View File

@ -0,0 +1,59 @@
/*
Copyright [2020] [bvn13]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.bvn13.covid19.site.controllers;
import com.bvn13.covid19.site.model.CovidDayStats;
import com.bvn13.covid19.site.service.CovidStatsMaker;
import com.bvn13.covid19.site.service.CovidStatsResponseMaker;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import java.time.ZoneId;
@RequiredArgsConstructor
@RestController
@RequestMapping("/stats")
public class LastStatsController {
private final CovidStatsMaker covidStatsMaker;
private final CovidStatsResponseMaker covidStatsResponseMaker;
@Value("${app.zone-id}")
private String zoneIdStr;
private ZoneId zoneId;
@PostConstruct
public void init() {
zoneId = ZoneId.of(zoneIdStr);
}
@GetMapping("/last")
public CovidDayStats getStatistics() {
return covidStatsMaker.findLastUpdate()
.map(covidUpdate -> CovidDayStats.builder()
.datetime(covidUpdate.getDatetime())
.updatedOn(covidUpdate.getCreatedOn().atZone(zoneId))
.stats(covidStatsResponseMaker.convertStats(covidStatsMaker.findCovidStatsByUpdateInfoId(covidUpdate.getId())))
.build())
.orElse(CovidDayStats.builder().build());
}
}

View File

@ -0,0 +1,16 @@
package com.bvn13.covid19.site.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/")
public class MainController {
@GetMapping
public String mainPage() {
return "main-page";
}
}

View File

@ -0,0 +1,23 @@
package com.bvn13.covid19.site.controllers;
import com.bvn13.covid19.site.service.CovidStatsMaker;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RequiredArgsConstructor
@RestController
@RequestMapping("/regions")
public class RegionsController {
private final CovidStatsMaker covidStatsMaker;
@GetMapping
public List<String> getAllRegions() {
return covidStatsMaker.findAllRegionsNames();
}
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package com.bvn13.covid19.api.converters;
package com.bvn13.covid19.site.converters;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package com.bvn13.covid19.api.dtos;
package com.bvn13.covid19.site.dtos;
import lombok.Value;

View File

@ -0,0 +1,15 @@
package com.bvn13.covid19.site.model;
import lombok.Builder;
import lombok.Value;
import java.util.List;
@Builder
@Value
public class CovidAllStats {
List<String> regions;
List<CovidDayStats> progress;
}

View File

@ -14,28 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package com.bvn13.covid19.api.model;
package com.bvn13.covid19.site.model;
import lombok.Builder;
import lombok.Value;
import java.time.ZonedDateTime;
@Builder
@Value
public class CovidStatsInfo {
public class CovidData {
String region;
long sick;
long healed;
long died;
@Builder
@Value
public static class Delta {
long sick;
long healed;
long died;
}
}

View File

@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package com.bvn13.covid19.api.model;
package com.bvn13.covid19.site.model;
import lombok.Builder;
import lombok.NonNull;
import lombok.Singular;
import lombok.Value;
@ -25,11 +26,24 @@ import java.util.List;
@Builder
@Value
public class CovidStatsResponse {
public class CovidDayStats implements Comparable<CovidDayStats> {
ZonedDateTime updatedOn;
ZonedDateTime datetime;
@Singular(value = "stats")
List<CovidStatsInfo> stats;
List<CovidData> stats;
@Override
public int compareTo(@NonNull CovidDayStats another) {
if (this.equals(another)) {
return 0;
} else {
return updatedOn.isBefore(another.getUpdatedOn())
? -1
: updatedOn.isEqual(another.getUpdatedOn())
? 0
: 1;
}
}
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package com.bvn13.covid19.api.repositories;
package com.bvn13.covid19.site.repositories;
import com.bvn13.covid19.model.entities.CovidStat;
import com.bvn13.covid19.model.entities.CovidUpdate;

View File

@ -14,22 +14,26 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package com.bvn13.covid19.api.repositories;
package com.bvn13.covid19.site.repositories;
import com.bvn13.covid19.api.dtos.CovidUpdateInfoDto;
import com.bvn13.covid19.site.dtos.CovidUpdateInfoDto;
import com.bvn13.covid19.model.entities.CovidUpdate;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Optional;
@Repository
public interface CovidUpdateInfosRepository extends JpaRepository<CovidUpdate, Long> {
public interface CovidUpdatesRepository extends JpaRepository<CovidUpdate, Long> {
@Query("select new com.bvn13.covid19.api.dtos.CovidUpdateInfoDto(max(U.createdOn)) from CovidUpdate U")
Optional<CovidUpdateInfoDto> findLastUpdateInfo();
@Query("select new com.bvn13.covid19.site.dtos.CovidUpdateInfoDto(max(U.createdOn)) from CovidUpdate U")
Optional<CovidUpdateInfoDto> findLastUpdate();
@Query("select U from CovidUpdate U where U.id in (select max(U1.id) from CovidUpdate U1 group by U1.datetime)")
Collection<CovidUpdate> findAllLastUpdatesPerDay();
Optional<CovidUpdate> findFirstByCreatedOn(LocalDateTime createdOn);

View File

@ -0,0 +1,9 @@
package com.bvn13.covid19.site.repositories;
import com.bvn13.covid19.model.entities.Region;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RegionsRepository extends JpaRepository<Region, Long> {
}

View File

@ -14,12 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package com.bvn13.covid19.api.service;
package com.bvn13.covid19.site.service;
import com.bvn13.covid19.api.repositories.CovidStatsRepository;
import com.bvn13.covid19.api.repositories.CovidUpdateInfosRepository;
import com.bvn13.covid19.model.entities.CovidStat;
import com.bvn13.covid19.model.entities.CovidUpdate;
import com.bvn13.covid19.model.entities.Region;
import com.bvn13.covid19.site.repositories.CovidStatsRepository;
import com.bvn13.covid19.site.repositories.CovidUpdatesRepository;
import com.bvn13.covid19.site.repositories.RegionsRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
@ -27,30 +29,33 @@ import org.springframework.stereotype.Component;
import javax.transaction.Transactional;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Component
public class CovidStatsMaker {
private final CovidStatsRepository covidRepository;
private final CovidUpdateInfosRepository updatesRepository;
private final CovidStatsRepository statsRepository;
private final CovidUpdatesRepository updatesRepository;
private final RegionsRepository regionsRepository;
@Transactional
public Collection<CovidStat> getLastCovidStats() {
return updatesRepository.findLastUpdateInfo()
return updatesRepository.findLastUpdate()
.flatMap(updateInfo -> updatesRepository.findFirstByCreatedOn(updateInfo.getCreatedOn()))
.map(covidRepository::findAllByUpdateInfo)
.map(statsRepository::findAllByUpdateInfo)
.orElse(Collections.emptyList());
}
@Cacheable(
cacheNames = "covid-last-update-info",
cacheNames = "covid-last-update",
unless = "#result == null"
)
@Transactional
public Optional<CovidUpdate> findLastUpdateInfo() {
return updatesRepository.findLastUpdateInfo()
public Optional<CovidUpdate> findLastUpdate() {
return updatesRepository.findLastUpdate()
.flatMap(updateInfo -> updatesRepository.findFirstByCreatedOn(updateInfo.getCreatedOn()));
}
@ -60,7 +65,23 @@ public class CovidStatsMaker {
unless = "#result == null || #result.size() <= 0"
)
public Collection<CovidStat> findCovidStatsByUpdateInfoId(long updateInfoId) {
return covidRepository.findAllByUpdateInfo_Id(updateInfoId);
return statsRepository.findAllByUpdateInfo_Id(updateInfoId);
}
@Cacheable(
cacheNames = "covid-all-days-updates",
unless = "#result == null || #result.size() <= 0"
)
public Collection<CovidUpdate> findAllLastUpdatesPerDay() {
return updatesRepository.findAllLastUpdatesPerDay();
}
@Cacheable(
cacheNames = "covid-regions",
unless = "#result == null || #result.size() <= 0"
)
public List<String> findAllRegionsNames() {
return regionsRepository.findAll().stream().map(Region::getName).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,25 @@
package com.bvn13.covid19.site.service;
import com.bvn13.covid19.model.entities.CovidStat;
import com.bvn13.covid19.site.model.CovidData;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class CovidStatsResponseMaker {
public List<CovidData> convertStats(Collection<CovidStat> stats) {
return stats.stream()
.map(stat -> CovidData.builder()
.region(stat.getRegion().getName())
.sick(stat.getSick())
.healed(stat.getHealed())
.died(stat.getDied())
.build())
.collect(Collectors.toList());
}
}

View File

@ -3,6 +3,7 @@ server:
app:
zone-id: Europe/Moscow
main-url: http://localhost:8080
spring:
application:
@ -12,7 +13,7 @@ spring:
type: caffeine
caffeine:
spec: expireAfterWrite=15m
cache-names: covid-last-update-info, covid-stats-by-update-info-id
cache-names: covid-last-update, covid-all-days-updates, covid-stats-by-update-info-id, covid-regions
flyway:
enabled: false

View File

@ -0,0 +1 @@
@keyframes chartjs-render-animation{from{opacity:.99}to{opacity:1}}.chartjs-render-monitor{animation:chartjs-render-animation 1ms}.chartjs-size-monitor,.chartjs-size-monitor-expand,.chartjs-size-monitor-shrink{position:absolute;direction:ltr;left:0;top:0;right:0;bottom:0;overflow:hidden;pointer-events:none;visibility:hidden;z-index:-1}.chartjs-size-monitor-expand>div{position:absolute;width:1000000px;height:1000000px;left:0;top:0}.chartjs-size-monitor-shrink>div{position:absolute;width:200%;height:200%;left:0;top:0}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n():"function"==typeof define&&define.amd?define(n):n()}(0,function(){"use strict";function e(e){var n=this.constructor;return this.then(function(t){return n.resolve(e()).then(function(){return t})},function(t){return n.resolve(e()).then(function(){return n.reject(t)})})}function n(e){return!(!e||"undefined"==typeof e.length)}function t(){}function o(e){if(!(this instanceof o))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],c(e,this)}function r(e,n){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,o._immediateFn(function(){var t=1===e._state?n.onFulfilled:n.onRejected;if(null!==t){var o;try{o=t(e._value)}catch(r){return void f(n.promise,r)}i(n.promise,o)}else(1===e._state?i:f)(n.promise,e._value)})):e._deferreds.push(n)}function i(e,n){try{if(n===e)throw new TypeError("A promise cannot be resolved with itself.");if(n&&("object"==typeof n||"function"==typeof n)){var t=n.then;if(n instanceof o)return e._state=3,e._value=n,void u(e);if("function"==typeof t)return void c(function(e,n){return function(){e.apply(n,arguments)}}(t,n),e)}e._state=1,e._value=n,u(e)}catch(r){f(e,r)}}function f(e,n){e._state=2,e._value=n,u(e)}function u(e){2===e._state&&0===e._deferreds.length&&o._immediateFn(function(){e._handled||o._unhandledRejectionFn(e._value)});for(var n=0,t=e._deferreds.length;t>n;n++)r(e,e._deferreds[n]);e._deferreds=null}function c(e,n){var t=!1;try{e(function(e){t||(t=!0,i(n,e))},function(e){t||(t=!0,f(n,e))})}catch(o){if(t)return;t=!0,f(n,o)}}var a=setTimeout;o.prototype["catch"]=function(e){return this.then(null,e)},o.prototype.then=function(e,n){var o=new this.constructor(t);return r(this,new function(e,n,t){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof n?n:null,this.promise=t}(e,n,o)),o},o.prototype["finally"]=e,o.all=function(e){return new o(function(t,o){function r(e,n){try{if(n&&("object"==typeof n||"function"==typeof n)){var u=n.then;if("function"==typeof u)return void u.call(n,function(n){r(e,n)},o)}i[e]=n,0==--f&&t(i)}catch(c){o(c)}}if(!n(e))return o(new TypeError("Promise.all accepts an array"));var i=Array.prototype.slice.call(e);if(0===i.length)return t([]);for(var f=i.length,u=0;i.length>u;u++)r(u,i[u])})},o.resolve=function(e){return e&&"object"==typeof e&&e.constructor===o?e:new o(function(n){n(e)})},o.reject=function(e){return new o(function(n,t){t(e)})},o.race=function(e){return new o(function(t,r){if(!n(e))return r(new TypeError("Promise.race accepts an array"));for(var i=0,f=e.length;f>i;i++)o.resolve(e[i]).then(t,r)})},o._immediateFn="function"==typeof setImmediate&&function(e){setImmediate(e)}||function(e){a(e,0)},o._unhandledRejectionFn=function(e){void 0!==console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)};var l=function(){if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if("undefined"!=typeof global)return global;throw Error("unable to locate global object")}();"Promise"in l?l.Promise.prototype["finally"]||(l.Promise.prototype["finally"]=e):l.Promise=o});

View File

@ -0,0 +1,158 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Home page</title>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<!-- <link rel="stylesheet" th:href="@{/css/chart.min.css}">-->
</head>
<body>
<p>
<span th:text="'Today is: ' + ${#dates.format(#dates.createNow(), 'dd MMM yyyy HH:mm')}" th:remove="tag"></span>
</p>
<p>
<a th:href="${@mainConfig.getMainUrl()} + '/stats/last'">Последние данные</a>
</p>
<p>
<a th:href="${@mainConfig.getMainUrl()} + '/stats/all'">Все данные</a>
</p>
<p>
<label for="region">Регион</label>
<select id="region"></select>
</p>
<div style="width: 75%;">
<canvas id="chart"></canvas>
</div>
<script th:inline="javascript">
var mainUrl = [[${@mainConfig.getMainUrl()}]];
document.addEventListener('DOMContentLoaded', () => {
(function ($) {
function setUpRegions(regions) {
$("#region > option").each((i, el) => {
$(el).remove();
});
_.forEach(regions, (region) => {
$("#region").append("<option value='" + region + "'>" + region + "</option>");
});
onRegionChanged();
}
function getRegion() {
return document.getElementById("region").value;
}
function onRegionChanged() {
var region = getRegion();
loadStatsForRegion(region);
}
function loadStatsForRegion(region) {
fetch(mainUrl + '/stats/all?region=' + region)
.then(value => {
return value.json()
})
.then(json => {
console.log(json);
showData(json);
});
}
function showData(json) {
var labels = _.map(json.progress, (progress) => progress.datetime.substr(0, 10));
var sick = _.map(json.progress, (progress) => progress.stats.length > 0 ? progress.stats[0].sick : 0);
var healed = _.map(json.progress, (progress) => progress.stats.length > 0 ? progress.stats[0].healed : 0);
var died = _.map(json.progress, (progress) => progress.stats.length > 0 ? progress.stats[0].died : 0);
var myChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Всего',
data: sick,
backgroundColor: 'red',
borderColor: 'red',
borderWidth: 1,
fill: false
}, {
label: 'Выздоровело',
data: healed,
backgroundColor: 'green',
borderColor: 'green',
borderWidth: 1,
fill: false
}, {
label: 'Умерло',
data: died,
backgroundColor: 'black',
borderColor: 'black',
borderWidth: 1,
fill: false
}]
},
options: {
responsive: true,
elements: {
line: {
tension: 0.000001
}
},
scales: {
xAxes: [{
display: true,
scaleLabel: {
display: true,
labelString: 'Дата'
}
}],
yAxes: [{
display: true,
scaleLabel: {
display: true,
labelString: 'Количество (чел.)'
},
ticks: {
beginAtZero: true
}
}]
}
}
});
myChart.canvas.parentNode.style.width = '1024px';
// myChart.canvas.parentNode.style.height = '1024px';
}
$("#region").change(() => onRegionChanged());
var ctx = document.getElementById('chart').getContext('2d');
fetch(mainUrl + '/regions')
.then(value => {
return value.json()
})
.then(json => setUpRegions(json));
})(jQuery);
});
</script>
<script th:src="@{/js/jquery-3.5.0.min.js}"></script>
<script th:src="@{/js/polyfill.min.js}"></script>
<script th:src="@{/js/fetch.min.js}"></script>
<script th:src="@{/js/chart.min.js}"></script>
<script th:src="@{/js/lodash.js}"></script>
</body>
</html>

View File

@ -1,4 +1,4 @@
package com.bvn13.covid19.api;
package com.bvn13.covid19.site;
import com.bvn13.covid19.model.Covid19ModelConfig;
import org.junit.jupiter.api.Test;
@ -14,7 +14,7 @@ import org.springframework.test.context.support.AnnotationConfigContextLoader;
classes = { Covid19ModelConfig.class },
loader = AnnotationConfigContextLoader.class)
@Profile("test")
class Covid19ApiApplicationTests {
class Covid19SiteApplicationTests {
@Test
void contextLoads() {

View File

@ -8,5 +8,5 @@ rootProject.name = 'covid19'
include ':covid19-model'
include ':covid19-db-migrator'
include ':covid19-api'
include ':covid19-site'
include ':covid19-scheduler'