chart/chart-vertical-bar.js


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

/**
 * Vertical bar chart
 * @extends Chart
 */
class VerticalBarChart extends Chart {

    constructor(_parent, _dummyId, _id, _title, _subtitle, _is_cat_list, _dimension, _group, _description, _filters, _selected, _hovered, _filterCustomData, _updateAll, _previewAll) {
        super(_parent, _dummyId, _id, _title, _subtitle, _is_cat_list, _dimension, _group, _description, _filters, _selected, _hovered, _filterCustomData, _updateAll, _previewAll);

        this.labelValuePadding = 5;
        this.innerTickSize = 3;

        this.x = null;
        this.y = null;
        this.xAxis = null;
        this.yAxis = null;
        this.grid = null;
        this.labels = null;
        this.bars_active = null;
        this.bars_preview = null;
        this.gBrush = null;
        this.brush = null;
        this.handle = null;

        this.tickSize = 3;

        this.labelHeight = getTextDimension(this.data[0].title,this.textLabelFont).height;

        this.initMargins(this.data);
        this.initSVGDimensions(this.data);
        this.initSVG();
        this.sortData();
    }

    /**
     * Initializes the sizes of the margins of the chart
     * @param data
     */
    initMargins(data) {
        this.margin = {top: 5,right: 10,bottom: 40,left: 35};
    }

    /**
     * Initializes the SVG dimensions
     * @param data
     */
    initSVGDimensions(data) {
        this.width = getWidth(this.container) - 20 - this.margin.left - this.margin.right; // 20px padding
        this.height = getHeight(this.container) - 20 - getHeight(getVisHeader(this.container)) - this.margin.top - this.margin.bottom; // 20px padding + header height
    }


    /**
     * Initializes the SVG
     */
    initSVG() {
        this.svg
            .attr("width", this.width + this.margin.left + this.margin.right)
            .attr("height", this.height + this.margin.top + this.margin.bottom)
        this.svg.g = this.svg
            .append("g")
            .attr("transform","translate(" + this.margin.left + "," + this.margin.top + ")");
    }

    /**
     * Sorts the data labels in lexicographical order
     */
    sortData() {
        //sort bars based on value
        this.data = this.data.sort(function (a, b) {
            return a.key - b.key || ('' + a.title).localeCompare(b.title);
        });

        // format the data
        this.data.forEach(function(d) {
            d.value = +d.value;
        });
    }

    /**
     * Calls methods to draw the chart and the statistics
     */
    draw() {
        this.drawChart();
        this.drawStatistic();
    }

    /**
     * Calls method to update the chart
     */
    update() {
        this.updateChart();
    }

    preview(reset) {
        this.previewChart(reset);
    }

