@jsamuel1/pptxgenjs 4.1.3 → 4.1.4

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.
@@ -1,4 +1,4 @@
1
- /* PptxGenJS 4.1.1 @ 2026-06-07T21:17:30.873Z */
1
+ /* PptxGenJS 4.1.4 @ 2026-06-08T05:10:40.789Z */
2
2
  import JSZip from 'jszip';
3
3
 
4
4
  /******************************************************************************
@@ -842,11 +842,14 @@ function genXmlGradientFill(props) {
842
842
  const stops = [...props.stops].sort((a, b) => (a.position || 0) - (b.position || 0));
843
843
  const gsList = stops
844
844
  .map(stop => {
845
- // position 0–100 → `pos` in thousandths of a percent (× 1000)
846
- const pos = Math.round((stop.position || 0) * 1000);
845
+ // position 0–100 → `pos` in thousandths of a percent (× 1000).
846
+ // `pos` is ST_PositiveFixedPercentage [0,100000]; clamp the 0–100 input
847
+ // before scaling so out-of-range stops stay schema-valid (clamp-don't-crash).
848
+ const pos = Math.round(Math.max(0, Math.min(100, stop.position || 0)) * 1000);
847
849
  // Per-stop transparency uses PROMPT.md direct mapping (100 = opaque → 100000; 40 → 40000).
848
850
  // NOTE: this differs from the solid-fill path which inverts via `(100 - transparency) * 1000`.
849
- const inner = typeof stop.transparency === 'number' ? `<a:alpha val="${Math.round(stop.transparency * 1000)}"/>` : '';
851
+ // `a:alpha@val` is also ST_PositiveFixedPercentage [0,100000]; clamp into [0,100] first.
852
+ const inner = typeof stop.transparency === 'number' ? `<a:alpha val="${Math.round(Math.max(0, Math.min(100, stop.transparency)) * 1000)}"/>` : '';
850
853
  return `<a:gs pos="${pos}">${createColorElement(stop.color, inner)}</a:gs>`;
851
854
  })
852
855
  .join('');
@@ -1089,6 +1092,75 @@ function svgPathToOoxml(svgPathD, width, height) {
1089
1092
  }
1090
1093
  return `<a:custGeom><a:avLst/><a:gdLst/><a:ahLst/><a:cxnLst/><a:rect l="l" t="t" r="r" b="b"/><a:pathLst><a:path w="${pathW}" h="${pathH}">${xml}</a:path></a:pathLst></a:custGeom>`;
1091
1094
  }
1095
+ /**
1096
+ * Compute evenly-spaced grid cell positions within a bounding area.
1097
+ * - Pure math utility (no OOXML emission); returns one `{ x, y, w, h }` (inches) per item, in item order.
1098
+ * - Eliminates repetitive grid-math when positioning capability cards, icon grids, comparison layouts, etc.
1099
+ *
1100
+ * Calculation (per item `i`):
1101
+ * cellW = (area.w - (columns - 1) * gapX) / columns
1102
+ * rows = ceil(items / columns)
1103
+ * cellH = (area.h - (rows - 1) * gapY) / rows
1104
+ * col = i % columns; row = floor(i / columns)
1105
+ * x = area.x + col * (cellW + gapX); y = area.y + row * (cellH + gapY)
1106
+ *
1107
+ * @param {LayoutGridProps} props - grid options
1108
+ * @returns {LayoutGridResult} array of `{ x, y, w, h }` cells (inches), one per item
1109
+ * @throws {Error} when `area` has zero/negative width or height
1110
+ * @example pptx.layoutGrid({ items: 6, columns: 3, area: { x: 0.5, y: 2, w: 12, h: 4 }, gap: 0.2 })
1111
+ */
1112
+ function layoutGrid(props) {
1113
+ var _a, _b, _c, _d, _e;
1114
+ const { items, columns, area } = props;
1115
+ const gap = (_a = props.gap) !== null && _a !== void 0 ? _a : 0.2;
1116
+ const gapX = (_b = props.gapX) !== null && _b !== void 0 ? _b : gap;
1117
+ const gapY = (_c = props.gapY) !== null && _c !== void 0 ? _c : gap;
1118
+ const padding = (_d = props.padding) !== null && _d !== void 0 ? _d : 0;
1119
+ const align = (_e = props.align) !== null && _e !== void 0 ? _e : 'start';
1120
+ // Edge case: no items -> empty result
1121
+ if (!items || items <= 0)
1122
+ return [];
1123
+ // Guard: a zero/negative area can't be subdivided
1124
+ if (!area || !(area.w > 0) || !(area.h > 0))
1125
+ throw new Error('layoutGrid: `area` requires positive `w` and `h`');
1126
+ if (!(columns > 0))
1127
+ throw new Error('layoutGrid: `columns` must be a positive number');
1128
+ const rows = Math.ceil(items / columns);
1129
+ const cellW = (area.w - (columns - 1) * gapX) / columns;
1130
+ const cellH = (area.h - (rows - 1) * gapY) / rows;
1131
+ const result = [];
1132
+ for (let i = 0; i < items; i++) {
1133
+ const col = i % columns;
1134
+ const row = Math.floor(i / columns);
1135
+ // Items on the final (possibly partial) row can be re-aligned within the area
1136
+ let rowCellW = cellW;
1137
+ let rowOffsetX = 0;
1138
+ const isLastRow = row === rows - 1;
1139
+ const lastRowCount = items - (rows - 1) * columns;
1140
+ if (isLastRow && lastRowCount < columns && lastRowCount > 0) {
1141
+ const rowCols = lastRowCount;
1142
+ if (align === 'stretch') {
1143
+ // Widen the partial row's cells to fill the full area width
1144
+ rowCellW = (area.w - (rowCols - 1) * gapX) / rowCols;
1145
+ }
1146
+ else if (align === 'center') {
1147
+ // Centre the partial row (cells keep their size)
1148
+ const rowWidth = rowCols * cellW + (rowCols - 1) * gapX;
1149
+ rowOffsetX = (area.w - rowWidth) / 2;
1150
+ }
1151
+ }
1152
+ const x = area.x + rowOffsetX + col * (rowCellW + gapX);
1153
+ const y = area.y + row * (cellH + gapY);
1154
+ // `padding` insets each cell box on all sides
1155
+ result.push({
1156
+ x: x + padding,
1157
+ y: y + padding,
1158
+ w: rowCellW - 2 * padding,
1159
+ h: cellH - 2 * padding,
1160
+ });
1161
+ }
1162
+ return result;
1163
+ }
1092
1164
 
