@odoo/o-spreadsheet 19.5.0-alpha.11 → 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 (50) hide show
  1. package/dist/material_symbols_outlined.css +9 -0
  2. package/dist/o_spreadsheet.cjs +2313 -656
  3. package/dist/o_spreadsheet.css +169 -33
  4. package/dist/o_spreadsheet.esm.js +2305 -648
  5. package/dist/o_spreadsheet.iife.js +2313 -656
  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/helpers.d.ts +4 -0
  32. package/dist/types/functions/module_operators.d.ts +1 -1
  33. package/dist/types/functions/module_text.d.ts +11 -11
  34. package/dist/types/functions/module_web.d.ts +1 -1
  35. package/dist/types/helpers/carousel_helpers.d.ts +8 -1
  36. package/dist/types/helpers/data_analysis.d.ts +16 -0
  37. package/dist/types/helpers/figures/charts/chart_data_source_helpers.d.ts +10 -0
  38. package/dist/types/helpers/figures/charts/chart_suggestion_engine.d.ts +8 -0
  39. package/dist/types/helpers/figures/charts/chart_ui_common.d.ts +8 -1
  40. package/dist/types/helpers/figures/charts/charts_suggestions_constants.d.ts +37 -0
  41. package/dist/types/helpers/figures/charts/gauge_chart_rendering.d.ts +6 -2
  42. package/dist/types/helpers/text_helper.d.ts +1 -1
  43. package/dist/types/index.d.ts +3 -1
  44. package/dist/types/registries/figures_registry.d.ts +1 -0
  45. package/dist/types/stores/cell_hover_overlay_store.d.ts +15 -0
  46. package/dist/types/stores/chart_drag_store.d.ts +10 -0
  47. package/dist/types/stores/grid_renderer_store.d.ts +3 -2
  48. package/dist/types/types/chart/chart_suggestion.d.ts +136 -0
  49. package/dist/types/types/commands.d.ts +16 -9
  50. 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.11
6
- * @date 2026-08-12T09:17:24.237Z
7
- * @hash bc056d7
5
+ * @version 19.5.0-alpha.12
6
+ * @date 2026-08-19T09:47:03.774Z
7
+ * @hash 68ca68e
8
8
  */
9
9
 
10
10
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
@@ -207,6 +207,16 @@ function getBoundingRectAsPOJO(el) {
207
207
  height: rect.height
208
208
  };
209
209
  }
210
+ function getBoundingRectWithMargins(el) {
211
+ const style = getComputedStyle(el);
212
+ const rect = el.getBoundingClientRect();
213
+ return {
214
+ x: rect.x,
215
+ y: rect.y,
216
+ width: rect.width + parseInt(style.marginLeft || "0") + parseInt(style.marginRight || "0"),
217
+ height: rect.height + parseInt(style.marginTop || "0") + parseInt(style.marginBottom || "0")
218
+ };
219
+ }
210
220
  /**
211
221
  * Iterate over all the children of `el` in the dom tree starting at `el`, depth first.
212
222
  */
@@ -650,6 +660,14 @@ const DEFAULT_CAROUSEL_TITLE_STYLE = {
650
660
  fontSize: 18,
651
661
  color: TEXT_BODY
652
662
  };
663
+ const CAROUSEL_LAYOUT = {
664
+ paddingX: 24,
665
+ paddingY: 8,
666
+ headerPaddingTop: 4,
667
+ separatorWidth: 1,
668
+ headerLineHeight: 1.5,
669
+ minHeaderHeight: 25
670
+ };
653
671
  const DEFAULT_TOKEN_COLOR = "light-dark(#000000, #ffffff)";
654
672
  const functionColor = DEFAULT_TOKEN_COLOR;
655
673
  const operatorColor = "#3da4ab";
