// @ts-strict-ignore
import { DecimalPipe, formatNumber } from "@angular/common";
import { AfterContentInit, AfterViewInit, ChangeDetectorRef, Directive, EventEmitter, HostListener, inject, Input, OnDestroy, OnInit, Output, signal, ViewChild, WritableSignal, } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { TranslateService } from "@ngx-translate/core";
import * as Chart from "chart.js";
import "chartjs-adapter-date-fns";
import annotationPlugin from "chartjs-plugin-annotation";
import ChartDataLabels from "chartjs-plugin-datalabels";
import { v4 as uuidv4 } from "uuid";

import { calculateResolution, ChronoUnit, DEFAULT_NUMBER_CHART_OPTIONS, DEFAULT_TIME_CHART_OPTIONS, isLabelVisible, Resolution, setLabelVisible, } from "src/app/edge/history/shared";
import { QueryHistoricTimeseriesEnergyPerPeriodResponse } from "src/app/shared/jsonrpc/response/queryHistoricTimeseriesEnergyPerPeriodResponse";
import { DefaultTypes } from "src/app/shared/type/defaulttypes";
import { JsonrpcResponseError } from "../../jsonrpc/base";
import { JsonRpcUtils } from "../../jsonrpc/jsonrpcutils";
import { QueryHistoricTimeseriesDataRequest } from "../../jsonrpc/request/queryHistoricTimeseriesDataRequest";
import { QueryHistoricTimeseriesEnergyPerPeriodRequest } from "../../jsonrpc/request/queryHistoricTimeseriesEnergyPerPeriodRequest";
import { QueryHistoricTimeseriesEnergyRequest } from "../../jsonrpc/request/queryHistoricTimeseriesEnergyRequest";
import { QueryHistoricTimeseriesDataResponse } from "../../jsonrpc/response/queryHistoricTimeseriesDataResponse";
import { QueryHistoricTimeseriesEnergyResponse } from "../../jsonrpc/response/queryHistoricTimeseriesEnergyResponse";
import { FormatSecondsToDurationPipe } from "../../pipe/formatSecondsToDuration/formatSecondsToDuration.pipe";
import { LayoutRefreshService } from "../../service/layoutRefreshService";
import { RouteService } from "../../service/route.service";
import { ChannelAddress, Currency, Edge, EdgeConfig, Logger, Service, Utils } from "../../shared";
import { Language } from "../../type/language";
import { ArrayUtils } from "../../utils/array/array.utils";
import { ColorUtils } from "../../utils/color/color.utils";
import { DateUtils } from "../../utils/date/dateutils";
import { DateTimeUtils } from "../../utils/datetime/datetime-utils";
import { ObjectUtils } from "../../utils/object/object-utils";
import { TimeUtils } from "../../utils/time/timeutils";
import { ChartAxis, HistoryUtils, YAxisType } from "../../utils/utils";
import { FooterNavigationComponent } from "../footer/subnavigation/footerNavigation";
import { NavigationService } from "../navigation/service/navigation.service";
import { ViewUtils } from "../navigation/view/shared/shared";
import { Converter } from "../shared/converter";
import { ChartConstants, XAxisType } from "./chart.constants";
import { ChartTypes } from "./chart.types";

Chart.Chart.register(annotationPlugin);
Chart.Chart.register(ChartDataLabels);

/**
 * Declare custom Tooltip Positioner "bottom". See example at
 * https://www.chartjs.org/docs/latest/samples/tooltip/position.html
 */
declare module "chart.js" {
    interface TooltipPositionerMap {
        bottom: Chart.TooltipPositionerFunction<Chart.ChartType>;
    }
}
Chart.Tooltip.positioners["bottom"] = function (elements, eventPosition) {
    const chartArea = this.chart.chartArea;
    const eventX = eventPosition.x ?? (chartArea.left + chartArea.right) / 2;

    const isRightHalf = eventX > (chartArea.left + chartArea.right) / 2;
    const padding = 20;
    const x = isRightHalf ? eventX - padding : eventX + padding;

    return {
        x,
        y: chartArea.bottom,
        xAlign: isRightHalf ? "right" : "left",
    };
};

// NOTE: Auto-refresh of widgets is currently disabled to reduce server load

@Directive()
export abstract class AbstractHistoryChart implements OnInit, OnDestroy, AfterViewInit, AfterContentInit {
    protected static readonly phaseColors: string[] = ["rgb(255,127,80)", "rgb(91, 92, 214)", "rgb(128,128,0)"];

    /** Title for Chart, diplayed above the Chart */
    @Input() public chartTitle: string = "";

    /** Optional chart height in percent (0–100) of the available view height. */
    @Input() public customChartHeightPercentage?: number;

    /** TODO: workaround with Observables, to not have to pass the period on Initialisation */
    @Input() public component: EdgeConfig.Component;
    @Input() public showPhases: boolean = false;
    @Input() public showTotal: boolean = false;
    @Input() public isOnlyChart: boolean = false;
    @Input() public xAxisScalingType: XAxisType = XAxisType.TIMESERIES;
    @Output() public setChartConfig: EventEmitter<ChartTypes.ChartConfig> = new EventEmitter();
    @ViewChild(FooterNavigationComponent, { static: true })
    public footerNavigation: FooterNavigationComponent;

    public edge: Edge | null = null;
    public loading: boolean = true;
    public labels: ChartTypes.Label[] = [];
    public datasets: ChartTypes.Dataset[] = [];
    public options: Chart.ChartOptions | null = DEFAULT_TIME_CHART_OPTIONS();
    public colors: any[] = [];
    public chartObject: HistoryUtils.ChartData | null = null;
    protected spinnerId: string = uuidv4();
    protected chartType: "line" | "bar" = "line";
    protected chartTypeSignal: WritableSignal<"line" | "bar"> = signal("line");
    protected isDataExisting: boolean = true;
    protected config: EdgeConfig = null;
    protected errorResponse: JsonrpcResponseError | null = null;
    protected legendOptions: {
        label: string;
        strokeThroughHidingStyle: boolean;
        hideLabelInLegend: boolean;
    }[] = [];
    protected channelData: { data: HistoryUtils.ChannelData } = {
        data: {},
    };
    protected viewHeight: string | null = null;
    protected routeService: RouteService = inject(RouteService);
    private layoutRefresh = inject(LayoutRefreshService);

    constructor(
        public service: Service,
        public cdRef: ChangeDetectorRef,
        protected translate: TranslateService,
        protected route: ActivatedRoute,
        protected logger: Logger,
        protected navigationService: NavigationService,
    ) {
        this.service.historyPeriod.subscribe(() => {
            this.updateChart();
        });
    }

