@canvas-components/pivot-table 0.1.8 → 0.1.9

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
@@ -572,6 +572,11 @@ interface PivotLayoutSnapshot {
572
572
  start: number;
573
573
  end: number;
574
574
  };
575
+ /** 固定在表体底部的末尾汇总行范围。 */
576
+ frozenBottomRowRange: {
577
+ start: number;
578
+ end: number;
579
+ } | null;
575
580
  visibleColumnRange: {
576
581
  start: number;
577
582
  end: number;
package/dist/index.d.ts CHANGED
@@ -572,6 +572,11 @@ interface PivotLayoutSnapshot {
572
572
  start: number;
573
573
  end: number;
574
574
  };
575
+ /** 固定在表体底部的末尾汇总行范围。 */
576
+ frozenBottomRowRange: {
577
+ start: number;
578
+ end: number;
579
+ } | null;
575
580
  visibleColumnRange: {
576
581
  start: number;
577
582
  end: number;
package/dist/index.js CHANGED
@@ -974,7 +974,7 @@ function computePivotScrollbarLayout(params) {
974
974
  }
975
975
  if (hasVertical) {
976
976
  const fullHeight = Math.max(
977
- params.bodyHeight - VERTICAL_TOP_INSET - (hasHorizontal ? SCROLLBAR_THICKNESS : 0),
977
+ params.bodyHeight - VERTICAL_TOP_INSET,
978
978
  0
979
979
  );
980
980
  const buttonSize = Math.min(SCROLLBAR_THICKNESS, fullHeight / 3);
@@ -1030,6 +1030,7 @@ var DEFAULT_ROW_HEIGHT = 40;
1030
1030
  var DEFAULT_COLUMN_WIDTH = 120;
1031
1031
  var DEFAULT_HEADER_SIZE = 40;
1032
1032
  var DEFAULT_ROW_DIMENSION_WIDTH2 = 140;
1033
+ var SCROLLBAR_THICKNESS2 = 10;
1033
1034
  function buildPivotLayoutSnapshot(params) {
1034
1035
  const { model, options, visibleRowNodes, visibleColumnNodes, width, height } = params;
1035
1036
  const indicatorLayout = options.indicatorLayout ?? "columns";
@@ -1062,8 +1063,8 @@ function buildPivotLayoutSnapshot(params) {
1062
1063
  const columnHeaderHeight = columnHeaderLevels * DEFAULT_HEADER_SIZE;
1063
1064
  const reportFilterHeight = (options.filterDimensions?.length ?? 0) * rowHeight;
1064
1065
  const tableHeaderHeight = reportFilterHeight + columnHeaderHeight;
1065
- const bodyWidth = Math.max(width - rowHeaderWidth, 0);
1066
- const availableBodyHeight = Math.max(height - tableHeaderHeight, 0);
1066
+ const rawBodyWidth = Math.max(width - rowHeaderWidth, 0);
1067
+ const rawAvailableBodyHeight = Math.max(height - tableHeaderHeight, 0);
1067
1068
  const rowSlots = createPivotAxisSlots({
1068
1069
  nodes: visibleRowNodes,
1069
1070
  indicators: options.indicators.map((indicator) => indicator.key),
@@ -1086,19 +1087,52 @@ function buildPivotLayoutSnapshot(params) {
1086
1087
  const columnOffsets = createOffsets(columnWidths);
1087
1088
  const totalBodyWidth = columnOffsets[columnOffsets.length - 1] ?? 0;
1088
1089
  const totalBodyHeight = rowSlots.length * rowHeight;
1090
+ let hasHorizontalScrollbar = totalBodyWidth > rawBodyWidth;
1091
+ let hasVerticalScrollbar = totalBodyHeight > rawAvailableBodyHeight;
1092
+ for (let index = 0; index < 2; index += 1) {
1093
+ hasHorizontalScrollbar = totalBodyWidth > Math.max(
1094
+ rawBodyWidth - (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1095
+ 0
1096
+ );
1097
+ hasVerticalScrollbar = totalBodyHeight > Math.max(
1098
+ rawAvailableBodyHeight - (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1099
+ 0
1100
+ );
1101
+ }
1102
+ const bodyWidth = Math.max(
1103
+ rawBodyWidth - (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1104
+ 0
1105
+ );
1106
+ const availableBodyHeight = Math.max(
1107
+ rawAvailableBodyHeight - (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1108
+ 0
1109
+ );
1110
+ const frozenBottomRowRange = resolveFrozenBottomRowRange(rowSlots);
1111
+ const frozenBottomRowCount = frozenBottomRowRange ? frozenBottomRowRange.end - frozenBottomRowRange.start : 0;
1112
+ const frozenBottomHeight = frozenBottomRowCount * rowHeight;
1113
+ const scrollableRowCount = frozenBottomRowRange?.start ?? rowSlots.length;
1114
+ const totalScrollableBodyHeight = scrollableRowCount * rowHeight;
1089
1115
  const tableWidth = rowHeaderWidth + Math.min(bodyWidth, totalBodyWidth);
1090
1116
  const bodyHeight = Math.min(availableBodyHeight, totalBodyHeight);
1091
- const fittedHeight = tableHeaderHeight + bodyHeight;
1117
+ const visibleFrozenBottomHeight = Math.min(frozenBottomHeight, bodyHeight);
1118
+ const scrollableBodyHeight = Math.max(
1119
+ bodyHeight - visibleFrozenBottomHeight,
1120
+ 0
1121
+ );
1122
+ const fittedHeight = tableHeaderHeight + bodyHeight + (hasHorizontalScrollbar ? SCROLLBAR_THICKNESS2 : 0);
1092
1123
  const maxScrollLeft = Math.max(totalBodyWidth - bodyWidth, 0);
1093
- const maxScrollTop = Math.max(totalBodyHeight - bodyHeight, 0);
1124
+ const maxScrollTop = Math.max(
1125
+ totalScrollableBodyHeight - scrollableBodyHeight,
1126
+ 0
1127
+ );
1094
1128
  const scroll = {
1095
1129
  left: clamp(params.scroll.left, 0, maxScrollLeft),
1096
1130
  top: clamp(params.scroll.top, 0, maxScrollTop)
1097
1131
  };
1098
1132
  const visibleRowRange = resolveVisibleRange({
1099
- count: rowSlots.length,
1133
+ count: scrollableRowCount,
1100
1134
  itemSize: rowHeight,
1101
- viewportSize: bodyHeight,
1135
+ viewportSize: scrollableBodyHeight,
1102
1136
  offset: scroll.top,
1103
1137
  overscan: options.overscanRowCount ?? 1
1104
1138
  });
@@ -1133,6 +1167,23 @@ function buildPivotLayoutSnapshot(params) {
1133
1167
  dimensionCount: options.rows.length,
1134
1168
  indicators: options.indicators
1135
1169
  });
1170
+ if (frozenBottomRowRange) {
1171
+ rowHeaderCells.push(
1172
+ ...buildRowHeaderCells({
1173
+ model,
1174
+ options,
1175
+ slots: rowSlots,
1176
+ range: frozenBottomRowRange,
1177
+ rowHeight,
1178
+ rowDimensionWidths,
1179
+ columnHeaderHeight: tableHeaderHeight,
1180
+ scrollTop: frozenBottomRowRange.start * rowHeight - scrollableBodyHeight,
1181
+ expandedNodeIds: params.expandedRowNodeIds,
1182
+ dimensionCount: options.rows.length,
1183
+ indicators: options.indicators
1184
+ })
1185
+ );
1186
+ }
1136
1187
  const columnHeaderCells = buildColumnHeaderCells({
1137
1188
  model,
1138
1189
  options,
@@ -1159,14 +1210,34 @@ function buildPivotLayoutSnapshot(params) {
1159
1210
  columnHeaderHeight: tableHeaderHeight,
1160
1211
  scroll
1161
1212
  });
1213
+ if (frozenBottomRowRange) {
1214
+ bodyCells.push(
1215
+ ...buildBodyCells({
1216
+ model,
1217
+ rowSlots,
1218
+ columnSlots,
1219
+ rowRange: frozenBottomRowRange,
1220
+ columnRange: visibleColumnRange,
1221
+ rowHeight,
1222
+ columnWidths,
1223
+ columnOffsets,
1224
+ rowHeaderWidth,
1225
+ columnHeaderHeight: tableHeaderHeight,
1226
+ scroll: {
1227
+ left: scroll.left,
1228
+ top: frozenBottomRowRange.start * rowHeight - scrollableBodyHeight
1229
+ }
1230
+ })
1231
+ );
1232
+ }
1162
1233
  const scrollbar = computePivotScrollbarLayout({
1163
- width,
1234
+ width: tableWidth + (hasVerticalScrollbar ? SCROLLBAR_THICKNESS2 : 0),
1164
1235
  height: fittedHeight,
1165
1236
  bodyTop: tableHeaderHeight,
1166
- bodyHeight,
1237
+ bodyHeight: scrollableBodyHeight,
1167
1238
  bodyWidth,
1168
1239
  totalBodyWidth,
1169
- totalBodyHeight,
1240
+ totalBodyHeight: totalScrollableBodyHeight,
1170
1241
  scrollLeft: scroll.left,
1171
1242
  scrollTop: scroll.top,
1172
1243
  maxScrollLeft,
@@ -1218,6 +1289,7 @@ function buildPivotLayoutSnapshot(params) {
1218
1289
  rowSlots,
1219
1290
  columnSlots,
1220
1291
  visibleRowRange,
1292
+ frozenBottomRowRange,
1221
1293
  visibleColumnRange,
1222
1294
  reportFilterCells,
1223
1295
  rowFieldHeaderCells,
@@ -1352,6 +1424,13 @@ function createAxisSlot(node, indicatorKey) {
1352
1424
  kind: node?.kind ?? "empty"
1353
1425
  };
1354
1426
  }
1427
+ function resolveFrozenBottomRowRange(slots) {
1428
+ let start = slots.length;
1429
+ while (start > 0 && slots[start - 1]?.kind === "grandTotal") {
1430
+ start -= 1;
1431
+ }
1432
+ return start < slots.length ? { start, end: slots.length } : null;
1433
+ }
1355
1434
  function buildRowHeaderCells(params) {
1356
1435
  const cells = [];
1357
1436
  for (let rowIndex = params.range.start; rowIndex < params.range.end; rowIndex += 1) {
@@ -2614,8 +2693,8 @@ function resolvePivotSelectionRect(layout, selection) {
2614
2693
  const kind = resolvePivotSelectionKind(selection);
2615
2694
  const startX = kind === "row" || kind === "row-cell" ? layout.rowHeaderRect.x : layout.bodyRect.x + (layout.columnOffsets[indexes.startColumnIndex] ?? 0) - layout.scroll.left;
2616
2695
  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;
2696
+ const startY = kind === "column" ? layout.columnHeaderRect.y : resolvePivotRowTop(layout, indexes.startRowIndex);
2697
+ const endY = resolvePivotRowTop(layout, indexes.endRowIndex) + layout.rowHeight;
2619
2698
  const clipRect = resolvePivotSelectionClipRect(layout, selection);
2620
2699
  const visibleStartX = Math.max(startX, clipRect.x);
2621
2700
  const visibleEndX = Math.min(endX, clipRect.x + clipRect.width);
@@ -2629,6 +2708,13 @@ function resolvePivotSelectionRect(layout, selection) {
2629
2708
  height: visibleEndY - visibleStartY
2630
2709
  };
2631
2710
  }
2711
+ function resolvePivotRowTop(layout, rowIndex) {
2712
+ const frozenRange = layout.frozenBottomRowRange;
2713
+ if (frozenRange && rowIndex >= frozenRange.start && rowIndex < frozenRange.end) {
2714
+ return layout.bodyRect.y + layout.bodyRect.height - (frozenRange.end - rowIndex) * layout.rowHeight;
2715
+ }
2716
+ return layout.bodyRect.y + rowIndex * layout.rowHeight - layout.scroll.top;
2717
+ }
2632
2718
  function resolvePivotSelectionClipRect(layout, selection) {
2633
2719
  const kind = resolvePivotSelectionKind(selection);
2634
2720
  if (kind === "row" || kind === "row-cell") {
@@ -2773,26 +2859,29 @@ var defaultPivotTableTheme = {
2773
2859
  };
2774
2860
 
2775
2861
  // src/rendering/draw-scrollbars.ts
2776
- function drawPivotScrollbars(context, layout) {
2862
+ function drawPivotScrollbars(context, layout, borderColor) {
2777
2863
  context.save();
2778
2864
  if (layout.horizontal) {
2779
- drawAxis(context, layout.horizontal, "horizontal");
2865
+ drawAxis(context, layout.horizontal, "horizontal", borderColor);
2780
2866
  }
2781
2867
  if (layout.vertical) {
2782
- drawAxis(context, layout.vertical, "vertical");
2868
+ drawAxis(context, layout.vertical, "vertical", borderColor);
2783
2869
  }
2784
2870
  context.restore();
2785
2871
  }
2786
- function drawAxis(context, layout, axis) {
2787
- drawTrack(context, layout.track);
2872
+ function drawAxis(context, layout, axis, borderColor) {
2873
+ drawTrack(context, layout.track, borderColor);
2788
2874
  drawButton(context, layout.decreaseButton, axis, "decrease");
2789
2875
  drawButton(context, layout.increaseButton, axis, "increase");
2790
2876
  drawThumb(context, layout.thumb);
2791
2877
  }
2792
- function drawTrack(context, rect) {
2878
+ function drawTrack(context, rect, borderColor) {
2793
2879
  context.fillStyle = "rgba(148, 163, 184, 0.16)";
2880
+ context.strokeStyle = borderColor;
2881
+ context.lineWidth = 1;
2794
2882
  roundRect(context, rect, rect.height / 2);
2795
2883
  context.fill();
2884
+ context.stroke();
2796
2885
  }
2797
2886
  function drawThumb(context, rect) {
2798
2887
  context.fillStyle = "rgba(100, 116, 139, 0.8)";
@@ -3060,7 +3149,7 @@ function renderPivotTable(params) {
3060
3149
  theme.borderColor
3061
3150
  );
3062
3151
  if (options.debug) drawDebugOverlay(context, layout, theme);
3063
- drawPivotScrollbars(context, layout.scrollbar);
3152
+ drawPivotScrollbars(context, layout.scrollbar, theme.borderColor);
3064
3153
  drawPivotViewportRightBorder(
3065
3154
  context,
3066
3155
  { x: 0, y: 0, width: layout.tableWidth, height: contentHeight },
@@ -3578,7 +3667,7 @@ function hitTestPivotLayout(layout, x, y) {
3578
3667
  onSortIcon: false
3579
3668
  };
3580
3669
  }
3581
- const bodyCell = layout.bodyCells.find((cell) => contains(cell.rect, x, y));
3670
+ const bodyCell = findLastContaining(layout.bodyCells, x, y);
3582
3671
  if (bodyCell) {
3583
3672
  return {
3584
3673
  region: "body",
@@ -3588,9 +3677,7 @@ function hitTestPivotLayout(layout, x, y) {
3588
3677
  onSortIcon: false
3589
3678
  };
3590
3679
  }
3591
- const rowHeaderCell = layout.rowHeaderCells.find(
3592
- (cell) => contains(cell.rect, x, y)
3593
- );
3680
+ const rowHeaderCell = findLastContaining(layout.rowHeaderCells, x, y);
3594
3681
  if (rowHeaderCell) {
3595
3682
  return {
3596
3683
  region: "rowHeader",
@@ -3643,6 +3730,13 @@ function isSortIconHit(cell, x) {
3643
3730
  function contains(rect, x, y) {
3644
3731
  return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
3645
3732
  }
3733
+ function findLastContaining(cells, x, y) {
3734
+ for (let index = cells.length - 1; index >= 0; index -= 1) {
3735
+ const cell = cells[index];
3736
+ if (cell && contains(cell.rect, x, y)) return cell;
3737
+ }
3738
+ return void 0;
3739
+ }
3646
3740
 
3647
3741
  // src/interaction/text-selection.ts
3648
3742
  function resolvePivotTextOffset(params) {
@@ -3879,6 +3973,16 @@ function createAddress(rowSlot, columnSlot, indicatorKey) {
3879
3973
  // src/domain/interaction/row-selection.ts
3880
3974
  function resolvePivotRowIndexAtY(layout, y) {
3881
3975
  if (layout.rowSlots.length === 0 || layout.rowHeight <= 0) return null;
3976
+ const frozenRange = layout.frozenBottomRowRange;
3977
+ if (frozenRange) {
3978
+ const frozenTop = layout.bodyRect.y + layout.bodyRect.height - (frozenRange.end - frozenRange.start) * layout.rowHeight;
3979
+ if (y >= frozenTop) {
3980
+ return Math.min(
3981
+ frozenRange.start + Math.floor((y - frozenTop) / layout.rowHeight),
3982
+ frozenRange.end - 1
3983
+ );
3984
+ }
3985
+ }
3882
3986
  const index = Math.floor(
3883
3987
  (y - layout.bodyRect.y + layout.scroll.top) / layout.rowHeight
3884
3988
  );
@@ -4584,11 +4688,16 @@ var PivotTable = class {
4584
4688
  (slot) => (slot.nodeId ?? "") === address.columnNodeId && (!slot.indicatorKey || slot.indicatorKey === address.indicatorKey)
4585
4689
  );
4586
4690
  if (rowIndex < 0 || columnIndex < 0) return false;
4587
- const top = resolveVisibleOffset({
4691
+ const frozenRange = this.layout.frozenBottomRowRange;
4692
+ const frozenBottomHeight = frozenRange ? (frozenRange.end - frozenRange.start) * this.layout.rowHeight : 0;
4693
+ const top = frozenRange && rowIndex >= frozenRange.start ? this.scroll.top : resolveVisibleOffset({
4588
4694
  current: this.scroll.top,
4589
4695
  itemStart: rowIndex * this.layout.rowHeight,
4590
4696
  itemSize: this.layout.rowHeight,
4591
- viewportSize: this.layout.bodyRect.height
4697
+ viewportSize: Math.max(
4698
+ this.layout.bodyRect.height - frozenBottomHeight,
4699
+ 0
4700
+ )
4592
4701
  });
4593
4702
  const left = resolveVisibleOffset({
4594
4703
  current: this.scroll.left,