znation HF Staff commited on
Commit
8e5c2f5
·
1 Parent(s): b2d40c3

claude: add vega-lite vis

Browse files

prompt: Let's add a new feature. When a dataaset is selected, use DuckDB wasm to identify the column names and column types. Then build up a dropdown list of possible 2D plots, with
1 column on the X axis, and 1 other column on the Y axis. When the user selects one of the 2D plots, use a pre-built Vega-Lite spec to render the plot using Vega-Lite and
vega-embed. The Vega-Lite specs used should be as follows:
* A numeric column on both the X and Y axes should use a scatter plot like on https://vega.github.io/vega-lite/examples/point_2d.html
* A numeric column on the X axis and a text column on the Y axis should use a bar chart like https://vega.github.io/vega-lite/examples/bar_aggregate.html, where the aggregate
operator is "count" on the text field.
* A text column by a text column should not be able to be visualized -- upon further thought, let's remove those from the dropdown.
* Any other column types should result in an error when used describing the type(s).

Files changed (2) hide show
  1. index.html +263 -1
  2. style.css +33 -5
index.html CHANGED
@@ -8,6 +8,9 @@
8
  <script src="https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@latest/dist/duckdb-mvp.wasm.js"></script>
9
  <script src="https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@latest/dist/duckdb-browser-mvp.worker.js"></script>
10
  <script type="module" src="https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@latest/dist/duckdb-browser-mvp.worker.js"></script>
 
 
 
11
  </head>
12
  <body>
13
  <div class="container">
@@ -56,6 +59,18 @@
56
 
57
  <div id="status" class="status"></div>
58
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  <div id="resultsContainer" class="results-container" style="display: none;">
60
  <h2>Results</h2>
61
  <div id="results" class="results"></div>
@@ -67,6 +82,8 @@
67
 
68
  let db = null;
69
  let conn = null;
 
 
70
 
71
  // Initialize DuckDB
72
  async function initDuckDB() {
@@ -139,6 +156,223 @@
139
  return div.innerHTML;
140
  }
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  // Handle form submission
143
  async function handleSubmit(e) {
144
  e.preventDefault();
@@ -196,10 +430,38 @@
196
  }
197
 
198
  // Handle dropdown selection
199
- document.getElementById('urlSelect').addEventListener('change', function(e) {
200
  const selectedUrl = e.target.value;
201
  if (selectedUrl) {
202
  document.getElementById('parquetUrl').value = selectedUrl;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  }
204
  });
205
 
 
8
  <script src="https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@latest/dist/duckdb-mvp.wasm.js"></script>
9
  <script src="https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@latest/dist/duckdb-browser-mvp.worker.js"></script>
10
  <script type="module" src="https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@latest/dist/duckdb-browser-mvp.worker.js"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
14
  </head>
15
  <body>
16
  <div class="container">
 
59
 
60
  <div id="status" class="status"></div>
61
 
62
+ <div id="visualizationSection" class="visualization-section" style="display: none;">
63
+ <h2>Visualization</h2>
64
+ <div class="form-group">
65
+ <label for="vizSelect">Select a 2D plot</label>
66
+ <select id="vizSelect">
67
+ <option value="">-- Choose columns to visualize --</option>
68
+ </select>
69
+ </div>
70
+ <button type="button" id="renderVizBtn" disabled>Render Visualization</button>
71
+ <div id="vizContainer" class="viz-container"></div>
72
+ </div>
73
+
74
  <div id="resultsContainer" class="results-container" style="display: none;">
75
  <h2>Results</h2>
76
  <div id="results" class="results"></div>
 
82
 
83
  let db = null;
84
  let conn = null;
85
+ let currentDatasetUrl = null;
86
+ let columnInfo = [];
87
 
88
  // Initialize DuckDB
89
  async function initDuckDB() {
 
156
  return div.innerHTML;
157
  }
158
 