    /**
     * Fills the chart with required data
     *
     * @param chartType Chart visualization type to generate: "line" or "bar".
     * @param energyPeriodResponse The response of a {@link QueryHistoricTimeseriesEnergyPerPeriodRequest} or
     *   {@link QueryHistoricTimeseriesDataResponse}
     * @param energyResponse The response of a {@link QueryHistoricTimeseriesEnergyResponse}
     */
    public static fillChart(
        chartType: "line" | "bar",
        chartObject: HistoryUtils.ChartData,
        energyPeriodResponse: QueryHistoricTimeseriesDataResponse | QueryHistoricTimeseriesEnergyPerPeriodResponse,
        energyResponse?: QueryHistoricTimeseriesEnergyResponse,
    ) {
        if (Utils.isDataEmpty(energyPeriodResponse)) {
            return {
                datasets: ChartConstants.EMPTY_DATASETS,
                labels: [],
                legendOptions: [],
            };
        }

        const channelData: { data: { [name: string]: number[] } } = {
            data: {},
        };
        const result = energyPeriodResponse.result;
        const labels: Date[] = [];
        for (const timestamp of result.timestamps) {
            labels.push(new Date(timestamp));
        }

        chartObject.input?.forEach((element) => {
            let channelAddress: ChannelAddress | null = null;
            if (chartType == "bar" && element.energyChannel) {
                channelAddress = element.energyChannel;
            } else {
                channelAddress = element.powerChannel;
            }

            if (channelAddress?.toString() in result.data) {
                channelData.data[element.name] =
                    HistoryUtils.CONVERT_WATT_TO_KILOWATT_OR_KILOWATTHOURS(result.data[channelAddress.toString()])?.map(
                        (value) => {
                            if (value == null) {
                                return null;
                            }

                            if (element.converter) {
                                return element.converter(value);
                            }

                            return value;
                        },
                    ) ?? null;
            }
        });

        // Fill datasets, labels and colors
        const datasets: Chart.ChartDataset[] = [];
        const displayValues: HistoryUtils.DisplayValue<HistoryUtils.CustomOptions>[] = chartObject.output(
            channelData.data,
            labels,
        );
        const legendOptions: {
            label: string;
            strokeThroughHidingStyle: boolean;
            hideLabelInLegend: boolean;
        }[] = [];
        displayValues.forEach((displayValue, index) => {
            let nameSuffix = null;

            // Check if energyResponse is available
            if (energyResponse && displayValue.nameSuffix && displayValue.nameSuffix(energyResponse) != null) {
                nameSuffix = displayValue.nameSuffix(energyResponse);
            }

            const yAxis = chartObject.yAxes.find(
                (yaxis) => yaxis?.yAxisId == (displayValue?.yAxisId ?? chartObject.yAxes[0]?.yAxisId),
            );

            // Filter existing values
            if (displayValue) {
                const label = AbstractHistoryChart.getTooltipsLabelName(displayValue.name, yAxis?.unit, nameSuffix);
                const data: number[] | null = displayValue.converter();

                if (data === null || data === undefined) {
                    return;
                }

                const configuration = AbstractHistoryChart.fillData(displayValue, label, chartObject, chartType, data);
                datasets.push(...configuration.datasets);
                legendOptions.push(...configuration.legendOptions);
            }
        });

        return {
            datasets: datasets,
            labels: labels,
            legendOptions: legendOptions,
            channelData: channelData,
        };
    }

    public static fillData(
        element: HistoryUtils.DisplayValue<HistoryUtils.CustomOptions>,
        label: string,
        chartObject: HistoryUtils.ChartData,
        chartType: "line" | "bar",
        data: number[] | null,
    ): {
        datasets: Chart.ChartDataset[];
        legendOptions: {
            label: string;
            strokeThroughHidingStyle: boolean;
            hideLabelInLegend: boolean;
        }[];
    } {
        const legendOptions: {
            label: string;
            strokeThroughHidingStyle: boolean;
            hideLabelInLegend: boolean;
        }[] = [];
        const datasets: Chart.ChartDataset[] = [];
        let normalizedData: (number | null)[] = data;

        if (chartObject.normalizeOutputData == true) {
            normalizedData = JsonRpcUtils.normalizeQueryData(data);
        }

        // Enable one dataset to be displayed in multiple stacks
        if (Array.isArray(element.stack)) {
            for (const stack of element.stack) {
                datasets.push(
                    AbstractHistoryChart.getDataSet(
                        element,
                        label,
                        normalizedData,
                        stack,
                        chartObject,
                        element.custom?.type ?? chartType,
                    ),
                );
                legendOptions.push(AbstractHistoryChart.getLegendOptions(label, element));
            }
        } else {
            datasets.push(
                AbstractHistoryChart.getDataSet(
                    element,
                    label,
                    normalizedData,
                    element.stack,
                    chartObject,
                    element.custom?.type ?? chartType,
                ),
            );
            legendOptions.push(AbstractHistoryChart.getLegendOptions(label, element));
        }

        return {
            datasets: datasets,
            legendOptions: legendOptions,
        };
    }

    /**
     * Gets the legendOptions for a displayValue
     *
     * @param label The label
     * @param element The displayValue
     * @returns The label, the hidingStyle of the legendLabel: strokeThroughHidingStyle, hideLabelInLegend
     */
    public static getLegendOptions(
        label: string,
        element: HistoryUtils.DisplayValue<HistoryUtils.CustomOptions>,
    ): {
        label: string;
        strokeThroughHidingStyle: boolean;
        hideLabelInLegend: boolean;
    } {
        return {
            label: label,
            strokeThroughHidingStyle: element.noStrokeThroughLegendIfHidden,
            hideLabelInLegend: element.hideLabelInLegend ?? false,
        };
    }

    /**
     * Gets the color for the legend and chart for a displayValue
     *
     * @param color The color
     * @returns The backgroundColor and borderColor
     */
    public static getColors(
        color: string,
        chartType: "line" | "bar",
    ): { backgroundColor: string; borderColor: string } {
        return {
            backgroundColor:
                "rgba(" +
                (chartType == "bar"
                    ? color.split("(").pop().split(")")[0] + ",0.7)"
                    : color.split("(").pop().split(")")[0] + ",0.05)"),
            borderColor: "rgba(" + color.split("(").pop().split(")")[0] + ",1)",
        };
    }

    /**
     * Change ChartOptions dependent on chartType
     *
     * @param chartType The chart type
     * @returns Chart options
     */
    public static applyChartTypeSpecificOptionsChanges(
        chartType: "bar" | "line",
        options: Chart.ChartOptions,
        service: Service,
        chartObject: HistoryUtils.ChartData | null,
    ): Chart.ChartOptions {
        switch (chartType) {
            case "bar": {
                options.plugins.tooltip.mode = "x";
                options.scales.x["offset"] = true;
                options.scales.x.ticks["source"] = "data";
                let barPercentage = 1;
                switch (service.periodString) {
                    case DefaultTypes.PeriodString.CUSTOM: {
                        barPercentage = 0.7;
                        break;
                    }
                    case DefaultTypes.PeriodString.MONTH: {
                        if (service.getIsSmartphoneResolution() == true) {
                            barPercentage = 1;
                        } else {
                            barPercentage = 0.9;
                        }
                        break;
                    }
                    case DefaultTypes.PeriodString.YEAR: {
                        if (service.getIsSmartphoneResolution() == true) {
                            barPercentage = 1;
                        } else {
                            barPercentage = 0.8;
                        }
                        break;
                    }
                    default:
                        break;
                }

                options.datasets.bar = {
                    barPercentage: barPercentage,
                };
                break;
            }

            case "line":
                options.scales.x["offset"] = false;
                options.scales.x.ticks["source"] = "data";
                options.plugins.tooltip.mode = "index";

                if (chartObject) {
                    for (const yAxis of chartObject.yAxes) {
                        options.scales[yAxis.yAxisId]["stacked"] = false;
                    }
                }
                break;
        }

        return options;
    }

