@odoo/o-spreadsheet 19.5.0-alpha.11 → 19.5.0-alpha.13

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.
Files changed (56) hide show
  1. package/dist/material_symbols_outlined.css +9 -0
  2. package/dist/o_spreadsheet.cjs +2578 -900
  3. package/dist/o_spreadsheet.css +182 -41
  4. package/dist/o_spreadsheet.esm.js +2570 -892
  5. package/dist/o_spreadsheet.iife.js +2578 -900
  6. package/dist/o_spreadsheet.min.iife.js +344 -344
  7. package/dist/o_spreadsheet.xml +238 -150
  8. package/dist/types/actions/data_actions.d.ts +1 -0
  9. package/dist/types/components/bottom_bar/bottom_bar_statistic/bottom_bar_statistic.d.ts +3 -0
  10. package/dist/types/components/dashboard/clickable_cell_sort_icon/clickable_cell_sort_icon.d.ts +1 -1
  11. package/dist/types/components/figures/figure/figure.d.ts +1 -0
  12. package/dist/types/components/figures/figure_carousel/figure_carousel.d.ts +10 -0
  13. package/dist/types/components/figures/figure_container/figure_container.d.ts +1 -2
  14. package/dist/types/components/grid/grid.d.ts +1 -0
  15. package/dist/types/components/helpers/chart_drag_and_drop.d.ts +15 -0
  16. package/dist/types/components/helpers/dom_helpers.d.ts +3 -2
  17. package/dist/types/components/link/link_editor/link_editor.d.ts +1 -1
  18. package/dist/types/components/side_panel/carousel_panel/carousel_panel.d.ts +1 -1
  19. package/dist/types/components/side_panel/chart/building_blocks/color_scale/color_scale_picker.d.ts +1 -1
  20. package/dist/types/components/side_panel/chart/scorecard_chart_panel/scorecard_chart_config_panel.d.ts +6 -10
  21. package/dist/types/components/side_panel/data_analysis/chart_suggestion_preview.d.ts +19 -0
  22. package/dist/types/components/side_panel/data_analysis/data_analysis_panel.d.ts +17 -0
  23. package/dist/types/components/side_panel/data_analysis/data_analysis_store.d.ts +15 -0
  24. package/dist/types/components/side_panel/pivot/pivot_layout_configurator/pivot_layout_configurator.d.ts +1 -6
  25. package/dist/types/components/side_panel/side_panel/side_panel_store.d.ts +2 -1
  26. package/dist/types/components/standalone_grid_canvas/carousel_data_view_print.d.ts +32 -0
  27. package/dist/types/components/standalone_grid_canvas/standalone_grid_canvas.d.ts +6 -0
  28. package/dist/types/components/standalone_viewport/standalone_viewport.d.ts +6 -3
  29. package/dist/types/components/standalone_viewport/standalone_viewport_store.d.ts +2 -1
  30. package/dist/types/components/tables/hovered_table_store.d.ts +6 -7
  31. package/dist/types/constants.d.ts +13 -1
  32. package/dist/types/functions/helpers.d.ts +4 -0
  33. package/dist/types/functions/module_operators.d.ts +1 -1
  34. package/dist/types/functions/module_text.d.ts +11 -11
  35. package/dist/types/functions/module_web.d.ts +1 -1
  36. package/dist/types/helpers/carousel_helpers.d.ts +8 -1
  37. package/dist/types/helpers/data_analysis.d.ts +16 -0
  38. package/dist/types/helpers/figures/chart.d.ts +1 -1
  39. package/dist/types/helpers/figures/charts/chart_data_source_helpers.d.ts +10 -0
  40. package/dist/types/helpers/figures/charts/chart_suggestion_engine.d.ts +8 -0
  41. package/dist/types/helpers/figures/charts/chart_ui_common.d.ts +8 -1
  42. package/dist/types/helpers/figures/charts/charts_suggestions_constants.d.ts +37 -0
  43. package/dist/types/helpers/figures/charts/gauge_chart_rendering.d.ts +6 -2
  44. package/dist/types/helpers/text_helper.d.ts +1 -1
  45. package/dist/types/index.d.ts +3 -1
  46. package/dist/types/registries/figures_registry.d.ts +1 -0
  47. package/dist/types/stores/cell_hover_overlay_store.d.ts +15 -0
  48. package/dist/types/stores/chart_drag_store.d.ts +10 -0
  49. package/dist/types/stores/grid_renderer_store.d.ts +3 -2
  50. package/dist/types/stores/main_viewport_store.d.ts +3 -0
  51. package/dist/types/stores/viewports_store.d.ts +0 -1
  52. package/dist/types/types/chart/chart.d.ts +3 -1
  53. package/dist/types/types/chart/chart_suggestion.d.ts +136 -0
  54. package/dist/types/types/chart/scorecard_chart.d.ts +3 -4
  55. package/dist/types/types/commands.d.ts +16 -11
  56. package/package.json +6 -5
@@ -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 19.5.0-alpha.11
6
- * @date 2026-08-12T09:17:24.237Z
7
- * @hash bc056d7
5
+ * @version 19.5.0-alpha.13
6
+ * @date 2026-08-21T15:33:44.046Z
7
+ * @hash a73559e
8
8
  */
9
9
 
10
10
  import * as owl from "@odoo/owl";
@@ -206,6 +206,16 @@ function getBoundingRectAsPOJO(el) {
206
206
  height: rect.height
207
207
  };
208
208
  }
209
+ function getBoundingRectWithMargins(el) {
210
+ const style = getComputedStyle(el);
211
+ const rect = el.getBoundingClientRect();
212
+ return {
213
+ x: rect.x,
214
+ y: rect.y,
215
+ width: rect.width + parseInt(style.marginLeft || "0") + parseInt(style.marginRight || "0"),
216
+ height: rect.height + parseInt(style.marginTop || "0") + parseInt(style.marginBottom || "0")
217
+ };
218
+ }
209
219
  /**
210
220
  * Iterate over all the children of `el` in the dom tree starting at `el`, depth first.
211
221
  */
@@ -391,7 +401,13 @@ function createAction(item) {
391
401
  if (isEnabled(env)) return item.execute(env, isMiddleClick);
392
402
  } : void 0,
393
403
  children: children ? (env) => {
394
- return children.map((child) => typeof child === "function" ? child(env) : child).flat().map(createAction).sort((a, b) => a.sequence - b.sequence);
404
+ const uniqueChildren = {};
405
+ children.flatMap((child) => typeof child === "function" ? child(env) : child).forEach((childSpec) => {
406
+ const childAction = createAction(childSpec);
407
+ if (uniqueChildren[childAction.id]) throw new Error(`Duplicate child action id "${childAction.id}" in action "${itemId}".`);
408
+ uniqueChildren[childAction.id] = childAction;
409
+ });
410
+ return Object.values(uniqueChildren).sort((a, b) => a.sequence - b.sequence);
395
411
  } : () => [],
396
412
  isReadonlyAllowed: item.isReadonlyAllowed || false,
397
413
  isEnabledOnLockedSheet: item.isEnabledOnLockedSheet || false,
@@ -649,6 +665,14 @@ const DEFAULT_CAROUSEL_TITLE_STYLE = {
649
665
  fontSize: 18,
650
666
  color: TEXT_BODY
651
667
  };
668
+ const CAROUSEL_LAYOUT = {
669
+ paddingX: 24,
670
+ paddingY: 8,
671
+ headerPaddingTop: 4,
672
+ separatorWidth: 1,
673
+ headerLineHeight: 1.5,
674
+ minHeaderHeight: 25
675
+ };
652
676
  const DEFAULT_TOKEN_COLOR = "light-dark(#000000, #ffffff)";
653
677
  const functionColor = DEFAULT_TOKEN_COLOR;
654
678
  const operatorColor = "#3da4ab";
@@ -4071,6 +4095,18 @@ function toString(data) {
4071
4095
  default: return "";
4072
4096
  }
4073
4097
  }
4098
+ /**
4099
+ * Only converts the decimal separator, ignore number format such as % or dates
4100
+ */
4101
+ function toLocaleString(data, locale) {
4102
+ const value = toValue(data);
4103
+ switch (typeof value) {
4104
+ case "string": return value;
4105
+ case "number": return value.toString().replace(".", locale.decimalSeparator);
4106
+ case "boolean": return value ? "TRUE" : "FALSE";
4107
+ default: return "";
4108
+ }
4109
+ }
4074
4110
  /** Normalize string by setting it to lowercase and replacing accent letters with plain letters */
