[REF] *: Update Chart.js to V4.3

Change version of Chart.js from V2.9.3 to V4.3.0
You can find changelog on
https://www.chartjs.org/docs/latest/migration/v4-migration.html

Version 4.3.0 of chart.js no longer uses moment.js. This task is a
prerequisite to completely remove the use of moment.js (task 3391739)

Why add aspectRatio : 2 ? => Canvas aspect ratio (i.e. width / height,
a value of 1 representing a square canvas). Note that this option
is ignored if the height is explicitly defined either as attribute
or via the style. The default value varies by chart type; Radial charts
(doughnut, pie, polarArea, radar) default to 1 and others default to 2.

Why no more Chart.animationService.advance(); ?
There is no longer an equivalent in this version.
However, we have verified that the problem is no longer present in this
version.

Why use now getElementsAtEventForMode ?
It's clearly noted in the changelog. follow the link above.

task-3392075

Part-of: odoo/odoo#127259
This commit is contained in:
Pierre Pulinckx (pipu)
2023-08-16 12:47:34 +02:00
parent 63480e6895
commit 7e3c1ecdb8
24 changed files with 16675 additions and 16804 deletions
@@ -8,8 +8,8 @@ export class SkillsGraphRenderer extends GraphRenderer {
getScaleOptions() {
const scaleOptions = super.getScaleOptions();
if ('yAxes' in scaleOptions) {
scaleOptions['yAxes'][0]['ticks']['suggestedMax'] = 100;
if ('y' in scaleOptions) {
scaleOptions.y.suggestedMax = 100;
}
return scaleOptions;
+1
View File
@@ -226,6 +226,7 @@
'web/static/lib/nearest/jquery.nearest.js',
'web/static/lib/stacktracejs/stacktrace.js',
'web/static/lib/Chart/Chart.js',
'web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js',
# 'web/static/tests/legacy/main_tests.js',
'web/static/tests/helpers/**/*.js',
@@ -1,363 +0,0 @@
/*!
* chartjs-gauge.js v0.3.0
* https://github.com/haiiaaa/chartjs-gauge/
* (c) 2021 chartjs-gauge.js Contributors
* Released under the MIT License
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('chart.js')) :
typeof define === 'function' && define.amd ? define(['chart.js'], factory) :
(global = global || self, global.Gauge = factory(global.Chart));
}(this, (function (Chart) { 'use strict';
Chart = Chart && Object.prototype.hasOwnProperty.call(Chart, 'default') ? Chart['default'] : Chart;
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function ownKeys(object, enumerableOnly) {
var keys = Object.keys(object);
if (Object.getOwnPropertySymbols) {
var symbols = Object.getOwnPropertySymbols(object);
if (enumerableOnly) symbols = symbols.filter(function (sym) {
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
});
keys.push.apply(keys, symbols);
}
return keys;
}
function _objectSpread2(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
if (i % 2) {
ownKeys(Object(source), true).forEach(function (key) {
_defineProperty(target, key, source[key]);
});
} else if (Object.getOwnPropertyDescriptors) {
Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
} else {
ownKeys(Object(source)).forEach(function (key) {
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
});
}
}
return target;
}
Chart.defaults._set('gauge', {
needle: {
// Needle circle radius as the percentage of the chart area width
radiusPercentage: 2,
// Needle width as the percentage of the chart area width
widthPercentage: 3.2,
// Needle length as the percentage of the interval between inner radius (0%) and outer radius (100%) of the arc
lengthPercentage: 80,
// The color of the needle
color: 'rgba(0, 0, 0, 1)'
},
valueLabel: {
// fontSize: undefined
display: true,
formatter: null,
color: 'rgba(255, 255, 255, 1)',
backgroundColor: 'rgba(0, 0, 0, 1)',
borderRadius: 5,
padding: {
top: 5,
right: 5,
bottom: 5,
left: 5
},
bottomMarginPercentage: 5
},
animation: {
duration: 1000,
animateRotate: true,
animateScale: false
},
// The percentage of the chart that we cut out of the middle.
cutoutPercentage: 50,
// The rotation of the chart, where the first data arc begins.
rotation: -Math.PI,
// The total circumference of the chart.
circumference: Math.PI,
legend: {
display: false
},
tooltips: {
enabled: false
}
});
var GaugeController = Chart.controllers.doughnut.extend({
getValuePercent: function getValuePercent(_ref, value) {
var minValue = _ref.minValue,
data = _ref.data;
var min = minValue || 0;
var max = [undefined, null].includes(data[data.length - 1]) ? 1 : data[data.length - 1];
var length = max - min;
var percent = (value - min) / length;
return percent;
},
getWidth: function getWidth(chart) {
return chart.chartArea.right - chart.chartArea.left;
},
getTranslation: function getTranslation(chart) {
var chartArea = chart.chartArea,
offsetX = chart.offsetX,
offsetY = chart.offsetY;
var centerX = (chartArea.left + chartArea.right) / 2;
var centerY = (chartArea.top + chartArea.bottom) / 2;
var dx = centerX + offsetX;
var dy = centerY + offsetY;
return {
dx: dx,
dy: dy
};
},
getAngle: function getAngle(_ref2) {
var chart = _ref2.chart,
valuePercent = _ref2.valuePercent;
var _chart$options = chart.options,
rotation = _chart$options.rotation,
circumference = _chart$options.circumference;
return rotation + circumference * valuePercent;
},
/* TODO set min padding, not applied until chart.update() (also chartArea must have been set)
setBottomPadding(chart) {
const needleRadius = this.getNeedleRadius(chart);
const padding = this.chart.config.options.layout.padding;
if (needleRadius > padding.bottom) {
padding.bottom = needleRadius;
return true;
}
return false;
},
*/
drawNeedle: function drawNeedle(ease) {
if (!this.chart.animating) {
// triggered when hovering
ease = 1;
}
var _this$chart = this.chart,
ctx = _this$chart.ctx,
config = _this$chart.config,
innerRadius = _this$chart.innerRadius,
outerRadius = _this$chart.outerRadius;
var dataset = config.data.datasets[this.index];
var _this$getMeta = this.getMeta(),
previous = _this$getMeta.previous;
var _config$options$needl = config.options.needle,
radiusPercentage = _config$options$needl.radiusPercentage,
widthPercentage = _config$options$needl.widthPercentage,
lengthPercentage = _config$options$needl.lengthPercentage,
color = _config$options$needl.color;
var width = this.getWidth(this.chart);
var needleRadius = radiusPercentage / 100 * width;
var needleWidth = widthPercentage / 100 * width;
var needleLength = lengthPercentage / 100 * (outerRadius - innerRadius) + innerRadius; // center
var _this$getTranslation = this.getTranslation(this.chart),
dx = _this$getTranslation.dx,
dy = _this$getTranslation.dy; // interpolate
var origin = this.getAngle({
chart: this.chart,
valuePercent: previous.valuePercent
}); // TODO valuePercent is in current.valuePercent also
var target = this.getAngle({
chart: this.chart,
valuePercent: this.getValuePercent(dataset, dataset.value)
});
var angle = origin + (target - origin) * ease; // draw
ctx.save();
ctx.translate(dx, dy);
ctx.rotate(angle);
ctx.fillStyle = color; // draw circle
ctx.beginPath();
ctx.ellipse(0, 0, needleRadius, needleRadius, 0, 0, 2 * Math.PI);
ctx.fill(); // draw needle
ctx.beginPath();
ctx.moveTo(0, needleWidth / 2);
ctx.lineTo(needleLength, 0);
ctx.lineTo(0, -needleWidth / 2);
ctx.fill();
ctx.restore();
},
drawValueLabel: function drawValueLabel(ease) {
// eslint-disable-line no-unused-vars
if (!this.chart.config.options.valueLabel.display) {
return;
}
var _this$chart2 = this.chart,
ctx = _this$chart2.ctx,
config = _this$chart2.config;
var defaultFontFamily = config.options.defaultFontFamily;
var dataset = config.data.datasets[this.index];
var _config$options$value = config.options.valueLabel,
formatter = _config$options$value.formatter,
fontSize = _config$options$value.fontSize,
color = _config$options$value.color,
backgroundColor = _config$options$value.backgroundColor,
borderRadius = _config$options$value.borderRadius,
padding = _config$options$value.padding,
bottomMarginPercentage = _config$options$value.bottomMarginPercentage;
var width = this.getWidth(this.chart);
var bottomMargin = bottomMarginPercentage / 100 * width;
var fmt = formatter || function (value) {
return value;
};
var valueText = fmt(dataset.value).toString();
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
if (fontSize) {
ctx.font = "".concat(fontSize, "px ").concat(defaultFontFamily);
} // const { width: textWidth, actualBoundingBoxAscent, actualBoundingBoxDescent } = ctx.measureText(valueText);
// const textHeight = actualBoundingBoxAscent + actualBoundingBoxDescent;
var _ctx$measureText = ctx.measureText(valueText),
textWidth = _ctx$measureText.width; // approximate height until browsers support advanced TextMetrics
var textHeight = Math.max(ctx.measureText('m').width, ctx.measureText("\uFF37").width);
var x = -(padding.left + textWidth / 2);
var y = -(padding.top + textHeight / 2);
var w = padding.left + textWidth + padding.right;
var h = padding.top + textHeight + padding.bottom; // center
var _this$getTranslation2 = this.getTranslation(this.chart),
dx = _this$getTranslation2.dx,
dy = _this$getTranslation2.dy; // add rotation
var rotation = this.chart.options.rotation % (Math.PI * 2.0);
dx += bottomMargin * Math.cos(rotation + Math.PI / 2);
dy += bottomMargin * Math.sin(rotation + Math.PI / 2); // draw
ctx.save();
ctx.translate(dx, dy); // draw background
ctx.beginPath();
Chart.helpers.canvas.roundedRect(ctx, x, y, w, h, borderRadius);
ctx.fillStyle = backgroundColor;
ctx.fill(); // draw value text
ctx.fillStyle = color || config.options.defaultFontColor;
var magicNumber = 0.075; // manual testing
ctx.fillText(valueText, 0, textHeight * magicNumber);
ctx.restore();
},
// overrides
update: function update(reset) {
var dataset = this.chart.config.data.datasets[this.index];
dataset.minValue = dataset.minValue || 0;
var meta = this.getMeta();
var initialValue = {
valuePercent: 0
}; // animations on will call update(reset) before update()
if (reset) {
meta.previous = null;
meta.current = initialValue;
} else {
dataset.data.sort(function (a, b) {
return a - b;
});
meta.previous = meta.current || initialValue;
meta.current = {
valuePercent: this.getValuePercent(dataset, dataset.value)
};
}
Chart.controllers.doughnut.prototype.update.call(this, reset);
},
updateElement: function updateElement(arc, index, reset) {
// TODO handle reset and options.animation
Chart.controllers.doughnut.prototype.updateElement.call(this, arc, index, reset);
var dataset = this.getDataset();
var data = dataset.data; // const { options } = this.chart.config;
// scale data
var previousValue = index === 0 ? dataset.minValue : data[index - 1];
var value = data[index];
var startAngle = this.getAngle({
chart: this.chart,
valuePercent: this.getValuePercent(dataset, previousValue)
});
var endAngle = this.getAngle({
chart: this.chart,
valuePercent: this.getValuePercent(dataset, value)
});
var circumference = endAngle - startAngle;
arc._model = _objectSpread2({}, arc._model, {
startAngle: startAngle,
endAngle: endAngle,
circumference: circumference
});
},
draw: function draw(ease) {
Chart.controllers.doughnut.prototype.draw.call(this, ease);
this.drawNeedle(ease);
this.drawValueLabel(ease);
}
});
/* eslint-disable max-len, func-names */
var polyfill = function polyfill() {
if (CanvasRenderingContext2D.prototype.ellipse === undefined) {
CanvasRenderingContext2D.prototype.ellipse = function (x, y, radiusX, radiusY, rotation, startAngle, endAngle, antiClockwise) {
this.save();
this.translate(x, y);
this.rotate(rotation);
this.scale(radiusX, radiusY);
this.arc(0, 0, 1, startAngle, endAngle, antiClockwise);
this.restore();
};
}
};
polyfill();
Chart.controllers.gauge = GaugeController;
Chart.Gauge = function (context, config) {
config.type = 'gauge';
return new Chart(context, config);
};
var index = Chart.Gauge;
return index;
})));
@@ -65,37 +65,34 @@ function getBarConfiguration(chart, labels, locale) {
labels: { fontColor },
};
legend.position = chart.legendPosition;
config.options.legend = legend;
config.options.plugins = config.options.plugins || {};
config.options.plugins.legend = legend;
config.options.layout = {
padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
};
config.options.scales = {
xAxes: [
{
ticks: {
// x axis configuration
maxRotation: 60,
minRotation: 15,
padding: 5,
labelOffset: 2,
fontColor,
},
x: {
ticks: {
// x axis configuration
maxRotation: 60,
minRotation: 15,
padding: 5,
labelOffset: 2,
color: fontColor,
},
],
yAxes: [
{
position: chart.verticalAxisPosition,
ticks: {
fontColor,
// y axis configuration
beginAtZero: true, // the origin of the y axis is always zero
},
},
y: {
position: chart.verticalAxisPosition,
ticks: {
color: fontColor,
// y axis configuration
},
],
beginAtZero: true, // the origin of the y axis is always zero
},
};
if (chart.stacked) {
config.options.scales.xAxes[0].stacked = true;
config.options.scales.yAxes[0].stacked = true;
config.options.scales.x.stacked = true;
config.options.scales.y.stacked = true;
}
return config;
}
@@ -79,10 +79,10 @@ function getLineConfiguration(chart, labels, locale) {
...config.options.legend,
display: chart.legendPosition !== "none",
labels: {
fontColor,
color: fontColor,
generateLabels(chart) {
const { data } = chart;
const labels = window.Chart.defaults.global.legend.labels.generateLabels(chart);
const labels = window.Chart.defaults.plugins.legend.labels.generateLabels(chart);
for (const [index, label] of labels.entries()) {
label.fillStyle = data.datasets[index].borderColor;
}
@@ -91,36 +91,33 @@ function getLineConfiguration(chart, labels, locale) {
},
};
legend.position = chart.legendPosition;
config.options.legend = legend;
config.options.plugins = config.options.plugins || {};
config.options.plugins.legend = legend;
config.options.layout = {
padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
};
config.options.scales = {
xAxes: [
{
ticks: {
// x axis configuration
maxRotation: 60,
minRotation: 15,
padding: 5,
labelOffset: 2,
fontColor,
},
x: {
ticks: {
// x axis configuration
maxRotation: 60,
minRotation: 15,
padding: 5,
labelOffset: 2,
color: fontColor,
},
],
yAxes: [
{
position: chart.verticalAxisPosition,
ticks: {
fontColor,
// y axis configuration
beginAtZero: true, // the origin of the y axis is always zero
},
},
y: {
position: chart.verticalAxisPosition,
ticks: {
color: fontColor,
// y axis configuration
},
],
beginAtZero: true, // the origin of the y axis is always zero
},
};
if (chart.stacked) {
config.options.scales.yAxes[0].stacked = true;
config.options.scales.y.stacked = true;
}
return config;
}
@@ -48,14 +48,15 @@ function getPieConfiguration(chart, labels, locale) {
labels: { fontColor },
};
legend.position = chart.legendPosition;
config.options.legend = legend;
config.options.plugins = config.options.plugins || {};
config.options.plugins.legend = legend;
config.options.layout = {
padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
};
config.options.tooltips = {
config.options.plugins.tooltip = {
callbacks: {
title: function (tooltipItems, data) {
return data.datasets[tooltipItems[0].datasetIndex].label;
title: function (tooltipItem) {
return tooltipItem.label;
},
},
};
@@ -102,6 +102,5 @@ export function isEmpty(item) {
*/
export async function loadSpreadsheetDependencies() {
await loadJS("/web/static/lib/Chart/Chart.js");
// chartjs-gauge should only be loaded when Chart.js is fully loaded !
await loadJS("/spreadsheet/static/lib/chartjs-gauge/chartjs-gauge.js");
await loadJS("/web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js");
}
@@ -68,6 +68,7 @@ export async function freezeOdooData(model) {
for (const figure of sheet.figures) {
if (figure.tag === "chart" && figure.data.type.startsWith("odoo_")) {
await loadJS("/web/static/lib/Chart/Chart.js");
await loadJS("/web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js");
const img = odooChartToImage(model, figure);
figure.tag = "image";
figure.data = {
@@ -220,15 +220,15 @@ QUnit.module("spreadsheet > odoo chart plugin", {}, () => {
const bar = model.getters.getChartDefinition(barChartId);
const line = model.getters.getChartDefinition(lineChartId);
assert.strictEqual(
model.getters.getChartRuntime(pieChartId).chartJsConfig.options.legend.display,
model.getters.getChartRuntime(pieChartId).chartJsConfig.options.plugins.legend.display,
true
);
assert.strictEqual(
model.getters.getChartRuntime(barChartId).chartJsConfig.options.legend.display,
model.getters.getChartRuntime(barChartId).chartJsConfig.options.plugins.legend.display,
true
);
assert.strictEqual(
model.getters.getChartRuntime(lineChartId).chartJsConfig.options.legend.display,
model.getters.getChartRuntime(lineChartId).chartJsConfig.options.plugins.legend.display,
true
);
model.dispatch("UPDATE_CHART", {
@@ -256,15 +256,15 @@ QUnit.module("spreadsheet > odoo chart plugin", {}, () => {
sheetId,
});
assert.strictEqual(
model.getters.getChartRuntime(pieChartId).chartJsConfig.options.legend.display,
model.getters.getChartRuntime(pieChartId).chartJsConfig.options.plugins.legend.display,
false
);
assert.strictEqual(
model.getters.getChartRuntime(barChartId).chartJsConfig.options.legend.display,
model.getters.getChartRuntime(barChartId).chartJsConfig.options.plugins.legend.display,
false
);
assert.strictEqual(
model.getters.getChartRuntime(lineChartId).chartJsConfig.options.legend.display,
model.getters.getChartRuntime(lineChartId).chartJsConfig.options.plugins.legend.display,
false
);
});
@@ -282,12 +282,8 @@ QUnit.module("spreadsheet > odoo chart plugin", {}, () => {
id: chartId,
sheetId,
});
assert.ok(
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.xAxes[0].stacked
);
assert.ok(
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.yAxes[0].stacked
);
assert.ok(model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.x.stacked);
assert.ok(model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.y.stacked);
model.dispatch("UPDATE_CHART", {
definition: {
...definition,
@@ -296,12 +292,8 @@ QUnit.module("spreadsheet > odoo chart plugin", {}, () => {
id: chartId,
sheetId,
});
assert.notOk(
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.xAxes[0].stacked
);
assert.notOk(
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.yAxes[0].stacked
);
assert.notOk(model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.x.stacked);
assert.notOk(model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.y.stacked);
});
QUnit.test("Can copy/paste Odoo chart", async (assert) => {
@@ -378,12 +370,8 @@ QUnit.module("spreadsheet > odoo chart plugin", {}, () => {
id: chartId,
sheetId,
});
assert.notOk(
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.xAxes[0].stacked
);
assert.ok(
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.yAxes[0].stacked
);
assert.notOk(model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.x.stacked);
assert.ok(model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.y.stacked);
model.dispatch("UPDATE_CHART", {
definition: {
...definition,
@@ -392,12 +380,8 @@ QUnit.module("spreadsheet > odoo chart plugin", {}, () => {
id: chartId,
sheetId,
});
assert.notOk(
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.xAxes[0].stacked
);
assert.notOk(
model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.yAxes[0].stacked
);
assert.notOk(model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.x.stacked);
assert.notOk(model.getters.getChartRuntime(chartId).chartJsConfig.options.scales.y.stacked);
});
QUnit.test(
@@ -19,6 +19,7 @@ import { registry } from "@web/core/registry";
*/
export async function mountSpreadsheet(model) {
await loadJS("/web/static/lib/Chart/Chart.js");
await loadJS("/web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js");
const app = new App(Spreadsheet, {
props: { model },
templates: templates,
+2 -1
View File
@@ -61,6 +61,7 @@ sent mails with personal token for the invitation of the survey.
'assets': {
'survey.survey_assets': [
'web/static/lib/Chart/Chart.js',
'web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js',
'survey/static/src/js/survey_image_zoomer.js',
'/survey/static/src/xml/survey_image_zoomer_templates.xml',
'survey/static/src/js/survey_quick_access.js',
@@ -79,7 +80,7 @@ sent mails with personal token for the invitation of the survey.
'survey/static/src/xml/survey_breadcrumb_templates.xml',
],
'survey.survey_user_input_session_assets': [
'survey/static/src/js/libs/chartjs-plugin-datalabels.min.js',
'survey/static/src/js/libs/chartjs-plugin-datalabels.js',
'survey/static/src/js/survey_session_colors.js',
'survey/static/src/js/survey_session_chart.js',
'survey/static/src/js/survey_session_text_answers.js',
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+84 -66
View File
@@ -77,6 +77,7 @@ publicWidget.registry.SurveyResultPagination = publicWidget.Widget.extend({
publicWidget.registry.SurveyResultChart = publicWidget.Widget.extend({
jsLibs: [
'/web/static/lib/Chart/Chart.js',
'/web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js',
],
//--------------------------------------------------------------------------
@@ -146,24 +147,33 @@ publicWidget.registry.SurveyResultChart = publicWidget.Widget.extend({
},
options: {
scales: {
xAxes: [{
x: {
ticks: {
callback: this._customTick(25),
callback: function (val, index) {
// For a category axis, the val is the index so the lookup via getLabelForValue is needed
const value = this.getLabelForValue(val);
const tickLimit = 25;
return value?.length > tickLimit
? `${value.slice(0, tickLimit)}...`
: value;
},
},
}],
yAxes: [{
},
y: {
ticks: {
beginAtZero: true,
precision: 0,
},
}],
beginAtZero: true,
},
},
tooltips: {
callbacks: {
title: function (tooltipItem, data) {
return data.labels[tooltipItem[0].index];
}
}
plugins: {
tooltip: {
callbacks: {
title: function (tooltipItem) {
return tooltipItem.label;
},
},
},
},
},
};
@@ -193,25 +203,34 @@ publicWidget.registry.SurveyResultChart = publicWidget.Widget.extend({
})
},
options: {
legend: {
display: false,
plugins: {
legend: {
display: false,
},
tooltip: {
enabled: false,
},
},
scales: {
xAxes: [{
x: {
ticks: {
callback: this._customTick(35),
callback: function (val, index) {
// For a category axis, the val is the index so the lookup via getLabelForValue is needed
const value = this.getLabelForValue(val);
const tickLimit = 35;
return value?.length > tickLimit
? `${value.slice(0, tickLimit)}...`
: value;
},
},
}],
yAxes: [{
},
y: {
ticks: {
beginAtZero: true,
precision: 0,
},
}],
beginAtZero: true,
},
},
tooltips: {
enabled: false,
}
},
};
},
@@ -237,7 +256,10 @@ publicWidget.registry.SurveyResultChart = publicWidget.Widget.extend({
return D3_COLORS[index % 20];
}),
}]
}
},
options: {
aspectRatio: 2,
},
};
},
@@ -261,10 +283,13 @@ publicWidget.registry.SurveyResultChart = publicWidget.Widget.extend({
}]
},
options: {
title: {
display: true,
text: _t("Overall Performance"),
plugins: {
title: {
display: true,
text: _t("Overall Performance"),
},
},
aspectRatio: 2,
}
};
},
@@ -325,65 +350,58 @@ publicWidget.registry.SurveyResultChart = publicWidget.Widget.extend({
datasets: datasets
},
options: {
title: {
display: true,
text: _t("Performance by Section"),
},
legend: {
display: true,
plugins: {
title: {
display: true,
text: _t("Performance by Section"),
},
legend: {
display: true,
},
tooltip: {
callbacks: {
label: (tooltipItem) => {
const xLabel = tooltipItem.label;
var roundedValue = Math.round(tooltipItem.parsed.y * 100) / 100;
return `${xLabel}: ${roundedValue}%`;
},
},
},
},
scales: {
xAxes: [{
x: {
ticks: {
callback: this._customTick(20),
callback: function (val, index) {
// For a category axis, the val is the index so the lookup via getLabelForValue is needed
const value = this.getLabelForValue(val);
const tickLimit = 20;
return value?.length > tickLimit
? `${value.slice(0, tickLimit)}...`
: value;
},
},
}],
yAxes: [{
},
y: {
gridLines: {
display: false,
},
ticks: {
beginAtZero: true,
precision: 0,
callback: function (label) {
return label + '%';
},
suggestedMin: 0,
suggestedMax: 100,
maxTicksLimit: 5,
stepSize: 25,
},
}],
beginAtZero: true,
suggestedMin: 0,
suggestedMax: 100,
},
},
tooltips: {
callbacks: {
label: function (tooltipItem, data) {
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || '';
var roundedValue = Math.round(tooltipItem.yLabel * 100) / 100;
return `${datasetLabel}: ${roundedValue}%`;
}
}
}
},
};
},
/**
* Custom Tick function to replace overflowing text with '...'
*
* @private
* @param {Integer} tickLimit
*/
_customTick: function (tickLimit) {
return function (label) {
if (label.length <= tickLimit) {
return label;
} else {
return label.slice(0, tickLimit) + '...';
}
};
},
/**
* Loads the chart using the provided Chart library.
*
@@ -126,35 +126,37 @@ publicWidget.registry.SurveySessionChart = publicWidget.Widget.extend({
},
anchor: 'end',
align: 'top',
}
},
legend: {
display: false,
},
legend: {
display: false,
},
tooltip: {
enabled: false,
},
},
scales: {
yAxes: [{
y: {
ticks: {
display: false,
},
gridLines: {
grid: {
display: false
}
}],
xAxes: [{
},
x: {
ticks: {
maxRotation: 0,
fontSize: '35',
fontStyle: 'bold',
fontColor: '#212529'
font: {
size :"35",
weight:"bold"
},
color : '#212529'
},
gridLines: {
grid: {
drawOnChartArea: false,
color: 'rgba(0, 0, 0, 0.2)'
}
}]
},
tooltips: {
enabled: false,
}
},
layout: {
padding: {
@@ -219,7 +221,7 @@ publicWidget.registry.SurveySessionChart = publicWidget.Widget.extend({
const allWords = chart.data.labels.reduce((accumulator, words) => accumulator.concat(' '.concat(words)));
const maxWordLength = Math.max(...allWords.split(' ').map((word) => word.length));
fontRatio = maxWordLength > charPerLine ? minRatio : fontRatio;
chart.options.scales.xAxes[0].ticks.fontSize = Math.min(parseInt(chart.options.scales.xAxes[0].ticks.fontSize), chart.width * fontRatio / (nbrCol));
chart.options.scales.x.ticks.font.size = Math.min(parseInt(chart.options.scales.x.ticks.font.size), chart.width * fontRatio / (nbrCol));
}
chart.data.labels.forEach(function (label, index, labelsList) {
+1
View File
@@ -553,6 +553,7 @@ This module provides the core of the Odoo Web Client.
'web/static/lib/nearest/jquery.nearest.js',
'web/static/lib/stacktracejs/stacktrace.js',
'web/static/lib/Chart/Chart.js',
'web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js',
# 'web/static/tests/legacy/main_tests.js',
'web/static/tests/helpers/**/*.js',
+14885 -16138
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,109 @@
/*!
* chartjs-adapter-luxon v1.3.1
* https://www.chartjs.org
* (c) 2023 chartjs-adapter-luxon Contributors
* Released under the MIT license
*/
(function (global, factory) {
typeof exports === "object" && typeof module !== "undefined"
? factory(require("chart.js"), require("luxon"))
: typeof define === "function" && define.amd
? define(["chart.js", "luxon"], factory)
: ((global =
typeof globalThis !== "undefined" ? globalThis : global || self),
factory(global.Chart, global.luxon));
})(this, function (chart_js, luxon) {
"use strict";
const FORMATS = {
datetime: luxon.DateTime.DATETIME_MED_WITH_SECONDS,
millisecond: "h:mm:ss.SSS a",
second: luxon.DateTime.TIME_WITH_SECONDS,
minute: luxon.DateTime.TIME_SIMPLE,
hour: { hour: "numeric" },
day: { day: "numeric", month: "short" },
week: "DD",
month: { month: "short", year: "numeric" },
quarter: "'Q'q - yyyy",
year: { year: "numeric" },
};
chart_js._adapters._date.override({
_id: "luxon", // DEBUG
/**
* @private
*/
_create: function (time) {
return luxon.DateTime.fromMillis(time, this.options);
},
init(chartOptions) {
if (!this.options.locale) {
this.options.locale = chartOptions.locale;
}
},
formats: function () {
return FORMATS;
},
parse: function (value, format) {
const options = this.options;
const type = typeof value;
if (value === null || type === "undefined") {
return null;
}
if (type === "number") {
value = this._create(value);
} else if (type === "string") {
if (typeof format === "string") {
value = luxon.DateTime.fromFormat(value, format, options);
} else {
value = luxon.DateTime.fromISO(value, options);
}
} else if (value instanceof Date) {
value = luxon.DateTime.fromJSDate(value, options);
} else if (type === "object" && !(value instanceof luxon.DateTime)) {
value = luxon.DateTime.fromObject(value, options);
}
return value.isValid ? value.valueOf() : null;
},
format: function (time, format) {
const datetime = this._create(time);
return typeof format === "string"
? datetime.toFormat(format)
: datetime.toLocaleString(format);
},
add: function (time, amount, unit) {
const args = {};
args[unit] = amount;
return this._create(time).plus(args).valueOf();
},
diff: function (max, min, unit) {
return this._create(max).diff(this._create(min)).as(unit).valueOf();
},
startOf: function (time, unit, weekday) {
if (unit === "isoWeek") {
weekday = Math.trunc(Math.min(Math.max(0, weekday), 6));
const dateTime = this._create(time);
return dateTime
.minus({ days: (dateTime.weekday - weekday + 7) % 7 })
.startOf("day")
.valueOf();
}
return unit ? this._create(time).startOf(unit).valueOf() : time;
},
endOf: function (time, unit) {
return this._create(time).endOf(unit).valueOf();
},
});
});
@@ -56,32 +56,33 @@ export class GaugeField extends Component {
],
},
options: {
circumference: Math.PI,
rotation: -Math.PI,
circumference: 180,
rotation: 270,
responsive: true,
tooltips: {
displayColors: false,
callbacks: {
label: function (tooltipItems) {
if (tooltipItems.index === 0) {
return _t("Value: ") + gaugeValue;
}
return _t("Max: ") + maxLabel;
maintainAspectRatio: false,
cutout: "70%",
layout: {
padding: 5,
},
plugins: {
title: {
display: true,
text: this.title,
padding: 4,
},
tooltip: {
displayColors: false,
callbacks: {
label: function (tooltipItem) {
if (tooltipItem.dataIndex === 0) {
return _t("Value: ") + gaugeValue;
}
return _t("Max: ") + maxLabel;
},
},
},
},
title: {
display: true,
text: this.title,
padding: 4,
},
layout: {
padding: {
bottom: 5,
},
},
maintainAspectRatio: false,
cutoutPercentage: 70,
aspectRatio: 2,
},
};
this.chart = new Chart(this.canvasRef.el, config);
@@ -48,11 +48,6 @@ export class JournalDashboardGraphField extends Component {
config = this.getBarChartConfig();
}
this.chart = new Chart(this.canvasRef.el, config);
// To perform its animations, ChartJS will perform each animation
// step in the next animation frame. The initial rendering itself
// is delayed for consistency. We can avoid this by manually
// advancing the animation service.
Chart.animationService.advance();
}
getLineChartConfig() {
const labels = this.data[0].values.map(function (pt) {
@@ -79,10 +74,21 @@ export class JournalDashboardGraphField extends Component {
],
},
options: {
legend: { display: false },
plugins: {
legend: { display: false },
tooltip: {
intersect: false,
position: "nearest",
caretSize: 0,
},
},
scales: {
yAxes: [{ display: false }],
xAxes: [{ display: false }],
y: {
display: false,
},
x: {
display: false,
},
},
maintainAspectRatio: false,
elements: {
@@ -90,11 +96,6 @@ export class JournalDashboardGraphField extends Component {
tension: 0.000001,
},
},
tooltips: {
intersect: false,
position: "nearest",
caretSize: 0,
},
},
};
}
@@ -131,16 +132,20 @@ export class JournalDashboardGraphField extends Component {
],
},
options: {
legend: { display: false },
plugins: {
legend: { display: false },
tooltip: {
intersect: false,
position: "nearest",
caretSize: 0,
},
},
scales: {
yAxes: [{ display: false }],
y: {
display: false,
},
},
maintainAspectRatio: false,
tooltips: {
intersect: false,
position: "nearest",
caretSize: 0,
},
elements: {
line: {
tension: 0.000001,
@@ -57,7 +57,10 @@ export class GraphRenderer extends Component {
this.tooltip = null;
this.legendTooltip = null;
onWillStart(() => loadJS("/web/static/lib/Chart/Chart.js"));
onWillStart(async () => {
await loadJS("/web/static/lib/Chart/Chart.js");
await loadJS("/web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js");
});
useEffect(() => this.renderChart());
onWillUnmount(this.onWillUnmount);
@@ -103,9 +106,10 @@ export class GraphRenderer extends Component {
* Creates a custom HTML tooltip.
* @param {Object} data
* @param {Object} metaData
* @param {Object} tooltipModel see chartjs documentation
* @param {Object} context see chartjs documentation
*/
customTooltip(data, metaData, tooltipModel) {
customTooltip(data, metaData, context) {
const tooltipModel = context.tooltip;
const { measure, measures, disableLinking, mode } = metaData;
this.rootRef.el.style.cursor = "";
this.removeTooltips();
@@ -185,7 +189,7 @@ export class GraphRenderer extends Component {
}
/**
* Used to format correctly the values in tooltips and yAxes.
* Used to format correctly the values in tooltip and y.
* @param {number} value
* @param {boolean} [allIntegers=true]
* @returns {string}
@@ -275,7 +279,7 @@ export class GraphRenderer extends Component {
const { mode, stacked } = this.model.metaData;
const elementOptions = {};
if (mode === "bar") {
elementOptions.rectangle = { borderWidth: 1 };
elementOptions.bar = { borderWidth: 1 };
} else if (mode === "line") {
elementOptions.line = { fill: stacked, tension: 0 };
}
@@ -453,6 +457,7 @@ export class GraphRenderer extends Component {
* @returns {Object}
*/
getScaleOptions() {
const labels = this.model.data.labels;
const { allIntegers, fields, groupBy, measure, measures, mode, stacked } =
this.model.metaData;
if (mode === "pie") {
@@ -460,25 +465,30 @@ export class GraphRenderer extends Component {
}
const xAxe = {
type: "category",
scaleLabel: {
title: {
display: Boolean(groupBy.length),
labelString: groupBy.length ? fields[groupBy[0].fieldName].string : "",
text: groupBy.length ? fields[groupBy[0].fieldName].string : "",
},
ticks: {
callback: (val, index) => {
const value = labels[index];
return shortenLabel(value);
},
},
ticks: { callback: (value) => shortenLabel(value) },
};
const yAxe = {
type: "linear",
scaleLabel: {
labelString: measures[measure].string,
title: {
text: measures[measure].string,
},
ticks: {
callback: (value) => this.formatValue(value, allIntegers),
suggestedMax: 0,
suggestedMin: 0,
},
suggestedMax: 0,
suggestedMin: 0,
stacked: mode === "line" && stacked ? stacked : undefined,
};
return { xAxes: [xAxe], yAxes: [yAxe] };
return { x: xAxe, y: yAxe };
}
/**
@@ -496,7 +506,7 @@ export class GraphRenderer extends Component {
const sortedDataPoints = sortBy(tooltipModel.dataPoints, "yLabel", "desc");
const items = [];
for (const item of sortedDataPoints) {
const index = item.index;
const index = item.dataIndex;
// If `datasetIndex` is not found in the `datasets`, then it refers to the `lineOverlayDataset`.
const dataset = data.datasets[item.datasetIndex] || this.model.lineOverlayDataset;
let label = dataset.trueLabels[index];
@@ -512,7 +522,7 @@ export class GraphRenderer extends Component {
}
boxColor = dataset.backgroundColor[index];
const totalData = dataset.data.reduce((a, b) => a + b, 0);
percentage = totalData && ((dataset.data[item.index] * 100) / totalData).toFixed(2);
percentage = totalData && ((dataset.data[index] * 100) / totalData).toFixed(2);
} else {
if (groupBy.length > 1 || domains.length > 1) {
label = `${label} / ${dataset.label}`;
@@ -533,7 +543,7 @@ export class GraphRenderer extends Component {
const { mode } = metaData;
const tooltipOptions = {
enabled: false,
custom: this.customTooltip.bind(this, data, metaData),
external: this.customTooltip.bind(this, data, metaData),
};
if (mode === "line") {
tooltipOptions.mode = "index";
@@ -547,14 +557,19 @@ export class GraphRenderer extends Component {
* @param {MouseEvent} ev
*/
onGraphClicked(ev) {
const [activeElement] = this.chart.getElementAtEvent(ev);
const [activeElement] = this.chart.getElementsAtEventForMode(
ev,
"nearest",
{ intersect: true },
false
);
if (!activeElement) {
return;
}
const { _datasetIndex, _index } = activeElement;
const { domains } = this.chart.data.datasets[_datasetIndex];
const { datasetIndex, index } = activeElement;
const { domains } = this.chart.data.datasets[datasetIndex];
if (domains) {
this.onGraphClickedFinal(domains[_index]);
this.onGraphClickedFinal(domains[index]);
}
}
@@ -623,8 +638,10 @@ export class GraphRenderer extends Component {
const options = {
maintainAspectRatio: false,
scales: this.getScaleOptions(),
legend: this.getLegendOptions(),
tooltips: this.getTooltipOptions(),
plugins: {
legend: this.getLegendOptions(),
tooltip: this.getTooltipOptions(),
},
elements: this.getElementOptions(),
};
if (!disableLinking && mode !== "line") {
@@ -664,11 +681,6 @@ export class GraphRenderer extends Component {
}
const config = this.getChartConfig();
this.chart = new Chart(this.canvasRef.el, config);
// To perform its animations, ChartJS will perform each animation
// step in the next animation frame. The initial rendering itself
// is delayed for consistency. We can avoid this by manually
// advancing the animation service.
Chart.animationService.advance();
}
/**
@@ -74,7 +74,7 @@ function checkLegend(assert, graph, expectedLegendLabels) {
expectedLegendLabels =
expectedLegendLabels instanceof Array ? expectedLegendLabels : [expectedLegendLabels];
const chart = getChart(graph);
const actualLegendLabels = chart.config.options.legend.labels
const actualLegendLabels = chart.config.options.plugins.legend.labels
.generateLabels(chart)
.map((o) => o.text);
assert.deepEqual(actualLegendLabels, expectedLegendLabels);
@@ -91,13 +91,13 @@ function checkTooltip(assert, graph, expectedTooltipContent, index, datasetIndex
if (yLabel !== undefined && (datasetIndex === undefined || datasetIndex === i)) {
dataPoints.push({
datasetIndex: i,
index,
dataIndex: index,
yLabel,
});
}
}
const tooltipModel = { opacity: 1, x: 1, y: 1, dataPoints };
getChart(graph).config.options.tooltips.custom(tooltipModel);
getChart(graph).config.options.plugins.tooltip.external({ tooltip: tooltipModel });
const { title, lines } = expectedTooltipContent;
const lineLabels = [];
const lineValues = [];
@@ -135,15 +135,15 @@ function checkModeIs(assert, graph, mode) {
}
function getScaleY(graph) {
return getChart(graph).config.options.scales.yAxes;
return getChart(graph).config.options.scales.y;
}
function getXAxeLabel(graph) {
return getChart(graph).config.options.scales.xAxes[0].scaleLabel.labelString;
return getChart(graph).config.options.scales.x.title.text;
}
function getYAxeLabel(graph) {
return getChart(graph).config.options.scales.yAxes[0].scaleLabel.labelString;
return getChart(graph).config.options.scales.y.title.text;
}
export async function clickOnDataset(graph) {
@@ -1050,7 +1050,7 @@ QUnit.module("Views", (hooks) => {
checkModeIs(assert, graph, "line");
assert.strictEqual(graph.model.metaData.stacked, true, "graph should be stacked.");
assert.strictEqual(
getScaleY(graph).every((y) => y.stacked),
getScaleY(graph).stacked,
true,
"The y axes should have stacked property set to true"
);
@@ -1063,7 +1063,7 @@ QUnit.module("Views", (hooks) => {
"graph should be a classic line chart."
);
assert.strictEqual(
getScaleY(graph).every((y) => y.stacked == undefined),
getScaleY(graph).stacked == undefined,
true,
"The y axes should have stacked property set to undefined"
);
@@ -1090,7 +1090,7 @@ QUnit.module("Views", (hooks) => {
"graph should be a classic line chart."
);
assert.strictEqual(
getScaleY(graph).every((y) => y.stacked),
!!getScaleY(graph).stacked,
false,
"the y axes should have a stacked property set to false since the stacked property in line chart is false."
);
@@ -1140,7 +1140,7 @@ QUnit.module("Views", (hooks) => {
assert.strictEqual(graph.model.metaData.stacked, true, "should be stacked by default.");
assert.strictEqual(
getScaleY(graph).every((y) => y.stacked),
getScaleY(graph).stacked,
true,
"the stacked property in y axes should be true when the stacked is enabled in line chart"
);
@@ -2360,8 +2360,8 @@ QUnit.module("Views", (hooks) => {
assert.expect(6);
const graph = await makeView({ serverData, type: "graph", resModel: "foo" });
function checkMeasure(measure) {
const yAxe = getChart(graph).config.options.scales.yAxes[0];
assert.strictEqual(yAxe.scaleLabel.labelString, measure);
const yAxe = getChart(graph).config.options.scales.y;
assert.strictEqual(yAxe.title.text, measure);
const item = [...target.querySelectorAll(".o_menu_item")].find(
(el) => el.innerText === measure
);
@@ -8,6 +8,7 @@ const ChartWidget = publicWidget.Widget.extend({
disabledInEditableMode: false,
jsLibs: [
'/web/static/lib/Chart/Chart.js',
'/web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js',
],
/**
@@ -36,38 +37,67 @@ const ChartWidget = publicWidget.Widget.extend({
el.borderWidth = this.el.dataset.borderWidth;
});
const radialAxis = {
beginAtZero: true,
};
const linearAxis = {
type: "linear",
stacked: this.el.dataset.stacked === "true",
beginAtZero: true,
min: parseInt(this.el.dataset.ticksMin),
max: parseInt(this.el.dataset.ticksMax),
};
const categoryAxis = {
type: "category",
};
// Make chart data
const chartData = {
type: this.el.dataset.type,
data: data,
options: {
legend: {
display: this.el.dataset.legendPosition !== 'none',
position: this.el.dataset.legendPosition,
plugins: {
legend: {
display: this.el.dataset.legendPosition !== 'none',
position: this.el.dataset.legendPosition,
},
tooltip: {
enabled: this.el.dataset.tooltipDisplay === 'true',
position: "custom",
},
title: {
display: !!this.el.dataset.title,
text: this.el.dataset.title,
},
},
tooltips: {
enabled: this.el.dataset.tooltipDisplay === 'true',
position: 'custom',
},
title: {
display: !!this.el.dataset.title,
text: this.el.dataset.title,
scales: {
x: categoryAxis,
y: linearAxis,
},
aspectRatio: 2,
},
};
// Add type specific options
if (this.el.dataset.type === 'radar') {
chartData.options.scale = {
ticks: {
beginAtZero: true,
}
chartData.options.scales = {
r: radialAxis,
};
} else if (this.el.dataset.type === "horizontalBar") {
chartData.type = "bar";
chartData.options.scales = {
x: linearAxis,
y: categoryAxis,
};
chartData.options.indexAxis = "y";
} else if (['pie', 'doughnut'].includes(this.el.dataset.type)) {
chartData.options.tooltips.callbacks = {
label: (tooltipItem, data) => {
const label = data.datasets[tooltipItem.datasetIndex].label;
const secondLabel = data.labels[tooltipItem.index];
chartData.options.scales = {};
chartData.options.plugins.tooltip.callbacks = {
label: (tooltipItem) => {
const label = tooltipItem.label;
const secondLabel = tooltipItem.dataset.label;
let final = label;
if (label) {
if (secondLabel) {
@@ -76,36 +106,9 @@ const ChartWidget = publicWidget.Widget.extend({
} else if (secondLabel) {
final = secondLabel;
}
return final + ':' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
return final + ':' + tooltipItem.formattedValue;
},
};
} else {
// For extending the range of the axis while maintaining the auto
// fit behavior
const beforeBuildTicks = (scale) => {
scale.min = parseInt(this.el.dataset.ticksMin) || scale.min;
scale.max = parseInt(this.el.dataset.ticksMax) || scale.max;
};
chartData.options.scales = {
xAxes: [{
stacked: this.el.dataset.stacked === 'true',
ticks: {
beginAtZero: true
},
// Maintaining the gap between two values on
// vertical/horizontally axis
beforeBuildTicks: beforeBuildTicks,
}],
yAxes: [{
stacked: this.el.dataset.stacked === 'true',
ticks: {
beginAtZero: true
},
// Maintaining the gap between two values on
// vertical/horizontally axis
beforeBuildTicks: beforeBuildTicks,
}],
};
}
// Disable animation in edit mode
@@ -6,6 +6,7 @@ import publicWidget from "@web/legacy/js/public/public_widget";
var BarChart = publicWidget.Widget.extend({
jsLibs: [
'/web/static/lib/Chart/Chart.js',
'/web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js',
],
/**
* @constructor
@@ -68,6 +69,7 @@ var BarChart = publicWidget.Widget.extend({
var PieChart = publicWidget.Widget.extend({
jsLibs: [
'/web/static/lib/Chart/Chart.js',
'/web/static/lib/chartjs-adapter-luxon/chartjs-adapter-luxon.js',
],
/**
* @override
@@ -104,6 +106,9 @@ var PieChart = publicWidget.Widget.extend({
label: this.data.length > 0 ? this.data[0].key : _t('No data'),
}]
},
options: {
aspectRatio: 2,
},
};
var canvas = this.$('canvas')[0];