de.symeda.sormas.ui.statistics.StatisticsView.java Source code

Java tutorial

Introduction

Here is the source code for de.symeda.sormas.ui.statistics.StatisticsView.java

Source

/*******************************************************************************
 * SORMAS - Surveillance Outbreak Response Management & Analysis System
 * Copyright  2016-2018 Helmholtz-Zentrum fr Infektionsforschung GmbH (HZI)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *******************************************************************************/
package de.symeda.sormas.ui.statistics;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TreeMap;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.text.StringEscapeUtils;

import com.vaadin.icons.VaadinIcons;
import com.vaadin.server.FileDownloader;
import com.vaadin.server.Page;
import com.vaadin.server.StreamResource;
import com.vaadin.shared.ui.ContentMode;
import com.vaadin.ui.AbstractOrderedLayout;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.Notification;
import com.vaadin.ui.Notification.Type;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.themes.ValoTheme;
import com.vaadin.v7.ui.CheckBox;

import de.symeda.sormas.api.CaseMeasure;
import de.symeda.sormas.api.Disease;
import de.symeda.sormas.api.FacadeProvider;
import de.symeda.sormas.api.IntegerRange;
import de.symeda.sormas.api.Month;
import de.symeda.sormas.api.MonthOfYear;
import de.symeda.sormas.api.Quarter;
import de.symeda.sormas.api.QuarterOfYear;
import de.symeda.sormas.api.ReferenceDto;
import de.symeda.sormas.api.Year;
import de.symeda.sormas.api.caze.CaseClassification;
import de.symeda.sormas.api.caze.CaseOutcome;
import de.symeda.sormas.api.i18n.Captions;
import de.symeda.sormas.api.i18n.Descriptions;
import de.symeda.sormas.api.i18n.I18nProperties;
import de.symeda.sormas.api.i18n.Strings;
import de.symeda.sormas.api.person.Sex;
import de.symeda.sormas.api.region.DistrictReferenceDto;
import de.symeda.sormas.api.region.GeoLatLon;
import de.symeda.sormas.api.region.RegionReferenceDto;
import de.symeda.sormas.api.statistics.StatisticsCaseAttribute;
import de.symeda.sormas.api.statistics.StatisticsCaseCriteria;
import de.symeda.sormas.api.statistics.StatisticsCaseSubAttribute;
import de.symeda.sormas.api.statistics.StatisticsGroupingKey;
import de.symeda.sormas.api.statistics.StatisticsHelper;
import de.symeda.sormas.api.statistics.StatisticsHelper.StatisticsKeyComparator;
import de.symeda.sormas.api.utils.DataHelper;
import de.symeda.sormas.api.utils.DateHelper;
import de.symeda.sormas.api.utils.EpiWeek;
import de.symeda.sormas.ui.dashboard.map.DashboardMapComponent;
import de.symeda.sormas.ui.highcharts.HighChart;
import de.symeda.sormas.ui.map.LeafletMap;
import de.symeda.sormas.ui.map.LeafletPolygon;
import de.symeda.sormas.ui.statistics.StatisticsFilterElement.TokenizableValue;
import de.symeda.sormas.ui.statistics.StatisticsVisualizationType.StatisticsVisualizationChartType;
import de.symeda.sormas.ui.utils.CssStyles;
import de.symeda.sormas.ui.utils.DownloadUtil;

public class StatisticsView extends AbstractStatisticsView {

    private static final long serialVersionUID = -4440568319850399685L;

    public static final String VIEW_NAME = ROOT_VIEW_NAME;

    private VerticalLayout filtersLayout;
    private VerticalLayout resultsLayout;
    private CheckBox zeroValues;
    private Button exportButton;
    private final Label emptyResultLabel;
    private StatisticsCaseGrid statisticsCaseGrid;
    private StatisticsVisualizationComponent visualizationComponent;
    private List<StatisticsFilterComponent> filterComponents = new ArrayList<>();
    private StatisticsCaseCriteria caseCriteria;

    public StatisticsView() {
        super(VIEW_NAME);
        setWidth(100, Unit.PERCENTAGE);

        emptyResultLabel = new Label(I18nProperties.getString(Strings.infoNoCasesFoundStatistics));

        // Main layout
        VerticalLayout statisticsLayout = new VerticalLayout();
        statisticsLayout.setMargin(true);
        statisticsLayout.setSpacing(true);
        statisticsLayout.setWidth(100, Unit.PERCENTAGE);

        // Filters layout
        addFiltersLayout(statisticsLayout);

        // Visualization layout
        Label visualizationTitle = new Label(I18nProperties.getString(Strings.headingVisualization));
        visualizationTitle.setWidthUndefined();
        CssStyles.style(visualizationTitle, CssStyles.STATISTICS_TITLE);
        statisticsLayout.addComponent(visualizationTitle);

        visualizationComponent = new StatisticsVisualizationComponent();
        CssStyles.style(visualizationComponent, CssStyles.STATISTICS_TITLE_BOX);
        statisticsLayout.addComponent(visualizationComponent);

        // Options layout
        addOptionsLayout(statisticsLayout);

        // Generate button
        addGenerateButton(statisticsLayout);

        // Results layout
        addResultsLayout(statisticsLayout);

        // Disclaimer
        Label disclaimer = new Label(VaadinIcons.INFO_CIRCLE.getHtml() + " "
                + I18nProperties.getString(Strings.infoStatisticsDisclaimer), ContentMode.HTML);
        statisticsLayout.addComponent(disclaimer);

        addComponent(statisticsLayout);
    }