159
+ // Determine if a DuckDB type is numeric
160
+ function isNumericType(type) {
161
+ const numericTypes = ['TINYINT', 'SMALLINT', 'INTEGER', 'BIGINT', 'HUGEINT',
162
+ 'FLOAT', 'DOUBLE', 'DECIMAL', 'NUMERIC', 'REAL'];
163
+ return numericTypes.some(t => type.toUpperCase().includes(t));
164
+ }
165
+
166
+ // Determine if a DuckDB type is text
167
+ function isTextType(type) {
168
+ const textTypes = ['VARCHAR', 'CHAR', 'TEXT', 'STRING'];
169
+ return textTypes.some(t => type.toUpperCase().includes(t));
170
+ }
171
+
172
+ // Detect columns and their types from the dataset
173
+ async function detectColumns(url) {
174
+ try {
175
+ setStatus('Detecting column types...', 'info');
176
+
177
+ // Initialize DuckDB if not already done
178
+ if (!db) {
179
+ await initDuckDB();
180
+ }
181
+
182
+ // Register the parquet file
183
+ try {
184
+ await db.registerFileURL(
185
+ 'data.parquet',
186
+ url,
187
+ duckdb.DuckDBDataProtocol.HTTP,
188
+ false
189
+ );
190
+ } catch {}
191
+
192
+ // Query to get column information
193
+ const result = await conn.query("DESCRIBE 'data.parquet'");
194
+ const rows = result.toArray();
195
+
196
+ columnInfo = rows.map(row => ({
197
+ name: row.column_name,
198
+ type: row.column_type
199
+ }));
200
+
201
+ setStatus(`Detected ${columnInfo.length} columns`, 'success');
202
+ buildVisualizationDropdown();
203
+
204
+ } catch (error) {
205
+ console.error('Error detecting columns:', error);
206
+ setStatus(`Error detecting columns: ${error.message}`, 'error');
207
+ columnInfo = [];
208
+ }
209
+ }
210
+
211
+ // Build the visualization dropdown with valid combinations
212
+ function buildVisualizationDropdown() {
213
+ const vizSelect = document.getElementById('vizSelect');
214
+ const vizSection = document.getElementById('visualizationSection');
215
+
216
+ // Clear existing options except first
217
+ vizSelect.innerHTML = '<option value="">-- Choose columns to visualize --</option>';
218
+
219
+ const validCombinations = [];
220
+
221
+ // Generate all valid column combinations
222
+ for (let i = 0; i < columnInfo.length; i++) {
223
+ const xCol = columnInfo[i];
224
+ const xIsNumeric = isNumericType(xCol.type);
225
+ const xIsText = isTextType(xCol.type);
226
+
227
+ for (let j = 0; j < columnInfo.length; j++) {
228
+ if (i === j) continue; // Skip same column
229
+
230
+ const yCol = columnInfo[j];
231
+ const yIsNumeric = isNumericType(yCol.type);
232
+ const yIsText = isTextType(yCol.type);
233
+
234
+ // Valid combinations:
235
+ // 1. Numeric x Numeric (scatter plot)
236
+ // 2. Numeric x Text (bar chart with count)
237
+ // 3. Text x Numeric (bar chart with count)
238
+ // Skip: Text x Text
239
+
240
+ if ((xIsNumeric && yIsNumeric) ||
241
+ (xIsNumeric && yIsText) ||
242
+ (xIsText && yIsNumeric)) {
243
+
244
+ let chartType;
245
+ if (xIsNumeric && yIsNumeric) {
246
+ chartType = 'scatter';
247
+ } else if (xIsNumeric && yIsText) {
248
+ chartType = 'bar';
249
+ } else if (xIsText && yIsNumeric) {
250
+ chartType = 'bar';
251
+ }
252
+
253
+ validCombinations.push({
254
+ xCol: xCol.name,
255
+ xType: xCol.type,
256
+ yCol: yCol.name,
257
+ yType: yCol.type,
258
+ chartType: chartType
259
+ });
260
+
261
+ const option = document.createElement('option');
262
+ option.value = JSON.stringify({ xCol: xCol.name, yCol: yCol.name, chartType });
263
+ option.textContent = `${xCol.name} (${xCol.type}) × ${yCol.name} (${yCol.type}) - ${chartType}`;
264
+ vizSelect.appendChild(option);
265
+ } else if (!xIsNumeric && !xIsText && !yIsNumeric && !yIsText) {
266
+ // Other unsupported type combinations - we'll handle error when selected
267
+ validCombinations.push({
268
+ xCol: xCol.name,
269
+ xType: xCol.type,
270
+ yCol: yCol.name,
271
+ yType: yCol.type,
272
+ chartType: 'unsupported'
273
+ });
274
+
275
+ const option = document.createElement('option');
276
+ option.value = JSON.stringify({ xCol: xCol.name, yCol: yCol.name, chartType: 'unsupported' });
277
+ option.textContent = `${xCol.name} (${xCol.type}) × ${yCol.name} (${yCol.type}) - unsupported`;
278
+ vizSelect.appendChild(option);
279
+ }
280
+ }
281
+ }
282
+
283
+ if (validCombinations.length > 0) {
284
+ vizSection.style.display = 'block';
285
+ } else {
286
+ vizSection.style.display = 'none';
287
+ setStatus('No valid column combinations found for visualization', 'error');
288
+ }
289
+ }
290
+
291
+ // Render visualization using Vega-Lite
292
+ async function renderVisualization(xCol, yCol, chartType) {
293
+ const vizContainer = document.getElementById('vizContainer');
294
+ vizContainer.innerHTML = ''; // Clear previous
295
+
296
+ if (chartType === 'unsupported') {
297
+ setStatus(`Error: Unsupported column type combination. X column type: ${columnInfo.find(c => c.name === xCol)?.type}, Y column type: ${columnInfo.find(c => c.name === yCol)?.type}`, 'error');
298
+ return;
299
+ }
300
+
301
+ try {
302
+ setStatus('Fetching data for visualization...', 'info');
303
+
304
+ // Fetch data (limit to reasonable amount for visualization)
305
+ const query = `SELECT "${xCol}", "${yCol}" FROM 'data.parquet' LIMIT 1000`;
306
+ const result = await conn.query(query);
307
+ const data = result.toArray();
308
+
309
+ setStatus('Rendering visualization...', 'info');
310
+
311
+ let spec;
312
+
313
+ if (chartType === 'scatter') {
314
+ // Numeric x Numeric: Scatter plot
315
+ spec = {
316
+ $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
317
+ description: `Scatter plot of ${xCol} vs ${yCol}`,
318
+ data: { values: data },
319
+ mark: 'point',
320
+ encoding: {
321
+ x: { field: xCol, type: 'quantitative' },
322
+ y: { field: yCol, type: 'quantitative' }
323
+ },
324
+ width: 600,
325
+ height: 400
326
+ };
327
+ } else if (chartType === 'bar') {
328
+ // One numeric, one text: Bar chart with count aggregation
329
+ const xColInfo = columnInfo.find(c => c.name === xCol);
330
+ const yColInfo = columnInfo.find(c => c.name === yCol);
331
+
332
+ const xIsNumeric = isNumericType(xColInfo.type);
333
+ const yIsNumeric = isNumericType(yColInfo.type);
334
+
335
+ if (xIsNumeric && !yIsNumeric) {
336
+ // X is numeric, Y is text: count text on Y, aggregate on X
337
+ spec = {
338
+ $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
339
+ description: `Bar chart of ${yCol} counts`,
340
+ data: { values: data },
341
+ mark: 'bar',
342
+ encoding: {
343
+ y: { field: yCol, type: 'nominal' },
344
+ x: { aggregate: 'count', type: 'quantitative' }
345
+ },
346
+ width: 600,
347
+ height: 400
348
+ };
349
+ } else {
350
+ // X is text, Y is numeric: count text on X, aggregate on Y
351
+ spec = {
352
+ $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
353
+ description: `Bar chart of ${xCol} counts`,
354
+ data: { values: data },
355
+ mark: 'bar',
356
+ encoding: {
357
+ x: { field: xCol, type: 'nominal' },
358
+ y: { aggregate: 'count', type: 'quantitative' }
359
+ },
360
+ width: 600,
361
+ height: 400
362
+ };
363
+ }
364
+ }
365
+
366
+ // Render using vega-embed
367
+ await vegaEmbed('#vizContainer', spec);
368
+ setStatus('Visualization rendered successfully!', 'success');
369
+
370
+ } catch (error) {
371
+ console.error('Error rendering visualization:', error);
372
+ setStatus(`Error rendering visualization: ${error.message}`, 'error');
373
+ }
374
+ }
375
+
376
  // Handle form submission