    /**
     * Gets the dataset for a displayValue
     *
     * @param element The displayValue
     * @param label The label
     * @param data The data
     * @param stack The stack
     * @returns A dataset
     */
    public static getDataSet(
        element: HistoryUtils.DisplayValue<HistoryUtils.CustomOptions>,
        label: string,
        data: number[],
        stack: number,
        chartObject: HistoryUtils.ChartData,
        chartType: "line" | "bar",
    ): Chart.ChartDataset {
        const colors = AbstractHistoryChart.getColors(element.color, chartType);
        const dataset: Chart.ChartDataset = {
            label: label,
            data: data,
            hidden: !isLabelVisible(element.name, !element.hiddenOnInit),
            yAxisID:
                element.yAxisId != null
                    ? element.yAxisId
                    : chartObject.yAxes.find((element) => element.yAxisId == ChartAxis.LEFT)?.yAxisId,
            order: element.order ?? Number.MAX_VALUE,
            maxBarThickness: 100,
            borderWidth: 2,
            ...(stack != null ? { stack: stack.toString() } : {}),
            ...(element.borderDash != null ? { borderDash: element.borderDash } : {}),
            ...(element.hideShadow ? { fill: !element.hideShadow } : {}),
            ...(element.custom?.type ? { type: chartType } : {}),
            ...colors,
            ...ChartConstants.Plugins.Datasets.HOVER_ENHANCE(colors),
        };

        if (chartType === "line" && element.stepped != null) {
            (dataset as Chart.ChartDataset<"line", (number | null)[]>).stepped = element.stepped;
        }

        return dataset;
    }

    public static getYAxisTitle(
        title: YAxisType,
        translate: TranslateService,
        chartType: "bar" | "line",
        customTitle?: string,
    ): string {
        switch (title) {
            case YAxisType.RELAY:
                if (chartType === "line") {
                    // Hide YAxis title
                    return "";
                }
                return translate.instant("EDGE.INDEX.WIDGETS.CHANNELTRESHOLD.ACTIVE_TIME_OVER_PERIOD");
            case YAxisType.TIME:
                return translate.instant("EDGE.INDEX.WIDGETS.CHANNELTRESHOLD.ACTIVE_TIME_OVER_PERIOD");
            case YAxisType.RESTRICTION:
            case YAxisType.PERCENTAGE:
                return "%";
            case YAxisType.REACTIVE:
                return "var";
            case YAxisType.ENERGY:
                if (chartType == "bar") {
                    return "kWh";
                } else {
                    return "kW";
                }
            case YAxisType.POWER:
                return "kW";
            case YAxisType.HEAT_PUMP:
            case YAxisType.ENERIX_CONTROL:
                return translate.instant("GENERAL.STATE");
            case YAxisType.VOLTAGE:
                return "V";
            case YAxisType.CURRENT:
                return "A";
            case YAxisType.TEMPERATURE:
                return "°C";
            case YAxisType.NONE:
                return "";
            default:
                return "";
        }
    }

