Skip to content

Commit 8dfc48e

Browse files
committed
fix some grid issues
1 parent 8784aac commit 8dfc48e

8 files changed

Lines changed: 211 additions & 43 deletions

File tree

pywry/pywry/app.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,10 @@ def show_dataframe( # pylint: disable=too-many-branches
944944
on_cell_click: Any = None,
945945
on_row_selected: Any = None,
946946
server_side: bool = False,
947+
row_selection: Any = False,
948+
pagination: bool | None = None,
949+
pagination_page_size: int = 100,
950+
enable_cell_span: bool | None = None,
947951
) -> NativeWindowHandle | BaseWidget:
948952
"""Show a DataFrame in an AG Grid table.
949953
@@ -1014,7 +1018,13 @@ def show_dataframe( # pylint: disable=too-many-branches
10141018
toolbars=toolbars,
10151019
modals=modals,
10161020
callbacks=inline_callbacks,
1017-
open_browser=is_browser_mode, # Open in browser for BROWSER mode
1021+
open_browser=is_browser_mode,
1022+
column_defs=column_defs,
1023+
grid_options=grid_options,
1024+
row_selection=row_selection,
1025+
pagination=pagination,
1026+
pagination_page_size=pagination_page_size,
1027+
enable_cell_span=enable_cell_span,
10181028
)
10191029
self._register_inline_widget(widget)
10201030
return widget

pywry/pywry/frontend/src/aggrid-defaults.js

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ window.PYWRY_AGGRID_DEFAULT_COL_DEF = {
7272
sortable: true,
7373
resizable: true,
7474
wrapText: true,
75-
wrapHeaderText: true,
75+
wrapHeaderText: false,
7676
autoHeight: true,
7777
filterParams: {
7878
buttons: ['apply', 'clear', 'reset'],
@@ -82,60 +82,53 @@ window.PYWRY_AGGRID_DEFAULT_COL_DEF = {
8282
};
8383

8484
/**
85-
* Format numbers intelligently:
86-
* - Large integers with trailing zeros → K/M/B (75000 → "75K")
87-
* - Large integers without trailing zeros → commas (75123 → "75,123")
88-
* - Small decimals (< 1) → preserve full precision, no truncation
89-
* - Very small numbers (many leading zeros) → scientific notation
90-
* - Regular decimals → preserve full precision
85+
* Format numbers with predictable compact notation.
9186
*
92-
* @param {number} value - The number to format
93-
* @returns {string} Formatted number string
87+
* Rules:
88+
* - Always promote large magnitudes to the highest sensible unit (K/M/B/T).
89+
* - Avoid giant K values (e.g. 19,225,200K) by promoting to M/B/T.
90+
* - Keep small/medium numbers comma-formatted unless K is clean (divisible by 1,000).
91+
* - Preserve decimal precision for normal decimals; scientific for very tiny values.
9492
*/
9593
window.PYWRY_FORMAT_NUMBER = function(value) {
9694
if (value == null || isNaN(value)) return '';
9795

9896
var absValue = Math.abs(value);
99-
var sign = value < 0 ? '-' : '';
10097

101-
// Very small numbers (with many leading zeros after decimal) → scientific notation
102-
// e.g., 0.00000123 → "1.23e-6"
98+
// Very small non-zero values: keep scientific notation readable
10399
if (absValue > 0 && absValue < 0.0001) {
104-
return value.toExponential();
100+
return value.toExponential(2);
105101
}
106102

107-
// Handle non-integers (decimals) - add thousand separators
103+
// Non-integers: preserve precision while adding comma separators to integer part
108104
if (!Number.isInteger(value)) {
109-
var parts = value.toString().split('.');
110-
var integerPart = parseInt(parts[0]);
105+
var parts = String(value).split('.');
106+
var integerPart = Number(parts[0] || 0);
111107
var decimalPart = parts[1] || '';
112-
113-
// Format integer part with commas
114108
var formattedInteger = integerPart.toLocaleString('en-US');
115-
116-
// Return with decimal part preserved
117109
return decimalPart ? formattedInteger + '.' + decimalPart : formattedInteger;
118110
}
119111

120-
// From here, we're dealing with integers only
121-
// Abbreviate integers with trailing zeros consistently
122-
123-
// Billions (1,000,000,000+) - must be divisible by 1B
124-
if (absValue >= 1e9 && absValue % 1e9 === 0) {
125-
return sign + (absValue / 1e9).toFixed(0) + 'B';
112+
function formatCompact(n, divisor, suffix) {
113+
var compact = n / divisor;
114+
var decimals = compact >= 100 ? 0 : (compact >= 10 ? 1 : 2);
115+
var rounded = compact.toFixed(decimals);
116+
if (rounded.indexOf('.') !== -1) {
117+
rounded = rounded.replace(/0+$/, '').replace(/\.$/, '');
118+
}
119+
return rounded + suffix;
126120
}
127121

128-
// Millions (1,000,000+) - must be divisible by 1M
129-
if (absValue >= 1e6 && absValue % 1e6 === 0) {
130-
return sign + (absValue / 1e6).toFixed(0) + 'M';
131-
}
122+
// Always use the highest magnitude for million+ values
123+
if (absValue >= 1e12) return formatCompact(value, 1e12, 'T');
124+
if (absValue >= 1e9) return formatCompact(value, 1e9, 'B');
125+
if (absValue >= 1e6) return formatCompact(value, 1e6, 'M');
132126

133-
// Thousands (1,000+) - must be divisible by 1K
127+
// For thousands, keep comma format unless it's a clean thousand (e.g., 75,000 -> 75K)
134128
if (absValue >= 1e3 && absValue % 1e3 === 0) {
135-
return sign + (absValue / 1e3).toFixed(0) + 'K';
129+
return formatCompact(value, 1e3, 'K');
136130
}
137131

138-
// Otherwise use thousand separators for non-abbreviatable integers
139132
return value.toLocaleString('en-US');
140133
};
141134

@@ -157,6 +150,13 @@ window.PYWRY_AGGRID_PROCESS_COLUMN_DEFS = function(columnDefs) {
157150
delete processed.cellDataType;
158151
}
159152

153+
if (processed.headerTooltip === undefined || processed.headerTooltip === null || processed.headerTooltip === '') {
154+
var headerLabel = processed.headerName || processed.field || processed.colId;
155+
if (headerLabel !== undefined && headerLabel !== null && String(headerLabel).length > 8) {
156+
processed.headerTooltip = String(headerLabel);
157+
}
158+
}
159+
160160
// Convert valueGetter string to function
161161
// Expression can use: params, data, node, colDef, column, api, columnApi, context
162162
if (typeof processed.valueGetter === 'string') {
@@ -288,6 +288,7 @@ window.PYWRY_AGGRID_BUILD_OPTIONS = function(config, gridId) {
288288
*/
289289
window.PYWRY_AGGRID_BUILD_CLIENT_OPTIONS = function(config, id, rowData, rowCount, truncatedRows) {
290290
var LARGE_DATASET_THRESHOLD = 10000;
291+
var isNativeWebKit = document.documentElement.classList.contains('pywry-native');
291292

292293
// Pagination logic:
293294
// - If config.pagination === true: always enable
@@ -337,6 +338,9 @@ window.PYWRY_AGGRID_BUILD_CLIENT_OPTIONS = function(config, id, rowData, rowCoun
337338
suppressMenuHide: true,
338339
enableCellTextSelection: true,
339340
ensureDomOrder: true,
341+
suppressAnimationFrame: isNativeWebKit,
342+
tooltipShowDelay: 1200,
343+
tooltipHideDelay: 5000,
340344
// Row spanning support (AG Grid v32+)
341345
enableCellSpan: config.enableCellSpan || false,
342346

@@ -390,6 +394,16 @@ window.PYWRY_AGGRID_BUILD_CLIENT_OPTIONS = function(config, id, rowData, rowCoun
390394
' of ' + (rowCount + truncatedRows).toLocaleString() + ' rows'
391395
});
392396
}
397+
},
398+
399+
onBodyScroll: function(event) {
400+
if (!isNativeWebKit) return;
401+
if (!event || !event.api) return;
402+
window.requestAnimationFrame(function() {
403+
try {
404+
event.api.redrawRows();
405+
} catch (e) {}
406+
});
393407
}
394408
};
395409

@@ -415,6 +429,7 @@ window.PYWRY_AGGRID_BUILD_SERVER_SIDE_OPTIONS = function(config, id, serverConfi
415429
var totalRows = serverConfig.totalRows || 0;
416430
var blockSize = serverConfig.blockSize || 500; // Rows per block for infinite scroll
417431
var currentFilteredTotal = totalRows;
432+
var isNativeWebKit = document.documentElement.classList.contains('pywry-native');
418433

419434
// Pending requests
420435
var pendingRequests = {};
@@ -517,6 +532,9 @@ window.PYWRY_AGGRID_BUILD_SERVER_SIDE_OPTIONS = function(config, id, serverConfi
517532
suppressMenuHide: true,
518533
enableCellTextSelection: true,
519534
ensureDomOrder: true,
535+
suppressAnimationFrame: isNativeWebKit,
536+
tooltipShowDelay: 1200,
537+
tooltipHideDelay: 5000,
520538

521539
// Row ID for selection persistence
522540
getRowId: function(params) {
@@ -616,6 +634,16 @@ window.PYWRY_AGGRID_BUILD_SERVER_SIDE_OPTIONS = function(config, id, serverConfi
616634
' rows). Use filters to narrow down results.'
617635
});
618636
}
637+
},
638+
639+
onBodyScroll: function(event) {
640+
if (!isNativeWebKit) return;
641+
if (!event || !event.api) return;
642+
window.requestAnimationFrame(function() {
643+
try {
644+
event.api.redrawRows();
645+
} catch (e) {}
646+
});
619647
}
620648
};
621649

pywry/pywry/frontend/src/main.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,21 @@ function setupCustomScrollbar(scrollContainer) {
305305
// Skip if already set up
306306
if (scrollContainer.dataset.customScrollbar) return;
307307

308-
// Skip Plotly containers only - they have their own responsive layout
308+
// Skip Plotly containers - they have their own responsive layout
309309
if (scrollContainer.querySelector('.pywry-plotly') ||
310310
scrollContainer.querySelector('.js-plotly-plot')) {
311311
scrollContainer.dataset.customScrollbar = 'skipped';
312312
return;
313313
}
314314

315+
// Skip AG Grid containers - AG Grid manages its own scrolling/virtualization
316+
if (scrollContainer.querySelector('.pywry-grid') ||
317+
scrollContainer.querySelector('.ag-root-wrapper') ||
318+
scrollContainer.closest('.ag-root-wrapper')) {
319+
scrollContainer.dataset.customScrollbar = 'skipped';
320+
return;
321+
}
322+
315323
scrollContainer.dataset.customScrollbar = 'true';
316324

317325
// Standard pywry-scroll-container handling
@@ -354,6 +362,22 @@ function setupCustomScrollbar(scrollContainer) {
354362
var scrollTimeout = null;
355363
var trackPadding = 6; // Padding inside the track for the thumb
356364

365+
function disableCustomScrollbarForAgGrid() {
366+
if (!(scrollContainer.querySelector('.pywry-grid') ||
367+
scrollContainer.querySelector('.ag-root-wrapper') ||
368+
scrollContainer.closest('.ag-root-wrapper'))) {
369+
return false;
370+
}
371+
if (trackV.parentNode) trackV.parentNode.removeChild(trackV);
372+
if (trackH.parentNode) trackH.parentNode.removeChild(trackH);
373+
wrapper.classList.remove('has-both-scrollbars', 'is-scrolling');
374+
scrollContainer.classList.remove('has-scrollbar-v', 'has-scrollbar-h');
375+
scrollContainer.dataset.customScrollbar = 'skipped';
376+
return true;
377+
}
378+
379+
if (disableCustomScrollbarForAgGrid()) return;
380+
357381
function updateScrollbars() {
358382
var scrollHeight = scrollContainer.scrollHeight;
359383
var clientHeight = scrollContainer.clientHeight;
@@ -519,6 +543,12 @@ function setupCustomScrollbar(scrollContainer) {
519543
window.addEventListener('resize', updateScrollbars);
520544

521545
// Re-observe for content changes
522-
var contentObserver = new MutationObserver(updateScrollbars);
546+
var contentObserver = new MutationObserver(function() {
547+
if (disableCustomScrollbarForAgGrid()) {
548+
contentObserver.disconnect();
549+
return;
550+
}
551+
updateScrollbars();
552+
});
523553
contentObserver.observe(scrollContainer, { childList: true, subtree: true, characterData: true });
524554
}

pywry/pywry/frontend/src/scrollbar.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,22 @@ window.PYWRY_SCROLLBARS = (function() {
119119
var scrollTimeout = null;
120120
var trackPadding = 6; // Padding inside the track for the thumb
121121

122+
function disableCustomScrollbarForAgGrid() {
123+
if (!(scrollContainer.querySelector('.pywry-grid') ||
124+
scrollContainer.querySelector('.ag-root-wrapper') ||
125+
scrollContainer.closest('.ag-root-wrapper'))) {
126+
return false;
127+
}
128+
if (trackV.parentNode) trackV.parentNode.removeChild(trackV);
129+
if (trackH.parentNode) trackH.parentNode.removeChild(trackH);
130+
wrapper.classList.remove('has-both-scrollbars', 'is-scrolling');
131+
scrollContainer.classList.remove('has-scrollbar-v', 'has-scrollbar-h');
132+
scrollContainer.dataset.customScrollbar = 'skipped';
133+
return true;
134+
}
135+
136+
if (disableCustomScrollbarForAgGrid()) return;
137+
122138
function updateScrollbars() {
123139
var scrollHeight = scrollContainer.scrollHeight;
124140
var clientHeight = scrollContainer.clientHeight;
@@ -296,6 +312,10 @@ window.PYWRY_SCROLLBARS = (function() {
296312
// Use ResizeObserver to track size changes - observe ALL descendants AND toolbars
297313
if (typeof ResizeObserver !== 'undefined') {
298314
var resizeObserver = new ResizeObserver(function() {
315+
if (disableCustomScrollbarForAgGrid()) {
316+
resizeObserver.disconnect();
317+
return;
318+
}
299319
updateScrollbars();
300320
});
301321

@@ -326,6 +346,11 @@ window.PYWRY_SCROLLBARS = (function() {
326346

327347
// Watch for new elements and observe them too (including new toolbars)
328348
var elementObserver = new MutationObserver(function(mutations) {
349+
if (disableCustomScrollbarForAgGrid()) {
350+
resizeObserver.disconnect();
351+
elementObserver.disconnect();
352+
return;
353+
}
329354
mutations.forEach(function(mutation) {
330355
mutation.addedNodes.forEach(function(node) {
331356
if (node.nodeType === 1) {

pywry/pywry/frontend/style/pywry.css

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ html.pywry-native .pywry-scroll-container::-webkit-scrollbar {
2121
display: none;
2222
}
2323
/* Only reserve space for scrollbar when content overflows (but not for Plotly) */
24-
html.pywry-native .pywry-scroll-container.has-scrollbar-v:not(:has(.pywry-plotly)):not(:has(.js-plotly-plot)),
25-
.pywry-custom-scrollbar .pywry-scroll-container.has-scrollbar-v:not(:has(.pywry-plotly)):not(:has(.js-plotly-plot)) {
24+
html.pywry-native .pywry-scroll-container.has-scrollbar-v:not(:has(.pywry-plotly)):not(:has(.js-plotly-plot)):not(:has(.pywry-grid)):not(:has(.ag-root-wrapper)),
25+
.pywry-custom-scrollbar .pywry-scroll-container.has-scrollbar-v:not(:has(.pywry-plotly)):not(:has(.js-plotly-plot)):not(:has(.pywry-grid)):not(:has(.ag-root-wrapper)) {
2626
padding-right: 18px; /* 4px margin + 10px track + 4px inner margin */
2727
}
28-
html.pywry-native .pywry-scroll-container.has-scrollbar-h:not(:has(.pywry-plotly)):not(:has(.js-plotly-plot)),
29-
.pywry-custom-scrollbar .pywry-scroll-container.has-scrollbar-h:not(:has(.pywry-plotly)):not(:has(.js-plotly-plot)) {
28+
html.pywry-native .pywry-scroll-container.has-scrollbar-h:not(:has(.pywry-plotly)):not(:has(.js-plotly-plot)):not(:has(.pywry-grid)):not(:has(.ag-root-wrapper)),
29+
.pywry-custom-scrollbar .pywry-scroll-container.has-scrollbar-h:not(:has(.pywry-plotly)):not(:has(.js-plotly-plot)):not(:has(.pywry-grid)):not(:has(.ag-root-wrapper)) {
3030
padding-bottom: 18px; /* 4px margin + 10px track + 4px inner margin */
3131
}
3232
html.pywry-native .pywry-scroll-container::-webkit-scrollbar,
@@ -97,6 +97,20 @@ html.pywry-native .ag-root-wrapper.is-scrolling .pywry-scrollbar-track-h,
9797
opacity: 1;
9898
}
9999

100+
/* AG Grid uses its own scrollbars/virtualization; disable custom overlay tracks */
101+
html.pywry-native .pywry-scroll-wrapper:has(.pywry-grid) > .pywry-scrollbar-track-v,
102+
html.pywry-native .pywry-scroll-wrapper:has(.pywry-grid) > .pywry-scrollbar-track-h,
103+
html.pywry-native .pywry-scroll-wrapper:has(.ag-root-wrapper) > .pywry-scrollbar-track-v,
104+
html.pywry-native .pywry-scroll-wrapper:has(.ag-root-wrapper) > .pywry-scrollbar-track-h,
105+
.pywry-custom-scrollbar .pywry-scroll-wrapper:has(.pywry-grid) > .pywry-scrollbar-track-v,
106+
.pywry-custom-scrollbar .pywry-scroll-wrapper:has(.pywry-grid) > .pywry-scrollbar-track-h,
107+
.pywry-custom-scrollbar .pywry-scroll-wrapper:has(.ag-root-wrapper) > .pywry-scrollbar-track-v,
108+
.pywry-custom-scrollbar .pywry-scroll-wrapper:has(.ag-root-wrapper) > .pywry-scrollbar-track-h {
109+
pointer-events: none !important;
110+
opacity: 0 !important;
111+
display: none !important;
112+
}
113+
100114
/* Custom scrollbar thumb - vertical */
101115
html.pywry-native .pywry-scrollbar-thumb-v,
102116
.pywry-custom-scrollbar .pywry-scrollbar-thumb-v {
@@ -2698,6 +2712,16 @@ html.light .pywry-input-date {
26982712
outline: none !important;
26992713
}
27002714

2715+
.ag-header-cell-label {
2716+
min-width: 0;
2717+
}
2718+
2719+
.ag-header-cell-text {
2720+
overflow: hidden;
2721+
text-overflow: ellipsis;
2722+
white-space: nowrap;
2723+
}
2724+
27012725
/* AG Grid scrollbar styling - NATIVE WINDOW ONLY */
27022726
/* Use AG Grid's browser-color-scheme parameter for appropriate scrollbar colors on macOS */
27032727
/* Dark themes get dark scrollbars */

pywry/pywry/grid.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,13 @@ class GridOptions(AGGridModel):
481481
row_selection: dict[str, Any] | bool | None = Field(default=None, alias="rowSelection")
482482
cell_selection: bool | None = Field(default=True, alias="cellSelection")
483483

484+
@field_validator("row_selection", mode="before")
485+
@classmethod
486+
def _coerce_row_selection(cls, v: Any) -> dict[str, Any] | bool | None:
487+
if isinstance(v, RowSelection):
488+
return v.to_dict()
489+
return v
490+
484491
# === Layout ===
485492
dom_layout: DomLayoutType = Field(default="normal", alias="domLayout")
486493

pywry/pywry/mcp/handlers.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,6 @@ def _handle_chat_stop_generation(ctx: HandlerContext) -> HandlerResult:
857857

858858

859859
def _handle_chat_manage_thread(ctx: HandlerContext) -> HandlerResult:
860-
861860
widget_id = ctx.args["widget_id"]
862861
action = ctx.args["action"]
863862
thread_id = ctx.args.get("thread_id")

0 commit comments

Comments
 (0)