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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/material_symbols_outlined.css +9 -0
  2. package/dist/o_spreadsheet.cjs +2325 -659
  3. package/dist/o_spreadsheet.css +169 -33
  4. package/dist/o_spreadsheet.esm.js +2317 -651
  5. package/dist/o_spreadsheet.iife.js +2325 -659
  6. package/dist/o_spreadsheet.min.iife.js +344 -344
  7. package/dist/o_spreadsheet.xml +209 -128
  8. package/dist/types/actions/data_actions.d.ts +1 -0
  9. package/dist/types/components/bottom_bar/bottom_bar_statistic/bottom_bar_statistic.d.ts +3 -0
  10. package/dist/types/components/dashboard/clickable_cell_sort_icon/clickable_cell_sort_icon.d.ts +1 -1
  11. package/dist/types/components/figures/figure/figure.d.ts +1 -0
  12. package/dist/types/components/figures/figure_carousel/figure_carousel.d.ts +10 -0
  13. package/dist/types/components/figures/figure_container/figure_container.d.ts +1 -2
  14. package/dist/types/components/grid/grid.d.ts +1 -0
  15. package/dist/types/components/helpers/chart_drag_and_drop.d.ts +15 -0
  16. package/dist/types/components/helpers/dom_helpers.d.ts +3 -2
  17. package/dist/types/components/link/link_editor/link_editor.d.ts +1 -1
  18. package/dist/types/components/side_panel/carousel_panel/carousel_panel.d.ts +1 -1
  19. package/dist/types/components/side_panel/chart/building_blocks/color_scale/color_scale_picker.d.ts +1 -1
  20. package/dist/types/components/side_panel/data_analysis/chart_suggestion_preview.d.ts +19 -0
  21. package/dist/types/components/side_panel/data_analysis/data_analysis_panel.d.ts +17 -0
  22. package/dist/types/components/side_panel/data_analysis/data_analysis_store.d.ts +15 -0
  23. package/dist/types/components/side_panel/pivot/pivot_layout_configurator/pivot_layout_configurator.d.ts +1 -6
  24. package/dist/types/components/side_panel/side_panel/side_panel_store.d.ts +2 -1
  25. package/dist/types/components/standalone_grid_canvas/carousel_data_view_print.d.ts +32 -0
  26. package/dist/types/components/standalone_grid_canvas/standalone_grid_canvas.d.ts +6 -0
  27. package/dist/types/components/standalone_viewport/standalone_viewport.d.ts +6 -3
  28. package/dist/types/components/standalone_viewport/standalone_viewport_store.d.ts +2 -1
  29. package/dist/types/components/tables/hovered_table_store.d.ts +6 -7
  30. package/dist/types/constants.d.ts +13 -1
  31. package/dist/types/functions/function_registry.d.ts +3 -0
  32. package/dist/types/functions/helpers.d.ts +4 -0
  33. package/dist/types/functions/module_operators.d.ts +1 -1
  34. package/dist/types/functions/module_text.d.ts +11 -11
  35. package/dist/types/functions/module_web.d.ts +1 -1
  36. package/dist/types/helpers/carousel_helpers.d.ts +8 -1
  37. package/dist/types/helpers/data_analysis.d.ts +16 -0
  38. package/dist/types/helpers/figures/charts/chart_data_source_helpers.d.ts +10 -0
  39. package/dist/types/helpers/figures/charts/chart_suggestion_engine.d.ts +8 -0
  40. package/dist/types/helpers/figures/charts/chart_ui_common.d.ts +8 -1
  41. package/dist/types/helpers/figures/charts/charts_suggestions_constants.d.ts +37 -0
  42. package/dist/types/helpers/figures/charts/gauge_chart_rendering.d.ts +6 -2
  43. package/dist/types/helpers/text_helper.d.ts +1 -1
  44. package/dist/types/index.d.ts +5 -2
  45. package/dist/types/plugins/core/squisher.d.ts +2 -1
  46. package/dist/types/registries/figures_registry.d.ts +1 -0
  47. package/dist/types/stores/cell_hover_overlay_store.d.ts +15 -0
  48. package/dist/types/stores/chart_drag_store.d.ts +10 -0
  49. package/dist/types/stores/grid_renderer_store.d.ts +3 -2
  50. package/dist/types/types/chart/chart_suggestion.d.ts +136 -0
  51. package/dist/types/types/commands.d.ts +16 -9
  52. package/package.json +4 -4
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 19.5.0-alpha.10
6
- * @date 2026-08-10T12:33:47.263Z
7
- * @hash 39a6fbc
5
+ * @version 19.5.0-alpha.12
6
+ * @date 2026-08-19T09:47:03.774Z
7
+ * @hash 68ca68e
8
8
  */
9
9
 
10
10
  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, "");
@@ -5986,6 +6016,9 @@ var FunctionRegistry = class extends Registry {
5986
6016
  }
5987
6017
  };
5988
6018
  const functionRegistry = new FunctionRegistry();
6019
+ /** Formulas using one of these functions are never squished: they are always exported
6020
+ * as a full formula string, and they don't become the base formula of the next cells. */
6021
+ const NonSquishableFunctionRegistry = new Registry();
5989
6022
 
5990
6023
  //#endregion
5991
6024
  //#region src/helpers/references.ts
