[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 closes odoo/odoo#127259 Related: odoo/enterprise#45679 Signed-off-by: Lucas Lefèvre (lul) <lul@odoo.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -3475,14 +3475,15 @@
|
||||
createChart(chartData) {
|
||||
const canvas = this.canvas.el;
|
||||
const ctx = canvas.getContext("2d");
|
||||
// @ts-ignore
|
||||
this.chart = new window.Chart(ctx, chartData);
|
||||
}
|
||||
updateChartJs(chartRuntime) {
|
||||
const chartData = chartRuntime.chartJsConfig;
|
||||
if (chartData.data && chartData.data.datasets) {
|
||||
this.chart.data = chartData.data;
|
||||
if (chartData.options?.title) {
|
||||
this.chart.config.options.title = chartData.options.title;
|
||||
if (chartData.options?.plugins?.title) {
|
||||
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
||||
}
|
||||
if (chartData.options && "valueLabel" in chartData.options) {
|
||||
if (chartData.options?.valueLabel) {
|
||||
@@ -3492,12 +3493,13 @@
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.chart.data.datasets = undefined;
|
||||
this.chart.data.datasets = [];
|
||||
}
|
||||
this.chart.config.options.tooltips = chartData.options?.tooltips;
|
||||
this.chart.config.options.legend = chartData.options?.legend;
|
||||
this.chart.config.options.plugins.tooltip = chartData.options.plugins.tooltip;
|
||||
this.chart.config.options.plugins.legend = chartData.options.plugins.legend;
|
||||
this.chart.config.options.scales = chartData.options?.scales;
|
||||
this.chart.update({ duration: 0 });
|
||||
// ?
|
||||
this.chart.update("active");
|
||||
}
|
||||
}
|
||||
ChartJsComponent.props = {
|
||||
@@ -16179,49 +16181,40 @@
|
||||
* Get a default chart js configuration
|
||||
*/
|
||||
function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale }) {
|
||||
return {
|
||||
type: chart.type,
|
||||
options: {
|
||||
// https://www.chartjs.org/docs/latest/general/responsive.html
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
|
||||
const options = {
|
||||
// https://www.chartjs.org/docs/latest/general/responsive.html
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
layout: {
|
||||
padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
fill: false, // do not fill the area under line charts
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
fill: false, // do not fill the area under line charts
|
||||
},
|
||||
point: {
|
||||
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
||||
},
|
||||
point: {
|
||||
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
||||
},
|
||||
animation: {
|
||||
duration: 0, // general animation time
|
||||
},
|
||||
hover: {
|
||||
animationDuration: 10, // duration of animations when hovering an item
|
||||
},
|
||||
responsiveAnimationDuration: 0,
|
||||
},
|
||||
animation: false,
|
||||
plugins: {
|
||||
title: {
|
||||
display: !!chart.title,
|
||||
fontSize: 22,
|
||||
fontStyle: "normal",
|
||||
text: _t(chart.title),
|
||||
fontColor,
|
||||
color: fontColor,
|
||||
font: { size: 22, weight: "normal" },
|
||||
},
|
||||
legend: {
|
||||
// Disable default legend onClick (show/hide dataset), to allow us to set a global onClick on the chart container.
|
||||
// If we want to re-enable this in the future, we need to override the default onClick to stop the event propagation
|
||||
onClick: undefined,
|
||||
onClick: () => { },
|
||||
},
|
||||
tooltips: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (tooltipItem, data) {
|
||||
let xLabel = data.datasets?.[tooltipItem.datasetIndex || 0]?.label;
|
||||
const yLabel = tooltipItem.yLabel !== ""
|
||||
? tooltipItem.yLabel
|
||||
: data.datasets?.[tooltipItem.datasetIndex || 0]?.data?.[tooltipItem.index || 0];
|
||||
label: function (tooltipItem) {
|
||||
let xLabel = tooltipItem.label;
|
||||
// tooltipItem.parsed.y can be an object or a number for pie charts
|
||||
const yLabel = tooltipItem.parsed.y ?? tooltipItem.parsed;
|
||||
const yLabelStr = format && typeof yLabel === "number"
|
||||
? formatValue(yLabel, { format, locale })
|
||||
: yLabel?.toLocaleString() || "";
|
||||
@@ -16230,10 +16223,15 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
type: chart.type,
|
||||
options,
|
||||
data: {
|
||||
labels: labels.map(truncateLabel),
|
||||
datasets: [],
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
}
|
||||
function getChartLabelFormat(getters, range) {
|
||||
@@ -16430,7 +16428,7 @@
|
||||
const fontColor = chartFontColor(chart.background);
|
||||
const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
|
||||
const legend = {
|
||||
labels: { fontColor },
|
||||
labels: { color: fontColor },
|
||||
};
|
||||
if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
|
||||
legend.display = false;
|
||||
@@ -16438,42 +16436,35 @@
|
||||
else {
|
||||
legend.position = chart.legendPosition;
|
||||
}
|
||||
config.options.legend = { ...config.options?.legend, ...legend };
|
||||
config.options.plugins.legend = { ...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: {
|
||||
padding: 5,
|
||||
color: fontColor,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
position: chart.verticalAxisPosition,
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: fontColor,
|
||||
callback: (value) => {
|
||||
return localeFormat.format
|
||||
? formatValue(value, localeFormat)
|
||||
: value?.toLocaleString() || value;
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
position: chart.verticalAxisPosition,
|
||||
ticks: {
|
||||
fontColor,
|
||||
// y axis configuration
|
||||
beginAtZero: true,
|
||||
callback: (value) => {
|
||||
return localeFormat.format
|
||||
? formatValue(value, localeFormat)
|
||||
: value?.toLocaleString() || value;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
if (chart.stacked) {
|
||||
config.options.scales.xAxes[0].stacked = true;
|
||||
config.options.scales.yAxes[0].stacked = true;
|
||||
// @ts-ignore chart.js type is broken
|
||||
config.options.scales.x.stacked = true;
|
||||
// @ts-ignore chart.js type is broken
|
||||
config.options.scales.y.stacked = true;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
@@ -16677,17 +16668,18 @@
|
||||
padding: { left: 30, right: 30, top: chart.title ? 10 : 25, bottom: 25 },
|
||||
};
|
||||
config.options.needle = {
|
||||
radiusPercentage: 2,
|
||||
widthPercentage: 3.2,
|
||||
lengthPercentage: 80,
|
||||
color: "#000000",
|
||||
width: 10,
|
||||
borderColor: "#000000",
|
||||
backgroundColor: "#000000",
|
||||
};
|
||||
config.options.valueLabel = {
|
||||
display: false,
|
||||
formatter: null,
|
||||
color: "#FFFFFF",
|
||||
font: {
|
||||
size: 30,
|
||||
color: "#FFFFFF",
|
||||
},
|
||||
backgroundColor: "#000000",
|
||||
fontSize: 30,
|
||||
borderColor: "#000000",
|
||||
borderRadius: 5,
|
||||
padding: {
|
||||
top: 5,
|
||||
@@ -16695,7 +16687,6 @@
|
||||
bottom: 5,
|
||||
left: 5,
|
||||
},
|
||||
bottomMarginPercentage: 5,
|
||||
};
|
||||
return config;
|
||||
}
|
||||
@@ -16737,16 +16728,12 @@
|
||||
data.push(point.value);
|
||||
backgroundColor.push(point.color);
|
||||
});
|
||||
// There's a bug in gauge lib when the last element in `data` is 0 (i.e. when the range maximum is 0).
|
||||
// The value wrongly fallbacks to 1 because 0 is falsy
|
||||
// See https://github.com/haiiaaa/chartjs-gauge/pull/33
|
||||
// https://github.com/haiiaaa/chartjs-gauge/blob/2ea50541d754d710cb30c2502fa690ac5dc27afd/src/controllers/controller.gauge.js#L52
|
||||
data.push(maxNeedleValue);
|
||||
backgroundColor.push(colors.upperColor);
|
||||
const dataRange = chart.dataRange;
|
||||
const deltaBeyondRangeLimit = needleCoverage / 30;
|
||||
let needleValue = minNeedleValue - deltaBeyondRangeLimit; // make needle value always at the minimum by default
|
||||
let cellFormatter = null;
|
||||
let cellFormatter = undefined;
|
||||
let displayValue = false;
|
||||
if (dataRange !== undefined) {
|
||||
const cell = getters.getEvaluatedCell({
|
||||
@@ -16761,6 +16748,7 @@
|
||||
// smaller and slightly larger than minRange and maxRange to mark the fact
|
||||
// that the needle is out of the range limits
|
||||
needleValue = clip(cell.value, minNeedleValue - deltaBeyondRangeLimit, maxNeedleValue + deltaBeyondRangeLimit);
|
||||
// show the original value, not the clipped one
|
||||
cellFormatter = () => getters.getRangeFormattedValues(dataRange)[0];
|
||||
displayValue = true;
|
||||
}
|
||||
@@ -16808,31 +16796,29 @@
|
||||
},
|
||||
};
|
||||
/**
|
||||
* Regex to test if a format string is a date format that can be translated into a moment time format
|
||||
* Regex to test if a format string is a date format that can be translated into a luxon time format
|
||||
*/
|
||||
const timeFormatMomentCompatible = /^((d|dd|m|mm|yyyy|yy|hh|h|ss|a)(-|:|\s|\/))*(d|dd|m|mm|yyyy|yy|hh|h|ss|a)$/i;
|
||||
const timeFormatLuxonCompatible = /^((d|dd|m|mm|yyyy|yy|hh|h|ss|a)(-|:|\s|\/))*(d|dd|m|mm|yyyy|yy|hh|h|ss|a)$/i;
|
||||
/** Get the time options for the XAxis of ChartJS */
|
||||
function getChartTimeOptions(labels, labelFormat, locale) {
|
||||
const momentFormat = convertDateFormatForMoment(labelFormat);
|
||||
const timeUnit = getBestTimeUnitForScale(labels, momentFormat, locale);
|
||||
const luxonFormat = convertDateFormatForLuxon(labelFormat);
|
||||
const timeUnit = getBestTimeUnitForScale(labels, luxonFormat, locale);
|
||||
const displayFormats = {};
|
||||
if (timeUnit) {
|
||||
displayFormats[timeUnit] = momentFormat;
|
||||
displayFormats[timeUnit] = luxonFormat;
|
||||
}
|
||||
return {
|
||||
parser: momentFormat,
|
||||
parser: luxonFormat,
|
||||
displayFormats,
|
||||
unit: timeUnit,
|
||||
unit: timeUnit ?? false,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Convert the given date format into a format that moment.js understands.
|
||||
*
|
||||
* https://momentjs.com/docs/#/parsing/string-format/
|
||||
* https://github.com/moment/luxon/blob/master/docs/formatting.md#table-of-tokens
|
||||
*/
|
||||
function convertDateFormatForMoment(format) {
|
||||
format = format.replace(/y/g, "Y");
|
||||
format = format.replace(/d/g, "D");
|
||||
function convertDateFormatForLuxon(format) {
|
||||
// "m" before "h" === month, "m" after "h" === minute
|
||||
const indexH = format.indexOf("h");
|
||||
if (indexH >= 0) {
|
||||
@@ -16903,6 +16889,8 @@
|
||||
return "year";
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const Chart = window.Chart;
|
||||
class LineChart extends AbstractChart {
|
||||
dataSets;
|
||||
labelRange;
|
||||
@@ -17032,7 +17020,7 @@
|
||||
return canBeDateChart(labelRange, getters) || canBeLinearChart(labelRange, getters);
|
||||
}
|
||||
function getChartAxisType(chart, getters) {
|
||||
if (isDateChart(chart, getters)) {
|
||||
if (isDateChart(chart, getters) && isLuxonTimeAdapterInstalled()) {
|
||||
return "time";
|
||||
}
|
||||
if (isLinearChart(chart, getters)) {
|
||||
@@ -17055,7 +17043,7 @@
|
||||
col: labelRange.zone.left,
|
||||
row: labelRange.zone.top,
|
||||
}).format;
|
||||
return Boolean(labelFormat && timeFormatMomentCompatible.test(labelFormat));
|
||||
return Boolean(labelFormat && timeFormatLuxonCompatible.test(labelFormat));
|
||||
}
|
||||
function canBeLinearChart(labelRange, getters) {
|
||||
if (!labelRange) {
|
||||
@@ -17075,10 +17063,11 @@
|
||||
const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
|
||||
const legend = {
|
||||
labels: {
|
||||
fontColor,
|
||||
color: fontColor,
|
||||
generateLabels(chart) {
|
||||
// color the legend labels with the dataset color, without any transparency
|
||||
const { data } = chart;
|
||||
const labels = window.Chart.defaults.global.legend.labels.generateLabels(chart);
|
||||
const labels = Chart.defaults.plugins.legend.labels.generateLabels(chart);
|
||||
for (const [index, label] of labels.entries()) {
|
||||
label.fillStyle = data.datasets[index].borderColor;
|
||||
}
|
||||
@@ -17092,41 +17081,33 @@
|
||||
else {
|
||||
legend.position = chart.legendPosition;
|
||||
}
|
||||
config.options.legend = { ...config.options?.legend, ...legend };
|
||||
Object.assign(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: {
|
||||
padding: 5,
|
||||
color: fontColor,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
position: chart.verticalAxisPosition,
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
color: fontColor,
|
||||
callback: (value) => {
|
||||
return localeFormat.format
|
||||
? formatValue(value, localeFormat)
|
||||
: value?.toLocaleString() || value;
|
||||
},
|
||||
},
|
||||
],
|
||||
yAxes: [
|
||||
{
|
||||
position: chart.verticalAxisPosition,
|
||||
ticks: {
|
||||
fontColor,
|
||||
// y axis configuration
|
||||
beginAtZero: true,
|
||||
callback: (value) => {
|
||||
return localeFormat.format
|
||||
? formatValue(value, localeFormat)
|
||||
: value?.toLocaleString() || value;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
if (chart.stacked) {
|
||||
config.options.scales.yAxes[0].stacked = true;
|
||||
if (chart.stacked && config.options?.scales?.y) {
|
||||
// @ts-ignore chart.js type is wrong
|
||||
config.options.scales.y.stacked = true;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
@@ -17153,15 +17134,18 @@
|
||||
const config = getLineConfiguration(chart, labels, localeFormat);
|
||||
const labelFormat = getChartLabelFormat(getters, chart.labelRange);
|
||||
if (axisType === "time") {
|
||||
config.options.scales.xAxes[0].type = "time";
|
||||
config.options.scales.xAxes[0].time = getChartTimeOptions(labels, labelFormat, locale);
|
||||
config.options.scales.xAxes[0].ticks.maxTicksLimit = 15;
|
||||
const axis = {
|
||||
type: "time",
|
||||
time: getChartTimeOptions(labels, labelFormat, locale),
|
||||
};
|
||||
Object.assign(config.options.scales.x, axis);
|
||||
config.options.scales.x.ticks.maxTicksLimit = 15;
|
||||
}
|
||||
else if (axisType === "linear") {
|
||||
config.options.scales.xAxes[0].type = "linear";
|
||||
config.options.scales.xAxes[0].ticks.callback = (value) => formatValue(value, localeFormat);
|
||||
config.options.tooltips.callbacks.title = (tooltipItem) => {
|
||||
return formatValue(tooltipItem[0]?.xLabel || "", localeFormat);
|
||||
config.options.scales.x.type = "linear";
|
||||
config.options.scales.x.ticks.callback = (value) => formatValue(value, { format: labelFormat, locale });
|
||||
config.options.plugins.tooltip.callbacks.title = (tooltipItem) => {
|
||||
return formatValue(tooltipItem[0]?.label || "", localeFormat);
|
||||
};
|
||||
}
|
||||
const colors = new ChartColors();
|
||||
@@ -17179,7 +17163,7 @@
|
||||
const dataset = {
|
||||
label,
|
||||
data,
|
||||
lineTension: 0,
|
||||
tension: 0,
|
||||
borderColor: color,
|
||||
backgroundColor,
|
||||
pointBackgroundColor: color,
|
||||
@@ -17189,6 +17173,22 @@
|
||||
}
|
||||
return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
|
||||
}
|
||||
let missingTimeAdapterAlreadyWarned = false;
|
||||
function isLuxonTimeAdapterInstalled() {
|
||||
// @ts-ignore
|
||||
if (!window.Chart) {
|
||||
return false;
|
||||
}
|
||||
// @ts-ignore
|
||||
const adapter = new window.Chart._adapters._date({});
|
||||
// @ts-ignore
|
||||
const isInstalled = adapter._id === "luxon";
|
||||
if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
|
||||
missingTimeAdapterAlreadyWarned = true;
|
||||
console.warn("'chartjs-adapter-luxon' time adapter is not installed. Time scale axes are disabled.");
|
||||
}
|
||||
return isInstalled;
|
||||
}
|
||||
|
||||
class PieChart extends AbstractChart {
|
||||
dataSets;
|
||||
@@ -17292,7 +17292,7 @@
|
||||
const fontColor = chartFontColor(chart.background);
|
||||
const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
|
||||
const legend = {
|
||||
labels: { fontColor },
|
||||
labels: { color: fontColor },
|
||||
};
|
||||
if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
|
||||
legend.display = false;
|
||||
@@ -17300,12 +17300,12 @@
|
||||
else {
|
||||
legend.position = chart.legendPosition;
|
||||
}
|
||||
config.options.legend = { ...config.options?.legend, ...legend };
|
||||
Object.assign(config.options.plugins.legend || {}, legend);
|
||||
config.options.layout = {
|
||||
padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
|
||||
};
|
||||
config.options.tooltips.callbacks.title = function (tooltipItems, data) {
|
||||
return data.datasets[tooltipItems[0].datasetIndex].label;
|
||||
config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
|
||||
return tooltipItems[0].dataset.label;
|
||||
};
|
||||
return config;
|
||||
}
|
||||
@@ -18069,6 +18069,278 @@
|
||||
};
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (window.Chart) {
|
||||
// @ts-ignore
|
||||
const DoughnutController = window.Chart?.DoughnutController;
|
||||
/**
|
||||
* Example :
|
||||
* const chart = new Chart(ctx, {
|
||||
* type: "gauge",
|
||||
* data: {
|
||||
* datasets: [
|
||||
* {
|
||||
* borderWidth: 5,
|
||||
* data: [10, 20, 10],
|
||||
* value: 23,
|
||||
* backgroundColor: [
|
||||
* "#dc3545", //red
|
||||
* "#ffc107", //orange
|
||||
* "#28a745", //green
|
||||
* ],
|
||||
* },
|
||||
* ],
|
||||
* },
|
||||
* options: {
|
||||
* plugins: {
|
||||
* title: {
|
||||
* display: true,
|
||||
* text: "Custom Chart Title",
|
||||
* padding: {
|
||||
* top: 10,
|
||||
* bottom: 30,
|
||||
* },
|
||||
* font : {
|
||||
* size : 30
|
||||
* }
|
||||
* },
|
||||
* },
|
||||
* valueLabel: {
|
||||
* display: true,
|
||||
* format: (value) => {
|
||||
* return value.toFixed(2) + "%";
|
||||
* },
|
||||
* font : {
|
||||
* size : 35,
|
||||
* family : "Arial",
|
||||
* color : "#FC0",
|
||||
* },
|
||||
* backgroundColor : "#ccc",
|
||||
* borderColor : "#090",
|
||||
* borderRadius : 50
|
||||
* },
|
||||
* needle: {
|
||||
* display: true,
|
||||
* width: 50,
|
||||
* color: "#DC5",
|
||||
* backgroundColor : "#ccc",
|
||||
* },
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
class GaugeController extends DoughnutController {
|
||||
static id = "gauge";
|
||||
static defaults = {
|
||||
...DoughnutController.defaults,
|
||||
circumference: 180,
|
||||
rotation: 270,
|
||||
};
|
||||
static overrides = {
|
||||
aspectRatio: 2,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
get chartHeight() {
|
||||
return Math.abs(this.chart.chartArea.top - this.chart.chartArea.bottom);
|
||||
}
|
||||
get chartWidth() {
|
||||
return Math.abs(this.chart.chartArea.left - this.chart.chartArea.right);
|
||||
}
|
||||
get needleOptions() {
|
||||
const { config } = this.chart;
|
||||
const options = config.options;
|
||||
// @ts-ignore
|
||||
return options?.needle || {};
|
||||
}
|
||||
get valueLabelOptions() {
|
||||
const { config } = this.chart;
|
||||
const options = config.options;
|
||||
// @ts-ignore
|
||||
return options?.valueLabel || {};
|
||||
}
|
||||
get minValue() {
|
||||
const dataset = this.getDataset();
|
||||
// @ts-ignore
|
||||
return dataset.minValue || 0;
|
||||
}
|
||||
getValueAngleInPercent(value) {
|
||||
const dataset = this.getDataset();
|
||||
const data = dataset.data || [];
|
||||
const max = Math.max(...data);
|
||||
const min = this.minValue;
|
||||
return (value - min) / (max - min);
|
||||
}
|
||||
drawValueLabel(params) {
|
||||
const { ctx } = this.chart;
|
||||
if (this.valueLabelOptions.display === false) {
|
||||
return;
|
||||
}
|
||||
// draw background rectangle
|
||||
ctx.save();
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = params.valueLabel.backgroundColor;
|
||||
ctx.strokeStyle = params.valueLabel.borderColor;
|
||||
ctx.roundRect(params.valueLabel.rect.x, params.valueLabel.rect.y, params.valueLabel.rect.width, params.valueLabel.rect.height, params.valueLabel.borderRadius);
|
||||
ctx.stroke();
|
||||
ctx.fill();
|
||||
// draw value text
|
||||
ctx.textAlign = "center";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.font = params.valueLabel.font;
|
||||
ctx.fillStyle = params.valueLabel.textColor;
|
||||
ctx.fillText(params.valueLabel.valueText, params.valueLabel.textPosition.x, params.valueLabel.textPosition.y);
|
||||
ctx.restore();
|
||||
}
|
||||
drawNeedle(params) {
|
||||
const { ctx } = this.chart;
|
||||
if (this.needleOptions.display === false) {
|
||||
return;
|
||||
}
|
||||
// translate & rotate next paths
|
||||
ctx.save();
|
||||
ctx.translate(params.needle.position.x, params.needle.position.y);
|
||||
ctx.rotate(-Math.PI / 2 + Math.PI * params.needle.percent);
|
||||
// draw circle
|
||||
ctx.beginPath();
|
||||
ctx.fillStyle = params.needle.backgroundColor;
|
||||
ctx.strokeStyle = params.needle.borderColor;
|
||||
ctx.ellipse(0, 0, params.needle.width / 2, params.needle.width / 2, 0, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
// draw needle
|
||||
ctx.fillStyle = params.needle.backgroundColor;
|
||||
ctx.strokeStyle = params.needle.borderColor;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-params.needle.width / 2, 0);
|
||||
ctx.lineTo(0, -params.needle.height + 10);
|
||||
ctx.lineTo(+params.needle.width / 2, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
computeRenderingParams() {
|
||||
const { ctx, config } = this.chart;
|
||||
const options = config.options;
|
||||
options.layout = options.layout || {};
|
||||
options.layout.padding = options.layout.padding || {};
|
||||
const formatter = this.valueLabelOptions.formatter;
|
||||
const fmt = typeof formatter === "function" ? formatter : (value) => value;
|
||||
const dataset = this.getDataset();
|
||||
// @ts-ignore value is a custom property for gauge charts
|
||||
const value = dataset.value || 0;
|
||||
const valueText = fmt(value).toString();
|
||||
const percent = this.getValueAngleInPercent(value);
|
||||
const fontFamily = this.valueLabelOptions.font?.family || "Arial";
|
||||
const optionsFontSize = this.valueLabelOptions.font?.size || 0;
|
||||
const fontSize = optionsFontSize >= 1 ? optionsFontSize : 30;
|
||||
const font = `${fontSize}px ${fontFamily}`;
|
||||
const padding = {
|
||||
right: 0,
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
...this.valueLabelOptions.padding,
|
||||
};
|
||||
padding.left = padding.left >= 0 ? padding.left : 10;
|
||||
padding.right = padding.right >= 0 ? padding.right : 10;
|
||||
padding.top = padding.top >= 0 ? padding.top : 10;
|
||||
padding.bottom = padding.bottom >= 0 ? padding.bottom : 10;
|
||||
const chartArea = this.chart.chartArea;
|
||||
const offsetX = this.offsetX || 0;
|
||||
const offsetY = this.offsetY || 0;
|
||||
const centerX = (chartArea.left + chartArea.right) / 2;
|
||||
const centerY = (chartArea.top + chartArea.bottom) / 2;
|
||||
const center = {
|
||||
x: centerX + offsetX,
|
||||
y: centerY + offsetY,
|
||||
};
|
||||
const textPosition = {
|
||||
x: center.x,
|
||||
y: center.y,
|
||||
};
|
||||
ctx.save();
|
||||
ctx.font = font;
|
||||
const metrics = ctx.measureText(valueText);
|
||||
ctx.restore();
|
||||
const textHeight = metrics.fontBoundingBoxAscent - metrics.fontBoundingBoxDescent;
|
||||
return {
|
||||
needle: {
|
||||
borderColor: this.needleOptions.borderColor || "#000",
|
||||
backgroundColor: this.needleOptions.backgroundColor || "#000",
|
||||
width: this.needleOptions.width || 10,
|
||||
height: Math.min(this.chartHeight, this.chartWidth / 2),
|
||||
position: {
|
||||
x: textPosition.x,
|
||||
y: textPosition.y,
|
||||
},
|
||||
percent,
|
||||
},
|
||||
valueLabel: {
|
||||
textColor: this.valueLabelOptions.font?.color || "#FFF",
|
||||
backgroundColor: this.valueLabelOptions.backgroundColor || "#000",
|
||||
borderColor: this.valueLabelOptions.borderColor || "#000",
|
||||
borderRadius: this.valueLabelOptions.borderRadius || 10,
|
||||
rect: {
|
||||
x: center.x - metrics.width / 2 - padding.left,
|
||||
y: center.y + textHeight / 2 + padding.top,
|
||||
width: metrics.width + padding.left + padding.right,
|
||||
height: -(textHeight + 6 + padding.top + padding.bottom),
|
||||
},
|
||||
valueText,
|
||||
textPosition,
|
||||
textHeight,
|
||||
font,
|
||||
},
|
||||
};
|
||||
}
|
||||
draw() {
|
||||
super.draw();
|
||||
const params = this.computeRenderingParams();
|
||||
this.drawNeedle(params);
|
||||
this.drawValueLabel(params);
|
||||
}
|
||||
updateElements(elements, start, count, mode) {
|
||||
super.updateElements(elements, start, count, mode);
|
||||
const dataset = this.getDataset();
|
||||
const data = this.getDataset().data;
|
||||
// @ts-ignore
|
||||
const minValue = dataset.minValue;
|
||||
// @ts-ignore
|
||||
const rotation = this.chart.options.rotation || 0;
|
||||
// @ts-ignore
|
||||
const circumference = this.chart.options.circumference || 0;
|
||||
for (let arcIndex = 0; arcIndex < data.length; arcIndex++) {
|
||||
const previousValue = arcIndex === 0 ? minValue : data[arcIndex - 1];
|
||||
const startAngleInPercent = this.getValueAngleInPercent(previousValue);
|
||||
const endAngleInPercent = this.getValueAngleInPercent(data[arcIndex]);
|
||||
const startAngle = degreesToRadians(rotation + circumference * startAngleInPercent) - Math.PI / 2;
|
||||
const endAngle = degreesToRadians(rotation + circumference * endAngleInPercent) - Math.PI / 2;
|
||||
const arcCircumference = endAngle - startAngle;
|
||||
const arc = elements[arcIndex];
|
||||
const propertiesUpdates = {
|
||||
startAngle,
|
||||
endAngle,
|
||||
circumference: arcCircumference,
|
||||
};
|
||||
this.updateElement(arc, arcIndex, propertiesUpdates, mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
function degreesToRadians(degrees) {
|
||||
return (degrees * Math.PI) / 180;
|
||||
}
|
||||
// @ts-ignore
|
||||
window.Chart.register(GaugeController);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -36418,7 +36690,8 @@
|
||||
if (!chart) {
|
||||
throw new Error(`No chart for the given id: ${figureId}`);
|
||||
}
|
||||
this.history.update("charts", figureId, this.createRuntimeChart(chart));
|
||||
const runtime = this.createRuntimeChart(chart);
|
||||
this.history.update("charts", figureId, runtime);
|
||||
}
|
||||
return this.charts[figureId];
|
||||
}
|
||||
@@ -49506,7 +49779,14 @@
|
||||
`;
|
||||
}
|
||||
|
||||
const TABLE_DEFAULT_STYLE = escapeXml /*xml*/ `<tableStyleInfo name="TableStyleLight8" showFirstColumn="0" showLastColumn="0" showRowStripes="0" showColumnStripes="0"/>`;
|
||||
const TABLE_DEFAULT_ATTRS = [
|
||||
["name", "TableStyleLight8"],
|
||||
["showFirstColumn", "0"],
|
||||
["showLastColumn", "0"],
|
||||
["showRowStripes", "0"],
|
||||
["showColumnStripes", "0"],
|
||||
];
|
||||
const TABLE_DEFAULT_STYLE = escapeXml /*xml*/ `<tableStyleInfo ${formatAttributes(TABLE_DEFAULT_ATTRS)}/>`;
|
||||
function createTable(table, tableId, sheetData) {
|
||||
const tableAttributes = [
|
||||
["id", tableId],
|
||||
@@ -50625,9 +50905,9 @@
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
|
||||
__info__.version = '16.5.0-alpha.3';
|
||||
__info__.date = '2023-08-09T12:01:15.162Z';
|
||||
__info__.hash = 'b0e8fd5';
|
||||
__info__.version = '16.5.0-alpha.4';
|
||||
__info__.date = '2023-08-10T09:44:45.710Z';
|
||||
__info__.hash = 'c8db018';
|
||||
|
||||
|
||||
})(this.o_spreadsheet = this.o_spreadsheet || {}, owl);
|
||||
|
||||
@@ -3951,7 +3951,7 @@
|
||||
</t>
|
||||
</odoo>
|
||||
<!--
|
||||
__info__.version = '16.5.0-alpha.3';
|
||||
__info__.date = '2023-08-09T12:01:17.859Z';
|
||||
__info__.hash = 'b0e8fd5';
|
||||
__info__.version = '16.5.0-alpha.4';
|
||||
__info__.date = '2023-08-10T09:44:48.609Z';
|
||||
__info__.hash = 'c8db018';
|
||||
-->
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Vendored
+14885
-16138
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);
|
||||
|
||||
+25
-20
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user