argent-grid 0.2.0 → 0.3.0

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.
Files changed (55) hide show
  1. package/AGENTS.md +70 -27
  2. package/e2e/advanced.spec.ts +1 -1
  3. package/e2e/benchmark.spec.ts +7 -7
  4. package/e2e/cell-renderers.spec.ts +152 -0
  5. package/e2e/debug-streaming.spec.ts +31 -0
  6. package/e2e/dnd.spec.ts +73 -0
  7. package/e2e/screenshots.spec.ts +1 -1
  8. package/e2e/visual.spec.ts +30 -9
  9. package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
  10. package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
  11. package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
  12. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  13. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  14. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  15. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  16. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  17. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  18. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  19. package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
  20. package/package.json +5 -5
  21. package/plan.md +30 -34
  22. package/src/lib/components/argent-grid.component.css +258 -549
  23. package/src/lib/components/argent-grid.component.html +272 -306
  24. package/src/lib/components/argent-grid.component.ts +585 -135
  25. package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
  26. package/src/lib/components/argent-grid.selection.spec.ts +2 -2
  27. package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
  28. package/src/lib/components/set-filter/set-filter.component.ts +7 -2
  29. package/src/lib/rendering/canvas-renderer.spec.ts +148 -1
  30. package/src/lib/rendering/canvas-renderer.ts +177 -286
  31. package/src/lib/rendering/render/cells.ts +122 -5
  32. package/src/lib/rendering/render/column-utils.ts +27 -5
  33. package/src/lib/rendering/render/hit-test.ts +6 -11
  34. package/src/lib/rendering/render/index.ts +15 -6
  35. package/src/lib/rendering/render/lines.ts +12 -6
  36. package/src/lib/rendering/render/primitives.ts +269 -7
  37. package/src/lib/rendering/render/types.ts +2 -1
  38. package/src/lib/rendering/render/walk.ts +39 -19
  39. package/src/lib/services/grid.service.spec.ts +76 -0
  40. package/src/lib/services/grid.service.ts +451 -114
  41. package/src/lib/themes/theme-quartz.ts +2 -2
  42. package/src/lib/types/ag-grid-types.ts +500 -0
  43. package/src/stories/Advanced.stories.ts +78 -17
  44. package/src/stories/ArgentGrid.stories.ts +50 -26
  45. package/src/stories/Benchmark.stories.ts +17 -15
  46. package/src/stories/CellRenderers.stories.ts +205 -31
  47. package/src/stories/Filtering.stories.ts +56 -16
  48. package/src/stories/Grouping.stories.ts +86 -13
  49. package/src/stories/Streaming.stories.ts +57 -0
  50. package/src/stories/Theming.stories.ts +23 -10
  51. package/src/stories/Tooltips.stories.ts +381 -0
  52. package/src/stories/benchmark-wrapper.component.ts +69 -29
  53. package/src/stories/story-utils.ts +88 -0
  54. package/src/stories/streaming-wrapper.component.ts +441 -0
  55. package/tsconfig.json +1 -0
@@ -5,6 +5,14 @@
5
5
  */
6
6
 
7
7
  import { ColDef, Column, GridApi, IRowNode } from '../../types/ag-grid-types';
8
+ import {
9
+ drawBadge,
10
+ drawButton,
11
+ drawCheckbox,
12
+ drawProgressBar,
13
+ drawRating,
14
+ drawSparkline,
15
+ } from './primitives';
8
16
  import { getFontFromTheme } from './theme';
9
17
  import { CellDrawContext, ColumnPrepResult, GridTheme } from './types';
10
18
 
@@ -109,23 +117,92 @@ export function drawCellBackground<TData = any>(
109
117
  }
110
118
 
111
119
  /**
112
- * Draw cell content (text)
120
+ * Draw cell content (text or specialized renderer)
113
121
  */
