hisptz/90-90-90-cascade-graph-widget

View on GitHub
src/app/core/helpers/draw-chart.helper.ts

Summary

Maintainability
F
5 days
Test Coverage
import { ChartConfiguration } from '../models/chart-configuration.model';
import * as _ from 'lodash';

export function extendOtherChartOptions(
    initialChartObject: any,
    analyticsObject: any,
    chartConfiguration: any
): any {
    const yAxisSeriesItems: any[] = getAxisItems(
        analyticsObject,
        chartConfiguration.yAxisType
    );

    /**
     * Sort the corresponding series
     */
    const sortedSeries = getSortableSeries(
        getChartSeries(
            analyticsObject,
            getAxisItemsNew(analyticsObject, chartConfiguration.xAxisType, true),
            yAxisSeriesItems,
            chartConfiguration
        ),
        chartConfiguration.cumulativeValues ? -1 : chartConfiguration.sortOrder
    );

    /**
     * Update series with axis options
     */
    const seriesWithAxisOptions = updateSeriesWithAxisOptions(
        sortedSeries,
        chartConfiguration.multiAxisTypes,
        chartConfiguration.touched
    );

    /**
     * Update colors by considering if series has data
     */
    const newColors: any[] = _.filter(
        _.map(seriesWithAxisOptions, seriesObject =>
            seriesObject.data[0] ? seriesObject.data[0].color : undefined
        ),
        color => color
    );

    return {
        ...initialChartObject,
        yAxis: getYAxisOptions(chartConfiguration),
        xAxis: getXAxisOptions(
            getRefinedXAxisCategories(seriesWithAxisOptions),
            chartConfiguration.type
        ),
        colors: newColors.length > 0 ? newColors : initialChartObject.colors,
        series: seriesWithAxisOptions,
    };
}

function getAxisItems(
    analyticsObject: any,
    axisType: string,
    isCategory: boolean = false
) {
    let items: any[] = [];
    const metadataNames = analyticsObject.metaData.names;
    const itemKeys = analyticsObject.metaData[axisType];

    if (itemKeys) {
        items = _.map(itemKeys, itemKey => {
            return {
                id: itemKey,
                name: metadataNames[itemKey],
            };
        });
    }

    return items;
}


function getAxisItemsNew(
    analyticsObject: any,
    axisTypeArray: any[],
    isCategory: boolean = false
) {
    let items: any[] = [];
    const metadataNames = analyticsObject.metaData.names;
    axisTypeArray.forEach((axisType, axisIndex) => {
        const itemKeys = analyticsObject.metaData[axisType];
        if (itemKeys) {
            if (axisIndex > 0) {
                const availableItems = _.assign([], items);
                items = [];
                itemKeys.forEach(itemKey => {
                    availableItems.forEach(item => {
                        items.push({
                            id: item.id + '_' + itemKey,
                            name: item.name + '_' + metadataNames[itemKey].trim(),
                        });
                    });
                });
            } else {
                items = _.map(itemKeys, itemKey => {
                    return {
                        id: itemKey,
                        name: metadataNames[itemKey].trim(),
                    };
                });
            }
        }
    });

    return items;
}

function getChartSeries(
    analyticsObject: any,
    xAxisItems: any[],
    yAxisItems: any[],
    chartConfiguration: any
) {
    const series = yAxisItems.map((yAxisItem, yAxisIndex) => {
        return {
            name: yAxisItem.name,
            id: yAxisItem.id,
            index: yAxisIndex,
            turboThreshold: 0,
            pointPlacement: chartConfiguration.type === 'radar' ? 'on' : undefined,
            data: getSeriesData(
                analyticsObject,
                chartConfiguration,
                yAxisItem.id,
                xAxisItems
            ),
            type: getAllowedChartType(chartConfiguration.type),
        };
    });

    return series;
}

