znation HF Staff commited on
Commit
87a673a
·
1 Parent(s): 349dbe4

feature: ai prompt to create vis

Browse files

Now let's take the project in a different direction. Instead of having a dropdown of choices to create 2D plots, let's give the user a text box in which to enter a prompt for
an LLM. From the user's perspective, they can ask for any visualization they want on top of the Parquet file. The LLM is given a system prompt telling it to output a Vega-Lite
spec implementing the user's prompt. We can then display the resulting Vega-Lite. In order to implement the LLM, use Hugging Face Inference Providers. Follow the example at
https://huggingface.co/docs/inference-providers/en/index?javascript-clients=fetch#javascript.

Files changed (1) hide show
  1. index.html +116 -196
index.html CHANGED
@@ -51,19 +51,25 @@
51
  <div id="status" class="status"></div>
52
 
53
  <div id="visualizationSection" class="visualization-section" style="display: none;">
54
- <h2>Visualization</h2>
55
  <div class="form-group">
56
- <label for="xColSelect">X Axis</label>
57
- <select id="xColSelect">
58
- <option value="">-- Select X column --</option>
59
- </select>
 
 
 
60
  </div>
61
  <div class="form-group">
62
- <label for="yColSelect">Y Axis</label>
63
- <select id="yColSelect">
64
- <option value="">-- Select Y column --</option>
65
- </select>
 
 
66
  </div>
 
67
  <div id="vizContainer" class="viz-container"></div>
68
  </div>
69
 
@@ -164,7 +170,7 @@
164
  }));
165
 
166
  setStatus(`Detected ${columnInfo.length} columns`, 'success');
167
- buildVisualizationDropdown();
168
 
169
  } catch (error) {
170
  console.error('Error detecting columns:', error);
@@ -173,127 +179,14 @@
173
  }
174
  }
175
 
