(function () {
'use strict';
// The following colors will be used if you pick "Automatic" color.
var BaseColors = {
'Blue': '#4572A7',
'Red': '#AA4643',
'Green': '#89A54E',
'Purple': '#80699B',
'Cyan': '#3D96AE',
'Orange': '#DB843D',
'Light Blue': '#92A8CD',
'Lilac': '#A47D7C',
'Light Green': '#B5CA92',
'Brown': '#A52A2A',
'Black': '#000000',
'Gray': '#808080',
'Pink': '#FFC0CB',
'Dark Blue': '#00008b'
}
// Additional colors for the user to choose from:
var ColorPalette = _.extend({}, BaseColors, {
'Indian Red': '#F8766D',
'Green 2': '#53B400',
'Green 3': '#00C094',
'DarkTurquoise': '#00B6EB',
'Dark Violet': '#A58AFF',
'Pink 2' : '#FB61D7'
});
var ColorPaletteArray = _.values(BaseColors);
var fillXValues = function(seriesList) {
var xValues = _.sortBy(_.union.apply(_, _.pluck(seriesList, 'x')), _.identity);
_.each(seriesList, function(series) {
series.x = _.sortBy(series.x, _.identity);
_.each(xValues, function(value, index) {
if (series.x[index] !== value) {
series.x.splice(index, 0, value);
series.y.splice(index, 0, null);
}
});
});
};
var storeOriginalHeightForEachSeries = function(seriesList) {
_.each(seriesList, function(series) {
if(!_.has(series,'visible')){
series.visible = true;
series.original_y = series.y.slice();
}
});
};
var getEnabledSeries = function(seriesList){
return _.filter(seriesList, function(series) {
return series.visible === true;
});
};
var initializeTextAndHover = function(seriesList){
_.each(seriesList, function(series) {
series.text = [];
series.hoverinfo = 'text+name';
});
};
var normalAreaStacking = function(seriesList) {
fillXValues(seriesList);
storeOriginalHeightForEachSeries(seriesList);
initializeTextAndHover(seriesList);
seriesList = getEnabledSeries(seriesList);
_.each(seriesList, function(series, seriesIndex, list){
_.each(series.y, function(undefined, yIndex, undefined2){
var cumulativeHeightOfPreviousSeries = seriesIndex > 0 ? list[seriesIndex-1].y[yIndex] : 0;
var cumulativeHeightWithThisSeries = cumulativeHeightOfPreviousSeries + series.original_y[yIndex];
series.y[yIndex] = cumulativeHeightWithThisSeries;
series.text.push('Value: ' + series.original_y[yIndex] + '
Sum: ' + cumulativeHeightWithThisSeries);
});
});
};
var lastVisibleY = function(seriesList, lastSeriesIndex, yIndex){
for(; lastSeriesIndex >= 0; lastSeriesIndex--){
if(seriesList[lastSeriesIndex].visible === true){
return seriesList[lastSeriesIndex].y[yIndex];
}
}
return 0;
}
var percentAreaStacking = function(seriesList) {
if (seriesList.length === 0) {
return;
}
fillXValues(seriesList);
storeOriginalHeightForEachSeries(seriesList);
initializeTextAndHover(seriesList);
_.each(seriesList[0].y, function(seriesY, yIndex, undefined){
var sumOfCorrespondingDataPoints = _.reduce(seriesList, function(total, series){
return total + series.original_y[yIndex];
}, 0);
_.each(seriesList, function(series, seriesIndex, list){
var percentage = (series.original_y[yIndex] / sumOfCorrespondingDataPoints ) * 100;
var previousVisiblePercentage = lastVisibleY(seriesList, seriesIndex-1, yIndex);
series.y[yIndex] = percentage + previousVisiblePercentage;
series.text.push('Value: ' + series.original_y[yIndex] + '
Relative: ' + percentage.toFixed(2) + '%');
});
});
};
var percentBarStacking = function(seriesList) {
if (seriesList.length === 0) {
return;
}
fillXValues(seriesList);
initializeTextAndHover(seriesList);
for (var i = 0; i < seriesList[0].y.length; i++) {
var sum = 0;
for(var j = 0; j < seriesList.length; j++) {
sum += seriesList[j].y[i];
}
for(var j = 0; j < seriesList.length; j++) {
var value = seriesList[j].y[i] / sum * 100;
seriesList[j].text.push('Value: ' + seriesList[j].y[i] + '
Relative: ' + value.toFixed(2) + '%');
seriesList[j].y[i] = value;
}
}
}
var normalizeValue = function(value) {
if (moment.isMoment(value)) {
return value.format("YYYY-MM-DD HH:mm:ss");
}
return value;
}
angular.module('plotly', [])
.constant('ColorPalette', ColorPalette)
.directive('plotlyChart', function () {
var bottomMargin = 50;
return {
restrict: 'E',
template: '
',
scope: {
options: "=",
series: "=",
height: "="
},
link: function (scope, element) {
var getScaleType = function(scale) {
if (scale === 'datetime') {
return 'date';
}
if (scale === 'logarithmic') {
return 'log';
}
return scale;
};
var setType = function(series, type) {
if (type === 'column') {
series.type = 'bar';
} else if (type === 'line') {
series.mode = 'lines';
} else if (type === 'area') {
series.fill = scope.options.series.stacking === null ? 'tozeroy' : 'tonexty';
series.mode = 'lines';
} else if (type === 'scatter') {
series.type = 'scatter';
series.mode = 'markers';
}
};
var getColor = function(index) {
return ColorPaletteArray[index % ColorPaletteArray.length];
};
var calculateHeight = function() {
var height = Math.max(scope.height, (scope.height - 50) + bottomMargin);
return height;
}
var recalculateOptions = function() {
scope.data.length = 0;
scope.layout.showlegend = _.has(scope.options, 'legend') ? scope.options.legend.enabled : true;
if(_.has(scope.options, 'bottomMargin')) {
bottomMargin = parseInt(scope.options.bottomMargin);
scope.layout.margin.b = bottomMargin;
}
delete scope.layout.barmode;
delete scope.layout.xaxis;
delete scope.layout.yaxis;
delete scope.layout.yaxis2;
if (scope.options.globalSeriesType === 'pie') {
var hasX = _.contains(_.values(scope.options.columnMapping), 'x');
var rows = scope.series.length > 2 ? 2 : 1;
var cellsInRow = Math.ceil(scope.series.length / rows);
var cellWidth = 1 / cellsInRow;
var cellHeight = 1 / rows;
var xPadding = 0.02;
var yPadding = 0.05;
_.each(scope.series, function(series, index) {
var xPosition = (index % cellsInRow) * cellWidth;
var yPosition = Math.floor(index / cellsInRow) * cellHeight;
var plotlySeries = {values: [], labels: [], type: 'pie', hole: .4,
marker: {colors: ColorPaletteArray},
text: series.name, textposition: 'inside', name: series.name,
domain: {x: [xPosition, xPosition + cellWidth - xPadding],
y: [yPosition, yPosition + cellHeight - yPadding]}};
_.each(series.data, function(row, index) {
plotlySeries.values.push(row.y);
plotlySeries.labels.push(hasX ? row.x : 'Slice ' + index);
});
scope.data.push(plotlySeries);
});
return;
}
var hasY2 = false;
var sortX = scope.options.sortX === true || scope.options.sortX === undefined;
var useUnifiedXaxis = sortX && scope.options.xAxis.type === 'category';
var unifiedX = null;
if (useUnifiedXaxis) {
unifiedX = _.sortBy(_.union.apply(_, _.map(scope.series, function(s) { return _.pluck(s.data, 'x'); })), _.identity);
}
_.each(scope.series, function(series, index) {
var seriesOptions = scope.options.seriesOptions[series.name] || {type: scope.options.globalSeriesType};
var plotlySeries = {x: [],
y: [],
name: seriesOptions.name || series.name,
marker: {color: seriesOptions.color ? seriesOptions.color : getColor(index)}};
if (seriesOptions.yAxis === 1 && (scope.options.series.stacking === null || seriesOptions.type === 'line')) {
hasY2 = true;
plotlySeries.yaxis = 'y2';
}
setType(plotlySeries, seriesOptions.type);
var data = series.data;
if (sortX) {
data = _.sortBy(data, 'x');
}
if (useUnifiedXaxis && index === 0) {
var values = {};
_.each(data, function(row) {
values[row.x] = row.y;
});
_.each(unifiedX, function(x) {
plotlySeries.x.push(normalizeValue(x));
plotlySeries.y.push(normalizeValue(values[x] || null));
});
} else {
_.each(data, function(row) {
plotlySeries.x.push(normalizeValue(row.x));
plotlySeries.y.push(normalizeValue(row.y));
});
}
scope.data.push(plotlySeries);
});
var getTitle = function(axis) {
if (angular.isDefined(axis) && angular.isDefined(axis.title)) {
return axis.title.text;
}
return null;
};
scope.layout.xaxis = {title: getTitle(scope.options.xAxis),
type: getScaleType(scope.options.xAxis.type)};
if (angular.isDefined(scope.options.xAxis.labels)) {
scope.layout.xaxis.showticklabels = scope.options.xAxis.labels.enabled;
}
if (angular.isArray(scope.options.yAxis)) {
scope.layout.yaxis = {title: getTitle(scope.options.yAxis[0]),
type: getScaleType(scope.options.yAxis[0].type)};
}
if (hasY2 && angular.isDefined(scope.options.yAxis)) {
scope.layout.yaxis2 = {title: getTitle(scope.options.yAxis[1]),
type: getScaleType(scope.options.yAxis[1].type),
overlaying: 'y',
side: 'right'};
} else {
delete scope.layout.yaxis2;
}
if (scope.options.series.stacking === 'normal') {
scope.layout.barmode = 'stack';
if (scope.options.globalSeriesType === 'area') {
normalAreaStacking(scope.data);
}
} else if (scope.options.series.stacking === 'percent') {
scope.layout.barmode = 'stack';
if (scope.options.globalSeriesType === 'area') {
percentAreaStacking(scope.data);
} else if (scope.options.globalSeriesType === 'column') {
percentBarStacking(scope.data);
}
}
scope.layout.margin.b = bottomMargin;
scope.layout.height = calculateHeight();
};
scope.$watch('series', recalculateOptions);
scope.$watch('options', recalculateOptions, true);
scope.layout = {margin: {l: 50, r: 50, b: bottomMargin, t: 20, pad: 4}, height: calculateHeight(), autosize: true, hovermode: 'closest'};
scope.plotlyOptions = {showLink: false, displaylogo: false};
scope.data = [];
var element = element[0].children[0];
Plotly.newPlot(element, scope.data, scope.layout, scope.plotlyOptions);
element.on('plotly_afterplot', function(d) {
if(scope.options.globalSeriesType === 'area' && (scope.options.series.stacking === 'normal' || scope.options.series.stacking === 'percent')){
$(element).find(".legendtoggle").each(function(i, rectDiv) {
d3.select(rectDiv).on('click', function () {
var maxIndex = scope.data.length - 1;
var itemClicked = scope.data[maxIndex - i];
itemClicked.visible = (itemClicked.visible === true) ? 'legendonly' : true;
if (scope.options.series.stacking === 'normal') {
normalAreaStacking(scope.data);
} else if (scope.options.series.stacking === 'percent') {
percentAreaStacking(scope.data);
}
Plotly.redraw(element);
});
});
}
});
scope.$watch('layout', function (layout, old) {
if (angular.equals(layout, old)) {
return;
}
Plotly.relayout(element, layout);
}, true);
scope.$watch('data', function (data, old) {
if (!_.isEmpty(data)) {
Plotly.redraw(element);
}
}, true);
}
};
});
})();