argent-grid 0.1.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/.github/workflows/ci.yml +69 -0
- package/.github/workflows/pages.yml +6 -12
- package/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/tsconfig.json +24 -0
- package/AGENTS.md +70 -27
- package/README.md +51 -34
- package/angular.json +66 -0
- package/biome.json +66 -0
- package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
- package/docs/AG-GRID-COMPARISON.md +725 -0
- package/docs/CELL-RENDERER-GUIDE.md +241 -0
- package/docs/CONTEXT-MENU-GUIDE.md +371 -0
- package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
- package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
- package/docs/PERFORMANCE-REVIEW.md +571 -0
- package/docs/RESEARCH-STATUS.md +234 -0
- package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
- package/docs/STORYBOOK-REFACTOR.md +215 -0
- package/docs/STORYBOOK-STATUS.md +156 -0
- package/docs/TEST-COVERAGE-REPORT.md +276 -0
- package/docs/THEME-API-GUIDE.md +445 -0
- package/docs/THEME-API-PLAN.md +364 -0
- package/e2e/advanced.spec.ts +109 -0
- package/e2e/argentgrid.spec.ts +65 -0
- package/e2e/benchmark.spec.ts +52 -0
- 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 +52 -0
- package/e2e/theming.spec.ts +35 -0
- package/e2e/visual.spec.ts +112 -0
- 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 +21 -7
- package/plan.md +56 -28
- package/playwright.config.ts +38 -0
- package/setup-vitest.ts +10 -13
- package/src/lib/argent-grid.module.ts +10 -12
- package/src/lib/components/argent-grid.component.css +281 -321
- package/src/lib/components/argent-grid.component.html +295 -207
- package/src/lib/components/argent-grid.component.spec.ts +120 -160
- package/src/lib/components/argent-grid.component.ts +1193 -290
- package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
- package/src/lib/components/argent-grid.selection.spec.ts +132 -0
- package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
- package/src/lib/components/set-filter/set-filter.component.ts +307 -0
- package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
- package/src/lib/directives/click-outside.directive.ts +19 -0
- package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
- package/src/lib/rendering/canvas-renderer.ts +456 -452
- package/src/lib/rendering/live-data-handler.ts +110 -0
- package/src/lib/rendering/live-data-optimizations.ts +133 -0
- package/src/lib/rendering/render/blit.spec.ts +16 -27
- package/src/lib/rendering/render/blit.ts +48 -36
- package/src/lib/rendering/render/cells.spec.ts +132 -0
- package/src/lib/rendering/render/cells.ts +167 -28
- package/src/lib/rendering/render/column-utils.ts +95 -0
- package/src/lib/rendering/render/hit-test.ts +50 -0
- package/src/lib/rendering/render/index.ts +88 -76
- package/src/lib/rendering/render/lines.ts +53 -47
- package/src/lib/rendering/render/primitives.ts +423 -0
- package/src/lib/rendering/render/theme.spec.ts +8 -12
- package/src/lib/rendering/render/theme.ts +7 -10
- package/src/lib/rendering/render/types.ts +3 -2
- package/src/lib/rendering/render/walk.spec.ts +35 -38
- package/src/lib/rendering/render/walk.ts +94 -64
- package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
- package/src/lib/rendering/utils/damage-tracker.ts +6 -18
- package/src/lib/rendering/utils/index.ts +1 -1
- package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
- package/src/lib/services/grid.service.spec.ts +1241 -201
- package/src/lib/services/grid.service.ts +1204 -235
- package/src/lib/themes/parts/color-schemes.ts +132 -0
- package/src/lib/themes/parts/icon-sets.ts +258 -0
- package/src/lib/themes/theme-builder.ts +347 -0
- package/src/lib/themes/theme-quartz.ts +72 -0
- package/src/lib/themes/types.ts +238 -0
- package/src/lib/types/ag-grid-types.ts +573 -14
- package/src/public-api.ts +39 -9
- package/src/stories/Advanced.stories.ts +249 -0
- package/src/stories/ArgentGrid.stories.ts +301 -0
- package/src/stories/Benchmark.stories.ts +76 -0
- package/src/stories/CellRenderers.stories.ts +395 -0
- package/src/stories/Filtering.stories.ts +292 -0
- package/src/stories/Grouping.stories.ts +290 -0
- package/src/stories/Streaming.stories.ts +57 -0
- package/src/stories/Theming.stories.ts +137 -0
- package/src/stories/Tooltips.stories.ts +381 -0
- package/src/stories/benchmark-wrapper.component.ts +355 -0
- package/src/stories/story-utils.ts +88 -0
- package/src/stories/streaming-wrapper.component.ts +441 -0
- package/tsconfig.json +1 -0
- package/tsconfig.storybook.json +10 -0
- package/vitest.config.ts +9 -9
- package/demo-app/README.md +0 -70
- package/demo-app/angular.json +0 -78
- package/demo-app/e2e/benchmark.spec.ts +0 -53
- package/demo-app/e2e/demo-page.spec.ts +0 -77
- package/demo-app/e2e/grid-features.spec.ts +0 -269
- package/demo-app/package-lock.json +0 -14023
- package/demo-app/package.json +0 -36
- package/demo-app/playwright-test-menu.js +0 -19
- package/demo-app/playwright.config.ts +0 -23
- package/demo-app/src/app/app.component.ts +0 -10
- package/demo-app/src/app/app.config.ts +0 -13
- package/demo-app/src/app/app.routes.ts +0 -7
- package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
- package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
- package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
- package/demo-app/src/index.html +0 -19
- package/demo-app/src/main.ts +0 -6
- package/demo-app/tsconfig.json +0 -31
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Draws grid lines (borders) efficiently.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { Column,
|
|
7
|
+
import { Column, GridApi } from '../../types/ag-grid-types';
|
|
8
8
|
import { GridTheme, Rectangle } from './types';
|
|
9
9
|
|
|
10
10
|
// ============================================================================
|
|
@@ -22,9 +22,14 @@ export function drawCrispLine(
|
|
|
22
22
|
y2: number
|
|
23
23
|
): void {
|
|
24
24
|
ctx.beginPath();
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
// For 1px lines, we want the center of the line to be at X.5
|
|
26
|
+
const snapX1 = Math.floor(x1) + 0.5;
|
|
27
|
+
const snapY1 = Math.floor(y1) + 0.5;
|
|
28
|
+
const snapX2 = Math.floor(x2) + 0.5;
|
|
29
|
+
const snapY2 = Math.floor(y2) + 0.5;
|
|
30
|
+
|
|
31
|
+
ctx.moveTo(snapX1, snapY1);
|
|
32
|
+
ctx.lineTo(snapX2, snapY2);
|
|
28
33
|
ctx.stroke();
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -66,7 +71,8 @@ export function drawRowLines(
|
|
|
66
71
|
rowHeight: number,
|
|
67
72
|
scrollTop: number,
|
|
68
73
|
viewportWidth: number,
|
|
69
|
-
theme: GridTheme
|
|
74
|
+
theme: GridTheme,
|
|
75
|
+
api?: GridApi
|
|
70
76
|
): void {
|
|
71
77
|
ctx.strokeStyle = theme.borderColor || theme.gridLineColor;
|
|
72
78
|
ctx.lineWidth = 1;
|
|
@@ -74,9 +80,12 @@ export function drawRowLines(
|
|
|
74
80
|
ctx.beginPath();
|
|
75
81
|
|
|
76
82
|
for (let row = startRow; row <= endRow; row++) {
|
|
77
|
-
const y = Math.floor(row * rowHeight - scrollTop)
|
|
78
|
-
|
|
79
|
-
|
|
83
|
+
const y = Math.floor(api ? api.getRowY(row) - scrollTop : row * rowHeight - scrollTop);
|
|
84
|
+
// Draw border at the bottom of the row (y-0.5) to match DOM border-bottom
|
|
85
|
+
const borderY = y - 0.5;
|
|
86
|
+
if (borderY < 0) continue; // Skip top border if it's outside
|
|
87
|
+
ctx.moveTo(0, borderY);
|
|
88
|
+
ctx.lineTo(viewportWidth, borderY);
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
ctx.stroke();
|
|
@@ -97,7 +106,9 @@ export function drawColumnLines(
|
|
|
97
106
|
theme: GridTheme,
|
|
98
107
|
startRow: number = 0,
|
|
99
108
|
endRow: number = 0,
|
|
100
|
-
rowHeight: number = 32
|
|
109
|
+
rowHeight: number = 32,
|
|
110
|
+
api?: GridApi,
|
|
111
|
+
availableWidth?: number
|
|
101
112
|
): void {
|
|
102
113
|
ctx.strokeStyle = theme.borderColor || theme.gridLineColor;
|
|
103
114
|
ctx.lineWidth = 1;
|
|
@@ -107,17 +118,20 @@ export function drawColumnLines(
|
|
|
107
118
|
scrollX,
|
|
108
119
|
viewportWidth,
|
|
109
120
|
leftPinnedWidth,
|
|
110
|
-
rightPinnedWidth
|
|
121
|
+
rightPinnedWidth,
|
|
122
|
+
availableWidth
|
|
111
123
|
);
|
|
112
124
|
|
|
113
125
|
// Calculate Y range for drawing
|
|
114
|
-
const drawY1 = Math.
|
|
115
|
-
|
|
126
|
+
const drawY1 = Math.floor(
|
|
127
|
+
api ? api.getRowY(startRow) - scrollTop : startRow * rowHeight - scrollTop
|
|
128
|
+
);
|
|
129
|
+
const drawY2 = Math.floor(api ? api.getRowY(endRow) - scrollTop : endRow * rowHeight - scrollTop);
|
|
116
130
|
|
|
117
131
|
ctx.beginPath();
|
|
118
132
|
|
|
119
133
|
for (const x of columnPositions) {
|
|
120
|
-
const borderX = Math.floor(x)
|
|
134
|
+
const borderX = Math.floor(x) - 0.5;
|
|
121
135
|
ctx.moveTo(borderX, drawY1);
|
|
122
136
|
ctx.lineTo(borderX, drawY2);
|
|
123
137
|
}
|
|
@@ -133,35 +147,39 @@ export function getColumnBorderPositions(
|
|
|
133
147
|
scrollX: number,
|
|
134
148
|
viewportWidth: number,
|
|
135
149
|
leftPinnedWidth: number,
|
|
136
|
-
rightPinnedWidth: number
|
|
150
|
+
rightPinnedWidth: number,
|
|
151
|
+
availableWidth?: number
|
|
137
152
|
): number[] {
|
|
138
153
|
const positions: number[] = [];
|
|
139
154
|
|
|
140
|
-
const leftPinned = columns.filter(c => c.pinned === 'left');
|
|
141
|
-
const rightPinned = columns.filter(c => c.pinned === 'right');
|
|
142
|
-
const centerColumns = columns.filter(c => !c.pinned);
|
|
155
|
+
const leftPinned = columns.filter((c) => c.pinned === 'left');
|
|
156
|
+
const rightPinned = columns.filter((c) => c.pinned === 'right');
|
|
157
|
+
const centerColumns = columns.filter((c) => !c.pinned);
|
|
158
|
+
|
|
159
|
+
const effectiveWidth = availableWidth ?? viewportWidth;
|
|
143
160
|
|
|
144
161
|
// Left pinned column borders
|
|
145
162
|
let x = 0;
|
|
146
163
|
for (const col of leftPinned) {
|
|
147
|
-
x += col.width;
|
|
164
|
+
x += Math.floor(col.width);
|
|
148
165
|
positions.push(x);
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
// Center column borders
|
|
152
|
-
x = leftPinnedWidth - scrollX;
|
|
169
|
+
x = Math.floor(leftPinnedWidth) - scrollX;
|
|
170
|
+
const centerEndX = Math.floor(effectiveWidth - rightPinnedWidth);
|
|
153
171
|
for (const col of centerColumns) {
|
|
154
|
-
x += col.width;
|
|
155
|
-
// Only include if visible
|
|
156
|
-
if (x > leftPinnedWidth && x <
|
|
172
|
+
x += Math.floor(col.width);
|
|
173
|
+
// Only include if visible in the center area
|
|
174
|
+
if (x > leftPinnedWidth && x < centerEndX) {
|
|
157
175
|
positions.push(x);
|
|
158
176
|
}
|
|
159
177
|
}
|
|
160
178
|
|
|
161
179
|
// Right pinned column borders
|
|
162
|
-
x =
|
|
180
|
+
x = centerEndX;
|
|
163
181
|
for (const col of rightPinned) {
|
|
164
|
-
x += col.width;
|
|
182
|
+
x += Math.floor(col.width);
|
|
165
183
|
positions.push(x);
|
|
166
184
|
}
|
|
167
185
|
|
|
@@ -183,10 +201,11 @@ export function drawGridLines(
|
|
|
183
201
|
viewportHeight: number,
|
|
184
202
|
leftPinnedWidth: number,
|
|
185
203
|
rightPinnedWidth: number,
|
|
186
|
-
theme: GridTheme
|
|
204
|
+
theme: GridTheme,
|
|
205
|
+
api?: GridApi
|
|
187
206
|
): void {
|
|
188
207
|
// Draw horizontal lines
|
|
189
|
-
drawRowLines(ctx, startRow, endRow, rowHeight, scrollTop, viewportWidth, theme);
|
|
208
|
+
drawRowLines(ctx, startRow, endRow, rowHeight, scrollTop, viewportWidth, theme, api);
|
|
190
209
|
|
|
191
210
|
// Draw vertical lines
|
|
192
211
|
drawColumnLines(
|
|
@@ -201,7 +220,8 @@ export function drawGridLines(
|
|
|
201
220
|
theme,
|
|
202
221
|
startRow,
|
|
203
222
|
endRow,
|
|
204
|
-
rowHeight
|
|
223
|
+
rowHeight,
|
|
224
|
+
api
|
|
205
225
|
);
|
|
206
226
|
}
|
|
207
227
|
|
|
@@ -261,11 +281,7 @@ export function drawRangeSelectionBorder(
|
|
|
261
281
|
lineWidth?: number;
|
|
262
282
|
} = {}
|
|
263
283
|
): void {
|
|
264
|
-
const {
|
|
265
|
-
color = '#1976d2',
|
|
266
|
-
fillColor = 'rgba(25, 118, 210, 0.1)',
|
|
267
|
-
lineWidth = 1
|
|
268
|
-
} = options;
|
|
284
|
+
const { color = '#1976d2', fillColor = 'rgba(25, 118, 210, 0.1)', lineWidth = 1 } = options;
|
|
269
285
|
|
|
270
286
|
// Draw fill
|
|
271
287
|
if (fillColor) {
|
|
@@ -304,7 +320,7 @@ export function drawPinnedRegionBorders(
|
|
|
304
320
|
// Left pinned border
|
|
305
321
|
if (leftPinnedWidth > 0) {
|
|
306
322
|
ctx.beginPath();
|
|
307
|
-
const x = Math.floor(leftPinnedWidth)
|
|
323
|
+
const x = Math.floor(leftPinnedWidth) - 0.5;
|
|
308
324
|
ctx.moveTo(x, 0);
|
|
309
325
|
ctx.lineTo(x, viewportHeight);
|
|
310
326
|
ctx.stroke();
|
|
@@ -313,7 +329,7 @@ export function drawPinnedRegionBorders(
|
|
|
313
329
|
// Right pinned border
|
|
314
330
|
if (rightPinnedWidth > 0) {
|
|
315
331
|
ctx.beginPath();
|
|
316
|
-
const x = Math.floor(viewportWidth - rightPinnedWidth)
|
|
332
|
+
const x = Math.floor(viewportWidth - rightPinnedWidth) - 0.5;
|
|
317
333
|
ctx.moveTo(x, 0);
|
|
318
334
|
ctx.lineTo(x, viewportHeight);
|
|
319
335
|
ctx.stroke();
|
|
@@ -332,12 +348,7 @@ export function drawPinnedRegionShadows(
|
|
|
332
348
|
): void {
|
|
333
349
|
// Left shadow (on the right edge of left pinned)
|
|
334
350
|
if (leftPinnedWidth > 0) {
|
|
335
|
-
const gradient = ctx.createLinearGradient(
|
|
336
|
-
leftPinnedWidth,
|
|
337
|
-
0,
|
|
338
|
-
leftPinnedWidth + 4,
|
|
339
|
-
0
|
|
340
|
-
);
|
|
351
|
+
const gradient = ctx.createLinearGradient(leftPinnedWidth, 0, leftPinnedWidth + 4, 0);
|
|
341
352
|
gradient.addColorStop(0, 'rgba(0, 0, 0, 0.1)');
|
|
342
353
|
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|
343
354
|
|
|
@@ -348,16 +359,11 @@ export function drawPinnedRegionShadows(
|
|
|
348
359
|
// Right shadow (on the left edge of right pinned)
|
|
349
360
|
if (rightPinnedWidth > 0) {
|
|
350
361
|
const shadowX = viewportWidth - rightPinnedWidth;
|
|
351
|
-
const gradient = ctx.createLinearGradient(
|
|
352
|
-
shadowX - 4,
|
|
353
|
-
0,
|
|
354
|
-
shadowX,
|
|
355
|
-
0
|
|
356
|
-
);
|
|
362
|
+
const gradient = ctx.createLinearGradient(shadowX - 4, 0, shadowX, 0);
|
|
357
363
|
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
|
|
358
364
|
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.1)');
|
|
359
365
|
|
|
360
366
|
ctx.fillStyle = gradient;
|
|
361
367
|
ctx.fillRect(shadowX - 4, 0, 4, viewportHeight);
|
|
362
368
|
}
|
|
363
|
-
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering Primitives for Canvas Renderer
|
|
3
|
+
*
|
|
4
|
+
* Provides specialized drawing functions for grid UI elements:
|
|
5
|
+
* - Checkboxes
|
|
6
|
+
* - Group indicators (expand/collapse)
|
|
7
|
+
* - Sparklines
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
BadgeOptions,
|
|
12
|
+
ButtonOptions,
|
|
13
|
+
ProgressOptions,
|
|
14
|
+
RatingOptions,
|
|
15
|
+
SparklineOptions,
|
|
16
|
+
} from '../../types/ag-grid-types';
|
|
17
|
+
import { GridTheme } from './types';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Draw a grid checkbox
|
|
21
|
+
*/
|
|
22
|
+
export function drawCheckbox(
|
|
23
|
+
ctx: CanvasRenderingContext2D,
|
|
24
|
+
x: number,
|
|
25
|
+
y: number,
|
|
26
|
+
size: number,
|
|
27
|
+
checked: boolean,
|
|
28
|
+
theme: GridTheme
|
|
29
|
+
): void {
|
|
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;
|
|
33
|
+
ctx.strokeRect(Math.floor(x) + 0.5, Math.floor(y) + 0.5, size, size);
|
|
34
|
+
|
|
35
|
+
// Draw checkmark if checked
|
|
36
|
+
if (checked) {
|
|
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;
|
|
43
|
+
ctx.beginPath();
|
|
44
|
+
const padding = 3;
|
|
45
|
+
const checkX = x + padding;
|
|
46
|
+
const checkY = y + size / 2;
|
|
47
|
+
const checkWidth = size - padding * 2;
|
|
48
|
+
|
|
49
|
+
// Draw checkmark
|
|
50
|
+
ctx.moveTo(checkX, checkY);
|
|
51
|
+
ctx.lineTo(checkX + checkWidth / 3, checkY + checkWidth / 3);
|
|
52
|
+
ctx.lineTo(checkX + checkWidth, checkY - checkWidth / 3);
|
|
53
|
+
ctx.stroke();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Draw a group expand/collapse indicator
|
|
59
|
+
*/
|
|
60
|
+
export function drawGroupIndicator(
|
|
61
|
+
ctx: CanvasRenderingContext2D,
|
|
62
|
+
x: number,
|
|
63
|
+
y: number,
|
|
64
|
+
rowHeight: number,
|
|
65
|
+
expanded: boolean,
|
|
66
|
+
theme: GridTheme
|
|
67
|
+
): void {
|
|
68
|
+
ctx.beginPath();
|
|
69
|
+
ctx.strokeStyle = theme.textCell;
|
|
70
|
+
ctx.lineWidth = 1;
|
|
71
|
+
const centerY = Math.floor(y + rowHeight / 2);
|
|
72
|
+
const size = theme.groupIndicatorSize;
|
|
73
|
+
|
|
74
|
+
if (expanded) {
|
|
75
|
+
// Expanded: horizontal line
|
|
76
|
+
ctx.moveTo(Math.floor(x), centerY);
|
|
77
|
+
ctx.lineTo(Math.floor(x + size), centerY);
|
|
78
|
+
} else {
|
|
79
|
+
// Collapsed: plus sign
|
|
80
|
+
const halfSize = size / 2;
|
|
81
|
+
ctx.moveTo(Math.floor(x), centerY);
|
|
82
|
+
ctx.lineTo(Math.floor(x + size), centerY);
|
|
83
|
+
ctx.moveTo(Math.floor(x + halfSize), centerY - halfSize);
|
|
84
|
+
ctx.lineTo(Math.floor(x + halfSize), centerY + halfSize);
|
|
85
|
+
}
|
|
86
|
+
ctx.stroke();
|
|
87
|
+
}
|
|
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
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Draw a sparkline within a cell
|
|
343
|
+
*/
|
|
344
|
+
export function drawSparkline(
|
|
345
|
+
ctx: CanvasRenderingContext2D,
|
|
346
|
+
data: any[],
|
|
347
|
+
x: number,
|
|
348
|
+
y: number,
|
|
349
|
+
width: number,
|
|
350
|
+
height: number,
|
|
351
|
+
options: SparklineOptions
|
|
352
|
+
): void {
|
|
353
|
+
if (!Array.isArray(data) || data.length === 0) return;
|
|
354
|
+
|
|
355
|
+
const padding = options.padding || { top: 4, bottom: 4, left: 4, right: 4 };
|
|
356
|
+
const drawX = x + (padding.left || 0);
|
|
357
|
+
const drawY = y + (padding.top || 0);
|
|
358
|
+
const drawWidth = width - (padding.left || 0) - (padding.right || 0);
|
|
359
|
+
const drawHeight = height - (padding.top || 0) - (padding.bottom || 0);
|
|
360
|
+
|
|
361
|
+
if (drawWidth <= 0 || drawHeight <= 0) return;
|
|
362
|
+
|
|
363
|
+
const min = Math.min(...data);
|
|
364
|
+
const max = Math.max(...data);
|
|
365
|
+
const range = max - min || 1;
|
|
366
|
+
|
|
367
|
+
const type = options.type || 'line';
|
|
368
|
+
|
|
369
|
+
ctx.save();
|
|
370
|
+
|
|
371
|
+
if (type === 'line' || type === 'area') {
|
|
372
|
+
ctx.beginPath();
|
|
373
|
+
for (let i = 0; i < data.length; i++) {
|
|
374
|
+
const px = drawX + (i / (data.length - 1)) * drawWidth;
|
|
375
|
+
const py = drawY + drawHeight - ((data[i] - min) / range) * drawHeight;
|
|
376
|
+
|
|
377
|
+
if (i === 0) ctx.moveTo(px, py);
|
|
378
|
+
else ctx.lineTo(px, py);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (type === 'area') {
|
|
382
|
+
const areaOptions = options.area || {};
|
|
383
|
+
ctx.lineTo(drawX + drawWidth, drawY + drawHeight);
|
|
384
|
+
ctx.lineTo(drawX, drawY + drawHeight);
|
|
385
|
+
ctx.closePath();
|
|
386
|
+
ctx.fillStyle = areaOptions.fill || 'rgba(33, 150, 243, 0.3)';
|
|
387
|
+
ctx.fill();
|
|
388
|
+
|
|
389
|
+
// Stroke the top line
|
|
390
|
+
ctx.beginPath();
|
|
391
|
+
for (let i = 0; i < data.length; i++) {
|
|
392
|
+
const px = drawX + (i / (data.length - 1)) * drawWidth;
|
|
393
|
+
const py = drawY + drawHeight - ((data[i] - min) / range) * drawHeight;
|
|
394
|
+
if (i === 0) ctx.moveTo(px, py);
|
|
395
|
+
else ctx.lineTo(px, py);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const lineOptions = (type === 'area' ? options.area : options.line) || {};
|
|
400
|
+
ctx.strokeStyle = lineOptions.stroke || '#2196f3';
|
|
401
|
+
ctx.lineWidth = lineOptions.strokeWidth || 1.5;
|
|
402
|
+
ctx.lineJoin = 'round';
|
|
403
|
+
ctx.lineCap = 'round';
|
|
404
|
+
ctx.stroke();
|
|
405
|
+
} else if (type === 'column' || type === 'bar') {
|
|
406
|
+
const colOptions = (type === 'bar' ? options.bar || options.column : options.column) || {};
|
|
407
|
+
const colPadding = colOptions.padding || 0.1;
|
|
408
|
+
const colWidth = drawWidth / data.length;
|
|
409
|
+
const barWidth = colWidth * (1 - colPadding);
|
|
410
|
+
|
|
411
|
+
ctx.fillStyle = colOptions.fill || '#2196f3';
|
|
412
|
+
|
|
413
|
+
for (let i = 0; i < data.length; i++) {
|
|
414
|
+
const px = drawX + i * colWidth + (colWidth * colPadding) / 2;
|
|
415
|
+
const valHeight = ((data[i] - min) / range) * drawHeight;
|
|
416
|
+
const py = drawY + drawHeight - valHeight;
|
|
417
|
+
|
|
418
|
+
ctx.fillRect(Math.floor(px), Math.floor(py), Math.floor(barWidth), Math.ceil(valHeight));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
ctx.restore();
|
|
423
|
+
}
|
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
-
|
|
8
|
+
createTheme,
|
|
9
9
|
DARK_THEME,
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
DEFAULT_THEME,
|
|
11
|
+
getCellBackgroundColor,
|
|
12
12
|
getFontFromTheme,
|
|
13
13
|
getRowTheme,
|
|
14
|
-
getCellBackgroundColor,
|
|
15
14
|
getThemePreset,
|
|
16
|
-
|
|
15
|
+
mergeTheme,
|
|
16
|
+
THEME_PRESETS,
|
|
17
17
|
} from './theme';
|
|
18
|
-
import { GridTheme
|
|
18
|
+
import { GridTheme } from './types';
|
|
19
19
|
|
|
20
20
|
describe('Theme System', () => {
|
|
21
21
|
describe('DEFAULT_THEME', () => {
|
|
@@ -86,11 +86,7 @@ describe('Theme System', () => {
|
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it('should apply multiple overrides in order', () => {
|
|
89
|
-
const result = mergeTheme(
|
|
90
|
-
DEFAULT_THEME,
|
|
91
|
-
{ bgCell: '#ff0000' },
|
|
92
|
-
{ bgCell: '#00ff00' }
|
|
93
|
-
);
|
|
89
|
+
const result = mergeTheme(DEFAULT_THEME, { bgCell: '#ff0000' }, { bgCell: '#00ff00' });
|
|
94
90
|
|
|
95
91
|
expect(result.bgCell).toBe('#00ff00');
|
|
96
92
|
});
|
|
@@ -279,4 +275,4 @@ describe('Theme System', () => {
|
|
|
279
275
|
expect(comfortableFull.fontSize).toBeGreaterThan(DEFAULT_THEME.fontSize);
|
|
280
276
|
});
|
|
281
277
|
});
|
|
282
|
-
});
|
|
278
|
+
});
|