176
- // Populate X and Y axis dropdowns with columns
177
- function buildVisualizationDropdown() {
178
- const xColSelect = document.getElementById('xColSelect');
179
- const yColSelect = document.getElementById('yColSelect');
180
  const vizSection = document.getElementById('visualizationSection');
181
-
182
- // Clear existing options except first
183
- xColSelect.innerHTML = '<option value="">-- Select X column --</option>';
184
- yColSelect.innerHTML = '<option value="">-- Select Y column --</option>';
185
-
186
- // Populate both dropdowns with all visualizable columns (numeric or text)
187
- columnInfo.forEach(col => {
188
- const isNumeric = isNumericType(col.type);
189
- const isText = isTextType(col.type);
190
-
191
- if (isNumeric || isText) {
192
- const xOption = document.createElement('option');
193
- xOption.value = col.name;
194
- xOption.textContent = `${col.name} (${col.type})`;
195
- xColSelect.appendChild(xOption);
196
-
197
- const yOption = document.createElement('option');
198
- yOption.value = col.name;
199
- yOption.textContent = `${col.name} (${col.type})`;
200
- yColSelect.appendChild(yOption);
201
- }
202
- });
203
-
204
- // Find first valid pair and pre-select it
205
- let firstValidPair = null;
206
- for (let i = 0; i < columnInfo.length && !firstValidPair; i++) {
207
- const xCol = columnInfo[i];
208
- const xIsNumeric = isNumericType(xCol.type);
209
- const xIsText = isTextType(xCol.type);
210
-
211
- if (!xIsNumeric && !xIsText) continue;
212
-
213
- for (let j = 0; j < columnInfo.length; j++) {
214
- if (i === j) continue; // Skip same column
215
-
216
- const yCol = columnInfo[j];
217
- const yIsNumeric = isNumericType(yCol.type);
218
- const yIsText = isTextType(yCol.type);
219
-
220
- // Valid combinations: at least one must be numeric
221
- if ((xIsNumeric && yIsNumeric) ||
222
- (xIsNumeric && yIsText) ||
223
- (xIsText && yIsNumeric)) {
224
- firstValidPair = { x: xCol.name, y: yCol.name };
225
- break;
226
- }
227
- }
228
- }
229
-
230
- if (firstValidPair) {
231
- // Pre-select the first valid pair
232
- xColSelect.value = firstValidPair.x;
233
- yColSelect.value = firstValidPair.y;
234
- vizSection.style.display = 'block';
235
-
236
- // Render the visualization immediately
237
- renderVisualization(firstValidPair.x, firstValidPair.y, 'scatter');
238
- } else if (columnInfo.length > 0) {
239
  vizSection.style.display = 'block';
240
  } else {
241
  vizSection.style.display = 'none';
242
- setStatus('No valid column combinations found for visualization', 'error');
243
- }
244
- }
245
-
246
- // Render visualization using Vega-Lite
247
- async function renderVisualization(xCol, yCol, chartType) {
248
- const vizContainer = document.getElementById('vizContainer');
249
- vizContainer.innerHTML = ''; // Clear previous
250
-
251
- if (chartType === 'unsupported') {
252
- 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');
253
- return;
254
- }
255
-
256
- try {
257
- setStatus('Fetching data for visualization...', 'info');
258
-
259
- // Fetch data (limit to reasonable amount for visualization)
260
- const query = `SELECT "${xCol}", "${yCol}" FROM 'data.parquet' LIMIT 1000`;
261
- const result = await conn.query(query);
262
- const data = result.toArray();
263
-
264
- setStatus('Rendering visualization...', 'info');
265
-
266
- let spec;
267
-
268
- if (chartType === 'scatter') {
269
- // Scatter plot: determine type for each axis based on column type
270
- const xColInfo = columnInfo.find(c => c.name === xCol);
271
- const yColInfo = columnInfo.find(c => c.name === yCol);
272
-
273
- const xType = isNumericType(xColInfo.type) ? 'quantitative' : 'nominal';
274
- const yType = isNumericType(yColInfo.type) ? 'quantitative' : 'nominal';
275
-
276
- spec = {
277
- $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
278
- description: `Scatter plot of ${xCol} vs ${yCol}`,
279
- data: { values: data },
280
- mark: 'point',
281
- encoding: {
282
- x: { field: xCol, type: xType },
283
- y: { field: yCol, type: yType }
284
- },
285
- width: 600,
286
- height: 400
287
- };
288
- }
289
-
290
- // Render using vega-embed
291
- await vegaEmbed('#vizContainer', spec);
292
- setStatus('Visualization rendered successfully!', 'success');
293
-
294
- } catch (error) {
295
- console.error('Error rendering visualization:', error);
296
- setStatus(`Error rendering visualization: ${error.message}`, 'error');
297
  }
298
  }
299
 
@@ -345,90 +238,117 @@
345
  }
346
  });
347
 
348
- // Check if selected columns form a valid combination and render
349
- function isValidCombination(xCol, yCol) {
350
- if (!xCol || !yCol || xCol === yCol) return false;
351
-
352
- const xColInfo = columnInfo.find(c => c.name === xCol);
353
- const yColInfo = columnInfo.find(c => c.name === yCol);
354
-
355
- if (!xColInfo || !yColInfo) return false;
356
 
357
- const xIsNumeric = isNumericType(xColInfo.type);
358
- const xIsText = isTextType(xColInfo.type);
359
- const yIsNumeric = isNumericType(yColInfo.type);
360
- const yIsText = isTextType(yColInfo.type);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
 
362
- // At least one must be numeric, and both must be numeric or text
363
- return ((xIsNumeric || xIsText) && (yIsNumeric || yIsText) &&
364
- (xIsNumeric || yIsNumeric));
365
- }
366
 
