@odoo/o-spreadsheet 18.2.5 → 18.2.7

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.
@@ -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 18.2.5
6
- * @date 2025-03-26T12:47:44.113Z
7
- * @hash 4675edd
5
+ * @version 18.2.7
6
+ * @date 2025-04-14T17:19:31.011Z
7
+ * @hash e187958
8
8
  */
9
9
 
10
10
  'use strict';
@@ -806,8 +806,7 @@ function removeFalsyAttributes(obj) {
806
806
  *
807
807
  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
808
808
  */
809
- const whiteSpaceSpecialCharacters = [
810
- " ",
809
+ const specialWhiteSpaceSpecialCharacters = [
811
810
  "\t",
812
811
  "\f",
813
812
  "\v",
@@ -822,7 +821,7 @@ const whiteSpaceSpecialCharacters = [
822
821
  String.fromCharCode(parseInt("3000", 16)),
823
822
  String.fromCharCode(parseInt("feff", 16)),
824
823
  ];
825
- const whiteSpaceRegexp = new RegExp(whiteSpaceSpecialCharacters.join("|"), "g");
824
+ const specialWhiteSpaceRegexp = new RegExp(specialWhiteSpaceSpecialCharacters.join("|"), "g");
826
825
  const newLineRegexp = /(\r\n|\r)/g;
827
826
  /**
828
827
  * Replace all different newlines characters by \n
@@ -3576,6 +3575,7 @@ const coreTypes = new Set([
3576
3575
  "CLEAR_FORMATTING",
3577
3576
  "SET_BORDER",
3578
3577
  "SET_ZONE_BORDERS",
3578
+ "SET_BORDERS_ON_TARGET",
3579
3579
  /** CHART */
3580
3580
  "CREATE_CHART",
3581
3581
  "UPDATE_CHART",
@@ -6725,6 +6725,7 @@ class AbstractCellClipboardHandler extends ClipboardHandler {
6725
6725
  }
6726
6726
 
6727
6727
  class BorderClipboardHandler extends AbstractCellClipboardHandler {
6728
+ queuedBordersToAdd = {};
6728
6729
  copy(data) {
6729
6730
  const sheetId = data.sheetId;
6730
6731
  if (data.zones.length === 0) {
@@ -6755,6 +6756,7 @@ class BorderClipboardHandler extends AbstractCellClipboardHandler {
6755
6756
  const { left, top } = zones[0];
6756
6757
  this.pasteZone(sheetId, left, top, content.borders);
6757
6758
  }
6759
+ this.executeQueuedChanges(sheetId);
6758
6760
  }
6759
6761
  pasteZone(sheetId, col, row, borders) {
6760
6762
  for (const [r, rowBorders] of borders.entries()) {
@@ -6773,7 +6775,20 @@ class BorderClipboardHandler extends AbstractCellClipboardHandler {
6773
6775
  ...targetBorders,
6774
6776
  ...originBorders,
6775
6777
  };
6776
- this.dispatch("SET_BORDER", { ...target, border });
6778
+ const borderKey = JSON.stringify(border);
6779
+ if (!this.queuedBordersToAdd[borderKey]) {
6780
+ this.queuedBordersToAdd[borderKey] = [];
6781
+ }
6782
+ this.queuedBordersToAdd[borderKey].push(positionToZone(target));
6783
+ }
6784
+ executeQueuedChanges(pasteSheetTarget) {
6785
+ for (const borderKey in this.queuedBordersToAdd) {
6786
+ const zones = this.queuedBordersToAdd[borderKey];
6787
+ const border = JSON.parse(borderKey);
6788
+ const target = recomputeZones(zones, []);
6789
+ this.dispatch("SET_BORDERS_ON_TARGET", { sheetId: pasteSheetTarget, target, border });
6790
+ }
6791
+ this.queuedBordersToAdd = {};
6777
6792
  }
6778
6793
  }
6779
6794
 
@@ -6799,8 +6814,12 @@ function tokenize(str, locale = DEFAULT_LOCALE) {
6799
6814
  str = replaceNewLines(str);
6800
6815
  const chars = new TokenizingChars(str);
6801
6816
  const result = [];
6817
+ const tokenizeSpace = specialWhiteSpaceRegexp.test(str)
6818
+ ? tokenizeSpecialCharacterSpace
6819
+ : tokenizeSimpleSpace;
6802
6820
  while (!chars.isOver()) {
6803
- let token = tokenizeSpace(chars) ||
6821
+ let token = tokenizeNewLine(chars) ||
6822
+ tokenizeSpace(chars) ||
6804
6823
  tokenizeArgsSeparator(chars, locale) ||
6805
6824
  tokenizeParenthesis(chars) ||
6806
6825
  tokenizeOperator(chars) ||
@@ -6934,17 +6953,19 @@ function tokenizeSymbol(chars) {
6934
6953
  }
6935
6954
  return null;
6936
6955
  }
6937
- function tokenizeSpace(chars) {
6938
- let length = 0;
6939
- while (chars.current === NEWLINE) {
6940
- length++;
6941
- chars.shift();
6956
+ function tokenizeSpecialCharacterSpace(chars) {
6957
+ let spaces = "";
6958
+ while (chars.current === " " || (chars.current && chars.current.match(specialWhiteSpaceRegexp))) {
6959
+ spaces += chars.shift();
6942
6960
  }
6943
- if (length) {
6944
- return { type: "SPACE", value: NEWLINE.repeat(length) };
6961
+ if (spaces) {
6962
+ return { type: "SPACE", value: spaces };
6945
6963
  }
6964
+ return null;
6965
+ }
6966
+ function tokenizeSimpleSpace(chars) {
6946
6967
  let spaces = "";
6947
- while (chars.current && chars.current.match(whiteSpaceRegexp)) {
6968
+ while (chars.current === " ") {
6948
6969
  spaces += chars.shift();
6949
6970
  }
6950
6971
  if (spaces) {
@@ -6952,6 +6973,17 @@ function tokenizeSpace(chars) {
6952
6973
  }
6953
6974
  return null;
6954
6975
  }
6976
+ function tokenizeNewLine(chars) {
6977
+ let length = 0;
6978
+ while (chars.current === NEWLINE) {
6979
+ length++;
6980
+ chars.shift();
6981
+ }
6982
+ if (length) {
6983
+ return { type: "SPACE", value: NEWLINE.repeat(length) };
6984
+ }
6985
+ return null;
6986
+ }
6955
6987
  function tokenizeInvalidRange(chars) {
6956
6988
  if (chars.currentStartsWith(CellErrorType.InvalidReference)) {
6957
6989
  chars.advanceBy(CellErrorType.InvalidReference.length);
@@ -8696,12 +8728,13 @@ class ConditionalFormatClipboardHandler extends AbstractCellClipboardHandler {
8696
8728
  }
8697
8729
  pasteCf(origin, target, isCutOperation) {
8698
8730
  if (origin?.rules && origin.rules.length > 0) {
8731
+ const originZone = positionToZone(origin.position);
8699
8732
  const zone = positionToZone(target);
8700
8733
  for (const rule of origin.rules) {
8701
8734
  const toRemoveZones = [];
8702
8735
  if (isCutOperation) {
8703
8736
  //remove from current rule
8704
- toRemoveZones.push(positionToZone(origin.position));
8737
+ toRemoveZones.push(originZone);
8705
8738
  }
8706
8739
  if (origin.position.sheetId === target.sheetId) {
8707
8740
  this.adaptCFRules(origin.position.sheetId, rule, [zone], toRemoveZones);
@@ -8815,6 +8848,7 @@ class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
8815
8848
  pasteDataValidation(origin, target, isCutOperation) {
8816
8849
  if (origin) {
8817
8850
  const zone = positionToZone(target);
8851
+ const originZone = positionToZone(origin.position);
8818
8852
  const rule = origin.rule;
8819
8853
  if (!rule) {
8820
8854
  const targetRule = this.getters.getValidationRuleForCell(target);
@@ -8826,7 +8860,7 @@ class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
8826
8860
  }
8827
8861
  const toRemoveZone = [];
8828
8862
  if (isCutOperation) {
8829
- toRemoveZone.push(positionToZone(origin.position));
8863
+ toRemoveZone.push(originZone);
8830
8864
  }
8831
8865
  if (origin.position.sheetId === target.sheetId) {
8832
8866
  const copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule, false);
@@ -8886,7 +8920,7 @@ class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
8886
8920
  continue;
8887
8921
  }
8888
8922
  this.dispatch("ADD_DATA_VALIDATION_RULE", {
8889
- rule: dv,
8923
+ rule: { id: dv.id, criterion: dv.criterion, isBlocking: dv.isBlocking },
8890
8924
  ranges: newDvZones.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
8891
8925
  sheetId,
8892
8926
  });
@@ -9578,6 +9612,159 @@ class ComposerFocusStore extends SpreadsheetStore {
9578
9612
  }
9579
9613
  }
9580
9614
 
9615
+ /**
9616
+ * This file is largely inspired by owl 1.
9617
+ * `css` tag has been removed from owl 2 without workaround to manage css.
9618
+ * So, the solution was to import the behavior of owl 1 directly in our
9619
+ * codebase, with one difference: the css is added to the sheet as soon as the
9620
+ * css tag is executed. In owl 1, the css was added as soon as a Component was
9621
+ * created for the first time.
9622
+ */
9623
+ const STYLESHEETS = {};
9624
+ let nextId = 0;
9625
+ /**
9626
+ * CSS tag helper for defining inline stylesheets. With this, one can simply define
9627
+ * an inline stylesheet with just the following code:
9628
+ * ```js
9629
+ * css`.component-a { color: red; }`;
9630
+ * ```
9631
+ */
9632
+ function css(strings, ...args) {
9633
+ const name = `__sheet__${nextId++}`;
9634
+ const value = String.raw(strings, ...args);
9635
+ registerSheet(name, value);
9636
+ activateSheet(name);
9637
+ return name;
9638
+ }
9639
+ function processSheet(str) {
9640
+ const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
9641
+ const selectorStack = [];
9642
+ const parts = [];
9643
+ let rules = [];
9644
+ function generateSelector(stackIndex, parentSelector) {
9645
+ const parts = [];
9646
+ for (const selector of selectorStack[stackIndex]) {
9647
+ let part = (parentSelector && parentSelector + " " + selector) || selector;
9648
+ if (part.includes("&")) {
9649
+ part = selector.replace(/&/g, parentSelector || "");
9650
+ }
9651
+ if (stackIndex < selectorStack.length - 1) {
9652
+ part = generateSelector(stackIndex + 1, part);
9653
+ }
9654
+ parts.push(part);
9655
+ }
9656
+ return parts.join(", ");
9657
+ }
9658
+ function generateRules() {
9659
+ if (rules.length) {
9660
+ parts.push(generateSelector(0) + " {");
9661
+ parts.push(...rules);
9662
+ parts.push("}");
9663
+ rules = [];
9664
+ }
9665
+ }
9666
+ while (tokens.length) {
9667
+ let token = tokens.shift();
9668
+ if (token === "}") {
9669
+ generateRules();
9670
+ selectorStack.pop();
9671
+ }
9672
+ else {
9673
+ if (tokens[0] === "{") {
9674
+ generateRules();
9675
+ selectorStack.push(token.split(/\s*,\s*/));
9676
+ tokens.shift();
9677
+ }
9678
+ if (tokens[0] === ";") {
9679
+ rules.push(" " + token + ";");
9680
+ }
9681
+ }
9682
+ }
9683
+ return parts.join("\n");
9684
+ }
9685
+ function registerSheet(id, css) {
9686
+ const sheet = document.createElement("style");
9687
+ sheet.textContent = processSheet(css);
9688
+ STYLESHEETS[id] = sheet;
9689
+ }
9690
+ function activateSheet(id) {
9691
+ const sheet = STYLESHEETS[id];
9692
+ sheet.setAttribute("component", id);
9693
+ document.head.appendChild(sheet);
9694
+ }
9695
+ function getTextDecoration({ strikethrough, underline, }) {
9696
+ if (!strikethrough && !underline) {
9697
+ return "none";
9698
+ }
9699
+ return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
9700
+ }
9701
+ /**
9702
+ * Convert the cell style to CSS properties.
9703
+ */
9704
+ function cellStyleToCss(style) {
9705
+ const attributes = cellTextStyleToCss(style);
9706
+ if (!style)
9707
+ return attributes;
9708
+ if (style.fillColor) {
9709
+ attributes["background"] = style.fillColor;
9710
+ }
9711
+ return attributes;
9712
+ }
9713
+ /**
9714
+ * Convert the cell text style to CSS properties.
9715
+ */
9716
+ function cellTextStyleToCss(style) {
9717
+ const attributes = {};
9718
+ if (!style)
9719
+ return attributes;
9720
+ if (style.bold) {
9721
+ attributes["font-weight"] = "bold";
9722
+ }
9723
+ if (style.italic) {
9724
+ attributes["font-style"] = "italic";
9725
+ }
9726
+ if (style.strikethrough || style.underline) {
9727
+ let decoration = style.strikethrough ? "line-through" : "";
9728
+ decoration = style.underline ? decoration + " underline" : decoration;
9729
+ attributes["text-decoration"] = decoration;
9730
+ }
9731
+ if (style.textColor) {
9732
+ attributes["color"] = style.textColor;
9733
+ }
9734
+ return attributes;
9735
+ }
9736
+ /**
9737
+ * Transform CSS properties into a CSS string.
9738
+ */
9739
+ function cssPropertiesToCss(attributes) {
9740
+ let styleStr = "";
9741
+ for (const attName in attributes) {
9742
+ if (!attributes[attName]) {
9743
+ continue;
9744
+ }
9745
+ styleStr += `${attName}:${attributes[attName]}; `;
9746
+ }
9747
+ return styleStr;
9748
+ }
9749
+ function getElementMargins(el) {
9750
+ const style = window.getComputedStyle(el);
9751
+ return {
9752
+ top: parseInt(style.marginTop, 10) || 0,
9753
+ bottom: parseInt(style.marginBottom, 10) || 0,
9754
+ left: parseInt(style.marginLeft, 10) || 0,
9755
+ right: parseInt(style.marginRight, 10) || 0,
9756
+ };
9757
+ }
9758
+
9759
+ const chartJsExtensionRegistry = new Registry();
9760
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
9761
+ function getChartJSConstructor() {
9762
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
9763
+ window.Chart.register(...chartJsExtensionRegistry.getAll());
9764
+ }
9765
+ return window.Chart;
9766
+ }
9767
+
9581
9768
  const TREND_LINE_XAXIS_ID = "x1";
9582
9769
  const MOVING_AVERAGE_TREND_LINE_XAXIS_ID = "xMovingAverage";
9583
9770
  /**
@@ -10126,341 +10313,79 @@ function getNextNonEmptyBar(bars, startIndex) {
10126
10313
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10127
10314
  }
10128
10315
 
10129
- const GAUGE_PADDING_SIDE = 30;
10130
- const GAUGE_PADDING_TOP = 10;
10131
- const GAUGE_PADDING_BOTTOM = 20;
10132
- const GAUGE_LABELS_FONT_SIZE = 12;
10133
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10134
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10135
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10136
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
10137
- function drawGaugeChart(canvas, runtime) {
10138
- const canvasBoundingRect = canvas.getBoundingClientRect();
10139
- canvas.width = canvasBoundingRect.width;
10140
- canvas.height = canvasBoundingRect.height;
10141
- const ctx = canvas.getContext("2d");
10142
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10143
- drawBackground(ctx, config);
10144
- drawGauge(ctx, config);
10145
- drawInflectionValues(ctx, config);
10146
- drawLabels(ctx, config);
10147
- drawTitle(ctx, config);
10148
- }
10149
- function drawGauge(ctx, config) {
10150
- ctx.save();
10151
- const gauge = config.gauge;
10152
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10153
- const arcCenterY = gauge.rect.y + gauge.rect.height;
10154
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10155
- if (arcRadius < 0) {
10156
- return;
10157
- }
10158
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10159
- // Gauge background
10160
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10161
- ctx.beginPath();
10162
- ctx.lineWidth = gauge.arcWidth;
10163
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10164
- ctx.stroke();
10165
- // Gauge value
10166
- ctx.strokeStyle = gauge.color;
10167
- ctx.beginPath();
10168
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10169
- ctx.stroke();
10170
- ctx.restore();
10171
- }
10172
- function drawBackground(ctx, config) {
10173
- ctx.save();
10174
- ctx.fillStyle = config.backgroundColor;
10175
- ctx.fillRect(0, 0, config.width, config.height);
10176
- ctx.restore();
10177
- }
10178
- function drawLabels(ctx, config) {
10179
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10180
- ctx.save();
10181
- ctx.textAlign = "center";
10182
- ctx.fillStyle = label.color;
10183
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10184
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10185
- ctx.restore();
10186
- }
10187
- }
10188
- function drawInflectionValues(ctx, config) {
10189
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10190
- for (const inflectionValue of config.inflectionValues) {
10191
- ctx.save();
10192
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10193
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10194
- ctx.lineWidth = 2;
10195
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10196
- ctx.beginPath();
10197
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
10198
- ctx.lineTo(0, -height - 3);
10199
- ctx.stroke();
10200
- ctx.textAlign = "center";
10201
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10202
- ctx.fillStyle = inflectionValue.color;
10203
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10204
- ctx.fillText(inflectionValue.label, 0, textY);
10205
- ctx.restore();
10206
- }
10207
- }
10208
- function drawTitle(ctx, config) {
10209
- ctx.save();
10210
- const title = config.title;
10211
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10212
- ctx.textBaseline = "middle";
10213
- ctx.fillStyle = title.color;
10214
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10215
- ctx.restore();
10216
- }
10217
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10218
- const maxValue = runtime.maxValue;
10219
- const minValue = runtime.minValue;
10220
- const gaugeValue = runtime.gaugeValue;
10221
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10222
- const gaugeArcWidth = gaugeRect.width / 6;
10223
- const gaugePercentage = gaugeValue
10224
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10225
- : 0;
10226
- const gaugeValuePosition = {
10227
- x: boundingRect.width / 2,
10228
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10229
- };
10230
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10231
- // Scale down the font size if the gaugeRect is too small
10232
- if (gaugeRect.height < 300) {
10233
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10234
- }
10235
- // Scale down the font size if the text is too long
10236
- const maxTextWidth = gaugeRect.width / 2;
10237
- const gaugeLabel = gaugeValue?.label || "-";
10238
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10239
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10316
+ css /* scss */ `
10317
+ .o-spreadsheet {
10318
+ .o-chart-custom-tooltip {
10319
+ font-size: 12px;
10320
+ background-color: #fff;
10321
+ z-index: ${ComponentsImportance.FigureTooltip};
10240
10322
  }
10241
- const minLabelPosition = {
10242
- x: gaugeRect.x + gaugeArcWidth / 2,
10243
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10244
- };
10245
- const maxLabelPosition = {
10246
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10247
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10323
+ }
10324
+ `;
10325
+ chartJsExtensionRegistry.add("chartShowValuesPlugin", chartShowValuesPlugin);
10326
+ chartJsExtensionRegistry.add("waterfallLinesPlugin", waterfallLinesPlugin);
10327
+ class ChartJsComponent extends owl.Component {
10328
+ static template = "o-spreadsheet-ChartJsComponent";
10329
+ static props = {
10330
+ figure: Object,
10248
10331
  };
10249
- const textColor = chartMutedFontColor(runtime.background);
10250
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10251
- let x = 0, titleWidth = 0, titleHeight = 0;
10252
- if (runtime.title.text) {
10253
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10254
- }
10255
- switch (runtime.title.align) {
10256
- case "right":
10257
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
10258
- break;
10259
- case "center":
10260
- x = (boundingRect.width - titleWidth) / 2;
10261
- break;
10262
- case "left":
10263
- default:
10264
- x = CHART_PADDING$1;
10265
- break;
10332
+ canvas = owl.useRef("graphContainer");
10333
+ chart;
10334
+ currentRuntime;
10335
+ get background() {
10336
+ return this.chartRuntime.background;
10266
10337
  }
10267
- return {
10268
- width: boundingRect.width,
10269
- height: boundingRect.height,
10270
- title: {
10271
- label: runtime.title.text ?? "",
10272
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10273
- textPosition: {
10274
- x,
10275
- y: CHART_PADDING_TOP + titleHeight / 2,
10276
- },
10277
- color: runtime.title.color ?? textColor,
10278
- bold: runtime.title.bold,
10279
- italic: runtime.title.italic,
10280
- },
10281
- backgroundColor: runtime.background,
10282
- gauge: {
10283
- rect: gaugeRect,
10284
- arcWidth: gaugeArcWidth,
10285
- percentage: clip(gaugePercentage, 0, 1),
10286
- color: getGaugeColor(runtime),
10287
- },
10288
- inflectionValues,
10289
- gaugeValue: {
10290
- label: gaugeLabel,
10291
- textPosition: gaugeValuePosition,
10292
- fontSize: gaugeValueFontSize,
10293
- color: textColor,
10294
- },
10295
- minLabel: {
10296
- label: runtime.minValue.label,
10297
- textPosition: minLabelPosition,
10298
- fontSize: GAUGE_LABELS_FONT_SIZE,
10299
- color: textColor,
10300
- },
10301
- maxLabel: {
10302
- label: runtime.maxValue.label,
10303
- textPosition: maxLabelPosition,
10304
- fontSize: GAUGE_LABELS_FONT_SIZE,
10305
- color: textColor,
10306
- },
10307
- };
10308
- }
10309
- /**
10310
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10311
- * space for the title and labels.
10312
- */
10313
- function getGaugeRect(boundingRect, title) {
10314
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10315
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10316
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10317
- let gaugeWidth;
10318
- let gaugeHeight;
10319
- if (drawWidth > 2 * drawHeight) {
10320
- gaugeWidth = 2 * drawHeight;
10321
- gaugeHeight = drawHeight;
10338
+ get canvasStyle() {
10339
+ return `background-color: ${this.background}`;
10322
10340
  }
10323
- else {
10324
- gaugeWidth = drawWidth;
10325
- gaugeHeight = drawWidth / 2;
10341
+ get chartRuntime() {
10342
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
10343
+ if (!("chartJsConfig" in runtime)) {
10344
+ throw new Error("Unsupported chart runtime");
10345
+ }
10346
+ return runtime;
10326
10347
  }
10327
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10328
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10329
- return {
10330
- x: gaugeX,
10331
- y: gaugeY,
10332
- width: gaugeWidth,
10333
- height: gaugeHeight,
10334
- };
10335
- }
10336
- /**
10337
- * Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
10338
- *
10339
- * Also compute an offset for the text so that it doesn't overlap with other text.
10340
- */
10341
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10342
- const maxValue = runtime.maxValue;
10343
- const minValue = runtime.minValue;
10344
- const gaugeCircleCenter = {
10345
- x: gaugeRect.x + gaugeRect.width / 2,
10346
- y: gaugeRect.y + gaugeRect.height,
10347
- };
10348
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10349
- const inflectionValues = [];
10350
- const inflectionValuesTextRects = [];
10351
- for (const inflectionValue of runtime.inflectionValues) {
10352
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10353
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10354
- const angle = Math.PI - Math.PI * percentage;
10355
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10356
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10357
- gaugeCircleCenter.x, // center of the gauge circle
10358
- gaugeCircleCenter.y, // center of the gauge circle
10359
- labelWidth + 2, // width of the text + some margin
10360
- GAUGE_LABELS_FONT_SIZE // height of the text
10361
- );
10362
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10363
- ? GAUGE_LABELS_FONT_SIZE
10364
- : 0;
10365
- inflectionValuesTextRects.push(textRect);
10366
- inflectionValues.push({
10367
- rotation: angle,
10368
- label: inflectionValue.label,
10369
- fontSize: GAUGE_LABELS_FONT_SIZE,
10370
- color: textColor,
10371
- offset,
10348
+ setup() {
10349
+ owl.onMounted(() => {
10350
+ const runtime = this.chartRuntime;
10351
+ this.currentRuntime = runtime;
10352
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
10353
+ this.createChart(deepCopy(runtime.chartJsConfig));
10354
+ });
10355
+ owl.onWillUnmount(() => this.chart?.destroy());
10356
+ owl.useEffect(() => {
10357
+ const runtime = this.chartRuntime;
10358
+ if (runtime !== this.currentRuntime) {
10359
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
10360
+ this.chart?.destroy();
10361
+ this.createChart(deepCopy(runtime.chartJsConfig));
10362
+ }
10363
+ else {
10364
+ this.updateChartJs(deepCopy(runtime.chartJsConfig));
10365
+ }
10366
+ this.currentRuntime = runtime;
10367
+ }
10372
10368
  });
10373
10369
  }
10374
- return inflectionValues;
10375
- }
10376
- function getGaugeColor(runtime) {
10377
- const gaugeValue = runtime.gaugeValue?.value;
10378
- if (gaugeValue === undefined) {
10379
- return GAUGE_BACKGROUND_COLOR;
10380
- }
10381
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
10382
- const inflectionValue = runtime.inflectionValues[i];
10383
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10384
- return runtime.colors[i];
10385
- }
10386
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10387
- return runtime.colors[i];
10388
- }
10389
- }
10390
- return runtime.colors.at(-1);
10391
- }
10392
- function getSegmentsOfRectangle(rectangle) {
10393
- return [
10394
- { start: rectangle.topLeft, end: rectangle.topRight },
10395
- { start: rectangle.topRight, end: rectangle.bottomRight },
10396
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10397
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
10398
- ];
10399
- }
10400
- /**
10401
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10402
- * is not handled.
10403
- */
10404
- function doSegmentIntersect(segment1, segment2) {
10405
- const A = segment1.start;
10406
- const B = segment1.end;
10407
- const C = segment2.start;
10408
- const D = segment2.end;
10409
- /**
10410
- * Line segment intersection algorithm
10411
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10412
- */
10413
- function ccw(a, b, c) {
10414
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10370
+ createChart(chartData) {
10371
+ const canvas = this.canvas.el;
10372
+ const ctx = canvas.getContext("2d");
10373
+ const Chart = getChartJSConstructor();
10374
+ this.chart = new Chart(ctx, chartData);
10415
10375
  }
10416
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10417
- }
10418
- function doRectanglesIntersect(rect1, rect2) {
10419
- const segments1 = getSegmentsOfRectangle(rect1);
10420
- const segments2 = getSegmentsOfRectangle(rect2);
10421
- for (const segment1 of segments1) {
10422
- for (const segment2 of segments2) {
10423
- if (doSegmentIntersect(segment1, segment2)) {
10424
- return true;
10376
+ updateChartJs(chartData) {
10377
+ if (chartData.data && chartData.data.datasets) {
10378
+ this.chart.data = chartData.data;
10379
+ if (chartData.options?.plugins?.title) {
10380
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
10425
10381
  }
10426
10382
  }
10383
+ else {
10384
+ this.chart.data.datasets = [];
10385
+ }
10386
+ this.chart.config.options = chartData.options;
10387
+ this.chart.update();
10427
10388
  }
10428
- return false;
10429
- }
10430
- /**
10431
- * Get the rectangle that is tangent to a circle at a given angle.
10432
- *
10433
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10434
- */
10435
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10436
- const cos = Math.cos(angle);
10437
- const sin = Math.sin(angle);
10438
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10439
- const x = cos * radius;
10440
- const y = sin * radius;
10441
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10442
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10443
- const y2 = cos * (rectWidth / 2);
10444
- const bottomRight = {
10445
- x: x + x2 + circleCenterX,
10446
- y: circleCenterY - (y - y2),
10447
- };
10448
- const bottomLeft = {
10449
- x: x - x2 + circleCenterX,
10450
- y: circleCenterY - (y + y2),
10451
- };
10452
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10453
- const xp = cos * (radius + rectHeight);
10454
- const yp = sin * (radius + rectHeight);
10455
- const topLeft = {
10456
- x: xp - x2 + circleCenterX,
10457
- y: circleCenterY - (yp + y2),
10458
- };
10459
- const topRight = {
10460
- x: xp + x2 + circleCenterX,
10461
- y: circleCenterY - (yp - y2),
10462
- };
10463
- return { bottomLeft, bottomRight, topRight, topLeft };
10464
10389
  }
10465
10390
 
10466
10391
  /**
@@ -11042,299 +10967,6 @@ class ScorecardChartConfigBuilder {
11042
10967
  }
11043
10968
  }
11044
10969
 
11045
- const CHART_COMMON_OPTIONS = {
11046
- // https://www.chartjs.org/docs/latest/general/responsive.html
11047
- responsive: true, // will resize when its container is resized
11048
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
11049
- elements: {
11050
- line: {
11051
- fill: false, // do not fill the area under line charts
11052
- },
11053
- point: {
11054
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
11055
- },
11056
- },
11057
- animation: false,
11058
- };
11059
- function chartToImage(runtime, figure, type) {
11060
- // wrap the canvas in a div with a fixed size because chart.js would
11061
- // fill the whole page otherwise
11062
- const div = document.createElement("div");
11063
- div.style.width = `${figure.width}px`;
11064
- div.style.height = `${figure.height}px`;
11065
- const canvas = document.createElement("canvas");
11066
- div.append(canvas);
11067
- canvas.setAttribute("width", figure.width.toString());
11068
- canvas.setAttribute("height", figure.height.toString());
11069
- // we have to add the canvas to the DOM otherwise it won't be rendered
11070
- document.body.append(div);
11071
- if ("chartJsConfig" in runtime) {
11072
- const config = deepCopy(runtime.chartJsConfig);
11073
- config.plugins = [backgroundColorChartJSPlugin];
11074
- const Chart = getChartJSConstructor();
11075
- const chart = new Chart(canvas, config);
11076
- const imgContent = chart.toBase64Image();
11077
- chart.destroy();
11078
- div.remove();
11079
- return imgContent;
11080
- }
11081
- else if (type === "scorecard") {
11082
- const design = getScorecardConfiguration(figure, runtime);
11083
- drawScoreChart(design, canvas);
11084
- const imgContent = canvas.toDataURL();
11085
- div.remove();
11086
- return imgContent;
11087
- }
11088
- else if (type === "gauge") {
11089
- drawGaugeChart(canvas, runtime);
11090
- const imgContent = canvas.toDataURL();
11091
- div.remove();
11092
- return imgContent;
11093
- }
11094
- return undefined;
11095
- }
11096
- /**
11097
- * Custom chart.js plugin to set the background color of the canvas
11098
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11099
- */
11100
- const backgroundColorChartJSPlugin = {
11101
- id: "customCanvasBackgroundColor",
11102
- beforeDraw: (chart) => {
11103
- const { ctx } = chart;
11104
- ctx.save();
11105
- ctx.globalCompositeOperation = "destination-over";
11106
- ctx.fillStyle = "#ffffff";
11107
- ctx.fillRect(0, 0, chart.width, chart.height);
11108
- ctx.restore();
11109
- },
11110
- };
11111
- /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11112
- function getChartJSConstructor() {
11113
- if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11114
- window.Chart.register(chartShowValuesPlugin);
11115
- window.Chart.register(waterfallLinesPlugin);
11116
- }
11117
- return window.Chart;
11118
- }
11119
-
11120
- /**
11121
- * This file is largely inspired by owl 1.
11122
- * `css` tag has been removed from owl 2 without workaround to manage css.
11123
- * So, the solution was to import the behavior of owl 1 directly in our
11124
- * codebase, with one difference: the css is added to the sheet as soon as the
11125
- * css tag is executed. In owl 1, the css was added as soon as a Component was
11126
- * created for the first time.
11127
- */
11128
- const STYLESHEETS = {};
11129
- let nextId = 0;
11130
- /**
11131
- * CSS tag helper for defining inline stylesheets. With this, one can simply define
11132
- * an inline stylesheet with just the following code:
11133
- * ```js
11134
- * css`.component-a { color: red; }`;
11135
- * ```
11136
- */
11137
- function css(strings, ...args) {
11138
- const name = `__sheet__${nextId++}`;
11139
- const value = String.raw(strings, ...args);
11140
- registerSheet(name, value);
11141
- activateSheet(name);
11142
- return name;
11143
- }
11144
- function processSheet(str) {
11145
- const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
11146
- const selectorStack = [];
11147
- const parts = [];
11148
- let rules = [];
11149
- function generateSelector(stackIndex, parentSelector) {
11150
- const parts = [];
11151
- for (const selector of selectorStack[stackIndex]) {
11152
- let part = (parentSelector && parentSelector + " " + selector) || selector;
11153
- if (part.includes("&")) {
11154
- part = selector.replace(/&/g, parentSelector || "");
11155
- }
11156
- if (stackIndex < selectorStack.length - 1) {
11157
- part = generateSelector(stackIndex + 1, part);
11158
- }
11159
- parts.push(part);
11160
- }
11161
- return parts.join(", ");
11162
- }
11163
- function generateRules() {
11164
- if (rules.length) {
11165
- parts.push(generateSelector(0) + " {");
11166
- parts.push(...rules);
11167
- parts.push("}");
11168
- rules = [];
11169
- }
11170
- }
11171
- while (tokens.length) {
11172
- let token = tokens.shift();
11173
- if (token === "}") {
11174
- generateRules();
11175
- selectorStack.pop();
11176
- }
11177
- else {
11178
- if (tokens[0] === "{") {
11179
- generateRules();
11180
- selectorStack.push(token.split(/\s*,\s*/));
11181
- tokens.shift();
11182
- }
11183
- if (tokens[0] === ";") {
11184
- rules.push(" " + token + ";");
11185
- }
11186
- }
11187
- }
11188
- return parts.join("\n");
11189
- }
11190
- function registerSheet(id, css) {
11191
- const sheet = document.createElement("style");
11192
- sheet.textContent = processSheet(css);
11193
- STYLESHEETS[id] = sheet;
11194
- }
11195
- function activateSheet(id) {
11196
- const sheet = STYLESHEETS[id];
11197
- sheet.setAttribute("component", id);
11198
- document.head.appendChild(sheet);
11199
- }
11200
- function getTextDecoration({ strikethrough, underline, }) {
11201
- if (!strikethrough && !underline) {
11202
- return "none";
11203
- }
11204
- return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
11205
- }
11206
- /**
11207
- * Convert the cell style to CSS properties.
11208
- */
11209
- function cellStyleToCss(style) {
11210
- const attributes = cellTextStyleToCss(style);
11211
- if (!style)
11212
- return attributes;
11213
- if (style.fillColor) {
11214
- attributes["background"] = style.fillColor;
11215
- }
11216
- return attributes;
11217
- }
11218
- /**
11219
- * Convert the cell text style to CSS properties.
11220
- */
11221
- function cellTextStyleToCss(style) {
11222
- const attributes = {};
11223
- if (!style)
11224
- return attributes;
11225
- if (style.bold) {
11226
- attributes["font-weight"] = "bold";
11227
- }
11228
- if (style.italic) {
11229
- attributes["font-style"] = "italic";
11230
- }
11231
- if (style.strikethrough || style.underline) {
11232
- let decoration = style.strikethrough ? "line-through" : "";
11233
- decoration = style.underline ? decoration + " underline" : decoration;
11234
- attributes["text-decoration"] = decoration;
11235
- }
11236
- if (style.textColor) {
11237
- attributes["color"] = style.textColor;
11238
- }
11239
- return attributes;
11240
- }
11241
- /**
11242
- * Transform CSS properties into a CSS string.
11243
- */
11244
- function cssPropertiesToCss(attributes) {
11245
- let styleStr = "";
11246
- for (const attName in attributes) {
11247
- if (!attributes[attName]) {
11248
- continue;
11249
- }
11250
- styleStr += `${attName}:${attributes[attName]}; `;
11251
- }
11252
- return styleStr;
11253
- }
11254
- function getElementMargins(el) {
11255
- const style = window.getComputedStyle(el);
11256
- return {
11257
- top: parseInt(style.marginTop, 10) || 0,
11258
- bottom: parseInt(style.marginBottom, 10) || 0,
11259
- left: parseInt(style.marginLeft, 10) || 0,
11260
- right: parseInt(style.marginRight, 10) || 0,
11261
- };
11262
- }
11263
-
11264
- css /* scss */ `
11265
- .o-spreadsheet {
11266
- .o-chart-custom-tooltip {
11267
- font-size: 12px;
11268
- background-color: #fff;
11269
- z-index: ${ComponentsImportance.FigureTooltip};
11270
- }
11271
- }
11272
- `;
11273
- class ChartJsComponent extends owl.Component {
11274
- static template = "o-spreadsheet-ChartJsComponent";
11275
- static props = {
11276
- figure: Object,
11277
- };
11278
- canvas = owl.useRef("graphContainer");
11279
- chart;
11280
- currentRuntime;
11281
- get background() {
11282
- return this.chartRuntime.background;
11283
- }
11284
- get canvasStyle() {
11285
- return `background-color: ${this.background}`;
11286
- }
11287
- get chartRuntime() {
11288
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11289
- if (!("chartJsConfig" in runtime)) {
11290
- throw new Error("Unsupported chart runtime");
11291
- }
11292
- return runtime;
11293
- }
11294
- setup() {
11295
- owl.onMounted(() => {
11296
- const runtime = this.chartRuntime;
11297
- this.currentRuntime = runtime;
11298
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
11299
- this.createChart(deepCopy(runtime.chartJsConfig));
11300
- });
11301
- owl.onWillUnmount(() => this.chart?.destroy());
11302
- owl.useEffect(() => {
11303
- const runtime = this.chartRuntime;
11304
- if (runtime !== this.currentRuntime) {
11305
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11306
- this.chart?.destroy();
11307
- this.createChart(deepCopy(runtime.chartJsConfig));
11308
- }
11309
- else {
11310
- this.updateChartJs(deepCopy(runtime));
11311
- }
11312
- this.currentRuntime = runtime;
11313
- }
11314
- });
11315
- }
11316
- createChart(chartData) {
11317
- const canvas = this.canvas.el;
11318
- const ctx = canvas.getContext("2d");
11319
- const Chart = getChartJSConstructor();
11320
- this.chart = new Chart(ctx, chartData);
11321
- }
11322
- updateChartJs(chartRuntime) {
11323
- const chartData = chartRuntime.chartJsConfig;
11324
- if (chartData.data && chartData.data.datasets) {
11325
- this.chart.data = chartData.data;
11326
- if (chartData.options?.plugins?.title) {
11327
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
11328
- }
11329
- }
11330
- else {
11331
- this.chart.data.datasets = [];
11332
- }
11333
- this.chart.config.options = chartData.options;
11334
- this.chart.update();
11335
- }
11336
- }
11337
-
11338
10970
  class ScorecardChart extends owl.Component {
11339
10971
  static template = "o-spreadsheet-ScorecardChart";
11340
10972
  static props = {
@@ -22932,6 +22564,343 @@ function getDateIntervals(dates) {
22932
22564
 
22933
22565
  const cellPopoverRegistry = new Registry();
22934
22566
 
22567
+ const GAUGE_PADDING_SIDE = 30;
22568
+ const GAUGE_PADDING_TOP = 10;
22569
+ const GAUGE_PADDING_BOTTOM = 20;
22570
+ const GAUGE_LABELS_FONT_SIZE = 12;
22571
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22572
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22573
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22574
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
22575
+ function drawGaugeChart(canvas, runtime) {
22576
+ const canvasBoundingRect = canvas.getBoundingClientRect();
22577
+ canvas.width = canvasBoundingRect.width;
22578
+ canvas.height = canvasBoundingRect.height;
22579
+ const ctx = canvas.getContext("2d");
22580
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
22581
+ drawBackground(ctx, config);
22582
+ drawGauge(ctx, config);
22583
+ drawInflectionValues(ctx, config);
22584
+ drawLabels(ctx, config);
22585
+ drawTitle(ctx, config);
22586
+ }
22587
+ function drawGauge(ctx, config) {
22588
+ ctx.save();
22589
+ const gauge = config.gauge;
22590
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
22591
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
22592
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
22593
+ if (arcRadius < 0) {
22594
+ return;
22595
+ }
22596
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
22597
+ // Gauge background
22598
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
22599
+ ctx.beginPath();
22600
+ ctx.lineWidth = gauge.arcWidth;
22601
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
22602
+ ctx.stroke();
22603
+ // Gauge value
22604
+ ctx.strokeStyle = gauge.color;
22605
+ ctx.beginPath();
22606
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
22607
+ ctx.stroke();
22608
+ ctx.restore();
22609
+ }
22610
+ function drawBackground(ctx, config) {
22611
+ ctx.save();
22612
+ ctx.fillStyle = config.backgroundColor;
22613
+ ctx.fillRect(0, 0, config.width, config.height);
22614
+ ctx.restore();
22615
+ }
22616
+ function drawLabels(ctx, config) {
22617
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
22618
+ ctx.save();
22619
+ ctx.textAlign = "center";
22620
+ ctx.fillStyle = label.color;
22621
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
22622
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
22623
+ ctx.restore();
22624
+ }
22625
+ }
22626
+ function drawInflectionValues(ctx, config) {
22627
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
22628
+ for (const inflectionValue of config.inflectionValues) {
22629
+ ctx.save();
22630
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22631
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22632
+ ctx.lineWidth = 2;
22633
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22634
+ ctx.beginPath();
22635
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
22636
+ ctx.lineTo(0, -height - 3);
22637
+ ctx.stroke();
22638
+ ctx.textAlign = "center";
22639
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
22640
+ ctx.fillStyle = inflectionValue.color;
22641
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
22642
+ ctx.fillText(inflectionValue.label, 0, textY);
22643
+ ctx.restore();
22644
+ }
22645
+ }
22646
+ function drawTitle(ctx, config) {
22647
+ ctx.save();
22648
+ const title = config.title;
22649
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
22650
+ ctx.textBaseline = "middle";
22651
+ ctx.fillStyle = title.color;
22652
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
22653
+ ctx.restore();
22654
+ }
22655
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22656
+ const maxValue = runtime.maxValue;
22657
+ const minValue = runtime.minValue;
22658
+ const gaugeValue = runtime.gaugeValue;
22659
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
22660
+ const gaugeArcWidth = gaugeRect.width / 6;
22661
+ const gaugePercentage = gaugeValue
22662
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
22663
+ : 0;
22664
+ const gaugeValuePosition = {
22665
+ x: boundingRect.width / 2,
22666
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
22667
+ };
22668
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
22669
+ // Scale down the font size if the gaugeRect is too small
22670
+ if (gaugeRect.height < 300) {
22671
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
22672
+ }
22673
+ // Scale down the font size if the text is too long
22674
+ const maxTextWidth = gaugeRect.width / 2;
22675
+ const gaugeLabel = gaugeValue?.label || "-";
22676
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
22677
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
22678
+ }
22679
+ const minLabelPosition = {
22680
+ x: gaugeRect.x + gaugeArcWidth / 2,
22681
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22682
+ };
22683
+ const maxLabelPosition = {
22684
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22685
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22686
+ };
22687
+ const textColor = chartMutedFontColor(runtime.background);
22688
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22689
+ let x = 0, titleWidth = 0, titleHeight = 0;
22690
+ if (runtime.title.text) {
22691
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22692
+ }
22693
+ switch (runtime.title.align) {
22694
+ case "right":
22695
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
22696
+ break;
22697
+ case "center":
22698
+ x = (boundingRect.width - titleWidth) / 2;
22699
+ break;
22700
+ case "left":
22701
+ default:
22702
+ x = CHART_PADDING$1;
22703
+ break;
22704
+ }
22705
+ return {
22706
+ width: boundingRect.width,
22707
+ height: boundingRect.height,
22708
+ title: {
22709
+ label: runtime.title.text ?? "",
22710
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22711
+ textPosition: {
22712
+ x,
22713
+ y: CHART_PADDING_TOP + titleHeight / 2,
22714
+ },
22715
+ color: runtime.title.color ?? textColor,
22716
+ bold: runtime.title.bold,
22717
+ italic: runtime.title.italic,
22718
+ },
22719
+ backgroundColor: runtime.background,
22720
+ gauge: {
22721
+ rect: gaugeRect,
22722
+ arcWidth: gaugeArcWidth,
22723
+ percentage: clip(gaugePercentage, 0, 1),
22724
+ color: getGaugeColor(runtime),
22725
+ },
22726
+ inflectionValues,
22727
+ gaugeValue: {
22728
+ label: gaugeLabel,
22729
+ textPosition: gaugeValuePosition,
22730
+ fontSize: gaugeValueFontSize,
22731
+ color: textColor,
22732
+ },
22733
+ minLabel: {
22734
+ label: runtime.minValue.label,
22735
+ textPosition: minLabelPosition,
22736
+ fontSize: GAUGE_LABELS_FONT_SIZE,
22737
+ color: textColor,
22738
+ },
22739
+ maxLabel: {
22740
+ label: runtime.maxValue.label,
22741
+ textPosition: maxLabelPosition,
22742
+ fontSize: GAUGE_LABELS_FONT_SIZE,
22743
+ color: textColor,
22744
+ },
22745
+ };
22746
+ }
22747
+ /**
22748
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
22749
+ * space for the title and labels.
22750
+ */
22751
+ function getGaugeRect(boundingRect, title) {
22752
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
22753
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
22754
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
22755
+ let gaugeWidth;
22756
+ let gaugeHeight;
22757
+ if (drawWidth > 2 * drawHeight) {
22758
+ gaugeWidth = 2 * drawHeight;
22759
+ gaugeHeight = drawHeight;
22760
+ }
22761
+ else {
22762
+ gaugeWidth = drawWidth;
22763
+ gaugeHeight = drawWidth / 2;
22764
+ }
22765
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
22766
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
22767
+ return {
22768
+ x: gaugeX,
22769
+ y: gaugeY,
22770
+ width: gaugeWidth,
22771
+ height: gaugeHeight,
22772
+ };
22773
+ }
22774
+ /**
22775
+ * Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
22776
+ *
22777
+ * Also compute an offset for the text so that it doesn't overlap with other text.
22778
+ */
22779
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
22780
+ const maxValue = runtime.maxValue;
22781
+ const minValue = runtime.minValue;
22782
+ const gaugeCircleCenter = {
22783
+ x: gaugeRect.x + gaugeRect.width / 2,
22784
+ y: gaugeRect.y + gaugeRect.height,
22785
+ };
22786
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
22787
+ const inflectionValues = [];
22788
+ const inflectionValuesTextRects = [];
22789
+ for (const inflectionValue of runtime.inflectionValues) {
22790
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
22791
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
22792
+ const angle = Math.PI - Math.PI * percentage;
22793
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
22794
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
22795
+ gaugeCircleCenter.x, // center of the gauge circle
22796
+ gaugeCircleCenter.y, // center of the gauge circle
22797
+ labelWidth + 2, // width of the text + some margin
22798
+ GAUGE_LABELS_FONT_SIZE // height of the text
22799
+ );
22800
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
22801
+ ? GAUGE_LABELS_FONT_SIZE
22802
+ : 0;
22803
+ inflectionValuesTextRects.push(textRect);
22804
+ inflectionValues.push({
22805
+ rotation: angle,
22806
+ label: inflectionValue.label,
22807
+ fontSize: GAUGE_LABELS_FONT_SIZE,
22808
+ color: textColor,
22809
+ offset,
22810
+ });
22811
+ }
22812
+ return inflectionValues;
22813
+ }
22814
+ function getGaugeColor(runtime) {
22815
+ const gaugeValue = runtime.gaugeValue?.value;
22816
+ if (gaugeValue === undefined) {
22817
+ return GAUGE_BACKGROUND_COLOR;
22818
+ }
22819
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
22820
+ const inflectionValue = runtime.inflectionValues[i];
22821
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
22822
+ return runtime.colors[i];
22823
+ }
22824
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
22825
+ return runtime.colors[i];
22826
+ }
22827
+ }
22828
+ return runtime.colors.at(-1);
22829
+ }
22830
+ function getSegmentsOfRectangle(rectangle) {
22831
+ return [
22832
+ { start: rectangle.topLeft, end: rectangle.topRight },
22833
+ { start: rectangle.topRight, end: rectangle.bottomRight },
22834
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
22835
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
22836
+ ];
22837
+ }
22838
+ /**
22839
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
22840
+ * is not handled.
22841
+ */
22842
+ function doSegmentIntersect(segment1, segment2) {
22843
+ const A = segment1.start;
22844
+ const B = segment1.end;
22845
+ const C = segment2.start;
22846
+ const D = segment2.end;
22847
+ /**
22848
+ * Line segment intersection algorithm
22849
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
22850
+ */
22851
+ function ccw(a, b, c) {
22852
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
22853
+ }
22854
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
22855
+ }
22856
+ function doRectanglesIntersect(rect1, rect2) {
22857
+ const segments1 = getSegmentsOfRectangle(rect1);
22858
+ const segments2 = getSegmentsOfRectangle(rect2);
22859
+ for (const segment1 of segments1) {
22860
+ for (const segment2 of segments2) {
22861
+ if (doSegmentIntersect(segment1, segment2)) {
22862
+ return true;
22863
+ }
22864
+ }
22865
+ }
22866
+ return false;
22867
+ }
22868
+ /**
22869
+ * Get the rectangle that is tangent to a circle at a given angle.
22870
+ *
22871
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
22872
+ */
22873
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
22874
+ const cos = Math.cos(angle);
22875
+ const sin = Math.sin(angle);
22876
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
22877
+ const x = cos * radius;
22878
+ const y = sin * radius;
22879
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
22880
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
22881
+ const y2 = cos * (rectWidth / 2);
22882
+ const bottomRight = {
22883
+ x: x + x2 + circleCenterX,
22884
+ y: circleCenterY - (y - y2),
22885
+ };
22886
+ const bottomLeft = {
22887
+ x: x - x2 + circleCenterX,
22888
+ y: circleCenterY - (y + y2),
22889
+ };
22890
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
22891
+ const xp = cos * (radius + rectHeight);
22892
+ const yp = sin * (radius + rectHeight);
22893
+ const topLeft = {
22894
+ x: xp - x2 + circleCenterX,
22895
+ y: circleCenterY - (yp + y2),
22896
+ };
22897
+ const topRight = {
22898
+ x: xp + x2 + circleCenterX,
22899
+ y: circleCenterY - (yp - y2),
22900
+ };
22901
+ return { bottomLeft, bottomRight, topRight, topLeft };
22902
+ }
22903
+
22935
22904
  class GaugeChartComponent extends owl.Component {
22936
22905
  static template = "o-spreadsheet-GaugeChartComponent";
22937
22906
  canvas = owl.useRef("chartContainer");
@@ -22964,6 +22933,73 @@ function toXlsxHexColor(color) {
22964
22933
  return color;
22965
22934
  }
22966
22935
 
22936
+ const CHART_COMMON_OPTIONS = {
22937
+ // https://www.chartjs.org/docs/latest/general/responsive.html
22938
+ responsive: true, // will resize when its container is resized
22939
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
22940
+ elements: {
22941
+ line: {
22942
+ fill: false, // do not fill the area under line charts
22943
+ },
22944
+ point: {
22945
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
22946
+ },
22947
+ },
22948
+ animation: false,
22949
+ };
22950
+ function chartToImage(runtime, figure, type) {
22951
+ // wrap the canvas in a div with a fixed size because chart.js would
22952
+ // fill the whole page otherwise
22953
+ const div = document.createElement("div");
22954
+ div.style.width = `${figure.width}px`;
22955
+ div.style.height = `${figure.height}px`;
22956
+ const canvas = document.createElement("canvas");
22957
+ div.append(canvas);
22958
+ canvas.setAttribute("width", figure.width.toString());
22959
+ canvas.setAttribute("height", figure.height.toString());
22960
+ // we have to add the canvas to the DOM otherwise it won't be rendered
22961
+ document.body.append(div);
22962
+ if ("chartJsConfig" in runtime) {
22963
+ const config = deepCopy(runtime.chartJsConfig);
22964
+ config.plugins = [backgroundColorChartJSPlugin];
22965
+ const Chart = getChartJSConstructor();
22966
+ const chart = new Chart(canvas, config);
22967
+ const imgContent = chart.toBase64Image();
22968
+ chart.destroy();
22969
+ div.remove();
22970
+ return imgContent;
22971
+ }
22972
+ else if (type === "scorecard") {
22973
+ const design = getScorecardConfiguration(figure, runtime);
22974
+ drawScoreChart(design, canvas);
22975
+ const imgContent = canvas.toDataURL();
22976
+ div.remove();
22977
+ return imgContent;
22978
+ }
22979
+ else if (type === "gauge") {
22980
+ drawGaugeChart(canvas, runtime);
22981
+ const imgContent = canvas.toDataURL();
22982
+ div.remove();
22983
+ return imgContent;
22984
+ }
22985
+ return undefined;
22986
+ }
22987
+ /**
22988
+ * Custom chart.js plugin to set the background color of the canvas
22989
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
22990
+ */
22991
+ const backgroundColorChartJSPlugin = {
22992
+ id: "customCanvasBackgroundColor",
22993
+ beforeDraw: (chart) => {
22994
+ const { ctx } = chart;
22995
+ ctx.save();
22996
+ ctx.globalCompositeOperation = "destination-over";
22997
+ ctx.fillStyle = "#ffffff";
22998
+ ctx.fillRect(0, 0, chart.width, chart.height);
22999
+ ctx.restore();
23000
+ },
23001
+ };
23002
+
22967
23003
  /**
22968
23004
  * Represent a raw XML string
22969
23005
  */
@@ -34234,7 +34270,6 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
34234
34270
  duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
34235
34271
  formatChartDatasetValue: formatChartDatasetValue,
34236
34272
  formatTickValue: formatTickValue,
34237
- getChartJSConstructor: getChartJSConstructor,
34238
34273
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
34239
34274
  getDefinedAxis: getDefinedAxis,
34240
34275
  getPieColors: getPieColors,
@@ -45082,7 +45117,8 @@ css /* scss */ `
45082
45117
  &.pivot-dimension-invalid {
45083
45118
  background-color: #ffdddd;
45084
45119
  border-color: red !important;
45085
- select {
45120
+ select,
45121
+ input {
45086
45122
  background-color: #ffdddd;
45087
45123
  }
45088
45124
  }
@@ -46943,7 +46979,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
46943
46979
  this.notification.notifyUser({
46944
46980
  type: "info",
46945
46981
  text: _t("Pivot updates only work with dynamic pivot tables. Use %s or re-insert the static pivot from the Data menu.", pivotExample),
46946
- sticky: false,
46982
+ sticky: true,
46947
46983
  });
46948
46984
  }
46949
46985
  }
@@ -53295,6 +53331,15 @@ class BordersPlugin extends CorePlugin {
53295
53331
  case "SET_BORDER":
53296
53332
  this.setBorder(cmd.sheetId, cmd.col, cmd.row, cmd.border);
53297
53333
  break;
53334
+ case "SET_BORDERS_ON_TARGET":
53335
+ for (const zone of cmd.target) {
53336
+ for (let row = zone.top; row <= zone.bottom; row++) {
53337
+ for (let col = zone.left; col <= zone.right; col++) {
53338
+ this.setBorder(cmd.sheetId, col, row, cmd.border);
53339
+ }
53340
+ }
53341
+ }
53342
+ break;
53298
53343
  case "SET_ZONE_BORDERS":
53299
53344
  if (cmd.border) {
53300
53345
  const target = cmd.target.map((zone) => this.getters.expandZone(cmd.sheetId, zone));
@@ -63057,25 +63102,6 @@ class AutofillPlugin extends UIPlugin {
63057
63102
  case "AUTOFILL_AUTO":
63058
63103
  this.autofillAuto();
63059
63104
  break;
63060
- case "AUTOFILL_CELL":
63061
- this.autoFillMerge(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
63062
- const sheetId = this.getters.getActiveSheetId();
63063
- this.dispatch("UPDATE_CELL", {
63064
- sheetId,
63065
- col: cmd.col,
63066
- row: cmd.row,
63067
- style: cmd.style || null,
63068
- content: cmd.content || "",
63069
- format: cmd.format || "",
63070
- });
63071
- this.dispatch("SET_BORDER", {
63072
- sheetId,
63073
- col: cmd.col,
63074
- row: cmd.row,
63075
- border: cmd.border,
63076
- });
63077
- this.autofillCF(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
63078
- this.autofillDV(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
63079
63105
  }
63080
63106
  }
63081
63107
  // ---------------------------------------------------------------------------
@@ -63099,6 +63125,7 @@ class AutofillPlugin extends UIPlugin {
63099
63125
  }
63100
63126
  const source = this.getters.getSelectedZone();
63101
63127
  const target = this.autofillZone;
63128
+ const autofillCellsData = [];
63102
63129
  switch (this.direction) {
63103
63130
  case "down" /* DIRECTION.DOWN */:
63104
63131
  for (let col = source.left; col <= source.right; col++) {
@@ -63108,7 +63135,7 @@ class AutofillPlugin extends UIPlugin {
63108
63135
  }
63109
63136
  const generator = this.createGenerator(xcs);
63110
63137
  for (let row = target.top; row <= target.bottom; row++) {
63111
- this.computeNewCell(generator, col, row, apply);
63138
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
63112
63139
  }
63113
63140
  }
63114
63141
  break;
@@ -63120,7 +63147,7 @@ class AutofillPlugin extends UIPlugin {
63120
63147
  }
63121
63148
  const generator = this.createGenerator(xcs);
63122
63149
  for (let row = target.bottom; row >= target.top; row--) {
63123
- this.computeNewCell(generator, col, row, apply);
63150
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
63124
63151
  }
63125
63152
  }
63126
63153
  break;
@@ -63132,7 +63159,7 @@ class AutofillPlugin extends UIPlugin {
63132
63159
  }
63133
63160
  const generator = this.createGenerator(xcs);
63134
63161
  for (let col = target.right; col >= target.left; col--) {
63135
- this.computeNewCell(generator, col, row, apply);
63162
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
63136
63163
  }
63137
63164
  }
63138
63165
  break;
@@ -63144,12 +63171,26 @@ class AutofillPlugin extends UIPlugin {
63144
63171
  }
63145
63172
  const generator = this.createGenerator(xcs);
63146
63173
  for (let col = target.left; col <= target.right; col++) {
63147
- this.computeNewCell(generator, col, row, apply);
63174
+ autofillCellsData.push(this.computeNewCell(generator, col, row));
63148
63175
  }
63149
63176
  }
63150
63177
  break;
63151
63178
  }
63152
63179
  if (apply) {
63180
+ const bordersZones = {};
63181
+ const cfNewRanges = {};
63182
+ const dvNewZones = {};
63183
+ const sheetId = this.getters.getActiveSheetId();
63184
+ for (const data of autofillCellsData) {
63185
+ this.collectBordersData(data, bordersZones);
63186
+ this.autofillMerge(sheetId, data);
63187
+ this.autofillCell(sheetId, data);
63188
+ this.collectConditionalFormatsData(sheetId, data, cfNewRanges);
63189
+ this.collectDataValidationsData(sheetId, data, dvNewZones);
63190
+ }
63191
+ this.autofillBorders(sheetId, bordersZones);
63192
+ this.autofillConditionalFormats(sheetId, cfNewRanges);
63193
+ this.autofillDataValidations(sheetId, dvNewZones);
63153
63194
  this.autofillZone = undefined;
63154
63195
  this.selection.resizeAnchorZone(this.direction, this.steps);
63155
63196
  this.lastCellSelected = {};
@@ -63158,6 +63199,95 @@ class AutofillPlugin extends UIPlugin {
63158
63199
  this.tooltip = undefined;
63159
63200
  }
63160
63201
  }
63202
+ collectBordersData(data, bordersPositions) {
63203
+ const key = JSON.stringify(data.border);
63204
+ if (!(key in bordersPositions)) {
63205
+ bordersPositions[key] = [];
63206
+ }
63207
+ bordersPositions[key].push(positionToZone({ col: data.col, row: data.row }));
63208
+ }
63209
+ collectConditionalFormatsData(sheetId, data, cfNewRanges) {
63210
+ const { originCol, originRow, col, row } = data;
63211
+ const cfsAtOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);
63212
+ const xc = toXC(col, row);
63213
+ for (const cf of cfsAtOrigin) {
63214
+ if (!(cf.id in cfNewRanges)) {
63215
+ cfNewRanges[cf.id] = [];
63216
+ }
63217
+ cfNewRanges[cf.id].push(xc);
63218
+ }
63219
+ }
63220
+ collectDataValidationsData(sheetId, data, dvNewZones) {
63221
+ const { originCol, originRow, col, row } = data;
63222
+ const cellPosition = { sheetId, col: originCol, row: originRow };
63223
+ const dvsAtOrigin = this.getters.getValidationRuleForCell(cellPosition);
63224
+ if (!dvsAtOrigin) {
63225
+ return;
63226
+ }
63227
+ if (!(dvsAtOrigin.id in dvNewZones)) {
63228
+ dvNewZones[dvsAtOrigin.id] = [];
63229
+ }
63230
+ dvNewZones[dvsAtOrigin.id].push(positionToZone({ col, row }));
63231
+ }
63232
+ autofillCell(sheetId, data) {
63233
+ this.dispatch("UPDATE_CELL", {
63234
+ sheetId,
63235
+ col: data.col,
63236
+ row: data.row,
63237
+ content: data.content || "",
63238
+ style: data.style || null,
63239
+ format: data.format || "",
63240
+ });
63241
+ // Still usefull in odoo ATM to autofill field sync
63242
+ this.dispatch("AUTOFILL_CELL", data);
63243
+ }
63244
+ autofillBorders(sheetId, bordersPositions) {
63245
+ for (const stringifiedBorder in bordersPositions) {
63246
+ const border = stringifiedBorder === "undefined" ? undefined : JSON.parse(stringifiedBorder);
63247
+ this.dispatch("SET_BORDERS_ON_TARGET", {
63248
+ sheetId,
63249
+ border,
63250
+ target: recomputeZones(bordersPositions[stringifiedBorder]),
63251
+ });
63252
+ }
63253
+ }
63254
+ autofillConditionalFormats(sheetId, cfNewRanges) {
63255
+ for (const cfId in cfNewRanges) {
63256
+ const changes = cfNewRanges[cfId];
63257
+ const cf = this.getters.getConditionalFormats(sheetId).find((cf) => cf.id === cfId);
63258
+ if (!cf) {
63259
+ continue;
63260
+ }
63261
+ const newCfRanges = this.getters.getAdaptedCfRanges(sheetId, cf, changes.map(toZone), []);
63262
+ if (newCfRanges) {
63263
+ this.dispatch("ADD_CONDITIONAL_FORMAT", {
63264
+ cf: {
63265
+ id: cf.id,
63266
+ rule: cf.rule,
63267
+ stopIfTrue: cf.stopIfTrue,
63268
+ },
63269
+ ranges: newCfRanges,
63270
+ sheetId,
63271
+ });
63272
+ }
63273
+ }
63274
+ }
63275
+ autofillDataValidations(sheetId, dvNewZones) {
63276
+ for (const dvId in dvNewZones) {
63277
+ const changes = dvNewZones[dvId];
63278
+ const dvOrigin = this.getters.getDataValidationRule(sheetId, dvId);
63279
+ if (!dvOrigin) {
63280
+ continue;
63281
+ }
63282
+ const dvRangesXcs = dvOrigin.ranges.map((range) => range.zone);
63283
+ const newDvRanges = recomputeZones(dvRangesXcs.concat(changes), []);
63284
+ this.dispatch("ADD_DATA_VALIDATION_RULE", {
63285
+ rule: dvOrigin,
63286
+ ranges: newDvRanges.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
63287
+ sheetId,
63288
+ });
63289
+ }
63290
+ }
63161
63291
  /**
63162
63292
  * Select a cell which becomes the last cell of the autofillZone
63163
63293
  */
@@ -63236,22 +63366,20 @@ class AutofillPlugin extends UIPlugin {
63236
63366
  /**
63237
63367
  * Generate the next cell
63238
63368
  */
63239
- computeNewCell(generator, col, row, apply) {
63369
+ computeNewCell(generator, col, row) {
63240
63370
  const { cellData, tooltip, origin } = generator.next();
63241
63371
  const { content, style, border, format } = cellData;
63242
63372
  this.tooltip = tooltip;
63243
- if (apply) {
63244
- this.dispatch("AUTOFILL_CELL", {
63245
- originCol: origin.col,
63246
- originRow: origin.row,
63247
- col,
63248
- row,
63249
- content,
63250
- style,
63251
- border,
63252
- format,
63253
- });
63254
- }
63373
+ return {
63374
+ originCol: origin.col,
63375
+ originRow: origin.row,
63376
+ col,
63377
+ row,
63378
+ content,
63379
+ style,
63380
+ border,
63381
+ format,
63382
+ };
63255
63383
  }
63256
63384
  /**
63257
63385
  * Get the rule associated to the current cell
@@ -63319,8 +63447,8 @@ class AutofillPlugin extends UIPlugin {
63319
63447
  ? position[first].value
63320
63448
  : position[second].value;
63321
63449
  }
63322
- autoFillMerge(originCol, originRow, col, row) {
63323
- const sheetId = this.getters.getActiveSheetId();
63450
+ autofillMerge(sheetId, data) {
63451
+ const { originCol, originRow, col, row } = data;
63324
63452
  const position = { sheetId, col, row };
63325
63453
  const originPosition = { sheetId, col: originCol, row: originRow };
63326
63454
  if (this.getters.isInMerge(position) && !this.getters.isInMerge(originPosition)) {
@@ -63347,35 +63475,6 @@ class AutofillPlugin extends UIPlugin {
63347
63475
  });
63348
63476
  }
63349
63477
  }
63350
- autofillCF(originCol, originRow, col, row) {
63351
- const sheetId = this.getters.getActiveSheetId();
63352
- const cfOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);
63353
- for (const cf of cfOrigin) {
63354
- const newCfRanges = this.getters.getAdaptedCfRanges(sheetId, cf, [positionToZone({ col, row })], []);
63355
- if (newCfRanges) {
63356
- this.dispatch("ADD_CONDITIONAL_FORMAT", {
63357
- cf: deepCopy(cf),
63358
- ranges: newCfRanges,
63359
- sheetId,
63360
- });
63361
- }
63362
- }
63363
- }
63364
- autofillDV(originCol, originRow, col, row) {
63365
- const sheetId = this.getters.getActiveSheetId();
63366
- const cellPosition = { sheetId, col: originCol, row: originRow };
63367
- const dvOrigin = this.getters.getValidationRuleForCell(cellPosition);
63368
- if (!dvOrigin) {
63369
- return;
63370
- }
63371
- const dvRangesZones = dvOrigin.ranges.map((range) => range.zone);
63372
- const newDvRanges = recomputeZones(dvRangesZones.concat(positionToZone({ col, row })), []);
63373
- this.dispatch("ADD_DATA_VALIDATION_RULE", {
63374
- rule: dvOrigin,
63375
- ranges: newDvRanges.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
63376
- sheetId,
63377
- });
63378
- }
63379
63478
  // ---------------------------------------------------------------------------
63380
63479
  // Grid rendering
63381
63480
  // ---------------------------------------------------------------------------
@@ -75890,6 +75989,7 @@ const registries = {
75890
75989
  supportedPivotPositionalFormulaRegistry,
75891
75990
  pivotToFunctionValueRegistry,
75892
75991
  migrationStepRegistry,
75992
+ chartJsExtensionRegistry,
75893
75993
  };
75894
75994
  const helpers = {
75895
75995
  arg,
@@ -76090,6 +76190,6 @@ exports.tokenColors = tokenColors;
76090
76190
  exports.tokenize = tokenize;
76091
76191
 
76092
76192
 
76093
- __info__.version = "18.2.5";
76094
- __info__.date = "2025-03-26T12:47:44.113Z";
76095
- __info__.hash = "4675edd";
76193
+ __info__.version = "18.2.7";
76194
+ __info__.date = "2025-04-14T17:19:31.011Z";
76195
+ __info__.hash = "e187958";