function getSeriesData(
    analyticsObject: any,
    chartConfiguration: any,
    yAxisItemId: string,
    xAxisItems: any[]
) {
    const data: any[] = [];
    /**
     * Get index to locate data for y axis
     */
    const yAxisItemIndex = _.findIndex(
        analyticsObject.headers,
        _.find(analyticsObject.headers, ['name', chartConfiguration.yAxisType])
    );

    /**
     * Get index for value attribute to get the data
     */
    const dataIndex = _.findIndex(
        analyticsObject.headers,
        _.find(analyticsObject.headers, ['name', 'value'])
    );

    /**
     * Get index to locate data for x axis
     */
    const xAxisItemIndex = _.map(
        chartConfiguration.xAxisType,
        (xAxisType: any) => {
            return _.findIndex(
                analyticsObject.headers,
                _.find(analyticsObject.headers, ['name', xAxisType])
            );
        }
    ).join('_');

    if (xAxisItems) {
        xAxisItems.forEach(xAxisItem => {
            /**
             * Get the required data depending on xAxis and yAxis
             */
            const seriesValue = getSeriesValue(
                analyticsObject.rows,
                yAxisItemIndex,
                yAxisItemId,
                xAxisItemIndex,
                xAxisItem.id,
                dataIndex
            );

            data.push({
                id: xAxisItem.id,
                name: xAxisItem.name,
                dataLabels: getDataLabelsOptions(chartConfiguration),
                y: seriesValue,
            });
        });
    }

    return data;
}

function getDataLabelsOptions(chartConfiguration: any) {
    let dataLabels = null;

    switch (chartConfiguration.type) {
        case 'pie':
            dataLabels = {
                enabled: chartConfiguration.showData,
                format:
                    '{point.name}<br/> <b>{point.y}</b> ( {point.percentage:.1f} % )',
            };
            break;
        default:
            dataLabels = {
                enabled: chartConfiguration.showData,
            };
            break;
    }

    return dataLabels;
}

function getSeriesValue(
    analyticsRows,
    yAxisItemIndex,
    yAxisItemId,
    xAxisItemIndex,
    xAxisItemId,
    dataIndex
) {
    let finalValue = 0;
    const seriesValues = _.map(analyticsRows, row => {
        let seriesValue = 0;
        let xAxisRowId = '';
        _.forEach(xAxisItemIndex.split('_'), (axisIndex: any) => {
            xAxisRowId += xAxisRowId !== '' ? '_' : '';
            xAxisRowId += row[axisIndex];
        });

        if (row[yAxisItemIndex] === yAxisItemId && xAxisRowId === xAxisItemId) {
            const value = parseFloat(row[dataIndex]);
            if (isNaN(value)) {
                return row[dataIndex];
            }
            seriesValue += value;
        }
        return seriesValue;
    }).filter(value => value !== 0);

    if (seriesValues) {
        // Check if series values have non numeric content
        if (_.some(seriesValues, seriesValue => isNaN(seriesValue))) {
            return '';
        }
        // TODO find best way to identify ratios
        const isRatio = _.some(
            seriesValues,
            seriesValue => seriesValue.toString().split('.')[1]
        );

        const valueSum =
            seriesValues.length > 0
                ? seriesValues.reduce((sum, count) => sum + count)
                : 0;

        if (isRatio) {
            finalValue = parseFloat((valueSum / seriesValues.length).toFixed(2));
        } else {
            finalValue = valueSum;
        }
    }

    return finalValue !== 0 ? finalValue : null;
}


function getSortableSeries(series, sortOrder) {
    let newSeries = [...series];
    let seriesCategories = [];

    /**
     * Combine all available series for sorting
     */
    const combinedSeriesData = [
        ...getCombinedSeriesData(_.map(series, seriesObject => seriesObject.data)),
    ];

    if (sortOrder === 1) {
        seriesCategories = _.map(
            _.reverse(_.sortBy(combinedSeriesData, ['y'])),
            seriesData => seriesData.id
        );
        newSeries = _.map(newSeries, seriesObject => {
            const newSeriesObject: any = { ...seriesObject };

            if (seriesCategories.length > 0) {
                newSeriesObject.data = [
                    ..._.map(seriesCategories, seriesCategory =>
                        _.find(seriesObject.data, ['id', seriesCategory])
                    ),
                ];
            }

            return newSeriesObject;
        });
    } else if (sortOrder === -1) {
        seriesCategories = _.map(
            _.sortBy(combinedSeriesData, ['y']),
            seriesData => seriesData.id
        );
        newSeries = _.map(series, seriesObject => {
            const newSeriesObject: any = { ...seriesObject };

            if (seriesCategories.length > 0) {
                newSeriesObject.data = [
                    ..._.map(seriesCategories, seriesCategory =>
                        _.find(seriesObject.data, ['id', seriesCategory])
                    ),
                ];
            }
            return newSeriesObject;
        });
    }
    return newSeries;
}