367
- // Get error message for invalid column combination
368
- function getInvalidCombinationError(xCol, yCol) {
369
- if (!xCol || !yCol) {
370
- return 'Please select both X and Y columns';
371
- }
372
 
373
- if (xCol === yCol) {
374
- return 'Cannot visualize a column against itself. Please select different columns for X and Y axes';
375
- }
 
 
 
 
 
 
 
 
 
376
 
377
- const xColInfo = columnInfo.find(c => c.name === xCol);
378
- const yColInfo = columnInfo.find(c => c.name === yCol);
 
 
 
379
 
380
- if (!xColInfo || !yColInfo) {
381
- return 'Selected column not found';
382
- }
383
 
384
- const xIsNumeric = isNumericType(xColInfo.type);
385
- const xIsText = isTextType(xColInfo.type);
386
- const yIsNumeric = isNumericType(yColInfo.type);
387
- const yIsText = isTextType(yColInfo.type);
388
 
389
- if (xIsText && yIsText) {
390
- return `Cannot visualize text × text. At least one axis must be numeric. X: ${xColInfo.type}, Y: ${yColInfo.type}`;
 
391
  }
 
392
 
393
- if (!xIsNumeric && !xIsText) {
394
- return `X column type "${xColInfo.type}" is not supported. Please select a numeric or text column`;
395
- }
 
396
 
397
- if (!yIsNumeric && !yIsText) {
398
- return `Y column type "${yColInfo.type}" is not supported. Please select a numeric or text column`;
 
399
  }
400
 
401
- return 'Invalid column combination';
402
- }
403
-
404
- // Handle X axis selection
405
- document.getElementById('xColSelect').addEventListener('change', async function(e) {
406
- const xCol = e.target.value;
407
- const yCol = document.getElementById('yColSelect').value;
408
-
409
- if (isValidCombination(xCol, yCol)) {
410
- await renderVisualization(xCol, yCol, 'scatter');
411
- } else if (xCol || yCol) {
412
- // Show error and clear visualization
413
- const errorMsg = getInvalidCombinationError(xCol, yCol);
414
- setStatus(errorMsg, 'error');
415
- document.getElementById('vizContainer').innerHTML = '';
416
  }
417
- });
418
 
419
- // Handle Y axis selection
420
- document.getElementById('yColSelect').addEventListener('change', async function(e) {
421
- const yCol = e.target.value;
422
- const xCol = document.getElementById('xColSelect').value;
423
-
424
- if (isValidCombination(xCol, yCol)) {
425
- await renderVisualization(xCol, yCol, 'scatter');
426
- } else if (xCol || yCol) {
427
- // Show error and clear visualization
428
- const errorMsg = getInvalidCombinationError(xCol, yCol);
429
- setStatus(errorMsg, 'error');
430
- document.getElementById('vizContainer').innerHTML = '';
431
- }
432
  });
433
 
434
  // Set up event listeners
 
51
  <div id="status" class="status"></div>
52
 
53
  <div id="visualizationSection" class="visualization-section" style="display: none;">
54
+ <h2>Create Visualization</h2>
55
  <div class="form-group">
56
+ <label for="hfToken">Hugging Face Token (required for LLM)</label>
57
+ <input
58
+ type="password"
59
+ id="hfToken"
60
+ placeholder="Enter your HF token with Inference Providers permission"
61
+ />
62
+ <small>Get a token from <a href="https://huggingface.co/settings/tokens" target="_blank">HF Settings</a> with "Make calls to Inference Providers" permission</small>
63
  </div>
64
  <div class="form-group">
65
+ <label for="vizPrompt">Describe the visualization you want</label>
66
+ <textarea
67
+ id="vizPrompt"
68
+ rows="3"
69
+ placeholder="e.g., Show a scatter plot of price vs quantity, Create a bar chart showing count by category..."
70
+ ></textarea>
71
  </div>
72
+ <button type="button" id="generateVizBtn">Generate Visualization</button>
73
  <div id="vizContainer" class="viz-container"></div>
74
  </div>
75
 
 
170
  }));
171
 
172
  setStatus(`Detected ${columnInfo.length} columns`, 'success');
173
+ showVisualizationSection();
174
 
175
  } catch (error) {
176
  console.error('Error detecting columns:', error);
 
179
  }
180
  }
181
 
182
+ // Show visualization section after dataset is loaded
183
+ function showVisualizationSection() {
 
 
184
  const vizSection = document.getElementById('visualizationSection');
185
+ if (columnInfo.length > 0) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  vizSection.style.display = 'block';
187
  } else {
188
  vizSection.style.display = 'none';
189
+ setStatus('No columns found in dataset', 'error');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  }
191
  }
192
 
 
238
  }
239
  });
240
 
