Skip to content

Commit 980498a

Browse files
authored
wip: kernel computation feature (#27)
* fix: improve data parser * feat: init kernel computing feature powered by duckdb
1 parent f93a1e6 commit 980498a

File tree

12 files changed

+284
-48
lines changed

12 files changed

+284
-48
lines changed

DESCRIPTION

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: GWalkR
22
Title: Interactive Exploratory Data Analysis Tool
3-
Version: 0.1.5
3+
Version: 0.2.0
44
Authors@R: c(
55
person("Yue", "Yu", , "[email protected]", role = c("aut", "cre"),
66
comment = c(ORCID = "0000-0002-9302-0793")),
@@ -17,4 +17,7 @@ Imports:
1717
htmlwidgets,
1818
jsonlite,
1919
openssl,
20-
shiny
20+
shiny,
21+
shinycssloaders,
22+
DBI,
23+
duckdb

NAMESPACE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
export(gwalkr)
44
export(gwalkrOutput)
55
export(renderGwalkr)
6+
import(DBI)
7+
import(duckdb)
68
import(htmlwidgets)
79
import(openssl)
810
import(shiny)
11+
import(shinycssloaders)
12+
importFrom(jsonlite,toJSON)

R/duckdb_utils.R

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
library(DBI)
2+
library(duckdb)
3+
4+
my_env <- new.env()
5+
6+
duckdb_register_con <- function(df) {
7+
my_env$con <- dbConnect(duckdb::duckdb(), ":memory:")
8+
DBI::dbWriteTable(my_env$con, "gwalkr_mid_table", as.data.frame(df), overwrite = FALSE)
9+
}
10+
11+
duckdb_unregister_con <- function(df) {
12+
if (!is.null(my_env$con)) {
13+
dbDisconnect(my_env$con)
14+
my_env$con <- NULL # Set to NULL after disconnecting
15+
}
16+
}
17+
18+
duckdb_get_field_meta <- function() {
19+
if (exists("con", envir = my_env)) {
20+
result <- dbGetQuery(my_env$con, 'SELECT * FROM gwalkr_mid_table LIMIT 1')
21+
if (nrow(result) > 0) {
22+
return(get_data_meta_type(result))
23+
}
24+
} else {
25+
stop("Database connection not found.")
26+
}
27+
}
28+
29+
duckdb_get_data <- function(sql) {
30+
if (exists("con", envir = my_env)) {
31+
result <- dbGetQuery(my_env$con, sql)
32+
if (nrow(result) > 0) {
33+
return(result)
34+
}
35+
} else {
36+
stop("Database connection not found.")
37+
}
38+
}
39+
40+
get_data_meta_type <- function(data) {
41+
meta_types <- list()
42+
43+
for (key in names(data)) {
44+
value <- data[[key]]
45+
field_meta_type <- if (inherits(value, "POSIXct")) {
46+
if (!is.null(attr(value, "tzone"))) "datetime_tz" else "datetime"
47+
} else if (is.numeric(value)) {
48+
"number"
49+
} else {
50+
"string"
51+
}
52+
meta_types <- append(meta_types, list(list(key = key, type = field_meta_type)))
53+
}
54+
55+
return(meta_types)
56+
}

R/gwalkr.R

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@
44
#'
55
#' @import htmlwidgets
66
#' @import openssl
7+
#' @importFrom jsonlite toJSON
8+
#' @import shiny
9+
#' @import shinycssloaders
10+
#' @import DBI
11+
#' @import duckdb
712
#'
813
#' @param data A data frame to be visualized in the GWalkR. The data frame should not be empty.
914
#' @param lang A character string specifying the language for the widget. Possible values are "en" (default), "ja", "zh".
1015
#' @param dark A character string specifying the dark mode preference. Possible values are "light" (default), "dark", "media".
11-
#' @param columnSpecs An optional list of lists to manually specify the types of some columns in the data frame.
12-
#' Each top level element in the list corresponds to a column, and the list assigned to each column should have
13-
#' two elements: `analyticalType` and `semanticType`. `analyticalType` can
14-
#' only be one of "measure" or "dimension". `semanticType` can only be one of
16+
#' @param columnSpecs An optional list of lists to manually specify the types of some columns in the data frame.
17+
#' Each top level element in the list corresponds to a column, and the list assigned to each column should have
18+
#' two elements: `analyticalType` and `semanticType`. `analyticalType` can
19+
#' only be one of "measure" or "dimension". `semanticType` can only be one of
1520
#' "quantitative", "temporal", "nominal" or "ordinal". For example:
1621
#' \code{list(
1722
#' "gender" = list(analyticalType = "dimension", semanticType = "nominal"),
@@ -28,42 +33,47 @@
2833
#' gwalkr(mtcars)
2934
#'
3035
#' @export
31-
gwalkr <- function(data, lang = "en", dark = "light", columnSpecs = list(), visConfig = NULL, visConfigFile = NULL, toolbarExclude = list()) {
36+
gwalkr <- function(data, lang = "en", dark = "light", columnSpecs = list(), visConfig = NULL, visConfigFile = NULL, toolbarExclude = list(), useKernel = FALSE) {
3237
if (!is.data.frame(data)) stop("data must be a data frame")
3338
if (!is.null(visConfig) && !is.null(visConfigFile)) stop("visConfig and visConfigFile are mutually exclusive")
3439
lang <- match.arg(lang, choices = c("en", "ja", "zh"))
3540

3641
rawFields <- raw_fields(data, columnSpecs)
3742
colnames(data) <- sapply(colnames(data), fname_encode)
38-
43+
3944
if (!is.null(visConfigFile)) {
4045
visConfig <- readLines(visConfigFile, warn=FALSE)
4146
}
42-
# forward options using x
43-
x = list(
44-
dataSource = jsonlite::toJSON(data),
45-
rawFields = rawFields,
46-
i18nLang = lang,
47-
visSpec = visConfig,
48-
dark = dark,
49-
toolbarExclude = toolbarExclude
50-
)
5147

52-
# create widget
53-
htmlwidgets::createWidget(
54-
name = 'gwalkr',
55-
x,
56-
package = 'GWalkR',
57-
width='100%',
58-
height='100%'
59-
)
48+
if (useKernel) {
49+
gwalkr_kernel(data, lang, dark, rawFields, visConfig, toolbarExclude)
50+
} else {
51+
x = list(
52+
dataSource = toJSON(data),
53+
rawFields = rawFields,
54+
i18nLang = lang,
55+
visSpec = visConfig,
56+
dark = dark,
57+
toolbarExclude = toolbarExclude,
58+
useKernel = FALSE
59+
)
60+
61+
# create widget
62+
htmlwidgets::createWidget(
63+
name = 'gwalkr',
64+
x,
65+
package = 'GWalkR',
66+
width='100%',
67+
height='100%'
68+
)
69+
}
6070
}
6171

6272
#' Shiny bindings for gwalkr
6373
#'
6474
#' Output and render functions for using gwalkr within Shiny
6575
#' applications and interactive Rmd documents.
66-
#'
76+
#'
6777
#' @import shiny
6878
#'
6979
#' @param outputId output variable to read from

R/gwalkr_kernel.R

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
gwalkr_kernel <- function(data, lang, dark, rawFields, visConfig, toolbarExclude) {
2+
cat("GWalkR kernel mode init...")
3+
4+
filter_func <- function(data, req) {
5+
query <- parseQueryString(req$QUERY_STRING)
6+
7+
res <- duckdb_get_data(query$sql)
8+
9+
json <- toJSON(
10+
res,
11+
auto_unbox = TRUE
12+
)
13+
14+
httpResponse(
15+
status = 200L,
16+
content_type = "application/json",
17+
content = json
18+
)
19+
}
20+
21+
app <- shinyApp(
22+
ui = fluidPage(
23+
shinycssloaders::withSpinner(
24+
gwalkrOutput("gwalkr_kernel"),
25+
proxy.height="400px"
26+
)
27+
),
28+
29+
server = function(input, output, session) {
30+
path <- session$registerDataObj(
31+
"GWALKR",
32+
NULL,
33+
filter_func
34+
)
35+
36+
duckdb_register_con(data)
37+
fieldMetas <- duckdb_get_field_meta()
38+
39+
x = list(
40+
rawFields = rawFields,
41+
i18nLang = lang,
42+
visSpec = visConfig,
43+
dark = dark,
44+
toolbarExclude = toolbarExclude,
45+
useKernel = TRUE,
46+
fieldMetas = fieldMetas,
47+
endpointPath = path
48+
)
49+
50+
output$gwalkr_kernel = renderGwalkr({
51+
htmlwidgets::createWidget(
52+
name = 'gwalkr',
53+
x,
54+
package = 'GWalkR',
55+
width='100%',
56+
height='100%'
57+
)
58+
})
59+
session$onSessionEnded(function() {
60+
cat("GwalkR closed")
61+
duckdb_unregister_con()
62+
})
63+
},
64+
65+
options=c(launch.browser = .rs.invokeShinyPaneViewer)
66+
)
67+
68+
if (interactive()) app
69+
}

man/gwalkr.Rd

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web_app/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@
1111
},
1212
"dependencies": {
1313
"@kanaries/graphic-walker": "^0.4.70",
14+
"@kanaries/gw-dsl-parser": "^0.1.49",
1415
"@rollup/plugin-commonjs": "^25.0.2",
1516
"@rollup/plugin-replace": "^5.0.2",
1617
"@rollup/plugin-terser": "^0.4.3",
1718
"@rollup/plugin-typescript": "^11.1.2",
1819
"mobx-react-lite": "^3.4.3",
1920
"react": "^18.2.0",
2021
"react-dom": "^18.2.0",
21-
"styled-components": "^5.3.6"
22+
"styled-components": "^5.3.6",
23+
"vite-plugin-wasm": "^3.3.0"
2224
},
2325
"devDependencies": {
2426
"@types/react": "^18.2.14",

web_app/src/dataSource/index.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { IDataQueryPayload, IRow } from "@kanaries/graphic-walker/interfaces";
2+
import { parser_dsl_with_meta } from "@kanaries/gw-dsl-parser";
3+
4+
const DEFAULT_LIMIT = 50_000;
5+
6+
const sendHTTPData = (sql: string, endpointPath: string) => {
7+
return new Promise((resolve, reject) => {
8+
fetch(`${endpointPath}&sql=${encodeURIComponent(sql)}`)
9+
.then((response) => response.json())
10+
.then((data) => {
11+
console.log("Processed data from R:", data);
12+
resolve(data);
13+
})
14+
.catch((error) => {
15+
console.error("Error:", error);
16+
reject(error);
17+
});
18+
});
19+
};
20+
21+
export function getDataFromKernelBySql(fieldMetas: { key: string; type: string }[], endpointPath: string) {
22+
return async (payload: IDataQueryPayload) => {
23+
const sql = parser_dsl_with_meta(
24+
"gwalkr_mid_table",
25+
JSON.stringify({ ...payload, limit: payload.limit ?? DEFAULT_LIMIT }),
26+
JSON.stringify({ gwalkr_mid_table: fieldMetas })
27+
);
28+
const result = (await sendHTTPData(sql, endpointPath)) ?? [];
29+
return result as IRow[];
30+
};
31+
}

web_app/src/index.tsx

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ import CodeExportModal from "./components/codeExportModal";
99
import { StyleSheetManager } from "styled-components";
1010
import tailwindStyle from "tailwindcss/tailwind.css?inline";
1111
import formatSpec from "./utils/formatSpec";
12+
import { getDataFromKernelBySql } from "./dataSource";
13+
14+
import initDslParser from "@kanaries/gw-dsl-parser";
15+
import wasmPath from "@kanaries/gw-dsl-parser/gw_dsl_parser_bg.wasm?url";
1216

1317
const App: React.FC<IAppProps> = observer((propsIn) => {
14-
const { dataSource, visSpec, rawFields, toolbarExclude, ...props } = propsIn;
18+
const { dataSource, visSpec, rawFields, toolbarExclude, useKernel, ...props } = propsIn;
1519
const storeRef = React.useRef<VizSpecStore | null>(null);
1620

1721
const specList = visSpec ? formatSpec(JSON.parse(visSpec) as any[], rawFields) : undefined;
@@ -25,14 +29,34 @@ const App: React.FC<IAppProps> = observer((propsIn) => {
2529
exclude: toolbarExclude ? [...toolbarExclude, "export_code"] : ["export_code"],
2630
extra: tools,
2731
};
28-
return (
29-
<React.StrictMode>
30-
<div className="h-full w-full overflow-y-scroll font-sans">
31-
<CodeExportModal open={exportOpen} setOpen={setExportOpen} globalStore={storeRef} />
32-
<GraphicWalker {...props} storeRef={storeRef} data={dataSource} toolbar={toolbarConfig} fields={rawFields} chart={specList} />
33-
</div>
34-
</React.StrictMode>
35-
);
32+
33+
if (useKernel) {
34+
const { endpointPath, fieldMetas } = propsIn;
35+
return (
36+
<React.StrictMode>
37+
<div className="h-full w-full overflow-y-scroll font-sans">
38+
<CodeExportModal open={exportOpen} setOpen={setExportOpen} globalStore={storeRef} />
39+
<GraphicWalker
40+
{...props}
41+
storeRef={storeRef}
42+
toolbar={toolbarConfig}
43+
fields={rawFields}
44+
chart={specList}
45+
computation={getDataFromKernelBySql(fieldMetas, endpointPath)}
46+
/>
47+
</div>
48+
</React.StrictMode>
49+
);
50+
} else {
51+
return (
52+
<React.StrictMode>
53+
<div className="h-full w-full overflow-y-scroll font-sans">
54+
<CodeExportModal open={exportOpen} setOpen={setExportOpen} globalStore={storeRef} />
55+
<GraphicWalker {...props} storeRef={storeRef} data={dataSource} toolbar={toolbarConfig} fields={rawFields} chart={specList} />
56+
</div>
57+
</React.StrictMode>
58+
);
59+
}
3660
});
3761

3862
const GWalkR = (props: IAppProps, id: string) => {
@@ -46,11 +70,26 @@ const GWalkR = (props: IAppProps, id: string) => {
4670
shadowRoot.appendChild(styleElement);
4771

4872
const root = createRoot(shadowRoot);
49-
root.render(
50-
<StyleSheetManager target={shadowRoot}>
51-
<App {...props} />
52-
</StyleSheetManager>
53-
);
73+
74+
if (props.useKernel) {
75+
initDslParser(wasmPath)
76+
.then(() => {
77+
root.render(
78+
<StyleSheetManager target={shadowRoot}>
79+
<App {...props} />
80+
</StyleSheetManager>
81+
);
82+
})
83+
.catch((e) => {
84+
console.error(e);
85+
});
86+
} else {
87+
root.render(
88+
<StyleSheetManager target={shadowRoot}>
89+
<App {...props} />
90+
</StyleSheetManager>
91+
);
92+
}
5493
}
5594
// If you want to execute GWalkR after the document has loaded, you can do it here.
5695
// But remember, you will need to provide the 'props' and 'id' parameters.

0 commit comments

Comments
 (0)