    /**
     * Gets chart options - {@link Chart.ChartOptions}.
     *
     * @param chartObject The chartObject
     * @param chartType The current chart type
     * @param service The service
     * @param translate The translate service
     * @param legendOptions The legend options
     * @param channelData The channel data
     * @param locale The locale
     * @returns Options
     */
    public static getOptions(
        chartObject: HistoryUtils.ChartData,
        chartType: "line" | "bar",
        service: Service,
        translate: TranslateService,
        legendOptions: { label: string; strokeThroughHidingStyle: boolean }[],
        channelData: { data: { [name: string]: number[] } },
        config: EdgeConfig,
        datasets: Chart.ChartDataset[],
        chartOptionsType: XAxisType,
        labels: ChartTypes.Label[],
    ): Chart.ChartOptions {
        let tooltipsLabel: string | null = null;
        let options: Chart.ChartOptions = Utils.deepCopy(
            <Chart.ChartOptions>(
                Utils.deepCopy(AbstractHistoryChart.getDefaultXAxisOptions(chartOptionsType, service, labels))
            ),
        );
        const displayValues: HistoryUtils.DisplayValue<HistoryUtils.CustomOptions>[] = chartObject.output(
            channelData.data,
            labels,
        );

        chartObject.yAxes
            .filter((el) => el satisfies HistoryUtils.yAxes)
            .forEach((element) => {
                options = AbstractHistoryChart.getYAxisOptions(
                    options,
                    element,
                    translate,
                    chartType,
                    datasets,
                    true,
                    chartObject.tooltip.formatNumber,
                );
            });

        options.plugins.tooltip.callbacks.labelPointStyle = function (context: { dataset: Chart.ChartDataset }) {
            return ChartConstants.Plugins.ToolTips.POINT_STYLE(context.dataset);
        };

        options.plugins.tooltip.callbacks.title = (tooltipItems: Chart.TooltipItem<any>[]): string => {
            if (tooltipItems?.length === 0) {
                return null;
            }
            return AbstractHistoryChart.toTooltipTitle(
                service.historyPeriod.value.from,
                service.historyPeriod.value.to,
                tooltipItems[0]?.label,
                service,
                chartOptionsType,
            );
        };
        options = AbstractHistoryChart.applyChartTypeSpecificOptionsChanges(chartType, options, service, chartObject);

        options.plugins.tooltip.callbacks.label = (item: Chart.TooltipItem<any>) => {
            const label = item.dataset.label;
            const value = item.dataset.data[item.dataIndex];

            const displayValue = displayValues.find((element) => element.name === label?.split(":")?.[0]);

            if (displayValue.hiddenInTooltip) {
                return null;
            }
            const unit =
                chartObject.yAxes?.find((el) => el.yAxisId === displayValue.yAxisId)?.unit ??
                chartObject.yAxes[0]?.unit;

            if (value === null) {
                return;
            }

            if (unit != null) {
                tooltipsLabel = AbstractHistoryChart.getToolTipsAfterTitleLabel(unit, chartType, value, translate);
            }

            return AbstractHistoryChart.getToolTipsSuffix(
                label,
                value,
                displayValue.custom?.formatNumber ?? chartObject.tooltip.formatNumber,
                unit,
                chartType,
                translate,
                config,
            );
        };

        options.plugins.tooltip.callbacks.labelColor = (item: Chart.TooltipItem<any>) => {
            let backgroundColor = item.dataset.backgroundColor;

            if (Array.isArray(backgroundColor)) {
                backgroundColor = backgroundColor[0];
            }

            if (!backgroundColor) {
                backgroundColor = item.dataset.borderColor || "rgba(0, 0, 0, 0.5)";
            }

            return {
                borderColor: ColorUtils.changeOpacityFromRGBA(backgroundColor, 1),
                backgroundColor: ColorUtils.changeOpacityFromRGBA(backgroundColor, 1),
            };
        };

        options.plugins.legend.labels.generateLabels = function (chart: Chart.Chart) {
            const chartLegendLabelItems: Chart.LegendItem[] = [];
            chart.data.datasets.forEach((dataset: Chart.ChartDataset, index) => {
                const legendItem = legendOptions?.find((element) => element.label == dataset.label);
                //Remove duplicates like 'directConsumption' from legend
                if (chartLegendLabelItems.filter((element) => element["text"] == dataset.label).length > 0) {
                    return;
                }

                const isHidden = legendItem?.strokeThroughHidingStyle ?? null;

                const chartLegendLabelItem: Chart.LegendItem = {
                    text: dataset.label,
                    datasetIndex: index,
                    fontColor: getComputedStyle(document.documentElement).getPropertyValue("--ion-color-text"),
                    ...(dataset.backgroundColor != null && {
                        fillStyle: dataset.backgroundColor.toString(),
                    }),
                    hidden: isHidden != null ? isHidden : !chart.isDatasetVisible(index),
                    lineWidth: 2,
                    ...(dataset.borderColor != null && {
                        strokeStyle: dataset.borderColor.toString(),
                    }),
                    ...(dataset["borderDash"] != null && {
                        lineDash: dataset["borderDash"],
                    }),
                    ...ChartConstants.Plugins.Legend.POINT_STYLE(dataset),
                };

                const currentDisplayValue = displayValues.find(
                    (el) => el.name == chartLegendLabelItem.text?.split(":")?.[0],
                );

                if (currentDisplayValue?.custom) {
                    const show = !chartLegendLabelItem.hidden;
                    // Hide plugin features on label generate
                    chart.options = AbstractHistoryChart.activateOrDeactivatePlugin(
                        currentDisplayValue,
                        options,
                        chartType,
                        show,
                    );
                }

                chartLegendLabelItems.push(chartLegendLabelItem);
            });

            setTimeout(() => {
                if (!(chart as any)._updated) {
                    (chart as any)._updated = true; // Prevent multiple updates
                    chart.update();
                }
            }, 0);

            return chartLegendLabelItems;
        };

        options.plugins.tooltip.callbacks.afterTitle = function (items: Chart.TooltipItem<any>[]) {
            const locale: string = Language.geti18nLocale();

            if (items?.length === 0) {
                return null;
            }

            // only way to figure out, which stack is active
            const tooltipItem = items[0]; // Assuming only one tooltip item is displayed
            const datasetIndex = tooltipItem.dataIndex;
            // Get the dataset object
            const datasets = items.map((element) => element.dataset);

            // Assuming the dataset is a bar chart using the 'stacked' option
            const stack = items[0].dataset.stack || datasetIndex;
            const yAxisId = items[0].dataset.yAxisID;

            // If only one item in stack do not show sum of values
            if (items.length <= 1) {
                return null;
            }

            const afterTitle =
                typeof chartObject.tooltip?.afterTitle == "function" ? chartObject.tooltip?.afterTitle(stack) : null;

            const cumulatedValue = datasets
                .filter((el) => el.stack == stack)
                .reduce((_total, dataset) => Utils.addSafely(_total, Math.abs(dataset.data[datasetIndex])), 0);
            const unit = chartObject.yAxes?.find((el) => el.yAxisId === yAxisId)?.unit ?? chartObject.yAxes[0]?.unit;

            if (unit != null) {
                tooltipsLabel = AbstractHistoryChart.getToolTipsAfterTitleLabel(
                    unit,
                    chartType,
                    cumulatedValue,
                    translate,
                );
            }

            if (afterTitle) {
                return (
                    afterTitle +
                    ": " +
                    formatNumber(cumulatedValue, locale, chartObject.tooltip.formatNumber) +
                    " " +
                    tooltipsLabel
                );
            }

            return null;
        };

        options.plugins.tooltip.enabled = chartObject.tooltip.enabled ?? true;

        // Remove duplicates from legend, if legendItem with two or more occurrences in legend, use one legendItem to trigger them both
        options.plugins.legend.onClick = function (
            event: Chart.ChartEvent,
            legendItem: Chart.LegendItem,
            legend: Chart.LegendElement<any>,
        ) {
            const chart: Chart.Chart = this.chart;

            function rebuildScales(chart: Chart.Chart) {
                let options = chart.options;
                chartObject.yAxes.forEach((element) => {
                    options = AbstractHistoryChart.getYAxisOptions(
                        options,
                        element,
                        translate,
                        chartType,
                        _dataSets,
                        true,
                        chartObject.tooltip.formatNumber,
                    );
                });
            }

            const legendItems = chart.data.datasets.reduce((arr, ds, i) => {
                if (ds.label == legendItem.text) {
                    arr.push({ label: ds.label, index: i });
                }
                return arr;
            }, []);

            // Should avoid same datasets in multiple stacks and in legend to be always visible => can be hidden with single legend hide
            const hasBeenChanged: Map<string, boolean> = new Map();

            legendItems.forEach((item) => {
                /**
                 * Shows or hides datasets
                 *
                 * @param label The legendItem label
                 * @param chart The chart
                 * @param datasetIndex The dataset index
                 * @returns
                 * @info
                 */
                function showOrHideLabel(label: string, chart: Chart.Chart, datasetIndex: number) {
                    if (hasBeenChanged.has(label)) {
                        return;
                    }

                    const isLabelHidden = !chart.isDatasetVisible(datasetIndex);
                    setLabelVisible(label, isLabelHidden);
                    hasBeenChanged.set(label, isLabelHidden);
                }

                showOrHideLabel(item.label, chart, legendItem.datasetIndex);

                const meta = chart.getDatasetMeta(item.index);
                const currentDisplayValue = displayValues.find((el) => el.name == legendItem.text.split(":")[0]);

                if (currentDisplayValue.custom) {
                    const hidden = !chart.data.datasets[item.index].hidden;
                    // Hide plugin features on click
                    chart.options = AbstractHistoryChart.activateOrDeactivatePlugin(
                        currentDisplayValue,
                        options,
                        chartType,
                        !hidden,
                    );
                }

                // See controller.isDatasetVisible comment
                meta.hidden = meta.hidden === null ? !chart.data.datasets[item.index].hidden : null;
            });

            function showOrHideYAxis(datasets: Chart.ChartDataset[], chart: Chart.Chart) {
                for (const key of Object.keys(ObjectUtils.excludeProperties(chart.options.scales, ["x"]))) {
                    const axisDatasets = datasets.filter((d) => d["yAxisID"] === key);
                    // It is important to update the options in the outer scope!
                    options.scales[key].display = axisDatasets.some((d) => !d.hidden);
                }
            }

            /** Needs to be set, cause property async set */
            const _dataSets: Chart.ChartDataset[] = datasets.map((v, k) => {
                if (k === legendItem.datasetIndex) {
                    v.hidden = !v.hidden;
                }
                return v;
            });

            rebuildScales(chart);
            showOrHideYAxis(_dataSets, chart);
            chart.update();
        };

        options.scales.x.ticks["source"] = "auto";
        options.scales.x.ticks.maxTicksLimit = 31;
        options.scales.x["bounds"] = "ticks";
        options.scales.x.ticks.color = getComputedStyle(document.documentElement).getPropertyValue(
            "--ion-color-chart-xAxis-ticks",
        );
        Chart.defaults.font.family = getComputedStyle(document.documentElement).getPropertyValue("--ion-font-family");

        options.plugins["syncChart"] = {
            group: 1,
        };
        return options;
    }