    private void addFiltersLayout(VerticalLayout statisticsLayout) {
        Label filtersLayoutTitle = new Label(I18nProperties.getString(Strings.headingFilters));
        filtersLayoutTitle.setWidthUndefined();
        CssStyles.style(filtersLayoutTitle, CssStyles.STATISTICS_TITLE);
        statisticsLayout.addComponent(filtersLayoutTitle);

        VerticalLayout filtersSectionLayout = new VerticalLayout();
        CssStyles.style(filtersSectionLayout, CssStyles.STATISTICS_TITLE_BOX);
        filtersSectionLayout.setSpacing(true);
        filtersSectionLayout.setWidth(100, Unit.PERCENTAGE);
        Label filtersInfoText = new Label(I18nProperties.getString(Strings.infoStatisticsFilter), ContentMode.HTML);
        filtersSectionLayout.addComponent(filtersInfoText);

        filtersLayout = new VerticalLayout();
        filtersLayout.setSpacing(true);
        filtersLayout.setMargin(false);
        filtersSectionLayout.addComponent(filtersLayout);

        // Filters footer
        HorizontalLayout filtersSectionFooter = new HorizontalLayout();
        {
            filtersSectionFooter.setSpacing(true);

            Button addFilterButton = new Button(I18nProperties.getCaption(Captions.statisticsAddFilter),
                    VaadinIcons.PLUS);
            CssStyles.style(addFilterButton, ValoTheme.BUTTON_PRIMARY);
            addFilterButton.addClickListener(e -> {
                filtersLayout.addComponent(createFilterComponentLayout());
            });
            filtersSectionFooter.addComponent(addFilterButton);

            Button resetFiltersButton = new Button(I18nProperties.getCaption(Captions.statisticsResetFilters));
            resetFiltersButton.addClickListener(e -> {
                filtersLayout.removeAllComponents();
                filterComponents.clear();
            });
            filtersSectionFooter.addComponent(resetFiltersButton);
        }
        filtersSectionLayout.addComponent(filtersSectionFooter);

        statisticsLayout.addComponent(filtersSectionLayout);
    }

    private HorizontalLayout createFilterComponentLayout() {
        HorizontalLayout filterComponentLayout = new HorizontalLayout();
        filterComponentLayout.setSpacing(true);
        filterComponentLayout.setWidth(100, Unit.PERCENTAGE);

        StatisticsFilterComponent filterComponent = new StatisticsFilterComponent();

        Button removeFilterButton = new Button(VaadinIcons.CLOSE);
        removeFilterButton.setDescription(I18nProperties.getCaption(Captions.statisticsRemoveFilter));
        CssStyles.style(removeFilterButton, CssStyles.FORCE_CAPTION);
        removeFilterButton.addClickListener(e -> {
            filterComponents.remove(filterComponent);
            filtersLayout.removeComponent(filterComponentLayout);
        });

        filterComponentLayout.addComponent(removeFilterButton);
        filterComponents.add(filterComponent);
        filterComponentLayout.addComponent(filterComponent);
        filterComponentLayout.setExpandRatio(filterComponent, 1);

        return filterComponentLayout;
    }

    private void addResultsLayout(VerticalLayout statisticsLayout) {
        Label resultsLayoutTitle = new Label(I18nProperties.getString(Strings.headingResults));
        resultsLayoutTitle.setWidthUndefined();
        CssStyles.style(resultsLayoutTitle, CssStyles.STATISTICS_TITLE);
        statisticsLayout.addComponent(resultsLayoutTitle);

        resultsLayout = new VerticalLayout();
        resultsLayout.setWidth(100, Unit.PERCENTAGE);
        resultsLayout.setSpacing(true);
        CssStyles.style(resultsLayout, CssStyles.STATISTICS_TITLE_BOX);
        resultsLayout.addComponent(new Label(I18nProperties.getString(Strings.infoStatisticsResults)));

        statisticsLayout.addComponent(resultsLayout);
    }

    private void addOptionsLayout(VerticalLayout statisticsLayout) {
        Label optionsTitle = new Label(I18nProperties.getCaption(Captions.options));
        optionsTitle.setWidthUndefined();
        CssStyles.style(optionsTitle, CssStyles.STATISTICS_TITLE);
        statisticsLayout.addComponent(optionsTitle);

        HorizontalLayout optionsLayout = new HorizontalLayout();
        optionsLayout.setWidth(100, Unit.PERCENTAGE);
        optionsLayout.setSpacing(true);
        CssStyles.style(optionsLayout, CssStyles.STATISTICS_TITLE_BOX);
        {
            zeroValues = new CheckBox(I18nProperties.getCaption(Captions.statisticsShowZeroValues));
            zeroValues.setValue(false);
            optionsLayout.addComponent(zeroValues);
        }
        statisticsLayout.addComponent(optionsLayout);
    }