1093
1165
  /**
1094
1166
  * PptxGenJS: Table Generation
@@ -2513,6 +2585,122 @@ function addCalloutDefinition(target, opts) {
2513
2585
  textOpts.objectName = options.objectName;
2514
2586
  addTextDefinition(target, [{ text: options.text || '', options: null }], textOpts, false);
2515
2587
  }
2588
+ /**
2589
+ * Adds a structured "card" to a slide definition (docs/feature-card-helper.md).
2590
+ * Builds a single shape group (`<p:grpSp>`) containing a rounded-rect background and,
2591
+ * as applicable, an icon container + icon (SVG path or emoji/text), a title, a description,
2592
+ * and a top-right badge — all positioned with group-relative coordinates. When `animation`
2593
+ * is supplied it is attached to the group object so the whole card animates as one.
2594
+ * @param {PresSlide} target slide the card should be added to
2595
+ * @param {CardProps} opts card options
2596
+ */
2597
+ function addCardDefinition(target, opts) {
2598
+ const options = typeof opts === 'object' ? opts : {};
2599
+ const x = options.x !== undefined ? Number(options.x) : 1;
2600
+ const y = options.y !== undefined ? Number(options.y) : 1;
2601
+ const w = options.w !== undefined ? Number(options.w) : 3;
2602
+ const h = options.h !== undefined ? Number(options.h) : 2;
2603
+ const padding = 0.2;
2604
+ const cornerRadius = options.cornerRadius !== undefined ? options.cornerRadius : 0.12;
2605
+ const fill = options.fill !== undefined ? options.fill : '1a1a24';
2606
+ const align = options.align === 'left' ? 'left' : 'center';
2607
+ const iconPosition = options.iconPosition === 'left' ? 'left' : 'top';
2608
+ const iconSize = options.iconSize !== undefined ? Number(options.iconSize) : 0.4;
2609
+ const hasIcon = options.icon !== undefined && options.icon !== null;
2610
+ const titleFont = options.titleFont || {};
2611
+ const descFont = options.descFont || {};
2612
+ const textAlign = align === 'center' ? 'center' : 'left';
2613
+ // Create the group container; children below use coords relative to the group origin (0,0..w,h)
2614
+ const group = addGroupDefinition(target, { x, y, w, h, objectName: options.objectName });
2615
+ // The group is the just-pushed top-level slide object — grab it so card-level animation can attach
2616
+ const groupObj = target._slideObjects[target._slideObjects.length - 1];
2617
+ if (options.animation && groupObj && groupObj.options)
2618
+ groupObj.options.animation = options.animation;
2619
+ // 1) Background roundRect (fill + optional border/shadow/glow). `rectRadius` maps inches -> adj.
2620
+ const bgOpts = {
2621
+ x: 0, y: 0, w, h,
2622
+ fill: typeof fill === 'string' ? { color: fill } : fill,
2623
+ rectRadius: cornerRadius,
2624
+ };
2625
+ if (options.border)
2626
+ bgOpts.line = { color: options.border.color || '2A2438', width: options.border.width || 1 };
2627
+ if (options.shadow)
2628
+ bgOpts.shadow = Object.assign({ type: 'outer' }, options.shadow);
2629
+ if (options.glow)
2630
+ bgOpts.glow = options.glow;
2631
+ group.addShape(SHAPE_TYPE.ROUNDED_RECTANGLE, bgOpts);
2632
+ // 2) Layout anchors for icon / title / description (group-relative inches)
2633
+ let iconX = (w - iconSize) / 2;
2634
+ let iconY = padding;
2635
+ let titleX = padding;
2636
+ let titleY = hasIcon ? padding + iconSize + 0.15 : padding;
2637
+ let titleW = w - 2 * padding;
2638
+ const titleH = 0.4;
2639
+ if (iconPosition === 'left' && hasIcon) {
2640
+ iconX = padding;
2641
+ iconY = padding;
2642
+ titleX = padding + iconSize + 0.2;
2643
+ titleY = padding;
2644
+ titleW = w - titleX - padding;
2645
+ }
2646
+ const descX = titleX;
2647
+ const descY = titleY + titleH + 0.05;
2648
+ const descW = titleW;
2649
+ const descH = Math.max(0.2, h - descY - padding);
2650
+ // 3) Icon container + glyph
2651
+ if (hasIcon) {
2652
+ const iconFill = options.iconFill !== undefined ? options.iconFill : '7C3AED';
2653
+ group.addShape(SHAPE_TYPE.ROUNDED_RECTANGLE, {
2654
+ x: iconX, y: iconY, w: iconSize, h: iconSize,
2655
+ fill: { color: iconFill }, rectRadius: cornerRadius / 2, line: { type: 'none' },
2656
+ });
2657
+ const glyphColor = titleFont.color || 'E4E4ED';
2658
+ if (typeof options.icon === 'string') {
2659
+ // Emoji / text glyph centred in the container
2660
+ group.addText(options.icon, {
2661
+ x: iconX, y: iconY, w: iconSize, h: iconSize,
2662
+ align: 'center', valign: 'middle', fontSize: Math.round(iconSize * 36), color: glyphColor,
2663
+ });
2664
+ }
2665
+ else if (options.icon && typeof options.icon === 'object' && options.icon.svgPath) {
2666
+ // SVG path glyph, inset within the container; emits <a:custGeom>
2667
+ const inset = iconSize * 0.22;
2668
+ group.addShape('rect', {
2669
+ x: iconX + inset, y: iconY + inset, w: iconSize - 2 * inset, h: iconSize - 2 * inset,
2670
+ svgPath: options.icon.svgPath, fill: { color: glyphColor }, line: { type: 'none' },
2671
+ });
2672
+ }
2673
+ }
2674
+ // 4) Title
2675
+ group.addText(options.title || '', {
2676
+ x: titleX, y: titleY, w: titleW, h: titleH,
2677
+ fontFace: titleFont.face, fontSize: titleFont.size !== undefined ? titleFont.size : 13,
2678
+ bold: titleFont.bold !== undefined ? titleFont.bold : true,
2679
+ color: titleFont.color || 'E4E4ED', align: textAlign, valign: 'top',
2680
+ });
2681
+ // 5) Description (shrink-to-fit so overflow stays inside the card)
2682
+ if (options.description) {
2683
+ group.addText(options.description, {
2684
+ x: descX, y: descY, w: descW, h: descH,
2685
+ fontFace: descFont.face, fontSize: descFont.size !== undefined ? descFont.size : 10,
2686
+ bold: descFont.bold !== undefined ? descFont.bold : false,
2687
+ color: descFont.color || '8A8A9A', align: textAlign, valign: 'top', fit: 'shrink',
2688
+ });
2689
+ }
2690
+ // 6) Badge (top-right)
2691
+ if (options.badge && options.badge.text) {
2692
+ const badgeH = 0.28;
2693
+ const badgeW = Math.max(0.5, options.badge.text.length * 0.11 + 0.2);
2694
+ group.addShape(SHAPE_TYPE.ROUNDED_RECTANGLE, {
2695
+ x: w - badgeW - padding, y: padding, w: badgeW, h: badgeH,
2696
+ fill: { color: options.badge.fill || '10B981' }, rectRadius: badgeH / 2, line: { type: 'none' },
2697
+ });
2698
+ group.addText(options.badge.text, {
2699
+ x: w - badgeW - padding, y: padding, w: badgeW, h: badgeH,
2700
+ align: 'center', valign: 'middle', fontSize: 8, bold: true, color: options.badge.color || 'FFFFFF',
2701
+ });
2702
+ }
2703
+ }
2516
2704
  /**
2517
2705
  * Feature 6: Adds a shape group to a slide definition and returns a group handle.
2518
2706
  * The group emits a `<p:grpSp>` whose `<a:xfrm>` carries the absolute position/size plus
@@ -2856,9 +3044,14 @@ function addTextDefinition(target, text, opts, isPlaceholder) {
2856
3044
  const value = from + i;
2857
3045
  const frameOpts = Object.assign({}, opts);
2858
3046
  delete frameOpts.counter;
2859
- // Sequential entrance: first frame immediately, each later frame one step after the previous.
2860
- frameOpts.animation = { type: 'appear', trigger: 'afterPrevious', delay: i === 0 ? 0 : stepMs };
2861
- // Every frame except the last hides itself one step after appearing -> count-up effect.
3047
+ // Odometer entrance: ALL frames live in ONE parallel build step (`withPrevious`),
3048
+ // each appearing at a CUMULATIVE delay from the container start (frame 0 -> 0,
3049
+ // frame 1 -> stepMs, frame 2 -> 2*stepMs, ...). `afterPrevious` would split each
3050
+ // frame into a separate build step in genXmlTiming, breaking the count-up; using
3051
+ // `withPrevious` keeps them in one group so the staggered delays drive the odometer.
3052
+ frameOpts.animation = { type: 'appear', trigger: 'withPrevious', delay: i * stepMs };
3053
+ // Every frame except the last hides itself one step after IT appears (the exit delay
3054
+ // is relative to each frame's own appearance), so frame N+1 masks frame N -> count-up.
2862
3055
  if (i < frameCount - 1)
2863
3056
  frameOpts._counterExit = stepMs;
2864
3057
  else
@@ -3267,6 +3460,16 @@ class Slide {
3267
3460
  addCalloutDefinition(this, options);
3268
3461
  return this;
3269
3462
  }
3463
+ /**
3464
+ * Add a structured card (rounded-rect background + optional icon/title/description/badge)
3465
+ * to Slide as a single shape group.
3466
+ * @param {CardProps} options - card options
3467
+ * @return {Slide} this Slide
3468
+ */
3469
+ addCard(options) {
3470
+ addCardDefinition(this, options);
3471
+ return this;
3472
+ }
3270
3473
  /**
3271
3474
  * Add table to Slide
3272
3475
  * @param {TableRow[]} tableRows - table rows
@@ -3988,7 +4191,7 @@ function makeXmlCharts(rel) {
3988
4191
  * @example '<c:lineChart>'
3989
4192
  * @return {string} XML chart
3990
4193
  */
