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