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