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