@@ -9245,8 +9278,8 @@ function computeCachedTextDimension(context, text, font) {
9245
9278
  function fontSizeInPixels(fontSize) {
9246
9279
  return Math.round(fontSize * 96 / 72);
9247
9280
  }
9248
- function computeTextFont(style, fontUnit = "pt") {
9249
- 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}`;
9250
9283
  }
9251
9284
  function computeTextFontSizeInPixels(style) {
9252
9285
  return fontSizeInPixels(style?.fontSize || DEFAULT_FONT_SIZE);
@@ -11880,7 +11913,7 @@ var ChartJsComponent = class extends Component {
11880
11913
  chartId: types.string(),
11881
11914
  isFullScreen: types.boolean().optional()
11882
11915
  });
11883
- canvas = (0, _odoo_owl.signal)(null);
11916
+ canvas = _odoo_owl.signal.ref(HTMLCanvasElement);
11884
11917
  chart;
11885
11918
  currentRuntime;
11886
11919
  animationStore;
@@ -12603,7 +12636,7 @@ var ScorecardChart = class extends Component {
12603
12636
  chartId: types.string(),
12604
12637
  isFullScreen: types.boolean().optional()
12605
12638
  });
12606
- canvas = (0, _odoo_owl.signal)(null);
12639
+ canvas = _odoo_owl.signal.ref(HTMLCanvasElement);
12607
12640
  zoomStore;
12608
12641
  get runtime() {
12609
12642
  return this.env.model.getters.getChartRuntime(this.props.chartId);
@@ -13988,6 +14021,44 @@ function getCarouselItemTitle(getters, item) {
13988
14021
  const definition = getters.getChartDefinition(item.chartId);
13989
14022
  return (chartSubtypeRegistry.getAll().find((c) => c.matcher?.(definition)) || chartSubtypeRegistry.get(definition.type)).displayName;
13990
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
+ }
13991
14062
 
13992
14063
  //#endregion
13993
14064
  //#region src/helpers/figures/charts/gauge_chart_rendering.ts
@@ -13999,7 +14070,7 @@ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
13999
14070
  const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
14000
14071
  const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
14001
14072
  const GAUGE_TITLE_SECTION_HEIGHT = 25;
14002
- function drawGaugeChart(canvas, runtime, zoom = 1, dimensions) {
14073
+ function drawGaugeChart(canvas, runtime, zoom = 1, dimensions, options) {
14003
14074
  const size = dimensions ?? getCanvasSize(canvas);
14004
14075
  const dpr = typeof globalThis.devicePixelRatio === "number" ? globalThis.devicePixelRatio : 1;
14005
14076
  canvas.width = size.width * dpr;
@@ -14012,7 +14083,7 @@ function drawGaugeChart(canvas, runtime, zoom = 1, dimensions) {
14012
14083
  height: size.height / zoom,
14013
14084
  x: 0,
14014
14085
  y: 0
14015
- }, runtime, ctx);
14086
+ }, runtime, ctx, options);
14016
14087
  drawBackground(ctx, config);
14017
14088
  drawGauge(ctx, config);
14018
14089
  drawInflectionValues(ctx, config);
@@ -14100,7 +14171,8 @@ function drawTitle(ctx, config) {
14100
14171
  ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
14101
14172
  ctx.restore();
14102
14173
  }
14103
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
14174
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx, options) {
14175
+ const labelFontSize = options?.labelFontSize ?? 12;
14104
14176
  const maxValue = runtime.maxValue;
14105
14177
  const minValue = runtime.minValue;
14106
14178
  const gaugeValue = getGaugeValue(runtime, "animated");
@@ -14118,14 +14190,14 @@ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
14118
14190
  if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
14119
14191
  const minLabelPosition = {
14120
14192
  x: gaugeRect.x + gaugeArcWidth / 2,
14121
- y: gaugeRect.y + gaugeRect.height + 12
14193
+ y: gaugeRect.y + gaugeRect.height + labelFontSize
14122
14194
  };
14123
14195
  const maxLabelPosition = {
14124
14196
  x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
14125
- y: gaugeRect.y + gaugeRect.height + 12
14197
+ y: gaugeRect.y + gaugeRect.height + labelFontSize
14126
14198
  };
14127
14199
  const textColor = chartMutedFontColor(runtime.background);
14128
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
14200
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx, labelFontSize);
14129
14201
  let x = 0, titleWidth = 0, titleHeight = 0;
14130
14202
  if (runtime.title.text) ({width: titleWidth, height: titleHeight} = computeTextDimension(ctx, runtime.title.text, {
14131
14203
  fontSize: 16,
@@ -14173,13 +14245,13 @@ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
14173
14245
  minLabel: {
14174
14246
  label: runtime.minValue.label,
14175
14247
  textPosition: minLabelPosition,
14176
- fontSize: 12,
14248
+ fontSize: labelFontSize,
14177
14249
  color: textColor
14178
14250
  },
14179
14251
  maxLabel: {
14180
14252
  label: runtime.maxValue.label,
14181
14253
  textPosition: maxLabelPosition,
14182
- fontSize: 12,
14254
+ fontSize: labelFontSize,
14183
14255
  color: textColor
14184
14256
  }
14185
14257
  };
@@ -14213,27 +14285,27 @@ function getGaugeRect(boundingRect, title) {
14213
14285
  *
14214
14286
  * Also compute an offset for the text so that it doesn't overlap with other text.
14215
14287
  */
14216
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
14288
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx, labelFontSize = 12) {
14217
14289
  const maxValue = runtime.maxValue;
14218
14290
  const minValue = runtime.minValue;
14219
14291
  const gaugeCircleCenter = {
14220
14292
  x: gaugeRect.x + gaugeRect.width / 2,
14221
14293
  y: gaugeRect.y + gaugeRect.height
14222
14294
  };
14223
- const textStyle = { fontSize: 12 };
14295
+ const textStyle = { fontSize: labelFontSize };
14224
14296
  const inflectionValues = [];
14225
14297
  const inflectionValuesTextRects = [];
14226
14298
  for (const inflectionValue of runtime.inflectionValues) {
14227
14299
  const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
14228
14300
  const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
14229
14301
  const angle = Math.PI - Math.PI * percentage;
14230
- const textRect = getRectangleTangentToCircle(angle, gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, gaugeCircleCenter.x, gaugeCircleCenter.y, labelWidth + 2, 12);
14231
- 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;
14232
14304
  inflectionValuesTextRects.push(textRect);
14233
14305
  inflectionValues.push({
14234
14306
  rotation: angle,
14235
14307
  label: inflectionValue.label,
14236
- fontSize: 12,
14308
+ fontSize: labelFontSize,
14237
14309
  color: textColor,
14238
14310
  offset
14239
14311
  });
@@ -14353,6 +14425,44 @@ const CHART_COMMON_OPTIONS = {
14353
14425
  "mouseup"
14354
14426
  ]
14355
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
+ }
14356
14466
  async function chartToImageUrl(runtime, figure, type) {
14357
14467
  try {
14358
14468
  const canvas = createRenderingSurface(figure.width, figure.height);
@@ -14400,7 +14510,7 @@ async function canvasToObjectUrl(canvas) {
14400
14510
  *
14401
14511
  * @returns a cleanup function to be called after the drawing is no longer needed (to free Chart.js resources)
14402
14512
  */
14403
- function drawChartOnCanvas(canvas, runtime, size, type) {
14513
+ function drawChartOnCanvas(canvas, runtime, size, type, zoom = 1) {
14404
14514
  if ("chartJsConfig" in runtime) {
14405
14515
  if (!globalThis.Chart) throw new Error("Chart.js library is not loaded");
14406
14516
  const extensionsLoaded = areChartJSExtensionsLoaded();
@@ -14416,8 +14526,8 @@ function drawChartOnCanvas(canvas, runtime, size, type) {
14416
14526
  chart.destroy();
14417
14527
  if (!extensionsLoaded) unregisterChartJsExtensions();
14418
14528
  };
14419
- } else if (type === "scorecard") drawScoreChart(getScorecardConfiguration(size, runtime), canvas);
14420
- 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);
14421
14531
  return () => {};
14422
14532
  }
14423
14533
 
@@ -14955,7 +15065,7 @@ var ZoomableChartStore = class extends SpreadsheetStore {
14955
15065
  var ZoomableChartJsComponent = class extends ChartJsComponent {
14956
15066
  static template = "o-spreadsheet-ZoomableChartJsComponent";
14957
15067
  store;
14958
- masterChartCanvas = (0, _odoo_owl.signal)(null);
15068
+ masterChartCanvas = _odoo_owl.signal.ref(HTMLCanvasElement);
14959
15069
  masterChart;
14960
15070
  mode;
14961
15071
  hasLinearScale;
@@ -15709,7 +15819,7 @@ var GaugeChartComponent = class extends Component {
15709
15819
  chartId: types.string(),
15710
15820
  isFullScreen: types.boolean().optional()
15711
15821
  });
15712
- canvas = (0, _odoo_owl.signal)(null);
15822
+ canvas = _odoo_owl.signal.ref(HTMLCanvasElement);
15713
15823
  animationStore;
15714
15824
  zoomStore;
15715
15825
  get runtime() {
@@ -15946,7 +16056,7 @@ var Menu = class extends Component {
15946
16056
  onKeyDown: types.function().optional(),
15947
16057
  disableKeyboardNavigation: types.boolean().optional()
15948
16058
  });
15949
- menuRef = (0, _odoo_owl.signal)(null);
16059
+ menuRef = _odoo_owl.signal.ref();
15950
16060
  setup() {
15951
16061
  useLayoutEffect(() => {
15952
16062
  const menuEl = this.menuRef();
@@ -16066,8 +16176,8 @@ var Popover = class extends Component {
16066
16176
  zIndex: types.number().optional(),
16067
16177
  class: types.string().optional()
16068
16178
  });
16069
- popoverRef = (0, _odoo_owl.signal)(null);
16070
- popoverContentRef = (0, _odoo_owl.signal)(null);
16179
+ popoverRef = _odoo_owl.signal.ref();
16180
+ popoverContentRef = _odoo_owl.signal.ref();
16071
16181
  currentPosition = void 0;
16072
16182
  currentDisplayValue = void 0;
16073
16183
  spreadsheetRect = useSpreadsheetRect();
@@ -16255,7 +16365,7 @@ var MenuPopover = class MenuPopover extends Component {
16255
16365
  isHoveringChild: false
16256
16366
  });
16257
16367
  state = (0, _odoo_owl.proxy)({ hoveredMenu: this.props.autoSelectFirstItem ? this.getNextEnabledMenuItem() : void 0 });
16258
- menuRef = (0, _odoo_owl.signal)(null);
16368
+ menuRef = _odoo_owl.signal.ref();
16259
16369
  openingTimeOut = useTimeOut();
16260
16370
  setup() {
16261
16371
  const domFocusableElementStore = useStore(DOMFocusableElementStore);
@@ -16544,43 +16654,29 @@ var PositionMap = class {
16544
16654
  };
16545
16655
 
16546
16656
  //#endregion
16547
- //#region src/components/tables/hovered_table_store.ts
16548
- var HoveredTableStore = class extends SpreadsheetStore {
16549
- mutators = ["clear", "hover"];
16550
- 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 = [];
16551
16666
  overlayColors = new PositionMap();
16552
- hover(position) {
16553
- if (!this.getters.isDashboard() || deepEquals(this.position, position)) return "noStateChange";
16554
- this.position = position;
16555
- 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
+ }
16556
16674
  }
16557
- clear() {
16558
- this.position = void 0;
16675
+ register(highlightProvider) {
16676
+ this.providers.push(highlightProvider);
16559
16677
  }
16560
- computeOverlay() {
16561
- this.overlayColors = new PositionMap();
16562
- if (!this.position) return;
16563
- const { sheetId, col, row } = this.position;
16564
- const table = this.getters.getTable({
16565
- sheetId,
16566
- col,
16567
- row
16568
- });
16569
- if (!table) return;
16570
- const { left, right, top } = table.range.zone;
16571
- const isTableHeader = row < top + table.config.numberOfHeaders;
16572
- const doesTableRowHaveContent = range(left, right + 1).some((col) => {
16573
- return !this.getters.isColHidden(sheetId, col) && this.getters.getEvaluatedCell({
16574
- sheetId,
16575
- col,
16576
- row
16577
- }).formattedValue;
16578
- });
16579
- if (!isTableHeader && doesTableRowHaveContent) for (let col = left; col <= right; col++) this.overlayColors.set({
16580
- sheetId,
16581
- col,
16582
- row
16583
- }, TABLE_HOVER_BACKGROUND_COLOR);
16678
+ unRegister(highlightProvider) {
16679
+ this.providers = this.providers.filter((h) => h !== highlightProvider);
16584
16680
  }
16585
16681
  };
16586
16682
 
@@ -16592,9 +16688,9 @@ var ClickableCellSortIcon = class extends Component {
16592
16688
  position: types.CellPosition(),
16593
16689
  sortDirection: types.or([types.SortDirection, types.literal("none")])
16594
16690
  });
16595
- hoveredTableStore;
16691
+ hoveredCellOverlayStore;
16596
16692
  setup() {
16597
- this.hoveredTableStore = useStore(HoveredTableStore);
16693
+ this.hoveredCellOverlayStore = useStore(CellHoverOverlayStore);
16598
16694
  }
16599
16695
  get style() {
16600
16696
  const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.position);
@@ -16614,7 +16710,7 @@ var ClickableCellSortIcon = class extends Component {
16614
16710
  }
16615
16711
  }
16616
16712
  getBackgroundColor(cellStyle) {
16617
- const overlayColor = this.hoveredTableStore.overlayColors.get(this.props.position);
16713
+ const overlayColor = this.hoveredCellOverlayStore.overlayColors.get(this.props.position);
16618
16714
  if (overlayColor) return blendColors(cellStyle.fillColor || "#FFFFFF", overlayColor);
16619
16715
  return cellStyle.fillColor || "#FFFFFF";
16620
16716
  }
@@ -21353,7 +21449,7 @@ var HoveredIconStore = class extends SpreadsheetStore {
21353
21449
  //#region src/components/grid_overlay/grid_overlay.ts
21354
21450
  function useCellHovered(env, gridRef) {
21355
21451
  const delayedHoveredCell = useStore(DelayedHoveredCellStore);
21356
- const hoveredTable = useStore(HoveredTableStore);
21452
+ const cellHoverOverlay = useStore(CellHoverOverlayStore);
21357
21453
  const viewStore = useStore(ViewportsStore);
21358
21454
  const hoveredPosition = {
21359
21455
  col: void 0,
@@ -21397,7 +21493,9 @@ function useCellHovered(env, gridRef) {
21397
21493
  if (isChildEvent(gridRef(), zoomedMouseEvent.ev)) {
21398
21494
  ({x, y} = getOffsetRelativeToOverlay(zoomedMouseEvent));
21399
21495
  lastMoved = Date.now();
21400
- hoveredTable.hover(getPosition());
21496
+ const position = getPosition();
21497
+ if (position.col < 0 || position.row < 0) cellHoverOverlay.hover(void 0);
21498
+ else cellHoverOverlay.hover(getPosition());
21401
21499
  }
21402
21500
  }
21403
21501
  function recompute() {
@@ -21408,7 +21506,7 @@ function useCellHovered(env, gridRef) {
21408
21506
  const zoomedMouseEvent = withZoom(env, e);
21409
21507
  const { x, y } = getOffsetRelativeToOverlay(zoomedMouseEvent);
21410
21508
  const gridRect = getElBoundingRect(gridRef());
21411
- 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);
21412
21510
  else return pause();
21413
21511
  }
21414
21512
  (0, _odoo_owl.useListener)(gridRef, "pointermove", (ev) => !env.isMobile() && updateMousePosition(withZoom(env, ev)));
@@ -21445,7 +21543,7 @@ var GridOverlay = class extends Component {
21445
21543
  gridOverlayDimensions: types.string(),
21446
21544
  hasFooter: types.boolean().optional(() => true)
21447
21545
  });
21448
- gridOverlayRef = (0, _odoo_owl.signal)(null);
21546
+ gridOverlayRef = _odoo_owl.signal.ref();
21449
21547
  cellPopovers;
21450
21548
  paintFormatStore;
21451
21549
  hoveredIconStore;
@@ -21472,7 +21570,7 @@ var GridOverlay = class extends Component {
21472
21570
  return el;
21473
21571
  }
21474
21572
  get style() {
21475
- return this.props.gridOverlayDimensions + cssPropertiesToCss({ cursor: this.hoveredIconStore.hoveredIcon ? "pointer" : "default" });
21573
+ return this.props.gridOverlayDimensions + cssPropertiesToCss({ cursor: this.hoveredIconStore.hoveredIcon ? "pointer" : "inherit" });
21476
21574
  }
21477
21575
  get isPaintingFormat() {
21478
21576
  return this.paintFormatStore.isActive;
@@ -22112,8 +22210,9 @@ var FormulaFingerprintStore = class extends SpreadsheetStore {
22112
22210
  const CELL_ANIMATION_DURATION = 200;
22113
22211
  var GridRenderer = class extends DisposableStore {
22114
22212
  renderer;
22213
+ storeGetters = ["drawLayer"];
22115
22214
  fingerprints;
22116
- hoveredTables;
22215
+ cellHoverOverlayStore = this.get(CellHoverOverlayStore);
22117
22216
  hoveredIcon;
22118
22217
  lastRenderSheetId = void 0;
22119
22218
  lastRenderBoxes = /* @__PURE__ */ new Map();
@@ -22127,15 +22226,14 @@ var GridRenderer = class extends DisposableStore {
22127
22226
  const model = get(ModelStore);
22128
22227
  this.getters = model.getters;
22129
22228
  this.fingerprints = get(FormulaFingerprintStore);
22130
- this.hoveredTables = get(HoveredTableStore);
22131
22229
  this.hoveredIcon = get(HoveredIconStore);
22132
22230
  model.on("command-dispatched", this, this.handle);
22133
22231
  model.on("command-finalized", this, this.finalize);
22134
- this.renderer.register(this);
22232
+ if (this.renderer) this.renderer.register(this);
22135
22233
  this.onDispose(() => {
22136
22234
  model.off("command-dispatched", this);
22137
22235
  model.off("command-finalized", this);
22138
- this.renderer.unRegister(this);
22236
+ if (this.renderer) this.renderer.unRegister(this);
22139
22237
  });
22140
22238
  }
22141
22239
  handle(cmd) {
@@ -22679,7 +22777,7 @@ var GridRenderer = class extends DisposableStore {
22679
22777
  border: this.getters.getCellComputedBorder(position) || void 0,
22680
22778
  style,
22681
22779
  dataBarFill,
22682
- overlayColor: this.hoveredTables.overlayColors.get(position),
22780
+ overlayColor: this.cellHoverOverlayStore.overlayColors.get(position),
22683
22781
  isError: cell.type === "error" && !!cell.message || this.getters.isDataValidationInvalid(position),
22684
22782
  icons: cellIcons,
22685
22783
  disabledAnimation: this.zonesWithPreventedAnimationsInNextFrame[sheetId]?.some((z) => isZoneInside(zone, z) || overlap(zone, z))
@@ -22855,6 +22953,7 @@ var GridRenderer = class extends DisposableStore {
22855
22953
  return boxes;
22856
22954
  }
22857
22955
  getBoxesWithAnimations(boxes, oldBoxes, timeStamp) {
22956
+ if (!this.renderer) return boxes;
22858
22957
  this.updateAnimationsProgress(timeStamp);
22859
22958
  this.addNewAnimations(boxes, oldBoxes, timeStamp);
22860
22959
  if (this.animations.size > 0) {
@@ -22999,7 +23098,7 @@ var ScrollBar = class extends Component {
22999
23098
  offset: types.Pixel(),
23000
23099
  onScroll: types.function()
23001
23100
  });
23002
- scrollbarRef = (0, _odoo_owl.signal)(null);
23101
+ scrollbarRef = _odoo_owl.signal.ref();
23003
23102
  scrollbar;
23004
23103
  setup() {
23005
23104
  this.scrollbar = new ScrollBar$1(this.scrollbarRef(), this.props.direction);
@@ -23101,6 +23200,9 @@ var StandaloneViewportStore = class extends SpreadsheetStore {
23101
23200
  zoneToDisplay: this.range.zone,
23102
23201
  getFooterSize: () => 0
23103
23202
  });
23203
+ const cellHoverOverlayStore = this.get(CellHoverOverlayStore);
23204
+ cellHoverOverlayStore.register(this);
23205
+ this.onDispose(() => cellHoverOverlayStore.unRegister(this));
23104
23206
  }
23105
23207
  handle(cmd) {
23106
23208
  this.cachedColDimensions = void 0;
@@ -23249,6 +23351,16 @@ var StandaloneViewportStore = class extends SpreadsheetStore {
23249
23351
  }
23250
23352
  return normalizedWeights;
23251
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
+ }
23252
23364
  };
23253
23365
 
23254
23366
  //#endregion
@@ -23265,10 +23377,11 @@ var StandaloneViewport = class extends Component {
23265
23377
  range: types.Range(),
23266
23378
  canResizeColumns: types.boolean().optional(true),
23267
23379
  onResizeColumns: types.function().optional(),
23268
- columnWeights: types.array().optional()
23380
+ columnWeights: types.array().optional(),
23381
+ size: types.object()
23269
23382
  });
23270
- canvasRef = (0, _odoo_owl.signal)(null);
23271
- containerRef = (0, _odoo_owl.signal)(null);
23383
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
23384
+ containerRef = _odoo_owl.signal.ref();
23272
23385
  store;
23273
23386
  dndState = (0, _odoo_owl.proxy)({ col: void 0 });
23274
23387
  onMouseWheel;
@@ -23280,8 +23393,8 @@ var StandaloneViewport = class extends Component {
23280
23393
  useChildStoreProvider([
23281
23394
  ViewportsStore,
23282
23395
  HoveredIconStore,
23283
- HoveredTableStore,
23284
23396
  ClickableCellsStore,
23397
+ CellHoverOverlayStore,
23285
23398
  DelayedHoveredCellStore,
23286
23399
  CellPopoverStore
23287
23400
  ]);
@@ -23290,18 +23403,10 @@ var StandaloneViewport = class extends Component {
23290
23403
  this.zoomStore = useStore(ZoomStore);
23291
23404
  this.cellPopoverStore = useStore(CellPopoverStore);
23292
23405
  this.rendererStore = useLocalStore(RendererStore, ["Background", "Chart"]);
23293
- const resizeObserver = new ResizeObserver(() => {
23294
- this.store.setContainerSize(this.containerWidth, this.containerHeight);
23295
- });
23296
- (0, _odoo_owl.useEffect)(() => {
23297
- const el = this.containerRef();
23298
- if (el) resizeObserver.observe(el);
23299
- return () => resizeObserver.disconnect();
23300
- });
23301
23406
  (0, _odoo_owl.useEffect)(() => {
23302
23407
  this.store.setRange(this.props.range);
23303
23408
  if (this.dndState.col === void 0) this.store.setCustomColWeights(this.props.columnWeights);
23304
- this.store.setContainerSize(this.containerWidth, this.containerHeight);
23409
+ this.store.setContainerSize(this.contentWidth, this.props.size.height);
23305
23410
  });
23306
23411
  useGridDrawing({
23307
23412
  canvasRef: this.canvasRef,
@@ -23320,14 +23425,11 @@ var StandaloneViewport = class extends Component {
23320
23425
  }
23321
23426
  });
23322
23427
  }
23323
- get containerWidth() {
23324
- return Math.floor(getElBoundingRect(this.containerRef()).width / this.zoomStore.zoomLevel);
23325
- }
23326
- get containerHeight() {
23327
- 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;
23328
23430
  }
23329
23431
  get hasVerticalScrollBar() {
23330
- 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;
23331
23433
  }
23332
23434
  get scrollBarContainerStyle() {
23333
23435
  return cssPropertiesToCss({ width: `${this.zoomStore.scrollBarWidth}px` });
@@ -23379,7 +23481,7 @@ var StandaloneViewport = class extends Component {
23379
23481
  };
23380
23482
  const onMouseMove = (ev) => {
23381
23483
  deltaX = withZoom(this.env, ev).clientX - initialX;
23382
- const weightDelta = deltaX / this.containerWidth * totalWeight;
23484
+ const weightDelta = deltaX / this.props.size.width * totalWeight;
23383
23485
  this.store.resizeColumn(resizer.col, weightDelta, startingColWeights);
23384
23486
  };
23385
23487
  startDnd(onMouseMove, onMouseUp);
@@ -23409,8 +23511,8 @@ var Select = class extends Component {
23409
23511
  name: types.string().optional(),
23410
23512
  title: types.string().optional()
23411
23513
  });
23412
- selectRef = (0, _odoo_owl.signal)(null);
23413
- dropdownRef = (0, _odoo_owl.signal)(null);
23514
+ selectRef = _odoo_owl.signal.ref();
23515
+ dropdownRef = _odoo_owl.signal.ref();
23414
23516
  state = (0, _odoo_owl.proxy)({
23415
23517
  isPopoverOpen: false,
23416
23518
  hoveredValue: void 0
@@ -23520,7 +23622,7 @@ var InfoPopover = class extends Component {
23520
23622
  setup() {
23521
23623
  useExternalListener(window, "click", this.onExternalClick, { capture: true });
23522
23624
  }
23523
- infoRef = (0, _odoo_owl.signal)(null);
23625
+ infoRef = _odoo_owl.signal.ref();
23524
23626
  get popoverProps() {
23525
23627
  return {
23526
23628
  anchorRect: this.props.anchorRect,
@@ -23665,8 +23767,8 @@ var CarouselFigure = class extends Component {
23665
23767
  isFullScreen: types.boolean().optional(),
23666
23768
  openContextMenu: types.function().optional()
23667
23769
  });
23668
- carouselTabsRef = (0, _odoo_owl.signal)(null);
23669
- carouselTabsDropdownRef = (0, _odoo_owl.signal)(null);
23770
+ carouselTabsRef = _odoo_owl.signal.ref();
23771
+ carouselTabsDropdownRef = _odoo_owl.signal.ref();
23670
23772
  menuState = (0, _odoo_owl.proxy)({
23671
23773
  isOpen: false,
23672
23774
  anchorRect: null,
@@ -23734,6 +23836,19 @@ var CarouselFigure = class extends Component {
23734
23836
  } else cssProperties["background-color"] = backgroundColor;
23735
23837
  return cssPropertiesToCss(cssProperties);
23736
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
+ }
23737
23852
  get title() {
23738
23853
  return this.env.model.getters.dynamicTranslate(this.carousel.title?.text ?? "");
23739
23854
  }
@@ -23743,6 +23858,14 @@ var CarouselFigure = class extends Component {
23743
23858
  ...this.carousel.title
23744
23859
  })));
23745
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
+ }
23746
23869
  updateTabsVisibility() {
23747
23870
  const tabsContainerEl = this.carouselTabsRef();
23748
23871
  const dropDownEl = this.carouselTabsDropdownRef();
@@ -23834,6 +23957,13 @@ var CarouselFigure = class extends Component {
23834
23957
  get canResizeDataViewColumns() {
23835
23958
  return !this.env.model.getters.isReadonly() && !this.env.model.getters.isSheetLocked(this.env.model.getters.getActiveSheetId());
23836
23959
  }
23960
+ get carouselLayout() {
23961
+ return getCarouselLayout({
23962
+ ...this.props.figureUI,
23963
+ x: 0,
23964
+ y: 0
23965
+ }, this.carousel, this.selectedCarouselItem);
23966
+ }
23837
23967
  };
23838
23968
 
23839
23969
  //#endregion
@@ -23874,7 +24004,7 @@ var ImageFigure = class extends Component {
23874
24004
  figureUI: types.FigureUI(),
23875
24005
  openContextMenu: types.function()
23876
24006
  });
23877
- menuButtonRef = (0, _odoo_owl.signal)(null);
24007
+ menuButtonRef = _odoo_owl.signal.ref();
23878
24008
  showMenu(ev) {
23879
24009
  if (!this.env.model.getters.getSelectedFigureIds().includes(this.props.figureUI.id)) this.env.model.dispatch("SELECT_FIGURE", {
23880
24010
  figureId: this.props.figureUI.id,
@@ -23902,7 +24032,8 @@ figureRegistry.add("chart", {
23902
24032
  menuBuilder: getChartMenuActions,
23903
24033
  borderWidth: (getters) => getters.isDashboard() ? 0 : 1,
23904
24034
  hasShadow: (getters) => getters.isDashboard(),
23905
- isRounded: (getters) => getters.isDashboard()
24035
+ isRounded: (getters) => getters.isDashboard(),
24036
+ isThemeDependant: true
23906
24037
  });
23907
24038
  figureRegistry.add("image", {
23908
24039
  Component: ImageFigure,
@@ -23911,14 +24042,16 @@ figureRegistry.add("image", {
23911
24042
  borderWidth: () => 0,
23912
24043
  hasShadow: () => false,
23913
24044
  isRounded: () => false,
23914
- menuBuilder: getImageMenuActions
24045
+ menuBuilder: getImageMenuActions,
24046
+ isThemeDependant: false
23915
24047
  });
23916
24048
  figureRegistry.add("carousel", {
23917
24049
  Component: CarouselFigure,
23918
24050
  menuBuilder: getCarouselMenuActions,
23919
24051
  borderWidth: (getters) => getters.isDashboard() ? 0 : 1,
23920
24052
  hasShadow: (getters) => getters.isDashboard(),
23921
- isRounded: (getters) => getters.isDashboard()
24053
+ isRounded: (getters) => getters.isDashboard(),
24054
+ isThemeDependant: true
23922
24055
  });
23923
24056
 
23924
24057
  //#endregion
@@ -23940,10 +24073,13 @@ var FigureComponent = class extends Component {
23940
24073
  anchorRect: null,
23941
24074
  menuItems: []
23942
24075
  });
23943
- figureRef = (0, _odoo_owl.signal)(null);
24076
+ figureRef = _odoo_owl.signal.ref();
23944
24077
  get isSelected() {
23945
24078
  return !this.env.model.getters.isDashboard() && this.env.model.getters.getSelectedFigureIds().includes(this.props.figureUI.id);
23946
24079
  }
24080
+ get isThemeDependant() {
24081
+ return figureRegistry.get(this.props.figureUI.tag).isThemeDependant;
24082
+ }
23947
24083
  get figureRegistry() {
23948
24084
  return figureRegistry;
23949
24085
  }
@@ -29542,6 +29678,35 @@ async function interactivePasteFromOS(env, target, parsedClipboardContent, paste
29542
29678
  }));
29543
29679
  }
29544
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
+
29545
29710
  //#endregion
29546
29711
  //#region src/helpers/figures/charts/smart_chart_engine.ts
29547
29712
  const DEFAULT_BAR_CHART_CONFIG = {
@@ -29572,9 +29737,6 @@ const DEFAULT_LINE_CHART_CONFIG = {
29572
29737
  labelsAsText: false,
29573
29738
  humanize: true
29574
29739
  };
29575
- function getUnboundRange(getters, zone) {
29576
- return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
29577
- }
29578
29740
  function detectColumnType(cells) {
29579
29741
  if (!cells.length) return "empty";
29580
29742
  const counts = {
@@ -29621,14 +29783,6 @@ function getCellStats(getters, zone) {
29621
29783
  totalCount: values.length
29622
29784
  };
29623
29785
  }
29624
- function isDatasetTitled(getters, column) {
29625
- const titleCell = getters.getEvaluatedCell({
29626
- sheetId: getters.getActiveSheetId(),
29627
- col: column.zone.left,
29628
- row: column.zone.top
29629
- });
29630
- return !["number", "empty"].includes(titleCell.type);
29631
- }
29632
29786
  /**
29633
29787
  * Builds a chart definition for a single column selection. The logic to detect the chart type is as follows:
29634
29788
  * - If the column contains a single cell, create a scorecard.
@@ -29640,8 +29794,7 @@ function isDatasetTitled(getters, column) {
29640
29794
  function buildSingleColumnChart(column, getters) {
29641
29795
  const { type, zone } = column;
29642
29796
  const sheetId = getters.getActiveSheetId();
29643
- const dataSetsHaveTitle = isDatasetTitled(getters, column);
29644
- const dataRange = getUnboundRange(getters, zone);
29797
+ const dataSetsHaveTitle = isDatasetTitled(getters, column.zone);
29645
29798
  const titleCell = getters.getEvaluatedCell({
29646
29799
  sheetId,
29647
29800
  col: zone.left,
@@ -29652,14 +29805,7 @@ function buildSingleColumnChart(column, getters) {
29652
29805
  case "percentage": return {
29653
29806
  type: "pie",
29654
29807
  title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29655
- dataSource: {
29656
- type: "range",
29657
- dataSets: [{
29658
- dataRange,
29659
- dataSetId: "0"
29660
- }],
29661
- dataSetsHaveTitle
29662
- },
29808
+ dataSource: rangeSource([dataset(zone, getters)], dataSetsHaveTitle),
29663
29809
  dataSetStyles: {},
29664
29810
  legendPosition: "none"
29665
29811
  };
@@ -29670,45 +29816,28 @@ function buildSingleColumnChart(column, getters) {
29670
29816
  return {
29671
29817
  type: "pie",
29672
29818
  title: hasUniqueTitle ? { text: String(titleCell.value) } : {},
29673
- dataSource: {
29674
- type: "range",
29675
- dataSets: [{
29676
- dataRange,
29677
- dataSetId: "0"
29678
- }],
29679
- labelRange: dataRange,
29680
- dataSetsHaveTitle: hasUniqueTitle
29681
- },
29819
+ dataSource: rangeSource([dataset(zone, getters)], hasUniqueTitle, getUnboundRange(getters, zone)),
29682
29820
  dataSetStyles: {},
29683
29821
  aggregated: true,
29684
29822
  legendPosition: "top"
29685
29823
  };
29686
- case "date": return {
29687
- type: "calendar",
29688
- title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29689
- dataSource: {
29690
- type: "range",
29691
- dataSets: [],
29692
- dataSetsHaveTitle,
29693
- labelRange: dataRange
29694
- },
29695
- dataSetStyles: {},
29696
- legendPosition: "left",
29697
- horizontalGroupBy: "day_of_week",
29698
- verticalGroupBy: "month_number"
29699
- };
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
+ }
29700
29836
  }
29701
29837
  return {
29702
29838
  ...DEFAULT_BAR_CHART_CONFIG,
29703
29839
  title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
29704
- dataSource: {
29705
- type: "range",
29706
- dataSets: [{
29707
- dataRange,
29708
- dataSetId: "0"
29709
- }],
29710
- dataSetsHaveTitle
29711
- },
29840
+ dataSource: rangeSource([dataset(zone, getters)], dataSetsHaveTitle),
29712
29841
  dataSetStyles: {}
29713
29842
  };
29714
29843
  }
@@ -29726,15 +29855,7 @@ function buildTwoColumnChart(columns, getters) {
29726
29855
  if (columns[1].type === "percentage") return {
29727
29856
  type: "pie",
29728
29857
  title: {},
29729
- dataSource: {
29730
- type: "range",
29731
- dataSets: [{
29732
- dataRange: getUnboundRange(getters, columns[1].zone),
29733
- dataSetId: "0"
29734
- }],
29735
- labelRange: getUnboundRange(getters, columns[0].zone),
29736
- dataSetsHaveTitle: isDatasetTitled(getters, columns[1])
29737
- },
29858
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[1].zone), getUnboundRange(getters, columns[0].zone)),
29738
29859
  dataSetStyles: {},
29739
29860
  aggregated: true,
29740
29861
  legendPosition: "none"
@@ -29742,15 +29863,7 @@ function buildTwoColumnChart(columns, getters) {
29742
29863
  if (columns[0].type === "number" && columns[1].type === "number") return {
29743
29864
  type: "scatter",
29744
29865
  title: {},
29745
- dataSource: {
29746
- type: "range",
29747
- dataSets: [{
29748
- dataRange: getUnboundRange(getters, columns[1].zone),
29749
- dataSetId: "0"
29750
- }],
29751
- labelRange: getUnboundRange(getters, columns[0].zone),
29752
- dataSetsHaveTitle: isDatasetTitled(getters, columns[1])
29753
- },
29866
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[1].zone), getUnboundRange(getters, columns[0].zone)),
29754
29867
  dataSetStyles: {},
29755
29868
  labelsAsText: false,
29756
29869
  legendPosition: "none"
@@ -29758,49 +29871,25 @@ function buildTwoColumnChart(columns, getters) {
29758
29871
  if (columns[0].type === "date" && columns[1].type === "number") return {
29759
29872
  ...DEFAULT_LINE_CHART_CONFIG,
29760
29873
  type: "line",
29761
- dataSource: {
29762
- type: "range",
29763
- dataSets: [{
29764
- dataRange: getUnboundRange(getters, columns[1].zone),
29765
- dataSetId: "0"
29766
- }],
29767
- labelRange: getUnboundRange(getters, columns[0].zone),
29768
- dataSetsHaveTitle: isDatasetTitled(getters, columns[0])
29769
- },
29874
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[0].zone), getUnboundRange(getters, columns[0].zone)),
29770
29875
  dataSetStyles: {}
29771
29876
  };
29772
29877
  if (columns[0].type === "text" && columns[1].type === "number") {
29773
29878
  const textColumn = columns[0];
29774
29879
  const numberColumn = columns[1];
29775
29880
  const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
29776
- const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn);
29881
+ const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn.zone);
29777
29882
  if (uniqueCount !== totalCount) return {
29778
29883
  type: "treemap",
29779
29884
  title: {},
29780
- dataSource: {
29781
- type: "range",
29782
- dataSets: [{
29783
- dataRange: getUnboundRange(getters, textColumn.zone),
29784
- dataSetId: "0"
29785
- }],
29786
- labelRange: getUnboundRange(getters, numberColumn.zone),
29787
- dataSetsHaveTitle
29788
- },
29885
+ dataSource: rangeSource([dataset(textColumn.zone, getters)], dataSetsHaveTitle, getUnboundRange(getters, numberColumn.zone)),
29789
29886
  dataSetStyles: {},
29790
29887
  legendPosition: "none"
29791
29888
  };
29792
29889
  }
29793
29890
  return {
29794
29891
  ...DEFAULT_BAR_CHART_CONFIG,
29795
- dataSource: {
29796
- type: "range",
29797
- dataSets: [{
29798
- dataRange: getUnboundRange(getters, columns[1].zone),
29799
- dataSetId: "0"
29800
- }],
29801
- labelRange: getUnboundRange(getters, columns[0].zone),
29802
- dataSetsHaveTitle: isDatasetTitled(getters, columns[1])
29803
- },
29892
+ dataSource: rangeSource([dataset(columns[1].zone, getters)], isDatasetTitled(getters, columns[1].zone), getUnboundRange(getters, columns[0].zone)),
29804
29893
  dataSetStyles: {}
29805
29894
  };
29806
29895
  }
@@ -29814,42 +29903,26 @@ function buildTwoColumnChart(columns, getters) {
29814
29903
  */
29815
29904
  function buildMultiColumnChart(columns, getters) {
29816
29905
  if (columns.length < 3) throw new Error("buildMultiColumnChart expects at least three columns");
29817
- const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col));
29906
+ const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col.zone));
29818
29907
  const lastColumn = columns[columns.length - 1];
29819
29908
  const columnsExceptLast = columns.slice(0, columns.length - 1);
29820
29909
  if ((lastColumn.type === "percentage" || lastColumn.type === "number") && columnsExceptLast.every((col) => col.type === "text")) {
29821
- const dataSets = columnsExceptLast.map(({ zone }, i) => ({
29822
- dataRange: getUnboundRange(getters, zone),
29823
- dataSetId: i.toString()
29824
- }));
29910
+ const dataSets = columnsExceptLast.map(({ zone }, i) => dataset(zone, getters, String(i)));
29825
29911
  return {
29826
29912
  type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap",
29827
29913
  title: {},
29828
- dataSource: {
29829
- type: "range",
29830
- dataSets,
29831
- dataSetsHaveTitle,
29832
- labelRange: getUnboundRange(getters, lastColumn.zone)
29833
- },
29914
+ dataSource: rangeSource(dataSets, dataSetsHaveTitle, getUnboundRange(getters, lastColumn.zone)),
29834
29915
  dataSetStyles: {},
29835
29916
  legendPosition: "none"
29836
29917
  };
29837
29918
  }
29838
29919
  const firstColumn = columns[0];
29839
29920
  const columnsExceptFirst = columns.slice(1);
29840
- const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }, i) => ({
29841
- dataRange: getUnboundRange(getters, zone),
29842
- dataSetId: i.toString()
29843
- }));
29921
+ const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }, i) => dataset(zone, getters, String(i)));
29844
29922
  if (columnsExceptFirst.every((col) => col.type === "percentage")) return {
29845
29923
  type: "pie",
29846
29924
  title: {},
29847
- dataSource: {
29848
- type: "range",
29849
- dataSets: rangesOfColumnsExceptFirst,
29850
- labelRange: getUnboundRange(getters, firstColumn.zone),
29851
- dataSetsHaveTitle
29852
- },
29925
+ dataSource: rangeSource(rangesOfColumnsExceptFirst, dataSetsHaveTitle, getUnboundRange(getters, firstColumn.zone)),
29853
29926
  dataSetStyles: {},
29854
29927
  aggregated: false,
29855
29928
  legendPosition: "top"
@@ -29857,23 +29930,13 @@ function buildMultiColumnChart(columns, getters) {
29857
29930
  if (firstColumn.type === "date" && columnsExceptFirst.every((col) => col.type === "number")) return {
29858
29931
  ...DEFAULT_LINE_CHART_CONFIG,
29859
29932
  type: "line",
29860
- dataSource: {
29861
- type: "range",
29862
- dataSets: rangesOfColumnsExceptFirst,
29863
- labelRange: getUnboundRange(getters, firstColumn.zone),
29864
- dataSetsHaveTitle
29865
- },
29933
+ dataSource: rangeSource(rangesOfColumnsExceptFirst, dataSetsHaveTitle, getUnboundRange(getters, firstColumn.zone)),
29866
29934
  dataSetStyles: {},
29867
29935
  legendPosition: "top"
29868
29936
  };
29869
29937
  return {
29870
29938
  ...DEFAULT_BAR_CHART_CONFIG,
29871
- dataSource: {
29872
- type: "range",
29873
- dataSets: rangesOfColumnsExceptFirst,
29874
- labelRange: getUnboundRange(getters, firstColumn.zone),
29875
- dataSetsHaveTitle
29876
- },
29939
+ dataSource: rangeSource(rangesOfColumnsExceptFirst, dataSetsHaveTitle, getUnboundRange(getters, firstColumn.zone)),
29877
29940
  dataSetStyles: {},
29878
29941
  legendPosition: "top"
29879
29942
  };
@@ -29900,17 +29963,10 @@ function buildScorecard(zone, getters) {
29900
29963
  function getSmartChartDefinition(zones, getters) {
29901
29964
  const columns = categorizeColumns(zones, getters);
29902
29965
  if (columns.length === 0 || columns.every((col) => col.type === "empty")) {
29903
- const dataSets = columns.map(({ zone }, i) => ({
29904
- dataRange: getUnboundRange(getters, zone),
29905
- dataSetId: i.toString()
29906
- }));
29966
+ const dataSets = columns.map(({ zone }, i) => dataset(zone, getters, String(i)));
29907
29967
  return {
29908
29968
  ...DEFAULT_BAR_CHART_CONFIG,
29909
- dataSource: {
29910
- type: "range",
29911
- dataSets,
29912
- dataSetsHaveTitle: false
29913
- }
29969
+ dataSource: rangeSource(dataSets, false)
29914
29970
  };
29915
29971
  }
29916
29972
  const nonEmptyColumns = columns.filter((col) => col.type !== "empty");
@@ -31598,7 +31654,7 @@ var SelectionInput = class extends Component {
31598
31654
  dragAndDrop = useDragAndDropListItems();
31599
31655
  focusedInputRef = _odoo_owl.signal.ref(HTMLInputElement);
31600
31656
  unfocusedInputRef = _odoo_owl.signal.ref(HTMLInputElement);
31601
- selectionRef = (0, _odoo_owl.signal)(null);
31657
+ selectionRef = _odoo_owl.signal.ref();
31602
31658
  DOMFocusableElementStore;
31603
31659
  store;
31604
31660
  get ranges() {
@@ -32105,7 +32161,7 @@ var ColorPickerWidget = class extends Component {
32105
32161
  dropdownMaxHeight: types.Pixel().optional(),
32106
32162
  class: types.string().optional()
32107
32163
  });
32108
- colorPickerButtonRef = (0, _odoo_owl.signal)(null);
32164
+ colorPickerButtonRef = _odoo_owl.signal.ref();
32109
32165
  get iconStyle() {
32110
32166
  return this.props.currentColor ? `border-color: ${this.props.currentColor}` : "border-bottom-style: hidden";
32111
32167
  }
@@ -32133,8 +32189,8 @@ var NumberEditor = class extends Component {
32133
32189
  });
32134
32190
  dropdown = (0, _odoo_owl.proxy)({ isOpen: false });
32135
32191
  inputRef = _odoo_owl.signal.ref(HTMLInputElement);
32136
- rootEditorRef = (0, _odoo_owl.signal)(null);
32137
- valueListRef = (0, _odoo_owl.signal)(null);
32192
+ rootEditorRef = _odoo_owl.signal.ref();
32193
+ valueListRef = _odoo_owl.signal.ref();
32138
32194
  DOMFocusableElementStore;
32139
32195
  setup() {
32140
32196
  this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
@@ -32440,7 +32496,7 @@ var ChartTypePickerPopover = class extends Component {
32440
32496
  });
32441
32497
  categories = chartCategories;
32442
32498
  chartTypeByCategories = {};
32443
- popoverRef = (0, _odoo_owl.signal)(null);
32499
+ popoverRef = _odoo_owl.signal.ref();
32444
32500
  setup() {
32445
32501
  (0, _odoo_owl.useListener)(window, "pointerdown", this.onExternalClick.bind(this), { capture: true });
32446
32502
  for (const subtypeProperties of chartSubtypeRegistry.getAll()) {
@@ -32465,7 +32521,7 @@ var CogWheelMenu = class extends Component {
32465
32521
  static template = "o-spreadsheet-CogWheelMenu";
32466
32522
  static components = { MenuPopover };
32467
32523
  props = (0, _odoo_owl.useProps)({ items: types.array(types.ActionSpec()) });
32468
- buttonRef = (0, _odoo_owl.signal)(null);
32524
+ buttonRef = _odoo_owl.signal.ref();
32469
32525
  menuState = (0, _odoo_owl.proxy)({
32470
32526
  isOpen: false,
32471
32527
  anchorRect: null,
@@ -32509,8 +32565,8 @@ var CarouselPanel = class extends Component {
32509
32565
  });
32510
32566
  DEFAULT_CAROUSEL_TITLE_STYLE = DEFAULT_CAROUSEL_TITLE_STYLE;
32511
32567
  dragAndDrop = useDragAndDropListItems();
32512
- previewListRef = (0, _odoo_owl.signal)(null);
32513
- addChartButton = (0, _odoo_owl.signal)(null);
32568
+ previewListRef = _odoo_owl.signal.ref();
32569
+ addChartButton = _odoo_owl.signal.ref(HTMLButtonElement);
32514
32570
  state = (0, _odoo_owl.proxy)({
32515
32571
  popoverProps: void 0,
32516
32572
  currentRange: void 0
@@ -32954,7 +33010,7 @@ var BarConfigPanel = class extends GenericChartConfigPanel {
32954
33010
  var Collapse = class extends Component {
32955
33011
  static template = "o-spreadsheet-Collapse";
32956
33012
  props = (0, _odoo_owl.useProps)({ isCollapsed: types.boolean() });
32957
- contentRef = (0, _odoo_owl.signal)(null);
33013
+ contentRef = _odoo_owl.signal.ref();
32958
33014
  setup() {
32959
33015
  (0, _odoo_owl.onMounted)(() => {
32960
33016
  if (this.props.isCollapsed) this.contentRef()?.classList.add("d-none");
@@ -33007,7 +33063,7 @@ var ChartAnnotation = class extends Component {
33007
33063
  TextInput
33008
33064
  };
33009
33065
  props = (0, _odoo_owl.useProps)(chartSidePanelPropsDefinition);
33010
- editorRef = (0, _odoo_owl.signal)(null);
33066
+ editorRef = _odoo_owl.signal.ref();
33011
33067
  state = (0, _odoo_owl.proxy)({
33012
33068
  annotationTextInput: this.props.definition.annotationText || "",
33013
33069
  isOverflowing: false
@@ -33329,7 +33385,7 @@ var RoundColorPicker = class extends Component {
33329
33385
  onColorPicked: types.function(),
33330
33386
  disableNoColor: types.boolean().optional()
33331
33387
  });
33332
- colorPickerButtonRef = (0, _odoo_owl.signal)(null);
33388
+ colorPickerButtonRef = _odoo_owl.signal.ref();
33333
33389
  state;
33334
33390
  setup() {
33335
33391
  this.state = (0, _odoo_owl.proxy)({ pickerOpened: false });
@@ -36171,6 +36227,7 @@ var helpers_index_exports$1 = /* @__PURE__ */ __exportAll({
36171
36227
  getDefinedAxis: () => getDefinedAxis,
36172
36228
  getPieColors: () => getPieColors,
36173
36229
  isTrendLineAxis: () => isTrendLineAxis,
36230
+ limitChartConfigDataPoints: () => limitChartConfigDataPoints,
36174
36231
  shouldRemoveFirstLabel: () => shouldRemoveFirstLabel,
36175
36232
  toExcelDataset: () => toExcelDataset,
36176
36233
  toExcelLabelRange: () => toExcelLabelRange,
@@ -36941,7 +36998,7 @@ var ColorScalePicker = class extends Component {
36941
36998
  popoverProps: void 0,
36942
36999
  popoverStyle: ""
36943
37000
  });
36944
- popoverRef = (0, _odoo_owl.signal)(null);
37001
+ popoverRef = _odoo_owl.signal.ref(HTMLTableElement);
36945
37002
  setup() {
36946
37003
  useExternalListener(window, "click", this.closePopover);
36947
37004
  }
@@ -37190,7 +37247,7 @@ var TextValueProvider = class extends Component {
37190
37247
  onValueSelected: types.function(),
37191
37248
  onValueHovered: types.function()
37192
37249
  });
37193
- autoCompleteListRef = (0, _odoo_owl.signal)(null);
37250
+ autoCompleteListRef = _odoo_owl.signal.ref();
37194
37251
  setup() {
37195
37252
  (0, _odoo_owl.useEffect)(() => {
37196
37253
  const selectedIndex = this.props.selectedIndex;
@@ -37481,7 +37538,7 @@ var SpeechBubble = class extends Component {
37481
37538
  anchorRect: types.Rect()
37482
37539
  });
37483
37540
  spreadsheetRect = useSpreadsheetRect();
37484
- bubbleRef = (0, _odoo_owl.signal)(null);
37541
+ bubbleRef = _odoo_owl.signal.ref();
37485
37542
  setup() {
37486
37543
  (0, _odoo_owl.useEffect)(() => {
37487
37544
  const el = this.bubbleRef();
@@ -37526,8 +37583,8 @@ var Composer = class extends Component {
37526
37583
  showAssistant: types.boolean().optional(true)
37527
37584
  });
37528
37585
  DOMFocusableElementStore;
37529
- composerRef = (0, _odoo_owl.signal)(null);
37530
- containerRef = (0, _odoo_owl.signal)(null);
37586
+ composerRef = _odoo_owl.signal.ref();
37587
+ containerRef = _odoo_owl.signal.ref();
37531
37588
  contentHelper = new ContentEditableHelper(this.composerRef());
37532
37589
  composerState = (0, _odoo_owl.proxy)({
37533
37590
  positionStart: 0,
@@ -40029,7 +40086,7 @@ var ChartTypePicker = class extends Component {
40029
40086
  chartId: types.UID(),
40030
40087
  chartPanelStore: types.Store()
40031
40088
  });
40032
- selectRef = (0, _odoo_owl.signal)(null);
40089
+ selectRef = _odoo_owl.signal.ref();
40033
40090
  state = (0, _odoo_owl.proxy)({ popoverProps: void 0 });
40034
40091
  getSupportedChartTypes() {
40035
40092
  let supportedTypes;
@@ -40597,7 +40654,7 @@ var ColumnStatsPanel = class extends Component {
40597
40654
  highlightPositions: []
40598
40655
  });
40599
40656
  store;
40600
- chartCanvasRef = (0, _odoo_owl.signal)(null);
40657
+ chartCanvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
40601
40658
  chart;
40602
40659
  setup() {
40603
40660
  this.store = useStore(ColumnStatisticsStore);
@@ -42025,7 +42082,7 @@ var ConditionalFormatPreview = class extends Component {
42025
42082
  class: types.string()
42026
42083
  });
42027
42084
  icons = ICONS;
42028
- cfPreviewRef = (0, _odoo_owl.signal)(null);
42085
+ cfPreviewRef = _odoo_owl.signal.ref();
42029
42086
  setup() {
42030
42087
  useHighlightsOnHover(this.cfPreviewRef, this);
42031
42088
  }
@@ -42082,7 +42139,7 @@ var ConditionalFormatPreviewList = class extends Component {
42082
42139
  static components = { ConditionalFormatPreview };
42083
42140
  props = (0, _odoo_owl.useProps)({ onCloseSidePanel: types.function() });
42084
42141
  dragAndDrop = useDragAndDropListItems();
42085
- cfListRef = (0, _odoo_owl.signal)(null);
42142
+ cfListRef = _odoo_owl.signal.ref();
42086
42143
  get conditionalFormats() {
42087
42144
  return this.env.model.getters.getConditionalFormats(this.env.model.getters.getActiveSheetId());
42088
42145
  }
@@ -42142,6 +42199,1477 @@ var ConditionalFormatPreviewList = class extends Component {
42142
42199
  }
42143
42200
  };
42144
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
+
42145
43673
  //#endregion
42146
43674
  //#region src/registries/data_source_registry.ts
42147
43675
  const unusedDataSourceRegistry = new Registry();
@@ -42218,7 +43746,7 @@ var DataSourcesCleanup = class extends Component {
42218
43746
  var DataValidationPreview = class extends Component {
42219
43747
  static template = "o-spreadsheet-DataValidationPreview";
42220
43748
  props = (0, _odoo_owl.useProps)({ rule: types.DataValidationRule() });
42221
- dvPreviewRef = (0, _odoo_owl.signal)(null);
43749
+ dvPreviewRef = _odoo_owl.signal.ref();
42222
43750
  setup() {
42223
43751
  useHighlightsOnHover(this.dvPreviewRef, this);
42224
43752
  }
@@ -43717,7 +45245,7 @@ var NamedRangePreview = class extends Component {
43717
45245
  };
43718
45246
  props = (0, _odoo_owl.useProps)({ namedRange: types.NamedRange() });
43719
45247
  state = (0, _odoo_owl.proxy)({});
43720
- namedRangePreviewRef = (0, _odoo_owl.signal)(null);
45248
+ namedRangePreviewRef = _odoo_owl.signal.ref();
43721
45249
  setup() {
43722
45250
  useHighlightsOnHover(this.namedRangePreviewRef, this);
43723
45251
  }
@@ -44208,7 +45736,7 @@ var FilterMenuValueItem = class extends Component {
44208
45736
  onClick: types.function(),
44209
45737
  scrolledTo: types.or([types.literal("top"), types.literal("bottom")]).optional()
44210
45738
  });
44211
- itemRef = (0, _odoo_owl.signal)(null);
45739
+ itemRef = _odoo_owl.signal.ref();
44212
45740
  setup() {
44213
45741
  (0, _odoo_owl.onWillPatch)(() => {
44214
45742
  if (this.props.scrolledTo) this.scrollListToSelectedValue();
@@ -44448,7 +45976,7 @@ var PivotFilterEditor = class extends Component {
44448
45976
  onFiltersUpdated: types.function()
44449
45977
  });
44450
45978
  state;
44451
- buttonFilter = (0, _odoo_owl.signal)(null);
45979
+ buttonFilter = _odoo_owl.signal.ref();
44452
45980
  popover;
44453
45981
  setup() {
44454
45982
  useExternalListener(window, "click", (ev) => {
@@ -44577,11 +46105,11 @@ var AddDimensionButton = class extends Component {
44577
46105
  onFieldPicked: types.function(),
44578
46106
  fields: types.array()
44579
46107
  });
44580
- buttonRef = (0, _odoo_owl.signal)(null);
46108
+ buttonRef = _odoo_owl.signal.ref(HTMLButtonElement);
44581
46109
  popover = (0, _odoo_owl.proxy)({ isOpen: false });
44582
46110
  search = (0, _odoo_owl.proxy)({ input: "" });
44583
46111
  autoComplete;
44584
- autofocusRef = (0, _odoo_owl.signal)(null);
46112
+ autofocusRef = _odoo_owl.signal.ref(HTMLInputElement);
44585
46113
  setup() {
44586
46114
  this.autoComplete = useLocalStore(AutoCompleteStore);
44587
46115
  this.autoComplete.useProvider(this.getProvider());
@@ -44990,7 +46518,7 @@ var PivotLayoutConfigurator = class extends Component {
44990
46518
  getScrollableContainerEl: types.function().optional(),
44991
46519
  pivotId: types.UID()
44992
46520
  });
44993
- dimensionsRef = (0, _odoo_owl.signal)(null);
46521
+ dimensionsRef = _odoo_owl.signal.ref();
44994
46522
  dragAndDrop = useDragAndDropListItems();
44995
46523
  AGGREGATORS = AGGREGATORS;
44996
46524
  isDateOrDatetimeField = isDateOrDatetimeField;
@@ -45061,16 +46589,7 @@ var PivotLayoutConfigurator = class extends Component {
45061
46589
  });
45062
46590
  }
45063
46591
  getDimensionElementsRects() {
45064
- return Array.from(this.dimensionsRef().children).map((el) => {
45065
- const style = getComputedStyle(el);
45066
- const rect = el.getBoundingClientRect();
45067
- return {
45068
- x: rect.x,
45069
- y: rect.y,
45070
- width: rect.width + parseInt(style.marginLeft || "0") + parseInt(style.marginRight || "0"),
45071
- height: rect.height + parseInt(style.marginTop || "0") + parseInt(style.marginBottom || "0")
45072
- };
45073
- });
46592
+ return Array.from(this.dimensionsRef().children).map((el) => getBoundingRectWithMargins(el));
45074
46593
  }
45075
46594
  removeDimension(dimension) {
45076
46595
  const { columns, rows } = this.props.definition;
@@ -45635,7 +47154,7 @@ var PivotSpreadsheetSidePanel = class extends Component {
45635
47154
  });
45636
47155
  store;
45637
47156
  state;
45638
- pivotSidePanelRef = (0, _odoo_owl.signal)(null);
47157
+ pivotSidePanelRef = _odoo_owl.signal.ref();
45639
47158
  setup() {
45640
47159
  this.store = useLocalStore(PivotSidePanelStore, this.props.pivotId);
45641
47160
  this.state = (0, _odoo_owl.proxy)({
@@ -46875,7 +48394,7 @@ var TableStylePreview = class extends Component {
46875
48394
  selected: types.boolean().optional(),
46876
48395
  onClick: types.function().optional()
46877
48396
  });
46878
- canvasRef = (0, _odoo_owl.signal)(null);
48397
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
46879
48398
  menu = (0, _odoo_owl.proxy)({
46880
48399
  isOpen: false,
46881
48400
  anchorRect: null,
@@ -46969,7 +48488,7 @@ var TableStylesPopover = class extends Component {
46969
48488
  tableStyles: types.RecordOf(),
46970
48489
  type: types.or([types.literal("table"), types.literal("pivot")])
46971
48490
  });
46972
- tableStyleListRef = (0, _odoo_owl.signal)(null);
48491
+ tableStyleListRef = _odoo_owl.signal.ref();
46973
48492
  state = (0, _odoo_owl.proxy)({ selectedCategory: this.initialSelectedCategory });
46974
48493
  setup() {
46975
48494
  useExternalListener(window, "click", this.onExternalClick, { capture: true });
@@ -48340,6 +49859,10 @@ sidePanelRegistry.add("ColumnStats", {
48340
49859
  title: _t("Column statistics"),
48341
49860
  Body: ColumnStatsPanel
48342
49861
  });
49862
+ sidePanelRegistry.add("DataAnalysisPanel", {
49863
+ title: _t("Data analysis"),
49864
+ Body: DataAnalysisPanel
49865
+ });
48343
49866
  sidePanelRegistry.add("TableSidePanel", {
48344
49867
  title: _t("Edit table"),
48345
49868
  Body: TablePanel,
@@ -48447,6 +49970,7 @@ var SidePanelStore = class extends SpreadsheetStore {
48447
49970
  "changePanelSize",
48448
49971
  "resetPanelSize",
48449
49972
  "togglePinPanel",
49973
+ "togglePinnedSidePanel",
48450
49974
  "closeMainPanel",
48451
49975
  "changeSpreadsheetWidth",
48452
49976
  "toggleCollapsePanel"
@@ -48598,6 +50122,19 @@ var SidePanelStore = class extends SpreadsheetStore {
48598
50122
  this.secondaryPanel = void 0;
48599
50123
  }
48600
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
+ }
48601
50138
  toggleCollapsePanel(panel) {
48602
50139
  const panelInfo = this[panel];
48603
50140
  if (!panelInfo) return;
@@ -48976,6 +50513,219 @@ var MenuItemRegistry = class extends Registry {
48976
50513
  }
48977
50514
  };
48978
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
+
48979
50729
  //#endregion
48980
50730
  //#region src/helpers/ui/merge_interactive.ts
48981
50731
  const AddMergeInteractiveContent = {
@@ -49190,6 +50940,10 @@ cellMenuRegistry.add("cut", {
49190
50940
  }).addChild("paste_format_only", ["paste_special"], {
49191
50941
  ...pasteSpecialFormat,
49192
50942
  sequence: 20
50943
+ }).add("data_analysis", {
50944
+ ...dataAnalysis,
50945
+ sequence: 50,
50946
+ separator: true
49193
50947
  }).add("add_row_before", {
49194
50948
  ...cellInsertRowsBefore,
49195
50949
  sequence: 70
@@ -49297,211 +51051,6 @@ cellMenuRegistry.add("cut", {
49297
51051
  sequence: 30
49298
51052
  });
49299
51053
 
49300
- //#endregion
49301
- //#region src/helpers/sort_interactive.ts
49302
- function interactiveSortSelection(env, sheetId, anchor, zone, sortDirection) {
49303
- let multiColumns = zone.right > zone.left;
49304
- if (env.model.getters.doesIntersectMerge(sheetId, zone)) {
49305
- multiColumns = false;
49306
- let table;
49307
- for (let row = zone.top; row <= zone.bottom; row++) {
49308
- table = [];
49309
- for (let col = zone.left; col <= zone.right; col++) {
49310
- const merge = env.model.getters.getMerge({
49311
- sheetId,
49312
- col,
49313
- row
49314
- });
49315
- if (merge && !table.includes(merge.id.toString())) table.push(merge.id.toString());
49316
- }
49317
- if (table.length >= 2) {
49318
- multiColumns = true;
49319
- break;
49320
- }
49321
- }
49322
- }
49323
- if (multiColumns) {
49324
- interactiveSort(env, sheetId, anchor, zone, sortDirection);
49325
- return;
49326
- }
49327
- const contiguousZone = env.model.getters.getContiguousZone(sheetId, zone);
49328
- if (isEqual(contiguousZone, zone)) interactiveSort(env, sheetId, anchor, zone, sortDirection);
49329
- 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));
49330
- }
49331
- function interactiveSort(env, sheetId, anchor, zone, sortDirection, sortOptions) {
49332
- const result = env.model.dispatch("SORT_CELLS", {
49333
- sheetId,
49334
- col: anchor.col,
49335
- row: anchor.row,
49336
- zone,
49337
- sortDirection,
49338
- sortOptions
49339
- });
49340
- if (result.isCancelledBecause("InvalidSortZone")) {
49341
- const { col, row } = anchor;
49342
- env.model.selection.selectZone({
49343
- cell: {
49344
- col,
49345
- row
49346
- },
49347
- zone
49348
- });
49349
- env.raiseError(_t("Cannot sort. To sort, select only cells or only merges that have the same size."));
49350
- }
49351
- if (result.isCancelledBecause("SortZoneWithArrayFormulas")) {
49352
- const { col, row } = anchor;
49353
- env.model.selection.selectZone({
49354
- cell: {
49355
- col,
49356
- row
49357
- },
49358
- zone
49359
- });
49360
- env.raiseError(_t("Cannot sort a zone with array formulas."));
49361
- }
49362
- }
49363
-
49364
- //#endregion
49365
- //#region src/stores/trim_whitespace_store.ts
49366
- var TrimWhitespaceStore = class extends SpreadsheetStore {
49367
- mutators = ["trimWhitespace"];
49368
- notificationStore = this.get(NotificationStore);
49369
- trimWhitespace() {
49370
- this.model.dispatch("TRIM_WHITESPACE");
49371
- }
49372
- handle(cmd) {
49373
- switch (cmd.type) {
49374
- case "TRIM_WHITESPACE":
49375
- this._trimWhitespace();
49376
- break;
49377
- }
49378
- }
49379
- _trimWhitespace() {
49380
- const zones = recomputeZones(this.getters.getSelectedZones());
49381
- const sheetId = this.getters.getActiveSheetId();
49382
- let count = 0;
49383
- for (const { col, row } of zones.map(positions).flat()) {
49384
- const cell = this.getters.getCell({
49385
- col,
49386
- row,
49387
- sheetId
49388
- });
49389
- if (!cell) continue;
49390
- const currentContent = !cell.isFormula ? cell.content : cell.compiledFormula.toFormulaString(this.getters);
49391
- const trimmedContent = trimContent(currentContent);
49392
- if (trimmedContent !== currentContent) {
49393
- count += 1;
49394
- this.model.dispatch("UPDATE_CELL", {
49395
- sheetId,
49396
- col,
49397
- row,
49398
- content: trimmedContent
49399
- });
49400
- }
49401
- }
49402
- const text = count ? _t("Trimmed whitespace from %s cells.", count) : _t("No selected cells had whitespace trimmed.");
49403
- this.notificationStore.notifyUser({
49404
- type: "info",
49405
- text,
49406
- sticky: false
49407
- });
49408
- }
49409
- };
49410
-
49411
- //#endregion
49412
- //#region src/actions/data_actions.ts
49413
- const sortRange = {
49414
- name: _t("Sort range"),
49415
- isVisible: IS_ONLY_ONE_RANGE,
49416
- icon: "o-spreadsheet-Icon.SORT_RANGE"
49417
- };
49418
- const sortAscending = {
49419
- name: _t("Ascending (A ⟶ Z)"),
49420
- execute: (env) => {
49421
- const { anchor, zones } = env.model.getters.getSelection();
49422
- interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "asc");
49423
- },
49424
- icon: "o-spreadsheet-Icon.SORT_ASCENDING"
49425
- };
49426
- const dataCleanup = {
49427
- name: _t("Data cleanup"),
49428
- icon: "o-spreadsheet-Icon.DATA_CLEANUP"
49429
- };
49430
- const removeDuplicates = {
49431
- name: _t("Remove duplicates"),
49432
- execute: (env) => {
49433
- if (getZoneArea(env.model.getters.getSelectedZone()) === 1) env.model.selection.selectTableAroundSelection();
49434
- env.openSidePanel("RemoveDuplicates", {});
49435
- },
49436
- isEnabled: (env) => !env.isSmall
49437
- };
49438
- const trimWhitespace = {
49439
- name: _t("Trim whitespace"),
49440
- execute: (env) => {
49441
- env.getStore(TrimWhitespaceStore).trimWhitespace();
49442
- }
49443
- };
49444
- const cleanupDataSources = {
49445
- name: _t("Remove unused data sources"),
49446
- execute: (env) => env.openSidePanel("DataSourceCleanup", {}),
49447
- isEnabled: (env) => !env.isSmall
49448
- };
49449
- const sortDescending = {
49450
- name: _t("Descending (Z ⟶ A)"),
49451
- execute: (env) => {
49452
- const { anchor, zones } = env.model.getters.getSelection();
49453
- interactiveSortSelection(env, env.model.getters.getActiveSheetId(), anchor.cell, zones[0], "desc");
49454
- },
49455
- icon: "o-spreadsheet-Icon.SORT_DESCENDING"
49456
- };
49457
- const createRemoveFilter = { ...CREATE_OR_REMOVE_FILTER_ACTION };
49458
- const createRemoveFilterTool = {
49459
- ...CREATE_OR_REMOVE_FILTER_ACTION,
49460
- isActive: (env) => SELECTED_TABLE_HAS_FILTERS(env)
49461
- };
49462
- const splitToColumns = {
49463
- name: _t("Split text to columns"),
49464
- sequence: 1,
49465
- execute: (env) => env.openSidePanel("SplitToColumns", {}),
49466
- isEnabled: (env) => !env.isSmall && env.model.getters.isSingleColSelected(),
49467
- icon: "o-spreadsheet-Icon.SPLIT_TEXT"
49468
- };
49469
- const columnStatistics = {
49470
- name: _t("Column statistics"),
49471
- execute: (env) => env.openSidePanel("ColumnStats", {}),
49472
- icon: "o-spreadsheet-Icon.COLUMN_STATS"
49473
- };
49474
- const reinsertDynamicPivotMenu = {
49475
- id: "reinsert_dynamic_pivot",
49476
- name: _t("Re-insert dynamic pivot"),
49477
- sequence: 60,
49478
- icon: "o-spreadsheet-Icon.INSERT_PIVOT",
49479
- children: [REINSERT_DYNAMIC_PIVOT_CHILDREN],
49480
- isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
49481
- };
49482
- const reinsertStaticPivotMenu = {
49483
- id: "reinsert_static_pivot",
49484
- name: _t("Re-insert static pivot"),
49485
- sequence: 70,
49486
- icon: "o-spreadsheet-Icon.INSERT_PIVOT",
49487
- children: [REINSERT_STATIC_PIVOT_CHILDREN],
49488
- isVisible: (env) => env.model.getters.getPivotIds().some((id) => env.model.getters.getPivot(id).isValid())
49489
- };
49490
- const calculationMode = {
49491
- name: _t("Calculation"),
49492
- icon: "o-spreadsheet-Icon.CALCULATOR"
49493
- };
49494
- const automaticCalculation = {
49495
- name: _t("Automatic"),
49496
- execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: true }),
49497
- isActive: (env) => env.model.getters.isAutomaticEvaluationEnabled()
49498
- };
49499
- const manualCalculation = {
49500
- name: _t("Manual"),
49501
- execute: (env) => env.model.dispatch("SET_AUTOMATIC_EVALUATION", { enabled: false }),
49502
- isActive: (env) => !env.model.getters.isAutomaticEvaluationEnabled()
49503
- };
49504
-
49505
51054
  //#endregion
49506
51055
  //#region src/registries/menus/col_menu_registry.ts
49507
51056
  const colMenuRegistry = new MenuItemRegistry();
@@ -49535,6 +51084,9 @@ colMenuRegistry.add("cut", {
49535
51084
  }).addChild("sort_descending", ["sort_columns"], {
49536
51085
  ...sortDescending,
49537
51086
  sequence: 20
51087
+ }).add("data_analysis", {
51088
+ ...dataAnalysis,
51089
+ sequence: 55
49538
51090
  }).add("column_statistics", {
49539
51091
  ...columnStatistics,
49540
51092
  sequence: 60,
@@ -52350,14 +53902,15 @@ var FiguresContainer = class extends Component {
52350
53902
  selectedRect: void 0,
52351
53903
  horizontalSnap: void 0,
52352
53904
  verticalSnap: void 0,
52353
- cancelDnd: void 0,
52354
- overlappingChartOrCarousel: void 0
53905
+ cancelDnd: void 0
52355
53906
  });
52356
53907
  viewStore;
52357
53908
  zoomStore;
53909
+ chartDragStore;
52358
53910
  setup() {
52359
53911
  this.viewStore = useStore(ViewportsStore);
52360
53912
  this.zoomStore = useStore(ZoomStore);
53913
+ this.chartDragStore = useStore(ChartDragStore);
52361
53914
  (0, _odoo_owl.onMounted)(() => {
52362
53915
  this.render();
52363
53916
  });
@@ -52371,7 +53924,7 @@ var FiguresContainer = class extends Component {
52371
53924
  this.dnd.selectedRect = void 0;
52372
53925
  this.dnd.horizontalSnap = void 0;
52373
53926
  this.dnd.verticalSnap = void 0;
52374
- this.dnd.overlappingChartOrCarousel = void 0;
53927
+ this.chartDragStore.setHighlightedFigure(void 0);
52375
53928
  this.dnd.cancelDnd = void 0;
52376
53929
  }
52377
53930
  });
@@ -52497,6 +54050,7 @@ var FiguresContainer = class extends Component {
52497
54050
  maxY: this.env.model.getters.getRowDimensions(sheetId, this.env.model.getters.getNumberRows(sheetId) - 1).end
52498
54051
  };
52499
54052
  let hasStartedDnd = false;
54053
+ let overlappingChartOrCarousel = void 0;
52500
54054
  const onMouseMove = (ev) => {
52501
54055
  const currentMousePosition = {
52502
54056
  x: ev.clientX / zoom,
@@ -52508,10 +54062,10 @@ var FiguresContainer = class extends Component {
52508
54062
  hasStartedDnd = true;
52509
54063
  const selectedFigures = dragFigureForMove(currentMousePosition, initialMousePosition, initialFigures, maxDimensions, initialScrollPosition, this.viewStore.activeSheetScrollInfo);
52510
54064
  const draggedFigure = selectedFigures.find((f) => f.id === draggedFigureId);
52511
- let overlappingChartOrCarousel = void 0;
54065
+ overlappingChartOrCarousel = void 0;
52512
54066
  const otherFigures = this.getOtherFigures(selectedFigures.map((f) => f.id));
52513
- if (draggedFigure && !selectedFigures.find((f) => f.tag !== "chart")) overlappingChartOrCarousel = this.getOverlappingFigure(draggedFigure, otherFigures);
52514
- 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);
52515
54069
  if (!overlappingChartOrCarousel) {
52516
54070
  const snapReturn = snapForMove(this.env, selectedFigures, otherFigures);
52517
54071
  this.dnd.selectedFigures = snapReturn.snappedFigures;
@@ -52533,7 +54087,7 @@ var FiguresContainer = class extends Component {
52533
54087
  else this.env.model.dispatch("SELECT_FIGURE", { figureId: figureUI.id });
52534
54088
  return;
52535
54089
  }
52536
- if (!this.dnd.overlappingChartOrCarousel) {
54090
+ if (!overlappingChartOrCarousel) {
52537
54091
  const payloads = this.dnd.selectedFigures?.map((f) => {
52538
54092
  return {
52539
54093
  sheetId,
@@ -52543,14 +54097,14 @@ var FiguresContainer = class extends Component {
52543
54097
  }) || [];
52544
54098
  this.env.model.dispatch("MOVE_FIGURES", { figures: payloads });
52545
54099
  } else {
52546
- const overlappingFigureId = this.dnd.overlappingChartOrCarousel.id;
54100
+ const overlappingFigureId = overlappingChartOrCarousel.id;
52547
54101
  const chartFigureIds = this.dnd.selectedFigures?.map((f) => f.id) || [];
52548
- 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", {
52549
54103
  sheetId,
52550
54104
  carouselFigureId: overlappingFigureId,
52551
54105
  chartFigureIds
52552
54106
  });
52553
- 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", {
52554
54108
  sheetId,
52555
54109
  baseFigureId: overlappingFigureId,
52556
54110
  chartFigureIds: [overlappingFigureId, ...chartFigureIds]
@@ -52561,7 +54115,7 @@ var FiguresContainer = class extends Component {
52561
54115
  this.dnd.selectedRect = void 0;
52562
54116
  this.dnd.horizontalSnap = void 0;
52563
54117
  this.dnd.verticalSnap = void 0;
52564
- this.dnd.overlappingChartOrCarousel = void 0;
54118
+ this.chartDragStore.setHighlightedFigure(void 0);
52565
54119
  };
52566
54120
  this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
52567
54121
  }
@@ -52617,7 +54171,6 @@ var FiguresContainer = class extends Component {
52617
54171
  this.dnd.selectedRect = void 0;
52618
54172
  this.dnd.horizontalSnap = void 0;
52619
54173
  this.dnd.verticalSnap = void 0;
52620
- this.dnd.overlappingChartOrCarousel = void 0;
52621
54174
  };
52622
54175
  this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
52623
54176
  }
@@ -52627,12 +54180,12 @@ var FiguresContainer = class extends Component {
52627
54180
  getFigureStyle(figureUI) {
52628
54181
  if (figureUI.id !== this.dnd.draggedFigure?.id) return "";
52629
54182
  return cssPropertiesToCss({
52630
- opacity: this.dnd.overlappingChartOrCarousel?.id ? "0.6" : "0.9",
54183
+ opacity: this.chartDragStore.highlightedFigureId ? "0.6" : "0.9",
52631
54184
  cursor: "grabbing"
52632
54185
  });
52633
54186
  }
52634
54187
  getFigureClass(figureUI) {
52635
- if (figureUI.id !== this.dnd.overlappingChartOrCarousel?.id) return "";
54188
+ if (figureUI.id !== this.chartDragStore.highlightedFigureId) return "";
52636
54189
  return "o-add-to-carousel";
52637
54190
  }
52638
54191
  getSnap(snapLine) {
@@ -52675,26 +54228,6 @@ var FiguresContainer = class extends Component {
52675
54228
  height: `100%`
52676
54229
  });
52677
54230
  }
52678
- getOverlappingFigure(figureUI, otherFigures) {
52679
- if (figureUI.tag !== "chart") return;
52680
- const figureCenterX = figureUI.x + figureUI.width / 2;
52681
- const figureCenterY = figureUI.y + figureUI.height / 2;
52682
- let bestMatch;
52683
- let smallestDistance = Infinity;
52684
- for (const figure of otherFigures) {
52685
- if (figure.tag !== "chart" && figure.tag !== "carousel") continue;
52686
- const targetCenterX = figure.x + figure.width / 2;
52687
- const targetCenterY = figure.y + figure.height / 2;
52688
- const distanceX = Math.abs(figureCenterX - targetCenterX);
52689
- const distanceY = Math.abs(figureCenterY - targetCenterY);
52690
- const squaredDistance = distanceX ** 2 + distanceY ** 2;
52691
- if (distanceX <= figureUI.width / 2 && distanceY <= figureUI.height / 2 && squaredDistance < smallestDistance) {
52692
- smallestDistance = squaredDistance;
52693
- bestMatch = figure;
52694
- }
52695
- }
52696
- return bestMatch;
52697
- }
52698
54231
  };
52699
54232
 
52700
54233
  //#endregion
@@ -52994,7 +54527,7 @@ var AbstractResizer = class extends Component {
52994
54527
  var ColResizer = class extends AbstractResizer {
52995
54528
  static template = "o-spreadsheet-ColResizer";
52996
54529
  static components = { UnhideColumnHeaders };
52997
- colResizerRef = (0, _odoo_owl.signal)(null);
54530
+ colResizerRef = _odoo_owl.signal.ref();
52998
54531
  setup() {
52999
54532
  super.setup();
53000
54533
  this.PADDING = 15;
@@ -53128,7 +54661,7 @@ var ColResizer = class extends AbstractResizer {
53128
54661
  var RowResizer = class extends AbstractResizer {
53129
54662
  static template = "o-spreadsheet-RowResizer";
53130
54663
  static components = { UnhideRowHeaders };
53131
- rowResizerRef = (0, _odoo_owl.signal)(null);
54664
+ rowResizerRef = _odoo_owl.signal.ref();
53132
54665
  setup() {
53133
54666
  super.setup();
53134
54667
  this.PADDING = 5;
@@ -53737,9 +55270,8 @@ var TableResizer = class extends Component {
53737
55270
  useStore(TableResizeStore);
53738
55271
  }
53739
55272
  get containerStyle() {
53740
- const tableZone = this.props.table.range.zone;
53741
55273
  const sheetId = this.props.table.range.sheetId;
53742
- if (this.env.model.getters.isReadonly() || this.env.model.getters.isSheetLocked(sheetId)) return cssPropertiesToCss({ display: "none" });
55274
+ const tableZone = this.props.table.range.zone;
53743
55275
  const bottomRight = {
53744
55276
  ...tableZone,
53745
55277
  left: tableZone.right,
@@ -53826,8 +55358,8 @@ var Grid = class extends Component {
53826
55358
  HEADER_HEIGHT = 26;
53827
55359
  HEADER_WIDTH = 48;
53828
55360
  menuState;
53829
- gridRef = (0, _odoo_owl.signal)(null);
53830
- canvasRef = (0, _odoo_owl.signal)(null);
55361
+ gridRef = _odoo_owl.signal.ref();
55362
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
53831
55363
  highlightStore;
53832
55364
  viewStore;
53833
55365
  zoomStore;
@@ -54458,6 +55990,9 @@ var Grid = class extends Component {
54458
55990
  const sheetId = this.env.model.getters.getActiveSheetId();
54459
55991
  return this.env.model.getters.getClientsToDisplay(sheetId);
54460
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
+ }
54461
55996
  };
54462
55997
 
54463
55998
  //#endregion
@@ -54476,7 +56011,7 @@ var FullScreenFigure = class extends Component {
54476
56011
  static template = "o-spreadsheet-FullScreenFigure";
54477
56012
  static components = { ChartFigure };
54478
56013
  fullScreenFigureStore;
54479
- fullScreenFigureRef = (0, _odoo_owl.signal)(null);
56014
+ fullScreenFigureRef = _odoo_owl.signal.ref();
54480
56015
  spreadsheetRect = useSpreadsheetRect();
54481
56016
  figureRegistry = figureRegistry;
54482
56017
  setup() {
@@ -54724,6 +56259,41 @@ var PivotHTMLRenderer = class extends Component {
54724
56259
  }
54725
56260
  };
54726
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
+
54727
56297
  //#endregion
54728
56298
  //#region src/helpers/figures/charts/runtime/helpers_index.ts
54729
56299
  var helpers_index_exports = /* @__PURE__ */ __exportAll({
@@ -55514,7 +57084,8 @@ var Squisher = class {
55514
57084
  * The result of this method is:
55515
57085
  * - if the cell is not a formula, returns the content as is and resets the base formula
55516
57086
  * - if the cell is a formula:
55517
- * - if there is no previous formula, or the normalized formula is different from the previous one, resets the base formula to this one and returns the full formula string
57087
+ * - if there is no previous formula, or the normalized formula is different from the previous one or it contains blacklisted functions (see NonSquishableFunctionRegistry),
57088
+ * resets the base formula to this one and returns the full formula string
55518
57089
  * - else, compares the literal values and range dependencies to the previous formula, and for each parameter:
55519
57090
  * - for numbers: returns a relative change (+N or -N) if possible, else the full number or "=" if unchanged
55520
57091
  * - for strings: returns the full string if changed, else "="
@@ -55522,6 +57093,10 @@ var Squisher = class {
55522
57093
  * */
55523
57094
  squish(cell, forSheetId) {
55524
57095
  if (cell.isFormula) {
57096
+ if (NonSquishableFunctionRegistry.getAll().some((functionName) => cell.compiledFormula.usesSymbol(functionName))) {
57097
+ this.resetBaseFormula();
57098
+ return cell.compiledFormula.toFormulaString(this.getters, this.rangeToStringOptions);
57099
+ }
55525
57100
  let numbers = [];
55526
57101
  let strings = [];
55527
57102
  let references = [];
@@ -65508,7 +67083,7 @@ var Session = class extends EventBus {
65508
67083
  }
65509
67084
  message = {
65510
67085
  ...message,
65511
- commands: this.commandSquisher.squish(revision.commands)
67086
+ commands: revision.commands
65512
67087
  };
65513
67088
  }
65514
67089
  if (this.isReplayingInitialRevisions) throw new Error(`Trying to send a new revision while replaying initial revision. This can lead to endless dispatches every time the spreadsheet is open.
@@ -68005,6 +69580,12 @@ var FigureUIPlugin = class extends UIPlugin {
68005
69580
  const figures = cmd.chartFigureIds.map((id) => this.getters.getFigure(cmd.sheetId, id));
68006
69581
  const baseFigureId = this.getters.getFigure(cmd.sheetId, cmd.baseFigureId);
68007
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;
68008
69589
  }
68009
69590
  return "Success";
68010
69591
  }