    /**
     * Draws the chart
     */
    drawChart() {
        const _this = this;

        let quantitativeAxis = !isNaN(parseFloat(this.data[0].title));
        //let x_domain = quantitativeAxis ? d3.range(d3.min(this.data, function(d){ return d.title; }),d3.max(this.data, function(d){ return d.title+1; })) : this.data.map(el => el.title);

        let x_domain = this.data.map(el => el.title);

        let y_max = d3.max(this.data, function(d){ return d.value; });

        this.x = d3.scaleBand()
            .range([0, this.width])
            .padding(0.1);

        this.y = d3.scaleLinear()
            .range([this.height,0]);

        // Scale the range of the data in the domains
        this.x.domain(x_domain)
        this.y.domain([0, y_max]);

        this.xAxis = d3.axisBottom(this.x)
            .tickSizeInner(this.innerTickSize)
            .tickPadding(getLongestTextDimension(this.data.map(el=>el.value),this.labelFontSizeWeight).height+this.labelValuePadding);

        this.yAxis = d3.axisLeft(this.y)
            .tickSizeInner(this.innerTickSize)
            .ticks(this.tickSize)
            .tickFormat(d3.format(".2s"));

        this.grid = this.svg.g.append("g")
            .attrs({
                class: "grid"
            })
            .call(d3.axisLeft(this.y)
                .ticks(this.tickSize)
                .tickSize(-_this.width)
                .tickFormat("")
            );

        // append active bars
        let active_bars = this.svg.g.append("g").attrs({class:"bars-active"});
        this.bars_active = active_bars.selectAll(".bar-active")
            .data(this.data)
            .enter()
            .append("rect")
            .attr("class", "bar-active")
            .attr("y", (d) => _this.y(d.value))
            .attr("x", (d) => _this.x(d.title))
            .attr("height", function(d) {
                return _this.height - _this.y(d.value);
            })
            .attr("width", function() { return _this.x.bandwidth()})
            .on("mouseover", (d,i) => _this.handleLabelMouseOver(d,i,_this))
            .on("mouseout", (d,i) => _this.handleLabelMouseOut(d,i,_this))
            .on("click", (d,i) => _this.handleLabelMouseClick(d,i,_this));

        // append preview bars
        let preview_bars = this.svg.g.append("g").attrs({class:"bars-preview"});
        this.bars_preview = preview_bars.selectAll(".bar-preview")
            .data(this.data)
            .enter().append("rect")
            .attr("class", "bar-preview")
            .attr("width", function() { return _this.x.bandwidth()})
            .attr("y", (d) => _this.y(d.value))
            .attr("x", (d) => _this.x(d.title))
            .attr("height", function(d) {
                return _this.height - _this.y(0);
            })
            .on("mouseover", (d,i) => _this.handleLabelMouseOver(d,i,_this))
            .on("mouseout", (d,i) => _this.handleLabelMouseOut(d,i,_this))
            .on("click", (d,i) => _this.handleLabelMouseClick(d,i,_this));


        this.labels = this.svg.g.selectAll(".chart-data-label")
            .data(this.data)
            .enter()
            .append("text")
            .text((d,i) => d.value)
            .attr("class", "chart-data-label")
            .attr("y", function(d, i) {
                return _this.height + _this.labelHeight + _this.innerTickSize;
            })
            .attr("x", function(d) {
                return _this.x(d.title) + _this.x.bandwidth()/2;
            })
            .attr("fill", "#969da3")
            .attr("font-size", "14px")
            .attr("text-anchor","middle")
            .on("mouseover", (d,i) => _this.handleLabelMouseOver(d,i,_this))
            .on("mouseout", (d,i) => _this.handleLabelMouseOut(d,i,_this))
            .on("click", (d,i) => _this.handleLabelMouseClick(d,i,_this));

        // add the x Axis
        this.svg.g.append("g")
            .attrs({
                class: "x axis bottom primary-axis",
                transform: "translate(0, " + this.height  +")"
            })
            .call(this.xAxis);
        // add the y Axis
        this.svg.g.append("g")
            .attrs({
                class: "y axis left secondary-axis"
            })
            .call(this.yAxis);

        let ticks = this.svg.g.selectAll(".tick text")
            .attr("class","axis-label");


        this.svg.select(".x.axis").selectAll(".tick")
            .data(this.data)
            .on("mouseover", (d,i) => _this.handleLabelMouseOver(d,i,_this))
            .on("mouseout", (d,i) => _this.handleLabelMouseOut(d,i,_this))
            .on("click", (d,i) => _this.handleLabelMouseClick(d,i,_this));

        //Brush initialization
        this.brush = d3.brushX()
            .extent([[8,0],[this.width+this.margin.left+this.margin.right-8, this.height+this.margin.top]])
            .on("start brush", brushmoved)
            .on("end", brushend);

        this.gBrush = this.svg
            .append("g")
            .attr("class", "brush")
            .call(this.brush);

        this.gBrush.selectAll("rect")
            .attr("height", this.height);

        this.gBrush.selectAll(".resize").append("path").attr("d", resizePath);

        /**
         * Resizes the path according to if start or end
         * @param d   defines to resize the start or the end
         * @returns {string}
         */
        function resizePath(d) {
            let e = +(d.type == "e"),
                x = e ? 1 : -1,
                y = _this.height / 2;
            return "M" + (.5 * x) + "," + y
                + "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6)
                + "V" + (2 * y - 6)
                + "A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y)
                + "Z"
                + "M" + (2.5 * x) + "," + (y + 8)
                + "V" + (2 * y - 8)
                + "M" + (4.5 * x) + "," + (y + 8)
                + "V" + (2 * y - 8);
        }

        // Initializes a handle on both left and right side of the brush
        this.handle = this.gBrush.selectAll(".handle--custom")
            .data([{type: "w"}, {type: "e"}])
            .enter().append("path")
            .attr("class", "handle--custom")
            .attr("stroke", "#000")
            .attr("stroke-width", 1)
            .attr("cursor", "ew-resize")
            .attr("display", "none")
            .attr("d", resizePath);


        /**
         * Filters the data when the brush is moved
         */
        function brushend(...args) {
            brushmoved(...args,true);
        }

        /**
         * Filters the data when the brush is moved
         */
        function brushmoved() {
            let is_end = arguments[3] || false;
            let extent = d3.event.selection;

            //console.log("temp " + temp.key);
            if (extent == null || extent[0] === extent[1]) {
                _this.hideBrush(_this);
                _this.filter_active = false;
                _this.current_extent_labels = null;
                _this.resetFilter('active',_this);
                _this.selected_entries = [];
            } else {
                let startSelectionX = extent[0]-_this.margin.left;
                let endSelectionX = extent[1]-_this.margin.left;
                _this.handle.style("display", "block");

                let xs = [], widths = [];
                _this.svg.g.selectAll(".bar-active").datum(function() {
                    let x = +this.getAttribute("x");
                    let width = +this.getAttribute("width");
                    xs.push(x);
                    widths.push(width);
                });

                let selectedLabels = [];
                let selectedIndeces = [];
                _this.selected_entries = [];

                xs.forEach((x, i) => {
                    _this.data[i].filtered = false;
                    if((startSelectionX <= x && x <= endSelectionX) ||
                        (startSelectionX <= x+widths[i] && x+widths[i] <= endSelectionX) ||
                        (x < startSelectionX && startSelectionX<x+widths[i] && x < endSelectionX && endSelectionX<x+widths[i])){
                        selectedLabels.push(_this.data[i].key);
                        selectedLabels.push(_this.data[i].key);
                        _this.data[i].filtered = true;
                    }
                });

                let min = d3.min(selectedLabels);
                let max = d3.max(selectedLabels);

                let startLabel = typeof min === "undefined" ? null : min;
                let endLabel = typeof min === "undefined" ? null : max;

                if(startLabel != null && endLabel != null) {
                    _this.current_extent_labels = [startLabel-0.001,endLabel+0.001];
                    _this.old_extent_labels = _this.current_extent_labels;
                    _this.filter_active = true;
                    _this.filterOnExtent(_this.current_extent_labels,'active',_this);
                    _this.updateAll(is_end);
                } else if(_this.filters.active.start != null) {
                    _this.filter_active = false;
                    _this.resetFilter('active',_this);
                    _this.selected_entries = [];
                    _this.updateAll(true);
                }

                _this.bars_active.classed("selected", function() {
                    let x = this.x.animVal.value;
                    let width = this.width.animVal.value;
                    return (startSelectionX <= x &&  x <= endSelectionX) ||
                        (startSelectionX <= (x + width) && (x + width) <= endSelectionX) ||
                        (x < startSelectionX && startSelectionX < x+width && x < endSelectionX && endSelectionX < x+width);
                });


                _this.handle.attr("display", null).attr("transform", function (d, i) {
                    return "translate(" + [extent[i], -_this.height / 4] + ")";
                });
            }
        }
    }

