/*
column-graphs 1.0.0
Author Markus Tuominen, Finnish Social Science Data Archive
*/
/*
BSD 3-Clause License
Copyright (c) 2018, Finnish Social Science Data Archive (FSD) / University of Tampere
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/** @module columnGraphs **/
(function (root, factory) {
if(typeof define === "function" && define.amd) {
define(["d3"], function(d3){
return (root.cg = factory(d3));
});
}
else if(typeof module === "object" && module.exports) {
module.exports = (root.cg = factory(require("d3")));
}
else {
root.cg = factory(root.d3);
}
}(this, function(d3) {
/**
* Functions inside this namespace can be used outside of the module with cg.functionName().
* All the functions here call a private function of the same name in the columnGraphs module.
* Contains functions for initializing data and metadata, drawing graphs and helper functions like wrapping text or getting text width.
* <br />
* <br />
* Click the links to the private function to check the parameters and functionality description.
* @version 1.0.0
* @exports cg
* @namespace
*/
var cg = {
/**
* Calls private function {@link module:columnGraphs.optimizedResize.add}.
* @memberof cg
* @method addResizeFunc
*/
addResizeFunc:function(func){
optimizedResize.add(func);
},
/**
* Calls private function {@link module:columnGraphs.addTitle}.
* @memberof cg
* @method addTitle
*/
addTitle:function(title,className){
addTitle(title,className);
},
/**
* Calls private function {@link module:columnGraphs.drawCompareCategories}.
* @memberof cg
* @method drawCompareCategories
*/
drawCompareCategories:function(valueAs) {
drawCompareCategories(false,valueAs);
},
/**
* Calls private function {@link module:columnGraphs.drawStackedBars}.
* @memberof cg
* @method drawStackedBars
*/
drawStackedBars:function(valueAs){
drawStackedBars(valueAs);
},
/**
* Calls private function {@link module:columnGraphs.getArrayMax}.
* @memberof cg
* @method getArrayMax
*/
getArrayMax:function(numArray){
getArrayMax(numArray);
},
/**
* Calls private function {@link module:columnGraphs.getArrayMin}.
* @memberof cg
* @method getArrayMin
*/
getArrayMin:function(numArray){
getArrayMin(numArray);
},
/**
* Calls private function {@link module:columnGraphs.getAxisTextWidths}.
* @memberof cg
* @method getAxisTextWidths
*/
getAxisTextWidths:function(axis,degrees,textFontName,textFontSize){
getAxisTextWidths(axis,degrees,textFontName,textFontSize);
},
/**
* Calls private function {@link module:columnGraphs.getConf}.
* @memberof cg
* @method getConf
*/
getConf:function(name){
return getConf(name);
},
/**
* Calls private function {@link module:columnGraphs.getTextColor}.
* @memberof cg
* @method getTextColor
*/
getTextColor:function(hex){
getTextColor(hex);
},
/**
* Calls private function {@link module:columnGraphs.getTextWidth}.
* @memberof cg
* @method getTextWidth
*/
getTextWidth:function(text,fontName,fontSize){
getTextWidth(text,fontName,fontSize);
},
/**
* Calls private function {@link module:columnGraphs.initData}.
* @memberof cg
* @method initData
*/
initData:function(data,groupKey,valueKey){
initData(data,groupKey,valueKey);
},
/**
* Calls private function {@link module:columnGraphs.initMetadata}.
* @memberof cg
* @method initMetadata
*/
initMetadata:function(meta,choices,groups){
initMetadata(meta,choices,groups);
},
/**
* Calls private function {@link module:columnGraphs.initSvg}.
* @memberof cg
* @method initSvg
*/
initSvg:function(width,height,mainDiv){
initSvg(width,height,mainDiv);
},
/**
* Calls private function {@link module:columnGraphs.optimizedResize.remove}.
* @memberof cg
* @method removeResizeFunc
*/
removeResizeFunc:function(func){
optimizedResize.remove(func);
},
/**
* Calls private function {@link module:columnGraphs.roundOneDec}.
* @memberof cg
* @method roundOneDec
*/
roundOneDec:function(num){
roundOneDec(num);
},
/**
* Calls private function {@link module:columnGraphs.roundTwoDec}.
* @memberof cg
* @method roundTwoDec
*/
roundTwoDec:function(num){
roundTwoDec(num);
},
/**
* Calls private function {@link module:columnGraphs.setConf}.
* @memberof cg
* @method setConf
*/
setConf:function(name,value){
setConf(name,value);
},
/**
* Calls private function {@link module:columnGraphs.wrapText}.
* @memberof cg
* @method wrapText
*/
wrapText:function(text,width,fontSize,customClass){
wrapText(text,width,fontSize,customClass);
}
};
/** Object holding default values for config variables. Note: not all of these can be changed just by changing them here for example defaults.locale and instead they need to be changed with setConf().
* @private
* @type {function}
* @memberof module:columnGraphs
*/
var defaults = {
all: 'All',
colorChoices: d3.scaleOrdinal(d3.schemeCategory10),
colorCompare: "#9e9ac8",
colorHigher: "#74c476",
colorLower: "#fdae6b",
colorUnselected: "#f2f2f2",
dataCount: false,
font: 'sans-serif',
fontSize: 14,
fontSizeAxis: 12,
fontSizeLegend: 13,
graphs: [['Select graph','none'],['Stacked bars','stacked'],['Compare categories','compare']],
graphSelectEnabled: true,
height: 500,
locale: 'en-US',
mainDiv: 'cg-main',
margin: {top: 20, right: 20, bottom: 30, left: 50},
marginAuto: false,
marginExtra: 8,
marginOptions: {top: 20, right: 20, bottom: 20, left: 0},
marginSelect: 15,
none: 'None',
sortStrings: ['Default order','Ascending','Descending'],
valueAs: ['percentage','.0%',''],
values: true,
valuesTotal: true,
width: 600,
widthMin: 320,
widthOptions: 145,
widthScrollbar: 17
};
/**
* Config object for variables that can be changed with {@link module:columnGraphs.setConf} and retrieved with {@link module:columnGraphs.getConf}.
* @namespace
* @memberof module:columnGraphs
*/
var config = {
/**
* String for select option that selects all categories.
* @type {string}
* @default module:columnGraphs.defaults.all
* @memberof module:columnGraphs.config
*/
all: defaults.all,
/**
* Color used for the different choices (stacked bars). Has exception in {@link module:columnGraphs.setConf} so the given array of colors is automatically put in d3.scaleOrdinal().
* @type {Array}
* @memberof module:columnGraphs.config
*/
colorChoices: defaults.colorChoices,
/**
* Color used for the selected choice (compare groups).
* @type {string}
* @memberof module:columnGraphs.config
*/
colorCompare: defaults.colorCompare,
/**
* Color used for the choice that has higher value than selected (compare groups).
* @type {string}
* @memberof module:columnGraphs.config
*/
colorHigher: defaults.colorHigher,
/**
* Color used for the choice that has lower value than selected (compare groups).
* @type {string}
* @memberof module:columnGraphs.config
*/
colorLower: defaults.colorLower,
/**
* Color used for choices that are hidden because they are unselected (stacked bars, compare groups).
* @type {string}
* @memberof module:columnGraphs.config
*/
colorUnselected: defaults.colorUnselected,
/**
* Tells {@link module:columnGraphs.initData} whether to count total values or to look for 'Count' key to get total values.
* @type {boolean}
* @memberof module:columnGraphs.config
*/
dataCount: defaults.dataCount,
/**
* Font used for texts.
* @type {int}
* @memberof module:columnGraphs.config
*/
font: defaults.font,
/**
* Main font size used for most texts.
* @type {int}
* @memberof module:columnGraphs.config
*/
fontSize: defaults.fontSize,
/**
* Font size for axis texts. Default is 2px smaller than main font size.
* @type {int}
* @memberof module:columnGraphs.config
*/
fontSizeAxis: defaults.fontSizeAxis,
/**
* Font size for choice legend texts. Default is 1px smaller than main font size.
* @type {int}
* @memberof module:columnGraphs.config
*/
fontSizeLegend: defaults.fontSizeLegend,
/**
* Array containing arrays of strings for graph select. First element in array is select text and second element is select value.
* Default is [['Select graph','none'],['Stacked bars','stacked'],['Compare categories','compare']].
* @type {Array}
* @memberof module:columnGraphs.config
*/
graphs: defaults.graphs,
/**
* Enables select for changing graph type in the options div.
* @type {boolean}
* @memberof module:columnGraphs.config
*/
graphSelectEnabled: defaults.graphSelectEnabled,
/**
* Max height for main graph svg.
* @type {int}
* @memberof module:columnGraphs.config
*/
height: defaults.height,
/**
* Locale to use. Default is D3's default locale 'en-US'. Check {@link https://unpkg.com/d3-format/locale/} for a list of possible values.
* @type {string}
* @memberof module:columnGraphs.config
*/
locale: defaults.locale,
/**
* Id of the main div.
* @type {string}
* @memberof module:columnGraphs.config
*/
mainDiv: defaults.mainDiv,
/**
* Margins for main graph svg.
* @type {Object}
* @memberof module:columnGraphs.config
*/
margin: defaults.margin,
/**
* Automatically changes left and bottom margins to fit texts.
* @type {Object}
* @memberof module:columnGraphs.config
*/
marginAuto: defaults.marginAuto,
/**
* Extra left margin for columnGraphs-graph-svg, fixes it going outside of columnGraphs-main svg.
* @type {int}
* @memberof module:columnGraphs.config
*/
marginExtra: defaults.marginExtra,
/**
* Margins for options div.
* @type {int}
* @memberof module:columnGraphs.config
*/
marginOptions: defaults.marginOptions,
/**
* Margins for options div.
* @type {int}
* @memberof module:columnGraphs.config
*/
marginSelect: defaults.marginSelect,
/**
* String for select option that doesn't choose any of the groups or categories.
* @type {string}
* @memberof module:columnGraphs.config
*/
none: defaults.none,
/**
* Array containing three strings for sort select. Default is ['Default order','Ascending','Descending'].
* @type {Array}
* @memberof module:columnGraphs.config
*/
sortStrings: defaults.sortStrings,
/**
* Array that contains type of value for y, D3 format and optional text. To change from showing percentage on y to absolute values with decimal notation (rounded to significant digits) would be ['absolute','r'].
* Third value in array is used to add text after the value, for example set ['absolute','d',' pcs'] would be decimal notation (rounded to integers) and ' pcs' added after the value.
* @type {Array}
* @memberof module:columnGraphs.config
*/
valueAs: defaults.valueAs,
/**
* True to show value texts on graph.
* @type {boolean}
* @memberof module:columnGraphs.config
*/
values: defaults.values,
/**
* True to show total value texts on stacked bars graph.
* @type {boolean}
* @memberof module:columnGraphs.config
*/
valuesTotal: defaults.valuesTotal,
/**
* Max width for main graph svg. Has exception in {@link module:columnGraphs.setConf} so originalWidth also gets updated.
* @type {int}
* @memberof module:columnGraphs.config
*/
width: defaults.width,
/**
* Minimum width for main graph svg.
* @type {int}
* @memberof module:columnGraphs.config
*/
widthMin: defaults.widthMin,
/**
* Max width for options svg.
* @type {int}
* @memberof module:columnGraphs.config
*/
widthOptions: defaults.widthOptions,
/**
* Width of the scrollbar.
* @type {int}
* @memberof module:columnGraphs.config
*/
widthScrollbar: defaults.widthScrollbar
};
/**
* Set a new value for a config variable by name.
* @private
* @memberof module:columnGraphs
* @param {string} name - Name of the config variable to change.
* @param {*} value - New value for the config variable. Use value 'default' to set to default value.
*/
function setConf(name,value){
if(config.hasOwnProperty(name)){
if(value === 'default'){
config[name] = defaults[name];
}
else{
if(name === 'colorChoices'){
config[name] = d3.scaleOrdinal(value);
}
else{
config[name] = value;
}
}
if(name === 'width'){
originalWidth = value+config.margin.left+config.margin.right;
}
else if(name === 'valueAs'){
config.valueAs = config.valueAs || ['percentage','.0%'];
config.valueAs[1] = config.valueAs[1] || '';
config.valueAs[2] = config.valueAs[2] || '';
}
else if(name === 'locale'){
d3.json("https://unpkg.com/d3-format/locale/"+config.locale+".json", function(error, locale) {
if (error) throw error;
d3.formatDefaultLocale(locale);
redraw();
});
}
if(svg){
initSvg();
}
}
else{
console.log("setConf(): Config with the name '"+name+"' was not found");
}
}
/**
* Get the current value of a config variable by name.
* @private
* @memberof module:columnGraphs
* @param {string} name - Name of the config variable.
* @returns {*} Value of the config variable.
*/
function getConf(name){
return config[name];
}
/**
* Adds title element h1 for the graph.
* @private
* @memberof module:columnGraphs
* @param {string} title - Text for the title element.
* @param {string}[className='cg-title'] Class for the title element.
*/
function addTitle(title,className){
var mainDiv = document.getElementById(config.mainDiv);
try{
if(!mainDiv){ throw "Div '"+config.mainDiv+"' not found"; }
}
catch(err){
console.log("addTitle(): "+err);
return;
}
var titleEl = document.createElement('h1');
titleEl.innerHTML = title;
className = className || 'cg-title';
titleEl.classList.add(className);
mainDiv.insertBefore(titleEl, mainDiv.firstChild);
}
/**
* Creates a select for choosing which graph type to show.
* @private
* @memberof module:columnGraphs
*/
function createGraphSelect(){
try{
if(!groups || !choices){ throw "Metadata not initialized"; }
}
catch(err){
console.log("createGraphSelect(): "+err);
return;
}
var optionsDiv = document.getElementById('cg-options-selects');
var selectGraphDiv = document.createElement('div');
selectGraphDiv.id = "selectGraphDiv";
selectGraphDiv.style.display = config.graphSelectEnabled === true ? "block" : "none";
selectGraphDiv.style.marginBottom = config.marginSelect+"px";
var graphsLabel = document.createElement('label');
var selectGraph = document.createElement('select');
selectGraph.id = 'selectGraph';
config.graphs.forEach(function (value, i) {
var option = document.createElement("option");
option.value = value[1].toString();
option.text = value[0];
if(value[1].toString().toLowerCase() === selectedGraph.toLowerCase()){
option.setAttribute("selected","true");
}
selectGraph.appendChild(option);
});
graphsLabel.appendChild(selectGraph);
selectGraphDiv.appendChild(graphsLabel);
optionsDiv.appendChild(selectGraphDiv);
// Change the selected group with dropdown
selectGraph.addEventListener("change", function() {
selectedGraph = selectGraph.options[selectGraph.selectedIndex].value;
hiddenChoices = [];
d3.selectAll('.cg-graph').remove();
redraw();
});
createGroupSelect();
}
/**
* Creates a select for choosing which group to show on graph.
* @private
* @memberof module:columnGraphs
*/
function createGroupSelect(){
var optionsDiv = document.getElementById('cg-options-selects');
var groupsLabel = document.createElement('label');
var selectGroupDiv = document.createElement('div');
selectGroupDiv.id = "selectGroupDiv";
selectGroupDiv.style.marginBottom = config.marginSelect+"px";
var selectGroup = document.createElement('select');
selectGroup.id = 'selectGroup';
Object.keys(groups).forEach(function (value, i) {
var option = document.createElement("option");
option.value = value;
option.text = value;
if(i === 0){
option.setAttribute("selected","true");
}
selectGroup.appendChild(option);
});
groupsLabel.appendChild(selectGroup);
selectGroupDiv.appendChild(groupsLabel);
optionsDiv.appendChild(selectGroupDiv);
// Change the selected group with dropdown
selectGroup.addEventListener("change", function() {
d3.selectAll('.cg-graph').remove();
changeGroup();
initData(dataOriginal);
redraw();
});
createCatgrySelect();
createSortSelect();
}
/**
* Creates a select for choosing which catgry of the group is selected.
* @private
* @memberof module:columnGraphs
*/
function createCatgrySelect(){
var optionsDiv = document.getElementById('cg-options-selects');
var groupsLabel = document.createElement('label');
var selectCatgryDiv = document.createElement('div');
selectCatgryDiv.id = "selectCatgryDiv";
selectCatgryDiv.style.marginBottom = config.marginSelect+"px";
var selectCatgry = document.createElement('select');
selectCatgry.id = 'selectCatgry';
groupsLabel.appendChild(selectCatgry);
selectCatgryDiv.appendChild(groupsLabel);
optionsDiv.appendChild(selectCatgryDiv);
changeGroup();
// Change the selected catgry with dropdown
selectCatgry.addEventListener("change", function() {
var selectText = selectCatgry.options[selectCatgry.selectedIndex].text;
switch(selectedGraph) {
case 'compare':
for(var i=0, j=d3.selectAll(".axis-x .tick text")._groups[0].length; i<j; i+=1){
// Check x-axis ticks for the catgry selected in dropdown
if(d3.selectAll(".axis-x .tick text")._groups[0][i].innerHTML === selectText){
compareToIndex = parseInt(i);
redraw();
break;
}
}
break;
case 'stacked':
redraw();
break;
case 'none':
redraw();
break;
default:
}
});
}
/**
* Adds option to select or removes option from select.
* @private
* @memberof module:columnGraphs
* @param {string} - Id of select.
* @param {string} - Text of option to add/remove.
* @param {boolean} - True to remove, otherwise add.
* @param {boolean} - True to select if added.
* @returns {boolean} True if option was removed and false if not.
*/
function addRemoveOption(targetSelect,optionText,remove,selectOption){
var select = document.getElementById(targetSelect);
var optionExists;
if(select.options.length){
optionExists = select.options[0].text === optionText;
}
else{
optionExists = false;
}
var returnValue = false;
if(remove !== true && !optionExists){
var option = document.createElement("option");
option.value = optionText;
option.text = optionText;
if(selectOption === true){
option.setAttribute("selected","true");
if(optionText === config.all){
compareToIndex = config.all;
}
}
select.insertBefore(option, select.firstChild);
return returnValue;
}
else if(remove === true && optionExists){
if(select.options[select.selectedIndex].text === optionText){
returnValue = true;
}
select.remove(0);
changeGroup();
return returnValue;
}
}
/**
* Changes the categories to reflect selected group.
* @private
* @memberof module:columnGraphs
*/
function changeGroup(){
var selectGroup = document.getElementById('selectGroup');
var group = groups[selectGroup.options[selectGroup.selectedIndex].text];
var groupLabels = group ? Object.values(group) : [config.none];
var selectCatgry = document.getElementById('selectCatgry');
selectCatgry.options.length = 0;
for(var key in groupLabels) {
if(groupLabels.hasOwnProperty(key)) {
var option = document.createElement("option");
option.value = key;
option.text = groupLabels[key];
if(parseInt(key) === 0){
option.setAttribute("selected","true");
}
selectCatgry.appendChild(option);
}
}
}
/**
* Creates a select for choosing the sort for graph.
* Sort options: default (by group key), ascending (value), descending (value).
* @private
* @memberof module:columnGraphs
*/
function createSortSelect(){
var optionsDiv = document.getElementById('cg-options-selects');
var sortLabel = document.createElement('label');
var selectSortDiv = document.createElement('div');
selectSortDiv.id = "selectSortDiv";
selectSortDiv.style.marginBottom = config.marginSelect+"px";
var selectSort = document.createElement('select');
selectSort.id = 'selectSort';
config.sortStrings.forEach(function (value, i) {
var option = document.createElement("option");
option.value = (parseInt(i)-1).toString();
option.text = value;
if(i === 0){
option.setAttribute("selected","true");
}
selectSort.appendChild(option);
});
sortLabel.appendChild(selectSort);
selectSortDiv.appendChild(sortLabel);
optionsDiv.appendChild(selectSortDiv);
var resizeEvent = window.document.createEvent('UIEvents');
resizeEvent.initUIEvent('resize', true, false, window, 0);
window.dispatchEvent(resizeEvent);
selectSort.addEventListener("change", function() {
redraw();
});
}
/**
* Updates select's selected option according to parameters.
* @private
* @memberof module:columnGraphs
* @param {string} selectId - Id of select.
* @param {string} updateBy - Value or text string to check for.
* @param {boolean} checkText - True checks the option texts. False or undefined checks the option values.
*/
function updateSelect(selectId,updateBy,checkText){
var select = document.getElementById(selectId);
var toCheck = checkText === true ? 'text' : 'value';
for(i=0, j=select.options.length; i<j; i+=1) {
if(select.options[i][toCheck] === updateBy){
select.selectedIndex = i;
break;
}
}
}
/**
* Disable or enable select.
* @private
* @memberof module:columnGraphs
* @param {string} selectId - Id of select.
* @param {string}[disable=false] - True to disable and false to enable.
*/
function disableEnableSelect(selectId,disable){
if(selectId){
disable = disable || false;
document.getElementById(selectId).disabled = disable;
}
}
/**
* Redraws the graph depending on selected graph.
* @private
* @memberof module:columnGraphs
* @param {boolean}[categoryFromSelect] True selects category from select and false updates select.
*/
function redraw(categoryFromSelect){
switch(selectedGraph) {
case 'compare':
sortCompare(categoryFromSelect);
break;
case 'stacked':
sortStacked();
break;
case 'none':
x.rangeRound([0, config.width]).domain(['']);
xAxis.call(d3.axisBottom(x));
break;
default:
}
}
/** Svg element for main graph created with D3.
* @private
* @type {SVGElement}
* @memberof module:columnGraphs
*/
var svg;
/** Svg element for options created with D3.
* @private
* @type {SVGElement}
* @memberof module:columnGraphs
*/
var svgOptions;
/** G element inside main svg created with D3, affected by margins.
* @private
* @type {SVGGroupElement}
* @memberof module:columnGraphs
*/
var g;
/** Options for horizontal axis created with D3.
* @private
* @type {function}
* @memberof module:columnGraphs
*/
var x;
/** Horizontal axis created with D3.
* @private
* @type {SVGAxisElement}
* @memberof module:columnGraphs
*/
var xAxis;
/** Options for vertical axis created with D3.
* @private
* @type {function}
* @memberof module:columnGraphs
*/
var y;
/** Vertical axis created with D3.
* @private
* @type {SVGAxisElement}
* @memberof module:columnGraphs
*/
var yAxis;
/** Height for options svg. Calculated and set at the end of {@link module:columnGraphs.addLegend}.
* @private
* @type {int}
* @memberof module:columnGraphs
*/
var heightOptions = 0;
/**
* Initializes the required divs and svgs for graph and options.
* @private
* @memberof module:columnGraphs
* @param {int}[width=600] - Width for main svg that is used for graph.
* @param {int}[height=500] - Height for main svg that is used for graph
* @param {string}[mainDiv='cg-main'] - Id for the main div (without #).
*/
function initSvg(width,height,mainDiv){
if(width !== '' && width !== ' ' && typeof width !== 'undefined'){
setConf('width',width + config.margin.left + config.margin.right);
}
if(height !== '' && height !== ' ' && typeof height !== 'undefined'){
setConf('height',height + config.margin.top + config.margin.bottom);
}
if(mainDiv !== '' && mainDiv !== ' ' && typeof mainDiv !== 'undefined'){
setConf('mainDiv',mainDiv);
}
var chartDiv = document.getElementById(config.mainDiv);
try{
if(!chartDiv){ throw "Div '"+config.mainDiv+"' not found"; }
}
catch(err){
console.log("initSvg(): "+err);
return;
}
if(svg){
chartDiv.innerHTML = "";
}
// Get position of chart div to add its left to options div
var chartDivRect = chartDiv.getBoundingClientRect();
var optionsDiv = document.getElementById('cg-options');
if(!optionsDiv){
optionsDiv = document.createElement('div');
optionsDiv.id = "cg-options";
optionsDiv.classList.add('cg-options');
chartDiv.append(optionsDiv);
}
optionsDiv.style.display = "inline-block";
optionsDiv.style.verticalAlign = "top";
optionsDiv.style.marginTop = config.marginOptions.top+"px";
optionsDiv.style.marginBottom = config.marginOptions.bottom+"px";
// Add div inside options div for other options besides svg
var otherOptionsDiv = document.getElementById('cg-options-selects');
if(!otherOptionsDiv){
otherOptionsDiv = document.createElement('div');
otherOptionsDiv.id = 'cg-options-selects';
optionsDiv.append(otherOptionsDiv);
}
otherOptionsDiv.style.marginRight = config.marginOptions.right+"px";
otherOptionsDiv.style.display = "inline-block";
otherOptionsDiv.style.verticalAlign = "top";
// Select container divs with d3
chartDiv = d3.select('#'+config.mainDiv);
optionsDiv = d3.select('#cg-options');
svg = chartDiv
.insert("svg","#cg-options")
.attr("id","cg-main-svg")
.attr("width", config.width + config.margin.left + config.margin.right)
.attr("height", config.height + config.margin.top + config.margin.bottom);
svgOptions = optionsDiv
.append("svg")
.attr("id","cg-options-svg")
.attr("width", config.widthOptions)
.attr("height", heightOptions);
g = svg.append("g")
.attr("id","cg-graph-svg")
.attr("transform", "translate(" + (config.margin.left+config.marginExtra) + "," + config.margin.top + ")");
x = d3.scaleBand()
.rangeRound([0, config.width])
.padding(0.25)
.align(0.5);
y = d3.scaleLinear()
.rangeRound([config.height, 0])
.domain([0, 1]);
g.append("g")
.attr("class", "axis axis-x")
.attr("transform", "translate(0," + config.height + ")")
.style("font-size", config.fontSizeAxis+"px")
.call(d3.axisBottom(x));
xAxis = d3.select('.axis-x');
g.append("g")
.attr("class", "axis axis-y")
.style("font-size", config.fontSizeAxis+"px")
.call(d3.axisLeft(y).ticks(10, "%"));
yAxis = d3.select('.axis-y');
createGraphSelect();
}
/** Contains keys and values for choices, e.g. {1: "Agree", 2: "Disagree"}.
* @private
* @type {Object}
* @memberof module:columnGraphs
*/
var choices;
/** Contains keys and values for groups. Value of a group is another object containing keys and values for categories of the group.
* For example {Age: {1: "15-19", 2: "20-24"}, Gender: {1: "Male", 2: "Female"}}.
* @private
* @type {Object}
* @memberof module:columnGraphs
*/
var groups;
/**
* Initializes the metadata to be used in graphs.
* @private
* @memberof module:columnGraphs
* @param {Object} metaInitial - Metadata in JSON format, for example {"Choices":{"1":"Agree","2":"Disagree"},"Groups":{"Age":{"1":"15-19v","2":"20-24v"}}}
* @param {string}[choicesString='Choices'] - Name of the key that specifies the choices/answers in JSON file.
* @param {string}[groupsString='Groups'] - Name of the key that specifies the groups/categories in JSON file.
*/
function initMetadata(metaInitial,choicesString,groupsString){
choicesString = choicesString || 'Choices';
groupsString = groupsString || 'Groups';
choices = metaInitial[choicesString] || { All: config.all };
groups = metaInitial[groupsString] || { None: {none: config.none} };
if(Object.keys(groups).length === 0 && groups.constructor === Object){
groups = { None: {none: config.none} };
}
}
/** Original data in JSON format, for example [{"value": 2,"Age": 11},{"value": 4,"Age": 4}].
* @private
* @type {Array}
* @memberof module:columnGraphs
*/
var dataOriginal;
/** Cleaned, nested and stacked data to be used for drawing graphs. Contains all the needed data to draw even complicated graphs like compare groups.
* Data is an array of categories of a group with each category being an array of choices for that category.
* For example:<br />
* [[{"count": 7,"totalAll": 1160,"total": 72,"catgryLabel": "15-19","catgryKey": "1","choiceLabel": "Agree","choiceKey": "1","y0": 0,"y1": 7},<br />
* { "count": 16,"totalAll": 1160,"total": 72,"catgryLabel": "15-19","catgryKey": "1","choiceLabel": "Disagree","choiceKey": "2","y0": 7,"y1": 23}],<br />
* [{"count": 10,"totalAll": 1160,"total": 70,"catgryLabel": "20-24","catgryKey": "2","choiceLabel": "Agree","choiceKey": "1","y0": 0,"y1": 10},<br />
* {"count": 13,"totalAll": 1160,"total": 70,"catgryLabel": "20-24","catgryKey": "2","choiceLabel": "Disagree","choiceKey": "2","y0": 10,"y1": 23}]]
* @private
* @type {Array}
* @memberof module:columnGraphs
*/
var data;
/**
* Initializes the data to be used in graphs.
* @private
* @memberof module:columnGraphs
* @param {Object} dataInitial - Data in JSON format, for example [{"value": 2,"Age": 11},{"value": 4,"Age": 4}].
* @param {string}[groupKey=Object.keys(groups)[0]] - Name of the key that specifies the selected group in JSON file.
* @param {string}[valueKey='Value'] - Name of the key that specifies the actual data value in JSON file.
*/
function initData(dataInitial,groupKey,valueKey){
if(dataInitial && dataInitial !== dataOriginal){
dataOriginal = dataInitial;
}
if(document.getElementById('selectGroup') && !groupKey){
var select = document.getElementById("selectGroup");
groupKey = select.options[select.selectedIndex].value;
}
data = [];
var dataTemp = [];
var groupKeys = groups ? Object.keys(groups) : [];
groupKey = groupKey || groupKeys[0];
var choiceKeys = choices ? Object.keys(choices) : [];
valueKey = valueKey || 'Value';
var noneKey = '1';
if(groupKeys.indexOf(groupKey) === -1 && groupKey !== config.none){
console.log("initData(): Group key not found in metadata");
return;
}
// Deep copy original data
dataCopy = JSON.parse(JSON.stringify(dataOriginal));
// Remove null values or values that don't have correct ValueKey from dataCopy
for(var i=0; i<dataCopy.length; i+=1){
if(dataCopy[i][valueKey] === ' ' || dataCopy[i][valueKey] === '' || dataCopy[i][valueKey] === 'null' ||
dataCopy[i][valueKey] === null || typeof dataCopy[i][valueKey] === 'undefined' || choiceKeys.indexOf(dataCopy[i][valueKey].toString()) === -1){
dataCopy.splice(i, 1);
i-=1;
}
}
var totalAll = config.dataCount === true ? dataCopy.reduce(function (a, b) { return a + b.Count; }, 0) : dataCopy.length;
if(totalAll === 0){
totalAll = 1;
}
dataTemp = d3.nest()
.key(function (d) { return d[groupKey] || noneKey; })
.key(function (d) { return d[valueKey]; })
.rollup(function (d) {
if(config.dataCount === true){
return {
count: d.reduce(function (a, b) { return a + b.Count; }, 0),
totalAll: totalAll
};
}
else{
return {
count: d.length,
totalAll: totalAll
};
}
})
.object(dataCopy);
var dataToStack = Object.values(d3.nest()
.key(function (d) { return d[groupKey] || noneKey; })
.key(function (d) { return d[valueKey]; })
.rollup(function (d) {
if(config.dataCount === true){
return d.reduce(function (a, b) { return a + b.Count; }, 0);
}
else{
return d.length;
}
})
.entries(dataCopy));
var dataStackedTemp = [];
for(var j=0, m=dataToStack.length; j<m; j+=1){
var objTemp = {group: dataToStack[j].key};
for(var k=0, n=dataToStack[j].values.length; k<n; k+=1){
objTemp[dataToStack[j].values[k].key] = dataToStack[j].values[k].value;
}
dataStackedTemp.push(objTemp);
}
var dataStacked = d3.stack()
.keys(Object.keys(choices))
(dataStackedTemp);
var lookup = {};
for(var o=0, q=dataStacked.length; o<q; o+=1) {
lookup[dataStacked[o].key] = dataStacked[o];
var tempObj = {};
for(var p=0, r=dataStacked[o].length; p<r; p+=1){
tempObj[dataStacked[o][p].data.group] = lookup[dataStacked[o].key][p];
}
lookup[dataStacked[o].key] = tempObj;
}
var catgryKeys = groupKey === config.none ? noneKey : Object.keys(groups[groupKey]);
// Count total of each choice
var totals = [];
var catgryKey;
var choiceKey;
var choiceKeyPrev;
for(var c=0; c<catgryKeys.length; c+=1){
catgryKey = catgryKeys[c];
if(dataTemp.hasOwnProperty(catgryKey)) {
for(choiceKey in dataTemp[catgryKey]) {
if(dataTemp[catgryKey].hasOwnProperty(choiceKey)) {
if(!totals[catgryKey]){
totals[catgryKey] = dataTemp[catgryKey][choiceKey].count;
}
else{
totals[catgryKey] += dataTemp[catgryKey][choiceKey].count;
}
choiceKeyPrev = choiceKey;
}
}
}
else{
dataTemp[catgryKey] = {};
totals[catgryKey] = 1;
}
choiceKeyPrev = undefined;
}
// Add total of each choice, add missing choices, add label texts and add choice texts/keys
var currentChoiceKey;
var index;
var catgryKeyIndex = 0;
for(var d=0; d<catgryKeys.length; d+=1){
catgryKey = catgryKeys[d];
for(choiceKey in choices) {
if(choices.hasOwnProperty(choiceKey)) {
if(dataTemp.hasOwnProperty(catgryKey) && dataTemp[catgryKey].hasOwnProperty(choiceKey)){
dataTemp[catgryKey][choiceKey].total = totals[catgryKey];
dataTemp[catgryKey][choiceKey].catgryLabel = groups[groupKey] ? groups[groupKey][catgryKey] : '';
dataTemp[catgryKey][choiceKey].catgryKey = catgryKey;
dataTemp[catgryKey][choiceKey].choiceLabel = choices[choiceKey];
dataTemp[catgryKey][choiceKey].choiceKey = choiceKey;
dataTemp[catgryKey][choiceKey].y0 = lookup[choiceKey][catgryKey][0];
dataTemp[catgryKey][choiceKey].y1 = lookup[choiceKey][catgryKey][1];
}
else{
var catgryLabel = groups[groupKey] ? groups[groupKey][catgryKey] : '';
var y0 = typeof choiceK === 'undefined' ? 0 : lookup[choiceKey][catgryKey][0];
var y1 = typeof choiceK === 'undefined' ? 0 : lookup[choiceKey][catgryKey][1];
dataTemp[catgryKey][choiceKey] = {count: 0, totalAll: totalAll, total: totals[catgryKey], catgryLabel: catgryLabel, catgryKey: catgryKey, choiceLabel: choices[choiceKey], choiceKey: choiceKey, y0: y0, y1: y1};
}
}
}
data.push(Object.values(Object.values(dataTemp)[catgryKeyIndex]));
catgryKeyIndex+=1;
}
}
/** Currently selected graph type. Valid values are 'none', 'stacked' and 'compare'.
* @private
* @type {string}
* @memberof module:columnGraphs
*/
var selectedGraph = 'none';
/** Array of choices that are hidden on the graph either because the function requires it or user has deselected them by clicking on the legend rectangles/text.
* @private
* @type {Array}
* @memberof module:columnGraphs
*/
var hiddenChoices = [];
/**
* Creates a stacked bars graph.
* @private
* @memberof module:columnGraphs
* @param {array}[valueAs] Changes graph from percentage to absolute values if given value ['absolute','']. Second string in array is D3 format for y axis. Works exactly same as calling setConf('valueAs',['absolute','']) before drawing.
*/
function drawStackedBars(valueAs){
try{
if(data.length === 0){ throw "Data not set"; }
else if(!svg){ throw "Svg not initialized"; }
}
catch(err){
console.log("drawStackedBars(): " + err);
return;
}
if(valueAs){
config.valueAs = valueAs;
}
selectedGraph = 'stacked';
if(config.graphSelectEnabled === true){
updateSelect('selectGraph',selectedGraph);
}
addRemoveOption('selectGroup',config.none);
var selectCatgry = document.getElementById('selectCatgry');
var selectCatgryText = selectCatgry.options[selectCatgry.selectedIndex].text;
if(selectCatgryText !== config.none){
addRemoveOption('selectCatgry',config.all,'',true);
selectCatgryText = selectCatgry.options[selectCatgry.selectedIndex].text;
}
var selectCatgryValue = selectCatgry.options[selectCatgry.selectedIndex].value;
d3.selectAll('.cg-graph').remove();
var choiceKeys = Object.keys(choices);
var selectGroupText = document.getElementById('selectGroup').options[document.getElementById('selectGroup').selectedIndex].text;
var groupLabels = groups[selectGroupText];
var catgryFirst = data[0][0].catgryKey;
var catgryLast = data[data.length-1][0].catgryKey;
var noneOrAll = selectCatgryText !== config.none && selectCatgryText !== config.all ? false : true;
var dataTemp = data;
var xDomain = dataTemp.map(function(value,index) { return value[0].catgryLabel; });
if(!noneOrAll){
xDomain = [Object.values(groupLabels)[selectCatgryValue]];
data.forEach(function(value,index) {
if(xDomain[0] === value[0].catgryLabel){
dataTemp = [data[index]];
}
});
}
var labelKeys = noneOrAll ? data.map(function(value,index) { return value[0].catgryKey; }) : [Object.keys(groupLabels)[selectCatgryValue]];
var totalValue = [];
for(var i=0, k=labelKeys.length; i<k; i+=1){
totalValue[i] = 0;
}
for(var a=0, c=choiceKeys.length; a<c; a+=1){
for(var b=0, d=labelKeys.length; b<d; b+=1){
if(hiddenChoices.indexOf(choiceKeys[a].toString()) === -1){
if(config.valueAs[0] === 'absolute'){
totalValue[b] += dataTemp[b][a].count;
}
else{
if(typeof svg.select('#polygon_'+choiceKeys[a]+'_'+labelKeys[b])._groups[0][0] === 'undefined'){
totalValue[b] += dataTemp[b][a].count/dataTemp[b][a].total;
}
else{
totalValue[b] += parseInt(svg.select('#polygon_'+choiceKeys[a]+'_'+labelKeys[b]).attr('height'))/y(0);
}
}
}
if(a === choiceKeys.length-1){
totalValue[b] = config.valueAs[0] === 'absolute' ? roundOneDec(totalValue[b]) : roundOneDec(totalValue[b]*100);
}
}
}
// Get max domain for y axis
var maxDomainY = config.valueAs[0] === 'absolute' && d3.max(totalValue.map(function(value,index) { return value; })) !== 0 ? d3.max(totalValue.map(function(value,index) { return value; }))*1.1 : 1;
y
.rangeRound([config.height, 0])
.domain([0, maxDomainY]);
yAxis.call(d3.axisLeft(y).ticks(10).tickFormat(function(d, i){ return d3.format(config.valueAs[1])(d)+config.valueAs[2]; }));
checkYAxisTextWidths();
var padding = selectCatgryText !== config.all ? 0.5 : 0.25;
x
.rangeRound([0, config.width])
.padding(padding)
.domain(xDomain);
checkXAxisTextWidths();
var container = g.selectAll(".container")
.data(dataTemp)
.enter().append("g")
.attr("class", "cg-graph container");
var bars = container.append("g")
.attr("class", "cg-graph container-bars");
var prevHeight;
bars.selectAll("rect")
.data(function (d) { return d; })
.enter().append("rect")
.attr("id",function (d,i) { return noneOrAll ? "polygon_"+d.choiceKey+"_"+d.catgryKey : "polygon_"+d.choiceKey; })
.attr("y", function (d,i) {
if(i === 0){
prevHeight = 0;
}
if(hiddenChoices.indexOf(choiceKeys[i].toString()) !== -1){
prevHeight += config.valueAs[0] === 'absolute' ? y(d.y0)-y(d.y1) : y(d.y0/d.total)-y(d.y1/d.total);
}
else{
return config.valueAs[0] === 'absolute' ? y(d.y1)+prevHeight : y(d.y1/d.total)+prevHeight;
}
})
.attr("style",function(d,i){
var visibility = hiddenChoices.indexOf(choiceKeys[i].toString()) === -1 ? "visible" : "hidden";
return "visibility:"+visibility+";fill:"+config.colorChoices(i)+";";
})
.attr("height", function (d,i){ return config.valueAs[0] === 'absolute' ? y(d.y0)-y(d.y1) : y(d.y0/d.total)-y(d.y1/d.total); })
.attr("width",function(d){ return x.bandwidth(); })
.attr("x", function (d) { return x(d.catgryLabel); });
if(config.values){
var values = container.append("g")
.attr("class", "cg-graph container-values");
values.selectAll(".values")
.data(function (d) { return d; })
.enter().append("text")
.attr("class","values")
.attr("dy",".36em")
.attr("fill",function(d,i){
return getTextColor(config.colorChoices(i));
})
.text(function(d){ return config.valueAs[0] === 'absolute' ? (d.y0-d.y1)*-1 : roundOneDec((d.y0/d.total-d.y1/d.total)*-100); })
.attr("style",function(d,i){
var visibility = (hiddenChoices.indexOf(choiceKeys[i].toString()) === -1) &&
(config.valueAs[0] === 'absolute' && (roundOneDec((d.y0-d.y1)*-1) > 1) || (config.valueAs[0] !== 'absolute' && roundOneDec((d.y0/d.total-d.y1/d.total)*-100) > 1)) ? "visible" : "hidden";
return "visibility: "+visibility+"; font-size: "+config.fontSize+"px;";
})
.attr("transform", function(d,i){
if(i === 0){
prevHeight = 0;
}
if(hiddenChoices.indexOf(choiceKeys[i].toString()) !== -1){
prevHeight += config.valueAs[0] === 'absolute' ? y(d.y0)-y(d.y1) : y(d.y0/d.total)-y(d.y1/d.total);
}
var textWidth = config.valueAs[0] === 'absolute' ? getTextWidth(roundOneDec((d.y0-d.y1)*-1),config.font,config.fontSize) : getTextWidth(roundOneDec((d.y0/d.total-d.y1/d.total)*-100),config.font,config.fontSize);
var rotation = Math.ceil(x.bandwidth()-1) <= textWidth ? 270 : 0;
var height = config.valueAs[0] === 'absolute' ? (y(d.y1)+(y(d.y0)-y(d.y1))/2) : (y(d.y1/d.total)+(y(d.y0/d.total)-y(d.y1/d.total))/2);
return "translate("+(x(d.catgryLabel)+x.bandwidth()/2)+","+(prevHeight+height)+")rotate("+rotation+")";
})
.attr("text-anchor", "middle");
}
if(config.valuesTotal && hiddenChoices.length !== choiceKeys.length-1){
var valuesTotal = g.append("g")
.attr("class", "cg-graph container-valuesTotal");
// Set total value, valuesTotal[background variable group]
// valuesTotal[background variable group] - userMaxScale needs to result in high enough height when scaled with y()
var tempRotation = [];
var tempY = [];
var textAnchor = [];
valuesTotal.selectAll(".valuesTotal")
.data(totalValue)
.enter().append("text")
.attr("class", "valuesTotal values noPointerEvents")
.attr("dy",".36em")
.attr("fill","black")
.text(function(d){ return d; })
.attr("style",function (d,i){
if(d/100 <= 0.99 || config.valueAs[0] === 'absolute'){
var visibility = d > 1 ? "visible" : "hidden";
var divisor = config.valueAs[0] === 'absolute' ? 1 : 100;
// Rotate if element width is smaller than text width
if(Math.ceil(x.bandwidth()) <= getTextWidth(d.toString(),config.font,config.fontSize) && config.height-(y(d/divisor)-y(0))*-1 >= 10){
tempRotation[i] = 270;
tempY[i] = y(totalValue[i]/divisor)-4;
textAnchor[i] = "left";
return "visibility: "+visibility+"; font-size: "+config.fontSize+"px;";
}
else{
tempRotation[i] = 0;
tempY[i] = (y(totalValue[i]/divisor)-8);
textAnchor[i] = "middle";
return "visibility: "+visibility+"; font-size: "+config.fontSize+"px;";
}
}
else{
tempRotation[i] = 0;
tempY[i] = 0;
return "visibility: hidden;";
}
})
.data(xDomain)
.attr("transform",function(d,i,j) {
return "translate("+(x(d)+(x.bandwidth())/2)+","+tempY[i]+")rotate("+tempRotation[i]+")";
})
.attr("text-anchor",function(d,i){
return textAnchor[i];
});
}
addLegend();
d3.selectAll('.legend')
.on('click', function (d,i){
if(hiddenChoices.indexOf(choiceKeys[i].toString()) !== -1){
hiddenChoices = hiddenChoices.filter(function(e) { return e !== choiceKeys[i]; });
}
else{
hiddenChoices.push(choiceKeys[i]);
}
sortStacked();
});
document.addEventListener("keydown", keyDown, false);
}
/** Selected choice's index number, other choices will be hidden.
* @private
* @type {int}
* @memberof module:columnGraphs
*/
var choiceIndex = 0;
/** Selected category's index number for compare categories to know which one to compare other categories against.
* @private
* @type {int}
* @memberof module:columnGraphs
*/
var compareToIndex = 0;
/**
* Creates a graph that compares categories of a group, one choice at a time. Has a boolean parameter categoryFromSelect as first param but it's only used internally so it can't be set (true selects category from select and false updates select).
* @private
* @memberof module:columnGraphs
* @param {array}[valueAs] Changes graph from percentage to absolute values if given value ['absolute','']. Second string in array is D3 format for y axis. Works exactly same as calling setConf('valueAs',['absolute','']) before drawing.
*/
function drawCompareCategories(categoryFromSelect,valueAs){
try{
if(data.length === 0){ throw "Data not set"; }
else if(!svg){ throw "Svg not initialized"; }
}
catch(err){
console.log("drawCompareCategories(): " + err);
return;
}
if(valueAs){
config.valueAs = valueAs;
}
selectedGraph = 'compare';
if(config.graphSelectEnabled === true){
updateSelect('selectGraph',selectedGraph);
}
if(Object.keys(groups).length === 1 && Object.keys(groups)[0] === config.none){
addRemoveOption('selectGroup',config.none);
}
else if(addRemoveOption('selectGroup',config.none,true) === true){
initData();
drawCompareCategories(categoryFromSelect);
return;
}
if(compareToIndex === config.all){
compareToIndex = 0;
}
addRemoveOption('selectCatgry',config.all,true);
d3.selectAll('.cg-graph').remove();
var choiceKeys = Object.keys(choices);
if(hiddenChoices.length < choiceKeys.length-1){
choiceIndex = 0;
hiddenChoices = JSON.parse(JSON.stringify(choiceKeys));
hiddenChoices = hiddenChoices.splice(1,hiddenChoices.length);
}
var choice = choiceKeys[choiceIndex];
var selectGroupText = document.getElementById('selectGroup').options[document.getElementById('selectGroup').selectedIndex].text;
var groupLabels = groups[selectGroupText];
var compareTo = selectGroupText === config.none ? 0 : Object.keys(groupLabels)[compareToIndex];
var catgryFirst = data[0][0].catgryKey;
var catgryLast = data[data.length-1][0].catgryKey;
// Get the y and height of the bar that other ones are being compared to
var compareToValue;
var compareToHeight;
x
.rangeRound([0, config.width])
.domain(data.map(function(value,index) { return value[0].catgryLabel; }));
checkXAxisTextWidths();
var select = document.getElementById("selectCatgry");
if(categoryFromSelect === true){
// Get selected category from selectedCatgry dropdown and change compareToIndex accordingly
var selectText = select.options[select.selectedIndex].text;
for(var i=0, j=d3.selectAll(".axis-x .tick text")._groups[0].length; i<j; i+=1){
if(d3.selectAll(".axis-x .tick text")._groups[0][i].innerHTML === selectText){
compareToIndex = parseInt(i);
break;
}
}
}
else{
// Update selectCatgry dropdown's value to be the same as currently selected
updateSelect('selectCatgry',data[compareToIndex][0].catgryLabel,true);
}
// Get max domain for y axis
var maxDomainY = config.valueAs[0] === 'absolute' ? d3.max(data.map(function(value,index) { return value[choiceIndex].count; })) : roundOneDec(d3.max(data.map(function(value,index) { return value[choiceIndex].count/value[choiceIndex].total; }))*100)/100;
maxDomainY = config.valueAs[0] === 'absolute' ? (maxDomainY === 0 ? 1 : maxDomainY*1.1) : (maxDomainY*1.1 > 1 ? 1 : maxDomainY*1.1);
y
.rangeRound([config.height, 0])
.domain([0, maxDomainY]);
yAxis.call(d3.axisLeft(y).ticks(10).tickFormat(function(d, i){ return d3.format(config.valueAs[1])(d)+config.valueAs[2]; }));
checkYAxisTextWidths();
var divisor = config.valueAs[0] === 'absolute' ? 1 : data[compareToIndex][choiceIndex].total;
compareToValue = data[compareToIndex][choiceIndex].count/divisor;
compareToHeight = (y(data[compareToIndex][choiceIndex].count/divisor)-y(0))*-1;
var bars = g.append("g")
.attr("class", "bars cg-graph");
bars.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("fill", function(d,i) {
if(i === compareToIndex){
return config.colorCompare;
}
else{
var currentValue = config.valueAs[0] === 'absolute' ? d[choiceIndex].count : d[choiceIndex].count/d[choiceIndex].total;
return compareToHeight > (y(currentValue)-y(0))*-1 ? config.colorLower : config.colorHigher;
}
})
.attr("x", function(d,i) { return x(d[choiceIndex].catgryLabel); })
.attr("y", function (d,i) {
var currentValue = config.valueAs[0] === 'absolute' ? d[choiceIndex].count : d[choiceIndex].count/d[choiceIndex].total;
return compareToValue > currentValue ? y(compareToValue) : y(currentValue);
})
.attr("height", function (d,i) {
if(i === compareToIndex){
return compareToHeight;
}
else{
var currentValue = config.valueAs[0] === 'absolute' ? d[choiceIndex].count : d[choiceIndex].count/d[choiceIndex].total;
var currentHeight = (y(currentValue)-y(0))*-1;
return compareToHeight > currentHeight ? compareToHeight-currentHeight : currentHeight-compareToHeight;
}
})
.attr("width", x.bandwidth());
// Horizontal line where the selected bar's top is
g.append("line")
.attr("class","cg-graph")
.style("stroke",config.colorCompare)
.style("stroke-width",2)
.attr("x1", function(d) { return x(data[0][choiceIndex].catgryLabel); })
.attr("y1",function () { return y(compareToValue); })
.attr("x2", function(d) { return x(data[data.length-1][choiceIndex].catgryLabel)+x.bandwidth(); })
.attr("y2",function () { return y(compareToValue); });
// Arrows pointing in the direction of difference
var arrows = g.append("g")
.attr("class", "arrows cg-graph");
arrows.selectAll("path")
.data(data)
.enter().append("path")
.attr("class", "arrows")
.style('fill', function (d,i) {
if(i === compareToIndex){
return config.colorCompare;
}
else{
var currentValue = config.valueAs[0] === 'absolute' ? d[choiceIndex].count : d[choiceIndex].count/d[choiceIndex].total;
return compareToValue > currentValue ? config.colorLower : config.colorHigher;
}
})
.attr("d", function (d,i){
var pathData;
if(i !== compareToIndex){
var xStart = x(d[choiceIndex].catgryLabel)+x.bandwidth()/2;
var yStart;
var currentValue = config.valueAs[0] === 'absolute' ? d[choiceIndex].count : d[choiceIndex].count/d[choiceIndex].total;
var currentHeight = (y(currentValue)-y(0))*-1;
var arrowSize = x.bandwidth()*0.5;
var yOffset = 3;
if(arrowSize > y(0)-currentHeight-yOffset*2){
arrowSize = y(0)-currentHeight-yOffset*2;
}
if(arrowSize > 80){
arrowSize = 80;
}
// Draw arrow pointing down if compare category is higher than current category and if there's at least some space below bar
if(compareToValue > currentValue && currentHeight > 10) {
yStart = (y(compareToValue))+(compareToHeight-currentHeight)+yOffset;
pathData = "M " + xStart + " " + yStart + " " +
"L " + (xStart + arrowSize/4) + " " + yStart + " " +
"L " + (xStart + arrowSize/4) + " " + (yStart + arrowSize/2) + " " +
"L " + (xStart + arrowSize/2) + " " + (yStart + arrowSize/2) + " " +
"L " + xStart + " " + (yStart + arrowSize) + " " +
"L " + (xStart - arrowSize/2) + " " + (yStart + arrowSize/2) + " " +
"L " + (xStart - arrowSize/4) + " " + (yStart + arrowSize/2) + " " +
"L " + (xStart - arrowSize/4) + " " + yStart + " z";
}
// Draw arrow pointing up if compare category is smaller than current category
else if(compareToValue < currentValue){
yStart = (y(compareToValue))-(currentHeight-compareToHeight)-yOffset;
pathData = "M " + xStart + " " + yStart + " " +
"L " + (xStart - arrowSize/4) + " " + yStart + " " +
"L " + (xStart - arrowSize/4) + " " + (yStart - arrowSize/2) + " " +
"L " + (xStart - arrowSize/2) + " " + (yStart - arrowSize/2) + " " +
"L " + xStart + " " + (yStart - arrowSize) + " " +
"L " + (xStart + arrowSize/2) + " " + (yStart - arrowSize/2) + " " +
"L " + (xStart + arrowSize/4) + " " + (yStart - arrowSize/2) + " " +
"L " + (xStart + arrowSize/4) + " " + yStart + " z";
}
}
return pathData;
});
if(config.values){
var rotate = [];
var yOffset = [];
var values = g.append("g")
.attr("class", "values cg-graph");
values.selectAll(".text")
.data(data)
.enter().append("text")
.attr("class", "values")
.attr("fill","#262626")
.text(function (d,i,j){
var valueWidth;
var symbol = config.valueAs[0] === 'absolute' ? '' : '%';
var currentValue = config.valueAs[0] === 'absolute' ? d[choiceIndex].count : d[choiceIndex].count/d[choiceIndex].total;
var currentHeight = (y(currentValue)-y(0))*-1;
var value = compareToIndex === i ? (config.valueAs[0] === 'absolute' ? compareToValue : roundOneDec(compareToValue*100)) : (config.valueAs[0] === 'absolute' ? currentValue-compareToValue : roundOneDec((currentValue-compareToValue)*100));
var currentValueRounded = config.valueAs[0] === 'absolute' ? currentValue : roundOneDec((currentValue)*100);
if(compareToValue > currentValue || (compareToValue === currentValue && compareToIndex > i) || i === compareToIndex){
valueWidth = i === compareToIndex ? getTextWidth(value+symbol,config.font,(config.fontSize-2)) : getTextWidth(value+symbol+" \xa0("+currentValueRounded+symbol+")",config.font,(config.fontSize-2));
rotate[i] = 90;
yOffset[i] = 2;
// Move value y position by width-height if it's too long to fit
if(valueWidth > compareToHeight){
yOffset[i] -= valueWidth-compareToHeight+2;
}
return i === compareToIndex ? value+symbol : value+symbol+" \xa0("+currentValueRounded+symbol+")";
}
else if(compareToValue < currentValue || (compareToValue === currentValue && compareToIndex < i)){
valueWidth = getTextWidth("+"+value+symbol+" \xa0("+currentValueRounded+symbol+")",config.font,(config.fontSize-2));
rotate[i] = 270;
yOffset[i] = -2;
// Move value y position by width-(svg height-height) if it's too long to fit
if(valueWidth > (config.height-compareToHeight)){
yOffset[i] += valueWidth-(config.height-compareToHeight)-2;
}
return "+"+value+symbol+" \xa0("+currentValueRounded+symbol+")";
}
})
.attr("dy", "0.35em")
.attr("text-anchor", "left")
.style("font", (config.fontSize-2)+"px "+config.font)
.attr("transform",function(d,i) {
return "translate("+(x(d[choiceIndex].catgryLabel)+x.bandwidth()/2)+","+(y(compareToValue)+yOffset[i])+") rotate("+rotate[i]+")";
});
}
addLegend();
d3.selectAll('.legend')
.on('click', function (d,i){
choiceIndex = i;
hiddenChoices = JSON.parse(JSON.stringify(choiceKeys));
hiddenChoices = hiddenChoices.filter(function(e) { return e !== choiceKeys[choiceIndex]; });
sortCompare();
});
document.addEventListener("keydown", keyDown, false);
}
/**
* Adds legends for choices in the svg for options inside the div for options.
* @private
* @memberof module:columnGraphs
*/
function addLegend(){
var offsetFromWrappedText = 0;
var legend = svgOptions.selectAll(".legend")
.data(Object.values(choices))
.enter().append("g")
.attr("class", "cg-graph legend clickable")
.attr("transform", function(d, i) { return "translate(0," + (i * (config.fontSizeLegend*2)) + ")"; });
legend.append("text")
.attr("x", config.fontSizeLegend*2)
.attr("y", config.fontSizeLegend*0.75)
.attr("dy", "0.35em")
.attr("text-anchor", "start")
.style("font", config.fontSizeLegend+"px "+config.font)
.style("opacity", function(d,i){
return hiddenChoices.indexOf(Object.keys(choices)[i].toString()) !== -1 ? 0.4 : 1;
})
.text(function(d) { return d; })
.call(function (d,i){
linesWrapped = wrapText(d,config.widthOptions-config.fontSizeLegend*2,config.fontSizeLegend+2,"legendText");
});
legend.append("rect")
.attr("x", 0)
.attr('y', function (d, i){
if(i !== 0){
offsetFromWrappedText += countInArray(linesWrapped,(i-1))*(config.fontSizeLegend+2);
}
return offsetFromWrappedText;
})
.attr("width", config.fontSizeLegend*1.5)
.attr("height", config.fontSizeLegend*1.5)
.attr("fill", function(d,i){
if(hiddenChoices.indexOf(Object.keys(choices)[i].toString()) !== -1){
return config.colorUnselected;
}
else{
return selectedGraph === 'compare' ? config.colorCompare : config.colorChoices(i);
}
});
// Set height of cg-options-svg to fit all lines
heightOptions = (config.fontSizeLegend*2)*Object.keys(choices).length+offsetFromWrappedText+config.fontSizeLegend/2;
svgOptions.attr("height", heightOptions);
function countInArray(array, lookfor) {
var count = 0;
for(var i=0, j=array.length; i<j; i+=1) {
if(array[i] === lookfor){
count++;
}
}
return count;
}
}
/**
* Sorts the data by value (or category key if value is the same) for stacked bars graph.
* @private
* @memberof module:columnGraphs
*/
function sortStacked(){
var selectGroup = document.getElementById('selectGroup');
var groupLabels = groups[selectGroup.options[selectGroup.selectedIndex].value];
var selectCatgryText = document.getElementById('selectCatgry').options[document.getElementById('selectCatgry').selectedIndex].text;
var selectCatgryValue = document.getElementById('selectCatgry').options[document.getElementById('selectCatgry').selectedIndex].value;
var labelKeys = data.map(function(value,index) { return value[0].catgryKey; });
var choiceKeys = Object.keys(choices);
var totalValue = [];
for(var i=0, k=labelKeys.length; i<k; i+=1){
totalValue[i] = 0;
}
for(var a=0, c=choiceKeys.length; a<c; a+=1){
for(var b=0, d=labelKeys.length; b<d; b+=1){
if(hiddenChoices.indexOf(choiceKeys[a].toString()) === -1){
if(config.valueAs[0] === 'absolute'){
totalValue[b] += data[b][a].count;
}
else{
if(typeof svg.select('#polygon_'+choiceKeys[a]+'_'+labelKeys[b])._groups[0][0] === 'undefined'){
totalValue[b] += data[b][a].count/data[b][a].total;
}
else{
totalValue[b] += parseInt(svg.select('#polygon_'+choiceKeys[a]+'_'+labelKeys[b]).attr('height'))/y(0);
}
}
}
if(a === choiceKeys.length-1){
totalValue[b] = [labelKeys[b],roundOneDec(totalValue[b]*100)];
}
}
}
totalValue.sort(function (x, y){
if(document.getElementById("selectSort").value === "-1" || x[1] === y[1]){
if(parseInt(x[0]) > parseInt(y[0])){
return 1;
}
else if(parseInt(x[0]) < parseInt(y[0])){
return -1;
}
return 0;
}
else if(x[1] > y[1]){
return 1;
}
else if(x[1] < y[1]){
return -1;
}
return 0;
});
if(document.getElementById("selectSort").value === "1"){
totalValue.reverse();
}
var referenceArray = [];
for(var j=0, l=totalValue.length; j<l; j+=1){
referenceArray.push(totalValue[j][0]);
}
data.sort(function (x, y){
return referenceArray.indexOf(x[0].catgryKey) - referenceArray.indexOf(y[0].catgryKey);
});
drawStackedBars();
}
/**
* Sorts the data by value (or category key if value is the same) for compare groups graph.
* @private
* @memberof module:columnGraphs
* @param {boolean}[categoryFromSelect] True selects category from select and false updates select.
*/
function sortCompare(categoryFromSelect){
var choice = Object.keys(choices)[choiceIndex];
// Sort data by default or ascending (reverse ascended data if descending is selected)
data.sort(function (x, y){
var xValue = config.valueAs[0] === 'absolute' ? x[choiceIndex].count : x[choiceIndex].count/x[choiceIndex].total;
var yValue = config.valueAs[0] === 'absolute' ? y[choiceIndex].count : y[choiceIndex].count/y[choiceIndex].total;
if(document.getElementById("selectSort").value === "-1" || xValue === yValue){
if(parseInt(x[0].catgryKey) > parseInt(y[0].catgryKey)){
return 1;
}
else if(parseInt(x[0].catgryKey) < parseInt(y[0].catgryKey)){
return -1;
}
return 0;
}
else if(xValue > yValue){
return 1;
}
else if(xValue < yValue){
return -1;
}
return 0;
});
if(document.getElementById("selectSort").value === "1"){
data.reverse();
}
if(categoryFromSelect !== false){
drawCompareCategories(true);
}
else{
drawCompareCategories();
}
}
/**
* Adds keyboard shortcuts for compare groups graph for changing group and sorting the graph.
* @private
* @memberof module:columnGraphs
* @param {event} e - Triggered event by key press.
*/
function keyDown(e) {
if(document.getElementById('selectCatgry').options[document.getElementById('selectCatgry').selectedIndex].text !== config.none){
var groupLabels = Object.values(groups[document.getElementById('selectGroup').options[document.getElementById('selectGroup').selectedIndex].text]);
var catgryNum = Object.keys(groupLabels).length-1;
var choiceNum = Object.keys(choices).length-1;
var select = document.getElementById("selectSort");
switch(e.which) {
case 104: // Numpad 8 to change sort to ascending
select.selectedIndex = 1;
redraw();
break;
case 101: // Numpad 5 to change sort to default
select.selectedIndex = 0;
redraw();
break;
case 98: // Numpad 2 to change sort to descending
select.selectedIndex = 2;
redraw();
break;
case 37: // Left arrow to switch group to the adjacent group on left or last if first one is selected
if(selectedGraph === 'compare'){
compareToIndex = compareToIndex === 0 ? catgryNum : compareToIndex-1;
redraw(false);
}
else if(selectedGraph === 'stacked'){
compareToIndex = (compareToIndex === 0 ? config.all : (compareToIndex === config.all ? catgryNum : compareToIndex-1));
updateSelect('selectCatgry',compareToIndex.toString());
redraw();
}
break;
case 39: // Right arrow to switch group to the adjacent group on right or first if last one is selected
if(selectedGraph === 'compare'){
compareToIndex = compareToIndex === catgryNum ? 0 : compareToIndex+1;
redraw(false);
}
else if(selectedGraph === 'stacked'){
compareToIndex = (compareToIndex === catgryNum ? config.all : (compareToIndex === config.all ? 0 : compareToIndex+1));
updateSelect('selectCatgry',compareToIndex.toString());
redraw();
}
break;
case 38: // Up arrow to switch selected choice
choiceIndex = choiceIndex === 0 ? choiceNum : choiceIndex-1;
hiddenChoices = JSON.parse(JSON.stringify(Object.keys(choices)));
hiddenChoices = hiddenChoices.filter(function(e) { return e !== Object.keys(choices)[choiceIndex]; });
redraw();
break;
case 40: // Down arrow to switch selected choice
choiceIndex = choiceIndex === choiceNum ? 0 : choiceIndex+1;
hiddenChoices = JSON.parse(JSON.stringify(Object.keys(choices)));
hiddenChoices = hiddenChoices.filter(function(e) { return e !== Object.keys(choices)[choiceIndex]; });
redraw();
break;
default:
return;
}
e.preventDefault();
}
}
/**
* Rounds given number to have one decimal at most.
* @private
* @memberof module:columnGraphs
* @param {float} num - Number to round.
*/
function roundOneDec(num) {
return +(Math.round(num + "e+1") + "e-1");
}
/**
* Rounds given number to have two decimals at most.
* @private
* @memberof module:columnGraphs
* @param {float} num - Number to round.
*/
function roundTwoDec(num) {
return +(Math.round(num + "e+2") + "e-2");
}
/**
* Gets width of text in pixels with the given font and size.
* @private
* @memberof module:columnGraphs
* @param {string} text - Text to check width of.
* @param {string}[textFontName=config.font] - Name of the font.
* @param {int}[textFontSize=config.fontSize] - Size of the font.
*/
function getTextWidth(text,textFontName,textFontSize){
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var font = textFontName || config.font;
var size = textFontSize || config.fontSize;
context.font = size + 'px ' + font;
return context.measureText(text).width;
}
/**
* Checks the luminance of color by Rec. 709 and returns 'white' or 'black' depending on which one is easier to see over color.
* @private
* @memberof module:columnGraphs
* @param {string} hex - Color as hex string.
*/
function getTextColor(hex){
var color = hex.substring(1);
var rgb = parseInt(color, 16);
var r = (rgb >> 16) & 0xff;
var g = (rgb >> 8) & 0xff;
var b = (rgb >> 0) & 0xff;
var luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
return luma < 127 ? "white" : "black";
}
/**
* Wraps text to fit inside given width.
* Returns an array with a line number for each wrapped line so the length of array tells the number of times text had to be wrapped.
* For example [2,2,4] means that third text had to be wrapped twice, fifth line was wrapped once and other lines didn't need any wrapping.
* @private
* @memberof module:columnGraphs
* @param {string} text - Text to wrap.
* @param {int} maxWidth - Maximum width for text before wrapping.
* @param {int}[textFontSize=20] - Text's font size.
* @param {string}[customClass] Adds given custom class to wrapped texts. Any custom class also adds a class with the line number of text, e.g. "wrappedLine_1".
*/
function wrapText(text, maxWidth, textFontSize, customClass) {
var previousLineNumber = 0;
var linesWrapped = [];
var counter = 0;
if(textFontSize === null || typeof textFontSize === 'undefined'){
textFontSize = 20;
}
text.each(function(){
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
x = text.attr("x"),
y = text.attr("y"),
dy = parseFloat(text.attr("dy")) || "0.35em";
y = parseInt(y)+parseInt(previousLineNumber*textFontSize);
counter++;
var tspan = text.text(null).append("tspan").attr("class","wrappedLine_"+previousLineNumber+" "+customClass).attr("x", x).attr("y", y).attr("dy", dy + "em");
while ((word = words.pop())) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > maxWidth && line.length > 1) {
line.pop();
tspan.text(line.join(" "));
line = [word];
y = parseInt(y)+parseInt(textFontSize);
++previousLineNumber;
if(customClass !== null && typeof customClass !== 'undefined'){
tspan = text.append("tspan").attr("class","wrappedLine_"+previousLineNumber+" "+customClass).attr("x", x).attr("y", y).attr("dy",dy + "em").text(word);
}
else{
tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy",dy + "em").text(word);
}
linesWrapped.push(counter-1);
}
}
});
return linesWrapped;
}
/**
* Wraps axis text to fit inside given width.
* Returns an array with a line number for each wrapped line so the length of array tells the number of times text had to be wrapped.
* For example [2,2,4] means that third text had to be wrapped twice, fifth line was wrapped once and other lines didn't need any wrapping.
* @private
* @memberof module:columnGraphs
* @param {string} text - Text to wrap.
* @param {int} maxWidth - Maximum width for text before wrapping.
* @param {int}[textFontSize=config.fontSizeAxis] - Text's font size.
* @param {string}[customClass] Adds given custom class to wrapped texts. Any custom class also adds a class with the line number of text, e.g. "wrappedLine_1".
*/
function wrapAxis(text, maxWidth, textFontSize, customClass) {
var previousLineNumber = 0;
var linesWrapped = [];
var counter = 0;
if(textFontSize === null || typeof textFontSize === 'undefined'){
textFontSize = config.fontSizeAxis;
}
text.each(function(){
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
x = 0,
y = text.attr("y"),
dy = parseFloat(text.attr("dy")) || "0.35em";
y = parseInt(y);
counter++;
var tspan = text.text(null).attr("title",function(d){ return d; }).append("tspan").style("text-anchor", "end").attr("class","wrappedLine_"+previousLineNumber+" "+customClass).attr("x", x).attr("y", y).attr("dy", dy + "em");
while ((word = words.pop())) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > maxWidth && line.length > 1) {
line.pop();
tspan.text(line.join(" "));
line = [word];
y = parseInt(y)+parseInt(textFontSize);
++previousLineNumber;
if(customClass !== null && typeof customClass !== 'undefined'){
tspan = text.append("tspan").style("text-anchor", "end").attr("class","wrappedLine_"+previousLineNumber+" "+customClass).attr("x", x).attr("y", y).attr("dy",dy + "em").text(word);
}
else{
tspan = text.append("tspan").style("text-anchor", "end").attr("x", x).attr("y", y).attr("dy",dy + "em").text(word);
}
linesWrapped.push(counter-1);
}
}
});
return linesWrapped;
}
/**
* Optimizes resize callbacks.
* @private
* @class optimizedResize
* @memberof module:columnGraphs
*/
var optimizedResize = (function() {
var callbacks = [],
running = false;
// Fired on resize event
function resize(){
if(!running) {
running = true;
if(window.requestAnimationFrame){
window.requestAnimationFrame(runCallbacks);
}
else {
setTimeout(runCallbacks, 66);
}
}
}
// Run the actual callbacks
function runCallbacks() {
callbacks.forEach(function(callback) {
callback();
});
running = false;
}
// Adds callback to loop
function addCallback(callback){
callback = callback || resizeGraph;
callbacks.push(callback);
}
function removeCallback(callback){
callback = callback || resizeGraph;
var index = callbacks.indexOf(callback);
if(index !== -1) {
callbacks.splice(index, 1);
}
}
return {
/**
* Add additional callback to resize event. Adds the default resize functionality by default if empty.
* @memberof module:columnGraphs.optimizedResize
* @method
* @param {function}[callback=resizeGraph] - Function to add as callback.
*/
add: function(callback) {
if(!callbacks.length) {
window.addEventListener('resize', resize);
}
addCallback(callback);
},
/**
* Remove callback from resize event. Removes the default resize functionality by default if empty.
* @memberof module:columnGraphs.optimizedResize
* @method
* @param {function}[callback=resizeGraph] - Function to remove from callback.
*/
remove: function(callback) {
if(!callbacks.length) {
window.addEventListener('resize', resize);
}
removeCallback(callback);
}
};
}());
/** Set to original width including margins left and right.
* @private
* @type {int}
* @memberof module:columnGraphs
*/
var originalWidth = config.width+config.margin.left+config.margin.right;
/** Current width of window.
* @private
* @type {int}
* @memberof module:columnGraphs
*/
var windowWidth;
/** Current height of window.
* @private
* @type {int}
* @memberof module:columnGraphs
*/
var windowHeight;
/**
* Resizes graph if graph is bigger than window width or smaller than original width.
* @private
* @memberof module:columnGraphs
*/
function resizeGraph(){
if((!windowWidth && !windowHeight) || windowWidth !== window.innerWidth || windowHeight !== window.innerHeight){
windowWidth = window.innerWidth;
windowHeight = window.innerHeight;
var graphSvg = document.getElementById('cg-graph-svg');
var otherOptionsDiv = document.getElementById('cg-options-selects');
var optionsMarginLeft = 20;
var offsetLeft = getOffset(graphSvg).left;
// Change options div styles after the div is moved under svg so the options are next to each other
if(windowWidth < originalWidth+config.widthOptions+config.margin.right+optionsMarginLeft+config.widthScrollbar+offsetLeft){
otherOptionsDiv.style.display = "inline-block";
otherOptionsDiv.style.marginLeft = optionsMarginLeft+"px";
}
else{
otherOptionsDiv.style.display = "block";
otherOptionsDiv.style.marginLeft = "0";
}
// Change main graph svg width if window is too small
if((graphSvg.getBoundingClientRect().width+config.margin.left+config.margin.right-config.widthScrollbar+offsetLeft > windowWidth && windowWidth < originalWidth+offsetLeft) || graphSvg.getBoundingClientRect().width+config.margin.left+config.margin.right-config.widthScrollbar < originalWidth){
if(windowWidth < config.widthMin+offsetLeft){
config.width = config.widthMin-config.margin.left-config.margin.right;
}
else if(windowWidth > originalWidth+offsetLeft){
config.width = originalWidth-config.margin.left-config.margin.right;
}
else{
config.width = windowWidth-config.margin.left-config.margin.right-offsetLeft;
}
svg.attr("width",config.width+config.margin.left+config.margin.right);
redraw();
}
}
function getOffset(element){
var box = element.getBoundingClientRect();
var body = document.body;
var docElement = document.documentElement;
var scrollTop = window.pageYOffset || docElement.scrollTop || body.scrollTop;
var scrollLeft = window.pageXOffset || docElement.scrollLeft || body.scrollLeft;
var clientTop = docElement.clientTop || body.clientTop || 0;
var clientLeft = docElement.clientLeft || body.clientLeft || 0;
var top = box.top + scrollTop - clientTop;
var left = box.left + scrollLeft - clientLeft;
return { top: Math.round(top), left: Math.round(left) };
}
}
/**
* Checks x axis text widths and rotates texts if needed.
* @private
* @memberof module:columnGraphs
*/
function checkXAxisTextWidths(){
xAxis
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "rotate(0)")
.style("text-anchor", "middle");
var maxTextWidth = getArrayMax(getAxisTextWidths(xAxis));
var step = x.step();
if(maxTextWidth > step){
xAxis
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "rotate(-45)")
.style("text-anchor", "end")
.call(function (d,i){
wrapAxis(d,step+config.margin.left+10,config.fontSizeAxis,"axisText");
});
if(config.marginAuto === true){
maxTextWidth = parseInt(getArrayMax(getAxisTextWidths(xAxis)));
config.margin.bottom = maxTextWidth+10;
svg.attr("height",config.height+config.margin.top+config.margin.bottom);
}
}
else{
if(config.marginAuto === true){
config.margin.bottom = 30;
svg.attr("height",config.height+config.margin.top+config.margin.bottom);
}
}
}
/**
* Checks y axis text widths and rotates texts if needed.
* @private
* @memberof module:columnGraphs
*/
function checkYAxisTextWidths(){
if(config.marginAuto === true){
var maxTextWidth = parseInt(getArrayMax(getAxisTextWidths(yAxis)));
if(maxTextWidth > config.margin.left || maxTextWidth < config.margin.left+10){
config.margin.left = parseInt(maxTextWidth)+10;
svg.attr("width",config.width+config.margin.left+config.margin.right);
g.attr("transform", "translate(" + (config.margin.left+config.marginExtra) + "," + config.margin.top + ")");
}
}
}
/**
* Returns an array of axis text widths.
* @private
* @memberof module:columnGraphs
* @param {SVGAxisElement} axis - Axis element to get texts from.
* @param {string}[textFontName=config.font] - Name of the font.
* @param {int}[textFontSize=config.fontSizeAxis] - Size of the font.
*/
function getAxisTextWidths(axis,textFontName,textFontSize){
var textWidths = [];
var font = textFontName || config.font;
var size = textFontSize || config.fontSizeAxis;
for(var i=0, len=axis.selectAll('text')._groups[0].length; i<len; i+=1){
textWidths.push(axis.selectAll('text')._groups[0][i].getBoundingClientRect().width);
}
return textWidths;
}
/**
* Returns maximum value in array.
* @private
* @memberof module:columnGraphs
* @param {Array} numArray - Array of numbers.
*/
function getArrayMax(numArray){
return Math.max.apply(null, numArray);
}
/**
* Returns minimum value in array.
* @private
* @memberof module:columnGraphs
* @param {Array} numArray - Array of numbers.
*/
function getArrayMin(numArray){
return Math.min.apply(null, numArray);
}
optimizedResize.add(resizeGraph);
return cg;
}));