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