    private void addGenerateButton(VerticalLayout statisticsLayout) {
        Button generateButton = new Button(I18nProperties.getCaption(Captions.actionGenerate));
        CssStyles.style(generateButton, ValoTheme.BUTTON_PRIMARY);
        generateButton.addClickListener(e -> {
            // Check whether there is any invalid empty filter or grouping data
            Notification errorNotification = null;
            for (StatisticsFilterComponent filterComponent : filterComponents) {
                if (filterComponent.getSelectedAttribute() != StatisticsCaseAttribute.REGION_DISTRICT
                        && (filterComponent.getSelectedAttribute() == null
                                || filterComponent.getSelectedAttribute().getSubAttributes().length > 0
                                        && filterComponent.getSelectedSubAttribute() == null)) {
                    errorNotification = new Notification(
                            I18nProperties.getString(Strings.messageSpecifyFilterAttributes), Type.WARNING_MESSAGE);
                    break;
                }
            }

            if (errorNotification == null && visualizationComponent.getRowsAttribute() != null
                    && visualizationComponent.getRowsAttribute().getSubAttributes().length > 0
                    && visualizationComponent.getRowsSubAttribute() == null) {
                errorNotification = new Notification(I18nProperties.getString(Strings.messageSpecifyRowAttribute),
                        Type.WARNING_MESSAGE);
            } else if (errorNotification == null && visualizationComponent.getColumnsAttribute() != null
                    && visualizationComponent.getColumnsAttribute().getSubAttributes().length > 0
                    && visualizationComponent.getColumnsSubAttribute() == null) {
                errorNotification = new Notification(
                        I18nProperties.getString(Strings.messageSpecifyColumnAttribute), Type.WARNING_MESSAGE);
            }

            if (errorNotification != null) {
                errorNotification.setDelayMsec(-1);
                errorNotification.show(Page.getCurrent());
            } else {
                resultsLayout.removeAllComponents();
                switch (visualizationComponent.getVisualizationType()) {
                case TABLE:
                    generateTable();
                    break;
                case MAP:
                    generateMap();
                    break;
                default:
                    generateChart();
                    break;
                }
            }
        });

        statisticsLayout.addComponent(generateButton);
    }

    public void generateTable() {
        List<Object[]> resultData = generateStatistics();

        if (resultData.isEmpty()) {
            resultsLayout.addComponent(emptyResultLabel);
            return;
        }

        exportButton = new Button(I18nProperties.getCaption(Captions.export));
        exportButton.setDescription(I18nProperties.getDescription(Descriptions.descExportButton));
        exportButton.addStyleName(ValoTheme.BUTTON_PRIMARY);
        exportButton.setIcon(VaadinIcons.TABLE);
        resultsLayout.addComponent(exportButton);
        resultsLayout.setComponentAlignment(exportButton, Alignment.TOP_RIGHT);

        statisticsCaseGrid = new StatisticsCaseGrid(visualizationComponent.getRowsAttribute(),
                visualizationComponent.getRowsSubAttribute(), visualizationComponent.getColumnsAttribute(),
                visualizationComponent.getColumnsSubAttribute(), zeroValues.getValue(), resultData, caseCriteria);
        resultsLayout.addComponent(statisticsCaseGrid);
        resultsLayout.setExpandRatio(statisticsCaseGrid, 1);

        StreamResource streamResource = DownloadUtil.createGridExportStreamResource(
                statisticsCaseGrid.getContainerDataSource(), statisticsCaseGrid.getColumns(), "sormas_statistics",
                "sormas_statistics_" + DateHelper.formatDateForExport(new Date()) + ".csv");
        FileDownloader fileDownloader = new FileDownloader(streamResource);
        fileDownloader.extend(exportButton);
    }

