[REF] reimplement journal dashboard widget with Chart.js
This commit is one part of the task 'removing nvd3'. We use now Chart.js to draw charts. Task ID: 1946138
This commit is contained in:
committed by
Mathieu Duckerts-Antoine
parent
2af74a5f53
commit
769806ad13
@@ -39,84 +39,19 @@
|
||||
|
||||
.o_sample_data_label {
|
||||
@include o-position-absolute($bottom: 25%, $right: 0);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Style for the widget "dashboard_graph"
|
||||
.o_dashboard_graph {
|
||||
&.o_graph_linechart {
|
||||
> svg {
|
||||
height: 75px;
|
||||
// Display cut bullets to full bullets
|
||||
g.nvd3.nv-wrap.nv-line clipPath {
|
||||
display: none;
|
||||
}
|
||||
position: relative;
|
||||
margin: 16px -16px;
|
||||
|
||||
// X axis
|
||||
g.nv-x.nv-axis {
|
||||
transform: translateY(50px);
|
||||
|
||||
// Remove max and min labels
|
||||
g.nv-axisMaxMin {
|
||||
display: none;
|
||||
}
|
||||
g.tick.major {
|
||||
// Don't know why, but leads to strange bold text
|
||||
// stroke: $o-brand-odoo;
|
||||
|
||||
line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// !important are needed because nvd3 uses inline style on elements
|
||||
|
||||
// Remove strokes
|
||||
g.nv-linesWrap {
|
||||
g.nv-group.nv-series-0 {
|
||||
fill-opacity: 0.3 !important;
|
||||
fill: $o-brand-odoo !important;
|
||||
stroke: $o-brand-odoo !important;
|
||||
|
||||
.nv-point {
|
||||
visibility: hidden;
|
||||
r: 3;
|
||||
stroke: $o-brand-odoo !important;
|
||||
fill: white;
|
||||
stroke-width: 2;
|
||||
}
|
||||
.nv-point:nth-child(n+1) {
|
||||
visibility: visible;
|
||||
fill-opacity: .95 !important;
|
||||
stroke-opacity: .95 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.o_graph_barchart {
|
||||
> svg {
|
||||
height: 75px;
|
||||
overflow: visible;
|
||||
g.nv-barsWrap {
|
||||
g.nv-group.nv-series-0 {
|
||||
g.past {
|
||||
opacity: 0.5;
|
||||
fill: $o-brand-odoo !important;
|
||||
}
|
||||
g.future {
|
||||
opacity: 0.5;
|
||||
fill: $o-brand-primary !important;
|
||||
}
|
||||
g.o_sample_data {
|
||||
opacity: 0.1;
|
||||
fill: gray !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
canvas {
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.o_sample_data .o_dashboard_graph.o_graph_linechart > svg g.nv-linesWrap g.nv-group.nv-series-0 {
|
||||
|
||||
@@ -2483,35 +2483,16 @@ var FieldToggleBoolean = AbstractField.extend({
|
||||
|
||||
var JournalDashboardGraph = AbstractField.extend({
|
||||
className: "o_dashboard_graph",
|
||||
cssLibs: [
|
||||
'/web/static/lib/nvd3/nv.d3.css'
|
||||
],
|
||||
jsLibs: [
|
||||
'/web/static/lib/nvd3/d3.v3.js',
|
||||
'/web/static/lib/nvd3/nv.d3.js',
|
||||
'/web/static/src/js/libs/nvd3.js'
|
||||
'/web/static/lib/Chart/Chart.js',
|
||||
],
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.graph_type = this.attrs.graph_type;
|
||||
this.data = JSON.parse(this.value);
|
||||
},
|
||||
start: function () {
|
||||
this._onResize = this._onResize.bind(this);
|
||||
nv.utils.windowResize(this._onResize);
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
destroy: function () {
|
||||
if ('nv' in window && nv.utils && nv.utils.offWindowResize) {
|
||||
// if the widget is destroyed before the lazy loaded libs (nv) are
|
||||
// actually loaded (i.e. after the widget has actually started),
|
||||
// nv is undefined, but the handler isn't bound yet anyway
|
||||
nv.utils.offWindowResize(this._onResize);
|
||||
}
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
/**
|
||||
* The widget view uses the nv(d3) lib to render the graph. This lib
|
||||
* The widget view uses the ChartJS lib to render the graph. This lib
|
||||
* requires that the rendering is done directly into the DOM (so that it can
|
||||
* correctly compute positions). However, the views are always rendered in
|
||||
* fragments, and appended to the DOM once ready (to prevent them from
|
||||
@@ -2535,23 +2516,7 @@ var JournalDashboardGraph = AbstractField.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_customizeChart: function () {
|
||||
if (this.graph_type === 'bar') {
|
||||
// Add classes related to time on each bar of the bar chart
|
||||
var bar_classes = _.map(this.data[0].values, function (v) {return v.type; });
|
||||
|
||||
_.each(this.$('.nv-bar'), function (v, k){
|
||||
// classList doesn't work with phantomJS & addClass doesn't work with a SVG element
|
||||
$(v).attr('class', $(v).attr('class') + ' ' + bar_classes[k]);
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Render the widget only when it is in the DOM. This is because nvd3
|
||||
* renders graph only when it is in DOM, apparently to compute available
|
||||
* height and width for instance.
|
||||
* Render the widget only when it is in the DOM.
|
||||
*
|
||||
* @override
|
||||
* @private
|
||||
@@ -2568,89 +2533,101 @@ var JournalDashboardGraph = AbstractField.extend({
|
||||
* @private
|
||||
*/
|
||||
_renderInDOM: function () {
|
||||
// note: the rendering of this widget is aynchronous as nvd3 does a
|
||||
// setTimeout(0) before executing the callback given to addGraph
|
||||
var self = this;
|
||||
this.$el.empty();
|
||||
this.chart = null;
|
||||
nv.addGraph(function () {
|
||||
self.$svg = self.$el.append('<svg>');
|
||||
switch (self.graph_type) {
|
||||
case "line":
|
||||
self.$svg.addClass('o_graph_linechart');
|
||||
|
||||
self.chart = nv.models.lineChart();
|
||||
self.chart.forceY([0]);
|
||||
self.chart.options({
|
||||
x: function (d, u) { return u; },
|
||||
margin: {'left': 0, 'right': 0, 'top': 5, 'bottom': 0},
|
||||
showYAxis: false,
|
||||
showLegend: false,
|
||||
});
|
||||
self.chart.xAxis.tickFormat(function (d) {
|
||||
var label = '';
|
||||
_.each(self.data, function (v){
|
||||
if (v.values[d] && v.values[d].x){
|
||||
label = v.values[d].x;
|
||||
}
|
||||
});
|
||||
return label;
|
||||
});
|
||||
self.chart.yAxis.tickFormat(d3.format(',.2f'));
|
||||
|
||||
self.chart.tooltip.contentGenerator(function (key) {
|
||||
return qweb.render('GraphCustomTooltip', {
|
||||
'color': key.point.color,
|
||||
'key': self.data[0].key,
|
||||
'value': d3.format(',.2f')(key.point.y)
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case "bar":
|
||||
self.$svg.addClass('o_graph_barchart');
|
||||
|
||||
self.chart = nv.models.discreteBarChart()
|
||||
.x(function (d) { return d.label; })
|
||||
.y(function (d) { return d.value; })
|
||||
.showValues(false)
|
||||
.showYAxis(false)
|
||||
.color(['#875A7B', '#526774', '#FA8072'])
|
||||
.margin({'left': 0, 'right': 0, 'top': 20, 'bottom': 20});
|
||||
|
||||
self.chart.xAxis.axisLabel(self.data[0].title);
|
||||
self.chart.yAxis.tickFormat(d3.format(',.2f'));
|
||||
|
||||
self.chart.tooltip.contentGenerator(function (key) {
|
||||
return qweb.render('GraphCustomTooltip', {
|
||||
'color': key.color,
|
||||
'key': self.data[0].key,
|
||||
'value': d3.format(',.2f')(key.data.value)
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
d3.select(self.$('svg')[0])
|
||||
.datum(self.data)
|
||||
.transition().duration(600)
|
||||
.call(self.chart);
|
||||
|
||||
self._customizeChart();
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onResize: function () {
|
||||
if (this.chart) {
|
||||
this.chart.update();
|
||||
this._customizeChart();
|
||||
var config, cssClass;
|
||||
if (this.graph_type === 'line') {
|
||||
config = this._getLineChartConfig();
|
||||
cssClass = 'o_graph_linechart';
|
||||
} else if (this.graph_type === 'bar') {
|
||||
config = this._getBarChartConfig();
|
||||
cssClass = 'o_graph_barchart';
|
||||
}
|
||||
this.$canvas = $('<canvas/>');
|
||||
this.$el.addClass(cssClass);
|
||||
this.$el.empty();
|
||||
this.$el.append(this.$canvas);
|
||||
var context = this.$canvas[0].getContext('2d');
|
||||
this.chart = new Chart(context, config);
|
||||
},
|
||||
_getLineChartConfig: function () {
|
||||
var labels = this.data[0].values.map(function (pt) {
|
||||
return pt.x;
|
||||
});
|
||||
var borderColor = this.data[0].is_sample_data ? '#dddddd' : '#875a7b';
|
||||
var backgroundColor = this.data[0].is_sample_data ? '#ebebeb' : '#dcd0d9';
|
||||
return {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: this.data[0].values,
|
||||
fill: 'start',
|
||||
label: this.data[0].key,
|
||||
backgroundColor: backgroundColor,
|
||||
borderColor: borderColor,
|
||||
borderWidth: 2,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
legend: {display: false},
|
||||
scales: {
|
||||
yAxes: [{display: false}],
|
||||
xAxes: [{display: false}]
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0.000001
|
||||
}
|
||||
},
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
position: 'nearest',
|
||||
caretSize: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
_getBarChartConfig: function () {
|
||||
var data = [];
|
||||
var labels = [];
|
||||
var backgroundColor = [];
|
||||
|
||||
this.data[0].values.forEach(function (pt) {
|
||||
data.push(pt.value);
|
||||
labels.push(pt.label);
|
||||
var color = pt.type === 'past' ? '#ccbdc8' : (pt.type === 'future' ? '#a5d8d7' : '#ebebeb');
|
||||
backgroundColor.push(color);
|
||||
});
|
||||
return {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
data: data,
|
||||
fill: 'start',
|
||||
label: this.data[0].key,
|
||||
backgroundColor: backgroundColor,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
legend: {display: false},
|
||||
scales: {
|
||||
yAxes: [{display: false}],
|
||||
},
|
||||
maintainAspectRatio: false,
|
||||
tooltips: {
|
||||
intersect: false,
|
||||
position: 'nearest',
|
||||
caretSize: 0,
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0.000001
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -187,7 +187,6 @@
|
||||
|
||||
.o_dashboard_graph {
|
||||
overflow: hidden;
|
||||
margin: ($o-kanban-dashboard-vpadding * 2) (-$o-kanban-dashboard-hpadding);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2269,10 +2269,7 @@ QUnit.module('basic_fields', {
|
||||
});
|
||||
|
||||
QUnit.test('graph dashboard widget attach/detach callbacks', async function (assert) {
|
||||
// This widget is rendered with nvd3, and nvd3 renders the graphs when
|
||||
// the svg is in the DOM. The intent of this test is to determine when
|
||||
// the field widgets are in the DOM, so that we can tell nvd3 to render
|
||||
// them.
|
||||
// This widget is rendered with Chart.js.
|
||||
var done = assert.async();
|
||||
assert.expect(6);
|
||||
|
||||
@@ -2319,9 +2316,8 @@ QUnit.module('basic_fields', {
|
||||
|
||||
QUnit.test('graph dashboard widget is rendered correctly', async function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(4);
|
||||
assert.expect(3);
|
||||
|
||||
var graph_key = JSON.parse(this.data.partner.records[0].graph_data)[0].key;
|
||||
createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
@@ -2336,34 +2332,20 @@ QUnit.module('basic_fields', {
|
||||
'</templates></kanban>',
|
||||
domain: [['id', 'in', [1, 2]]],
|
||||
}).then(function (kanban) {
|
||||
// nvd3 seems to do a setTimeout(0) each time the addGraph function is
|
||||
// called, which is done twice in this case as there are 2 records.
|
||||
// for that reason, we need to do two setTimeout(0) as well here to ensure
|
||||
// that both graphs are rendered before starting to check if the rendering
|
||||
// is correct.
|
||||
concurrency.delay(0).then(function () {
|
||||
return concurrency.delay(0);
|
||||
}).then(function () {
|
||||
assert.strictEqual(kanban.$('.o_kanban_record:first() .o_graph_barchart svg').length, 1,
|
||||
assert.strictEqual(kanban.$('.o_kanban_record:first() .o_graph_barchart').length, 1,
|
||||
"graph of first record should be a barchart");
|
||||
assert.strictEqual(kanban.$('.o_kanban_record:nth(1) .o_graph_linechart svg').length, 1,
|
||||
assert.strictEqual(kanban.$('.o_kanban_record:nth(1) .o_dashboard_graph').length, 1,
|
||||
"graph of second record should be a linechart");
|
||||
|
||||
var evt = document.createEvent("MouseEvents"); //taken ref from https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/initMouseEvent
|
||||
evt.initMouseEvent("mouseover", true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null);
|
||||
$('.discreteBar')[0].dispatchEvent(evt);
|
||||
var tooltip = $('.nvtooltip').find('table').find('.key')[0].innerText.trim();
|
||||
assert.equal(tooltip, graph_key, "graph tooltip should be generated ");
|
||||
$('.nvtooltip').remove();
|
||||
|
||||
// force a re-rendering of the first record (to check if the
|
||||
// previous rendered graph is correctly removed from the DOM)
|
||||
var firstRecordState = kanban.model.get(kanban.handle).data[0];
|
||||
return kanban.renderer.updateRecord(firstRecordState);
|
||||
}).then(function () {
|
||||
return concurrency.delay(0); // one graph is re-rendered
|
||||
return concurrency.delay(0);
|
||||
}).then(function () {
|
||||
assert.strictEqual(kanban.$('.o_kanban_record:first() svg').length, 1,
|
||||
assert.strictEqual(kanban.$('.o_kanban_record:first() canvas').length, 1,
|
||||
"there should be only one rendered graph by record");
|
||||
|
||||
kanban.destroy();
|
||||
@@ -2372,87 +2354,6 @@ QUnit.module('basic_fields', {
|
||||
});
|
||||
});
|
||||
|
||||
QUnit.test('graph dashboard widget does not need nv to be destroyed', async function (assert) {
|
||||
// this test ensures that the JournalDashboardGraph widget doesn't crash
|
||||
// when being destroyed before nv has been loaded
|
||||
assert.expect(2);
|
||||
|
||||
testUtils.mock.patch(basicFields.JournalDashboardGraph, {
|
||||
destroy: function () {
|
||||
assert.step('destroy');
|
||||
var nv = window.nv;
|
||||
delete window.nv;
|
||||
this._super.apply(this, arguments);
|
||||
window.nv = nv;
|
||||
}
|
||||
});
|
||||
|
||||
var kanban = await createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test">' +
|
||||
'<field name="graph_type"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>' +
|
||||
'</div>' +
|
||||
'</t>' +
|
||||
'</templates></kanban>',
|
||||
domain: [['id', 'in', [1]]],
|
||||
});
|
||||
|
||||
kanban.destroy();
|
||||
testUtils.mock.unpatch(basicFields.JournalDashboardGraph);
|
||||
|
||||
assert.verifySteps(['destroy']);
|
||||
|
||||
// Wait nvd3 to fully render the graph. If ommited, may slow down following tests.
|
||||
return concurrency.delay(0);
|
||||
});
|
||||
|
||||
QUnit.test('graph dashboard widget can be destroyed when nv is partially loaded', async function (assert) {
|
||||
// this test ensures that the JournalDashboardGraph widget doesn't crash
|
||||
// when being destroyed before nv has been completely loaded
|
||||
assert.expect(2);
|
||||
|
||||
testUtils.mock.patch(basicFields.JournalDashboardGraph, {
|
||||
destroy: function () {
|
||||
assert.step('destroy');
|
||||
// nv is fully loaded only when nvd3.js has been loaded
|
||||
// which happens sequentially after nv.d3.js
|
||||
// we simulate this race condition with:
|
||||
var offWindowResize = window.nv.utils.offWindowResize;
|
||||
window.nv.utils.offWindowResize = undefined;
|
||||
this._super.apply(this, arguments);
|
||||
window.nv.utils.offWindowResize = offWindowResize;
|
||||
},
|
||||
});
|
||||
|
||||
var kanban = await createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban class="o_kanban_test">' +
|
||||
'<field name="graph_type"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div>' +
|
||||
'<field name="graph_data" t-att-graph_type="record.graph_type.raw_value" widget="dashboard_graph"/>' +
|
||||
'</div>' +
|
||||
'</t>' +
|
||||
'</templates></kanban>',
|
||||
domain: [['id', 'in', [1]]],
|
||||
});
|
||||
|
||||
kanban.destroy();
|
||||
testUtils.mock.unpatch(basicFields.JournalDashboardGraph);
|
||||
|
||||
assert.verifySteps(['destroy']);
|
||||
|
||||
// Wait nvd3 to fully render the graph. If ommited, may slow down following tests.
|
||||
return concurrency.delay(0);
|
||||
});
|
||||
|
||||
QUnit.test('rendering of a field with dashboard_graph widget in an updated kanban view (ungrouped)', async function (assert) {
|
||||
|
||||
var done = assert.async();
|
||||
@@ -2473,16 +2374,12 @@ QUnit.module('basic_fields', {
|
||||
domain: [['id', 'in', [1, 2]]],
|
||||
}).then(function (kanban) {
|
||||
concurrency.delay(0).then(function () {
|
||||
return concurrency.delay(0);
|
||||
}).then(function () {
|
||||
assert.containsN(kanban, '.o_dashboard_graph svg', 2, "there should be two graph rendered");
|
||||
assert.containsN(kanban, '.o_dashboard_graph canvas', 2, "there should be two graph rendered");
|
||||
return kanban.update({});
|
||||
}).then(function () {
|
||||
return concurrency.delay(0); // one graph is re-rendered
|
||||
}).then(function () {
|
||||
return concurrency.delay(0); // one graph is re-rendered
|
||||
}).then(function () {
|
||||
assert.containsN(kanban, '.o_dashboard_graph svg', 2, "there should be one graph rendered");
|
||||
assert.containsN(kanban, '.o_dashboard_graph canvas', 2, "there should be one graph rendered");
|
||||
kanban.destroy();
|
||||
done();
|
||||
});
|
||||
@@ -2509,14 +2406,10 @@ QUnit.module('basic_fields', {
|
||||
domain: [['id', 'in', [1, 2]]],
|
||||
}).then(function (kanban) {
|
||||
concurrency.delay(0).then(function () {
|
||||
return concurrency.delay(0);
|
||||
}).then(function () {
|
||||
assert.containsN(kanban, '.o_dashboard_graph svg', 2, "there should be two graph rendered");
|
||||
assert.containsN(kanban, '.o_dashboard_graph canvas', 2, "there should be two graph rendered");
|
||||
return kanban.update({groupBy: ['selection'], domain: [['int_field', '=', 10]]});
|
||||
}).then(function () {
|
||||
return concurrency.delay(0);
|
||||
}).then(function () {
|
||||
assert.containsOnce(kanban, '.o_dashboard_graph svg', "there should be one graph rendered");
|
||||
assert.containsOnce(kanban, '.o_dashboard_graph canvas', "there should be one graph rendered");
|
||||
kanban.destroy();
|
||||
done();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user