chart/chart.js

/**
 * Copyright (c) 2019
 *
 * @summary chart class
 * @author Elitza Vasileva
 * @author Bernhard Pointner
 */

/**
 * Chart class
 */
class Chart {
    constructor(_parent, _dummyId, _id, _title, _subtitle, _is_cat_list, _dimension, _group, _description, _filters, _selected, _hovered, _filterCustomData, _updateAll, _previewAll) {
        this.id = _id;
        this.title = _title;
        this.subtitle = _subtitle;
        this.parent = _parent;
        this.dimension = _dimension; // unsorted!!!
        this.group = _group; // unsorted!!!
        this.description = _description; // unsorted!!!
        this.filters = _filters; // filters consisting of active filter and preview filters
        this.filterCustomData = _filterCustomData;
        this.updateAll = _updateAll;
        this.previewAll = _previewAll;

        this.data = null;
        this.preview_data = null;
        this.filtered_data = null;

        // selected and hovered array - must same order as group
        this.selected = _selected;
        this.hovered = _hovered;

        this.filter_button = null;

        this.old_filtered_data = null;
        this.old_extent_labels = null;
        this.current_extent_labels = null;

        this.is_cat_list = _is_cat_list; // special chart because of multiple dimensions for one single chart

        this.preview_active = false;
        this.filter_active = false;
        this.hover_active = false;

        this.sort_by = [];

        this.container = addNewVis(_parent, _dummyId, this.id, this.title, this.subtitle);
        this.margin = null;
        this.width = null;
        this.height = null;
        this.svg = this.container.append("svg");

        this.textLabelFont = 'sans-serif';
        this.textLabelSize = "14px"
        this.textLabelWeight = "400";
        this.labelFontSizeWeight = this.textLabelWeight + ' ' + this.textLabelSize + ' ' + this.textLabelFont;

        this.addFilterButtonClickEvents();
        this.updateData(false);
    }

    /**
     * Updates the data when filtering over the charts
     * @param preview   true when the preview data has to be updated
     * @param reset_preview   true when the updated preview data has to be reset
     */
    updateData(preview, reset_preview) {

        if(preview) {
            let old_data = JSON.parse(JSON.stringify(this.preview_data)); // deep clone
            if(reset_preview) {
                this.preview_data = JSON.parse(JSON.stringify(this.data)); // deep clone
                this.filtered_data = JSON.parse(JSON.stringify(this.preview_data)); // deep clone
            } else {
                if(!this.is_cat_list){
                    this.preview_data = this.group.all().map((group,i) => new Object({
                        key: group.key,
                        title: group.key,
                        value: group.value,
                        filtered: false
                    }));
                } else {
                    this.preview_data = this.group.map(group => group.all()).map((group,i) => new Object({
                        key: this.description[i].key,
                        title: this.description[i].title,
                        value: (typeof group[1] === "undefined") ? 0 : group[1].value,
                        filtered: false
                    }));
                }
            }

            // sort data by reference to get it in the same order like active data
            this.sortDataByReference(this.preview_data, this.data.map(el => el.key), "key");

            // reset selected status
            this.resetFiltered(this.preview_data,old_data);

            // reset value if selected
            this.filtered_data = JSON.parse(JSON.stringify(this.preview_data)); // deep clone
            this.setFilteredData(this.filtered_data);
        } else {
            let old_data = JSON.parse(JSON.stringify(this.data)); // deep clone
            if (!this.is_cat_list) {
                this.data = this.group.all().map((group, i) => new Object({
                    key: group.key,
                    title: group.key,
                    value: group.value,
                    filtered: false
                }));
            } else {
                this.data = this.group.map(group => group.all()).map((group, i) => new Object({
                    key: this.description[i].key,
                    title: this.description[i].title,
                    value: (typeof group[1] === "undefined") ? 0 : group[1].value,
                    filtered: false
                }));
            }

            if(old_data !== null) {
                // sort data by reference to get it in the same order like active data
                this.sortDataByReference(this.data,old_data.map(el=>el.key),"key");

                // reset selected status
                this.resetFiltered(this.data,old_data);

                // reset value if selected
                this.filtered_data = JSON.parse(JSON.stringify(this.data)); // deep clone
                this.setFilteredData(this.filtered_data);
            }

            this.preview_data = JSON.parse(JSON.stringify(this.data)); // deep clone
        }
    }
    
    

