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