    /**
     * Gets the yAxis options
     *
     * @param options The chart options
     * @param element The yAxis
     * @param translate The translate service
     * @param chartType The current chart type
     * @param locale The current locale
     * @returns The chart options {@link Chart.ChartOptions}
     */
    public static getYAxisOptions(
        options: Chart.ChartOptions,
        element: HistoryUtils.yAxes,
        translate: TranslateService,
        chartType: "line" | "bar",
        datasets: ChartTypes.Dataset[],
        showYAxisType?: boolean,
        formatNumber?: HistoryUtils.ChartData["tooltip"]["formatNumber"],
    ): Chart.ChartOptions {
        const locale: string = Language.geti18nLocale();
        const baseConfig = ChartConstants.DEFAULT_Y_SCALE_OPTIONS(
            element,
            translate,
            chartType,
            datasets,
            showYAxisType,
            formatNumber,
        );

        switch (element.unit) {
            case YAxisType.RELAY:
                options.scales[element.yAxisId] = {
                    ...baseConfig,
                    min: 0,
                    max: 1,
                    beginAtZero: true,
                    ticks: {
                        ...baseConfig.ticks,
                        stepSize: 1,
                        // Two states are possible
                        callback: function (value, index, ticks: Chart.Tick[]) {
                            return Converter.ON_OFF(translate)(value);
                        },
                        padding: 5,
                    },
                };
                break;
            case YAxisType.RESTRICTION:
            case YAxisType.PERCENTAGE:
                options.scales[element.yAxisId] = {
                    ...baseConfig,
                    stacked: false,
                    beginAtZero: true,
                    max: 100,
                    min: 0,
                    type: "linear",
                    ticks: {
                        ...baseConfig.ticks,
                        padding: 5,
                        stepSize: 20,
                        callback: function (value) {
                            if (Number.isInteger(value)) {
                                return value;
                            }
                            return "";
                        },
                    },
                };
                break;
            case YAxisType.TEMPERATURE:
                options.scales[element.yAxisId] = {
                    ...baseConfig,
                    stacked: false,
                    type: "linear",
                    ticks: {
                        ...baseConfig.ticks,
                        stepSize: 4,
                    },
                };
                break;

            case YAxisType.TIME:
                options.scales[element.yAxisId] = {
                    ...baseConfig,
                    min: 0,
                    ticks: {
                        ...baseConfig.ticks,
                        callback: function (value, index, values) {
                            if (typeof value === "number") {
                                return TimeUtils.formatSecondsToDuration(value, locale);
                            }
                        },
                    },
                };
                break;
            case YAxisType.HEAT_PUMP:
                {
                    const { callback, ...rest } = baseConfig.ticks;
                    options.scales[element.yAxisId] = {
                        ...baseConfig,
                        min: 1,
                        max: 4,
                        beginAtZero: true,
                        ticks: {
                            ...rest,
                            stepSize: 1,
                        },
                    };
                }
                break;
            case YAxisType.ENERIX_CONTROL:
                {
                    const { callback, ...rest } = baseConfig.ticks;
                    options.scales[element.yAxisId] = {
                        ...baseConfig,
                        min: 1,
                        max: 3,
                        beginAtZero: true,
                        ticks: {
                            ...rest,
                            stepSize: 1,
                        },
                    };
                }
                break;
            case YAxisType.HEATING_ELEMENT:
                {
                    const { callback, ...rest } = baseConfig.ticks;
                    options.scales[element.yAxisId] = {
                        ...baseConfig,
                        min: 0,
                        max: 3,
                        beginAtZero: true,
                        ticks: {
                            ...rest,
                            stepSize: 1,
                        },
                    };
                }
                break;
            case YAxisType.VOLTAGE:
            case YAxisType.CURRENT:
                options.scales[element.yAxisId] = {
                    ...baseConfig,
                    beginAtZero: false,
                };
                break;
            case YAxisType.CURRENCY:
                options.scales[element.yAxisId] = {
                    ...baseConfig,
                    beginAtZero: false,
                    ticks: {
                        ...baseConfig.ticks,
                        source: "auto",
                    },
                };
                break;
            case YAxisType.POWER:
            case YAxisType.ENERGY:
                options.scales[element.yAxisId] = {
                    ...baseConfig,
                    max:
                        baseConfig?.max && baseConfig?.min && baseConfig?.max === baseConfig?.min
                            ? baseConfig.max + 1
                            : baseConfig.max,
                };
                break;
            case YAxisType.REACTIVE:
            case YAxisType.NONE:
                options.scales[element.yAxisId] = baseConfig;
                break;
        }
        return options;
    }

    /**
     * Gets the Name for the tooltips label
     *
     * @param baseName The baseName = the value
     * @param unit The unit
     * @param suffix The suffix, a number that will be added to the baseName
     * @returns A string, that is either the baseName, if no suffix is provided, or a baseName with a formatted number
     */
    public static getTooltipsLabelName(baseName: string, unit: YAxisType, suffix?: number | string): string {
        const locale: string = Language.geti18nLocale();
        if (suffix != null) {
            if (typeof suffix === "string") {
                return baseName + " " + suffix;
            } else {
                switch (unit) {
                    case YAxisType.ENERGY:
                        return baseName + ": " + formatNumber(suffix / 1000, locale, "1.0-1") + " kWh";
                    case YAxisType.PERCENTAGE:
                        return baseName + ": " + formatNumber(suffix, locale, "1.0-1") + " %";
                    case YAxisType.RESTRICTION:
                    case YAxisType.RELAY:
                    case YAxisType.HEAT_PUMP:
                    case YAxisType.ENERIX_CONTROL:
                    case YAxisType.TIME: {
                        const pipe = new FormatSecondsToDurationPipe(new DecimalPipe(Language.DE.key));
                        return baseName + ": " + pipe.transform(suffix);
                    }
                    case YAxisType.TEMPERATURE:
                        return baseName + ": " + formatNumber(suffix / 10, locale, "1.0-1") + " °C";
                    default:
                        return baseName;
                }
            }
        }
        return baseName;
    }

