@odoo/o-spreadsheet 17.5.0-alpha.2 → 17.5.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 17.5.0-alpha.2
6
- * @date 2024-07-24T10:25:55.208Z
7
- * @hash 15fd5a8
5
+ * @version 17.5.0-alpha.3
6
+ * @date 2024-08-02T08:24:53.425Z
7
+ * @hash 767725f
8
8
  */
9
9
 
10
10
  'use strict';
@@ -2240,6 +2240,7 @@ exports.CommandResult = void 0;
2240
2240
  CommandResult["InvalidTableResize"] = "InvalidTableResize";
2241
2241
  CommandResult["PivotIdNotFound"] = "PivotIdNotFound";
2242
2242
  CommandResult["EmptyName"] = "EmptyName";
2243
+ CommandResult["ValueCellIsInvalidFormula"] = "ValueCellIsInvalidFormula";
2243
2244
  })(exports.CommandResult || (exports.CommandResult = {}));
2244
2245
 
2245
2246
  const PLAIN_TEXT_FORMAT = "@"; // see OpenXML spec §18.8.31
@@ -2767,13 +2768,16 @@ const wildcardToRegExp = memoize(function wildcardToRegExp(operand) {
2767
2768
  }
2768
2769
  return new RegExp("^" + exp + "$", "i");
2769
2770
  });
