@canvas-components/pivot-table 0.1.9 → 0.2.2

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
@@ -1,4 +1,4 @@
1
- import { stringifyDataIndex, getValueByDataIndex, clamp, serializeCsvRows, isPointInRect, copyTextSync, copyText } from '@canvas-components/utils';
1
+ import { stringifyDataIndex, getValueByDataIndex, dividePreciseNumber, addPreciseNumbers, clamp, serializeCsvRows, isPointInRect, copyTextSync, copyText } from '@canvas-components/utils';
2
2
  export { getValueByDataIndex, isSameDataIndex, stringifyDataIndex } from '@canvas-components/utils';
3
3
  import * as XLSX from 'xlsx';
4
4
 
@@ -309,8 +309,6 @@ function indexPivotNodes(root) {
309
309
  visit(root);
310
310
  return nodeMap;
311
311
  }
312
-
313
- // src/domain/aggregation/built-in-aggregators.ts
314
312
  function createBuiltInPivotAggregators() {
315
313
  return {
316
314
  sum: createNumericAggregator("sum"),
@@ -341,14 +339,14 @@ function createNumericAggregator(type) {
341
339
  return;
342
340
  }
343
341
  state.numericCount += 1;
344
- state.sum += numericValue;
342
+ state.sum = addPreciseNumbers(state.sum, numericValue) ?? state.sum;
345
343
  state.min = state.min == null ? numericValue : Math.min(state.min, numericValue);
346
344
  state.max = state.max == null ? numericValue : Math.max(state.max, numericValue);
347
345
  },
348
346
  merge(state, source) {
349
347
  state.count += source.count;
350
348
  state.numericCount += source.numericCount;
351
- state.sum += source.sum;
349
+ state.sum = addPreciseNumbers(state.sum, source.sum) ?? state.sum;
352
350
  state.min = mergeMinimum(state.min, source.min);
353
351
  state.max = mergeMaximum(state.max, source.max);
354
352
  source.distinctValues.forEach((value) => state.distinctValues.add(value));
@@ -356,9 +354,11 @@ function createNumericAggregator(type) {
356
354
  finalize(state) {
357
355
  if (type === "count") return state.count;
358
356
  if (type === "distinctCount") return state.distinctValues.size;
359
- if (type === "sum") return state.sum;
357
+ if (type === "sum") return Number(state.sum);
360
358
  if (type === "avg") {
361
- return state.numericCount === 0 ? null : state.sum / state.numericCount;
359
+ if (state.numericCount === 0) return null;
360
+ const average = dividePreciseNumber(state.sum, state.numericCount);
361
+ return average == null ? null : Number(average);
362
362
  }
363
363
  if (type === "min") return state.min;
364
364
  return state.max;
@@ -369,7 +369,7 @@ function createAccumulator() {
369
369
  return {
370
370
  count: 0,
371
371
  numericCount: 0,
372
- sum: 0,
372
+ sum: "0",
373
373
  min: null,
374
374
  max: null,
375
375
  distinctValues: /* @__PURE__ */ new Set()
@@ -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);
@@ -2631,6 +2628,7 @@ function createPivotDomHost(container) {
2631
2628
  right: "0",
2632
2629
  height: "100%",
2633
2630
  overflow: "hidden",
2631
+ borderRadius: "8px",
2634
2632
  touchAction: "none"
2635
2633
  });
2636
2634
  Object.assign(canvas.style, {
@@ -2750,18 +2748,50 @@ function resolvePivotSelectionKind(selection) {
2750
2748
  }
2751
2749
  return selection?.activeNode?.axis ?? "cell";
2752
2750
  }
2753
- function drawPivotSelectionBorder(context, rect, color) {
2751
+ function drawPivotSelectionBorder(context, rect, color, options) {
2754
2752
  context.save();
2755
2753
  context.strokeStyle = color;
2756
2754
  context.lineWidth = 1;
2757
- context.strokeRect(
2758
- rect.x + 0.5,
2759
- rect.y + 0.5,
2760
- Math.max(rect.width - 1, 0),
2761
- Math.max(rect.height - 1, 0)
2762
- );
2755
+ const left = rect.x + 0.5;
2756
+ const top = rect.y + 0.5;
2757
+ const width = Math.max(rect.width - 1, 0);
2758
+ const height = Math.max(rect.height - 1, 0);
2759
+ const right = left + width;
2760
+ const bottom = top + height;
2761
+ const viewport = options?.viewportRect;
2762
+ const radius = Math.min(options?.radius ?? 8, width / 2, height / 2);
2763
+ const topLeft = viewport && isSameCoordinate(rect.x, viewport.x) && isSameCoordinate(rect.y, viewport.y) ? radius : 0;
2764
+ const topRight = viewport && isSameCoordinate(rect.x + rect.width, viewport.x + viewport.width) && isSameCoordinate(rect.y, viewport.y) ? radius : 0;
2765
+ const bottomRight = viewport && isSameCoordinate(rect.x + rect.width, viewport.x + viewport.width) && isSameCoordinate(rect.y + rect.height, viewport.y + viewport.height) ? radius : 0;
2766
+ const bottomLeft = viewport && isSameCoordinate(rect.x, viewport.x) && isSameCoordinate(rect.y + rect.height, viewport.y + viewport.height) ? radius : 0;
2767
+ if (!topLeft && !topRight && !bottomRight && !bottomLeft) {
2768
+ context.strokeRect(left, top, width, height);
2769
+ context.restore();
2770
+ return;
2771
+ }
2772
+ context.beginPath();
2773
+ context.moveTo(left + topLeft, top);
2774
+ context.lineTo(right - topRight, top);
2775
+ if (topRight) context.arcTo(right, top, right, top + topRight, topRight);
2776
+ else context.lineTo(right, top);
2777
+ context.lineTo(right, bottom - bottomRight);
2778
+ if (bottomRight) {
2779
+ context.arcTo(right, bottom, right - bottomRight, bottom, bottomRight);
2780
+ } else context.lineTo(right, bottom);
2781
+ context.lineTo(left + bottomLeft, bottom);
2782
+ if (bottomLeft) {
2783
+ context.arcTo(left, bottom, left, bottom - bottomLeft, bottomLeft);
2784
+ } else context.lineTo(left, bottom);
2785
+ context.lineTo(left, top + topLeft);
2786
+ if (topLeft) context.arcTo(left, top, left + topLeft, top, topLeft);
2787
+ else context.lineTo(left, top);
2788
+ context.closePath();
2789
+ context.stroke();
2763
2790
  context.restore();
2764
2791
  }
2792
+ function isSameCoordinate(first, second) {
2793
+ return Math.abs(first - second) < 0.01;
2794
+ }
2765
2795
  function resolveSelectionIndexes(layout, selection) {
2766
2796
  const range = selection?.ranges[0];
2767
2797
  if (range) {
@@ -2859,29 +2889,127 @@ var defaultPivotTableTheme = {
2859
2889
  };
2860
2890
 
2861
2891
  // src/rendering/draw-scrollbars.ts
2862
- function drawPivotScrollbars(context, layout, borderColor) {
2892
+ function drawPivotScrollbars(context, layout, borderColor, verticalFrame) {
2863
2893
  context.save();
2894
+ if (layout.horizontal && verticalFrame) {
2895
+ drawHorizontalScrollbarMask(
2896
+ context,
2897
+ layout.horizontal,
2898
+ verticalFrame.bodyBackgroundColor
2899
+ );
2900
+ }
2901
+ if (layout.vertical && verticalFrame) {
2902
+ drawVerticalScrollbarFrame(
2903
+ context,
2904
+ layout.vertical,
2905
+ verticalFrame,
2906
+ borderColor
2907
+ );
2908
+ }
2864
2909
  if (layout.horizontal) {
2865
2910
  drawAxis(context, layout.horizontal, "horizontal", borderColor);
2866
2911
  }
2867
2912
  if (layout.vertical) {
2868
2913
  drawAxis(context, layout.vertical, "vertical", borderColor);
2869
2914
  }
2915
+ if (layout.horizontal) {
2916
+ drawHorizontalScrollbarTopBorder(
2917
+ context,
2918
+ layout.horizontal,
2919
+ layout.vertical,
2920
+ borderColor
2921
+ );
2922
+ }
2923
+ context.restore();
2924
+ }
2925
+ function drawHorizontalScrollbarMask(context, layout, backgroundColor) {
2926
+ const left = layout.decreaseButton.x;
2927
+ const right = layout.increaseButton.x + layout.increaseButton.width;
2928
+ const top = layout.decreaseButton.y;
2929
+ const height = layout.decreaseButton.height;
2930
+ if (right <= left || height <= 0) return;
2931
+ context.save();
2932
+ context.fillStyle = backgroundColor;
2933
+ context.fillRect(left, top, right - left, height);
2934
+ context.restore();
2935
+ }
2936
+ function drawHorizontalScrollbarTopBorder(context, horizontal, vertical, borderColor) {
2937
+ const left = Math.round(horizontal.decreaseButton.x) + 0.5;
2938
+ const horizontalRight = horizontal.increaseButton.x + horizontal.increaseButton.width;
2939
+ const verticalRight = vertical ? vertical.decreaseButton.x + vertical.decreaseButton.width : horizontalRight;
2940
+ const right = Math.round(Math.max(horizontalRight, verticalRight)) - 0.5;
2941
+ const top = Math.round(horizontal.decreaseButton.y) + 0.5;
2942
+ if (right <= left) return;
2943
+ context.save();
2944
+ context.strokeStyle = borderColor;
2945
+ context.lineWidth = 1;
2946
+ context.beginPath();
2947
+ context.moveTo(left, top);
2948
+ context.lineTo(right, top);
2949
+ context.stroke();
2870
2950
  context.restore();
2871
2951
  }
2872
2952
  function drawAxis(context, layout, axis, borderColor) {
2873
- drawTrack(context, layout.track, borderColor);
2874
2953
  drawButton(context, layout.decreaseButton, axis, "decrease");
2875
2954
  drawButton(context, layout.increaseButton, axis, "increase");
2876
2955
  drawThumb(context, layout.thumb);
2956
+ drawTrackBorder(context, layout.track, axis, borderColor);
2957
+ }
2958
+ function drawTrackBorder(context, rect, axis, borderColor) {
2959
+ const left = Math.round(rect.x) + 0.5;
2960
+ const right = Math.round(rect.x + rect.width) - 0.5;
2961
+ const top = Math.round(rect.y) + 0.5;
2962
+ const bottom = Math.round(rect.y + rect.height) - 0.5;
2963
+ if (right <= left || bottom <= top) return;
2964
+ context.save();
2965
+ context.strokeStyle = borderColor;
2966
+ context.lineWidth = 1;
2967
+ context.beginPath();
2968
+ if (axis === "horizontal") {
2969
+ context.moveTo(left, top);
2970
+ context.lineTo(right, top);
2971
+ } else {
2972
+ context.moveTo(left, top);
2973
+ context.lineTo(left, bottom);
2974
+ }
2975
+ context.stroke();
2976
+ context.restore();
2877
2977
  }
2878
- function drawTrack(context, rect, borderColor) {
2879
- context.fillStyle = "rgba(148, 163, 184, 0.16)";
2978
+ function drawVerticalScrollbarFrame(context, layout, frame, borderColor) {
2979
+ const left = Math.round(layout.decreaseButton.x) + 0.5;
2980
+ const top = Math.round(frame.top) + 0.5;
2981
+ const bottom = Math.round(frame.top + frame.height) - 0.5;
2982
+ const headerBottom = Math.round(frame.headerBottom) + 0.5;
2983
+ if (bottom <= top) return;
2984
+ context.save();
2985
+ context.fillStyle = frame.headerBackgroundColor;
2986
+ context.fillRect(
2987
+ layout.decreaseButton.x,
2988
+ frame.top,
2989
+ layout.decreaseButton.width,
2990
+ Math.max(frame.headerBottom - frame.top, 0)
2991
+ );
2992
+ context.fillStyle = frame.bodyBackgroundColor;
2993
+ context.fillRect(
2994
+ layout.decreaseButton.x,
2995
+ frame.headerBottom,
2996
+ layout.decreaseButton.width,
2997
+ Math.max(frame.top + frame.height - frame.headerBottom, 0)
2998
+ );
2880
2999
  context.strokeStyle = borderColor;
2881
3000
  context.lineWidth = 1;
2882
- roundRect(context, rect, rect.height / 2);
2883
- context.fill();
3001
+ context.beginPath();
3002
+ context.moveTo(left, top);
3003
+ context.lineTo(left, bottom);
2884
3004
  context.stroke();
3005
+ if (headerBottom > top && headerBottom < bottom) {
3006
+ const right = Math.round(layout.decreaseButton.x + layout.decreaseButton.width) - 0.5;
3007
+ context.beginPath();
3008
+ context.moveTo(left, headerBottom);
3009
+ context.lineTo(right, headerBottom);
3010
+ context.stroke();
3011
+ }
3012
+ context.restore();
2885
3013
  }
2886
3014
  function drawThumb(context, rect) {
2887
3015
  context.fillStyle = "rgba(100, 116, 139, 0.8)";
@@ -2889,28 +3017,25 @@ function drawThumb(context, rect) {
2889
3017
  context.fill();
2890
3018
  }
2891
3019
  function drawButton(context, rect, axis, direction) {
2892
- context.fillStyle = "rgba(226, 232, 240, 0.92)";
2893
- context.strokeStyle = "rgba(148, 163, 184, 0.65)";
2894
- context.lineWidth = 1;
2895
- roundRect(context, rect, rect.height / 2);
2896
- context.fill();
2897
- context.stroke();
2898
3020
  context.fillStyle = "rgba(71, 85, 105, 0.88)";
2899
3021
  context.beginPath();
2900
3022
  const centerX = rect.x + rect.width / 2;
2901
3023
  const centerY = rect.y + rect.height / 2;
3024
+ const iconSize = Math.min(rect.width, rect.height) * 0.64;
3025
+ const directionOffset = iconSize * 0.4;
3026
+ const crossAxisOffset = iconSize / 2;
2902
3027
  if (axis === "horizontal") {
2903
- const tipX = centerX + (direction === "decrease" ? -1 : 1) * rect.width * 0.14;
2904
- const tailX = centerX + (direction === "decrease" ? 1 : -1) * rect.width * 0.14;
3028
+ const tipX = centerX + (direction === "decrease" ? -1 : 1) * directionOffset;
3029
+ const tailX = centerX + (direction === "decrease" ? 1 : -1) * directionOffset;
2905
3030
  context.moveTo(tipX, centerY);
2906
- context.lineTo(tailX, centerY - rect.height * 0.22);
2907
- context.lineTo(tailX, centerY + rect.height * 0.22);
3031
+ context.lineTo(tailX, centerY - crossAxisOffset);
3032
+ context.lineTo(tailX, centerY + crossAxisOffset);
2908
3033
  } else {
2909
- const tipY = centerY + (direction === "decrease" ? -1 : 1) * rect.height * 0.14;
2910
- const tailY = centerY + (direction === "decrease" ? 1 : -1) * rect.height * 0.14;
3034
+ const tipY = centerY + (direction === "decrease" ? -1 : 1) * directionOffset;
3035
+ const tailY = centerY + (direction === "decrease" ? 1 : -1) * directionOffset;
2911
3036
  context.moveTo(centerX, tipY);
2912
- context.lineTo(centerX - rect.width * 0.22, tailY);
2913
- context.lineTo(centerX + rect.width * 0.22, tailY);
3037
+ context.lineTo(centerX - crossAxisOffset, tailY);
3038
+ context.lineTo(centerX + crossAxisOffset, tailY);
2914
3039
  }
2915
3040
  context.closePath();
2916
3041
  context.fill();
@@ -2974,14 +3099,22 @@ function drawPivotViewportRightBorder(context, rect, color) {
2974
3099
  context.stroke();
2975
3100
  context.restore();
2976
3101
  }
2977
- function drawPivotViewportBottomBorder(context, rect, color) {
2978
- const y = rect.y + rect.height - 0.5;
3102
+ function clipPivotViewport(context, rect, radius = 8) {
3103
+ appendRoundedRectPath(context, rect, radius);
3104
+ context.clip();
3105
+ }
3106
+ function drawPivotViewportBorder(context, rect, color, radius = 8) {
3107
+ const borderRect = {
3108
+ x: rect.x + 0.5,
3109
+ y: rect.y + 0.5,
3110
+ width: Math.max(rect.width - 1, 0),
3111
+ height: Math.max(rect.height - 1, 0)
3112
+ };
3113
+ if (borderRect.width <= 0 || borderRect.height <= 0) return;
2979
3114
  context.save();
2980
3115
  context.strokeStyle = color;
2981
3116
  context.lineWidth = 1;
2982
- context.beginPath();
2983
- context.moveTo(rect.x, y);
2984
- context.lineTo(rect.x + rect.width, y);
3117
+ appendRoundedRectPath(context, borderRect, radius);
2985
3118
  context.stroke();
2986
3119
  context.restore();
2987
3120
  }
@@ -2999,6 +3132,21 @@ function drawPivotHeaderBoundaries(context, options, color) {
2999
3132
  context.stroke();
3000
3133
  context.restore();
3001
3134
  }
3135
+ function appendRoundedRectPath(context, rect, radius) {
3136
+ const nextRadius = Math.max(
3137
+ Math.min(radius, rect.width / 2, rect.height / 2),
3138
+ 0
3139
+ );
3140
+ const right = rect.x + rect.width;
3141
+ const bottom = rect.y + rect.height;
3142
+ context.beginPath();
3143
+ context.moveTo(rect.x + nextRadius, rect.y);
3144
+ context.arcTo(right, rect.y, right, bottom, nextRadius);
3145
+ context.arcTo(right, bottom, rect.x, bottom, nextRadius);
3146
+ context.arcTo(rect.x, bottom, rect.x, rect.y, nextRadius);
3147
+ context.arcTo(rect.x, rect.y, right, rect.y, nextRadius);
3148
+ context.closePath();
3149
+ }
3002
3150
 
3003
3151
  // src/domain/layout/sticky-row-header.ts
3004
3152
  function resolveStickyRowHeaderContentRect(params) {
@@ -3035,7 +3183,21 @@ function renderPivotTable(params) {
3035
3183
  activeNodePathKey
3036
3184
  );
3037
3185
  const areaSelection = isPivotAreaSelection(layout, params.selection);
3186
+ const viewportWidth = resolvePivotViewportWidth(layout);
3187
+ const tableViewportRect = {
3188
+ x: 0,
3189
+ y: 0,
3190
+ width: layout.tableWidth,
3191
+ height: layout.height
3192
+ };
3038
3193
  context.clearRect(0, 0, layout.width, layout.height);
3194
+ context.save();
3195
+ clipPivotViewport(context, {
3196
+ x: 0,
3197
+ y: 0,
3198
+ width: viewportWidth,
3199
+ height: layout.height
3200
+ });
3039
3201
  context.font = `${theme.fontSize}px ${theme.fontFamily}`;
3040
3202
  context.textBaseline = "middle";
3041
3203
  withClip(context, layout.reportFilterRect, () => {
@@ -3068,7 +3230,7 @@ function renderPivotTable(params) {
3068
3230
  (cell) => drawPivotGridRect(context, cell.rect, theme.borderColor, {
3069
3231
  skipTop: true,
3070
3232
  skipLeft: cell.rect.x > 0,
3071
- skipRight: isSameCoordinate(
3233
+ skipRight: isSameCoordinate2(
3072
3234
  cell.rect.x + cell.rect.width,
3073
3235
  layout.rowHeaderRect.x + layout.rowHeaderRect.width
3074
3236
  )
@@ -3091,11 +3253,11 @@ function renderPivotTable(params) {
3091
3253
  (cell) => drawPivotGridRect(context, cell.rect, theme.borderColor, {
3092
3254
  skipTop: cell.rect.y > 0,
3093
3255
  skipLeft: true,
3094
- skipRight: isSameCoordinate(
3256
+ skipRight: isSameCoordinate2(
3095
3257
  cell.rect.x + cell.rect.width,
3096
3258
  layout.tableWidth
3097
3259
  ),
3098
- skipBottom: isSameCoordinate(
3260
+ skipBottom: isSameCoordinate2(
3099
3261
  cell.rect.y + cell.rect.height,
3100
3262
  layout.columnHeaderRect.y + layout.columnHeaderRect.height
3101
3263
  )
@@ -3127,7 +3289,7 @@ function renderPivotTable(params) {
3127
3289
  (cell) => drawPivotGridRect(context, cell.rect, theme.borderColor, {
3128
3290
  skipTop: true,
3129
3291
  skipLeft: true,
3130
- skipRight: isSameCoordinate(
3292
+ skipRight: isSameCoordinate2(
3131
3293
  cell.rect.x + cell.rect.width,
3132
3294
  layout.tableWidth
3133
3295
  )
@@ -3149,15 +3311,24 @@ function renderPivotTable(params) {
3149
3311
  theme.borderColor
3150
3312
  );
3151
3313
  if (options.debug) drawDebugOverlay(context, layout, theme);
3152
- drawPivotScrollbars(context, layout.scrollbar, theme.borderColor);
3153
- drawPivotViewportRightBorder(
3154
- context,
3155
- { x: 0, y: 0, width: layout.tableWidth, height: contentHeight },
3156
- theme.borderColor
3157
- );
3158
- drawPivotViewportBottomBorder(
3314
+ drawPivotScrollbars(context, layout.scrollbar, theme.borderColor, {
3315
+ top: 0,
3316
+ height: contentHeight,
3317
+ headerBottom: layout.bodyRect.y,
3318
+ headerBackgroundColor: theme.columnHeaderBackgroundColor,
3319
+ bodyBackgroundColor: theme.backgroundColor
3320
+ });
3321
+ if (params.resizeGuideX != null) {
3322
+ drawPivotResizeGuide(context, params.resizeGuideX, contentHeight);
3323
+ }
3324
+ context.restore();
3325
+ context.save();
3326
+ clipPivotViewport(context, tableViewportRect);
3327
+ drawPivotViewportRightBorder(context, tableViewportRect, theme.borderColor);
3328
+ context.restore();
3329
+ drawPivotViewportBorder(
3159
3330
  context,
3160
- { x: 0, y: 0, width: layout.tableWidth, height: contentHeight },
3331
+ { x: 0, y: 0, width: viewportWidth, height: contentHeight },
3161
3332
  theme.borderColor
3162
3333
  );
3163
3334
  const selectionRect = resolvePivotSelectionRect(layout, params.selection);
@@ -3166,22 +3337,28 @@ function renderPivotTable(params) {
3166
3337
  layout,
3167
3338
  params.selection
3168
3339
  );
3340
+ context.save();
3341
+ clipPivotViewport(context, tableViewportRect);
3169
3342
  withClip(context, selectionClipRect, () => {
3170
3343
  drawPivotSelectionBorder(
3171
3344
  context,
3172
3345
  selectionRect,
3173
- theme.selectionBorderColor
3346
+ theme.selectionBorderColor,
3347
+ { viewportRect: tableViewportRect }
3174
3348
  );
3175
3349
  });
3176
- }
3177
- if (params.resizeGuideX != null) {
3178
- drawPivotResizeGuide(context, params.resizeGuideX, contentHeight);
3350
+ context.restore();
3179
3351
  }
3180
3352
  return {
3181
3353
  headerCellCount: layout.reportFilterCells.length + layout.rowFieldHeaderCells.length + layout.rowHeaderCells.length + layout.columnHeaderCells.length,
3182
3354
  bodyCellCount: layout.bodyCells.length
3183
3355
  };
3184
3356
  }
3357
+ function resolvePivotViewportWidth(layout) {
3358
+ const horizontalRight = layout.scrollbar.horizontal ? layout.scrollbar.horizontal.increaseButton.x + layout.scrollbar.horizontal.increaseButton.width : 0;
3359
+ const verticalRight = layout.scrollbar.vertical ? layout.scrollbar.vertical.decreaseButton.x + layout.scrollbar.vertical.decreaseButton.width : 0;
3360
+ return Math.max(layout.tableWidth, horizontalRight, verticalRight);
3361
+ }
3185
3362
  function drawPivotResizeGuide(context, x, height) {
3186
3363
  context.save();
3187
3364
  context.strokeStyle = "#1677ff";
@@ -3216,7 +3393,7 @@ function drawDebugOverlay(context, layout, theme) {
3216
3393
  layout.bodyRect
3217
3394
  ].forEach((rect) => strokeRect(context, rect, "#f04438"));
3218
3395
  context.setLineDash([]);
3219
- context.font = `12px ${theme.fontFamily}`;
3396
+ context.font = `${theme.fontSize}px ${theme.fontFamily}`;
3220
3397
  context.textAlign = "left";
3221
3398
  context.textBaseline = "top";
3222
3399
  context.fillStyle = "#f04438";
@@ -3475,7 +3652,7 @@ function resolveSlotPathKey(slots, nodeId, indicatorKey) {
3475
3652
  (slot) => (slot.nodeId ?? "") === nodeId && (!slot.indicatorKey || slot.indicatorKey === indicatorKey)
3476
3653
  )?.pathKey ?? null;
3477
3654
  }
3478
- function isSameCoordinate(first, second) {
3655
+ function isSameCoordinate2(first, second) {
3479
3656
  return Math.abs(first - second) < 0.01;
3480
3657
  }
3481
3658
  function resolveBodyDisplayText(cell, indicator) {
@@ -3935,6 +4112,16 @@ function hitTestPivotScrollbar(layout, x, y) {
3935
4112
  }
3936
4113
  return null;
3937
4114
  }
4115
+ function resolvePivotScrollbarTrackScroll(params) {
4116
+ const movableLength = Math.max(params.trackLength - params.thumbLength, 0);
4117
+ if (movableLength === 0) return 0;
4118
+ const thumbStart = clamp(
4119
+ params.pointerValue - params.thumbLength / 2,
4120
+ params.trackStart,
4121
+ params.trackStart + movableLength
4122
+ );
4123
+ return (thumbStart - params.trackStart) / movableLength * params.maxScroll;
4124
+ }
3938
4125
  function resolveRole(layout, x, y) {
3939
4126
  if (isPointInRect(x, y, layout.thumb)) return "thumb";
3940
4127
  if (isPointInRect(x, y, layout.decreaseButton)) return "decrease-button";
@@ -4101,6 +4288,8 @@ var PivotTable = class {
4101
4288
  __publicField(this, "wheelScrollTarget", null);
4102
4289
  __publicField(this, "wheelScrollFrameId", null);
4103
4290
  __publicField(this, "wheelScrollLastTime", 0);
4291
+ __publicField(this, "continuousScrollTimer", null);
4292
+ __publicField(this, "continuousScrollPointerId", null);
4104
4293
  __publicField(this, "resizeSession", null);
4105
4294
  __publicField(this, "resizeGuideX", null);
4106
4295
  __publicField(this, "suppressNextClick", false);
@@ -4179,6 +4368,7 @@ var PivotTable = class {
4179
4368
  this.scheduleRender();
4180
4369
  });
4181
4370
  __publicField(this, "handleClick", (event) => {
4371
+ this.options.ui?.onContextMenuRequest?.(null);
4182
4372
  if (this.suppressNextClick) {
4183
4373
  this.suppressNextClick = false;
4184
4374
  return;
@@ -4497,6 +4687,8 @@ var PivotTable = class {
4497
4687
  });
4498
4688
  __publicField(this, "handlePointerUp", (event) => {
4499
4689
  if (!this.host) return;
4690
+ const endedContinuousScroll = this.continuousScrollPointerId === event.pointerId;
4691
+ if (endedContinuousScroll) this.stopContinuousScrollbarScroll();
4500
4692
  const endedCellSelection = this.cellSelectionDrag?.pointerId === event.pointerId;
4501
4693
  if (endedCellSelection) this.cellSelectionDrag = null;
4502
4694
  if (this.textSelectionDrag?.pointerId === event.pointerId) {
@@ -4515,7 +4707,7 @@ var PivotTable = class {
4515
4707
  if (this.scrollbarDrag?.pointerId === event.pointerId) {
4516
4708
  this.scrollbarDrag = null;
4517
4709
  }
4518
- if (!this.resizeSession && !this.scrollbarDrag && !endedCellSelection && !endedRowHeaderSelection && !this.host.root.hasPointerCapture(event.pointerId))
4710
+ if (!this.resizeSession && !this.scrollbarDrag && !endedContinuousScroll && !endedCellSelection && !endedRowHeaderSelection && !this.host.root.hasPointerCapture(event.pointerId))
4519
4711
  return;
4520
4712
  if (this.host.root.hasPointerCapture(event.pointerId)) {
4521
4713
  this.host.root.releasePointerCapture(event.pointerId);
@@ -4761,6 +4953,7 @@ var PivotTable = class {
4761
4953
  if (this.frameId !== null) cancelAnimationFrame(this.frameId);
4762
4954
  this.frameId = null;
4763
4955
  this.stopWheelScrollSmoothing();
4956
+ this.stopContinuousScrollbarScroll();
4764
4957
  this.stopResizeObserver?.();
4765
4958
  this.stopResizeObserver = null;
4766
4959
  this.host?.root.removeEventListener("pointermove", this.handlePointerMove);
@@ -5066,7 +5259,7 @@ var PivotTable = class {
5066
5259
  measurePivotText(text) {
5067
5260
  const context = this.host?.canvas.getContext("2d");
5068
5261
  if (!context) return text.length * 8;
5069
- context.font = `${this.options.theme?.fontSize ?? 13}px ${this.options.theme?.fontFamily ?? "sans-serif"}`;
5262
+ context.font = `${this.options.theme?.fontSize ?? 14}px ${this.options.theme?.fontFamily ?? "sans-serif"}`;
5070
5263
  return context.measureText(text).width;
5071
5264
  }
5072
5265
  updateRowHeaderSelectionDrag(event) {
@@ -5129,17 +5322,27 @@ var PivotTable = class {
5129
5322
  event.preventDefault();
5130
5323
  this.suppressNextClick = true;
5131
5324
  if (hit.role === "decrease-button" || hit.role === "increase-button") {
5132
- this.stepScrollbar(hit.axis, hit.role === "decrease-button" ? -1 : 1);
5325
+ this.startContinuousScrollbarScroll(
5326
+ hit.axis,
5327
+ hit.role === "decrease-button" ? -1 : 1,
5328
+ event.pointerId
5329
+ );
5330
+ this.host.root.setPointerCapture(event.pointerId);
5133
5331
  return;
5134
5332
  }
5135
5333
  const rootRect = this.host.root.getBoundingClientRect();
5136
5334
  const pointer = hit.axis === "horizontal" ? event.clientX - rootRect.left : event.clientY - rootRect.top;
5137
5335
  if (hit.role === "track") {
5138
- const thumbStart2 = hit.axis === "horizontal" ? axisLayout.thumb.x : axisLayout.thumb.y;
5139
- const direction = pointer < thumbStart2 ? -1 : 1;
5336
+ const nextScroll = resolvePivotScrollbarTrackScroll({
5337
+ pointerValue: pointer,
5338
+ trackStart: hit.axis === "horizontal" ? axisLayout.track.x : axisLayout.track.y,
5339
+ trackLength: hit.axis === "horizontal" ? axisLayout.track.width : axisLayout.track.height,
5340
+ thumbLength: hit.axis === "horizontal" ? axisLayout.thumb.width : axisLayout.thumb.height,
5341
+ maxScroll: hit.axis === "horizontal" ? this.layout.viewport.maxScrollLeft : this.layout.viewport.maxScrollTop
5342
+ });
5140
5343
  this.applyScrollPosition(
5141
- this.scroll.left + (hit.axis === "horizontal" ? direction * this.layout.bodyRect.width : 0),
5142
- this.scroll.top + (hit.axis === "vertical" ? direction * this.layout.bodyRect.height : 0)
5344
+ hit.axis === "horizontal" ? nextScroll : this.scroll.left,
5345
+ hit.axis === "vertical" ? nextScroll : this.scroll.top
5143
5346
  );
5144
5347
  return;
5145
5348
  }
@@ -5183,6 +5386,21 @@ var PivotTable = class {
5183
5386
  this.scroll.top + (axis === "vertical" ? direction * this.layout.rowHeight : 0)
5184
5387
  );
5185
5388
  }
5389
+ startContinuousScrollbarScroll(axis, direction, pointerId) {
5390
+ this.stopContinuousScrollbarScroll();
5391
+ this.continuousScrollPointerId = pointerId;
5392
+ this.stepScrollbar(axis, direction);
5393
+ this.continuousScrollTimer = setInterval(() => {
5394
+ this.stepScrollbar(axis, direction);
5395
+ }, 80);
5396
+ }
5397
+ stopContinuousScrollbarScroll() {
5398
+ if (this.continuousScrollTimer !== null) {
5399
+ clearInterval(this.continuousScrollTimer);
5400
+ this.continuousScrollTimer = null;
5401
+ }
5402
+ this.continuousScrollPointerId = null;
5403
+ }
5186
5404
  applyScrollPosition(left, top) {
5187
5405
  if (!this.layout) return;
5188
5406
  const next = {
@@ -5427,6 +5645,90 @@ function toArrayBuffer(value) {
5427
5645
  return buffer;
5428
5646
  }
5429
5647
 
5648
+ // src/adapters/storage/indexeddb-field-layout-storage.ts
5649
+ var DATABASE_NAME = "canvas_components_pivot_table_config";
5650
+ var STORE_NAME = "field_layout";
5651
+ var DB_VERSION = 1;
5652
+ var dbPromise = null;
5653
+ async function savePivotFieldLayout(key, layout) {
5654
+ try {
5655
+ const db = await openDatabase();
5656
+ await requestToPromise(
5657
+ db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).put(cloneLayout(layout), key)
5658
+ );
5659
+ } catch (error) {
5660
+ console.error("[PivotTable] \u5B57\u6BB5\u5E03\u5C40\u4FDD\u5B58\u5931\u8D25\uFF1A", error);
5661
+ }
5662
+ }
5663
+ async function loadPivotFieldLayout(key) {
5664
+ try {
5665
+ const db = await openDatabase();
5666
+ const value = await requestToPromise(
5667
+ db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(key)
5668
+ );
5669
+ return isPivotFieldLayout(value) ? cloneLayout(value) : void 0;
5670
+ } catch (error) {
5671
+ console.error("[PivotTable] \u5B57\u6BB5\u5E03\u5C40\u8BFB\u53D6\u5931\u8D25\uFF1A", error);
5672
+ return void 0;
5673
+ }
5674
+ }
5675
+ async function clearPivotFieldLayout(key) {
5676
+ try {
5677
+ const db = await openDatabase();
5678
+ await requestToPromise(
5679
+ db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME).delete(key)
5680
+ );
5681
+ } catch (error) {
5682
+ console.error("[PivotTable] \u5B57\u6BB5\u5E03\u5C40\u6E05\u9664\u5931\u8D25\uFF1A", error);
5683
+ }
5684
+ }
5685
+ function openDatabase() {
5686
+ if (dbPromise) return dbPromise;
5687
+ dbPromise = new Promise((resolve, reject) => {
5688
+ if (typeof indexedDB === "undefined") {
5689
+ reject(new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 IndexedDB"));
5690
+ return;
5691
+ }
5692
+ const request = indexedDB.open(DATABASE_NAME, DB_VERSION);
5693
+ request.onupgradeneeded = () => {
5694
+ const db = request.result;
5695
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
5696
+ db.createObjectStore(STORE_NAME);
5697
+ }
5698
+ };
5699
+ request.onsuccess = () => resolve(request.result);
5700
+ request.onerror = () => reject(request.error);
5701
+ });
5702
+ dbPromise.catch(() => {
5703
+ dbPromise = null;
5704
+ });
5705
+ return dbPromise;
5706
+ }
5707
+ function requestToPromise(request) {
5708
+ return new Promise((resolve, reject) => {
5709
+ request.onsuccess = () => resolve(request.result);
5710
+ request.onerror = () => reject(request.error);
5711
+ });
5712
+ }
5713
+ function isPivotFieldLayout(value) {
5714
+ if (!value || typeof value !== "object") return false;
5715
+ const layout = value;
5716
+ return ["filters", "columns", "rows", "values"].every(
5717
+ (key) => isStringArray(layout[key])
5718
+ );
5719
+ }
5720
+ function isStringArray(value) {
5721
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
5722
+ }
5723
+ function cloneLayout(layout) {
5724
+ return {
5725
+ filters: [...layout.filters],
5726
+ columns: [...layout.columns],
5727
+ rows: [...layout.rows],
5728
+ values: [...layout.values]
5729
+ };
5730
+ }
5731
+
5430
5732
  // src/domain/field/pivot-field-layout.ts
5431
5733
  function createEmptyPivotFieldLayout() {
5432
5734
  return { filters: [], columns: [], rows: [], values: [] };
@@ -5574,6 +5876,6 @@ function areaToLayoutKey(area) {
5574
5876
  return "values";
5575
5877
  }
5576
5878
 
5577
- export { PivotTable, aggregatePivotRecords, buildPivotDimensionTree, buildPivotLayoutSnapshot, computePivotScrollbarLayout, createBuiltInPivotAggregators, createEmptyPivotFieldLayout, createEmptyPivotFilterState, createPivotAggregatorRegistry, createPivotAxisSlots, createPivotCellKey, createPivotExportMatrix, createPivotFieldCatalog, createPivotFieldLayout, createPivotSelectionText, defaultPivotTableTheme, encodePivotDimensionValue, encodePivotPath, exportPivotMatrixToCsv, exportPivotMatrixToXlsx, filterPivotRecords, findPivotFieldArea, flattenVisiblePivotLeaves, getPivotDimensionFilterOptions, hitTestPivotLayout, hitTestPivotResize, hitTestPivotScrollbar, movePivotField, removePivotField, renderPivotTable, resolvePivotDefaultSorts, resolvePivotDimensionKey, resolvePivotDimensionTitle, resolvePivotDimensionValue, resolvePivotDimensionWidth, resolvePivotFieldLayoutOptions, resolvePivotIndicatorAggregator, resolvePivotValueFieldTitle };
5879
+ export { PivotTable, aggregatePivotRecords, buildPivotDimensionTree, buildPivotLayoutSnapshot, clearPivotFieldLayout, computePivotScrollbarLayout, createBuiltInPivotAggregators, createEmptyPivotFieldLayout, createEmptyPivotFilterState, createPivotAggregatorRegistry, createPivotAxisSlots, createPivotCellKey, createPivotExportMatrix, createPivotFieldCatalog, createPivotFieldLayout, createPivotSelectionText, defaultPivotTableTheme, encodePivotDimensionValue, encodePivotPath, exportPivotMatrixToCsv, exportPivotMatrixToXlsx, filterPivotRecords, findPivotFieldArea, flattenVisiblePivotLeaves, getPivotDimensionFilterOptions, hitTestPivotLayout, hitTestPivotResize, hitTestPivotScrollbar, loadPivotFieldLayout, movePivotField, removePivotField, renderPivotTable, resolvePivotDefaultSorts, resolvePivotDimensionKey, resolvePivotDimensionTitle, resolvePivotDimensionValue, resolvePivotDimensionWidth, resolvePivotFieldLayoutOptions, resolvePivotIndicatorAggregator, resolvePivotValueFieldTitle, savePivotFieldLayout };
5578
5880
  //# sourceMappingURL=index.js.map
5579
5881
  //# sourceMappingURL=index.js.map