    @SuppressWarnings("unchecked")
    public void generateChart() {
        List<Object[]> resultData = generateStatistics();

        if (resultData.isEmpty()) {
            resultsLayout.addComponent(emptyResultLabel);
            return;
        }

        StatisticsVisualizationChartType chartType = visualizationComponent.getVisualizationChartType();
        StatisticsCaseAttribute xAxisAttribute = visualizationComponent.getColumnsAttribute();
        StatisticsCaseSubAttribute xAxisSubAttribute = visualizationComponent.getColumnsSubAttribute();
        StatisticsCaseAttribute seriesAttribute = visualizationComponent.getRowsAttribute();

        HighChart chart = new HighChart();
        chart.setWidth(100, Unit.PERCENTAGE);
        chart.setHeight(580, Unit.PIXELS);

        StringBuilder hcjs = new StringBuilder();
        hcjs.append("var options = {");

        final int xAxisIdIndex;
        final int seriesIdIndex;
        hcjs.append("chart:{ " + " ignoreHiddenSeries: false, " + " type: '");
        switch (chartType) {
        case COLUMN:
        case STACKED_COLUMN:
            hcjs.append("column");
            seriesIdIndex = seriesAttribute != null ? 1 : -1;
            xAxisIdIndex = xAxisAttribute != null ? (seriesIdIndex >= 1 ? 2 : 1) : -1;
            break;
        case LINE:
            hcjs.append("line");
            seriesIdIndex = seriesAttribute != null ? 1 : -1;
            xAxisIdIndex = xAxisAttribute != null ? (seriesIdIndex >= 1 ? 2 : 1) : -1;
            break;
        case PIE:
            hcjs.append("pie");
            xAxisIdIndex = -1;
            seriesIdIndex = seriesAttribute != null ? 1 : -1;
            break;
        default:
            throw new IllegalArgumentException(chartType.toString());
        }

        hcjs.append("', " + " backgroundColor: 'transparent' " + "}," + "credits:{ enabled: false },"
                + "exporting:{ " + " enabled: true,"
                + " buttons:{ contextButton:{ theme:{ fill: 'transparent' } } }" + "}," + "title:{ text: '' },");

        TreeMap<StatisticsGroupingKey, String> xAxisCaptions = new TreeMap<>(new StatisticsKeyComparator());
        TreeMap<StatisticsGroupingKey, String> seriesCaptions = new TreeMap<>(new StatisticsKeyComparator());
        boolean appendUnknownXAxisCaption = false;
        if (xAxisIdIndex >= 1 || seriesIdIndex >= 1) {
            // Build captions for x-axis and/or series
            for (Object[] row : resultData) {
                if (xAxisIdIndex >= 1) {
                    if (!StatisticsHelper.isNullOrUnknown(row[xAxisIdIndex])) {
                        xAxisCaptions.putIfAbsent((StatisticsGroupingKey) row[xAxisIdIndex],
                                row[xAxisIdIndex].toString());
                    } else {
                        appendUnknownXAxisCaption = true;
                    }
                }
                if (seriesIdIndex >= 1) {
                    if (!StatisticsHelper.isNullOrUnknown(row[seriesIdIndex])) {
                        seriesCaptions.putIfAbsent((StatisticsGroupingKey) row[seriesIdIndex],
                                row[seriesIdIndex].toString());
                    }
                }
            }
        }

        if (chartType != StatisticsVisualizationChartType.PIE) {
            // If zero values are ticked, add missing captions to the list; this involves
            // every possible value of the chosen attribute unless a filter has been
            // set for the same attribute; in this case, only values that are part of the
            // filter are chosen
            if (zeroValues.getValue() == true && xAxisAttribute != null) {
                List<Object> values = StatisticsHelper.getAllAttributeValues(xAxisAttribute, xAxisSubAttribute);
                List<StatisticsGroupingKey> filterValues = (List<StatisticsGroupingKey>) caseCriteria
                        .getFilterValuesForGrouping(xAxisAttribute, xAxisSubAttribute);
                for (Object value : values) {
                    Object formattedValue = StatisticsHelper.buildGroupingKey(value, xAxisAttribute,
                            xAxisSubAttribute);
                    if (formattedValue != null
                            && (CollectionUtils.isEmpty(filterValues) || filterValues.contains(formattedValue))) {
                        xAxisCaptions.putIfAbsent((StatisticsGroupingKey) formattedValue,
                                formattedValue.toString());
                    }
                }
            }

            hcjs.append("xAxis: { categories: [");
            if (xAxisIdIndex >= 1) {
                xAxisCaptions.forEach((key, value) -> {
                    hcjs.append("'").append(xAxisCaptions.get(key)).append("',");
                });

                if (appendUnknownXAxisCaption) {
                    hcjs.append("'").append(getEscapedFragment(StatisticsHelper.UNKNOWN)).append("'");
                }
            } else {
                hcjs.append("'").append(getEscapedFragment(StatisticsHelper.TOTAL)).append("'");
            }
            int numberOfCategories = xAxisIdIndex >= 1
                    ? appendUnknownXAxisCaption ? xAxisCaptions.size() + 1 : xAxisCaptions.size()
                    : 1;
            hcjs.append("], min: 0, max: " + (numberOfCategories - 1) + "},");

            hcjs.append("yAxis: { min: 0, title: { text: '").append(getEscapedFragment(StatisticsHelper.CASE_COUNT))
                    .append("' },").append("allowDecimals: false, softMax: 10, " + "stackLabels: { enabled: true, "
                            + "style: {fontWeight: 'normal', textOutline: '0', gridLineColor: '#000000', color: (Highcharts.theme && Highcharts.theme.textColor) || 'gray' } } },");

            hcjs.append(
                    "tooltip: { headerFormat: '<b>{point.x}</b><br/>', pointFormat: '{series.name}: {point.y}<br/>"
                            + I18nProperties.getCaption(Captions.total) + ": {point.stackTotal}'},");
        }

        hcjs.append("legend: { verticalAlign: 'top', backgroundColor: 'transparent', align: 'left', "
                + "borderWidth: 0, shadow: false, margin: 30, padding: 0 },");

        hcjs.append(
                "colors: ['#FF0000','#6691C4','#ffba08','#519e8a','#ed254e','#39a0ed','#FF8C00','#344055','#D36135','#82d173'],");

        if (chartType == StatisticsVisualizationChartType.STACKED_COLUMN
                || chartType == StatisticsVisualizationChartType.COLUMN) {
            hcjs.append("plotOptions: { column: { borderWidth: 0, ");
            if (chartType == StatisticsVisualizationChartType.STACKED_COLUMN) {
                hcjs.append("stacking: 'normal', ");
            }
            hcjs.append("groupPadding: 0.05, pointPadding: 0, " + "dataLabels: {" + "enabled: true,"
                    + "formatter: function() { if (this.y > 0) return this.y; }," + "color: '#444',"
                    + "backgroundColor: 'rgba(255, 255, 255, 0.75)'," + "borderRadius: 3," + "padding: 3,"
                    + "style:{textOutline:'none'}" + "} } },");
        }

        hcjs.append("series: [");
        if (seriesIdIndex < 1 && xAxisIdIndex < 1) {
            hcjs.append("{ name: '").append(getEscapedFragment(StatisticsHelper.CASE_COUNT))
                    .append("', dataLabels: { allowOverlap: false }").append(", data: [['")
                    .append(getEscapedFragment(StatisticsHelper.CASE_COUNT)).append("',")
                    .append(resultData.get(0)[0].toString()).append("]]}");
        } else if (visualizationComponent.getVisualizationChartType() == StatisticsVisualizationChartType.PIE) {
            hcjs.append("{ name: '").append(getEscapedFragment(StatisticsHelper.CASE_COUNT))
                    .append("', dataLabels: { allowOverlap: false }").append(", data: [");
            TreeMap<StatisticsGroupingKey, Object[]> seriesElements = new TreeMap<>(new StatisticsKeyComparator());
            Object[] unknownSeriesElement = null;
            for (Object[] row : resultData) {
                Object seriesId = row[seriesIdIndex];
                if (StatisticsHelper.isNullOrUnknown(seriesId)) {
                    unknownSeriesElement = row;
                } else {
                    seriesElements.put((StatisticsGroupingKey) seriesId, row);
                }
            }

            seriesElements.forEach((key, value) -> {
                Object seriesValue = value[0];
                Object seriesId = value[seriesIdIndex];
                hcjs.append("['").append(seriesCaptions.get(seriesId)).append("',").append(seriesValue)
                        .append("],");
            });
            if (unknownSeriesElement != null) {
                Object seriesValue = unknownSeriesElement[0];
                hcjs.append("['").append(getEscapedFragment(StatisticsHelper.CASE_COUNT)).append("',")
                        .append(seriesValue).append("],");
            }
            hcjs.append("]}");
        } else {
            // StatisticsGroupingKey seriesKey = null;
            Object seriesKey = null;
            TreeMap<StatisticsGroupingKey, String> seriesStrings = new TreeMap<>(new StatisticsKeyComparator());
            final StringBuilder currentSeriesString = new StringBuilder();
            final StringBuilder unknownSeriesString = new StringBuilder();
            final StringBuilder totalSeriesString = new StringBuilder();
            TreeMap<Integer, Number> currentSeriesValues = new TreeMap<>();

            for (Object[] row : resultData) {
                // Retrieve series caption of the current row
                Object rowSeriesKey;
                if (seriesIdIndex >= 1) {
                    if (!StatisticsHelper.isNullOrUnknown(row[seriesIdIndex])) {
                        rowSeriesKey = row[seriesIdIndex]; // seriesCaptions.get(
                    } else {
                        rowSeriesKey = StatisticsHelper.UNKNOWN;
                    }
                } else {
                    rowSeriesKey = StatisticsHelper.TOTAL;
                }

                // If the first row or a row with a new caption is processed, save the data and
                // begin a new series
                if (!DataHelper.equal(seriesKey, rowSeriesKey)) {
                    finalizeChartSegment(seriesKey, currentSeriesValues, unknownSeriesString, currentSeriesString,
                            totalSeriesString, seriesStrings);

                    // Append the start sequence of the next series String
                    if (StatisticsHelper.isNullOrUnknown(rowSeriesKey)) {
                        seriesKey = StatisticsHelper.UNKNOWN;
                        unknownSeriesString.append("{ name: '").append(getEscapedFragment(StatisticsHelper.UNKNOWN))
                                .append("', dataLabels: { allowOverlap: false }, data: [");
                    } else if (rowSeriesKey.equals(StatisticsHelper.TOTAL)) {
                        seriesKey = StatisticsHelper.TOTAL;
                        totalSeriesString.append("{name : '").append(getEscapedFragment(StatisticsHelper.TOTAL))
                                .append("', dataLabels: { allowOverlap: false }, data: [");
                    } else {
                        seriesKey = (StatisticsGroupingKey) row[seriesIdIndex];
                        currentSeriesString.append("{ name: '")
                                .append(StringEscapeUtils.escapeEcmaScript(seriesCaptions.get(seriesKey)))
                                .append("', dataLabels: { allowOverlap: false }, data: [");
                    }
                }

                Object value = row[0];
                if (xAxisIdIndex >= 1) {
                    Object xAxisId = row[xAxisIdIndex];
                    int captionPosition = StatisticsHelper.isNullOrUnknown(xAxisId) ? xAxisCaptions.size()
                            : xAxisCaptions.headMap((StatisticsGroupingKey) xAxisId).size();
                    currentSeriesValues.put(captionPosition, (Number) value);
                } else {
                    currentSeriesValues.put(0, (Number) value);
                }
            }

            // Add the last series
            finalizeChartSegment(seriesKey, currentSeriesValues, unknownSeriesString, currentSeriesString,
                    totalSeriesString, seriesStrings);

            seriesStrings.forEach((key, value) -> {
                hcjs.append(value);
            });

            // Add the "Unknown" series
            if (unknownSeriesString.length() > 0) {
                hcjs.append(unknownSeriesString.toString());
            }

            // Add the "Total" series
            if (totalSeriesString.length() > 0) {
                hcjs.append(totalSeriesString.toString());
            }

            // Remove last three characters to avoid invalid chart
            hcjs.delete(hcjs.length() - 3, hcjs.length());

            hcjs.append("]}");
        }
        hcjs.append("]};");

        chart.setHcjs(hcjs.toString());
        resultsLayout.addComponent(chart);
        resultsLayout.setExpandRatio(chart, 1);
    }