114
122
  export function drawCellContent<TData = any>(
115
123
  ctx: CanvasRenderingContext2D,
116
124
  _prep: ColumnPrepResult<TData>,
117
125
  context: CellDrawContext<TData>
118
126
  ): void {
119
- const { x, y, width, height, formattedValue, theme } = context;
127
+ const { x, y, width, height, value, formattedValue, theme, colDef, rowNode, api } = context;
120
128
 
129
+ // 1. Check for dedicated checkbox renderer or internal selection column
130
+ if (colDef?.cellRenderer === 'checkbox' || context.column.colId === 'ag-Grid-SelectionColumn') {
131
+ const isChecked = colDef?.cellRenderer === 'checkbox' ? !!value : !!rowNode?.selected;
132
+ const size = 14;
133
+ const bx = Math.floor(x + (width - size) / 2);
134
+ const by = Math.floor(y + (height - size) / 2);
135
+
136
+ drawCheckbox(ctx, bx, by, size, isChecked, theme);
137
+ return; // Dedicated checkbox column only shows checkbox
138
+ }
139
+
140
+ // 2. Check for sparkline
141
+ if (colDef?.sparklineOptions) {
142
+ drawSparkline(ctx, value, x, y, width, height, colDef.sparklineOptions);
143
+ return;
144
+ }
145
+
146
+ // 3. Check for progress bar
147
+ if (colDef?.progressOptions) {
148
+ drawProgressBar(ctx, Number(value), x, y, width, height, colDef.progressOptions);
149
+ return;
150
+ }
151
+
152
+ // 4. Check for badge
153
+ if (colDef?.badgeOptions) {
154
+ drawBadge(ctx, String(value ?? ''), x, y, width, height, colDef.badgeOptions);
155
+ return;
156
+ }
157
+
158
+ // 5. Check for button
159
+ if (colDef?.buttonOptions) {
160
+ const opts = colDef.buttonOptions;
161
+ const label =
162
+ typeof opts.label === 'function'
163
+ ? opts.label({
164
+ value,
165
+ data: rowNode?.data,
166
+ node: rowNode!,
167
+ colDef: colDef!,
168
+ api: api!,
169
+ })
170
+ : opts.label;
171
+ drawButton(ctx, label, x, y, width, height, opts);
172
+ return;
173
+ }
174
+
175
+ // 6. Check for rating
176
+ if (colDef?.cellRenderer === 'rating' || colDef?.ratingOptions) {
177
+ drawRating(ctx, Number(value), x, y, width, height, colDef?.ratingOptions);
178
+ return;
179
+ }
180
+
181
+ // 7. Default: Text rendering
121
182
  if (!formattedValue) return;
122
183
 
123
184
  // Calculate text position with padding
124
185
  const textX = x + theme.cellPadding;
125
186
  const textY = y + height / 2; // Centered vertically
126
187
 
188
+ // Handle cellStyle color
189
+ let textColor = theme.textCell;
190
+ if (colDef?.cellStyle) {
191
+ const style =
192
+ typeof colDef.cellStyle === 'function'
193
+ ? colDef.cellStyle({
194
+ value,
195
+ data: rowNode?.data,
196
+ node: rowNode!,
197
+ column: context.column,
198
+ api: api!,
199
+ })
200
+ : colDef.cellStyle;
201
+ if (style?.color) textColor = style.color;
202
+ }
203
+
127
204
  // Set text properties
128
- ctx.fillStyle = theme.textCell;
205
+ ctx.fillStyle = textColor;
129
206
  ctx.textBaseline = 'middle';
130
207
 
131
208
  // Truncate text if needed
@@ -282,7 +359,10 @@ export function calculateColumnWidth<TData = any>(
282
359
  */
283
360
  export function stripHtmlTags(html: string): string {
284
361
  if (!html) return '';
285
- return html.replace(/<[^>]*>/g, '');
362
+ return html
363
+ .replace(/<[^>]*>/g, '')
364
+ .replace(/\s+/g, ' ')
365
+ .trim();
286
366
  }
287
367
 
288
368
  export function getFormattedValue<TData = any>(
@@ -339,6 +419,42 @@ export function getFormattedValue<TData = any>(
339
419
  // BATCH CELL RENDERING
340
420
  // ============================================================================
341
421
 
422
+ /**
423
+ * Get the value for a cell, respecting valueGetter if present
424
+ */
425
+ export function getCellValue<TData = any>(
426
+ column: Column,
427
+ colDef: ColDef<TData> | null,
428
+ rowNode: IRowNode<TData>,
429
+ api: GridApi<TData>
430
+ ): any {
431
+ // 1. Prioritize valueGetter
432
+ if (colDef?.valueGetter) {
433
+ if (typeof colDef.valueGetter === 'function') {
434
+ try {
435
+ return colDef.valueGetter({
436
+ data: rowNode.data,
437
+ node: rowNode,
438
+ colDef,
439
+ api,
440
+ column,
441
+ context: api.getGridOption('context'),
442
+ } as any);
443
+ } catch (e) {
444
+ console.warn('Value getter error:', e);
445
+ }
446
+ }
447
+ // Note: String expressions for valueGetter are not supported in the canvas renderer yet
448
+ }
449
+
450
+ // 2. Fallback to field
451
+ if (column.field) {
452
+ return getValueByPath(rowNode.data, column.field);
453
+ }
454
+
455
+ return undefined;
456
+ }
457
+
342
458
  /**
343
459
  * Render all cells in a row
344
460
  */
@@ -365,7 +481,7 @@ export function renderRow<TData = any>(
365
481
  if (!prep) continue;
366
482
 
367
483
  const x = getCellX(column);
368
- const value = column.field ? getValueByPath(rowNode.data, column.field) : undefined;
484
+ const value = getCellValue(column, prep.colDef, rowNode, api);
369
485
  const formattedValue = getFormattedValue(value, prep.colDef, rowNode.data, rowNode, api);
370
486
 
371
487
  const context: CellDrawContext<TData> = {
@@ -384,6 +500,7 @@ export function renderRow<TData = any>(
384
500
  isSelected: options.isSelected || rowNode.selected,
385
501
  isHovered: options.isHovered || false,
386
502
  isEvenRow,
503
+ api,
387
504
  };
388
505
 
389
506
  drawCell(ctx, prep, context);
@@ -4,7 +4,29 @@
4
4
  * Helper functions for column management and definition lookup.
5
5
  */
6
6
 
7
- import { ColDef, Column, GridApi } from '../../types/ag-grid-types';
7
+ import { ColDef, Column, ColumnGroup, GridApi } from '../../types/ag-grid-types';
8
+
9
+ /**
10
+ * Check if a column or column group is visible, respecting columnGroupShow
11
+ */
12
+ export function isColumnVisible(item: Column | ColumnGroup): boolean {
13
+ if (item.columnGroupShow) {
14
+ const parent = item.parent;
15
+ if (parent) {
16
+ if (item.columnGroupShow === 'open') {
17
+ return parent.expanded;
18
+ }
19
+ if (item.columnGroupShow === 'closed') {
20
+ return !parent.expanded;
21
+ }
22
+ }
23
+ }
24
+
25
+ if ('children' in item) {
26
+ return item.children.some((child) => isColumnVisible(child));
27
+ }
28
+ return item.visible;
29
+ }
8
30
 
9
31
  /**
10
32
  * Find the Column Definition for a given Column
@@ -60,14 +82,14 @@ export function getColumnX(
60
82
 
61
83
  // Adjust for pinned columns and scroll position
62
84
  if (targetCol.pinned === 'left') {
63
- return baseX;
85
+ return Math.floor(baseX);
64
86
  } else if (targetCol.pinned === 'right') {
65
87
  // When right-pinned, we need to know the offset from the right edge
66
88
  // Our positions are accumulated from left to right.
67
89
  // We need to find where the right-pinned section starts.
68
- const rightPinnedStartX = viewportWidth - rightPinnedWidth;
69
- return rightPinnedStartX + (baseX - (viewportWidth - rightPinnedWidth));
90
+ const rightPinnedStartX = Math.floor(viewportWidth - rightPinnedWidth);
91
+ return rightPinnedStartX + Math.floor(baseX - (viewportWidth - rightPinnedWidth));
70
92
  } else {
71
- return leftPinnedWidth - scrollLeft + (baseX - leftPinnedWidth);
93
+ return Math.floor(leftPinnedWidth - scrollLeft + (baseX - leftPinnedWidth));
72
94
  }
73
95
  }
@@ -5,17 +5,9 @@
5
5
  */
6
6
 
7
7
  import { Column } from '../../types/ag-grid-types';
8
+ import { HitTestResult } from './types';
8
9
  import { getColumnAtX, getRowAtY } from './walk';
9
10
 
10
- /**
11
- * Result of a grid hit test
12
- */
13
- export interface HitTestResult {
14
- rowIndex: number;
15
- columnIndex: number;
16
- column: Column | null;
17
- }
18
-
19
11
  /**
20
12
  * Perform a hit test on the grid
21
13
  */
@@ -26,18 +18,21 @@ export function performHitTest(
26
18
  scrollTop: number,
27
19
  scrollLeft: number,
28
20
  viewportWidth: number,
29
- columns: Column[]
21
+ columns: Column[],
22
+ availableWidth?: number
30
23
  ): HitTestResult {
31
24
  // Use walker utility for row detection
32
25
  const rowIndex = getRowAtY(canvasY, rowHeight, scrollTop);
33
26
 
34
27
  // Use walker utility for column detection
35
- const result = getColumnAtX(columns, canvasX, scrollLeft, viewportWidth);
28
+ const result = getColumnAtX(columns, canvasX, scrollLeft, viewportWidth, availableWidth);
36
29
 
37
30
  return {
38
31
  rowIndex,
39
32
  columnIndex: result.index,
40
33
  column: result.column,
34
+ rowNode: null, // Should be populated by caller if needed
35
+ hitArea: result.column ? 'cell' : 'empty',
41
36
  };
42
37
  }
43
38
 
@@ -6,12 +6,6 @@
6
6
 
7
7
  // Live data optimizations
8
8
  export { LiveDataOptimizations } from '../live-data-optimizations';
9
- // Rendering primitives
10
- export { drawCheckbox, drawGroupIndicator, drawSparkline } from './primitives';
11
- // Hit testing
12
- export * from './hit-test';
13
- // Column utilities
14
- export * from './column-utils';
15
9
  // Blitting optimization
16
10
  export {
17
11
  BlitState,
@@ -32,6 +26,7 @@ export {
32
26
  drawCellBackground,
33
27
  drawCellContent,
34
28
  drawGroupIndicators,
29
+ getCellValue,
35
30
  getFormattedValue,
36
31
  getValueByPath,
37
32
  measureText,
@@ -40,6 +35,10 @@ export {
40
35
  renderRow,
41
36
  truncateText,
42
37
  } from './cells';
38
+ // Column utilities
39
+ export * from './column-utils';
40
+ // Hit testing
41
+ export * from './hit-test';
43
42
  // Grid lines
44
43
  export {
45
44
  drawBorder,
@@ -55,6 +54,16 @@ export {
55
54
  drawVerticalLine,
56
55
  getColumnBorderPositions,
57
56
  } from './lines';
57
+ // Rendering primitives
58
+ export {
59
+ drawBadge,
60
+ drawButton,
61
+ drawCheckbox,
62
+ drawGroupIndicator,
63
+ drawProgressBar,
64
+ drawRating,
65
+ drawSparkline,
66
+ } from './primitives';
58
67
  // Theme (re-export the DEFAULT_THEME and utilities)
59
68
  export {
60
69
  createTheme,
@@ -107,7 +107,8 @@ export function drawColumnLines(
107
107
  startRow: number = 0,
108
108
  endRow: number = 0,
109
109
  rowHeight: number = 32,
110
- api?: GridApi
110
+ api?: GridApi,
111
+ availableWidth?: number
111
112
  ): void {
112
113
  ctx.strokeStyle = theme.borderColor || theme.gridLineColor;
113
114
  ctx.lineWidth = 1;
@@ -117,7 +118,8 @@ export function drawColumnLines(
117
118
  scrollX,
118
119
  viewportWidth,
119
120
  leftPinnedWidth,
120
- rightPinnedWidth
121
+ rightPinnedWidth,
122
+ availableWidth
121
123
  );
122
124
 
123
125
  // Calculate Y range for drawing
@@ -145,7 +147,8 @@ export function getColumnBorderPositions(
145
147
  scrollX: number,
146
148
  viewportWidth: number,
147
149
  leftPinnedWidth: number,
148
- rightPinnedWidth: number
150
+ rightPinnedWidth: number,
151
+ availableWidth?: number
149
152
  ): number[] {
150
153
  const positions: number[] = [];
151
154
 
@@ -153,6 +156,8 @@ export function getColumnBorderPositions(
153
156
  const rightPinned = columns.filter((c) => c.pinned === 'right');
154
157
  const centerColumns = columns.filter((c) => !c.pinned);
155
158
 
159
+ const effectiveWidth = availableWidth ?? viewportWidth;
160
+
156
161
  // Left pinned column borders
157
162
  let x = 0;
158
163
  for (const col of leftPinned) {
@@ -162,16 +167,17 @@ export function getColumnBorderPositions(
162
167
 
163
168
  // Center column borders
164
169
  x = Math.floor(leftPinnedWidth) - scrollX;
170
+ const centerEndX = Math.floor(effectiveWidth - rightPinnedWidth);
165
171
  for (const col of centerColumns) {
166
172
  x += Math.floor(col.width);
167
- // Only include if visible
168
- if (x > leftPinnedWidth && x < viewportWidth - rightPinnedWidth) {
173
+ // Only include if visible in the center area
174
+ if (x > leftPinnedWidth && x < centerEndX) {
169
175
  positions.push(x);
170
176
  }
171
177
  }
172
178
 
173
179
  // Right pinned column borders
174
- x = Math.floor(viewportWidth - rightPinnedWidth);
180
+ x = centerEndX;
175
181
  for (const col of rightPinned) {
176
182
  x += Math.floor(col.width);
177
183
  positions.push(x);
@@ -7,7 +7,13 @@
7
7
  * - Sparklines
8
8
  */
9
9
 
10
- import { SparklineOptions } from '../../types/ag-grid-types';
10
+ import {
11
+ BadgeOptions,
12
+ ButtonOptions,
13
+ ProgressOptions,
14
+ RatingOptions,
15
+ SparklineOptions,
16
+ } from '../../types/ag-grid-types';
11
17
  import { GridTheme } from './types';
12
18
 
13
19
  /**
@@ -21,15 +27,19 @@ export function drawCheckbox(
21
27
  checked: boolean,
22
28
  theme: GridTheme
23
29
  ): void {
24
- // Draw checkbox border
25
- ctx.strokeStyle = theme.borderColor;
26
- ctx.lineWidth = 1.2;
30
+ // Draw checkbox border - Use a more obvious color than the standard grid border
31
+ ctx.strokeStyle = '#94a3b8'; // Slate-400 for better visibility
32
+ ctx.lineWidth = 1.5;
27
33
  ctx.strokeRect(Math.floor(x) + 0.5, Math.floor(y) + 0.5, size, size);
28
34
 
29
35
  // Draw checkmark if checked
30
36
  if (checked) {
31
- ctx.strokeStyle = theme.textCell;
32
- ctx.lineWidth = 1.2;
37
+ // Fill background when checked for even better visibility
38
+ ctx.fillStyle = '#3b82f6'; // Blue-500
39
+ ctx.fillRect(Math.floor(x) + 0.5, Math.floor(y) + 0.5, size, size);
40
+
41
+ ctx.strokeStyle = '#ffffff'; // White checkmark on blue background
42
+ ctx.lineWidth = 2;
33
43
  ctx.beginPath();
34
44
  const padding = 3;
35
45
  const checkX = x + padding;
@@ -76,6 +86,258 @@ export function drawGroupIndicator(
76
86
  ctx.stroke();
77
87
  }
78
88
 
89
+ /**
90
+ * Draw a button within a cell.
91
+ * Returns the bounding box so callers can perform hit-testing.
92
+ */
93
+ export function drawButton<TData = any>(
94
+ ctx: CanvasRenderingContext2D,
95
+ label: string,
96
+ x: number,
97
+ y: number,
98
+ width: number,
99
+ height: number,
100
+ options: ButtonOptions<TData> = { label }
101
+ ): { bx: number; by: number; bw: number; bh: number } {
102
+ const variant = options.variant ?? 'primary';
103
+ const borderRadius = options.borderRadius ?? 4;
104
+ const paddingX = options.paddingX ?? 12;
105
+ const fontSize = options.fontSize ?? 12;
106
+
107
+ // Variant colour defaults
108
+ const VARIANTS: Record<string, { fill: string; text: string; border?: string }> = {
109
+ primary: { fill: '#3b82f6', text: '#ffffff' },
110
+ secondary: { fill: '#f3f4f6', text: '#374151', border: '#9ca3af' },
111
+ danger: { fill: '#ef4444', text: '#ffffff' },
112
+ ghost: { fill: '#f9fafb', text: '#6b7280', border: '#d1d5db' },
113
+ };
114
+ const defaults = VARIANTS[variant] ?? VARIANTS.primary;
115
+ const fillColor = options.fill ?? defaults.fill;
116
+ const textColor = options.textColor ?? defaults.text;
117
+ const borderColor = options.borderColor ?? defaults.border;
118
+
119
+ ctx.save();
120
+ ctx.font = `500 ${fontSize}px sans-serif`;
121
+ const textW = ctx.measureText(label).width;
122
+ const bw = Math.min(textW + paddingX * 2, width - 8);
123
+ const bh = fontSize + 10;
124
+ const bx = Math.floor(x + (width - bw) / 2);
125
+ const by = Math.floor(y + (height - bh) / 2);
126
+
127
+ // Background
128
+ if (fillColor && fillColor !== 'transparent') {
129
+ ctx.fillStyle = fillColor;
130
+ ctx.beginPath();
131
+ ctx.roundRect(bx, by, bw, bh, borderRadius);
132
+ ctx.fill();
133
+ }
134
+
135
+ // Border (secondary / ghost)
136
+ if (borderColor) {
137
+ ctx.strokeStyle = borderColor;
138
+ ctx.lineWidth = 1;
139
+ ctx.beginPath();
140
+ ctx.roundRect(bx + 0.5, by + 0.5, bw - 1, bh - 1, borderRadius);
141
+ ctx.stroke();
142
+ }
143
+
144
+ // Label
145
+ ctx.fillStyle = textColor;
146
+ ctx.textBaseline = 'middle';
147
+ ctx.textAlign = 'center';
148
+ ctx.fillText(label, bx + bw / 2, by + bh / 2);
149
+
150
+ ctx.restore();
151
+ return { bx, by, bw, bh };
152
+ }
153
+
154
+ /**
155
+ * Draw a badge/pill within a cell
156
+ */
157
+ export function drawBadge(
158
+ ctx: CanvasRenderingContext2D,
159
+ value: string,
160
+ x: number,
161
+ y: number,
162
+ width: number,
163
+ height: number,
164
+ options: BadgeOptions = {}
165
+ ): void {
166
+ const colorMap = options.colorMap ?? {};
167
+ const defaultColors = options.defaultColors ?? { fill: '#f3f4f6', text: '#6b7280' };
168
+ const { fill: bgColor, text: textColor } = colorMap[value] ?? defaultColors;
169
+ const borderRadius = options.borderRadius ?? 9999;
170
+ const paddingX = options.paddingX ?? 8;
171
+ const fontSize = options.fontSize ?? 11;
172
+
173
+ ctx.save();
174
+ ctx.font = `500 ${fontSize}px sans-serif`;
175
+ const textWidth = ctx.measureText(value).width;
176
+ const badgeWidth = textWidth + paddingX * 2;
177
+ const badgeHeight = fontSize + 8;
178
+ const bx = Math.floor(x + (width - badgeWidth) / 2);
179
+ const by = Math.floor(y + (height - badgeHeight) / 2);
180
+
181
+ // Draw background pill
182
+ ctx.fillStyle = bgColor;
183
+ ctx.beginPath();
184
+ ctx.roundRect(bx, by, badgeWidth, badgeHeight, Math.min(borderRadius, badgeHeight / 2));
185
+ ctx.fill();
186
+
187
+ // Draw text
188
+ ctx.fillStyle = textColor;
189
+ ctx.textBaseline = 'middle';
190
+ ctx.textAlign = 'center';
191
+ ctx.fillText(value, bx + badgeWidth / 2, by + badgeHeight / 2);
192
+
193
+ ctx.restore();
194
+ }
195
+
196
+ /**
197
+ * Draw a progress bar within a cell
198
+ */
199
+ export function drawProgressBar(
200
+ ctx: CanvasRenderingContext2D,
201
+ value: number,
202
+ x: number,
203
+ y: number,
204
+ width: number,
205
+ height: number,
206
+ options: ProgressOptions = {}
207
+ ): void {
208
+ const min = options.min ?? 0;
209
+ const max = options.max ?? 100;
210
+ const barHeight = options.barHeight ?? 8;
211
+ const borderRadius = options.borderRadius ?? 4;
212
+ const trackColor = options.trackColor ?? '#e5e7eb';
213
+ const showLabel = options.showLabel !== false;
214
+
215
+ const pct = Math.min(1, Math.max(0, (value - min) / (max - min)));
216
+
217
+ // Layout: bar + optional label
218
+ const labelWidth = showLabel ? 40 : 0;
219
+ const labelGap = showLabel ? 8 : 0;
220
+ const trackWidth = width - labelWidth - labelGap - 8; // 8px left padding
221
+ const trackX = x + 4;
222
+ const trackY = Math.floor(y + (height - barHeight) / 2);
223
+
224
+ ctx.save();
225
+
226
+ // Draw track
227
+ ctx.fillStyle = trackColor;
228
+ ctx.beginPath();
229
+ ctx.roundRect(trackX, trackY, trackWidth, barHeight, borderRadius);
230
+ ctx.fill();
231
+
232
+ // Resolve fill color
233
+ let fillColor: string;
234
+ if (typeof options.fill === 'function') {
235
+ fillColor = options.fill(value);
236
+ } else if (options.fill) {
237
+ fillColor = options.fill;
238
+ } else {
239
+ // Default traffic-light coloring
240
+ fillColor = pct >= 0.8 ? '#22c55e' : pct >= 0.6 ? '#eab308' : '#ef4444';
241
+ }
242
+
243
+ // Draw filled portion
244
+ if (pct > 0) {
245
+ ctx.fillStyle = fillColor;
246
+ ctx.beginPath();
247
+ ctx.roundRect(
248
+ trackX,
249
+ trackY,
250
+ Math.max(borderRadius * 2, trackWidth * pct),
251
+ barHeight,
252
+ borderRadius
253
+ );
254
+ ctx.fill();
255
+ }
256
+
257
+ // Draw label
258
+ if (showLabel) {
259
+ const label = options.labelFormatter ? options.labelFormatter(value) : `${value}%`;
260
+ ctx.fillStyle = fillColor;
261
+ ctx.font = 'bold 11px sans-serif';
262
+ ctx.textBaseline = 'middle';
263
+ ctx.textAlign = 'left';
264
+ ctx.fillText(label, trackX + trackWidth + labelGap, y + height / 2);
265
+ }
266
+
267
+ ctx.restore();
268
+ }
269
+
270
+ /**
271
+ * Draw a rating (stars) within a cell
272
+ */
273
+ export function drawRating(
274
+ ctx: CanvasRenderingContext2D,
275
+ value: number,
276
+ x: number,
277
+ y: number,
278
+ width: number,
279
+ height: number,
280
+ options: RatingOptions = {}
281
+ ): void {
282
+ const max = options.max ?? 5;
283
+ const size = options.size ?? 14;
284
+ const color = options.color ?? '#ffb400';
285
+ const emptyColor = options.emptyColor ?? '#e5e7eb';
286
+ const gap = 2;
287
+
288
+ const totalWidth = max * size + (max - 1) * gap;
289
+ const startX = x + (width - totalWidth) / 2;
290
+ const centerY = y + height / 2;
291
+
292
+ ctx.save();
293
+
294
+ for (let i = 0; i < max; i++) {
295
+ const starX = startX + i * (size + gap);
296
+ const isFilled = i < Math.round(value);
297
+
298
+ ctx.fillStyle = isFilled ? color : emptyColor;
299
+ drawStar(ctx, starX + size / 2, centerY, 5, size / 2, size / 4);
300
+ ctx.fill();
301
+ }
302
+
303
+ ctx.restore();
304
+ }
305
+
306
+ /**
307
+ * Helper to draw a star shape
308
+ */
309
+ function drawStar(
310
+ ctx: CanvasRenderingContext2D,
311
+ cx: number,
312
+ cy: number,
313
+ spikes: number,
314
+ outerRadius: number,
315
+ innerRadius: number
316
+ ): void {
317
+ let rot = (Math.PI / 2) * 3;
318
+ let x = cx;
319
+ let y = cy;
320
+ const step = Math.PI / spikes;
321
+
322
+ ctx.beginPath();
323
+ ctx.moveTo(cx, cy - outerRadius);
324
+
325
+ for (let i = 0; i < spikes; i++) {
326
+ x = cx + Math.cos(rot) * outerRadius;
327
+ y = cy + Math.sin(rot) * outerRadius;
328
+ ctx.lineTo(x, y);
329
+ rot += step;
330
+
331
+ x = cx + Math.cos(rot) * innerRadius;
332
+ y = cy + Math.sin(rot) * innerRadius;
333
+ ctx.lineTo(x, y);
334
+ rot += step;
335
+ }
336
+
337
+ ctx.lineTo(cx, cy - outerRadius);
338
+ ctx.closePath();
339
+ }
340
+
79
341
  /**
80
342
  * Draw a sparkline within a cell
81
343
  */
@@ -141,7 +403,7 @@ export function drawSparkline(
141
403
  ctx.lineCap = 'round';
142
404
  ctx.stroke();
143
405
  } else if (type === 'column' || type === 'bar') {
144
- const colOptions = options.column || {};
406
+ const colOptions = (type === 'bar' ? options.bar || options.column : options.column) || {};
145
407
  const colPadding = colOptions.padding || 0.1;
146
408
  const colWidth = drawWidth / data.length;
147
409
  const barWidth = colWidth * (1 - colPadding);
@@ -4,7 +4,7 @@
4
4
  * Shared type definitions used across the rendering modules.
5
5
  */
6
6
 
7
- import { ColDef, Column, IRowNode } from '../../types/ag-grid-types';
7
+ import { ColDef, Column, GridApi, IRowNode } from '../../types/ag-grid-types';
8
8
 
9
9
  // ============================================================================
10
10
  // CORE RENDERING TYPES
@@ -116,6 +116,7 @@ export interface CellDrawContext<TData = any> {
116
116
  isSelected: boolean;
117
117
  isHovered: boolean;
118
118
  isEvenRow: boolean;
119
+ api: GridApi<TData>;
119
120
  }
120
121
 
121
122
  /**