@domoinc/multi-metric-comparison-bar-chart
Version:
MultiMetricComparisonBarChart - Domo Widget
413 lines (371 loc) • 14.1 kB
JavaScript
var d3 = require('d3');
var d3Chart = require('d3.chart');
var BaseWidget = require('@domoinc/base-widget');
var SummaryNumber = require('@domoinc/summary-number');
var daTheme2 = require('@domoinc/da-theme2');
var ComparisonBarChart = require('@domoinc/comparison-bar-chart');
/*----------------------------------------------------------------------------------
----------------------------------------------------------------------------------
// MultiMetricComparisonBarChart:
----------------------------------------------------------------------------------
----------------------------------------------------------------------------------*/
module.exports = BaseWidget.extend('MultiMetricComparisonBarChart', {
initialize: function() {
'use strict';
var _Chart = this;
_Chart._dataObject = {};
//----------------------------------------------------------------------------------
// Config
//----------------------------------------------------------------------------------
this._newConfig = {
//plugin user configable configs
width: {
name: 'Widget Width',
description: 'Width of the widget',
value: 400,
type: 'number',
units: 'px'
},
height: {
// name: 'Widget Height',
description: 'Height of the widget',
value: 400,
type: 'number',
units: 'px'
},
barHeight: {
name: 'Bar Height',
description: 'Height of the bars',
category: 'Bars',
value: 30,
type: 'number',
units: 'px'
},
chartGap: {
name: 'Bar Spacing',
description: 'The space between the bars',
value: 10,
type: 'number',
units: 'px'
},
generalWashoutColor: daTheme2.themeElements.washoutColor(),
//----------------
// TEXT
//----------------
textFontFamily: daTheme2.themeElements.h1FontFamily(),
//----------------
// TITLE
//----------------
titleWeight: {
name: 'Title Font Weight',
description: 'Font weight for the title text',
category: 'General',
value: 700,
type: 'number',
units: ''
},
titleSize: {
name: 'Title Font Size',
description: 'Font size for the titles',
category: 'General',
value: 12,
type: 'number',
units: 'px'
},
titleColor: daTheme2.themeElements.h1FontColor({
name: 'Title Font Color',
description: 'Font color for the titles'
}),
//----------------
// LABELS
//----------------
labelSize: {
name: 'Label Font Size',
description: 'Font size for the labels',
category: 'General',
value: 12,
type: 'number',
units: 'px'
},
labelColor: daTheme2.themeElements.h5FontColor({
name: 'Label Font Color',
description: 'Font color for the labels',
}),
//----------------
// GENERAL
//----------------
showAnimations: {
name: 'Enable Animation',
description: 'Enable or disable the animation',
category: 'Animation',
type: 'select',
value: {name: 'Show', value: true},
options: [
{name: 'Show', value: true},
{name: 'Hide', value: false},
]
},
//widget default configs
chartName: {
description: 'Name of chart for reporting',
type: 'string',
value: 'MultiMetricComparisonBarChart'
},
chartPrimarySeriesColors: daTheme2.themeElements.primaryColorSeries(),
//Configs not in the plugin
textLabel: {
description: 'The function used for the text labels',
value: function(d, i) {
return d[0] + ' ' + summaryNumber.summaryNumber(d[1]);
},
type: 'function'
}
};
this.mergeConfig(_Chart._newConfig);
//----------------------------------------------------------------------------------
// Data Definition:
//----------------------------------------------------------------------------------
_Chart._newDataDefinition = {
'Metric': {
type: 'string',
validate: function(d) { return this.accessor(d) !== undefined; },
accessor: function(line) { return String(line[0]); }
},
'Label': {
type: 'string',
validate: function(d) { return this.accessor(d) !== undefined; },
accessor: function(line) { return String(line[1]); }
},
'Value': {
type: 'number',
validate: function(d) { return !isNaN(this.accessor(d)) && this.accessor(d) > 0; },
accessor: function(line) { return Number(line[2]); }
}
};
this.mergeDataDefinition(_Chart._newDataDefinition);
//----------------------------------------------------------------------------------
// Static - Anything that is defined here will never change
// If extending 'BaseWidget' : _Chart._layerGroup is the NEW BASE
//----------------------------------------------------------------------------------
var summaryNumber = new SummaryNumber();
var _manualColorScale = false;
var _colorScale = d3.scale.ordinal();
_Chart._chartGroup = _Chart._layerGroup.append('g').attr('class', 'chartGroup');
_Chart.transform = function(data) {
var validData = _Chart.validateData(data);
calculateTextHeight();
moveNotifier();
initChartVariables(validData);
setColorScale(validData);
return validData;
};
//----------------------------------------------------------------------------------
// Layers:
//----------------------------------------------------------------------------------
_Chart.layer('chartGroup', this._chartGroup, {
dataBind: function(validData) {
var data = validData.metrics;
return this.selectAll(' g.chart').data(data);
},
insert: function() {
return this.append('g');
}
})
.on('enter', function() {
// adds chart to group
this.each(function(d, i) {
this.__chart__ = d3.select(this)
.chart('ComparisonBarChart')
.on('dispatch:mouseenter', hover)
.on('dispatch:mouseleave', unhover);
this.__chart__.colorScale(_Chart.colorScale());
});
return this.classed('chart', true);
})
.on('merge', function() {
this.attr('transform', function(d, i) {
return 'translate(0,' + (i * (_Chart.innerChartHeight + _Chart.c('chartGap'))) + ')';
});
this.each(function(d, i) {
this.__chart__.c({
'height': _Chart.innerChartHeight,
'width': _Chart.c('width'),
'barHeight': _Chart.c('barHeight'),
'chartPrimarySeriesColors': _Chart.c('chartPrimarySeriesColors'),
'textFontFamily': _Chart.c('textFontFamily'),
'title': d,
'titleWeight': _Chart.c('titleWeight'),
'titleSize': _Chart.c('titleSize'),
'titleColor': _Chart.c('titleColor'),
'labelSize': _Chart.c('labelSize'),
'labelColor': _Chart.c('labelColor'),
'generalWashoutColor': _Chart.c('generalWashoutColor'),
'showAnimations': _Chart.c('showAnimations').value,
'textLabel': _Chart.c('textLabel')
})
.draw(_Chart._dataObject[d]);
});
return this;
})
.on('exit', function() {
return this.remove();
});
//----------------------------------------------------------------------------------
// Event Functions
//----------------------------------------------------------------------------------
/**
* Sets off dispatch:mouseenter:bar trigger event. If _Chart.c('showAnimations') is true,
* it changes the color of the opposite bar to generaWashoutColor
* @param {[Object]} obj Object with series, name, and data properties
* @param {[SVG]} element SVG element that is being hovered over
*/
function hover(obj, element) {
var thisClass = d3.select(element).classed('left') ? 'left' : 'right';
_Chart.trigger('dispatch:mouseenter:bar', obj, element);
var trigger = 'external:mouseover:' + thisClass;
if (_Chart.c('showAnimations').value) {
_Chart._chartGroup.selectAll('.chart').each(function() {
this.__chart__.trigger(trigger);
});
}
}
/**
* Sets off dispatch:mouseleave:bar trigger event. If _Chart.c('showAnimations') is true,
* it changes the color of the opposite bar to its original color
* @param {[SVG]} element SVG element that was hovered off of
*/
function unhover(obj, element) {
_Chart.trigger('dispatch:mouseleave:bar', obj, element);
if (_Chart.c('showAnimations').value) {
_Chart._chartGroup.selectAll('.chart').each(function() {
this.__chart__.trigger('external:mouseout');
});
}
}
//----------------------------------------------------------------------------------
// Helper Functions
//----------------------------------------------------------------------------------
/**
* Initiates the charts variables
* @param {[Array]} validData 2D array of data
* @return {[Array]} validDAta 2D array of data with a metrics property
*/
function initChartVariables(validData) {
// saves the data in this object with a hierarchy
_Chart._dataObject = {};
// passes only the metrics onto the layer
var metrics = [];
// loop through to add hieararchy
getMetrics(metrics, validData);
var chartSize = metrics.length * (_Chart.innerChartHeight + _Chart.c('chartGap'));
var excessData = [];
//Remove excess data from metrics
removeExcessData(excessData, chartSize, metrics);
//If there is excess data, notify the user.
excessDataNotifier(excessData);
validData.metrics = metrics;
return validData;
}
/**
* Removes excess data so the chart doesn't draw more bars than what the height can hold
* @param {[Array]} excessData Empty array
* @param {[Number]} chartSize Size of the chart to accomodate all of the bars drawn
* @param {[Array]} metrics Array of strings that represent the metrics that are being measured
*/
function removeExcessData(excessData, chartSize, metrics) {
while (chartSize > _Chart.c('height')) {
excessData.push(metrics.pop());
chartSize = metrics.length * (_Chart.innerChartHeight + _Chart.c('chartGap'))
}
}
/**
* Notifies the user if any of the data is removed
* @param {[Array]} excessData Array of the excess data
*/
function excessDataNotifier(excessData) {
if (excessData.length > 0) {
_Chart._notifier.base.attr('transform', 'translate(0,-20)')
_Chart._notifier.appendMessage(_Chart.c('chartName'), 'DISCARDED_VALID_DATA',
'There are too many bars for chart\'s height!', ['Metric:String', 'Label:String', 'Value:Number'], excessData);
_Chart._notifier.draw();
}
}
/**
* Loops through the data and sets its metrics and _Chart._dataObject
* @param {[Array]} metrics Empty array
* @param {[Array]} validData 2D array of data
*/
function getMetrics(metrics, validData) {
for (var i = 0; i < validData.length; i++) {
var metric = _Chart.a('Metric')(validData[i]);
var array = [
_Chart.a('Label')(validData[i]),
_Chart.a('Value')(validData[i])
];
// checks if metric already exists in object
if (_Chart._dataObject[metric] === undefined) {
_Chart._dataObject[metric] = [];
metrics.push(metric);
}
// adds array under metric
// only allows 2 per metric
if (_Chart._dataObject[metric].length <= 2) {
_Chart._dataObject[metric].push(array);
} else {
_Chart._notifier.appendMessage(_Chart.c('chartName'), 'WARN', 'Too many rows', validData[i]);
}
}
}
/**
* Set the color scale for the chart
*/
function setColorScale(data) {
var uniqueLabels;
if (!_manualColorScale) {
uniqueLabels = data.map(function(d) {
return _Chart.a('Label')(d);
}).filter(function(el,i,a) {
return i === a.indexOf(el);
});
_colorScale = d3.scale.ordinal()
.domain(uniqueLabels)
.range(daTheme2.getPrimaryColorsForNumberOfSeries(uniqueLabels.length, _Chart.c('chartPrimarySeriesColors')));
}
}
function calculateTextHeight() {
var title = _Chart._layerGroup.append('text')
.classed('tempText', true)
.text('title')
.style('font-size', _Chart.c('titleSize'))
.style('font-family', 'Open Sans')
.style('opacity', 0);
var label = _Chart._layerGroup.append('text')
.text('label')
.classed('tempText', true)
.style('font-size', _Chart.c('labelSize'))
.style('font-family', 'Open Sans')
.style('opacity', 0);
var titleHeight = title.node().getBBox().height * 0.9;
var labelHeight = label.node().getBBox().height * 0.9;
_Chart.titleHeight = titleHeight;
_Chart.textHeight = titleHeight + labelHeight;
_Chart._layerGroup.selectAll('.tempText').remove();
_Chart.innerChartHeight = _Chart.c('barHeight') + _Chart.textHeight;
}
function moveNotifier() {
_Chart._notifier.base.attr('transform', 'translate(0,' + (-_Chart.titleHeight) + ')');
}
//----------------------------------------------------------------------------------
// Public Functions
//----------------------------------------------------------------------------------
_Chart.colorScale = function(_) {
if (_) {
_manualColorScale = true;
_colorScale = _;
return _Chart;
} else {
return _colorScale;
}
};
}
});