@odoo/o-spreadsheet 19.5.0-alpha.10 → 19.5.0-alpha.12

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 (52) hide show
  1. package/dist/material_symbols_outlined.css +9 -0
  2. package/dist/o_spreadsheet.cjs +2325 -659
  3. package/dist/o_spreadsheet.css +169 -33
  4. package/dist/o_spreadsheet.esm.js +2317 -651
  5. package/dist/o_spreadsheet.iife.js +2325 -659
  6. package/dist/o_spreadsheet.min.iife.js +344 -344
  7. package/dist/o_spreadsheet.xml +209 -128
  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/data_analysis/chart_suggestion_preview.d.ts +19 -0
  21. package/dist/types/components/side_panel/data_analysis/data_analysis_panel.d.ts +17 -0
  22. package/dist/types/components/side_panel/data_analysis/data_analysis_store.d.ts +15 -0
  23. package/dist/types/components/side_panel/pivot/pivot_layout_configurator/pivot_layout_configurator.d.ts +1 -6
  24. package/dist/types/components/side_panel/side_panel/side_panel_store.d.ts +2 -1
  25. package/dist/types/components/standalone_grid_canvas/carousel_data_view_print.d.ts +32 -0
  26. package/dist/types/components/standalone_grid_canvas/standalone_grid_canvas.d.ts +6 -0
  27. package/dist/types/components/standalone_viewport/standalone_viewport.d.ts +6 -3
  28. package/dist/types/components/standalone_viewport/standalone_viewport_store.d.ts +2 -1
  29. package/dist/types/components/tables/hovered_table_store.d.ts +6 -7
  30. package/dist/types/constants.d.ts +13 -1
  31. package/dist/types/functions/function_registry.d.ts +3 -0
  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/charts/chart_data_source_helpers.d.ts +10 -0
  39. package/dist/types/helpers/figures/charts/chart_suggestion_engine.d.ts +8 -0
  40. package/dist/types/helpers/figures/charts/chart_ui_common.d.ts +8 -1
  41. package/dist/types/helpers/figures/charts/charts_suggestions_constants.d.ts +37 -0
  42. package/dist/types/helpers/figures/charts/gauge_chart_rendering.d.ts +6 -2
  43. package/dist/types/helpers/text_helper.d.ts +1 -1
  44. package/dist/types/index.d.ts +5 -2
  45. package/dist/types/plugins/core/squisher.d.ts +2 -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/types/chart/chart_suggestion.d.ts +136 -0
  51. package/dist/types/types/commands.d.ts +16 -9
  52. package/package.json +4 -4
@@ -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.10
6
- * @date 2026-08-10T12:33:47.263Z
7
- * @hash 39a6fbc
5
+ * @version 19.5.0-alpha.12
6
+ * @date 2026-08-19T09:47:03.774Z
7
+ * @hash 68ca68e
8
8
  */
9
9
 
10
10
  (function(exports, _odoo_owl) {
@@ -208,6 +208,16 @@ _odoo_owl = __toESM(_odoo_owl, 1);
208
208
  height: rect.height
209
209
  };
210
210
  }
211
+ function getBoundingRectWithMargins(el) {
212
+ const style = getComputedStyle(el);
213
+ const rect = el.getBoundingClientRect();
214
+ return {
215
+ x: rect.x,
216
+ y: rect.y,
217
+ width: rect.width + parseInt(style.marginLeft || "0") + parseInt(style.marginRight || "0"),
218
+ height: rect.height + parseInt(style.marginTop || "0") + parseInt(style.marginBottom || "0")
219
+ };
220
+ }
211
221
  /**
212
222
  * Iterate over all the children of `el` in the dom tree starting at `el`, depth first.
213
223
  */
@@ -651,6 +661,14 @@ _odoo_owl = __toESM(_odoo_owl, 1);
651
661
  fontSize: 18,
652
662
  color: TEXT_BODY
653
663
  };
664
+ const CAROUSEL_LAYOUT = {
665
+ paddingX: 24,
666
+ paddingY: 8,
667
+ headerPaddingTop: 4,
668
+ separatorWidth: 1,
669
+ headerLineHeight: 1.5,
670
+ minHeaderHeight: 25
671
+ };
654
672
  const DEFAULT_TOKEN_COLOR = "light-dark(#000000, #ffffff)";
655
673
  const functionColor = DEFAULT_TOKEN_COLOR;
656
674
  const operatorColor = "#3da4ab";
@@ -2635,7 +2653,7 @@ stores.inject(MyMetaStore, storeInstance);
2635
2653
  *
2636
2654
  * The end goal is to eliminate all compatibility shims.
2637
2655
  */
2638
- const { __ODOO_COMPATIBILITY_LAYER_ADDED__, blockDom: blockDom$1, config, onMounted: onMounted$24, onPatched: onPatched$5, onWillUnmount: onWillUnmount$18, App: OwlApp, Component: OwlComponent, EnvPlugin: OwlEnvPlugin, useChildEnv: OwlUseChildEnv, useChildSubEnv: OwlUseChildSubEnv, useComponent: OwlUseComponent, useEnv: OwlUseEnv, useExternalListener: OwlUseExternalListener, useLayoutEffect: OwlUseLayoutEffect, useSubEnv: OwlUseSubEnv, Plugin, plugin, props, providePlugins, useScope, xml: xml$4 } = _odoo_owl;
2656
+ const { __ODOO_COMPATIBILITY_LAYER_ADDED__, blockDom: blockDom$1, config, onMounted: onMounted$26, onPatched: onPatched$5, onWillUnmount: onWillUnmount$20, App: OwlApp, Component: OwlComponent, EnvPlugin: OwlEnvPlugin, useChildEnv: OwlUseChildEnv, useChildSubEnv: OwlUseChildSubEnv, useComponent: OwlUseComponent, useEnv: OwlUseEnv, useExternalListener: OwlUseExternalListener, useLayoutEffect: OwlUseLayoutEffect, useSubEnv: OwlUseSubEnv, Plugin, plugin, props, providePlugins, useScope, xml: xml$5 } = _odoo_owl;
2639
2657
  const isOdooCompatLoaded = __ODOO_COMPATIBILITY_LAYER_ADDED__ === true;
2640
2658
  var _Component = class extends OwlComponent {
2641
2659
  static template = "";
@@ -2656,8 +2674,8 @@ stores.inject(MyMetaStore, storeInstance);
2656
2674
  function _useExternalListener(target, eventName, handler, eventParams) {
2657
2675
  const node = useScope();
2658
2676
  const boundHandler = handler.bind(node.component);
2659
- onMounted$24(() => target.addEventListener(eventName, boundHandler, eventParams));
2660
- onWillUnmount$18(() => target.removeEventListener(eventName, boundHandler, eventParams));
2677
+ onMounted$26(() => target.addEventListener(eventName, boundHandler, eventParams));
2678
+ onWillUnmount$20(() => target.removeEventListener(eventName, boundHandler, eventParams));
2661
2679
  }
2662
2680
  function onWillRender(cb) {
2663
2681
  const node = useScope();
@@ -2672,7 +2690,7 @@ stores.inject(MyMetaStore, storeInstance);
2672
2690
  let cleanup;
2673
2691
  /** @type {any[]} */
2674
2692
  let dependencies;
2675
- onMounted$24(() => {
2693
+ onMounted$26(() => {
2676
2694
  dependencies = computeDependencies();
2677
2695
  cleanup = effect(...dependencies);
2678
2696
  });
@@ -2689,7 +2707,7 @@ stores.inject(MyMetaStore, storeInstance);
2689
2707
  cleanup = effect(...dependencies);
2690
2708
  }
2691
2709
  });
2692
- onWillUnmount$18(() => cleanup && cleanup());
2710
+ onWillUnmount$20(() => cleanup && cleanup());
2693
2711
  }
2694
2712
  var _EnvPlugin = class extends Plugin {
2695
2713
  static id = "__ENV__";
@@ -2762,7 +2780,7 @@ stores.inject(MyMetaStore, storeInstance);
2762
2780
  }
2763
2781
  };