377
  async function handleSubmit(e) {
378
  e.preventDefault();
 
430
  }
431
 
432
  // Handle dropdown selection
433
+ document.getElementById('urlSelect').addEventListener('change', async function(e) {
434
  const selectedUrl = e.target.value;
435
  if (selectedUrl) {
436
  document.getElementById('parquetUrl').value = selectedUrl;
437
+ currentDatasetUrl = selectedUrl;
438
+ await detectColumns(selectedUrl);
439
+ }
440
+ });
441
+
442
+ // Handle manual URL input (detect when user blurs or presses enter)
443
+ document.getElementById('parquetUrl').addEventListener('blur', async function(e) {
444
+ const url = e.target.value.trim();
445
+ if (url && url !== currentDatasetUrl) {
446
+ currentDatasetUrl = url;
447
+ await detectColumns(url);
448
+ }
449
+ });
450
+
451
+ // Handle visualization selection
452
+ document.getElementById('vizSelect').addEventListener('change', function(e) {
453
+ const renderBtn = document.getElementById('renderVizBtn');
454
+ renderBtn.disabled = !e.target.value;
455
+ });
456
+
457
+ // Handle render visualization button
458
+ document.getElementById('renderVizBtn').addEventListener('click', async function() {
459
+ const vizSelect = document.getElementById('vizSelect');
460
+ const selectedValue = vizSelect.value;
461
+
462
+ if (selectedValue) {
463
+ const { xCol, yCol, chartType } = JSON.parse(selectedValue);
464
+ await renderVisualization(xCol, yCol, chartType);
465
  }
466
  });