@@ -2634,7 +2652,7 @@ function unregisterChartJsExtensions() {
2634
2652
  *
2635
2653
  * The end goal is to eliminate all compatibility shims.
2636
2654
  */
2637
- 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;
2655
+ 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;
2638
2656
  const isOdooCompatLoaded = __ODOO_COMPATIBILITY_LAYER_ADDED__ === true;
2639
2657
  var _Component = class extends OwlComponent {
2640
2658
  static template = "";
@@ -2655,8 +2673,8 @@ function _useComponent() {
2655
2673
  function _useExternalListener(target, eventName, handler, eventParams) {
2656
2674
  const node = useScope();
2657
2675
  const boundHandler = handler.bind(node.component);
2658
- onMounted$24(() => target.addEventListener(eventName, boundHandler, eventParams));
2659
- onWillUnmount$18(() => target.removeEventListener(eventName, boundHandler, eventParams));
2676
+ onMounted$26(() => target.addEventListener(eventName, boundHandler, eventParams));
2677
+ onWillUnmount$20(() => target.removeEventListener(eventName, boundHandler, eventParams));
2660
2678
  }
2661
2679
  function onWillRender(cb) {
2662
2680
  const node = useScope();
@@ -2671,7 +2689,7 @@ function _useLayoutEffect(effect, computeDependencies = () => [NaN]) {
2671
2689
  let cleanup;
2672
2690
  /** @type {any[]} */
2673
2691
  let dependencies;
2674
- onMounted$24(() => {
2692
+ onMounted$26(() => {
2675
2693
  dependencies = computeDependencies();
2676
2694
  cleanup = effect(...dependencies);
2677
2695
  });
@@ -2688,7 +2706,7 @@ function _useLayoutEffect(effect, computeDependencies = () => [NaN]) {
2688
2706
  cleanup = effect(...dependencies);
2689
2707
  }
2690
2708
  });
2691
- onWillUnmount$18(() => cleanup && cleanup());
2709
+ onWillUnmount$20(() => cleanup && cleanup());
2692
2710
  }
2693
2711
  var _EnvPlugin = class extends Plugin {
2694
2712
  static id = "__ENV__";
@@ -2761,7 +2779,7 @@ var VPortal = class extends blockDom$1.text("").constructor {
2761
2779
  }
2762
2780
  };
2763
2781
  var Portal = class extends OwlComponent {
2764
- static template = xml$4`<t t-call-slot="default"/>`;
2782
+ static template = xml$5`<t t-call-slot="default"/>`;
2765
2783
  constructor(node) {
2766
2784
  super(node);
2767
2785
  this.props = props();
@@ -2771,7 +2789,7 @@ var Portal = class extends OwlComponent {
2771
2789
  const node = this.__owl__;
2772
2790
  const renderContent = node.renderFn;
2773
2791
  node.renderFn = (...args) => new VPortal(node.props.selector, renderContent(...args));
2774
- onMounted$24(() => {
2792
+ onMounted$26(() => {
2775
2793
  const portal = node.bdom;
2776
2794
  if (!portal.target) {
2777
2795
  const target = document.querySelector(node.props.selector);
@@ -2779,7 +2797,7 @@ var Portal = class extends OwlComponent {
2779
2797
  else throw new Error("invalid portal target");
2780
2798
  }
2781
2799
  });
2782
- onWillUnmount$18(() => {
2800
+ onWillUnmount$20(() => {
2783
2801
  node.bdom.remove();
2784
2802
  });
2785
2803
  }
@@ -4072,6 +4090,18 @@ function toString(data) {
4072
4090
  default: return "";
4073
4091
  }
4074
4092
  }
4093
+ /**
4094
+ * Only converts the decimal separator, ignore number format such as % or dates
4095
+ */
4096
+ function toLocaleString(data, locale) {
4097
+ const value = toValue(data);
4098
+ switch (typeof value) {
4099
+ case "string": return value;
4100
+ case "number": return value.toString().replace(".", locale.decimalSeparator);
4101
+ case "boolean": return value ? "TRUE" : "FALSE";
4102
+ default: return "";
4103
+ }
4104
+ }
4075
4105
  /** Normalize string by setting it to lowercase and replacing accent letters with plain letters */
4076
4106
  const normalizeString = memoize(function normalizeString(str) {
4077
4107
  return str.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
@@ -9248,8 +9278,8 @@ function computeCachedTextDimension(context, text, font) {
9248
9278
  function fontSizeInPixels(fontSize) {
9249
9279
  return Math.round(fontSize * 96 / 72);
9250
9280
  }
9251
- function computeTextFont(style, fontUnit = "pt") {
9252
- return `${style.italic ? "italic " : ""}${style.bold ? "bold" : "400"} ${(fontUnit === "pt" ? computeTextFontSizeInPixels(style) : style.fontSize) ?? DEFAULT_FONT_SIZE}px ${DEFAULT_FONT}`;
9281
+ function computeTextFont(style, fontUnit = "pt", fontWeight = 400) {
9282
+ return `${style.italic ? "italic " : ""}${style.bold ? "bold" : fontWeight} ${(fontUnit === "pt" ? computeTextFontSizeInPixels(style) : style.fontSize) ?? DEFAULT_FONT_SIZE}px ${DEFAULT_FONT}`;
9253
9283
  }
9254
9284
  function computeTextFontSizeInPixels(style) {
9255
9285
  return fontSizeInPixels(style?.fontSize || DEFAULT_FONT_SIZE);
@@ -11883,7 +11913,7 @@ var ChartJsComponent = class extends Component {
11883
11913
  chartId: types.string(),
11884
11914
  isFullScreen: types.boolean().optional()
11885
11915
  });
11886
- canvas = (0, _odoo_owl.signal)(null);
11916
+ canvas = _odoo_owl.signal.ref(HTMLCanvasElement);
11887
11917
  chart;
11888
11918
  currentRuntime;
11889
11919
  animationStore;
@@ -12606,7 +12636,7 @@ var ScorecardChart = class extends Component {
12606
12636
  chartId: types.string(),
12607
12637
  isFullScreen: types.boolean().optional()
12608
12638
  });
12609
- canvas = (0, _odoo_owl.signal)(null);
12639
+ canvas = _odoo_owl.signal.ref(HTMLCanvasElement);
12610
12640
  zoomStore;
12611
12641
  get runtime() {
12612
12642
  return this.env.model.getters.getChartRuntime(this.props.chartId);
@@ -13991,6 +14021,44 @@ function getCarouselItemTitle(getters, item) {
13991
14021
  const definition = getters.getChartDefinition(item.chartId);
13992
14022
  return (chartSubtypeRegistry.getAll().find((c) => c.matcher?.(definition)) || chartSubtypeRegistry.get(definition.type)).displayName;
13993
14023
  }
14024
+ function getCarouselLayout(figureRect, carousel, selectedItem) {
14025
+ const layout = CAROUSEL_LAYOUT;
14026
+ const title = {
14027
+ ...DEFAULT_CAROUSEL_TITLE_STYLE,
14028
+ ...carousel.title
14029
+ };
14030
+ const x = figureRect.x + layout.paddingX;
14031
+ const width = Math.max(0, figureRect.width - 2 * layout.paddingX);
14032
+ const titleHeight = title.text ? title.fontSize * layout.headerLineHeight : 0;
14033
+ const headerRect = {
14034
+ x,
14035
+ y: figureRect.y + layout.paddingY + layout.headerPaddingTop,
14036
+ width,
14037
+ height: Math.max(layout.minHeaderHeight, titleHeight)
14038
+ };
14039
+ const headerBottom = headerRect.y + headerRect.height;
14040
+ const separatorRect = title.text ? {
14041
+ x,
14042
+ y: headerBottom + layout.paddingY,
14043
+ width,
14044
+ height: layout.separatorWidth
14045
+ } : void 0;
14046
+ const contentPaddingTop = selectedItem?.type === "carouselDataView" ? layout.paddingY : 0;
14047
+ const rectAboveContent = separatorRect || headerRect;
14048
+ const contentY = rectAboveContent.y + rectAboveContent.height + contentPaddingTop;
14049
+ const contentBottom = figureRect.y + figureRect.height - layout.paddingY;
14050
+ return {
14051
+ headerRect,
14052
+ separatorRect,
14053
+ contentRect: {
14054
+ x,
14055
+ y: contentY,
14056
+ width,
14057
+ height: Math.max(0, contentBottom - contentY)
14058
+ },
14059
+ headerLineHeight: layout.headerLineHeight
14060
+ };
14061
+ }
13994
14062
 
13995
14063
  //#endregion
13996
14064
  //#region src/helpers/figures/charts/gauge_chart_rendering.ts
@@ -14002,7 +14070,7 @@ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
14002
14070
  const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
14003
14071
  const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
14004
14072
  const GAUGE_TITLE_SECTION_HEIGHT = 25;
14005
- function drawGaugeChart(canvas, runtime, zoom = 1, dimensions) {
14073
+ function drawGaugeChart(canvas, runtime, zoom = 1, dimensions, options) {
14006
14074
  const size = dimensions ?? getCanvasSize(canvas);
14007
14075
  const dpr = typeof globalThis.devicePixelRatio === "number" ? globalThis.devicePixelRatio : 1;
14008
14076
  canvas.width = size.width * dpr;
@@ -14015,7 +14083,7 @@ function drawGaugeChart(canvas, runtime, zoom = 1, dimensions) {
14015
14083
  height: size.height / zoom,
14016
14084
  x: 0,
14017
14085
  y: 0
14018
- }, runtime, ctx);
14086
+ }, runtime, ctx, options);
14019
14087
  drawBackground(ctx, config);
14020
14088
  drawGauge(ctx, config);
14021
14089
  drawInflectionValues(ctx, config);
@@ -14103,7 +14171,8 @@ function drawTitle(ctx, config) {
14103
14171
  ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
14104
14172
  ctx.restore();
14105
14173
  }
14106
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
14174
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx, options) {
14175
+ const labelFontSize = options?.labelFontSize ?? 12;
14107
14176
  const maxValue = runtime.maxValue;
14108
14177
  const minValue = runtime.minValue;
14109
14178
  const gaugeValue = getGaugeValue(runtime, "animated");
@@ -14121,14 +14190,14 @@ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
14121
14190
  if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
14122
14191
  const minLabelPosition = {
14123
14192
  x: gaugeRect.x + gaugeArcWidth / 2,
14124
- y: gaugeRect.y + gaugeRect.height + 12
14193
+ y: gaugeRect.y + gaugeRect.height + labelFontSize
14125
14194
  };
14126
14195
  const maxLabelPosition = {
14127
14196
  x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
14128
- y: gaugeRect.y + gaugeRect.height + 12
14197
+ y: gaugeRect.y + gaugeRect.height + labelFontSize
14129
14198
  };
14130
14199
  const textColor = chartMutedFontColor(runtime.background);
14131
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
14200
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx, labelFontSize);
14132
14201
  let x = 0, titleWidth = 0, titleHeight = 0;
14133
14202
  if (runtime.title.text) ({width: titleWidth, height: titleHeight} = computeTextDimension(ctx, runtime.title.text, {
14134
14203
  fontSize: 16,
@@ -14176,13 +14245,13 @@ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
14176
14245
  minLabel: {
14177
14246
  label: runtime.minValue.label,
14178
14247
  textPosition: minLabelPosition,
14179
- fontSize: 12,
14248
+ fontSize: labelFontSize,
14180
14249
  color: textColor
14181
14250
  },
14182
14251
  maxLabel: {
14183
14252
  label: runtime.maxValue.label,
14184
14253
  textPosition: maxLabelPosition,
14185
- fontSize: 12,
14254
+ fontSize: labelFontSize,
14186
14255
  color: textColor
14187
14256
  }
14188
14257
  };
@@ -14216,27 +14285,27 @@ function getGaugeRect(boundingRect, title) {
14216
14285
  *
14217
14286
  * Also compute an offset for the text so that it doesn't overlap with other text.
14218
14287
  */
14219
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
14288
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx, labelFontSize = 12) {
14220
14289
  const maxValue = runtime.maxValue;
14221
14290
  const minValue = runtime.minValue;
14222
14291
  const gaugeCircleCenter = {
14223
14292
  x: gaugeRect.x + gaugeRect.width / 2,
14224
14293
  y: gaugeRect.y + gaugeRect.height
14225
14294
  };
14226
- const textStyle = { fontSize: 12 };
14295
+ const textStyle = { fontSize: labelFontSize };
14227
14296
  const inflectionValues = [];
14228
14297
  const inflectionValuesTextRects = [];
14229
14298
  for (const inflectionValue of runtime.inflectionValues) {
14230
14299
  const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
14231
14300
  const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
14232
14301
  const angle = Math.PI - Math.PI * percentage;
14233
- const textRect = getRectangleTangentToCircle(angle, gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, gaugeCircleCenter.x, gaugeCircleCenter.y, labelWidth + 2, 12);
14234
- const offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect)) ? 12 : 0;
14302
+ const textRect = getRectangleTangentToCircle(angle, gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, gaugeCircleCenter.x, gaugeCircleCenter.y, labelWidth + 2, labelFontSize);
14303
+ const offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect)) ? labelFontSize : 0;
14235
14304
  inflectionValuesTextRects.push(textRect);
14236
14305
  inflectionValues.push({
14237
14306
  rotation: angle,
14238
14307
  label: inflectionValue.label,
14239
- fontSize: 12,
14308
+ fontSize: labelFontSize,
14240
14309
  color: textColor,
14241
14310
  offset
14242
14311
  });
@@ -14356,6 +14425,44 @@ const CHART_COMMON_OPTIONS = {
14356
14425
  "mouseup"
14357
14426
  ]
14358
14427
  };
14428
+ /**
14429
+ * Sample down a Chart.js configuration's data points to at most `maxPoints`, evenly spaced.
14430
+ * Used for lightweight previews (drag preview, chart suggestions) where rendering every point
14431
+ * of a large dataset would make Chart.js slow without any visible benefit on a small canvas.
14432
+ */
14433
+ function limitChartConfigDataPoints(config, maxPoints = 150) {
14434
+ const data = config.data;
14435
+ if (!data) return config;
14436
+ return {
14437
+ ...config,
14438
+ data: {
14439
+ ...data,
14440
+ labels: limitArrayDataPoints(data.labels, maxPoints),
14441
+ datasets: data.datasets?.map((dataset) => limitDatasetDataPoints(dataset, maxPoints))
14442
+ }
14443
+ };
14444
+ }
14445
+ function limitDatasetDataPoints(dataset, maxPoints) {
14446
+ const values = dataset.data;
14447
+ if (!Array.isArray(values) || values.length <= maxPoints) return dataset;
14448
+ const length = values.length;
14449
+ const indices = sampleIndices(length, maxPoints);
14450
+ const limited = { ...dataset };
14451
+ for (const key of Object.keys(limited)) {
14452
+ const value = limited[key];
14453
+ if (Array.isArray(value) && value.length === length) limited[key] = indices.map((i) => value[i]);
14454
+ }
14455
+ return limited;
14456
+ }
14457
+ function limitArrayDataPoints(arr, maxPoints) {
14458
+ if (!Array.isArray(arr) || arr.length <= maxPoints) return arr;
14459
+ return sampleIndices(arr.length, maxPoints).map((i) => arr[i]);
14460
+ }
14461
+ /** Evenly spaced indices sampled from [0, length). */
14462
+ function sampleIndices(length, maxPoints) {
14463
+ const step = length / maxPoints;
14464
+ return Array.from({ length: maxPoints }, (_, i) => Math.min(length - 1, Math.floor(i * step)));
14465
+ }
14359
14466
  async function chartToImageUrl(runtime, figure, type) {
14360
14467
  try {
14361
14468
  const canvas = createRenderingSurface(figure.width, figure.height);
@@ -14403,7 +14510,7 @@ async function canvasToObjectUrl(canvas) {
14403
14510
  *
14404
14511
  * @returns a cleanup function to be called after the drawing is no longer needed (to free Chart.js resources)
14405
14512
  */
14406
- function drawChartOnCanvas(canvas, runtime, size, type) {
14513
+ function drawChartOnCanvas(canvas, runtime, size, type, zoom = 1) {
14407
14514
  if ("chartJsConfig" in runtime) {
14408
14515
  if (!globalThis.Chart) throw new Error("Chart.js library is not loaded");
14409
14516
  const extensionsLoaded = areChartJSExtensionsLoaded();
@@ -14419,8 +14526,8 @@ function drawChartOnCanvas(canvas, runtime, size, type) {
14419
14526
  chart.destroy();
14420
14527
  if (!extensionsLoaded) unregisterChartJsExtensions();
14421
14528
  };
14422
- } else if (type === "scorecard") drawScoreChart(getScorecardConfiguration(size, runtime), canvas);
14423
- else if (type === "gauge") drawGaugeChart(canvas, runtime, 1, size);
14529
+ } else if (type === "scorecard") drawScoreChart(getScorecardConfiguration(size, runtime), canvas, zoom);
14530
+ else if (type === "gauge") drawGaugeChart(canvas, runtime, zoom, size);
14424
14531
  return () => {};
14425
14532
  }
14426
14533
 
@@ -14958,7 +15065,7 @@ var ZoomableChartStore = class extends SpreadsheetStore {
14958
15065
  var ZoomableChartJsComponent = class extends ChartJsComponent {
14959
15066
  static template = "o-spreadsheet-ZoomableChartJsComponent";
14960
15067
  store;
14961
- masterChartCanvas = (0, _odoo_owl.signal)(null);
15068
+ masterChartCanvas = _odoo_owl.signal.ref(HTMLCanvasElement);
14962
15069
  masterChart;
14963
15070
  mode;
14964
15071
  hasLinearScale;
@@ -15712,7 +15819,7 @@ var GaugeChartComponent = class extends Component {
15712
15819
  chartId: types.string(),
15713
15820
  isFullScreen: types.boolean().optional()
15714
15821
  });
15715
- canvas = (0, _odoo_owl.signal)(null);
15822
+ canvas = _odoo_owl.signal.ref(HTMLCanvasElement);
15716
15823
  animationStore;
15717
15824
  zoomStore;
15718
15825
  get runtime() {
@@ -15949,7 +16056,7 @@ var Menu = class extends Component {
15949
16056
  onKeyDown: types.function().optional(),
15950
16057
  disableKeyboardNavigation: types.boolean().optional()
15951
16058
  });
15952
- menuRef = (0, _odoo_owl.signal)(null);
16059
+ menuRef = _odoo_owl.signal.ref();
15953
16060
  setup() {
15954
16061
  useLayoutEffect(() => {
15955
16062
  const menuEl = this.menuRef();
@@ -16069,8 +16176,8 @@ var Popover = class extends Component {
16069
16176
  zIndex: types.number().optional(),
16070
16177
  class: types.string().optional()
16071
16178
  });
16072
- popoverRef = (0, _odoo_owl.signal)(null);
16073
- popoverContentRef = (0, _odoo_owl.signal)(null);
16179
+ popoverRef = _odoo_owl.signal.ref();
16180
+ popoverContentRef = _odoo_owl.signal.ref();
16074
16181
  currentPosition = void 0;
16075
16182
  currentDisplayValue = void 0;
16076
16183
  spreadsheetRect = useSpreadsheetRect();
@@ -16258,7 +16365,7 @@ var MenuPopover = class MenuPopover extends Component {
16258
16365
  isHoveringChild: false
16259
16366
  });
16260
16367
  state = (0, _odoo_owl.proxy)({ hoveredMenu: this.props.autoSelectFirstItem ? this.getNextEnabledMenuItem() : void 0 });
16261
- menuRef = (0, _odoo_owl.signal)(null);
16368
+ menuRef = _odoo_owl.signal.ref();
16262
16369
  openingTimeOut = useTimeOut();
16263
16370
  setup() {
16264
16371
  const domFocusableElementStore = useStore(DOMFocusableElementStore);
@@ -16547,43 +16654,29 @@ var PositionMap = class {
16547
16654
  };
16548
16655
 
16549
16656
  //#endregion
16550
- //#region src/components/tables/hovered_table_store.ts
16551
- var HoveredTableStore = class extends SpreadsheetStore {
16552
- mutators = ["clear", "hover"];
16553
- position;
16657
+ //#region src/stores/cell_hover_overlay_store.ts
16658
+ var CellHoverOverlayStore = class extends SpreadsheetStore {
16659
+ mutators = [
16660
+ "hover",
16661
+ "register",
16662
+ "unRegister"
16663
+ ];
16664
+ storeGetters = ["getCellHoverOverlayColor"];
16665
+ providers = [];
16554
16666
  overlayColors = new PositionMap();
16555
- hover(position) {
16556
- if (!this.getters.isDashboard() || deepEquals(this.position, position)) return "noStateChange";
16557
- this.position = position;
16558
- this.computeOverlay();
16667
+ hover(hoveredPosition) {
16668
+ this.overlayColors = new PositionMap();
16669
+ if (!hoveredPosition) return;
16670
+ for (const provider of this.providers) {
16671
+ const positions = provider.getHighlightedPositions(hoveredPosition);
16672
+ for (const position of positions) this.overlayColors.set(position, TABLE_HOVER_BACKGROUND_COLOR);
16673
+ }
16559
16674
  }
16560
- clear() {
16561
- this.position = void 0;
16675
+ register(highlightProvider) {
16676
+ this.providers.push(highlightProvider);
16562
16677
  }
16563
- computeOverlay() {
16564
- this.overlayColors = new PositionMap();
16565
- if (!this.position) return;
16566
- const { sheetId, col, row } = this.position;
16567
- const table = this.getters.getTable({
16568
- sheetId,
16569
- col,
16570
- row
16571
- });
16572
- if (!table) return;
16573
- const { left, right, top } = table.range.zone;
16574
- const isTableHeader = row < top + table.config.numberOfHeaders;
16575
- const doesTableRowHaveContent = range(left, right + 1).some((col) => {
16576
- return !this.getters.isColHidden(sheetId, col) && this.getters.getEvaluatedCell({
16577
- sheetId,
16578
- col,
16579
- row
16580
- }).formattedValue;
16581
- });
16582
- if (!isTableHeader && doesTableRowHaveContent) for (let col = left; col <= right; col++) this.overlayColors.set({
16583
- sheetId,
16584
- col,
16585
- row
16586
- }, TABLE_HOVER_BACKGROUND_COLOR);
16678
+ unRegister(highlightProvider) {
16679
+ this.providers = this.providers.filter((h) => h !== highlightProvider);
16587
16680
  }
16588
16681
  };
16589
16682
 
@@ -16595,9 +16688,9 @@ var ClickableCellSortIcon = class extends Component {
16595
16688
  position: types.CellPosition(),
16596
16689
  sortDirection: types.or([types.SortDirection, types.literal("none")])
16597
16690
  });
16598
- hoveredTableStore;
16691
+ hoveredCellOverlayStore;
16599
16692
  setup() {
16600
- this.hoveredTableStore = useStore(HoveredTableStore);
16693
+ this.hoveredCellOverlayStore = useStore(CellHoverOverlayStore);
16601
16694
  }
16602
16695
  get style() {
16603
16696
  const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.position);
@@ -16617,7 +16710,7 @@ var ClickableCellSortIcon = class extends Component {
16617
16710
  }
16618
16711
  }
16619
16712
  getBackgroundColor(cellStyle) {
16620
- const overlayColor = this.hoveredTableStore.overlayColors.get(this.props.position);
16713
+ const overlayColor = this.hoveredCellOverlayStore.overlayColors.get(this.props.position);
16621
16714
  if (overlayColor) return blendColors(cellStyle.fillColor || "#FFFFFF", overlayColor);
16622
16715
  return cellStyle.fillColor || "#FFFFFF";
16623
16716
  }
@@ -21356,7 +21449,7 @@ var HoveredIconStore = class extends SpreadsheetStore {
21356
21449
  //#region src/components/grid_overlay/grid_overlay.ts
21357
21450
  function useCellHovered(env, gridRef) {
21358
21451
  const delayedHoveredCell = useStore(DelayedHoveredCellStore);
21359
- const hoveredTable = useStore(HoveredTableStore);
21452
+ const cellHoverOverlay = useStore(CellHoverOverlayStore);
21360
21453
  const viewStore = useStore(ViewportsStore);
21361
21454
  const hoveredPosition = {
21362
21455
  col: void 0,
@@ -21400,7 +21493,9 @@ function useCellHovered(env, gridRef) {
21400
21493
  if (isChildEvent(gridRef(), zoomedMouseEvent.ev)) {
21401
21494
  ({x, y} = getOffsetRelativeToOverlay(zoomedMouseEvent));
21402
21495
  lastMoved = Date.now();
21403
- hoveredTable.hover(getPosition());
21496
+ const position = getPosition();
21497
+ if (position.col < 0 || position.row < 0) cellHoverOverlay.hover(void 0);
21498
+ else cellHoverOverlay.hover(getPosition());
21404
21499
  }
21405
21500
  }
21406
21501
  function recompute() {
@@ -21411,7 +21506,7 @@ function useCellHovered(env, gridRef) {
21411
21506
  const zoomedMouseEvent = withZoom(env, e);
21412
21507
  const { x, y } = getOffsetRelativeToOverlay(zoomedMouseEvent);
21413
21508
  const gridRect = getElBoundingRect(gridRef());
21414
- if (y < 0 || y > gridRect.height || x < 0 || x > gridRect.width) return updateMousePosition(zoomedMouseEvent);
21509
+ if (y <= 0 || y >= gridRect.height || x <= 0 || x >= gridRect.width) return updateMousePosition(zoomedMouseEvent);
21415
21510
  else return pause();
21416
21511
  }
21417
21512
  (0, _odoo_owl.useListener)(gridRef, "pointermove", (ev) => !env.isMobile() && updateMousePosition(withZoom(env, ev)));
@@ -21448,7 +21543,7 @@ var GridOverlay = class extends Component {
21448
21543
  gridOverlayDimensions: types.string(),
21449
21544
  hasFooter: types.boolean().optional(() => true)
21450
21545
  });
21451
- gridOverlayRef = (0, _odoo_owl.signal)(null);
21546
+ gridOverlayRef = _odoo_owl.signal.ref();
21452
21547
  cellPopovers;
21453
21548
  paintFormatStore;
21454
21549
  hoveredIconStore;
@@ -21475,7 +21570,7 @@ var GridOverlay = class extends Component {
21475
21570
  return el;
21476
21571
  }
21477
21572
  get style() {
21478
- return this.props.gridOverlayDimensions + cssPropertiesToCss({ cursor: this.hoveredIconStore.hoveredIcon ? "pointer" : "default" });
21573
+ return this.props.gridOverlayDimensions + cssPropertiesToCss({ cursor: this.hoveredIconStore.hoveredIcon ? "pointer" : "inherit" });
21479
21574
  }
21480
21575
  get isPaintingFormat() {
21481
21576
  return this.paintFormatStore.isActive;
@@ -22115,8 +22210,9 @@ var FormulaFingerprintStore = class extends SpreadsheetStore {
22115
22210
  const CELL_ANIMATION_DURATION = 200;
22116
22211
  var GridRenderer = class extends DisposableStore {
22117
22212
  renderer;
22213
+ storeGetters = ["drawLayer"];
22118
22214
  fingerprints;
22119
- hoveredTables;
22215
+ cellHoverOverlayStore = this.get(CellHoverOverlayStore);
22120
22216
  hoveredIcon;
22121
22217
  lastRenderSheetId = void 0;
22122
22218
  lastRenderBoxes = /* @__PURE__ */ new Map();
@@ -22130,15 +22226,14 @@ var GridRenderer = class extends DisposableStore {
22130
22226
  const model = get(ModelStore);
22131
22227
  this.getters = model.getters;
22132
22228
  this.fingerprints = get(FormulaFingerprintStore);
22133
- this.hoveredTables = get(HoveredTableStore);
22134
22229
  this.hoveredIcon = get(HoveredIconStore);
22135
22230
  model.on("command-dispatched", this, this.handle);
22136
22231
  model.on("command-finalized", this, this.finalize);
22137
- this.renderer.register(this);
22232
+ if (this.renderer) this.renderer.register(this);
22138
22233
  this.onDispose(() => {
22139
22234
  model.off("command-dispatched", this);
22140
22235
  model.off("command-finalized", this);
22141
- this.renderer.unRegister(this);
22236
+ if (this.renderer) this.renderer.unRegister(this);
22142
22237
  });
22143
22238
  }
22144
22239
  handle(cmd) {
@@ -22682,7 +22777,7 @@ var GridRenderer = class extends DisposableStore {
22682
22777
  border: this.getters.getCellComputedBorder(position) || void 0,
22683
22778
  style,
22684
22779
  dataBarFill,
22685
- overlayColor: this.hoveredTables.overlayColors.get(position),
22780
+ overlayColor: this.cellHoverOverlayStore.overlayColors.get(position),
22686
22781
  isError: cell.type === "error" && !!cell.message || this.getters.isDataValidationInvalid(position),
22687
22782
  icons: cellIcons,
22688
22783
  disabledAnimation: this.zonesWithPreventedAnimationsInNextFrame[sheetId]?.some((z) => isZoneInside(zone, z) || overlap(zone, z))
@@ -22858,6 +22953,7 @@ var GridRenderer = class extends DisposableStore {
22858
22953
  return boxes;
22859
22954
  }
22860
22955
  getBoxesWithAnimations(boxes, oldBoxes, timeStamp) {
22956
+ if (!this.renderer) return boxes;
22861
22957
  this.updateAnimationsProgress(timeStamp);
22862
22958
  this.addNewAnimations(boxes, oldBoxes, timeStamp);
22863
22959
  if (this.animations.size > 0) {
@@ -23002,7 +23098,7 @@ var ScrollBar = class extends Component {
23002
23098
  offset: types.Pixel(),
23003
23099
  onScroll: types.function()
23004
23100
  });
23005
- scrollbarRef = (0, _odoo_owl.signal)(null);
23101
+ scrollbarRef = _odoo_owl.signal.ref();
23006
23102
  scrollbar;
23007
23103
  setup() {
23008
23104
  this.scrollbar = new ScrollBar$1(this.scrollbarRef(), this.props.direction);
@@ -23104,6 +23200,9 @@ var StandaloneViewportStore = class extends SpreadsheetStore {
23104
23200
  zoneToDisplay: this.range.zone,
23105
23201
  getFooterSize: () => 0
23106
23202
  });
23203
+ const cellHoverOverlayStore = this.get(CellHoverOverlayStore);
23204
+ cellHoverOverlayStore.register(this);
23205
+ this.onDispose(() => cellHoverOverlayStore.unRegister(this));
23107
23206
  }
23108
23207
  handle(cmd) {
23109
23208
  this.cachedColDimensions = void 0;
@@ -23252,6 +23351,16 @@ var StandaloneViewportStore = class extends SpreadsheetStore {
23252
23351
  }
23253
23352
  return normalizedWeights;
23254
23353
  }
23354
+ getHighlightedPositions(position) {
23355
+ const highlightedPositions = [];
23356
+ const { col, row, sheetId } = position;
23357
+ if (sheetId !== this.range.sheetId || !isInside(col, row, this.range.zone)) return highlightedPositions;
23358
+ for (let col = this.range.zone.left; col <= this.range.zone.right; col++) highlightedPositions.push({
23359
+ ...position,
23360
+ col
23361
+ });
23362
+ return highlightedPositions;
23363
+ }
23255
23364
  };
23256
23365
 
23257
23366
  //#endregion
@@ -23268,10 +23377,11 @@ var StandaloneViewport = class extends Component {
23268
23377
  range: types.Range(),
23269
23378
  canResizeColumns: types.boolean().optional(true),
23270
23379
  onResizeColumns: types.function().optional(),
23271
- columnWeights: types.array().optional()
23380
+ columnWeights: types.array().optional(),
23381
+ size: types.object()
23272
23382
  });
23273
- canvasRef = (0, _odoo_owl.signal)(null);
23274
- containerRef = (0, _odoo_owl.signal)(null);
23383
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
23384
+ containerRef = _odoo_owl.signal.ref();
23275
23385
  store;
23276
23386
  dndState = (0, _odoo_owl.proxy)({ col: void 0 });
23277
23387
  onMouseWheel;
@@ -23283,8 +23393,8 @@ var StandaloneViewport = class extends Component {
23283
23393
  useChildStoreProvider([
23284
23394
  ViewportsStore,
23285
23395
  HoveredIconStore,
23286
- HoveredTableStore,
23287
23396
  ClickableCellsStore,
23397
+ CellHoverOverlayStore,
23288
23398
  DelayedHoveredCellStore,
23289
23399
  CellPopoverStore
23290
23400
  ]);
@@ -23293,18 +23403,10 @@ var StandaloneViewport = class extends Component {
23293
23403
  this.zoomStore = useStore(ZoomStore);
23294
23404
  this.cellPopoverStore = useStore(CellPopoverStore);
23295
23405
  this.rendererStore = useLocalStore(RendererStore, ["Background", "Chart"]);
23296
- const resizeObserver = new ResizeObserver(() => {
23297
- this.store.setContainerSize(this.containerWidth, this.containerHeight);
23298
- });
23299
- (0, _odoo_owl.useEffect)(() => {
23300
- const el = this.containerRef();
23301
- if (el) resizeObserver.observe(el);
23302
- return () => resizeObserver.disconnect();
23303
- });
23304
23406
  (0, _odoo_owl.useEffect)(() => {
23305
23407
  this.store.setRange(this.props.range);
23306
23408
  if (this.dndState.col === void 0) this.store.setCustomColWeights(this.props.columnWeights);
23307
- this.store.setContainerSize(this.containerWidth, this.containerHeight);
23409
+ this.store.setContainerSize(this.contentWidth, this.props.size.height);
23308
23410
  });
23309
23411
  useGridDrawing({
23310
23412
  canvasRef: this.canvasRef,
@@ -23323,14 +23425,11 @@ var StandaloneViewport = class extends Component {
23323
23425
  }
23324
23426
  });
23325
23427
  }
23326
- get containerWidth() {
23327
- return Math.floor(getElBoundingRect(this.containerRef()).width / this.zoomStore.zoomLevel);
23328
- }
23329
- get containerHeight() {
23330
- return Math.floor(getElBoundingRect(this.containerRef()).height / this.zoomStore.zoomLevel);
23428
+ get contentWidth() {
23429
+ return this.hasVerticalScrollBar ? this.props.size.width - this.zoomStore.scrollBarWidth : this.props.size.width;
23331
23430
  }
23332
23431
  get hasVerticalScrollBar() {
23333
- return this.containerHeight && this.viewStore.mainViewportCoordinates.y + this.viewStore.mainViewportRect.height > this.containerHeight;
23432
+ return this.viewStore.mainViewportCoordinates.y + this.viewStore.mainViewportRect.height > this.props.size.height;
23334
23433
  }
23335
23434
  get scrollBarContainerStyle() {
23336
23435
  return cssPropertiesToCss({ width: `${this.zoomStore.scrollBarWidth}px` });
@@ -23382,7 +23481,7 @@ var StandaloneViewport = class extends Component {
23382
23481
  };
23383
23482
  const onMouseMove = (ev) => {
23384
23483
  deltaX = withZoom(this.env, ev).clientX - initialX;
23385
- const weightDelta = deltaX / this.containerWidth * totalWeight;
23484
+ const weightDelta = deltaX / this.props.size.width * totalWeight;
23386
23485
  this.store.resizeColumn(resizer.col, weightDelta, startingColWeights);
23387
23486
  };
23388
23487
  startDnd(onMouseMove, onMouseUp);
@@ -23412,8 +23511,8 @@ var Select = class extends Component {
23412
23511
  name: types.string().optional(),
23413
23512
  title: types.string().optional()
23414
23513
  });
23415
- selectRef = (0, _odoo_owl.signal)(null);
23416
- dropdownRef = (0, _odoo_owl.signal)(null);
23514
+ selectRef = _odoo_owl.signal.ref();
23515
+ dropdownRef = _odoo_owl.signal.ref();
23417
23516
  state = (0, _odoo_owl.proxy)({
23418
23517
  isPopoverOpen: false,
23419
23518
  hoveredValue: void 0
@@ -23523,7 +23622,7 @@ var InfoPopover = class extends Component {
23523
23622
  setup() {
23524
23623
  useExternalListener(window, "click", this.onExternalClick, { capture: true });
23525
23624
  }
23526
- infoRef = (0, _odoo_owl.signal)(null);
23625
+ infoRef = _odoo_owl.signal.ref();
23527
23626
  get popoverProps() {
23528
23627
  return {
23529
23628
  anchorRect: this.props.anchorRect,
@@ -23668,8 +23767,8 @@ var CarouselFigure = class extends Component {
23668
23767
  isFullScreen: types.boolean().optional(),
23669
23768
  openContextMenu: types.function().optional()
23670
23769
  });
23671
- carouselTabsRef = (0, _odoo_owl.signal)(null);
23672
- carouselTabsDropdownRef = (0, _odoo_owl.signal)(null);
23770
+ carouselTabsRef = _odoo_owl.signal.ref();
23771
+ carouselTabsDropdownRef = _odoo_owl.signal.ref();
23673
23772
  menuState = (0, _odoo_owl.proxy)({
23674
23773
  isOpen: false,
23675
23774
  anchorRect: null,
@@ -23737,6 +23836,19 @@ var CarouselFigure = class extends Component {
23737
23836
  } else cssProperties["background-color"] = backgroundColor;
23738
23837
  return cssPropertiesToCss(cssProperties);
23739
23838
  }
23839
+ get contentStyle() {
23840
+ return cssPropertiesToCss(this.rectToCss(this.carouselLayout.contentRect));
23841
+ }
23842
+ get headerStyle() {
23843
+ return cssPropertiesToCss({
23844
+ ...this.rectToCss(this.carouselLayout.headerRect),
23845
+ "line-height": String(this.carouselLayout.headerLineHeight)
23846
+ });
23847
+ }
23848
+ get separatorStyle() {
23849
+ const separatorRect = this.carouselLayout.separatorRect;
23850
+ return separatorRect ? cssPropertiesToCss(this.rectToCss(separatorRect)) : "";
23851
+ }
23740
23852
  get title() {
23741
23853
  return this.env.model.getters.dynamicTranslate(this.carousel.title?.text ?? "");
23742
23854
  }
@@ -23746,6 +23858,14 @@ var CarouselFigure = class extends Component {
23746
23858
  ...this.carousel.title
23747
23859
  })));
23748
23860
  }
23861
+ rectToCss(rect) {
23862
+ return {
23863
+ top: `${rect.y}px`,
23864
+ left: `${rect.x}px`,
23865
+ width: `${rect.width}px`,
23866
+ height: `${rect.height}px`
23867
+ };
23868
+ }
23749
23869
  updateTabsVisibility() {
23750
23870
  const tabsContainerEl = this.carouselTabsRef();
23751
23871
  const dropDownEl = this.carouselTabsDropdownRef();
@@ -23837,6 +23957,13 @@ var CarouselFigure = class extends Component {
23837
23957
  get canResizeDataViewColumns() {
23838
23958
  return !this.env.model.getters.isReadonly() && !this.env.model.getters.isSheetLocked(this.env.model.getters.getActiveSheetId());
23839
23959
  }
23960
+ get carouselLayout() {
23961
+ return getCarouselLayout({
23962
+ ...this.props.figureUI,
23963
+ x: 0,
23964
+ y: 0
23965
+ }, this.carousel, this.selectedCarouselItem);
23966
+ }
23840
23967
  };
23841
23968
 
23842
23969
  //#endregion
@@ -23877,7 +24004,7 @@ var ImageFigure = class extends Component {
23877
24004
  figureUI: types.FigureUI(),
23878
24005
  openContextMenu: types.function()
23879
24006
  });
23880
- menuButtonRef = (0, _odoo_owl.signal)(null);
24007
+ menuButtonRef = _odoo_owl.signal.ref();
23881
24008
  showMenu(ev) {
23882
24009
  if (!this.env.model.getters.getSelectedFigureIds().includes(this.props.figureUI.id)) this.env.model.dispatch("SELECT_FIGURE", {
23883
24010
  figureId: this.props.figureUI.id,
@@ -23905,7 +24032,8 @@ figureRegistry.add("chart", {
23905
24032
  menuBuilder: getChartMenuActions,
23906
24033
  borderWidth: (getters) => getters.isDashboard() ? 0 : 1,
23907
24034
  hasShadow: (getters) => getters.isDashboard(),
23908
- isRounded: (getters) => getters.isDashboard()
24035
+ isRounded: (getters) => getters.isDashboard(),
24036
+ isThemeDependant: true
23909
24037
  });
23910
24038
  figureRegistry.add("image", {
23911
24039
  Component: ImageFigure,
@@ -23914,14 +24042,16 @@ figureRegistry.add("image", {
23914
24042
  borderWidth: () => 0,
23915
24043
  hasShadow: () => false,
23916
24044
  isRounded: () => false,
23917
- menuBuilder: getImageMenuActions
24045
+ menuBuilder: getImageMenuActions,
24046
+ isThemeDependant: false
23918
24047
  });
23919
24048
  figureRegistry.add("carousel", {
23920
24049
  Component: CarouselFigure,
23921
24050
  menuBuilder: getCarouselMenuActions,
23922
24051
  borderWidth: (getters) => getters.isDashboard() ? 0 : 1,
23923
24052
  hasShadow: (getters) => getters.isDashboard(),
23924
- isRounded: (getters) => getters.isDashboard()
24053
+ isRounded: (getters) => getters.isDashboard(),
24054
+ isThemeDependant: true
23925
24055
  });
23926
24056
 
23927
24057
  //#endregion
@@ -23943,10 +24073,13 @@ var FigureComponent = class extends Component {
23943
24073
  anchorRect: null,
23944
24074
  menuItems: []
23945
24075
  });
23946
- figureRef = (0, _odoo_owl.signal)(null);
24076
+ figureRef = _odoo_owl.signal.ref();
23947
24077
  get isSelected() {
23948
24078
  return !this.env.model.getters.isDashboard() && this.env.model.getters.getSelectedFigureIds().includes(this.props.figureUI.id);
23949
24079
  }
24080
+ get isThemeDependant() {
24081
+ return figureRegistry.get(this.props.figureUI.tag).isThemeDependant;
24082
+ }
23950
24083
  get figureRegistry() {
23951
24084
  return figureRegistry;
23952
24085
  }
@@ -29545,6 +29678,35 @@ async function interactivePasteFromOS(env, target, parsedClipboardContent, paste
29545
29678
  }));
29546
29679
  }
29547
29680
 
29681
+ //#endregion
29682
+ //#region src/helpers/figures/charts/chart_data_source_helpers.ts
29683
+ function getUnboundRange(getters, zone) {
29684
+ return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
29685
+ }
29686
+ function isDatasetTitled(getters, zone) {
29687
+ const sheetId = getters.getActiveSheetId();
29688
+ const cell = getters.getEvaluatedCell({
29689
+ sheetId,
29690
+ col: zone.left,
29691
+ row: zone.top
29692
+ });
29693
+ return !["number", "empty"].includes(cell.type);
29694
+ }
29695
+ function dataset(zone, getters, id = "0") {
29696
+ return {
29697
+ dataRange: getUnboundRange(getters, zone),
29698
+ dataSetId: id
29699
+ };
29700
+ }
29701
+ function rangeSource(dataSets, dataSetsHaveTitle, labelRange) {
29702
+ return {
29703
+ type: "range",
29704
+ dataSets,
29705
+ dataSetsHaveTitle,
29706
+ labelRange
29707
+ };
29708
+ }
29709
+
29548
29710
  //#endregion
29549
29711
  //#region src/helpers/figures/charts/smart_chart_engine.ts
29550
29712
  const DEFAULT_BAR_CHART_CONFIG = {
@@ -29575,9 +29737,6 @@ const DEFAULT_LINE_CHART_CONFIG = {
29575
29737
  labelsAsText: false,
29576
29738
  humanize: true
29577
29739
  };
29578
- function getUnboundRange(getters, zone) {
29579
- return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
29580
- }
29581
29740
  function detectColumnType(cells) {
29582
29741
  if (!cells.length) return "empty";
29583
29742
  const counts = {
@@ -29624,14 +29783,6 @@ function getCellStats(getters, zone) {
29624
29783
  totalCount: values.length
29625
29784
  };
29626
29785
  }
29627
- function isDatasetTitled(getters, column) {
29628
- const titleCell = getters.getEvaluatedCell({
29629
- sheetId: getters.getActiveSheetId(),
29630
- col: column.zone.left,
29631
- row: column.zone.top
29632
- });
29633
- return !["number", "empty"].includes(titleCell.type);
29634
- }
29635
29786
  /**
29636
29787
  * Builds a chart definition for a single column selection. The logic to detect the chart type is as follows:
29637
29788
  * - If the column contains a single cell, create a scorecard.
@@ -29643,8 +29794,7 @@ function isDatasetTitled(getters, column) {
29643
29794
  function buildSingleColumnChart(column, getters) {
29644
29795
  const { type, zone } = column;
29645
29796
  const sheetId = getters.getActiveSheetId();
29646
- const dataSetsHaveTitle = isDatasetTitled(getters, column);
29647
- const dataRange = getUnboundRange(getters, zone);
29797
+ const dataSetsHaveTitle = isDatasetTitled(getters, column.zone);
29648
29798
  const titleCell = getters.getEvaluatedCell({
29649
29799
  sheetId,
29650
29800
  col: zone.left,
@@ -29655,14 +29805,7 @@ function buildSingleColumnChart(column, getters) {
29655
29805
  case "percentage": return {
29656
29806
  type: "pie",
29657
29807
  title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29658
- dataSource: {
29659
- type: "range",
29660
- dataSets: [{
29661
- dataRange,
29662
- dataSetId: "0"
29663
- }],
29664
- dataSetsHaveTitle
29665
- },
29808
+ dataSource: rangeSource([dataset(zone, getters)], dataSetsHaveTitle),
29666
29809
  dataSetStyles: {},
29667
29810
  legendPosition: "none"
29668
29811
  };
@@ -29673,45 +29816,28 @@ function buildSingleColumnChart(column, getters) {
29673
29816
  return {
29674
29817
  type: "pie",
29675
29818
  title: hasUniqueTitle ? { text: String(titleCell.value) } : {},
29676
- dataSource: {
29677
- type: "range",
29678
- dataSets: [{
29679
- dataRange,
29680
- dataSetId: "0"
29681
- }],
29682
- labelRange: dataRange,
29683
- dataSetsHaveTitle: hasUniqueTitle
29684
- },
29819
+ dataSource: rangeSource([dataset(zone, getters)], hasUniqueTitle, getUnboundRange(getters, zone)),
29685
29820
  dataSetStyles: {},
29686
29821
  aggregated: true,
29687
29822
  legendPosition: "top"
29688
29823
  };
29689
- case "date": return {
29690
- type: "calendar",
29691
- title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29692
- dataSource: {
29693
- type: "range",
29694
- dataSets: [],
29695
- dataSetsHaveTitle,
29696
- labelRange: dataRange
29697
- },
29698
- dataSetStyles: {},
29699
- legendPosition: "left",
29700
- horizontalGroupBy: "day_of_week",
29701
- verticalGroupBy: "month_number"
29702
- };
29824
+ case "date": {
29825
+ const dataRange = getUnboundRange(getters, zone);
29826
+ return {
29827
+ type: "calendar",
29828
+ title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29829
+ dataSource: rangeSource([], dataSetsHaveTitle, dataRange),
29830
+ dataSetStyles: {},
29831
+ legendPosition: "left",
29832
+ horizontalGroupBy: "day_of_week",
29833
+ verticalGroupBy: "month_number"
29834
+ };
29835
+ }
29703
29836
  }
29704
29837
  return {
29705
29838
  ...DEFAULT_BAR_CHART_CONFIG,
29706
29839
  title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29707
- dataSource: {
29708
- type: "range",
29709
- dataSets: [{
29710
- dataRange,
29711
- dataSetId: "0"
29712
- }],
29713
- dataSetsHaveTitle
29714
- },
29840
+ dataSource: rangeSource([dataset(zone, getters)], dataSetsHaveTitle),
29715
29841
  dataSetStyles: {}
29716
29842
  };
29717
29843
  }
@@ -29729,15 +29855,7 @@ function buildTwoColumnChart(columns, getters) {
29729
29855
  if (columns[1].type === "percentage") return {
29730
29856
  type: "pie",
29731
29857
  title: {},
29732
- dataSource: {
29733
- type: "range",
29734
- dataSets: [{
29735
- dataRange: getUnboundRange(getters, columns[1].zone),
29736
- dataSetId: "0"
29737
- }],
29738
- labelRange: getUnboundRange(getters, columns[0].zone),
29739
- dataSetsHaveTitle: isDatasetTitled(getters, columns[1])
29740
- },
29858
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[1].zone), getUnboundRange(getters, columns[0].zone)),
29741
29859
  dataSetStyles: {},
29742
29860
  aggregated: true,
29743
29861
  legendPosition: "none"
@@ -29745,15 +29863,7 @@ function buildTwoColumnChart(columns, getters) {
29745
29863
  if (columns[0].type === "number" && columns[1].type === "number") return {
29746
29864
  type: "scatter",
29747
29865
  title: {},
29748
- dataSource: {
29749
- type: "range",
29750
- dataSets: [{
29751
- dataRange: getUnboundRange(getters, columns[1].zone),
29752
- dataSetId: "0"
29753
- }],
29754
- labelRange: getUnboundRange(getters, columns[0].zone),
29755
- dataSetsHaveTitle: isDatasetTitled(getters, columns[1])
29756
- },
29866
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[1].zone), getUnboundRange(getters, columns[0].zone)),
29757
29867
  dataSetStyles: {},
29758
29868
  labelsAsText: false,
29759
29869
  legendPosition: "none"
@@ -29761,49 +29871,25 @@ function buildTwoColumnChart(columns, getters) {
29761
29871
  if (columns[0].type === "date" && columns[1].type === "number") return {
29762
29872
  ...DEFAULT_LINE_CHART_CONFIG,
29763
29873
  type: "line",
29764
- dataSource: {
29765
- type: "range",
29766
- dataSets: [{
29767
- dataRange: getUnboundRange(getters, columns[1].zone),
29768
- dataSetId: "0"
29769
- }],
29770
- labelRange: getUnboundRange(getters, columns[0].zone),
29771
- dataSetsHaveTitle: isDatasetTitled(getters, columns[0])
29772
- },
29874
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[0].zone), getUnboundRange(getters, columns[0].zone)),
29773
29875
  dataSetStyles: {}
29774
29876
  };
29775
29877
  if (columns[0].type === "text" && columns[1].type === "number") {
29776
29878
  const textColumn = columns[0];
29777
29879
  const numberColumn = columns[1];
29778
29880
  const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
29779
- const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn);
29881
+ const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn.zone);
29780
29882
  if (uniqueCount !== totalCount) return {
29781
29883
  type: "treemap",
29782
29884
  title: {},
29783
- dataSource: {
29784
- type: "range",
29785
- dataSets: [{
29786
- dataRange: getUnboundRange(getters, textColumn.zone),
29787
- dataSetId: "0"
29788
- }],
29789
- labelRange: getUnboundRange(getters, numberColumn.zone),
29790
- dataSetsHaveTitle
29791
- },
29885
+ dataSource: rangeSource([dataset(textColumn.zone, getters)], dataSetsHaveTitle, getUnboundRange(getters, numberColumn.zone)),
29792
29886
  dataSetStyles: {},
29793
29887
  legendPosition: "none"
29794
29888
  };
29795
29889
  }
29796
29890
  return {
29797
29891
  ...DEFAULT_BAR_CHART_CONFIG,
29798
- dataSource: {
29799
- type: "range",
29800
- dataSets: [{
29801
- dataRange: getUnboundRange(getters, columns[1].zone),
29802
- dataSetId: "0"
29803
- }],
29804
- labelRange: getUnboundRange(getters, columns[0].zone),
29805
- dataSetsHaveTitle: isDatasetTitled(getters, columns[1])
29806
- },
29892
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[1].zone), getUnboundRange(getters, columns[0].zone)),
29807
29893
  dataSetStyles: {}
29808
29894
  };
29809
29895
  }
@@ -29817,42 +29903,26 @@ function buildTwoColumnChart(columns, getters) {
29817
29903
  */
29818
29904
  function buildMultiColumnChart(columns, getters) {
29819
29905
  if (columns.length < 3) throw new Error("buildMultiColumnChart expects at least three columns");
29820
- const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col));
29906
+ const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col.zone));
29821
29907
  const lastColumn = columns[columns.length - 1];
29822
29908
  const columnsExceptLast = columns.slice(0, columns.length - 1);
29823
29909
  if ((lastColumn.type === "percentage" || lastColumn.type === "number") && columnsExceptLast.every((col) => col.type === "text")) {
29824
- const dataSets = columnsExceptLast.map(({ zone }, i) => ({
29825
- dataRange: getUnboundRange(getters, zone),
29826
- dataSetId: i.toString()
29827
- }));
29910
+ const dataSets = columnsExceptLast.map(({ zone }, i) => dataset(zone, getters, String(i)));
29828
29911
  return {
29829
29912
  type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap",
29830
29913
  title: {},
29831
- dataSource: {
29832
- type: "range",
29833
- dataSets,
29834
- dataSetsHaveTitle,
29835
- labelRange: getUnboundRange(getters, lastColumn.zone)
29836
- },
29914
+ dataSource: rangeSource(dataSets, dataSetsHaveTitle, getUnboundRange(getters, lastColumn.zone)),
29837
29915
  dataSetStyles: {},
29838
29916
  legendPosition: "none"
29839
29917
  };
29840
29918
  }
29841
29919
  const firstColumn = columns[0];
29842
29920
  const columnsExceptFirst = columns.slice(1);
29843
- const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }, i) => ({
29844
- dataRange: getUnboundRange(getters, zone),
29845
- dataSetId: i.toString()
29846
- }));
29921
+ const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }, i) => dataset(zone, getters, String(i)));
29847
29922
  if (columnsExceptFirst.every((col) => col.type === "percentage")) return {
29848
29923
  type: "pie",
29849
29924
  title: {},
29850
- dataSource: {
29851
- type: "range",
29852
- dataSets: rangesOfColumnsExceptFirst,
29853
- labelRange: getUnboundRange(getters, firstColumn.zone),
29854
- dataSetsHaveTitle
29855
- },
29925
+ dataSource: rangeSource(rangesOfColumnsExceptFirst, dataSetsHaveTitle, getUnboundRange(getters, firstColumn.zone)),
29856
29926
  dataSetStyles: {},
29857
29927
  aggregated: false,
29858
29928
  legendPosition: "top"
@@ -29860,23 +29930,13 @@ function buildMultiColumnChart(columns, getters) {
29860
29930
  if (firstColumn.type === "date" && columnsExceptFirst.every((col) => col.type === "number")) return {
29861
29931
  ...DEFAULT_LINE_CHART_CONFIG,
29862
29932
  type: "line",
29863
- dataSource: {
29864
- type: "range",
29865
- dataSets: rangesOfColumnsExceptFirst,
29866
- labelRange: getUnboundRange(getters, firstColumn.zone),
29867
- dataSetsHaveTitle
29868
- },
29933
+ dataSource: rangeSource(rangesOfColumnsExceptFirst, dataSetsHaveTitle, getUnboundRange(getters, firstColumn.zone)),
29869
29934
  dataSetStyles: {},
29870
29935
  legendPosition: "top"
29871
29936
  };
29872
29937
  return {
29873
29938
  ...DEFAULT_BAR_CHART_CONFIG,
29874
- dataSource: {
29875
- type: "range",
29876
- dataSets: rangesOfColumnsExceptFirst,
29877
- labelRange: getUnboundRange(getters, firstColumn.zone),
29878
- dataSetsHaveTitle
29879
- },
29939
+ dataSource: rangeSource(rangesOfColumnsExceptFirst, dataSetsHaveTitle, getUnboundRange(getters, firstColumn.zone)),
29880
29940
  dataSetStyles: {},
29881
29941
  legendPosition: "top"
29882
29942
  };
@@ -29903,17 +29963,10 @@ function buildScorecard(zone, getters) {
29903
29963
  function getSmartChartDefinition(zones, getters) {
29904
29964
  const columns = categorizeColumns(zones, getters);
29905
29965
  if (columns.length === 0 || columns.every((col) => col.type === "empty")) {
29906
- const dataSets = columns.map(({ zone }, i) => ({
29907
- dataRange: getUnboundRange(getters, zone),
29908
- dataSetId: i.toString()
29909
- }));
29966
+ const dataSets = columns.map(({ zone }, i) => dataset(zone, getters, String(i)));
29910
29967
  return {
29911
29968
  ...DEFAULT_BAR_CHART_CONFIG,
29912
- dataSource: {
29913
- type: "range",
29914
- dataSets,
29915
- dataSetsHaveTitle: false
29916
- }
29969
+ dataSource: rangeSource(dataSets, false)
29917
29970
  };
29918
29971
  }
29919
29972
  const nonEmptyColumns = columns.filter((col) => col.type !== "empty");
@@ -31601,7 +31654,7 @@ var SelectionInput = class extends Component {
31601
31654
  dragAndDrop = useDragAndDropListItems();
31602
31655
  focusedInputRef = _odoo_owl.signal.ref(HTMLInputElement);
31603
31656
  unfocusedInputRef = _odoo_owl.signal.ref(HTMLInputElement);
31604
- selectionRef = (0, _odoo_owl.signal)(null);
31657
+ selectionRef = _odoo_owl.signal.ref();
31605
31658
  DOMFocusableElementStore;
31606
31659
  store;
31607
31660
  get ranges() {
@@ -32108,7 +32161,7 @@ var ColorPickerWidget = class extends Component {
32108
32161
  dropdownMaxHeight: types.Pixel().optional(),
32109
32162
  class: types.string().optional()
32110
32163
  });
32111
- colorPickerButtonRef = (0, _odoo_owl.signal)(null);
32164
+ colorPickerButtonRef = _odoo_owl.signal.ref();
32112
32165
  get iconStyle() {
32113
32166
  return this.props.currentColor ? `border-color: ${this.props.currentColor}` : "border-bottom-style: hidden";
32114
32167
  }
@@ -32136,8 +32189,8 @@ var NumberEditor = class extends Component {
32136
32189
  });
32137
32190
  dropdown = (0, _odoo_owl.proxy)({ isOpen: false });
32138
32191
  inputRef = _odoo_owl.signal.ref(HTMLInputElement);
32139
- rootEditorRef = (0, _odoo_owl.signal)(null);
32140
- valueListRef = (0, _odoo_owl.signal)(null);
32192
+ rootEditorRef = _odoo_owl.signal.ref();
32193
+ valueListRef = _odoo_owl.signal.ref();
32141
32194
  DOMFocusableElementStore;
32142
32195
  setup() {
32143
32196
  this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
@@ -32443,7 +32496,7 @@ var ChartTypePickerPopover = class extends Component {
32443
32496
  });
32444
32497
  categories = chartCategories;
32445
32498
  chartTypeByCategories = {};
32446
- popoverRef = (0, _odoo_owl.signal)(null);
32499
+ popoverRef = _odoo_owl.signal.ref();
32447
32500
  setup() {
32448
32501
  (0, _odoo_owl.useListener)(window, "pointerdown", this.onExternalClick.bind(this), { capture: true });
32449
32502
  for (const subtypeProperties of chartSubtypeRegistry.getAll()) {
@@ -32468,7 +32521,7 @@ var CogWheelMenu = class extends Component {
32468
32521
  static template = "o-spreadsheet-CogWheelMenu";
32469
32522
  static components = { MenuPopover };
32470
32523
  props = (0, _odoo_owl.useProps)({ items: types.array(types.ActionSpec()) });
32471
- buttonRef = (0, _odoo_owl.signal)(null);
32524
+ buttonRef = _odoo_owl.signal.ref();
32472
32525
  menuState = (0, _odoo_owl.proxy)({
32473
32526
  isOpen: false,
32474
32527
  anchorRect: null,
@@ -32512,8 +32565,8 @@ var CarouselPanel = class extends Component {
32512
32565
  });
32513
32566
  DEFAULT_CAROUSEL_TITLE_STYLE = DEFAULT_CAROUSEL_TITLE_STYLE;
32514
32567
  dragAndDrop = useDragAndDropListItems();
32515
- previewListRef = (0, _odoo_owl.signal)(null);
32516
- addChartButton = (0, _odoo_owl.signal)(null);
32568
+ previewListRef = _odoo_owl.signal.ref();
32569
+ addChartButton = _odoo_owl.signal.ref(HTMLButtonElement);
32517
32570
  state = (0, _odoo_owl.proxy)({
32518
32571
  popoverProps: void 0,
32519
32572
  currentRange: void 0
@@ -32957,7 +33010,7 @@ var BarConfigPanel = class extends GenericChartConfigPanel {
32957
33010
  var Collapse = class extends Component {
32958
33011
  static template = "o-spreadsheet-Collapse";
32959
33012
  props = (0, _odoo_owl.useProps)({ isCollapsed: types.boolean() });
32960
- contentRef = (0, _odoo_owl.signal)(null);
33013
+ contentRef = _odoo_owl.signal.ref();
32961
33014
  setup() {
32962
33015
  (0, _odoo_owl.onMounted)(() => {
32963
33016
  if (this.props.isCollapsed) this.contentRef()?.classList.add("d-none");
@@ -33010,7 +33063,7 @@ var ChartAnnotation = class extends Component {
33010
33063
  TextInput
33011
33064
  };
33012
33065
  props = (0, _odoo_owl.useProps)(chartSidePanelPropsDefinition);
33013
- editorRef = (0, _odoo_owl.signal)(null);
33066
+ editorRef = _odoo_owl.signal.ref();
33014
33067
  state = (0, _odoo_owl.proxy)({
33015
33068
  annotationTextInput: this.props.definition.annotationText || "",
33016
33069
  isOverflowing: false
@@ -33332,7 +33385,7 @@ var RoundColorPicker = class extends Component {
33332
33385
  onColorPicked: types.function(),
33333
33386
  disableNoColor: types.boolean().optional()
33334
33387
  });
33335
- colorPickerButtonRef = (0, _odoo_owl.signal)(null);
33388
+ colorPickerButtonRef = _odoo_owl.signal.ref();
33336
33389
  state;
33337
33390
  setup() {
33338
33391
  this.state = (0, _odoo_owl.proxy)({ pickerOpened: false });
@@ -36174,6 +36227,7 @@ var helpers_index_exports$1 = /* @__PURE__ */ __exportAll({
36174
36227
  getDefinedAxis: () => getDefinedAxis,
36175
36228
  getPieColors: () => getPieColors,
36176
36229
  isTrendLineAxis: () => isTrendLineAxis,
36230
+ limitChartConfigDataPoints: () => limitChartConfigDataPoints,
36177
36231
  shouldRemoveFirstLabel: () => shouldRemoveFirstLabel,
36178
36232
  toExcelDataset: () => toExcelDataset,
36179
36233
  toExcelLabelRange: () => toExcelLabelRange,
@@ -36944,7 +36998,7 @@ var ColorScalePicker = class extends Component {
36944
36998
  popoverProps: void 0,
36945
36999
  popoverStyle: ""
36946
37000
  });
36947
- popoverRef = (0, _odoo_owl.signal)(null);
37001
+ popoverRef = _odoo_owl.signal.ref(HTMLTableElement);
36948
37002
  setup() {
36949
37003
  useExternalListener(window, "click", this.closePopover);
36950
37004
  }
@@ -37193,7 +37247,7 @@ var TextValueProvider = class extends Component {
37193
37247
  onValueSelected: types.function(),
37194
37248
  onValueHovered: types.function()
37195
37249
  });
37196
- autoCompleteListRef = (0, _odoo_owl.signal)(null);
37250
+ autoCompleteListRef = _odoo_owl.signal.ref();
37197
37251
  setup() {
37198
37252
  (0, _odoo_owl.useEffect)(() => {
37199
37253
  const selectedIndex = this.props.selectedIndex;
@@ -37484,7 +37538,7 @@ var SpeechBubble = class extends Component {
37484
37538
  anchorRect: types.Rect()
37485
37539
  });
37486
37540
  spreadsheetRect = useSpreadsheetRect();
37487
- bubbleRef = (0, _odoo_owl.signal)(null);
37541
+ bubbleRef = _odoo_owl.signal.ref();
37488
37542
  setup() {
37489
37543
  (0, _odoo_owl.useEffect)(() => {
37490
37544
  const el = this.bubbleRef();
@@ -37529,8 +37583,8 @@ var Composer = class extends Component {
37529
37583
  showAssistant: types.boolean().optional(true)
37530
37584
  });
37531
37585
  DOMFocusableElementStore;
37532
- composerRef = (0, _odoo_owl.signal)(null);
37533
- containerRef = (0, _odoo_owl.signal)(null);
37586
+ composerRef = _odoo_owl.signal.ref();
37587
+ containerRef = _odoo_owl.signal.ref();
37534
37588
  contentHelper = new ContentEditableHelper(this.composerRef());
37535
37589
  composerState = (0, _odoo_owl.proxy)({
37536
37590
  positionStart: 0,
@@ -40032,7 +40086,7 @@ var ChartTypePicker = class extends Component {
40032
40086
  chartId: types.UID(),
40033
40087
  chartPanelStore: types.Store()
40034
40088
  });
40035
- selectRef = (0, _odoo_owl.signal)(null);
40089
+ selectRef = _odoo_owl.signal.ref();
40036
40090
  state = (0, _odoo_owl.proxy)({ popoverProps: void 0 });
40037
40091
  getSupportedChartTypes() {
40038
40092
  let supportedTypes;
@@ -40600,7 +40654,7 @@ var ColumnStatsPanel = class extends Component {
40600
40654
  highlightPositions: []
40601
40655
  });
40602
40656
  store;
40603
- chartCanvasRef = (0, _odoo_owl.signal)(null);
40657
+ chartCanvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
40604
40658
  chart;
40605
40659
  setup() {
40606
40660
  this.store = useStore(ColumnStatisticsStore);
@@ -42028,7 +42082,7 @@ var ConditionalFormatPreview = class extends Component {
42028
42082
  class: types.string()
42029
42083
  });
42030
42084
  icons = ICONS;
42031
- cfPreviewRef = (0, _odoo_owl.signal)(null);
42085
+ cfPreviewRef = _odoo_owl.signal.ref();
42032
42086
  setup() {
42033
42087
  useHighlightsOnHover(this.cfPreviewRef, this);
42034
42088
  }
@@ -42085,7 +42139,7 @@ var ConditionalFormatPreviewList = class extends Component {
42085
42139
  static components = { ConditionalFormatPreview };
42086
42140
  props = (0, _odoo_owl.useProps)({ onCloseSidePanel: types.function() });
42087
42141
  dragAndDrop = useDragAndDropListItems();
42088
- cfListRef = (0, _odoo_owl.signal)(null);
42142
+ cfListRef = _odoo_owl.signal.ref();
42089
42143
  get conditionalFormats() {
42090
42144
  return this.env.model.getters.getConditionalFormats(this.env.model.getters.getActiveSheetId());
42091
42145
  }
@@ -42145,6 +42199,1477 @@ var ConditionalFormatPreviewList = class extends Component {
42145
42199
  }
42146
42200
  };
42147
42201
 
42202
+ //#endregion
42203
+ //#region src/stores/chart_drag_store.ts
42204
+ /**
42205
+ * Holds the id of the figure that a dragged chart suggestion is currently
42206
+ * hovering over, so it can be highlighted reactively instead of through direct DOM manipulation.
42207
+ */
42208
+ var ChartDragStore = class {
42209
+ mutators = ["setHighlightedFigure"];
42210
+ highlightedFigureId = void 0;
42211
+ setHighlightedFigure(figureId) {
42212
+ this.highlightedFigureId = figureId;
42213
+ }
42214
+ };
42215
+
42216
+ //#endregion
42217
+ //#region src/components/helpers/chart_drag_and_drop.ts
42218
+ function getDefaultChartFigureSize(type) {
42219
+ if (type === "scorecard") return {
42220
+ width: 213,
42221
+ height: 101
42222
+ };
42223
+ return {
42224
+ width: 536,
42225
+ height: 335
42226
+ };
42227
+ }
42228
+ function getCarouselOverlappingChart(figureUI, otherFigures, matchTags) {
42229
+ if (figureUI.tag !== "chart") return;
42230
+ const figureCenterX = figureUI.x + figureUI.width / 2;
42231
+ const figureCenterY = figureUI.y + figureUI.height / 2;
42232
+ let bestMatch;
42233
+ let smallestDistance = Infinity;
42234
+ for (const figure of otherFigures) {
42235
+ if (!matchTags.includes(figure.tag)) continue;
42236
+ const targetCenterX = figure.x + figure.width / 2;
42237
+ const targetCenterY = figure.y + figure.height / 2;
42238
+ const distanceX = Math.abs(figureCenterX - targetCenterX);
42239
+ const distanceY = Math.abs(figureCenterY - targetCenterY);
42240
+ const squaredDistance = distanceX ** 2 + distanceY ** 2;
42241
+ if (distanceX <= figureUI.width / 2 && distanceY <= figureUI.height / 2 && squaredDistance < smallestDistance) {
42242
+ smallestDistance = squaredDistance;
42243
+ bestMatch = figure;
42244
+ }
42245
+ }
42246
+ return bestMatch;
42247
+ }
42248
+ /**
42249
+ * Start dragging a floating preview of the given chart definition, following the mouse.
42250
+ * On drop, creates the chart on the grid (or inside a carousel if dropped on one).
42251
+ */
42252
+ function startChartDragAndDrop(env, definition, ev) {
42253
+ const zoom = env.getStore(ZoomStore).zoomLevel;
42254
+ const gridPosition = gridOverlayPosition(zoom);
42255
+ const spreadsheet = document.querySelector(".o-spreadsheet");
42256
+ if (!spreadsheet) return;
42257
+ const startX = ev.clientX / zoom;
42258
+ const startY = ev.clientY / zoom;
42259
+ const { width, height } = getDefaultChartFigureSize(definition.type);
42260
+ const figureWidth = width * zoom;
42261
+ const figureHeight = height * zoom;
42262
+ const getters = env.model.getters;
42263
+ const sheetId = getters.getActiveSheetId();
42264
+ let container = null;
42265
+ let destroyChart = void 0;
42266
+ const chartDragStore = env.getStore(ChartDragStore);
42267
+ const previousCursor = document.body.style.cursor;
42268
+ document.body.style.cursor = "grabbing";
42269
+ /** Grid coordinates (in sheet pixels) of a mouse position, or undefined if outside the grid. */
42270
+ const getGridPosition = (clientX, clientY) => {
42271
+ if (clientX > gridPosition.x + gridPosition.width || clientY > gridPosition.y + gridPosition.height) return;
42272
+ const { scrollX, scrollY } = env.getStore(ViewportsStore).activeSheetScrollInfo;
42273
+ return {
42274
+ x: Math.max(0, (clientX - gridPosition.x) / zoom + scrollX),
42275
+ y: Math.max(0, (clientY - gridPosition.y) / zoom + scrollY)
42276
+ };
42277
+ };
42278
+ /** Carousel or standalone chart the dragged chart is dropped onto, if any. */
42279
+ const getOverlappingFigure = (clientX, clientY) => {
42280
+ const position = getGridPosition(clientX, clientY);
42281
+ if (!position) return;
42282
+ const figureUI = {
42283
+ tag: "chart",
42284
+ ...position,
42285
+ width,
42286
+ height
42287
+ };
42288
+ const otherFigures = env.getStore(ViewportsStore).visibleFigures;
42289
+ return getCarouselOverlappingChart(figureUI, otherFigures, ["carousel", "chart"]);
42290
+ };
42291
+ const halfWidth = figureWidth / 2;
42292
+ const halfHeight = figureHeight / 2;
42293
+ const onMouseMove = (e) => {
42294
+ if (Math.abs(e.clientX - startX) <= 5 && Math.abs(e.clientY - startY) <= 5) return;
42295
+ if (container === null) {
42296
+ container = document.createElement("div");
42297
+ container.className = "o-chart-drag-preview os-theme-dependant position-fixed border pe-none";
42298
+ container.style.width = `${width}px`;
42299
+ container.style.height = `${height}px`;
42300
+ container.style.zoom = `${zoom}`;
42301
+ const canvas = document.createElement("canvas");
42302
+ canvas.className = "w-100 h-100";
42303
+ container.appendChild(canvas);
42304
+ spreadsheet.appendChild(container);
42305
+ destroyChart = drawChartOnCanvas(canvas, SpreadsheetChart.fromStrDefinition(getters, sheetId, definition).getRuntime(getters, "newChart"), {
42306
+ width,
42307
+ height
42308
+ }, definition.type, zoom);
42309
+ }
42310
+ container.style.left = `${Math.max(gridPosition.x, (e.clientX - halfWidth) / zoom)}px`;
42311
+ container.style.top = `${Math.max(gridPosition.y, (e.clientY - halfHeight) / zoom)}px`;
42312
+ const overlappingFigure = getOverlappingFigure(e.clientX - halfWidth, e.clientY - halfHeight);
42313
+ container.style.opacity = overlappingFigure?.id ? "0.6" : "0.9";
42314
+ chartDragStore.setHighlightedFigure(overlappingFigure?.id);
42315
+ };
42316
+ const onMouseUp = (mouseEvent) => {
42317
+ chartDragStore.setHighlightedFigure(void 0);
42318
+ if (container !== null) {
42319
+ spreadsheet.removeChild(container);
42320
+ container = null;
42321
+ }
42322
+ destroyChart?.();
42323
+ document.body.style.cursor = previousCursor;
42324
+ let position = getGridPosition(mouseEvent.clientX - halfWidth, mouseEvent.clientY - halfHeight);
42325
+ if (Math.abs(mouseEvent.clientX / zoom - startX) <= 5 && Math.abs(mouseEvent.clientY / zoom - startY) <= 5) position = {
42326
+ x: 0,
42327
+ y: 0
42328
+ };
42329
+ else if (!position || position.x + halfWidth > gridPosition.width) return;
42330
+ const { col, row, offset } = env.getStore(ViewportsStore).viewports.getPositionAnchorOffset(sheetId, position);
42331
+ const payload = {
42332
+ chartId: UuidGenerator.smallUuid(),
42333
+ figureId: UuidGenerator.smallUuid(),
42334
+ sheetId,
42335
+ size: {
42336
+ width,
42337
+ height
42338
+ },
42339
+ definition,
42340
+ col,
42341
+ row,
42342
+ offset
42343
+ };
42344
+ const overlappingFigure = getOverlappingFigure(mouseEvent.clientX - halfWidth, mouseEvent.clientY - halfHeight);
42345
+ if (overlappingFigure?.tag === "carousel") env.model.dispatch("ADD_NEW_CHART_TO_CAROUSEL", {
42346
+ sheetId,
42347
+ figureId: overlappingFigure.id,
42348
+ newChartId: UuidGenerator.smallUuid(),
42349
+ chartDefinition: definition
42350
+ });
42351
+ else if (overlappingFigure?.tag === "chart") env.model.dispatch("CREATE_CHART_AND_MERGE_INTO_CAROUSEL", {
42352
+ chartId: payload.chartId,
42353
+ figureId: payload.figureId,
42354
+ sheetId: payload.sheetId,
42355
+ definition: payload.definition,
42356
+ baseFigureId: overlappingFigure.id
42357
+ });
42358
+ else env.model.dispatch("CREATE_CHART", payload);
42359
+ };
42360
+ startDnd(onMouseMove, onMouseUp);
42361
+ }
42362
+
42363
+ //#endregion
42364
+ //#region src/components/side_panel/data_analysis/chart_suggestion_preview.ts
42365
+ const PREVIEW_BUBBLE_RADIUS_RATIO = .3;
42366
+ const MIN_PREVIEW_BUBBLE_RADIUS = 1;
42367
+ var ChartSuggestionPreview = class extends Component {
42368
+ static template = "o-spreadsheet-ChartSuggestionPreview";
42369
+ props = (0, _odoo_owl.useProps)({
42370
+ definition: types.ChartDefinition(),
42371
+ description: types.string(),
42372
+ onPointerDown: types.function()
42373
+ });
42374
+ chartCanvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
42375
+ chart;
42376
+ setup() {
42377
+ (0, _odoo_owl.onMounted)(() => registerChartJSExtensions());
42378
+ (0, _odoo_owl.onWillUnmount)(() => this.destroyChart());
42379
+ useLayoutEffect(() => {
42380
+ this.updateChart();
42381
+ }, () => [this.props.definition]);
42382
+ }
42383
+ getChartConfiguration() {
42384
+ const getters = this.env.model.getters;
42385
+ const activeSheetId = getters.getActiveSheetId();
42386
+ const runtime = SpreadsheetChart.fromStrDefinition(getters, activeSheetId, this.props.definition).getRuntime(getters, "myChart");
42387
+ if (!("chartJsConfig" in runtime)) return null;
42388
+ let config = runtime.chartJsConfig;
42389
+ const existingScales = config.options?.scales ?? {};
42390
+ config = {
42391
+ ...config,
42392
+ data: this.props.definition.type === "bubble" ? {
42393
+ ...config.data,
42394
+ datasets: (config.data?.datasets || []).map((dataset) => ({
42395
+ ...dataset,
42396
+ pointRadius: this.scaleBubblePointRadius(dataset.pointRadius),
42397
+ pointHoverRadius: this.scaleBubblePointRadius(dataset.pointHoverRadius)
42398
+ }))
42399
+ } : config.data,
42400
+ options: {
42401
+ ...config.options,
42402
+ plugins: {
42403
+ ...config.options?.plugins,
42404
+ legend: { display: false },
42405
+ title: { display: false },
42406
+ tooltip: { enabled: false }
42407
+ },
42408
+ events: [],
42409
+ animation: false,
42410
+ scales: this.getScalesCongif(existingScales)
42411
+ }
42412
+ };
42413
+ return limitChartConfigDataPoints(config);
42414
+ }
42415
+ getScalesCongif(scales) {
42416
+ if (scales.x) scales.x = {
42417
+ ...scales.x,
42418
+ ticks: { display: false },
42419
+ border: { display: false }
42420
+ };
42421
+ if (scales.y) scales.y = {
42422
+ ...scales.y,
42423
+ ticks: { display: false },
42424
+ border: { display: false }
42425
+ };
42426
+ if (scales.r) scales.r = {
42427
+ ...scales.r,
42428
+ ticks: { display: false },
42429
+ pointLabels: { display: false }
42430
+ };
42431
+ return scales;
42432
+ }
42433
+ scaleBubblePointRadius(pointRadius) {
42434
+ if (Array.isArray(pointRadius)) return pointRadius.map((radius) => radius <= 0 ? radius : Math.max(MIN_PREVIEW_BUBBLE_RADIUS, radius * PREVIEW_BUBBLE_RADIUS_RATIO));
42435
+ if (typeof pointRadius === "number") return pointRadius <= 0 ? pointRadius : Math.max(MIN_PREVIEW_BUBBLE_RADIUS, pointRadius * PREVIEW_BUBBLE_RADIUS_RATIO);
42436
+ return pointRadius ?? MIN_PREVIEW_BUBBLE_RADIUS;
42437
+ }
42438
+ updateChart() {
42439
+ this.destroyChart();
42440
+ const config = this.getChartConfiguration();
42441
+ if (!config) {
42442
+ this.drawNativeChart();
42443
+ return;
42444
+ }
42445
+ const ctx = this.chartCanvasRef()?.getContext("2d");
42446
+ if (!ctx || !globalThis.Chart) return;
42447
+ this.chart = new globalThis.Chart(ctx, config);
42448
+ }
42449
+ drawNativeChart() {
42450
+ const canvas = this.chartCanvasRef();
42451
+ if (!canvas) return;
42452
+ const getters = this.env.model.getters;
42453
+ let runtime = SpreadsheetChart.fromStrDefinition(getters, getters.getActiveSheetId(), this.props.definition).getRuntime(getters, "myChart");
42454
+ const { type } = this.props.definition;
42455
+ if (type === "scorecard") {
42456
+ const rect = canvas.getBoundingClientRect();
42457
+ runtime = {
42458
+ ...runtime,
42459
+ keyValueStyle: {
42460
+ ...runtime.keyValueStyle,
42461
+ fontSize: 16
42462
+ }
42463
+ };
42464
+ drawScoreChart(getScorecardConfiguration({
42465
+ width: rect.width || 130,
42466
+ height: rect.height || 120
42467
+ }, runtime), canvas);
42468
+ } else if (type === "gauge") {
42469
+ let gaugeRuntime = runtime;
42470
+ if (gaugeRuntime.title) gaugeRuntime = {
42471
+ ...gaugeRuntime,
42472
+ title: { text: "" }
42473
+ };
42474
+ drawGaugeChart(canvas, gaugeRuntime, 1, void 0, { labelFontSize: 8 });
42475
+ }
42476
+ }
42477
+ destroyChart() {
42478
+ this.chart?.destroy();
42479
+ this.chart = void 0;
42480
+ }
42481
+ };
42482
+
42483
+ //#endregion
42484
+ //#region src/helpers/data_analysis.ts
42485
+ function analyzeColumns(zones, getters) {
42486
+ return getZonesByColumns(zones).map((zone) => analyzeColumn(zone, getters));
42487
+ }
42488
+ function analyzeColumn(zone, getters) {
42489
+ const sheetId = getters.getActiveSheetId();
42490
+ const cells = getters.getEvaluatedCellsInZone(sheetId, zone).filter((c) => c.type !== "empty");
42491
+ if (!cells.length) return {
42492
+ zone,
42493
+ type: "empty",
42494
+ hasHeader: false,
42495
+ rowCount: 0,
42496
+ uniqueCount: 0,
42497
+ uniqueRatio: 0,
42498
+ nonEmpty: []
42499
+ };
42500
+ const firstCell = cells[0];
42501
+ const rest = cells.slice(1);
42502
+ const hasHeader = firstCell.type === "text" && rest.some((c) => c.type !== "text");
42503
+ const dataCells = hasHeader ? rest : cells;
42504
+ const numericValues = dataCells.filter((c) => c.type === "number").map((c) => c.value);
42505
+ const allVals = dataCells.map((c) => String(c.value ?? ""));
42506
+ const uniqueCount = new Set(allVals).size;
42507
+ return {
42508
+ zone,
42509
+ type: computeColumnType(dataCells),
42510
+ header: hasHeader ? firstCell.value : void 0,
42511
+ hasHeader,
42512
+ rowCount: dataCells.length,
42513
+ uniqueCount,
42514
+ uniqueRatio: allVals.length > 0 ? uniqueCount / allVals.length : 0,
42515
+ maxValue: numericValues.length ? numericValues.reduce((max, v) => v > max ? v : max, numericValues[0]) : void 0,
42516
+ nonEmpty: dataCells
42517
+ };
42518
+ }
42519
+ function computeColumnType(cells) {
42520
+ if (cells.length === 0) return "empty";
42521
+ if (cells.every((c) => c.type === "error")) return "error";
42522
+ cells = cells.filter((c) => c.type !== "error");
42523
+ if (cells.every((c) => c.type === "boolean")) return "boolean";
42524
+ if (cells.every((c) => c.type === "number" && !!c.format && isDateTimeFormat(c.format))) return "date";
42525
+ else if (cells.every((c) => c.type === "number")) {
42526
+ if (cells.every((c) => c.format?.includes("%"))) return "percentage";
42527
+ return "number";
42528
+ } else {
42529
+ const textVals = cells.filter((c) => c.type === "text").map((c) => c.value);
42530
+ if (textVals.length > 0) {
42531
+ const unique = new Set(textVals).size;
42532
+ return unique / textVals.length < .75 && unique <= 20 ? "categorical" : "label";
42533
+ }
42534
+ }
42535
+ return "empty";
42536
+ }
42537
+
42538
+ //#endregion
42539
+ //#region src/helpers/figures/charts/charts_suggestions_constants.ts
42540
+ /** Above this many rows, shape-based charts (radar, scatter, pyramid) become unreadable. */
42541
+ const MAX_ROWS_FOR_SHAPE_CHART = 10;
42542
+ function barChart(titleText, source, opts = {}) {
42543
+ return {
42544
+ ...opts,
42545
+ type: "bar",
42546
+ title: { text: titleText },
42547
+ dataSource: source,
42548
+ dataSetStyles: {},
42549
+ legendPosition: opts.legendPosition ?? "none",
42550
+ stacked: opts.stacked ?? false,
42551
+ humanize: true,
42552
+ aggregated: opts.aggregated ?? false
42553
+ };
42554
+ }
42555
+ function lineChart(titleText, source, opts = {}) {
42556
+ return {
42557
+ ...opts,
42558
+ type: "line",
42559
+ title: { text: titleText },
42560
+ dataSource: source,
42561
+ dataSetStyles: {},
42562
+ legendPosition: opts.legendPosition ?? "none",
42563
+ stacked: opts.stacked ?? false,
42564
+ cumulative: opts.cumulative ?? false,
42565
+ labelsAsText: false,
42566
+ humanize: true
42567
+ };
42568
+ }
42569
+ function pieChart(titleText, source, opts = {}) {
42570
+ return {
42571
+ ...opts,
42572
+ type: "pie",
42573
+ title: { text: titleText },
42574
+ dataSource: source,
42575
+ dataSetStyles: {},
42576
+ legendPosition: opts.legendPosition ?? "top",
42577
+ humanize: true
42578
+ };
42579
+ }
42580
+ function radarChart(titleText, source, opts = {}) {
42581
+ return {
42582
+ ...opts,
42583
+ type: "radar",
42584
+ title: { text: titleText },
42585
+ dataSource: source,
42586
+ dataSetStyles: {},
42587
+ legendPosition: opts.legendPosition ?? "none",
42588
+ stacked: false,
42589
+ humanize: true
42590
+ };
42591
+ }
42592
+ function sunburstChart(titleText, source) {
42593
+ return {
42594
+ type: "sunburst",
42595
+ title: { text: titleText },
42596
+ dataSource: source,
42597
+ dataSetStyles: {},
42598
+ legendPosition: "none"
42599
+ };
42600
+ }
42601
+ function treemapChart(titleText, source) {
42602
+ return {
42603
+ type: "treemap",
42604
+ title: { text: titleText },
42605
+ dataSource: source,
42606
+ dataSetStyles: {},
42607
+ legendPosition: "none"
42608
+ };
42609
+ }
42610
+ function calendarChart(titleText, source) {
42611
+ return {
42612
+ type: "calendar",
42613
+ title: { text: titleText },
42614
+ dataSource: source,
42615
+ dataSetStyles: {},
42616
+ legendPosition: "none",
42617
+ horizontalGroupBy: "month_number",
42618
+ verticalGroupBy: "day_of_week",
42619
+ humanize: true
42620
+ };
42621
+ }
42622
+ function scatterChart(titleText, source) {
42623
+ return {
42624
+ type: "scatter",
42625
+ title: { text: titleText },
42626
+ dataSource: source,
42627
+ dataSetStyles: {},
42628
+ legendPosition: "none",
42629
+ labelsAsText: false,
42630
+ humanize: true
42631
+ };
42632
+ }
42633
+ function pyramidChart(titleText, source, opts = {}) {
42634
+ return {
42635
+ type: "pyramid",
42636
+ title: { text: titleText },
42637
+ dataSource: source,
42638
+ dataSetStyles: {},
42639
+ legendPosition: opts.legendPosition ?? "top",
42640
+ stacked: false,
42641
+ horizontal: true,
42642
+ humanize: true
42643
+ };
42644
+ }
42645
+ function comboChart(titleText, source, opts = {}) {
42646
+ return {
42647
+ ...opts,
42648
+ type: "combo",
42649
+ title: { text: titleText },
42650
+ dataSource: source,
42651
+ dataSetStyles: {},
42652
+ legendPosition: "top",
42653
+ humanize: true
42654
+ };
42655
+ }
42656
+ function bubbleChart(titleText, xRange, yRanges, sizeRange, labelRange, dataSetsHaveTitle) {
42657
+ return {
42658
+ type: "bubble",
42659
+ title: { text: titleText },
42660
+ humanize: true,
42661
+ dataSetsHaveTitle,
42662
+ yRanges,
42663
+ xRange,
42664
+ sizeRange,
42665
+ labelRange,
42666
+ labelsAsText: false,
42667
+ legendPosition: "none",
42668
+ bubbleColor: { color: "multiple" },
42669
+ verticalAxisPosition: "left"
42670
+ };
42671
+ }
42672
+ function scorecardChart(titleText, keyValue, opts = {}) {
42673
+ return {
42674
+ ...opts,
42675
+ type: "scorecard",
42676
+ title: { text: titleText },
42677
+ keyValue,
42678
+ baselineMode: opts.baselineMode ?? "difference",
42679
+ baselineColorUp: DEFAULT_SCORECARD_BASELINE_COLOR_UP,
42680
+ baselineColorDown: DEFAULT_SCORECARD_BASELINE_COLOR_DOWN,
42681
+ humanize: opts.humanize ?? true
42682
+ };
42683
+ }
42684
+ function gaugeChart(titleText, dataRange, rangeMin, rangeMax) {
42685
+ return {
42686
+ type: "gauge",
42687
+ title: { text: titleText },
42688
+ dataRange,
42689
+ sectionRule: {
42690
+ colors: {
42691
+ lowerColor: DEFAULT_GAUGE_LOWER_COLOR,
42692
+ middleColor: DEFAULT_GAUGE_MIDDLE_COLOR,
42693
+ upperColor: DEFAULT_GAUGE_UPPER_COLOR
42694
+ },
42695
+ rangeMin,
42696
+ rangeMax,
42697
+ lowerInflectionPoint: {
42698
+ type: "percentage",
42699
+ value: "33",
42700
+ operator: "<="
42701
+ },
42702
+ upperInflectionPoint: {
42703
+ type: "percentage",
42704
+ value: "66",
42705
+ operator: "<="
42706
+ }
42707
+ },
42708
+ humanize: true
42709
+ };
42710
+ }
42711
+ /** Pattern A — Single numeric column */
42712
+ const SINGLE_NUMBER_COLUMN_SUGGESTIONS = [
42713
+ {
42714
+ description: _t("Highlights the most recent value compared to the previous one."),
42715
+ isApplicable: ({ rowCount }) => rowCount < 3,
42716
+ build: (ctx) => scorecardChart(ctx.title, ctx.lastCellXC, {
42717
+ baseline: ctx.prevCellXC,
42718
+ baselineMode: "difference"
42719
+ })
42720
+ },
42721
+ {
42722
+ description: _t("Shows the position of the last value within the data's min-max range."),
42723
+ isApplicable: ({ rowCount }) => rowCount === 3,
42724
+ build: (ctx) => gaugeChart(ctx.title, ctx.lastCellXC, `=${ctx.firstCellXC}`, `=${ctx.prevCellXC}`)
42725
+ },
42726
+ {
42727
+ description: _t("Compares individual values side-by-side."),
42728
+ isApplicable: ({ rowCount }) => rowCount > 1,
42729
+ build: (ctx) => barChart(ctx.title, ctx.source)
42730
+ },
42731
+ {
42732
+ description: _t("Shows the evolution of all values over the range."),
42733
+ isApplicable: ({ rowCount }) => rowCount > 2,
42734
+ build: (ctx) => lineChart(ctx.title, ctx.source)
42735
+ },
42736
+ {
42737
+ description: _t("Emphasizes total accumulation over the range."),
42738
+ isApplicable: ({ rowCount }) => rowCount > 2,
42739
+ build: (ctx) => lineChart(ctx.title, ctx.source, {
42740
+ fillArea: true,
42741
+ cumulative: true
42742
+ })
42743
+ }
42744
+ ];
42745
+ /** Pattern B — Single percentage column */
42746
+ const SINGLE_PERCENTAGE_COLUMN_SUGGESTIONS = [
42747
+ {
42748
+ description: _t("Shows the last percentage value with its baseline."),
42749
+ isApplicable: ({ rowCount }) => rowCount < 3,
42750
+ build: (ctx) => scorecardChart(ctx.title, ctx.lastCellXC, {
42751
+ baseline: ctx.prevCellXC,
42752
+ baselineMode: "percentage"
42753
+ })
42754
+ },
42755
+ {
42756
+ description: _t("Natural fit for a 0–100% range."),
42757
+ isApplicable: ({ rowCount }) => rowCount === 1,
42758
+ build: (ctx) => gaugeChart(ctx.title, ctx.lastCellXC, "0", ctx.isAboveOne ? "100" : "1")
42759
+ },
42760
+ {
42761
+ description: _t("Natural fit for a 0–100% range."),
42762
+ isApplicable: ({ rowCount }) => rowCount === 3,
42763
+ build: (ctx) => gaugeChart(ctx.title, ctx.lastCellXC, `=${ctx.firstCellXC}`, `=${ctx.prevCellXC}`)
42764
+ },
42765
+ {
42766
+ description: _t("Shows completion against total."),
42767
+ isApplicable: ({ rowCount }) => rowCount > 1,
42768
+ build: (ctx) => pieChart(ctx.title, ctx.source, { isDoughnut: true })
42769
+ },
42770
+ {
42771
+ description: _t("Compares all percentage values side-by-side."),
42772
+ isApplicable: ({ rowCount }) => rowCount > 1,
42773
+ build: (ctx) => barChart(ctx.title, ctx.source)
42774
+ }
42775
+ ];
42776
+ /** Pattern C — Single date column */
42777
+ const SINGLE_DATE_COLUMN_SUGGESTIONS = [{
42778
+ description: _t("Shows the last date value."),
42779
+ isApplicable: ({ rowCount }) => rowCount === 1,
42780
+ build: (ctx) => scorecardChart(ctx.title, ctx.lastCellXC)
42781
+ }];
42782
+ /** Pattern D — Single categorical column */
42783
+ const SINGLE_CATEGORICAL_COLUMN_SUGGESTIONS = [
42784
+ {
42785
+ description: _t("Shows the share of each category."),
42786
+ build: (ctx) => pieChart(ctx.title, ctx.source, {
42787
+ aggregated: true,
42788
+ legendPosition: "top"
42789
+ })
42790
+ },
42791
+ {
42792
+ description: _t("Same as pie, cleaner proportional look."),
42793
+ build: (ctx) => pieChart(ctx.title, ctx.source, {
42794
+ aggregated: true,
42795
+ isDoughnut: true,
42796
+ legendPosition: "top"
42797
+ })
42798
+ },
42799
+ {
42800
+ description: _t("Absolute count per category."),
42801
+ build: (ctx) => barChart(ctx.title, {
42802
+ ...ctx.source,
42803
+ labelRange: ctx.range
42804
+ }, {
42805
+ legendPosition: "none",
42806
+ aggregated: true
42807
+ })
42808
+ }
42809
+ ];
42810
+ /** Pattern E — Single label column */
42811
+ const SINGLE_LABEL_COLUMN_SUGGESTIONS = [{
42812
+ description: _t("Displays a key performance indicator."),
42813
+ isApplicable: ({ rowCount }) => rowCount === 1,
42814
+ build: (ctx) => scorecardChart(ctx.title, ctx.lastCellXC, {
42815
+ baselineMode: "text",
42816
+ humanize: false
42817
+ })
42818
+ }];
42819
+ /** Pattern F — Categorical + Number */
42820
+ const CATEGORICAL_VS_NUMBER_SUGGESTIONS = [
42821
+ {
42822
+ description: _t("Classic category-vs-value comparison."),
42823
+ build: (ctx) => barChart(ctx.title, ctx.source, { aggregated: true })
42824
+ },
42825
+ {
42826
+ description: _t("Better when category labels are long."),
42827
+ build: (ctx) => barChart(ctx.title, ctx.source, {
42828
+ horizontal: true,
42829
+ aggregated: true
42830
+ })
42831
+ },
42832
+ {
42833
+ description: _t("Share of total per category."),
42834
+ build: (ctx) => pieChart(ctx.title, ctx.source, {
42835
+ legendPosition: "top",
42836
+ aggregated: true
42837
+ })
42838
+ },
42839
+ {
42840
+ description: _t("Proportional area — good for many categories."),
42841
+ build: (ctx) => treemapChart(ctx.title, ctx.treemapSource)
42842
+ }
42843
+ ];
42844
+ /** Patterns G & J — Date + Number or Date + Percentage */
42845
+ const DATE_VS_SERIES_SUGGESTIONS = [
42846
+ {
42847
+ description: _t("Best for visualizing time-series trends."),
42848
+ build: (ctx) => lineChart(ctx.title, ctx.source)
42849
+ },
42850
+ {
42851
+ description: _t("Emphasizes total volume over time."),
42852
+ build: (ctx) => lineChart(ctx.title, ctx.source, {
42853
+ fillArea: true,
42854
+ cumulative: true
42855
+ })
42856
+ },
42857
+ {
42858
+ description: _t("Period-by-period comparison."),
42859
+ build: (ctx) => barChart(ctx.title, ctx.source)
42860
+ },
42861
+ {
42862
+ description: _t("Shows intensity variation across days of the year."),
42863
+ isApplicable: ({ isPercentage }) => !isPercentage,
42864
+ build: (ctx) => calendarChart(ctx.title, ctx.source)
42865
+ }
42866
+ ];
42867
+ /** Pattern H — Number + Number */
42868
+ const NUMBER_VS_NUMBER_SUGGESTIONS = [
42869
+ {
42870
+ description: _t("Highlights the second metric compared to the first one."),
42871
+ isApplicable: ({ rowCount1, rowCount2 }) => rowCount1 === 1 && rowCount2 === 1,
42872
+ build: (ctx) => scorecardChart(ctx.title, ctx.lastCellXC, {
42873
+ baseline: ctx.prevCellXC,
42874
+ baselineMode: "difference"
42875
+ })
42876
+ },
42877
+ {
42878
+ description: _t("Side-by-side comparison of two numeric series."),
42879
+ build: (ctx) => barChart(ctx.title, ctx.sourceBoth, { legendPosition: "top" })
42880
+ },
42881
+ {
42882
+ description: _t("Reveals correlation between two numeric variables."),
42883
+ isApplicable: ({ rowCount1 }) => rowCount1 > 2,
42884
+ build: (ctx) => scatterChart(ctx.title, ctx.source2)
42885
+ },
42886
+ {
42887
+ description: _t("Bar for the first series, line for the second — good for mixed scales."),
42888
+ isApplicable: ({ rowCount1 }) => rowCount1 > 2,
42889
+ build: (ctx) => comboChart(ctx.title, ctx.sourceBoth)
42890
+ },
42891
+ {
42892
+ description: _t("When both metrics contribute to a total."),
42893
+ isApplicable: ({ rowCount1 }) => rowCount1 > 2,
42894
+ build: (ctx) => lineChart(ctx.title, ctx.sourceBoth, {
42895
+ stacked: true,
42896
+ fillArea: true,
42897
+ legendPosition: "top"
42898
+ })
42899
+ },
42900
+ {
42901
+ description: _t("Shape-based comparison when rows represent named entities."),
42902
+ isApplicable: ({ rowCount1 }) => rowCount1 > 2 && rowCount1 <= MAX_ROWS_FOR_SHAPE_CHART,
42903
+ build: (ctx) => radarChart(ctx.title, ctx.sourceBoth, { legendPosition: "top" })
42904
+ }
42905
+ ];
42906
+ /** Pattern I — Categorical + Percentage */
42907
+ const CATEGORICAL_VS_PERCENTAGE_SUGGESTIONS = [
42908
+ {
42909
+ description: _t("Vertical comparison of rates per category."),
42910
+ build: (ctx) => barChart(ctx.title, ctx.source)
42911
+ },
42912
+ {
42913
+ description: _t("Progress-bar style per category."),
42914
+ build: (ctx) => barChart(ctx.title, ctx.source, { horizontal: true })
42915
+ },
42916
+ {
42917
+ description: _t("Share of total percentage across categories."),
42918
+ build: (ctx) => pieChart(ctx.title, ctx.source, { legendPosition: "top" })
42919
+ },
42920
+ {
42921
+ description: _t("Comparison of completion rates across categories."),
42922
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
42923
+ build: (ctx) => radarChart(ctx.title, ctx.source)
42924
+ }
42925
+ ];
42926
+ /** Pattern K — Label + Number */
42927
+ const LABEL_VS_NUMBER_SUGGESTIONS = [
42928
+ {
42929
+ description: _t("Highlights the most recent value for the named entity."),
42930
+ isApplicable: ({ rowCount }) => rowCount === 1,
42931
+ build: (ctx) => scorecardChart("", ctx.lastCellXC, {
42932
+ humanize: false,
42933
+ baselineMode: "text",
42934
+ baseline: ctx.prevCellXC
42935
+ })
42936
+ },
42937
+ {
42938
+ description: _t("Vertical comparison across named items."),
42939
+ isApplicable: ({ rowCount }) => rowCount > 1,
42940
+ build: (ctx) => barChart(ctx.title, ctx.source)
42941
+ },
42942
+ {
42943
+ description: _t("Works well for named entities with long labels."),
42944
+ isApplicable: ({ rowCount }) => rowCount > 1,
42945
+ build: (ctx) => barChart(ctx.title, ctx.source, { horizontal: true })
42946
+ },
42947
+ {
42948
+ description: _t("Share of total per category."),
42949
+ isApplicable: ({ rowCount }) => rowCount > 1 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
42950
+ build: (ctx) => pieChart(ctx.title, ctx.source, {
42951
+ legendPosition: "top",
42952
+ aggregated: true
42953
+ })
42954
+ },
42955
+ {
42956
+ description: _t("Shape-based comparison across labeled items."),
42957
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
42958
+ build: (ctx) => radarChart(ctx.title, ctx.source)
42959
+ }
42960
+ ];
42961
+ /** Pattern M — Categorical + Multiple Numbers */
42962
+ const CATEGORICAL_VS_MULTIPLE_NUMBERS_SUGGESTIONS = [
42963
+ {
42964
+ description: _t("Side-by-side comparison across categories for each series."),
42965
+ build: (ctx) => barChart(ctx.title, ctx.source, { legendPosition: "top" })
42966
+ },
42967
+ {
42968
+ description: _t("Shows composition and total per category."),
42969
+ build: (ctx) => barChart(ctx.title, ctx.source, {
42970
+ stacked: true,
42971
+ legendPosition: "top"
42972
+ })
42973
+ },
42974
+ {
42975
+ description: _t("Trend per series across categories."),
42976
+ build: (ctx) => lineChart(ctx.title, ctx.source, { legendPosition: "top" })
42977
+ },
42978
+ {
42979
+ description: _t("Volume and composition across categories."),
42980
+ build: (ctx) => lineChart(ctx.title, ctx.source, {
42981
+ stacked: true,
42982
+ fillArea: true,
42983
+ legendPosition: "top"
42984
+ })
42985
+ },
42986
+ {
42987
+ description: _t("Shape comparison across metrics (best for ≤ 10 rows)."),
42988
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
42989
+ build: (ctx) => radarChart(ctx.title, ctx.source, { legendPosition: "top" })
42990
+ }
42991
+ ];
42992
+ /** Pattern N — Date + Multiple Numbers */
42993
+ const DATE_VS_MULTIPLE_NUMBERS_SUGGESTIONS = [
42994
+ {
42995
+ description: _t("Trend comparison across multiple metrics over time."),
42996
+ build: (ctx) => lineChart(ctx.title, ctx.source, { legendPosition: "top" })
42997
+ },
42998
+ {
42999
+ description: _t("Volume composition over time."),
43000
+ build: (ctx) => lineChart(ctx.title, ctx.source, {
43001
+ stacked: true,
43002
+ fillArea: true,
43003
+ legendPosition: "top"
43004
+ })
43005
+ },
43006
+ {
43007
+ description: _t("Bar for the primary metric, line for the others."),
43008
+ build: (ctx) => comboChart(ctx.title, ctx.source)
43009
+ },
43010
+ {
43011
+ description: _t("Period-by-period grouped comparison."),
43012
+ build: (ctx) => barChart(ctx.title, ctx.source, { legendPosition: "top" })
43013
+ }
43014
+ ];
43015
+ /** Pattern O — Categorical + Categorical + Number */
43016
+ const MULTIPLE_CATEGORICALS_VS_NUMBER_SUGGESTIONS = [
43017
+ {
43018
+ description: _t("Two-level hierarchy weighted by value."),
43019
+ build: (ctx) => sunburstChart(ctx.title, ctx.hierarchySource)
43020
+ },
43021
+ {
43022
+ description: _t("Proportional nested area weighted by value."),
43023
+ build: (ctx) => treemapChart(ctx.title, ctx.hierarchySource)
43024
+ },
43025
+ {
43026
+ description: _t("One series per inner category, grouped by outer category."),
43027
+ build: (ctx) => barChart(ctx.title, ctx.barSource, { legendPosition: "top" })
43028
+ },
43029
+ {
43030
+ description: _t("Contribution of inner categories per outer category."),
43031
+ build: (ctx) => barChart(ctx.title, ctx.barSource, {
43032
+ stacked: true,
43033
+ legendPosition: "top"
43034
+ })
43035
+ }
43036
+ ];
43037
+ /** Pattern P — Categorical + Date + Number */
43038
+ const CATEGORICAL_DATE_NUMBER_SUGGESTIONS = [
43039
+ {
43040
+ description: _t("Trend of values over time."),
43041
+ build: (ctx) => lineChart(ctx.title, ctx.sourceByDate, { legendPosition: "top" })
43042
+ },
43043
+ {
43044
+ description: _t("Volume contribution over time."),
43045
+ build: (ctx) => lineChart(ctx.title, ctx.sourceByDate, {
43046
+ stacked: true,
43047
+ fillArea: true,
43048
+ legendPosition: "top"
43049
+ })
43050
+ },
43051
+ {
43052
+ description: _t("Period × category side-by-side comparison."),
43053
+ build: (ctx) => barChart(ctx.title, ctx.sourceByCat, { legendPosition: "top" })
43054
+ },
43055
+ {
43056
+ description: _t("Composition per period over time."),
43057
+ build: (ctx) => barChart(ctx.title, ctx.sourceByDate, {
43058
+ stacked: true,
43059
+ legendPosition: "top"
43060
+ })
43061
+ }
43062
+ ];
43063
+ /** Pattern Q — Label + Multiple Numbers */
43064
+ const LABEL_VS_MULTIPLE_NUMBERS_SUGGESTIONS = [
43065
+ {
43066
+ description: _t("Side-by-side per entity — works well for long labels."),
43067
+ build: (ctx) => barChart(ctx.title, ctx.source, {
43068
+ horizontal: true,
43069
+ legendPosition: "top"
43070
+ })
43071
+ },
43072
+ {
43073
+ description: _t("Grouped bars per entity for direct metric comparison."),
43074
+ build: (ctx) => barChart(ctx.title, ctx.source, { legendPosition: "top" })
43075
+ },
43076
+ {
43077
+ description: _t("Shape/profile comparison across metrics for each entity."),
43078
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
43079
+ build: (ctx) => radarChart(ctx.title, ctx.source, { legendPosition: "top" })
43080
+ },
43081
+ {
43082
+ description: _t("Correlation between the two numeric metrics across entities."),
43083
+ isApplicable: ({ rowCount, numColsCount }) => rowCount > 2 && numColsCount === 2,
43084
+ build: (ctx) => scatterChart(ctx.title, ctx.scatterSource)
43085
+ },
43086
+ {
43087
+ description: _t("Three metrics in one view: X position, Y position and bubble size."),
43088
+ isApplicable: ({ numColsCount }) => numColsCount === 3,
43089
+ build: (ctx) => {
43090
+ const bubble = ctx.bubble;
43091
+ return bubbleChart(ctx.title, bubble.xRange, bubble.yRanges, bubble.sizeRange, bubble.labelRange, bubble.hasTitle);
43092
+ }
43093
+ }
43094
+ ];
43095
+ /** Pattern R — Categorical + Two Numbers (Population Pyramid or Grouped) */
43096
+ const CATEGORICAL_TWO_NUMBERS_SUGGESTIONS = [
43097
+ {
43098
+ description: _t("Natural fit for symmetric or opposing values per category."),
43099
+ isApplicable: ({ isPyramid }) => isPyramid,
43100
+ build: (ctx) => pyramidChart(ctx.title, ctx.sourceBoth)
43101
+ },
43102
+ {
43103
+ description: _t("Side-by-side comparison of both metrics per category."),
43104
+ build: (ctx) => barChart(ctx.title, ctx.sourceBoth, {
43105
+ legendPosition: "top",
43106
+ aggregated: true
43107
+ })
43108
+ },
43109
+ {
43110
+ description: _t("Bar for the first series, line for the second — good for mixed scales."),
43111
+ build: (ctx) => comboChart(ctx.title, ctx.sourceBoth, { aggregated: true })
43112
+ },
43113
+ {
43114
+ description: _t("Shows total and composition per category."),
43115
+ isApplicable: ({ isPyramid }) => !isPyramid,
43116
+ build: (ctx) => barChart(ctx.title, ctx.sourceBoth, {
43117
+ stacked: true,
43118
+ legendPosition: "top",
43119
+ aggregated: true
43120
+ })
43121
+ }
43122
+ ];
43123
+ /** Pattern S — Many Numbers (3+ numeric columns, no categorical/date) */
43124
+ const MANY_NUMBERS_SUGGESTIONS = [
43125
+ {
43126
+ description: _t("Shows the position of the last value within the data's min-max range."),
43127
+ isApplicable: ({ colsLength, rowCount }) => colsLength === 3 && rowCount === 1,
43128
+ build: (ctx) => gaugeChart(ctx.title, ctx.lastCellXC, `=${ctx.firstCellXC}`, `=${ctx.secondCellXC}`)
43129
+ },
43130
+ {
43131
+ description: _t("Trend comparison across all metrics."),
43132
+ isApplicable: ({ rowCount }) => rowCount > 2,
43133
+ build: (ctx) => lineChart(ctx.title, ctx.source, { legendPosition: "top" })
43134
+ },
43135
+ {
43136
+ description: _t("Overall shape/profile across all metrics."),
43137
+ isApplicable: ({ rowCount }) => rowCount > 2 && rowCount <= MAX_ROWS_FOR_SHAPE_CHART,
43138
+ build: (ctx) => radarChart(ctx.title, ctx.source, { legendPosition: "top" })
43139
+ },
43140
+ {
43141
+ description: _t("Side-by-side comparison of all numeric metrics."),
43142
+ build: (ctx) => barChart(ctx.title, ctx.source, { legendPosition: "top" })
43143
+ },
43144
+ {
43145
+ description: _t("Side-by-side comparison of all numeric metrics."),
43146
+ build: (ctx) => barChart(ctx.title, ctx.source, {
43147
+ legendPosition: "top",
43148
+ stacked: true
43149
+ })
43150
+ }
43151
+ ];
43152
+
43153
+ //#endregion
43154
+ //#region src/helpers/figures/charts/chart_suggestion_engine.ts
43155
+ const PYRAMID_HEADER_KEYWORD_PATTERNS = [
43156
+ "male",
43157
+ "female",
43158
+ "man",
43159
+ "woman",
43160
+ "boy",
43161
+ "girl",
43162
+ "before",
43163
+ "after",
43164
+ "previous",
43165
+ "current",
43166
+ "positive",
43167
+ "negative",
43168
+ "gain",
43169
+ "loss",
43170
+ "income",
43171
+ "expense",
43172
+ "revenue",
43173
+ "cost"
43174
+ ].map((keyword) => new RegExp(`\\b${keyword}\\b`, "i"));
43175
+ function matchesColumnType(spec, type) {
43176
+ return Array.isArray(spec) ? spec.includes(type) : spec === type;
43177
+ }
43178
+ function matchesShape(pattern, shape) {
43179
+ const { leadingTypes, extension } = pattern;
43180
+ const expectedLength = leadingTypes.length + (extension?.minSize ?? 0);
43181
+ if (extension ? shape.length < expectedLength : shape.length !== expectedLength) return false;
43182
+ return shape.every((type, i) => {
43183
+ const spec = i < leadingTypes.length ? leadingTypes[i] : extension?.type;
43184
+ return spec !== void 0 && matchesColumnType(spec, type);
43185
+ });
43186
+ }
43187
+ function isPyramidLike(numCol1, numCol2) {
43188
+ const h1 = numCol1.header ?? "";
43189
+ const h2 = numCol2.header ?? "";
43190
+ return PYRAMID_HEADER_KEYWORD_PATTERNS.some((re) => re.test(h1) || re.test(h2));
43191
+ }
43192
+ /** Returns the first and last cell XC coordinates, and the second-to-last cell XC coordinate if it exists. */
43193
+ function interestingCellsXc(col) {
43194
+ if (!col) return {
43195
+ firstCellXC: "",
43196
+ lastCellXC: "",
43197
+ prevCellXC: void 0
43198
+ };
43199
+ const firstCellPosition = col.nonEmpty.at(0)?.position;
43200
+ const firstCellXC = firstCellPosition ? toXC(firstCellPosition?.col, firstCellPosition?.row) : "";
43201
+ const lastCellPosition = col.nonEmpty.at(-1)?.position;
43202
+ const lastCellXC = lastCellPosition ? toXC(lastCellPosition?.col, lastCellPosition?.row) : "";
43203
+ const prevCellPosition = col.nonEmpty.at(-2)?.position;
43204
+ return {
43205
+ firstCellXC,
43206
+ lastCellXC,
43207
+ prevCellXC: prevCellPosition ? toXC(prevCellPosition?.col, prevCellPosition?.row) : void 0
43208
+ };
43209
+ }
43210
+ /** Pattern A — Single numeric column */
43211
+ function buildSingleNumberContext([col], getters) {
43212
+ const title = col.header ?? _t("Value");
43213
+ const { firstCellXC, lastCellXC, prevCellXC } = interestingCellsXc(col);
43214
+ return {
43215
+ title,
43216
+ source: rangeSource([dataset(col.zone, getters)], col.hasHeader),
43217
+ rowCount: col.rowCount,
43218
+ firstCellXC,
43219
+ lastCellXC,
43220
+ prevCellXC
43221
+ };
43222
+ }
43223
+ /** Pattern B — Single percentage column */
43224
+ function buildSinglePercentageContext([col], getters) {
43225
+ const hasTitle = col.hasHeader;
43226
+ const title = col.header ?? _t("Rate");
43227
+ const { firstCellXC, lastCellXC, prevCellXC } = interestingCellsXc(col);
43228
+ const source = rangeSource([dataset(col.zone, getters)], hasTitle);
43229
+ const isAboveOne = (col.maxValue ?? 0) > 1;
43230
+ return {
43231
+ title,
43232
+ source,
43233
+ rowCount: col.rowCount,
43234
+ firstCellXC,
43235
+ lastCellXC,
43236
+ prevCellXC,
43237
+ isAboveOne
43238
+ };
43239
+ }
43240
+ /** Pattern C — Single date column */
43241
+ function buildSingleDateContext([col]) {
43242
+ const { lastCellXC } = interestingCellsXc(col);
43243
+ return {
43244
+ title: col.header ?? _t("Date"),
43245
+ lastCellXC,
43246
+ rowCount: col.rowCount
43247
+ };
43248
+ }
43249
+ /** Pattern D — Single categorical column */
43250
+ function buildSingleCategoricalContext([col], getters) {
43251
+ const hasTitle = col.hasHeader;
43252
+ const title = col.header ?? _t("Category");
43253
+ const range = getUnboundRange(getters, col.zone);
43254
+ return {
43255
+ title,
43256
+ source: rangeSource([dataset(col.zone, getters)], hasTitle, range),
43257
+ range
43258
+ };
43259
+ }
43260
+ /** Pattern E — Single label column */
43261
+ function buildSingleLabelContext([col]) {
43262
+ const title = col.header ?? _t("Label");
43263
+ const { lastCellXC } = interestingCellsXc(col);
43264
+ return {
43265
+ title,
43266
+ lastCellXC,
43267
+ rowCount: col.rowCount
43268
+ };
43269
+ }
43270
+ /** Pattern F — Categorical + Number */
43271
+ function buildCategoricalVsNumberContext([catCol, numCol], getters) {
43272
+ const labelRange = getUnboundRange(getters, catCol.zone);
43273
+ const hasTitle = numCol.hasHeader;
43274
+ return {
43275
+ title: numCol.header ? _t("%(numberHeader)s by %(categoryHeader)s", {
43276
+ numberHeader: numCol.header,
43277
+ categoryHeader: catCol.header ?? _t("Category")
43278
+ }) : _t("By Category"),
43279
+ source: rangeSource([dataset(numCol.zone, getters)], hasTitle, labelRange),
43280
+ treemapSource: rangeSource([dataset(catCol.zone, getters)], hasTitle, getUnboundRange(getters, numCol.zone))
43281
+ };
43282
+ }
43283
+ /** Patterns G & J — Date + Number or Date + Percentage */
43284
+ function buildDateVsSeriesContext([dateCol, seriesCol], getters) {
43285
+ const isPercentage = seriesCol.type === "percentage";
43286
+ const labelRange = getUnboundRange(getters, dateCol.zone);
43287
+ const hasTitle = isDatasetTitled(getters, dateCol.zone);
43288
+ return {
43289
+ title: seriesCol.header ? _t("%(seriesHeader)s over time", { seriesHeader: seriesCol.header }) : isPercentage ? _t("Rate over Time") : _t("Over Time"),
43290
+ source: rangeSource([dataset(seriesCol.zone, getters)], hasTitle, labelRange),
43291
+ isPercentage
43292
+ };
43293
+ }
43294
+ /** Pattern H — Number + Number */
43295
+ function buildNumberVsNumberContext([col1, col2], getters) {
43296
+ const title = col1.header && col2.header ? _t("%(col2Header)s vs %(col1Header)s", {
43297
+ col2Header: col2.header,
43298
+ col1Header: col1.header
43299
+ }) : _t("Correlation");
43300
+ const hasTitle = col1.hasHeader || col2.hasHeader;
43301
+ const source2 = rangeSource([dataset(col2.zone, getters)], col2.hasHeader, getUnboundRange(getters, col1.zone));
43302
+ const sourceBoth = rangeSource([dataset(col1.zone, getters, "0"), dataset(col2.zone, getters, "1")], hasTitle);
43303
+ const { lastCellXC } = interestingCellsXc(col2);
43304
+ const { lastCellXC: prevCellXC } = interestingCellsXc(col1);
43305
+ return {
43306
+ title,
43307
+ source2,
43308
+ sourceBoth,
43309
+ rowCount1: col1.rowCount,
43310
+ rowCount2: col2.rowCount,
43311
+ lastCellXC,
43312
+ prevCellXC
43313
+ };
43314
+ }
43315
+ /** Pattern I — Categorical + Percentage */
43316
+ function buildCategoricalVsPercentageContext([catCol, pctCol], getters) {
43317
+ const labelRange = getUnboundRange(getters, catCol.zone);
43318
+ const hasTitle = pctCol.hasHeader;
43319
+ return {
43320
+ title: pctCol.header ? _t("%(percentageHeader)s by %(categoryHeader)s", {
43321
+ percentageHeader: pctCol.header,
43322
+ categoryHeader: catCol.header ?? _t("Category")
43323
+ }) : _t("Rates by Category"),
43324
+ source: rangeSource([dataset(pctCol.zone, getters)], hasTitle, labelRange),
43325
+ rowCount: catCol.rowCount
43326
+ };
43327
+ }
43328
+ /** Pattern K — Label + Number */
43329
+ function buildLabelVsNumberContext([labelCol, numCol], getters) {
43330
+ const labelRange = getUnboundRange(getters, labelCol.zone);
43331
+ const hasTitle = numCol.hasHeader;
43332
+ const title = numCol.header ? _t("%(numberHeader)s by %(labelHeader)s", {
43333
+ numberHeader: numCol.header,
43334
+ labelHeader: labelCol.header ?? _t("Name")
43335
+ }) : _t("By Name");
43336
+ const source = rangeSource([dataset(numCol.zone, getters)], hasTitle, labelRange);
43337
+ const { lastCellXC } = interestingCellsXc(numCol);
43338
+ const { lastCellXC: prevCellXC } = interestingCellsXc(labelCol);
43339
+ return {
43340
+ title,
43341
+ source,
43342
+ rowCount: labelCol.rowCount,
43343
+ lastCellXC,
43344
+ prevCellXC
43345
+ };
43346
+ }
43347
+ /** Pattern M — Categorical + Multiple Numbers */
43348
+ function buildCategoricalVsMultipleNumbersContext([catCol, ...numCols], getters) {
43349
+ const labelRange = getUnboundRange(getters, catCol.zone);
43350
+ const hasTitle = numCols.some((c) => c.hasHeader);
43351
+ return {
43352
+ title: catCol.header ? _t("By %(header)s", { header: catCol.header }) : _t("Multi-series"),
43353
+ source: rangeSource(numCols.map((c, i) => dataset(c.zone, getters, String(i))), hasTitle, labelRange),
43354
+ rowCount: catCol.rowCount
43355
+ };
43356
+ }
43357
+ /** Pattern N — Date + Multiple Numbers */
43358
+ function buildDateVsMultipleNumbersContext([dateCol, ...numCols], getters) {
43359
+ const labelRange = getUnboundRange(getters, dateCol.zone);
43360
+ const hasTitle = numCols.some((c) => c.hasHeader) || isDatasetTitled(getters, dateCol.zone);
43361
+ return {
43362
+ title: numCols.length === 1 && numCols[0].header ? _t("%(header)s over time", { header: numCols[0].header }) : _t("Multi-series over Time"),
43363
+ source: rangeSource(numCols.map((c, i) => dataset(c.zone, getters, String(i))), hasTitle, labelRange)
43364
+ };
43365
+ }
43366
+ /** Pattern O — Categorical + Categorical + Number */
43367
+ function buildMultipleCategoricalsVsNumberContext([cat1, cat2, numCol], getters) {
43368
+ const title = numCol.header ? _t("%(numHeader)s by %(cat1Header)s and %(cat2Header)s", {
43369
+ numHeader: numCol.header,
43370
+ cat1Header: cat1.header ?? _t("Level 1"),
43371
+ cat2Header: cat2.header ?? _t("Level 2")
43372
+ }) : _t("Two-level hierarchy");
43373
+ const hasTitle = numCol.hasHeader;
43374
+ return {
43375
+ title,
43376
+ hierarchySource: rangeSource([dataset(cat1.zone, getters, "0"), dataset(cat2.zone, getters, "1")], hasTitle, getUnboundRange(getters, numCol.zone)),
43377
+ barSource: rangeSource([dataset(numCol.zone, getters)], hasTitle, getUnboundRange(getters, cat1.zone))
43378
+ };
43379
+ }
43380
+ /** Pattern P — Categorical + Date + Number */
43381
+ function buildCategoricalDateNumberContext([catCol, dateCol, numCol], getters) {
43382
+ const dateRange = getUnboundRange(getters, dateCol.zone);
43383
+ const catRange = getUnboundRange(getters, catCol.zone);
43384
+ const hasTitle = numCol.hasHeader;
43385
+ return {
43386
+ title: numCol.header ? _t("%(numHeader)s by %(catHeader)s over %(dateHeader)s", {
43387
+ numHeader: numCol.header,
43388
+ catHeader: catCol.header ?? _t("Category"),
43389
+ dateHeader: dateCol.header ?? _t("Time")
43390
+ }) : _t("Multi-series over Time"),
43391
+ sourceByDate: rangeSource([dataset(numCol.zone, getters)], hasTitle, dateRange),
43392
+ sourceByCat: rangeSource([dataset(numCol.zone, getters)], hasTitle, catRange)
43393
+ };
43394
+ }
43395
+ /** Pattern Q — Label + Multiple Numbers */
43396
+ function buildLabelVsMultipleNumbersContext([labelCol, ...numCols], getters) {
43397
+ const labelRange = getUnboundRange(getters, labelCol.zone);
43398
+ const hasTitle = numCols.some((c) => c.hasHeader);
43399
+ const title = labelCol.header ? _t("By %(header)s", { header: labelCol.header }) : _t("Profile Comparison");
43400
+ const source = rangeSource(numCols.map((c, i) => dataset(c.zone, getters, String(i))), hasTitle, labelRange);
43401
+ const scatterSource = rangeSource([dataset(numCols[1].zone, getters)], numCols[1].hasHeader, getUnboundRange(getters, numCols[0].zone));
43402
+ const bubble = numCols.length === 3 ? {
43403
+ xRange: getUnboundRange(getters, numCols[0].zone),
43404
+ yRanges: [getUnboundRange(getters, numCols[1].zone)],
43405
+ sizeRange: getUnboundRange(getters, numCols[2].zone),
43406
+ labelRange,
43407
+ hasTitle
43408
+ } : void 0;
43409
+ return {
43410
+ title,
43411
+ source,
43412
+ rowCount: labelCol.rowCount,
43413
+ numColsCount: numCols.length,
43414
+ scatterSource,
43415
+ bubble
43416
+ };
43417
+ }
43418
+ /** Pattern R — Categorical + Two Numbers (Population Pyramid or Grouped) */
43419
+ function buildCategoricalTwoNumbersContext([catCol, numCol1, numCol2], getters) {
43420
+ const catRange = getUnboundRange(getters, catCol.zone);
43421
+ const hasTitle = numCol1.hasHeader || numCol2.hasHeader;
43422
+ const title = numCol1.header && numCol2.header ? _t("%(num1Header)s vs %(num2Header)s by %(catHeader)s", {
43423
+ num1Header: numCol1.header,
43424
+ num2Header: numCol2.header,
43425
+ catHeader: catCol.header ?? _t("Category")
43426
+ }) : catCol.header ? _t("By %(header)s", { header: catCol.header }) : _t("Category Comparison");
43427
+ const isPyramid = isPyramidLike(numCol1, numCol2);
43428
+ return {
43429
+ title,
43430
+ sourceBoth: rangeSource([dataset(numCol1.zone, getters, "0"), dataset(numCol2.zone, getters, "1")], hasTitle, catRange),
43431
+ isPyramid
43432
+ };
43433
+ }
43434
+ /** Pattern S — Many Numbers (3+ numeric columns, no categorical/date) */
43435
+ function buildManyNumbersContext(cols, getters) {
43436
+ const title = cols.every((c) => c.hasHeader) ? cols.map((c) => c.header).join(" / ") : _t("KPI Overview");
43437
+ const hasTitle = cols.some((c) => c.hasHeader);
43438
+ const source = rangeSource(cols.map((c, i) => dataset(c.zone, getters, String(i))), hasTitle);
43439
+ const { lastCellXC } = interestingCellsXc(cols[2]);
43440
+ const { lastCellXC: firstCellXC } = interestingCellsXc(cols[0]);
43441
+ const { lastCellXC: secondCellXC } = interestingCellsXc(cols[1]);
43442
+ return {
43443
+ title,
43444
+ source,
43445
+ colsLength: cols.length,
43446
+ rowCount: cols[0].rowCount,
43447
+ lastCellXC,
43448
+ firstCellXC,
43449
+ secondCellXC
43450
+ };
43451
+ }
43452
+ const NUMBER_OR_PERCENTAGE = ["number", "percentage"];
43453
+ const EXACT_PATTERNS = [
43454
+ {
43455
+ pattern: { leadingTypes: ["number"] },
43456
+ buildContext: buildSingleNumberContext,
43457
+ suggestions: SINGLE_NUMBER_COLUMN_SUGGESTIONS
43458
+ },
43459
+ {
43460
+ pattern: { leadingTypes: ["percentage"] },
43461
+ buildContext: buildSinglePercentageContext,
43462
+ suggestions: SINGLE_PERCENTAGE_COLUMN_SUGGESTIONS
43463
+ },
43464
+ {
43465
+ pattern: { leadingTypes: ["date"] },
43466
+ buildContext: buildSingleDateContext,
43467
+ suggestions: SINGLE_DATE_COLUMN_SUGGESTIONS
43468
+ },
43469
+ {
43470
+ pattern: { leadingTypes: ["categorical"] },
43471
+ buildContext: buildSingleCategoricalContext,
43472
+ suggestions: SINGLE_CATEGORICAL_COLUMN_SUGGESTIONS
43473
+ },
43474
+ {
43475
+ pattern: { leadingTypes: ["label"] },
43476
+ buildContext: buildSingleLabelContext,
43477
+ suggestions: SINGLE_LABEL_COLUMN_SUGGESTIONS
43478
+ },
43479
+ {
43480
+ pattern: { leadingTypes: ["categorical", "number"] },
43481
+ buildContext: buildCategoricalVsNumberContext,
43482
+ suggestions: CATEGORICAL_VS_NUMBER_SUGGESTIONS
43483
+ },
43484
+ {
43485
+ pattern: { leadingTypes: ["date", NUMBER_OR_PERCENTAGE] },
43486
+ buildContext: buildDateVsSeriesContext,
43487
+ suggestions: DATE_VS_SERIES_SUGGESTIONS
43488
+ },
43489
+ {
43490
+ pattern: { leadingTypes: ["number", "number"] },
43491
+ buildContext: buildNumberVsNumberContext,
43492
+ suggestions: NUMBER_VS_NUMBER_SUGGESTIONS
43493
+ },
43494
+ {
43495
+ pattern: { leadingTypes: ["categorical", "percentage"] },
43496
+ buildContext: buildCategoricalVsPercentageContext,
43497
+ suggestions: CATEGORICAL_VS_PERCENTAGE_SUGGESTIONS
43498
+ },
43499
+ {
43500
+ pattern: { leadingTypes: ["label", "number"] },
43501
+ buildContext: buildLabelVsNumberContext,
43502
+ suggestions: LABEL_VS_NUMBER_SUGGESTIONS
43503
+ },
43504
+ {
43505
+ pattern: { leadingTypes: ["label", "percentage"] },
43506
+ buildContext: buildCategoricalVsPercentageContext,
43507
+ suggestions: CATEGORICAL_VS_PERCENTAGE_SUGGESTIONS
43508
+ },
43509
+ {
43510
+ pattern: { leadingTypes: [
43511
+ "categorical",
43512
+ "categorical",
43513
+ "number"
43514
+ ] },
43515
+ buildContext: buildMultipleCategoricalsVsNumberContext,
43516
+ suggestions: MULTIPLE_CATEGORICALS_VS_NUMBER_SUGGESTIONS
43517
+ },
43518
+ {
43519
+ pattern: { leadingTypes: [
43520
+ "categorical",
43521
+ "label",
43522
+ "number"
43523
+ ] },
43524
+ buildContext: buildMultipleCategoricalsVsNumberContext,
43525
+ suggestions: MULTIPLE_CATEGORICALS_VS_NUMBER_SUGGESTIONS
43526
+ },
43527
+ {
43528
+ pattern: { leadingTypes: [
43529
+ "categorical",
43530
+ "date",
43531
+ NUMBER_OR_PERCENTAGE
43532
+ ] },
43533
+ buildContext: buildCategoricalDateNumberContext,
43534
+ suggestions: CATEGORICAL_DATE_NUMBER_SUGGESTIONS
43535
+ },
43536
+ {
43537
+ pattern: { leadingTypes: [
43538
+ "categorical",
43539
+ NUMBER_OR_PERCENTAGE,
43540
+ NUMBER_OR_PERCENTAGE
43541
+ ] },
43542
+ buildContext: buildCategoricalTwoNumbersContext,
43543
+ suggestions: CATEGORICAL_TWO_NUMBERS_SUGGESTIONS
43544
+ }
43545
+ ];
43546
+ const EXTENDABLE_PATTERNS = [
43547
+ {
43548
+ pattern: {
43549
+ leadingTypes: [],
43550
+ extension: {
43551
+ type: NUMBER_OR_PERCENTAGE,
43552
+ minSize: 3
43553
+ }
43554
+ },
43555
+ buildContext: buildManyNumbersContext,
43556
+ suggestions: MANY_NUMBERS_SUGGESTIONS
43557
+ },
43558
+ {
43559
+ pattern: {
43560
+ leadingTypes: ["label"],
43561
+ extension: {
43562
+ type: NUMBER_OR_PERCENTAGE,
43563
+ minSize: 2
43564
+ }
43565
+ },
43566
+ buildContext: buildLabelVsMultipleNumbersContext,
43567
+ suggestions: LABEL_VS_MULTIPLE_NUMBERS_SUGGESTIONS
43568
+ },
43569
+ {
43570
+ pattern: {
43571
+ leadingTypes: ["categorical"],
43572
+ extension: {
43573
+ type: NUMBER_OR_PERCENTAGE,
43574
+ minSize: 2
43575
+ }
43576
+ },
43577
+ buildContext: buildCategoricalVsMultipleNumbersContext,
43578
+ suggestions: CATEGORICAL_VS_MULTIPLE_NUMBERS_SUGGESTIONS
43579
+ },
43580
+ {
43581
+ pattern: {
43582
+ leadingTypes: ["date"],
43583
+ extension: {
43584
+ type: NUMBER_OR_PERCENTAGE,
43585
+ minSize: 2
43586
+ }
43587
+ },
43588
+ buildContext: buildDateVsMultipleNumbersContext,
43589
+ suggestions: DATE_VS_MULTIPLE_NUMBERS_SUGGESTIONS
43590
+ }
43591
+ ];
43592
+ function getChartSuggestions(zones, getters) {
43593
+ const cols = analyzeColumns(zones, getters);
43594
+ if (cols.some((c) => c.type === "error")) return [];
43595
+ const nonEmpty = cols.filter((c) => c.type !== "empty");
43596
+ if (!nonEmpty.length) return [];
43597
+ const shape = nonEmpty.map((c) => c.type);
43598
+ const rule = EXACT_PATTERNS.find((rule) => matchesShape(rule.pattern, shape)) ?? EXTENDABLE_PATTERNS.find((rule) => matchesShape(rule.pattern, shape));
43599
+ if (!rule) return [];
43600
+ const ctx = rule.buildContext(nonEmpty, getters);
43601
+ return rule.suggestions.filter((suggestion) => suggestion.isApplicable?.(ctx) ?? true).map((suggestion) => ({
43602
+ description: suggestion.description.toString(),
43603
+ definition: suggestion.build(ctx)
43604
+ }));
43605
+ }
43606
+
43607
+ //#endregion
43608
+ //#region src/components/side_panel/data_analysis/data_analysis_store.ts
43609
+ var DataAnalysisStore = class extends SpreadsheetStore {
43610
+ mutators = [];
43611
+ hasData = false;
43612
+ chartSuggestions = [];
43613
+ isDirty = false;
43614
+ ranges;
43615
+ constructor(get) {
43616
+ super(get);
43617
+ this.model.selection.observe(this, { handleEvent: () => this.refresh() });
43618
+ this.onDispose(() => {
43619
+ this.model.selection.unobserve(this);
43620
+ });
43621
+ this.refresh();
43622
+ }
43623
+ handle(cmd) {
43624
+ if (invalidateEvaluationCommands.has(cmd.type) || cmd.type === "UPDATE_CELL" && ("content" in cmd || "format" in cmd)) this.isDirty = true;
43625
+ switch (cmd.type) {
43626
+ case "HIDE_COLUMNS_ROWS":
43627
+ case "UNHIDE_COLUMNS_ROWS":
43628
+ case "GROUP_HEADERS":
43629
+ case "UNGROUP_HEADERS":
43630
+ case "ACTIVATE_SHEET":
43631
+ case "ACTIVATE_NEXT_SHEET":
43632
+ case "ACTIVATE_PREVIOUS_SHEET":
43633
+ case "EVALUATE_CELLS":
43634
+ case "SET_FORMATTING":
43635
+ case "CLEAR_FORMATTING":
43636
+ this.isDirty = true;
43637
+ break;
43638
+ }
43639
+ }
43640
+ finalize() {
43641
+ if (this.isDirty) {
43642
+ this.refresh();
43643
+ this.isDirty = false;
43644
+ }
43645
+ }
43646
+ refresh() {
43647
+ const sheetId = this.getters.getActiveSheetId();
43648
+ const zones = this.getters.getSelectedZones();
43649
+ this.ranges = zones.map(zoneToXc);
43650
+ this.hasData = zones.some((zone) => this.getters.getEvaluatedCellsInZone(sheetId, zone).some((cell) => cell.type !== "empty"));
43651
+ const suggestions = this.hasData ? getChartSuggestions(zones, this.getters) : [];
43652
+ this.chartSuggestions = suggestions;
43653
+ }
43654
+ };
43655
+
43656
+ //#endregion
43657
+ //#region src/components/side_panel/data_analysis/data_analysis_panel.ts
43658
+ var DataAnalysisPanel = class extends Component {
43659
+ static template = "o-spreadsheet-DataAnalysisPanel";
43660
+ static components = {
43661
+ Section,
43662
+ ChartSuggestionPreview
43663
+ };
43664
+ store;
43665
+ setup() {
43666
+ this.store = useLocalStore(DataAnalysisStore);
43667
+ }
43668
+ onStartChartSuggestionDrag(definition, ev) {
43669
+ startChartDragAndDrop(this.env, definition, ev);
43670
+ }
43671
+ };
43672
+
42148
43673
  //#endregion
42149
43674
  //#region src/registries/data_source_registry.ts
42150
43675
  const unusedDataSourceRegistry = new Registry();
@@ -42221,7 +43746,7 @@ var DataSourcesCleanup = class extends Component {
42221
43746
  var DataValidationPreview = class extends Component {
42222
43747
  static template = "o-spreadsheet-DataValidationPreview";
42223
43748
  props = (0, _odoo_owl.useProps)({ rule: types.DataValidationRule() });
42224
- dvPreviewRef = (0, _odoo_owl.signal)(null);
43749
+ dvPreviewRef = _odoo_owl.signal.ref();
42225
43750
  setup() {
42226
43751
  useHighlightsOnHover(this.dvPreviewRef, this);
42227
43752
  }
@@ -43720,7 +45245,7 @@ var NamedRangePreview = class extends Component {
43720
45245
  };
43721
45246
  props = (0, _odoo_owl.useProps)({ namedRange: types.NamedRange() });
43722
45247
  state = (0, _odoo_owl.proxy)({});
43723
- namedRangePreviewRef = (0, _odoo_owl.signal)(null);
45248
+ namedRangePreviewRef = _odoo_owl.signal.ref();
43724
45249
  setup() {
43725
45250
  useHighlightsOnHover(this.namedRangePreviewRef, this);
43726
45251
  }
@@ -44211,7 +45736,7 @@ var FilterMenuValueItem = class extends Component {
44211
45736
  onClick: types.function(),
44212
45737
  scrolledTo: types.or([types.literal("top"), types.literal("bottom")]).optional()
44213
45738
  });
44214
- itemRef = (0, _odoo_owl.signal)(null);
45739
+ itemRef = _odoo_owl.signal.ref();
44215
45740
  setup() {
44216
45741
  (0, _odoo_owl.onWillPatch)(() => {
44217
45742
  if (this.props.scrolledTo) this.scrollListToSelectedValue();
@@ -44451,7 +45976,7 @@ var PivotFilterEditor = class extends Component {
44451
45976
  onFiltersUpdated: types.function()
44452
45977
  });
44453
45978
  state;
44454
- buttonFilter = (0, _odoo_owl.signal)(null);
45979
+ buttonFilter = _odoo_owl.signal.ref();
44455
45980
  popover;
44456
45981
  setup() {
44457
45982
  useExternalListener(window, "click", (ev) => {
@@ -44580,11 +46105,11 @@ var AddDimensionButton = class extends Component {
44580
46105
  onFieldPicked: types.function(),
44581
46106
  fields: types.array()
44582
46107
  });
44583
- buttonRef = (0, _odoo_owl.signal)(null);
46108
+ buttonRef = _odoo_owl.signal.ref(HTMLButtonElement);
44584
46109
  popover = (0, _odoo_owl.proxy)({ isOpen: false });
44585
46110
  search = (0, _odoo_owl.proxy)({ input: "" });
44586
46111
  autoComplete;
44587
- autofocusRef = (0, _odoo_owl.signal)(null);
46112
+ autofocusRef = _odoo_owl.signal.ref(HTMLInputElement);
44588
46113
  setup() {
44589
46114
  this.autoComplete = useLocalStore(AutoCompleteStore);
44590
46115
  this.autoComplete.useProvider(this.getProvider());
@@ -44993,7 +46518,7 @@ var PivotLayoutConfigurator = class extends Component {
44993
46518
  getScrollableContainerEl: types.function().optional(),
44994
46519
  pivotId: types.UID()
44995
46520
  });
44996
- dimensionsRef = (0, _odoo_owl.signal)(null);
46521
+ dimensionsRef = _odoo_owl.signal.ref();
44997
46522
  dragAndDrop = useDragAndDropListItems();
44998
46523
  AGGREGATORS = AGGREGATORS;
44999
46524
  isDateOrDatetimeField = isDateOrDatetimeField;
@@ -45064,16 +46589,7 @@ var PivotLayoutConfigurator = class extends Component {
45064
46589
  });
45065
46590
  }
45066
46591
  getDimensionElementsRects() {
45067
- return Array.from(this.dimensionsRef().children).map((el) => {
45068
- const style = getComputedStyle(el);
45069
- const rect = el.getBoundingClientRect();
45070
- return {
45071
- x: rect.x,
45072
- y: rect.y,
45073
- width: rect.width + parseInt(style.marginLeft || "0") + parseInt(style.marginRight || "0"),
45074
- height: rect.height + parseInt(style.marginTop || "0") + parseInt(style.marginBottom || "0")
45075
- };
45076
- });
46592
+ return Array.from(this.dimensionsRef().children).map((el) => getBoundingRectWithMargins(el));
45077
46593
  }
45078
46594
  removeDimension(dimension) {
45079
46595
  const { columns, rows } = this.props.definition;
@@ -45638,7 +47154,7 @@ var PivotSpreadsheetSidePanel = class extends Component {
45638
47154
  });
45639
47155
  store;
45640
47156
  state;
45641
- pivotSidePanelRef = (0, _odoo_owl.signal)(null);
47157
+ pivotSidePanelRef = _odoo_owl.signal.ref();
45642
47158
  setup() {
45643
47159
  this.store = useLocalStore(PivotSidePanelStore, this.props.pivotId);
45644
47160
  this.state = (0, _odoo_owl.proxy)({
@@ -46878,7 +48394,7 @@ var TableStylePreview = class extends Component {
46878
48394
  selected: types.boolean().optional(),
46879
48395
  onClick: types.function().optional()
46880
48396
  });
46881
- canvasRef = (0, _odoo_owl.signal)(null);
48397
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
46882
48398
  menu = (0, _odoo_owl.proxy)({
46883
48399
  isOpen: false,
46884
48400
  anchorRect: null,
@@ -46972,7 +48488,7 @@ var TableStylesPopover = class extends Component {
46972
48488
  tableStyles: types.RecordOf(),
46973
48489
  type: types.or([types.literal("table"), types.literal("pivot")])
46974
48490
  });
46975
- tableStyleListRef = (0, _odoo_owl.signal)(null);
48491
+ tableStyleListRef = _odoo_owl.signal.ref();
46976
48492
  state = (0, _odoo_owl.proxy)({ selectedCategory: this.initialSelectedCategory });
46977
48493
  setup() {
46978
48494
  useExternalListener(window, "click", this.onExternalClick, { capture: true });
@@ -48343,6 +49859,10 @@ sidePanelRegistry.add("ColumnStats", {
48343
49859
  title: _t("Column statistics"),
48344
49860
  Body: ColumnStatsPanel
48345
49861
  });
49862
+ sidePanelRegistry.add("DataAnalysisPanel", {
49863
+ title: _t("Data analysis"),
49864
+ Body: DataAnalysisPanel
49865
+ });
48346
49866
  sidePanelRegistry.add("TableSidePanel", {
48347
49867
  title: _t("Edit table"),
48348
49868
  Body: TablePanel,
@@ -48450,6 +49970,7 @@ var SidePanelStore = class extends SpreadsheetStore {
48450
49970
  "changePanelSize",
48451
49971
  "resetPanelSize",
48452
49972
  "togglePinPanel",
49973
+ "togglePinnedSidePanel",
48453
49974
  "closeMainPanel",
48454
49975
  "changeSpreadsheetWidth",
48455
49976
  "toggleCollapsePanel"
@@ -48601,6 +50122,19 @@ var SidePanelStore = class extends SpreadsheetStore {
48601
50122
  this.secondaryPanel = void 0;
48602
50123
  }
48603
50124
  }
50125
+ togglePinnedSidePanel(componentTag, panelProps = {}) {
50126
+ if (this.mainPanel?.componentTag === componentTag && this.isMainPanelOpen) {
50127
+ this.closeMainPanel();
50128
+ return;
50129
+ }
50130
+ if (this.secondaryPanel?.componentTag === componentTag && this.isSecondaryPanelOpen) {
50131
+ this.secondaryPanel?.currentPanelProps.onCloseSidePanel?.();
50132
+ this.secondaryPanel = void 0;
50133
+ return;
50134
+ }
50135
+ this.open(componentTag, panelProps);
50136
+ if (!this.mainPanel?.isPinned) this.togglePinPanel();
50137
+ }
48604
50138
  toggleCollapsePanel(panel) {
48605
50139
  const panelInfo = this[panel];
48606
50140
  if (!panelInfo) return;
@@ -48979,6 +50513,219 @@ var MenuItemRegistry = class extends Registry {
48979
50513
  }
48980
50514
  };
48981
50515
 
50516
+ //#endregion
50517
+ //#region src/helpers/sort_interactive.ts
50518
+ function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) {
50519
+ let multiColumns = zone.right > zone.left;
50520
+ if (env.model.getters.doesIntersectMerge(sheetId, zone)) {
50521
+ multiColumns = false;
50522
+ let table;
50523
+ for (let row = zone.top; row <= zone.bottom; row++) {
50524
+ table = [];
50525
+ for (let col = zone.left; col <= zone.right; col++) {
50526
+ const merge = env.model.getters.getMerge({
50527
+ sheetId,
50528
+ col,
50529
+ row
50530
+ });
50531
+ if (merge && !table.includes(merge.id.toString())) table.push(merge.id.toString());
50532
+ }
50533
+ if (table.length >= 2) {
50534
+ multiColumns = true;
50535
+ break;
50536
+ }
50537
+ }
50538
+ }
50539
+ if (multiColumns) {
50540
+ interactiveSort(env, sheetId, anchor, zone, sortDirection);
50541
+ return;
50542
+ }
50543
+ const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);
50544
+ if (isEqual(contiguousZone, zone)) interactiveSort(env, sheetId, anchor, zone, sortDirection);
50545
+ 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));
50546
+ }
50547
+ function interactiveSort(env, sheetId, anchor, zone, sortDirection, sortOptions) {
50548
+ const result = env.model.dispatch("SORT_CELLS", {
50549
+ sheetId,
50550
+ col: anchor.col,
50551
+ row: anchor.row,
50552
+ zone,
50553
+ sortDirection,
50554
+ sortOptions
50555
+ });
50556
+ if (result.isCancelledBecause("InvalidSortZone")) {
50557
+ const { col, row } = anchor;
50558
+ env.model.selection.selectZone({
50559
+ cell: {
50560
+ col,
50561
+ row
50562
+ },
50563
+ zone
50564
+ });
50565
+ env.raiseError(_t("Cannot sort. To sort, select only cells or only merges that have the same size."));
50566
+ }
50567
+ if (result.isCancelledBecause("SortZoneWithArrayFormulas")) {
50568
+ const { col, row } = anchor;
50569
+ env.model.selection.selectZone({
50570
+ cell: {
50571
+ col,
50572
+ row
50573
+ },
50574
+ zone
50575
+ });
50576
+ env.raiseError(_t("Cannot sort a zone with array formulas."));
50577
+ }
50578
+ }
50579
+
50580
+ //#endregion
50581
+ //#region src/stores/trim_whitespace_store.ts
50582
+ var TrimWhitespaceStore = class extends SpreadsheetStore {
50583
+ mutators = ["trimWhitespace"];
50584
+ notificationStore = this.get(NotificationStore);
50585
+ trimWhitespace() {
50586
+ this.model.dispatch("TRIM_WHITESPACE");
50587
+ }
50588
+ handle(cmd) {
50589
+ switch (cmd.type) {
50590
+ case "TRIM_WHITESPACE":
50591
+ this._trimWhitespace();
50592
+ break;
50593
+ }
50594
+ }
50595
+ _trimWhitespace() {
50596
+ const zones = recomputeZones(this.getters.getSelectedZones());
50597
+ const sheetId = this.getters.getActiveSheetId();
50598
+ let count = 0;
50599
+ for (const { col, row } of zones.map(positions).flat()) {
50600
+ const cell = this.getters.getCell({
50601
+ col,
50602
+ row,
50603
+ sheetId
50604
+ });
50605
+ if (!cell) continue;
50606
+ const currentContent = !cell.isFormula ? cell.content : cell.compiledFormula.toFormulaString(this.getters);
50607
+ const trimmedContent = trimContent(currentContent);
50608
+ if (trimmedContent !== currentContent) {
50609
+ count += 1;
50610
+ this.model.dispatch("UPDATE_CELL", {
50611
+ sheetId,
50612
+ col,
50613
+ row,
50614
+ content: trimmedContent
50615
+ });
50616
+ }
50617
+ }
50618
+ const text = count ? _t("Trimmed whitespace from %s cells.", count) : _t("No selected cells had whitespace trimmed.");
50619
+ this.notificationStore.notifyUser({
50620
+ type: "info",
50621
+ text,
50622
+ sticky: false
50623
+ });
50624
+ }
50625
+ };
50626
+
50627
+ //#endregion
50628
+ //#region src/actions/data_actions.ts
50629
+ const sortRange = {
50630
+ name: _t("Sort range"),
50631
+ isVisible: IS_ONLY_ONE_RANGE,
50632
+ icon: "o-spreadsheet-Icon.SORT_RANGE"
50633
+ };
50634
+ const sortAscending = {
50635
+ name: _t("Ascending (A ⟶ Z)"),
50636
+ execute: (env) => {
50637
+ const { anchor, zones } = env.model.getters.getSelection();
50638
+ interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "asc");
50639
+ },
50640
+ icon: "o-spreadsheet-Icon.SORT_ASCENDING"
50641
+ };
50642
+ const dataCleanup = {
50643
+ name: _t("Data cleanup"),
50644
+ icon: "o-spreadsheet-Icon.DATA_CLEANUP"
50645
+ };
50646
+ const removeDuplicates = {
50647
+ name: _t("Remove duplicates"),
50648
+ execute: (env) => {
50649
+ if (getZoneArea(env.model.getters.getSelectedZone()) === 1) env.model.selection.selectTableAroundSelection();
50650
+ env.openSidePanel("RemoveDuplicates", {});
50651
+ },
50652
+ isEnabled: (env) => !env.isSmall
50653
+ };
50654
+ const trimWhitespace = {
50655
+ name: _t("Trim whitespace"),
50656
+ execute: (env) => {
50657
+ env.getStore(TrimWhitespaceStore).trimWhitespace();
50658
+ }
50659
+ };
50660
+ const cleanupDataSources = {
50661
+ name: _t("Remove unused data sources"),
50662
+ execute: (env) => env.openSidePanel("DataSourceCleanup", {}),
50663
+ isEnabled: (env) => !env.isSmall
50664
+ };
50665
+ const sortDescending = {
50666
+ name: _t("Descending (Z ⟶ A)"),
50667
+ execute: (env) => {
50668
+ const { anchor, zones } = env.model.getters.getSelection();
50669
+ interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "desc");
50670
+ },
50671
+ icon: "o-spreadsheet-Icon.SORT_DESCENDING"
50672
+ };
50673
+ const createRemoveFilter = { ...CREATE_OR_REMOVE_FILTER_ACTION };
50674
+ const createRemoveFilterTool = {
50675
+ ...CREATE_OR_REMOVE_FILTER_ACTION,
50676
+ isActive: (env) => SELECTED_TABLE_HAS_FILTERS(env)
50677
+ };
50678
+ const splitToColumns = {
50679
+ name: _t("Split text to columns"),
50680
+ sequence: 1,
50681
+ execute: (env) => env.openSidePanel("SplitToColumns", {}),
50682
+ isEnabled: (env) => !env.isSmall && env.model.getters.isSingleColSelected(),
50683
+ icon: "o-spreadsheet-Icon.SPLIT_TEXT"
50684
+ };
50685
+ const dataAnalysis = {
50686
+ name: _t("Data analysis"),
50687
+ execute: (env) => {
50688
+ env.getStore(SidePanelStore).togglePinnedSidePanel("DataAnalysisPanel");
50689
+ },
50690
+ icon: "o-spreadsheet-Icon.COLUMN_STATS",
50691
+ isEnabled: (env) => !env.isSmall
50692
+ };
50693
+ const columnStatistics = {
50694
+ name: _t("Column statistics"),
50695
+ execute: (env) => env.openSidePanel("ColumnStats", {}),
50696
+ icon: "o-spreadsheet-Icon.COLUMN_STATS"
50697
+ };
50698
+ const reinsertDynamicPivotMenu = {
50699
+ id: "reinsert_dynamic_pivot",
50700
+ name: _t("Re-insert dynamic pivot"),
50701
+ sequence: 60,
50702
+ icon: "o-spreadsheet-Icon.INSERT_PIVOT",
50703
+ children: [REINSERT_DYNAMIC_PIVOT_CHILDREN],
50704
+ isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
50705
+ };
50706
+ const reinsertStaticPivotMenu = {
50707
+ id: "reinsert_static_pivot",
50708
+ name: _t("Re-insert static pivot"),
50709
+ sequence: 70,
50710
+ icon: "o-spreadsheet-Icon.INSERT_PIVOT",
50711
+ children: [REINSERT_STATIC_PIVOT_CHILDREN],
50712
+ isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
50713
+ };
50714
+ const calculationMode = {
50715
+ name: _t("Calculation"),
50716
+ icon: "o-spreadsheet-Icon.CALCULATOR"
50717
+ };
50718
+ const automaticCalculation = {
50719
+ name: _t("Automatic"),
50720
+ execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: true }),
50721
+ isActive: (env) => env.model.getters.isAutomaticEvaluationEnabled()
50722
+ };
50723
+ const manualCalculation = {
50724
+ name: _t("Manual"),
50725
+ execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: false }),
50726
+ isActive: (env) => !env.model.getters.isAutomaticEvaluationEnabled()
50727
+ };
50728
+
48982
50729
  //#endregion
48983
50730
  //#region src/helpers/ui/merge_interactive.ts
48984
50731
  const AddMergeInteractiveContent = {
@@ -49193,6 +50940,10 @@ cellMenuRegistry.add("cut", {
49193
50940
  }).addChild("paste_format_only", ["paste_special"], {
49194
50941
  ...pasteSpecialFormat,
49195
50942
  sequence: 20
50943
+ }).add("data_analysis", {
50944
+ ...dataAnalysis,
50945
+ sequence: 50,
50946
+ separator: true
49196
50947
  }).add("add_row_before", {
49197
50948
  ...cellInsertRowsBefore,
49198
50949
  sequence: 70
@@ -49300,211 +51051,6 @@ cellMenuRegistry.add("cut", {
49300
51051
  sequence: 30
49301
51052
  });
49302
51053
 
49303
- //#endregion
49304
- //#region src/helpers/sort_interactive.ts
49305
- function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) {
49306
- let multiColumns = zone.right > zone.left;
49307
- if (env.model.getters.doesIntersectMerge(sheetId, zone)) {
49308
- multiColumns = false;
49309
- let table;
49310
- for (let row = zone.top; row <= zone.bottom; row++) {
49311
- table = [];
49312
- for (let col = zone.left; col <= zone.right; col++) {
49313
- const merge = env.model.getters.getMerge({
49314
- sheetId,
49315
- col,
49316
- row
49317
- });
49318
- if (merge && !table.includes(merge.id.toString())) table.push(merge.id.toString());
49319
- }
49320
- if (table.length >= 2) {
49321
- multiColumns = true;
49322
- break;
49323
- }
49324
- }
49325
- }
49326
- if (multiColumns) {
49327
- interactiveSort(env, sheetId, anchor, zone, sortDirection);
49328
- return;
49329
- }
49330
- const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);
49331
- if (isEqual(contiguousZone, zone)) interactiveSort(env, sheetId, anchor, zone, sortDirection);
49332
- 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));
49333
- }
49334
- function interactiveSort(env, sheetId, anchor, zone, sortDirection, sortOptions) {
49335
- const result = env.model.dispatch("SORT_CELLS", {
49336
- sheetId,
49337
- col: anchor.col,
49338
- row: anchor.row,
49339
- zone,
49340
- sortDirection,
49341
- sortOptions
49342
- });
49343
- if (result.isCancelledBecause("InvalidSortZone")) {
49344
- const { col, row } = anchor;
49345
- env.model.selection.selectZone({
49346
- cell: {
49347
- col,
49348
- row
49349
- },
49350
- zone
49351
- });
49352
- env.raiseError(_t("Cannot sort. To sort, select only cells or only merges that have the same size."));
49353
- }
49354
- if (result.isCancelledBecause("SortZoneWithArrayFormulas")) {
49355
- const { col, row } = anchor;
49356
- env.model.selection.selectZone({
49357
- cell: {
49358
- col,
49359
- row
49360
- },
49361
- zone
49362
- });
49363
- env.raiseError(_t("Cannot sort a zone with array formulas."));
49364
- }
49365
- }
49366
-
49367
- //#endregion
49368
- //#region src/stores/trim_whitespace_store.ts
49369
- var TrimWhitespaceStore = class extends SpreadsheetStore {
49370
- mutators = ["trimWhitespace"];
49371
- notificationStore = this.get(NotificationStore);
49372
- trimWhitespace() {
49373
- this.model.dispatch("TRIM_WHITESPACE");
49374
- }
49375
- handle(cmd) {
49376
- switch (cmd.type) {
49377
- case "TRIM_WHITESPACE":
49378
- this._trimWhitespace();
49379
- break;
49380
- }
49381
- }
49382
- _trimWhitespace() {
49383
- const zones = recomputeZones(this.getters.getSelectedZones());
49384
- const sheetId = this.getters.getActiveSheetId();
49385
- let count = 0;
49386
- for (const { col, row } of zones.map(positions).flat()) {
49387
- const cell = this.getters.getCell({
49388
- col,
49389
- row,
49390
- sheetId
49391
- });
49392
- if (!cell) continue;
49393
- const currentContent = !cell.isFormula ? cell.content : cell.compiledFormula.toFormulaString(this.getters);
49394
- const trimmedContent = trimContent(currentContent);
49395
- if (trimmedContent !== currentContent) {
49396
- count += 1;
49397
- this.model.dispatch("UPDATE_CELL", {
49398
- sheetId,
49399
- col,
49400
- row,
49401
- content: trimmedContent
49402
- });
49403
- }
49404
- }
49405
- const text = count ? _t("Trimmed whitespace from %s cells.", count) : _t("No selected cells had whitespace trimmed.");
49406
- this.notificationStore.notifyUser({
49407
- type: "info",
49408
- text,
49409
- sticky: false
49410
- });
49411
- }
49412
- };
49413
-
49414
- //#endregion
49415
- //#region src/actions/data_actions.ts
49416
- const sortRange = {
49417
- name: _t("Sort range"),
49418
- isVisible: IS_ONLY_ONE_RANGE,
49419
- icon: "o-spreadsheet-Icon.SORT_RANGE"
49420
- };
49421
- const sortAscending = {
49422
- name: _t("Ascending (A ⟶ Z)"),
49423
- execute: (env) => {
49424
- const { anchor, zones } = env.model.getters.getSelection();
49425
- interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "asc");
49426
- },
49427
- icon: "o-spreadsheet-Icon.SORT_ASCENDING"
49428
- };
49429
- const dataCleanup = {
49430
- name: _t("Data cleanup"),
49431
- icon: "o-spreadsheet-Icon.DATA_CLEANUP"
49432
- };
49433
- const removeDuplicates = {
49434
- name: _t("Remove duplicates"),
49435
- execute: (env) => {
49436
- if (getZoneArea(env.model.getters.getSelectedZone()) === 1) env.model.selection.selectTableAroundSelection();
49437
- env.openSidePanel("RemoveDuplicates", {});
49438
- },
49439
- isEnabled: (env) => !env.isSmall
49440
- };
49441
- const trimWhitespace = {
49442
- name: _t("Trim whitespace"),
49443
- execute: (env) => {
49444
- env.getStore(TrimWhitespaceStore).trimWhitespace();
49445
- }
49446
- };
49447
- const cleanupDataSources = {
49448
- name: _t("Remove unused data sources"),
49449
- execute: (env) => env.openSidePanel("DataSourceCleanup", {}),
49450
- isEnabled: (env) => !env.isSmall
49451
- };
49452
- const sortDescending = {
49453
- name: _t("Descending (Z ⟶ A)"),
49454
- execute: (env) => {
49455
- const { anchor, zones } = env.model.getters.getSelection();
49456
- interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "desc");
49457
- },
49458
- icon: "o-spreadsheet-Icon.SORT_DESCENDING"
49459
- };
49460
- const createRemoveFilter = { ...CREATE_OR_REMOVE_FILTER_ACTION };
49461
- const createRemoveFilterTool = {
49462
- ...CREATE_OR_REMOVE_FILTER_ACTION,
49463
- isActive: (env) => SELECTED_TABLE_HAS_FILTERS(env)
49464
- };
49465
- const splitToColumns = {
49466
- name: _t("Split text to columns"),
49467
- sequence: 1,
49468
- execute: (env) => env.openSidePanel("SplitToColumns", {}),
49469
- isEnabled: (env) => !env.isSmall && env.model.getters.isSingleColSelected(),
49470
- icon: "o-spreadsheet-Icon.SPLIT_TEXT"
49471
- };
49472
- const columnStatistics = {
49473
- name: _t("Column statistics"),
49474
- execute: (env) => env.openSidePanel("ColumnStats", {}),
49475
- icon: "o-spreadsheet-Icon.COLUMN_STATS"
49476
- };
49477
- const reinsertDynamicPivotMenu = {
49478
- id: "reinsert_dynamic_pivot",
49479
- name: _t("Re-insert dynamic pivot"),
49480
- sequence: 60,
49481
- icon: "o-spreadsheet-Icon.INSERT_PIVOT",
49482
- children: [REINSERT_DYNAMIC_PIVOT_CHILDREN],
49483
- isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
49484
- };
49485
- const reinsertStaticPivotMenu = {
49486
- id: "reinsert_static_pivot",
49487
- name: _t("Re-insert static pivot"),
49488
- sequence: 70,
49489
- icon: "o-spreadsheet-Icon.INSERT_PIVOT",
49490
- children: [REINSERT_STATIC_PIVOT_CHILDREN],
49491
- isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
49492
- };
49493
- const calculationMode = {
49494
- name: _t("Calculation"),
49495
- icon: "o-spreadsheet-Icon.CALCULATOR"
49496
- };
49497
- const automaticCalculation = {
49498
- name: _t("Automatic"),
49499
- execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: true }),
49500
- isActive: (env) => env.model.getters.isAutomaticEvaluationEnabled()
49501
- };
49502
- const manualCalculation = {
49503
- name: _t("Manual"),
49504
- execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: false }),
49505
- isActive: (env) => !env.model.getters.isAutomaticEvaluationEnabled()
49506
- };
49507
-
49508
51054
  //#endregion
49509
51055
  //#region src/registries/menus/col_menu_registry.ts
49510
51056
  const colMenuRegistry = new MenuItemRegistry();
@@ -49538,6 +51084,9 @@ colMenuRegistry.add("cut", {
49538
51084
  }).addChild("sort_descending", ["sort_columns"], {
49539
51085
  ...sortDescending,
49540
51086
  sequence: 20
51087
+ }).add("data_analysis", {
51088
+ ...dataAnalysis,
51089
+ sequence: 55
49541
51090
  }).add("column_statistics", {
49542
51091
  ...columnStatistics,
49543
51092
  sequence: 60,
@@ -52353,14 +53902,15 @@ var FiguresContainer = class extends Component {
52353
53902
  selectedRect: void 0,
52354
53903
  horizontalSnap: void 0,
52355
53904
  verticalSnap: void 0,
52356
- cancelDnd: void 0,
52357
- overlappingChartOrCarousel: void 0
53905
+ cancelDnd: void 0
52358
53906
  });
52359
53907
  viewStore;
52360
53908
  zoomStore;
53909
+ chartDragStore;
52361
53910
  setup() {
52362
53911
  this.viewStore = useStore(ViewportsStore);
52363
53912
  this.zoomStore = useStore(ZoomStore);
53913
+ this.chartDragStore = useStore(ChartDragStore);
52364
53914
  (0, _odoo_owl.onMounted)(() => {
52365
53915
  this.render();
52366
53916
  });
@@ -52374,7 +53924,7 @@ var FiguresContainer = class extends Component {
52374
53924
  this.dnd.selectedRect = void 0;
52375
53925
  this.dnd.horizontalSnap = void 0;
52376
53926
  this.dnd.verticalSnap = void 0;
52377
- this.dnd.overlappingChartOrCarousel = void 0;
53927
+ this.chartDragStore.setHighlightedFigure(void 0);
52378
53928
  this.dnd.cancelDnd = void 0;
52379
53929
  }
52380
53930
  });
@@ -52500,6 +54050,7 @@ var FiguresContainer = class extends Component {
52500
54050
  maxY: this.env.model.getters.getRowDimensions(sheetId, this.env.model.getters.getNumberRows(sheetId) - 1).end
52501
54051
  };
52502
54052
  let hasStartedDnd = false;
54053
+ let overlappingChartOrCarousel = void 0;
52503
54054
  const onMouseMove = (ev) => {
52504
54055
  const currentMousePosition = {
52505
54056
  x: ev.clientX / zoom,
@@ -52511,10 +54062,10 @@ var FiguresContainer = class extends Component {
52511
54062
  hasStartedDnd = true;
52512
54063
  const selectedFigures = dragFigureForMove(currentMousePosition, initialMousePosition, initialFigures, maxDimensions, initialScrollPosition, this.viewStore.activeSheetScrollInfo);
52513
54064
  const draggedFigure = selectedFigures.find((f) => f.id === draggedFigureId);
52514
- let overlappingChartOrCarousel = void 0;
54065
+ overlappingChartOrCarousel = void 0;
52515
54066
  const otherFigures = this.getOtherFigures(selectedFigures.map((f) => f.id));
52516
- if (draggedFigure && !selectedFigures.find((f) => f.tag !== "chart")) overlappingChartOrCarousel = this.getOverlappingFigure(draggedFigure, otherFigures);
52517
- this.dnd.overlappingChartOrCarousel = overlappingChartOrCarousel;
54067
+ if (draggedFigure && !selectedFigures.find((f) => f.tag !== "chart")) overlappingChartOrCarousel = getCarouselOverlappingChart(draggedFigure, otherFigures, ["carousel", "chart"]);
54068
+ this.chartDragStore.setHighlightedFigure(overlappingChartOrCarousel?.id);
52518
54069
  if (!overlappingChartOrCarousel) {
52519
54070
  const snapReturn = snapForMove(this.env, selectedFigures, otherFigures);
52520
54071
  this.dnd.selectedFigures = snapReturn.snappedFigures;
@@ -52536,7 +54087,7 @@ var FiguresContainer = class extends Component {
52536
54087
  else this.env.model.dispatch("SELECT_FIGURE", { figureId: figureUI.id });
52537
54088
  return;
52538
54089
  }
52539
- if (!this.dnd.overlappingChartOrCarousel) {
54090
+ if (!overlappingChartOrCarousel) {
52540
54091
  const payloads = this.dnd.selectedFigures?.map((f) => {
52541
54092
  return {
52542
54093
  sheetId,
@@ -52546,14 +54097,14 @@ var FiguresContainer = class extends Component {
52546
54097
  }) || [];
52547
54098
  this.env.model.dispatch("MOVE_FIGURES", { figures: payloads });
52548
54099
  } else {
52549
- const overlappingFigureId = this.dnd.overlappingChartOrCarousel.id;
54100
+ const overlappingFigureId = overlappingChartOrCarousel.id;
52550
54101
  const chartFigureIds = this.dnd.selectedFigures?.map((f) => f.id) || [];
52551
- if (this.dnd.overlappingChartOrCarousel.tag === "carousel") this.env.model.dispatch("ADD_FIGURES_CHART_TO_CAROUSEL", {
54102
+ if (overlappingChartOrCarousel.tag === "carousel") this.env.model.dispatch("ADD_FIGURES_CHART_TO_CAROUSEL", {
52552
54103
  sheetId,
52553
54104
  carouselFigureId: overlappingFigureId,
52554
54105
  chartFigureIds
52555
54106
  });
52556
- else if (this.dnd.overlappingChartOrCarousel.tag === "chart") this.env.model.dispatch("MERGE_CHART_FIGURES_INTO_CAROUSEL", {
54107
+ else if (overlappingChartOrCarousel.tag === "chart") this.env.model.dispatch("MERGE_CHART_FIGURES_INTO_CAROUSEL", {
52557
54108
  sheetId,
52558
54109
  baseFigureId: overlappingFigureId,
52559
54110
  chartFigureIds: [overlappingFigureId, ...chartFigureIds]
@@ -52564,7 +54115,7 @@ var FiguresContainer = class extends Component {
52564
54115
  this.dnd.selectedRect = void 0;
52565
54116
  this.dnd.horizontalSnap = void 0;
52566
54117
  this.dnd.verticalSnap = void 0;
52567
- this.dnd.overlappingChartOrCarousel = void 0;
54118
+ this.chartDragStore.setHighlightedFigure(void 0);
52568
54119
  };
52569
54120
  this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
52570
54121
  }
@@ -52620,7 +54171,6 @@ var FiguresContainer = class extends Component {
52620
54171
  this.dnd.selectedRect = void 0;
52621
54172
  this.dnd.horizontalSnap = void 0;
52622
54173
  this.dnd.verticalSnap = void 0;
52623
- this.dnd.overlappingChartOrCarousel = void 0;
52624
54174
  };
52625
54175
  this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
52626
54176
  }
@@ -52630,12 +54180,12 @@ var FiguresContainer = class extends Component {
52630
54180
  getFigureStyle(figureUI) {
52631
54181
  if (figureUI.id !== this.dnd.draggedFigure?.id) return "";
52632
54182
  return cssPropertiesToCss({
52633
- opacity: this.dnd.overlappingChartOrCarousel?.id ? "0.6" : "0.9",
54183
+ opacity: this.chartDragStore.highlightedFigureId ? "0.6" : "0.9",
52634
54184
  cursor: "grabbing"
52635
54185
  });
52636
54186
  }
52637
54187
  getFigureClass(figureUI) {
52638
- if (figureUI.id !== this.dnd.overlappingChartOrCarousel?.id) return "";
54188
+ if (figureUI.id !== this.chartDragStore.highlightedFigureId) return "";
52639
54189
  return "o-add-to-carousel";
52640
54190
  }
52641
54191
  getSnap(snapLine) {
@@ -52678,26 +54228,6 @@ var FiguresContainer = class extends Component {
52678
54228
  height: `100%`
52679
54229
  });
52680
54230
  }
52681
- getOverlappingFigure(figureUI, otherFigures) {
52682
- if (figureUI.tag !== "chart") return;
52683
- const figureCenterX = figureUI.x + figureUI.width / 2;
52684
- const figureCenterY = figureUI.y + figureUI.height / 2;
52685
- let bestMatch;
52686
- let smallestDistance = Infinity;
52687
- for (const figure of otherFigures) {
52688
- if (figure.tag !== "chart" && figure.tag !== "carousel") continue;
52689
- const targetCenterX = figure.x + figure.width / 2;
52690
- const targetCenterY = figure.y + figure.height / 2;
52691
- const distanceX = Math.abs(figureCenterX - targetCenterX);
52692
- const distanceY = Math.abs(figureCenterY - targetCenterY);
52693
- const squaredDistance = distanceX ** 2 + distanceY ** 2;
52694
- if (distanceX <= figureUI.width / 2 && distanceY <= figureUI.height / 2 && squaredDistance < smallestDistance) {
52695
- smallestDistance = squaredDistance;
52696
- bestMatch = figure;
52697
- }
52698
- }
52699
- return bestMatch;
52700
- }
52701
54231
  };
52702
54232
 
52703
54233
  //#endregion
@@ -52997,7 +54527,7 @@ var AbstractResizer = class extends Component {
52997
54527
  var ColResizer = class extends AbstractResizer {
52998
54528
  static template = "o-spreadsheet-ColResizer";
52999
54529
  static components = { UnhideColumnHeaders };
53000
- colResizerRef = (0, _odoo_owl.signal)(null);
54530
+ colResizerRef = _odoo_owl.signal.ref();
53001
54531
  setup() {
53002
54532
  super.setup();
53003
54533
  this.PADDING = 15;
@@ -53131,7 +54661,7 @@ var ColResizer = class extends AbstractResizer {
53131
54661
  var RowResizer = class extends AbstractResizer {
53132
54662
  static template = "o-spreadsheet-RowResizer";
53133
54663
  static components = { UnhideRowHeaders };
53134
- rowResizerRef = (0, _odoo_owl.signal)(null);
54664
+ rowResizerRef = _odoo_owl.signal.ref();
53135
54665
  setup() {
53136
54666
  super.setup();
53137
54667
  this.PADDING = 5;
@@ -53740,9 +55270,8 @@ var TableResizer = class extends Component {
53740
55270
  useStore(TableResizeStore);
53741
55271
  }
53742
55272
  get containerStyle() {
53743
- const tableZone = this.props.table.range.zone;
53744
55273
  const sheetId = this.props.table.range.sheetId;
53745
- if (this.env.model.getters.isReadonly() || this.env.model.getters.isSheetLocked(sheetId)) return cssPropertiesToCss({ display: "none" });
55274
+ const tableZone = this.props.table.range.zone;
53746
55275
  const bottomRight = {
53747
55276
  ...tableZone,
53748
55277
  left: tableZone.right,
@@ -53829,8 +55358,8 @@ var Grid = class extends Component {
53829
55358
  HEADER_HEIGHT = 26;
53830
55359
  HEADER_WIDTH = 48;
53831
55360
  menuState;
53832
- gridRef = (0, _odoo_owl.signal)(null);
53833
- canvasRef = (0, _odoo_owl.signal)(null);
55361
+ gridRef = _odoo_owl.signal.ref();
55362
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
53834
55363
  highlightStore;
53835
55364
  viewStore;
53836
55365
  zoomStore;
@@ -54461,6 +55990,9 @@ var Grid = class extends Component {
54461
55990
  const sheetId = this.env.model.getters.getActiveSheetId();
54462
55991
  return this.env.model.getters.getClientsToDisplay(sheetId);
54463
55992
  }
55993
+ get displayTableResizer() {
55994
+ return this.env.model.getters.isGridSelectionActive() && !this.env.model.getters.isReadonly() && !this.env.model.getters.isSheetLocked(this.env.model.getters.getActiveSheetId());
55995
+ }
54464
55996
  };
54465
55997
 
54466
55998
  //#endregion
@@ -54479,7 +56011,7 @@ var FullScreenFigure = class extends Component {
54479
56011
  static template = "o-spreadsheet-FullScreenFigure";
54480
56012
  static components = { ChartFigure };
54481
56013
  fullScreenFigureStore;
54482
- fullScreenFigureRef = (0, _odoo_owl.signal)(null);
56014
+ fullScreenFigureRef = _odoo_owl.signal.ref();
54483
56015
  spreadsheetRect = useSpreadsheetRect();
54484
56016
  figureRegistry = figureRegistry;
54485
56017
  setup() {
@@ -54727,6 +56259,41 @@ var PivotHTMLRenderer = class extends Component {
54727
56259
  }
54728
56260
  };
54729
56261
 
56262
+ //#endregion
56263
+ //#region src/components/tables/hovered_table_store.ts
56264
+ var HoveredTableStore = class extends SpreadsheetStore {
56265
+ mutators = ["hover"];
56266
+ storeGetters = ["getCellHoverOverlayColor"];
56267
+ position;
56268
+ constructor(get) {
56269
+ super(get);
56270
+ const cellHoverOverlayStore = this.get(CellHoverOverlayStore);
56271
+ cellHoverOverlayStore.register(this);
56272
+ this.onDispose(() => cellHoverOverlayStore.unRegister(this));
56273
+ }
56274
+ getHighlightedPositions(hoveredPosition) {
56275
+ const highlightedPositions = [];
56276
+ const { sheetId, row } = hoveredPosition;
56277
+ const table = this.getters.getTable(hoveredPosition);
56278
+ if (!table) return highlightedPositions;
56279
+ const { left, right, top } = table.range.zone;
56280
+ const isTableHeader = row < top + table.config.numberOfHeaders;
56281
+ const doesTableRowHaveContent = range(left, right + 1).some((col) => {
56282
+ return !this.getters.isColHidden(sheetId, col) && this.getters.getEvaluatedCell({
56283
+ sheetId,
56284
+ col,
56285
+ row
56286
+ }).formattedValue;
56287
+ });
56288
+ if (!isTableHeader && doesTableRowHaveContent) for (let col = left; col <= right; col++) highlightedPositions.push({
56289
+ sheetId,
56290
+ col,
56291
+ row
56292
+ });
56293
+ return highlightedPositions;
56294
+ }
56295
+ };
56296
+
54730
56297
  //#endregion
54731
56298
  //#region src/helpers/figures/charts/runtime/helpers_index.ts
54732
56299
  var helpers_index_exports = /* @__PURE__ */ __exportAll({
@@ -68013,6 +69580,12 @@ var FigureUIPlugin = class extends UIPlugin {
68013
69580
  const figures = cmd.chartFigureIds.map((id) => this.getters.getFigure(cmd.sheetId, id));
68014
69581
  const baseFigureId = this.getters.getFigure(cmd.sheetId, cmd.baseFigureId);
68015
69582
  if (figures.some((f) => f === void 0 || f.tag !== "chart") || !baseFigureId || baseFigureId.tag !== "chart") return "FigureDoesNotExist";
69583
+ break;
69584
+ case "CREATE_CHART_AND_MERGE_INTO_CAROUSEL":
69585
+ const baseFigure = this.getters.getFigure(cmd.sheetId, cmd.baseFigureId);
69586
+ if (this.getters.getFigure(cmd.sheetId, cmd.figureId) || !baseFigure) return "InvalidFigureId";
69587
+ if (baseFigure.tag !== "chart") return "FigureDoesNotExist";
69588
+ break;
68016
69589
  }
68017
69590
  return "Success";
68018
69591
  }
@@ -68048,6 +69621,29 @@ var FigureUIPlugin = class extends UIPlugin {
68048
69621
  carouselFigureId,
68049
69622
  chartFigureIds: cmd.chartFigureIds
68050
69623
  });
69624
+ break;
69625
+ case "CREATE_CHART_AND_MERGE_INTO_CAROUSEL":
69626
+ const baseFigureToMerge = this.getters.getFigure(cmd.sheetId, cmd.baseFigureId);
69627
+ if (!baseFigureToMerge) throw new Error(`Figure ${cmd.baseFigureId} does not exists.`);
69628
+ this.dispatch("CREATE_CHART", {
69629
+ chartId: cmd.chartId,
69630
+ figureId: cmd.figureId,
69631
+ sheetId: cmd.sheetId,
69632
+ definition: cmd.definition,
69633
+ col: baseFigureToMerge.col,
69634
+ row: baseFigureToMerge.row,
69635
+ offset: baseFigureToMerge.offset,
69636
+ size: {
69637
+ width: baseFigureToMerge.width,
69638
+ height: baseFigureToMerge.height
69639
+ }
69640
+ });
69641
+ this.dispatch("MERGE_CHART_FIGURES_INTO_CAROUSEL", {
69642
+ sheetId: cmd.sheetId,
69643
+ baseFigureId: cmd.baseFigureId,
69644
+ chartFigureIds: [cmd.baseFigureId, cmd.figureId]
69645
+ });
69646
+ break;
68051
69647
  }
68052
69648
  }
68053
69649
  getFigureUI(sheetId, figure) {
@@ -69737,6 +71333,9 @@ topbarMenuRegistry.add("file", {
69737
71333
  }).addChild("split_to_columns", ["data"], {
69738
71334
  ...splitToColumns,
69739
71335
  sequence: 20
71336
+ }).addChild("data_analysis", ["data"], {
71337
+ ...dataAnalysis,
71338
+ sequence: 24
69740
71339
  }).addChild("column_statistics", ["data"], {
69741
71340
  ...columnStatistics,
69742
71341
  sequence: 25
@@ -70138,7 +71737,7 @@ var RippleEffect = class extends Component {
70138
71737
  onAnimationEnd: types.function(),
70139
71738
  style: types.string()
70140
71739
  });
70141
- rippleRef = (0, _odoo_owl.signal)(null);
71740
+ rippleRef = _odoo_owl.signal.ref();
70142
71741
  setup() {
70143
71742
  let animation = void 0;
70144
71743
  (0, _odoo_owl.onMounted)(() => {
@@ -70192,7 +71791,7 @@ var Ripple = class extends Component {
70192
71791
  onAnimationEnd: types.function().optional(() => () => {}),
70193
71792
  class: types.string().optional("")
70194
71793
  });
70195
- childContainerRef = (0, _odoo_owl.signal)(null);
71794
+ childContainerRef = _odoo_owl.signal.ref();
70196
71795
  state = (0, _odoo_owl.proxy)({ ripples: [] });
70197
71796
  currentId = 1;
70198
71797
  onClick(ev) {
@@ -70463,9 +72062,9 @@ var BottomBarSheet = class extends Component {
70463
72062
  isEditing: false,
70464
72063
  openedPicker: void 0
70465
72064
  });
70466
- sheetDivRef = (0, _odoo_owl.signal)(null);
70467
- iconRef = (0, _odoo_owl.signal)(null);
70468
- sheetNameRef = (0, _odoo_owl.signal)(null);
72065
+ sheetDivRef = _odoo_owl.signal.ref();
72066
+ iconRef = _odoo_owl.signal.ref();
72067
+ sheetNameRef = _odoo_owl.signal.ref();
70469
72068
  editionState = "initializing";
70470
72069
  DOMFocusableElementStore;
70471
72070
  setup() {
@@ -70749,8 +72348,10 @@ var BottomBarStatistic = class extends Component {
70749
72348
  });
70750
72349
  state = (0, _odoo_owl.proxy)({ selectedStatisticFn: "" });
70751
72350
  store;
72351
+ sidePanelStore;
70752
72352
  setup() {
70753
72353
  this.store = useStore(AggregateStatisticsStore);
72354
+ this.sidePanelStore = useStore(SidePanelStore);
70754
72355
  (0, _odoo_owl.onWillUpdateProps)(() => {
70755
72356
  if (Object.values(this.store.statisticFnResults).every((result) => result?.value === void 0)) this.props.closeContextMenu();
70756
72357
  });
@@ -70790,6 +72391,12 @@ var BottomBarStatistic = class extends Component {
70790
72391
  format: fnValue.format()
70791
72392
  });
70792
72393
  }
72394
+ get isDataAnalysisOpen() {
72395
+ return this.sidePanelStore.mainPanel?.componentTag === "DataAnalysisPanel" && this.sidePanelStore.isMainPanelOpen || this.sidePanelStore.secondaryPanel?.componentTag === "DataAnalysisPanel" && this.sidePanelStore.isSecondaryPanelOpen;
72396
+ }
72397
+ toggleDataAnalysisPanel() {
72398
+ this.sidePanelStore.togglePinnedSidePanel("DataAnalysisPanel");
72399
+ }
70793
72400
  };
70794
72401
 
70795
72402
  //#endregion
@@ -70804,8 +72411,8 @@ var BottomBar = class extends Component {
70804
72411
  BottomBarSheet,
70805
72412
  BottomBarStatistic
70806
72413
  };
70807
- bottomBarRef = (0, _odoo_owl.signal)(null);
70808
- sheetListRef = (0, _odoo_owl.signal)(null);
72414
+ bottomBarRef = _odoo_owl.signal.ref();
72415
+ sheetListRef = _odoo_owl.signal.ref();
70809
72416
  dragAndDrop = useDragAndDropListItems();
70810
72417
  targetScroll = void 0;
70811
72418
  state = (0, _odoo_owl.proxy)({
@@ -71018,12 +72625,13 @@ var SpreadsheetDashboard = class extends Component {
71018
72625
  hoveredCell;
71019
72626
  viewStore;
71020
72627
  zoomStore;
71021
- gridRef = (0, _odoo_owl.signal)(null);
71022
- canvasRef = (0, _odoo_owl.signal)(null);
72628
+ gridRef = _odoo_owl.signal.ref();
72629
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
71023
72630
  setup() {
71024
72631
  this.hoveredCell = useStore(DelayedHoveredCellStore);
71025
72632
  this.viewStore = useStore(ViewportsStore);
71026
72633
  this.zoomStore = useStore(ZoomStore);
72634
+ useLocalStore(HoveredTableStore);
71027
72635
  const rendererStore = useLocalStore(RendererStore, OrderedLayers().filter((layer) => layer !== "Headers"));
71028
72636
  useChildSubEnv({ getPopoverContainerRect: () => this.zoomStore.getZoomedRect(this.getGridRect()) });
71029
72637
  useGridDrawing({
@@ -71441,8 +73049,8 @@ var RibbonMenu = class extends Component {
71441
73049
  static components = { Menu };
71442
73050
  props = (0, _odoo_owl.useProps)({ onClose: types.function() });
71443
73051
  rootItems = topbarMenuRegistry.getMenuItems();
71444
- menuRef = (0, _odoo_owl.signal)(null);
71445
- containerRef = (0, _odoo_owl.signal)(null);
73052
+ menuRef = _odoo_owl.signal.ref();
73053
+ containerRef = _odoo_owl.signal.ref();
71446
73054
  state = (0, _odoo_owl.proxy)({
71447
73055
  menuItems: this.rootItems,
71448
73056
  title: _t("Menu Bar"),
@@ -71515,7 +73123,7 @@ var SmallBottomBar = class extends Component {
71515
73123
  composerStore;
71516
73124
  viewStore;
71517
73125
  composerInterface;
71518
- composerRef = (0, _odoo_owl.signal)(null);
73126
+ composerRef = _odoo_owl.signal.ref();
71519
73127
  menuState = (0, _odoo_owl.proxy)({ isOpen: false });
71520
73128
  setup() {
71521
73129
  this.composerFocusStore = useStore(ComposerFocusStore);
@@ -71594,6 +73202,49 @@ var SmallBottomBar = class extends Component {
71594
73202
  }
71595
73203
  };
71596
73204
 
73205
+ //#endregion
73206
+ //#region src/components/standalone_grid_canvas/carousel_data_view_print.ts
73207
+ var CarouselDataViewPrint = class extends Component {
73208
+ static template = _odoo_owl.xml``;
73209
+ props = (0, _odoo_owl.useProps)({
73210
+ range: types.Range(),
73211
+ columnWeights: types.array().optional(),
73212
+ rect: types.object(),
73213
+ rendererStore: types.object()
73214
+ });
73215
+ viewportsStore;
73216
+ gridRender;
73217
+ setup() {
73218
+ useChildStoreProvider([ViewportsStore]);
73219
+ const store = useLocalStore(StandaloneViewportStore, this.props.range, this.props.columnWeights);
73220
+ this.gridRender = useLocalStore(GridRenderer, null);
73221
+ this.viewportsStore = useStore(ViewportsStore);
73222
+ (0, _odoo_owl.useEffect)(() => {
73223
+ store.setContainerSize(this.props.rect.width, this.props.rect.height);
73224
+ });
73225
+ (0, _odoo_owl.onMounted)(() => this.props.rendererStore.register(this));
73226
+ (0, _odoo_owl.onWillUnmount)(() => this.props.rendererStore.unRegister(this));
73227
+ }
73228
+ get renderingLayers() {
73229
+ return ["Chart"];
73230
+ }
73231
+ drawLayer(renderingContext) {
73232
+ const { ctx } = renderingContext;
73233
+ ctx.save();
73234
+ ctx.translate(this.props.rect.x, this.props.rect.y);
73235
+ ctx.beginPath();
73236
+ ctx.rect(0, 0, this.props.rect.width, this.props.rect.height);
73237
+ ctx.clip();
73238
+ const subRenderingCtx = {
73239
+ ...renderingContext,
73240
+ sheetId: this.viewportsStore.displayedSheetId,
73241
+ viewports: this.viewportsStore.viewports
73242
+ };
73243
+ this.gridRender.drawLayer(subRenderingCtx, "Background", void 0);
73244
+ ctx.restore();
73245
+ }
73246
+ };
73247
+
71597
73248
  //#endregion
71598
73249
  //#region src/components/standalone_grid_canvas/figure_renderer_store.ts
71599
73250
  /** Store that draws the figures directly onto the canvas */
@@ -71619,29 +73270,25 @@ var FigureRendererStore = class extends DisposableStore {
71619
73270
  drawLayer(renderingCtx) {
71620
73271
  const { viewports, ctx } = renderingCtx;
71621
73272
  const visibleFigures = viewports.getVisibleFigures(renderingCtx.sheetId);
71622
- const { x: offsetX, y: offsetY } = viewports.getViewportOffset(renderingCtx.sheetId);
73273
+ const scrollOffset = viewports.getViewportOffset(renderingCtx.sheetId);
71623
73274
  for (const figure of visibleFigures) {
71624
- const x = figure.x - offsetX;
71625
- const y = figure.y - offsetY;
73275
+ const figureRect = {
73276
+ ...figure,
73277
+ x: figure.x - scrollOffset.x,
73278
+ y: figure.y - scrollOffset.y
73279
+ };
73280
+ const { x, y, width, height } = figureRect;
71626
73281
  if (figure.tag === "chart") {
71627
73282
  const chartId = this.getters.getChartIdFromFigureId(figure.id);
71628
- if (chartId) {
71629
- const chartRect = {
71630
- x,
71631
- y,
71632
- width: figure.width,
71633
- height: figure.height
71634
- };
71635
- this.drawChart(renderingCtx, chartId, chartRect);
71636
- }
73283
+ if (chartId) this.drawChart(renderingCtx, chartId, figureRect);
71637
73284
  } else if (figure.tag === "image") {
71638
73285
  const loadedImage = this.loadedImages[this.getters.getImagePath(figure.id)];
71639
- if (loadedImage) ctx.drawImage(loadedImage, x, y, figure.width, figure.height);
71640
- } else if (figure.tag === "carousel") this.drawCarousel(renderingCtx, figure);
73286
+ if (loadedImage) ctx.drawImage(loadedImage, x, y, width, height);
73287
+ } else if (figure.tag === "carousel") this.drawCarousel(renderingCtx, figure, figureRect);
71641
73288
  if (!this.getters.isDashboard()) {
71642
73289
  ctx.strokeStyle = GRAY_400;
71643
73290
  ctx.lineWidth = 1;
71644
- ctx.strokeRect(x, y, figure.width, figure.height);
73291
+ ctx.strokeRect(x, y, width, height);
71645
73292
  }
71646
73293
  }
71647
73294
  }
@@ -71659,53 +73306,37 @@ var FigureRendererStore = class extends DisposableStore {
71659
73306
  renderingCtx.ctx.drawImage(chartCanvas, x, y, width, height);
71660
73307
  cleanUp();
71661
73308
  }
71662
- drawCarousel(renderingCtx, figure) {
71663
- const { viewports: sheetView, ctx } = renderingCtx;
71664
- const { x: offsetX, y: offsetY } = sheetView.getViewportOffset(renderingCtx.sheetId);
71665
- const x = figure.x - offsetX;
71666
- const y = figure.y - offsetY;
73309
+ drawCarousel(renderingCtx, figure, figureRect) {
73310
+ const { ctx } = renderingCtx;
71667
73311
  const carousel = this.getters.getCarousel(figure.id);
71668
73312
  const chartId = this.getters.getChartIdFromFigureId(figure.id);
71669
73313
  if (!carousel) return;
73314
+ const layout = getCarouselLayout(figureRect, carousel, this.getters.getSelectedCarouselItem(figure.id));
71670
73315
  const chartDefinition = chartId ? this.getters.getChartDefinition(chartId) : void 0;
73316
+ ctx.save();
73317
+ ctx.beginPath();
73318
+ ctx.rect(figureRect.x, figureRect.y, figureRect.width, figureRect.height);
73319
+ ctx.clip();
73320
+ ctx.fillStyle = chartDefinition?.background || this.getters.getSpreadsheetTheme().backgroundColor;
73321
+ ctx.fillRect(figureRect.x, figureRect.y, figureRect.width, figureRect.height);
71671
73322
  const title = {
71672
73323
  ...DEFAULT_CAROUSEL_TITLE_STYLE,
71673
73324
  ...carousel.title
71674
73325
  };
71675
- const headerPadding = 4;
71676
- const headerSize = title.fontSize + headerPadding * 2;
71677
- if (!title.text && chartId) {
71678
- const chartRect = {
71679
- x,
71680
- y,
71681
- width: figure.width,
71682
- height: figure.height
71683
- };
71684
- this.drawChart(renderingCtx, chartId, chartRect);
71685
- } else if (title.text) {
71686
- ctx.save();
71687
- ctx.fillStyle = chartDefinition?.background || this.getters.getSpreadsheetTheme().backgroundColor;
71688
- ctx.fillRect(x, y, figure.width, headerSize);
71689
- ctx.font = computeTextFont(chartStyleToCellStyle(title));
73326
+ if (title.text) {
73327
+ ctx.font = computeTextFont(chartStyleToCellStyle(title), "px", 500);
71690
73328
  ctx.fillStyle = title.color;
71691
- ctx.fillText(title.text, x + headerPadding, y + headerPadding + title.fontSize);
71692
- ctx.restore();
71693
- const chartRect = {
71694
- x,
71695
- y: y + headerSize,
71696
- width: figure.width,
71697
- height: figure.height - headerSize
71698
- };
71699
- if (chartId) this.drawChart(renderingCtx, chartId, chartRect);
71700
- else if (!this.getters.isDashboard()) {
71701
- ctx.strokeStyle = GRAY_400;
71702
- ctx.lineWidth = 1;
71703
- ctx.beginPath();
71704
- ctx.moveTo(x, y + headerSize);
71705
- ctx.lineTo(x + figure.width, y + headerSize);
71706
- ctx.stroke();
71707
- }
73329
+ ctx.textBaseline = "middle";
73330
+ const textY = Math.ceil(layout.headerRect.y + layout.headerRect.height / 2);
73331
+ ctx.fillText(title.text, layout.headerRect.x, textY);
73332
+ }
73333
+ const separator = layout.separatorRect;
73334
+ if (separator) {
73335
+ ctx.fillStyle = GRAY_400;
73336
+ ctx.fillRect(separator.x, separator.y, separator.width, separator.height);
71708
73337
  }
73338
+ ctx.restore();
73339
+ if (chartId) this.drawChart(renderingCtx, chartId, layout.contentRect);
71709
73340
  }
71710
73341
  };
71711
73342
 
@@ -71713,12 +73344,13 @@ var FigureRendererStore = class extends DisposableStore {
71713
73344
  //#region src/components/standalone_grid_canvas/standalone_grid_canvas.ts
71714
73345
  var StandaloneGridCanvas = class extends Component {
71715
73346
  static template = "o-spreadsheet-StandaloneGridCanvas";
73347
+ static components = { CarouselDataViewPrint };
71716
73348
  props = (0, _odoo_owl.useProps)({
71717
73349
  sheetId: types.UID(),
71718
73350
  zone: types.Zone(),
71719
73351
  renderingCtx: types.object()
71720
73352
  });
71721
- canvasRef = (0, _odoo_owl.signal)(null);
73353
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
71722
73354
  rendererStore;
71723
73355
  figureRendererStore;
71724
73356
  setup() {
@@ -71740,6 +73372,28 @@ var StandaloneGridCanvas = class extends Component {
71740
73372
  this.figureRendererStore.addLoadedImage(path, await createImageBitmap(blob));
71741
73373
  }));
71742
73374
  }
73375
+ get carouselDataViews() {
73376
+ const { sheetId, viewports } = this.props.renderingCtx;
73377
+ return viewports.getVisibleFigures(sheetId).filter((figure) => figure.tag === "carousel").map((figure) => {
73378
+ const carouselItem = this.env.model.getters.getSelectedCarouselItem(figure.id);
73379
+ if (carouselItem?.type !== "carouselDataView" || !carouselItem.range) return;
73380
+ const carousel = this.env.model.getters.getCarousel(figure.id);
73381
+ const scroll = viewports.getViewportOffset(sheetId);
73382
+ const { x, y, width, height } = figure;
73383
+ const layout = getCarouselLayout({
73384
+ x: x - scroll.x,
73385
+ y: y - scroll.y,
73386
+ width,
73387
+ height
73388
+ }, carousel, carouselItem);
73389
+ return {
73390
+ range: carouselItem.range,
73391
+ columnWeights: carouselItem.columnWeights,
73392
+ rect: layout.contentRect,
73393
+ rendererStore: this.rendererStore
73394
+ };
73395
+ }).filter(isDefined);
73396
+ }
71743
73397
  };
71744
73398
 
71745
73399
  //#endregion
@@ -72201,7 +73855,7 @@ var NamedRangeSelector = class extends Component {
72201
73855
  anchorRect: null,
72202
73856
  menuItems: []
72203
73857
  });
72204
- namedRangeSelectorRef = (0, _odoo_owl.signal)(null);
73858
+ namedRangeSelectorRef = _odoo_owl.signal.ref();
72205
73859
  setup() {
72206
73860
  this.topBarToolStore = useToolBarDropdownStore();
72207
73861
  this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
@@ -72399,7 +74053,7 @@ var BorderEditor = class extends Component {
72399
74053
  anchorRect: types.Rect()
72400
74054
  });
72401
74055
  BORDER_POSITIONS = BORDER_POSITIONS;
72402
- lineStyleButtonRef = (0, _odoo_owl.signal)(null);
74056
+ lineStyleButtonRef = _odoo_owl.signal.ref();
72403
74057
  borderStyles = borderStyles;
72404
74058
  state = (0, _odoo_owl.proxy)({ activeTool: void 0 });
72405
74059
  toggleDropdownTool(tool) {
@@ -72466,7 +74120,7 @@ var BorderEditorWidget = class extends Component {
72466
74120
  });
72467
74121
  topBarToolStore;
72468
74122
  viewStore;
72469
- borderEditorButtonRef = (0, _odoo_owl.signal)(null);
74123
+ borderEditorButtonRef = _odoo_owl.signal.ref();
72470
74124
  state = (0, _odoo_owl.proxy)({
72471
74125
  currentColor: DEFAULT_BORDER_DESC.color,
72472
74126
  currentStyle: DEFAULT_BORDER_DESC.style,
@@ -72696,7 +74350,7 @@ var DropdownAction = class extends Component {
72696
74350
  childClass: types.string()
72697
74351
  });
72698
74352
  topBarToolStore;
72699
- actionRef = (0, _odoo_owl.signal)(null);
74353
+ actionRef = _odoo_owl.signal.ref();
72700
74354
  setup() {
72701
74355
  this.topBarToolStore = useToolBarDropdownStore();
72702
74356
  }
@@ -72759,7 +74413,7 @@ var NumberFormatsTool = class extends Component {
72759
74413
  props = (0, _odoo_owl.useProps)({ class: types.string() });
72760
74414
  formatNumberMenuItemSpec = formatNumberMenuItemSpec;
72761
74415
  topBarToolStore;
72762
- buttonRef = (0, _odoo_owl.signal)(null);
74416
+ buttonRef = _odoo_owl.signal.ref();
72763
74417
  state = (0, _odoo_owl.proxy)({
72764
74418
  anchorRect: {
72765
74419
  x: 0,
@@ -73048,12 +74702,12 @@ var TopBar = class extends Component {
73048
74702
  composerFocusStore;
73049
74703
  fingerprints;
73050
74704
  topBarToolStore;
73051
- toolBarContainerRef = (0, _odoo_owl.signal)(null);
73052
- toolbarRef = (0, _odoo_owl.signal)(null);
73053
- namedRangesRef = (0, _odoo_owl.signal)(null);
73054
- topBarTopRef = (0, _odoo_owl.signal)(null);
73055
- moreToolsContainerRef = (0, _odoo_owl.signal)(null);
73056
- moreToolsButtonRef = (0, _odoo_owl.signal)(null);
74705
+ toolBarContainerRef = _odoo_owl.signal.ref();
74706
+ toolbarRef = _odoo_owl.signal.ref();
74707
+ namedRangesRef = _odoo_owl.signal.ref();
74708
+ topBarTopRef = _odoo_owl.signal.ref();
74709
+ moreToolsContainerRef = _odoo_owl.signal.ref();
74710
+ moreToolsButtonRef = _odoo_owl.signal.ref();
73057
74711
  spreadsheetRect = useSpreadsheetRect();
73058
74712
  setup() {
73059
74713
  this.composerFocusStore = useStore(ComposerFocusStore);
@@ -73315,7 +74969,7 @@ var Spreadsheet = class extends Component {
73315
74969
  SpreadsheetPrint
73316
74970
  };
73317
74971
  sidePanel;
73318
- spreadsheetRef = (0, _odoo_owl.signal)(null);
74972
+ spreadsheetRef = _odoo_owl.signal.ref();
73319
74973
  spreadsheetRect = useSpreadsheetRect();
73320
74974
  state = (0, _odoo_owl.proxy)({ printModeEnabled: false });
73321
74975
  _focusGrid;
@@ -79323,12 +80977,13 @@ var FilterMenu = class extends Component {
79323
80977
  }
79324
80978
  sortFilterZone(sortDirection) {
79325
80979
  const filterPosition = this.props.filterPosition;
79326
- const tableZone = this.table?.range.zone;
80980
+ const table = this.table;
80981
+ const tableZone = table?.range.zone;
79327
80982
  if (!filterPosition || !tableZone || tableZone.top === tableZone.bottom) return;
79328
80983
  const sheetId = this.viewStore.displayedSheetId;
79329
80984
  const contentZone = {
79330
80985
  ...tableZone,
79331
- top: tableZone.top + 1
80986
+ top: tableZone.top + table.config.numberOfHeaders
79332
80987
  };
79333
80988
  const sortAnchor = {
79334
80989
  col: filterPosition.col,
@@ -79486,9 +81141,9 @@ var LinkEditor = class extends Component {
79486
81141
  onClosed: types.function().optional()
79487
81142
  });
79488
81143
  static size = { maxHeight: 500 };
79489
- urlInput = (0, _odoo_owl.signal)(null);
79490
- suggestionListRef = (0, _odoo_owl.signal)(null);
79491
- urlInputContainer = (0, _odoo_owl.signal)(null);
81144
+ urlInput = _odoo_owl.signal.ref(HTMLInputElement);
81145
+ suggestionListRef = _odoo_owl.signal.ref();
81146
+ urlInputContainer = _odoo_owl.signal.ref();
79492
81147
  state;
79493
81148
  viewStore;
79494
81149
  setup() {
@@ -81606,7 +83261,7 @@ const ARRAYTOTEXT = {
81606
83261
  else if (_format === 0) {
81607
83262
  const rowSeparator = this.locale.decimalSeparator === "," ? "/" : ",";
81608
83263
  return { value: transposeMatrix(_array).flatMap((row) => row.map((value) => {
81609
- return isEvaluationError(value.value) ? value.value : toString(value);
83264
+ return isEvaluationError(value.value) ? value.value : toLocaleString(value, this.locale);
81610
83265
  })).join(rowSeparator) };
81611
83266
  } else return new EvaluationError(_t("Format must be 0 or 1"));
81612
83267
  },
@@ -87654,7 +89309,7 @@ const CONCAT = {
87654
89309
  description: _t("Concatenation of two values."),
87655
89310
  args: [arg("value1 (string)", _t("The value to which value2 will be appended.")), arg("value2 (string)", _t("The value to append to value1."))],
87656
89311
  compute: function(value1, value2) {
87657
- return { value: toString(value1) + toString(value2) };
89312
+ return { value: toLocaleString(value1, this.locale) + toLocaleString(value2, this.locale) };
87658
89313
  },
87659
89314
  isExported: true
87660
89315
  };
@@ -88537,7 +90192,7 @@ const CLEAN = {
88537
90192
  description: _t("Remove non-printable characters from a piece of text."),
88538
90193
  args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))],
88539
90194
  compute: function(text) {
88540
- const _text = toString(text);
90195
+ const _text = toLocaleString(text, this.locale);
88541
90196
  let cleanedStr = "";
88542
90197
  for (const char of _text) if (char && char.charCodeAt(0) > 31) cleanedStr += char;
88543
90198
  return { value: cleanedStr };
@@ -88548,7 +90203,7 @@ const CONCATENATE = {
88548
90203
  description: _t("Appends strings to one another."),
88549
90204
  args: [arg("string (string, range<string>, repeating)", _t("String to append in sequence."))],
88550
90205
  compute: function(...datas) {
88551
- return { value: reduceAny(datas, (acc, a) => acc + toString(a), "") };
90206
+ return { value: reduceAny(datas, (acc, a) => acc + toLocaleString(a, this.locale), "") };
88552
90207
  },
88553
90208
  isExported: true
88554
90209
  };
@@ -88556,7 +90211,7 @@ const EXACT = {
88556
90211
  description: _t("Tests whether two strings are identical."),
88557
90212
  args: [arg("string1 (string)", _t("The first string to compare.")), arg("string2 (string)", _t("The second string to compare."))],
88558
90213
  compute: function(string1, string2) {
88559
- return { value: toString(string1) === toString(string2) };
90214
+ return { value: toLocaleString(string1, this.locale) === toLocaleString(string2, this.locale) };
88560
90215
  },
88561
90216
  isExported: true
88562
90217
  };
@@ -88568,8 +90223,8 @@ const FIND = {
88568
90223
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
88569
90224
  ],
88570
90225
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
88571
- const _searchFor = toString(searchFor);
88572
- const _textToSearch = toString(textToSearch);
90226
+ const _searchFor = toLocaleString(searchFor, this.locale);
90227
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
88573
90228
  const _startingAt = toNumber(startingAt, this.locale);
88574
90229
  if (_textToSearch === "") return new EvaluationError(_t("The text_to_search must be non-empty."));
88575
90230
  if (_startingAt < 1) return new EvaluationError(_t("The starting_at (%s) must be greater than or equal to 1.", _startingAt));
@@ -88606,8 +90261,8 @@ const JOIN = {
88606
90261
  description: _t("Concatenates elements of arrays with delimiter."),
88607
90262
  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."))],
88608
90263
  compute: function(delimiter, ...valuesOrArrays) {
88609
- const _delimiter = toString(delimiter);
88610
- return { value: reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toString(a), "") };
90264
+ const _delimiter = toLocaleString(delimiter, this.locale);
90265
+ return { value: reduceAny(valuesOrArrays, (acc, a) => (acc ? acc + _delimiter : "") + toLocaleString(a, this.locale), "") };
88611
90266
  }
88612
90267
  };
88613
90268
  const LEFT = {
@@ -88616,7 +90271,7 @@ const LEFT = {
88616
90271
  compute: function(text, ...args) {
88617
90272
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
88618
90273
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
88619
- return { value: toString(text).substring(0, _numberOfCharacters) };
90274
+ return { value: toLocaleString(text, this.locale).substring(0, _numberOfCharacters) };
88620
90275
  },
88621
90276
  isExported: true
88622
90277
  };
@@ -88624,7 +90279,7 @@ const LEN = {
88624
90279
  description: _t("Length of a string."),
88625
90280
  args: [arg("text (string)", _t("The string whose length will be returned."))],
88626
90281
  compute: function(text) {
88627
- return { value: toString(text).length };
90282
+ return { value: toLocaleString(text, this.locale).length };
88628
90283
  },
88629
90284
  isExported: true
88630
90285
  };
@@ -88632,7 +90287,7 @@ const LOWER = {
88632
90287
  description: _t("Converts a specified string to lowercase."),
88633
90288
  args: [arg("text (string)", _t("The string to convert to lowercase."))],
88634
90289
  compute: function(text) {
88635
- return { value: toString(text).toLowerCase() };
90290
+ return { value: toLocaleString(text, this.locale).toLowerCase() };
88636
90291
  },
88637
90292
  isExported: true
88638
90293
  };
@@ -88644,7 +90299,7 @@ const MID = {
88644
90299
  arg("extract_length (number)", _t("The length of the segment to extract."))
88645
90300
  ],
88646
90301
  compute: function(text, starting_at, extract_length) {
88647
- const _text = toString(text);
90302
+ const _text = toLocaleString(text, this.locale);
88648
90303
  const _starting_at = toNumber(starting_at, this.locale);
88649
90304
  const _extract_length = toNumber(extract_length, this.locale);
88650
90305
  if (_starting_at < 1) return new EvaluationError(_t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString()));
@@ -88657,7 +90312,7 @@ const PROPER = {
88657
90312
  description: _t("Capitalizes each word in a specified string."),
88658
90313
  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."))],
88659
90314
  compute: function(text) {
88660
- return { value: toString(text).replace(wordRegex, (word) => {
90315
+ return { value: toLocaleString(text, this.locale).replace(wordRegex, (word) => {
88661
90316
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
88662
90317
  }) };
88663
90318
  },
@@ -88807,8 +90462,8 @@ const REPLACE = {
88807
90462
  if (_position < 1) return new EvaluationError(_t("The position (%s) must be greater than or equal to 1.", _position));
88808
90463
  const _length = toNumber(length, this.locale);
88809
90464
  if (_length < 0) return new EvaluationError(_t("The length (%s) must be positive or zero.", _length));
88810
- const _text = toString(text);
88811
- const _newText = toString(newText);
90465
+ const _text = toLocaleString(text, this.locale);
90466
+ const _newText = toLocaleString(newText, this.locale);
88812
90467
  return { value: _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length) };
88813
90468
  },
88814
90469
  isExported: true
@@ -88819,7 +90474,7 @@ const RIGHT = {
88819
90474
  compute: function(text, ...args) {
88820
90475
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
88821
90476
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or zero.", _numberOfCharacters));
88822
- const _text = toString(text);
90477
+ const _text = toLocaleString(text, this.locale);
88823
90478
  const stringLength = _text.length;
88824
90479
  return { value: _text.substring(stringLength - _numberOfCharacters, stringLength) };
88825
90480
  },
@@ -88833,8 +90488,8 @@ const SEARCH = {
88833
90488
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
88834
90489
  ],
88835
90490
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
88836
- const _searchFor = toString(searchFor).toLowerCase();
88837
- const _textToSearch = toString(textToSearch).toLowerCase();
90491
+ const _searchFor = toLocaleString(searchFor, this.locale).toLowerCase();
90492
+ const _textToSearch = toLocaleString(textToSearch, this.locale).toLowerCase();
88838
90493
  const _startingAt = toNumber(startingAt, this.locale);
88839
90494
  if (_textToSearch === "") return {
88840
90495
  value: CellErrorType.GenericError,
@@ -88864,8 +90519,8 @@ const SPLIT = {
88864
90519
  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."))
88865
90520
  ],
88866
90521
  computeArray: function(text, delimiter, splitByEach = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, removeEmptyText = { value: SPLIT_DEFAULT_REMOVE_EMPTY_TEXT }) {
88867
- const _text = toString(text);
88868
- const _delimiter = escapeRegExp(toString(delimiter));
90522
+ const _text = toLocaleString(text, this.locale);
90523
+ const _delimiter = escapeRegExp(toLocaleString(delimiter, this.locale));
88869
90524
  const _splitByEach = toBoolean(splitByEach);
88870
90525
  const _removeEmptyText = toBoolean(removeEmptyText);
88871
90526
  if (_delimiter.length <= 0) return new EvaluationError(_t("The delimiter (%s) must be not be empty.", _delimiter));
@@ -88887,10 +90542,10 @@ const SUBSTITUTE = {
88887
90542
  compute: function(textToSearch, searchFor, replaceWith, occurrenceNumber) {
88888
90543
  const _occurrenceNumber = toNumber(occurrenceNumber, this.locale);
88889
90544
  if (_occurrenceNumber < 0) return new EvaluationError(_t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber));
88890
- const _textToSearch = toString(textToSearch);
88891
- const _searchFor = toString(searchFor);
90545
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
90546
+ const _searchFor = toLocaleString(searchFor, this.locale);
88892
90547
  if (_searchFor === "") return { value: _textToSearch };
88893
- const _replaceWith = toString(replaceWith);
90548
+ const _replaceWith = toLocaleString(replaceWith, this.locale);
88894
90549
  const reg = new RegExp(escapeRegExp(_searchFor), "g");
88895
90550
  if (_occurrenceNumber === 0) return { value: _textToSearch.replace(reg, _replaceWith) };
88896
90551
  let n = 0;
@@ -88913,10 +90568,10 @@ const TEXTJOIN = {
88913
90568
  arg("texts (string, range<string>, repeating)", _t("Text item to join."))
88914
90569
  ],
88915
90570
  compute: function(delimiter, ignoreEmpty = { value: TEXTJOIN_DEFAULT_IGNORE_EMPTY }, ...textsOrArrays) {
88916
- const _delimiter = toString(delimiter);
90571
+ const _delimiter = toLocaleString(delimiter, this.locale);
88917
90572
  const _ignoreEmpty = toBoolean(ignoreEmpty);
88918
90573
  let n = 0;
88919
- return { value: reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toString(a) === "") ? (n++ ? acc + _delimiter : "") + toString(a) : acc, "") };
90574
+ return { value: reduceAny(textsOrArrays, (acc, a) => !(_ignoreEmpty && toLocaleString(a, this.locale) === "") ? (n++ ? acc + _delimiter : "") + toLocaleString(a, this.locale) : acc, "") };
88920
90575
  },
88921
90576
  isExported: true
88922
90577
  };
@@ -88969,7 +90624,7 @@ const TRIM = {
88969
90624
  description: _t("Removes space characters."),
88970
90625
  args: [arg("text (string)", _t("The text or reference to a cell containing text to be trimmed."))],
88971
90626
  compute: function(text) {
88972
- return { value: trimContent(toString(text)) };
90627
+ return { value: trimContent(toLocaleString(text, this.locale)) };
88973
90628
  },
88974
90629
  isExported: true
88975
90630
  };
@@ -88977,7 +90632,7 @@ const UPPER = {
88977
90632
  description: _t("Converts a specified string to uppercase."),
88978
90633
  args: [arg("text (string)", _t("The string to convert to uppercase."))],
88979
90634
  compute: function(text) {
88980
- return { value: toString(text).toUpperCase() };
90635
+ return { value: toLocaleString(text, this.locale).toUpperCase() };
88981
90636
  },
88982
90637
  isExported: true
88983
90638
  };
@@ -89074,7 +90729,7 @@ const HYPERLINK = {
89074
90729
  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."))],
89075
90730
  compute: function(url, linkLabel) {
89076
90731
  const processedUrl = toString(url).trim();
89077
- const processedLabel = toString(linkLabel) || processedUrl;
90732
+ const processedLabel = toLocaleString(linkLabel, this.locale) || processedUrl;
89078
90733
  if (processedUrl === "") return { value: processedLabel };
89079
90734
  return { value: markdownLink(processedLabel, processedUrl) };
89080
90735
  },
@@ -89304,7 +90959,9 @@ const helpers = {
89304
90959
  computeCachedTextDimension,
89305
90960
  createComputeFunction,
89306
90961
  isMobileOS,
89307
- drawHighlight
90962
+ drawHighlight,
90963
+ getBoundingRectWithMargins,
90964
+ hasInteractiveElementInEventTree
89308
90965
  };
89309
90966
  const links = {
89310
90967
  isMarkdownLink,
@@ -89536,6 +91193,6 @@ exports.stores = stores;
89536
91193
  exports.tokenColors = tokenColors;
89537
91194
  exports.tokenize = tokenize;
89538
91195
 
89539
- __info__.version = "19.5.0-alpha.11";
89540
- __info__.date = "2026-08-12T09:17:24.237Z";
89541
- __info__.hash = "bc056d7";
91196
+ __info__.version = "19.5.0-alpha.12";
91197
+ __info__.date = "2026-08-19T09:47:03.774Z";
91198
+ __info__.hash = "68ca68e";