2770
- function evaluatePredicate(value = "", criterion) {
2771
+ function evaluatePredicate(value = "", criterion, locale) {
2771
2772
  const { operator, operand } = criterion;
2772
2773
  if (operand === undefined || value === null || operand === null) {
2773
2774
  return false;
2774
2775
  }
2775
2776
  if (typeof operand === "number" && operator === "=") {
2776
- return value.toString() === operand.toString();
2777
+ if (typeof value === "string" && (isNumber(value, locale) || isDateTime(value, locale))) {
2778
+ return toNumber(value, locale) === operand;
2779
+ }
2780
+ return value === operand;
2777
2781
  }
2778
2782
  if (operator === "<>" || operator === "=") {
2779
2783
  let result;
@@ -2855,7 +2859,7 @@ function visitMatchingRanges(args, cb, locale, isQuery = false) {
2855
2859
  for (let k = 0; k < countArg - 1; k += 2) {
2856
2860
  const criteriaValue = toMatrix(args[k])[i][j].value;
2857
2861
  const criterion = predicates[k / 2];
2858
- validatedPredicates = evaluatePredicate(criteriaValue ?? undefined, criterion);
2862
+ validatedPredicates = evaluatePredicate(criteriaValue ?? undefined, criterion, locale);
2859
2863
  if (!validatedPredicates) {
2860
2864
  break;
2861
2865
  }
@@ -6163,6 +6167,132 @@ function countUnique(args) {
6163
6167
  return reduceAny(args, (acc, a) => (isDataNonEmpty(a) ? acc.add(a?.value) : acc), new Set()).size;
6164
6168
  }
6165
6169
 
6170
+ function getUnitMatrix(n) {
6171
+ const matrix = Array(n);
6172
+ for (let i = 0; i < n; i++) {
6173
+ matrix[i] = Array(n).fill(0);
6174
+ matrix[i][i] = 1;
6175
+ }
6176
+ return matrix;
6177
+ }
6178
+ /**
6179
+ * Invert a matrix and compute its determinant using Gaussian Elimination.
6180
+ *
6181
+ * The Matrix should be a square matrix, and should be indexed [col][row] instead of the
6182
+ * standard mathematical indexing [row][col].
6183
+ */
6184
+ function invertMatrix(M) {
6185
+ // Use Gaussian Elimination to calculate the inverse:
6186
+ // (1) 'augment' the matrix (left) by the identity (on the right)
6187
+ // (2) Turn the matrix on the left into the identity using elementary row operations
6188
+ // (3) The matrix on the right becomes the inverse (was the identity matrix)
6189
+ //
6190
+ // There are 3 elementary row operations:
6191
+ // (a) Swap 2 rows. This multiply the determinant by -1.
6192
+ // (b) Multiply a row by a scalar. This multiply the determinant by that scalar.
6193
+ // (c) Add to a row a multiple of another row. This does not change the determinant.
6194
+ if (M.length !== M[0].length) {
6195
+ throw new EvaluationError(_t("Function [[FUNCTION_NAME]] invert matrix error, only square matrices are invertible"));
6196
+ }
6197
+ let determinant = 1;
6198
+ const dim = M.length;
6199
+ const I = getUnitMatrix(dim);
6200
+ const C = M.map((row) => row.slice());
6201
+ // Perform elementary row operations
6202
+ for (let pivot = 0; pivot < dim; pivot++) {
6203
+ let diagonalElement = C[pivot][pivot];
6204
+ // if we have a 0 on the diagonal we'll need to swap with a lower row
6205
+ if (diagonalElement === 0) {
6206
+ //look through every row below the i'th row
6207
+ for (let row = pivot + 1; row < dim; row++) {
6208
+ //if the ii'th row has a non-0 in the i'th col, swap it with that row
6209
+ if (C[pivot][row] != 0) {
6210
+ swapMatrixRows(C, pivot, row);
6211
+ swapMatrixRows(I, pivot, row);
6212
+ determinant *= -1;
6213
+ break;
6214
+ }
6215
+ }
6216
+ diagonalElement = C[pivot][pivot];
6217
+ //if it's still 0, matrix isn't invertible
6218
+ if (diagonalElement === 0) {
6219
+ return { determinant: 0 };
6220
+ }
6221
+ }
6222
+ // Scale this row down by e (so we have a 1 on the diagonal)
6223
+ for (let col = 0; col < dim; col++) {
6224
+ C[col][pivot] = C[col][pivot] / diagonalElement;
6225
+ I[col][pivot] = I[col][pivot] / diagonalElement;
6226
+ }
6227
+ determinant *= diagonalElement;
6228
+ // Subtract a multiple of the current row from ALL of
6229
+ // the other rows so that there will be 0's in this column in the
6230
+ // rows above and below this one
6231
+ for (let row = 0; row < dim; row++) {
6232
+ if (row === pivot) {
6233
+ continue;
6234
+ }
6235
+ // We want to change this element to 0
6236
+ const e = C[pivot][row];
6237
+ // Subtract (the row above(or below) scaled by e) from (the
6238
+ // current row) but start at the i'th column and assume all the
6239
+ // stuff left of diagonal is 0 (which it should be if we made this
6240
+ // algorithm correctly)
6241
+ for (let col = 0; col < dim; col++) {
6242
+ C[col][row] -= e * C[col][pivot];
6243
+ I[col][row] -= e * I[col][pivot];
6244
+ }
6245
+ }
6246
+ }
6247
+ // We've done all operations, C should be the identity matrix I should be the inverse
6248
+ return { inverted: I, determinant };
6249
+ }
6250
+ function swapMatrixRows(matrix, row1, row2) {
6251
+ for (let i = 0; i < matrix.length; i++) {
6252
+ const tmp = matrix[i][row1];
6253
+ matrix[i][row1] = matrix[i][row2];
6254
+ matrix[i][row2] = tmp;
6255
+ }
6256
+ }
6257
+ /**
6258
+ * Matrix multiplication of 2 matrices.
6259
+ * ex: matrix1 : n x l, matrix2 : m x n => result : m x l
6260
+ *
6261
+ * Note: we use indexing [col][row] instead of the standard mathematical notation [row][col]
6262
+ */
6263
+ function multiplyMatrices(matrix1, matrix2) {
6264
+ if (matrix1.length !== matrix2[0].length) {
6265
+ throw new EvaluationError(_t("Cannot multiply matrices : incompatible matrices size."));
6266
+ }
6267
+ const rowsM1 = matrix1[0].length;
6268
+ const colsM2 = matrix2.length;
6269
+ const n = matrix1.length;
6270
+ const result = Array(colsM2);
6271
+ for (let col = 0; col < colsM2; col++) {
6272
+ result[col] = Array(rowsM1);
6273
+ for (let row = 0; row < rowsM1; row++) {
6274
+ let sum = 0;
6275
+ for (let k = 0; k < n; k++) {
6276
+ sum += matrix1[k][row] * matrix2[col][k];
6277
+ }
6278
+ result[col][row] = sum;
6279
+ }
6280
+ }
6281
+ return result;
6282
+ }
6283
+ /**
6284
+ * Return the input if it's a scalar or the first element of the input if it's a matrix.
6285
+ */
6286
+ function toScalar(matrix) {
6287
+ if (!isMatrix(matrix)) {
6288
+ return matrix;
6289
+ }
6290
+ if (matrix.length !== 1 || matrix[0].length !== 1) {
6291
+ throw new EvaluationError(_t("The value should be a scalar or a 1x1 matrix"));
6292
+ }
6293
+ return matrix[0][0];
6294
+ }
6295
+
6166
6296
  function assertSameNumberOfElements(...args) {
6167
6297
  const dims = args[0].length;
6168
6298
  args.forEach((arg, i) => assert(() => arg.length === dims, _t("[[FUNCTION_NAME]] has mismatched dimensions for argument %s (%s vs %s).", i.toString(), dims.toString(), arg.length.toString())));
@@ -6209,6 +6339,161 @@ function min(values, locale) {
6209
6339
  const result = reduceNumbers(values, (acc, a) => (a < acc ? a : acc), Infinity, locale);
6210
6340
  return result === Infinity ? 0 : result;
6211
6341
  }
6342
+ function prepareDataForRegression(X, Y, newX) {
6343
+ const _X = X[0].length ? X : [range(1, Y.flat().length + 1)];
6344
+ const nVar = _X.length;
6345
+ let _newX = newX[0].length ? newX : _X;
6346
+ _newX = _newX.length === nVar ? transposeMatrix(_newX) : _newX;
6347
+ return { _X, _newX };
6348
+ }
6349
+ /*
6350
+ * This function performs a linear regression on the data set. It returns an array with two elements.
6351
+ * The first element is the slope, and the second element is the intercept.
6352
+ * The linear regression line is: y = slope*x + intercept
6353
+ * The function use the least squares method to find the best fit for the data set :
6354
+ * see https://www.mathsisfun.com/data/least-squares-regression.html
6355
+ * https://www.statology.org/standard-error-of-estimate/
6356
+ * https://agronomy4future.org/?p=16670
6357
+ * https://vitalflux.com/interpreting-f-statistics-in-linear-regression-formula-examples/
6358
+ * https://web.ist.utl.pt/~ist11038/compute/errtheory/,regression/regrthroughorigin.pdf
6359
+ */
6360
+ function fullLinearRegression(X, Y, computeIntercept = true, verbose = false) {
6361
+ const y = Y.flat();
6362
+ const n = y.length;
6363
+ let { _X } = prepareDataForRegression(X, Y, [[]]);
6364
+ _X = _X.length === n ? transposeMatrix(_X) : _X.slice();
6365
+ assertSameNumberOfElements(_X[0], y);
6366
+ const nVar = _X.length;
6367
+ const nDeg = n - nVar - (computeIntercept ? 1 : 0);
6368
+ const yMatrix = [y];
6369
+ const xMatrix = transposeMatrix(_X.reverse());
6370
+ let avgX = [];
6371
+ for (let i = 0; i < nVar; i++) {
6372
+ avgX.push(0);
6373
+ if (computeIntercept) {
6374
+ for (const xij of _X[i]) {
6375
+ avgX[i] += xij;
6376
+ }
6377
+ avgX[i] /= n;
6378
+ }
6379
+ }
6380
+ let avgY = 0;
6381
+ if (computeIntercept) {
6382
+ for (const yi of y) {
6383
+ avgY += yi;
6384
+ }
6385
+ avgY /= n;
6386
+ }
6387
+ const redX = xMatrix.map((row) => row.map((value, i) => value - avgX[i]));
6388
+ if (computeIntercept) {
6389
+ xMatrix.forEach((row) => row.push(1));
6390
+ }
6391
+ const coeffs = getLMSCoefficients(xMatrix, yMatrix);
6392
+ if (!computeIntercept) {
6393
+ coeffs.push([0]);
6394
+ }
6395
+ if (!verbose) {
6396
+ return coeffs;
6397
+ }
6398
+ const dot1 = multiplyMatrices(redX, transposeMatrix(redX));
6399
+ const { inverted: dotInv } = invertMatrix(dot1);
6400
+ if (dotInv === undefined) {
6401
+ throw new EvaluationError(_t("Matrix is not invertible"));
6402
+ }
6403
+ let SSE = 0, SSR = 0;
6404
+ for (let i = 0; i < n; i++) {
6405
+ const yi = y[i] - avgY;
6406
+ let temp = 0;
6407
+ for (let j = 0; j < nVar; j++) {
6408
+ const xi = redX[i][j];
6409
+ temp += xi * coeffs[j][0];
6410
+ }
6411
+ const ei = yi - temp;
6412
+ SSE += ei * ei;
6413
+ SSR += temp * temp;
6414
+ }
6415
+ const RMSE = Math.sqrt(SSE / nDeg);
6416
+ const r2 = SSR / (SSR + SSE);
6417
+ const f_stat = SSR / nVar / (SSE / nDeg);
6418
+ const deltaCoeffs = [];
6419
+ for (let i = 0; i < nVar; i++) {
6420
+ deltaCoeffs.push(RMSE * Math.sqrt(dotInv[i][i]));
6421
+ }
6422
+ if (computeIntercept) {
6423
+ const dot2 = multiplyMatrices(dotInv, [avgX]);
6424
+ const dot3 = multiplyMatrices(transposeMatrix([avgX]), dot2);
6425
+ deltaCoeffs.push(RMSE * Math.sqrt(dot3[0][0] + 1 / y.length));
6426
+ }
6427
+ const returned = [
6428
+ [coeffs[0][0], deltaCoeffs[0], r2, f_stat, SSR],
6429
+ [coeffs[1][0], deltaCoeffs[1], RMSE, nDeg, SSE],
6430
+ ];
6431
+ for (let i = 2; i < nVar; i++) {
6432
+ returned.push([coeffs[i][0], deltaCoeffs[i], "", "", ""]);
6433
+ }
6434
+ if (computeIntercept) {
6435
+ returned.push([coeffs[nVar][0], deltaCoeffs[nVar], "", "", ""]);
6436
+ }
6437
+ else {
6438
+ returned.push([0, "", "", "", ""]);
6439
+ }
6440
+ return returned;
6441
+ }
6442
+ /*
6443
+ This function performs a polynomial regression on the data set. It returns the coefficients of
6444
+ the polynomial function that best fits the data set.
6445
+ The polynomial function is: y = c0 + c1*x + c2*x^2 + ... + cn*x^n, where n is the order (degree)
6446
+ of the polynomial. The returned coefficients are then in the form: [c0, c1, c2, ..., cn]
6447
+ The function is based on the method of least squares :
6448
+ see: https://mathworld.wolfram.com/LeastSquaresFittingPolynomial.html
6449
+ */
6450
+ function polynomialRegression(flatY, flatX, order, intercept) {
6451
+ assertSameNumberOfElements(flatX, flatY);
6452
+ assert(() => order >= 1, _t("Function [[FUNCTION_NAME]] A regression of order less than 1 cannot be possible."));
6453
+ const yMatrix = [flatY];
6454
+ const xMatrix = flatX.map((x) => range(0, order).map((i) => Math.pow(x, order - i)));
6455
+ if (intercept) {
6456
+ xMatrix.forEach((row) => row.push(1));
6457
+ }
6458
+ const coeffs = getLMSCoefficients(xMatrix, yMatrix);
6459
+ if (!intercept) {
6460
+ coeffs.push([0]);
6461
+ }
6462
+ return coeffs;
6463
+ }
6464
+ function getLMSCoefficients(xMatrix, yMatrix) {
6465
+ const xMatrixT = transposeMatrix(xMatrix);
6466
+ const dot1 = multiplyMatrices(xMatrix, xMatrixT);
6467
+ const { inverted: dotInv } = invertMatrix(dot1);
6468
+ if (dotInv === undefined) {
6469
+ throw new EvaluationError(_t("Matrix is not invertible"));
6470
+ }
6471
+ const dot2 = multiplyMatrices(xMatrix, yMatrix);
6472
+ return transposeMatrix(multiplyMatrices(dotInv, dot2));
6473
+ }
6474
+ function evaluatePolynomial(coeffs, x, order) {
6475
+ return coeffs.reduce((acc, coeff, i) => acc + coeff * Math.pow(x, order - i), 0);
6476
+ }
6477
+ function expM(M) {
6478
+ return M.map((col) => col.map((cell) => Math.exp(cell)));
6479
+ }
6480
+ function logM(M) {
6481
+ return M.map((col) => col.map((cell) => Math.log(cell)));
6482
+ }
6483
+ function predictLinearValues(Y, X, newX, computeIntercept) {
6484
+ const { _X, _newX } = prepareDataForRegression(X, Y, newX);
6485
+ const coeffs = fullLinearRegression(_X, Y, computeIntercept, false);
6486
+ const nVar = coeffs.length - 1;
6487
+ const newY = _newX.map((col) => {
6488
+ let value = 0;
6489
+ for (let i = 0; i < nVar; i++) {
6490
+ value += coeffs[i][0] * col[nVar - i - 1];
6491
+ }
6492
+ value += coeffs[nVar][0];
6493
+ return [value];
6494
+ });
6495
+ return newY.length === newX.length ? newY : transposeMatrix(newY);
6496
+ }
6212
6497
 
6213
6498
  const pivotTimeAdapterRegistry = new Registry();
6214
6499
  function pivotTimeAdapter(granularity) {
@@ -6546,6 +6831,9 @@ function toNormalizedPivotValue(dimension, groupValue) {
6546
6831
  const groupValueString = typeof groupValue === "boolean"
6547
6832
  ? toString(groupValue).toLocaleLowerCase()
6548
6833
  : toString(groupValue);
6834
+ if (groupValueString === "null") {
6835
+ return null;
6836
+ }
6549
6837
  if (!pivotNormalizationValueRegistry.contains(dimension.type)) {
6550
6838
  throw new EvaluationError(_t("Field %(field)s is not supported because of its type (%(type)s)", {
6551
6839
  field: dimension.displayName,
@@ -6566,6 +6854,9 @@ function normalizeDateTime(value, granularity) {
6566
6854
  return pivotTimeAdapter(granularity).normalizeFunctionValue(value);
6567
6855
  }
6568
6856
  function toFunctionPivotValue(value, dimension) {
6857
+ if (value === null) {
6858
+ return `"null"`;
6859
+ }
6569
6860
  if (!pivotToFunctionValueRegistry.contains(dimension.type)) {
6570
6861
  return `"${value}"`;
6571
6862
  }
@@ -7767,6 +8058,7 @@ class ComposerFocusStore extends SpreadsheetStore {
7767
8058
  }
7768
8059
  }
7769
8060
 
8061
+ const TREND_LINE_XAXIS_ID = "x1";
7770
8062
  /**
7771
8063
  * This file contains helpers that are common to different charts (mainly
7772
8064
  * line, bar and pie charts)
@@ -8088,6 +8380,84 @@ function getDefinedAxis(definition) {
8088
8380
  useLeftAxis ||= !useRightAxis;
8089
8381
  return { useLeftAxis, useRightAxis };
8090
8382
  }
8383
+ function computeChartPadding({ displayTitle, displayLegend, }) {
8384
+ let top = 25;
8385
+ if (displayTitle) {
8386
+ top = 0;
8387
+ }
8388
+ else if (displayLegend) {
8389
+ top = 10;
8390
+ }
8391
+ return { left: 20, right: 20, top, bottom: 10 };
8392
+ }
8393
+ function getTrendDatasetForBarChart(config, dataset) {
8394
+ const filteredValues = [];
8395
+ const filteredLabels = [];
8396
+ const labels = [];
8397
+ for (let i = 0; i < dataset.data.length; i++) {
8398
+ if (dataset.data[i] !== null) {
8399
+ filteredValues.push(dataset.data[i]);
8400
+ filteredLabels.push(i + 1);
8401
+ }
8402
+ labels.push(i + 1);
8403
+ }
8404
+ const newLabels = range(0.5, labels.length + 0.55, 0.2);
8405
+ const newValues = interpolateData(config, filteredValues, filteredLabels, newLabels);
8406
+ if (!newValues.length) {
8407
+ return;
8408
+ }
8409
+ return getFullTrendingLineDataSet(dataset, config, newValues);
8410
+ }
8411
+ function getFullTrendingLineDataSet(dataset, config, data) {
8412
+ const backgroundColor = config.color ?? lightenColor(dataset.backgroundColor, 0.5);
8413
+ return {
8414
+ ...dataset,
8415
+ type: "line",
8416
+ xAxisID: TREND_LINE_XAXIS_ID,
8417
+ label: dataset.label ? _t("Trend line for %s", dataset.label) : "",
8418
+ data,
8419
+ order: -1,
8420
+ showLine: true,
8421
+ pointRadius: 0,
8422
+ backgroundColor,
8423
+ borderColor: backgroundColor,
8424
+ borderDash: [5, 5],
8425
+ };
8426
+ }
8427
+ function interpolateData(config, values, labels, newLabels) {
8428
+ if (values.length === 0 || labels.length === 0 || newLabels.length === 0) {
8429
+ return [];
8430
+ }
8431
+ switch (config.type) {
8432
+ case "polynomial": {
8433
+ const order = config.order ?? 2;
8434
+ if (order === 1) {
8435
+ return predictLinearValues([values], [labels], [newLabels], true)[0];
8436
+ }
8437
+ const coeffs = polynomialRegression(values, labels, order, true).flat();
8438
+ return newLabels.map((v) => evaluatePolynomial(coeffs, v, order));
8439
+ }
8440
+ case "exponential": {
8441
+ const positiveLogValues = [];
8442
+ const filteredLabels = [];
8443
+ for (let i = 0; i < values.length; i++) {
8444
+ if (values[i] > 0) {
8445
+ positiveLogValues.push(Math.log(values[i]));
8446
+ filteredLabels.push(labels[i]);
8447
+ }
8448
+ }
8449
+ if (!filteredLabels.length) {
8450
+ return [];
8451
+ }
8452
+ return expM(predictLinearValues([positiveLogValues], [filteredLabels], [newLabels], true))[0];
8453
+ }
8454
+ case "logarithmic": {
8455
+ return predictLinearValues([values], logM([labels]), logM([newLabels]), true)[0];
8456
+ }
8457
+ default:
8458
+ return [];
8459
+ }
8460
+ }
8091
8461
 
8092
8462
  /** This is a chartJS plugin that will draw the values of each data next to the point/bar/pie slice */
8093
8463
  const chartShowValuesPlugin = {
@@ -9217,132 +9587,6 @@ function assertSquareMatrix(errorStr, arg) {
9217
9587
  assert(() => arg.length === arg[0].length, errorStr);
9218
9588
  }
9219
9589
 
9220
- function getUnitMatrix(n) {
9221
- const matrix = Array(n);
9222
- for (let i = 0; i < n; i++) {
9223
- matrix[i] = Array(n).fill(0);
9224
- matrix[i][i] = 1;
9225
- }
9226
- return matrix;
9227
- }
9228
- /**
9229
- * Invert a matrix and compute its determinant using Gaussian Elimination.
9230
- *
9231
- * The Matrix should be a square matrix, and should be indexed [col][row] instead of the
9232
- * standard mathematical indexing [row][col].
9233
- */
9234
- function invertMatrix(M) {
9235
- // Use Gaussian Elimination to calculate the inverse:
9236
- // (1) 'augment' the matrix (left) by the identity (on the right)
9237
- // (2) Turn the matrix on the left into the identity using elementary row operations
9238
- // (3) The matrix on the right becomes the inverse (was the identity matrix)
9239
- //
9240
- // There are 3 elementary row operations:
9241
- // (a) Swap 2 rows. This multiply the determinant by -1.
9242
- // (b) Multiply a row by a scalar. This multiply the determinant by that scalar.
9243
- // (c) Add to a row a multiple of another row. This does not change the determinant.
9244
- if (M.length !== M[0].length) {
9245
- throw new EvaluationError(_t("Function [[FUNCTION_NAME]] invert matrix error, only square matrices are invertible"));
9246
- }
9247
- let determinant = 1;
9248
- const dim = M.length;
9249
- const I = getUnitMatrix(dim);
9250
- const C = M.map((row) => row.slice());
9251
- // Perform elementary row operations
9252
- for (let pivot = 0; pivot < dim; pivot++) {
9253
- let diagonalElement = C[pivot][pivot];
9254
- // if we have a 0 on the diagonal we'll need to swap with a lower row
9255
- if (diagonalElement === 0) {
9256
- //look through every row below the i'th row
9257
- for (let row = pivot + 1; row < dim; row++) {
9258
- //if the ii'th row has a non-0 in the i'th col, swap it with that row
9259
- if (C[pivot][row] != 0) {
9260
- swapMatrixRows(C, pivot, row);
9261
- swapMatrixRows(I, pivot, row);
9262
- determinant *= -1;
9263
- break;
9264
- }
9265
- }
9266
- diagonalElement = C[pivot][pivot];
9267
- //if it's still 0, matrix isn't invertible
9268
- if (diagonalElement === 0) {
9269
- return { determinant: 0 };
9270
- }
9271
- }
9272
- // Scale this row down by e (so we have a 1 on the diagonal)
9273
- for (let col = 0; col < dim; col++) {
9274
- C[col][pivot] = C[col][pivot] / diagonalElement;
9275
- I[col][pivot] = I[col][pivot] / diagonalElement;
9276
- }
9277
- determinant *= diagonalElement;
9278
- // Subtract a multiple of the current row from ALL of
9279
- // the other rows so that there will be 0's in this column in the
9280
- // rows above and below this one
9281
- for (let row = 0; row < dim; row++) {
9282
- if (row === pivot) {
9283
- continue;
9284
- }
9285
- // We want to change this element to 0
9286
- const e = C[pivot][row];
9287
- // Subtract (the row above(or below) scaled by e) from (the
9288
- // current row) but start at the i'th column and assume all the
9289
- // stuff left of diagonal is 0 (which it should be if we made this
9290
- // algorithm correctly)
9291
- for (let col = 0; col < dim; col++) {
9292
- C[col][row] -= e * C[col][pivot];
9293
- I[col][row] -= e * I[col][pivot];
9294
- }
9295
- }
9296
- }
9297
- // We've done all operations, C should be the identity matrix I should be the inverse
9298
- return { inverted: I, determinant };
9299
- }
9300
- function swapMatrixRows(matrix, row1, row2) {
9301
- for (let i = 0; i < matrix.length; i++) {
9302
- const tmp = matrix[i][row1];
9303
- matrix[i][row1] = matrix[i][row2];
9304
- matrix[i][row2] = tmp;
9305
- }
9306
- }
9307
- /**
9308
- * Matrix multiplication of 2 matrices.
9309
- * ex: matrix1 : n x l, matrix2 : m x n => result : m x l
9310
- *
9311
- * Note: we use indexing [col][row] instead of the standard mathematical notation [row][col]
9312
- */
9313
- function multiplyMatrices(matrix1, matrix2) {
9314
- if (matrix1.length !== matrix2[0].length) {
9315
- throw new EvaluationError(_t("Cannot multiply matrices : incompatible matrices size."));
9316
- }
9317
- const rowsM1 = matrix1[0].length;
9318
- const colsM2 = matrix2.length;
9319
- const n = matrix1.length;
9320
- const result = Array(colsM2);
9321
- for (let col = 0; col < colsM2; col++) {
9322
- result[col] = Array(rowsM1);
9323
- for (let row = 0; row < rowsM1; row++) {
9324
- let sum = 0;
9325
- for (let k = 0; k < n; k++) {
9326
- sum += matrix1[k][row] * matrix2[col][k];
9327
- }
9328
- result[col][row] = sum;
9329
- }
9330
- }
9331
- return result;
9332
- }
9333
- /**
9334
- * Return the input if it's a scalar or the first element of the input if it's a matrix.
9335
- */
9336
- function toScalar(matrix) {
9337
- if (!isMatrix(matrix)) {
9338
- return matrix;
9339
- }
9340
- if (matrix.length !== 1 || matrix[0].length !== 1) {
9341
- throw new EvaluationError(_t("The value should be a scalar or a 1x1 matrix"));
9342
- }
9343
- return matrix[0][0];
9344
- }
9345
-
9346
9590
  // -----------------------------------------------------------------------------
9347
9591
  // ARRAY_CONSTRAIN
9348
9592
  // -----------------------------------------------------------------------------
@@ -11073,161 +11317,6 @@ function centile(data, percent, isInclusive, locale) {
11073
11317
  }
11074
11318
  return percentile(sortedArray, _percent, isInclusive);
11075
11319
  }
11076
- function prepareDataForRegression(X, Y, newX) {
11077
- const _X = X[0].length ? X : [range(1, Y.flat().length + 1)];
11078
- const nVar = _X.length;
11079
- let _newX = newX[0].length ? newX : _X;
11080
- _newX = _newX.length === nVar ? transposeMatrix(_newX) : _newX;
11081
- return { _X, _newX };
11082
- }
11083
- /*
11084
- * This function performs a linear regression on the data set. It returns an array with two elements.
11085
- * The first element is the slope, and the second element is the intercept.
11086
- * The linear regression line is: y = slope*x + intercept
11087
- * The function use the least squares method to find the best fit for the data set :
11088
- * see https://www.mathsisfun.com/data/least-squares-regression.html
11089
- * https://www.statology.org/standard-error-of-estimate/
11090
- * https://agronomy4future.org/?p=16670
11091
- * https://vitalflux.com/interpreting-f-statistics-in-linear-regression-formula-examples/
11092
- * https://web.ist.utl.pt/~ist11038/compute/errtheory/,regression/regrthroughorigin.pdf
11093
- */
11094
- function fullLinearRegression(X, Y, computeIntercept = true, verbose = false) {
11095
- const y = Y.flat();
11096
- const n = y.length;
11097
- let { _X } = prepareDataForRegression(X, Y, [[]]);
11098
- _X = _X.length === n ? transposeMatrix(_X) : _X.slice();
11099
- assertSameNumberOfElements(_X[0], y);
11100
- const nVar = _X.length;
11101
- const nDeg = n - nVar - (computeIntercept ? 1 : 0);
11102
- const yMatrix = [y];
11103
- const xMatrix = transposeMatrix(_X.reverse());
11104
- let avgX = [];
11105
- for (let i = 0; i < nVar; i++) {
11106
- avgX.push(0);
11107
- if (computeIntercept) {
11108
- for (const xij of _X[i]) {
11109
- avgX[i] += xij;
11110
- }
11111
- avgX[i] /= n;
11112
- }
11113
- }
11114
- let avgY = 0;
11115
- if (computeIntercept) {
11116
- for (const yi of y) {
11117
- avgY += yi;
11118
- }
11119
- avgY /= n;
11120
- }
11121
- const redX = xMatrix.map((row) => row.map((value, i) => value - avgX[i]));
11122
- if (computeIntercept) {
11123
- xMatrix.forEach((row) => row.push(1));
11124
- }
11125
- const coeffs = getLMSCoefficients(xMatrix, yMatrix);
11126
- if (!computeIntercept) {
11127
- coeffs.push([0]);
11128
- }
11129
- if (!verbose) {
11130
- return coeffs;
11131
- }
11132
- const dot1 = multiplyMatrices(redX, transposeMatrix(redX));
11133
- const { inverted: dotInv } = invertMatrix(dot1);
11134
- if (dotInv === undefined) {
11135
- throw new EvaluationError(_t("Matrix is not invertible"));
11136
- }
11137
- let SSE = 0, SSR = 0;
11138
- for (let i = 0; i < n; i++) {
11139
- const yi = y[i] - avgY;
11140
- let temp = 0;
11141
- for (let j = 0; j < nVar; j++) {
11142
- const xi = redX[i][j];
11143
- temp += xi * coeffs[j][0];
11144
- }
11145
- const ei = yi - temp;
11146
- SSE += ei * ei;
11147
- SSR += temp * temp;
11148
- }
11149
- const RMSE = Math.sqrt(SSE / nDeg);
11150
- const r2 = SSR / (SSR + SSE);
11151
- const f_stat = SSR / nVar / (SSE / nDeg);
11152
- const deltaCoeffs = [];
11153
- for (let i = 0; i < nVar; i++) {
11154
- deltaCoeffs.push(RMSE * Math.sqrt(dotInv[i][i]));
11155
- }
11156
- if (computeIntercept) {
11157
- const dot2 = multiplyMatrices(dotInv, [avgX]);
11158
- const dot3 = multiplyMatrices(transposeMatrix([avgX]), dot2);
11159
- deltaCoeffs.push(RMSE * Math.sqrt(dot3[0][0] + 1 / y.length));
11160
- }
11161
- const returned = [
11162
- [coeffs[0][0], deltaCoeffs[0], r2, f_stat, SSR],
11163
- [coeffs[1][0], deltaCoeffs[1], RMSE, nDeg, SSE],
11164
- ];
11165
- for (let i = 2; i < nVar; i++) {
11166
- returned.push([coeffs[i][0], deltaCoeffs[i], "", "", ""]);
11167
- }
11168
- if (computeIntercept) {
11169
- returned.push([coeffs[nVar][0], deltaCoeffs[nVar], "", "", ""]);
11170
- }
11171
- else {
11172
- returned.push([0, "", "", "", ""]);
11173
- }
11174
- return returned;
11175
- }
11176
- /*
11177
- This function performs a polynomial regression on the data set. It returns the coefficients of
11178
- the polynomial function that best fits the data set.
11179
- The polynomial function is: y = c0 + c1*x + c2*x^2 + ... + cn*x^n, where n is the order (degree)
11180
- of the polynomial. The returned coefficients are then in the form: [c0, c1, c2, ..., cn]
11181
- The function is based on the method of least squares :
11182
- see: https://mathworld.wolfram.com/LeastSquaresFittingPolynomial.html
11183
- */
11184
- function polynomialRegression(flatY, flatX, order, intercept) {
11185
- assertSameNumberOfElements(flatX, flatY);
11186
- assert(() => order >= 1, _t("Function [[FUNCTION_NAME]] A regression of order less than 1 cannot be possible."));
11187
- const yMatrix = [flatY];
11188
- const xMatrix = flatX.map((x) => range(0, order).map((i) => Math.pow(x, order - i)));
11189
- if (intercept) {
11190
- xMatrix.forEach((row) => row.push(1));
11191
- }
11192
- const coeffs = getLMSCoefficients(xMatrix, yMatrix);
11193
- if (!intercept) {
11194
- coeffs.push([0]);
11195
- }
11196
- return coeffs;
11197
- }
11198
- function getLMSCoefficients(xMatrix, yMatrix) {
11199
- const xMatrixT = transposeMatrix(xMatrix);
11200
- const dot1 = multiplyMatrices(xMatrix, xMatrixT);
11201
- const { inverted: dotInv } = invertMatrix(dot1);
11202
- if (dotInv === undefined) {
11203
- throw new EvaluationError(_t("Matrix is not invertible"));
11204
- }
11205
- const dot2 = multiplyMatrices(xMatrix, yMatrix);
11206
- return transposeMatrix(multiplyMatrices(dotInv, dot2));
11207
- }
11208
- function evaluatePolynomial(coeffs, x, order) {
11209
- return coeffs.reduce((acc, coeff, i) => acc + coeff * Math.pow(x, order - i), 0);
11210
- }
11211
- function expM(M) {
11212
- return M.map((col) => col.map((cell) => Math.exp(cell)));
11213
- }
11214
- function logM(M) {
11215
- return M.map((col) => col.map((cell) => Math.log(cell)));
11216
- }
11217
- function predictLinearValues(Y, X, newX, computeIntercept) {
11218
- const { _X, _newX } = prepareDataForRegression(X, Y, newX);
11219
- const coeffs = fullLinearRegression(_X, Y, computeIntercept, false);
11220
- const nVar = coeffs.length - 1;
11221
- const newY = _newX.map((col) => {
11222
- let value = 0;
11223
- for (let i = 0; i < nVar; i++) {
11224
- value += coeffs[i][0] * col[nVar - i - 1];
11225
- }
11226
- value += coeffs[nVar][0];
11227
- return [value];
11228
- });
11229
- return newY.length === newX.length ? newY : transposeMatrix(newY);
11230
- }
11231
11320
  // -----------------------------------------------------------------------------
11232
11321
  // AVEDEV
11233
11322
  // -----------------------------------------------------------------------------
@@ -18979,6 +19068,78 @@ function isCtrlKey(ev) {
18979
19068
  return isMacOS() ? ev.metaKey : ev.ctrlKey;
18980
19069
  }
18981
19070
 
19071
+ /**
19072
+ * Return the o-spreadsheet element position relative
19073
+ * to the browser viewport.
19074
+ */
19075
+ function useSpreadsheetRect() {
19076
+ const position = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
19077
+ let spreadsheetElement = null;
19078
+ function updatePosition() {
19079
+ if (!spreadsheetElement) {
19080
+ spreadsheetElement = document.querySelector(".o-spreadsheet");
19081
+ }
19082
+ if (spreadsheetElement) {
19083
+ const { top, left, width, height } = spreadsheetElement.getBoundingClientRect();
19084
+ position.x = left;
19085
+ position.y = top;
19086
+ position.width = width;
19087
+ position.height = height;
19088
+ }
19089
+ }
19090
+ owl.onMounted(updatePosition);
19091
+ owl.onPatched(updatePosition);
19092
+ return position;
19093
+ }
19094
+ /**
19095
+ * Return the component (or ref's component) BoundingRect, relative
19096
+ * to the upper left corner of the screen (<body> element).
19097
+ *
19098
+ * Note: when used with a <Portal/> component, it will
19099
+ * return the portal position, not the teleported position.
19100
+ */
19101
+ function useAbsoluteBoundingRect(ref) {
19102
+ const rect = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
19103
+ function updateElRect() {
19104
+ const el = ref.el;
19105
+ if (el === null) {
19106
+ return;
19107
+ }
19108
+ const { top, left, width, height } = el.getBoundingClientRect();
19109
+ rect.x = left;
19110
+ rect.y = top;
19111
+ rect.width = width;
19112
+ rect.height = height;
19113
+ }
19114
+ owl.onMounted(updateElRect);
19115
+ owl.onPatched(updateElRect);
19116
+ return rect;
19117
+ }
19118
+ /**
19119
+ * Get the rectangle inside which a popover should stay when being displayed.
19120
+ * It's the value defined in `env.getPopoverContainerRect`, or the Rect of the "o-spreadsheet"
19121
+ * element by default.
19122
+ *
19123
+ * Coordinates are expressed expressed as absolute DOM position.
19124
+ */
19125
+ function usePopoverContainer() {
19126
+ const container = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
19127
+ const component = owl.useComponent();
19128
+ const spreadsheetRect = useSpreadsheetRect();
19129
+ function updateRect() {
19130
+ const env = component.env;
19131
+ const newRect = "getPopoverContainerRect" in env ? env.getPopoverContainerRect() : spreadsheetRect;
19132
+ container.x = newRect.x;
19133
+ container.y = newRect.y;
19134
+ container.width = newRect.width;
19135
+ container.height = newRect.height;
19136
+ }
19137
+ updateRect();
19138
+ owl.onMounted(updateRect);
19139
+ owl.onPatched(updateRect);
19140
+ return container;
19141
+ }
19142
+
18982
19143
  const arrowMap = {
18983
19144
  ArrowDown: "down",
18984
19145
  ArrowLeft: "left",
@@ -19577,7 +19738,9 @@ class CellComposer extends owl.Component {
19577
19738
  argToFocus: 0,
19578
19739
  });
19579
19740
  compositionActive = false;
19741
+ spreadsheetRect = useSpreadsheetRect();
19580
19742
  get assistantStyle() {
19743
+ const composerRect = this.composerRef.el.getBoundingClientRect();
19581
19744
  const assistantStyle = {};
19582
19745
  assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
19583
19746
  const proposals = this.autoCompleteState.provider?.proposals;
@@ -19602,8 +19765,11 @@ class CellComposer extends owl.Component {
19602
19765
  assistantStyle.right = `0px`;
19603
19766
  }
19604
19767
  }
19605
- else if (this.props.delimitation) {
19606
- assistantStyle["max-height"] = `${this.props.delimitation.height}px`;
19768
+ else {
19769
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
19770
+ if (composerRect.left + ASSISTANT_WIDTH > this.spreadsheetRect.width) {
19771
+ assistantStyle.right = `0px`;
19772
+ }
19607
19773
  }
19608
19774
  return cssPropertiesToCss(assistantStyle);
19609
19775
  }
@@ -21691,6 +21857,7 @@ const CfTerms = {
21691
21857
  ["ValueUpperInvalidFormula" /* CommandResult.ValueUpperInvalidFormula */]: _t("Invalid upper inflection point formula"),
21692
21858
  ["ValueLowerInvalidFormula" /* CommandResult.ValueLowerInvalidFormula */]: _t("Invalid lower inflection point formula"),
21693
21859
  ["EmptyRange" /* CommandResult.EmptyRange */]: _t("A range needs to be defined"),
21860
+ ["ValueCellIsInvalidFormula" /* CommandResult.ValueCellIsInvalidFormula */]: _t("At least one of the provided values is an invalid formula"),
21694
21861
  Unexpected: _t("The rule is invalid for an unknown reason"),
21695
21862
  },
21696
21863
  ColorScale: _t("Color scale"),
@@ -22224,7 +22391,22 @@ class BarChart extends AbstractChart {
22224
22391
  return new BarChart(definition, this.sheetId, this.getters);
22225
22392
  }
22226
22393
  }
22227
- function getBarConfiguration(chart, labels, localeFormat) {
22394
+ function createBarChartRuntime(chart, getters) {
22395
+ const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
22396
+ let labels = labelValues.formattedValues;
22397
+ let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
22398
+ if (chart.dataSetsHaveTitle &&
22399
+ dataSetsValues[0] &&
22400
+ labels.length > dataSetsValues[0].data.length) {
22401
+ labels.shift();
22402
+ }
22403
+ ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
22404
+ if (chart.aggregated) {
22405
+ ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
22406
+ }
22407
+ const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
22408
+ const locale = getters.getLocale();
22409
+ const localeFormat = { format: dataSetFormat, locale };
22228
22410
  const fontColor = chartFontColor(chart.background);
22229
22411
  const config = getDefaultChartJsRuntime(chart, labels, fontColor, {
22230
22412
  ...localeFormat,
@@ -22233,7 +22415,7 @@ function getBarConfiguration(chart, labels, localeFormat) {
22233
22415
  const legend = {
22234
22416
  labels: { color: fontColor },
22235
22417
  };
22236
- if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
22418
+ if (chart.legendPosition === "none") {
22237
22419
  legend.display = false;
22238
22420
  }
22239
22421
  else {
@@ -22241,7 +22423,10 @@ function getBarConfiguration(chart, labels, localeFormat) {
22241
22423
  }
22242
22424
  config.options.plugins.legend = { ...config.options.plugins?.legend, ...legend };
22243
22425
  config.options.layout = {
22244
- padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
22426
+ padding: computeChartPadding({
22427
+ displayTitle: !!chart.title.text,
22428
+ displayLegend: chart.legendPosition === "top",
22429
+ }),
22245
22430
  };
22246
22431
  config.options.indexAxis = chart.horizontal ? "y" : "x";
22247
22432
  const formatCallback = (value) => {
@@ -22299,27 +22484,11 @@ function getBarConfiguration(chart, labels, localeFormat) {
22299
22484
  horizontal: chart.horizontal,
22300
22485
  callback: formatCallback,
22301
22486
  };
22302
- return config;
22303
- }
22304
- function createBarChartRuntime(chart, getters) {
22305
- const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
22306
- let labels = labelValues.formattedValues;
22307
- let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
22308
- if (chart.dataSetsHaveTitle &&
22309
- dataSetsValues[0] &&
22310
- labels.length > dataSetsValues[0].data.length) {
22311
- labels.shift();
22312
- }
22313
- ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
22314
- if (chart.aggregated) {
22315
- ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
22316
- }
22317
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
22318
- const locale = getters.getLocale();
22319
- const config = getBarConfiguration(chart, labels, { format: dataSetFormat, locale });
22320
22487
  const colors = new ColorGenerator();
22488
+ const trendDatasets = [];
22321
22489
  const definition = chart.getDefinition();
22322
- for (const { label, data } of dataSetsValues) {
22490
+ for (const index in dataSetsValues) {
22491
+ const { label, data } = dataSetsValues[index];
22323
22492
  const color = colors.next();
22324
22493
  const dataset = {
22325
22494
  label,
@@ -22328,8 +22497,6 @@ function createBarChartRuntime(chart, getters) {
22328
22497
  backgroundColor: color,
22329
22498
  };
22330
22499
  config.data.datasets.push(dataset);
22331
- }
22332
- for (const [index, dataset] of config.data.datasets.entries()) {
22333
22500
  if (definition.dataSets?.[index]?.backgroundColor) {
22334
22501
  const color = definition.dataSets[index].backgroundColor;
22335
22502
  dataset.backgroundColor = color;
@@ -22342,6 +22509,30 @@ function createBarChartRuntime(chart, getters) {
22342
22509
  if (definition.dataSets?.[index]?.yAxisId && !chart.horizontal) {
22343
22510
  dataset["yAxisID"] = definition.dataSets[index].yAxisId;
22344
22511
  }
22512
+ const trend = definition.dataSets?.[index].trend;
22513
+ if (!trend?.display || chart.horizontal) {
22514
+ continue;
22515
+ }
22516
+ const trendDataset = getTrendDatasetForBarChart(trend, dataset);
22517
+ if (trendDataset) {
22518
+ trendDatasets.push(trendDataset);
22519
+ }
22520
+ }
22521
+ if (trendDatasets.length) {
22522
+ /* We add a second x axis here to draw the trend lines, with the labels length being
22523
+ * set so that the second axis points match the classical x axis
22524
+ */
22525
+ const maxLength = Math.max(...trendDatasets.map((trendDataset) => trendDataset.data.length));
22526
+ config.options.scales[TREND_LINE_XAXIS_ID] = {
22527
+ ...xAxis,
22528
+ labels: Array(maxLength).fill(""),
22529
+ offset: false,
22530
+ display: false,
22531
+ };
22532
+ /* These datasets must be inserted after the original
22533
+ * datasets to ensure the way we distinguish the originals and trendLine datasets after
22534
+ */
22535
+ trendDatasets.forEach((x) => config.data.datasets.push(x));
22345
22536
  }
22346
22537
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
22347
22538
  }
@@ -22540,90 +22731,58 @@ function isLuxonTimeAdapterInstalled() {
22540
22731
  }
22541
22732
  return isInstalled;
22542
22733
  }
22543
- function getLineOrScatterConfiguration(chart, labels, options) {
22544
- const fontColor = chartFontColor(chart.background);
22545
- const config = getDefaultChartJsRuntime(chart, labels, fontColor, options);
22546
- const legend = {
22547
- labels: {
22548
- color: fontColor,
22549
- generateLabels(chart) {
22550
- // color the legend labels with the dataset color, without any transparency
22551
- const { data } = chart;
22552
- const labels = window.Chart.defaults.plugins.legend.labels.generateLabels(chart);
22553
- for (const [index, label] of labels.entries()) {
22554
- label.fillStyle = data.datasets[index].borderColor;
22734
+ function getTrendDatasetForLineChart(config, dataset, axisType, locale) {
22735
+ const filteredValues = [];
22736
+ const filteredLabels = [];
22737
+ const labels = [];
22738
+ const datasetLength = dataset.data.length;
22739
+ switch (axisType) {
22740
+ case "category":
22741
+ for (let i = 0; i < datasetLength; i++) {
22742
+ if (dataset.data[i] !== null) {
22743
+ filteredValues.push(dataset.data[i]);
22744
+ filteredLabels.push(i + 1);
22555
22745
  }
22556
- return labels;
22557
- },
22558
- },
22559
- };
22560
- if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
22561
- legend.display = false;
22562
- }
22563
- else {
22564
- legend.position = chart.legendPosition;
22565
- }
22566
- Object.assign(config.options.plugins.legend || {}, legend);
22567
- config.options.layout = {
22568
- padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
22569
- };
22570
- config.options.scales = {
22571
- x: {
22572
- ticks: {
22573
- padding: 5,
22574
- color: fontColor,
22575
- },
22576
- title: getChartAxisTitleRuntime(chart.axesDesign?.x),
22577
- },
22578
- };
22579
- const formatCallback = (value) => {
22580
- value = Number(value);
22581
- if (isNaN(value))
22582
- return value;
22583
- const { locale, format } = options;
22584
- return formatValue(value, {
22585
- locale,
22586
- format: !format && Math.abs(value) >= 1000 ? "#,##" : format,
22587
- });
22588
- };
22589
- const yAxis = {
22590
- beginAtZero: true, // the origin of the y axis is always zero
22591
- ticks: {
22592
- color: fontColor,
22593
- callback: formatCallback,
22594
- },
22595
- };
22596
- const { useLeftAxis, useRightAxis } = getDefinedAxis(chart.getDefinition());
22597
- if (useLeftAxis) {
22598
- config.options.scales.y = {
22599
- ...yAxis,
22600
- position: "left",
22601
- title: getChartAxisTitleRuntime(chart.axesDesign?.y),
22602
- };
22746
+ labels.push(i + 1);
22747
+ }
22748
+ break;
22749
+ case "linear":
22750
+ for (const point of dataset.data) {
22751
+ const label = Number(point.x);
22752
+ if (isNaN(label)) {
22753
+ continue;
22754
+ }
22755
+ if (point.y !== null) {
22756
+ filteredValues.push(point.y);
22757
+ filteredLabels.push(label);
22758
+ }
22759
+ labels.push(label);
22760
+ }
22761
+ break;
22762
+ case "time":
22763
+ for (const point of dataset.data) {
22764
+ const date = toJsDate({ value: point.x }, locale).getTime();
22765
+ if (point.y !== null) {
22766
+ filteredValues.push(point.y);
22767
+ filteredLabels.push(date);
22768
+ }
22769
+ labels.push(date);
22770
+ }
22771
+ break;
22603
22772
  }
22604
- if (useRightAxis) {
22605
- config.options.scales.y1 = {
22606
- ...yAxis,
22607
- position: "right",
22608
- title: getChartAxisTitleRuntime(chart.axesDesign?.y1),
22609
- };
22773
+ const xmin = Math.min(...labels);
22774
+ const xmax = Math.max(...labels);
22775
+ if (xmax === xmin) {
22776
+ return;
22610
22777
  }
22611
- if ("stacked" in chart && chart.stacked) {
22612
- if (useLeftAxis) {
22613
- // @ts-ignore chart.js type is broken
22614
- config.options.scales.y.stacked = true;
22615
- }
22616
- if (useRightAxis) {
22617
- // @ts-ignore chart.js type is broken
22618
- config.options.scales.y1.stacked = true;
22619
- }
22778
+ const numberOfStep = 5 * labels.length;
22779
+ const step = (xmax - xmin) / numberOfStep;
22780
+ const newLabels = range(xmin, xmax + step / 2, step);
22781
+ const newValues = interpolateData(config, filteredValues, filteredLabels, newLabels);
22782
+ if (!newValues.length) {
22783
+ return;
22620
22784
  }
22621
- config.options.plugins.chartShowValuesPlugin = {
22622
- showValues: chart.showValues,
22623
- background: chart.background,
22624
- callback: formatCallback,
22625
- };
22626
- return config;
22785
+ return getFullTrendingLineDataSet(dataset, config, newValues);
22627
22786
  }
22628
22787
  function createLineOrScatterChartRuntime(chart, getters) {
22629
22788
  const axisType = getChartAxisType(chart, getters);
@@ -22646,7 +22805,97 @@ function createLineOrScatterChartRuntime(chart, getters) {
22646
22805
  const truncateLabels = axisType === "category";
22647
22806
  const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
22648
22807
  const options = { format: dataSetFormat, locale, truncateLabels };
22649
- const config = getLineOrScatterConfiguration(chart, labels, options);
22808
+ const fontColor = chartFontColor(chart.background);
22809
+ const config = getDefaultChartJsRuntime(chart, labels, fontColor, options);
22810
+ const legend = {
22811
+ labels: {
22812
+ color: fontColor,
22813
+ generateLabels(chart) {
22814
+ // color the legend labels with the dataset color, without any transparency
22815
+ const { data } = chart;
22816
+ const labels = window.Chart.defaults.plugins.legend.labels.generateLabels(chart);
22817
+ for (const [index, label] of labels.entries()) {
22818
+ label.fillStyle = data.datasets[index].borderColor;
22819
+ }
22820
+ return labels;
22821
+ },
22822
+ },
22823
+ };
22824
+ if (chart.legendPosition === "none") {
22825
+ legend.display = false;
22826
+ }
22827
+ else {
22828
+ legend.position = chart.legendPosition;
22829
+ }
22830
+ Object.assign(config.options.plugins.legend || {}, legend);
22831
+ config.options.layout = {
22832
+ padding: computeChartPadding({
22833
+ displayTitle: !!chart.title.text,
22834
+ displayLegend: chart.legendPosition === "top",
22835
+ }),
22836
+ };
22837
+ const xAxis = {
22838
+ ticks: {
22839
+ padding: 5,
22840
+ color: fontColor,
22841
+ },
22842
+ title: getChartAxisTitleRuntime(chart.axesDesign?.x),
22843
+ };
22844
+ config.options.scales = {
22845
+ x: xAxis,
22846
+ };
22847
+ const formatCallback = (value) => {
22848
+ value = Number(value);
22849
+ if (isNaN(value))
22850
+ return value;
22851
+ const { locale, format } = options;
22852
+ return formatValue(value, {
22853
+ locale,
22854
+ format: !format && Math.abs(value) >= 1000 ? "#,##" : format,
22855
+ });
22856
+ };
22857
+ const yAxis = {
22858
+ beginAtZero: true, // the origin of the y axis is always zero
22859
+ ticks: {
22860
+ color: fontColor,
22861
+ callback: formatCallback,
22862
+ },
22863
+ };
22864
+ const { useLeftAxis, useRightAxis } = getDefinedAxis(chart.getDefinition());
22865
+ if (useLeftAxis) {
22866
+ config.options.scales.y = {
22867
+ ...yAxis,
22868
+ position: "left",
22869
+ title: getChartAxisTitleRuntime(chart.axesDesign?.y),
22870
+ };
22871
+ }
22872
+ if (useRightAxis) {
22873
+ config.options.scales.y1 = {
22874
+ ...yAxis,
22875
+ position: "right",
22876
+ title: getChartAxisTitleRuntime(chart.axesDesign?.y1),
22877
+ };
22878
+ }
22879
+ if ("stacked" in chart && chart.stacked) {
22880
+ if (useLeftAxis) {
22881
+ // @ts-ignore chart.js type is broken
22882
+ config.options.scales.y.stacked = true;
22883
+ }
22884
+ if (useRightAxis) {
22885
+ // @ts-ignore chart.js type is broken
22886
+ config.options.scales.y1.stacked = true;
22887
+ }
22888
+ }
22889
+ config.options.plugins.chartShowValuesPlugin = {
22890
+ showValues: chart.showValues,
22891
+ background: chart.background,
22892
+ callback: formatCallback,
22893
+ };
22894
+ if (chart.dataSetsHaveTitle &&
22895
+ dataSetsValues[0] &&
22896
+ labels.length > dataSetsValues[0].data.length) {
22897
+ labels.shift();
22898
+ }
22650
22899
  const labelFormat = getChartLabelFormat(getters, chart.labelRange);
22651
22900
  if (axisType === "time") {
22652
22901
  const axis = {
@@ -22659,11 +22908,19 @@ function createLineOrScatterChartRuntime(chart, getters) {
22659
22908
  else if (axisType === "linear") {
22660
22909
  config.options.scales.x.type = "linear";
22661
22910
  config.options.scales.x.ticks.callback = (value) => formatValue(value, { format: labelFormat, locale });
22662
- config.options.plugins.tooltip.callbacks.title = (tooltipItem) => {
22663
- return formatValue(tooltipItem[0].parsed.x || tooltipItem[0].label, {
22664
- locale,
22665
- format: labelFormat,
22666
- });
22911
+ config.options.plugins.tooltip.callbacks.title = () => "";
22912
+ config.options.plugins.tooltip.callbacks.label = (tooltipItem) => {
22913
+ const dataSetPoint = dataSetsValues[tooltipItem.datasetIndex].data[tooltipItem.dataIndex];
22914
+ let label = tooltipItem.label || labelValues.values[tooltipItem.dataIndex];
22915
+ if (isNumber(label, locale)) {
22916
+ label = toNumber(label, locale);
22917
+ }
22918
+ const formattedX = formatValue(label, { locale, format: labelFormat });
22919
+ const formattedY = formatValue(dataSetPoint, { locale, format: dataSetFormat });
22920
+ const dataSetTitle = tooltipItem.dataset.label;
22921
+ return formattedX
22922
+ ? `${dataSetTitle}: (${formattedX}, ${formattedY})`
22923
+ : `${dataSetTitle}: ${formattedY}`;
22667
22924
  };
22668
22925
  }
22669
22926
  const areaChart = "fillArea" in chart ? chart.fillArea : false;
@@ -22703,6 +22960,8 @@ function createLineOrScatterChartRuntime(chart, getters) {
22703
22960
  };
22704
22961
  config.data.datasets.push(dataset);
22705
22962
  }
22963
+ let maxLength = 0;
22964
+ const trendDatasets = [];
22706
22965
  for (const [index, dataset] of config.data.datasets.entries()) {
22707
22966
  if (definition.dataSets?.[index]?.backgroundColor) {
22708
22967
  const color = definition.dataSets[index].backgroundColor;
@@ -22718,6 +22977,32 @@ function createLineOrScatterChartRuntime(chart, getters) {
22718
22977
  if (definition.dataSets?.[index]?.yAxisId) {
22719
22978
  dataset["yAxisID"] = definition.dataSets[index].yAxisId;
22720
22979
  }
22980
+ const trend = definition.dataSets?.[index].trend;
22981
+ if (!trend?.display) {
22982
+ continue;
22983
+ }
22984
+ const trendDataset = getTrendDatasetForLineChart(trend, dataset, axisType, locale);
22985
+ if (trendDataset) {
22986
+ maxLength = Math.max(maxLength, trendDataset.data.length);
22987
+ trendDatasets.push(trendDataset);
22988
+ dataSetsValues.push(trendDataset);
22989
+ }
22990
+ }
22991
+ if (trendDatasets.length) {
22992
+ /* We add a second x axis here to draw the trend lines, with the labels length being
22993
+ * set so that the second axis points match the classical x axis
22994
+ */
22995
+ config.options.scales[TREND_LINE_XAXIS_ID] = {
22996
+ ...xAxis,
22997
+ type: "category",
22998
+ labels: range(0, maxLength).map((x) => x.toString()),
22999
+ offset: false,
23000
+ display: false,
23001
+ };
23002
+ /* These datasets must be inserted after the original datasets to ensure the way we
23003
+ * distinguish the originals and trendLine datasets after
23004
+ */
23005
+ trendDatasets.forEach((x) => config.data.datasets.push(x));
22721
23006
  }
22722
23007
  return {
22723
23008
  chartJsConfig: config,
@@ -22877,7 +23162,7 @@ function createComboChartRuntime(chart, getters) {
22877
23162
  labels: { color: fontColor },
22878
23163
  reverse: true,
22879
23164
  };
22880
- if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
23165
+ if (chart.legendPosition === "none") {
22881
23166
  legend.display = false;
22882
23167
  }
22883
23168
  else {
@@ -22885,7 +23170,10 @@ function createComboChartRuntime(chart, getters) {
22885
23170
  }
22886
23171
  config.options.plugins.legend = { ...config.options.plugins?.legend, ...legend };
22887
23172
  config.options.layout = {
22888
- padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
23173
+ padding: computeChartPadding({
23174
+ displayTitle: !!chart.title.text,
23175
+ displayLegend: chart.legendPosition === "top",
23176
+ }),
22889
23177
  };
22890
23178
  config.options.scales = {
22891
23179
  x: {
@@ -22947,6 +23235,8 @@ function createComboChartRuntime(chart, getters) {
22947
23235
  callback: formatCallback(mainDataSetFormat),
22948
23236
  };
22949
23237
  const colors = new ColorGenerator();
23238
+ let maxLength = 0;
23239
+ const trendDatasets = [];
22950
23240
  for (let [index, { label, data }] of dataSetsValues.entries()) {
22951
23241
  const design = definition.dataSets[index];
22952
23242
  const color = colors.next();
@@ -22960,6 +23250,33 @@ function createComboChartRuntime(chart, getters) {
22960
23250
  order: -index,
22961
23251
  };
22962
23252
  config.data.datasets.push(dataset);
23253
+ const trend = definition.dataSets?.[index].trend;
23254
+ if (!trend?.display) {
23255
+ continue;
23256
+ }
23257
+ maxLength = Math.max(maxLength, data.length);
23258
+ const trendDataset = getTrendDatasetForBarChart(trend, dataset);
23259
+ if (trendDataset) {
23260
+ trendDatasets.push(trendDataset);
23261
+ }
23262
+ }
23263
+ if (trendDatasets.length) {
23264
+ /* We add a second x axis here to draw the trend lines, with the labels length being
23265
+ * set so that the second axis points match the classical x axis
23266
+ */
23267
+ config.options.scales[TREND_LINE_XAXIS_ID] = {
23268
+ ticks: {
23269
+ padding: 5,
23270
+ color: fontColor,
23271
+ },
23272
+ labels: Array(10 * maxLength + 1).fill(""),
23273
+ offset: false,
23274
+ display: false,
23275
+ };
23276
+ /* These datasets must be inserted after the original datasets to ensure the way we
23277
+ * distinguish the originals and trendLine datasets after
23278
+ */
23279
+ trendDatasets.forEach((x) => config.data.datasets.push(x));
22963
23280
  }
22964
23281
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
22965
23282
  }
@@ -23816,28 +24133,12 @@ class ScatterChart extends AbstractChart {
23816
24133
  }
23817
24134
  }
23818
24135
  function createScatterChartRuntime(chart, getters) {
23819
- const { chartJsConfig, background, dataSetsValues, dataSetFormat, labelValues, labelFormat } = createLineOrScatterChartRuntime(chart, getters);
24136
+ const { chartJsConfig, background } = createLineOrScatterChartRuntime(chart, getters);
23820
24137
  // use chartJS line chart and disable the lines instead of chartJS scatter chart. This is because the scatter chart
23821
24138
  // have less options than the line chart (it only works with linear labels)
23822
24139
  chartJsConfig.type = "line";
23823
- const configOptions = chartJsConfig.options;
23824
- const locale = getters.getLocale();
23825
- configOptions.plugins.tooltip.callbacks.title = () => "";
23826
- configOptions.plugins.tooltip.callbacks.label = (tooltipItem) => {
23827
- const dataSetPoint = dataSetsValues[tooltipItem.datasetIndex].data[tooltipItem.dataIndex];
23828
- let label = tooltipItem.label || labelValues.values[tooltipItem.dataIndex];
23829
- if (isNumber(label, locale)) {
23830
- label = toNumber(label, locale);
23831
- }
23832
- const formattedX = formatValue(label, { locale, format: labelFormat });
23833
- const formattedY = formatValue(dataSetPoint, { locale, format: dataSetFormat });
23834
- const dataSetTitle = tooltipItem.dataset.label;
23835
- return formattedX
23836
- ? `${dataSetTitle}: (${formattedX}, ${formattedY})`
23837
- : `${dataSetTitle}: ${formattedY}`;
23838
- };
23839
24140
  for (const dataSet of chartJsConfig.data.datasets) {
23840
- dataSet.showLine = false;
24141
+ dataSet.showLine = "showLine" in dataSet ? dataSet.showLine : false;
23841
24142
  }
23842
24143
  return { chartJsConfig, background };
23843
24144
  }
@@ -24875,78 +25176,6 @@ function zoneToRect(zone) {
24875
25176
  };
24876
25177
  }
24877
25178
 
24878
- /**
24879
- * Return the o-spreadsheet element position relative
24880
- * to the browser viewport.
24881
- */
24882
- function useSpreadsheetRect() {
24883
- const position = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
24884
- let spreadsheetElement = null;
24885
- function updatePosition() {
24886
- if (!spreadsheetElement) {
24887
- spreadsheetElement = document.querySelector(".o-spreadsheet");
24888
- }
24889
- if (spreadsheetElement) {
24890
- const { top, left, width, height } = spreadsheetElement.getBoundingClientRect();
24891
- position.x = left;
24892
- position.y = top;
24893
- position.width = width;
24894
- position.height = height;
24895
- }
24896
- }
24897
- owl.onMounted(updatePosition);
24898
- owl.onPatched(updatePosition);
24899
- return position;
24900
- }
24901
- /**
24902
- * Return the component (or ref's component) BoundingRect, relative
24903
- * to the upper left corner of the screen (<body> element).
24904
- *
24905
- * Note: when used with a <Portal/> component, it will
24906
- * return the portal position, not the teleported position.
24907
- */
24908
- function useAbsoluteBoundingRect(ref) {
24909
- const rect = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
24910
- function updateElRect() {
24911
- const el = ref.el;
24912
- if (el === null) {
24913
- return;
24914
- }
24915
- const { top, left, width, height } = el.getBoundingClientRect();
24916
- rect.x = left;
24917
- rect.y = top;
24918
- rect.width = width;
24919
- rect.height = height;
24920
- }
24921
- owl.onMounted(updateElRect);
24922
- owl.onPatched(updateElRect);
24923
- return rect;
24924
- }
24925
- /**
24926
- * Get the rectangle inside which a popover should stay when being displayed.
24927
- * It's the value defined in `env.getPopoverContainerRect`, or the Rect of the "o-spreadsheet"
24928
- * element by default.
24929
- *
24930
- * Coordinates are expressed expressed as absolute DOM position.
24931
- */
24932
- function usePopoverContainer() {
24933
- const container = owl.useState({ x: 0, y: 0, width: 0, height: 0 });
24934
- const component = owl.useComponent();
24935
- const spreadsheetRect = useSpreadsheetRect();
24936
- function updateRect() {
24937
- const env = component.env;
24938
- const newRect = "getPopoverContainerRect" in env ? env.getPopoverContainerRect() : spreadsheetRect;
24939
- container.x = newRect.x;
24940
- container.y = newRect.y;
24941
- container.width = newRect.width;
24942
- container.height = newRect.height;
24943
- }
24944
- updateRect();
24945
- owl.onMounted(updateRect);
24946
- owl.onPatched(updateRect);
24947
- return container;
24948
- }
24949
-
24950
25179
  css /* scss */ `
24951
25180
  .o-popover {
24952
25181
  position: absolute;
@@ -27714,7 +27943,7 @@ const FIX_FORMULAS = {
27714
27943
  if (!pivot.isValid()) {
27715
27944
  return;
27716
27945
  }
27717
- env.model.dispatch("INSERT_PIVOT", {
27946
+ env.model.dispatch("SPLIT_PIVOT_FORMULA", {
27718
27947
  sheetId,
27719
27948
  col,
27720
27949
  row,
@@ -31190,11 +31419,7 @@ class ChartWithAxisDesignPanel extends owl.Component {
31190
31419
  });
31191
31420
  }
31192
31421
  getDataSeries() {
31193
- const runtime = this.env.model.getters.getChartRuntime(this.props.figureId);
31194
- if (!runtime || !("chartJsConfig" in runtime)) {
31195
- return [];
31196
- }
31197
- return runtime.chartJsConfig.data.datasets.map((d) => d.label);
31422
+ return this.props.definition.dataSets.map((d, i) => d.label ?? `${ChartTerms.Series} ${i + 1}`);
31198
31423
  }
31199
31424
  updateSerieEditor(ev) {
31200
31425
  const chartId = this.props.figureId;
@@ -31206,9 +31431,10 @@ class ChartWithAxisDesignPanel extends owl.Component {
31206
31431
  this.state.index = selectedIndex;
31207
31432
  }
31208
31433
  updateDataSeriesColor(color) {
31209
- const dataSets = this.props.definition.dataSets;
31210
- if (!dataSets?.[this.state.index])
31434
+ const dataSets = [...this.props.definition.dataSets];
31435
+ if (!dataSets?.[this.state.index]) {
31211
31436
  return;
31437
+ }
31212
31438
  dataSets[this.state.index] = {
31213
31439
  ...dataSets[this.state.index],
31214
31440
  backgroundColor: color,
@@ -31217,16 +31443,18 @@ class ChartWithAxisDesignPanel extends owl.Component {
31217
31443
  }
31218
31444
  getDataSerieColor() {
31219
31445
  const dataSets = this.props.definition.dataSets;
31220
- if (!dataSets?.[this.state.index])
31446
+ if (!dataSets?.[this.state.index]) {
31221
31447
  return "";
31448
+ }
31222
31449
  const color = dataSets[this.state.index].backgroundColor;
31223
31450
  return color ? toHex(color) : getNthColor(this.state.index);
31224
31451
  }
31225
31452
  updateDataSeriesAxis(ev) {
31226
31453
  const axis = ev.target.value;
31227
- const dataSets = this.props.definition.dataSets;
31228
- if (!dataSets?.[this.state.index])
31454
+ const dataSets = [...this.props.definition.dataSets];
31455
+ if (!dataSets?.[this.state.index]) {
31229
31456
  return;
31457
+ }
31230
31458
  dataSets[this.state.index] = {
31231
31459
  ...dataSets[this.state.index],
31232
31460
  yAxisId: axis === "left" ? "y" : "y1",
@@ -31235,8 +31463,9 @@ class ChartWithAxisDesignPanel extends owl.Component {
31235
31463
  }
31236
31464
  getDataSerieAxis() {
31237
31465
  const dataSets = this.props.definition.dataSets;
31238
- if (!dataSets?.[this.state.index])
31466
+ if (!dataSets?.[this.state.index]) {
31239
31467
  return "left";
31468
+ }
31240
31469
  return dataSets[this.state.index].yAxisId === "y1" ? "right" : "left";
31241
31470
  }
31242
31471
  get canHaveTwoVerticalAxis() {
@@ -31244,9 +31473,10 @@ class ChartWithAxisDesignPanel extends owl.Component {
31244
31473
  }
31245
31474
  updateDataSeriesLabel(ev) {
31246
31475
  const label = ev.target.value;
31247
- const dataSets = this.props.definition.dataSets;
31248
- if (!dataSets?.[this.state.index])
31476
+ const dataSets = [...this.props.definition.dataSets];
31477
+ if (!dataSets?.[this.state.index]) {
31249
31478
  return;
31479
+ }
31250
31480
  dataSets[this.state.index] = {
31251
31481
  ...dataSets[this.state.index],
31252
31482
  label,
@@ -31263,6 +31493,81 @@ class ChartWithAxisDesignPanel extends owl.Component {
31263
31493
  updateShowValues(showValues) {
31264
31494
  this.props.updateChart(this.props.figureId, { showValues });
31265
31495
  }
31496
+ toggleDataTrend(display) {
31497
+ const dataSets = [...this.props.definition.dataSets];
31498
+ if (!dataSets?.[this.state.index]) {
31499
+ return;
31500
+ }
31501
+ dataSets[this.state.index] = {
31502
+ ...dataSets[this.state.index],
31503
+ trend: {
31504
+ type: "polynomial",
31505
+ order: 1,
31506
+ ...dataSets[this.state.index].trend,
31507
+ display,
31508
+ },
31509
+ };
31510
+ this.props.updateChart(this.props.figureId, { dataSets });
31511
+ }
31512
+ getTrendLineConfiguration() {
31513
+ const dataSets = this.props.definition.dataSets;
31514
+ return dataSets?.[this.state.index]?.trend;
31515
+ }
31516
+ getTrendType(config) {
31517
+ if (!config) {
31518
+ return "";
31519
+ }
31520
+ return config.type === "polynomial" && config.order === 1 ? "linear" : config.type;
31521
+ }
31522
+ onChangeTrendType(ev) {
31523
+ const type = ev.target.value;
31524
+ let config;
31525
+ switch (type) {
31526
+ case "linear":
31527
+ case "polynomial":
31528
+ config = {
31529
+ type: "polynomial",
31530
+ order: type === "linear" ? 1 : 2,
31531
+ };
31532
+ break;
31533
+ case "exponential":
31534
+ case "logarithmic":
31535
+ config = { type };
31536
+ break;
31537
+ default:
31538
+ return;
31539
+ }
31540
+ this.updateTrendLineValue(config);
31541
+ }
31542
+ onChangePolynomialDegree(ev) {
31543
+ const element = ev.target;
31544
+ const order = parseInt(element.value || "1");
31545
+ if (order < 2) {
31546
+ element.value = `${this.getTrendLineConfiguration()?.order ?? 2}`;
31547
+ return;
31548
+ }
31549
+ this.updateTrendLineValue({ order });
31550
+ }
31551
+ getTrendLineColor() {
31552
+ return this.getTrendLineConfiguration()?.color ?? setColorAlpha(this.getDataSerieColor(), 0.5);
31553
+ }
31554
+ updateTrendLineColor(color) {
31555
+ this.updateTrendLineValue({ color });
31556
+ }
31557
+ updateTrendLineValue(config) {
31558
+ const dataSets = [...this.props.definition.dataSets];
31559
+ if (!dataSets?.[this.state.index]) {
31560
+ return;
31561
+ }
31562
+ dataSets[this.state.index] = {
31563
+ ...dataSets[this.state.index],
31564
+ trend: {
31565
+ ...dataSets[this.state.index].trend,
31566
+ ...config,
31567
+ },
31568
+ };
31569
+ this.props.updateChart(this.props.figureId, { dataSets });
31570
+ }
31266
31571
  }
31267
31572
 
31268
31573
  class GaugeChartConfigPanel extends owl.Component {
@@ -33584,10 +33889,11 @@ class ConditionalFormattingEditor extends owl.Component {
33584
33889
  * Cell Is Rule
33585
33890
  ****************************************************************************/
33586
33891
  get isValue1Invalid() {
33587
- return !!this.state.errors?.includes("FirstArgMissing" /* CommandResult.FirstArgMissing */);
33892
+ return (this.state.errors.includes("FirstArgMissing" /* CommandResult.FirstArgMissing */) ||
33893
+ this.state.errors.includes("ValueCellIsInvalidFormula" /* CommandResult.ValueCellIsInvalidFormula */));
33588
33894
  }
33589
33895
  get isValue2Invalid() {
33590
- return !!this.state.errors?.includes("SecondArgMissing" /* CommandResult.SecondArgMissing */);
33896
+ return this.state.errors.includes("SecondArgMissing" /* CommandResult.SecondArgMissing */);
33591
33897
  }
33592
33898
  toggleStyle(tool) {
33593
33899
  const style = this.state.rules.cellIs.style;
@@ -35826,6 +36132,14 @@ css /* scss */ `
35826
36132
  .pivot-dim-operator-label {
35827
36133
  min-width: 120px;
35828
36134
  }
36135
+
36136
+ &.pivot-dimension-invalid {
36137
+ background-color: #ffdddd;
36138
+ border-color: red !important;
36139
+ select {
36140
+ background-color: #ffdddd;
36141
+ }
36142
+ }
35829
36143
  }
35830
36144
  `;
35831
36145
  class PivotDimension extends owl.Component {
@@ -47572,12 +47886,13 @@ class BordersPlugin extends CorePlugin {
47572
47886
  this.clearBorders(cmd.sheetId, cmd.target);
47573
47887
  break;
47574
47888
  case "REMOVE_COLUMNS_ROWS":
47575
- for (let el of [...cmd.elements].sort((a, b) => b - a)) {
47889
+ const elements = [...cmd.elements].sort((a, b) => b - a);
47890
+ for (const group of groupConsecutive(elements)) {
47576
47891
  if (cmd.dimension === "COL") {
47577
- this.shiftBordersHorizontally(cmd.sheetId, el + 1, -1);
47892
+ this.shiftBordersHorizontally(cmd.sheetId, group[group.length - 1] + 1, -group.length);
47578
47893
  }
47579
47894
  else {
47580
- this.shiftBordersVertically(cmd.sheetId, el + 1, -1);
47895
+ this.shiftBordersVertically(cmd.sheetId, group[group.length - 1] + 1, -group.length);
47581
47896
  }
47582
47897
  }
47583
47898
  break;
@@ -49014,7 +49329,7 @@ class ConditionalFormatPlugin extends CorePlugin {
49014
49329
  "LessThan",
49015
49330
  "LessThanOrEqual",
49016
49331
  "NotContains",
49017
- ]), this.checkOperatorArgsNumber(0, ["IsEmpty", "IsNotEmpty"]));
49332
+ ]), this.checkOperatorArgsNumber(0, ["IsEmpty", "IsNotEmpty"]), this.checkCFValues);
49018
49333
  case "ColorScaleRule": {
49019
49334
  return this.checkValidations(rule, this.chainValidations(this.checkThresholds(this.checkFormulaCompilation)), this.chainValidations(this.checkThresholds(this.checkNaN), this.batchValidations(this.checkMinBiggerThanMax, this.checkMinBiggerThanMid, this.checkMidBiggerThanMax
49020
49335
  // Those three validations can be factorized further
@@ -49131,6 +49446,17 @@ class ConditionalFormatPlugin extends CorePlugin {
49131
49446
  }
49132
49447
  return "Success" /* CommandResult.Success */;
49133
49448
  }
49449
+ checkCFValues(rule) {
49450
+ for (const value of rule.values) {
49451
+ if (!value.startsWith("="))
49452
+ continue;
49453
+ const compiledFormula = compile(value || "");
49454
+ if (compiledFormula.isBadExpression) {
49455
+ return "ValueCellIsInvalidFormula" /* CommandResult.ValueCellIsInvalidFormula */;
49456
+ }
49457
+ }
49458
+ return "Success" /* CommandResult.Success */;
49459
+ }
49134
49460
  removeConditionalFormatting(id, sheet) {
49135
49461
  const cfIndex = this.cfRules[sheet].findIndex((s) => s.id === id);
49136
49462
  if (cfIndex !== -1) {
@@ -51615,12 +51941,7 @@ class SheetPlugin extends CorePlugin {
51615
51941
  });
51616
51942
  }
51617
51943
  if (colIndex > deletedColumn) {
51618
- this.dispatch("UPDATE_CELL_POSITION", {
51619
- sheetId: sheet.id,
51620
- cellId: cellId,
51621
- col: colIndex - 1,
51622
- row: rowIndex,
51623
- });
51944
+ this.setNewPosition(cellId, sheet.id, colIndex - 1, rowIndex);
51624
51945
  }
51625
51946
  }
51626
51947
  }
@@ -51630,7 +51951,7 @@ class SheetPlugin extends CorePlugin {
51630
51951
  * Move the cells after a column or rows insertion
51631
51952
  */
51632
51953
  moveCellsOnAddition(sheet, addedElement, quantity, dimension) {
51633
- const commands = [];
51954
+ const updates = [];
51634
51955
  for (let rowIndex = 0; rowIndex < sheet.rows.length; rowIndex++) {
51635
51956
  const row = sheet.rows[rowIndex];
51636
51957
  if (dimension !== "rows" || rowIndex >= addedElement) {
@@ -51639,20 +51960,20 @@ class SheetPlugin extends CorePlugin {
51639
51960
  const cellId = row.cells[i];
51640
51961
  if (cellId) {
51641
51962
  if (dimension === "rows" || colIndex >= addedElement) {
51642
- commands.push({
51643
- type: "UPDATE_CELL_POSITION",
51963
+ updates.push({
51644
51964
  sheetId: sheet.id,
51645
51965
  cellId: cellId,
51646
51966
  col: colIndex + (dimension === "columns" ? quantity : 0),
51647
51967
  row: rowIndex + (dimension === "rows" ? quantity : 0),
51968
+ type: "UPDATE_CELL_POSITION",
51648
51969
  });
51649
51970
  }
51650
51971
  }
51651
51972
  }
51652
51973
  }
51653
51974
  }
51654
- for (let cmd of commands.reverse()) {
51655
- this.dispatch(cmd.type, cmd);
51975
+ for (let update of updates.reverse()) {
51976
+ this.updateCellPosition(update);
51656
51977
  }
51657
51978
  }
51658
51979
  /**
@@ -51685,12 +52006,7 @@ class SheetPlugin extends CorePlugin {
51685
52006
  const colIndex = Number(i);
51686
52007
  const cellId = row.cells[i];
51687
52008
  if (cellId) {
51688
- this.dispatch("UPDATE_CELL_POSITION", {
51689
- sheetId: sheet.id,
51690
- cellId: cellId,
51691
- col: colIndex,
51692
- row: rowIndex - numberRows,
51693
- });
52009
+ this.setNewPosition(cellId, sheet.id, colIndex, rowIndex - numberRows);
51694
52010
  }
51695
52011
  }
51696
52012
  }
@@ -54456,6 +54772,9 @@ class Evaluator {
54456
54772
  if (!this.blockedArrayFormulas.has(position)) {
54457
54773
  this.invalidateSpreading(position);
54458
54774
  }
54775
+ if (this.spreadingRelations.isArrayFormula(position)) {
54776
+ this.spreadingRelations.removeNode(position);
54777
+ }
54459
54778
  const cell = this.getters.getCell(position);
54460
54779
  if (cell === undefined) {
54461
54780
  return EMPTY_CELL;
@@ -54496,7 +54815,6 @@ class Evaluator {
54496
54815
  this.assertSheetHasEnoughSpaceToSpreadFormulaResult(formulaPosition, formulaReturn);
54497
54816
  const nbColumns = formulaReturn.length;
54498
54817
  const nbRows = formulaReturn[0].length;
54499
- this.spreadingRelations.removeNode(formulaPosition);
54500
54818
  forEachSpreadPositionInMatrix(nbColumns, nbRows, this.updateSpreadRelation(formulaPosition));
54501
54819
  this.assertNoMergedCellsInSpreadZone(formulaPosition, formulaReturn);
54502
54820
  forEachSpreadPositionInMatrix(nbColumns, nbRows, this.checkCollision(formulaPosition));
@@ -58205,6 +58523,8 @@ class InsertPivotPlugin extends UIPlugin {
58205
58523
  case "INSERT_PIVOT_WITH_TABLE":
58206
58524
  this.insertPivotWithTable(cmd.sheetId, cmd.col, cmd.row, cmd.pivotId, cmd.table);
58207
58525
  break;
58526
+ case "SPLIT_PIVOT_FORMULA":
58527
+ this.splitPivotFormula(cmd.sheetId, cmd.col, cmd.row, cmd.pivotId, cmd.table);
58208
58528
  }
58209
58529
  }
58210
58530
  insertNewPivot(pivotId, sheetId) {
@@ -58330,6 +58650,36 @@ class InsertPivotPlugin extends UIPlugin {
58330
58650
  });
58331
58651
  }
58332
58652
  }
58653
+ splitPivotFormula(sheetId, col, row, pivotId, pivotTableData) {
58654
+ this.dispatch("INSERT_PIVOT", {
58655
+ sheetId,
58656
+ col,
58657
+ row,
58658
+ pivotId,
58659
+ table: pivotTableData,
58660
+ });
58661
+ const table = this.getters.getCoreTable({ sheetId, col, row });
58662
+ if (table?.type === "dynamic") {
58663
+ const zone = positionToZone({ col, row });
58664
+ const { cols, rows, measures, fieldsType } = pivotTableData;
58665
+ const pivotTable = new SpreadsheetPivotTable(cols, rows, measures, fieldsType || {});
58666
+ const colNumber = pivotTable.getNumberOfDataColumns() + 1;
58667
+ const rowNumber = pivotTable.columns.length + pivotTable.rows.length;
58668
+ const tableZone = {
58669
+ left: col,
58670
+ top: row,
58671
+ right: col + colNumber - 1,
58672
+ bottom: row + rowNumber - 1,
58673
+ };
58674
+ const rangeData = this.getters.getRangeDataFromZone(sheetId, tableZone);
58675
+ this.dispatch("UPDATE_TABLE", {
58676
+ sheetId,
58677
+ zone,
58678
+ newTableRange: rangeData,
58679
+ tableType: "static",
58680
+ });
58681
+ }
58682
+ }
58333
58683
  }
58334
58684
 
58335
58685
  class SortPlugin extends UIPlugin {
@@ -63410,6 +63760,8 @@ css /* scss */ `
63410
63760
 
63411
63761
  .o-sidePanel-handle-container {
63412
63762
  width: 8px;
63763
+ position: fixed;
63764
+ top: 50%;
63413
63765
  }
63414
63766
  .o-sidePanel-handle {
63415
63767
  cursor: col-resize;
@@ -63817,13 +64169,6 @@ class TopBarComposer extends owl.Component {
63817
64169
  "border-color": SELECTION_BORDER_COLOR,
63818
64170
  });
63819
64171
  }
63820
- get delimitation() {
63821
- const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
63822
- return {
63823
- width,
63824
- height,
63825
- };
63826
- }
63827
64172
  onFocus(selection) {
63828
64173
  this.composerFocusStore.focusComposer(this.composerInterface, { selection });
63829
64174
  }
@@ -66045,7 +66390,7 @@ function createEmptyStructure(node) {
66045
66390
  }
66046
66391
 
66047
66392
  class StateObserver {
66048
- changes = [];
66393
+ changes;
66049
66394
  commands = [];
66050
66395
  /**
66051
66396
  * Record the changes which could happen in the given callback, save them in a
@@ -66077,7 +66422,7 @@ class StateObserver {
66077
66422
  if (value[key] === val) {
66078
66423
  return;
66079
66424
  }
66080
- this.changes.push({
66425
+ this.changes?.push({
66081
66426
  key,
66082
66427
  target: value,
66083
66428
  before: value[key],
@@ -68744,6 +69089,6 @@ exports.tokenColors = tokenColors;
68744
69089
  exports.tokenize = tokenize;
68745
69090
 
68746
69091
 
68747
- __info__.version = "17.5.0-alpha.2";
68748
- __info__.date = "2024-07-24T10:25:55.208Z";
68749
- __info__.hash = "15fd5a8";
69092
+ __info__.version = "17.5.0-alpha.3";
69093
+ __info__.date = "2024-08-02T08:24:53.425Z";
69094
+ __info__.hash = "767725f";