-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.html
194 lines (193 loc) Β· 24.2 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
<!DOCTYPE html>
<!-- This file was auto-generated by exmd at 2023-04-07T13:33:30.085Z. Do NOT edit by hand! -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>π§ͺ πΈοΈ WebR, Vite + π¦ DuckDB via Observable's Standard Library</title><meta property="og:title" content="π§ͺ πΈοΈ WebR, Vite + π¦ DuckDB via Observable's Standard Library">
<meta property="twitter:title" content="π§ͺ πΈοΈ WebR, Vite + π¦ DuckDB via Observable's Standard Library">
<meta property="og:description" content="A Toy Modeling Example">
<meta property="twitter:description" content="A Toy Modeling Example">
<meta property="og:site" content="https://rud.is/w/webr-vite-duckdb">
<meta property="og:site_name" content="WebR Exeriments">
<meta property="og:image:url" content="https://rud.is/w/webr-vite-duckdb/preview.png">
<meta property="og:image:width" content="1932">
<meta property="og:image:height" content="1170">
<meta property="og:image:alt" content="example">
<meta property="twitter:site_name" content="@hrbrmstr">
<meta property="twitter:domain" content="rud.is">
<meta property="twitter:card" content="summary_large_image">
<meta property="article:published_time" content="2023-04-07T13:33:30.085Z">
<link rel='apple-touch-icon' sizes='180x180' href='./favicon/apple-touch-icon.png'/>
<link rel='icon' type='image/png' sizes='32x32' href='./favicon/favicon-32x32.png'/>
<link rel='icon' type='image/png' sizes='16x16' href='./favicon/favicon-16x16.png'/>
<link rel='manifest' href='./favicon/site.webmanifest'/>
<link href='./src/index.css' rel='stylesheet'/>
<link href='./src/components.css' rel='stylesheet'/>
<script type='module' src='./src/main.js'></script>
</head>
<body>
<h1>π§ͺ πΈοΈ Vite + π¦ DuckDB via Observable's Standard Library</h1>
<p><status-message id="webr-status" text="WebR Loadingβ¦"></status-message></p>
<h2>A Toy Modeling Example</h2>
<hr>
<p>Experiment Hypothesis:</p>
<blockquote>
<p><em>We can use DuckDB to wrangle data for us, let R do some "modeling", and let Observable Plot show us the results</em></p>
</blockquote>
<p>Experiment parameters:</p>
<ul>
<li>Webr</li>
<li>An R function we'll make directly callable as a JS function</li>
<li>Observable Standard Library's <code>DuckDBCLient</code></li>
<li>Observable Plot</li>
<li>Lit (web components)</li>
<li>Vite (for building)</li>
</ul>
<hr>
<h2>When Will GreyNoise Have 1,000 "Tags"</h2>
<p><action-button disabled label="Tell me, Carnac!" id="carnac-button"><img src="carnac.jpg" width="15%"/></action-button></p>
<p>GreyNoise will reach 1,000 tags on or about <span id="predicted-date">ββββββ</span>.</p>
<ojs-shorthand-plot disabled id="tag-volume" chartTitle="GreyNoise Current Tag Volume">
</ojs-shorthand-plot>
<h2>Adopt, Adapt, And Improve</h2>
<p>Building off of the <a href="https://rud.is/w/vite-duckdb">previous experiment</a>, today we will combine DuckDB data ops with WebR, letting R do some trivial modeling with <code>glm</code> on data we load and wrangle with DuckDB.</p>
<blockquote>
<p><em>Let's be super clear, right up front: this data is small enough to load into R, process in R, and then model and plot in R without any other packages (save {svglite}). It is deliberately a toy example to make it easier to work with while showing the core concepts of loading from a database, doing a more than trivial database query, passing data to R, and getting a result back.</em></p>
</blockquote>
<p>At work, one of the core work products from my team are what we call "<a href="https://viz.greynoise.io/cheat-sheet/tags">tags</a>". They are detection rules for vulnerability exploit checks/attempts, good/bad actors, and more. We're coming up on the human-psyche-significant "1,000" value for total number of tags. Today's example predicts when that happens based on the volume time series.</p>
<p>Here are the tables we have:</p>
<p><simple-message id="describe-tables"></simple-message></p>
<p>This is the schema for our <code>tags</code> table:</p>
<p><data-frame-view height="150" label="Tags Schema" id="tags-schema"></data-frame-view></p>
<p>This is what's in it:</p>
<p><data-frame-view label="Tags" id="tags-view"></data-frame-view></p>
<p>Now, we need to compute the <em>cumulative sum</em> for each day and keep track of <em>days elapsed</em> so we can pass those vectors to our model.</p>
<p>It's not a horrible SQL query, especially if we break it up using common table expressions (ref: <code>duckdb.js</code>):</p>
<pre class="shiki " style="background-color: #0b0e14" tabindex="0"><code><span class="line"><span style="color: #ACB6BF8C; font-style: italic">-- Setup a date range that spans the entire min/max created_at</span></span>
<span class="line"><span style="color: #ACB6BF8C; font-style: italic">-- We need this b/c we don't have tags every day so there are</span></span>
<span class="line"><span style="color: #ACB6BF8C; font-style: italic">-- gaps in the time series</span></span>
<span class="line"><span style="color: #FF8F40">WITH</span><span style="color: #BFBDB6"> date_range </span><span style="color: #FF8F40">AS</span><span style="color: #BFBDB6"> (</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">SELECT</span><span style="color: #BFBDB6"> UNNEST(</span><span style="color: #F07178">generate_series</span><span style="color: #BFBDB6">(</span></span>
<span class="line"><span style="color: #BFBDB6"> (</span><span style="color: #FF8F40">SELECT</span><span style="color: #BFBDB6"> </span><span style="color: #F07178">MIN</span><span style="color: #BFBDB6">(created_at) </span><span style="color: #FF8F40">FROM</span><span style="color: #BFBDB6"> tags),</span></span>
<span class="line"><span style="color: #BFBDB6"> (</span><span style="color: #FF8F40">SELECT</span><span style="color: #BFBDB6"> </span><span style="color: #F07178">MAX</span><span style="color: #BFBDB6">(created_at) </span><span style="color: #FF8F40">FROM</span><span style="color: #BFBDB6"> tags),</span></span>
<span class="line"><span style="color: #BFBDB6"> INTERVAL </span><span style="color: #AAD94C">'1 day'</span></span>
<span class="line"><span style="color: #BFBDB6"> )) </span><span style="color: #FF8F40">AS</span><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">date</span></span>
<span class="line"><span style="color: #BFBDB6">),</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ACB6BF8C; font-style: italic">-- count number of tags/day</span></span>
<span class="line"><span style="color: #BFBDB6">grouped_tags </span><span style="color: #FF8F40">AS</span><span style="color: #BFBDB6"> (</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">SELECT</span></span>
<span class="line"><span style="color: #BFBDB6"> created_at,</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #F07178">COUNT</span><span style="color: #BFBDB6">(</span><span style="color: #F29668">*</span><span style="color: #BFBDB6">) </span><span style="color: #FF8F40">AS</span><span style="color: #BFBDB6"> daily_count</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">FROM</span></span>
<span class="line"><span style="color: #BFBDB6"> tags</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">GROUP BY</span></span>
<span class="line"><span style="color: #BFBDB6"> created_at</span></span>
<span class="line"><span style="color: #BFBDB6">),</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ACB6BF8C; font-style: italic">-- join to the full range and fill in values</span></span>
<span class="line"><span style="color: #BFBDB6">joined_dates_counts </span><span style="color: #FF8F40">AS</span><span style="color: #BFBDB6"> (</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">SELECT</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #95E6CB">dr</span><span style="color: #BFBDB6">.</span><span style="color: #95E6CB">date</span><span style="color: #BFBDB6">,</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #F07178">COALESCE</span><span style="color: #BFBDB6">(</span><span style="color: #95E6CB">gt</span><span style="color: #BFBDB6">.</span><span style="color: #95E6CB">daily_count</span><span style="color: #BFBDB6">, </span><span style="color: #D2A6FF">0</span><span style="color: #BFBDB6">) </span><span style="color: #FF8F40">AS</span><span style="color: #BFBDB6"> filled_daily_count</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">FROM</span></span>
<span class="line"><span style="color: #BFBDB6"> date_range dr</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">LEFT JOIN</span></span>
<span class="line"><span style="color: #BFBDB6"> grouped_tags gt</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">ON</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #95E6CB">dr</span><span style="color: #BFBDB6">.</span><span style="color: #95E6CB">date</span><span style="color: #BFBDB6"> </span><span style="color: #F29668">=</span><span style="color: #BFBDB6"> </span><span style="color: #95E6CB">gt</span><span style="color: #BFBDB6">.</span><span style="color: #95E6CB">created_at</span></span>
<span class="line"><span style="color: #BFBDB6">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ACB6BF8C; font-style: italic">-- get the cumulative sum and days since the min created_at</span></span>
<span class="line"><span style="color: #FF8F40">SELECT</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">date</span><span style="color: #BFBDB6">,</span></span>
<span class="line"><span style="color: #BFBDB6"> filled_daily_count,</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #F07178">SUM</span><span style="color: #BFBDB6">(filled_daily_count) </span><span style="color: #FF8F40">OVER</span><span style="color: #BFBDB6"> (</span><span style="color: #FF8F40">ORDER BY</span><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">date</span><span style="color: #BFBDB6">) </span><span style="color: #FF8F40">AS</span><span style="color: #BFBDB6"> running_cumulative_sum,</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #F07178">DATEDIFF</span><span style="color: #BFBDB6">(</span><span style="color: #AAD94C">'day'</span><span style="color: #BFBDB6">, (</span><span style="color: #FF8F40">SELECT</span><span style="color: #BFBDB6"> </span><span style="color: #F07178">MIN</span><span style="color: #BFBDB6">(</span><span style="color: #FF8F40">date</span><span style="color: #BFBDB6">) </span><span style="color: #FF8F40">FROM</span><span style="color: #BFBDB6"> joined_dates_counts), </span><span style="color: #FF8F40">date</span><span style="color: #BFBDB6">) </span><span style="color: #FF8F40">AS</span><span style="color: #BFBDB6"> days_elapsed</span></span>
<span class="line"><span style="color: #FF8F40">FROM</span></span>
<span class="line"><span style="color: #BFBDB6"> joined_dates_counts;</span></span>
<span class="line"></span></code></pre>
<p>Here's what those "tag stats" look like:</p>
<p><data-frame-view label="Tag Stats" id="tags-stats-view"></data-frame-view></p>
<p>We will use R to predict when the tag count will reach a specified value, this is the function we'll be using (ref: <code>r.js</code>):</p>
<pre class="shiki " style="background-color: #0b0e14" tabindex="0"><code><span class="line"><span style="color: #FF8F40">function</span><span style="color: #BFBDB6">(csum</span><span style="color: #BFBDB6B3">,</span><span style="color: #BFBDB6"> days_elapsed</span><span style="color: #BFBDB6B3">,</span><span style="color: #BFBDB6"> target_csum) {</span></span>
<span class="line"></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #ACB6BF8C; font-style: italic"># saddest. model. ever.</span></span>
<span class="line"></span>
<span class="line"><span style="color: #BFBDB6"> model </span><span style="color: #F29668"><-</span><span style="color: #BFBDB6"> </span><span style="color: #F07178">glm</span><span style="color: #BFBDB6">(csum </span><span style="color: #FF8F40">~</span><span style="color: #BFBDB6"> days_elapsed, </span><span style="color: #D2A6FF">family</span><span style="color: #BFBDB6"> </span><span style="color: #F29668">=</span><span style="color: #BFBDB6"> </span><span style="color: #AAD94C">"poisson"</span><span style="color: #BFBDB6">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #BFBDB6"> predicted_days_elapsed </span><span style="color: #F29668"><-</span><span style="color: #BFBDB6"> days_elapsed</span></span>
<span class="line"><span style="color: #BFBDB6"> predicted_days_elapsed_ret </span><span style="color: #F29668"><-</span><span style="color: #BFBDB6"> </span><span style="color: #F07178">c</span><span style="color: #BFBDB6">()</span></span>
<span class="line"><span style="color: #BFBDB6"> predicted_days_csum_ret </span><span style="color: #F29668"><-</span><span style="color: #BFBDB6"> </span><span style="color: #F07178">c</span><span style="color: #BFBDB6">()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">while</span><span style="color: #BFBDB6"> (</span><span style="color: #D2A6FF">TRUE</span><span style="color: #BFBDB6">) {</span></span>
<span class="line"></span>
<span class="line"><span style="color: #BFBDB6"> predicted_days_elapsed </span><span style="color: #F29668"><-</span><span style="color: #BFBDB6"> </span><span style="color: #F07178">max</span><span style="color: #BFBDB6">(predicted_days_elapsed) </span><span style="color: #F29668">+</span><span style="color: #BFBDB6"> </span><span style="color: #D2A6FF">1</span></span>
<span class="line"></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #F07178">predict</span><span style="color: #BFBDB6">(</span></span>
<span class="line"><span style="color: #BFBDB6"> model, </span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #D2A6FF">newdata</span><span style="color: #BFBDB6"> </span><span style="color: #F29668">=</span><span style="color: #BFBDB6"> </span><span style="color: #F07178">data.frame</span><span style="color: #BFBDB6">(</span><span style="color: #D2A6FF">days_elapsed</span><span style="color: #BFBDB6"> </span><span style="color: #F29668">=</span><span style="color: #BFBDB6"> predicted_days_elapsed), </span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #D2A6FF">type</span><span style="color: #BFBDB6"> </span><span style="color: #F29668">=</span><span style="color: #BFBDB6"> </span><span style="color: #AAD94C">"response"</span></span>
<span class="line"><span style="color: #BFBDB6"> ) </span><span style="color: #F29668">-></span><span style="color: #BFBDB6"> predicted_csum</span></span>
<span class="line"></span>
<span class="line"><span style="color: #BFBDB6"> predicted_days_csum_ret </span><span style="color: #F29668"><-</span><span style="color: #BFBDB6"> </span><span style="color: #F07178">c</span><span style="color: #BFBDB6">(predicted_days_csum_ret, predicted_csum)</span></span>
<span class="line"><span style="color: #BFBDB6"> predicted_days_elapsed_ret </span><span style="color: #F29668"><-</span><span style="color: #BFBDB6"> </span><span style="color: #F07178">c</span><span style="color: #BFBDB6">(predicted_days_elapsed_ret, predicted_days_elapsed)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">if</span><span style="color: #BFBDB6"> (predicted_csum </span><span style="color: #F29668">>=</span><span style="color: #BFBDB6"> target_csum) </span><span style="color: #FF8F40">break</span></span>
<span class="line"></span>
<span class="line"><span style="color: #BFBDB6"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #F07178">data.frame</span><span style="color: #BFBDB6">(</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #D2A6FF">days_elapsed</span><span style="color: #BFBDB6"> </span><span style="color: #F29668">=</span><span style="color: #BFBDB6"> predicted_days_elapsed_ret,</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #D2A6FF">tagCount</span><span style="color: #BFBDB6"> </span><span style="color: #F29668">=</span><span style="color: #BFBDB6"> predicted_days_csum_ret</span></span>
<span class="line"><span style="color: #BFBDB6"> )</span></span>
<span class="line"></span>
<span class="line"><span style="color: #BFBDB6">}</span></span>
<span class="line"></span></code></pre>
<p>Sure, that could be fancier, but we don't need fancy for this example.</p>
<p>We then use the fact that:</p>
<pre class="shiki " style="background-color: #0b0e14" tabindex="0"><code><span class="line"><span style="color: #FF8F40">await</span><span style="color: #BFBDB6"> </span><span style="color: #FFB454">R</span><span style="color: #AAD94C">`function NAME(β¦) {}`</span></span>
<span class="line"></span></code></pre>
<p>produces a <em>callable</em> JS function (also in <code>r.js</code>) and we use it with the vectors we made from the database</p>
<pre class="shiki " style="background-color: #0b0e14" tabindex="0"><code><span class="line"><span style="color: #ACB6BF8C; font-style: italic">// call the function</span></span>
<span class="line"><span style="color: #FF8F40">const</span><span style="color: #BFBDB6"> nDays </span><span style="color: #F29668">=</span><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">await</span><span style="color: #BFBDB6"> </span><span style="color: #FFB454">predict</span><span style="color: #BFBDB6">(</span></span>
<span class="line"><span style="color: #BFBDB6"> tagsCumSum</span><span style="color: #F29668">.</span><span style="color: #FFB454">map</span><span style="color: #BFBDB6">(</span><span style="color: #D2A6FF">d</span><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">=></span><span style="color: #BFBDB6"> d</span><span style="color: #F29668">.</span><span style="color: #BFBDB6">csum)</span><span style="color: #BFBDB6B3">,</span></span>
<span class="line"><span style="color: #BFBDB6"> tagsCumSum</span><span style="color: #F29668">.</span><span style="color: #FFB454">map</span><span style="color: #BFBDB6">(</span><span style="color: #D2A6FF">d</span><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">=></span><span style="color: #BFBDB6"> d</span><span style="color: #F29668">.</span><span style="color: #BFBDB6">days_elapsed)</span><span style="color: #BFBDB6B3">,</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #D2A6FF">1_000</span></span>
<span class="line"><span style="color: #BFBDB6">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ACB6BF8C; font-style: italic">// get the last ("1,000" prediction) elapsed day and min date </span></span>
<span class="line"><span style="color: #FF8F40">const</span><span style="color: #BFBDB6"> lastDay </span><span style="color: #F29668">=</span><span style="color: #BFBDB6"> nDays</span><span style="color: #F29668">.</span><span style="color: #BFBDB6">values[</span><span style="color: #D2A6FF">0</span><span style="color: #BFBDB6">]</span><span style="color: #F29668">.</span><span style="color: #BFBDB6">values[ nDays</span><span style="color: #F29668">.</span><span style="color: #BFBDB6">values[</span><span style="color: #D2A6FF">0</span><span style="color: #BFBDB6">]</span><span style="color: #F29668">.</span><span style="color: #BFBDB6">values</span><span style="color: #F29668">.</span><span style="color: #BFBDB6">length</span><span style="color: #F29668">-</span><span style="color: #D2A6FF">1</span><span style="color: #BFBDB6">]</span></span>
<span class="line"><span style="color: #FF8F40">const</span><span style="color: #BFBDB6"> minDate </span><span style="color: #F29668">=</span><span style="color: #BFBDB6"> </span><span style="color: #FFB454">ddbResToArray</span><span style="color: #BFBDB6">(</span></span>
<span class="line"><span style="color: #BFBDB6"> </span><span style="color: #FF8F40">await</span><span style="color: #BFBDB6"> </span><span style="color: #BFBDB6">db</span><span style="color: #F29668">.</span><span style="color: #FFB454">sql</span><span style="color: #AAD94C">`SELECT min(created_at) AS min_date FROM tags`</span></span>
<span class="line"><span style="color: #BFBDB6">)[</span><span style="color: #D2A6FF">0</span><span style="color: #BFBDB6">]</span><span style="color: #F29668">.</span><span style="color: #BFBDB6">min_date</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ACB6BF8C; font-style: italic">// β¦</span></span>
<span class="line"></span>
<span class="line"><span style="color: #ACB6BF8C; font-style: italic">// display the computed "1,000" date</span></span>
<span class="line"><span style="color: #BFBDB6">predictedDate</span><span style="color: #F29668">.</span><span style="color: #BFBDB6">textContent </span><span style="color: #F29668">=</span><span style="color: #BFBDB6"> </span><span style="color: #FFB454">addDays</span><span style="color: #BFBDB6">(minDate</span><span style="color: #BFBDB6B3">,</span><span style="color: #BFBDB6"> lastDay)</span><span style="color: #F29668">.</span><span style="color: #FFB454">toDateString</span><span style="color: #BFBDB6">()</span></span>
<span class="line"></span></code></pre>
<h2>Project Layout</h2>
<p>Core files:</p>
<pre class="shiki " style="background-color: #0b0e14" tabindex="0"><code><span class="line"><span style="color: #bfbdb6">βββ index.md # what we render into index.html via the justfile</span></span>
<span class="line"><span style="color: #bfbdb6">βββ src</span></span>
<span class="line"><span style="color: #bfbdb6">βΒ Β βββ components.css # CSS specific to component styling</span></span>
<span class="line"><span style="color: #bfbdb6">βΒ Β βββ index.css # core SSS</span></span>
<span class="line"><span style="color: #bfbdb6">βΒ Β βββ action-button.js # Lit component for the button</span></span>
<span class="line"><span style="color: #bfbdb6">βΒ Β βββ data-frame-view.js # Lit component for displaying tables</span></span>
<span class="line"><span style="color: #bfbdb6">βΒ Β βββ ojs-shorthand-plot.js # Lit component for Observable plots</span></span>
<span class="line"><span style="color: #bfbdb6">βΒ Β βββ simple-message.js # Lit component for simple output messages/text</span></span>
<span class="line"><span style="color: #bfbdb6">βΒ Β βββ status-message.js # Lit component for my WebR status message up top</span></span>
<span class="line"><span style="color: #bfbdb6">βΒ Β βββ main.js # main app runner</span></span>
<span class="line"><span style="color: #bfbdb6">βΒ Β βββ r.js # WebR context creation and support functions</span></span>
<span class="line"><span style="color: #bfbdb6">βΒ Β βββ duckdb.js # DuckDB context creation and support functions and queries</span></span>
<span class="line"><span style="color: #bfbdb6">βΒ Β βββ utils.js # Miscellaneous utilities</span></span>
<span class="line"><span style="color: #bfbdb6">βββ</span></span>
<span class="line"><span style="color: #bfbdb6"></span></span></code></pre>
<h2>FIN</h2>
<p>You can find the source <a href="https://github.com/hrbrmstr/webr-vite-duckdb">on GitHub</a>.</p>
<p style="text-align: center">Brought to you by @hrbrmstr</p>
<p style='font-size:8pt'>"Carnac" image by The Tonight Show Starring Johnny Carson, Fair use, https://en.wikipedia.org/w/index.php?curid=2560897</p><!-- extra body bits -->
</body>
</html>