@seatlayer/core 0.15.1 → 0.16.1

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.
package/dist/index.d.cts CHANGED
@@ -362,6 +362,8 @@ type SeatStatus = 'free' | 'held' | 'booked' | 'not_for_sale';
362
362
  interface RendererCallbacks {
363
363
  onSelect?: (seat: ExpandedSeat) => void;
364
364
  onDeselect?: (seat: ExpandedSeat) => void;
365
+ /** Buyer tried to add a seat after the active selection cap was reached. */
366
+ onSelectionLimit?: (maxSelection: number) => void;
365
367
  /** seat is null when the pointer leaves any seat. */
366
368
  onHover?: (seat: ExpandedSeat | null) => void;
367
369
  /** Keyboard focus moved to a seat (arrow-key navigation) — for screen-reader announcements. */
@@ -461,6 +463,13 @@ interface ISeatmapRenderer {
461
463
  getStatus(seatId: string): SeatStatus;
462
464
  getSelection(): ExpandedSeat[];
463
465
  clearSelection(): void;
466
+ /** Update the buyer selection cap without rebuilding the chart or camera. */
467
+ setMaxSelection?(maxSelection: number): void;
468
+ /**
469
+ * Programmatically restore free seats (for example an Undo action). Added
470
+ * seats respect the active cap and do not reopen a confirmation popover.
471
+ */
472
+ select?(seatIds: string[]): ExpandedSeat[];
464
473
  /**
465
474
  * Dynamically update organizer-only interaction without rebuilding the
466
475
  * renderer. No buyer surface calls this; every behavior remains gated by
@@ -522,6 +531,9 @@ interface ISeatmapRenderer {
522
531
  /** Price-band filter (F4): dim free seats whose category is NOT in `keys`
523
532
  * (null clears). The widget resolves which categories fall in the band. */
524
533
  setCategoryFilter?(keys: string[] | null): void;
534
+ /** Smoothly frame the currently available seats in these categories. `null`
535
+ * returns to the full chart. Used after an explicit buyer price-filter action. */
536
+ focusCategories?(keys: string[] | null): void;
525
537
  /** Dim the seats of these section/zone ids (organizer manager: held-back inventory). */
526
538
  setDimmedSections?(ids: string[] | null): void;
527
539
  /**
@@ -587,7 +599,7 @@ interface ISeatmapRenderer {
587
599
  sectionMembers?(id: string): string[];
588
600
  /**
589
601
  * Smoothly glide (pan+zoom) the camera to frame a section (by id) or a world-
590
- * space bounds rect over ~450ms easeInOutCubic. `prefers-reduced-motion` snaps.
602
+ * space bounds rect over a calm easeInOutCubic glide. `prefers-reduced-motion` snaps.
591
603
  * A pointer-down (grab/pan) cancels an in-flight glide. Slice 5 "glide in".
592
604
  */
593
605
  focusRegion?(target: string | {
@@ -597,6 +609,8 @@ interface ISeatmapRenderer {
597
609
  height: number;
598
610
  }, opts?: {
599
611
  animate?: boolean;
612
+ minScale?: number;
613
+ durationMs?: number;
600
614
  }): void;
601
615
  /** Current LOD rung derived from zoom (for the ZONES/SECTIONS/SEATS pill). */
602
616
  getRung?(): LodRung;
@@ -908,6 +922,8 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
908
922
  getStatus(seatId: string): SeatStatus;
909
923
  getSelection(): ExpandedSeat[];
910
924
  clearSelection(): void;
925
+ setMaxSelection(maxSelection: number): void;
926
+ select(seatIds: string[]): ExpandedSeat[];
911
927
  /** Switch organizer interaction in place so the host preserves camera, LOD,
912
928
  * focus and live status state while moving between Monitor and Block. */
913
929
  setManageInteraction(options: {
@@ -970,6 +986,9 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
970
986
  * widget resolves which categories fall inside the buyer's chosen band.
971
987
  */
972
988
  setCategoryFilter(keys: string[] | null): void;
989
+ /** Frame the currently available inventory that survived a buyer price
990
+ * filter. Clearing the filter glides back to the full venue. */
991
+ focusCategories(keys: string[] | null): void;
973
992
  /**
974
993
  * Switch the projection between flat top-down and the isometric "3D" view
975
994
  * (rotate + y-squash about the chart centre, plus per-elevation lift). Tweens
@@ -1214,7 +1233,7 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
1214
1233
  private zoomToBounds;
1215
1234
  /**
1216
1235
  * Smoothly glide the camera to frame a section (by id) or a world-space bounds
1217
- * rect — the Slice 5 "glide in". Pan+zoom tween over ~450ms easeInOutCubic; the
1236
+ * rect — the Slice 5 "glide in". Pan+zoom tween over a calm easeInOutCubic; the
1218
1237
  * melt/LOD rides the camera every frame. `prefers-reduced-motion` (or
1219
1238
  * `opts.animate === false`) snaps via zoomToBounds. A grab (pointer-down) or a
1220
1239
  * newer glide cancels an in-flight one.
@@ -1227,6 +1246,7 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
1227
1246
  }, opts?: {
1228
1247
  animate?: boolean;
1229
1248
  minScale?: number;
1249
+ durationMs?: number;
1230
1250
  }): void;
1231
1251
  /** Cancel an in-flight camera glide (a grab or a newer glide interrupts it). */
1232
1252
  private cancelGlide;
@@ -1453,7 +1473,7 @@ declare class PickerController {
1453
1473
  private readonly opts;
1454
1474
  private readonly api;
1455
1475
  private readonly key;
1456
- private readonly maxSelection;
1476
+ private maxSelection;
1457
1477
  private renderer;
1458
1478
  private _doc;
1459
1479
  /** Section/zone ids hidden from buyers this event (3.3) — seats vanish, not grey. */
@@ -1514,6 +1534,8 @@ declare class PickerController {
1514
1534
  seatDetails(seatId: string): SeatHoverDetails | null;
1515
1535
  clearSelection(): void;
1516
1536
  deselect(ids: string[]): void;
1537
+ setMaxSelection(maxSelection: number): void;
1538
+ select(ids: string[]): PickerSeat[];
1517
1539
  /**
1518
1540
  * Hold the current selection (or a given label set). The controller does the
1519
1541
  * renderer/hold side (409 → deselect + repaint the taken seats held) and then
@@ -1614,6 +1636,10 @@ declare class PickerController {
1614
1636
  /** Price-band filter (F4): dim free seats whose category is outside `keys`
1615
1637
  * (null clears). The widget resolves which categories fall in the band. */
1616
1638
  setCategoryFilter(keys: string[] | null): void;
1639
+ /** An explicit buyer price-filter action should not only dim the rest of the
1640
+ * chart: clear any older section drill-in and guide the camera to the seats
1641
+ * that remain relevant. Clearing the filter glides back to the full chart. */
1642
+ focusCategoryFilter(keys: string[] | null): void;
1617
1643
  /** World-space rect currently visible + full chart bounds (F3 minimap frame). */
1618
1644
  getViewport(): {
1619
1645
  visible: {
package/dist/index.d.ts CHANGED
@@ -362,6 +362,8 @@ type SeatStatus = 'free' | 'held' | 'booked' | 'not_for_sale';
362
362
  interface RendererCallbacks {
363
363
  onSelect?: (seat: ExpandedSeat) => void;
364
364
  onDeselect?: (seat: ExpandedSeat) => void;
365
+ /** Buyer tried to add a seat after the active selection cap was reached. */
366
+ onSelectionLimit?: (maxSelection: number) => void;
365
367
  /** seat is null when the pointer leaves any seat. */
366
368
  onHover?: (seat: ExpandedSeat | null) => void;
367
369
  /** Keyboard focus moved to a seat (arrow-key navigation) — for screen-reader announcements. */
@@ -461,6 +463,13 @@ interface ISeatmapRenderer {
461
463
  getStatus(seatId: string): SeatStatus;
462
464
  getSelection(): ExpandedSeat[];
463
465
  clearSelection(): void;
466
+ /** Update the buyer selection cap without rebuilding the chart or camera. */
467
+ setMaxSelection?(maxSelection: number): void;
468
+ /**
469
+ * Programmatically restore free seats (for example an Undo action). Added
470
+ * seats respect the active cap and do not reopen a confirmation popover.
471
+ */
472
+ select?(seatIds: string[]): ExpandedSeat[];
464
473
  /**
465
474
  * Dynamically update organizer-only interaction without rebuilding the
466
475
  * renderer. No buyer surface calls this; every behavior remains gated by
@@ -522,6 +531,9 @@ interface ISeatmapRenderer {
522
531
  /** Price-band filter (F4): dim free seats whose category is NOT in `keys`
523
532
  * (null clears). The widget resolves which categories fall in the band. */
524
533
  setCategoryFilter?(keys: string[] | null): void;
534
+ /** Smoothly frame the currently available seats in these categories. `null`
535
+ * returns to the full chart. Used after an explicit buyer price-filter action. */
536
+ focusCategories?(keys: string[] | null): void;
525
537
  /** Dim the seats of these section/zone ids (organizer manager: held-back inventory). */
526
538
  setDimmedSections?(ids: string[] | null): void;
527
539
  /**
@@ -587,7 +599,7 @@ interface ISeatmapRenderer {
587
599
  sectionMembers?(id: string): string[];
588
600
  /**
589
601
  * Smoothly glide (pan+zoom) the camera to frame a section (by id) or a world-
590
- * space bounds rect over ~450ms easeInOutCubic. `prefers-reduced-motion` snaps.
602
+ * space bounds rect over a calm easeInOutCubic glide. `prefers-reduced-motion` snaps.
591
603
  * A pointer-down (grab/pan) cancels an in-flight glide. Slice 5 "glide in".
592
604
  */
593
605
  focusRegion?(target: string | {
@@ -597,6 +609,8 @@ interface ISeatmapRenderer {
597
609
  height: number;
598
610
  }, opts?: {
599
611
  animate?: boolean;
612
+ minScale?: number;
613
+ durationMs?: number;
600
614
  }): void;
601
615
  /** Current LOD rung derived from zoom (for the ZONES/SECTIONS/SEATS pill). */
602
616
  getRung?(): LodRung;
@@ -908,6 +922,8 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
908
922
  getStatus(seatId: string): SeatStatus;
909
923
  getSelection(): ExpandedSeat[];
910
924
  clearSelection(): void;
925
+ setMaxSelection(maxSelection: number): void;
926
+ select(seatIds: string[]): ExpandedSeat[];
911
927
  /** Switch organizer interaction in place so the host preserves camera, LOD,
912
928
  * focus and live status state while moving between Monitor and Block. */
913
929
  setManageInteraction(options: {
@@ -970,6 +986,9 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
970
986
  * widget resolves which categories fall inside the buyer's chosen band.
971
987
  */
972
988
  setCategoryFilter(keys: string[] | null): void;
989
+ /** Frame the currently available inventory that survived a buyer price
990
+ * filter. Clearing the filter glides back to the full venue. */
991
+ focusCategories(keys: string[] | null): void;
973
992
  /**
974
993
  * Switch the projection between flat top-down and the isometric "3D" view
975
994
  * (rotate + y-squash about the chart centre, plus per-elevation lift). Tweens
@@ -1214,7 +1233,7 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
1214
1233
  private zoomToBounds;
1215
1234
  /**
1216
1235
  * Smoothly glide the camera to frame a section (by id) or a world-space bounds
1217
- * rect — the Slice 5 "glide in". Pan+zoom tween over ~450ms easeInOutCubic; the
1236
+ * rect — the Slice 5 "glide in". Pan+zoom tween over a calm easeInOutCubic; the
1218
1237
  * melt/LOD rides the camera every frame. `prefers-reduced-motion` (or
1219
1238
  * `opts.animate === false`) snaps via zoomToBounds. A grab (pointer-down) or a
1220
1239
  * newer glide cancels an in-flight one.
@@ -1227,6 +1246,7 @@ declare class SeatmapRenderer implements ISeatmapRenderer {
1227
1246
  }, opts?: {
1228
1247
  animate?: boolean;
1229
1248
  minScale?: number;
1249
+ durationMs?: number;
1230
1250
  }): void;
1231
1251
  /** Cancel an in-flight camera glide (a grab or a newer glide interrupts it). */
1232
1252
  private cancelGlide;
@@ -1453,7 +1473,7 @@ declare class PickerController {
1453
1473
  private readonly opts;
1454
1474
  private readonly api;
1455
1475
  private readonly key;
1456
- private readonly maxSelection;
1476
+ private maxSelection;
1457
1477
  private renderer;
1458
1478
  private _doc;
1459
1479
  /** Section/zone ids hidden from buyers this event (3.3) — seats vanish, not grey. */
@@ -1514,6 +1534,8 @@ declare class PickerController {
1514
1534
  seatDetails(seatId: string): SeatHoverDetails | null;
1515
1535
  clearSelection(): void;
1516
1536
  deselect(ids: string[]): void;
1537
+ setMaxSelection(maxSelection: number): void;
1538
+ select(ids: string[]): PickerSeat[];
1517
1539
  /**
1518
1540
  * Hold the current selection (or a given label set). The controller does the
1519
1541
  * renderer/hold side (409 → deselect + repaint the taken seats held) and then
@@ -1614,6 +1636,10 @@ declare class PickerController {
1614
1636
  /** Price-band filter (F4): dim free seats whose category is outside `keys`
1615
1637
  * (null clears). The widget resolves which categories fall in the band. */
1616
1638
  setCategoryFilter(keys: string[] | null): void;
1639
+ /** An explicit buyer price-filter action should not only dim the rest of the
1640
+ * chart: clear any older section drill-in and guide the camera to the seats
1641
+ * that remain relevant. Clearing the filter glides back to the full chart. */
1642
+ focusCategoryFilter(keys: string[] | null): void;
1617
1643
  /** World-space rect currently visible + full chart bounds (F3 minimap frame). */
1618
1644
  getViewport(): {
1619
1645
  visible: {
package/dist/index.js CHANGED
@@ -644,6 +644,7 @@ var ISO_ANGLE_DEG = -11.5;
644
644
  var ISO_SQUASH = 0.58;
645
645
  var LIFT_PER_STEP = 58;
646
646
  var ISO_TWEEN_MS = 320;
647
+ var CAMERA_GLIDE_MS = 650;
647
648
  var BLOCK_FILL_ALPHA = 0.85;
648
649
  var SOLD_DARKEN = 0.5;
649
650
  var SECTION_LABEL_PX = 20;
@@ -1270,6 +1271,24 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1270
1271
  for (const id of ids) this.setSelected(id, false, true);
1271
1272
  this.overlayLayer.batchDraw();
1272
1273
  }
1274
+ setMaxSelection(maxSelection) {
1275
+ this.opts.maxSelection = Math.max(0, Math.floor(maxSelection));
1276
+ }
1277
+ select(seatIds) {
1278
+ const added = [];
1279
+ for (const id of seatIds) {
1280
+ if (this.selection.has(id) || !this.isSelectable(id)) continue;
1281
+ if (this.selection.size >= this.opts.maxSelection) {
1282
+ this.opts.onSelectionLimit?.(this.opts.maxSelection);
1283
+ break;
1284
+ }
1285
+ this.setSelected(id, true, true);
1286
+ const seat = this.seatById.get(id);
1287
+ if (seat) added.push(seat);
1288
+ }
1289
+ if (added.length) this.overlayLayer.batchDraw();
1290
+ return added;
1291
+ }
1273
1292
  /** Switch organizer interaction in place so the host preserves camera, LOD,
1274
1293
  * focus and live status state while moving between Monitor and Block. */
1275
1294
  setManageInteraction(options) {
@@ -1598,6 +1617,40 @@ var _SeatmapRenderer = class _SeatmapRenderer {
1598
1617
  this.seatLayer.batchDraw();
1599
1618
  }
1600
1619
  }
1620
+ /** Frame the currently available inventory that survived a buyer price
1621
+ * filter. Clearing the filter glides back to the full venue. */
1622
+ focusCategories(keys) {
1623
+ if (!keys?.length) {
1624
+ this.focusRegion(this.bounds, { durationMs: CAMERA_GLIDE_MS });
1625
+ return;
1626
+ }
1627
+ const wanted = new Set(keys);
1628
+ const matching = this.seats.filter((seat) => {
1629
+ if (!wanted.has(seat.categoryKey) || this.seatInClosedSection(seat.id)) return false;
1630
+ const status = this.statusById.get(seat.id) ?? "free";
1631
+ return status === "free" || this.ownedHold.has(seat.id);
1632
+ });
1633
+ if (!matching.length) return;
1634
+ let minX = Infinity;
1635
+ let maxX = -Infinity;
1636
+ let minY = Infinity;
1637
+ let maxY = -Infinity;
1638
+ for (const seat of matching) {
1639
+ minX = Math.min(minX, seat.x);
1640
+ maxX = Math.max(maxX, seat.x);
1641
+ minY = Math.min(minY, seat.y);
1642
+ maxY = Math.max(maxY, seat.y);
1643
+ }
1644
+ const pad = Math.max(24, this.seatR * 3);
1645
+ minX -= pad;
1646
+ maxX += pad;
1647
+ minY -= pad;
1648
+ maxY += pad;
1649
+ this.focusRegion(
1650
+ { x: minX, y: minY, width: Math.max(40, maxX - minX), height: Math.max(40, maxY - minY) },
1651
+ { durationMs: CAMERA_GLIDE_MS }
1652
+ );
1653
+ }
1601
1654
  // ---- isometric ("3D") view mode -------------------------------------------
1602
1655
  /**
1603
1656
  * Switch the projection between flat top-down and the isometric "3D" view
@@ -2786,7 +2839,10 @@ var _SeatmapRenderer = class _SeatmapRenderer {
2786
2839
  if (seat) this.opts.onDeselect?.(seat);
2787
2840
  } else {
2788
2841
  if (!this.isSelectable(id)) return;
2789
- if (this.selection.size >= this.opts.maxSelection) return;
2842
+ if (this.selection.size >= this.opts.maxSelection) {
2843
+ this.opts.onSelectionLimit?.(this.opts.maxSelection);
2844
+ return;
2845
+ }
2790
2846
  this.setSelected(id, true);
2791
2847
  const seat = this.seatById.get(id);
2792
2848
  if (seat) this.opts.onSelect?.(seat);
@@ -3124,7 +3180,7 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3124
3180
  }
3125
3181
  /**
3126
3182
  * Smoothly glide the camera to frame a section (by id) or a world-space bounds
3127
- * rect — the Slice 5 "glide in". Pan+zoom tween over ~450ms easeInOutCubic; the
3183
+ * rect — the Slice 5 "glide in". Pan+zoom tween over a calm easeInOutCubic; the
3128
3184
  * melt/LOD rides the camera every frame. `prefers-reduced-motion` (or
3129
3185
  * `opts.animate === false`) snaps via zoomToBounds. A grab (pointer-down) or a
3130
3186
  * newer glide cancels an in-flight one.
@@ -3156,13 +3212,13 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3156
3212
  return;
3157
3213
  }
3158
3214
  const start = performance.now();
3159
- const DUR = 450;
3215
+ const duration = Math.max(180, Math.min(1200, opts?.durationMs ?? CAMERA_GLIDE_MS));
3160
3216
  const step = (now) => {
3161
3217
  if (this.destroyed) {
3162
3218
  this.glideRaf = 0;
3163
3219
  return;
3164
3220
  }
3165
- const raw = Math.min(1, (now - start) / DUR);
3221
+ const raw = Math.min(1, (now - start) / duration);
3166
3222
  const e = raw < 0.5 ? 4 * raw * raw * raw : 1 - Math.pow(-2 * raw + 2, 3) / 2;
3167
3223
  const sc = fromScale + (toScale - fromScale) * e;
3168
3224
  this.stage.scale({ x: sc, y: sc });
@@ -3287,15 +3343,49 @@ var _SeatmapRenderer = class _SeatmapRenderer {
3287
3343
  if (seat.kind === "booth") continue;
3288
3344
  if (seat.x < x0 || seat.x > x1 || seat.y < y0 || seat.y > y1) continue;
3289
3345
  const status = this.statusById.get(seat.id) ?? "free";
3290
- const statusCue = status === "booked" ? "\xD7" : status === "held" && !this.ownedHold.has(seat.id) ? "H" : null;
3346
+ const unavailable = status === "booked" || status === "held" && !this.ownedHold.has(seat.id);
3347
+ if (unavailable) {
3348
+ const cue = new Group({ x: seat.x, y: seat.y, listening: false });
3349
+ if (status === "held") {
3350
+ cue.add(new Rect({
3351
+ x: -4.2,
3352
+ y: -0.7,
3353
+ width: 8.4,
3354
+ height: 6.5,
3355
+ cornerRadius: 1.3,
3356
+ stroke: "#ffffff",
3357
+ strokeWidth: 1.25,
3358
+ listening: false
3359
+ }));
3360
+ cue.add(new Line({
3361
+ points: [-2.4, -0.8, -2.4, -2.7, -1.2, -4, 0, -4.35, 1.2, -4, 2.4, -2.7, 2.4, -0.8],
3362
+ stroke: "#ffffff",
3363
+ strokeWidth: 1.2,
3364
+ lineCap: "round",
3365
+ lineJoin: "round",
3366
+ listening: false
3367
+ }));
3368
+ } else {
3369
+ cue.add(new Line({
3370
+ points: [-4.2, 4.2, 4.2, -4.2],
3371
+ stroke: "#ffffff",
3372
+ strokeWidth: 1.8,
3373
+ lineCap: "round",
3374
+ listening: false
3375
+ }));
3376
+ }
3377
+ this.labelGroup.add(cue);
3378
+ if (++count >= MAX_LABELS) break;
3379
+ continue;
3380
+ }
3291
3381
  const t2 = new Text({
3292
3382
  x: seat.x,
3293
3383
  y: seat.y,
3294
- text: statusCue ?? seat.label,
3295
- fontSize: statusCue ? status === "booked" ? 13 : 8 : 7,
3296
- fontStyle: statusCue ? "800" : "600",
3384
+ text: seat.label,
3385
+ fontSize: 7,
3386
+ fontStyle: "600",
3297
3387
  fontFamily: this.labelFont(),
3298
- fill: statusCue ? "#ffffff" : this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
3388
+ fill: this.theme.seatLabelColor ?? DEF_SEAT_LABEL,
3299
3389
  listening: false,
3300
3390
  perfectDrawEnabled: false
3301
3391
  });
@@ -3519,6 +3609,7 @@ var PickerController = class {
3519
3609
  this.opts.onDeselect?.(seat);
3520
3610
  this.emitSelectionChange();
3521
3611
  },
3612
+ onSelectionLimit: this.opts.onSelectionLimit,
3522
3613
  onHover: (seat) => {
3523
3614
  this.opts.onHover?.(seat);
3524
3615
  if (this.opts.onSeatHover) this.opts.onSeatHover(seat ? this.describeSeat(seat) : null);
@@ -3586,6 +3677,15 @@ var PickerController = class {
3586
3677
  this.renderer?.deselect(ids);
3587
3678
  this.emitSelectionChange();
3588
3679
  }
3680
+ setMaxSelection(maxSelection) {
3681
+ this.maxSelection = Math.max(0, Math.floor(maxSelection));
3682
+ this.renderer?.setMaxSelection?.(this.maxSelection);
3683
+ }
3684
+ select(ids) {
3685
+ const added = this.renderer?.select?.(ids) ?? [];
3686
+ if (added.length) this.emitSelectionChange();
3687
+ return added.map((seat) => this.toSeat(seat));
3688
+ }
3589
3689
  // ---- booking machine ------------------------------------------------------
3590
3690
  /**
3591
3691
  * Hold the current selection (or a given label set). The controller does the
@@ -3952,6 +4052,17 @@ var PickerController = class {
3952
4052
  setCategoryFilter(keys) {
3953
4053
  this.renderer?.setCategoryFilter?.(keys);
3954
4054
  }
4055
+ /** An explicit buyer price-filter action should not only dim the rest of the
4056
+ * chart: clear any older section drill-in and guide the camera to the seats
4057
+ * that remain relevant. Clearing the filter glides back to the full chart. */
4058
+ focusCategoryFilter(keys) {
4059
+ const renderer = this.renderer;
4060
+ if (!renderer) return;
4061
+ renderer.clearSectionFocus?.();
4062
+ this.opts.onSectionFocus?.(null);
4063
+ if (renderer.focusCategories) renderer.focusCategories(keys);
4064
+ else renderer.zoomToFit();
4065
+ }
3955
4066
  /** World-space rect currently visible + full chart bounds (F3 minimap frame). */
3956
4067
  getViewport() {
3957
4068
  const r = this.renderer;