Skip to content

Commit

Permalink
#399 Add multiline chart
Browse files Browse the repository at this point in the history
  • Loading branch information
nadouani committed Dec 14, 2017
1 parent f686ded commit 1d10ac7
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 0 deletions.
61 changes: 61 additions & 0 deletions ui/app/scripts/directives/dashboard/filter-editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
(function() {
'use strict';
angular.module('theHiveDirectives').directive('filterEditor', function($q, UserSrv) {
return {
restrict: 'E',
scope: {
filter: '=?',
entity: '=',
metadata: '='
},
templateUrl: 'views/directives/dashboard/filter-editor.html',
link: function(scope) {
scope.editorFor = function(filter) {
if (filter.type === null) {
return;
}
var field = scope.metadata[scope.entity].attributes[filter.field];
var type = field.type;

if ((type === 'string' || type === 'number') && field.values.length > 0) {
return 'enumeration';
}

return filter.type;
};

scope.promiseFor = function(filter, query) {
var field = scope.metadata[scope.entity].attributes[filter.field];

var promise = null;

if(field.type === 'user') {
promise = UserSrv.autoComplete(query);
} else if (field.values.length > 0) {
promise = $q.resolve(
_.map(field.values, function(item, index) {
return {
text: item,
label: field.labels[index] || item
};
})
);
} else {
promise = $q.resolve([]);
}

return promise.then(function(response) {
var list = [];

list = _.filter(response, function(item) {
var regex = new RegExp(query, 'gi');
return regex.test(item.label);
});

return $q.resolve(list);
});
};
}
};
});
})();
209 changes: 209 additions & 0 deletions ui/app/scripts/directives/dashboard/multiline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
(function() {
'use strict';
angular.module('theHiveDirectives').directive('dashboardMultiline', function($http, $state, DashboardSrv, NotificationSrv) {
return {
restrict: 'E',
scope: {
filter: '=?',
options: '=',
entity: '=',
autoload: '=',
mode: '=',
refreshOn: '@',
resizeOn: '@',
metadata: '='
},
template: '<c3 chart="chart" resize-on="{{resizeOn}}" error="error" on-save-csv="getCsv()"></c3>',
link: function(scope) {
scope.error = false;
scope.chart = {};

scope.intervals = DashboardSrv.timeIntervals;
scope.interval = scope.intervals[2];

scope.buildSelect = function(serie, index) {
var s = {
_agg: serie.agg,
_name: 'agg_' + (index + 1),
_query: serie.query || {}
};

if(serie.agg !== 'count') {
s._field = serie.field;
}

return s;
}

scope.buildSerie = function(serie, q, index) {
return {
model: serie.entity,
query: q,
stats: [{
_agg: 'time',
_fields: [serie.dateField],
_interval: scope.options.interval || scope.interval.code,
_select: [scope.buildSelect(serie, index)]
}]
};
};

scope.load = function() {
if(!scope.options.series || scope.options.series.length === 0) {
scope.error = true;
return;
}

var query = DashboardSrv.buildChartQuery(null, scope.options.query);
var postData = {
stats: _.map(scope.options.series, function(serie, index) {
return scope.buildSerie(serie, query, index);
})
};

var statsPromise = $http.post('./api/_stats', postData);

statsPromise.then(function(response) {
scope.error = false;
var labels = _.keys(response.data).map(function(d) {
return moment(d * 1).format('YYYY-MM-DD');
});
var len = labels.length,
data = {_date: (new Array(len)).fill(0)},
rawData = {};

_.each(response.data, function(value, key) {
//rawData[key] = value[scope.options.field]
rawData[key] = {};
_.each(_.values(value), function(val) {
_.extend(rawData[key], val);
});
});

_.each(rawData, function(value) {
_.each(_.keys(value), function(key){
data[key] = (new Array(len)).fill(0);
});
});

var i = 0;
var orderedDates = _.sortBy(_.keys(rawData));

_.each(orderedDates, function(key) {
var value = rawData[key];
data._date[i] = moment(key * 1).format('YYYY-MM-DD');

_.each(_.keys(value), function(item) {
data[item][i] = value[item];
});

i++;
});

scope.types = {};
scope.names = {};
scope.axes = {};
scope.colors = {};

var serieTypes = _.uniq(_.pluck(scope.options.series, 'type')).length;

_.each(scope.options.series, function(serie, index) {
var key = serie.field,
agg = serie.agg,
columnKey = 'agg_' + (index + 1);

scope.types[columnKey] = serie.type || 'line';
scope.names[columnKey] = serie.label || (agg === 'count' ? 'count' : (agg + ' of ' + key));
scope.axes[columnKey] = serieTypes === 1 ? 'y' : ((scope.types[columnKey] === 'bar') ? 'y2' : 'y');
scope.colors[columnKey] = serie.color;
});

// Compute stack groups
var groups = {};
_.each(scope.types, function(value, key) {
if (groups[value]) {
groups[value].push(key);
} else {
groups[value] = [key];
}
});
scope.groups = scope.options.stacked === true ? _.values(groups) : {};

scope.data = data;

var chart = {
data: {
x: '_date',
json: scope.data,
names: scope.names || {},
type: scope.type || 'bar',
types: scope.types || {},
axes: scope.axes || {},
colors: scope.colors || {},
groups: scope.groups || []
},
bar: {
width: {
ratio: 1 - Math.exp(-len/20)
}
},
axis: {
x: {
type: 'timeseries',
tick: {
format: '%Y-%m-%d',
rotate: 90,
height: 50
}
},
y2: {
show: _.values(scope.axes).indexOf('y2') !== -1
}
},
zoom: {
enabled: scope.options.zoom || false
}
};

scope.chart = chart;
}, function(err) {
scope.error = true;
NotificationSrv.log('Failed to fetch data, please edit the widget definition', 'error');
});
};

scope.getCsv = function() {
var dates = scope.data._date;
var keys = _.keys(scope.data);
var headers = _.extend({_date: 'Date'}, scope.names);

var csv = [{data: _.map(keys, function(key){
return headers[key] || key;
}).join(';')}];

var row = [];
for(var i=0; i<dates.length; i++) {
row = _.map(keys, function(key) {
return scope.data[key][i];
});

csv.push({data: row.join(';')});
}

return csv;
};

if (scope.autoload === true) {
scope.load();
}

if (!_.isEmpty(scope.refreshOn)) {
scope.$on(scope.refreshOn, function(event, filter) {
scope.filter = filter;
scope.load();
});
}
}
};
});
})();
13 changes: 13 additions & 0 deletions ui/app/views/directives/dashboard/multiline/basic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<div class="form-group">
<label>Title</label>
<input type="text" class="form-control" placeholder="Ex: cases per TLP" ng-model="component.options.title">
</div>
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<label>Interval</label>
<select class="form-control" ng-model="component.options.interval"
ng-options="item.code as item.label for item in timeIntervals"></select>
</div>
</div>
</div>
14 changes: 14 additions & 0 deletions ui/app/views/directives/dashboard/multiline/edit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<uib-tabset class="nav-tabs-custom" active="layout.activeTab">
<uib-tab index="0">
<uib-tab-heading>
<i class="fa fa-bars"></i> Basic
</uib-tab-heading>
<ng-include src="'views/directives/dashboard/multiline/basic.html'"></ng-include>
</uib-tab>
<uib-tab index="1">
<uib-tab-heading>
<i class="fa fa-sort"></i> Series
</uib-tab-heading>
<ng-include src="'views/directives/dashboard/multiline/series.html'"></ng-include>
</uib-tab>
</uib-tabset>
26 changes: 26 additions & 0 deletions ui/app/views/directives/dashboard/multiline/serie.filters.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<div ng-if="serie.filters.length > 0">
<strong>Serie's filter</strong>
</div>
<div class="row mb-xxxs" ng-repeat="filter in serie.filters track by $index">
<div class="col-sm-4">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeSerieFilter(serie, $index)">
<i class="fa fa-times text-danger"></i>
</button>
</span>
<select class="form-control" ng-model="filter.field"
ng-options="item.name as item.name for (key, item) in metadata[serie.entity].attributes"
ng-change="setFilterField(filter, serie.entity)"></select>
</div>
</div>
<!-- <div class="col-sm-8" ng-include="'views/directives/dashboard/filter-editor.html'"></div> -->
<div class="col-sm-8">
<filter-editor metadata="metadata" filter="filter" entity="serie.entity"></filter-editor>
</div>
</div>
<div class="mt-xxs">
<a href ng-click="addSerieFilter(serie)">
<i class="fa fa-plus"></i> Add a filter
</a>
</div>
70 changes: 70 additions & 0 deletions ui/app/views/directives/dashboard/multiline/series.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<div ng-if="!component.options.series || component.options.series.length === 0" class="empty-message">
No series defined. <a href ng-click="addSerie()">Add a serie</a>
</div>
<div class="mb-xxxs dashboard-serie" ng-repeat="serie in component.options.series track by $index">
<div class="form-inline mb-xxxs">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="removeSerie($index)">
<i class="fa fa-times text-danger"></i>
</button>
</span>
<div class="form-group">
<select class="form-control" ng-model="serie.entity"
ng-options="item as metadata[item].label for item in metadata.entities"></select>
</div>
</div>
<div class="form-group">
<select class="form-control" ng-model="serie.dateField"
ng-options="item.name as item.name for item in pickFields(metadata[serie.entity].attributes, ['date']) | orderBy:'name'"></select>
</div>
</div>
<div class="form-inline">