3991
- function makeChartType(chartType, data, opts, valAxisId, catAxisId, isMultiTypeChart) {
4194
+ function makeChartType(chartType, data, opts, valAxisId, catAxisId, _isMultiTypeChart) {
3992
4195
  // NOTE: "Chart Range" (as shown in "select Chart Area dialog") is calculated.
3993
4196
  // ....: Ensure each X/Y Axis/Col has same row height (esp. applicable to XY Scatter where X can often be larger than Y's)
3994
4197
  let colorIndex = -1; // Maintain the color index by region
@@ -4013,6 +4216,11 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId, isMultiTypeC
4013
4216
  if (chartType === CHART_TYPE.RADAR) {
4014
4217
  strXml += '<c:radarStyle val="' + opts.radarStyle + '"/>';
4015
4218
  }
4219
+ // `c:grouping` is a required first child of CT_LineChart (EG_LineChartShared, minOccurs=1)
4220
+ // and must precede `c:varyColors`. Without it, line/combo charts fail schema validation.
4221
+ if (chartType === CHART_TYPE.LINE) {
4222
+ strXml += '<c:grouping val="standard"/>';
4223
+ }
4016
4224
  strXml += '<c:varyColors val="0"/>';
4017
4225
  // 2: "Series" block for every data row
4018
4226
  /* EX1:
@@ -4088,7 +4296,26 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId, isMultiTypeC
4088
4296
  }
4089
4297
  strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW);
4090
4298
  strXml += ' </c:spPr>';
4091
- strXml += ' <c:invertIfNegative val="0"/>';
4299
+ // `c:invertIfNegative` is only valid on bar/bar3D series (CT_BarSer). Emitting it for
4300
+ // area/line/radar series violates the OOXML schema (invalid child of CT_AreaSer/CT_LineSer/CT_RadarSer).
4301
+ if (chartType === CHART_TYPE.BAR || chartType === CHART_TYPE.BAR3D) {
4302
+ strXml += ' <c:invertIfNegative val="0"/>';
4303
+ }
4304
+ // 'c:marker' tag: `lineDataSymbol`
4305
+ // NOTE: CT_LineSer requires `marker` to precede `dLbls` (sequence: …spPr, marker?, dPt*, dLbls?…),
4306
+ // so this block must be emitted before the `c:dLbls` block below or line/combo charts fail schema validation.
4307
+ if (chartType === CHART_TYPE.LINE || chartType === CHART_TYPE.RADAR) {
4308
+ strXml += '<c:marker>';
4309
+ strXml += ' <c:symbol val="' + opts.lineDataSymbol + '"/>';
4310
+ if (opts.lineDataSymbolSize)
4311
+ strXml += `<c:size val="${opts.lineDataSymbolSize}"/>`; // Defaults to "auto" otherwise (but this is usually too small, so there is a default)
4312
+ strXml += ' <c:spPr>';
4313
+ strXml += ` <a:solidFill>${createColorElement(opts.chartColors[obj._dataIndex + 1 > opts.chartColors.length ? Math.floor(Math.random() * opts.chartColors.length) : obj._dataIndex])}</a:solidFill>`;
4314
+ strXml += ` <a:ln w="${opts.lineDataSymbolLineSize}" cap="flat"><a:solidFill>${createColorElement(opts.lineDataSymbolLineColor || seriesColor)}</a:solidFill><a:prstDash val="solid"/><a:round/></a:ln>`;
4315
+ strXml += ' <a:effectLst/>';
4316
+ strXml += ' </c:spPr>';
4317
+ strXml += '</c:marker>';
4318
+ }
4092
4319
  // Data Labels per series
4093
4320
  // NOTE: [20190117] Adding these to RADAR chart causes unrecoverable corruption!
4094
4321
  if (chartType !== CHART_TYPE.RADAR) {
@@ -4109,19 +4336,6 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId, isMultiTypeC
4109
4336
  strXml += `<c:showLeaderLines val="${opts.showLeaderLines ? '1' : '0'}"/>`;
4110
4337
  strXml += '</c:dLbls>';
4111
4338
  }
4112
- // 'c:marker' tag: `lineDataSymbol`
4113
- if (chartType === CHART_TYPE.LINE || chartType === CHART_TYPE.RADAR) {
4114
- strXml += '<c:marker>';
4115
- strXml += ' <c:symbol val="' + opts.lineDataSymbol + '"/>';
4116
- if (opts.lineDataSymbolSize)
4117
- strXml += `<c:size val="${opts.lineDataSymbolSize}"/>`; // Defaults to "auto" otherwise (but this is usually too small, so there is a default)
4118
- strXml += ' <c:spPr>';
4119
- strXml += ` <a:solidFill>${createColorElement(opts.chartColors[obj._dataIndex + 1 > opts.chartColors.length ? Math.floor(Math.random() * opts.chartColors.length) : obj._dataIndex])}</a:solidFill>`;
4120
- strXml += ` <a:ln w="${opts.lineDataSymbolLineSize}" cap="flat"><a:solidFill>${createColorElement(opts.lineDataSymbolLineColor || seriesColor)}</a:solidFill><a:prstDash val="solid"/><a:round/></a:ln>`;
4121
- strXml += ' <a:effectLst/>';
4122
- strXml += ' </c:spPr>';
4123
- strXml += '</c:marker>';
4124
- }
4125
4339
  // Allow users with a single data set to pass their own array of colors (check for this using != ours)
4126
4340
  // Color chart bars various colors when >1 color
4127
4341
  // NOTE: `<c:dPt>` created with various colors will change PPT legend by design so each dataPt/color is an legend item!
@@ -4270,6 +4484,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId, isMultiTypeC
4270
4484
  // 2: Series: (One for each Y-Axis)
4271
4485
  colorIndex = -1;
4272
4486
  data.filter((_obj, idx) => idx > 0).forEach((obj, idx) => {
4487
+ var _a;
4273
4488
  colorIndex++;
4274
4489
  strXml += '<c:ser>';
4275
4490
  strXml += ` <c:idx val="${idx}"/>`;
@@ -4322,7 +4537,7 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId, isMultiTypeC
4322
4537
  // Option: scatter data point labels
4323
4538
  if (opts.showLabel) {
4324
4539
  const chartUuid = getUuid('-xxxx-xxxx-xxxx-xxxxxxxxxxxx');
4325
- if (obj.labels[0] && (opts.dataLabelFormatScatter === 'custom' || opts.dataLabelFormatScatter === 'customXY')) {
4540
+ if (((_a = obj.labels) === null || _a === void 0 ? void 0 : _a[0]) && (opts.dataLabelFormatScatter === 'custom' || opts.dataLabelFormatScatter === 'customXY')) {
4326
4541
  strXml += '<c:dLbls>';
4327
4542
  obj.labels[0].forEach((label, idx) => {
4328
4543
  if (opts.dataLabelFormatScatter === 'custom' || opts.dataLabelFormatScatter === 'customXY') {
@@ -4791,8 +5006,12 @@ function makeChartType(chartType, data, opts, valAxisId, catAxisId, isMultiTypeC
4791
5006
  // 4: Close "SERIES"
4792
5007
  strXml += ' </c:ser>';
4793
5008
  strXml += ` <c:firstSliceAng val="${opts.firstSliceAng ? Math.round(opts.firstSliceAng) : 0}"/>`;
4794
- if (chartType === CHART_TYPE.DOUGHNUT)
4795
- strXml += `<c:holeSize val="${typeof opts.holeSize === 'number' ? opts.holeSize : '50'}"/>`;
5009
+ if (chartType === CHART_TYPE.DOUGHNUT) {
5010
+ // ST_HoleSize restricts to [10,90]; clamp out-of-range input (clamp-don't-crash)
5011
+ const rawHoleSize = typeof opts.holeSize === 'number' ? opts.holeSize : 50;
5012
+ const holeSizeVal = Math.max(10, Math.min(90, Math.round(rawHoleSize)));
5013
+ strXml += `<c:holeSize val="${holeSizeVal}"/>`;
5014
+ }
4796
5015
  strXml += '</c:' + chartType + 'Chart>';
4797
5016
  // Done with Doughnut/Pie
4798
5017
  break;
@@ -4885,11 +5104,15 @@ function makeCatAxis(opts, axisId, valAxisId) {
4885
5104
  strXml += ' </c:txPr>';
4886
5105
  strXml += ' <c:crossAx val="' + valAxisId + '"/>';
4887
5106
  strXml += ` <c:${typeof opts.valAxisCrossesAt === 'number' ? 'crossesAt' : 'crosses'} val="${opts.valAxisCrossesAt || 'autoZero'}"/>`;
4888
- strXml += ' <c:auto val="1"/>';
4889
- strXml += ' <c:lblAlgn val="ctr"/>';
4890
- strXml += ` <c:noMultiLvlLbl val="${opts.catAxisMultiLevelLabels ? 0 : 1}"/>`;
4891
- if (opts.catAxisLabelFrequency)
4892
- strXml += ' <c:tickLblSkip val="' + opts.catAxisLabelFrequency + '"/>';
5107
+ // NOTE: `auto`/`lblAlgn`/`noMultiLvlLbl`/`tickLblSkip` are CT_CatAx-only children and are invalid on the
5108
+ // CT_ValAx that scatter/bubble/bubble3D charts emit for their (numeric) x-axis. Gate them to the catAx/dateAx case.
5109
+ if (!(opts._type === CHART_TYPE.SCATTER || opts._type === CHART_TYPE.BUBBLE || opts._type === CHART_TYPE.BUBBLE3D)) {
5110
+ strXml += ' <c:auto val="1"/>';
5111
+ strXml += ' <c:lblAlgn val="ctr"/>';
5112
+ strXml += ` <c:noMultiLvlLbl val="${opts.catAxisMultiLevelLabels ? 0 : 1}"/>`;
5113
+ if (opts.catAxisLabelFrequency)
5114
+ strXml += ' <c:tickLblSkip val="' + opts.catAxisLabelFrequency + '"/>';
5115
+ }
4893
5116
  // Issue#149: PPT will auto-adjust these as needed after calcing the date bounds, so we only include them when specified by user
4894
5117
  // Allow major and minor units to be set for double value axis charts
4895
5118
  if (opts.catLabelFormatCode || opts._type === CHART_TYPE.SCATTER || opts._type === CHART_TYPE.BUBBLE || opts._type === CHART_TYPE.BUBBLE3D) {
@@ -7177,6 +7400,36 @@ function genXmlTiming(slide) {
7177
7400
  const presetMap = { appear: 1, fadeIn: 10, flyIn: 2, zoomIn: 23 };
7178
7401
  // trigger -> build-step wrapper nodeType
7179
7402
  const wrapNodeTypeMap = { afterPrevious: 'afterEffect', withPrevious: 'withEffect', onClick: 'clickEffect' };
7403
+ // --- Pre-process group/stagger sugar into trigger/delay -------------------
7404
+ // `group` is syntactic sugar over `trigger` (spec: docs/feature-animation-stagger.md).
7405
+ // Walking the animated objects in order: the first object of each consecutive `group`
7406
+ // run becomes the group leader (-> afterPrevious, starting a new build step); the rest
7407
+ // of the run join it (-> withPrevious). When `stagger` is set, the Nth item within the
7408
+ // run (0-indexed) gets `delay = N * stagger`. Objects without a `group` are left untouched
7409
+ // so the explicit `trigger` behaviour stays byte-for-byte identical (backwards-compat).
7410
+ // Entries are shallow-copied before mutation so the caller's `options.animation` is never
7411
+ // modified (keeps repeated `stream()`/`write()` calls deterministic).
7412
+ let prevGroup;
7413
+ let groupIndex = 0;
7414
+ animated.forEach(entry => {
7415
+ const a = entry.anim;
7416
+ if (typeof a.group === 'number') {
7417
+ const isNewGroup = a.group !== prevGroup;
7418
+ if (isNewGroup)
7419
+ groupIndex = 0;
7420
+ else
7421
+ groupIndex++;
7422
+ const resolved = Object.assign(Object.assign({}, a), { trigger: isNewGroup ? 'afterPrevious' : 'withPrevious' });
7423
+ if (typeof a.stagger === 'number')
7424
+ resolved.delay = groupIndex * a.stagger;
7425
+ entry.anim = resolved;
7426
+ prevGroup = a.group;
7427
+ }
7428
+ else {
7429
+ // Ungrouped object forms its own step; reset so a later same-numbered group is a fresh run
7430
+ prevGroup = undefined;
7431
+ }
7432
+ });
7180
7433
  const steps = [];
7181
7434
  animated.forEach(entry => {
7182
7435
  var _a;
@@ -7569,7 +7822,7 @@ function makeXmlViewProps() {
7569
7822
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
7570
7823
  * SOFTWARE.
7571
7824
  */
7572
- const VERSION = '4.1.1';
7825
+ const VERSION = '4.1.4';
7573
7826
  class PptxGenJS {
7574
7827
  set layout(value) {
7575
7828
  const newLayout = this.LAYOUTS[value];
@@ -7922,6 +8175,7 @@ class PptxGenJS {
7922
8175
  addImage: null,
7923
8176
  addMedia: null,
7924
8177
  addNotes: null,
8178
+ addCard: null,
7925
8179
  addShape: null,
7926
8180
  addTable: null,
7927
8181
  addText: null,
@@ -8084,6 +8338,16 @@ class PptxGenJS {
8084
8338
  }
8085
8339
  return newSlide;
8086
8340
  }
8341
+ /**
8342
+ * Compute evenly-spaced grid cell positions within a bounding area.
8343
+ * - Pure layout helper: returns one `{ x, y, w, h }` (inches) per item; emits no slide content.
8344
+ * @param {LayoutGridProps} props - grid options
8345
+ * @returns {LayoutGridResult} array of `{ x, y, w, h }` cells (inches), one per item
8346
+ * @example const grid = pptx.layoutGrid({ items: 6, columns: 3, area: { x: 0.5, y: 2, w: 12, h: 4 }, gap: 0.2 })
8347
+ */
8348
+ layoutGrid(props) {
8349
+ return layoutGrid(props);
8350
+ }
8087
8351
  /**
8088
8352
  * Create a custom Slide Layout in any size
8089
8353
  * @param {PresLayout} layout - layout properties