@@ -68040,6 +69621,29 @@ var FigureUIPlugin = class extends UIPlugin {
68040
69621
  carouselFigureId,
68041
69622
  chartFigureIds: cmd.chartFigureIds
68042
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;
68043
69647
  }
68044
69648
  }
68045
69649
  getFigureUI(sheetId, figure) {
@@ -69729,6 +71333,9 @@ topbarMenuRegistry.add("file", {
69729
71333
  }).addChild("split_to_columns", ["data"], {
69730
71334
  ...splitToColumns,
69731
71335
  sequence: 20
71336
+ }).addChild("data_analysis", ["data"], {
71337
+ ...dataAnalysis,
71338
+ sequence: 24
69732
71339
  }).addChild("column_statistics", ["data"], {
69733
71340
  ...columnStatistics,
69734
71341
  sequence: 25
@@ -70130,7 +71737,7 @@ var RippleEffect = class extends Component {
70130
71737
  onAnimationEnd: types.function(),
70131
71738
  style: types.string()
70132
71739
  });
70133
- rippleRef = (0, _odoo_owl.signal)(null);
71740
+ rippleRef = _odoo_owl.signal.ref();
70134
71741
  setup() {
70135
71742
  let animation = void 0;
70136
71743
  (0, _odoo_owl.onMounted)(() => {
@@ -70184,7 +71791,7 @@ var Ripple = class extends Component {
70184
71791
  onAnimationEnd: types.function().optional(() => () => {}),
70185
71792
  class: types.string().optional("")
70186
71793
  });
70187
- childContainerRef = (0, _odoo_owl.signal)(null);
71794
+ childContainerRef = _odoo_owl.signal.ref();
70188
71795
  state = (0, _odoo_owl.proxy)({ ripples: [] });
70189
71796
  currentId = 1;
70190
71797
  onClick(ev) {
@@ -70455,9 +72062,9 @@ var BottomBarSheet = class extends Component {
70455
72062
  isEditing: false,
70456
72063
  openedPicker: void 0
70457
72064
  });
70458
- sheetDivRef = (0, _odoo_owl.signal)(null);
70459
- iconRef = (0, _odoo_owl.signal)(null);
70460
- sheetNameRef = (0, _odoo_owl.signal)(null);
72065
+ sheetDivRef = _odoo_owl.signal.ref();
72066
+ iconRef = _odoo_owl.signal.ref();
72067
+ sheetNameRef = _odoo_owl.signal.ref();
70461
72068
  editionState = "initializing";
70462
72069
  DOMFocusableElementStore;
70463
72070
  setup() {
@@ -70741,8 +72348,10 @@ var BottomBarStatistic = class extends Component {
70741
72348
  });
70742
72349
  state = (0, _odoo_owl.proxy)({ selectedStatisticFn: "" });
70743
72350
  store;
72351
+ sidePanelStore;
70744
72352
  setup() {
70745
72353
  this.store = useStore(AggregateStatisticsStore);
72354
+ this.sidePanelStore = useStore(SidePanelStore);
70746
72355
  (0, _odoo_owl.onWillUpdateProps)(() => {
70747
72356
  if (Object.values(this.store.statisticFnResults).every((result) => result?.value === void 0)) this.props.closeContextMenu();
70748
72357
  });
@@ -70782,6 +72391,12 @@ var BottomBarStatistic = class extends Component {
70782
72391
  format: fnValue.format()
70783
72392
  });
70784
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
+ }
70785
72400
  };
70786
72401
 
70787
72402
  //#endregion
@@ -70796,8 +72411,8 @@ var BottomBar = class extends Component {
70796
72411
  BottomBarSheet,
70797
72412
  BottomBarStatistic
70798
72413
  };
70799
- bottomBarRef = (0, _odoo_owl.signal)(null);
70800
- sheetListRef = (0, _odoo_owl.signal)(null);
72414
+ bottomBarRef = _odoo_owl.signal.ref();
72415
+ sheetListRef = _odoo_owl.signal.ref();
70801
72416
  dragAndDrop = useDragAndDropListItems();
70802
72417
  targetScroll = void 0;
70803
72418
  state = (0, _odoo_owl.proxy)({
@@ -71010,12 +72625,13 @@ var SpreadsheetDashboard = class extends Component {
71010
72625
  hoveredCell;
71011
72626
  viewStore;
71012
72627
  zoomStore;
71013
- gridRef = (0, _odoo_owl.signal)(null);
71014
- canvasRef = (0, _odoo_owl.signal)(null);
72628
+ gridRef = _odoo_owl.signal.ref();
72629
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
71015
72630
  setup() {
71016
72631
  this.hoveredCell = useStore(DelayedHoveredCellStore);
71017
72632
  this.viewStore = useStore(ViewportsStore);
71018
72633
  this.zoomStore = useStore(ZoomStore);
72634
+ useLocalStore(HoveredTableStore);
71019
72635
  const rendererStore = useLocalStore(RendererStore, OrderedLayers().filter((layer) => layer !== "Headers"));
71020
72636
  useChildSubEnv({ getPopoverContainerRect: () => this.zoomStore.getZoomedRect(this.getGridRect()) });
71021
72637
  useGridDrawing({
@@ -71433,8 +73049,8 @@ var RibbonMenu = class extends Component {
71433
73049
  static components = { Menu };
71434
73050
  props = (0, _odoo_owl.useProps)({ onClose: types.function() });
71435
73051
  rootItems = topbarMenuRegistry.getMenuItems();
71436
- menuRef = (0, _odoo_owl.signal)(null);
71437
- containerRef = (0, _odoo_owl.signal)(null);
73052
+ menuRef = _odoo_owl.signal.ref();
73053
+ containerRef = _odoo_owl.signal.ref();
71438
73054
  state = (0, _odoo_owl.proxy)({
71439
73055
  menuItems: this.rootItems,
71440
73056
  title: _t("Menu Bar"),
@@ -71507,7 +73123,7 @@ var SmallBottomBar = class extends Component {
71507
73123
  composerStore;
71508
73124
  viewStore;
71509
73125
  composerInterface;
71510
- composerRef = (0, _odoo_owl.signal)(null);
73126
+ composerRef = _odoo_owl.signal.ref();
71511
73127
  menuState = (0, _odoo_owl.proxy)({ isOpen: false });
71512
73128
  setup() {
71513
73129
  this.composerFocusStore = useStore(ComposerFocusStore);
@@ -71586,6 +73202,49 @@ var SmallBottomBar = class extends Component {
71586
73202
  }
71587
73203
  };
71588
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
+
71589
73248
  //#endregion
71590
73249
  //#region src/components/standalone_grid_canvas/figure_renderer_store.ts
71591
73250
  /** Store that draws the figures directly onto the canvas */
@@ -71611,29 +73270,25 @@ var FigureRendererStore = class extends DisposableStore {
71611
73270
  drawLayer(renderingCtx) {
71612
73271
  const { viewports, ctx } = renderingCtx;
71613
73272
  const visibleFigures = viewports.getVisibleFigures(renderingCtx.sheetId);
71614
- const { x: offsetX, y: offsetY } = viewports.getViewportOffset(renderingCtx.sheetId);
73273
+ const scrollOffset = viewports.getViewportOffset(renderingCtx.sheetId);
71615
73274
  for (const figure of visibleFigures) {
71616
- const x = figure.x - offsetX;
71617
- 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;
71618
73281
  if (figure.tag === "chart") {
71619
73282
  const chartId = this.getters.getChartIdFromFigureId(figure.id);
71620
- if (chartId) {
71621
- const chartRect = {
71622
- x,
71623
- y,
71624
- width: figure.width,
71625
- height: figure.height
71626
- };
71627
- this.drawChart(renderingCtx, chartId, chartRect);
71628
- }
73283
+ if (chartId) this.drawChart(renderingCtx, chartId, figureRect);
71629
73284
  } else if (figure.tag === "image") {
71630
73285
  const loadedImage = this.loadedImages[this.getters.getImagePath(figure.id)];
71631
- if (loadedImage) ctx.drawImage(loadedImage, x, y, figure.width, figure.height);
71632
- } 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);
71633
73288
  if (!this.getters.isDashboard()) {
71634
73289
  ctx.strokeStyle = GRAY_400;
71635
73290
  ctx.lineWidth = 1;
71636
- ctx.strokeRect(x, y, figure.width, figure.height);
73291
+ ctx.strokeRect(x, y, width, height);
71637
73292
  }
71638
73293
  }
71639
73294
  }
@@ -71651,53 +73306,37 @@ var FigureRendererStore = class extends DisposableStore {
71651
73306
  renderingCtx.ctx.drawImage(chartCanvas, x, y, width, height);
71652
73307
  cleanUp();
71653
73308
  }
71654
- drawCarousel(renderingCtx, figure) {
71655
- const { viewports: sheetView, ctx } = renderingCtx;
71656
- const { x: offsetX, y: offsetY } = sheetView.getViewportOffset(renderingCtx.sheetId);
71657
- const x = figure.x - offsetX;
71658
- const y = figure.y - offsetY;
73309
+ drawCarousel(renderingCtx, figure, figureRect) {
73310
+ const { ctx } = renderingCtx;
71659
73311
  const carousel = this.getters.getCarousel(figure.id);
71660
73312
  const chartId = this.getters.getChartIdFromFigureId(figure.id);
71661
73313
  if (!carousel) return;
73314
+ const layout = getCarouselLayout(figureRect, carousel, this.getters.getSelectedCarouselItem(figure.id));
71662
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);
71663
73322
  const title = {
71664
73323
  ...DEFAULT_CAROUSEL_TITLE_STYLE,
71665
73324
  ...carousel.title
71666
73325
  };
71667
- const headerPadding = 4;
71668
- const headerSize = title.fontSize + headerPadding * 2;
71669
- if (!title.text && chartId) {
71670
- const chartRect = {
71671
- x,
71672
- y,
71673
- width: figure.width,
71674
- height: figure.height
71675
- };
71676
- this.drawChart(renderingCtx, chartId, chartRect);
71677
- } else if (title.text) {
71678
- ctx.save();
71679
- ctx.fillStyle = chartDefinition?.background || this.getters.getSpreadsheetTheme().backgroundColor;
71680
- ctx.fillRect(x, y, figure.width, headerSize);
71681
- ctx.font = computeTextFont(chartStyleToCellStyle(title));
73326
+ if (title.text) {
73327
+ ctx.font = computeTextFont(chartStyleToCellStyle(title), "px", 500);
71682
73328
  ctx.fillStyle = title.color;
71683
- ctx.fillText(title.text, x + headerPadding, y + headerPadding + title.fontSize);
71684
- ctx.restore();
71685
- const chartRect = {
71686
- x,
71687
- y: y + headerSize,
71688
- width: figure.width,
71689
- height: figure.height - headerSize
71690
- };
71691
- if (chartId) this.drawChart(renderingCtx, chartId, chartRect);
71692
- else if (!this.getters.isDashboard()) {
71693
- ctx.strokeStyle = GRAY_400;
71694
- ctx.lineWidth = 1;
71695
- ctx.beginPath();
71696
- ctx.moveTo(x, y + headerSize);
71697
- ctx.lineTo(x + figure.width, y + headerSize);
71698
- ctx.stroke();
71699
- }
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);
71700
73337
  }
73338
+ ctx.restore();
73339
+ if (chartId) this.drawChart(renderingCtx, chartId, layout.contentRect);
71701
73340
  }
71702
73341
  };
71703
73342
 
@@ -71705,12 +73344,13 @@ var FigureRendererStore = class extends DisposableStore {
71705
73344
  //#region src/components/standalone_grid_canvas/standalone_grid_canvas.ts
71706
73345
  var StandaloneGridCanvas = class extends Component {
71707
73346
  static template = "o-spreadsheet-StandaloneGridCanvas";
73347
+ static components = { CarouselDataViewPrint };
71708
73348
  props = (0, _odoo_owl.useProps)({
71709
73349
  sheetId: types.UID(),
71710
73350
  zone: types.Zone(),
71711
73351
  renderingCtx: types.object()
71712
73352
  });
71713
- canvasRef = (0, _odoo_owl.signal)(null);
73353
+ canvasRef = _odoo_owl.signal.ref(HTMLCanvasElement);
71714
73354
  rendererStore;
71715
73355
  figureRendererStore;
71716
73356
  setup() {
@@ -71732,6 +73372,28 @@ var StandaloneGridCanvas = class extends Component {
71732
73372
  this.figureRendererStore.addLoadedImage(path, await createImageBitmap(blob));
71733
73373
  }));
71734
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
+ }
71735
73397
  };
71736
73398
 
71737
73399
  //#endregion
@@ -72193,7 +73855,7 @@ var NamedRangeSelector = class extends Component {
72193
73855
  anchorRect: null,
72194
73856
  menuItems: []
72195
73857
  });
72196
- namedRangeSelectorRef = (0, _odoo_owl.signal)(null);
73858
+ namedRangeSelectorRef = _odoo_owl.signal.ref();
72197
73859
  setup() {
72198
73860
  this.topBarToolStore = useToolBarDropdownStore();
72199
73861
  this.DOMFocusableElementStore = useStore(DOMFocusableElementStore);
@@ -72391,7 +74053,7 @@ var BorderEditor = class extends Component {
72391
74053
  anchorRect: types.Rect()
72392
74054
  });
72393
74055
  BORDER_POSITIONS = BORDER_POSITIONS;
72394
- lineStyleButtonRef = (0, _odoo_owl.signal)(null);
74056
+ lineStyleButtonRef = _odoo_owl.signal.ref();
72395
74057
  borderStyles = borderStyles;
72396
74058
  state = (0, _odoo_owl.proxy)({ activeTool: void 0 });
72397
74059
  toggleDropdownTool(tool) {
@@ -72458,7 +74120,7 @@ var BorderEditorWidget = class extends Component {
72458
74120
  });
72459
74121
  topBarToolStore;
72460
74122
  viewStore;
72461
- borderEditorButtonRef = (0, _odoo_owl.signal)(null);
74123
+ borderEditorButtonRef = _odoo_owl.signal.ref();
72462
74124
  state = (0, _odoo_owl.proxy)({
72463
74125
  currentColor: DEFAULT_BORDER_DESC.color,
72464
74126
  currentStyle: DEFAULT_BORDER_DESC.style,
@@ -72688,7 +74350,7 @@ var DropdownAction = class extends Component {
72688
74350
  childClass: types.string()
72689
74351
  });
72690
74352
  topBarToolStore;
72691
- actionRef = (0, _odoo_owl.signal)(null);
74353
+ actionRef = _odoo_owl.signal.ref();
72692
74354
  setup() {
72693
74355
  this.topBarToolStore = useToolBarDropdownStore();
72694
74356
  }
@@ -72751,7 +74413,7 @@ var NumberFormatsTool = class extends Component {
72751
74413
  props = (0, _odoo_owl.useProps)({ class: types.string() });
72752
74414
  formatNumberMenuItemSpec = formatNumberMenuItemSpec;
72753
74415
  topBarToolStore;
72754
- buttonRef = (0, _odoo_owl.signal)(null);
74416
+ buttonRef = _odoo_owl.signal.ref();
72755
74417
  state = (0, _odoo_owl.proxy)({
72756
74418
  anchorRect: {
72757
74419
  x: 0,
@@ -73040,12 +74702,12 @@ var TopBar = class extends Component {
73040
74702
  composerFocusStore;
73041
74703
  fingerprints;
73042
74704
  topBarToolStore;
73043
- toolBarContainerRef = (0, _odoo_owl.signal)(null);
73044
- toolbarRef = (0, _odoo_owl.signal)(null);
73045
- namedRangesRef = (0, _odoo_owl.signal)(null);
73046
- topBarTopRef = (0, _odoo_owl.signal)(null);
73047
- moreToolsContainerRef = (0, _odoo_owl.signal)(null);
73048
- 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();
73049
74711
  spreadsheetRect = useSpreadsheetRect();
73050
74712
  setup() {
73051
74713
  this.composerFocusStore = useStore(ComposerFocusStore);
@@ -73307,7 +74969,7 @@ var Spreadsheet = class extends Component {
73307
74969
  SpreadsheetPrint
73308
74970
  };
73309
74971
  sidePanel;
73310
- spreadsheetRef = (0, _odoo_owl.signal)(null);
74972
+ spreadsheetRef = _odoo_owl.signal.ref();
73311
74973
  spreadsheetRect = useSpreadsheetRect();
73312
74974
  state = (0, _odoo_owl.proxy)({ printModeEnabled: false });
73313
74975
  _focusGrid;
@@ -79315,12 +80977,13 @@ var FilterMenu = class extends Component {
79315
80977
  }
79316
80978
  sortFilterZone(sortDirection) {
79317
80979
  const filterPosition = this.props.filterPosition;
79318
- const tableZone = this.table?.range.zone;
80980
+ const table = this.table;
80981
+ const tableZone = table?.range.zone;
79319
80982
  if (!filterPosition || !tableZone || tableZone.top === tableZone.bottom) return;
79320
80983
  const sheetId = this.viewStore.displayedSheetId;
79321
80984
  const contentZone = {
79322
80985
  ...tableZone,
79323
- top: tableZone.top + 1
80986
+ top: tableZone.top + table.config.numberOfHeaders
79324
80987
  };
79325
80988
  const sortAnchor = {
79326
80989
  col: filterPosition.col,
@@ -79478,9 +81141,9 @@ var LinkEditor = class extends Component {
79478
81141
  onClosed: types.function().optional()
79479
81142
  });
79480
81143
  static size = { maxHeight: 500 };
79481
- urlInput = (0, _odoo_owl.signal)(null);
79482
- suggestionListRef = (0, _odoo_owl.signal)(null);
79483
- 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();
79484
81147
  state;
79485
81148
  viewStore;
79486
81149
  setup() {
@@ -81598,7 +83261,7 @@ const ARRAYTOTEXT = {
81598
83261
  else if (_format === 0) {
81599
83262
  const rowSeparator = this.locale.decimalSeparator === "," ? "/" : ",";
81600
83263
  return { value: transposeMatrix(_array).flatMap((row) => row.map((value) => {
81601
- return isEvaluationError(value.value) ? value.value : toString(value);
83264
+ return isEvaluationError(value.value) ? value.value : toLocaleString(value, this.locale);
81602
83265
  })).join(rowSeparator) };
81603
83266
  } else return new EvaluationError(_t("Format must be 0 or 1"));
81604
83267
  },
@@ -87646,7 +89309,7 @@ const CONCAT = {
87646
89309
  description: _t("Concatenation of two values."),
87647
89310
  args: [arg("value1 (string)", _t("The value to which value2 will be appended.")), arg("value2 (string)", _t("The value to append to value1."))],
87648
89311
  compute: function(value1, value2) {
87649
- return { value: toString(value1) + toString(value2) };
89312
+ return { value: toLocaleString(value1, this.locale) + toLocaleString(value2, this.locale) };
87650
89313
  },
87651
89314
  isExported: true
87652
89315
  };
@@ -88529,7 +90192,7 @@ const CLEAN = {
88529
90192
  description: _t("Remove non-printable characters from a piece of text."),
88530
90193
  args: [arg("text (string)", _t("The text whose non-printable characters are to be removed."))],
88531
90194
  compute: function(text) {
88532
- const _text = toString(text);
90195
+ const _text = toLocaleString(text, this.locale);
88533
90196
  let cleanedStr = "";
88534
90197
  for (const char of _text) if (char && char.charCodeAt(0) > 31) cleanedStr += char;
88535
90198
  return { value: cleanedStr };
@@ -88540,7 +90203,7 @@ const CONCATENATE = {
88540
90203
  description: _t("Appends strings to one another."),
88541
90204
  args: [arg("string (string, range<string>, repeating)", _t("String to append in sequence."))],
88542
90205
  compute: function(...datas) {
88543
- return { value: reduceAny(datas, (acc, a) => acc + toString(a), "") };
90206
+ return { value: reduceAny(datas, (acc, a) => acc + toLocaleString(a, this.locale), "") };
88544
90207
  },
88545
90208
  isExported: true
88546
90209
  };
@@ -88548,7 +90211,7 @@ const EXACT = {
88548
90211
  description: _t("Tests whether two strings are identical."),
88549
90212
  args: [arg("string1 (string)", _t("The first string to compare.")), arg("string2 (string)", _t("The second string to compare."))],
88550
90213
  compute: function(string1, string2) {
88551
- return { value: toString(string1) === toString(string2) };
90214
+ return { value: toLocaleString(string1, this.locale) === toLocaleString(string2, this.locale) };
88552
90215
  },
88553
90216
  isExported: true
88554
90217
  };
@@ -88560,8 +90223,8 @@ const FIND = {
88560
90223
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
88561
90224
  ],
88562
90225
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
88563
- const _searchFor = toString(searchFor);
88564
- const _textToSearch = toString(textToSearch);
90226
+ const _searchFor = toLocaleString(searchFor, this.locale);
90227
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
88565
90228
  const _startingAt = toNumber(startingAt, this.locale);
88566
90229
  if (_textToSearch === "") return new EvaluationError(_t("The text_to_search must be non-empty."));
88567
90230
  if (_startingAt < 1) return new EvaluationError(_t("The starting_at (%s) must be greater than or equal to 1.", _startingAt));
@@ -88598,8 +90261,8 @@ const JOIN = {
88598
90261
  description: _t("Concatenates elements of arrays with delimiter."),
88599
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."))],
88600
90263
  compute: function(delimiter, ...valuesOrArrays) {
88601
- const _delimiter = toString(delimiter);
88602
- 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), "") };
88603
90266
  }
88604
90267
  };
88605
90268
  const LEFT = {
@@ -88608,7 +90271,7 @@ const LEFT = {
88608
90271
  compute: function(text, ...args) {
88609
90272
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
88610
90273
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or null.", _numberOfCharacters));
88611
- return { value: toString(text).substring(0, _numberOfCharacters) };
90274
+ return { value: toLocaleString(text, this.locale).substring(0, _numberOfCharacters) };
88612
90275
  },
88613
90276
  isExported: true
88614
90277
  };
@@ -88616,7 +90279,7 @@ const LEN = {
88616
90279
  description: _t("Length of a string."),
88617
90280
  args: [arg("text (string)", _t("The string whose length will be returned."))],
88618
90281
  compute: function(text) {
88619
- return { value: toString(text).length };
90282
+ return { value: toLocaleString(text, this.locale).length };
88620
90283
  },
88621
90284
  isExported: true
88622
90285
  };
@@ -88624,7 +90287,7 @@ const LOWER = {
88624
90287
  description: _t("Converts a specified string to lowercase."),
88625
90288
  args: [arg("text (string)", _t("The string to convert to lowercase."))],
88626
90289
  compute: function(text) {
88627
- return { value: toString(text).toLowerCase() };
90290
+ return { value: toLocaleString(text, this.locale).toLowerCase() };
88628
90291
  },
88629
90292
  isExported: true
88630
90293
  };
@@ -88636,7 +90299,7 @@ const MID = {
88636
90299
  arg("extract_length (number)", _t("The length of the segment to extract."))
88637
90300
  ],
88638
90301
  compute: function(text, starting_at, extract_length) {
88639
- const _text = toString(text);
90302
+ const _text = toLocaleString(text, this.locale);
88640
90303
  const _starting_at = toNumber(starting_at, this.locale);
88641
90304
  const _extract_length = toNumber(extract_length, this.locale);
88642
90305
  if (_starting_at < 1) return new EvaluationError(_t("The starting_at argument (%s) must be positive greater than one.", _starting_at.toString()));
@@ -88649,7 +90312,7 @@ const PROPER = {
88649
90312
  description: _t("Capitalizes each word in a specified string."),
88650
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."))],
88651
90314
  compute: function(text) {
88652
- return { value: toString(text).replace(wordRegex, (word) => {
90315
+ return { value: toLocaleString(text, this.locale).replace(wordRegex, (word) => {
88653
90316
  return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
88654
90317
  }) };
88655
90318
  },
@@ -88799,8 +90462,8 @@ const REPLACE = {
88799
90462
  if (_position < 1) return new EvaluationError(_t("The position (%s) must be greater than or equal to 1.", _position));
88800
90463
  const _length = toNumber(length, this.locale);
88801
90464
  if (_length < 0) return new EvaluationError(_t("The length (%s) must be positive or zero.", _length));
88802
- const _text = toString(text);
88803
- const _newText = toString(newText);
90465
+ const _text = toLocaleString(text, this.locale);
90466
+ const _newText = toLocaleString(newText, this.locale);
88804
90467
  return { value: _text.substring(0, _position - 1) + _newText + _text.substring(_position - 1 + _length) };
88805
90468
  },
88806
90469
  isExported: true
@@ -88811,7 +90474,7 @@ const RIGHT = {
88811
90474
  compute: function(text, ...args) {
88812
90475
  const _numberOfCharacters = args.length ? toNumber(args[0], this.locale) : 1;
88813
90476
  if (_numberOfCharacters < 0) return new EvaluationError(_t("The number_of_characters (%s) must be positive or zero.", _numberOfCharacters));
88814
- const _text = toString(text);
90477
+ const _text = toLocaleString(text, this.locale);
88815
90478
  const stringLength = _text.length;
88816
90479
  return { value: _text.substring(stringLength - _numberOfCharacters, stringLength) };
88817
90480
  },
@@ -88825,8 +90488,8 @@ const SEARCH = {
88825
90488
  arg(`starting_at (number, default=${DEFAULT_STARTING_AT})`, _t("The character within text_to_search at which to start the search."))
88826
90489
  ],
88827
90490
  compute: function(searchFor, textToSearch, startingAt = { value: DEFAULT_STARTING_AT }) {
88828
- const _searchFor = toString(searchFor).toLowerCase();
88829
- const _textToSearch = toString(textToSearch).toLowerCase();
90491
+ const _searchFor = toLocaleString(searchFor, this.locale).toLowerCase();
90492
+ const _textToSearch = toLocaleString(textToSearch, this.locale).toLowerCase();
88830
90493
  const _startingAt = toNumber(startingAt, this.locale);
88831
90494
  if (_textToSearch === "") return {
88832
90495
  value: CellErrorType.GenericError,
@@ -88856,8 +90519,8 @@ const SPLIT = {
88856
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."))
88857
90520
  ],
88858
90521
  computeArray: function(text, delimiter, splitByEach = { value: SPLIT_DEFAULT_SPLIT_BY_EACH }, removeEmptyText = { value: SPLIT_DEFAULT_REMOVE_EMPTY_TEXT }) {
88859
- const _text = toString(text);
88860
- const _delimiter = escapeRegExp(toString(delimiter));
90522
+ const _text = toLocaleString(text, this.locale);
90523
+ const _delimiter = escapeRegExp(toLocaleString(delimiter, this.locale));
88861
90524
  const _splitByEach = toBoolean(splitByEach);
88862
90525
  const _removeEmptyText = toBoolean(removeEmptyText);
88863
90526
  if (_delimiter.length <= 0) return new EvaluationError(_t("The delimiter (%s) must be not be empty.", _delimiter));
@@ -88879,10 +90542,10 @@ const SUBSTITUTE = {
88879
90542
  compute: function(textToSearch, searchFor, replaceWith, occurrenceNumber) {
88880
90543
  const _occurrenceNumber = toNumber(occurrenceNumber, this.locale);
88881
90544
  if (_occurrenceNumber < 0) return new EvaluationError(_t("The occurrenceNumber (%s) must be positive or null.", _occurrenceNumber));
88882
- const _textToSearch = toString(textToSearch);
88883
- const _searchFor = toString(searchFor);
90545
+ const _textToSearch = toLocaleString(textToSearch, this.locale);
90546
+ const _searchFor = toLocaleString(searchFor, this.locale);
88884
90547
  if (_searchFor === "") return { value: _textToSearch };
88885
- const _replaceWith = toString(replaceWith);
90548
+ const _replaceWith = toLocaleString(replaceWith, this.locale);
88886
90549
  const reg = new RegExp(escapeRegExp(_searchFor), "g");
88887
90550
  if (_occurrenceNumber === 0) return { value: _textToSearch.replace(reg, _replaceWith) };
88888
90551
  let n = 0;
@@ -88905,10 +90568,10 @@ const TEXTJOIN = {
88905
90568
  arg("texts (string, range<string>, repeating)", _t("Text item to join."))
88906
90569
  ],
88907
90570
  compute: function(delimiter, ignoreEmpty = { value: TEXTJOIN_DEFAULT_IGNORE_EMPTY }, ...textsOrArrays) {
88908
- const _delimiter = toString(delimiter);
90571
+ const _delimiter = toLocaleString(delimiter, this.locale);
88909
90572
  const _ignoreEmpty = toBoolean(ignoreEmpty);
88910
90573
  let n = 0;
88911
- 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, "") };
88912
90575
  },
88913
90576
  isExported: true
88914
90577
  };
@@ -88961,7 +90624,7 @@ const TRIM = {
88961
90624
  description: _t("Removes space characters."),
88962
90625
  args: [arg("text (string)", _t("The text or reference to a cell containing text to be trimmed."))],
88963
90626
  compute: function(text) {
88964
- return { value: trimContent(toString(text)) };
90627
+ return { value: trimContent(toLocaleString(text, this.locale)) };
88965
90628
  },
88966
90629
  isExported: true
88967
90630
  };
@@ -88969,7 +90632,7 @@ const UPPER = {
88969
90632
  description: _t("Converts a specified string to uppercase."),
88970
90633
  args: [arg("text (string)", _t("The string to convert to uppercase."))],
88971
90634
  compute: function(text) {
88972
- return { value: toString(text).toUpperCase() };
90635
+ return { value: toLocaleString(text, this.locale).toUpperCase() };
88973
90636
  },
88974
90637
  isExported: true
88975
90638
  };
@@ -89066,7 +90729,7 @@ const HYPERLINK = {
89066
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."))],
89067
90730
  compute: function(url, linkLabel) {
89068
90731
  const processedUrl = toString(url).trim();
89069
- const processedLabel = toString(linkLabel) || processedUrl;
90732
+ const processedLabel = toLocaleString(linkLabel, this.locale) || processedUrl;
89070
90733
  if (processedUrl === "") return { value: processedLabel };
89071
90734
  return { value: markdownLink(processedLabel, processedUrl) };
89072
90735
  },
@@ -89213,7 +90876,8 @@ const registries = {
89213
90876
  chartJsExtensionRegistry,
89214
90877
  onIterationEndEvaluationRegistry,
89215
90878
  specificRangeTransformRegistry,
89216
- unusedDataSourceRegistry
90879
+ unusedDataSourceRegistry,
90880
+ NonSquishableFunctionRegistry
89217
90881
  };
89218
90882
  const helpers = {
89219
90883
  arg,
@@ -89295,7 +90959,9 @@ const helpers = {
89295
90959
  computeCachedTextDimension,
89296
90960
  createComputeFunction,
89297
90961
  isMobileOS,
89298
- drawHighlight
90962
+ drawHighlight,
90963
+ getBoundingRectWithMargins,
90964
+ hasInteractiveElementInEventTree
89299
90965
  };
89300
90966
  const links = {
89301
90967
  isMarkdownLink,
@@ -89527,6 +91193,6 @@ exports.stores = stores;
89527
91193
  exports.tokenColors = tokenColors;
89528
91194
  exports.tokenize = tokenize;
89529
91195
 
89530
- __info__.version = "19.5.0-alpha.10";
89531
- __info__.date = "2026-08-10T12:33:47.263Z";
89532
- __info__.hash = "39a6fbc";
91196
+ __info__.version = "19.5.0-alpha.12";
91197
+ __info__.date = "2026-08-19T09:47:03.774Z";
91198
+ __info__.hash = "68ca68e";