    private String getEscapedFragment(String i18nFragmentKeykey) {
        return StringEscapeUtils.escapeEcmaScript(I18nProperties.getCaption(i18nFragmentKeykey));
    }

    private void finalizeChartSegment(Object seriesKey, TreeMap<Integer, Number> currentKeyValues,
            StringBuilder unknownKeyString, StringBuilder currentKeyString, StringBuilder totalKeyString,
            TreeMap<StatisticsGroupingKey, String> columnStrings) {
        if (seriesKey != null) {
            if (StatisticsHelper.isNullOrUnknown(seriesKey)) {
                currentKeyValues.forEach((key, value) -> {
                    unknownKeyString.append("[").append(key).append(",").append(value).append("],");
                });
                unknownKeyString.append("]},");
                currentKeyValues.clear();
                currentKeyString.setLength(0);
            } else if (seriesKey.equals(StatisticsHelper.TOTAL)) {
                currentKeyValues.forEach((key, value) -> {
                    totalKeyString.append("[").append(key).append(",").append(value).append("],");
                });
                totalKeyString.append("]},");
                currentKeyValues.clear();
                currentKeyString.setLength(0);
            } else {
                StatisticsGroupingKey seriesGroupingKey = (StatisticsGroupingKey) seriesKey;
                currentKeyValues.forEach((key, value) -> {
                    currentKeyString.append("[").append(key).append(",").append(value).append("],");
                });
                currentKeyString.append("]},");
                columnStrings.put(seriesGroupingKey, currentKeyString.toString());
                currentKeyValues.clear();
                currentKeyString.setLength(0);
            }
        }
    }