function getCombinedSeriesData(seriesData: any) {
    let combinedSeriesData = [];
    seriesData.forEach(seriesDataArray => {
        seriesDataArray.forEach(seriesDataObject => {
            const availableSeriesData = _.find(combinedSeriesData, [
                'id',
                seriesDataObject.id,
            ]);
            if (!availableSeriesData) {
                combinedSeriesData = [...combinedSeriesData, seriesDataObject];
            } else {
                const seriesDataIndex = _.findIndex(
                    combinedSeriesData,
                    availableSeriesData
                );
                const newSeriesObject = { ...seriesDataObject };
                newSeriesObject.y += availableSeriesData.y;
                combinedSeriesData = [
                    ...combinedSeriesData.slice(0, seriesDataIndex),
                    newSeriesObject,
                    ...combinedSeriesData.slice(seriesDataIndex + 1),
                ];
            }
        });
    });

    return combinedSeriesData;
}

function getYAxisOptions(chartConfiguration: any) {
    const yAxes: any[] = chartConfiguration.axes;
    let newYAxes: any[] = [];

    if (yAxes.length === 0) {
        newYAxes = _.assign(
            [],
            [
                {
                    min: chartConfiguration.rangeAxisMinValue,
                    max: chartConfiguration.rangeAxisMaxValue,
                    title: {
                        text: '',
                        style: {
                            color: '#000000',
                            fontWeight: 'normal',
                            fontSize: '14px',
                        },
                    },
                },
            ]
        );
    } else {
        newYAxes = _.map(yAxes, (yAxis: any, yAxisIndex: any) => {
            return {
                min: chartConfiguration.rangeAxisMinValue,
                max: chartConfiguration.rangeAxisMaxValue,
                title: {
                    text: yAxis.name,
                    style: { color: '#000000', fontWeight: 'normal', fontSize: '14px' },
                },
                opposite: yAxis.orientation === 'left' ? false : true,
            };
        });
    }

    return _.map(newYAxes, (yAxis: any) => {
        /**
         * Get more options depending on chart type
         */
        switch (chartConfiguration.type) {
            case 'radar':
                yAxis['gridLineInterpolation'] = 'polygon';
                yAxis['lineWidth'] = 0;
                break;
            case 'solidgauge':
                yAxis['lineWidth'] = 0;
                yAxis['labels'] = {
                    y: 16,
                };
                yAxis['max'] = 100;
                break;
            case 'stacked_column':
                yAxis['stackLabels'] = {
                    enabled: false,
                    style: {
                        fontWeight: 'bold',
                    },
                };
                break;
            default:
                yAxis['labels'] = {
                    style: { color: '#000000', fontWeight: 'normal', fontSize: '14px' },
                };
                yAxis['plotLines'] = [
                    {
                        color: '#000000',
                        dashStyle: 'Solid',
                        value: chartConfiguration.targetLineValue,
                        width: 2,
                        zIndex: 1000,
                        label: {
                            text: chartConfiguration.targetLineLabel,
                        },
                    },
                    {
                        color: '#000000',
                        dashStyle: 'Solid',
                        value: chartConfiguration.baseLineValue,
                        zIndex: 1000,
                        width: 2,
                        label: {
                            text: chartConfiguration.baseLineLabel,
                        },
                    },
                ];
                break;
        }
        return yAxis;
    });
}

function getXAxisOptions(
    xAxisCategories: any[],
    chartConfiguration: ChartConfiguration
) {
    let xAxisOptions = {};

    switch (chartConfiguration.type) {
        case 'radar':
            xAxisOptions = _.assign(
                {},
                {
                    categories: xAxisCategories,
                    tickmarkPlacement: 'on',
                    lineWidth: 0,
                }
            );
            break;
        default:
            xAxisOptions = _.assign(
                {},
                {
                    categories: xAxisCategories,
                    labels: {
                        rotation:
                            xAxisCategories.length <= 5
                                ? 0
                                : xAxisCategories.length >= 10
                                    ? -45
                                    : -45,
                        style: {
                            color: '#000000',
                            fontWeight: 'normal',
                            fontSize: '12px',
                            textOverflow: 'none',
                        },
                    },
                }
            );
            break;
    }

    return xAxisOptions;
}

