@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.js CHANGED
@@ -728,14 +728,11 @@ function matchesSearchKeyword(value, filter) {
728
728
  return filter.searchExact ? text === keyword : text.includes(keyword);
729
729
  }
730
730
  function matchesFilterConditions(value, filter) {
731
- const first = filter.condition;
732
- const second = filter.secondCondition;
733
- const firstActive = first && first.operator !== "none";
734
- const secondActive = second && second.operator !== "none";
735
- if (!firstActive && !secondActive) return true;
736
- if (!firstActive) return evaluateCondition(value, second);
737
- if (!secondActive) return evaluateCondition(value, first);
738
- return filter.conditionRelation === "or" ? evaluateCondition(value, first) || evaluateCondition(value, second) : evaluateCondition(value, first) && evaluateCondition(value, second);
731
+ const conditions = (filter.conditions?.length ? filter.conditions : [filter.condition, filter.secondCondition]).filter(
732
+ (condition) => condition != null && condition.operator !== "none"
733
+ );
734
+ if (conditions.length === 0) return true;
735
+ return filter.conditionRelation === "or" ? conditions.some((condition) => evaluateCondition(value, condition)) : conditions.every((condition) => evaluateCondition(value, condition));
739
736
  }
740
737
  function evaluateCondition(value, condition) {
741
738
  const text = value == null ? "" : String(value);
@@ -974,7 +971,7 @@ function computePivotScrollbarLayout(params) {
974
971
  }
975
972
  if (hasVertical) {
976
973
  const fullHeight = Math.max(
977
- params.bodyHeight - VERTICAL_TOP_INSET - (hasHorizontal ? SCROLLBAR_THICKNESS : 0),
974
+ params.bodyHeight - VERTICAL_TOP_INSET,
978
975
  0
979
976
  );
980
977
  const buttonSize = Math.min(SCROLLBAR_THICKNESS, fullHeight / 3);
@@ -1030,6 +1027,7 @@ var DEFAULT_ROW_HEIGHT = 40;
1030
1027
  var DEFAULT_COLUMN_WIDTH = 120;
1031
1028
  var DEFAULT_HEADER_SIZE = 40;
1032
1029
  var DEFAULT_ROW_DIMENSION_WIDTH2 = 140;
1030
+ var SCROLLBAR_THICKNESS2 = 10;
1033
1031
  function buildPivotLayoutSnapshot(params) {
1034
1032
  const { model, options, visibleRowNodes, visibleColumnNodes, width, height } = params;
1035
1033
  const indicatorLayout = options.indicatorLayout ?? "columns";
@@ -1062,8 +1060,8 @@ function buildPivotLayoutSnapshot(params) {
1062
1060
  const columnHeaderHeight = columnHeaderLevels * DEFAULT_HEADER_SIZE;
1063
1061
  const reportFilterHeight = (options.filterDimensions?.length ?? 0) * rowHeight;
1064
1062
  const tableHeaderHeight = reportFilterHeight + columnHeaderHeight;
1065
- const bodyWidth = Math.max(width - rowHeaderWidth, 0);
1066
- const availableBodyHeight = Math.max(height - tableHeaderHeight, 0);
1063
+ const rawBodyWidth = Math.max(width - rowHeaderWidth, 0);
1064
+ const rawAvailableBodyHeight = Math.max(height - tableHeaderHeight, 0);
1067
1065
  const rowSlots = createPivotAxisSlots({
1068
1066
  nodes: visibleRowNodes,
1069
1067
  indicators: options.indicators.map((indicator) => indicator.key),
@@ -1086,19 +1084,52 @@ function buildPivotLayoutSnapshot(params) {
1086
1084
  const columnOffsets = createOffsets(columnWidths);
1087
1085
  const totalBodyWidth = columnOffsets[columnOffsets.length - 1] ?? 0;
1088
1086
  const totalBodyHeight = rowSlots.length * rowHeight;
1087
+ let hasHorizontalScrollbar = totalBodyWidth > rawBodyWidth;
1088
+ let hasVerticalScrollbar = totalBodyHeight > rawAvailableBodyHeight;
1089
+ for (let index = 0; index < 2; index += 1) {
1090
+ hasHorizontalScrollbar = totalBodyWidth > Math.max(
1091
+ rawBodyWidth - (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1092
+ 0
1093
+ );
1094
+ hasVerticalScrollbar = totalBodyHeight > Math.max(
1095
+ rawAvailableBodyHeight - (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1096
+ 0
1097
+ );
1098
+ }
1099
+ const bodyWidth = Math.max(
1100
+ rawBodyWidth - (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1101
+ 0
1102
+ );
1103
+ const availableBodyHeight = Math.max(
1104
+ rawAvailableBodyHeight - (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1105
+ 0
1106
+ );
1107
+ const frozenBottomRowRange = resolveFrozenBottomRowRange(rowSlots);
1108
+ const frozenBottomRowCount = frozenBottomRowRange ? frozenBottomRowRange.end - frozenBottomRowRange.start : 0;
1109
+ const frozenBottomHeight = frozenBottomRowCount * rowHeight;
1110
+ const scrollableRowCount = frozenBottomRowRange?.start ?? rowSlots.length;
1111
+ const totalScrollableBodyHeight = scrollableRowCount * rowHeight;
1089
1112
  const tableWidth = rowHeaderWidth + Math.min(bodyWidth, totalBodyWidth);
1090
1113
  const bodyHeight = Math.min(availableBodyHeight, totalBodyHeight);
1091
- const fittedHeight = tableHeaderHeight + bodyHeight;
1114
+ const visibleFrozenBottomHeight = Math.min(frozenBottomHeight, bodyHeight);
1115
+ const scrollableBodyHeight = Math.max(
1116
+ bodyHeight - visibleFrozenBottomHeight,
1117
+ 0
1118
+ );
1119
+ const fittedHeight = tableHeaderHeight + bodyHeight + (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0);
1092
1120
  const maxScrollLeft = Math.max(totalBodyWidth - bodyWidth, 0);
1093
- const maxScrollTop = Math.max(totalBodyHeight - bodyHeight, 0);
1121
+ const maxScrollTop = Math.max(
1122
+ totalScrollableBodyHeight - scrollableBodyHeight,
1123
+ 0
1124
+ );
1094
1125
  const scroll = {
1095
1126
  left: clamp(params.scroll.left, 0, maxScrollLeft),
1096
1127
  top: clamp(params.scroll.top, 0, maxScrollTop)
1097
1128
  };
1098
1129
  const visibleRowRange = resolveVisibleRange({
1099
- count: rowSlots.length,
1130
+ count: scrollableRowCount,
1100
1131
  itemSize: rowHeight,
1101
- viewportSize: bodyHeight,
1132
+ viewportSize: scrollableBodyHeight,
1102
1133
  offset: scroll.top,
1103
1134
  overscan: options.overscanRowCount ?? 1
1104
1135
  });
@@ -1133,6 +1164,23 @@ function buildPivotLayoutSnapshot(params) {
1133
1164
  dimensionCount: options.rows.length,
1134
1165
  indicators: options.indicators
1135
1166
  });
1167
+ if (frozenBottomRowRange) {
1168
+ rowHeaderCells.push(
1169
+ ...buildRowHeaderCells({
1170
+ model,
1171
+ options,
1172
+ slots: rowSlots,
1173
+ range: frozenBottomRowRange,
1174
+ rowHeight,
1175
+ rowDimensionWidths,
1176
+ columnHeaderHeight: tableHeaderHeight,
1177
+ scrollTop: frozenBottomRowRange.start * rowHeight - scrollableBodyHeight,
1178
+ expandedNodeIds: params.expandedRowNodeIds,
1179
+ dimensionCount: options.rows.length,
1180
+ indicators: options.indicators
1181
+ })
1182
+ );
1183
+ }
1136
1184
  const columnHeaderCells = buildColumnHeaderCells({
1137
1185
  model,
1138
1186
  options,
@@ -1159,14 +1207,34 @@ function buildPivotLayoutSnapshot(params) {
1159
1207
  columnHeaderHeight: tableHeaderHeight,
1160
1208
  scroll
1161
1209
  });
1210
+ if (frozenBottomRowRange) {
1211
+ bodyCells.push(
1212
+ ...buildBodyCells({
1213
+ model,
1214
+ rowSlots,
1215
+ columnSlots,
1216
+ rowRange: frozenBottomRowRange,
1217
+ columnRange: visibleColumnRange,
1218
+ rowHeight,
1219
+ columnWidths,
1220
+ columnOffsets,
1221
+ rowHeaderWidth,
1222
+ columnHeaderHeight: tableHeaderHeight,
1223
+ scroll: {
1224
+ left: scroll.left,
1225
+ top: frozenBottomRowRange.start * rowHeight - scrollableBodyHeight
1226
+ }
1227
+ })
1228
+ );
1229
+ }
1162
1230
  const scrollbar = computePivotScrollbarLayout({
1163
- width,
1231
+ width: tableWidth + (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1164
1232
  height: fittedHeight,
1165
1233
  bodyTop: tableHeaderHeight,
1166
- bodyHeight,
1234
+ bodyHeight: scrollableBodyHeight,
1167
1235
  bodyWidth,
1168
1236
  totalBodyWidth,
1169
- totalBodyHeight,
1237
+ totalBodyHeight: totalScrollableBodyHeight,
1170
1238
  scrollLeft: scroll.left,
1171
1239
  scrollTop: scroll.top,
1172
1240
  maxScrollLeft,
@@ -1218,6 +1286,7 @@ function buildPivotLayoutSnapshot(params) {
1218
1286
  rowSlots,
1219
1287
  columnSlots,
1220
1288
  visibleRowRange,
1289
+ frozenBottomRowRange,
1221
1290
  visibleColumnRange,
1222
1291
  reportFilterCells,
1223
1292
  rowFieldHeaderCells,
@@ -1352,6 +1421,13 @@ function createAxisSlot(node, indicatorKey) {
1352
1421
  kind: node?.kind ?? "empty"
1353
1422
  };
1354
1423
  }
1424
+ function resolveFrozenBottomRowRange(slots) {
1425
+ let start = slots.length;
1426
+ while (start > 0 && slots[start - 1]?.kind === "grandTotal") {
1427
+ start -= 1;
1428
+ }
1429
+ return start < slots.length ? { start, end: slots.length } : null;
1430
+ }
1355
1431
  function buildRowHeaderCells(params) {
1356
1432
  const cells = [];
1357
1433
  for (let rowIndex = params.range.start; rowIndex < params.range.end; rowIndex += 1) {
@@ -2552,6 +2628,7 @@ function createPivotDomHost(container) {
2552
2628
  right: "0",
2553
2629
  height: "100%",
2554
2630
  overflow: "hidden",
2631
+ borderRadius: "8px",
2555
2632
  touchAction: "none"
2556
2633
  });
2557
2634
  Object.assign(canvas.style, {
@@ -2614,8 +2691,8 @@ function resolvePivotSelectionRect(layout, selection) {
2614
2691
  const kind = resolvePivotSelectionKind(selection);
2615
2692
  const startX = kind === "row" || kind === "row-cell" ? layout.rowHeaderRect.x : layout.bodyRect.x + (layout.columnOffsets[indexes.startColumnIndex] ?? 0) - layout.scroll.left;
2616
2693
  const endX = layout.bodyRect.x + (layout.columnOffsets[indexes.endColumnIndex] ?? 0) + (layout.columnWidths[indexes.endColumnIndex] ?? layout.columnWidth) - layout.scroll.left;
2617
- const startY = kind === "column" ? layout.columnHeaderRect.y : layout.bodyRect.y + indexes.startRowIndex * layout.rowHeight - layout.scroll.top;
2618
- const endY = layout.bodyRect.y + (indexes.endRowIndex + 1) * layout.rowHeight - layout.scroll.top;
2694
+ const startY = kind === "column" ? layout.columnHeaderRect.y : resolvePivotRowTop(layout, indexes.startRowIndex);
2695
+ const endY = resolvePivotRowTop(layout, indexes.endRowIndex) + layout.rowHeight;
2619
2696
  const clipRect = resolvePivotSelectionClipRect(layout, selection);
2620
2697
  const visibleStartX = Math.max(startX, clipRect.x);
2621
2698
  const visibleEndX = Math.min(endX, clipRect.x + clipRect.width);
@@ -2629,6 +2706,13 @@ function resolvePivotSelectionRect(layout, selection) {
2629
2706
  height: visibleEndY - visibleStartY
2630
2707
  };
2631
2708
  }
2709
+ function resolvePivotRowTop(layout, rowIndex) {
2710
+ const frozenRange = layout.frozenBottomRowRange;
2711
+ if (frozenRange && rowIndex >= frozenRange.start && rowIndex < frozenRange.end) {
2712
+ return layout.bodyRect.y + layout.bodyRect.height - (frozenRange.end - rowIndex) * layout.rowHeight;
2713
+ }
2714
+ return layout.bodyRect.y + rowIndex * layout.rowHeight - layout.scroll.top;
2715
+ }
2632
2716
  function resolvePivotSelectionClipRect(layout, selection) {
2633
2717
  const kind = resolvePivotSelectionKind(selection);
2634
2718
  if (kind === "row" || kind === "row-cell") {
@@ -2773,26 +2857,127 @@ var defaultPivotTableTheme = {
2773
2857
  };
2774
2858
 
2775
2859
  // src/rendering/draw-scrollbars.ts
2776
- function drawPivotScrollbars(context, layout) {
2860
+ function drawPivotScrollbars(context, layout, borderColor, verticalFrame) {
2777
2861
  context.save();
2862
+ if (layout.horizontal && verticalFrame) {
2863
+ drawHorizontalScrollbarMask(
2864
+ context,
2865
+ layout.horizontal,
2866
+ verticalFrame.bodyBackgroundColor
2867
+ );
2868
+ }
2869
+ if (layout.vertical && verticalFrame) {
2870
+ drawVerticalScrollbarFrame(
2871
+ context,
2872
+ layout.vertical,
2873
+ verticalFrame,
2874
+ borderColor
2875
+ );
2876
+ }
2778
2877
  if (layout.horizontal) {
2779
- drawAxis(context, layout.horizontal, "horizontal");
2878
+ drawAxis(context, layout.horizontal, "horizontal", borderColor);
2780
2879
  }
2781
2880
  if (layout.vertical) {
2782
- drawAxis(context, layout.vertical, "vertical");
2881
+ drawAxis(context, layout.vertical, "vertical", borderColor);
2882
+ }
2883
+ if (layout.horizontal) {
2884
+ drawHorizontalScrollbarTopBorder(
2885
+ context,
2886
+ layout.horizontal,
2887
+ layout.vertical,
2888
+ borderColor
2889
+ );
2783
2890
  }
2784
2891
  context.restore();
2785
2892
  }
2786
- function drawAxis(context, layout, axis) {
2787
- drawTrack(context, layout.track);
2893
+ function drawHorizontalScrollbarMask(context, layout, backgroundColor) {
2894
+ const left = layout.decreaseButton.x;
2895
+ const right = layout.increaseButton.x + layout.increaseButton.width;
2896
+ const top = layout.decreaseButton.y;
2897
+ const height = layout.decreaseButton.height;
2898
+ if (right <= left || height <= 0) return;
2899
+ context.save();
2900
+ context.fillStyle = backgroundColor;
2901
+ context.fillRect(left, top, right - left, height);
2902
+ context.restore();
2903
+ }
2904
+ function drawHorizontalScrollbarTopBorder(context, horizontal, vertical, borderColor) {
2905
+ const left = Math.round(horizontal.decreaseButton.x) + 0.5;
2906
+ const horizontalRight = horizontal.increaseButton.x + horizontal.increaseButton.width;
2907
+ const verticalRight = vertical ? vertical.decreaseButton.x + vertical.decreaseButton.width : horizontalRight;
2908
+ const right = Math.round(Math.max(horizontalRight, verticalRight)) - 0.5;
2909
+ const top = Math.round(horizontal.decreaseButton.y) + 0.5;
2910
+ if (right <= left) return;
2911
+ context.save();
2912
+ context.strokeStyle = borderColor;
2913
+ context.lineWidth = 1;
2914
+ context.beginPath();
2915
+ context.moveTo(left, top);
2916
+ context.lineTo(right, top);
2917
+ context.stroke();
2918
+ context.restore();
2919
+ }
2920
+ function drawAxis(context, layout, axis, borderColor) {
2788
2921
  drawButton(context, layout.decreaseButton, axis, "decrease");
2789
2922
  drawButton(context, layout.increaseButton, axis, "increase");
2790
2923
  drawThumb(context, layout.thumb);
2924
+ drawTrackBorder(context, layout.track, axis, borderColor);
2925
+ }
2926
+ function drawTrackBorder(context, rect, axis, borderColor) {
2927
+ const left = Math.round(rect.x) + 0.5;
2928
+ const right = Math.round(rect.x + rect.width) - 0.5;
2929
+ const top = Math.round(rect.y) + 0.5;
2930
+ const bottom = Math.round(rect.y + rect.height) - 0.5;
2931
+ if (right <= left || bottom <= top) return;
2932
+ context.save();
2933
+ context.strokeStyle = borderColor;
2934
+ context.lineWidth = 1;
2935
+ context.beginPath();
2936
+ if (axis === "horizontal") {
2937
+ context.moveTo(left, top);
2938
+ context.lineTo(right, top);
2939
+ } else {
2940
+ context.moveTo(left, top);
2941
+ context.lineTo(left, bottom);
2942
+ }
2943
+ context.stroke();
2944
+ context.restore();
2791
2945
  }
2792
- function drawTrack(context, rect) {
2793
- context.fillStyle = "rgba(148, 163, 184, 0.16)";
2794
- roundRect(context, rect, rect.height / 2);
2795
- context.fill();
2946
+ function drawVerticalScrollbarFrame(context, layout, frame, borderColor) {
2947
+ const left = Math.round(layout.decreaseButton.x) + 0.5;
2948
+ const top = Math.round(frame.top) + 0.5;
2949
+ const bottom = Math.round(frame.top + frame.height) - 0.5;
2950
+ const headerBottom = Math.round(frame.headerBottom) + 0.5;
2951
+ if (bottom <= top) return;
2952
+ context.save();
2953
+ context.fillStyle = frame.headerBackgroundColor;
2954
+ context.fillRect(
2955
+ layout.decreaseButton.x,
2956
+ frame.top,
2957
+ layout.decreaseButton.width,
2958
+ Math.max(frame.headerBottom - frame.top, 0)
2959
+ );
2960
+ context.fillStyle = frame.bodyBackgroundColor;
2961
+ context.fillRect(
2962
+ layout.decreaseButton.x,
2963
+ frame.headerBottom,
2964
+ layout.decreaseButton.width,
2965
+ Math.max(frame.top + frame.height - frame.headerBottom, 0)
2966
+ );
2967
+ context.strokeStyle = borderColor;
2968
+ context.lineWidth = 1;
2969
+ context.beginPath();
2970
+ context.moveTo(left, top);
2971
+ context.lineTo(left, bottom);
2972
+ context.stroke();
2973
+ if (headerBottom > top && headerBottom < bottom) {
2974
+ const right = Math.round(layout.decreaseButton.x + layout.decreaseButton.width) - 0.5;
2975
+ context.beginPath();
2976
+ context.moveTo(left, headerBottom);
2977
+ context.lineTo(right, headerBottom);
2978
+ context.stroke();
2979
+ }
2980
+ context.restore();
2796
2981
  }
2797
2982
  function drawThumb(context, rect) {
2798
2983
  context.fillStyle = "rgba(100, 116, 139, 0.8)";
@@ -2800,28 +2985,25 @@ function drawThumb(context, rect) {
2800
2985
  context.fill();
2801
2986
  }
2802
2987
  function drawButton(context, rect, axis, direction) {
2803
- context.fillStyle = "rgba(226, 232, 240, 0.92)";
2804
- context.strokeStyle = "rgba(148, 163, 184, 0.65)";
2805
- context.lineWidth = 1;
2806
- roundRect(context, rect, rect.height / 2);
2807
- context.fill();
2808
- context.stroke();
2809
2988
  context.fillStyle = "rgba(71, 85, 105, 0.88)";
2810
2989
  context.beginPath();
2811
2990
  const centerX = rect.x + rect.width / 2;
2812
2991
  const centerY = rect.y + rect.height / 2;
2992
+ const iconSize = Math.min(rect.width, rect.height) * 0.64;
2993
+ const directionOffset = iconSize * 0.4;
2994
+ const crossAxisOffset = iconSize / 2;
2813
2995
  if (axis === "horizontal") {
2814
- const tipX = centerX + (direction === "decrease" ? -1 : 1) * rect.width * 0.14;
2815
- const tailX = centerX + (direction === "decrease" ? 1 : -1) * rect.width * 0.14;
2996
+ const tipX = centerX + (direction === "decrease" ? -1 : 1) * directionOffset;
2997
+ const tailX = centerX + (direction === "decrease" ? 1 : -1) * directionOffset;
2816
2998
  context.moveTo(tipX, centerY);
2817
- context.lineTo(tailX, centerY - rect.height * 0.22);
2818
- context.lineTo(tailX, centerY + rect.height * 0.22);
2999
+ context.lineTo(tailX, centerY - crossAxisOffset);
3000
+ context.lineTo(tailX, centerY + crossAxisOffset);
2819
3001
  } else {
2820
- const tipY = centerY + (direction === "decrease" ? -1 : 1) * rect.height * 0.14;
2821
- const tailY = centerY + (direction === "decrease" ? 1 : -1) * rect.height * 0.14;
3002
+ const tipY = centerY + (direction === "decrease" ? -1 : 1) * directionOffset;
3003
+ const tailY = centerY + (direction === "decrease" ? 1 : -1) * directionOffset;
2822
3004
  context.moveTo(centerX, tipY);
2823
- context.lineTo(centerX - rect.width * 0.22, tailY);
2824
- context.lineTo(centerX + rect.width * 0.22, tailY);
3005
+ context.lineTo(centerX - crossAxisOffset, tailY);
3006
+ context.lineTo(centerX + crossAxisOffset, tailY);
2825
3007
  }
2826
3008
  context.closePath();
2827
3009
  context.fill();
@@ -2874,25 +3056,22 @@ function drawPivotGridRect(context, rect, color, options) {
2874
3056
  context.stroke();
2875
3057
  context.restore();
2876
3058
  }
2877
- function drawPivotViewportRightBorder(context, rect, color) {
2878
- const x = rect.x + rect.width - 0.5;
2879
- context.save();
2880
- context.strokeStyle = color;
2881
- context.lineWidth = 1;
2882
- context.beginPath();
2883
- context.moveTo(x, rect.y);
2884
- context.lineTo(x, rect.y + rect.height);
2885
- context.stroke();
2886
- context.restore();
3059
+ function clipPivotViewport(context, rect, radius = 8) {
3060
+ appendRoundedRectPath(context, rect, radius);
3061
+ context.clip();
2887
3062
  }
2888
- function drawPivotViewportBottomBorder(context, rect, color) {
2889
- const y = rect.y + rect.height - 0.5;
3063
+ function drawPivotViewportBorder(context, rect, color, radius = 8) {
3064
+ const borderRect = {
3065
+ x: rect.x + 0.5,
3066
+ y: rect.y + 0.5,
3067
+ width: Math.max(rect.width - 1, 0),
3068
+ height: Math.max(rect.height - 1, 0)
3069
+ };
3070
+ if (borderRect.width <= 0 || borderRect.height <= 0) return;
2890
3071
  context.save();
2891
3072
  context.strokeStyle = color;
2892
3073
  context.lineWidth = 1;
2893
- context.beginPath();
2894
- context.moveTo(rect.x, y);
2895
- context.lineTo(rect.x + rect.width, y);
3074
+ appendRoundedRectPath(context, borderRect, radius);
2896
3075
  context.stroke();
2897
3076
  context.restore();
2898
3077
  }
@@ -2910,6 +3089,21 @@ function drawPivotHeaderBoundaries(context, options, color) {
2910
3089
  context.stroke();
2911
3090
  context.restore();
2912
3091
  }
3092
+ function appendRoundedRectPath(context, rect, radius) {
3093
+ const nextRadius = Math.max(
3094
+ Math.min(radius, rect.width / 2, rect.height / 2),
3095
+ 0
3096
+ );
3097
+ const right = rect.x + rect.width;
3098
+ const bottom = rect.y + rect.height;
3099
+ context.beginPath();
3100
+ context.moveTo(rect.x + nextRadius, rect.y);
3101
+ context.arcTo(right, rect.y, right, bottom, nextRadius);
3102
+ context.arcTo(right, bottom, rect.x, bottom, nextRadius);
3103
+ context.arcTo(rect.x, bottom, rect.x, rect.y, nextRadius);
3104
+ context.arcTo(rect.x, rect.y, right, rect.y, nextRadius);
3105
+ context.closePath();
3106
+ }
2913
3107
 
2914
3108
  // src/domain/layout/sticky-row-header.ts
2915
3109
  function resolveStickyRowHeaderContentRect(params) {
@@ -2946,7 +3140,15 @@ function renderPivotTable(params) {
2946
3140
  activeNodePathKey
2947
3141
  );
2948
3142
  const areaSelection = isPivotAreaSelection(layout, params.selection);
3143
+ const viewportWidth = resolvePivotViewportWidth(layout);
2949
3144
  context.clearRect(0, 0, layout.width, layout.height);
3145
+ context.save();
3146
+ clipPivotViewport(context, {
3147
+ x: 0,
3148
+ y: 0,
3149
+ width: viewportWidth,
3150
+ height: layout.height
3151
+ });
2950
3152
  context.font = `${theme.fontSize}px ${theme.fontFamily}`;
2951
3153
  context.textBaseline = "middle";
2952
3154
  withClip(context, layout.reportFilterRect, () => {
@@ -3060,17 +3262,13 @@ function renderPivotTable(params) {
3060
3262
  theme.borderColor
3061
3263
  );
3062
3264
  if (options.debug) drawDebugOverlay(context, layout, theme);
3063
- drawPivotScrollbars(context, layout.scrollbar);
3064
- drawPivotViewportRightBorder(
3065
- context,
3066
- { x: 0, y: 0, width: layout.tableWidth, height: contentHeight },
3067
- theme.borderColor
3068
- );
3069
- drawPivotViewportBottomBorder(
3070
- context,
3071
- { x: 0, y: 0, width: layout.tableWidth, height: contentHeight },
3072
- theme.borderColor
3073
- );
3265
+ drawPivotScrollbars(context, layout.scrollbar, theme.borderColor, {
3266
+ top: 0,
3267
+ height: contentHeight,
3268
+ headerBottom: layout.bodyRect.y,
3269
+ headerBackgroundColor: theme.columnHeaderBackgroundColor,
3270
+ bodyBackgroundColor: theme.backgroundColor
3271
+ });
3074
3272
  const selectionRect = resolvePivotSelectionRect(layout, params.selection);
3075
3273
  if (selectionRect) {
3076
3274
  const selectionClipRect = resolvePivotSelectionClipRect(
@@ -3088,11 +3286,22 @@ function renderPivotTable(params) {
3088
3286
  if (params.resizeGuideX != null) {
3089
3287
  drawPivotResizeGuide(context, params.resizeGuideX, contentHeight);
3090
3288
  }
3289
+ context.restore();
3290
+ drawPivotViewportBorder(
3291
+ context,
3292
+ { x: 0, y: 0, width: viewportWidth, height: contentHeight },
3293
+ theme.borderColor
3294
+ );
3091
3295
  return {
3092
3296
  headerCellCount: layout.reportFilterCells.length + layout.rowFieldHeaderCells.length + layout.rowHeaderCells.length + layout.columnHeaderCells.length,
3093
3297
  bodyCellCount: layout.bodyCells.length
3094
3298
  };
3095
3299
  }
3300
+ function resolvePivotViewportWidth(layout) {
3301
+ const horizontalRight = layout.scrollbar.horizontal ? layout.scrollbar.horizontal.increaseButton.x + layout.scrollbar.horizontal.increaseButton.width : 0;
3302
+ const verticalRight = layout.scrollbar.vertical ? layout.scrollbar.vertical.decreaseButton.x + layout.scrollbar.vertical.decreaseButton.width : 0;
3303
+ return Math.max(layout.tableWidth, horizontalRight, verticalRight);
3304
+ }
3096
3305
  function drawPivotResizeGuide(context, x, height) {
3097
3306
  context.save();
3098
3307
  context.strokeStyle = "#1677ff";
@@ -3578,7 +3787,7 @@ function hitTestPivotLayout(layout, x, y) {
3578
3787
  onSortIcon: false
3579
3788
  };
3580
3789
  }
3581
- const bodyCell = layout.bodyCells.find((cell) => contains(cell.rect, x, y));
3790
+ const bodyCell = findLastContaining(layout.bodyCells, x, y);
3582
3791
  if (bodyCell) {
3583
3792
  return {
3584
3793
  region: "body",
@@ -3588,9 +3797,7 @@ function hitTestPivotLayout(layout, x, y) {
3588
3797
  onSortIcon: false
3589
3798
  };
3590
3799
  }
3591
- const rowHeaderCell = layout.rowHeaderCells.find(
3592
- (cell) => contains(cell.rect, x, y)
3593
- );
3800
+ const rowHeaderCell = findLastContaining(layout.rowHeaderCells, x, y);
3594
3801
  if (rowHeaderCell) {
3595
3802
  return {
3596
3803
  region: "rowHeader",
@@ -3643,6 +3850,13 @@ function isSortIconHit(cell, x) {
3643
3850
  function contains(rect, x, y) {
3644
3851
  return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
3645
3852
  }
3853
+ function findLastContaining(cells, x, y) {
3854
+ for (let index = cells.length - 1; index >= 0; index -= 1) {
3855
+ const cell = cells[index];
3856
+ if (cell && contains(cell.rect, x, y)) return cell;
3857
+ }
3858
+ return void 0;
3859
+ }
3646
3860
 
3647
3861
  // src/interaction/text-selection.ts
3648
3862
  function resolvePivotTextOffset(params) {
@@ -3841,6 +4055,16 @@ function hitTestPivotScrollbar(layout, x, y) {
3841
4055
  }
3842
4056
  return null;
3843
4057
  }
4058
+ function resolvePivotScrollbarTrackScroll(params) {
4059
+ const movableLength = Math.max(params.trackLength - params.thumbLength, 0);
4060
+ if (movableLength === 0) return 0;
4061
+ const thumbStart = clamp(
4062
+ params.pointerValue - params.thumbLength / 2,
4063
+ params.trackStart,
4064
+ params.trackStart + movableLength
4065
+ );
4066
+ return (thumbStart - params.trackStart) / movableLength * params.maxScroll;
4067
+ }
3844
4068
  function resolveRole(layout, x, y) {
3845
4069
  if (isPointInRect(x, y, layout.thumb)) return "thumb";
3846
4070
  if (isPointInRect(x, y, layout.decreaseButton)) return "decrease-button";
@@ -3879,6 +4103,16 @@ function createAddress(rowSlot, columnSlot, indicatorKey) {
3879
4103
  // src/domain/interaction/row-selection.ts
3880
4104
  function resolvePivotRowIndexAtY(layout, y) {
3881
4105
  if (layout.rowSlots.length === 0 || layout.rowHeight <= 0) return null;
4106
+ const frozenRange = layout.frozenBottomRowRange;
4107
+ if (frozenRange) {
4108
+ const frozenTop = layout.bodyRect.y + layout.bodyRect.height - (frozenRange.end - frozenRange.start) * layout.rowHeight;
4109
+ if (y >= frozenTop) {
4110
+ return Math.min(
4111
+ frozenRange.start + Math.floor((y - frozenTop) / layout.rowHeight),
4112
+ frozenRange.end - 1
4113
+ );
4114
+ }
4115
+ }
3882
4116
  const index = Math.floor(
3883
4117
  (y - layout.bodyRect.y + layout.scroll.top) / layout.rowHeight
3884
4118
  );
@@ -3997,6 +4231,8 @@ var PivotTable = class {
3997
4231
  __publicField(this, "wheelScrollTarget", null);
3998
4232
  __publicField(this, "wheelScrollFrameId", null);
3999
4233
  __publicField(this, "wheelScrollLastTime", 0);
4234
+ __publicField(this, "continuousScrollTimer", null);
4235
+ __publicField(this, "continuousScrollPointerId", null);
4000
4236
  __publicField(this, "resizeSession", null);
4001
4237
  __publicField(this, "resizeGuideX", null);
4002
4238
  __publicField(this, "suppressNextClick", false);
@@ -4075,6 +4311,7 @@ var PivotTable = class {
4075
4311
  this.scheduleRender();
4076
4312
  });
4077
4313
  __publicField(this, "handleClick", (event) => {
4314
+ this.options.ui?.onContextMenuRequest?.(null);
4078
4315
  if (this.suppressNextClick) {
4079
4316
  this.suppressNextClick = false;
4080
4317
  return;
@@ -4393,6 +4630,8 @@ var PivotTable = class {
4393
4630
  });
4394
4631
  __publicField(this, "handlePointerUp", (event) => {
4395
4632
  if (!this.host) return;
4633
+ const endedContinuousScroll = this.continuousScrollPointerId === event.pointerId;
4634
+ if (endedContinuousScroll) this.stopContinuousScrollbarScroll();
4396
4635
  const endedCellSelection = this.cellSelectionDrag?.pointerId === event.pointerId;
4397
4636
  if (endedCellSelection) this.cellSelectionDrag = null;
4398
4637
  if (this.textSelectionDrag?.pointerId === event.pointerId) {
@@ -4411,7 +4650,7 @@ var PivotTable = class {
4411
4650
  if (this.scrollbarDrag?.pointerId === event.pointerId) {
4412
4651
  this.scrollbarDrag = null;
4413
4652
  }
4414
- if (!this.resizeSession && !this.scrollbarDrag && !endedCellSelection && !endedRowHeaderSelection && !this.host.root.hasPointerCapture(event.pointerId))
4653
+ if (!this.resizeSession && !this.scrollbarDrag && !endedContinuousScroll && !endedCellSelection && !endedRowHeaderSelection && !this.host.root.hasPointerCapture(event.pointerId))
4415
4654
  return;
4416
4655
  if (this.host.root.hasPointerCapture(event.pointerId)) {
4417
4656
  this.host.root.releasePointerCapture(event.pointerId);
@@ -4584,11 +4823,16 @@ var PivotTable = class {
4584
4823
  (slot) => (slot.nodeId ?? "") === address.columnNodeId && (!slot.indicatorKey || slot.indicatorKey === address.indicatorKey)
4585
4824
  );
4586
4825
  if (rowIndex < 0 || columnIndex < 0) return false;
4587
- const top = resolveVisibleOffset({
4826
+ const frozenRange = this.layout.frozenBottomRowRange;
4827
+ const frozenBottomHeight = frozenRange ? (frozenRange.end - frozenRange.start) * this.layout.rowHeight : 0;
4828
+ const top = frozenRange && rowIndex >= frozenRange.start ? this.scroll.top : resolveVisibleOffset({
4588
4829
  current: this.scroll.top,
4589
4830
  itemStart: rowIndex * this.layout.rowHeight,
4590
4831
  itemSize: this.layout.rowHeight,
4591
- viewportSize: this.layout.bodyRect.height
4832
+ viewportSize: Math.max(
4833
+ this.layout.bodyRect.height - frozenBottomHeight,
4834
+ 0
4835
+ )
4592
4836
  });
4593
4837
  const left = resolveVisibleOffset({
4594
4838
  current: this.scroll.left,
@@ -4652,6 +4896,7 @@ var PivotTable = class {
4652
4896
  if (this.frameId !== null) cancelAnimationFrame(this.frameId);
4653
4897
  this.frameId = null;
4654
4898
  this.stopWheelScrollSmoothing();
4899
+ this.stopContinuousScrollbarScroll();
4655
4900
  this.stopResizeObserver?.();
4656
4901
  this.stopResizeObserver = null;
4657
4902
  this.host?.root.removeEventListener("pointermove", this.handlePointerMove);
@@ -5020,17 +5265,27 @@ var PivotTable = class {
5020
5265
  event.preventDefault();
5021
5266
  this.suppressNextClick = true;
5022
5267
  if (hit.role === "decrease-button" || hit.role === "increase-button") {
5023
- this.stepScrollbar(hit.axis, hit.role === "decrease-button" ? -1 : 1);
5268
+ this.startContinuousScrollbarScroll(
5269
+ hit.axis,
5270
+ hit.role === "decrease-button" ? -1 : 1,
5271
+ event.pointerId
5272
+ );
5273
+ this.host.root.setPointerCapture(event.pointerId);
5024
5274
  return;
5025
5275
  }
5026
5276
  const rootRect = this.host.root.getBoundingClientRect();
5027
5277
  const pointer = hit.axis === "horizontal" ? event.clientX - rootRect.left : event.clientY - rootRect.top;
5028
5278
  if (hit.role === "track") {
5029
- const thumbStart2 = hit.axis === "horizontal" ? axisLayout.thumb.x : axisLayout.thumb.y;
5030
- const direction = pointer < thumbStart2 ? -1 : 1;
5279
+ const nextScroll = resolvePivotScrollbarTrackScroll({
5280
+ pointerValue: pointer,
5281
+ trackStart: hit.axis === "horizontal" ? axisLayout.track.x : axisLayout.track.y,
5282
+ trackLength: hit.axis === "horizontal" ? axisLayout.track.width : axisLayout.track.height,
5283
+ thumbLength: hit.axis === "horizontal" ? axisLayout.thumb.width : axisLayout.thumb.height,
5284
+ maxScroll: hit.axis === "horizontal" ? this.layout.viewport.maxScrollLeft : this.layout.viewport.maxScrollTop
5285
+ });
5031
5286
  this.applyScrollPosition(
5032
- this.scroll.left + (hit.axis === "horizontal" ? direction * this.layout.bodyRect.width : 0),
5033
- this.scroll.top + (hit.axis === "vertical" ? direction * this.layout.bodyRect.height : 0)
5287
+ hit.axis === "horizontal" ? nextScroll : this.scroll.left,
5288
+ hit.axis === "vertical" ? nextScroll : this.scroll.top
5034
5289
  );
5035
5290
  return;
5036
5291
  }
@@ -5074,6 +5329,21 @@ var PivotTable = class {
5074
5329
  this.scroll.top + (axis === "vertical" ? direction * this.layout.rowHeight : 0)
5075
5330
  );
5076
5331
  }
5332
+ startContinuousScrollbarScroll(axis, direction, pointerId) {
5333
+ this.stopContinuousScrollbarScroll();
5334
+ this.continuousScrollPointerId = pointerId;
5335
+ this.stepScrollbar(axis, direction);
5336
+ this.continuousScrollTimer = setInterval(() => {
5337
+ this.stepScrollbar(axis, direction);
5338
+ }, 80);
5339
+ }
5340
+ stopContinuousScrollbarScroll() {
5341
+ if (this.continuousScrollTimer !== null) {
5342
+ clearInterval(this.continuousScrollTimer);
5343
+ this.continuousScrollTimer = null;
5344
+ }
5345
+ this.continuousScrollPointerId = null;
5346
+ }
5077
5347
  applyScrollPosition(left, top) {
5078
5348
  if (!this.layout) return;
5079
5349
  const next = {