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