[IMP] web: graph: add bar/line chart sorting
PURPOSE Currently, reporting views such as the bar and line charts have their x-axis sorted either alphabetically or according to a sequence. When reporting, the user would be interested in sorting the x-axis values by their measure. SPECIFICATIONS Add 'ascending' and 'descending' options in graph view for bar and line charts Task 2070103 closes odoo/odoo#49970 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com> Co-authored-by: Mohammed Shekha <msh@odoo.com>
This commit is contained in:
committed by
Aaron Bohy
co-authored by
Mohammed Shekha
parent
5d50509be8
commit
fa00ff97be
@@ -184,6 +184,11 @@ var GraphController = AbstractController.extend({
|
||||
.data('stacked', state.stacked)
|
||||
.toggleClass('active', state.stacked)
|
||||
.toggleClass('o_hidden', state.mode !== 'bar');
|
||||
this.$buttons
|
||||
.find('.o_graph_button[data-order]')
|
||||
.toggleClass('o_hidden', state.mode === 'pie' || !!Object.keys(state.timeRanges).length)
|
||||
.filter('.o_graph_button[data-order="' + state.orderBy + '"]')
|
||||
.toggleClass('active', !!state.orderBy);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -258,6 +263,10 @@ var GraphController = AbstractController.extend({
|
||||
this.update({ mode: $target.data('mode') });
|
||||
} else if ($target.data('mode') === 'stack') {
|
||||
this.update({ stacked: !$target.data('stacked') });
|
||||
} else if (['asc', 'desc'].includes($target.data('order'))) {
|
||||
const order = $target.data('order');
|
||||
const state = this.model.get();
|
||||
this.update({ orderBy: state.orderBy === order ? false : order });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -72,6 +72,7 @@ return AbstractModel.extend({
|
||||
origins: [],
|
||||
stacked: params.stacked,
|
||||
timeRanges: params.timeRanges,
|
||||
orderBy: params.orderBy
|
||||
});
|
||||
|
||||
this._computeDerivedParams();
|
||||
@@ -126,6 +127,10 @@ return AbstractModel.extend({
|
||||
this.chart.stacked = params.stacked;
|
||||
return Promise.resolve();
|
||||
}
|
||||
if ('orderBy' in params) {
|
||||
this.chart.orderBy = params.orderBy;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return this._loadGraph();
|
||||
},
|
||||
|
||||
|
||||
@@ -769,6 +769,7 @@ return AbstractRenderer.extend({
|
||||
});
|
||||
}
|
||||
var dataPoints = this._filterDataPoints();
|
||||
dataPoints = this._sortDataPoints(dataPoints);
|
||||
if (!dataPoints.length && this.state.mode !== 'pie') {
|
||||
this.$el.append(qweb.render('View.NoContentHelper'));
|
||||
} else if (this.isInDOM) {
|
||||
@@ -1006,6 +1007,38 @@ return AbstractRenderer.extend({
|
||||
}
|
||||
return shortLabel;
|
||||
},
|
||||
/**
|
||||
* Sort datapoints according to the current order (ASC or DESC).
|
||||
*
|
||||
* Note: this should be moved to the model at some point.
|
||||
*
|
||||
* @private
|
||||
* @param {Object[]} dataPoints
|
||||
* @returns {Object[]} sorted dataPoints if orderby set on state
|
||||
*/
|
||||
_sortDataPoints(dataPoints) {
|
||||
if (!Object.keys(this.state.timeRanges).length && this.state.orderBy &&
|
||||
['bar', 'line'].includes(this.state.mode) && this.state.groupBy.length) {
|
||||
// group data by their x-axis value, and then sort datapoints
|
||||
// based on the sum of values by group in ascending/descending order
|
||||
const groupByFieldName = this.state.groupBy[0].split(':')[0];
|
||||
const groupedByMany2One = this.fields[groupByFieldName].type === 'many2one';
|
||||
const groupedDataPoints = {};
|
||||
dataPoints.forEach(function (dataPoint) {
|
||||
const key = groupedByMany2One ? dataPoint.resId : dataPoint.labels[0];
|
||||
groupedDataPoints[key] = groupedDataPoints[key] || [];
|
||||
groupedDataPoints[key].push(dataPoint);
|
||||
});
|
||||
dataPoints = _.sortBy(groupedDataPoints, function (group) {
|
||||
return group.reduce((sum, dataPoint) => sum + dataPoint.value, 0);
|
||||
});
|
||||
dataPoints = dataPoints.flat();
|
||||
if (this.state.orderBy === 'desc') {
|
||||
dataPoints = dataPoints.reverse('value');
|
||||
}
|
||||
}
|
||||
return dataPoints;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
|
||||
@@ -134,6 +134,7 @@ var GraphView = AbstractView.extend({
|
||||
this.rendererParams.disableLinking = !!JSON.parse(this.arch.attrs.disable_linking || '0');
|
||||
|
||||
this.loadParams.mode = this.arch.attrs.type || 'bar';
|
||||
this.loadParams.orderBy = this.arch.attrs.order;
|
||||
this.loadParams.measure = measure || '__count__';
|
||||
this.loadParams.groupBys = groupBys;
|
||||
this.loadParams.fields = this.fields;
|
||||
|
||||
@@ -1046,6 +1046,10 @@
|
||||
<div class="btn-group" role="toolbar" aria-label="Change graph">
|
||||
<button class="btn btn-secondary fa fa-database o_graph_button" title="Stacked" aria-label="Stacked" data-mode="stack"/>
|
||||
</div>
|
||||
<div class="btn-group" role="toolbar" aria-label="Sort graph">
|
||||
<button class="btn btn-secondary fa fa-sort-amount-desc o_graph_button" title="Descending" aria-label="Descending" data-order="desc"/>
|
||||
<button class="btn btn-secondary fa fa-sort-amount-asc o_graph_button" title="Ascending" aria-label="Ascending" data-order="asc"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div t-name="GraphView.CustomTooltip" class="o_graph_custom_tooltip">
|
||||
|
||||
@@ -1119,6 +1119,162 @@ QUnit.module('Views', {
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('graph view sort by measure', async function (assert) {
|
||||
assert.expect(18);
|
||||
|
||||
// change first record from foo as there are 4 records count for each product
|
||||
this.data.product.records.push({ id: 38, display_name: "zphone"});
|
||||
this.data.foo.records[7].product_id = 38;
|
||||
|
||||
const graph = await createView({
|
||||
View: GraphView,
|
||||
model: "foo",
|
||||
data: this.data,
|
||||
arch: `<graph string="Partners" order="desc">
|
||||
<field name="product_id"/>
|
||||
</graph>`,
|
||||
});
|
||||
|
||||
assert.containsN(graph, 'button[data-order]', 2,
|
||||
"there should be two order buttons for sorting axis labels in bar mode");
|
||||
assert.checkLegend(graph, 'Count', 'measure should be by count');
|
||||
assert.hasClass(graph.$('button[data-order="desc"]'), 'active',
|
||||
'sorting should be applie on descending order by default when sorting="desc"');
|
||||
assert.checkDatasets(graph, 'data', {data: [4, 3, 1]});
|
||||
|
||||
await testUtils.dom.click(graph.$buttons.find('button[data-order="asc"]'));
|
||||
assert.hasClass(graph.$('button[data-order="asc"]'), 'active',
|
||||
"ascending order should be applied");
|
||||
assert.checkDatasets(graph, 'data', {data: [1, 3, 4]});
|
||||
|
||||
await testUtils.dom.click(graph.$buttons.find('button[data-order="desc"]'));
|
||||
assert.hasClass(graph.$('button[data-order="desc"]'), 'active',
|
||||
"descending order button should be active");
|
||||
assert.checkDatasets(graph, 'data', { data: [4, 3, 1] });
|
||||
|
||||
// again click on descending button to deactivate order button
|
||||
await testUtils.dom.click(graph.$buttons.find('button[data-order="desc"]'));
|
||||
assert.doesNotHaveClass(graph.$('button[data-order="desc"]'), 'active',
|
||||
"descending order button should not be active");
|
||||
assert.checkDatasets(graph, 'data', {data: [4, 3, 1]});
|
||||
|
||||
// set line mode
|
||||
await testUtils.dom.click(graph.$buttons.find('button[data-mode="line"]'));
|
||||
assert.containsN(graph, 'button[data-order]', 2,
|
||||
"there should be two order buttons for sorting axis labels in line mode");
|
||||
assert.checkLegend(graph, 'Count', 'measure should be by count');
|
||||
assert.doesNotHaveClass(graph.$('button[data-order="desc"]'), 'active',
|
||||
"descending order should be applied");
|
||||
assert.checkDatasets(graph, 'data', {data: [4, 3, 1]});
|
||||
|
||||
await testUtils.dom.click(graph.$buttons.find('button[data-order="asc"]'));
|
||||
assert.hasClass(graph.$('button[data-order="asc"]'), 'active',
|
||||
"ascending order button should be active");
|
||||
assert.checkDatasets(graph, 'data', { data: [1, 3, 4] });
|
||||
|
||||
await testUtils.dom.click(graph.$buttons.find('button[data-order="desc"]'));
|
||||
assert.hasClass(graph.$('button[data-order="desc"]'), 'active',
|
||||
"descending order button should be active");
|
||||
assert.checkDatasets(graph, 'data', { data: [4, 3, 1] });
|
||||
|
||||
graph.destroy();
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('graph view sort by measure for grouped data', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
// change first record from foo as there are 4 records count for each product
|
||||
this.data.product.records.push({ id: 38, display_name: "zphone", });
|
||||
this.data.foo.records[7].product_id = 38;
|
||||
|
||||
const graph = await createView({
|
||||
View: GraphView,
|
||||
model: "foo",
|
||||
data: this.data,
|
||||
arch: `<graph string="Partners">
|
||||
<field name="product_id"/>
|
||||
<field name="bar"/>
|
||||
</graph>`,
|
||||
});
|
||||
|
||||
assert.checkLegend(graph, ["true","false"], 'measure should be by count');
|
||||
assert.containsN(graph, 'button[data-order]', 2,
|
||||
"there should be two order buttons for sorting axis labels");
|
||||
assert.checkDatasets(graph, 'data', [{data: [3, 0, 0]}, {data: [1, 3, 1]}]);
|
||||
|
||||
await testUtils.dom.click(graph.$buttons.find('button[data-order="asc"]'));
|
||||
assert.hasClass(graph.$('button[data-order="asc"]'), 'active',
|
||||
"ascending order should be applied by default");
|
||||
assert.checkDatasets(graph, 'data', [{ data: [1, 3, 1] }, { data: [0, 0, 3] }]);
|
||||
|
||||
await testUtils.dom.click(graph.$buttons.find('button[data-order="desc"]'));
|
||||
assert.hasClass(graph.$('button[data-order="desc"]'), 'active',
|
||||
"ascending order button should be active");
|
||||
assert.checkDatasets(graph, 'data', [{data: [1, 3, 1]}, {data: [3, 0, 0]}]);
|
||||
|
||||
// again click on descending button to deactivate order button
|
||||
await testUtils.dom.click(graph.$buttons.find('button[data-order="desc"]'));
|
||||
assert.doesNotHaveClass(graph.$('button[data-order="desc"]'), 'active',
|
||||
"descending order button should not be active");
|
||||
assert.checkDatasets(graph, 'data', [{ data: [3, 0, 0] }, { data: [1, 3, 1] }]);
|
||||
|
||||
graph.destroy();
|
||||
|
||||
});
|
||||
|
||||
QUnit.test('graph view sort by measure for multiple grouped data', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
// change first record from foo as there are 4 records count for each product
|
||||
this.data.product.records.push({ id: 38, display_name: "zphone" });
|
||||
this.data.foo.records[7].product_id = 38;
|
||||
|
||||
// add few more records to data to have grouped data date wise
|
||||
const data = [
|
||||
{id: 9, foo: 48, bar: false, product_id: 41, date: "2016-04-01"},
|
||||
{id: 10, foo: 49, bar: false, product_id: 41, date: "2016-04-01"},
|
||||
{id: 11, foo: 50, bar: true, product_id: 37, date: "2016-01-03"},
|
||||
{id: 12, foo: 50, bar: true, product_id: 41, date: "2016-01-03"},
|
||||
];
|
||||
|
||||
Object.assign(this.data.foo.records, data);
|
||||
|
||||
const graph = await createView({
|
||||
View: GraphView,
|
||||
model: "foo",
|
||||
data: this.data,
|
||||
arch: `<graph string="Partners">
|
||||
<field name="product_id"/>
|
||||
<field name="date"/>
|
||||
</graph>`,
|
||||
groupBy: ['date', 'product_id']
|
||||
});
|
||||
|
||||
assert.checkLegend(graph, ["xpad","xphone","zphone"], 'measure should be by count');
|
||||
assert.containsN(graph, 'button[data-order]', 2,
|
||||
"there should be two order buttons for sorting axis labels");
|
||||
assert.checkDatasets(graph, 'data', [{data: [2, 1, 1, 2]}, {data: [0, 1, 0, 0]}, {data: [1, 0, 0, 0]}]);
|
||||
|
||||
await testUtils.dom.click(graph.$buttons.find('button[data-order="asc"]'));
|
||||
assert.hasClass(graph.$('button[data-order="asc"]'), 'active',
|
||||
"ascending order should be applied by default");
|
||||
assert.checkDatasets(graph, 'data', [{ data: [1, 1, 2, 2] }, { data: [0, 1, 0, 0] }, { data: [0, 0, 0, 1] }]);
|
||||
|
||||
await testUtils.dom.click(graph.$buttons.find('button[data-order="desc"]'));
|
||||
assert.hasClass(graph.$('button[data-order="desc"]'), 'active',
|
||||
"descending order button should be active");
|
||||
assert.checkDatasets(graph, 'data', [{data: [1, 0, 0, 0]}, {data: [2, 2, 1, 1]}, {data: [0, 0, 1, 0]}]);
|
||||
|
||||
// again click on descending button to deactivate order button
|
||||
await testUtils.dom.click(graph.$buttons.find('button[data-order="desc"]'));
|
||||
assert.doesNotHaveClass(graph.$('button[data-order="desc"]'), 'active',
|
||||
"descending order button should not be active");
|
||||
assert.checkDatasets(graph, 'data', [{ data: [2, 1, 1, 2] }, { data: [0, 1, 0, 0] }, { data: [1, 0, 0, 0] }]);
|
||||
|
||||
graph.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('GraphView: comparison mode', {
|
||||
beforeEach: async function () {
|
||||
this.data.foo.records[0].date = '2016-12-15';
|
||||
@@ -1287,7 +1443,7 @@ QUnit.module('Views', {
|
||||
},
|
||||
}, function () {
|
||||
QUnit.test('comparison with one groupby equal to comparison date field', async function (assert) {
|
||||
assert.expect(10);
|
||||
assert.expect(11);
|
||||
|
||||
this.combinationsToCheck = {
|
||||
'last_30_days,previous_period,day': {
|
||||
@@ -1335,6 +1491,9 @@ QUnit.module('Views', {
|
||||
await this.setMode('pie');
|
||||
await this.testCombinations(combinations, assert);
|
||||
|
||||
// isNotVisible can not have two elements so checking visibility of first element
|
||||
assert.isNotVisible(this.actionManager.$('button[data-order]:first'),
|
||||
"there should not be order button in comparison mode")
|
||||
assert.ok(true, "No combination causes a crash");
|
||||
});
|
||||
|
||||
|
||||
@@ -1077,6 +1077,10 @@ attributes:
|
||||
within a group
|
||||
``disable_linking``
|
||||
set to ``True`` to prevent from redirecting clicks on graph to list view
|
||||
``order``
|
||||
if set, x-axis values will be sorted by default according their measure with
|
||||
respect to the given order (``asc`` or ``desc``). Only used for ``bar`` and
|
||||
``pie`` charts.
|
||||
|
||||
The only allowed element within a graph view is ``field`` which can have the
|
||||
following attributes:
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
</rng:optional>
|
||||
<rng:optional><rng:attribute name="js_class"/></rng:optional>
|
||||
<rng:optional><rng:attribute name="stacked"/></rng:optional>
|
||||
<rng:optional><rng:attribute name="order"/></rng:optional>
|
||||
<rng:optional><rng:attribute name="orientation"/></rng:optional>
|
||||
<rng:optional><rng:attribute name="interval"/></rng:optional>
|
||||
<rng:optional><rng:attribute name="disable_linking"/></rng:optional>
|
||||
|
||||
Reference in New Issue
Block a user