    public void generateMap() {
        List<Object[]> resultData = generateStatistics();

        if (resultData.isEmpty()) {
            resultsLayout.addComponent(emptyResultLabel);
            return;
        }

        HorizontalLayout mapLayout = new HorizontalLayout();
        mapLayout.setSpacing(true);
        mapLayout.setMargin(false);
        mapLayout.setWidth(100, Unit.PERCENTAGE);
        mapLayout.setHeightUndefined();

        LeafletMap map = new LeafletMap();
        map.setTileLayerOpacity(0.5f);
        map.setWidth(100, Unit.PERCENTAGE);
        map.setHeight(580, Unit.PIXELS);
        map.setZoom(6);
        GeoLatLon mapCenter = FacadeProvider.getGeoShapeProvider().getCenterOfAllRegions();
        map.setCenter(mapCenter.getLon(), mapCenter.getLat());

        List<RegionReferenceDto> regions = FacadeProvider.getRegionFacade().getAllAsReference();

        List<LeafletPolygon> outlinePolygones = new ArrayList<LeafletPolygon>();

        // draw outlines of all regions
        for (RegionReferenceDto region : regions) {

            GeoLatLon[][] regionShape = FacadeProvider.getGeoShapeProvider().getRegionShape(region);
            if (regionShape == null) {
                continue;
            }

            for (int part = 0; part < regionShape.length; part++) {
                GeoLatLon[] regionShapePart = regionShape[part];
                LeafletPolygon polygon = new LeafletPolygon();
                polygon.setCaption(region.getCaption());
                // fillOpacity is used, so we can still hover the region
                polygon.setOptions("{\"weight\": 1, \"color\": '#888', \"fillOpacity\": 0.02}");
                polygon.setLatLons(regionShapePart);
                outlinePolygones.add(polygon);
            }
        }

        map.addPolygonGroup("outlines", outlinePolygones);

        resultData.sort((a, b) -> {
            return Long.compare(((Number) a[0]).longValue(), ((Number) b[0]).longValue());
        });

        BigDecimal valuesLowerQuartile = new BigDecimal(
                resultData.size() > 0 ? ((Number) resultData.get((int) (resultData.size() * 0.25))[0]).longValue()
                        : null);
        BigDecimal valuesMedian = new BigDecimal(
                resultData.size() > 0 ? ((Number) resultData.get((int) (resultData.size() * 0.5))[0]).longValue()
                        : null);
        BigDecimal valuesUpperQuartile = new BigDecimal(
                resultData.size() > 0 ? ((Number) resultData.get((int) (resultData.size() * 0.75))[0]).longValue()
                        : null);

        List<LeafletPolygon> resultPolygons = new ArrayList<LeafletPolygon>();

        // Draw relevant district fills
        for (Object[] resultRow : resultData) {

            ReferenceDto regionOrDistrict = (ReferenceDto) resultRow[1];
            String shapeUuid = regionOrDistrict.getUuid();
            BigDecimal regionOrDistrictValue = new BigDecimal(((Number) resultRow[0]).longValue());
            GeoLatLon[][] shape;
            switch (visualizationComponent.getVisualizationMapType()) {
            case REGIONS:
                shape = FacadeProvider.getGeoShapeProvider().getRegionShape(new RegionReferenceDto(shapeUuid));
                break;
            case DISTRICTS:
                shape = FacadeProvider.getGeoShapeProvider().getDistrictShape(new DistrictReferenceDto(shapeUuid));
                break;
            default:
                throw new IllegalArgumentException(visualizationComponent.getVisualizationMapType().toString());
            }

            if (shape == null) {
                continue;
            }

            for (int part = 0; part < shape.length; part++) {
                GeoLatLon[] shapePart = shape[part];
                String fillColor;
                if (regionOrDistrictValue.compareTo(BigDecimal.ZERO) == 0) {
                    fillColor = "#000";
                } else if (regionOrDistrictValue.compareTo(valuesLowerQuartile) < 0) {
                    fillColor = "#FEDD6C";
                } else if (regionOrDistrictValue.compareTo(valuesMedian) < 0) {
                    fillColor = "#FDBF44";
                } else if (regionOrDistrictValue.compareTo(valuesUpperQuartile) < 0) {
                    fillColor = "#F47B20";
                } else {
                    fillColor = "#ED1B24";
                }

                LeafletPolygon polygon = new LeafletPolygon();
                polygon.setCaption(regionOrDistrict.getCaption() + "<br>" + regionOrDistrictValue);
                // fillOpacity is used, so we can still hover the region
                polygon.setOptions("{\"stroke\": false, \"color\": '" + fillColor + "', \"fillOpacity\": 0.8}");
                polygon.setLatLons(shapePart);
                resultPolygons.add(polygon);
            }
        }
        map.addPolygonGroup("results", resultPolygons);

        mapLayout.addComponent(map);
        mapLayout.setExpandRatio(map, 1);

        AbstractOrderedLayout regionLegend = DashboardMapComponent.buildRegionLegend(true, CaseMeasure.CASE_COUNT,
                false, valuesLowerQuartile, valuesMedian, valuesUpperQuartile);
        Label legendHeader = new Label(I18nProperties.getCaption(Captions.dashboardMapKey));
        CssStyles.style(legendHeader, CssStyles.H4, CssStyles.VSPACE_4, CssStyles.VSPACE_TOP_NONE);
        regionLegend.addComponent(legendHeader, 0);

        mapLayout.addComponent(regionLegend);
        mapLayout.setExpandRatio(regionLegend, 0);

        resultsLayout.addComponent(mapLayout);
        resultsLayout.setExpandRatio(mapLayout, 1);
    }