241
+ // Generate Vega-Lite spec using LLM
242
+ async function generateVisualization(prompt, hfToken) {
243
+ const vizContainer = document.getElementById('vizContainer');
244
+ vizContainer.innerHTML = '';
 
 
 
 
245
 
246
+ try {
247
+ setStatus('Generating visualization with LLM...', 'info');
248
+
249
+ // Prepare column information for the LLM
250
+ const columnDescriptions = columnInfo.map(col => `- ${col.name}: ${col.type}`).join('\n');
251
+
252
+ // Create system prompt
253
+ const systemPrompt = `You are a data visualization assistant that generates Vega-Lite specifications.
254
+
255
+ Available dataset columns:
256
+ ${columnDescriptions}
257
+
258
+ Instructions:
259
+ 1. Generate a valid Vega-Lite v5 specification based on the user's request
260
+ 2. Use ONLY columns that exist in the dataset above
261
+ 3. The data will be provided as an array of objects in the "data.values" field
262
+ 4. Output ONLY the JSON specification, no explanations or markdown
263
+ 5. Do not include the data itself, just reference fields by name
264
+ 6. Include appropriate width and height (e.g., 600x400)
265
+ 7. Make sure the spec is complete and valid
266
+
267
+ Output only the JSON spec starting with { and ending with }.`;
268
+
269
+ // Call HF Inference API
270
+ const response = await fetch(
271
+ "https://router.huggingface.co/v1/chat/completions",
272
+ {
273
+ method: "POST",
274
+ headers: {
275
+ Authorization: `Bearer ${hfToken}`,
276
+ "Content-Type": "application/json",
277
+ },
278
+ body: JSON.stringify({
279
+ model: "openai/gpt-4o-mini:fastest",
280
+ messages: [
281
+ {
282
+ role: "system",
283
+ content: systemPrompt
284
+ },
285
+ {
286
+ role: "user",
287
+ content: prompt
288
+ }
289
+ ],
290
+ temperature: 0.7,
291
+ max_tokens: 2000
292
+ }),
293
+ }
294
+ );
295
 
296
+ if (!response.ok) {
297
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
298
+ }
 
299
 
300
+ const data = await response.json();
301
+ const vegaSpec = data.choices[0].message.content;
 
 
 
302
 
303
+ // Parse and validate the Vega-Lite spec
304
+ let spec;
305
+ try {
306
+ // Try to extract JSON if wrapped in markdown code blocks
307
+ let jsonStr = vegaSpec.trim();
308
+ if (jsonStr.startsWith('```')) {
309
+ jsonStr = jsonStr.replace(/```json\n?/g, '').replace(/```\n?/g, '');
310
+ }
311
+ spec = JSON.parse(jsonStr);
312
+ } catch (e) {
313
+ throw new Error(`Failed to parse LLM response as JSON: ${e.message}`);
314
+ }
315
 
316
+ // Fetch data for the visualization
317
+ setStatus('Fetching data for visualization...', 'info');
318
+ const query = `SELECT * FROM 'data.parquet' LIMIT 1000`;
319
+ const result = await conn.query(query);
320
+ const dataArray = result.toArray();
321
 
322
+ // Inject data into the spec
323
+ spec.data = { values: dataArray };
 
324
 
325
+ // Render the visualization
326
+ setStatus('Rendering visualization...', 'info');
327
+ await vegaEmbed('#vizContainer', spec);
328
+ setStatus('Visualization generated successfully!', 'success');
329
 
330
+ } catch (error) {
331
+ console.error('Error generating visualization:', error);
332
+ setStatus(`Error: ${error.message}`, 'error');
333
  }
334
+ }
335
 
336
+ // Handle generate visualization button
337
+ document.getElementById('generateVizBtn').addEventListener('click', async function() {
338
+ const prompt = document.getElementById('vizPrompt').value.trim();
339
+ const hfToken = document.getElementById('hfToken').value.trim();
340
 
341
+ if (!prompt) {
342
+ setStatus('Please enter a visualization prompt', 'error');
343
+ return;
344
  }
345
 
346
+ if (!hfToken) {
347
+ setStatus('Please enter your Hugging Face token', 'error');
348
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
349
  }
 
350
 
351
+ await generateVisualization(prompt, hfToken);
 
 
 
 
 
 
 
 
 
 
 
 
352
  });
353
 
354
  // Set up event listeners