    /**
     * Gets the tooltips label, dependent on YAxisType
     *
     * @param title The YAxisType
     * @returns The tooltips suffix
     */
    public static getToolTipsSuffix(
        label: any,
        value: number,
        format: string,
        title: YAxisType,
        chartType: "bar" | "line",
        translate: TranslateService,
        config: EdgeConfig,
    ): string {
        const locale: string = Language.geti18nLocale();
        const prefix: string = label.split(":")[0];
        let suffix: string;
        switch (title) {
            case YAxisType.RELAY:
                return prefix + ": " + Converter.ON_OFF(translate)(value);
            case YAxisType.HEAT_PUMP:
                return prefix + ": " + ChartConstants.Plugins.ToolTips.HEAT_PUMP_SUFFIX(translate, value);
            case YAxisType.ENERIX_CONTROL:
                return prefix + ": " + ChartConstants.Plugins.ToolTips.ENERIX_CONTROL_SUFFIX(translate, value);
            case YAxisType.TIME: {
                const pipe = new FormatSecondsToDurationPipe(new DecimalPipe(locale));
                return prefix + ": " + pipe.transform(value, true);
            }
            case YAxisType.CURRENCY: {
                const meta: EdgeConfig.Component = config?.getComponent("_meta");
                const currency: string = config?.getPropertyFromComponent<string>(meta, "currency");
                suffix = Currency.getCurrencyLabelByCurrency(currency);
                break;
            }
            case YAxisType.RESTRICTION:
            case YAxisType.PERCENTAGE:
                suffix = AbstractHistoryChart.getToolTipsAfterTitleLabel(title, chartType, value, translate);
                break;
            case YAxisType.VOLTAGE:
                suffix = "V";
                break;
            case YAxisType.CURRENT:
                suffix = "A";
                break;
            case YAxisType.TEMPERATURE:
                suffix = "°C";
                break;
            case YAxisType.POWER:
                suffix = "W";
                break;
            case YAxisType.ENERGY:
                if (chartType == "bar") {
                    suffix = "kWh";
                } else {
                    suffix = "kW";
                }
                break;
            case YAxisType.REACTIVE:
                suffix = "var";
                break;
            default:
                suffix = "";
                break;
        }

        return prefix + ": " + formatNumber(value, locale, format) + " " + suffix;
    }

    /**
     * Gets the default x axis chart options
     *
     * @param xAxisType The x axis type
     * @param service The service
     * @param labels The x axis ticks labels
     * @returns Chartoptions
     */
    public static getDefaultXAxisOptions(
        xAxisType: XAxisType,
        service: Service,
        labels: ChartTypes.Label[],
    ): Chart.ChartOptions {
        let options: Chart.ChartOptions;
        switch (xAxisType) {
            case XAxisType.NUMBER:
                options = DEFAULT_NUMBER_CHART_OPTIONS(labels);
                break;
            case XAxisType.TIMESERIES:
                options = <Chart.ChartOptions>Utils.deepCopy(DEFAULT_TIME_CHART_OPTIONS());
                options.scales.x["time"].unit = calculateResolution(
                    service,
                    service.historyPeriod.value.from,
                    service.historyPeriod.value.to,
                ).timeFormat;
                break;
        }

        return options;
    }

    /**
     * Generates a Tooltip Title string from a 'fromDate' and 'toDate'.
     *
     * @param fromDate The From-Date
     * @param toDate The To-Date
     * @param date Date from TooltipItem
     * @returns Period for Tooltip Header
     */
    protected static toTooltipTitle(
        fromDate: Date,
        toDate: Date,
        label: string,
        service: Service,
        chartOptionsType: XAxisType,
    ): string {
        const unit = calculateResolution(service, fromDate, toDate).resolution.unit;

        if (chartOptionsType === XAxisType.NUMBER) {
            return null;
        }

        const date: Date = DateUtils.stringToDate(label);

        switch (unit) {
            case ChronoUnit.Type.YEARS:
                return date.toLocaleDateString("default", { year: "numeric" });
            case ChronoUnit.Type.MONTHS:
                return date.toLocaleDateString("default", { month: "long" });
            case ChronoUnit.Type.DAYS:
                return date.toLocaleDateString("default", {
                    day: "2-digit",
                    month: "long",
                });
            default:
                return (
                    date.toLocaleString("default", {
                        day: "2-digit",
                        month: "2-digit",
                        year: "2-digit",
                    }) +
                    " " +
                    date.toLocaleTimeString("default", {
                        hour12: false,
                        hour: "2-digit",
                        minute: "2-digit",
                    })
                );
        }
    }

    /**
     * Removes the external plugin features
     *
     * @param options The chart options
     * @returns The chart options
     */
    protected static removePlugins(options: Chart.ChartOptions): Chart.ChartOptions {
        options.plugins["annotation"] = {};
        options.plugins["datalabels"] = {
            display: false,
        };
        return options;
    }

    /**
     * Gets the tooltips label, dependent on YAxisType
     *
     * @param title The YAxisType
     * @returns The tooltips title with the corresponding unit
     */
    protected static getToolTipsAfterTitleLabel(
        title: YAxisType | null,
        chartType: "bar" | "line",
        value: number | string | null,
        translate: TranslateService,
    ): string {
        switch (title) {
            case YAxisType.RELAY:
                return Converter.ON_OFF(translate)(value);
            case YAxisType.TIME:
                return "h";
            case YAxisType.RESTRICTION:
            case YAxisType.PERCENTAGE:
                return "%";
            case YAxisType.VOLTAGE:
                return "V";
            case YAxisType.CURRENT:
                return "A";
            case YAxisType.TEMPERATURE:
                return "°C";
            case YAxisType.REACTIVE:
                return "var";
            case YAxisType.ENERGY:
                if (chartType == "bar") {
                    return "kWh";
                } else {
                    return "kW";
                }
            default:
                return "";
        }
    }

    /**
     * Activates or Deactivated chartjs plugins
     *
     * @param displayValue The current displayValue
     * @param options The options
     * @param chartType The chartType
     * @param show If plugin should be shown
     * @returns Updated chart options
     * @note only applied for 'annotation'- plugin
     */
    private static activateOrDeactivatePlugin(
        displayValue: HistoryUtils.DisplayValue<HistoryUtils.CustomOptions>,
        options: Chart.ChartOptions,
        chartType: "line" | "bar",
        show: boolean,
    ): Chart.ChartOptions {
        if (!displayValue.custom) {
            return options;
        }

        switch (displayValue.custom["pluginType"]) {
            case "box":
                if ((displayValue.custom as HistoryUtils.BoxCustomOptions).annotations.length > 0) {
                    options.plugins["annotation"] = {
                        annotations: (displayValue.custom as HistoryUtils.BoxCustomOptions).annotations.map(
                            (annotation) => ({
                                ...annotation,
                                ...AbstractHistoryChart.getColors(displayValue.color, chartType),
                                display: show,
                            }),
                        ),
                    };
                }
                break;
            case "datalabels":
                options.plugins["datalabels"] = ChartConstants.Plugins.BAR_CHART_DATALABELS(
                    (displayValue.custom as HistoryUtils.DataLabelsCustomOptions).datalabels.displayUnit,
                    true,
                );
                Chart.Chart.register(ChartConstants.Plugins.BAR_CHART_DATALABELS("kWh", true).plugin);
                break;
        }
        return options;
    }

    @HostListener("window:resize", ["$event.target.innerHeight"])
    onResize(height: number) {
        this.ngAfterViewInit();
    }

    /**
     * Start NGX-Spinner
     *
     * Spinner will appear inside html tag only
     *
     * @example
     *     <ngx-spinner name="spinnerId"></ngx-spinner>
     *     the spinnerId represents a uuidv4() generated id
     */
    public startSpinner() {
        this.service.startSpinner(this.spinnerId);
    }

    /**
     * Stop NGX-Spinner
     *
     * @param selector Selector for specific spinner
     */
    public stopSpinner() {
        this.service.stopSpinner(this.spinnerId);
        this.layoutRefresh.request(300);
    }

