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