467
 
style.css CHANGED
@@ -85,7 +85,8 @@ textarea {
85
  min-height: 100px;
86
  }
87
 
88
- button[type="submit"] {
 
89
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
90
  color: white;
91
  border: none;
@@ -98,16 +99,19 @@ button[type="submit"] {
98
  width: 100%;
99
  }
100
 
101
- button[type="submit"]:hover:not(:disabled) {
 
102
  transform: translateY(-2px);
103
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
104
  }
105
 
106
- button[type="submit"]:active:not(:disabled) {
 
107
  transform: translateY(0);
108
  }
109
 
110
- button[type="submit"]:disabled {
 
111
  opacity: 0.6;
112
  cursor: not-allowed;
113
  }
@@ -138,6 +142,29 @@ button[type="submit"]:disabled {
138
  border-left: 4px solid #f56565;
139
  }
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  .results-container {
142
  margin-top: 2rem;
143
  padding-top: 2rem;
@@ -211,7 +238,8 @@ tr:hover {
211
  font-size: 1.5rem;
212
  }
213
 
214
- button[type="submit"] {
 
215
  padding: 0.75rem 1.5rem;
216
  }
217
 
 
85
  min-height: 100px;
86
  }
87
 
88
+ button[type="submit"],
89
+ button[type="button"] {
90
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
91
  color: white;
92
  border: none;
 
99
  width: 100%;
100
  }
101
 
102
+ button[type="submit"]:hover:not(:disabled),
103
+ button[type="button"]:hover:not(:disabled) {
104
  transform: translateY(-2px);
105
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
106
  }
107
 
108
+ button[type="submit"]:active:not(:disabled),
109
+ button[type="button"]:active:not(:disabled) {
110
  transform: translateY(0);
111
  }
112
 
113
+ button[type="submit"]:disabled,
114
+ button[type="button"]:disabled {
115
  opacity: 0.6;
116
  cursor: not-allowed;
117
  }
 
142
  border-left: 4px solid #f56565;
143
  }
144
 
145
+ .visualization-section {
146
+ margin-top: 2rem;
147
+ padding-top: 2rem;
148
+ border-top: 2px solid #e2e8f0;
149
+ }
150
+
151
+ .viz-container {
152
+ margin-top: 1.5rem;
153
+ padding: 1rem;
154
+ background: #f7fafc;
155
+ border-radius: 8px;
156
+ min-height: 200px;
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: center;
160
+ }
161
+
162
+ .viz-container:empty::after {
163
+ content: 'Visualization will appear here';
164
+ color: #a0aec0;
165
+ font-style: italic;
166
+ }
167
+
168
  .results-container {
169
  margin-top: 2rem;
170
  padding-top: 2rem;
 
238
  font-size: 1.5rem;
239
  }
240
 
241
+ button[type="submit"],
242
+ button[type="button"] {
243
  padding: 0.75rem 1.5rem;
244
  }
245