@domoinc/base-widget
Version:
BaseWidget - Domo Widget
441 lines (371 loc) • 13.3 kB
JavaScript
'use strict';
var d3 = require('d3');
var d3Chart = require('d3.chart');
var Notifier = require('@domoinc/notifier');
var _ = require('lodash');
//----------------------------------------------------------------------------------
//----------------------------------------------------------------------------------
// Base Widget:
// This widget doesn't do anything by it self but it contains all the "base"
// features that our widgets need. This widget should be "extended" to do anthing that
// you need it to do.
//----------------------------------------------------------------------------------
//----------------------------------------------------------------------------------
function roundToOneDecimal(x) {
return Math.round(x * 10) / 10;
}
function createGroups(base, isSvg) {
var groups = {};
if (isSvg) {
groups.container = base;
groups.layer = base.append('g');
groups.notifier = base.append('g');
}
else {
groups.container = base.append('div')
.style('position', 'relative');
groups.layer = groups.container.append('div')
.style({
position: 'relative',
width: '100%',
height: '100%'
});
groups.notifier = groups.container.append('svg')
.style({
position: 'absolute',
top: 0,
left: 0
})
.attr({
width: '100%',
height: '100%'
});
}
groups.layer.classed('layer-group', true);
groups.notifier.classed('notifier', true)
.style({
display: 'none',
'pointer-events': 'none'
});
return groups;
}
module.exports = d3Chart('BaseWidget', {
//**********************************************************************************
// Anything in the initialization method will only run once.
// i.e. It will not be run every time you call draw.
//**********************************************************************************
initialize: function () {
// Make the chart constructor accessible to all other methods.
var _Chart = this;
//----------------------------------------------------------------------------------
// Default Values for the wiget's configurable options.
// Valid types for UI elements are: string, number, color, select, boolean
//----------------------------------------------------------------------------------
_Chart._config = {
height: {
name: 'Chart Height',
description: '',
category: 'Dimensions',
type: 'number',
value: 250,
units: 'px'
},
width: {
name: 'Chart Width',
description: '',
category: 'Dimensions',
type: 'number',
value: 250,
units: 'px'
},
shouldValidate: {
description: 'Flag for turning off data validation',
type: 'boolean',
value: true
},
updateSizeableConfigs: {
description: 'Flag for turning off the mimic of illustrator\'s scale functionality',
type: 'boolean',
value: true
},
isOnMobile: {
description: 'If true, it signals to the widget that it is running on a mobile device. Should be called before draw and then NEVER changed.',
type:'boolean',
value: false
}
};
//----------------------------------------------------------------------------------
// Data Definition:
// Set functions on how to access and validate data values.
//----------------------------------------------------------------------------------
// This should be set in the "extended chart".
_Chart._dataDefinition = {};
//----------------------------------------------------------------------------------
// Static - Anything that is defined here will never change
//----------------------------------------------------------------------------------
var isSvg = _Chart.base.node().namespaceURI.indexOf('svg') !== -1;
var groups = createGroups(_Chart.base, isSvg);
_Chart._layerGroup = groups.layer;
//----------------------------------------------------------------------------------
// Notifier:
// This must come after any other layers/groups
//----------------------------------------------------------------------------------
_Chart._notifier = new Notifier(groups.notifier);
_Chart._notifier
.width(function () {
return _Chart.config('width');
});
_Chart._notifier
.height(function () {
return _Chart.config('height');
});
_Chart._dim = function() {
return Math.min(_Chart.c('width'), _Chart.c('height'));
};
//----------------------------------------------------------------------------------
// Data validation function and chart variable setup.
//----------------------------------------------------------------------------------
_Chart.transform = function (data) {
_Chart.updateSizeableConfigs();
if (!isSvg) {
groups.container.style({
width: _Chart.c('width') + 'px',
height: _Chart.c('height') + 'px'
});
}
return _Chart.validateData(data);
};
},
//----------------------------------------------------------------------------------
// Helper Functions
//----------------------------------------------------------------------------------
/**
* Merges the extending chart's config with the base's config
* @param {object} newConfig The extending chart's config
*/
mergeConfig: function mergeConfig(newConfig) {
var _Chart = this;
if (_Chart._config) {
_Chart._config = _.extend(_Chart._config, newConfig);
}
else {
_Chart._config = newConfig;
}
},
/**
* Merges the extending chart's data definitions with the base's data definition
* @param {object} newDefinition The extending chart's data definition
*/
mergeDataDefinition: function mergeDataDefinition(newDefinition) {
var _Chart = this;
if (_Chart._dataDefinition) {
_Chart._dataDefinition = _.extend(_Chart._dataDefinition, newDefinition);
}
else {
_Chart._dataDefinition = newDefinition;
}
},
/**
* Runs data though your validation functions and throws out bad data.
* @param {Array} data The data you passed to the draw method of your chart.
* @return {Array} The validated data.
*/
validateData: function (data) {
var _Chart = this;
var validData = data;
_Chart._notifier.clearDataValidationMessages();
_Chart._notifier.base.style('display', 'none');
if (_Chart.c('shouldValidate')) {
if (!validData || !_.flatten(validData).length) {
validData = [];
}
else {
validData = validData.filter(function (line) {
for (var col in _Chart._dataDefinition) {
if (!_Chart._dataDefinition[col].validate(line)) {
_Chart._notifier.appendMessage(_Chart.c('chartName'), 'WARN', 'Invalid value at column for ' + col, _Chart._dataDefinition[col], line);
return false;
}
}
return true;
});
}
if (!validData.length) {
_Chart._notifier.appendMessage(_Chart.config('chartName'), 'NO_DATA', 'No Data', _Chart._dataDefinition, data);
_Chart._notifier.base.style('display', '');
}
}
_Chart._notifier.draw();
return validData;
},
//----------------------------------------------------------------------------------
// Public Functions:
// Getter and Setter functions are documented here.
//----------------------------------------------------------------------------------
/**
* Sets one or many of the data definition properties for the chart
* @param {object} obj Contains data definiton objects you would like to set or update.
* @return {object} Returns the chart for chainability.
*/
dataDefinition: function (obj) {
if (arguments.length === 0) {
return this._dataDefinition;
}
for (var key in obj) {
if (this._dataDefinition[key]) {
for (var subKey in obj[key]) {
if (obj[key].hasOwnProperty(subKey)) {
this._dataDefinition[key][subKey] = obj[key][subKey];
}
}
}
else {
console.warn(key + ' is not a valid data property.');
}
}
return this;
},
/**
* This will get or set any of the chart's configuration options defined
* in the 'chart._config' object.
* @param {String or Object} item If string, it will return the value for that config item.
*
* If object, it will update that config item with set value.
* @param {String or Number} [value] The value to update config item with.
* @return {object} The chart to preserve chainability.
*/
config: function (item, value) {
var _Chart = this;
if (item === undefined) {
return this;
}
if (value === undefined && typeof item === 'string') {
return this._config[item] ? this._config[item].value : undefined;
}
var items;
if (typeof item === 'object') {
items = item;
}
else {
items = {};
items[item] = value;
}
for (var key in items) {
var c = this._config[key];
if (!c) {
console.warn(this.c('chartName') + ':' + key + ' is not a valid config property.');
}
else {
c.value = items[key];
if (isFinite(c.percent)) {
var dim = c.dim || _Chart._dim;
c.percent = c.value / dim();
}
if (c.onChange) {
console.warn(this.c('chartName') + ':' + key + ': Config\'s onChange function is deprecated and will be removed shortly.');
c.onChange.call(this, items[key]);
}
}
}
return this;
},
/**
* This will get or set any of the chart's accessors defined
* in the 'chart._dataDefinition' object.
* @param {String or Object} item If string, it will return the function for that accessor item. If object, it will update that accessor with set function.
* @param {function} [value] The function to update accessor item with.
* @return {object} The chart to preserve chainability.
*/
accessors: function (item, value) {
var _Chart = this;
if (item === undefined) {
return this;
}
if (value === undefined && typeof item === 'string') {
if (this._dataDefinition[item]) {
return function(line) {
return line ?
_Chart._dataDefinition[item].accessor(line) :
_Chart._dataDefinition[item].default;
};
}
else {
console.warn(item + ' is not a valid accessor.');
return d3.scale.identity();
}
}
var items;
if (typeof item === 'object') {
items = item;
}
else {
items = {};
items[item] = value;
}
for (var key in items) {
if (this._dataDefinition[key]) {
this._dataDefinition[key].accessor = items[key];
}
else {
console.warn(key + ' is not a valid accessor.');
}
}
return this;
},
/**
* Shorthand for config getter/setter
*/
c: function (item, value) {
return this.config.apply(this, arguments);
},
/**
* Shorthand for accessor getter function
*/
a: function (item, value) {
return this.accessors.apply(this, arguments);
},
/**
* Visits each config element.
* If a config element has a size function it will update that configs value
* with the returned result of its size function.
*/
updateSizeableConfigs: function () {
if (!this.c('updateSizeableConfigs')) return;
var _Chart = this;
var configKeys = Object.keys(this._config);
for (var i = 0; i < configKeys.length; i++) {
var configElement = this._config[configKeys[i]];
if (isFinite(configElement.percent)) {
var dim = configElement.dim || _Chart._dim;
configElement.value = roundToOneDecimal(configElement.percent * dim());
}
}
},
/**
* Takes a theme obj and applies it to the widget's config elements.
*
* @param themeObj {Object} {"name": "Dark Bold", "domoDefinedTheme": true, "config": { gaugeFillPrimaryColor: '#1C5CAE'} };
*/
applyTheme: function (themeObj) {
if (themeObj && themeObj.config) {
var applyThemeConfigs = themeObj.config;
var widgetConfigs = this._config;
for (var widgetConfigElementName in widgetConfigs) {
var hasValidThemeElement = (widgetConfigs.hasOwnProperty(widgetConfigElementName) &&
widgetConfigs[widgetConfigElementName].theme !== undefined);
if (hasValidThemeElement) {
if (applyThemeConfigs[widgetConfigs[widgetConfigElementName].theme]) {
widgetConfigs[widgetConfigElementName].value = applyThemeConfigs[widgetConfigs[widgetConfigElementName].theme];
}
}
}
return true;
}
return false;
},
/**
* This function will be called to clean up any pointers before the chart is destroyed.
* Extending charts should implement this function.
*/
destroy: function destroy() {}
});