@canvas-components/pivot-table 0.1.8 → 0.2.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.cjs CHANGED
@@ -749,14 +749,11 @@ function matchesSearchKeyword(value, filter) {
749
749
  return filter.searchExact ? text === keyword : text.includes(keyword);
750
750
  }
751
751
  function matchesFilterConditions(value, filter) {
752
- const first = filter.condition;
753
- const second = filter.secondCondition;
754
- const firstActive = first && first.operator !== "none";
755
- const secondActive = second && second.operator !== "none";
756
- if (!firstActive && !secondActive) return true;
757
- if (!firstActive) return evaluateCondition(value, second);
758
- if (!secondActive) return evaluateCondition(value, first);
759
- return filter.conditionRelation === "or" ? evaluateCondition(value, first) || evaluateCondition(value, second) : evaluateCondition(value, first) && evaluateCondition(value, second);
752
+ const conditions = (filter.conditions?.length ? filter.conditions : [filter.condition, filter.secondCondition]).filter(
753
+ (condition) => condition != null && condition.operator !== "none"
754
+ );
755
+ if (conditions.length === 0) return true;
756
+ return filter.conditionRelation === "or" ? conditions.some((condition) => evaluateCondition(value, condition)) : conditions.every((condition) => evaluateCondition(value, condition));
760
757
  }