function getRefinedXAxisCategories(series: any[]) {
    let newCategories: any[] = [];
    if (series) {
        const seriesDataObjects = _.map(
            series,
            (seriesObject: any) => seriesObject.data
        );

        if (seriesDataObjects) {
            const seriesCategoryNamesArray = _.map(seriesDataObjects, seriesData => {
                return _.map(seriesData, data => {
                    // const nameArray = data.name.split('_');
                    const nameArray = [data.name];
                    const newCategoryArray = [];
                    if (nameArray) {
                        const reversedNameArray = _.reverse(nameArray);
                        _.times(nameArray.length, (num: number) => {
                            if (num === 0) {
                                newCategoryArray.push({ name: reversedNameArray[num] });
                            } else {
                                const parentCategory: any = _.find(newCategoryArray, [
                                    'name',
                                    reversedNameArray[num - 1],
                                ]);

                                if (parentCategory) {
                                    const parentCategoryIndex = _.findIndex(
                                        newCategoryArray,
                                        parentCategory
                                    );
                                    let newChildrenCategories: any[] = parentCategory.categories
                                        ? parentCategory.categories
                                        : [];
                                    newChildrenCategories = _.concat(
                                        newChildrenCategories,
                                        reversedNameArray[num]
                                    );
                                    parentCategory.categories = _.assign(
                                        [],
                                        newChildrenCategories
                                    );

                                    newCategoryArray[parentCategoryIndex] = parentCategory;
                                }
                            }
                        });
                    }
                    return newCategoryArray[0];
                });
            });

            if (seriesCategoryNamesArray) {
                const groupedCategoryNames = _.groupBy(
                    seriesCategoryNamesArray[0],
                    'name'
                );
                const categoryNameGroupKeys = _.map(
                    seriesCategoryNamesArray[0],
                    category => category.name
                );
                const sanitizedCategoryNames: any[] = [];
                _.forEach(categoryNameGroupKeys, (key: any) => {
                    const categories = _.filter(
                        _.map(groupedCategoryNames[key], (categoryObject: any) => {
                            return categoryObject.categories
                                ? categoryObject.categories[0]
                                : null;
                        }),
                        (category: any) => category !== null
                    );
                    if (categories.length === 0) {
                        sanitizedCategoryNames.push({ name: key });
                    } else {
                        sanitizedCategoryNames.push({
                            name: key,
                            categories: categories,
                        });
                    }
                });

                newCategories = _.assign([], sanitizedCategoryNames);
            }
        }
    }

    return _.uniqBy(newCategories, 'name');
}

function updateSeriesWithAxisOptions(
    series: any[],
    multiAxisOptions: any[],
    touched: boolean = false
) {
    return _.map(series, (seriesObject: any) => {
        const newSeriesObject = _.clone(seriesObject);
        const availableAxisOption: any = _.find(multiAxisOptions, [
            'id',
            newSeriesObject.id,
        ]);
        if (availableAxisOption) {
            newSeriesObject.yAxis = availableAxisOption.axis
                ? availableAxisOption.axis === 'left'
                    ? 0
                    : 1
                : 0;

            newSeriesObject.type =
                availableAxisOption.type !== '' && !touched
                    ? getAllowedChartType(availableAxisOption.type)
                    : seriesObject.type;

            if (availableAxisOption.type === 'dotted') {
                newSeriesObject.lineWidth = 0;
                newSeriesObject.states = {
                    hover: {
                        enabled: false,
                    },
                };
            }

            /**
             *Also apply colors on chart
             */
            newSeriesObject.data = _.map(newSeriesObject.data, dataObject => {
                const newDataObject = _.clone(dataObject);
                if (availableAxisOption.color !== '') {
                    newDataObject.color = availableAxisOption.color;
                }
                return newDataObject;
            });
        }
        return newSeriesObject;
    });
}

function getAllowedChartType(chartType: string): string {
    let newChartType = '';
    switch (chartType) {
        case 'radar':
            newChartType = 'line';
            break;
        case 'dotted':
            newChartType = 'line';
            break;
        default:
            const splitedChartType: any[] = chartType.split('_');
            newChartType =
                splitedChartType.length > 1 ? splitedChartType[1] : splitedChartType[0];
            break;
    }
    return newChartType;
}