GovernmentBodyChartDataManagerImpl.java

  1. /*
  2.  * Copyright 2010-2025 James Pether Sörling
  3.  *
  4.  * Licensed under the Apache License, Version 2.0 (the "License");
  5.  * you may not use this file except in compliance with the License.
  6.  * You may obtain a copy of the License at
  7.  *
  8.  *   http://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  * Unless required by applicable law or agreed to in writing, software
  11.  * distributed under the License is distributed on an "AS IS" BASIS,
  12.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  * See the License for the specific language governing permissions and
  14.  * limitations under the License.
  15.  *
  16.  *  $Id$
  17.  *  $HeadURL$
  18. */
  19. package com.hack23.cia.web.impl.ui.application.views.common.chartfactory.impl;

  20. import java.util.Collections;
  21. import java.util.Comparator;
  22. import java.util.List;
  23. import java.util.Locale;
  24. import java.util.Map;
  25. import java.util.Objects;
  26. import java.util.Optional;
  27. import java.util.stream.Collectors;

  28. import org.dussan.vaadin.dcharts.DCharts;
  29. import org.dussan.vaadin.dcharts.base.elements.XYseries;
  30. import org.dussan.vaadin.dcharts.data.DataSeries;
  31. import org.dussan.vaadin.dcharts.options.Series;
  32. import org.springframework.beans.factory.annotation.Autowired;
  33. import org.springframework.stereotype.Service;

  34. import com.hack23.cia.service.external.esv.api.EsvApi;
  35. import com.hack23.cia.service.external.esv.api.GovernmentBodyAnnualOutcomeSummary;
  36. import com.hack23.cia.service.external.esv.api.GovernmentBodyAnnualSummary;
  37. import com.hack23.cia.web.impl.ui.application.views.common.chartfactory.api.GovernmentBodyChartDataManager;
  38. import com.vaadin.ui.AbstractOrderedLayout;
  39. import com.vaadin.ui.VerticalLayout;

  40. /**
  41.  * The Class GovernmentBodyChartDataManagerImpl.
  42.  */
  43. @Service
  44. public final class GovernmentBodyChartDataManagerImpl extends AbstractChartDataManagerImpl
  45.         implements GovernmentBodyChartDataManager {

  46.     /** The Constant ALL_GOVERNMENT_BODIES. */
  47.     private static final String ALL_GOVERNMENT_BODIES = "All government bodies";

  48.     /** The Constant ANNUAL_EXPENDITURE. */
  49.     private static final String ANNUAL_EXPENDITURE = "Annual Expenditure";

  50.     /** The Constant ANNUAL_HEADCOUNT. */
  51.     private static final String ANNUAL_HEADCOUNT = "Annual headcount";

  52.     /** The Constant ANNUAL_HEADCOUNT_ALL_MINISTRIES. */
  53.     private static final String ANNUAL_HEADCOUNT_ALL_MINISTRIES = "Annual headcount, all ministries";

  54.     /** The Constant ANNUAL_HEADCOUNT_SUMMARY_ALL_GOVERNMENT_BODIES. */
  55.     private static final String ANNUAL_HEADCOUNT_SUMMARY_ALL_GOVERNMENT_BODIES =
  56.             "Annual headcount summary, all government bodies";

  57.     /** The Constant ANNUAL_HEADCOUNT_TOTAL_ALL_GOVERNMENT_BODIES. */
  58.     private static final String ANNUAL_HEADCOUNT_TOTAL_ALL_GOVERNMENT_BODIES =
  59.             "Annual headcount total all government bodies";

  60.     /** The Constant ANNUAL_INCOME. */
  61.     private static final String ANNUAL_INCOME = "Annual Income";

  62.     /** The Constant EXPENDITURE_GROUP_NAME. */
  63.     private static final String EXPENDITURE_GROUP_NAME = "Utgiftsområdesnamn";

  64.     /** The Constant INKOMSTTITELGRUPPSNAMN. */
  65.     private static final String INKOMSTTITELGRUPPSNAMN = "Inkomsttitelgruppsnamn";

  66.     /** The Constant INKOMSTTITELSNAMN. */
  67.     private static final String INKOMSTTITELSNAMN = "Inkomsttitelsnamn";

  68.     /** The Constant ANSLAGSPOSTSNAMN. */
  69.     private static final String ANSLAGSPOSTSNAMN = "Anslagspostsnamn";

  70.     /** The esv api. */
  71.     @Autowired
  72.     private EsvApi esvApi;


  73.     /**
  74.      * Adds a data point to the DataSeries if the year and value are valid and value > 0.
  75.      *
  76.      * @param dataSeries the data series
  77.      * @param year the year
  78.      * @param value the value
  79.      */
  80.     private static void addDataPoint(final DataSeries dataSeries, final Integer year, final Number value) {
  81.         if (dataSeries == null || year == null || value == null) {
  82.             return;
  83.         }
  84.         final double doubleValue = value.doubleValue();
  85.         if (doubleValue > 0) {
  86.             // Ensure consistent date format: " 01-JAN-YYYY"
  87.             final String formattedDate = String.format(Locale.ENGLISH," 01-JAN-%d", year);
  88.             dataSeries.add(formattedDate, doubleValue);
  89.         }
  90.     }

  91.     /**
  92.      * Creates a chart using the provided data/series objects, then appends it to the given layout.
  93.      *
  94.      * @param layout the layout
  95.      * @param label the label
  96.      * @param dataSeries the data series
  97.      * @param series the series
  98.      */
  99.     private void addChartToLayout(final AbstractOrderedLayout layout, final String label,
  100.                                   final DataSeries dataSeries, final Series series) {
  101.         Objects.requireNonNull(layout, "Layout cannot be null");
  102.         Objects.requireNonNull(label, "Label cannot be null");

  103.         final DCharts chart = new DCharts()
  104.                 .setDataSeries(dataSeries)
  105.                 .setOptions(getChartOptions().createOptionsXYDateFloatLogYAxisLegendOutside(series))
  106.                 .show();

  107.         ChartUtils.addChart(layout, label, chart, true);
  108.     }

  109.     /**
  110.      * Consolidates logic to retrieve, group, and process data by a descriptive field
  111.      * (e.g., 'Utgiftsområdesnamn' or 'Inkomsttitelgruppsnamn'), feeding the results into a DataSeries.
  112.      *
  113.      * @param dataSeries the data series
  114.      * @param series the series
  115.      * @param groupedData the grouped data
  116.      */
  117.     private void buildAnnualOutcomeDataSeriesByField(final DataSeries dataSeries, final Series series,
  118.             final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> groupedData) {
  119.         Optional.ofNullable(groupedData)
  120.             .ifPresent(data -> data.entrySet().stream()
  121.                 .filter(entry -> entry.getValue() != null && !entry.getValue().isEmpty())
  122.                 .forEach(entry -> {
  123.                     series.addSeries(new XYseries().setLabel(entry.getKey()));
  124.                     dataSeries.newSeries();

  125.                     entry.getValue().stream()
  126.                         .filter(Objects::nonNull)
  127.                         .forEach(summary -> {
  128.                             // Only process if we have a valid year and value map
  129.                             Optional.ofNullable(summary.getValueMap())
  130.                                 .ifPresent(valueMap -> {
  131.                                     // Get the year directly from the summary
  132.                                     final Integer year = summary.getYear();
  133.                                     // Get the total value for this year
  134.                                     final Double totalValue = valueMap.values().stream()
  135.                                         .filter(Objects::nonNull)
  136.                                         .mapToDouble(Number::doubleValue)
  137.                                         .sum();

  138.                                     // Only add if we have valid year and value
  139.                                     if (year != null && totalValue > 0) {
  140.                                         addDataPoint(dataSeries, year, totalValue);
  141.                                     }
  142.                                 });
  143.                         });
  144.                 }));
  145.     }

  146.     /**
  147.      * Helper method to generate a headcount data series given a map of year -> list of summaries.
  148.      *
  149.      * @param dataSeries the data series
  150.      * @param series the series
  151.      * @param yearlyData the yearly data
  152.      * @param label the label
  153.      */
  154.     private void buildHeadcountDataSeries(final DataSeries dataSeries, final Series series,
  155.                                           final Map<Integer, List<GovernmentBodyAnnualSummary>> yearlyData,
  156.                                           final String label) {
  157.         Optional.ofNullable(yearlyData)
  158.         .ifPresent(data -> {
  159.             series.addSeries(new XYseries().setLabel(label));
  160.             dataSeries.newSeries();

  161.             data.entrySet().stream()
  162.                 .filter(entry -> Objects.nonNull(entry.getValue()) && !entry.getValue().isEmpty())
  163.                 .forEach(entry -> {
  164.                     final int totalHeadcount = entry.getValue().stream()
  165.                         .filter(Objects::nonNull)
  166.                         .mapToInt(GovernmentBodyAnnualSummary::getHeadCount)
  167.                         .sum();
  168.                     addDataPoint(dataSeries, entry.getKey(), totalHeadcount);
  169.                 });
  170.         });
  171.     }

  172.     /**
  173.      * Consolidates logic for creating a chart that is grouped by some string field in descriptionFields.
  174.      *
  175.      * @param layout the layout
  176.      * @param field the field
  177.      * @param chartLabel the chart label
  178.      */
  179.     private void createMinistryFieldSummary(final AbstractOrderedLayout layout, final String field, final String chartLabel) {
  180.         Objects.requireNonNull(layout, "Layout cannot be null");
  181.         Objects.requireNonNull(field, "Field cannot be null");
  182.         Objects.requireNonNull(chartLabel, "Chart label cannot be null");

  183.         final DataSeries dataSeries = new DataSeries();
  184.         final Series series = new Series();

  185.         Optional.ofNullable(esvApi.getGovernmentBodyReportByMinistry())
  186.             .ifPresent(reportByMinistry ->
  187.                 reportByMinistry.entrySet().stream()
  188.                     .filter(entry -> entry.getValue() != null && !entry.getValue().isEmpty())
  189.                     .forEach(entry -> {
  190.                         final Map<Integer, Double> annualTotals = entry.getValue().stream()
  191.                             .filter(Objects::nonNull)
  192.                             .filter(summary -> Optional.ofNullable(summary.getDescriptionFields())
  193.                                 .map(fields -> fields.get(field))
  194.                                 .isPresent())
  195.                             .collect(Collectors.groupingBy(
  196.                                 GovernmentBodyAnnualOutcomeSummary::getYear,
  197.                                 Collectors.summingDouble(GovernmentBodyAnnualOutcomeSummary::getYearTotal)
  198.                             ));

  199.                         if (!annualTotals.isEmpty()) {
  200.                             series.addSeries(new XYseries().setLabel(entry.getKey()));
  201.                             dataSeries.newSeries();
  202.                             annualTotals.forEach((year, total) ->
  203.                                 addDataPoint(dataSeries, year + 1, total));
  204.                         }
  205.                     }));

  206.         addChartToLayout(layout, chartLabel, dataSeries, series);
  207.     }

  208.     // ==================== Public Chart Methods ====================

  209.     @Override
  210.     public void createGovernmentBodyExpenditureSummaryChart(final VerticalLayout layout) {
  211.         Objects.requireNonNull(layout, "Layout cannot be null");
  212.         final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> report =
  213.                 esvApi.getGovernmentBodyReportByField(EXPENDITURE_GROUP_NAME);

  214.         final DataSeries dataSeries = new DataSeries();
  215.         final Series series = new Series();
  216.         buildAnnualOutcomeDataSeriesByField(dataSeries, series, report);

  217.         addChartToLayout(layout, ANNUAL_EXPENDITURE, dataSeries, series);
  218.     }

  219.     @Override
  220.     public void createGovernmentBodyExpenditureSummaryChart(final VerticalLayout layout,
  221.                                                             final String governmentBodyName) {
  222.         Objects.requireNonNull(layout, "Layout cannot be null");
  223.         Objects.requireNonNull(governmentBodyName, "Government body name cannot be null");

  224.         final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> allSummaries = esvApi.getGovernmentBodyReport();

  225.         final List<GovernmentBodyAnnualOutcomeSummary> summariesForBody = Optional.ofNullable(allSummaries)
  226.                 .map(summaries -> summaries.get(governmentBodyName))
  227.                 .orElse(Collections.emptyList());

  228.         final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> grouped = summariesForBody.stream()
  229.                 .filter(Objects::nonNull)
  230.                 .filter(summary -> Optional.ofNullable(summary.getDescriptionFields())
  231.                     .map(fields -> fields.get(ANSLAGSPOSTSNAMN))
  232.                     .isPresent())
  233.                 .sorted(Comparator.comparing(GovernmentBodyAnnualOutcomeSummary::getYear))
  234.                 .collect(Collectors.groupingBy(
  235.                     summary -> summary.getDescriptionFields().get(ANSLAGSPOSTSNAMN)));

  236.         final DataSeries dataSeries = new DataSeries();
  237.         final Series series = new Series();
  238.         buildAnnualOutcomeDataSeriesByField(dataSeries, series, grouped);

  239.         addChartToLayout(layout, governmentBodyName + " " + ANNUAL_EXPENDITURE, dataSeries, series);
  240.     }

  241.     @Override
  242.     public void createGovernmentBodyHeadcountSummaryChart(final VerticalLayout layout) {
  243.         Objects.requireNonNull(layout, "Layout cannot be null");

  244.         final Map<Integer, List<GovernmentBodyAnnualSummary>> map = esvApi.getData();
  245.         final DataSeries dataSeries = new DataSeries();
  246.         final Series series = new Series();

  247.         buildHeadcountDataSeries(dataSeries, series, map, ALL_GOVERNMENT_BODIES);
  248.         addChartToLayout(layout, ANNUAL_HEADCOUNT_TOTAL_ALL_GOVERNMENT_BODIES, dataSeries, series);
  249.     }

  250.     @Override
  251.     public void createGovernmentBodyHeadcountSummaryChart(final VerticalLayout layout,
  252.                                                           final String governmentBodyName) {
  253.         Objects.requireNonNull(layout, "Layout cannot be null");
  254.         Objects.requireNonNull(governmentBodyName, "Government body name cannot be null");

  255.         final Map<Integer, GovernmentBodyAnnualSummary> map = esvApi.getDataPerGovernmentBody(governmentBodyName);

  256.         final DataSeries dataSeries = new DataSeries();
  257.         final Series series = new Series();
  258.         series.addSeries(new XYseries().setLabel(governmentBodyName));
  259.         dataSeries.newSeries();

  260.         Optional.ofNullable(map)
  261.         .ifPresent(yearSummaryMap -> yearSummaryMap.entrySet().stream()
  262.             .filter(entry -> Objects.nonNull(entry.getValue()))
  263.             .forEach(entry -> addDataPoint(dataSeries, entry.getKey(), entry.getValue().getHeadCount())));

  264.         addChartToLayout(layout, governmentBodyName + " " + ANNUAL_HEADCOUNT, dataSeries, series);
  265.     }

  266.     @Override
  267.     public void createGovernmentBodyIncomeSummaryChart(final VerticalLayout layout) {
  268.         Objects.requireNonNull(layout, "Layout cannot be null");

  269.         final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> report =
  270.                 esvApi.getGovernmentBodyReportByField(INKOMSTTITELGRUPPSNAMN);

  271.         final DataSeries dataSeries = new DataSeries();
  272.         final Series series = new Series();
  273.         buildAnnualOutcomeDataSeriesByField(dataSeries, series, report);

  274.         addChartToLayout(layout, ANNUAL_INCOME, dataSeries, series);
  275.     }

  276.     @Override
  277.     public void createGovernmentBodyIncomeSummaryChart(final VerticalLayout layout,
  278.                                                        final String governmentBodyName) {
  279.         Objects.requireNonNull(layout, "Layout cannot be null");
  280.         Objects.requireNonNull(governmentBodyName, "Government body name cannot be null");

  281.         final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> allSummaries = esvApi.getGovernmentBodyReport();

  282.         final List<GovernmentBodyAnnualOutcomeSummary> summariesForBody = Optional.ofNullable(allSummaries)
  283.                 .map(summaries -> summaries.get(governmentBodyName))
  284.                 .orElse(Collections.emptyList());

  285.         final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> grouped = summariesForBody.stream()
  286.                 .filter(Objects::nonNull)
  287.                 .filter(summary -> Optional.ofNullable(summary.getDescriptionFields())
  288.                     .map(fields -> fields.get(INKOMSTTITELSNAMN))
  289.                     .isPresent())
  290.                 .sorted(Comparator.comparing(GovernmentBodyAnnualOutcomeSummary::getYear))
  291.                 .collect(Collectors.groupingBy(
  292.                     summary -> summary.getDescriptionFields().get(INKOMSTTITELSNAMN)));

  293.         final DataSeries dataSeries = new DataSeries();
  294.         final Series series = new Series();
  295.         buildAnnualOutcomeDataSeriesByField(dataSeries, series, grouped);

  296.         addChartToLayout(layout, governmentBodyName + " " + ANNUAL_INCOME, dataSeries, series);
  297.     }

  298.     @Override
  299.     public void createMinistryGovernmentBodyExpenditureSummaryChart(final AbstractOrderedLayout layout) {
  300.         Objects.requireNonNull(layout, "Layout cannot be null");
  301.         createMinistryFieldSummary(layout, EXPENDITURE_GROUP_NAME, "MinistryGovernmentBodySpendingSummaryChart");
  302.     }

  303.     @Override
  304.     public void createMinistryGovernmentBodyExpenditureSummaryChart(final VerticalLayout layout,
  305.                                                                     final String governmentBodyName) {
  306.         Objects.requireNonNull(layout, "Layout cannot be null");
  307.         Objects.requireNonNull(governmentBodyName, "Government body name cannot be null");

  308.         final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> report =
  309.                 esvApi.getGovernmentBodyReportByFieldAndMinistry(EXPENDITURE_GROUP_NAME, governmentBodyName);

  310.         final DataSeries dataSeries = new DataSeries();
  311.         final Series series = new Series();
  312.         buildAnnualOutcomeDataSeriesByField(dataSeries, series, report);

  313.         addChartToLayout(layout, governmentBodyName + " " + ANNUAL_EXPENDITURE, dataSeries, series);
  314.     }

  315.     @Override
  316.     public void createMinistryGovernmentBodyHeadcountSummaryChart(final AbstractOrderedLayout layout) {
  317.         Objects.requireNonNull(layout, "Layout cannot be null");

  318.         final Map<Integer, List<GovernmentBodyAnnualSummary>> map = esvApi.getData();
  319.         final List<String> ministryNames = esvApi.getMinistryNames();

  320.         final DataSeries dataSeries = new DataSeries();
  321.         final Series series = new Series();

  322.         Optional.ofNullable(ministryNames)
  323.             .filter(names -> map != null)
  324.             .ifPresent(names -> names.stream()
  325.                 .forEach(ministryName -> {
  326.                     series.addSeries(new XYseries().setLabel(ministryName));
  327.                     dataSeries.newSeries();

  328.                     map.entrySet().stream()
  329.                         .filter(entry -> entry.getValue() != null)
  330.                         .forEach(entry -> {
  331.                             final int headcount = entry.getValue().stream()
  332.                                 .filter(Objects::nonNull)
  333.                                 .filter(summary -> summary.getMinistry() != null)
  334.                                 .filter(summary -> summary.getMinistry().equalsIgnoreCase(ministryName))
  335.                                 .mapToInt(GovernmentBodyAnnualSummary::getHeadCount)
  336.                                 .sum();
  337.                             addDataPoint(dataSeries, entry.getKey(), headcount);
  338.                         });
  339.                 }));

  340.         addChartToLayout(layout, ANNUAL_HEADCOUNT_ALL_MINISTRIES, dataSeries, series);
  341.     }

  342.     @Override
  343.     public void createMinistryGovernmentBodyHeadcountSummaryChart(final AbstractOrderedLayout layout,
  344.                                                                   final String governmentBodyName) {
  345.         Objects.requireNonNull(layout, "Layout cannot be null");
  346.         Objects.requireNonNull(governmentBodyName, "Government body name cannot be null");

  347.         final Map<Integer, List<GovernmentBodyAnnualSummary>> map = esvApi.getDataPerMinistry(governmentBodyName);
  348.         final List<String> governmentBodyNames = esvApi.getGovernmentBodyNames(governmentBodyName);

  349.         final DataSeries dataSeries = new DataSeries();
  350.         final Series series = new Series();

  351.         Optional.ofNullable(governmentBodyNames)
  352.             .filter(names -> map != null)
  353.             .ifPresent(names -> names.stream()
  354.                 .forEach(govBodyName -> {
  355.                     series.addSeries(new XYseries().setLabel(govBodyName));
  356.                     dataSeries.newSeries();

  357.                     map.entrySet().stream()
  358.                         .filter(entry -> entry.getValue() != null)
  359.                         .forEach(entry -> {
  360.                             final int headcount = entry.getValue().stream()
  361.                                 .filter(Objects::nonNull)
  362.                                 .filter(summary -> govBodyName.equalsIgnoreCase(summary.getName()))
  363.                                 .mapToInt(GovernmentBodyAnnualSummary::getHeadCount)
  364.                                 .sum();
  365.                             addDataPoint(dataSeries, entry.getKey(), headcount);
  366.                         });
  367.                 }));

  368.         if (map != null && governmentBodyNames != null) {
  369.             addChartToLayout(layout,
  370.                     governmentBodyName + " " + ANNUAL_HEADCOUNT_SUMMARY_ALL_GOVERNMENT_BODIES,
  371.                     dataSeries,
  372.                     series);
  373.         }
  374.     }

  375.     @Override
  376.     public void createMinistryGovernmentBodyIncomeSummaryChart(final AbstractOrderedLayout layout) {
  377.         Objects.requireNonNull(layout, "Layout cannot be null");
  378.         createMinistryFieldSummary(layout, INKOMSTTITELGRUPPSNAMN, "MinistryGovernmentBodyIncomeSummaryChart");
  379.     }

  380.     @Override
  381.     public void createMinistryGovernmentBodyIncomeSummaryChart(final VerticalLayout layout,
  382.                                                                final String governmentBodyName) {
  383.         Objects.requireNonNull(layout, "Layout cannot be null");
  384.         Objects.requireNonNull(governmentBodyName, "Government body name cannot be null");

  385.         final Map<String, List<GovernmentBodyAnnualOutcomeSummary>> report =
  386.                 esvApi.getGovernmentBodyReportByFieldAndMinistry(INKOMSTTITELGRUPPSNAMN, governmentBodyName);

  387.         final DataSeries dataSeries = new DataSeries();
  388.         final Series series = new Series();
  389.         buildAnnualOutcomeDataSeriesByField(dataSeries, series, report);

  390.         addChartToLayout(layout, governmentBodyName + " " + ANNUAL_INCOME, dataSeries, series);
  391.     }
  392. }