761
758
  function evaluateCondition(value, condition) {
762
759
  const text = value == null ? "" : String(value);
@@ -995,7 +992,7 @@ function computePivotScrollbarLayout(params) {
995
992
  }
996
993
  if (hasVertical) {
997
994
  const fullHeight = Math.max(
998
- params.bodyHeight - VERTICAL_TOP_INSET - (hasHorizontal ? SCROLLBAR_THICKNESS : 0),
995
+ params.bodyHeight - VERTICAL_TOP_INSET,
999
996
  0
1000
997
  );
1001
998
  const buttonSize = Math.min(SCROLLBAR_THICKNESS, fullHeight / 3);
@@ -1051,6 +1048,7 @@ var DEFAULT_ROW_HEIGHT = 40;
1051
1048
  var DEFAULT_COLUMN_WIDTH = 120;
1052
1049
  var DEFAULT_HEADER_SIZE = 40;
1053
1050
  var DEFAULT_ROW_DIMENSION_WIDTH2 = 140;
1051
+ var SCROLLBAR_THICKNESS2 = 10;
1054
1052
  function buildPivotLayoutSnapshot(params) {
1055
1053
  const { model, options, visibleRowNodes, visibleColumnNodes, width, height } = params;
1056
1054
  const indicatorLayout = options.indicatorLayout ?? "columns";
@@ -1083,8 +1081,8 @@ function buildPivotLayoutSnapshot(params) {
1083
1081
  const columnHeaderHeight = columnHeaderLevels * DEFAULT_HEADER_SIZE;
1084
1082
  const reportFilterHeight = (options.filterDimensions?.length ?? 0) * rowHeight;
1085
1083
  const tableHeaderHeight = reportFilterHeight + columnHeaderHeight;
1086
- const bodyWidth = Math.max(width - rowHeaderWidth, 0);
1087
- const availableBodyHeight = Math.max(height - tableHeaderHeight, 0);
1084
+ const rawBodyWidth = Math.max(width - rowHeaderWidth, 0);
1085
+ const rawAvailableBodyHeight = Math.max(height - tableHeaderHeight, 0);
1088
1086
  const rowSlots = createPivotAxisSlots({
1089
1087
  nodes: visibleRowNodes,
1090
1088
  indicators: options.indicators.map((indicator) => indicator.key),
@@ -1107,19 +1105,52 @@ function buildPivotLayoutSnapshot(params) {
1107
1105
  const columnOffsets = createOffsets(columnWidths);
1108
1106
  const totalBodyWidth = columnOffsets[columnOffsets.length - 1] ?? 0;
1109
1107
  const totalBodyHeight = rowSlots.length * rowHeight;
1108
+ let hasHorizontalScrollbar = totalBodyWidth > rawBodyWidth;
1109
+ let hasVerticalScrollbar = totalBodyHeight > rawAvailableBodyHeight;
1110
+ for (let index = 0; index < 2; index += 1) {
1111
+ hasHorizontalScrollbar = totalBodyWidth > Math.max(
1112
+ rawBodyWidth - (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1113
+ 0
1114
+ );
1115
+ hasVerticalScrollbar = totalBodyHeight > Math.max(
1116
+ rawAvailableBodyHeight - (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1117
+ 0
1118
+ );
1119
+ }
1120
+ const bodyWidth = Math.max(
1121
+ rawBodyWidth - (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1122
+ 0
1123
+ );
1124
+ const availableBodyHeight = Math.max(
1125
+ rawAvailableBodyHeight - (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1126
+ 0
1127
+ );
1128
+ const frozenBottomRowRange = resolveFrozenBottomRowRange(rowSlots);
1129
+ const frozenBottomRowCount = frozenBottomRowRange ? frozenBottomRowRange.end - frozenBottomRowRange.start : 0;
1130
+ const frozenBottomHeight = frozenBottomRowCount * rowHeight;
1131
+ const scrollableRowCount = frozenBottomRowRange?.start ?? rowSlots.length;
1132
+ const totalScrollableBodyHeight = scrollableRowCount * rowHeight;
1110
1133
  const tableWidth = rowHeaderWidth + Math.min(bodyWidth, totalBodyWidth);
1111
1134
  const bodyHeight = Math.min(availableBodyHeight, totalBodyHeight);
1112
- const fittedHeight = tableHeaderHeight + bodyHeight;
1135
+ const visibleFrozenBottomHeight = Math.min(frozenBottomHeight, bodyHeight);
1136
+ const scrollableBodyHeight = Math.max(
1137
+ bodyHeight - visibleFrozenBottomHeight,
1138
+ 0
1139
+ );
1140
+ const fittedHeight = tableHeaderHeight + bodyHeight + (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0);
1113
1141
  const maxScrollLeft = Math.max(totalBodyWidth - bodyWidth, 0);
1114
- const maxScrollTop = Math.max(totalBodyHeight - bodyHeight, 0);
1142
+ const maxScrollTop = Math.max(
1143
+ totalScrollableBodyHeight - scrollableBodyHeight,
1144
+ 0
1145
+ );
1115
1146
  const scroll = {
1116
1147
  left: utils.clamp(params.scroll.left, 0, maxScrollLeft),
1117
1148
  top: utils.clamp(params.scroll.top, 0, maxScrollTop)
1118
1149
  };
1119
1150
  const visibleRowRange = resolveVisibleRange({
1120
- count: rowSlots.length,
1151
+ count: scrollableRowCount,
1121
1152
  itemSize: rowHeight,
1122
- viewportSize: bodyHeight,
1153
+ viewportSize: scrollableBodyHeight,
1123
1154
  offset: scroll.top,
1124
1155
  overscan: options.overscanRowCount ?? 1
1125
1156
  });
@@ -1154,6 +1185,23 @@ function buildPivotLayoutSnapshot(params) {
1154
1185
  dimensionCount: options.rows.length,
1155
1186
  indicators: options.indicators
1156
1187
  });
1188
+ if (frozenBottomRowRange) {
1189
+ rowHeaderCells.push(
1190
+ ...buildRowHeaderCells({
1191
+ model,
1192
+ options,
1193
+ slots: rowSlots,
1194
+ range: frozenBottomRowRange,
1195
+ rowHeight,
1196
+ rowDimensionWidths,
1197
+ columnHeaderHeight: tableHeaderHeight,
1198
+ scrollTop: frozenBottomRowRange.start * rowHeight - scrollableBodyHeight,
1199
+ expandedNodeIds: params.expandedRowNodeIds,
1200
+ dimensionCount: options.rows.length,
1201
+ indicators: options.indicators
1202
+ })
1203
+ );
1204
+ }
1157
1205
  const columnHeaderCells = buildColumnHeaderCells({
1158
1206
  model,
1159
1207
  options,
@@ -1180,14 +1228,34 @@ function buildPivotLayoutSnapshot(params) {
1180
1228
  columnHeaderHeight: tableHeaderHeight,
1181
1229
  scroll
1182
1230
  });
1231
+ if (frozenBottomRowRange) {
1232
+ bodyCells.push(
1233
+ ...buildBodyCells({
1234
+ model,
1235
+ rowSlots,
1236
+ columnSlots,
1237
+ rowRange: frozenBottomRowRange,
1238
+ columnRange: visibleColumnRange,
1239
+ rowHeight,
1240
+ columnWidths,
1241
+ columnOffsets,
1242
+ rowHeaderWidth,
1243
+ columnHeaderHeight: tableHeaderHeight,
1244
+ scroll: {
1245
+ left: scroll.left,
1246
+ top: frozenBottomRowRange.start * rowHeight - scrollableBodyHeight
1247
+ }
1248
+ })
1249
+ );
1250
+ }
1183
1251
  const scrollbar = computePivotScrollbarLayout({
1184
- width,
1252
+ width: tableWidth + (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1185
1253
  height: fittedHeight,
1186
1254
  bodyTop: tableHeaderHeight,
1187
- bodyHeight,
1255
+ bodyHeight: scrollableBodyHeight,
1188
1256
  bodyWidth,
1189
1257
  totalBodyWidth,
1190
- totalBodyHeight,
1258
+ totalBodyHeight: totalScrollableBodyHeight,
1191
1259
  scrollLeft: scroll.left,
1192
1260
  scrollTop: scroll.top,
1193
1261
  maxScrollLeft,
@@ -1239,6 +1307,7 @@ function buildPivotLayoutSnapshot(params) {
1239
1307
  rowSlots,
1240
1308
  columnSlots,
1241
1309
  visibleRowRange,
1310
+ frozenBottomRowRange,
1242
1311
  visibleColumnRange,
1243
1312
  reportFilterCells,
1244
1313
  rowFieldHeaderCells,
@@ -1373,6 +1442,13 @@ function createAxisSlot(node, indicatorKey) {
1373
1442
  kind: node?.kind ?? "empty"
1374
1443
  };
1375
1444
  }
1445
+ function resolveFrozenBottomRowRange(slots) {
1446
+ let start = slots.length;
1447
+ while (start > 0 && slots[start - 1]?.kind === "grandTotal") {
1448
+ start -= 1;
1449
+ }
1450
+ return start < slots.length ? { start, end: slots.length } : null;
1451
+ }
1376
1452
  function buildRowHeaderCells(params) {
1377
1453
  const cells = [];
1378
1454
  for (let rowIndex = params.range.start; rowIndex < params.range.end; rowIndex += 1) {
@@ -2573,6 +2649,7 @@ function createPivotDomHost(container) {
2573
2649
  right: "0",
2574
2650
  height: "100%",
2575
2651
  overflow: "hidden",
2652
+ borderRadius: "8px",
2576
2653
  touchAction: "none"
2577
2654
  });
2578
2655
  Object.assign(canvas.style, {
@@ -2635,8 +2712,8 @@ function resolvePivotSelectionRect(layout, selection) {
2635
2712
  const kind = resolvePivotSelectionKind(selection);
2636
2713
  const startX = kind === "row" || kind === "row-cell" ? layout.rowHeaderRect.x : layout.bodyRect.x + (layout.columnOffsets[indexes.startColumnIndex] ?? 0) - layout.scroll.left;
2637
2714
  const endX = layout.bodyRect.x + (layout.columnOffsets[indexes.endColumnIndex] ?? 0) + (layout.columnWidths[indexes.endColumnIndex] ?? layout.columnWidth) - layout.scroll.left;
2638
- const startY = kind === "column" ? layout.columnHeaderRect.y : layout.bodyRect.y + indexes.startRowIndex * layout.rowHeight - layout.scroll.top;
2639
- const endY = layout.bodyRect.y + (indexes.endRowIndex + 1) * layout.rowHeight - layout.scroll.top;
2715
+ const startY = kind === "column" ? layout.columnHeaderRect.y : resolvePivotRowTop(layout, indexes.startRowIndex);
2716
+ const endY = resolvePivotRowTop(layout, indexes.endRowIndex) + layout.rowHeight;
2640
2717
  const clipRect = resolvePivotSelectionClipRect(layout, selection);
2641
2718
  const visibleStartX = Math.max(startX, clipRect.x);
2642
2719
  const visibleEndX = Math.min(endX, clipRect.x + clipRect.width);
@@ -2650,6 +2727,13 @@ function resolvePivotSelectionRect(layout, selection) {
2650
2727
  height: visibleEndY - visibleStartY
2651
2728
  };
2652
2729
  }
2730
+ function resolvePivotRowTop(layout, rowIndex) {
2731
+ const frozenRange = layout.frozenBottomRowRange;
2732
+ if (frozenRange && rowIndex >= frozenRange.start && rowIndex < frozenRange.end) {
2733
+ return layout.bodyRect.y + layout.bodyRect.height - (frozenRange.end - rowIndex) * layout.rowHeight;
2734
+ }
2735
+ return layout.bodyRect.y + rowIndex * layout.rowHeight - layout.scroll.top;
2736
+ }
2653
2737
  function resolvePivotSelectionClipRect(layout, selection) {
2654
2738
  const kind = resolvePivotSelectionKind(selection);
2655
2739
  if (kind === "row" || kind === "row-cell") {
@@ -2794,26 +2878,127 @@ var defaultPivotTableTheme = {
2794
2878
  };
2795
2879
 
2796
2880
  // src/rendering/draw-scrollbars.ts
2797
- function drawPivotScrollbars(context, layout) {
2881
+ function drawPivotScrollbars(context, layout, borderColor, verticalFrame) {
2798
2882
  context.save();
2883
+ if (layout.horizontal && verticalFrame) {
2884
+ drawHorizontalScrollbarMask(
2885
+ context,
2886
+ layout.horizontal,
2887
+ verticalFrame.bodyBackgroundColor
2888
+ );
2889
+ }
2890
+ if (layout.vertical && verticalFrame) {
2891
+ drawVerticalScrollbarFrame(
2892
+ context,
2893
+ layout.vertical,
2894
+ verticalFrame,
2895
+ borderColor
2896
+ );
2897
+ }
2799
2898
  if (layout.horizontal) {
2800
- drawAxis(context, layout.horizontal, "horizontal");
2899
+ drawAxis(context, layout.horizontal, "horizontal", borderColor);
2801
2900
  }
2802
2901
  if (layout.vertical) {
2803
- drawAxis(context, layout.vertical, "vertical");
2902
+ drawAxis(context, layout.vertical, "vertical", borderColor);
2903
+ }
2904
+ if (layout.horizontal) {
2905
+ drawHorizontalScrollbarTopBorder(
2906
+ context,
2907
+ layout.horizontal,
2908
+ layout.vertical,
2909
+ borderColor
2910
+ );
2804
2911
  }
2805
2912
  context.restore();
2806
2913
  }
2807
- function drawAxis(context, layout, axis) {
2808
- drawTrack(context, layout.track);
2914
+ function drawHorizontalScrollbarMask(context, layout, backgroundColor) {
2915
+ const left = layout.decreaseButton.x;
2916
+ const right = layout.increaseButton.x + layout.increaseButton.width;
2917
+ const top = layout.decreaseButton.y;
2918
+ const height = layout.decreaseButton.height;
2919
+ if (right <= left || height <= 0) return;
2920
+ context.save();
2921
+ context.fillStyle = backgroundColor;
2922
+ context.fillRect(left, top, right - left, height);
2923
+ context.restore();
2924
+ }
2925
+ function drawHorizontalScrollbarTopBorder(context, horizontal, vertical, borderColor) {
2926
+ const left = Math.round(horizontal.decreaseButton.x) + 0.5;
2927
+ const horizontalRight = horizontal.increaseButton.x + horizontal.increaseButton.width;
2928
+ const verticalRight = vertical ? vertical.decreaseButton.x + vertical.decreaseButton.width : horizontalRight;
2929
+ const right = Math.round(Math.max(horizontalRight, verticalRight)) - 0.5;
2930
+ const top = Math.round(horizontal.decreaseButton.y) + 0.5;
2931
+ if (right <= left) return;
2932
+ context.save();
2933
+ context.strokeStyle = borderColor;
2934
+ context.lineWidth = 1;
2935
+ context.beginPath();
2936
+ context.moveTo(left, top);
2937
+ context.lineTo(right, top);
2938
+ context.stroke();
2939
+ context.restore();
2940
+ }
2941
+ function drawAxis(context, layout, axis, borderColor) {
2809
2942
  drawButton(context, layout.decreaseButton, axis, "decrease");
2810
2943
  drawButton(context, layout.increaseButton, axis, "increase");
2811
2944
  drawThumb(context, layout.thumb);
2945
+ drawTrackBorder(context, layout.track, axis, borderColor);
2946
+ }
2947
+ function drawTrackBorder(context, rect, axis, borderColor) {
2948
+ const left = Math.round(rect.x) + 0.5;
2949
+ const right = Math.round(rect.x + rect.width) - 0.5;
2950
+ const top = Math.round(rect.y) + 0.5;
2951
+ const bottom = Math.round(rect.y + rect.height) - 0.5;
2952
+ if (right <= left || bottom <= top) return;
2953
+ context.save();
2954
+ context.strokeStyle = borderColor;
2955
+ context.lineWidth = 1;
2956
+ context.beginPath();
2957
+ if (axis === "horizontal") {
2958
+ context.moveTo(left, top);
2959
+ context.lineTo(right, top);
2960
+ } else {
2961
+ context.moveTo(left, top);
2962
+ context.lineTo(left, bottom);
2963
+ }
2964
+ context.stroke();
2965
+ context.restore();
2812
2966
  }
2813
- function drawTrack(context, rect) {
2814
- context.fillStyle = "rgba(148, 163, 184, 0.16)";
2815
- roundRect(context, rect, rect.height / 2);
2816
- context.fill();
2967
+ function drawVerticalScrollbarFrame(context, layout, frame, borderColor) {
2968
+ const left = Math.round(layout.decreaseButton.x) + 0.5;
2969
+ const top = Math.round(frame.top) + 0.5;
2970
+ const bottom = Math.round(frame.top + frame.height) - 0.5;
2971
+ const headerBottom = Math.round(frame.headerBottom) + 0.5;
2972
+ if (bottom <= top) return;
2973
+ context.save();
2974
+ context.fillStyle = frame.headerBackgroundColor;
2975
+ context.fillRect(
2976
+ layout.decreaseButton.x,
2977
+ frame.top,
2978
+ layout.decreaseButton.width,
2979
+ Math.max(frame.headerBottom - frame.top, 0)
2980
+ );
2981
+ context.fillStyle = frame.bodyBackgroundColor;
2982
+ context.fillRect(
2983
+ layout.decreaseButton.x,
2984
+ frame.headerBottom,
2985
+ layout.decreaseButton.width,
2986
+ Math.max(frame.top + frame.height - frame.headerBottom, 0)
2987
+ );
2988
+ context.strokeStyle = borderColor;
2989
+ context.lineWidth = 1;
2990
+ context.beginPath();
2991
+ context.moveTo(left, top);
2992
+ context.lineTo(left, bottom);
2993
+ context.stroke();
2994
+ if (headerBottom > top && headerBottom < bottom) {
2995
+ const right = Math.round(layout.decreaseButton.x + layout.decreaseButton.width) - 0.5;
2996
+ context.beginPath();
2997
+ context.moveTo(left, headerBottom);
2998
+ context.lineTo(right, headerBottom);
2999
+ context.stroke();
3000
+ }
3001
+ context.restore();
2817
3002
  }
2818
3003
  function drawThumb(context, rect) {
2819
3004
  context.fillStyle = "rgba(100, 116, 139, 0.8)";
@@ -2821,28 +3006,25 @@ function drawThumb(context, rect) {
2821
3006
  context.fill();
2822
3007
  }
2823
3008
  function drawButton(context, rect, axis, direction) {
2824
- context.fillStyle = "rgba(226, 232, 240, 0.92)";
2825
- context.strokeStyle = "rgba(148, 163, 184, 0.65)";
2826
- context.lineWidth = 1;
2827
- roundRect(context, rect, rect.height / 2);
2828
- context.fill();
2829
- context.stroke();
2830
3009
  context.fillStyle = "rgba(71, 85, 105, 0.88)";
2831
3010
  context.beginPath();
2832
3011
  const centerX = rect.x + rect.width / 2;
2833
3012
  const centerY = rect.y + rect.height / 2;
3013
+ const iconSize = Math.min(rect.width, rect.height) * 0.64;
3014
+ const directionOffset = iconSize * 0.4;
3015
+ const crossAxisOffset = iconSize / 2;
2834
3016
  if (axis === "horizontal") {
2835
- const tipX = centerX + (direction === "decrease" ? -1 : 1) * rect.width * 0.14;
2836
- const tailX = centerX + (direction === "decrease" ? 1 : -1) * rect.width * 0.14;
3017
+ const tipX = centerX + (direction === "decrease" ? -1 : 1) * directionOffset;
3018
+ const tailX = centerX + (direction === "decrease" ? 1 : -1) * directionOffset;
2837
3019
  context.moveTo(tipX, centerY);
2838
- context.lineTo(tailX, centerY - rect.height * 0.22);
2839
- context.lineTo(tailX, centerY + rect.height * 0.22);
3020
+ context.lineTo(tailX, centerY - crossAxisOffset);
3021
+ context.lineTo(tailX, centerY + crossAxisOffset);
2840
3022
  } else {
2841
- const tipY = centerY + (direction === "decrease" ? -1 : 1) * rect.height * 0.14;
2842
- const tailY = centerY + (direction === "decrease" ? 1 : -1) * rect.height * 0.14;
3023
+ const tipY = centerY + (direction === "decrease" ? -1 : 1) * directionOffset;
3024
+ const tailY = centerY + (direction === "decrease" ? 1 : -1) * directionOffset;
2843
3025
  context.moveTo(centerX, tipY);
2844
- context.lineTo(centerX - rect.width * 0.22, tailY);
2845
- context.lineTo(centerX + rect.width * 0.22, tailY);
3026
+ context.lineTo(centerX - crossAxisOffset, tailY);
3027
+ context.lineTo(centerX + crossAxisOffset, tailY);
2846
3028
  }
2847
3029
  context.closePath();
2848
3030
  context.fill();
@@ -2895,25 +3077,22 @@ function drawPivotGridRect(context, rect, color, options) {
2895
3077
  context.stroke();
2896
3078
  context.restore();
2897
3079
  }
2898
- function drawPivotViewportRightBorder(context, rect, color) {
2899
- const x = rect.x + rect.width - 0.5;
2900
- context.save();
2901
- context.strokeStyle = color;
2902
- context.lineWidth = 1;
2903
- context.beginPath();
2904
- context.moveTo(x, rect.y);
2905
- context.lineTo(x, rect.y + rect.height);
2906
- context.stroke();
2907
- context.restore();
3080
+ function clipPivotViewport(context, rect, radius = 8) {
3081
+ appendRoundedRectPath(context, rect, radius);
3082
+ context.clip();
2908
3083
  }
2909
- function drawPivotViewportBottomBorder(context, rect, color) {
2910
- const y = rect.y + rect.height - 0.5;
3084
+ function drawPivotViewportBorder(context, rect, color, radius = 8) {
3085
+ const borderRect = {
3086
+ x: rect.x + 0.5,
3087
+ y: rect.y + 0.5,
3088
+ width: Math.max(rect.width - 1, 0),
3089
+ height: Math.max(rect.height - 1, 0)
3090
+ };
3091
+ if (borderRect.width <= 0 || borderRect.height <= 0) return;
2911
3092
  context.save();
2912
3093
  context.strokeStyle = color;
2913
3094
  context.lineWidth = 1;
2914
- context.beginPath();
2915
- context.moveTo(rect.x, y);
2916
- context.lineTo(rect.x + rect.width, y);
3095
+ appendRoundedRectPath(context, borderRect, radius);
2917
3096
  context.stroke();
2918
3097
  context.restore();
2919
3098
  }
@@ -2931,6 +3110,21 @@ function drawPivotHeaderBoundaries(context, options, color) {
2931
3110
  context.stroke();
2932
3111
  context.restore();
2933
3112
  }
3113
+ function appendRoundedRectPath(context, rect, radius) {
3114
+ const nextRadius = Math.max(
3115
+ Math.min(radius, rect.width / 2, rect.height / 2),
3116
+ 0
3117
+ );
3118
+ const right = rect.x + rect.width;
3119
+ const bottom = rect.y + rect.height;
3120
+ context.beginPath();
3121
+ context.moveTo(rect.x + nextRadius, rect.y);
3122
+ context.arcTo(right, rect.y, right, bottom, nextRadius);
3123
+ context.arcTo(right, bottom, rect.x, bottom, nextRadius);
3124
+ context.arcTo(rect.x, bottom, rect.x, rect.y, nextRadius);
3125
+ context.arcTo(rect.x, rect.y, right, rect.y, nextRadius);
3126
+ context.closePath();
3127
+ }
2934
3128
 
2935
3129
  // src/domain/layout/sticky-row-header.ts
2936
3130
  function resolveStickyRowHeaderContentRect(params) {
@@ -2967,7 +3161,15 @@ function renderPivotTable(params) {
2967
3161
  activeNodePathKey
2968
3162
  );
2969
3163
  const areaSelection = isPivotAreaSelection(layout, params.selection);
3164
+ const viewportWidth = resolvePivotViewportWidth(layout);
2970
3165
  context.clearRect(0, 0, layout.width, layout.height);
3166
+ context.save();
3167
+ clipPivotViewport(context, {
3168
+ x: 0,
3169
+ y: 0,
3170
+ width: viewportWidth,
3171
+ height: layout.height
3172
+ });
2971
3173
  context.font = `${theme.fontSize}px ${theme.fontFamily}`;
2972
3174
  context.textBaseline = "middle";
2973
3175
  withClip(context, layout.reportFilterRect, () => {
@@ -3081,17 +3283,13 @@ function renderPivotTable(params) {
3081
3283
  theme.borderColor
3082
3284
  );
3083
3285
  if (options.debug) drawDebugOverlay(context, layout, theme);
3084
- drawPivotScrollbars(context, layout.scrollbar);
3085
- drawPivotViewportRightBorder(
3086
- context,
3087
- { x: 0, y: 0, width: layout.tableWidth, height: contentHeight },
3088
- theme.borderColor
3089
- );
3090
- drawPivotViewportBottomBorder(
3091
- context,
3092
- { x: 0, y: 0, width: layout.tableWidth, height: contentHeight },
3093
- theme.borderColor
3094
- );
3286
+ drawPivotScrollbars(context, layout.scrollbar, theme.borderColor, {
3287
+ top: 0,
3288
+ height: contentHeight,
3289
+ headerBottom: layout.bodyRect.y,
3290
+ headerBackgroundColor: theme.columnHeaderBackgroundColor,
3291
+ bodyBackgroundColor: theme.backgroundColor
3292
+ });
3095
3293
  const selectionRect = resolvePivotSelectionRect(layout, params.selection);
3096
3294
  if (selectionRect) {
3097
3295
  const selectionClipRect = resolvePivotSelectionClipRect(
@@ -3109,11 +3307,22 @@ function renderPivotTable(params) {
3109
3307
  if (params.resizeGuideX != null) {
3110
3308
  drawPivotResizeGuide(context, params.resizeGuideX, contentHeight);
3111
3309
  }
3310
+ context.restore();
3311
+ drawPivotViewportBorder(
3312
+ context,
3313
+ { x: 0, y: 0, width: viewportWidth, height: contentHeight },
3314
+ theme.borderColor
3315
+ );
3112
3316
  return {
3113
3317
  headerCellCount: layout.reportFilterCells.length + layout.rowFieldHeaderCells.length + layout.rowHeaderCells.length + layout.columnHeaderCells.length,
3114
3318
  bodyCellCount: layout.bodyCells.length
3115
3319
  };
3116
3320
  }
3321
+ function resolvePivotViewportWidth(layout) {
3322
+ const horizontalRight = layout.scrollbar.horizontal ? layout.scrollbar.horizontal.increaseButton.x + layout.scrollbar.horizontal.increaseButton.width : 0;
3323
+ const verticalRight = layout.scrollbar.vertical ? layout.scrollbar.vertical.decreaseButton.x + layout.scrollbar.vertical.decreaseButton.width : 0;
3324
+ return Math.max(layout.tableWidth, horizontalRight, verticalRight);
3325
+ }
3117
3326
  function drawPivotResizeGuide(context, x, height) {
3118
3327
  context.save();
3119
3328
  context.strokeStyle = "#1677ff";
@@ -3599,7 +3808,7 @@ function hitTestPivotLayout(layout, x, y) {
3599
3808
  onSortIcon: false
3600
3809
  };
3601
3810
  }
3602
- const bodyCell = layout.bodyCells.find((cell) => contains(cell.rect, x, y));
3811
+ const bodyCell = findLastContaining(layout.bodyCells, x, y);
3603
3812
  if (bodyCell) {
3604
3813
  return {
3605
3814
  region: "body",
@@ -3609,9 +3818,7 @@ function hitTestPivotLayout(layout, x, y) {
3609
3818
  onSortIcon: false
3610
3819
  };
3611
3820
  }
3612
- const rowHeaderCell = layout.rowHeaderCells.find(
3613
- (cell) => contains(cell.rect, x, y)
3614
- );
3821
+ const rowHeaderCell = findLastContaining(layout.rowHeaderCells, x, y);
3615
3822
  if (rowHeaderCell) {
3616
3823
  return {
3617
3824
  region: "rowHeader",
@@ -3664,6 +3871,13 @@ function isSortIconHit(cell, x) {
3664
3871
  function contains(rect, x, y) {
3665
3872
  return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
3666
3873
  }
3874
+ function findLastContaining(cells, x, y) {
3875
+ for (let index = cells.length - 1; index >= 0; index -= 1) {
3876
+ const cell = cells[index];
3877
+ if (cell && contains(cell.rect, x, y)) return cell;
3878
+ }
3879
+ return void 0;
3880
+ }
3667
3881
 
3668
3882
  // src/interaction/text-selection.ts
3669
3883
  function resolvePivotTextOffset(params) {
@@ -3862,6 +4076,16 @@ function hitTestPivotScrollbar(layout, x, y) {
3862
4076
  }
3863
4077
  return null;
3864
4078
  }
4079
+ function resolvePivotScrollbarTrackScroll(params) {
4080
+ const movableLength = Math.max(params.trackLength - params.thumbLength, 0);
4081
+ if (movableLength === 0) return 0;
4082
+ const thumbStart = utils.clamp(
4083
+ params.pointerValue - params.thumbLength / 2,
4084
+ params.trackStart,
4085
+ params.trackStart + movableLength
4086
+ );
4087
+ return (thumbStart - params.trackStart) / movableLength * params.maxScroll;
4088
+ }
3865
4089
  function resolveRole(layout, x, y) {
3866
4090
  if (utils.isPointInRect(x, y, layout.thumb)) return "thumb";
3867
4091
  if (utils.isPointInRect(x, y, layout.decreaseButton)) return "decrease-button";
@@ -3900,6 +4124,16 @@ function createAddress(rowSlot, columnSlot, indicatorKey) {
3900
4124
  // src/domain/interaction/row-selection.ts
3901
4125
  function resolvePivotRowIndexAtY(layout, y) {
3902
4126
  if (layout.rowSlots.length === 0 || layout.rowHeight <= 0) return null;
4127
+ const frozenRange = layout.frozenBottomRowRange;
4128
+ if (frozenRange) {
4129
+ const frozenTop = layout.bodyRect.y + layout.bodyRect.height - (frozenRange.end - frozenRange.start) * layout.rowHeight;
4130
+ if (y >= frozenTop) {
4131
+ return Math.min(
4132
+ frozenRange.start + Math.floor((y - frozenTop) / layout.rowHeight),
4133
+ frozenRange.end - 1
4134
+ );
4135
+ }
4136
+ }
3903
4137
  const index = Math.floor(
3904
4138
  (y - layout.bodyRect.y + layout.scroll.top) / layout.rowHeight
3905
4139
  );
@@ -4018,6 +4252,8 @@ var PivotTable = class {
4018
4252
  __publicField(this, "wheelScrollTarget", null);
4019
4253
  __publicField(this, "wheelScrollFrameId", null);
4020
4254
  __publicField(this, "wheelScrollLastTime", 0);
4255
+ __publicField(this, "continuousScrollTimer", null);
4256
+ __publicField(this, "continuousScrollPointerId", null);
4021
4257
  __publicField(this, "resizeSession", null);
4022
4258
  __publicField(this, "resizeGuideX", null);
4023
4259
  __publicField(this, "suppressNextClick", false);
@@ -4096,6 +4332,7 @@ var PivotTable = class {
4096
4332
  this.scheduleRender();
4097
4333
  });
4098
4334
  __publicField(this, "handleClick", (event) => {
4335
+ this.options.ui?.onContextMenuRequest?.(null);
4099
4336
  if (this.suppressNextClick) {
4100
4337
  this.suppressNextClick = false;
4101
4338
  return;
@@ -4414,6 +4651,8 @@ var PivotTable = class {
4414
4651
  });
4415
4652
  __publicField(this, "handlePointerUp", (event) => {
4416
4653
  if (!this.host) return;
4654
+ const endedContinuousScroll = this.continuousScrollPointerId === event.pointerId;
4655
+ if (endedContinuousScroll) this.stopContinuousScrollbarScroll();
4417
4656
  const endedCellSelection = this.cellSelectionDrag?.pointerId === event.pointerId;
4418
4657
  if (endedCellSelection) this.cellSelectionDrag = null;
4419
4658
  if (this.textSelectionDrag?.pointerId === event.pointerId) {
@@ -4432,7 +4671,7 @@ var PivotTable = class {
4432
4671
  if (this.scrollbarDrag?.pointerId === event.pointerId) {
4433
4672
  this.scrollbarDrag = null;
4434
4673
  }
4435
- if (!this.resizeSession && !this.scrollbarDrag && !endedCellSelection && !endedRowHeaderSelection && !this.host.root.hasPointerCapture(event.pointerId))
4674
+ if (!this.resizeSession && !this.scrollbarDrag && !endedContinuousScroll && !endedCellSelection && !endedRowHeaderSelection && !this.host.root.hasPointerCapture(event.pointerId))
4436
4675
  return;
4437
4676
  if (this.host.root.hasPointerCapture(event.pointerId)) {
4438
4677
  this.host.root.releasePointerCapture(event.pointerId);
@@ -4605,11 +4844,16 @@ var PivotTable = class {
4605
4844
  (slot) => (slot.nodeId ?? "") === address.columnNodeId && (!slot.indicatorKey || slot.indicatorKey === address.indicatorKey)
4606
4845
  );
4607
4846
  if (rowIndex < 0 || columnIndex < 0) return false;
4608
- const top = resolveVisibleOffset({
4847
+ const frozenRange = this.layout.frozenBottomRowRange;
4848
+ const frozenBottomHeight = frozenRange ? (frozenRange.end - frozenRange.start) * this.layout.rowHeight : 0;
4849
+ const top = frozenRange && rowIndex >= frozenRange.start ? this.scroll.top : resolveVisibleOffset({
4609
4850
  current: this.scroll.top,
4610
4851
  itemStart: rowIndex * this.layout.rowHeight,
4611
4852
  itemSize: this.layout.rowHeight,
4612
- viewportSize: this.layout.bodyRect.height
4853
+ viewportSize: Math.max(
4854
+ this.layout.bodyRect.height - frozenBottomHeight,
4855
+ 0
4856
+ )
4613
4857
  });
4614
4858
  const left = resolveVisibleOffset({
4615
4859
  current: this.scroll.left,
@@ -4673,6 +4917,7 @@ var PivotTable = class {
4673
4917
  if (this.frameId !== null) cancelAnimationFrame(this.frameId);
4674
4918
  this.frameId = null;
4675
4919
  this.stopWheelScrollSmoothing();
4920
+ this.stopContinuousScrollbarScroll();
4676
4921
  this.stopResizeObserver?.();
4677
4922
  this.stopResizeObserver = null;
4678
4923
  this.host?.root.removeEventListener("pointermove", this.handlePointerMove);
@@ -5041,17 +5286,27 @@ var PivotTable = class {
5041
5286
  event.preventDefault();
5042
5287
  this.suppressNextClick = true;
5043
5288
  if (hit.role === "decrease-button" || hit.role === "increase-button") {
5044
- this.stepScrollbar(hit.axis, hit.role === "decrease-button" ? -1 : 1);
5289
+ this.startContinuousScrollbarScroll(
5290
+ hit.axis,
5291
+ hit.role === "decrease-button" ? -1 : 1,
5292
+ event.pointerId
5293
+ );
5294
+ this.host.root.setPointerCapture(event.pointerId);
5045
5295
  return;
5046
5296
  }
5047
5297
  const rootRect = this.host.root.getBoundingClientRect();
5048
5298
  const pointer = hit.axis === "horizontal" ? event.clientX - rootRect.left : event.clientY - rootRect.top;
5049
5299
  if (hit.role === "track") {
5050
- const thumbStart2 = hit.axis === "horizontal" ? axisLayout.thumb.x : axisLayout.thumb.y;
5051
- const direction = pointer < thumbStart2 ? -1 : 1;
5300
+ const nextScroll = resolvePivotScrollbarTrackScroll({
5301
+ pointerValue: pointer,
5302
+ trackStart: hit.axis === "horizontal" ? axisLayout.track.x : axisLayout.track.y,
5303
+ trackLength: hit.axis === "horizontal" ? axisLayout.track.width : axisLayout.track.height,
5304
+ thumbLength: hit.axis === "horizontal" ? axisLayout.thumb.width : axisLayout.thumb.height,
5305
+ maxScroll: hit.axis === "horizontal" ? this.layout.viewport.maxScrollLeft : this.layout.viewport.maxScrollTop
5306
+ });
5052
5307
  this.applyScrollPosition(
5053
- this.scroll.left + (hit.axis === "horizontal" ? direction * this.layout.bodyRect.width : 0),
5054
- this.scroll.top + (hit.axis === "vertical" ? direction * this.layout.bodyRect.height : 0)
5308
+ hit.axis === "horizontal" ? nextScroll : this.scroll.left,
5309
+ hit.axis === "vertical" ? nextScroll : this.scroll.top
5055
5310
  );
5056
5311
  return;
5057
5312
  }
@@ -5095,6 +5350,21 @@ var PivotTable = class {
5095
5350
  this.scroll.top + (axis === "vertical" ? direction * this.layout.rowHeight : 0)
5096
5351
  );
5097
5352
  }
5353
+ startContinuousScrollbarScroll(axis, direction, pointerId) {
5354
+ this.stopContinuousScrollbarScroll();
5355
+ this.continuousScrollPointerId = pointerId;
5356
+ this.stepScrollbar(axis, direction);
5357
+ this.continuousScrollTimer = setInterval(() => {
5358
+ this.stepScrollbar(axis, direction);
5359
+ }, 80);
5360
+ }
5361
+ stopContinuousScrollbarScroll() {
5362
+ if (this.continuousScrollTimer !== null) {
5363
+ clearInterval(this.continuousScrollTimer);
5364
+ this.continuousScrollTimer = null;
5365
+ }
5366
+ this.continuousScrollPointerId = null;
5367
+ }
5098
5368
  applyScrollPosition(left, top) {
5099
5369
  if (!this.layout) return;
5100
5370
  const next = {