chart/chart-horizontal-bar.js

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

/**
 * Horizontal bar chart
 * @extends Chart
 */
class HorizontalBarChart 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 = 10;
        this.innerTickSize = 3;

        this.x = null;
        this.y = null;
        this.xAxisTop = null;
        this.xAxisBottom = null;
        this.yAxis = null;
        this.grid = null;
        this.labels = null;
        this.bars_active = null;
        this.bars_preview = null;

        this.tickSize = 3;


        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: 16,right: 8,bottom: 16,left: getLongestTextDimension(data.map(el=>el.title),this.labelFontSizeWeight).width+getLongestTextDimension(data.map(el=>el.value),this.labelFontSizeWeight).width+this.labelValuePadding+this.innerTickSize+5};
    }


    /**
     * 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.value - b.value || ('' + b.title).localeCompare(a.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 methods to update the chart and the statistics
     */
    update() {
        this.updateChart();
        this.updateStatistic();
    }

    /**
     * Calls a method to calculate the preview of the chart
     * @param reset
     */
    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);

        // set the ranges
        this.y = d3.scaleBand()
            .range([this.height, 0])
            .padding(0.1);

        this.x = d3.scaleLinear()
            .range([0, this.width]);

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

        this.yAxis = d3.axisLeft(this.y)
            .tickSizeInner(this.innerTickSize)
            .tickSizeOuter(0)
            .tickPadding(getLongestTextDimension(this.data.map(el=>el.value),this.labelFontSizeWeight).width+this.labelValuePadding);
        this.xAxisTop = d3.axisTop(this.x)
            .tickSizeInner(this.innerTickSize)
            .tickSizeOuter(0)
            .ticks(this.tickSize)
            .tickFormat(d3.format(".2s"));
        this.xAxisBottom = d3.axisBottom(this.x)
            .tickSizeInner(this.innerTickSize)
            .tickSizeOuter(0)
            .ticks(this.tickSize)
            .tickFormat(d3.format(".2s"));

        this.grid = this.svg.g.append("g")
            .attrs({
                class: "grid",
                transform: "translate(0, " + this.height + ")"
            })
            .call(d3.axisBottom(this.x)
                .ticks(this.tickSize)
                .tickSize(-_this.height)
                .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("width", (d) => _this.x(d.value))
            .attr("y", (d) => _this.y(d.title))
            .attr("height", _this.y.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(d) { return _this.x(_this.x.length-1); } )
            .attr("y", function(d) { return _this.y(d.title); })
            .attr("height", _this.y.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));

        this.labels = this.svg.g.append("g").attr("class","data-labels").selectAll(".data-label")
            .data(this.data)
            .enter()
            .append("text")
            .text(function(d) {
                return d.value;
            })
            .attr("class", "data-label")
            .attr("y", function(d, i) {
                return _this.y(d.title) + _this.y.bandwidth()/2 + 5;
            })
            .attr("x", function(d, i) {
                return -getWidth(d3.select(this)) - _this.innerTickSize - 2;
            })
            .attr("fill", "#969da3")
            .attr("font-size", "14px")
            .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 y axis
        this.svg.g.append("g")
            .attr("class", "y axis primary-axis")
            .call(this.yAxis);

        // add the y axis
        this.svg.g.append("g")
            .attrs({
                class: "x axis top secondary-axis"
            })
            .call(this.xAxisTop);
        this.svg.g.append("g")
            .attrs({
                class: "x axis bottom secondary-axis",
                transform: "translate(0, " + this.height + ")"
            })
            .call(this.xAxisBottom);

        let ticks = this.svg.g.selectAll(".primary-axis text")
            .data(this.data)
            .attr("class","axis-label")
            .classed("zeroed", (d,i) => d.value === 0);

        this.svg.select(".y.axis").selectAll(".tick")
            .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));

    }

    /**
     * Draws the statistics of the chart
     */
    drawStatistic() {
        const _this = this;
        if(!this.svg.g.selectAll(".statistic-container").empty()) {
            this.svg.g.selectAll(".statistic-container").remove();
        }

        this.svg.g.append("rect").attrs({
            x: -_this.margin.left,
            y: _this.height+1,
            width: _this.width+_this.margin.left+_this.margin.right+"px",
            height: "0.05px",
            class: "statistic-line"
        });

        let statistic_container = this.svg.g.append("g").attrs({class:"statistic-container",id: "relation-statistic",
            transform: (d,i) => "translate("+(-_this.tickSize)+","+(_this.height+_this.margin.bottom)+")"});

        let text_rows = this.description.length + " Rows";
        let text_legend = "#";

        statistic_container.append("text")
            .attrs({
                "text-anchor":"end",
                transform: (d,i) => "translate("+(-_this.tickSize)+",0)"
            })
            .text(text_legend);

        statistic_container.append("text")
            .attrs({
                id: "statistic-text",
                "text-anchor":"end",
                transform: (d,i) => "translate("+(-getLongestTextDimension(_this.data.map(el=>el.value),_this.labelFontSizeWeight).width-_this.labelValuePadding)+",0)"
            })
            .text(text_rows);
    }

    /**
     * Updates the statistics of the chart
     */
    updateStatistic() {
        let statistic_container = this.svg.g.select("#relation-statistic");
        let text_rows = this.description.length + " Rows";
        statistic_container.select("#statistic-text").text(text_rows);
    }

    /* 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.data[i];
        //console.log(JSON.parse(JSON.stringify(cat.value)));

        if(!cat.filtered && cat.value !== 0) {
            //console.log("test");
            _this.hovered[i] = true;
            this.filters.preview.addFilterOnDimensionIndex(cat.key - 1, 1, "AND");
            _this.filterDimensionOnDescriptionKey.bind(_this);
            _this.filterDimensionOnDescriptionKey(i, 1);
        }

        _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.data[i];
        //console.log(cat,i);
        //console.log(JSON.parse(JSON.stringify(cat.value)));



        if(!_this.selected[i] && !cat.filtered && cat.value !== 0) {
            _this.hovered[i] = false;
            this.filters.preview.removeFilterOnDimensionIndex(cat.key-1);
            _this.resetFilterDimensionOnDescriptionKey(i);
        }

        _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);

        if(!_this.selected[i] && cat.value !== 0) {
            cat.filtered = true;
            _this.selected[i] = true;
            this.filters.active.addFilterOnDimensionIndex(cat.key-1,1,"AND");
            _this.filterDimensionOnDescriptionKey.bind(_this);
            _this.filterDimensionOnDescriptionKey(i,1);
        } else {
            cat.filtered = false;
            _this.selected[i] = false;
            this.filters.active.removeFilterOnDimensionIndex(cat.key-1);
            _this.resetFilterDimensionOnDescriptionKey(i,1);
        }

        _this.updateAll(true);
    }

    /**
     * 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)));

        let data = this.data;

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

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

        // update the x axis
        this.xAxisTop
            .scale(this.x);
        this.xAxisBottom
            .scale(this.x);
        this.svg.g.selectAll(".x.axis.top")
            .call(this.xAxisTop);
        this.svg.g.selectAll(".x.axis.bottom")
            .call(this.xAxisBottom);

        this.svg.g.selectAll(".grid")
            .call(d3.axisBottom(this.x)
                .ticks(this.tickSize)
                .tickSize(-_this.height)
                .tickFormat("")
            );

        // update active labels
        this.svg.g.selectAll(".data-label")
            .data(data)
            .text(function(d) {
                return d.value;
            })
            .attr("x", function(d, i) {
                return -getWidth(d3.select(this)) - _this.innerTickSize - 2;
            });

        // set labels hovered
        this.svg.g.selectAll(".y.axis .axis-label")
            .data(data)
            .classed("selected", (d,i) => _this.hovered[i] || _this.selected[i])
            .classed("zeroed", (d,i) => d.value === 0);
    }

    /**
     * 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.preview_data;

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

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

        // preview labels
        this.svg.g.selectAll(".data-label")
            .data(preview_data)
            .text(function(d) {
                return d.value;
            })
            .attr("x", function(d, i) {
                return -getWidth(d3.select(this)) - _this.innerTickSize - 2;
            });

        // set labels hovered
        this.svg.g.selectAll(".y.axis .axis-label")
            .data(preview_data)
            .classed("selected", (d,i) => _this.hovered[i] ||_this.selected[i]);
    }
}