    ngOnInit() {
        this.startSpinner();
        this.service.getCurrentEdge().then((edge) => {
            this.service
                .getConfig()
                .then((config) => {
                    // store important variables publically
                    this.edge = edge;
                    this.config = config;
                })
                .then(() => {
                    this.chartObject = this.getChartData();
                    this.loadChart();
                });
        });
    }

    ngOnDestroy() {
        this.options = AbstractHistoryChart.removePlugins(this.options);
    }

    ngAfterViewInit() {
        this.viewHeight = this.calculateViewHeight();
        this.cdRef.detectChanges(); // Avoids ExpressionChangedAfterItHasBeenCheckedError
    }

    calculateViewHeight(): string {
        return ViewUtils.getChartContentHeightInVh(this.navigationService.position()) + "dvh";
    }

    ngAfterContentInit() {
        setTimeout(() => {
            // TODO: rm after new navigation refactoring complete
            let counter = 0;
            const interval = setInterval(() => {
                this.ngAfterViewInit();

                if (counter > 10) {
                    clearInterval(interval);
                }
                counter++;
            });
        });
    }

    protected getChartHeight(): number | null {
        return ViewUtils.getChartContentHeightInVh(this.navigationService.position());
    }

    protected updateChart() {
        this.startSpinner();
        this.loadChart();
    }

    /** Used to loadChart, dependent on the resolution */
    protected async loadChart() {
        this.labels = [];
        this.errorResponse = null;

        const unit: ChronoUnit.Type = calculateResolution(
            this.service,
            this.service.historyPeriod.value.from,
            this.service.historyPeriod.value.to,
        ).resolution.unit;

        // Show Barchart if resolution is days or months
        if (ChronoUnit.isAtLeast(unit, ChronoUnit.Type.DAYS)) {
            await this.loadBarChart(unit);
        } else {
            await this.loadLineChart(unit);
        }

        this.setChartConfig.emit({
            chartType: this.chartType,
            datasets: this.datasets,
            labels: this.labels,
            options: this.options,
        });
    }

    /**
     * Sends the Historic Timeseries Data Query and makes sure the result is not empty.
     *
     * @param fromDate The From-Date
     * @param toDate The To-Date
     * @param edge The current Edge
     * @param ws The websocket
     */
    protected queryHistoricTimeseriesData(
        fromDate: Date,
        toDate: Date,
        res?: Resolution,
    ): Promise<QueryHistoricTimeseriesDataResponse> {
        this.isDataExisting = true;
        const resolution = res ?? calculateResolution(this.service, fromDate, toDate).resolution;

        return new Promise<QueryHistoricTimeseriesDataResponse>((resolve, reject) => {
            this.service.getCurrentEdge().then((edge) =>
                this.service.getConfig().then(async () => {
                    const channelAddresses = (await this.getChannelAddresses()).powerChannels;
                    const request = new QueryHistoricTimeseriesDataRequest(
                        DateUtils.maxDate(fromDate, this.edge?.firstSetupProtocol),
                        toDate,
                        channelAddresses,
                        resolution,
                    );

                    edge.sendRequest(this.service.websocket, request)
                        .then((response) => {
                            const result = (response as QueryHistoricTimeseriesDataResponse)?.result;
                            let responseToReturn: QueryHistoricTimeseriesDataResponse;

                            if (Object.keys(result).length !== 0) {
                                responseToReturn = response as QueryHistoricTimeseriesDataResponse;
                            } else {
                                this.errorResponse = new JsonrpcResponseError(request.id, {
                                    code: 1,
                                    message: "Empty Result",
                                });
                                responseToReturn = new QueryHistoricTimeseriesDataResponse(response.id, {
                                    timestamps: [null],
                                    data: { null: null },
                                });
                            }

                            if (Utils.isDataEmpty(responseToReturn)) {
                                this.isDataExisting = false;
                                this.initializeChart();
                            }
                            resolve(responseToReturn);
                        })
                        .catch((error) => {
                            if (error instanceof JsonrpcResponseError) {
                                this.errorResponse = error;
                            }
                            this.initializeChart();
                        });
                }),
            );
        });
    }

    /**
     * Sends the Historic Timeseries Energy per Period Query and makes sure the result is not empty. Symbolizes First
     * substracted from last Datapoint for each period, only used for cumulated channel
     *
     * @param fromDate The From-Date
     * @param toDate The To-Date
     */
    protected queryHistoricTimeseriesEnergyPerPeriod(
        fromDate: Date,
        toDate: Date,
        resolution: ReturnType<typeof calculateResolution>["resolution"] = calculateResolution(
            this.service,
            fromDate,
            toDate,
        ).resolution,
    ): Promise<QueryHistoricTimeseriesEnergyPerPeriodResponse | null> {
        this.isDataExisting = true;
        const result: Promise<QueryHistoricTimeseriesEnergyPerPeriodResponse> =
            new Promise<QueryHistoricTimeseriesEnergyPerPeriodResponse>((resolve, reject) => {
                this.service.getCurrentEdge().then((edge) => {
                    this.service.getConfig().then(async () => {
                        const channelAddresses = (await this.getChannelAddresses()).energyChannels.filter(
                            (element) => element != null,
                        );
                        const request = new QueryHistoricTimeseriesEnergyPerPeriodRequest(
                            DateUtils.maxDate(fromDate, edge?.firstSetupProtocol),
                            toDate,
                            channelAddresses,
                            resolution,
                        );
                        if (channelAddresses.length > 0) {
                            edge.sendRequest(this.service.websocket, request)
                                .then((response) => {
                                    const result = (response as QueryHistoricTimeseriesEnergyPerPeriodResponse)?.result;
                                    if (Object.keys(result).length != 0) {
                                        resolve(response as QueryHistoricTimeseriesEnergyPerPeriodResponse);
                                    } else {
                                        this.errorResponse = new JsonrpcResponseError(request.id, {
                                            code: 1,
                                            message: "Empty Result",
                                        });
                                        resolve(
                                            new QueryHistoricTimeseriesEnergyPerPeriodResponse(response.id, {
                                                timestamps: [],
                                                data: {},
                                            }),
                                        );
                                    }
                                })
                                .catch((response) => {
                                    this.errorResponse = response;
                                    this.initializeChart();
                                });
                        } else {
                            this.initializeChart();
                        }
                    });
                });
            }).then((response) => {
                // Check if channelAddresses are empty
                if (Utils.isDataEmpty(response)) {
                    // load defaultchart
                    this.isDataExisting = false;
                    this.stopSpinner();
                    this.initializeChart();
                }
                return response;
            });
        return result;
    }

