Is it possible to resize all columns while enjoying the benefits of virtualization? If your grid cells contain only text and you know the font then yes, it's possible. In this post, you will see a module that can collect minimal width needed for each column. You will see a demo app and the usage.
TL;DR
You can use measureText method to calculate widths of text and then use setColumnWidth to adjust columns. This the demo and this is the repo. It's quite likely that you don't need this technique (suppressColumnVirtualisation
might be enough), but read on if your grid has huge amount of columns and users need high data density...
Virtualization Vs Resize
I've been porting some old UI with grids based on HTML table to ag-Grid
. Users appreciate the features added by ag-Grid
(such as filters, pinning, sorting, resizing, reordering, grouping...) but noticed one change for worse: not all columns were automatically sized to take the smallest amount of space possible. In enterprise applications, data density is really important (not recognizing that is a recipe for failure). Too much white space means that people might miss important information or have to waste time scrolling...
ag-Grid
is capable of handing huge amount of rows and columns thanks to virtualization. The grid creates elements only for rows and columns which are currently visible (plus some buffer) to avoid producing bloated DOM (too many elements severely degrade performance). Virtualization means that the API methods designed to automatically resize columns are only capable of resizing the rendered columns. ag-Grid
has suppressColumnVirtualisation
option but in my case, girds can have as many as 300 columns so turning virtualization off is not possible for performance reasons.
The Solution
So is it possible to resize all columns while enjoying the benefits of virtualization? If your grid cells contain only text and you know the font then yes, it's possible.
You can calculate the width of text for each grid cell and collect the minimal width needed for each of the gird columns. ag-Grid
API might then be used to set desired column sizes. Text width calculation could be done efficiently with measureText method on Canvas.
Here's a module that can collect minimal width needed for each column (it doesn't have any dependency on React):
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const collectMaxWidth = (text, group, maxWidths) => {
const width = context.measureText(text).width;
const maxWidth = maxWidths.get(group);
if (maxWidth === undefined || width > maxWidth) {
maxWidths.set(group, width);
}
};
const collectMaxWidthCached = (text, group, maxWidths, widthsCache) => {
const cachedWidth = widthsCache.get(text);
let width;
if (cachedWidth === undefined) {
width = context.measureText(text).width;
widthsCache.set(text, width);
} else {
width = cachedWidth;
}
const maxWidth = maxWidths.get(group);
if (maxWidth === undefined || width > maxWidth) {
maxWidths.set(group, width);
}
};
const calculateColumnWidths = config => {
console.time('Column widths calculation');
const maxWidths = new Map()
if (config.measureHeaders) {
context.font = config.headerFont;
config.columnDefs.forEach(column => {
collectMaxWidth(column.headerName, column.field, maxWidths);
});
}
context.font = config.rowFont;
config.rowData.forEach(row => {
config.columnDefs.forEach(column => {
if (config.cache) {
collectMaxWidthCached(row[column.field],
column.field, maxWidths, config.cache);
} else {
collectMaxWidth(row[column.field], column.field, maxWidths);
}
});
});
const updatedColumnDefs = config.columnDefs.map(cd => ({
...cd,
width: Math.ceil(maxWidths.get(cd.field) + config.padding)
}));
console.timeEnd('Column widths calculation');
return updatedColumnDefs;
};
export default calculateColumnWidths;
The module creates canvas
element and then a 2D context is retrieved from it. The context
is used in collectMaxWidth
function to measure size of provided text by invoking context.measureText(text).width
.
The module also has collectMaxWidthCached
function which can offer a performance improvement based on the fact that grid data is often repetitive. If some string was already measured, there's no need to use canvas
API again - taking the value from JavaScript Map is super quick. So unless you are extremely worried about memory limits, use the cached version.
The module exports calculateColumnWidths
function which takes config
object with following properties:
columnDefs
- Array used to specify ag-Grid columns (limit this if you have hidden columns) rowData
- Array of grid records (limit this if you use paging or filtering) measureHeaders
- If true
, then column headers should be taken into account (caveat: header icons are ignored) headerFont
- Determines column header font rowFont
- Determines normal grid cell font padding
- Additional width added to measured text (you need to choose it experimentally, mind varying header icons). cache
- JS Map used for speed boost (length of each unique non-header text is measured only once), pass null
to skip caching.
Below is an example of button's click handler from my demo app what uses calculateColumnWidths
:
const handleResizeWithCustomClick = () => {
console.time('Resize all columns (including widths calculation)');
if (gridApi && gridColumnApi) {
const updatedColumDefs = calculateColumnWidths({
columnDefs,
rowData,
measureHeaders: true,
headerFont: 'bold 12px Arial',
rowFont: 'normal 12px Arial',
padding: 30,
cache: useWidthsCache ? textWidthsCache : null
});
updatedColumDefs.forEach(def => gridColumnApi.setColumnWidth(def.field, def.width));
}
console.timeEnd('Resize all columns (including widths calculation)');
};
Notice how setColumnWidth
from ag-Grid
Column API is used to apply calculated width. Mind the comments about paging/filtering and the difference between using setColumnWidth
and calling Grid API setColumnDefs
or updating state bound to grid's columnDefs
property...
Demo App
The app uses React 16.13.1 and ag-Grid Community 23.0.1 (I've tested it in Chrome 80, Firefox 74, Edge 44).
Clone the repo, do npm install
and npm start
to run the app locally (just like with any other thing started with create-react-app
)...
Usage
Click on "Generate data (100 rows with 300 columns)" button to create 30K grid cells. You should then click the "Resize columns with columnApi.autoSizeColumns" button and scroll to the right to notice that only visible columns were resized:
Now reload the page, click data generation again and press "Resize columns with custom text measure
" and scroll the gird horizontally. You should notice that all columns were resized:
Clicking resizing button the second time should give you better performance because of text measure caching and because ag-Grid
itself has less work to do. These are the timings from Chrome:
I would say that 100ms for 30K cell measure is quite fast! With caching, it drops to 5ms! You may also notice that most of the time is not really spent in my text measuring function but in the ag-Grid which handles columns resize.
I've noticed one weird performance issue: if you scroll to the right of the grid before clicking the resize button, the time spent by ag-Grid
handling columnApi.autoSizeColumns
calls increases significantly. Doing bulk resize with gridApi.setColumnDefs
or by updating array used in for columnDefs
property solves the performance issue at the cost of column settings reset (exact behavior depends on deltaColumnMode)...