/*
* Copyright 2023 Anyware Services
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* This tool displays some statistics about entries of a given form.
* @private
*/
Ext.define('Ametys.plugins.forms.tool.FormsStatisticsTool', {
extend: 'Ametys.tool.SelectionTool',
/**
* @private
* @property {Ext.panel.Panel} _mainPanel The main panel of this tool.
*/
constructor: function(config)
{
this.callParent(arguments);
Ametys.message.MessageBus.on(Ametys.message.Message.CREATED, this._onMessageCreated, this);
Ametys.message.MessageBus.on(Ametys.message.Message.MODIFIED, this._onMessageModified, this);
Ametys.message.MessageBus.on(Ametys.message.Message.DELETED, this._onMessageDeleted, this);
},
setParams: function(params)
{
this.callParent(arguments);
// Register the tool on the history tool
var toolId = this.getFactory().getId();
var toolParams = this.getParams();
Ametys.navhistory.HistoryDAO.addEntry({
id: this.getId(),
label: this.getTitle(),
description: this.getDescription(),
iconSmall: this.getSmallIcon(),
iconMedium: this.getMediumIcon(),
iconLarge: this.getLargeIcon(),
type: Ametys.navhistory.HistoryDAO.TOOL_TYPE,
action: Ext.bind(Ametys.tool.ToolsManager.openTool, Ametys.tool.ToolsManager, [toolId, toolParams], false)
});
},
createPanel: function()
{
this._mainPanel = Ext.create('Ext.panel.Panel', {
scrollable: true,
border: false,
cls: 'form-statistics-panel'
});
return this._mainPanel;
},
refresh: function()
{
var formId = this._currentSelectionTargets[0].getParameters().id;
if (formId)
{
this._mainPanel.unmask();
Ametys.plugins.forms.dao.FormDAO.getStatistics([formId], this._drawPanels, {scope: this, arguments: [Ext.bind(this.showUpToDate, this)]})
}
},
setNoSelectionMatchState: function (message)
{
this.callParent(arguments);
if (this._mainPanel)
{
if (this._mainPanel.rendered)
{
this._mainPanel.mask("{{i18n PLUGINS_FORMS_STATISTICS_TOOL_NOSELECTION}}", 'form-no-selection-match-mask');
}
else
{
this._mainPanel.on("afterrender", function (panel) {
panel.mask("{{i18n PLUGINS_FORMS_STATISTICS_TOOL_NOSELECTION}}", 'form-no-selection-match-mask');
})
}
}
},
/**
* Draws the panels after retrieving the stats from the server.
* @param {Object} stats The stats retrieved.
* @param {Array} args The arguments :
* @param {Function} args.callback The callback function after the panels were drawn.
* @private
*/
_drawPanels: function(stats, args)
{
this._mainPanel.removeAll();
var questions = stats.questions;
var globalPanel = this._getGlobalPanel(stats);
this._mainPanel.add(globalPanel);
if (stats.nbEntries > 0)
{
for (var i = 0; i < questions.length; i++) {
this._mainPanel.add(this._getQuestionPanel(questions[i], i+1));
}
}
var cb = args[0];
if (Ext.isFunction(cb))
{
cb();
}
},
/**
* Gets the first panel which displays the title of the form and the nbEntries count.
* @param {Object} stats The stats.
* @private
*/
_getGlobalPanel: function(stats)
{
var html =
'<h1 class="title" >' +
"{{i18n PLUGINS_FORMS_STATISTICS_GLOBAL_TITLE_1}}" +
stats.title +
"{{i18n PLUGINS_FORMS_STATISTICS_GLOBAL_TITLE_2}}" +
'</h1>' +
'<p>' +
"{{i18n PLUGINS_FORMS_STATISTICS_GLOBAL_TEXT_1}}" +
stats.nbEntries +
( stats.nbEntries == 1 ? "{{i18n PLUGINS_FORMS_STATISTICS_GLOBAL_TEXT_SINGLE}}" : "{{i18n PLUGINS_FORMS_STATISTICS_GLOBAL_TEXT_MULTIPLE}}")
'</p>'
;
var globalPanel = Ext.create('Ext.panel.Panel', {
cls: 'form-global-panel',
html: html
});
return globalPanel;
},
/**
* Gets the panel for a question.
* @param {Object} question The question object.
* @param {Number} index The index of this question.
* @private
*/
_getQuestionPanel: function(question, index)
{
var title = "{{i18n PLUGINS_FORMS_STATISTICS_QUESTION_PANEL_TITLE_1}}" + index + "{{i18n PLUGINS_FORMS_STATISTICS_QUESTION_PANEL_TITLE_2}}" + question.title;
if (question.mandatory)
{
title = title + "{{i18n PLUGINS_FORMS_STATISTICS_QUESTION_PANEL_TITLE_MANDATORY}}";
}
var type = question.typeId;
var questionPanel;
if (type == 'form.ChoicesList')
{
questionPanel = this._getChoiceQuestionPanel(question, title);
}
else if (type == 'form.Matrix')
{
questionPanel = this._getMatrixQuestionPanel(question, title);
}
else
{
questionPanel = this._getDefaultQuestionPanel(question, title);
}
return questionPanel;
},
/**
* Gets the panel for a default question.
* @param {Object} question The question object.
* @param {String} title The title of this panel.
* @private
*/
_getDefaultQuestionPanel: function(question, title)
{
var graphData = [];
var data = [];
var choices = question.options[0].choices;
var count,
totalCount = 0;
for (var i = 0; i < choices.length; i++)
{
var choiceValue = choices[i].value;
if (choiceValue == 'answered' || choiceValue == 'empty')
{
count = parseInt(choices[i].count);
choiceValueLabel = choiceValue == 'answered' ? "{{i18n PLUGINS_FORMS_STATISTICS_ANSWERED}}" : "{{i18n PLUGINS_FORMS_STATISTICS_NOT_ANSWERED}}"
totalCount += count;
graphData.push({
label: choiceValueLabel,
count: count
});
data.push([choiceValueLabel, count]);
}
else if (choiceValue == 'true' || choiceValue == 'false')
{
count = parseInt(choices[i].count);
choiceValueLabel = choiceValue == 'true' ? "{{i18n PLUGINS_FORMS_STATISTICS_TRUE}}" : "{{i18n PLUGINS_FORMS_STATISTICS_FALSE}}"
totalCount += count;
graphData.push({
label: choiceValueLabel,
count: count
});
data.push([choiceValueLabel, count]);
}
}
var gridPanel = Ext.create('Ext.grid.Panel', {
cls: 'question-text-grid',
scrollable: true,
region: 'center',
store: {
autoDestroy: true,
fields: ['label', 'count'],
data: data
},
stateful: true,
stateId: this.self.getName() + "$grid-question-text-" + question.id,
columns: [
{stateId: "grid-question-text-" + question.id + '-column-text', header: "{{i18n PLUGINS_FORMS_STATISTICS_TEXT_LABEL}}", sortable: false, dataIndex: 'label'},
{stateId: "grid-question-choice-" + question.id + '-column-count', header: "{{i18n PLUGINS_FORMS_STATISTICS_CHOICE_COUNT}}", sortable: true, dataIndex: 'count', width: 100,
renderer: function (value, record) {
// BIG HACK because graph does not handle well the value 0.
if (value == 0.00000001)
{
return 0;
}
return value;
}
},
{stateId: "grid-question-choice-" + question.id + '-column-proportion', header: "{{i18n PLUGINS_FORMS_STATISTICS_CHOICE_PERCENT}}", sortable: true, dataIndex: 'count', width: 100,
renderer: function(value) {
var percentage = (totalCount == 0) ? 0 : Math.round(value*100 / totalCount);
return percentage + '%';
}
}],
forceFit: true,
disableSelection: true
});
var graphPanel = Ext.create('Ext.chart.PolarChart', {
region: 'east',
width: 400,
store: {
fields: ['label', 'count'],
data: graphData
},
legend: {
docked: 'right',
toggleable: false
},
series: [{
type: 'pie',
xField: 'count',
label: {
field: 'label',
calloutLine: {
color: 'rgba(0,0,0,0)' // Transparent to hide callout line
},
renderer: function(val) {
return ''; // Empty label to hide text
}
},
tips: {
trackMouse: true,
renderer: function(tooltip, item) {
var percentage = (totalCount == 0) ? 0 : Math.round(item.get('count')*100 / totalCount);
tooltip.setHtml(item.get('label') + '<br/>' + item.get('count') + '<br/>' + percentage + '%');
}
}
}]
});
var questionPanel = Ext.create('Ext.panel.Panel', {
cls: 'form-question-panel question-text',
title: title,
layout: 'border',
collapsible: true,
scrollable: false,
height: 200,
items: [ gridPanel, graphPanel ]
});
return questionPanel;
},
/**
* Gets the panel for a CHOICE question.
* @param {Object} question The question object.
* @param {String} title The title of this panel.
* @private
*/
_getChoiceQuestionPanel: function(question, title)
{
var data = [];
var choices = question.options[0].choices;
var count,
hiddenTab = [],
totalCount = 0;
for (var i = 0; i < choices.length; i++)
{
count = choices[i].count;
totalCount += count;
hiddenTab.push(count == 0)
data.push([
choices[i].label,
count,
choices[i].value
]);
}
var store = Ext.create('Ext.data.Store', {
fields: [
{ name: 'label' },
{
name: 'count',
convert: function (value, record) {
// BIG HACK because graph does not handle well the value 0.
if (value == 0)
{
return 0.00000001;
}
return value;
}
},
{ name: 'value'}
],
data: data
});
var gridPanel = Ext.create('Ext.grid.Panel', {
cls: 'question-text-grid',
scrollable: true,
region: 'center',
store: store,
stateful: true,
stateId: this.self.getName() + "$grid-question-choice-" + question.id,
columns: [
{stateId: "grid-question-choice-" + question.id + '-column-label', header: "{{i18n PLUGINS_FORMS_STATISTICS_CHOICE_LABEL}}", sortable: true, dataIndex: 'label', width: 600,
renderer: Ext.bind(this._renderLabelWithColor, this)
},
{stateId: "grid-question-choice-" + question.id + '-column-count', header: "{{i18n PLUGINS_FORMS_STATISTICS_CHOICE_COUNT}}", sortable: true, dataIndex: 'count', width: 100,
renderer: function (value, record) {
// BIG HACK because graph does not handle well the value 0.
if (value == 0.00000001)
{
return 0;
}
return value;
}
},
{stateId: "grid-question-choice-" + question.id + '-column-proportion', header: "{{i18n PLUGINS_FORMS_STATISTICS_CHOICE_PERCENT}}", sortable: true, dataIndex: 'count', width: 100,
renderer: function(value) {
var percentage = (totalCount == 0) ? 0 : Math.round(value*100 / totalCount);
return percentage + '%';
}
}
],
forceFit: true,
disableSelection: true
});
var graphPanel = Ext.create('Ext.chart.PolarChart', {
region: 'east',
width: 400,
store: store,
series: [{
type: 'pie',
xField: 'count',
label: {
field: 'label',
calloutLine: {
color: 'rgba(0,0,0,0)' // Transparent to hide callout line
},
renderer: function(val) {
return ''; // Empty label to hide text
}
},
// BIG HACK because graph does not handle well the value 0.
hidden: hiddenTab,
tooltip: {
trackMouse: true,
renderer: function(tooltip, item) {
var percentage = (totalCount == 0) ? 0 : Math.round(item.get('count')*100 / totalCount);
var value = item.get('label');
var title = value;
if (value == '__internal_other')
{
title = "{{i18n plugin.forms:PLUGINS_FORMS_DISPLAY_OTHER_OPTION_COMBOBOX}}";
}
else if (value == '__internal_not_answered')
{
title = "{{i18n plugin.forms:PLUGINS_FORMS_STATISTICS_NOT_ANSWERED}}";
}
tooltip.setHtml(title + '<br/>' + item.get('count') + '<br/>' + percentage + '%');
}
}
}]
});
var questionPanel = Ext.create('Ext.panel.Panel', {
cls: 'form-question-panel question-choice',
title: title,
layout: 'border',
collapsible: true,
scrollable: false,
height: Math.max(200, 55+21 * choices.length),
items: [ gridPanel, graphPanel ]
});
return questionPanel;
},
/**
* Render label to add color hint for legend grapg
* @param {String} value The label.
* @param {Object} record The record
* @private
*/
_renderLabelWithColor: function(value, record)
{
var colors = Ext.Factory.chartTheme("default").getColors()
var index = record.recordIndex % colors.length;
var title = value;
if (value == '__internal_other')
{
title = "{{i18n plugin.forms:PLUGINS_FORMS_DISPLAY_OTHER_OPTION_COMBOBOX}}";
}
else if (value == '__internal_not_answered')
{
title = "{{i18n plugin.forms:PLUGINS_FORMS_STATISTICS_NOT_ANSWERED}}";
}
return "<span style=\"width:10px;height:10px;background-color:" + colors[index] + ";display: inline-block;border-radius:50%;margin-right:5px;margin-bottom:-1px;\"></span>" + title;
},
/**
* Gets the panel for a MATRIX question.
* @param {Object} question The question object.
* @param {String} title The title of this panel.
* @private
*/
_getMatrixQuestionPanel: function(question, title)
{
var data = [],
yFields = [],
titles = [];
var columns = [{
stateId: "grid-question-matrix-" + question.id + '-column-label',
header: "{{i18n PLUGINS_FORMS_STATISTICS_MATRIX_OPTION}}",
sortable: true,
dataIndex: 'label',
width: 600
}];
var choices = question.options[0].choices;
for (var i = 0; i < choices.length; i++)
{
columns.push({
stateId: "grid-question-matrix-" + question.id + '-column-choice-' + choices[i].value,
header: this._renderLabelWithColor(choices[i].label, {recordIndex : i}),
sortable: true,
dataIndex: choices[i].value,
width: 100,
renderer: function(value, metaData, record, rowIndex) {
var percentage = (totalCount[rowIndex] == 0) ? 0 : Math.round(value*100 / totalCount[rowIndex]);
return value + ' (' + percentage + '%' + ')';
}
});
yFields.push(choices[i].value);
titles.push(choices[i].label);
}
var options = question.options,
optionData,
choices,
totalCount = [];
for (var i = 0; i < options.length; i++) {
optionData = {"label": options[i].label};
choices = options[i].choices;
totalCount[i] = 0;
for (var j = 0; j < choices.length; j++) {
count = parseInt(choices[j].count);
totalCount[i] += count;
optionData[choices[j].value] = count;
}
data.push(optionData);
}
var store = Ext.create('Ext.data.Store', {
data: data
});
var gridPanel = Ext.create('Ext.grid.Panel', {
cls: 'question-text-grid',
scrollable: true,
region: 'center',
store: store,
stateful: true,
stateId: this.self.getName() + "$grid-question-matrix-" + question.id,
columns: columns,
forceFit: true,
disableSelection: true
});
var graphPanel = Ext.create('Ext.chart.CartesianChart', {
region: 'east',
width: 400,
store: store,
flipXY: true,
axes: [{
type: 'numeric',
position: 'bottom',
grid: true,
minimum: 0
}, {
type: 'category',
position: 'left'
}],
series: [{
type: 'bar',
title: titles,
xField: 'label',
yField: yFields,
axis: 'bottom',
tooltip: {
trackMouse: true,
renderer: Ext.bind(this._renderTooltipForMatrixGraph, this, [totalCount, choices], 1),
}
}]
});
var questionPanel = Ext.create('Ext.panel.Panel', {
cls: 'form-question-panel question-matrix',
title: title,
layout: 'border',
collapsible: true,
scrollable: false,
height: Math.max(250, 100+21 * options.length),
items: [ gridPanel, graphPanel ]
});
return questionPanel;
},
/**
* Render tooltip for matrix graph
* @param {Object} tooltip The tooltip.
* @param {Object} totalCount The list of count for each choices.
* @param {Object} choices The list of choices.
* @param {Object} record The record.
* @param {Object} serie The selected serie.
* @private
*/
_renderTooltipForMatrixGraph: function(tooltip, totalCount, choices, record, serie)
{
var html = "<ul>";
for (var i in record.data)
{
var value = record.data[i];
if (value != 0)
{
for (var j in choices)
{
if (choices[j].value == i)
{
var label = choices[j].label;
var percentage = (totalCount[serie.index] == 0) ? 0 : Math.round(value*100 / totalCount[serie.index]);
html += "<li>" + label + ": " + value + " (" + percentage + "%)</li>";
}
}
}
}
html += "</ul>";
tooltip.setHtml(html);
},
/**
* Listener on creation message.
* @param {Ametys.message.Message} message The creation message.
* @private
*/
_onMessageCreated: function(message)
{
var pageTarget = message.getTarget(Ametys.message.MessageTarget.FORM_PAGE);
if (pageTarget && this._currentSelectionTargets && this._currentSelectionTargets.length > 0 && pageTarget.getParameters().formId == this._currentSelectionTargets[0].getParameters().id)
{
this.showOutOfDate();
}
var questionTarget = message.getTarget(Ametys.message.MessageTarget.FORM_QUESTION);
if (questionTarget && this._currentSelectionTargets && this._currentSelectionTargets.length > 0 && questionTarget.getParameters().formId == this._currentSelectionTargets[0].getParameters().id)
{
this.showOutOfDate();
}
},
/**
* Listener on edition message.
* @param {Ametys.message.Message} message The edition message.
* @private
*/
_onMessageModified: function(message)
{
var target = message.getTarget(Ametys.message.MessageTarget.FORM_TARGET);
if (target && this._currentSelectionTargets && this._currentSelectionTargets.length > 0 && target.getParameters().id == this._currentSelectionTargets[0].getParameters().id)
{
this.showOutOfDate();
}
},
/**
* Listener on deletion message.
* @param {Ametys.message.Message} message The deletion message.
* @private
*/
_onMessageDeleted: function(message)
{
var target = message.getTarget(Ametys.message.MessageTarget.FORM_TARGET);
if (target && this._currentSelectionTargets && this._currentSelectionTargets.length > 0 && target.getParameters().id == this._currentSelectionTargets[0].getParameters().id)
{
this.close();
}
}
});