    /**
     * Sends the Historic Timeseries Energy per Period Query and makes sure the result is not empty. Symbolizes First
     * substracted from last Datapoint for each period, only used for cumulated channel
     *
     * @param fromDate The From-Date
     * @param toDate The To-Date
     */
    protected queryHistoricTimeseriesEnergy(
        fromDate: Date,
        toDate: Date,
    ): Promise<QueryHistoricTimeseriesEnergyResponse> {
        this.isDataExisting = true;

        const result: Promise<QueryHistoricTimeseriesEnergyResponse> =
            new Promise<QueryHistoricTimeseriesEnergyResponse>((resolve, reject) => {
                this.service.getCurrentEdge().then((edge) => {
                    this.service.getConfig().then(async () => {
                        const channelAddresses =
                            (await this.getChannelAddresses()).energyChannels?.filter((element) => element != null) ??
                            [];
                        const request = new QueryHistoricTimeseriesEnergyRequest(
                            DateUtils.maxDate(fromDate, edge?.firstSetupProtocol),
                            toDate,
                            channelAddresses,
                        );
                        if (channelAddresses.length > 0) {
                            edge.sendRequest(this.service.websocket, request)
                                .then((response) => {
                                    const result = (response as QueryHistoricTimeseriesEnergyResponse)?.result;
                                    if (Object.keys(result).length != 0) {
                                        resolve(response as QueryHistoricTimeseriesEnergyResponse);
                                    } else {
                                        this.errorResponse = new JsonrpcResponseError(request.id, {
                                            code: 1,
                                            message: "Empty Result",
                                        });
                                        resolve(
                                            new QueryHistoricTimeseriesEnergyResponse(response.id, {
                                                data: { null: null },
                                            }),
                                        );
                                    }
                                })
                                .catch((response) => {
                                    this.errorResponse = response;
                                    this.initializeChart();
                                });
                        } else {
                            resolve(null);
                        }
                    });
                });
            });

        return result;
    }

    /** Sets the Labels of the Chart */
    protected setChartLabel() {
        this.options = AbstractHistoryChart.getOptions(
            this.chartObject,
            this.chartType,
            this.service,
            this.translate,
            this.legendOptions,
            this.channelData,
            this.config,
            this.datasets,
            this.xAxisScalingType,
            this.labels,
        );
        this.viewHeight = this.calculateViewHeight();
        this.loading = false;
        this.stopSpinner();
    }

    /**
     * Initializes empty chart on error
     *
     * @param spinnerSelector To stop spinner
     */
    protected initializeChart() {
        this.datasets = HistoryUtils.createEmptyDataset(this.translate);
        this.labels = [];
        this.loading = false;
        this.options.scales["y"] = {
            display: false,
        };

        this.stopSpinner();
    }

    /** Initialize Chart with no chartGrid or axes shown */
    protected initializeChartWithBlankCanvas() {
        this.datasets = [];
        this.labels = [];
        this.options.scales = {};
        this.loading = false;
        this.stopSpinner();
    }

    /** Executed before {@link setChartLabel setChartLabel} */
    protected beforeSetChartLabel(): void {}

    protected afterGetChartData(): void {}

    protected loadLineChart(unit: ChronoUnit.Type, chartType: typeof this.chartType = this.chartType) {
        return new Promise<void>((resolve) => {
            Promise.all([
                this.queryHistoricTimeseriesData(
                    this.service.historyPeriod.value.from,
                    this.service.historyPeriod.value.to,
                ),
                this.queryHistoricTimeseriesEnergy(
                    this.service.historyPeriod.value.from,
                    this.service.historyPeriod.value.to,
                ),
            ])
                .then(([dataResponse, energyResponse]) => {
                    this.chartType = "line";
                    dataResponse = DateTimeUtils.normalizeTimestamps(unit, dataResponse);
                    this.chartObject = this.getChartData();
                    const displayValues = AbstractHistoryChart.fillChart(
                        this.chartType,
                        this.chartObject,
                        dataResponse,
                        energyResponse,
                    );
                    this.datasets = displayValues.datasets;
                    this.legendOptions = displayValues.legendOptions;
                    this.labels = displayValues.labels;
                    this.channelData = displayValues.channelData;
                    this.beforeSetChartLabel();
                    this.setChartLabel();
                })
                .catch(() => {
                    this.initializeChart();
                    // Show empty chart
                    resolve();
                })
                .finally(() => resolve());
        });
    }

    protected loadBarChart(unit: ChronoUnit.Type): Promise<void> {
        return new Promise((resolve) => {
            Promise.all([
                this.queryHistoricTimeseriesEnergyPerPeriod(
                    this.service.historyPeriod.value.from,
                    this.service.historyPeriod.value.to,
                ),
                this.queryHistoricTimeseriesEnergy(
                    this.service.historyPeriod.value.from,
                    this.service.historyPeriod.value.to,
                ),
            ])
                .then(([energyPeriodResponse, energyResponse]) => {
                    this.chartType = "bar";
                    this.chartObject = this.getChartData();
                    // TODO after chartjs migration, look for config
                    energyPeriodResponse = DateTimeUtils.normalizeTimestamps(unit, energyPeriodResponse);

                    const displayValues = AbstractHistoryChart.fillChart(
                        this.chartType,
                        this.chartObject,
                        energyPeriodResponse,
                        energyResponse,
                    );
                    this.datasets = displayValues.datasets;
                    this.legendOptions = displayValues.legendOptions;
                    this.labels = displayValues.labels;
                    this.channelData = displayValues.channelData;

                    this.beforeSetChartLabel();
                    this.setChartLabel();
                    resolve();
                })
                .catch(() => {
                    this.initializeChart();
                    // Show empty chart
                    resolve();
                })
                .finally(() => resolve());
        });
    }

    protected loadTimeLineChart(unit: ChronoUnit.Type): Promise<void> {
        return new Promise((resolve) => {
            Promise.all([
                this.queryHistoricTimeseriesData(
                    this.service.historyPeriod.value.from,
                    this.service.historyPeriod.value.to,
                ),
            ])
                .then(([energyPeriodResponse]) => {
                    this.chartType = "bar";
                    this.chartObject = this.getChartData();
                    // TODO after chartjs migration, look for config
                    energyPeriodResponse = DateTimeUtils.normalizeTimestamps(unit, energyPeriodResponse);

                    const displayValues = AbstractHistoryChart.fillChart(
                        this.chartType,
                        this.chartObject,
                        energyPeriodResponse,
                    );
                    this.datasets = displayValues.datasets;
                    this.legendOptions = displayValues.legendOptions;
                    this.labels = displayValues.labels;
                    this.channelData = displayValues.channelData;

                    this.beforeSetChartLabel();
                    this.setChartLabel();
                    resolve();
                })
                .catch(() => {
                    this.initializeChart();
                    // Show empty chart
                    resolve();
                })
                .finally(() => resolve());
        });
    }

    /** Gets the ChannelAddresses that should be queried. */
    private getChannelAddresses(): Promise<{
        powerChannels: ChannelAddress[];
        energyChannels: ChannelAddress[];
    }> {
        return new Promise<{
            powerChannels: ChannelAddress[];
            energyChannels: ChannelAddress[];
        }>((resolve) => {
            if (this.chartObject?.input) {
                resolve({
                    powerChannels: ArrayUtils.sanitize(this.chartObject.input.map((element) => element.powerChannel)),
                    energyChannels: ArrayUtils.sanitize(this.chartObject.input.map((element) => element.energyChannel)),
                });
            }
        });
    }

    protected abstract getChartData(): HistoryUtils.ChartData | null;
}

export enum ChartType {
    LINE = "line",
    BAR = "bar",
}