4075
4111
  const normalizeString = memoize(function normalizeString(str) {
4076
4112
  return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
@@ -5377,7 +5413,7 @@ function _roundFormat(internalFormat) {
5377
5413
  }
5378
5414
  function humanizeNumber({ value, format }, locale) {
5379
5415
  const numberValue = tryToNumber(value, locale);
5380
- if (numberValue === void 0) return "";
5416
+ if (numberValue === void 0) return value?.toString() || "";
5381
5417
  let numberFormat = format;
5382
5418
  if (Math.abs(numberValue) < 1e3) {
5383
5419
  const hasDecimal = numberValue % 1 !== 0;
@@ -8092,8 +8128,6 @@ let CommandResult = /* @__PURE__ */ function(CommandResult) {
8092
8128
  CommandResult["InvalidXRange"] = "InvalidXRange";
8093
8129
  CommandResult["InvalidLabelRange"] = "InvalidLabelRange";
8094
8130
  CommandResult["InvalidBubbleSizeRange"] = "InvalidBubbleSizeRange";
8095
- CommandResult["InvalidScorecardKeyValue"] = "InvalidScorecardKeyValue";
8096
- CommandResult["InvalidScorecardBaseline"] = "InvalidScorecardBaseline";
8097
8131
  CommandResult["InvalidGaugeDataRange"] = "InvalidGaugeDataRange";
8098
8132
  CommandResult["EmptyGaugeRangeMin"] = "EmptyGaugeRangeMin";
8099
8133
  CommandResult["GaugeRangeMinNaN"] = "GaugeRangeMinNaN";
@@ -9247,8 +9281,8 @@ function computeCachedTextDimension(context, text, font) {
9247
9281
  function fontSizeInPixels(fontSize) {
9248
9282
  return Math.round(fontSize * 96 / 72);
9249
9283
  }
9250
- function computeTextFont(style, fontUnit = "pt") {
9251
- return `${style.italic ? "italic " : ""}${style.bold ? "bold" : "400"} ${(fontUnit === "pt" ? computeTextFontSizeInPixels(style) : style.fontSize) ?? DEFAULT_FONT_SIZE}px ${DEFAULT_FONT}`;
9284
+ function computeTextFont(style, fontUnit = "pt", fontWeight = 400) {
9285
+ return `${style.italic ? "italic " : ""}${style.bold ? "bold" : fontWeight} ${(fontUnit === "pt" ? computeTextFontSizeInPixels(style) : style.fontSize) ?? DEFAULT_FONT_SIZE}px ${DEFAULT_FONT}`;
9252
9286
  }
9253
9287
  function computeTextFontSizeInPixels(style) {
9254
9288
  return fontSizeInPixels(style?.fontSize || DEFAULT_FONT_SIZE);
@@ -11882,7 +11916,7 @@ var ChartJsComponent = class extends Component {
11882
11916
  chartId: types$1.string(),
11883
11917
  isFullScreen: types$1.boolean().optional()
11884
11918
  });
11885
- canvas = signal(null);
11919
+ canvas = signal.ref(HTMLCanvasElement);
11886
11920
  chart;
11887
11921
  currentRuntime;
11888
11922
  animationStore;
@@ -11980,6 +12014,110 @@ var ChartJsComponent = class extends Component {
11980
12014
  }
11981
12015
  };
11982
12016
 
12017
+ //#endregion
12018
+ //#region src/functions/helper_matrices.ts
12019
+ function getUnitMatrix(n) {
12020
+ const matrix = Array(n);
12021
+ for (let i = 0; i < n; i++) {
12022
+ matrix[i] = Array(n).fill(0);
12023
+ matrix[i][i] = 1;
12024
+ }
12025
+ return matrix;
12026
+ }
12027
+ /**
12028
+ * Invert a matrix and compute its determinant using Gaussian Elimination.
12029
+ *
12030
+ * The Matrix should be a square matrix, and should be indexed [col][row] instead of the
12031
+ * standard mathematical indexing [row][col].
12032
+ */
12033
+ function invertMatrix(M) {
12034
+ if (M.length < 1 || M[0].length < 1) throw new Error("invertMatrix: an empty matrix cannot be inverted.");
12035
+ if (M.length !== M[0].length) throw new Error("invertMatrix: only square matrices are invertible");
12036
+ let determinant = 1;
12037
+ const dim = M.length;
12038
+ const I = getUnitMatrix(dim);
12039
+ const C = M.map((row) => row.slice());
12040
+ for (let pivot = 0; pivot < dim; pivot++) {
12041
+ let diagonalElement = C[pivot][pivot];
12042
+ if (diagonalElement === 0) {
12043
+ for (let row = pivot + 1; row < dim; row++) if (C[pivot][row] !== 0) {
12044
+ swapMatrixRows(C, pivot, row);
12045
+ swapMatrixRows(I, pivot, row);
12046
+ determinant *= -1;
12047
+ break;
12048
+ }
12049
+ diagonalElement = C[pivot][pivot];
12050
+ if (diagonalElement === 0) return { determinant: 0 };
12051
+ }
12052
+ for (let col = 0; col < dim; col++) {
12053
+ C[col][pivot] = C[col][pivot] / diagonalElement;
12054
+ I[col][pivot] = I[col][pivot] / diagonalElement;
12055
+ }
12056
+ determinant *= diagonalElement;
12057
+ for (let row = 0; row < dim; row++) {
12058
+ if (row === pivot) continue;
12059
+ const e = C[pivot][row];
12060
+ for (let col = 0; col < dim; col++) {
12061
+ C[col][row] -= e * C[col][pivot];
12062
+ I[col][row] -= e * I[col][pivot];
12063
+ }
12064
+ }
12065
+ }
12066
+ return {
12067
+ inverted: I,
12068
+ determinant
12069
+ };
12070
+ }
12071
+ function swapMatrixRows(matrix, row1, row2) {
12072
+ for (let i = 0; i < matrix.length; i++) {
12073
+ const tmp = matrix[i][row1];
12074
+ matrix[i][row1] = matrix[i][row2];
12075
+ matrix[i][row2] = tmp;
12076
+ }
12077
+ }
12078
+ /**
12079
+ * Matrix multiplication of 2 matrices.
12080
+ * ex: matrix1 : n x l, matrix2 : m x n => result : m x l
12081
+ *
12082
+ * Note: we use indexing [col][row] instead of the standard mathematical notation [row][col]
12083
+ */
12084
+ function multiplyMatrices(matrix1, matrix2) {
12085
+ if (matrix1.length < 1 || matrix2.length < 1) throw new Error("multiplyMatrices: empty matrices cannot be multiplied.");
12086
+ if (matrix1.length !== matrix2[0].length) throw new Error("multiplyMatrices: incompatible matrices size.");
12087
+ const rowsM1 = matrix1[0].length;
12088
+ const colsM2 = matrix2.length;
12089
+ const n = matrix1.length;
12090
+ const result = Array(colsM2);
12091
+ for (let col = 0; col < colsM2; col++) {
12092
+ result[col] = Array(rowsM1);
12093
+ for (let row = 0; row < rowsM1; row++) {
12094
+ let sum = 0;
12095
+ for (let k = 0; k < n; k++) sum += matrix1[k][row] * matrix2[col][k];
12096
+ result[col][row] = sum;
12097
+ }
12098
+ }
12099
+ return result;
12100
+ }
12101
+ /**
12102
+ * Return the input if it's a scalar or the first element of the input if it's a matrix.
12103
+ */
12104
+ function toScalar(arg) {
12105
+ if (!isMatrix(arg)) return arg;
12106
+ if (!isSingleElementMatrix(arg)) throw new Error("The value should be a scalar or a 1x1 matrix");
12107
+ return arg[0][0];
12108
+ }
12109
+ function isSingleElementMatrix(matrix) {
12110
+ return matrix.length === 1 && matrix[0].length === 1;
12111
+ }
12112
+ function isMultipleElementMatrix(arg) {
12113
+ return isMatrix(arg) && !isSingleElementMatrix(arg);
12114
+ }
12115
+ function getMatrixArgIndices(args) {
12116
+ const indices = [];
12117
+ for (let i = 0; i < args.length; i++) if (isMultipleElementMatrix(args[i])) indices.push(i);
12118
+ return indices;
12119
+ }
12120
+
11983
12121
  //#endregion
11984
12122
  //#region src/helpers/figures/charts/abstract_chart.ts
11985
12123
  var AbstractChart = class {
@@ -11995,11 +12133,42 @@ var AbstractChart = class {
11995
12133
 
11996
12134
  //#endregion
11997
12135
  //#region src/helpers/figures/charts/scorecard_chart.ts
12136
+ function getData$1(value, getters, sheetId) {
12137
+ if (!value) return {
12138
+ scalar: void 0,
12139
+ range: void 0
12140
+ };
12141
+ if (!isFormula(value)) return {
12142
+ scalar: { value },
12143
+ range: void 0
12144
+ };
12145
+ const result = getters.evaluateFormulaResult(sheetId, value);
12146
+ let scalar = isMultipleElementMatrix(result) ? result[0][0] : toScalar(result);
12147
+ let range = void 0;
12148
+ const xc = getFormulaRangeXc(value);
12149
+ if (xc) {
12150
+ range = createValidRange(getters, sheetId, xc);
12151
+ if (range) {
12152
+ if (getters.getEvaluatedCell({
12153
+ sheetId: range.sheetId,
12154
+ col: range.zone.left,
12155
+ row: range.zone.top
12156
+ }).type === "empty") scalar = void 0;
12157
+ }
12158
+ }
12159
+ return {
12160
+ scalar,
12161
+ range
12162
+ };
12163
+ }
11998
12164
  function getBaselineText(baseline, keyValue, baselineMode, humanizeNumbers, locale) {
11999
12165
  if (!baseline) return "";
12000
- else if (baselineMode === "text" || keyValue?.type !== "number" || baseline.type !== "number") {
12166
+ else if (baselineMode === "text" || typeof keyValue?.value !== "number" || typeof baseline.value !== "number") {
12001
12167
  if (humanizeNumbers) return humanizeNumber(baseline, locale);
12002
- return baseline.formattedValue;
12168
+ return formatValue(baseline.value, {
12169
+ format: baseline.format,
12170
+ locale
12171
+ });
12003
12172
  }
12004
12173
  let { value, format } = baseline;
12005
12174
  if (baselineMode === "progress") {
@@ -12020,29 +12189,31 @@ function getBaselineText(baseline, keyValue, baselineMode, humanizeNumbers, loca
12020
12189
  locale
12021
12190
  });
12022
12191
  }
12023
- function getKeyValueText(keyValueCell, humanizeNumbers, locale) {
12024
- if (!keyValueCell) return "";
12025
- if (humanizeNumbers) return humanizeNumber(keyValueCell, locale);
12026
- return keyValueCell.formattedValue ?? String(keyValueCell.value ?? "");
12192
+ function getKeyValueText(keyValue, humanizeNumbers, locale) {
12193
+ if (keyValue?.value === void 0 || keyValue?.value === null) return "";
12194
+ if (humanizeNumbers) return humanizeNumber(keyValue, locale);
12195
+ return keyValue.format ? formatValue(keyValue.value, {
12196
+ format: keyValue.format,
12197
+ locale
12198
+ }) : String(keyValue.value ?? "");
12027
12199
  }
12028
12200
  function getBaselineColor(baseline, baselineMode, keyValue, colorUp, colorDown) {
12029
- if (baselineMode === "text" || baselineMode === "progress" || baseline?.type !== "number" || keyValue?.type !== "number") return;
12201
+ if (baselineMode === "text" || baselineMode === "progress" || typeof baseline?.value !== "number" || typeof keyValue?.value !== "number") return;
12030
12202
  const diff = keyValue.value - baseline.value;
12031
12203
  if (diff > 0) return colorUp;
12032
12204
  else if (diff < 0) return colorDown;
12033
12205
  }
12034
12206
  function getBaselineArrowDirection(baseline, keyValue, baselineMode) {
12035
- if (baselineMode === "text" || baseline?.type !== "number" || keyValue?.type !== "number") return "neutral";
12207
+ if (baselineMode === "text" || typeof baseline?.value !== "number" || typeof keyValue?.value !== "number") return "neutral";
12036
12208
  const diff = keyValue.value - baseline.value;
12037
12209
  if (diff > 0) return "up";
12038
12210
  else if (diff < 0) return "down";
12039
12211
  return "neutral";
12040
12212
  }
12041
- function checkKeyValue(definition) {
12042
- return definition.keyValue && !rangeReference.test(definition.keyValue) ? "InvalidScorecardKeyValue" : "Success";
12043
- }
12044
- function checkBaseline(definition) {
12045
- return definition.baseline && !rangeReference.test(definition.baseline) ? "InvalidScorecardBaseline" : "Success";
12213
+ function getFormulaRangeXc(formula) {
12214
+ if (!formula || !isFormula(formula)) return;
12215
+ const content = formula.slice(1);
12216
+ return rangeReference.test(content) ? content : void 0;
12046
12217
  }
12047
12218
  const Path2DConstructor = globalThis.Path2D;
12048
12219
  const arrowDownPath = Path2DConstructor && new Path2DConstructor("M8.6 4.8a.5.5 0 0 1 0 .75l-3.9 3.9a.5 .5 0 0 1 -.75 0l-3.8 -3.9a.5 .5 0 0 1 0 -.75l.4-.4a.5.5 0 0 1 .75 0l2.3 2.4v-5.7c0-.25.25-.5.5-.5h.6c.25 0 .5.25.5.5v5.8l2.3 -2.4a.5.5 0 0 1 .75 0z");
@@ -12059,84 +12230,76 @@ const ScorecardChart$1 = {
12059
12230
  "baselineColorUp",
12060
12231
  "baselineColorDown"
12061
12232
  ],
12062
- fromStrDefinition(definition, sheetId, getters) {
12063
- const baseline = createValidRange(getters, sheetId, definition.baseline);
12064
- const keyValue = createValidRange(getters, sheetId, definition.keyValue);
12233
+ fromStrDefinition: (definition) => definition,
12234
+ validateDefinition(validator, definition) {
12235
+ return "Success";
12236
+ },
12237
+ copyInSheetId: (definition, sheetIdFrom, sheetIdTo, getters) => {
12238
+ const adaptFormula = (formula) => getters.copyFormulaStringForSheet(sheetIdFrom, sheetIdTo, formula, "keepSameReference");
12065
12239
  return {
12066
12240
  ...definition,
12067
- baseline,
12068
- keyValue
12241
+ keyValue: definition.keyValue ? adaptFormula(definition.keyValue) : definition.keyValue,
12242
+ baseline: definition.baseline ? adaptFormula(definition.baseline) : definition.baseline
12069
12243
  };
12070
12244
  },
12071
- validateDefinition(validator, definition) {
12072
- return validator.checkValidations(definition, checkKeyValue, checkBaseline);
12073
- },
12074
- copyInSheetId: (definition) => definition,
12075
12245
  getDefinitionFromContextCreation(context, dataSourceBuilder) {
12246
+ const dataRange = context.dataSource?.type === "range" ? context.dataSource?.dataSets?.[0]?.dataRange : void 0;
12247
+ const keyValue = (context.scorecardKeyValueFormula === void 0 || getFormulaRangeXc(context.scorecardKeyValueFormula)) && dataRange ? `=${dataRange}` : context.scorecardKeyValueFormula;
12248
+ const baseline = (context.scorecardBaselineFormula === void 0 || getFormulaRangeXc(context.scorecardBaselineFormula)) && context.auxiliaryRange ? `=${context.auxiliaryRange}` : context.scorecardBaselineFormula;
12076
12249
  return {
12077
12250
  background: context.background,
12078
12251
  type: "scorecard",
12079
- keyValue: context.dataSource?.type === "range" ? context.dataSource?.dataSets?.[0]?.dataRange : void 0,
12252
+ keyValue,
12080
12253
  title: context.title || { text: "" },
12081
12254
  baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
12082
12255
  baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
12083
12256
  baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
12084
- baseline: context.auxiliaryRange || "",
12257
+ baseline,
12085
12258
  humanize: context.humanize,
12086
12259
  annotationLink: context.annotationLink,
12087
12260
  annotationText: context.annotationText
12088
12261
  };
12089
12262
  },
12090
- transformDefinition(definition, chartSheetId, { adaptRangeString }) {
12263
+ transformDefinition(definition, chartSheetId, { adaptFormulaString }) {
12091
12264
  let baseline;
12092
12265
  let keyValue;
12093
- if (definition.baseline) {
12094
- const { changeType, range: adaptedRange } = adaptRangeString(chartSheetId, definition.baseline);
12095
- if (changeType !== "REMOVE") baseline = adaptedRange;
12096
- }
12097
- if (definition.keyValue) {
12098
- const { changeType, range: adaptedRange } = adaptRangeString(chartSheetId, definition.keyValue);
12099
- if (changeType !== "REMOVE") keyValue = adaptedRange;
12100
- }
12266
+ if (definition.baseline) baseline = adaptFormulaString(chartSheetId, definition.baseline);
12267
+ if (definition.keyValue) keyValue = adaptFormulaString(chartSheetId, definition.keyValue);
12101
12268
  return {
12102
12269
  ...definition,
12103
12270
  baseline,
12104
12271
  keyValue
12105
12272
  };
12106
12273
  },
12107
- duplicateInDuplicatedSheet(definition, sheetIdFrom, sheetIdTo) {
12108
- const baseline = duplicateLabelRangeInDuplicatedSheet(sheetIdFrom, sheetIdTo, definition.baseline);
12109
- const keyValue = duplicateLabelRangeInDuplicatedSheet(sheetIdFrom, sheetIdTo, definition.keyValue);
12274
+ duplicateInDuplicatedSheet(definition, sheetIdFrom, sheetIdTo, getters) {
12275
+ const adaptFormula = (formula) => getters.copyFormulaStringForSheet(sheetIdFrom, sheetIdTo, formula, "moveReference");
12110
12276
  return {
12111
12277
  ...definition,
12112
- baseline,
12113
- keyValue
12114
- };
12115
- },
12116
- toStrDefinition(definition, sheetId, getters) {
12117
- return {
12118
- ...definition,
12119
- keyValue: definition.keyValue ? getters.getRangeString(definition.keyValue, sheetId) : void 0,
12120
- baseline: definition.baseline ? getters.getRangeString(definition.baseline, sheetId) : void 0
12278
+ keyValue: definition.keyValue ? adaptFormula(definition.keyValue) : definition.keyValue,
12279
+ baseline: definition.baseline ? adaptFormula(definition.baseline) : definition.baseline
12121
12280
  };
12122
12281
  },
12282
+ toStrDefinition: (definition) => definition,
12123
12283
  getContextCreation(definition, dataSource) {
12284
+ const keyValueXc = getFormulaRangeXc(definition.keyValue);
12124
12285
  return {
12125
12286
  ...definition,
12126
12287
  dataSource: {
12127
12288
  type: "range",
12128
- dataSets: definition.keyValue ? [{
12129
- dataRange: definition.keyValue,
12289
+ dataSets: keyValueXc ? [{
12290
+ dataRange: keyValueXc,
12130
12291
  dataSetId: "0"
12131
12292
  }] : []
12132
12293
  },
12133
- auxiliaryRange: definition.baseline
12294
+ auxiliaryRange: getFormulaRangeXc(definition.baseline),
12295
+ scorecardKeyValueFormula: definition.keyValue,
12296
+ scorecardBaselineFormula: definition.baseline
12134
12297
  };
12135
12298
  },
12136
12299
  getDefinitionForExcel: () => void 0,
12137
- updateRanges(definition, adapterFunctions) {
12138
- const baseline = adaptChartRange(definition.baseline, adapterFunctions);
12139
- const keyValue = adaptChartRange(definition.keyValue, adapterFunctions);
12300
+ updateRanges(definition, adapterFunctions, sheetId) {
12301
+ const baseline = definition.baseline ? adapterFunctions.adaptFormulaString(sheetId, definition.baseline) : definition.baseline;
12302
+ const keyValue = definition.keyValue ? adapterFunctions.adaptFormulaString(sheetId, definition.keyValue) : definition.keyValue;
12140
12303
  if (definition.baseline === baseline && definition.keyValue === keyValue) return definition;
12141
12304
  return {
12142
12305
  ...definition,
@@ -12144,32 +12307,21 @@ const ScorecardChart$1 = {
12144
12307
  keyValue
12145
12308
  };
12146
12309
  },
12147
- getFormulas: () => [],
12148
- getRuntime(getters, definition) {
12310
+ getFormulas(getters, sheetId, definition) {
12311
+ const formulas = [];
12312
+ if (definition.keyValue && isFormula(definition.keyValue)) formulas.push(CompiledFormula.Compile(definition.keyValue, sheetId, getters));
12313
+ if (definition.baseline && isFormula(definition.baseline)) formulas.push(CompiledFormula.Compile(definition.baseline, sheetId, getters));
12314
+ return formulas;
12315
+ },
12316
+ getRuntime(getters, definition, _dataExtractor, sheetId) {
12149
12317
  let formattedKeyValue = "";
12150
- let keyValueCell;
12318
+ const { scalar: keyValue, range: keyValueRange } = getData$1(definition.keyValue, getters, sheetId);
12151
12319
  const locale = getters.getLocale();
12152
- if (definition.keyValue) {
12153
- const keyValuePosition = {
12154
- sheetId: definition.keyValue.sheetId,
12155
- col: definition.keyValue.zone.left,
12156
- row: definition.keyValue.zone.top
12157
- };
12158
- keyValueCell = getters.getEvaluatedCell(keyValuePosition);
12159
- formattedKeyValue = getKeyValueText(keyValueCell, definition.humanize ?? true, locale);
12160
- }
12161
- let baselineCell;
12162
- const baseline = definition.baseline;
12163
- if (baseline) {
12164
- const baselinePosition = {
12165
- sheetId: baseline.sheetId,
12166
- col: baseline.zone.left,
12167
- row: baseline.zone.top
12168
- };
12169
- baselineCell = getters.getEvaluatedCell(baselinePosition);
12170
- }
12171
- const { background, fontColor } = getters.getStyleOfSingleCellChart(definition.background, definition.keyValue);
12172
- const baselineDisplay = getBaselineText(baselineCell, keyValueCell, definition.baselineMode, definition.humanize ?? true, locale);
12320
+ if (keyValue !== null && keyValue !== void 0) formattedKeyValue = getKeyValueText(keyValue, definition.humanize ?? true, locale);
12321
+ else formattedKeyValue = "";
12322
+ const { scalar: baseline, range: baselineRange } = getData$1(definition.baseline, getters, sheetId);
12323
+ const { background, fontColor } = getters.getStyleOfSingleCellChart(definition.background, keyValueRange);
12324
+ const baselineDisplay = getBaselineText(baseline, keyValue, definition.baselineMode, definition.humanize ?? true, locale);
12173
12325
  const baselineValue = definition.baselineMode === "progress" && isNumber(baselineDisplay, locale) ? toNumber(baselineDisplay, locale) : 0;
12174
12326
  const title = definition.title;
12175
12327
  return {
@@ -12180,16 +12332,16 @@ const ScorecardChart$1 = {
12180
12332
  keyValue: formattedKeyValue,
12181
12333
  keyDescr: definition.keyDescr?.text ? getters.dynamicTranslate(definition.keyDescr.text) : "",
12182
12334
  baselineDisplay,
12183
- baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, definition.baselineMode),
12184
- baselineColor: getBaselineColor(baselineCell, definition.baselineMode, keyValueCell, definition.baselineColorUp, definition.baselineColorDown),
12335
+ baselineArrow: getBaselineArrowDirection(baseline, keyValue, definition.baselineMode),
12336
+ baselineColor: getBaselineColor(baseline, definition.baselineMode, keyValue, definition.baselineColorUp, definition.baselineColorDown),
12185
12337
  baselineDescr: definition.baselineMode !== "progress" && definition.baselineDescr?.text ? getters.dynamicTranslate(definition.baselineDescr.text) : "",
12186
12338
  fontColor,
12187
12339
  background,
12188
12340
  baselineStyle: {
12189
- ...definition.baselineMode !== "percentage" && definition.baselineMode !== "progress" && baseline ? getters.getCellComputedStyle({
12190
- sheetId: baseline.sheetId,
12191
- col: baseline.zone.left,
12192
- row: baseline.zone.top
12341
+ ...definition.baselineMode !== "percentage" && definition.baselineMode !== "progress" && baselineRange ? getters.getCellComputedStyle({
12342
+ sheetId: baselineRange.sheetId,
12343
+ col: baselineRange.zone.left,
12344
+ row: baselineRange.zone.top
12193
12345
  }) : void 0,
12194
12346
  fontSize: definition.baselineDescr?.fontSize,
12195
12347
  align: definition.baselineDescr?.align
@@ -12199,10 +12351,10 @@ const ScorecardChart$1 = {
12199
12351
  ...definition.baselineDescr
12200
12352
  },
12201
12353
  keyValueStyle: {
12202
- ...definition.keyValue ? getters.getCellComputedStyle({
12203
- sheetId: definition.keyValue.sheetId,
12204
- col: definition.keyValue.zone.left,
12205
- row: definition.keyValue.zone.top
12354
+ ...keyValueRange ? getters.getCellComputedStyle({
12355
+ sheetId: keyValueRange.sheetId,
12356
+ col: keyValueRange.zone.left,
12357
+ row: keyValueRange.zone.top
12206
12358
  }) : void 0,
12207
12359
  fontSize: definition.keyDescr?.fontSize,
12208
12360
  align: definition.keyDescr?.align
@@ -12605,7 +12757,7 @@ var ScorecardChart = class extends Component {
12605
12757
  chartId: types$1.string(),
12606
12758
  isFullScreen: types$1.boolean().optional()
12607
12759
  });
12608
- canvas = signal(null);
12760
+ canvas = signal.ref(HTMLCanvasElement);
12609
12761
  zoomStore;
12610
12762
  get runtime() {
12611
12763
  return this.env.model.getters.getChartRuntime(this.props.chartId);
@@ -13704,33 +13856,8 @@ var ViewportsStore = class extends SpreadsheetStore {
13704
13856
  displayedSheetId = this.model.getters.getActiveSheetId();
13705
13857
  constructor(get) {
13706
13858
  super(get);
13707
- this.model.selection.observe(this, { handleEvent: this.handleEvent.bind(this) });
13708
- this.onDispose(() => {
13709
- this.model.selection.unobserve(this);
13710
- });
13711
13859
  this.viewports.resetViewports(this.displayedSheetId);
13712
13860
  }
13713
- handleEvent(event) {
13714
- const eventSheetId = this.getters.getActiveSheetId();
13715
- if (event.options.scrollIntoView) {
13716
- const oldZone = event.previousAnchor.zone;
13717
- const newZone = event.anchor.zone;
13718
- const isUpdateAnchorEvent = event.mode === "updateAnchor";
13719
- const sameZone = isEqual(oldZone, newZone);
13720
- let { col, row } = isUpdateAnchorEvent && sameZone ? event.anchor.cell : findCellInNewZone(oldZone, newZone);
13721
- if (isUpdateAnchorEvent && !sameZone) {
13722
- const { top, bottom, left, right } = this.viewports.getMainInternalViewport(eventSheetId);
13723
- if (oldZone.left === newZone.left && oldZone.right === newZone.right) col = left > col || col > right ? left : col;
13724
- if (oldZone.top === newZone.top && oldZone.bottom === newZone.bottom) row = top > row || row > bottom ? top : row;
13725
- }
13726
- col = Math.min(col, this.getters.getNumberCols(eventSheetId) - 1);
13727
- row = Math.min(row, this.getters.getNumberRows(eventSheetId) - 1);
13728
- if (!this.sheetsWithDirtyViewports.has(eventSheetId)) this.viewports.refreshViewport(eventSheetId, {
13729
- col,
13730
- row
13731
- });
13732
- }
13733
- }
13734
13861
  handle(cmd) {
13735
13862
  if (invalidateEvaluationCommands.has(cmd.type)) for (const sheetId of this.getters.getSheetIds()) this.sheetsWithDirtyViewports.add(sheetId);
13736
13863
  switch (cmd.type) {
@@ -13830,6 +13957,7 @@ var ViewportsStore = class extends SpreadsheetStore {
13830
13957
  this.shiftVertically(topRowDims.end - boundaryTopY - viewportHeight);
13831
13958
  }
13832
13959
  scrollToCell(sheetId, col, row) {
13960
+ if (this.sheetsWithDirtyViewports.has(sheetId)) return;
13833
13961
  this.viewports.refreshViewport(sheetId, {
13834
13962
  col,
13835
13963
  row
@@ -13990,6 +14118,44 @@ function getCarouselItemTitle(getters, item) {
13990
14118
  const definition = getters.getChartDefinition(item.chartId);
13991
14119
  return (chartSubtypeRegistry.getAll().find((c) => c.matcher?.(definition)) || chartSubtypeRegistry.get(definition.type)).displayName;
13992
14120
  }
14121
+ function getCarouselLayout(figureRect, carousel, selectedItem) {
14122
+ const layout = CAROUSEL_LAYOUT;
14123
+ const title = {
14124
+ ...DEFAULT_CAROUSEL_TITLE_STYLE,
14125
+ ...carousel.title
14126
+ };
14127
+ const x = figureRect.x + layout.paddingX;
14128
+ const width = Math.max(0, figureRect.width - 2 * layout.paddingX);
14129
+ const titleHeight = title.text ? title.fontSize * layout.headerLineHeight : 0;
14130
+ const headerRect = {
14131
+ x,
14132
+ y: figureRect.y + layout.paddingY + layout.headerPaddingTop,
14133
+ width,
14134
+ height: Math.max(layout.minHeaderHeight, titleHeight)
14135
+ };
14136
+ const headerBottom = headerRect.y + headerRect.height;
14137
+ const separatorRect = title.text ? {
14138
+ x,
14139
+ y: headerBottom + layout.paddingY,
14140
+ width,
14141
+ height: layout.separatorWidth
14142
+ } : void 0;
14143
+ const contentPaddingTop = selectedItem?.type === "carouselDataView" ? layout.paddingY : 0;
14144
+ const rectAboveContent = separatorRect || headerRect;
14145
+ const contentY = rectAboveContent.y + rectAboveContent.height + contentPaddingTop;
14146
+ const contentBottom = figureRect.y + figureRect.height - layout.paddingY;
14147
+ return {
14148
+ headerRect,
14149
+ separatorRect,
14150
+ contentRect: {
14151
+ x,
14152
+ y: contentY,
14153
+ width,
14154
+ height: Math.max(0, contentBottom - contentY)
14155
+ },
14156
+ headerLineHeight: layout.headerLineHeight
14157
+ };
14158
+ }
13993
14159
 
13994
14160
  //#endregion
13995
14161
  //#region src/helpers/figures/charts/gauge_chart_rendering.ts
@@ -14001,7 +14167,7 @@ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
14001
14167
  const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
14002
14168
  const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
14003
14169
  const GAUGE_TITLE_SECTION_HEIGHT = 25;
14004
- function drawGaugeChart(canvas, runtime, zoom = 1, dimensions) {
14170
+ function drawGaugeChart(canvas, runtime, zoom = 1, dimensions, options) {
14005
14171
  const size = dimensions ?? getCanvasSize(canvas);
14006
14172
  const dpr = typeof globalThis.devicePixelRatio === "number" ? globalThis.devicePixelRatio : 1;
14007
14173
  canvas.width = size.width * dpr;
@@ -14014,7 +14180,7 @@ function drawGaugeChart(canvas, runtime, zoom = 1, dimensions) {
14014
14180
  height: size.height / zoom,
14015
14181
  x: 0,
14016
14182
  y: 0
14017
- }, runtime, ctx);
14183
+ }, runtime, ctx, options);
14018
14184
  drawBackground(ctx, config);
14019
14185
  drawGauge(ctx, config);
14020
14186
  drawInflectionValues(ctx, config);
@@ -14102,7 +14268,8 @@ function drawTitle(ctx, config) {
14102
14268
  ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
14103
14269
  ctx.restore();
14104
14270
  }
14105
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
14271
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx, options) {
14272
+ const labelFontSize = options?.labelFontSize ?? 12;
14106
14273
  const maxValue = runtime.maxValue;
14107
14274
  const minValue = runtime.minValue;
14108
14275
  const gaugeValue = getGaugeValue(runtime, "animated");
@@ -14120,14 +14287,14 @@ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
14120
14287
  if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
14121
14288
  const minLabelPosition = {
14122
14289
  x: gaugeRect.x + gaugeArcWidth / 2,
14123
- y: gaugeRect.y + gaugeRect.height + 12
14290
+ y: gaugeRect.y + gaugeRect.height + labelFontSize
14124
14291
  };
14125
14292
  const maxLabelPosition = {
14126
14293
  x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
14127
- y: gaugeRect.y + gaugeRect.height + 12
14294
+ y: gaugeRect.y + gaugeRect.height + labelFontSize
14128
14295
  };
14129
14296
  const textColor = chartMutedFontColor(runtime.background);
14130
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
14297
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx, labelFontSize);
14131
14298
  let x = 0, titleWidth = 0, titleHeight = 0;
14132
14299
  if (runtime.title.text) ({width: titleWidth, height: titleHeight} = computeTextDimension(ctx, runtime.title.text, {
14133
14300
  fontSize: 16,
@@ -14175,13 +14342,13 @@ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
14175
14342
  minLabel: {
14176
14343
  label: runtime.minValue.label,
14177
14344
  textPosition: minLabelPosition,
14178
- fontSize: 12,
14345
+ fontSize: labelFontSize,
14179
14346
  color: textColor
14180
14347
  },
14181
14348
  maxLabel: {
14182
14349
  label: runtime.maxValue.label,
14183
14350
  textPosition: maxLabelPosition,
14184
- fontSize: 12,
14351
+ fontSize: labelFontSize,
14185
14352
  color: textColor
14186
14353
  }
14187
14354
  };
@@ -14215,27 +14382,27 @@ function getGaugeRect(boundingRect, title) {
14215
14382
  *
14216
14383
  * Also compute an offset for the text so that it doesn't overlap with other text.
14217
14384
  */
14218
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
14385
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx, labelFontSize = 12) {
14219
14386
  const maxValue = runtime.maxValue;
14220
14387
  const minValue = runtime.minValue;
14221
14388
  const gaugeCircleCenter = {
14222
14389
  x: gaugeRect.x + gaugeRect.width / 2,
14223
14390
  y: gaugeRect.y + gaugeRect.height
14224
14391
  };
14225
- const textStyle = { fontSize: 12 };
14392
+ const textStyle = { fontSize: labelFontSize };
14226
14393
  const inflectionValues = [];
14227
14394
  const inflectionValuesTextRects = [];
14228
14395
  for (const inflectionValue of runtime.inflectionValues) {
14229
14396
  const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
14230
14397
  const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
14231
14398
  const angle = Math.PI - Math.PI * percentage;
14232
- const textRect = getRectangleTangentToCircle(angle, gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, gaugeCircleCenter.x, gaugeCircleCenter.y, labelWidth + 2, 12);
14233
- const offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect)) ? 12 : 0;
14399
+ const textRect = getRectangleTangentToCircle(angle, gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, gaugeCircleCenter.x, gaugeCircleCenter.y, labelWidth + 2, labelFontSize);
14400
+ const offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect)) ? labelFontSize : 0;
14234
14401
  inflectionValuesTextRects.push(textRect);
14235
14402
  inflectionValues.push({
14236
14403
  rotation: angle,
14237
14404
  label: inflectionValue.label,
14238
- fontSize: 12,
14405
+ fontSize: labelFontSize,
14239
14406
  color: textColor,
14240
14407
  offset
14241
14408
  });
@@ -14355,6 +14522,44 @@ const CHART_COMMON_OPTIONS = {
14355
14522
  "mouseup"
14356
14523
  ]
14357
14524
  };
14525
+ /**
14526
+ * Sample down a Chart.js configuration's data points to at most `maxPoints`, evenly spaced.
14527
+ * Used for lightweight previews (drag preview, chart suggestions) where rendering every point
14528
+ * of a large dataset would make Chart.js slow without any visible benefit on a small canvas.
14529
+ */
14530
+ function limitChartConfigDataPoints(config, maxPoints = 150) {
14531
+ const data = config.data;
14532
+ if (!data) return config;
14533
+ return {
14534
+ ...config,
14535
+ data: {
14536
+ ...data,
14537
+ labels: limitArrayDataPoints(data.labels, maxPoints),
14538
+ datasets: data.datasets?.map((dataset) => limitDatasetDataPoints(dataset, maxPoints))
14539
+ }
14540
+ };
14541
+ }
14542
+ function limitDatasetDataPoints(dataset, maxPoints) {
14543
+ const values = dataset.data;
14544
+ if (!Array.isArray(values) || values.length <= maxPoints) return dataset;
14545
+ const length = values.length;
14546
+ const indices = sampleIndices(length, maxPoints);
14547
+ const limited = { ...dataset };
14548
+ for (const key of Object.keys(limited)) {
14549
+ const value = limited[key];
14550
+ if (Array.isArray(value) && value.length === length) limited[key] = indices.map((i) => value[i]);
14551
+ }
14552
+ return limited;
14553
+ }
14554
+ function limitArrayDataPoints(arr, maxPoints) {
14555
+ if (!Array.isArray(arr) || arr.length <= maxPoints) return arr;
14556
+ return sampleIndices(arr.length, maxPoints).map((i) => arr[i]);
14557
+ }
14558
+ /** Evenly spaced indices sampled from [0, length). */
14559
+ function sampleIndices(length, maxPoints) {
14560
+ const step = length / maxPoints;
14561
+ return Array.from({ length: maxPoints }, (_, i) => Math.min(length - 1, Math.floor(i * step)));
14562
+ }
14358
14563
  async function chartToImageUrl(runtime, figure, type) {
14359
14564
  try {
14360
14565
  const canvas = createRenderingSurface(figure.width, figure.height);
@@ -14402,7 +14607,7 @@ async function canvasToObjectUrl(canvas) {
14402
14607
  *
14403
14608
  * @returns a cleanup function to be called after the drawing is no longer needed (to free Chart.js resources)
14404
14609
  */
14405
- function drawChartOnCanvas(canvas, runtime, size, type) {
14610
+ function drawChartOnCanvas(canvas, runtime, size, type, zoom = 1) {
14406
14611
  if ("chartJsConfig" in runtime) {
14407
14612
  if (!globalThis.Chart) throw new Error("Chart.js library is not loaded");
14408
14613
  const extensionsLoaded = areChartJSExtensionsLoaded();
@@ -14418,8 +14623,8 @@ function drawChartOnCanvas(canvas, runtime, size, type) {
14418
14623
  chart.destroy();
14419
14624
  if (!extensionsLoaded) unregisterChartJsExtensions();
14420
14625
  };
14421
- } else if (type === "scorecard") drawScoreChart(getScorecardConfiguration(size, runtime), canvas);
14422
- else if (type === "gauge") drawGaugeChart(canvas, runtime, 1, size);
14626
+ } else if (type === "scorecard") drawScoreChart(getScorecardConfiguration(size, runtime), canvas, zoom);
14627
+ else if (type === "gauge") drawGaugeChart(canvas, runtime, zoom, size);
14423
14628
  return () => {};
14424
14629
  }
14425
14630
 
@@ -14957,7 +15162,7 @@ var ZoomableChartStore = class extends SpreadsheetStore {
14957
15162
  var ZoomableChartJsComponent = class extends ChartJsComponent {
14958
15163
  static template = "o-spreadsheet-ZoomableChartJsComponent";
14959
15164
  store;
14960
- masterChartCanvas = signal(null);
15165
+ masterChartCanvas = signal.ref(HTMLCanvasElement);
14961
15166
  masterChart;
14962
15167
  mode;
14963
15168
  hasLinearScale;
@@ -15711,7 +15916,7 @@ var GaugeChartComponent = class extends Component {
15711
15916
  chartId: types$1.string(),
15712
15917
  isFullScreen: types$1.boolean().optional()
15713
15918
  });
15714
- canvas = signal(null);
15919
+ canvas = signal.ref(HTMLCanvasElement);
15715
15920
  animationStore;
15716
15921
  zoomStore;
15717
15922
  get runtime() {
@@ -15948,7 +16153,7 @@ var Menu = class extends Component {
15948
16153
  onKeyDown: types$1.function().optional(),
15949
16154
  disableKeyboardNavigation: types$1.boolean().optional()
15950
16155
  });
15951
- menuRef = signal(null);
16156
+ menuRef = signal.ref();
15952
16157
  setup() {
15953
16158
  useLayoutEffect(() => {
15954
16159
  const menuEl = this.menuRef();
@@ -16068,8 +16273,8 @@ var Popover = class extends Component {
16068
16273
  zIndex: types$1.number().optional(),
16069
16274
  class: types$1.string().optional()
16070
16275
  });
16071
- popoverRef = signal(null);
16072
- popoverContentRef = signal(null);
16276
+ popoverRef = signal.ref();
16277
+ popoverContentRef = signal.ref();
16073
16278
  currentPosition = void 0;
16074
16279
  currentDisplayValue = void 0;
16075
16280
  spreadsheetRect = useSpreadsheetRect();
@@ -16257,7 +16462,7 @@ var MenuPopover = class MenuPopover extends Component {
16257
16462
  isHoveringChild: false
16258
16463
  });
16259
16464
  state = proxy({ hoveredMenu: this.props.autoSelectFirstItem ? this.getNextEnabledMenuItem() : void 0 });
16260
- menuRef = signal(null);
16465
+ menuRef = signal.ref();
16261
16466
  openingTimeOut = useTimeOut();
16262
16467
  setup() {
16263
16468
  const domFocusableElementStore = useStore(DOMFocusableElementStore);
@@ -16546,43 +16751,29 @@ var PositionMap = class {
16546
16751
  };
16547
16752
 
16548
16753
  //#endregion
16549
- //#region src/components/tables/hovered_table_store.ts
16550
- var HoveredTableStore = class extends SpreadsheetStore {
16551
- mutators = ["clear", "hover"];
16552
- position;
16754
+ //#region src/stores/cell_hover_overlay_store.ts
16755
+ var CellHoverOverlayStore = class extends SpreadsheetStore {
16756
+ mutators = [
16757
+ "hover",
16758
+ "register",
16759
+ "unRegister"
16760
+ ];
16761
+ storeGetters = ["getCellHoverOverlayColor"];
16762
+ providers = [];
16553
16763
  overlayColors = new PositionMap();
16554
- hover(position) {
16555
- if (!this.getters.isDashboard() || deepEquals(this.position, position)) return "noStateChange";
16556
- this.position = position;
16557
- this.computeOverlay();
16764
+ hover(hoveredPosition) {
16765
+ this.overlayColors = new PositionMap();
16766
+ if (!hoveredPosition) return;
16767
+ for (const provider of this.providers) {
16768
+ const positions = provider.getHighlightedPositions(hoveredPosition);
16769
+ for (const position of positions) this.overlayColors.set(position, TABLE_HOVER_BACKGROUND_COLOR);
16770
+ }
16558
16771
  }
16559
- clear() {
16560
- this.position = void 0;
16772
+ register(highlightProvider) {
16773
+ this.providers.push(highlightProvider);
16561
16774
  }
16562
- computeOverlay() {
16563
- this.overlayColors = new PositionMap();
16564
- if (!this.position) return;
16565
- const { sheetId, col, row } = this.position;
16566
- const table = this.getters.getTable({
16567
- sheetId,
16568
- col,
16569
- row
16570
- });
16571
- if (!table) return;
16572
- const { left, right, top } = table.range.zone;
16573
- const isTableHeader = row < top + table.config.numberOfHeaders;
16574
- const doesTableRowHaveContent = range(left, right + 1).some((col) => {
16575
- return !this.getters.isColHidden(sheetId, col) && this.getters.getEvaluatedCell({
16576
- sheetId,
16577
- col,
16578
- row
16579
- }).formattedValue;
16580
- });
16581
- if (!isTableHeader && doesTableRowHaveContent) for (let col = left; col <= right; col++) this.overlayColors.set({
16582
- sheetId,
16583
- col,
16584
- row
16585
- }, TABLE_HOVER_BACKGROUND_COLOR);
16775
+ unRegister(highlightProvider) {
16776
+ this.providers = this.providers.filter((h) => h !== highlightProvider);
16586
16777
  }
16587
16778
  };
16588
16779
 
@@ -16594,9 +16785,9 @@ var ClickableCellSortIcon = class extends Component {
16594
16785
  position: types$1.CellPosition(),
16595
16786
  sortDirection: types$1.or([types$1.SortDirection, types$1.literal("none")])
16596
16787
  });
16597
- hoveredTableStore;
16788
+ hoveredCellOverlayStore;
16598
16789
  setup() {
16599
- this.hoveredTableStore = useStore(HoveredTableStore);
16790
+ this.hoveredCellOverlayStore = useStore(CellHoverOverlayStore);
16600
16791
  }
16601
16792
  get style() {
16602
16793
  const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.position);
@@ -16616,7 +16807,7 @@ var ClickableCellSortIcon = class extends Component {
16616
16807
  }
16617
16808
  }
16618
16809
  getBackgroundColor(cellStyle) {
16619
- const overlayColor = this.hoveredTableStore.overlayColors.get(this.props.position);
16810
+ const overlayColor = this.hoveredCellOverlayStore.overlayColors.get(this.props.position);
16620
16811
  if (overlayColor) return blendColors(cellStyle.fillColor || "#FFFFFF", overlayColor);
16621
16812
  return cellStyle.fillColor || "#FFFFFF";
16622
16813
  }
@@ -16859,110 +17050,6 @@ function isSquareMatrix(arg) {
16859
17050
  }
16860
17051
  const expectNumberGreaterThanOrEqualToOne = (value) => _t("The function [[FUNCTION_NAME]] expects a number value to be greater than or equal to 1, but receives %s.", value);
16861
17052
 
16862
- //#endregion
16863
- //#region src/functions/helper_matrices.ts
16864
- function getUnitMatrix(n) {
16865
- const matrix = Array(n);
16866
- for (let i = 0; i < n; i++) {
16867
- matrix[i] = Array(n).fill(0);
16868
- matrix[i][i] = 1;
16869
- }
16870
- return matrix;
16871
- }
16872
- /**
16873
- * Invert a matrix and compute its determinant using Gaussian Elimination.
16874
- *
16875
- * The Matrix should be a square matrix, and should be indexed [col][row] instead of the
16876
- * standard mathematical indexing [row][col].
16877
- */
16878
- function invertMatrix(M) {
16879
- if (M.length < 1 || M[0].length < 1) throw new Error("invertMatrix: an empty matrix cannot be inverted.");
16880
- if (M.length !== M[0].length) throw new Error("invertMatrix: only square matrices are invertible");
16881
- let determinant = 1;
16882
- const dim = M.length;
16883
- const I = getUnitMatrix(dim);
16884
- const C = M.map((row) => row.slice());
16885
- for (let pivot = 0; pivot < dim; pivot++) {
16886
- let diagonalElement = C[pivot][pivot];
16887
- if (diagonalElement === 0) {
16888
- for (let row = pivot + 1; row < dim; row++) if (C[pivot][row] !== 0) {
16889
- swapMatrixRows(C, pivot, row);
16890
- swapMatrixRows(I, pivot, row);
16891
- determinant *= -1;
16892
- break;
16893
- }
16894
- diagonalElement = C[pivot][pivot];
16895
- if (diagonalElement === 0) return { determinant: 0 };
16896
- }
16897
- for (let col = 0; col < dim; col++) {
16898
- C[col][pivot] = C[col][pivot] / diagonalElement;
16899
- I[col][pivot] = I[col][pivot] / diagonalElement;
16900
- }
16901
- determinant *= diagonalElement;
16902
- for (let row = 0; row < dim; row++) {
16903
- if (row === pivot) continue;
16904
- const e = C[pivot][row];
16905
- for (let col = 0; col < dim; col++) {
16906
- C[col][row] -= e * C[col][pivot];
16907
- I[col][row] -= e * I[col][pivot];
16908
- }
16909
- }
16910
- }
16911
- return {
16912
- inverted: I,
16913
- determinant
16914
- };
16915
- }
16916
- function swapMatrixRows(matrix, row1, row2) {
16917
- for (let i = 0; i < matrix.length; i++) {
16918
- const tmp = matrix[i][row1];
16919
- matrix[i][row1] = matrix[i][row2];
16920
- matrix[i][row2] = tmp;
16921
- }
16922
- }
16923
- /**
16924
- * Matrix multiplication of 2 matrices.
16925
- * ex: matrix1 : n x l, matrix2 : m x n => result : m x l
16926
- *
16927
- * Note: we use indexing [col][row] instead of the standard mathematical notation [row][col]
16928
- */
16929
- function multiplyMatrices(matrix1, matrix2) {
16930
- if (matrix1.length < 1 || matrix2.length < 1) throw new Error("multiplyMatrices: empty matrices cannot be multiplied.");
16931
- if (matrix1.length !== matrix2[0].length) throw new Error("multiplyMatrices: incompatible matrices size.");
16932
- const rowsM1 = matrix1[0].length;
16933
- const colsM2 = matrix2.length;
16934
- const n = matrix1.length;
16935
- const result = Array(colsM2);
16936
- for (let col = 0; col < colsM2; col++) {
16937
- result[col] = Array(rowsM1);
16938
- for (let row = 0; row < rowsM1; row++) {
16939
- let sum = 0;
16940
- for (let k = 0; k < n; k++) sum += matrix1[k][row] * matrix2[col][k];
16941
- result[col][row] = sum;
16942
- }
16943
- }
16944
- return result;
16945
- }
16946
- /**
16947
- * Return the input if it's a scalar or the first element of the input if it's a matrix.
16948
- */
16949
- function toScalar(arg) {
16950
- if (!isMatrix(arg)) return arg;
16951
- if (!isSingleElementMatrix(arg)) throw new Error("The value should be a scalar or a 1x1 matrix");
16952
- return arg[0][0];
16953
- }
16954
- function isSingleElementMatrix(matrix) {
16955
- return matrix.length === 1 && matrix[0].length === 1;
16956
- }
16957
- function isMultipleElementMatrix(arg) {
16958
- return isMatrix(arg) && !isSingleElementMatrix(arg);
16959
- }
16960
- function getMatrixArgIndices(args) {
16961
- const indices = [];
16962
- for (let i = 0; i < args.length; i++) if (isMultipleElementMatrix(args[i])) indices.push(i);
16963
- return indices;
16964
- }
16965
-
16966
17053
  //#endregion
16967
17054
  //#region src/functions/helper_statistical.ts
16968
17055
  function assertSameNumberOfElements(...args) {
@@ -18113,8 +18200,6 @@ const ChartTerms = {
18113
18200
  Unexpected: _t("The chart definition is invalid for an unknown reason"),
18114
18201
  ["InvalidDataSet"]: _t("The dataset is invalid"),
18115
18202
  ["InvalidLabelRange"]: _t("Labels are invalid"),
18116
- ["InvalidScorecardKeyValue"]: _t("The key value is invalid"),
18117
- ["InvalidScorecardBaseline"]: _t("The baseline value is invalid"),
18118
18203
  ["InvalidGaugeDataRange"]: _t("The data range is invalid"),
18119
18204
  ["EmptyGaugeRangeMin"]: _t("A minimum range limit value is needed"),
18120
18205
  ["GaugeRangeMinNaN"]: _t("The minimum range limit value must be a number"),
@@ -21355,7 +21440,7 @@ var HoveredIconStore = class extends SpreadsheetStore {
21355
21440
  //#region src/components/grid_overlay/grid_overlay.ts
21356
21441
  function useCellHovered(env, gridRef) {
21357
21442
  const delayedHoveredCell = useStore(DelayedHoveredCellStore);
21358
- const hoveredTable = useStore(HoveredTableStore);
21443
+ const cellHoverOverlay = useStore(CellHoverOverlayStore);
21359
21444
  const viewStore = useStore(ViewportsStore);
21360
21445
  const hoveredPosition = {
21361
21446
  col: void 0,
@@ -21399,7 +21484,9 @@ function useCellHovered(env, gridRef) {
21399
21484
  if (isChildEvent(gridRef(), zoomedMouseEvent.ev)) {
21400
21485
  ({x, y} = getOffsetRelativeToOverlay(zoomedMouseEvent));
21401
21486
  lastMoved = Date.now();
21402
- hoveredTable.hover(getPosition());
21487
+ const position = getPosition();
21488
+ if (position.col < 0 || position.row < 0) cellHoverOverlay.hover(void 0);
21489
+ else cellHoverOverlay.hover(getPosition());
21403
21490
  }
21404
21491
  }
21405
21492
  function recompute() {
@@ -21410,7 +21497,7 @@ function useCellHovered(env, gridRef) {
21410
21497
  const zoomedMouseEvent = withZoom(env, e);
21411
21498
  const { x, y } = getOffsetRelativeToOverlay(zoomedMouseEvent);
21412
21499
  const gridRect = getElBoundingRect(gridRef());
21413
- if (y < 0 || y > gridRect.height || x < 0 || x > gridRect.width) return updateMousePosition(zoomedMouseEvent);
21500
+ if (y <= 0 || y >= gridRect.height || x <= 0 || x >= gridRect.width) return updateMousePosition(zoomedMouseEvent);
21414
21501
  else return pause();
21415
21502
  }
21416
21503
  useListener(gridRef, "pointermove", (ev) => !env.isMobile() && updateMousePosition(withZoom(env, ev)));
@@ -21447,7 +21534,7 @@ var GridOverlay = class extends Component {
21447
21534
  gridOverlayDimensions: types$1.string(),
21448
21535
  hasFooter: types$1.boolean().optional(() => true)
21449
21536
  });
21450
- gridOverlayRef = signal(null);
21537
+ gridOverlayRef = signal.ref();
21451
21538
  cellPopovers;
21452
21539
  paintFormatStore;
21453
21540
  hoveredIconStore;
@@ -21474,7 +21561,7 @@ var GridOverlay = class extends Component {
21474
21561
  return el;
21475
21562
  }
21476
21563
  get style() {
21477
- return this.props.gridOverlayDimensions + cssPropertiesToCss({ cursor: this.hoveredIconStore.hoveredIcon ? "pointer" : "default" });
21564
+ return this.props.gridOverlayDimensions + cssPropertiesToCss({ cursor: this.hoveredIconStore.hoveredIcon ? "pointer" : "inherit" });
21478
21565
  }
21479
21566
  get isPaintingFormat() {
21480
21567
  return this.paintFormatStore.isActive;
@@ -21789,7 +21876,7 @@ function getPivotIconSvg(isCollapsed, isHovered) {
21789
21876
  };
21790
21877
  }
21791
21878
  function getDataFilterIcon(isActive, isHighContrast, isHovered) {
21792
- const symbolPath = isActive ? "M18.6 3.5H4.29c-.7 0-1.06.85-.56 1.35l6.1 6.1v6.8c0 .26.13.5.34.65l2.64 1.85a.79.79 0 0 0 1.25-.65v-8.64l6.1-6.1a.79.79 0 0 0-.56-1.35" : "M 339.667 681 L 510.333 681 L 510.333 595.667 L 339.667 595.667 L 339.667 681 Z M 41 169 L 41 254.333 L 809 254.333 L 809 169 L 41 169 Z M 169 467.667 L 681 467.667 L 681 382.333 L 169 382.333 L 169 467.667 Z";
21879
+ const symbolPath = isActive ? "M 11 20 Q 10.575 20 10.2875 19.7125 T 10 19 L 10 13 L 4.2 5.6 Q 3.825 5.1 4.0875 4.55 T 5 4 L 19 4 Q 19.65 4 19.9125 4.55 T 19.8 5.6 L 14 13 L 14 19 Q 14 19.425 13.7125 19.7125 T 13 20 L 11 20 Z" : "M 339.667 681 L 510.333 681 L 510.333 595.667 L 339.667 595.667 L 339.667 681 Z M 41 169 L 41 254.333 L 809 254.333 L 809 169 L 41 169 Z M 169 467.667 L 681 467.667 L 681 382.333 L 169 382.333 L 169 467.667 Z";
21793
21880
  const hoverBackgroundPath = isActive ? "M0,0 h24 v24 h-24" : "M0,0 h850 v850 h-850";
21794
21881
  const colors = {
21795
21882
  iconColor: FILTERS_COLOR,
@@ -22114,8 +22201,9 @@ var FormulaFingerprintStore = class extends SpreadsheetStore {
22114
22201
  const CELL_ANIMATION_DURATION = 200;
22115
22202
  var GridRenderer = class extends DisposableStore {
22116
22203
  renderer;
22204
+ storeGetters = ["drawLayer"];
22117
22205
  fingerprints;
22118
- hoveredTables;
22206
+ cellHoverOverlayStore = this.get(CellHoverOverlayStore);
22119
22207
  hoveredIcon;
22120
22208
  lastRenderSheetId = void 0;
22121
22209
  lastRenderBoxes = /* @__PURE__ */ new Map();
@@ -22129,15 +22217,14 @@ var GridRenderer = class extends DisposableStore {
22129
22217
  const model = get(ModelStore);
22130
22218
  this.getters = model.getters;
22131
22219
  this.fingerprints = get(FormulaFingerprintStore);
22132
- this.hoveredTables = get(HoveredTableStore);
22133
22220
  this.hoveredIcon = get(HoveredIconStore);
22134
22221
  model.on("command-dispatched", this, this.handle);
22135
22222
  model.on("command-finalized", this, this.finalize);
22136
- this.renderer.register(this);
22223
+ if (this.renderer) this.renderer.register(this);
22137
22224
  this.onDispose(() => {
22138
22225
  model.off("command-dispatched", this);
22139
22226
  model.off("command-finalized", this);
22140
- this.renderer.unRegister(this);
22227
+ if (this.renderer) this.renderer.unRegister(this);
22141
22228
  });
22142
22229
  }
22143
22230
  handle(cmd) {
@@ -22681,7 +22768,7 @@ var GridRenderer = class extends DisposableStore {
22681
22768
  border: this.getters.getCellComputedBorder(position) || void 0,
22682
22769
  style,
22683
22770
  dataBarFill,
22684
- overlayColor: this.hoveredTables.overlayColors.get(position),
22771
+ overlayColor: this.cellHoverOverlayStore.overlayColors.get(position),
22685
22772
  isError: cell.type === "error" && !!cell.message || this.getters.isDataValidationInvalid(position),
22686
22773
  icons: cellIcons,
22687
22774
  disabledAnimation: this.zonesWithPreventedAnimationsInNextFrame[sheetId]?.some((z) => isZoneInside(zone, z) || overlap(zone, z))
@@ -22857,6 +22944,7 @@ var GridRenderer = class extends DisposableStore {
22857
22944
  return boxes;
22858
22945
  }
22859
22946
  getBoxesWithAnimations(boxes, oldBoxes, timeStamp) {
22947
+ if (!this.renderer) return boxes;
22860
22948
  this.updateAnimationsProgress(timeStamp);
22861
22949
  this.addNewAnimations(boxes, oldBoxes, timeStamp);
22862
22950
  if (this.animations.size > 0) {
@@ -23001,7 +23089,7 @@ var ScrollBar = class extends Component {
23001
23089
  offset: types$1.Pixel(),
23002
23090
  onScroll: types$1.function()
23003
23091
  });
23004
- scrollbarRef = signal(null);
23092
+ scrollbarRef = signal.ref();
23005
23093
  scrollbar;
23006
23094
  setup() {
23007
23095
  this.scrollbar = new ScrollBar$1(this.scrollbarRef(), this.props.direction);
@@ -23103,6 +23191,9 @@ var StandaloneViewportStore = class extends SpreadsheetStore {
23103
23191
  zoneToDisplay: this.range.zone,
23104
23192
  getFooterSize: () => 0
23105
23193
  });
23194
+ const cellHoverOverlayStore = this.get(CellHoverOverlayStore);
23195
+ cellHoverOverlayStore.register(this);
23196
+ this.onDispose(() => cellHoverOverlayStore.unRegister(this));
23106
23197
  }
23107
23198
  handle(cmd) {
23108
23199
  this.cachedColDimensions = void 0;
@@ -23251,6 +23342,16 @@ var StandaloneViewportStore = class extends SpreadsheetStore {
23251
23342
  }
23252
23343
  return normalizedWeights;
23253
23344
  }
23345
+ getHighlightedPositions(position) {
23346
+ const highlightedPositions = [];
23347
+ const { col, row, sheetId } = position;
23348
+ if (sheetId !== this.range.sheetId || !isInside(col, row, this.range.zone)) return highlightedPositions;
23349
+ for (let col = this.range.zone.left; col <= this.range.zone.right; col++) highlightedPositions.push({
23350
+ ...position,
23351
+ col
23352
+ });
23353
+ return highlightedPositions;
23354
+ }
23254
23355
  };
23255
23356
 
23256
23357
  //#endregion
@@ -23267,10 +23368,11 @@ var StandaloneViewport = class extends Component {
23267
23368
  range: types$1.Range(),
23268
23369
  canResizeColumns: types$1.boolean().optional(true),
23269
23370
  onResizeColumns: types$1.function().optional(),
23270
- columnWeights: types$1.array().optional()
23371
+ columnWeights: types$1.array().optional(),
23372
+ size: types$1.object()
23271
23373
  });
23272
- canvasRef = signal(null);
23273
- containerRef = signal(null);
23374
+ canvasRef = signal.ref(HTMLCanvasElement);
23375
+ containerRef = signal.ref();
23274
23376
  store;
23275
23377
  dndState = proxy({ col: void 0 });
23276
23378
  onMouseWheel;
@@ -23282,8 +23384,8 @@ var StandaloneViewport = class extends Component {
23282
23384
  useChildStoreProvider([
23283
23385
  ViewportsStore,
23284
23386
  HoveredIconStore,
23285
- HoveredTableStore,
23286
23387
  ClickableCellsStore,
23388
+ CellHoverOverlayStore,
23287
23389
  DelayedHoveredCellStore,
23288
23390
  CellPopoverStore
23289
23391
  ]);
@@ -23292,18 +23394,10 @@ var StandaloneViewport = class extends Component {
23292
23394
  this.zoomStore = useStore(ZoomStore);
23293
23395
  this.cellPopoverStore = useStore(CellPopoverStore);
23294
23396
  this.rendererStore = useLocalStore(RendererStore, ["Background", "Chart"]);
23295
- const resizeObserver = new ResizeObserver(() => {
23296
- this.store.setContainerSize(this.containerWidth, this.containerHeight);
23297
- });
23298
- useEffect(() => {
23299
- const el = this.containerRef();
23300
- if (el) resizeObserver.observe(el);
23301
- return () => resizeObserver.disconnect();
23302
- });
23303
23397
  useEffect(() => {
23304
23398
  this.store.setRange(this.props.range);
23305
23399
  if (this.dndState.col === void 0) this.store.setCustomColWeights(this.props.columnWeights);
23306
- this.store.setContainerSize(this.containerWidth, this.containerHeight);
23400
+ this.store.setContainerSize(this.contentWidth, this.props.size.height);
23307
23401
  });
23308
23402
  useGridDrawing({
23309
23403
  canvasRef: this.canvasRef,
@@ -23322,14 +23416,11 @@ var StandaloneViewport = class extends Component {
23322
23416
  }
23323
23417
  });
23324
23418
  }
23325
- get containerWidth() {
23326
- return Math.floor(getElBoundingRect(this.containerRef()).width / this.zoomStore.zoomLevel);
23327
- }
23328
- get containerHeight() {
23329
- return Math.floor(getElBoundingRect(this.containerRef()).height / this.zoomStore.zoomLevel);
23419
+ get contentWidth() {
23420
+ return this.hasVerticalScrollBar ? this.props.size.width - this.zoomStore.scrollBarWidth : this.props.size.width;
23330
23421
  }
23331
23422
  get hasVerticalScrollBar() {
23332
- return this.containerHeight && this.viewStore.mainViewportCoordinates.y + this.viewStore.mainViewportRect.height > this.containerHeight;
23423
+ return this.viewStore.mainViewportCoordinates.y + this.viewStore.mainViewportRect.height > this.props.size.height;
23333
23424
  }
23334
23425
  get scrollBarContainerStyle() {
23335
23426
  return cssPropertiesToCss({ width: `${this.zoomStore.scrollBarWidth}px` });
@@ -23381,7 +23472,7 @@ var StandaloneViewport = class extends Component {
23381
23472
  };
23382
23473
  const onMouseMove = (ev) => {
23383
23474
  deltaX = withZoom(this.env, ev).clientX - initialX;
23384
- const weightDelta = deltaX / this.containerWidth * totalWeight;
23475
+ const weightDelta = deltaX / this.props.size.width * totalWeight;
23385
23476
  this.store.resizeColumn(resizer.col, weightDelta, startingColWeights);
23386
23477
  };
23387
23478
  startDnd(onMouseMove, onMouseUp);
@@ -23411,8 +23502,8 @@ var Select = class extends Component {
23411
23502
  name: types$1.string().optional(),
23412
23503
  title: types$1.string().optional()
23413
23504
  });
23414
- selectRef = signal(null);
23415
- dropdownRef = signal(null);
23505
+ selectRef = signal.ref();
23506
+ dropdownRef = signal.ref();
23416
23507
  state = proxy({
23417
23508
  isPopoverOpen: false,
23418
23509
  hoveredValue: void 0
@@ -23522,7 +23613,7 @@ var InfoPopover = class extends Component {
23522
23613
  setup() {
23523
23614
  useExternalListener(window, "click", this.onExternalClick, { capture: true });
23524
23615
  }
23525
- infoRef = signal(null);
23616
+ infoRef = signal.ref();
23526
23617
  get popoverProps() {
23527
23618
  return {
23528
23619
  anchorRect: this.props.anchorRect,
@@ -23667,8 +23758,8 @@ var CarouselFigure = class extends Component {
23667
23758
  isFullScreen: types$1.boolean().optional(),
23668
23759
  openContextMenu: types$1.function().optional()
23669
23760
  });
23670
- carouselTabsRef = signal(null);
23671
- carouselTabsDropdownRef = signal(null);
23761
+ carouselTabsRef = signal.ref();
23762
+ carouselTabsDropdownRef = signal.ref();
23672
23763
  menuState = proxy({
23673
23764
  isOpen: false,
23674
23765
  anchorRect: null,
@@ -23736,6 +23827,19 @@ var CarouselFigure = class extends Component {
23736
23827
  } else cssProperties["background-color"] = backgroundColor;
23737
23828
  return cssPropertiesToCss(cssProperties);
23738
23829
  }
23830
+ get contentStyle() {
23831
+ return cssPropertiesToCss(this.rectToCss(this.carouselLayout.contentRect));
23832
+ }
23833
+ get headerStyle() {
23834
+ return cssPropertiesToCss({
23835
+ ...this.rectToCss(this.carouselLayout.headerRect),
23836
+ "line-height": String(this.carouselLayout.headerLineHeight)
23837
+ });
23838
+ }
23839
+ get separatorStyle() {
23840
+ const separatorRect = this.carouselLayout.separatorRect;
23841
+ return separatorRect ? cssPropertiesToCss(this.rectToCss(separatorRect)) : "";
23842
+ }
23739
23843
  get title() {
23740
23844
  return this.env.model.getters.dynamicTranslate(this.carousel.title?.text ?? "");
23741
23845
  }
@@ -23745,6 +23849,14 @@ var CarouselFigure = class extends Component {
23745
23849
  ...this.carousel.title
23746
23850
  })));
23747
23851
  }
23852
+ rectToCss(rect) {
23853
+ return {
23854
+ top: `${rect.y}px`,
23855
+ left: `${rect.x}px`,
23856
+ width: `${rect.width}px`,
23857
+ height: `${rect.height}px`
23858
+ };
23859
+ }
23748
23860
  updateTabsVisibility() {
23749
23861
  const tabsContainerEl = this.carouselTabsRef();
23750
23862
  const dropDownEl = this.carouselTabsDropdownRef();
@@ -23836,6 +23948,13 @@ var CarouselFigure = class extends Component {
23836
23948
  get canResizeDataViewColumns() {
23837
23949
  return !this.env.model.getters.isReadonly() && !this.env.model.getters.isSheetLocked(this.env.model.getters.getActiveSheetId());
23838
23950
  }
23951
+ get carouselLayout() {
23952
+ return getCarouselLayout({
23953
+ ...this.props.figureUI,
23954
+ x: 0,
23955
+ y: 0
23956
+ }, this.carousel, this.selectedCarouselItem);
23957
+ }
23839
23958
  };
23840
23959
 
23841
23960
  //#endregion
@@ -23876,7 +23995,7 @@ var ImageFigure = class extends Component {
23876
23995
  figureUI: types$1.FigureUI(),
23877
23996
  openContextMenu: types$1.function()
23878
23997
  });
23879
- menuButtonRef = signal(null);
23998
+ menuButtonRef = signal.ref();
23880
23999
  showMenu(ev) {
23881
24000
  if (!this.env.model.getters.getSelectedFigureIds().includes(this.props.figureUI.id)) this.env.model.dispatch("SELECT_FIGURE", {
23882
24001
  figureId: this.props.figureUI.id,
@@ -23904,7 +24023,8 @@ figureRegistry.add("chart", {
23904
24023
  menuBuilder: getChartMenuActions,
23905
24024
  borderWidth: (getters) => getters.isDashboard() ? 0 : 1,
23906
24025
  hasShadow: (getters) => getters.isDashboard(),
23907
- isRounded: (getters) => getters.isDashboard()
24026
+ isRounded: (getters) => getters.isDashboard(),
24027
+ isThemeDependant: true
23908
24028
  });
23909
24029
  figureRegistry.add("image", {
23910
24030
  Component: ImageFigure,
@@ -23913,14 +24033,16 @@ figureRegistry.add("image", {
23913
24033
  borderWidth: () => 0,
23914
24034
  hasShadow: () => false,
23915
24035
  isRounded: () => false,
23916
- menuBuilder: getImageMenuActions
24036
+ menuBuilder: getImageMenuActions,
24037
+ isThemeDependant: false
23917
24038
  });
23918
24039
  figureRegistry.add("carousel", {
23919
24040
  Component: CarouselFigure,
23920
24041
  menuBuilder: getCarouselMenuActions,
23921
24042
  borderWidth: (getters) => getters.isDashboard() ? 0 : 1,
23922
24043
  hasShadow: (getters) => getters.isDashboard(),
23923
- isRounded: (getters) => getters.isDashboard()
24044
+ isRounded: (getters) => getters.isDashboard(),
24045
+ isThemeDependant: true
23924
24046
  });
23925
24047
 
23926
24048
  //#endregion
@@ -23942,10 +24064,13 @@ var FigureComponent = class extends Component {
23942
24064
  anchorRect: null,
23943
24065
  menuItems: []
23944
24066
  });
23945
- figureRef = signal(null);
24067
+ figureRef = signal.ref();
23946
24068
  get isSelected() {
23947
24069
  return !this.env.model.getters.isDashboard() && this.env.model.getters.getSelectedFigureIds().includes(this.props.figureUI.id);
23948
24070
  }
24071
+ get isThemeDependant() {
24072
+ return figureRegistry.get(this.props.figureUI.tag).isThemeDependant;
24073
+ }
23949
24074
  get figureRegistry() {
23950
24075
  return figureRegistry;
23951
24076
  }
@@ -29144,6 +29269,20 @@ migrationStepRegistry.add("0.1", { migrate(data) {
29144
29269
  figure.data.chartDefinitions[chartId] = upgrade(definition);
29145
29270
  }
29146
29271
  return data;
29272
+ } }).add("19.5.1", { migrate(data) {
29273
+ function upgrade(definition) {
29274
+ if (definition.type !== "scorecard") return definition;
29275
+ definition = { ...definition };
29276
+ if (definition.keyValue) definition.keyValue = `=${definition.keyValue}`;
29277
+ if (definition.baseline) definition.baseline = `=${definition.baseline}`;
29278
+ return definition;
29279
+ }
29280
+ for (const sheet of data.sheets || []) for (const figure of sheet.figures || []) if (figure.tag === "chart") figure.data = upgrade(figure.data);
29281
+ else if (figure.tag === "carousel") for (const chartId in figure.data.chartDefinitions) {
29282
+ const definition = figure.data.chartDefinitions[chartId];
29283
+ figure.data.chartDefinitions[chartId] = upgrade(definition);
29284
+ }
29285
+ return data;
29147
29286
  } });
29148
29287
  function fixOverlappingFilters(data) {
29149
29288
  for (const sheet of data.sheets || []) {
@@ -29544,6 +29683,35 @@ async function interactivePasteFromOS(env, target, parsedClipboardContent, paste
29544
29683
  }));
29545
29684
  }
29546
29685
 
29686
+ //#endregion
29687
+ //#region src/helpers/figures/charts/chart_data_source_helpers.ts
29688
+ function getUnboundRange(getters, zone) {
29689
+ return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
29690
+ }
29691
+ function isDatasetTitled(getters, zone) {
29692
+ const sheetId = getters.getActiveSheetId();
29693
+ const cell = getters.getEvaluatedCell({
29694
+ sheetId,
29695
+ col: zone.left,
29696
+ row: zone.top
29697
+ });
29698
+ return !["number", "empty"].includes(cell.type);
29699
+ }
29700
+ function dataset(zone, getters, id = "0") {
29701
+ return {
29702
+ dataRange: getUnboundRange(getters, zone),
29703
+ dataSetId: id
29704
+ };
29705
+ }
29706
+ function rangeSource(dataSets, dataSetsHaveTitle, labelRange) {
29707
+ return {
29708
+ type: "range",
29709
+ dataSets,
29710
+ dataSetsHaveTitle,
29711
+ labelRange
29712
+ };
29713
+ }
29714
+
29547
29715
  //#endregion
29548
29716
  //#region src/helpers/figures/charts/smart_chart_engine.ts
29549
29717
  const DEFAULT_BAR_CHART_CONFIG = {
@@ -29574,9 +29742,6 @@ const DEFAULT_LINE_CHART_CONFIG = {
29574
29742
  labelsAsText: false,
29575
29743
  humanize: true
29576
29744
  };
29577
- function getUnboundRange(getters, zone) {
29578
- return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
29579
- }
29580
29745
  function detectColumnType(cells) {
29581
29746
  if (!cells.length) return "empty";
29582
29747
  const counts = {
@@ -29623,14 +29788,6 @@ function getCellStats(getters, zone) {
29623
29788
  totalCount: values.length
29624
29789
  };
29625
29790
  }
29626
- function isDatasetTitled(getters, column) {
29627
- const titleCell = getters.getEvaluatedCell({
29628
- sheetId: getters.getActiveSheetId(),
29629
- col: column.zone.left,
29630
- row: column.zone.top
29631
- });
29632
- return !["number", "empty"].includes(titleCell.type);
29633
- }
29634
29791
  /**
29635
29792
  * Builds a chart definition for a single column selection. The logic to detect the chart type is as follows:
29636
29793
  * - If the column contains a single cell, create a scorecard.
@@ -29642,8 +29799,7 @@ function isDatasetTitled(getters, column) {
29642
29799
  function buildSingleColumnChart(column, getters) {
29643
29800
  const { type, zone } = column;
29644
29801
  const sheetId = getters.getActiveSheetId();
29645
- const dataSetsHaveTitle = isDatasetTitled(getters, column);
29646
- const dataRange = getUnboundRange(getters, zone);
29802
+ const dataSetsHaveTitle = isDatasetTitled(getters, column.zone);
29647
29803
  const titleCell = getters.getEvaluatedCell({
29648
29804
  sheetId,
29649
29805
  col: zone.left,
@@ -29654,14 +29810,7 @@ function buildSingleColumnChart(column, getters) {
29654
29810
  case "percentage": return {
29655
29811
  type: "pie",
29656
29812
  title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29657
- dataSource: {
29658
- type: "range",
29659
- dataSets: [{
29660
- dataRange,
29661
- dataSetId: "0"
29662
- }],
29663
- dataSetsHaveTitle
29664
- },
29813
+ dataSource: rangeSource([dataset(zone, getters)], dataSetsHaveTitle),
29665
29814
  dataSetStyles: {},
29666
29815
  legendPosition: "none"
29667
29816
  };
@@ -29672,45 +29821,28 @@ function buildSingleColumnChart(column, getters) {
29672
29821
  return {
29673
29822
  type: "pie",
29674
29823
  title: hasUniqueTitle ? { text: String(titleCell.value) } : {},
29675
- dataSource: {
29676
- type: "range",
29677
- dataSets: [{
29678
- dataRange,
29679
- dataSetId: "0"
29680
- }],
29681
- labelRange: dataRange,
29682
- dataSetsHaveTitle: hasUniqueTitle
29683
- },
29824
+ dataSource: rangeSource([dataset(zone, getters)], hasUniqueTitle, getUnboundRange(getters, zone)),
29684
29825
  dataSetStyles: {},
29685
29826
  aggregated: true,
29686
29827
  legendPosition: "top"
29687
29828
  };
29688
- case "date": return {
29689
- type: "calendar",
29690
- title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29691
- dataSource: {
29692
- type: "range",
29693
- dataSets: [],
29694
- dataSetsHaveTitle,
29695
- labelRange: dataRange
29696
- },
29697
- dataSetStyles: {},
29698
- legendPosition: "left",
29699
- horizontalGroupBy: "day_of_week",
29700
- verticalGroupBy: "month_number"
29701
- };
29829
+ case "date": {
29830
+ const dataRange = getUnboundRange(getters, zone);
29831
+ return {
29832
+ type: "calendar",
29833
+ title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29834
+ dataSource: rangeSource([], dataSetsHaveTitle, dataRange),
29835
+ dataSetStyles: {},
29836
+ legendPosition: "left",
29837
+ horizontalGroupBy: "day_of_week",
29838
+ verticalGroupBy: "month_number"
29839
+ };
29840
+ }
29702
29841
  }
29703
29842
  return {
29704
29843
  ...DEFAULT_BAR_CHART_CONFIG,
29705
29844
  title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29706
- dataSource: {
29707
- type: "range",
29708
- dataSets: [{
29709
- dataRange,
29710
- dataSetId: "0"
29711
- }],
29712
- dataSetsHaveTitle
29713
- },
29845
+ dataSource: rangeSource([dataset(zone, getters)], dataSetsHaveTitle),
29714
29846
  dataSetStyles: {}
29715
29847
  };
29716
29848
  }
@@ -29728,15 +29860,7 @@ function buildTwoColumnChart(columns, getters) {
29728
29860
  if (columns[1].type === "percentage") return {
29729
29861
  type: "pie",
29730
29862
  title: {},
29731
- dataSource: {
29732
- type: "range",
29733
- dataSets: [{
29734
- dataRange: getUnboundRange(getters, columns[1].zone),
29735
- dataSetId: "0"
29736
- }],
29737
- labelRange: getUnboundRange(getters, columns[0].zone),
29738
- dataSetsHaveTitle: isDatasetTitled(getters, columns[1])
29739
- },
29863
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[1].zone), getUnboundRange(getters, columns[0].zone)),
29740
29864
  dataSetStyles: {},
29741
29865
  aggregated: true,
29742
29866
  legendPosition: "none"
@@ -29744,15 +29868,7 @@ function buildTwoColumnChart(columns, getters) {
29744
29868
  if (columns[0].type === "number" && columns[1].type === "number") return {
29745
29869
  type: "scatter",
29746
29870
  title: {},
29747
- dataSource: {
29748
- type: "range",
29749
- dataSets: [{
29750
- dataRange: getUnboundRange(getters, columns[1].zone),
29751
- dataSetId: "0"
29752
- }],
29753
- labelRange: getUnboundRange(getters, columns[0].zone),
29754
- dataSetsHaveTitle: isDatasetTitled(getters, columns[1])
29755
- },
29871
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[1].zone), getUnboundRange(getters, columns[0].zone)),
29756
29872
  dataSetStyles: {},
29757
29873
  labelsAsText: false,
29758
29874
  legendPosition: "none"
@@ -29760,49 +29876,25 @@ function buildTwoColumnChart(columns, getters) {
29760
29876
  if (columns[0].type === "date" && columns[1].type === "number") return {
29761
29877
  ...DEFAULT_LINE_CHART_CONFIG,
29762
29878
  type: "line",
29763
- dataSource: {
29764
- type: "range",
29765
- dataSets: [{
29766
- dataRange: getUnboundRange(getters, columns[1].zone),
29767
- dataSetId: "0"
29768
- }],
29769
- labelRange: getUnboundRange(getters, columns[0].zone),
29770
- dataSetsHaveTitle: isDatasetTitled(getters, columns[0])
29771
- },
29879
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[0].zone), getUnboundRange(getters, columns[0].zone)),
29772
29880
  dataSetStyles: {}
29773
29881
  };
29774
29882
  if (columns[0].type === "text" && columns[1].type === "number") {
29775
29883
  const textColumn = columns[0];
29776
29884
  const numberColumn = columns[1];
29777
29885
  const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
29778
- const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn);
29886
+ const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn.zone);
29779
29887
  if (uniqueCount !== totalCount) return {
29780
29888
  type: "treemap",
29781
29889
  title: {},
29782
- dataSource: {
29783
- type: "range",
29784
- dataSets: [{
29785
- dataRange: getUnboundRange(getters, textColumn.zone),
29786
- dataSetId: "0"
29787
- }],
29788
- labelRange: getUnboundRange(getters, numberColumn.zone),
29789
- dataSetsHaveTitle
29790
- },
29890
+ dataSource: rangeSource([dataset(textColumn.zone, getters)], dataSetsHaveTitle, getUnboundRange(getters, numberColumn.zone)),
29791
29891
  dataSetStyles: {},
29792
29892
  legendPosition: "none"
29793
29893
  };
29794
29894
  }
29795
29895
  return {
29796
29896
  ...DEFAULT_BAR_CHART_CONFIG,
29797
- dataSource: {
29798
- type: "range",
29799
- dataSets: [{
29800
- dataRange: getUnboundRange(getters, columns[1].zone),
29801
- dataSetId: "0"
29802
- }],
29803
- labelRange: getUnboundRange(getters, columns[0].zone),
29804
- dataSetsHaveTitle: isDatasetTitled(getters, columns[1])
29805
- },
29897
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[1].zone), getUnboundRange(getters, columns[0].zone)),
29806
29898
  dataSetStyles: {}
29807
29899
  };
29808
29900
  }
@@ -29816,42 +29908,26 @@ function buildTwoColumnChart(columns, getters) {
29816
29908
  */
29817
29909
  function buildMultiColumnChart(columns, getters) {
29818
29910
  if (columns.length < 3) throw new Error("buildMultiColumnChart expects at least three columns");
29819
- const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col));
29911
+ const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col.zone));
29820
29912
  const lastColumn = columns[columns.length - 1];
29821
29913
  const columnsExceptLast = columns.slice(0, columns.length - 1);
29822
29914
  if ((lastColumn.type === "percentage" || lastColumn.type === "number") && columnsExceptLast.every((col) => col.type === "text")) {
29823
- const dataSets = columnsExceptLast.map(({ zone }, i) => ({
29824
- dataRange: getUnboundRange(getters, zone),
29825
- dataSetId: i.toString()
29826
- }));
29915
+ const dataSets = columnsExceptLast.map(({ zone }, i) => dataset(zone, getters, String(i)));
29827
29916
  return {
29828
29917
  type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap",
29829
29918
  title: {},
29830
- dataSource: {
29831
- type: "range",
29832
- dataSets,
29833
- dataSetsHaveTitle,
29834
- labelRange: getUnboundRange(getters, lastColumn.zone)
29835
- },
29919
+ dataSource: rangeSource(dataSets, dataSetsHaveTitle, getUnboundRange(getters, lastColumn.zone)),
29836
29920
  dataSetStyles: {},
29837
29921
  legendPosition: "none"
29838
29922
  };
29839
29923
  }
29840
29924
  const firstColumn = columns[0];
29841
29925
  const columnsExceptFirst = columns.slice(1);
29842
- const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }, i) => ({
29843
- dataRange: getUnboundRange(getters, zone),
29844
- dataSetId: i.toString()
29845
- }));
29926
+ const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }, i) => dataset(zone, getters, String(i)));
29846
29927
  if (columnsExceptFirst.every((col) => col.type === "percentage")) return {
29847
29928
  type: "pie",
29848
29929
  title: {},
29849
- dataSource: {
29850
- type: "range",
29851
- dataSets: rangesOfColumnsExceptFirst,
29852
- labelRange: getUnboundRange(getters, firstColumn.zone),
29853
- dataSetsHaveTitle
29854
- },
29930
+ dataSource: rangeSource(rangesOfColumnsExceptFirst, dataSetsHaveTitle, getUnboundRange(getters, firstColumn.zone)),
29855
29931
  dataSetStyles: {},
29856
29932
  aggregated: false,
29857
29933
  legendPosition: "top"
@@ -29859,23 +29935,13 @@ function buildMultiColumnChart(columns, getters) {
29859
29935
  if (firstColumn.type === "date" && columnsExceptFirst.every((col) => col.type === "number")) return {
29860
29936
  ...DEFAULT_LINE_CHART_CONFIG,
29861
29937
  type: "line",
29862
- dataSource: {
29863
- type: "range",
29864
- dataSets: rangesOfColumnsExceptFirst,
29865
- labelRange: getUnboundRange(getters, firstColumn.zone),
29866
- dataSetsHaveTitle
29867
- },
29938
+ dataSource: rangeSource(rangesOfColumnsExceptFirst, dataSetsHaveTitle, getUnboundRange(getters, firstColumn.zone)),
29868
29939
  dataSetStyles: {},
29869
29940
  legendPosition: "top"
29870
29941
  };
29871
29942
  return {
29872
29943
  ...DEFAULT_BAR_CHART_CONFIG,
29873
- dataSource: {
29874
- type: "range",
29875
- dataSets: rangesOfColumnsExceptFirst,
29876
- labelRange: getUnboundRange(getters, firstColumn.zone),
29877
- dataSetsHaveTitle
29878
- },
29944
+ dataSource: rangeSource(rangesOfColumnsExceptFirst, dataSetsHaveTitle, getUnboundRange(getters, firstColumn.zone)),
29879
29945
  dataSetStyles: {},
29880
29946
  legendPosition: "top"
29881
29947
  };
@@ -29889,7 +29955,7 @@ function buildScorecard(zone, getters) {
29889
29955
  return {
29890
29956
  type: "scorecard",
29891
29957
  title: {},
29892
- keyValue: getUnboundRange(getters, zone),
29958
+ keyValue: `=${getUnboundRange(getters, zone)}`,
29893
29959
  background: cell?.style?.fillColor,
29894
29960
  baselineMode: DEFAULT_SCORECARD_BASELINE_MODE,
29895
29961
  baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
@@ -29902,17 +29968,10 @@ function buildScorecard(zone, getters) {
29902
29968
  function getSmartChartDefinition(zones, getters) {
29903
29969
  const columns = categorizeColumns(zones, getters);
29904
29970
  if (columns.length === 0 || columns.every((col) => col.type === "empty")) {
29905
- const dataSets = columns.map(({ zone }, i) => ({
29906
- dataRange: getUnboundRange(getters, zone),
29907
- dataSetId: i.toString()
29908
- }));
29971
+ const dataSets = columns.map(({ zone }, i) => dataset(zone, getters, String(i)));
29909
29972
  return {
29910
29973
  ...DEFAULT_BAR_CHART_CONFIG,
29911
- dataSource: {
29912
- type: "range",
29913
- dataSets,
29914
- dataSetsHaveTitle: false
29915
- }
29974
+ dataSource: rangeSource(dataSets, false)
29916
29975
  };
29917
29976
  }
29918
29977
  const nonEmptyColumns = columns.filter((col) => col.type !== "empty");
@@ -31600,7 +31659,7 @@ var SelectionInput = class extends Component {
31600
31659
  dragAndDrop = useDragAndDropListItems();
31601
31660
  focusedInputRef = signal.ref(HTMLInputElement);
31602
31661
  unfocusedInputRef = signal.ref(HTMLInputElement);
31603
- selectionRef = signal(null);
31662
+ selectionRef = signal.ref();
31604
31663
  DOMFocusableElementStore;
31605
31664
  store;
31606
31665
  get ranges() {
@@ -32107,7 +32166,7 @@ var ColorPickerWidget = class extends Component {
32107
32166
  dropdownMaxHeight: types$1.Pixel().optional(),
32108
32167
  class: types$1.string().optional()
32109
32168
  });
32110
- colorPickerButtonRef = signal(null);
32169
+ colorPickerButtonRef = signal.ref();
32111
32170
  get iconStyle() {
32112
32171
  return this.props.currentColor ? `border-color: ${this.props.currentColor}` : "border-bottom-style: hidden";
32113
32172
  }
@@ -32135,8 +32194,8 @@ var NumberEditor = class extends Component {
32135
32194
  });
32136
32195
  dropdown = proxy({ isOpen: false });
32137
32196
  inputRef = signal.ref(HTMLInputElement);
32138
- rootEditorRef = signal(null);
32139
- valueListRef = signal(null);
32197
+ rootEditorRef = signal.ref();
32198
+ valueListRef = signal.ref();
32140
32199
  DOMFocusableElementStore;
32141
32200
  setup() {
32142
32201
  this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
@@ -32442,7 +32501,7 @@ var ChartTypePickerPopover = class extends Component {
32442
32501
  });
32443
32502
  categories = chartCategories;
32444
32503
  chartTypeByCategories = {};
32445
- popoverRef = signal(null);
32504
+ popoverRef = signal.ref();
32446
32505
  setup() {
32447
32506
  useListener(window, "pointerdown", this.onExternalClick.bind(this), { capture: true });
32448
32507
  for (const subtypeProperties of chartSubtypeRegistry.getAll()) {
@@ -32467,7 +32526,7 @@ var CogWheelMenu = class extends Component {
32467
32526
  static template = "o-spreadsheet-CogWheelMenu";
32468
32527
  static components = { MenuPopover };
32469
32528
  props = useProps({ items: types$1.array(types$1.ActionSpec()) });
32470
- buttonRef = signal(null);
32529
+ buttonRef = signal.ref();
32471
32530
  menuState = proxy({
32472
32531
  isOpen: false,
32473
32532
  anchorRect: null,
@@ -32511,8 +32570,8 @@ var CarouselPanel = class extends Component {
32511
32570
  });
32512
32571
  DEFAULT_CAROUSEL_TITLE_STYLE = DEFAULT_CAROUSEL_TITLE_STYLE;
32513
32572
  dragAndDrop = useDragAndDropListItems();
32514
- previewListRef = signal(null);
32515
- addChartButton = signal(null);
32573
+ previewListRef = signal.ref();
32574
+ addChartButton = signal.ref(HTMLButtonElement);
32516
32575
  state = proxy({
32517
32576
  popoverProps: void 0,
32518
32577
  currentRange: void 0
@@ -32956,7 +33015,7 @@ var BarConfigPanel = class extends GenericChartConfigPanel {
32956
33015
  var Collapse = class extends Component {
32957
33016
  static template = "o-spreadsheet-Collapse";
32958
33017
  props = useProps({ isCollapsed: types$1.boolean() });
32959
- contentRef = signal(null);
33018
+ contentRef = signal.ref();
32960
33019
  setup() {
32961
33020
  onMounted(() => {
32962
33021
  if (this.props.isCollapsed) this.contentRef()?.classList.add("d-none");
@@ -33009,7 +33068,7 @@ var ChartAnnotation = class extends Component {
33009
33068
  TextInput
33010
33069
  };
33011
33070
  props = useProps(chartSidePanelPropsDefinition);
33012
- editorRef = signal(null);
33071
+ editorRef = signal.ref();
33013
33072
  state = proxy({
33014
33073
  annotationTextInput: this.props.definition.annotationText || "",
33015
33074
  isOverflowing: false
@@ -33331,7 +33390,7 @@ var RoundColorPicker = class extends Component {
33331
33390
  onColorPicked: types$1.function(),
33332
33391
  disableNoColor: types$1.boolean().optional()
33333
33392
  });
33334
- colorPickerButtonRef = signal(null);
33393
+ colorPickerButtonRef = signal.ref();
33335
33394
  state;
33336
33395
  setup() {
33337
33396
  this.state = proxy({ pickerOpened: false });
@@ -36173,6 +36232,7 @@ var helpers_index_exports$1 = /* @__PURE__ */ __exportAll({
36173
36232
  getDefinedAxis: () => getDefinedAxis,
36174
36233
  getPieColors: () => getPieColors,
36175
36234
  isTrendLineAxis: () => isTrendLineAxis,
36235
+ limitChartConfigDataPoints: () => limitChartConfigDataPoints,
36176
36236
  shouldRemoveFirstLabel: () => shouldRemoveFirstLabel,
36177
36237
  toExcelDataset: () => toExcelDataset,
36178
36238
  toExcelLabelRange: () => toExcelLabelRange,
@@ -36943,7 +37003,7 @@ var ColorScalePicker = class extends Component {
36943
37003
  popoverProps: void 0,
36944
37004
  popoverStyle: ""
36945
37005
  });
36946
- popoverRef = signal(null);
37006
+ popoverRef = signal.ref(HTMLTableElement);
36947
37007
  setup() {
36948
37008
  useExternalListener(window, "click", this.closePopover);
36949
37009
  }
@@ -37192,7 +37252,7 @@ var TextValueProvider = class extends Component {
37192
37252
  onValueSelected: types$1.function(),
37193
37253
  onValueHovered: types$1.function()
37194
37254
  });
37195
- autoCompleteListRef = signal(null);
37255
+ autoCompleteListRef = signal.ref();
37196
37256
  setup() {
37197
37257
  useEffect(() => {
37198
37258
  const selectedIndex = this.props.selectedIndex;
@@ -37483,7 +37543,7 @@ var SpeechBubble = class extends Component {
37483
37543
  anchorRect: types$1.Rect()
37484
37544
  });
37485
37545
  spreadsheetRect = useSpreadsheetRect();
37486
- bubbleRef = signal(null);
37546
+ bubbleRef = signal.ref();
37487
37547
  setup() {
37488
37548
  useEffect(() => {
37489
37549
  const el = this.bubbleRef();
@@ -37528,8 +37588,8 @@ var Composer = class extends Component {
37528
37588
  showAssistant: types$1.boolean().optional(true)
37529
37589
  });
37530
37590
  DOMFocusableElementStore;
37531
- composerRef = signal(null);
37532
- containerRef = signal(null);
37591
+ composerRef = signal.ref();
37592
+ containerRef = signal.ref();
37533
37593
  contentHelper = new ContentEditableHelper(this.composerRef());
37534
37594
  composerState = proxy({
37535
37595
  positionStart: 0,
@@ -39593,10 +39653,10 @@ var ScatterConfigPanel = class extends GenericChartConfigPanel {
39593
39653
  var ScorecardChartConfigPanel = class extends Component {
39594
39654
  static template = "o-spreadsheet-ScorecardChartConfigPanel";
39595
39655
  static components = {
39596
- SelectionInput,
39597
39656
  ChartErrorSection,
39598
39657
  Section,
39599
- Select
39658
+ Select,
39659
+ StandaloneComposer
39600
39660
  };
39601
39661
  props = useProps(chartSidePanelPropsDefinition);
39602
39662
  state = proxy({
@@ -39608,30 +39668,18 @@ var ScorecardChartConfigPanel = class extends Component {
39608
39668
  get errorMessages() {
39609
39669
  return [...this.state.keyValueDispatchResult?.reasons || [], ...this.state.baselineDispatchResult?.reasons || []].filter((reason) => reason !== "NoChanges").map((error) => ChartTerms.Errors[error] || ChartTerms.Errors.Unexpected);
39610
39670
  }
39611
- get isKeyValueInvalid() {
39612
- return !!this.state.keyValueDispatchResult?.isCancelledBecause("InvalidScorecardKeyValue");
39613
- }
39614
- get isBaselineInvalid() {
39615
- return !!this.state.baselineDispatchResult?.isCancelledBecause("InvalidScorecardBaseline");
39616
- }
39617
- onKeyValueRangeChanged(ranges) {
39618
- this.keyValue = ranges[0];
39619
- this.state.keyValueDispatchResult = this.props.canUpdateChart(this.props.chartId, { keyValue: this.keyValue });
39620
- }
39621
- updateKeyValueRange() {
39671
+ onConfirmKeyValue(keyValue) {
39672
+ this.keyValue = keyValue;
39622
39673
  this.state.keyValueDispatchResult = this.props.updateChart(this.props.chartId, { keyValue: this.keyValue });
39623
39674
  }
39624
- getKeyValueRange() {
39675
+ getKeyValue() {
39625
39676
  return this.keyValue || "";
39626
39677
  }
39627
- onBaselineRangeChanged(ranges) {
39628
- this.baseline = ranges[0];
39629
- this.state.baselineDispatchResult = this.props.canUpdateChart(this.props.chartId, { baseline: this.baseline });
39630
- }
39631
- updateBaselineRange() {
39678
+ onConfirmBaseline(baseline) {
39679
+ this.baseline = baseline;
39632
39680
  this.state.baselineDispatchResult = this.props.updateChart(this.props.chartId, { baseline: this.baseline });
39633
39681
  }
39634
- getBaselineRange() {
39682
+ getBaseline() {
39635
39683
  return this.baseline || "";
39636
39684
  }
39637
39685
  updateBaselineMode(baselineMode) {
@@ -40031,7 +40079,7 @@ var ChartTypePicker = class extends Component {
40031
40079
  chartId: types$1.UID(),
40032
40080
  chartPanelStore: types$1.Store()
40033
40081
  });
40034
- selectRef = signal(null);
40082
+ selectRef = signal.ref();
40035
40083
  state = proxy({ popoverProps: void 0 });
40036
40084
  getSupportedChartTypes() {
40037
40085
  let supportedTypes;
@@ -40088,6 +40136,7 @@ var MainChartPanelStore = class extends SpreadsheetStore {
40088
40136
  changeChartType(chartId, newDisplayType) {
40089
40137
  const currentCreationContext = this.getters.getContextCreationChart(chartId);
40090
40138
  const savedCreationContext = this.creationContexts[chartId] || {};
40139
+ const auxiliaryRange = currentCreationContext && "auxiliaryRange" in currentCreationContext ? currentCreationContext.auxiliaryRange : savedCreationContext.auxiliaryRange;
40091
40140
  let dataSetStyles = savedCreationContext.dataSetStyles ?? currentCreationContext?.dataSetStyles;
40092
40141
  let dataSource = {
40093
40142
  ...savedCreationContext.dataSource,
@@ -40109,7 +40158,8 @@ var MainChartPanelStore = class extends SpreadsheetStore {
40109
40158
  dataSetsHaveTitle: false,
40110
40159
  ...savedCreationContext.dataSource,
40111
40160
  ...currentCreationContext?.dataSource,
40112
- dataSets: newRanges ?? []
40161
+ dataSets: newRanges ?? [],
40162
+ labelRange: auxiliaryRange
40113
40163
  };
40114
40164
  }
40115
40165
  this.creationContexts[chartId] = {
@@ -40138,7 +40188,7 @@ var MainChartPanelStore = class extends SpreadsheetStore {
40138
40188
  dataSetsStartWithSameRanges(currentDataSets, savedDataSets, currentStyles, savedStyles) {
40139
40189
  return currentDataSets.every((ds, i) => {
40140
40190
  const savedDs = savedDataSets[i];
40141
- return deepEquals(ds.dataRange, savedDs.dataRange) && deepEquals(currentStyles?.[ds.dataSetId], savedStyles?.[savedDs.dataSetId]);
40191
+ return deepEquals(ds.dataRange, savedDs?.dataRange) && deepEquals(currentStyles?.[ds.dataSetId], savedStyles?.[savedDs?.dataSetId]);
40142
40192
  });
40143
40193
  }
40144
40194
  /**
@@ -40599,7 +40649,7 @@ var ColumnStatsPanel = class extends Component {
40599
40649
  highlightPositions: []
40600
40650
  });
40601
40651
  store;
40602
- chartCanvasRef = signal(null);
40652
+ chartCanvasRef = signal.ref(HTMLCanvasElement);
40603
40653
  chart;
40604
40654
  setup() {
40605
40655
  this.store = useStore(ColumnStatisticsStore);
@@ -42027,7 +42077,7 @@ var ConditionalFormatPreview = class extends Component {
42027
42077
  class: types$1.string()
42028
42078
  });
42029
42079
  icons = ICONS;
42030
- cfPreviewRef = signal(null);
42080
+ cfPreviewRef = signal.ref();
42031
42081
  setup() {
42032
42082
  useHighlightsOnHover(this.cfPreviewRef, this);
42033
42083
  }
@@ -42084,7 +42134,7 @@ var ConditionalFormatPreviewList = class extends Component {
42084
42134
  static components = { ConditionalFormatPreview };
42085
42135
  props = useProps({ onCloseSidePanel: types$1.function() });
42086
42136
  dragAndDrop = useDragAndDropListItems();
42087
- cfListRef = signal(null);
42137
+ cfListRef = signal.ref();
42088
42138
  get conditionalFormats() {
42089
42139
  return this.env.model.getters.getConditionalFormats(this.env.model.getters.getActiveSheetId());
42090
42140
  }
@@ -42144,6 +42194,1477 @@ var ConditionalFormatPreviewList = class extends Component {
42144
42194
  }
42145
42195
  };
42146
42196
 
42197
+ //#endregion
42198
+ //#region src/stores/chart_drag_store.ts
42199
+ /**
42200
+ * Holds the id of the figure that a dragged chart suggestion is currently
42201
+ * hovering over, so it can be highlighted reactively instead of through direct DOM manipulation.
42202
+ */
42203
+ var ChartDragStore = class {
42204
+ mutators = ["setHighlightedFigure"];
42205
+ highlightedFigureId = void 0;
42206
+ setHighlightedFigure(figureId) {
42207
+ this.highlightedFigureId = figureId;
42208
+ }
42209
+ };
42210
+
42211
+ //#endregion
42212
+ //#region src/components/helpers/chart_drag_and_drop.ts
42213
+ function getDefaultChartFigureSize(type) {
42214
+ if (type === "scorecard") return {
42215
+ width: 213,
42216
+ height: 101
42217
+ };
42218
+ return {
42219
+ width: 536,
42220
+ height: 335
42221
+ };
42222
+ }
42223
+ function getCarouselOverlappingChart(figureUI, otherFigures, matchTags) {
42224
+ if (figureUI.tag !== "chart") return;
42225
+ const figureCenterX = figureUI.x + figureUI.width / 2;
42226
+ const figureCenterY = figureUI.y + figureUI.height / 2;
42227
+ let bestMatch;
42228
+ let smallestDistance = Infinity;
42229
+ for (const figure of otherFigures) {
42230
+ if (!matchTags.includes(figure.tag)) continue;
42231
+ const targetCenterX = figure.x + figure.width / 2;
42232
+ const targetCenterY = figure.y + figure.height / 2;
42233
+ const distanceX = Math.abs(figureCenterX - targetCenterX);
42234
+ const distanceY = Math.abs(figureCenterY - targetCenterY);
42235
+ const squaredDistance = distanceX ** 2 + distanceY ** 2;
42236
+ if (distanceX <= figureUI.width / 2 && distanceY <= figureUI.height / 2 && squaredDistance < smallestDistance) {
42237
+ smallestDistance = squaredDistance;
42238
+ bestMatch = figure;
42239
+ }
42240
+ }
42241
+ return bestMatch;
42242
+ }
42243
+ /**
42244
+ * Start dragging a floating preview of the given chart definition, following the mouse.
42245
+ * On drop, creates the chart on the grid (or inside a carousel if dropped on one).
42246
+ */
42247
+ function startChartDragAndDrop(env, definition, ev) {
42248
+ const zoom = env.getStore(ZoomStore).zoomLevel;
42249
+ const gridPosition = gridOverlayPosition(zoom);
42250
+ const spreadsheet = document.querySelector(".o-spreadsheet");
42251
+ if (!spreadsheet) return;
42252
+ const startX = ev.clientX / zoom;
42253
+ const startY = ev.clientY / zoom;
42254
+ const { width, height } = getDefaultChartFigureSize(definition.type);
42255
+ const figureWidth = width * zoom;
42256
+ const figureHeight = height * zoom;
42257
+ const getters = env.model.getters;
42258
+ const sheetId = getters.getActiveSheetId();
42259
+ let container = null;
42260
+ let destroyChart = void 0;
42261
+ const chartDragStore = env.getStore(ChartDragStore);
42262
+ const previousCursor = document.body.style.cursor;
42263
+ document.body.style.cursor = "grabbing";
42264
+ /** Grid coordinates (in sheet pixels) of a mouse position, or undefined if outside the grid. */
42265
+ const getGridPosition = (clientX, clientY) => {
42266
+ if (clientX > gridPosition.x + gridPosition.width || clientY > gridPosition.y + gridPosition.height) return;
42267
+ const { scrollX, scrollY } = env.getStore(ViewportsStore).activeSheetScrollInfo;
42268
+ return {
42269
+ x: Math.max(0, (clientX - gridPosition.x) / zoom + scrollX),
42270
+ y: Math.max(0, (clientY - gridPosition.y) / zoom + scrollY)
42271
+ };
42272
+ };
42273
+ /** Carousel or standalone chart the dragged chart is dropped onto, if any. */
42274
+ const getOverlappingFigure = (clientX, clientY) => {
42275
+ const position = getGridPosition(clientX, clientY);
42276
+ if (!position) return;
42277
+ const figureUI = {
42278
+ tag: "chart",
42279
+ ...position,
42280
+ width,
42281
+ height
42282
+ };
42283
+ const otherFigures = env.getStore(ViewportsStore).visibleFigures;
42284
+ return getCarouselOverlappingChart(figureUI, otherFigures, ["carousel", "chart"]);
42285
+ };
42286
+ const halfWidth = figureWidth / 2;
42287
+ const halfHeight = figureHeight / 2;
42288
+ const onMouseMove = (e) => {
42289
+ if (Math.abs(e.clientX - startX) <= 5 && Math.abs(e.clientY - startY) <= 5) return;
42290
+ if (container === null) {
42291
+ container = document.createElement("div");
42292
+ container.className = "o-chart-drag-preview os-theme-dependant position-fixed border pe-none";
42293
+ container.style.width = `${width}px`;
42294
+ container.style.height = `${height}px`;
42295
+ container.style.zoom = `${zoom}`;
42296
+ const canvas = document.createElement("canvas");
42297
+ canvas.className = "w-100 h-100";
42298
+ container.appendChild(canvas);
42299
+ spreadsheet.appendChild(container);
42300
+ destroyChart = drawChartOnCanvas(canvas, SpreadsheetChart.fromStrDefinition(getters, sheetId, definition).getRuntime(getters, "newChart"), {
42301
+ width,
42302
+ height
42303
+ }, definition.type, zoom);
42304
+ }
42305
+ container.style.left = `${Math.max(gridPosition.x, (e.clientX - halfWidth) / zoom)}px`;
42306
+ container.style.top = `${Math.max(gridPosition.y, (e.clientY - halfHeight) / zoom)}px`;
42307
+ const overlappingFigure = getOverlappingFigure(e.clientX - halfWidth, e.clientY - halfHeight);
42308
+ container.style.opacity = overlappingFigure?.id ? "0.6" : "0.9";
42309
+ chartDragStore.setHighlightedFigure(overlappingFigure?.id);
42310
+ };
42311
+ const onMouseUp = (mouseEvent) => {
42312
+ chartDragStore.setHighlightedFigure(void 0);
42313
+ if (container !== null) {
42314
+ spreadsheet.removeChild(container);
42315
+ container = null;
42316
+ }
42317
+ destroyChart?.();
42318
+ document.body.style.cursor = previousCursor;
42319
+ let position = getGridPosition(mouseEvent.clientX - halfWidth, mouseEvent.clientY - halfHeight);
42320
+ if (Math.abs(mouseEvent.clientX / zoom - startX) <= 5 && Math.abs(mouseEvent.clientY / zoom - startY) <= 5) position = {
42321
+ x: 0,
42322
+ y: 0
42323
+ };
42324
+ else if (!position || position.x + halfWidth > gridPosition.width) return;
42325
+ const { col, row, offset } = env.getStore(ViewportsStore).viewports.getPositionAnchorOffset(sheetId, position);
42326
+ const payload = {
42327
+ chartId: UuidGenerator.smallUuid(),
42328
+ figureId: UuidGenerator.smallUuid(),
42329
+ sheetId,
42330
+ size: {
42331
+ width,
42332
+ height
42333
+ },
42334
+ definition,
42335
+ col,
42336
+ row,
42337
+ offset
42338
+ };
42339
+ const overlappingFigure = getOverlappingFigure(mouseEvent.clientX - halfWidth, mouseEvent.clientY - halfHeight);
42340
+ if (overlappingFigure?.tag === "carousel") env.model.dispatch("ADD_NEW_CHART_TO_CAROUSEL", {
42341
+ sheetId,
42342
+ figureId: overlappingFigure.id,
42343
+ newChartId: UuidGenerator.smallUuid(),
42344
+ chartDefinition: definition
42345
+ });
42346
+ else if (overlappingFigure?.tag === "chart") env.model.dispatch("CREATE_CHART_AND_MERGE_INTO_CAROUSEL", {
42347
+ chartId: payload.chartId,
42348
+ figureId: payload.figureId,
42349
+ sheetId: payload.sheetId,
42350
+ definition: payload.definition,
42351
+ baseFigureId: overlappingFigure.id
42352
+ });
42353
+ else env.model.dispatch("CREATE_CHART", payload);
42354
+ };
42355
+ startDnd(onMouseMove, onMouseUp);
42356
+ }
42357
+
42358
+ //#endregion
42359
+ //#region src/components/side_panel/data_analysis/chart_suggestion_preview.ts
42360
+ const PREVIEW_BUBBLE_RADIUS_RATIO = .3;
42361
+ const MIN_PREVIEW_BUBBLE_RADIUS = 1;
42362
+ var ChartSuggestionPreview = class extends Component {
42363
+ static template = "o-spreadsheet-ChartSuggestionPreview";
42364
+ props = useProps({
42365
+ definition: types$1.ChartDefinition(),
42366
+ description: types$1.string(),
42367
+ onPointerDown: types$1.function()
42368
+ });
42369
+ chartCanvasRef = signal.ref(HTMLCanvasElement);
42370
+ chart;
42371
+ setup() {
42372
+ onMounted(() => registerChartJSExtensions());
42373
+ onWillUnmount(() => this.destroyChart());
42374
+ useLayoutEffect(() => {
42375
+ this.updateChart();
42376
+ }, () => [this.props.definition]);
42377
+ }
42378
+ getChartConfiguration() {
42379
+ const getters = this.env.model.getters;
42380
+ const activeSheetId = getters.getActiveSheetId();
42381
+ const runtime = SpreadsheetChart.fromStrDefinition(getters, activeSheetId, this.props.definition).getRuntime(getters, "myChart");
42382
+ if (!("chartJsConfig" in runtime)) return null;
42383
+ let config = runtime.chartJsConfig;
42384
+ const existingScales = config.options?.scales ?? {};
42385
+ config = {
42386
+ ...config,
42387
+ data: this.props.definition.type === "bubble" ? {
42388
+ ...config.data,
42389
+ datasets: (config.data?.datasets || []).map((dataset) => ({
42390
+ ...dataset,
42391
+ pointRadius: this.scaleBubblePointRadius(dataset.pointRadius),
42392
+ pointHoverRadius: this.scaleBubblePointRadius(dataset.pointHoverRadius)
42393
+ }))
42394
+ } : config.data,
42395
+ options: {
42396
+ ...config.options,
42397
+ plugins: {
42398
+ ...config.options?.plugins,
42399
+ legend: { display: false },
42400
+ title: { display: false },
42401
+ tooltip: { enabled: false }
42402
+ },
42403
+ events: [],
42404
+ animation: false,
42405
+ scales: this.getScalesCongif(existingScales)
42406
+ }
42407
+ };
42408
+ return limitChartConfigDataPoints(config);
42409
+ }
42410
+ getScalesCongif(scales) {
42411
+ if (scales.x) scales.x = {
42412
+ ...scales.x,
42413
+ ticks: { display: false },
42414
+ border: { display: false }
42415
+ };
42416
+ if (scales.y) scales.y = {
42417
+ ...scales.y,
42418
+ ticks: { display: false },
42419
+ border: { display: false }
42420
+ };
42421
+ if (scales.r) scales.r = {
42422
+ ...scales.r,
42423
+ ticks: { display: false },
42424
+ pointLabels: { display: false }
42425
+ };
42426
+ return scales;
42427
+ }
42428
+ scaleBubblePointRadius(pointRadius) {
42429
+ if (Array.isArray(pointRadius)) return pointRadius.map((radius) => radius <= 0 ? radius : Math.max(MIN_PREVIEW_BUBBLE_RADIUS, radius * PREVIEW_BUBBLE_RADIUS_RATIO));
42430
+ if (typeof pointRadius === "number") return pointRadius <= 0 ? pointRadius : Math.max(MIN_PREVIEW_BUBBLE_RADIUS, pointRadius * PREVIEW_BUBBLE_RADIUS_RATIO);
42431
+ return pointRadius ?? MIN_PREVIEW_BUBBLE_RADIUS;
42432
+ }
42433
+ updateChart() {
42434
+ this.destroyChart();
42435
+ const config = this.getChartConfiguration();
42436
+ if (!config) {
42437
+ this.drawNativeChart();
42438
+ return;
42439
+ }
42440
+ const ctx = this.chartCanvasRef()?.getContext("2d");
42441
+ if (!ctx || !globalThis.Chart) return;
42442
+ this.chart = new globalThis.Chart(ctx, config);
42443
+ }
42444
+ drawNativeChart() {
42445
+ const canvas = this.chartCanvasRef();
42446
+ if (!canvas) return;
42447
+ const getters = this.env.model.getters;
42448
+ let runtime = SpreadsheetChart.fromStrDefinition(getters, getters.getActiveSheetId(), this.props.definition).getRuntime(getters, "myChart");
42449
+ const { type } = this.props.definition;
42450
+ if (type === "scorecard") {
42451
+ const rect = canvas.getBoundingClientRect();
42452
+ runtime = {
42453
+ ...runtime,
42454
+ keyValueStyle: {
42455
+ ...runtime.keyValueStyle,
42456
+ fontSize: 16
42457
+ }
42458
+ };
42459
+ drawScoreChart(getScorecardConfiguration({
42460
+ width: rect.width || 130,
42461
+ height: rect.height || 120
42462
+ }, runtime), canvas);
42463
+ } else if (type === "gauge") {
42464
+ let gaugeRuntime = runtime;
42465
+ if (gaugeRuntime.title) gaugeRuntime = {
42466
+ ...gaugeRuntime,
42467
+ title: { text: "" }
42468
+ };
42469
+ drawGaugeChart(canvas, gaugeRuntime, 1, void 0, { labelFontSize: 8 });
42470
+ }
42471
+ }
42472
+ destroyChart() {
42473
+ this.chart?.destroy();
42474
+ this.chart = void 0;
42475
+ }
42476
+ };
42477
+
42478
+ //#endregion
42479
+ //#region src/helpers/data_analysis.ts
42480
+ function analyzeColumns(zones, getters) {
42481
+ return getZonesByColumns(zones).map((zone) => analyzeColumn(zone, getters));
42482
+ }
42483
+ function analyzeColumn(zone, getters) {
42484
+ const sheetId = getters.getActiveSheetId();
42485
+ const cells = getters.getEvaluatedCellsInZone(sheetId, zone).filter((c) => c.type !== "empty");
42486
+ if (!cells.length) return {
42487
+ zone,
42488
+ type: "empty",
42489
+ hasHeader: false,
42490
+ rowCount: 0,
42491
+ uniqueCount: 0,
42492
+ uniqueRatio: 0,
42493
+ nonEmpty: []
42494
+ };
42495
+ const firstCell = cells[0];
42496
+ const rest = cells.slice(1);
42497
+ const hasHeader = firstCell.type === "text" && rest.some((c) => c.type !== "text");
42498
+ const dataCells = hasHeader ? rest : cells;
42499
+ const numericValues = dataCells.filter((c) => c.type === "number").map((c) => c.value);
42500
+ const allVals = dataCells.map((c) => String(c.value ?? ""));
42501
+ const uniqueCount = new Set(allVals).size;
42502
+ return {
42503
+ zone,
42504
+ type: computeColumnType(dataCells),
42505
+ header: hasHeader ? firstCell.value : void 0,
42506
+ hasHeader,
42507
+ rowCount: dataCells.length,
42508
+ uniqueCount,
42509
+ uniqueRatio: allVals.length > 0 ? uniqueCount / allVals.length : 0,
42510
+ maxValue: numericValues.length ? numericValues.reduce((max, v) => v > max ? v : max, numericValues[0]) : void 0,
42511
+ nonEmpty: dataCells
42512
+ };
42513
+ }
42514
+ function computeColumnType(cells) {
42515
+ if (cells.length === 0) return "empty";
42516
+ if (cells.every((c) => c.type === "error")) return "error";
42517
+ cells = cells.filter((c) => c.type !== "error");
42518
+ if (cells.every((c) => c.type === "boolean")) return "boolean";
42519
+ if (cells.every((c) => c.type === "number" && !!c.format && isDateTimeFormat(c.format))) return "date";
42520
+ else if (cells.every((c) => c.type === "number")) {
42521
+ if (cells.every((c) => c.format?.includes("%"))) return "percentage";
42522
+ return "number";
42523
+ } else {
42524
+ const textVals = cells.filter((c) => c.type === "text").map((c) => c.value);
42525
+ if (textVals.length > 0) {
42526
+ const unique = new Set(textVals).size;
42527
+ return unique / textVals.length < .75 && unique <= 20 ? "categorical" : "label";
42528
+ }
42529
+ }
42530
+ return "empty";
42531
+ }
42532
+
42533
+ //#endregion
42534
+ //#region src/helpers/figures/charts/charts_suggestions_constants.ts
42535
+ /** Above this many rows, shape-based charts (radar, scatter, pyramid) become unreadable. */
42536
+ const MAX_ROWS_FOR_SHAPE_CHART = 10;
42537
+ function barChart(titleText, source, opts = {}) {
42538
+ return {
42539
+ ...opts,
42540
+ type: "bar",
42541
+ title: { text: titleText },
42542
+ dataSource: source,
42543
+ dataSetStyles: {},
42544
+ legendPosition: opts.legendPosition ?? "none",
42545
+ stacked: opts.stacked ?? false,
42546
+ humanize: true,
42547
+ aggregated: opts.aggregated ?? false
42548
+ };
42549
+ }
42550
+ function lineChart(titleText, source, opts = {}) {
42551
+ return {
42552
+ ...opts,
42553
+ type: "line",
42554
+ title: { text: titleText },
42555
+ dataSource: source,
42556
+ dataSetStyles: {},
42557
+ legendPosition: opts.legendPosition ?? "none",
42558
+ stacked: opts.stacked ?? false,
42559
+ cumulative: opts.cumulative ?? false,
42560
+ labelsAsText: false,
42561
+ humanize: true
42562
+ };
42563
+ }
42564
+ function pieChart(titleText, source, opts = {}) {
42565
+ return {
42566
+ ...opts,
42567
+ type: "pie",
42568
+ title: { text: titleText },
42569
+ dataSource: source,
42570
+ dataSetStyles: {},
42571
+ legendPosition: opts.legendPosition ?? "top",
42572
+ humanize: true
42573
+ };
42574
+ }
42575
+ function radarChart(titleText, source, opts = {}) {
42576
+ return {
42577
+ ...opts,
42578
+ type: "radar",
42579
+ title: { text: titleText },
42580
+ dataSource: source,
42581
+ dataSetStyles: {},
42582
+ legendPosition: opts.legendPosition ?? "none",
42583
+ stacked: false,
42584
+ humanize: true
42585
+ };
42586
+ }
42587
+ function sunburstChart(titleText, source) {
42588
+ return {
42589
+ type: "sunburst",
42590
+ title: { text: titleText },
42591
+ dataSource: source,
42592
+ dataSetStyles: {},
42593
+ legendPosition: "none"
42594
+ };
42595
+ }
42596
+ function treemapChart(titleText, source) {
42597
+ return {
42598
+ type: "treemap",
42599
+ title: { text: titleText },
42600
+ dataSource: source,
42601
+ dataSetStyles: {},
42602
+ legendPosition: "none"
42603
+ };
42604
+ }
42605
+ function calendarChart(titleText, source) {
42606
+ return {
42607
+ type: "calendar",
42608
+ title: { text: titleText },
42609
+ dataSource: source,
42610
+ dataSetStyles: {},
42611
+ legendPosition: "none",
42612
+ horizontalGroupBy: "month_number",
42613
+ verticalGroupBy: "day_of_week",
42614
+ humanize: true
42615
+ };
42616
+ }
42617
+ function scatterChart(titleText, source) {
42618
+ return {
42619
+ type: "scatter",
42620
+ title: { text: titleText },
42621
+ dataSource: source,
42622
+ dataSetStyles: {},
42623
+ legendPosition: "none",
42624
+ labelsAsText: false,
42625
+ humanize: true
42626
+ };
42627
+ }
42628
+ function pyramidChart(titleText, source, opts = {}) {
42629
+ return {
42630
+ type: "pyramid",
42631
+ title: { text: titleText },
42632
+ dataSource: source,
42633
+ dataSetStyles: {},
42634
+ legendPosition: opts.legendPosition ?? "top",
42635
+ stacked: false,
42636
+ horizontal: true,
42637
+ humanize: true
42638
+ };
42639
+ }
42640
+ function comboChart(titleText, source, opts = {}) {
42641
+ return {
42642
+ ...opts,
42643
+ type: "combo",
42644
+ title: { text: titleText },
42645
+ dataSource: source,
42646
+ dataSetStyles: {},
42647
+ legendPosition: "top",
42648
+ humanize: true
42649
+ };
42650
+ }
42651
+ function bubbleChart(titleText, xRange, yRanges, sizeRange, labelRange, dataSetsHaveTitle) {
42652
+ return {
42653
+ type: "bubble",
42654
+ title: { text: titleText },
42655
+ humanize: true,
42656
+ dataSetsHaveTitle,
42657
+ yRanges,
42658
+ xRange,
42659
+ sizeRange,
42660
+ labelRange,
42661
+ labelsAsText: false,
42662
+ legendPosition: "none",
42663
+ bubbleColor: { color: "multiple" },
42664
+ verticalAxisPosition: "left"
42665
+ };
42666
+ }
42667
+ function scorecardChart(titleText, keyValue, opts = {}) {
42668
+ return {
42669
+ ...opts,
42670
+ type: "scorecard",
42671
+ title: { text: titleText },
42672
+ keyValue,
42673
+ baselineMode: opts.baselineMode ?? "difference",
42674
+ baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
42675
+ baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
42676
+ humanize: opts.humanize ?? true
42677
+ };
42678
+ }
42679
+ function gaugeChart(titleText, dataRange, rangeMin, rangeMax) {
42680
+ return {
42681
+ type: "gauge",
42682
+ title: { text: titleText },
42683
+ dataRange,
42684
+ sectionRule: {
42685
+ colors: {
42686
+ lowerColor: DEFAULT_GAUGE_LOWER_COLOR,
42687
+ middleColor: DEFAULT_GAUGE_MIDDLE_COLOR,
42688
+ upperColor: DEFAULT_GAUGE_UPPER_COLOR
42689
+ },
42690
+ rangeMin,
42691
+ rangeMax,
42692
+ lowerInflectionPoint: {
42693
+ type: "percentage",
42694
+ value: "33",
42695
+ operator: "<="
42696
+ },
42697
+ upperInflectionPoint: {
42698
+ type: "percentage",
42699
+ value: "66",
42700
+ operator: "<="
42701
+ }
42702
+ },
42703
+ humanize: true
42704
+ };
42705
+ }
42706
+ /** Pattern A — Single numeric column */
42707
+ const SINGLE_NUMBER_COLUMN_SUGGESTIONS = [
42708
+ {
42709
+ description: _t("Highlights the most recent value compared to the previous one."),
42710
+ isApplicable: ({ rowCount }) => rowCount < 3,
42711
+ build: (ctx) => scorecardChart(ctx.title, `=${ctx.lastCellXC}`, {
42712
+ baseline: ctx.prevCellXC ? `=${ctx.prevCellXC}` : void 0,
42713
+ baselineMode: "difference"
42714
+ })
42715
+ },
42716
+ {
42717
+ description: _t("Shows the position of the last value within the data's min-max range."),
42718
+ isApplicable: ({ rowCount }) => rowCount === 3,
42719
+ build: (ctx) => gaugeChart(ctx.title, ctx.lastCellXC, `=${ctx.firstCellXC}`, `=${ctx.prevCellXC}`)
42720
+ },
42721
+ {
42722
+ description: _t("Compares individual values side-by-side."),
42723
+ isApplicable: ({ rowCount }) => rowCount > 1,
42724
+ build: (ctx) => barChart(ctx.title, ctx.source)
42725
+ },
42726
+ {
42727
+ description: _t("Shows the evolution of all values over the range."),
42728
+ isApplicable: ({ rowCount }) => rowCount > 2,
42729
+ build: (ctx) => lineChart(ctx.title, ctx.source)
42730
+ },
42731
+ {
42732
+ description: _t("Emphasizes total accumulation over the range."),
42733
+ isApplicable: ({ rowCount }) => rowCount > 2,
42734
+ build: (ctx) => lineChart(ctx.title, ctx.source, {
42735
+ fillArea: true,
42736
+ cumulative: true
42737
+ })
42738
+ }
42739
+ ];
42740
+ /** Pattern B — Single percentage column */
42741
+ const SINGLE_PERCENTAGE_COLUMN_SUGGESTIONS = [
42742
+ {
42743
+ description: _t("Shows the last percentage value with its baseline."),
42744
+ isApplicable: ({ rowCount }) => rowCount < 3,
42745
+ build: (ctx) => scorecardChart(ctx.title, `=${ctx.lastCellXC}`, {
42746
+ baseline: ctx.prevCellXC ? `=${ctx.prevCellXC}` : void 0,
42747
+ baselineMode: "percentage"
42748
+ })
42749
+ },
42750
+ {
42751
+ description: _t("Natural fit for a 0–100% range."),
42752
+ isApplicable: ({ rowCount }) => rowCount === 1,
42753
+ build: (ctx) => gaugeChart(ctx.title, ctx.lastCellXC, "0", ctx.isAboveOne ? "100" : "1")
42754
+ },
42755
+ {
42756
+ description: _t("Natural fit for a 0–100% range."),
42757
+ isApplicable: ({ rowCount }) => rowCount === 3,
42758
+ build: (ctx) => gaugeChart(ctx.title, ctx.lastCellXC, `=${ctx.firstCellXC}`, `=${ctx.prevCellXC}`)
42759
+ },
42760
+ {
42761
+ description: _t("Shows completion against total."),
42762
+ isApplicable: ({ rowCount }) => rowCount > 1,
42763
+ build: (ctx) => pieChart(ctx.title, ctx.source, { isDoughnut: true })
42764
+ },
42765
+ {
42766
+ description: _t("Compares all percentage values side-by-side."),
42767
+ isApplicable: ({ rowCount }) => rowCount > 1,
42768
+ build: (ctx) => barChart(ctx.title, ctx.source)
42769
+ }
42770
+ ];
42771
+ /** Pattern C — Single date column */
42772
+ const SINGLE_DATE_COLUMN_SUGGESTIONS = [{
42773
+ description: _t("Shows the last date value."),
42774
+ isApplicable: ({ rowCount }) => rowCount === 1,
42775
+ build: (ctx) => scorecardChart(ctx.title, `=${ctx.lastCellXC}`)
42776
+ }];
42777
+ /** Pattern D — Single categorical column */
42778
+ const SINGLE_CATEGORICAL_COLUMN_SUGGESTIONS = [
42779
+ {
42780
+ description: _t("Shows the share of each category."),
42781
+ build: (ctx) => pieChart(ctx.title, ctx.source, {
42782
+ aggregated: true,
42783
+ legendPosition: "top"
42784
+ })
42785
+ },
42786
+ {
42787
+ description: _t("Same as pie, cleaner proportional look."),
42788
+ build: (ctx) => pieChart(ctx.title, ctx.source, {
42789
+ aggregated: true,
42790
+ isDoughnut: true,
42791
+ legendPosition: "top"
42792
+ })
42793
+ },
42794
+ {
42795
+ description: _t("Absolute count per category."),
42796
+ build: (ctx) => barChart(ctx.title, {
42797
+ ...ctx.source,
42798
+ labelRange: ctx.range
42799
+ }, {
42800
+ legendPosition: "none",
42801
+ aggregated: true
42802
+ })
42803
+ }
42804
+ ];
42805
+ /** Pattern E — Single label column */
42806
+ const SINGLE_LABEL_COLUMN_SUGGESTIONS = [{
42807
+ description: _t("Displays a key performance indicator."),
42808
+ isApplicable: ({ rowCount }) => rowCount === 1,
42809
+ build: (ctx) => scorecardChart(ctx.title, `=${ctx.lastCellXC}`, {
42810
+ baselineMode: "text",
42811
+ humanize: false
42812
+ })
42813
+ }];
42814
+ /** Pattern F — Categorical + Number */
42815
+ const CATEGORICAL_VS_NUMBER_SUGGESTIONS = [
42816
+ {
42817
+ description: _t("Classic category-vs-value comparison."),
42818
+ build: (ctx) => barChart(ctx.title, ctx.source, { aggregated: true })
42819
+ },
42820
+ {
42821
+ description: _t("Better when category labels are long."),
42822
+ build: (ctx) => barChart(ctx.title, ctx.source, {
42823
+ horizontal: true,
42824
+ aggregated: true
42825
+ })
42826
+ },
42827
+ {
42828
+ description: _t("Share of total per category."),
42829
+ build: (ctx) => pieChart(ctx.title, ctx.source, {
42830
+ legendPosition: "top",
42831
+ aggregated: true
42832
+ })
42833
+ },
42834
+ {
42835
+ description: _t("Proportional area — good for many categories."),
42836
+ build: (ctx) => treemapChart(ctx.title, ctx.treemapSource)
42837
+ }
42838
+ ];
42839
+ /** Patterns G & J — Date + Number or Date + Percentage */
42840
+ const DATE_VS_SERIES_SUGGESTIONS = [
42841
+ {
42842
+ description: _t("Best for visualizing time-series trends."),
42843
+ build: (ctx) => lineChart(ctx.title, ctx.source)
42844
+ },
42845
+ {
42846
+ description: _t("Emphasizes total volume over time."),
42847
+ build: (ctx) => lineChart(ctx.title, ctx.source, {
42848
+ fillArea: true,
42849
+ cumulative: true
42850
+ })
42851
+ },
42852
+ {
42853
+ description: _t("Period-by-period comparison."),
42854
+ build: (ctx) => barChart(ctx.title, ctx.source)
42855
+ },
42856
+ {
42857
+ description: _t("Shows intensity variation across days of the year."),
42858
+ isApplicable: ({ isPercentage }) => !isPercentage,
42859
+ build: (ctx) => calendarChart(ctx.title, ctx.source)
42860
+ }
42861
+ ];
42862
+ /** Pattern H — Number + Number */
42863
+ const NUMBER_VS_NUMBER_SUGGESTIONS = [
42864
+ {
42865
+ description: _t("Highlights the second metric compared to the first one."),
42866
+ isApplicable: ({ rowCount1, rowCount2 }) => rowCount1 === 1 && rowCount2 === 1,
42867
+ build: (ctx) => scorecardChart(ctx.title, `=${ctx.lastCellXC}`, {
42868
+ baseline: ctx.prevCellXC ? `=${ctx.prevCellXC}` : void 0,
42869
+ baselineMode: "difference"
42870
+ })
42871
+ },
42872
+ {
42873
+ description: _t("Side-by-side comparison of two numeric series."),
42874
+ build: (ctx) => barChart(ctx.title, ctx.sourceBoth, { legendPosition: "top" })
42875
+ },
42876
+ {
42877
+ description: _t("Reveals correlation between two numeric variables."),
42878
+ isApplicable: ({ rowCount1 }) => rowCount1 > 2,
42879
+ build: (ctx) => scatterChart(ctx.title, ctx.source2)
42880
+ },
42881
+ {
42882
+ description: _t("Bar for the first series, line for the second — good for mixed scales."),
42883
+ isApplicable: ({ rowCount1 }) => rowCount1 > 2,
42884
+ build: (ctx) => comboChart(ctx.title, ctx.sourceBoth)
42885
+ },
42886
+ {
42887
+ description: _t("When both metrics contribute to a total."),
42888
+ isApplicable: ({ rowCount1 }) => rowCount1 > 2,
42889
+ build: (ctx) => lineChart(ctx.title, ctx.sourceBoth, {
42890
+ stacked: true,
42891
+ fillArea: true,
42892
+ legendPosition: "top"
42893
+ })
42894
+ },
42895
+ {
42896
+ description: _t("Shape-based comparison when rows represent named entities."),
42897
+ isApplicable: ({ rowCount1 }) => rowCount1 > 2 && rowCount1 <= MAX_ROWS_FOR_SHAPE_CHART,
42898
+ build: (ctx) => radarChart(ctx.title, ctx.sourceBoth, { legendPosition: "top" })
42899
+ }
42900
+ ];
42901
+ /** Pattern I — Categorical + Percentage */
42902
+ const CATEGORICAL_VS_PERCENTAGE_SUGGESTIONS = [
42903
+ {
42904
+ description: _t("Vertical comparison of rates per category."),
42905
+ build: (ctx) => barChart(ctx.title, ctx.source)
42906
+ },
42907
+ {
42908
+ description: _t("Progress-bar style per category."),
42909
+ build: (ctx) => barChart(ctx.title, ctx.source, { horizontal: true })
42910
+ },
42911
+ {
42912
+ description: _t("Share of total percentage across categories."),
42913
+ build: (ctx) => pieChart(ctx.title, ctx.source, { legendPosition: "top" })
42914
+ },
42915
+ {
42916
+ description: _t("Comparison of completion rates across categories."),
42917
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
42918
+ build: (ctx) => radarChart(ctx.title, ctx.source)
42919
+ }
42920
+ ];
42921
+ /** Pattern K — Label + Number */
42922
+ const LABEL_VS_NUMBER_SUGGESTIONS = [
42923
+ {
42924
+ description: _t("Highlights the most recent value for the named entity."),
42925
+ isApplicable: ({ rowCount }) => rowCount === 1,
42926
+ build: (ctx) => scorecardChart("", `=${ctx.lastCellXC}`, {
42927
+ humanize: false,
42928
+ baselineMode: "text",
42929
+ baseline: ctx.prevCellXC ? `=${ctx.prevCellXC}` : void 0
42930
+ })
42931
+ },
42932
+ {
42933
+ description: _t("Vertical comparison across named items."),
42934
+ isApplicable: ({ rowCount }) => rowCount > 1,
42935
+ build: (ctx) => barChart(ctx.title, ctx.source)
42936
+ },
42937
+ {
42938
+ description: _t("Works well for named entities with long labels."),
42939
+ isApplicable: ({ rowCount }) => rowCount > 1,
42940
+ build: (ctx) => barChart(ctx.title, ctx.source, { horizontal: true })
42941
+ },
42942
+ {
42943
+ description: _t("Share of total per category."),
42944
+ isApplicable: ({ rowCount }) => rowCount > 1 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
42945
+ build: (ctx) => pieChart(ctx.title, ctx.source, {
42946
+ legendPosition: "top",
42947
+ aggregated: true
42948
+ })
42949
+ },
42950
+ {
42951
+ description: _t("Shape-based comparison across labeled items."),
42952
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
42953
+ build: (ctx) => radarChart(ctx.title, ctx.source)
42954
+ }
42955
+ ];
42956
+ /** Pattern M — Categorical + Multiple Numbers */
42957
+ const CATEGORICAL_VS_MULTIPLE_NUMBERS_SUGGESTIONS = [
42958
+ {
42959
+ description: _t("Side-by-side comparison across categories for each series."),
42960
+ build: (ctx) => barChart(ctx.title, ctx.source, { legendPosition: "top" })
42961
+ },
42962
+ {
42963
+ description: _t("Shows composition and total per category."),
42964
+ build: (ctx) => barChart(ctx.title, ctx.source, {
42965
+ stacked: true,
42966
+ legendPosition: "top"
42967
+ })
42968
+ },
42969
+ {
42970
+ description: _t("Trend per series across categories."),
42971
+ build: (ctx) => lineChart(ctx.title, ctx.source, { legendPosition: "top" })
42972
+ },
42973
+ {
42974
+ description: _t("Volume and composition across categories."),
42975
+ build: (ctx) => lineChart(ctx.title, ctx.source, {
42976
+ stacked: true,
42977
+ fillArea: true,
42978
+ legendPosition: "top"
42979
+ })
42980
+ },
42981
+ {
42982
+ description: _t("Shape comparison across metrics (best for ≤ 10 rows)."),
42983
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
42984
+ build: (ctx) => radarChart(ctx.title, ctx.source, { legendPosition: "top" })
42985
+ }
42986
+ ];
42987
+ /** Pattern N — Date + Multiple Numbers */
42988
+ const DATE_VS_MULTIPLE_NUMBERS_SUGGESTIONS = [
42989
+ {
42990
+ description: _t("Trend comparison across multiple metrics over time."),
42991
+ build: (ctx) => lineChart(ctx.title, ctx.source, { legendPosition: "top" })
42992
+ },
42993
+ {
42994
+ description: _t("Volume composition over time."),
42995
+ build: (ctx) => lineChart(ctx.title, ctx.source, {
42996
+ stacked: true,
42997
+ fillArea: true,
42998
+ legendPosition: "top"
42999
+ })
43000
+ },
43001
+ {
43002
+ description: _t("Bar for the primary metric, line for the others."),
43003
+ build: (ctx) => comboChart(ctx.title, ctx.source)
43004
+ },
43005
+ {
43006
+ description: _t("Period-by-period grouped comparison."),
43007
+ build: (ctx) => barChart(ctx.title, ctx.source, { legendPosition: "top" })
43008
+ }
43009
+ ];
43010
+ /** Pattern O — Categorical + Categorical + Number */
43011
+ const MULTIPLE_CATEGORICALS_VS_NUMBER_SUGGESTIONS = [
43012
+ {
43013
+ description: _t("Two-level hierarchy weighted by value."),
43014
+ build: (ctx) => sunburstChart(ctx.title, ctx.hierarchySource)
43015
+ },
43016
+ {
43017
+ description: _t("Proportional nested area weighted by value."),
43018
+ build: (ctx) => treemapChart(ctx.title, ctx.hierarchySource)
43019
+ },
43020
+ {
43021
+ description: _t("One series per inner category, grouped by outer category."),
43022
+ build: (ctx) => barChart(ctx.title, ctx.barSource, { legendPosition: "top" })
43023
+ },
43024
+ {
43025
+ description: _t("Contribution of inner categories per outer category."),
43026
+ build: (ctx) => barChart(ctx.title, ctx.barSource, {
43027
+ stacked: true,
43028
+ legendPosition: "top"
43029
+ })
43030
+ }
43031
+ ];
43032
+ /** Pattern P — Categorical + Date + Number */
43033
+ const CATEGORICAL_DATE_NUMBER_SUGGESTIONS = [
43034
+ {
43035
+ description: _t("Trend of values over time."),
43036
+ build: (ctx) => lineChart(ctx.title, ctx.sourceByDate, { legendPosition: "top" })
43037
+ },
43038
+ {
43039
+ description: _t("Volume contribution over time."),
43040
+ build: (ctx) => lineChart(ctx.title, ctx.sourceByDate, {
43041
+ stacked: true,
43042
+ fillArea: true,
43043
+ legendPosition: "top"
43044
+ })
43045
+ },
43046
+ {
43047
+ description: _t("Period × category side-by-side comparison."),
43048
+ build: (ctx) => barChart(ctx.title, ctx.sourceByCat, { legendPosition: "top" })
43049
+ },
43050
+ {
43051
+ description: _t("Composition per period over time."),
43052
+ build: (ctx) => barChart(ctx.title, ctx.sourceByDate, {
43053
+ stacked: true,
43054
+ legendPosition: "top"
43055
+ })
43056
+ }
43057
+ ];
43058
+ /** Pattern Q — Label + Multiple Numbers */
43059
+ const LABEL_VS_MULTIPLE_NUMBERS_SUGGESTIONS = [
43060
+ {
43061
+ description: _t("Side-by-side per entity — works well for long labels."),
43062
+ build: (ctx) => barChart(ctx.title, ctx.source, {
43063
+ horizontal: true,
43064
+ legendPosition: "top"
43065
+ })
43066
+ },
43067
+ {
43068
+ description: _t("Grouped bars per entity for direct metric comparison."),
43069
+ build: (ctx) => barChart(ctx.title, ctx.source, { legendPosition: "top" })
43070
+ },
43071
+ {
43072
+ description: _t("Shape/profile comparison across metrics for each entity."),
43073
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
43074
+ build: (ctx) => radarChart(ctx.title, ctx.source, { legendPosition: "top" })
43075
+ },
43076
+ {
43077
+ description: _t("Correlation between the two numeric metrics across entities."),
43078
+ isApplicable: ({ rowCount, numColsCount }) => rowCount > 2 && numColsCount === 2,
43079
+ build: (ctx) => scatterChart(ctx.title, ctx.scatterSource)
43080
+ },
43081
+ {
43082
+ description: _t("Three metrics in one view: X position, Y position and bubble size."),
43083
+ isApplicable: ({ numColsCount }) => numColsCount === 3,
43084
+ build: (ctx) => {
43085
+ const bubble = ctx.bubble;
43086
+ return bubbleChart(ctx.title, bubble.xRange, bubble.yRanges, bubble.sizeRange, bubble.labelRange, bubble.hasTitle);
43087
+ }
43088
+ }
43089
+ ];
43090
+ /** Pattern R — Categorical + Two Numbers (Population Pyramid or Grouped) */
43091
+ const CATEGORICAL_TWO_NUMBERS_SUGGESTIONS = [
43092
+ {
43093
+ description: _t("Natural fit for symmetric or opposing values per category."),
43094
+ isApplicable: ({ isPyramid }) => isPyramid,
43095
+ build: (ctx) => pyramidChart(ctx.title, ctx.sourceBoth)
43096
+ },
43097
+ {
43098
+ description: _t("Side-by-side comparison of both metrics per category."),
43099
+ build: (ctx) => barChart(ctx.title, ctx.sourceBoth, {
43100
+ legendPosition: "top",
43101
+ aggregated: true
43102
+ })
43103
+ },
43104
+ {
43105
+ description: _t("Bar for the first series, line for the second — good for mixed scales."),
43106
+ build: (ctx) => comboChart(ctx.title, ctx.sourceBoth, { aggregated: true })
43107
+ },
43108
+ {
43109
+ description: _t("Shows total and composition per category."),
43110
+ isApplicable: ({ isPyramid }) => !isPyramid,
43111
+ build: (ctx) => barChart(ctx.title, ctx.sourceBoth, {
43112
+ stacked: true,
43113
+ legendPosition: "top",
43114
+ aggregated: true
43115
+ })
43116
+ }
43117
+ ];
43118
+ /** Pattern S — Many Numbers (3+ numeric columns, no categorical/date) */
43119
+ const MANY_NUMBERS_SUGGESTIONS = [
43120
+ {
43121
+ description: _t("Shows the position of the last value within the data's min-max range."),
43122
+ isApplicable: ({ colsLength, rowCount }) => colsLength === 3 && rowCount === 1,
43123
+ build: (ctx) => gaugeChart(ctx.title, ctx.lastCellXC, `=${ctx.firstCellXC}`, `=${ctx.secondCellXC}`)
43124
+ },
43125
+ {
43126
+ description: _t("Trend comparison across all metrics."),
43127
+ isApplicable: ({ rowCount }) => rowCount > 2,
43128
+ build: (ctx) => lineChart(ctx.title, ctx.source, { legendPosition: "top" })
43129
+ },
43130
+ {
43131
+ description: _t("Overall shape/profile across all metrics."),
43132
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
43133
+ build: (ctx) => radarChart(ctx.title, ctx.source, { legendPosition: "top" })
43134
+ },
43135
+ {
43136
+ description: _t("Side-by-side comparison of all numeric metrics."),
43137
+ build: (ctx) => barChart(ctx.title, ctx.source, { legendPosition: "top" })
43138
+ },
43139
+ {
43140
+ description: _t("Side-by-side comparison of all numeric metrics."),
43141
+ build: (ctx) => barChart(ctx.title, ctx.source, {
43142
+ legendPosition: "top",
43143
+ stacked: true
43144
+ })
43145
+ }
43146
+ ];
43147
+
43148
+ //#endregion
43149
+ //#region src/helpers/figures/charts/chart_suggestion_engine.ts
43150
+ const PYRAMID_HEADER_KEYWORD_PATTERNS = [
43151
+ "male",
43152
+ "female",
43153
+ "man",
43154
+ "woman",
43155
+ "boy",
43156
+ "girl",
43157
+ "before",
43158
+ "after",
43159
+ "previous",
43160
+ "current",
43161
+ "positive",
43162
+ "negative",
43163
+ "gain",
43164
+ "loss",
43165
+ "income",
43166
+ "expense",
43167
+ "revenue",
43168
+ "cost"
43169
+ ].map((keyword) => new RegExp(`\\b${keyword}\\b`, "i"));
43170
+ function matchesColumnType(spec, type) {
43171
+ return Array.isArray(spec) ? spec.includes(type) : spec === type;
43172
+ }
43173
+ function matchesShape(pattern, shape) {
43174
+ const { leadingTypes, extension } = pattern;
43175
+ const expectedLength = leadingTypes.length + (extension?.minSize ?? 0);
43176
+ if (extension ? shape.length < expectedLength : shape.length !== expectedLength) return false;
43177
+ return shape.every((type, i) => {
43178
+ const spec = i < leadingTypes.length ? leadingTypes[i] : extension?.type;
43179
+ return spec !== void 0 && matchesColumnType(spec, type);
43180
+ });
43181
+ }
43182
+ function isPyramidLike(numCol1, numCol2) {
43183
+ const h1 = numCol1.header ?? "";
43184
+ const h2 = numCol2.header ?? "";
43185
+ return PYRAMID_HEADER_KEYWORD_PATTERNS.some((re) => re.test(h1) || re.test(h2));
43186
+ }
43187
+ /** Returns the first and last cell XC coordinates, and the second-to-last cell XC coordinate if it exists. */
43188
+ function interestingCellsXc(col) {
43189
+ if (!col) return {
43190
+ firstCellXC: "",
43191
+ lastCellXC: "",
43192
+ prevCellXC: void 0
43193
+ };
43194
+ const firstCellPosition = col.nonEmpty.at(0)?.position;
43195
+ const firstCellXC = firstCellPosition ? toXC(firstCellPosition?.col, firstCellPosition?.row) : "";
43196
+ const lastCellPosition = col.nonEmpty.at(-1)?.position;
43197
+ const lastCellXC = lastCellPosition ? toXC(lastCellPosition?.col, lastCellPosition?.row) : "";
43198
+ const prevCellPosition = col.nonEmpty.at(-2)?.position;
43199
+ return {
43200
+ firstCellXC,
43201
+ lastCellXC,
43202
+ prevCellXC: prevCellPosition ? toXC(prevCellPosition?.col, prevCellPosition?.row) : void 0
43203
+ };
43204
+ }
43205
+ /** Pattern A — Single numeric column */
43206
+ function buildSingleNumberContext([col], getters) {
43207
+ const title = col.header ?? _t("Value");
43208
+ const { firstCellXC, lastCellXC, prevCellXC } = interestingCellsXc(col);
43209
+ return {
43210
+ title,
43211
+ source: rangeSource([dataset(col.zone, getters)], col.hasHeader),
43212
+ rowCount: col.rowCount,
43213
+ firstCellXC,
43214
+ lastCellXC,
43215
+ prevCellXC
43216
+ };
43217
+ }
43218
+ /** Pattern B — Single percentage column */
43219
+ function buildSinglePercentageContext([col], getters) {
43220
+ const hasTitle = col.hasHeader;
43221
+ const title = col.header ?? _t("Rate");
43222
+ const { firstCellXC, lastCellXC, prevCellXC } = interestingCellsXc(col);
43223
+ const source = rangeSource([dataset(col.zone, getters)], hasTitle);
43224
+ const isAboveOne = (col.maxValue ?? 0) > 1;
43225
+ return {
43226
+ title,
43227
+ source,
43228
+ rowCount: col.rowCount,
43229
+ firstCellXC,
43230
+ lastCellXC,
43231
+ prevCellXC,
43232
+ isAboveOne
43233
+ };
43234
+ }
43235
+ /** Pattern C — Single date column */
43236
+ function buildSingleDateContext([col]) {
43237
+ const { lastCellXC } = interestingCellsXc(col);
43238
+ return {
43239
+ title: col.header ?? _t("Date"),
43240
+ lastCellXC,
43241
+ rowCount: col.rowCount
43242
+ };
43243
+ }
43244
+ /** Pattern D — Single categorical column */
43245
+ function buildSingleCategoricalContext([col], getters) {
43246
+ const hasTitle = col.hasHeader;
43247
+ const title = col.header ?? _t("Category");
43248
+ const range = getUnboundRange(getters, col.zone);
43249
+ return {
43250
+ title,
43251
+ source: rangeSource([dataset(col.zone, getters)], hasTitle, range),
43252
+ range
43253
+ };
43254
+ }
43255
+ /** Pattern E — Single label column */
43256
+ function buildSingleLabelContext([col]) {
43257
+ const title = col.header ?? _t("Label");
43258
+ const { lastCellXC } = interestingCellsXc(col);
43259
+ return {
43260
+ title,
43261
+ lastCellXC,
43262
+ rowCount: col.rowCount
43263
+ };
43264
+ }
43265
+ /** Pattern F — Categorical + Number */
43266
+ function buildCategoricalVsNumberContext([catCol, numCol], getters) {
43267
+ const labelRange = getUnboundRange(getters, catCol.zone);
43268
+ const hasTitle = numCol.hasHeader;
43269
+ return {
43270
+ title: numCol.header ? _t("%(numberHeader)s by %(categoryHeader)s", {
43271
+ numberHeader: numCol.header,
43272
+ categoryHeader: catCol.header ?? _t("Category")
43273
+ }) : _t("By Category"),
43274
+ source: rangeSource([dataset(numCol.zone, getters)], hasTitle, labelRange),
43275
+ treemapSource: rangeSource([dataset(catCol.zone, getters)], hasTitle, getUnboundRange(getters, numCol.zone))
43276
+ };
43277
+ }
43278
+ /** Patterns G & J — Date + Number or Date + Percentage */
43279
+ function buildDateVsSeriesContext([dateCol, seriesCol], getters) {
43280
+ const isPercentage = seriesCol.type === "percentage";
43281
+ const labelRange = getUnboundRange(getters, dateCol.zone);
43282
+ const hasTitle = isDatasetTitled(getters, dateCol.zone);
43283
+ return {
43284
+ title: seriesCol.header ? _t("%(seriesHeader)s over time", { seriesHeader: seriesCol.header }) : isPercentage ? _t("Rate over Time") : _t("Over Time"),
43285
+ source: rangeSource([dataset(seriesCol.zone, getters)], hasTitle, labelRange),
43286
+ isPercentage
43287
+ };
43288
+ }
43289
+ /** Pattern H — Number + Number */
43290
+ function buildNumberVsNumberContext([col1, col2], getters) {
43291
+ const title = col1.header && col2.header ? _t("%(col2Header)s vs %(col1Header)s", {
43292
+ col2Header: col2.header,
43293
+ col1Header: col1.header
43294
+ }) : _t("Correlation");
43295
+ const hasTitle = col1.hasHeader || col2.hasHeader;
43296
+ const source2 = rangeSource([dataset(col2.zone, getters)], col2.hasHeader, getUnboundRange(getters, col1.zone));
43297
+ const sourceBoth = rangeSource([dataset(col1.zone, getters, "0"), dataset(col2.zone, getters, "1")], hasTitle);
43298
+ const { lastCellXC } = interestingCellsXc(col2);
43299
+ const { lastCellXC: prevCellXC } = interestingCellsXc(col1);
43300
+ return {
43301
+ title,
43302
+ source2,
43303
+ sourceBoth,
43304
+ rowCount1: col1.rowCount,
43305
+ rowCount2: col2.rowCount,
43306
+ lastCellXC,
43307
+ prevCellXC
43308
+ };
43309
+ }
43310
+ /** Pattern I — Categorical + Percentage */
43311
+ function buildCategoricalVsPercentageContext([catCol, pctCol], getters) {
43312
+ const labelRange = getUnboundRange(getters, catCol.zone);
43313
+ const hasTitle = pctCol.hasHeader;
43314
+ return {
43315
+ title: pctCol.header ? _t("%(percentageHeader)s by %(categoryHeader)s", {
43316
+ percentageHeader: pctCol.header,
43317
+ categoryHeader: catCol.header ?? _t("Category")
43318
+ }) : _t("Rates by Category"),
43319
+ source: rangeSource([dataset(pctCol.zone, getters)], hasTitle, labelRange),
43320
+ rowCount: catCol.rowCount
43321
+ };
43322
+ }
43323
+ /** Pattern K — Label + Number */
43324
+ function buildLabelVsNumberContext([labelCol, numCol], getters) {
43325
+ const labelRange = getUnboundRange(getters, labelCol.zone);
43326
+ const hasTitle = numCol.hasHeader;
43327
+ const title = numCol.header ? _t("%(numberHeader)s by %(labelHeader)s", {
43328
+ numberHeader: numCol.header,
43329
+ labelHeader: labelCol.header ?? _t("Name")
43330
+ }) : _t("By Name");
43331
+ const source = rangeSource([dataset(numCol.zone, getters)], hasTitle, labelRange);
43332
+ const { lastCellXC } = interestingCellsXc(numCol);
43333
+ const { lastCellXC: prevCellXC } = interestingCellsXc(labelCol);
43334
+ return {
43335
+ title,
43336
+ source,
43337
+ rowCount: labelCol.rowCount,
43338
+ lastCellXC,
43339
+ prevCellXC
43340
+ };
43341
+ }
43342
+ /** Pattern M — Categorical + Multiple Numbers */
43343
+ function buildCategoricalVsMultipleNumbersContext([catCol, ...numCols], getters) {
43344
+ const labelRange = getUnboundRange(getters, catCol.zone);
43345
+ const hasTitle = numCols.some((c) => c.hasHeader);
43346
+ return {
43347
+ title: catCol.header ? _t("By %(header)s", { header: catCol.header }) : _t("Multi-series"),
43348
+ source: rangeSource(numCols.map((c, i) => dataset(c.zone, getters, String(i))), hasTitle, labelRange),
43349
+ rowCount: catCol.rowCount
43350
+ };
43351
+ }
43352
+ /** Pattern N — Date + Multiple Numbers */
43353
+ function buildDateVsMultipleNumbersContext([dateCol, ...numCols], getters) {
43354
+ const labelRange = getUnboundRange(getters, dateCol.zone);
43355
+ const hasTitle = numCols.some((c) => c.hasHeader) || isDatasetTitled(getters, dateCol.zone);
43356
+ return {
43357
+ title: numCols.length === 1 && numCols[0].header ? _t("%(header)s over time", { header: numCols[0].header }) : _t("Multi-series over Time"),
43358
+ source: rangeSource(numCols.map((c, i) => dataset(c.zone, getters, String(i))), hasTitle, labelRange)
43359
+ };
43360
+ }
43361
+ /** Pattern O — Categorical + Categorical + Number */
43362
+ function buildMultipleCategoricalsVsNumberContext([cat1, cat2, numCol], getters) {
43363
+ const title = numCol.header ? _t("%(numHeader)s by %(cat1Header)s and %(cat2Header)s", {
43364
+ numHeader: numCol.header,
43365
+ cat1Header: cat1.header ?? _t("Level 1"),
43366
+ cat2Header: cat2.header ?? _t("Level 2")
43367
+ }) : _t("Two-level hierarchy");
43368
+ const hasTitle = numCol.hasHeader;
43369
+ return {
43370
+ title,
43371
+ hierarchySource: rangeSource([dataset(cat1.zone, getters, "0"), dataset(cat2.zone, getters, "1")], hasTitle, getUnboundRange(getters, numCol.zone)),
43372
+ barSource: rangeSource([dataset(numCol.zone, getters)], hasTitle, getUnboundRange(getters, cat1.zone))
43373
+ };
43374
+ }
43375
+ /** Pattern P — Categorical + Date + Number */
43376
+ function buildCategoricalDateNumberContext([catCol, dateCol, numCol], getters) {
43377
+ const dateRange = getUnboundRange(getters, dateCol.zone);
43378
+ const catRange = getUnboundRange(getters, catCol.zone);
43379
+ const hasTitle = numCol.hasHeader;
43380
+ return {
43381
+ title: numCol.header ? _t("%(numHeader)s by %(catHeader)s over %(dateHeader)s", {
43382
+ numHeader: numCol.header,
43383
+ catHeader: catCol.header ?? _t("Category"),
43384
+ dateHeader: dateCol.header ?? _t("Time")
43385
+ }) : _t("Multi-series over Time"),
43386
+ sourceByDate: rangeSource([dataset(numCol.zone, getters)], hasTitle, dateRange),
43387
+ sourceByCat: rangeSource([dataset(numCol.zone, getters)], hasTitle, catRange)
43388
+ };
43389
+ }
43390
+ /** Pattern Q — Label + Multiple Numbers */
43391
+ function buildLabelVsMultipleNumbersContext([labelCol, ...numCols], getters) {
43392
+ const labelRange = getUnboundRange(getters, labelCol.zone);
43393
+ const hasTitle = numCols.some((c) => c.hasHeader);
43394
+ const title = labelCol.header ? _t("By %(header)s", { header: labelCol.header }) : _t("Profile Comparison");
43395
+ const source = rangeSource(numCols.map((c, i) => dataset(c.zone, getters, String(i))), hasTitle, labelRange);
43396
+ const scatterSource = rangeSource([dataset(numCols[1].zone, getters)], numCols[1].hasHeader, getUnboundRange(getters, numCols[0].zone));
43397
+ const bubble = numCols.length === 3 ? {
43398
+ xRange: getUnboundRange(getters, numCols[0].zone),
43399
+ yRanges: [getUnboundRange(getters, numCols[1].zone)],
43400
+ sizeRange: getUnboundRange(getters, numCols[2].zone),
43401
+ labelRange,
43402
+ hasTitle
43403
+ } : void 0;
43404
+ return {
43405
+ title,
43406
+ source,
43407
+ rowCount: labelCol.rowCount,
43408
+ numColsCount: numCols.length,
43409
+ scatterSource,
43410
+ bubble
43411
+ };
43412
+ }
43413
+ /** Pattern R — Categorical + Two Numbers (Population Pyramid or Grouped) */
43414
+ function buildCategoricalTwoNumbersContext([catCol, numCol1, numCol2], getters) {
43415
+ const catRange = getUnboundRange(getters, catCol.zone);
43416
+ const hasTitle = numCol1.hasHeader || numCol2.hasHeader;
43417
+ const title = numCol1.header && numCol2.header ? _t("%(num1Header)s vs %(num2Header)s by %(catHeader)s", {
43418
+ num1Header: numCol1.header,
43419
+ num2Header: numCol2.header,
43420
+ catHeader: catCol.header ?? _t("Category")
43421
+ }) : catCol.header ? _t("By %(header)s", { header: catCol.header }) : _t("Category Comparison");
43422
+ const isPyramid = isPyramidLike(numCol1, numCol2);
43423
+ return {
43424
+ title,
43425
+ sourceBoth: rangeSource([dataset(numCol1.zone, getters, "0"), dataset(numCol2.zone, getters, "1")], hasTitle, catRange),
43426
+ isPyramid
43427
+ };
43428
+ }
43429
+ /** Pattern S — Many Numbers (3+ numeric columns, no categorical/date) */
43430
+ function buildManyNumbersContext(cols, getters) {
43431
+ const title = cols.every((c) => c.hasHeader) ? cols.map((c) => c.header).join(" / ") : _t("KPI Overview");
43432
+ const hasTitle = cols.some((c) => c.hasHeader);
43433
+ const source = rangeSource(cols.map((c, i) => dataset(c.zone, getters, String(i))), hasTitle);
43434
+ const { lastCellXC } = interestingCellsXc(cols[2]);
43435
+ const { lastCellXC: firstCellXC } = interestingCellsXc(cols[0]);
43436
+ const { lastCellXC: secondCellXC } = interestingCellsXc(cols[1]);
43437
+ return {
43438
+ title,
43439
+ source,
43440
+ colsLength: cols.length,
43441
+ rowCount: cols[0].rowCount,
43442
+ lastCellXC,
43443
+ firstCellXC,
43444
+ secondCellXC
43445
+ };
43446
+ }
43447
+ const NUMBER_OR_PERCENTAGE = ["number", "percentage"];
43448
+ const EXACT_PATTERNS = [
43449
+ {
43450
+ pattern: { leadingTypes: ["number"] },
43451
+ buildContext: buildSingleNumberContext,
43452
+ suggestions: SINGLE_NUMBER_COLUMN_SUGGESTIONS
43453
+ },
43454
+ {
43455
+ pattern: { leadingTypes: ["percentage"] },
43456
+ buildContext: buildSinglePercentageContext,
43457
+ suggestions: SINGLE_PERCENTAGE_COLUMN_SUGGESTIONS
43458
+ },
43459
+ {
43460
+ pattern: { leadingTypes: ["date"] },
43461
+ buildContext: buildSingleDateContext,
43462
+ suggestions: SINGLE_DATE_COLUMN_SUGGESTIONS
43463
+ },
43464
+ {
43465
+ pattern: { leadingTypes: ["categorical"] },
43466
+ buildContext: buildSingleCategoricalContext,
43467
+ suggestions: SINGLE_CATEGORICAL_COLUMN_SUGGESTIONS
43468
+ },
43469
+ {
43470
+ pattern: { leadingTypes: ["label"] },
43471
+ buildContext: buildSingleLabelContext,
43472
+ suggestions: SINGLE_LABEL_COLUMN_SUGGESTIONS
43473
+ },
43474
+ {
43475
+ pattern: { leadingTypes: ["categorical", "number"] },
43476
+ buildContext: buildCategoricalVsNumberContext,
43477
+ suggestions: CATEGORICAL_VS_NUMBER_SUGGESTIONS
43478
+ },
43479
+ {
43480
+ pattern: { leadingTypes: ["date", NUMBER_OR_PERCENTAGE] },
43481
+ buildContext: buildDateVsSeriesContext,
43482
+ suggestions: DATE_VS_SERIES_SUGGESTIONS
43483
+ },
43484
+ {
43485
+ pattern: { leadingTypes: ["number", "number"] },
43486
+ buildContext: buildNumberVsNumberContext,
43487
+ suggestions: NUMBER_VS_NUMBER_SUGGESTIONS
43488
+ },
43489
+ {
43490
+ pattern: { leadingTypes: ["categorical", "percentage"] },
43491
+ buildContext: buildCategoricalVsPercentageContext,
43492
+ suggestions: CATEGORICAL_VS_PERCENTAGE_SUGGESTIONS
43493
+ },
43494
+ {
43495
+ pattern: { leadingTypes: ["label", "number"] },
43496
+ buildContext: buildLabelVsNumberContext,
43497
+ suggestions: LABEL_VS_NUMBER_SUGGESTIONS
43498
+ },
43499
+ {
43500
+ pattern: { leadingTypes: ["label", "percentage"] },
43501
+ buildContext: buildCategoricalVsPercentageContext,
43502
+ suggestions: CATEGORICAL_VS_PERCENTAGE_SUGGESTIONS
43503
+ },
43504
+ {
43505
+ pattern: { leadingTypes: [
43506
+ "categorical",
43507
+ "categorical",
43508
+ "number"
43509
+ ] },
43510
+ buildContext: buildMultipleCategoricalsVsNumberContext,
43511
+ suggestions: MULTIPLE_CATEGORICALS_VS_NUMBER_SUGGESTIONS
43512
+ },
43513
+ {
43514
+ pattern: { leadingTypes: [
43515
+ "categorical",
43516
+ "label",
43517
+ "number"
43518
+ ] },
43519
+ buildContext: buildMultipleCategoricalsVsNumberContext,
43520
+ suggestions: MULTIPLE_CATEGORICALS_VS_NUMBER_SUGGESTIONS
43521
+ },
43522
+ {
43523
+ pattern: { leadingTypes: [
43524
+ "categorical",
43525
+ "date",
43526
+ NUMBER_OR_PERCENTAGE
43527
+ ] },
43528
+ buildContext: buildCategoricalDateNumberContext,
43529
+ suggestions: CATEGORICAL_DATE_NUMBER_SUGGESTIONS
43530
+ },
43531
+ {
43532
+ pattern: { leadingTypes: [
43533
+ "categorical",
43534
+ NUMBER_OR_PERCENTAGE,
43535
+ NUMBER_OR_PERCENTAGE
43536
+ ] },
43537
+ buildContext: buildCategoricalTwoNumbersContext,
43538
+ suggestions: CATEGORICAL_TWO_NUMBERS_SUGGESTIONS
43539
+ }
43540
+ ];
43541
+ const EXTENDABLE_PATTERNS = [
43542
+ {
43543
+ pattern: {
43544
+ leadingTypes: [],
43545
+ extension: {
43546
+ type: NUMBER_OR_PERCENTAGE,
43547
+ minSize: 3
43548
+ }
43549
+ },
43550
+ buildContext: buildManyNumbersContext,
43551
+ suggestions: MANY_NUMBERS_SUGGESTIONS
43552
+ },
43553
+ {
43554
+ pattern: {
43555
+ leadingTypes: ["label"],
43556
+ extension: {
43557
+ type: NUMBER_OR_PERCENTAGE,
43558
+ minSize: 2
43559
+ }
43560
+ },
43561
+ buildContext: buildLabelVsMultipleNumbersContext,
43562
+ suggestions: LABEL_VS_MULTIPLE_NUMBERS_SUGGESTIONS
43563
+ },
43564
+ {
43565
+ pattern: {
43566
+ leadingTypes: ["categorical"],
43567
+ extension: {
43568
+ type: NUMBER_OR_PERCENTAGE,
43569
+ minSize: 2
43570
+ }
43571
+ },
43572
+ buildContext: buildCategoricalVsMultipleNumbersContext,
43573
+ suggestions: CATEGORICAL_VS_MULTIPLE_NUMBERS_SUGGESTIONS
43574
+ },
43575
+ {
43576
+ pattern: {
43577
+ leadingTypes: ["date"],
43578
+ extension: {
43579
+ type: NUMBER_OR_PERCENTAGE,
43580
+ minSize: 2
43581
+ }
43582
+ },
43583
+ buildContext: buildDateVsMultipleNumbersContext,
43584
+ suggestions: DATE_VS_MULTIPLE_NUMBERS_SUGGESTIONS
43585
+ }
43586
+ ];
43587
+ function getChartSuggestions(zones, getters) {
43588
+ const cols = analyzeColumns(zones, getters);
43589
+ if (cols.some((c) => c.type === "error")) return [];
43590
+ const nonEmpty = cols.filter((c) => c.type !== "empty");
43591
+ if (!nonEmpty.length) return [];
43592
+ const shape = nonEmpty.map((c) => c.type);
43593
+ const rule = EXACT_PATTERNS.find((rule) => matchesShape(rule.pattern, shape)) ?? EXTENDABLE_PATTERNS.find((rule) => matchesShape(rule.pattern, shape));
43594
+ if (!rule) return [];
43595
+ const ctx = rule.buildContext(nonEmpty, getters);
43596
+ return rule.suggestions.filter((suggestion) => suggestion.isApplicable?.(ctx) ?? true).map((suggestion) => ({
43597
+ description: suggestion.description.toString(),
43598
+ definition: suggestion.build(ctx)
43599
+ }));
43600
+ }
43601
+
43602
+ //#endregion
43603
+ //#region src/components/side_panel/data_analysis/data_analysis_store.ts
43604
+ var DataAnalysisStore = class extends SpreadsheetStore {
43605
+ mutators = [];
43606
+ hasData = false;
43607
+ chartSuggestions = [];
43608
+ isDirty = false;
43609
+ ranges;
43610
+ constructor(get) {
43611
+ super(get);
43612
+ this.model.selection.observe(this, { handleEvent: () => this.refresh() });
43613
+ this.onDispose(() => {
43614
+ this.model.selection.unobserve(this);
43615
+ });
43616
+ this.refresh();
43617
+ }
43618
+ handle(cmd) {
43619
+ if (invalidateEvaluationCommands.has(cmd.type) || cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd)) this.isDirty = true;
43620
+ switch (cmd.type) {
43621
+ case "HIDE_COLUMNS_ROWS":
43622
+ case "UNHIDE_COLUMNS_ROWS":
43623
+ case "GROUP_HEADERS":
43624
+ case "UNGROUP_HEADERS":
43625
+ case "ACTIVATE_SHEET":
43626
+ case "ACTIVATE_NEXT_SHEET":
43627
+ case "ACTIVATE_PREVIOUS_SHEET":
43628
+ case "EVALUATE_CELLS":
43629
+ case "SET_FORMATTING":
43630
+ case "CLEAR_FORMATTING":
43631
+ this.isDirty = true;
43632
+ break;
43633
+ }
43634
+ }
43635
+ finalize() {
43636
+ if (this.isDirty) {
43637
+ this.refresh();
43638
+ this.isDirty = false;
43639
+ }
43640
+ }
43641
+ refresh() {
43642
+ const sheetId = this.getters.getActiveSheetId();
43643
+ const zones = this.getters.getSelectedZones();
43644
+ this.ranges = zones.map(zoneToXc);
43645
+ this.hasData = zones.some((zone) => this.getters.getEvaluatedCellsInZone(sheetId, zone).some((cell) => cell.type !== "empty"));
43646
+ const suggestions = this.hasData ? getChartSuggestions(zones, this.getters) : [];
43647
+ this.chartSuggestions = suggestions;
43648
+ }
43649
+ };
43650
+
43651
+ //#endregion
43652
+ //#region src/components/side_panel/data_analysis/data_analysis_panel.ts
43653
+ var DataAnalysisPanel = class extends Component {
43654
+ static template = "o-spreadsheet-DataAnalysisPanel";
43655
+ static components = {
43656
+ Section,
43657
+ ChartSuggestionPreview
43658
+ };
43659
+ store;
43660
+ setup() {
43661
+ this.store = useLocalStore(DataAnalysisStore);
43662
+ }
43663
+ onStartChartSuggestionDrag(definition, ev) {
43664
+ startChartDragAndDrop(this.env, definition, ev);
43665
+ }
43666
+ };
43667
+
42147
43668
  //#endregion
42148
43669
  //#region src/registries/data_source_registry.ts
42149
43670
  const unusedDataSourceRegistry = new Registry();
@@ -42220,7 +43741,7 @@ var DataSourcesCleanup = class extends Component {
42220
43741
  var DataValidationPreview = class extends Component {
42221
43742
  static template = "o-spreadsheet-DataValidationPreview";
42222
43743
  props = useProps({ rule: types$1.DataValidationRule() });
42223
- dvPreviewRef = signal(null);
43744
+ dvPreviewRef = signal.ref();
42224
43745
  setup() {
42225
43746
  useHighlightsOnHover(this.dvPreviewRef, this);
42226
43747
  }
@@ -43719,7 +45240,7 @@ var NamedRangePreview = class extends Component {
43719
45240
  };
43720
45241
  props = useProps({ namedRange: types$1.NamedRange() });
43721
45242
  state = proxy({});
43722
- namedRangePreviewRef = signal(null);
45243
+ namedRangePreviewRef = signal.ref();
43723
45244
  setup() {
43724
45245
  useHighlightsOnHover(this.namedRangePreviewRef, this);
43725
45246
  }
@@ -44210,7 +45731,7 @@ var FilterMenuValueItem = class extends Component {
44210
45731
  onClick: types$1.function(),
44211
45732
  scrolledTo: types$1.or([types$1.literal("top"), types$1.literal("bottom")]).optional()
44212
45733
  });
44213
- itemRef = signal(null);
45734
+ itemRef = signal.ref();
44214
45735
  setup() {
44215
45736
  onWillPatch(() => {
44216
45737
  if (this.props.scrolledTo) this.scrollListToSelectedValue();
@@ -44450,7 +45971,7 @@ var PivotFilterEditor = class extends Component {
44450
45971
  onFiltersUpdated: types$1.function()
44451
45972
  });
44452
45973
  state;
44453
- buttonFilter = signal(null);
45974
+ buttonFilter = signal.ref();
44454
45975
  popover;
44455
45976
  setup() {
44456
45977
  useExternalListener(window, "click", (ev) => {
@@ -44579,11 +46100,11 @@ var AddDimensionButton = class extends Component {
44579
46100
  onFieldPicked: types$1.function(),
44580
46101
  fields: types$1.array()
44581
46102
  });
44582
- buttonRef = signal(null);
46103
+ buttonRef = signal.ref(HTMLButtonElement);
44583
46104
  popover = proxy({ isOpen: false });
44584
46105
  search = proxy({ input: "" });
44585
46106
  autoComplete;
44586
- autofocusRef = signal(null);
46107
+ autofocusRef = signal.ref(HTMLInputElement);
44587
46108
  setup() {
44588
46109
  this.autoComplete = useLocalStore(AutoCompleteStore);
44589
46110
  this.autoComplete.useProvider(this.getProvider());
@@ -44992,7 +46513,7 @@ var PivotLayoutConfigurator = class extends Component {
44992
46513
  getScrollableContainerEl: types$1.function().optional(),
44993
46514
  pivotId: types$1.UID()
44994
46515
  });
44995
- dimensionsRef = signal(null);
46516
+ dimensionsRef = signal.ref();
44996
46517
  dragAndDrop = useDragAndDropListItems();
44997
46518
  AGGREGATORS = AGGREGATORS;
44998
46519
  isDateOrDatetimeField = isDateOrDatetimeField;
@@ -45063,16 +46584,7 @@ var PivotLayoutConfigurator = class extends Component {
45063
46584
  });
45064
46585
  }
45065
46586
  getDimensionElementsRects() {
45066
- return Array.from(this.dimensionsRef().children).map((el) => {
45067
- const style = getComputedStyle(el);
45068
- const rect = el.getBoundingClientRect();
45069
- return {
45070
- x: rect.x,
45071
- y: rect.y,
45072
- width: rect.width + parseInt(style.marginLeft || "0") + parseInt(style.marginRight || "0"),
45073
- height: rect.height + parseInt(style.marginTop || "0") + parseInt(style.marginBottom || "0")
45074
- };
45075
- });
46587
+ return Array.from(this.dimensionsRef().children).map((el) => getBoundingRectWithMargins(el));
45076
46588
  }
45077
46589
  removeDimension(dimension) {
45078
46590
  const { columns, rows } = this.props.definition;
@@ -45637,7 +47149,7 @@ var PivotSpreadsheetSidePanel = class extends Component {
45637
47149
  });
45638
47150
  store;
45639
47151
  state;
45640
- pivotSidePanelRef = signal(null);
47152
+ pivotSidePanelRef = signal.ref();
45641
47153
  setup() {
45642
47154
  this.store = useLocalStore(PivotSidePanelStore, this.props.pivotId);
45643
47155
  this.state = proxy({
@@ -46877,7 +48389,7 @@ var TableStylePreview = class extends Component {
46877
48389
  selected: types$1.boolean().optional(),
46878
48390
  onClick: types$1.function().optional()
46879
48391
  });
46880
- canvasRef = signal(null);
48392
+ canvasRef = signal.ref(HTMLCanvasElement);
46881
48393
  menu = proxy({
46882
48394
  isOpen: false,
46883
48395
  anchorRect: null,
@@ -46971,7 +48483,7 @@ var TableStylesPopover = class extends Component {
46971
48483
  tableStyles: types$1.RecordOf(),
46972
48484
  type: types$1.or([types$1.literal("table"), types$1.literal("pivot")])
46973
48485
  });
46974
- tableStyleListRef = signal(null);
48486
+ tableStyleListRef = signal.ref();
46975
48487
  state = proxy({ selectedCategory: this.initialSelectedCategory });
46976
48488
  setup() {
46977
48489
  useExternalListener(window, "click", this.onExternalClick, { capture: true });
@@ -48342,6 +49854,10 @@ sidePanelRegistry.add("ColumnStats", {
48342
49854
  title: _t("Column statistics"),
48343
49855
  Body: ColumnStatsPanel
48344
49856
  });
49857
+ sidePanelRegistry.add("DataAnalysisPanel", {
49858
+ title: _t("Data analysis"),
49859
+ Body: DataAnalysisPanel
49860
+ });
48345
49861
  sidePanelRegistry.add("TableSidePanel", {
48346
49862
  title: _t("Edit table"),
48347
49863
  Body: TablePanel,
@@ -48449,6 +49965,7 @@ var SidePanelStore = class extends SpreadsheetStore {
48449
49965
  "changePanelSize",
48450
49966
  "resetPanelSize",
48451
49967
  "togglePinPanel",
49968
+ "togglePinnedSidePanel",
48452
49969
  "closeMainPanel",
48453
49970
  "changeSpreadsheetWidth",
48454
49971
  "toggleCollapsePanel"
@@ -48600,6 +50117,19 @@ var SidePanelStore = class extends SpreadsheetStore {
48600
50117
  this.secondaryPanel = void 0;
48601
50118
  }
48602
50119
  }
50120
+ togglePinnedSidePanel(componentTag, panelProps = {}) {
50121
+ if (this.mainPanel?.componentTag === componentTag && this.isMainPanelOpen) {
50122
+ this.closeMainPanel();
50123
+ return;
50124
+ }
50125
+ if (this.secondaryPanel?.componentTag === componentTag && this.isSecondaryPanelOpen) {
50126
+ this.secondaryPanel?.currentPanelProps.onCloseSidePanel?.();
50127
+ this.secondaryPanel = void 0;
50128
+ return;
50129
+ }
50130
+ this.open(componentTag, panelProps);
50131
+ if (!this.mainPanel?.isPinned) this.togglePinPanel();
50132
+ }
48603
50133
  toggleCollapsePanel(panel) {
48604
50134
  const panelInfo = this[panel];
48605
50135
  if (!panelInfo) return;
@@ -48978,6 +50508,219 @@ var MenuItemRegistry = class extends Registry {
48978
50508
  }
48979
50509
  };
48980
50510
 
50511
+ //#endregion
50512
+ //#region src/helpers/sort_interactive.ts
50513
+ function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) {
50514
+ let multiColumns = zone.right > zone.left;
50515
+ if (env.model.getters.doesIntersectMerge(sheetId, zone)) {
50516
+ multiColumns = false;
50517
+ let table;
50518
+ for (let row = zone.top; row <= zone.bottom; row++) {
50519
+ table = [];
50520
+ for (let col = zone.left; col <= zone.right; col++) {
50521
+ const merge = env.model.getters.getMerge({
50522
+ sheetId,
50523
+ col,
50524
+ row
50525
+ });
50526
+ if (merge && !table.includes(merge.id.toString())) table.push(merge.id.toString());
50527
+ }
50528
+ if (table.length >= 2) {
50529
+ multiColumns = true;
50530
+ break;
50531
+ }
50532
+ }
50533
+ }
50534
+ if (multiColumns) {
50535
+ interactiveSort(env, sheetId, anchor, zone, sortDirection);
50536
+ return;
50537
+ }
50538
+ const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);
50539
+ if (isEqual(contiguousZone, zone)) interactiveSort(env, sheetId, anchor, zone, sortDirection);
50540
+ else env.askConfirmation(_t("We found data next to your selection. Since this data was not selected, it will not be sorted. Do you want to extend your selection?"), () => interactiveSort(env, sheetId, anchor, contiguousZone, sortDirection), () => interactiveSort(env, sheetId, anchor, zone, sortDirection));
50541
+ }
50542
+ function interactiveSort(env, sheetId, anchor, zone, sortDirection, sortOptions) {
50543
+ const result = env.model.dispatch("SORT_CELLS", {
50544
+ sheetId,
50545
+ col: anchor.col,
50546
+ row: anchor.row,
50547
+ zone,
50548
+ sortDirection,
50549
+ sortOptions
50550
+ });
50551
+ if (result.isCancelledBecause("InvalidSortZone")) {
50552
+ const { col, row } = anchor;
50553
+ env.model.selection.selectZone({
50554
+ cell: {
50555
+ col,
50556
+ row
50557
+ },
50558
+ zone
50559
+ });
50560
+ env.raiseError(_t("Cannot sort. To sort, select only cells or only merges that have the same size."));
50561
+ }
50562
+ if (result.isCancelledBecause("SortZoneWithArrayFormulas")) {
50563
+ const { col, row } = anchor;
50564
+ env.model.selection.selectZone({
50565
+ cell: {
50566
+ col,
50567
+ row
50568
+ },
50569
+ zone
50570
+ });
50571
+ env.raiseError(_t("Cannot sort a zone with array formulas."));
50572
+ }
50573
+ }
50574
+
50575
+ //#endregion
50576
+ //#region src/stores/trim_whitespace_store.ts
50577
+ var TrimWhitespaceStore = class extends SpreadsheetStore {
50578
+ mutators = ["trimWhitespace"];
50579
+ notificationStore = this.get(NotificationStore);
50580
+ trimWhitespace() {
50581
+ this.model.dispatch("TRIM_WHITESPACE");
50582
+ }
50583
+ handle(cmd) {
50584
+ switch (cmd.type) {
50585
+ case "TRIM_WHITESPACE":
50586
+ this._trimWhitespace();
50587
+ break;
50588
+ }
50589
+ }
50590
+ _trimWhitespace() {
50591
+ const zones = recomputeZones(this.getters.getSelectedZones());
50592
+ const sheetId = this.getters.getActiveSheetId();
50593
+ let count = 0;
50594
+ for (const { col, row } of zones.map(positions).flat()) {
50595
+ const cell = this.getters.getCell({
50596
+ col,
50597
+ row,
50598
+ sheetId
50599
+ });
50600
+ if (!cell) continue;
50601
+ const currentContent = !cell.isFormula ? cell.content : cell.compiledFormula.toFormulaString(this.getters);
50602
+ const trimmedContent = trimContent(currentContent);
50603
+ if (trimmedContent !== currentContent) {
50604
+ count += 1;
50605
+ this.model.dispatch("UPDATE_CELL", {
50606
+ sheetId,
50607
+ col,
50608
+ row,
50609
+ content: trimmedContent
50610
+ });
50611
+ }
50612
+ }
50613
+ const text = count ? _t("Trimmed whitespace from %s cells.", count) : _t("No selected cells had whitespace trimmed.");
50614
+ this.notificationStore.notifyUser({
50615
+ type: "info",
50616
+ text,
50617
+ sticky: false
50618
+ });
50619
+ }
50620
+ };
50621
+
50622
+ //#endregion
50623
+ //#region src/actions/data_actions.ts
50624
+ const sortRange = {
50625
+ name: _t("Sort range"),
50626
+ isVisible: IS_ONLY_ONE_RANGE,
50627
+ icon: "o-spreadsheet-Icon.SORT_RANGE"
50628
+ };
50629
+ const sortAscending = {
50630
+ name: _t("Ascending (A ⟶ Z)"),
50631
+ execute: (env) => {
50632
+ const { anchor, zones } = env.model.getters.getSelection();
50633
+ interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "asc");
50634
+ },
50635
+ icon: "o-spreadsheet-Icon.SORT_ASCENDING"
50636
+ };
50637
+ const dataCleanup = {
50638
+ name: _t("Data cleanup"),
50639
+ icon: "o-spreadsheet-Icon.DATA_CLEANUP"
50640
+ };
50641
+ const removeDuplicates = {
50642
+ name: _t("Remove duplicates"),
50643
+ execute: (env) => {
50644
+ if (getZoneArea(env.model.getters.getSelectedZone()) === 1) env.model.selection.selectTableAroundSelection();
50645
+ env.openSidePanel("RemoveDuplicates", {});
50646
+ },
50647
+ isEnabled: (env) => !env.isSmall
50648
+ };
50649
+ const trimWhitespace = {
50650
+ name: _t("Trim whitespace"),
50651
+ execute: (env) => {
50652
+ env.getStore(TrimWhitespaceStore).trimWhitespace();
50653
+ }
50654
+ };
50655
+ const cleanupDataSources = {
50656
+ name: _t("Remove unused data sources"),
50657
+ execute: (env) => env.openSidePanel("DataSourceCleanup", {}),
50658
+ isEnabled: (env) => !env.isSmall
50659
+ };
50660
+ const sortDescending = {
50661
+ name: _t("Descending (Z ⟶ A)"),
50662
+ execute: (env) => {
50663
+ const { anchor, zones } = env.model.getters.getSelection();
50664
+ interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "desc");
50665
+ },
50666
+ icon: "o-spreadsheet-Icon.SORT_DESCENDING"
50667
+ };
50668
+ const createRemoveFilter = { ...CREATE_OR_REMOVE_FILTER_ACTION };
50669
+ const createRemoveFilterTool = {
50670
+ ...CREATE_OR_REMOVE_FILTER_ACTION,
50671
+ isActive: (env) => SELECTED_TABLE_HAS_FILTERS(env)
50672
+ };
50673
+ const splitToColumns = {
50674
+ name: _t("Split text to columns"),
50675
+ sequence: 1,
50676
+ execute: (env) => env.openSidePanel("SplitToColumns", {}),
50677
+ isEnabled: (env) => !env.isSmall && env.model.getters.isSingleColSelected(),
50678
+ icon: "o-spreadsheet-Icon.SPLIT_TEXT"
50679
+ };
50680
+ const dataAnalysis = {
50681
+ name: _t("Data analysis"),
50682
+ execute: (env) => {
50683
+ env.getStore(SidePanelStore).togglePinnedSidePanel("DataAnalysisPanel");
50684
+ },
50685
+ icon: "o-spreadsheet-Icon.COLUMN_STATS",
50686
+ isEnabled: (env) => !env.isSmall
50687
+ };
50688
+ const columnStatistics = {
50689
+ name: _t("Column statistics"),
50690
+ execute: (env) => env.openSidePanel("ColumnStats", {}),
50691
+ icon: "o-spreadsheet-Icon.COLUMN_STATS"
50692
+ };
50693
+ const reinsertDynamicPivotMenu = {
50694
+ id: "reinsert_dynamic_pivot",
50695
+ name: _t("Re-insert dynamic pivot"),
50696
+ sequence: 60,
50697
+ icon: "o-spreadsheet-Icon.INSERT_PIVOT",
50698
+ children: [REINSERT_DYNAMIC_PIVOT_CHILDREN],
50699
+ isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
50700
+ };
50701
+ const reinsertStaticPivotMenu = {
50702
+ id: "reinsert_static_pivot",
50703
+ name: _t("Re-insert static pivot"),
50704
+ sequence: 70,
50705
+ icon: "o-spreadsheet-Icon.INSERT_PIVOT",
50706
+ children: [REINSERT_STATIC_PIVOT_CHILDREN],
50707
+ isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
50708
+ };
50709
+ const calculationMode = {
50710
+ name: _t("Calculation"),
50711
+ icon: "o-spreadsheet-Icon.CALCULATOR"
50712
+ };
50713
+ const automaticCalculation = {
50714
+ name: _t("Automatic"),
50715
+ execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: true }),
50716
+ isActive: (env) => env.model.getters.isAutomaticEvaluationEnabled()
50717
+ };
50718
+ const manualCalculation = {
50719
+ name: _t("Manual"),
50720
+ execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: false }),
50721
+ isActive: (env) => !env.model.getters.isAutomaticEvaluationEnabled()
50722
+ };
50723
+
48981
50724
  //#endregion
48982
50725
  //#region src/helpers/ui/merge_interactive.ts
48983
50726
  const AddMergeInteractiveContent = {
@@ -49192,6 +50935,10 @@ cellMenuRegistry.add("cut", {
49192
50935
  }).addChild("paste_format_only", ["paste_special"], {
49193
50936
  ...pasteSpecialFormat,
49194
50937
  sequence: 20
50938
+ }).add("data_analysis", {
50939
+ ...dataAnalysis,
50940
+ sequence: 50,
50941
+ separator: true
49195
50942
  }).add("add_row_before", {
49196
50943
  ...cellInsertRowsBefore,
49197
50944
  sequence: 70
@@ -49299,211 +51046,6 @@ cellMenuRegistry.add("cut", {
49299
51046
  sequence: 30
49300
51047
  });
49301
51048
 
49302
- //#endregion
49303
- //#region src/helpers/sort_interactive.ts
49304
- function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) {
49305
- let multiColumns = zone.right > zone.left;
49306
- if (env.model.getters.doesIntersectMerge(sheetId, zone)) {
49307
- multiColumns = false;
49308
- let table;
49309
- for (let row = zone.top; row <= zone.bottom; row++) {
49310
- table = [];
49311
- for (let col = zone.left; col <= zone.right; col++) {
49312
- const merge = env.model.getters.getMerge({
49313
- sheetId,
49314
- col,
49315
- row
49316
- });
49317
- if (merge && !table.includes(merge.id.toString())) table.push(merge.id.toString());
49318
- }
49319
- if (table.length >= 2) {
49320
- multiColumns = true;
49321
- break;
49322
- }
49323
- }
49324
- }
49325
- if (multiColumns) {
49326
- interactiveSort(env, sheetId, anchor, zone, sortDirection);
49327
- return;
49328
- }
49329
- const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);
49330
- if (isEqual(contiguousZone, zone)) interactiveSort(env, sheetId, anchor, zone, sortDirection);
49331
- else env.askConfirmation(_t("We found data next to your selection. Since this data was not selected, it will not be sorted. Do you want to extend your selection?"), () => interactiveSort(env, sheetId, anchor, contiguousZone, sortDirection), () => interactiveSort(env, sheetId, anchor, zone, sortDirection));
49332
- }
49333
- function interactiveSort(env, sheetId, anchor, zone, sortDirection, sortOptions) {
49334
- const result = env.model.dispatch("SORT_CELLS", {
49335
- sheetId,
49336
- col: anchor.col,
49337
- row: anchor.row,
49338
- zone,
49339
- sortDirection,
49340
- sortOptions
49341
- });
49342
- if (result.isCancelledBecause("InvalidSortZone")) {
49343
- const { col, row } = anchor;
49344
- env.model.selection.selectZone({
49345
- cell: {
49346
- col,
49347
- row
49348
- },
49349
- zone
49350
- });
49351
- env.raiseError(_t("Cannot sort. To sort, select only cells or only merges that have the same size."));
49352
- }
49353
- if (result.isCancelledBecause("SortZoneWithArrayFormulas")) {
49354
- const { col, row } = anchor;
49355
- env.model.selection.selectZone({
49356
- cell: {
49357
- col,
49358
- row
49359
- },
49360
- zone
49361
- });
49362
- env.raiseError(_t("Cannot sort a zone with array formulas."));
49363
- }
49364
- }
49365
-
49366
- //#endregion
49367
- //#region src/stores/trim_whitespace_store.ts
49368
- var TrimWhitespaceStore = class extends SpreadsheetStore {
49369
- mutators = ["trimWhitespace"];
49370
- notificationStore = this.get(NotificationStore);
49371
- trimWhitespace() {
49372
- this.model.dispatch("TRIM_WHITESPACE");
49373
- }
49374
- handle(cmd) {
49375
- switch (cmd.type) {
49376
- case "TRIM_WHITESPACE":
49377
- this._trimWhitespace();
49378
- break;
49379
- }
49380
- }
49381
- _trimWhitespace() {
49382
- const zones = recomputeZones(this.getters.getSelectedZones());
49383
- const sheetId = this.getters.getActiveSheetId();
49384
- let count = 0;
49385
- for (const { col, row } of zones.map(positions).flat()) {
49386
- const cell = this.getters.getCell({
49387
- col,
49388
- row,
49389
- sheetId
49390
- });
49391
- if (!cell) continue;
49392
- const currentContent = !cell.isFormula ? cell.content : cell.compiledFormula.toFormulaString(this.getters);
49393
- const trimmedContent = trimContent(currentContent);
49394
- if (trimmedContent !== currentContent) {
49395
- count += 1;
49396
- this.model.dispatch("UPDATE_CELL", {
49397
- sheetId,
49398
- col,
49399
- row,
49400
- content: trimmedContent
49401
- });
49402
- }
49403
- }
49404
- const text = count ? _t("Trimmed whitespace from %s cells.", count) : _t("No selected cells had whitespace trimmed.");
49405
- this.notificationStore.notifyUser({
49406
- type: "info",
49407
- text,
49408
- sticky: false
49409
- });
49410
- }
49411
- };
49412
-
49413
- //#endregion
49414
- //#region src/actions/data_actions.ts
49415
- const sortRange = {
49416
- name: _t("Sort range"),
49417
- isVisible: IS_ONLY_ONE_RANGE,
49418
- icon: "o-spreadsheet-Icon.SORT_RANGE"
49419
- };
49420
- const sortAscending = {
49421
- name: _t("Ascending (A ⟶ Z)"),
49422
- execute: (env) => {
49423
- const { anchor, zones } = env.model.getters.getSelection();
49424
- interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "asc");
49425
- },
49426
- icon: "o-spreadsheet-Icon.SORT_ASCENDING"
49427
- };
49428
- const dataCleanup = {
49429
- name: _t("Data cleanup"),
49430
- icon: "o-spreadsheet-Icon.DATA_CLEANUP"
49431
- };
49432
- const removeDuplicates = {
49433
- name: _t("Remove duplicates"),
49434
- execute: (env) => {
49435
- if (getZoneArea(env.model.getters.getSelectedZone()) === 1) env.model.selection.selectTableAroundSelection();
49436
- env.openSidePanel("RemoveDuplicates", {});
49437
- },
49438
- isEnabled: (env) => !env.isSmall
49439
- };
49440
- const trimWhitespace = {
49441
- name: _t("Trim whitespace"),
49442
- execute: (env) => {
49443
- env.getStore(TrimWhitespaceStore).trimWhitespace();
49444
- }
49445
- };
49446
- const cleanupDataSources = {
49447
- name: _t("Remove unused data sources"),
49448
- execute: (env) => env.openSidePanel("DataSourceCleanup", {}),
49449
- isEnabled: (env) => !env.isSmall
49450
- };
49451
- const sortDescending = {
49452
- name: _t("Descending (Z ⟶ A)"),
49453
- execute: (env) => {
49454
- const { anchor, zones } = env.model.getters.getSelection();
49455
- interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "desc");
49456
- },
49457
- icon: "o-spreadsheet-Icon.SORT_DESCENDING"
49458
- };
49459
- const createRemoveFilter = { ...CREATE_OR_REMOVE_FILTER_ACTION };
49460
- const createRemoveFilterTool = {
49461
- ...CREATE_OR_REMOVE_FILTER_ACTION,
49462
- isActive: (env) => SELECTED_TABLE_HAS_FILTERS(env)
49463
- };
49464
- const splitToColumns = {
49465
- name: _t("Split text to columns"),
49466
- sequence: 1,
49467
- execute: (env) => env.openSidePanel("SplitToColumns", {}),
49468
- isEnabled: (env) => !env.isSmall && env.model.getters.isSingleColSelected(),
49469
- icon: "o-spreadsheet-Icon.SPLIT_TEXT"
49470
- };
49471
- const columnStatistics = {
49472
- name: _t("Column statistics"),
49473
- execute: (env) => env.openSidePanel("ColumnStats", {}),
49474
- icon: "o-spreadsheet-Icon.COLUMN_STATS"
49475
- };
49476
- const reinsertDynamicPivotMenu = {
49477
- id: "reinsert_dynamic_pivot",
49478
- name: _t("Re-insert dynamic pivot"),
49479
- sequence: 60,
49480
- icon: "o-spreadsheet-Icon.INSERT_PIVOT",
49481
- children: [REINSERT_DYNAMIC_PIVOT_CHILDREN],
49482
- isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
49483
- };
49484
- const reinsertStaticPivotMenu = {
49485
- id: "reinsert_static_pivot",
49486
- name: _t("Re-insert static pivot"),
49487
- sequence: 70,
49488
- icon: "o-spreadsheet-Icon.INSERT_PIVOT",
49489
- children: [REINSERT_STATIC_PIVOT_CHILDREN],
49490
- isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
49491
- };
49492
- const calculationMode = {
49493
- name: _t("Calculation"),
49494
- icon: "o-spreadsheet-Icon.CALCULATOR"
49495
- };
49496
- const automaticCalculation = {
49497
- name: _t("Automatic"),
49498
- execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: true }),
49499
- isActive: (env) => env.model.getters.isAutomaticEvaluationEnabled()
49500
- };
49501
- const manualCalculation = {
49502
- name: _t("Manual"),
49503
- execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: false }),
49504
- isActive: (env) => !env.model.getters.isAutomaticEvaluationEnabled()
49505
- };
49506
-
49507
51049
  //#endregion
49508
51050
  //#region src/registries/menus/col_menu_registry.ts
49509
51051
  const colMenuRegistry = new MenuItemRegistry();
@@ -49537,6 +51079,9 @@ colMenuRegistry.add("cut", {
49537
51079
  }).addChild("sort_descending", ["sort_columns"], {
49538
51080
  ...sortDescending,
49539
51081
  sequence: 20
51082
+ }).add("data_analysis", {
51083
+ ...dataAnalysis,
51084
+ sequence: 55
49540
51085
  }).add("column_statistics", {
49541
51086
  ...columnStatistics,
49542
51087
  sequence: 60,
@@ -52352,14 +53897,15 @@ var FiguresContainer = class extends Component {
52352
53897
  selectedRect: void 0,
52353
53898
  horizontalSnap: void 0,
52354
53899
  verticalSnap: void 0,
52355
- cancelDnd: void 0,
52356
- overlappingChartOrCarousel: void 0
53900
+ cancelDnd: void 0
52357
53901
  });
52358
53902
  viewStore;
52359
53903
  zoomStore;
53904
+ chartDragStore;
52360
53905
  setup() {
52361
53906
  this.viewStore = useStore(ViewportsStore);
52362
53907
  this.zoomStore = useStore(ZoomStore);
53908
+ this.chartDragStore = useStore(ChartDragStore);
52363
53909
  onMounted(() => {
52364
53910
  this.render();
52365
53911
  });
@@ -52373,7 +53919,7 @@ var FiguresContainer = class extends Component {
52373
53919
  this.dnd.selectedRect = void 0;
52374
53920
  this.dnd.horizontalSnap = void 0;
52375
53921
  this.dnd.verticalSnap = void 0;
52376
- this.dnd.overlappingChartOrCarousel = void 0;
53922
+ this.chartDragStore.setHighlightedFigure(void 0);
52377
53923
  this.dnd.cancelDnd = void 0;
52378
53924
  }
52379
53925
  });
@@ -52499,6 +54045,7 @@ var FiguresContainer = class extends Component {
52499
54045
  maxY: this.env.model.getters.getRowDimensions(sheetId, this.env.model.getters.getNumberRows(sheetId) - 1).end
52500
54046
  };
52501
54047
  let hasStartedDnd = false;
54048
+ let overlappingChartOrCarousel = void 0;
52502
54049
  const onMouseMove = (ev) => {
52503
54050
  const currentMousePosition = {
52504
54051
  x: ev.clientX / zoom,
@@ -52510,10 +54057,10 @@ var FiguresContainer = class extends Component {
52510
54057
  hasStartedDnd = true;
52511
54058
  const selectedFigures = dragFigureForMove(currentMousePosition, initialMousePosition, initialFigures, maxDimensions, initialScrollPosition, this.viewStore.activeSheetScrollInfo);
52512
54059
  const draggedFigure = selectedFigures.find((f) => f.id === draggedFigureId);
52513
- let overlappingChartOrCarousel = void 0;
54060
+ overlappingChartOrCarousel = void 0;
52514
54061
  const otherFigures = this.getOtherFigures(selectedFigures.map((f) => f.id));
52515
- if (draggedFigure && !selectedFigures.find((f) => f.tag !== "chart")) overlappingChartOrCarousel = this.getOverlappingFigure(draggedFigure, otherFigures);
52516
- this.dnd.overlappingChartOrCarousel = overlappingChartOrCarousel;
54062
+ if (draggedFigure && !selectedFigures.find((f) => f.tag !== "chart")) overlappingChartOrCarousel = getCarouselOverlappingChart(draggedFigure, otherFigures, ["carousel", "chart"]);
54063
+ this.chartDragStore.setHighlightedFigure(overlappingChartOrCarousel?.id);
52517
54064
  if (!overlappingChartOrCarousel) {
52518
54065
  const snapReturn = snapForMove(this.env, selectedFigures, otherFigures);
52519
54066
  this.dnd.selectedFigures = snapReturn.snappedFigures;
@@ -52535,7 +54082,7 @@ var FiguresContainer = class extends Component {
52535
54082
  else this.env.model.dispatch("SELECT_FIGURE", { figureId: figureUI.id });
52536
54083
  return;
52537
54084
  }
52538
- if (!this.dnd.overlappingChartOrCarousel) {
54085
+ if (!overlappingChartOrCarousel) {
52539
54086
  const payloads = this.dnd.selectedFigures?.map((f) => {
52540
54087
  return {
52541
54088
  sheetId,
@@ -52545,14 +54092,14 @@ var FiguresContainer = class extends Component {
52545
54092
  }) || [];
52546
54093
  this.env.model.dispatch("MOVE_FIGURES", { figures: payloads });
52547
54094
  } else {
52548
- const overlappingFigureId = this.dnd.overlappingChartOrCarousel.id;
54095
+ const overlappingFigureId = overlappingChartOrCarousel.id;
52549
54096
  const chartFigureIds = this.dnd.selectedFigures?.map((f) => f.id) || [];
52550
- if (this.dnd.overlappingChartOrCarousel.tag === "carousel") this.env.model.dispatch("ADD_FIGURES_CHART_TO_CAROUSEL", {
54097
+ if (overlappingChartOrCarousel.tag === "carousel") this.env.model.dispatch("ADD_FIGURES_CHART_TO_CAROUSEL", {
52551
54098
  sheetId,
52552
54099
  carouselFigureId: overlappingFigureId,
52553
54100
  chartFigureIds
52554
54101
  });
52555
- else if (this.dnd.overlappingChartOrCarousel.tag === "chart") this.env.model.dispatch("MERGE_CHART_FIGURES_INTO_CAROUSEL", {
54102
+ else if (overlappingChartOrCarousel.tag === "chart") this.env.model.dispatch("MERGE_CHART_FIGURES_INTO_CAROUSEL", {
52556
54103
  sheetId,
52557
54104
  baseFigureId: overlappingFigureId,
52558
54105
  chartFigureIds: [overlappingFigureId, ...chartFigureIds]
@@ -52563,7 +54110,7 @@ var FiguresContainer = class extends Component {
52563
54110
  this.dnd.selectedRect = void 0;
52564
54111
  this.dnd.horizontalSnap = void 0;
52565
54112
  this.dnd.verticalSnap = void 0;
52566
- this.dnd.overlappingChartOrCarousel = void 0;
54113
+ this.chartDragStore.setHighlightedFigure(void 0);
52567
54114
  };
52568
54115
  this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
52569
54116
  }
@@ -52619,7 +54166,6 @@ var FiguresContainer = class extends Component {
52619
54166
  this.dnd.selectedRect = void 0;
52620
54167
  this.dnd.horizontalSnap = void 0;
52621
54168
  this.dnd.verticalSnap = void 0;
52622
- this.dnd.overlappingChartOrCarousel = void 0;
52623
54169
  };
52624
54170
  this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
52625
54171
  }
@@ -52629,12 +54175,12 @@ var FiguresContainer = class extends Component {
52629
54175
  getFigureStyle(figureUI) {
52630
54176
  if (figureUI.id !== this.dnd.draggedFigure?.id) return "";
52631
54177
  return cssPropertiesToCss({
52632
- opacity: this.dnd.overlappingChartOrCarousel?.id ? "0.6" : "0.9",
54178
+ opacity: this.chartDragStore.highlightedFigureId ? "0.6" : "0.9",
52633
54179
  cursor: "grabbing"
52634
54180
  });
52635
54181
  }
52636
54182
  getFigureClass(figureUI) {
52637
- if (figureUI.id !== this.dnd.overlappingChartOrCarousel?.id) return "";
54183
+ if (figureUI.id !== this.chartDragStore.highlightedFigureId) return "";
52638
54184
  return "o-add-to-carousel";
52639
54185
  }
52640
54186
  getSnap(snapLine) {
@@ -52677,26 +54223,6 @@ var FiguresContainer = class extends Component {
52677
54223
  height: `100%`
52678
54224
  });
52679
54225
  }
52680
- getOverlappingFigure(figureUI, otherFigures) {
52681
- if (figureUI.tag !== "chart") return;
52682
- const figureCenterX = figureUI.x + figureUI.width / 2;
52683
- const figureCenterY = figureUI.y + figureUI.height / 2;
52684
- let bestMatch;
52685
- let smallestDistance = Infinity;
52686
- for (const figure of otherFigures) {
52687
- if (figure.tag !== "chart" && figure.tag !== "carousel") continue;
52688
- const targetCenterX = figure.x + figure.width / 2;
52689
- const targetCenterY = figure.y + figure.height / 2;
52690
- const distanceX = Math.abs(figureCenterX - targetCenterX);
52691
- const distanceY = Math.abs(figureCenterY - targetCenterY);
52692
- const squaredDistance = distanceX ** 2 + distanceY ** 2;
52693
- if (distanceX <= figureUI.width / 2 && distanceY <= figureUI.height / 2 && squaredDistance < smallestDistance) {
52694
- smallestDistance = squaredDistance;
52695
- bestMatch = figure;
52696
- }
52697
- }
52698
- return bestMatch;
52699
- }
52700
54226
  };
52701
54227
 
52702
54228
  //#endregion
@@ -52996,7 +54522,7 @@ var AbstractResizer = class extends Component {
52996
54522
  var ColResizer = class extends AbstractResizer {
52997
54523
  static template = "o-spreadsheet-ColResizer";
52998
54524
  static components = { UnhideColumnHeaders };
52999
- colResizerRef = signal(null);
54525
+ colResizerRef = signal.ref();
53000
54526
  setup() {
53001
54527
  super.setup();
53002
54528
  this.PADDING = 15;
@@ -53130,7 +54656,7 @@ var ColResizer = class extends AbstractResizer {
53130
54656
  var RowResizer = class extends AbstractResizer {
53131
54657
  static template = "o-spreadsheet-RowResizer";
53132
54658
  static components = { UnhideRowHeaders };
53133
- rowResizerRef = signal(null);
54659
+ rowResizerRef = signal.ref();
53134
54660
  setup() {
53135
54661
  super.setup();
53136
54662
  this.PADDING = 5;
@@ -53739,9 +55265,8 @@ var TableResizer = class extends Component {
53739
55265
  useStore(TableResizeStore);
53740
55266
  }
53741
55267
  get containerStyle() {
53742
- const tableZone = this.props.table.range.zone;
53743
55268
  const sheetId = this.props.table.range.sheetId;
53744
- if (this.env.model.getters.isReadonly() || this.env.model.getters.isSheetLocked(sheetId)) return cssPropertiesToCss({ display: "none" });
55269
+ const tableZone = this.props.table.range.zone;
53745
55270
  const bottomRight = {
53746
55271
  ...tableZone,
53747
55272
  left: tableZone.right,
@@ -53828,8 +55353,8 @@ var Grid = class extends Component {
53828
55353
  HEADER_HEIGHT = 26;
53829
55354
  HEADER_WIDTH = 48;
53830
55355
  menuState;
53831
- gridRef = signal(null);
53832
- canvasRef = signal(null);
55356
+ gridRef = signal.ref();
55357
+ canvasRef = signal.ref(HTMLCanvasElement);
53833
55358
  highlightStore;
53834
55359
  viewStore;
53835
55360
  zoomStore;
@@ -54460,6 +55985,9 @@ var Grid = class extends Component {
54460
55985
  const sheetId = this.env.model.getters.getActiveSheetId();
54461
55986
  return this.env.model.getters.getClientsToDisplay(sheetId);
54462
55987
  }
55988
+ get displayTableResizer() {
55989
+ return this.env.model.getters.isGridSelectionActive() && !this.env.model.getters.isReadonly() && !this.env.model.getters.isSheetLocked(this.env.model.getters.getActiveSheetId());
55990
+ }
54463
55991
  };
54464
55992
 
54465
55993
  //#endregion
@@ -54478,7 +56006,7 @@ var FullScreenFigure = class extends Component {
54478
56006
  static template = "o-spreadsheet-FullScreenFigure";
54479
56007
  static components = { ChartFigure };
54480
56008
  fullScreenFigureStore;
54481
- fullScreenFigureRef = signal(null);
56009
+ fullScreenFigureRef = signal.ref();
54482
56010
  spreadsheetRect = useSpreadsheetRect();
54483
56011
  figureRegistry = figureRegistry;
54484
56012
  setup() {
@@ -54726,6 +56254,41 @@ var PivotHTMLRenderer = class extends Component {
54726
56254
  }
54727
56255
  };
54728
56256
 
56257
+ //#endregion
56258
+ //#region src/components/tables/hovered_table_store.ts
56259
+ var HoveredTableStore = class extends SpreadsheetStore {
56260
+ mutators = ["hover"];
56261
+ storeGetters = ["getCellHoverOverlayColor"];
56262
+ position;
56263
+ constructor(get) {
56264
+ super(get);
56265
+ const cellHoverOverlayStore = this.get(CellHoverOverlayStore);
56266
+ cellHoverOverlayStore.register(this);
56267
+ this.onDispose(() => cellHoverOverlayStore.unRegister(this));
56268
+ }
56269
+ getHighlightedPositions(hoveredPosition) {
56270
+ const highlightedPositions = [];
56271
+ const { sheetId, row } = hoveredPosition;
56272
+ const table = this.getters.getTable(hoveredPosition);
56273
+ if (!table) return highlightedPositions;
56274
+ const { left, right, top } = table.range.zone;
56275
+ const isTableHeader = row < top + table.config.numberOfHeaders;
56276
+ const doesTableRowHaveContent = range(left, right + 1).some((col) => {
56277
+ return !this.getters.isColHidden(sheetId, col) && this.getters.getEvaluatedCell({
56278
+ sheetId,
56279
+ col,
56280
+ row
56281
+ }).formattedValue;
56282
+ });
56283
+ if (!isTableHeader && doesTableRowHaveContent) for (let col = left; col <= right; col++) highlightedPositions.push({
56284
+ sheetId,
56285
+ col,
56286
+ row
56287
+ });
56288
+ return highlightedPositions;
56289
+ }
56290
+ };
56291
+
54729
56292
  //#endregion
54730
56293
  //#region src/helpers/figures/charts/runtime/helpers_index.ts
54731
56294
  var helpers_index_exports = /* @__PURE__ */ __exportAll({
@@ -67828,6 +69391,12 @@ var FigureUIPlugin = class extends UIPlugin {
67828
69391
  const figures = cmd.chartFigureIds.map((id) => this.getters.getFigure(cmd.sheetId, id));
67829
69392
  const baseFigureId = this.getters.getFigure(cmd.sheetId, cmd.baseFigureId);
67830
69393
  if (figures.some((f) => f === void 0 || f.tag !== "chart") || !baseFigureId || baseFigureId.tag !== "chart") return "FigureDoesNotExist";
69394
+ break;
69395
+ case "CREATE_CHART_AND_MERGE_INTO_CAROUSEL":
69396
+ const baseFigure = this.getters.getFigure(cmd.sheetId, cmd.baseFigureId);
69397
+ if (this.getters.getFigure(cmd.sheetId, cmd.figureId) || !baseFigure) return "InvalidFigureId";
69398
+ if (baseFigure.tag !== "chart") return "FigureDoesNotExist";
69399
+ break;
67831
69400
  }
67832
69401
  return "Success";
67833
69402
  }
@@ -67863,6 +69432,29 @@ var FigureUIPlugin = class extends UIPlugin {
67863
69432
  carouselFigureId,
67864
69433
  chartFigureIds: cmd.chartFigureIds
67865
69434
  });
69435
+ break;
69436
+ case "CREATE_CHART_AND_MERGE_INTO_CAROUSEL":
69437
+ const baseFigureToMerge = this.getters.getFigure(cmd.sheetId, cmd.baseFigureId);
69438
+ if (!baseFigureToMerge) throw new Error(`Figure ${cmd.baseFigureId} does not exists.`);
69439
+ this.dispatch("CREATE_CHART", {
69440
+ chartId: cmd.chartId,
69441
+ figureId: cmd.figureId,
69442
+ sheetId: cmd.sheetId,
69443
+ definition: cmd.definition,
69444
+ col: baseFigureToMerge.col,
69445
+ row: baseFigureToMerge.row,
69446
+ offset: baseFigureToMerge.offset,
69447
+ size: {
69448
+ width: baseFigureToMerge.width,
69449
+ height: baseFigureToMerge.height
69450
+ }
69451
+ });
69452
+ this.dispatch("MERGE_CHART_FIGURES_INTO_CAROUSEL", {
69453
+ sheetId: cmd.sheetId,
69454
+ baseFigureId: cmd.baseFigureId,
69455
+ chartFigureIds: [cmd.baseFigureId, cmd.figureId]
69456
+ });
69457
+ break;
67866
69458
  }
67867
69459
  }
67868
69460
  getFigureUI(sheetId, figure) {
@@ -69552,6 +71144,9 @@ topbarMenuRegistry.add("file", {
69552
71144
  }).addChild("split_to_columns", ["data"], {
69553
71145
  ...splitToColumns,
69554
71146
  sequence: 20
71147
+ }).addChild("data_analysis", ["data"], {
71148
+ ...dataAnalysis,
71149
+ sequence: 24
69555
71150
  }).addChild("column_statistics", ["data"], {
69556
71151
  ...columnStatistics,
69557
71152
  sequence: 25
@@ -69896,6 +71491,31 @@ var ImageProvider = class {
69896
71491
  var MainViewportStore = class extends SpreadsheetStore {
69897
71492
  viewStore = this.get(ViewportsStore);
69898
71493
  sheetIdAtFinalize = void 0;
71494
+ constructor(get) {
71495
+ super(get);
71496
+ this.model.selection.observe(this, { handleEvent: this.handleEvent.bind(this) });
71497
+ this.onDispose(() => {
71498
+ this.model.selection.unobserve(this);
71499
+ });
71500
+ }
71501
+ handleEvent(event) {
71502
+ const eventSheetId = this.getters.getActiveSheetId();
71503
+ if (event.options.scrollIntoView) {
71504
+ const oldZone = event.previousAnchor.zone;
71505
+ const newZone = event.anchor.zone;
71506
+ const isUpdateAnchorEvent = event.mode === "updateAnchor";
71507
+ const sameZone = isEqual(oldZone, newZone);
71508
+ let { col, row } = isUpdateAnchorEvent && sameZone ? event.anchor.cell : findCellInNewZone(oldZone, newZone);
71509
+ if (isUpdateAnchorEvent && !sameZone) {
71510
+ const { top, bottom, left, right } = this.viewStore.viewports.getMainInternalViewport(eventSheetId);
71511
+ if (oldZone.left === newZone.left && oldZone.right === newZone.right) col = left > col || col > right ? left : col;
71512
+ if (oldZone.top === newZone.top && oldZone.bottom === newZone.bottom) row = top > row || row > bottom ? top : row;
71513
+ }
71514
+ col = Math.min(col, this.getters.getNumberCols(eventSheetId) - 1);
71515
+ row = Math.min(row, this.getters.getNumberRows(eventSheetId) - 1);
71516
+ this.viewStore.scrollToCell(eventSheetId, col, row);
71517
+ }
71518
+ }
69899
71519
  handle(cmd) {
69900
71520
  switch (cmd.type) {
69901
71521
  case "UNDO":
@@ -69953,7 +71573,7 @@ var RippleEffect = class extends Component {
69953
71573
  onAnimationEnd: types$1.function(),
69954
71574
  style: types$1.string()
69955
71575
  });
69956
- rippleRef = signal(null);
71576
+ rippleRef = signal.ref();
69957
71577
  setup() {
69958
71578
  let animation = void 0;
69959
71579
  onMounted(() => {
@@ -70007,7 +71627,7 @@ var Ripple = class extends Component {
70007
71627
  onAnimationEnd: types$1.function().optional(() => () => {}),
70008
71628
  class: types$1.string().optional("")
70009
71629
  });
70010
- childContainerRef = signal(null);
71630
+ childContainerRef = signal.ref();
70011
71631
  state = proxy({ ripples: [] });
70012
71632
  currentId = 1;
70013
71633
  onClick(ev) {
@@ -70278,9 +71898,9 @@ var BottomBarSheet = class extends Component {
70278
71898
  isEditing: false,
70279
71899
  openedPicker: void 0
70280
71900
  });
70281
- sheetDivRef = signal(null);
70282
- iconRef = signal(null);
70283
- sheetNameRef = signal(null);
71901
+ sheetDivRef = signal.ref();
71902
+ iconRef = signal.ref();
71903
+ sheetNameRef = signal.ref();
70284
71904
  editionState = "initializing";
70285
71905
  DOMFocusableElementStore;
70286
71906
  setup() {
@@ -70564,8 +72184,10 @@ var BottomBarStatistic = class extends Component {
70564
72184
  });
70565
72185
  state = proxy({ selectedStatisticFn: "" });
70566
72186
  store;
72187
+ sidePanelStore;
70567
72188
  setup() {
70568
72189
  this.store = useStore(AggregateStatisticsStore);
72190
+ this.sidePanelStore = useStore(SidePanelStore);
70569
72191
  onWillUpdateProps(() => {
70570
72192
  if (Object.values(this.store.statisticFnResults).every((result) => result?.value === void 0)) this.props.closeContextMenu();
70571
72193
  });
@@ -70605,6 +72227,12 @@ var BottomBarStatistic = class extends Component {
70605
72227
  format: fnValue.format()
70606
72228
  });
70607
72229
  }
72230
+ get isDataAnalysisOpen() {
72231
+ return this.sidePanelStore.mainPanel?.componentTag === "DataAnalysisPanel" && this.sidePanelStore.isMainPanelOpen || this.sidePanelStore.secondaryPanel?.componentTag === "DataAnalysisPanel" && this.sidePanelStore.isSecondaryPanelOpen;
72232
+ }
72233
+ toggleDataAnalysisPanel() {
72234
+ this.sidePanelStore.togglePinnedSidePanel("DataAnalysisPanel");
72235
+ }
70608
72236
  };
70609
72237
 
70610
72238
  //#endregion
@@ -70619,8 +72247,8 @@ var BottomBar = class extends Component {
70619
72247
  BottomBarSheet,
70620
72248
  BottomBarStatistic
70621
72249
  };
70622
- bottomBarRef = signal(null);
70623
- sheetListRef = signal(null);
72250
+ bottomBarRef = signal.ref();
72251
+ sheetListRef = signal.ref();
70624
72252
  dragAndDrop = useDragAndDropListItems();
70625
72253
  targetScroll = void 0;
70626
72254
  state = proxy({
@@ -70833,12 +72461,13 @@ var SpreadsheetDashboard = class extends Component {
70833
72461
  hoveredCell;
70834
72462
  viewStore;
70835
72463
  zoomStore;
70836
- gridRef = signal(null);
70837
- canvasRef = signal(null);
72464
+ gridRef = signal.ref();
72465
+ canvasRef = signal.ref(HTMLCanvasElement);
70838
72466
  setup() {
70839
72467
  this.hoveredCell = useStore(DelayedHoveredCellStore);
70840
72468
  this.viewStore = useStore(ViewportsStore);
70841
72469
  this.zoomStore = useStore(ZoomStore);
72470
+ useLocalStore(HoveredTableStore);
70842
72471
  const rendererStore = useLocalStore(RendererStore, OrderedLayers().filter((layer) => layer !== "Headers"));
70843
72472
  useChildSubEnv({ getPopoverContainerRect: () => this.zoomStore.getZoomedRect(this.getGridRect()) });
70844
72473
  useGridDrawing({
@@ -71256,8 +72885,8 @@ var RibbonMenu = class extends Component {
71256
72885
  static components = { Menu };
71257
72886
  props = useProps({ onClose: types$1.function() });
71258
72887
  rootItems = topbarMenuRegistry.getMenuItems();
71259
- menuRef = signal(null);
71260
- containerRef = signal(null);
72888
+ menuRef = signal.ref();
72889
+ containerRef = signal.ref();
71261
72890
  state = proxy({
71262
72891
  menuItems: this.rootItems,
71263
72892
  title: _t("Menu Bar"),
@@ -71330,7 +72959,7 @@ var SmallBottomBar = class extends Component {
71330
72959
  composerStore;
71331
72960
  viewStore;
71332
72961
  composerInterface;
71333
- composerRef = signal(null);
72962
+ composerRef = signal.ref();
71334
72963
  menuState = proxy({ isOpen: false });
71335
72964
  setup() {
71336
72965
  this.composerFocusStore = useStore(ComposerFocusStore);
@@ -71409,6 +73038,49 @@ var SmallBottomBar = class extends Component {
71409
73038
  }
71410
73039
  };
71411
73040
 
73041
+ //#endregion
73042
+ //#region src/components/standalone_grid_canvas/carousel_data_view_print.ts
73043
+ var CarouselDataViewPrint = class extends Component {
73044
+ static template = xml``;
73045
+ props = useProps({
73046
+ range: types$1.Range(),
73047
+ columnWeights: types$1.array().optional(),
73048
+ rect: types$1.object(),
73049
+ rendererStore: types$1.object()
73050
+ });
73051
+ viewportsStore;
73052
+ gridRender;
73053
+ setup() {
73054
+ useChildStoreProvider([ViewportsStore]);
73055
+ const store = useLocalStore(StandaloneViewportStore, this.props.range, this.props.columnWeights);
73056
+ this.gridRender = useLocalStore(GridRenderer, null);
73057
+ this.viewportsStore = useStore(ViewportsStore);
73058
+ useEffect(() => {
73059
+ store.setContainerSize(this.props.rect.width, this.props.rect.height);
73060
+ });
73061
+ onMounted(() => this.props.rendererStore.register(this));
73062
+ onWillUnmount(() => this.props.rendererStore.unRegister(this));
73063
+ }
73064
+ get renderingLayers() {
73065
+ return ["Chart"];
73066
+ }
73067
+ drawLayer(renderingContext) {
73068
+ const { ctx } = renderingContext;
73069
+ ctx.save();
73070
+ ctx.translate(this.props.rect.x, this.props.rect.y);
73071
+ ctx.beginPath();
73072
+ ctx.rect(0, 0, this.props.rect.width, this.props.rect.height);
73073
+ ctx.clip();
73074
+ const subRenderingCtx = {
73075
+ ...renderingContext,
73076
+ sheetId: this.viewportsStore.displayedSheetId,
73077
+ viewports: this.viewportsStore.viewports
73078
+ };
73079
+ this.gridRender.drawLayer(subRenderingCtx, "Background", void 0);
73080
+ ctx.restore();
73081
+ }
73082
+ };
73083
+
71412
73084
  //#endregion
71413
73085
  //#region src/components/standalone_grid_canvas/figure_renderer_store.ts
71414
73086
  /** Store that draws the figures directly onto the canvas */
@@ -71434,29 +73106,25 @@ var FigureRendererStore = class extends DisposableStore {
71434
73106
  drawLayer(renderingCtx) {
71435
73107
  const { viewports, ctx } = renderingCtx;
71436
73108
  const visibleFigures = viewports.getVisibleFigures(renderingCtx.sheetId);
71437
- const { x: offsetX, y: offsetY } = viewports.getViewportOffset(renderingCtx.sheetId);
73109
+ const scrollOffset = viewports.getViewportOffset(renderingCtx.sheetId);
71438
73110
  for (const figure of visibleFigures) {
71439
- const x = figure.x - offsetX;
71440
- const y = figure.y - offsetY;
73111
+ const figureRect = {
73112
+ ...figure,
73113
+ x: figure.x - scrollOffset.x,
73114
+ y: figure.y - scrollOffset.y
73115
+ };
73116
+ const { x, y, width, height } = figureRect;
71441
73117
  if (figure.tag === "chart") {
71442
73118
  const chartId = this.getters.getChartIdFromFigureId(figure.id);
71443
- if (chartId) {
71444
- const chartRect = {
71445
- x,
71446
- y,
71447
- width: figure.width,
71448
- height: figure.height
71449
- };
71450
- this.drawChart(renderingCtx, chartId, chartRect);
71451
- }
73119
+ if (chartId) this.drawChart(renderingCtx, chartId, figureRect);
71452
73120
  } else if (figure.tag === "image") {
71453
73121
  const loadedImage = this.loadedImages[this.getters.getImagePath(figure.id)];
71454
- if (loadedImage) ctx.drawImage(loadedImage, x, y, figure.width, figure.height);
71455
- } else if (figure.tag === "carousel") this.drawCarousel(renderingCtx, figure);
73122
+ if (loadedImage) ctx.drawImage(loadedImage, x, y, width, height);
73123
+ } else if (figure.tag === "carousel") this.drawCarousel(renderingCtx, figure, figureRect);
71456
73124
  if (!this.getters.isDashboard()) {
71457
73125
  ctx.strokeStyle = GRAY_400;
71458
73126
  ctx.lineWidth = 1;
71459
- ctx.strokeRect(x, y, figure.width, figure.height);
73127
+ ctx.strokeRect(x, y, width, height);
71460
73128
  }
71461
73129
  }
71462
73130
  }
@@ -71474,53 +73142,37 @@ var FigureRendererStore = class extends DisposableStore {
71474
73142
  renderingCtx.ctx.drawImage(chartCanvas, x, y, width, height);
71475
73143
  cleanUp();
71476
73144
  }
71477
- drawCarousel(renderingCtx, figure) {
71478
- const { viewports: sheetView, ctx } = renderingCtx;
71479
- const { x: offsetX, y: offsetY } = sheetView.getViewportOffset(renderingCtx.sheetId);
71480
- const x = figure.x - offsetX;
71481
- const y = figure.y - offsetY;
73145
+ drawCarousel(renderingCtx, figure, figureRect) {
73146
+ const { ctx } = renderingCtx;
71482
73147
  const carousel = this.getters.getCarousel(figure.id);
71483
73148
  const chartId = this.getters.getChartIdFromFigureId(figure.id);
71484
73149
  if (!carousel) return;
73150
+ const layout = getCarouselLayout(figureRect, carousel, this.getters.getSelectedCarouselItem(figure.id));
71485
73151
  const chartDefinition = chartId ? this.getters.getChartDefinition(chartId) : void 0;
73152
+ ctx.save();
73153
+ ctx.beginPath();
73154
+ ctx.rect(figureRect.x, figureRect.y, figureRect.width, figureRect.height);
73155
+ ctx.clip();
73156
+ ctx.fillStyle = chartDefinition?.background || this.getters.getSpreadsheetTheme().backgroundColor;
73157
+ ctx.fillRect(figureRect.x, figureRect.y, figureRect.width, figureRect.height);
71486
73158
  const title = {
71487
73159
  ...DEFAULT_CAROUSEL_TITLE_STYLE,
71488
73160
  ...carousel.title
71489
73161
  };
71490
- const headerPadding = 4;
71491
- const headerSize = title.fontSize + headerPadding * 2;
71492
- if (!title.text && chartId) {
71493
- const chartRect = {
71494
- x,
71495
- y,
71496
- width: figure.width,
71497
- height: figure.height
71498
- };
71499
- this.drawChart(renderingCtx, chartId, chartRect);
71500
- } else if (title.text) {
71501
- ctx.save();
71502
- ctx.fillStyle = chartDefinition?.background || this.getters.getSpreadsheetTheme().backgroundColor;
71503
- ctx.fillRect(x, y, figure.width, headerSize);
71504
- ctx.font = computeTextFont(chartStyleToCellStyle(title));
73162
+ if (title.text) {
73163
+ ctx.font = computeTextFont(chartStyleToCellStyle(title), "px", 500);
71505
73164
  ctx.fillStyle = title.color;
71506
- ctx.fillText(title.text, x + headerPadding, y + headerPadding + title.fontSize);
71507
- ctx.restore();
71508
- const chartRect = {
71509
- x,
71510
- y: y + headerSize,
71511
- width: figure.width,
71512
- height: figure.height - headerSize
71513
- };
71514
- if (chartId) this.drawChart(renderingCtx, chartId, chartRect);
71515
- else if (!this.getters.isDashboard()) {
71516
- ctx.strokeStyle = GRAY_400;
71517
- ctx.lineWidth = 1;
71518
- ctx.beginPath();
71519
- ctx.moveTo(x, y + headerSize);
71520
- ctx.lineTo(x + figure.width, y + headerSize);
71521
- ctx.stroke();
71522
- }
73165
+ ctx.textBaseline = "middle";
73166
+ const textY = Math.ceil(layout.headerRect.y + layout.headerRect.height / 2);
73167
+ ctx.fillText(title.text, layout.headerRect.x, textY);
71523
73168
  }
73169
+ const separator = layout.separatorRect;
73170
+ if (separator) {
73171
+ ctx.fillStyle = GRAY_400;
73172
+ ctx.fillRect(separator.x, separator.y, separator.width, separator.height);
73173
+ }
73174
+ ctx.restore();
73175
+ if (chartId) this.drawChart(renderingCtx, chartId, layout.contentRect);
71524
73176
  }
71525
73177
  };
71526
73178
 
@@ -71528,12 +73180,13 @@ var FigureRendererStore = class extends DisposableStore {
71528
73180
  //#region src/components/standalone_grid_canvas/standalone_grid_canvas.ts
71529
73181
  var StandaloneGridCanvas = class extends Component {
71530
73182
  static template = "o-spreadsheet-StandaloneGridCanvas";
73183
+ static components = { CarouselDataViewPrint };
71531
73184
  props = useProps({
71532
73185
  sheetId: types$1.UID(),
71533
73186
  zone: types$1.Zone(),
71534
73187
  renderingCtx: types$1.object()
71535
73188
  });
71536
- canvasRef = signal(null);
73189
+ canvasRef = signal.ref(HTMLCanvasElement);
71537
73190
  rendererStore;
71538
73191
  figureRendererStore;
71539
73192
  setup() {
@@ -71555,6 +73208,28 @@ var StandaloneGridCanvas = class extends Component {
71555
73208
  this.figureRendererStore.addLoadedImage(path, await createImageBitmap(blob));
71556
73209
  }));
71557
73210
  }
73211
+ get carouselDataViews() {
73212
+ const { sheetId, viewports } = this.props.renderingCtx;
73213
+ return viewports.getVisibleFigures(sheetId).filter((figure) => figure.tag === "carousel").map((figure) => {
73214
+ const carouselItem = this.env.model.getters.getSelectedCarouselItem(figure.id);
73215
+ if (carouselItem?.type !== "carouselDataView" || !carouselItem.range) return;
73216
+ const carousel = this.env.model.getters.getCarousel(figure.id);
73217
+ const scroll = viewports.getViewportOffset(sheetId);
73218
+ const { x, y, width, height } = figure;
73219
+ const layout = getCarouselLayout({
73220
+ x: x - scroll.x,
73221
+ y: y - scroll.y,
73222
+ width,
73223
+ height
73224
+ }, carousel, carouselItem);
73225
+ return {
73226
+ range: carouselItem.range,
73227
+ columnWeights: carouselItem.columnWeights,
73228
+ rect: layout.contentRect,
73229
+ rendererStore: this.rendererStore
73230
+ };
73231
+ }).filter(isDefined);
73232
+ }
71558
73233
  };
71559
73234
 
71560
73235
  //#endregion
@@ -72016,7 +73691,7 @@ var NamedRangeSelector = class extends Component {
72016
73691
  anchorRect: null,
72017
73692
  menuItems: []
72018
73693
  });
72019
- namedRangeSelectorRef = signal(null);
73694
+ namedRangeSelectorRef = signal.ref();
72020
73695
  setup() {
72021
73696
  this.topBarToolStore = useToolBarDropdownStore();
72022
73697
  this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
@@ -72214,7 +73889,7 @@ var BorderEditor = class extends Component {
72214
73889
  anchorRect: types$1.Rect()
72215
73890
  });
72216
73891
  BORDER_POSITIONS = BORDER_POSITIONS;
72217
- lineStyleButtonRef = signal(null);
73892
+ lineStyleButtonRef = signal.ref();
72218
73893
  borderStyles = borderStyles;
72219
73894
  state = proxy({ activeTool: void 0 });
72220
73895
  toggleDropdownTool(tool) {
@@ -72281,7 +73956,7 @@ var BorderEditorWidget = class extends Component {
72281
73956
  });
72282
73957
  topBarToolStore;
72283
73958
  viewStore;
72284
- borderEditorButtonRef = signal(null);
73959
+ borderEditorButtonRef = signal.ref();
72285
73960
  state = proxy({
72286
73961
  currentColor: DEFAULT_BORDER_DESC.color,
72287
73962
  currentStyle: DEFAULT_BORDER_DESC.style,
@@ -72511,7 +74186,7 @@ var DropdownAction = class extends Component {
72511
74186
  childClass: types$1.string()
72512
74187
  });
72513
74188
  topBarToolStore;
72514
- actionRef = signal(null);
74189
+ actionRef = signal.ref();
72515
74190
  setup() {
72516
74191
  this.topBarToolStore = useToolBarDropdownStore();
72517
74192
  }
@@ -72574,7 +74249,7 @@ var NumberFormatsTool = class extends Component {
72574
74249
  props = useProps({ class: types$1.string() });
72575
74250
  formatNumberMenuItemSpec = formatNumberMenuItemSpec;
72576
74251
  topBarToolStore;
72577
- buttonRef = signal(null);
74252
+ buttonRef = signal.ref();
72578
74253
  state = proxy({
72579
74254
  anchorRect: {
72580
74255
  x: 0,
@@ -72863,12 +74538,12 @@ var TopBar = class extends Component {
72863
74538
  composerFocusStore;
72864
74539
  fingerprints;
72865
74540
  topBarToolStore;
72866
- toolBarContainerRef = signal(null);
72867
- toolbarRef = signal(null);
72868
- namedRangesRef = signal(null);
72869
- topBarTopRef = signal(null);
72870
- moreToolsContainerRef = signal(null);
72871
- moreToolsButtonRef = signal(null);
74541
+ toolBarContainerRef = signal.ref();
74542
+ toolbarRef = signal.ref();
74543
+ namedRangesRef = signal.ref();
74544
+ topBarTopRef = signal.ref();
74545
+ moreToolsContainerRef = signal.ref();
74546
+ moreToolsButtonRef = signal.ref();
72872
74547
  spreadsheetRect = useSpreadsheetRect();
72873
74548
  setup() {
72874
74549
  this.composerFocusStore = useStore(ComposerFocusStore);
@@ -73130,7 +74805,7 @@ var Spreadsheet = class extends Component {
73130
74805
  SpreadsheetPrint
73131
74806
  };
73132
74807
  sidePanel;
73133
- spreadsheetRef = signal(null);
74808
+ spreadsheetRef = signal.ref();
73134
74809
  spreadsheetRect = useSpreadsheetRect();
73135
74810
  state = proxy({ printModeEnabled: false });
73136
74811
  _focusGrid;
@@ -79138,12 +80813,13 @@ var FilterMenu = class extends Component {
79138
80813
  }
79139
80814
  sortFilterZone(sortDirection) {
79140
80815
  const filterPosition = this.props.filterPosition;
79141
- const tableZone = this.table?.range.zone;
80816
+ const table = this.table;
80817
+ const tableZone = table?.range.zone;
79142
80818
  if (!filterPosition || !tableZone || tableZone.top === tableZone.bottom) return;
79143
80819
  const sheetId = this.viewStore.displayedSheetId;
79144
80820
  const contentZone = {
79145
80821
  ...tableZone,
79146
- top: tableZone.top + 1
80822
+ top: tableZone.top + table.config.numberOfHeaders
79147
80823
  };
79148
80824
  const sortAnchor = {
79149
80825
  col: filterPosition.col,
@@ -79301,9 +80977,9 @@ var LinkEditor = class extends Component {
79301
80977
  onClosed: types$1.function().optional()
79302
80978
  });
79303
80979
  static size = { maxHeight: 500 };
79304
- urlInput = signal(null);
79305
- suggestionListRef = signal(null);
79306
- urlInputContainer = signal(null);
80980
+ urlInput = signal.ref(HTMLInputElement);
80981
+ suggestionListRef = signal.ref();
80982
+ urlInputContainer = signal.ref();
79307
80983
  state;
79308
80984
  viewStore;
79309
80985
  setup() {
@@ -81421,7 +83097,7 @@ const ARRAYTOTEXT = {
81421
83097
  else if (_format === 0) {
81422
83098
  const rowSeparator = this.locale.decimalSeparator === "," ? "/" : ",";
81423
83099
  return { value: transposeMatrix(_array).flatMap((row) => row.map((value) => {
81424
- return isEvaluationError(value.value) ? value.value : toString(value);
83100
+ return isEvaluationError(value.value) ? value.value : toLocaleString(value, this.locale);
81425
83101
  })).join(rowSeparator) };
81426
83102
  } else return new EvaluationError(_t("Format must be 0 or 1"));
81427
83103
  },
@@ -87469,7 +89145,7 @@ const CONCAT = {
87469
89145
  description: _t("Concatenation of two values."),
87470
89146
  args: [arg("value1 (string)", _t("The value to which value2 will be appended.")), arg("value2 (string)", _t("The value to append to value1."))],
87471
89147
  compute: function(value1, value2) {
87472
- return { value: toString(value1) + toString(value2) };
89148
+ return { value: toLocaleString(value1, this.locale) + toLocaleString(value2, this.locale) };
87473
89149
  },
87474
89150
  isExported: true
87475
89151
  };
@@ -88352,7 +90028,7 @@ const CLEAN = {
88352
90028
  description: _t("Remove non-printable characters from a piece of text."),
88353
90029
  args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))],
88354
90030
  compute: function(text) {
88355
- const _text = toString(text);
90031
+ const _text = toLocaleString(text, this.locale);
88356
90032
  let cleanedStr = "";
88357
90033
  for (const char of _text) if (char && char.charCodeAt(0) > 31) cleanedStr += char;
88358
90034
  return { value: cleanedStr };
@@ -88363,7 +90039,7 @@ const CONCATENATE = {
88363
90039
  description: _t("Appends strings to one another."),
88364
90040
  args: [arg("string (string, range<string>, repeating)", _t("String to append in sequence."))],
88365
90041
  compute: function(...datas) {
88366
- return { value: reduceAny(datas, (acc, a) => acc + toString(a), "") };
90042
+ return { value: reduceAny(datas, (acc, a) => acc + toLocaleString(a, this.locale), "") };
88367
90043
  },
88368
90044
  isExported: true
88369
90045
  };
@@ -88371,7 +90047,7 @@ const EXACT = {
88371
90047
  description: _t("Tests whether two strings are identical."),
88372
90048
  args: [arg("string1 (string)", _t("The first string to compare.")), arg("string2 (string)", _t("The second string to compare."))],
88373
90049
  compute: function(string1, string2) {
88374
- return { value: toString(string1) === toString(string2) };
90050
+ return { value: toLocaleString(string1, this.locale) === toLocaleString(string2, this.locale) };
88375
90051
  },
88376
90052
  isExported: true
88377
90053
  };
@@ -88383,8 +90059,8 @@ const FIND = {
88383
90059
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
88384
90060
  ],
88385
90061
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
88386
- const _searchFor = toString(searchFor);
88387
- const _textToSearch = toString(textToSearch);
90062
+ const _searchFor = toLocaleString(searchFor, this.locale);
90063
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
88388
90064
  const _startingAt = toNumber(startingAt, this.locale);
88389
90065
  if (_textToSearch === "") return new EvaluationError(_t("The text_to_search must be non-empty."));
88390
90066
  if (_startingAt < 1) return new EvaluationError(_t("The starting_at (%s) must be greater than or equal to 1.", _startingAt));
@@ -88421,8 +90097,8 @@ const JOIN = {
88421
90097
  description: _t("Concatenates elements of arrays with delimiter."),
88422
90098
  args: [arg("delimiter (string)", _t("The character or string to place between each concatenated value.")), arg("value_or_array (string, range<string>, repeating)", _t("Value to be appended using delimiter."))],
88423
90099
  compute: function(delimiter, ...valuesOrArrays) {
88424
- const _delimiter = toString(delimiter);
88425
- return { value: reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), "") };
90100
+ const _delimiter = toLocaleString(delimiter, this.locale);
90101
+ return { value: reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toLocaleString(a, this.locale), "") };
88426
90102
  }
88427
90103
  };
88428
90104
  const LEFT = {
@@ -88431,7 +90107,7 @@ const LEFT = {
88431
90107
  compute: function(text, ...args) {
88432
90108
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
88433
90109
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
88434
- return { value: toString(text).substring(0, _numberOfCharacters) };
90110
+ return { value: toLocaleString(text, this.locale).substring(0, _numberOfCharacters) };
88435
90111
  },
88436
90112
  isExported: true
88437
90113
  };
@@ -88439,7 +90115,7 @@ const LEN = {
88439
90115
  description: _t("Length of a string."),
88440
90116
  args: [arg("text (string)", _t("The string whose length will be returned."))],
88441
90117
  compute: function(text) {
88442
- return { value: toString(text).length };
90118
+ return { value: toLocaleString(text, this.locale).length };
88443
90119
  },
88444
90120
  isExported: true
88445
90121
  };
@@ -88447,7 +90123,7 @@ const LOWER = {
88447
90123
  description: _t("Converts a specified string to lowercase."),
88448
90124
  args: [arg("text (string)", _t("The string to convert to lowercase."))],
88449
90125
  compute: function(text) {
88450
- return { value: toString(text).toLowerCase() };
90126
+ return { value: toLocaleString(text, this.locale).toLowerCase() };
88451
90127
  },
88452
90128
  isExported: true
88453
90129
  };
@@ -88459,7 +90135,7 @@ const MID = {
88459
90135
  arg("extract_length (number)", _t("The length of the segment to extract."))
88460
90136
  ],
88461
90137
  compute: function(text, starting_at, extract_length) {
88462
- const _text = toString(text);
90138
+ const _text = toLocaleString(text, this.locale);
88463
90139
  const _starting_at = toNumber(starting_at, this.locale);
88464
90140
  const _extract_length = toNumber(extract_length, this.locale);
88465
90141
  if (_starting_at < 1) return new EvaluationError(_t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString()));
@@ -88472,7 +90148,7 @@ const PROPER = {
88472
90148
  description: _t("Capitalizes each word in a specified string."),
88473
90149
  args: [arg("text_to_capitalize (string)", _t("The text which will be returned with the first letter of each word in uppercase and all other letters in lowercase."))],
88474
90150
  compute: function(text) {
88475
- return { value: toString(text).replace(wordRegex, (word) => {
90151
+ return { value: toLocaleString(text, this.locale).replace(wordRegex, (word) => {
88476
90152
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
88477
90153
  }) };
88478
90154
  },
@@ -88622,8 +90298,8 @@ const REPLACE = {
88622
90298
  if (_position < 1) return new EvaluationError(_t("The position (%s) must be greater than or equal to 1.", _position));
88623
90299
  const _length = toNumber(length, this.locale);
88624
90300
  if (_length < 0) return new EvaluationError(_t("The length (%s) must be positive or zero.", _length));
88625
- const _text = toString(text);
88626
- const _newText = toString(newText);
90301
+ const _text = toLocaleString(text, this.locale);
90302
+ const _newText = toLocaleString(newText, this.locale);
88627
90303
  return { value: _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length) };
88628
90304
  },
88629
90305
  isExported: true
@@ -88634,7 +90310,7 @@ const RIGHT = {
88634
90310
  compute: function(text, ...args) {
88635
90311
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
88636
90312
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or zero.", _numberOfCharacters));
88637
- const _text = toString(text);
90313
+ const _text = toLocaleString(text, this.locale);
88638
90314
  const stringLength = _text.length;
88639
90315
  return { value: _text.substring(stringLength - _numberOfCharacters, stringLength) };
88640
90316
  },
@@ -88648,8 +90324,8 @@ const SEARCH = {
88648
90324
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
88649
90325
  ],
88650
90326
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
88651
- const _searchFor = toString(searchFor).toLowerCase();
88652
- const _textToSearch = toString(textToSearch).toLowerCase();
90327
+ const _searchFor = toLocaleString(searchFor, this.locale).toLowerCase();
90328
+ const _textToSearch = toLocaleString(textToSearch, this.locale).toLowerCase();
88653
90329
  const _startingAt = toNumber(startingAt, this.locale);
88654
90330
  if (_textToSearch === "") return {
88655
90331
  value: CellErrorType.GenericError,
@@ -88679,8 +90355,8 @@ const SPLIT = {
88679
90355
  arg(`remove_empty_text (boolean, default=${SPLIT_DEFAULT_REMOVE_EMPTY_TEXT})`, _t("Whether or not to remove empty text messages from the split results. The default behavior is to treat consecutive delimiters as one (if TRUE). If FALSE, empty cells values are added between consecutive delimiters."))
88680
90356
  ],
88681
90357
  computeArray: function(text, delimiter, splitByEach = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, removeEmptyText = { value: SPLIT_DEFAULT_REMOVE_EMPTY_TEXT }) {
88682
- const _text = toString(text);
88683
- const _delimiter = escapeRegExp(toString(delimiter));
90358
+ const _text = toLocaleString(text, this.locale);
90359
+ const _delimiter = escapeRegExp(toLocaleString(delimiter, this.locale));
88684
90360
  const _splitByEach = toBoolean(splitByEach);
88685
90361
  const _removeEmptyText = toBoolean(removeEmptyText);
88686
90362
  if (_delimiter.length <= 0) return new EvaluationError(_t("The delimiter (%s) must be not be empty.", _delimiter));
@@ -88702,10 +90378,10 @@ const SUBSTITUTE = {
88702
90378
  compute: function(textToSearch, searchFor, replaceWith, occurrenceNumber) {
88703
90379
  const _occurrenceNumber = toNumber(occurrenceNumber, this.locale);
88704
90380
  if (_occurrenceNumber < 0) return new EvaluationError(_t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber));
88705
- const _textToSearch = toString(textToSearch);
88706
- const _searchFor = toString(searchFor);
90381
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
90382
+ const _searchFor = toLocaleString(searchFor, this.locale);
88707
90383
  if (_searchFor === "") return { value: _textToSearch };
88708
- const _replaceWith = toString(replaceWith);
90384
+ const _replaceWith = toLocaleString(replaceWith, this.locale);
88709
90385
  const reg = new RegExp(escapeRegExp(_searchFor), "g");
88710
90386
  if (_occurrenceNumber === 0) return { value: _textToSearch.replace(reg, _replaceWith) };
88711
90387
  let n = 0;
@@ -88728,10 +90404,10 @@ const TEXTJOIN = {
88728
90404
  arg("texts (string, range<string>, repeating)", _t("Text item to join."))
88729
90405
  ],
88730
90406
  compute: function(delimiter, ignoreEmpty = { value: TEXTJOIN_DEFAULT_IGNORE_EMPTY }, ...textsOrArrays) {
88731
- const _delimiter = toString(delimiter);
90407
+ const _delimiter = toLocaleString(delimiter, this.locale);
88732
90408
  const _ignoreEmpty = toBoolean(ignoreEmpty);
88733
90409
  let n = 0;
88734
- return { value: reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, "") };
90410
+ return { value: reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toLocaleString(a, this.locale) === "") ? (n++ ? acc + _delimiter : "") + toLocaleString(a, this.locale) : acc, "") };
88735
90411
  },
88736
90412
  isExported: true
88737
90413
  };
@@ -88784,7 +90460,7 @@ const TRIM = {
88784
90460
  description: _t("Removes space characters."),
88785
90461
  args: [arg("text (string)", _t("The text or reference to a cell containing text to be trimmed."))],
88786
90462
  compute: function(text) {
88787
- return { value: trimContent(toString(text)) };
90463
+ return { value: trimContent(toLocaleString(text, this.locale)) };
88788
90464
  },
88789
90465
  isExported: true
88790
90466
  };
@@ -88792,7 +90468,7 @@ const UPPER = {
88792
90468
  description: _t("Converts a specified string to uppercase."),
88793
90469
  args: [arg("text (string)", _t("The string to convert to uppercase."))],
88794
90470
  compute: function(text) {
88795
- return { value: toString(text).toUpperCase() };
90471
+ return { value: toLocaleString(text, this.locale).toUpperCase() };
88796
90472
  },
88797
90473
  isExported: true
88798
90474
  };
@@ -88889,7 +90565,7 @@ const HYPERLINK = {
88889
90565
  args: [arg("url (string)", _t("The full URL of the link enclosed in quotation marks.")), arg("link_label (string, optional)", _t("The text to display in the cell, enclosed in quotation marks."))],
88890
90566
  compute: function(url, linkLabel) {
88891
90567
  const processedUrl = toString(url).trim();
88892
- const processedLabel = toString(linkLabel) || processedUrl;
90568
+ const processedLabel = toLocaleString(linkLabel, this.locale) || processedUrl;
88893
90569
  if (processedUrl === "") return { value: processedLabel };
88894
90570
  return { value: markdownLink(processedLabel, processedUrl) };
88895
90571
  },
@@ -89119,7 +90795,9 @@ const helpers = {
89119
90795
  computeCachedTextDimension,
89120
90796
  createComputeFunction,
89121
90797
  isMobileOS,
89122
- drawHighlight
90798
+ drawHighlight,
90799
+ getBoundingRectWithMargins,
90800
+ hasInteractiveElementInEventTree
89123
90801
  };
89124
90802
  const links = {
89125
90803
  isMarkdownLink,
@@ -89257,6 +90935,6 @@ const chartHelpers = {
89257
90935
  //#endregion
89258
90936
  export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, BadExpressionError, CHART_TYPES, CellErrorType, CellValueType, CircularDependencyError, ClientDisconnectedError, ClipboardMIMEType, CommandResult, CompiledFormula, CorePlugin, CoreViewPlugin, DEFAULT_LOCALE, DEFAULT_LOCALES, DEFAULT_LOCALE_DIGIT_GROUPING, DIRECTION, DispatchResult, DivisionByZeroError, EvaluationError, InvalidReferenceError, LocalTransportService, Model, NEXT_VALUE, NotAvailableError, NumberTooLargeError, OrderedLayers, PREVIOUS_VALUE, PivotRuntimeDefinition, Registry, Revision, SPREADSHEET_DIMENSIONS, SplillBlockedError, Spreadsheet, SpreadsheetPivotTable, UIPlugin, UnknownFunctionError, __info__, addFunction, addRenderingLayer, astToFormula, availableConditionalFormatOperators, availableDataValidationOperators, availableFiltersOperators, borderPositions, borderStyles, canExecuteInReadonly, categories, chartHelpers, compatibility, components, composerFocusTypes, constants, convertAstNodes, coreTypes, createAutocompleteArgumentsProvider, errorTypes, filterDateCriterionOperators, filterNumberCriterionOperators, filterTextCriterionOperators, findCellInNewZone, functionCache, getCaretDownSvg, getCaretUpSvg, helpers, hooks, invalidSubtotalFormulasCommands, invalidateBordersCommands, invalidateCFEvaluationCommands, invalidateChartEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, isCoreCommand, isHeadersDependant, isMatrix, isPositionDependent, isRangeDependant, isSheetDependent, isTargetDependent, isZoneDependent, iterateAstNodes, links, load, lockedSheetAllowedCommands, parse, parseTokens, readonlyAllowedCommands, registries, schemeToColorScale, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
89259
90937
 
89260
- __info__.version = "19.5.0-alpha.11";
89261
- __info__.date = "2026-08-12T09:17:24.237Z";
89262
- __info__.hash = "bc056d7";
90938
+ __info__.version = "19.5.0-alpha.13";
90939
+ __info__.date = "2026-08-21T15:33:44.046Z";
90940
+ __info__.hash = "a73559e";