UNPKG

@domoinc/multi-metric-comparison-bar-chart

Version:

MultiMetricComparisonBarChart - Domo Widget

413 lines (371 loc) 14.1 kB
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; } }; } });