    drawStatistic() {

    }

    updateStatistic() {

    }

    /* mouse events */
    /**
     * Handles events when hovering over an element with the mouse
     * @param d   element
     * @param i   index
     * @param _this   reference to this
     */
    handleLabelMouseOver(d, i, _this) {
        let cat = _this.preview_data[i];
        //console.log(cat);

        if((_this.current_extent_labels !== null && cat.key>=_this.current_extent_labels[0] && cat.key<=_this.current_extent_labels[1]) || _this.current_extent_labels === null ) {
            let startLabel = cat.key-0.001;
            let endLabel = cat.key+0.001;

            _this.old_extent_labels = _this.current_extent_labels !== null ? Array.from(_this.current_extent_labels) : null;
            _this.current_extent_labels = [startLabel, endLabel];

            _this.old_filtered_data = JSON.parse(JSON.stringify(_this.preview_data));

            _this.preview_data.forEach(el => el.filtered = false);
            cat.filtered = true;

            _this.hover_active = true;
            _this.filterOnExtent(_this.current_extent_labels,'preview',_this);
            _this.previewAll(false);
        }
    }

    /**
     * Event handler for when the mouse is out of an element
     * @param d   element
     * @param i   index
     * @param _this   reference to this
     */
    handleLabelMouseOut(d, i, _this) {
        let cat = _this.preview_data[i];
        //console.log(cat,i);

        if(_this.current_extent_labels !== null && _this.hover_active) {
            //console.log("test out");
            _this.preview_data = JSON.parse(JSON.stringify(_this.old_filtered_data));
            if (_this.old_extent_labels !== null) {
                //console.log("test out1");
                _this.filterOnExtent(_this.old_extent_labels, 'preview', _this);
                _this.current_extent_labels = Array.from(_this.old_extent_labels);
            } else {
                //console.log("test out2");
                _this.current_extent_labels = null;
                _this.resetFilter('preview', _this);
            }

            _this.hover_active = false;
            _this.previewAll(true);
        }
    }