    // TODO: override
    sortData() {}

    /**
        Sorts data by a reference array of keys
        @data   data to be sorted
        @key_array   reference array of keys
        @key   object key on which the data should be sorted
     */
    sortDataByReference(data,key_array,key) {
        function sortFunc(a, b) {
            return key_array.indexOf(a[key]) - key_array.indexOf(b[key]);
        }
        data.sort(sortFunc.bind(key_array));
    }

    /**
        Resets filtered status of entry
        @data   data to be sorted
        @old_data .. old data
     */
    resetFiltered(data,old_data) {
        if(old_data != null) {
            data.forEach((el,i) => {
                el.filtered = old_data[i].filtered;
            });
        }
    }

    /**
        Resets value if selected
        @data   data to be sorted
     */
    setFilteredData(data_filtered) {
        if(data_filtered != null) {
            data_filtered.forEach((el, i) => {
                el.value = el.filtered ? el.value : 0;
            });
        }
    }

    // TODO: override
    draw() {}

    // TODO: override
    update() {}

    // TODO: override
    preview(reset) {}

    // TODO: override
    drawStatistic() {}

    // TODO: override
    updateStatistic() {}


    // add filter button click events
    addFilterButtonClickEvents() {
        const _this = this;
        this.filter_button = d3.select("#"+this.id+" .vis-filter-container");
        this.filter_button.on("click", _this.handleFilterButtonMouseClick.bind(_this));
    }

    // handle filter button
    handleFilterButtonMouseClick() {
        let visible = this.filter_button.style("display") === "block";

        if(visible) {
            this.filter_button.style("display", "none");
            this.resetAllFilterDimensions();
        } else {
            this.filter_button.style("display", "block");
        }
    }

    /**
     * Show filter button
     * @param show   true || false
     */
    showFilterButton(show) {
        this.filter_button.style("display", show ? "block" : "none");
    }

    /**
    * Set filter on crossfilter dimension
    * @param index of data because dimension is unsorted => index == -1   no array available for dimension
    * @param match
    */
    filterDimensionOnDescriptionKey(index,match) {
        // note: key must be the index of the dimension
        if(index != -1) {
            this.dimension[this.data[index].key-1].filter(match);
        } else {
            this.dimension.filter(match);
        }
    }


    /**
     * Reset filter on crossfilter dimension
     * @param index of data because dimension is unsorted
     *          index   index of dimension if chart is cat list
     */
    resetFilterDimensionOnDescriptionKey(index) {
        // note: key must be the index of the dimension
        if(this.is_cat_list) {
            this.dimension[this.data[index].key-1].filterAll();
        } else {
            this.dimension.filterAll();
        }
    }

    /**
     * Resets all the filter dimensions for all charts
     */
    resetAllFilterDimensions() {
        const _this = this;
        this.data.forEach((el,i) => {
            _this.selected[i] = false;
            _this.hovered[i] = false;
            if(el.filtered) {
                el.filtered = false;
                _this.filters.active.resetFilter();
                _this.filters.preview.resetFilter();
                _this.resetFilterDimensionOnDescriptionKey(i);
            }
        });
        this.old_filtered_data = null;
        this.old_extent_labels = null;
        this.current_extent_labels = null;
        this.preview_active = false;
        this.filter_active = false;
        this.hover_active = false;

        _this.updateAll(true);
    }

    /**
     * Checks if the data has been filtered
     */
    checkIfDataIsFiltered() {
        let num_filter_active = this.is_cat_list ? this.selected.filter(el=>el) : (this.current_extent_labels !== null ? this.current_extent_labels : []);
        this.showFilterButton(num_filter_active.length !== 0);
    }
}