Skip to content

Commit 545ddd8

Browse files
Use ag-grid in data viewer (#708)
* Use ag-grid * Update dataview functions * Update grid resize * Use balham theme (more compact) * Update grid config * Update grid options * Remove bookstrap from webpack config * getListHtml supports theming * No need to convert factor matrix * Use '(row)' as the header of rownames * Auto update theme * Remove unused import * Update yarn.lock * Use filterParams * Update gridOptions to allow copy * Update list viewer css * Update json viewer css * Update json viewer css * Update json viewer css * CSS for dataviewer - May necessitate a .css file? * Add .ag-row-hover * Update css Co-authored-by: ElianHugh <[email protected]>
1 parent c126649 commit 545ddd8

File tree

5 files changed

+227
-137
lines changed

5 files changed

+227
-137
lines changed

R/vsc.R

Lines changed: 37 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -241,26 +241,38 @@ if (use_httpgd && "httpgd" %in% .packages(all.available = TRUE)) {
241241

242242
show_view <- !identical(getOption("vsc.view", "Two"), FALSE)
243243
if (show_view) {
244-
dataview_data_type <- function(x) {
245-
if (is.numeric(x)) {
246-
if (is.null(attr(x, "class"))) {
247-
"num"
248-
} else {
249-
"num-fmt"
244+
get_column_def <- function(name, field, value) {
245+
filter <- TRUE
246+
if (is.numeric(value)) {
247+
type <- "numericColumn"
248+
if (is.null(attr(value, "class"))) {
249+
filter <- "agNumberColumnFilter"
250250
}
251-
} else if (inherits(x, "Date")) {
252-
"date"
251+
} else if (inherits(value, "Date")) {
252+
type <- "dateColumn"
253+
filter <- "agDateColumnFilter"
253254
} else {
254-
"string"
255+
type <- "textColumn"
256+
filter <- "agTextColumnFilter"
255257
}
258+
list(
259+
headerName = name,
260+
field = field,
261+
type = type,
262+
filter = filter
263+
)
256264
}
257265

258266
dataview_table <- function(data) {
267+
if (is.matrix(data)) {
268+
data <- as.data.frame.matrix(data)
269+
}
270+
259271
if (is.data.frame(data)) {
260272
nrow <- nrow(data)
261273
colnames <- colnames(data)
262274
if (is.null(colnames)) {
263-
colnames <- sprintf("(X%d)", seq_len(ncol(data)))
275+
colnames <- sprintf("V%d", seq_len(ncol(data)))
264276
} else {
265277
colnames <- trimws(colnames)
266278
}
@@ -270,49 +282,23 @@ if (show_view) {
270282
} else {
271283
rownames <- seq_len(nrow)
272284
}
285+
colnames <- c("(row)", colnames)
286+
fields <- sprintf("x%d", seq_along(colnames))
273287
data <- c(list(" " = rownames), .subset(data))
274-
colnames <- c(" ", colnames)
275-
types <- vapply(data, dataview_data_type,
276-
character(1L), USE.NAMES = FALSE)
277-
data <- vapply(data, function(x) {
278-
trimws(format(x))
279-
}, character(nrow), USE.NAMES = FALSE)
280-
dim(data) <- c(length(rownames), length(colnames))
281-
} else if (is.matrix(data)) {
282-
if (is.factor(data)) {
283-
data <- format(data)
284-
}
285-
types <- rep(dataview_data_type(data), ncol(data))
286-
colnames <- colnames(data)
287-
colnames(data) <- NULL
288-
if (is.null(colnames)) {
289-
colnames <- sprintf("(X%d)", seq_len(ncol(data)))
290-
} else {
291-
colnames <- trimws(colnames)
292-
}
293-
rownames <- rownames(data)
294-
rownames(data) <- NULL
295-
data <- trimws(format(data))
296-
if (is.null(rownames)) {
297-
types <- c("num", types)
298-
rownames <- seq_len(nrow(data))
299-
} else {
300-
types <- c("string", types)
301-
rownames <- trimws(rownames)
302-
}
303-
dim(data) <- c(length(rownames), length(colnames))
304-
colnames <- c(" ", colnames)
305-
data <- cbind(rownames, data)
288+
names(data) <- fields
289+
class(data) <- "data.frame"
290+
attr(data, "row.names") <- .set_row_names(nrow)
291+
columns <- .mapply(get_column_def,
292+
list(colnames, fields, data),
293+
NULL
294+
)
295+
list(
296+
columns = columns,
297+
data = data
298+
)
306299
} else {
307-
stop("data must be data.frame or matrix")
300+
stop("data must be a data.frame or a matrix")
308301
}
309-
columns <- .mapply(function(title, type) {
310-
class <- if (type == "string") "text-left" else "text-right"
311-
list(title = scalar(title),
312-
className = scalar(class),
313-
type = scalar(type))
314-
}, list(colnames, types), NULL)
315-
list(columns = columns, data = data)
316302
}
317303

318304
show_dataview <- function(x, title, uuid = NULL,
@@ -377,7 +363,7 @@ if (show_view) {
377363
if (is.data.frame(x) || is.matrix(x)) {
378364
data <- dataview_table(x)
379365
file <- tempfile(tmpdir = tempdir, fileext = ".json")
380-
jsonlite::write_json(data, file, matrix = "rowmajor")
366+
jsonlite::write_json(data, file, auto_unbox = TRUE)
381367
request("dataview", source = "table", type = "json",
382368
title = title, file = file, viewer = viewer, uuid = uuid)
383369
} else if (is.list(x)) {

package.json

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,12 +1434,10 @@
14341434
"yamljs": "^0.3.0"
14351435
},
14361436
"dependencies": {
1437+
"ag-grid-community": "^25.3.0",
14371438
"bootstrap": "^5.0.1",
14381439
"cheerio": "1.0.0-rc.10",
14391440
"crypto": "^1.0.1",
1440-
"datatables.net": "^1.10.25",
1441-
"datatables.net-bs4": "^1.10.25",
1442-
"datatables.net-fixedheader-jqui": "^3.1.9",
14431441
"ejs": "^3.1.6",
14441442
"fs-extra": "^10.0.0",
14451443
"highlight.js": "^10.7.2",

src/session.ts

Lines changed: 179 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -347,51 +347,165 @@ export async function showDataView(source: string, type: string, title: string,
347347
export async function getTableHtml(webview: Webview, file: string): Promise<string> {
348348
resDir = isGuestSession ? guestResDir : resDir;
349349
const content = await readContent(file, 'utf8');
350-
351350
return `
352351
<!DOCTYPE html>
353352
<html lang="en">
354353
<head>
355354
<meta charset="utf-8">
356355
<meta name="viewport" content="width=device-width, initial-scale=1">
357-
<link href="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'bootstrap.min.css'))))}" rel="stylesheet">
358-
<link href="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'dataTables.bootstrap4.min.css'))))}" rel="stylesheet">
359-
<link href="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'fixedHeader.jqueryui.min.css'))))}" rel="stylesheet">
360-
<style type="text/css">
356+
<style media="only screen">
357+
html, body {
358+
height: 100%;
359+
width: 100%;
360+
margin: 0;
361+
box-sizing: border-box;
362+
-webkit-overflow-scrolling: touch;
363+
}
364+
365+
html {
366+
position: absolute;
367+
top: 0;
368+
left: 0;
369+
padding: 0;
370+
overflow: auto;
371+
}
372+
361373
body {
362-
color: black;
363-
background-color: white;
374+
padding: 0;
375+
overflow: auto;
376+
}
377+
378+
/* Styling for wrapper and header */
379+
380+
[class*="vscode"] div.ag-root-wrapper {
381+
background-color: var(--vscode-editor-background);
382+
}
383+
384+
[class*="vscode"] div.ag-header {
385+
background-color: var(--vscode-sideBar-background);
386+
}
387+
388+
[class*="vscode"] div.ag-header-cell[aria-sort="ascending"], div.ag-header-cell[aria-sort="descending"] {
389+
color: var(--vscode-textLink-activeForeground);
364390
}
365-
table {
366-
font-size: 0.75em;
391+
392+
/* Styling for rows and cells */
393+
394+
[class*="vscode"] div.ag-row {
395+
color: var(--vscode-editor-foreground);
396+
}
397+
398+
[class*="vscode"] .ag-row-hover {
399+
background-color: var(--vscode-list-hoverBackground) !important;
400+
color: var(--vscode-list-hoverForeground);
401+
}
402+
403+
[class*="vscode"] .ag-row-selected {
404+
background-color: var(--vscode-editor-selectionBackground) !important;
405+
color: var(--vscode-editor-selectionForeground) !important;
406+
}
407+
408+
[class*="vscode"] div.ag-row-even {
409+
border: 0px;
410+
background-color: var(--vscode-editor-background);
411+
}
412+
413+
[class*="vscode"] div.ag-row-odd {
414+
border: 0px;
415+
background-color: var(--vscode-sideBar-background);
416+
}
417+
418+
[class*="vscode"] div.ag-ltr div.ag-has-focus div.ag-cell-focus:not(div.ag-cell-range-selected) {
419+
border-color: var(--vscode-editorCursor-foreground);
420+
}
421+
422+
/* Styling for the filter pop-up */
423+
424+
[class*="vscode"] div.ag-menu {
425+
background-color: var(--vscode-notifications-background);
426+
color: var(--vscode-notifications-foreground);
427+
border-color: var(--vscode-notifications-border);
428+
}
429+
430+
[class*="vscode"] div.ag-filter-apply-panel-button {
431+
background-color: var(--vscode-button-background);
432+
color: var(--vscode-button-foreground);
433+
border: 0;
434+
padding: 5px 10px;
435+
font-size: 12px;
436+
}
437+
438+
[class*="vscode"] div.ag-picker-field-wrapper {
439+
background-color: var(--vscode-editor-background);
440+
color: var(--vscode-editor-foreground);
441+
border-color: var(--vscode-notificationCenter-border);
442+
}
443+
444+
[class*="vscode"] input[class^=ag-] {
445+
border-color: var(--vscode-notificationCenter-border) !important;
367446
}
368447
</style>
369-
</head>
370-
<body>
371-
<div class="container-fluid">
372-
<table id="data-table" class="display table table-sm table-striped table-condensed table-hover"></table>
373-
</div>
374-
<script src="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'jquery.min.js'))))}"></script>
375-
<script src="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'jquery.dataTables.min.js'))))}"></script>
376-
<script src="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'dataTables.bootstrap4.min.js'))))}"></script>
377-
<script src="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'dataTables.fixedHeader.min.js'))))}"></script>
378-
<script src="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'fixedHeader.jqueryui.min.js'))))}"></script>
448+
<script src="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'ag-grid-community.min.noStyle.js'))))}"></script>
449+
<link href="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'ag-grid.min.css'))))}" rel="stylesheet">
450+
<link href="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'ag-theme-balham.min.css'))))}" rel="stylesheet">
451+
<link href="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'ag-theme-balham-dark.min.css'))))}" rel="stylesheet">
379452
<script>
380-
var data = ${String(content)};
381-
$(document).ready(function () {
382-
$("#data-table").DataTable({
383-
data: data.data,
384-
columns: data.columns,
385-
paging: false,
386-
autoWidth: false,
387-
order: [],
388-
fixedHeader: true
389-
});
390-
$("#data-table tbody").on("click", "tr", function() {
391-
$(this).toggleClass("table-active");
453+
const data = ${String(content)};
454+
function updateTheme() {
455+
const gridDiv = document.querySelector('#myGrid');
456+
if (document.body.classList.contains('vscode-light')) {
457+
gridDiv.className = 'ag-theme-balham';
458+
} else {
459+
gridDiv.className = 'ag-theme-balham-dark';
460+
}
461+
}
462+
function autoSizeAll(skipHeader) {
463+
var allColumnIds = [];
464+
gridOptions.columnApi.getAllColumns().forEach(function (column) {
465+
allColumnIds.push(column.colId);
392466
});
467+
gridOptions.columnApi.autoSizeColumns(allColumnIds, skipHeader);
468+
}
469+
const gridOptions = {
470+
defaultColDef: {
471+
sortable: true,
472+
resizable: true,
473+
filter: true,
474+
filterParams: {
475+
buttons: ['reset', 'apply']
476+
}
477+
},
478+
columnDefs: data.columns,
479+
rowData: data.data,
480+
rowSelection: 'multiple',
481+
pagination: true,
482+
enableCellTextSelection: true,
483+
ensureDomOrder: true,
484+
onGridReady: function (params) {
485+
gridOptions.api.sizeColumnsToFit();
486+
autoSizeAll(false);
487+
}
488+
};
489+
document.addEventListener('DOMContentLoaded', () => {
490+
const gridDiv = document.querySelector('#myGrid');
491+
new agGrid.Grid(gridDiv, gridOptions);
393492
});
493+
function onload() {
494+
updateTheme();
495+
const observer = new MutationObserver(function (event) {
496+
updateTheme();
497+
});
498+
observer.observe(document.body, {
499+
attributes: true,
500+
attributeFilter: ['class'],
501+
childList: false,
502+
characterData: false
503+
});
504+
}
394505
</script>
506+
</head>
507+
<body onload='onload()'>
508+
<div id="myGrid" style="height: 100%;"></div>
395509
</body>
396510
</html>
397511
`;
@@ -412,11 +526,42 @@ export async function getListHtml(webview: Webview, file: string): Promise<strin
412526
<link href="${String(webview.asWebviewUri(Uri.file(path.join(resDir, 'jquery.json-viewer.css'))))}" rel="stylesheet">
413527
<style type="text/css">
414528
body {
415-
color: black;
416-
background-color: white;
529+
color: var(--vscode-editor-foreground);
530+
background-color: var(--vscode-editor-background);
531+
}
532+
533+
.json-document {
534+
padding: 0 0;
417535
}
536+
418537
pre#json-renderer {
419-
border: 1px solid #aaa;
538+
font-family: var(--vscode-editor-font-family);
539+
border: 0;
540+
}
541+
542+
ul.json-dict, ol.json-array {
543+
color: var(--vscode-symbolIcon-fieldForeground);
544+
border-left: 1px dotted var(--vscode-editorLineNumber-foreground);
545+
}
546+
547+
.json-literal {
548+
color: var(--vscode-symbolIcon-variableForeground);
549+
}
550+
551+
.json-string {
552+
color: var(--vscode-symbolIcon-stringForeground);
553+
}
554+
555+
a.json-toggle:before {
556+
color: var(--vscode-button-secondaryBackground);
557+
}
558+
559+
a.json-toggle:hover:before {
560+
color: var(--vscode-button-secondaryHoverBackground);
561+
}
562+
563+
a.json-placeholder {
564+
color: var(--vscode-input-placeholderForeground);
420565
}
421566
</style>
422567
<script>

webpack.config.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,10 @@ module.exports = {
4242
patterns: [
4343
{ from: './node_modules/jquery/dist/jquery.min.js', to: 'resources' },
4444
{ from: './node_modules/jquery.json-viewer/json-viewer', to: 'resources' },
45-
{ from: './node_modules/bootstrap/dist/js/bootstrap.min.js', to: 'resources' },
46-
{ from: './node_modules/bootstrap/dist/css/bootstrap.min.css', to: 'resources' },
47-
{ from: './node_modules/datatables.net-bs4/js/dataTables.bootstrap4.min.js', to: 'resources' },
48-
{ from: './node_modules/datatables.net-bs4/css/dataTables.bootstrap4.min.css', to: 'resources' },
49-
{ from: './node_modules/datatables.net/js/jquery.dataTables.min.js', to: 'resources' },
50-
{ from: './node_modules/datatables.net-fixedheader/js/dataTables.fixedHeader.min.js', to: 'resources' },
51-
{ from: './node_modules/datatables.net-fixedheader-jqui/js/fixedHeader.jqueryui.min.js', to: 'resources' },
52-
{ from: './node_modules/datatables.net-fixedheader-jqui/css/fixedHeader.jqueryui.min.css', to: 'resources' },
45+
{ from: './node_modules/ag-grid-community/dist/ag-grid-community.min.noStyle.js', to: 'resources' },
46+
{ from: './node_modules/ag-grid-community/dist/styles/ag-grid.min.css', to: 'resources' },
47+
{ from: './node_modules/ag-grid-community/dist/styles/ag-theme-balham.min.css', to: 'resources' },
48+
{ from: './node_modules/ag-grid-community/dist/styles/ag-theme-balham-dark.min.css', to: 'resources' },
5349
]
5450
}),
5551
],

0 commit comments

Comments
 (0)