Files
ERP/sophal/js/view/view.chart.js

992 lines
23 KiB
JavaScript

/*
* Axelor Business Solutions
*
* Copyright (C) 2005-2019 Axelor (<http://axelor.com>).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
(function(){
/* global d3: true, nv: true, D3Funnel: true, RadarChart: true, GaugeChart: true */
"use strict";
var ui = angular.module('axelor.ui');
ui.ChartCtrl = ChartCtrl;
ui.ChartCtrl.$inject = ['$scope', '$element', '$http', 'ActionService'];
function ChartCtrl($scope, $element, $http, ActionService) {
var views = $scope._views;
var view = $scope.view = views.chart;
var viewChart = null;
var searchScope = null;
var clickHandler = null;
var actionHandler = null;
var loading = false;
var unwatch = null;
function refresh() {
if (viewChart && searchScope && $scope.searchFields && !searchScope.isValid()) {
return;
}
var context = $scope._context || {};
if ($scope.getContext) {
context = _.extend({}, $scope.getContext(), context);
}
if (searchScope) {
context = _.extend({}, context, searchScope.getContext());
}
context = _.extend({}, context, { _domainAction: $scope._viewAction });
loading = true;
var params = {
data: context
};
if (viewChart) {
params.fields = ['dataset'];
}
return $http.post('ws/meta/chart/' + view.name, params).then(function(response) {
var res = response.data;
var data = res.data;
var isInitial = viewChart === null;
if (viewChart === null) {
viewChart = data;
if (data.config && data.config.onClick) {
clickHandler = ActionService.handler($scope, $element, {
action: data.config.onClick
});
}
if (data.config && data.config.onAction) {
actionHandler = ActionService.handler($scope, $element, {
action: data.config.onAction
});
}
if (data.config && data.config.onActionTitle) {
$scope.actionTitle = data.config.onActionTitle;
}
} else {
data = _.extend({}, viewChart, data);
}
if ($scope.searchFields === undefined && data.search) {
$scope.searchFields = data.search;
$scope.searchInit = data.onInit;
$scope.usingSQL = data.usingSQL;
} else {
$scope.render(data);
if (isInitial) {
refresh(); // force loading data
}
}
loading = false;
}, function () {
loading = false;
});
}
$scope.setSearchScope = function (formScope) {
searchScope = formScope;
};
$scope.hasAction = function () {
return !!actionHandler;
};
$scope.handleAction = function (data) {
if (actionHandler) {
actionHandler._getContext = function () {
return _.extend({}, { _data: data }, {
_model: $scope._model || 'com.axelor.meta.db.MetaView',
_chart: view.name
});
};
actionHandler.handle();
}
};
$scope.handleClick = function (e) {
if (clickHandler) {
clickHandler._getContext = function () {
return _.extend({}, e.data.raw, {
_model: $scope._model || 'com.axelor.meta.db.MetaView',
_chart: view.name
});
};
clickHandler.handle();
}
};
$scope.onRefresh = function(force) {
if (unwatch || loading) {
return;
}
// in case of onInit
if ($scope.searchInit && !(searchScope||{}).record && !force) {
return;
}
unwatch = $scope.$watch(function chartRefreshWatch() {
if ($element.is(":hidden")) {
return;
}
unwatch();
unwatch = null;
refresh();
});
};
$scope.render = function(data) {
};
// refresh to load chart
$scope.onRefresh();
}
ChartFormCtrl.$inject = ['$scope', '$element', 'ViewService', 'DataSource'];
function ChartFormCtrl($scope, $element, ViewService, DataSource) {
$scope._dataSource = DataSource.create('com.axelor.meta.db.MetaView');
ui.FormViewCtrl.call(this, $scope, $element);
$scope.setEditable();
$scope.setSearchScope($scope);
function fixFields(fields) {
_.each(fields, function(field){
if (field.type == 'reference') {
field.type = 'MANY_TO_ONE';
field.canNew = false;
field.canEdit = false;
}
if (field.type)
field.type = field.type.toUpperCase();
else
field.type = 'STRING';
});
return fields;
}
var unwatch = $scope.$watch('searchFields', function chartSearchFieldsWatch(fields) {
if (!fields) {
return;
}
unwatch();
var meta = { fields: fixFields(fields) };
var view = {
type: 'form',
items: [{
type: 'panel',
noframe: true,
items: _.map(meta.fields, function (item) {
var props = _.extend({}, item, {
showTitle: false,
placeholder: item.title || item.autoTitle
});
if (item.multiple && (item.target || item.selection)) {
item.widget = item.target ? "TagSelect" : "MultiSelect";
}
return props;
})
}]
};
ViewService.process(meta, view);
view.onLoad = $scope.searchInit;
$scope.fields = meta.fields;
$scope.schema = view;
$scope.schema.loaded = true;
var interval;
function reload() {
$scope.$parent.onRefresh();
$scope.$applyAsync();
}
function delayedReload() {
clearTimeout(interval);
interval = setTimeout(reload, 500);
}
function onNewOrEdit() {
if ($scope.$events.onLoad) {
$scope.$events.onLoad().then(delayedReload);
}
}
var __getContext = $scope.getContext;
$scope.getContext = function () {
var ctx = __getContext.call(this);
_.each(meta.fields, function (item) {
if (item.multiple && (item.target || item.selection)) {
var value = ctx[item.name];
if (_.isArray(value)) value = _.pluck(value, "id");
if (_.isString(value)) value = value.split(/\s*,\s*/g);
ctx[item.name] = value;
} else if (item.target && $scope.usingSQL) {
var value = ctx[item.name];
if (value) {
ctx[item.name] = value.id;
}
}
});
return ctx;
};
$scope.$on('on:new', onNewOrEdit);
$scope.$on('on:edit', onNewOrEdit);
$scope.$watch('record', function chartSearchRecordWatch(record) {
if (interval === undefined) {
interval = null;
return;
}
if ($scope.isValid()) delayedReload();
}, true);
$scope.$watch('$events.onLoad', function chartOnLoadWatch(handler) {
if (handler) {
handler().then(delayedReload);
}
});
});
}
function $conv(value, type) {
if (!value && type === 'text') return 'N/A';
if (!value) return 0;
if (_.isNumber(value)) return value;
if (/^(-)?\d+(\.\d+)?$/.test(value)) {
return +value;
}
return value;
}
function applyXY(chart, data) {
var type = data.xType;
chart.y(function (d) { return d.y; });
if (type == "date") {
return chart.x(function (d) { return moment(d.x).toDate(); });
}
return chart.x(function (d) { return d.x; });
}
var themes = {
// default
d3: d3.scale.category10().range(),
// material
material: [
'#f44336', // Red
'#E91E63', // Pink
'#9c27b0', // Purple
'#673ab7', // Deep Purple
'#3f51b5', // Indigo
'#2196F3', // Blue
'#03a9f4', // Light Blue
'#00bcd4', // Cyan
'#009688', // Teal
'#4caf50', // Green
'#8bc34a', // Light Green
'#cddc39', // Lime
'#ffeb3b', // Yellow
'#ffc107', // Amber
'#ff9800', // Orange
'#ff5722', // Deep Orange
'#795548', // Brown
'#9e9e9e', // Grey
'#607d8b', // Blue Grey
],
// chart.js
chartjs: [
'#ff6384', '#ff9f40', '#ffcd56', '#4bc0c0',
'#36a2eb', '#9966ff', '#c9cbcf',
],
// echart - roma
roma: [
'#E01F54','#001852','#f5e8c8','#b8d2c7','#c6b38e',
'#a4d8c2','#f3d999','#d3758f','#dcc392','#2e4783',
'#82b6e9','#ff6347','#a092f1','#0a915d','#eaf889',
'#6699FF','#ff6666','#3cb371','#d5b158','#38b6b6',
],
// echart - macarons
macarons: [
'#2ec7c9','#b6a2de','#5ab1ef','#ffb980','#d87a80',
'#8d98b3','#e5cf0d','#97b552','#95706d','#dc69aa',
'#07a2a4','#9a7fd1','#588dd5','#f5994e','#c05050',
'#59678c','#c9ab00','#7eb00a','#6f5553','#c14089',
]
};
function colors(names, shades, type) {
var given = themes[names] ? themes[names] : names;
given = given || themes.material;
given = _.isArray(given) ? given : given.split(',');
if (given && shades > 1) {
var n = Math.max(0, Math.min(+(shades) || 4, 4));
return _.flatten(given.map(function (c) {
return _.first(_.range(0, n + 1).map(d3.scale.linear().domain([0, n + 1]).range([c, 'white'])), n);
}));
}
return given;
}
var CHARTS = {};
function PlusData(series, data) {
var result = _.chain(data.dataset)
.groupBy(data.xAxis)
.map(function (group, name) {
var value = 0;
_.each(group, function (item) {
value += $conv(item[series.key]);
});
var raw = {};
if (group[0]) {
raw[data.xAxis] = name;
raw[series.key] = value;
raw[data.xAxis + 'Id'] = group[0][data.xAxis + 'Id'];
}
return {
x: name === 'null' ? 'N/A' : name,
y: value,
raw: raw
};
}).value();
return result;
}
function PlotData(series, data) {
var ticks = _.chain(data.dataset).pluck(data.xAxis).unique().map(function (v) { return $conv(v, data.xType); }).value();
var groupBy = series.groupBy;
var datum = [];
_.chain(data.dataset).groupBy(groupBy)
.map(function (group, groupName) {
var name = groupBy ? groupName : null;
var values = _.map(group, function (item) {
var x = $conv(item[data.xAxis], data.xType) || 0;
var y = $conv(item[series.key] || name || 0);
return { x: x, y: y, raw: item };
});
var my = _.pluck(values, 'x');
var missing = _.difference(ticks, my);
if (ticks.length === missing.length) {
return;
}
_.each(missing, function(x) {
values.push({ x: x, y: 0 });
});
values = _.sortBy(values, 'x');
datum.push({
key: name || series.title,
type: series.type,
values: values
});
});
return datum;
}
function PieChart(scope, element, data) {
var series = _.first(data.series);
var datum = PlusData(series, data);
var config = data.config || {};
var chart = nv.models.pieChart()
.showLabels(false)
.height(null)
.width(null)
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
if (series.type === "donut") {
chart.donut(true)
.donutRatio(0.40);
}
if (_.toBoolean(config.percent)) {
chart.showLabels(true)
.labelType("percent")
.labelThreshold(0.05);
}
d3.select(element[0])
.datum(datum)
.transition().duration(1200).call(chart);
chart.pie.dispatch.on('elementClick', function (e) {
scope.handleClick(e);
});
return chart;
}
CHARTS.pie = PieChart;
CHARTS.donut = PieChart;
function DBarChart(scope, element, data) {
var series = _.first(data.series);
var datum = PlusData(series, data);
datum = [{
key: data.title,
values: datum
}];
var chart = nv.models.discreteBarChart()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.staggerLabels(true)
.showValues(true);
d3.select(element[0])
.datum(datum)
.transition().duration(500).call(chart);
chart.discretebar.dispatch.on('elementClick', function (e) {
scope.handleClick(e);
});
return chart;
}
function BarChart(scope, element, data) {
var series = _.first(data.series);
var datum = PlotData(series, data);
var chart = nv.models.multiBarChart()
.reduceXTicks(false);
chart.multibar.hideable(true);
chart.stacked(data.stacked);
d3.select(element[0])
.datum(datum)
.transition().duration(500).call(chart);
chart.multibar.dispatch.on('elementClick', function (e) {
scope.handleClick(e);
});
return chart;
}
function HBarChart(scope, element, data) {
var series = _.first(data.series);
var datum = PlotData(series, data);
var chart = nv.models.multiBarHorizontalChart();
chart.stacked(data.stacked);
d3.select(element[0])
.datum(datum)
.transition().duration(500).call(chart);
chart.multibar.dispatch.on('elementClick', function (e) {
scope.handleClick(e);
});
return chart;
}
function FunnelChart(scope, element, data) {
if(!data.dataset){
return;
}
var chart = new D3Funnel(element[0]);
var w = element.width();
var h = element.height();
var config = _.extend({}, data.config);
var props = {
fillType: 'gradient',
hoverEffects: true,
dynamicArea: true,
animation: 200};
if(config.width){
props.width = w*config.width/100;
}
if(config.height){
props.height = h*config.height/100;
}
var series = _.first(data.series) || {};
var opts = [];
_.each(data.dataset, function(dat){
opts.push([dat[data.xAxis],($conv(dat[series.key])||0)]);
});
chart.draw(opts, props);
chart.update = function(){};
return chart;
}
CHARTS.bar = BarChart;
CHARTS.dbar = DBarChart;
CHARTS.hbar = HBarChart;
CHARTS.funnel = FunnelChart;
function LineChart(scope, element, data) {
var series = _.first(data.series);
var datum = PlotData(series, data);
var chart = nv.models.lineChart()
.showLegend(true)
.showYAxis(true)
.showXAxis(true);
applyXY(chart, data);
d3.select(element[0])
.datum(datum)
.transition().duration(500).call(chart);
return chart;
}
function AreaChart(scope, element, data) {
var series = _.first(data.series);
var datum = PlotData(series, data);
var chart = nv.models.stackedAreaChart();
applyXY(chart, data);
d3.select(element[0])
.datum(datum)
.transition().duration(500).call(chart);
return chart;
}
CHARTS.line = LineChart;
CHARTS.area = AreaChart;
function RadarCharter(scope, element, data) {
var result = _.map(data.dataset, function(item) {
return _.map(data.series, function(s) {
var title = s.title || s.key,
value = item[s.key];
return {
axis: title,
value: $conv(value) || 0
};
});
});
var id = _.uniqueId('_radarChart'),
parent = element.parent();
parent.attr('id', id)
.addClass('radar-chart')
.empty();
var size = Math.min(parent.innerWidth(), parent.innerHeight());
RadarChart.draw('#'+id, result, {
w: size,
h: size
});
parent.children('svg')
.css('width', 'auto')
.css('margin', 'auto')
.css('margin-top', 10);
return null;
}
function GaugeCharter(scope, element, data) {
var config = data.config,
min = +(config.min) || 0,
max = +(config.max) || 100,
value = 0;
var item = _.first(data.dataset),
series = _.first(data.series),
key = series.key || data.xAxis;
if (item) {
value = item[key] || value;
}
var w = element.width();
var h = element.height();
var parent = element.hide().parent();
parent.children('svg').remove();
parent.append(element);
var chart = GaugeChart(parent[0], {
size: 300,
clipWidth: 300,
clipHeight: h,
ringWidth: 60,
minValue: min,
maxValue: max,
transitionMs: 4000
});
chart.render();
chart.update(value);
parent.children('svg:last')
.css('display', 'block')
.css('width', 'auto')
.css('margin', 'auto')
.css('margin-top', 0);
}
function TextChart(scope, element, data) {
var config = _.extend({
strong: true,
shadow: false,
fontSize: 22
}, data.config);
var values = _.first(data.dataset) || {};
var series = _.first(data.series) || {};
var value = values[series.key];
if (config.format) {
value = _t(config.format, value);
}
var svg = d3.select(element.empty()[0]);
var text = svg.append("svg:text")
.attr("x", "50%")
.attr("y", "50%")
.attr("dy", ".3em")
.attr("text-anchor", "middle")
.text(value);
if (config.color) text.attr("fill", config.color);
if (config.fontSize) text.style("font-size", config.fontSize);
if (_.toBoolean(config.strong)) text.style("font-weight", "bold");
if (_.toBoolean(config.shadow)) text.style("text-shadow", "0 1px 2px rgba(0, 0, 0, .5)");
}
CHARTS.text = TextChart;
CHARTS.radar = RadarCharter;
CHARTS.gauge = GaugeCharter;
function Chart(scope, element, data) {
var type = null;
var config = data.config || {};
for(var i = 0 ; i < data.series.length ; i++) {
type = data.series[i].type;
if (type === "bar" && !data.series[i].groupBy) type = "dbar";
if (type === "pie" || type === "dbar" || type === "radar" || type === "gauge") {
break;
}
}
if (type === "pie" && data.series.length > 1) {
return;
}
if (type !== "radar" && data.series.length > 1) {
type = "multi";
}
// clean up last instance
(function () {
var chart = element.off('adjustSize').empty().data('chart');
if (chart && chart.tooltip && chart.tooltip.id) {
d3.select('#' + chart.tooltip.id()).remove();
}
})();
nv.addGraph(function generate() {
var noData = _t('No records found.');
if (data.dataset && data.dataset.stacktrace) {
noData = data.dataset.message;
data.dataset = [];
}
var maker = CHARTS[type] || CHARTS.bar || function () {};
var chart = maker(scope, element, data);
if (!chart) {
return;
}
// series scale attribute
var series = _.first(data.series);
var scale = series && series.scale;
// format as integer if no scale is specified
// and data has integer series values
if (!isInteger(scale) && hasIntegerValues(data)) {
scale = 0;
}
if (isInteger(scale)) {
var format = '.' + scale + 'f';
chart.yAxis && chart.yAxis.tickFormat(d3.format(format));
chart.valueFormat && chart.valueFormat(d3.format(format));
}
if (chart.color) {
chart.color(colors(config.colors, config.shades, type));
}
if (chart.noData) {
chart.noData(noData);
}
if(chart.controlLabels) {
chart.controlLabels({
grouped: _t('Grouped'),
stacked: _t('Stacked'),
stream: _t('Stream'),
expanded: _t('Expanded'),
stack_percent: _t('Stack %')
});
}
var tickFormats = {
"date" : function (d) {
var f = config.xFormat;
return moment(d).format(f || 'YYYY-MM-DD');
},
"month" : function(d) {
var v = "" + d;
var f = config.xFormat;
if (v.indexOf(".") > -1) return "";
if (_.isString(d) && /^(\d+)$/.test(d)) {
d = parseInt(d);
}
if (_.isNumber(d)) {
return moment([moment().year(), d - 1, 1]).format(f || "MMM");
}
if (_.isString(d) && d.indexOf('-') > 0) {
return moment(d).format(f || 'MMM, YYYY');
}
return d;
},
"year" : function(d) {
return moment([moment().year(), d - 1, 1]).format("YYYY");
},
"number": d3.format(',f'),
"decimal": d3.format(',.1f'),
"text": function(d) { return d; }
};
var tickFormat = tickFormats[data.xType];
if (chart.xAxis && tickFormat) {
chart.xAxis
.rotateLabels(-45)
.tickFormat(tickFormat);
}
if (chart.yAxis && data.yTitle) {
chart.yAxis.axisLabel(data.yTitle);
}
var margin = data.xType === 'date' ? { 'bottom': 65 } : null;
['top', 'left', 'bottom', 'right'].forEach(function (side) {
var key = 'margin-' + side;
var val = parseInt(config[key]);
if (val) {
(margin||(margin={}))[side] = val;
}
});
if (chart.margin && margin) {
chart.margin(margin);
}
var lastWidth = 0;
var lastHeight = 0;
function adjust() {
if (!element[0] || element.parent().is(":hidden")) {
return;
}
var rect = element[0].getBoundingClientRect();
var w = rect.width,
h = rect.height;
if (w === lastWidth && h === lastHeight) {
return;
}
lastWidth = w;
lastHeight = h;
chart.update();
}
element.data('chart', chart);
scope.$onAdjust(adjust, 100);
setTimeout(chart.update, 10);
return chart;
});
}
function hasIntegerValues(data) {
var series = _.first(data.series);
var dataset = _.first(data.dataset);
return series && dataset && isInteger(dataset[series.key]);
}
function isInteger(n) {
return (n ^ 0) === n;
}
var directiveFn = function(){
return {
controller: ChartCtrl,
link: function(scope, element, attrs) {
var svg = element.children('svg');
var form = element.children('.chart-controls');
function doExport(data) {
var dataset = data.dataset || [];
var header = [];
_.each(dataset, function (item) {
header = _.unique(_.flatten([header, _.keys(item)]));
});
var content = "data:text/csv;charset=utf-8," + header.join(';') + '\n';
dataset.forEach(function (item) {
var row = header.map(function (key) {
var val = item[key];
if (val === undefined || val === null) {
val = '';
}
return '"' + (''+val).replace(/"/g, '""') + '"';
});
content += row.join(';') + '\n';
});
var name = (data.title || 'export').toLowerCase();
ui.download(encodeURI(content), _.underscored(name) + '.csv');
}
scope.render = function(data) {
if (element.is(":hidden")) {
return;
}
setTimeout(function () {
svg.height(element.height() - form.height()).width('100%');
if (!scope.dashlet || !scope.dashlet.title) {
scope.title = data.title;
}
Chart(scope, svg, data);
var canExport = data && _.isArray(data.dataset);
scope.canExport = function () {
return canExport;
};
scope.onExport = function () {
doExport(data);
};
scope.onAction = function () {
scope.handleAction(data && data.dataset);
};
return;
});
};
function onNewOrEdit() {
if (scope.searchInit && scope.searchFields) {
return;
}
scope.onRefresh(true);
}
scope.$on('on:new', onNewOrEdit);
scope.$on('on:edit', onNewOrEdit);
},
replace: true,
template:
'<div class="chart-container" style="background-color: white; ">'+
'<div ui-chart-form></div>'+
'<svg></svg>'+
'</div>'
};
};
ui.directive('uiChartForm', function () {
return {
scope: true,
controller: ChartFormCtrl,
link: function (scope, element, attrs, ctrls) {
},
replace: true,
template:
"<div class='chart-controls'>" +
"<div ui-view-form x-handler='this'></div>" +
"</div>"
};
});
ui.directive('uiViewChart', directiveFn);
ui.directive('uiPortletChart', directiveFn);
})();