    private List<Object[]> generateStatistics() {
        fillCaseCriteria();

        List<Object[]> resultData = FacadeProvider.getCaseFacade().queryCaseCount(caseCriteria,
                visualizationComponent.getRowsAttribute(), visualizationComponent.getRowsSubAttribute(),
                visualizationComponent.getColumnsAttribute(), visualizationComponent.getColumnsSubAttribute());

        return resultData;
    }

    private void fillCaseCriteria() {
        caseCriteria = new StatisticsCaseCriteria();

        for (StatisticsFilterComponent filterComponent : filterComponents) {
            StatisticsFilterElement filterElement = filterComponent.getFilterElement();
            switch (filterComponent.getSelectedAttribute()) {
            case SEX:
                if (filterElement.getSelectedValues() != null) {
                    List<Sex> sexes = new ArrayList<>();
                    for (TokenizableValue tokenizableValue : filterElement.getSelectedValues()) {
                        if (tokenizableValue.getValue().toString().toLowerCase().equals(StatisticsHelper.UNKNOWN)) {
                            caseCriteria.sexUnknown(true);
                        } else {
                            sexes.add((Sex) tokenizableValue.getValue());
                        }
                    }
                    caseCriteria.sexes(sexes);
                }
                break;
            case DISEASE:
                if (filterElement.getSelectedValues() != null) {
                    List<Disease> diseases = new ArrayList<>();
                    for (TokenizableValue tokenizableValue : filterElement.getSelectedValues()) {
                        diseases.add((Disease) tokenizableValue.getValue());
                    }
                    caseCriteria.diseases(diseases);
                }
                break;
            case CLASSIFICATION:
                if (filterElement.getSelectedValues() != null) {
                    List<CaseClassification> classifications = new ArrayList<>();
                    for (TokenizableValue tokenizableValue : filterElement.getSelectedValues()) {
                        classifications.add((CaseClassification) tokenizableValue.getValue());
                    }
                    caseCriteria.classifications(classifications);
                }
                break;
            case OUTCOME:
                if (filterElement.getSelectedValues() != null) {
                    List<CaseOutcome> outcomes = new ArrayList<>();
                    for (TokenizableValue tokenizableValue : filterElement.getSelectedValues()) {
                        outcomes.add((CaseOutcome) tokenizableValue.getValue());
                    }
                    caseCriteria.outcomes(outcomes);
                }
                break;
            case AGE_INTERVAL_1_YEAR:
            case AGE_INTERVAL_5_YEARS:
            case AGE_INTERVAL_CHILDREN_COARSE:
            case AGE_INTERVAL_CHILDREN_FINE:
            case AGE_INTERVAL_CHILDREN_MEDIUM:
            case AGE_INTERVAL_BASIC:
                if (filterElement.getSelectedValues() != null) {
                    List<IntegerRange> ageIntervals = new ArrayList<>();
                    for (TokenizableValue tokenizableValue : filterElement.getSelectedValues()) {
                        ageIntervals.add((IntegerRange) tokenizableValue.getValue());
                    }
                    caseCriteria.addAgeIntervals(ageIntervals);
                }
                break;
            case REGION_DISTRICT:
                StatisticsFilterRegionDistrictElement regionDistrictElement = (StatisticsFilterRegionDistrictElement) filterElement;
                if (regionDistrictElement.getSelectedRegions() != null) {
                    List<RegionReferenceDto> regions = new ArrayList<>();
                    for (TokenizableValue tokenizableValue : regionDistrictElement.getSelectedRegions()) {
                        regions.add((RegionReferenceDto) tokenizableValue.getValue());
                    }
                    caseCriteria.regions(regions);
                }
                if (regionDistrictElement.getSelectedDistricts() != null) {
                    List<DistrictReferenceDto> districts = new ArrayList<>();
                    for (TokenizableValue tokenizableValue : regionDistrictElement.getSelectedDistricts()) {
                        districts.add((DistrictReferenceDto) tokenizableValue.getValue());
                    }
                    caseCriteria.districts(districts);
                }
                break;
            default:
                switch (filterComponent.getSelectedSubAttribute()) {
                case YEAR:
                    if (filterElement.getSelectedValues() != null) {
                        List<Year> years = new ArrayList<>();
                        for (TokenizableValue tokenizableValue : filterElement.getSelectedValues()) {
                            years.add((Year) tokenizableValue.getValue());
                        }
                        caseCriteria.years(years, filterComponent.getSelectedAttribute());
                    }
                    break;
                case QUARTER:
                    if (filterElement.getSelectedValues() != null) {
                        List<Quarter> quarters = new ArrayList<>();
                        for (TokenizableValue tokenizableValue : filterElement.getSelectedValues()) {
                            quarters.add((Quarter) tokenizableValue.getValue());
                        }
                        caseCriteria.quarters(quarters, filterComponent.getSelectedAttribute());
                    }
                    break;
                case MONTH:
                    if (filterElement.getSelectedValues() != null) {
                        List<Month> months = new ArrayList<>();
                        for (TokenizableValue tokenizableValue : filterElement.getSelectedValues()) {
                            months.add((Month) tokenizableValue.getValue());
                        }
                        caseCriteria.months(months, filterComponent.getSelectedAttribute());
                    }
                    break;
                case EPI_WEEK:
                    if (filterElement.getSelectedValues() != null) {
                        List<EpiWeek> epiWeeks = new ArrayList<>();
                        for (TokenizableValue tokenizableValue : filterElement.getSelectedValues()) {
                            epiWeeks.add((EpiWeek) tokenizableValue.getValue());
                        }
                        caseCriteria.epiWeeks(epiWeeks, filterComponent.getSelectedAttribute());
                    }
                    break;
                case QUARTER_OF_YEAR:
                    if (filterElement.getSelectedValues() != null) {
                        List<QuarterOfYear> quartersOfYear = new ArrayList<>();
                        for (TokenizableValue tokenizableValue : filterElement.getSelectedValues()) {
                            quartersOfYear.add((QuarterOfYear) tokenizableValue.getValue());
                        }
                        caseCriteria.quartersOfYear(quartersOfYear, filterComponent.getSelectedAttribute());
                    }
                    break;
                case MONTH_OF_YEAR:
                    if (filterElement.getSelectedValues() != null) {
                        List<MonthOfYear> monthsOfYear = new ArrayList<>();
                        for (TokenizableValue tokenizableValue : filterElement.getSelectedValues()) {
                            monthsOfYear.add((MonthOfYear) tokenizableValue.getValue());
                        }
                        caseCriteria.monthsOfYear(monthsOfYear, filterComponent.getSelectedAttribute());
                    }
                    break;
                case EPI_WEEK_OF_YEAR:
                    if (filterElement.getSelectedValues() != null) {
                        List<EpiWeek> epiWeeksOfYear = new ArrayList<>();
                        for (TokenizableValue tokenizableValue : filterElement.getSelectedValues()) {
                            epiWeeksOfYear.add((EpiWeek) tokenizableValue.getValue());
                        }
                        caseCriteria.epiWeeksOfYear(epiWeeksOfYear, filterComponent.getSelectedAttribute());
                    }
                    break;
                case DATE_RANGE:
                    caseCriteria.dateRange((Date) filterElement.getSelectedValues().get(0).getValue(),
                            (Date) filterElement.getSelectedValues().get(1).getValue(),
                            filterComponent.getSelectedAttribute());
                    break;
                default:
                    throw new IllegalArgumentException(filterComponent.getSelectedSubAttribute().toString());
                }
            }
        }
    }

}