@domoinc/multiline-chart
Version:
MultiLineChart - Domo Widget
508 lines (455 loc) • 16.3 kB
JavaScript
var d3 = require('d3');
var d3Chart = require('d3.chart');
var BaseWidget = require('@domoinc/base-widget');
var DomoTooltip = require('@domoinc/domo-tooltip');
var daTheme2 = require('@domoinc/da-theme2');
var SummaryNumber = require('@domoinc/summary-number');
var Utilities = require('@domoinc/utilities');
var _ = require('lodash');
//----------------------------------------------------------------------------------
//----------------------------------------------------------------------------------
// Axis
//----------------------------------------------------------------------------------
//----------------------------------------------------------------------------------
module.exports = BaseWidget.extend('Axis', {
//**********************************************************************************
// Anything in the initialization method will only run once.
// i.e. It will not be run every time you call draw.
//**********************************************************************************
initialize: function () {
'use strict';
var _Chart = this;
var _scale = d3.scale.linear();
//----------------------------------------------------------------------------------
// Default Values for the wiget's configurable options.
// Valid types for UI elements are: string, number, color, select, boolean
// NOTE: Height and Width have already been set on the base widget.
//
// Valid Properties: [Name], description, [category], type, value, [onChange]
//----------------------------------------------------------------------------------
this._newConfig = {
addBaseline: {
description: 'Adds the baseline path above the labels',
type: 'boolean',
value: false,
},
addZeroline: {
description: 'Makes the gridline corresponding to the zero tick show, and have increased stroke width',
type: 'boolean',
value: false,
},
addGridlines: {
description: 'Flag to show vertical or horizontal grid lines.',
type: 'boolean',
value: false,
},
addTicks: {
description: 'Flag to show the tick lines on the axis.',
type: 'boolean',
value: false
},
addLabels: {
description: 'Flag to show the text labels on the axis',
type: 'boolean',
value: true,
},
intelligentTicks: {
description: 'Try and show an appropriate number of ticks given available space.',
type: 'boolean',
value: true
},
intelligentTrunc: {
description: 'Truncate the labels so they do not over lap.',
type: 'boolean',
value: true
},
maxLegendSpace: {
description: '',
type: 'number',
value: 50
},
orient: {
description: 'Sets the orientation of the axis. Options: bottom, top, left, right',
type: 'string',
value: 'bottom'
},
scale: {
description: 'd3 scale object for the axis.',
type: 'scale',
value: _scale,
},
tickFormat: {
description: 'Label format function for the tick labels.',
type: 'function',
value: function (d) {
return service.summaryNumber(d);
}
},
tickPadding: {
description: 'Padding between the tick line and text.',
type: 'number',
value: 7
},
tickSize: {
description: 'Size of the tick line.',
type: 'number',
value: 5
},
tickSpacing: {
description: 'Space between each tick.',
type: 'number',
value: 40
},
axesLabelLetterSpacing: {
description: 'Letter spacing for the axes labels.',
type: 'number',
value: 0,
units: 'px'
},
x: {
description: 'The x coordinate for the axis.',
type: 'number',
value: 0
},
y: {
description: 'The y coordinate for the axis.',
type: 'number',
value: 0
},
duration: {
description: 'Duration of the animation',
value: 750,
type: 'number',
units: 'ms',
},
textFontFamily: daTheme2.themeElements.h1FontFamily({
name: 'Text Font Family',
description: '',
}),
tooltipBackgroundColor: daTheme2.themeElements.tooltipBackground(),
tooltipTextSize: {
name: 'Tooltip Text Size',
value: 14,
units: 'px',
type: 'number',
},
tooltipTextColor: daTheme2.themeElements.tooltipFontColor(),
showTooltip: {
value: false,
type: 'boolean',
},
chartName: {
description: 'Name of chart for Reporting.',
type: 'string',
value: 'Axis'
},
//THEME Elements
/*----------------------------------------------------------------------------------
//Axes (4):
----------------------------------------------------------------------------------*/
axesLineColor: daTheme2.themeElements.axesLineColor(),
axesLabelColor: daTheme2.themeElements.axesFontColor(),
axesLabelSize: daTheme2.themeElements.axesFontSize(),
axesLabelFontFamily: daTheme2.themeElements.axesFontFamily()
};
// Update config object from base chart.
this.mergeConfig(_Chart._newConfig);
//----------------------------------------------------------------------------------
// Static - Anything that is defined here will never change
//----------------------------------------------------------------------------------
var service = new SummaryNumber();
_Chart._axisBase = _Chart._layerGroup.classed('axis', true);
_Chart._axis = d3.svg.axis();
_Chart._svg = findSvg(_Chart._layerGroup.node());
function findSvg(selection) {
var selectionParent = selection.parentNode;
if (selectionParent.tagName === 'svg') {
return d3.select(selectionParent);
} else {
return findSvg(selectionParent);
}
}
var tooltip = _Chart._svg.append('g')
.classed('axisTooltip', true)
.chart('DomoTooltip')
.c('format', 'text')
.c('container', _Chart._svg);
tooltip._appends.append('text').classed('axisLabel', true);
/*----------------------------------------------------------------------------------
// Transform
----------------------------------------------------------------------------------*/
this.transform = function (data) {
updateTooltip();
verifyOreintation();
if (_Chart.c('scale') === _scale) {
setUpDefaultScale(data);
}
drawAxis();
return data;
};
//----------------------------------------------------------------------------------
// Helper Functions
//----------------------------------------------------------------------------------
/**
* Verifies orientation of the axis 'orient' config.
* Sets config to 'bottom' of not a valid config.
*/
function verifyOreintation() {
var validOptions = [
'top', // horizontal axis with ticks above the domain path
'bottom', // horizontal axis with ticks below the domain path
'left', // vertical axis with ticks to the left of the domain path
'right', // vertical axis with ticks to the right of the domain path
];
if (_.indexOf(validOptions, _Chart.c('orient')) < 0) {
console.warn('Not a valid option for orient property.' +
'Valid options are "bottom", "top", "left", "right".');
_Chart.c('orient', 'bottom');
}
}
/**
* Sets Data Scale
* @param data
*/
function setUpDefaultScale(data) {
_Chart.c('scale').domain([0, d3.max(data)]);
if (_Chart.c('orient') === 'bottom' || _Chart.c('orient') === 'top') {
_Chart.c('scale').range([0, _Chart.c('width')]);
}
else {
_Chart.c('scale').range([_Chart.c('height'), 0]);
}
}
/**
* DrawAxis
*/
function drawAxis() {
setupAxis();
_Chart._axisBase.transition().duration(_Chart.c('duration')).call(_Chart._axis);
styleAxis();
}
/**
* SetupAxis
*/
function setupAxis() {
_Chart._axisBase.attr('transform', 'translate(' + _Chart.c('x') + ',' + _Chart.c('y') + ')');
_Chart._axis
.scale(_Chart.c('scale'))
.tickFormat(_Chart.c('tickFormat'))
.tickSize(_Chart.c('tickSize'))
.tickPadding(_Chart.c('tickPadding'))
.orient(_Chart.c('orient'));
if (_Chart.c('intelligentTicks')) {
intelligentTicksNumTicks();
}
if (!_Chart.c('addTicks')) {
_Chart._axis.tickSize(0);
}
//if you don't add gridlines, tick will position and
//be sized automatically by tickSize option on _Chart._axis
if (_Chart.c('addGridlines')) {
var size = getTickSize();
_Chart._axis.innerTickSize(-size);
_Chart._axis.outerTickSize(0);
}
}
/**
* get the tick size depending if gridlines are added
* and axis orientation
*/
function getTickSize() {
var size = _Chart.c('width');
if (_Chart.c('orient') === 'bottom' || _Chart.c('orient') === 'top') {
size = _Chart.c('height');
}
if (_Chart.c('addTicks')) {
size += _Chart.c('tickSize');
}
return size;
}
/**
* get tick transform depending on axis orientation
* this function is only needed if there is ticks and gridlines
*/
function getTickTransform() {
var tickSize = _Chart.c('tickSize');
switch (_Chart.c('orient')) {
case 'top':
return 'translate(0,-' + tickSize + ')';
case 'bottom':
return 'translate(0,' + tickSize + ')';
case 'left':
return 'translate(-' + tickSize + ',0)';
case 'right':
return 'translate(' + tickSize + ',0)';
}
}
/**
* StyleAxis
*/
function styleAxis() {
_Chart._axisBase.selectAll('path')
.style({
'fill': 'none',
'stroke': _Chart.c('addBaseline') ? _Chart.c('axesLineColor') : 'none',
'stroke-width': 2,
});
_Chart._axisBase.selectAll('line')
.attr({
transform: function(){
if (_Chart.c('addTicks') && _Chart.c('addGridlines')) {
return getTickTransform();
}
}
})
.style('stroke', _Chart.c('axesLineColor'))
.each(function(d) {
// Add zeroline
var elem;
if (d === 0 && _Chart.c('addZeroline')) {
elem = d3.select(this);
elem.style('stroke-width', 2);
if (_Chart.c('addTicks')){
elem.attr('transform', getTickTransform);
}
if (_Chart.c('orient') === 'top') {
elem.transition().attr({
'y2': getTickSize(),
'x2': 0,
});
} else if (_Chart.c('orient') === 'bottom'){
elem.transition().attr({
'y2': -getTickSize(),
'x2': 0,
});
} else if (_Chart.c('orient') === 'left'){
elem.transition().attr({
'x2': getTickSize(),
'y2': 0,
});
} else if (_Chart.c('orient') === 'right'){
elem.transition().attr({
'x2': -getTickSize(),
'y2': 0,
});
}
} else if (d === 0 && !_Chart.c('addZeroline')) { // Remove zeroline
elem = d3.select(this);
elem.style('stroke-width', 1);
}
});
_Chart._axisBase.selectAll('text')
.on('mousemove', hoverMove)
.on('mouseout', hoverOff)
.style({
'fill': _Chart.c('axesLabelColor'),
'font-family': _Chart.c('axesLabelFontFamily'),
'font-weight': 400,
'font-size': _Chart.c('addLabels') ? _Chart.c('axesLabelSize') + 'px' : 0.1 + 'px',
'letter-spacing': _Chart.c('axesLabelLetterSpacing') + 'px'
});
if (_Chart.c('intelligentTicks') || !_Chart.c('intelligentTrunc')) {
intelligentTicksTruncate();
}
}
/**
* Tries to set an appropriate number of ticks given chart space.
*/
function intelligentTicksNumTicks() {
var numTicks = Math.round(getLengthOfAxis() / _Chart.c('tickSpacing'));
_Chart._axis.ticks(numTicks);
setTicksForPossibleOrdinalScale(numTicks);
}
//**********************************************************************************
// Ordinal scale do not respond to the 'ticks' function on a scale.
//**********************************************************************************
function setTicksForPossibleOrdinalScale(numTicks) {
//Most likely an ordinal scale... try tickValues instead
if (_Chart.c('scale').hasOwnProperty('rangePoints')) {
var domain = _Chart.c('scale').domain();
_Chart._axis.tickValues(domain); // ensure the tickValues are always set to the recent domain values
if (numTicks < domain.length) {
var showMod = Math.round(domain.length / numTicks);
var showTicks = _.filter(domain, function (d, i) {
return i % showMod === 0;
});
var lastDomain = domain[domain.length - 1];
var lastShown = showTicks[showTicks.length - 1];
//Last Tick Conditional: Fix the Last tick. Show one / move it to the last number in the domain.
if (domain.length - (showTicks.length * showMod) > showMod * 0.50) {
showTicks.push(lastDomain);
}
else {
showTicks[showTicks.length - 1] = lastDomain;
}
_Chart._axis.tickValues(showTicks);
}
}
}
/**
* Truncates tick labels to that they don't overlay.
*/
function intelligentTicksTruncate() {
var unattachedAxis = d3.select(document.createElement('g')).call(_Chart._axis);
var unattachedTextCnt = unattachedAxis.selectAll('text').size();
var orient = _Chart.c('orient');
var trueSpacing = orient === 'left' || orient === 'right' ?
_Chart.c('maxLegendSpace') : Math.round(getLengthOfAxis() / unattachedTextCnt);
_Chart._axisBase.selectAll('text')
.each(function (d) {
d3.domoStrings.truncToFit(d3.select(this), trueSpacing);
});
}
/**
* Returns length of axis given the current orientation.
*/
function getLengthOfAxis() {
var orient = _Chart.c('orient');
return orient === 'bottom' || orient === 'top' ? _Chart.c('width') : _Chart.c('height');
}
/**
* hover move
*/
function hoverMove(d) {
if (_Chart.c('showTooltip')) {
var valueIsString = !parseFloat(d); //if it's a string, and not a string number
//if the string data is different from the text
var dataIsDifferentFromText = String(d) !== d3.select(this).text(); // jshint ignore:line
var labelIsTruncated = valueIsString && dataIsDifferentFromText;
if (labelIsTruncated) {
var coordinates = d3.mouse(_Chart._svg.node());
var x = coordinates[0];
var y = coordinates[1];
var point = {
x: x,
y: y,
};
tooltip._appends.select('text').text(function() { return _Chart.c('tickFormat')(d); })
tooltip.trigger('draw');
tooltip.trigger('moveTo', point)
}
}
}
/**
* hover off
*/
function hoverOff(d) {
if (_Chart.c('showTooltip')) {
tooltip.trigger('remove');
}
}
/**
* update tooltip
*/
function updateTooltip() {
tooltip._appends.selectAll('text')
.style({
'font-family': _Chart.c('textFontFamily'),
'font-size': _Chart.c('tooltipTextSize') + 'px',
'fill': _Chart.c('tooltipTextColor'),
});
tooltip._appends.select('rect').style('fill', _Chart.c('tooltipTextColor'));
tooltip.c('tooltipBackgroundColor', _Chart.c('tooltipBackgroundColor'))
}
}
});