    /**
     * Event handler for when clicking on an element with the mouse
     * @param d   element
     * @param i   index
     * @param _this   reference to this
     */
    handleLabelMouseClick(d, i, _this) {
        let cat = _this.data[i];
        //console.log(cat);

        let startLabel = cat.key-0.001;
        let endLabel = cat.key+0.001;


        if(!_this.filter_active) {
            //console.log("test click");
            _this.current_extent_labels = [startLabel, endLabel];
            _this.old_extent_labels = [startLabel, endLabel];
            _this.setBrush(cat.key,cat.key,_this);
            _this.bars_active.classed("selected", function(d,j) {
                return _this.data[j].key == cat.key;
            });

            _this.hover_active = false;
            _this.resetFilter('preview', _this);
            _this.previewAll(true);

            _this.filterOnExtent(_this.current_extent_labels,'active',_this);
            _this.filter_active = true;
            _this.updateAll(true);
        } else {
            //console.log("test click2");
            _this.current_extent_labels = null;
            _this.preview_data.forEach(el => el.filtered = false);
            this.handle.attr("display", "none");
            _this.bars_active.classed("selected", false);

            _this.filter_active = false;
            _this.resetFilter('active',_this);
            _this.selected_entries = [];
            _this.updateAll(true);
        }
    }

    /**
     * Set brush to extent of start label and end label
     * @param startLabel   defines the start label
     * @param endLabel   defines the end label
     * @param _this   reference of this
     */
    setBrush(startLabel,endLabel,_this) {
        let extent = [_this.x(startLabel)+_this.margin.left,_this.x(endLabel)+_this.x.bandwidth()+_this.margin.left];

        _this.gBrush.call(_this.brush.move, extent);

        _this.handle.attr("display", null).attr("transform", function (d, i) {
            return "translate(" + [extent[i], -_this.height / 4] + ")";
        });
    }

    /**
     * Hides the brush
     * @param _this   reference of this
     */
    hideBrush(_this) {
        _this.gBrush.selectAll(".selection").style("display", "none");
        _this.handle.style("display", "none");
        _this.bars_active.classed("selected", false);
    }

    /**
     * Filter depending on the extent of the brush
     * @param extent   defines the extent
     * @param type   defines the type
     * @param _this   reference to this
     */
    filterOnExtent(extent,type,_this) {
        _this.filters[type].updateFilter(extent[0], extent[1]);
        _this.filterDimensionOnDescriptionKey.bind(_this);
        _this.filterDimensionOnDescriptionKey(-1, [extent[0], extent[1]]);
    }
    /**
     * Resets the filter
     * @param type   defines the type
     * @param _this   reference to this
     */
    resetFilter(type,_this) {
        _this.filters[type].resetFilter();
        _this.resetFilterDimensionOnDescriptionKey(-1);
    }

    /**
     * Updates the chart
     */
    updateChart() {
        const _this = this;

        // update data of chart
        this.updateData(false,false);
        this.checkIfDataIsFiltered();

        //console.log(JSON.parse(JSON.stringify(this.data)));
        //console.log(JSON.parse(JSON.stringify(this.preview_data)));

        let data = this.filter_active ? this.filtered_data : this.data;

        if(!this.filter_active) {
            this.hideBrush(this);
        }

        //console.log(JSON.parse(JSON.stringify(data)));

        // Scale the range of the data in the domains
        let y_max = d3.max(data, function(d){ return d.value; });
        this.y.domain([0, y_max]);

        // update bars
        this.svg.g.selectAll(".bar-active")
            .data(data)
            .attr("y", (d) => _this.y(d.value))
            .attr("height", (d,i) => _this.height - _this.y(d.value));

        // update the y axis
        this.yAxis
            .scale(this.y);
        this.svg.g.selectAll(".y.axis.left")
            .call(this.yAxis);

        this.svg.g.selectAll(".grid")
            .call(d3.axisLeft(this.y)
                .ticks(this.tickSize)
                .tickSize(-_this.width)
                .tickFormat("")
            );

        // update labels
        this.svg.g.selectAll(".chart-data-label")
            .data(data)
            .text((d,i) => d.value)
            .attr("y", function(d, i) {
                return _this.height + _this.labelHeight + _this.innerTickSize;
            });
    }

    /**
     * Shows the preview information when hovering above the elements of the chart
     * @param reset
     */
    previewChart(reset) {
        const _this = this;

        // update data of chart
        this.updateData(true,reset);

        let preview_data = this.filter_active || this.hover_active ? this.filtered_data : this.preview_data;

        //console.log(JSON.parse(JSON.stringify(this.data)));
        //console.log(JSON.parse(JSON.stringify(this.preview_data)));
        //console.log(JSON.parse(JSON.stringify(preview_data)));

        //console.log(this.filter_active, this.hover_active,JSON.parse(JSON.stringify(preview_data)));

        // preview bars
        this.svg.g.selectAll(".bar-preview")
            .data(preview_data)
            .attr("y", (d) => _this.y(d.value))
            .attr("height", (d,i) => _this.height - (reset ? _this.y(0) : _this.y(d.value)));


        // preview labels
        this.svg.g.selectAll(".chart-data-label")
            .data(preview_data)
            .text((d,i) => d.value)
            .attr("y", function(d, i) {
                return _this.height + _this.labelHeight + _this.innerTickSize;
            });
    }
}