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.
- package/AGENTS.md +70 -27
- package/e2e/advanced.spec.ts +1 -1
- package/e2e/benchmark.spec.ts +7 -7
- package/e2e/cell-renderers.spec.ts +152 -0
- package/e2e/debug-streaming.spec.ts +31 -0
- package/e2e/dnd.spec.ts +73 -0
- package/e2e/screenshots.spec.ts +1 -1
- package/e2e/visual.spec.ts +30 -9
- package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
- package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
- package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
- package/package.json +5 -5
- package/plan.md +30 -34
- package/src/lib/components/argent-grid.component.css +258 -549
- package/src/lib/components/argent-grid.component.html +272 -306
- package/src/lib/components/argent-grid.component.ts +585 -135
- package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
- package/src/lib/components/argent-grid.selection.spec.ts +2 -2
- package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
- package/src/lib/components/set-filter/set-filter.component.ts +7 -2
- package/src/lib/rendering/canvas-renderer.spec.ts +148 -1
- package/src/lib/rendering/canvas-renderer.ts +177 -286
- package/src/lib/rendering/render/cells.ts +122 -5
- package/src/lib/rendering/render/column-utils.ts +27 -5
- package/src/lib/rendering/render/hit-test.ts +6 -11
- package/src/lib/rendering/render/index.ts +15 -6
- package/src/lib/rendering/render/lines.ts +12 -6
- package/src/lib/rendering/render/primitives.ts +269 -7
- package/src/lib/rendering/render/types.ts +2 -1
- package/src/lib/rendering/render/walk.ts +39 -19
- package/src/lib/services/grid.service.spec.ts +76 -0
- package/src/lib/services/grid.service.ts +451 -114
- package/src/lib/themes/theme-quartz.ts +2 -2
- package/src/lib/types/ag-grid-types.ts +500 -0
- package/src/stories/Advanced.stories.ts +78 -17
- package/src/stories/ArgentGrid.stories.ts +50 -26
- package/src/stories/Benchmark.stories.ts +17 -15
- package/src/stories/CellRenderers.stories.ts +205 -31
- package/src/stories/Filtering.stories.ts +56 -16
- package/src/stories/Grouping.stories.ts +86 -13
- package/src/stories/Streaming.stories.ts +57 -0
- package/src/stories/Theming.stories.ts +23 -10
- package/src/stories/Tooltips.stories.ts +381 -0
- package/src/stories/benchmark-wrapper.component.ts +69 -29
- package/src/stories/story-utils.ts +88 -0
- package/src/stories/streaming-wrapper.component.ts +441 -0
- 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 =
|
|
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
|
|
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.
|
|
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 <
|
|
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 =
|
|
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 {
|
|
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 =
|
|
26
|
-
ctx.lineWidth = 1.
|
|
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
|
-
|
|
32
|
-
ctx.
|
|
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
|
/**
|