added previous stats data to api responses - per each stats data provided

develop
bvn13 2020-04-21 02:21:13 +03:00
parent c70993ebff
commit 3006a00ebb
13 changed files with 331 additions and 86 deletions

View File

@ -24,7 +24,7 @@ import java.time.LocalDateTime;
@Data @Data
@Entity @Entity
@Table(schema = "covid", name = "cvd_stats") @Table(schema = "covid", name = "cvd_stats")
public class CovidStat { public class CovidStat implements StatsProvider {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY, generator = "cvd_stats_seq") @GeneratedValue(strategy = GenerationType.IDENTITY, generator = "cvd_stats_seq")

View File

@ -0,0 +1,23 @@
/*
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.model.entities;
public interface StatsProvider {
long getSick();
long getHealed();
long getDied();
}

View File

@ -1,11 +1,30 @@
/*
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; package com.bvn13.covid19.site.controllers;
import com.bvn13.covid19.model.entities.CovidStat; 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.model.entities.StatsProvider;
import com.bvn13.covid19.site.model.CovidAllStats; 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.model.CovidDayStats;
import com.bvn13.covid19.site.service.CovidStatsMaker; import com.bvn13.covid19.site.repositories.RegionsRepository;
import com.bvn13.covid19.site.service.CovidStatsResponseMaker; import com.bvn13.covid19.site.service.StatisticsPreparator;
import com.bvn13.covid19.site.service.StatisticsAggregator;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -16,10 +35,7 @@ import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.ArrayList; import java.util.*;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
@ -27,8 +43,9 @@ import java.util.stream.Collectors;
@RequestMapping("/stats") @RequestMapping("/stats")
public class AllStatsController { public class AllStatsController {
private final CovidStatsMaker covidStatsMaker; private final StatisticsPreparator statisticsPreparator;
private final CovidStatsResponseMaker covidStatsResponseMaker; private final StatisticsAggregator statisticsAggregator;
private final RegionsRepository regionsRepository;
@Value("${app.zone-id}") @Value("${app.zone-id}")
private String zoneIdStr; private String zoneIdStr;
@ -51,45 +68,53 @@ public class AllStatsController {
} }
private CovidAllStats constructResponseForRegion(String regionName) { private CovidAllStats constructResponseForRegion(String regionName) {
return CovidAllStats.builder() return regionsRepository.findFirstByName(regionName)
.regions(Collections.singletonList(regionName)) .map(region -> CovidAllStats.builder()
.progress(covidStatsMaker.findAllLastUpdatesPerDay().stream() .regions(Collections.singletonList(region.getName()))
.map(covidUpdate -> CovidDayStats.builder() .progress(compoundProgressForRegions(Collections.singletonList(region)))
.datetime(covidUpdate.getDatetime()) .build()
.updatedOn(covidUpdate.getCreatedOn().atZone(zoneId)) )
.stats(convertStatsToData(findCovidStatsByUpdateIdAndRegion(covidUpdate.getId(), regionName))) .orElse(CovidAllStats.builder()
.build()) .regions(Collections.singletonList(regionName))
.sorted(CovidDayStats::compareTo) .progress(Collections.emptyList())
.collect(Collectors.toList())) .build()
.build(); );
} }
private CovidAllStats constructResponseForAllRegions() { private CovidAllStats constructResponseForAllRegions() {
return CovidAllStats.builder() return CovidAllStats.builder()
.regions(covidStatsMaker.findAllRegionsNames()) .regions(statisticsPreparator.findAllRegionsNames())
.progress(covidStatsMaker.findAllLastUpdatesPerDay().stream() .progress(compoundProgressForRegions(regionsRepository.findAll()))
.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(); .build();
} }
private List<CovidData> convertStatsToData(Collection<CovidStat> stats) { private List<CovidDayStats> compoundProgressForRegions(List<Region> regions) {
return covidStatsResponseMaker.convertStats(stats); return statisticsPreparator.findAllLastUpdatesPerDay().stream()
} .map(covidUpdate -> prepareDayStats(covidUpdate, regions))
.sorted(CovidDayStats::compareTo)
private List<CovidStat> findCovidStatsByUpdateIdAndRegion(long updateId, String region) {
return covidStatsMaker.findCovidStatsByUpdateInfoId(updateId).stream()
.filter(covidStat -> region.equals(covidStat.getRegion().getName()))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
private List<CovidStat> findCovidStatsByUpdateId(long updateId) { private CovidDayStats prepareDayStats(CovidUpdate currentUpdate, List<Region> regions) {
return new ArrayList<>(covidStatsMaker.findCovidStatsByUpdateInfoId(updateId)); List<CovidStat> currentStats = findCovidStatsByUpdateIdAndRegion(currentUpdate.getId(), regions);
Optional<CovidUpdate> prevUpdate = statisticsPreparator.findPrevUpdateByDate(currentUpdate.getDatetime());
Map<Region, StatsProvider> prevStats = prevUpdate
.map(covidUpdate -> statisticsPreparator.findCovidStatsByUpdateInfoId(covidUpdate.getId()).stream()
.collect(Collectors.toMap(CovidStat::getRegion, (cs) -> (StatsProvider) cs))
)
.orElseGet(() -> new HashMap<>(0));
return CovidDayStats.builder()
.datetime(currentUpdate.getDatetime())
.updatedOn(currentUpdate.getCreatedOn().atZone(zoneId))
.stats(statisticsAggregator.prepareStats(currentStats, prevStats))
.build();
}
private List<CovidStat> findCovidStatsByUpdateIdAndRegion(long updateId, List<Region> regions) {
return statisticsPreparator.findCovidStatsByUpdateInfoId(updateId).stream()
.filter(covidStat -> regions.contains(covidStat.getRegion()))
.collect(Collectors.toList());
} }
} }

View File

@ -16,9 +16,13 @@ limitations under the License.
package com.bvn13.covid19.site.controllers; package com.bvn13.covid19.site.controllers;
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.model.entities.StatsProvider;
import com.bvn13.covid19.site.model.CovidDayStats; import com.bvn13.covid19.site.model.CovidDayStats;
import com.bvn13.covid19.site.service.CovidStatsMaker; import com.bvn13.covid19.site.service.StatisticsPreparator;
import com.bvn13.covid19.site.service.CovidStatsResponseMaker; import com.bvn13.covid19.site.service.StatisticsAggregator;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@ -27,14 +31,19 @@ import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController @RestController
@RequestMapping("/stats") @RequestMapping("/stats")
public class LastStatsController { public class LastStatsController {
private final CovidStatsMaker covidStatsMaker; private final StatisticsPreparator statisticsPreparator;
private final CovidStatsResponseMaker covidStatsResponseMaker; private final StatisticsAggregator statisticsAggregator;
@Value("${app.zone-id}") @Value("${app.zone-id}")
private String zoneIdStr; private String zoneIdStr;
@ -47,13 +56,24 @@ public class LastStatsController {
@GetMapping("/last") @GetMapping("/last")
public CovidDayStats getStatistics() { public CovidDayStats getStatistics() {
return covidStatsMaker.findLastUpdate() Optional<CovidUpdate> lastUpdate = statisticsPreparator.findLastUpdate();
.map(covidUpdate -> CovidDayStats.builder() if (lastUpdate.isPresent()) {
.datetime(covidUpdate.getDatetime()) Collection<CovidStat> currentStats = statisticsPreparator.findCovidStatsByUpdateInfoId(lastUpdate.get().getId());
.updatedOn(covidUpdate.getCreatedOn().atZone(zoneId)) Optional<CovidUpdate> prevUpdate = statisticsPreparator.findPrevUpdateByDate(lastUpdate.get().getDatetime());
.stats(covidStatsResponseMaker.convertStats(covidStatsMaker.findCovidStatsByUpdateInfoId(covidUpdate.getId()))) Map<Region, StatsProvider> prevStats = prevUpdate
.build()) .map(covidUpdate -> statisticsPreparator.findCovidStatsByUpdateInfoId(covidUpdate.getId()).stream()
.orElse(CovidDayStats.builder().build()); .collect(Collectors.toMap(CovidStat::getRegion, (cs) -> (StatsProvider) cs))
)
.orElseGet(() -> new HashMap<>(0));
return CovidDayStats.builder()
.datetime(lastUpdate.get().getDatetime())
.updatedOn(lastUpdate.get().getCreatedOn().atZone(zoneId))
.stats(statisticsAggregator.prepareStats(currentStats, prevStats))
.build();
} else {
return CovidDayStats.builder().build();
}
} }
} }

View File

@ -1,6 +1,22 @@
/*
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; package com.bvn13.covid19.site.controllers;
import com.bvn13.covid19.site.service.CovidStatsMaker; import com.bvn13.covid19.site.service.StatisticsPreparator;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -13,11 +29,11 @@ import java.util.List;
@RequestMapping("/regions") @RequestMapping("/regions")
public class RegionsController { public class RegionsController {
private final CovidStatsMaker covidStatsMaker; private final StatisticsPreparator statisticsPreparator;
@GetMapping @GetMapping
public List<String> getAllRegions() { public List<String> getAllRegions() {
return covidStatsMaker.findAllRegionsNames(); return statisticsPreparator.findAllRegionsNames();
} }
} }

View File

@ -16,16 +16,27 @@ limitations under the License.
package com.bvn13.covid19.site.model; package com.bvn13.covid19.site.model;
import com.bvn13.covid19.model.entities.StatsProvider;
import lombok.Builder; import lombok.Builder;
import lombok.Value; import lombok.Value;
@Builder @Builder(toBuilder = true)
@Value @Value
public class CovidData { public class CovidData implements StatsProvider {
String region; String region;
long sick; long sick;
long healed; long healed;
long died; long died;
Delta previous;
@Builder
@Value
public static class Delta implements StatsProvider {
long sick;
long healed;
long died;
}
} }

View File

@ -20,9 +20,12 @@ import com.bvn13.covid19.site.dtos.CovidUpdateInfoDto;
import com.bvn13.covid19.model.entities.CovidUpdate; import com.bvn13.covid19.model.entities.CovidUpdate;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
@ -32,6 +35,9 @@ public interface CovidUpdatesRepository extends JpaRepository<CovidUpdate, Long>
@Query("select new com.bvn13.covid19.site.dtos.CovidUpdateInfoDto(max(U.createdOn)) from CovidUpdate U") @Query("select new com.bvn13.covid19.site.dtos.CovidUpdateInfoDto(max(U.createdOn)) from CovidUpdate U")
Optional<CovidUpdateInfoDto> findLastUpdate(); Optional<CovidUpdateInfoDto> findLastUpdate();
@Query("select U from CovidUpdate U where U.datetime >= :date1 and U.datetime < :date2")
Optional<CovidUpdate> findByDateOfUpdate(@Param("date1") ZonedDateTime date1, @Param("date2") ZonedDateTime date2);
@Query("select U from CovidUpdate U where U.id in (select max(U1.id) from CovidUpdate U1 group by U1.datetime)") @Query("select U from CovidUpdate U where U.id in (select max(U1.id) from CovidUpdate U1 group by U1.datetime)")
Collection<CovidUpdate> findAllLastUpdatesPerDay(); Collection<CovidUpdate> findAllLastUpdatesPerDay();

View File

@ -4,6 +4,9 @@ import com.bvn13.covid19.model.entities.Region;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository @Repository
public interface RegionsRepository extends JpaRepository<Region, Long> { public interface RegionsRepository extends JpaRepository<Region, Long> {
Optional<Region> findFirstByName(String name);
} }

View File

@ -1,25 +0,0 @@
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

@ -0,0 +1,63 @@
/*
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.service;
import com.bvn13.covid19.model.entities.CovidStat;
import com.bvn13.covid19.model.entities.Region;
import com.bvn13.covid19.model.entities.StatsProvider;
import com.bvn13.covid19.site.model.CovidData;
import lombok.RequiredArgsConstructor;
import org.springframework.data.util.Pair;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Component
public class StatisticsAggregator {
private static final StatsProvider zero = CovidData.Delta.builder()
.sick(0L)
.healed(0L)
.died(0L)
.build();
public List<CovidData> prepareStats(Collection<CovidStat> current, Map<Region, StatsProvider> previous) {
return current.stream()
.map(stat -> Pair.of(stat, Optional.ofNullable(previous.get(stat.getRegion()))))
.map(statPair -> CovidData.builder()
.region(statPair.getFirst().getRegion().getName())
.sick(statPair.getFirst().getSick())
.healed(statPair.getFirst().getHealed())
.died(statPair.getFirst().getDied())
.previous(CovidData.Delta.builder()
.sick(statPair.getSecond().orElse(zero).getSick())
.healed(statPair.getSecond().orElse(zero).getHealed())
.died(statPair.getSecond().orElse(zero).getDied())
.build()
)
.build()
)
.collect(Collectors.toList());
}
}

View File

@ -23,10 +23,17 @@ import com.bvn13.covid19.site.repositories.CovidStatsRepository;
import com.bvn13.covid19.site.repositories.CovidUpdatesRepository; import com.bvn13.covid19.site.repositories.CovidUpdatesRepository;
import com.bvn13.covid19.site.repositories.RegionsRepository; import com.bvn13.covid19.site.repositories.RegionsRepository;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.transaction.Transactional; import javax.transaction.Transactional;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -35,12 +42,30 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
@Component @Component
public class CovidStatsMaker { public class StatisticsPreparator {
private final CovidStatsRepository statsRepository; private final CovidStatsRepository statsRepository;
private final CovidUpdatesRepository updatesRepository; private final CovidUpdatesRepository updatesRepository;
private final RegionsRepository regionsRepository; private final RegionsRepository regionsRepository;
private LocalDate projectStartDate;
private ZonedDateTime projectStartZonedDate;
@Value("${app.zone-id}")
private String zoneIdStr;
@PostConstruct
public void init() {
ZoneId zoneId = ZoneId.of(zoneIdStr);
projectStartZonedDate = projectStartDate.atTime(0, 0, 0).atZone(zoneId);
}
@Value("${app.project-start-date}")
private void setProjectStartDate(String ld) {
if (ld != null && !ld.isEmpty()) {
projectStartDate = LocalDate.parse(ld);
}
}
@Transactional @Transactional
public Collection<CovidStat> getLastCovidStats() { public Collection<CovidStat> getLastCovidStats() {
return updatesRepository.findLastUpdate() return updatesRepository.findLastUpdate()
@ -59,6 +84,26 @@ public class CovidStatsMaker {
.flatMap(updateInfo -> updatesRepository.findFirstByCreatedOn(updateInfo.getCreatedOn())); .flatMap(updateInfo -> updatesRepository.findFirstByCreatedOn(updateInfo.getCreatedOn()));
} }
@Cacheable(
cacheNames = "covid-prev-update-by-date",
unless = "#result == null"
)
public Optional<CovidUpdate> findPrevUpdateByDate(ZonedDateTime date) {
if (date.isBefore(projectStartZonedDate)) {
return Optional.empty();
} else {
Optional<CovidUpdate> update = updatesRepository.findByDateOfUpdate(
dateWithTime(prevDate(date), 0, 0, 0),
dateWithTime(date, 0, 0, 0)
);
if (update.isPresent()) {
return update;
} else {
return findPrevUpdateByDate(prevDate(date));
}
}
}
@Cacheable( @Cacheable(
cacheNames = "covid-stats-by-update-info-id", cacheNames = "covid-stats-by-update-info-id",
condition = "#updateInfoId > 0", condition = "#updateInfoId > 0",
@ -84,4 +129,15 @@ public class CovidStatsMaker {
return regionsRepository.findAll().stream().map(Region::getName).collect(Collectors.toList()); return regionsRepository.findAll().stream().map(Region::getName).collect(Collectors.toList());
} }
private ZonedDateTime prevDate(ZonedDateTime date) {
return date.minus(1, ChronoUnit.DAYS);
}
private ZonedDateTime dateWithTime(ZonedDateTime date, int hour, int minute, int second) {
return date
.withHour(hour)
.withMinute(minute)
.withSecond(second);
}
} }

View File

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

View File

@ -2,7 +2,7 @@
<html xmlns:th="http://www.thymeleaf.org"> <html xmlns:th="http://www.thymeleaf.org">
<head> <head>
<title>Home page</title> <title>Статистика COVID19 в России</title>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
@ -28,6 +28,13 @@
<canvas id="chart"></canvas> <canvas id="chart"></canvas>
</div> </div>
<p>
<a href="https://github.com/bvn13/covid19-ru">Исходный код</a>
</p>
<p>
<a href="https://twitter.com/bvn13">@bvn13 - Автор проекта</a>
</p>
<script th:inline="javascript"> <script th:inline="javascript">
var mainUrl = [[${@mainConfig.getMainUrl()}]]; var mainUrl = [[${@mainConfig.getMainUrl()}]];
@ -68,34 +75,73 @@
function showData(json) { function showData(json) {
var labels = _.map(json.progress, (progress) => progress.datetime.substr(0, 10)); 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 sick = _.map(json.progress, (progress) => progress.stats.length > 0 ? progress.stats[0].sick : 0);
var sickDeltas = _.map(json.progress, (progress) =>
progress.stats.length > 0
? progress.stats[0].sick - progress.stats[0].previous.sick
: 0
);
var healed = _.map(json.progress, (progress) => progress.stats.length > 0 ? progress.stats[0].healed : 0); var healed = _.map(json.progress, (progress) => progress.stats.length > 0 ? progress.stats[0].healed : 0);
var healedDeltas = _.map(json.progress, (progress) =>
progress.stats.length > 0
? progress.stats[0].healed - progress.stats[0].previous.healed
: 0
);
var died = _.map(json.progress, (progress) => progress.stats.length > 0 ? progress.stats[0].died : 0); var died = _.map(json.progress, (progress) => progress.stats.length > 0 ? progress.stats[0].died : 0);
var diedDeltas = _.map(json.progress, (progress) =>
progress.stats.length > 0
? progress.stats[0].died - progress.stats[0].previous.died
: 0
);
var myChart = new Chart(ctx, { var myChart = new Chart(ctx, {
type: 'line', type: 'line',
data: { data: {
labels: labels, labels: labels,
datasets: [{ datasets: [{
label: 'Всего', label: 'Всего (чел.)',
data: sick, data: sick,
backgroundColor: 'red', backgroundColor: 'red',
borderColor: 'red', borderColor: 'red',
borderWidth: 1, borderWidth: 1,
fill: false fill: false
}, { }, {
label: 'Выздоровело', label: 'Выздоровело (чел.)',
data: healed, data: healed,
backgroundColor: 'green', backgroundColor: 'green',
borderColor: 'green', borderColor: 'green',
borderWidth: 1, borderWidth: 1,
fill: false fill: false
}, { }, {
label: 'Умерло', label: 'Умерло (чел.)',
data: died, data: died,
backgroundColor: 'black', backgroundColor: 'black',
borderColor: 'black', borderColor: 'black',
borderWidth: 1, borderWidth: 1,
fill: false fill: false
}, {
label: 'Всего (дельта чел.)',
data: sickDeltas,
backgroundColor: 'red',
borderColor: 'red',
borderWidth: 1,
borderDash: [5, 15],
fill: false
}, {
label: 'Выздоровело (дельта чел.)',
data: healedDeltas,
backgroundColor: 'green',
borderColor: 'green',
borderWidth: 1,
borderDash: [5, 15],
fill: false
}, {
label: 'Умерло (дельта чел.)',
data: diedDeltas,
backgroundColor: 'black',
borderColor: 'black',
borderWidth: 1,
borderDash: [5, 15],
fill: false
}] }]
}, },
options: { options: {