<div class="form-group">
<select class="form-control" ng-model="serie.agg"
ng-options="item.label as item.id for (key, item) in aggregations"
ng-change="setSerieAgg(serie)">
<option value="" disabled selected></option>
</select>
</div>
<div class="form-group">
<select class="form-control" ng-model="serie.field" ng-disabled="serie.agg === 'count'"
ng-options="item.name as item.name for (key, item) in fieldsForAggregation(metadata[component.options.entity].attributes, serie.agg)">
<option value="" disabled selected>-- Select field --</option>
</select>
</div>
<div class="form-group">
<select class="form-control" ng-model="serie.type"
ng-options="item for item in serieTypes"></select>
</div>

<div class="form-group">
<input class="form-control" type="text" ng-model="serie.label" placeholder="Label">
</div>
<div class="input-group">
<input type="text" class="form-control" ng-model="serie.color" placeholder="Color" size="8">
<span class="input-group-btn">
<button colorpicker colorpicker-close-on-select class="btn btn-default" ng-model="serie.color" type="button">
<i class="fa fa-stop" style="color: {{serie.color}};" ng-class="{'fa-stop': serie.color, 'fa-ellipsis-h': !serie.color}"></i>
</button>
</span>
</div>
</div>
<div class="ml-m mt-xs">
<ng-include src="'views/directives/dashboard/multiline/serie.filters.html'"></ng-include>
</div>
</div>
<div ng-if="component.options.series && component.options.series.length > 1" class="mv-xs">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="component.options.stacked"> Stack the series by type
</label>
</div>
</div>

<div ng-if="component.options.series && component.options.series.length > 0" class="mv-xs">
<a href ng-click="addSerie()">
<i class="fa fa-plus"></i> Add a serie
</a>
</div>

0 comments on commit 1d10ac7

Please sign in to comment.