2764
2782
  var Portal = class extends OwlComponent {
2765
- static template = xml$4`<t t-call-slot="default"/>`;
2783
+ static template = xml$5`<t t-call-slot="default"/>`;
2766
2784
  constructor(node) {
2767
2785
  super(node);
2768
2786
  this.props = props();
@@ -2772,7 +2790,7 @@ stores.inject(MyMetaStore, storeInstance);
2772
2790
  const node = this.__owl__;
2773
2791
  const renderContent = node.renderFn;
2774
2792
  node.renderFn = (...args) => new VPortal(node.props.selector, renderContent(...args));
2775
- onMounted$24(() => {
2793
+ onMounted$26(() => {
2776
2794
  const portal = node.bdom;
2777
2795
  if (!portal.target) {
2778
2796
  const target = document.querySelector(node.props.selector);
@@ -2780,7 +2798,7 @@ stores.inject(MyMetaStore, storeInstance);
2780
2798
  else throw new Error("invalid portal target");
2781
2799
  }
2782
2800
  });
2783
- onWillUnmount$18(() => {
2801
+ onWillUnmount$20(() => {
2784
2802
  node.bdom.remove();
2785
2803
  });
2786
2804
  }
@@ -4073,6 +4091,18 @@ set(value) {
4073
4091
  default: return "";
4074
4092
  }
4075
4093
  }
4094
+ /**
4095
+ * Only converts the decimal separator, ignore number format such as % or dates
4096
+ */
4097
+ function toLocaleString(data, locale) {
4098
+ const value = toValue(data);
4099
+ switch (typeof value) {
4100
+ case "string": return value;
4101
+ case "number": return value.toString().replace(".", locale.decimalSeparator);
4102
+ case "boolean": return value ? "TRUE" : "FALSE";
4103
+ default: return "";
4104
+ }
4105
+ }
4076
4106
  /** Normalize string by setting it to lowercase and replacing accent letters with plain letters */
4077
4107
  const normalizeString = memoize(function normalizeString(str) {
4078
4108
  return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
@@ -5987,6 +6017,9 @@ set(value) {
5987
6017
  }
5988
6018
  };
5989
6019
  const functionRegistry = new FunctionRegistry();
6020
+ /** Formulas using one of these functions are never squished: they are always exported
6021
+ * as a full formula string, and they don't become the base formula of the next cells. */
6022
+ const NonSquishableFunctionRegistry = new Registry();
5990
6023
 
5991
6024
  //#endregion
5992
6025
  //#region src/helpers/references.ts
@@ -9246,8 +9279,8 @@ set(value) {
9246
9279
  function fontSizeInPixels(fontSize) {
9247
9280
  return Math.round(fontSize * 96 / 72);
9248
9281
  }
9249
- function computeTextFont(style, fontUnit = "pt") {
9250
- return `${style.italic ? "italic " : ""}${style.bold ? "bold" : "400"} ${(fontUnit === "pt" ? computeTextFontSizeInPixels(style) : style.fontSize) ?? DEFAULT_FONT_SIZE}px ${DEFAULT_FONT}`;
9282
+ function computeTextFont(style, fontUnit = "pt", fontWeight = 400) {
9283
+ return `${style.italic ? "italic " : ""}${style.bold ? "bold" : fontWeight} ${(fontUnit === "pt" ? computeTextFontSizeInPixels(style) : style.fontSize) ?? DEFAULT_FONT_SIZE}px ${DEFAULT_FONT}`;
9251
9284
  }
9252
9285
  function computeTextFontSizeInPixels(style) {
9253
9286
  return fontSizeInPixels(style?.fontSize || DEFAULT_FONT_SIZE);
@@ -11881,7 +11914,7 @@ set(value) {
11881
11914
  chartId: types.string(),
11882
11915
  isFullScreen: types.boolean().optional()
11883
11916
  });
11884
- canvas = (0, _odoo_owl.signal)(null);
11917
+ canvas = _odoo_owl.signal.ref(HTMLCanvasElement);
11885
11918
  chart;
11886
11919
  currentRuntime;
11887
11920
  animationStore;
@@ -12604,7 +12637,7 @@ set(value) {
12604
12637
  chartId: types.string(),
12605
12638
  isFullScreen: types.boolean().optional()
12606
12639
  });
12607
- canvas = (0, _odoo_owl.signal)(null);
12640
+ canvas = _odoo_owl.signal.ref(HTMLCanvasElement);
12608
12641
  zoomStore;
12609
12642
  get runtime() {
12610
12643
  return this.env.model.getters.getChartRuntime(this.props.chartId);
@@ -13989,6 +14022,44 @@ set(value) {
13989
14022
  const definition = getters.getChartDefinition(item.chartId);
13990
14023
  return (chartSubtypeRegistry.getAll().find((c) => c.matcher?.(definition)) || chartSubtypeRegistry.get(definition.type)).displayName;
13991
14024
  }
14025
+ function getCarouselLayout(figureRect, carousel, selectedItem) {
14026
+ const layout = CAROUSEL_LAYOUT;
14027
+ const title = {
14028
+ ...DEFAULT_CAROUSEL_TITLE_STYLE,
14029
+ ...carousel.title
14030
+ };
14031
+ const x = figureRect.x + layout.paddingX;
14032
+ const width = Math.max(0, figureRect.width - 2 * layout.paddingX);
14033
+ const titleHeight = title.text ? title.fontSize * layout.headerLineHeight : 0;
14034
+ const headerRect = {
14035
+ x,
14036
+ y: figureRect.y + layout.paddingY + layout.headerPaddingTop,
14037
+ width,
14038
+ height: Math.max(layout.minHeaderHeight, titleHeight)
14039
+ };
14040
+ const headerBottom = headerRect.y + headerRect.height;
14041
+ const separatorRect = title.text ? {
14042
+ x,
14043
+ y: headerBottom + layout.paddingY,
14044
+ width,
14045
+ height: layout.separatorWidth
14046
+ } : void 0;
14047
+ const contentPaddingTop = selectedItem?.type === "carouselDataView" ? layout.paddingY : 0;
14048
+ const rectAboveContent = separatorRect || headerRect;
14049
+ const contentY = rectAboveContent.y + rectAboveContent.height + contentPaddingTop;
14050
+ const contentBottom = figureRect.y + figureRect.height - layout.paddingY;
14051
+ return {
14052
+ headerRect,
14053
+ separatorRect,
14054
+ contentRect: {
14055
+ x,
14056
+ y: contentY,
14057
+ width,
14058
+ height: Math.max(0, contentBottom - contentY)
14059
+ },
14060
+ headerLineHeight: layout.headerLineHeight
14061
+ };
14062
+ }
13992
14063
 
13993
14064
  //#endregion
13994
14065
  //#region src/helpers/figures/charts/gauge_chart_rendering.ts
@@ -14000,7 +14071,7 @@ set(value) {
14000
14071
  const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
14001
14072
  const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
14002
14073
  const GAUGE_TITLE_SECTION_HEIGHT = 25;
14003
- function drawGaugeChart(canvas, runtime, zoom = 1, dimensions) {
14074
+ function drawGaugeChart(canvas, runtime, zoom = 1, dimensions, options) {
14004
14075
  const size = dimensions ?? getCanvasSize(canvas);
14005
14076
  const dpr = typeof globalThis.devicePixelRatio === "number" ? globalThis.devicePixelRatio : 1;
14006
14077
  canvas.width = size.width * dpr;
@@ -14013,7 +14084,7 @@ set(value) {
14013
14084
  height: size.height / zoom,
14014
14085
  x: 0,
14015
14086
  y: 0
14016
- }, runtime, ctx);
14087
+ }, runtime, ctx, options);
14017
14088
  drawBackground(ctx, config);
14018
14089
  drawGauge(ctx, config);
14019
14090
  drawInflectionValues(ctx, config);
@@ -14101,7 +14172,8 @@ set(value) {
14101
14172
  ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
14102
14173
  ctx.restore();
14103
14174
  }
14104
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
14175
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx, options) {
14176
+ const labelFontSize = options?.labelFontSize ?? 12;
14105
14177
  const maxValue = runtime.maxValue;
14106
14178
  const minValue = runtime.minValue;
14107
14179
  const gaugeValue = getGaugeValue(runtime, "animated");
@@ -14119,14 +14191,14 @@ set(value) {
14119
14191
  if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
14120
14192
  const minLabelPosition = {
14121
14193
  x: gaugeRect.x + gaugeArcWidth / 2,
14122
- y: gaugeRect.y + gaugeRect.height + 12
14194
+ y: gaugeRect.y + gaugeRect.height + labelFontSize
14123
14195
  };
14124
14196
  const maxLabelPosition = {
14125
14197
  x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
14126
- y: gaugeRect.y + gaugeRect.height + 12
14198
+ y: gaugeRect.y + gaugeRect.height + labelFontSize
14127
14199
  };
14128
14200
  const textColor = chartMutedFontColor(runtime.background);
14129
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
14201
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx, labelFontSize);
14130
14202
  let x = 0, titleWidth = 0, titleHeight = 0;
14131
14203
  if (runtime.title.text) ({width: titleWidth, height: titleHeight} = computeTextDimension(ctx, runtime.title.text, {
14132
14204
  fontSize: 16,
@@ -14174,13 +14246,13 @@ set(value) {
14174
14246
  minLabel: {
14175
14247
  label: runtime.minValue.label,
14176
14248
  textPosition: minLabelPosition,
14177
- fontSize: 12,
14249
+ fontSize: labelFontSize,
14178
14250
  color: textColor
14179
14251
  },
14180
14252
  maxLabel: {
14181
14253
  label: runtime.maxValue.label,
14182
14254
  textPosition: maxLabelPosition,
14183
- fontSize: 12,
14255
+ fontSize: labelFontSize,
14184
14256
  color: textColor
14185
14257
  }
14186
14258
  };
@@ -14214,27 +14286,27 @@ set(value) {
14214
14286
  *
14215
14287
  * Also compute an offset for the text so that it doesn't overlap with other text.
14216
14288
  */
14217
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
14289
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx, labelFontSize = 12) {
14218
14290
  const maxValue = runtime.maxValue;
14219
14291
  const minValue = runtime.minValue;
14220
14292
  const gaugeCircleCenter = {
14221
14293
  x: gaugeRect.x + gaugeRect.width / 2,
14222
14294
  y: gaugeRect.y + gaugeRect.height
14223
14295
  };
14224
- const textStyle = { fontSize: 12 };
14296
+ const textStyle = { fontSize: labelFontSize };
14225
14297
  const inflectionValues = [];
14226
14298
  const inflectionValuesTextRects = [];
14227
14299
  for (const inflectionValue of runtime.inflectionValues) {
14228
14300
  const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
14229
14301
  const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
14230
14302
  const angle = Math.PI - Math.PI * percentage;
14231
- const textRect = getRectangleTangentToCircle(angle, gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, gaugeCircleCenter.x, gaugeCircleCenter.y, labelWidth + 2, 12);
14232
- const offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect)) ? 12 : 0;
14303
+ const textRect = getRectangleTangentToCircle(angle, gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, gaugeCircleCenter.x, gaugeCircleCenter.y, labelWidth + 2, labelFontSize);
14304
+ const offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect)) ? labelFontSize : 0;
14233
14305
  inflectionValuesTextRects.push(textRect);
14234
14306
  inflectionValues.push({
14235
14307
  rotation: angle,
14236
14308
  label: inflectionValue.label,
14237
- fontSize: 12,
14309
+ fontSize: labelFontSize,
14238
14310
  color: textColor,
14239
14311
  offset
14240
14312
  });
@@ -14354,6 +14426,44 @@ set(value) {
14354
14426
  "mouseup"
14355
14427
  ]
14356
14428
  };
14429
+ /**
14430
+ * Sample down a Chart.js configuration's data points to at most `maxPoints`, evenly spaced.
14431
+ * Used for lightweight previews (drag preview, chart suggestions) where rendering every point
14432
+ * of a large dataset would make Chart.js slow without any visible benefit on a small canvas.
14433
+ */
14434
+ function limitChartConfigDataPoints(config, maxPoints = 150) {
14435
+ const data = config.data;
14436
+ if (!data) return config;
14437
+ return {
14438
+ ...config,
14439
+ data: {
14440
+ ...data,
14441
+ labels: limitArrayDataPoints(data.labels, maxPoints),
14442
+ datasets: data.datasets?.map((dataset) => limitDatasetDataPoints(dataset, maxPoints))
14443
+ }
14444
+ };
14445
+ }
14446
+ function limitDatasetDataPoints(dataset, maxPoints) {
14447
+ const values = dataset.data;
14448
+ if (!Array.isArray(values) || values.length <= maxPoints) return dataset;
14449
+ const length = values.length;
14450
+ const indices = sampleIndices(length, maxPoints);
14451
+ const limited = { ...dataset };
14452
+ for (const key of Object.keys(limited)) {
14453
+ const value = limited[key];
14454
+ if (Array.isArray(value) && value.length === length) limited[key] = indices.map((i) => value[i]);
14455
+ }
14456
+ return limited;
14457
+ }
14458
+ function limitArrayDataPoints(arr, maxPoints) {
14459
+ if (!Array.isArray(arr) || arr.length <= maxPoints) return arr;
14460
+ return sampleIndices(arr.length, maxPoints).map((i) => arr[i]);
14461
+ }
14462
+ /** Evenly spaced indices sampled from [0, length). */
14463
+ function sampleIndices(length, maxPoints) {
14464
+ const step = length / maxPoints;
14465
+ return Array.from({ length: maxPoints }, (_, i) => Math.min(length - 1, Math.floor(i * step)));
14466
+ }
14357
14467
  async function chartToImageUrl(runtime, figure, type) {
14358
14468
  try {
14359
14469
  const canvas = createRenderingSurface(figure.width, figure.height);
@@ -14401,7 +14511,7 @@ set(value) {
14401
14511
  *
14402
14512
  * @returns a cleanup function to be called after the drawing is no longer needed (to free Chart.js resources)
14403
14513
  */
14404
- function drawChartOnCanvas(canvas, runtime, size, type) {
14514
+ function drawChartOnCanvas(canvas, runtime, size, type, zoom = 1) {
14405
14515
  if ("chartJsConfig" in runtime) {
14406
14516
  if (!globalThis.Chart) throw new Error("Chart.js library is not loaded");
14407
14517
  const extensionsLoaded = areChartJSExtensionsLoaded();
@@ -14417,8 +14527,8 @@ set(value) {
14417
14527
  chart.destroy();
14418
14528
  if (!extensionsLoaded) unregisterChartJsExtensions();
14419
14529
  };
14420
- } else if (type === "scorecard") drawScoreChart(getScorecardConfiguration(size, runtime), canvas);
14421
- else if (type === "gauge") drawGaugeChart(canvas, runtime, 1, size);
14530
+ } else if (type === "scorecard") drawScoreChart(getScorecardConfiguration(size, runtime), canvas, zoom);
14531
+ else if (type === "gauge") drawGaugeChart(canvas, runtime, zoom, size);
14422
14532
  return () => {};
14423
14533
  }
14424
14534
 
@@ -14956,7 +15066,7 @@ set(value) {
14956
15066
  var ZoomableChartJsComponent = class extends ChartJsComponent {
14957
15067
  static template = "o-spreadsheet-ZoomableChartJsComponent";
14958
15068
  store;
14959
- masterChartCanvas = (0, _odoo_owl.signal)(null);
15069
+ masterChartCanvas = _odoo_owl.signal.ref(HTMLCanvasElement);
14960
15070
  masterChart;
14961
15071
  mode;
14962
15072
  hasLinearScale;
@@ -15710,7 +15820,7 @@ set(value) {
15710
15820
  chartId: types.string(),
15711
15821
  isFullScreen: types.boolean().optional()
15712
15822
  });
15713
- canvas = (0, _odoo_owl.signal)(null);
15823
+ canvas = _odoo_owl.signal.ref(HTMLCanvasElement);
15714
15824
  animationStore;
15715
15825
  zoomStore;
15716
15826
  get runtime() {
@@ -15947,7 +16057,7 @@ set(value) {
15947
16057
  onKeyDown: types.function().optional(),
15948
16058
  disableKeyboardNavigation: types.boolean().optional()
15949
16059
  });
15950
- menuRef = (0, _odoo_owl.signal)(null);
16060
+ menuRef = _odoo_owl.signal.ref();
15951
16061
  setup() {
15952
16062
  useLayoutEffect(() => {
15953
16063
  const menuEl = this.menuRef();
@@ -16067,8 +16177,8 @@ set(value) {
16067
16177
  zIndex: types.number().optional(),
16068
16178
  class: types.string().optional()
16069
16179
  });
16070
- popoverRef = (0, _odoo_owl.signal)(null);
16071
- popoverContentRef = (0, _odoo_owl.signal)(null);
16180
+ popoverRef = _odoo_owl.signal.ref();
16181
+ popoverContentRef = _odoo_owl.signal.ref();
16072
16182
  currentPosition = void 0;
16073
16183
  currentDisplayValue = void 0;
16074
16184
  spreadsheetRect = useSpreadsheetRect();
@@ -16256,7 +16366,7 @@ set(value) {
16256
16366
  isHoveringChild: false
16257
16367
  });
16258
16368
  state = (0, _odoo_owl.proxy)({ hoveredMenu: this.props.autoSelectFirstItem ? this.getNextEnabledMenuItem() : void 0 });
16259
- menuRef = (0, _odoo_owl.signal)(null);
16369
+ menuRef = _odoo_owl.signal.ref();
16260
16370
  openingTimeOut = useTimeOut();
16261
16371
  setup() {
16262
16372
  const domFocusableElementStore = useStore(DOMFocusableElementStore);
@@ -16545,43 +16655,29 @@ set(value) {
16545
16655
  };
16546
16656
 
16547
16657
  //#endregion
16548
- //#region src/components/tables/hovered_table_store.ts
16549
- var HoveredTableStore = class extends SpreadsheetStore {
16550
- mutators = ["clear", "hover"];
16551
- position;
16658
+ //#region src/stores/cell_hover_overlay_store.ts
16659
+ var CellHoverOverlayStore = class extends SpreadsheetStore {
16660
+ mutators = [
16661
+ "hover",
16662
+ "register",
16663
+ "unRegister"
16664
+ ];
16665
+ storeGetters = ["getCellHoverOverlayColor"];
16666
+ providers = [];
16552
16667
  overlayColors = new PositionMap();
16553
- hover(position) {
16554
- if (!this.getters.isDashboard() || deepEquals(this.position, position)) return "noStateChange";
16555
- this.position = position;
16556
- this.computeOverlay();
16668
+ hover(hoveredPosition) {
16669
+ this.overlayColors = new PositionMap();
16670
+ if (!hoveredPosition) return;
16671
+ for (const provider of this.providers) {
16672
+ const positions = provider.getHighlightedPositions(hoveredPosition);
16673
+ for (const position of positions) this.overlayColors.set(position, TABLE_HOVER_BACKGROUND_COLOR);
16674
+ }
16557
16675
  }
16558
- clear() {
16559
- this.position = void 0;
16676
+ register(highlightProvider) {
16677
+ this.providers.push(highlightProvider);
16560
16678
  }
16561
- computeOverlay() {
16562
- this.overlayColors = new PositionMap();
16563
- if (!this.position) return;
16564
- const { sheetId, col, row } = this.position;
16565
- const table = this.getters.getTable({
16566
- sheetId,
16567
- col,
16568
- row
16569
- });
16570
- if (!table) return;
16571
- const { left, right, top } = table.range.zone;
16572
- const isTableHeader = row < top + table.config.numberOfHeaders;
16573
- const doesTableRowHaveContent = range(left, right + 1).some((col) => {
16574
- return !this.getters.isColHidden(sheetId, col) && this.getters.getEvaluatedCell({
16575
- sheetId,
16576
- col,
16577
- row
16578
- }).formattedValue;
16579
- });
16580
- if (!isTableHeader && doesTableRowHaveContent) for (let col = left; col <= right; col++) this.overlayColors.set({
16581
- sheetId,
16582
- col,
16583
- row
16584
- }, TABLE_HOVER_BACKGROUND_COLOR);
16679
+ unRegister(highlightProvider) {
16680
+ this.providers = this.providers.filter((h) => h !== highlightProvider);
16585
16681
  }
16586
16682
  };
16587
16683
 
@@ -16593,9 +16689,9 @@ set(value) {
16593
16689
  position: types.CellPosition(),
16594
16690
  sortDirection: types.or([types.SortDirection, types.literal("none")])
16595
16691
  });
16596
- hoveredTableStore;
16692
+ hoveredCellOverlayStore;
16597
16693
  setup() {
16598
- this.hoveredTableStore = useStore(HoveredTableStore);
16694
+ this.hoveredCellOverlayStore = useStore(CellHoverOverlayStore);
16599
16695
  }
16600
16696
  get style() {
16601
16697
  const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.position);
@@ -16615,7 +16711,7 @@ set(value) {
16615
16711
  }
16616
16712
  }
16617
16713
  getBackgroundColor(cellStyle) {
16618
- const overlayColor = this.hoveredTableStore.overlayColors.get(this.props.position);
16714
+ const overlayColor = this.hoveredCellOverlayStore.overlayColors.get(this.props.position);
16619
16715
  if (overlayColor) return blendColors(cellStyle.fillColor || "#FFFFFF", overlayColor);
16620
16716
  return cellStyle.fillColor || "#FFFFFF";
16621
16717
  }
@@ -21354,7 +21450,7 @@ set(value) {
21354
21450
  //#region src/components/grid_overlay/grid_overlay.ts
21355
21451
  function useCellHovered(env, gridRef) {
21356
21452
  const delayedHoveredCell = useStore(DelayedHoveredCellStore);
21357
- const hoveredTable = useStore(HoveredTableStore);
21453
+ const cellHoverOverlay = useStore(CellHoverOverlayStore);
21358
21454
  const viewStore = useStore(ViewportsStore);
21359
21455
  const hoveredPosition = {
21360
21456
  col: void 0,
@@ -21398,7 +21494,9 @@ set(value) {
21398
21494
  if (isChildEvent(gridRef(), zoomedMouseEvent.ev)) {
21399
21495
  ({x, y} = getOffsetRelativeToOverlay(zoomedMouseEvent));
21400
21496
  lastMoved = Date.now();
21401
- hoveredTable.hover(getPosition());
21497
+ const position = getPosition();
21498
+ if (position.col < 0 || position.row < 0) cellHoverOverlay.hover(void 0);
21499
+ else cellHoverOverlay.hover(getPosition());
21402
21500
  }
21403
21501
  }
21404
21502
  function recompute() {
@@ -21409,7 +21507,7 @@ set(value) {
21409
21507
  const zoomedMouseEvent = withZoom(env, e);
21410
21508
  const { x, y } = getOffsetRelativeToOverlay(zoomedMouseEvent);
21411
21509
  const gridRect = getElBoundingRect(gridRef());
21412
- if (y < 0 || y > gridRect.height || x < 0 || x > gridRect.width) return updateMousePosition(zoomedMouseEvent);
21510
+ if (y <= 0 || y >= gridRect.height || x <= 0 || x >= gridRect.width) return updateMousePosition(zoomedMouseEvent);
21413
21511
  else return pause();
21414
21512
  }
21415
21513
  (0, _odoo_owl.useListener)(gridRef, "pointermove", (ev) => !env.isMobile() && updateMousePosition(withZoom(env, ev)));
@@ -21446,7 +21544,7 @@ set(value) {
21446
21544
  gridOverlayDimensions: types.string(),
21447
21545
  hasFooter: types.boolean().optional(() => true)
21448
21546
  });
21449
- gridOverlayRef = (0, _odoo_owl.signal)(null);
21547
+ gridOverlayRef = _odoo_owl.signal.ref();
21450
21548
  cellPopovers;
21451
21549
  paintFormatStore;
21452
21550
  hoveredIconStore;
@@ -21473,7 +21571,7 @@ set(value) {
21473
21571
  return el;
21474
21572
  }
21475
21573
  get style() {
21476
- return this.props.gridOverlayDimensions + cssPropertiesToCss({ cursor: this.hoveredIconStore.hoveredIcon ? "pointer" : "default" });
21574
+ return this.props.gridOverlayDimensions + cssPropertiesToCss({ cursor: this.hoveredIconStore.hoveredIcon ? "pointer" : "inherit" });
21477
21575
  }
21478
21576
  get isPaintingFormat() {
21479
21577
  return this.paintFormatStore.isActive;
@@ -22113,8 +22211,9 @@ set(value) {
22113
22211
  const CELL_ANIMATION_DURATION = 200;
22114
22212
  var GridRenderer = class extends DisposableStore {
22115
22213
  renderer;
22214
+ storeGetters = ["drawLayer"];
22116
22215
  fingerprints;
22117
- hoveredTables;
22216
+ cellHoverOverlayStore = this.get(CellHoverOverlayStore);
22118
22217
  hoveredIcon;
22119
22218
  lastRenderSheetId = void 0;
22120
22219
  lastRenderBoxes = /* @__PURE__ */ new Map();
@@ -22128,15 +22227,14 @@ set(value) {
22128
22227
  const model = get(ModelStore);
22129
22228
  this.getters = model.getters;
22130
22229
  this.fingerprints = get(FormulaFingerprintStore);
22131
- this.hoveredTables = get(HoveredTableStore);
22132
22230
  this.hoveredIcon = get(HoveredIconStore);
22133
22231
  model.on("command-dispatched", this, this.handle);
22134
22232
  model.on("command-finalized", this, this.finalize);
22135
- this.renderer.register(this);
22233
+ if (this.renderer) this.renderer.register(this);
22136
22234
  this.onDispose(() => {
22137
22235
  model.off("command-dispatched", this);
22138
22236
  model.off("command-finalized", this);
22139
- this.renderer.unRegister(this);
22237
+ if (this.renderer) this.renderer.unRegister(this);
22140
22238
  });
22141
22239
  }
22142
22240
  handle(cmd) {
@@ -22680,7 +22778,7 @@ set(value) {
22680
22778
  border: this.getters.getCellComputedBorder(position) || void 0,
22681
22779
  style,
22682
22780
  dataBarFill,
22683
- overlayColor: this.hoveredTables.overlayColors.get(position),
22781
+ overlayColor: this.cellHoverOverlayStore.overlayColors.get(position),
22684
22782
  isError: cell.type === "error" && !!cell.message || this.getters.isDataValidationInvalid(position),
22685
22783
  icons: cellIcons,
22686
22784
  disabledAnimation: this.zonesWithPreventedAnimationsInNextFrame[sheetId]?.some((z) => isZoneInside(zone, z) || overlap(zone, z))
@@ -22856,6 +22954,7 @@ set(value) {
22856
22954
  return boxes;
22857
22955
  }
22858
22956
  getBoxesWithAnimations(boxes, oldBoxes, timeStamp) {
22957
+ if (!this.renderer) return boxes;
22859
22958
  this.updateAnimationsProgress(timeStamp);
22860
22959
  this.addNewAnimations(boxes, oldBoxes, timeStamp);
22861
22960
  if (this.animations.size > 0) {
@@ -23000,7 +23099,7 @@ set(value) {
23000
23099
  offset: types.Pixel(),
23001
23100
  onScroll: types.function()
23002
23101
  });
23003
- scrollbarRef = (0, _odoo_owl.signal)(null);
23102
+ scrollbarRef = _odoo_owl.signal.ref();
23004
23103
  scrollbar;
23005
23104
  setup() {
23006
23105
  this.scrollbar = new ScrollBar$1(this.scrollbarRef(), this.props.direction);
@@ -23102,6 +23201,9 @@ set(value) {
23102
23201
  zoneToDisplay: this.range.zone,
23103
23202
  getFooterSize: () => 0
23104
23203
  });
23204
+ const cellHoverOverlayStore = this.get(CellHoverOverlayStore);
23205
+ cellHoverOverlayStore.register(this);
23206
+ this.onDispose(() => cellHoverOverlayStore.unRegister(this));
23105
23207
  }
23106
23208
  handle(cmd) {
23107
23209
  this.cachedColDimensions = void 0;
@@ -23250,6 +23352,16 @@ set(value) {
23250
23352
  }
23251
23353
  return normalizedWeights;
23252
23354
  }
23355
+ getHighlightedPositions(position) {
23356
+ const highlightedPositions = [];
23357
+ const { col, row, sheetId } = position;
23358
+ if (sheetId !== this.range.sheetId || !isInside(col, row, this.range.zone)) return highlightedPositions;
23359
+ for (let col = this.range.zone.left; col <= this.range.zone.right; col++) highlightedPositions.push({
23360
+ ...position,
23361
+ col
23362
+ });
23363
+ return highlightedPositions;
23364
+ }
23253
23365
  };
23254
23366
 
23255
23367
  //#endregion
@@ -23266,10 +23378,11 @@ set(value) {
23266
23378
  range: types.Range(),
23267
23379
  canResizeColumns: types.boolean().optional(true),
23268
23380
  onResizeColumns: types.function().optional(),
23269
- columnWeights: types.array().optional()
23381
+ columnWeights: types.array().optional(),
23382
+ size: types.object()
23270
23383
  });
23271
- canvasRef = (0, _odoo_owl.signal)(null);
23272
- containerRef = (0, _odoo_owl.signal)(null);
23384
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
23385
+ containerRef = _odoo_owl.signal.ref();
23273
23386
  store;
23274
23387
  dndState = (0, _odoo_owl.proxy)({ col: void 0 });
23275
23388
  onMouseWheel;
@@ -23281,8 +23394,8 @@ set(value) {
23281
23394
  useChildStoreProvider([
23282
23395
  ViewportsStore,
23283
23396
  HoveredIconStore,
23284
- HoveredTableStore,
23285
23397
  ClickableCellsStore,
23398
+ CellHoverOverlayStore,
23286
23399
  DelayedHoveredCellStore,
23287
23400
  CellPopoverStore
23288
23401
  ]);
@@ -23291,18 +23404,10 @@ set(value) {
23291
23404
  this.zoomStore = useStore(ZoomStore);
23292
23405
  this.cellPopoverStore = useStore(CellPopoverStore);
23293
23406
  this.rendererStore = useLocalStore(RendererStore, ["Background", "Chart"]);
23294
- const resizeObserver = new ResizeObserver(() => {
23295
- this.store.setContainerSize(this.containerWidth, this.containerHeight);
23296
- });
23297
- (0, _odoo_owl.useEffect)(() => {
23298
- const el = this.containerRef();
23299
- if (el) resizeObserver.observe(el);
23300
- return () => resizeObserver.disconnect();
23301
- });
23302
23407
  (0, _odoo_owl.useEffect)(() => {
23303
23408
  this.store.setRange(this.props.range);
23304
23409
  if (this.dndState.col === void 0) this.store.setCustomColWeights(this.props.columnWeights);
23305
- this.store.setContainerSize(this.containerWidth, this.containerHeight);
23410
+ this.store.setContainerSize(this.contentWidth, this.props.size.height);
23306
23411
  });
23307
23412
  useGridDrawing({
23308
23413
  canvasRef: this.canvasRef,
@@ -23321,14 +23426,11 @@ set(value) {
23321
23426
  }
23322
23427
  });
23323
23428
  }
23324
- get containerWidth() {
23325
- return Math.floor(getElBoundingRect(this.containerRef()).width / this.zoomStore.zoomLevel);
23326
- }
23327
- get containerHeight() {
23328
- return Math.floor(getElBoundingRect(this.containerRef()).height / this.zoomStore.zoomLevel);
23429
+ get contentWidth() {
23430
+ return this.hasVerticalScrollBar ? this.props.size.width - this.zoomStore.scrollBarWidth : this.props.size.width;
23329
23431
  }
23330
23432
  get hasVerticalScrollBar() {
23331
- return this.containerHeight && this.viewStore.mainViewportCoordinates.y + this.viewStore.mainViewportRect.height > this.containerHeight;
23433
+ return this.viewStore.mainViewportCoordinates.y + this.viewStore.mainViewportRect.height > this.props.size.height;
23332
23434
  }
23333
23435
  get scrollBarContainerStyle() {
23334
23436
  return cssPropertiesToCss({ width: `${this.zoomStore.scrollBarWidth}px` });
@@ -23380,7 +23482,7 @@ set(value) {
23380
23482
  };
23381
23483
  const onMouseMove = (ev) => {
23382
23484
  deltaX = withZoom(this.env, ev).clientX - initialX;
23383
- const weightDelta = deltaX / this.containerWidth * totalWeight;
23485
+ const weightDelta = deltaX / this.props.size.width * totalWeight;
23384
23486
  this.store.resizeColumn(resizer.col, weightDelta, startingColWeights);
23385
23487
  };
23386
23488
  startDnd(onMouseMove, onMouseUp);
@@ -23410,8 +23512,8 @@ set(value) {
23410
23512
  name: types.string().optional(),
23411
23513
  title: types.string().optional()
23412
23514
  });
23413
- selectRef = (0, _odoo_owl.signal)(null);
23414
- dropdownRef = (0, _odoo_owl.signal)(null);
23515
+ selectRef = _odoo_owl.signal.ref();
23516
+ dropdownRef = _odoo_owl.signal.ref();
23415
23517
  state = (0, _odoo_owl.proxy)({
23416
23518
  isPopoverOpen: false,
23417
23519
  hoveredValue: void 0
@@ -23521,7 +23623,7 @@ set(value) {
23521
23623
  setup() {
23522
23624
  useExternalListener(window, "click", this.onExternalClick, { capture: true });
23523
23625
  }
23524
- infoRef = (0, _odoo_owl.signal)(null);
23626
+ infoRef = _odoo_owl.signal.ref();
23525
23627
  get popoverProps() {
23526
23628
  return {
23527
23629
  anchorRect: this.props.anchorRect,
@@ -23666,8 +23768,8 @@ set(value) {
23666
23768
  isFullScreen: types.boolean().optional(),
23667
23769
  openContextMenu: types.function().optional()
23668
23770
  });
23669
- carouselTabsRef = (0, _odoo_owl.signal)(null);
23670
- carouselTabsDropdownRef = (0, _odoo_owl.signal)(null);
23771
+ carouselTabsRef = _odoo_owl.signal.ref();
23772
+ carouselTabsDropdownRef = _odoo_owl.signal.ref();
23671
23773
  menuState = (0, _odoo_owl.proxy)({
23672
23774
  isOpen: false,
23673
23775
  anchorRect: null,
@@ -23735,6 +23837,19 @@ set(value) {
23735
23837
  } else cssProperties["background-color"] = backgroundColor;
23736
23838
  return cssPropertiesToCss(cssProperties);
23737
23839
  }
23840
+ get contentStyle() {
23841
+ return cssPropertiesToCss(this.rectToCss(this.carouselLayout.contentRect));
23842
+ }
23843
+ get headerStyle() {
23844
+ return cssPropertiesToCss({
23845
+ ...this.rectToCss(this.carouselLayout.headerRect),
23846
+ "line-height": String(this.carouselLayout.headerLineHeight)
23847
+ });
23848
+ }
23849
+ get separatorStyle() {
23850
+ const separatorRect = this.carouselLayout.separatorRect;
23851
+ return separatorRect ? cssPropertiesToCss(this.rectToCss(separatorRect)) : "";
23852
+ }
23738
23853
  get title() {
23739
23854
  return this.env.model.getters.dynamicTranslate(this.carousel.title?.text ?? "");
23740
23855
  }
@@ -23744,6 +23859,14 @@ set(value) {
23744
23859
  ...this.carousel.title
23745
23860
  })));
23746
23861
  }
23862
+ rectToCss(rect) {
23863
+ return {
23864
+ top: `${rect.y}px`,
23865
+ left: `${rect.x}px`,
23866
+ width: `${rect.width}px`,
23867
+ height: `${rect.height}px`
23868
+ };
23869
+ }
23747
23870
  updateTabsVisibility() {
23748
23871
  const tabsContainerEl = this.carouselTabsRef();
23749
23872
  const dropDownEl = this.carouselTabsDropdownRef();
@@ -23835,6 +23958,13 @@ set(value) {
23835
23958
  get canResizeDataViewColumns() {
23836
23959
  return !this.env.model.getters.isReadonly() && !this.env.model.getters.isSheetLocked(this.env.model.getters.getActiveSheetId());
23837
23960
  }
23961
+ get carouselLayout() {
23962
+ return getCarouselLayout({
23963
+ ...this.props.figureUI,
23964
+ x: 0,
23965
+ y: 0
23966
+ }, this.carousel, this.selectedCarouselItem);
23967
+ }
23838
23968
  };
23839
23969
 
23840
23970
  //#endregion
@@ -23875,7 +24005,7 @@ set(value) {
23875
24005
  figureUI: types.FigureUI(),
23876
24006
  openContextMenu: types.function()
23877
24007
  });
23878
- menuButtonRef = (0, _odoo_owl.signal)(null);
24008
+ menuButtonRef = _odoo_owl.signal.ref();
23879
24009
  showMenu(ev) {
23880
24010
  if (!this.env.model.getters.getSelectedFigureIds().includes(this.props.figureUI.id)) this.env.model.dispatch("SELECT_FIGURE", {
23881
24011
  figureId: this.props.figureUI.id,
@@ -23903,7 +24033,8 @@ set(value) {
23903
24033
  menuBuilder: getChartMenuActions,
23904
24034
  borderWidth: (getters) => getters.isDashboard() ? 0 : 1,
23905
24035
  hasShadow: (getters) => getters.isDashboard(),
23906
- isRounded: (getters) => getters.isDashboard()
24036
+ isRounded: (getters) => getters.isDashboard(),
24037
+ isThemeDependant: true
23907
24038
  });
23908
24039
  figureRegistry.add("image", {
23909
24040
  Component: ImageFigure,
@@ -23912,14 +24043,16 @@ set(value) {
23912
24043
  borderWidth: () => 0,
23913
24044
  hasShadow: () => false,
23914
24045
  isRounded: () => false,
23915
- menuBuilder: getImageMenuActions
24046
+ menuBuilder: getImageMenuActions,
24047
+ isThemeDependant: false
23916
24048
  });
23917
24049
  figureRegistry.add("carousel", {
23918
24050
  Component: CarouselFigure,
23919
24051
  menuBuilder: getCarouselMenuActions,
23920
24052
  borderWidth: (getters) => getters.isDashboard() ? 0 : 1,
23921
24053
  hasShadow: (getters) => getters.isDashboard(),
23922
- isRounded: (getters) => getters.isDashboard()
24054
+ isRounded: (getters) => getters.isDashboard(),
24055
+ isThemeDependant: true
23923
24056
  });
23924
24057
 
23925
24058
  //#endregion
@@ -23941,10 +24074,13 @@ set(value) {
23941
24074
  anchorRect: null,
23942
24075
  menuItems: []
23943
24076
  });
23944
- figureRef = (0, _odoo_owl.signal)(null);
24077
+ figureRef = _odoo_owl.signal.ref();
23945
24078
  get isSelected() {
23946
24079
  return !this.env.model.getters.isDashboard() && this.env.model.getters.getSelectedFigureIds().includes(this.props.figureUI.id);
23947
24080
  }
24081
+ get isThemeDependant() {
24082
+ return figureRegistry.get(this.props.figureUI.tag).isThemeDependant;
24083
+ }
23948
24084
  get figureRegistry() {
23949
24085
  return figureRegistry;
23950
24086
  }
@@ -29543,6 +29679,35 @@ set(value) {
29543
29679
  }));
29544
29680
  }
29545
29681
 
29682
+ //#endregion
29683
+ //#region src/helpers/figures/charts/chart_data_source_helpers.ts
29684
+ function getUnboundRange(getters, zone) {
29685
+ return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
29686
+ }
29687
+ function isDatasetTitled(getters, zone) {
29688
+ const sheetId = getters.getActiveSheetId();
29689
+ const cell = getters.getEvaluatedCell({
29690
+ sheetId,
29691
+ col: zone.left,
29692
+ row: zone.top
29693
+ });
29694
+ return !["number", "empty"].includes(cell.type);
29695
+ }
29696
+ function dataset(zone, getters, id = "0") {
29697
+ return {
29698
+ dataRange: getUnboundRange(getters, zone),
29699
+ dataSetId: id
29700
+ };
29701
+ }
29702
+ function rangeSource(dataSets, dataSetsHaveTitle, labelRange) {
29703
+ return {
29704
+ type: "range",
29705
+ dataSets,
29706
+ dataSetsHaveTitle,
29707
+ labelRange
29708
+ };
29709
+ }
29710
+
29546
29711
  //#endregion
29547
29712
  //#region src/helpers/figures/charts/smart_chart_engine.ts
29548
29713
  const DEFAULT_BAR_CHART_CONFIG = {
@@ -29573,9 +29738,6 @@ set(value) {
29573
29738
  labelsAsText: false,
29574
29739
  humanize: true
29575
29740
  };
29576
- function getUnboundRange(getters, zone) {
29577
- return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
29578
- }
29579
29741
  function detectColumnType(cells) {
29580
29742
  if (!cells.length) return "empty";
29581
29743
  const counts = {
@@ -29622,14 +29784,6 @@ set(value) {
29622
29784
  totalCount: values.length
29623
29785
  };
29624
29786
  }
29625
- function isDatasetTitled(getters, column) {
29626
- const titleCell = getters.getEvaluatedCell({
29627
- sheetId: getters.getActiveSheetId(),
29628
- col: column.zone.left,
29629
- row: column.zone.top
29630
- });
29631
- return !["number", "empty"].includes(titleCell.type);
29632
- }
29633
29787
  /**
29634
29788
  * Builds a chart definition for a single column selection. The logic to detect the chart type is as follows:
29635
29789
  * - If the column contains a single cell, create a scorecard.
@@ -29641,8 +29795,7 @@ set(value) {
29641
29795
  function buildSingleColumnChart(column, getters) {
29642
29796
  const { type, zone } = column;
29643
29797
  const sheetId = getters.getActiveSheetId();
29644
- const dataSetsHaveTitle = isDatasetTitled(getters, column);
29645
- const dataRange = getUnboundRange(getters, zone);
29798
+ const dataSetsHaveTitle = isDatasetTitled(getters, column.zone);
29646
29799
  const titleCell = getters.getEvaluatedCell({
29647
29800
  sheetId,
29648
29801
  col: zone.left,
@@ -29653,14 +29806,7 @@ set(value) {
29653
29806
  case "percentage": return {
29654
29807
  type: "pie",
29655
29808
  title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29656
- dataSource: {
29657
- type: "range",
29658
- dataSets: [{
29659
- dataRange,
29660
- dataSetId: "0"
29661
- }],
29662
- dataSetsHaveTitle
29663
- },
29809
+ dataSource: rangeSource([dataset(zone, getters)], dataSetsHaveTitle),
29664
29810
  dataSetStyles: {},
29665
29811
  legendPosition: "none"
29666
29812
  };
@@ -29671,45 +29817,28 @@ set(value) {
29671
29817
  return {
29672
29818
  type: "pie",
29673
29819
  title: hasUniqueTitle ? { text: String(titleCell.value) } : {},
29674
- dataSource: {
29675
- type: "range",
29676
- dataSets: [{
29677
- dataRange,
29678
- dataSetId: "0"
29679
- }],
29680
- labelRange: dataRange,
29681
- dataSetsHaveTitle: hasUniqueTitle
29682
- },
29820
+ dataSource: rangeSource([dataset(zone, getters)], hasUniqueTitle, getUnboundRange(getters, zone)),
29683
29821
  dataSetStyles: {},
29684
29822
  aggregated: true,
29685
29823
  legendPosition: "top"
29686
29824
  };
29687
- case "date": return {
29688
- type: "calendar",
29689
- title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29690
- dataSource: {
29691
- type: "range",
29692
- dataSets: [],
29693
- dataSetsHaveTitle,
29694
- labelRange: dataRange
29695
- },
29696
- dataSetStyles: {},
29697
- legendPosition: "left",
29698
- horizontalGroupBy: "day_of_week",
29699
- verticalGroupBy: "month_number"
29700
- };
29825
+ case "date": {
29826
+ const dataRange = getUnboundRange(getters, zone);
29827
+ return {
29828
+ type: "calendar",
29829
+ title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29830
+ dataSource: rangeSource([], dataSetsHaveTitle, dataRange),
29831
+ dataSetStyles: {},
29832
+ legendPosition: "left",
29833
+ horizontalGroupBy: "day_of_week",
29834
+ verticalGroupBy: "month_number"
29835
+ };
29836
+ }
29701
29837
  }
29702
29838
  return {
29703
29839
  ...DEFAULT_BAR_CHART_CONFIG,
29704
29840
  title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29705
- dataSource: {
29706
- type: "range",
29707
- dataSets: [{
29708
- dataRange,
29709
- dataSetId: "0"
29710
- }],
29711
- dataSetsHaveTitle
29712
- },
29841
+ dataSource: rangeSource([dataset(zone, getters)], dataSetsHaveTitle),
29713
29842
  dataSetStyles: {}
29714
29843
  };
29715
29844
  }
@@ -29727,15 +29856,7 @@ set(value) {
29727
29856
  if (columns[1].type === "percentage") return {
29728
29857
  type: "pie",
29729
29858
  title: {},
29730
- dataSource: {
29731
- type: "range",
29732
- dataSets: [{
29733
- dataRange: getUnboundRange(getters, columns[1].zone),
29734
- dataSetId: "0"
29735
- }],
29736
- labelRange: getUnboundRange(getters, columns[0].zone),
29737
- dataSetsHaveTitle: isDatasetTitled(getters, columns[1])
29738
- },
29859
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[1].zone), getUnboundRange(getters, columns[0].zone)),
29739
29860
  dataSetStyles: {},
29740
29861
  aggregated: true,
29741
29862
  legendPosition: "none"
@@ -29743,15 +29864,7 @@ set(value) {
29743
29864
  if (columns[0].type === "number" && columns[1].type === "number") return {
29744
29865
  type: "scatter",
29745
29866
  title: {},
29746
- dataSource: {
29747
- type: "range",
29748
- dataSets: [{
29749
- dataRange: getUnboundRange(getters, columns[1].zone),
29750
- dataSetId: "0"
29751
- }],
29752
- labelRange: getUnboundRange(getters, columns[0].zone),
29753
- dataSetsHaveTitle: isDatasetTitled(getters, columns[1])
29754
- },
29867
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[1].zone), getUnboundRange(getters, columns[0].zone)),
29755
29868
  dataSetStyles: {},
29756
29869
  labelsAsText: false,
29757
29870
  legendPosition: "none"
@@ -29759,49 +29872,25 @@ set(value) {
29759
29872
  if (columns[0].type === "date" && columns[1].type === "number") return {
29760
29873
  ...DEFAULT_LINE_CHART_CONFIG,
29761
29874
  type: "line",
29762
- dataSource: {
29763
- type: "range",
29764
- dataSets: [{
29765
- dataRange: getUnboundRange(getters, columns[1].zone),
29766
- dataSetId: "0"
29767
- }],
29768
- labelRange: getUnboundRange(getters, columns[0].zone),
29769
- dataSetsHaveTitle: isDatasetTitled(getters, columns[0])
29770
- },
29875
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[0].zone), getUnboundRange(getters, columns[0].zone)),
29771
29876
  dataSetStyles: {}
29772
29877
  };
29773
29878
  if (columns[0].type === "text" && columns[1].type === "number") {
29774
29879
  const textColumn = columns[0];
29775
29880
  const numberColumn = columns[1];
29776
29881
  const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
29777
- const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn);
29882
+ const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn.zone);
29778
29883
  if (uniqueCount !== totalCount) return {
29779
29884
  type: "treemap",
29780
29885
  title: {},
29781
- dataSource: {
29782
- type: "range",
29783
- dataSets: [{
29784
- dataRange: getUnboundRange(getters, textColumn.zone),
29785
- dataSetId: "0"
29786
- }],
29787
- labelRange: getUnboundRange(getters, numberColumn.zone),
29788
- dataSetsHaveTitle
29789
- },
29886
+ dataSource: rangeSource([dataset(textColumn.zone, getters)], dataSetsHaveTitle, getUnboundRange(getters, numberColumn.zone)),
29790
29887
  dataSetStyles: {},
29791
29888
  legendPosition: "none"
29792
29889
  };
29793
29890
  }
29794
29891
  return {
29795
29892
  ...DEFAULT_BAR_CHART_CONFIG,
29796
- dataSource: {
29797
- type: "range",
29798
- dataSets: [{
29799
- dataRange: getUnboundRange(getters, columns[1].zone),
29800
- dataSetId: "0"
29801
- }],
29802
- labelRange: getUnboundRange(getters, columns[0].zone),
29803
- dataSetsHaveTitle: isDatasetTitled(getters, columns[1])
29804
- },
29893
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[1].zone), getUnboundRange(getters, columns[0].zone)),
29805
29894
  dataSetStyles: {}
29806
29895
  };
29807
29896
  }
@@ -29815,42 +29904,26 @@ set(value) {
29815
29904
  */
29816
29905
  function buildMultiColumnChart(columns, getters) {
29817
29906
  if (columns.length < 3) throw new Error("buildMultiColumnChart expects at least three columns");
29818
- const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col));
29907
+ const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col.zone));
29819
29908
  const lastColumn = columns[columns.length - 1];
29820
29909
  const columnsExceptLast = columns.slice(0, columns.length - 1);
29821
29910
  if ((lastColumn.type === "percentage" || lastColumn.type === "number") && columnsExceptLast.every((col) => col.type === "text")) {
29822
- const dataSets = columnsExceptLast.map(({ zone }, i) => ({
29823
- dataRange: getUnboundRange(getters, zone),
29824
- dataSetId: i.toString()
29825
- }));
29911
+ const dataSets = columnsExceptLast.map(({ zone }, i) => dataset(zone, getters, String(i)));
29826
29912
  return {
29827
29913
  type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap",
29828
29914
  title: {},
29829
- dataSource: {
29830
- type: "range",
29831
- dataSets,
29832
- dataSetsHaveTitle,
29833
- labelRange: getUnboundRange(getters, lastColumn.zone)
29834
- },
29915
+ dataSource: rangeSource(dataSets, dataSetsHaveTitle, getUnboundRange(getters, lastColumn.zone)),
29835
29916
  dataSetStyles: {},
29836
29917
  legendPosition: "none"
29837
29918
  };
29838
29919
  }
29839
29920
  const firstColumn = columns[0];
29840
29921
  const columnsExceptFirst = columns.slice(1);
29841
- const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }, i) => ({
29842
- dataRange: getUnboundRange(getters, zone),
29843
- dataSetId: i.toString()
29844
- }));
29922
+ const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }, i) => dataset(zone, getters, String(i)));
29845
29923
  if (columnsExceptFirst.every((col) => col.type === "percentage")) return {
29846
29924
  type: "pie",
29847
29925
  title: {},
29848
- dataSource: {
29849
- type: "range",
29850
- dataSets: rangesOfColumnsExceptFirst,
29851
- labelRange: getUnboundRange(getters, firstColumn.zone),
29852
- dataSetsHaveTitle
29853
- },
29926
+ dataSource: rangeSource(rangesOfColumnsExceptFirst, dataSetsHaveTitle, getUnboundRange(getters, firstColumn.zone)),
29854
29927
  dataSetStyles: {},
29855
29928
  aggregated: false,
29856
29929
  legendPosition: "top"
@@ -29858,23 +29931,13 @@ set(value) {
29858
29931
  if (firstColumn.type === "date" && columnsExceptFirst.every((col) => col.type === "number")) return {
29859
29932
  ...DEFAULT_LINE_CHART_CONFIG,
29860
29933
  type: "line",
29861
- dataSource: {
29862
- type: "range",
29863
- dataSets: rangesOfColumnsExceptFirst,
29864
- labelRange: getUnboundRange(getters, firstColumn.zone),
29865
- dataSetsHaveTitle
29866
- },
29934
+ dataSource: rangeSource(rangesOfColumnsExceptFirst, dataSetsHaveTitle, getUnboundRange(getters, firstColumn.zone)),
29867
29935
  dataSetStyles: {},
29868
29936
  legendPosition: "top"
29869
29937
  };
29870
29938
  return {
29871
29939
  ...DEFAULT_BAR_CHART_CONFIG,
29872
- dataSource: {
29873
- type: "range",
29874
- dataSets: rangesOfColumnsExceptFirst,
29875
- labelRange: getUnboundRange(getters, firstColumn.zone),
29876
- dataSetsHaveTitle
29877
- },
29940
+ dataSource: rangeSource(rangesOfColumnsExceptFirst, dataSetsHaveTitle, getUnboundRange(getters, firstColumn.zone)),
29878
29941
  dataSetStyles: {},
29879
29942
  legendPosition: "top"
29880
29943
  };
@@ -29901,17 +29964,10 @@ set(value) {
29901
29964
  function getSmartChartDefinition(zones, getters) {
29902
29965
  const columns = categorizeColumns(zones, getters);
29903
29966
  if (columns.length === 0 || columns.every((col) => col.type === "empty")) {
29904
- const dataSets = columns.map(({ zone }, i) => ({
29905
- dataRange: getUnboundRange(getters, zone),
29906
- dataSetId: i.toString()
29907
- }));
29967
+ const dataSets = columns.map(({ zone }, i) => dataset(zone, getters, String(i)));
29908
29968
  return {
29909
29969
  ...DEFAULT_BAR_CHART_CONFIG,
29910
- dataSource: {
29911
- type: "range",
29912
- dataSets,
29913
- dataSetsHaveTitle: false
29914
- }
29970
+ dataSource: rangeSource(dataSets, false)
29915
29971
  };
29916
29972
  }
29917
29973
  const nonEmptyColumns = columns.filter((col) => col.type !== "empty");
@@ -31599,7 +31655,7 @@ set(value) {
31599
31655
  dragAndDrop = useDragAndDropListItems();
31600
31656
  focusedInputRef = _odoo_owl.signal.ref(HTMLInputElement);
31601
31657
  unfocusedInputRef = _odoo_owl.signal.ref(HTMLInputElement);
31602
- selectionRef = (0, _odoo_owl.signal)(null);
31658
+ selectionRef = _odoo_owl.signal.ref();
31603
31659
  DOMFocusableElementStore;
31604
31660
  store;
31605
31661
  get ranges() {
@@ -32106,7 +32162,7 @@ set(value) {
32106
32162
  dropdownMaxHeight: types.Pixel().optional(),
32107
32163
  class: types.string().optional()
32108
32164
  });
32109
- colorPickerButtonRef = (0, _odoo_owl.signal)(null);
32165
+ colorPickerButtonRef = _odoo_owl.signal.ref();
32110
32166
  get iconStyle() {
32111
32167
  return this.props.currentColor ? `border-color: ${this.props.currentColor}` : "border-bottom-style: hidden";
32112
32168
  }
@@ -32134,8 +32190,8 @@ set(value) {
32134
32190
  });
32135
32191
  dropdown = (0, _odoo_owl.proxy)({ isOpen: false });
32136
32192
  inputRef = _odoo_owl.signal.ref(HTMLInputElement);
32137
- rootEditorRef = (0, _odoo_owl.signal)(null);
32138
- valueListRef = (0, _odoo_owl.signal)(null);
32193
+ rootEditorRef = _odoo_owl.signal.ref();
32194
+ valueListRef = _odoo_owl.signal.ref();
32139
32195
  DOMFocusableElementStore;
32140
32196
  setup() {
32141
32197
  this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
@@ -32441,7 +32497,7 @@ set(value) {
32441
32497
  });
32442
32498
  categories = chartCategories;
32443
32499
  chartTypeByCategories = {};
32444
- popoverRef = (0, _odoo_owl.signal)(null);
32500
+ popoverRef = _odoo_owl.signal.ref();
32445
32501
  setup() {
32446
32502
  (0, _odoo_owl.useListener)(window, "pointerdown", this.onExternalClick.bind(this), { capture: true });
32447
32503
  for (const subtypeProperties of chartSubtypeRegistry.getAll()) {
@@ -32466,7 +32522,7 @@ set(value) {
32466
32522
  static template = "o-spreadsheet-CogWheelMenu";
32467
32523
  static components = { MenuPopover };
32468
32524
  props = (0, _odoo_owl.useProps)({ items: types.array(types.ActionSpec()) });
32469
- buttonRef = (0, _odoo_owl.signal)(null);
32525
+ buttonRef = _odoo_owl.signal.ref();
32470
32526
  menuState = (0, _odoo_owl.proxy)({
32471
32527
  isOpen: false,
32472
32528
  anchorRect: null,
@@ -32510,8 +32566,8 @@ set(value) {
32510
32566
  });
32511
32567
  DEFAULT_CAROUSEL_TITLE_STYLE = DEFAULT_CAROUSEL_TITLE_STYLE;
32512
32568
  dragAndDrop = useDragAndDropListItems();
32513
- previewListRef = (0, _odoo_owl.signal)(null);
32514
- addChartButton = (0, _odoo_owl.signal)(null);
32569
+ previewListRef = _odoo_owl.signal.ref();
32570
+ addChartButton = _odoo_owl.signal.ref(HTMLButtonElement);
32515
32571
  state = (0, _odoo_owl.proxy)({
32516
32572
  popoverProps: void 0,
32517
32573
  currentRange: void 0
@@ -32955,7 +33011,7 @@ set(value) {
32955
33011
  var Collapse = class extends Component {
32956
33012
  static template = "o-spreadsheet-Collapse";
32957
33013
  props = (0, _odoo_owl.useProps)({ isCollapsed: types.boolean() });
32958
- contentRef = (0, _odoo_owl.signal)(null);
33014
+ contentRef = _odoo_owl.signal.ref();
32959
33015
  setup() {
32960
33016
  (0, _odoo_owl.onMounted)(() => {
32961
33017
  if (this.props.isCollapsed) this.contentRef()?.classList.add("d-none");
@@ -33008,7 +33064,7 @@ set(value) {
33008
33064
  TextInput
33009
33065
  };
33010
33066
  props = (0, _odoo_owl.useProps)(chartSidePanelPropsDefinition);
33011
- editorRef = (0, _odoo_owl.signal)(null);
33067
+ editorRef = _odoo_owl.signal.ref();
33012
33068
  state = (0, _odoo_owl.proxy)({
33013
33069
  annotationTextInput: this.props.definition.annotationText || "",
33014
33070
  isOverflowing: false
@@ -33330,7 +33386,7 @@ set(value) {
33330
33386
  onColorPicked: types.function(),
33331
33387
  disableNoColor: types.boolean().optional()
33332
33388
  });
33333
- colorPickerButtonRef = (0, _odoo_owl.signal)(null);
33389
+ colorPickerButtonRef = _odoo_owl.signal.ref();
33334
33390
  state;
33335
33391
  setup() {
33336
33392
  this.state = (0, _odoo_owl.proxy)({ pickerOpened: false });
@@ -36172,6 +36228,7 @@ set(value) {
36172
36228
  getDefinedAxis: () => getDefinedAxis,
36173
36229
  getPieColors: () => getPieColors,
36174
36230
  isTrendLineAxis: () => isTrendLineAxis,
36231
+ limitChartConfigDataPoints: () => limitChartConfigDataPoints,
36175
36232
  shouldRemoveFirstLabel: () => shouldRemoveFirstLabel,
36176
36233
  toExcelDataset: () => toExcelDataset,
36177
36234
  toExcelLabelRange: () => toExcelLabelRange,
@@ -36942,7 +36999,7 @@ set(value) {
36942
36999
  popoverProps: void 0,
36943
37000
  popoverStyle: ""
36944
37001
  });
36945
- popoverRef = (0, _odoo_owl.signal)(null);
37002
+ popoverRef = _odoo_owl.signal.ref(HTMLTableElement);
36946
37003
  setup() {
36947
37004
  useExternalListener(window, "click", this.closePopover);
36948
37005
  }
@@ -37191,7 +37248,7 @@ set(value) {
37191
37248
  onValueSelected: types.function(),
37192
37249
  onValueHovered: types.function()
37193
37250
  });
37194
- autoCompleteListRef = (0, _odoo_owl.signal)(null);
37251
+ autoCompleteListRef = _odoo_owl.signal.ref();
37195
37252
  setup() {
37196
37253
  (0, _odoo_owl.useEffect)(() => {
37197
37254
  const selectedIndex = this.props.selectedIndex;
@@ -37482,7 +37539,7 @@ set(value) {
37482
37539
  anchorRect: types.Rect()
37483
37540
  });
37484
37541
  spreadsheetRect = useSpreadsheetRect();
37485
- bubbleRef = (0, _odoo_owl.signal)(null);
37542
+ bubbleRef = _odoo_owl.signal.ref();
37486
37543
  setup() {
37487
37544
  (0, _odoo_owl.useEffect)(() => {
37488
37545
  const el = this.bubbleRef();
@@ -37527,8 +37584,8 @@ set(value) {
37527
37584
  showAssistant: types.boolean().optional(true)
37528
37585
  });
37529
37586
  DOMFocusableElementStore;
37530
- composerRef = (0, _odoo_owl.signal)(null);
37531
- containerRef = (0, _odoo_owl.signal)(null);
37587
+ composerRef = _odoo_owl.signal.ref();
37588
+ containerRef = _odoo_owl.signal.ref();
37532
37589
  contentHelper = new ContentEditableHelper(this.composerRef());
37533
37590
  composerState = (0, _odoo_owl.proxy)({
37534
37591
  positionStart: 0,
@@ -40030,7 +40087,7 @@ set(value) {
40030
40087
  chartId: types.UID(),
40031
40088
  chartPanelStore: types.Store()
40032
40089
  });
40033
- selectRef = (0, _odoo_owl.signal)(null);
40090
+ selectRef = _odoo_owl.signal.ref();
40034
40091
  state = (0, _odoo_owl.proxy)({ popoverProps: void 0 });
40035
40092
  getSupportedChartTypes() {
40036
40093
  let supportedTypes;
@@ -40598,7 +40655,7 @@ set(value) {
40598
40655
  highlightPositions: []
40599
40656
  });
40600
40657
  store;
40601
- chartCanvasRef = (0, _odoo_owl.signal)(null);
40658
+ chartCanvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
40602
40659
  chart;
40603
40660
  setup() {
40604
40661
  this.store = useStore(ColumnStatisticsStore);
@@ -42026,7 +42083,7 @@ set(value) {
42026
42083
  class: types.string()
42027
42084
  });
42028
42085
  icons = ICONS;
42029
- cfPreviewRef = (0, _odoo_owl.signal)(null);
42086
+ cfPreviewRef = _odoo_owl.signal.ref();
42030
42087
  setup() {
42031
42088
  useHighlightsOnHover(this.cfPreviewRef, this);
42032
42089
  }
@@ -42083,7 +42140,7 @@ set(value) {
42083
42140
  static components = { ConditionalFormatPreview };
42084
42141
  props = (0, _odoo_owl.useProps)({ onCloseSidePanel: types.function() });
42085
42142
  dragAndDrop = useDragAndDropListItems();
42086
- cfListRef = (0, _odoo_owl.signal)(null);
42143
+ cfListRef = _odoo_owl.signal.ref();
42087
42144
  get conditionalFormats() {
42088
42145
  return this.env.model.getters.getConditionalFormats(this.env.model.getters.getActiveSheetId());
42089
42146
  }
@@ -42143,6 +42200,1477 @@ set(value) {
42143
42200
  }
42144
42201
  };
42145
42202
 
42203
+ //#endregion
42204
+ //#region src/stores/chart_drag_store.ts
42205
+ /**
42206
+ * Holds the id of the figure that a dragged chart suggestion is currently
42207
+ * hovering over, so it can be highlighted reactively instead of through direct DOM manipulation.
42208
+ */
42209
+ var ChartDragStore = class {
42210
+ mutators = ["setHighlightedFigure"];
42211
+ highlightedFigureId = void 0;
42212
+ setHighlightedFigure(figureId) {
42213
+ this.highlightedFigureId = figureId;
42214
+ }
42215
+ };
42216
+
42217
+ //#endregion
42218
+ //#region src/components/helpers/chart_drag_and_drop.ts
42219
+ function getDefaultChartFigureSize(type) {
42220
+ if (type === "scorecard") return {
42221
+ width: 213,
42222
+ height: 101
42223
+ };
42224
+ return {
42225
+ width: 536,
42226
+ height: 335
42227
+ };
42228
+ }
42229
+ function getCarouselOverlappingChart(figureUI, otherFigures, matchTags) {
42230
+ if (figureUI.tag !== "chart") return;
42231
+ const figureCenterX = figureUI.x + figureUI.width / 2;
42232
+ const figureCenterY = figureUI.y + figureUI.height / 2;
42233
+ let bestMatch;
42234
+ let smallestDistance = Infinity;
42235
+ for (const figure of otherFigures) {
42236
+ if (!matchTags.includes(figure.tag)) continue;
42237
+ const targetCenterX = figure.x + figure.width / 2;
42238
+ const targetCenterY = figure.y + figure.height / 2;
42239
+ const distanceX = Math.abs(figureCenterX - targetCenterX);
42240
+ const distanceY = Math.abs(figureCenterY - targetCenterY);
42241
+ const squaredDistance = distanceX ** 2 + distanceY ** 2;
42242
+ if (distanceX <= figureUI.width / 2 && distanceY <= figureUI.height / 2 && squaredDistance < smallestDistance) {
42243
+ smallestDistance = squaredDistance;
42244
+ bestMatch = figure;
42245
+ }
42246
+ }
42247
+ return bestMatch;
42248
+ }
42249
+ /**
42250
+ * Start dragging a floating preview of the given chart definition, following the mouse.
42251
+ * On drop, creates the chart on the grid (or inside a carousel if dropped on one).
42252
+ */
42253
+ function startChartDragAndDrop(env, definition, ev) {
42254
+ const zoom = env.getStore(ZoomStore).zoomLevel;
42255
+ const gridPosition = gridOverlayPosition(zoom);
42256
+ const spreadsheet = document.querySelector(".o-spreadsheet");
42257
+ if (!spreadsheet) return;
42258
+ const startX = ev.clientX / zoom;
42259
+ const startY = ev.clientY / zoom;
42260
+ const { width, height } = getDefaultChartFigureSize(definition.type);
42261
+ const figureWidth = width * zoom;
42262
+ const figureHeight = height * zoom;
42263
+ const getters = env.model.getters;
42264
+ const sheetId = getters.getActiveSheetId();
42265
+ let container = null;
42266
+ let destroyChart = void 0;
42267
+ const chartDragStore = env.getStore(ChartDragStore);
42268
+ const previousCursor = document.body.style.cursor;
42269
+ document.body.style.cursor = "grabbing";
42270
+ /** Grid coordinates (in sheet pixels) of a mouse position, or undefined if outside the grid. */
42271
+ const getGridPosition = (clientX, clientY) => {
42272
+ if (clientX > gridPosition.x + gridPosition.width || clientY > gridPosition.y + gridPosition.height) return;
42273
+ const { scrollX, scrollY } = env.getStore(ViewportsStore).activeSheetScrollInfo;
42274
+ return {
42275
+ x: Math.max(0, (clientX - gridPosition.x) / zoom + scrollX),
42276
+ y: Math.max(0, (clientY - gridPosition.y) / zoom + scrollY)
42277
+ };
42278
+ };
42279
+ /** Carousel or standalone chart the dragged chart is dropped onto, if any. */
42280
+ const getOverlappingFigure = (clientX, clientY) => {
42281
+ const position = getGridPosition(clientX, clientY);
42282
+ if (!position) return;
42283
+ const figureUI = {
42284
+ tag: "chart",
42285
+ ...position,
42286
+ width,
42287
+ height
42288
+ };
42289
+ const otherFigures = env.getStore(ViewportsStore).visibleFigures;
42290
+ return getCarouselOverlappingChart(figureUI, otherFigures, ["carousel", "chart"]);
42291
+ };
42292
+ const halfWidth = figureWidth / 2;
42293
+ const halfHeight = figureHeight / 2;
42294
+ const onMouseMove = (e) => {
42295
+ if (Math.abs(e.clientX - startX) <= 5 && Math.abs(e.clientY - startY) <= 5) return;
42296
+ if (container === null) {
42297
+ container = document.createElement("div");
42298
+ container.className = "o-chart-drag-preview os-theme-dependant position-fixed border pe-none";
42299
+ container.style.width = `${width}px`;
42300
+ container.style.height = `${height}px`;
42301
+ container.style.zoom = `${zoom}`;
42302
+ const canvas = document.createElement("canvas");
42303
+ canvas.className = "w-100 h-100";
42304
+ container.appendChild(canvas);
42305
+ spreadsheet.appendChild(container);
42306
+ destroyChart = drawChartOnCanvas(canvas, SpreadsheetChart.fromStrDefinition(getters, sheetId, definition).getRuntime(getters, "newChart"), {
42307
+ width,
42308
+ height
42309
+ }, definition.type, zoom);
42310
+ }
42311
+ container.style.left = `${Math.max(gridPosition.x, (e.clientX - halfWidth) / zoom)}px`;
42312
+ container.style.top = `${Math.max(gridPosition.y, (e.clientY - halfHeight) / zoom)}px`;
42313
+ const overlappingFigure = getOverlappingFigure(e.clientX - halfWidth, e.clientY - halfHeight);
42314
+ container.style.opacity = overlappingFigure?.id ? "0.6" : "0.9";
42315
+ chartDragStore.setHighlightedFigure(overlappingFigure?.id);
42316
+ };
42317
+ const onMouseUp = (mouseEvent) => {
42318
+ chartDragStore.setHighlightedFigure(void 0);
42319
+ if (container !== null) {
42320
+ spreadsheet.removeChild(container);
42321
+ container = null;
42322
+ }
42323
+ destroyChart?.();
42324
+ document.body.style.cursor = previousCursor;
42325
+ let position = getGridPosition(mouseEvent.clientX - halfWidth, mouseEvent.clientY - halfHeight);
42326
+ if (Math.abs(mouseEvent.clientX / zoom - startX) <= 5 && Math.abs(mouseEvent.clientY / zoom - startY) <= 5) position = {
42327
+ x: 0,
42328
+ y: 0
42329
+ };
42330
+ else if (!position || position.x + halfWidth > gridPosition.width) return;
42331
+ const { col, row, offset } = env.getStore(ViewportsStore).viewports.getPositionAnchorOffset(sheetId, position);
42332
+ const payload = {
42333
+ chartId: UuidGenerator.smallUuid(),
42334
+ figureId: UuidGenerator.smallUuid(),
42335
+ sheetId,
42336
+ size: {
42337
+ width,
42338
+ height
42339
+ },
42340
+ definition,
42341
+ col,
42342
+ row,
42343
+ offset
42344
+ };
42345
+ const overlappingFigure = getOverlappingFigure(mouseEvent.clientX - halfWidth, mouseEvent.clientY - halfHeight);
42346
+ if (overlappingFigure?.tag === "carousel") env.model.dispatch("ADD_NEW_CHART_TO_CAROUSEL", {
42347
+ sheetId,
42348
+ figureId: overlappingFigure.id,
42349
+ newChartId: UuidGenerator.smallUuid(),
42350
+ chartDefinition: definition
42351
+ });
42352
+ else if (overlappingFigure?.tag === "chart") env.model.dispatch("CREATE_CHART_AND_MERGE_INTO_CAROUSEL", {
42353
+ chartId: payload.chartId,
42354
+ figureId: payload.figureId,
42355
+ sheetId: payload.sheetId,
42356
+ definition: payload.definition,
42357
+ baseFigureId: overlappingFigure.id
42358
+ });
42359
+ else env.model.dispatch("CREATE_CHART", payload);
42360
+ };
42361
+ startDnd(onMouseMove, onMouseUp);
42362
+ }
42363
+
42364
+ //#endregion
42365
+ //#region src/components/side_panel/data_analysis/chart_suggestion_preview.ts
42366
+ const PREVIEW_BUBBLE_RADIUS_RATIO = .3;
42367
+ const MIN_PREVIEW_BUBBLE_RADIUS = 1;
42368
+ var ChartSuggestionPreview = class extends Component {
42369
+ static template = "o-spreadsheet-ChartSuggestionPreview";
42370
+ props = (0, _odoo_owl.useProps)({
42371
+ definition: types.ChartDefinition(),
42372
+ description: types.string(),
42373
+ onPointerDown: types.function()
42374
+ });
42375
+ chartCanvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
42376
+ chart;
42377
+ setup() {
42378
+ (0, _odoo_owl.onMounted)(() => registerChartJSExtensions());
42379
+ (0, _odoo_owl.onWillUnmount)(() => this.destroyChart());
42380
+ useLayoutEffect(() => {
42381
+ this.updateChart();
42382
+ }, () => [this.props.definition]);
42383
+ }
42384
+ getChartConfiguration() {
42385
+ const getters = this.env.model.getters;
42386
+ const activeSheetId = getters.getActiveSheetId();
42387
+ const runtime = SpreadsheetChart.fromStrDefinition(getters, activeSheetId, this.props.definition).getRuntime(getters, "myChart");
42388
+ if (!("chartJsConfig" in runtime)) return null;
42389
+ let config = runtime.chartJsConfig;
42390
+ const existingScales = config.options?.scales ?? {};
42391
+ config = {
42392
+ ...config,
42393
+ data: this.props.definition.type === "bubble" ? {
42394
+ ...config.data,
42395
+ datasets: (config.data?.datasets || []).map((dataset) => ({
42396
+ ...dataset,
42397
+ pointRadius: this.scaleBubblePointRadius(dataset.pointRadius),
42398
+ pointHoverRadius: this.scaleBubblePointRadius(dataset.pointHoverRadius)
42399
+ }))
42400
+ } : config.data,
42401
+ options: {
42402
+ ...config.options,
42403
+ plugins: {
42404
+ ...config.options?.plugins,
42405
+ legend: { display: false },
42406
+ title: { display: false },
42407
+ tooltip: { enabled: false }
42408
+ },
42409
+ events: [],
42410
+ animation: false,
42411
+ scales: this.getScalesCongif(existingScales)
42412
+ }
42413
+ };
42414
+ return limitChartConfigDataPoints(config);
42415
+ }
42416
+ getScalesCongif(scales) {
42417
+ if (scales.x) scales.x = {
42418
+ ...scales.x,
42419
+ ticks: { display: false },
42420
+ border: { display: false }
42421
+ };
42422
+ if (scales.y) scales.y = {
42423
+ ...scales.y,
42424
+ ticks: { display: false },
42425
+ border: { display: false }
42426
+ };
42427
+ if (scales.r) scales.r = {
42428
+ ...scales.r,
42429
+ ticks: { display: false },
42430
+ pointLabels: { display: false }
42431
+ };
42432
+ return scales;
42433
+ }
42434
+ scaleBubblePointRadius(pointRadius) {
42435
+ if (Array.isArray(pointRadius)) return pointRadius.map((radius) => radius <= 0 ? radius : Math.max(MIN_PREVIEW_BUBBLE_RADIUS, radius * PREVIEW_BUBBLE_RADIUS_RATIO));
42436
+ if (typeof pointRadius === "number") return pointRadius <= 0 ? pointRadius : Math.max(MIN_PREVIEW_BUBBLE_RADIUS, pointRadius * PREVIEW_BUBBLE_RADIUS_RATIO);
42437
+ return pointRadius ?? MIN_PREVIEW_BUBBLE_RADIUS;
42438
+ }
42439
+ updateChart() {
42440
+ this.destroyChart();
42441
+ const config = this.getChartConfiguration();
42442
+ if (!config) {
42443
+ this.drawNativeChart();
42444
+ return;
42445
+ }
42446
+ const ctx = this.chartCanvasRef()?.getContext("2d");
42447
+ if (!ctx || !globalThis.Chart) return;
42448
+ this.chart = new globalThis.Chart(ctx, config);
42449
+ }
42450
+ drawNativeChart() {
42451
+ const canvas = this.chartCanvasRef();
42452
+ if (!canvas) return;
42453
+ const getters = this.env.model.getters;
42454
+ let runtime = SpreadsheetChart.fromStrDefinition(getters, getters.getActiveSheetId(), this.props.definition).getRuntime(getters, "myChart");
42455
+ const { type } = this.props.definition;
42456
+ if (type === "scorecard") {
42457
+ const rect = canvas.getBoundingClientRect();
42458
+ runtime = {
42459
+ ...runtime,
42460
+ keyValueStyle: {
42461
+ ...runtime.keyValueStyle,
42462
+ fontSize: 16
42463
+ }
42464
+ };
42465
+ drawScoreChart(getScorecardConfiguration({
42466
+ width: rect.width || 130,
42467
+ height: rect.height || 120
42468
+ }, runtime), canvas);
42469
+ } else if (type === "gauge") {
42470
+ let gaugeRuntime = runtime;
42471
+ if (gaugeRuntime.title) gaugeRuntime = {
42472
+ ...gaugeRuntime,
42473
+ title: { text: "" }
42474
+ };
42475
+ drawGaugeChart(canvas, gaugeRuntime, 1, void 0, { labelFontSize: 8 });
42476
+ }
42477
+ }
42478
+ destroyChart() {
42479
+ this.chart?.destroy();
42480
+ this.chart = void 0;
42481
+ }
42482
+ };
42483
+
42484
+ //#endregion
42485
+ //#region src/helpers/data_analysis.ts
42486
+ function analyzeColumns(zones, getters) {
42487
+ return getZonesByColumns(zones).map((zone) => analyzeColumn(zone, getters));
42488
+ }
42489
+ function analyzeColumn(zone, getters) {
42490
+ const sheetId = getters.getActiveSheetId();
42491
+ const cells = getters.getEvaluatedCellsInZone(sheetId, zone).filter((c) => c.type !== "empty");
42492
+ if (!cells.length) return {
42493
+ zone,
42494
+ type: "empty",
42495
+ hasHeader: false,
42496
+ rowCount: 0,
42497
+ uniqueCount: 0,
42498
+ uniqueRatio: 0,
42499
+ nonEmpty: []
42500
+ };
42501
+ const firstCell = cells[0];
42502
+ const rest = cells.slice(1);
42503
+ const hasHeader = firstCell.type === "text" && rest.some((c) => c.type !== "text");
42504
+ const dataCells = hasHeader ? rest : cells;
42505
+ const numericValues = dataCells.filter((c) => c.type === "number").map((c) => c.value);
42506
+ const allVals = dataCells.map((c) => String(c.value ?? ""));
42507
+ const uniqueCount = new Set(allVals).size;
42508
+ return {
42509
+ zone,
42510
+ type: computeColumnType(dataCells),
42511
+ header: hasHeader ? firstCell.value : void 0,
42512
+ hasHeader,
42513
+ rowCount: dataCells.length,
42514
+ uniqueCount,
42515
+ uniqueRatio: allVals.length > 0 ? uniqueCount / allVals.length : 0,
42516
+ maxValue: numericValues.length ? numericValues.reduce((max, v) => v > max ? v : max, numericValues[0]) : void 0,
42517
+ nonEmpty: dataCells
42518
+ };
42519
+ }
42520
+ function computeColumnType(cells) {
42521
+ if (cells.length === 0) return "empty";
42522
+ if (cells.every((c) => c.type === "error")) return "error";
42523
+ cells = cells.filter((c) => c.type !== "error");
42524
+ if (cells.every((c) => c.type === "boolean")) return "boolean";
42525
+ if (cells.every((c) => c.type === "number" && !!c.format && isDateTimeFormat(c.format))) return "date";
42526
+ else if (cells.every((c) => c.type === "number")) {
42527
+ if (cells.every((c) => c.format?.includes("%"))) return "percentage";
42528
+ return "number";
42529
+ } else {
42530
+ const textVals = cells.filter((c) => c.type === "text").map((c) => c.value);
42531
+ if (textVals.length > 0) {
42532
+ const unique = new Set(textVals).size;
42533
+ return unique / textVals.length < .75 && unique <= 20 ? "categorical" : "label";
42534
+ }
42535
+ }
42536
+ return "empty";
42537
+ }
42538
+
42539
+ //#endregion
42540
+ //#region src/helpers/figures/charts/charts_suggestions_constants.ts
42541
+ /** Above this many rows, shape-based charts (radar, scatter, pyramid) become unreadable. */
42542
+ const MAX_ROWS_FOR_SHAPE_CHART = 10;
42543
+ function barChart(titleText, source, opts = {}) {
42544
+ return {
42545
+ ...opts,
42546
+ type: "bar",
42547
+ title: { text: titleText },
42548
+ dataSource: source,
42549
+ dataSetStyles: {},
42550
+ legendPosition: opts.legendPosition ?? "none",
42551
+ stacked: opts.stacked ?? false,
42552
+ humanize: true,
42553
+ aggregated: opts.aggregated ?? false
42554
+ };
42555
+ }
42556
+ function lineChart(titleText, source, opts = {}) {
42557
+ return {
42558
+ ...opts,
42559
+ type: "line",
42560
+ title: { text: titleText },
42561
+ dataSource: source,
42562
+ dataSetStyles: {},
42563
+ legendPosition: opts.legendPosition ?? "none",
42564
+ stacked: opts.stacked ?? false,
42565
+ cumulative: opts.cumulative ?? false,
42566
+ labelsAsText: false,
42567
+ humanize: true
42568
+ };
42569
+ }
42570
+ function pieChart(titleText, source, opts = {}) {
42571
+ return {
42572
+ ...opts,
42573
+ type: "pie",
42574
+ title: { text: titleText },
42575
+ dataSource: source,
42576
+ dataSetStyles: {},
42577
+ legendPosition: opts.legendPosition ?? "top",
42578
+ humanize: true
42579
+ };
42580
+ }
42581
+ function radarChart(titleText, source, opts = {}) {
42582
+ return {
42583
+ ...opts,
42584
+ type: "radar",
42585
+ title: { text: titleText },
42586
+ dataSource: source,
42587
+ dataSetStyles: {},
42588
+ legendPosition: opts.legendPosition ?? "none",
42589
+ stacked: false,
42590
+ humanize: true
42591
+ };
42592
+ }
42593
+ function sunburstChart(titleText, source) {
42594
+ return {
42595
+ type: "sunburst",
42596
+ title: { text: titleText },
42597
+ dataSource: source,
42598
+ dataSetStyles: {},
42599
+ legendPosition: "none"
42600
+ };
42601
+ }
42602
+ function treemapChart(titleText, source) {
42603
+ return {
42604
+ type: "treemap",
42605
+ title: { text: titleText },
42606
+ dataSource: source,
42607
+ dataSetStyles: {},
42608
+ legendPosition: "none"
42609
+ };
42610
+ }
42611
+ function calendarChart(titleText, source) {
42612
+ return {
42613
+ type: "calendar",
42614
+ title: { text: titleText },
42615
+ dataSource: source,
42616
+ dataSetStyles: {},
42617
+ legendPosition: "none",
42618
+ horizontalGroupBy: "month_number",
42619
+ verticalGroupBy: "day_of_week",
42620
+ humanize: true
42621
+ };
42622
+ }
42623
+ function scatterChart(titleText, source) {
42624
+ return {
42625
+ type: "scatter",
42626
+ title: { text: titleText },
42627
+ dataSource: source,
42628
+ dataSetStyles: {},
42629
+ legendPosition: "none",
42630
+ labelsAsText: false,
42631
+ humanize: true
42632
+ };
42633
+ }
42634
+ function pyramidChart(titleText, source, opts = {}) {
42635
+ return {
42636
+ type: "pyramid",
42637
+ title: { text: titleText },
42638
+ dataSource: source,
42639
+ dataSetStyles: {},
42640
+ legendPosition: opts.legendPosition ?? "top",
42641
+ stacked: false,
42642
+ horizontal: true,
42643
+ humanize: true
42644
+ };
42645
+ }
42646
+ function comboChart(titleText, source, opts = {}) {
42647
+ return {
42648
+ ...opts,
42649
+ type: "combo",
42650
+ title: { text: titleText },
42651
+ dataSource: source,
42652
+ dataSetStyles: {},
42653
+ legendPosition: "top",
42654
+ humanize: true
42655
+ };
42656
+ }
42657
+ function bubbleChart(titleText, xRange, yRanges, sizeRange, labelRange, dataSetsHaveTitle) {
42658
+ return {
42659
+ type: "bubble",
42660
+ title: { text: titleText },
42661
+ humanize: true,
42662
+ dataSetsHaveTitle,
42663
+ yRanges,
42664
+ xRange,
42665
+ sizeRange,
42666
+ labelRange,
42667
+ labelsAsText: false,
42668
+ legendPosition: "none",
42669
+ bubbleColor: { color: "multiple" },
42670
+ verticalAxisPosition: "left"
42671
+ };
42672
+ }
42673
+ function scorecardChart(titleText, keyValue, opts = {}) {
42674
+ return {
42675
+ ...opts,
42676
+ type: "scorecard",
42677
+ title: { text: titleText },
42678
+ keyValue,
42679
+ baselineMode: opts.baselineMode ?? "difference",
42680
+ baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
42681
+ baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
42682
+ humanize: opts.humanize ?? true
42683
+ };
42684
+ }
42685
+ function gaugeChart(titleText, dataRange, rangeMin, rangeMax) {
42686
+ return {
42687
+ type: "gauge",
42688
+ title: { text: titleText },
42689
+ dataRange,
42690
+ sectionRule: {
42691
+ colors: {
42692
+ lowerColor: DEFAULT_GAUGE_LOWER_COLOR,
42693
+ middleColor: DEFAULT_GAUGE_MIDDLE_COLOR,
42694
+ upperColor: DEFAULT_GAUGE_UPPER_COLOR
42695
+ },
42696
+ rangeMin,
42697
+ rangeMax,
42698
+ lowerInflectionPoint: {
42699
+ type: "percentage",
42700
+ value: "33",
42701
+ operator: "<="
42702
+ },
42703
+ upperInflectionPoint: {
42704
+ type: "percentage",
42705
+ value: "66",
42706
+ operator: "<="
42707
+ }
42708
+ },
42709
+ humanize: true
42710
+ };
42711
+ }
42712
+ /** Pattern A — Single numeric column */
42713
+ const SINGLE_NUMBER_COLUMN_SUGGESTIONS = [
42714
+ {
42715
+ description: _t("Highlights the most recent value compared to the previous one."),
42716
+ isApplicable: ({ rowCount }) => rowCount < 3,
42717
+ build: (ctx) => scorecardChart(ctx.title, ctx.lastCellXC, {
42718
+ baseline: ctx.prevCellXC,
42719
+ baselineMode: "difference"
42720
+ })
42721
+ },
42722
+ {
42723
+ description: _t("Shows the position of the last value within the data's min-max range."),
42724
+ isApplicable: ({ rowCount }) => rowCount === 3,
42725
+ build: (ctx) => gaugeChart(ctx.title, ctx.lastCellXC, `=${ctx.firstCellXC}`, `=${ctx.prevCellXC}`)
42726
+ },
42727
+ {
42728
+ description: _t("Compares individual values side-by-side."),
42729
+ isApplicable: ({ rowCount }) => rowCount > 1,
42730
+ build: (ctx) => barChart(ctx.title, ctx.source)
42731
+ },
42732
+ {
42733
+ description: _t("Shows the evolution of all values over the range."),
42734
+ isApplicable: ({ rowCount }) => rowCount > 2,
42735
+ build: (ctx) => lineChart(ctx.title, ctx.source)
42736
+ },
42737
+ {
42738
+ description: _t("Emphasizes total accumulation over the range."),
42739
+ isApplicable: ({ rowCount }) => rowCount > 2,
42740
+ build: (ctx) => lineChart(ctx.title, ctx.source, {
42741
+ fillArea: true,
42742
+ cumulative: true
42743
+ })
42744
+ }
42745
+ ];
42746
+ /** Pattern B — Single percentage column */
42747
+ const SINGLE_PERCENTAGE_COLUMN_SUGGESTIONS = [
42748
+ {
42749
+ description: _t("Shows the last percentage value with its baseline."),
42750
+ isApplicable: ({ rowCount }) => rowCount < 3,
42751
+ build: (ctx) => scorecardChart(ctx.title, ctx.lastCellXC, {
42752
+ baseline: ctx.prevCellXC,
42753
+ baselineMode: "percentage"
42754
+ })
42755
+ },
42756
+ {
42757
+ description: _t("Natural fit for a 0–100% range."),
42758
+ isApplicable: ({ rowCount }) => rowCount === 1,
42759
+ build: (ctx) => gaugeChart(ctx.title, ctx.lastCellXC, "0", ctx.isAboveOne ? "100" : "1")
42760
+ },
42761
+ {
42762
+ description: _t("Natural fit for a 0–100% range."),
42763
+ isApplicable: ({ rowCount }) => rowCount === 3,
42764
+ build: (ctx) => gaugeChart(ctx.title, ctx.lastCellXC, `=${ctx.firstCellXC}`, `=${ctx.prevCellXC}`)
42765
+ },
42766
+ {
42767
+ description: _t("Shows completion against total."),
42768
+ isApplicable: ({ rowCount }) => rowCount > 1,
42769
+ build: (ctx) => pieChart(ctx.title, ctx.source, { isDoughnut: true })
42770
+ },
42771
+ {
42772
+ description: _t("Compares all percentage values side-by-side."),
42773
+ isApplicable: ({ rowCount }) => rowCount > 1,
42774
+ build: (ctx) => barChart(ctx.title, ctx.source)
42775
+ }
42776
+ ];
42777
+ /** Pattern C — Single date column */
42778
+ const SINGLE_DATE_COLUMN_SUGGESTIONS = [{
42779
+ description: _t("Shows the last date value."),
42780
+ isApplicable: ({ rowCount }) => rowCount === 1,
42781
+ build: (ctx) => scorecardChart(ctx.title, ctx.lastCellXC)
42782
+ }];
42783
+ /** Pattern D — Single categorical column */
42784
+ const SINGLE_CATEGORICAL_COLUMN_SUGGESTIONS = [
42785
+ {
42786
+ description: _t("Shows the share of each category."),
42787
+ build: (ctx) => pieChart(ctx.title, ctx.source, {
42788
+ aggregated: true,
42789
+ legendPosition: "top"
42790
+ })
42791
+ },
42792
+ {
42793
+ description: _t("Same as pie, cleaner proportional look."),
42794
+ build: (ctx) => pieChart(ctx.title, ctx.source, {
42795
+ aggregated: true,
42796
+ isDoughnut: true,
42797
+ legendPosition: "top"
42798
+ })
42799
+ },
42800
+ {
42801
+ description: _t("Absolute count per category."),
42802
+ build: (ctx) => barChart(ctx.title, {
42803
+ ...ctx.source,
42804
+ labelRange: ctx.range
42805
+ }, {
42806
+ legendPosition: "none",
42807
+ aggregated: true
42808
+ })
42809
+ }
42810
+ ];
42811
+ /** Pattern E — Single label column */
42812
+ const SINGLE_LABEL_COLUMN_SUGGESTIONS = [{
42813
+ description: _t("Displays a key performance indicator."),
42814
+ isApplicable: ({ rowCount }) => rowCount === 1,
42815
+ build: (ctx) => scorecardChart(ctx.title, ctx.lastCellXC, {
42816
+ baselineMode: "text",
42817
+ humanize: false
42818
+ })
42819
+ }];
42820
+ /** Pattern F — Categorical + Number */
42821
+ const CATEGORICAL_VS_NUMBER_SUGGESTIONS = [
42822
+ {
42823
+ description: _t("Classic category-vs-value comparison."),
42824
+ build: (ctx) => barChart(ctx.title, ctx.source, { aggregated: true })
42825
+ },
42826
+ {
42827
+ description: _t("Better when category labels are long."),
42828
+ build: (ctx) => barChart(ctx.title, ctx.source, {
42829
+ horizontal: true,
42830
+ aggregated: true
42831
+ })
42832
+ },
42833
+ {
42834
+ description: _t("Share of total per category."),
42835
+ build: (ctx) => pieChart(ctx.title, ctx.source, {
42836
+ legendPosition: "top",
42837
+ aggregated: true
42838
+ })
42839
+ },
42840
+ {
42841
+ description: _t("Proportional area — good for many categories."),
42842
+ build: (ctx) => treemapChart(ctx.title, ctx.treemapSource)
42843
+ }
42844
+ ];
42845
+ /** Patterns G & J — Date + Number or Date + Percentage */
42846
+ const DATE_VS_SERIES_SUGGESTIONS = [
42847
+ {
42848
+ description: _t("Best for visualizing time-series trends."),
42849
+ build: (ctx) => lineChart(ctx.title, ctx.source)
42850
+ },
42851
+ {
42852
+ description: _t("Emphasizes total volume over time."),
42853
+ build: (ctx) => lineChart(ctx.title, ctx.source, {
42854
+ fillArea: true,
42855
+ cumulative: true
42856
+ })
42857
+ },
42858
+ {
42859
+ description: _t("Period-by-period comparison."),
42860
+ build: (ctx) => barChart(ctx.title, ctx.source)
42861
+ },
42862
+ {
42863
+ description: _t("Shows intensity variation across days of the year."),
42864
+ isApplicable: ({ isPercentage }) => !isPercentage,
42865
+ build: (ctx) => calendarChart(ctx.title, ctx.source)
42866
+ }
42867
+ ];
42868
+ /** Pattern H — Number + Number */
42869
+ const NUMBER_VS_NUMBER_SUGGESTIONS = [
42870
+ {
42871
+ description: _t("Highlights the second metric compared to the first one."),
42872
+ isApplicable: ({ rowCount1, rowCount2 }) => rowCount1 === 1 && rowCount2 === 1,
42873
+ build: (ctx) => scorecardChart(ctx.title, ctx.lastCellXC, {
42874
+ baseline: ctx.prevCellXC,
42875
+ baselineMode: "difference"
42876
+ })
42877
+ },
42878
+ {
42879
+ description: _t("Side-by-side comparison of two numeric series."),
42880
+ build: (ctx) => barChart(ctx.title, ctx.sourceBoth, { legendPosition: "top" })
42881
+ },
42882
+ {
42883
+ description: _t("Reveals correlation between two numeric variables."),
42884
+ isApplicable: ({ rowCount1 }) => rowCount1 > 2,
42885
+ build: (ctx) => scatterChart(ctx.title, ctx.source2)
42886
+ },
42887
+ {
42888
+ description: _t("Bar for the first series, line for the second — good for mixed scales."),
42889
+ isApplicable: ({ rowCount1 }) => rowCount1 > 2,
42890
+ build: (ctx) => comboChart(ctx.title, ctx.sourceBoth)
42891
+ },
42892
+ {
42893
+ description: _t("When both metrics contribute to a total."),
42894
+ isApplicable: ({ rowCount1 }) => rowCount1 > 2,
42895
+ build: (ctx) => lineChart(ctx.title, ctx.sourceBoth, {
42896
+ stacked: true,
42897
+ fillArea: true,
42898
+ legendPosition: "top"
42899
+ })
42900
+ },
42901
+ {
42902
+ description: _t("Shape-based comparison when rows represent named entities."),
42903
+ isApplicable: ({ rowCount1 }) => rowCount1 > 2 && rowCount1 <= MAX_ROWS_FOR_SHAPE_CHART,
42904
+ build: (ctx) => radarChart(ctx.title, ctx.sourceBoth, { legendPosition: "top" })
42905
+ }
42906
+ ];
42907
+ /** Pattern I — Categorical + Percentage */
42908
+ const CATEGORICAL_VS_PERCENTAGE_SUGGESTIONS = [
42909
+ {
42910
+ description: _t("Vertical comparison of rates per category."),
42911
+ build: (ctx) => barChart(ctx.title, ctx.source)
42912
+ },
42913
+ {
42914
+ description: _t("Progress-bar style per category."),
42915
+ build: (ctx) => barChart(ctx.title, ctx.source, { horizontal: true })
42916
+ },
42917
+ {
42918
+ description: _t("Share of total percentage across categories."),
42919
+ build: (ctx) => pieChart(ctx.title, ctx.source, { legendPosition: "top" })
42920
+ },
42921
+ {
42922
+ description: _t("Comparison of completion rates across categories."),
42923
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
42924
+ build: (ctx) => radarChart(ctx.title, ctx.source)
42925
+ }
42926
+ ];
42927
+ /** Pattern K — Label + Number */
42928
+ const LABEL_VS_NUMBER_SUGGESTIONS = [
42929
+ {
42930
+ description: _t("Highlights the most recent value for the named entity."),
42931
+ isApplicable: ({ rowCount }) => rowCount === 1,
42932
+ build: (ctx) => scorecardChart("", ctx.lastCellXC, {
42933
+ humanize: false,
42934
+ baselineMode: "text",
42935
+ baseline: ctx.prevCellXC
42936
+ })
42937
+ },
42938
+ {
42939
+ description: _t("Vertical comparison across named items."),
42940
+ isApplicable: ({ rowCount }) => rowCount > 1,
42941
+ build: (ctx) => barChart(ctx.title, ctx.source)
42942
+ },
42943
+ {
42944
+ description: _t("Works well for named entities with long labels."),
42945
+ isApplicable: ({ rowCount }) => rowCount > 1,
42946
+ build: (ctx) => barChart(ctx.title, ctx.source, { horizontal: true })
42947
+ },
42948
+ {
42949
+ description: _t("Share of total per category."),
42950
+ isApplicable: ({ rowCount }) => rowCount > 1 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
42951
+ build: (ctx) => pieChart(ctx.title, ctx.source, {
42952
+ legendPosition: "top",
42953
+ aggregated: true
42954
+ })
42955
+ },
42956
+ {
42957
+ description: _t("Shape-based comparison across labeled items."),
42958
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
42959
+ build: (ctx) => radarChart(ctx.title, ctx.source)
42960
+ }
42961
+ ];
42962
+ /** Pattern M — Categorical + Multiple Numbers */
42963
+ const CATEGORICAL_VS_MULTIPLE_NUMBERS_SUGGESTIONS = [
42964
+ {
42965
+ description: _t("Side-by-side comparison across categories for each series."),
42966
+ build: (ctx) => barChart(ctx.title, ctx.source, { legendPosition: "top" })
42967
+ },
42968
+ {
42969
+ description: _t("Shows composition and total per category."),
42970
+ build: (ctx) => barChart(ctx.title, ctx.source, {
42971
+ stacked: true,
42972
+ legendPosition: "top"
42973
+ })
42974
+ },
42975
+ {
42976
+ description: _t("Trend per series across categories."),
42977
+ build: (ctx) => lineChart(ctx.title, ctx.source, { legendPosition: "top" })
42978
+ },
42979
+ {
42980
+ description: _t("Volume and composition across categories."),
42981
+ build: (ctx) => lineChart(ctx.title, ctx.source, {
42982
+ stacked: true,
42983
+ fillArea: true,
42984
+ legendPosition: "top"
42985
+ })
42986
+ },
42987
+ {
42988
+ description: _t("Shape comparison across metrics (best for ≤ 10 rows)."),
42989
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
42990
+ build: (ctx) => radarChart(ctx.title, ctx.source, { legendPosition: "top" })
42991
+ }
42992
+ ];
42993
+ /** Pattern N — Date + Multiple Numbers */
42994
+ const DATE_VS_MULTIPLE_NUMBERS_SUGGESTIONS = [
42995
+ {
42996
+ description: _t("Trend comparison across multiple metrics over time."),
42997
+ build: (ctx) => lineChart(ctx.title, ctx.source, { legendPosition: "top" })
42998
+ },
42999
+ {
43000
+ description: _t("Volume composition over time."),
43001
+ build: (ctx) => lineChart(ctx.title, ctx.source, {
43002
+ stacked: true,
43003
+ fillArea: true,
43004
+ legendPosition: "top"
43005
+ })
43006
+ },
43007
+ {
43008
+ description: _t("Bar for the primary metric, line for the others."),
43009
+ build: (ctx) => comboChart(ctx.title, ctx.source)
43010
+ },
43011
+ {
43012
+ description: _t("Period-by-period grouped comparison."),
43013
+ build: (ctx) => barChart(ctx.title, ctx.source, { legendPosition: "top" })
43014
+ }
43015
+ ];
43016
+ /** Pattern O — Categorical + Categorical + Number */
43017
+ const MULTIPLE_CATEGORICALS_VS_NUMBER_SUGGESTIONS = [
43018
+ {
43019
+ description: _t("Two-level hierarchy weighted by value."),
43020
+ build: (ctx) => sunburstChart(ctx.title, ctx.hierarchySource)
43021
+ },
43022
+ {
43023
+ description: _t("Proportional nested area weighted by value."),
43024
+ build: (ctx) => treemapChart(ctx.title, ctx.hierarchySource)
43025
+ },
43026
+ {
43027
+ description: _t("One series per inner category, grouped by outer category."),
43028
+ build: (ctx) => barChart(ctx.title, ctx.barSource, { legendPosition: "top" })
43029
+ },
43030
+ {
43031
+ description: _t("Contribution of inner categories per outer category."),
43032
+ build: (ctx) => barChart(ctx.title, ctx.barSource, {
43033
+ stacked: true,
43034
+ legendPosition: "top"
43035
+ })
43036
+ }
43037
+ ];
43038
+ /** Pattern P — Categorical + Date + Number */
43039
+ const CATEGORICAL_DATE_NUMBER_SUGGESTIONS = [
43040
+ {
43041
+ description: _t("Trend of values over time."),
43042
+ build: (ctx) => lineChart(ctx.title, ctx.sourceByDate, { legendPosition: "top" })
43043
+ },
43044
+ {
43045
+ description: _t("Volume contribution over time."),
43046
+ build: (ctx) => lineChart(ctx.title, ctx.sourceByDate, {
43047
+ stacked: true,
43048
+ fillArea: true,
43049
+ legendPosition: "top"
43050
+ })
43051
+ },
43052
+ {
43053
+ description: _t("Period × category side-by-side comparison."),
43054
+ build: (ctx) => barChart(ctx.title, ctx.sourceByCat, { legendPosition: "top" })
43055
+ },
43056
+ {
43057
+ description: _t("Composition per period over time."),
43058
+ build: (ctx) => barChart(ctx.title, ctx.sourceByDate, {
43059
+ stacked: true,
43060
+ legendPosition: "top"
43061
+ })
43062
+ }
43063
+ ];
43064
+ /** Pattern Q — Label + Multiple Numbers */
43065
+ const LABEL_VS_MULTIPLE_NUMBERS_SUGGESTIONS = [
43066
+ {
43067
+ description: _t("Side-by-side per entity — works well for long labels."),
43068
+ build: (ctx) => barChart(ctx.title, ctx.source, {
43069
+ horizontal: true,
43070
+ legendPosition: "top"
43071
+ })
43072
+ },
43073
+ {
43074
+ description: _t("Grouped bars per entity for direct metric comparison."),
43075
+ build: (ctx) => barChart(ctx.title, ctx.source, { legendPosition: "top" })
43076
+ },
43077
+ {
43078
+ description: _t("Shape/profile comparison across metrics for each entity."),
43079
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
43080
+ build: (ctx) => radarChart(ctx.title, ctx.source, { legendPosition: "top" })
43081
+ },
43082
+ {
43083
+ description: _t("Correlation between the two numeric metrics across entities."),
43084
+ isApplicable: ({ rowCount, numColsCount }) => rowCount > 2 && numColsCount === 2,
43085
+ build: (ctx) => scatterChart(ctx.title, ctx.scatterSource)
43086
+ },
43087
+ {
43088
+ description: _t("Three metrics in one view: X position, Y position and bubble size."),
43089
+ isApplicable: ({ numColsCount }) => numColsCount === 3,
43090
+ build: (ctx) => {
43091
+ const bubble = ctx.bubble;
43092
+ return bubbleChart(ctx.title, bubble.xRange, bubble.yRanges, bubble.sizeRange, bubble.labelRange, bubble.hasTitle);
43093
+ }
43094
+ }
43095
+ ];
43096
+ /** Pattern R — Categorical + Two Numbers (Population Pyramid or Grouped) */
43097
+ const CATEGORICAL_TWO_NUMBERS_SUGGESTIONS = [
43098
+ {
43099
+ description: _t("Natural fit for symmetric or opposing values per category."),
43100
+ isApplicable: ({ isPyramid }) => isPyramid,
43101
+ build: (ctx) => pyramidChart(ctx.title, ctx.sourceBoth)
43102
+ },
43103
+ {
43104
+ description: _t("Side-by-side comparison of both metrics per category."),
43105
+ build: (ctx) => barChart(ctx.title, ctx.sourceBoth, {
43106
+ legendPosition: "top",
43107
+ aggregated: true
43108
+ })
43109
+ },
43110
+ {
43111
+ description: _t("Bar for the first series, line for the second — good for mixed scales."),
43112
+ build: (ctx) => comboChart(ctx.title, ctx.sourceBoth, { aggregated: true })
43113
+ },
43114
+ {
43115
+ description: _t("Shows total and composition per category."),
43116
+ isApplicable: ({ isPyramid }) => !isPyramid,
43117
+ build: (ctx) => barChart(ctx.title, ctx.sourceBoth, {
43118
+ stacked: true,
43119
+ legendPosition: "top",
43120
+ aggregated: true
43121
+ })
43122
+ }
43123
+ ];
43124
+ /** Pattern S — Many Numbers (3+ numeric columns, no categorical/date) */
43125
+ const MANY_NUMBERS_SUGGESTIONS = [
43126
+ {
43127
+ description: _t("Shows the position of the last value within the data's min-max range."),
43128
+ isApplicable: ({ colsLength, rowCount }) => colsLength === 3 && rowCount === 1,
43129
+ build: (ctx) => gaugeChart(ctx.title, ctx.lastCellXC, `=${ctx.firstCellXC}`, `=${ctx.secondCellXC}`)
43130
+ },
43131
+ {
43132
+ description: _t("Trend comparison across all metrics."),
43133
+ isApplicable: ({ rowCount }) => rowCount > 2,
43134
+ build: (ctx) => lineChart(ctx.title, ctx.source, { legendPosition: "top" })
43135
+ },
43136
+ {
43137
+ description: _t("Overall shape/profile across all metrics."),
43138
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
43139
+ build: (ctx) => radarChart(ctx.title, ctx.source, { legendPosition: "top" })
43140
+ },
43141
+ {
43142
+ description: _t("Side-by-side comparison of all numeric metrics."),
43143
+ build: (ctx) => barChart(ctx.title, ctx.source, { legendPosition: "top" })
43144
+ },
43145
+ {
43146
+ description: _t("Side-by-side comparison of all numeric metrics."),
43147
+ build: (ctx) => barChart(ctx.title, ctx.source, {
43148
+ legendPosition: "top",
43149
+ stacked: true
43150
+ })
43151
+ }
43152
+ ];
43153
+
43154
+ //#endregion
43155
+ //#region src/helpers/figures/charts/chart_suggestion_engine.ts
43156
+ const PYRAMID_HEADER_KEYWORD_PATTERNS = [
43157
+ "male",
43158
+ "female",
43159
+ "man",
43160
+ "woman",
43161
+ "boy",
43162
+ "girl",
43163
+ "before",
43164
+ "after",
43165
+ "previous",
43166
+ "current",
43167
+ "positive",
43168
+ "negative",
43169
+ "gain",
43170
+ "loss",
43171
+ "income",
43172
+ "expense",
43173
+ "revenue",
43174
+ "cost"
43175
+ ].map((keyword) => new RegExp(`\\b${keyword}\\b`, "i"));
43176
+ function matchesColumnType(spec, type) {
43177
+ return Array.isArray(spec) ? spec.includes(type) : spec === type;
43178
+ }
43179
+ function matchesShape(pattern, shape) {
43180
+ const { leadingTypes, extension } = pattern;
43181
+ const expectedLength = leadingTypes.length + (extension?.minSize ?? 0);
43182
+ if (extension ? shape.length < expectedLength : shape.length !== expectedLength) return false;
43183
+ return shape.every((type, i) => {
43184
+ const spec = i < leadingTypes.length ? leadingTypes[i] : extension?.type;
43185
+ return spec !== void 0 && matchesColumnType(spec, type);
43186
+ });
43187
+ }
43188
+ function isPyramidLike(numCol1, numCol2) {
43189
+ const h1 = numCol1.header ?? "";
43190
+ const h2 = numCol2.header ?? "";
43191
+ return PYRAMID_HEADER_KEYWORD_PATTERNS.some((re) => re.test(h1) || re.test(h2));
43192
+ }
43193
+ /** Returns the first and last cell XC coordinates, and the second-to-last cell XC coordinate if it exists. */
43194
+ function interestingCellsXc(col) {
43195
+ if (!col) return {
43196
+ firstCellXC: "",
43197
+ lastCellXC: "",
43198
+ prevCellXC: void 0
43199
+ };
43200
+ const firstCellPosition = col.nonEmpty.at(0)?.position;
43201
+ const firstCellXC = firstCellPosition ? toXC(firstCellPosition?.col, firstCellPosition?.row) : "";
43202
+ const lastCellPosition = col.nonEmpty.at(-1)?.position;
43203
+ const lastCellXC = lastCellPosition ? toXC(lastCellPosition?.col, lastCellPosition?.row) : "";
43204
+ const prevCellPosition = col.nonEmpty.at(-2)?.position;
43205
+ return {
43206
+ firstCellXC,
43207
+ lastCellXC,
43208
+ prevCellXC: prevCellPosition ? toXC(prevCellPosition?.col, prevCellPosition?.row) : void 0
43209
+ };
43210
+ }
43211
+ /** Pattern A — Single numeric column */
43212
+ function buildSingleNumberContext([col], getters) {
43213
+ const title = col.header ?? _t("Value");
43214
+ const { firstCellXC, lastCellXC, prevCellXC } = interestingCellsXc(col);
43215
+ return {
43216
+ title,
43217
+ source: rangeSource([dataset(col.zone, getters)], col.hasHeader),
43218
+ rowCount: col.rowCount,
43219
+ firstCellXC,
43220
+ lastCellXC,
43221
+ prevCellXC
43222
+ };
43223
+ }
43224
+ /** Pattern B — Single percentage column */
43225
+ function buildSinglePercentageContext([col], getters) {
43226
+ const hasTitle = col.hasHeader;
43227
+ const title = col.header ?? _t("Rate");
43228
+ const { firstCellXC, lastCellXC, prevCellXC } = interestingCellsXc(col);
43229
+ const source = rangeSource([dataset(col.zone, getters)], hasTitle);
43230
+ const isAboveOne = (col.maxValue ?? 0) > 1;
43231
+ return {
43232
+ title,
43233
+ source,
43234
+ rowCount: col.rowCount,
43235
+ firstCellXC,
43236
+ lastCellXC,
43237
+ prevCellXC,
43238
+ isAboveOne
43239
+ };
43240
+ }
43241
+ /** Pattern C — Single date column */
43242
+ function buildSingleDateContext([col]) {
43243
+ const { lastCellXC } = interestingCellsXc(col);
43244
+ return {
43245
+ title: col.header ?? _t("Date"),
43246
+ lastCellXC,
43247
+ rowCount: col.rowCount
43248
+ };
43249
+ }
43250
+ /** Pattern D — Single categorical column */
43251
+ function buildSingleCategoricalContext([col], getters) {
43252
+ const hasTitle = col.hasHeader;
43253
+ const title = col.header ?? _t("Category");
43254
+ const range = getUnboundRange(getters, col.zone);
43255
+ return {
43256
+ title,
43257
+ source: rangeSource([dataset(col.zone, getters)], hasTitle, range),
43258
+ range
43259
+ };
43260
+ }
43261
+ /** Pattern E — Single label column */
43262
+ function buildSingleLabelContext([col]) {
43263
+ const title = col.header ?? _t("Label");
43264
+ const { lastCellXC } = interestingCellsXc(col);
43265
+ return {
43266
+ title,
43267
+ lastCellXC,
43268
+ rowCount: col.rowCount
43269
+ };
43270
+ }
43271
+ /** Pattern F — Categorical + Number */
43272
+ function buildCategoricalVsNumberContext([catCol, numCol], getters) {
43273
+ const labelRange = getUnboundRange(getters, catCol.zone);
43274
+ const hasTitle = numCol.hasHeader;
43275
+ return {
43276
+ title: numCol.header ? _t("%(numberHeader)s by %(categoryHeader)s", {
43277
+ numberHeader: numCol.header,
43278
+ categoryHeader: catCol.header ?? _t("Category")
43279
+ }) : _t("By Category"),
43280
+ source: rangeSource([dataset(numCol.zone, getters)], hasTitle, labelRange),
43281
+ treemapSource: rangeSource([dataset(catCol.zone, getters)], hasTitle, getUnboundRange(getters, numCol.zone))
43282
+ };
43283
+ }
43284
+ /** Patterns G & J — Date + Number or Date + Percentage */
43285
+ function buildDateVsSeriesContext([dateCol, seriesCol], getters) {
43286
+ const isPercentage = seriesCol.type === "percentage";
43287
+ const labelRange = getUnboundRange(getters, dateCol.zone);
43288
+ const hasTitle = isDatasetTitled(getters, dateCol.zone);
43289
+ return {
43290
+ title: seriesCol.header ? _t("%(seriesHeader)s over time", { seriesHeader: seriesCol.header }) : isPercentage ? _t("Rate over Time") : _t("Over Time"),
43291
+ source: rangeSource([dataset(seriesCol.zone, getters)], hasTitle, labelRange),
43292
+ isPercentage
43293
+ };
43294
+ }
43295
+ /** Pattern H — Number + Number */
43296
+ function buildNumberVsNumberContext([col1, col2], getters) {
43297
+ const title = col1.header && col2.header ? _t("%(col2Header)s vs %(col1Header)s", {
43298
+ col2Header: col2.header,
43299
+ col1Header: col1.header
43300
+ }) : _t("Correlation");
43301
+ const hasTitle = col1.hasHeader || col2.hasHeader;
43302
+ const source2 = rangeSource([dataset(col2.zone, getters)], col2.hasHeader, getUnboundRange(getters, col1.zone));
43303
+ const sourceBoth = rangeSource([dataset(col1.zone, getters, "0"), dataset(col2.zone, getters, "1")], hasTitle);
43304
+ const { lastCellXC } = interestingCellsXc(col2);
43305
+ const { lastCellXC: prevCellXC } = interestingCellsXc(col1);
43306
+ return {
43307
+ title,
43308
+ source2,
43309
+ sourceBoth,
43310
+ rowCount1: col1.rowCount,
43311
+ rowCount2: col2.rowCount,
43312
+ lastCellXC,
43313
+ prevCellXC
43314
+ };
43315
+ }
43316
+ /** Pattern I — Categorical + Percentage */
43317
+ function buildCategoricalVsPercentageContext([catCol, pctCol], getters) {
43318
+ const labelRange = getUnboundRange(getters, catCol.zone);
43319
+ const hasTitle = pctCol.hasHeader;
43320
+ return {
43321
+ title: pctCol.header ? _t("%(percentageHeader)s by %(categoryHeader)s", {
43322
+ percentageHeader: pctCol.header,
43323
+ categoryHeader: catCol.header ?? _t("Category")
43324
+ }) : _t("Rates by Category"),
43325
+ source: rangeSource([dataset(pctCol.zone, getters)], hasTitle, labelRange),
43326
+ rowCount: catCol.rowCount
43327
+ };
43328
+ }
43329
+ /** Pattern K — Label + Number */
43330
+ function buildLabelVsNumberContext([labelCol, numCol], getters) {
43331
+ const labelRange = getUnboundRange(getters, labelCol.zone);
43332
+ const hasTitle = numCol.hasHeader;
43333
+ const title = numCol.header ? _t("%(numberHeader)s by %(labelHeader)s", {
43334
+ numberHeader: numCol.header,
43335
+ labelHeader: labelCol.header ?? _t("Name")
43336
+ }) : _t("By Name");
43337
+ const source = rangeSource([dataset(numCol.zone, getters)], hasTitle, labelRange);
43338
+ const { lastCellXC } = interestingCellsXc(numCol);
43339
+ const { lastCellXC: prevCellXC } = interestingCellsXc(labelCol);
43340
+ return {
43341
+ title,
43342
+ source,
43343
+ rowCount: labelCol.rowCount,
43344
+ lastCellXC,
43345
+ prevCellXC
43346
+ };
43347
+ }
43348
+ /** Pattern M — Categorical + Multiple Numbers */
43349
+ function buildCategoricalVsMultipleNumbersContext([catCol, ...numCols], getters) {
43350
+ const labelRange = getUnboundRange(getters, catCol.zone);
43351
+ const hasTitle = numCols.some((c) => c.hasHeader);
43352
+ return {
43353
+ title: catCol.header ? _t("By %(header)s", { header: catCol.header }) : _t("Multi-series"),
43354
+ source: rangeSource(numCols.map((c, i) => dataset(c.zone, getters, String(i))), hasTitle, labelRange),
43355
+ rowCount: catCol.rowCount
43356
+ };
43357
+ }
43358
+ /** Pattern N — Date + Multiple Numbers */
43359
+ function buildDateVsMultipleNumbersContext([dateCol, ...numCols], getters) {
43360
+ const labelRange = getUnboundRange(getters, dateCol.zone);
43361
+ const hasTitle = numCols.some((c) => c.hasHeader) || isDatasetTitled(getters, dateCol.zone);
43362
+ return {
43363
+ title: numCols.length === 1 && numCols[0].header ? _t("%(header)s over time", { header: numCols[0].header }) : _t("Multi-series over Time"),
43364
+ source: rangeSource(numCols.map((c, i) => dataset(c.zone, getters, String(i))), hasTitle, labelRange)
43365
+ };
43366
+ }
43367
+ /** Pattern O — Categorical + Categorical + Number */
43368
+ function buildMultipleCategoricalsVsNumberContext([cat1, cat2, numCol], getters) {
43369
+ const title = numCol.header ? _t("%(numHeader)s by %(cat1Header)s and %(cat2Header)s", {
43370
+ numHeader: numCol.header,
43371
+ cat1Header: cat1.header ?? _t("Level 1"),
43372
+ cat2Header: cat2.header ?? _t("Level 2")
43373
+ }) : _t("Two-level hierarchy");
43374
+ const hasTitle = numCol.hasHeader;
43375
+ return {
43376
+ title,
43377
+ hierarchySource: rangeSource([dataset(cat1.zone, getters, "0"), dataset(cat2.zone, getters, "1")], hasTitle, getUnboundRange(getters, numCol.zone)),
43378
+ barSource: rangeSource([dataset(numCol.zone, getters)], hasTitle, getUnboundRange(getters, cat1.zone))
43379
+ };
43380
+ }
43381
+ /** Pattern P — Categorical + Date + Number */
43382
+ function buildCategoricalDateNumberContext([catCol, dateCol, numCol], getters) {
43383
+ const dateRange = getUnboundRange(getters, dateCol.zone);
43384
+ const catRange = getUnboundRange(getters, catCol.zone);
43385
+ const hasTitle = numCol.hasHeader;
43386
+ return {
43387
+ title: numCol.header ? _t("%(numHeader)s by %(catHeader)s over %(dateHeader)s", {
43388
+ numHeader: numCol.header,
43389
+ catHeader: catCol.header ?? _t("Category"),
43390
+ dateHeader: dateCol.header ?? _t("Time")
43391
+ }) : _t("Multi-series over Time"),
43392
+ sourceByDate: rangeSource([dataset(numCol.zone, getters)], hasTitle, dateRange),
43393
+ sourceByCat: rangeSource([dataset(numCol.zone, getters)], hasTitle, catRange)
43394
+ };
43395
+ }
43396
+ /** Pattern Q — Label + Multiple Numbers */
43397
+ function buildLabelVsMultipleNumbersContext([labelCol, ...numCols], getters) {
43398
+ const labelRange = getUnboundRange(getters, labelCol.zone);
43399
+ const hasTitle = numCols.some((c) => c.hasHeader);
43400
+ const title = labelCol.header ? _t("By %(header)s", { header: labelCol.header }) : _t("Profile Comparison");
43401
+ const source = rangeSource(numCols.map((c, i) => dataset(c.zone, getters, String(i))), hasTitle, labelRange);
43402
+ const scatterSource = rangeSource([dataset(numCols[1].zone, getters)], numCols[1].hasHeader, getUnboundRange(getters, numCols[0].zone));
43403
+ const bubble = numCols.length === 3 ? {
43404
+ xRange: getUnboundRange(getters, numCols[0].zone),
43405
+ yRanges: [getUnboundRange(getters, numCols[1].zone)],
43406
+ sizeRange: getUnboundRange(getters, numCols[2].zone),
43407
+ labelRange,
43408
+ hasTitle
43409
+ } : void 0;
43410
+ return {
43411
+ title,
43412
+ source,
43413
+ rowCount: labelCol.rowCount,
43414
+ numColsCount: numCols.length,
43415
+ scatterSource,
43416
+ bubble
43417
+ };
43418
+ }
43419
+ /** Pattern R — Categorical + Two Numbers (Population Pyramid or Grouped) */
43420
+ function buildCategoricalTwoNumbersContext([catCol, numCol1, numCol2], getters) {
43421
+ const catRange = getUnboundRange(getters, catCol.zone);
43422
+ const hasTitle = numCol1.hasHeader || numCol2.hasHeader;
43423
+ const title = numCol1.header && numCol2.header ? _t("%(num1Header)s vs %(num2Header)s by %(catHeader)s", {
43424
+ num1Header: numCol1.header,
43425
+ num2Header: numCol2.header,
43426
+ catHeader: catCol.header ?? _t("Category")
43427
+ }) : catCol.header ? _t("By %(header)s", { header: catCol.header }) : _t("Category Comparison");
43428
+ const isPyramid = isPyramidLike(numCol1, numCol2);
43429
+ return {
43430
+ title,
43431
+ sourceBoth: rangeSource([dataset(numCol1.zone, getters, "0"), dataset(numCol2.zone, getters, "1")], hasTitle, catRange),
43432
+ isPyramid
43433
+ };
43434
+ }
43435
+ /** Pattern S — Many Numbers (3+ numeric columns, no categorical/date) */
43436
+ function buildManyNumbersContext(cols, getters) {
43437
+ const title = cols.every((c) => c.hasHeader) ? cols.map((c) => c.header).join(" / ") : _t("KPI Overview");
43438
+ const hasTitle = cols.some((c) => c.hasHeader);
43439
+ const source = rangeSource(cols.map((c, i) => dataset(c.zone, getters, String(i))), hasTitle);
43440
+ const { lastCellXC } = interestingCellsXc(cols[2]);
43441
+ const { lastCellXC: firstCellXC } = interestingCellsXc(cols[0]);
43442
+ const { lastCellXC: secondCellXC } = interestingCellsXc(cols[1]);
43443
+ return {
43444
+ title,
43445
+ source,
43446
+ colsLength: cols.length,
43447
+ rowCount: cols[0].rowCount,
43448
+ lastCellXC,
43449
+ firstCellXC,
43450
+ secondCellXC
43451
+ };
43452
+ }
43453
+ const NUMBER_OR_PERCENTAGE = ["number", "percentage"];
43454
+ const EXACT_PATTERNS = [
43455
+ {
43456
+ pattern: { leadingTypes: ["number"] },
43457
+ buildContext: buildSingleNumberContext,
43458
+ suggestions: SINGLE_NUMBER_COLUMN_SUGGESTIONS
43459
+ },
43460
+ {
43461
+ pattern: { leadingTypes: ["percentage"] },
43462
+ buildContext: buildSinglePercentageContext,
43463
+ suggestions: SINGLE_PERCENTAGE_COLUMN_SUGGESTIONS
43464
+ },
43465
+ {
43466
+ pattern: { leadingTypes: ["date"] },
43467
+ buildContext: buildSingleDateContext,
43468
+ suggestions: SINGLE_DATE_COLUMN_SUGGESTIONS
43469
+ },
43470
+ {
43471
+ pattern: { leadingTypes: ["categorical"] },
43472
+ buildContext: buildSingleCategoricalContext,
43473
+ suggestions: SINGLE_CATEGORICAL_COLUMN_SUGGESTIONS
43474
+ },
43475
+ {
43476
+ pattern: { leadingTypes: ["label"] },
43477
+ buildContext: buildSingleLabelContext,
43478
+ suggestions: SINGLE_LABEL_COLUMN_SUGGESTIONS
43479
+ },
43480
+ {
43481
+ pattern: { leadingTypes: ["categorical", "number"] },
43482
+ buildContext: buildCategoricalVsNumberContext,
43483
+ suggestions: CATEGORICAL_VS_NUMBER_SUGGESTIONS
43484
+ },
43485
+ {
43486
+ pattern: { leadingTypes: ["date", NUMBER_OR_PERCENTAGE] },
43487
+ buildContext: buildDateVsSeriesContext,
43488
+ suggestions: DATE_VS_SERIES_SUGGESTIONS
43489
+ },
43490
+ {
43491
+ pattern: { leadingTypes: ["number", "number"] },
43492
+ buildContext: buildNumberVsNumberContext,
43493
+ suggestions: NUMBER_VS_NUMBER_SUGGESTIONS
43494
+ },
43495
+ {
43496
+ pattern: { leadingTypes: ["categorical", "percentage"] },
43497
+ buildContext: buildCategoricalVsPercentageContext,
43498
+ suggestions: CATEGORICAL_VS_PERCENTAGE_SUGGESTIONS
43499
+ },
43500
+ {
43501
+ pattern: { leadingTypes: ["label", "number"] },
43502
+ buildContext: buildLabelVsNumberContext,
43503
+ suggestions: LABEL_VS_NUMBER_SUGGESTIONS
43504
+ },
43505
+ {
43506
+ pattern: { leadingTypes: ["label", "percentage"] },
43507
+ buildContext: buildCategoricalVsPercentageContext,
43508
+ suggestions: CATEGORICAL_VS_PERCENTAGE_SUGGESTIONS
43509
+ },
43510
+ {
43511
+ pattern: { leadingTypes: [
43512
+ "categorical",
43513
+ "categorical",
43514
+ "number"
43515
+ ] },
43516
+ buildContext: buildMultipleCategoricalsVsNumberContext,
43517
+ suggestions: MULTIPLE_CATEGORICALS_VS_NUMBER_SUGGESTIONS
43518
+ },
43519
+ {
43520
+ pattern: { leadingTypes: [
43521
+ "categorical",
43522
+ "label",
43523
+ "number"
43524
+ ] },
43525
+ buildContext: buildMultipleCategoricalsVsNumberContext,
43526
+ suggestions: MULTIPLE_CATEGORICALS_VS_NUMBER_SUGGESTIONS
43527
+ },
43528
+ {
43529
+ pattern: { leadingTypes: [
43530
+ "categorical",
43531
+ "date",
43532
+ NUMBER_OR_PERCENTAGE
43533
+ ] },
43534
+ buildContext: buildCategoricalDateNumberContext,
43535
+ suggestions: CATEGORICAL_DATE_NUMBER_SUGGESTIONS
43536
+ },
43537
+ {
43538
+ pattern: { leadingTypes: [
43539
+ "categorical",
43540
+ NUMBER_OR_PERCENTAGE,
43541
+ NUMBER_OR_PERCENTAGE
43542
+ ] },
43543
+ buildContext: buildCategoricalTwoNumbersContext,
43544
+ suggestions: CATEGORICAL_TWO_NUMBERS_SUGGESTIONS
43545
+ }
43546
+ ];
43547
+ const EXTENDABLE_PATTERNS = [
43548
+ {
43549
+ pattern: {
43550
+ leadingTypes: [],
43551
+ extension: {
43552
+ type: NUMBER_OR_PERCENTAGE,
43553
+ minSize: 3
43554
+ }
43555
+ },
43556
+ buildContext: buildManyNumbersContext,
43557
+ suggestions: MANY_NUMBERS_SUGGESTIONS
43558
+ },
43559
+ {
43560
+ pattern: {
43561
+ leadingTypes: ["label"],
43562
+ extension: {
43563
+ type: NUMBER_OR_PERCENTAGE,
43564
+ minSize: 2
43565
+ }
43566
+ },
43567
+ buildContext: buildLabelVsMultipleNumbersContext,
43568
+ suggestions: LABEL_VS_MULTIPLE_NUMBERS_SUGGESTIONS
43569
+ },
43570
+ {
43571
+ pattern: {
43572
+ leadingTypes: ["categorical"],
43573
+ extension: {
43574
+ type: NUMBER_OR_PERCENTAGE,
43575
+ minSize: 2
43576
+ }
43577
+ },
43578
+ buildContext: buildCategoricalVsMultipleNumbersContext,
43579
+ suggestions: CATEGORICAL_VS_MULTIPLE_NUMBERS_SUGGESTIONS
43580
+ },
43581
+ {
43582
+ pattern: {
43583
+ leadingTypes: ["date"],
43584
+ extension: {
43585
+ type: NUMBER_OR_PERCENTAGE,
43586
+ minSize: 2
43587
+ }
43588
+ },
43589
+ buildContext: buildDateVsMultipleNumbersContext,
43590
+ suggestions: DATE_VS_MULTIPLE_NUMBERS_SUGGESTIONS
43591
+ }
43592
+ ];
43593
+ function getChartSuggestions(zones, getters) {
43594
+ const cols = analyzeColumns(zones, getters);
43595
+ if (cols.some((c) => c.type === "error")) return [];
43596
+ const nonEmpty = cols.filter((c) => c.type !== "empty");
43597
+ if (!nonEmpty.length) return [];
43598
+ const shape = nonEmpty.map((c) => c.type);
43599
+ const rule = EXACT_PATTERNS.find((rule) => matchesShape(rule.pattern, shape)) ?? EXTENDABLE_PATTERNS.find((rule) => matchesShape(rule.pattern, shape));
43600
+ if (!rule) return [];
43601
+ const ctx = rule.buildContext(nonEmpty, getters);
43602
+ return rule.suggestions.filter((suggestion) => suggestion.isApplicable?.(ctx) ?? true).map((suggestion) => ({
43603
+ description: suggestion.description.toString(),
43604
+ definition: suggestion.build(ctx)
43605
+ }));
43606
+ }
43607
+
43608
+ //#endregion
43609
+ //#region src/components/side_panel/data_analysis/data_analysis_store.ts
43610
+ var DataAnalysisStore = class extends SpreadsheetStore {
43611
+ mutators = [];
43612
+ hasData = false;
43613
+ chartSuggestions = [];
43614
+ isDirty = false;
43615
+ ranges;
43616
+ constructor(get) {
43617
+ super(get);
43618
+ this.model.selection.observe(this, { handleEvent: () => this.refresh() });
43619
+ this.onDispose(() => {
43620
+ this.model.selection.unobserve(this);
43621
+ });
43622
+ this.refresh();
43623
+ }
43624
+ handle(cmd) {
43625
+ if (invalidateEvaluationCommands.has(cmd.type) || cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd)) this.isDirty = true;
43626
+ switch (cmd.type) {
43627
+ case "HIDE_COLUMNS_ROWS":
43628
+ case "UNHIDE_COLUMNS_ROWS":
43629
+ case "GROUP_HEADERS":
43630
+ case "UNGROUP_HEADERS":
43631
+ case "ACTIVATE_SHEET":
43632
+ case "ACTIVATE_NEXT_SHEET":
43633
+ case "ACTIVATE_PREVIOUS_SHEET":
43634
+ case "EVALUATE_CELLS":
43635
+ case "SET_FORMATTING":
43636
+ case "CLEAR_FORMATTING":
43637
+ this.isDirty = true;
43638
+ break;
43639
+ }
43640
+ }
43641
+ finalize() {
43642
+ if (this.isDirty) {
43643
+ this.refresh();
43644
+ this.isDirty = false;
43645
+ }
43646
+ }
43647
+ refresh() {
43648
+ const sheetId = this.getters.getActiveSheetId();
43649
+ const zones = this.getters.getSelectedZones();
43650
+ this.ranges = zones.map(zoneToXc);
43651
+ this.hasData = zones.some((zone) => this.getters.getEvaluatedCellsInZone(sheetId, zone).some((cell) => cell.type !== "empty"));
43652
+ const suggestions = this.hasData ? getChartSuggestions(zones, this.getters) : [];
43653
+ this.chartSuggestions = suggestions;
43654
+ }
43655
+ };
43656
+
43657
+ //#endregion
43658
+ //#region src/components/side_panel/data_analysis/data_analysis_panel.ts
43659
+ var DataAnalysisPanel = class extends Component {
43660
+ static template = "o-spreadsheet-DataAnalysisPanel";
43661
+ static components = {
43662
+ Section,
43663
+ ChartSuggestionPreview
43664
+ };
43665
+ store;
43666
+ setup() {
43667
+ this.store = useLocalStore(DataAnalysisStore);
43668
+ }
43669
+ onStartChartSuggestionDrag(definition, ev) {
43670
+ startChartDragAndDrop(this.env, definition, ev);
43671
+ }
43672
+ };
43673
+
42146
43674
  //#endregion
42147
43675
  //#region src/registries/data_source_registry.ts
42148
43676
  const unusedDataSourceRegistry = new Registry();
@@ -42219,7 +43747,7 @@ set(value) {
42219
43747
  var DataValidationPreview = class extends Component {
42220
43748
  static template = "o-spreadsheet-DataValidationPreview";
42221
43749
  props = (0, _odoo_owl.useProps)({ rule: types.DataValidationRule() });
42222
- dvPreviewRef = (0, _odoo_owl.signal)(null);
43750
+ dvPreviewRef = _odoo_owl.signal.ref();
42223
43751
  setup() {
42224
43752
  useHighlightsOnHover(this.dvPreviewRef, this);
42225
43753
  }
@@ -43718,7 +45246,7 @@ set(value) {
43718
45246
  };
43719
45247
  props = (0, _odoo_owl.useProps)({ namedRange: types.NamedRange() });
43720
45248
  state = (0, _odoo_owl.proxy)({});
43721
- namedRangePreviewRef = (0, _odoo_owl.signal)(null);
45249
+ namedRangePreviewRef = _odoo_owl.signal.ref();
43722
45250
  setup() {
43723
45251
  useHighlightsOnHover(this.namedRangePreviewRef, this);
43724
45252
  }
@@ -44209,7 +45737,7 @@ set(value) {
44209
45737
  onClick: types.function(),
44210
45738
  scrolledTo: types.or([types.literal("top"), types.literal("bottom")]).optional()
44211
45739
  });
44212
- itemRef = (0, _odoo_owl.signal)(null);
45740
+ itemRef = _odoo_owl.signal.ref();
44213
45741
  setup() {
44214
45742
  (0, _odoo_owl.onWillPatch)(() => {
44215
45743
  if (this.props.scrolledTo) this.scrollListToSelectedValue();
@@ -44449,7 +45977,7 @@ set(value) {
44449
45977
  onFiltersUpdated: types.function()
44450
45978
  });
44451
45979
  state;
44452
- buttonFilter = (0, _odoo_owl.signal)(null);
45980
+ buttonFilter = _odoo_owl.signal.ref();
44453
45981
  popover;
44454
45982
  setup() {
44455
45983
  useExternalListener(window, "click", (ev) => {
@@ -44578,11 +46106,11 @@ set(value) {
44578
46106
  onFieldPicked: types.function(),
44579
46107
  fields: types.array()
44580
46108
  });
44581
- buttonRef = (0, _odoo_owl.signal)(null);
46109
+ buttonRef = _odoo_owl.signal.ref(HTMLButtonElement);
44582
46110
  popover = (0, _odoo_owl.proxy)({ isOpen: false });
44583
46111
  search = (0, _odoo_owl.proxy)({ input: "" });
44584
46112
  autoComplete;
44585
- autofocusRef = (0, _odoo_owl.signal)(null);
46113
+ autofocusRef = _odoo_owl.signal.ref(HTMLInputElement);
44586
46114
  setup() {
44587
46115
  this.autoComplete = useLocalStore(AutoCompleteStore);
44588
46116
  this.autoComplete.useProvider(this.getProvider());
@@ -44991,7 +46519,7 @@ set(value) {
44991
46519
  getScrollableContainerEl: types.function().optional(),
44992
46520
  pivotId: types.UID()
44993
46521
  });
44994
- dimensionsRef = (0, _odoo_owl.signal)(null);
46522
+ dimensionsRef = _odoo_owl.signal.ref();
44995
46523
  dragAndDrop = useDragAndDropListItems();
44996
46524
  AGGREGATORS = AGGREGATORS;
44997
46525
  isDateOrDatetimeField = isDateOrDatetimeField;
@@ -45062,16 +46590,7 @@ set(value) {
45062
46590
  });
45063
46591
  }
45064
46592
  getDimensionElementsRects() {
45065
- return Array.from(this.dimensionsRef().children).map((el) => {
45066
- const style = getComputedStyle(el);
45067
- const rect = el.getBoundingClientRect();
45068
- return {
45069
- x: rect.x,
45070
- y: rect.y,
45071
- width: rect.width + parseInt(style.marginLeft || "0") + parseInt(style.marginRight || "0"),
45072
- height: rect.height + parseInt(style.marginTop || "0") + parseInt(style.marginBottom || "0")
45073
- };
45074
- });
46593
+ return Array.from(this.dimensionsRef().children).map((el) => getBoundingRectWithMargins(el));
45075
46594
  }
45076
46595
  removeDimension(dimension) {
45077
46596
  const { columns, rows } = this.props.definition;
@@ -45636,7 +47155,7 @@ set(value) {
45636
47155
  });
45637
47156
  store;
45638
47157
  state;
45639
- pivotSidePanelRef = (0, _odoo_owl.signal)(null);
47158
+ pivotSidePanelRef = _odoo_owl.signal.ref();
45640
47159
  setup() {
45641
47160
  this.store = useLocalStore(PivotSidePanelStore, this.props.pivotId);
45642
47161
  this.state = (0, _odoo_owl.proxy)({
@@ -46876,7 +48395,7 @@ set(value) {
46876
48395
  selected: types.boolean().optional(),
46877
48396
  onClick: types.function().optional()
46878
48397
  });
46879
- canvasRef = (0, _odoo_owl.signal)(null);
48398
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
46880
48399
  menu = (0, _odoo_owl.proxy)({
46881
48400
  isOpen: false,
46882
48401
  anchorRect: null,
@@ -46970,7 +48489,7 @@ set(value) {
46970
48489
  tableStyles: types.RecordOf(),
46971
48490
  type: types.or([types.literal("table"), types.literal("pivot")])
46972
48491
  });
46973
- tableStyleListRef = (0, _odoo_owl.signal)(null);
48492
+ tableStyleListRef = _odoo_owl.signal.ref();
46974
48493
  state = (0, _odoo_owl.proxy)({ selectedCategory: this.initialSelectedCategory });
46975
48494
  setup() {
46976
48495
  useExternalListener(window, "click", this.onExternalClick, { capture: true });
@@ -48341,6 +49860,10 @@ set(value) {
48341
49860
  title: _t("Column statistics"),
48342
49861
  Body: ColumnStatsPanel
48343
49862
  });
49863
+ sidePanelRegistry.add("DataAnalysisPanel", {
49864
+ title: _t("Data analysis"),
49865
+ Body: DataAnalysisPanel
49866
+ });
48344
49867
  sidePanelRegistry.add("TableSidePanel", {
48345
49868
  title: _t("Edit table"),
48346
49869
  Body: TablePanel,
@@ -48448,6 +49971,7 @@ set(value) {
48448
49971
  "changePanelSize",
48449
49972
  "resetPanelSize",
48450
49973
  "togglePinPanel",
49974
+ "togglePinnedSidePanel",
48451
49975
  "closeMainPanel",
48452
49976
  "changeSpreadsheetWidth",
48453
49977
  "toggleCollapsePanel"
@@ -48599,6 +50123,19 @@ set(value) {
48599
50123
  this.secondaryPanel = void 0;
48600
50124
  }
48601
50125
  }
50126
+ togglePinnedSidePanel(componentTag, panelProps = {}) {
50127
+ if (this.mainPanel?.componentTag === componentTag && this.isMainPanelOpen) {
50128
+ this.closeMainPanel();
50129
+ return;
50130
+ }
50131
+ if (this.secondaryPanel?.componentTag === componentTag && this.isSecondaryPanelOpen) {
50132
+ this.secondaryPanel?.currentPanelProps.onCloseSidePanel?.();
50133
+ this.secondaryPanel = void 0;
50134
+ return;
50135
+ }
50136
+ this.open(componentTag, panelProps);
50137
+ if (!this.mainPanel?.isPinned) this.togglePinPanel();
50138
+ }
48602
50139
  toggleCollapsePanel(panel) {
48603
50140
  const panelInfo = this[panel];
48604
50141
  if (!panelInfo) return;
@@ -48977,6 +50514,219 @@ set(value) {
48977
50514
  }
48978
50515
  };
48979
50516
 
50517
+ //#endregion
50518
+ //#region src/helpers/sort_interactive.ts
50519
+ function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) {
50520
+ let multiColumns = zone.right > zone.left;
50521
+ if (env.model.getters.doesIntersectMerge(sheetId, zone)) {
50522
+ multiColumns = false;
50523
+ let table;
50524
+ for (let row = zone.top; row <= zone.bottom; row++) {
50525
+ table = [];
50526
+ for (let col = zone.left; col <= zone.right; col++) {
50527
+ const merge = env.model.getters.getMerge({
50528
+ sheetId,
50529
+ col,
50530
+ row
50531
+ });
50532
+ if (merge && !table.includes(merge.id.toString())) table.push(merge.id.toString());
50533
+ }
50534
+ if (table.length >= 2) {
50535
+ multiColumns = true;
50536
+ break;
50537
+ }
50538
+ }
50539
+ }
50540
+ if (multiColumns) {
50541
+ interactiveSort(env, sheetId, anchor, zone, sortDirection);
50542
+ return;
50543
+ }
50544
+ const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);
50545
+ if (isEqual(contiguousZone, zone)) interactiveSort(env, sheetId, anchor, zone, sortDirection);
50546
+ 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));
50547
+ }
50548
+ function interactiveSort(env, sheetId, anchor, zone, sortDirection, sortOptions) {
50549
+ const result = env.model.dispatch("SORT_CELLS", {
50550
+ sheetId,
50551
+ col: anchor.col,
50552
+ row: anchor.row,
50553
+ zone,
50554
+ sortDirection,
50555
+ sortOptions
50556
+ });
50557
+ if (result.isCancelledBecause("InvalidSortZone")) {
50558
+ const { col, row } = anchor;
50559
+ env.model.selection.selectZone({
50560
+ cell: {
50561
+ col,
50562
+ row
50563
+ },
50564
+ zone
50565
+ });
50566
+ env.raiseError(_t("Cannot sort. To sort, select only cells or only merges that have the same size."));
50567
+ }
50568
+ if (result.isCancelledBecause("SortZoneWithArrayFormulas")) {
50569
+ const { col, row } = anchor;
50570
+ env.model.selection.selectZone({
50571
+ cell: {
50572
+ col,
50573
+ row
50574
+ },
50575
+ zone
50576
+ });
50577
+ env.raiseError(_t("Cannot sort a zone with array formulas."));
50578
+ }
50579
+ }
50580
+
50581
+ //#endregion
50582
+ //#region src/stores/trim_whitespace_store.ts
50583
+ var TrimWhitespaceStore = class extends SpreadsheetStore {
50584
+ mutators = ["trimWhitespace"];
50585
+ notificationStore = this.get(NotificationStore);
50586
+ trimWhitespace() {
50587
+ this.model.dispatch("TRIM_WHITESPACE");
50588
+ }
50589
+ handle(cmd) {
50590
+ switch (cmd.type) {
50591
+ case "TRIM_WHITESPACE":
50592
+ this._trimWhitespace();
50593
+ break;
50594
+ }
50595
+ }
50596
+ _trimWhitespace() {
50597
+ const zones = recomputeZones(this.getters.getSelectedZones());
50598
+ const sheetId = this.getters.getActiveSheetId();
50599
+ let count = 0;
50600
+ for (const { col, row } of zones.map(positions).flat()) {
50601
+ const cell = this.getters.getCell({
50602
+ col,
50603
+ row,
50604
+ sheetId
50605
+ });
50606
+ if (!cell) continue;
50607
+ const currentContent = !cell.isFormula ? cell.content : cell.compiledFormula.toFormulaString(this.getters);
50608
+ const trimmedContent = trimContent(currentContent);
50609
+ if (trimmedContent !== currentContent) {
50610
+ count += 1;
50611
+ this.model.dispatch("UPDATE_CELL", {
50612
+ sheetId,
50613
+ col,
50614
+ row,
50615
+ content: trimmedContent
50616
+ });
50617
+ }
50618
+ }
50619
+ const text = count ? _t("Trimmed whitespace from %s cells.", count) : _t("No selected cells had whitespace trimmed.");
50620
+ this.notificationStore.notifyUser({
50621
+ type: "info",
50622
+ text,
50623
+ sticky: false
50624
+ });
50625
+ }
50626
+ };
50627
+
50628
+ //#endregion
50629
+ //#region src/actions/data_actions.ts
50630
+ const sortRange = {
50631
+ name: _t("Sort range"),
50632
+ isVisible: IS_ONLY_ONE_RANGE,
50633
+ icon: "o-spreadsheet-Icon.SORT_RANGE"
50634
+ };
50635
+ const sortAscending = {
50636
+ name: _t("Ascending (A ⟶ Z)"),
50637
+ execute: (env) => {
50638
+ const { anchor, zones } = env.model.getters.getSelection();
50639
+ interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "asc");
50640
+ },
50641
+ icon: "o-spreadsheet-Icon.SORT_ASCENDING"
50642
+ };
50643
+ const dataCleanup = {
50644
+ name: _t("Data cleanup"),
50645
+ icon: "o-spreadsheet-Icon.DATA_CLEANUP"
50646
+ };
50647
+ const removeDuplicates = {
50648
+ name: _t("Remove duplicates"),
50649
+ execute: (env) => {
50650
+ if (getZoneArea(env.model.getters.getSelectedZone()) === 1) env.model.selection.selectTableAroundSelection();
50651
+ env.openSidePanel("RemoveDuplicates", {});
50652
+ },
50653
+ isEnabled: (env) => !env.isSmall
50654
+ };
50655
+ const trimWhitespace = {
50656
+ name: _t("Trim whitespace"),
50657
+ execute: (env) => {
50658
+ env.getStore(TrimWhitespaceStore).trimWhitespace();
50659
+ }
50660
+ };
50661
+ const cleanupDataSources = {
50662
+ name: _t("Remove unused data sources"),
50663
+ execute: (env) => env.openSidePanel("DataSourceCleanup", {}),
50664
+ isEnabled: (env) => !env.isSmall
50665
+ };
50666
+ const sortDescending = {
50667
+ name: _t("Descending (Z ⟶ A)"),
50668
+ execute: (env) => {
50669
+ const { anchor, zones } = env.model.getters.getSelection();
50670
+ interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "desc");
50671
+ },
50672
+ icon: "o-spreadsheet-Icon.SORT_DESCENDING"
50673
+ };
50674
+ const createRemoveFilter = { ...CREATE_OR_REMOVE_FILTER_ACTION };
50675
+ const createRemoveFilterTool = {
50676
+ ...CREATE_OR_REMOVE_FILTER_ACTION,
50677
+ isActive: (env) => SELECTED_TABLE_HAS_FILTERS(env)
50678
+ };
50679
+ const splitToColumns = {
50680
+ name: _t("Split text to columns"),
50681
+ sequence: 1,
50682
+ execute: (env) => env.openSidePanel("SplitToColumns", {}),
50683
+ isEnabled: (env) => !env.isSmall && env.model.getters.isSingleColSelected(),
50684
+ icon: "o-spreadsheet-Icon.SPLIT_TEXT"
50685
+ };
50686
+ const dataAnalysis = {
50687
+ name: _t("Data analysis"),
50688
+ execute: (env) => {
50689
+ env.getStore(SidePanelStore).togglePinnedSidePanel("DataAnalysisPanel");
50690
+ },
50691
+ icon: "o-spreadsheet-Icon.COLUMN_STATS",
50692
+ isEnabled: (env) => !env.isSmall
50693
+ };
50694
+ const columnStatistics = {
50695
+ name: _t("Column statistics"),
50696
+ execute: (env) => env.openSidePanel("ColumnStats", {}),
50697
+ icon: "o-spreadsheet-Icon.COLUMN_STATS"
50698
+ };
50699
+ const reinsertDynamicPivotMenu = {
50700
+ id: "reinsert_dynamic_pivot",
50701
+ name: _t("Re-insert dynamic pivot"),
50702
+ sequence: 60,
50703
+ icon: "o-spreadsheet-Icon.INSERT_PIVOT",
50704
+ children: [REINSERT_DYNAMIC_PIVOT_CHILDREN],
50705
+ isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
50706
+ };
50707
+ const reinsertStaticPivotMenu = {
50708
+ id: "reinsert_static_pivot",
50709
+ name: _t("Re-insert static pivot"),
50710
+ sequence: 70,
50711
+ icon: "o-spreadsheet-Icon.INSERT_PIVOT",
50712
+ children: [REINSERT_STATIC_PIVOT_CHILDREN],
50713
+ isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
50714
+ };
50715
+ const calculationMode = {
50716
+ name: _t("Calculation"),
50717
+ icon: "o-spreadsheet-Icon.CALCULATOR"
50718
+ };
50719
+ const automaticCalculation = {
50720
+ name: _t("Automatic"),
50721
+ execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: true }),
50722
+ isActive: (env) => env.model.getters.isAutomaticEvaluationEnabled()
50723
+ };
50724
+ const manualCalculation = {
50725
+ name: _t("Manual"),
50726
+ execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: false }),
50727
+ isActive: (env) => !env.model.getters.isAutomaticEvaluationEnabled()
50728
+ };
50729
+
48980
50730
  //#endregion
48981
50731
  //#region src/helpers/ui/merge_interactive.ts
48982
50732
  const AddMergeInteractiveContent = {
@@ -49191,6 +50941,10 @@ set(value) {
49191
50941
  }).addChild("paste_format_only", ["paste_special"], {
49192
50942
  ...pasteSpecialFormat,
49193
50943
  sequence: 20
50944
+ }).add("data_analysis", {
50945
+ ...dataAnalysis,
50946
+ sequence: 50,
50947
+ separator: true
49194
50948
  }).add("add_row_before", {
49195
50949
  ...cellInsertRowsBefore,
49196
50950
  sequence: 70
@@ -49298,211 +51052,6 @@ set(value) {
49298
51052
  sequence: 30
49299
51053
  });
49300
51054
 
49301
- //#endregion
49302
- //#region src/helpers/sort_interactive.ts
49303
- function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) {
49304
- let multiColumns = zone.right > zone.left;
49305
- if (env.model.getters.doesIntersectMerge(sheetId, zone)) {
49306
- multiColumns = false;
49307
- let table;
49308
- for (let row = zone.top; row <= zone.bottom; row++) {
49309
- table = [];
49310
- for (let col = zone.left; col <= zone.right; col++) {
49311
- const merge = env.model.getters.getMerge({
49312
- sheetId,
49313
- col,
49314
- row
49315
- });
49316
- if (merge && !table.includes(merge.id.toString())) table.push(merge.id.toString());
49317
- }
49318
- if (table.length >= 2) {
49319
- multiColumns = true;
49320
- break;
49321
- }
49322
- }
49323
- }
49324
- if (multiColumns) {
49325
- interactiveSort(env, sheetId, anchor, zone, sortDirection);
49326
- return;
49327
- }
49328
- const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);
49329
- if (isEqual(contiguousZone, zone)) interactiveSort(env, sheetId, anchor, zone, sortDirection);
49330
- 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));
49331
- }
49332
- function interactiveSort(env, sheetId, anchor, zone, sortDirection, sortOptions) {
49333
- const result = env.model.dispatch("SORT_CELLS", {
49334
- sheetId,
49335
- col: anchor.col,
49336
- row: anchor.row,
49337
- zone,
49338
- sortDirection,
49339
- sortOptions
49340
- });
49341
- if (result.isCancelledBecause("InvalidSortZone")) {
49342
- const { col, row } = anchor;
49343
- env.model.selection.selectZone({
49344
- cell: {
49345
- col,
49346
- row
49347
- },
49348
- zone
49349
- });
49350
- env.raiseError(_t("Cannot sort. To sort, select only cells or only merges that have the same size."));
49351
- }
49352
- if (result.isCancelledBecause("SortZoneWithArrayFormulas")) {
49353
- const { col, row } = anchor;
49354
- env.model.selection.selectZone({
49355
- cell: {
49356
- col,
49357
- row
49358
- },
49359
- zone
49360
- });
49361
- env.raiseError(_t("Cannot sort a zone with array formulas."));
49362
- }
49363
- }
49364
-
49365
- //#endregion
49366
- //#region src/stores/trim_whitespace_store.ts
49367
- var TrimWhitespaceStore = class extends SpreadsheetStore {
49368
- mutators = ["trimWhitespace"];
49369
- notificationStore = this.get(NotificationStore);
49370
- trimWhitespace() {
49371
- this.model.dispatch("TRIM_WHITESPACE");
49372
- }
49373
- handle(cmd) {
49374
- switch (cmd.type) {
49375
- case "TRIM_WHITESPACE":
49376
- this._trimWhitespace();
49377
- break;
49378
- }
49379
- }
49380
- _trimWhitespace() {
49381
- const zones = recomputeZones(this.getters.getSelectedZones());
49382
- const sheetId = this.getters.getActiveSheetId();
49383
- let count = 0;
49384
- for (const { col, row } of zones.map(positions).flat()) {
49385
- const cell = this.getters.getCell({
49386
- col,
49387
- row,
49388
- sheetId
49389
- });
49390
- if (!cell) continue;
49391
- const currentContent = !cell.isFormula ? cell.content : cell.compiledFormula.toFormulaString(this.getters);
49392
- const trimmedContent = trimContent(currentContent);
49393
- if (trimmedContent !== currentContent) {
49394
- count += 1;
49395
- this.model.dispatch("UPDATE_CELL", {
49396
- sheetId,
49397
- col,
49398
- row,
49399
- content: trimmedContent
49400
- });
49401
- }
49402
- }
49403
- const text = count ? _t("Trimmed whitespace from %s cells.", count) : _t("No selected cells had whitespace trimmed.");
49404
- this.notificationStore.notifyUser({
49405
- type: "info",
49406
- text,
49407
- sticky: false
49408
- });
49409
- }
49410
- };
49411
-
49412
- //#endregion
49413
- //#region src/actions/data_actions.ts
49414
- const sortRange = {
49415
- name: _t("Sort range"),
49416
- isVisible: IS_ONLY_ONE_RANGE,
49417
- icon: "o-spreadsheet-Icon.SORT_RANGE"
49418
- };
49419
- const sortAscending = {
49420
- name: _t("Ascending (A ⟶ Z)"),
49421
- execute: (env) => {
49422
- const { anchor, zones } = env.model.getters.getSelection();
49423
- interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "asc");
49424
- },
49425
- icon: "o-spreadsheet-Icon.SORT_ASCENDING"
49426
- };
49427
- const dataCleanup = {
49428
- name: _t("Data cleanup"),
49429
- icon: "o-spreadsheet-Icon.DATA_CLEANUP"
49430
- };
49431
- const removeDuplicates = {
49432
- name: _t("Remove duplicates"),
49433
- execute: (env) => {
49434
- if (getZoneArea(env.model.getters.getSelectedZone()) === 1) env.model.selection.selectTableAroundSelection();
49435
- env.openSidePanel("RemoveDuplicates", {});
49436
- },
49437
- isEnabled: (env) => !env.isSmall
49438
- };
49439
- const trimWhitespace = {
49440
- name: _t("Trim whitespace"),
49441
- execute: (env) => {
49442
- env.getStore(TrimWhitespaceStore).trimWhitespace();
49443
- }
49444
- };
49445
- const cleanupDataSources = {
49446
- name: _t("Remove unused data sources"),
49447
- execute: (env) => env.openSidePanel("DataSourceCleanup", {}),
49448
- isEnabled: (env) => !env.isSmall
49449
- };
49450
- const sortDescending = {
49451
- name: _t("Descending (Z ⟶ A)"),
49452
- execute: (env) => {
49453
- const { anchor, zones } = env.model.getters.getSelection();
49454
- interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "desc");
49455
- },
49456
- icon: "o-spreadsheet-Icon.SORT_DESCENDING"
49457
- };
49458
- const createRemoveFilter = { ...CREATE_OR_REMOVE_FILTER_ACTION };
49459
- const createRemoveFilterTool = {
49460
- ...CREATE_OR_REMOVE_FILTER_ACTION,
49461
- isActive: (env) => SELECTED_TABLE_HAS_FILTERS(env)
49462
- };
49463
- const splitToColumns = {
49464
- name: _t("Split text to columns"),
49465
- sequence: 1,
49466
- execute: (env) => env.openSidePanel("SplitToColumns", {}),
49467
- isEnabled: (env) => !env.isSmall && env.model.getters.isSingleColSelected(),
49468
- icon: "o-spreadsheet-Icon.SPLIT_TEXT"
49469
- };
49470
- const columnStatistics = {
49471
- name: _t("Column statistics"),
49472
- execute: (env) => env.openSidePanel("ColumnStats", {}),
49473
- icon: "o-spreadsheet-Icon.COLUMN_STATS"
49474
- };
49475
- const reinsertDynamicPivotMenu = {
49476
- id: "reinsert_dynamic_pivot",
49477
- name: _t("Re-insert dynamic pivot"),
49478
- sequence: 60,
49479
- icon: "o-spreadsheet-Icon.INSERT_PIVOT",
49480
- children: [REINSERT_DYNAMIC_PIVOT_CHILDREN],
49481
- isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
49482
- };
49483
- const reinsertStaticPivotMenu = {
49484
- id: "reinsert_static_pivot",
49485
- name: _t("Re-insert static pivot"),
49486
- sequence: 70,
49487
- icon: "o-spreadsheet-Icon.INSERT_PIVOT",
49488
- children: [REINSERT_STATIC_PIVOT_CHILDREN],
49489
- isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
49490
- };
49491
- const calculationMode = {
49492
- name: _t("Calculation"),
49493
- icon: "o-spreadsheet-Icon.CALCULATOR"
49494
- };
49495
- const automaticCalculation = {
49496
- name: _t("Automatic"),
49497
- execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: true }),
49498
- isActive: (env) => env.model.getters.isAutomaticEvaluationEnabled()
49499
- };
49500
- const manualCalculation = {
49501
- name: _t("Manual"),
49502
- execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: false }),
49503
- isActive: (env) => !env.model.getters.isAutomaticEvaluationEnabled()
49504
- };
49505
-
49506
51055
  //#endregion
49507
51056
  //#region src/registries/menus/col_menu_registry.ts
49508
51057
  const colMenuRegistry = new MenuItemRegistry();
@@ -49536,6 +51085,9 @@ set(value) {
49536
51085
  }).addChild("sort_descending", ["sort_columns"], {
49537
51086
  ...sortDescending,
49538
51087
  sequence: 20
51088
+ }).add("data_analysis", {
51089
+ ...dataAnalysis,
51090
+ sequence: 55
49539
51091
  }).add("column_statistics", {
49540
51092
  ...columnStatistics,
49541
51093
  sequence: 60,
@@ -52351,14 +53903,15 @@ set(value) {
52351
53903
  selectedRect: void 0,
52352
53904
  horizontalSnap: void 0,
52353
53905
  verticalSnap: void 0,
52354
- cancelDnd: void 0,
52355
- overlappingChartOrCarousel: void 0
53906
+ cancelDnd: void 0
52356
53907
  });
52357
53908
  viewStore;
52358
53909
  zoomStore;
53910
+ chartDragStore;
52359
53911
  setup() {
52360
53912
  this.viewStore = useStore(ViewportsStore);
52361
53913
  this.zoomStore = useStore(ZoomStore);
53914
+ this.chartDragStore = useStore(ChartDragStore);
52362
53915
  (0, _odoo_owl.onMounted)(() => {
52363
53916
  this.render();
52364
53917
  });
@@ -52372,7 +53925,7 @@ set(value) {
52372
53925
  this.dnd.selectedRect = void 0;
52373
53926
  this.dnd.horizontalSnap = void 0;
52374
53927
  this.dnd.verticalSnap = void 0;
52375
- this.dnd.overlappingChartOrCarousel = void 0;
53928
+ this.chartDragStore.setHighlightedFigure(void 0);
52376
53929
  this.dnd.cancelDnd = void 0;
52377
53930
  }
52378
53931
  });
@@ -52498,6 +54051,7 @@ set(value) {
52498
54051
  maxY: this.env.model.getters.getRowDimensions(sheetId, this.env.model.getters.getNumberRows(sheetId) - 1).end
52499
54052
  };
52500
54053
  let hasStartedDnd = false;
54054
+ let overlappingChartOrCarousel = void 0;
52501
54055
  const onMouseMove = (ev) => {
52502
54056
  const currentMousePosition = {
52503
54057
  x: ev.clientX / zoom,
@@ -52509,10 +54063,10 @@ set(value) {
52509
54063
  hasStartedDnd = true;
52510
54064
  const selectedFigures = dragFigureForMove(currentMousePosition, initialMousePosition, initialFigures, maxDimensions, initialScrollPosition, this.viewStore.activeSheetScrollInfo);
52511
54065
  const draggedFigure = selectedFigures.find((f) => f.id === draggedFigureId);
52512
- let overlappingChartOrCarousel = void 0;
54066
+ overlappingChartOrCarousel = void 0;
52513
54067
  const otherFigures = this.getOtherFigures(selectedFigures.map((f) => f.id));
52514
- if (draggedFigure && !selectedFigures.find((f) => f.tag !== "chart")) overlappingChartOrCarousel = this.getOverlappingFigure(draggedFigure, otherFigures);
52515
- this.dnd.overlappingChartOrCarousel = overlappingChartOrCarousel;
54068
+ if (draggedFigure && !selectedFigures.find((f) => f.tag !== "chart")) overlappingChartOrCarousel = getCarouselOverlappingChart(draggedFigure, otherFigures, ["carousel", "chart"]);
54069
+ this.chartDragStore.setHighlightedFigure(overlappingChartOrCarousel?.id);
52516
54070
  if (!overlappingChartOrCarousel) {
52517
54071
  const snapReturn = snapForMove(this.env, selectedFigures, otherFigures);
52518
54072
  this.dnd.selectedFigures = snapReturn.snappedFigures;
@@ -52534,7 +54088,7 @@ set(value) {
52534
54088
  else this.env.model.dispatch("SELECT_FIGURE", { figureId: figureUI.id });
52535
54089
  return;
52536
54090
  }
52537
- if (!this.dnd.overlappingChartOrCarousel) {
54091
+ if (!overlappingChartOrCarousel) {
52538
54092
  const payloads = this.dnd.selectedFigures?.map((f) => {
52539
54093
  return {
52540
54094
  sheetId,
@@ -52544,14 +54098,14 @@ set(value) {
52544
54098
  }) || [];
52545
54099
  this.env.model.dispatch("MOVE_FIGURES", { figures: payloads });
52546
54100
  } else {
52547
- const overlappingFigureId = this.dnd.overlappingChartOrCarousel.id;
54101
+ const overlappingFigureId = overlappingChartOrCarousel.id;
52548
54102
  const chartFigureIds = this.dnd.selectedFigures?.map((f) => f.id) || [];
52549
- if (this.dnd.overlappingChartOrCarousel.tag === "carousel") this.env.model.dispatch("ADD_FIGURES_CHART_TO_CAROUSEL", {
54103
+ if (overlappingChartOrCarousel.tag === "carousel") this.env.model.dispatch("ADD_FIGURES_CHART_TO_CAROUSEL", {
52550
54104
  sheetId,
52551
54105
  carouselFigureId: overlappingFigureId,
52552
54106
  chartFigureIds
52553
54107
  });
52554
- else if (this.dnd.overlappingChartOrCarousel.tag === "chart") this.env.model.dispatch("MERGE_CHART_FIGURES_INTO_CAROUSEL", {
54108
+ else if (overlappingChartOrCarousel.tag === "chart") this.env.model.dispatch("MERGE_CHART_FIGURES_INTO_CAROUSEL", {
52555
54109
  sheetId,
52556
54110
  baseFigureId: overlappingFigureId,
52557
54111
  chartFigureIds: [overlappingFigureId, ...chartFigureIds]
@@ -52562,7 +54116,7 @@ set(value) {
52562
54116
  this.dnd.selectedRect = void 0;
52563
54117
  this.dnd.horizontalSnap = void 0;
52564
54118
  this.dnd.verticalSnap = void 0;
52565
- this.dnd.overlappingChartOrCarousel = void 0;
54119
+ this.chartDragStore.setHighlightedFigure(void 0);
52566
54120
  };
52567
54121
  this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
52568
54122
  }
@@ -52618,7 +54172,6 @@ set(value) {
52618
54172
  this.dnd.selectedRect = void 0;
52619
54173
  this.dnd.horizontalSnap = void 0;
52620
54174
  this.dnd.verticalSnap = void 0;
52621
- this.dnd.overlappingChartOrCarousel = void 0;
52622
54175
  };
52623
54176
  this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
52624
54177
  }
@@ -52628,12 +54181,12 @@ set(value) {
52628
54181
  getFigureStyle(figureUI) {
52629
54182
  if (figureUI.id !== this.dnd.draggedFigure?.id) return "";
52630
54183
  return cssPropertiesToCss({
52631
- opacity: this.dnd.overlappingChartOrCarousel?.id ? "0.6" : "0.9",
54184
+ opacity: this.chartDragStore.highlightedFigureId ? "0.6" : "0.9",
52632
54185
  cursor: "grabbing"
52633
54186
  });
52634
54187
  }
52635
54188
  getFigureClass(figureUI) {
52636
- if (figureUI.id !== this.dnd.overlappingChartOrCarousel?.id) return "";
54189
+ if (figureUI.id !== this.chartDragStore.highlightedFigureId) return "";
52637
54190
  return "o-add-to-carousel";
52638
54191
  }
52639
54192
  getSnap(snapLine) {
@@ -52676,26 +54229,6 @@ set(value) {
52676
54229
  height: `100%`
52677
54230
  });
52678
54231
  }
52679
- getOverlappingFigure(figureUI, otherFigures) {
52680
- if (figureUI.tag !== "chart") return;
52681
- const figureCenterX = figureUI.x + figureUI.width / 2;
52682
- const figureCenterY = figureUI.y + figureUI.height / 2;
52683
- let bestMatch;
52684
- let smallestDistance = Infinity;
52685
- for (const figure of otherFigures) {
52686
- if (figure.tag !== "chart" && figure.tag !== "carousel") continue;
52687
- const targetCenterX = figure.x + figure.width / 2;
52688
- const targetCenterY = figure.y + figure.height / 2;
52689
- const distanceX = Math.abs(figureCenterX - targetCenterX);
52690
- const distanceY = Math.abs(figureCenterY - targetCenterY);
52691
- const squaredDistance = distanceX ** 2 + distanceY ** 2;
52692
- if (distanceX <= figureUI.width / 2 && distanceY <= figureUI.height / 2 && squaredDistance < smallestDistance) {
52693
- smallestDistance = squaredDistance;
52694
- bestMatch = figure;
52695
- }
52696
- }
52697
- return bestMatch;
52698
- }
52699
54232
  };
52700
54233
 
52701
54234
  //#endregion
@@ -52995,7 +54528,7 @@ set(value) {
52995
54528
  var ColResizer = class extends AbstractResizer {
52996
54529
  static template = "o-spreadsheet-ColResizer";
52997
54530
  static components = { UnhideColumnHeaders };
52998
- colResizerRef = (0, _odoo_owl.signal)(null);
54531
+ colResizerRef = _odoo_owl.signal.ref();
52999
54532
  setup() {
53000
54533
  super.setup();
53001
54534
  this.PADDING = 15;
@@ -53129,7 +54662,7 @@ set(value) {
53129
54662
  var RowResizer = class extends AbstractResizer {
53130
54663
  static template = "o-spreadsheet-RowResizer";
53131
54664
  static components = { UnhideRowHeaders };
53132
- rowResizerRef = (0, _odoo_owl.signal)(null);
54665
+ rowResizerRef = _odoo_owl.signal.ref();
53133
54666
  setup() {
53134
54667
  super.setup();
53135
54668
  this.PADDING = 5;
@@ -53738,9 +55271,8 @@ set(value) {
53738
55271
  useStore(TableResizeStore);
53739
55272
  }
53740
55273
  get containerStyle() {
53741
- const tableZone = this.props.table.range.zone;
53742
55274
  const sheetId = this.props.table.range.sheetId;
53743
- if (this.env.model.getters.isReadonly() || this.env.model.getters.isSheetLocked(sheetId)) return cssPropertiesToCss({ display: "none" });
55275
+ const tableZone = this.props.table.range.zone;
53744
55276
  const bottomRight = {
53745
55277
  ...tableZone,
53746
55278
  left: tableZone.right,
@@ -53827,8 +55359,8 @@ set(value) {
53827
55359
  HEADER_HEIGHT = 26;
53828
55360
  HEADER_WIDTH = 48;
53829
55361
  menuState;
53830
- gridRef = (0, _odoo_owl.signal)(null);
53831
- canvasRef = (0, _odoo_owl.signal)(null);
55362
+ gridRef = _odoo_owl.signal.ref();
55363
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
53832
55364
  highlightStore;
53833
55365
  viewStore;
53834
55366
  zoomStore;
@@ -54459,6 +55991,9 @@ set(value) {
54459
55991
  const sheetId = this.env.model.getters.getActiveSheetId();
54460
55992
  return this.env.model.getters.getClientsToDisplay(sheetId);
54461
55993
  }
55994
+ get displayTableResizer() {
55995
+ return this.env.model.getters.isGridSelectionActive() && !this.env.model.getters.isReadonly() && !this.env.model.getters.isSheetLocked(this.env.model.getters.getActiveSheetId());
55996
+ }
54462
55997
  };
54463
55998
 
54464
55999
  //#endregion
@@ -54477,7 +56012,7 @@ set(value) {
54477
56012
  static template = "o-spreadsheet-FullScreenFigure";
54478
56013
  static components = { ChartFigure };
54479
56014
  fullScreenFigureStore;
54480
- fullScreenFigureRef = (0, _odoo_owl.signal)(null);
56015
+ fullScreenFigureRef = _odoo_owl.signal.ref();
54481
56016
  spreadsheetRect = useSpreadsheetRect();
54482
56017
  figureRegistry = figureRegistry;
54483
56018
  setup() {
@@ -54725,6 +56260,41 @@ set(value) {
54725
56260
  }
54726
56261
  };
54727
56262
 
56263
+ //#endregion
56264
+ //#region src/components/tables/hovered_table_store.ts
56265
+ var HoveredTableStore = class extends SpreadsheetStore {
56266
+ mutators = ["hover"];
56267
+ storeGetters = ["getCellHoverOverlayColor"];
56268
+ position;
56269
+ constructor(get) {
56270
+ super(get);
56271
+ const cellHoverOverlayStore = this.get(CellHoverOverlayStore);
56272
+ cellHoverOverlayStore.register(this);
56273
+ this.onDispose(() => cellHoverOverlayStore.unRegister(this));
56274
+ }
56275
+ getHighlightedPositions(hoveredPosition) {
56276
+ const highlightedPositions = [];
56277
+ const { sheetId, row } = hoveredPosition;
56278
+ const table = this.getters.getTable(hoveredPosition);
56279
+ if (!table) return highlightedPositions;
56280
+ const { left, right, top } = table.range.zone;
56281
+ const isTableHeader = row < top + table.config.numberOfHeaders;
56282
+ const doesTableRowHaveContent = range(left, right + 1).some((col) => {
56283
+ return !this.getters.isColHidden(sheetId, col) && this.getters.getEvaluatedCell({
56284
+ sheetId,
56285
+ col,
56286
+ row
56287
+ }).formattedValue;
56288
+ });
56289
+ if (!isTableHeader && doesTableRowHaveContent) for (let col = left; col <= right; col++) highlightedPositions.push({
56290
+ sheetId,
56291
+ col,
56292
+ row
56293
+ });
56294
+ return highlightedPositions;
56295
+ }
56296
+ };
56297
+
54728
56298
  //#endregion
54729
56299
  //#region src/helpers/figures/charts/runtime/helpers_index.ts
54730
56300
  var helpers_index_exports = /* @__PURE__ */ __exportAll({
@@ -55515,7 +57085,8 @@ set(value) {
55515
57085
  * The result of this method is:
55516
57086
  * - if the cell is not a formula, returns the content as is and resets the base formula
55517
57087
  * - if the cell is a formula:
55518
- * - if there is no previous formula, or the normalized formula is different from the previous one, resets the base formula to this one and returns the full formula string
57088
+ * - if there is no previous formula, or the normalized formula is different from the previous one or it contains blacklisted functions (see NonSquishableFunctionRegistry),
57089
+ * resets the base formula to this one and returns the full formula string
55519
57090
  * - else, compares the literal values and range dependencies to the previous formula, and for each parameter:
55520
57091
  * - for numbers: returns a relative change (+N or -N) if possible, else the full number or "=" if unchanged
55521
57092
  * - for strings: returns the full string if changed, else "="
@@ -55523,6 +57094,10 @@ set(value) {
55523
57094
  * */
55524
57095
  squish(cell, forSheetId) {
55525
57096
  if (cell.isFormula) {
57097
+ if (NonSquishableFunctionRegistry.getAll().some((functionName) => cell.compiledFormula.usesSymbol(functionName))) {
57098
+ this.resetBaseFormula();
57099
+ return cell.compiledFormula.toFormulaString(this.getters, this.rangeToStringOptions);
57100
+ }
55526
57101
  let numbers = [];
55527
57102
  let strings = [];
55528
57103
  let references = [];
@@ -65325,7 +66900,7 @@ set(value) {
65325
66900
  }
65326
66901
  message = {
65327
66902
  ...message,
65328
- commands: this.commandSquisher.squish(revision.commands)
66903
+ commands: revision.commands
65329
66904
  };
65330
66905
  }
65331
66906
  if (this.isReplayingInitialRevisions) throw new Error(`Trying to send a new revision while replaying initial revision. This can lead to endless dispatches every time the spreadsheet is open.
@@ -67822,6 +69397,12 @@ set(value) {
67822
69397
  const figures = cmd.chartFigureIds.map((id) => this.getters.getFigure(cmd.sheetId, id));
67823
69398
  const baseFigureId = this.getters.getFigure(cmd.sheetId, cmd.baseFigureId);
67824
69399
  if (figures.some((f) => f === void 0 || f.tag !== "chart") || !baseFigureId || baseFigureId.tag !== "chart") return "FigureDoesNotExist";
69400
+ break;
69401
+ case "CREATE_CHART_AND_MERGE_INTO_CAROUSEL":
69402
+ const baseFigure = this.getters.getFigure(cmd.sheetId, cmd.baseFigureId);
69403
+ if (this.getters.getFigure(cmd.sheetId, cmd.figureId) || !baseFigure) return "InvalidFigureId";
69404
+ if (baseFigure.tag !== "chart") return "FigureDoesNotExist";
69405
+ break;
67825
69406
  }
67826
69407
  return "Success";
67827
69408
  }
@@ -67857,6 +69438,29 @@ set(value) {
67857
69438
  carouselFigureId,
67858
69439
  chartFigureIds: cmd.chartFigureIds
67859
69440
  });
69441
+ break;
69442
+ case "CREATE_CHART_AND_MERGE_INTO_CAROUSEL":
69443
+ const baseFigureToMerge = this.getters.getFigure(cmd.sheetId, cmd.baseFigureId);
69444
+ if (!baseFigureToMerge) throw new Error(`Figure ${cmd.baseFigureId} does not exists.`);
69445
+ this.dispatch("CREATE_CHART", {
69446
+ chartId: cmd.chartId,
69447
+ figureId: cmd.figureId,
69448
+ sheetId: cmd.sheetId,
69449
+ definition: cmd.definition,
69450
+ col: baseFigureToMerge.col,
69451
+ row: baseFigureToMerge.row,
69452
+ offset: baseFigureToMerge.offset,
69453
+ size: {
69454
+ width: baseFigureToMerge.width,
69455
+ height: baseFigureToMerge.height
69456
+ }
69457
+ });
69458
+ this.dispatch("MERGE_CHART_FIGURES_INTO_CAROUSEL", {
69459
+ sheetId: cmd.sheetId,
69460
+ baseFigureId: cmd.baseFigureId,
69461
+ chartFigureIds: [cmd.baseFigureId, cmd.figureId]
69462
+ });
69463
+ break;
67860
69464
  }
67861
69465
  }
67862
69466
  getFigureUI(sheetId, figure) {
@@ -69546,6 +71150,9 @@ set(value) {
69546
71150
  }).addChild("split_to_columns", ["data"], {
69547
71151
  ...splitToColumns,
69548
71152
  sequence: 20
71153
+ }).addChild("data_analysis", ["data"], {
71154
+ ...dataAnalysis,
71155
+ sequence: 24
69549
71156
  }).addChild("column_statistics", ["data"], {
69550
71157
  ...columnStatistics,
69551
71158
  sequence: 25
@@ -69947,7 +71554,7 @@ set(value) {
69947
71554
  onAnimationEnd: types.function(),
69948
71555
  style: types.string()
69949
71556
  });
69950
- rippleRef = (0, _odoo_owl.signal)(null);
71557
+ rippleRef = _odoo_owl.signal.ref();
69951
71558
  setup() {
69952
71559
  let animation = void 0;
69953
71560
  (0, _odoo_owl.onMounted)(() => {
@@ -70001,7 +71608,7 @@ set(value) {
70001
71608
  onAnimationEnd: types.function().optional(() => () => {}),
70002
71609
  class: types.string().optional("")
70003
71610
  });
70004
- childContainerRef = (0, _odoo_owl.signal)(null);
71611
+ childContainerRef = _odoo_owl.signal.ref();
70005
71612
  state = (0, _odoo_owl.proxy)({ ripples: [] });
70006
71613
  currentId = 1;
70007
71614
  onClick(ev) {
@@ -70272,9 +71879,9 @@ set(value) {
70272
71879
  isEditing: false,
70273
71880
  openedPicker: void 0
70274
71881
  });
70275
- sheetDivRef = (0, _odoo_owl.signal)(null);
70276
- iconRef = (0, _odoo_owl.signal)(null);
70277
- sheetNameRef = (0, _odoo_owl.signal)(null);
71882
+ sheetDivRef = _odoo_owl.signal.ref();
71883
+ iconRef = _odoo_owl.signal.ref();
71884
+ sheetNameRef = _odoo_owl.signal.ref();
70278
71885
  editionState = "initializing";
70279
71886
  DOMFocusableElementStore;
70280
71887
  setup() {
@@ -70558,8 +72165,10 @@ set(value) {
70558
72165
  });
70559
72166
  state = (0, _odoo_owl.proxy)({ selectedStatisticFn: "" });
70560
72167
  store;
72168
+ sidePanelStore;
70561
72169
  setup() {
70562
72170
  this.store = useStore(AggregateStatisticsStore);
72171
+ this.sidePanelStore = useStore(SidePanelStore);
70563
72172
  (0, _odoo_owl.onWillUpdateProps)(() => {
70564
72173
  if (Object.values(this.store.statisticFnResults).every((result) => result?.value === void 0)) this.props.closeContextMenu();
70565
72174
  });
@@ -70599,6 +72208,12 @@ set(value) {
70599
72208
  format: fnValue.format()
70600
72209
  });
70601
72210
  }
72211
+ get isDataAnalysisOpen() {
72212
+ return this.sidePanelStore.mainPanel?.componentTag === "DataAnalysisPanel" && this.sidePanelStore.isMainPanelOpen || this.sidePanelStore.secondaryPanel?.componentTag === "DataAnalysisPanel" && this.sidePanelStore.isSecondaryPanelOpen;
72213
+ }
72214
+ toggleDataAnalysisPanel() {
72215
+ this.sidePanelStore.togglePinnedSidePanel("DataAnalysisPanel");
72216
+ }
70602
72217
  };
70603
72218
 
70604
72219
  //#endregion
@@ -70613,8 +72228,8 @@ set(value) {
70613
72228
  BottomBarSheet,
70614
72229
  BottomBarStatistic
70615
72230
  };
70616
- bottomBarRef = (0, _odoo_owl.signal)(null);
70617
- sheetListRef = (0, _odoo_owl.signal)(null);
72231
+ bottomBarRef = _odoo_owl.signal.ref();
72232
+ sheetListRef = _odoo_owl.signal.ref();
70618
72233
  dragAndDrop = useDragAndDropListItems();
70619
72234
  targetScroll = void 0;
70620
72235
  state = (0, _odoo_owl.proxy)({
@@ -70827,12 +72442,13 @@ set(value) {
70827
72442
  hoveredCell;
70828
72443
  viewStore;
70829
72444
  zoomStore;
70830
- gridRef = (0, _odoo_owl.signal)(null);
70831
- canvasRef = (0, _odoo_owl.signal)(null);
72445
+ gridRef = _odoo_owl.signal.ref();
72446
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
70832
72447
  setup() {
70833
72448
  this.hoveredCell = useStore(DelayedHoveredCellStore);
70834
72449
  this.viewStore = useStore(ViewportsStore);
70835
72450
  this.zoomStore = useStore(ZoomStore);
72451
+ useLocalStore(HoveredTableStore);
70836
72452
  const rendererStore = useLocalStore(RendererStore, OrderedLayers().filter((layer) => layer !== "Headers"));
70837
72453
  useChildSubEnv({ getPopoverContainerRect: () => this.zoomStore.getZoomedRect(this.getGridRect()) });
70838
72454
  useGridDrawing({
@@ -71250,8 +72866,8 @@ set(value) {
71250
72866
  static components = { Menu };
71251
72867
  props = (0, _odoo_owl.useProps)({ onClose: types.function() });
71252
72868
  rootItems = topbarMenuRegistry.getMenuItems();
71253
- menuRef = (0, _odoo_owl.signal)(null);
71254
- containerRef = (0, _odoo_owl.signal)(null);
72869
+ menuRef = _odoo_owl.signal.ref();
72870
+ containerRef = _odoo_owl.signal.ref();
71255
72871
  state = (0, _odoo_owl.proxy)({
71256
72872
  menuItems: this.rootItems,
71257
72873
  title: _t("Menu Bar"),
@@ -71324,7 +72940,7 @@ set(value) {
71324
72940
  composerStore;
71325
72941
  viewStore;
71326
72942
  composerInterface;
71327
- composerRef = (0, _odoo_owl.signal)(null);
72943
+ composerRef = _odoo_owl.signal.ref();
71328
72944
  menuState = (0, _odoo_owl.proxy)({ isOpen: false });
71329
72945
  setup() {
71330
72946
  this.composerFocusStore = useStore(ComposerFocusStore);
@@ -71403,6 +73019,49 @@ set(value) {
71403
73019
  }
71404
73020
  };
71405
73021
 
73022
+ //#endregion
73023
+ //#region src/components/standalone_grid_canvas/carousel_data_view_print.ts
73024
+ var CarouselDataViewPrint = class extends Component {
73025
+ static template = _odoo_owl.xml``;
73026
+ props = (0, _odoo_owl.useProps)({
73027
+ range: types.Range(),
73028
+ columnWeights: types.array().optional(),
73029
+ rect: types.object(),
73030
+ rendererStore: types.object()
73031
+ });
73032
+ viewportsStore;
73033
+ gridRender;
73034
+ setup() {
73035
+ useChildStoreProvider([ViewportsStore]);
73036
+ const store = useLocalStore(StandaloneViewportStore, this.props.range, this.props.columnWeights);
73037
+ this.gridRender = useLocalStore(GridRenderer, null);
73038
+ this.viewportsStore = useStore(ViewportsStore);
73039
+ (0, _odoo_owl.useEffect)(() => {
73040
+ store.setContainerSize(this.props.rect.width, this.props.rect.height);
73041
+ });
73042
+ (0, _odoo_owl.onMounted)(() => this.props.rendererStore.register(this));
73043
+ (0, _odoo_owl.onWillUnmount)(() => this.props.rendererStore.unRegister(this));
73044
+ }
73045
+ get renderingLayers() {
73046
+ return ["Chart"];
73047
+ }
73048
+ drawLayer(renderingContext) {
73049
+ const { ctx } = renderingContext;
73050
+ ctx.save();
73051
+ ctx.translate(this.props.rect.x, this.props.rect.y);
73052
+ ctx.beginPath();
73053
+ ctx.rect(0, 0, this.props.rect.width, this.props.rect.height);
73054
+ ctx.clip();
73055
+ const subRenderingCtx = {
73056
+ ...renderingContext,
73057
+ sheetId: this.viewportsStore.displayedSheetId,
73058
+ viewports: this.viewportsStore.viewports
73059
+ };
73060
+ this.gridRender.drawLayer(subRenderingCtx, "Background", void 0);
73061
+ ctx.restore();
73062
+ }
73063
+ };
73064
+
71406
73065
  //#endregion
71407
73066
  //#region src/components/standalone_grid_canvas/figure_renderer_store.ts
71408
73067
  /** Store that draws the figures directly onto the canvas */
@@ -71428,29 +73087,25 @@ set(value) {
71428
73087
  drawLayer(renderingCtx) {
71429
73088
  const { viewports, ctx } = renderingCtx;
71430
73089
  const visibleFigures = viewports.getVisibleFigures(renderingCtx.sheetId);
71431
- const { x: offsetX, y: offsetY } = viewports.getViewportOffset(renderingCtx.sheetId);
73090
+ const scrollOffset = viewports.getViewportOffset(renderingCtx.sheetId);
71432
73091
  for (const figure of visibleFigures) {
71433
- const x = figure.x - offsetX;
71434
- const y = figure.y - offsetY;
73092
+ const figureRect = {
73093
+ ...figure,
73094
+ x: figure.x - scrollOffset.x,
73095
+ y: figure.y - scrollOffset.y
73096
+ };
73097
+ const { x, y, width, height } = figureRect;
71435
73098
  if (figure.tag === "chart") {
71436
73099
  const chartId = this.getters.getChartIdFromFigureId(figure.id);
71437
- if (chartId) {
71438
- const chartRect = {
71439
- x,
71440
- y,
71441
- width: figure.width,
71442
- height: figure.height
71443
- };
71444
- this.drawChart(renderingCtx, chartId, chartRect);
71445
- }
73100
+ if (chartId) this.drawChart(renderingCtx, chartId, figureRect);
71446
73101
  } else if (figure.tag === "image") {
71447
73102
  const loadedImage = this.loadedImages[this.getters.getImagePath(figure.id)];
71448
- if (loadedImage) ctx.drawImage(loadedImage, x, y, figure.width, figure.height);
71449
- } else if (figure.tag === "carousel") this.drawCarousel(renderingCtx, figure);
73103
+ if (loadedImage) ctx.drawImage(loadedImage, x, y, width, height);
73104
+ } else if (figure.tag === "carousel") this.drawCarousel(renderingCtx, figure, figureRect);
71450
73105
  if (!this.getters.isDashboard()) {
71451
73106
  ctx.strokeStyle = GRAY_400;
71452
73107
  ctx.lineWidth = 1;
71453
- ctx.strokeRect(x, y, figure.width, figure.height);
73108
+ ctx.strokeRect(x, y, width, height);
71454
73109
  }
71455
73110
  }
71456
73111
  }
@@ -71468,53 +73123,37 @@ set(value) {
71468
73123
  renderingCtx.ctx.drawImage(chartCanvas, x, y, width, height);
71469
73124
  cleanUp();
71470
73125
  }
71471
- drawCarousel(renderingCtx, figure) {
71472
- const { viewports: sheetView, ctx } = renderingCtx;
71473
- const { x: offsetX, y: offsetY } = sheetView.getViewportOffset(renderingCtx.sheetId);
71474
- const x = figure.x - offsetX;
71475
- const y = figure.y - offsetY;
73126
+ drawCarousel(renderingCtx, figure, figureRect) {
73127
+ const { ctx } = renderingCtx;
71476
73128
  const carousel = this.getters.getCarousel(figure.id);
71477
73129
  const chartId = this.getters.getChartIdFromFigureId(figure.id);
71478
73130
  if (!carousel) return;
73131
+ const layout = getCarouselLayout(figureRect, carousel, this.getters.getSelectedCarouselItem(figure.id));
71479
73132
  const chartDefinition = chartId ? this.getters.getChartDefinition(chartId) : void 0;
73133
+ ctx.save();
73134
+ ctx.beginPath();
73135
+ ctx.rect(figureRect.x, figureRect.y, figureRect.width, figureRect.height);
73136
+ ctx.clip();
73137
+ ctx.fillStyle = chartDefinition?.background || this.getters.getSpreadsheetTheme().backgroundColor;
73138
+ ctx.fillRect(figureRect.x, figureRect.y, figureRect.width, figureRect.height);
71480
73139
  const title = {
71481
73140
  ...DEFAULT_CAROUSEL_TITLE_STYLE,
71482
73141
  ...carousel.title
71483
73142
  };
71484
- const headerPadding = 4;
71485
- const headerSize = title.fontSize + headerPadding * 2;
71486
- if (!title.text && chartId) {
71487
- const chartRect = {
71488
- x,
71489
- y,
71490
- width: figure.width,
71491
- height: figure.height
71492
- };
71493
- this.drawChart(renderingCtx, chartId, chartRect);
71494
- } else if (title.text) {
71495
- ctx.save();
71496
- ctx.fillStyle = chartDefinition?.background || this.getters.getSpreadsheetTheme().backgroundColor;
71497
- ctx.fillRect(x, y, figure.width, headerSize);
71498
- ctx.font = computeTextFont(chartStyleToCellStyle(title));
73143
+ if (title.text) {
73144
+ ctx.font = computeTextFont(chartStyleToCellStyle(title), "px", 500);
71499
73145
  ctx.fillStyle = title.color;
71500
- ctx.fillText(title.text, x + headerPadding, y + headerPadding + title.fontSize);
71501
- ctx.restore();
71502
- const chartRect = {
71503
- x,
71504
- y: y + headerSize,
71505
- width: figure.width,
71506
- height: figure.height - headerSize
71507
- };
71508
- if (chartId) this.drawChart(renderingCtx, chartId, chartRect);
71509
- else if (!this.getters.isDashboard()) {
71510
- ctx.strokeStyle = GRAY_400;
71511
- ctx.lineWidth = 1;
71512
- ctx.beginPath();
71513
- ctx.moveTo(x, y + headerSize);
71514
- ctx.lineTo(x + figure.width, y + headerSize);
71515
- ctx.stroke();
71516
- }
73146
+ ctx.textBaseline = "middle";
73147
+ const textY = Math.ceil(layout.headerRect.y + layout.headerRect.height / 2);
73148
+ ctx.fillText(title.text, layout.headerRect.x, textY);
73149
+ }
73150
+ const separator = layout.separatorRect;
73151
+ if (separator) {
73152
+ ctx.fillStyle = GRAY_400;
73153
+ ctx.fillRect(separator.x, separator.y, separator.width, separator.height);
71517
73154
  }
73155
+ ctx.restore();
73156
+ if (chartId) this.drawChart(renderingCtx, chartId, layout.contentRect);
71518
73157
  }
71519
73158
  };
71520
73159
 
@@ -71522,12 +73161,13 @@ set(value) {
71522
73161
  //#region src/components/standalone_grid_canvas/standalone_grid_canvas.ts
71523
73162
  var StandaloneGridCanvas = class extends Component {
71524
73163
  static template = "o-spreadsheet-StandaloneGridCanvas";
73164
+ static components = { CarouselDataViewPrint };
71525
73165
  props = (0, _odoo_owl.useProps)({
71526
73166
  sheetId: types.UID(),
71527
73167
  zone: types.Zone(),
71528
73168
  renderingCtx: types.object()
71529
73169
  });
71530
- canvasRef = (0, _odoo_owl.signal)(null);
73170
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
71531
73171
  rendererStore;
71532
73172
  figureRendererStore;
71533
73173
  setup() {
@@ -71549,6 +73189,28 @@ set(value) {
71549
73189
  this.figureRendererStore.addLoadedImage(path, await createImageBitmap(blob));
71550
73190
  }));
71551
73191
  }
73192
+ get carouselDataViews() {
73193
+ const { sheetId, viewports } = this.props.renderingCtx;
73194
+ return viewports.getVisibleFigures(sheetId).filter((figure) => figure.tag === "carousel").map((figure) => {
73195
+ const carouselItem = this.env.model.getters.getSelectedCarouselItem(figure.id);
73196
+ if (carouselItem?.type !== "carouselDataView" || !carouselItem.range) return;
73197
+ const carousel = this.env.model.getters.getCarousel(figure.id);
73198
+ const scroll = viewports.getViewportOffset(sheetId);
73199
+ const { x, y, width, height } = figure;
73200
+ const layout = getCarouselLayout({
73201
+ x: x - scroll.x,
73202
+ y: y - scroll.y,
73203
+ width,
73204
+ height
73205
+ }, carousel, carouselItem);
73206
+ return {
73207
+ range: carouselItem.range,
73208
+ columnWeights: carouselItem.columnWeights,
73209
+ rect: layout.contentRect,
73210
+ rendererStore: this.rendererStore
73211
+ };
73212
+ }).filter(isDefined);
73213
+ }
71552
73214
  };
71553
73215
 
71554
73216
  //#endregion
@@ -72010,7 +73672,7 @@ set(value) {
72010
73672
  anchorRect: null,
72011
73673
  menuItems: []
72012
73674
  });
72013
- namedRangeSelectorRef = (0, _odoo_owl.signal)(null);
73675
+ namedRangeSelectorRef = _odoo_owl.signal.ref();
72014
73676
  setup() {
72015
73677
  this.topBarToolStore = useToolBarDropdownStore();
72016
73678
  this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
@@ -72208,7 +73870,7 @@ set(value) {
72208
73870
  anchorRect: types.Rect()
72209
73871
  });
72210
73872
  BORDER_POSITIONS = BORDER_POSITIONS;
72211
- lineStyleButtonRef = (0, _odoo_owl.signal)(null);
73873
+ lineStyleButtonRef = _odoo_owl.signal.ref();
72212
73874
  borderStyles = borderStyles;
72213
73875
  state = (0, _odoo_owl.proxy)({ activeTool: void 0 });
72214
73876
  toggleDropdownTool(tool) {
@@ -72275,7 +73937,7 @@ set(value) {
72275
73937
  });
72276
73938
  topBarToolStore;
72277
73939
  viewStore;
72278
- borderEditorButtonRef = (0, _odoo_owl.signal)(null);
73940
+ borderEditorButtonRef = _odoo_owl.signal.ref();
72279
73941
  state = (0, _odoo_owl.proxy)({
72280
73942
  currentColor: DEFAULT_BORDER_DESC.color,
72281
73943
  currentStyle: DEFAULT_BORDER_DESC.style,
@@ -72505,7 +74167,7 @@ set(value) {
72505
74167
  childClass: types.string()
72506
74168
  });
72507
74169
  topBarToolStore;
72508
- actionRef = (0, _odoo_owl.signal)(null);
74170
+ actionRef = _odoo_owl.signal.ref();
72509
74171
  setup() {
72510
74172
  this.topBarToolStore = useToolBarDropdownStore();
72511
74173
  }
@@ -72568,7 +74230,7 @@ set(value) {
72568
74230
  props = (0, _odoo_owl.useProps)({ class: types.string() });
72569
74231
  formatNumberMenuItemSpec = formatNumberMenuItemSpec;
72570
74232
  topBarToolStore;
72571
- buttonRef = (0, _odoo_owl.signal)(null);
74233
+ buttonRef = _odoo_owl.signal.ref();
72572
74234
  state = (0, _odoo_owl.proxy)({
72573
74235
  anchorRect: {
72574
74236
  x: 0,
@@ -72857,12 +74519,12 @@ set(value) {
72857
74519
  composerFocusStore;
72858
74520
  fingerprints;
72859
74521
  topBarToolStore;
72860
- toolBarContainerRef = (0, _odoo_owl.signal)(null);
72861
- toolbarRef = (0, _odoo_owl.signal)(null);
72862
- namedRangesRef = (0, _odoo_owl.signal)(null);
72863
- topBarTopRef = (0, _odoo_owl.signal)(null);
72864
- moreToolsContainerRef = (0, _odoo_owl.signal)(null);
72865
- moreToolsButtonRef = (0, _odoo_owl.signal)(null);
74522
+ toolBarContainerRef = _odoo_owl.signal.ref();
74523
+ toolbarRef = _odoo_owl.signal.ref();
74524
+ namedRangesRef = _odoo_owl.signal.ref();
74525
+ topBarTopRef = _odoo_owl.signal.ref();
74526
+ moreToolsContainerRef = _odoo_owl.signal.ref();
74527
+ moreToolsButtonRef = _odoo_owl.signal.ref();
72866
74528
  spreadsheetRect = useSpreadsheetRect();
72867
74529
  setup() {
72868
74530
  this.composerFocusStore = useStore(ComposerFocusStore);
@@ -73124,7 +74786,7 @@ set(value) {
73124
74786
  SpreadsheetPrint
73125
74787
  };
73126
74788
  sidePanel;
73127
- spreadsheetRef = (0, _odoo_owl.signal)(null);
74789
+ spreadsheetRef = _odoo_owl.signal.ref();
73128
74790
  spreadsheetRect = useSpreadsheetRect();
73129
74791
  state = (0, _odoo_owl.proxy)({ printModeEnabled: false });
73130
74792
  _focusGrid;
@@ -79132,12 +80794,13 @@ set(value) {
79132
80794
  }
79133
80795
  sortFilterZone(sortDirection) {
79134
80796
  const filterPosition = this.props.filterPosition;
79135
- const tableZone = this.table?.range.zone;
80797
+ const table = this.table;
80798
+ const tableZone = table?.range.zone;
79136
80799
  if (!filterPosition || !tableZone || tableZone.top === tableZone.bottom) return;
79137
80800
  const sheetId = this.viewStore.displayedSheetId;
79138
80801
  const contentZone = {
79139
80802
  ...tableZone,
79140
- top: tableZone.top + 1
80803
+ top: tableZone.top + table.config.numberOfHeaders
79141
80804
  };
79142
80805
  const sortAnchor = {
79143
80806
  col: filterPosition.col,
@@ -79295,9 +80958,9 @@ set(value) {
79295
80958
  onClosed: types.function().optional()
79296
80959
  });
79297
80960
  static size = { maxHeight: 500 };
79298
- urlInput = (0, _odoo_owl.signal)(null);
79299
- suggestionListRef = (0, _odoo_owl.signal)(null);
79300
- urlInputContainer = (0, _odoo_owl.signal)(null);
80961
+ urlInput = _odoo_owl.signal.ref(HTMLInputElement);
80962
+ suggestionListRef = _odoo_owl.signal.ref();
80963
+ urlInputContainer = _odoo_owl.signal.ref();
79301
80964
  state;
79302
80965
  viewStore;
79303
80966
  setup() {
@@ -81415,7 +83078,7 @@ set(value) {
81415
83078
  else if (_format === 0) {
81416
83079
  const rowSeparator = this.locale.decimalSeparator === "," ? "/" : ",";
81417
83080
  return { value: transposeMatrix(_array).flatMap((row) => row.map((value) => {
81418
- return isEvaluationError(value.value) ? value.value : toString(value);
83081
+ return isEvaluationError(value.value) ? value.value : toLocaleString(value, this.locale);
81419
83082
  })).join(rowSeparator) };
81420
83083
  } else return new EvaluationError(_t("Format must be 0 or 1"));
81421
83084
  },
@@ -87463,7 +89126,7 @@ set(value) {
87463
89126
  description: _t("Concatenation of two values."),
87464
89127
  args: [arg("value1 (string)", _t("The value to which value2 will be appended.")), arg("value2 (string)", _t("The value to append to value1."))],
87465
89128
  compute: function(value1, value2) {
87466
- return { value: toString(value1) + toString(value2) };
89129
+ return { value: toLocaleString(value1, this.locale) + toLocaleString(value2, this.locale) };
87467
89130
  },
87468
89131
  isExported: true
87469
89132
  };
@@ -88346,7 +90009,7 @@ set(value) {
88346
90009
  description: _t("Remove non-printable characters from a piece of text."),
88347
90010
  args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))],
88348
90011
  compute: function(text) {
88349
- const _text = toString(text);
90012
+ const _text = toLocaleString(text, this.locale);
88350
90013
  let cleanedStr = "";
88351
90014
  for (const char of _text) if (char && char.charCodeAt(0) > 31) cleanedStr += char;
88352
90015
  return { value: cleanedStr };
@@ -88357,7 +90020,7 @@ set(value) {
88357
90020
  description: _t("Appends strings to one another."),
88358
90021
  args: [arg("string (string, range<string>, repeating)", _t("String to append in sequence."))],
88359
90022
  compute: function(...datas) {
88360
- return { value: reduceAny(datas, (acc, a) => acc + toString(a), "") };
90023
+ return { value: reduceAny(datas, (acc, a) => acc + toLocaleString(a, this.locale), "") };
88361
90024
  },
88362
90025
  isExported: true
88363
90026
  };
@@ -88365,7 +90028,7 @@ set(value) {
88365
90028
  description: _t("Tests whether two strings are identical."),
88366
90029
  args: [arg("string1 (string)", _t("The first string to compare.")), arg("string2 (string)", _t("The second string to compare."))],
88367
90030
  compute: function(string1, string2) {
88368
- return { value: toString(string1) === toString(string2) };
90031
+ return { value: toLocaleString(string1, this.locale) === toLocaleString(string2, this.locale) };
88369
90032
  },
88370
90033
  isExported: true
88371
90034
  };
@@ -88377,8 +90040,8 @@ set(value) {
88377
90040
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
88378
90041
  ],
88379
90042
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
88380
- const _searchFor = toString(searchFor);
88381
- const _textToSearch = toString(textToSearch);
90043
+ const _searchFor = toLocaleString(searchFor, this.locale);
90044
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
88382
90045
  const _startingAt = toNumber(startingAt, this.locale);
88383
90046
  if (_textToSearch === "") return new EvaluationError(_t("The text_to_search must be non-empty."));
88384
90047
  if (_startingAt < 1) return new EvaluationError(_t("The starting_at (%s) must be greater than or equal to 1.", _startingAt));
@@ -88415,8 +90078,8 @@ set(value) {
88415
90078
  description: _t("Concatenates elements of arrays with delimiter."),
88416
90079
  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."))],
88417
90080
  compute: function(delimiter, ...valuesOrArrays) {
88418
- const _delimiter = toString(delimiter);
88419
- return { value: reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), "") };
90081
+ const _delimiter = toLocaleString(delimiter, this.locale);
90082
+ return { value: reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toLocaleString(a, this.locale), "") };
88420
90083
  }
88421
90084
  };
88422
90085
  const LEFT = {
@@ -88425,7 +90088,7 @@ set(value) {
88425
90088
  compute: function(text, ...args) {
88426
90089
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
88427
90090
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
88428
- return { value: toString(text).substring(0, _numberOfCharacters) };
90091
+ return { value: toLocaleString(text, this.locale).substring(0, _numberOfCharacters) };
88429
90092
  },
88430
90093
  isExported: true
88431
90094
  };
@@ -88433,7 +90096,7 @@ set(value) {
88433
90096
  description: _t("Length of a string."),
88434
90097
  args: [arg("text (string)", _t("The string whose length will be returned."))],
88435
90098
  compute: function(text) {
88436
- return { value: toString(text).length };
90099
+ return { value: toLocaleString(text, this.locale).length };
88437
90100
  },
88438
90101
  isExported: true
88439
90102
  };
@@ -88441,7 +90104,7 @@ set(value) {
88441
90104
  description: _t("Converts a specified string to lowercase."),
88442
90105
  args: [arg("text (string)", _t("The string to convert to lowercase."))],
88443
90106
  compute: function(text) {
88444
- return { value: toString(text).toLowerCase() };
90107
+ return { value: toLocaleString(text, this.locale).toLowerCase() };
88445
90108
  },
88446
90109
  isExported: true
88447
90110
  };
@@ -88453,7 +90116,7 @@ set(value) {
88453
90116
  arg("extract_length (number)", _t("The length of the segment to extract."))
88454
90117
  ],
88455
90118
  compute: function(text, starting_at, extract_length) {
88456
- const _text = toString(text);
90119
+ const _text = toLocaleString(text, this.locale);
88457
90120
  const _starting_at = toNumber(starting_at, this.locale);
88458
90121
  const _extract_length = toNumber(extract_length, this.locale);
88459
90122
  if (_starting_at < 1) return new EvaluationError(_t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString()));
@@ -88466,7 +90129,7 @@ set(value) {
88466
90129
  description: _t("Capitalizes each word in a specified string."),
88467
90130
  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."))],
88468
90131
  compute: function(text) {
88469
- return { value: toString(text).replace(wordRegex, (word) => {
90132
+ return { value: toLocaleString(text, this.locale).replace(wordRegex, (word) => {
88470
90133
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
88471
90134
  }) };
88472
90135
  },
@@ -88616,8 +90279,8 @@ set(value) {
88616
90279
  if (_position < 1) return new EvaluationError(_t("The position (%s) must be greater than or equal to 1.", _position));
88617
90280
  const _length = toNumber(length, this.locale);
88618
90281
  if (_length < 0) return new EvaluationError(_t("The length (%s) must be positive or zero.", _length));
88619
- const _text = toString(text);
88620
- const _newText = toString(newText);
90282
+ const _text = toLocaleString(text, this.locale);
90283
+ const _newText = toLocaleString(newText, this.locale);
88621
90284
  return { value: _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length) };
88622
90285
  },
88623
90286
  isExported: true
@@ -88628,7 +90291,7 @@ set(value) {
88628
90291
  compute: function(text, ...args) {
88629
90292
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
88630
90293
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or zero.", _numberOfCharacters));
88631
- const _text = toString(text);
90294
+ const _text = toLocaleString(text, this.locale);
88632
90295
  const stringLength = _text.length;
88633
90296
  return { value: _text.substring(stringLength - _numberOfCharacters, stringLength) };
88634
90297
  },
@@ -88642,8 +90305,8 @@ set(value) {
88642
90305
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
88643
90306
  ],
88644
90307
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
88645
- const _searchFor = toString(searchFor).toLowerCase();
88646
- const _textToSearch = toString(textToSearch).toLowerCase();
90308
+ const _searchFor = toLocaleString(searchFor, this.locale).toLowerCase();
90309
+ const _textToSearch = toLocaleString(textToSearch, this.locale).toLowerCase();
88647
90310
  const _startingAt = toNumber(startingAt, this.locale);
88648
90311
  if (_textToSearch === "") return {
88649
90312
  value: CellErrorType.GenericError,
@@ -88673,8 +90336,8 @@ set(value) {
88673
90336
  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."))
88674
90337
  ],
88675
90338
  computeArray: function(text, delimiter, splitByEach = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, removeEmptyText = { value: SPLIT_DEFAULT_REMOVE_EMPTY_TEXT }) {
88676
- const _text = toString(text);
88677
- const _delimiter = escapeRegExp(toString(delimiter));
90339
+ const _text = toLocaleString(text, this.locale);
90340
+ const _delimiter = escapeRegExp(toLocaleString(delimiter, this.locale));
88678
90341
  const _splitByEach = toBoolean(splitByEach);
88679
90342
  const _removeEmptyText = toBoolean(removeEmptyText);
88680
90343
  if (_delimiter.length <= 0) return new EvaluationError(_t("The delimiter (%s) must be not be empty.", _delimiter));
@@ -88696,10 +90359,10 @@ set(value) {
88696
90359
  compute: function(textToSearch, searchFor, replaceWith, occurrenceNumber) {
88697
90360
  const _occurrenceNumber = toNumber(occurrenceNumber, this.locale);
88698
90361
  if (_occurrenceNumber < 0) return new EvaluationError(_t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber));
88699
- const _textToSearch = toString(textToSearch);
88700
- const _searchFor = toString(searchFor);
90362
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
90363
+ const _searchFor = toLocaleString(searchFor, this.locale);
88701
90364
  if (_searchFor === "") return { value: _textToSearch };
88702
- const _replaceWith = toString(replaceWith);
90365
+ const _replaceWith = toLocaleString(replaceWith, this.locale);
88703
90366
  const reg = new RegExp(escapeRegExp(_searchFor), "g");
88704
90367
  if (_occurrenceNumber === 0) return { value: _textToSearch.replace(reg, _replaceWith) };
88705
90368
  let n = 0;
@@ -88722,10 +90385,10 @@ set(value) {
88722
90385
  arg("texts (string, range<string>, repeating)", _t("Text item to join."))
88723
90386
  ],
88724
90387
  compute: function(delimiter, ignoreEmpty = { value: TEXTJOIN_DEFAULT_IGNORE_EMPTY }, ...textsOrArrays) {
88725
- const _delimiter = toString(delimiter);
90388
+ const _delimiter = toLocaleString(delimiter, this.locale);
88726
90389
  const _ignoreEmpty = toBoolean(ignoreEmpty);
88727
90390
  let n = 0;
88728
- return { value: reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, "") };
90391
+ return { value: reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toLocaleString(a, this.locale) === "") ? (n++ ? acc + _delimiter : "") + toLocaleString(a, this.locale) : acc, "") };
88729
90392
  },
88730
90393
  isExported: true
88731
90394
  };
@@ -88778,7 +90441,7 @@ set(value) {
88778
90441
  description: _t("Removes space characters."),
88779
90442
  args: [arg("text (string)", _t("The text or reference to a cell containing text to be trimmed."))],
88780
90443
  compute: function(text) {
88781
- return { value: trimContent(toString(text)) };
90444
+ return { value: trimContent(toLocaleString(text, this.locale)) };
88782
90445
  },
88783
90446
  isExported: true
88784
90447
  };
@@ -88786,7 +90449,7 @@ set(value) {
88786
90449
  description: _t("Converts a specified string to uppercase."),
88787
90450
  args: [arg("text (string)", _t("The string to convert to uppercase."))],
88788
90451
  compute: function(text) {
88789
- return { value: toString(text).toUpperCase() };
90452
+ return { value: toLocaleString(text, this.locale).toUpperCase() };
88790
90453
  },
88791
90454
  isExported: true
88792
90455
  };
@@ -88883,7 +90546,7 @@ set(value) {
88883
90546
  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."))],
88884
90547
  compute: function(url, linkLabel) {
88885
90548
  const processedUrl = toString(url).trim();
88886
- const processedLabel = toString(linkLabel) || processedUrl;
90549
+ const processedLabel = toLocaleString(linkLabel, this.locale) || processedUrl;
88887
90550
  if (processedUrl === "") return { value: processedLabel };
88888
90551
  return { value: markdownLink(processedLabel, processedUrl) };
88889
90552
  },
@@ -89030,7 +90693,8 @@ set(value) {
89030
90693
  chartJsExtensionRegistry,
89031
90694
  onIterationEndEvaluationRegistry,
89032
90695
  specificRangeTransformRegistry,
89033
- unusedDataSourceRegistry
90696
+ unusedDataSourceRegistry,
90697
+ NonSquishableFunctionRegistry
89034
90698
  };
89035
90699
  const helpers = {
89036
90700
  arg,
@@ -89112,7 +90776,9 @@ set(value) {
89112
90776
  computeCachedTextDimension,
89113
90777
  createComputeFunction,
89114
90778
  isMobileOS,
89115
- drawHighlight
90779
+ drawHighlight,
90780
+ getBoundingRectWithMargins,
90781
+ hasInteractiveElementInEventTree
89116
90782
  };
89117
90783
  const links = {
89118
90784
  isMarkdownLink,
@@ -89344,8 +91010,8 @@ exports.stores = stores;
89344
91010
  exports.tokenColors = tokenColors;
89345
91011
  exports.tokenize = tokenize;
89346
91012
 
89347
- __info__.version = "19.5.0-alpha.10";
89348
- __info__.date = "2026-08-10T12:33:47.263Z";
89349
- __info__.hash = "39a6fbc";
91013
+ __info__.version = "19.5.0-alpha.12";
91014
+ __info__.date = "2026-08-19T09:47:03.774Z";
91015
+ __info__.hash = "68ca68e";
89350
91016
 
89351
91017
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);